mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Adding UI for reverse columns
Summary: - Adding an UI for two-way reference column. - Reusing table name as label for the reverse column Test Plan: Updated Reviewers: georgegevoian Reviewed By: georgegevoian Differential Revision: https://phab.getgrist.com/D4344
This commit is contained in:
@@ -2,7 +2,7 @@ import { ColumnTransform } from 'app/client/components/ColumnTransform';
|
||||
import { Cursor } from 'app/client/components/Cursor';
|
||||
import { FormulaTransform } from 'app/client/components/FormulaTransform';
|
||||
import { GristDoc } from 'app/client/components/GristDoc';
|
||||
import { addColTypeSuffix, guessWidgetOptionsSync } from 'app/client/components/TypeConversion';
|
||||
import { addColTypeSuffix, guessWidgetOptionsSync, inferColTypeSuffix } from 'app/client/components/TypeConversion';
|
||||
import { TypeTransform } from 'app/client/components/TypeTransform';
|
||||
import { FloatingEditor } from 'app/client/widgets/FloatingEditor';
|
||||
import { UnsavedChange } from 'app/client/components/UnsavedChanges';
|
||||
@@ -365,7 +365,7 @@ export class FieldBuilder extends Disposable {
|
||||
// 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(); // same as this.origColumn.
|
||||
const calculatedType = addColTypeSuffix(newType, column, this._docModel);
|
||||
const calculatedType = inferColTypeSuffix(newType, column) ?? addColTypeSuffix(newType, column, this._docModel);
|
||||
const fields = this.field.viewSection.peek().selectedFields.peek();
|
||||
// If we selected multiple empty/formula columns, make the change for all of them.
|
||||
if (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {DropdownConditionConfig} from 'app/client/components/DropdownConditionConfig';
|
||||
import {
|
||||
FormFieldRulesConfig,
|
||||
FormOptionsSortConfig,
|
||||
FormSelectConfig
|
||||
} from 'app/client/components/Forms/FormConfig';
|
||||
import {DropdownConditionConfig} from 'app/client/components/DropdownConditionConfig';
|
||||
import {GristDoc} from 'app/client/components/GristDoc';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {DataRowModel} from 'app/client/models/DataRowModel';
|
||||
@@ -15,6 +15,7 @@ import {hideInPrintView, testId, theme} 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 {ReverseReferenceConfig} from 'app/client/widgets/ReverseReferenceConfig';
|
||||
import {isFullReferencingType, isVersions} from 'app/common/gristTypes';
|
||||
import {UIRowId} from 'app/plugin/GristAPI';
|
||||
import {Computed, dom, styled} from 'grainjs';
|
||||
@@ -58,6 +59,7 @@ export class Reference extends NTextBox {
|
||||
return [
|
||||
this.buildTransformConfigDom(),
|
||||
dom.create(DropdownConditionConfig, this.field, gristDoc),
|
||||
dom.create(ReverseReferenceConfig, this.field),
|
||||
cssLabel(t('CELL FORMAT')),
|
||||
super.buildConfigDom(gristDoc),
|
||||
];
|
||||
|
||||
236
app/client/widgets/ReverseReferenceConfig.ts
Normal file
236
app/client/widgets/ReverseReferenceConfig.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
import {allCommands} from 'app/client/components/commands';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {TableRec} from 'app/client/models/DocModel';
|
||||
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
|
||||
import {
|
||||
cssLabelText,
|
||||
cssRow,
|
||||
cssSeparator
|
||||
} from 'app/client/ui/RightPanelStyles';
|
||||
import {withInfoTooltip} from 'app/client/ui/tooltips';
|
||||
import {textButton} from 'app/client/ui2018/buttons';
|
||||
import {testId, theme, vars} from 'app/client/ui2018/cssVars';
|
||||
import {cssIconButton, icon} from 'app/client/ui2018/icons';
|
||||
import {confirmModal} from 'app/client/ui2018/modals';
|
||||
import {not} from 'app/common/gutil';
|
||||
import {Computed, Disposable, dom, styled} from 'grainjs';
|
||||
|
||||
const t = makeT('ReverseReferenceConfig');
|
||||
|
||||
/**
|
||||
* Configuratino for two-way reference column shown in the right panel.
|
||||
*/
|
||||
export class ReverseReferenceConfig extends Disposable {
|
||||
private _refTable: Computed<TableRec | null>;
|
||||
private _isConfigured: Computed<boolean>;
|
||||
private _reverseTable: Computed<string>;
|
||||
private _reverseColumn: Computed<string>;
|
||||
private _reverseType: Computed<string>;
|
||||
private _disabled: Computed<boolean>;
|
||||
|
||||
constructor(private _field: ViewFieldRec) {
|
||||
super();
|
||||
|
||||
this._refTable = Computed.create(this, (use) => use(use(this._field.column).refTable));
|
||||
this._isConfigured = Computed.create(this, (use) => {
|
||||
const column = use(this._field.column);
|
||||
return use(column.hasReverse);
|
||||
});
|
||||
this._reverseTable = Computed.create(this, this._refTable, (use, refTable) => {
|
||||
return refTable ? use(refTable.tableNameDef) : '';
|
||||
});
|
||||
this._reverseColumn = Computed.create(this, (use) => {
|
||||
const column = use(this._field.column);
|
||||
const reverseCol = use(column.reverseColModel);
|
||||
return reverseCol ? use(reverseCol.label) ?? use(reverseCol.colId) : '';
|
||||
});
|
||||
this._reverseType = Computed.create(this, (use) => {
|
||||
const column = use(this._field.column);
|
||||
const reverseCol = use(column.reverseColModel);
|
||||
return reverseCol ? use(reverseCol.pureType) : '';
|
||||
});
|
||||
this._disabled = Computed.create(this, (use) => {
|
||||
// If is formula or is trigger formula.
|
||||
const column = use(this._field.column);
|
||||
return Boolean(use(column.formula));
|
||||
});
|
||||
}
|
||||
|
||||
public buildDom() {
|
||||
return dom('div',
|
||||
dom.maybe(not(this._isConfigured), () => [
|
||||
cssRow(
|
||||
dom.style('margin-top', '16px'),
|
||||
cssRow.cls('-disabled', this._disabled),
|
||||
withInfoTooltip(
|
||||
textButton(
|
||||
t('Add two-way reference'),
|
||||
dom.on('click', (e) => this._toggle(e)),
|
||||
testId('add-reverse-columm'),
|
||||
dom.prop('disabled', this._disabled),
|
||||
),
|
||||
'twoWayReferences'
|
||||
),
|
||||
),
|
||||
]),
|
||||
dom.maybe(this._isConfigured, () => cssTwoWayConfig(
|
||||
// TWO-WAY REFERENCE (?) [Remove]
|
||||
cssRow(
|
||||
dom.style('justify-content', 'space-between'),
|
||||
withInfoTooltip(
|
||||
cssLabelText(
|
||||
t('Two-way Reference'),
|
||||
),
|
||||
'twoWayReferences'
|
||||
),
|
||||
cssIconButton(
|
||||
icon('Remove'),
|
||||
dom.on('click', (e) => this._toggle(e)),
|
||||
dom.style('cursor', 'pointer'),
|
||||
testId('remove-reverse-column'),
|
||||
),
|
||||
),
|
||||
cssRow(
|
||||
cssContent(
|
||||
cssClipLine(
|
||||
cssClipItem(
|
||||
cssCapitalize(t('Table'), dom.style('margin-right', '8px')),
|
||||
dom('span', dom.text(this._reverseTable)),
|
||||
),
|
||||
),
|
||||
cssFlexBetween(
|
||||
cssClipItem(
|
||||
cssCapitalize(t('Column'), dom.style('margin-right', '8px')),
|
||||
dom('span', dom.text(this._reverseColumn)),
|
||||
cssGrayText('(', dom.text(this._reverseType), ')')
|
||||
),
|
||||
cssIconButton(
|
||||
cssShowOnHover.cls(''),
|
||||
cssNoClip.cls(''),
|
||||
cssIconAccent('Pencil'),
|
||||
dom.on('click', () => this._editConfigClick()),
|
||||
dom.style('cursor', 'pointer'),
|
||||
testId('edit-reverse-column'),
|
||||
),
|
||||
),
|
||||
),
|
||||
testId('reverse-column-label'),
|
||||
),
|
||||
cssSeparator(
|
||||
dom.style('margin-top', '16px'),
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
private async _toggle(e: Event) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const column = this._field.column.peek();
|
||||
if (!this._isConfigured.get()) {
|
||||
await column.addReverseColumn();
|
||||
return;
|
||||
}
|
||||
const onConfirm = async () => {
|
||||
await column.removeReverseColumn();
|
||||
};
|
||||
|
||||
const revColumnLabel = column.reverseColModel.peek().label.peek() || column.reverseColModel.peek().colId.peek();
|
||||
const revTableName = column.reverseColModel.peek().table.peek().tableNameDef.peek();
|
||||
|
||||
const promptTitle = t('Delete column {{column}} in table {{table}}?', {
|
||||
column: dom('b', revColumnLabel),
|
||||
table: dom('b', revTableName),
|
||||
});
|
||||
|
||||
const myTable = column.table.peek().tableNameDef.peek();
|
||||
const myName = column.label.peek() || column.colId.peek();
|
||||
|
||||
const explanation = t('It is the reverse of the reference column {{column}} in table {{table}}.', {
|
||||
column: dom('b', myName),
|
||||
table: dom('b', myTable)
|
||||
});
|
||||
|
||||
confirmModal(
|
||||
promptTitle,
|
||||
t('Delete'),
|
||||
onConfirm,
|
||||
{
|
||||
explanation,
|
||||
width: 'fixed-wide'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async _editConfigClick() {
|
||||
const rawViewSection = this._refTable.get()?.rawViewSection.peek();
|
||||
if (!rawViewSection) { return; }
|
||||
await allCommands.showRawData.run(this._refTable.get()?.rawViewSectionRef.peek());
|
||||
const reverseColId = this._field.column.peek().reverseColModel.peek().colId.peek();
|
||||
if (!reverseColId) { return; } // might happen if it is censored.
|
||||
const targetField = rawViewSection.viewFields.peek().all()
|
||||
.find(f => f.colId.peek() === reverseColId);
|
||||
if (!targetField) { return; }
|
||||
await allCommands.setCursor.run(null, targetField);
|
||||
}
|
||||
}
|
||||
|
||||
const cssTwoWayConfig = styled('div', ``);
|
||||
const cssShowOnHover = styled('div', `
|
||||
visibility: hidden;
|
||||
.${cssTwoWayConfig.className}:hover & {
|
||||
visibility: visible;
|
||||
}
|
||||
`);
|
||||
|
||||
const cssContent = styled('div', `
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
`);
|
||||
|
||||
|
||||
const cssFlexRow = styled('div', `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
`);
|
||||
|
||||
const cssFlexBetween = styled(cssFlexRow, `
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
`);
|
||||
|
||||
const cssCapitalize = styled('span', `
|
||||
text-transform: uppercase;
|
||||
font-size: ${vars.xsmallFontSize};
|
||||
color: ${theme.lightText};
|
||||
`);
|
||||
|
||||
const cssClipLine = styled('div', `
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 3px;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
`);
|
||||
|
||||
const cssClipItem = styled('div', `
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`);
|
||||
|
||||
const cssNoClip = styled('div', `
|
||||
flex: none;
|
||||
`);
|
||||
|
||||
const cssGrayText = styled('span', `
|
||||
color: ${theme.lightText};
|
||||
margin-left: 4px;
|
||||
`);
|
||||
|
||||
const cssIconAccent = styled(icon, `
|
||||
--icon-color: ${theme.accentIcon};
|
||||
`);
|
||||
Reference in New Issue
Block a user