(core) Change how formula columns can be converted to data.

Summary:
- No longer convert data columns to formula by typing a leading "=". Instead,
  show a tooltip with a link to click if the conversion was intended.
- No longer convert a formula column to data by deleting its formula. Leave the
  column empty instead.
- Offer the option "Convert formula to data" in column menu for formulas.
- Offer the option to "Clear column"
- If a subset of rows is shown, offer "Clear values" and "Clear entire column".

- Add logic to detect when a view shows a subset of all rows.
- Factor out showTooltip() from showTransientTooltip().

- Add a bunch of test cases to cover various combinations (there are small
  variations in options depending on whether all rows are shown, on whether
  multiple columns are selected, and whether columns include data columns).

Test Plan: Added a bunch of test cases.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D2746
This commit is contained in:
Dmitry S
2021-03-05 10:17:07 -05:00
parent 8a1e803316
commit 48e90c4998
16 changed files with 377 additions and 156 deletions

View File

@@ -642,4 +642,11 @@ BaseView.prototype.createFilterMenu = function(openCtl, field) {
return createFilterMenu(openCtl, this._sectionFilter, field, this._filteredRowSource, this.tableModel.tableData);
};
/**
* Whether the rows shown by this view are a proper subset of all rows in the table.
*/
BaseView.prototype.isFiltered = function() {
return this._filteredRowSource.getNumRows() < this.tableModel.tableData.numRecords();
};
module.exports = BaseView;

View File

@@ -28,6 +28,7 @@ const {onDblClickMatchElem} = require('app/client/lib/dblclick');
// Grist UI Components
const {Holder} = require('grainjs');
const {menu} = require('../ui2018/menus');
const {calcFieldsCondition} = require('../ui/GridViewMenus');
const {ColumnAddMenu, ColumnContextMenu, MultiColumnMenu, RowContextMenu} = require('../ui/GridViewMenus');
const {setPopupToCreateDom} = require('popweasel');
const {testId} = require('app/client/ui2018/cssVars');
@@ -184,6 +185,8 @@ GridView.gridCommands = {
hideField: function() { this.hideField(this.cursor.fieldIndex()); },
deleteFields: function() { this.deleteColumns(this.getSelection()); },
clearValues: function() { this.clearValues(this.getSelection()); },
clearColumns: function() { this._clearColumns(this.getSelection()); },
convertFormulasToData: function() { this._convertFormulasToData(this.getSelection()); },
copy: function() { return this.copy(this.getSelection()); },
cut: function() { return this.cut(this.getSelection()); },
paste: function(pasteObj, cutCallback) { return this.paste(pasteObj, cutCallback); },
@@ -409,16 +412,18 @@ GridView.prototype.clearSelection = function() {
};
/**
* Given a selection object, sets all references in the object to the empty string.
* @param {Object} selection
* Given a selection object, sets all cells referred to by the selection to the empty string. If
* only formula columns are selected, only open the formula editor to the empty formula.
* @param {CopySelection} selection
*/
GridView.prototype.clearValues = function(selection) {
console.debug('GridView.clearValues', selection);
selection.rowIds = _.without(selection.rowIds, 'new');
if (selection.rowIds.length === 0) {
// If only the addRow was selected, don't send an action.
return;
} else if (selection.fields.length === 1 && selection.fields[0].column().isRealFormula()) {
// If only the addRow was selected, don't send an action.
if (selection.rowIds.length === 0) { return; }
const options = this._getColumnMenuOptions(selection);
if (options.isFormula === true) {
this.activateEditorAtCursor('');
} else {
let clearAction = tableUtil.makeDeleteAction(selection);
@@ -428,6 +433,30 @@ GridView.prototype.clearValues = function(selection) {
}
};
GridView.prototype._clearColumns = function(selection) {
const fields = selection.fields;
return this.gristDoc.docModel.columns.sendTableAction(
['BulkUpdateRecord', fields.map(f => f.colRef.peek()), {
isFormula: fields.map(f => true),
formula: fields.map(f => ''),
}]
);
};
GridView.prototype._convertFormulasToData = function(selection) {
// Convert all isFormula columns to data, including empty columns. This is sometimes useful
// (e.g. since a truly empty column undergoes a conversion on first data entry, which may be
// prevented by ACL rules).
const fields = selection.fields.filter(f => f.column.peek().isFormula.peek());
if (!fields.length) { return null; }
return this.gristDoc.docModel.columns.sendTableAction(
['BulkUpdateRecord', fields.map(f => f.colRef.peek()), {
isFormula: fields.map(f => false),
formula: fields.map(f => ''),
}]
);
};
GridView.prototype.selectAll = function() {
this.cellSelector.selectArea(0, 0, this.getLastDataRowIndex(),
this.viewSection.viewFields().peekLength - 1);
@@ -801,7 +830,7 @@ GridView.prototype.buildDom = function() {
trigger: [],
});
},
menu(ctl => this.columnContextMenu(ctl, this.getSelection().colIds, field, filterTriggerCtl)),
menu(ctl => this.columnContextMenu(ctl, this.getSelection(), field, filterTriggerCtl)),
testId('column-menu-trigger'),
)
);
@@ -1223,23 +1252,33 @@ GridView.prototype._selectMovedElements = function(start, end, newIndex, numEles
// ===========================================================================
// CONTEXT MENUS
GridView.prototype.columnContextMenu = function (ctl, selectedColIds, field, filterTriggerCtl) {
GridView.prototype.columnContextMenu = function(ctl, copySelection, field, filterTriggerCtl) {
const selectedColIds = copySelection.colIds;
this.ctxMenuHolder.autoDispose(ctl);
const isReadonly = this.gristDoc.isReadonly.get();
const options = this._getColumnMenuOptions(copySelection);
if (selectedColIds.length > 1 && selectedColIds.includes(field.column().colId())) {
return MultiColumnMenu({isReadonly});
return MultiColumnMenu(options);
} else {
return ColumnContextMenu({
disableModify: Boolean(field.disableModify.peek()),
filterOpenFunc: () => filterTriggerCtl.open(),
useNewUI: this.gristDoc.app.useNewUI,
sortSpec: this.gristDoc.viewModel.activeSection.peek().activeSortSpec.peek(),
colId: field.column.peek().id.peek(),
isReadonly
...options,
});
}
};
GridView.prototype._getColumnMenuOptions = function(copySelection) {
return {
numColumns: copySelection.fields.length,
disableModify: calcFieldsCondition(copySelection.fields, f => f.disableModify.peek()),
isReadonly: this.gristDoc.isReadonly.get(),
isFiltered: this.isFiltered(),
isFormula: calcFieldsCondition(copySelection.fields, f => f.column.peek().isRealFormula.peek()),
};
}
GridView.prototype._columnFilterMenu = function(ctl, field) {
this.ctxMenuHolder.autoDispose(ctl);
return this.createFilterMenu(ctl, field);

View File

@@ -335,6 +335,14 @@ exports.groups = [{
name: 'deleteFields',
keys: ['Alt+-'],
desc: 'Delete the currently selected columns'
}, {
name: 'clearColumns',
keys: [],
desc: 'Clear the selected columns'
}, {
name: 'convertFormulasToData',
keys: [],
desc: 'Convert the selected columns from formula to data'
}, {
name: 'addSection',
keys: [],