(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

@@ -32,6 +32,9 @@ const {COMMENTS} = require('app/client/models/features');
const {DismissedPopup} = require('app/common/Prefs');
const {markAsSeen} = require('app/client/models/UserPrefs');
const {buildConfirmDelete, reportUndo} = require('app/client/components/modals');
const {buildReassignModal} = require('app/client/ui/buildReassignModal');
const {MutedError} = require('app/client/models/errors');
/**
* BaseView forms the basis for ViewSection classes.
@@ -648,7 +651,17 @@ BaseView.prototype.sendPasteActions = function(cutCallback, actions) {
// If the cut occurs on an edit restricted cell, there may be no cut action.
if (cutAction) { actions.unshift(cutAction); }
}
return this.gristDoc.docData.sendActions(actions);
return this.gristDoc.docData.sendActions(actions).catch(ex => {
if (ex.code === 'UNIQUE_REFERENCE_VIOLATION') {
buildReassignModal({
docModel: this.gristDoc.docModel,
actions,
}).catch(reportError);
throw new MutedError();
} else {
throw ex;
}
});
};
BaseView.prototype.buildDom = function() {

View File

@@ -343,6 +343,9 @@ export class Comm extends dispose.Disposable implements GristServerAPI, DocListA
if (message.details) {
err.details = message.details;
}
if (message.error?.startsWith('[Sandbox] UniqueReferenceError')) {
err.code = 'UNIQUE_REFERENCE_VIOLATION';
}
err.shouldFork = message.shouldFork;
log.warn(`Comm response #${reqId} ${r.methodName} ERROR:${code} ${message.error}`
+ (message.shouldFork ? ` (should fork)` : ''));

View File

@@ -55,7 +55,8 @@ const {NEW_FILTER_JSON} = require('app/client/models/ColumnFilter');
const {CombinedStyle} = require("app/client/models/Styles");
const {buildRenameColumn} = require('app/client/ui/ColumnTitle');
const {makeT} = require('app/client/lib/localization');
const { isList } = require('app/common/gristTypes');
const {isList} = require('app/common/gristTypes');
const t = makeT('GridView');
@@ -628,27 +629,25 @@ GridView.prototype.paste = async function(data, cutCallback) {
if (actions.length > 0) {
let cursorPos = this.cursor.getCursorPos();
return this.sendPasteActions(cutCallback, actions)
.then(results => {
// If rows were added, get their rowIds from the action results.
let addRowIds = (actions[0][0] === 'BulkAddRecord' ? results[0] : []);
console.assert(addRowIds.length <= updateRowIds.length,
`Unexpected number of added rows: ${addRowIds.length} of ${updateRowIds.length}`);
let newRowIds = updateRowIds.slice(0, updateRowIds.length - addRowIds.length)
.concat(addRowIds);
const results = await this.sendPasteActions(cutCallback, actions);
// If rows were added, get their rowIds from the action results.
let addRowIds = (actions[0][0] === 'BulkAddRecord' ? results[0] : []);
console.assert(addRowIds.length <= updateRowIds.length,
`Unexpected number of added rows: ${addRowIds.length} of ${updateRowIds.length}`);
let newRowIds = updateRowIds.slice(0, updateRowIds.length - addRowIds.length)
.concat(addRowIds);
// Restore the cursor to the right rowId, even if it jumped.
this.cursor.setCursorPos({rowId: cursorPos.rowId === 'new' ? addRowIds[0] : cursorPos.rowId});
// Restore the cursor to the right rowId, even if it jumped.
this.cursor.setCursorPos({rowId: cursorPos.rowId === 'new' ? addRowIds[0] : cursorPos.rowId});
// Restore the selection if it would select the correct rows.
let topRowIndex = this.viewData.getRowIndex(newRowIds[0]);
if (newRowIds.every((r, i) => r === this.viewData.getRowId(topRowIndex + i))) {
this.cellSelector.selectArea(topRowIndex, leftIndex,
topRowIndex + outputHeight - 1, leftIndex + outputWidth - 1);
}
// Restore the selection if it would select the correct rows.
let topRowIndex = this.viewData.getRowIndex(newRowIds[0]);
if (newRowIds.every((r, i) => r === this.viewData.getRowId(topRowIndex + i))) {
this.cellSelector.selectArea(topRowIndex, leftIndex,
topRowIndex + outputHeight - 1, leftIndex + outputWidth - 1);
}
commands.allCommands.clearCopySelection.run();
});
await commands.allCommands.clearCopySelection.run();
}
};

View File

@@ -207,6 +207,16 @@ export class GristDoc extends DisposableWithEvents {
public isTimingOn = Observable.create(this, false);
/**
* Checks if it is ok to show raw data popup for currently selected section.
* We can't show raw data if:
* - we already have full screen section (which looks the same)
* - we are already showing raw data
*
* Extracted to single computed as it is used here and in menus.
*/
public canShowRawData: Computed<boolean>;
private _actionLog: ActionLog;
private _undoStack: UndoStack;
private _lastOwnActionGroup: ActionGroupWithCursorPos | null = null;
@@ -498,7 +508,21 @@ export class GristDoc extends DisposableWithEvents {
reloadPlugins() {
void this.docComm.reloadPlugins().then(() => G.window.location.reload(false));
},
async showRawData(sectionId: number = 0) {
if (!this.canShowRawData.get()) {
return;
}
if (!sectionId) {
const viewSection = this.viewModel.activeSection();
if (viewSection?.isDisposed()) { return; }
if (viewSection.isRaw.peek()) {
return;
}
sectionId = viewSection.id.peek();
}
const anchorUrlState = { hash: { sectionId, popup: true } };
await urlState().pushUrl(anchorUrlState, { replace: true });
},
// Command to be manually triggered on cell selection. Moves the cursor to the selected cell.
// This is overridden by the formula editor to insert "$col" variables when clicking cells.
setCursor: this.onSetCursorPos.bind(this),
@@ -603,6 +627,14 @@ export class GristDoc extends DisposableWithEvents {
this._prevSectionId = null;
}
}));
this.canShowRawData = Computed.create(this, (use) => {
const isSinglePage = use(urlState().state).params?.style === 'singlePage';
if (isSinglePage || use(this.maximizedSectionId)) {
return false;
}
return true;
});
}
/**

View File

@@ -47,6 +47,25 @@ export function addColTypeSuffix(type: string, column: ColumnRec, docModel: DocM
}
}
/**
* Infers the suffix for a column type, based on the type of the column and the type to convert it to.
* Currently only used for Ref and RefList types, where the suffix is the tableId of the reference.
*/
export function inferColTypeSuffix(newPure: string, column: ColumnRec) {
// We can infer only for Ref and RefList types.
if (newPure !== "Ref" && newPure !== "RefList") {
return null;
}
// If the old type was also Ref/RefList, just return the tableId from the old type.
const existingTable = column.type.peek().split(':')[1];
const oldPure = gristTypes.extractTypeFromColType(column.type.peek());
if (existingTable && (oldPure === "Ref" || oldPure === "RefList")) {
return `${newPure}:${existingTable}`;
}
return null;
}
/**
* Looks through the data of the given column to find the first value of the form
* [R|r, <tableId>, <rowId>] (a Reference(List) value returned from a formula), and returns the tableId