(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:
Dmitry S
2021-03-16 23:45:44 -04:00
parent e2d3b70509
commit b4c34cedad
18 changed files with 554 additions and 292 deletions

View File

@@ -290,6 +290,18 @@ BaseView.prototype.activateEditorAtCursor = function(input) {
}
};
/**
* Move the floating RowModel for editing to the current cursor position, and return it.
*
* This is used for opening the formula editor in the side panel; the current row is used to get
* possible exception info from the formula.
*/
BaseView.prototype.moveEditRowToCursor = function() {
var rowId = this.viewData.getRowId(this.cursor.rowIndex());
this.editRowModel.assign(rowId);
return this.editRowModel;
};
// Copy an anchor link for the current row to the clipboard.
BaseView.prototype.copyLink = async function() {
const rowId = this.viewData.getRowId(this.cursor.rowIndex());

View File

@@ -1,169 +0,0 @@
var ko = require('knockout');
var dispose = require('../lib/dispose');
var dom = require('../lib/dom');
var kd = require('../lib/koDom');
var kf = require('../lib/koForm');
var modelUtil = require('../models/modelUtil');
var gutil = require('app/common/gutil');
var AceEditor = require('./AceEditor');
var RefSelect = require('./RefSelect');
const {dom: grainjsDom, makeTestId} = require('grainjs');
const testId = makeTestId('test-fconfigtab-');
function FieldConfigTab(options) {
this.gristDoc = options.gristDoc;
this.fieldBuilder = options.fieldBuilder;
this.origColRef = this.autoDispose(ko.computed(() =>
this.fieldBuilder() ? this.fieldBuilder().origColumn.origColRef() : null));
this.isColumnValid = this.autoDispose(ko.computed(() => Boolean(this.origColRef())));
this.origColumn = this.autoDispose(
this.gristDoc.docModel.columns.createFloatingRowModel(this.origColRef));
this.disableModify = this.autoDispose(ko.computed(() =>
this.origColumn.disableModify() || this.origColumn.isTransforming()));
this.colId = modelUtil.customComputed({
read: () => this.origColumn.colId(),
save: val => this.origColumn.colId.saveOnly(val)
});
this.showColId = this.autoDispose(ko.pureComputed({
read: () => {
let label = this.origColumn.label();
let derivedColId = label ? gutil.sanitizeIdent(label) : null;
return derivedColId === this.colId() && !this.origColumn.untieColIdFromLabel();
}
}));
this.isDerivedFromLabel = this.autoDispose(ko.pureComputed({
read: () => !this.origColumn.untieColIdFromLabel(),
write: newValue => this.origColumn.untieColIdFromLabel.saveOnly(!newValue)
}));
// Indicates whether this is a ref col that references a different table.
this.isForeignRefCol = this.autoDispose(ko.pureComputed(() => {
let type = this.origColumn.type();
return type && gutil.startsWith(type, 'Ref:') &&
this.origColumn.table().tableId() !== gutil.removePrefix(type, 'Ref:');
}));
// Create an instance of AceEditor that can be built for each column
this.formulaEditor = this.autoDispose(AceEditor.create({observable: this.origColumn.formula}));
// Builder for the reference display column multiselect.
this.refSelect = this.autoDispose(RefSelect.create(this));
if (options.contentCallback) {
options.contentCallback(this.buildConfigDomObj());
} else {
this.autoDispose(this.gristDoc.addOptionsTab(
'Field', dom('span.glyphicon.glyphicon-sort-by-attributes'),
this.buildConfigDomObj(),
{ 'category': 'options', 'show': this.fieldBuilder }
));
}
}
dispose.makeDisposable(FieldConfigTab);
// Builds object with FieldConfigTab dom builder and settings for the sidepane.
// TODO: Field still cannot be filtered/filter settings cannot be opened from FieldConfigTab.
// This should be considered.
FieldConfigTab.prototype.buildConfigDomObj = function() {
return [{
'buildDom': this._buildNameDom.bind(this),
'keywords': ['field', 'column', 'name', 'title']
}, {
'header': true,
'items': [{
'buildDom': this._buildFormulaDom.bind(this),
'keywords': ['field', 'column', 'formula']
}]
}, {
'header': true,
'label': 'Format Cells',
'items': [{
'buildDom': this._buildFormatDom.bind(this),
'keywords': ['field', 'type', 'widget', 'options', 'alignment', 'justify', 'justification']
}]
}, {
'header': true,
'label': 'Additional Columns',
'showObs': this.isForeignRefCol,
'items': [{
'buildDom': () => this.refSelect.buildDom(),
'keywords': ['additional', 'columns', 'reference', 'formula']
}]
}, {
'header': true,
'label': 'Transform',
'items': [{
'buildDom': this._buildTransformDom.bind(this),
'keywords': ['field', 'type']
}]
}];
};
FieldConfigTab.prototype._buildNameDom = function() {
return grainjsDom.maybe(this.isColumnValid, () => dom('div',
kf.row(
1, dom('div.glyphicon.glyphicon-sort-by-attributes.config_icon'),
4, kf.label('Field'),
13, kf.text(this.origColumn.label, { disabled: this.disableModify },
dom.testId("FieldConfigTab_fieldLabel"),
testId('field-label'))
),
kf.row(
kd.hide(this.showColId),
1, dom('div.glyphicon.glyphicon-tag.config_icon'),
4, kf.label('ID'),
13, kf.text(this.colId, { disabled: this.disableModify },
dom.testId("FieldConfigTab_colId"),
testId('field-col-id'))
),
kf.row(
8, kf.lightLabel("Use Name as ID?"),
1, kf.checkbox(this.isDerivedFromLabel,
dom.testId("FieldConfigTab_deriveId"),
testId('field-derive-id'))
)
));
};
FieldConfigTab.prototype._buildFormulaDom = function() {
return grainjsDom.maybe(this.isColumnValid, () => dom('div',
kf.row(
3, kf.buttonGroup(
kf.checkButton(this.origColumn.isFormula,
dom('span.formula_button_f', '\u0192'),
dom('span.formula_button_x', 'x'),
kd.toggleClass('disabled', this.disableModify),
{ title: 'Change to formula column' }
)
),
15, dom('div.transform_editor', this.formulaEditor.buildDom())
),
kf.helpRow(
3, dom('span'),
15, kf.lightLabel(kd.text(
() => this.origColumn.isFormula() ? 'Formula' : 'Default Formula'))
)
));
};
FieldConfigTab.prototype._buildTransformDom = function() {
return grainjsDom.maybe(this.fieldBuilder, builder => builder.buildTransformDom());
};
FieldConfigTab.prototype._buildFormatDom = function() {
return grainjsDom.maybe(this.fieldBuilder, builder => [
builder.buildSelectTypeDom(),
builder.buildSelectWidgetDom(),
builder.buildConfigDom()
]);
};
module.exports = FieldConfigTab;

View File

@@ -435,12 +435,7 @@ 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 => ''),
}]
);
return this.gristDoc.clearColumns(fields.map(f => f.colRef.peek()));
};
GridView.prototype._convertFormulasToData = function(selection) {
@@ -449,12 +444,7 @@ GridView.prototype._convertFormulasToData = function(selection) {
// 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 => ''),
}]
);
return this.gristDoc.convertFormulasToData(fields.map(f => f.colRef.peek()));
};
GridView.prototype.selectAll = function() {

View File

@@ -492,6 +492,26 @@ export class GristDoc extends DisposableWithEvents {
}
}
// Turn the given columns into empty columns, losing any data stored in them.
public async clearColumns(colRefs: number[]): Promise<void> {
await this.docModel.columns.sendTableAction(
['BulkUpdateRecord', colRefs, {
isFormula: colRefs.map(f => true),
formula: colRefs.map(f => ''),
}]
);
}
// Convert the given columns to data, saving the calculated values and unsetting the formulas.
public async convertFormulasToData(colRefs: number[]): Promise<void> {
return this.docModel.columns.sendTableAction(
['BulkUpdateRecord', colRefs, {
isFormula: colRefs.map(f => false),
formula: colRefs.map(f => ''),
}]
);
}
public getCsvLink() {
return this.docComm.docUrl('gen_csv') + '?' + encodeQueryParams({
...this.docComm.getUrlParams(),

View File

@@ -14,15 +14,21 @@ const {menu, menuItem, menuText} = require('app/client/ui2018/menus');
/**
* Builder for the reference display multiselect.
*/
function RefSelect(fieldConfigTab) {
this.docModel = fieldConfigTab.gristDoc.docModel;
this.origColumn = fieldConfigTab.origColumn;
this.colId = fieldConfigTab.colId;
this.isForeignRefCol = fieldConfigTab.isForeignRefCol;
function RefSelect(options) {
this.docModel = options.docModel;
this.origColumn = options.origColumn;
this.colId = this.origColumn.colId;
// Indicates whether this is a ref col that references a different table.
// (That's the only time when RefSelect is offered.)
this.isForeignRefCol = this.autoDispose(ko.computed(() => {
const t = this.origColumn.refTable();
return Boolean(t && t.getRowId() !== this.origColumn.parentId());
}));
// Computed for the current fieldBuilder's field, if it exists.
this.fieldObs = this.autoDispose(ko.computed(() => {
let builder = fieldConfigTab.fieldBuilder();
let builder = options.fieldBuilder();
return builder ? builder.field : null;
}));

View File

@@ -1,6 +1,6 @@
// This module is unused except to group some modules for a webpack bundle.
// TODO It is a vestige of the old ViewPane.js, and can go away with some bundling improvements.
import * as FieldConfigTab from 'app/client/components/FieldConfigTab';
import * as ViewConfigTab from 'app/client/components/ViewConfigTab';
export {FieldConfigTab, ViewConfigTab};
import * as FieldConfig from 'app/client/ui/FieldConfig';
export {ViewConfigTab, FieldConfig};