gristlabs_grist-core/app/client/widgets/Reference.ts
Alex Hall c470c4041b (core) Use visibleCol instead of displayCol with createFormatter
Summary:
Some things (like rendering cells) use the `visibleCol` for `createFormatter`, while other things (like `CopySelection`) used the `displayCol`. For references, the display column has type Any and doesn't know about the original formatting. This resulted in formatting being lost when copying from reference columns even though formatting was preserved when copying from the original (visible) column which looked identical. This diff fixes this and ensures that `createFormatter` is always used with the `visibleCol`. This was agreed on in https://grist.slack.com/archives/C0234CPPXPA/p1639571321043000

Additionally:

- Replaces the functions `createVisibleColFormatter` computed properties `visibleColFormatter` as suggested by a `TODO`.
- Extracts common code from `createVisibleColFormatter` in `ColumnRec` and `ViewFieldRec`

Test Plan: Fixed a test in CopyPaste which displayed the previous inconsistent behaviour.

Reviewers: jarek

Reviewed By: jarek

Differential Revision: https://phab.getgrist.com/D3189
2021-12-16 22:19:36 +02:00

138 lines
5.2 KiB
TypeScript

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 {isFullReferencingType, isVersions} from 'app/common/gristTypes';
import {Computed, dom, styled} from 'grainjs';
/**
* Reference - The widget for displaying references to another table's records.
*/
export class Reference extends NTextBox {
protected _formatValue: Computed<(val: any) => string>;
private _visibleColRef: Computed<number>;
private _validCols: Computed<Array<IOptionFull<number>>>;
constructor(field: ViewFieldRec) {
super(field);
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: isFullReferencingType(use(col.type)) || 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.field.visibleColFormatter);
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 [
this.buildTransformConfigDom(),
cssLabel('CELL FORMAT'),
super.buildConfigDom()
];
}
public buildTransformConfigDom() {
return [
cssLabel('SHOW COLUMN'),
cssRow(
select(this._visibleColRef, this._validCols),
testId('fbuilder-ref-col-select')
)
];
}
public buildDom(row: DataRowModel) {
// Note: we require 2 observables here because changes to the cell value (reference id)
// and the display value (display column) are not bundled. This can cause `formattedValue`
// to briefly display incorrect values (e.g. [Blank] when adding a reference to an empty cell)
// because the cell value changes before the display column has a chance to update.
//
// TODO: Look into a better solution (perhaps updating the display formula to return [Blank]).
const referenceId = Computed.create(null, (use) => {
const id = row.cells[use(this.field.colId)];
return id && use(id);
});
const formattedValue = Computed.create(null, (use) => {
let [value, hasBlankReference] = ['', false];
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 {value, hasBlankReference};
}
const displayValueObs = row.cells[use(use(this.field.displayColModel).colId)];
if (!displayValueObs) {
return {value, hasBlankReference};
}
const displayValue = use(displayValueObs);
value = isVersions(displayValue) ?
// 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.
use(this._formatValue)(displayValue[1].local || displayValue[1].parent) :
use(this._formatValue)(displayValue);
hasBlankReference = referenceId.get() !== 0 && value.trim() === '';
return {value, hasBlankReference};
});
return cssRef(
dom.autoDispose(formattedValue),
dom.autoDispose(referenceId),
cssRef.cls('-blank', use => use(formattedValue).hasBlankReference),
dom.style('text-align', this.alignment),
dom.cls('text_wrapping', this.wrapping),
cssRefIcon('FieldReference', testId('ref-link-icon')),
dom.text(use => {
if (use(referenceId) === 0) { return ''; }
if (use(formattedValue).hasBlankReference) { return '[Blank]'; }
return use(formattedValue).value;
})
);
}
}
const cssRefIcon = styled(icon, `
float: left;
background-color: ${colors.slate};
margin: -1px 2px 2px 0;
`);
const cssRef = styled('div.field_clip', `
&-blank {
color: ${colors.slate}
}
`);