mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Update UI for formula and column label/id in the right-side panel.
Summary: - Update styling of label, id, and "derived ID from label" checkbox. - Implement a label which shows 'Data Column' vs 'Formula Column' vs 'Empty Column', and a dropdown with column actions (such as Clear/Convert) - Implement new formula display in the side-panel, and open the standard FormulaEditor when clicked. - Remove old FieldConfigTab, of which now very little would be used. - Fix up remaining code that relied on it (RefSelect) Test Plan: Fixed old tests, added new browser cases, and a case for a new helper function. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D2757
This commit is contained in:
@@ -19,7 +19,7 @@ import { buttonSelect } from 'app/client/ui2018/buttonSelect';
|
||||
import { IOptionFull, menu, select } from 'app/client/ui2018/menus';
|
||||
import { DiffBox } from 'app/client/widgets/DiffBox';
|
||||
import { buildErrorDom } from 'app/client/widgets/ErrorDom';
|
||||
import { FieldEditor, saveWithoutEditor } from 'app/client/widgets/FieldEditor';
|
||||
import { FieldEditor, openSideFormulaEditor, saveWithoutEditor } from 'app/client/widgets/FieldEditor';
|
||||
import { NewAbstractWidget } from 'app/client/widgets/NewAbstractWidget';
|
||||
import * as UserType from 'app/client/widgets/UserType';
|
||||
import * as UserTypeImpl from 'app/client/widgets/UserTypeImpl';
|
||||
@@ -213,7 +213,7 @@ export class FieldBuilder extends Disposable {
|
||||
cssRow(
|
||||
grainjsDom.autoDispose(selectType),
|
||||
select(selectType, this.availableTypes, {
|
||||
disabled: (use) => use(this.isTransformingFormula) || use(this.origColumn.disableModify) ||
|
||||
disabled: (use) => use(this.isTransformingFormula) || use(this.origColumn.disableModifyBase) ||
|
||||
use(this.isCallPending)
|
||||
}),
|
||||
testId('type-select')
|
||||
@@ -301,7 +301,7 @@ export class FieldBuilder extends Disposable {
|
||||
dom('span.glyphicon.glyphicon-flash'),
|
||||
dom.testId("FieldBuilder_editTransform"),
|
||||
kd.toggleClass('disabled', () => this.isTransformingType() || this.origColumn.isFormula() ||
|
||||
this.origColumn.disableModify())
|
||||
this.origColumn.disableModifyBase())
|
||||
)
|
||||
)
|
||||
),
|
||||
@@ -450,7 +450,6 @@ export class FieldBuilder extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public buildEditorDom(editRow: DataRowModel, mainRowModel: DataRowModel, options: {
|
||||
init?: string
|
||||
}) {
|
||||
@@ -495,8 +494,20 @@ export class FieldBuilder extends Disposable {
|
||||
this.gristDoc.fieldEditorHolder.autoDispose(fieldEditor);
|
||||
}
|
||||
|
||||
|
||||
public isEditorActive() {
|
||||
return !this._fieldEditorHolder.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the formula editor in the side pane. It will be positioned over refElem.
|
||||
*/
|
||||
public openSideFormulaEditor(editRow: DataRowModel, refElem: Element) {
|
||||
const editorHolder = openSideFormulaEditor({
|
||||
gristDoc: this.gristDoc,
|
||||
field: this.field,
|
||||
editRow,
|
||||
refElem,
|
||||
});
|
||||
this.gristDoc.fieldEditorHolder.autoDispose(editorHolder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,17 @@ import {Cursor} from 'app/client/components/Cursor';
|
||||
import {GristDoc} from 'app/client/components/GristDoc';
|
||||
import {UnsavedChange} from 'app/client/components/UnsavedChanges';
|
||||
import {DataRowModel} from 'app/client/models/DataRowModel';
|
||||
import {ColumnRec} from 'app/client/models/entities/ColumnRec';
|
||||
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
|
||||
import {reportError} from 'app/client/models/errors';
|
||||
import {showTooltipToCreateFormula} from 'app/client/widgets/EditorTooltip';
|
||||
import {FormulaEditor} from 'app/client/widgets/FormulaEditor';
|
||||
import {IEditorCommandGroup, NewBaseEditor} from 'app/client/widgets/NewBaseEditor';
|
||||
import {asyncOnce} from "app/common/AsyncCreate";
|
||||
import {CellValue} from "app/common/DocActions";
|
||||
import {isRaisedException} from 'app/common/gristTypes';
|
||||
import * as gutil from 'app/common/gutil';
|
||||
import {Disposable, Holder, Observable} from 'grainjs';
|
||||
import {Disposable, Holder, IDisposable, MultiHolder, Observable} from 'grainjs';
|
||||
import isEqual = require('lodash/isEqual');
|
||||
|
||||
type IEditorConstructor = typeof NewBaseEditor;
|
||||
@@ -53,7 +55,7 @@ export class FieldEditor extends Disposable {
|
||||
private _editCommands: IEditorCommandGroup;
|
||||
private _editorCtor: IEditorConstructor;
|
||||
private _editorHolder: Holder<NewBaseEditor> = Holder.create(this);
|
||||
private _saveEditPromise: Promise<boolean>|null = null;
|
||||
private _saveEdit = asyncOnce(() => this._doSaveEdit());
|
||||
|
||||
constructor(options: {
|
||||
gristDoc: GristDoc,
|
||||
@@ -116,18 +118,7 @@ export class FieldEditor extends Disposable {
|
||||
this._offerToMakeFormula();
|
||||
}
|
||||
|
||||
// Whenever focus returns to the Clipboard component, close the editor by saving the value.
|
||||
this._gristDoc.app.on('clipboard_focus', this._saveEdit, this);
|
||||
|
||||
// TODO: This should ideally include a callback that returns true only when the editor value
|
||||
// has changed. Currently an open editor is considered unsaved even when unchanged.
|
||||
UnsavedChange.create(this, async () => { await this._saveEdit(); });
|
||||
|
||||
this.onDispose(() => {
|
||||
this._gristDoc.app.off('clipboard_focus', this._saveEdit, this);
|
||||
// Unset field.editingFormula flag when the editor closes.
|
||||
this._field.editingFormula(false);
|
||||
});
|
||||
setupEditorCleanup(this, this._gristDoc, this._field, this._saveEdit);
|
||||
}
|
||||
|
||||
// cursorPos refers to the position of the caret within the editor.
|
||||
@@ -143,23 +134,12 @@ export class FieldEditor extends Disposable {
|
||||
// we defer this mode until the user types something.
|
||||
this._field.editingFormula(isFormula && editValue !== undefined);
|
||||
|
||||
let formulaError: Observable<CellValue>|undefined;
|
||||
if (column.isFormula() && isRaisedException(cellCurrentValue)) {
|
||||
const fv = formulaError = Observable.create(null, cellCurrentValue);
|
||||
this._gristDoc.docData.getFormulaError(column.table().tableId(),
|
||||
this._field.colId(),
|
||||
this._editRow.getRowId()
|
||||
)
|
||||
.then(value => { fv.set(value); })
|
||||
.catch(reportError);
|
||||
}
|
||||
|
||||
// Replace the item in the Holder with a new one, disposing the previous one.
|
||||
const editor = this._editorHolder.autoDispose(editorCtor.create({
|
||||
gristDoc: this._gristDoc,
|
||||
field: this._field,
|
||||
cellValue,
|
||||
formulaError,
|
||||
formulaError: getFormulaError(this._gristDoc, this._editRow, column),
|
||||
editValue,
|
||||
cursorPos,
|
||||
commands: this._editCommands,
|
||||
@@ -212,10 +192,6 @@ export class FieldEditor extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
private async _saveEdit() {
|
||||
return this._saveEditPromise || (this._saveEditPromise = this._doSaveEdit());
|
||||
}
|
||||
|
||||
// Returns true if Enter/Shift+Enter should NOT move the cursor, for instance if the current
|
||||
// record got reordered (i.e. the cursor jumped), or when editing a formula.
|
||||
private async _doSaveEdit(): Promise<boolean> {
|
||||
@@ -269,3 +245,94 @@ export class FieldEditor extends Disposable {
|
||||
return isFormula || (saveIndex !== cursor.rowIndex());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a formula editor in the side pane. Returns a Disposable that owns the editor.
|
||||
*/
|
||||
export function openSideFormulaEditor(options: {
|
||||
gristDoc: GristDoc,
|
||||
field: ViewFieldRec,
|
||||
editRow: DataRowModel, // Needed to get exception value, if any.
|
||||
refElem: Element, // Element in the side pane over which to position the editor.
|
||||
}): IDisposable {
|
||||
const {gristDoc, field, editRow, refElem} = options;
|
||||
const holder = MultiHolder.create(null);
|
||||
const column = field.column();
|
||||
|
||||
// AsyncOnce ensures it's called once even if triggered multiple times.
|
||||
const saveEdit = asyncOnce(async () => {
|
||||
const formula = editor.getCellValue();
|
||||
if (formula !== column.formula.peek()) {
|
||||
await column.updateColValues({formula});
|
||||
}
|
||||
holder.dispose(); // Deactivate the editor.
|
||||
});
|
||||
|
||||
// These are the commands for while the editor is active.
|
||||
const editCommands = {
|
||||
fieldEditSave: () => { saveEdit().catch(reportError); },
|
||||
fieldEditSaveHere: () => { saveEdit().catch(reportError); },
|
||||
fieldEditCancel: () => { holder.dispose(); },
|
||||
};
|
||||
|
||||
// Replace the item in the Holder with a new one, disposing the previous one.
|
||||
const editor = FormulaEditor.create(holder, {
|
||||
gristDoc,
|
||||
field,
|
||||
cellValue: column.formula(),
|
||||
formulaError: getFormulaError(gristDoc, editRow, column),
|
||||
editValue: undefined,
|
||||
cursorPos: Number.POSITIVE_INFINITY, // Position of the caret within the editor.
|
||||
commands: editCommands,
|
||||
cssClass: 'formula_editor_sidepane',
|
||||
});
|
||||
editor.attach(refElem);
|
||||
|
||||
// Enter formula-editing mode (highlight formula icons; click on a column inserts its ID).
|
||||
field.editingFormula(true);
|
||||
setupEditorCleanup(holder, gristDoc, field, saveEdit);
|
||||
return holder;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* For an active editor, set up its cleanup:
|
||||
* - saving on click-away (when focus returns to Grist "clipboard" element)
|
||||
* - unset field.editingFormula mode
|
||||
* - Arrange for UnsavedChange protection against leaving the page with unsaved changes.
|
||||
*/
|
||||
function setupEditorCleanup(
|
||||
owner: MultiHolder, gristDoc: GristDoc, field: ViewFieldRec, saveEdit: () => Promise<unknown>
|
||||
) {
|
||||
// Whenever focus returns to the Clipboard component, close the editor by saving the value.
|
||||
gristDoc.app.on('clipboard_focus', saveEdit);
|
||||
|
||||
// TODO: This should ideally include a callback that returns true only when the editor value
|
||||
// has changed. Currently an open editor is considered unsaved even when unchanged.
|
||||
UnsavedChange.create(owner, async () => { await saveEdit(); });
|
||||
|
||||
owner.onDispose(() => {
|
||||
gristDoc.app.off('clipboard_focus', saveEdit);
|
||||
// Unset field.editingFormula flag when the editor closes.
|
||||
field.editingFormula(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* If the cell at the given row and column is a formula value containing an exception, return an
|
||||
* observable with this exception, and fetch more details to add to the observable.
|
||||
*/
|
||||
function getFormulaError(
|
||||
gristDoc: GristDoc, editRow: DataRowModel, column: ColumnRec
|
||||
): Observable<CellValue>|undefined {
|
||||
const colId = column.colId.peek();
|
||||
let formulaError: Observable<CellValue>|undefined;
|
||||
const cellCurrentValue = editRow.cells[colId].peek();
|
||||
if (column.isFormula() && isRaisedException(cellCurrentValue)) {
|
||||
const fv = formulaError = Observable.create(null, cellCurrentValue);
|
||||
gristDoc.docData.getFormulaError(column.table().tableId(), colId, editRow.getRowId())
|
||||
.then(value => { fv.set(value); })
|
||||
.catch(reportError);
|
||||
}
|
||||
return formulaError;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,11 @@ import {dom, Observable, styled} from 'grainjs';
|
||||
// How wide to expand the FormulaEditor when an error is shown in it.
|
||||
const minFormulaErrorWidth = 400;
|
||||
|
||||
export interface IFormulaEditorOptions extends Options {
|
||||
cssClass?: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Required parameters:
|
||||
* @param {RowModel} options.field: ViewSectionField (i.e. column) being edited.
|
||||
@@ -30,7 +35,7 @@ export class FormulaEditor extends NewBaseEditor {
|
||||
private _dom: HTMLElement;
|
||||
private _editorPlacement: EditorPlacement;
|
||||
|
||||
constructor(options: Options) {
|
||||
constructor(options: IFormulaEditorOptions) {
|
||||
super(options);
|
||||
this._formulaEditor = AceEditor.create({
|
||||
// A bit awkward, but we need to assume calcSize is not used until attach() has been called
|
||||
@@ -49,6 +54,7 @@ export class FormulaEditor extends NewBaseEditor {
|
||||
this.autoDispose(this._formulaEditor);
|
||||
this._dom = dom('div.default_editor',
|
||||
createMobileButtons(options.commands),
|
||||
options.cssClass ? dom.cls(options.cssClass) : null,
|
||||
|
||||
// This shouldn't be needed, but needed for tests.
|
||||
dom.on('mousedown', (ev) => {
|
||||
|
||||
@@ -38,7 +38,7 @@ export abstract class NewBaseEditor extends Disposable {
|
||||
* Editors and provided by FieldBuilder. TODO: remove this method once all editors have been
|
||||
* updated to new-style Disposables.
|
||||
*/
|
||||
public static create(owner: IDisposableOwner|null, options: Options): NewBaseEditor;
|
||||
public static create<Opt extends Options>(owner: IDisposableOwner|null, options: Opt): NewBaseEditor;
|
||||
public static create(options: Options): NewBaseEditor;
|
||||
public static create(ownerOrOptions: any, options?: any): NewBaseEditor {
|
||||
return options ?
|
||||
|
||||
@@ -13,21 +13,22 @@
|
||||
color: #D0D0D0;
|
||||
}
|
||||
|
||||
.formula_field::before, .formula_field_edit::before {
|
||||
content: '=';
|
||||
.formula_field::before, .formula_field_edit::before, .formula_field_sidepane::before {
|
||||
/* based on standard icon styles */
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 4px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
line-height: 12px;
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
left: 2px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
-webkit-mask-position: center;
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: var(--icon-FunctionResult);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: var(--icon-color, black);
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.formula_field::before {
|
||||
|
||||
@@ -9,10 +9,22 @@
|
||||
|
||||
.formula_editor {
|
||||
background-color: white;
|
||||
padding: 2px 0 2px 18px;
|
||||
padding: 4px 0 2px 21px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* styles specific to the formula editor in the side panel */
|
||||
.default_editor.formula_editor_sidepane {
|
||||
border-radius: 3px;
|
||||
}
|
||||
.formula_editor_sidepane > .formula_editor {
|
||||
padding: 5px 0 5px 24px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.formula_editor_sidepane > .formula_field_edit::before, .formula_field_sidepane::before {
|
||||
left: 4px;
|
||||
}
|
||||
|
||||
.celleditor_cursor_editor {
|
||||
background-color: white;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user