mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 19:23:03 +00:00
fix(connection): refresh and display connection notes
This commit is contained in:
@@ -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>
|
||||
|
||||
+37
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "コピーしました",
|
||||
|
||||
@@ -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: "복사했습니다",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "已复制",
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user