(core) JS error on creator panel and formula editor.

Summary:
Fixing js error that happens when closing creator panel with active formula editor.

Styling behavior menu with common styles.

Test Plan: Browser tests

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D3150
This commit is contained in:
Jarosław Sadziński 2021-11-30 09:59:04 +01:00
parent 551ea28fc4
commit e482427e83
6 changed files with 52 additions and 26 deletions

View File

@ -625,10 +625,19 @@ export class GristDoc extends DisposableWithEvents {
); );
} }
// Convert column to pure formula column. // Updates formula for a column.
public async convertToFormula(colRefs: number, formula: string): Promise<void> { public async updateFormula(colRef: number, formula: string): Promise<void> {
return this.docModel.columns.sendTableAction( return this.docModel.columns.sendTableAction(
['UpdateRecord', colRefs, { ['UpdateRecord', colRef, {
formula,
}]
);
}
// Convert column to pure formula column.
public async convertToFormula(colRef: number, formula: string): Promise<void> {
return this.docModel.columns.sendTableAction(
['UpdateRecord', colRef, {
isFormula: true, isFormula: true,
formula, formula,
recalcWhen: RecalcWhen.DEFAULT, recalcWhen: RecalcWhen.DEFAULT,

View File

@ -66,10 +66,11 @@ export function buildNameConfig(owner: MultiHolder, origColumn: ColumnRec, curso
]; ];
} }
type SaveHandler = (column: ColumnRec, formula: string) => Promise<void>;
type BuildEditor = ( type BuildEditor = (
cellElem: Element, cellElem: Element,
editValue?: string, editValue?: string,
onSave?: (formula: string) => Promise<void>, onSave?: SaveHandler,
onCancel?: () => void) => void; onCancel?: () => void) => void;
type BEHAVIOR = "empty"|"formula"|"data"; type BEHAVIOR = "empty"|"formula"|"data";
@ -111,6 +112,8 @@ export function buildFormulaConfig(
// Clear state when column has changed // Clear state when column has changed
owner.autoDispose(origColumn.id.subscribe(clearState)); owner.autoDispose(origColumn.id.subscribe(clearState));
owner.autoDispose(origColumn.formula.subscribe(clearState));
owner.autoDispose(origColumn.isFormula.subscribe(clearState));
// Menu helper that will show normal menu with some default options // Menu helper that will show normal menu with some default options
const menu = (label: DomContents, options: DomElementArg[]) => const menu = (label: DomContents, options: DomElementArg[]) =>
@ -177,31 +180,44 @@ export function buildFormulaConfig(
const setFormula = () => (maybeFormula.set(true), formulaField?.focus()); const setFormula = () => (maybeFormula.set(true), formulaField?.focus());
const setTrigger = () => (maybeTrigger.set(true), formulaField?.focus()); const setTrigger = () => (maybeTrigger.set(true), formulaField?.focus());
// Actions on save formula // Actions on save formula. Those actions are using column that comes from FormulaEditor.
// Formula editor scope is broader then RightPanel, it can be disposed after RightPanel is closed,
// and in some cases, when window is in background, it won't be disposed at all when panel is closed.
// Converts column to formula column or updates formula on a formula column. // Converts column to formula column.
const onSaveConvertToFormula = async (formula: string) => { const onSaveConvertToFormula = async (column: ColumnRec, formula: string) => {
// For non formula column, we will not convert it to formula column when expression is empty,
// as it means we were trying to convert data column to formula column, but changed our mind.
const notBlank = Boolean(formula); const notBlank = Boolean(formula);
const trueFormula = !maybeFormula.get(); // But when the column is a formula column, empty formula expression is acceptable (it will
if (notBlank || trueFormula) { await gristDoc.convertToFormula(origColumn.id.peek(), formula); } // convert column to empty column).
clearState(); const trueFormula = column.formula.peek();
if (notBlank || trueFormula) { await gristDoc.convertToFormula(column.id.peek(), formula); }
// Clear state only when owner was not disposed
if (!owner.isDisposed()) {
clearState();
}
}; };
// Updates formula or convert column to trigger formula column if necessary. // Updates formula or convert column to trigger formula column if necessary.
const onSaveConvertToTrigger = async (formula: string) => { const onSaveConvertToTrigger = async (column: ColumnRec, formula: string) => {
if (formula && maybeTrigger.get()) { // If formula expression is not empty, and column was plain data column (without a formula)
// Convert column to trigger if (formula && !column.hasTriggerFormula.peek()) {
await gristDoc.convertToTrigger(origColumn.id.peek(), formula); // then convert column to a trigger formula column
} else if (origColumn.hasTriggerFormula.peek()) { await gristDoc.convertToTrigger(column.id.peek(), formula);
// This is true trigger formula, just update the formula (or make it blank) } else if (column.hasTriggerFormula.peek()) {
await origColumn.formula.setAndSave(formula); // else, if it was already a trigger formula column, just update formula.
await gristDoc.updateFormula(column.id.peek(), formula);
}
// Clear state only when owner was not disposed
if (!owner.isDisposed()) {
clearState();
} }
clearState();
}; };
const errorMessage = createFormulaErrorObs(owner, gristDoc, origColumn); const errorMessage = createFormulaErrorObs(owner, gristDoc, origColumn);
// Helper that will create different flavors for formula builder. // Helper that will create different flavors for formula builder.
const formulaBuilder = (onSave: (formula: string) => Promise<void>) => [ const formulaBuilder = (onSave: SaveHandler) => [
cssRow(formulaField = buildFormula( cssRow(formulaField = buildFormula(
origColumn, origColumn,
buildEditor, buildEditor,
@ -286,7 +302,7 @@ function buildFormula(
column: ColumnRec, column: ColumnRec,
buildEditor: BuildEditor, buildEditor: BuildEditor,
placeholder: string, placeholder: string,
onSave?: (formula: string) => Promise<void>, onSave?: SaveHandler,
onCancel?: () => void) { onCancel?: () => void) {
return cssFieldFormula(column.formula, {placeholder, maxLines: 2}, return cssFieldFormula(column.formula, {placeholder, maxLines: 2},
dom.cls('formula_field_sidepane'), dom.cls('formula_field_sidepane'),

View File

@ -22,7 +22,7 @@ import {domAsync} from 'app/client/lib/domAsync';
import * as imports from 'app/client/lib/imports'; import * as imports from 'app/client/lib/imports';
import {createSessionObs} from 'app/client/lib/sessionObs'; import {createSessionObs} from 'app/client/lib/sessionObs';
import {reportError} from 'app/client/models/AppModel'; import {reportError} from 'app/client/models/AppModel';
import {ViewSectionRec} from 'app/client/models/DocModel'; import {ColumnRec, ViewSectionRec} from 'app/client/models/DocModel';
import {GridOptions} from 'app/client/ui/GridOptions'; import {GridOptions} from 'app/client/ui/GridOptions';
import {attachPageWidgetPicker, IPageWidget, toPageWidget} from 'app/client/ui/PageWidgetPicker'; import {attachPageWidgetPicker, IPageWidget, toPageWidget} from 'app/client/ui/PageWidgetPicker';
import {linkFromId, linkId, selectBy} from 'app/client/ui/selectBy'; import {linkFromId, linkId, selectBy} from 'app/client/ui/selectBy';
@ -236,7 +236,7 @@ export class RightPanel extends Disposable {
// Simulate user typing on the cell - open editor with an initial value. // Simulate user typing on the cell - open editor with an initial value.
editValue?: string, editValue?: string,
// Custom save handler. // Custom save handler.
onSave?: (formula: string) => Promise<void>, onSave?: (column: ColumnRec, formula: string) => Promise<void>,
// Custom cancel handler. // Custom cancel handler.
onCancel?: () => void,) { onCancel?: () => void,) {
const vsi = this._gristDoc.viewModel.activeSection().viewInstance(); const vsi = this._gristDoc.viewModel.activeSection().viewInstance();

View File

@ -335,6 +335,7 @@ export function selectMenu(
items: () => DomElementArg[], items: () => DomElementArg[],
...args: IDomArgs<HTMLDivElement> ...args: IDomArgs<HTMLDivElement>
) { ) {
const _menu = cssSelectMenuElem(testId('select-menu'));
return cssSelectBtn( return cssSelectBtn(
label, label,
icon('Dropdown'), icon('Dropdown'),
@ -342,7 +343,7 @@ export function selectMenu(
items, items,
{ {
...weasel.defaultMenuOptions, ...weasel.defaultMenuOptions,
menuCssClass: cssSelectMenuElem.className + ' grist-floating-menu', menuCssClass: _menu.className + ' grist-floating-menu',
stretchToSelector : `.${cssSelectBtn.className}`, stretchToSelector : `.${cssSelectBtn.className}`,
trigger : [(triggerElem, ctl) => { trigger : [(triggerElem, ctl) => {
const isDisabled = () => triggerElem.classList.contains('disabled'); const isDisabled = () => triggerElem.classList.contains('disabled');

View File

@ -522,7 +522,7 @@ export class FieldBuilder extends Disposable {
editRow: DataRowModel, editRow: DataRowModel,
refElem: Element, refElem: Element,
editValue?: string, editValue?: string,
onSave?: (formula: string) => Promise<void>, onSave?: (column: ColumnRec, formula: string) => Promise<void>,
onCancel?: () => void) { onCancel?: () => void) {
const editorHolder = openFormulaEditor({ const editorHolder = openFormulaEditor({
gristDoc: this.gristDoc, gristDoc: this.gristDoc,

View File

@ -383,7 +383,7 @@ export function openFormulaEditor(options: {
// Element over which to position the editor. // Element over which to position the editor.
refElem: Element, refElem: Element,
editValue?: string, editValue?: string,
onSave?: (formula: string) => Promise<void>, onSave?: (column: ColumnRec, formula: string) => Promise<void>,
onCancel?: () => void, onCancel?: () => void,
// Called after editor is created to set up editor cleanup (e.g. saving on click-away). // Called after editor is created to set up editor cleanup (e.g. saving on click-away).
setupCleanup: ( setupCleanup: (
@ -401,7 +401,7 @@ export function openFormulaEditor(options: {
const saveEdit = asyncOnce(async () => { const saveEdit = asyncOnce(async () => {
const formula = editor.getCellValue(); const formula = editor.getCellValue();
if (options.onSave) { if (options.onSave) {
await options.onSave(formula as string); await options.onSave(column, formula as string);
} else if (formula !== column.formula.peek()) { } else if (formula !== column.formula.peek()) {
await column.updateColValues({formula}); await column.updateColValues({formula});
} }