feat(cms): recover drafts and failed imports with verified UI workflows
This commit is contained in:
1 parent
410a1e466c
commit
88790d1a0d
84 files changed
+3952
-288
No files matched your search
@@ -0,0 +1,18 @@
|
||||
import { useState } from "react";
|
||||
import { ImportAttachmentRecovery } from "@/components/admin/studio/import-attachment-recovery";
|
||||
export function AttachmentHarness() {
|
||||
const [resumed, setResumed] = useState(0);
|
||||
return (
|
||||
<>
|
||||
<ImportAttachmentRecovery
|
||||
jobId="aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
|
||||
classname="fixture_chair"
|
||||
disabled={false}
|
||||
onResumed={async () => {
|
||||
setResumed((value) => value + 1);
|
||||
}}
|
||||
/>
|
||||
<output aria-label="Resumed count">{resumed}</output>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { Toaster } from "sonner";
|
||||
import { EventForm } from "@/app/admin/events/event-form";
|
||||
import { CmsSettingsForm } from "@/app/admin/settings/cms-settings-form";
|
||||
import { TicketsTable } from "@/app/admin/tickets/tickets-table";
|
||||
import { ArticleForm } from "@/components/admin/article-form";
|
||||
import { FurnitureSyncQueue } from "@/components/admin/studio/furniture-sync-queue";
|
||||
import type { ArticleSaveResult } from "@/lib/article-input";
|
||||
import { themePaletteCss } from "@/lib/theme-css";
|
||||
import { PRESETS } from "@/lib/theme-presets";
|
||||
import messages from "@/messages/en.json";
|
||||
import { AttachmentHarness } from "./attachment-harness";
|
||||
import { FurnitureJobsHarness } from "./furniture-jobs-harness";
|
||||
import { fixtureAction } from "./server-actions";
|
||||
|
||||
const theme = document.createElement("style");
|
||||
theme.textContent = themePaletteCss(
|
||||
":root.dark",
|
||||
PRESETS["Atom (golden)"].dark,
|
||||
);
|
||||
document.head.appendChild(theme);
|
||||
const route = window.location.pathname;
|
||||
const root = document.getElementById("root");
|
||||
if (!root) throw Error("Fixture root missing");
|
||||
const rows = [
|
||||
{
|
||||
id: 1,
|
||||
subject: "Account assistance",
|
||||
creator: "SampleMember",
|
||||
creatorId: 7,
|
||||
category: "general",
|
||||
messages: 2,
|
||||
status: "open",
|
||||
priority: "normal",
|
||||
assignee: "Moderator",
|
||||
date: "01/01/2030",
|
||||
awaitingStaff: true,
|
||||
replyAgeMinutes: 90,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
subject: "Event question",
|
||||
creator: "AnotherMember",
|
||||
creatorId: 8,
|
||||
category: "events",
|
||||
messages: 1,
|
||||
status: "waiting",
|
||||
priority: "high",
|
||||
assignee: "—",
|
||||
date: "01/01/2030",
|
||||
awaitingStaff: false,
|
||||
replyAgeMinutes: 15,
|
||||
},
|
||||
];
|
||||
const saveArticle = async (form: FormData): Promise<ArticleSaveResult> =>
|
||||
fixtureAction("article", Object.fromEntries(form));
|
||||
|
||||
function ArticleFixture() {
|
||||
const [visible, setVisible] = useState(true);
|
||||
return (
|
||||
<>
|
||||
{route.includes("recovery") && (
|
||||
<button type="button" onClick={() => setVisible(false)}>
|
||||
Close editor fixture
|
||||
</button>
|
||||
)}
|
||||
{visible && (
|
||||
<ArticleForm
|
||||
userId={route.includes("recovery") ? "7" : undefined}
|
||||
action={saveArticle}
|
||||
articleKey="new"
|
||||
defaultValues={{
|
||||
title: "Community update",
|
||||
slug: "community-update",
|
||||
image: "/fixture/cover.svg",
|
||||
shortStory: "A short update for the community.",
|
||||
fullStory:
|
||||
"<p>Join the upcoming event and explore the hotel together.</p>",
|
||||
status: "draft",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
createRoot(root).render(
|
||||
<NextIntlClientProvider
|
||||
locale="en"
|
||||
timeZone="UTC"
|
||||
now={new Date("2030-01-01T12:00:00Z")}
|
||||
messages={messages}
|
||||
>
|
||||
<main data-admin className="min-w-0 mx-auto max-w-6xl p-5">
|
||||
<h1 className="mb-5 text-2xl font-bold">
|
||||
{route.includes("settings")
|
||||
? "CMS settings"
|
||||
: route.includes("tickets")
|
||||
? "Support desk"
|
||||
: route.includes("articles")
|
||||
? "News editor"
|
||||
: "Furniture synchronization"}
|
||||
</h1>
|
||||
{route.includes("attachment-harness") ? (
|
||||
<AttachmentHarness />
|
||||
) : route.includes("events/recovery") ? (
|
||||
<EventForm
|
||||
userId="7"
|
||||
eventTypes={[
|
||||
{ id: 1, name: "Community", slug: "community", color: "#123456" },
|
||||
]}
|
||||
defaultValues={{
|
||||
id: 12,
|
||||
title: "Original event",
|
||||
description: "Event details",
|
||||
typeId: 1,
|
||||
status: "draft",
|
||||
isRecurring: 0,
|
||||
startsAt: new Date("2030-01-01T12:00:00Z"),
|
||||
}}
|
||||
/>
|
||||
) : route.includes("jobs-harness") ? (
|
||||
<FurnitureJobsHarness />
|
||||
) : route.includes("settings") ? (
|
||||
<CmsSettingsForm
|
||||
canEdit
|
||||
values={{ default_dark: "0", cms_hotel_name: "Example Hotel" }}
|
||||
/>
|
||||
) : route.includes("tickets") ? (
|
||||
<TicketsTable
|
||||
data={{ rows, total: 2, page: 1, perPage: 20, lastPage: 1 }}
|
||||
/>
|
||||
) : route.includes("articles") ? (
|
||||
<ArticleFixture />
|
||||
) : (
|
||||
<FurnitureSyncQueue kind="official" />
|
||||
)}
|
||||
</main>
|
||||
<Toaster />
|
||||
</NextIntlClientProvider>,
|
||||
);
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useState } from "react";
|
||||
import { useFurnitureJobs } from "@/components/admin/studio/use-furniture-jobs";
|
||||
|
||||
export function FurnitureJobsHarness() {
|
||||
const [completions, setCompletions] = useState(0);
|
||||
const history = useFurnitureJobs(
|
||||
() => setCompletions((value) => value + 1),
|
||||
true,
|
||||
);
|
||||
return (
|
||||
<section aria-label="Import history hook">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void history.refresh();
|
||||
void history.refresh();
|
||||
}}
|
||||
>
|
||||
Refresh twice
|
||||
</button>
|
||||
<button type="button" onClick={() => void history.navigate("next")}>
|
||||
Next page
|
||||
</button>
|
||||
<button type="button" onClick={() => void history.navigate("previous")}>
|
||||
Previous page
|
||||
</button>
|
||||
<output aria-label="Page">{history.page}</output>
|
||||
<output aria-label="Completions">{completions}</output>
|
||||
<output aria-label="Loading">{String(history.loading)}</output>
|
||||
<output aria-label="Jobs">
|
||||
{history.jobs.map((job) => `${job.id}:${job.state}`).join(",")}
|
||||
</output>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
const router = {
|
||||
push: (url: string) => {
|
||||
window.location.href = url;
|
||||
},
|
||||
replace: (url: string) => {
|
||||
window.location.replace(url);
|
||||
},
|
||||
refresh: () => {},
|
||||
};
|
||||
export const useRouter = () => router;
|
||||
export const usePathname = () => window.location.pathname;
|
||||
export const useSearchParams = () =>
|
||||
new URLSearchParams(window.location.search);
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { AnchorHTMLAttributes } from "react";
|
||||
export default function FixtureLink({
|
||||
prefetch: _prefetch,
|
||||
...props
|
||||
}: AnchorHTMLAttributes<HTMLAnchorElement> & { prefetch?: boolean }) {
|
||||
return <a {...props} />;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
export async function fixtureAction(name: string, input: unknown = {}) {
|
||||
const response = await fetch(`/fixture/actions/${name}`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
if (!response.ok) throw Error(`Fixture action failed: ${response.status}`);
|
||||
return response.json();
|
||||
}
|
||||
export const saveManagedSettings = (input: unknown) =>
|
||||
fixtureAction("settings", input);
|
||||
export const listTableViews = (input: unknown) =>
|
||||
fixtureAction("list-views", input);
|
||||
export const saveTableView = (input: unknown) =>
|
||||
fixtureAction("save-view", input);
|
||||
export const deleteTableView = (input: unknown) =>
|
||||
fixtureAction("delete-view", input);
|
||||
export const loadArticleRecovery = (key: unknown) =>
|
||||
fixtureAction("load-recovery", { key });
|
||||
export const autosaveArticle = (...input: unknown[]) =>
|
||||
fixtureAction("autosave", input);
|
||||
export const clearArticleRecovery = (...input: unknown[]) =>
|
||||
fixtureAction("clear-recovery", input);
|
||||
export const uploadMediaAndReturn = () => fixtureAction("upload");
|
||||
|
||||
export const createEvent = (input: unknown) =>
|
||||
fixtureAction("create-event", input);
|
||||
export const updateEvent = (input: unknown) =>
|
||||
fixtureAction("update-event", input);
|
||||
Reference in new issue
Block a user