(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:
Jarosław Sadziński
2024-09-13 15:20:18 +02:00
parent da6c39aa50
commit e97a45143f
25 changed files with 1356 additions and 137 deletions

View File

@@ -63,6 +63,11 @@ export interface ColumnRec extends IRowModel<"_grist_Tables_column"> {
displayColModel: ko.Computed<ColumnRec>;
visibleColModel: ko.Computed<ColumnRec>;
// Reverse Ref/RefList column for this column. Only for Ref/RefList columns in two-way relations.
reverseColModel: ko.Computed<ColumnRec>;
// If this column has a relation.
hasReverse: ko.Computed<boolean>;
disableModifyBase: ko.Computed<boolean>; // True if column config can't be modified (name, type, etc.)
disableModify: ko.Computed<boolean>; // True if column can't be modified (is summary) or is being transformed.
disableEditData: ko.Computed<boolean>; // True to disable editing of the data in this column.
@@ -94,6 +99,12 @@ export interface ColumnRec extends IRowModel<"_grist_Tables_column"> {
saveDisplayFormula(formula: string): Promise<void>|undefined;
createValueParser(): (value: string) => any;
/** Helper method to add a reverse column (only for Ref/RefList) */
addReverseColumn(): Promise<void>;
/** Helper method to remove a reverse column (only for Ref/RefList) */
removeReverseColumn(): Promise<void>;
}
export function createColumnRec(this: ColumnRec, docModel: DocModel): void {
@@ -138,6 +149,8 @@ export function createColumnRec(this: ColumnRec, docModel: DocModel): void {
// The display column to use for the column, or the column itself when no displayCol is set.
this.displayColModel = refRecord(docModel.columns, this.displayColRef);
this.reverseColModel = refRecord(docModel.columns, this.reverseCol);
this.hasReverse = this.autoDispose(ko.pureComputed(() => Boolean(this.reverseColModel().id())));
this.visibleColModel = refRecord(docModel.columns, this.visibleCol);
this.disableModifyBase = ko.pureComputed(() => Boolean(this.summarySourceCol()));
@@ -184,6 +197,21 @@ export function createColumnRec(this: ColumnRec, docModel: DocModel): void {
}
return JSON.stringify(options);
});
this.addReverseColumn = () => {
return docModel.docData.sendAction(['AddReverseColumn', this.table.peek().tableId.peek(), this.colId.peek()]);
};
this.removeReverseColumn = async () => {
if (!this.hasReverse.peek()) {
throw new Error("Column does not have a reverse column");
}
// Remove the other column. Data engine will take care of removing the relation.
const column = this.reverseColModel.peek();
const tableId = column.table.peek().tableId.peek();
const colId = column.colId.peek();
return await docModel.docData.sendAction(['RemoveColumn', tableId, colId]);
};
}
export function formatterForRec(

View File

@@ -129,11 +129,10 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
this.colId = this.autoDispose(ko.pureComputed(() => this.column().colId()));
this.label = this.autoDispose(ko.pureComputed(() => this.column().label()));
this.origLabel = this.autoDispose(ko.pureComputed(() => this.origCol().label()));
this.description = modelUtil.savingComputed({
this.description = this.autoDispose(modelUtil.savingComputed({
read: () => this.column().description(),
write: (setter, val) => setter(this.column().description, val)
});
}));
// displayLabel displays label by default but switches to the more helpful colId whenever a
// formula field in the view is being edited.
this.displayLabel = modelUtil.savingComputed({
@@ -143,17 +142,17 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
// The field knows when we are editing a formula, so that all rows can reflect that.
const _editingFormula = ko.observable(false);
this.editingFormula = ko.pureComputed({
this.editingFormula = this.autoDispose(ko.pureComputed({
read: () => _editingFormula(),
write: val => {
// Whenever any view field changes its editingFormula status, let the docModel know.
docModel.editingFormula(val);
_editingFormula(val);
}
});
}));
// CSS class to add to formula cells, incl. to show that we are editing this field's formula.
this.formulaCssClass = ko.pureComputed<string|null>(() => {
this.formulaCssClass = this.autoDispose(ko.pureComputed<string|null>(() => {
const col = this.column();
// If the current column is transforming, assign the CSS class "transform_field"
@@ -175,7 +174,7 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
else {
return null;
}
});
}));
// The fields's display column
this._displayColModel = refRecord(docModel.columns, this.displayCol);
@@ -203,10 +202,10 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
// Display col ref to use for the field, defaulting to the plain column itself.
this.displayColRef = this.autoDispose(ko.pureComputed(() => this._fieldOrColumn().displayCol() || this.colRef()));
this.visibleColRef = modelUtil.addSaveInterface(ko.pureComputed({
this.visibleColRef = modelUtil.addSaveInterface(this.autoDispose(ko.pureComputed({
read: () => this._fieldOrColumn().visibleCol(),
write: (colRef) => this._fieldOrColumn().visibleCol(colRef),
}),
})),
colRef => docModel.docData.bundleActions(null, async () => {
const col = docModel.columns.getRowModel(colRef);
await Promise.all([
@@ -222,9 +221,13 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
// Helper for Reference/ReferenceList columns, which returns a formatter according to the visibleCol
// associated with this field. If no visible column available, return formatting for the field itself.
this.visibleColFormatter = ko.pureComputed(() => formatterForRec(this, this.column(), docModel, 'vcol'));
this.visibleColFormatter = this.autoDispose(
ko.pureComputed(() => formatterForRec(this, this.column(), docModel, 'vcol'))
);
this.formatter = ko.pureComputed(() => formatterForRec(this, this.column(), docModel, 'full'));
this.formatter = this.autoDispose(
ko.pureComputed(() => formatterForRec(this, this.column(), docModel, 'full'))
);
this.createValueParser = function() {
const fieldRef = this.useColOptions.peek() ? undefined : this.id.peek();
@@ -264,8 +267,8 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
this.headerFontStrikethrough = this.widgetOptionsJson.prop('headerFontStrikethrough');
this.question = this.widgetOptionsJson.prop('question');
this.documentSettings = ko.pureComputed(() => docModel.docInfoRow.documentSettingsJson());
this.style = ko.pureComputed({
this.documentSettings = this.autoDispose(ko.pureComputed(() => docModel.docInfoRow.documentSettingsJson()));
this.style = this.autoDispose(ko.pureComputed({
read: () => ({
textColor: this.textColor(),
fillColor: this.fillColor(),
@@ -277,8 +280,8 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
write: (style: Style) => {
this.widgetOptionsJson.update(style);
},
});
this.headerStyle = ko.pureComputed({
}));
this.headerStyle = this.autoDispose(ko.pureComputed({
read: () => ({
headerTextColor: this.headerTextColor(),
headerFillColor: this.headerFillColor(),
@@ -290,19 +293,21 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
write: (headerStyle: HeaderStyle) => {
this.widgetOptionsJson.update(headerStyle);
},
});
}));
this.tableId = ko.pureComputed(() => this.column().table().tableId());
this.tableId = this.autoDispose(ko.pureComputed(() => this.column().table().tableId()));
this.rulesList = modelUtil.savingComputed({
read: () => this._fieldOrColumn().rules(),
write: (setter, val) => setter(this._fieldOrColumn().rules, val)
});
this.rulesCols = refListRecords(docModel.columns, ko.pureComputed(() => this._fieldOrColumn().rules()));
this.rulesColsIds = ko.pureComputed(() => this.rulesCols().map(c => c.colId()));
this.rulesCols = this.autoDispose(
refListRecords(docModel.columns, ko.pureComputed(() => this._fieldOrColumn().rules()))
);
this.rulesColsIds = this.autoDispose(ko.pureComputed(() => this.rulesCols().map(c => c.colId())));
this.rulesStyles = modelUtil.fieldWithDefault(
this.widgetOptionsJson.prop("rulesOptions") as modelUtil.KoSaveableObservable<Style[]>,
[]);
this.hasRules = ko.pureComputed(() => this.rulesCols().length > 0);
this.hasRules = this.autoDispose(ko.pureComputed(() => this.rulesCols().length > 0));
// Helper method to add an empty rule (either initial or additional one).
// Style options are added to widget options directly and can be briefly out of sync,