mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
feat(updates): centralize client and component update management
This commit is contained in:
+161
-42
@@ -24,8 +24,12 @@ import { useSavedSqlStore } from "@/stores/savedSqlStore";
|
||||
import { usePromptTemplateStore } from "@/stores/promptTemplateStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { useAppUpdater } from "@/composables/useAppUpdater";
|
||||
import { canDownloadAndInstallUpdate, useAppUpdater } from "@/composables/useAppUpdater";
|
||||
import { useMcpUpdateBadge } from "@/composables/useMcpUpdateBadge";
|
||||
import { useComponentUpdates, type ComponentUpdateCategory } from "@/composables/useComponentUpdates";
|
||||
import { driverStoreUpdateBadgeCount, showMcpUpdateBadge } from "@/lib/updates/updateBadges";
|
||||
import { markPendingComponentUpdatesAfterAppUpdate, resolveUpdateAllAction, runPendingComponentUpdatePlan, takePendingComponentUpdatesAfterAppRestart, type PendingComponentUpdatePlan } from "@/lib/updates/componentUpdateOrchestration";
|
||||
import { isUpdatePreviewMockEnabled } from "@/lib/updates/updatePreviewMock";
|
||||
import { useExportTracker } from "@/composables/useExportTracker";
|
||||
import { useFileDrop } from "@/composables/useFileDrop";
|
||||
import { useLargeSqlFileStreamingFallback } from "@/composables/useLargeSqlFileFallback";
|
||||
@@ -251,6 +255,7 @@ const { setupFileDrop } = useFileDrop();
|
||||
const { openInStreamingExecutorOnTooLarge } = useLargeSqlFileStreamingFallback();
|
||||
|
||||
const isDesktop = isTauriRuntime();
|
||||
const componentUpdates = useComponentUpdates({ isDesktop: isDesktop || isUpdatePreviewMockEnabled() });
|
||||
const windowContext = resolveWindowContext();
|
||||
const isDetachedWindowContext = windowContext.kind === "detached-tab";
|
||||
const detachedContextTabId = windowContext.kind === "detached-tab" ? windowContext.tabId : undefined;
|
||||
@@ -310,7 +315,9 @@ const activeAiRunCount = computed(() => (isDesktop ? activeDesktopAiRuns().lengt
|
||||
const awaitingAiRunCount = computed(() => (isDesktop ? activeDesktopAiRuns().filter((run) => run.status === "awaiting_write_confirmation").length : 0));
|
||||
const { mcpUpdateAvailable, refreshMcpUpdateStatus, handleMcpStatusChanged } = useMcpUpdateBadge({
|
||||
isDesktop,
|
||||
updateNotificationsEnabled: () => settingsStore.editorSettings.updateNotificationsEnabled,
|
||||
// Update availability remains visible when every auto-update switch is off;
|
||||
// the switches control installation, not whether the user can be reminded.
|
||||
updateNotificationsEnabled: () => true,
|
||||
});
|
||||
const drawDesktopWindowFrame = shouldDrawDesktopWindowFrame(isMacOS(), isDesktop, isWindows());
|
||||
const UPDATE_CHECK_INTERVAL_MS = 60 * 60 * 1000;
|
||||
@@ -747,18 +754,13 @@ const activeConnection = computed(() => {
|
||||
// without inferring Oracle from the raw transport type.
|
||||
|
||||
function updateAgentDriverUpdateCount(count: number) {
|
||||
if (!settingsStore.editorSettings.updateNotificationsEnabled) {
|
||||
agentDriverUpdateCount.value = 0;
|
||||
return;
|
||||
}
|
||||
agentDriverUpdateCount.value = count;
|
||||
}
|
||||
|
||||
async function refreshAgentDriverUpdateCount() {
|
||||
if (!isDesktop || !settingsStore.editorSettings.updateNotificationsEnabled) return;
|
||||
if (!isDesktop) return;
|
||||
try {
|
||||
const drivers = await api.listInstalledAgents();
|
||||
if (!settingsStore.editorSettings.updateNotificationsEnabled) return;
|
||||
updateAgentDriverUpdateCount(countAvailableAgentDriverUpdates(drivers));
|
||||
} catch {
|
||||
// Driver update availability is only a badge hint; keep the existing count if the registry cannot be reached.
|
||||
@@ -879,6 +881,18 @@ function requestActiveEditorExecuteInNewResultTab() {
|
||||
void tryExecuteInNewResultTab();
|
||||
}
|
||||
|
||||
const toolbarAgentDriverUpdateCount = computed(() => Math.max(agentDriverUpdateCount.value, componentUpdates.driverUpdateCount.value));
|
||||
const toolbarDriverUpdateCount = computed(() => toolbarAgentDriverUpdateCount.value);
|
||||
const toolbarJdbcUpdateAvailable = computed(() => componentUpdates.jdbcUpdateAvailable.value);
|
||||
const toolbarMcpUpdateAvailable = computed(() => mcpUpdateAvailable.value || componentUpdates.mcpUpdateAvailable.value);
|
||||
const toolbarPluginUpdateAvailable = computed(() => componentUpdates.pluginUpdateCount.value > 0);
|
||||
const toolbarHasUpdateAvailable = computed(() => hasUpdateAvailable.value || toolbarDriverUpdateCount.value > 0 || toolbarJdbcUpdateAvailable.value || toolbarMcpUpdateAvailable.value || toolbarPluginUpdateAvailable.value);
|
||||
const showDriverStoreUpdateBadge = computed(() => driverStoreUpdateBadgeCount(settingsStore.editorSettings.autoUpdateDrivers, settingsStore.editorSettings.autoUpdateJdbc, toolbarDriverUpdateCount.value, toolbarJdbcUpdateAvailable.value));
|
||||
const showMcpSettingsUpdateBadge = computed(() => showMcpUpdateBadge(settingsStore.editorSettings.autoUpdateMcp, toolbarMcpUpdateAvailable.value));
|
||||
const manualCheckingAllUpdates = ref(false);
|
||||
const checkingAllUpdates = computed(() => manualCheckingAllUpdates.value || checkingUpdates.value || componentUpdates.loading.value);
|
||||
const updatingAllUpdates = ref(false);
|
||||
|
||||
// Per-group editor toolbars call back into this App-owned orchestration. The
|
||||
// group focuses itself on pointerdown/focusin before any toolbar event, so the
|
||||
// acting tab is passed explicitly instead of relying on the focused group.
|
||||
@@ -891,7 +905,7 @@ const specialPageTabs = computed(() => ({
|
||||
pluginCenterActive: pluginCenterActive.value,
|
||||
driverStoreOpen: driverStoreTabOpen.value,
|
||||
driverStoreActive: driverStoreActive.value,
|
||||
driverUpdateCount: toolbarAgentDriverUpdateCount.value,
|
||||
driverUpdateCount: showDriverStoreUpdateBadge.value,
|
||||
}));
|
||||
provide(GROUP_TAB_BAR_PORTAL, createGroupTabBarPortal(computed(() => !isDetachedWindowContext && (driverStoreActive.value || pluginCenterActive.value || settingsStore.settingsPageActive))));
|
||||
provide(EDITOR_TOOLBAR_ACTIONS, {
|
||||
@@ -1034,8 +1048,6 @@ function toggleTabBarCollapsed() {
|
||||
setTabBarCollapsed(!tabBarCollapsed.value);
|
||||
}
|
||||
|
||||
const updateNotificationsEnabled = computed(() => settingsStore.editorSettings.updateNotificationsEnabled);
|
||||
|
||||
function openSettings(initialTab = "appearance", initialSection?: string) {
|
||||
settingsInitialTab.value = initialTab;
|
||||
settingsInitialSection.value = initialSection;
|
||||
@@ -1176,9 +1188,95 @@ function openPluginConnectionDialog(pluginId: string, providerId: string) {
|
||||
connectionPluginProvider.value = { pluginId, providerId };
|
||||
showConnectionDialog.value = true;
|
||||
}
|
||||
const toolbarAgentDriverUpdateCount = computed(() => (updateNotificationsEnabled.value ? agentDriverUpdateCount.value : 0));
|
||||
const toolbarHasUpdateAvailable = computed(() => updateNotificationsEnabled.value && hasUpdateAvailable.value);
|
||||
const toolbarMcpUpdateAvailable = computed(() => updateNotificationsEnabled.value && mcpUpdateAvailable.value);
|
||||
async function checkAllUpdates() {
|
||||
if (manualCheckingAllUpdates.value) return;
|
||||
manualCheckingAllUpdates.value = true;
|
||||
const startedAt = Date.now();
|
||||
try {
|
||||
await Promise.allSettled([checkUpdates({ silent: true }), componentUpdates.refresh()]);
|
||||
const remaining = 500 - (Date.now() - startedAt);
|
||||
if (remaining > 0) await new Promise((resolve) => setTimeout(resolve, remaining));
|
||||
} finally {
|
||||
manualCheckingAllUpdates.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openDriverStoreFromUpdate(target?: DriverStoreTab) {
|
||||
closeSettingsPage();
|
||||
openDriverStorePage(target);
|
||||
}
|
||||
|
||||
function handleToolbarUpdateClick() {
|
||||
showUpdateDialog.value = true;
|
||||
if (!toolbarHasUpdateAvailable.value && !checkingAllUpdates.value) void checkAllUpdates();
|
||||
}
|
||||
|
||||
function reportComponentUpdateResult(result: Awaited<ReturnType<typeof componentUpdates.installCategory>>) {
|
||||
const updatedComponents = [result.drivers > 0 ? t("settings.updateDrivers") : "", result.jdbc ? t("settings.updateJdbc") : "", result.mcp ? t("settings.updateMcp") : "", result.plugins > 0 ? t("settings.updatePlugins") : ""].filter(Boolean);
|
||||
if (updatedComponents.length) toast(t("updates.componentsAutoUpdated", { components: updatedComponents.join(t("updates.componentListSeparator")) }));
|
||||
if (result.skippedDrivers > 0) toast(t("updates.componentsAutoUpdateSkipped"), 6000);
|
||||
if (result.failed.length) toast(t("updates.componentsAutoUpdateFailed", { count: result.failed.length }), 6000);
|
||||
}
|
||||
|
||||
async function rememberComponentUpdatesForRestartedApp(plan: PendingComponentUpdatePlan = { kind: "auto" }) {
|
||||
const fromVersion = appVersion.value || updateInfo.value?.current_version || (await api.getAppVersion().catch(() => ""));
|
||||
return markPendingComponentUpdatesAfterAppUpdate(fromVersion, updateInfo.value?.latest_version || "", plan);
|
||||
}
|
||||
|
||||
async function consumePendingComponentUpdatesAfterRestart() {
|
||||
const currentVersion = await api.getAppVersion().catch(() => "");
|
||||
if (currentVersion && !appVersion.value) appVersion.value = currentVersion;
|
||||
const pending = takePendingComponentUpdatesAfterAppRestart(currentVersion);
|
||||
if (!pending) return;
|
||||
reportComponentUpdateResult(await runPendingComponentUpdatePlan(pending, componentUpdates));
|
||||
}
|
||||
|
||||
async function installDownloadedUpdateWithComponentUpdates() {
|
||||
await rememberComponentUpdatesForRestartedApp();
|
||||
await installDownloadedUpdate();
|
||||
}
|
||||
|
||||
async function restartAppWithComponentUpdates() {
|
||||
await rememberComponentUpdatesForRestartedApp();
|
||||
await restartApp();
|
||||
}
|
||||
|
||||
function installComponentUpdates(category: ComponentUpdateCategory) {
|
||||
return componentUpdates.installCategory(category).then(reportComponentUpdateResult);
|
||||
}
|
||||
|
||||
function availableComponentUpdateCategories(): ComponentUpdateCategory[] {
|
||||
const categories: ComponentUpdateCategory[] = [];
|
||||
if (toolbarDriverUpdateCount.value > 0) categories.push("drivers");
|
||||
if (toolbarJdbcUpdateAvailable.value) categories.push("jdbc");
|
||||
if (toolbarMcpUpdateAvailable.value) categories.push("mcp");
|
||||
if (componentUpdates.pluginUpdateCount.value > 0) categories.push("plugins");
|
||||
return categories;
|
||||
}
|
||||
|
||||
async function updateAllAvailable() {
|
||||
if (updatingAllUpdates.value) return;
|
||||
updatingAllUpdates.value = true;
|
||||
showUpdateDialog.value = true;
|
||||
try {
|
||||
const categories = availableComponentUpdateCategories();
|
||||
const action = resolveUpdateAllAction({
|
||||
hasAppUpdate: hasUpdateAvailable.value && isDesktop,
|
||||
appUpdateCanInstall: canDownloadAndInstallUpdate(updateInfo.value, isDesktop),
|
||||
appUpdatePrepared: updateDownloaded.value || updateReady.value,
|
||||
hasComponentUpdates: categories.length > 0,
|
||||
});
|
||||
if (action === "none") return;
|
||||
if (action === "update-components") {
|
||||
reportComponentUpdateResult(await componentUpdates.installCategories(categories));
|
||||
return;
|
||||
}
|
||||
if (action === "download-app") await downloadUpdateInBackground();
|
||||
if (updateDownloaded.value || updateReady.value) await rememberComponentUpdatesForRestartedApp({ kind: "manual", categories });
|
||||
} finally {
|
||||
updatingAllUpdates.value = false;
|
||||
}
|
||||
}
|
||||
const hasSqlFileConnections = computed(() => connectionStore.connections.some((c) => supportsSqlFileExecution(c.db_type)));
|
||||
const queryEditorDdlDatabaseType = computed(() => {
|
||||
if (!queryEditorDdlTarget.value?.connectionId) return undefined;
|
||||
@@ -3542,6 +3640,9 @@ async function initApp() {
|
||||
updateWindowReady = true;
|
||||
await initializeUpdatePreparation();
|
||||
await initializeUpdater();
|
||||
if (!isDetachedWindowContext) {
|
||||
void consumePendingComponentUpdatesAfterRestart();
|
||||
}
|
||||
}
|
||||
|
||||
void promptTemplateStore.init();
|
||||
@@ -3607,27 +3708,11 @@ function openDriverStoreFromEvent(event: Event) {
|
||||
}
|
||||
|
||||
function runUpdateNotificationChecks() {
|
||||
if (!updateNotificationsEnabled.value) return;
|
||||
void refreshAgentDriverUpdateCount();
|
||||
void refreshMcpUpdateStatus();
|
||||
void componentUpdates.refresh();
|
||||
}
|
||||
|
||||
watch(updateNotificationsEnabled, (enabled) => {
|
||||
if (!enabled) {
|
||||
agentDriverUpdateCount.value = 0;
|
||||
mcpUpdateAvailable.value = false;
|
||||
if (updateCheckTimer) {
|
||||
clearInterval(updateCheckTimer);
|
||||
updateCheckTimer = undefined;
|
||||
}
|
||||
return;
|
||||
}
|
||||
runUpdateNotificationChecks();
|
||||
if (!updateCheckTimer) {
|
||||
updateCheckTimer = setInterval(runUpdateNotificationChecks, UPDATE_CHECK_INTERVAL_MS);
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
console.log("[STARTUP] onMounted begin");
|
||||
const mountStart = performance.now();
|
||||
@@ -3702,7 +3787,7 @@ onMounted(async () => {
|
||||
setupFileDrop().catch(() => {});
|
||||
setTimeout(() => {
|
||||
runUpdateNotificationChecks();
|
||||
if (updateNotificationsEnabled.value && !updateCheckTimer) {
|
||||
if (!updateCheckTimer) {
|
||||
updateCheckTimer = setInterval(runUpdateNotificationChecks, UPDATE_CHECK_INTERVAL_MS);
|
||||
}
|
||||
}, 10_000);
|
||||
@@ -3774,15 +3859,15 @@ onUnmounted(() => {
|
||||
:show-driver-store="showDriverStore"
|
||||
:show-plugin-center="showPluginCenter"
|
||||
:show-settings-page="showSettingsPage"
|
||||
:checking-updates="checkingUpdates"
|
||||
:checking-updates="checkingAllUpdates"
|
||||
:has-update-available="toolbarHasUpdateAvailable"
|
||||
:is-downloading-update="isDownloadingUpdate"
|
||||
:download-progress="downloadProgress"
|
||||
:update-version="updateInfo?.latest_version"
|
||||
:update-ready-to-install="updateDownloaded"
|
||||
:update-ready="updateReady"
|
||||
:agent-driver-update-count="toolbarAgentDriverUpdateCount"
|
||||
:has-mcp-update-available="toolbarMcpUpdateAvailable"
|
||||
:agent-driver-update-count="showDriverStoreUpdateBadge"
|
||||
:has-mcp-update-available="showMcpSettingsUpdateBadge"
|
||||
:has-connections="connectionStore.connections.length > 0"
|
||||
:has-sql-file-connections="hasSqlFileConnections"
|
||||
@new-connection="showConnectionDialog = true"
|
||||
@@ -3794,10 +3879,10 @@ onUnmounted(() => {
|
||||
@toggle-sql-library="toggleRightSidebarPanel('sqlLibrary')"
|
||||
@toggle-sql-file-panel="toggleRightSidebarPanel('sqlFile')"
|
||||
@open-github="openGitHub"
|
||||
@open-settings="openSettings(toolbarMcpUpdateAvailable ? 'mcp' : 'appearance')"
|
||||
@open-settings="openSettings(showMcpSettingsUpdateBadge ? 'mcp' : 'appearance')"
|
||||
@open-driver-store="openDriverStorePage"
|
||||
@open-plugin-center="openPluginCenterPage()"
|
||||
@check-updates="checkUpdates()"
|
||||
@check-updates="handleToolbarUpdateClick"
|
||||
@open-transfer="dialogs.showTransferDialog.value = true"
|
||||
@open-sql-file="dialogs.showSqlFileDialog.value = true"
|
||||
@open-schema-diff="dialogs.showSchemaDiffDialog.value = true"
|
||||
@@ -3850,7 +3935,15 @@ onUnmounted(() => {
|
||||
@cancel-tab-close="cancelPendingAppClose"
|
||||
@detach-tab="detachTab"
|
||||
>
|
||||
<DriverStorePage v-if="driverStoreTabOpen" v-show="driverStoreActive" v-model:active-tab="driverStoreActiveTab" class="flex-1 min-h-0" :update-notifications-enabled="updateNotificationsEnabled" :focus-target="driverStoreFocus" @update-count-change="updateAgentDriverUpdateCount" />
|
||||
<DriverStorePage
|
||||
v-if="driverStoreTabOpen"
|
||||
v-show="driverStoreActive"
|
||||
v-model:active-tab="driverStoreActiveTab"
|
||||
class="flex-1 min-h-0"
|
||||
:update-notifications-enabled="settingsStore.editorSettings.autoUpdateDrivers"
|
||||
:focus-target="driverStoreFocus"
|
||||
@update-count-change="updateAgentDriverUpdateCount"
|
||||
/>
|
||||
<PluginCenterPage v-if="pluginCenterTabOpen" v-show="pluginCenterActive" class="flex-1 min-h-0" :focus-target="pluginCenterFocus" :install-url-request="pluginCenterInstallRequest" @new-connection="openPluginConnectionDialog" @plugin-runtime-replaced="refreshPluginWorkbenches" />
|
||||
<EditorSettingsPage
|
||||
v-if="settingsPageTabOpen"
|
||||
@@ -3863,10 +3956,25 @@ onUnmounted(() => {
|
||||
:ai-config-draft="settingsAiConfigDraft"
|
||||
:ai-config-request-id="settingsAiConfigRequestId"
|
||||
:app-version="appVersion"
|
||||
:checking-updates="checkingUpdates"
|
||||
:checking-updates="checkingAllUpdates"
|
||||
:updating-all-updates="updatingAllUpdates || componentUpdates.updating.value"
|
||||
:app-update-available="hasUpdateAvailable"
|
||||
:app-update-version="updateInfo?.latest_version"
|
||||
:driver-update-count="toolbarDriverUpdateCount"
|
||||
:jdbc-update-available="toolbarJdbcUpdateAvailable"
|
||||
:mcp-update-available="toolbarMcpUpdateAvailable"
|
||||
:plugin-update-count="componentUpdates.pluginUpdateCount.value"
|
||||
class="flex-1 min-h-0"
|
||||
@update:open="(open: boolean) => (open ? activateSettingsPage() : closeSettingsPage())"
|
||||
@check-updates="checkUpdates()"
|
||||
@check-updates="checkAllUpdates"
|
||||
@update-all="updateAllAvailable"
|
||||
@open-update-center="handleToolbarUpdateClick"
|
||||
@open-driver-store="openDriverStoreFromUpdate"
|
||||
@open-plugin-center="
|
||||
closeSettingsPage();
|
||||
openPluginCenterPage();
|
||||
"
|
||||
@open-mcp-settings="openSettings('mcp')"
|
||||
@ai-config-deep-link-handled="settingsAiConfigDraft = null"
|
||||
/>
|
||||
</AppTabBar>
|
||||
@@ -4204,13 +4312,24 @@ onUnmounted(() => {
|
||||
:update-ready="updateReady"
|
||||
:is-ignoring-update="isIgnoringUpdate"
|
||||
:active-task-count="activeUpdateTaskCount"
|
||||
:driver-updates="componentUpdates.driverUpdates.value"
|
||||
:jdbc-update="componentUpdates.jdbcPluginStatus.value"
|
||||
:mcp-update="componentUpdates.mcpStatus.value"
|
||||
:plugin-updates="componentUpdates.pluginUpdates.value"
|
||||
:component-updates-loading="componentUpdates.loading.value"
|
||||
:component-updates-error="componentUpdates.lastError.value"
|
||||
:component-updates-updating="componentUpdates.updating.value"
|
||||
:updating-component="componentUpdates.updatingCategory.value"
|
||||
:is-updating-all="updatingAllUpdates || componentUpdates.updating.value"
|
||||
@open-latest-release="openLatestRelease"
|
||||
@change-download-source="changeUpdateDownloadSource"
|
||||
@download-in-background="downloadUpdateInBackground"
|
||||
@cancel-download="cancelDownload"
|
||||
@install-downloaded="installDownloadedUpdate"
|
||||
@restart="restartApp"
|
||||
@install-downloaded="installDownloadedUpdateWithComponentUpdates"
|
||||
@restart="restartAppWithComponentUpdates"
|
||||
@ignore-version="ignoreCurrentVersion"
|
||||
@install-component-updates="installComponentUpdates"
|
||||
@update-all="updateAllAvailable"
|
||||
/>
|
||||
<ExternalSqlFileChangeDialog :prompt="externalSqlFilePrompt" @decide="externalSqlFileChanges.resolvePrompt" />
|
||||
<CloseActionPromptDialog v-if="isDesktop && showCloseActionPrompt" :open="showCloseActionPrompt" @update:open="handleCloseActionPromptOpenChange" @quit="chooseQuit" @minimize="chooseMinimize" />
|
||||
|
||||
@@ -6196,7 +6196,6 @@ async function loadSshConfigHosts() {
|
||||
async function loadAgentDrivers() {
|
||||
try {
|
||||
agentDrivers.value = await api.listInstalledAgentsLocal();
|
||||
if (!settingsStore.editorSettings.updateNotificationsEnabled) return;
|
||||
api
|
||||
.listInstalledAgents()
|
||||
.then((drivers) => {
|
||||
|
||||
@@ -113,6 +113,7 @@ import { EDITOR_FONT_FAMILY_CSS_VAR, EDITOR_FONT_SIZE_CSS_VAR, createRunStatemen
|
||||
import { orderAiConfigsForDisplay } from "@/lib/ai/aiConfigOrdering";
|
||||
import { isAiConnectionTestConfigCurrent } from "@/lib/ai/aiConnectionTest";
|
||||
import { MAX_AGENT_TURNS_DEFAULT, MAX_AGENT_TURNS_MAX, MAX_AGENT_TURNS_MIN, maxAgentTurnsOutOfRange, normalizeMaxAgentTurns } from "@/lib/ai/maxAgentTurns";
|
||||
import type { DriverStoreTab } from "@/lib/connection/agentDriverInstallHint";
|
||||
import ThemeCustomizerDialog from "./ThemeCustomizerDialog.vue";
|
||||
import DataGridTypeColorSchemeDialog from "@/components/grid/DataGridTypeColorSchemeDialog.vue";
|
||||
import { DATA_GRID_TYPE_COLOR_SCHEME_AUTO_ID, cloneDataGridTypeColorSchemes, type DataGridTypeColorScheme } from "@/lib/dataGrid/dataGridTypeColorScheme";
|
||||
@@ -383,14 +384,60 @@ const props = defineProps<{
|
||||
aiConfigRequestId?: number;
|
||||
appVersion?: string;
|
||||
checkingUpdates?: boolean;
|
||||
updatingAllUpdates?: boolean;
|
||||
appUpdateAvailable?: boolean;
|
||||
appUpdateVersion?: string;
|
||||
driverUpdateCount?: number;
|
||||
jdbcUpdateAvailable?: boolean;
|
||||
mcpUpdateAvailable?: boolean;
|
||||
pluginUpdateCount?: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:open": [value: boolean];
|
||||
"check-updates": [];
|
||||
"update-all": [];
|
||||
"open-driver-store": [target?: DriverStoreTab];
|
||||
"open-plugin-center": [];
|
||||
"open-mcp-settings": [];
|
||||
"open-update-center": [];
|
||||
"ai-config-deep-link-handled": [];
|
||||
}>();
|
||||
|
||||
const hasAnyUpdate = computed(() => Boolean(props.appUpdateAvailable || (props.driverUpdateCount || 0) > 0 || props.jdbcUpdateAvailable || props.mcpUpdateAvailable || (props.pluginUpdateCount || 0) > 0));
|
||||
const updateCheckItemKeys = ["app", "drivers", "jdbc", "mcp", "plugins"] as const;
|
||||
type UpdateCheckItem = (typeof updateCheckItemKeys)[number];
|
||||
const updateCheckLoading = ref<Record<UpdateCheckItem, boolean>>({ app: false, drivers: false, jdbc: false, mcp: false, plugins: false });
|
||||
let updateCheckRevealTimers: ReturnType<typeof setTimeout>[] = [];
|
||||
let updateCheckWasActive = false;
|
||||
|
||||
watch(
|
||||
() => props.checkingUpdates,
|
||||
(checking) => {
|
||||
for (const timer of updateCheckRevealTimers) clearTimeout(timer);
|
||||
updateCheckRevealTimers = [];
|
||||
if (checking) {
|
||||
updateCheckWasActive = true;
|
||||
for (const key of updateCheckItemKeys) updateCheckLoading.value[key] = true;
|
||||
return;
|
||||
}
|
||||
if (!updateCheckWasActive) return;
|
||||
updateCheckWasActive = false;
|
||||
updateCheckItemKeys.forEach((key, index) => {
|
||||
updateCheckRevealTimers.push(
|
||||
setTimeout(() => {
|
||||
updateCheckLoading.value[key] = false;
|
||||
}, index * 120),
|
||||
);
|
||||
});
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
for (const timer of updateCheckRevealTimers) clearTimeout(timer);
|
||||
});
|
||||
|
||||
const isSettingsPage = computed(() => props.variant === "page");
|
||||
const settingsVisible = computed(() => isSettingsPage.value || props.open === true);
|
||||
const settingsRootComponent = computed(() => (isSettingsPage.value ? "div" : Dialog));
|
||||
@@ -688,8 +735,11 @@ const tableHoverLookupModeDescription = computed(() => {
|
||||
return t(key);
|
||||
});
|
||||
const editClickTableNavigationTarget = ref<ClickTableNavigationTarget>(settingsStore.editorSettings.clickTableNavigationTarget);
|
||||
const editUpdateNotificationsEnabled = ref(settingsStore.editorSettings.updateNotificationsEnabled);
|
||||
const editAutoDownloadUpdates = ref(settingsStore.editorSettings.autoDownloadUpdates);
|
||||
const editAutoUpdateApp = ref(settingsStore.editorSettings.autoUpdateApp);
|
||||
const editAutoUpdateDrivers = ref(settingsStore.editorSettings.autoUpdateDrivers);
|
||||
const editAutoUpdateJdbc = ref(settingsStore.editorSettings.autoUpdateJdbc);
|
||||
const editAutoUpdateMcp = ref(settingsStore.editorSettings.autoUpdateMcp);
|
||||
const editAutoUpdatePlugins = ref(settingsStore.editorSettings.autoUpdatePlugins);
|
||||
const editSidebarHiddenTablePrefixes = ref(settingsStore.editorSettings.sidebarHiddenTablePrefixes.join("\n"));
|
||||
const editSidebarCopyTableNameSeparator = ref<ColumnNameCopySeparator>(settingsStore.editorSettings.sidebarCopyTableNameSeparator);
|
||||
const editSidebarCopyTableNameIncludeSchema = ref(settingsStore.editorSettings.sidebarCopyTableNameIncludeSchema);
|
||||
@@ -920,8 +970,13 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
|
||||
formatSqlOnSqlFileSave: editFormatSqlOnSqlFileSave.value,
|
||||
showTableDdlHoverPreview: editShowTableDdlHoverPreview.value,
|
||||
tableHoverLookupMode: editTableHoverLookupMode.value,
|
||||
updateNotificationsEnabled: editUpdateNotificationsEnabled.value,
|
||||
autoDownloadUpdates: editAutoDownloadUpdates.value,
|
||||
updateNotificationsEnabled: editAutoUpdateApp.value,
|
||||
autoDownloadUpdates: editAutoUpdateApp.value,
|
||||
autoUpdateApp: editAutoUpdateApp.value,
|
||||
autoUpdateDrivers: editAutoUpdateDrivers.value,
|
||||
autoUpdateJdbc: editAutoUpdateJdbc.value,
|
||||
autoUpdateMcp: editAutoUpdateMcp.value,
|
||||
autoUpdatePlugins: editAutoUpdatePlugins.value,
|
||||
sidebarObjectInfoMode: editSidebarObjectInfoMode.value,
|
||||
sidebarAllowHorizontalScroll: editSidebarAllowHorizontalScroll.value,
|
||||
sidebarShowTooltips: editSidebarShowTooltips.value,
|
||||
@@ -1555,8 +1610,11 @@ function syncEditorSettingsDraftFromStore() {
|
||||
editShowTableDdlHoverPreview.value = settingsStore.editorSettings.showTableDdlHoverPreview;
|
||||
editTableHoverLookupMode.value = settingsStore.editorSettings.tableHoverLookupMode;
|
||||
editClickTableNavigationTarget.value = settingsStore.editorSettings.clickTableNavigationTarget;
|
||||
editUpdateNotificationsEnabled.value = settingsStore.editorSettings.updateNotificationsEnabled;
|
||||
editAutoDownloadUpdates.value = settingsStore.editorSettings.autoDownloadUpdates;
|
||||
editAutoUpdateApp.value = settingsStore.editorSettings.autoUpdateApp;
|
||||
editAutoUpdateDrivers.value = settingsStore.editorSettings.autoUpdateDrivers;
|
||||
editAutoUpdateJdbc.value = settingsStore.editorSettings.autoUpdateJdbc;
|
||||
editAutoUpdateMcp.value = settingsStore.editorSettings.autoUpdateMcp;
|
||||
editAutoUpdatePlugins.value = settingsStore.editorSettings.autoUpdatePlugins;
|
||||
editSidebarHiddenTablePrefixes.value = settingsStore.editorSettings.sidebarHiddenTablePrefixes.join("\n");
|
||||
editSidebarCopyTableNameSeparator.value = settingsStore.editorSettings.sidebarCopyTableNameSeparator;
|
||||
editSidebarCopyTableNameIncludeSchema.value = settingsStore.editorSettings.sidebarCopyTableNameIncludeSchema;
|
||||
@@ -1676,8 +1734,13 @@ const editorSettingsDraftRefs: EditorSettingsDraftRefMap = {
|
||||
formatSqlOnSqlFileSave: editFormatSqlOnSqlFileSave,
|
||||
showTableDdlHoverPreview: editShowTableDdlHoverPreview,
|
||||
tableHoverLookupMode: editTableHoverLookupMode,
|
||||
updateNotificationsEnabled: editUpdateNotificationsEnabled,
|
||||
autoDownloadUpdates: editAutoDownloadUpdates,
|
||||
updateNotificationsEnabled: editAutoUpdateApp,
|
||||
autoDownloadUpdates: editAutoUpdateApp,
|
||||
autoUpdateApp: editAutoUpdateApp,
|
||||
autoUpdateDrivers: editAutoUpdateDrivers,
|
||||
autoUpdateJdbc: editAutoUpdateJdbc,
|
||||
autoUpdateMcp: editAutoUpdateMcp,
|
||||
autoUpdatePlugins: editAutoUpdatePlugins,
|
||||
sidebarObjectInfoMode: editSidebarObjectInfoMode,
|
||||
sidebarAllowHorizontalScroll: editSidebarAllowHorizontalScroll,
|
||||
sidebarShowTooltips: editSidebarShowTooltips,
|
||||
@@ -2164,10 +2227,13 @@ function resetDefaultsForTab(tab: SettingsCategory) {
|
||||
editShortcuts.value = normalizeShortcutSettings(DEFAULT_EDITOR_SETTINGS.shortcuts);
|
||||
} else if (tab === "snippets") {
|
||||
editSnippets.value = DEFAULT_SQL_SNIPPETS.map((s) => ({ ...s }));
|
||||
} else if (tab === "about") {
|
||||
} else if (tab === "updates") {
|
||||
editAutoUpdateApp.value = DEFAULT_EDITOR_SETTINGS.autoUpdateApp;
|
||||
editAutoUpdateDrivers.value = DEFAULT_EDITOR_SETTINGS.autoUpdateDrivers;
|
||||
editAutoUpdateJdbc.value = DEFAULT_EDITOR_SETTINGS.autoUpdateJdbc;
|
||||
editAutoUpdateMcp.value = DEFAULT_EDITOR_SETTINGS.autoUpdateMcp;
|
||||
editAutoUpdatePlugins.value = DEFAULT_EDITOR_SETTINGS.autoUpdatePlugins;
|
||||
editUpdateDownloadSource.value = DEFAULT_EDITOR_SETTINGS.updateDownloadSource;
|
||||
editUpdateNotificationsEnabled.value = DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled;
|
||||
editAutoDownloadUpdates.value = DEFAULT_EDITOR_SETTINGS.autoDownloadUpdates;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2266,8 +2332,11 @@ function resetAllDefaults() {
|
||||
editFormatSqlOnSqlFileSave.value = DEFAULT_EDITOR_SETTINGS.formatSqlOnSqlFileSave;
|
||||
editShowTableDdlHoverPreview.value = DEFAULT_EDITOR_SETTINGS.showTableDdlHoverPreview;
|
||||
editTableHoverLookupMode.value = DEFAULT_EDITOR_SETTINGS.tableHoverLookupMode;
|
||||
editUpdateNotificationsEnabled.value = DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled;
|
||||
editAutoDownloadUpdates.value = DEFAULT_EDITOR_SETTINGS.autoDownloadUpdates;
|
||||
editAutoUpdateApp.value = DEFAULT_EDITOR_SETTINGS.autoUpdateApp;
|
||||
editAutoUpdateDrivers.value = DEFAULT_EDITOR_SETTINGS.autoUpdateDrivers;
|
||||
editAutoUpdateJdbc.value = DEFAULT_EDITOR_SETTINGS.autoUpdateJdbc;
|
||||
editAutoUpdateMcp.value = DEFAULT_EDITOR_SETTINGS.autoUpdateMcp;
|
||||
editAutoUpdatePlugins.value = DEFAULT_EDITOR_SETTINGS.autoUpdatePlugins;
|
||||
editSidebarObjectInfoMode.value = DEFAULT_EDITOR_SETTINGS.sidebarObjectInfoMode;
|
||||
editSidebarAllowHorizontalScroll.value = DEFAULT_EDITOR_SETTINGS.sidebarAllowHorizontalScroll;
|
||||
editSidebarShowTooltips.value = DEFAULT_EDITOR_SETTINGS.sidebarShowTooltips;
|
||||
@@ -2689,10 +2758,11 @@ const settingsCategoryNav = computed<{ value: SettingsCategory; label: string }[
|
||||
{ value: "sync", label: t("settings.syncTab") },
|
||||
{ value: "ai", label: t("settings.aiTab") },
|
||||
{ value: "mcp" as const, label: t("settings.mcpTab") },
|
||||
{ value: "updates" as const, label: t("settings.updatesTab") },
|
||||
...(isWeb ? [{ value: "security" as const, label: t("settings.securityTab") }] : []),
|
||||
{ value: "about", label: t("settings.aboutTab") },
|
||||
]);
|
||||
const settingsTabsWithApplyFooter = new Set<SettingsCategory>(["editor", "formatter", "appearance", "navigation", "data", "shortcuts", "snippets"]);
|
||||
const settingsTabsWithApplyFooter = new Set<SettingsCategory>(["editor", "formatter", "appearance", "navigation", "data", "shortcuts", "snippets", "updates"]);
|
||||
|
||||
function hasSettingsApplyFooter(value: SettingsCategory): boolean {
|
||||
return settingsTabsWithApplyFooter.has(value);
|
||||
@@ -9860,6 +9930,111 @@ LIMIT 100;</pre
|
||||
</Tabs>
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeSettingsTab === 'updates'" data-settings-search-id="updates" :class="['flex flex-col gap-5 py-2', settingsSearchTargetClass('updates')]">
|
||||
<div class="space-y-3 rounded-lg border bg-muted/20 p-4">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<Label>{{ t("settings.updateStatusTitle") }}</Label>
|
||||
<p class="text-sm text-muted-foreground">{{ t("settings.updateStatusDescription") }}</p>
|
||||
</div>
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<Button type="button" variant="outline" size="sm" class="h-8 min-w-20" :disabled="props.checkingUpdates || props.updatingAllUpdates" :aria-label="t('settings.checkUpdates')" @click="emit('check-updates')">
|
||||
<Loader2 v-if="props.checkingUpdates" class="h-3.5 w-3.5 animate-spin" />
|
||||
<span v-else>{{ t("settings.checkUpdates") }}</span>
|
||||
</Button>
|
||||
<Button v-if="hasAnyUpdate" type="button" size="sm" class="h-8" :disabled="props.checkingUpdates || props.updatingAllUpdates" @click="emit('update-all')">
|
||||
<Loader2 v-if="props.updatingAllUpdates" class="h-3.5 w-3.5 animate-spin" />
|
||||
<RefreshCw v-else class="h-3.5 w-3.5" />
|
||||
{{ t("settings.updateAll") }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-2">
|
||||
<div class="flex items-center justify-between gap-3 rounded-md border bg-background px-3 py-2.5 text-sm">
|
||||
<button type="button" class="min-w-0 space-y-0.5 rounded-sm text-left transition-colors hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50" @click="emit('open-update-center')">
|
||||
<div class="font-medium">{{ t("settings.updateClient") }}</div>
|
||||
<div class="flex items-center gap-1.5" :class="updateCheckLoading.app ? 'text-muted-foreground' : props.appUpdateAvailable ? 'text-primary' : 'text-muted-foreground'">
|
||||
<Loader2 v-if="updateCheckLoading.app" class="h-3 w-3 animate-spin" />
|
||||
<span>{{ updateCheckLoading.app ? t("updates.checking") : props.appUpdateAvailable ? props.appUpdateVersion || t("settings.updateAvailable") : t("settings.upToDate") }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<Label for="auto-update-app" class="text-xs font-normal text-muted-foreground">{{ t("settings.autoUpdate") }}</Label>
|
||||
<Switch id="auto-update-app" v-model="editAutoUpdateApp" :aria-label="t('settings.autoUpdateApp')" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-3 rounded-md border bg-background px-3 py-2.5 text-sm">
|
||||
<button type="button" class="min-w-0 space-y-0.5 rounded-sm text-left transition-colors hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50" @click="emit('open-driver-store', 'agent')">
|
||||
<div class="font-medium">{{ t("settings.updateDrivers") }}</div>
|
||||
<div class="flex items-center gap-1.5" :class="updateCheckLoading.drivers ? 'text-muted-foreground' : (props.driverUpdateCount || 0) > 0 ? 'text-primary' : 'text-muted-foreground'">
|
||||
<Loader2 v-if="updateCheckLoading.drivers" class="h-3 w-3 animate-spin" />
|
||||
<span>{{ updateCheckLoading.drivers ? t("updates.checking") : (props.driverUpdateCount || 0) > 0 ? t("settings.updateCount", { count: props.driverUpdateCount }) : t("settings.upToDate") }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<Label for="auto-update-drivers" class="text-xs font-normal text-muted-foreground">{{ t("settings.autoUpdate") }}</Label>
|
||||
<Switch id="auto-update-drivers" v-model="editAutoUpdateDrivers" :aria-label="t('settings.autoUpdateDrivers')" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-3 rounded-md border bg-background px-3 py-2.5 text-sm">
|
||||
<button type="button" class="min-w-0 space-y-0.5 rounded-sm text-left transition-colors hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50" @click="emit('open-driver-store', 'jdbc')">
|
||||
<div class="font-medium">{{ t("settings.updateJdbc") }}</div>
|
||||
<div class="flex items-center gap-1.5" :class="updateCheckLoading.jdbc ? 'text-muted-foreground' : props.jdbcUpdateAvailable ? 'text-primary' : 'text-muted-foreground'">
|
||||
<Loader2 v-if="updateCheckLoading.jdbc" class="h-3 w-3 animate-spin" />
|
||||
<span>{{ updateCheckLoading.jdbc ? t("updates.checking") : props.jdbcUpdateAvailable ? t("settings.updateAvailable") : t("settings.upToDate") }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<Label for="auto-update-jdbc" class="text-xs font-normal text-muted-foreground">{{ t("settings.autoUpdate") }}</Label>
|
||||
<Switch id="auto-update-jdbc" v-model="editAutoUpdateJdbc" :aria-label="t('settings.autoUpdateJdbc')" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-3 rounded-md border bg-background px-3 py-2.5 text-sm">
|
||||
<button type="button" class="min-w-0 space-y-0.5 rounded-sm text-left transition-colors hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50" @click="emit('open-mcp-settings')">
|
||||
<div class="font-medium">{{ t("settings.updateMcp") }}</div>
|
||||
<div class="flex items-center gap-1.5" :class="updateCheckLoading.mcp ? 'text-muted-foreground' : props.mcpUpdateAvailable ? 'text-primary' : 'text-muted-foreground'">
|
||||
<Loader2 v-if="updateCheckLoading.mcp" class="h-3 w-3 animate-spin" />
|
||||
<span>{{ updateCheckLoading.mcp ? t("updates.checking") : props.mcpUpdateAvailable ? t("settings.updateAvailable") : t("settings.upToDate") }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<Label for="auto-update-mcp" class="text-xs font-normal text-muted-foreground">{{ t("settings.autoUpdate") }}</Label>
|
||||
<Switch id="auto-update-mcp" v-model="editAutoUpdateMcp" :aria-label="t('settings.autoUpdateMcp')" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-3 rounded-md border bg-background px-3 py-2.5 text-sm">
|
||||
<button type="button" class="min-w-0 space-y-0.5 rounded-sm text-left transition-colors hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50" @click="emit('open-plugin-center')">
|
||||
<div class="font-medium">{{ t("settings.updatePlugins") }}</div>
|
||||
<div class="flex items-center gap-1.5" :class="updateCheckLoading.plugins ? 'text-muted-foreground' : (props.pluginUpdateCount || 0) > 0 ? 'text-primary' : 'text-muted-foreground'">
|
||||
<Loader2 v-if="updateCheckLoading.plugins" class="h-3 w-3 animate-spin" />
|
||||
<span>{{ updateCheckLoading.plugins ? t("updates.checking") : (props.pluginUpdateCount || 0) > 0 ? t("settings.updateCount", { count: props.pluginUpdateCount }) : t("settings.upToDate") }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<Label for="auto-update-plugins" class="text-xs font-normal text-muted-foreground">{{ t("settings.autoUpdate") }}</Label>
|
||||
<Switch id="auto-update-plugins" v-model="editAutoUpdatePlugins" :aria-label="t('settings.autoUpdatePlugins')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="border-t pt-3 text-xs text-muted-foreground">{{ t("settings.updateRestartHint") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<Label>{{ t("settings.updateDownloadSource") }}</Label>
|
||||
<p class="text-sm text-muted-foreground">{{ t("settings.updateDownloadSourceDescription") }}</p>
|
||||
</div>
|
||||
<Select :model-value="editUpdateDownloadSource" @update:model-value="onUpdateDownloadSourceChange">
|
||||
<SelectTrigger class="h-9 w-full sm:w-[180px]"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="official">{{ t("settings.updateDownloadSourceOfficial") }}</SelectItem>
|
||||
<SelectItem value="cnb">{{ t("settings.updateDownloadSourceCnb") }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<ChangelogPanel :checking-updates="props.checkingUpdates" @check-updates="emit('check-updates')" />
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeSettingsTab === 'security' && isWeb" data-settings-search-id="security" :class="['flex flex-col gap-5 py-2', settingsSearchTargetClass('security')]">
|
||||
<div class="space-y-3">
|
||||
<Label class="text-base">{{ t("auth.changePassword") }}</Label>
|
||||
@@ -9959,46 +10134,6 @@ LIMIT 100;</pre
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-item flex flex-col gap-4 rounded-lg border p-4">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<Label for="update-notifications-enabled">{{ t("settings.updateNotificationsEnabled") }}</Label>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ t("settings.updateNotificationsEnabledDescription") }}
|
||||
</p>
|
||||
</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>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ t("settings.updateDownloadSourceDescription") }}
|
||||
</p>
|
||||
</div>
|
||||
<Select :model-value="editUpdateDownloadSource" @update:model-value="onUpdateDownloadSourceChange">
|
||||
<SelectTrigger class="h-9 w-full sm:w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="official">{{ t("settings.updateDownloadSourceOfficial") }}</SelectItem>
|
||||
<SelectItem value="cnb">{{ t("settings.updateDownloadSourceCnb") }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ChangelogPanel :checking-updates="props.checkingUpdates" @check-updates="emit('check-updates')" />
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-3">
|
||||
<button type="button" class="rounded-lg border p-4 text-left transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" @click="openExternalUrl('https://qm.qq.com/cgi-bin/qm/qr?k=&group_code=1087880322')">
|
||||
<div class="flex items-center gap-2 text-sm font-medium">
|
||||
|
||||
@@ -622,9 +622,21 @@ const toolbarStyle = computed(() => {
|
||||
<template v-if="toolbarItems.checkUpdates">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button v-show="isRightItemVisible('checkUpdates')" data-toolbar-update-trigger variant="ghost" size="icon" class="toolbar-action-button relative h-8 w-8 shrink-0" @click="emit('check-updates')">
|
||||
<ToolbarUpdateIcon />
|
||||
<span v-if="hasUpdateAvailable" class="absolute right-1.5 top-1.5 h-2 w-2 rounded-full bg-red-500 ring-2 ring-background" />
|
||||
<Button
|
||||
v-show="isRightItemVisible('checkUpdates')"
|
||||
data-toolbar-update-trigger
|
||||
:data-toolbar-update-action="hasUpdateAvailable ? '' : undefined"
|
||||
:variant="hasUpdateAvailable ? 'default' : 'ghost'"
|
||||
:size="hasUpdateAvailable ? 'sm' : 'icon'"
|
||||
class="toolbar-action-button shrink-0"
|
||||
:class="hasUpdateAvailable ? 'h-7 gap-1.5 px-2 text-xs' : 'relative h-8 w-8'"
|
||||
@click="emit('check-updates')"
|
||||
>
|
||||
<template v-if="hasUpdateAvailable">
|
||||
<CloudDownload class="h-3.5 w-3.5" />
|
||||
<span>{{ t("updates.updateAction") }}</span>
|
||||
</template>
|
||||
<ToolbarUpdateIcon v-else :available="false" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ updateTooltip }}</TooltipContent>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { CloudDownload } from "@lucide/vue";
|
||||
// Background checking/downloading deliberately never animates the toolbar.
|
||||
defineProps<{ loading?: boolean; downloading?: boolean; progress?: number | null }>();
|
||||
defineProps<{ loading?: boolean; downloading?: boolean; progress?: number | null; available?: boolean }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span data-toolbar-update-icon class="relative block h-4 w-4" aria-hidden="true">
|
||||
<CloudDownload data-toolbar-update-idle class="h-4 w-4" />
|
||||
<CloudDownload data-toolbar-update-idle class="h-4 w-4" :class="available ? 'text-primary' : undefined" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
+93
-6
@@ -5,8 +5,10 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import i18n from "@/i18n";
|
||||
import UpdateDialog from "@/components/layout/UpdateDialog.vue";
|
||||
|
||||
const runtimeState = vi.hoisted(() => ({ tauri: true }));
|
||||
|
||||
vi.mock("@/lib/backend/tauriRuntime", () => ({
|
||||
isTauriRuntime: () => true,
|
||||
isTauriRuntime: () => runtimeState.tauri,
|
||||
}));
|
||||
|
||||
const mountedApps: App[] = [];
|
||||
@@ -29,7 +31,7 @@ async function flushDialog() {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
async function mountDialog(activeTaskCount: number, initialState: Partial<DialogState> = {}, installDownloaded = vi.fn(async () => {})) {
|
||||
async function mountDialog(activeTaskCount: number, initialState: Partial<DialogState> = {}, installDownloaded = vi.fn(async () => {}), extraProps: Record<string, unknown> = {}) {
|
||||
const state = reactive<DialogState>({
|
||||
open: true,
|
||||
portableMode: false,
|
||||
@@ -91,6 +93,7 @@ async function mountDialog(activeTaskCount: number, initialState: Partial<Dialog
|
||||
updateReady: state.updateReady,
|
||||
isIgnoringUpdate: state.isIgnoringUpdate,
|
||||
activeTaskCount,
|
||||
...extraProps,
|
||||
"onDownload-in-background": downloadInBackground,
|
||||
"onCancel-download": cancelDownload,
|
||||
"onInstall-downloaded": handleInstallDownloaded,
|
||||
@@ -134,10 +137,25 @@ async function clickOutside() {
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
runtimeState.tauri = true;
|
||||
for (const app of mountedApps.splice(0)) app.unmount();
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
describe("UpdateDialog web runtime", () => {
|
||||
it("keeps web updates as Docker instructions without desktop install controls", async () => {
|
||||
runtimeState.tauri = false;
|
||||
|
||||
await mountDialog(0);
|
||||
|
||||
expect(document.body.textContent).toContain("Docker users should run");
|
||||
expect(document.body.textContent).toContain("docker compose pull && docker compose up -d");
|
||||
expect(buttonWithText("Open Release")).toBeDefined();
|
||||
expect(downloadButton()).toBeUndefined();
|
||||
expect(installDownloadedButton()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("UpdateDialog active task guard", () => {
|
||||
it("shows the task warning but still allows starting a background download while work is running", async () => {
|
||||
await mountDialog(2);
|
||||
@@ -252,7 +270,6 @@ describe("UpdateDialog close protection", () => {
|
||||
it("allows closing while a downloaded update is idle", async () => {
|
||||
const { state } = await mountDialog(0, { updateDownloaded: true, downloadProgress: 100 });
|
||||
|
||||
expect(buttonWithText("Cancel")).toBeDefined();
|
||||
expect(document.body.querySelector('[data-slot="dialog-close"]')).not.toBeNull();
|
||||
await pressEscape();
|
||||
|
||||
@@ -262,7 +279,6 @@ describe("UpdateDialog close protection", () => {
|
||||
it("prevents closing while installation is in progress", async () => {
|
||||
const { state } = await mountDialog(0, { updateDownloaded: true, isInstallingUpdate: true });
|
||||
|
||||
expect(buttonWithText("Cancel")).toBeUndefined();
|
||||
expect(document.body.querySelector('[data-slot="dialog-close"]')).toBeNull();
|
||||
await pressEscape();
|
||||
|
||||
@@ -286,7 +302,7 @@ describe("UpdateDialog close protection", () => {
|
||||
|
||||
rejectInstall(new Error("install failed"));
|
||||
await flushDialog();
|
||||
expect(buttonWithText("Cancel")).toBeDefined();
|
||||
expect(document.body.querySelector('[data-slot="dialog-close"]')).not.toBeNull();
|
||||
await pressEscape();
|
||||
|
||||
expect(state.open).toBe(false);
|
||||
@@ -320,7 +336,6 @@ describe("UpdateDialog close protection", () => {
|
||||
expect(state.updateDownloaded).toBe(false);
|
||||
expect(state.updateReady).toBe(true);
|
||||
expect(buttonWithText("Restart")).toBeDefined();
|
||||
expect(buttonWithText("Cancel")).toBeDefined();
|
||||
await pressEscape();
|
||||
expect(state.open).toBe(false);
|
||||
expect(installDownloaded).toHaveBeenCalledOnce();
|
||||
@@ -363,3 +378,75 @@ describe("UpdateDialog release notes safety", () => {
|
||||
expect(Array.from(document.body.querySelectorAll("a")).every((anchor) => anchor.href.startsWith("https://"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("UpdateDialog aggregate update center", () => {
|
||||
it("keeps the client restart gated while update all is still updating components", async () => {
|
||||
await mountDialog(0, { updateDownloaded: true, downloadProgress: 100 }, undefined, { isUpdatingAll: true });
|
||||
|
||||
expect(installDownloadedButton()?.disabled).toBe(true);
|
||||
expect(buttonWithText("Ignore this version")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("disables component actions while an automatic update is active", async () => {
|
||||
await mountDialog(0, {}, undefined, {
|
||||
componentUpdatesUpdating: true,
|
||||
driverUpdates: [{ db_type: "mysql", label: "MySQL", version: "9.0.0", installed_version: "8.0.0", update_available: true }],
|
||||
});
|
||||
|
||||
expect(buttonWithText("Update all")?.disabled).toBe(true);
|
||||
|
||||
const driversTab = document.body.querySelector<HTMLButtonElement>('[data-update-tab="drivers"]');
|
||||
driversTab?.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, cancelable: true }));
|
||||
driversTab?.click();
|
||||
await flushDialog();
|
||||
|
||||
expect(buttonWithText("Update Now")?.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("organizes component updates into tabs and installs the selected category", async () => {
|
||||
const installComponentUpdates = vi.fn();
|
||||
const updateAll = vi.fn();
|
||||
await mountDialog(0, {}, undefined, {
|
||||
driverUpdates: [{ db_type: "mysql", label: "MySQL", version: "9.0.0", installed_version: "8.0.0", update_available: true }],
|
||||
jdbcUpdate: { installed: true, version: "0.1.0", latest_version: "0.2.0", update_available: true, compatible: true, path: "/tmp/jdbc" },
|
||||
mcpUpdate: { installed: true, npm_available: true, current_version: "1.0.0", latest_version: "1.1.0", update_available: true },
|
||||
pluginUpdates: [
|
||||
{
|
||||
key: "official:example",
|
||||
status: "update",
|
||||
artifact: { target: "universal", url: "https://example.com/plugin.dbxp", sha256: "hash" },
|
||||
repository: { id: "official" },
|
||||
plugin: { id: "example", latestVersion: "1.1.0" },
|
||||
name: "Example plugin",
|
||||
},
|
||||
],
|
||||
"onInstall-component-updates": installComponentUpdates,
|
||||
"onUpdate-all": updateAll,
|
||||
});
|
||||
|
||||
expect(document.body.querySelector('[data-update-tab="app"]')).not.toBeNull();
|
||||
expect(document.body.querySelector('[data-update-tab="drivers"]')?.textContent).toContain("Database drivers");
|
||||
expect(document.body.querySelector('[data-update-tab="jdbc"]')?.textContent).toContain("JDBC");
|
||||
expect(document.body.querySelector('[data-update-tab="mcp"]')?.textContent).toContain("MCP");
|
||||
expect(document.body.querySelector('[data-update-tab="plugins"]')?.textContent).toContain("Plugins");
|
||||
expect(document.body.querySelector<HTMLElement>("[data-update-scroll-region]")?.classList.contains("overflow-auto")).toBe(true);
|
||||
expect(document.body.querySelector<HTMLElement>("[data-update-footer]")?.className).toContain("mx-0");
|
||||
expect(document.body.querySelector<HTMLElement>("[data-update-footer]")?.className).toContain("px-[22px]");
|
||||
expect(document.body.querySelector<HTMLElement>("[data-update-footer]")?.className).toContain("pb-2.5");
|
||||
expect(document.body.querySelector<HTMLElement>("[data-update-footer]")?.className).toContain("pt-2.5");
|
||||
expect(buttonWithText("Cancel")).toBeUndefined();
|
||||
|
||||
buttonWithText("Update all")?.click();
|
||||
await flushDialog();
|
||||
expect(updateAll).toHaveBeenCalledOnce();
|
||||
|
||||
const driversTab = document.body.querySelector<HTMLButtonElement>('[data-update-tab="drivers"]');
|
||||
driversTab?.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, cancelable: true }));
|
||||
driversTab?.click();
|
||||
await flushDialog();
|
||||
buttonWithText("Update Now")?.click();
|
||||
await flushDialog();
|
||||
|
||||
expect(installComponentUpdates).toHaveBeenCalledWith("drivers");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,32 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { AlertTriangle, Loader2 } from "@lucide/vue";
|
||||
import { AlertTriangle, CheckCircle2, Loader2, RefreshCw } from "@lucide/vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import type { UpdateInfo } from "@/lib/backend/api";
|
||||
import type { UpdateDownloadSource } from "@/lib/backend/tauri";
|
||||
import type { AgentDriverInfo, McpServerStatus, UpdateDownloadSource } from "@/lib/backend/tauri";
|
||||
import type { JdbcPluginStatus } from "@/types/database";
|
||||
import type { MarketplacePluginListing } from "@/lib/plugins/pluginMarketplace";
|
||||
import { mcpUpdateAvailability } from "@/lib/mcp/mcpUpdateStatus";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { isUpdatePreviewMockEnabled } from "@/lib/updates/updatePreviewMock";
|
||||
import { canDownloadAndInstallUpdate } from "@/composables/useAppUpdater";
|
||||
import type { ComponentUpdateCategory } from "@/composables/useComponentUpdates";
|
||||
|
||||
type UpdateTab = "app" | ComponentUpdateCategory;
|
||||
|
||||
const open = defineModel<boolean>("open", { required: true });
|
||||
|
||||
const props = defineProps<{
|
||||
updateInfo: UpdateInfo | null;
|
||||
updateCheckMessage: string;
|
||||
isDownloadingUpdate: boolean;
|
||||
downloadProgress: number | null;
|
||||
updateDownloaded: boolean;
|
||||
isInstallingUpdate: boolean;
|
||||
isPreparingUpdate?: boolean;
|
||||
updateReady: boolean;
|
||||
isIgnoringUpdate: boolean;
|
||||
activeTaskCount: number;
|
||||
checkingUpdates: boolean;
|
||||
updateCheckFailed: boolean;
|
||||
updateDownloadSource: UpdateDownloadSource;
|
||||
}>();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
updateInfo: UpdateInfo | null;
|
||||
updateCheckMessage: string;
|
||||
isDownloadingUpdate: boolean;
|
||||
downloadProgress: number | null;
|
||||
updateDownloaded: boolean;
|
||||
isInstallingUpdate: boolean;
|
||||
isPreparingUpdate?: boolean;
|
||||
updateReady: boolean;
|
||||
isIgnoringUpdate: boolean;
|
||||
activeTaskCount: number;
|
||||
checkingUpdates: boolean;
|
||||
updateCheckFailed: boolean;
|
||||
updateDownloadSource: UpdateDownloadSource;
|
||||
driverUpdates?: AgentDriverInfo[];
|
||||
jdbcUpdate?: JdbcPluginStatus | null;
|
||||
mcpUpdate?: McpServerStatus | null;
|
||||
pluginUpdates?: MarketplacePluginListing[];
|
||||
componentUpdatesLoading?: boolean;
|
||||
componentUpdatesError?: string;
|
||||
componentUpdatesUpdating?: boolean;
|
||||
updatingComponent?: ComponentUpdateCategory | null;
|
||||
isUpdatingAll?: boolean;
|
||||
}>(),
|
||||
{
|
||||
driverUpdates: () => [],
|
||||
jdbcUpdate: null,
|
||||
mcpUpdate: null,
|
||||
pluginUpdates: () => [],
|
||||
componentUpdatesLoading: false,
|
||||
componentUpdatesError: "",
|
||||
componentUpdatesUpdating: false,
|
||||
updatingComponent: null,
|
||||
isUpdatingAll: false,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
"open-latest-release": [];
|
||||
@@ -36,21 +65,49 @@ const emit = defineEmits<{
|
||||
restart: [];
|
||||
"ignore-version": [];
|
||||
"change-download-source": [source: UpdateDownloadSource];
|
||||
"install-component-updates": [category: ComponentUpdateCategory];
|
||||
"update-all": [];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const isDesktop = isTauriRuntime();
|
||||
|
||||
const isDesktop = isTauriRuntime() || isUpdatePreviewMockEnabled();
|
||||
const selectedTab = ref<UpdateTab>("app");
|
||||
const renderedNotes = ref("");
|
||||
// Only active file replacement (installation) must trap the dialog. A background
|
||||
// download survives the dialog closing, so closing it never cancels the download.
|
||||
const isCloseBlocked = computed(() => props.isInstallingUpdate);
|
||||
const blocksImplicitDismiss = computed(() => props.isInstallingUpdate);
|
||||
const canIgnoreVersion = computed(() => props.updateInfo?.update_available === true && !props.isDownloadingUpdate && !props.isInstallingUpdate && !props.updateReady);
|
||||
|
||||
function handleCancel() {
|
||||
handleOpenChange(false);
|
||||
}
|
||||
const hasAppUpdate = computed(() => props.updateDownloaded || props.updateReady || props.updateInfo?.update_available === true);
|
||||
const mcpAvailable = computed(() => (props.mcpUpdate ? mcpUpdateAvailability(props.mcpUpdate) === true : false));
|
||||
const tabs = computed(() => {
|
||||
const available: Array<{ id: UpdateTab; label: string; count: number }> = [];
|
||||
if (hasAppUpdate.value) available.push({ id: "app", label: t("settings.updateClient"), count: 1 });
|
||||
if (props.driverUpdates.length) available.push({ id: "drivers", label: t("settings.updateDrivers"), count: props.driverUpdates.length });
|
||||
if (props.jdbcUpdate?.update_available) available.push({ id: "jdbc", label: t("settings.updateJdbc"), count: 1 });
|
||||
if (mcpAvailable.value) available.push({ id: "mcp", label: t("settings.updateMcp"), count: 1 });
|
||||
if (props.pluginUpdates.length) available.push({ id: "plugins", label: t("settings.updatePlugins"), count: props.pluginUpdates.length });
|
||||
return available.length ? available : [{ id: "app" as const, label: t("settings.updateClient"), count: 0 }];
|
||||
});
|
||||
const totalUpdateCount = computed(() => tabs.value.reduce((total, tab) => total + tab.count, 0));
|
||||
const hasComponentUpdates = computed(() => props.driverUpdates.length > 0 || props.jdbcUpdate?.update_available === true || mcpAvailable.value || props.pluginUpdates.length > 0);
|
||||
const selectedCategory = computed<ComponentUpdateCategory | null>(() => (selectedTab.value === "app" ? null : selectedTab.value));
|
||||
const selectedCategoryHasUpdate = computed(() => {
|
||||
if (selectedTab.value === "drivers") return props.driverUpdates.length > 0;
|
||||
if (selectedTab.value === "jdbc") return props.jdbcUpdate?.update_available === true;
|
||||
if (selectedTab.value === "mcp") return mcpAvailable.value;
|
||||
if (selectedTab.value === "plugins") return props.pluginUpdates.length > 0;
|
||||
return false;
|
||||
});
|
||||
const isUpdatingSelectedCategory = computed(() => selectedCategory.value !== null && props.updatingComponent === selectedCategory.value);
|
||||
const isAnyComponentUpdating = computed(() => props.componentUpdatesUpdating || props.updatingComponent !== null);
|
||||
const isCloseBlocked = computed(() => props.isInstallingUpdate || isUpdatingSelectedCategory.value);
|
||||
const blocksImplicitDismiss = computed(() => isCloseBlocked.value);
|
||||
const canIgnoreVersion = computed(() => props.updateInfo?.update_available === true && !props.isDownloadingUpdate && !props.isInstallingUpdate && !props.updateReady && !props.isUpdatingAll);
|
||||
|
||||
watch(
|
||||
tabs,
|
||||
(nextTabs) => {
|
||||
if (!nextTabs.some((tab) => tab.id === selectedTab.value)) selectedTab.value = nextTabs[0]?.id ?? "app";
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function handleOpenChange(nextOpen: boolean) {
|
||||
if (nextOpen) {
|
||||
@@ -106,6 +163,7 @@ watch(
|
||||
<Dialog :open="open" @update:open="handleOpenChange">
|
||||
<DialogContent
|
||||
class="sm:max-w-[700px]"
|
||||
:class="'flex max-h-[min(720px,calc(var(--dbx-viewport-height)-48px))] flex-col gap-0 overflow-hidden p-0'"
|
||||
:show-close-button="!isCloseBlocked"
|
||||
@interact-outside="
|
||||
(e: Event) => {
|
||||
@@ -118,92 +176,195 @@ watch(
|
||||
}
|
||||
"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ updateInfo?.update_available ? t("updates.availableTitle") : t("updates.title") }}</DialogTitle>
|
||||
<DialogHeader class="border-b py-4 pl-5 pr-12">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<DialogTitle class="flex min-w-0 items-center gap-2">
|
||||
<span>{{ t("updates.centerTitle") }}</span>
|
||||
<span v-if="totalUpdateCount > 0" class="rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">{{ t("updates.centerAvailable", { count: totalUpdateCount }) }}</span>
|
||||
</DialogTitle>
|
||||
<Button v-if="hasComponentUpdates" type="button" size="sm" class="h-8 shrink-0" :disabled="props.isUpdatingAll || props.checkingUpdates || props.isDownloadingUpdate || props.isInstallingUpdate || isAnyComponentUpdating" @click="emit('update-all')">
|
||||
<Loader2 v-if="props.isUpdatingAll" class="h-3.5 w-3.5 animate-spin" />
|
||||
<RefreshCw v-else class="h-3.5 w-3.5" />
|
||||
{{ t("updates.updateAll") }}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<div class="space-y-3 text-sm">
|
||||
<p v-if="updateInfo?.update_available">
|
||||
{{
|
||||
t("updates.availableMessage", {
|
||||
current: updateInfo.current_version,
|
||||
latest: updateInfo.latest_version,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<p v-else-if="!checkingUpdates" class="text-muted-foreground">
|
||||
{{ updateCheckMessage || t("updates.upToDate", { version: updateInfo?.current_version || "" }) }}
|
||||
</p>
|
||||
<div
|
||||
v-if="updateInfo?.update_available && updateInfo.release_notes"
|
||||
class="max-h-48 overflow-auto rounded-md border bg-muted/30 p-3 text-xs [&_h1]:text-sm [&_h1]:font-semibold [&_h1]:mb-1 [&_h2]:text-sm [&_h2]:font-semibold [&_h2]:mb-1 [&_h3]:text-xs [&_h3]:font-semibold [&_h3]:mb-1 [&_ul]:list-disc [&_ul]:pl-4 [&_ul]:my-1 [&_ol]:list-decimal [&_ol]:pl-4 [&_ol]:my-1 [&_li]:my-0.5 [&_p]:my-1 [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-[11px] [&_a]:text-primary [&_a]:underline"
|
||||
v-html="renderedNotes"
|
||||
@click="handleReleaseNotesClick"
|
||||
/>
|
||||
<p v-if="!isDesktop && updateInfo?.update_available" class="text-xs text-muted-foreground">
|
||||
{{ t("updates.dockerUsersRun") }}
|
||||
<code class="bg-muted px-1 py-0.5 rounded text-[11px]">docker compose pull && docker compose up -d</code>
|
||||
{{ t("updates.toUpdate") }}
|
||||
</p>
|
||||
<p v-if="isDesktop && updateInfo?.update_available && updateInfo.portable_mode && !updateInfo.manual_update_only" class="text-xs text-muted-foreground">
|
||||
{{ t("updates.portableAutomaticUpdate") }}
|
||||
</p>
|
||||
<p v-if="isDesktop && updateInfo?.update_available && updateInfo.manual_update_only" class="text-xs text-muted-foreground">
|
||||
{{ t("updates.windows7ManualUpdate") }}
|
||||
</p>
|
||||
<div v-if="canDownloadAndInstallUpdate(updateInfo, isDesktop) && activeTaskCount > 0" role="alert" class="flex items-start gap-2 rounded-md border border-amber-500/40 bg-amber-500/10 px-3 py-2 text-xs text-amber-700 dark:text-amber-300">
|
||||
<AlertTriangle class="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<span>{{ t("updates.activeTasksBlockUpdate", { count: activeTaskCount }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="checkingUpdates" class="text-sm text-muted-foreground">{{ t("updates.checking") }}</p>
|
||||
<p v-if="updateDownloaded && !isInstallingUpdate" class="text-sm">{{ t("updates.downloadedReady", { version: updateInfo?.latest_version }) }}</p>
|
||||
<p v-if="updateCheckFailed && updateInfo?.update_available" role="alert" class="text-sm text-destructive">{{ updateCheckMessage }}</p>
|
||||
<DialogFooter class="relative min-w-0 sm:justify-end">
|
||||
<div v-if="!updateReady && !isInstallingUpdate && !updateDownloaded && (updateInfo?.update_available || updateCheckFailed)" class="flex items-center gap-1.5 self-start sm:absolute sm:left-4 sm:top-1/2 sm:-translate-y-1/2">
|
||||
<span class="text-xs text-muted-foreground">{{ t("updates.source") }}</span>
|
||||
<Select :model-value="updateDownloadSource" :disabled="checkingUpdates || isIgnoringUpdate" @update:model-value="(value) => value && emit('change-download-source', value as UpdateDownloadSource)">
|
||||
<SelectTrigger class="h-8 w-[150px] text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="official">{{ t("updates.sourceOfficial") }}</SelectItem>
|
||||
<SelectItem value="cnb">{{ t("updates.sourceCnb") }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<template v-if="updateInfo?.update_available">
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-2 sm:justify-end">
|
||||
<Button v-if="!isCloseBlocked" variant="outline" class="shrink-0" @click="handleCancel">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button v-if="canIgnoreVersion" variant="ghost" class="shrink-0" :disabled="isIgnoringUpdate" @click="emit('ignore-version')">
|
||||
<Loader2 v-if="isIgnoringUpdate" class="h-4 w-4 animate-spin" />
|
||||
{{ t("updates.ignoreVersion") }}
|
||||
</Button>
|
||||
<Button variant="outline" class="shrink-0" @click="emit('open-latest-release')">{{ t("updates.openRelease") }}</Button>
|
||||
|
||||
<div class="flex min-h-0 flex-1 flex-col">
|
||||
<div class="border-b px-4 py-2">
|
||||
<div role="tablist" :aria-label="t('updates.centerTitle')" class="inline-flex h-9 w-full items-center justify-center rounded-md bg-muted p-[3px] text-muted-foreground">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
type="button"
|
||||
role="tab"
|
||||
:aria-selected="selectedTab === tab.id"
|
||||
:data-state="selectedTab === tab.id ? 'active' : 'inactive'"
|
||||
:data-update-tab="tab.id"
|
||||
class="relative inline-flex h-full flex-1 select-none items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-transparent px-2 text-sm font-medium transition-colors hover:text-foreground focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50"
|
||||
:class="selectedTab === tab.id ? 'bg-background text-foreground shadow-sm' : 'text-foreground/60'"
|
||||
@click="selectedTab = tab.id"
|
||||
>
|
||||
<span>{{ tab.label }}</span>
|
||||
<span v-if="tab.count > 0" class="rounded-full bg-foreground/10 px-1.5 text-[11px]">{{ tab.count }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="canDownloadAndInstallUpdate(updateInfo, isDesktop)">
|
||||
<Button v-if="updateReady" class="shrink-0" :disabled="activeTaskCount > 0 || isIgnoringUpdate" @click="emit('restart')">{{ t("updates.restart") }}</Button>
|
||||
<Button v-else-if="isInstallingUpdate" class="shrink-0" disabled>
|
||||
</div>
|
||||
|
||||
<div data-update-scroll-region class="min-h-0 flex-1 overflow-auto px-5 py-4 text-sm">
|
||||
<template v-if="selectedTab === 'app'">
|
||||
<div class="space-y-3">
|
||||
<p v-if="updateInfo?.update_available">
|
||||
{{
|
||||
t("updates.availableMessage", {
|
||||
current: updateInfo.current_version,
|
||||
latest: updateInfo.latest_version,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<p v-else-if="!checkingUpdates" class="text-muted-foreground">
|
||||
{{ updateCheckMessage || t("updates.upToDate", { version: updateInfo?.current_version || "" }) }}
|
||||
</p>
|
||||
<div
|
||||
v-if="updateInfo?.update_available && updateInfo.release_notes"
|
||||
class="max-h-64 overflow-auto rounded-md border bg-muted/30 p-3 text-xs [&_h1]:mb-1 [&_h1]:text-sm [&_h1]:font-semibold [&_h2]:mb-1 [&_h2]:text-sm [&_h2]:font-semibold [&_h3]:mb-1 [&_h3]:text-xs [&_h3]:font-semibold [&_ol]:my-1 [&_ol]:list-decimal [&_ol]:pl-4 [&_p]:my-1 [&_ul]:my-1 [&_ul]:list-disc [&_ul]:pl-4 [&_li]:my-0.5 [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:text-[11px] [&_a]:text-primary [&_a]:underline"
|
||||
v-html="renderedNotes"
|
||||
@click="handleReleaseNotesClick"
|
||||
/>
|
||||
<p v-if="!isDesktop && updateInfo?.update_available" class="text-xs text-muted-foreground">
|
||||
{{ t("updates.dockerUsersRun") }}
|
||||
<code class="rounded bg-muted px-1 py-0.5 text-[11px]">docker compose pull && docker compose up -d</code>
|
||||
{{ t("updates.toUpdate") }}
|
||||
</p>
|
||||
<p v-if="isDesktop && updateInfo?.update_available && updateInfo.portable_mode && !updateInfo.manual_update_only" class="text-xs text-muted-foreground">
|
||||
{{ t("updates.portableAutomaticUpdate") }}
|
||||
</p>
|
||||
<p v-if="isDesktop && updateInfo?.update_available && updateInfo.manual_update_only" class="text-xs text-muted-foreground">
|
||||
{{ t("updates.windows7ManualUpdate") }}
|
||||
</p>
|
||||
<div v-if="canDownloadAndInstallUpdate(updateInfo, isDesktop) && activeTaskCount > 0" role="alert" class="flex items-start gap-2 rounded-md border border-amber-500/40 bg-amber-500/10 px-3 py-2 text-xs text-amber-700 dark:text-amber-300">
|
||||
<AlertTriangle class="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<span>{{ t("updates.activeTasksBlockUpdate", { count: activeTaskCount }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="checkingUpdates" class="mt-3 flex items-center gap-2 text-muted-foreground">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t(isPreparingUpdate ? "updates.preparing" : "updates.installing") }}
|
||||
</Button>
|
||||
<template v-else-if="isDownloadingUpdate">
|
||||
<Button variant="ghost" class="shrink-0" @click="emit('cancel-download')">{{ t("updates.cancelDownload") }}</Button>
|
||||
<Button class="w-52 shrink-0 tabular-nums" disabled>
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t("updates.downloading", { progress: downloadProgress ?? 0 }) }}
|
||||
</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(updateCheckFailed ? "updates.retryDownload" : "updates.downloadInBackground") }}</Button>
|
||||
{{ t("updates.checking") }}
|
||||
</p>
|
||||
<p v-if="updateDownloaded && !isInstallingUpdate" class="mt-3">{{ t("updates.downloadedReady", { version: updateInfo?.latest_version }) }}</p>
|
||||
<p v-if="updateCheckFailed && updateInfo?.update_available" role="alert" class="mt-3 text-destructive">{{ updateCheckMessage }}</p>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-2 sm:justify-end">
|
||||
<Button v-if="!isCloseBlocked" variant="outline" class="shrink-0" @click="handleCancel">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button v-if="updateCheckMessage" class="shrink-0" @click="emit('open-latest-release')">{{ t("updates.openRelease") }}</Button>
|
||||
|
||||
<template v-else>
|
||||
<div v-if="componentUpdatesLoading && !selectedCategoryHasUpdate" class="flex items-center justify-center gap-2 py-12 text-muted-foreground">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t("updates.checking") }}
|
||||
</div>
|
||||
<div v-else class="space-y-2">
|
||||
<div v-if="componentUpdatesError" class="mb-3 flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/5 p-3 text-destructive">
|
||||
<AlertTriangle class="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<span>{{ componentUpdatesError }}</span>
|
||||
</div>
|
||||
<template v-if="selectedTab === 'drivers'">
|
||||
<div v-for="driver in driverUpdates" :key="driver.db_type" class="flex items-center justify-between gap-4 rounded-md border p-3">
|
||||
<div class="min-w-0">
|
||||
<div class="font-medium">{{ driver.label }}</div>
|
||||
<div class="mt-0.5 text-xs text-muted-foreground">{{ driver.installed_version || driver.version }} → {{ driver.version }}</div>
|
||||
</div>
|
||||
<span class="shrink-0 rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">{{ t("settings.updateAvailable") }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="selectedTab === 'jdbc' && jdbcUpdate?.update_available">
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border p-3">
|
||||
<div class="min-w-0">
|
||||
<div class="font-medium">{{ t("settings.updateJdbc") }}</div>
|
||||
<div class="mt-0.5 text-xs text-muted-foreground">{{ jdbcUpdate.version || t("updates.notInstalled") }} → {{ jdbcUpdate.latest_version || t("settings.updateAvailable") }}</div>
|
||||
</div>
|
||||
<span class="shrink-0 rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">{{ t("settings.updateAvailable") }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="selectedTab === 'mcp' && mcpAvailable && mcpUpdate">
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border p-3">
|
||||
<div class="min-w-0">
|
||||
<div class="font-medium">{{ t("settings.updateMcp") }}</div>
|
||||
<div class="mt-0.5 text-xs text-muted-foreground">{{ mcpUpdate.current_version || t("updates.notInstalled") }} → {{ mcpUpdate.latest_version || t("settings.updateAvailable") }}</div>
|
||||
</div>
|
||||
<span class="shrink-0 rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">{{ t("settings.updateAvailable") }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="selectedTab === 'plugins'">
|
||||
<div v-for="plugin in pluginUpdates" :key="plugin.key" class="flex items-center justify-between gap-4 rounded-md border p-3">
|
||||
<div class="min-w-0">
|
||||
<div class="truncate font-medium">{{ plugin.name }}</div>
|
||||
<div class="mt-0.5 text-xs text-muted-foreground">{{ plugin.installed?.manifest.version || t("updates.notInstalled") }} → {{ plugin.plugin.latestVersion }}</div>
|
||||
</div>
|
||||
<span class="shrink-0 rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">{{ t("settings.updateAvailable") }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="!selectedCategoryHasUpdate" class="flex flex-col items-center justify-center gap-2 py-12 text-muted-foreground">
|
||||
<CheckCircle2 class="h-6 w-6 text-emerald-500" />
|
||||
{{ t("settings.upToDate") }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<DialogFooter v-if="selectedTab === 'app'" data-update-footer class="mx-0 mb-0 min-w-0 rounded-none border-t px-[22px] pb-2.5 pt-2.5 sm:items-center sm:justify-end">
|
||||
<div v-if="!updateReady && !isInstallingUpdate && !updateDownloaded && (updateInfo?.update_available || updateCheckFailed)" class="flex items-center gap-1.5 self-start sm:mr-auto sm:self-center">
|
||||
<span class="text-xs text-muted-foreground">{{ t("updates.source") }}</span>
|
||||
<Select :model-value="updateDownloadSource" :disabled="checkingUpdates || isIgnoringUpdate" @update:model-value="(value) => value && emit('change-download-source', value as UpdateDownloadSource)">
|
||||
<SelectTrigger class="h-8 w-[150px] text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="official">{{ t("updates.sourceOfficial") }}</SelectItem>
|
||||
<SelectItem value="cnb">{{ t("updates.sourceCnb") }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</template>
|
||||
</DialogFooter>
|
||||
<template v-if="updateInfo?.update_available">
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-2 sm:justify-end">
|
||||
<Button v-if="canIgnoreVersion" variant="ghost" class="shrink-0" :disabled="isIgnoringUpdate" @click="emit('ignore-version')">
|
||||
<Loader2 v-if="isIgnoringUpdate" class="h-4 w-4 animate-spin" />
|
||||
{{ t("updates.ignoreVersion") }}
|
||||
</Button>
|
||||
<Button variant="outline" class="shrink-0" @click="emit('open-latest-release')">{{ t("updates.openRelease") }}</Button>
|
||||
</div>
|
||||
<template v-if="canDownloadAndInstallUpdate(updateInfo, isDesktop)">
|
||||
<Button v-if="updateReady" class="shrink-0" :disabled="activeTaskCount > 0 || isIgnoringUpdate || isUpdatingAll" @click="emit('restart')">{{ t("updates.restart") }}</Button>
|
||||
<Button v-else-if="isInstallingUpdate" class="shrink-0" disabled>
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t(isPreparingUpdate ? "updates.preparing" : "updates.installing") }}
|
||||
</Button>
|
||||
<template v-else-if="isDownloadingUpdate">
|
||||
<Button variant="ghost" class="shrink-0" @click="emit('cancel-download')">{{ t("updates.cancelDownload") }}</Button>
|
||||
<Button class="w-52 shrink-0 tabular-nums" disabled>
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t("updates.downloading", { progress: downloadProgress ?? 0 }) }}
|
||||
</Button>
|
||||
</template>
|
||||
<Button v-else-if="updateDownloaded" class="shrink-0" :disabled="activeTaskCount > 0 || isIgnoringUpdate || isUpdatingAll" @click="emit('install-downloaded')">{{ t("updates.restartAndUpdate") }}</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>
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-2 sm:justify-end">
|
||||
<Button v-if="updateCheckMessage" class="shrink-0" @click="emit('open-latest-release')">{{ t("updates.openRelease") }}</Button>
|
||||
</div>
|
||||
</template>
|
||||
</DialogFooter>
|
||||
|
||||
<DialogFooter v-else data-update-footer class="mx-0 mb-0 rounded-none border-t px-[22px] pb-2.5 pt-2.5 sm:items-center sm:justify-end">
|
||||
<Button v-if="selectedCategory" :disabled="!selectedCategoryHasUpdate || isAnyComponentUpdating || isInstallingUpdate || isUpdatingAll" @click="emit('install-component-updates', selectedCategory)">
|
||||
<Loader2 v-if="isUpdatingSelectedCategory" class="h-4 w-4 animate-spin" />
|
||||
{{ t(isUpdatingSelectedCategory ? "updates.updating" : "updates.updateNow") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { createApp, h } from "vue";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import ToolbarUpdateIcon from "../ToolbarUpdateIcon.vue";
|
||||
describe("silent toolbar update icon", () => {
|
||||
@@ -11,4 +13,20 @@ describe("silent toolbar update icon", () => {
|
||||
expect(container.querySelector("[data-toolbar-update-progress], [data-toolbar-update-scan]")).toBeNull();
|
||||
app.unmount();
|
||||
});
|
||||
|
||||
it("renders a text Update action whenever any aggregate update is available", () => {
|
||||
const source = readFileSync(resolve(process.cwd(), "apps/desktop/src/components/layout/AppToolbar.vue"), "utf8");
|
||||
|
||||
expect(source).toContain('v-if="hasUpdateAvailable"');
|
||||
expect(source).toContain("data-toolbar-update-action");
|
||||
expect(source).toContain('t("updates.updateAction")');
|
||||
});
|
||||
|
||||
it("passes only opt-out component badges to their separate toolbar entries", () => {
|
||||
const source = readFileSync(resolve(process.cwd(), "apps/desktop/src/App.vue"), "utf8");
|
||||
|
||||
expect(source).toContain(':agent-driver-update-count="showDriverStoreUpdateBadge"');
|
||||
expect(source).toContain(':has-mcp-update-available="showMcpSettingsUpdateBadge"');
|
||||
expect(source).not.toContain(':agent-driver-update-count="toolbarDriverUpdateCount"');
|
||||
});
|
||||
});
|
||||
|
||||
+27
-18
@@ -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, autoDownloadUpdates: true });
|
||||
const settings = reactive({ updateDownloadSource: "official", ignoredUpdateVersion: "", updateNotificationsEnabled: true, autoDownloadUpdates: true, autoUpdateApp: 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 };
|
||||
@@ -59,6 +59,7 @@ beforeEach(() => {
|
||||
settings.ignoredUpdateVersion = "";
|
||||
settings.updateNotificationsEnabled = true;
|
||||
settings.autoDownloadUpdates = true;
|
||||
settings.autoUpdateApp = true;
|
||||
mocks.checkForUpdates.mockResolvedValue(info);
|
||||
mocks.downloadUpdate.mockResolvedValue(cache);
|
||||
mocks.getDownloadedUpdate.mockResolvedValue(null);
|
||||
@@ -72,7 +73,7 @@ afterEach(() => {
|
||||
});
|
||||
describe("silent update lifecycle", () => {
|
||||
it("checks and shows a badge without downloading when automatic downloads are disabled", async () => {
|
||||
settings.autoDownloadUpdates = false;
|
||||
settings.autoUpdateApp = false;
|
||||
const updater = mount();
|
||||
await updater.initialize();
|
||||
await flush();
|
||||
@@ -89,7 +90,7 @@ describe("silent update lifecycle", () => {
|
||||
expect(mocks.installDownloadedUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
it("surfaces a newer release instead of a stale package with automatic downloads disabled", async () => {
|
||||
settings.autoDownloadUpdates = false;
|
||||
settings.autoUpdateApp = false;
|
||||
mocks.getDownloadedUpdate.mockResolvedValue(cache);
|
||||
mocks.checkForUpdates.mockResolvedValue({ ...info, latest_version: "1.2.0", release_name: "v1.2.0" });
|
||||
const updater = mount();
|
||||
@@ -108,7 +109,7 @@ describe("silent update lifecycle", () => {
|
||||
expect(updater.updateDownloaded.value).toBe(true);
|
||||
});
|
||||
it("preserves a prepared package with automatic downloads disabled", async () => {
|
||||
settings.autoDownloadUpdates = false;
|
||||
settings.autoUpdateApp = false;
|
||||
mocks.getDownloadedUpdate.mockResolvedValue(cache);
|
||||
const updater = mount();
|
||||
await updater.initialize();
|
||||
@@ -121,11 +122,11 @@ describe("silent update lifecycle", () => {
|
||||
expect(mocks.installDownloadedUpdate).toHaveBeenCalledWith(cache.cache_id, cache.version);
|
||||
});
|
||||
it("starts a silent download when the preference is enabled", async () => {
|
||||
settings.autoDownloadUpdates = false;
|
||||
settings.autoUpdateApp = false;
|
||||
const updater = mount();
|
||||
await updater.initialize();
|
||||
await flush();
|
||||
settings.autoDownloadUpdates = true;
|
||||
settings.autoUpdateApp = true;
|
||||
await vi.waitFor(() => expect(updater.updateDownloaded.value).toBe(true));
|
||||
expect(mocks.downloadUpdate).toHaveBeenCalledOnce();
|
||||
expect(updater.showUpdateDialog.value).toBe(false);
|
||||
@@ -137,23 +138,23 @@ describe("silent update lifecycle", () => {
|
||||
const updater = mount();
|
||||
const checking = updater.checkUpdates({ silent: true });
|
||||
await vi.waitFor(() => expect(mocks.downloadUpdate).toHaveBeenCalledOnce());
|
||||
settings.autoDownloadUpdates = false;
|
||||
settings.autoUpdateApp = 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;
|
||||
settings.autoUpdateApp = 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;
|
||||
settings.autoUpdateApp = true;
|
||||
await nextTick();
|
||||
settings.autoDownloadUpdates = false;
|
||||
settings.autoUpdateApp = false;
|
||||
await flush();
|
||||
expect(mocks.cancelUpdateDownload).not.toHaveBeenCalled();
|
||||
updater.showUpdateDialog.value = false;
|
||||
@@ -172,13 +173,13 @@ describe("silent update lifecycle", () => {
|
||||
expect(mocks.installDownloadedUpdate).not.toHaveBeenCalled();
|
||||
expect(mocks.relaunch).not.toHaveBeenCalled();
|
||||
});
|
||||
it("shows no badge until a download finishes, and closing does not cancel", async () => {
|
||||
it("keeps the cloud icon available while downloading, and closing does not cancel", async () => {
|
||||
const pending = deferred<typeof cache>();
|
||||
mocks.downloadUpdate.mockReturnValue(pending.promise);
|
||||
const updater = mount();
|
||||
const checking = updater.checkUpdates();
|
||||
await flush();
|
||||
expect(updater.hasUpdateAvailable.value).toBe(false);
|
||||
expect(updater.hasUpdateAvailable.value).toBe(true);
|
||||
updater.showUpdateDialog.value = false;
|
||||
pending.resolve(cache);
|
||||
await checking;
|
||||
@@ -245,16 +246,24 @@ describe("silent update lifecycle", () => {
|
||||
expect(mocks.discardDownloadedUpdate).not.toHaveBeenCalled();
|
||||
expect(updater.updateDownloaded.value).toBe(true);
|
||||
});
|
||||
it("hides ready badge when notifications disabled but preserves manual installation", async () => {
|
||||
it("keeps a ready update available when automatic app updates are disabled", async () => {
|
||||
mocks.getDownloadedUpdate.mockResolvedValue(cache);
|
||||
const updater = mount();
|
||||
await updater.initialize();
|
||||
settings.updateNotificationsEnabled = false;
|
||||
settings.autoUpdateApp = false;
|
||||
await flush();
|
||||
expect(updater.hasUpdateAvailable.value).toBe(false);
|
||||
expect(updater.hasUpdateAvailable.value).toBe(true);
|
||||
expect(updater.updateDownloaded.value).toBe(true);
|
||||
expect(mocks.discardDownloadedUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
it("shows a remote app update without downloading it when automatic app updates are disabled", async () => {
|
||||
settings.autoUpdateApp = false;
|
||||
const updater = mount();
|
||||
await updater.checkUpdates({ silent: true });
|
||||
expect(updater.hasUpdateAvailable.value).toBe(true);
|
||||
expect(updater.updateDownloaded.value).toBe(false);
|
||||
expect(mocks.downloadUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
it("ignores progress belonging to another version or download attempt", async () => {
|
||||
const pending = deferred<typeof cache>();
|
||||
mocks.downloadUpdate.mockReturnValue(pending.promise);
|
||||
@@ -366,7 +375,7 @@ describe("silent update lifecycle", () => {
|
||||
expect(updater.updateDownloaded.value).toBe(false);
|
||||
});
|
||||
|
||||
it("resumes after notifications are reenabled while cancellation is pending", async () => {
|
||||
it("resumes after automatic app updates are reenabled while cancellation is pending", async () => {
|
||||
const pendingDownload = deferred<typeof cache>();
|
||||
const pendingCancel = deferred<void>();
|
||||
mocks.downloadUpdate.mockReturnValueOnce(pendingDownload.promise);
|
||||
@@ -374,9 +383,9 @@ describe("silent update lifecycle", () => {
|
||||
const updater = mount();
|
||||
await updater.initialize();
|
||||
await vi.waitFor(() => expect(mocks.downloadUpdate).toHaveBeenCalledTimes(1));
|
||||
settings.updateNotificationsEnabled = false;
|
||||
settings.autoUpdateApp = false;
|
||||
await nextTick();
|
||||
settings.updateNotificationsEnabled = true;
|
||||
settings.autoUpdateApp = true;
|
||||
await nextTick();
|
||||
pendingDownload.reject(new Error("cancelled"));
|
||||
pendingCancel.resolve();
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useComponentUpdates } from "@/composables/useComponentUpdates";
|
||||
import { runPendingComponentUpdatePlan, type PendingComponentUpdates } from "@/lib/updates/componentUpdateOrchestration";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
listInstalledAgents: vi.fn(),
|
||||
jdbcPluginStatus: vi.fn(),
|
||||
checkMcpServerStatus: vi.fn(),
|
||||
listPlugins: vi.fn(),
|
||||
fetchPluginMarketplaceCatalogs: vi.fn(),
|
||||
checkAgentUpdateBlockers: vi.fn(),
|
||||
upgradeAllAgents: vi.fn(),
|
||||
installJdbcPlugin: vi.fn(),
|
||||
installMcpServer: vi.fn(),
|
||||
installMarketplacePlugin: vi.fn(),
|
||||
buildMarketplacePluginListings: vi.fn(),
|
||||
}));
|
||||
|
||||
const settings = {
|
||||
editorSettings: {
|
||||
autoUpdateDrivers: true,
|
||||
autoUpdateJdbc: true,
|
||||
autoUpdateMcp: true,
|
||||
autoUpdatePlugins: true,
|
||||
},
|
||||
};
|
||||
|
||||
vi.mock("@/lib/backend/api", () => mocks);
|
||||
vi.mock("@/stores/settingsStore", () => ({ useSettingsStore: () => settings }));
|
||||
vi.mock("@/i18n", () => ({ currentLocale: () => "zh-CN" }));
|
||||
vi.mock("@/lib/plugins/pluginMarketplace", () => ({ buildMarketplacePluginListings: mocks.buildMarketplacePluginListings }));
|
||||
|
||||
const mcpStatus = {
|
||||
installed: true,
|
||||
npm_available: true,
|
||||
current_version: "1.0.0",
|
||||
latest_version: "1.1.0",
|
||||
update_available: true,
|
||||
};
|
||||
|
||||
const pluginUpdate = {
|
||||
status: "update",
|
||||
artifact: { target: "universal", url: "https://example.com/plugin.dbxp", sha256: "hash" },
|
||||
repository: { id: "official" },
|
||||
plugin: { id: "example", latestVersion: "1.1.0" },
|
||||
name: "Example plugin",
|
||||
};
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T | PromiseLike<T>) => void;
|
||||
const promise = new Promise<T>((resolvePromise) => {
|
||||
resolve = resolvePromise;
|
||||
});
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
Object.assign(settings.editorSettings, {
|
||||
autoUpdateDrivers: true,
|
||||
autoUpdateJdbc: true,
|
||||
autoUpdateMcp: true,
|
||||
autoUpdatePlugins: true,
|
||||
});
|
||||
mocks.listInstalledAgents.mockResolvedValue([{ db_type: "mysql", update_available: true }]);
|
||||
mocks.jdbcPluginStatus.mockResolvedValue({ installed: true, update_available: true });
|
||||
mocks.checkMcpServerStatus.mockResolvedValue(mcpStatus);
|
||||
mocks.listPlugins.mockResolvedValue([]);
|
||||
mocks.fetchPluginMarketplaceCatalogs.mockResolvedValue([]);
|
||||
mocks.buildMarketplacePluginListings.mockReturnValue([pluginUpdate]);
|
||||
mocks.checkAgentUpdateBlockers.mockResolvedValue([]);
|
||||
mocks.upgradeAllAgents.mockResolvedValue({ upgraded: 1, failed: [], cancelled: 0 });
|
||||
mocks.installJdbcPlugin.mockResolvedValue({ installed: true, update_available: false });
|
||||
mocks.installMcpServer.mockResolvedValue("updated");
|
||||
mocks.installMarketplacePlugin.mockResolvedValue({});
|
||||
});
|
||||
|
||||
describe("useComponentUpdates", () => {
|
||||
it("counts agent, JDBC, MCP, and plugin updates", async () => {
|
||||
const updates = useComponentUpdates({ isDesktop: true });
|
||||
|
||||
expect(await updates.refresh()).toBe(true);
|
||||
expect(updates.driverUpdateCount.value).toBe(1);
|
||||
expect(updates.jdbcUpdateAvailable.value).toBe(true);
|
||||
expect(updates.mcpUpdateAvailable.value).toBe(true);
|
||||
expect(updates.pluginUpdateCount.value).toBe(1);
|
||||
expect(updates.totalUpdateCount.value).toBe(4);
|
||||
});
|
||||
|
||||
it("installs every enabled component category after an app update", async () => {
|
||||
const updates = useComponentUpdates({ isDesktop: true });
|
||||
|
||||
const result = await updates.autoUpdateEnabledComponents();
|
||||
|
||||
expect(mocks.checkAgentUpdateBlockers).toHaveBeenCalledWith(["mysql"]);
|
||||
expect(mocks.upgradeAllAgents).toHaveBeenCalledOnce();
|
||||
expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce();
|
||||
expect(mocks.installMcpServer).toHaveBeenCalledOnce();
|
||||
expect(mocks.installMarketplacePlugin).toHaveBeenCalledWith({ repositoryId: "official", pluginId: "example", version: "1.1.0" });
|
||||
expect(result).toEqual({ drivers: 1, jdbc: true, mcp: true, plugins: 1, skippedDrivers: 0, failed: [] });
|
||||
});
|
||||
|
||||
it("shares one update operation between automatic and manual callers", async () => {
|
||||
const jdbcInstall = deferred<{ installed: boolean; update_available: boolean }>();
|
||||
mocks.installJdbcPlugin.mockReturnValue(jdbcInstall.promise);
|
||||
const updates = useComponentUpdates({ isDesktop: true });
|
||||
|
||||
const automatic = updates.autoUpdateEnabledComponents();
|
||||
await vi.waitFor(() => expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce());
|
||||
const manual = updates.installCategory("jdbc");
|
||||
|
||||
expect(updates.updating.value).toBe(true);
|
||||
expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce();
|
||||
|
||||
jdbcInstall.resolve({ installed: true, update_available: false });
|
||||
const [automaticResult, manualResult] = await Promise.all([automatic, manual]);
|
||||
|
||||
expect(manualResult).toEqual(automaticResult);
|
||||
expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce();
|
||||
expect(mocks.installMcpServer).toHaveBeenCalledOnce();
|
||||
expect(mocks.installMarketplacePlugin).toHaveBeenCalledOnce();
|
||||
expect(updates.updating.value).toBe(false);
|
||||
});
|
||||
|
||||
it("still detects updates but does not install disabled categories", async () => {
|
||||
Object.assign(settings.editorSettings, {
|
||||
autoUpdateDrivers: false,
|
||||
autoUpdateJdbc: false,
|
||||
autoUpdateMcp: false,
|
||||
autoUpdatePlugins: false,
|
||||
});
|
||||
const updates = useComponentUpdates({ isDesktop: true });
|
||||
|
||||
const result = await updates.autoUpdateEnabledComponents();
|
||||
|
||||
expect(updates.totalUpdateCount.value).toBe(4);
|
||||
expect(mocks.upgradeAllAgents).not.toHaveBeenCalled();
|
||||
expect(mocks.installJdbcPlugin).not.toHaveBeenCalled();
|
||||
expect(mocks.installMcpServer).not.toHaveBeenCalled();
|
||||
expect(mocks.installMarketplacePlugin).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ drivers: 0, jdbc: false, mcp: false, plugins: 0, skippedDrivers: 0, failed: [] });
|
||||
});
|
||||
|
||||
it("installs every manually selected category when no DBX update exists and automatic updates are disabled", async () => {
|
||||
Object.assign(settings.editorSettings, {
|
||||
autoUpdateDrivers: false,
|
||||
autoUpdateJdbc: false,
|
||||
autoUpdateMcp: false,
|
||||
autoUpdatePlugins: false,
|
||||
});
|
||||
const updates = useComponentUpdates({ isDesktop: true });
|
||||
|
||||
const result = await updates.installCategories(["drivers", "jdbc", "mcp", "plugins"]);
|
||||
|
||||
expect(mocks.upgradeAllAgents).toHaveBeenCalledOnce();
|
||||
expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce();
|
||||
expect(mocks.installMcpServer).toHaveBeenCalledOnce();
|
||||
expect(mocks.installMarketplacePlugin).toHaveBeenCalledOnce();
|
||||
expect(result).toEqual({ drivers: 1, jdbc: true, mcp: true, plugins: 1, skippedDrivers: 0, failed: [] });
|
||||
});
|
||||
|
||||
it("resumes a persisted manual update-all plan after restart while automatic updates are disabled", async () => {
|
||||
Object.assign(settings.editorSettings, {
|
||||
autoUpdateDrivers: false,
|
||||
autoUpdateJdbc: false,
|
||||
autoUpdateMcp: false,
|
||||
autoUpdatePlugins: false,
|
||||
});
|
||||
const updates = useComponentUpdates({ isDesktop: true });
|
||||
const pending: PendingComponentUpdates = {
|
||||
fromVersion: "0.6.16",
|
||||
targetVersion: "0.6.17",
|
||||
plan: { kind: "manual", categories: ["drivers", "jdbc", "mcp", "plugins"] },
|
||||
};
|
||||
|
||||
const result = await runPendingComponentUpdatePlan(pending, updates);
|
||||
|
||||
expect(mocks.upgradeAllAgents).toHaveBeenCalledOnce();
|
||||
expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce();
|
||||
expect(mocks.installMcpServer).toHaveBeenCalledOnce();
|
||||
expect(mocks.installMarketplacePlugin).toHaveBeenCalledOnce();
|
||||
expect(result).toEqual({ drivers: 1, jdbc: true, mcp: true, plugins: 1, skippedDrivers: 0, failed: [] });
|
||||
});
|
||||
|
||||
it("skips blocked agent updates while allowing other component updates", async () => {
|
||||
mocks.checkAgentUpdateBlockers.mockResolvedValue([{ db_type: "mysql", label: "MySQL" }]);
|
||||
const updates = useComponentUpdates({ isDesktop: true });
|
||||
|
||||
const result = await updates.autoUpdateEnabledComponents();
|
||||
|
||||
expect(mocks.upgradeAllAgents).not.toHaveBeenCalled();
|
||||
expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce();
|
||||
expect(mocks.installMcpServer).toHaveBeenCalledOnce();
|
||||
expect(mocks.installMarketplacePlugin).toHaveBeenCalledOnce();
|
||||
expect(result.skippedDrivers).toBe(1);
|
||||
});
|
||||
|
||||
it("does not install from stale state when update detection fails", async () => {
|
||||
mocks.listInstalledAgents.mockRejectedValue(new Error("registry unavailable"));
|
||||
mocks.jdbcPluginStatus.mockRejectedValue(new Error("JDBC registry unavailable"));
|
||||
mocks.checkMcpServerStatus.mockRejectedValue(new Error("MCP registry unavailable"));
|
||||
mocks.listPlugins.mockRejectedValue(new Error("plugin store unavailable"));
|
||||
mocks.fetchPluginMarketplaceCatalogs.mockRejectedValue(new Error("catalog unavailable"));
|
||||
const updates = useComponentUpdates({ isDesktop: true });
|
||||
|
||||
const result = await updates.autoUpdateEnabledComponents();
|
||||
|
||||
expect(result.failed).toEqual(["drivers: registry unavailable; JDBC: JDBC registry unavailable; MCP: MCP registry unavailable; plugins: plugin store unavailable; plugin catalog: catalog unavailable"]);
|
||||
expect(mocks.upgradeAllAgents).not.toHaveBeenCalled();
|
||||
expect(mocks.installJdbcPlugin).not.toHaveBeenCalled();
|
||||
expect(mocks.installMcpServer).not.toHaveBeenCalled();
|
||||
expect(mocks.installMarketplacePlugin).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("continues independent updates when the plugin catalog is unavailable", async () => {
|
||||
mocks.fetchPluginMarketplaceCatalogs.mockRejectedValue(new Error("catalog unavailable"));
|
||||
const updates = useComponentUpdates({ isDesktop: true });
|
||||
|
||||
const result = await updates.autoUpdateEnabledComponents();
|
||||
|
||||
expect(mocks.upgradeAllAgents).toHaveBeenCalledOnce();
|
||||
expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce();
|
||||
expect(mocks.installMcpServer).toHaveBeenCalledOnce();
|
||||
expect(mocks.installMarketplacePlugin).not.toHaveBeenCalled();
|
||||
expect(result.failed).toEqual(["plugin catalog: catalog unavailable"]);
|
||||
});
|
||||
|
||||
it("allows a category to be installed manually even when automatic updates are disabled", async () => {
|
||||
Object.assign(settings.editorSettings, {
|
||||
autoUpdateDrivers: false,
|
||||
autoUpdateJdbc: false,
|
||||
autoUpdateMcp: false,
|
||||
autoUpdatePlugins: false,
|
||||
});
|
||||
const updates = useComponentUpdates({ isDesktop: true });
|
||||
await updates.refresh();
|
||||
|
||||
const result = await updates.installCategory("jdbc");
|
||||
|
||||
expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce();
|
||||
expect(result).toEqual({ drivers: 0, jdbc: true, mcp: false, plugins: 0, skippedDrivers: 0, failed: [] });
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import type { UpdateDownloadProgress } from "@/lib/backend/tauri";
|
||||
import { currentLocale } from "@/i18n";
|
||||
import { shouldBlockAppUpdate } from "@/lib/app/appUpdateTaskGuard";
|
||||
import { uuid } from "@/lib/common/utils";
|
||||
import { isUpdatePreviewMockEnabled, previewAppUpdateInfo } from "@/lib/updates/updatePreviewMock";
|
||||
|
||||
interface UseAppUpdaterOptions {
|
||||
getActiveTaskCount?: () => number;
|
||||
@@ -134,14 +135,8 @@ export function useAppUpdater(options: UseAppUpdaterOptions = {}) {
|
||||
const isInstallingUpdate = computed(() => phase.value === "installing" || isPreparingUpdate.value);
|
||||
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 && (!autoDownloadEnabled.value || !isTauriRuntime() || updateInfo.value.manual_update_only))) &&
|
||||
!isUpdateIgnored(updateInfo.value, settingsStore.editorSettings.ignoredUpdateVersion),
|
||||
);
|
||||
const autoUpdateEnabled = computed(() => settingsStore.editorSettings.autoUpdateApp !== false);
|
||||
const hasUpdateAvailable = computed(() => (updateDownloaded.value || updateReady.value || updateInfo.value?.update_available === true) && !isUpdateIgnored(updateInfo.value, settingsStore.editorSettings.ignoredUpdateVersion));
|
||||
const latestReleaseUrl = "https://github.com/t8y2/dbx/releases/latest";
|
||||
let generation = 0;
|
||||
let activeDownload: Promise<void> | undefined;
|
||||
@@ -170,7 +165,7 @@ export function useAppUpdater(options: UseAppUpdaterOptions = {}) {
|
||||
}
|
||||
function scheduleRetry() {
|
||||
clearRetry();
|
||||
if (!initialized || disposed || !notificationsEnabled.value || retries >= 3) return;
|
||||
if (!initialized || disposed || retries >= 3) return;
|
||||
const delay = [60_000, 300_000, 900_000][retries++];
|
||||
retryTimer = setTimeout(() => {
|
||||
retryTimer = undefined;
|
||||
@@ -220,12 +215,18 @@ export function useAppUpdater(options: UseAppUpdaterOptions = {}) {
|
||||
if (!checkOptions.silent) showUpdateDialog.value = true;
|
||||
// A downloaded-but-uninstalled update keeps the app in the ready phase; checks
|
||||
// continue so a newer release can replace the cached package.
|
||||
if ((phase.value !== "idle" && phase.value !== "ready") || (checkOptions.silent && !notificationsEnabled.value)) return;
|
||||
if (phase.value !== "idle" && phase.value !== "ready") return;
|
||||
clearRetry();
|
||||
const token = ++generation;
|
||||
phase.value = "checking";
|
||||
clearError();
|
||||
try {
|
||||
if (isUpdatePreviewMockEnabled()) {
|
||||
if (token !== generation || disposed) return;
|
||||
updateInfo.value = previewAppUpdateInfo(updateInfo.value?.current_version || "");
|
||||
phase.value = "idle";
|
||||
return;
|
||||
}
|
||||
const info = await api.checkForUpdates(currentLocale(), normalizeUpdateDownloadSource(settingsStore.editorSettings.updateDownloadSource));
|
||||
if (token !== generation || disposed) return;
|
||||
updateInfo.value = info;
|
||||
@@ -237,12 +238,12 @@ export function useAppUpdater(options: UseAppUpdaterOptions = {}) {
|
||||
const cached = downloaded.value;
|
||||
if (cached && !isNewerRemoteVersion(info.latest_version, cached.version)) {
|
||||
// Keep a prepared package installable without replacing it automatically.
|
||||
if (!autoDownloadEnabled.value || !notificationsEnabled.value) setDownloaded(cached);
|
||||
if (!autoUpdateEnabled.value) setDownloaded(cached);
|
||||
return;
|
||||
}
|
||||
// A cached package older than the remote release must not mask the newer version.
|
||||
if (cached && !(await discardSupersededUpdate(cached))) return;
|
||||
if (!autoDownloadEnabled.value || !notificationsEnabled.value) return;
|
||||
if (!autoUpdateEnabled.value) return;
|
||||
await downloadUpdateInBackground(true);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -269,7 +270,7 @@ export function useAppUpdater(options: UseAppUpdaterOptions = {}) {
|
||||
}
|
||||
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;
|
||||
if (automatic && !autoUpdateEnabled.value) return;
|
||||
automaticDownload = automatic;
|
||||
const version = updateInfo.value!.latest_version;
|
||||
const token = ++generation;
|
||||
@@ -457,26 +458,17 @@ export function useAppUpdater(options: UseAppUpdaterOptions = {}) {
|
||||
}, 3_600_000);
|
||||
void checkUpdates({ silent: true });
|
||||
}
|
||||
const stopSettingsWatch = watch(notificationsEnabled, (enabled) => {
|
||||
if (!initialized || disposed) return;
|
||||
const settle = enabled ? (cancelOperation ?? Promise.resolve()) : cancelDownload();
|
||||
void settle
|
||||
.then(() => {
|
||||
if (notificationsEnabled.value && !disposed) void checkUpdates({ silent: true });
|
||||
})
|
||||
.catch(fail);
|
||||
});
|
||||
const stopAutoDownloadWatch = watch(autoDownloadEnabled, (enabled) => {
|
||||
const stopSettingsWatch = watch(autoUpdateEnabled, (enabled) => {
|
||||
if (disposed) return;
|
||||
if (!enabled) {
|
||||
clearRetry();
|
||||
if (automaticDownload) void cancelDownload().catch(fail);
|
||||
return;
|
||||
}
|
||||
if (!initialized || !notificationsEnabled.value) return;
|
||||
if (!initialized) return;
|
||||
void (cancelOperation ?? Promise.resolve())
|
||||
.then(() => {
|
||||
if (autoDownloadEnabled.value && notificationsEnabled.value && !disposed) void checkUpdates({ silent: true });
|
||||
if (autoUpdateEnabled.value && !disposed) void checkUpdates({ silent: true });
|
||||
})
|
||||
.catch(fail);
|
||||
});
|
||||
@@ -485,7 +477,6 @@ export function useAppUpdater(options: UseAppUpdaterOptions = {}) {
|
||||
clearRetry();
|
||||
clearInterval(hourlyTimer);
|
||||
stopSettingsWatch();
|
||||
stopAutoDownloadWatch();
|
||||
updatePreparationRelease?.();
|
||||
updatePreparationRelease = undefined;
|
||||
void cancelDownload().catch(() => {});
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
import { computed, ref } from "vue";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { currentLocale } from "@/i18n";
|
||||
import { buildMarketplacePluginListings, type MarketplacePluginListing } from "@/lib/plugins/pluginMarketplace";
|
||||
import { mcpUpdateAvailability } from "@/lib/mcp/mcpUpdateStatus";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { isUpdatePreviewMockEnabled, previewDriverUpdates, previewJdbcUpdate, previewMcpUpdate, previewPluginUpdates } from "@/lib/updates/updatePreviewMock";
|
||||
import type { ComponentUpdateCategory } from "@/lib/updates/componentUpdateOrchestration";
|
||||
import type { AgentDriverInfo, McpServerStatus } from "@/lib/backend/tauri";
|
||||
import type { JdbcPluginStatus } from "@/types/database";
|
||||
|
||||
export type { ComponentUpdateCategory } from "@/lib/updates/componentUpdateOrchestration";
|
||||
|
||||
export interface ComponentUpdateResult {
|
||||
drivers: number;
|
||||
jdbc: boolean;
|
||||
mcp: boolean;
|
||||
plugins: number;
|
||||
skippedDrivers: number;
|
||||
failed: string[];
|
||||
}
|
||||
|
||||
function emptyResult(): ComponentUpdateResult {
|
||||
return { drivers: 0, jdbc: false, mcp: false, plugins: 0, skippedDrivers: 0, failed: [] };
|
||||
}
|
||||
|
||||
export function useComponentUpdates(options: { isDesktop: boolean }) {
|
||||
const settingsStore = useSettingsStore();
|
||||
const drivers = ref<AgentDriverInfo[]>([]);
|
||||
const jdbcPluginStatus = ref<JdbcPluginStatus | null>(null);
|
||||
const mcpStatus = ref<McpServerStatus | null>(null);
|
||||
const pluginListings = ref<MarketplacePluginListing[]>([]);
|
||||
const loading = ref(false);
|
||||
const updating = ref(false);
|
||||
const updatingCategory = ref<ComponentUpdateCategory | null>(null);
|
||||
const lastError = ref("");
|
||||
|
||||
const driverUpdates = computed(() => drivers.value.filter((driver) => driver.update_available));
|
||||
const pluginUpdates = computed(() => pluginListings.value.filter((listing) => listing.status === "update"));
|
||||
const driverUpdateCount = computed(() => driverUpdates.value.length);
|
||||
const jdbcUpdateAvailable = computed(() => jdbcPluginStatus.value?.update_available === true);
|
||||
const mcpUpdateAvailable = computed(() => (mcpStatus.value ? mcpUpdateAvailability(mcpStatus.value) === true : false));
|
||||
const pluginUpdateCount = computed(() => pluginUpdates.value.length);
|
||||
const totalUpdateCount = computed(() => driverUpdateCount.value + (jdbcUpdateAvailable.value ? 1 : 0) + (mcpUpdateAvailable.value ? 1 : 0) + pluginUpdateCount.value);
|
||||
|
||||
let refreshPromise: Promise<boolean> | null = null;
|
||||
let activeUpdatePromise: Promise<ComponentUpdateResult> | null = null;
|
||||
|
||||
function refresh() {
|
||||
if (!options.isDesktop) return Promise.resolve(false);
|
||||
if (refreshPromise) return refreshPromise;
|
||||
refreshPromise = (async () => {
|
||||
loading.value = true;
|
||||
lastError.value = "";
|
||||
try {
|
||||
if (isUpdatePreviewMockEnabled()) {
|
||||
drivers.value = previewDriverUpdates();
|
||||
jdbcPluginStatus.value = previewJdbcUpdate();
|
||||
mcpStatus.value = previewMcpUpdate();
|
||||
pluginListings.value = previewPluginUpdates();
|
||||
return true;
|
||||
}
|
||||
const [agentResult, jdbcResult, mcpResult, installedPluginsResult, catalogsResult] = await Promise.allSettled([api.listInstalledAgents(), api.jdbcPluginStatus(), api.checkMcpServerStatus(), api.listPlugins(), api.fetchPluginMarketplaceCatalogs()]);
|
||||
const errors: string[] = [];
|
||||
if (agentResult.status === "fulfilled") drivers.value = agentResult.value;
|
||||
else {
|
||||
drivers.value = [];
|
||||
errors.push(`drivers: ${agentResult.reason instanceof Error ? agentResult.reason.message : String(agentResult.reason)}`);
|
||||
}
|
||||
if (jdbcResult.status === "fulfilled") jdbcPluginStatus.value = jdbcResult.value;
|
||||
else {
|
||||
jdbcPluginStatus.value = null;
|
||||
errors.push(`JDBC: ${jdbcResult.reason instanceof Error ? jdbcResult.reason.message : String(jdbcResult.reason)}`);
|
||||
}
|
||||
if (mcpResult.status === "fulfilled") mcpStatus.value = mcpResult.value;
|
||||
else {
|
||||
mcpStatus.value = null;
|
||||
errors.push(`MCP: ${mcpResult.reason instanceof Error ? mcpResult.reason.message : String(mcpResult.reason)}`);
|
||||
}
|
||||
if (installedPluginsResult.status === "fulfilled" && catalogsResult.status === "fulfilled") {
|
||||
pluginListings.value = buildMarketplacePluginListings(catalogsResult.value, installedPluginsResult.value, currentLocale());
|
||||
} else {
|
||||
pluginListings.value = [];
|
||||
if (installedPluginsResult.status === "rejected") errors.push(`plugins: ${installedPluginsResult.reason instanceof Error ? installedPluginsResult.reason.message : String(installedPluginsResult.reason)}`);
|
||||
if (catalogsResult.status === "rejected") errors.push(`plugin catalog: ${catalogsResult.reason instanceof Error ? catalogsResult.reason.message : String(catalogsResult.reason)}`);
|
||||
}
|
||||
lastError.value = errors.join("; ");
|
||||
return errors.length === 0;
|
||||
} catch (error) {
|
||||
lastError.value = error instanceof Error ? error.message : String(error);
|
||||
return false;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
refreshPromise = null;
|
||||
}
|
||||
})();
|
||||
return refreshPromise;
|
||||
}
|
||||
|
||||
async function updateDrivers(result: ComponentUpdateResult) {
|
||||
const updatable = driverUpdates.value.map((driver) => driver.db_type);
|
||||
if (!updatable.length) return;
|
||||
if (isUpdatePreviewMockEnabled()) {
|
||||
result.drivers += updatable.length;
|
||||
return;
|
||||
}
|
||||
const blockers = await api.checkAgentUpdateBlockers(updatable);
|
||||
if (blockers.length) {
|
||||
result.skippedDrivers = blockers.length;
|
||||
return;
|
||||
}
|
||||
const upgraded = await api.upgradeAllAgents();
|
||||
result.drivers += upgraded.upgraded;
|
||||
if (upgraded.failed.length) result.failed.push(...upgraded.failed.map((item) => `${item.db_type}: ${item.error}`));
|
||||
}
|
||||
|
||||
async function updateJdbc(result: ComponentUpdateResult) {
|
||||
if (!jdbcUpdateAvailable.value) return;
|
||||
if (isUpdatePreviewMockEnabled()) {
|
||||
result.jdbc = true;
|
||||
return;
|
||||
}
|
||||
await api.installJdbcPlugin();
|
||||
result.jdbc = true;
|
||||
}
|
||||
|
||||
async function updateMcp(result: ComponentUpdateResult) {
|
||||
if (!mcpUpdateAvailable.value) return;
|
||||
if (isUpdatePreviewMockEnabled()) {
|
||||
result.mcp = true;
|
||||
return;
|
||||
}
|
||||
await api.installMcpServer();
|
||||
result.mcp = true;
|
||||
}
|
||||
|
||||
async function updatePlugins(result: ComponentUpdateResult) {
|
||||
const updatable = pluginUpdates.value.filter((item) => item.artifact);
|
||||
if (isUpdatePreviewMockEnabled()) {
|
||||
result.plugins += updatable.length;
|
||||
return;
|
||||
}
|
||||
for (const listing of updatable) {
|
||||
try {
|
||||
await api.installMarketplacePlugin({
|
||||
repositoryId: listing.repository.id,
|
||||
pluginId: listing.plugin.id,
|
||||
version: listing.plugin.latestVersion,
|
||||
});
|
||||
result.plugins += 1;
|
||||
} catch (error) {
|
||||
result.failed.push(`${listing.name}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function categoryLabel(category: ComponentUpdateCategory): string {
|
||||
return category === "jdbc" ? "JDBC" : category === "mcp" ? "MCP" : category;
|
||||
}
|
||||
|
||||
async function runCategory(category: ComponentUpdateCategory, result: ComponentUpdateResult) {
|
||||
updatingCategory.value = category;
|
||||
try {
|
||||
if (category === "drivers") await updateDrivers(result);
|
||||
else if (category === "jdbc") await updateJdbc(result);
|
||||
else if (category === "mcp") await updateMcp(result);
|
||||
else await updatePlugins(result);
|
||||
} catch (error) {
|
||||
result.failed.push(`${categoryLabel(category)}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function runUpdateOperation(categories: ComponentUpdateCategory[]): Promise<ComponentUpdateResult> {
|
||||
const result = emptyResult();
|
||||
if (!options.isDesktop) return result;
|
||||
if (activeUpdatePromise) return activeUpdatePromise;
|
||||
updating.value = true;
|
||||
const operation = (async () => {
|
||||
await refresh();
|
||||
const initialCheckError = lastError.value;
|
||||
if (initialCheckError) result.failed.push(initialCheckError);
|
||||
for (const category of categories) await runCategory(category, result);
|
||||
await refresh();
|
||||
if (lastError.value && lastError.value !== initialCheckError) result.failed.push(lastError.value);
|
||||
return result;
|
||||
})();
|
||||
activeUpdatePromise = operation;
|
||||
try {
|
||||
return await operation;
|
||||
} finally {
|
||||
if (activeUpdatePromise === operation) activeUpdatePromise = null;
|
||||
updating.value = false;
|
||||
updatingCategory.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function installCategory(category: ComponentUpdateCategory): Promise<ComponentUpdateResult> {
|
||||
return runUpdateOperation([category]);
|
||||
}
|
||||
|
||||
function installCategories(categories: ComponentUpdateCategory[]): Promise<ComponentUpdateResult> {
|
||||
return runUpdateOperation([...new Set(categories)]);
|
||||
}
|
||||
|
||||
function autoUpdateEnabledComponents(): Promise<ComponentUpdateResult> {
|
||||
const categories: ComponentUpdateCategory[] = [];
|
||||
if (settingsStore.editorSettings.autoUpdateDrivers) categories.push("drivers");
|
||||
if (settingsStore.editorSettings.autoUpdateJdbc) categories.push("jdbc");
|
||||
if (settingsStore.editorSettings.autoUpdateMcp) categories.push("mcp");
|
||||
if (settingsStore.editorSettings.autoUpdatePlugins) categories.push("plugins");
|
||||
return runUpdateOperation(categories);
|
||||
}
|
||||
|
||||
return {
|
||||
drivers,
|
||||
driverUpdates,
|
||||
jdbcPluginStatus,
|
||||
mcpStatus,
|
||||
pluginListings,
|
||||
pluginUpdates,
|
||||
loading,
|
||||
updating,
|
||||
updatingCategory,
|
||||
lastError,
|
||||
driverUpdateCount,
|
||||
jdbcUpdateAvailable,
|
||||
mcpUpdateAvailable,
|
||||
pluginUpdateCount,
|
||||
totalUpdateCount,
|
||||
refresh,
|
||||
installCategory,
|
||||
installCategories,
|
||||
autoUpdateEnabledComponents,
|
||||
};
|
||||
}
|
||||
@@ -341,6 +341,8 @@ export default withEnglishFallback({
|
||||
preparing: "Yeniləmə hazırlanır…",
|
||||
retryDownload: "Endirməni yenidən sınayın",
|
||||
title: "Yeniləmələr",
|
||||
centerTitle: "Yeniləmə mərkəzi",
|
||||
centerAvailable: "{count} yeniləmə mövcuddur",
|
||||
check: "Yeniləmələri yoxla",
|
||||
checking: "Yeniləmələr yoxlanılır…",
|
||||
updateReadyTooltip: "Yeniləmə hazırdır — quraşdırmaq üçün klikləyin",
|
||||
@@ -375,6 +377,15 @@ export default withEnglishFallback({
|
||||
portableHelperFailed: "Daşınabilən yeniləmə köməkçisi işə salına bilmədi: {error}",
|
||||
installFailed: "Yeniləməni quraşdırmaq mümkün olmadı: {error}",
|
||||
restart: "Çıx və yenidən başlat",
|
||||
updateAction: "Yenilə",
|
||||
updateNow: "İndi yenilə",
|
||||
updating: "Yenilənir…",
|
||||
notInstalled: "Quraşdırılmayıb",
|
||||
componentsAutoUpdated: "Avtomatik yeniləndi: {components}.",
|
||||
componentListSeparator: ", ",
|
||||
componentsAutoUpdateFailed: "{count} komponent yeniləməsi uğursuz oldu. Sonra əl ilə yenidən cəhd edin.",
|
||||
updateAll: "Hamısını yenilə",
|
||||
componentsAutoUpdateSkipped: "Bəzi sürücü yeniləmələri aktiv əlaqələr istifadə etdiyi üçün təxirə salındı.",
|
||||
restartFailed: "Tətbiqi yenidən başlatmaq mümkün olmadı: {error}",
|
||||
exitAndUpdate: "Çıx və yenilə",
|
||||
dockerUsersRun: "Docker istifadəçiləri bu əmri işlətməlidir",
|
||||
@@ -6986,9 +6997,38 @@ 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 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.",
|
||||
updateNotificationsEnabledDescription: "Tətbiq və komponent yeniləmələrini avtomatik yoxlayır və xatırlatmalar göstərir. Hər kateqoriyanın avtomatik quraşdırılıb-quraşdırılmayacağı yeniləmə səhifəsindən idarə olunur.",
|
||||
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.",
|
||||
autoDownloadUpdatesDescription: "Köhnə uyğunluq parametri. Artıq yeniləmə səhifəsindəki “DBX-i avtomatik yenilə” ilə eynidir. Quraşdırma üçün hələ də təsdiq tələb olunur.",
|
||||
updatesTab: "Yeniləmə idarəetməsi",
|
||||
updatesDescription: "DBX, verilənlər bazası sürücüləri, JDBC, MCP serveri və plagin yeniləmələrini bir yerdən idarə edin.",
|
||||
autoUpdateApp: "DBX-i avtomatik yenilə",
|
||||
autoUpdateAppDescription: "Yeni DBX versiyalarını arxa planda endirin. Yenidən başlatma və quraşdırma vaxtını siz seçirsiniz.",
|
||||
autoUpdateDrivers: "Verilənlər bazası sürücülərini avtomatik yenilə",
|
||||
autoUpdateDriversDescription: "DBX yenidən başladıqdan sonra aktiv əlaqə mane olmadıqda mövcud sürücü yeniləmələrini quraşdırın.",
|
||||
autoUpdateJdbc: "JDBC-ni avtomatik yenilə",
|
||||
autoUpdateJdbcDescription: "DBX yenidən başladıqdan sonra JDBC plagin mühitini avtomatik yeniləyin.",
|
||||
autoUpdateMcp: "MCP-ni avtomatik yenilə",
|
||||
autoUpdateMcpDescription: "DBX yenidən başladıqdan sonra daha yeni versiya varsa DBX tərəfindən idarə olunan MCP Serveri yeniləyin.",
|
||||
autoUpdatePlugins: "Plaginləri avtomatik yenilə",
|
||||
autoUpdatePluginsDescription: "DBX yenidən başladıqdan sonra imzalanmış marketplace plagin yeniləmələrini quraşdırın.",
|
||||
autoUpdate: "Avtomatik yeniləmə",
|
||||
updateStatusTitle: "Yeniləmə statusu",
|
||||
updateStatusDescription: "Hər komponentin yeniləmə statusuna baxın və avtomatik yenilənib-yenilənməyəcəyini seçin.",
|
||||
checkUpdates: "İndi yoxla",
|
||||
updateAll: "Hamısını yenilə",
|
||||
updateClient: "DBX",
|
||||
updateDrivers: "Verilənlər bazası sürücüləri",
|
||||
updateJdbc: "JDBC",
|
||||
updateMcp: "MCP",
|
||||
updatePlugins: "Plaginlər",
|
||||
updateAvailable: "Yeniləmə mövcuddur",
|
||||
updateCount: "{count} mövcuddur",
|
||||
upToDate: "Ən son versiyadadır",
|
||||
openDriverManager: "Sürücü idarəetməsini aç",
|
||||
openPluginCenter: "Plagin mərkəzini aç",
|
||||
openMcpSettings: "MCP parametrlərini aç",
|
||||
updateRestartHint: "Komponent yeniləmələri DBX uğurla yenilənib yenidən başladıqdan sonra icra olunur. Aktiv əlaqələr sürücü yeniləmələrini təhlükəsiz vaxta qədər təxirə sala bilə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)",
|
||||
|
||||
@@ -484,6 +484,8 @@ export default {
|
||||
preparing: "Preparing update…",
|
||||
retryDownload: "Retry Download",
|
||||
title: "Updates",
|
||||
centerTitle: "Update Center",
|
||||
centerAvailable: "{count} available",
|
||||
check: "Check for updates",
|
||||
checking: "Checking for updates…",
|
||||
updateReadyTooltip: "Update ready — click to install",
|
||||
@@ -518,6 +520,15 @@ export default {
|
||||
portableHelperFailed: "The portable update helper could not start: {error}",
|
||||
installFailed: "Update installation failed: {error}",
|
||||
restart: "Exit & Restart",
|
||||
updateAction: "Update",
|
||||
updateNow: "Update Now",
|
||||
updating: "Updating…",
|
||||
notInstalled: "Not installed",
|
||||
componentsAutoUpdated: "Automatically updated: {components}.",
|
||||
componentListSeparator: ", ",
|
||||
componentsAutoUpdateFailed: "{count} component updates failed. Retry them manually later.",
|
||||
updateAll: "Update all",
|
||||
componentsAutoUpdateSkipped: "Some driver updates were postponed because active connections are using them.",
|
||||
restartFailed: "Failed to restart app: {error}",
|
||||
exitAndUpdate: "Exit & Update",
|
||||
dockerUsersRun: "Docker users should run",
|
||||
@@ -7214,9 +7225,38 @@ export default {
|
||||
closeActionQuit: "Quit DBX",
|
||||
closeActionMinimize: "Minimize to tray",
|
||||
updateNotificationsEnabled: "Enable update reminders",
|
||||
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.",
|
||||
updateNotificationsEnabledDescription: "Automatically check for application and component updates and show reminders. The update page controls whether each category installs automatically.",
|
||||
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.",
|
||||
autoDownloadUpdatesDescription: "Legacy compatibility setting. It now mirrors Automatically update DBX on the Updates page. Installation still requires confirmation.",
|
||||
updatesTab: "Update Management",
|
||||
updatesDescription: "Manage application, database driver, JDBC, MCP server, and plugin updates in one place.",
|
||||
autoUpdateApp: "Automatically update DBX",
|
||||
autoUpdateAppDescription: "Download new DBX versions in the background. You choose when to restart and install.",
|
||||
autoUpdateDrivers: "Automatically update database drivers",
|
||||
autoUpdateDriversDescription: "After a DBX restart, install available driver updates when no active connection blocks them.",
|
||||
autoUpdateJdbc: "Automatically update JDBC",
|
||||
autoUpdateJdbcDescription: "After a DBX restart, update the JDBC plugin runtime automatically.",
|
||||
autoUpdateMcp: "Automatically update MCP",
|
||||
autoUpdateMcpDescription: "After a DBX restart, update the managed DBX MCP Server when a newer version is available.",
|
||||
autoUpdatePlugins: "Automatically update plugins",
|
||||
autoUpdatePluginsDescription: "After a DBX restart, install signed marketplace plugin updates.",
|
||||
autoUpdate: "Auto-update",
|
||||
updateStatusTitle: "Update status",
|
||||
updateStatusDescription: "Review each component's update status and choose whether it updates automatically.",
|
||||
checkUpdates: "Check now",
|
||||
updateAll: "Update all",
|
||||
updateClient: "DBX",
|
||||
updateDrivers: "Database drivers",
|
||||
updateJdbc: "JDBC",
|
||||
updateMcp: "MCP",
|
||||
updatePlugins: "Plugins",
|
||||
updateAvailable: "Update available",
|
||||
updateCount: "{count} available",
|
||||
upToDate: "Up to date",
|
||||
openDriverManager: "Open Driver Manager",
|
||||
openPluginCenter: "Open Plugin Center",
|
||||
openMcpSettings: "Open MCP settings",
|
||||
updateRestartHint: "Component updates run after a successful DBX upgrade restart. Active connections can postpone driver updates until it is safe.",
|
||||
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)",
|
||||
|
||||
@@ -475,6 +475,8 @@ export default withEnglishFallback({
|
||||
preparing: "Preparando actualización…",
|
||||
retryDownload: "Reintentar descarga",
|
||||
title: "Actualizaciones",
|
||||
centerTitle: "Centro de actualizaciones",
|
||||
centerAvailable: "{count} disponibles",
|
||||
check: "Buscar actualizaciones",
|
||||
checking: "Buscando actualizaciones…",
|
||||
updateReadyTooltip: "Actualización lista, haz clic para instalar",
|
||||
@@ -506,6 +508,15 @@ export default withEnglishFallback({
|
||||
portableHelperFailed: "No se pudo iniciar el asistente de actualización portátil: {error}",
|
||||
installFailed: "Error al instalar la actualización: {error}",
|
||||
restart: "Salir y reiniciar",
|
||||
updateAction: "Actualizar",
|
||||
updateNow: "Actualizar ahora",
|
||||
updating: "Actualizando…",
|
||||
notInstalled: "No instalado",
|
||||
componentsAutoUpdated: "Actualizados automáticamente: {components}.",
|
||||
componentListSeparator: ", ",
|
||||
componentsAutoUpdateFailed: "{count} actualizaciones de componentes fallaron. Vuelve a intentarlas manualmente más tarde.",
|
||||
updateAll: "Actualizar todo",
|
||||
componentsAutoUpdateSkipped: "Algunas actualizaciones de controladores se aplazaron porque hay conexiones activas usándolos.",
|
||||
restartFailed: "Error al reiniciar la aplicación: {error}",
|
||||
exitAndUpdate: "Salir y actualizar",
|
||||
dockerUsersRun: "Los usuarios de Docker deben ejecutar",
|
||||
@@ -6766,9 +6777,38 @@ export default withEnglishFallback({
|
||||
closeActionQuit: "Salir de DBX",
|
||||
closeActionMinimize: "Minimizar a la bandeja",
|
||||
updateNotificationsEnabled: "Activar recordatorios de actualización",
|
||||
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.",
|
||||
updateNotificationsEnabledDescription: "Comprueba automáticamente las actualizaciones de la aplicación y los componentes y muestra recordatorios. La página de actualizaciones controla si cada categoría se instala automáticamente.",
|
||||
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.",
|
||||
autoDownloadUpdatesDescription: "Ajuste de compatibilidad heredado. Ahora refleja «Actualizar DBX automáticamente» en la página de actualizaciones. La instalación aún requiere confirmación.",
|
||||
updatesTab: "Gestión de actualizaciones",
|
||||
updatesDescription: "Gestiona en un solo lugar las actualizaciones de la aplicación, controladores de bases de datos, JDBC, servidor MCP y plugins.",
|
||||
autoUpdateApp: "Actualizar DBX automáticamente",
|
||||
autoUpdateAppDescription: "Descarga nuevas versiones de DBX en segundo plano. Tú eliges cuándo reiniciar e instalar.",
|
||||
autoUpdateDrivers: "Actualizar automáticamente los controladores de bases de datos",
|
||||
autoUpdateDriversDescription: "Tras reiniciar DBX, instala las actualizaciones de controladores disponibles cuando ninguna conexión activa las bloquee.",
|
||||
autoUpdateJdbc: "Actualizar JDBC automáticamente",
|
||||
autoUpdateJdbcDescription: "Tras reiniciar DBX, actualiza automáticamente el entorno de plugins JDBC.",
|
||||
autoUpdateMcp: "Actualizar MCP automáticamente",
|
||||
autoUpdateMcpDescription: "Tras reiniciar DBX, actualiza el servidor MCP gestionado por DBX cuando haya una versión más reciente.",
|
||||
autoUpdatePlugins: "Actualizar plugins automáticamente",
|
||||
autoUpdatePluginsDescription: "Tras reiniciar DBX, instala las actualizaciones firmadas de plugins del mercado.",
|
||||
autoUpdate: "Actualización automática",
|
||||
updateStatusTitle: "Estado de actualizaciones",
|
||||
updateStatusDescription: "Revisa el estado de actualización de cada componente y elige si se actualiza automáticamente.",
|
||||
checkUpdates: "Comprobar ahora",
|
||||
updateAll: "Actualizar todo",
|
||||
updateClient: "DBX",
|
||||
updateDrivers: "Controladores de bases de datos",
|
||||
updateJdbc: "JDBC",
|
||||
updateMcp: "MCP",
|
||||
updatePlugins: "Plugins",
|
||||
updateAvailable: "Actualización disponible",
|
||||
updateCount: "{count} disponibles",
|
||||
upToDate: "Actualizado",
|
||||
openDriverManager: "Abrir el administrador de controladores",
|
||||
openPluginCenter: "Abrir el centro de plugins",
|
||||
openMcpSettings: "Abrir la configuración de MCP",
|
||||
updateRestartHint: "Las actualizaciones de componentes se ejecutan después de reiniciar DBX tras una actualización correcta. Las conexiones activas pueden posponer las actualizaciones de controladores hasta que sea seguro.",
|
||||
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)",
|
||||
|
||||
@@ -474,6 +474,8 @@ export default withEnglishFallback({
|
||||
preparing: "Preparazione aggiornamento…",
|
||||
retryDownload: "Riprova download",
|
||||
title: "Aggiornamenti",
|
||||
centerTitle: "Centro aggiornamenti",
|
||||
centerAvailable: "{count} disponibili",
|
||||
check: "Verifica aggiornamenti",
|
||||
checking: "Verifica degli aggiornamenti in corso…",
|
||||
updateReadyTooltip: "Aggiornamento pronto, clicca per installare",
|
||||
@@ -505,6 +507,15 @@ export default withEnglishFallback({
|
||||
portableHelperFailed: "Impossibile avviare l'helper di aggiornamento portatile: {error}",
|
||||
installFailed: "Installazione dell'aggiornamento non riuscita: {error}",
|
||||
restart: "Esci e Riavvia",
|
||||
updateAction: "Aggiorna",
|
||||
updateNow: "Aggiorna ora",
|
||||
updating: "Aggiornamento…",
|
||||
notInstalled: "Non installato",
|
||||
componentsAutoUpdated: "Aggiornati automaticamente: {components}.",
|
||||
componentListSeparator: ", ",
|
||||
componentsAutoUpdateFailed: "{count} aggiornamenti dei componenti non sono riusciti. Riprova manualmente più tardi.",
|
||||
updateAll: "Aggiorna tutto",
|
||||
componentsAutoUpdateSkipped: "Alcuni aggiornamenti dei driver sono stati posticipati perché sono in uso da connessioni attive.",
|
||||
restartFailed: "Impossibile riavviare l'app: {error}",
|
||||
exitAndUpdate: "Esci e Aggiorna",
|
||||
dockerUsersRun: "Gli utenti Docker dovrebbero eseguire",
|
||||
@@ -6766,9 +6777,38 @@ export default withEnglishFallback({
|
||||
closeActionQuit: "Esci da DBX",
|
||||
closeActionMinimize: "Riduci a icona",
|
||||
updateNotificationsEnabled: "Abilita promemoria aggiornamenti",
|
||||
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.",
|
||||
updateNotificationsEnabledDescription: "Controlla automaticamente gli aggiornamenti dell'app e dei componenti e mostra promemoria. La pagina Aggiornamenti determina se ogni categoria viene installata automaticamente.",
|
||||
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.",
|
||||
autoDownloadUpdatesDescription: "Impostazione di compatibilità precedente. Ora corrisponde ad «Aggiorna automaticamente DBX» nella pagina Aggiornamenti. L'installazione richiede comunque conferma.",
|
||||
updatesTab: "Gestione aggiornamenti",
|
||||
updatesDescription: "Gestisci in un unico posto gli aggiornamenti di applicazione, driver di database, JDBC, server MCP e plugin.",
|
||||
autoUpdateApp: "Aggiorna automaticamente DBX",
|
||||
autoUpdateAppDescription: "Scarica le nuove versioni di DBX in background. Scegli tu quando riavviare e installare.",
|
||||
autoUpdateDrivers: "Aggiorna automaticamente i driver di database",
|
||||
autoUpdateDriversDescription: "Dopo il riavvio di DBX, installa gli aggiornamenti dei driver disponibili quando nessuna connessione attiva li blocca.",
|
||||
autoUpdateJdbc: "Aggiorna automaticamente JDBC",
|
||||
autoUpdateJdbcDescription: "Dopo il riavvio di DBX, aggiorna automaticamente il runtime dei plugin JDBC.",
|
||||
autoUpdateMcp: "Aggiorna automaticamente MCP",
|
||||
autoUpdateMcpDescription: "Dopo il riavvio di DBX, aggiorna il server MCP gestito da DBX quando è disponibile una versione più recente.",
|
||||
autoUpdatePlugins: "Aggiorna automaticamente i plugin",
|
||||
autoUpdatePluginsDescription: "Dopo il riavvio di DBX, installa gli aggiornamenti firmati dei plugin dal marketplace.",
|
||||
autoUpdate: "Aggiornamento automatico",
|
||||
updateStatusTitle: "Stato aggiornamenti",
|
||||
updateStatusDescription: "Controlla lo stato di aggiornamento di ogni componente e scegli se aggiornarlo automaticamente.",
|
||||
checkUpdates: "Controlla ora",
|
||||
updateAll: "Aggiorna tutto",
|
||||
updateClient: "DBX",
|
||||
updateDrivers: "Driver di database",
|
||||
updateJdbc: "JDBC",
|
||||
updateMcp: "MCP",
|
||||
updatePlugins: "Plugin",
|
||||
updateAvailable: "Aggiornamento disponibile",
|
||||
updateCount: "{count} disponibili",
|
||||
upToDate: "Aggiornato",
|
||||
openDriverManager: "Apri gestione driver",
|
||||
openPluginCenter: "Apri centro plugin",
|
||||
openMcpSettings: "Apri impostazioni MCP",
|
||||
updateRestartHint: "Gli aggiornamenti dei componenti vengono eseguiti dopo il riavvio di DBX al termine di un aggiornamento riuscito. Le connessioni attive possono posticipare gli aggiornamenti dei driver fino a quando è sicuro.",
|
||||
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)",
|
||||
|
||||
@@ -475,6 +475,8 @@ export default withEnglishFallback({
|
||||
preparing: "更新を準備中…",
|
||||
retryDownload: "ダウンロードを再試行",
|
||||
title: "アップデート",
|
||||
centerTitle: "アップデートセンター",
|
||||
centerAvailable: "{count} 件更新可能",
|
||||
check: "アップデートを確認",
|
||||
checking: "アップデートを確認中…",
|
||||
updateReadyTooltip: "アップデートの準備ができました。クリックしてインストール",
|
||||
@@ -506,6 +508,15 @@ export default withEnglishFallback({
|
||||
portableHelperFailed: "ポータブル版アップデートのヘルパーを起動できませんでした: {error}",
|
||||
installFailed: "アップデートのインストールに失敗しました: {error}",
|
||||
restart: "終了して再起動",
|
||||
updateAction: "更新",
|
||||
updateNow: "今すぐ更新",
|
||||
updating: "更新中…",
|
||||
notInstalled: "未インストール",
|
||||
componentsAutoUpdated: "自動更新しました: {components}。",
|
||||
componentListSeparator: "、",
|
||||
componentsAutoUpdateFailed: "{count} 件のコンポーネント更新に失敗しました。後で手動で再試行してください。",
|
||||
updateAll: "すべて更新",
|
||||
componentsAutoUpdateSkipped: "アクティブな接続で使用されているため、一部のドライバー更新を延期しました。",
|
||||
restartFailed: "アプリの再起動に失敗しました: {error}",
|
||||
exitAndUpdate: "終了してアップデート",
|
||||
dockerUsersRun: "Dockerユーザーは以下を実行してください",
|
||||
@@ -6804,9 +6815,38 @@ export default withEnglishFallback({
|
||||
closeActionQuit: "DBXを終了",
|
||||
closeActionMinimize: "トレイに最小化",
|
||||
updateNotificationsEnabled: "アップデート通知を有効にする",
|
||||
updateNotificationsEnabledDescription: "アプリとドライバーの更新を自動確認し、通知を表示します。無効にすると自動確認、自動ダウンロード、更新バッジが停止します。手動確認は可能です。",
|
||||
updateNotificationsEnabledDescription: "アプリとコンポーネントの更新を自動確認し、通知を表示します。各カテゴリを自動インストールするかどうかは更新ページで管理します。",
|
||||
autoDownloadUpdates: "アプリの更新を自動ダウンロード",
|
||||
autoDownloadUpdatesDescription: "更新通知が有効な場合、更新パッケージを自動ダウンロードします。インストールには確認が必要です。初期設定はオフです。手動ダウンロードは可能で、ダウンロード済みの更新は保持されます。",
|
||||
autoDownloadUpdatesDescription: "旧バージョンとの互換設定です。更新ページの「DBX を自動更新」と同じ設定になります。インストールには引き続き確認が必要です。",
|
||||
updatesTab: "更新管理",
|
||||
updatesDescription: "DBX、データベースドライバー、JDBC、MCP サーバー、プラグインの更新を一か所で管理します。",
|
||||
autoUpdateApp: "DBX を自動更新",
|
||||
autoUpdateAppDescription: "新しい DBX バージョンをバックグラウンドでダウンロードします。再起動とインストールのタイミングは選択できます。",
|
||||
autoUpdateDrivers: "データベースドライバーを自動更新",
|
||||
autoUpdateDriversDescription: "DBX の再起動後、アクティブな接続で使用されていないドライバー更新をインストールします。",
|
||||
autoUpdateJdbc: "JDBC を自動更新",
|
||||
autoUpdateJdbcDescription: "DBX の再起動後、JDBC プラグインランタイムを自動的に更新します。",
|
||||
autoUpdateMcp: "MCP を自動更新",
|
||||
autoUpdateMcpDescription: "DBX の再起動後、新しいバージョンがある場合は DBX が管理する MCP Server を更新します。",
|
||||
autoUpdatePlugins: "プラグインを自動更新",
|
||||
autoUpdatePluginsDescription: "DBX の再起動後、署名済みマーケットプレイスプラグインの更新をインストールします。",
|
||||
autoUpdate: "自動更新",
|
||||
updateStatusTitle: "更新状態",
|
||||
updateStatusDescription: "各コンポーネントの更新状態を確認し、自動更新するかどうかを選択します。",
|
||||
checkUpdates: "今すぐ確認",
|
||||
updateAll: "すべて更新",
|
||||
updateClient: "DBX",
|
||||
updateDrivers: "データベースドライバー",
|
||||
updateJdbc: "JDBC",
|
||||
updateMcp: "MCP",
|
||||
updatePlugins: "プラグイン",
|
||||
updateAvailable: "更新があります",
|
||||
updateCount: "{count} 件更新可能",
|
||||
upToDate: "最新です",
|
||||
openDriverManager: "ドライバー管理を開く",
|
||||
openPluginCenter: "プラグインセンターを開く",
|
||||
openMcpSettings: "MCP 設定を開く",
|
||||
updateRestartHint: "コンポーネントの更新は、DBX の更新後に正常に再起動したあとで実行されます。アクティブな接続がある場合、ドライバーの更新は安全になるまで延期されます。",
|
||||
debugLoggingEnabled: "デバッグログを有効にする",
|
||||
debugLoggingEnabledDescription: "トラブルシューティング時に、より詳細なローカル診断ログを記録します。デフォルトでは無効です。",
|
||||
metadataCacheMemoryLimit: "メタデータメモリキャッシュ上限",
|
||||
|
||||
@@ -482,6 +482,8 @@ export default withEnglishFallback({
|
||||
preparing: "업데이트 준비 중…",
|
||||
retryDownload: "다운로드 재시도",
|
||||
title: "업데이트",
|
||||
centerTitle: "업데이트 센터",
|
||||
centerAvailable: "{count}개 사용 가능",
|
||||
check: "업데이트 확인",
|
||||
checking: "업데이트 확인 중…",
|
||||
updateReadyTooltip: "업데이트 준비 완료 — 클릭하여 설치",
|
||||
@@ -513,6 +515,15 @@ export default withEnglishFallback({
|
||||
portableHelperFailed: "포터블 업데이트 도우미를 시작하지 못했습니다: {error}",
|
||||
installFailed: "업데이트 설치에 실패했습니다: {error}",
|
||||
restart: "종료 후 다시 시작",
|
||||
updateAction: "업데이트",
|
||||
updateNow: "지금 업데이트",
|
||||
updating: "업데이트 중…",
|
||||
notInstalled: "설치되지 않음",
|
||||
componentsAutoUpdated: "자동 업데이트 완료: {components}.",
|
||||
componentListSeparator: ", ",
|
||||
componentsAutoUpdateFailed: "구성 요소 업데이트 {count}개가 실패했습니다. 나중에 수동으로 다시 시도하세요.",
|
||||
updateAll: "모두 업데이트",
|
||||
componentsAutoUpdateSkipped: "활성 연결에서 사용 중이어서 일부 드라이버 업데이트를 연기했습니다.",
|
||||
restartFailed: "앱을 다시 시작하는 데 실패했습니다: {error}",
|
||||
exitAndUpdate: "종료 후 업데이트",
|
||||
dockerUsersRun: "Docker 사용자는 다음 명령을 실행하세요",
|
||||
@@ -6526,9 +6537,38 @@ export default withEnglishFallback({
|
||||
closeActionQuit: "DBX 종료",
|
||||
closeActionMinimize: "트레이로 최소화",
|
||||
updateNotificationsEnabled: "업데이트 알림 활성화",
|
||||
updateNotificationsEnabledDescription: "앱과 드라이버 업데이트를 자동 확인하고 알림을 표시합니다. 끄면 자동 확인, 자동 다운로드 및 업데이트 배지가 중지됩니다. 수동 확인은 가능합니다.",
|
||||
updateNotificationsEnabledDescription: "앱과 구성 요소 업데이트를 자동으로 확인하고 알림을 표시합니다. 각 범주의 자동 설치 여부는 업데이트 페이지에서 관리합니다.",
|
||||
autoDownloadUpdates: "앱 업데이트 자동 다운로드",
|
||||
autoDownloadUpdatesDescription: "업데이트 알림이 켜져 있으면 업데이트 패키지를 자동으로 다운로드합니다. 설치 전에는 확인이 필요합니다. 기본값은 꺼짐입니다. 수동 다운로드가 가능하며 기존 다운로드는 유지됩니다.",
|
||||
autoDownloadUpdatesDescription: "이전 버전과의 호환 설정입니다. 이제 업데이트 페이지의 'DBX 자동 업데이트'와 동일하게 동작합니다. 설치 전에는 계속 확인이 필요합니다.",
|
||||
updatesTab: "업데이트 관리",
|
||||
updatesDescription: "앱, 데이터베이스 드라이버, JDBC, MCP 서버 및 플러그인 업데이트를 한곳에서 관리하세요.",
|
||||
autoUpdateApp: "DBX 자동 업데이트",
|
||||
autoUpdateAppDescription: "새 DBX 버전을 백그라운드에서 다운로드합니다. 다시 시작하고 설치할 시점은 직접 선택할 수 있습니다.",
|
||||
autoUpdateDrivers: "데이터베이스 드라이버 자동 업데이트",
|
||||
autoUpdateDriversDescription: "DBX를 다시 시작한 후 활성 연결이 사용하지 않는 드라이버 업데이트를 설치합니다.",
|
||||
autoUpdateJdbc: "JDBC 자동 업데이트",
|
||||
autoUpdateJdbcDescription: "DBX를 다시 시작한 후 JDBC 플러그인 런타임을 자동으로 업데이트합니다.",
|
||||
autoUpdateMcp: "MCP 자동 업데이트",
|
||||
autoUpdateMcpDescription: "DBX를 다시 시작한 후 새 버전이 있으면 DBX에서 관리하는 MCP Server를 업데이트합니다.",
|
||||
autoUpdatePlugins: "플러그인 자동 업데이트",
|
||||
autoUpdatePluginsDescription: "DBX를 다시 시작한 후 서명된 마켓플레이스 플러그인 업데이트를 설치합니다.",
|
||||
autoUpdate: "자동 업데이트",
|
||||
updateStatusTitle: "업데이트 상태",
|
||||
updateStatusDescription: "각 구성 요소의 업데이트 상태를 확인하고 자동 업데이트 여부를 선택하세요.",
|
||||
checkUpdates: "지금 확인",
|
||||
updateAll: "모두 업데이트",
|
||||
updateClient: "DBX",
|
||||
updateDrivers: "데이터베이스 드라이버",
|
||||
updateJdbc: "JDBC",
|
||||
updateMcp: "MCP",
|
||||
updatePlugins: "플러그인",
|
||||
updateAvailable: "업데이트 가능",
|
||||
updateCount: "{count}개 사용 가능",
|
||||
upToDate: "최신 버전",
|
||||
openDriverManager: "드라이버 관리 열기",
|
||||
openPluginCenter: "플러그인 센터 열기",
|
||||
openMcpSettings: "MCP 설정 열기",
|
||||
updateRestartHint: "구성 요소 업데이트는 DBX 업데이트 후 다시 시작이 완료되면 실행됩니다. 활성 연결이 있으면 드라이버 업데이트가 안전한 시점까지 연기될 수 있습니다.",
|
||||
updateDownloadSource: "업데이트 다운로드 소스",
|
||||
updateDownloadSourceDescription: "앱 업데이트, 데이터베이스 에이전트, 드라이버, 관리 JRE를 다운로드할 위치를 선택하세요. 공식 소스를 권장합니다. 중국 본토 네트워크에서는 CNB가 더 빠를 수 있습니다.",
|
||||
updateDownloadSourceOfficial: "공식 소스 (권장)",
|
||||
|
||||
@@ -475,6 +475,8 @@ export default withEnglishFallback({
|
||||
preparing: "Preparando atualização…",
|
||||
retryDownload: "Tentar baixar novamente",
|
||||
title: "Atualizações",
|
||||
centerTitle: "Central de atualizações",
|
||||
centerAvailable: "{count} disponíveis",
|
||||
check: "Verificar atualizações",
|
||||
checking: "Verificando atualizações…",
|
||||
updateReadyTooltip: "Atualização pronta, clique para instalar",
|
||||
@@ -506,6 +508,15 @@ export default withEnglishFallback({
|
||||
portableHelperFailed: "Não foi possível iniciar o assistente de atualização portátil: {error}",
|
||||
installFailed: "Falha ao instalar a atualização: {error}",
|
||||
restart: "Sair e Reiniciar",
|
||||
updateAction: "Atualizar",
|
||||
updateNow: "Atualizar agora",
|
||||
updating: "Atualizando…",
|
||||
notInstalled: "Não instalado",
|
||||
componentsAutoUpdated: "Atualizados automaticamente: {components}.",
|
||||
componentListSeparator: ", ",
|
||||
componentsAutoUpdateFailed: "{count} atualizações de componentes falharam. Tente novamente manualmente mais tarde.",
|
||||
updateAll: "Atualizar tudo",
|
||||
componentsAutoUpdateSkipped: "Algumas atualizações de drivers foram adiadas porque há conexões ativas usando os drivers.",
|
||||
restartFailed: "Falha ao reiniciar o aplicativo: {error}",
|
||||
exitAndUpdate: "Sair e Atualizar",
|
||||
dockerUsersRun: "Usuários do Docker devem executar",
|
||||
@@ -6768,9 +6779,38 @@ export default withEnglishFallback({
|
||||
closeActionQuit: "Sair do DBX",
|
||||
closeActionMinimize: "Minimizar para a bandeja",
|
||||
updateNotificationsEnabled: "Ativar lembretes de atualização",
|
||||
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.",
|
||||
updateNotificationsEnabledDescription: "Verifica automaticamente atualizações do aplicativo e dos componentes e mostra lembretes. A página de atualizações controla se cada categoria é instalada automaticamente.",
|
||||
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.",
|
||||
autoDownloadUpdatesDescription: "Configuração de compatibilidade legada. Agora corresponde a 'Atualizar o DBX automaticamente' na página de atualizações. A instalação ainda exige confirmação.",
|
||||
updatesTab: "Gerenciamento de atualizações",
|
||||
updatesDescription: "Gerencie em um só lugar as atualizações do aplicativo, drivers de banco de dados, JDBC, servidor MCP e plugins.",
|
||||
autoUpdateApp: "Atualizar o DBX automaticamente",
|
||||
autoUpdateAppDescription: "Baixa novas versões do DBX em segundo plano. Você escolhe quando reiniciar e instalar.",
|
||||
autoUpdateDrivers: "Atualizar automaticamente os drivers de banco de dados",
|
||||
autoUpdateDriversDescription: "Após reiniciar o DBX, instala as atualizações de drivers disponíveis quando nenhuma conexão ativa as estiver usando.",
|
||||
autoUpdateJdbc: "Atualizar o JDBC automaticamente",
|
||||
autoUpdateJdbcDescription: "Após reiniciar o DBX, atualiza automaticamente o runtime de plugins JDBC.",
|
||||
autoUpdateMcp: "Atualizar o MCP automaticamente",
|
||||
autoUpdateMcpDescription: "Após reiniciar o DBX, atualiza o servidor MCP gerenciado pelo DBX quando houver uma versão mais recente.",
|
||||
autoUpdatePlugins: "Atualizar plugins automaticamente",
|
||||
autoUpdatePluginsDescription: "Após reiniciar o DBX, instala atualizações assinadas de plugins do marketplace.",
|
||||
autoUpdate: "Atualização automática",
|
||||
updateStatusTitle: "Status das atualizações",
|
||||
updateStatusDescription: "Revise o status de atualização de cada componente e escolha se ele deve ser atualizado automaticamente.",
|
||||
checkUpdates: "Verificar agora",
|
||||
updateAll: "Atualizar tudo",
|
||||
updateClient: "DBX",
|
||||
updateDrivers: "Drivers de banco de dados",
|
||||
updateJdbc: "JDBC",
|
||||
updateMcp: "MCP",
|
||||
updatePlugins: "Plugins",
|
||||
updateAvailable: "Atualização disponível",
|
||||
updateCount: "{count} disponíveis",
|
||||
upToDate: "Atualizado",
|
||||
openDriverManager: "Abrir gerenciador de drivers",
|
||||
openPluginCenter: "Abrir central de plugins",
|
||||
openMcpSettings: "Abrir configurações do MCP",
|
||||
updateRestartHint: "As atualizações dos componentes são executadas após o DBX reiniciar com sucesso após uma atualização. Conexões ativas podem adiar as atualizações de drivers até que seja seguro.",
|
||||
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)",
|
||||
|
||||
@@ -343,6 +343,8 @@ export default withEnglishFallback({
|
||||
preparing: "Güncelleme hazırlanıyor…",
|
||||
retryDownload: "İndirmeyi yeniden dene",
|
||||
title: "Güncellemeler",
|
||||
centerTitle: "Güncelleme Merkezi",
|
||||
centerAvailable: "{count} güncelleme mevcut",
|
||||
check: "Güncellemeleri denetle",
|
||||
checking: "Güncellemeler denetleniyor…",
|
||||
updateReadyTooltip: "Güncelleme hazır — kurmak için tıklayın",
|
||||
@@ -377,6 +379,15 @@ export default withEnglishFallback({
|
||||
portableHelperFailed: "Taşınabilir güncelleme yardımcısı başlatılamadı: {error}",
|
||||
installFailed: "Güncelleme kurulumu başarısız: {error}",
|
||||
restart: "Çık ve Yeniden Başlat",
|
||||
updateAction: "Güncelle",
|
||||
updateNow: "Şimdi Güncelle",
|
||||
updating: "Güncelleniyor…",
|
||||
notInstalled: "Kurulu değil",
|
||||
componentsAutoUpdated: "Otomatik güncellendi: {components}.",
|
||||
componentListSeparator: ", ",
|
||||
componentsAutoUpdateFailed: "{count} bileşen güncellemesi başarısız oldu. Daha sonra elle yeniden deneyin.",
|
||||
updateAll: "Tümünü güncelle",
|
||||
componentsAutoUpdateSkipped: "Bazı sürücü güncellemeleri etkin bağlantılar tarafından kullanıldığı için ertelendi.",
|
||||
restartFailed: "Uygulama yeniden başlatılamadı: {error}",
|
||||
exitAndUpdate: "Çık ve Güncelle",
|
||||
dockerUsersRun: "Docker kullanıcıları güncellemek için",
|
||||
@@ -6868,9 +6879,38 @@ 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 ve anımsatıcıları gösterir. Kapatıldığında otomatik kontroller, otomatik indirmeler ve rozetler durur. Elle kontrol kullanılabilir.",
|
||||
updateNotificationsEnabledDescription: "Uygulama ve bileşen güncellemelerini otomatik denetler ve anımsatıcıları gösterir. Her kategorinin otomatik kurulup kurulmayacağı güncelleme sayfasından yönetilir.",
|
||||
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.",
|
||||
autoDownloadUpdatesDescription: "Eski sürümlerle uyumluluk ayarıdır. Artık güncelleme sayfasındaki 'DBX'i otomatik güncelle' ile aynıdır. Kurulum için yine de onay gerekir.",
|
||||
updatesTab: "Güncelleme yönetimi",
|
||||
updatesDescription: "Uygulama, veritabanı sürücüleri, JDBC, MCP sunucusu ve eklenti güncellemelerini tek yerden yönetin.",
|
||||
autoUpdateApp: "DBX'i otomatik güncelle",
|
||||
autoUpdateAppDescription: "Yeni DBX sürümlerini arka planda indirir. Yeniden başlatma ve kurulum zamanını siz seçersiniz.",
|
||||
autoUpdateDrivers: "Veritabanı sürücülerini otomatik güncelle",
|
||||
autoUpdateDriversDescription: "DBX yeniden başlatıldıktan sonra etkin bir bağlantı engellemiyorsa kullanılabilir sürücü güncellemelerini kurar.",
|
||||
autoUpdateJdbc: "JDBC'yi otomatik güncelle",
|
||||
autoUpdateJdbcDescription: "DBX yeniden başlatıldıktan sonra JDBC eklenti çalışma zamanını otomatik günceller.",
|
||||
autoUpdateMcp: "MCP'yi otomatik güncelle",
|
||||
autoUpdateMcpDescription: "DBX yeniden başlatıldıktan sonra daha yeni bir sürüm varsa DBX tarafından yönetilen MCP Server'ı günceller.",
|
||||
autoUpdatePlugins: "Eklentileri otomatik güncelle",
|
||||
autoUpdatePluginsDescription: "DBX yeniden başlatıldıktan sonra imzalı marketplace eklenti güncellemelerini kurar.",
|
||||
autoUpdate: "Otomatik güncelleme",
|
||||
updateStatusTitle: "Güncelleme durumu",
|
||||
updateStatusDescription: "Her bileşenin güncelleme durumunu inceleyin ve otomatik güncellenip güncellenmeyeceğini seçin.",
|
||||
checkUpdates: "Şimdi denetle",
|
||||
updateAll: "Tümünü güncelle",
|
||||
updateClient: "DBX",
|
||||
updateDrivers: "Veritabanı sürücüleri",
|
||||
updateJdbc: "JDBC",
|
||||
updateMcp: "MCP",
|
||||
updatePlugins: "Eklentiler",
|
||||
updateAvailable: "Güncelleme mevcut",
|
||||
updateCount: "{count} güncelleme mevcut",
|
||||
upToDate: "Güncel",
|
||||
openDriverManager: "Sürücü Yöneticisi'ni aç",
|
||||
openPluginCenter: "Eklenti merkezini aç",
|
||||
openMcpSettings: "MCP ayarlarını aç",
|
||||
updateRestartHint: "Bileşen güncellemeleri, DBX başarıyla güncellenip yeniden başlatıldıktan sonra çalıştırılır. Etkin bağlantılar sürücü güncellemelerini güvenli olana kadar erteleyebilir.",
|
||||
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)",
|
||||
|
||||
@@ -400,6 +400,8 @@ export default withEnglishFallback({
|
||||
preparing: "正在准备更新…",
|
||||
retryDownload: "重试下载",
|
||||
title: "更新",
|
||||
centerTitle: "更新中心",
|
||||
centerAvailable: "{count} 项可用更新",
|
||||
check: "检查更新",
|
||||
checking: "正在检查更新…",
|
||||
updateReadyTooltip: "更新已就绪,点击安装",
|
||||
@@ -434,6 +436,15 @@ export default withEnglishFallback({
|
||||
portableHelperFailed: "便携版更新助手启动失败:{error}。便携版更新依赖 Windows PowerShell,可先安装或修复 PowerShell,也可打开下载页手动更新。",
|
||||
installFailed: "更新安装失败:{error}",
|
||||
restart: "退出并重启",
|
||||
updateAction: "更新",
|
||||
updateNow: "立即更新",
|
||||
updating: "正在更新…",
|
||||
notInstalled: "未安装",
|
||||
componentsAutoUpdated: "已自动更新:{components}。",
|
||||
componentListSeparator: "、",
|
||||
componentsAutoUpdateFailed: "有 {count} 项组件自动更新失败,请稍后手动重试。",
|
||||
updateAll: "更新全部",
|
||||
componentsAutoUpdateSkipped: "部分驱动正在被活动连接使用,已暂缓更新。",
|
||||
restartFailed: "重启应用失败:{error}",
|
||||
exitAndUpdate: "退出并更新",
|
||||
dockerUsersRun: "Docker 用户请运行",
|
||||
@@ -7188,9 +7199,38 @@ export default withEnglishFallback({
|
||||
closeActionQuit: "退出程序",
|
||||
closeActionMinimize: "最小化到托盘",
|
||||
updateNotificationsEnabled: "启用更新提醒",
|
||||
updateNotificationsEnabledDescription: "自动检查应用和驱动更新并显示更新提醒。关闭后停止自动检查、自动下载和更新红点;仍可手动检查更新。",
|
||||
updateNotificationsEnabledDescription: "自动检查应用和组件更新并显示更新提醒;是否自动安装由升级页中的各项开关控制。",
|
||||
autoDownloadUpdates: "自动下载应用更新",
|
||||
autoDownloadUpdatesDescription: "启用后,在更新提醒开启时自动下载安装包,安装前仍需确认。默认关闭;关闭后可手动下载,已下载的更新会保留。",
|
||||
autoDownloadUpdatesDescription: "旧版兼容设置;现在与升级页中的“自动更新 DBX”保持一致。安装前仍需确认。",
|
||||
updatesTab: "更新管理",
|
||||
updatesDescription: "在一个页面集中管理 DBX、数据库驱动、JDBC、MCP 服务和插件的更新。",
|
||||
autoUpdateApp: "自动更新 DBX",
|
||||
autoUpdateAppDescription: "在后台下载新的 DBX 版本,重启和安装时机仍由你决定。",
|
||||
autoUpdateDrivers: "自动更新数据库驱动",
|
||||
autoUpdateDriversDescription: "DBX 重启后,在没有活动连接阻塞时安装可用的驱动更新。",
|
||||
autoUpdateJdbc: "自动更新 JDBC",
|
||||
autoUpdateJdbcDescription: "DBX 重启后自动更新 JDBC 插件运行时。",
|
||||
autoUpdateMcp: "自动更新 MCP",
|
||||
autoUpdateMcpDescription: "DBX 重启后,如果有新版本则更新 DBX 管理的 MCP Server。",
|
||||
autoUpdatePlugins: "自动更新插件",
|
||||
autoUpdatePluginsDescription: "DBX 重启后安装已签名的市场插件更新。",
|
||||
autoUpdate: "自动更新",
|
||||
updateStatusTitle: "更新状态",
|
||||
updateStatusDescription: "查看各组件更新状态,并分别设置是否自动更新。",
|
||||
checkUpdates: "立即检查",
|
||||
updateAll: "一键更新",
|
||||
updateClient: "DBX",
|
||||
updateDrivers: "数据库驱动",
|
||||
updateJdbc: "JDBC",
|
||||
updateMcp: "MCP",
|
||||
updatePlugins: "插件",
|
||||
updateAvailable: "有可用更新",
|
||||
updateCount: "{count} 个可更新",
|
||||
upToDate: "已是最新",
|
||||
openDriverManager: "打开驱动管理",
|
||||
openPluginCenter: "打开插件中心",
|
||||
openMcpSettings: "打开 MCP 设置",
|
||||
updateRestartHint: "组件更新会在 DBX 成功升级并重启后执行。活动连接占用驱动时,会暂缓驱动更新直到安全时机。",
|
||||
updateDownloadSource: "更新下载源",
|
||||
updateDownloadSourceDescription: "选择应用更新、数据库 Agent、驱动和托管 JRE 的下载来源。官方源为推荐选项,CNB 适合国内网络环境。",
|
||||
updateDownloadSourceOfficial: "官方源(推荐)",
|
||||
|
||||
@@ -478,6 +478,8 @@ export default withEnglishFallback({
|
||||
preparing: "正在準備更新…",
|
||||
retryDownload: "重試下載",
|
||||
title: "更新",
|
||||
centerTitle: "更新中心",
|
||||
centerAvailable: "{count} 項可用更新",
|
||||
check: "檢查更新",
|
||||
checking: "正在檢查更新…",
|
||||
updateReadyTooltip: "更新已就緒,點擊安裝",
|
||||
@@ -509,6 +511,15 @@ export default withEnglishFallback({
|
||||
portableHelperFailed: "可攜版更新助手啟動失敗:{error}。可攜版更新依賴 Windows PowerShell,可先安裝或修復 PowerShell,也可開啟下載頁手動更新。",
|
||||
installFailed: "更新安裝失敗:{error}",
|
||||
restart: "結束並重新啟動",
|
||||
updateAction: "更新",
|
||||
updateNow: "立即更新",
|
||||
updating: "正在更新…",
|
||||
notInstalled: "未安裝",
|
||||
componentsAutoUpdated: "已自動更新:{components}。",
|
||||
componentListSeparator: "、",
|
||||
componentsAutoUpdateFailed: "有 {count} 項元件自動更新失敗,請稍後手動重試。",
|
||||
updateAll: "更新全部",
|
||||
componentsAutoUpdateSkipped: "部分驅動程式正在被作用中的連線使用,已暫緩更新。",
|
||||
restartFailed: "重新啟動應用程式失敗:{error}",
|
||||
exitAndUpdate: "結束並更新",
|
||||
dockerUsersRun: "Docker 使用者請執行",
|
||||
@@ -6097,9 +6108,38 @@ export default withEnglishFallback({
|
||||
closeActionQuit: "退出程式",
|
||||
closeActionMinimize: "最小化到系統匣",
|
||||
updateNotificationsEnabled: "啟用更新提醒",
|
||||
updateNotificationsEnabledDescription: "自動檢查應用程式和驅動程式更新並顯示提醒。關閉後停止自動檢查、自動下載和更新紅點;仍可手動檢查更新。",
|
||||
updateNotificationsEnabledDescription: "自動檢查應用程式和元件更新並顯示提醒;是否自動安裝由更新頁面中的各項開關控制。",
|
||||
autoDownloadUpdates: "自動下載應用程式更新",
|
||||
autoDownloadUpdatesDescription: "啟用後,在更新提醒開啟時自動下載安裝套件,安裝前仍需確認。預設關閉;關閉後可手動下載,已下載的更新會保留。",
|
||||
autoDownloadUpdatesDescription: "舊版相容設定;現在與更新頁面中的「自動更新 DBX」保持一致。安裝前仍需確認。",
|
||||
updatesTab: "更新管理",
|
||||
updatesDescription: "在同一頁集中管理 DBX、資料庫驅動程式、JDBC、MCP 伺服器與外掛的更新。",
|
||||
autoUpdateApp: "自動更新 DBX",
|
||||
autoUpdateAppDescription: "在背景下載新的 DBX 版本,重新啟動與安裝時機仍由你決定。",
|
||||
autoUpdateDrivers: "自動更新資料庫驅動程式",
|
||||
autoUpdateDriversDescription: "DBX 重新啟動後,在沒有作用中連線阻塞時安裝可用的驅動程式更新。",
|
||||
autoUpdateJdbc: "自動更新 JDBC",
|
||||
autoUpdateJdbcDescription: "DBX 重新啟動後自動更新 JDBC 外掛執行環境。",
|
||||
autoUpdateMcp: "自動更新 MCP",
|
||||
autoUpdateMcpDescription: "DBX 重新啟動後,如果有新版本則更新 DBX 管理的 MCP Server。",
|
||||
autoUpdatePlugins: "自動更新外掛",
|
||||
autoUpdatePluginsDescription: "DBX 重新啟動後安裝已簽署的外掛商店更新。",
|
||||
autoUpdate: "自動更新",
|
||||
updateStatusTitle: "更新狀態",
|
||||
updateStatusDescription: "查看各元件的更新狀態,並分別設定是否自動更新。",
|
||||
checkUpdates: "立即檢查",
|
||||
updateAll: "更新全部",
|
||||
updateClient: "DBX",
|
||||
updateDrivers: "資料庫驅動程式",
|
||||
updateJdbc: "JDBC",
|
||||
updateMcp: "MCP",
|
||||
updatePlugins: "外掛",
|
||||
updateAvailable: "有可用更新",
|
||||
updateCount: "{count} 項可更新",
|
||||
upToDate: "已是最新",
|
||||
openDriverManager: "開啟驅動程式管理",
|
||||
openPluginCenter: "開啟外掛中心",
|
||||
openMcpSettings: "開啟 MCP 設定",
|
||||
updateRestartHint: "元件更新會在 DBX 成功更新並重新啟動後執行。作用中的連線佔用驅動程式時,會暫緩驅動程式更新直到安全時機。",
|
||||
debugLoggingEnabled: "啟用偵錯日誌",
|
||||
debugLoggingEnabledDescription: "開啟後在本機記錄更詳細的使用者側診斷日誌,回報問題時可暫時開啟;預設關閉。",
|
||||
metadataCacheMemoryLimit: "中繼資料記憶體快取上限",
|
||||
|
||||
@@ -626,7 +626,19 @@ function hasExplicitShortcut(settings: Partial<ShortcutSettings> | undefined, ac
|
||||
}
|
||||
|
||||
function shortcutsUseSameKeys(first: string, second: string, platform = globalThis.navigator?.platform || ""): boolean {
|
||||
return !!first && !!second && formatShortcut(first, platform).toLowerCase() === formatShortcut(second, platform).toLowerCase();
|
||||
const comparisonKey = (shortcut: string) =>
|
||||
formatShortcut(shortcut, platform)
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.map((stroke) => {
|
||||
const parts = stroke.split("+").filter(Boolean);
|
||||
if (parts.length <= 1) return stroke.toLowerCase();
|
||||
const key = parts.pop()!;
|
||||
return [...parts.sort(), key].join("+").toLowerCase();
|
||||
})
|
||||
.join(" ");
|
||||
return !!first && !!second && comparisonKey(first) === comparisonKey(second);
|
||||
}
|
||||
|
||||
function shortcutDefaultForPlatform(definition: ShortcutDefinition, platform: string): string {
|
||||
|
||||
@@ -27,6 +27,7 @@ const categoryLabels = {
|
||||
sync: "Sync",
|
||||
ai: "AI",
|
||||
mcp: "MCP",
|
||||
updates: "Updates",
|
||||
security: "Security",
|
||||
about: "About",
|
||||
} satisfies Record<SettingsCategory, string>;
|
||||
|
||||
@@ -92,6 +92,11 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
|
||||
"tableHoverLookupMode",
|
||||
"updateNotificationsEnabled",
|
||||
"autoDownloadUpdates",
|
||||
"autoUpdateApp",
|
||||
"autoUpdateDrivers",
|
||||
"autoUpdateJdbc",
|
||||
"autoUpdateMcp",
|
||||
"autoUpdatePlugins",
|
||||
"sidebarObjectInfoMode",
|
||||
"sidebarAllowHorizontalScroll",
|
||||
"sidebarShowTooltips",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export type SettingsCategory = "editor" | "formatter" | "appearance" | "navigation" | "data" | "backups" | "tunnels" | "shortcuts" | "snippets" | "sync" | "ai" | "mcp" | "security" | "about";
|
||||
export type SettingsCategory = "editor" | "formatter" | "appearance" | "navigation" | "data" | "backups" | "tunnels" | "shortcuts" | "snippets" | "sync" | "ai" | "mcp" | "updates" | "security" | "about";
|
||||
|
||||
const SETTINGS_CATEGORIES: readonly SettingsCategory[] = ["editor", "formatter", "appearance", "navigation", "data", "backups", "tunnels", "shortcuts", "snippets", "sync", "ai", "mcp", "security", "about"];
|
||||
const SETTINGS_CATEGORIES: readonly SettingsCategory[] = ["editor", "formatter", "appearance", "navigation", "data", "backups", "tunnels", "shortcuts", "snippets", "sync", "ai", "mcp", "updates", "security", "about"];
|
||||
|
||||
/**
|
||||
* Maps retired settings tabs to their current home so saved links and external
|
||||
@@ -267,9 +267,12 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
|
||||
{ id: "security-password", category: "security", titleKey: "auth.changePassword", targetId: "security", visible: webOnly },
|
||||
{ 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" },
|
||||
{ id: "updates-app", category: "updates", titleKey: "settings.autoUpdateApp", descriptionKey: "settings.autoUpdateAppDescription", targetId: "updates" },
|
||||
{ id: "updates-drivers", category: "updates", titleKey: "settings.autoUpdateDrivers", descriptionKey: "settings.autoUpdateDriversDescription", targetId: "updates" },
|
||||
{ id: "updates-jdbc", category: "updates", titleKey: "settings.autoUpdateJdbc", descriptionKey: "settings.autoUpdateJdbcDescription", targetId: "updates" },
|
||||
{ id: "updates-mcp", category: "updates", titleKey: "settings.autoUpdateMcp", descriptionKey: "settings.autoUpdateMcpDescription", targetId: "updates" },
|
||||
{ id: "updates-plugins", category: "updates", titleKey: "settings.autoUpdatePlugins", descriptionKey: "settings.autoUpdatePluginsDescription", targetId: "updates" },
|
||||
{ id: "updates-source", category: "updates", titleKey: "settings.updateDownloadSource", descriptionKey: "settings.updateDownloadSourceDescription", targetId: "updates" },
|
||||
];
|
||||
|
||||
export function resolveSettingsSearchEntries(definitions: readonly SettingsSearchDefinition[], context: SettingsSearchContext, translate: Translate, categoryLabels: Readonly<Record<SettingsCategory, string>>): SettingsSearchEntry[] {
|
||||
|
||||
@@ -176,7 +176,7 @@ const SETTINGS_TRANSFER_CATEGORY_KEYS: Record<SettingsTransferCategoryId, readon
|
||||
],
|
||||
shortcuts: ["shortcuts", "sqlShortcuts"],
|
||||
snippets: ["snippets"],
|
||||
other: ["updateDownloadSource", "updateNotificationsEnabled", "autoDownloadUpdates"],
|
||||
other: ["updateDownloadSource", "updateNotificationsEnabled", "autoDownloadUpdates", "autoUpdateApp", "autoUpdateDrivers", "autoUpdateJdbc", "autoUpdateMcp", "autoUpdatePlugins"],
|
||||
};
|
||||
|
||||
const KEY_TO_CATEGORY = new Map<string, SettingsTransferCategoryId>();
|
||||
@@ -273,6 +273,11 @@ const PASS_THROUGH_BOOLEAN_KEYS = [
|
||||
"sidebarShowTooltips",
|
||||
"updateNotificationsEnabled",
|
||||
"autoDownloadUpdates",
|
||||
"autoUpdateApp",
|
||||
"autoUpdateDrivers",
|
||||
"autoUpdateJdbc",
|
||||
"autoUpdateMcp",
|
||||
"autoUpdatePlugins",
|
||||
] as const satisfies readonly EditorSettingsDraftKey[];
|
||||
|
||||
const PASS_THROUGH_FIELD_VALIDATORS: Partial<Record<EditorSettingsDraftKey, (value: unknown) => boolean>> = {
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { clearPendingComponentUpdatesAfterAppUpdate, markPendingComponentUpdatesAfterAppUpdate, resolveUpdateAllAction, runPendingComponentUpdatePlan, takePendingComponentUpdatesAfterAppRestart } from "@/lib/updates/componentUpdateOrchestration";
|
||||
|
||||
describe("component update orchestration", () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it("keeps pending component updates across a failed or ordinary startup", () => {
|
||||
expect(markPendingComponentUpdatesAfterAppUpdate("0.6.16", "0.6.17")).toBe(true);
|
||||
|
||||
expect(takePendingComponentUpdatesAfterAppRestart("0.6.16")).toBeNull();
|
||||
expect(takePendingComponentUpdatesAfterAppRestart("v0.6.16")).toBeNull();
|
||||
expect(takePendingComponentUpdatesAfterAppRestart("0.6.17")).toEqual({ fromVersion: "0.6.16", targetVersion: "0.6.17", plan: { kind: "auto" } });
|
||||
});
|
||||
|
||||
it("consumes a successful restart exactly once", () => {
|
||||
markPendingComponentUpdatesAfterAppUpdate("0.6.16", "0.6.17");
|
||||
|
||||
expect(takePendingComponentUpdatesAfterAppRestart("0.6.17")).not.toBeNull();
|
||||
expect(takePendingComponentUpdatesAfterAppRestart("0.6.17")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not run components on startup when no DBX update was requested", () => {
|
||||
expect(takePendingComponentUpdatesAfterAppRestart("0.6.16")).toBeNull();
|
||||
});
|
||||
|
||||
it("discards malformed pending state", () => {
|
||||
localStorage.setItem("dbx:updates:pending-components-after-restart", "not-json");
|
||||
|
||||
expect(takePendingComponentUpdatesAfterAppRestart("0.6.17")).toBeNull();
|
||||
expect(localStorage.getItem("dbx:updates:pending-components-after-restart")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not clear pending state when the current version cannot be read", () => {
|
||||
markPendingComponentUpdatesAfterAppUpdate("0.6.16", "0.6.17");
|
||||
|
||||
expect(takePendingComponentUpdatesAfterAppRestart("")).toBeNull();
|
||||
expect(takePendingComponentUpdatesAfterAppRestart("0.6.17")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("preserves explicit update-all categories across restart", () => {
|
||||
markPendingComponentUpdatesAfterAppUpdate("0.6.16", "0.6.17", { kind: "manual", categories: ["drivers", "plugins"] });
|
||||
|
||||
expect(takePendingComponentUpdatesAfterAppRestart("0.6.17")).toEqual({
|
||||
fromVersion: "0.6.16",
|
||||
targetVersion: "0.6.17",
|
||||
plan: { kind: "manual", categories: ["drivers", "plugins"] },
|
||||
});
|
||||
});
|
||||
|
||||
it("does not replace a manual update-all plan with the automatic restart plan", () => {
|
||||
markPendingComponentUpdatesAfterAppUpdate("0.6.16", "0.6.17", { kind: "manual", categories: ["jdbc"] });
|
||||
markPendingComponentUpdatesAfterAppUpdate("0.6.16", "0.6.17");
|
||||
|
||||
expect(takePendingComponentUpdatesAfterAppRestart("0.6.17")?.plan).toEqual({ kind: "manual", categories: ["jdbc"] });
|
||||
});
|
||||
|
||||
it("retains a manual plan after a failed update and consumes it once after retry", () => {
|
||||
markPendingComponentUpdatesAfterAppUpdate("0.6.16", "0.6.17", { kind: "manual", categories: ["plugins"] });
|
||||
|
||||
expect(takePendingComponentUpdatesAfterAppRestart("0.6.16")).toBeNull();
|
||||
markPendingComponentUpdatesAfterAppUpdate("0.6.16", "0.6.17");
|
||||
expect(takePendingComponentUpdatesAfterAppRestart("0.6.16")).toBeNull();
|
||||
expect(takePendingComponentUpdatesAfterAppRestart("0.6.17")?.plan).toEqual({ kind: "manual", categories: ["plugins"] });
|
||||
expect(takePendingComponentUpdatesAfterAppRestart("0.6.17")).toBeNull();
|
||||
});
|
||||
|
||||
it("treats pending state from the previous implementation as an automatic plan", () => {
|
||||
localStorage.setItem("dbx:updates:pending-components-after-restart", JSON.stringify({ fromVersion: "0.6.16", targetVersion: "0.6.17" }));
|
||||
|
||||
expect(takePendingComponentUpdatesAfterAppRestart("0.6.17")?.plan).toEqual({ kind: "auto" });
|
||||
});
|
||||
|
||||
it("downloads DBX before deferring component updates", () => {
|
||||
expect(resolveUpdateAllAction({ hasAppUpdate: true, appUpdateCanInstall: true, appUpdatePrepared: false, hasComponentUpdates: true })).toBe("download-app");
|
||||
expect(resolveUpdateAllAction({ hasAppUpdate: true, appUpdateCanInstall: true, appUpdatePrepared: true, hasComponentUpdates: true })).toBe("defer-components");
|
||||
});
|
||||
|
||||
it("updates components immediately when no DBX update exists or the package cannot be installed", () => {
|
||||
expect(resolveUpdateAllAction({ hasAppUpdate: false, appUpdateCanInstall: true, appUpdatePrepared: false, hasComponentUpdates: true })).toBe("update-components");
|
||||
expect(resolveUpdateAllAction({ hasAppUpdate: true, appUpdateCanInstall: false, appUpdatePrepared: false, hasComponentUpdates: true })).toBe("update-components");
|
||||
});
|
||||
|
||||
it("does nothing when no update is available", () => {
|
||||
expect(resolveUpdateAllAction({ hasAppUpdate: false, appUpdateCanInstall: true, appUpdatePrepared: false, hasComponentUpdates: false })).toBe("none");
|
||||
});
|
||||
|
||||
it("can explicitly clear pending state", () => {
|
||||
markPendingComponentUpdatesAfterAppUpdate("0.6.16", "0.6.17");
|
||||
clearPendingComponentUpdatesAfterAppUpdate();
|
||||
|
||||
expect(takePendingComponentUpdatesAfterAppRestart("0.6.17")).toBeNull();
|
||||
});
|
||||
|
||||
it("dispatches automatic and manual plans to their matching installers", async () => {
|
||||
const installCategories = vi.fn().mockResolvedValue("manual");
|
||||
const autoUpdateEnabledComponents = vi.fn().mockResolvedValue("auto");
|
||||
const updates = { installCategories, autoUpdateEnabledComponents };
|
||||
|
||||
await expect(runPendingComponentUpdatePlan({ fromVersion: "0.6.16", targetVersion: "0.6.17", plan: { kind: "manual", categories: ["jdbc"] } }, updates)).resolves.toBe("manual");
|
||||
await expect(runPendingComponentUpdatePlan({ fromVersion: "0.6.16", targetVersion: "0.6.17", plan: { kind: "auto" } }, updates)).resolves.toBe("auto");
|
||||
|
||||
expect(installCategories).toHaveBeenCalledWith(["jdbc"]);
|
||||
expect(autoUpdateEnabledComponents).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { driverStoreUpdateBadgeCount, showMcpUpdateBadge } from "@/lib/updates/updateBadges";
|
||||
|
||||
describe("component update entry badges", () => {
|
||||
it("hides separate driver and JDBC badges while their automatic updates are enabled", () => {
|
||||
expect(driverStoreUpdateBadgeCount(true, true, 3, true)).toBe(0);
|
||||
});
|
||||
|
||||
it("shows badges only for categories whose automatic updates are disabled", () => {
|
||||
expect(driverStoreUpdateBadgeCount(false, true, 3, true)).toBe(3);
|
||||
expect(driverStoreUpdateBadgeCount(true, false, 3, true)).toBe(1);
|
||||
expect(driverStoreUpdateBadgeCount(false, false, 3, true)).toBe(4);
|
||||
});
|
||||
|
||||
it("hides the MCP settings badge while MCP automatic updates are enabled", () => {
|
||||
expect(showMcpUpdateBadge(true, true)).toBe(false);
|
||||
expect(showMcpUpdateBadge(false, true)).toBe(true);
|
||||
expect(showMcpUpdateBadge(false, false)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
import { safeLocalStorageGet, safeLocalStorageRemove, safeLocalStorageSet } from "@/lib/backend/safeStorage";
|
||||
|
||||
const PENDING_COMPONENT_UPDATES_STORAGE_KEY = "dbx:updates:pending-components-after-restart";
|
||||
|
||||
export const COMPONENT_UPDATE_CATEGORIES = ["drivers", "jdbc", "mcp", "plugins"] as const;
|
||||
|
||||
export type ComponentUpdateCategory = (typeof COMPONENT_UPDATE_CATEGORIES)[number];
|
||||
|
||||
export type PendingComponentUpdatePlan = { kind: "auto" } | { kind: "manual"; categories: ComponentUpdateCategory[] };
|
||||
|
||||
export interface PendingComponentUpdates {
|
||||
fromVersion: string;
|
||||
targetVersion: string;
|
||||
plan: PendingComponentUpdatePlan;
|
||||
}
|
||||
|
||||
export type UpdateAllAction = "none" | "download-app" | "defer-components" | "update-components";
|
||||
|
||||
function normalizeVersion(version: string): string {
|
||||
return version.trim().replace(/^[vV]/, "");
|
||||
}
|
||||
|
||||
function isComponentUpdateCategory(value: unknown): value is ComponentUpdateCategory {
|
||||
return typeof value === "string" && COMPONENT_UPDATE_CATEGORIES.includes(value as ComponentUpdateCategory);
|
||||
}
|
||||
|
||||
function normalizeManualCategories(value: unknown): ComponentUpdateCategory[] | null {
|
||||
if (!Array.isArray(value) || !value.every(isComponentUpdateCategory)) return null;
|
||||
return [...new Set(value)];
|
||||
}
|
||||
|
||||
function normalizePendingComponentUpdatePlan(plan: PendingComponentUpdatePlan): PendingComponentUpdatePlan | null {
|
||||
if (plan.kind === "auto") return plan;
|
||||
const categories = normalizeManualCategories(plan.categories);
|
||||
return categories ? { kind: "manual", categories } : null;
|
||||
}
|
||||
|
||||
function parsePendingComponentUpdatePlan(value: unknown): PendingComponentUpdatePlan | null {
|
||||
if (value === undefined) return { kind: "auto" };
|
||||
if (!value || typeof value !== "object") return null;
|
||||
const plan = value as { kind?: unknown; categories?: unknown };
|
||||
if (plan.kind === "auto") return { kind: "auto" };
|
||||
if (plan.kind !== "manual") return null;
|
||||
const categories = normalizeManualCategories(plan.categories);
|
||||
return categories ? { kind: "manual", categories } : null;
|
||||
}
|
||||
|
||||
function parsePendingComponentUpdates(raw: string | null): PendingComponentUpdates | null {
|
||||
if (!raw) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as Partial<Omit<PendingComponentUpdates, "plan">> & { plan?: unknown };
|
||||
const fromVersion = typeof parsed.fromVersion === "string" ? parsed.fromVersion.trim() : "";
|
||||
const targetVersion = typeof parsed.targetVersion === "string" ? parsed.targetVersion.trim() : "";
|
||||
const plan = parsePendingComponentUpdatePlan(parsed.plan);
|
||||
return fromVersion && plan ? { fromVersion, targetVersion, plan } : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function markPendingComponentUpdatesAfterAppUpdate(fromVersion: string, targetVersion: string, plan: PendingComponentUpdatePlan = { kind: "auto" }): boolean {
|
||||
const normalizedFromVersion = fromVersion.trim();
|
||||
if (!normalizedFromVersion) return false;
|
||||
const normalizedPlan = normalizePendingComponentUpdatePlan(plan);
|
||||
if (!normalizedPlan) return false;
|
||||
const existing = parsePendingComponentUpdates(safeLocalStorageGet(PENDING_COMPONENT_UPDATES_STORAGE_KEY));
|
||||
if (normalizedPlan.kind === "auto" && existing?.plan.kind === "manual" && normalizeVersion(existing.fromVersion) === normalizeVersion(normalizedFromVersion)) return true;
|
||||
return safeLocalStorageSet(
|
||||
PENDING_COMPONENT_UPDATES_STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
fromVersion: normalizedFromVersion,
|
||||
targetVersion: targetVersion.trim(),
|
||||
plan: normalizedPlan,
|
||||
} satisfies PendingComponentUpdates),
|
||||
);
|
||||
}
|
||||
|
||||
export function clearPendingComponentUpdatesAfterAppUpdate() {
|
||||
safeLocalStorageRemove(PENDING_COMPONENT_UPDATES_STORAGE_KEY);
|
||||
}
|
||||
|
||||
export function takePendingComponentUpdatesAfterAppRestart(currentVersion: string): PendingComponentUpdates | null {
|
||||
const pending = parsePendingComponentUpdates(safeLocalStorageGet(PENDING_COMPONENT_UPDATES_STORAGE_KEY));
|
||||
if (!pending) {
|
||||
clearPendingComponentUpdatesAfterAppUpdate();
|
||||
return null;
|
||||
}
|
||||
const normalizedCurrentVersion = normalizeVersion(currentVersion);
|
||||
if (!normalizedCurrentVersion || normalizedCurrentVersion === normalizeVersion(pending.fromVersion)) return null;
|
||||
clearPendingComponentUpdatesAfterAppUpdate();
|
||||
return pending;
|
||||
}
|
||||
|
||||
export function resolveUpdateAllAction(options: { hasAppUpdate: boolean; appUpdateCanInstall: boolean; appUpdatePrepared: boolean; hasComponentUpdates: boolean }): UpdateAllAction {
|
||||
if (!options.hasAppUpdate || !options.appUpdateCanInstall) return options.hasComponentUpdates ? "update-components" : "none";
|
||||
return options.appUpdatePrepared ? "defer-components" : "download-app";
|
||||
}
|
||||
|
||||
export function runPendingComponentUpdatePlan<T>(pending: PendingComponentUpdates, updates: { installCategories: (categories: ComponentUpdateCategory[]) => Promise<T>; autoUpdateEnabledComponents: () => Promise<T> }): Promise<T> {
|
||||
return pending.plan.kind === "manual" ? updates.installCategories(pending.plan.categories) : updates.autoUpdateEnabledComponents();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export function driverStoreUpdateBadgeCount(autoUpdateDrivers: boolean, autoUpdateJdbc: boolean, driverUpdateCount: number, jdbcUpdateAvailable: boolean): number {
|
||||
return (autoUpdateDrivers ? 0 : driverUpdateCount) + (!autoUpdateJdbc && jdbcUpdateAvailable ? 1 : 0);
|
||||
}
|
||||
|
||||
export function showMcpUpdateBadge(autoUpdateMcp: boolean, mcpUpdateAvailable: boolean): boolean {
|
||||
return !autoUpdateMcp && mcpUpdateAvailable;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import type { UpdateInfo } from "@/lib/backend/api";
|
||||
import type { AgentDriverInfo, McpServerStatus } from "@/lib/backend/tauri";
|
||||
import type { JdbcPluginStatus } from "@/types/database";
|
||||
import type { MarketplacePluginListing } from "@/lib/plugins/pluginMarketplace";
|
||||
|
||||
export function isUpdatePreviewMockEnabled(): boolean {
|
||||
return import.meta.env.DEV && import.meta.env.VITE_DBX_MOCK_UPDATES === "1";
|
||||
}
|
||||
|
||||
export function previewAppUpdateInfo(currentVersion: string): UpdateInfo {
|
||||
const current = currentVersion || "0.6.15";
|
||||
const showAppUpdate = import.meta.env.VITE_DBX_MOCK_UPDATE_SCENARIO !== "components";
|
||||
return {
|
||||
current_version: current,
|
||||
latest_version: showAppUpdate ? "0.6.16" : current,
|
||||
update_available: showAppUpdate,
|
||||
portable_mode: false,
|
||||
manual_update_only: false,
|
||||
release_name: showAppUpdate ? "DBX v0.6.16 Preview" : `DBX v${current}`,
|
||||
release_url: "https://github.com/t8y2/dbx/releases",
|
||||
release_notes: showAppUpdate
|
||||
? `## 更新预览
|
||||
|
||||
### 新功能
|
||||
- 更新中心集中展示 DBX、驱动、JDBC、MCP 与插件更新
|
||||
- 支持按更新类别查看版本信息和更新内容
|
||||
|
||||
### 修复
|
||||
- 优化自动更新开关默认值
|
||||
- 修复独立入口红点与聚合更新状态不一致的问题
|
||||
|
||||
### 体验优化
|
||||
- 更新检查在后台静默执行
|
||||
- 组件更新仅在无阻塞任务时安装`
|
||||
: "",
|
||||
};
|
||||
}
|
||||
|
||||
export function previewDriverUpdates(): AgentDriverInfo[] {
|
||||
const fixtures = [
|
||||
["mysql", "MySQL", "8.0.35", "9.0.1", 18_400_000],
|
||||
["postgresql", "PostgreSQL", "16.2", "17.0", 15_800_000],
|
||||
["oracle", "Oracle", "23.4.0", "23.5.0", 42_600_000],
|
||||
["sqlserver", "SQL Server", "12.8.1", "13.0.2", 21_300_000],
|
||||
["mariadb", "MariaDB", "3.3.7", "3.4.1", 12_900_000],
|
||||
["dameng", "达梦 DM8", "8.1.3.100", "8.1.3.140", 26_700_000],
|
||||
["kingbase", "KingbaseES", "9.1.0", "9.2.0", 24_500_000],
|
||||
["tdengine", "TDengine", "3.3.5", "3.3.7", 31_200_000],
|
||||
["clickhouse", "ClickHouse", "0.8.5", "0.8.6", 17_600_000],
|
||||
["mongodb", "MongoDB", "5.4.0", "5.5.1", 19_800_000],
|
||||
["redis", "Redis", "1.7.0", "1.8.0", 9_400_000],
|
||||
["db2", "DB2", "12.1.0", "12.1.2", 28_100_000],
|
||||
] as const;
|
||||
return fixtures.map(([db_type, label, installed_version, version, size]) => ({
|
||||
db_type,
|
||||
label,
|
||||
version,
|
||||
size,
|
||||
installed: true,
|
||||
installed_version,
|
||||
update_available: true,
|
||||
requires_java_runtime: true,
|
||||
jre: "21",
|
||||
jre_installed: true,
|
||||
}));
|
||||
}
|
||||
|
||||
export function previewJdbcUpdate(): JdbcPluginStatus {
|
||||
return {
|
||||
installed: true,
|
||||
version: "0.1.0",
|
||||
protocol_version: 1,
|
||||
compatible: true,
|
||||
latest_version: "0.2.0",
|
||||
latest_protocol_version: 2,
|
||||
update_available: true,
|
||||
path: "/preview/jdbc-agent",
|
||||
};
|
||||
}
|
||||
|
||||
export function previewMcpUpdate(): McpServerStatus {
|
||||
return {
|
||||
installed: true,
|
||||
npm_available: true,
|
||||
node_path: "/preview/bin/node",
|
||||
node_version: "v22.18.0",
|
||||
current_version: "0.4.88",
|
||||
latest_version: "0.4.89",
|
||||
update_available: true,
|
||||
bin_path: "/preview/bin/dbx-mcp",
|
||||
native_bin_path: null,
|
||||
script_path: "/preview/lib/dbx-mcp/index.js",
|
||||
data_dir: "/preview/data/mcp",
|
||||
install_command: "npm install -g @dbx-app/mcp-server",
|
||||
update_command: "npm update -g @dbx-app/mcp-server",
|
||||
uninstall_command: "npm uninstall -g @dbx-app/mcp-server",
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function previewPluginUpdates(): MarketplacePluginListing[] {
|
||||
return [
|
||||
{
|
||||
key: "official:io.dbx.ssh",
|
||||
repository: { id: "official", name: "DBX Official", kind: "official", enabled: true, managed: true },
|
||||
plugin: {
|
||||
id: "io.dbx.ssh",
|
||||
name: "SSH Tunnel",
|
||||
description: "通过 SSH 隧道安全访问数据库。",
|
||||
publisher: "DBX",
|
||||
verified: true,
|
||||
tags: ["ssh", "tunnel"],
|
||||
permissions: ["network"],
|
||||
latestVersion: "0.4.78",
|
||||
versions: [],
|
||||
},
|
||||
name: "SSH Tunnel",
|
||||
description: "通过 SSH 隧道安全访问数据库。",
|
||||
target: "universal",
|
||||
artifact: { target: "universal", url: "https://example.invalid/ssh.dbxp", sha256: "preview", signingKeyId: "preview" },
|
||||
installed: { manifest: { id: "io.dbx.ssh", name: "SSH Tunnel", version: "0.4.77", drivers: [] }, compatibility: { compatible: true } },
|
||||
verified: true,
|
||||
status: "update",
|
||||
},
|
||||
{
|
||||
key: "official:io.dbx.data-tools",
|
||||
repository: { id: "official", name: "DBX Official", kind: "official", enabled: true, managed: true },
|
||||
plugin: {
|
||||
id: "io.dbx.data-tools",
|
||||
name: "Data Tools",
|
||||
description: "数据生成、转换和校验工具集。",
|
||||
publisher: "DBX",
|
||||
verified: true,
|
||||
tags: ["data", "tools"],
|
||||
permissions: ["database"],
|
||||
latestVersion: "3.3.0",
|
||||
versions: [],
|
||||
},
|
||||
name: "Data Tools",
|
||||
description: "数据生成、转换和校验工具集。",
|
||||
target: "universal",
|
||||
artifact: { target: "universal", url: "https://example.invalid/data-tools.dbxp", sha256: "preview", signingKeyId: "preview" },
|
||||
installed: { manifest: { id: "io.dbx.data-tools", name: "Data Tools", version: "3.2.0", drivers: [] }, compatibility: { compatible: true } },
|
||||
verified: true,
|
||||
status: "update",
|
||||
},
|
||||
];
|
||||
}
|
||||
+11
-5
@@ -273,11 +273,17 @@ describe("normalizeEditorSettings", () => {
|
||||
expect(normalizeEditorSettings({}).updateDownloadSource).toBe("official");
|
||||
});
|
||||
|
||||
it("requires opting into automatic update downloads and preserves the preference", () => {
|
||||
expect(normalizeEditorSettings({}).autoDownloadUpdates).toBe(false);
|
||||
it("migrates legacy update opt-outs without overriding explicit category settings", () => {
|
||||
expect(normalizeEditorSettings({}).autoDownloadUpdates).toBe(true);
|
||||
expect(normalizeEditorSettings({ autoDownloadUpdates: true }).autoDownloadUpdates).toBe(true);
|
||||
expect(normalizeEditorSettings({ autoDownloadUpdates: false }).autoDownloadUpdates).toBe(false);
|
||||
expect(normalizeEditorSettings({ autoDownloadUpdates: "true" } as any).autoDownloadUpdates).toBe(false);
|
||||
expect(normalizeEditorSettings({ autoDownloadUpdates: false }).autoUpdateDrivers).toBe(true);
|
||||
expect(normalizeEditorSettings({ updateNotificationsEnabled: false }).autoUpdateApp).toBe(false);
|
||||
expect(normalizeEditorSettings({ updateNotificationsEnabled: false }).autoUpdateDrivers).toBe(false);
|
||||
expect(normalizeEditorSettings({ updateNotificationsEnabled: false }).autoUpdateJdbc).toBe(false);
|
||||
expect(normalizeEditorSettings({ updateNotificationsEnabled: false }).autoUpdateMcp).toBe(false);
|
||||
expect(normalizeEditorSettings({ updateNotificationsEnabled: false }).autoUpdatePlugins).toBe(false);
|
||||
expect(normalizeEditorSettings({ autoUpdateApp: false }).autoDownloadUpdates).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves explicit editor themes from saved settings", () => {
|
||||
@@ -971,7 +977,7 @@ describe("settingsStore persisted settings initialization", () => {
|
||||
theme: "xcode-dark",
|
||||
executeMode: "all",
|
||||
executeModeDefaultVersion: 1,
|
||||
updateNotificationsEnabled: false,
|
||||
updateNotificationsEnabled: true,
|
||||
});
|
||||
const saveEditorSettings = vi.fn().mockResolvedValue(undefined);
|
||||
vi.doMock("@/lib/backend/api", () => ({ loadEditorSettings, saveEditorSettings }));
|
||||
@@ -989,7 +995,7 @@ describe("settingsStore persisted settings initialization", () => {
|
||||
fontSize: 17,
|
||||
theme: "xcode-dark",
|
||||
executeMode: "all",
|
||||
updateNotificationsEnabled: false,
|
||||
updateNotificationsEnabled: true,
|
||||
appLayout: "separated",
|
||||
});
|
||||
expect(saveEditorSettings).toHaveBeenCalledWith(expect.objectContaining({ fontSize: 17, theme: "xcode-dark", appLayout: "separated" }));
|
||||
|
||||
@@ -867,8 +867,15 @@ export interface EditorSettings {
|
||||
generateSqlIncludeDatabaseName: boolean;
|
||||
generateSqlQuoteIdentifiers: boolean;
|
||||
formatSqlOnSqlFileSave: boolean;
|
||||
/** Legacy alias retained for settings-file compatibility. Mirrors autoUpdateApp. */
|
||||
updateNotificationsEnabled: boolean;
|
||||
/** Legacy setting retained for compatibility. Mirrors autoUpdateApp after the centralized update controls are enabled. */
|
||||
autoDownloadUpdates: boolean;
|
||||
autoUpdateApp: boolean;
|
||||
autoUpdateDrivers: boolean;
|
||||
autoUpdateJdbc: boolean;
|
||||
autoUpdateMcp: boolean;
|
||||
autoUpdatePlugins: boolean;
|
||||
sidebarHiddenTablePrefixes: string[];
|
||||
sidebarCopyTableNameSeparator: ColumnNameCopySeparator;
|
||||
sidebarCopyTableNameIncludeSchema: boolean;
|
||||
@@ -1117,7 +1124,12 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
||||
generateSqlQuoteIdentifiers: true,
|
||||
formatSqlOnSqlFileSave: false,
|
||||
updateNotificationsEnabled: true,
|
||||
autoDownloadUpdates: false,
|
||||
autoDownloadUpdates: true,
|
||||
autoUpdateApp: true,
|
||||
autoUpdateDrivers: true,
|
||||
autoUpdateJdbc: true,
|
||||
autoUpdateMcp: true,
|
||||
autoUpdatePlugins: true,
|
||||
sidebarHiddenTablePrefixes: [],
|
||||
sidebarCopyTableNameSeparator: "comma",
|
||||
sidebarCopyTableNameIncludeSchema: false,
|
||||
@@ -1449,6 +1461,17 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
||||
const normalizedExtractorOptions = normalizeDataGridExtractorOptions(settings.dataGridExtractorOptions);
|
||||
const isLegacyExtractorOptions = typeof savedExtractorMigrationVersion !== "number" || savedExtractorMigrationVersion < DATA_GRID_EXTRACTOR_OPTIONS_MIGRATION_VERSION;
|
||||
const dataGridExtractorOptions = isLegacyExtractorOptions && normalizedExtractorOptions.dsv.nullText === "NULL" ? { ...normalizedExtractorOptions, dsv: { ...normalizedExtractorOptions.dsv, nullText: "" } } : normalizedExtractorOptions;
|
||||
// Preserve the explicit intent behind the legacy update controls. Disabling
|
||||
// update reminders was a full opt-out; disabling automatic downloads only
|
||||
// opted out of downloading the DBX package itself.
|
||||
const legacyUpdateOptOut = settings.updateNotificationsEnabled === false;
|
||||
const legacyAutoDownload = typeof settings.autoDownloadUpdates === "boolean" ? settings.autoDownloadUpdates : undefined;
|
||||
const autoUpdateApp = typeof settings.autoUpdateApp === "boolean" ? settings.autoUpdateApp : legacyUpdateOptOut ? false : (legacyAutoDownload ?? DEFAULT_EDITOR_SETTINGS.autoUpdateApp);
|
||||
const autoDownloadUpdates = autoUpdateApp;
|
||||
const autoUpdateDrivers = typeof settings.autoUpdateDrivers === "boolean" ? settings.autoUpdateDrivers : legacyUpdateOptOut ? false : DEFAULT_EDITOR_SETTINGS.autoUpdateDrivers;
|
||||
const autoUpdateJdbc = typeof settings.autoUpdateJdbc === "boolean" ? settings.autoUpdateJdbc : legacyUpdateOptOut ? false : DEFAULT_EDITOR_SETTINGS.autoUpdateJdbc;
|
||||
const autoUpdateMcp = typeof settings.autoUpdateMcp === "boolean" ? settings.autoUpdateMcp : legacyUpdateOptOut ? false : DEFAULT_EDITOR_SETTINGS.autoUpdateMcp;
|
||||
const autoUpdatePlugins = typeof settings.autoUpdatePlugins === "boolean" ? settings.autoUpdatePlugins : legacyUpdateOptOut ? false : DEFAULT_EDITOR_SETTINGS.autoUpdatePlugins;
|
||||
return {
|
||||
fontFamily: normalizeFontFamily(settings.fontFamily, DEFAULT_EDITOR_SETTINGS.fontFamily),
|
||||
fontSize: settings.fontSize ?? DEFAULT_EDITOR_SETTINGS.fontSize,
|
||||
@@ -1634,8 +1657,13 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
||||
generateSqlIncludeDatabaseName: settings.generateSqlIncludeDatabaseName === true,
|
||||
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,
|
||||
updateNotificationsEnabled: autoUpdateApp,
|
||||
autoDownloadUpdates,
|
||||
autoUpdateApp,
|
||||
autoUpdateDrivers,
|
||||
autoUpdateJdbc,
|
||||
autoUpdateMcp,
|
||||
autoUpdatePlugins,
|
||||
sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(settings.sidebarHiddenTablePrefixes),
|
||||
sidebarCopyTableNameSeparator: normalizeSidebarCopyTableNameSeparator(settings.sidebarCopyTableNameSeparator),
|
||||
sidebarCopyTableNameIncludeSchema: settings.sidebarCopyTableNameIncludeSchema === true,
|
||||
@@ -2384,8 +2412,25 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
if (partial.generateSqlIncludeDatabaseName !== undefined) editorSettings.value.generateSqlIncludeDatabaseName = partial.generateSqlIncludeDatabaseName === true;
|
||||
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.updateNotificationsEnabled !== undefined) {
|
||||
editorSettings.value.updateNotificationsEnabled = partial.updateNotificationsEnabled;
|
||||
editorSettings.value.autoUpdateApp = partial.updateNotificationsEnabled;
|
||||
editorSettings.value.autoDownloadUpdates = partial.updateNotificationsEnabled;
|
||||
}
|
||||
if (partial.autoUpdateApp !== undefined) {
|
||||
editorSettings.value.autoUpdateApp = partial.autoUpdateApp;
|
||||
editorSettings.value.updateNotificationsEnabled = partial.autoUpdateApp;
|
||||
editorSettings.value.autoDownloadUpdates = partial.autoUpdateApp;
|
||||
}
|
||||
if (partial.autoDownloadUpdates !== undefined) {
|
||||
editorSettings.value.autoDownloadUpdates = partial.autoDownloadUpdates === true;
|
||||
editorSettings.value.autoUpdateApp = partial.autoDownloadUpdates === true;
|
||||
editorSettings.value.updateNotificationsEnabled = partial.autoDownloadUpdates === true;
|
||||
}
|
||||
if (partial.autoUpdateDrivers !== undefined) editorSettings.value.autoUpdateDrivers = partial.autoUpdateDrivers;
|
||||
if (partial.autoUpdateJdbc !== undefined) editorSettings.value.autoUpdateJdbc = partial.autoUpdateJdbc;
|
||||
if (partial.autoUpdateMcp !== undefined) editorSettings.value.autoUpdateMcp = partial.autoUpdateMcp;
|
||||
if (partial.autoUpdatePlugins !== undefined) editorSettings.value.autoUpdatePlugins = partial.autoUpdatePlugins;
|
||||
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;
|
||||
|
||||
+1
-1
@@ -124,7 +124,7 @@ test("wires the active task guard into update installation and restart, but not
|
||||
// starting the background download itself never blocks.
|
||||
assert.match(updaterSource, /shouldBlockAppUpdate\(activeTaskCount.value\)/);
|
||||
assert.match(dialogSource, /role="alert"[\s\S]*updates\.activeTasksBlockUpdate/);
|
||||
assert.equal(dialogSource.match(/:disabled="activeTaskCount > 0 \|\| isIgnoringUpdate"/g)?.length, 2);
|
||||
assert.equal(dialogSource.match(/:disabled="activeTaskCount > 0 \|\| isIgnoringUpdate \|\| isUpdatingAll"/g)?.length, 2);
|
||||
assert.match(dialogSource, /ignore-version/);
|
||||
assert.match(updaterSource, /ignoreCurrentVersion/);
|
||||
});
|
||||
|
||||
+60
@@ -400,8 +400,68 @@ test("defaults auto-close brackets to on and preserves saved booleans", () => {
|
||||
|
||||
test("defaults update notifications to enabled", () => {
|
||||
assert.equal(DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled, true);
|
||||
assert.equal(DEFAULT_EDITOR_SETTINGS.autoUpdateApp, true);
|
||||
assert.equal(DEFAULT_EDITOR_SETTINGS.autoUpdateDrivers, true);
|
||||
assert.equal(DEFAULT_EDITOR_SETTINGS.autoUpdateJdbc, true);
|
||||
assert.equal(DEFAULT_EDITOR_SETTINGS.autoUpdateMcp, true);
|
||||
assert.equal(DEFAULT_EDITOR_SETTINGS.autoUpdatePlugins, true);
|
||||
assert.equal(normalizeEditorSettings({}).updateNotificationsEnabled, true);
|
||||
assert.equal(normalizeEditorSettings({}).autoUpdateApp, true);
|
||||
assert.equal(normalizeEditorSettings({}).autoUpdateDrivers, true);
|
||||
assert.equal(normalizeEditorSettings({}).autoUpdateJdbc, true);
|
||||
assert.equal(normalizeEditorSettings({}).autoUpdateMcp, true);
|
||||
assert.equal(normalizeEditorSettings({}).autoUpdatePlugins, true);
|
||||
assert.equal(normalizeEditorSettings({ updateNotificationsEnabled: false } as any).updateNotificationsEnabled, false);
|
||||
assert.equal(normalizeEditorSettings({ updateNotificationsEnabled: false } as any).autoUpdateApp, false);
|
||||
assert.equal(normalizeEditorSettings({ updateNotificationsEnabled: false } as any).autoUpdateDrivers, false);
|
||||
assert.equal(normalizeEditorSettings({ updateNotificationsEnabled: false } as any).autoUpdateJdbc, false);
|
||||
assert.equal(normalizeEditorSettings({ updateNotificationsEnabled: false } as any).autoUpdateMcp, false);
|
||||
assert.equal(normalizeEditorSettings({ updateNotificationsEnabled: false } as any).autoUpdatePlugins, false);
|
||||
assert.equal(normalizeEditorSettings({ autoDownloadUpdates: false }).autoUpdateApp, false);
|
||||
assert.equal(normalizeEditorSettings({ autoDownloadUpdates: false }).autoUpdateDrivers, true);
|
||||
assert.equal(normalizeEditorSettings({ autoDownloadUpdates: false }).autoUpdateJdbc, true);
|
||||
assert.equal(normalizeEditorSettings({ autoDownloadUpdates: false }).autoUpdateMcp, true);
|
||||
assert.equal(normalizeEditorSettings({ autoDownloadUpdates: false }).autoUpdatePlugins, true);
|
||||
assert.equal(normalizeEditorSettings({ autoUpdateApp: false }).updateNotificationsEnabled, false);
|
||||
assert.equal(normalizeEditorSettings({ autoUpdateApp: false }).autoUpdateApp, false);
|
||||
assert.equal(normalizeEditorSettings({ autoUpdateApp: false }).autoUpdateDrivers, true);
|
||||
assert.equal(normalizeEditorSettings({ autoUpdateApp: false }).autoUpdateJdbc, true);
|
||||
assert.equal(normalizeEditorSettings({ autoUpdateApp: false }).autoUpdateMcp, true);
|
||||
assert.equal(normalizeEditorSettings({ autoUpdateApp: false }).autoUpdatePlugins, true);
|
||||
});
|
||||
|
||||
test("centralized automatic updates default on while explicit category opt-outs are preserved", () => {
|
||||
const migrated = normalizeEditorSettings({ updateNotificationsEnabled: false } as any);
|
||||
|
||||
assert.deepEqual(
|
||||
{
|
||||
app: migrated.autoUpdateApp,
|
||||
drivers: migrated.autoUpdateDrivers,
|
||||
jdbc: migrated.autoUpdateJdbc,
|
||||
mcp: migrated.autoUpdateMcp,
|
||||
plugins: migrated.autoUpdatePlugins,
|
||||
},
|
||||
{ app: false, drivers: false, jdbc: false, mcp: false, plugins: false },
|
||||
);
|
||||
|
||||
const explicitlyConfigured = normalizeEditorSettings({
|
||||
updateNotificationsEnabled: false,
|
||||
autoUpdateApp: true,
|
||||
autoUpdateDrivers: false,
|
||||
autoUpdateJdbc: false,
|
||||
autoUpdateMcp: false,
|
||||
autoUpdatePlugins: true,
|
||||
} as any);
|
||||
assert.deepEqual(
|
||||
{
|
||||
app: explicitlyConfigured.autoUpdateApp,
|
||||
drivers: explicitlyConfigured.autoUpdateDrivers,
|
||||
jdbc: explicitlyConfigured.autoUpdateJdbc,
|
||||
mcp: explicitlyConfigured.autoUpdateMcp,
|
||||
plugins: explicitlyConfigured.autoUpdatePlugins,
|
||||
},
|
||||
{ app: true, drivers: false, jdbc: false, mcp: false, plugins: true },
|
||||
);
|
||||
});
|
||||
|
||||
test("defaults sidebar table search to disabled and preserves saved booleans", () => {
|
||||
|
||||
Reference in New Issue
Block a user