@anthusai/papyrus 1.3.0 → 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/model-attachment-upload/handler.ts +1 -0
- 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 +8 -1
- package/app/newsroom/articles/page.tsx +10 -0
- 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-client-provider.tsx +17 -2
- package/components/news-desk-page.tsx +0 -1
- package/components/news-desk-taxonomy-client.ts +154 -240
- package/components/newspaper.tsx +4 -4
- 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 +153 -43
- package/components/newsroom-client-shell.tsx +8 -1
- package/components/newsroom-newsprint-board.css +5 -46
- package/components/newsroom-ops-overview.tsx +27 -5
- package/components/newsroom-organizer-shell.tsx +36 -13
- package/components/newsroom-organizer.css +269 -13
- package/components/newsroom-references-view.tsx +118 -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-priming.tsx +85 -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 +280 -679
- 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/article-flow.ts +95 -0
- package/lib/article-frame-solver.ts +519 -0
- package/lib/blog-feature-solver.ts +1 -109
- package/lib/category-repository.ts +1 -1
- package/lib/chrome-text.ts +504 -0
- 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 +1133 -0
- package/lib/furniture.ts +385 -0
- package/lib/graphql-content-repository.ts +42 -7
- package/lib/item-stream.ts +47 -0
- package/lib/labels.ts +37 -0
- package/lib/layout-config.ts +237 -0
- package/lib/layout-plan.ts +40 -11
- package/lib/layout-scenarios.ts +21 -11
- package/lib/layout-types.ts +457 -0
- package/lib/layout-warnings.ts +8 -0
- package/lib/local-preview-section-fixtures.ts +72 -0
- package/lib/local-preview-seed.ts +286 -0
- package/lib/local-preview-tenant.ts +27 -0
- package/lib/local-preview.ts +3 -0
- package/lib/newspaper-layout.ts +17 -3410
- package/lib/newsroom-articles.ts +2 -0
- package/lib/newsroom-assignments.ts +14 -1
- package/lib/newsroom-board.ts +99 -47
- 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 +9 -16
- 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/pretext-layout.ts +22 -0
- 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/relevance-status.ts +73 -0
- package/lib/reporting-story-budget.ts +2 -2
- package/lib/semantic-graph.ts +5 -5
- package/lib/site-brand.ts +28 -1
- 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/text-columns.ts +251 -0
- package/lib/wiki.ts +56 -0
- package/package.json +46 -3
- package/renderers/pretext/presentation-shell.tsx +1 -1
- package/renderers/pretext/rhythm-article-page.tsx +1 -18
- package/routes.manifest.json +34 -15
- package/app/newsroom/work/page.tsx +0 -10
package/lib/newsroom-articles.ts
CHANGED
|
@@ -59,6 +59,7 @@ export type NewsroomArticleEditorState = {
|
|
|
59
59
|
|
|
60
60
|
export type EditableArticle = {
|
|
61
61
|
id: string;
|
|
62
|
+
lineageId?: string | null;
|
|
62
63
|
type: string;
|
|
63
64
|
section: string | null;
|
|
64
65
|
slug: string;
|
|
@@ -267,6 +268,7 @@ export async function loadEditableArticle(client: ArticlesDataClient, id: string
|
|
|
267
268
|
const status: ArticleStatus = record.status === "published" ? "published" : "draft";
|
|
268
269
|
return {
|
|
269
270
|
id: String(record.id),
|
|
271
|
+
lineageId: typeof record.lineageId === "string" ? record.lineageId : null,
|
|
270
272
|
type: String(record.type),
|
|
271
273
|
section: typeof record.section === "string" ? record.section : null,
|
|
272
274
|
slug: String(record.slug),
|
|
@@ -18,6 +18,19 @@ export type AssignmentMetrics = {
|
|
|
18
18
|
canceled: number;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
export const BOOKKEEPING_ASSIGNMENT_TYPE_KEYS: ReadonlySet<string> = new Set([
|
|
22
|
+
"analysis.reindex",
|
|
23
|
+
"curation.reference-intake",
|
|
24
|
+
"reference.corpus-accession",
|
|
25
|
+
"reference.identifier-backfill",
|
|
26
|
+
"reference.doi-backfill",
|
|
27
|
+
"reference.text-extraction",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export function isEditorialWorkAssignment(assignment: Pick<AssignmentRecord, "assignmentTypeKey">): boolean {
|
|
31
|
+
return !BOOKKEEPING_ASSIGNMENT_TYPE_KEYS.has(assignment.assignmentTypeKey?.trim() ?? "");
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
export function assignmentTypeKeyForFilter(assignment: AssignmentRecord): string {
|
|
22
35
|
return assignment.assignmentTypeKey?.trim() || "unknown";
|
|
23
36
|
}
|
|
@@ -28,7 +41,7 @@ export function formatAssignmentTypeLabel(typeKey: string | null | undefined): s
|
|
|
28
41
|
if (key === "analysis.reindex") return "Analysis re-index";
|
|
29
42
|
if (key === "newsroom.research" || key === "research") return "Research";
|
|
30
43
|
if (key === "curation.reference-intake") return "Reference intake";
|
|
31
|
-
if (key === "reporting.
|
|
44
|
+
if (key === "reporting.candidate") return "Reporting candidate";
|
|
32
45
|
return key
|
|
33
46
|
.split(/[.:_-]+/)
|
|
34
47
|
.filter(Boolean)
|
package/lib/newsroom-board.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
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,
|
|
4
7
|
contentItemStageKeys,
|
|
5
8
|
contentItemStageLabel,
|
|
6
9
|
latestVersionPerContentItem,
|
|
7
10
|
type ContentItemVersionRecord,
|
|
8
11
|
} from "./content-item-lifecycle";
|
|
12
|
+
import { contentItemExcerpt } from "./front-matter-fields";
|
|
13
|
+
import { assignmentStatusLabel } from "./newsroom-assignments";
|
|
9
14
|
import { getContentItemName, getContentItemStageDefinitions } from "./site-brand";
|
|
10
15
|
|
|
11
16
|
export type NativeBoardKind = "contentItem" | "assignment";
|
|
@@ -14,23 +19,26 @@ export interface NativeBoardIssue extends KanbanIssue {
|
|
|
14
19
|
nativeId: string;
|
|
15
20
|
nativeStatus: string;
|
|
16
21
|
href: string;
|
|
22
|
+
/** A few lines to show under the headline: the blurb, else the subtitle. */
|
|
23
|
+
excerpt?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function contentItemBoardPathSegment(): string {
|
|
27
|
+
return getContentItemName().plural.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "articles";
|
|
17
28
|
}
|
|
18
29
|
|
|
19
30
|
export function contentItemBoardType(): string {
|
|
20
31
|
return getContentItemName().singular.toLowerCase();
|
|
21
32
|
}
|
|
22
33
|
|
|
23
|
-
|
|
24
|
-
return kind === "contentItem" ? contentItemBoardType() : kind;
|
|
25
|
-
}
|
|
34
|
+
export const ASSIGNMENT_BOARD_TYPE = "assignment";
|
|
26
35
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
contentItemVersions?: readonly ContentItemVersionRecord[];
|
|
36
|
+
const ASSIGNMENT_STATUSES_BEFORE_OTHERS = ["open", "claimed"];
|
|
37
|
+
const ASSIGNMENT_STATUSES_AFTER_OTHERS = ["completed", "canceled"];
|
|
38
|
+
|
|
39
|
+
/** One card per content item, in the column for its lifecycle stage; its assignments are counted on the card. */
|
|
40
|
+
export function projectContentItemBoard(records: {
|
|
41
|
+
contentItemVersions: readonly ContentItemVersionRecord[];
|
|
34
42
|
assignments?: readonly AssignmentRecord[];
|
|
35
43
|
}): NativeBoardIssue[] {
|
|
36
44
|
const assignmentCountByContentItem = new Map<string, number>();
|
|
@@ -40,54 +48,98 @@ export function projectNewsroomBoard(records: {
|
|
|
40
48
|
}
|
|
41
49
|
}
|
|
42
50
|
const contentItemType = contentItemBoardType();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
51
|
+
const stageDefinitions = getContentItemStageDefinitions();
|
|
52
|
+
const stageKeys = contentItemStageKeys(stageDefinitions);
|
|
53
|
+
const contentItemVersionsOnBoard = latestVersionPerContentItem(records.contentItemVersions)
|
|
54
|
+
.map(version => ({ version, stage: canonicalContentItemStage(version.stage ?? "idea", stageDefinitions) }))
|
|
55
|
+
.filter(({ stage }) => stageKeys.includes(stage) || !ASSIGNMENT_PHASE_STAGE_KEYS.includes(stage));
|
|
56
|
+
return contentItemVersionsOnBoard.map(({ version, stage }) => {
|
|
57
|
+
const workCount = assignmentCountByContentItem.get(version.lineageId) ?? 0;
|
|
58
|
+
return {
|
|
59
|
+
id: `contentItem:${version.lineageId}`, nativeId: version.lineageId, kind: "contentItem" as const, type: contentItemType,
|
|
60
|
+
title: version.headline || version.title || version.slug, nativeStatus: stage, status: `${contentItemType}/${stage}`,
|
|
61
|
+
priority: 2, description: workCount ? `${workCount} assignment${workCount === 1 ? "" : "s"} on this ${contentItemType}.` : undefined,
|
|
62
|
+
updated_at: version.updatedAt ?? undefined, href: `/newsroom/articles/${encodeURIComponent(version.id)}`,
|
|
63
|
+
excerpt: contentItemExcerpt(version),
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** One card per assignment, in the column for its status. */
|
|
69
|
+
export function projectAssignmentBoard(assignments: readonly AssignmentRecord[]): NativeBoardIssue[] {
|
|
70
|
+
return assignments.map(row => ({
|
|
71
|
+
id: `assignment:${row.id}`, nativeId: row.id, kind: "assignment" as const, type: ASSIGNMENT_BOARD_TYPE,
|
|
72
|
+
title: row.title, nativeStatus: row.status, status: `${ASSIGNMENT_BOARD_TYPE}/${row.status}`,
|
|
73
|
+
priority: row.priority ?? 2, assignee: row.assigneeId || undefined,
|
|
74
|
+
description: row.summary || row.brief || undefined,
|
|
75
|
+
created_at: row.createdAt, updated_at: row.updatedAt,
|
|
76
|
+
href: `/newsroom/assignments/${encodeURIComponent(row.id)}`,
|
|
77
|
+
}));
|
|
63
78
|
}
|
|
64
79
|
|
|
65
80
|
export function contentItemColumnOrder(
|
|
66
81
|
stagesPresentOnBoard: readonly string[],
|
|
67
82
|
stageKeys: readonly string[] = contentItemStageKeys(getContentItemStageDefinitions()),
|
|
68
83
|
): string[] {
|
|
69
|
-
const lastStage = stageKeys[stageKeys.length - 1];
|
|
70
84
|
const unrecognizedStages = [...new Set(stagesPresentOnBoard)].filter(stage => !stageKeys.includes(stage));
|
|
71
|
-
|
|
85
|
+
const stageBeforeWhichUnrecognizedStagesSit = stageKeys.includes(PUBLISHED_STAGE_KEY) ? PUBLISHED_STAGE_KEY : stageKeys[stageKeys.length - 1];
|
|
86
|
+
const insertionIndex = stageKeys.indexOf(stageBeforeWhichUnrecognizedStagesSit);
|
|
87
|
+
return [...stageKeys.slice(0, insertionIndex), ...unrecognizedStages, ...stageKeys.slice(insertionIndex)];
|
|
72
88
|
}
|
|
73
89
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const statuses = [...contentItemStatuses, ...otherStatuses];
|
|
90
|
+
export function assignmentColumnOrder(statusesPresentOnBoard: readonly string[]): string[] {
|
|
91
|
+
const knownStatuses = [...ASSIGNMENT_STATUSES_BEFORE_OTHERS, ...ASSIGNMENT_STATUSES_AFTER_OTHERS];
|
|
92
|
+
const otherStatuses = [...new Set(statusesPresentOnBoard)].filter(status => !knownStatuses.includes(status));
|
|
93
|
+
return [...ASSIGNMENT_STATUSES_BEFORE_OTHERS, ...otherStatuses, ...ASSIGNMENT_STATUSES_AFTER_OTHERS];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function boardConfigForType(type: string, columns: readonly { key: string; name: string; collapsed?: boolean }[]): KanbanConfig {
|
|
97
|
+
const statuses = columns.map(column => ({ ...column, category: "native" }));
|
|
83
98
|
return {
|
|
84
|
-
hierarchy: [], types:
|
|
85
|
-
statuses,
|
|
99
|
+
hierarchy: [], types: [type], statuses,
|
|
86
100
|
categories: [{ name: "native" }], priorities: {}, type_colors: {},
|
|
87
|
-
workflows: Object.fromEntries(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
101
|
+
workflows: { [type]: Object.fromEntries(statuses.map(status => [status.key, []])) },
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Content items read left to right through the lifecycle; stages marked collapsed-until-opened start collapsed. */
|
|
106
|
+
export function contentItemBoardConfig(issues: readonly NativeBoardIssue[]): KanbanConfig {
|
|
107
|
+
const contentItemType = contentItemBoardType();
|
|
108
|
+
return boardConfigForType(contentItemType, contentItemColumnOrder(issues.map(issue => issue.nativeStatus))
|
|
109
|
+
.map(stage => ({
|
|
110
|
+
key: `${contentItemType}/${stage}`,
|
|
111
|
+
name: contentItemStageLabel(stage, getContentItemStageDefinitions()),
|
|
112
|
+
collapsed: getContentItemStageDefinitions().some(definition => definition.key === stage && definition.collapsedUntilOpened),
|
|
113
|
+
})));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function assignmentBoardConfig(issues: readonly NativeBoardIssue[]): KanbanConfig {
|
|
117
|
+
return boardConfigForType(ASSIGNMENT_BOARD_TYPE, assignmentColumnOrder(issues.map(issue => issue.nativeStatus))
|
|
118
|
+
.map(status => ({ key: `${ASSIGNMENT_BOARD_TYPE}/${status}`, name: assignmentStatusLabel(status) })));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function matchesDesk(candidate: string | null | undefined, deskId: string): boolean {
|
|
122
|
+
return Boolean(candidate) && candidate!.trim().toLowerCase() === deskId.trim().toLowerCase();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function deskContainsContentItem(version: ContentItemVersionRecord, deskId: string): boolean {
|
|
126
|
+
return matchesDesk(version.section, deskId);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function deskContainsAssignment(assignment: AssignmentRecord, deskId: string): boolean {
|
|
130
|
+
return matchesDesk(assignment.sectionId, deskId) || matchesDesk(assignment.sectionKey, deskId);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A desk board shows stories and assignments side by side; each keeps its own columns and the type filter narrows to one. */
|
|
134
|
+
export function deskBoardConfig(issues: readonly NativeBoardIssue[]): KanbanConfig {
|
|
135
|
+
const storiesConfig = contentItemBoardConfig(issues.filter(issue => issue.kind === "contentItem"));
|
|
136
|
+
const assignmentsConfig = assignmentBoardConfig(issues.filter(issue => issue.kind === "assignment"));
|
|
137
|
+
return {
|
|
138
|
+
...storiesConfig,
|
|
139
|
+
types: [...(storiesConfig.types ?? []), ...(assignmentsConfig.types ?? [])],
|
|
140
|
+
statuses: [...storiesConfig.statuses, ...assignmentsConfig.statuses],
|
|
141
|
+
categories: storiesConfig.categories,
|
|
142
|
+
workflows: { ...storiesConfig.workflows, ...assignmentsConfig.workflows },
|
|
91
143
|
};
|
|
92
144
|
}
|
|
93
145
|
|
|
@@ -194,7 +194,7 @@ export function applyPilobolusDemoDashboardContent(
|
|
|
194
194
|
});
|
|
195
195
|
|
|
196
196
|
localized.assignments = localized.assignments.map((assignment, index) => {
|
|
197
|
-
if (assignment.assignmentTypeKey === "reporting.
|
|
197
|
+
if (assignment.assignmentTypeKey === "reporting.candidate") {
|
|
198
198
|
return {
|
|
199
199
|
...assignment,
|
|
200
200
|
title: "Field-notes reporting candidate 1: Strangler-fig control-loop shift",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { toPublicNewsroomPath } from "./newsroom-base-path";
|
|
2
2
|
|
|
3
3
|
export const FORUM_THREAD_ROUTE_PREFIX = "/newsroom/forum/";
|
|
4
|
-
export const INSIGHT_THREAD_ROUTE_PREFIX = "/newsroom/insights/";
|
|
5
4
|
export const FORUM_MESSAGE_HASH_PREFIX = "message-";
|
|
6
5
|
|
|
7
6
|
export function isForumThreadId(id: string | null | undefined): boolean {
|
|
@@ -9,11 +8,6 @@ export function isForumThreadId(id: string | null | undefined): boolean {
|
|
|
9
8
|
return value.startsWith("message-thread-");
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
export function isInsightThreadId(id: string | null | undefined): boolean {
|
|
13
|
-
const value = String(id ?? "").trim();
|
|
14
|
-
return Boolean(value) && !isForumThreadId(value);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
11
|
export function getForumMessageAnchorId(messageId: string): string {
|
|
18
12
|
return `${FORUM_MESSAGE_HASH_PREFIX}${encodeURIComponent(messageId.trim())}`;
|
|
19
13
|
}
|
|
@@ -33,7 +27,7 @@ export function parseForumMessageAnchorFromHash(hash: string | null | undefined)
|
|
|
33
27
|
export type ParsedNewsroomForumRoute = {
|
|
34
28
|
threadId: string | null;
|
|
35
29
|
messageId: string | null;
|
|
36
|
-
surface: "edition_forum" |
|
|
30
|
+
surface: "edition_forum" | null;
|
|
37
31
|
};
|
|
38
32
|
|
|
39
33
|
export function parseNewsroomForumRoute(pathname: string, hash?: string | null): ParsedNewsroomForumRoute {
|
|
@@ -43,13 +37,6 @@ export function parseNewsroomForumRoute(pathname: string, hash?: string | null):
|
|
|
43
37
|
return { threadId: null, messageId: parseForumMessageAnchorFromHash(hash), surface: null };
|
|
44
38
|
}
|
|
45
39
|
const tail = segments.slice(newsroomIndex + 1);
|
|
46
|
-
if (tail[0] === "insights" && tail[1]) {
|
|
47
|
-
return {
|
|
48
|
-
threadId: decodeURIComponent(tail[1]),
|
|
49
|
-
messageId: parseForumMessageAnchorFromHash(hash),
|
|
50
|
-
surface: "insight_forum",
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
40
|
if (tail[0] === "forum" && tail[1]) {
|
|
54
41
|
return {
|
|
55
42
|
threadId: decodeURIComponent(tail[1]),
|
|
@@ -86,24 +73,11 @@ export function buildForumThreadUrl(
|
|
|
86
73
|
return toPublicNewsroomPath(`/newsroom/forum/${encodedThreadId}${query ? `?${query}` : ""}${hash ? `#${hash}` : ""}`);
|
|
87
74
|
}
|
|
88
75
|
|
|
89
|
-
export function buildInsightThreadUrl(
|
|
90
|
-
threadId: string,
|
|
91
|
-
options?: { messageId?: string | null; demo?: boolean },
|
|
92
|
-
): string {
|
|
93
|
-
const encodedThreadId = encodeURIComponent(threadId.trim());
|
|
94
|
-
const params = new URLSearchParams();
|
|
95
|
-
if (options?.demo) params.set("demo", "1");
|
|
96
|
-
const query = params.toString();
|
|
97
|
-
const hash = options?.messageId ? getForumMessageAnchorId(options.messageId) : "";
|
|
98
|
-
return toPublicNewsroomPath(`${INSIGHT_THREAD_ROUTE_PREFIX}${encodedThreadId}${query ? `?${query}` : ""}${hash ? `#${hash}` : ""}`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
76
|
export function buildThreadUrlForSurface(
|
|
102
77
|
surface: ParsedNewsroomForumRoute["surface"],
|
|
103
78
|
threadId: string,
|
|
104
79
|
options?: { messageId?: string | null; demo?: boolean },
|
|
105
80
|
): string {
|
|
106
|
-
if (surface === "insight_forum") return buildInsightThreadUrl(threadId, options);
|
|
107
81
|
return buildForumThreadUrl(threadId, options);
|
|
108
82
|
}
|
|
109
83
|
|
|
@@ -122,7 +96,7 @@ export function pushForumThreadUrl(
|
|
|
122
96
|
const surface = options?.surface ?? readCurrentForumRoute().surface;
|
|
123
97
|
const url = threadId
|
|
124
98
|
? buildThreadUrlForSurface(surface, threadId, options)
|
|
125
|
-
:
|
|
99
|
+
: buildNewsroomForumIndexUrl(options?.demo);
|
|
126
100
|
const current = `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
127
101
|
if (current === url) return;
|
|
128
102
|
if (options?.replace) {
|
|
@@ -132,24 +106,9 @@ export function pushForumThreadUrl(
|
|
|
132
106
|
}
|
|
133
107
|
}
|
|
134
108
|
|
|
135
|
-
export function pushInsightThreadUrl(
|
|
136
|
-
threadId: string | null,
|
|
137
|
-
options?: { messageId?: string | null; demo?: boolean; replace?: boolean },
|
|
138
|
-
) {
|
|
139
|
-
pushForumThreadUrl(threadId, { ...options, surface: "insight_forum" });
|
|
140
|
-
}
|
|
141
|
-
|
|
142
109
|
export function buildNewsroomForumIndexUrl(demo?: boolean): string {
|
|
143
110
|
const params = new URLSearchParams();
|
|
144
111
|
if (demo) params.set("demo", "1");
|
|
145
112
|
const query = params.toString();
|
|
146
113
|
return toPublicNewsroomPath(query ? `/newsroom?${query}` : "/newsroom");
|
|
147
114
|
}
|
|
148
|
-
|
|
149
|
-
export function buildNewsroomInsightsIndexUrl(demo?: boolean, domain?: string): string {
|
|
150
|
-
const params = new URLSearchParams();
|
|
151
|
-
if (demo) params.set("demo", "1");
|
|
152
|
-
if (domain?.trim()) params.set("domain", domain.trim());
|
|
153
|
-
const query = params.toString();
|
|
154
|
-
return toPublicNewsroomPath(query ? `/newsroom/insights?${query}` : "/newsroom/insights");
|
|
155
|
-
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { browserPathToInternalNewsroomPath, toPublicNewsroomPath } from "./newsroom-base-path";
|
|
2
|
-
export type NewsroomIndexTab = "references" | "messages" | "assignments"
|
|
2
|
+
export type NewsroomIndexTab = "references" | "messages" | "assignments";
|
|
3
3
|
|
|
4
4
|
export type ReferenceIndexOrder = "published" | "imported";
|
|
5
5
|
|
|
6
6
|
export type ReferencesIndexFilters = { status: string; processing: string; order: ReferenceIndexOrder };
|
|
7
7
|
export type MessagesIndexFilters = { kind: string; domain: string };
|
|
8
8
|
export type AssignmentsIndexFilters = { status: string; type: string; view: string };
|
|
9
|
-
export type InsightsIndexFilters = { domain: string };
|
|
10
9
|
|
|
11
10
|
export const DEFAULT_REFERENCES_INDEX_FILTERS: ReferencesIndexFilters = {
|
|
12
11
|
status: "exclude-pending",
|
|
@@ -25,10 +24,6 @@ export const DEFAULT_ASSIGNMENTS_INDEX_FILTERS: AssignmentsIndexFilters = {
|
|
|
25
24
|
view: "queue",
|
|
26
25
|
};
|
|
27
26
|
|
|
28
|
-
export const DEFAULT_INSIGHTS_INDEX_FILTERS: InsightsIndexFilters = {
|
|
29
|
-
domain: "",
|
|
30
|
-
};
|
|
31
|
-
|
|
32
27
|
export function referencesStatusFromUrl(value: string): string {
|
|
33
28
|
return value === "exclude-pending" ? "__exclude_pending" : value;
|
|
34
29
|
}
|
|
@@ -72,12 +67,6 @@ export function effectiveAssignmentsIndexFilters(partial?: Partial<AssignmentsIn
|
|
|
72
67
|
};
|
|
73
68
|
}
|
|
74
69
|
|
|
75
|
-
export function effectiveInsightsIndexFilters(partial?: Partial<InsightsIndexFilters>): InsightsIndexFilters {
|
|
76
|
-
return {
|
|
77
|
-
domain: partial?.domain?.trim() ?? DEFAULT_INSIGHTS_INDEX_FILTERS.domain,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
70
|
export function readReferencesIndexFilters(searchParams: URLSearchParams): ReferencesIndexFilters {
|
|
82
71
|
const statusParam = searchParams.get("status")?.trim() ?? "";
|
|
83
72
|
const orderParam = searchParams.get("order")?.trim();
|
|
@@ -103,12 +92,6 @@ export function readAssignmentsIndexFilters(searchParams: URLSearchParams): Assi
|
|
|
103
92
|
});
|
|
104
93
|
}
|
|
105
94
|
|
|
106
|
-
export function readInsightsIndexFilters(searchParams: URLSearchParams): InsightsIndexFilters {
|
|
107
|
-
return effectiveInsightsIndexFilters({
|
|
108
|
-
domain: searchParams.get("domain")?.trim() ?? undefined,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
95
|
export function buildReferencesIndexQuery(filters: ReferencesIndexFilters): string {
|
|
113
96
|
const params = new URLSearchParams();
|
|
114
97
|
if (filters.status && filters.status !== DEFAULT_REFERENCES_INDEX_FILTERS.status) {
|
|
@@ -137,26 +120,18 @@ export function buildAssignmentsIndexQuery(filters: AssignmentsIndexFilters): st
|
|
|
137
120
|
return query ? `?${query}` : "";
|
|
138
121
|
}
|
|
139
122
|
|
|
140
|
-
export function buildInsightsIndexQuery(filters: InsightsIndexFilters): string {
|
|
141
|
-
const params = new URLSearchParams();
|
|
142
|
-
if (filters.domain.trim()) params.set("domain", filters.domain.trim());
|
|
143
|
-
const query = params.toString();
|
|
144
|
-
return query ? `?${query}` : "";
|
|
145
|
-
}
|
|
146
|
-
|
|
147
123
|
export function buildNewsroomIndexWebPath(
|
|
148
124
|
tab: NewsroomIndexTab,
|
|
149
|
-
filters: ReferencesIndexFilters | MessagesIndexFilters | AssignmentsIndexFilters
|
|
125
|
+
filters: ReferencesIndexFilters | MessagesIndexFilters | AssignmentsIndexFilters,
|
|
150
126
|
): string {
|
|
151
127
|
if (tab === "references") return toPublicNewsroomPath(`/newsroom/references${buildReferencesIndexQuery(filters as ReferencesIndexFilters)}`);
|
|
152
128
|
if (tab === "messages") return toPublicNewsroomPath(`/newsroom/messages${buildMessagesIndexQuery(filters as MessagesIndexFilters)}`);
|
|
153
|
-
if (tab === "insights") return toPublicNewsroomPath(`/newsroom/insights${buildInsightsIndexQuery(filters as InsightsIndexFilters)}`);
|
|
154
129
|
return toPublicNewsroomPath(`/newsroom/assignments${buildAssignmentsIndexQuery(filters as AssignmentsIndexFilters)}`);
|
|
155
130
|
}
|
|
156
131
|
|
|
157
132
|
export function syncBrowserNewsroomIndexUrl(
|
|
158
133
|
tab: NewsroomIndexTab,
|
|
159
|
-
filters: ReferencesIndexFilters | MessagesIndexFilters | AssignmentsIndexFilters
|
|
134
|
+
filters: ReferencesIndexFilters | MessagesIndexFilters | AssignmentsIndexFilters,
|
|
160
135
|
options?: { replace?: boolean },
|
|
161
136
|
) {
|
|
162
137
|
if (typeof window === "undefined") return;
|
|
@@ -192,15 +167,3 @@ export function parseAssignmentIdFromNewsroomPathname(pathname: string | null |
|
|
|
192
167
|
return segment;
|
|
193
168
|
}
|
|
194
169
|
}
|
|
195
|
-
|
|
196
|
-
export function parseInsightThreadIdFromNewsroomPathname(pathname: string | null | undefined): string | null {
|
|
197
|
-
const internalPathname = pathname ? browserPathToInternalNewsroomPath(pathname) : null;
|
|
198
|
-
if (!internalPathname?.startsWith("/newsroom/insights/")) return null;
|
|
199
|
-
const segment = internalPathname.slice("/newsroom/insights/".length).split("/")[0]?.trim() ?? "";
|
|
200
|
-
if (!segment) return null;
|
|
201
|
-
try {
|
|
202
|
-
return decodeURIComponent(segment);
|
|
203
|
-
} catch {
|
|
204
|
-
return segment;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { generateClient } from "aws-amplify/data";
|
|
2
2
|
import { getUrl, uploadData } from "aws-amplify/storage";
|
|
3
3
|
import type { Schema } from "../amplify/data/resource";
|
|
4
|
+
import { isLocalPreviewEnabled } from "./local-preview";
|
|
5
|
+
import { createLocalPreviewMediaAssets } from "./local-preview-seed";
|
|
4
6
|
import type { MediaAssetFields, MediaAssetRecord, NewsroomMediaBackend } from "./newsroom-media";
|
|
5
7
|
|
|
6
8
|
type MediaListPage = {
|
|
@@ -113,6 +115,8 @@ export function createDemoMediaBackend(): NewsroomMediaBackend {
|
|
|
113
115
|
rows.set(fields.itemId, [...(rows.get(fields.itemId) ?? []), fields]);
|
|
114
116
|
},
|
|
115
117
|
async listMediaAssets(itemId) {
|
|
118
|
+
const previewRows = createLocalPreviewMediaAssets(itemId);
|
|
119
|
+
if (previewRows.length) return previewRows;
|
|
116
120
|
return (rows.get(itemId) ?? []).map((fields) => ({
|
|
117
121
|
id: fields.id,
|
|
118
122
|
sortKey: fields.sortKey,
|
|
@@ -122,6 +126,7 @@ export function createDemoMediaBackend(): NewsroomMediaBackend {
|
|
|
122
126
|
}));
|
|
123
127
|
},
|
|
124
128
|
async imageUrl(storagePath) {
|
|
129
|
+
if (isLocalPreviewEnabled() && storagePath.startsWith("media/")) return `/api/local-preview/media?key=${encodeURIComponent(storagePath)}`;
|
|
125
130
|
const object = objects.get(storagePath);
|
|
126
131
|
if (!object) throw new Error(`No demo object at ${storagePath}.`);
|
|
127
132
|
return URL.createObjectURL(object);
|
package/lib/newsroom-nav.ts
CHANGED
|
@@ -5,7 +5,6 @@ export type NewsroomNavTabId =
|
|
|
5
5
|
| "overview"
|
|
6
6
|
| "articles"
|
|
7
7
|
| "messages"
|
|
8
|
-
| "insights"
|
|
9
8
|
| "assignments"
|
|
10
9
|
| "references"
|
|
11
10
|
| "topics"
|
|
@@ -28,9 +27,8 @@ export const NEWSROOM_OPS_NAV: NewsroomNavItem[] = [
|
|
|
28
27
|
{ id: "assignments", label: "Assignments", detail: "Work queue", href: newsroomHref("assignments"), mobilePrimary: true },
|
|
29
28
|
{ id: "references", label: "References", detail: "Knowledge base", href: newsroomHref("references"), mobilePrimary: true },
|
|
30
29
|
{ id: "messages", label: "Messages", detail: "Forum", href: newsroomHref("messages") },
|
|
31
|
-
{ id: "insights", label: "Insights", detail: "Research threads", href: newsroomHref("insights") },
|
|
32
30
|
{ id: "topics", label: "Topics", detail: "Taxonomy", href: newsroomHref("topics"), mobilePrimary: true },
|
|
33
|
-
{ id: "concepts", label: "
|
|
31
|
+
{ id: "concepts", label: "Concept wiki", detail: "Knowledge graph", href: newsroomHref("concepts") },
|
|
34
32
|
{ id: "search", label: "Search", detail: "Knowledge base", href: newsroomHref("search") },
|
|
35
33
|
{ id: "administration", label: "Administration", detail: "Users & policies", href: newsroomHref("administration") },
|
|
36
34
|
];
|
|
@@ -1,36 +1,29 @@
|
|
|
1
|
+
import { contentItemBoardPathSegment } from "./newsroom-board";
|
|
1
2
|
import { getContentItemName } from "./site-brand";
|
|
2
3
|
|
|
3
4
|
const contentItemPluralLowercase = getContentItemName().plural.toLowerCase();
|
|
4
5
|
export type OrganizerDestination = {
|
|
5
6
|
key: string; label: string; description: string; path: string;
|
|
6
|
-
access?: "admin"; primary?: boolean; unavailable?: string;
|
|
7
|
+
access?: "admin"; primary?: boolean; unavailable?: string; alsoActiveOnPaths?: readonly string[];
|
|
7
8
|
};
|
|
8
9
|
export const ORGANIZER_NAV_GROUPS: readonly { title: string; items: readonly OrganizerDestination[] }[] = [
|
|
9
10
|
{ title: "Editorial work", items: [
|
|
10
|
-
{ key: "
|
|
11
|
-
{ key: "
|
|
12
|
-
{ key: "articles", label: getContentItemName().plural, description: "Write, revise and prepare copy for publication.", path: "/newsroom/articles" },
|
|
13
|
-
{ key: "assignments", label: "Assignments", description: "Claim work and follow research and reporting progress.", path: "/newsroom/assignments" },
|
|
11
|
+
{ key: "articles", label: getContentItemName().plural, description: `Follow ${contentItemPluralLowercase} across their stages and open one to write or revise it.`, path: `/newsroom/${contentItemBoardPathSegment()}`, alsoActiveOnPaths: ["/newsroom/articles"], primary: true },
|
|
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
|
-
{ key: "insights", label: "Insights", description: "Discuss findings and research threads.", path: "/newsroom/insights" },
|
|
16
14
|
] },
|
|
17
15
|
{ title: "Research & knowledge", items: [
|
|
18
16
|
{ key: "references", label: "References", description: "Inspect sources and make editorial inclusion decisions.", path: "/newsroom/references", primary: true },
|
|
19
|
-
{ key: "
|
|
20
|
-
{ key: "
|
|
21
|
-
{ key: "
|
|
22
|
-
{ key: "search", label: "Knowledge search", description: "Find evidence across the knowledge base.", path: "/newsroom/search" },
|
|
17
|
+
{ key: "concepts", label: "Concepts", description: "A wiki of the entities in the knowledge graph.", path: "/newsroom/concepts" },
|
|
18
|
+
{ key: "sections", label: "Sections", description: "Organize the publication’s sections and its ontology.", path: "/newsroom/administration/sections" },
|
|
19
|
+
{ key: "desks", label: "Taxonomy", description: "Maintain topic sets and category versions, and review topic proposals.", path: "/newsroom/desks", alsoActiveOnPaths: ["/newsroom/topics"] },
|
|
23
20
|
] },
|
|
24
21
|
{ title: "Administration", items: [
|
|
25
|
-
{ key: "
|
|
26
|
-
{ key: "sections", label: "Sections", description: "Organize the publication’s sections.", path: "/newsroom/administration/sections" },
|
|
27
|
-
{ key: "procedures", label: "Procedures", description: "Manage procedure definitions and versions.", path: "/newsroom/administration/procedures", access: "admin" },
|
|
28
|
-
{ key: "users", label: "Users & identity merging", description: "Review people, roles and identity merges.", path: "/newsroom/administration/users", access: "admin" },
|
|
29
|
-
{ key: "administration", label: "Administration", description: "Open the existing CMS settings workspace.", path: "/newsroom/administration" },
|
|
22
|
+
{ key: "administration", label: "Administration", description: "Policies, procedures, users and settings.", path: "/newsroom/administration", alsoActiveOnPaths: ["/newsroom/administration/policies", "/newsroom/administration/procedures", "/newsroom/administration/users"] },
|
|
30
23
|
] },
|
|
31
24
|
];
|
|
32
25
|
const all = ORGANIZER_NAV_GROUPS.flatMap(group => group.items);
|
|
33
|
-
export const ORGANIZER_PRIMARY_NAV = ["
|
|
26
|
+
export const ORGANIZER_PRIMARY_NAV = ["articles", "references", "messages"].map(key => all.find(item => item.key === key)!);
|
|
34
27
|
|
|
35
28
|
export const ORGANIZER_SETTINGS_DESTINATION = {
|
|
36
29
|
label: "Settings", path: "/newsroom/settings", description: "Theme, motion and interface preferences.",
|
|
@@ -13,7 +13,7 @@ export function resolveNewsDeskTab(
|
|
|
13
13
|
: raw === "desks" && !organizer ? "topics" : raw;
|
|
14
14
|
switch (value) {
|
|
15
15
|
case "desks": case "administration": case "topics": case "concepts":
|
|
16
|
-
case "references": case "
|
|
16
|
+
case "references": case "messages": case "assignments": case "search":
|
|
17
17
|
return value;
|
|
18
18
|
default: return "overview";
|
|
19
19
|
}
|
package/lib/newsroom-sections.ts
CHANGED
|
@@ -50,3 +50,15 @@ export function displayNewsroomSectionShortTitle(section: NewsroomSectionRecord)
|
|
|
50
50
|
export function buildNewsroomSectionHref(sectionId: string, demo?: boolean): string {
|
|
51
51
|
return getNewsroomNavHref(`/newsroom/sections/${encodeURIComponent(sectionId)}`, demo);
|
|
52
52
|
}
|
|
53
|
+
|
|
54
|
+
export type NewsroomDeskKind = "core" | "rotating";
|
|
55
|
+
export type NewsroomDeskOption = { value: string; label: string; kind: NewsroomDeskKind };
|
|
56
|
+
|
|
57
|
+
export function newsroomDeskKind(section: NewsroomSectionRecord): NewsroomDeskKind {
|
|
58
|
+
return normalizeNewsroomSectionType(section.type) === "canonical" ? "core" : "rotating";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The desks a story or assignment can be placed on: the same enabled, ordered desks the Overview page lists. */
|
|
62
|
+
export function newsroomDeskOptions(sections: NewsroomSectionRecord[]): NewsroomDeskOption[] {
|
|
63
|
+
return sortNewsroomSections(sections).filter(isEnabledNewsroomSection).map(section => ({ value: section.id, label: section.title, kind: newsroomDeskKind(section) }));
|
|
64
|
+
}
|