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 {colors, testId} from 'app/client/ui2018/cssVars';
|
||||
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 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.
|
||||
public buildDom(maxRows: number = 6): DomElementArg {
|
||||
public buildDom(maxRows: number = 6): DomContents {
|
||||
return dom.domComputed(this._isEditing, (editMode) => {
|
||||
if (editMode) {
|
||||
// Edit mode dom.
|
||||
|
@ -58,6 +58,9 @@ export interface ViewFieldRec extends IRowModel<"_grist_Views_section_field"> {
|
||||
|
||||
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.
|
||||
activeFilter: modelUtil.CustomComputed<string>;
|
||||
|
||||
@ -180,6 +183,13 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
|
||||
this.widgetOptionsJson = modelUtil.jsonObservable(this._widgetOptionsStr,
|
||||
(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.
|
||||
this.activeFilter = modelUtil.customComputed({
|
||||
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 {testId} from 'app/client/ui2018/cssVars';
|
||||
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.
|
||||
@ -18,27 +18,15 @@ export class NTextBox extends NewAbstractWidget {
|
||||
super(field);
|
||||
|
||||
this.alignment = fromKoSave<string>(this.options.prop('alignment'));
|
||||
const wrap = this.options.prop('wrap');
|
||||
|
||||
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.wrapping = fromKo(this.field.wrapping);
|
||||
|
||||
this.autoDispose(this.wrapping.addListener(() => {
|
||||
this.field.viewSection().events.trigger('rowHeightChange');
|
||||
}));
|
||||
}
|
||||
|
||||
public buildConfigDom() {
|
||||
return dom('div',
|
||||
public buildConfigDom(): DomContents {
|
||||
return [
|
||||
cssRow(
|
||||
alignmentSelect(this.alignment),
|
||||
dom('div', {style: 'margin-left: 8px;'},
|
||||
@ -46,7 +34,7 @@ export class NTextBox extends NewAbstractWidget {
|
||||
testId('tb-wrap-text')
|
||||
)
|
||||
)
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
public buildDom(row: DataRowModel) {
|
||||
|
@ -10,7 +10,7 @@ import {cssLabel, cssRow} from 'app/client/ui/RightPanel';
|
||||
import {colorSelect} from 'app/client/ui2018/buttonSelect';
|
||||
import {colors, testId} from 'app/client/ui2018/cssVars';
|
||||
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';
|
||||
|
||||
|
||||
@ -43,13 +43,13 @@ export abstract class NewAbstractWidget extends Disposable {
|
||||
/**
|
||||
* 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.
|
||||
* 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[] {
|
||||
return [
|
||||
|
@ -10,7 +10,7 @@ import {icon} from 'app/client/ui2018/icons';
|
||||
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, DomElementArg, fromKo, MultiHolder, Observable, styled} from 'grainjs';
|
||||
import {Computed, dom, DomContents, DomElementArg, fromKo, MultiHolder, Observable, styled} from 'grainjs';
|
||||
|
||||
|
||||
const modeOptions: Array<ISelectorOption<NumMode>> = [
|
||||
@ -32,7 +32,7 @@ export class NumericTextBox extends NTextBox {
|
||||
super(field);
|
||||
}
|
||||
|
||||
public buildConfigDom() {
|
||||
public buildConfigDom(): DomContents {
|
||||
// Holder for all computeds created here. It gets disposed with the returned DOM element.
|
||||
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 setSign = (val: NumSign) => setSave('numSign', val !== numSign.get() ? val : undefined);
|
||||
|
||||
return dom.update(super.buildConfigDom(),
|
||||
dom.autoDispose(holder),
|
||||
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')),
|
||||
),
|
||||
@ -79,7 +80,7 @@ export class NumericTextBox extends NTextBox {
|
||||
decimals('min', minDecimals, defaultMin, setMinDecimals, testId('numeric-min-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 {NTextEditor} = require('./NTextEditor');
|
||||
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.
|
||||
@ -15,15 +18,15 @@ const nameToWidget = {
|
||||
'TextBox': NTextBox,
|
||||
'TextEditor': NTextEditor,
|
||||
'NumericTextBox': NumericTextBox,
|
||||
'HyperLinkTextBox': require('./HyperLinkTextBox'),
|
||||
'HyperLinkTextBox': HyperLinkTextBox,
|
||||
'HyperLinkEditor': HyperLinkEditor,
|
||||
'Spinner': Spinner,
|
||||
'CheckBox': require('./CheckBox'),
|
||||
'CheckBoxEditor': require('./CheckBoxEditor'),
|
||||
'Reference': require('./Reference'),
|
||||
'Reference': Reference,
|
||||
'Switch': require('./Switch'),
|
||||
'ReferenceEditor': ReferenceEditor,
|
||||
'ChoiceTextBox': require('./ChoiceTextBox'),
|
||||
'ChoiceTextBox': ChoiceTextBox,
|
||||
'ChoiceEditor': require('./ChoiceEditor'),
|
||||
'DateTimeTextBox': require('./DateTimeTextBox'),
|
||||
'DateTextBox': require('./DateTextBox'),
|
||||
|
Loading…
Reference in New Issue
Block a user