(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

@@ -112,15 +112,24 @@ export class AttachmentsWidget extends NewAbstractWidget {
}
public buildConfigDom(): Element {
const inputRange = input(fromKo(this._height), {onInput: true}, {
style: 'margin: 0 5px;',
type: 'range',
min: '16',
max: '96',
value: '36'
}, testId('thumbnail-size'));
const options = this.field.config.options;
const height = options.prop('height');
const inputRange = input(
fromKo(height),
{onInput: true}, {
style: 'margin: 0 5px;',
type: 'range',
min: '16',
max: '96',
value: '36'
},
testId('thumbnail-size'),
// When multiple columns are selected, we can only edit height when all
// columns support it.
dom.prop('disabled', use => use(options.disabled('height'))),
);
// Save the height on change event (when the user releases the drag button)
onElem(inputRange, 'change', (ev: any) => { this._height.setAndSave(ev.target.value); });
onElem(inputRange, 'change', (ev: any) => { height.setAndSave(ev.target.value).catch(reportError); });
return cssRow(
sizeLabel('Size'),
inputRange

View File

@@ -1,11 +1,12 @@
import {allCommands} from 'app/client/components/commands';
import {GristDoc} from 'app/client/components/GristDoc';
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
import {Style} from 'app/client/models/Styles';
import {textButton} from 'app/client/ui2018/buttons';
import {ColorOption, colorSelect} from 'app/client/ui2018/ColorSelect';
import {theme, vars} from 'app/client/ui2018/cssVars';
import {ConditionalStyle} from 'app/client/widgets/ConditionalStyle';
import {Disposable, dom, DomContents, fromKo, MultiHolder, Observable, styled} from 'grainjs';
import {Computed, Disposable, dom, DomContents, fromKo, MultiHolder, Observable, styled} from 'grainjs';
export class CellStyle extends Disposable {
private _textColor: Observable<string|undefined>;
@@ -21,16 +22,29 @@ export class CellStyle extends Disposable {
private _defaultTextColor: string
) {
super();
this._textColor = fromKo(this._field.textColor);
this._fillColor = fromKo(this._field.fillColor);
this._fontBold = fromKo(this._field.fontBold);
this._fontUnderline = fromKo(this._field.fontUnderline);
this._fontItalic = fromKo(this._field.fontItalic);
this._fontStrikethrough = fromKo(this._field.fontStrikethrough);
this._textColor = fromKo(this._field.config.textColor);
this._fillColor = fromKo(this._field.config.fillColor);
this._fontBold = fromKo(this._field.config.fontBold);
this._fontUnderline = fromKo(this._field.config.fontUnderline);
this._fontItalic = fromKo(this._field.config.fontItalic);
this._fontStrikethrough = fromKo(this._field.config.fontStrikethrough);
}
public buildDom(): DomContents {
const holder = new MultiHolder();
const hasMixedStyle = Computed.create(holder, use => {
if (!use(this._field.config.multiselect)) { return false; }
const commonStyle = [
use(this._field.config.options.mixed('textColor')),
use(this._field.config.options.mixed('fillColor')),
use(this._field.config.options.mixed('fontBold')),
use(this._field.config.options.mixed('fontUnderline')),
use(this._field.config.options.mixed('fontItalic')),
use(this._field.config.options.mixed('fontStrikethrough'))
];
return commonStyle.some(Boolean);
});
let state: Style[]|null = null;
return [
cssLine(
cssLabel('CELL STYLE', dom.autoDispose(holder)),
@@ -49,12 +63,16 @@ export class CellStyle extends Disposable {
fontItalic: this._fontItalic,
fontUnderline: this._fontUnderline,
fontStrikethrough: this._fontStrikethrough
},
// Calling `field.widgetOptionsJson.save()` saves both fill and text color settings.
() => this._field.widgetOptionsJson.save()
}, {
// Calling `field.config.options.save()` saves all options at once.
onSave: () => this._field.config.options.save(),
onOpen: () => state = this._field.config.copyStyles(),
onRevert: () => this._field.config.setStyles(state),
placeholder: use => use(hasMixedStyle) ? 'Mixed style' : 'Default cell style'
}
)
),
dom.create(ConditionalStyle, "Cell Style", this._field, this._gristDoc)
dom.create(ConditionalStyle, "Cell Style", this._field, this._gristDoc, fromKo(this._field.config.multiselect))
];
}
}

View File

@@ -1,5 +1,6 @@
import {createGroup} from 'app/client/components/commands';
import {ACIndexImpl, ACItem, ACResults, buildHighlightedDom, normalizeText, HighlightFunc} from 'app/client/lib/ACIndex';
import {ACIndexImpl, ACItem, ACResults,
buildHighlightedDom, HighlightFunc, normalizeText} from 'app/client/lib/ACIndex';
import {IAutocompleteOptions} from 'app/client/lib/autocomplete';
import {IToken, TokenField, tokenFieldStyles} from 'app/client/lib/TokenField';
import {colors, testId, theme} from 'app/client/ui2018/cssVars';
@@ -10,10 +11,10 @@ import {FieldOptions, NewBaseEditor} from 'app/client/widgets/NewBaseEditor';
import {csvEncodeRow} from 'app/common/csvFormat';
import {CellValue} from "app/common/DocActions";
import {decodeObject, encodeObject} from 'app/plugin/objtypes';
import {dom, styled} from 'grainjs';
import {ChoiceOptions, getRenderFillColor, getRenderTextColor} from 'app/client/widgets/ChoiceTextBox';
import {choiceToken, cssChoiceACItem, cssChoiceToken} from 'app/client/widgets/ChoiceToken';
import {icon} from 'app/client/ui2018/icons';
import {dom, styled} from 'grainjs';
export class ChoiceItem implements ACItem, IToken {
public cleanText: string = normalizeText(this.label);

View File

@@ -6,7 +6,7 @@ import {colors, testId, theme} from 'app/client/ui2018/cssVars';
import {editableLabel} from 'app/client/ui2018/editableLabel';
import {icon} from 'app/client/ui2018/icons';
import {ChoiceOptionsByName, IChoiceOptions} from 'app/client/widgets/ChoiceTextBox';
import {Computed, Disposable, dom, DomContents, DomElementArg, Holder, Observable, styled} from 'grainjs';
import {Computed, Disposable, dom, DomContents, DomElementArg, Holder, MultiHolder, Observable, styled} from 'grainjs';
import {createCheckers, iface, ITypeSuite, opt, union} from 'ts-interface-checker';
import isEqual = require('lodash/isEqual');
import uniqBy = require('lodash/uniqBy');
@@ -95,7 +95,8 @@ export class ChoiceListEntry extends Disposable {
private _values: Observable<string[]>,
private _choiceOptionsByName: Observable<ChoiceOptionsByName>,
private _onSave: (values: string[], choiceOptions: ChoiceOptionsByName, renames: Record<string, string>) => void,
private _disabled: Observable<boolean>
private _disabled: Observable<boolean>,
private _mixed: Observable<boolean>,
) {
super();
@@ -110,10 +111,12 @@ export class ChoiceListEntry extends Disposable {
public buildDom(maxRows: number = 6): DomContents {
return dom.domComputed(this._isEditing, (editMode) => {
if (editMode) {
// If we have mixed values, we can't show any options on the editor.
const initialValue = this._mixed.get() ? [] : this._values.get().map(label => {
return new ChoiceItem(label, label, this._choiceOptionsByName.get().get(label));
});
const tokenField = TokenField.ctor<ChoiceItem>().create(this._tokenFieldHolder, {
initialValue: this._values.get().map(label => {
return new ChoiceItem(label, label, this._choiceOptionsByName.get().get(label));
}),
initialValue,
renderToken: token => this._renderToken(token),
createToken: label => new ChoiceItem(label, null),
clipboardToTokens: clipboardToChoices,
@@ -155,57 +158,67 @@ export class ChoiceListEntry extends Disposable {
dom.onKeyDown({Enter$: () => this._save()}),
);
} else {
const someValues = Computed.create(null, this._values, (_use, values) =>
const holder = new MultiHolder();
const someValues = Computed.create(holder, this._values, (_use, values) =>
values.length <= maxRows ? values : values.slice(0, maxRows - 1));
const noChoices = Computed.create(holder, someValues, (_use, values) => values.length === 0);
return cssVerticalFlex(
cssListBoxInactive(
dom.cls(cssBlockedCursor.className, this._disabled),
dom.autoDispose(someValues),
dom.maybe(use => use(someValues).length === 0, () =>
row('No choices configured')
),
dom.domComputed(this._choiceOptionsByName, (choiceOptions) =>
dom.forEach(someValues, val => {
return row(
cssTokenColorInactive(
dom.style('background-color', getFillColor(choiceOptions.get(val)) || '#FFFFFF'),
dom.style('color', getTextColor(choiceOptions.get(val)) || '#000000'),
dom.cls('font-bold', choiceOptions.get(val)?.fontBold ?? false),
dom.cls('font-underline', choiceOptions.get(val)?.fontUnderline ?? false),
dom.cls('font-italic', choiceOptions.get(val)?.fontItalic ?? false),
dom.cls('font-strikethrough', choiceOptions.get(val)?.fontStrikethrough ?? false),
'T',
testId('choice-list-entry-color')
),
cssTokenLabel(
val,
testId('choice-list-entry-label')
dom.autoDispose(holder),
dom.maybe(this._mixed, () => [
cssListBoxInactive(
dom.cls(cssBlockedCursor.className, this._disabled),
row('Mixed configuration')
)
]),
dom.maybe(use => !use(this._mixed), () => [
cssListBoxInactive(
dom.cls(cssBlockedCursor.className, this._disabled),
dom.maybe(noChoices, () => row('No choices configured')),
dom.domComputed(this._choiceOptionsByName, (choiceOptions) =>
dom.forEach(someValues, val => {
return row(
cssTokenColorInactive(
dom.style('background-color', getFillColor(choiceOptions.get(val)) || '#FFFFFF'),
dom.style('color', getTextColor(choiceOptions.get(val)) || '#000000'),
dom.cls('font-bold', choiceOptions.get(val)?.fontBold ?? false),
dom.cls('font-underline', choiceOptions.get(val)?.fontUnderline ?? false),
dom.cls('font-italic', choiceOptions.get(val)?.fontItalic ?? false),
dom.cls('font-strikethrough', choiceOptions.get(val)?.fontStrikethrough ?? false),
'T',
testId('choice-list-entry-color')
),
cssTokenLabel(
val,
testId('choice-list-entry-label')
)
);
}),
),
// Show description row for any remaining rows
dom.maybe(use => use(this._values).length > maxRows, () =>
row(
dom('span',
testId('choice-list-entry-label'),
dom.text((use) => `+${use(this._values).length - (maxRows - 1)} more`)
)
);
}),
),
// Show description row for any remaining rows
dom.maybe(use => use(this._values).length > maxRows, () =>
row(
dom('span',
testId('choice-list-entry-label'),
dom.text((use) => `+${use(this._values).length - (maxRows - 1)} more`)
)
)
),
dom.on('click', () => this._startEditing()),
cssListBoxInactive.cls("-disabled", this._disabled),
testId('choice-list-entry')
),
dom.on('click', () => this._startEditing()),
cssListBoxInactive.cls("-disabled", this._disabled),
testId('choice-list-entry')
),
dom.maybe(use => !use(this._disabled), () =>
]),
dom.maybe(use => !use(this._disabled), () => [
cssButtonRow(
primaryButton('Edit',
primaryButton(
dom.text(use => use(this._mixed) ? 'Reset' : 'Edit'),
dom.on('click', () => this._startEditing()),
testId('choice-list-entry-edit')
)
)
)
),
),
]),
);
}
});

View File

@@ -7,7 +7,7 @@ import {testId} from 'app/client/ui2018/cssVars';
import {ChoiceListEntry} from 'app/client/widgets/ChoiceListEntry';
import {choiceToken, DEFAULT_FILL_COLOR, DEFAULT_TEXT_COLOR} from 'app/client/widgets/ChoiceToken';
import {NTextBox} from 'app/client/widgets/NTextBox';
import {Computed, dom, fromKo, styled} from 'grainjs';
import {Computed, dom, styled} from 'grainjs';
export type IChoiceOptions = Style
export type ChoiceOptions = Record<string, IChoiceOptions | undefined>;
@@ -66,16 +66,29 @@ export class ChoiceTextBox extends NTextBox {
}
public buildConfigDom() {
const disabled = Computed.create(null,
use => use(this.field.disableModify)
|| use(use(this.field.column).disableEditData)
|| use(this.field.config.options.disabled('choices'))
);
const mixed = Computed.create(null,
use => !use(disabled)
&& (use(this.field.config.options.mixed('choices')) || use(this.field.config.options.mixed('choiceOptions')))
);
return [
super.buildConfigDom(),
cssLabel('CHOICES'),
cssRow(
dom.autoDispose(disabled),
dom.autoDispose(mixed),
dom.create(
ChoiceListEntry,
this._choiceValues,
this._choiceOptionsByName,
this.save.bind(this),
fromKo(this.field.column().disableEditData)
disabled,
mixed
)
)
];
@@ -95,11 +108,10 @@ export class ChoiceTextBox extends NTextBox {
protected save(choices: string[], choiceOptions: ChoiceOptionsByName, renames: Record<string, string>) {
const options = {
...this.options.peek(),
choices,
choiceOptions: toObject(choiceOptions)
};
return this.field.updateChoices(renames, options);
return this.field.config.updateChoices(renames, options);
}
}

View File

@@ -27,7 +27,7 @@ export class ConditionalStyle extends Disposable {
private _label: string,
private _ruleOwner: RuleOwner,
private _gristDoc: GristDoc,
private _disabled?: Observable<boolean>
) {
super();
this._currentRecord = Computed.create(this, use => {
@@ -70,7 +70,8 @@ export class ConditionalStyle extends Disposable {
textButton(
'Add conditional style',
testId('add-conditional-style'),
dom.on('click', () => this._ruleOwner.addEmptyRule())
dom.on('click', () => this._ruleOwner.addEmptyRule()),
dom.prop('disabled', this._disabled)
),
dom.hide(use => use(this._ruleOwner.hasRules))
),
@@ -78,7 +79,7 @@ export class ConditionalStyle extends Disposable {
use => use(this._ruleOwner.rulesCols),
(owner, rules) =>
cssRuleList(
dom.show(rules.length > 0),
dom.show(use => rules.length > 0 && (!this._disabled || !use(this._disabled))),
...rules.map((column, ruleIndex) => {
const textColor = this._buildStyleOption(owner, ruleIndex, 'textColor');
const fillColor = this._buildStyleOption(owner, ruleIndex, 'fillColor');
@@ -127,9 +128,10 @@ export class ConditionalStyle extends Disposable {
fontItalic,
fontUnderline,
fontStrikethrough
},
save,
this._label || 'Conditional Style'
}, {
onSave: save,
placeholder: this._label || 'Conditional Style',
}
)
),
cssRemoveButton(
@@ -146,6 +148,7 @@ export class ConditionalStyle extends Disposable {
textButton('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))
),

View File

@@ -7,13 +7,14 @@ import {currencies} from 'app/common/Locales';
interface CurrencyPickerOptions {
// The label to use in the select menu for the default option.
defaultCurrencyLabel: string;
disabled?: Observable<boolean>;
}
export function buildCurrencyPicker(
owner: IDisposableOwner,
currency: Observable<string|undefined>,
onSave: (value: string|undefined) => void,
{defaultCurrencyLabel}: CurrencyPickerOptions
{defaultCurrencyLabel, disabled}: CurrencyPickerOptions
) {
const currencyItems: ACSelectItem[] = currencies
.map(item => ({
@@ -35,6 +36,7 @@ export function buildCurrencyPicker(
return buildACSelect(owner,
{
acIndex, valueObs,
disabled,
save(_, item: ACSelectItem | undefined) {
// Save only if we have found a match
if (!item) {

View File

@@ -7,7 +7,7 @@ var kf = require('../lib/koForm');
var AbstractWidget = require('./AbstractWidget');
const {fromKoSave} = require('app/client/lib/fromKoSave');
const {alignmentSelect} = require('app/client/ui2018/buttonSelect');
const {alignmentSelect, cssButtonSelect} = require('app/client/ui2018/buttonSelect');
const {cssRow, cssLabel} = require('app/client/ui/RightPanelStyles');
const {cssTextInput} = require("app/client/ui2018/editableLabel");
const {styled, fromKo} = require('grainjs');
@@ -21,18 +21,21 @@ function DateTextBox(field) {
AbstractWidget.call(this, field);
this.alignment = this.options.prop('alignment');
this.dateFormat = this.options.prop('dateFormat');
this.isCustomDateFormat = this.options.prop('isCustomDateFormat');
// These properties are only used in configuration.
this.dateFormat = this.field.config.options.prop('dateFormat');
this.isCustomDateFormat = this.field.config.options.prop('isCustomDateFormat');
this.mixedDateFormat = ko.pureComputed(() => this.dateFormat() === null || this.isCustomDateFormat() === null);
// Helper to set 'dateFormat' and 'isCustomDateFormat' from the set of default date format strings.
this.standardDateFormat = this.autoDispose(ko.computed({
owner: this,
read: function() { return this.isCustomDateFormat() ? 'Custom' : this.dateFormat(); },
read: function() { return this.mixedDateFormat() ? null : this.isCustomDateFormat() ? 'Custom' : this.dateFormat(); },
write: function(val) {
if (val === 'Custom') { this.isCustomDateFormat.setAndSave(true); }
else {
this.options.update({isCustomDateFormat: false, dateFormat: val});
this.options.save();
this.field.config.options.update({isCustomDateFormat: false, dateFormat: val});
this.field.config.options.save();
}
}
}));
@@ -44,12 +47,18 @@ dispose.makeDisposable(DateTextBox);
_.extend(DateTextBox.prototype, AbstractWidget.prototype);
DateTextBox.prototype.buildDateConfigDom = function() {
var self = this;
const disabled = this.field.config.options.disabled('dateFormat');
return dom('div',
cssLabel("Date Format"),
cssRow(dom(select(fromKo(self.standardDateFormat), [...dateFormatOptions, "Custom"]), dom.testId("Widget_dateFormat"))),
kd.maybe(self.isCustomDateFormat, function() {
return cssRow(dom(textbox(self.dateFormat), dom.testId("Widget_dateCustomFormat")));
cssRow(dom(select(
fromKo(this.standardDateFormat),
[...dateFormatOptions, "Custom"],
{ disabled, defaultLabel: "Mixed format" },
), dom.testId("Widget_dateFormat"))),
kd.maybe(() => !this.mixedDateFormat() && this.isCustomDateFormat(), () => {
return cssRow(dom(
textbox(this.dateFormat, { disabled }),
dom.testId("Widget_dateCustomFormat")));
})
);
};
@@ -58,7 +67,10 @@ DateTextBox.prototype.buildConfigDom = function() {
return dom('div',
this.buildDateConfigDom(),
cssRow(
alignmentSelect(fromKoSave(this.alignment))
alignmentSelect(
fromKoSave(this.field.config.options.prop('alignment')),
cssButtonSelect.cls('-disabled', this.field.config.options.disabled('alignment')),
),
)
);
};
@@ -91,8 +103,8 @@ const cssFocus = styled('div', `
`)
// helper method to create old style textbox that looks like a new one
function textbox(value) {
const textDom = kf.text(value);
function textbox(value, options) {
const textDom = kf.text(value, options ?? {});
const tzInput = textDom.querySelector('input');
dom(tzInput,
kd.cssClass(cssTextInput.className),

View File

@@ -9,7 +9,7 @@ var DateTextBox = require('./DateTextBox');
var gutil = require('app/common/gutil');
const {fromKoSave} = require('app/client/lib/fromKoSave');
const {alignmentSelect} = require('app/client/ui2018/buttonSelect');
const {alignmentSelect, cssButtonSelect} = require('app/client/ui2018/buttonSelect');
const {cssRow, cssLabel} = require('app/client/ui/RightPanelStyles');
const {cssTextInput} = require("app/client/ui2018/editableLabel");
const {dom: gdom, styled, fromKo} = require('grainjs');
@@ -30,8 +30,9 @@ function DateTimeTextBox(field) {
this._setTimezone = (val) => field.column().type.setAndSave('DateTime:' + val);
this.timeFormat = this.options.prop('timeFormat');
this.isCustomTimeFormat = this.options.prop('isCustomTimeFormat');
this.timeFormat = this.field.config.options.prop('timeFormat');
this.isCustomTimeFormat = this.field.config.options.prop('isCustomTimeFormat');
this.mixedTimeFormat = ko.pureComputed(() => this.timeFormat() === null || this.isCustomTimeFormat() === null);
// Helper to set 'timeFormat' and 'isCustomTimeFormat' from the set of default time format strings.
this.standardTimeFormat = this.autoDispose(ko.computed({
@@ -54,21 +55,39 @@ _.extend(DateTimeTextBox.prototype, DateTextBox.prototype);
* builds only the necessary dom for the transform config menu.
*/
DateTimeTextBox.prototype.buildConfigDom = function(isTransformConfig) {
var self = this;
const disabled = ko.pureComputed(() => {
return this.field.config.options.disabled('timeFormat')() || this.field.column().disableEditData();
});
const alignment = fromKoSave(this.field.config.options.prop('alignment'));
return dom('div',
cssLabel("Timezone"),
cssRow(
gdom.create(buildTZAutocomplete, moment, fromKo(this._timezone), this._setTimezone,
{ disabled : fromKo(this.field.column().disableEditData)}),
{ disabled : fromKo(disabled)}),
),
self.buildDateConfigDom(),
this.buildDateConfigDom(),
cssLabel("Time Format"),
cssRow(dom(select(fromKo(self.standardTimeFormat), [...timeFormatOptions, "Custom"]), dom.testId("Widget_timeFormat"))),
kd.maybe(self.isCustomTimeFormat, function() {
return cssRow(dom(textbox(self.timeFormat), dom.testId("Widget_timeCustomFormat")));
cssRow(dom(
select(
fromKo(this.standardTimeFormat),
[...timeFormatOptions, "Custom"],
{ disabled : fromKo(disabled), defaultLabel: 'Mixed format' }
),
dom.testId("Widget_timeFormat")
)),
kd.maybe(() => !this.mixedTimeFormat() && this.isCustomTimeFormat(), () => {
return cssRow(
dom(
textbox(this.timeFormat, { disabled: this.field.config.options.disabled('timeFormat')}),
dom.testId("Widget_timeCustomFormat")
)
);
}),
isTransformConfig ? null : cssRow(
alignmentSelect(fromKoSave(this.alignment))
alignmentSelect(
alignment,
cssButtonSelect.cls('-disabled', this.field.config.options.disabled('alignment')),
)
)
);
};
@@ -94,8 +113,8 @@ const cssFocus = styled('div', `
// helper method to create old style textbox that looks like a new one
function textbox(value) {
const textDom = kf.text(value);
function textbox(value, options) {
const textDom = kf.text(value, options || {});
const tzInput = textDom.querySelector('input');
dom(tzInput,
kd.cssClass(cssTextInput.className),

View File

@@ -16,7 +16,7 @@ import { SaveableObjObservable, setSaveValue } from 'app/client/models/modelUtil
import { CombinedStyle, Style } from 'app/client/models/Styles';
import { FieldSettingsMenu } from 'app/client/ui/FieldMenus';
import { cssBlockedCursor, cssLabel, cssRow } from 'app/client/ui/RightPanelStyles';
import { buttonSelect } from 'app/client/ui2018/buttonSelect';
import { buttonSelect, cssButtonSelect } from 'app/client/ui2018/buttonSelect';
import { theme } from 'app/client/ui2018/cssVars';
import { IOptionFull, menu, select } from 'app/client/ui2018/menus';
import { DiffBox } from 'app/client/widgets/DiffBox';
@@ -31,7 +31,7 @@ import * as gristTypes from 'app/common/gristTypes';
import { getReferencedTableId, isFullReferencingType } from 'app/common/gristTypes';
import { CellValue } from 'app/plugin/GristData';
import { Computed, Disposable, fromKo, dom as grainjsDom,
Holder, IDisposable, makeTestId, styled, toKo } from 'grainjs';
Holder, IDisposable, makeTestId, MultiHolder, styled, toKo } from 'grainjs';
import * as ko from 'knockout';
import * as _ from 'underscore';
@@ -156,22 +156,7 @@ export class FieldBuilder extends Disposable {
}
}));
this.widget = ko.pureComputed<object>({
owner: this,
read() { return this.options().widget; },
write(widget) {
// Reset the entire JSON, so that all options revert to their defaults.
const previous = this.options.peek();
this.options.setAndSave({
widget,
// Persists color settings across widgets (note: we cannot use `field.fillColor` to get the
// current value because it returns a default value for `undefined`. Same for `field.textColor`.
fillColor: previous.fillColor,
textColor: previous.textColor,
}).catch(reportError);
}
});
this.widget = ko.pureComputed(() => this.field.widget());
// Whether there is a pending call that transforms column.
this.isCallPending = ko.observable(false);
@@ -221,11 +206,36 @@ export class FieldBuilder extends Disposable {
value: label,
icon: typeWidgets[label].icon
}));
return widgetOptions.length <= 1 ? null : [
if (widgetOptions.length <= 1) { return null; }
// Here we need to accommodate the fact that the widget can be null, which
// won't be visible on a select component when disabled.
const defaultWidget = Computed.create(null, use => {
if (widgetOptions.length <= 2) {
return;
}
const value = use(this.field.config.widget);
return value;
});
defaultWidget.onWrite((value) => this.field.config.widget(value));
const disabled = Computed.create(null, use => !use(this.field.config.sameWidgets));
return [
cssLabel('CELL FORMAT'),
cssRow(
widgetOptions.length <= 2 ? buttonSelect(fromKo(this.widget), widgetOptions) :
select(fromKo(this.widget), widgetOptions),
grainjsDom.autoDispose(defaultWidget),
widgetOptions.length <= 2 ?
buttonSelect(
fromKo(this.field.config.widget),
widgetOptions,
cssButtonSelect.cls("-disabled", disabled),
) :
select(
defaultWidget,
widgetOptions,
{
disabled,
defaultLabel: 'Mixed format'
}
),
testId('widget-select')
)
];
@@ -236,21 +246,42 @@ export class FieldBuilder extends Disposable {
* Build the type change dom.
*/
public buildSelectTypeDom() {
const selectType = Computed.create(null, (use) => use(fromKo(this._readOnlyPureType)));
selectType.onWrite(newType => newType === this._readOnlyPureType.peek() || this._setType(newType));
const holder = new MultiHolder();
const commonType = Computed.create(holder, use => use(use(this.field.viewSection).columnsType));
const selectType = Computed.create(holder, (use) => {
const myType = use(fromKo(this._readOnlyPureType));
return use(commonType) === 'mixed' ? '' : myType;
});
selectType.onWrite(newType => {
const sameType = newType === this._readOnlyPureType.peek();
if (!sameType || commonType.get() === 'mixed') {
return this._setType(newType);
}
});
const onDispose = () => (this.isDisposed() || selectType.set(this.field.column().pureType()));
const allFormulas = Computed.create(holder, use => use(use(this.field.viewSection).columnsAllIsFormula));
return [
cssRow(
grainjsDom.autoDispose(selectType),
grainjsDom.autoDispose(holder),
select(selectType, this._availableTypes, {
disabled: (use) => use(this._isTransformingFormula) || use(this.origColumn.disableModifyBase) ||
disabled: (use) =>
// If we are transforming column at this moment (applying a formula to change data),
use(this._isTransformingFormula) ||
// If this is a summary column
use(this.origColumn.disableModifyBase) ||
// If there are multiple column selected, but all have different type than Any.
(use(this.field.config.multiselect) && !use(allFormulas)) ||
// If we are waiting for a server response
use(this.isCallPending),
menuCssClass: cssTypeSelectMenu.className,
defaultLabel: 'Mixed types'
}),
testId('type-select'),
grainjsDom.cls('tour-type-selector'),
grainjsDom.cls(cssBlockedCursor.className, this.origColumn.disableModifyBase)
grainjsDom.cls(cssBlockedCursor.className, use =>
use(this.origColumn.disableModifyBase) ||
(use(this.field.config.multiselect) && !use(allFormulas))
),
),
grainjsDom.maybe((use) => use(this._isRef) && !use(this._isTransformingType), () => this._buildRefTableSelect()),
grainjsDom.maybe(this._isTransformingType, () => {
@@ -273,9 +304,19 @@ export class FieldBuilder extends Disposable {
public _setType(newType: string): Promise<unknown>|undefined {
if (this.origColumn.isFormula()) {
// Do not type transform a new/empty column or a formula column. Just make a best guess for
// the full type, and set it.
// the full type, and set it. If multiple columns are selected (and all are formulas/empty),
// then we will set the type for all of them using full type guessed from the first column.
const column = this.field.column();
column.type.setAndSave(addColTypeSuffix(newType, column, this._docModel)).catch(reportError);
const calculatedType = addColTypeSuffix(newType, column, this._docModel);
// If we selected multiple empty/formula columns, make the change for all of them.
if (this.field.viewSection.peek().selectedFields.peek().length > 1 &&
['formula', 'empty'].indexOf(this.field.viewSection.peek().columnsBehavior.peek())) {
return this.gristDoc.docData.bundleActions("Changing multiple column types", () =>
Promise.all(this.field.viewSection.peek().selectedFields.peek().map(f =>
f.column.peek().type.setAndSave(calculatedType)
))).catch(reportError);
}
column.type.setAndSave(calculatedType).catch(reportError);
} else if (!this.columnTransform) {
this.columnTransform = TypeTransform.create(null, this.gristDoc, this);
return this.columnTransform.prepare(newType);
@@ -295,14 +336,18 @@ export class FieldBuilder extends Disposable {
icon: 'FieldTable' as const
}))
);
const isDisabled = Computed.create(null, use => {
return use(this.origColumn.disableModifyBase) || use(this.field.config.multiselect);
});
return [
cssLabel('DATA FROM TABLE'),
cssRow(
dom.autoDispose(allTables),
dom.autoDispose(isDisabled),
select(fromKo(this._refTableId), allTables, {
// Disallow changing the destination table when the column should not be modified
// (specifically when it's a group-by column of a summary table).
disabled: this.origColumn.disableModifyBase,
disabled: isDisabled,
}),
testId('ref-table-select')
)
@@ -357,39 +402,58 @@ export class FieldBuilder extends Disposable {
// the dom created by the widgetImpl to get out of sync.
return dom('div',
kd.maybe(() => !this._isTransformingType() && this.widgetImpl(), (widget: NewAbstractWidget) =>
dom('div',
widget.buildConfigDom(),
cssSeparator(),
widget.buildColorConfigDom(this.gristDoc),
dom('div', widget.buildConfigDom(), cssSeparator())
)
);
}
// If there is more than one field for this column (i.e. present in multiple views).
kd.maybe(() => this.origColumn.viewFields().all().length > 1, () =>
dom('div.fieldbuilder_settings',
kf.row(
kd.toggleClass('fieldbuilder_settings_header', true),
kf.label(
dom('div.fieldbuilder_settings_button',
dom.testId('FieldBuilder_settings'),
kd.text(() => this.field.useColOptions() ? 'Common' : 'Separate'), ' ▾',
menu(() => FieldSettingsMenu(
this.field.useColOptions(),
this.field.viewSection().isRaw(),
{
useSeparate: () => this.fieldSettingsUseSeparate(),
saveAsCommon: () => this.fieldSettingsSaveAsCommon(),
revertToCommon: () => this.fieldSettingsRevertToCommon(),
},
)),
),
'Field in ',
kd.text(() => this.origColumn.viewFields().all().length),
' views'
)
)
)
)
public buildColorConfigDom() {
// NOTE: adding a grainjsDom .maybe here causes the disposable order of the widgetImpl and
// the dom created by the widgetImpl to get out of sync.
return dom('div',
kd.maybe(() => !this._isTransformingType() && this.widgetImpl(), (widget: NewAbstractWidget) =>
dom('div', widget.buildColorConfigDom(this.gristDoc))
)
);
}
/**
* Builds the FieldBuilder Options Config DOM. Calls the buildConfigDom function of its widgetImpl.
*/
public buildSettingOptions() {
// NOTE: adding a grainjsDom .maybe here causes the disposable order of the widgetImpl and
// the dom created by the widgetImpl to get out of sync.
return dom('div',
kd.maybe(() => !this._isTransformingType() && this.widgetImpl(), (widget: NewAbstractWidget) =>
dom('div',
// If there is more than one field for this column (i.e. present in multiple views).
kd.maybe(() => this.origColumn.viewFields().all().length > 1, () =>
dom('div.fieldbuilder_settings',
kf.row(
kd.toggleClass('fieldbuilder_settings_header', true),
kf.label(
dom('div.fieldbuilder_settings_button',
dom.testId('FieldBuilder_settings'),
kd.text(() => this.field.useColOptions() ? 'Common' : 'Separate'), ' ▾',
menu(() => FieldSettingsMenu(
this.field.useColOptions(),
this.field.viewSection().isRaw(),
{
useSeparate: () => this.fieldSettingsUseSeparate(),
saveAsCommon: () => this.fieldSettingsSaveAsCommon(),
revertToCommon: () => this.fieldSettingsRevertToCommon(),
},
)),
),
'Field in ',
kd.text(() => this.origColumn.viewFields().all().length),
' views'
)
)
)
)
)
)
);
}

View File

@@ -3,12 +3,12 @@ import { findLinks } from 'app/client/lib/textUtils';
import { DataRowModel } from 'app/client/models/DataRowModel';
import { ViewFieldRec } from 'app/client/models/entities/ViewFieldRec';
import { cssRow } from 'app/client/ui/RightPanelStyles';
import { alignmentSelect, makeButtonSelect } from 'app/client/ui2018/buttonSelect';
import { alignmentSelect, cssButtonSelect, makeButtonSelect } from 'app/client/ui2018/buttonSelect';
import { colors, testId } from 'app/client/ui2018/cssVars';
import { cssIconBackground, icon } from 'app/client/ui2018/icons';
import { gristLink } from 'app/client/ui2018/links';
import { NewAbstractWidget, Options } from 'app/client/widgets/NewAbstractWidget';
import { dom, DomArg, DomContents, fromKo, Observable, styled } from 'grainjs';
import { Computed, dom, DomArg, DomContents, fromKo, Observable, styled } from 'grainjs';
/**
* TextBox - The most basic widget for displaying text information.
@@ -20,8 +20,8 @@ export class NTextBox extends NewAbstractWidget {
constructor(field: ViewFieldRec, options: Options = {}) {
super(field, options);
this.alignment = fromKoSave<string>(this.options.prop('alignment'));
this.wrapping = fromKo(this.field.wrapping);
this.alignment = fromKo(this.options.prop('alignment'));
this.wrapping = fromKo(this.field.wrap);
this.autoDispose(this.wrapping.addListener(() => {
this.field.viewSection().events.trigger('rowHeightChange');
@@ -29,11 +29,28 @@ export class NTextBox extends NewAbstractWidget {
}
public buildConfigDom(): DomContents {
const toggle = () => {
const newValue = !this.field.config.wrap.peek();
this.field.config.wrap.setAndSave(newValue).catch(reportError);
};
const options = this.field.config.options;
// Some options might be disabled, as more than one column is selected.
// Prop observable is owned by the options object.
const alignmentDisabled = Computed.create(this, use => use(options.disabled('alignment')));
const wrapDisabled = Computed.create(this, (use) => use(options.disabled('wrap')));
return [
cssRow(
alignmentSelect(this.alignment),
alignmentSelect(
fromKoSave(this.field.config.alignment),
cssButtonSelect.cls('-disabled', alignmentDisabled),
),
dom('div', {style: 'margin-left: 8px;'},
makeButtonSelect(this.wrapping, [{value: true, icon: 'Wrap'}], this._toggleWrap.bind(this), {}),
makeButtonSelect(
fromKo(this.field.config.wrap),
[{value: true, icon: 'Wrap'}],
toggle,
cssButtonSelect.cls('-disabled', wrapDisabled),
),
testId('tb-wrap-text')
)
)
@@ -48,12 +65,6 @@ export class NTextBox extends NewAbstractWidget {
dom.domComputed((use) => use(row._isAddRow) ? null : makeLinks(use(this.valueFormatter).formatAny(use(value))))
);
}
private _toggleWrap() {
const newValue = !this.wrapping.get();
this.options.update({wrap: newValue});
(this.options as any).save();
}
}
function makeLinks(text: string) {

View File

@@ -4,15 +4,16 @@
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
import {reportError} from 'app/client/models/errors';
import {cssLabel, cssRow} from 'app/client/ui/RightPanelStyles';
import {ISelectorOption, makeButtonSelect} from 'app/client/ui2018/buttonSelect';
import {cssButtonSelect, ISelectorOption, makeButtonSelect} from 'app/client/ui2018/buttonSelect';
import {testId, theme} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {buildCurrencyPicker} from 'app/client/widgets/CurrencyPicker';
import {NTextBox} from 'app/client/widgets/NTextBox';
import {clamp} from 'app/common/gutil';
import {buildNumberFormat, NumberFormatOptions, NumMode, NumSign} from 'app/common/NumberFormat';
import {Computed, dom, DomContents, DomElementArg, fromKo, MultiHolder, Observable, styled} from 'grainjs';
import {buildCurrencyPicker} from "app/client/widgets/CurrencyPicker";
import * as LocaleCurrency from "locale-currency";
import {BindableValue, Computed, dom, DomContents, DomElementArg,
fromKo, MultiHolder, Observable, styled} from 'grainjs';
import * as LocaleCurrency from 'locale-currency';
const modeOptions: Array<ISelectorOption<NumMode>> = [
@@ -40,17 +41,19 @@ export class NumericTextBox extends NTextBox {
// Resolved options, to show default min/max decimals, which change depending on numMode.
const resolved = Computed.create<Intl.ResolvedNumberFormatOptions>(holder, (use) => {
const {numMode} = use(this.options);
const {numMode} = use(this.field.config.options);
const docSettings = use(this.field.documentSettings);
return buildNumberFormat({numMode}, docSettings).resolvedOptions();
});
// Prepare various observables that reflect the options in the UI.
const options = fromKo(this.options);
const fieldOptions = this.field.config.options;
const options = fromKo(fieldOptions);
const docSettings = fromKo(this.field.documentSettings);
const numMode = Computed.create(holder, options, (use, opts) => (opts.numMode as NumMode) || null);
const numSign = Computed.create(holder, options, (use, opts) => opts.numSign || null);
const currency = Computed.create(holder, options, (use, opts) => opts.currency);
const disabled = Computed.create(holder, use => use(this.field.config.options.disabled('currency')));
const minDecimals = Computed.create(holder, options, (use, opts) => numberOrDefault(opts.decimals, ''));
const maxDecimals = Computed.create(holder, options, (use, opts) => numberOrDefault(opts.maxDecimals, ''));
const defaultMin = Computed.create(holder, resolved, (use, res) => res.minimumFractionDigits);
@@ -59,13 +62,13 @@ export class NumericTextBox extends NTextBox {
settings.currency ?? LocaleCurrency.getCurrency(settings.locale ?? 'en-US')
);
// Save a value as the given property in this.options() observable. Set it, save, and revert
// Save a value as the given property in fieldOptions observable. Set it, save, and revert
// on save error. This is similar to what modelUtil.setSaveValue() does.
const setSave = (prop: keyof NumberFormatOptions, value: unknown) => {
const orig = {...this.options.peek()};
const orig = {...fieldOptions.peek()};
if (value !== orig[prop]) {
this.options({...orig, [prop]: value, ...updateOptions(prop, value)});
this.options.save().catch((err) => { reportError(err); this.options(orig); });
fieldOptions({...orig, [prop]: value, ...updateOptions(prop, value)});
fieldOptions.save().catch((err) => { reportError(err); fieldOptions(orig); });
}
};
@@ -78,28 +81,30 @@ export class NumericTextBox extends NTextBox {
const setSign = (val: NumSign) => setSave('numSign', val !== numSign.get() ? val : undefined);
const setCurrency = (val: string|undefined) => setSave('currency', val);
const disabledStyle = cssButtonSelect.cls('-disabled', disabled);
return [
super.buildConfigDom(),
cssLabel('Number Format'),
cssRow(
dom.autoDispose(holder),
makeButtonSelect(numMode, modeOptions, setMode, {}, cssModeSelect.cls(''), testId('numeric-mode')),
makeButtonSelect(numSign, signOptions, setSign, {}, cssSignSelect.cls(''), testId('numeric-sign')),
makeButtonSelect(numMode, modeOptions, setMode, disabledStyle, cssModeSelect.cls(''), testId('numeric-mode')),
makeButtonSelect(numSign, signOptions, setSign, disabledStyle, cssSignSelect.cls(''), testId('numeric-sign')),
),
dom.maybe((use) => use(numMode) === 'currency', () => [
cssLabel('Currency'),
cssRow(
dom.domComputed(docCurrency, (defaultCurrency) =>
buildCurrencyPicker(holder, currency, setCurrency,
{defaultCurrencyLabel: `Default currency (${defaultCurrency})`})
{defaultCurrencyLabel: `Default currency (${defaultCurrency})`, disabled})
),
testId("numeric-currency")
)
]),
cssLabel('Decimals'),
cssRow(
decimals('min', minDecimals, defaultMin, setMinDecimals, testId('numeric-min-decimals')),
decimals('max', maxDecimals, defaultMax, setMaxDecimals, testId('numeric-max-decimals')),
decimals('min', minDecimals, defaultMin, setMinDecimals, disabled, testId('numeric-min-decimals')),
decimals('max', maxDecimals, defaultMax, setMaxDecimals, disabled, testId('numeric-max-decimals')),
),
];
}
@@ -107,7 +112,7 @@ export class NumericTextBox extends NTextBox {
function numberOrDefault<T>(value: unknown, def: T): number | T {
return typeof value !== 'undefined' ? Number(value) : def;
return value !== null && value !== undefined ? Number(value) : def;
}
// Helper used by setSave() above to reset some properties when switching modes.
@@ -123,9 +128,12 @@ function decimals(
label: string,
value: Observable<number | ''>,
defaultValue: Observable<number>,
setFunc: (val?: number) => void, ...args: DomElementArg[]
setFunc: (val?: number) => void,
disabled: BindableValue<boolean>,
...args: DomElementArg[]
) {
return cssDecimalsBox(
cssDecimalsBox.cls('-disabled', disabled),
cssNumLabel(label),
cssNumInput({type: 'text', size: '2', min: '0'},
dom.prop('value', value),
@@ -161,6 +169,10 @@ const cssDecimalsBox = styled('div', `
&:first-child {
margin-right: 16px;
}
&-disabled {
background-color: ${theme.rightPanelToggleButtonDisabledBg};
pointer-events: none;
}
`);
const cssNumLabel = styled('div', `

View File

@@ -46,10 +46,14 @@ export class Reference extends NTextBox {
}
public buildTransformConfigDom() {
const disabled = Computed.create(null, use => use(this.field.config.multiselect));
return [
cssLabel('SHOW COLUMN'),
cssRow(
select(this._visibleColRef, this._validCols),
dom.autoDispose(disabled),
select(this._visibleColRef, this._validCols, {
disabled
}),
testId('fbuilder-ref-col-select')
)
];

View File

@@ -1,4 +1,4 @@
var _ = require('underscore');
import _ from 'underscore';
/**
* Given a widget name and a type, return the name of the widget that would
@@ -15,7 +15,7 @@ var _ = require('underscore');
* }
* }
*/
function getWidgetConfiguration(widgetName, type) {
export function getWidgetConfiguration(widgetName: string, type: string) {
const oneTypeDef = typeDefs[type] || typeDefs.Text;
if (!(widgetName in oneTypeDef.widgets)) {
widgetName = oneTypeDef.default;
@@ -25,20 +25,17 @@ function getWidgetConfiguration(widgetName, type) {
config: oneTypeDef.widgets[widgetName]
};
}
exports.getWidgetConfiguration = getWidgetConfiguration;
function mergeOptions(options, type) {
export function mergeOptions(options: any, type: string) {
const {name, config} = getWidgetConfiguration(options.widget, type);
return _.defaults({widget: name}, options, config.options);
}
exports.mergeOptions = mergeOptions;
// Contains the list of types with their storage types, possible widgets, default widgets,
// and defaults for all widget settings
// The names of widgets are used, instead of the actual classes needed, in order to limit
// the spread of dependencies. See ./UserTypeImpl for actual classes.
var typeDefs = {
export const typeDefs: any = {
Any: {
label: 'Any',
icon: 'FieldAny',
@@ -48,7 +45,8 @@ var typeDefs = {
editCons: 'TextEditor',
icon: 'FieldTextbox',
options: {
alignment: 'left'
alignment: 'left',
wrap: undefined,
}
}
},
@@ -64,6 +62,7 @@ var typeDefs = {
icon: 'FieldTextbox',
options: {
alignment: 'left',
wrap: undefined,
}
},
HyperLink: {
@@ -72,6 +71,7 @@ var typeDefs = {
icon: 'FieldLink',
options: {
alignment: 'left',
wrap: undefined,
}
}
},
@@ -86,7 +86,13 @@ var typeDefs = {
editCons: 'TextEditor',
icon: 'FieldTextbox',
options: {
alignment: 'right'
alignment: 'right',
wrap: undefined,
decimals: undefined,
maxDecimals: undefined,
numMode: undefined,
numSign: undefined,
currency: undefined,
}
},
Spinner: {
@@ -94,7 +100,13 @@ var typeDefs = {
editCons: 'TextEditor',
icon: 'FieldSpinner',
options: {
alignment: 'right'
alignment: 'right',
wrap: undefined,
decimals: undefined,
maxDecimals: undefined,
numMode: undefined,
numSign: undefined,
currency: undefined,
}
}
},
@@ -110,7 +122,12 @@ var typeDefs = {
icon: 'FieldTextbox',
options: {
decimals: 0,
alignment: 'right'
alignment: 'right',
wrap: undefined,
maxDecimals: undefined,
numMode: undefined,
numSign: undefined,
currency: undefined,
}
},
Spinner: {
@@ -119,7 +136,12 @@ var typeDefs = {
icon: 'FieldSpinner',
options: {
decimals: 0,
alignment: 'right'
alignment: 'right',
wrap: undefined,
maxDecimals: undefined,
numMode: undefined,
numSign: undefined,
currency: undefined,
}
}
},
@@ -134,7 +156,8 @@ var typeDefs = {
editCons: 'TextEditor',
icon: 'FieldTextbox',
options: {
alignment: 'center'
alignment: 'center',
wrap: undefined,
}
},
CheckBox: {
@@ -198,8 +221,9 @@ var typeDefs = {
icon: 'FieldTextbox',
options: {
alignment: 'left',
choices: null,
choiceOptions: null
wrap: undefined,
choices: undefined,
choiceOptions: undefined
}
}
},
@@ -215,8 +239,9 @@ var typeDefs = {
icon: 'FieldTextbox',
options: {
alignment: 'left',
choices: null,
choiceOptions: null
wrap: undefined,
choices: undefined,
choiceOptions: undefined
}
}
},
@@ -231,7 +256,8 @@ var typeDefs = {
editCons: 'ReferenceEditor',
icon: 'FieldReference',
options: {
alignment: 'left'
alignment: 'left',
wrap: undefined,
}
}
},
@@ -246,7 +272,8 @@ var typeDefs = {
editCons: 'ReferenceListEditor',
icon: 'FieldReference',
options: {
alignment: 'left'
alignment: 'left',
wrap: undefined
}
}
},
@@ -268,4 +295,3 @@ var typeDefs = {
default: 'Attachments'
}
};
exports.typeDefs = typeDefs;

View File

@@ -1,59 +0,0 @@
const {NTextBox} = require('./NTextBox');
const {NumericTextBox} = require('./NumericTextBox');
const {Spinner} = require('./Spinner');
const {AttachmentsWidget} = require('./AttachmentsWidget');
const {AttachmentsEditor} = require('./AttachmentsEditor');
const UserType = require('./UserType');
const {HyperLinkEditor} = require('./HyperLinkEditor');
const {NTextEditor} = require('./NTextEditor');
const {ReferenceEditor} = require('./ReferenceEditor');
const {ReferenceList} = require('./ReferenceList');
const {ReferenceListEditor} = require('./ReferenceListEditor');
const {HyperLinkTextBox} = require('./HyperLinkTextBox');
const {ChoiceTextBox } = require('./ChoiceTextBox');
const {Reference} = require('./Reference');
/**
* Convert the name of a widget to its implementation.
*/
const nameToWidget = {
'TextBox': NTextBox,
'TextEditor': NTextEditor,
'NumericTextBox': NumericTextBox,
'HyperLinkTextBox': HyperLinkTextBox,
'HyperLinkEditor': HyperLinkEditor,
'Spinner': Spinner,
'CheckBox': require('./CheckBox'),
'CheckBoxEditor': require('./CheckBoxEditor'),
'Reference': Reference,
'Switch': require('./Switch'),
'ReferenceEditor': ReferenceEditor,
'ReferenceList': ReferenceList,
'ReferenceListEditor': ReferenceListEditor,
'ChoiceTextBox': ChoiceTextBox,
'ChoiceEditor': require('./ChoiceEditor'),
'ChoiceListCell': require('./ChoiceListCell').ChoiceListCell,
'ChoiceListEditor': require('./ChoiceListEditor').ChoiceListEditor,
'DateTimeTextBox': require('./DateTimeTextBox'),
'DateTextBox': require('./DateTextBox'),
'DateEditor': require('./DateEditor'),
'AttachmentsWidget': AttachmentsWidget,
'AttachmentsEditor': AttachmentsEditor,
'DateTimeEditor': require('./DateTimeEditor'),
};
exports.nameToWidget = nameToWidget;
/** return a good class to instantiate for viewing a widget/type combination */
function getWidgetConstructor(widget, type) {
const {config} = UserType.getWidgetConfiguration(widget, type);
return nameToWidget[config.cons];
}
exports.getWidgetConstructor = getWidgetConstructor;
/** return a good class to instantiate for editing a widget/type combination */
function getEditorConstructor(widget, type) {
const {config} = UserType.getWidgetConfiguration(widget, type);
return nameToWidget[config.editCons];
}
exports.getEditorConstructor = getEditorConstructor;

View File

@@ -0,0 +1,71 @@
import {AttachmentsEditor} from 'app/client/widgets/AttachmentsEditor';
import {AttachmentsWidget} from 'app/client/widgets/AttachmentsWidget';
import CheckBox from 'app/client/widgets/CheckBox';
import CheckBoxEditor from 'app/client/widgets/CheckBoxEditor';
import ChoiceEditor from 'app/client/widgets/ChoiceEditor';
import {ChoiceListCell} from 'app/client/widgets/ChoiceListCell';
import {ChoiceListEditor} from 'app/client/widgets/ChoiceListEditor';
import {ChoiceTextBox} from 'app/client/widgets/ChoiceTextBox';
import DateEditor from 'app/client/widgets/DateEditor';
import DateTextBox from 'app/client/widgets/DateTextBox';
import DateTimeEditor from 'app/client/widgets/DateTimeEditor';
import DateTimeTextBox from 'app/client/widgets/DateTimeTextBox';
import {HyperLinkEditor} from 'app/client/widgets/HyperLinkEditor';
import {HyperLinkTextBox} from 'app/client/widgets/HyperLinkTextBox';
import {NewAbstractWidget} from 'app/client/widgets/NewAbstractWidget';
import {NewBaseEditor} from 'app/client/widgets/NewBaseEditor';
import {NTextBox} from 'app/client/widgets/NTextBox';
import {NTextEditor} from 'app/client/widgets/NTextEditor';
import {NumericTextBox} from 'app/client/widgets/NumericTextBox';
import {Reference} from 'app/client/widgets/Reference';
import {ReferenceEditor} from 'app/client/widgets/ReferenceEditor';
import {ReferenceList} from 'app/client/widgets/ReferenceList';
import {ReferenceListEditor} from 'app/client/widgets/ReferenceListEditor';
import {Spinner} from 'app/client/widgets/Spinner';
import Switch from 'app/client/widgets/Switch';
import {getWidgetConfiguration} from 'app/client/widgets/UserType';
import {GristType} from 'app/plugin/GristData';
/**
* Convert the name of a widget to its implementation.
*/
export const nameToWidget = {
'TextBox': NTextBox,
'TextEditor': NTextEditor,
'NumericTextBox': NumericTextBox,
'HyperLinkTextBox': HyperLinkTextBox,
'HyperLinkEditor': HyperLinkEditor,
'Spinner': Spinner,
'CheckBox': CheckBox,
'CheckBoxEditor': CheckBoxEditor,
'Reference': Reference,
'Switch': Switch,
'ReferenceEditor': ReferenceEditor,
'ReferenceList': ReferenceList,
'ReferenceListEditor': ReferenceListEditor,
'ChoiceTextBox': ChoiceTextBox,
'ChoiceEditor': ChoiceEditor,
'ChoiceListCell': ChoiceListCell,
'ChoiceListEditor': ChoiceListEditor,
'DateTimeTextBox': DateTimeTextBox,
'DateTextBox': DateTextBox,
'DateEditor': DateEditor,
'AttachmentsWidget': AttachmentsWidget,
'AttachmentsEditor': AttachmentsEditor,
'DateTimeEditor': DateTimeEditor,
};
export interface WidgetConstructor {create: (...args: any[]) => NewAbstractWidget}
/** return a good class to instantiate for viewing a widget/type combination */
export function getWidgetConstructor(widget: string, type: string): WidgetConstructor {
const {config} = getWidgetConfiguration(widget, type as GristType);
return nameToWidget[config.cons as keyof typeof nameToWidget] as any;
}
/** return a good class to instantiate for editing a widget/type combination */
export function getEditorConstructor(widget: string, type: string): typeof NewBaseEditor {
const {config} = getWidgetConfiguration(widget, type as GristType);
return nameToWidget[config.editCons as keyof typeof nameToWidget] as any;
}