feat(editorial): validate publications and preserve partial event updates
CI / check (push) Successful in 57s
CI / deploy (push) Successful in 18s
CI / publish-container (push) Successful in 1m20s

This commit is contained in:
Simo committed 2026-09-11 00:36:55 +02:00
1 parent 76f0420d64
commit db4acbb46e
14 files changed
+613 -16

No files matched your search

+39 -4
View File
@@ -7,10 +7,12 @@ import { useUnsavedChanges } from "@/hooks/use-unsaved-changes";
import type { ArticleDraft } from "@/lib/article-draft";
import type { ArticleSaveResult } from "@/lib/article-input";
import { slugify } from "@/lib/format";
import { publicationIssues } from "@/lib/publication-preflight";
import { ArticlePreview, type ArticlePreviewData } from "./article-preview";
import { useArticleRecovery } from "./article-recovery";
import { FormFieldError, useFormErrorFocus } from "./form-feedback";
import { MediaPicker } from "./media-picker";
import { PublicationPreflight } from "./publication-preflight";
import { RichText } from "./rich-text";
export function ArticleForm({
@@ -91,14 +93,33 @@ export function ArticleForm({
useFormErrorFocus(formRef, fieldErrors, pending);
const [preview, setPreview] = useState<ArticlePreviewData | null>(null);
const suggestedSlug = useMemo(() => slugify(title), [title]);
const effectiveSlug = slugify(slugTouched ? slug : suggestedSlug);
const issues = publicationIssues({
kind: "article",
image,
body,
slug: slugTouched ? slug : suggestedSlug,
normalizedSlug: effectiveSlug,
status,
publishAt,
});
return (
<form
ref={formRef}
onChangeCapture={markDirty}
onChangeCapture={() => {
markDirty();
if (formRef.current)
setBody(String(new FormData(formRef.current).get("fullStory") ?? ""));
}}
onSubmit={(event) => {
event.preventDefault();
if (saving.current) return;
if (
saving.current ||
(status !== "draft" &&
issues.some((issue) => issue.severity === "error"))
)
return;
const data = new FormData(event.currentTarget);
if (status === "scheduled" && publishAt) {
const date = new Date(publishAt);
@@ -262,7 +283,13 @@ export function ArticleForm({
</span>
<RichText
key={bodyVersion}
onChange={markDirty}
onChange={() => {
markDirty();
if (formRef.current)
setBody(
String(new FormData(formRef.current).get("fullStory") ?? ""),
);
}}
name="fullStory"
defaultValue={body}
placeholder={t("bodyPlaceholder")}
@@ -314,6 +341,10 @@ export function ArticleForm({
) : null}
</div>
<PublicationPreflight
issues={issues}
paths={["/news", `/news/${effectiveSlug}`, "/"]}
/>
<div className="sticky bottom-0 flex flex-wrap gap-2 border-t border-[var(--admin-border)] bg-[var(--admin-surface)] py-3">
<button
type="button"
@@ -334,7 +365,11 @@ export function ArticleForm({
<button
type="submit"
className="btn btn-primary"
disabled={pending}
disabled={
pending ||
(status !== "draft" &&
issues.some((issue) => issue.severity === "error"))
}
aria-busy={pending}
>
{pending ? tAction("saving") : t("save")}