@anthusai/papyrus 1.3.0 → 1.4.0-next.2
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/functions/model-attachment-upload/handler.ts +1 -0
- package/app/newsroom/[section]/page.tsx +8 -0
- package/app/newsroom/articles/page.tsx +10 -0
- package/components/news-desk-client-provider.tsx +17 -2
- package/components/news-desk-taxonomy-client.ts +86 -3
- package/components/newspaper.tsx +4 -4
- package/components/newsroom-board-view.tsx +70 -29
- package/components/newsroom-client-shell.tsx +8 -1
- package/components/newsroom-organizer-shell.tsx +13 -8
- package/components/newsroom-organizer.css +5 -5
- package/components/newsroom-references-view.tsx +18 -0
- package/components/reference-cyclotron-priming.tsx +85 -0
- package/components/topic-steering-workspace.tsx +35 -1
- package/lib/article-flow.ts +95 -0
- package/lib/article-frame-solver.ts +519 -0
- package/lib/chrome-text.ts +504 -0
- package/lib/content-item-lifecycle.ts +32 -5
- package/lib/front-mosaic.ts +1132 -0
- package/lib/furniture.ts +385 -0
- package/lib/labels.ts +37 -0
- package/lib/layout-config.ts +237 -0
- package/lib/layout-plan.ts +37 -9
- package/lib/layout-scenarios.ts +21 -11
- package/lib/layout-types.ts +457 -0
- package/lib/local-preview-seed.ts +95 -0
- package/lib/local-preview.ts +3 -0
- package/lib/newspaper-layout.ts +17 -3410
- package/lib/newsroom-board.ts +70 -47
- package/lib/newsroom-organizer-nav.ts +5 -5
- package/lib/pretext-layout.ts +22 -0
- package/lib/relevance-status.ts +73 -0
- package/lib/site-brand.ts +8 -1
- package/lib/text-columns.ts +251 -0
- package/package.json +13 -2
- package/renderers/pretext/presentation-shell.tsx +1 -1
- package/routes.manifest.json +1 -9
- package/app/newsroom/work/page.tsx +0 -10
|
@@ -1,6 +1,8 @@
|
|
|
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
|
+
import { NewsroomBoardView } from "../../../components/newsroom-board-view";
|
|
4
6
|
import { NewsDeskPage } from "../../../components/news-desk-page";
|
|
5
7
|
import type { NewsDeskTab } from "../../../components/topic-steering-workspace";
|
|
6
8
|
|
|
@@ -42,9 +44,15 @@ const NEWS_DESK_ROUTE_SECTIONS = new Set<NewsDeskTab>([
|
|
|
42
44
|
|
|
43
45
|
export default async function NewsDeskSectionPage({ params, searchParams }: NewsDeskSectionPageProps) {
|
|
44
46
|
const { section } = await params;
|
|
47
|
+
if (section === "work") redirect(toPublicNewsroomPath(`/newsroom/${contentItemBoardPathSegment()}`));
|
|
45
48
|
if (section === "users") redirect(toPublicNewsroomPath("/newsroom/administration/users"));
|
|
46
49
|
if (section === "doctrine") redirect(toPublicNewsroomPath("/newsroom/administration/policies"));
|
|
47
50
|
if (section === "desks" && resolveActiveSiteBrand().newsroomLayout !== "organizer") redirect(toPublicNewsroomPath("/newsroom/topics"));
|
|
51
|
+
if (section === contentItemBoardPathSegment() && resolveActiveSiteBrand().newsroomLayout === "organizer") return <NewsroomBoardView subject="contentItem" />;
|
|
48
52
|
if (!NEWS_DESK_ROUTE_SECTIONS.has(section as NewsDeskTab)) notFound();
|
|
53
|
+
if (section === "assignments" && resolveActiveSiteBrand().newsroomLayout === "organizer") {
|
|
54
|
+
const resolved = await searchParams;
|
|
55
|
+
if (!resolved?.assignment && resolved?.demo !== "1") return <NewsroomBoardView subject="assignment" />;
|
|
56
|
+
}
|
|
49
57
|
return <NewsDeskPage section={section} searchParams={searchParams} />;
|
|
50
58
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { redirect } from "next/navigation";
|
|
2
|
+
import { contentItemBoardPathSegment } from "../../../lib/newsroom-board";
|
|
3
|
+
import { toPublicNewsroomPath } from "../../../lib/newsroom-base-path";
|
|
4
|
+
import { NewsroomBoardView } from "../../../components/newsroom-board-view";
|
|
5
|
+
import { resolveActiveSiteBrand } from "../../../lib/site-brand-tenant";
|
|
1
6
|
import { NewsroomArticlesView } from "../../../components/newsroom-articles-view";
|
|
2
7
|
|
|
3
8
|
export const dynamic = "force-dynamic";
|
|
@@ -9,5 +14,10 @@ type NewsroomArticlesPageProps = {
|
|
|
9
14
|
export default async function NewsroomArticlesPage({ searchParams }: NewsroomArticlesPageProps) {
|
|
10
15
|
const resolved = await searchParams;
|
|
11
16
|
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
|
+
}
|
|
12
22
|
return <NewsroomArticlesView demo={demoValue === "1"} />;
|
|
13
23
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Hub } from "aws-amplify/utils";
|
|
4
4
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
5
5
|
import type { DevSandboxEditorAuth } from "../lib/dev-sandbox-editor-auth";
|
|
6
|
+
import type { CategorySteeringDashboard } from "../lib/category-repository";
|
|
6
7
|
import { ensureDevSandboxEditorSession } from "./dev-sandbox-editor-sign-in";
|
|
7
8
|
import {
|
|
8
9
|
loadEditorAssignmentsData,
|
|
@@ -46,7 +47,12 @@ const NewsDeskClientContext = createContext<NewsDeskClientContextValue | null>(n
|
|
|
46
47
|
export function NewsDeskClientProvider({
|
|
47
48
|
children,
|
|
48
49
|
devSandboxEditorAuth = null,
|
|
49
|
-
|
|
50
|
+
localPreviewDashboard = null,
|
|
51
|
+
}: Readonly<{
|
|
52
|
+
children: React.ReactNode;
|
|
53
|
+
devSandboxEditorAuth?: DevSandboxEditorAuth | null;
|
|
54
|
+
localPreviewDashboard?: CategorySteeringDashboard | null;
|
|
55
|
+
}>) {
|
|
50
56
|
const [shell, setShell] = useState<NewsDeskShellState>(() => createInitialNewsDeskShellState());
|
|
51
57
|
const [searchTransition, setSearchTransition] = useState<NewsDeskSearchTransition | null>(null);
|
|
52
58
|
const shellRef = useRef(shell);
|
|
@@ -58,6 +64,15 @@ export function NewsDeskClientProvider({
|
|
|
58
64
|
|
|
59
65
|
const refreshDashboard = useCallback(async () => {
|
|
60
66
|
const sequence = ++bootstrapSequenceRef.current;
|
|
67
|
+
if (localPreviewDashboard) {
|
|
68
|
+
setShell((current) => resolveDashboardReady(
|
|
69
|
+
current,
|
|
70
|
+
localPreviewDashboard,
|
|
71
|
+
{ status: "signedIn", label: "Local preview editor", email: null },
|
|
72
|
+
new Date().toISOString(),
|
|
73
|
+
));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
61
76
|
setShell((current) => beginAccessCheck(current));
|
|
62
77
|
|
|
63
78
|
try {
|
|
@@ -101,7 +116,7 @@ export function NewsDeskClientProvider({
|
|
|
101
116
|
error instanceof Error ? error.message : "Could not load Newsroom data.",
|
|
102
117
|
));
|
|
103
118
|
}
|
|
104
|
-
}, [devSandboxEditorAuth]);
|
|
119
|
+
}, [devSandboxEditorAuth, localPreviewDashboard]);
|
|
105
120
|
|
|
106
121
|
const refreshAssignments = useCallback(async () => {
|
|
107
122
|
try {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { isLocalPreviewEnabled } from "../lib/local-preview";
|
|
4
|
+
import { createLocalPreviewAssignments, createLocalPreviewContentItemVersions, createLocalPreviewReferences } from "../lib/local-preview-seed";
|
|
3
5
|
import { generateClient } from "aws-amplify/data";
|
|
4
6
|
import { testEditorOverrideAllowed } from "../lib/test-editor-override";
|
|
5
7
|
import { downloadData, getUrl } from "aws-amplify/storage";
|
|
@@ -39,7 +41,7 @@ import type {
|
|
|
39
41
|
UserDirectoryEntry,
|
|
40
42
|
} from "../lib/category-repository";
|
|
41
43
|
import { relationTypeKey } from "../lib/semantic-graph";
|
|
42
|
-
import {
|
|
44
|
+
import { legacyStageKeysToLoad, type ContentItemVersionRecord } from "../lib/content-item-lifecycle";
|
|
43
45
|
import { getContentItemStageDefinitions } from "../lib/site-brand";
|
|
44
46
|
import { DOCTRINE_DEFINITIONS, getCategoryDoctrineDefinitions, type DoctrineCategory } from "../lib/doctrine";
|
|
45
47
|
import { configureAmplifyClient } from "./amplify-client-provider";
|
|
@@ -1863,6 +1865,7 @@ export function normalizeNewsroomReferencePageOrder(value?: string | null): News
|
|
|
1863
1865
|
}
|
|
1864
1866
|
|
|
1865
1867
|
export async function loadNewsroomReferencePage(options: NewsroomReferencePageOptions = {}): Promise<NewsroomRecordPage<ReferenceRecord>> {
|
|
1868
|
+
if (isLocalPreviewEnabled()) return { items: createLocalPreviewReferences(), nextToken: null, hasMore: false };
|
|
1866
1869
|
configureAmplifyClient();
|
|
1867
1870
|
const order = normalizeNewsroomReferencePageOrder(options.order);
|
|
1868
1871
|
if (order === "imported") {
|
|
@@ -2146,6 +2149,7 @@ export async function loadEditorOverviewEditionData(): Promise<{
|
|
|
2146
2149
|
}
|
|
2147
2150
|
|
|
2148
2151
|
export async function loadNewsroomAssignmentPage(options: NewsroomAssignmentPageOptions = {}): Promise<NewsroomRecordPage<AssignmentRecord>> {
|
|
2152
|
+
if (isLocalPreviewEnabled()) return { items: createLocalPreviewAssignments(), nextToken: null, hasMore: false };
|
|
2149
2153
|
return loadNewsroomFeedPage<AssignmentRecord>({
|
|
2150
2154
|
query: NEWSROOM_ASSIGNMENT_FEED_QUERY,
|
|
2151
2155
|
field: "listAssignmentsByNewsroomFeedAndCreatedAt",
|
|
@@ -2168,13 +2172,23 @@ const CONTENT_ITEMS_BY_STAGE_QUERY = `
|
|
|
2168
2172
|
}
|
|
2169
2173
|
`;
|
|
2170
2174
|
|
|
2171
|
-
export async function loadNewsroomContentItemVersions(): Promise<ContentItemVersionRecord[]> {
|
|
2175
|
+
export async function loadNewsroomContentItemVersions(options: { includeCollapsedUntilOpenedStages?: readonly string[] } = {}): Promise<ContentItemVersionRecord[]> {
|
|
2176
|
+
const stageDefinitions = getContentItemStageDefinitions();
|
|
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
|
+
}
|
|
2172
2186
|
configureAmplifyClient();
|
|
2173
2187
|
const client = generateClient<Schema>() as unknown as {
|
|
2174
2188
|
graphql: (options: Record<string, unknown>) => Promise<GraphQLConnectionResponse<ContentItemVersionRecord>>;
|
|
2175
2189
|
};
|
|
2176
2190
|
const field = "listItemsByStageAndUpdatedAt";
|
|
2177
|
-
const versionsPerStage = await Promise.all(
|
|
2191
|
+
const versionsPerStage = await Promise.all(stagesToLoad.map(async (stage) => {
|
|
2178
2192
|
const versions: ContentItemVersionRecord[] = [];
|
|
2179
2193
|
let nextToken: string | null = null;
|
|
2180
2194
|
do {
|
|
@@ -3826,3 +3840,72 @@ export async function requestCyclotronReviewRate(
|
|
|
3826
3840
|
content: JSON.stringify(request),
|
|
3827
3841
|
});
|
|
3828
3842
|
}
|
|
3843
|
+
|
|
3844
|
+
const LIST_PRIMING_REQUEST_PAYLOADS_QUERY = `
|
|
3845
|
+
query ListKnowledgeRawPayloadsByOwnerAndKind($ownerId: ID!, $payloadKind: ModelStringKeyConditionInput, $limit: Int, $nextToken: String) {
|
|
3846
|
+
listKnowledgeRawPayloadsByOwnerAndKind(ownerId: $ownerId, payloadKind: $payloadKind, limit: $limit, nextToken: $nextToken) {
|
|
3847
|
+
items { id }
|
|
3848
|
+
nextToken
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
`;
|
|
3852
|
+
|
|
3853
|
+
/** Ask the next decide-relevance sweep to prime the running cyclotron with one pasted URL. */
|
|
3854
|
+
export async function requestCyclotronPriming(
|
|
3855
|
+
cyclotronId: string,
|
|
3856
|
+
request: import("../lib/relevance-status").PrimingRequest,
|
|
3857
|
+
): Promise<void> {
|
|
3858
|
+
const { primingRequestPayloadId, PRIMING_REQUEST_KIND, safeId } = await import("../lib/relevance-status");
|
|
3859
|
+
const id = primingRequestPayloadId(cyclotronId, request.requestId);
|
|
3860
|
+
await runGraphql(CREATE_KNOWLEDGE_RAW_PAYLOAD_MUTATION, {
|
|
3861
|
+
input: {
|
|
3862
|
+
id,
|
|
3863
|
+
ownerType: "cyclotron",
|
|
3864
|
+
ownerId: safeId(cyclotronId),
|
|
3865
|
+
payloadKind: PRIMING_REQUEST_KIND,
|
|
3866
|
+
createdAt: request.requestedAt,
|
|
3867
|
+
updatedAt: request.requestedAt,
|
|
3868
|
+
},
|
|
3869
|
+
});
|
|
3870
|
+
await uploadModelPayloadForOwner({
|
|
3871
|
+
ownerKind: "knowledgeRawPayload",
|
|
3872
|
+
ownerId: id,
|
|
3873
|
+
ownerLineageId: id,
|
|
3874
|
+
role: "raw_payload",
|
|
3875
|
+
sortKey: "priming-request",
|
|
3876
|
+
filename: "priming-request.json",
|
|
3877
|
+
mediaType: "application/json",
|
|
3878
|
+
content: JSON.stringify(request),
|
|
3879
|
+
});
|
|
3880
|
+
}
|
|
3881
|
+
|
|
3882
|
+
/** The saved priming requests of a cyclotron, newest first, each with the sweep's outcome once there is one. */
|
|
3883
|
+
export async function loadCyclotronPrimingRequests(
|
|
3884
|
+
cyclotronId: string,
|
|
3885
|
+
): Promise<import("../lib/relevance-status").PrimingRequestView[]> {
|
|
3886
|
+
const { PRIMING_OUTCOME_ROLE, PRIMING_REQUEST_KIND, isPrimingOutcome, safeId } = await import("../lib/relevance-status");
|
|
3887
|
+
const ids: string[] = [];
|
|
3888
|
+
let nextToken: string | null = null;
|
|
3889
|
+
do {
|
|
3890
|
+
const page: { listKnowledgeRawPayloadsByOwnerAndKind?: { items?: Array<{ id: string } | null>; nextToken?: string | null } | null } =
|
|
3891
|
+
await runGraphql(LIST_PRIMING_REQUEST_PAYLOADS_QUERY, {
|
|
3892
|
+
ownerId: safeId(cyclotronId), payloadKind: { eq: PRIMING_REQUEST_KIND }, limit: 100, nextToken,
|
|
3893
|
+
});
|
|
3894
|
+
const connection = page.listKnowledgeRawPayloadsByOwnerAndKind;
|
|
3895
|
+
ids.push(...(connection?.items ?? []).filter((item): item is { id: string } => Boolean(item)).map((item) => item.id));
|
|
3896
|
+
nextToken = connection?.nextToken ?? null;
|
|
3897
|
+
} while (nextToken);
|
|
3898
|
+
const views = await Promise.all(ids.map(async (id) => {
|
|
3899
|
+
const payloads = await loadModelPayloadsForOwner("knowledgeRawPayload", id, ["raw_payload", PRIMING_OUTCOME_ROLE]);
|
|
3900
|
+
const request = payloads.find((payload) => payload.attachment.role === "raw_payload")?.json;
|
|
3901
|
+
const outcome = payloads.find((payload) => payload.attachment.role === PRIMING_OUTCOME_ROLE)?.json;
|
|
3902
|
+
if (!request || typeof request !== "object") return null;
|
|
3903
|
+
return {
|
|
3904
|
+
request: request as import("../lib/relevance-status").PrimingRequest,
|
|
3905
|
+
outcome: isPrimingOutcome(outcome) ? outcome : null,
|
|
3906
|
+
};
|
|
3907
|
+
}));
|
|
3908
|
+
return views
|
|
3909
|
+
.filter((view): view is import("../lib/relevance-status").PrimingRequestView => view !== null)
|
|
3910
|
+
.sort((left, right) => right.request.requestedAt.localeCompare(left.request.requestedAt));
|
|
3911
|
+
}
|
package/components/newspaper.tsx
CHANGED
|
@@ -1139,10 +1139,10 @@ function FrontStoryBlock({
|
|
|
1139
1139
|
<div className="jump-line">
|
|
1140
1140
|
{block.jumpTargetPage ? (
|
|
1141
1141
|
disableLinks ? (
|
|
1142
|
-
<span>{block.jumpLabel
|
|
1142
|
+
<span>{block.jumpLabel}</span>
|
|
1143
1143
|
) : (
|
|
1144
1144
|
<Link href={getPageHref(block.jumpTargetPage, editionBasePath)} onClick={(event) => handleJumpClick(event, block.jumpTargetPage ?? 1, scrollToPage)}>
|
|
1145
|
-
{block.jumpLabel
|
|
1145
|
+
{block.jumpLabel}
|
|
1146
1146
|
</Link>
|
|
1147
1147
|
)
|
|
1148
1148
|
) : (
|
|
@@ -1178,10 +1178,10 @@ function FrontStoryBlock({
|
|
|
1178
1178
|
<div className="jump-line">
|
|
1179
1179
|
{block.jumpTargetPage ? (
|
|
1180
1180
|
disableLinks ? (
|
|
1181
|
-
<span>{block.jumpLabel
|
|
1181
|
+
<span>{block.jumpLabel}</span>
|
|
1182
1182
|
) : (
|
|
1183
1183
|
<Link href={getPageHref(block.jumpTargetPage, editionBasePath)} onClick={(event) => handleJumpClick(event, block.jumpTargetPage ?? 1, scrollToPage)}>
|
|
1184
|
-
{block.jumpLabel
|
|
1184
|
+
{block.jumpLabel}
|
|
1185
1185
|
</Link>
|
|
1186
1186
|
)
|
|
1187
1187
|
) : (
|
|
@@ -2,12 +2,25 @@
|
|
|
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, useBoardColumnState } from "@kanbus/ui";
|
|
6
6
|
import "@kanbus/ui/styles/kanban.css";
|
|
7
7
|
import "./newsroom-newsprint-board.css";
|
|
8
|
-
import {
|
|
8
|
+
import { PlusIcon } from "lucide-react";
|
|
9
|
+
import { buttonVariants } from "./ui/button";
|
|
10
|
+
import { cn } from "../lib/utils";
|
|
11
|
+
import { newsroomHref } from "../lib/newsroom-base-path";
|
|
12
|
+
import {
|
|
13
|
+
ASSIGNMENT_BOARD_TYPE,
|
|
14
|
+
assignmentBoardConfig,
|
|
15
|
+
canReadNewsroomBoard,
|
|
16
|
+
contentItemBoardConfig,
|
|
17
|
+
contentItemBoardType,
|
|
18
|
+
projectAssignmentBoard,
|
|
19
|
+
projectContentItemBoard,
|
|
20
|
+
type NativeBoardIssue,
|
|
21
|
+
} from "../lib/newsroom-board";
|
|
9
22
|
import type { AssignmentRecord } from "../lib/category-repository";
|
|
10
|
-
import { getContentItemName, SITE_BRAND } from "../lib/site-brand";
|
|
23
|
+
import { getContentItemName, getContentItemStageDefinitions, SITE_BRAND } from "../lib/site-brand";
|
|
11
24
|
import { getNewsroomNavHref } from "../lib/newsroom-nav";
|
|
12
25
|
import { loadNewsroomAssignmentPage, loadNewsroomContentItemVersions, loadNewsroomPublishedContentItemVersions } from "./news-desk-taxonomy-client";
|
|
13
26
|
import { useOptionalNewsDeskClient } from "./news-desk-client-provider";
|
|
@@ -17,6 +30,7 @@ import { useResolvedPapyrusTheme } from "./use-resolved-papyrus-theme";
|
|
|
17
30
|
import { readLocalReaderSettings, subscribeReaderSettingsChanges } from "./reader-settings";
|
|
18
31
|
|
|
19
32
|
const STATIC_MOTION = { mode: "static" } as const;
|
|
33
|
+
const BOARD_REFRESH_INTERVAL_MILLISECONDS = 30_000;
|
|
20
34
|
|
|
21
35
|
async function loadEveryNewsroomAssignment() {
|
|
22
36
|
const assignments: AssignmentRecord[] = [];
|
|
@@ -29,20 +43,22 @@ async function loadEveryNewsroomAssignment() {
|
|
|
29
43
|
return assignments;
|
|
30
44
|
}
|
|
31
45
|
|
|
32
|
-
|
|
33
|
-
|
|
46
|
+
export type NewsroomBoardSubject = "contentItem" | "assignment";
|
|
47
|
+
|
|
48
|
+
/** Read-only board of one record kind over authenticated CMS records; existing workspaces own every mutation. */
|
|
49
|
+
export function NewsroomBoardView({ subject }: { subject: NewsroomBoardSubject }) {
|
|
34
50
|
const session = useOptionalNewsDeskClient();
|
|
35
51
|
const allowed = canReadNewsroomBoard(session?.shell.phase);
|
|
36
52
|
const theme = useResolvedPapyrusTheme();
|
|
37
53
|
const [lowMotion, setLowMotion] = useState(false);
|
|
38
54
|
const [issues, setIssues] = useState<NativeBoardIssue[]>([]);
|
|
39
|
-
const [filter, setFilter] = useState(contentItemBoardType());
|
|
40
55
|
const [view, setView] = useState<"board" | "list">("board");
|
|
41
|
-
const [revision, setRevision] = useState(0);
|
|
42
56
|
const [selected, setSelected] = useState<string | null>(null);
|
|
43
57
|
const [loading, setLoading] = useState(false);
|
|
44
58
|
const [error, setError] = useState<string | null>(null);
|
|
45
59
|
const generation = useRef(0);
|
|
60
|
+
const openedCollapsedUntilOpenedStagesRef = useRef<string[]>([]);
|
|
61
|
+
const refreshIssuesRef = useRef<((initial: boolean) => void) | null>(null);
|
|
46
62
|
useEffect(() => {
|
|
47
63
|
setLowMotion(readLocalReaderSettings().motion === "low");
|
|
48
64
|
return subscribeReaderSettingsChanges(settings => setLowMotion(settings.motion === "low"));
|
|
@@ -51,35 +67,60 @@ export function NewsroomBoardView() {
|
|
|
51
67
|
const request = ++generation.current;
|
|
52
68
|
setIssues([]); setSelected(null); setError(null);
|
|
53
69
|
if (!allowed) { setLoading(false); return; }
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
const loadIssues = () => subject === "contentItem"
|
|
71
|
+
? Promise.all([
|
|
72
|
+
Promise.all([loadNewsroomContentItemVersions({ includeCollapsedUntilOpenedStages: openedCollapsedUntilOpenedStagesRef.current }), loadNewsroomPublishedContentItemVersions()]).then(([workingVersions, publishedVersions]) => [...workingVersions, ...publishedVersions]),
|
|
73
|
+
loadEveryNewsroomAssignment(),
|
|
74
|
+
]).then(([contentItemVersions, assignments]) => projectContentItemBoard({ contentItemVersions, assignments }))
|
|
75
|
+
: loadEveryNewsroomAssignment().then(projectAssignmentBoard);
|
|
76
|
+
const refresh = (initial: boolean) => {
|
|
77
|
+
if (initial) setLoading(true);
|
|
78
|
+
loadIssues().then(loadedIssues => {
|
|
79
|
+
if (generation.current !== request) return;
|
|
80
|
+
setIssues(loadedIssues);
|
|
81
|
+
setError(null);
|
|
82
|
+
}).catch((reason: unknown) => {
|
|
83
|
+
if (generation.current === request && initial) setError(reason instanceof Error ? reason.message : "Could not load newsroom records.");
|
|
84
|
+
}).finally(() => { if (generation.current === request && initial) setLoading(false); });
|
|
85
|
+
};
|
|
86
|
+
refreshIssuesRef.current = refresh;
|
|
87
|
+
refresh(true);
|
|
88
|
+
const refreshWhenVisible = () => { if (document.visibilityState === "visible") refresh(false); };
|
|
89
|
+
const interval = window.setInterval(refreshWhenVisible, BOARD_REFRESH_INTERVAL_MILLISECONDS);
|
|
90
|
+
document.addEventListener("visibilitychange", refreshWhenVisible);
|
|
91
|
+
return () => { generation.current = request + 1; window.clearInterval(interval); document.removeEventListener("visibilitychange", refreshWhenVisible); };
|
|
92
|
+
}, [allowed, subject]);
|
|
93
|
+
const config = useMemo(() => subject === "contentItem" ? contentItemBoardConfig(issues) : assignmentBoardConfig(issues), [issues, subject]);
|
|
94
|
+
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
|
+
const pageTitle = subject === "contentItem" ? getContentItemName().plural : "Assignments";
|
|
67
104
|
const issue = issues.find(item => item.id === selected);
|
|
68
105
|
if (!allowed) return <NewsDeskAccessGate shell={session?.shell ?? null} />;
|
|
69
|
-
return <NewsroomOpsShell activeTab="
|
|
106
|
+
return <NewsroomOpsShell activeTab={subject === "contentItem" ? "articles" : "assignments"} appTitle={SITE_BRAND.appTitle} backLabel={SITE_BRAND.backToHomeLabel} pageTitle={pageTitle}
|
|
107
|
+
headerActions={<>
|
|
108
|
+
<div role="group" aria-label="Record presentation" className="organizer-board-view-toggle"><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>
|
|
109
|
+
{subject === "contentItem" && (
|
|
110
|
+
<Link className={cn(buttonVariants({ size: "sm" }))} data-newsroom-article-new href={getNewsroomNavHref(newsroomHref("articles", "new"))}>
|
|
111
|
+
<PlusIcon aria-hidden="true" />
|
|
112
|
+
{`New ${getContentItemName().singular.toLowerCase()}`}
|
|
113
|
+
</Link>
|
|
114
|
+
)}
|
|
115
|
+
</>}>
|
|
70
116
|
<div className="organizer-native-board papyrus-newsprint-board" data-color-mode={theme}>
|
|
71
|
-
<div className="organizer-board-toolbar">
|
|
72
|
-
<IssueTypeFilter config={config} value={filter} onChange={value => { setFilter(value); setSelected(null); }} name="Record type" />
|
|
73
|
-
<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><button type="button" disabled={loading} onClick={() => setRevision(current => current + 1)}>Refresh records</button></div>
|
|
74
|
-
</div>
|
|
75
|
-
<p className="organizer-board-note">{`Each ${getContentItemName().singular.toLowerCase()} appears once, in the column for its stage. Open a record to use its existing editor or workflow controls.`}</p>
|
|
76
117
|
{error && <NewsroomOpsStatusBanner tone="error">{error}</NewsroomOpsStatusBanner>}
|
|
77
118
|
<p role="status" className="organizer-board-note">{loading ? "Loading records…" : `${issues.length} records loaded.`}</p>
|
|
78
119
|
<div className="organizer-board-body" data-detail-open={Boolean(issue)}>
|
|
79
|
-
<IssueCollection issues={issues} config={config} query={{ type:
|
|
120
|
+
<IssueCollection issues={issues} config={config} query={{ type: boardType }} view={view}
|
|
80
121
|
motion={lowMotion ? STATIC_MOTION : undefined}
|
|
81
|
-
|
|
82
|
-
selectedIssueId={selected} detailOpen={Boolean(issue)} onSelectIssue={item => setSelected(item.id)} ariaLabel=
|
|
122
|
+
collapsedColumns={columnState.collapsedColumns} onToggleCollapse={columnState.toggleColumn} initialScroll="start"
|
|
123
|
+
selectedIssueId={selected} detailOpen={Boolean(issue)} onSelectIssue={item => setSelected(item.id)} ariaLabel={pageTitle}
|
|
83
124
|
emptyMessage={loading ? "Loading records…" : error ? "Records could not be loaded." : "No records in this selection."} />
|
|
84
125
|
{issue && <IssueDetailPanel issue={issue} onClose={() => setSelected(null)} fields={[{ label: "CMS record ID", value: issue.nativeId }, { label: "Status", value: issue.nativeStatus }]}>
|
|
85
126
|
<Link href={getNewsroomNavHref(issue.href)}>Open {issue.type}</Link>
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { NewsDeskClientProvider } from "./news-desk-client-provider";
|
|
2
|
+
import { createDemoCategorySteeringDashboard } from "../lib/category-repository";
|
|
2
3
|
import { resolveDevSandboxEditorAuth } from "../lib/dev-sandbox-editor-auth";
|
|
4
|
+
import { isLocalPreviewEnabled } from "../lib/local-preview";
|
|
5
|
+
import { getBrandNewsroomSectionsConfigPath, resolveActiveSiteBrand } from "../lib/site-brand-tenant";
|
|
3
6
|
|
|
4
7
|
export function NewsroomClientShell({ children }: Readonly<{ children: React.ReactNode }>) {
|
|
5
8
|
const devSandboxEditorAuth = resolveDevSandboxEditorAuth();
|
|
9
|
+
const activeBrand = resolveActiveSiteBrand();
|
|
10
|
+
const localPreviewDashboard = isLocalPreviewEnabled()
|
|
11
|
+
? createDemoCategorySteeringDashboard(activeBrand.id, getBrandNewsroomSectionsConfigPath(activeBrand))
|
|
12
|
+
: null;
|
|
6
13
|
|
|
7
14
|
return (
|
|
8
|
-
<NewsDeskClientProvider devSandboxEditorAuth={devSandboxEditorAuth}>
|
|
15
|
+
<NewsDeskClientProvider devSandboxEditorAuth={devSandboxEditorAuth} localPreviewDashboard={localPreviewDashboard}>
|
|
9
16
|
{children}
|
|
10
17
|
</NewsDeskClientProvider>
|
|
11
18
|
);
|
|
@@ -15,13 +15,13 @@ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGro
|
|
|
15
15
|
import type { NewsroomOpsShellProps } from "./newsroom-ops-shell";
|
|
16
16
|
import "./newsroom-organizer.css";
|
|
17
17
|
|
|
18
|
-
const ICONS = { overview: LayoutDashboard,
|
|
18
|
+
const ICONS = { overview: LayoutDashboard, articles: ClipboardList, references: BookOpen, messages: MessagesSquare };
|
|
19
19
|
|
|
20
20
|
/** Navigation is a presentation layer; existing route and server authorization still own access. */
|
|
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 => activePath ===
|
|
24
|
+
const active = all.filter(item => [item.path, ...(item.alsoActiveOnPaths ?? [])].some(candidate => activePath === candidate || (candidate !== "/newsroom" && activePath.startsWith(candidate + "/")))).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>
|
|
@@ -99,18 +99,23 @@ export function NewsroomOrganizerShell({ activeTab, appTitle, backHref = "/", ba
|
|
|
99
99
|
return () => { document.removeEventListener("focusin", remember); media.removeEventListener("change", change); cancelAnimationFrame(frame); };
|
|
100
100
|
}, []);
|
|
101
101
|
const activePath = browserPathToInternalNewsroomPath(pathname ?? "/newsroom");
|
|
102
|
+
const sidebarToggleLabel = collapsed ? "Expand sidebar" : "Collapse sidebar";
|
|
103
|
+
const sidebarToggle = <button type="button" className="organizer-toggle" title={sidebarToggleLabel}
|
|
104
|
+
aria-label={sidebarToggleLabel} aria-expanded={!collapsed} aria-controls="organizer-sidebar" onClick={() => setCollapsed(value => !value)}>
|
|
105
|
+
{collapsed ? <PanelLeftOpen size={16} aria-hidden="true" /> : <PanelLeftClose size={16} aria-hidden="true" />}
|
|
106
|
+
</button>;
|
|
102
107
|
return <div className="newsroom-organizer" data-news-desk data-newsroom-ops-shell="true" data-newsroom-chrome="organizer" data-sidebar-collapsed={collapsed || !showNavigation ? "true" : "false"}>
|
|
103
108
|
{showNavigation && <aside className="organizer-sidebar" hidden={collapsed} id="organizer-sidebar">
|
|
104
|
-
<
|
|
109
|
+
<div className="organizer-sidebar-head">
|
|
110
|
+
<Link className="organizer-brand" href={getNewsroomNavHref("/newsroom", demo)}>{appTitle}<small>The newsroom</small></Link>
|
|
111
|
+
{sidebarToggle}
|
|
112
|
+
</div>
|
|
105
113
|
<Navigation activePath={activePath} demo={demo} admin={admin} surface="sidebar" />
|
|
106
114
|
<Account placement="sidebar" />
|
|
107
115
|
</aside>}
|
|
108
116
|
<div className="organizer-workspace">
|
|
109
117
|
<header className="organizer-topbar" data-newsroom-ops-topbar>
|
|
110
|
-
{showNavigation &&
|
|
111
|
-
aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"} aria-expanded={!collapsed} aria-controls="organizer-sidebar" onClick={() => setCollapsed(value => !value)}>
|
|
112
|
-
{collapsed ? <PanelLeftOpen size={16} aria-hidden="true" /> : <PanelLeftClose size={16} aria-hidden="true" />}<span>{collapsed ? "Expand sidebar" : "Collapse sidebar"}</span>
|
|
113
|
-
</button>}
|
|
118
|
+
{showNavigation && collapsed && sidebarToggle}
|
|
114
119
|
<Link className="organizer-masthead" href={backHref} title={backLabel}>{appTitle}</Link>
|
|
115
120
|
<h1 data-newsroom-ops-title>{pageTitle}</h1><div className="organizer-header-actions">{headerActions}</div><Account placement="header" />
|
|
116
121
|
</header>
|
|
@@ -118,7 +123,7 @@ export function NewsroomOrganizerShell({ activeTab, appTitle, backHref = "/", ba
|
|
|
118
123
|
</div>
|
|
119
124
|
{showNavigation && <nav className="organizer-bottom-nav" aria-label="Primary newsroom navigation"><ul>
|
|
120
125
|
{ORGANIZER_PRIMARY_NAV.map(item => { const Icon = ICONS[item.key as keyof typeof ICONS]; return <li key={item.key}><Link
|
|
121
|
-
href={getNewsroomNavHref(item.path, demo)} title={item.description} aria-label={item.label} aria-current={activeTab === item.key
|
|
126
|
+
href={getNewsroomNavHref(item.path, demo)} title={item.description} aria-label={item.label} aria-current={activeTab === item.key ? "page" : undefined}>
|
|
122
127
|
<Icon size={18} aria-hidden="true" /><span>{item.label}</span></Link></li>; })}
|
|
123
128
|
<li><Sheet open={menuOpen} onOpenChange={setMenuOpen}><SheetTrigger className="organizer-more" aria-label="More"><Menu size={18} aria-hidden="true" /><span>More</span></SheetTrigger>
|
|
124
129
|
<SheetContent side="bottom" className="organizer-menu"><SheetHeader><SheetTitle>Newsroom</SheetTitle><SheetDescription>Workspaces and administration</SheetDescription></SheetHeader>
|
|
@@ -23,7 +23,8 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
|
|
|
23
23
|
.organizer-topbar{height:57px;min-height:57px;display:flex;align-items:center;gap:16px;border-bottom:1px solid var(--org-ink)}
|
|
24
24
|
.organizer-masthead{font:900 28px Newsroom Display,Georgia,serif;white-space:nowrap;max-width:30vw;overflow:hidden;text-overflow:ellipsis}
|
|
25
25
|
.organizer-topbar h1{font:400 12px Newsroom UI,system-ui;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:0}
|
|
26
|
-
.organizer-toggle{display:flex;align-items:center;
|
|
26
|
+
.organizer-toggle{display:flex;align-items:center;justify-content:center;width:36px;min-height:36px;padding:0;border:1px solid var(--org-line);flex-shrink:0}
|
|
27
|
+
.organizer-sidebar-head{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}.organizer-sidebar-head .organizer-brand{flex:1;min-width:0}
|
|
27
28
|
.organizer-header-actions{display:flex;align-items:center;gap:8px;min-width:0;max-width:45%;overflow-x:auto}
|
|
28
29
|
.organizer-main{min-width:0;min-height:0;overflow:auto;overscroll-behavior:contain;flex:1;padding:16px 0}
|
|
29
30
|
.organizer-main>[data-category-steering]{min-height:0}.organizer-demo-notice{font-size:11px;color:var(--org-muted);margin-bottom:12px}
|
|
@@ -36,10 +37,9 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
|
|
|
36
37
|
.organizer-bottom-nav{display:none}
|
|
37
38
|
.organizer-main:has(.organizer-native-board){display:flex;overflow:hidden}
|
|
38
39
|
.organizer-native-board{display:flex;flex-direction:column;flex:1;min-height:0;min-width:0;gap:12px}
|
|
39
|
-
.organizer-board-
|
|
40
|
-
.organizer-board-
|
|
41
|
-
.organizer-board-
|
|
42
|
-
.organizer-board-toolbar button[aria-pressed=true]{background:var(--org-ink);color:var(--org-paper)}
|
|
40
|
+
.organizer-board-view-toggle{display:flex;gap:4px;flex-shrink:0}
|
|
41
|
+
.organizer-board-view-toggle button{min-height:36px;padding:4px 12px;border:1px solid var(--org-line)}
|
|
42
|
+
.organizer-board-view-toggle button[aria-pressed=true]{background:var(--org-ink);color:var(--org-paper)}
|
|
43
43
|
.organizer-board-note{font-size:11px;line-height:1.5;margin:0;color:var(--org-muted)}
|
|
44
44
|
.organizer-board-body{position:relative;display:grid;grid-template-columns:minmax(0,1fr);flex:1;min-width:0;min-height:0;gap:16px}
|
|
45
45
|
.organizer-board-body[data-detail-open=true]{grid-template-columns:minmax(0,1fr) minmax(280px,360px)}
|
|
@@ -49,7 +49,9 @@ import { normalizeReferenceCurationStatus } from "../lib/reference-policy";
|
|
|
49
49
|
import { newsroomListRowClassName } from "../lib/newsroom-list-selection";
|
|
50
50
|
import { ReferenceSourcePreview } from "./reference-source-preview";
|
|
51
51
|
import { ReferenceRelevanceReview } from "./reference-relevance-review";
|
|
52
|
+
import { ReferenceCyclotronPriming, type PrimingFormInput } from "./reference-cyclotron-priming";
|
|
52
53
|
import { ReferenceCyclotronStatus, type ReviewRateChoice } from "./reference-cyclotron-status";
|
|
54
|
+
import type { PrimingRequestView } from "../lib/relevance-status";
|
|
53
55
|
import type { CyclotronStatus } from "cyclotron/cyclotron-status";
|
|
54
56
|
import type { RelevanceCuration, RelevanceDecisionView } from "../lib/relevance-decisions";
|
|
55
57
|
import {
|
|
@@ -62,6 +64,12 @@ type NewsroomReferencesViewProps = {
|
|
|
62
64
|
priming?: PrimingPanelProps;
|
|
63
65
|
/** The relevance cyclotron's status snapshot, shown above the list. */
|
|
64
66
|
cyclotronStatus?: CyclotronStatus | null;
|
|
67
|
+
/** Present for editors who may prime the running relevance cyclotron with a pasted URL. */
|
|
68
|
+
cyclotronPriming?: {
|
|
69
|
+
labels: readonly string[];
|
|
70
|
+
requests: PrimingRequestView[];
|
|
71
|
+
onRequest: (input: PrimingFormInput) => Promise<void> | void;
|
|
72
|
+
};
|
|
65
73
|
/** Present for editors who may ask for a manual review rate. */
|
|
66
74
|
onRequestReviewRate?: (choice: ReviewRateChoice) => Promise<void> | void;
|
|
67
75
|
demo?: boolean;
|
|
@@ -399,6 +407,7 @@ function ReferenceDetailPanel({
|
|
|
399
407
|
export function NewsroomReferencesView({
|
|
400
408
|
priming,
|
|
401
409
|
cyclotronStatus,
|
|
410
|
+
cyclotronPriming,
|
|
402
411
|
decisions,
|
|
403
412
|
onRequestReviewRate,
|
|
404
413
|
demo = false,
|
|
@@ -516,6 +525,15 @@ export function NewsroomReferencesView({
|
|
|
516
525
|
</div>
|
|
517
526
|
<ReferenceCurationControls disabled={disabled} />
|
|
518
527
|
{priming && !disabled ? <ReferencePriming {...priming} /> : null}
|
|
528
|
+
{cyclotronPriming && !disabled ? (
|
|
529
|
+
<div className="shrink-0">
|
|
530
|
+
<ReferenceCyclotronPriming
|
|
531
|
+
labels={cyclotronPriming.labels}
|
|
532
|
+
onRequestPriming={cyclotronPriming.onRequest}
|
|
533
|
+
requests={cyclotronPriming.requests}
|
|
534
|
+
/>
|
|
535
|
+
</div>
|
|
536
|
+
) : null}
|
|
519
537
|
{cyclotronStatus ? (
|
|
520
538
|
<div className="shrink-0">
|
|
521
539
|
<ReferenceCyclotronStatus onRequestRate={disabled ? undefined : onRequestReviewRate} status={cyclotronStatus} />
|