@growth-labs/cms 0.5.7 → 0.5.8
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/README.md +5 -0
- package/dist/integration/options.d.ts +26 -0
- package/dist/integration/options.d.ts.map +1 -1
- package/dist/integration/options.js +5 -0
- package/dist/integration/options.js.map +1 -1
- package/dist/routes/content.d.ts.map +1 -1
- package/dist/routes/content.js +6 -1
- package/dist/routes/content.js.map +1 -1
- package/dist/ui/client/mount.d.ts.map +1 -1
- package/dist/ui/client/mount.js +1 -0
- package/dist/ui/client/mount.js.map +1 -1
- package/dist/ui/editor/ContentForm.d.ts +5 -1
- package/dist/ui/editor/ContentForm.d.ts.map +1 -1
- package/dist/ui/editor/ContentForm.js +8 -2
- package/dist/ui/editor/ContentForm.js.map +1 -1
- package/dist/ui/editor/content-payload.d.ts +1 -0
- package/dist/ui/editor/content-payload.d.ts.map +1 -1
- package/dist/ui/editor/content-payload.js +6 -1
- package/dist/ui/editor/content-payload.js.map +1 -1
- package/dist/ui/screens/ContentRoute.d.ts +5 -1
- package/dist/ui/screens/ContentRoute.d.ts.map +1 -1
- package/dist/ui/screens/ContentRoute.js +3 -3
- package/dist/ui/screens/ContentRoute.js.map +1 -1
- package/dist/ui/screens/EditorScreen.d.ts +5 -1
- package/dist/ui/screens/EditorScreen.d.ts.map +1 -1
- package/dist/ui/screens/EditorScreen.js +2 -2
- package/dist/ui/screens/EditorScreen.js.map +1 -1
- package/dist/ui/screens/LibraryScreen.d.ts +5 -1
- package/dist/ui/screens/LibraryScreen.d.ts.map +1 -1
- package/dist/ui/screens/LibraryScreen.js +20 -14
- package/dist/ui/screens/LibraryScreen.js.map +1 -1
- package/dist/ui/screens/library-data.d.ts +6 -0
- package/dist/ui/screens/library-data.d.ts.map +1 -1
- package/dist/ui/screens/library-data.js +8 -1
- package/dist/ui/screens/library-data.js.map +1 -1
- package/dist/ui/screens/registry.d.ts +5 -1
- package/dist/ui/screens/registry.d.ts.map +1 -1
- package/dist/ui/screens/registry.js +2 -2
- package/dist/ui/screens/registry.js.map +1 -1
- package/package.json +1 -1
- package/src/integration/options.ts +6 -0
- package/src/routes/content.ts +7 -1
- package/src/ui/client/mount.tsx +1 -0
- package/src/ui/editor/ContentForm.tsx +30 -0
- package/src/ui/editor/content-payload.ts +6 -1
- package/src/ui/screens/ContentRoute.tsx +10 -1
- package/src/ui/screens/EditorScreen.tsx +3 -0
- package/src/ui/screens/LibraryScreen.tsx +76 -6
- package/src/ui/screens/library-data.ts +12 -1
- package/src/ui/screens/registry.tsx +8 -1
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
filterRows,
|
|
21
21
|
groupByStatus,
|
|
22
22
|
type LibraryRow,
|
|
23
|
+
primaryCategoryLabel,
|
|
23
24
|
type StatusCounts,
|
|
24
25
|
} from './library-data.js'
|
|
25
26
|
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
export interface LibraryScreenProps {
|
|
31
32
|
openDoc: (id: string | null, contentType?: ContentType) => void
|
|
32
33
|
allowInlineAuthorCreation?: boolean
|
|
34
|
+
primaryCategories?: Array<{ slug: string; label: string }>
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
// ---------------------------------------------------------------------------
|
|
@@ -74,6 +76,7 @@ interface LibraryState {
|
|
|
74
76
|
/** The currently active status tab filter. */
|
|
75
77
|
statusFilter: ContentStatus | 'all' | 'processing'
|
|
76
78
|
typeFilter: ContentType | ''
|
|
79
|
+
channelFilter: string
|
|
77
80
|
q: string
|
|
78
81
|
view: 'list' | 'board'
|
|
79
82
|
/** Row IDs currently selected. */
|
|
@@ -93,6 +96,7 @@ type LibraryAction =
|
|
|
93
96
|
| { type: 'fetch-error'; error: string }
|
|
94
97
|
| { type: 'set-status-filter'; status: ContentStatus | 'all' | 'processing' }
|
|
95
98
|
| { type: 'set-type-filter'; contentType: ContentType | '' }
|
|
99
|
+
| { type: 'set-channel-filter'; channel: string }
|
|
96
100
|
| { type: 'set-q'; q: string }
|
|
97
101
|
| { type: 'set-view'; view: 'list' | 'board' }
|
|
98
102
|
| { type: 'toggle-select'; id: string }
|
|
@@ -112,6 +116,7 @@ function initLibrary(): LibraryState {
|
|
|
112
116
|
error: null,
|
|
113
117
|
statusFilter: 'all',
|
|
114
118
|
typeFilter: '',
|
|
119
|
+
channelFilter: '',
|
|
115
120
|
q: '',
|
|
116
121
|
view: 'list',
|
|
117
122
|
selected: new Set(),
|
|
@@ -141,6 +146,8 @@ function libraryReducer(state: LibraryState, action: LibraryAction): LibraryStat
|
|
|
141
146
|
return { ...state, statusFilter: action.status, selected: new Set() }
|
|
142
147
|
case 'set-type-filter':
|
|
143
148
|
return { ...state, typeFilter: action.contentType, selected: new Set() }
|
|
149
|
+
case 'set-channel-filter':
|
|
150
|
+
return { ...state, channelFilter: action.channel, selected: new Set() }
|
|
144
151
|
case 'set-q':
|
|
145
152
|
return { ...state, q: action.q }
|
|
146
153
|
case 'set-view':
|
|
@@ -191,6 +198,7 @@ function getVisibleRows(state: LibraryState): LibraryRow[] {
|
|
|
191
198
|
? (state.statusFilter as ContentStatus)
|
|
192
199
|
: undefined,
|
|
193
200
|
type: state.typeFilter !== '' ? state.typeFilter : undefined,
|
|
201
|
+
channel: state.channelFilter || undefined,
|
|
194
202
|
q: state.q || undefined,
|
|
195
203
|
})
|
|
196
204
|
}
|
|
@@ -199,7 +207,11 @@ function getVisibleRows(state: LibraryState): LibraryRow[] {
|
|
|
199
207
|
// Main component
|
|
200
208
|
// ---------------------------------------------------------------------------
|
|
201
209
|
|
|
202
|
-
export function LibraryScreen({
|
|
210
|
+
export function LibraryScreen({
|
|
211
|
+
openDoc,
|
|
212
|
+
allowInlineAuthorCreation = true,
|
|
213
|
+
primaryCategories = [],
|
|
214
|
+
}: LibraryScreenProps) {
|
|
203
215
|
const { state: ws } = useWorkspace()
|
|
204
216
|
const siteBase = typeof window !== 'undefined' ? window.location.origin : undefined
|
|
205
217
|
const [lib, dispatch] = useReducer(libraryReducer, undefined, initLibrary)
|
|
@@ -217,6 +229,7 @@ export function LibraryScreen({ openDoc, allowInlineAuthorCreation = true }: Lib
|
|
|
217
229
|
params.set('status', lib.statusFilter)
|
|
218
230
|
}
|
|
219
231
|
if (lib.typeFilter) params.set('types', lib.typeFilter)
|
|
232
|
+
if (lib.channelFilter) params.set('channel', lib.channelFilter)
|
|
220
233
|
if (lib.q) params.set('q', lib.q)
|
|
221
234
|
|
|
222
235
|
const [listRes, countsRes] = await Promise.all([
|
|
@@ -247,7 +260,7 @@ export function LibraryScreen({ openDoc, allowInlineAuthorCreation = true }: Lib
|
|
|
247
260
|
} catch (err) {
|
|
248
261
|
dispatch({ type: 'fetch-error', error: String(err) })
|
|
249
262
|
}
|
|
250
|
-
}, [lib.statusFilter, lib.typeFilter, lib.q]) // fetchContent identity changes when filters change → useEffect re-runs
|
|
263
|
+
}, [lib.statusFilter, lib.typeFilter, lib.channelFilter, lib.q]) // fetchContent identity changes when filters change → useEffect re-runs
|
|
251
264
|
|
|
252
265
|
// Initial fetch + refetch when filters change
|
|
253
266
|
useEffect(() => {
|
|
@@ -373,6 +386,24 @@ export function LibraryScreen({ openDoc, allowInlineAuthorCreation = true }: Lib
|
|
|
373
386
|
))}
|
|
374
387
|
</div>
|
|
375
388
|
|
|
389
|
+
{primaryCategories.length > 0 && (
|
|
390
|
+
<select
|
|
391
|
+
value={lib.channelFilter}
|
|
392
|
+
onChange={(event) =>
|
|
393
|
+
dispatch({ type: 'set-channel-filter', channel: event.currentTarget.value })
|
|
394
|
+
}
|
|
395
|
+
aria-label="Content grouping"
|
|
396
|
+
className="input"
|
|
397
|
+
>
|
|
398
|
+
<option value="">All groupings</option>
|
|
399
|
+
{primaryCategories.map((category) => (
|
|
400
|
+
<option key={category.slug} value={category.slug}>
|
|
401
|
+
{category.label}
|
|
402
|
+
</option>
|
|
403
|
+
))}
|
|
404
|
+
</select>
|
|
405
|
+
)}
|
|
406
|
+
|
|
376
407
|
<div style={{ flex: 1 }} />
|
|
377
408
|
|
|
378
409
|
{/* View toggle — segmented control */}
|
|
@@ -457,6 +488,7 @@ export function LibraryScreen({ openDoc, allowInlineAuthorCreation = true }: Lib
|
|
|
457
488
|
(lib.view === 'list' ? (
|
|
458
489
|
<ListView
|
|
459
490
|
rows={visible}
|
|
491
|
+
primaryCategories={primaryCategories}
|
|
460
492
|
selected={lib.selected}
|
|
461
493
|
allSelected={allVisibleSelected}
|
|
462
494
|
onToggleSelect={(id) => dispatch({ type: 'toggle-select', id })}
|
|
@@ -469,7 +501,11 @@ export function LibraryScreen({ openDoc, allowInlineAuthorCreation = true }: Lib
|
|
|
469
501
|
onShare={(row) => dispatch({ type: 'open-share', row })}
|
|
470
502
|
/>
|
|
471
503
|
) : (
|
|
472
|
-
<BoardView
|
|
504
|
+
<BoardView
|
|
505
|
+
rows={visible}
|
|
506
|
+
primaryCategories={primaryCategories}
|
|
507
|
+
onOpen={(row) => openDoc(row.id, row.type)}
|
|
508
|
+
/>
|
|
473
509
|
))}
|
|
474
510
|
|
|
475
511
|
{/* Import modal */}
|
|
@@ -578,6 +614,7 @@ function NewContentMenu({ openDoc }: { openDoc: LibraryScreenProps['openDoc'] })
|
|
|
578
614
|
|
|
579
615
|
function ListView({
|
|
580
616
|
rows,
|
|
617
|
+
primaryCategories,
|
|
581
618
|
selected,
|
|
582
619
|
allSelected,
|
|
583
620
|
onToggleSelect,
|
|
@@ -586,6 +623,7 @@ function ListView({
|
|
|
586
623
|
onShare,
|
|
587
624
|
}: {
|
|
588
625
|
rows: LibraryRow[]
|
|
626
|
+
primaryCategories: Array<{ slug: string; label: string }>
|
|
589
627
|
selected: Set<string>
|
|
590
628
|
allSelected: boolean
|
|
591
629
|
onToggleSelect: (id: string) => void
|
|
@@ -609,6 +647,7 @@ function ListView({
|
|
|
609
647
|
</th>
|
|
610
648
|
<th>Title</th>
|
|
611
649
|
<th style={{ width: 110 }}>Type</th>
|
|
650
|
+
<th style={{ width: 120 }}>Grouping</th>
|
|
612
651
|
<th style={{ width: 120 }}>Status</th>
|
|
613
652
|
<th style={{ width: 150 }}>Author</th>
|
|
614
653
|
<th style={{ width: 130 }}>Updated</th>
|
|
@@ -620,6 +659,7 @@ function ListView({
|
|
|
620
659
|
<ListRow
|
|
621
660
|
key={row.id}
|
|
622
661
|
row={row}
|
|
662
|
+
primaryCategories={primaryCategories}
|
|
623
663
|
isSelected={selected.has(row.id)}
|
|
624
664
|
onSelect={() => onToggleSelect(row.id)}
|
|
625
665
|
onOpen={() => onOpen(row)}
|
|
@@ -635,12 +675,14 @@ function ListView({
|
|
|
635
675
|
|
|
636
676
|
function ListRow({
|
|
637
677
|
row,
|
|
678
|
+
primaryCategories,
|
|
638
679
|
isSelected,
|
|
639
680
|
onSelect,
|
|
640
681
|
onOpen,
|
|
641
682
|
onShare,
|
|
642
683
|
}: {
|
|
643
684
|
row: LibraryRow
|
|
685
|
+
primaryCategories: Array<{ slug: string; label: string }>
|
|
644
686
|
isSelected: boolean
|
|
645
687
|
onSelect: () => void
|
|
646
688
|
onOpen: () => void
|
|
@@ -742,6 +784,10 @@ function ListRow({
|
|
|
742
784
|
</span>
|
|
743
785
|
</td>
|
|
744
786
|
|
|
787
|
+
<td style={{ fontSize: 12.5, color: 'var(--ink-soft)' }}>
|
|
788
|
+
{primaryCategoryLabel(row.channel, primaryCategories)}
|
|
789
|
+
</td>
|
|
790
|
+
|
|
745
791
|
<td>
|
|
746
792
|
<StatusPill row={row} />
|
|
747
793
|
</td>
|
|
@@ -839,7 +885,15 @@ const BOARD_PILL: Record<(typeof BOARD_COLUMNS)[number]['key'], string> = {
|
|
|
839
885
|
published: 'published',
|
|
840
886
|
}
|
|
841
887
|
|
|
842
|
-
function BoardView({
|
|
888
|
+
function BoardView({
|
|
889
|
+
rows,
|
|
890
|
+
primaryCategories,
|
|
891
|
+
onOpen,
|
|
892
|
+
}: {
|
|
893
|
+
rows: LibraryRow[]
|
|
894
|
+
primaryCategories: Array<{ slug: string; label: string }>
|
|
895
|
+
onOpen: (row: LibraryRow) => void
|
|
896
|
+
}) {
|
|
843
897
|
const columns = groupByStatus(rows)
|
|
844
898
|
|
|
845
899
|
return (
|
|
@@ -888,7 +942,12 @@ function BoardView({ rows, onOpen }: { rows: LibraryRow[]; onOpen: (row: Library
|
|
|
888
942
|
</div>
|
|
889
943
|
)}
|
|
890
944
|
{colRows.map((row) => (
|
|
891
|
-
<BoardCard
|
|
945
|
+
<BoardCard
|
|
946
|
+
key={row.id}
|
|
947
|
+
row={row}
|
|
948
|
+
primaryCategories={primaryCategories}
|
|
949
|
+
onOpen={() => onOpen(row)}
|
|
950
|
+
/>
|
|
892
951
|
))}
|
|
893
952
|
</div>
|
|
894
953
|
</div>
|
|
@@ -898,7 +957,15 @@ function BoardView({ rows, onOpen }: { rows: LibraryRow[]; onOpen: (row: Library
|
|
|
898
957
|
)
|
|
899
958
|
}
|
|
900
959
|
|
|
901
|
-
function BoardCard({
|
|
960
|
+
function BoardCard({
|
|
961
|
+
row,
|
|
962
|
+
primaryCategories,
|
|
963
|
+
onOpen,
|
|
964
|
+
}: {
|
|
965
|
+
row: LibraryRow
|
|
966
|
+
primaryCategories: Array<{ slug: string; label: string }>
|
|
967
|
+
onOpen: () => void
|
|
968
|
+
}) {
|
|
902
969
|
const isProcessing =
|
|
903
970
|
row.processingState != null && !['ready', 'failed'].includes(row.processingState ?? '')
|
|
904
971
|
|
|
@@ -990,6 +1057,9 @@ function BoardCard({ row, onOpen }: { row: LibraryRow; onOpen: () => void }) {
|
|
|
990
1057
|
{row.title || '(Untitled)'}
|
|
991
1058
|
</div>
|
|
992
1059
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
1060
|
+
<span style={{ fontSize: 11.5, color: 'var(--ink-muted)' }}>
|
|
1061
|
+
{primaryCategoryLabel(row.channel, primaryCategories)}
|
|
1062
|
+
</span>
|
|
993
1063
|
<span style={{ fontSize: 11.5, color: 'var(--ink-muted)' }}>
|
|
994
1064
|
{row.updatedAt ? formatDate(row.updatedAt) : (row.authorName ?? '—')}
|
|
995
1065
|
</span>
|
|
@@ -25,6 +25,7 @@ export interface LibraryRow {
|
|
|
25
25
|
status: ContentStatus
|
|
26
26
|
slug: string
|
|
27
27
|
title: string
|
|
28
|
+
channel: string | null
|
|
28
29
|
visibility: 'free' | 'premium'
|
|
29
30
|
featured: boolean
|
|
30
31
|
publishAt: string | null
|
|
@@ -62,10 +63,19 @@ export interface BoardColumns {
|
|
|
62
63
|
export interface RowFilter {
|
|
63
64
|
status?: ContentStatus
|
|
64
65
|
type?: ContentType
|
|
66
|
+
channel?: string
|
|
65
67
|
/** Case-insensitive substring match against title and slug. */
|
|
66
68
|
q?: string
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
export function primaryCategoryLabel(
|
|
72
|
+
channel: string | null,
|
|
73
|
+
categories: Array<{ slug: string; label: string }>,
|
|
74
|
+
): string {
|
|
75
|
+
if (!channel) return '—'
|
|
76
|
+
return categories.find((category) => category.slug === channel)?.label ?? channel
|
|
77
|
+
}
|
|
78
|
+
|
|
69
79
|
const NON_PROCESSING_STATES = new Set(['ready', 'failed'])
|
|
70
80
|
|
|
71
81
|
/** Returns true when this row represents a video mid-flight in Foundry. */
|
|
@@ -161,12 +171,13 @@ export function groupByStatus(rows: LibraryRow[]): BoardColumns {
|
|
|
161
171
|
* matching the server-side LIKE behaviour so the list result is consistent.
|
|
162
172
|
*/
|
|
163
173
|
export function filterRows(rows: LibraryRow[], filter: RowFilter): LibraryRow[] {
|
|
164
|
-
const { status, type, q } = filter
|
|
174
|
+
const { status, type, channel, q } = filter
|
|
165
175
|
const lowerQ = q ? q.toLowerCase() : null
|
|
166
176
|
|
|
167
177
|
return rows.filter((row) => {
|
|
168
178
|
if (status !== undefined && row.status !== status) return false
|
|
169
179
|
if (type !== undefined && row.type !== type) return false
|
|
180
|
+
if (channel !== undefined && row.channel !== channel) return false
|
|
170
181
|
if (lowerQ !== null) {
|
|
171
182
|
const titleMatch = row.title.toLowerCase().includes(lowerQ)
|
|
172
183
|
const slugMatch = row.slug.toLowerCase().includes(lowerQ)
|
|
@@ -41,13 +41,20 @@ function CalendarRoute() {
|
|
|
41
41
|
|
|
42
42
|
export function createCmsScreens({
|
|
43
43
|
allowInlineAuthorCreation = true,
|
|
44
|
+
primaryCategories = [],
|
|
44
45
|
surveyId,
|
|
45
46
|
}: {
|
|
46
47
|
allowInlineAuthorCreation?: boolean
|
|
48
|
+
primaryCategories?: Array<{ slug: string; label: string }>
|
|
47
49
|
surveyId?: string
|
|
48
50
|
} = {}): ScreenRegistry {
|
|
49
51
|
return {
|
|
50
|
-
content: () =>
|
|
52
|
+
content: () => (
|
|
53
|
+
<ContentRoute
|
|
54
|
+
allowInlineAuthorCreation={allowInlineAuthorCreation}
|
|
55
|
+
primaryCategories={primaryCategories}
|
|
56
|
+
/>
|
|
57
|
+
),
|
|
51
58
|
'content-insights': () => <ContentInsightsScreen />,
|
|
52
59
|
dashboard: () => <DashboardRoute />,
|
|
53
60
|
calendar: () => <CalendarRoute />,
|