From 9a416eef7e765cef4b4b1fe3ab971307be465735 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Fri, 11 Jul 2025 17:38:09 +0200 Subject: [PATCH 1/7] feat: allow restore from published backup --- .../project-settings/project-settings.tsx | 25 +- .../project-settings/section-backups.tsx | 142 ++++++++++++ .../shared/nano-states/project-settings.ts | 11 +- .../postgrest/src/__generated__/db-types.ts | 218 +++++++++++------- .../migration.sql | 43 ++++ packages/project/package.json | 1 + packages/project/src/trpc/project-router.ts | 85 ++++++- pnpm-lock.yaml | 3 + 8 files changed, 432 insertions(+), 96 deletions(-) create mode 100644 apps/builder/app/builder/features/project-settings/section-backups.tsx create mode 100644 packages/prisma-client/prisma/migrations/20250710163439_restore_development_build/migration.sql diff --git a/apps/builder/app/builder/features/project-settings/project-settings.tsx b/apps/builder/app/builder/features/project-settings/project-settings.tsx index 833fd2869ad1..3651a26715c1 100644 --- a/apps/builder/app/builder/features/project-settings/project-settings.tsx +++ b/apps/builder/app/builder/features/project-settings/project-settings.tsx @@ -1,3 +1,4 @@ +import type { FunctionComponent } from "react"; import { useStore } from "@nanostores/react"; import { Dialog, @@ -11,22 +12,24 @@ import { ListItem, Text, } from "@webstudio-is/design-system"; -import { $openProjectSettings } from "~/shared/nano-states/project-settings"; +import { + $openProjectSettings, + type SectionName, +} from "~/shared/nano-states/project-settings"; +import { $isDesignMode } from "~/shared/nano-states"; +import { leftPanelWidth, rightPanelWidth } from "./utils"; import { SectionGeneral } from "./section-general"; import { SectionRedirects } from "./section-redirects"; import { SectionPublish } from "./section-publish"; import { SectionMarketplace } from "./section-marketplace"; -import { leftPanelWidth, rightPanelWidth } from "./utils"; -import type { FunctionComponent } from "react"; -import { $isDesignMode } from "~/shared/nano-states"; - -type SectionName = "general" | "redirects" | "publish" | "marketplace"; +import { SectionBackups } from "./section-backups"; const sections = new Map([ ["general", SectionGeneral], ["redirects", SectionRedirects], ["publish", SectionPublish], ["marketplace", SectionMarketplace], + ["backups", SectionBackups], ] as const); export const ProjectSettingsView = ({ @@ -39,6 +42,9 @@ export const ProjectSettingsView = ({ onOpenChange?: (isOpen: boolean) => void; }) => { const isDesignMode = useStore($isDesignMode); + const SectionComponent = currentSection + ? sections.get(currentSection) + : undefined; return ( - + - {currentSection === "general" && } - {currentSection === "redirects" && } - {currentSection === "publish" && } - {currentSection === "marketplace" && } + {SectionComponent && }
diff --git a/apps/builder/app/builder/features/project-settings/section-backups.tsx b/apps/builder/app/builder/features/project-settings/section-backups.tsx new file mode 100644 index 000000000000..30a7f33e0a54 --- /dev/null +++ b/apps/builder/app/builder/features/project-settings/section-backups.tsx @@ -0,0 +1,142 @@ +import { useStore } from "@nanostores/react"; +import { useEffect, useState } from "react"; +import { + Grid, + Text, + Button, + Select, + Dialog, + DialogTitle, + DialogTrigger, + DialogContent, + DialogClose, + Flex, + theme, + toast, + PanelBanner, + Link, + rawTheme, +} from "@webstudio-is/design-system"; +import { UpgradeIcon } from "@webstudio-is/icons"; +import { nativeClient, trpcClient } from "~/shared/trpc/trpc-client"; +import { $project, $userPlanFeatures } from "~/shared/nano-states"; +import { sectionSpacing } from "./utils"; +import cmsUpgradeBanner from "../settings-panel/cms-upgrade-banner.svg?url"; + +const formatPublishDate = (date: string) => { + try { + const formatter = new Intl.DateTimeFormat("en", { + dateStyle: "long", + timeStyle: "short", + }); + return formatter.format(new Date(date)); + } catch { + return date; + } +}; + +export const SectionBackups = () => { + const { hasProPlan } = useStore($userPlanFeatures); + const { data, load } = trpcClient.project.publishedBuilds.useQuery(); + const projectId = $project.get()?.id ?? ""; + useEffect(() => { + load({ projectId }); + }, [projectId]); + const options = data?.success ? data.data : []; + const [backupBuild = options.at(0), setBackupBuild] = useState< + undefined | (typeof options)[number] + >(); + const restore = async () => { + if (!backupBuild?.buildId) { + return; + } + const result = await nativeClient.project.restoreDevelopmentBuild.mutate({ + projectId, + fromBuildId: backupBuild.buildId, + }); + if (result.success) { + location.reload(); + return; + } + toast.error(result.error); + }; + + return ( + + Backups + option.buildId ?? ""} - getLabel={(option) => { - if (!option.createdAt) { - return; - } - let label = formatPublishDate(option.createdAt); - if (option.domains) { - label += ` (${option.domains})`; - } - return label; - }} - value={backupBuild} - onChange={setBackupBuild} - /> + {options.length ? ( + option.buildId ?? ""} - getLabel={(option) => { - if (!option.createdAt) { - return; - } - let label = formatPublishDate(option.createdAt); - if (option.domains) { - label += ` (${option.domains})`; - } - return label; - }} - value={backupBuild} - onChange={setBackupBuild} - /> - ) : ( - No backups - )} +