feat(editorial): validate publications and preserve partial event updates
This commit is contained in:
1 parent
76f0420d64
commit
db4acbb46e
14 files changed
+613
-16
No files matched your search
@@ -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")}
|
||||
|
||||
Reference in new issue
Block a user