mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-22 02:06:26 +00:00
feat(grid): add set NULL context menu action
This commit is contained in:
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user