(core) Floating formula editor

Summary:
Adding a way to detach an editor. Initially only implemented for the formula editor, includes redesign for the AI part.
- Initially, the detached editor is tight with the formula assistant and both are behind GRIST_FORMULA_ASSISTANT flag, but this can be relaxed
later on, as the detached editor can be used on its own.

- Detached editor is only supported in regular fields and on the creator panel. It is not supported yet for conditional styles, due to preview limitations.
- Old code for the assistant was removed completely, as it was only a temporary solution, but the AI conversation part was copied to the new one.
- Prompting was not modified in this diff, it will be included in the follow-up with more test cases.

Test Plan: Added only new tests; existing tests should pass.

Reviewers: JakubSerafin

Reviewed By: JakubSerafin

Differential Revision: https://phab.getgrist.com/D3863
This commit is contained in:
Jarosław Sadziński
2023-06-02 13:25:14 +02:00
parent e10067ff78
commit da323fb741
36 changed files with 2022 additions and 823 deletions

View File

@@ -8,18 +8,30 @@
*/
import * as Mousetrap from 'app/client/lib/Mousetrap';
import { arrayRemove } from 'app/common/gutil';
import {arrayRemove, unwrap} from 'app/common/gutil';
import dom from 'app/client/lib/dom';
import 'app/client/lib/koUtil'; // for subscribeInit
import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals';
import {CommandDef, CommandName, CommendGroupDef, groups} from 'app/client/components/commandList';
import {Disposable} from 'grainjs';
import {Disposable, Observable} from 'grainjs';
import * as _ from 'underscore';
import * as ko from 'knockout';
const G = getBrowserGlobals('window');
type BoolLike = boolean|ko.Observable<boolean>|ko.Computed<boolean>;
type BoolLike = boolean|ko.Observable<boolean>|ko.Computed<boolean>|Observable<boolean>;
/**
* A helper method that can create a subscription to ko or grains observables.
*/
function subscribe(value: Exclude<BoolLike, boolean>, fn: (value: boolean) => void) {
if (ko.isObservable(value)) {
return value.subscribe(fn);
} else if (value instanceof Observable) {
return value.addListener(fn);
} else {
throw new Error('Expected an observable');
}
}
// Same logic as used by mousetrap to map 'Mod' key to platform-specific key.
export const isMac = (typeof navigator !== 'undefined' && navigator &&
@@ -291,10 +303,11 @@ export class CommandGroup extends Disposable {
this.onDispose(this._removeGroup.bind(this));
// Finally, set the activation status of the command group, subscribing if an observable.
if (ko.isObservable(activate)) {
this.autoDispose((activate as any).subscribeInit(this.activate, this));
} else {
this.activate(activate as boolean);
if (typeof activate === 'boolean' || activate === undefined) {
this.activate(activate ?? false);
} else if (activate) {
this.autoDispose(subscribe(activate, (val) => this.activate(val)));
this.activate(unwrap(activate));
}
}
@@ -343,8 +356,8 @@ type BoundedMap<T> = { [key in CommandName]?: BoundedFunc<T> };
/**
* Just a shorthand for CommandGroup.create constructor.
*/
export function createGroup<T>(commands: BoundedMap<T>, context: T, activate?: BoolLike) {
return CommandGroup.create(null, commands, context, activate);
export function createGroup<T>(commands: BoundedMap<T>|null, context: T, activate?: BoolLike) {
return CommandGroup.create(null, commands ?? {}, context, activate);
}
//----------------------------------------------------------------------