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:
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
@@ -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() {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
}));
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user