Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { FunctionComponent } from "react";
import { useStore } from "@nanostores/react";
import {
Dialog,
Expand All @@ -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<SectionName, FunctionComponent>([
["general", SectionGeneral],
["redirects", SectionRedirects],
["publish", SectionPublish],
["marketplace", SectionMarketplace],
["backups", SectionBackups],
] as const);

export const ProjectSettingsView = ({
Expand All @@ -39,6 +42,9 @@ export const ProjectSettingsView = ({
onOpenChange?: (isOpen: boolean) => void;
}) => {
const isDesignMode = useStore($isDesignMode);
const SectionComponent = currentSection
? sections.get(currentSection)
: undefined;

return (
<Dialog
Expand Down Expand Up @@ -100,12 +106,9 @@ export const ProjectSettingsView = ({
})}
</Flex>
</List>
<ScrollArea>
<ScrollArea css={{ width: "100%" }}>
<Grid gap={2} css={{ py: theme.spacing[5] }}>
{currentSection === "general" && <SectionGeneral />}
{currentSection === "redirects" && <SectionRedirects />}
{currentSection === "publish" && <SectionPublish />}
{currentSection === "marketplace" && <SectionMarketplace />}
{SectionComponent && <SectionComponent />}
<div />
</Grid>
</ScrollArea>
Expand Down
146 changes: 146 additions & 0 deletions apps/builder/app/builder/features/project-settings/section-backups.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
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 });
}, [load, 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 (
<Grid gap={2} css={sectionSpacing}>
<Text variant="titles">Backups</Text>
<Select
placeholder="No backups"
options={options}
getValue={(option) => 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}
/>
<Dialog>
<DialogTrigger asChild>
<Button
css={{ justifySelf: "start" }}
disabled={!hasProPlan || options.length === 0}
>
Restore
</Button>
</DialogTrigger>
<DialogContent width={320}>
<DialogTitle>Restore published version</DialogTitle>
<Flex
direction="column"
css={{ padding: theme.panel.padding }}
gap={2}
>
<Text>
Are you sure you want to restore the project to its published
version?
</Text>
{backupBuild?.createdAt && (
<Text color="destructive">
All changes made after{" "}
{formatPublishDate(backupBuild.createdAt)} will be lost.
</Text>
)}
<Flex gap="2" justify="end">
<DialogClose>
<Button color="ghost">Cancel</Button>
</DialogClose>
<DialogClose>
<Button color="destructive" onClick={restore}>
Restore
</Button>
</DialogClose>
</Flex>
</Flex>
</DialogContent>
</Dialog>
{!hasProPlan && (
<PanelBanner>
<img
src={cmsUpgradeBanner}
alt="Upgrade for backups"
width={rawTheme.spacing[28]}
style={{ aspectRatio: "4.1" }}
/>
<Text variant="regularBold">Upgrade to restore from backups</Text>
<Flex align="center" gap={1}>
<UpgradeIcon />
<Link
color="inherit"
target="_blank"
href="https://webstudio.is/pricing"
>
Upgrade to Pro
</Link>
</Flex>
</PanelBanner>
)}
</Grid>
);
};
7 changes: 1 addition & 6 deletions apps/builder/app/shared/help.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import {
ContentIcon,
DiscordIcon,
LifeBuoyIcon,
YoutubeIcon,
} from "@webstudio-is/icons";
import { ContentIcon, DiscordIcon, YoutubeIcon } from "@webstudio-is/icons";

export const help = [
{
Expand Down
11 changes: 8 additions & 3 deletions apps/builder/app/shared/nano-states/project-settings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { atom } from "nanostores";

export const $openProjectSettings = atom<
"general" | "redirects" | "publish" | "marketplace" | undefined
>();
export type SectionName =
| "general"
| "redirects"
| "publish"
| "marketplace"
| "backups";

export const $openProjectSettings = atom<SectionName | undefined>();
Loading