mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Conditional formatting rules
Summary: Adding conditional formatting rules feature. Each column can have multiple styling rules which are applied in order when evaluated to a truthy value. - The creator panel has a new section: Cell Style - New user action AddEmptyRule for adding an empty rule - New columns in _grist_Table_columns and fields A new color picker will be introduced in a follow-up diff (as it is also used in choice/choice list/filters). Design document: https://grist.quip.com/FVzfAgoO5xOF/Conditional-Formatting-Implementation-Design Test Plan: new tests Reviewers: georgegevoian Reviewed By: georgegevoian Subscribers: alexmojaki Differential Revision: https://phab.getgrist.com/D3282
This commit is contained in:
@@ -36,6 +36,8 @@ import {createValidationRec, ValidationRec} from 'app/client/models/entities/Val
|
||||
import {createViewFieldRec, ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
|
||||
import {createViewRec, ViewRec} from 'app/client/models/entities/ViewRec';
|
||||
import {createViewSectionRec, ViewSectionRec} from 'app/client/models/entities/ViewSectionRec';
|
||||
import {GristObjCode} from 'app/plugin/GristData';
|
||||
import {decodeObject} from 'app/plugin/objtypes';
|
||||
|
||||
// Re-export all the entity types available. The recommended usage is like this:
|
||||
// import {ColumnRec, ViewFieldRec} from 'app/client/models/DocModel';
|
||||
@@ -95,6 +97,24 @@ export function refRecord<TRow extends MetaRowModel>(
|
||||
return ko.pureComputed(() => tableModel.getRowModel(rowIdObs() || 0, true));
|
||||
}
|
||||
|
||||
type RefListValue = [GristObjCode.List, ...number[]]|null;
|
||||
/**
|
||||
* Returns an observable with a list of records from another table, selected using RefList column.
|
||||
* @param {TableModel} tableModel: The model for the table to return a record from.
|
||||
* @param {ko.observable} rowsIdObs: An observable with a RefList value.
|
||||
*/
|
||||
export function refListRecords<TRow extends MetaRowModel>(
|
||||
tableModel: MetaTableModel<TRow>, rowsIdObs: ko.Observable<RefListValue>|ko.Computed<RefListValue>
|
||||
) {
|
||||
return ko.pureComputed(() => {
|
||||
const ids = decodeObject(rowsIdObs()) as number[]|null;
|
||||
if (!Array.isArray(ids)) {
|
||||
return [];
|
||||
}
|
||||
return ids.map(id => tableModel.getRowModel(id, true));
|
||||
});
|
||||
}
|
||||
|
||||
// Use an alias for brevity.
|
||||
type MTM<RowModel extends MetaRowModel> = MetaTableModel<RowModel>;
|
||||
|
||||
|
||||
19
app/client/models/Styles.ts
Normal file
19
app/client/models/Styles.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export interface Style {
|
||||
textColor?: string;
|
||||
fillColor?: string;
|
||||
}
|
||||
|
||||
export class CombinedStyle implements Style {
|
||||
public readonly textColor?: string;
|
||||
public readonly fillColor?: string;
|
||||
constructor(rules: Style[], flags: any[]) {
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
if (flags[i]) {
|
||||
const textColor = rules[i].textColor;
|
||||
const fillColor = rules[i].fillColor;
|
||||
this.textColor = textColor || this.textColor;
|
||||
this.fillColor = fillColor || this.fillColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import {ColumnRec, DocModel, IRowModel, refRecord, ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import {ColumnRec, DocModel, IRowModel, refListRecords, refRecord, ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import {formatterForRec} from 'app/client/models/entities/ColumnRec';
|
||||
import * as modelUtil from 'app/client/models/modelUtil';
|
||||
import {Style} from 'app/client/models/Styles';
|
||||
import * as UserType from 'app/client/widgets/UserType';
|
||||
import {DocumentSettings} from 'app/common/DocumentSettings';
|
||||
import {BaseFormatter} from 'app/common/ValueFormatter';
|
||||
@@ -68,6 +69,9 @@ export interface ViewFieldRec extends IRowModel<"_grist_Views_section_field"> {
|
||||
textColor: modelUtil.KoSaveableObservable<string|undefined>;
|
||||
fillColor: modelUtil.KoSaveableObservable<string>;
|
||||
|
||||
computedColor: ko.Computed<string|undefined>;
|
||||
computedFill: ko.Computed<string>;
|
||||
|
||||
documentSettings: ko.PureComputed<DocumentSettings>;
|
||||
|
||||
// Helper for Reference/ReferenceList columns, which returns a formatter according
|
||||
@@ -81,6 +85,26 @@ export interface ViewFieldRec extends IRowModel<"_grist_Views_section_field"> {
|
||||
// `formatter` formats actual cell values, e.g. a whole list from the display column.
|
||||
formatter: ko.Computed<BaseFormatter>;
|
||||
|
||||
// Field can have a list of conditional styling rules. Each style is a combination of a formula and options
|
||||
// that must by applied to a field. Style is persisted as a new hidden formula column and the list of such
|
||||
// columns is stored as Reference List property ('rules') in a field or column.
|
||||
// Rule for conditional style is a formula of the hidden column, style options are saved as JSON object in
|
||||
// a styleOptions field (in that hidden formula column).
|
||||
|
||||
// If this field (or column) has a list of conditional styling rules.
|
||||
hasRules: ko.Computed<boolean>;
|
||||
// List of columns that are used as rules for conditional styles.
|
||||
rulesCols: ko.Computed<ColumnRec[]>;
|
||||
// List of columns ids that are used as rules for conditional styles.
|
||||
rulesColsIds: ko.Computed<string[]>;
|
||||
// List of styles used by conditional rules.
|
||||
rulesStyles: modelUtil.KoSaveableObservable<Style[]>;
|
||||
|
||||
// Adds empty conditional style rule. Sets before sending to the server.
|
||||
addEmptyRule(): Promise<void>;
|
||||
// Removes one rule from the collection. Removes before sending update to the server.
|
||||
removeRule(index: number): Promise<void>;
|
||||
|
||||
createValueParser(): (value: string) => any;
|
||||
|
||||
// Helper which adds/removes/updates field's displayCol to match the formula.
|
||||
@@ -211,7 +235,7 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
|
||||
// GridView, to avoid interfering with zebra stripes.
|
||||
this.fillColor = modelUtil.savingComputed({
|
||||
read: () => fillColorProp(),
|
||||
write: (setter, val) => setter(fillColorProp, val.toUpperCase() === '#FFFFFF' ? '' : val),
|
||||
write: (setter, val) => setter(fillColorProp, val?.toUpperCase() === '#FFFFFF' ? '' : (val ?? '')),
|
||||
});
|
||||
|
||||
this.documentSettings = ko.pureComputed(() => docModel.docInfoRow.documentSettingsJson());
|
||||
@@ -231,4 +255,47 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
|
||||
};
|
||||
return docModel.docData.bundleActions("Update choices configuration", callback, actionOptions);
|
||||
};
|
||||
|
||||
this.rulesCols = refListRecords(docModel.columns, ko.pureComputed(() => this._fieldOrColumn().rules()));
|
||||
this.rulesColsIds = ko.pureComputed(() => this.rulesCols().map(c => c.colId()));
|
||||
this.rulesStyles = modelUtil.fieldWithDefault(
|
||||
this.widgetOptionsJson.prop("rulesOptions") as modelUtil.KoSaveableObservable<Style[]>,
|
||||
[]);
|
||||
this.hasRules = ko.pureComputed(() => this.rulesCols().length > 0);
|
||||
|
||||
// Helper method to add an empty rule (either initial or additional one).
|
||||
// Style options are added to widget options directly and can be briefly out of sync,
|
||||
// which is taken into account during rendering.
|
||||
this.addEmptyRule = async () => {
|
||||
const useCol = this.useColOptions.peek();
|
||||
const action = [
|
||||
'AddEmptyRule',
|
||||
this.column.peek().table.peek().tableId.peek(),
|
||||
useCol ? 0 : this.id.peek(), // field_ref
|
||||
useCol ? this.column.peek().id.peek() : 0, // col_ref
|
||||
];
|
||||
await docModel.docData.sendAction(action, `Update rules for ${this.colId.peek()}`);
|
||||
};
|
||||
|
||||
// Helper method to remove a rule.
|
||||
this.removeRule = async (index: number) => {
|
||||
const col = this.rulesCols.peek()[index];
|
||||
if (!col) {
|
||||
throw new Error(`There is no rule at index ${index}`);
|
||||
}
|
||||
const tableData = docModel.dataTables[col.table.peek().tableId.peek()].tableData;
|
||||
const newStyles = this.rulesStyles.peek().slice();
|
||||
if (newStyles.length >= index) {
|
||||
newStyles.splice(index, 1);
|
||||
} else {
|
||||
console.debug(`There are not style options at index ${index}`);
|
||||
}
|
||||
const callback = () =>
|
||||
Promise.all([
|
||||
this.rulesStyles.setAndSave(newStyles),
|
||||
tableData.sendTableAction(['RemoveColumn', col.colId.peek()])
|
||||
]);
|
||||
const actionOptions = {nestInActiveBundle: this.column.peek().isTransforming.peek()};
|
||||
await docModel.docData.bundleActions("Remove conditional rule", callback, actionOptions);
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user