feat(demo): add on-demand demo data reset endpoint and UI button

This commit is contained in:
tianma-if committed 2026-07-23 00:43:55 +08:00
1 parent 7a26750e93
commit dff5a155ef
6 files changed
+108

No files matched your search

+20
View File
@@ -2226,6 +2226,26 @@ app.get("/api/v1/resources/:id/blob", async (c) => {
return new Response(object.body, { headers });
});
app.post("/api/v1/demo/reset", async (c) => {
if (!isDemoMode(c.env) && !isLocalDemoSeedEnabled(c.env)) {
return c.json(
{
error: {
code: "demo_mode_disabled",
message: "Demo reset is only available when demo mode or local demo seed is enabled",
},
},
400
);
}
await resetDemoData(c.env, Date.now());
return c.json({
success: true,
message: "Demo seed data successfully restored",
});
});
app.patch("/api/v1/memos/:id", zValidator("json", MemoUpdateSchema), async (c) => {
const denied = requireScopes(c, "write:memos");
+22
View File
@@ -19,6 +19,7 @@ import {
CheckCircle2,
CircleUserRound,
Download,
RotateCcw,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
@@ -236,6 +237,9 @@ export const NotebookPane = ({
authRequired,
onLogout,
isLoggingOut,
demoMode = false,
onResetDemo,
isResettingDemo = false,
}: {
user: AuthUser | null;
view: string;
@@ -264,6 +268,9 @@ export const NotebookPane = ({
authRequired: boolean;
onLogout: () => void;
isLoggingOut: boolean;
demoMode?: boolean;
onResetDemo?: () => void;
isResettingDemo?: boolean;
}) => {
const { t } = useTranslation();
const { isInstallable, install } = usePwaInstall();
@@ -481,6 +488,21 @@ export const NotebookPane = ({
<footer className="border-t border-slate-200 bg-white/80 px-3 pb-[max(0.5rem,env(safe-area-inset-bottom))] pt-2 backdrop-blur-sm">
<div className="space-y-1">
{demoMode && onResetDemo && (
<button
onClick={onResetDemo}
disabled={isResettingDemo}
className="flex h-8 w-full items-center gap-3 rounded-md px-3 text-left text-sm font-semibold leading-none text-amber-700 hover:bg-amber-50 transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 disabled:opacity-50"
type="button"
title={t("demo.resetTooltip")}
aria-label={t("demo.resetButton")}
>
<span className="flex h-4 w-4 shrink-0 items-center justify-center">
<RotateCcw className={cn("h-4 w-4 text-amber-600", isResettingDemo && "animate-spin")} />
</span>
<span className="min-w-0 flex-1 truncate">{isResettingDemo ? t("demo.resetting") : t("demo.resetButton")}</span>
</button>
)}
{isInstallable && (
<button
onClick={install}
+41
View File
@@ -662,6 +662,32 @@ export const WorkspaceApp = ({
const [notebookNameDialog, setNotebookNameDialog] = useState<NotebookNameDialogState | null>(null);
const [notebookDeleteConfirmation, setNotebookDeleteConfirmation] = useState<Notebook | null>(null);
const [appNoticeDialog, setAppNoticeDialog] = useState<AppNoticeDialogState | null>(null);
const [demoResetConfirmationOpen, setDemoResetConfirmationOpen] = useState(false);
const resetDemoMutation = useMutation({
mutationFn: () => api.resetDemo(),
onSuccess: () => {
setDemoResetConfirmationOpen(false);
void Promise.all([
queryClient.invalidateQueries({ queryKey: ["memos"] }),
queryClient.invalidateQueries({ queryKey: ["memo"] }),
queryClient.invalidateQueries({ queryKey: ["notebooks"] }),
queryClient.invalidateQueries({ queryKey: ["resources"] }),
queryClient.invalidateQueries({ queryKey: ["tags"] }),
]);
setAppNoticeDialog({
title: t("demo.resetSuccess"),
description: t("demo.resetSuccess"),
});
},
onError: () => {
setDemoResetConfirmationOpen(false);
setAppNoticeDialog({
title: t("demo.resetFailed"),
description: t("demo.resetFailed"),
});
},
});
const [multiSelectKeyDown, setMultiSelectKeyDown] = useState(false);
const [imageCompressionEnabled, setImageCompressionEnabled] = useState(readImageCompressionPreference);
const [desktopFocusMode, setDesktopFocusMode] = useState(readDesktopFocusModePreference);
@@ -2424,6 +2450,9 @@ export const WorkspaceApp = ({
setActivePane("memos");
}}
onEmptyTrash={handleEmptyTrash}
demoMode={demoMode}
onResetDemo={() => setDemoResetConfirmationOpen(true)}
isResettingDemo={resetDemoMutation.isPending}
/>
</Suspense>
)}
@@ -2720,6 +2749,18 @@ export const WorkspaceApp = ({
onConfirm={() => setAppNoticeDialog(null)}
/>
)}
{demoResetConfirmationOpen && (
<AppConfirmDialog
title={t("demo.resetTitle")}
description={t("demo.resetDescription")}
confirmLabel={t("demo.resetConfirm")}
cancelLabel={t("common.cancel")}
isWorking={resetDemoMutation.isPending}
tone="primary"
onCancel={() => setDemoResetConfirmationOpen(false)}
onConfirm={() => resetDemoMutation.mutate()}
/>
)}
{visibleActivePane !== "editor" && !memoSelectionModeActive && (
<MobileBottomNav
activeItem={mobileBottomNavActive}
+5
View File
@@ -423,4 +423,9 @@ export const api = {
method: "POST",
body: JSON.stringify(payload),
}),
resetDemo: () =>
request<{ success: true }>("/api/v1/demo/reset", {
method: "POST",
}),
};
+10
View File
@@ -757,4 +757,14 @@ export const enUS = {
dismiss: "Got it",
},
},
demo: {
resetTitle: "Restore Demo Memos",
resetDescription: "Restores sample notes and notebooks in the demo environment to their initial state. Any edits or changes made will be reset.",
resetConfirm: "Confirm Restore",
resetButton: "Restore Demo Notes",
resetTooltip: "Reset sample notes and notebooks in the demo environment",
resetSuccess: "Sample notes successfully restored!",
resetFailed: "Failed to restore sample notes. Please try again later.",
resetting: "Restoring...",
},
} as const;
+10
View File
@@ -757,4 +757,14 @@ export const zhCN = {
dismiss: "我知道了",
},
},
demo: {
resetTitle: "还原示例笔记",
resetDescription: "将 Demo 环境的示例笔记和笔记本恢复为初始状态。期间对示例笔记所做的任何编辑或修改都会被清空并还原。",
resetConfirm: "确认还原",
resetButton: "还原示例笔记",
resetTooltip: "重置演示环境中的示例笔记与笔记本",
resetSuccess: "示例笔记已成功还原!",
resetFailed: "还原示例笔记失败,请稍后重试。",
resetting: "正在还原...",
},
} as const;