feat(grid): support deleting custom column formatters
This commit is contained in:
11 files changed
+508
-77
No files matched your search
@@ -1076,8 +1076,13 @@ const formatterJsonPath = ref("$.user.name");
|
||||
const formatterMaskPrefix = ref(4);
|
||||
const formatterMaskSuffix = ref(4);
|
||||
const formatterCustomId = ref(CUSTOM_FORMATTER_NEW);
|
||||
const formatterCustomCapturedDeleteVersion = ref<number>();
|
||||
const formatterCustomName = ref("");
|
||||
const formatterCustomTemplate = ref("${value}");
|
||||
const formatterCustomDeleteOpen = ref(false);
|
||||
const formatterCustomDeleteLoading = ref(false);
|
||||
const formatterCustomDeleteId = ref("");
|
||||
const formatterCustomDeleteName = ref("");
|
||||
const formatterForeignKeyRefSchema = ref("");
|
||||
const formatterForeignKeyRefTable = ref("");
|
||||
const formatterForeignKeyRefColumn = ref("");
|
||||
@@ -1508,10 +1513,12 @@ function loadFormatterDraft(formatter: ColumnFormatterConfig | undefined) {
|
||||
} else if (draft.kind === "custom-ref") {
|
||||
const saved = settingsStore.editorSettings.customColumnFormatters[draft.formatterId];
|
||||
formatterCustomId.value = saved ? saved.id : CUSTOM_FORMATTER_NEW;
|
||||
formatterCustomCapturedDeleteVersion.value = saved ? settingsStore.customColumnFormatterDeleteVersion(saved.id) : undefined;
|
||||
formatterCustomName.value = saved?.name ?? "";
|
||||
formatterCustomTemplate.value = saved?.template ?? "${value}";
|
||||
} else if (draft.kind === "custom-template") {
|
||||
formatterCustomId.value = CUSTOM_FORMATTER_NEW;
|
||||
formatterCustomCapturedDeleteVersion.value = undefined;
|
||||
formatterCustomName.value = "";
|
||||
formatterCustomTemplate.value = draft.template;
|
||||
} else if (draft.kind === "foreign-key-display") {
|
||||
@@ -1585,22 +1592,33 @@ function handleColumnFormatterOpenChange(value: boolean, columnIndex: number) {
|
||||
}
|
||||
}
|
||||
|
||||
function saveColumnFormatter(columnIndex: number) {
|
||||
async function saveColumnFormatter(columnIndex: number) {
|
||||
const column = props.result.columns[columnIndex];
|
||||
const key = column ? formatterKeyForColumn(column) : null;
|
||||
if (!key) return;
|
||||
let formatter = currentFormatterDraft();
|
||||
if (formatterKind.value === "custom-template" && formatterCustomName.value.trim()) {
|
||||
const id = formatterCustomId.value === CUSTOM_FORMATTER_NEW ? createCustomFormatterId() : formatterCustomId.value;
|
||||
const saved = settingsStore.upsertCustomColumnFormatter({
|
||||
id,
|
||||
name: formatterCustomName.value,
|
||||
template: formatterCustomTemplate.value,
|
||||
});
|
||||
if (saved) formatter = { kind: "custom-ref", formatterId: saved.id };
|
||||
try {
|
||||
let formatter = currentFormatterDraft();
|
||||
if (formatterKind.value === "custom-template" && formatterCustomName.value.trim()) {
|
||||
const id = formatterCustomId.value === CUSTOM_FORMATTER_NEW ? createCustomFormatterId() : formatterCustomId.value;
|
||||
const saved = await settingsStore.upsertCustomColumnFormatter(
|
||||
{
|
||||
id,
|
||||
name: formatterCustomName.value,
|
||||
template: formatterCustomTemplate.value,
|
||||
},
|
||||
formatterCustomCapturedDeleteVersion.value,
|
||||
);
|
||||
if (!saved) {
|
||||
selectCustomFormatter(CUSTOM_FORMATTER_NEW);
|
||||
return;
|
||||
}
|
||||
formatter = { kind: "custom-ref", formatterId: saved.id };
|
||||
}
|
||||
settingsStore.updateColumnFormatter(key, formatter);
|
||||
closeColumnFormatter();
|
||||
} catch (error) {
|
||||
toast(t("grid.tableOperationFailed", { message: translateBackendError(t, error) }), 5000);
|
||||
}
|
||||
settingsStore.updateColumnFormatter(key, formatter);
|
||||
closeColumnFormatter();
|
||||
}
|
||||
|
||||
function clearColumnFormatter(columnIndex: number) {
|
||||
@@ -1629,16 +1647,44 @@ function selectFormatterKind(value: FormatterDraftKind, columnIndex: number) {
|
||||
function selectCustomFormatter(value: string) {
|
||||
formatterCustomId.value = value;
|
||||
if (value === CUSTOM_FORMATTER_NEW) {
|
||||
formatterCustomCapturedDeleteVersion.value = undefined;
|
||||
formatterCustomName.value = "";
|
||||
formatterCustomTemplate.value = "${value}";
|
||||
return;
|
||||
}
|
||||
const saved = settingsStore.editorSettings.customColumnFormatters[value];
|
||||
if (!saved) return;
|
||||
formatterCustomCapturedDeleteVersion.value = settingsStore.customColumnFormatterDeleteVersion(saved.id);
|
||||
formatterCustomName.value = saved.name;
|
||||
formatterCustomTemplate.value = saved.template;
|
||||
}
|
||||
|
||||
function requestDeleteCustomFormatter() {
|
||||
if (formatterCustomId.value === CUSTOM_FORMATTER_NEW) return;
|
||||
const saved = settingsStore.editorSettings.customColumnFormatters[formatterCustomId.value];
|
||||
if (!saved) return;
|
||||
formatterCustomDeleteId.value = saved.id;
|
||||
formatterCustomDeleteName.value = saved.name;
|
||||
formatterCustomDeleteOpen.value = true;
|
||||
}
|
||||
|
||||
async function confirmDeleteCustomFormatter() {
|
||||
const id = formatterCustomDeleteId.value;
|
||||
if (!id || formatterCustomDeleteLoading.value) return;
|
||||
formatterCustomDeleteLoading.value = true;
|
||||
try {
|
||||
await settingsStore.deleteCustomColumnFormatter(id);
|
||||
if (formatterCustomId.value === id) selectCustomFormatter(CUSTOM_FORMATTER_NEW);
|
||||
formatterCustomDeleteOpen.value = false;
|
||||
formatterCustomDeleteId.value = "";
|
||||
formatterCustomDeleteName.value = "";
|
||||
} catch (error) {
|
||||
toast(t("grid.tableOperationFailed", { message: translateBackendError(t, error) }), 5000);
|
||||
} finally {
|
||||
formatterCustomDeleteLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function createCustomFormatterId(): string {
|
||||
return `fmt_${uuid()}`;
|
||||
}
|
||||
@@ -11991,17 +12037,31 @@ function currentGridContextMenuItems(): ContextMenuItem[] {
|
||||
<div class="text-xs font-medium text-muted-foreground">
|
||||
{{ t("grid.formatterSavedCustom") }}
|
||||
</div>
|
||||
<Select :model-value="formatterCustomId" @update:model-value="(value: any) => selectCustomFormatter(String(value))">
|
||||
<SelectTrigger class="h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="CUSTOM_FORMATTER_NEW">{{ t("grid.formatterNewCustom") }}</SelectItem>
|
||||
<SelectItem v-for="formatter in savedCustomFormatters" :key="formatter.id" :value="formatter.id">
|
||||
{{ formatter.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div class="flex items-center gap-1">
|
||||
<Select :model-value="formatterCustomId" @update:model-value="(value: any) => selectCustomFormatter(String(value))">
|
||||
<SelectTrigger class="h-8 min-w-0 flex-1 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="CUSTOM_FORMATTER_NEW">{{ t("grid.formatterNewCustom") }}</SelectItem>
|
||||
<SelectItem v-for="formatter in savedCustomFormatters" :key="formatter.id" :value="formatter.id">
|
||||
{{ formatter.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 shrink-0 text-destructive hover:text-destructive"
|
||||
:disabled="formatterCustomId === CUSTOM_FORMATTER_NEW"
|
||||
:title="t('grid.formatterDeleteCustom')"
|
||||
:aria-label="t('grid.formatterDeleteCustom')"
|
||||
@click.stop="requestDeleteCustomFormatter"
|
||||
>
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<label class="block space-y-1.5">
|
||||
<span class="text-xs font-medium text-muted-foreground">
|
||||
@@ -13140,6 +13200,15 @@ function currentGridContextMenuItems(): ContextMenuItem[] {
|
||||
<SqlPreviewPanel :sql="previewSqlText" :loading="isPreviewLoading" :can-undo="canUndoPendingChange" :can-redo="canRedoPendingChange" @undo="undoGridChange" @redo="redoGridChange" @close="closeSqlPreview" />
|
||||
</div>
|
||||
|
||||
<DangerConfirmDialog
|
||||
v-model:open="formatterCustomDeleteOpen"
|
||||
:title="t('grid.formatterDeleteCustom')"
|
||||
:message="t('grid.formatterDeleteCustomMessage', { name: formatterCustomDeleteName })"
|
||||
:confirm-label="t('grid.formatterDeleteCustom')"
|
||||
:loading="formatterCustomDeleteLoading"
|
||||
:close-on-confirm="false"
|
||||
@confirm="confirmDeleteCustomFormatter"
|
||||
/>
|
||||
<DangerConfirmDialog
|
||||
v-model:open="conditionalBulkEditConfirmOpen"
|
||||
:title="t('grid.conditionalBulkEditConfirmTitle')"
|
||||
|
||||
@@ -1728,6 +1728,8 @@ export default {
|
||||
formatterMaskSuffix: "Visible suffix",
|
||||
formatterSavedCustom: "Saved templates",
|
||||
formatterNewCustom: "New template",
|
||||
formatterDeleteCustom: "Delete template",
|
||||
formatterDeleteCustomMessage: 'Delete "{name}"? Formatting will be cleared from every column using this template.',
|
||||
formatterCustomName: "Template name",
|
||||
formatterCustomNamePlaceholder: "Example: User label",
|
||||
formatterCustomTemplateInput: "Template",
|
||||
|
||||
@@ -1612,6 +1612,8 @@ export default withEnglishFallback({
|
||||
formatterMaskSuffix: "Sufijo visible",
|
||||
formatterSavedCustom: "Plantillas guardadas",
|
||||
formatterNewCustom: "Nueva plantilla",
|
||||
formatterDeleteCustom: "Eliminar plantilla",
|
||||
formatterDeleteCustomMessage: '¿Eliminar "{name}"? Se quitará el formato de todas las columnas que usen esta plantilla.',
|
||||
formatterCustomName: "Nombre de plantilla",
|
||||
formatterCustomNamePlaceholder: "Ejemplo: Etiqueta de usuario",
|
||||
formatterCustomTemplateInput: "Plantilla",
|
||||
|
||||
@@ -1610,6 +1610,8 @@ export default withEnglishFallback({
|
||||
formatterMaskSuffix: "Suffisso visibile",
|
||||
formatterSavedCustom: "Modelli salvati",
|
||||
formatterNewCustom: "Nuovo modello",
|
||||
formatterDeleteCustom: "Elimina modello",
|
||||
formatterDeleteCustomMessage: 'Eliminare "{name}"? La formattazione verrà rimossa da tutte le colonne che usano questo modello.',
|
||||
formatterCustomName: "Nome modello",
|
||||
formatterCustomNamePlaceholder: "Esempio: Etichetta utente",
|
||||
formatterCustomTemplateInput: "Modello",
|
||||
|
||||
@@ -1628,6 +1628,8 @@ export default withEnglishFallback({
|
||||
formatterMaskSuffix: "表示する接尾辞",
|
||||
formatterSavedCustom: "保存済みテンプレート",
|
||||
formatterNewCustom: "新しいテンプレート",
|
||||
formatterDeleteCustom: "テンプレートを削除",
|
||||
formatterDeleteCustomMessage: "「{name}」を削除しますか?このテンプレートを使用しているすべての列から書式設定が解除されます。",
|
||||
formatterCustomName: "テンプレート名",
|
||||
formatterCustomNamePlaceholder: "例: ユーザーラベル",
|
||||
formatterCustomTemplateInput: "テンプレート",
|
||||
|
||||
@@ -1612,6 +1612,8 @@ export default withEnglishFallback({
|
||||
formatterMaskSuffix: "Sufixo visível",
|
||||
formatterSavedCustom: "Templates salvos",
|
||||
formatterNewCustom: "Novo template",
|
||||
formatterDeleteCustom: "Excluir template",
|
||||
formatterDeleteCustomMessage: 'Excluir "{name}"? A formatação será removida de todas as colunas que usam este template.',
|
||||
formatterCustomName: "Nome do template",
|
||||
formatterCustomNamePlaceholder: "Exemplo: Rótulo do usuário",
|
||||
formatterCustomTemplateInput: "Template",
|
||||
|
||||
@@ -1653,6 +1653,8 @@ export default withEnglishFallback({
|
||||
formatterMaskSuffix: "后缀保留",
|
||||
formatterSavedCustom: "已保存模板",
|
||||
formatterNewCustom: "新建模板",
|
||||
formatterDeleteCustom: "删除模板",
|
||||
formatterDeleteCustomMessage: "确定删除“{name}”吗?所有使用该模板的字段都会清除格式化设置。",
|
||||
formatterCustomName: "模板名称",
|
||||
formatterCustomNamePlaceholder: "例如:用户标签",
|
||||
formatterCustomTemplateInput: "模板",
|
||||
|
||||
@@ -1611,6 +1611,8 @@ export default withEnglishFallback({
|
||||
formatterMaskSuffix: "字尾保留",
|
||||
formatterSavedCustom: "已儲存範本",
|
||||
formatterNewCustom: "建立範本",
|
||||
formatterDeleteCustom: "刪除範本",
|
||||
formatterDeleteCustomMessage: "確定刪除「{name}」嗎?所有使用此範本的欄位都會清除格式設定。",
|
||||
formatterCustomName: "範本名稱",
|
||||
formatterCustomNamePlaceholder: "例如:使用者標籤",
|
||||
formatterCustomTemplateInput: "範本",
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user