feat(cms): recover drafts and failed imports with verified UI workflows
CI / check (push) Successful in 2m3s
CI / deploy (push) Successful in 1m31s
CI / publish-container (push) Successful in 1m41s

This commit is contained in:
Simo committed 2026-09-11 08:58:10 +02:00
1 parent 410a1e466c
commit 88790d1a0d
84 files changed
+3952 -288

No files matched your search

+29 -1
View File
@@ -3,6 +3,7 @@
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useEffect, useMemo, useRef, useState, useTransition } from "react";
import { useFormRecovery } from "@/hooks/use-form-recovery";
import { useUnsavedChanges } from "@/hooks/use-unsaved-changes";
import type { ArticleDraft } from "@/lib/article-draft";
import type { ArticleSaveResult } from "@/lib/article-input";
@@ -17,11 +18,13 @@ import { RichText } from "./rich-text";
export function ArticleForm({
action,
userId,
defaultValues,
articleKey = "new",
baseToken = "",
}: {
action: (formData: FormData) => Promise<ArticleSaveResult | undefined>;
userId?: string;
edit?: boolean;
articleKey?: string;
baseToken?: string;
@@ -45,6 +48,7 @@ export function ArticleForm({
const saving = useRef(false);
const editVersion = useRef(0);
function markDirty() {
localRecovery.changed();
editVersion.current++;
setDirty(true);
}
@@ -90,6 +94,24 @@ export function ArticleForm({
dirty,
restoreDraft,
);
const localRecovery = useFormRecovery({
serverDraft: recovery.draft,
userId,
kind: "article",
record: articleKey,
dirty,
snapshot: () => ({
title,
slug: slugTouched ? slug : slugify(title),
image,
shortStory: summary,
fullStory: body,
status,
publishAt,
}),
restore: (payload) =>
restoreDraft({ ...payload, baseToken } as ArticleDraft, false),
});
useFormErrorFocus(formRef, fieldErrors, pending);
const [preview, setPreview] = useState<ArticlePreviewData | null>(null);
const suggestedSlug = useMemo(() => slugify(title), [title]);
@@ -126,6 +148,7 @@ export function ArticleForm({
if (!Number.isNaN(date.getTime()))
data.set("publishAt", date.toISOString());
}
localRecovery.flush();
const submittedVersion = editVersion.current;
saving.current = true;
setSaveError(null);
@@ -139,7 +162,10 @@ export function ArticleForm({
setFieldErrors(result.fieldErrors ?? {});
return;
}
if (editVersion.current === submittedVersion) setDirty(false);
if (result?.ok && editVersion.current === submittedVersion) {
setDirty(false);
localRecovery.clear();
}
if (result?.ok) await recovery.clear().catch(() => {});
if (result?.ok && result.data?.redirectTo)
router.push(result.data.redirectTo);
@@ -164,6 +190,7 @@ export function ArticleForm({
>
<fieldset disabled={pending} className="contents">
<input type="hidden" name="baseToken" value={token} />
{localRecovery.panel}
{recovery.panel}
{saveError && (
<p role="alert" className="text-sm">
@@ -282,6 +309,7 @@ export function ArticleForm({
{t("body")}
</span>
<RichText
ariaLabel={t("body")}
key={bodyVersion}
onChange={() => {
markDirty();