@anthusai/papyrus 1.0.0-next.9 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/amplify/auth/prospectus-google-client.js +66 -0
- package/amplify/auth/prospectus-google-client.ts +71 -0
- package/amplify/auth/redirect-urls.js +54 -0
- package/amplify/auth/redirect-urls.ts +66 -0
- package/amplify/auth/resource.js +23 -5
- package/amplify/auth/resource.ts +31 -5
- package/amplify/data/resource.js +42 -80
- package/amplify/data/resource.ts +42 -80
- package/amplify/data/schema.js +45 -62
- package/amplify/data/schema.ts +47 -62
- package/amplify/functions/category-action/handler.ts +5 -3
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
- package/amplify/functions/console-chat-responder/resource.js +0 -7
- package/amplify/functions/console-chat-responder/resource.ts +0 -8
- package/amplify/functions/console-chat-responder/src/main.rs +0 -9
- package/amplify/functions/content-actions/handler.py +143 -1
- package/amplify/functions/content-actions/requirements.txt +2 -1
- package/amplify/functions/content-actions/resource.js +6 -1
- package/amplify/functions/content-actions/resource.ts +9 -2
- package/amplify/functions/email-submission-processor/handler.py +3 -1
- package/amplify/functions/renditions/handler.py +120 -0
- package/amplify/functions/renditions/requirements.txt +4 -0
- package/amplify/functions/ses-inbound-receive/handler.ts +32 -4
- package/amplify/functions/shared/email-submission.ts +109 -1
- package/amplify/functions/shared/python-bundle.js +42 -2
- package/amplify/functions/shared/python-bundle.ts +47 -2
- package/amplify/functions/shared/reference-curation-metadata.ts +29 -0
- package/amplify/inbound-email/ses-intake.js +86 -0
- package/amplify/inbound-email/ses-intake.ts +110 -0
- package/amplify/plugins/registry.js +19 -0
- package/amplify/plugins/registry.ts +23 -0
- package/amplify/plugins/signup/function/handler.py +200 -0
- package/amplify/plugins/signup/function/resource.js +27 -0
- package/amplify/plugins/signup/function/resource.ts +35 -0
- package/amplify/plugins/signup/papyrus.plugin.js +59 -0
- package/amplify/plugins/signup/papyrus.plugin.ts +74 -0
- package/amplify/plugins/signup/schema.js +40 -0
- package/amplify/plugins/signup/schema.ts +41 -0
- package/amplify/plugins/signup/waf.js +48 -0
- package/amplify/plugins/signup/waf.ts +51 -0
- package/amplify/plugins/types.js +1 -0
- package/amplify/plugins/types.ts +39 -0
- package/amplify/plugins/wire.js +24 -0
- package/amplify/plugins/wire.ts +26 -0
- package/amplify/publication/default-publication.js +18 -0
- package/amplify/publication/default-publication.ts +20 -0
- package/amplify/publication/invalidation-alarm.js +26 -0
- package/amplify/publication/invalidation-alarm.ts +30 -0
- package/amplify/publication/redirect-and-index.js +37 -0
- package/amplify/publication/renditions-function.js +74 -0
- package/amplify/publication/renditions-function.ts +112 -0
- package/amplify/publication/renditions-settings.js +51 -0
- package/amplify/publication/renditions-settings.ts +58 -0
- package/amplify/publication/reproject-on-deploy.js +73 -0
- package/amplify/publication/reproject-on-deploy.ts +78 -0
- package/amplify/publication/s3-projection-settings.js +67 -0
- package/amplify/publication/s3-projection-settings.ts +101 -0
- package/amplify/publication/s3-projection.js +151 -0
- package/amplify/publication/s3-projection.ts +201 -0
- package/amplify/resolver-waves.js +26 -0
- package/amplify/resolver-waves.ts +34 -0
- package/amplify/revalidate-invalidation-settings.js +30 -0
- package/amplify/revalidate-invalidation-settings.ts +35 -0
- package/amplify/revalidate-secret-policy.js +13 -0
- package/amplify/revalidate-secret-policy.ts +19 -0
- package/amplify/site-backend-config.js +65 -1
- package/amplify/site-backend-config.ts +153 -2
- package/amplify/site-backend-names.js +103 -0
- package/amplify/site-backend-names.ts +166 -0
- package/amplify/site-backend.js +150 -176
- package/amplify/site-backend.ts +189 -201
- package/amplify/storage-backups/construct.js +31 -0
- package/amplify/storage-backups/construct.ts +47 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +8 -1
- package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +6 -1
- package/app/[year]/[month]/[day]/page.tsx +1 -1
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +6 -1
- package/app/api/archive/editions/route.ts +1 -1
- package/app/api/auth/prospectus/callback/route.ts +9 -0
- package/app/api/auth/prospectus/selection/route.ts +9 -0
- package/app/api/auth/prospectus/session/route.ts +9 -0
- package/app/api/auth/prospectus/sign-in/route.ts +9 -0
- package/app/api/auth/prospectus/sign-out/route.ts +9 -0
- package/app/api/cyclotron/priming-proof/route.ts +11 -0
- package/app/api/media/[...path]/route.ts +5 -16
- package/app/api/revalidate/route.ts +15 -7
- package/app/archive/page.tsx +1 -1
- package/app/articles/[slug]/page.tsx +7 -5
- package/app/blog-rhythm.css +278 -0
- package/app/cyclotron/page.tsx +13 -0
- package/app/globals.css +2 -0
- package/app/layout.tsx +16 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +9 -6
- package/app/newsroom/[section]/page.tsx +6 -3
- package/app/newsroom/layout.tsx +4 -9
- package/app/newsroom/settings/page.tsx +6 -0
- package/app/newsroom/work/page.tsx +10 -0
- package/app/page.tsx +22 -8
- package/app/robots.ts +2 -5
- package/app/tailwind.css +1 -6
- package/components/amplify-client-provider.tsx +5 -22
- package/components/archive-shell.tsx +2 -22
- package/components/article-video.tsx +152 -5
- package/components/blog-page-background.tsx +23 -6
- package/components/news-desk-page.tsx +4 -15
- package/components/news-desk-taxonomy-client.ts +90 -3
- package/components/newspaper.tsx +6 -2
- package/components/newsroom-access-gate.tsx +2 -1
- package/components/newsroom-board-view.tsx +99 -0
- package/components/newsroom-client-shell.tsx +12 -0
- package/components/newsroom-curation-provider.tsx +17 -0
- package/components/newsroom-fonts/playfair-display-700.ttf +0 -0
- package/components/newsroom-fonts/playfair-display-900.ttf +0 -0
- package/components/newsroom-fonts/playfair-display-OFL.txt +93 -0
- package/components/newsroom-fonts/plus-jakarta-sans-400.ttf +0 -0
- package/components/newsroom-fonts/plus-jakarta-sans-600.ttf +0 -0
- package/components/newsroom-fonts/plus-jakarta-sans-OFL.txt +93 -0
- package/components/newsroom-newsprint-board.css +48 -0
- package/components/newsroom-ops-overview.tsx +28 -11
- package/components/newsroom-ops-shell.tsx +7 -1
- package/components/newsroom-organizer-shell.tsx +130 -0
- package/components/newsroom-organizer.css +63 -0
- package/components/newsroom-references-view.tsx +92 -8
- package/components/papyrus-console-shell.tsx +4 -2
- package/components/pictogram-figure.tsx +9 -2
- package/components/presentation-header.tsx +135 -0
- package/components/presentation-rhythm-hrule.tsx +17 -0
- package/components/priming-proof-page.tsx +94 -0
- package/components/publication-cyclotron-panel.tsx +57 -0
- package/components/reader-auth-control.tsx +8 -5
- package/components/reference-curation-controls.tsx +77 -0
- package/components/reference-cyclotron-status.tsx +109 -0
- package/components/reference-priming.tsx +18 -0
- package/components/reference-relevance-review.tsx +75 -0
- package/components/settings-page.tsx +2 -1
- package/components/signup-entries-panel.tsx +94 -0
- package/components/topic-steering-workspace.tsx +78 -8
- package/components/use-blog-header-obstacles.ts +81 -0
- package/components/use-brand-fonts-ready.ts +28 -0
- package/components/use-near-viewport.ts +30 -0
- package/components/use-rhythm-overlay.ts +29 -0
- package/infra/amplify-app-shell.js +166 -27
- package/infra/amplify-app-shell.ts +180 -31
- package/infra/build-specs.js +23 -7
- package/infra/build-specs.ts +27 -10
- package/infra/site-config.js +248 -36
- package/infra/site-config.ts +313 -51
- package/lib/amplify-outputs-path.ts +5 -1
- package/lib/amplify-server-runtime.ts +36 -1
- package/lib/approved-priming-proof-reports.ts +6 -0
- package/lib/blog-feature-solver.ts +459 -0
- package/lib/blog-rhythm.ts +134 -0
- package/lib/brand-fonts-watch.ts +43 -0
- package/lib/brand-fonts.ts +112 -0
- package/lib/cached-content-repository.ts +12 -0
- package/lib/category-repository.ts +43 -93
- package/lib/content-item-lifecycle.ts +55 -0
- package/lib/content-repository.ts +3 -0
- package/lib/content-types.ts +4 -0
- package/lib/curation-client.ts +61 -0
- package/lib/edition-routes.ts +31 -29
- package/lib/graphql-content-repository.ts +56 -22
- package/lib/media-route-response.ts +45 -0
- package/lib/newspaper-layout.ts +3 -2
- package/lib/newsroom-base-path.ts +12 -3
- package/lib/newsroom-board.ts +99 -0
- package/lib/newsroom-category-drilldown.ts +3 -2
- package/lib/newsroom-forum-routes.ts +6 -4
- package/lib/newsroom-index-filters.ts +14 -10
- package/lib/newsroom-nav.ts +3 -1
- package/lib/newsroom-organizer-nav.ts +37 -0
- package/lib/newsroom-section-routing.ts +20 -0
- package/lib/oauth-redirect-priority.ts +40 -0
- package/lib/papyrus-web-locations.ts +7 -6
- package/lib/plugin-settings.ts +11 -0
- package/lib/presentation-footer.ts +3 -2
- package/lib/pretext-layout.ts +12 -3
- package/lib/priming-proof-report.ts +90 -0
- package/lib/prospectus-publication-access.ts +53 -0
- package/lib/prospectus-session.ts +196 -0
- package/lib/prospectus-sign-in-routes.ts +284 -0
- package/lib/prospectus-site-identity.ts +303 -0
- package/lib/publication-cyclotron.ts +74 -0
- package/lib/rate-limit-retry.ts +55 -0
- package/lib/reader-base-path.ts +24 -0
- package/lib/reader-cache-lifetime.ts +5 -0
- package/lib/reader-static-params.ts +85 -4
- package/lib/reader-storage-url.ts +9 -12
- package/lib/reference-policy.ts +5 -0
- package/lib/relevance-decisions.ts +103 -0
- package/lib/relevance-status.ts +63 -0
- package/lib/renderer.ts +7 -0
- package/lib/revalidate-prewarm.ts +53 -0
- package/lib/revalidate-secret.ts +59 -0
- package/lib/robots-policy.ts +14 -0
- package/lib/root-route-redirect.ts +16 -0
- package/lib/semantic-graph.ts +7 -0
- package/lib/shared-credentials-provider.ts +61 -0
- package/lib/signup-csv.ts +24 -0
- package/lib/site-brand.ts +59 -0
- package/lib/staging-gated-path.ts +23 -0
- package/lib/staging-preview-object.ts +21 -5
- package/lib/test-editor-override.ts +4 -0
- package/lib/video-loading.ts +22 -0
- package/lib/video-script.ts +14 -0
- package/middleware.ts +11 -19
- package/package.json +73 -8
- package/renderers/pretext/article-page.tsx +39 -3
- package/renderers/pretext/presentation-shared.tsx +113 -0
- package/renderers/pretext/presentation-shell.tsx +15 -102
- package/renderers/pretext/rhythm-article-page.tsx +165 -0
- package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
- package/renderers/pretext/rhythm-shell-style.ts +35 -0
- package/routes.manifest.json +123 -12
- package/src/sync.mjs +30 -6
- package/src/with-papyrus.d.mts +9 -0
- package/src/with-papyrus.mjs +48 -3
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
- package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
- package/components/ai-elements/message.tsx +0 -360
|
@@ -3,7 +3,7 @@ import { generateClient } from "aws-amplify/data";
|
|
|
3
3
|
import type { Schema } from "../amplify/data/resource";
|
|
4
4
|
import type { Article, ArticleImage, ArticleImageAsset, ArticleImageLayout, ArticleImageThemeVariants, ArticleVideoAsset, ArticleVideoThemeVariants } from "./articles";
|
|
5
5
|
import { withContentLoadTiming } from "./content-load-timing";
|
|
6
|
-
import { getAmplifyServerRuntime } from "./amplify-server-runtime";
|
|
6
|
+
import { getAmplifyServerRuntime, runWithGuestAmplifyContext } from "./amplify-server-runtime";
|
|
7
7
|
import { resolveReaderStorageUrl, signStorageUrl } from "./reader-storage-url";
|
|
8
8
|
import type { ContentRepository, EditionContent, EditionRouteSummary, ListPublishedEditionsOptions, LoadEditionContentOptions } from "./content-types";
|
|
9
9
|
import { isDemoAmplifyOutputs } from "./demo-amplify-outputs";
|
|
@@ -18,7 +18,10 @@ import {
|
|
|
18
18
|
} from "./publication-items";
|
|
19
19
|
import { BodyIrError, matchBodyImages, projectBodyIr, type BodyProjection, type BodyProjectionImage } from "./markus-body";
|
|
20
20
|
import { SITE_BRAND } from "./site-brand";
|
|
21
|
+
import { shouldFetchVideoScripts } from "./video-mode";
|
|
22
|
+
import { collectVideoScriptTargets, parseVideoScriptRef, videomlItemSlug, type VideoScriptRef } from "./video-script";
|
|
21
23
|
import { currentContentSource, currentRequestClient, runWithContentSource } from "./content-source-context";
|
|
24
|
+
import { withRateLimitRetry } from "./rate-limit-retry";
|
|
22
25
|
import { getContentSource } from "./site-env";
|
|
23
26
|
|
|
24
27
|
const DEFAULT_EDITION_SLUG = "current";
|
|
@@ -131,10 +134,9 @@ const IMAGE_ROLES: NonNullable<ArticleImageAsset["roles"]> = [
|
|
|
131
134
|
"thumbnail",
|
|
132
135
|
];
|
|
133
136
|
|
|
134
|
-
let readerContextClient: DataClient | null = null;
|
|
135
137
|
let buildTimeClient: DataClient | null = null;
|
|
136
138
|
|
|
137
|
-
function authMode(): "userPool" | "identityPool" {
|
|
139
|
+
export function authMode(): "userPool" | "identityPool" {
|
|
138
140
|
return currentContentSource() === "drafts" ? "userPool" : "identityPool";
|
|
139
141
|
}
|
|
140
142
|
|
|
@@ -144,27 +146,20 @@ function isVisibleItemStatus(status: string): boolean {
|
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
async function withReaderGraphQLContext<T>(operation: () => Promise<T>): Promise<T> {
|
|
147
|
-
const {
|
|
149
|
+
const { config } = getAmplifyServerRuntime();
|
|
148
150
|
const source = getContentSource();
|
|
149
151
|
if (source === "drafts") {
|
|
150
152
|
// Drafts are read as the signed-in visitor: the client reads the session from the request cookies, and it is
|
|
151
153
|
// kept in the request's async context (never a module global) so one visitor's session cannot serve another.
|
|
152
154
|
const { cookies } = await import("next/headers");
|
|
155
|
+
await cookies();
|
|
153
156
|
const client = generateServerClientUsingCookies<Schema>({ config, cookies, authMode: "userPool" });
|
|
154
157
|
return runWithContentSource(source, operation, client);
|
|
155
158
|
}
|
|
156
|
-
return
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
readerContextClient = generateClient<Schema>({ authMode: authMode() });
|
|
161
|
-
try {
|
|
162
|
-
return await operation();
|
|
163
|
-
} finally {
|
|
164
|
-
readerContextClient = null;
|
|
165
|
-
}
|
|
166
|
-
},
|
|
167
|
-
}),
|
|
159
|
+
return withRateLimitRetry(() =>
|
|
160
|
+
runWithGuestAmplifyContext(() =>
|
|
161
|
+
runWithContentSource(source, operation, generateClient<Schema>({ authMode: authMode() })),
|
|
162
|
+
),
|
|
168
163
|
);
|
|
169
164
|
}
|
|
170
165
|
|
|
@@ -236,6 +231,10 @@ export const graphqlContentRepository: ContentRepository = {
|
|
|
236
231
|
return withReaderGraphQLContext(() => loadEditionItem(editionDate, itemSlug));
|
|
237
232
|
},
|
|
238
233
|
|
|
234
|
+
loadVideoScript(targetSlug) {
|
|
235
|
+
return withReaderGraphQLContext(() => loadVideoScriptForTarget(targetSlug));
|
|
236
|
+
},
|
|
237
|
+
|
|
239
238
|
listArticleSlugs() {
|
|
240
239
|
return withReaderGraphQLContext(async () => {
|
|
241
240
|
const items = await listItemsByTypeStatus(`${ARTICLE_TYPE}#${PUBLISHED_STATUS}`);
|
|
@@ -250,7 +249,6 @@ export const graphqlContentRepository: ContentRepository = {
|
|
|
250
249
|
function getClient(): DataClient {
|
|
251
250
|
const requestClient = currentRequestClient<DataClient>();
|
|
252
251
|
if (requestClient) return requestClient;
|
|
253
|
-
if (readerContextClient) return readerContextClient;
|
|
254
252
|
|
|
255
253
|
if (!buildTimeClient) {
|
|
256
254
|
getAmplifyServerRuntime();
|
|
@@ -379,6 +377,9 @@ async function loadEditionContentFromEdition(edition: GraphQLEdition): Promise<E
|
|
|
379
377
|
const sanitizedLayoutPlan = pruneLayoutPlanUnavailableItems(layoutPlan, availableItemSlugs, edition.id);
|
|
380
378
|
validateEditionLayoutPlanForItems(sanitizedLayoutPlan, items, `PublishedEdition(${edition.id}).layoutPlan`);
|
|
381
379
|
|
|
380
|
+
const editionVideo = await normalizeEditionVideoAsset(editionMetadata?.editionVideo);
|
|
381
|
+
const videoScripts = await loadEditionVideoScripts(items, editionVideo);
|
|
382
|
+
|
|
382
383
|
return {
|
|
383
384
|
id: edition.id,
|
|
384
385
|
source: "graphql",
|
|
@@ -389,10 +390,38 @@ async function loadEditionContentFromEdition(edition: GraphQLEdition): Promise<E
|
|
|
389
390
|
items,
|
|
390
391
|
sections: createEditionSectionPlan(items, edition.metadata),
|
|
391
392
|
suppressNewsDeskAppendix: editionMetadata?.suppressNewsDeskAppendix === true,
|
|
392
|
-
editionVideo
|
|
393
|
+
editionVideo,
|
|
394
|
+
videoScripts,
|
|
393
395
|
};
|
|
394
396
|
}
|
|
395
397
|
|
|
398
|
+
function videoScriptsEnabled(): boolean {
|
|
399
|
+
return Boolean(SITE_BRAND.videoPlayer) && shouldFetchVideoScripts();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function loadVideoScriptForTarget(targetSlug: string): Promise<VideoScriptRef | null> {
|
|
403
|
+
if (!videoScriptsEnabled()) return null;
|
|
404
|
+
const item = await getItemBySlug(videomlItemSlug(targetSlug));
|
|
405
|
+
if (!item || item.type !== "videoml" || !isVisibleItemStatus(item.status)) return null;
|
|
406
|
+
return parseVideoScriptRef(item);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function loadEditionVideoScripts(
|
|
410
|
+
items: PublicationItem[],
|
|
411
|
+
editionVideo: ArticleVideoAsset | null,
|
|
412
|
+
): Promise<Record<string, VideoScriptRef> | undefined> {
|
|
413
|
+
if (!videoScriptsEnabled()) return undefined;
|
|
414
|
+
const targets = collectVideoScriptTargets(items, editionVideo !== null);
|
|
415
|
+
const entries = await Promise.all(
|
|
416
|
+
targets.map(async (targetSlug) => {
|
|
417
|
+
const script = await loadVideoScriptForTarget(targetSlug);
|
|
418
|
+
return script ? ([targetSlug, script] as const) : null;
|
|
419
|
+
}),
|
|
420
|
+
);
|
|
421
|
+
const videoScripts = Object.fromEntries(entries.filter((entry): entry is [string, VideoScriptRef] => entry !== null));
|
|
422
|
+
return Object.keys(videoScripts).length > 0 ? videoScripts : undefined;
|
|
423
|
+
}
|
|
424
|
+
|
|
396
425
|
async function normalizeEditionVideoAsset(value: unknown): Promise<ArticleVideoAsset | null> {
|
|
397
426
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
398
427
|
const record = value as Record<string, unknown>;
|
|
@@ -817,7 +846,7 @@ async function normalizeArticle(item: GraphQLItem, mediaAssets: GraphQLMediaAsse
|
|
|
817
846
|
|
|
818
847
|
function projectPublishedItemBody(item: GraphQLItem): BodyProjection {
|
|
819
848
|
if (item.bodyIr === null || item.bodyIr === undefined || item.bodyIr === "") {
|
|
820
|
-
throw new BodyIrError(`PublishedItem ${item.slug} has no bodyIr; run: papyrus content convert-bodies`);
|
|
849
|
+
throw new BodyIrError(`PublishedItem ${item.slug} has no bodyIr; run: papyrus ops content convert-bodies`);
|
|
821
850
|
}
|
|
822
851
|
return projectBodyIr(item.bodyIr);
|
|
823
852
|
}
|
|
@@ -939,15 +968,20 @@ function parseAttachmentPointer(value: unknown): ContentAttachmentPointer | null
|
|
|
939
968
|
};
|
|
940
969
|
}
|
|
941
970
|
|
|
942
|
-
function readInlineExcerpt(editorial: unknown): string | null {
|
|
971
|
+
export function readInlineExcerpt(editorial: unknown): string | null {
|
|
943
972
|
const parsed = parseObjectMetadata(editorial);
|
|
944
973
|
if (!parsed) return null;
|
|
945
974
|
const direct = parsed.excerpt;
|
|
946
975
|
if (typeof direct === "string" && direct.trim()) return direct.trim();
|
|
976
|
+
const custom = parsed.customExcerpt;
|
|
977
|
+
if (typeof custom === "string" && custom.trim()) return custom.trim();
|
|
947
978
|
const newsroom = parsed.newsroom;
|
|
948
979
|
if (!newsroom || typeof newsroom !== "object" || Array.isArray(newsroom)) return null;
|
|
949
|
-
const
|
|
950
|
-
|
|
980
|
+
const newsroomRecord = newsroom as Record<string, unknown>;
|
|
981
|
+
const nestedExcerpt = newsroomRecord.excerpt;
|
|
982
|
+
if (typeof nestedExcerpt === "string" && nestedExcerpt.trim()) return nestedExcerpt.trim();
|
|
983
|
+
const nestedCustom = newsroomRecord.customExcerpt;
|
|
984
|
+
return typeof nestedCustom === "string" && nestedCustom.trim() ? nestedCustom.trim() : null;
|
|
951
985
|
}
|
|
952
986
|
|
|
953
987
|
function normalizePublicationItemType(value: string): Exclude<PublicationItemType, "article"> | null {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const PROXIED_IMAGE_EXTENSIONS = new Set(["jpg", "jpeg", "png", "gif", "webp", "avif", "svg"]);
|
|
2
|
+
|
|
3
|
+
export type MediaRouteDependencies = {
|
|
4
|
+
signStorageUrl: (storagePath: string) => Promise<string>;
|
|
5
|
+
fetchUpstream: (signedUrl: string) => Promise<Response>;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function isProxiedImagePath(storagePath: string): boolean {
|
|
9
|
+
const lastSegment = storagePath.split("/").pop() ?? "";
|
|
10
|
+
const dotIndex = lastSegment.lastIndexOf(".");
|
|
11
|
+
if (dotIndex < 0) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return PROXIED_IMAGE_EXTENSIONS.has(lastSegment.slice(dotIndex + 1).toLowerCase());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function buildMediaRouteResponse(
|
|
18
|
+
storagePath: string,
|
|
19
|
+
dependencies: MediaRouteDependencies,
|
|
20
|
+
): Promise<Response> {
|
|
21
|
+
const signedUrl = await dependencies.signStorageUrl(storagePath);
|
|
22
|
+
|
|
23
|
+
if (!isProxiedImagePath(storagePath)) {
|
|
24
|
+
return new Response(null, {
|
|
25
|
+
status: 307,
|
|
26
|
+
headers: { location: signedUrl, "cache-control": "private, max-age=300" },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const upstream = await dependencies.fetchUpstream(signedUrl);
|
|
31
|
+
if (!upstream.ok) {
|
|
32
|
+
return Response.json(
|
|
33
|
+
{ error: `Upstream media fetch failed (${upstream.status}).` },
|
|
34
|
+
{ status: upstream.status === 404 ? 404 : 502 },
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const headers = new Headers();
|
|
39
|
+
const contentType = upstream.headers.get("content-type");
|
|
40
|
+
if (contentType) {
|
|
41
|
+
headers.set("content-type", contentType);
|
|
42
|
+
}
|
|
43
|
+
headers.set("cache-control", "public, max-age=3600, stale-while-revalidate=86400");
|
|
44
|
+
return new Response(upstream.body, { status: 200, headers });
|
|
45
|
+
}
|
package/lib/newspaper-layout.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toPublicReaderPath } from "./reader-base-path";
|
|
1
2
|
import type { ArticleImageAsset, ArticleImageThemeVariants } from "./articles";
|
|
2
3
|
import {
|
|
3
4
|
type ArticleFrameBlockSpec,
|
|
@@ -896,9 +897,9 @@ function solveFrontFooter(blocks: SolvedBlock[], config: LayoutConfig, pageNumbe
|
|
|
896
897
|
: Math.min(entries.length, Math.max(1, config.columnCount <= 1 ? 1 : config.columnCount <= 3 ? 2 : 4));
|
|
897
898
|
const sectionRows = entries.length === 0 ? 0 : Math.ceil(entries.length / sectionColumns);
|
|
898
899
|
const utilityEntries: SolvedFrontFooterUtilityEntry[] = [
|
|
899
|
-
{ id: "archive", label: "Archive", href: "/archive", disabled: false },
|
|
900
|
+
{ id: "archive", label: "Archive", href: toPublicReaderPath("/archive"), disabled: false },
|
|
900
901
|
{ id: "newsDesk", label: "Newsroom", href: "/newsroom", disabled: false },
|
|
901
|
-
{ id: "settings", label: "Settings", href: "/settings", disabled: false },
|
|
902
|
+
{ id: "settings", label: "Settings", href: toPublicReaderPath("/settings"), disabled: false },
|
|
902
903
|
{ id: "login", label: "LOGIN", disabled: true },
|
|
903
904
|
];
|
|
904
905
|
const rowCount = 1 + Math.max(sectionRows, utilityEntries.length);
|
|
@@ -19,13 +19,22 @@ export function usesNewsroomRootPaths(): boolean {
|
|
|
19
19
|
export function toPublicNewsroomPath(internalPath: string): string {
|
|
20
20
|
const publicBase = getPublicNewsroomBasePath();
|
|
21
21
|
if (publicBase !== "") return internalPath;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
const suffixStart = internalPath.search(/[?#]/);
|
|
23
|
+
const pathOnly = suffixStart === -1 ? internalPath : internalPath.slice(0, suffixStart);
|
|
24
|
+
const suffix = suffixStart === -1 ? "" : internalPath.slice(suffixStart);
|
|
25
|
+
if (pathOnly === "/newsroom" || pathOnly === "/newsroom/") return `/${suffix}`;
|
|
26
|
+
if (pathOnly.startsWith("/newsroom/")) {
|
|
27
|
+
return `${pathOnly.slice("/newsroom".length) || "/"}${suffix}`;
|
|
25
28
|
}
|
|
26
29
|
return internalPath;
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
/** Maps a browser pathname to the internal `/newsroom/...` form on CMS-only hosts; identity elsewhere. */
|
|
33
|
+
export function browserPathToInternalNewsroomPath(browserPath: string): string {
|
|
34
|
+
if (!usesNewsroomRootPaths()) return browserPath;
|
|
35
|
+
return toInternalNewsroomPath(browserPath);
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
export function toInternalNewsroomPath(publicPath: string): string {
|
|
30
39
|
const publicBase = getPublicNewsroomBasePath();
|
|
31
40
|
const normalized = publicPath.startsWith("/") ? publicPath : `/${publicPath}`;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { KanbanConfig, KanbanIssue } from "@kanbus/ui";
|
|
2
|
+
import type { AssignmentRecord, ReferenceRecord } from "./category-repository";
|
|
3
|
+
import {
|
|
4
|
+
CONTENT_ITEM_STAGES,
|
|
5
|
+
contentItemStageLabel,
|
|
6
|
+
latestVersionPerContentItem,
|
|
7
|
+
type ContentItemVersionRecord,
|
|
8
|
+
} from "./content-item-lifecycle";
|
|
9
|
+
import { getContentItemName } from "./site-brand";
|
|
10
|
+
|
|
11
|
+
export type NativeBoardKind = "contentItem" | "reference" | "assignment";
|
|
12
|
+
export interface NativeBoardIssue extends KanbanIssue {
|
|
13
|
+
kind: NativeBoardKind;
|
|
14
|
+
nativeId: string;
|
|
15
|
+
nativeStatus: string;
|
|
16
|
+
href: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function contentItemBoardType(): string {
|
|
20
|
+
return getContentItemName().singular.toLowerCase();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function boardTypeForKind(kind: NativeBoardKind): string {
|
|
24
|
+
return kind === "contentItem" ? contentItemBoardType() : kind;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* One card per content item, in the column for its lifecycle stage. Assignments that are work on a content item
|
|
29
|
+
* are counted on that item's card rather than shown as separate cards. References and other work keep their own
|
|
30
|
+
* identity and native status.
|
|
31
|
+
*/
|
|
32
|
+
export function projectNewsroomBoard(records: {
|
|
33
|
+
contentItemVersions?: readonly ContentItemVersionRecord[];
|
|
34
|
+
references?: readonly ReferenceRecord[];
|
|
35
|
+
assignments?: readonly AssignmentRecord[];
|
|
36
|
+
}): NativeBoardIssue[] {
|
|
37
|
+
const assignmentCountByContentItem = new Map<string, number>();
|
|
38
|
+
for (const assignment of records.assignments ?? []) {
|
|
39
|
+
if (assignment.itemLineageId) {
|
|
40
|
+
assignmentCountByContentItem.set(assignment.itemLineageId, (assignmentCountByContentItem.get(assignment.itemLineageId) ?? 0) + 1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const contentItemType = contentItemBoardType();
|
|
44
|
+
return [
|
|
45
|
+
...latestVersionPerContentItem(records.contentItemVersions ?? []).map(version => {
|
|
46
|
+
const stage = version.stage ?? "idea";
|
|
47
|
+
const workCount = assignmentCountByContentItem.get(version.lineageId) ?? 0;
|
|
48
|
+
return {
|
|
49
|
+
id: `contentItem:${version.lineageId}`, nativeId: version.lineageId, kind: "contentItem" as const, type: contentItemType,
|
|
50
|
+
title: version.headline || version.title || version.slug, nativeStatus: stage, status: `${contentItemType}/${stage}`,
|
|
51
|
+
priority: 2, description: workCount ? `${workCount} assignment${workCount === 1 ? "" : "s"} on this ${contentItemType}.` : undefined,
|
|
52
|
+
updated_at: version.updatedAt ?? undefined, href: `/newsroom/articles/${encodeURIComponent(version.id)}`,
|
|
53
|
+
};
|
|
54
|
+
}),
|
|
55
|
+
...(records.references ?? []).filter(row => !row.versionState || row.versionState === "current").map(row => ({
|
|
56
|
+
id: `reference:${row.id}`, nativeId: row.id, kind: "reference" as const, type: "reference",
|
|
57
|
+
title: row.title || row.externalItemId || row.id,
|
|
58
|
+
nativeStatus: row.curationStatus || "pending", status: `reference/${row.curationStatus || "pending"}`,
|
|
59
|
+
priority: 2, description: row.curationStatusReason || undefined,
|
|
60
|
+
updated_at: row.updatedAt || row.importedAt || undefined,
|
|
61
|
+
href: `/newsroom/references/${encodeURIComponent(row.lineageId || row.id)}`,
|
|
62
|
+
})),
|
|
63
|
+
...(records.assignments ?? []).filter(row => !row.itemLineageId).map(row => ({
|
|
64
|
+
id: `assignment:${row.id}`, nativeId: row.id, kind: "assignment" as const, type: "assignment",
|
|
65
|
+
title: row.title, nativeStatus: row.status, status: `assignment/${row.status}`,
|
|
66
|
+
priority: row.priority ?? 2, assignee: row.assigneeId || undefined,
|
|
67
|
+
description: row.summary || row.brief || undefined,
|
|
68
|
+
created_at: row.createdAt, updated_at: row.updatedAt,
|
|
69
|
+
href: `/newsroom/assignments/${encodeURIComponent(row.id)}`,
|
|
70
|
+
})),
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Content items always show every lifecycle stage in order; unknown native statuses of other records stay visible. */
|
|
75
|
+
export function newsroomBoardConfig(issues: readonly NativeBoardIssue[]): KanbanConfig {
|
|
76
|
+
const contentItemType = contentItemBoardType();
|
|
77
|
+
const kinds: NativeBoardKind[] = ["contentItem", "reference", "assignment"];
|
|
78
|
+
const contentItemStatuses = [
|
|
79
|
+
...CONTENT_ITEM_STAGES,
|
|
80
|
+
...new Set(issues.filter(issue => issue.kind === "contentItem").map(issue => issue.nativeStatus)),
|
|
81
|
+
].filter((stage, index, stages) => stages.indexOf(stage) === index)
|
|
82
|
+
.map(stage => ({ key: `${contentItemType}/${stage}`, name: contentItemStageLabel(stage), category: "native" }));
|
|
83
|
+
const otherStatuses = [...new Set(issues.filter(issue => issue.kind !== "contentItem").map(issue => issue.status))]
|
|
84
|
+
.map(key => ({ key, name: key.replace("/", " · "), category: "native" }));
|
|
85
|
+
const statuses = [...contentItemStatuses, ...otherStatuses];
|
|
86
|
+
return {
|
|
87
|
+
hierarchy: [], types: kinds.map(boardTypeForKind),
|
|
88
|
+
statuses,
|
|
89
|
+
categories: [{ name: "native" }], priorities: {}, type_colors: {},
|
|
90
|
+
workflows: Object.fromEntries(kinds.map(kind => {
|
|
91
|
+
const type = boardTypeForKind(kind);
|
|
92
|
+
return [type, Object.fromEntries(statuses.filter(status => status.key.startsWith(`${type}/`)).map(status => [status.key, []]))];
|
|
93
|
+
})),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function canReadNewsroomBoard(phase: string | undefined): boolean {
|
|
98
|
+
return phase === "ready";
|
|
99
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toPublicNewsroomPath } from "./newsroom-base-path";
|
|
1
2
|
import type {
|
|
2
3
|
CategorySteeringCategory,
|
|
3
4
|
SemanticRelationRecord,
|
|
@@ -93,11 +94,11 @@ export function uniqueNeighborGroupsForCategoryContext(graph: CategoryGraph, con
|
|
|
93
94
|
export function topicHref(rootCategoryKey: string, selectedCategoryKey?: string | null): string {
|
|
94
95
|
const encodedRoot = encodeURIComponent(rootCategoryKey);
|
|
95
96
|
const encodedSelected = selectedCategoryKey && selectedCategoryKey !== rootCategoryKey ? `/${encodeURIComponent(selectedCategoryKey)}` : "";
|
|
96
|
-
return `/newsroom/topics/${encodedRoot}${encodedSelected}
|
|
97
|
+
return toPublicNewsroomPath(`/newsroom/topics/${encodedRoot}${encodedSelected}`);
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
export function categoryDrilldownHref(section: "references" | "concepts", categoryKey: string): string {
|
|
100
|
-
return `/newsroom/${section}/${encodeURIComponent(categoryKey)}
|
|
101
|
+
return toPublicNewsroomPath(`/newsroom/${section}/${encodeURIComponent(categoryKey)}`);
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
function matchesCategorySelection(category: CategorySteeringCategory, selection: string): boolean {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { toPublicNewsroomPath } from "./newsroom-base-path";
|
|
2
|
+
|
|
1
3
|
export const FORUM_THREAD_ROUTE_PREFIX = "/newsroom/forum/";
|
|
2
4
|
export const INSIGHT_THREAD_ROUTE_PREFIX = "/newsroom/insights/";
|
|
3
5
|
export const FORUM_MESSAGE_HASH_PREFIX = "message-";
|
|
@@ -81,7 +83,7 @@ export function buildForumThreadUrl(
|
|
|
81
83
|
if (options?.demo) params.set("demo", "1");
|
|
82
84
|
const query = params.toString();
|
|
83
85
|
const hash = options?.messageId ? getForumMessageAnchorId(options.messageId) : "";
|
|
84
|
-
return `/newsroom/forum/${encodedThreadId}${query ? `?${query}` : ""}${hash ? `#${hash}` : ""}
|
|
86
|
+
return toPublicNewsroomPath(`/newsroom/forum/${encodedThreadId}${query ? `?${query}` : ""}${hash ? `#${hash}` : ""}`);
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
export function buildInsightThreadUrl(
|
|
@@ -93,7 +95,7 @@ export function buildInsightThreadUrl(
|
|
|
93
95
|
if (options?.demo) params.set("demo", "1");
|
|
94
96
|
const query = params.toString();
|
|
95
97
|
const hash = options?.messageId ? getForumMessageAnchorId(options.messageId) : "";
|
|
96
|
-
return `${INSIGHT_THREAD_ROUTE_PREFIX}${encodedThreadId}${query ? `?${query}` : ""}${hash ? `#${hash}` : ""}
|
|
98
|
+
return toPublicNewsroomPath(`${INSIGHT_THREAD_ROUTE_PREFIX}${encodedThreadId}${query ? `?${query}` : ""}${hash ? `#${hash}` : ""}`);
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
export function buildThreadUrlForSurface(
|
|
@@ -141,7 +143,7 @@ export function buildNewsroomForumIndexUrl(demo?: boolean): string {
|
|
|
141
143
|
const params = new URLSearchParams();
|
|
142
144
|
if (demo) params.set("demo", "1");
|
|
143
145
|
const query = params.toString();
|
|
144
|
-
return query ? `/newsroom?${query}` : "/newsroom";
|
|
146
|
+
return toPublicNewsroomPath(query ? `/newsroom?${query}` : "/newsroom");
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
export function buildNewsroomInsightsIndexUrl(demo?: boolean, domain?: string): string {
|
|
@@ -149,5 +151,5 @@ export function buildNewsroomInsightsIndexUrl(demo?: boolean, domain?: string):
|
|
|
149
151
|
if (demo) params.set("demo", "1");
|
|
150
152
|
if (domain?.trim()) params.set("domain", domain.trim());
|
|
151
153
|
const query = params.toString();
|
|
152
|
-
return query ? `/newsroom/insights?${query}` : "/newsroom/insights";
|
|
154
|
+
return toPublicNewsroomPath(query ? `/newsroom/insights?${query}` : "/newsroom/insights");
|
|
153
155
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { browserPathToInternalNewsroomPath, toPublicNewsroomPath } from "./newsroom-base-path";
|
|
1
2
|
export type NewsroomIndexTab = "references" | "messages" | "assignments" | "insights";
|
|
2
3
|
|
|
3
4
|
export type ReferenceIndexOrder = "published" | "imported";
|
|
@@ -147,10 +148,10 @@ export function buildNewsroomIndexWebPath(
|
|
|
147
148
|
tab: NewsroomIndexTab,
|
|
148
149
|
filters: ReferencesIndexFilters | MessagesIndexFilters | AssignmentsIndexFilters | InsightsIndexFilters,
|
|
149
150
|
): string {
|
|
150
|
-
if (tab === "references") return `/newsroom/references${buildReferencesIndexQuery(filters as ReferencesIndexFilters)}
|
|
151
|
-
if (tab === "messages") return `/newsroom/messages${buildMessagesIndexQuery(filters as MessagesIndexFilters)}
|
|
152
|
-
if (tab === "insights") return `/newsroom/insights${buildInsightsIndexQuery(filters as InsightsIndexFilters)}
|
|
153
|
-
return `/newsroom/assignments${buildAssignmentsIndexQuery(filters as AssignmentsIndexFilters)}
|
|
151
|
+
if (tab === "references") return toPublicNewsroomPath(`/newsroom/references${buildReferencesIndexQuery(filters as ReferencesIndexFilters)}`);
|
|
152
|
+
if (tab === "messages") return toPublicNewsroomPath(`/newsroom/messages${buildMessagesIndexQuery(filters as MessagesIndexFilters)}`);
|
|
153
|
+
if (tab === "insights") return toPublicNewsroomPath(`/newsroom/insights${buildInsightsIndexQuery(filters as InsightsIndexFilters)}`);
|
|
154
|
+
return toPublicNewsroomPath(`/newsroom/assignments${buildAssignmentsIndexQuery(filters as AssignmentsIndexFilters)}`);
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
export function syncBrowserNewsroomIndexUrl(
|
|
@@ -168,8 +169,9 @@ export function syncBrowserNewsroomIndexUrl(
|
|
|
168
169
|
|
|
169
170
|
/** Lineage id from `/newsroom/references/<id>` (not index-only URLs). */
|
|
170
171
|
export function parseReferenceLineageIdFromNewsroomPathname(pathname: string | null | undefined): string | null {
|
|
171
|
-
|
|
172
|
-
|
|
172
|
+
const internalPathname = pathname ? browserPathToInternalNewsroomPath(pathname) : null;
|
|
173
|
+
if (!internalPathname?.startsWith("/newsroom/references/")) return null;
|
|
174
|
+
const segment = internalPathname.slice("/newsroom/references/".length).split("/")[0]?.trim() ?? "";
|
|
173
175
|
if (!segment) return null;
|
|
174
176
|
try {
|
|
175
177
|
return decodeURIComponent(segment);
|
|
@@ -180,8 +182,9 @@ export function parseReferenceLineageIdFromNewsroomPathname(pathname: string | n
|
|
|
180
182
|
|
|
181
183
|
/** Assignment id from `/newsroom/assignments/<id>` (not index-only URLs). */
|
|
182
184
|
export function parseAssignmentIdFromNewsroomPathname(pathname: string | null | undefined): string | null {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
+
const internalPathname = pathname ? browserPathToInternalNewsroomPath(pathname) : null;
|
|
186
|
+
if (!internalPathname?.startsWith("/newsroom/assignments/")) return null;
|
|
187
|
+
const segment = internalPathname.slice("/newsroom/assignments/".length).split("/")[0]?.trim() ?? "";
|
|
185
188
|
if (!segment) return null;
|
|
186
189
|
try {
|
|
187
190
|
return decodeURIComponent(segment);
|
|
@@ -191,8 +194,9 @@ export function parseAssignmentIdFromNewsroomPathname(pathname: string | null |
|
|
|
191
194
|
}
|
|
192
195
|
|
|
193
196
|
export function parseInsightThreadIdFromNewsroomPathname(pathname: string | null | undefined): string | null {
|
|
194
|
-
|
|
195
|
-
|
|
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() ?? "";
|
|
196
200
|
if (!segment) return null;
|
|
197
201
|
try {
|
|
198
202
|
return decodeURIComponent(segment);
|
package/lib/newsroom-nav.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { newsroomHref, toPublicNewsroomPath } from "./newsroom-base-path";
|
|
2
|
+
import { getContentItemName } from "./site-brand";
|
|
2
3
|
|
|
3
4
|
export type NewsroomNavTabId =
|
|
4
5
|
| "overview"
|
|
@@ -23,13 +24,14 @@ export type NewsroomNavItem = {
|
|
|
23
24
|
|
|
24
25
|
export const NEWSROOM_OPS_NAV: NewsroomNavItem[] = [
|
|
25
26
|
{ id: "overview", label: "Overview", detail: "Desk home", href: newsroomHref(), mobilePrimary: true },
|
|
26
|
-
{ id: "articles", label:
|
|
27
|
+
{ id: "articles", label: getContentItemName().plural, detail: "Drafts & published", href: newsroomHref("articles"), mobilePrimary: true },
|
|
27
28
|
{ id: "assignments", label: "Assignments", detail: "Work queue", href: newsroomHref("assignments"), mobilePrimary: true },
|
|
28
29
|
{ id: "references", label: "References", detail: "Knowledge base", href: newsroomHref("references"), mobilePrimary: true },
|
|
29
30
|
{ id: "messages", label: "Messages", detail: "Forum", href: newsroomHref("messages") },
|
|
30
31
|
{ id: "insights", label: "Insights", detail: "Research threads", href: newsroomHref("insights") },
|
|
31
32
|
{ id: "topics", label: "Topics", detail: "Taxonomy", href: newsroomHref("topics"), mobilePrimary: true },
|
|
32
33
|
{ id: "concepts", label: "Concepts", detail: "Ontology", href: newsroomHref("concepts") },
|
|
34
|
+
{ id: "search", label: "Search", detail: "Knowledge base", href: newsroomHref("search") },
|
|
33
35
|
{ id: "administration", label: "Administration", detail: "Users & policies", href: newsroomHref("administration") },
|
|
34
36
|
];
|
|
35
37
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getContentItemName } from "./site-brand";
|
|
2
|
+
|
|
3
|
+
const contentItemPluralLowercase = getContentItemName().plural.toLowerCase();
|
|
4
|
+
export type OrganizerDestination = {
|
|
5
|
+
key: string; label: string; description: string; path: string;
|
|
6
|
+
access?: "admin"; primary?: boolean; unavailable?: string;
|
|
7
|
+
};
|
|
8
|
+
export const ORGANIZER_NAV_GROUPS: readonly { title: string; items: readonly OrganizerDestination[] }[] = [
|
|
9
|
+
{ title: "Editorial work", items: [
|
|
10
|
+
{ key: "overview", label: "Overview", description: "See the desk and choose where to begin.", path: "/newsroom", primary: true },
|
|
11
|
+
{ key: "board", label: "Board", description: `Follow ${contentItemPluralLowercase}, sources and assignments across their stages.`, path: "/newsroom/work", primary: true },
|
|
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" },
|
|
14
|
+
{ 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
|
+
] },
|
|
17
|
+
{ title: "Research & knowledge", items: [
|
|
18
|
+
{ key: "references", label: "References", description: "Inspect sources and make editorial inclusion decisions.", path: "/newsroom/references", primary: true },
|
|
19
|
+
{ key: "desks", label: "Taxonomy", description: "Maintain topic sets and category versions.", path: "/newsroom/desks" },
|
|
20
|
+
{ key: "topics", label: "Topic proposals", description: "Review suggested changes to coverage.", path: "/newsroom/topics" },
|
|
21
|
+
{ key: "concepts", label: "Concepts", description: "Explore knowledge entities and relationships.", path: "/newsroom/concepts" },
|
|
22
|
+
{ key: "search", label: "Knowledge search", description: "Find evidence across the knowledge base.", path: "/newsroom/search" },
|
|
23
|
+
] },
|
|
24
|
+
{ title: "Administration", items: [
|
|
25
|
+
{ key: "policies", label: "Policies", description: "Set publication and category guidance.", path: "/newsroom/administration/policies" },
|
|
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" },
|
|
30
|
+
] },
|
|
31
|
+
];
|
|
32
|
+
const all = ORGANIZER_NAV_GROUPS.flatMap(group => group.items);
|
|
33
|
+
export const ORGANIZER_PRIMARY_NAV = ["overview", "board", "references", "messages"].map(key => all.find(item => item.key === key)!);
|
|
34
|
+
|
|
35
|
+
export const ORGANIZER_SETTINGS_DESTINATION = {
|
|
36
|
+
label: "Settings", path: "/newsroom/settings", description: "Theme, motion and interface preferences.",
|
|
37
|
+
} as const;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { NewsDeskTab } from "../components/topic-steering-workspace";
|
|
2
|
+
|
|
3
|
+
/** The organizer restores the existing taxonomy editor; legacy sites retain their aliases. */
|
|
4
|
+
export function resolveNewsDeskTab(
|
|
5
|
+
sectionValue: string | string[] | null | undefined,
|
|
6
|
+
legacyTabValue: string | string[] | null | undefined,
|
|
7
|
+
organizer = false,
|
|
8
|
+
): NewsDeskTab {
|
|
9
|
+
const section = Array.isArray(sectionValue) ? sectionValue[0] : sectionValue;
|
|
10
|
+
const legacyTab = Array.isArray(legacyTabValue) ? legacyTabValue[0] : legacyTabValue;
|
|
11
|
+
const raw = section ?? (legacyTab === "categories" ? "topics" : legacyTab);
|
|
12
|
+
const value = raw === "users" || raw === "doctrine" ? "administration"
|
|
13
|
+
: raw === "desks" && !organizer ? "topics" : raw;
|
|
14
|
+
switch (value) {
|
|
15
|
+
case "desks": case "administration": case "topics": case "concepts":
|
|
16
|
+
case "references": case "insights": case "messages": case "assignments": case "search":
|
|
17
|
+
return value;
|
|
18
|
+
default: return "overview";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
function originOf(value: string): string {
|
|
2
|
+
const trimmed = value.replace(/\/+$/, "");
|
|
3
|
+
try {
|
|
4
|
+
const parsed = new URL(trimmed);
|
|
5
|
+
const hostname = parsed.hostname === "127.0.0.1" || parsed.hostname === "::1" ? "localhost" : parsed.hostname;
|
|
6
|
+
return `${parsed.protocol}//${hostname}${parsed.port ? `:${parsed.port}` : ""}`;
|
|
7
|
+
} catch {
|
|
8
|
+
return trimmed;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function pathOf(value: string): string | null {
|
|
13
|
+
try {
|
|
14
|
+
return new URL(value).pathname.replace(/\/+$/, "");
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Puts the redirect URLs of the current origin first; among those, the
|
|
22
|
+
* newsroom return path leads when the site has one, so Amplify (which signs in
|
|
23
|
+
* with the first entry) returns an editor to the newsroom.
|
|
24
|
+
*/
|
|
25
|
+
export function prioritizeRedirectUrlsForOrigin(
|
|
26
|
+
urls: readonly string[],
|
|
27
|
+
currentOrigin: string,
|
|
28
|
+
publicNewsroomBasePath: string,
|
|
29
|
+
): string[] {
|
|
30
|
+
const origin = originOf(currentOrigin);
|
|
31
|
+
const newsroomPath = publicNewsroomBasePath.replace(/\/+$/, "");
|
|
32
|
+
const head = urls.filter((value) => originOf(value) === origin);
|
|
33
|
+
if (head.length === 0) return [...urls];
|
|
34
|
+
const tail = urls.filter((value) => originOf(value) !== origin);
|
|
35
|
+
const newsroomFirst =
|
|
36
|
+
newsroomPath === ""
|
|
37
|
+
? head
|
|
38
|
+
: [...head.filter((value) => pathOf(value) === newsroomPath), ...head.filter((value) => pathOf(value) !== newsroomPath)];
|
|
39
|
+
return [...newsroomFirst, ...tail];
|
|
40
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toInternalReaderPath, toPublicReaderPath } from "./reader-base-path";
|
|
1
2
|
import { PAPYRUS_OBJECT_KINDS, type PapyrusObjectKind } from "./papyrus-object-kinds";
|
|
2
3
|
import {
|
|
3
4
|
buildNewsroomIndexWebPath,
|
|
@@ -52,7 +53,7 @@ export function buildWebUiContext(webPath: string): PapyrusWebUiContext {
|
|
|
52
53
|
export function webPathToPapyrusLocation(webPath: string): PapyrusWebUiContext {
|
|
53
54
|
const normalized = normalizeWebPath(webPath);
|
|
54
55
|
const url = new URL(normalized, "https://papyrus.local");
|
|
55
|
-
const pathname = url.pathname || "/";
|
|
56
|
+
const pathname = toInternalReaderPath(url.pathname || "/");
|
|
56
57
|
|
|
57
58
|
if (pathname === "/" || pathname === "") {
|
|
58
59
|
return location("papyrus://site/home", normalized, { label: "Papyrus home" });
|
|
@@ -81,7 +82,7 @@ export function webPathToPapyrusLocation(webPath: string): PapyrusWebUiContext {
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
if (!pathname.startsWith("/newsroom")) {
|
|
84
|
-
const tail = pathname.replace(/^\/+/, "");
|
|
85
|
+
const tail = (url.pathname || "/").replace(/^\/+/, "");
|
|
85
86
|
return location(`papyrus://site/path/${encodeURIComponent(tail)}`, normalized, { label: pathname });
|
|
86
87
|
}
|
|
87
88
|
|
|
@@ -215,9 +216,9 @@ export function papyrusUriToWebPath(uri: string): PapyrusUriToWebPathResult {
|
|
|
215
216
|
const { kind, id } = parsed;
|
|
216
217
|
|
|
217
218
|
if (kind === "site") {
|
|
218
|
-
if (id === "home") return { ok: true, papyrusLocationUri: raw, webPath: "/" };
|
|
219
|
-
if (id === "archive") return { ok: true, papyrusLocationUri: raw, webPath: "/archive" };
|
|
220
|
-
if (id === "settings") return { ok: true, papyrusLocationUri: raw, webPath: "/settings" };
|
|
219
|
+
if (id === "home") return { ok: true, papyrusLocationUri: raw, webPath: toPublicReaderPath("/") };
|
|
220
|
+
if (id === "archive") return { ok: true, papyrusLocationUri: raw, webPath: toPublicReaderPath("/archive") };
|
|
221
|
+
if (id === "settings") return { ok: true, papyrusLocationUri: raw, webPath: toPublicReaderPath("/settings") };
|
|
221
222
|
if (id.startsWith("path/")) {
|
|
222
223
|
return { ok: true, papyrusLocationUri: raw, webPath: `/${decodeURIComponent(id.slice("path/".length))}` };
|
|
223
224
|
}
|
|
@@ -449,7 +450,7 @@ function objectKindToWebPath(kind: PapyrusObjectKind, objectId: string): string
|
|
|
449
450
|
if (kind === "category") return `/newsroom/topics?category=${encoded}`;
|
|
450
451
|
if (kind === "semanticNode") return `/newsroom/concepts?node=${encoded}`;
|
|
451
452
|
if (kind === "newsroomSection") return `/newsroom/sections/${encoded}`;
|
|
452
|
-
if (kind === "item") return `/articles/${encoded}
|
|
453
|
+
if (kind === "item") return toPublicReaderPath(`/articles/${encoded}`);
|
|
453
454
|
return `/newsroom?object=${encodeURIComponent(kind)}:${encoded}`;
|
|
454
455
|
}
|
|
455
456
|
|