fix(connection): refresh and display connection notes

This commit is contained in:
zipg
2026-08-11 21:53:38 +08:00
committed by GitHub
parent 967084c498
commit 6ea009a586
19 changed files with 199 additions and 19 deletions
@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { ObjectDirective } from "vue";
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
import { computed, nextTick, onMounted, onUnmounted, reactive, ref, watch } from "vue";
import { uuid } from "@/lib/common/utils";
import { useI18n } from "vue-i18n";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
@@ -25,6 +25,7 @@ import { useTunnelProfileStore } from "@/stores/tunnelProfileStore";
import { detachTunnelProfileLayer, tunnelProfileReferenceLayer, tunnelProfileSummary } from "@/lib/connection/tunnelProfiles";
import { applySshConfigHostAliasPrefill as prefillSshConfigHostAlias } from "@/lib/connection/sshConfigHosts";
import { canPersistConnectionTestResult, connectionEditDraftSyncAction } from "./connectionEditDraftSync";
import { createConnectionNoteVisibilityDraft, persistConnectionNoteVisibilityDraft as persistConnectionNoteVisibilityDraftState, resetConnectionNoteVisibilityDraft, setConnectionNoteVisibilityDraft, syncConnectionNoteVisibilityDraft } from "./connectionNoteVisibilityDraft";
import { REDIS_SCAN_PAGE_SIZE_DEFAULT, REDIS_SCAN_PAGE_SIZE_MIN, REDIS_SCAN_PAGE_SIZE_MAX, REDIS_SCAN_PAGE_SIZE_OPTIONS } from "@/lib/redis/redisKeyPattern";
import { normalizeGlobalConnectTimeoutSecs, normalizeGlobalQueryTimeoutSecs, useSettingsStore } from "@/stores/settingsStore";
import { useToast } from "@/composables/useToast";
@@ -189,6 +190,11 @@ type ConnectionTestState = ConnectionTestResult & { ok: boolean };
const { t } = useI18n();
const { toast } = useToast();
const settingsStore = useSettingsStore();
const connectionNoteVisibilityDraft = reactive(createConnectionNoteVisibilityDraft(settingsStore.editorSettings.sidebarShowConnectionNotes));
const showConnectionNotesInSidebar = computed({
get: () => connectionNoteVisibilityDraft.value,
set: (value: boolean) => setConnectionNoteVisibilityDraft(connectionNoteVisibilityDraft, value),
});
const editGlobalConnectTimeoutSecs = ref(settingsStore.editorSettings.globalConnectTimeoutSecs);
const editGlobalQueryTimeoutSecs = ref(settingsStore.editorSettings.globalQueryTimeoutSecs);
const open = defineModel<boolean>("open", { default: false });
@@ -2400,6 +2406,7 @@ watch(
([config, isOpen]) => {
const syncAction = connectionEditDraftSyncAction(config?.id ?? null, isOpen, editingId.value);
if (syncAction === "preserve") return;
resetConnectionNoteVisibilityDraft(connectionNoteVisibilityDraft, settingsStore.editorSettings.sidebarShowConnectionNotes);
editGlobalConnectTimeoutSecs.value = settingsStore.editorSettings.globalConnectTimeoutSecs;
editGlobalQueryTimeoutSecs.value = settingsStore.editorSettings.globalQueryTimeoutSecs;
if (syncAction === "hydrate" && config) {
@@ -2552,6 +2559,11 @@ watch(
{ immediate: true },
);
watch(
() => settingsStore.editorSettings.sidebarShowConnectionNotes,
(value) => syncConnectionNoteVisibilityDraft(connectionNoteVisibilityDraft, value),
);
const isEditing = ref(false);
watch(
() => editingId.value,
@@ -4656,6 +4668,7 @@ function openJdbcDriverManagerFromError() {
function resetForm() {
editingId.value = null;
form.value = defaultForm();
resetConnectionNoteVisibilityDraft(connectionNoteVisibilityDraft, settingsStore.editorSettings.sidebarShowConnectionNotes);
editGlobalConnectTimeoutSecs.value = settingsStore.editorSettings.globalConnectTimeoutSecs;
editGlobalQueryTimeoutSecs.value = settingsStore.editorSettings.globalQueryTimeoutSecs;
selectedTransportLayerId.value = null;
@@ -5013,11 +5026,16 @@ async function persistGlobalTimeoutDrafts() {
});
}
async function persistConnectionNoteVisibilityDraft() {
await persistConnectionNoteVisibilityDraftState(connectionNoteVisibilityDraft, settingsStore.editorSettings.sidebarShowConnectionNotes, (value) => settingsStore.updateEditorSettingsAndPersist({ sidebarShowConnectionNotes: value }));
}
async function save() {
if (!ensureConnectionHostResolvedFromUrl()) return;
if (isSaving.value) return;
const databaseInfoForSave = visibleTestDatabaseInfo.value ?? visibleSavedDatabaseInfo.value;
isSaving.value = true;
let connectionSaved = false;
try {
if (editingId.value) {
const updated = withSavedDatabaseInfo(connectionConfigForSubmit(editingId.value), databaseInfoForSave);
@@ -5025,6 +5043,8 @@ async function save() {
await ensureRequiredGaussdbMJdbcRuntime(updated);
await persistGlobalTimeoutDrafts();
await store.updateConnection(updated);
connectionSaved = true;
await persistConnectionNoteVisibilityDraft();
store.stopEditing();
} else {
const config = withSavedDatabaseInfo(connectionConfigForSubmit(draftTestConnectionId.value), databaseInfoForSave);
@@ -5032,6 +5052,8 @@ async function save() {
await ensureRequiredGaussdbMJdbcRuntime(config);
await persistGlobalTimeoutDrafts();
await store.addConnection(config);
connectionSaved = true;
await persistConnectionNoteVisibilityDraft();
draftTestConnectionId.value = uuid();
if (config.db_type === "jdbc") {
open.value = false;
@@ -5055,7 +5077,8 @@ async function save() {
}
open.value = false;
} catch (e: any) {
const message = mongodbAuthFailureHint(String(e?.message || e));
const cause = mongodbAuthFailureHint(String(e?.message || e));
const message = connectionSaved ? t("connection.savedSettingsFailed", { message: cause }) : cause;
testResult.value = { ok: false, message };
showConnectionError(message);
} finally {
@@ -7204,14 +7227,32 @@ function openExternalUrl(url: string) {
<div class="grid grid-cols-4 items-start gap-4">
<Label :class="connectionLabelTopClass">{{ t("connection.note") }}</Label>
<textarea
ref="noteTextareaRef"
v-model="form.note"
rows="1"
class="col-span-3 min-h-8 w-full min-w-0 resize-none overflow-y-hidden rounded-md border border-input bg-transparent px-2.5 py-1 text-base leading-5 transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 dark:bg-input/30 md:text-sm"
:placeholder="t('connection.notePlaceholder')"
@input="resizeNoteTextarea"
/>
<div class="col-span-3 flex min-w-0 items-start gap-3">
<textarea
ref="noteTextareaRef"
v-model="form.note"
rows="1"
class="min-h-8 min-w-0 flex-1 resize-none overflow-y-hidden rounded-md border border-input bg-transparent px-2.5 py-1 text-base leading-5 transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 dark:bg-input/30 md:text-sm"
:placeholder="t('connection.notePlaceholder')"
@input="resizeNoteTextarea"
/>
<div class="mt-1.5 flex shrink-0 items-center gap-2">
<div class="flex items-center gap-1">
<Label for="connection-note-sidebar-visibility" class="text-xs font-normal text-muted-foreground">
{{ t("connection.noteShow") }}
</Label>
<Tooltip>
<TooltipTrigger as-child>
<CircleHelp class="h-3.5 w-3.5 cursor-help text-muted-foreground hover:text-foreground" />
</TooltipTrigger>
<TooltipContent side="top" align="center" class="max-w-[280px] text-xs leading-relaxed">
{{ t("connection.noteShowInSidebar") }}
</TooltipContent>
</Tooltip>
</div>
<Switch id="connection-note-sidebar-visibility" v-model="showConnectionNotesInSidebar" :aria-label="t('connection.noteShowInSidebar')" />
</div>
</div>
</div>
</div>
</TabsContent>
@@ -0,0 +1,37 @@
import { describe, expect, it, vi } from "vitest";
import { createConnectionNoteVisibilityDraft, persistConnectionNoteVisibilityDraft, resetConnectionNoteVisibilityDraft, setConnectionNoteVisibilityDraft, syncConnectionNoteVisibilityDraft } from "../connectionNoteVisibilityDraft";
describe("connection note visibility draft", () => {
it("follows external settings updates until the user changes the switch", () => {
const draft = createConnectionNoteVisibilityDraft(false);
syncConnectionNoteVisibilityDraft(draft, true);
expect(draft).toEqual({ value: true, dirty: false });
setConnectionNoteVisibilityDraft(draft, false);
syncConnectionNoteVisibilityDraft(draft, true);
expect(draft).toEqual({ value: false, dirty: true });
});
it("discards an unsaved switch change when the dialog closes", () => {
const draft = createConnectionNoteVisibilityDraft(false);
setConnectionNoteVisibilityDraft(draft, true);
resetConnectionNoteVisibilityDraft(draft, false);
expect(draft).toEqual({ value: false, dirty: false });
});
it("keeps the draft dirty after persistence fails so retry remains possible", async () => {
const draft = createConnectionNoteVisibilityDraft(false);
const persist = vi.fn().mockRejectedValueOnce(new Error("storage unavailable")).mockResolvedValueOnce(undefined);
setConnectionNoteVisibilityDraft(draft, true);
await expect(persistConnectionNoteVisibilityDraft(draft, false, persist)).rejects.toThrow("storage unavailable");
expect(draft).toEqual({ value: true, dirty: true });
await persistConnectionNoteVisibilityDraft(draft, false, persist);
expect(draft).toEqual({ value: true, dirty: false });
expect(persist).toHaveBeenCalledTimes(2);
});
});
@@ -0,0 +1,29 @@
export interface ConnectionNoteVisibilityDraft {
value: boolean;
dirty: boolean;
}
export function createConnectionNoteVisibilityDraft(persistedValue: boolean): ConnectionNoteVisibilityDraft {
return { value: persistedValue, dirty: false };
}
export function setConnectionNoteVisibilityDraft(draft: ConnectionNoteVisibilityDraft, value: boolean): void {
draft.value = value;
draft.dirty = true;
}
export function syncConnectionNoteVisibilityDraft(draft: ConnectionNoteVisibilityDraft, persistedValue: boolean): void {
if (draft.dirty) return;
draft.value = persistedValue;
}
export function resetConnectionNoteVisibilityDraft(draft: ConnectionNoteVisibilityDraft, persistedValue: boolean): void {
draft.value = persistedValue;
draft.dirty = false;
}
export async function persistConnectionNoteVisibilityDraft(draft: ConnectionNoteVisibilityDraft, persistedValue: boolean, persist: (value: boolean) => Promise<void>): Promise<void> {
if (!draft.dirty) return;
if (draft.value !== persistedValue) await persist(draft.value);
draft.dirty = false;
}
@@ -509,7 +509,7 @@ function measureSidebarCommentLabelWidths() {
id,
depth,
alignable: isSidebarCommentAlignableNode(node),
hasComment: !!sidebarTreeNodeComment(node),
hasComment: !!sidebarTreeNodeComment(node, settingsStore.editorSettings.sidebarShowConnectionNotes),
labelWidth: context.measureText(sidebarCommentLabel(node)).width,
})),
);
@@ -526,7 +526,7 @@ function scheduleSidebarCommentLabelMeasure() {
function sidebarNodeHasTrailingMetadata(node: TreeNode): boolean {
const mode = settingsStore.editorSettings.sidebarObjectInfoMode;
if (mode.startsWith("comment-") && sidebarTreeNodeComment(node)) return true;
if (mode.startsWith("comment-") && sidebarTreeNodeComment(node, settingsStore.editorSettings.sidebarShowConnectionNotes)) return true;
return mode === "size" && sidebarStorageDisplayTypes.has(node.type) && !!formatSidebarObjectStorage(node.sizeBytes);
}
@@ -563,10 +563,14 @@ function scheduleSidebarTreeContentWidthMeasure() {
sidebarTreeContentMeasureFrame = window.requestAnimationFrame(measureSidebarTreeContentWidth);
}
watch([flatNodes, () => settingsStore.editorSettings.sidebarObjectInfoMode, () => settingsStore.editorSettings.sidebarHiddenTablePrefixes, () => settingsStore.editorSettings.uiFontFamily, () => settingsStore.editorSettings.uiScale], scheduleSidebarCommentLabelMeasure, {
flush: "post",
immediate: true,
});
watch(
[flatNodes, () => settingsStore.editorSettings.sidebarObjectInfoMode, () => settingsStore.editorSettings.sidebarShowConnectionNotes, () => settingsStore.editorSettings.sidebarHiddenTablePrefixes, () => settingsStore.editorSettings.uiFontFamily, () => settingsStore.editorSettings.uiScale],
scheduleSidebarCommentLabelMeasure,
{
flush: "post",
immediate: true,
},
);
watch([sidebarTreeNaturalWidthItems, () => settingsStore.editorSettings.uiFontFamily, () => settingsStore.editorSettings.uiScale], scheduleSidebarTreeContentWidthMeasure, {
flush: "post",
@@ -613,7 +613,7 @@ function isNodeDefaultSchema(): boolean {
const trailingComment = computed(() => {
if (!settingsStore.editorSettings.sidebarObjectInfoMode.startsWith("comment-")) return null;
return sidebarTreeNodeComment(activeNode.value);
return sidebarTreeNodeComment(activeNode.value, settingsStore.editorSettings.sidebarShowConnectionNotes);
});
function isRightAlignedComment(): boolean {
+3
View File
@@ -406,6 +406,8 @@ export default {
name: "Name",
namePlaceholder: "Connection name, auto-generated if empty",
note: "Notes",
noteShow: "Show",
noteShowInSidebar: "Show the note in the connection list",
notePlaceholder: "Do not store passwords in plaintext in notes",
type: "Type",
host: "Host",
@@ -941,6 +943,7 @@ export default {
errorIndicatorHint: "Connection error. Click to view details",
clearError: "Clear connection error",
saveFailed: "Failed to save connection: {message}",
savedSettingsFailed: "The connection was saved, but the display setting could not be saved: {message}. Retry to save the setting.",
deleted: "Connection deleted",
deletedSelected: "{count} connections deleted",
copied: "Copied",
+3
View File
@@ -407,6 +407,8 @@ export default withEnglishFallback({
name: "Nombre",
namePlaceholder: "Nombre de conexión, se genera automáticamente si está vacío",
note: "Notas",
noteShow: "Mostrar",
noteShowInSidebar: "Mostrar la nota en la lista de conexiones",
notePlaceholder: "No guardes contraseñas en texto plano en las notas",
type: "Tipo",
host: "Host",
@@ -741,6 +743,7 @@ export default withEnglishFallback({
errorIndicatorHint: "Error de conexión. Haz clic para ver los detalles",
clearError: "Limpiar error de conexión",
saveFailed: "Error al guardar la conexión: {message}",
savedSettingsFailed: "La conexión se guardó, pero no se pudo guardar la configuración de visualización: {message}. Vuelve a intentarlo para guardar la configuración.",
deleted: "Conexión eliminada",
deletedSelected: "{count} conexiones eliminadas",
copied: "Copiado",
+3
View File
@@ -406,6 +406,8 @@ export default withEnglishFallback({
name: "Nome",
namePlaceholder: "Nome connessione, generato automaticamente se vuoto",
note: "Note",
noteShow: "Mostra",
noteShowInSidebar: "Mostra la nota nell'elenco delle connessioni",
notePlaceholder: "Non salvare password in chiaro nelle note",
type: "Tipo",
host: "Host",
@@ -739,6 +741,7 @@ export default withEnglishFallback({
errorIndicatorHint: "Errore di connessione. Fai clic per visualizzare i dettagli",
clearError: "Cancella errore di connessione",
saveFailed: "Impossibile salvare la connessione: {message}",
savedSettingsFailed: "La connessione è stata salvata, ma non è stato possibile salvare l'impostazione di visualizzazione: {message}. Riprova per salvare l'impostazione.",
deleted: "Connessione eliminata",
deletedSelected: "{count} connessioni eliminate",
copied: "Copiato",
+3
View File
@@ -406,6 +406,8 @@ export default withEnglishFallback({
name: "名前",
namePlaceholder: "接続名(空の場合は自動生成)",
note: "メモ",
noteShow: "表示",
noteShowInSidebar: "接続一覧にメモを表示",
notePlaceholder: "メモにパスワードを平文で保存しないでください",
type: "タイプ",
host: "ホスト",
@@ -705,6 +707,7 @@ export default withEnglishFallback({
errorIndicatorHint: "接続エラー。クリックして詳細を表示",
clearError: "接続エラーをクリア",
saveFailed: "接続の保存に失敗しました: {message}",
savedSettingsFailed: "接続は保存されましたが、表示設定を保存できませんでした: {message}。設定を保存するには再試行してください。",
deleted: "接続を削除しました",
deletedSelected: "{count}件の接続を削除しました",
copied: "コピーしました",
+3
View File
@@ -400,6 +400,8 @@ export default withEnglishFallback({
name: "이름",
namePlaceholder: "연결 이름, 비워두면 자동 생성",
note: "메모",
noteShow: "표시",
noteShowInSidebar: "연결 목록에 메모 표시",
notePlaceholder: "메모에 비밀번호를 평문으로 저장하지 마세요",
type: "유형",
host: "호스트",
@@ -838,6 +840,7 @@ export default withEnglishFallback({
errorIndicatorHint: "연결 오류. 클릭하여 상세 보기",
clearError: "연결 오류 지우기",
saveFailed: "연결 저장 실패: {message}",
savedSettingsFailed: "연결은 저장되었지만 표시 설정을 저장하지 못했습니다: {message}. 설정을 저장하려면 다시 시도하세요.",
deleted: "연결을 삭제했습니다",
deletedSelected: "연결 {count}개 삭제",
copied: "복사했습니다",
+3
View File
@@ -407,6 +407,8 @@ export default withEnglishFallback({
name: "Nome",
namePlaceholder: "Nome da conexão, gerado automaticamente se vazio",
note: "Observações",
noteShow: "Exibir",
noteShowInSidebar: "Exibir a nota na lista de conexões",
notePlaceholder: "Não salve senhas em texto simples nas observações",
type: "Tipo",
host: "Host",
@@ -740,6 +742,7 @@ export default withEnglishFallback({
errorIndicatorHint: "Erro de conexão. Clique para ver os detalhes",
clearError: "Limpar erro de conexão",
saveFailed: "Falha ao salvar a conexão: {message}",
savedSettingsFailed: "A conexão foi salva, mas não foi possível salvar a configuração de exibição: {message}. Tente novamente para salvar a configuração.",
deleted: "Conexão excluída",
deletedSelected: "{count} conexões excluídas",
copied: "Copiado",
+3
View File
@@ -332,6 +332,8 @@ export default withEnglishFallback({
name: "名称",
namePlaceholder: "连接名称,留空则自动生成",
note: "备注",
noteShow: "显示",
noteShowInSidebar: "在连接列表中显示备注",
notePlaceholder: "请勿在备注中明文保存密码",
type: "类型",
host: "主机",
@@ -869,6 +871,7 @@ export default withEnglishFallback({
errorIndicatorHint: "连接错误,点击查看详情",
clearError: "清除连接错误",
saveFailed: "保存连接失败:{message}",
savedSettingsFailed: "连接已保存,但显示设置保存失败:{message}。请重试以保存该设置。",
deleted: "连接已删除",
deletedSelected: "已删除 {count} 个连接",
copied: "已复制",
+3
View File
@@ -407,6 +407,8 @@ export default withEnglishFallback({
name: "名稱",
namePlaceholder: "連線名稱,留空則自動產生",
note: "備註",
noteShow: "顯示",
noteShowInSidebar: "在連線清單中顯示備註",
notePlaceholder: "請勿在備註中以明文儲存密碼",
type: "類型",
host: "主機",
@@ -739,6 +741,7 @@ export default withEnglishFallback({
errorIndicatorHint: "連線錯誤,點擊檢視詳情",
clearError: "清除連線錯誤",
saveFailed: "儲存連線失敗:{message}",
savedSettingsFailed: "連線已儲存,但顯示設定儲存失敗:{message}。請重試以儲存該設定。",
deleted: "連線已刪除",
deletedSelected: "已刪除 {count} 個連線",
copied: "已複製",
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import { alignedCommentLeadingWidth, alignedSidebarCommentLabelWidths, canTreeNodeShowExpander, sidebarTreeNaturalContentWidth, trailingCommentAvailableWidth, treeLabelWidthClass, usesFullWidthTreeLabel } from "@/lib/sidebar/sidebarTreeItemLayout";
import { alignedCommentLeadingWidth, alignedSidebarCommentLabelWidths, canTreeNodeShowExpander, sidebarTreeNaturalContentWidth, sidebarTreeNodeComment, trailingCommentAvailableWidth, treeLabelWidthClass, usesFullWidthTreeLabel } from "@/lib/sidebar/sidebarTreeItemLayout";
import type { TreeNode } from "@/types/database";
describe("sidebar tree item layout", () => {
it("keeps a table row constrained when it displays a comment", () => {
@@ -7,6 +8,12 @@ describe("sidebar tree item layout", () => {
expect(usesFullWidthTreeLabel("table", true, true)).toBe(false);
});
it("shows connection notes only after the user opts in", () => {
const connection: TreeNode = { id: "connection-1", label: "MySQL", type: "connection", comment: "Production" };
expect(sidebarTreeNodeComment(connection, false)).toBeNull();
expect(sidebarTreeNodeComment(connection, true)).toBe("Production");
});
it("lets a table name consume the available row width before truncating when aligned", () => {
expect(treeLabelWidthClass({ fullWidth: false, hasTrailingComment: true, alignLeading: true })).toBe("min-w-0 flex-1 truncate");
// inline/right 模式 label 不撑满,让 comment 紧跟
@@ -127,8 +127,9 @@ export function alignedSidebarCommentLabelWidths(items: readonly SidebarCommentA
return widths;
}
export function sidebarTreeNodeComment(node: TreeNode): string | null {
export function sidebarTreeNodeComment(node: TreeNode, showConnectionNotes: boolean): string | null {
if (!commentTypes.has(node.type)) return null;
if (node.type === "connection" && !showConnectionNotes) return null;
if (node.type === "column" && node.meta && "comment" in node.meta) {
const comment = node.meta.comment;
return typeof comment === "string" && comment ? comment : null;
@@ -118,12 +118,18 @@ describe("connectionStore database info", () => {
const store = useConnectionStore();
await store.addConnection(config);
await store.connect(config);
const connectionNode = store.treeNodes[0];
connectionNode.isExpanded = true;
const existingChildIds = connectionNode.children?.map((child) => child.id);
await store.updateConnection({ ...config, note: "Production reporting" });
expect(saveConnections).toHaveBeenLastCalledWith([expect.objectContaining({ id: config.id, note: "Production reporting" })]);
expect(store.getConfig(config.id)?.note).toBe("Production reporting");
expect(store.connectedIds.has(config.id)).toBe(true);
expect(store.treeNodes[0].comment).toBe("Production reporting");
expect(store.treeNodes[0].isExpanded).toBe(true);
expect(store.treeNodes[0].children?.map((child) => child.id)).toEqual(existingChildIds);
});
it("does not delay connection success while optional metadata is loading", async () => {
+26
View File
@@ -41,6 +41,12 @@ describe("normalizeEditorSettings", () => {
expect(normalizeEditorSettings({ sidebarObjectInfoMode: "invalid" } as any).sidebarObjectInfoMode).toBe("comment-inline");
});
it("hides connection notes by default and preserves an explicit opt-in", () => {
expect(normalizeEditorSettings({}).sidebarShowConnectionNotes).toBe(false);
expect(normalizeEditorSettings({ sidebarShowConnectionNotes: true }).sidebarShowConnectionNotes).toBe(true);
expect(normalizeEditorSettings({ sidebarShowConnectionNotes: false }).sidebarShowConnectionNotes).toBe(false);
});
it("defaults SQL execution to the current statement and migrates legacy execute-all settings", () => {
expect(normalizeEditorSettings({}).executeMode).toBe("current");
expect(normalizeEditorSettings({ executeMode: "all" }).executeMode).toBe("current");
@@ -665,6 +671,26 @@ describe("settingsStore persisted settings initialization", () => {
}),
);
});
it("atomically updates connection note visibility and supports retry", async () => {
const loadEditorSettings = vi.fn().mockResolvedValue({ sidebarShowConnectionNotes: false });
const saveEditorSettings = vi.fn().mockResolvedValue(undefined);
vi.doMock("@/lib/backend/api", () => ({ loadEditorSettings, saveEditorSettings }));
const { useSettingsStore } = await import("@/stores/settingsStore");
const store = useSettingsStore();
await store.initEditorSettings();
saveEditorSettings.mockClear();
saveEditorSettings.mockRejectedValueOnce(new Error("storage unavailable")).mockResolvedValueOnce(undefined);
await expect(store.updateEditorSettingsAndPersist({ sidebarShowConnectionNotes: true })).rejects.toThrow("storage unavailable");
expect(store.editorSettings.sidebarShowConnectionNotes).toBe(false);
await store.updateEditorSettingsAndPersist({ sidebarShowConnectionNotes: true });
expect(store.editorSettings.sidebarShowConnectionNotes).toBe(true);
expect(saveEditorSettings).toHaveBeenCalledTimes(2);
expect(saveEditorSettings).toHaveBeenLastCalledWith(expect.objectContaining({ sidebarShowConnectionNotes: true }));
});
});
describe("settingsStore editor settings persistence", () => {
@@ -6763,6 +6763,7 @@ export const useConnectionStore = defineStore("connection", () => {
return inheritNaturalTreeNodeOrder(node, {
...existing,
label: node.label,
comment: node.comment,
pinned: node.pinned,
children: withSavedSqlRoot(node.connectionId!, existing.children || [], existing),
});
+4
View File
@@ -577,6 +577,7 @@ export interface EditorSettings {
updateNotificationsEnabled: boolean;
sidebarHiddenTablePrefixes: string[];
sidebarObjectInfoMode: SidebarObjectInfoMode;
sidebarShowConnectionNotes: boolean;
sidebarAllowHorizontalScroll: boolean;
columnFormatters: Record<string, ColumnFormatterConfig>;
customColumnFormatters: Record<string, CustomColumnFormatterConfig>;
@@ -767,6 +768,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
updateNotificationsEnabled: true,
sidebarHiddenTablePrefixes: [],
sidebarObjectInfoMode: "comment-inline",
sidebarShowConnectionNotes: false,
sidebarAllowHorizontalScroll: false,
columnFormatters: {},
customColumnFormatters: {},
@@ -1142,6 +1144,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
}
).sidebarShowDatabaseSizes,
),
sidebarShowConnectionNotes: settings.sidebarShowConnectionNotes === true,
sidebarAllowHorizontalScroll: settings.sidebarAllowHorizontalScroll ?? DEFAULT_EDITOR_SETTINGS.sidebarAllowHorizontalScroll,
columnFormatters: normalizeColumnFormatters(settings.columnFormatters),
customColumnFormatters: normalizeCustomColumnFormatters(settings.customColumnFormatters),
@@ -1678,6 +1681,7 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.updateNotificationsEnabled !== undefined) editorSettings.value.updateNotificationsEnabled = partial.updateNotificationsEnabled;
if (partial.sidebarHiddenTablePrefixes !== undefined) editorSettings.value.sidebarHiddenTablePrefixes = normalizeSidebarHiddenTablePrefixes(partial.sidebarHiddenTablePrefixes);
if (partial.sidebarObjectInfoMode !== undefined) editorSettings.value.sidebarObjectInfoMode = normalizeSidebarObjectInfoMode(partial.sidebarObjectInfoMode);
if (partial.sidebarShowConnectionNotes !== undefined) editorSettings.value.sidebarShowConnectionNotes = partial.sidebarShowConnectionNotes === true;
if (partial.sidebarAllowHorizontalScroll !== undefined) editorSettings.value.sidebarAllowHorizontalScroll = partial.sidebarAllowHorizontalScroll;
if (partial.columnFormatters !== undefined) editorSettings.value.columnFormatters = partial.columnFormatters;
if (partial.customColumnFormatters !== undefined) editorSettings.value.customColumnFormatters = partial.customColumnFormatters;