(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

@@ -26,7 +26,7 @@ function AceEditor(options) {
this.saveValueOnBlurEvent = !(options.saveValueOnBlurEvent === false);
this.calcSize = options.calcSize || ((_elem, size) => size);
this.gristDoc = options.gristDoc || null;
this.field = options.field || null;
this.column = options.column || null;
this.editorState = options.editorState || null;
this._readonly = options.readonly || false;
@@ -185,10 +185,10 @@ AceEditor.prototype.setFontSize = function(pxVal) {
AceEditor.prototype._setup = function() {
// Standard editor setup
this.editor = this.autoDisposeWith('destroy', ace.edit(this.editorDom));
if (this.gristDoc && this.field) {
if (this.gristDoc && this.column) {
const getSuggestions = (prefix) => {
const tableId = this.gristDoc.viewModel.activeSection().table().tableId();
const columnId = this.field.column().colId();
const columnId = this.column.colId();
return this.gristDoc.docComm.autocomplete(prefix, tableId, columnId);
};
setupAceEditorCompletions(this.editor, {getSuggestions});

View File

@@ -12,7 +12,7 @@ import {reportError} from 'app/client/models/errors';
import {KoSaveableObservable, ObjObservable, setSaveValue} from 'app/client/models/modelUtil';
import {SortedRowSet} from 'app/client/models/rowset';
import {IPageWidget, toPageWidget} from 'app/client/ui/PageWidgetPicker';
import {cssLabel, cssRow, cssSeparator} from 'app/client/ui/RightPanel';
import {cssLabel, cssRow, cssSeparator} from 'app/client/ui/RightPanelStyles';
import {cssFieldEntry, cssFieldLabel, IField, VisibleFieldsConfig } from 'app/client/ui/VisibleFieldsConfig';
import {IconName} from 'app/client/ui2018/IconList';
import {squareCheckbox} from 'app/client/ui2018/checkbox';

View File

@@ -37,6 +37,7 @@
min-height: 16px;
white-space: pre;
word-wrap: break-word;
color: black;
}
.g_record_detail_value.record-add {

View File

@@ -8,7 +8,7 @@
import * as AceEditor from 'app/client/components/AceEditor';
import {ColumnTransform} from 'app/client/components/ColumnTransform';
import {GristDoc} from 'app/client/components/GristDoc';
import {cssButtonRow} from 'app/client/ui/RightPanel';
import {cssButtonRow} from 'app/client/ui/RightPanelStyles';
import {basicButton, primaryButton} from 'app/client/ui2018/buttons';
import {testId} from 'app/client/ui2018/cssVars';
import {FieldBuilder} from 'app/client/widgets/FieldBuilder';

View File

@@ -1,26 +1,28 @@
/* globals alert, document, $ */
var _ = require('underscore');
var ko = require('knockout');
const _ = require('underscore');
const ko = require('knockout');
const debounce = require('lodash/debounce');
var gutil = require('app/common/gutil');
var BinaryIndexedTree = require('app/common/BinaryIndexedTree');
const gutil = require('app/common/gutil');
const BinaryIndexedTree = require('app/common/BinaryIndexedTree');
const {Sort} = require('app/common/SortSpec');
var dom = require('../lib/dom');
var kd = require('../lib/koDom');
var kf = require('../lib/koForm');
var koDomScrolly = require('../lib/koDomScrolly');
var tableUtil = require('../lib/tableUtil');
var {addToSort, sortBy} = require('../lib/sortUtil');
const dom = require('../lib/dom');
const kd = require('../lib/koDom');
const kf = require('../lib/koForm');
const koDomScrolly = require('../lib/koDomScrolly');
const tableUtil = require('../lib/tableUtil');
const {addToSort, sortBy} = require('../lib/sortUtil');
var commands = require('./commands');
var viewCommon = require('./viewCommon');
var Base = require('./Base');
var BaseView = require('./BaseView');
var selector = require('./Selector');
var {CopySelection} = require('./CopySelection');
const commands = require('./commands');
const viewCommon = require('./viewCommon');
const Base = require('./Base');
const BaseView = require('./BaseView');
const selector = require('./Selector');
const {CopySelection} = require('./CopySelection');
const koUtil = require('app/client/lib/koUtil');
const convert = require('color-convert');
const {renderAllRows} = require('app/client/components/Printing');
const {reportError} = require('app/client/models/AppModel');
@@ -40,6 +42,7 @@ const {contextMenu} = require('app/client/ui/contextMenu');
const {menuToggle} = require('app/client/ui/MenuToggle');
const {showTooltip} = require('app/client/ui/tooltips');
const {parsePasteForView} = require("./BaseView2");
const {CombinedStyle} = require("app/client/models/Styles");
// A threshold for interpreting a motionless click as a click rather than a drag.
@@ -1111,8 +1114,41 @@ GridView.prototype.buildDom = function() {
// rows. IsCellActive is only subscribed to columns for the active row. This way, when
// the cursor moves, there are (rows+2*columns) calls rather than rows*columns.
var isRowActive = ko.computed(() => row._index() === self.cursor.rowIndex());
const computedFlags = ko.pureComputed(() => {
return self.viewSection.rulesColsIds().map(colRef => {
if (row.cells[colRef]) { return row.cells[colRef]() || false; }
return false;
});
});
const computedRule = koUtil.withKoUtils(ko.pureComputed(() => {
if (row._isAddRow() || !row.id()) { return null; }
const flags = computedFlags();
if (flags.length === 0) { return null; }
const styles = self.viewSection.rulesStyles() || [];
return { style : new CombinedStyle(styles, flags) };
}, this).extend({deferred: true}));
const fillColor = buildStyleOption(self, computedRule, 'fillColor');
const zebraColor = ko.pureComputed(() => calcZebra(fillColor()));
const textColor = buildStyleOption(self, computedRule, 'textColor');
const fontBold = buildStyleOption(self, computedRule, 'fontBold');
const fontItalic = buildStyleOption(self, computedRule, 'fontItalic');
const fontUnderline = buildStyleOption(self, computedRule, 'fontUnderline');
const fontStrikethrough = buildStyleOption(self, computedRule, 'fontStrikethrough');
return dom('div.gridview_row',
dom.autoDispose(isRowActive),
dom.autoDispose(computedFlags),
dom.autoDispose(computedRule),
dom.autoDispose(textColor),
dom.autoDispose(fillColor),
dom.autoDispose(zebraColor),
dom.autoDispose(fontBold),
dom.autoDispose(fontItalic),
dom.autoDispose(fontUnderline),
dom.autoDispose(fontStrikethrough),
// rowid dom
dom('div.gridview_data_row_num',
@@ -1159,6 +1195,13 @@ GridView.prototype.buildDom = function() {
kd.toggleClass('record-add', row._isAddRow),
kd.style('borderLeftWidth', v.borderWidthPx),
kd.style('borderBottomWidth', v.borderWidthPx),
kd.toggleClass('font-bold', fontBold),
kd.toggleClass('font-underline', fontUnderline),
kd.toggleClass('font-italic', fontItalic),
kd.toggleClass('font-strikethrough', fontStrikethrough),
kd.style('--grist-row-background-color', fillColor),
kd.style('--grist-row-background-color-zebra', zebraColor),
kd.style('--grist-row-color', textColor),
//These are grabbed from v.optionsObj at start of GridView buildDom
kd.toggleClass('record-hlines', vHorizontalGridlines),
kd.toggleClass('record-vlines', vVerticalGridlines),
@@ -1611,6 +1654,15 @@ GridView.prototype._duplicateRows = async function() {
}
}
function buildStyleOption(owner, computedRule, optionName) {
return ko.computed(() => {
if (owner.isDisposed()) { return null; }
const rule = computedRule();
if (!rule || !rule.style) { return ''; }
return rule.style[optionName] || '';
});
}
// Helper to show tooltip over column selection in the full edit mode.
class HoverColumnTooltip {
constructor(el) {
@@ -1631,4 +1683,20 @@ class HoverColumnTooltip {
}
}
// Simple function that calculates good color for zebra stripes.
function calcZebra(hex) {
if (!hex || hex.length !== 7) { return hex; }
// HSL: [HUE, SATURATION, LIGHTNESS]
const hsl = convert.hex.hsl(hex.substr(1));
// For bright color, we will make it darker. Value was picked by hand, to
// produce #f8f8f8f out of #ffffff.
if (hsl[2] > 50) { hsl[2] -= 2.6; }
// For darker color, we will make it brighter. Value was picked by hand to look
// good for the darkest colors in our palette.
else if (hsl[2] > 1) { hsl[2] += 11; }
// For very dark colors
else { hsl[2] += 16; }
return `#${convert.hsl.hex(hsl)}`;
}
module.exports = GridView;

View File

@@ -798,7 +798,8 @@ export class Importer extends DisposableWithEvents {
const editRow = vsi?.moveEditRowToCursor();
const editorHolder = openFormulaEditor({
gristDoc: this._gristDoc,
field,
column: field.column(),
editingFormula: field.editingFormula,
refElem,
editRow,
setupCleanup: this._setupFormulaEditorCleanup.bind(this),
@@ -819,7 +820,7 @@ export class Importer extends DisposableWithEvents {
* focus.
*/
private _setupFormulaEditorCleanup(
owner: MultiHolder, _doc: GristDoc, field: ViewFieldRec, _saveEdit: () => Promise<unknown>
owner: MultiHolder, _doc: GristDoc, editingFormula: ko.Computed<boolean>, _saveEdit: () => Promise<unknown>
) {
const saveEdit = () => _saveEdit().catch(reportError);
@@ -828,7 +829,7 @@ export class Importer extends DisposableWithEvents {
owner.onDispose(() => {
this.off('importer_focus', saveEdit);
field.editingFormula(false);
editingFormula(false);
});
}

View File

@@ -10,7 +10,7 @@ import {ColumnTransform} from 'app/client/components/ColumnTransform';
import {GristDoc} from 'app/client/components/GristDoc';
import * as TypeConversion from 'app/client/components/TypeConversion';
import {reportError} from 'app/client/models/errors';
import {cssButtonRow} from 'app/client/ui/RightPanel';
import {cssButtonRow} from 'app/client/ui/RightPanelStyles';
import {basicButton, primaryButton} from 'app/client/ui2018/buttons';
import {testId} from 'app/client/ui2018/cssVars';
import {FieldBuilder} from 'app/client/widgets/FieldBuilder';

View File

@@ -14,7 +14,7 @@ const {addToSort} = require('app/client/lib/sortUtil');
const {updatePositions} = require('app/client/lib/sortUtil');
const {attachColumnFilterMenu} = require('app/client/ui/ColumnFilterMenu');
const {addFilterMenu} = require('app/client/ui/FilterBar');
const {cssIcon, cssRow} = require('app/client/ui/RightPanel');
const {cssIcon, cssRow} = require('app/client/ui/RightPanelStyles');
const {basicButton, primaryButton} = require('app/client/ui2018/buttons');
const {labeledLeftSquareCheckbox} = require("app/client/ui2018/checkbox");
const {colors} = require('app/client/ui2018/cssVars');

View File

@@ -4,3 +4,4 @@
import ViewConfigTab from 'app/client/components/ViewConfigTab';
import * as FieldConfig from 'app/client/ui/FieldConfig';
export {ViewConfigTab, FieldConfig};
export {ConditionalStyle} from 'app/client/widgets/ConditionalStyle';

View File

@@ -19,7 +19,8 @@
selected fields - this still remains white.
TODO: consider making this color the single source
*/
background: white;
background: var(--grist-row-background-color, white);
color: var(--grist-row-color, black);
}
.record.record-hlines { /* Overwrites style, width set on element */
@@ -27,7 +28,7 @@
}
.record.record-zebra.record-even {
background-color: #f8f8f8;
background-color: var(--grist-row-background-color-zebra, #f8f8f8);
}
.record.record-add {
@@ -71,12 +72,12 @@
width: 100%;
height: 100%;
background-color: var(--grist-diff-background-color, var(--grist-cell-background-color, unset));
--grist-actual-cell-color: var(--grist-diff-color, var(--grist-cell-color));
color: var(--grist-actual-cell-color, black);
--grist-actual-cell-color: var(--grist-diff-color, var(--grist-cell-color, var(--grist-row-color)));
color: var(--grist-actual-cell-color, unset);
}
.field.selected .field_clip {
mix-blend-mode: darken;
mix-blend-mode: luminosity;
}
.field_clip.invalid, .field_clip.field-error-from-style {