(core) Show count of formula errors in the column config in the right-side panel.

Summary:
- Cache the count by column, factoring out ColumnCache from
  ColumnACIndexes, which uses a similar pattern.
- Update error counts in response to column selection and to data changes.

Test Plan: Adds a test case for the new message

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D2780
This commit is contained in:
Dmitry S
2021-04-20 17:57:45 -04:00
parent 5479159960
commit 65a722501d
4 changed files with 123 additions and 32 deletions

View File

@@ -7,9 +7,10 @@ import {textInput} from 'app/client/ui2018/editableLabel';
import {cssIconButton, icon} from 'app/client/ui2018/icons';
import {menu, menuItem} from 'app/client/ui2018/menus';
import {sanitizeIdent} from 'app/common/gutil';
import {Computed, dom, fromKo, IDisposableOwner, Observable, styled} from 'grainjs';
import {Computed, dom, fromKo, MultiHolder, Observable, styled, subscribe} from 'grainjs';
import debounce = require('lodash/debounce');
export function buildNameConfig(owner: IDisposableOwner, origColumn: ColumnRec) {
export function buildNameConfig(owner: MultiHolder, origColumn: ColumnRec) {
const untieColId = origColumn.untieColIdFromLabel;
const editedLabel = Observable.create(owner, '');
@@ -50,10 +51,11 @@ export function buildNameConfig(owner: IDisposableOwner, origColumn: ColumnRec)
type BuildEditor = (cellElem: Element) => void;
export function buildFormulaConfig(
owner: IDisposableOwner, origColumn: ColumnRec, gristDoc: GristDoc, buildEditor: BuildEditor
owner: MultiHolder, origColumn: ColumnRec, gristDoc: GristDoc, buildEditor: BuildEditor
) {
const clearColumn = () => gristDoc.clearColumns([origColumn.id.peek()]);
const convertToData = () => gristDoc.convertFormulasToData([origColumn.id.peek()]);
const errorMessage = createFormulaErrorObs(owner, gristDoc, origColumn);
return dom.maybe(use => {
if (!use(origColumn.id)) { return null; } // Invalid column, show nothing.
@@ -73,7 +75,10 @@ export function buildFormulaConfig(
);
}
function buildFormulaRow(placeholder = 'Enter formula') {
return cssRow(dom.create(buildFormula, origColumn, buildEditor, placeholder));
return [
cssRow(dom.create(buildFormula, origColumn, buildEditor, placeholder)),
dom.maybe(errorMessage, errMsg => cssRow(cssError(errMsg), testId('field-error-count'))),
];
}
if (type === "empty") {
return [
@@ -104,7 +109,7 @@ export function buildFormulaConfig(
);
}
function buildFormula(owner: IDisposableOwner, column: ColumnRec, buildEditor: BuildEditor, placeholder: string) {
function buildFormula(owner: MultiHolder, column: ColumnRec, buildEditor: BuildEditor, placeholder: string) {
return cssFieldFormula(column.formula, {placeholder, maxLines: 2},
dom.cls('formula_field_sidepane'),
cssFieldFormula.cls('-disabled', column.disableModify),
@@ -115,6 +120,52 @@ function buildFormula(owner: IDisposableOwner, column: ColumnRec, buildEditor: B
);
}
/**
* Create and return an observable for the count of errors in a column, which gets updated in
* response to changes in origColumn and in user data.
*/
function createFormulaErrorObs(owner: MultiHolder, gristDoc: GristDoc, origColumn: ColumnRec) {
const errorMessage = Observable.create(owner, '');
// Count errors in origColumn when it's a formula column. Counts get cached by the
// tableData.countErrors() method, and invalidated on relevant data changes.
function countErrors() {
if (owner.isDisposed()) { return; }
const tableData = gristDoc.docData.getTable(origColumn.table.peek().tableId.peek());
if (tableData && origColumn.isRealFormula.peek()) {
const colId = origColumn.colId.peek();
const numCells = tableData.getColValues(colId)?.length || 0;
const numErrors = tableData.countErrors(colId) || 0;
errorMessage.set(
(numErrors === 0) ? '' :
(numCells === 1) ? `Error in the cell` :
(numErrors === numCells) ? `Errors in all ${numErrors} cells` :
`Errors in ${numErrors} of ${numCells} cells`
);
} else {
errorMessage.set('');
}
}
// Debounce the count calculation to defer it to the end of a bundle of actions.
const debouncedCountErrors = debounce(countErrors, 0);
// If there is an update to the data in the table, count errors again. Since the same UI is
// reused when different page widgets are selected, we need to re-create this subscription
// whenever the selected table changes. We use a Computed to both react to changes and dispose
// the previous subscription when it changes.
Computed.create(owner, (use) => {
const tableData = gristDoc.docData.getTable(use(use(origColumn.table).tableId));
return tableData ? use.owner.autoDispose(tableData.tableActionEmitter.addListener(debouncedCountErrors)) : null;
});
// The counts depend on the origColumn and its isRealFormula status, but with the debounced
// callback and subscription to data, subscribe to relevant changes manually (rather than using
// a Computed).
owner.autoDispose(subscribe(use => { use(origColumn.id); use(origColumn.isRealFormula); debouncedCountErrors(); }));
return errorMessage;
}
const cssFieldFormula = styled(buildHighlightedCode, `
flex: auto;
cursor: pointer;
@@ -195,3 +246,7 @@ const cssColTieConnectors = styled('div', `
border-left: none;
z-index: -1;
`);
const cssError = styled('div', `
color: ${colors.error};
`);