(core) Implementing row conditional formatting

Summary:
Conditional formatting can now be used for whole rows.
Related fix:
- Font styles weren't applicable for summary columns.
- Checkbox and slider weren't using colors properly

Test Plan: Existing and new tests

Reviewers: paulfitz, georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D3547
This commit is contained in:
Jarosław Sadziński
2022-08-08 15:32:50 +02:00
parent fbba6b8f52
commit 9e4d802405
52 changed files with 823 additions and 439 deletions

View File

@@ -0,0 +1,41 @@
import {ColumnRec, DocModel} from 'app/client/models/DocModel';
import {Style} from 'app/client/models/Styles';
import * as modelUtil from 'app/client/models/modelUtil';
export interface RuleOwner {
// Field or Section can have a list of conditional styling rules. Each style is a combination of a formula and options
// that must by applied. 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.
tableId: ko.Computed<string>;
// 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>;
}
export async function removeRule(docModel: DocModel, owner: RuleOwner, index: number) {
const col = owner.rulesCols.peek()[index];
if (!col) {
throw new Error(`There is no rule at index ${index}`);
}
const newStyles = owner.rulesStyles.peek()?.slice() ?? [];
if (newStyles.length >= index) {
newStyles.splice(index, 1);
} else {
console.debug(`There are not style options at index ${index}`);
}
await docModel.docData.bundleActions("Remove conditional rule", () =>
Promise.all([
owner.rulesStyles.setAndSave(newStyles),
docModel.docData.sendAction(['RemoveColumn', owner.tableId.peek(), col.colId.peek()])
])
);
}

View File

@@ -1,6 +1,7 @@
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 {removeRule, RuleOwner} from 'app/client/models/RuleOwner';
import {Style} from 'app/client/models/Styles';
import * as UserType from 'app/client/widgets/UserType';
import {DocumentSettings} from 'app/common/DocumentSettings';
@@ -9,7 +10,7 @@ import {createParser} from 'app/common/ValueParser';
import * as ko from 'knockout';
// Represents a page entry in the tree of pages.
export interface ViewFieldRec extends IRowModel<"_grist_Views_section_field"> {
export interface ViewFieldRec extends IRowModel<"_grist_Views_section_field">, RuleOwner {
viewSection: ko.Computed<ViewSectionRec>;
widthDef: modelUtil.KoSaveableObservable<number>;
@@ -86,26 +87,6 @@ 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.
@@ -253,6 +234,7 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
return docModel.docData.bundleActions("Update choices configuration", callback, actionOptions);
};
this.tableId = ko.pureComputed(() => this.column().table().tableId());
this.rulesCols = refListRecords(docModel.columns, ko.pureComputed(() => this._fieldOrColumn().rules()));
this.rulesColsIds = ko.pureComputed(() => this.rulesCols().map(c => c.colId()));
this.rulesStyles = modelUtil.fieldWithDefault(
@@ -274,25 +256,5 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
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);
};
this.removeRule = (index: number) => removeRule(docModel, this, index);
}

View File

@@ -8,6 +8,7 @@ import {
FilterRec,
IRowModel,
recordSet,
refListRecords,
refRecord,
TableRec,
ViewFieldRec,
@@ -22,13 +23,14 @@ 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 {removeRule, RuleOwner} from 'app/client/models/RuleOwner';
import {Computed, Holder, Observable} from 'grainjs';
import * as ko from 'knockout';
import defaults = require('lodash/defaults');
// Represents a section of user views, now also known as a "page widget" (e.g. a view may contain
// a grid section and a chart section).
export interface ViewSectionRec extends IRowModel<"_grist_Views_section"> {
export interface ViewSectionRec extends IRowModel<"_grist_Views_section">, RuleOwner {
viewFields: ko.Computed<KoArray<ViewFieldRec>>;
// All table columns associated with this view section, excluding hidden helper columns.
@@ -168,6 +170,7 @@ export interface ViewSectionRec extends IRowModel<"_grist_Views_section"> {
// List of selected rows
selectedRows: Observable<number[]>;
editingFormula: ko.Computed<boolean>;
// Save all filters of fields/columns in the section.
saveFilters(): Promise<void>;
@@ -235,7 +238,12 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
// All table columns associated with this view section, excluding any hidden helper columns.
this.columns = this.autoDispose(ko.pureComputed(() => this.table().columns().all().filter(c => !c.isHiddenCol())));
this.editingFormula = ko.pureComputed({
read: () => docModel.editingFormula(),
write: val => {
docModel.editingFormula(val);
}
});
const defaultOptions = {
verticalGridlines: true,
horizontalGridlines: true,
@@ -586,4 +594,25 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
this.allowSelectBy = Observable.create(this, false);
this.selectedRows = Observable.create(this, []);
this.tableId = ko.pureComputed(() => this.table().tableId());
const rawSection = ko.pureComputed(() => this.table().rawViewSection());
this.rulesCols = refListRecords(docModel.columns, ko.pureComputed(() => rawSection().rules()));
this.rulesColsIds = ko.pureComputed(() => this.rulesCols().map(c => c.colId()));
this.rulesStyles = modelUtil.savingComputed({
read: () => rawSection().optionsObj.prop("rulesOptions")() ?? [],
write: (setter, val) => setter(rawSection().optionsObj.prop("rulesOptions"), val)
});
this.hasRules = ko.pureComputed(() => this.rulesCols().length > 0);
this.addEmptyRule = async () => {
const action = [
'AddEmptyRule',
this.tableId.peek(),
null,
null
];
await docModel.docData.sendAction(action, `Update rules for ${this.table.peek().tableId.peek()}`);
};
this.removeRule = (index: number) => removeRule(docModel, this, index);
}