feat(updater): decouple update notice from auto download

Closes #9329
This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-09-16 23:50:18 +08:00
1 parent b891bf80fd
commit 2c97e44d82
20 files changed
+187 -18

No files matched your search

@@ -659,6 +659,7 @@ const editFormatSqlOnSqlFileSave = ref(settingsStore.editorSettings.formatSqlOnS
const editShowTableDdlHoverPreview = ref(settingsStore.editorSettings.showTableDdlHoverPreview);
const editClickTableNavigationTarget = ref<ClickTableNavigationTarget>(settingsStore.editorSettings.clickTableNavigationTarget);
const editUpdateNotificationsEnabled = ref(settingsStore.editorSettings.updateNotificationsEnabled);
const editAutoDownloadUpdates = ref(settingsStore.editorSettings.autoDownloadUpdates);
const editSidebarHiddenTablePrefixes = ref(settingsStore.editorSettings.sidebarHiddenTablePrefixes.join("\n"));
const editSidebarCopyTableNameSeparator = ref<ColumnNameCopySeparator>(settingsStore.editorSettings.sidebarCopyTableNameSeparator);
const editSidebarCopyTableNameIncludeSchema = ref(settingsStore.editorSettings.sidebarCopyTableNameIncludeSchema);
@@ -816,6 +817,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
formatSqlOnSqlFileSave: editFormatSqlOnSqlFileSave.value,
showTableDdlHoverPreview: editShowTableDdlHoverPreview.value,
updateNotificationsEnabled: editUpdateNotificationsEnabled.value,
autoDownloadUpdates: editAutoDownloadUpdates.value,
sidebarObjectInfoMode: editSidebarObjectInfoMode.value,
sidebarAllowHorizontalScroll: editSidebarAllowHorizontalScroll.value,
sidebarShowTooltips: editSidebarShowTooltips.value,
@@ -1341,6 +1343,7 @@ function syncEditorSettingsDraftFromStore() {
editShowTableDdlHoverPreview.value = settingsStore.editorSettings.showTableDdlHoverPreview;
editClickTableNavigationTarget.value = settingsStore.editorSettings.clickTableNavigationTarget;
editUpdateNotificationsEnabled.value = settingsStore.editorSettings.updateNotificationsEnabled;
editAutoDownloadUpdates.value = settingsStore.editorSettings.autoDownloadUpdates;
editSidebarHiddenTablePrefixes.value = settingsStore.editorSettings.sidebarHiddenTablePrefixes.join("\n");
editSidebarCopyTableNameSeparator.value = settingsStore.editorSettings.sidebarCopyTableNameSeparator;
editSidebarCopyTableNameIncludeSchema.value = settingsStore.editorSettings.sidebarCopyTableNameIncludeSchema;
@@ -1456,6 +1459,7 @@ const editorSettingsDraftRefs: EditorSettingsDraftRefMap = {
formatSqlOnSqlFileSave: editFormatSqlOnSqlFileSave,
showTableDdlHoverPreview: editShowTableDdlHoverPreview,
updateNotificationsEnabled: editUpdateNotificationsEnabled,
autoDownloadUpdates: editAutoDownloadUpdates,
sidebarObjectInfoMode: editSidebarObjectInfoMode,
sidebarAllowHorizontalScroll: editSidebarAllowHorizontalScroll,
sidebarShowTooltips: editSidebarShowTooltips,
@@ -1935,6 +1939,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
} else if (tab === "about") {
editUpdateDownloadSource.value = DEFAULT_EDITOR_SETTINGS.updateDownloadSource;
editUpdateNotificationsEnabled.value = DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled;
editAutoDownloadUpdates.value = DEFAULT_EDITOR_SETTINGS.autoDownloadUpdates;
}
}
@@ -2030,6 +2035,7 @@ function resetAllDefaults() {
editFormatSqlOnSqlFileSave.value = DEFAULT_EDITOR_SETTINGS.formatSqlOnSqlFileSave;
editShowTableDdlHoverPreview.value = DEFAULT_EDITOR_SETTINGS.showTableDdlHoverPreview;
editUpdateNotificationsEnabled.value = DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled;
editAutoDownloadUpdates.value = DEFAULT_EDITOR_SETTINGS.autoDownloadUpdates;
editSidebarObjectInfoMode.value = DEFAULT_EDITOR_SETTINGS.sidebarObjectInfoMode;
editSidebarAllowHorizontalScroll.value = DEFAULT_EDITOR_SETTINGS.sidebarAllowHorizontalScroll;
editSidebarShowTooltips.value = DEFAULT_EDITOR_SETTINGS.sidebarShowTooltips;
@@ -9631,6 +9637,15 @@ LIMIT 100;</pre
</div>
<Switch id="update-notifications-enabled" v-model="editUpdateNotificationsEnabled" />
</div>
<div class="flex items-center justify-between gap-4">
<div class="min-w-0 space-y-1">
<Label for="auto-download-updates">{{ t("settings.autoDownloadUpdates") }}</Label>
<p class="text-sm text-muted-foreground">
{{ t("settings.autoDownloadUpdatesDescription") }}
</p>
</div>
<Switch id="auto-download-updates" v-model="editAutoDownloadUpdates" />
</div>
<div class="flex flex-col gap-3 border-t border-border/60 pt-4 sm:flex-row sm:items-center sm:justify-between">
<div class="min-w-0 space-y-1">
<Label>{{ t("settings.updateDownloadSource") }}</Label>
+1 -1
View File
@@ -112,7 +112,7 @@ function buttonWithText(text: string): HTMLButtonElement | undefined {
}
function downloadButton(): HTMLButtonElement | undefined {
return buttonWithText("Retry Download");
return buttonWithText("Download in Background") ?? buttonWithText("Retry Download");
}
function cancelDownloadButton(): HTMLButtonElement | undefined {
@@ -194,7 +194,7 @@ watch(
</Button>
</template>
<Button v-else-if="updateDownloaded" class="shrink-0" :disabled="activeTaskCount > 0 || isIgnoringUpdate" @click="emit('install-downloaded')">{{ t("updates.restartAndUpdate") }}</Button>
<Button v-else-if="!checkingUpdates" class="shrink-0" :disabled="isIgnoringUpdate" @click="emit('download-in-background')">{{ t("updates.retryDownload") }}</Button>
<Button v-else-if="!checkingUpdates" class="shrink-0" :disabled="isIgnoringUpdate" @click="emit('download-in-background')">{{ t(updateCheckFailed ? "updates.retryDownload" : "updates.downloadInBackground") }}</Button>
</template>
</template>
<template v-else>
@@ -21,7 +21,7 @@ vi.mock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => true }));
vi.mock("@tauri-apps/api/event", () => ({ listen: mocks.listen }));
vi.mock("@tauri-apps/plugin-process", () => ({ relaunch: mocks.relaunch }));
vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: mocks.toast }) }));
const settings = reactive({ updateDownloadSource: "official", ignoredUpdateVersion: "", updateNotificationsEnabled: true });
const settings = reactive({ updateDownloadSource: "official", ignoredUpdateVersion: "", updateNotificationsEnabled: true, autoDownloadUpdates: true });
vi.mock("@/stores/settingsStore", () => ({ useSettingsStore: () => ({ editorSettings: settings, updateEditorSettingsAndPersist: mocks.persist }) }));
const info = { current_version: "1.0.0", latest_version: "1.1.0", update_available: true, portable_mode: false, manual_update_only: false, release_name: "v1.1.0", release_url: "https://example.com", release_notes: "Changes" };
const cache = { cache_id: "cached", version: "1.1.0", portable_mode: false, release_url: "https://example.com", release_notes: "Changes", downloaded_at: 1 };
@@ -58,6 +58,7 @@ beforeEach(() => {
settings.updateDownloadSource = "official";
settings.ignoredUpdateVersion = "";
settings.updateNotificationsEnabled = true;
settings.autoDownloadUpdates = true;
mocks.checkForUpdates.mockResolvedValue(info);
mocks.downloadUpdate.mockResolvedValue(cache);
mocks.getDownloadedUpdate.mockResolvedValue(null);
@@ -70,6 +71,96 @@ afterEach(() => {
vi.useRealTimers();
});
describe("silent update lifecycle", () => {
it("checks and shows a badge without downloading when automatic downloads are disabled", async () => {
settings.autoDownloadUpdates = false;
const updater = mount();
await updater.initialize();
await flush();
expect(mocks.checkForUpdates).toHaveBeenCalled();
expect(mocks.downloadUpdate).not.toHaveBeenCalled();
expect(updater.hasUpdateAvailable.value).toBe(true);
expect(updater.showUpdateDialog.value).toBe(false);
await updater.checkUpdates();
expect(updater.showUpdateDialog.value).toBe(true);
expect(mocks.downloadUpdate).not.toHaveBeenCalled();
await updater.downloadUpdateInBackground();
expect(mocks.downloadUpdate).toHaveBeenCalledOnce();
expect(updater.updateDownloaded.value).toBe(true);
expect(mocks.installDownloadedUpdate).not.toHaveBeenCalled();
});
it("surfaces a newer release instead of a stale package with automatic downloads disabled", async () => {
settings.autoDownloadUpdates = false;
mocks.getDownloadedUpdate.mockResolvedValue(cache);
mocks.checkForUpdates.mockResolvedValue({ ...info, latest_version: "1.2.0", release_name: "v1.2.0" });
const updater = mount();
await updater.initialize();
await flush();
expect(updater.updateInfo.value?.latest_version).toBe("1.2.0");
expect(mocks.discardDownloadedUpdate).toHaveBeenCalledWith(cache.cache_id);
expect(mocks.downloadUpdate).not.toHaveBeenCalled();
expect(updater.updateDownloaded.value).toBe(false);
expect(updater.hasUpdateAvailable.value).toBe(true);
await updater.installDownloadedUpdate();
expect(mocks.installDownloadedUpdate).not.toHaveBeenCalled();
mocks.downloadUpdate.mockResolvedValue({ ...cache, version: "1.2.0" });
await updater.downloadUpdateInBackground();
expect(mocks.downloadUpdate).toHaveBeenCalledWith("official", "1.2.0", expect.any(String), "Changes");
expect(updater.updateDownloaded.value).toBe(true);
});
it("preserves a prepared package with automatic downloads disabled", async () => {
settings.autoDownloadUpdates = false;
mocks.getDownloadedUpdate.mockResolvedValue(cache);
const updater = mount();
await updater.initialize();
await flush();
expect(mocks.discardDownloadedUpdate).not.toHaveBeenCalled();
expect(mocks.downloadUpdate).not.toHaveBeenCalled();
expect(updater.updateInfo.value?.latest_version).toBe(cache.version);
expect(updater.hasUpdateAvailable.value).toBe(true);
await updater.installDownloadedUpdate();
expect(mocks.installDownloadedUpdate).toHaveBeenCalledWith(cache.cache_id, cache.version);
});
it("starts a silent download when the preference is enabled", async () => {
settings.autoDownloadUpdates = false;
const updater = mount();
await updater.initialize();
await flush();
settings.autoDownloadUpdates = true;
await vi.waitFor(() => expect(updater.updateDownloaded.value).toBe(true));
expect(mocks.downloadUpdate).toHaveBeenCalledOnce();
expect(updater.showUpdateDialog.value).toBe(false);
});
it("cancels an automatic download when the preference is disabled", async () => {
const pending = deferred<typeof cache>();
mocks.downloadUpdate.mockReturnValue(pending.promise);
mocks.cancelUpdateDownload.mockImplementation(async () => pending.reject(new Error("cancelled")));
const updater = mount();
const checking = updater.checkUpdates({ silent: true });
await vi.waitFor(() => expect(mocks.downloadUpdate).toHaveBeenCalledOnce());
settings.autoDownloadUpdates = false;
await vi.waitFor(() => expect(updater.phase.value).toBe("idle"));
await checking;
expect(mocks.cancelUpdateDownload).toHaveBeenCalledOnce();
expect(updater.hasUpdateAvailable.value).toBe(true);
});
it("does not cancel a manually started background download when the preference changes", async () => {
settings.autoDownloadUpdates = false;
const updater = mount();
await updater.checkUpdates();
const pending = deferred<typeof cache>();
mocks.downloadUpdate.mockReturnValue(pending.promise);
const downloading = updater.downloadUpdateInBackground();
await flush();
settings.autoDownloadUpdates = true;
await nextTick();
settings.autoDownloadUpdates = false;
await flush();
expect(mocks.cancelUpdateDownload).not.toHaveBeenCalled();
updater.showUpdateDialog.value = false;
pending.resolve(cache);
await downloading;
expect(updater.updateDownloaded.value).toBe(true);
});
it("automatically downloads without surfacing or installing, even while idle", async () => {
const updater = mount();
await updater.checkUpdates({ silent: true });
+33 -4
View File
@@ -134,12 +134,17 @@ export function useAppUpdater(options: UseAppUpdaterOptions = {}) {
const updateReady = computed(() => phase.value === "restart");
const activeTaskCount = computed(() => Math.max(0, Math.trunc(options.getActiveTaskCount?.() ?? 0)));
const notificationsEnabled = computed(() => settingsStore.editorSettings.updateNotificationsEnabled !== false);
const autoDownloadEnabled = computed(() => settingsStore.editorSettings.autoDownloadUpdates === true);
const hasUpdateAvailable = computed(
() => notificationsEnabled.value && (updateDownloaded.value || updateReady.value || (updateInfo.value?.update_available === true && (!isTauriRuntime() || updateInfo.value.manual_update_only))) && !isUpdateIgnored(updateInfo.value, settingsStore.editorSettings.ignoredUpdateVersion),
() =>
notificationsEnabled.value &&
(updateDownloaded.value || updateReady.value || (updateInfo.value?.update_available === true && (!autoDownloadEnabled.value || !isTauriRuntime() || updateInfo.value.manual_update_only))) &&
!isUpdateIgnored(updateInfo.value, settingsStore.editorSettings.ignoredUpdateVersion),
);
const latestReleaseUrl = "https://github.com/t8y2/dbx/releases/latest";
let generation = 0;
let activeDownload: Promise<void> | undefined;
let automaticDownload = false;
let cancellation: Promise<void> | undefined;
let cancelOperation: Promise<void> | undefined;
let retryTimer: ReturnType<typeof setTimeout> | undefined;
@@ -223,9 +228,15 @@ export function useAppUpdater(options: UseAppUpdaterOptions = {}) {
if (!info.update_available) updateCheckMessage.value = t("updates.upToDate", { version: info.current_version });
if (canDownloadAndInstallUpdate(info, isTauriRuntime()) && !isUpdateIgnored(info, settingsStore.editorSettings.ignoredUpdateVersion)) {
const cached = downloaded.value;
if (cached && !isNewerRemoteVersion(info.latest_version, cached.version)) return;
if (cached && !isNewerRemoteVersion(info.latest_version, cached.version)) {
// Keep a prepared package installable without replacing it automatically.
if (!autoDownloadEnabled.value || !notificationsEnabled.value) setDownloaded(cached);
return;
}
// A cached package older than the remote release must not mask the newer version.
if (cached && !(await discardSupersededUpdate(cached))) return;
await downloadUpdateInBackground();
if (!autoDownloadEnabled.value || !notificationsEnabled.value) return;
await downloadUpdateInBackground(true);
}
} catch (error) {
if (token !== generation || disposed) return;
@@ -249,8 +260,10 @@ export function useAppUpdater(options: UseAppUpdaterOptions = {}) {
return false;
}
}
async function downloadUpdateInBackground() {
async function downloadUpdateInBackground(automatic = false) {
if (disposed || isIgnoringUpdate.value || phase.value !== "idle" || downloaded.value || !canDownloadAndInstallUpdate(updateInfo.value, isTauriRuntime())) return;
if (automatic && (!autoDownloadEnabled.value || !notificationsEnabled.value)) return;
automaticDownload = automatic;
const version = updateInfo.value!.latest_version;
const token = ++generation;
const attemptId = crypto.randomUUID();
@@ -292,6 +305,7 @@ export function useAppUpdater(options: UseAppUpdaterOptions = {}) {
activeDownload = run();
await activeDownload;
activeDownload = undefined;
automaticDownload = false;
}
function cancelDownload(): Promise<void> {
if (cancelOperation) return cancelOperation;
@@ -444,11 +458,26 @@ export function useAppUpdater(options: UseAppUpdaterOptions = {}) {
})
.catch(fail);
});
const stopAutoDownloadWatch = watch(autoDownloadEnabled, (enabled) => {
if (disposed) return;
if (!enabled) {
clearRetry();
if (automaticDownload) void cancelDownload().catch(fail);
return;
}
if (!initialized || !notificationsEnabled.value) return;
void (cancelOperation ?? Promise.resolve())
.then(() => {
if (autoDownloadEnabled.value && notificationsEnabled.value && !disposed) void checkUpdates({ silent: true });
})
.catch(fail);
});
function dispose() {
disposed = true;
clearRetry();
clearInterval(hourlyTimer);
stopSettingsWatch();
stopAutoDownloadWatch();
updatePreparationRelease?.();
updatePreparationRelease = undefined;
void cancelDownload().catch(() => {});
+3 -1
View File
@@ -6932,7 +6932,9 @@ export default withEnglishFallback({
closeActionQuit: "DBX-dən çıx",
closeActionMinimize: "Sistem treyinə kiçilt",
updateNotificationsEnabled: "Yeniləmə xatırlatmalarını aktiv et",
updateNotificationsEnabledDescription: "Tətbiq və sürücü yeniləmələrini avtomatik yoxlayır, tətbiq yeniləmələrini səssiz endirir. Söndürüldükdə avtomatik yoxlamalar, endirmələr və nişanlar dayanır. Əl ilə yoxlama və endirilmiş yeniləmələrin quraşdırılması mümkündür.",
updateNotificationsEnabledDescription: "Tətbiq və sürücü yeniləmələrini avtomatik yoxlayır və xatırlatmalar göstərir. Söndürüldükdə avtomatik yoxlamalar, avtomatik endirmələr və nişanlar dayanır. Əl ilə yoxlama mümkündür.",
autoDownloadUpdates: "Tətbiq yeniləmələrini avtomatik endir",
autoDownloadUpdatesDescription: "Xatırlatmalar aktiv olduqda yeniləmə paketlərini avtomatik endirir. Quraşdırma üçün təsdiq tələb olunur. Standart olaraq söndürülüb; əl ilə endirmək mümkündür və mövcud endirmələr saxlanılır.",
updateDownloadSource: "Yeniləmələrin endirmə mənbəyi",
updateDownloadSourceDescription: "Tətbiq yeniləmələrinin, verilənlər bazası agentlərinin, drayverlərin və idarə olunan JRE-lərin haradan endiriləcəyini seçin. Rəsmi mənbə tövsiyə olunur; CNB materik Çin şəbəkələrində daha sürətli ola bilər.",
updateDownloadSourceOfficial: "Rəsmi mənbə (tövsiyə olunur)",
+3 -1
View File
@@ -7121,7 +7121,9 @@ export default {
closeActionQuit: "Quit DBX",
closeActionMinimize: "Minimize to tray",
updateNotificationsEnabled: "Enable update reminders",
updateNotificationsEnabledDescription: "Automatically check for app and driver updates and silently download app updates. When disabled, automatic checks, app downloads and update badges stop. You can still check manually and install downloaded updates.",
updateNotificationsEnabledDescription: "Automatically check for app and driver updates and show reminders. When disabled, automatic checks, automatic downloads, and update badges stop. You can still check manually.",
autoDownloadUpdates: "Automatically download app updates",
autoDownloadUpdatesDescription: "When enabled with update reminders, download update packages automatically. Installation still requires confirmation. Off by default; you can download manually, and existing downloads are kept.",
updateDownloadSource: "Update download source",
updateDownloadSourceDescription: "Choose where app updates, database agents, drivers, and managed JREs are downloaded from. The official source is recommended; CNB can be faster on mainland China networks.",
updateDownloadSourceOfficial: "Official source (recommended)",
+3 -1
View File
@@ -6685,7 +6685,9 @@ export default withEnglishFallback({
closeActionQuit: "Salir de DBX",
closeActionMinimize: "Minimizar a la bandeja",
updateNotificationsEnabled: "Activar recordatorios de actualización",
updateNotificationsEnabledDescription: "Comprueba actualizaciones de la app y los controladores y descarga las de la app en silencio. Al desactivar se detienen las comprobaciones, descargas e indicadores. Puedes comprobar manualmente e instalar actualizaciones descargadas.",
updateNotificationsEnabledDescription: "Comprueba automáticamente las actualizaciones de la app y los controladores y muestra recordatorios. Al desactivar, se detienen las comprobaciones y descargas automáticas y los indicadores. Puedes comprobar manualmente.",
autoDownloadUpdates: "Descargar actualizaciones de la app automáticamente",
autoDownloadUpdatesDescription: "Con los recordatorios activados, descarga los paquetes automáticamente. La instalación requiere confirmación. Desactivado por defecto; puedes descargar manualmente y se conservan las descargas existentes.",
updateDownloadSource: "Origen de descarga de actualizaciones",
updateDownloadSourceDescription: "Elige de dónde se descargan los instaladores de actualización. Se recomienda la fuente oficial; CNB puede ser más rápido en redes de China continental.",
updateDownloadSourceOfficial: "Fuente oficial (recomendada)",
+3 -1
View File
@@ -6685,7 +6685,9 @@ export default withEnglishFallback({
closeActionQuit: "Esci da DBX",
closeActionMinimize: "Riduci a icona",
updateNotificationsEnabled: "Abilita promemoria aggiornamenti",
updateNotificationsEnabledDescription: "Controlla gli aggiornamenti di app e driver e scarica quelli dell’app senza notifiche. Disattivando, controlli automatici, download e indicatori si fermano. Restano disponibili controlli manuali e installazione degli aggiornamenti scaricati.",
updateNotificationsEnabledDescription: "Controlla automaticamente gli aggiornamenti di app e driver e mostra promemoria. Disattivando, si fermano controlli e download automatici e indicatori. Puoi comunque controllare manualmente.",
autoDownloadUpdates: "Scarica automaticamente gli aggiornamenti dell’app",
autoDownloadUpdatesDescription: "Con i promemoria attivi, scarica automaticamente i pacchetti. L’installazione richiede conferma. Disattivato per impostazione predefinita; puoi scaricare manualmente e i download esistenti vengono conservati.",
updateDownloadSource: "Fonte download aggiornamenti",
updateDownloadSourceDescription: "Scegli da dove scaricare gli installer degli aggiornamenti. La fonte ufficiale è consigliata; CNB può essere più veloce sulle reti della Cina continentale.",
updateDownloadSourceOfficial: "Fonte ufficiale (consigliata)",
+3 -1
View File
@@ -6723,7 +6723,9 @@ export default withEnglishFallback({
closeActionQuit: "DBXを終了",
closeActionMinimize: "トレイに最小化",
updateNotificationsEnabled: "アップデート通知を有効にする",
updateNotificationsEnabledDescription: "アプリとドライバーの更新を自動確認し、アプリ更新を通知せずにダウンロードします。無効にすると自動確認、ダウンロード、更新バッジが停止します。手動確認とダウンロード済み更新のインストールは可能です。",
updateNotificationsEnabledDescription: "アプリとドライバーの更新を自動確認し、通知を表示します。無効にすると自動確認、自動ダウンロード、更新バッジが停止します。手動確認は可能です。",
autoDownloadUpdates: "アプリの更新を自動ダウンロード",
autoDownloadUpdatesDescription: "更新通知が有効な場合、更新パッケージを自動ダウンロードします。インストールには確認が必要です。初期設定はオフです。手動ダウンロードは可能で、ダウンロード済みの更新は保持されます。",
debugLoggingEnabled: "デバッグログを有効にする",
debugLoggingEnabledDescription: "トラブルシューティング時に、より詳細なローカル診断ログを記録します。デフォルトでは無効です。",
metadataCacheMemoryLimit: "メタデータメモリキャッシュ上限",
+3 -1
View File
@@ -6437,7 +6437,9 @@ export default withEnglishFallback({
closeActionQuit: "DBX 종료",
closeActionMinimize: "트레이로 최소화",
updateNotificationsEnabled: "업데이트 알림 활성화",
updateNotificationsEnabledDescription: "앱과 드라이버 업데이트를 자동 확인하고 앱 업데이트를 조용히 다운로드합니다. 끄면 자동 확인, 앱 다운로드와 배지가 중지됩니다. 수동 확인 및 다운로드된 업데이트 설치는 가능합니다.",
updateNotificationsEnabledDescription: "앱과 드라이버 업데이트를 자동 확인하고 알림을 표시합니다. 끄면 자동 확인, 자동 다운로드 및 업데이트 배지가 중지됩니다. 수동 확인은 가능합니다.",
autoDownloadUpdates: "앱 업데이트 자동 다운로드",
autoDownloadUpdatesDescription: "업데이트 알림이 켜져 있으면 업데이트 패키지를 자동으로 다운로드합니다. 설치 전에는 확인이 필요합니다. 기본값은 꺼짐입니다. 수동 다운로드가 가능하며 기존 다운로드는 유지됩니다.",
updateDownloadSource: "업데이트 다운로드 소스",
updateDownloadSourceDescription: "앱 업데이트, 데이터베이스 에이전트, 드라이버, 관리 JRE를 다운로드할 위치를 선택하세요. 공식 소스를 권장합니다. 중국 본토 네트워크에서는 CNB가 더 빠를 수 있습니다.",
updateDownloadSourceOfficial: "공식 소스 (권장)",
+3 -1
View File
@@ -6687,7 +6687,9 @@ export default withEnglishFallback({
closeActionQuit: "Sair do DBX",
closeActionMinimize: "Minimizar para a bandeja",
updateNotificationsEnabled: "Ativar lembretes de atualização",
updateNotificationsEnabledDescription: "Verifica atualizações do app e dos drivers e baixa as do app silenciosamente. Ao desativar, as verificações, downloads e indicadores param. Você ainda pode verificar manualmente e instalar atualizações baixadas.",
updateNotificationsEnabledDescription: "Verifica automaticamente atualizações do app e dos drivers e mostra lembretes. Ao desativar, as verificações e os downloads automáticos e os indicadores param. Você ainda pode verificar manualmente.",
autoDownloadUpdates: "Baixar atualizações do app automaticamente",
autoDownloadUpdatesDescription: "Com os lembretes ativados, baixa os pacotes automaticamente. A instalação exige confirmação. Desativado por padrão; você pode baixar manualmente e os downloads existentes são mantidos.",
updateDownloadSource: "Fonte de download de atualização",
updateDownloadSourceDescription: "Escolha de onde os instaladores de atualização no aplicativo são baixados. A fonte oficial é recomendada; CNB pode ser mais rápido em redes da China continental.",
updateDownloadSourceOfficial: "Fonte oficial (recomendada)",
+3 -1
View File
@@ -6814,7 +6814,9 @@ export default withEnglishFallback({
closeActionQuit: "DBX'ten çık",
closeActionMinimize: "Tepsiye küçült",
updateNotificationsEnabled: "Güncelleme anımsatıcılarını etkinleştir",
updateNotificationsEnabledDescription: "Uygulama ve sürücü güncellemelerini otomatik kontrol eder, uygulama güncellemelerini sessizce indirir. Kapatıldığında otomatik kontroller, indirmeler ve rozetler durur. Elle kontrol ve indirilen güncellemeleri yükleme kullanılabilir.",
updateNotificationsEnabledDescription: "Uygulama ve sürücü güncellemelerini otomatik kontrol eder ve anımsatıcıları gösterir. Kapatıldığında otomatik kontroller, otomatik indirmeler ve rozetler durur. Elle kontrol kullanılabilir.",
autoDownloadUpdates: "Uygulama güncellemelerini otomatik indir",
autoDownloadUpdatesDescription: "Anımsatıcılar açıkken güncelleme paketlerini otomatik indirir. Kurulum için onay gerekir. Varsayılan olarak kapalıdır; elle indirme kullanılabilir ve mevcut indirmeler korunur.",
updateDownloadSource: "Güncelleme indirme kaynağı",
updateDownloadSourceDescription: "Uygulama güncellemelerinin, veritabanı agent'larının, sürücülerin ve yönetilen JRE'lerin nereden indirileceğini seçin. Resmî kaynak önerilir; Çin anakarası ağlarında CNB daha hızlı olabilir.",
updateDownloadSourceOfficial: "Resmî kaynak (önerilen)",
+3 -1
View File
@@ -7095,7 +7095,9 @@ export default withEnglishFallback({
closeActionQuit: "退出程序",
closeActionMinimize: "最小化到托盘",
updateNotificationsEnabled: "启用更新提醒",
updateNotificationsEnabledDescription: "自动检查应用和驱动更新,并静默下载应用更新。关闭后停止自动检查、应用下载和更新红点;仍可手动检查并安装已下载的更新。",
updateNotificationsEnabledDescription: "自动检查应用和驱动更新并显示更新提醒。关闭后停止自动检查、自动下载和更新红点;仍可手动检查更新。",
autoDownloadUpdates: "自动下载应用更新",
autoDownloadUpdatesDescription: "启用后,在更新提醒开启时自动下载安装包,安装前仍需确认。默认关闭;关闭后可手动下载,已下载的更新会保留。",
updateDownloadSource: "更新下载源",
updateDownloadSourceDescription: "选择应用更新、数据库 Agent、驱动和托管 JRE 的下载来源。官方源为推荐选项,CNB 适合国内网络环境。",
updateDownloadSourceOfficial: "官方源(推荐)",
+3 -1
View File
@@ -6004,7 +6004,9 @@ export default withEnglishFallback({
closeActionQuit: "退出程式",
closeActionMinimize: "最小化到系統匣",
updateNotificationsEnabled: "啟用更新提醒",
updateNotificationsEnabledDescription: "自動檢查應用程式和驅動程式更新,並靜默下載應用程式更新。關閉後停止自動檢查、應用程式下載和更新紅點;仍可手動檢查並安裝已下載的更新。",
updateNotificationsEnabledDescription: "自動檢查應用程式和驅動程式更新並顯示提醒。關閉後停止自動檢查、自動下載和更新紅點;仍可手動檢查更新。",
autoDownloadUpdates: "自動下載應用程式更新",
autoDownloadUpdatesDescription: "啟用後,在更新提醒開啟時自動下載安裝套件,安裝前仍需確認。預設關閉;關閉後可手動下載,已下載的更新會保留。",
debugLoggingEnabled: "啟用偵錯日誌",
debugLoggingEnabledDescription: "開啟後在本機記錄更詳細的使用者側診斷日誌,回報問題時可暫時開啟;預設關閉。",
metadataCacheMemoryLimit: "中繼資料記憶體快取上限",
@@ -86,6 +86,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
"formatSqlOnSqlFileSave",
"showTableDdlHoverPreview",
"updateNotificationsEnabled",
"autoDownloadUpdates",
"sidebarObjectInfoMode",
"sidebarAllowHorizontalScroll",
"sidebarShowTooltips",
@@ -263,6 +263,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
{ id: "about-support", category: "about", titleKey: "settings.supportInfoTitle", descriptionKey: "settings.supportInfoDescription", targetId: "about" },
{ id: "about-transfer", category: "about", titleKey: "settings.settingsTransferTitle", descriptionKey: "settings.settingsTransferDescription", targetId: "about" },
{ id: "about-update-notifications", category: "about", titleKey: "settings.updateNotificationsEnabled", descriptionKey: "settings.updateNotificationsEnabledDescription", targetId: "about" },
{ id: "about-auto-download-updates", category: "about", titleKey: "settings.autoDownloadUpdates", descriptionKey: "settings.autoDownloadUpdatesDescription", targetId: "about" },
{ id: "about-update", category: "about", titleKey: "settings.updateDownloadSource", descriptionKey: "settings.updateDownloadSourceDescription", targetId: "about" },
];
@@ -171,7 +171,7 @@ const SETTINGS_TRANSFER_CATEGORY_KEYS: Record<SettingsTransferCategoryId, readon
],
shortcuts: ["shortcuts", "sqlShortcuts"],
snippets: ["snippets"],
other: ["updateDownloadSource", "updateNotificationsEnabled"],
other: ["updateDownloadSource", "updateNotificationsEnabled", "autoDownloadUpdates"],
};
const KEY_TO_CATEGORY = new Map<string, SettingsTransferCategoryId>();
@@ -267,6 +267,7 @@ const PASS_THROUGH_BOOLEAN_KEYS = [
"sidebarAllowHorizontalScroll",
"sidebarShowTooltips",
"updateNotificationsEnabled",
"autoDownloadUpdates",
] as const satisfies readonly EditorSettingsDraftKey[];
const PASS_THROUGH_FIELD_VALIDATORS: Partial<Record<EditorSettingsDraftKey, (value: unknown) => boolean>> = {
@@ -273,6 +273,13 @@ describe("normalizeEditorSettings", () => {
expect(normalizeEditorSettings({}).updateDownloadSource).toBe("official");
});
it("requires opting into automatic update downloads and preserves the preference", () => {
expect(normalizeEditorSettings({}).autoDownloadUpdates).toBe(false);
expect(normalizeEditorSettings({ autoDownloadUpdates: true }).autoDownloadUpdates).toBe(true);
expect(normalizeEditorSettings({ autoDownloadUpdates: false }).autoDownloadUpdates).toBe(false);
expect(normalizeEditorSettings({ autoDownloadUpdates: "true" } as any).autoDownloadUpdates).toBe(false);
});
it("preserves explicit editor themes from saved settings", () => {
expect(normalizeEditorSettings({ theme: "xcode" }).theme).toBe("xcode");
expect(normalizeEditorSettings({ theme: "one-dark" }).theme).toBe("one-dark");
+4
View File
@@ -861,6 +861,7 @@ export interface EditorSettings {
generateSqlQuoteIdentifiers: boolean;
formatSqlOnSqlFileSave: boolean;
updateNotificationsEnabled: boolean;
autoDownloadUpdates: boolean;
sidebarHiddenTablePrefixes: string[];
sidebarCopyTableNameSeparator: ColumnNameCopySeparator;
sidebarCopyTableNameIncludeSchema: boolean;
@@ -1102,6 +1103,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
generateSqlQuoteIdentifiers: true,
formatSqlOnSqlFileSave: false,
updateNotificationsEnabled: true,
autoDownloadUpdates: false,
sidebarHiddenTablePrefixes: [],
sidebarCopyTableNameSeparator: "comma",
sidebarCopyTableNameIncludeSchema: false,
@@ -1598,6 +1600,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
generateSqlQuoteIdentifiers: typeof settings.generateSqlQuoteIdentifiers === "boolean" ? settings.generateSqlQuoteIdentifiers : DEFAULT_EDITOR_SETTINGS.generateSqlQuoteIdentifiers,
formatSqlOnSqlFileSave: settings.formatSqlOnSqlFileSave === true,
updateNotificationsEnabled: settings.updateNotificationsEnabled ?? DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled,
autoDownloadUpdates: settings.autoDownloadUpdates === true,
sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(settings.sidebarHiddenTablePrefixes),
sidebarCopyTableNameSeparator: normalizeSidebarCopyTableNameSeparator(settings.sidebarCopyTableNameSeparator),
sidebarCopyTableNameIncludeSchema: settings.sidebarCopyTableNameIncludeSchema === true,
@@ -2327,6 +2330,7 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.generateSqlQuoteIdentifiers !== undefined) editorSettings.value.generateSqlQuoteIdentifiers = partial.generateSqlQuoteIdentifiers === true;
if (partial.formatSqlOnSqlFileSave !== undefined) editorSettings.value.formatSqlOnSqlFileSave = partial.formatSqlOnSqlFileSave === true;
if (partial.updateNotificationsEnabled !== undefined) editorSettings.value.updateNotificationsEnabled = partial.updateNotificationsEnabled;
if (partial.autoDownloadUpdates !== undefined) editorSettings.value.autoDownloadUpdates = partial.autoDownloadUpdates === true;
if (partial.sidebarHiddenTablePrefixes !== undefined) editorSettings.value.sidebarHiddenTablePrefixes = normalizeSidebarHiddenTablePrefixes(partial.sidebarHiddenTablePrefixes);
if (partial.sidebarCopyTableNameSeparator !== undefined) editorSettings.value.sidebarCopyTableNameSeparator = normalizeSidebarCopyTableNameSeparator(partial.sidebarCopyTableNameSeparator);
if (partial.sidebarCopyTableNameIncludeSchema !== undefined) editorSettings.value.sidebarCopyTableNameIncludeSchema = partial.sidebarCopyTableNameIncludeSchema === true;