@growth-labs/cms 0.5.4 → 0.5.6
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 +55 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/integration/index.d.ts.map +1 -1
- package/dist/integration/index.js +9 -0
- package/dist/integration/index.js.map +1 -1
- package/dist/integration/options.d.ts +38 -0
- package/dist/integration/options.d.ts.map +1 -1
- package/dist/integration/options.js +13 -0
- package/dist/integration/options.js.map +1 -1
- package/dist/providers/null.d.ts +2 -1
- package/dist/providers/null.d.ts.map +1 -1
- package/dist/providers/null.js +9 -0
- package/dist/providers/null.js.map +1 -1
- package/dist/providers/types.d.ts +10 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/routes/authors.d.ts.map +1 -1
- package/dist/routes/authors.js +33 -8
- package/dist/routes/authors.js.map +1 -1
- package/dist/routes/authz-matrix.d.ts.map +1 -1
- package/dist/routes/authz-matrix.js +2 -0
- package/dist/routes/authz-matrix.js.map +1 -1
- package/dist/routes/context.d.ts +1 -1
- package/dist/routes/context.d.ts.map +1 -1
- package/dist/routes/context.js.map +1 -1
- package/dist/routes/index.d.ts +1 -0
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +2 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/recovery-errors.d.ts +21 -0
- package/dist/routes/recovery-errors.d.ts.map +1 -0
- package/dist/routes/recovery-errors.js +53 -0
- package/dist/routes/recovery-errors.js.map +1 -0
- package/dist/routes/surveys.d.ts +12 -0
- package/dist/routes/surveys.d.ts.map +1 -0
- package/dist/routes/surveys.js +109 -0
- package/dist/routes/surveys.js.map +1 -0
- package/dist/schema/insights-ingest.d.ts +46 -46
- package/dist/surveys/access.d.ts +4 -0
- package/dist/surveys/access.d.ts.map +1 -0
- package/dist/surveys/access.js +9 -0
- package/dist/surveys/access.js.map +1 -0
- package/dist/surveys/errors.d.ts +43 -0
- package/dist/surveys/errors.d.ts.map +1 -0
- package/dist/surveys/errors.js +42 -0
- package/dist/surveys/errors.js.map +1 -0
- package/dist/surveys/schema.d.ts +939 -0
- package/dist/surveys/schema.d.ts.map +1 -0
- package/dist/surveys/schema.js +385 -0
- package/dist/surveys/schema.js.map +1 -0
- package/dist/ui/api/surveys/[surveyId]/responses.csv.d.ts +3 -0
- package/dist/ui/api/surveys/[surveyId]/responses.csv.d.ts.map +1 -0
- package/dist/ui/api/surveys/[surveyId]/responses.csv.js +17 -0
- package/dist/ui/api/surveys/[surveyId]/responses.csv.js.map +1 -0
- package/dist/ui/api/surveys/[surveyId].d.ts +3 -0
- package/dist/ui/api/surveys/[surveyId].d.ts.map +1 -0
- package/dist/ui/api/surveys/[surveyId].js +17 -0
- package/dist/ui/api/surveys/[surveyId].js.map +1 -0
- package/dist/ui/client/mount.d.ts.map +1 -1
- package/dist/ui/client/mount.js +6 -2
- package/dist/ui/client/mount.js.map +1 -1
- package/dist/ui/commands.d.ts +5 -1
- package/dist/ui/commands.d.ts.map +1 -1
- package/dist/ui/commands.js +7 -0
- package/dist/ui/commands.js.map +1 -1
- package/dist/ui/components/CmsApp.d.ts +2 -0
- package/dist/ui/components/CmsApp.d.ts.map +1 -1
- package/dist/ui/components/CmsApp.js +14 -3
- package/dist/ui/components/CmsApp.js.map +1 -1
- package/dist/ui/components/CommandPalette.d.ts +2 -0
- package/dist/ui/components/CommandPalette.d.ts.map +1 -1
- package/dist/ui/components/CommandPalette.js +1 -1
- package/dist/ui/components/CommandPalette.js.map +1 -1
- package/dist/ui/components/ShareModal.d.ts +4 -1
- package/dist/ui/components/ShareModal.d.ts.map +1 -1
- package/dist/ui/components/ShareModal.js +39 -9
- package/dist/ui/components/ShareModal.js.map +1 -1
- package/dist/ui/components/SharePickers.d.ts +29 -2
- package/dist/ui/components/SharePickers.d.ts.map +1 -1
- package/dist/ui/components/SharePickers.js +33 -8
- package/dist/ui/components/SharePickers.js.map +1 -1
- package/dist/ui/components/Sidebar.d.ts +1 -0
- package/dist/ui/components/Sidebar.d.ts.map +1 -1
- package/dist/ui/components/Sidebar.js +1 -1
- package/dist/ui/components/Sidebar.js.map +1 -1
- package/dist/ui/hash-router.d.ts +1 -1
- package/dist/ui/hash-router.d.ts.map +1 -1
- package/dist/ui/hash-router.js +1 -0
- package/dist/ui/hash-router.js.map +1 -1
- package/dist/ui/nav.d.ts +5 -1
- package/dist/ui/nav.d.ts.map +1 -1
- package/dist/ui/nav.js +10 -2
- package/dist/ui/nav.js.map +1 -1
- package/dist/ui/screens/AuthorsScreen.d.ts.map +1 -1
- package/dist/ui/screens/AuthorsScreen.js +2 -1
- package/dist/ui/screens/AuthorsScreen.js.map +1 -1
- package/dist/ui/screens/ContentRoute.d.ts +3 -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 +2 -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 +2 -1
- package/dist/ui/screens/LibraryScreen.d.ts.map +1 -1
- package/dist/ui/screens/LibraryScreen.js +2 -2
- package/dist/ui/screens/LibraryScreen.js.map +1 -1
- package/dist/ui/screens/SocialShareScreen.d.ts +3 -1
- package/dist/ui/screens/SocialShareScreen.d.ts.map +1 -1
- package/dist/ui/screens/SocialShareScreen.js +20 -8
- package/dist/ui/screens/SocialShareScreen.js.map +1 -1
- package/dist/ui/screens/SurveyResponsesScreen.d.ts +28 -0
- package/dist/ui/screens/SurveyResponsesScreen.d.ts.map +1 -0
- package/dist/ui/screens/SurveyResponsesScreen.js +110 -0
- package/dist/ui/screens/SurveyResponsesScreen.js.map +1 -0
- package/dist/ui/screens/author-recovery.d.ts +11 -0
- package/dist/ui/screens/author-recovery.d.ts.map +1 -0
- package/dist/ui/screens/author-recovery.js +18 -0
- package/dist/ui/screens/author-recovery.js.map +1 -0
- package/dist/ui/screens/registry.d.ts +4 -0
- package/dist/ui/screens/registry.d.ts.map +1 -1
- package/dist/ui/screens/registry.js +20 -13
- package/dist/ui/screens/registry.js.map +1 -1
- package/dist/ui/screens/social-share-data.d.ts +23 -2
- package/dist/ui/screens/social-share-data.d.ts.map +1 -1
- package/dist/ui/screens/social-share-data.js +57 -11
- package/dist/ui/screens/social-share-data.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +2 -2
- package/src/integration/index.ts +9 -0
- package/src/integration/options.ts +15 -0
- package/src/providers/null.ts +19 -0
- package/src/providers/types.ts +7 -0
- package/src/routes/authors.ts +41 -20
- package/src/routes/authz-matrix.ts +2 -0
- package/src/routes/context.ts +2 -0
- package/src/routes/index.ts +6 -0
- package/src/routes/recovery-errors.ts +70 -0
- package/src/routes/surveys.ts +130 -0
- package/src/surveys/access.ts +11 -0
- package/src/surveys/errors.ts +51 -0
- package/src/surveys/schema.ts +435 -0
- package/src/ui/api/surveys/[surveyId]/responses.csv.ts +23 -0
- package/src/ui/api/surveys/[surveyId].ts +23 -0
- package/src/ui/client/mount.tsx +6 -2
- package/src/ui/commands.ts +14 -1
- package/src/ui/components/CmsApp.tsx +25 -3
- package/src/ui/components/CommandPalette.tsx +2 -1
- package/src/ui/components/ShareModal.tsx +102 -19
- package/src/ui/components/SharePickers.tsx +76 -27
- package/src/ui/components/Sidebar.tsx +10 -2
- package/src/ui/hash-router.ts +1 -0
- package/src/ui/nav.ts +15 -2
- package/src/ui/screens/AuthorsScreen.tsx +7 -2
- package/src/ui/screens/ContentRoute.tsx +13 -3
- package/src/ui/screens/EditorScreen.tsx +7 -1
- package/src/ui/screens/LibraryScreen.tsx +3 -1
- package/src/ui/screens/SocialShareScreen.tsx +53 -6
- package/src/ui/screens/SurveyResponsesScreen.tsx +271 -0
- package/src/ui/screens/author-recovery.ts +32 -0
- package/src/ui/screens/registry.tsx +28 -12
- package/src/ui/screens/social-share-data.ts +92 -13
|
@@ -26,10 +26,12 @@ import {
|
|
|
26
26
|
BOOST_MODES,
|
|
27
27
|
type BoostMode,
|
|
28
28
|
buildBatchPayloadFrom,
|
|
29
|
+
canonicalAuthorRosterStatus,
|
|
29
30
|
formatShareOutput,
|
|
30
31
|
initShareForm,
|
|
31
32
|
type LinkResult,
|
|
32
33
|
relevantFields,
|
|
34
|
+
requiresCanonicalAuthorRoster,
|
|
33
35
|
SHARE_MODES,
|
|
34
36
|
type ShareFormPrefill,
|
|
35
37
|
type ShareMode,
|
|
@@ -41,6 +43,7 @@ import {
|
|
|
41
43
|
toPlainText,
|
|
42
44
|
YOUTUBE_BUNDLE_PLACEMENTS,
|
|
43
45
|
} from '../screens/social-share-data.js'
|
|
46
|
+
import { type ShareLinksData, useShareLinksData } from './SharePickers.js'
|
|
44
47
|
|
|
45
48
|
// ---------------------------------------------------------------------------
|
|
46
49
|
// Props
|
|
@@ -62,6 +65,8 @@ export interface ShareModalPrefill {
|
|
|
62
65
|
export interface ShareModalProps {
|
|
63
66
|
prefill: ShareModalPrefill
|
|
64
67
|
onClose: () => void
|
|
68
|
+
allowInlineAuthorCreation?: boolean
|
|
69
|
+
shareLinksData?: ShareLinksData
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
// ---------------------------------------------------------------------------
|
|
@@ -79,7 +84,12 @@ const MODE_LABELS: Record<ShareMode, string> = {
|
|
|
79
84
|
// ShareModal
|
|
80
85
|
// ---------------------------------------------------------------------------
|
|
81
86
|
|
|
82
|
-
export function ShareModal({
|
|
87
|
+
export function ShareModal({
|
|
88
|
+
prefill,
|
|
89
|
+
onClose,
|
|
90
|
+
allowInlineAuthorCreation = true,
|
|
91
|
+
shareLinksData: suppliedShareLinksData,
|
|
92
|
+
}: ShareModalProps) {
|
|
83
93
|
const initialPrefill: ShareFormPrefill = {
|
|
84
94
|
destinationUrl: prefill.destinationUrl,
|
|
85
95
|
contentSlug: prefill.contentSlug,
|
|
@@ -97,6 +107,23 @@ export function ShareModal({ prefill, onClose }: ShareModalProps) {
|
|
|
97
107
|
const [submitError, setSubmitError] = useState<string | null>(null)
|
|
98
108
|
const [output, setOutput] = useState<ShareOutput | null>(null)
|
|
99
109
|
const [copied, setCopied] = useState(false)
|
|
110
|
+
const shouldVerifyAuthorRoster = requiresCanonicalAuthorRoster(
|
|
111
|
+
state.mode,
|
|
112
|
+
allowInlineAuthorCreation,
|
|
113
|
+
)
|
|
114
|
+
const fetchedShareLinksData = useShareLinksData(
|
|
115
|
+
!suppliedShareLinksData && !allowInlineAuthorCreation,
|
|
116
|
+
)
|
|
117
|
+
const shareLinksData = suppliedShareLinksData ?? fetchedShareLinksData
|
|
118
|
+
const authorRosterStatus = canonicalAuthorRosterStatus({
|
|
119
|
+
enabled: shouldVerifyAuthorRoster,
|
|
120
|
+
author: state.author,
|
|
121
|
+
authors: shareLinksData.authors,
|
|
122
|
+
loading: shareLinksData.loading,
|
|
123
|
+
error: shareLinksData.error,
|
|
124
|
+
})
|
|
125
|
+
const authorRosterFailure =
|
|
126
|
+
authorRosterStatus.kind === 'out-of-sync' || authorRosterStatus.kind === 'unavailable'
|
|
100
127
|
|
|
101
128
|
const goBase = typeof window !== 'undefined' ? `${window.location.origin}/go` : '/go'
|
|
102
129
|
|
|
@@ -114,6 +141,11 @@ export function ShareModal({ prefill, onClose }: ShareModalProps) {
|
|
|
114
141
|
setSubmitError(null)
|
|
115
142
|
setOutput(null)
|
|
116
143
|
|
|
144
|
+
if (authorRosterStatus.blocked) {
|
|
145
|
+
setSubmitError(authorRosterStatus.message)
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
117
149
|
const result = buildBatchPayloadFrom(state)
|
|
118
150
|
if (!result.ok) {
|
|
119
151
|
setSubmitError(result.error)
|
|
@@ -142,7 +174,7 @@ export function ShareModal({ prefill, onClose }: ShareModalProps) {
|
|
|
142
174
|
setSubmitting(false)
|
|
143
175
|
}
|
|
144
176
|
},
|
|
145
|
-
[state, goBase],
|
|
177
|
+
[state, goBase, authorRosterStatus],
|
|
146
178
|
)
|
|
147
179
|
|
|
148
180
|
const handleCopyAll = useCallback(() => {
|
|
@@ -353,22 +385,69 @@ export function ShareModal({ prefill, onClose }: ShareModalProps) {
|
|
|
353
385
|
{/* Author ref (inline — simplified for the modal; full picker in the screen) */}
|
|
354
386
|
{fields.includes('author') && (
|
|
355
387
|
<div style={fieldGroup}>
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
388
|
+
{allowInlineAuthorCreation ? (
|
|
389
|
+
<>
|
|
390
|
+
<label htmlFor="sm-author" className="field-label">
|
|
391
|
+
Author display name
|
|
392
|
+
</label>
|
|
393
|
+
<input
|
|
394
|
+
id="sm-author"
|
|
395
|
+
type="text"
|
|
396
|
+
className="input"
|
|
397
|
+
value={state.author.displayName ?? ''}
|
|
398
|
+
onChange={(e) =>
|
|
399
|
+
dispatch({
|
|
400
|
+
type: 'set-author',
|
|
401
|
+
ref: {
|
|
402
|
+
...state.author,
|
|
403
|
+
displayName: e.currentTarget.value,
|
|
404
|
+
create: true,
|
|
405
|
+
},
|
|
406
|
+
})
|
|
407
|
+
}
|
|
408
|
+
placeholder="Jane Smith"
|
|
409
|
+
/>
|
|
410
|
+
</>
|
|
411
|
+
) : authorRosterFailure ? (
|
|
412
|
+
<div
|
|
413
|
+
role="alert"
|
|
414
|
+
style={{
|
|
415
|
+
padding: '10px 12px',
|
|
416
|
+
border: '1px solid color-mix(in srgb, var(--red) 25%, transparent)',
|
|
417
|
+
borderRadius: 'var(--radius-sm)',
|
|
418
|
+
color: 'var(--red)',
|
|
419
|
+
}}
|
|
420
|
+
>
|
|
421
|
+
{authorRosterStatus.message}
|
|
422
|
+
</div>
|
|
423
|
+
) : state.author.id || state.author.slug || state.author.displayName ? (
|
|
424
|
+
<>
|
|
425
|
+
<div className="field-label">Canonical author</div>
|
|
426
|
+
<div className="input">
|
|
427
|
+
{state.author.displayName || state.author.slug || state.author.id}
|
|
428
|
+
</div>
|
|
429
|
+
{authorRosterStatus.kind === 'loading' && (
|
|
430
|
+
<div className="help" style={{ marginTop: 6 }}>
|
|
431
|
+
{authorRosterStatus.message}
|
|
432
|
+
</div>
|
|
433
|
+
)}
|
|
434
|
+
</>
|
|
435
|
+
) : state.mode === 'author-social' ? (
|
|
436
|
+
<div
|
|
437
|
+
role="alert"
|
|
438
|
+
style={{
|
|
439
|
+
padding: '10px 12px',
|
|
440
|
+
border: '1px solid color-mix(in srgb, var(--red) 25%, transparent)',
|
|
441
|
+
borderRadius: 'var(--radius-sm)',
|
|
442
|
+
color: 'var(--red)',
|
|
443
|
+
}}
|
|
444
|
+
>
|
|
445
|
+
No canonical author is linked to this content. Add or publish the author in the
|
|
446
|
+
Authors screen, then retry.
|
|
447
|
+
</div>
|
|
448
|
+
) : (
|
|
449
|
+
<div className="help">No author attribution selected (optional).</div>
|
|
450
|
+
)}
|
|
372
451
|
</div>
|
|
373
452
|
)}
|
|
374
453
|
|
|
@@ -514,7 +593,11 @@ export function ShareModal({ prefill, onClose }: ShareModalProps) {
|
|
|
514
593
|
<button type="button" className="btn" onClick={onClose}>
|
|
515
594
|
Cancel
|
|
516
595
|
</button>
|
|
517
|
-
<button
|
|
596
|
+
<button
|
|
597
|
+
type="submit"
|
|
598
|
+
className="btn primary"
|
|
599
|
+
disabled={submitting || authorRosterStatus.blocked}
|
|
600
|
+
>
|
|
518
601
|
<Icon name="share" size={14} />
|
|
519
602
|
{submitting ? 'Minting…' : 'Mint link(s)'}
|
|
520
603
|
</button>
|
|
@@ -27,11 +27,14 @@
|
|
|
27
27
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
28
28
|
import { Icon } from '../icons.js'
|
|
29
29
|
import { publicUrlFor } from '../screens/public-url.js'
|
|
30
|
-
import
|
|
31
|
-
AuthorRef,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
import {
|
|
31
|
+
type AuthorRef,
|
|
32
|
+
type CanonicalAuthorEntry,
|
|
33
|
+
canonicalAuthorRosterStatus,
|
|
34
|
+
type HandleRef,
|
|
35
|
+
requiresCanonicalAuthorRoster,
|
|
36
|
+
type ShareFormAction,
|
|
37
|
+
type ShareMode,
|
|
35
38
|
} from '../screens/social-share-data.js'
|
|
36
39
|
|
|
37
40
|
// ---------------------------------------------------------------------------
|
|
@@ -39,11 +42,7 @@ import type {
|
|
|
39
42
|
// SOURCE OF TRUTH: @growth-labs/analytics src/routes/share-links.ts GET response
|
|
40
43
|
// ---------------------------------------------------------------------------
|
|
41
44
|
|
|
42
|
-
interface AuthorEntry {
|
|
43
|
-
id: string
|
|
44
|
-
slug: string
|
|
45
|
-
displayName: string
|
|
46
|
-
}
|
|
45
|
+
export interface AuthorEntry extends CanonicalAuthorEntry {}
|
|
47
46
|
|
|
48
47
|
interface HandleEntry {
|
|
49
48
|
id: string
|
|
@@ -60,6 +59,13 @@ interface ShareLinksListResponse {
|
|
|
60
59
|
links: unknown[]
|
|
61
60
|
}
|
|
62
61
|
|
|
62
|
+
export interface ShareLinksData {
|
|
63
|
+
authors: AuthorEntry[]
|
|
64
|
+
handles: HandleEntry[]
|
|
65
|
+
loading: boolean
|
|
66
|
+
error: string | null
|
|
67
|
+
}
|
|
68
|
+
|
|
63
69
|
// LibraryRow subset — only what the content picker needs.
|
|
64
70
|
// SOURCE OF TRUTH: packages/cms/src/ui/screens/library-data.ts LibraryRow
|
|
65
71
|
interface ContentRow {
|
|
@@ -322,20 +328,21 @@ export function ContentPicker({ siteBase, dispatch }: ContentPickerProps) {
|
|
|
322
328
|
* so the component is self-contained; in practice the two fetches are identical
|
|
323
329
|
* and the browser deduplicates via HTTP cache / same-tick coalescing).
|
|
324
330
|
*/
|
|
325
|
-
function useShareLinksData(): {
|
|
326
|
-
authors: AuthorEntry[]
|
|
327
|
-
handles: HandleEntry[]
|
|
328
|
-
loading: boolean
|
|
329
|
-
error: string | null
|
|
330
|
-
} {
|
|
331
|
+
export function useShareLinksData(enabled = true): ShareLinksData {
|
|
331
332
|
const [authors, setAuthors] = useState<AuthorEntry[]>([])
|
|
332
333
|
const [handles, setHandles] = useState<HandleEntry[]>([])
|
|
333
|
-
const [loading, setLoading] = useState(
|
|
334
|
+
const [loading, setLoading] = useState(enabled)
|
|
334
335
|
const [error, setError] = useState<string | null>(null)
|
|
335
336
|
|
|
336
337
|
useEffect(() => {
|
|
338
|
+
if (!enabled) {
|
|
339
|
+
setLoading(false)
|
|
340
|
+
return
|
|
341
|
+
}
|
|
337
342
|
let cancelled = false
|
|
338
343
|
async function load() {
|
|
344
|
+
setLoading(true)
|
|
345
|
+
setError(null)
|
|
339
346
|
try {
|
|
340
347
|
const res = await fetch('/api/share-links')
|
|
341
348
|
if (!res.ok) {
|
|
@@ -357,7 +364,7 @@ function useShareLinksData(): {
|
|
|
357
364
|
return () => {
|
|
358
365
|
cancelled = true
|
|
359
366
|
}
|
|
360
|
-
}, [])
|
|
367
|
+
}, [enabled])
|
|
361
368
|
|
|
362
369
|
return { authors, handles, loading, error }
|
|
363
370
|
}
|
|
@@ -418,6 +425,8 @@ export interface AuthorPickerProps {
|
|
|
418
425
|
ref: AuthorRef
|
|
419
426
|
dispatch: React.Dispatch<ShareFormAction>
|
|
420
427
|
mode: ShareMode
|
|
428
|
+
allowInlineAuthorCreation?: boolean
|
|
429
|
+
shareLinksData?: ShareLinksData
|
|
421
430
|
}
|
|
422
431
|
|
|
423
432
|
/**
|
|
@@ -425,8 +434,15 @@ export interface AuthorPickerProps {
|
|
|
425
434
|
* Supports inline-create via `create:true` + `displayName`.
|
|
426
435
|
* Required for author-social; optional for newsletter/youtube.
|
|
427
436
|
*/
|
|
428
|
-
export function AuthorPicker({
|
|
429
|
-
|
|
437
|
+
export function AuthorPicker({
|
|
438
|
+
ref: authorRef,
|
|
439
|
+
dispatch,
|
|
440
|
+
mode,
|
|
441
|
+
allowInlineAuthorCreation = true,
|
|
442
|
+
shareLinksData,
|
|
443
|
+
}: AuthorPickerProps) {
|
|
444
|
+
const fetchedShareLinksData = useShareLinksData(!shareLinksData)
|
|
445
|
+
const { authors, loading, error } = shareLinksData ?? fetchedShareLinksData
|
|
430
446
|
const isRequired = mode === 'author-social'
|
|
431
447
|
const [addNew, setAddNew] = useState(false)
|
|
432
448
|
const [newName, setNewName] = useState('')
|
|
@@ -434,7 +450,19 @@ export function AuthorPicker({ ref: authorRef, dispatch, mode }: AuthorPickerPro
|
|
|
434
450
|
// If an existing author ref (with id/slug) is set, show a chip
|
|
435
451
|
const selectedExisting = authorRef.id
|
|
436
452
|
? (authors.find((a) => a.id === authorRef.id) ?? null)
|
|
437
|
-
:
|
|
453
|
+
: authorRef.slug
|
|
454
|
+
? (authors.find(
|
|
455
|
+
(a) => a.slug.trim().toLowerCase() === authorRef.slug?.trim().toLowerCase(),
|
|
456
|
+
) ?? null)
|
|
457
|
+
: null
|
|
458
|
+
const rosterStatus = canonicalAuthorRosterStatus({
|
|
459
|
+
enabled: requiresCanonicalAuthorRoster(mode, allowInlineAuthorCreation),
|
|
460
|
+
author: authorRef,
|
|
461
|
+
authors,
|
|
462
|
+
loading,
|
|
463
|
+
error,
|
|
464
|
+
})
|
|
465
|
+
const rosterDrift = rosterStatus.kind === 'out-of-sync' || rosterStatus.kind === 'unavailable'
|
|
438
466
|
|
|
439
467
|
const handleSelectExisting = (author: AuthorEntry) => {
|
|
440
468
|
dispatch({
|
|
@@ -465,8 +493,22 @@ export function AuthorPicker({ ref: authorRef, dispatch, mode }: AuthorPickerPro
|
|
|
465
493
|
<div>
|
|
466
494
|
<FieldLabel required={isRequired}>{isRequired ? 'Author' : 'Author (optional)'}</FieldLabel>
|
|
467
495
|
|
|
496
|
+
{rosterDrift && (
|
|
497
|
+
<div
|
|
498
|
+
role="alert"
|
|
499
|
+
style={{
|
|
500
|
+
padding: '10px 12px',
|
|
501
|
+
border: '1px solid color-mix(in srgb, var(--red) 25%, transparent)',
|
|
502
|
+
borderRadius: 'var(--radius-sm)',
|
|
503
|
+
color: 'var(--red)',
|
|
504
|
+
}}
|
|
505
|
+
>
|
|
506
|
+
{rosterStatus.message}
|
|
507
|
+
</div>
|
|
508
|
+
)}
|
|
509
|
+
|
|
468
510
|
{/* Show current selection as a chip */}
|
|
469
|
-
{displayValue ? (
|
|
511
|
+
{!rosterDrift && displayValue ? (
|
|
470
512
|
<SelectionChip
|
|
471
513
|
icon="users"
|
|
472
514
|
label={displayValue}
|
|
@@ -474,7 +516,7 @@ export function AuthorPicker({ ref: authorRef, dispatch, mode }: AuthorPickerPro
|
|
|
474
516
|
onClear={handleClear}
|
|
475
517
|
clearTitle="Clear author"
|
|
476
518
|
/>
|
|
477
|
-
) : (
|
|
519
|
+
) : !rosterDrift ? (
|
|
478
520
|
<>
|
|
479
521
|
{/* Existing authors list */}
|
|
480
522
|
{loading && (
|
|
@@ -521,12 +563,14 @@ export function AuthorPicker({ ref: authorRef, dispatch, mode }: AuthorPickerPro
|
|
|
521
563
|
className="panel panel-pad help"
|
|
522
564
|
style={{ margin: '0 0 8px', padding: '10px 12px' }}
|
|
523
565
|
>
|
|
524
|
-
|
|
566
|
+
{allowInlineAuthorCreation
|
|
567
|
+
? 'No authors yet — add one below.'
|
|
568
|
+
: 'No synchronized authors are available. Create and publish the author in the canonical Authors screen, then retry.'}
|
|
525
569
|
</div>
|
|
526
570
|
)}
|
|
527
571
|
|
|
528
572
|
{/* Inline add new */}
|
|
529
|
-
{addNew ? (
|
|
573
|
+
{allowInlineAuthorCreation && addNew ? (
|
|
530
574
|
<div style={{ display: 'flex', gap: 8 }}>
|
|
531
575
|
<input
|
|
532
576
|
type="text"
|
|
@@ -555,7 +599,7 @@ export function AuthorPicker({ ref: authorRef, dispatch, mode }: AuthorPickerPro
|
|
|
555
599
|
<Icon name="x" size={13} />
|
|
556
600
|
</button>
|
|
557
601
|
</div>
|
|
558
|
-
) : (
|
|
602
|
+
) : allowInlineAuthorCreation ? (
|
|
559
603
|
<button
|
|
560
604
|
type="button"
|
|
561
605
|
className="btn sm ghost"
|
|
@@ -565,8 +609,13 @@ export function AuthorPicker({ ref: authorRef, dispatch, mode }: AuthorPickerPro
|
|
|
565
609
|
<Icon name="plus" size={13} />
|
|
566
610
|
Add new author
|
|
567
611
|
</button>
|
|
568
|
-
)}
|
|
612
|
+
) : null}
|
|
569
613
|
</>
|
|
614
|
+
) : null}
|
|
615
|
+
{!rosterDrift && rosterStatus.kind === 'loading' && (
|
|
616
|
+
<div className="help" style={{ margin: '6px 0 0' }}>
|
|
617
|
+
{rosterStatus.message}
|
|
618
|
+
</div>
|
|
570
619
|
)}
|
|
571
620
|
</div>
|
|
572
621
|
)
|
|
@@ -5,10 +5,18 @@ import { navItems } from '../nav.js'
|
|
|
5
5
|
import { useWorkspace } from '../workspace-context.js'
|
|
6
6
|
import { SiteSwitcher } from './SiteSwitcher.js'
|
|
7
7
|
|
|
8
|
-
export function Sidebar(props: {
|
|
8
|
+
export function Sidebar(props: {
|
|
9
|
+
route: Route
|
|
10
|
+
navigate: (r: Route) => void
|
|
11
|
+
collapsed: boolean
|
|
12
|
+
surveyResultsEnabled?: boolean
|
|
13
|
+
}) {
|
|
9
14
|
const { state } = useWorkspace()
|
|
10
|
-
const groups = navItems()
|
|
11
15
|
const user = state.user
|
|
16
|
+
const groups = navItems(
|
|
17
|
+
{},
|
|
18
|
+
{ surveyResultsEnabled: props.surveyResultsEnabled, role: user?.role },
|
|
19
|
+
)
|
|
12
20
|
const initial = user?.name?.[0]?.toUpperCase() ?? user?.email?.[0]?.toUpperCase() ?? '?'
|
|
13
21
|
|
|
14
22
|
return (
|
package/src/ui/hash-router.ts
CHANGED
package/src/ui/nav.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// src/ui/nav.ts — the Masthead sidebar nav model. Two groups, 11 items
|
|
2
2
|
// (spec §6: Social Share added between Subscriptions and Settings).
|
|
3
3
|
|
|
4
|
+
import type { CmsRole } from '../schema/types.js'
|
|
5
|
+
import { canViewSurveyResponses } from '../surveys/access.js'
|
|
4
6
|
import type { Route } from './hash-router.js'
|
|
5
7
|
import type { IconName } from './icons.js'
|
|
6
8
|
|
|
@@ -33,6 +35,7 @@ export const NAV_MODEL: NavGroup[] = [
|
|
|
33
35
|
{ id: 'topics', label: 'Topics & tags', icon: 'tag' },
|
|
34
36
|
{ id: 'content-insights', label: 'Content Insights', icon: 'sparkle' },
|
|
35
37
|
{ id: 'subscriptions', label: 'Subscriptions', icon: 'inbox' },
|
|
38
|
+
{ id: 'survey-responses', label: 'Survey responses', icon: 'chart' },
|
|
36
39
|
{ id: 'social-share', label: 'Social Share', icon: 'share' },
|
|
37
40
|
{ id: 'settings', label: 'Settings', icon: 'settings' },
|
|
38
41
|
],
|
|
@@ -40,9 +43,19 @@ export const NAV_MODEL: NavGroup[] = [
|
|
|
40
43
|
]
|
|
41
44
|
|
|
42
45
|
/** Build the nav with live counts merged onto matching item ids. */
|
|
43
|
-
export function navItems(
|
|
46
|
+
export function navItems(
|
|
47
|
+
counts: Partial<Record<Route, number>> = {},
|
|
48
|
+
options: { surveyResultsEnabled?: boolean; role?: CmsRole | null } = {
|
|
49
|
+
surveyResultsEnabled: true,
|
|
50
|
+
role: 'owner',
|
|
51
|
+
},
|
|
52
|
+
): NavGroup[] {
|
|
53
|
+
const canViewSurvey =
|
|
54
|
+
options.surveyResultsEnabled !== false && canViewSurveyResponses(options.role)
|
|
44
55
|
return NAV_MODEL.map((g) => ({
|
|
45
56
|
group: g.group,
|
|
46
|
-
items: g.items
|
|
57
|
+
items: g.items
|
|
58
|
+
.filter((item) => item.id !== 'survey-responses' || canViewSurvey)
|
|
59
|
+
.map((i) => (counts[i.id] != null ? { ...i, count: counts[i.id] } : { ...i })),
|
|
47
60
|
}))
|
|
48
61
|
}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import { useCallback, useEffect, useReducer, useState } from 'react'
|
|
24
24
|
import { Icon } from '../icons.js'
|
|
25
|
+
import { formatAuthorWriteFailure } from './author-recovery.js'
|
|
25
26
|
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
27
28
|
// Data shape — mirrors serializeAuthor() in src/routes/authors.ts
|
|
@@ -182,11 +183,15 @@ export function AuthorsScreen() {
|
|
|
182
183
|
})
|
|
183
184
|
|
|
184
185
|
if (!res.ok) {
|
|
185
|
-
const detail = (await res.json().catch(() => null)) as {
|
|
186
|
+
const detail = (await res.json().catch(() => null)) as {
|
|
187
|
+
error?: string
|
|
188
|
+
errorClass?: string
|
|
189
|
+
author?: { id?: string; slug?: string }
|
|
190
|
+
} | null
|
|
186
191
|
setEditor((e) => ({
|
|
187
192
|
...e,
|
|
188
193
|
submitting: false,
|
|
189
|
-
error: detail
|
|
194
|
+
error: formatAuthorWriteFailure(detail, `${res.status} ${res.statusText}`),
|
|
190
195
|
}))
|
|
191
196
|
return
|
|
192
197
|
}
|
|
@@ -12,7 +12,11 @@ import { type ContentType, contentViewReducer, initialContentView } from './cont
|
|
|
12
12
|
import { EditorScreen } from './EditorScreen.js'
|
|
13
13
|
import { LibraryScreen } from './LibraryScreen.js'
|
|
14
14
|
|
|
15
|
-
export function ContentRoute(
|
|
15
|
+
export function ContentRoute({
|
|
16
|
+
allowInlineAuthorCreation = true,
|
|
17
|
+
}: {
|
|
18
|
+
allowInlineAuthorCreation?: boolean
|
|
19
|
+
}) {
|
|
16
20
|
const [view, dispatch] = useReducer(contentViewReducer, initialContentView)
|
|
17
21
|
|
|
18
22
|
function openDoc(id: string | null, contentType?: ContentType) {
|
|
@@ -28,8 +32,14 @@ export function ContentRoute() {
|
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
if (view.open === null) {
|
|
31
|
-
return <LibraryScreen openDoc={openDoc} />
|
|
35
|
+
return <LibraryScreen openDoc={openDoc} allowInlineAuthorCreation={allowInlineAuthorCreation} />
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
return
|
|
38
|
+
return (
|
|
39
|
+
<EditorScreen
|
|
40
|
+
docRef={view.open}
|
|
41
|
+
onClose={handleClose}
|
|
42
|
+
allowInlineAuthorCreation={allowInlineAuthorCreation}
|
|
43
|
+
/>
|
|
44
|
+
)
|
|
35
45
|
}
|
|
@@ -38,6 +38,7 @@ import type { ContentView } from './content-view.js'
|
|
|
38
38
|
export interface EditorScreenProps {
|
|
39
39
|
docRef: NonNullable<ContentView['open']>
|
|
40
40
|
onClose: () => void
|
|
41
|
+
allowInlineAuthorCreation?: boolean
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
// ---------------------------------------------------------------------------
|
|
@@ -66,7 +67,11 @@ const STATUS_PILL_COLORS: Record<ContentStatus, string> = {
|
|
|
66
67
|
// EditorScreen
|
|
67
68
|
// ---------------------------------------------------------------------------
|
|
68
69
|
|
|
69
|
-
export function EditorScreen({
|
|
70
|
+
export function EditorScreen({
|
|
71
|
+
docRef,
|
|
72
|
+
onClose,
|
|
73
|
+
allowInlineAuthorCreation = true,
|
|
74
|
+
}: EditorScreenProps) {
|
|
70
75
|
const contentType: ContentType = (docRef.contentType as ContentType | undefined) ?? 'article'
|
|
71
76
|
const siteBase = typeof window !== 'undefined' ? window.location.origin : undefined
|
|
72
77
|
|
|
@@ -397,6 +402,7 @@ export function EditorScreen({ docRef, onClose }: EditorScreenProps) {
|
|
|
397
402
|
},
|
|
398
403
|
siteBase,
|
|
399
404
|
)}
|
|
405
|
+
allowInlineAuthorCreation={allowInlineAuthorCreation}
|
|
400
406
|
onClose={() => setShareOpen(false)}
|
|
401
407
|
/>
|
|
402
408
|
)}
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
|
|
30
30
|
export interface LibraryScreenProps {
|
|
31
31
|
openDoc: (id: string | null, contentType?: ContentType) => void
|
|
32
|
+
allowInlineAuthorCreation?: boolean
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
// ---------------------------------------------------------------------------
|
|
@@ -198,7 +199,7 @@ function getVisibleRows(state: LibraryState): LibraryRow[] {
|
|
|
198
199
|
// Main component
|
|
199
200
|
// ---------------------------------------------------------------------------
|
|
200
201
|
|
|
201
|
-
export function LibraryScreen({ openDoc }: LibraryScreenProps) {
|
|
202
|
+
export function LibraryScreen({ openDoc, allowInlineAuthorCreation = true }: LibraryScreenProps) {
|
|
202
203
|
const { state: ws } = useWorkspace()
|
|
203
204
|
const siteBase = typeof window !== 'undefined' ? window.location.origin : undefined
|
|
204
205
|
const [lib, dispatch] = useReducer(libraryReducer, undefined, initLibrary)
|
|
@@ -487,6 +488,7 @@ export function LibraryScreen({ openDoc }: LibraryScreenProps) {
|
|
|
487
488
|
{lib.shareRow && (
|
|
488
489
|
<ShareModal
|
|
489
490
|
prefill={buildSharePrefill(lib.shareRow, siteBase)}
|
|
491
|
+
allowInlineAuthorCreation={allowInlineAuthorCreation}
|
|
490
492
|
onClose={() => dispatch({ type: 'close-share' })}
|
|
491
493
|
/>
|
|
492
494
|
)}
|