feat(updater): add entry to roll back to older versions
This commit is contained in:
15 files changed
+356
-18
No files matched your search
+127
-4
@@ -1,16 +1,34 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { createApp, defineComponent, h, nextTick, reactive, type App } from "vue";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import i18n from "@/i18n";
|
||||
import UpdateDialog from "@/components/layout/UpdateDialog.vue";
|
||||
|
||||
const runtimeState = vi.hoisted(() => ({ tauri: true }));
|
||||
|
||||
const updateMocks = vi.hoisted(() => ({
|
||||
fetchChangelog: vi.fn(),
|
||||
getAppVersion: vi.fn(),
|
||||
openExternal: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/backend/tauriRuntime", () => ({
|
||||
isTauriRuntime: () => runtimeState.tauri,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/app/changelog", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@/lib/app/changelog")>();
|
||||
return { ...actual, fetchChangelog: updateMocks.fetchChangelog };
|
||||
});
|
||||
|
||||
vi.mock("@/lib/backend/api", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@/lib/backend/api")>();
|
||||
return { ...actual, getAppVersion: updateMocks.getAppVersion };
|
||||
});
|
||||
|
||||
vi.mock("@tauri-apps/plugin-shell", () => ({ open: updateMocks.openExternal }));
|
||||
|
||||
const mountedApps: App[] = [];
|
||||
|
||||
interface DialogState {
|
||||
@@ -136,6 +154,13 @@ async function clickOutside() {
|
||||
await flushDialog();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// 默认没有历史版本:只有专门的历史版本用例才提供发布记录
|
||||
updateMocks.fetchChangelog.mockResolvedValue({ updatedAt: "", releases: [] });
|
||||
updateMocks.getAppVersion.mockResolvedValue("0.5.60");
|
||||
updateMocks.openExternal.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
runtimeState.tauri = true;
|
||||
for (const app of mountedApps.splice(0)) app.unmount();
|
||||
@@ -379,8 +404,105 @@ describe("UpdateDialog release notes safety", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("UpdateDialog older versions", () => {
|
||||
const release = (tag: string, date: string) => ({ tag, name: `DBX ${tag}`, date, sections: [] });
|
||||
const upToDateInfo = {
|
||||
current_version: "0.5.60",
|
||||
latest_version: "0.5.60",
|
||||
update_available: false,
|
||||
portable_mode: false,
|
||||
manual_update_only: false,
|
||||
release_name: "",
|
||||
release_url: "",
|
||||
release_notes: "",
|
||||
};
|
||||
const historyRegion = () => document.body.querySelector<HTMLElement>("[data-update-history]");
|
||||
const historyButton = (text: string) => Array.from(historyRegion()?.querySelectorAll("button") ?? []).find((button) => button.textContent?.includes(text));
|
||||
|
||||
it("lists only versions older than the current build and opens their release page", async () => {
|
||||
updateMocks.fetchChangelog.mockResolvedValue({ updatedAt: "2026-09-10", releases: [release("v0.5.61", "2026-09-10"), release("v0.5.60", "2026-09-01"), release("v0.5.59", "2026-08-20")] });
|
||||
|
||||
await mountDialog(0, {}, undefined, { updateInfo: upToDateInfo, updateCheckMessage: "DBX is up to date (0.5.60)." });
|
||||
await flushDialog();
|
||||
|
||||
// 默认折叠:即使没有可用更新也先收起,且列表只保留低于当前版本的版本
|
||||
const region = historyRegion();
|
||||
expect(region?.textContent).toContain("Older versions");
|
||||
expect(region?.textContent).not.toContain("v0.5.59");
|
||||
|
||||
historyButton("Older versions")?.click();
|
||||
await flushDialog();
|
||||
|
||||
expect(region?.textContent).toContain("Back up your data before rolling back");
|
||||
expect(region?.textContent).toContain("v0.5.59");
|
||||
expect(region?.textContent).not.toContain("v0.5.61");
|
||||
|
||||
historyButton("Open Release")?.click();
|
||||
await flushDialog();
|
||||
|
||||
expect(updateMocks.openExternal).toHaveBeenCalledWith("https://github.com/t8y2/dbx/releases/tag/v0.5.59");
|
||||
});
|
||||
|
||||
it("keeps the history collapsed until the user expands it", async () => {
|
||||
updateMocks.fetchChangelog.mockResolvedValue({ updatedAt: "2026-09-10", releases: [release("v0.5.61", "2026-09-10"), release("v0.5.59", "2026-08-20")] });
|
||||
|
||||
await mountDialog(0);
|
||||
await flushDialog();
|
||||
|
||||
expect(historyRegion()?.textContent).toContain("Older versions");
|
||||
expect(historyRegion()?.textContent).not.toContain("v0.5.59");
|
||||
|
||||
historyButton("Older versions")?.click();
|
||||
await flushDialog();
|
||||
|
||||
expect(historyRegion()?.textContent).toContain("v0.5.59");
|
||||
});
|
||||
|
||||
it("starts collapsed again after the dialog is reopened", async () => {
|
||||
updateMocks.fetchChangelog.mockResolvedValue({ updatedAt: "2026-09-10", releases: [release("v0.5.61", "2026-09-10"), release("v0.5.59", "2026-08-20")] });
|
||||
|
||||
const { state } = await mountDialog(0, {}, undefined, { updateInfo: upToDateInfo, updateCheckMessage: "DBX is up to date (0.5.60)." });
|
||||
await flushDialog();
|
||||
|
||||
historyButton("Older versions")?.click();
|
||||
await flushDialog();
|
||||
expect(historyRegion()?.textContent).toContain("v0.5.59");
|
||||
|
||||
state.open = false;
|
||||
await flushDialog();
|
||||
state.open = true;
|
||||
await flushDialog();
|
||||
|
||||
expect(historyRegion()?.textContent).toContain("Older versions");
|
||||
expect(historyRegion()?.textContent).not.toContain("v0.5.59");
|
||||
});
|
||||
|
||||
it("hides the history section when no release is older than the current build", async () => {
|
||||
updateMocks.fetchChangelog.mockResolvedValue({ updatedAt: "2026-09-10", releases: [release("v0.5.61", "2026-09-10"), release("v0.5.60", "2026-09-01")] });
|
||||
|
||||
await mountDialog(0);
|
||||
await flushDialog();
|
||||
|
||||
expect(historyRegion()).toBeNull();
|
||||
});
|
||||
|
||||
it("surfaces a changelog load failure instead of hiding it silently", async () => {
|
||||
updateMocks.fetchChangelog.mockRejectedValue(new Error("offline"));
|
||||
|
||||
await mountDialog(0, {}, undefined, { updateInfo: upToDateInfo, updateCheckMessage: "DBX is up to date (0.5.60)." });
|
||||
await flushDialog();
|
||||
|
||||
expect(historyRegion()?.textContent).toContain("Older versions");
|
||||
|
||||
historyButton("Older versions")?.click();
|
||||
await flushDialog();
|
||||
|
||||
expect(historyRegion()?.textContent).toContain("Failed to load older versions");
|
||||
});
|
||||
});
|
||||
|
||||
describe("UpdateDialog aggregate update center", () => {
|
||||
it("hides the tab bar when no updates are available", async () => {
|
||||
it("keeps only the client tab when nothing is updatable", async () => {
|
||||
await mountDialog(0, {}, undefined, {
|
||||
updateInfo: {
|
||||
current_version: "0.5.60",
|
||||
@@ -395,8 +517,9 @@ describe("UpdateDialog aggregate update center", () => {
|
||||
updateCheckMessage: "DBX is up to date (0.5.60).",
|
||||
});
|
||||
|
||||
expect(document.body.querySelector('[role="tablist"]')).toBeNull();
|
||||
expect(document.body.querySelector('[data-update-tab="app"]')).toBeNull();
|
||||
// 客户端页签常驻(承载回退入口),但没有可更新项时不再出现其他页签
|
||||
expect(document.body.querySelector('[data-update-tab="app"]')).not.toBeNull();
|
||||
expect(document.body.querySelector('[data-update-tab="drivers"]')).toBeNull();
|
||||
expect(document.body.textContent).toContain("DBX is up to date (0.5.60).");
|
||||
});
|
||||
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { AlertTriangle, CheckCircle2, Loader2, RefreshCw } from "@lucide/vue";
|
||||
import { AlertTriangle, CheckCircle2, ChevronDown, 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 { getAppVersion, type UpdateInfo } from "@/lib/backend/api";
|
||||
import type { AgentDriverInfo, McpServerStatus, UpdateDownloadSource } from "@/lib/backend/tauri";
|
||||
import type { JdbcPluginStatus } from "@/types/database";
|
||||
import { pluginSourceChange, 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 { canDownloadAndInstallUpdate, isNewerRemoteVersion, resolveReleaseTagUrl } from "@/composables/useAppUpdater";
|
||||
import type { ComponentUpdateCategory } from "@/composables/useComponentUpdates";
|
||||
import { currentLocale } from "@/i18n";
|
||||
import { changelogLangFromLocale, createLatestRequestGuard, fetchChangelog, type ChangelogRelease } from "@/lib/app/changelog";
|
||||
|
||||
type UpdateTab = "app" | ComponentUpdateCategory;
|
||||
|
||||
@@ -69,16 +71,40 @@ const emit = defineEmits<{
|
||||
"update-all": [];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t, locale } = useI18n();
|
||||
const isDesktop = isTauriRuntime() || isUpdatePreviewMockEnabled();
|
||||
const selectedTab = ref<UpdateTab>("app");
|
||||
const renderedNotes = ref("");
|
||||
|
||||
const hasAppUpdate = computed(() => props.updateDownloaded || props.updateReady || props.updateInfo?.update_available === true);
|
||||
const mcpAvailable = computed(() => (props.mcpUpdate ? mcpUpdateAvailability(props.mcpUpdate) === true : false));
|
||||
|
||||
/** 历史版本每页条数(与更新日志一致,避免一次性铺开所有版本) */
|
||||
const HISTORY_PAGE_SIZE = 10;
|
||||
const historyLoading = ref(false);
|
||||
const historyError = ref("");
|
||||
const historyReleases = ref<ChangelogRelease[]>([]);
|
||||
const historyVisibleCount = ref(HISTORY_PAGE_SIZE);
|
||||
const historyRequest = createLatestRequestGuard();
|
||||
/** 弹窗可能在“检查更新”完成前打开,这里保存单独取到的应用版本用于比较 */
|
||||
const fallbackVersion = ref("");
|
||||
const changelogLang = computed(() => changelogLangFromLocale(locale.value || currentLocale()));
|
||||
const currentVersion = computed(() => props.updateInfo?.current_version || fallbackVersion.value);
|
||||
// 只保留低于当前版本的发布记录:新版本走“客户端更新”,这里专用于回退
|
||||
const olderReleases = computed(() => {
|
||||
const current = currentVersion.value;
|
||||
if (!current) return [];
|
||||
return historyReleases.value.filter((release) => isNewerRemoteVersion(current, release.tag));
|
||||
});
|
||||
const visibleOlderReleases = computed(() => olderReleases.value.slice(0, historyVisibleCount.value));
|
||||
const hasMoreOlderReleases = computed(() => historyVisibleCount.value < olderReleases.value.length);
|
||||
/** 历史版本默认折叠,避免挤压更新信息 */
|
||||
const olderReleasesExpanded = ref(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 });
|
||||
// 客户端页签常驻:既承载更新流程,也是“回退到旧版本”的入口
|
||||
available.push({ id: "app", label: t("settings.updateClient"), count: hasAppUpdate.value ? 1 : 0 });
|
||||
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 });
|
||||
@@ -103,10 +129,29 @@ const isCloseBlocked = computed(() => props.isInstallingUpdate);
|
||||
const blocksImplicitDismiss = computed(() => isCloseBlocked.value);
|
||||
const canIgnoreVersion = computed(() => props.updateInfo?.update_available === true && !props.isDownloadingUpdate && !props.isInstallingUpdate && !props.updateReady && !props.isUpdatingAll);
|
||||
|
||||
/** 客户端页签默认收起历史版本,用户手动切过页签后不再自动切换 */
|
||||
const userSelectedTab = ref(false);
|
||||
|
||||
function selectTab(tab: UpdateTab) {
|
||||
userSelectedTab.value = true;
|
||||
selectedTab.value = tab;
|
||||
}
|
||||
|
||||
// 客户端页签常驻,但它没有更新时不应抢占默认落点:优先显示真正有待更新内容的页签
|
||||
function pickDefaultTab(nextTabs: Array<{ id: UpdateTab; count: number }>): UpdateTab {
|
||||
return nextTabs.find((tab) => tab.count > 0)?.id ?? "app";
|
||||
}
|
||||
|
||||
watch(
|
||||
tabs,
|
||||
(nextTabs) => {
|
||||
if (!nextTabs.some((tab) => tab.id === selectedTab.value)) selectedTab.value = nextTabs[0]?.id ?? "app";
|
||||
if (!nextTabs.some((tab) => tab.id === selectedTab.value)) {
|
||||
selectedTab.value = pickDefaultTab(nextTabs);
|
||||
return;
|
||||
}
|
||||
if (!userSelectedTab.value && selectedTab.value === "app" && !hasAppUpdate.value && nextTabs.some((tab) => tab.count > 0)) {
|
||||
selectedTab.value = pickDefaultTab(nextTabs);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -120,6 +165,69 @@ function handleOpenChange(nextOpen: boolean) {
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
function openExternalUrl(url: string) {
|
||||
if (isTauriRuntime()) {
|
||||
import("@tauri-apps/plugin-shell").then(({ open }) => open(url));
|
||||
} else {
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOlderReleases() {
|
||||
const requestId = historyRequest.begin();
|
||||
historyLoading.value = true;
|
||||
historyError.value = "";
|
||||
try {
|
||||
// 弹窗可能在“检查更新”完成前打开:先取一次应用版本,否则无法判断哪些版本更低
|
||||
if (!props.updateInfo?.current_version && !fallbackVersion.value) {
|
||||
fallbackVersion.value = await getAppVersion().catch(() => "");
|
||||
}
|
||||
const data = await fetchChangelog(changelogLang.value);
|
||||
if (!historyRequest.isCurrent(requestId)) return;
|
||||
historyReleases.value = data.releases;
|
||||
} catch (error) {
|
||||
if (!historyRequest.isCurrent(requestId)) return;
|
||||
historyError.value = error instanceof Error ? error.message : String(error);
|
||||
} finally {
|
||||
if (historyRequest.isCurrent(requestId)) historyLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function showMoreOlderReleases() {
|
||||
historyVisibleCount.value += HISTORY_PAGE_SIZE;
|
||||
}
|
||||
|
||||
function toggleOlderReleases() {
|
||||
olderReleasesExpanded.value = !olderReleasesExpanded.value;
|
||||
}
|
||||
|
||||
function formatOlderReleaseDate(dateStr: string) {
|
||||
if (!dateStr) return "";
|
||||
const date = new Date(dateStr);
|
||||
if (Number.isNaN(date.getTime())) return dateStr;
|
||||
try {
|
||||
return new Intl.DateTimeFormat(locale.value || currentLocale(), { year: "numeric", month: "short", day: "numeric" }).format(date);
|
||||
} catch {
|
||||
return dateStr;
|
||||
}
|
||||
}
|
||||
|
||||
// 回退只提供跳转:旧版本安装包由用户自行下载安装,应用不做静默降级
|
||||
function openOlderReleasePage(tag: string) {
|
||||
openExternalUrl(resolveReleaseTagUrl(tag, props.updateDownloadSource));
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [open.value, changelogLang.value] as const,
|
||||
([isOpen]) => {
|
||||
if (!isOpen) return;
|
||||
// 每次打开弹窗都回到折叠状态,不残留上次展开的状态
|
||||
olderReleasesExpanded.value = false;
|
||||
void loadOlderReleases();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function handleReleaseNotesClick(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
const anchor = target.closest("a");
|
||||
@@ -127,11 +235,7 @@ function handleReleaseNotesClick(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
const url = anchor.getAttribute("href");
|
||||
if (!url || !/^https?:\/\//i.test(url)) return;
|
||||
if (isTauriRuntime()) {
|
||||
import("@tauri-apps/plugin-shell").then(({ open }) => open(url));
|
||||
} else {
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
openExternalUrl(url);
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -205,7 +309,7 @@ watch(
|
||||
: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"
|
||||
@click="selectTab(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>
|
||||
@@ -255,6 +359,33 @@ watch(
|
||||
</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>
|
||||
|
||||
<div v-if="olderReleases.length || historyError" data-update-history class="mt-4 border-t pt-3">
|
||||
<button type="button" class="flex w-full items-center justify-between gap-2 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground" @click="toggleOlderReleases">
|
||||
<span>{{ t("updates.historyTab") }}</span>
|
||||
<ChevronDown class="h-3.5 w-3.5 shrink-0 transition-transform" :class="olderReleasesExpanded ? 'rotate-180' : ''" />
|
||||
</button>
|
||||
<div v-if="olderReleasesExpanded" class="mt-2 space-y-2">
|
||||
<p class="text-xs text-muted-foreground">{{ t("updates.historyHint") }}</p>
|
||||
<div v-if="historyLoading && !visibleOlderReleases.length" class="flex items-center justify-center gap-2 py-6 text-muted-foreground">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t("updates.checking") }}
|
||||
</div>
|
||||
<div v-else-if="historyError" role="alert" class="rounded-md border border-destructive/30 bg-destructive/5 p-3 text-destructive">
|
||||
{{ t("updates.historyLoadFailed") }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div v-for="release in visibleOlderReleases" :key="release.tag" class="flex items-center justify-between gap-4 rounded-md border p-3">
|
||||
<div class="min-w-0">
|
||||
<div class="font-medium">{{ release.tag }}</div>
|
||||
<div class="mt-0.5 text-xs text-muted-foreground">{{ t("updates.historyPublishedOn", { date: formatOlderReleaseDate(release.date) }) }}</div>
|
||||
</div>
|
||||
<Button variant="outline" class="shrink-0" @click="openOlderReleasePage(release.tag)">{{ t("updates.openRelease") }}</Button>
|
||||
</div>
|
||||
<Button v-if="hasMoreOlderReleases" variant="ghost" class="w-full" @click="showMoreOlderReleases">{{ t("updates.historyLoadMore") }}</Button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
@@ -363,7 +494,7 @@ watch(
|
||||
</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">
|
||||
<DialogFooter v-else 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">
|
||||
<template v-if="isAnyComponentUpdating">
|
||||
<Button variant="ghost" class="shrink-0" @click="handleOpenChange(false)">{{ t("updates.updateInBackground") }}</Button>
|
||||
<Button class="shrink-0" disabled>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { createApp, defineComponent, h, reactive, nextTick, type App } from "vue";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import i18n from "@/i18n";
|
||||
import { useAppUpdater } from "@/composables/useAppUpdater";
|
||||
import { isNewerRemoteVersion, resolveReleaseTagUrl, useAppUpdater } from "@/composables/useAppUpdater";
|
||||
const mocks = vi.hoisted(() => ({
|
||||
checkForUpdates: vi.fn(),
|
||||
downloadUpdate: vi.fn(),
|
||||
@@ -71,6 +71,23 @@ afterEach(() => {
|
||||
app?.unmount();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
describe("older version release pages", () => {
|
||||
it("maps a historical version to the matching GitHub release page", () => {
|
||||
expect(resolveReleaseTagUrl("0.5.59", "official")).toBe("https://github.com/t8y2/dbx/releases/tag/v0.5.59");
|
||||
expect(resolveReleaseTagUrl("v0.5.59", "official")).toBe("https://github.com/t8y2/dbx/releases/tag/v0.5.59");
|
||||
});
|
||||
|
||||
it("uses the CNB release page when the CNB download source is selected", () => {
|
||||
expect(resolveReleaseTagUrl("v0.5.59", "cnb")).toBe("https://cnb.cool/dbxio.com/dbx/-/releases/tag/v0.5.59");
|
||||
});
|
||||
|
||||
it("treats only strictly older releases as rollback candidates", () => {
|
||||
expect(isNewerRemoteVersion("0.5.60", "v0.5.59")).toBe(true);
|
||||
expect(isNewerRemoteVersion("0.5.60", "v0.5.60")).toBe(false);
|
||||
expect(isNewerRemoteVersion("0.5.60", "v0.5.61")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("silent update lifecycle", () => {
|
||||
it("checks and shows a badge without downloading when automatic downloads are disabled", async () => {
|
||||
settings.autoUpdateApp = false;
|
||||
|
||||
@@ -106,6 +106,18 @@ export function resolveUpdateReleaseUrl(info: api.UpdateInfo | null, source: unk
|
||||
return info?.release_url || fallbackUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按版本 tag 解析“下载页”地址:CNB 源指向 CNB 的 release 页,其余指向 GitHub release 页。
|
||||
* 用于历史版本回退:只做跳转,不在应用内下载/安装旧版本。
|
||||
*/
|
||||
export function resolveReleaseTagUrl(tag: string, source: unknown): string {
|
||||
const normalizedTag = tagVersion(tag);
|
||||
if (normalizeUpdateDownloadSource(source) === "cnb") {
|
||||
return `https://cnb.cool/dbxio.com/dbx/-/releases/tag/${encodeURIComponent(normalizedTag)}`;
|
||||
}
|
||||
return `https://github.com/t8y2/dbx/releases/tag/${encodeURIComponent(normalizedTag)}`;
|
||||
}
|
||||
|
||||
export async function resolveUpdaterProxy(): Promise<string | undefined> {
|
||||
if (!isTauriRuntime()) return undefined;
|
||||
try {
|
||||
|
||||
@@ -423,6 +423,11 @@ export default withEnglishFallback({
|
||||
exitAndUpdate: "Çıx və yenilə",
|
||||
dockerUsersRun: "Docker istifadəçiləri bu əmri işlətməlidir",
|
||||
toUpdate: "yeniləmək üçün",
|
||||
historyTab: "Köhnə versiyalar",
|
||||
historyHint: "Köhnə versiyaya qayıtmazdan əvvəl məlumatlarınızın ehtiyat nüsxəsini çıxarın. Köhnə versiyalar yeni versiyaların yaratdığı məlumatları və ya etimad məlumatlarını oxuya bilməz.",
|
||||
historyPublishedOn: "{date} tarixində yayımlandı",
|
||||
historyLoadFailed: "Köhnə versiyalar yüklənə bilmədi. Zəhmət olmasa sonra yenidən cəhd edin.",
|
||||
historyLoadMore: "Daha çox yüklə",
|
||||
},
|
||||
sidebar: {
|
||||
connections: "ƏLAQƏLƏR",
|
||||
|
||||
@@ -646,6 +646,11 @@ export default {
|
||||
exitAndUpdate: "Exit & Update",
|
||||
dockerUsersRun: "Docker users should run",
|
||||
toUpdate: "to update",
|
||||
historyTab: "Older versions",
|
||||
historyHint: "Back up your data before rolling back. Older versions may not be able to read data or credentials created by newer versions.",
|
||||
historyPublishedOn: "Released {date}",
|
||||
historyLoadFailed: "Failed to load older versions. Please try again later.",
|
||||
historyLoadMore: "Load more",
|
||||
},
|
||||
pluginDock: {
|
||||
close: "Close terminal",
|
||||
|
||||
@@ -635,6 +635,11 @@ export default withEnglishFallback({
|
||||
exitAndUpdate: "Salir y actualizar",
|
||||
dockerUsersRun: "Los usuarios de Docker deben ejecutar",
|
||||
toUpdate: "para actualizar",
|
||||
historyTab: "Versiones anteriores",
|
||||
historyHint: "Haz una copia de seguridad de tus datos antes de volver a una versión anterior. Es posible que las versiones antiguas no puedan leer datos o credenciales creados por versiones más recientes.",
|
||||
historyPublishedOn: "Publicado el {date}",
|
||||
historyLoadFailed: "No se pudieron cargar las versiones anteriores. Inténtalo de nuevo más tarde.",
|
||||
historyLoadMore: "Cargar más",
|
||||
source: "Fuente de actualizaciones",
|
||||
sourceOfficial: "Fuente oficial",
|
||||
sourceCnb: "CNB",
|
||||
|
||||
@@ -629,6 +629,11 @@ export default withEnglishFallback({
|
||||
exitAndUpdate: "Esci e Aggiorna",
|
||||
dockerUsersRun: "Gli utenti Docker dovrebbero eseguire",
|
||||
toUpdate: "per aggiornare",
|
||||
historyTab: "Versioni precedenti",
|
||||
historyHint: "Esegui un backup dei dati prima di tornare a una versione precedente. Le versioni più vecchie potrebbero non riuscire a leggere dati o credenziali creati dalle versioni più recenti.",
|
||||
historyPublishedOn: "Pubblicato il {date}",
|
||||
historyLoadFailed: "Impossibile caricare le versioni precedenti. Riprova più tardi.",
|
||||
historyLoadMore: "Carica altro",
|
||||
source: "Sorgente di aggiornamento",
|
||||
sourceOfficial: "Sorgente ufficiale",
|
||||
sourceCnb: "CNB",
|
||||
|
||||
@@ -630,6 +630,11 @@ export default withEnglishFallback({
|
||||
exitAndUpdate: "終了してアップデート",
|
||||
dockerUsersRun: "Dockerユーザーは以下を実行してください",
|
||||
toUpdate: "更新するには",
|
||||
historyTab: "過去のバージョン",
|
||||
historyHint: "旧バージョンに戻す前にデータをバックアップしてください。新しいバージョンで作成したデータや認証情報を読み取れない場合があります。",
|
||||
historyPublishedOn: "{date} 公開",
|
||||
historyLoadFailed: "過去のバージョンの読み込みに失敗しました。後でもう一度お試しください。",
|
||||
historyLoadMore: "さらに読み込む",
|
||||
source: "更新ソース",
|
||||
sourceOfficial: "公式ソース",
|
||||
sourceCnb: "CNB",
|
||||
|
||||
@@ -637,6 +637,11 @@ export default withEnglishFallback({
|
||||
exitAndUpdate: "종료 후 업데이트",
|
||||
dockerUsersRun: "Docker 사용자는 다음 명령을 실행하세요",
|
||||
toUpdate: "업데이트하려면",
|
||||
historyTab: "이전 버전",
|
||||
historyHint: "이전 버전으로 되돌리기 전에 데이터를 백업하세요. 최신 버전에서 만든 데이터나 자격 증명을 읽지 못할 수 있습니다.",
|
||||
historyPublishedOn: "{date} 릴리스",
|
||||
historyLoadFailed: "이전 버전을 불러오지 못했습니다. 잠시 후 다시 시도하세요.",
|
||||
historyLoadMore: "더 보기",
|
||||
source: "업데이트 소스",
|
||||
sourceOfficial: "공식 소스",
|
||||
sourceCnb: "CNB",
|
||||
|
||||
@@ -630,6 +630,11 @@ export default withEnglishFallback({
|
||||
exitAndUpdate: "Sair e Atualizar",
|
||||
dockerUsersRun: "Usuários do Docker devem executar",
|
||||
toUpdate: "para atualizar",
|
||||
historyTab: "Versões anteriores",
|
||||
historyHint: "Faça backup dos seus dados antes de voltar para uma versão anterior. Versões antigas podem não conseguir ler dados ou credenciais criados por versões mais recentes.",
|
||||
historyPublishedOn: "Publicado em {date}",
|
||||
historyLoadFailed: "Falha ao carregar as versões anteriores. Tente novamente mais tarde.",
|
||||
historyLoadMore: "Carregar mais",
|
||||
source: "Fonte de atualização",
|
||||
sourceOfficial: "Fonte oficial",
|
||||
sourceCnb: "CNB",
|
||||
|
||||
@@ -592,6 +592,11 @@ export default withEnglishFallback({
|
||||
exitAndUpdate: "Выйти и обновить",
|
||||
dockerUsersRun: "Пользователям Docker следует выполнить",
|
||||
toUpdate: "для обновления",
|
||||
historyTab: "История версий",
|
||||
historyHint: "Перед откатом к старой версии сначала сделайте резервную копию данных; старая версия может не поддерживать чтение данных или учётных данных, созданных новой версией.",
|
||||
historyPublishedOn: "Опубликовано {date}",
|
||||
historyLoadFailed: "Не удалось загрузить историю версий, повторите попытку позже.",
|
||||
historyLoadMore: "Загрузить ещё",
|
||||
},
|
||||
pluginDock: {
|
||||
close: "Закрыть терминал",
|
||||
|
||||
@@ -425,6 +425,11 @@ export default withEnglishFallback({
|
||||
exitAndUpdate: "Çık ve Güncelle",
|
||||
dockerUsersRun: "Docker kullanıcıları güncellemek için",
|
||||
toUpdate: "komutunu çalıştırmalı",
|
||||
historyTab: "Eski sürümler",
|
||||
historyHint: "Eski bir sürüme dönmeden önce verilerinizi yedekleyin. Eski sürümler, yeni sürümlerle oluşturulan verileri veya kimlik bilgilerini okuyamayabilir.",
|
||||
historyPublishedOn: "{date} tarihinde yayınlandı",
|
||||
historyLoadFailed: "Eski sürümler yüklenemedi. Lütfen daha sonra tekrar deneyin.",
|
||||
historyLoadMore: "Daha fazla yükle",
|
||||
},
|
||||
sidebar: {
|
||||
connections: "BAĞLANTILAR",
|
||||
|
||||
@@ -562,6 +562,11 @@ export default withEnglishFallback({
|
||||
exitAndUpdate: "退出并更新",
|
||||
dockerUsersRun: "Docker 用户请运行",
|
||||
toUpdate: "更新",
|
||||
historyTab: "历史版本",
|
||||
historyHint: "回退到旧版本前请先备份数据;旧版本可能无法读取新版本创建的数据或凭据。",
|
||||
historyPublishedOn: "发布于 {date}",
|
||||
historyLoadFailed: "加载历史版本失败,请稍后重试。",
|
||||
historyLoadMore: "加载更多",
|
||||
},
|
||||
pluginDock: {
|
||||
close: "关闭终端",
|
||||
|
||||
@@ -633,6 +633,11 @@ export default withEnglishFallback({
|
||||
exitAndUpdate: "結束並更新",
|
||||
dockerUsersRun: "Docker 使用者請執行",
|
||||
toUpdate: "更新",
|
||||
historyTab: "歷史版本",
|
||||
historyHint: "回退至舊版本前請先備份資料;舊版本可能無法讀取新版本建立的資料或憑證。",
|
||||
historyPublishedOn: "發佈於 {date}",
|
||||
historyLoadFailed: "載入歷史版本失敗,請稍後重試。",
|
||||
historyLoadMore: "載入更多",
|
||||
source: "更新來源",
|
||||
sourceOfficial: "官方來源",
|
||||
sourceCnb: "CNB",
|
||||
|
||||
Reference in new issue
Block a user