feat(grid): support deleting custom column formatters

This commit is contained in:
zipg authored and GitHub committed 2026-08-21 19:44:52 +08:00
1 parent 47e02b8da7
commit 8671a2fb2a
11 files changed
+508 -77

No files matched your search

+172
View File
@@ -29,6 +29,16 @@ beforeEach(() => {
saveEditorSettingsMock.mockClear();
});
function createDeferred() {
let resolve!: () => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<void>((resolvePromise, rejectPromise) => {
resolve = resolvePromise;
reject = rejectPromise;
});
return { promise, resolve, reject };
}
async function withMockLocalStorage(initial: Record<string, string>, run: () => void | Promise<void>) {
const previousDescriptor = Object.getOwnPropertyDescriptor(globalThis, "localStorage");
const values = new Map(Object.entries(initial));
@@ -627,6 +637,168 @@ test("keeps only valid saved column formatter configs", () => {
});
});
test("deleting a saved custom formatter clears every reference to it", async () => {
await withMockLocalStorage({}, async () => {
setActivePinia(createPinia());
const store = useSettingsStore();
await store.initEditorSettings();
store.updateEditorSettings({
customColumnFormatters: {
fmt_remove: { id: "fmt_remove", name: "Remove me", template: "remove:${value}" },
fmt_keep: { id: "fmt_keep", name: "Keep me", template: "keep:${value}" },
},
columnFormatters: {
first: { kind: "custom-ref", formatterId: "fmt_remove" },
second: { kind: "custom-ref", formatterId: "fmt_remove" },
keep: { kind: "custom-ref", formatterId: "fmt_keep" },
mask: { kind: "mask", prefix: 1, suffix: 1 },
},
});
await store.deleteCustomColumnFormatter("fmt_remove");
assert.deepEqual(store.editorSettings.customColumnFormatters, {
fmt_keep: { id: "fmt_keep", name: "Keep me", template: "keep:${value}" },
});
assert.deepEqual(store.editorSettings.columnFormatters, {
keep: { kind: "custom-ref", formatterId: "fmt_keep" },
mask: { kind: "mask", prefix: 1, suffix: 1 },
});
});
});
test("serializes concurrent custom formatter deletes", async () => {
await withMockLocalStorage({}, async () => {
setActivePinia(createPinia());
const store = useSettingsStore();
await store.initEditorSettings();
store.updateEditorSettings({
customColumnFormatters: {
fmt_a: { id: "fmt_a", name: "A", template: "a:${value}" },
fmt_b: { id: "fmt_b", name: "B", template: "b:${value}" },
fmt_keep: { id: "fmt_keep", name: "Keep", template: "keep:${value}" },
},
columnFormatters: {
first: { kind: "custom-ref", formatterId: "fmt_a" },
second: { kind: "custom-ref", formatterId: "fmt_b" },
keep: { kind: "custom-ref", formatterId: "fmt_keep" },
},
});
await store.persistEditorSettings();
await Promise.all([store.deleteCustomColumnFormatter("fmt_a"), store.deleteCustomColumnFormatter("fmt_b")]);
assert.deepEqual(store.editorSettings.customColumnFormatters, {
fmt_keep: { id: "fmt_keep", name: "Keep", template: "keep:${value}" },
});
assert.deepEqual(store.editorSettings.columnFormatters, {
keep: { kind: "custom-ref", formatterId: "fmt_keep" },
});
const saved = saveEditorSettingsMock.mock.calls.at(-1)?.[0] as { customColumnFormatters?: Record<string, unknown> } | undefined;
assert.deepEqual(saved?.customColumnFormatters, {
fmt_keep: { id: "fmt_keep", name: "Keep", template: "keep:${value}" },
});
});
});
test("queues formatter upserts behind a deferred delete save", async () => {
await withMockLocalStorage({}, async () => {
setActivePinia(createPinia());
const store = useSettingsStore();
await store.initEditorSettings();
store.updateEditorSettings({
customColumnFormatters: {
fmt_a: { id: "fmt_a", name: "A", template: "a:${value}" },
fmt_b: { id: "fmt_b", name: "B", template: "b:${value}" },
},
columnFormatters: {
first: { kind: "custom-ref", formatterId: "fmt_a" },
},
});
await store.persistEditorSettings();
const deleteSave = createDeferred();
saveEditorSettingsMock.mockImplementationOnce(() => deleteSave.promise);
const deletePromise = store.deleteCustomColumnFormatter("fmt_a");
await vi.waitFor(() => assert.equal(store.editorSettings.customColumnFormatters.fmt_a, undefined));
const upsertPromise = store.upsertCustomColumnFormatter({ id: "fmt_b", name: "B updated", template: "updated:${value}" });
await Promise.resolve();
assert.equal(store.editorSettings.customColumnFormatters.fmt_b.name, "B");
deleteSave.resolve();
await deletePromise;
await upsertPromise;
assert.deepEqual(store.editorSettings.customColumnFormatters, {
fmt_b: { id: "fmt_b", name: "B updated", template: "updated:${value}" },
});
assert.deepEqual(store.editorSettings.columnFormatters, {});
});
});
test("rolls back a failed delete before running a queued formatter upsert", async () => {
await withMockLocalStorage({}, async () => {
setActivePinia(createPinia());
const store = useSettingsStore();
await store.initEditorSettings();
store.updateEditorSettings({
customColumnFormatters: {
fmt_a: { id: "fmt_a", name: "A", template: "a:${value}" },
},
columnFormatters: {
first: { kind: "custom-ref", formatterId: "fmt_a" },
},
});
await store.persistEditorSettings();
const deleteSave = createDeferred();
saveEditorSettingsMock.mockImplementationOnce(() => deleteSave.promise);
const deletePromise = store.deleteCustomColumnFormatter("fmt_a");
const deleteRejected = assert.rejects(deletePromise, /disk full/);
await vi.waitFor(() => assert.equal(store.editorSettings.customColumnFormatters.fmt_a, undefined));
const upsertPromise = store.upsertCustomColumnFormatter({ id: "fmt_b", name: "B", template: "b:${value}" });
await Promise.resolve();
assert.equal(store.editorSettings.customColumnFormatters.fmt_b, undefined);
deleteSave.reject(new Error("disk full"));
await deleteRejected;
await upsertPromise;
assert.deepEqual(store.editorSettings.customColumnFormatters, {
fmt_a: { id: "fmt_a", name: "A", template: "a:${value}" },
fmt_b: { id: "fmt_b", name: "B", template: "b:${value}" },
});
assert.deepEqual(store.editorSettings.columnFormatters, {
first: { kind: "custom-ref", formatterId: "fmt_a" },
});
});
});
test("rejects stale formatter drafts but allows deliberate same-id recreation", async () => {
await withMockLocalStorage({}, async () => {
setActivePinia(createPinia());
const store = useSettingsStore();
await store.initEditorSettings();
store.updateEditorSettings({
customColumnFormatters: {
fmt_rebuild: { id: "fmt_rebuild", name: "Original", template: "original:${value}" },
},
});
await store.persistEditorSettings();
const capturedDeleteVersion = store.customColumnFormatterDeleteVersion("fmt_rebuild");
const staleFormatter = store.editorSettings.customColumnFormatters.fmt_rebuild;
await store.deleteCustomColumnFormatter("fmt_rebuild");
assert.equal(await store.upsertCustomColumnFormatter(staleFormatter, capturedDeleteVersion), undefined);
assert.equal(store.editorSettings.customColumnFormatters.fmt_rebuild, undefined);
const rebuilt = { id: "fmt_rebuild", name: "Rebuilt", template: "rebuilt:${value}" };
assert.deepEqual(await store.upsertCustomColumnFormatter(rebuilt), rebuilt);
assert.deepEqual(store.editorSettings.customColumnFormatters.fmt_rebuild, rebuilt);
assert.notEqual(store.customColumnFormatterDeleteVersion("fmt_rebuild"), capturedDeleteVersion);
});
});
test("AI provider presets include common hosted and local providers", () => {
assert.equal(AI_PROVIDER_PRESETS.gemini.endpoint, "https://generativelanguage.googleapis.com");
assert.equal(AI_PROVIDER_PRESETS.gemini.model, "gemini-1.5-pro");