(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:
Jarosław Sadziński
2022-10-14 12:07:19 +02:00
parent ab3cdb62ac
commit 8be920dd25
36 changed files with 2579 additions and 395 deletions

View File

@@ -6,7 +6,7 @@ import {IconName} from 'app/client/ui2018/IconList';
import {icon} from 'app/client/ui2018/icons';
import {cssSelectBtn} from 'app/client/ui2018/select';
import {isValidHex} from 'app/common/gutil';
import {Computed, Disposable, dom, Observable, onKeyDown, styled} from 'grainjs';
import {BindableValue, Computed, Disposable, dom, Observable, onKeyDown, styled} from 'grainjs';
import {defaultMenuOptions, IOpenController, setPopupToCreateDom} from 'popweasel';
export interface StyleOptions {
@@ -44,12 +44,25 @@ export class ColorOption {
*/
export function colorSelect(
styleOptions: StyleOptions,
onSave: () => Promise<void>,
placeholder = 'Default cell style'): Element {
options: {
// Handler to save the style.
onSave: () => Promise<void>,
// Invoked when user opens the color picker.
onOpen?: () => void,
// Invoked when user closes the color picker without saving.
onRevert?: () => void,
placeholder?: BindableValue<string>
}): Element {
const {
textColor,
fillColor,
} = styleOptions;
const {
onSave,
onOpen,
onRevert,
placeholder = 'Default cell style',
} = options;
const selectBtn = cssSelectBtn(
cssContent(
cssButtonIcon(
@@ -63,13 +76,16 @@ export function colorSelect(
cssLightBorder.cls(''),
testId('btn-icon'),
),
placeholder,
dom.text(placeholder),
),
icon('Dropdown'),
testId('color-select'),
);
const domCreator = (ctl: IOpenController) => buildColorPicker(ctl, styleOptions, onSave);
const domCreator = (ctl: IOpenController) => {
onOpen?.();
return buildColorPicker(ctl, styleOptions, onSave, onRevert);
};
setPopupToCreateDom(selectBtn, domCreator, {...defaultMenuOptions, placement: 'bottom-end'});
return selectBtn;
@@ -105,7 +121,9 @@ function buildColorPicker(ctl: IOpenController,
fontItalic,
fontStrikethrough
}: StyleOptions,
onSave: () => Promise<void>): Element {
onSave: () => Promise<void>,
onRevert?: () => void,
): Element {
const textColorModel = ColorModel.create(null, textColor.color);
const fillColorModel = ColorModel.create(null, fillColor.color);
const fontBoldModel = BooleanModel.create(null, fontBold);
@@ -119,7 +137,10 @@ function buildColorPicker(ctl: IOpenController,
const notChanged = Computed.create(null, use => models.every(m => use(m.needsSaving) === false));
function revert() {
models.forEach(m => m.revert());
onRevert?.();
if (!onRevert) {
models.forEach(m => m.revert());
}
ctl.close();
}
@@ -129,8 +150,10 @@ function buildColorPicker(ctl: IOpenController,
// TODO: disable the trigger btn while saving
await onSave();
} catch (e) {
/* Does no logging: onSave() callback is expected to handle their reporting */
models.forEach(m => m.revert());
onRevert?.();
if (!onRevert) {
models.forEach(m => m.revert());
}
}
}
models.forEach(m => m.dispose());

View File

@@ -25,6 +25,8 @@ export type ISelectorOption<T> = (T & string) | ISelectorOptionFull<T>;
* A "light" style is supported in CSS by passing cssButtonSelect.cls('-light') as an additional
* argument.
*
* A disabled state is supported by passing cssButtonSelect.cls('-disabled').
*
* Usage:
* const fruit = observable("apple");
* buttonSelect(fruit, ["apple", "banana", "mango"]);
@@ -61,13 +63,13 @@ export function buttonToggleSelect<T>(
/**
* Pre-made text alignment selector.
*/
export function alignmentSelect(obs: Observable<string>) {
export function alignmentSelect(obs: Observable<string>, ...domArgs: DomElementArg[]) {
const alignments: Array<ISelectorOption<string>> = [
{value: 'left', icon: 'LeftAlign'},
{value: 'center', icon: 'CenterAlign'},
{value: 'right', icon: 'RightAlign'}
];
return buttonSelect(obs, alignments, {}, testId('alignment-select'));
return buttonSelect(obs, alignments, {}, testId('alignment-select'), ...domArgs);
}
/**
@@ -216,6 +218,14 @@ const cssSelectorBtn = styled('div', `
border: none;
background-color: ${theme.hover};
}
.${cssButtonSelect.className}-disabled > &,
.${cssButtonSelect.className}-disabled > &:hover {
--icon-color: ${theme.rightPanelToggleButtonDisabledFg};
color: ${theme.rightPanelToggleButtonDisabledFg};
background-color: ${theme.rightPanelToggleButtonDisabledBg};
border-color: ${theme.buttonGroupBorder};
pointer-events: none;
}
`);
const cssSelectorLabel = styled('span', `