@anthusai/papyrus 1.6.0 → 1.7.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.
@@ -1,4 +1,6 @@
1
1
  import { notFound } from "next/navigation";
2
+ import { resolveActiveSiteBrand } from "../../../../lib/site-brand-tenant";
3
+ import { NewsroomBoardView } from "../../../../components/newsroom-board-view";
2
4
  import { NewsDeskPage } from "../../../../components/news-desk-page";
3
5
 
4
6
  export const dynamic = "force-dynamic";
@@ -13,5 +15,6 @@ export default async function NewsroomDeepSectionPage({ params, searchParams }:
13
15
  const normalizedSectionId = decodeURIComponent(sectionId ?? "").trim();
14
16
  if (!normalizedSectionId) notFound();
15
17
 
18
+ if (resolveActiveSiteBrand().newsroomLayout === "organizer") return <NewsroomBoardView subject="desk" deskId={normalizedSectionId} />;
16
19
  return <NewsDeskPage sectionPageId={normalizedSectionId} searchParams={searchParams} />;
17
20
  }
@@ -2236,6 +2236,7 @@ type PublishedItemBoardRow = {
2236
2236
  };
2237
2237
 
2238
2238
  export async function loadNewsroomPublishedContentItemVersions(): Promise<ContentItemVersionRecord[]> {
2239
+ if (isLocalPreviewEnabled()) return [];
2239
2240
  configureAmplifyClient();
2240
2241
  const client = generateClient<Schema>() as unknown as {
2241
2242
  graphql: (options: Record<string, unknown>) => Promise<GraphQLConnectionResponse<PublishedItemBoardRow>>;
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useEffect, useMemo, useRef, useState } from "react";
4
4
  import Link from "next/link";
5
- import { IssueCollection, IssueDetailPanel, useBoardColumnState } from "@kanbus/ui";
5
+ import { IssueCollection, IssueDetailPanel, IssueTypeFilter, issueTypeFromFilter, useBoardColumnState } from "@kanbus/ui";
6
6
  import "@kanbus/ui/styles/kanban.css";
7
7
  import "./newsroom-newsprint-board.css";
8
8
  import { PlusIcon } from "lucide-react";
@@ -15,11 +15,15 @@ import {
15
15
  canReadNewsroomBoard,
16
16
  contentItemBoardConfig,
17
17
  contentItemBoardType,
18
+ deskBoardConfig,
19
+ deskContainsAssignment,
20
+ deskContainsContentItem,
18
21
  projectAssignmentBoard,
19
22
  projectContentItemBoard,
20
23
  type NativeBoardIssue,
21
24
  } from "../lib/newsroom-board";
22
- import type { AssignmentRecord } from "../lib/category-repository";
25
+ import type { AssignmentRecord, NewsroomSectionRecord } from "../lib/category-repository";
26
+ import { displayNewsroomSectionShortTitle } from "../lib/newsroom-sections";
23
27
  import { getContentItemName, getContentItemStageDefinitions, SITE_BRAND } from "../lib/site-brand";
24
28
  import { getNewsroomNavHref } from "../lib/newsroom-nav";
25
29
  import { loadNewsroomAssignmentPage, loadNewsroomPublishedContentItemVersions } from "./news-desk-taxonomy-client";
@@ -46,10 +50,10 @@ async function loadEveryNewsroomAssignment() {
46
50
  return assignments;
47
51
  }
48
52
 
49
- export type NewsroomBoardSubject = "contentItem" | "assignment";
53
+ export type NewsroomBoardSubject = "contentItem" | "assignment" | "desk";
50
54
 
51
55
  /** Read-only board of one record kind over authenticated CMS records; existing workspaces own every mutation. */
52
- export function NewsroomBoardView({ subject }: { subject: NewsroomBoardSubject }) {
56
+ export function NewsroomBoardView({ subject, deskId }: { subject: NewsroomBoardSubject; deskId?: string }) {
53
57
  const session = useOptionalNewsDeskClient();
54
58
  const allowed = canReadNewsroomBoard(session?.shell.phase);
55
59
  const theme = useResolvedPapyrusTheme();
@@ -63,13 +67,20 @@ export function NewsroomBoardView({ subject }: { subject: NewsroomBoardSubject }
63
67
  const [supportingRecordsError, setSupportingRecordsError] = useState<string | null>(null);
64
68
  const generation = useRef(0);
65
69
  const boardBodyRef = useRef<HTMLDivElement | null>(null);
66
- const boardType = subject === "contentItem" ? contentItemBoardType() : ASSIGNMENT_BOARD_TYPE;
67
- const columnStateConfig = useMemo(() => subject === "contentItem" ? contentItemBoardConfig([]) : assignmentBoardConfig(assignmentBoardIssues), [assignmentBoardIssues, subject]);
70
+ const [typeFilter, setTypeFilter] = useState<string | null>("all");
71
+ const showsContentItems = subject !== "assignment";
72
+ const contentItemColumnKeyPrefix = `${contentItemBoardType()}/`;
73
+ const boardType = subject === "assignment" ? ASSIGNMENT_BOARD_TYPE : contentItemBoardType();
74
+ const deskAssignmentIssues = useMemo(() => subject === "desk" && deskId
75
+ ? projectAssignmentBoard(assignments.filter(assignment => deskContainsAssignment(assignment, deskId))) : [], [assignments, deskId, subject]);
76
+ const columnStateConfig = useMemo(() => subject === "contentItem" ? contentItemBoardConfig([])
77
+ : subject === "desk" ? deskBoardConfig(deskAssignmentIssues)
78
+ : assignmentBoardConfig(assignmentBoardIssues), [assignmentBoardIssues, deskAssignmentIssues, subject]);
68
79
  const columnState = useBoardColumnState({ config: columnStateConfig, storageKey: `papyrus.cms.columns.v1.${SITE_BRAND.id}.${subject}` });
69
- const openedCollapsedUntilOpenedStages = useMemo(() => subject !== "contentItem" ? [] : getContentItemStageDefinitions()
70
- .filter(definition => definition.collapsedUntilOpened && !columnState.collapsedColumns.has(`${boardType}/${definition.key}`))
71
- .map(definition => definition.key), [boardType, columnState.collapsedColumns, subject]);
72
- const pagedContentItems = usePagedContentItemVersions({ enabled: allowed && subject === "contentItem", openedCollapsedUntilOpenedStages });
80
+ const openedCollapsedUntilOpenedStages = useMemo(() => !showsContentItems ? [] : getContentItemStageDefinitions()
81
+ .filter(definition => definition.collapsedUntilOpened && !columnState.collapsedColumns.has(`${contentItemColumnKeyPrefix}${definition.key}`))
82
+ .map(definition => definition.key), [contentItemColumnKeyPrefix, columnState.collapsedColumns, showsContentItems]);
83
+ const pagedContentItems = usePagedContentItemVersions({ enabled: allowed && showsContentItems, openedCollapsedUntilOpenedStages });
73
84
  useEffect(() => {
74
85
  setLowMotion(readLocalReaderSettings().motion === "low");
75
86
  return subscribeReaderSettingsChanges(settings => setLowMotion(settings.motion === "low"));
@@ -78,7 +89,7 @@ export function NewsroomBoardView({ subject }: { subject: NewsroomBoardSubject }
78
89
  const request = ++generation.current;
79
90
  setAssignmentBoardIssues([]); setPublishedVersions([]); setAssignments([]); setSelected(null); setSupportingRecordsError(null);
80
91
  if (!allowed) { setSupportingRecordsLoading(false); return; }
81
- const loadSupportingRecords = () => subject === "contentItem"
92
+ const loadSupportingRecords = () => subject !== "assignment"
82
93
  ? Promise.all([loadNewsroomPublishedContentItemVersions(), loadEveryNewsroomAssignment()]).then(([published, everyAssignment]) => {
83
94
  if (generation.current === request) { setPublishedVersions(published); setAssignments(everyAssignment); }
84
95
  })
@@ -99,13 +110,19 @@ export function NewsroomBoardView({ subject }: { subject: NewsroomBoardSubject }
99
110
  document.addEventListener("visibilitychange", refreshWhenVisible);
100
111
  return () => { generation.current = request + 1; window.clearInterval(interval); document.removeEventListener("visibilitychange", refreshWhenVisible); };
101
112
  }, [allowed, subject]);
102
- const issues = useMemo(() => subject === "contentItem"
103
- ? projectContentItemBoard({ contentItemVersions: [...pagedContentItems.versions, ...publishedVersions], assignments })
104
- : assignmentBoardIssues, [assignmentBoardIssues, assignments, pagedContentItems.versions, publishedVersions, subject]);
105
- const loading = supportingRecordsLoading || (subject === "contentItem" && pagedContentItems.loading);
106
- const error = supportingRecordsError ?? (subject === "contentItem" ? pagedContentItems.error : null);
107
- const config = useMemo(() => subject === "contentItem" ? contentItemBoardConfig(issues) : assignmentBoardConfig(issues), [issues, subject]);
108
- const columnStageFromElement = (element: Element | null) => element?.closest("[data-column-key]")?.getAttribute("data-column-key")?.slice(boardType.length + 1) ?? null;
113
+ const issues = useMemo(() => {
114
+ if (subject === "assignment") return assignmentBoardIssues;
115
+ if (subject === "contentItem") return projectContentItemBoard({ contentItemVersions: [...pagedContentItems.versions, ...publishedVersions], assignments });
116
+ const versionsOnDesk = [...pagedContentItems.versions, ...publishedVersions].filter(version => deskId && deskContainsContentItem(version, deskId));
117
+ return [...projectContentItemBoard({ contentItemVersions: versionsOnDesk, assignments: assignments.filter(assignment => deskId && deskContainsAssignment(assignment, deskId)) }), ...deskAssignmentIssues];
118
+ }, [assignmentBoardIssues, assignments, deskAssignmentIssues, deskId, pagedContentItems.versions, publishedVersions, subject]);
119
+ const loading = supportingRecordsLoading || (showsContentItems && pagedContentItems.loading);
120
+ const error = supportingRecordsError ?? (showsContentItems ? pagedContentItems.error : null);
121
+ const config = useMemo(() => subject === "contentItem" ? contentItemBoardConfig(issues) : subject === "desk" ? deskBoardConfig(issues) : assignmentBoardConfig(issues), [issues, subject]);
122
+ const columnStageFromElement = (element: Element | null) => {
123
+ const columnKey = element?.closest("[data-column-key]")?.getAttribute("data-column-key");
124
+ return columnKey?.startsWith(contentItemColumnKeyPrefix) ? columnKey.slice(contentItemColumnKeyPrefix.length) : null;
125
+ };
109
126
  const loadMoreWhenColumnIsNearItsEnd = (columnScroll: HTMLElement) => {
110
127
  const distanceToEnd = columnScroll.scrollHeight - columnScroll.scrollTop - columnScroll.clientHeight;
111
128
  const stage = columnStageFromElement(columnScroll);
@@ -113,19 +130,20 @@ export function NewsroomBoardView({ subject }: { subject: NewsroomBoardSubject }
113
130
  };
114
131
  const handleBoardScroll = (event: React.UIEvent<HTMLDivElement>) => {
115
132
  const target = event.target as HTMLElement;
116
- if (subject === "contentItem" && target.classList?.contains("kb-column-scroll")) loadMoreWhenColumnIsNearItsEnd(target);
133
+ if (showsContentItems && target.classList?.contains("kb-column-scroll")) loadMoreWhenColumnIsNearItsEnd(target);
117
134
  };
118
135
  useEffect(() => {
119
- if (subject !== "contentItem") return;
136
+ if (!showsContentItems) return;
120
137
  boardBodyRef.current?.querySelectorAll<HTMLElement>(".kb-column-scroll").forEach(loadMoreWhenColumnIsNearItsEnd);
121
138
  });
122
- const pageTitle = subject === "contentItem" ? getContentItemName().plural : "Assignments";
139
+ const deskTitle = session?.shell.dashboard?.newsroomSections?.find(section => section.id === deskId)?.title ?? (deskId ? displayNewsroomSectionShortTitle({ id: deskId } as NewsroomSectionRecord) : "Desk");
140
+ const pageTitle = subject === "contentItem" ? getContentItemName().plural : subject === "desk" ? deskTitle : "Assignments";
123
141
  const issue = issues.find(item => item.id === selected);
124
142
  if (!allowed) return <NewsDeskAccessGate shell={session?.shell ?? null} />;
125
- return <NewsroomOpsShell activeTab={subject === "contentItem" ? "articles" : "assignments"} appTitle={SITE_BRAND.appTitle} backLabel={SITE_BRAND.backToHomeLabel} pageTitle={pageTitle}
143
+ return <NewsroomOpsShell activeTab={subject === "contentItem" ? "articles" : subject === "desk" ? "desks" : "assignments"} appTitle={SITE_BRAND.appTitle} backLabel={SITE_BRAND.backToHomeLabel} pageTitle={pageTitle}
126
144
  headerActions={<>
127
145
  <div role="group" aria-label="Record presentation" className="organizer-board-view-toggle"><button type="button" aria-pressed={view === "board"} onClick={() => setView("board")}>Board</button><button type="button" aria-pressed={view === "list"} onClick={() => setView("list")}>List</button></div>
128
- {subject === "contentItem" && (
146
+ {showsContentItems && (
129
147
  <Link className={cn(buttonVariants({ size: "sm" }))} data-newsroom-article-new href={getNewsroomNavHref(newsroomHref("articles", "new"))}>
130
148
  <PlusIcon aria-hidden="true" />
131
149
  {`New ${getContentItemName().singular.toLowerCase()}`}
@@ -135,8 +153,9 @@ export function NewsroomBoardView({ subject }: { subject: NewsroomBoardSubject }
135
153
  <div className="organizer-native-board papyrus-newsprint-board" data-color-mode={theme}>
136
154
  {error && <NewsroomOpsStatusBanner tone="error">{error}</NewsroomOpsStatusBanner>}
137
155
  <p role="status" className="organizer-board-note">{loading ? "Loading records…" : `${issues.length} records loaded.`}</p>
156
+ {subject === "desk" && <IssueTypeFilter config={config} value={typeFilter} onChange={setTypeFilter} name="Record type" className="organizer-board-type-filter" />}
138
157
  <div className="organizer-board-body" data-detail-open={Boolean(issue)} ref={boardBodyRef} onScrollCapture={handleBoardScroll}>
139
- <IssueCollection issues={issues} config={config} query={{ type: boardType }} view={view}
158
+ <IssueCollection issues={issues} config={config} query={{ type: subject === "desk" ? issueTypeFromFilter(typeFilter) : boardType }} view={view}
140
159
  motion={lowMotion ? STATIC_MOTION : undefined}
141
160
  collapsedColumns={columnState.collapsedColumns} onToggleCollapse={columnState.toggleColumn} initialScroll="start"
142
161
  selectedIssueId={selected} detailOpen={Boolean(issue)} onSelectIssue={item => setSelected(item.id)} ariaLabel={pageTitle}
@@ -114,6 +114,31 @@ export function assignmentBoardConfig(issues: readonly NativeBoardIssue[]): Kanb
114
114
  .map(status => ({ key: `${ASSIGNMENT_BOARD_TYPE}/${status}`, name: assignmentStatusLabel(status) })));
115
115
  }
116
116
 
117
+ function matchesDesk(candidate: string | null | undefined, deskId: string): boolean {
118
+ return Boolean(candidate) && candidate!.trim().toLowerCase() === deskId.trim().toLowerCase();
119
+ }
120
+
121
+ export function deskContainsContentItem(version: ContentItemVersionRecord, deskId: string): boolean {
122
+ return matchesDesk(version.section, deskId);
123
+ }
124
+
125
+ export function deskContainsAssignment(assignment: AssignmentRecord, deskId: string): boolean {
126
+ return matchesDesk(assignment.sectionId, deskId) || matchesDesk(assignment.sectionKey, deskId);
127
+ }
128
+
129
+ /** A desk board shows stories and assignments side by side; each keeps its own columns and the type filter narrows to one. */
130
+ export function deskBoardConfig(issues: readonly NativeBoardIssue[]): KanbanConfig {
131
+ const storiesConfig = contentItemBoardConfig(issues.filter(issue => issue.kind === "contentItem"));
132
+ const assignmentsConfig = assignmentBoardConfig(issues.filter(issue => issue.kind === "assignment"));
133
+ return {
134
+ ...storiesConfig,
135
+ types: [...(storiesConfig.types ?? []), ...(assignmentsConfig.types ?? [])],
136
+ statuses: [...storiesConfig.statuses, ...assignmentsConfig.statuses],
137
+ categories: storiesConfig.categories,
138
+ workflows: { ...storiesConfig.workflows, ...assignmentsConfig.workflows },
139
+ };
140
+ }
141
+
117
142
  export function canReadNewsroomBoard(phase: string | undefined): boolean {
118
143
  return phase === "ready";
119
144
  }
package/lib/site-stack.ts CHANGED
@@ -149,7 +149,7 @@ export const PILOBOL_US_THEME_PACK_TOKENS: ThemePackTokens = {
149
149
  };
150
150
 
151
151
  /**
152
- * Anthus palette, read off the live Gatsby site at `AnthusAI/Anth.us`
152
+ * Anthus palette, read off the live Gatsby site at `AnthusAI/Anth.us-legacy-Gatsby`
153
153
  * (`src/styles/variables.scss` `:root`). Colors only — Shadcn owns typography.
154
154
  * Mapping from that file's names to the pack's slots:
155
155
  *
package/package.json CHANGED
@@ -351,7 +351,7 @@
351
351
  "optional": true
352
352
  }
353
353
  },
354
- "version": "1.6.0",
354
+ "version": "1.7.0",
355
355
  "dependencies": {
356
356
  "@aws-amplify/adapter-nextjs": "^1.7.3",
357
357
  "@aws-crypto/sha256-js": "5.2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.6.0",
2
+ "version": "1.7.0",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/%5F_preview/[[...path]]/route.ts",