mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Convert a few widgets to typescript and grainjs.
Summary: No behavior changes. Diff includes an intermediate commit with only renames, for easier review. Test Plan: Existing tests should pass. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D2669
This commit is contained in:
parent
f24a82e8d4
commit
4539521dff
@ -1,7 +1,7 @@
|
|||||||
import {basicButton, primaryButton} from 'app/client/ui2018/buttons';
|
import {basicButton, primaryButton} from 'app/client/ui2018/buttons';
|
||||||
import {colors, testId} from 'app/client/ui2018/cssVars';
|
import {colors, testId} from 'app/client/ui2018/cssVars';
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
import {Computed, Disposable, dom, DomElementArg, Observable, styled} from 'grainjs';
|
import {Computed, Disposable, dom, DomContents, DomElementArg, Observable, styled} from 'grainjs';
|
||||||
import isEqual = require('lodash/isEqual');
|
import isEqual = require('lodash/isEqual');
|
||||||
import uniq = require('lodash/uniq');
|
import uniq = require('lodash/uniq');
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ export class ListEntry extends Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Arg maxRows indicates the number of rows to display when the textarea is inactive.
|
// Arg maxRows indicates the number of rows to display when the textarea is inactive.
|
||||||
public buildDom(maxRows: number = 6): DomElementArg {
|
public buildDom(maxRows: number = 6): DomContents {
|
||||||
return dom.domComputed(this._isEditing, (editMode) => {
|
return dom.domComputed(this._isEditing, (editMode) => {
|
||||||
if (editMode) {
|
if (editMode) {
|
||||||
// Edit mode dom.
|
// Edit mode dom.
|
||||||
|
@ -58,6 +58,9 @@ export interface ViewFieldRec extends IRowModel<"_grist_Views_section_field"> {
|
|||||||
|
|
||||||
widgetOptionsJson: modelUtil.SaveableObjObservable<any>;
|
widgetOptionsJson: modelUtil.SaveableObjObservable<any>;
|
||||||
|
|
||||||
|
// Whether lines should wrap in a cell.
|
||||||
|
wrapping: ko.Computed<boolean>;
|
||||||
|
|
||||||
// Observable for the parsed filter object saved to the field.
|
// Observable for the parsed filter object saved to the field.
|
||||||
activeFilter: modelUtil.CustomComputed<string>;
|
activeFilter: modelUtil.CustomComputed<string>;
|
||||||
|
|
||||||
@ -180,6 +183,13 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
|
|||||||
this.widgetOptionsJson = modelUtil.jsonObservable(this._widgetOptionsStr,
|
this.widgetOptionsJson = modelUtil.jsonObservable(this._widgetOptionsStr,
|
||||||
(opts: any) => UserType.mergeOptions(opts || {}, this.column().pureType()));
|
(opts: any) => UserType.mergeOptions(opts || {}, this.column().pureType()));
|
||||||
|
|
||||||
|
this.wrapping = ko.pureComputed(() => {
|
||||||
|
// When user has yet to specify a desired wrapping state, we use different defaults for
|
||||||
|
// GridView (no wrap) and DetailView (wrap).
|
||||||
|
// "??" is the newish "nullish coalescing" operator. How cool is that!
|
||||||
|
return this.widgetOptionsJson().wrap ?? (this.viewSection().parentKey() !== 'record');
|
||||||
|
});
|
||||||
|
|
||||||
// Observable for the active filter that's initialized from the value saved to the server.
|
// Observable for the active filter that's initialized from the value saved to the server.
|
||||||
this.activeFilter = modelUtil.customComputed({
|
this.activeFilter = modelUtil.customComputed({
|
||||||
read: () => { const f = this.filter(); return f === 'null' ? '' : f; }, // To handle old empty filters
|
read: () => { const f = this.filter(); return f === 'null' ? '' : f; }, // To handle old empty filters
|
||||||
|
@ -1,96 +0,0 @@
|
|||||||
var commands = require('../components/commands');
|
|
||||||
var dispose = require('../lib/dispose');
|
|
||||||
var kd = require('../lib/koDom');
|
|
||||||
var _ = require('underscore');
|
|
||||||
var TextBox = require('./TextBox');
|
|
||||||
|
|
||||||
const {fromKoSave} = require('app/client/lib/fromKoSave');
|
|
||||||
const {ListEntry} = require('app/client/lib/listEntry');
|
|
||||||
const {alignmentSelect} = require('app/client/ui2018/buttonSelect');
|
|
||||||
const {colors, testId} = require('app/client/ui2018/cssVars');
|
|
||||||
const {icon} = require('app/client/ui2018/icons');
|
|
||||||
const {menu, menuItem} = require('app/client/ui2018/menus');
|
|
||||||
const {cssLabel, cssRow} = require('app/client/ui/RightPanel');
|
|
||||||
const {dom, Computed, styled} = require('grainjs');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ChoiceTextBox - A textbox for choice values.
|
|
||||||
*/
|
|
||||||
function ChoiceTextBox(field) {
|
|
||||||
TextBox.call(this, field);
|
|
||||||
|
|
||||||
this.docData = field._table.docModel.docData;
|
|
||||||
this.colId = field.column().colId;
|
|
||||||
this.tableId = field.column().table().tableId;
|
|
||||||
|
|
||||||
this.choices = this.options.prop('choices');
|
|
||||||
this.choiceValues = Computed.create(this, (use) => use(this.choices) || [])
|
|
||||||
}
|
|
||||||
dispose.makeDisposable(ChoiceTextBox);
|
|
||||||
_.extend(ChoiceTextBox.prototype, TextBox.prototype);
|
|
||||||
|
|
||||||
|
|
||||||
ChoiceTextBox.prototype.buildDom = function(row) {
|
|
||||||
let value = row[this.field.colId()];
|
|
||||||
return cssChoiceField(
|
|
||||||
cssChoiceText(
|
|
||||||
kd.style('text-align', this.alignment),
|
|
||||||
kd.text(() => row._isAddRow() ? '' : this.valueFormatter().format(value())),
|
|
||||||
),
|
|
||||||
cssDropdownIcon('Dropdown',
|
|
||||||
// When choices exist, click dropdown icon to open edit autocomplete.
|
|
||||||
dom.on('click', () => this._hasChoices() && commands.allCommands.editField.run()),
|
|
||||||
// When choices do not exist, open a single-item menu to open the sidepane choice option editor.
|
|
||||||
menu(() => [
|
|
||||||
menuItem(commands.allCommands.fieldTabOpen.run, 'Add Choice Options')
|
|
||||||
], {
|
|
||||||
trigger: [(elem, ctl) => {
|
|
||||||
// Only open this menu if there are no choices.
|
|
||||||
dom.onElem(elem, 'click', () => this._hasChoices() || ctl.open());
|
|
||||||
}]
|
|
||||||
}),
|
|
||||||
testId('choice-dropdown')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ChoiceTextBox.prototype.buildConfigDom = function() {
|
|
||||||
return [
|
|
||||||
cssRow(
|
|
||||||
alignmentSelect(fromKoSave(this.alignment))
|
|
||||||
),
|
|
||||||
cssLabel('OPTIONS'),
|
|
||||||
cssRow(
|
|
||||||
dom.create(ListEntry, this.choiceValues, (values) => this.choices.saveOnly(values))
|
|
||||||
)
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
ChoiceTextBox.prototype.buildTransformConfigDom = function() {
|
|
||||||
return this.buildConfigDom();
|
|
||||||
};
|
|
||||||
|
|
||||||
ChoiceTextBox.prototype._hasChoices = function() {
|
|
||||||
return this.choiceValues.get().length > 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const cssChoiceField = styled('div.field_clip', `
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssChoiceText = styled('div', `
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssDropdownIcon = styled(icon, `
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: ${colors.lightGreen};
|
|
||||||
min-width: 16px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
`);
|
|
||||||
|
|
||||||
module.exports = ChoiceTextBox;
|
|
89
app/client/widgets/ChoiceTextBox.ts
Normal file
89
app/client/widgets/ChoiceTextBox.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import * as commands from 'app/client/components/commands';
|
||||||
|
import {ListEntry} from 'app/client/lib/listEntry';
|
||||||
|
import {DataRowModel} from 'app/client/models/DataRowModel';
|
||||||
|
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
|
||||||
|
import {KoSaveableObservable} from 'app/client/models/modelUtil';
|
||||||
|
import {cssLabel, cssRow} from 'app/client/ui/RightPanel';
|
||||||
|
import {alignmentSelect} from 'app/client/ui2018/buttonSelect';
|
||||||
|
import {colors, testId} from 'app/client/ui2018/cssVars';
|
||||||
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
|
import {menu, menuItem} from 'app/client/ui2018/menus';
|
||||||
|
import {NTextBox} from 'app/client/widgets/NTextBox';
|
||||||
|
import {Computed, dom, styled} from 'grainjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChoiceTextBox - A textbox for choice values.
|
||||||
|
*/
|
||||||
|
export class ChoiceTextBox extends NTextBox {
|
||||||
|
private _choices: KoSaveableObservable<string[]>;
|
||||||
|
private _choiceValues: Computed<string[]>;
|
||||||
|
|
||||||
|
constructor(field: ViewFieldRec) {
|
||||||
|
super(field);
|
||||||
|
this._choices = this.options.prop('choices');
|
||||||
|
this._choiceValues = Computed.create(this, (use) => use(this._choices) || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildDom(row: DataRowModel) {
|
||||||
|
const value = row.cells[this.field.colId()];
|
||||||
|
return cssChoiceField(
|
||||||
|
cssChoiceText(
|
||||||
|
dom.style('text-align', this.alignment),
|
||||||
|
dom.text((use) => use(row._isAddRow) ? '' : use(this.valueFormatter).format(use(value))),
|
||||||
|
),
|
||||||
|
cssDropdownIcon('Dropdown',
|
||||||
|
// When choices exist, click dropdown icon to open edit autocomplete.
|
||||||
|
dom.on('click', () => this._hasChoices() && commands.allCommands.editField.run()),
|
||||||
|
// When choices do not exist, open a single-item menu to open the sidepane choice option editor.
|
||||||
|
menu(() => [
|
||||||
|
menuItem(commands.allCommands.fieldTabOpen.run, 'Add Choice Options')
|
||||||
|
], {
|
||||||
|
trigger: [(elem, ctl) => {
|
||||||
|
// Only open this menu if there are no choices.
|
||||||
|
dom.onElem(elem, 'click', () => this._hasChoices() || ctl.open());
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
testId('choice-dropdown')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildConfigDom() {
|
||||||
|
return [
|
||||||
|
cssRow(
|
||||||
|
alignmentSelect(this.alignment)
|
||||||
|
),
|
||||||
|
cssLabel('OPTIONS'),
|
||||||
|
cssRow(
|
||||||
|
dom.create(ListEntry, this._choiceValues, (values) => this._choices.saveOnly(values))
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildTransformConfigDom() {
|
||||||
|
return this.buildConfigDom();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _hasChoices() {
|
||||||
|
return this._choiceValues.get().length > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssChoiceField = styled('div.field_clip', `
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssChoiceText = styled('div', `
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssDropdownIcon = styled(icon, `
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: ${colors.lightGreen};
|
||||||
|
min-width: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
`);
|
@ -1,78 +0,0 @@
|
|||||||
var _ = require('underscore');
|
|
||||||
var dom = require('../lib/dom');
|
|
||||||
var dispose = require('../lib/dispose');
|
|
||||||
var kd = require('../lib/koDom');
|
|
||||||
var TextBox = require('./TextBox');
|
|
||||||
const G = require('../lib/browserGlobals').get('URL');
|
|
||||||
|
|
||||||
const {colors, testId} = require('app/client/ui2018/cssVars');
|
|
||||||
const {icon} = require('app/client/ui2018/icons');
|
|
||||||
const {styled} = require('grainjs');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a widget for displaying links. Links can entered directly or following a title.
|
|
||||||
* The last entry following a space is used as the url.
|
|
||||||
* ie 'google https://www.google.com' would apears as 'google' to the user but link to the url.
|
|
||||||
*/
|
|
||||||
function HyperLinkTextBox(field) {
|
|
||||||
TextBox.call(this, field);
|
|
||||||
}
|
|
||||||
dispose.makeDisposable(HyperLinkTextBox);
|
|
||||||
_.extend(HyperLinkTextBox.prototype, TextBox.prototype);
|
|
||||||
|
|
||||||
HyperLinkTextBox.prototype.buildDom = function(row) {
|
|
||||||
var value = row[this.field.colId()];
|
|
||||||
return cssFieldClip(
|
|
||||||
kd.style('text-align', this.alignment),
|
|
||||||
kd.toggleClass('text_wrapping', this.wrapping),
|
|
||||||
kd.maybe(value, () =>
|
|
||||||
dom('a',
|
|
||||||
kd.attr('href', () => _constructUrl(value())),
|
|
||||||
kd.attr('target', '_blank'),
|
|
||||||
// As per Google and Mozilla recommendations to prevent opened links
|
|
||||||
// from running on the same process as Grist:
|
|
||||||
// https://developers.google.com/web/tools/lighthouse/audits/noopener
|
|
||||||
kd.attr('rel', 'noopener noreferrer'),
|
|
||||||
cssLinkIcon('FieldLink'),
|
|
||||||
testId('tb-link')
|
|
||||||
)
|
|
||||||
),
|
|
||||||
kd.text(() => _formatValue(value()))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats value like `foo bar baz` by discarding `baz` and returning `foo bar`.
|
|
||||||
*/
|
|
||||||
function _formatValue(value) {
|
|
||||||
// value might be null, at least transiently. Handle it to avoid an exception.
|
|
||||||
if (typeof value !== 'string') { return value; }
|
|
||||||
const index = value.lastIndexOf(' ');
|
|
||||||
return index >= 0 ? value.slice(0, index) : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given value like `foo bar baz`, constructs URL by checking if `baz` is a valid URL and,
|
|
||||||
* if not, prepending `http://`.
|
|
||||||
*/
|
|
||||||
function _constructUrl(value) {
|
|
||||||
const url = value.slice(value.lastIndexOf(' ') + 1);
|
|
||||||
try {
|
|
||||||
// Try to construct a valid URL
|
|
||||||
return (new G.URL(url)).toString();
|
|
||||||
} catch (e) {
|
|
||||||
// Not a valid URL, so try to prefix it with http
|
|
||||||
return 'http://' + url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cssFieldClip = styled('div.field_clip', `
|
|
||||||
color: ${colors.lightGreen};
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssLinkIcon = styled(icon, `
|
|
||||||
background-color: ${colors.lightGreen};
|
|
||||||
margin: -1px 2px 2px 0;
|
|
||||||
`);
|
|
||||||
|
|
||||||
module.exports = HyperLinkTextBox;
|
|
71
app/client/widgets/HyperLinkTextBox.ts
Normal file
71
app/client/widgets/HyperLinkTextBox.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import {DataRowModel} from 'app/client/models/DataRowModel';
|
||||||
|
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
|
||||||
|
import {colors, testId} from 'app/client/ui2018/cssVars';
|
||||||
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
|
import {NTextBox} from 'app/client/widgets/NTextBox';
|
||||||
|
import {dom, styled} from 'grainjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a widget for displaying links. Links can entered directly or following a title.
|
||||||
|
* The last entry following a space is used as the url.
|
||||||
|
* ie 'google https://www.google.com' would apears as 'google' to the user but link to the url.
|
||||||
|
*/
|
||||||
|
export class HyperLinkTextBox extends NTextBox {
|
||||||
|
constructor(field: ViewFieldRec) {
|
||||||
|
super(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildDom(row: DataRowModel) {
|
||||||
|
const value = row.cells[this.field.colId()];
|
||||||
|
return cssFieldClip(
|
||||||
|
dom.style('text-align', this.alignment),
|
||||||
|
dom.cls('text_wrapping', this.wrapping),
|
||||||
|
dom.maybe(value, () =>
|
||||||
|
dom('a',
|
||||||
|
dom.attr('href', (use) => _constructUrl(use(value))),
|
||||||
|
dom.attr('target', '_blank'),
|
||||||
|
// As per Google and Mozilla recommendations to prevent opened links
|
||||||
|
// from running on the same process as Grist:
|
||||||
|
// https://developers.google.com/web/tools/lighthouse/audits/noopener
|
||||||
|
dom.attr('rel', 'noopener noreferrer'),
|
||||||
|
cssLinkIcon('FieldLink'),
|
||||||
|
testId('tb-link')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
dom.text((use) => _formatValue(use(value))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats value like `foo bar baz` by discarding `baz` and returning `foo bar`.
|
||||||
|
*/
|
||||||
|
function _formatValue(value: string|null|undefined): string {
|
||||||
|
if (typeof value !== 'string') { return ''; }
|
||||||
|
const index = value.lastIndexOf(' ');
|
||||||
|
return index >= 0 ? value.slice(0, index) : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given value like `foo bar baz`, constructs URL by checking if `baz` is a valid URL and,
|
||||||
|
* if not, prepending `http://`.
|
||||||
|
*/
|
||||||
|
function _constructUrl(value: string): string {
|
||||||
|
const url = value.slice(value.lastIndexOf(' ') + 1);
|
||||||
|
try {
|
||||||
|
// Try to construct a valid URL
|
||||||
|
return (new URL(url)).toString();
|
||||||
|
} catch (e) {
|
||||||
|
// Not a valid URL, so try to prefix it with http
|
||||||
|
return 'http://' + url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssFieldClip = styled('div.field_clip', `
|
||||||
|
color: ${colors.lightGreen};
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssLinkIcon = styled(icon, `
|
||||||
|
background-color: ${colors.lightGreen};
|
||||||
|
margin: -1px 2px 2px 0;
|
||||||
|
`);
|
@ -5,7 +5,7 @@ import {cssRow} from 'app/client/ui/RightPanel';
|
|||||||
import {alignmentSelect, makeButtonSelect} from 'app/client/ui2018/buttonSelect';
|
import {alignmentSelect, makeButtonSelect} from 'app/client/ui2018/buttonSelect';
|
||||||
import {testId} from 'app/client/ui2018/cssVars';
|
import {testId} from 'app/client/ui2018/cssVars';
|
||||||
import {NewAbstractWidget} from 'app/client/widgets/NewAbstractWidget';
|
import {NewAbstractWidget} from 'app/client/widgets/NewAbstractWidget';
|
||||||
import {Computed, dom, Observable} from 'grainjs';
|
import {dom, DomContents, fromKo, Observable} from 'grainjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TextBox - The most basic widget for displaying text information.
|
* TextBox - The most basic widget for displaying text information.
|
||||||
@ -18,27 +18,15 @@ export class NTextBox extends NewAbstractWidget {
|
|||||||
super(field);
|
super(field);
|
||||||
|
|
||||||
this.alignment = fromKoSave<string>(this.options.prop('alignment'));
|
this.alignment = fromKoSave<string>(this.options.prop('alignment'));
|
||||||
const wrap = this.options.prop('wrap');
|
this.wrapping = fromKo(this.field.wrapping);
|
||||||
|
|
||||||
this.wrapping = Computed.create(this, (use) => {
|
|
||||||
const w = use(wrap);
|
|
||||||
if (w === null || w === undefined) {
|
|
||||||
// When user has yet to specify a desired wrapping state, GridView and DetailView have
|
|
||||||
// different default states. GridView defaults to wrapping disabled, while DetailView
|
|
||||||
// defaults to wrapping enabled.
|
|
||||||
return (this.field.viewSection().parentKey() === 'record') ? false : true;
|
|
||||||
} else {
|
|
||||||
return w;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.autoDispose(this.wrapping.addListener(() => {
|
this.autoDispose(this.wrapping.addListener(() => {
|
||||||
this.field.viewSection().events.trigger('rowHeightChange');
|
this.field.viewSection().events.trigger('rowHeightChange');
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
public buildConfigDom() {
|
public buildConfigDom(): DomContents {
|
||||||
return dom('div',
|
return [
|
||||||
cssRow(
|
cssRow(
|
||||||
alignmentSelect(this.alignment),
|
alignmentSelect(this.alignment),
|
||||||
dom('div', {style: 'margin-left: 8px;'},
|
dom('div', {style: 'margin-left: 8px;'},
|
||||||
@ -46,7 +34,7 @@ export class NTextBox extends NewAbstractWidget {
|
|||||||
testId('tb-wrap-text')
|
testId('tb-wrap-text')
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public buildDom(row: DataRowModel) {
|
public buildDom(row: DataRowModel) {
|
||||||
|
@ -10,7 +10,7 @@ import {cssLabel, cssRow} from 'app/client/ui/RightPanel';
|
|||||||
import {colorSelect} from 'app/client/ui2018/buttonSelect';
|
import {colorSelect} from 'app/client/ui2018/buttonSelect';
|
||||||
import {colors, testId} from 'app/client/ui2018/cssVars';
|
import {colors, testId} from 'app/client/ui2018/cssVars';
|
||||||
import {BaseFormatter, createFormatter} from 'app/common/ValueFormatter';
|
import {BaseFormatter, createFormatter} from 'app/common/ValueFormatter';
|
||||||
import {Disposable, fromKo, Observable, styled} from 'grainjs';
|
import {Disposable, DomContents, fromKo, Observable, styled} from 'grainjs';
|
||||||
import * as ko from 'knockout';
|
import * as ko from 'knockout';
|
||||||
|
|
||||||
|
|
||||||
@ -43,13 +43,13 @@ export abstract class NewAbstractWidget extends Disposable {
|
|||||||
/**
|
/**
|
||||||
* Builds the DOM showing configuration buttons and fields in the sidebar.
|
* Builds the DOM showing configuration buttons and fields in the sidebar.
|
||||||
*/
|
*/
|
||||||
public buildConfigDom(): Element|null { return null; }
|
public buildConfigDom(): DomContents { return null; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the transform prompt config DOM in the few cases where it is necessary.
|
* Builds the transform prompt config DOM in the few cases where it is necessary.
|
||||||
* Child classes need not override this function if they do not require transform config options.
|
* Child classes need not override this function if they do not require transform config options.
|
||||||
*/
|
*/
|
||||||
public buildTransformConfigDom(): Element|null { return null; }
|
public buildTransformConfigDom(): DomContents { return null; }
|
||||||
|
|
||||||
public buildColorConfigDom(): Element[] {
|
public buildColorConfigDom(): Element[] {
|
||||||
return [
|
return [
|
||||||
|
@ -10,7 +10,7 @@ import {icon} from 'app/client/ui2018/icons';
|
|||||||
import {NTextBox} from 'app/client/widgets/NTextBox';
|
import {NTextBox} from 'app/client/widgets/NTextBox';
|
||||||
import {clamp} from 'app/common/gutil';
|
import {clamp} from 'app/common/gutil';
|
||||||
import {buildNumberFormat, NumberFormatOptions, NumMode, NumSign} from 'app/common/NumberFormat';
|
import {buildNumberFormat, NumberFormatOptions, NumMode, NumSign} from 'app/common/NumberFormat';
|
||||||
import {Computed, dom, DomElementArg, fromKo, MultiHolder, Observable, styled} from 'grainjs';
|
import {Computed, dom, DomContents, DomElementArg, fromKo, MultiHolder, Observable, styled} from 'grainjs';
|
||||||
|
|
||||||
|
|
||||||
const modeOptions: Array<ISelectorOption<NumMode>> = [
|
const modeOptions: Array<ISelectorOption<NumMode>> = [
|
||||||
@ -32,7 +32,7 @@ export class NumericTextBox extends NTextBox {
|
|||||||
super(field);
|
super(field);
|
||||||
}
|
}
|
||||||
|
|
||||||
public buildConfigDom() {
|
public buildConfigDom(): DomContents {
|
||||||
// Holder for all computeds created here. It gets disposed with the returned DOM element.
|
// Holder for all computeds created here. It gets disposed with the returned DOM element.
|
||||||
const holder = new MultiHolder();
|
const holder = new MultiHolder();
|
||||||
|
|
||||||
@ -67,10 +67,11 @@ export class NumericTextBox extends NTextBox {
|
|||||||
const setMode = (val: NumMode) => setSave('numMode', val !== numMode.get() ? val : undefined);
|
const setMode = (val: NumMode) => setSave('numMode', val !== numMode.get() ? val : undefined);
|
||||||
const setSign = (val: NumSign) => setSave('numSign', val !== numSign.get() ? val : undefined);
|
const setSign = (val: NumSign) => setSave('numSign', val !== numSign.get() ? val : undefined);
|
||||||
|
|
||||||
return dom.update(super.buildConfigDom(),
|
return [
|
||||||
dom.autoDispose(holder),
|
super.buildConfigDom(),
|
||||||
cssLabel('Number Format'),
|
cssLabel('Number Format'),
|
||||||
cssRow(
|
cssRow(
|
||||||
|
dom.autoDispose(holder),
|
||||||
makeButtonSelect(numMode, modeOptions, setMode, {}, cssModeSelect.cls(''), testId('numeric-mode')),
|
makeButtonSelect(numMode, modeOptions, setMode, {}, cssModeSelect.cls(''), testId('numeric-mode')),
|
||||||
makeButtonSelect(numSign, signOptions, setSign, {}, cssSignSelect.cls(''), testId('numeric-sign')),
|
makeButtonSelect(numSign, signOptions, setSign, {}, cssSignSelect.cls(''), testId('numeric-sign')),
|
||||||
),
|
),
|
||||||
@ -79,7 +80,7 @@ export class NumericTextBox extends NTextBox {
|
|||||||
decimals('min', minDecimals, defaultMin, setMinDecimals, testId('numeric-min-decimals')),
|
decimals('min', minDecimals, defaultMin, setMinDecimals, testId('numeric-min-decimals')),
|
||||||
decimals('max', maxDecimals, defaultMax, setMaxDecimals, testId('numeric-max-decimals')),
|
decimals('max', maxDecimals, defaultMax, setMaxDecimals, testId('numeric-max-decimals')),
|
||||||
),
|
),
|
||||||
);
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,112 +0,0 @@
|
|||||||
var _ = require('underscore');
|
|
||||||
var ko = require('knockout');
|
|
||||||
|
|
||||||
var gutil = require('app/common/gutil');
|
|
||||||
|
|
||||||
var dispose = require('../lib/dispose');
|
|
||||||
var dom = require('../lib/dom');
|
|
||||||
var kd = require('../lib/koDom');
|
|
||||||
|
|
||||||
var TextBox = require('./TextBox');
|
|
||||||
|
|
||||||
const {colors, testId} = require('app/client/ui2018/cssVars');
|
|
||||||
const {icon} = require('app/client/ui2018/icons');
|
|
||||||
const {select} = require('app/client/ui2018/menus');
|
|
||||||
const {cssLabel, cssRow} = require('app/client/ui/RightPanel');
|
|
||||||
const {isVersions} = require('app/common/gristTypes');
|
|
||||||
const {Computed, styled} = require('grainjs');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reference - The widget for displaying references to another table's records.
|
|
||||||
*/
|
|
||||||
function Reference(field) {
|
|
||||||
TextBox.call(this, field);
|
|
||||||
var col = field.column();
|
|
||||||
this.field = field;
|
|
||||||
this.colId = col.colId;
|
|
||||||
this.docModel = this.field._table.docModel;
|
|
||||||
|
|
||||||
this._refValueFormatter = this.autoDispose(ko.computed(() =>
|
|
||||||
field.createVisibleColFormatter()));
|
|
||||||
|
|
||||||
this._visibleColRef = Computed.create(this, (use) => use(this.field.visibleColRef));
|
|
||||||
// Note that saveOnly is used here to prevent display value flickering on visible col change.
|
|
||||||
this._visibleColRef.onWrite((val) => this.field.visibleColRef.saveOnly(val));
|
|
||||||
|
|
||||||
this._validCols = Computed.create(this, (use) => {
|
|
||||||
const refTable = use(use(this.field.column).refTable);
|
|
||||||
if (!refTable) { return []; }
|
|
||||||
return use(use(refTable.columns))
|
|
||||||
.filter(col => !use(col.isHiddenCol))
|
|
||||||
.map(col => ({
|
|
||||||
label: use(col.label),
|
|
||||||
value: col.getRowId(),
|
|
||||||
icon: 'FieldColumn',
|
|
||||||
disabled: gutil.startsWith(use(col.type), 'Ref:') || use(col.isTransforming)
|
|
||||||
}))
|
|
||||||
.concat([{label: 'Row ID', value: 0, icon: 'FieldColumn'}]);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Computed returns a function that formats cell values.
|
|
||||||
this._formatValue = this.autoDispose(ko.computed(() => {
|
|
||||||
// If the field is pulling values from a display column, use a general-purpose formatter.
|
|
||||||
if (this.field.displayColRef() !== this.field.colRef()) {
|
|
||||||
const fmt = this._refValueFormatter();
|
|
||||||
return (val) => fmt.formatAny(val);
|
|
||||||
} else {
|
|
||||||
const refTable = this.field.column().refTable();
|
|
||||||
const refTableId = refTable ? refTable.tableId() : "";
|
|
||||||
return (val) => val > 0 ? `${refTableId}[${val}]` : "";
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
dispose.makeDisposable(Reference);
|
|
||||||
_.extend(Reference.prototype, TextBox.prototype);
|
|
||||||
|
|
||||||
Reference.prototype.buildConfigDom = function() {
|
|
||||||
return [
|
|
||||||
cssLabel('SHOW COLUMN'),
|
|
||||||
cssRow(
|
|
||||||
select(this._visibleColRef, this._validCols),
|
|
||||||
testId('fbuilder-ref-col-select')
|
|
||||||
)
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
Reference.prototype.buildTransformConfigDom = function() {
|
|
||||||
return this.buildConfigDom();
|
|
||||||
};
|
|
||||||
|
|
||||||
Reference.prototype.buildDom = function(row, options) {
|
|
||||||
const formattedValue = ko.computed(() => {
|
|
||||||
if (row._isAddRow() || this.isDisposed() || this.field.displayColModel().isDisposed()) {
|
|
||||||
// Work around JS errors during certain changes (noticed when visibleCol field gets removed
|
|
||||||
// for a column using per-field settings).
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
const value = row.cells[this.field.displayColModel().colId()];
|
|
||||||
if (!value) { return ""; }
|
|
||||||
const content = value();
|
|
||||||
if (isVersions(content)) {
|
|
||||||
// We can arrive here if the reference value is unchanged (viewed as a foreign key)
|
|
||||||
// but the content of its displayCol has changed. Postponing doing anything about
|
|
||||||
// this until we have three-way information for computed columns. For now,
|
|
||||||
// just showing one version of the cell. TODO: elaborate.
|
|
||||||
return this._formatValue()(content[1].local || content[1].parent);
|
|
||||||
}
|
|
||||||
return this._formatValue()(content);
|
|
||||||
});
|
|
||||||
return dom('div.field_clip',
|
|
||||||
dom.autoDispose(formattedValue),
|
|
||||||
cssRefIcon('FieldReference',
|
|
||||||
testId('ref-link-icon')
|
|
||||||
),
|
|
||||||
kd.text(formattedValue));
|
|
||||||
};
|
|
||||||
|
|
||||||
const cssRefIcon = styled(icon, `
|
|
||||||
background-color: ${colors.slate};
|
|
||||||
margin: -1px 2px 2px 0;
|
|
||||||
`);
|
|
||||||
|
|
||||||
module.exports = Reference;
|
|
106
app/client/widgets/Reference.ts
Normal file
106
app/client/widgets/Reference.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import {DataRowModel} from 'app/client/models/DataRowModel';
|
||||||
|
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
|
||||||
|
import {cssLabel, cssRow} from 'app/client/ui/RightPanel';
|
||||||
|
import {colors, testId} from 'app/client/ui2018/cssVars';
|
||||||
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
|
import {IOptionFull, select} from 'app/client/ui2018/menus';
|
||||||
|
import {NTextBox} from 'app/client/widgets/NTextBox';
|
||||||
|
import {isVersions} from 'app/common/gristTypes';
|
||||||
|
import {BaseFormatter} from 'app/common/ValueFormatter';
|
||||||
|
import {Computed, dom, styled} from 'grainjs';
|
||||||
|
import * as ko from 'knockout';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference - The widget for displaying references to another table's records.
|
||||||
|
*/
|
||||||
|
export class Reference extends NTextBox {
|
||||||
|
private _refValueFormatter: ko.Computed<BaseFormatter>;
|
||||||
|
private _visibleColRef: Computed<number>;
|
||||||
|
private _validCols: Computed<Array<IOptionFull<number>>>;
|
||||||
|
private _formatValue: Computed<(val: any) => string>;
|
||||||
|
|
||||||
|
constructor(field: ViewFieldRec) {
|
||||||
|
super(field);
|
||||||
|
|
||||||
|
this._refValueFormatter = this.autoDispose(ko.computed(() =>
|
||||||
|
field.createVisibleColFormatter()));
|
||||||
|
|
||||||
|
this._visibleColRef = Computed.create(this, (use) => use(this.field.visibleColRef));
|
||||||
|
// Note that saveOnly is used here to prevent display value flickering on visible col change.
|
||||||
|
this._visibleColRef.onWrite((val) => this.field.visibleColRef.saveOnly(val));
|
||||||
|
|
||||||
|
this._validCols = Computed.create(this, (use) => {
|
||||||
|
const refTable = use(use(this.field.column).refTable);
|
||||||
|
if (!refTable) { return []; }
|
||||||
|
return use(use(refTable.columns).getObservable())
|
||||||
|
.filter(col => !use(col.isHiddenCol))
|
||||||
|
.map<IOptionFull<number>>(col => ({
|
||||||
|
label: use(col.label),
|
||||||
|
value: col.getRowId(),
|
||||||
|
icon: 'FieldColumn',
|
||||||
|
disabled: use(col.type).startsWith('Ref:') || use(col.isTransforming)
|
||||||
|
}))
|
||||||
|
.concat([{label: 'Row ID', value: 0, icon: 'FieldColumn'}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Computed returns a function that formats cell values.
|
||||||
|
this._formatValue = Computed.create(this, (use) => {
|
||||||
|
// If the field is pulling values from a display column, use a general-purpose formatter.
|
||||||
|
if (use(this.field.displayColRef) !== use(this.field.colRef)) {
|
||||||
|
const fmt = use(this._refValueFormatter);
|
||||||
|
return (val: any) => fmt.formatAny(val);
|
||||||
|
} else {
|
||||||
|
const refTable = use(use(this.field.column).refTable);
|
||||||
|
const refTableId = refTable ? use(refTable.tableId) : "";
|
||||||
|
return (val: any) => val > 0 ? `${refTableId}[${val}]` : "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildConfigDom() {
|
||||||
|
return [
|
||||||
|
cssLabel('SHOW COLUMN'),
|
||||||
|
cssRow(
|
||||||
|
select(this._visibleColRef, this._validCols),
|
||||||
|
testId('fbuilder-ref-col-select')
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildTransformConfigDom() {
|
||||||
|
return this.buildConfigDom();
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildDom(row: DataRowModel) {
|
||||||
|
const formattedValue = Computed.create(null, (use) => {
|
||||||
|
if (use(row._isAddRow) || this.isDisposed() || use(this.field.displayColModel).isDisposed()) {
|
||||||
|
// Work around JS errors during certain changes (noticed when visibleCol field gets removed
|
||||||
|
// for a column using per-field settings).
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const value = row.cells[use(use(this.field.displayColModel).colId)];
|
||||||
|
if (!value) { return ""; }
|
||||||
|
const content = use(value);
|
||||||
|
if (isVersions(content)) {
|
||||||
|
// We can arrive here if the reference value is unchanged (viewed as a foreign key)
|
||||||
|
// but the content of its displayCol has changed. Postponing doing anything about
|
||||||
|
// this until we have three-way information for computed columns. For now,
|
||||||
|
// just showing one version of the cell. TODO: elaborate.
|
||||||
|
return use(this._formatValue)(content[1].local || content[1].parent);
|
||||||
|
}
|
||||||
|
return use(this._formatValue)(content);
|
||||||
|
});
|
||||||
|
return dom('div.field_clip',
|
||||||
|
dom.autoDispose(formattedValue),
|
||||||
|
cssRefIcon('FieldReference',
|
||||||
|
testId('ref-link-icon')
|
||||||
|
),
|
||||||
|
dom.text(formattedValue)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssRefIcon = styled(icon, `
|
||||||
|
background-color: ${colors.slate};
|
||||||
|
margin: -1px 2px 2px 0;
|
||||||
|
`);
|
@ -1,71 +0,0 @@
|
|||||||
var _ = require('underscore');
|
|
||||||
var ko = require('knockout');
|
|
||||||
var dom = require('../lib/dom');
|
|
||||||
var dispose = require('../lib/dispose');
|
|
||||||
var kd = require('../lib/koDom');
|
|
||||||
var AbstractWidget = require('./AbstractWidget');
|
|
||||||
var modelUtil = require('../models/modelUtil');
|
|
||||||
|
|
||||||
const {fromKoSave} = require('app/client/lib/fromKoSave');
|
|
||||||
const {alignmentSelect, buttonToggleSelect} = require('app/client/ui2018/buttonSelect');
|
|
||||||
const {testId} = require('app/client/ui2018/cssVars');
|
|
||||||
const {cssRow} = require('app/client/ui/RightPanel');
|
|
||||||
const {Computed} = require('grainjs');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TextBox - The most basic widget for displaying text information.
|
|
||||||
*/
|
|
||||||
function TextBox(field) {
|
|
||||||
AbstractWidget.call(this, field);
|
|
||||||
this.alignment = this.options.prop('alignment');
|
|
||||||
let wrap = this.options.prop('wrap');
|
|
||||||
this.wrapping = this.autoDispose(ko.computed({
|
|
||||||
read: () => {
|
|
||||||
let w = wrap();
|
|
||||||
if (w === null || w === undefined) {
|
|
||||||
// When user has yet to specify a desired wrapping state, GridView and DetailView have
|
|
||||||
// different default states. GridView defaults to wrapping disabled, while DetailView
|
|
||||||
// defaults to wrapping enabled.
|
|
||||||
return (this.field.viewSection().parentKey() === 'record') ? false : true;
|
|
||||||
} else {
|
|
||||||
return w;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
write: val => wrap(val)
|
|
||||||
}));
|
|
||||||
modelUtil.addSaveInterface(this.wrapping, val => wrap.saveOnly(val));
|
|
||||||
|
|
||||||
this.autoDispose(this.wrapping.subscribe(() =>
|
|
||||||
this.field.viewSection().events.trigger('rowHeightChange')
|
|
||||||
));
|
|
||||||
|
|
||||||
}
|
|
||||||
dispose.makeDisposable(TextBox);
|
|
||||||
_.extend(TextBox.prototype, AbstractWidget.prototype);
|
|
||||||
|
|
||||||
TextBox.prototype.buildConfigDom = function() {
|
|
||||||
const wrapping = Computed.create(null, use => use(this.wrapping));
|
|
||||||
wrapping.onWrite((val) => modelUtil.setSaveValue(this.wrapping, Boolean(val)));
|
|
||||||
|
|
||||||
return dom('div',
|
|
||||||
cssRow(
|
|
||||||
dom.autoDispose(wrapping),
|
|
||||||
alignmentSelect(fromKoSave(this.alignment)),
|
|
||||||
dom('div', {style: 'margin-left: 8px;'},
|
|
||||||
buttonToggleSelect(wrapping, [{value: true, icon: 'Wrap'}]),
|
|
||||||
testId('tb-wrap-text')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
TextBox.prototype.buildDom = function(row) {
|
|
||||||
let value = row[this.field.colId()];
|
|
||||||
return dom('div.field_clip',
|
|
||||||
kd.style('text-align', this.alignment),
|
|
||||||
kd.toggleClass('text_wrapping', this.wrapping),
|
|
||||||
kd.text(() => row._isAddRow() ? '' : this.valueFormatter().format(value()))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = TextBox;
|
|
@ -7,6 +7,9 @@ const UserType = require('./UserType');
|
|||||||
const {HyperLinkEditor} = require('./HyperLinkEditor');
|
const {HyperLinkEditor} = require('./HyperLinkEditor');
|
||||||
const {NTextEditor} = require('./NTextEditor');
|
const {NTextEditor} = require('./NTextEditor');
|
||||||
const {ReferenceEditor} = require('./ReferenceEditor');
|
const {ReferenceEditor} = require('./ReferenceEditor');
|
||||||
|
const {HyperLinkTextBox} = require('./HyperLinkTextBox');
|
||||||
|
const {ChoiceTextBox } = require('./ChoiceTextBox');
|
||||||
|
const {Reference} = require('./Reference');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert the name of a widget to its implementation.
|
* Convert the name of a widget to its implementation.
|
||||||
@ -15,15 +18,15 @@ const nameToWidget = {
|
|||||||
'TextBox': NTextBox,
|
'TextBox': NTextBox,
|
||||||
'TextEditor': NTextEditor,
|
'TextEditor': NTextEditor,
|
||||||
'NumericTextBox': NumericTextBox,
|
'NumericTextBox': NumericTextBox,
|
||||||
'HyperLinkTextBox': require('./HyperLinkTextBox'),
|
'HyperLinkTextBox': HyperLinkTextBox,
|
||||||
'HyperLinkEditor': HyperLinkEditor,
|
'HyperLinkEditor': HyperLinkEditor,
|
||||||
'Spinner': Spinner,
|
'Spinner': Spinner,
|
||||||
'CheckBox': require('./CheckBox'),
|
'CheckBox': require('./CheckBox'),
|
||||||
'CheckBoxEditor': require('./CheckBoxEditor'),
|
'CheckBoxEditor': require('./CheckBoxEditor'),
|
||||||
'Reference': require('./Reference'),
|
'Reference': Reference,
|
||||||
'Switch': require('./Switch'),
|
'Switch': require('./Switch'),
|
||||||
'ReferenceEditor': ReferenceEditor,
|
'ReferenceEditor': ReferenceEditor,
|
||||||
'ChoiceTextBox': require('./ChoiceTextBox'),
|
'ChoiceTextBox': ChoiceTextBox,
|
||||||
'ChoiceEditor': require('./ChoiceEditor'),
|
'ChoiceEditor': require('./ChoiceEditor'),
|
||||||
'DateTimeTextBox': require('./DateTimeTextBox'),
|
'DateTimeTextBox': require('./DateTimeTextBox'),
|
||||||
'DateTextBox': require('./DateTextBox'),
|
'DateTextBox': require('./DateTextBox'),
|
||||||
|
Loading…
Reference in New Issue
Block a user