mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Support reordering conditional styles
Summary: Conditional style rules can now be reordered by dragging and dropping them. Test Plan: Browser test. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D4251
This commit is contained in:
parent
85f1040439
commit
e299f4466b
@ -198,11 +198,10 @@ const cssControlLabel = styled('div', `
|
|||||||
|
|
||||||
// TODO: reuse them
|
// TODO: reuse them
|
||||||
const cssDragRow = styled('div', `
|
const cssDragRow = styled('div', `
|
||||||
display: flex !important;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 0 16px 0px 0px;
|
margin: 0 16px 0px 0px;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
cursor: grab;
|
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const cssFieldEntry = styled('div', `
|
const cssFieldEntry = styled('div', `
|
||||||
|
@ -136,6 +136,15 @@ div:hover > .kf_tooltip {
|
|||||||
|
|
||||||
.kf_draggable {
|
.kf_draggable {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kf_draggable--vertical {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the handle as grabbable, or the draggable element itself (if there is no handle). */
|
||||||
|
.ui-sortable-handle,
|
||||||
|
.kf_draggable:not(:has(.ui-sortable-handle)) {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -724,3 +733,7 @@ fieldset:disabled {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kf_drag_container.ui-sortable {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
@ -436,6 +436,8 @@ exports.collapsible = function(contentFunc, isMountedCollapsed) {
|
|||||||
* function on click.
|
* function on click.
|
||||||
* @param {String} options.axis Determines if the list is displayed vertically 'y' or
|
* @param {String} options.axis Determines if the list is displayed vertically 'y' or
|
||||||
* horizontally 'x'.
|
* horizontally 'x'.
|
||||||
|
* @param {String} options.handle The handle of the draggable. Defaults to the element
|
||||||
|
* itself.
|
||||||
* @param {Boolean|Function} drag_indicator Include the drag indicator. Defaults to true. Accepts
|
* @param {Boolean|Function} drag_indicator Include the drag indicator. Defaults to true. Accepts
|
||||||
* also a function that returns a dom element. In which
|
* also a function that returns a dom element. In which
|
||||||
* case, it will be used to create the drag indicator.
|
* case, it will be used to create the drag indicator.
|
||||||
@ -473,13 +475,13 @@ exports.draggableList = function(contentArray, itemCreateFunc, options) {
|
|||||||
// Fix for JQueryUI bug where mousedown on draggable elements fail to blur
|
// Fix for JQueryUI bug where mousedown on draggable elements fail to blur
|
||||||
// active element. See: https://bugs.jqueryui.com/ticket/4261
|
// active element. See: https://bugs.jqueryui.com/ticket/4261
|
||||||
dom.on('mousedown', () => G.document.activeElement.blur()),
|
dom.on('mousedown', () => G.document.activeElement.blur()),
|
||||||
|
kd.toggleClass('kf_draggable--vertical', options.axis === 'y'),
|
||||||
kd.cssClass(options.itemClass),
|
kd.cssClass(options.itemClass),
|
||||||
(options.drag_indicator ?
|
(options.drag_indicator ?
|
||||||
(typeof options.drag_indicator === 'boolean' ?
|
(typeof options.drag_indicator === 'boolean' ?
|
||||||
dom('span.kf_drag_indicator.glyphicon.glyphicon-option-vertical') :
|
dom('span.kf_drag_indicator.glyphicon.glyphicon-option-vertical') :
|
||||||
options.drag_indicator()
|
options.drag_indicator()
|
||||||
) : null),
|
) : null),
|
||||||
kd.style('display', options.axis === 'x' ? 'inline-block' : 'block'),
|
|
||||||
kd.domData('model', item),
|
kd.domData('model', item),
|
||||||
kd.maybe(removeFunc !== undefined && options.removeButton, function() {
|
kd.maybe(removeFunc !== undefined && options.removeButton, function() {
|
||||||
return dom('span.drag_delete.glyphicon.glyphicon-remove',
|
return dom('span.drag_delete.glyphicon.glyphicon-remove',
|
||||||
@ -502,7 +504,8 @@ exports.draggableList = function(contentArray, itemCreateFunc, options) {
|
|||||||
axis: options.axis,
|
axis: options.axis,
|
||||||
tolerance: "pointer",
|
tolerance: "pointer",
|
||||||
forcePlaceholderSize: true,
|
forcePlaceholderSize: true,
|
||||||
placeholder: 'kf_draggable__placeholder--' + (options.axis === 'x' ? 'horizontal' : 'vertical')
|
placeholder: 'kf_draggable__placeholder--' + (options.axis === 'x' ? 'horizontal' : 'vertical'),
|
||||||
|
handle: options.handle,
|
||||||
});
|
});
|
||||||
if (reorderFunc === undefined) {
|
if (reorderFunc === undefined) {
|
||||||
G.$(list).sortable("option", {disabled: true});
|
G.$(list).sortable("option", {disabled: true});
|
||||||
|
@ -11,7 +11,7 @@ export interface RuleOwner {
|
|||||||
// If this field (or column) has a list of conditional styling rules.
|
// If this field (or column) has a list of conditional styling rules.
|
||||||
hasRules: ko.Computed<boolean>;
|
hasRules: ko.Computed<boolean>;
|
||||||
// List of rules.
|
// List of rules.
|
||||||
rulesList: ko.Computed<[GristObjCode.List, ...number[]] | null>;
|
rulesList: modelUtil.KoSaveableObservable<[GristObjCode.List, ...number[]] | null>;
|
||||||
// List of columns that are used as rules for conditional styles.
|
// List of columns that are used as rules for conditional styles.
|
||||||
rulesCols: ko.Computed<ColumnRec[]>;
|
rulesCols: ko.Computed<ColumnRec[]>;
|
||||||
// List of columns ids that are used as rules for conditional styles.
|
// List of columns ids that are used as rules for conditional styles.
|
||||||
|
@ -293,7 +293,10 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.tableId = ko.pureComputed(() => this.column().table().tableId());
|
this.tableId = ko.pureComputed(() => this.column().table().tableId());
|
||||||
this.rulesList = ko.pureComputed(() => this._fieldOrColumn().rules());
|
this.rulesList = modelUtil.savingComputed({
|
||||||
|
read: () => this._fieldOrColumn().rules(),
|
||||||
|
write: (setter, val) => setter(this._fieldOrColumn().rules, val)
|
||||||
|
});
|
||||||
this.rulesCols = refListRecords(docModel.columns, ko.pureComputed(() => this._fieldOrColumn().rules()));
|
this.rulesCols = refListRecords(docModel.columns, ko.pureComputed(() => this._fieldOrColumn().rules()));
|
||||||
this.rulesColsIds = ko.pureComputed(() => this.rulesCols().map(c => c.colId()));
|
this.rulesColsIds = ko.pureComputed(() => this.rulesCols().map(c => c.colId()));
|
||||||
this.rulesStyles = modelUtil.fieldWithDefault(
|
this.rulesStyles = modelUtil.fieldWithDefault(
|
||||||
|
@ -824,6 +824,10 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
|
|||||||
|
|
||||||
this.tableId = this.autoDispose(ko.pureComputed(() => this.table().tableId()));
|
this.tableId = this.autoDispose(ko.pureComputed(() => this.table().tableId()));
|
||||||
const rawSection = this.autoDispose(ko.pureComputed(() => this.table().rawViewSection()));
|
const rawSection = this.autoDispose(ko.pureComputed(() => this.table().rawViewSection()));
|
||||||
|
this.rulesList = modelUtil.savingComputed({
|
||||||
|
read: () => rawSection().rules(),
|
||||||
|
write: (setter, val) => setter(rawSection().rules, val)
|
||||||
|
});
|
||||||
this.rulesCols = refListRecords(docModel.columns, ko.pureComputed(() => rawSection().rules()));
|
this.rulesCols = refListRecords(docModel.columns, ko.pureComputed(() => rawSection().rules()));
|
||||||
this.rulesColsIds = ko.pureComputed(() => this.rulesCols().map(c => c.colId()));
|
this.rulesColsIds = ko.pureComputed(() => this.rulesCols().map(c => c.colId()));
|
||||||
this.rulesStyles = modelUtil.savingComputed({
|
this.rulesStyles = modelUtil.savingComputed({
|
||||||
|
@ -270,7 +270,7 @@ export class SortConfig extends Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cssDragRow = styled('div', `
|
const cssDragRow = styled('div', `
|
||||||
display: flex !important;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 0 16px 0px 0px;
|
margin: 0 16px 0px 0px;
|
||||||
& > .kf_draggable_content {
|
& > .kf_draggable_content {
|
||||||
|
@ -440,7 +440,7 @@ function unselectDeletedFields(selection: Set<number>, event: {deleted: IField[]
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const cssDragRow = styled('div', `
|
export const cssDragRow = styled('div', `
|
||||||
display: flex !important;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 0 16px 0px 0px;
|
margin: 0 16px 0px 0px;
|
||||||
& > .kf_draggable_content {
|
& > .kf_draggable_content {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import * as kf from 'app/client/lib/koForm';
|
||||||
import {makeT} from 'app/client/lib/localization';
|
import {makeT} from 'app/client/lib/localization';
|
||||||
import {GristDoc} from 'app/client/components/GristDoc';
|
import {GristDoc} from 'app/client/components/GristDoc';
|
||||||
import {ColumnRec} from 'app/client/models/DocModel';
|
import {ColumnRec} from 'app/client/models/DocModel';
|
||||||
@ -10,22 +11,31 @@ import {withInfoTooltip} from 'app/client/ui/tooltips';
|
|||||||
import {textButton} from 'app/client/ui2018/buttons';
|
import {textButton} from 'app/client/ui2018/buttons';
|
||||||
import {ColorOption, colorSelect} from 'app/client/ui2018/ColorSelect';
|
import {ColorOption, colorSelect} from 'app/client/ui2018/ColorSelect';
|
||||||
import {theme, vars} from 'app/client/ui2018/cssVars';
|
import {theme, vars} from 'app/client/ui2018/cssVars';
|
||||||
|
import {cssDragger} from 'app/client/ui2018/draggableList';
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
import {setupEditorCleanup} from 'app/client/widgets/FieldEditor';
|
import {setupEditorCleanup} from 'app/client/widgets/FieldEditor';
|
||||||
import {cssError, openFormulaEditor} from 'app/client/widgets/FormulaEditor';
|
import {cssError, openFormulaEditor} from 'app/client/widgets/FormulaEditor';
|
||||||
import {isRaisedException, isValidRuleValue} from 'app/common/gristTypes';
|
import {isRaisedException, isValidRuleValue} from 'app/common/gristTypes';
|
||||||
import {RowRecord} from 'app/plugin/GristData';
|
import {GristObjCode, RowRecord} from 'app/plugin/GristData';
|
||||||
|
import {decodeObject} from 'app/plugin/objtypes';
|
||||||
import {Computed, Disposable, dom, DomContents, makeTestId, Observable, styled} from 'grainjs';
|
import {Computed, Disposable, dom, DomContents, makeTestId, Observable, styled} from 'grainjs';
|
||||||
import debounce = require('lodash/debounce');
|
import debounce = require('lodash/debounce');
|
||||||
|
|
||||||
const testId = makeTestId('test-widget-style-');
|
const testId = makeTestId('test-widget-style-');
|
||||||
const t = makeT('ConditionalStyle');
|
const t = makeT('ConditionalStyle');
|
||||||
|
|
||||||
|
type ColumnRecAndIndex = [ColumnRec, number];
|
||||||
|
|
||||||
export class ConditionalStyle extends Disposable {
|
export class ConditionalStyle extends Disposable {
|
||||||
// Holds data from currently selected record (holds data only when this field has conditional styles).
|
// Holds data from currently selected record (holds data only when this field has conditional styles).
|
||||||
private _currentRecord: Computed<RowRecord | undefined>;
|
private _currentRecord: Computed<RowRecord | undefined>;
|
||||||
// Helper field for refreshing current record data.
|
// Helper field for refreshing current record data.
|
||||||
private _dataChangeTrigger = Observable.create(this, 0);
|
private _dataChangeTrigger = Observable.create(this, 0);
|
||||||
|
// Rules columns with their respective rule index.
|
||||||
|
private _rulesColsWithIndex: Computed<ColumnRecAndIndex[]> = Computed.create(this, (use) => {
|
||||||
|
const rulesCols = use(this._ruleOwner.rulesCols);
|
||||||
|
return rulesCols.map((col, i) => [col, i]);
|
||||||
|
});
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _label: string,
|
private _label: string,
|
||||||
@ -83,17 +93,38 @@ export class ConditionalStyle extends Disposable {
|
|||||||
dom.hide(use => use(this._ruleOwner.hasRules))
|
dom.hide(use => use(this._ruleOwner.hasRules))
|
||||||
),
|
),
|
||||||
dom.domComputedOwned(
|
dom.domComputedOwned(
|
||||||
use => use(this._ruleOwner.rulesCols),
|
use => use(this._rulesColsWithIndex),
|
||||||
(owner, rules) =>
|
(owner, rules) =>
|
||||||
cssRuleList(
|
cssRuleList(
|
||||||
dom.show(use => rules.length > 0 && (!this._disabled || !use(this._disabled))),
|
dom.show(use => rules.length > 0 && (!this._disabled || !use(this._disabled))),
|
||||||
...rules.map((column, ruleIndex) => {
|
kf.draggableList(rules, (rule: ColumnRecAndIndex) => this._buildRule(owner, rule), {
|
||||||
const textColor = this._buildStyleOption(owner, ruleIndex, 'textColor');
|
reorder: this._reorderRule.bind(this),
|
||||||
const fillColor = this._buildStyleOption(owner, ruleIndex, 'fillColor');
|
removeButton: false,
|
||||||
const fontBold = this._buildStyleOption(owner, ruleIndex, 'fontBold');
|
drag_indicator: cssDragger,
|
||||||
const fontItalic = this._buildStyleOption(owner, ruleIndex, 'fontItalic');
|
itemClass: cssDragRow.className,
|
||||||
const fontUnderline = this._buildStyleOption(owner, ruleIndex, 'fontUnderline');
|
handle: `.${cssDragger.className}`,
|
||||||
const fontStrikethrough = this._buildStyleOption(owner, ruleIndex, 'fontStrikethrough');
|
}),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
cssRow(
|
||||||
|
textButton(t('Add another rule'),
|
||||||
|
dom.on('click', () => this._ruleOwner.addEmptyRule()),
|
||||||
|
testId('add-another-rule'),
|
||||||
|
dom.prop('disabled', use => this._disabled && use(this._disabled))
|
||||||
|
),
|
||||||
|
dom.show(use => use(this._ruleOwner.hasRules))
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _buildRule(owner: Disposable, rule: ColumnRecAndIndex) {
|
||||||
|
const [column, index] = rule;
|
||||||
|
const textColor = this._buildStyleOption(owner, index, 'textColor');
|
||||||
|
const fillColor = this._buildStyleOption(owner, index, 'fillColor');
|
||||||
|
const fontBold = this._buildStyleOption(owner, index, 'fontBold');
|
||||||
|
const fontItalic = this._buildStyleOption(owner, index, 'fontItalic');
|
||||||
|
const fontUnderline = this._buildStyleOption(owner, index, 'fontUnderline');
|
||||||
|
const fontStrikethrough = this._buildStyleOption(owner, index, 'fontStrikethrough');
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
// This will save both options.
|
// This will save both options.
|
||||||
await this._ruleOwner.rulesStyles.save();
|
await this._ruleOwner.rulesStyles.save();
|
||||||
@ -116,16 +147,16 @@ export class ConditionalStyle extends Disposable {
|
|||||||
t('Rule must return True or False'));
|
t('Rule must return True or False'));
|
||||||
});
|
});
|
||||||
return dom('div',
|
return dom('div',
|
||||||
testId(`conditional-rule-${ruleIndex}`),
|
testId(`conditional-rule-${index}`),
|
||||||
testId(`conditional-rule`), // for testing
|
testId(`conditional-rule`), // for testing
|
||||||
cssLineLabel('IF...'),
|
cssLineLabel(t('IF...')),
|
||||||
cssColumnsRow(
|
cssColumnsRow(
|
||||||
cssLeftColumn(
|
cssLeftColumn(
|
||||||
this._buildRuleFormula(column.formula, column, hasError),
|
this._buildRuleFormula(column.formula, column, hasError),
|
||||||
cssRuleError(
|
cssRuleError(
|
||||||
dom.text(errorMessage),
|
dom.text(errorMessage),
|
||||||
dom.show(hasError),
|
dom.show(hasError),
|
||||||
testId(`rule-error-${ruleIndex}`),
|
testId(`rule-error-${index}`),
|
||||||
),
|
),
|
||||||
colorSelect(
|
colorSelect(
|
||||||
{
|
{
|
||||||
@ -137,29 +168,45 @@ export class ConditionalStyle extends Disposable {
|
|||||||
fontStrikethrough
|
fontStrikethrough
|
||||||
}, {
|
}, {
|
||||||
onSave: save,
|
onSave: save,
|
||||||
placeholder: this._label || 'Conditional Style',
|
placeholder: this._label || t('Conditional Style'),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
cssRemoveButton(
|
cssRemoveButton(
|
||||||
'Remove',
|
'Remove',
|
||||||
testId(`remove-rule-${ruleIndex}`),
|
testId(`remove-rule-${index}`),
|
||||||
dom.on('click', () => this._ruleOwner.removeRule(ruleIndex))
|
dom.on('click', () => this._ruleOwner.removeRule(index))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
})
|
}
|
||||||
)
|
|
||||||
),
|
private async _reorderRule(rule: ColumnRecAndIndex, nextRule: ColumnRecAndIndex | null) {
|
||||||
cssRow(
|
const rulesList = decodeObject(this._ruleOwner.rulesList.peek());
|
||||||
textButton(t('Add another rule'),
|
if (!Array.isArray(rulesList) || rulesList.length === 0) {
|
||||||
dom.on('click', () => this._ruleOwner.addEmptyRule()),
|
throw new Error('No conditional style rules');
|
||||||
testId('add-another-rule'),
|
}
|
||||||
dom.prop('disabled', use => this._disabled && use(this._disabled))
|
|
||||||
),
|
const ruleColRef = rule[0].id.peek();
|
||||||
dom.show(use => use(this._ruleOwner.hasRules))
|
const nextRuleColRef = nextRule?.[0].id.peek();
|
||||||
),
|
const rulesStyles = [...this._ruleOwner.rulesStyles.peek()];
|
||||||
];
|
const ruleColRefIndex = rulesList.indexOf(ruleColRef);
|
||||||
|
|
||||||
|
// Remove the rule.
|
||||||
|
rulesList.splice(ruleColRefIndex, 1);
|
||||||
|
const [ruleStyle] = rulesStyles.splice(ruleColRefIndex, 1);
|
||||||
|
|
||||||
|
// Insert the removed rule before the next rule.
|
||||||
|
const nextRuleColRefIndex = nextRuleColRef ? rulesList.indexOf(nextRuleColRef) : rulesList.length;
|
||||||
|
rulesList.splice(nextRuleColRefIndex, 0, ruleColRef);
|
||||||
|
rulesStyles.splice(nextRuleColRefIndex, 0, ruleStyle);
|
||||||
|
|
||||||
|
await this._gristDoc.docModel.docData.bundleActions("Reorder conditional rules", () =>
|
||||||
|
Promise.all([
|
||||||
|
this._ruleOwner.rulesList.setAndSave([GristObjCode.List, ...rulesList]),
|
||||||
|
this._ruleOwner.rulesStyles.setAndSave(rulesStyles),
|
||||||
|
])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _buildStyleOption<T extends keyof Style>(owner: Disposable, index: number, option: T) {
|
private _buildStyleOption<T extends keyof Style>(owner: Disposable, index: number, option: T) {
|
||||||
@ -213,7 +260,7 @@ const cssIcon = styled(icon, `
|
|||||||
|
|
||||||
const cssLabel = styled('div', `
|
const cssLabel = styled('div', `
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin: 16px 16px 12px 16px;
|
margin: 16px 16px 12px 0px;
|
||||||
color: ${theme.text};
|
color: ${theme.text};
|
||||||
font-size: ${vars.xsmallFontSize};
|
font-size: ${vars.xsmallFontSize};
|
||||||
`);
|
`);
|
||||||
@ -265,8 +312,7 @@ const cssRuleError = styled(cssError, `
|
|||||||
|
|
||||||
const cssColumnsRow = styled(cssRow, `
|
const cssColumnsRow = styled(cssRow, `
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
margin-top: 0px;
|
margin: 0px 16px 0px 0px;
|
||||||
margin-bottom: 0px;
|
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const cssLeftColumn = styled('div', `
|
const cssLeftColumn = styled('div', `
|
||||||
@ -276,3 +322,13 @@ const cssLeftColumn = styled('div', `
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const cssDragRow = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
& > .kf_draggable_content {
|
||||||
|
margin: 4px 0;
|
||||||
|
flex: 1 1 0px;
|
||||||
|
min-width: 0px;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
@ -2349,6 +2349,10 @@ export function setFillColor(color: string) {
|
|||||||
return setColor(driver.find('.test-fill-input'), color);
|
return setColor(driver.find('.test-fill-input'), color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getStyleRuleAt(nr: number) {
|
||||||
|
return driver.find(`.test-widget-style-conditional-rule-${nr}`);
|
||||||
|
}
|
||||||
|
|
||||||
export async function styleRulesCount() {
|
export async function styleRulesCount() {
|
||||||
const rules = await driver.findAll('.test-widget-style-conditional-rule');
|
const rules = await driver.findAll('.test-widget-style-conditional-rule');
|
||||||
return rules.length;
|
return rules.length;
|
||||||
|
Loading…
Reference in New Issue
Block a user