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

+91
View File
@@ -805,6 +805,38 @@ describe("settingsStore editor settings persistence", () => {
setActivePinia(createPinia());
});
it("initializes legacy settings before queueing formatter mutations", async () => {
const loadEditorSettings = vi.fn().mockResolvedValue({
updateDownloadSource: "atomgit",
customColumnFormatters: {},
executeModeDefaultVersion: EXECUTE_MODE_CURRENT_DEFAULT_VERSION,
});
const saveEditorSettings = vi.fn().mockResolvedValue(undefined);
vi.doMock("@/lib/backend/api", () => ({ loadEditorSettings, saveEditorSettings }));
const { useSettingsStore } = await import("@/stores/settingsStore");
const store = useSettingsStore();
const formatter = { id: "fmt_pre_init", name: "Pre-init", template: "legacy:${value}" };
await store.upsertCustomColumnFormatter(formatter);
expect(loadEditorSettings).toHaveBeenCalledOnce();
expect(saveEditorSettings).toHaveBeenCalledTimes(2);
expect(saveEditorSettings.mock.calls[0][0]).toEqual(
expect.objectContaining({
updateDownloadSource: "cnb",
customColumnFormatters: {},
}),
);
expect(saveEditorSettings.mock.calls[1][0]).toEqual(
expect.objectContaining({
updateDownloadSource: "cnb",
customColumnFormatters: { fmt_pre_init: formatter },
}),
);
expect(store.editorSettings.customColumnFormatters.fmt_pre_init).toEqual(formatter);
});
it("rolls back a failed atomic update and allows retry", async () => {
const loadEditorSettings = vi.fn().mockResolvedValue({
ignoredUpdateVersion: "",
@@ -915,6 +947,65 @@ describe("settingsStore editor settings persistence", () => {
expect(store.editorSettings).toMatchObject({ ignoredUpdateVersion: "", theme: "xcode-dark" });
expect(saveEditorSettings.mock.calls[1][0]).toEqual(expect.objectContaining({ ignoredUpdateVersion: "", theme: "xcode-dark" }));
});
it("serializes queued saves before a failed formatter delete", async () => {
let persistedSettings: Record<string, unknown> = {
customColumnFormatters: {
fmt_a: { id: "fmt_a", name: "A", template: "a:${value}" },
},
columnFormatters: {
first: { kind: "custom-ref", formatterId: "fmt_a" },
},
executeModeDefaultVersion: EXECUTE_MODE_CURRENT_DEFAULT_VERSION,
};
const loadEditorSettings = vi.fn(async () => JSON.parse(JSON.stringify(persistedSettings)));
const saveEditorSettings = vi.fn(async (settings: Record<string, unknown>) => {
persistedSettings = JSON.parse(JSON.stringify(settings));
});
vi.doMock("@/lib/backend/api", () => ({ loadEditorSettings, saveEditorSettings }));
const { useSettingsStore } = await import("@/stores/settingsStore");
const store = useSettingsStore();
await store.initEditorSettings();
saveEditorSettings.mockClear();
let resolveFirstSave!: () => void;
let saveCall = 0;
saveEditorSettings.mockImplementation(async (settings: Record<string, unknown>) => {
saveCall += 1;
if (saveCall === 1) {
await new Promise<void>((resolve) => {
resolveFirstSave = resolve;
});
}
if (saveCall === 3) throw new Error("delete save failed");
persistedSettings = JSON.parse(JSON.stringify(settings));
});
store.updateEditorSettings({ theme: "xcode-dark" });
await vi.waitFor(() => expect(saveEditorSettings).toHaveBeenCalledOnce());
store.updateEditorSettings({ resultRunDisplayMode: "list" });
const deletePromise = store.deleteCustomColumnFormatter("fmt_a");
const deleteRejected = expect(deletePromise).rejects.toThrow("delete save failed");
await Promise.resolve();
await Promise.resolve();
expect(store.editorSettings.customColumnFormatters.fmt_a).toBeDefined();
resolveFirstSave();
await deleteRejected;
expect(saveEditorSettings).toHaveBeenCalledTimes(3);
expect(store.editorSettings.customColumnFormatters.fmt_a).toBeDefined();
expect(store.editorSettings.columnFormatters.first).toEqual({ kind: "custom-ref", formatterId: "fmt_a" });
setActivePinia(createPinia());
const restartedStore = useSettingsStore();
await restartedStore.initEditorSettings();
expect(restartedStore.editorSettings.customColumnFormatters.fmt_a).toEqual({ id: "fmt_a", name: "A", template: "a:${value}" });
expect(restartedStore.editorSettings.columnFormatters.first).toEqual({ kind: "custom-ref", formatterId: "fmt_a" });
});
});
describe("settingsStore sidebar connection sort persistence", () => {
+139 -54
View File
@@ -1304,23 +1304,34 @@ export const useSettingsStore = defineStore("settings", () => {
const isEditorSettingsLoaded = ref(false);
let initEditorSettingsPromise: Promise<void> | null = null;
let pendingEditorSettingsPatches: Partial<EditorSettings>[] = [];
let editorSettingsSaveQueue: Promise<void> | null = null;
let editorSettingsAtomicUpdateQueue: Promise<void> = Promise.resolve();
let editorSettingsOperationQueue: Promise<void> | null = null;
let editorSettingsPatchRevision = 0;
const editorSettingsFieldRevisions = new Map<keyof EditorSettings, number>();
const customColumnFormatterDeleteVersions = new Map<string, number>();
let pendingAiChatSelection: AiChatSelectionState | null = null;
let aiChatSelectionSaveRunning = false;
const editorSettings = ref<EditorSettings>(normalizeEditorSettings({}));
function enqueueEditorSettingsSave(): Promise<void> {
const saveCurrentSettings = () => api.saveEditorSettings(editorSettingsSnapshot(editorSettings.value));
const save = editorSettingsSaveQueue ? editorSettingsSaveQueue.catch(() => {}).then(saveCurrentSettings) : saveCurrentSettings();
const trackedSave = save.finally(() => {
if (editorSettingsSaveQueue === trackedSave) editorSettingsSaveQueue = null;
function enqueueEditorSettingsOperation<T>(operation: () => Promise<T>): Promise<T> {
const queuedOperation = editorSettingsOperationQueue ? editorSettingsOperationQueue.then(operation) : operation();
const trackedOperation = queuedOperation.then(
() => undefined,
() => undefined,
);
editorSettingsOperationQueue = trackedOperation;
void trackedOperation.finally(() => {
if (editorSettingsOperationQueue === trackedOperation) editorSettingsOperationQueue = null;
});
editorSettingsSaveQueue = trackedSave;
return trackedSave;
return queuedOperation;
}
function persistCurrentEditorSettings(): Promise<void> {
return api.saveEditorSettings(editorSettingsSnapshot(editorSettings.value));
}
function enqueueEditorSettingsSave(): Promise<void> {
return enqueueEditorSettingsOperation(persistCurrentEditorSettings);
}
function saveEditorSettings() {
@@ -1823,32 +1834,33 @@ export const useSettingsStore = defineStore("settings", () => {
await enqueueEditorSettingsSave();
}
async function enqueueEditorSettingsAtomicMutation<T>(mutation: () => Promise<T>): Promise<T> {
await initEditorSettings();
return enqueueEditorSettingsOperation(mutation);
}
function updateEditorSettingsAndPersist(partial: Partial<EditorSettings>): Promise<void> {
const update = editorSettingsAtomicUpdateQueue
.catch(() => {})
.then(async () => {
await initEditorSettings();
const previous = editorSettingsSnapshot(editorSettings.value);
applyEditorSettingsPatch(partial);
const revision = markEditorSettingsPatch(partial);
try {
await enqueueEditorSettingsSave();
} catch (error) {
const restored = editorSettingsSnapshot(editorSettings.value) as unknown as Record<string, unknown>;
const previousSettings = previous as unknown as Record<string, unknown>;
let changed = false;
for (const key of Object.keys(partial) as (keyof EditorSettings)[]) {
if (partial[key] === undefined || editorSettingsFieldRevisions.get(key) !== revision) continue;
restored[key] = previousSettings[key];
editorSettingsFieldRevisions.set(key, ++editorSettingsPatchRevision);
changed = true;
}
if (changed) editorSettings.value = normalizeEditorSettings(restored);
throw error;
return enqueueEditorSettingsAtomicMutation(async () => {
await initEditorSettings();
const previous = editorSettingsSnapshot(editorSettings.value);
applyEditorSettingsPatch(partial);
const revision = markEditorSettingsPatch(partial);
try {
await persistCurrentEditorSettings();
} catch (error) {
const restored = editorSettingsSnapshot(editorSettings.value) as unknown as Record<string, unknown>;
const previousSettings = previous as unknown as Record<string, unknown>;
let changed = false;
for (const key of Object.keys(partial) as (keyof EditorSettings)[]) {
if (partial[key] === undefined || editorSettingsFieldRevisions.get(key) !== revision) continue;
restored[key] = previousSettings[key];
editorSettingsFieldRevisions.set(key, ++editorSettingsPatchRevision);
changed = true;
}
});
editorSettingsAtomicUpdateQueue = update.catch(() => {});
return update;
if (changed) editorSettings.value = normalizeEditorSettings(restored);
throw error;
}
});
}
function updateColumnFormatter(key: string, formatter: ColumnFormatterConfig | undefined) {
@@ -1862,29 +1874,101 @@ export const useSettingsStore = defineStore("settings", () => {
updateEditorSettings({ columnFormatters });
}
function upsertCustomColumnFormatter(formatter: CustomColumnFormatterConfig): CustomColumnFormatterConfig | undefined {
const normalized = normalizeCustomColumnFormatter(formatter);
if (!normalized) return undefined;
updateEditorSettings({
customColumnFormatters: {
...editorSettings.value.customColumnFormatters,
[normalized.id]: normalized,
},
});
return normalized;
function customColumnFormatterDeleteVersion(id: string): number {
return customColumnFormatterDeleteVersions.get(id) ?? 0;
}
function deleteCustomColumnFormatter(id: string) {
const customColumnFormatters = {
...editorSettings.value.customColumnFormatters,
};
delete customColumnFormatters[id];
const columnFormatters = Object.fromEntries(
Object.entries(editorSettings.value.columnFormatters).filter(([, formatter]) => {
return formatter.kind !== "custom-ref" || formatter.formatterId !== id;
}),
);
updateEditorSettings({ customColumnFormatters, columnFormatters });
async function upsertCustomColumnFormatter(formatter: CustomColumnFormatterConfig, expectedDeleteVersion?: number): Promise<CustomColumnFormatterConfig | undefined> {
const normalized = normalizeCustomColumnFormatter(formatter);
if (!normalized) return undefined;
return enqueueEditorSettingsAtomicMutation(async () => {
await initEditorSettings();
if (expectedDeleteVersion !== undefined && customColumnFormatterDeleteVersion(normalized.id) !== expectedDeleteVersion) return undefined;
const previous = editorSettings.value.customColumnFormatters[normalized.id];
const partial = {
customColumnFormatters: {
...editorSettings.value.customColumnFormatters,
[normalized.id]: normalized,
},
} satisfies Partial<EditorSettings>;
applyEditorSettingsPatch(partial);
markEditorSettingsPatch(partial);
try {
await persistCurrentEditorSettings();
} catch (error) {
const customColumnFormatters = {
...editorSettings.value.customColumnFormatters,
};
const current = customColumnFormatters[normalized.id];
if (current?.name === normalized.name && current.template === normalized.template) {
if (previous) {
customColumnFormatters[normalized.id] = previous;
} else {
delete customColumnFormatters[normalized.id];
}
const rollback = { customColumnFormatters } satisfies Partial<EditorSettings>;
applyEditorSettingsPatch(rollback);
markEditorSettingsPatch(rollback);
}
throw error;
}
return normalized;
});
}
async function deleteCustomColumnFormatter(id: string): Promise<void> {
return enqueueEditorSettingsAtomicMutation(async () => {
await initEditorSettings();
const previousDeleteVersion = customColumnFormatterDeleteVersion(id);
const deleteVersion = previousDeleteVersion + 1;
customColumnFormatterDeleteVersions.set(id, deleteVersion);
const previousFormatter = editorSettings.value.customColumnFormatters[id];
const customColumnFormatters = {
...editorSettings.value.customColumnFormatters,
};
delete customColumnFormatters[id];
const removedColumnFormatters = Object.fromEntries(
Object.entries(editorSettings.value.columnFormatters).filter(([, formatter]) => {
return formatter.kind === "custom-ref" && formatter.formatterId === id;
}),
);
const columnFormatters = Object.fromEntries(
Object.entries(editorSettings.value.columnFormatters).filter(([, formatter]) => {
return formatter.kind !== "custom-ref" || formatter.formatterId !== id;
}),
);
const partial = { customColumnFormatters, columnFormatters } satisfies Partial<EditorSettings>;
applyEditorSettingsPatch(partial);
markEditorSettingsPatch(partial);
try {
await persistCurrentEditorSettings();
} catch (error) {
if (customColumnFormatterDeleteVersion(id) === deleteVersion) {
if (previousDeleteVersion === 0) {
customColumnFormatterDeleteVersions.delete(id);
} else {
customColumnFormatterDeleteVersions.set(id, previousDeleteVersion);
}
}
const restoredCustomColumnFormatters = {
...editorSettings.value.customColumnFormatters,
};
if (previousFormatter && !restoredCustomColumnFormatters[id]) restoredCustomColumnFormatters[id] = previousFormatter;
const restoredColumnFormatters = {
...editorSettings.value.columnFormatters,
};
for (const [key, formatter] of Object.entries(removedColumnFormatters)) {
if (!restoredColumnFormatters[key]) restoredColumnFormatters[key] = formatter;
}
const rollback = {
customColumnFormatters: restoredCustomColumnFormatters,
columnFormatters: restoredColumnFormatters,
} satisfies Partial<EditorSettings>;
applyEditorSettingsPatch(rollback);
markEditorSettingsPatch(rollback);
throw error;
}
});
}
return {
@@ -1921,6 +2005,7 @@ export const useSettingsStore = defineStore("settings", () => {
initMcpGlobalPolicy,
updateMcpGlobalPolicy,
updateColumnFormatter,
customColumnFormatterDeleteVersion,
upsertCustomColumnFormatter,
deleteCustomColumnFormatter,
};