feat(grid): add set NULL context menu action

This commit is contained in:
LRcoding
2026-07-15 15:25:25 +08:00
committed by GitHub
parent 10827f105a
commit 669f3a4914
4 changed files with 153 additions and 1 deletions
+15 -1
View File
@@ -7046,6 +7046,11 @@ function selectionHasEditableCells(): boolean {
return false;
}
function setSelectionNull() {
if (!props.editable || !selectionHasEditableCells()) return;
fillSelectionWithValue(null);
}
function openBulkEditDialog() {
if (!props.editable || !selectionHasEditableCells()) return;
bulkEditValue.value = "";
@@ -8989,10 +8994,19 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
}
if (props.editable && hasCellSelection.value) {
const hasEditableSelection = selectionHasEditableCells();
if (!contextHeaderColumn.value) {
items.push({
label: t("grid.setNull"),
action: setSelectionNull,
disabled: !hasEditableSelection,
icon: X,
});
}
items.push({
label: t("grid.bulkEditSelection"),
action: openBulkEditDialog,
disabled: !selectionHasEditableCells(),
disabled: !hasEditableSelection,
icon: Pencil,
});
}
@@ -299,6 +299,8 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
}
function touchPendingChanges() {
// Save errors describe the previous pending snapshot; edits, undo/redo, and rollback make them stale.
saveError.value = "";
pendingChangesVersion.value++;
}
+33
View File
@@ -0,0 +1,33 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { test } from "vitest";
import { compileScript, parse } from "vue/compiler-sfc";
const dataGridPath = "apps/desktop/src/components/grid/DataGrid.vue";
const dataGridSource = readFileSync(dataGridPath, "utf8");
test("DataGrid context menu script compiles", () => {
const { descriptor, errors } = parse(dataGridSource, { filename: dataGridPath });
assert.deepEqual(errors, []);
assert.ok(descriptor.scriptSetup);
compileScript(descriptor, { id: "data-grid-context-menu-test" });
});
test("set NULL applies a real null value only to editable selections", () => {
const handler = dataGridSource.match(/function setSelectionNull\(\) \{[^]*?\n\}/)?.[0] ?? "";
assert.match(handler, /if \(!props\.editable \|\| !selectionHasEditableCells\(\)\) return;/);
assert.match(handler, /fillSelectionWithValue\(null\);/);
assert.doesNotMatch(handler, /fillSelectionWithValue\(["'](?:NULL)?["']\)/);
});
test("editable cell selections expose set NULL before bulk edit", () => {
const menuBlock = dataGridSource.match(/if \(props\.editable && hasCellSelection\.value\) \{[^]*?\n \}/)?.[0] ?? "";
assert.match(menuBlock, /const hasEditableSelection = selectionHasEditableCells\(\);/);
assert.match(menuBlock, /if \(!contextHeaderColumn\.value\) \{/);
assert.match(menuBlock, /label: t\("grid\.setNull"\),\s+action: setSelectionNull,\s+disabled: !hasEditableSelection,\s+icon: X,/);
assert.match(menuBlock, /label: t\("grid\.bulkEditSelection"\),\s+action: openBulkEditDialog,\s+disabled: !hasEditableSelection,/);
assert.ok(menuBlock.indexOf('t("grid.setNull")') < menuBlock.indexOf('t("grid.bulkEditSelection")'));
});
+103
View File
@@ -618,6 +618,109 @@ test("undo and redo restore pending cell edits before save", () => {
assert.deepEqual(editor.rowDataWithChanges(result.value.rows[0], 0), [1, "Ada Lovelace"]);
});
test("setting a cell to NULL records pending SQL and supports undo and redo", async () => {
setActivePinia(createPinia());
installBrowserTestGlobals();
const result = computed(() => ({
columns: ["id", "name"],
rows: [[1, "Ada"] as CellValue[]],
}));
const editor = createPeopleGridEditor(result);
editor.applyCellValue(0, 1, null);
assert.equal(editor.dirtyRows.value.get(0)?.get(1), null);
assert.equal(editor.hasPendingChanges.value, true);
assert.deepEqual(editor.rowDataWithChanges(result.value.rows[0], 0), [1, null]);
const statements = await editor.previewChanges();
assert.deepEqual(statements, ['UPDATE "people" SET "name" = NULL WHERE "id" = 1;']);
assert.doesNotMatch(statements.join("\n"), /["']NULL["']/);
editor.undoPendingChange();
assert.equal(editor.dirtyRows.value.size, 0);
assert.deepEqual(editor.rowDataWithChanges(result.value.rows[0], 0), [1, "Ada"]);
editor.redoPendingChange();
assert.equal(editor.dirtyRows.value.get(0)?.get(1), null);
assert.deepEqual(editor.rowDataWithChanges(result.value.rows[0], 0), [1, null]);
});
test("setting an existing NULL cell to NULL is a no-op", async () => {
setActivePinia(createPinia());
installBrowserTestGlobals();
const result = computed(() => ({
columns: ["id", "name"],
rows: [[1, null] as CellValue[]],
}));
const editor = createPeopleGridEditor(result);
editor.applyCellValue(0, 1, null);
assert.equal(editor.dirtyRows.value.size, 0);
assert.equal(editor.hasPendingChanges.value, false);
assert.equal(editor.canUndoPendingChange.value, false);
assert.deepEqual(await editor.previewChanges(), []);
});
test("a failed NULL save keeps the pending cell edit", async () => {
setActivePinia(createPinia());
installBrowserTestGlobals();
const editor = createQuickEntryEditor({
quickEntryEnabled: false,
save: async () => {
throw new Error("NULL write rejected");
},
});
editor.applyCellValue(0, 1, null);
await editor.saveChanges();
assert.equal(editor.saveError.value, "NULL write rejected");
assert.equal(editor.dirtyRows.value.get(0)?.get(1), null);
assert.equal(editor.hasPendingChanges.value, true);
});
test("a NOT NULL validation failure keeps the pending NULL cell edit recoverable", async () => {
setActivePinia(createPinia());
installBrowserTestGlobals();
globalThis.fetch = (async (input) => {
if (String(input) !== "/api/query/prepare-data-grid-save") return new Response("unexpected request", { status: 500 });
return new Response(
JSON.stringify({
validationError: 'Column "name" does not allow NULL.',
statements: [],
rollbackStatements: [],
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
}) as typeof fetch;
const editor = createPeopleGridEditor();
editor.applyCellValue(0, 1, null);
await editor.saveChanges();
assert.equal(editor.saveError.value, 'Column "name" does not allow NULL.');
assert.equal(editor.dirtyRows.value.get(0)?.get(1), null);
assert.equal(editor.hasPendingChanges.value, true);
assert.equal(editor.canUndoPendingChange.value, true);
editor.undoPendingChange();
assert.equal(editor.dirtyRows.value.size, 0);
assert.equal(editor.saveError.value, "");
editor.redoPendingChange();
await editor.saveChanges();
assert.equal(editor.saveError.value, 'Column "name" does not allow NULL.');
editor.discardChanges();
assert.equal(editor.dirtyRows.value.size, 0);
assert.equal(editor.hasPendingChanges.value, false);
assert.equal(editor.saveError.value, "");
});
test("restoring a pending cell edit records undo and redo history", () => {
setActivePinia(createPinia());
installBrowserTestGlobals();