mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Multi-column configuration
Summary: Creator panel allows now to edit multiple columns at once for some options that are common for them. Options that are not common are disabled. List of options that can be edited for multiple columns: - Column behavior (but limited to empty/formula columns) - Alignment and wrapping - Default style - Number options (for numeric columns) - Column types (but only for empty/formula columns) If multiple columns of the same type are selected, most of the options are available to change, except formula, trigger formula and conditional styles. Editing column label or column id is disabled by default for multiple selection. Not related: some tests were fixed due to the change in the column label and id widget in grist-core (disabled attribute was replaced by readonly). Test Plan: Updated and new tests. Reviewers: georgegevoian Reviewed By: georgegevoian Differential Revision: https://phab.getgrist.com/D3598
This commit is contained in:
@@ -11,6 +11,9 @@ import {
|
||||
} from 'app/common/ValueFormatter';
|
||||
import * as ko from 'knockout';
|
||||
|
||||
// Column behavior type, used primarily in the UI.
|
||||
export type BEHAVIOR = "empty"|"formula"|"data";
|
||||
|
||||
// Represents a column in a user-defined table.
|
||||
export interface ColumnRec extends IRowModel<"_grist_Tables_column"> {
|
||||
table: ko.Computed<TableRec>;
|
||||
@@ -38,6 +41,9 @@ export interface ColumnRec extends IRowModel<"_grist_Tables_column"> {
|
||||
// Convenience observable to obtain and set the type with no suffix
|
||||
pureType: ko.Computed<string>;
|
||||
|
||||
// Column behavior as seen by the user.
|
||||
behavior: ko.Computed<BEHAVIOR>;
|
||||
|
||||
// The column's display column
|
||||
_displayColModel: ko.Computed<ColumnRec>;
|
||||
|
||||
@@ -132,6 +138,8 @@ export function createColumnRec(this: ColumnRec, docModel: DocModel): void {
|
||||
this.visibleColFormatter = ko.pureComputed(() => formatterForRec(this, this, docModel, 'vcol'));
|
||||
|
||||
this.formatter = ko.pureComputed(() => formatterForRec(this, this, docModel, 'full'));
|
||||
|
||||
this.behavior = ko.pureComputed(() => this.isEmpty() ? 'empty' : this.isFormula() ? 'formula' : 'data');
|
||||
}
|
||||
|
||||
export function formatterForRec(
|
||||
|
||||
@@ -3,6 +3,7 @@ import {formatterForRec} from 'app/client/models/entities/ColumnRec';
|
||||
import * as modelUtil from 'app/client/models/modelUtil';
|
||||
import {removeRule, RuleOwner} from 'app/client/models/RuleOwner';
|
||||
import {Style} from 'app/client/models/Styles';
|
||||
import {ViewFieldConfig} from 'app/client/models/ViewFieldConfig';
|
||||
import * as UserType from 'app/client/widgets/UserType';
|
||||
import {DocumentSettings} from 'app/common/DocumentSettings';
|
||||
import {BaseFormatter} from 'app/common/ValueFormatter';
|
||||
@@ -61,18 +62,23 @@ export interface ViewFieldRec extends IRowModel<"_grist_Views_section_field">, R
|
||||
// which takes into account the default options for column's type.
|
||||
widgetOptionsJson: modelUtil.SaveableObjObservable<any>;
|
||||
|
||||
// Whether lines should wrap in a cell.
|
||||
wrapping: ko.Computed<boolean>;
|
||||
|
||||
disableModify: ko.Computed<boolean>;
|
||||
disableEditData: ko.Computed<boolean>;
|
||||
|
||||
// Whether lines should wrap in a cell.
|
||||
wrap: modelUtil.KoSaveableObservable<boolean>;
|
||||
widget: modelUtil.KoSaveableObservable<string|undefined>;
|
||||
textColor: modelUtil.KoSaveableObservable<string|undefined>;
|
||||
fillColor: modelUtil.KoSaveableObservable<string|undefined>;
|
||||
fontBold: modelUtil.KoSaveableObservable<boolean|undefined>;
|
||||
fontUnderline: modelUtil.KoSaveableObservable<boolean|undefined>;
|
||||
fontItalic: modelUtil.KoSaveableObservable<boolean|undefined>;
|
||||
fontStrikethrough: modelUtil.KoSaveableObservable<boolean|undefined>;
|
||||
// Helper computed to change style of a cell without saving it.
|
||||
style: ko.PureComputed<Style>;
|
||||
|
||||
config: ViewFieldConfig;
|
||||
|
||||
documentSettings: ko.PureComputed<DocumentSettings>;
|
||||
|
||||
@@ -91,10 +97,6 @@ export interface ViewFieldRec extends IRowModel<"_grist_Views_section_field">, R
|
||||
|
||||
// Helper which adds/removes/updates field's displayCol to match the formula.
|
||||
saveDisplayFormula(formula: string): Promise<void>|undefined;
|
||||
|
||||
// Helper for Choice/ChoiceList columns, that saves widget options and renames values in a document
|
||||
// in one bundle
|
||||
updateChoices(renameMap: Record<string, string>, options: any): Promise<void>;
|
||||
}
|
||||
|
||||
export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void {
|
||||
@@ -147,17 +149,17 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
|
||||
// Whether this field uses column's widgetOptions (true) or its own (false).
|
||||
// During transform, use the transform column's options (which should be initialized to match
|
||||
// field or column when the transform starts TODO).
|
||||
this.useColOptions = ko.pureComputed(() => !this.widgetOptions() || this.column().isTransforming());
|
||||
this.useColOptions = this.autoDispose(ko.pureComputed(() => !this.widgetOptions() || this.column().isTransforming()));
|
||||
|
||||
// Helper that returns the RowModel for either this field or its column, depending on
|
||||
// useColOptions. Field and Column have a few identical fields:
|
||||
// .widgetOptions() // JSON string of options
|
||||
// .saveDisplayFormula() // Method to save the display formula
|
||||
// .displayCol() // Reference to an optional associated display column.
|
||||
this._fieldOrColumn = ko.pureComputed(() => this.useColOptions() ? this.column() : this);
|
||||
this._fieldOrColumn = this.autoDispose(ko.pureComputed(() => this.useColOptions() ? this.column() : this));
|
||||
|
||||
// Display col ref to use for the field, defaulting to the plain column itself.
|
||||
this.displayColRef = ko.pureComputed(() => this._fieldOrColumn().displayCol() || this.colRef());
|
||||
this.displayColRef = this.autoDispose(ko.pureComputed(() => this._fieldOrColumn().displayCol() || this.colRef()));
|
||||
|
||||
this.visibleColRef = modelUtil.addSaveInterface(ko.pureComputed({
|
||||
read: () => this._fieldOrColumn().visibleCol(),
|
||||
@@ -189,26 +191,23 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
|
||||
};
|
||||
|
||||
// The widgetOptions to read and write: either the column's or the field's own.
|
||||
this._widgetOptionsStr = modelUtil.savingComputed({
|
||||
this._widgetOptionsStr = this.autoDispose(modelUtil.savingComputed({
|
||||
read: () => this._fieldOrColumn().widgetOptions(),
|
||||
write: (setter, val) => setter(this._fieldOrColumn().widgetOptions, val)
|
||||
});
|
||||
}));
|
||||
|
||||
// Observable for the object with the current options, either for the field or for the column,
|
||||
// which takes into account the default options for this column's type.
|
||||
this.widgetOptionsJson = modelUtil.jsonObservable(this._widgetOptionsStr,
|
||||
(opts: any) => UserType.mergeOptions(opts || {}, this.column().pureType()));
|
||||
|
||||
this.wrapping = ko.pureComputed(() => {
|
||||
// When user has yet to specify a desired wrapping state, we use different defaults for
|
||||
// GridView (no wrap) and DetailView (wrap).
|
||||
// "??" is the newish "nullish coalescing" operator. How cool is that!
|
||||
return this.widgetOptionsJson().wrap ?? (this.viewSection().parentKey() !== 'record');
|
||||
});
|
||||
|
||||
this.disableModify = ko.pureComputed(() => this.column().disableModify());
|
||||
this.disableEditData = ko.pureComputed(() => this.column().disableEditData());
|
||||
this.widgetOptionsJson = this.autoDispose(modelUtil.jsonObservable(this._widgetOptionsStr,
|
||||
(opts: any) => UserType.mergeOptions(opts || {}, this.column().pureType())));
|
||||
|
||||
// When user has yet to specify a desired wrapping state, we use different defaults for
|
||||
// GridView (no wrap) and DetailView (wrap).
|
||||
this.wrap = this.autoDispose(modelUtil.fieldWithDefault(
|
||||
this.widgetOptionsJson.prop('wrap'),
|
||||
() => this.viewSection().parentKey() !== 'record'
|
||||
));
|
||||
this.widget = this.widgetOptionsJson.prop('widget');
|
||||
this.textColor = this.widgetOptionsJson.prop('textColor');
|
||||
this.fillColor = this.widgetOptionsJson.prop('fillColor');
|
||||
this.fontBold = this.widgetOptionsJson.prop('fontBold');
|
||||
@@ -217,22 +216,19 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
|
||||
this.fontStrikethrough = this.widgetOptionsJson.prop('fontStrikethrough');
|
||||
|
||||
this.documentSettings = ko.pureComputed(() => docModel.docInfoRow.documentSettingsJson());
|
||||
|
||||
this.updateChoices = async (renames, widgetOptions) => {
|
||||
// In case this column is being transformed - using Apply Formula to Data, bundle the action
|
||||
// together with the transformation.
|
||||
const actionOptions = {nestInActiveBundle: this.column.peek().isTransforming.peek()};
|
||||
const hasRenames = !!Object.entries(renames).length;
|
||||
const callback = async () => {
|
||||
await Promise.all([
|
||||
this.widgetOptionsJson.setAndSave(widgetOptions),
|
||||
hasRenames ?
|
||||
docModel.docData.sendAction(["RenameChoices", this.column().table().tableId(), this.colId(), renames]) :
|
||||
null
|
||||
]);
|
||||
};
|
||||
return docModel.docData.bundleActions("Update choices configuration", callback, actionOptions);
|
||||
};
|
||||
this.style = ko.pureComputed({
|
||||
read: () => ({
|
||||
textColor: this.textColor(),
|
||||
fillColor: this.fillColor(),
|
||||
fontBold: this.fontBold(),
|
||||
fontUnderline: this.fontUnderline(),
|
||||
fontItalic: this.fontItalic(),
|
||||
fontStrikethrough: this.fontStrikethrough(),
|
||||
}) as Style,
|
||||
write: (style: Style) => {
|
||||
this.widgetOptionsJson.update(style);
|
||||
},
|
||||
});
|
||||
|
||||
this.tableId = ko.pureComputed(() => this.column().table().tableId());
|
||||
this.rulesCols = refListRecords(docModel.columns, ko.pureComputed(() => this._fieldOrColumn().rules()));
|
||||
@@ -257,4 +253,10 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
|
||||
};
|
||||
|
||||
this.removeRule = (index: number) => removeRule(docModel, this, index);
|
||||
// Externalize widgetOptions configuration, to support changing those options
|
||||
// for multiple fields at once.
|
||||
this.config = new ViewFieldConfig(this, docModel);
|
||||
|
||||
this.disableModify = this.autoDispose(ko.pureComputed(() => this.column().disableModify()));
|
||||
this.disableEditData = this.autoDispose(ko.pureComputed(() => this.column().disableEditData()));
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {arrayRepeat} from 'app/common/gutil';
|
||||
import {Sort} from 'app/common/SortSpec';
|
||||
import {ColumnsToMap, WidgetColumnMap} from 'app/plugin/CustomSectionAPI';
|
||||
import {ColumnToMapImpl} from 'app/client/models/ColumnToMap';
|
||||
import {BEHAVIOR} from 'app/client/models/entities/ColumnRec';
|
||||
import {removeRule, RuleOwner} from 'app/client/models/RuleOwner';
|
||||
import {Computed, Holder, Observable} from 'grainjs';
|
||||
import * as ko from 'knockout';
|
||||
@@ -172,6 +173,18 @@ export interface ViewSectionRec extends IRowModel<"_grist_Views_section">, RuleO
|
||||
|
||||
editingFormula: ko.Computed<boolean>;
|
||||
|
||||
// Selected fields (columns) for the section.
|
||||
selectedFields: ko.Observable<ViewFieldRec[]>;
|
||||
|
||||
// Some computed observables for multi-select, used in the creator panel, by more than one widgets.
|
||||
|
||||
// Common column behavior or mixed.
|
||||
columnsBehavior: ko.PureComputed<BEHAVIOR|'mixed'>;
|
||||
// If all selected columns are empty or formula column.
|
||||
columnsAllIsFormula: ko.PureComputed<boolean>;
|
||||
// Common type of selected columns or mixed.
|
||||
columnsType: ko.PureComputed<string|'mixed'>;
|
||||
|
||||
// Save all filters of fields/columns in the section.
|
||||
saveFilters(): Promise<void>;
|
||||
|
||||
@@ -277,6 +290,25 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
|
||||
sectionId: customDefObj.prop('sectionId')
|
||||
};
|
||||
|
||||
this.selectedFields = ko.observable<any>([]);
|
||||
|
||||
// During schema change, some columns/fields might be disposed beyond our control.
|
||||
const selectedColumns = this.autoDispose(ko.pureComputed(() => this.selectedFields()
|
||||
.filter(f => !f.isDisposed())
|
||||
.map(f => f.column())
|
||||
.filter(c => !c.isDisposed())));
|
||||
this.columnsBehavior = ko.pureComputed(() => {
|
||||
const list = new Set(selectedColumns().map(c => c.behavior()));
|
||||
return list.size === 1 ? list.values().next().value : 'mixed';
|
||||
});
|
||||
this.columnsType = ko.pureComputed(() => {
|
||||
const list = new Set(selectedColumns().map(c => c.type()));
|
||||
return list.size === 1 ? list.values().next().value : 'mixed';
|
||||
});
|
||||
this.columnsAllIsFormula = ko.pureComputed(() => {
|
||||
return selectedColumns().every(c => c.isFormula());
|
||||
});
|
||||
|
||||
this.activeCustomOptions = modelUtil.customValue(this.customDef.widgetOptions);
|
||||
|
||||
this.saveCustomDef = async () => {
|
||||
|
||||
Reference in New Issue
Block a user