@anthusai/papyrus 1.4.0-next.1 → 1.4.0-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/amplify/data/resource.js +134 -3
- package/amplify/data/resource.ts +145 -3
- package/amplify/data/schema.js +131 -0
- package/amplify/data/schema.ts +142 -0
- package/amplify/functions/category-action/handler.ts +272 -20
- package/amplify/functions/console-chat-responder/src/main.rs +1 -1
- package/amplify/functions/content-actions/handler.py +23 -0
- package/amplify/functions/content-actions/requirements.txt +1 -1
- package/amplify/functions/content-actions/resource.js +1 -1
- package/amplify/functions/content-actions/resource.ts +1 -1
- package/amplify/functions/renditions/requirements.txt +1 -1
- package/amplify/functions/shared/curation-delegation-flow.ts +147 -0
- package/amplify/functions/shared/curation-delegation.ts +115 -0
- package/amplify/functions/shared/python-bundle.js +3 -3
- package/amplify/functions/shared/python-bundle.ts +3 -3
- package/amplify/functions/shared/reference-curation-metadata.ts +51 -0
- package/amplify/functions/shared/slack-events.ts +2 -2
- package/amplify/publication/renditions-function.js +1 -1
- package/amplify/publication/renditions-function.ts +1 -1
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -1
- package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -1
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +2 -1
- package/app/[year]/[month]/[day]/page.tsx +3 -0
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +2 -0
- package/app/api/archive/editions/route.ts +2 -0
- package/app/api/auth/prospectus/accounts/route.ts +9 -0
- package/app/api/local-preview/derive/route.ts +28 -0
- package/app/api/local-preview/media/route.ts +25 -0
- package/app/archive/page.tsx +3 -0
- package/app/globals.css +28 -28
- package/app/layout.tsx +8 -5
- package/app/newsroom/[section]/[...selection]/page.tsx +0 -1
- package/app/newsroom/[section]/page.tsx +3 -2
- package/app/newsroom/articles/page.tsx +8 -1
- package/app/newsroom/insights/[threadId]/page.tsx +5 -16
- package/app/newsroom/insights/page.tsx +4 -12
- package/app/newsroom/sections/[sectionId]/page.tsx +3 -0
- package/app/page.tsx +30 -1
- package/components/administration-delegations-panel.tsx +156 -0
- package/components/curation-delegation-client.ts +104 -0
- package/components/item-stream-view.tsx +63 -0
- package/components/markus-ir-preview.tsx +47 -2
- package/components/news-desk-page.tsx +0 -1
- package/components/news-desk-taxonomy-client.ts +82 -242
- package/components/newsroom-article-editor.tsx +138 -40
- package/components/newsroom-assignment-detail.tsx +79 -0
- package/components/newsroom-assignments-view.tsx +1 -1
- package/components/newsroom-board-view.tsx +128 -51
- package/components/newsroom-newsprint-board.css +5 -46
- package/components/newsroom-ops-overview.tsx +27 -5
- package/components/newsroom-organizer-shell.tsx +26 -8
- package/components/newsroom-organizer.css +267 -12
- package/components/newsroom-references-view.tsx +100 -23
- package/components/newsroom-topics-view.tsx +1 -1
- package/components/priming-proof-page.tsx +17 -8
- package/components/prospectus-account-chooser.tsx +131 -0
- package/components/reference-cyclotron-status.tsx +5 -5
- package/components/reference-cyclotron-suggestion.tsx +143 -0
- package/components/reference-live-agreement.tsx +62 -0
- package/components/reference-section-suggestion.tsx +104 -0
- package/components/story-reporting-trail-client.ts +91 -0
- package/components/story-reporting-trail.tsx +115 -0
- package/components/topic-steering-workspace.tsx +245 -678
- package/components/ui/sheet.tsx +10 -4
- package/components/use-paged-content-item-versions.ts +151 -0
- package/components/wiki-concepts-view.tsx +153 -0
- package/components/wiki-index-client.ts +51 -0
- package/infra/amplify-app-shell.js +17 -3
- package/infra/amplify-app-shell.ts +18 -3
- package/infra/site-config.js +6 -1
- package/infra/site-config.ts +7 -1
- package/lib/approved-priming-proof-reports.ts +6 -3
- package/lib/approved-priming-proofs/papyrus.json +90 -0
- package/lib/blog-feature-solver.ts +1 -109
- package/lib/category-repository.ts +1 -1
- package/lib/content-actions-client.ts +3 -0
- package/lib/content-actions-demo.ts +55 -0
- package/lib/content-item-lifecycle.ts +34 -5
- package/lib/content-source-context.ts +9 -1
- package/lib/curation-delegation-helpers.ts +69 -0
- package/lib/cyclotron-suggestions.ts +263 -0
- package/lib/front-matter-fields.ts +42 -0
- package/lib/front-mosaic.ts +2 -1
- package/lib/graphql-content-repository.ts +42 -7
- package/lib/item-stream.ts +47 -0
- package/lib/layout-plan.ts +3 -2
- package/lib/layout-warnings.ts +8 -0
- package/lib/local-preview-section-fixtures.ts +72 -0
- package/lib/local-preview-seed.ts +196 -7
- package/lib/local-preview-tenant.ts +27 -0
- package/lib/newsroom-articles.ts +2 -0
- package/lib/newsroom-assignments.ts +14 -1
- package/lib/newsroom-board.ts +52 -7
- package/lib/newsroom-demo-dashboard.ts +1 -1
- package/lib/newsroom-forum-routes.ts +2 -43
- package/lib/newsroom-index-filters.ts +3 -40
- package/lib/newsroom-media-backend.ts +5 -0
- package/lib/newsroom-nav.ts +1 -3
- package/lib/newsroom-organizer-nav.ts +8 -14
- package/lib/newsroom-section-routing.ts +1 -1
- package/lib/newsroom-sections.ts +12 -0
- package/lib/papyrus-web-locations.ts +3 -24
- package/lib/priming-proof-report.ts +1 -0
- package/lib/prospectus-account-client.ts +90 -0
- package/lib/prospectus-sign-in-routes.ts +45 -0
- package/lib/prospectus-site-identity.ts +13 -1
- package/lib/prospectus-tenant.ts +22 -0
- package/lib/reader-static-params.ts +2 -1
- package/lib/relevance-decisions.ts +29 -0
- package/lib/reporting-story-budget.ts +2 -2
- package/lib/semantic-graph.ts +5 -5
- package/lib/site-brand.ts +20 -0
- package/lib/site-env.ts +5 -0
- package/lib/site-section-suggestions.ts +120 -0
- package/lib/site-stack.ts +1 -1
- package/lib/story-reporting-trail.ts +106 -0
- package/lib/tenant-keys.ts +32 -0
- package/lib/wiki.ts +56 -0
- package/package.json +34 -2
- package/renderers/pretext/rhythm-article-page.tsx +1 -18
- package/routes.manifest.json +34 -7
|
@@ -14,10 +14,15 @@ import {
|
|
|
14
14
|
DialogTitle,
|
|
15
15
|
} from "./ui/dialog";
|
|
16
16
|
import { Input } from "./ui/input";
|
|
17
|
-
import {
|
|
17
|
+
import { isLocalPreviewEnabled } from "../lib/local-preview";
|
|
18
|
+
import { newsroomDeskOptions } from "../lib/newsroom-sections";
|
|
19
|
+
import { StoryReportingTrailView } from "./story-reporting-trail";
|
|
20
|
+
import { loadStoryReportingTrail } from "./story-reporting-trail-client";
|
|
21
|
+
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "./ui/select";
|
|
18
22
|
import { Spinner } from "./ui/spinner";
|
|
19
23
|
import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
|
|
20
24
|
import { Textarea } from "./ui/textarea";
|
|
25
|
+
import { FRONT_MATTER_BLURB_KEY, FRONT_MATTER_SUBTITLE_KEY, FRONT_MATTER_TITLE_KEY, readFrontMatterText, writeFrontMatterText } from "../lib/front-matter-fields";
|
|
21
26
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
|
|
22
27
|
import { ContentActionFailure, type ContentActionError } from "../lib/content-actions-client";
|
|
23
28
|
import { newsroomHref } from "../lib/newsroom-base-path";
|
|
@@ -59,7 +64,7 @@ type ValidationState =
|
|
|
59
64
|
| { status: "unavailable"; message: string };
|
|
60
65
|
|
|
61
66
|
type Notice = { tone: "ok" | "error" | "info"; text: string };
|
|
62
|
-
type MobileTab = "edit" | "preview";
|
|
67
|
+
type MobileTab = "edit" | "preview" | "trail" | "references";
|
|
63
68
|
|
|
64
69
|
function lineStartIndex(text: string, line: number): number {
|
|
65
70
|
let index = 0;
|
|
@@ -91,10 +96,15 @@ function failureMessage(error: unknown): string {
|
|
|
91
96
|
return error instanceof Error ? error.message : "The request failed.";
|
|
92
97
|
}
|
|
93
98
|
|
|
94
|
-
|
|
99
|
+
const NO_DESK_VALUE = "__no-desk__";
|
|
100
|
+
|
|
101
|
+
export function NewsroomArticleEditor({ articleId, demo, embedded = false }: { articleId: string; demo: boolean; embedded?: boolean }) {
|
|
95
102
|
const session = useOptionalNewsDeskClient();
|
|
96
|
-
const blocked = !demo && articlesAccessBlocked(session?.shell.phase);
|
|
97
|
-
const
|
|
103
|
+
const blocked = !(demo || isLocalPreviewEnabled()) && articlesAccessBlocked(session?.shell.phase);
|
|
104
|
+
const previewMode = demo || isLocalPreviewEnabled();
|
|
105
|
+
const backend: ArticlesBackend = useMemo(() => resolveArticlesBackend(previewMode), [previewMode]);
|
|
106
|
+
const [activeArticleId, setActiveArticleId] = useState(articleId);
|
|
107
|
+
useEffect(() => { setActiveArticleId(articleId); }, [articleId]);
|
|
98
108
|
const isNew = articleId === "new";
|
|
99
109
|
|
|
100
110
|
const [editor, setEditor] = useState<NewsroomArticleEditorState | null>(isNew ? emptyArticle() : null);
|
|
@@ -108,8 +118,15 @@ export function NewsroomArticleEditor({ articleId, demo }: { articleId: string;
|
|
|
108
118
|
const [notice, setNotice] = useState<Notice | null>(null);
|
|
109
119
|
const [conflictOpen, setConflictOpen] = useState(false);
|
|
110
120
|
const [unpublishOpen, setUnpublishOpen] = useState(false);
|
|
111
|
-
const [mobileTab, setMobileTab] = useState<MobileTab>("
|
|
121
|
+
const [mobileTab, setMobileTab] = useState<MobileTab>("preview");
|
|
122
|
+
const [focusedReferenceId, setFocusedReferenceId] = useState<string | null>(null);
|
|
123
|
+
const [lineageId, setLineageId] = useState<string | null>(null);
|
|
124
|
+
const [latestVersion, setLatestVersion] = useState<{ id: string; number: number } | null>(null);
|
|
125
|
+
const latestVersionNumber = latestVersion?.number ?? null;
|
|
126
|
+
const [changeReason, setChangeReason] = useState("");
|
|
127
|
+
const citedKeys = useMemo(() => Object.values(((bodyIr as { papyrus?: { citations?: Record<string, string[]> } } | null)?.papyrus?.citations) ?? {}).flat(), [bodyIr]);
|
|
112
128
|
const [sections, setSections] = useState<string[]>([]);
|
|
129
|
+
const deskOptions = useMemo(() => newsroomDeskOptions(session?.shell.dashboard?.newsroomSections ?? []), [session?.shell.dashboard?.newsroomSections]);
|
|
113
130
|
const [reloadToken, setReloadToken] = useState(0);
|
|
114
131
|
const [imageUrls, setImageUrls] = useState<Record<string, string>>({});
|
|
115
132
|
const [imageUploading, setImageUploading] = useState(false);
|
|
@@ -118,17 +135,18 @@ export function NewsroomArticleEditor({ articleId, demo }: { articleId: string;
|
|
|
118
135
|
const frontMatterRef = useRef<HTMLTextAreaElement | null>(null);
|
|
119
136
|
const bodyRef = useRef<HTMLTextAreaElement | null>(null);
|
|
120
137
|
const imageInputRef = useRef<HTMLInputElement | null>(null);
|
|
121
|
-
const mediaBackend = useMemo(() => resolveMediaBackend(
|
|
138
|
+
const mediaBackend = useMemo(() => resolveMediaBackend(previewMode), [previewMode]);
|
|
122
139
|
const validationSequence = useRef(0);
|
|
123
140
|
|
|
124
141
|
useEffect(() => {
|
|
125
142
|
if (blocked || isNew) return;
|
|
126
143
|
let cancelled = false;
|
|
127
144
|
backend
|
|
128
|
-
.loadArticle(
|
|
145
|
+
.loadArticle(activeArticleId)
|
|
129
146
|
.then((article) => {
|
|
130
147
|
if (cancelled) return;
|
|
131
148
|
setEditor(stateFromArticle(article));
|
|
149
|
+
setLineageId(article.lineageId ?? null);
|
|
132
150
|
setAliases(article.aliases);
|
|
133
151
|
setEverPublished(article.everPublished);
|
|
134
152
|
setSlugEdited(true);
|
|
@@ -140,7 +158,7 @@ export function NewsroomArticleEditor({ articleId, demo }: { articleId: string;
|
|
|
140
158
|
return () => {
|
|
141
159
|
cancelled = true;
|
|
142
160
|
};
|
|
143
|
-
}, [
|
|
161
|
+
}, [activeArticleId, backend, blocked, isNew, reloadToken]);
|
|
144
162
|
|
|
145
163
|
useEffect(() => {
|
|
146
164
|
if (blocked) return;
|
|
@@ -234,8 +252,12 @@ export function NewsroomArticleEditor({ articleId, demo }: { articleId: string;
|
|
|
234
252
|
patchEditor(patch);
|
|
235
253
|
};
|
|
236
254
|
|
|
255
|
+
const changeFrontMatterField = (key: string, value: string) => {
|
|
256
|
+
if (editor) changeFrontMatter(writeFrontMatterText(editor.frontMatterYaml, key, value));
|
|
257
|
+
};
|
|
258
|
+
|
|
237
259
|
const goToLine = (location: ErrorLocation) => {
|
|
238
|
-
setMobileTab("
|
|
260
|
+
setMobileTab("preview");
|
|
239
261
|
window.setTimeout(() => {
|
|
240
262
|
const target = location.field === "frontMatterYaml" ? frontMatterRef.current : bodyRef.current;
|
|
241
263
|
if (!target) return;
|
|
@@ -274,6 +296,41 @@ export function NewsroomArticleEditor({ articleId, demo }: { articleId: string;
|
|
|
274
296
|
}
|
|
275
297
|
};
|
|
276
298
|
|
|
299
|
+
useEffect(() => {
|
|
300
|
+
if (!lineageId || isNew) { setLatestVersion(null); return; }
|
|
301
|
+
let cancelled = false;
|
|
302
|
+
loadStoryReportingTrail(lineageId, activeArticleId)
|
|
303
|
+
.then(trail => { if (!cancelled) { const newest = trail.versions[0]; setLatestVersion(newest ? { id: newest.id, number: newest.versionNumber } : null); } })
|
|
304
|
+
.catch(() => { if (!cancelled) setLatestVersion(null); });
|
|
305
|
+
return () => { cancelled = true; };
|
|
306
|
+
}, [activeArticleId, isNew, lineageId, reloadToken]);
|
|
307
|
+
|
|
308
|
+
const saveNewVersion = useCallback(async () => {
|
|
309
|
+
if (!editor || editor.id === null) return;
|
|
310
|
+
setBusy("save");
|
|
311
|
+
try {
|
|
312
|
+
const result = await backend.actions.saveItemDraft({
|
|
313
|
+
id: editor.id,
|
|
314
|
+
type: editor.type,
|
|
315
|
+
slug: editor.slug,
|
|
316
|
+
section: editor.section.trim() ? editor.section.trim() : null,
|
|
317
|
+
frontMatterYaml: editor.frontMatterYaml,
|
|
318
|
+
bodyMarkus: editor.bodyMarkus,
|
|
319
|
+
aliases,
|
|
320
|
+
expectedContentHash: editor.contentHash,
|
|
321
|
+
newVersion: true,
|
|
322
|
+
changeReason: changeReason.trim() || null,
|
|
323
|
+
});
|
|
324
|
+
setChangeReason("");
|
|
325
|
+
setActiveArticleId(result.item.id);
|
|
326
|
+
setNotice({ tone: "ok", text: `Saved as version ${result.item.versionNumber ?? ""}.` });
|
|
327
|
+
} catch (error) {
|
|
328
|
+
setNotice({ tone: "error", text: failureMessage(error) });
|
|
329
|
+
} finally {
|
|
330
|
+
setBusy(null);
|
|
331
|
+
}
|
|
332
|
+
}, [aliases, backend, changeReason, editor]);
|
|
333
|
+
|
|
277
334
|
const saveDraft = useCallback(async (): Promise<string | null> => {
|
|
278
335
|
if (!editor) return null;
|
|
279
336
|
setBusy("save");
|
|
@@ -400,8 +457,9 @@ export function NewsroomArticleEditor({ articleId, demo }: { articleId: string;
|
|
|
400
457
|
|
|
401
458
|
const slugLocked = everPublished;
|
|
402
459
|
const invalid = validation.status === "invalid" || validation.status === "unavailable";
|
|
403
|
-
const
|
|
404
|
-
const
|
|
460
|
+
const isOlderVersion = latestVersionNumber !== null && editor.versionNumber !== null && editor.versionNumber < latestVersionNumber;
|
|
461
|
+
const publishBlocked = isOlderVersion || invalid || validation.status === "checking" || busy !== null || !editor.slug.trim();
|
|
462
|
+
const saveBlocked = isOlderVersion || invalid || busy !== null || !editor.slug.trim() || (!editor.dirty && editor.id !== null);
|
|
405
463
|
const stagingUrl = stagingPreviewUrl(
|
|
406
464
|
process.env.NEXT_PUBLIC_PAPYRUS_STAGING_URL,
|
|
407
465
|
editor.type,
|
|
@@ -480,6 +538,16 @@ export function NewsroomArticleEditor({ articleId, demo }: { articleId: string;
|
|
|
480
538
|
{busy === "save" ? <Spinner /> : null}
|
|
481
539
|
Save draft
|
|
482
540
|
</Button>
|
|
541
|
+
<Button
|
|
542
|
+
data-newsroom-article-save-version
|
|
543
|
+
disabled={isOlderVersion || invalid || busy !== null || editor.id === null || !editor.dirty}
|
|
544
|
+
onClick={() => void saveNewVersion()}
|
|
545
|
+
size="sm"
|
|
546
|
+
type="button"
|
|
547
|
+
variant="outline"
|
|
548
|
+
>
|
|
549
|
+
Save as new version
|
|
550
|
+
</Button>
|
|
483
551
|
<Button
|
|
484
552
|
data-newsroom-article-publish
|
|
485
553
|
disabled={publishBlocked}
|
|
@@ -493,7 +561,7 @@ export function NewsroomArticleEditor({ articleId, demo }: { articleId: string;
|
|
|
493
561
|
{editor.status === "published" ? (
|
|
494
562
|
<Button
|
|
495
563
|
data-newsroom-article-unpublish
|
|
496
|
-
disabled={busy !== null}
|
|
564
|
+
disabled={isOlderVersion || busy !== null}
|
|
497
565
|
onClick={() => setUnpublishOpen(true)}
|
|
498
566
|
size="sm"
|
|
499
567
|
type="button"
|
|
@@ -511,16 +579,9 @@ export function NewsroomArticleEditor({ articleId, demo }: { articleId: string;
|
|
|
511
579
|
</div>
|
|
512
580
|
) : null}
|
|
513
581
|
|
|
514
|
-
<
|
|
515
|
-
<TabsList>
|
|
516
|
-
<TabsTrigger className="min-w-[6rem]" value="edit">Edit</TabsTrigger>
|
|
517
|
-
<TabsTrigger className="min-w-[6rem]" value="preview">Preview</TabsTrigger>
|
|
518
|
-
</TabsList>
|
|
519
|
-
</Tabs>
|
|
520
|
-
|
|
521
|
-
<div className="grid gap-6 lg:grid-cols-2">
|
|
582
|
+
<div className="grid gap-6">
|
|
522
583
|
<div
|
|
523
|
-
className=
|
|
584
|
+
className="min-w-0 space-y-4"
|
|
524
585
|
data-newsroom-article-edit-column
|
|
525
586
|
>
|
|
526
587
|
<div className="grid gap-3 sm:grid-cols-3">
|
|
@@ -543,18 +604,29 @@ export function NewsroomArticleEditor({ articleId, demo }: { articleId: string;
|
|
|
543
604
|
</Select>
|
|
544
605
|
</label>
|
|
545
606
|
<label className="space-y-1 text-sm font-medium">
|
|
546
|
-
<span>
|
|
547
|
-
<
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
value={editor.section}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
607
|
+
<span>Desk</span>
|
|
608
|
+
<Select
|
|
609
|
+
onValueChange={(value) => {
|
|
610
|
+
if (typeof value === "string") patchEditor({ section: value === NO_DESK_VALUE ? "" : value });
|
|
611
|
+
}}
|
|
612
|
+
value={deskOptions.some(option => option.value === editor.section) ? editor.section : NO_DESK_VALUE}
|
|
613
|
+
>
|
|
614
|
+
<SelectTrigger aria-label="Desk" className="w-full" data-newsroom-article-section>
|
|
615
|
+
<SelectValue>{deskOptions.find(option => option.value === editor.section)?.label ?? "No desk"}</SelectValue>
|
|
616
|
+
</SelectTrigger>
|
|
617
|
+
<SelectContent>
|
|
618
|
+
<SelectItem value={NO_DESK_VALUE}>No desk</SelectItem>
|
|
619
|
+
{(["core", "rotating"] as const).map((kind) => deskOptions.some(option => option.kind === kind) ? (
|
|
620
|
+
<SelectGroup key={kind}>
|
|
621
|
+
<SelectLabel>{kind === "core" ? "Core desks" : "Rotating desks"}</SelectLabel>
|
|
622
|
+
{deskOptions.filter(option => option.kind === kind).map((option) => (
|
|
623
|
+
<SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>
|
|
624
|
+
))}
|
|
625
|
+
</SelectGroup>
|
|
626
|
+
) : null)}
|
|
627
|
+
</SelectContent>
|
|
628
|
+
</Select>
|
|
629
|
+
{editor.section.trim() && !deskOptions.some(option => option.value === editor.section) ? <small className="newsroom-desk-legacy-value">Old section value: {editor.section.trim()}</small> : null}
|
|
558
630
|
</label>
|
|
559
631
|
<label className="space-y-1 text-sm font-medium">
|
|
560
632
|
<span>Slug</span>
|
|
@@ -571,8 +643,27 @@ export function NewsroomArticleEditor({ articleId, demo }: { articleId: string;
|
|
|
571
643
|
</label>
|
|
572
644
|
</div>
|
|
573
645
|
|
|
646
|
+
{isOlderVersion ? <p className="newsroom-article-older-version" data-newsroom-article-older-version role="status">
|
|
647
|
+
This is version {editor.versionNumber} of {latestVersionNumber}, shown read-only. <button className="newsroom-article-link-button" onClick={() => setActiveArticleId(latestVersion?.id ?? activeArticleId)} type="button">Open the latest version</button>
|
|
648
|
+
</p> : null}
|
|
649
|
+
<div aria-label="Story view" className="newsroom-article-view-toggle" role="group">
|
|
650
|
+
<button aria-pressed={mobileTab === "preview"} className="organizer-header-button" data-newsroom-article-view="preview" onClick={() => setMobileTab("preview")} type="button">Preview</button>
|
|
651
|
+
<button aria-pressed={mobileTab === "edit"} className="organizer-header-button" data-newsroom-article-view="edit" onClick={() => setMobileTab("edit")} type="button">Edit</button>
|
|
652
|
+
<button aria-pressed={mobileTab === "trail"} className="organizer-header-button" data-newsroom-article-view="trail" onClick={() => setMobileTab("trail")} type="button">Messages</button>
|
|
653
|
+
<button aria-pressed={mobileTab === "references"} className="organizer-header-button" data-newsroom-article-view="references" onClick={() => { setFocusedReferenceId(null); setMobileTab("references"); }} type="button">References</button>
|
|
654
|
+
</div>
|
|
655
|
+
{(mobileTab === "trail" || mobileTab === "references") && editor.id ? <StoryReportingTrailView focusedReferenceId={focusedReferenceId} onShowReference={(referenceId) => { setFocusedReferenceId(referenceId); setMobileTab("references"); }} view={mobileTab === "references" ? "references" : "messages"} citedKeys={citedKeys} demo={previewMode} itemId={editor.id} lineageId={lineageId} onOpenVersion={(versionId) => { setActiveArticleId(versionId); setMobileTab("preview"); }} /> : null}
|
|
656
|
+
<div className={cn("space-y-4", mobileTab === "edit" ? "block" : "hidden")} data-newsroom-article-raw-code>
|
|
657
|
+
<div className="newsroom-article-front-matter-fields" data-newsroom-article-fields>
|
|
658
|
+
<label><span>Title</span><Input data-newsroom-article-field="title" onChange={(event) => changeFrontMatterField(FRONT_MATTER_TITLE_KEY, event.target.value)} value={readFrontMatterText(editor.frontMatterYaml, FRONT_MATTER_TITLE_KEY)} /></label>
|
|
659
|
+
<label><span>Subtitle</span><Input data-newsroom-article-field="subtitle" onChange={(event) => changeFrontMatterField(FRONT_MATTER_SUBTITLE_KEY, event.target.value)} placeholder="A line under the headline" value={readFrontMatterText(editor.frontMatterYaml, FRONT_MATTER_SUBTITLE_KEY)} /></label>
|
|
660
|
+
<label><span>Blurb</span><Textarea data-newsroom-article-field="blurb" onChange={(event) => changeFrontMatterField(FRONT_MATTER_BLURB_KEY, event.target.value)} placeholder="Two or three lines that describe the story on the board and in listings" rows={3} value={readFrontMatterText(editor.frontMatterYaml, FRONT_MATTER_BLURB_KEY)} /></label>
|
|
661
|
+
</div>
|
|
662
|
+
<Input aria-label="What changed in this version" className="max-w-xl" onChange={(event) => setChangeReason(event.target.value)} placeholder="What changed? Recorded when you save as a new version." value={changeReason} />
|
|
663
|
+
<details className="newsroom-article-front-matter" data-newsroom-article-front-matter-details>
|
|
664
|
+
<summary>Front matter (YAML)</summary>
|
|
574
665
|
<label className="block space-y-1 text-sm font-medium">
|
|
575
|
-
<span>Front matter (YAML)</span>
|
|
666
|
+
<span className="sr-only">Front matter (YAML)</span>
|
|
576
667
|
<Textarea
|
|
577
668
|
className={TEXTAREA_CLASS}
|
|
578
669
|
data-newsroom-article-front-matter
|
|
@@ -583,10 +674,11 @@ export function NewsroomArticleEditor({ articleId, demo }: { articleId: string;
|
|
|
583
674
|
value={editor.frontMatterYaml}
|
|
584
675
|
/>
|
|
585
676
|
</label>
|
|
677
|
+
</details>
|
|
586
678
|
|
|
587
679
|
<div className="space-y-1 text-sm font-medium">
|
|
588
680
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
589
|
-
<label htmlFor="newsroom-article-body">
|
|
681
|
+
<label className="sr-only" htmlFor="newsroom-article-body">Story body</label>
|
|
590
682
|
<Button
|
|
591
683
|
data-newsroom-article-insert-image
|
|
592
684
|
disabled={editor.id === null || imageUploading || busy !== null}
|
|
@@ -664,16 +756,20 @@ export function NewsroomArticleEditor({ articleId, demo }: { articleId: string;
|
|
|
664
756
|
})}
|
|
665
757
|
</ul>
|
|
666
758
|
) : null}
|
|
759
|
+
</div>
|
|
667
760
|
</div>
|
|
668
761
|
|
|
669
762
|
<div
|
|
670
|
-
className={cn("min-w-0", mobileTab === "preview" ? "block" : "hidden"
|
|
763
|
+
className={cn("min-w-0", mobileTab === "preview" ? "block" : "hidden")}
|
|
671
764
|
data-newsroom-article-preview-column
|
|
672
765
|
>
|
|
673
766
|
<div className="rounded-xl border border-border bg-card p-4" data-newsroom-article-preview>
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
767
|
+
{(readFrontMatterText(editor.frontMatterYaml, FRONT_MATTER_SUBTITLE_KEY) || readFrontMatterText(editor.frontMatterYaml, FRONT_MATTER_BLURB_KEY)) ? (
|
|
768
|
+
<div className="newsroom-article-summary" data-newsroom-article-summary>
|
|
769
|
+
{readFrontMatterText(editor.frontMatterYaml, FRONT_MATTER_SUBTITLE_KEY) ? <p className="newsroom-article-subtitle">{readFrontMatterText(editor.frontMatterYaml, FRONT_MATTER_SUBTITLE_KEY)}</p> : null}
|
|
770
|
+
{readFrontMatterText(editor.frontMatterYaml, FRONT_MATTER_BLURB_KEY) ? <p className="newsroom-article-blurb">{readFrontMatterText(editor.frontMatterYaml, FRONT_MATTER_BLURB_KEY)}</p> : null}
|
|
771
|
+
</div>
|
|
772
|
+
) : null}
|
|
677
773
|
{invalid && bodyIr ? (
|
|
678
774
|
<p className="mb-2 text-xs text-muted-foreground">Showing the last valid version.</p>
|
|
679
775
|
) : null}
|
|
@@ -725,6 +821,8 @@ export function NewsroomArticleEditor({ articleId, demo }: { articleId: string;
|
|
|
725
821
|
);
|
|
726
822
|
})();
|
|
727
823
|
|
|
824
|
+
if (embedded) return <div className="newsroom-embedded-article">{body}</div>;
|
|
825
|
+
|
|
728
826
|
return (
|
|
729
827
|
<NewsroomOpsShell
|
|
730
828
|
activeTab="articles"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import type { AssignmentRecord } from "../lib/category-repository";
|
|
6
|
+
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "./ui/select";
|
|
7
|
+
|
|
8
|
+
export type AssignmentDeskOption = { value: string; label: string; kind: "core" | "rotating" };
|
|
9
|
+
const NO_DESK_VALUE = "__no-desk__";
|
|
10
|
+
|
|
11
|
+
const assignmentDateFormat = new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" });
|
|
12
|
+
|
|
13
|
+
function formattedTimestamp(value: string | null | undefined): string | null {
|
|
14
|
+
if (!value) return null;
|
|
15
|
+
const parsed = new Date(value);
|
|
16
|
+
return Number.isNaN(parsed.getTime()) ? value : assignmentDateFormat.format(parsed);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type DetailField = { label: string; value: string | null | undefined };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The whole assignment, laid out by container width: one column in a narrow Board panel, labelled
|
|
23
|
+
* columns when the container is wide, such as the dedicated page on a large monitor.
|
|
24
|
+
*/
|
|
25
|
+
export function NewsroomAssignmentDetail({ assignment, deskOptions = [], onDeskChange, pageHref, showTitle = true }: {
|
|
26
|
+
assignment: AssignmentRecord;
|
|
27
|
+
deskOptions?: AssignmentDeskOption[];
|
|
28
|
+
onDeskChange?: (deskId: string | null) => Promise<void>;
|
|
29
|
+
pageHref?: string;
|
|
30
|
+
showTitle?: boolean;
|
|
31
|
+
}) {
|
|
32
|
+
const [deskError, setDeskError] = useState<string | null>(null);
|
|
33
|
+
const [savingDesk, setSavingDesk] = useState(false);
|
|
34
|
+
const currentDesk = assignment.sectionId || assignment.sectionKey || "";
|
|
35
|
+
const deskChoices = deskOptions;
|
|
36
|
+
const changeDesk = (value: string) => {
|
|
37
|
+
if (!onDeskChange) return;
|
|
38
|
+
setSavingDesk(true); setDeskError(null);
|
|
39
|
+
onDeskChange(value === NO_DESK_VALUE ? null : value).catch((reason: unknown) => setDeskError(reason instanceof Error ? reason.message : "Could not save the desk.")).finally(() => setSavingDesk(false));
|
|
40
|
+
};
|
|
41
|
+
const facts: DetailField[] = [
|
|
42
|
+
{ label: "Status", value: assignment.status },
|
|
43
|
+
{ label: "Type", value: assignment.assignmentTypeKey },
|
|
44
|
+
{ label: "Assignee", value: assignment.assigneeId ?? assignment.assigneeKey },
|
|
45
|
+
{ label: "Section", value: assignment.sectionKey },
|
|
46
|
+
{ label: "Created", value: formattedTimestamp(assignment.createdAt) },
|
|
47
|
+
{ label: "Updated", value: formattedTimestamp(assignment.updatedAt) },
|
|
48
|
+
{ label: "Completed", value: formattedTimestamp(assignment.completedAt) },
|
|
49
|
+
].filter(field => field.value);
|
|
50
|
+
const prose: DetailField[] = [
|
|
51
|
+
{ label: "Summary", value: assignment.summary },
|
|
52
|
+
{ label: "Brief", value: assignment.brief },
|
|
53
|
+
{ label: "Instructions", value: assignment.instructions },
|
|
54
|
+
].filter(field => field.value);
|
|
55
|
+
return <article className="newsroom-assignment-detail">
|
|
56
|
+
{showTitle && <h2 className="newsroom-assignment-detail-title">{assignment.title}</h2>}
|
|
57
|
+
<dl className="newsroom-assignment-detail-facts">
|
|
58
|
+
{facts.map(field => <div key={field.label}><dt>{field.label}</dt><dd>{field.value}</dd></div>)}
|
|
59
|
+
</dl>
|
|
60
|
+
{onDeskChange && <label className="newsroom-assignment-detail-desk">
|
|
61
|
+
<span>Desk</span>
|
|
62
|
+
<Select onValueChange={(value) => { if (typeof value === "string") changeDesk(value); }} value={deskChoices.some(option => option.value === currentDesk) ? currentDesk : NO_DESK_VALUE} disabled={savingDesk}>
|
|
63
|
+
<SelectTrigger aria-label="Desk" className="w-full" data-newsroom-assignment-desk>
|
|
64
|
+
<SelectValue>{deskChoices.find(option => option.value === currentDesk)?.label ?? "No desk"}</SelectValue>
|
|
65
|
+
</SelectTrigger>
|
|
66
|
+
<SelectContent>
|
|
67
|
+
<SelectItem value={NO_DESK_VALUE}>No desk</SelectItem>
|
|
68
|
+
{(["core", "rotating"] as const).map(kind => deskChoices.some(option => option.kind === kind) ? <SelectGroup key={kind}><SelectLabel>{kind === "core" ? "Core desks" : "Rotating desks"}</SelectLabel>{deskChoices.filter(option => option.kind === kind).map(option => <SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>)}</SelectGroup> : null)}
|
|
69
|
+
</SelectContent>
|
|
70
|
+
</Select>
|
|
71
|
+
{currentDesk && !deskChoices.some(option => option.value === currentDesk) ? <small className="newsroom-desk-legacy-value">Old section value: {currentDesk}</small> : null}
|
|
72
|
+
{deskError && <p role="alert" className="newsroom-assignment-detail-error">{deskError}</p>}
|
|
73
|
+
</label>}
|
|
74
|
+
<div className="newsroom-assignment-detail-prose">
|
|
75
|
+
{prose.map(field => <section key={field.label}><h3>{field.label}</h3><p>{field.value}</p></section>)}
|
|
76
|
+
</div>
|
|
77
|
+
{pageHref && <Link className="organizer-header-button" href={pageHref}>Open full page</Link>}
|
|
78
|
+
</article>;
|
|
79
|
+
}
|
|
@@ -117,7 +117,7 @@ function AssignmentDetailPanel({
|
|
|
117
117
|
detailExtras?: ReactNode;
|
|
118
118
|
}) {
|
|
119
119
|
const terminal = assignment.status === "completed" || assignment.status === "canceled";
|
|
120
|
-
const isReporting = assignment.assignmentTypeKey === "reporting.
|
|
120
|
+
const isReporting = assignment.assignmentTypeKey === "reporting.candidate";
|
|
121
121
|
|
|
122
122
|
return (
|
|
123
123
|
<div
|