@anthusai/papyrus 1.4.0-next.2 → 1.4.0
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/app/newsroom/[section]/page.tsx +1 -3
- package/app/newsroom/articles/page.tsx +1 -8
- package/components/news-desk-taxonomy-client.ts +4 -13
- package/components/newsroom-board-view.tsx +16 -24
- package/components/newsroom-organizer-shell.tsx +1 -1
- package/components/newsroom-organizer.css +4 -3
- package/lib/content-item-lifecycle.ts +5 -32
- package/lib/local-preview-seed.ts +4 -2
- package/lib/newsroom-board.ts +7 -23
- package/lib/newsroom-organizer-nav.ts +2 -3
- package/package.json +1 -1
- package/routes.manifest.json +1 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { resolveActiveSiteBrand } from "../../../lib/site-brand-tenant";
|
|
2
2
|
import { notFound, redirect } from "next/navigation";
|
|
3
3
|
import { toPublicNewsroomPath } from "../../../lib/newsroom-base-path";
|
|
4
|
-
import { contentItemBoardPathSegment } from "../../../lib/newsroom-board";
|
|
5
4
|
import { NewsroomBoardView } from "../../../components/newsroom-board-view";
|
|
6
5
|
import { NewsDeskPage } from "../../../components/news-desk-page";
|
|
7
6
|
import type { NewsDeskTab } from "../../../components/topic-steering-workspace";
|
|
@@ -44,11 +43,10 @@ const NEWS_DESK_ROUTE_SECTIONS = new Set<NewsDeskTab>([
|
|
|
44
43
|
|
|
45
44
|
export default async function NewsDeskSectionPage({ params, searchParams }: NewsDeskSectionPageProps) {
|
|
46
45
|
const { section } = await params;
|
|
47
|
-
if (section === "work") redirect(toPublicNewsroomPath(
|
|
46
|
+
if (section === "work") redirect(toPublicNewsroomPath("/newsroom/articles"));
|
|
48
47
|
if (section === "users") redirect(toPublicNewsroomPath("/newsroom/administration/users"));
|
|
49
48
|
if (section === "doctrine") redirect(toPublicNewsroomPath("/newsroom/administration/policies"));
|
|
50
49
|
if (section === "desks" && resolveActiveSiteBrand().newsroomLayout !== "organizer") redirect(toPublicNewsroomPath("/newsroom/topics"));
|
|
51
|
-
if (section === contentItemBoardPathSegment() && resolveActiveSiteBrand().newsroomLayout === "organizer") return <NewsroomBoardView subject="contentItem" />;
|
|
52
50
|
if (!NEWS_DESK_ROUTE_SECTIONS.has(section as NewsDeskTab)) notFound();
|
|
53
51
|
if (section === "assignments" && resolveActiveSiteBrand().newsroomLayout === "organizer") {
|
|
54
52
|
const resolved = await searchParams;
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { redirect } from "next/navigation";
|
|
2
|
-
import { contentItemBoardPathSegment } from "../../../lib/newsroom-board";
|
|
3
|
-
import { toPublicNewsroomPath } from "../../../lib/newsroom-base-path";
|
|
4
1
|
import { NewsroomBoardView } from "../../../components/newsroom-board-view";
|
|
5
2
|
import { resolveActiveSiteBrand } from "../../../lib/site-brand-tenant";
|
|
6
3
|
import { NewsroomArticlesView } from "../../../components/newsroom-articles-view";
|
|
@@ -14,10 +11,6 @@ type NewsroomArticlesPageProps = {
|
|
|
14
11
|
export default async function NewsroomArticlesPage({ searchParams }: NewsroomArticlesPageProps) {
|
|
15
12
|
const resolved = await searchParams;
|
|
16
13
|
const demoValue = Array.isArray(resolved?.demo) ? resolved?.demo[0] : resolved?.demo;
|
|
17
|
-
if (resolveActiveSiteBrand().newsroomLayout === "organizer" && demoValue !== "1")
|
|
18
|
-
const boardPathSegment = contentItemBoardPathSegment();
|
|
19
|
-
if (boardPathSegment !== "articles") redirect(toPublicNewsroomPath(`/newsroom/${boardPathSegment}`));
|
|
20
|
-
return <NewsroomBoardView subject="contentItem" />;
|
|
21
|
-
}
|
|
14
|
+
if (resolveActiveSiteBrand().newsroomLayout === "organizer" && demoValue !== "1") return <NewsroomBoardView subject="contentItem" />;
|
|
22
15
|
return <NewsroomArticlesView demo={demoValue === "1"} />;
|
|
23
16
|
}
|
|
@@ -41,7 +41,7 @@ import type {
|
|
|
41
41
|
UserDirectoryEntry,
|
|
42
42
|
} from "../lib/category-repository";
|
|
43
43
|
import { relationTypeKey } from "../lib/semantic-graph";
|
|
44
|
-
import {
|
|
44
|
+
import { contentItemStageKeys, type ContentItemVersionRecord } from "../lib/content-item-lifecycle";
|
|
45
45
|
import { getContentItemStageDefinitions } from "../lib/site-brand";
|
|
46
46
|
import { DOCTRINE_DEFINITIONS, getCategoryDoctrineDefinitions, type DoctrineCategory } from "../lib/doctrine";
|
|
47
47
|
import { configureAmplifyClient } from "./amplify-client-provider";
|
|
@@ -2172,23 +2172,14 @@ const CONTENT_ITEMS_BY_STAGE_QUERY = `
|
|
|
2172
2172
|
}
|
|
2173
2173
|
`;
|
|
2174
2174
|
|
|
2175
|
-
export async function loadNewsroomContentItemVersions(
|
|
2176
|
-
|
|
2177
|
-
const stagesToLoad = [
|
|
2178
|
-
...stageDefinitions
|
|
2179
|
-
.filter(definition => !definition.collapsedUntilOpened || options.includeCollapsedUntilOpenedStages?.includes(definition.key))
|
|
2180
|
-
.map(definition => definition.key),
|
|
2181
|
-
...legacyStageKeysToLoad(stageDefinitions),
|
|
2182
|
-
];
|
|
2183
|
-
if (isLocalPreviewEnabled()) {
|
|
2184
|
-
return createLocalPreviewContentItemVersions().filter(version => stagesToLoad.includes(version.stage ?? ""));
|
|
2185
|
-
}
|
|
2175
|
+
export async function loadNewsroomContentItemVersions(): Promise<ContentItemVersionRecord[]> {
|
|
2176
|
+
if (isLocalPreviewEnabled()) return createLocalPreviewContentItemVersions();
|
|
2186
2177
|
configureAmplifyClient();
|
|
2187
2178
|
const client = generateClient<Schema>() as unknown as {
|
|
2188
2179
|
graphql: (options: Record<string, unknown>) => Promise<GraphQLConnectionResponse<ContentItemVersionRecord>>;
|
|
2189
2180
|
};
|
|
2190
2181
|
const field = "listItemsByStageAndUpdatedAt";
|
|
2191
|
-
const versionsPerStage = await Promise.all(
|
|
2182
|
+
const versionsPerStage = await Promise.all(contentItemStageKeys(getContentItemStageDefinitions()).map(async (stage) => {
|
|
2192
2183
|
const versions: ContentItemVersionRecord[] = [];
|
|
2193
2184
|
let nextToken: string | null = null;
|
|
2194
2185
|
do {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
|
-
import { IssueCollection, IssueDetailPanel
|
|
5
|
+
import { IssueCollection, IssueDetailPanel } from "@kanbus/ui";
|
|
6
6
|
import "@kanbus/ui/styles/kanban.css";
|
|
7
7
|
import "./newsroom-newsprint-board.css";
|
|
8
8
|
import { PlusIcon } from "lucide-react";
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
type NativeBoardIssue,
|
|
21
21
|
} from "../lib/newsroom-board";
|
|
22
22
|
import type { AssignmentRecord } from "../lib/category-repository";
|
|
23
|
-
import { getContentItemName,
|
|
23
|
+
import { getContentItemName, SITE_BRAND } from "../lib/site-brand";
|
|
24
24
|
import { getNewsroomNavHref } from "../lib/newsroom-nav";
|
|
25
25
|
import { loadNewsroomAssignmentPage, loadNewsroomContentItemVersions, loadNewsroomPublishedContentItemVersions } from "./news-desk-taxonomy-client";
|
|
26
26
|
import { useOptionalNewsDeskClient } from "./news-desk-client-provider";
|
|
@@ -57,8 +57,6 @@ export function NewsroomBoardView({ subject }: { subject: NewsroomBoardSubject }
|
|
|
57
57
|
const [loading, setLoading] = useState(false);
|
|
58
58
|
const [error, setError] = useState<string | null>(null);
|
|
59
59
|
const generation = useRef(0);
|
|
60
|
-
const openedCollapsedUntilOpenedStagesRef = useRef<string[]>([]);
|
|
61
|
-
const refreshIssuesRef = useRef<((initial: boolean) => void) | null>(null);
|
|
62
60
|
useEffect(() => {
|
|
63
61
|
setLowMotion(readLocalReaderSettings().motion === "low");
|
|
64
62
|
return subscribeReaderSettingsChanges(settings => setLowMotion(settings.motion === "low"));
|
|
@@ -69,7 +67,7 @@ export function NewsroomBoardView({ subject }: { subject: NewsroomBoardSubject }
|
|
|
69
67
|
if (!allowed) { setLoading(false); return; }
|
|
70
68
|
const loadIssues = () => subject === "contentItem"
|
|
71
69
|
? Promise.all([
|
|
72
|
-
Promise.all([loadNewsroomContentItemVersions(
|
|
70
|
+
Promise.all([loadNewsroomContentItemVersions(), loadNewsroomPublishedContentItemVersions()]).then(([workingVersions, publishedVersions]) => [...workingVersions, ...publishedVersions]),
|
|
73
71
|
loadEveryNewsroomAssignment(),
|
|
74
72
|
]).then(([contentItemVersions, assignments]) => projectContentItemBoard({ contentItemVersions, assignments }))
|
|
75
73
|
: loadEveryNewsroomAssignment().then(projectAssignmentBoard);
|
|
@@ -83,7 +81,6 @@ export function NewsroomBoardView({ subject }: { subject: NewsroomBoardSubject }
|
|
|
83
81
|
if (generation.current === request && initial) setError(reason instanceof Error ? reason.message : "Could not load newsroom records.");
|
|
84
82
|
}).finally(() => { if (generation.current === request && initial) setLoading(false); });
|
|
85
83
|
};
|
|
86
|
-
refreshIssuesRef.current = refresh;
|
|
87
84
|
refresh(true);
|
|
88
85
|
const refreshWhenVisible = () => { if (document.visibilityState === "visible") refresh(false); };
|
|
89
86
|
const interval = window.setInterval(refreshWhenVisible, BOARD_REFRESH_INTERVAL_MILLISECONDS);
|
|
@@ -92,34 +89,29 @@ export function NewsroomBoardView({ subject }: { subject: NewsroomBoardSubject }
|
|
|
92
89
|
}, [allowed, subject]);
|
|
93
90
|
const config = useMemo(() => subject === "contentItem" ? contentItemBoardConfig(issues) : assignmentBoardConfig(issues), [issues, subject]);
|
|
94
91
|
const boardType = subject === "contentItem" ? contentItemBoardType() : ASSIGNMENT_BOARD_TYPE;
|
|
95
|
-
const columnState = useBoardColumnState({ config, storageKey: `papyrus.cms.columns.v1.${SITE_BRAND.id}.${subject}` });
|
|
96
|
-
const openedCollapsedUntilOpenedStageKeys = useMemo(() => subject !== "contentItem" ? "" : getContentItemStageDefinitions()
|
|
97
|
-
.filter(definition => definition.collapsedUntilOpened && !columnState.collapsedColumns.has(`${boardType}/${definition.key}`))
|
|
98
|
-
.map(definition => definition.key).join(","), [boardType, columnState.collapsedColumns, subject]);
|
|
99
|
-
useEffect(() => {
|
|
100
|
-
openedCollapsedUntilOpenedStagesRef.current = openedCollapsedUntilOpenedStageKeys ? openedCollapsedUntilOpenedStageKeys.split(",") : [];
|
|
101
|
-
if (openedCollapsedUntilOpenedStageKeys) refreshIssuesRef.current?.(false);
|
|
102
|
-
}, [openedCollapsedUntilOpenedStageKeys]);
|
|
103
92
|
const pageTitle = subject === "contentItem" ? getContentItemName().plural : "Assignments";
|
|
104
93
|
const issue = issues.find(item => item.id === selected);
|
|
105
94
|
if (!allowed) return <NewsDeskAccessGate shell={session?.shell ?? null} />;
|
|
106
95
|
return <NewsroomOpsShell activeTab={subject === "contentItem" ? "articles" : "assignments"} appTitle={SITE_BRAND.appTitle} backLabel={SITE_BRAND.backToHomeLabel} pageTitle={pageTitle}
|
|
107
|
-
headerActions={
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
</Link>
|
|
114
|
-
)}
|
|
115
|
-
</>}>
|
|
96
|
+
headerActions={subject === "contentItem" ? (
|
|
97
|
+
<Link className={cn(buttonVariants({ size: "sm" }))} data-newsroom-article-new href={getNewsroomNavHref(newsroomHref("articles", "new"))}>
|
|
98
|
+
<PlusIcon aria-hidden="true" />
|
|
99
|
+
{`New ${getContentItemName().singular.toLowerCase()}`}
|
|
100
|
+
</Link>
|
|
101
|
+
) : undefined}>
|
|
116
102
|
<div className="organizer-native-board papyrus-newsprint-board" data-color-mode={theme}>
|
|
103
|
+
<div className="organizer-board-toolbar">
|
|
104
|
+
<div role="group" aria-label="Record presentation"><button type="button" aria-pressed={view === "board"} onClick={() => setView("board")}>Board</button><button type="button" aria-pressed={view === "list"} onClick={() => setView("list")}>List</button></div>
|
|
105
|
+
</div>
|
|
106
|
+
<p className="organizer-board-note">{subject === "contentItem"
|
|
107
|
+
? `Each ${getContentItemName().singular.toLowerCase()} appears once, in the column for its stage. Open one to use its existing editor.`
|
|
108
|
+
: "Each assignment appears once, in the column for its status. Open one to use its existing workflow controls."}</p>
|
|
117
109
|
{error && <NewsroomOpsStatusBanner tone="error">{error}</NewsroomOpsStatusBanner>}
|
|
118
110
|
<p role="status" className="organizer-board-note">{loading ? "Loading records…" : `${issues.length} records loaded.`}</p>
|
|
119
111
|
<div className="organizer-board-body" data-detail-open={Boolean(issue)}>
|
|
120
112
|
<IssueCollection issues={issues} config={config} query={{ type: boardType }} view={view}
|
|
121
113
|
motion={lowMotion ? STATIC_MOTION : undefined}
|
|
122
|
-
|
|
114
|
+
columnStateKey={`papyrus.cms.columns.v1.${SITE_BRAND.id}.${subject}`} initialScroll="start"
|
|
123
115
|
selectedIssueId={selected} detailOpen={Boolean(issue)} onSelectIssue={item => setSelected(item.id)} ariaLabel={pageTitle}
|
|
124
116
|
emptyMessage={loading ? "Loading records…" : error ? "Records could not be loaded." : "No records in this selection."} />
|
|
125
117
|
{issue && <IssueDetailPanel issue={issue} onClose={() => setSelected(null)} fields={[{ label: "CMS record ID", value: issue.nativeId }, { label: "Status", value: issue.nativeStatus }]}>
|
|
@@ -21,7 +21,7 @@ const ICONS = { overview: LayoutDashboard, articles: ClipboardList, references:
|
|
|
21
21
|
function Navigation({ activePath, demo, admin, surface, onNavigate }: { activePath: string; demo: boolean; admin: boolean; surface: string; onNavigate?: () => void }) {
|
|
22
22
|
const id = useId();
|
|
23
23
|
const all = ORGANIZER_NAV_GROUPS.flatMap(group => group.items);
|
|
24
|
-
const active = all.filter(item =>
|
|
24
|
+
const active = all.filter(item => activePath === item.path || (item.path !== "/newsroom" && activePath.startsWith(item.path + "/"))).sort((a, b) => b.path.length - a.path.length)[0]?.key;
|
|
25
25
|
return <nav className="organizer-destinations" aria-label={surface === "sidebar" ? "Newsroom" : "All workspaces"}>
|
|
26
26
|
{ORGANIZER_NAV_GROUPS.map((group, index) => <section key={group.title} aria-labelledby={`${id}-${index}`}>
|
|
27
27
|
<h3 id={`${id}-${index}`}>{group.title}</h3>
|
|
@@ -37,9 +37,10 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
|
|
|
37
37
|
.organizer-bottom-nav{display:none}
|
|
38
38
|
.organizer-main:has(.organizer-native-board){display:flex;overflow:hidden}
|
|
39
39
|
.organizer-native-board{display:flex;flex-direction:column;flex:1;min-height:0;min-width:0;gap:12px}
|
|
40
|
-
.organizer-board-
|
|
41
|
-
.organizer-board-
|
|
42
|
-
.organizer-board-
|
|
40
|
+
.organizer-board-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;min-width:0;flex-wrap:wrap}
|
|
41
|
+
.organizer-board-toolbar [role=group]{display:flex;gap:8px;flex-wrap:wrap}
|
|
42
|
+
.organizer-board-toolbar button{min-height:44px;padding:8px 12px;border:1px solid var(--org-line)}
|
|
43
|
+
.organizer-board-toolbar button[aria-pressed=true]{background:var(--org-ink);color:var(--org-paper)}
|
|
43
44
|
.organizer-board-note{font-size:11px;line-height:1.5;margin:0;color:var(--org-muted)}
|
|
44
45
|
.organizer-board-body{position:relative;display:grid;grid-template-columns:minmax(0,1fr);flex:1;min-width:0;min-height:0;gap:16px}
|
|
45
46
|
.organizer-board-body[data-detail-open=true]{grid-template-columns:minmax(0,1fr) minmax(280px,360px)}
|
|
@@ -7,51 +7,24 @@
|
|
|
7
7
|
export type ContentItemStageDefinition = {
|
|
8
8
|
key: string;
|
|
9
9
|
label: string;
|
|
10
|
-
/** Closed or rarely visited stage: its board column starts collapsed and its cards load only when it is opened. */
|
|
11
|
-
collapsedUntilOpened?: boolean;
|
|
12
10
|
};
|
|
13
11
|
|
|
14
|
-
export const PUBLISHED_STAGE_KEY = "published";
|
|
15
|
-
|
|
16
|
-
/** Stage keys written before the story stages were simplified, read as the stage that replaced them. */
|
|
17
|
-
export const LEGACY_CONTENT_ITEM_STAGE_ALIASES: Readonly<Record<string, string>> = {
|
|
18
|
-
research: "reporting",
|
|
19
|
-
report: "reporting",
|
|
20
|
-
editor_select: "reporting",
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
/** Stages for work that has not become a story yet; they appear on the assignments board only. */
|
|
24
|
-
export const ASSIGNMENT_PHASE_STAGE_KEYS: readonly string[] = ["idea", "assignment"];
|
|
25
|
-
|
|
26
12
|
export const DEFAULT_CONTENT_ITEM_STAGE_DEFINITIONS: readonly ContentItemStageDefinition[] = [
|
|
27
|
-
{ key: "
|
|
13
|
+
{ key: "idea", label: "Idea" },
|
|
14
|
+
{ key: "assignment", label: "Assignment" },
|
|
15
|
+
{ key: "research", label: "Research" },
|
|
16
|
+
{ key: "report", label: "Report" },
|
|
17
|
+
{ key: "editor_select", label: "Editor select" },
|
|
28
18
|
{ key: "copywriting", label: "Copywriting" },
|
|
29
19
|
{ key: "illustration", label: "Illustration" },
|
|
30
20
|
{ key: "typesetting", label: "Typesetting" },
|
|
31
21
|
{ key: "published", label: "Published" },
|
|
32
|
-
{ key: "held", label: "Held" },
|
|
33
|
-
{ key: "spiked", label: "Spiked", collapsedUntilOpened: true },
|
|
34
22
|
];
|
|
35
23
|
|
|
36
24
|
export function contentItemStageKeys(stageDefinitions: readonly ContentItemStageDefinition[]): string[] {
|
|
37
25
|
return stageDefinitions.map(definition => definition.key);
|
|
38
26
|
}
|
|
39
27
|
|
|
40
|
-
export function canonicalContentItemStage(
|
|
41
|
-
stage: string,
|
|
42
|
-
stageDefinitions: readonly ContentItemStageDefinition[] = DEFAULT_CONTENT_ITEM_STAGE_DEFINITIONS,
|
|
43
|
-
): string {
|
|
44
|
-
const isDefined = stageDefinitions.some(definition => definition.key === stage);
|
|
45
|
-
return isDefined ? stage : LEGACY_CONTENT_ITEM_STAGE_ALIASES[stage] ?? stage;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function legacyStageKeysToLoad(stageDefinitions: readonly ContentItemStageDefinition[]): string[] {
|
|
49
|
-
const definedKeys = contentItemStageKeys(stageDefinitions);
|
|
50
|
-
return Object.entries(LEGACY_CONTENT_ITEM_STAGE_ALIASES)
|
|
51
|
-
.filter(([legacyKey, replacementKey]) => !definedKeys.includes(legacyKey) && definedKeys.includes(replacementKey))
|
|
52
|
-
.map(([legacyKey]) => legacyKey);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
28
|
export function contentItemStageLabel(
|
|
56
29
|
stage: string,
|
|
57
30
|
stageDefinitions: readonly ContentItemStageDefinition[] = DEFAULT_CONTENT_ITEM_STAGE_DEFINITIONS,
|
|
@@ -6,13 +6,15 @@ const SEED_TIMESTAMP_BASE = Date.parse("2026-10-01T12:00:00.000Z");
|
|
|
6
6
|
const ONE_HOUR_IN_MILLISECONDS = 60 * 60 * 1000;
|
|
7
7
|
|
|
8
8
|
const SEED_CONTENT_ITEM_HEADLINES_BY_POSITION: readonly (readonly string[])[] = [
|
|
9
|
+
["Readers ask what the newsroom should chase next", "A tip from the field: harbor dredging schedule"],
|
|
10
|
+
["City council budget vote draws a crowd", "Interview request: regional transit director"],
|
|
11
|
+
["Mapping every permit filed since January", "What the county audit left out"],
|
|
9
12
|
["Three sources confirm the zoning reversal", "Reporting packet: school board redistricting"],
|
|
13
|
+
["Editor shortlist: the week's lead candidates", "Editor shortlist: the weekend long read"],
|
|
10
14
|
["Draft in progress: the last ferry crossing", "Draft in progress: neighborhood bakery profile"],
|
|
11
15
|
["Illustration brief: flooded streets, aerial view", "Illustration brief: the market at dawn"],
|
|
12
16
|
["Layout pass: front page lead and rail", "Layout pass: the Saturday supplement"],
|
|
13
17
|
["Published: harbor safety review", "Published: the lighthouse keepers' archive"],
|
|
14
|
-
["Held for the winter issue: the lighthouse keepers' archive", "Evergreen: how the harbor dredging works"],
|
|
15
|
-
["Spiked: the unconfirmed council rumor", "Spiked: the recycled press release"],
|
|
16
18
|
];
|
|
17
19
|
|
|
18
20
|
function isoTimestampHoursBeforeBase(hours: number): string {
|
package/lib/newsroom-board.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import type { KanbanConfig, KanbanIssue } from "@kanbus/ui";
|
|
2
2
|
import type { AssignmentRecord } from "./category-repository";
|
|
3
3
|
import {
|
|
4
|
-
ASSIGNMENT_PHASE_STAGE_KEYS,
|
|
5
|
-
PUBLISHED_STAGE_KEY,
|
|
6
|
-
canonicalContentItemStage,
|
|
7
4
|
contentItemStageKeys,
|
|
8
5
|
contentItemStageLabel,
|
|
9
6
|
latestVersionPerContentItem,
|
|
@@ -20,10 +17,6 @@ export interface NativeBoardIssue extends KanbanIssue {
|
|
|
20
17
|
href: string;
|
|
21
18
|
}
|
|
22
19
|
|
|
23
|
-
export function contentItemBoardPathSegment(): string {
|
|
24
|
-
return getContentItemName().plural.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "articles";
|
|
25
|
-
}
|
|
26
|
-
|
|
27
20
|
export function contentItemBoardType(): string {
|
|
28
21
|
return getContentItemName().singular.toLowerCase();
|
|
29
22
|
}
|
|
@@ -45,12 +38,8 @@ export function projectContentItemBoard(records: {
|
|
|
45
38
|
}
|
|
46
39
|
}
|
|
47
40
|
const contentItemType = contentItemBoardType();
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const contentItemVersionsOnBoard = latestVersionPerContentItem(records.contentItemVersions)
|
|
51
|
-
.map(version => ({ version, stage: canonicalContentItemStage(version.stage ?? "idea", stageDefinitions) }))
|
|
52
|
-
.filter(({ stage }) => stageKeys.includes(stage) || !ASSIGNMENT_PHASE_STAGE_KEYS.includes(stage));
|
|
53
|
-
return contentItemVersionsOnBoard.map(({ version, stage }) => {
|
|
41
|
+
return latestVersionPerContentItem(records.contentItemVersions).map(version => {
|
|
42
|
+
const stage = version.stage ?? "idea";
|
|
54
43
|
const workCount = assignmentCountByContentItem.get(version.lineageId) ?? 0;
|
|
55
44
|
return {
|
|
56
45
|
id: `contentItem:${version.lineageId}`, nativeId: version.lineageId, kind: "contentItem" as const, type: contentItemType,
|
|
@@ -77,10 +66,9 @@ export function contentItemColumnOrder(
|
|
|
77
66
|
stagesPresentOnBoard: readonly string[],
|
|
78
67
|
stageKeys: readonly string[] = contentItemStageKeys(getContentItemStageDefinitions()),
|
|
79
68
|
): string[] {
|
|
69
|
+
const lastStage = stageKeys[stageKeys.length - 1];
|
|
80
70
|
const unrecognizedStages = [...new Set(stagesPresentOnBoard)].filter(stage => !stageKeys.includes(stage));
|
|
81
|
-
|
|
82
|
-
const insertionIndex = stageKeys.indexOf(stageBeforeWhichUnrecognizedStagesSit);
|
|
83
|
-
return [...stageKeys.slice(0, insertionIndex), ...unrecognizedStages, ...stageKeys.slice(insertionIndex)];
|
|
71
|
+
return [...stageKeys.filter(stage => stage !== lastStage), ...unrecognizedStages, lastStage];
|
|
84
72
|
}
|
|
85
73
|
|
|
86
74
|
export function assignmentColumnOrder(statusesPresentOnBoard: readonly string[]): string[] {
|
|
@@ -89,7 +77,7 @@ export function assignmentColumnOrder(statusesPresentOnBoard: readonly string[])
|
|
|
89
77
|
return [...ASSIGNMENT_STATUSES_BEFORE_OTHERS, ...otherStatuses, ...ASSIGNMENT_STATUSES_AFTER_OTHERS];
|
|
90
78
|
}
|
|
91
79
|
|
|
92
|
-
function boardConfigForType(type: string, columns: readonly { key: string; name: string
|
|
80
|
+
function boardConfigForType(type: string, columns: readonly { key: string; name: string }[]): KanbanConfig {
|
|
93
81
|
const statuses = columns.map(column => ({ ...column, category: "native" }));
|
|
94
82
|
return {
|
|
95
83
|
hierarchy: [], types: [type], statuses,
|
|
@@ -98,15 +86,11 @@ function boardConfigForType(type: string, columns: readonly { key: string; name:
|
|
|
98
86
|
};
|
|
99
87
|
}
|
|
100
88
|
|
|
101
|
-
/** Content items read left to right through the lifecycle
|
|
89
|
+
/** Content items read left to right through the lifecycle with the final stage last. */
|
|
102
90
|
export function contentItemBoardConfig(issues: readonly NativeBoardIssue[]): KanbanConfig {
|
|
103
91
|
const contentItemType = contentItemBoardType();
|
|
104
92
|
return boardConfigForType(contentItemType, contentItemColumnOrder(issues.map(issue => issue.nativeStatus))
|
|
105
|
-
.map(stage => ({
|
|
106
|
-
key: `${contentItemType}/${stage}`,
|
|
107
|
-
name: contentItemStageLabel(stage, getContentItemStageDefinitions()),
|
|
108
|
-
collapsed: getContentItemStageDefinitions().some(definition => definition.key === stage && definition.collapsedUntilOpened),
|
|
109
|
-
})));
|
|
93
|
+
.map(stage => ({ key: `${contentItemType}/${stage}`, name: contentItemStageLabel(stage, getContentItemStageDefinitions()) })));
|
|
110
94
|
}
|
|
111
95
|
|
|
112
96
|
export function assignmentBoardConfig(issues: readonly NativeBoardIssue[]): KanbanConfig {
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import { contentItemBoardPathSegment } from "./newsroom-board";
|
|
2
1
|
import { getContentItemName } from "./site-brand";
|
|
3
2
|
|
|
4
3
|
const contentItemPluralLowercase = getContentItemName().plural.toLowerCase();
|
|
5
4
|
export type OrganizerDestination = {
|
|
6
5
|
key: string; label: string; description: string; path: string;
|
|
7
|
-
access?: "admin"; primary?: boolean; unavailable?: string;
|
|
6
|
+
access?: "admin"; primary?: boolean; unavailable?: string;
|
|
8
7
|
};
|
|
9
8
|
export const ORGANIZER_NAV_GROUPS: readonly { title: string; items: readonly OrganizerDestination[] }[] = [
|
|
10
9
|
{ title: "Editorial work", items: [
|
|
11
10
|
{ key: "overview", label: "Overview", description: "See the desk and choose where to begin.", path: "/newsroom", primary: true },
|
|
12
|
-
{ key: "articles", label: getContentItemName().plural, description: `Follow ${contentItemPluralLowercase} across their stages and open one to write or revise it.`, path:
|
|
11
|
+
{ key: "articles", label: getContentItemName().plural, description: `Follow ${contentItemPluralLowercase} across their stages and open one to write or revise it.`, path: "/newsroom/articles", primary: true },
|
|
13
12
|
{ key: "assignments", label: "Assignments", description: "Follow research and reporting work across its statuses and claim it.", path: "/newsroom/assignments" },
|
|
14
13
|
{ key: "messages", label: "Messages", description: "Read and reply to newsroom conversations.", path: "/newsroom/messages", primary: true },
|
|
15
14
|
{ key: "insights", label: "Insights", description: "Discuss findings and research threads.", path: "/newsroom/insights" },
|
package/package.json
CHANGED
package/routes.manifest.json
CHANGED