@anthusai/papyrus 2.0.0-next.9 → 2.0.0-next.90
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/auth/resource.js +16 -1
- package/amplify/auth/resource.ts +19 -1
- package/amplify/data/function-handler.js +20 -0
- package/amplify/data/function-handler.ts +40 -0
- package/amplify/data/resource.js +151 -61
- package/amplify/data/resource.ts +157 -61
- package/amplify/data/schema.js +2096 -1980
- package/amplify/data/schema.ts +198 -69
- package/amplify/functions/assignment-action/data-access.js +2 -0
- package/amplify/functions/assignment-action/data-access.ts +2 -0
- package/amplify/functions/assignment-action/handler.ts +1 -1
- package/amplify/functions/automation-view/handler.py +17 -0
- package/amplify/functions/automation-view/resource.js +24 -0
- package/amplify/functions/automation-view/resource.ts +34 -0
- package/amplify/functions/automation-view/schema.js +26 -0
- package/amplify/functions/automation-view/schema.ts +28 -0
- package/amplify/functions/automation-view/wire.js +56 -0
- package/amplify/functions/automation-view/wire.ts +76 -0
- package/amplify/functions/content-actions/handler.py +61 -2
- package/amplify/functions/content-actions/requirements.txt +1 -0
- package/amplify/functions/content-actions/resource.js +1 -1
- package/amplify/functions/content-actions/resource.ts +1 -1
- package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/handler.py +3 -3
- package/amplify/functions/editorial-checks/requirements.txt +1 -0
- package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.js +7 -7
- package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.ts +7 -7
- package/amplify/functions/editorial-checks/schema.js +122 -0
- package/amplify/{plugins → functions}/editorial-checks/schema.ts +40 -12
- package/amplify/functions/editorial-checks/wire.js +53 -0
- package/amplify/functions/editorial-checks/wire.ts +61 -0
- package/amplify/functions/monitoring-view/handler.py +11 -0
- package/amplify/functions/monitoring-view/resource.js +24 -0
- package/amplify/functions/monitoring-view/resource.ts +34 -0
- package/amplify/functions/monitoring-view/schema.js +17 -0
- package/amplify/functions/monitoring-view/schema.ts +19 -0
- package/amplify/functions/monitoring-view/wire.js +37 -0
- package/amplify/functions/monitoring-view/wire.ts +47 -0
- package/amplify/functions/public-forms/core.js +247 -0
- package/amplify/functions/public-forms/core.ts +310 -0
- package/amplify/functions/public-forms/handler.ts +41 -0
- package/amplify/functions/public-forms/origins.js +35 -0
- package/amplify/functions/public-forms/origins.ts +43 -0
- package/amplify/functions/public-forms/resource.js +9 -0
- package/amplify/functions/public-forms/resource.ts +10 -0
- package/amplify/functions/public-forms/wire.js +25 -0
- package/amplify/functions/public-forms/wire.ts +32 -0
- package/amplify/functions/shared/editorial-context.ts +63 -4
- package/amplify/functions/shared/python-bundle.js +20 -14
- package/amplify/functions/shared/python-bundle.ts +25 -18
- package/amplify/plugins/newsletter/config.js +245 -0
- package/amplify/plugins/newsletter/config.ts +305 -0
- package/amplify/plugins/newsletter/function/handler.py +6 -0
- package/amplify/plugins/newsletter/function/requirements.txt +5 -0
- package/amplify/plugins/newsletter/ops/handler.py +12 -0
- package/amplify/plugins/newsletter/ops/resource.js +27 -0
- package/amplify/plugins/newsletter/ops/resource.ts +37 -0
- package/amplify/plugins/newsletter/papyrus.plugin.js +263 -0
- package/amplify/plugins/newsletter/papyrus.plugin.ts +311 -0
- package/amplify/plugins/newsletter/schema.js +57 -0
- package/amplify/plugins/newsletter/schema.ts +62 -0
- package/amplify/plugins/newsletter/secure-string-parameter.js +107 -0
- package/amplify/plugins/newsletter/secure-string-parameter.ts +120 -0
- package/amplify/plugins/newsletter/sending.js +140 -0
- package/amplify/plugins/newsletter/sending.ts +186 -0
- package/amplify/plugins/reader-steering/config.js +182 -0
- package/amplify/plugins/reader-steering/config.ts +290 -0
- package/amplify/plugins/reader-steering/function/ai_moderation.py +246 -0
- package/amplify/plugins/reader-steering/function/handler.py +1567 -0
- package/amplify/plugins/reader-steering/function/moderation-prompt.md +27 -0
- package/amplify/plugins/reader-steering/function/moderation.py +316 -0
- package/amplify/plugins/reader-steering/function/proposals.py +458 -0
- package/amplify/plugins/reader-steering/function/resource.js +27 -0
- package/amplify/plugins/reader-steering/function/resource.ts +35 -0
- package/amplify/plugins/reader-steering/papyrus.plugin.js +186 -0
- package/amplify/plugins/reader-steering/papyrus.plugin.ts +212 -0
- package/amplify/plugins/reader-steering/schema.js +229 -0
- package/amplify/plugins/reader-steering/schema.ts +231 -0
- package/amplify/plugins/registry.js +4 -2
- package/amplify/plugins/registry.ts +4 -2
- package/amplify/plugins/signup/schema.js +2 -1
- package/amplify/plugins/signup/schema.ts +2 -1
- package/amplify/plugins/types.ts +5 -0
- package/amplify/plugins/wire.js +1 -0
- package/amplify/plugins/wire.ts +1 -0
- package/amplify/publication/projection-lease.js +30 -0
- package/amplify/publication/projection-lease.ts +39 -0
- package/amplify/publication/redirect-and-index.js +105 -0
- package/amplify/publication/redirect-rules.js +73 -0
- package/amplify/publication/redirect-rules.ts +93 -0
- package/amplify/publication/s3-projection.js +7 -1
- package/amplify/publication/s3-projection.ts +9 -1
- package/amplify/site-backend-config.ts +18 -1
- package/amplify/site-backend-names.js +4 -2
- package/amplify/site-backend-names.ts +4 -2
- package/amplify/site-backend.js +45 -3
- package/amplify/site-backend.ts +51 -3
- package/app/api/auth/prospectus/sign-in/route.ts +4 -0
- package/app/api/newsletter/confirm/route.ts +13 -0
- package/app/api/newsletter/preferences/route.ts +13 -0
- package/app/api/newsletter/state/route.ts +9 -0
- package/app/api/newsletter/subscribe/route.ts +9 -0
- package/app/api/newsletter/unsubscribe/route.ts +13 -0
- package/app/api/newsroom/curation/context/route.ts +9 -0
- package/app/api/newsroom/curation/priming/route.ts +9 -0
- package/app/api/newsroom/curation/publication/route.ts +9 -0
- package/app/api/reader/comment/delete/route.ts +9 -0
- package/app/api/reader/comment/edit/route.ts +9 -0
- package/app/api/reader/comment/route.ts +9 -0
- package/app/api/reader/flag/route.ts +9 -0
- package/app/api/reader/handle/route.ts +9 -0
- package/app/api/reader/me/route.ts +9 -0
- package/app/api/reader/rating/route.ts +9 -0
- package/app/api/reader/report/route.ts +9 -0
- package/app/api/reader/retired/route.ts +9 -0
- package/app/api/reader/submit/route.ts +9 -0
- package/app/api/reader/vote/route.ts +9 -0
- package/bin/papyrus-infra.mjs +20 -1
- package/components/automation-panel.tsx +49 -0
- package/components/automation-run-now.tsx +97 -0
- package/components/automation-view.tsx +195 -0
- package/components/editorial-checks-admin-panel.tsx +38 -13
- package/components/moderation-panel.tsx +248 -0
- package/components/monitoring-overview-view.tsx +241 -0
- package/components/monitoring-panel.tsx +40 -0
- package/components/news-desk-client-provider.tsx +18 -18
- package/components/news-desk-taxonomy-client.ts +28 -8
- package/components/newsletter-panel.tsx +474 -0
- package/components/newsletter-sections.tsx +163 -0
- package/components/newsroom-access-gate.tsx +16 -2
- package/components/newsroom-article-editor.tsx +11 -11
- package/components/newsroom-articles-view.tsx +6 -6
- package/components/newsroom-assignment-detail.tsx +6 -6
- package/components/newsroom-board-view.tsx +8 -3
- package/components/newsroom-curation-bootstrap.tsx +45 -0
- package/components/newsroom-ops-overview.tsx +26 -96
- package/components/newsroom-ops-shell.tsx +2 -2
- package/components/newsroom-organizer.css +33 -0
- package/components/newsroom-overview-skeleton.tsx +25 -0
- package/components/newsroom-references-view.tsx +192 -49
- package/components/notification-center.tsx +277 -0
- package/components/publication-cyclotron-panel.tsx +15 -10
- package/components/reader-auth-state.ts +1 -1
- package/components/reference-add-entry.tsx +75 -0
- package/components/reference-curation-controls.tsx +31 -22
- package/components/reference-cyclotron-priming.tsx +58 -19
- package/components/reference-cyclotron-suggestion.tsx +10 -6
- package/components/reference-priming.tsx +14 -10
- package/components/reference-relevance-review.tsx +3 -3
- package/components/reference-section-suggestion.tsx +1 -1
- package/components/topic-steering-workspace.tsx +237 -158
- package/components/use-editorial-checks.ts +12 -10
- package/infra/amplify-app-shell.js +94 -16
- package/infra/amplify-app-shell.ts +106 -18
- package/infra/cloud-email-config.js +32 -0
- package/infra/cloud-email-config.ts +39 -0
- package/infra/cloud-email.js +57 -0
- package/infra/cloud-email.test.js +69 -0
- package/infra/cloud-email.test.ts +66 -0
- package/infra/cloud-email.ts +60 -0
- package/infra/index.js +1 -0
- package/infra/secure-string-parameter-handler.js +41 -0
- package/infra/secure-string-parameter-handler.ts +41 -0
- package/infra/secure-string-parameter.js +79 -0
- package/infra/secure-string-parameter.ts +91 -0
- package/infra/site-config.js +88 -6
- package/infra/site-config.ts +134 -6
- package/lib/automation-client.ts +54 -0
- package/lib/automation-fixture.ts +93 -0
- package/lib/automation-overview.ts +252 -0
- package/lib/category-repository.ts +14 -12
- package/lib/console-web-context.ts +1 -1
- package/lib/curation-routes.ts +345 -0
- package/lib/cyclotron-suggestions.ts +4 -4
- package/lib/editorial-checks-backend.ts +1 -1
- package/lib/editorial-checks-client.ts +19 -1
- package/lib/editorial-checks-demo.ts +2 -2
- package/lib/editorial-checks-settings.ts +3 -0
- package/lib/editorial-checks.ts +8 -11
- package/lib/editorial-preview.ts +5 -5
- package/lib/monitoring-client.ts +29 -0
- package/lib/monitoring-overview-fixture.ts +51 -0
- package/lib/monitoring-overview.ts +145 -0
- package/lib/news-desk-session.ts +12 -21
- package/lib/newsletter-api.ts +418 -0
- package/lib/newsletter-client.ts +46 -0
- package/lib/newsletter-fixture.ts +184 -0
- package/lib/newsletter-model.ts +202 -0
- package/lib/newsroom-demo-dashboard.ts +1 -1
- package/lib/newsroom-nav.ts +1 -1
- package/lib/newsroom-organizer-nav.ts +2 -1
- package/lib/prospectus-session.ts +15 -14
- package/lib/prospectus-sign-in-routes.ts +105 -20
- package/lib/prospectus-site-identity.ts +46 -6
- package/lib/reader-api.ts +145 -0
- package/lib/reader-moderation.ts +220 -0
- package/lib/reader-retirement-route.ts +89 -0
- package/lib/reader-subject.md +29 -0
- package/lib/reader-subject.ts +53 -0
- package/lib/reference-review-details.ts +71 -0
- package/lib/reference-review-request.ts +14 -0
- package/lib/relevance-decisions.ts +42 -10
- package/lib/relevance-status.ts +2 -2
- package/lib/review-client.ts +117 -0
- package/lib/review-fixture.ts +45 -0
- package/lib/review-requests.ts +244 -0
- package/lib/semantic-graph.ts +2 -0
- package/lib/site-brand-tenant.ts +11 -6
- package/lib/site-brand.ts +6 -3
- package/lib/site-origin.ts +67 -0
- package/lib/use-reference-review-data.ts +44 -0
- package/middleware.ts +14 -3
- package/package.json +56 -3
- package/routes.manifest.json +215 -2
- package/amplify/plugins/editorial-checks/function/requirements.txt +0 -1
- package/amplify/plugins/editorial-checks/papyrus.plugin.js +0 -71
- package/amplify/plugins/editorial-checks/papyrus.plugin.ts +0 -87
- package/amplify/plugins/editorial-checks/schema.js +0 -94
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import {
|
|
2
|
+
countsText,
|
|
3
|
+
formatInZone,
|
|
4
|
+
formatUtc,
|
|
5
|
+
issueEditorHref,
|
|
6
|
+
statusLabel,
|
|
7
|
+
statusMark,
|
|
8
|
+
type NewsletterDelivery,
|
|
9
|
+
type NewsletterEvent,
|
|
10
|
+
type NewsletterIssue,
|
|
11
|
+
type NewsletterOverview,
|
|
12
|
+
type NewsletterSubscriber,
|
|
13
|
+
} from "../lib/newsletter-model";
|
|
14
|
+
|
|
15
|
+
/** Presentation only: the panel fetches and holds state, these render what they are given, so every state is testable from fixtures. */
|
|
16
|
+
|
|
17
|
+
export function ListsCard({ overview }: { overview: NewsletterOverview }) {
|
|
18
|
+
const { totals } = overview;
|
|
19
|
+
return (
|
|
20
|
+
<article className="news-desk-doctrine-card" data-newsletter-section="lists">
|
|
21
|
+
<div className="news-desk-doctrine-card__header">
|
|
22
|
+
<div><h3>Lists</h3><span>Subscribers per list. A person on two lists counts on each.</span></div>
|
|
23
|
+
</div>
|
|
24
|
+
{overview.lists.length ? (
|
|
25
|
+
<dl className="news-desk-monitoring__usage">
|
|
26
|
+
{overview.lists.map((list) => (
|
|
27
|
+
<div key={list.id} data-newsletter-list={list.id}>
|
|
28
|
+
<dt>{list.name} ({list.kind === "roundup" ? "roundup" : list.kind === "alert" ? "alerts" : list.kind})</dt>
|
|
29
|
+
<dd>{list.confirmed}</dd>
|
|
30
|
+
<span className="news-desk-newsletter__quiet">confirmed · {list.pending} pending · {list.unsubscribed} unsubscribed</span>
|
|
31
|
+
</div>
|
|
32
|
+
))}
|
|
33
|
+
</dl>
|
|
34
|
+
) : <p>No list is configured.</p>}
|
|
35
|
+
<p className="news-desk-delegation-note" data-newsletter-totals>
|
|
36
|
+
Across the site: {totals.confirmed} confirmed, {totals.pending} waiting to confirm, {totals.unsubscribed} unsubscribed.
|
|
37
|
+
{" "}{overview.suppressed} {overview.suppressed === 1 ? "address is" : "addresses are"} suppressed after a bounce or complaint.
|
|
38
|
+
</p>
|
|
39
|
+
</article>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function SiteStatusCard({ paused, isAdmin, busy, onResume }: { paused: NewsletterOverview["paused"]; isAdmin: boolean; busy: boolean; onResume: () => void }) {
|
|
44
|
+
return (
|
|
45
|
+
<article className="news-desk-doctrine-card" data-newsletter-section="site-status">
|
|
46
|
+
<div className="news-desk-doctrine-card__header">
|
|
47
|
+
<div><h3>Site status</h3><span>Sending turns itself off when bounces or complaints run high</span></div>
|
|
48
|
+
</div>
|
|
49
|
+
{paused.paused ? (
|
|
50
|
+
<div className="category-steering-alert" role="alert" data-newsletter-paused>
|
|
51
|
+
<strong><span aria-hidden="true">■ </span>Sending is paused.</strong>{" "}
|
|
52
|
+
{paused.reason ? <>Reason: {paused.reason}. </> : null}
|
|
53
|
+
{paused.pausedAt ? <>Paused {formatUtc(paused.pausedAt)}. </> : null}
|
|
54
|
+
Issues due now are blocked until sending resumes; approve a blocked issue again afterwards.
|
|
55
|
+
<div className="news-desk-doctrine-card__actions">
|
|
56
|
+
{isAdmin ? (
|
|
57
|
+
<button type="button" disabled={busy} data-newsletter-resume onClick={onResume}>Resume sending</button>
|
|
58
|
+
) : <span>An admin can resume sending.</span>}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
) : (
|
|
62
|
+
<p data-newsletter-running><span aria-hidden="true">✓ </span>Sending is on.{paused.resumedAt ? ` Resumed ${formatUtc(paused.resumedAt)}.` : ""}</p>
|
|
63
|
+
)}
|
|
64
|
+
</article>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** What an issue row says about the issue; the buttons beside it belong to the panel. */
|
|
69
|
+
export function IssueSummary({ issue, zone, demo }: { issue: NewsletterIssue; zone: string; demo: boolean }) {
|
|
70
|
+
return (
|
|
71
|
+
<>
|
|
72
|
+
<header>
|
|
73
|
+
<strong>{issue.subject || "(no subject)"}</strong>
|
|
74
|
+
<span data-newsletter-status={issue.status}><span aria-hidden="true">{statusMark(issue.status)} </span>{statusLabel(issue.status)}</span>
|
|
75
|
+
</header>
|
|
76
|
+
<p>{issue.listName ?? issue.listId}{issue.kind ? ` · ${issue.kind}` : ""}</p>
|
|
77
|
+
{issue.sendAt ? (
|
|
78
|
+
<p data-newsletter-send-at>
|
|
79
|
+
{issue.status === "sent" || issue.status === "sending" ? "Scheduled for " : "Send at "}
|
|
80
|
+
<time dateTime={issue.sendAt} title={formatUtc(issue.sendAt)}>{formatInZone(issue.sendAt, zone)}</time>
|
|
81
|
+
{issue.approvedBy ? ` · approved by ${issue.approvedBy}` : ""}
|
|
82
|
+
</p>
|
|
83
|
+
) : null}
|
|
84
|
+
<p data-newsletter-counts>{countsText(issue.counts ?? {})}</p>
|
|
85
|
+
{issue.status === "blocked" ? (
|
|
86
|
+
<p className="category-steering-alert" data-newsletter-blocked>
|
|
87
|
+
Blocked: {issue.note ?? "see the issue"}. Fix the cause, then approve it again.
|
|
88
|
+
</p>
|
|
89
|
+
) : issue.note ? <p data-newsletter-note>Note: {issue.note}</p> : null}
|
|
90
|
+
{issue.editedSinceApproval ? (
|
|
91
|
+
<p className="category-steering-alert" data-newsletter-edited>
|
|
92
|
+
Edited since approval. When it comes due it will go back to draft and send nothing. Approve it again to send this version.
|
|
93
|
+
</p>
|
|
94
|
+
) : null}
|
|
95
|
+
<p className="news-desk-newsletter__links">
|
|
96
|
+
{issue.itemId ? <a href={issueEditorHref(issue.itemId, demo)} data-newsletter-editor-link>Open in the editor</a> : <span>No item</span>}
|
|
97
|
+
{issue.archiveUrl ? <> · <a href={issue.archiveUrl} target="_blank" rel="noreferrer noopener" data-newsletter-archive-link>Archive page</a></> : null}
|
|
98
|
+
</p>
|
|
99
|
+
</>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function DeliveriesCard({
|
|
104
|
+
issue, rows, events, hasMore, loading, isAdmin, onMore,
|
|
105
|
+
}: {
|
|
106
|
+
issue: NewsletterIssue; rows: NewsletterDelivery[]; events: NewsletterEvent[]; hasMore: boolean; loading: boolean; isAdmin: boolean; onMore: () => void;
|
|
107
|
+
}) {
|
|
108
|
+
return (
|
|
109
|
+
<article className="news-desk-doctrine-card" data-newsletter-section="deliveries" data-newsletter-deliveries-for={issue.issueId}>
|
|
110
|
+
<div className="news-desk-doctrine-card__header">
|
|
111
|
+
<div><h3>Delivery history</h3><span>{issue.subject} · {countsText(issue.counts ?? {})}</span></div>
|
|
112
|
+
</div>
|
|
113
|
+
{!isAdmin ? <p className="news-desk-delegation-note">Addresses are masked for editors.</p> : null}
|
|
114
|
+
{rows.length ? (
|
|
115
|
+
<table className="news-desk-newsletter__table">
|
|
116
|
+
<thead><tr><th scope="col">Address</th><th scope="col">Result</th><th scope="col">Queued (UTC)</th><th scope="col">Last event (UTC)</th></tr></thead>
|
|
117
|
+
<tbody>
|
|
118
|
+
{rows.map((row, index) => (
|
|
119
|
+
<tr key={`${row.address}-${index}`} data-newsletter-delivery={row.status}>
|
|
120
|
+
<td>{row.address}</td>
|
|
121
|
+
<td>{row.status}{row.error ? ` (${row.error})` : ""}</td>
|
|
122
|
+
<td>{formatUtc(row.at)}</td>
|
|
123
|
+
<td>{row.eventAt ? formatUtc(row.eventAt) : "none yet"}</td>
|
|
124
|
+
</tr>
|
|
125
|
+
))}
|
|
126
|
+
</tbody>
|
|
127
|
+
</table>
|
|
128
|
+
) : <p>{loading ? "Loading deliveries..." : "Nothing has been sent for this issue."}</p>}
|
|
129
|
+
{hasMore ? <div className="news-desk-doctrine-card__actions"><button type="button" disabled={loading} onClick={onMore}>Load more deliveries</button></div> : null}
|
|
130
|
+
<h4>Recent events for this issue</h4>
|
|
131
|
+
{events.length ? (
|
|
132
|
+
<ul className="news-desk-monitoring__list" data-newsletter-events>
|
|
133
|
+
{events.map((event, index) => (
|
|
134
|
+
<li key={`${event.at}-${index}`}>
|
|
135
|
+
<strong>{event.type}{event.detail ? ` (${event.detail})` : ""}</strong>
|
|
136
|
+
<span>{event.list ?? ""}</span>
|
|
137
|
+
<span>{formatUtc(event.at)}</span>
|
|
138
|
+
</li>
|
|
139
|
+
))}
|
|
140
|
+
</ul>
|
|
141
|
+
) : <p>No recent events for this issue. Events are kept for 90 days and the panel shows the latest 100 for the whole site.</p>}
|
|
142
|
+
</article>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function SubscribersTable({ rows, zone }: { rows: NewsletterSubscriber[]; zone: string }) {
|
|
147
|
+
return (
|
|
148
|
+
<table className="news-desk-newsletter__table" data-newsletter-subscribers>
|
|
149
|
+
<thead><tr><th scope="col">Address</th><th scope="col">Status</th><th scope="col">Lists</th><th scope="col">Signed up</th><th scope="col">Confirmed</th></tr></thead>
|
|
150
|
+
<tbody>
|
|
151
|
+
{rows.map((row) => (
|
|
152
|
+
<tr key={row.email} data-newsletter-subscriber={row.status}>
|
|
153
|
+
<td>{row.email}</td>
|
|
154
|
+
<td>{row.status}</td>
|
|
155
|
+
<td>{row.lists.length ? row.lists.map((entry) => `${entry.id} (${entry.status})`).join(", ") : "none"}</td>
|
|
156
|
+
<td>{formatInZone(row.createdAt, zone)}</td>
|
|
157
|
+
<td>{row.confirmedAt ? formatInZone(row.confirmedAt, zone) : "not yet"}</td>
|
|
158
|
+
</tr>
|
|
159
|
+
))}
|
|
160
|
+
</tbody>
|
|
161
|
+
</table>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -6,11 +6,13 @@ import { SITE_BRAND } from "../lib/site-brand";
|
|
|
6
6
|
import type { NewsDeskShellState } from "../lib/news-desk-session";
|
|
7
7
|
import { cn } from "../lib/utils";
|
|
8
8
|
import { NewsroomOpsShell } from "./newsroom-ops-shell";
|
|
9
|
+
import { NewsroomOverviewSkeleton } from "./newsroom-overview-skeleton";
|
|
9
10
|
import { ReaderAuthControl } from "./reader-auth-control";
|
|
10
11
|
import { buttonVariants } from "./ui/button";
|
|
11
12
|
|
|
12
13
|
export function NewsDeskAccessGate({ shell, showSectionTabs = false }: { shell: NewsDeskShellState | null; showSectionTabs?: boolean }) {
|
|
13
14
|
const accessPhase = shell?.phase ?? "checkingAccess";
|
|
15
|
+
const pending = accessPhase === "checkingAccess" || accessPhase === "loadingDesk";
|
|
14
16
|
|
|
15
17
|
return (
|
|
16
18
|
<NewsroomOpsShell
|
|
@@ -21,9 +23,20 @@ export function NewsDeskAccessGate({ shell, showSectionTabs = false }: { shell:
|
|
|
21
23
|
headerActions={(
|
|
22
24
|
<Link className={cn(buttonVariants({ variant: "ghost", size: "sm" }))} href="/settings">Settings</Link>
|
|
23
25
|
)}
|
|
24
|
-
pageTitle="Newsroom"
|
|
26
|
+
pageTitle={showSectionTabs ? "Notification Center" : "Newsroom"}
|
|
25
27
|
showNavigation={showSectionTabs}
|
|
26
28
|
>
|
|
29
|
+
{pending ? (showSectionTabs ? <NewsroomOverviewSkeleton /> : (
|
|
30
|
+
<div aria-label="Loading newsroom" className="space-y-4" data-newsroom-page-skeleton role="status">
|
|
31
|
+
<div className="h-6 w-48 rounded bg-muted" />
|
|
32
|
+
<div className="h-4 w-3/4 rounded bg-muted" />
|
|
33
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
34
|
+
{Array.from({ length: 4 }, (_, index) => <div className="h-32 rounded-xl border bg-card p-4" key={index}>
|
|
35
|
+
<div className="h-4 w-2/3 rounded bg-muted" /><div className="mt-4 h-3 w-1/2 rounded bg-muted" />
|
|
36
|
+
</div>)}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
)) : (
|
|
27
40
|
<section aria-live="polite" data-news-desk-access={accessPhase} data-news-desk-access-phase={accessPhase}>
|
|
28
41
|
<div className="space-y-3 rounded-xl border border-border bg-card p-6">
|
|
29
42
|
<p className="m-0 text-[0.7rem] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Access</p>
|
|
@@ -38,12 +51,13 @@ export function NewsDeskAccessGate({ shell, showSectionTabs = false }: { shell:
|
|
|
38
51
|
) : null}
|
|
39
52
|
</div>
|
|
40
53
|
</section>
|
|
54
|
+
)}
|
|
41
55
|
</NewsroomOpsShell>
|
|
42
56
|
);
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
function formatAccessTitle(state: NewsDeskShellState | null): string {
|
|
46
|
-
if (!state || state.phase === "checkingAccess") return "Checking
|
|
60
|
+
if (!state || state.phase === "checkingAccess") return "Checking Credentials";
|
|
47
61
|
if (state.phase === "loadingDesk") return "Loading Newsroom Records";
|
|
48
62
|
if (state.phase === "forbidden") return "Editor Role Required";
|
|
49
63
|
if (state.phase === "error") return "Newsroom Unavailable";
|
|
@@ -48,11 +48,9 @@ import {
|
|
|
48
48
|
import { getNewsroomNavHref } from "../lib/newsroom-nav";
|
|
49
49
|
import { SITE_BRAND } from "../lib/site-brand";
|
|
50
50
|
import { cn } from "../lib/utils";
|
|
51
|
-
import
|
|
52
|
-
import { applyReplacement, editorialChecksEnabled, inlineFindings, visibleFindings } from "../lib/editorial-checks";
|
|
51
|
+
import { applyReplacement, inlineFindings, visibleFindings } from "../lib/editorial-checks";
|
|
53
52
|
import { graphqlEditorialBackend } from "../lib/editorial-checks-client";
|
|
54
53
|
import { demoEditorialBackend } from "../lib/editorial-checks-demo";
|
|
55
|
-
import type { PluginOutputs } from "../lib/plugin-settings";
|
|
56
54
|
import type { FindingActions } from "./editorial-finding-card";
|
|
57
55
|
import { EditorialFindingsPanel } from "./editorial-findings-panel";
|
|
58
56
|
import { EditorialPreviewPopover, type PreviewPopoverTarget } from "./editorial-preview-popover";
|
|
@@ -219,7 +217,7 @@ export function NewsroomArticleEditor({ articleId, demo, embedded = false }: { a
|
|
|
219
217
|
const frontMatterYaml = editor?.frontMatterYaml ?? null;
|
|
220
218
|
const bodyMarkus = editor?.bodyMarkus ?? null;
|
|
221
219
|
const editorialBackend = useMemo(
|
|
222
|
-
() => (demo ? demoEditorialBackend : !previewMode
|
|
220
|
+
() => (demo ? demoEditorialBackend : !previewMode ? graphqlEditorialBackend : null),
|
|
223
221
|
[demo, previewMode],
|
|
224
222
|
);
|
|
225
223
|
const editorial = useEditorialChecks({
|
|
@@ -515,8 +513,10 @@ export function NewsroomArticleEditor({ articleId, demo, embedded = false }: { a
|
|
|
515
513
|
}
|
|
516
514
|
if (!editor) {
|
|
517
515
|
return (
|
|
518
|
-
<div
|
|
519
|
-
<
|
|
516
|
+
<div aria-label="Loading article" className="space-y-5" role="status">
|
|
517
|
+
<div className="h-8 w-2/3 rounded bg-muted" />
|
|
518
|
+
<div className="grid gap-4 sm:grid-cols-2"><div className="h-12 rounded bg-muted" /><div className="h-12 rounded bg-muted" /></div>
|
|
519
|
+
<div className="h-64 rounded-xl border bg-card p-4"><div className="h-4 w-5/6 rounded bg-muted" /><div className="mt-3 h-4 w-3/4 rounded bg-muted" /></div>
|
|
520
520
|
</div>
|
|
521
521
|
);
|
|
522
522
|
}
|
|
@@ -670,21 +670,21 @@ export function NewsroomArticleEditor({ articleId, demo, embedded = false }: { a
|
|
|
670
670
|
</Select>
|
|
671
671
|
</label>
|
|
672
672
|
<label className="space-y-1 text-sm font-medium">
|
|
673
|
-
<span>
|
|
673
|
+
<span>Section</span>
|
|
674
674
|
<Select
|
|
675
675
|
onValueChange={(value) => {
|
|
676
676
|
if (typeof value === "string") patchEditor({ section: value === NO_DESK_VALUE ? "" : value });
|
|
677
677
|
}}
|
|
678
678
|
value={deskOptions.some(option => option.value === editor.section) ? editor.section : NO_DESK_VALUE}
|
|
679
679
|
>
|
|
680
|
-
<SelectTrigger aria-label="
|
|
681
|
-
<SelectValue>{deskOptions.find(option => option.value === editor.section)?.label ?? "No
|
|
680
|
+
<SelectTrigger aria-label="Section" className="w-full" data-newsroom-article-section>
|
|
681
|
+
<SelectValue>{deskOptions.find(option => option.value === editor.section)?.label ?? "No section"}</SelectValue>
|
|
682
682
|
</SelectTrigger>
|
|
683
683
|
<SelectContent>
|
|
684
|
-
<SelectItem value={NO_DESK_VALUE}>No
|
|
684
|
+
<SelectItem value={NO_DESK_VALUE}>No section</SelectItem>
|
|
685
685
|
{(["core", "rotating"] as const).map((kind) => deskOptions.some(option => option.kind === kind) ? (
|
|
686
686
|
<SelectGroup key={kind}>
|
|
687
|
-
<SelectLabel>{kind === "core" ? "Core
|
|
687
|
+
<SelectLabel>{kind === "core" ? "Core sections" : "Rotating sections"}</SelectLabel>
|
|
688
688
|
{deskOptions.filter(option => option.kind === kind).map((option) => (
|
|
689
689
|
<SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>
|
|
690
690
|
))}
|
|
@@ -5,7 +5,6 @@ import Link from "next/link";
|
|
|
5
5
|
import { useEffect, useMemo, useState } from "react";
|
|
6
6
|
import { Badge } from "./ui/badge";
|
|
7
7
|
import { buttonVariants } from "./ui/button";
|
|
8
|
-
import { Spinner } from "./ui/spinner";
|
|
9
8
|
import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
|
|
10
9
|
import { SITE_BRAND } from "../lib/site-brand";
|
|
11
10
|
import { getNewsroomNavHref } from "../lib/newsroom-nav";
|
|
@@ -89,15 +88,16 @@ export function NewsroomArticlesView({ demo }: { demo: boolean }) {
|
|
|
89
88
|
</TabsList>
|
|
90
89
|
</Tabs>
|
|
91
90
|
{loadError ? <NewsroomOpsStatusBanner tone="error">{loadError}</NewsroomOpsStatusBanner> : null}
|
|
92
|
-
{rows === null && !loadError ? (
|
|
93
|
-
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
94
|
-
<Spinner /> Loading articles…
|
|
95
|
-
</div>
|
|
96
|
-
) : null}
|
|
97
91
|
{rows !== null && visibleRows.length === 0 ? (
|
|
98
92
|
<p className="text-sm text-muted-foreground">Nothing here yet.</p>
|
|
99
93
|
) : null}
|
|
100
94
|
<ul className="divide-y divide-border rounded-xl border border-border bg-card" data-newsroom-articles-list>
|
|
95
|
+
{rows === null && !loadError ? Array.from({ length: 5 }, (_, index) => (
|
|
96
|
+
<li aria-hidden="true" className="flex items-center justify-between px-4 py-3" key={index}>
|
|
97
|
+
<span className="w-2/3"><span className="block h-4 w-3/4 rounded bg-muted" /><span className="mt-2 block h-3 w-1/2 rounded bg-muted" /></span>
|
|
98
|
+
<span className="h-5 w-16 rounded bg-muted" />
|
|
99
|
+
</li>
|
|
100
|
+
)) : null}
|
|
101
101
|
{visibleRows.map((row) => (
|
|
102
102
|
<li key={row.id}>
|
|
103
103
|
<Link
|
|
@@ -36,7 +36,7 @@ export function NewsroomAssignmentDetail({ assignment, deskOptions = [], onDeskC
|
|
|
36
36
|
const changeDesk = (value: string) => {
|
|
37
37
|
if (!onDeskChange) return;
|
|
38
38
|
setSavingDesk(true); setDeskError(null);
|
|
39
|
-
onDeskChange(value === NO_DESK_VALUE ? null : value).catch((reason: unknown) => setDeskError(reason instanceof Error ? reason.message : "Could not save the
|
|
39
|
+
onDeskChange(value === NO_DESK_VALUE ? null : value).catch((reason: unknown) => setDeskError(reason instanceof Error ? reason.message : "Could not save the section.")).finally(() => setSavingDesk(false));
|
|
40
40
|
};
|
|
41
41
|
const facts: DetailField[] = [
|
|
42
42
|
{ label: "Status", value: assignment.status },
|
|
@@ -58,14 +58,14 @@ export function NewsroomAssignmentDetail({ assignment, deskOptions = [], onDeskC
|
|
|
58
58
|
{facts.map(field => <div key={field.label}><dt>{field.label}</dt><dd>{field.value}</dd></div>)}
|
|
59
59
|
</dl>
|
|
60
60
|
{onDeskChange && <label className="newsroom-assignment-detail-desk">
|
|
61
|
-
<span>
|
|
61
|
+
<span>Section</span>
|
|
62
62
|
<Select onValueChange={(value) => { if (typeof value === "string") changeDesk(value); }} value={deskChoices.some(option => option.value === currentDesk) ? currentDesk : NO_DESK_VALUE} disabled={savingDesk}>
|
|
63
|
-
<SelectTrigger aria-label="
|
|
64
|
-
<SelectValue>{deskChoices.find(option => option.value === currentDesk)?.label ?? "No
|
|
63
|
+
<SelectTrigger aria-label="Section" className="w-full" data-newsroom-assignment-desk>
|
|
64
|
+
<SelectValue>{deskChoices.find(option => option.value === currentDesk)?.label ?? "No section"}</SelectValue>
|
|
65
65
|
</SelectTrigger>
|
|
66
66
|
<SelectContent>
|
|
67
|
-
<SelectItem value={NO_DESK_VALUE}>No
|
|
68
|
-
{(["core", "rotating"] as const).map(kind => deskChoices.some(option => option.kind === kind) ? <SelectGroup key={kind}><SelectLabel>{kind === "core" ? "Core
|
|
67
|
+
<SelectItem value={NO_DESK_VALUE}>No section</SelectItem>
|
|
68
|
+
{(["core", "rotating"] as const).map(kind => deskChoices.some(option => option.kind === kind) ? <SelectGroup key={kind}><SelectLabel>{kind === "core" ? "Core sections" : "Rotating sections"}</SelectLabel>{deskChoices.filter(option => option.kind === kind).map(option => <SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>)}</SelectGroup> : null)}
|
|
69
69
|
</SelectContent>
|
|
70
70
|
</Select>
|
|
71
71
|
{currentDesk && !deskChoices.some(option => option.value === currentDesk) ? <small className="newsroom-desk-legacy-value">Old section value: {currentDesk}</small> : null}
|
|
@@ -160,7 +160,7 @@ export function NewsroomBoardView({ subject, deskId }: { subject: NewsroomBoardS
|
|
|
160
160
|
if (!allowed) return <NewsDeskAccessGate shell={session?.shell ?? null} />;
|
|
161
161
|
return <NewsroomOpsShell activeTab={subject === "contentItem" ? "articles" : subject === "desk" ? "desks" : "assignments"} appTitle={SITE_BRAND.appTitle} backLabel={SITE_BRAND.backToHomeLabel} pageTitle={pageTitle}
|
|
162
162
|
headerActions={<>
|
|
163
|
-
<span role="status" className="organizer-board-note">{
|
|
163
|
+
{!loading ? <span role="status" className="organizer-board-note">{issues.length} records loaded.</span> : null}
|
|
164
164
|
<div role="group" aria-label="Record presentation" className="organizer-board-view-toggle"><button type="button" className="organizer-header-button" aria-pressed={view === "board"} onClick={() => setView("board")}>Board</button><button type="button" className="organizer-header-button" aria-pressed={view === "list"} onClick={() => setView("list")}>List</button></div>
|
|
165
165
|
{showsContentItems && (
|
|
166
166
|
<Link className="organizer-header-button" data-newsroom-article-new href={getNewsroomNavHref(newsroomHref("articles", "new"))}>
|
|
@@ -174,11 +174,16 @@ export function NewsroomBoardView({ subject, deskId }: { subject: NewsroomBoardS
|
|
|
174
174
|
{error && <NewsroomOpsStatusBanner tone="error">{error}</NewsroomOpsStatusBanner>}
|
|
175
175
|
{subject === "desk" && <IssueTypeFilter config={config} value={typeFilter} onChange={setTypeFilter} name="Record type" className="organizer-board-type-filter" />}
|
|
176
176
|
<div className="organizer-board-body" data-detail-open={Boolean(issue)} ref={boardBodyRef} onScrollCapture={handleBoardScroll}>
|
|
177
|
-
|
|
177
|
+
{loading && !issues.length ? <div aria-label="Loading board records" className="grid w-full grid-cols-1 gap-3 sm:grid-cols-3" role="status">
|
|
178
|
+
{Array.from({ length: 3 }, (_, column) => <div className="min-h-64 space-y-3 rounded-xl border bg-card p-4" key={column}>
|
|
179
|
+
<div className="h-4 w-1/2 rounded bg-muted" />
|
|
180
|
+
{Array.from({ length: 2 }, (_, row) => <div className="h-24 rounded-lg border bg-background p-3" key={row}><div className="h-4 w-3/4 rounded bg-muted" /><div className="mt-3 h-3 w-1/2 rounded bg-muted" /></div>)}
|
|
181
|
+
</div>)}
|
|
182
|
+
</div> : <IssueCollection issues={issues} config={config} query={{ type: subject === "desk" ? issueTypeFromFilter(typeFilter) : boardType }} view={view}
|
|
178
183
|
motion={lowMotion ? STATIC_MOTION : undefined}
|
|
179
184
|
collapsedColumns={columnState.collapsedColumns} onToggleCollapse={columnState.toggleColumn} initialScroll="start"
|
|
180
185
|
selectedIssueId={selected} detailOpen={Boolean(issue)} onSelectIssue={item => setSelected(item.id)} ariaLabel={pageTitle}
|
|
181
|
-
emptyMessage={
|
|
186
|
+
emptyMessage={error ? "Records could not be loaded." : "No records in this selection."} />}
|
|
182
187
|
{issue && <IssueDetailPanel issue={issue} onClose={() => setSelected(null)} fields={[]}>
|
|
183
188
|
{issue.kind === "contentItem"
|
|
184
189
|
? <>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {useEffect,useState,type ReactNode} from 'react';
|
|
4
|
+
import {loadProspectusAccountState} from '../lib/prospectus-account-client';
|
|
5
|
+
import type {CurationBootstrap} from '../lib/curation-client';
|
|
6
|
+
import {NewsroomCurationProvider} from './newsroom-curation-provider';
|
|
7
|
+
|
|
8
|
+
type Fetcher=(input:string,init?:RequestInit)=>Promise<Response>;
|
|
9
|
+
const CONTEXT_PATH='/api/newsroom/curation/context';
|
|
10
|
+
|
|
11
|
+
function validBootstrap(value:unknown):value is CurationBootstrap{
|
|
12
|
+
const body=value as Partial<CurationBootstrap>|null;
|
|
13
|
+
const scope=body?.scope;
|
|
14
|
+
return !!scope&&[scope.accountId,scope.publicationId,scope.bindingRevision].every(item=>typeof item==='string'&&item.trim()!=='')
|
|
15
|
+
&&(body!.mode==='embedded'||body!.mode==='remote')&&Array.isArray(body!.capabilities);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Ask the server for the curation bootstrap, or null. Only a signed-in person with a selected
|
|
20
|
+
* account causes a context read; every other outcome (no Prospectus, signed out, no selection,
|
|
21
|
+
* any refusal or failure) is null, so the released CMS is unchanged.
|
|
22
|
+
*/
|
|
23
|
+
export async function loadCurationBootstrap(returnPath:string,fetcher:Fetcher=fetch):Promise<CurationBootstrap|null>{
|
|
24
|
+
try{
|
|
25
|
+
const account=await loadProspectusAccountState(returnPath,fetcher);
|
|
26
|
+
if(account.status!=='signedIn'||!account.selectedAccountId)return null;
|
|
27
|
+
const response=await fetcher(CONTEXT_PATH,{credentials:'same-origin',cache:'no-store'});
|
|
28
|
+
if(!response.ok)return null;
|
|
29
|
+
const body=await response.json().catch(()=>null);
|
|
30
|
+
if(!validBootstrap(body)||body.scope.accountId!==account.selectedAccountId)return null;
|
|
31
|
+
return body;
|
|
32
|
+
}catch{return null;}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Mounts the curation provider for the References tab. Fails closed to no provider content. */
|
|
36
|
+
export function NewsroomCurationBootstrap({enabled=true,children}:{enabled?:boolean;children:ReactNode}){
|
|
37
|
+
const [bootstrap,setBootstrap]=useState<CurationBootstrap|null>(null);
|
|
38
|
+
useEffect(()=>{
|
|
39
|
+
if(!enabled)return;
|
|
40
|
+
let current=true;
|
|
41
|
+
void loadCurationBootstrap(window.location.pathname).then(value=>{if(current)setBootstrap(value);});
|
|
42
|
+
return()=>{current=false;setBootstrap(null);};
|
|
43
|
+
},[enabled]);
|
|
44
|
+
return <NewsroomCurationProvider bootstrap={bootstrap}>{children}</NewsroomCurationProvider>;
|
|
45
|
+
}
|
|
@@ -1,24 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
BookOpenIcon,
|
|
5
|
-
ClipboardListIcon,
|
|
6
|
-
FileTextIcon,
|
|
7
|
-
LayersIcon,
|
|
8
|
-
MessageSquareIcon,
|
|
9
|
-
NetworkIcon,
|
|
10
|
-
SearchIcon,
|
|
11
|
-
SettingsIcon,
|
|
12
|
-
} from "lucide-react";
|
|
13
3
|
import Link from "next/link";
|
|
14
4
|
import { usePathname } from "next/navigation";
|
|
15
5
|
import { useMemo } from "react";
|
|
16
6
|
import { Badge } from "./ui/badge";
|
|
17
7
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
|
|
18
|
-
import type { AssignmentRecord,
|
|
19
|
-
import { getNewsroomNavHref
|
|
8
|
+
import type { AssignmentRecord, NewsroomSectionRecord, ReferenceRecord } from "../lib/category-repository";
|
|
9
|
+
import { getNewsroomNavHref } from "../lib/newsroom-nav";
|
|
20
10
|
import {
|
|
21
|
-
countReferencesByStatus,
|
|
22
11
|
filterReferencesByStatus,
|
|
23
12
|
formatReferenceListDate,
|
|
24
13
|
referenceDisplayTitle,
|
|
@@ -33,57 +22,33 @@ import {
|
|
|
33
22
|
sortNewsroomSections,
|
|
34
23
|
} from "../lib/newsroom-sections";
|
|
35
24
|
import { newsroomInboxRowClassName } from "../lib/newsroom-list-selection";
|
|
25
|
+
import type { RelevanceDecisionView } from "../lib/relevance-decisions";
|
|
26
|
+
import { NotificationCenter } from "./notification-center";
|
|
36
27
|
|
|
37
28
|
type NewsroomOpsOverviewProps = {
|
|
38
29
|
assignments: AssignmentRecord[];
|
|
30
|
+
actorLabel?: string;
|
|
31
|
+
decisions?: ReadonlyMap<string, RelevanceDecisionView>;
|
|
39
32
|
demo?: boolean;
|
|
40
|
-
|
|
33
|
+
humanCuratedReferenceLineageIds?: ReadonlySet<string>;
|
|
41
34
|
newsroomSections: NewsroomSectionRecord[];
|
|
42
35
|
references: ReferenceRecord[];
|
|
43
36
|
};
|
|
44
37
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const DESTINATION_ICONS: Record<DestinationKey, typeof BookOpenIcon> = {
|
|
48
|
-
articles: FileTextIcon,
|
|
49
|
-
assignments: ClipboardListIcon,
|
|
50
|
-
references: BookOpenIcon,
|
|
51
|
-
messages: MessageSquareIcon,
|
|
52
|
-
topics: LayersIcon,
|
|
53
|
-
concepts: NetworkIcon,
|
|
54
|
-
administration: SettingsIcon,
|
|
55
|
-
search: SearchIcon,
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const DESTINATIONS = NEWSROOM_OPS_NAV.filter((item) => item.id !== "overview").map((item) => ({
|
|
59
|
-
key: item.id as DestinationKey,
|
|
60
|
-
label: item.label,
|
|
61
|
-
detail: item.detail,
|
|
62
|
-
href: item.href,
|
|
63
|
-
icon: DESTINATION_ICONS[item.id as DestinationKey],
|
|
64
|
-
}));
|
|
65
|
-
|
|
66
|
-
function openAssignmentCount(assignments: AssignmentRecord[]): number {
|
|
67
|
-
return assignments.filter((assignment) => {
|
|
68
|
-
const status = assignment.status?.trim().toLowerCase() ?? "";
|
|
69
|
-
return status === "open" || status === "claimed" || status === "pending";
|
|
70
|
-
}).length;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function recentMessageCount(messages: MessageRecord[]): number {
|
|
74
|
-
return messages.filter((message) => message.status !== "archived").length;
|
|
75
|
-
}
|
|
38
|
+
const NO_DECISIONS: ReadonlyMap<string, RelevanceDecisionView> = new Map();
|
|
39
|
+
const NO_IDS: ReadonlySet<string> = new Set();
|
|
76
40
|
|
|
77
41
|
export function NewsroomOpsOverview({
|
|
42
|
+
actorLabel = "",
|
|
78
43
|
assignments,
|
|
44
|
+
decisions = NO_DECISIONS,
|
|
79
45
|
demo = false,
|
|
80
|
-
|
|
46
|
+
humanCuratedReferenceLineageIds = NO_IDS,
|
|
81
47
|
newsroomSections,
|
|
82
48
|
references,
|
|
83
49
|
}: NewsroomOpsOverviewProps) {
|
|
84
50
|
const pathname = usePathname();
|
|
85
51
|
const canonicalReferences = useMemo(() => selectCanonicalReferenceRecords(references), [references]);
|
|
86
|
-
const referenceCounts = useMemo(() => countReferencesByStatus(canonicalReferences), [canonicalReferences]);
|
|
87
52
|
const pendingReferences = useMemo(
|
|
88
53
|
() => filterReferencesByStatus(canonicalReferences, "pending").slice(0, 5),
|
|
89
54
|
[canonicalReferences],
|
|
@@ -104,12 +69,6 @@ export function NewsroomOpsOverview({
|
|
|
104
69
|
const coreDesks = useMemo(() => desks.filter(desk => newsroomDeskKind(desk) === "core"), [desks]);
|
|
105
70
|
const rotatingDesks = useMemo(() => desks.filter(desk => newsroomDeskKind(desk) === "rotating"), [desks]);
|
|
106
71
|
|
|
107
|
-
const destinationCounts: Partial<Record<DestinationKey, number | null>> = {
|
|
108
|
-
references: referenceCounts.pending ?? 0,
|
|
109
|
-
assignments: openAssignmentCount(assignments),
|
|
110
|
-
messages: recentMessageCount(messages),
|
|
111
|
-
};
|
|
112
|
-
|
|
113
72
|
return (
|
|
114
73
|
<div
|
|
115
74
|
className="space-y-8 font-sans"
|
|
@@ -117,44 +76,15 @@ export function NewsroomOpsOverview({
|
|
|
117
76
|
data-newsroom-ops-home="true"
|
|
118
77
|
>
|
|
119
78
|
<div className="space-y-8" data-newsroom-overview-feeds>
|
|
120
|
-
<
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
{
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
<Link
|
|
130
|
-
className="group block rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
131
|
-
data-newsroom-ops-destination={key}
|
|
132
|
-
data-newsroom-overview-section={key}
|
|
133
|
-
href={href}
|
|
134
|
-
key={key}
|
|
135
|
-
>
|
|
136
|
-
<Card className="h-full transition-colors group-hover:border-primary/40">
|
|
137
|
-
<CardHeader className="space-y-3 p-4">
|
|
138
|
-
<div className="flex items-center justify-between gap-2">
|
|
139
|
-
<span className="flex size-9 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
140
|
-
<Icon className="size-4" aria-hidden="true" />
|
|
141
|
-
</span>
|
|
142
|
-
{count != null && count > 0 ? (
|
|
143
|
-
<Badge variant="default">{count}</Badge>
|
|
144
|
-
) : null}
|
|
145
|
-
</div>
|
|
146
|
-
<div className="space-y-0.5">
|
|
147
|
-
<CardTitle className="text-base font-semibold">{meta.label}</CardTitle>
|
|
148
|
-
<CardDescription>{meta.detail}</CardDescription>
|
|
149
|
-
</div>
|
|
150
|
-
</CardHeader>
|
|
151
|
-
</Card>
|
|
152
|
-
</Link>
|
|
153
|
-
);
|
|
154
|
-
})}
|
|
155
|
-
</div>
|
|
156
|
-
</section>
|
|
157
|
-
|
|
79
|
+
<div aria-label="Notification Center feed" className="newsroom-overview-feed-frame rounded-xl border bg-card p-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" data-newsroom-overview-notifications role="region" tabIndex={0}>
|
|
80
|
+
<NotificationCenter
|
|
81
|
+
actorLabel={actorLabel}
|
|
82
|
+
decisions={decisions}
|
|
83
|
+
demo={demo}
|
|
84
|
+
humanCuratedReferenceLineageIds={humanCuratedReferenceLineageIds}
|
|
85
|
+
references={canonicalReferences}
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
158
88
|
<section aria-label="Needs attention" className="space-y-3">
|
|
159
89
|
<h2 className="text-sm font-medium text-muted-foreground">Needs attention</h2>
|
|
160
90
|
{pendingReferences.length || attentionAssignments.length ? (
|
|
@@ -208,16 +138,16 @@ export function NewsroomOpsOverview({
|
|
|
208
138
|
) : (
|
|
209
139
|
<Card>
|
|
210
140
|
<CardContent className="p-4 text-sm text-muted-foreground">
|
|
211
|
-
|
|
141
|
+
No pending references or open assignments. Check those destinations for other work.
|
|
212
142
|
</CardContent>
|
|
213
143
|
</Card>
|
|
214
144
|
)}
|
|
215
145
|
</section>
|
|
216
146
|
|
|
217
147
|
{desks.length ? (
|
|
218
|
-
<section aria-label="
|
|
219
|
-
<h2 className="text-sm font-medium text-muted-foreground">
|
|
220
|
-
{coreDesks.length ? <h3 className="newsroom-desk-group-heading" data-newsroom-ops-desk-group="core">Core
|
|
148
|
+
<section aria-label="Sections" className="space-y-3" data-newsroom-ops-desks>
|
|
149
|
+
<h2 className="text-sm font-medium text-muted-foreground">Sections</h2>
|
|
150
|
+
{coreDesks.length ? <h3 className="newsroom-desk-group-heading" data-newsroom-ops-desk-group="core">Core sections</h3> : null}
|
|
221
151
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
222
152
|
{coreDesks.map((section) => (
|
|
223
153
|
<Link
|
|
@@ -237,7 +167,7 @@ export function NewsroomOpsOverview({
|
|
|
237
167
|
</Link>
|
|
238
168
|
))}
|
|
239
169
|
</div>
|
|
240
|
-
{rotatingDesks.length ? <h3 className="newsroom-desk-group-heading" data-newsroom-ops-desk-group="rotating">Rotating
|
|
170
|
+
{rotatingDesks.length ? <h3 className="newsroom-desk-group-heading" data-newsroom-ops-desk-group="rotating">Rotating sections <small>appear for a while, then rotate out</small></h3> : null}
|
|
241
171
|
{rotatingDesks.length ? <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
242
172
|
{rotatingDesks.map((section) => (
|
|
243
173
|
<Link
|
|
@@ -8,7 +8,7 @@ import { cn } from "../lib/utils";
|
|
|
8
8
|
import { useEffect, useId, useState, type ReactNode } from "react";
|
|
9
9
|
import Link from "next/link";
|
|
10
10
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
11
|
-
import { BookOpen, ClipboardList, Menu, Search, MessagesSquare, PanelLeftClose, PanelLeftOpen, Settings, UserRound, LogIn, LogOut } from "lucide-react";
|
|
11
|
+
import { BookOpen, ClipboardList, House, Menu, Search, MessagesSquare, PanelLeftClose, PanelLeftOpen, Settings, UserRound, LogIn, LogOut } from "lucide-react";
|
|
12
12
|
import { ORGANIZER_NAV_GROUPS, ORGANIZER_PRIMARY_NAV, ORGANIZER_SETTINGS_DESTINATION } from "../lib/newsroom-organizer-nav";
|
|
13
13
|
import { browserPathToInternalNewsroomPath } from "../lib/newsroom-base-path";
|
|
14
14
|
import { getNewsroomNavHref } from "../lib/newsroom-nav";
|
|
@@ -55,7 +55,7 @@ function OrganizerSearch({ demo }: { demo: boolean }) {
|
|
|
55
55
|
</form>;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
const ICONS = { articles: ClipboardList, references: BookOpen, messages: MessagesSquare };
|
|
58
|
+
const ICONS = { overview: House, articles: ClipboardList, references: BookOpen, messages: MessagesSquare };
|
|
59
59
|
|
|
60
60
|
/** Navigation is a presentation layer; existing route and server authorization still own access. */
|
|
61
61
|
function Navigation({ activePath, demo, admin, surface, onNavigate }: { activePath: string; demo: boolean; admin: boolean; surface: string; onNavigate?: () => void }) {
|