@growth-labs/cms 0.8.26 → 0.8.30
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 +23 -0
- package/dist/engine/content-insights.d.ts +27 -3
- package/dist/engine/content-insights.d.ts.map +1 -1
- package/dist/engine/content-insights.js +83 -4
- package/dist/engine/content-insights.js.map +1 -1
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +1 -0
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/publication-receipt/receipt.d.ts +4 -4
- package/dist/engine/published-content.d.ts +4 -0
- package/dist/engine/published-content.d.ts.map +1 -1
- package/dist/engine/published-content.js +52 -16
- package/dist/engine/published-content.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/integration/options.d.ts +226 -13
- package/dist/integration/options.d.ts.map +1 -1
- package/dist/integration/options.js +17 -2
- package/dist/integration/options.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/content-insights.d.ts +4 -0
- package/dist/routes/content-insights.d.ts.map +1 -1
- package/dist/routes/content-insights.js +137 -3
- package/dist/routes/content-insights.js.map +1 -1
- package/dist/routes/fronts-publish.d.ts +1 -1
- package/dist/routes/fronts-publish.d.ts.map +1 -1
- package/dist/routes/fronts-publish.js +2 -2
- package/dist/routes/fronts-publish.js.map +1 -1
- package/dist/routes/surveys.d.ts.map +1 -1
- package/dist/routes/surveys.js +80 -4
- package/dist/routes/surveys.js.map +1 -1
- package/dist/schema/insights-ingest.d.ts +961 -97
- package/dist/schema/insights-ingest.d.ts.map +1 -1
- package/dist/schema/insights-ingest.js +38 -0
- package/dist/schema/insights-ingest.js.map +1 -1
- package/dist/schema/types.d.ts +20 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js.map +1 -1
- package/dist/surveys/auto-label.d.ts +31 -4
- package/dist/surveys/auto-label.d.ts.map +1 -1
- package/dist/surveys/auto-label.js +160 -29
- package/dist/surveys/auto-label.js.map +1 -1
- package/dist/surveys/schema.d.ts +155 -0
- package/dist/surveys/schema.d.ts.map +1 -1
- package/dist/surveys/schema.js +49 -0
- package/dist/surveys/schema.js.map +1 -1
- package/dist/ui/api/content/insights-inputs.d.ts.map +1 -1
- package/dist/ui/api/content/insights-inputs.js +11 -1
- package/dist/ui/api/content/insights-inputs.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/screens/SurveyResponsesScreen.d.ts +18 -3
- package/dist/ui/screens/SurveyResponsesScreen.d.ts.map +1 -1
- package/dist/ui/screens/SurveyResponsesScreen.js +75 -18
- package/dist/ui/screens/SurveyResponsesScreen.js.map +1 -1
- package/dist/ui/screens/registry.d.ts +3 -1
- package/dist/ui/screens/registry.d.ts.map +1 -1
- package/dist/ui/screens/registry.js +4 -2
- package/dist/ui/screens/registry.js.map +1 -1
- package/dist/ui/styles/broadsheet.css +1 -0
- package/package.json +1 -1
- package/src/engine/content-insights.ts +99 -6
- package/src/engine/index.ts +1 -0
- package/src/engine/published-content.ts +75 -16
- package/src/index.ts +1 -0
- package/src/integration/options.ts +21 -1
- package/src/providers/types.ts +9 -0
- package/src/routes/content-insights.ts +144 -2
- package/src/routes/fronts-publish.ts +2 -1
- package/src/routes/surveys.ts +106 -4
- package/src/schema/insights-ingest.ts +45 -0
- package/src/schema/types.ts +10 -0
- package/src/surveys/auto-label.ts +204 -33
- package/src/surveys/schema.ts +58 -0
- package/src/ui/api/content/insights-inputs.ts +8 -1
- package/src/ui/client/mount.tsx +1 -0
- package/src/ui/screens/SurveyResponsesScreen.tsx +198 -18
- package/src/ui/screens/registry.tsx +6 -2
- package/src/ui/styles/broadsheet.css +1 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
1
|
+
import { type ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
|
|
2
2
|
import type { CmsRole } from '../../schema/types.js'
|
|
3
3
|
import { canLabelSurveyAnswers, canViewSurveyVerbatims } from '../../surveys/access.js'
|
|
4
4
|
import {
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
import {
|
|
32
32
|
MAX_FREE_TEXT_ENTRIES,
|
|
33
33
|
type SurveyAnswerLabel,
|
|
34
|
+
type SurveyFeedbackTheme,
|
|
34
35
|
type SurveyQuestion,
|
|
35
36
|
type SurveyResults,
|
|
36
37
|
SurveyResultsSchema,
|
|
@@ -89,7 +90,72 @@ const PAGE_SIZE = 100
|
|
|
89
90
|
// Five groups is as many as a reader can tell apart by color; more fold into a note.
|
|
90
91
|
const MAX_GROUPS = 5
|
|
91
92
|
|
|
92
|
-
export
|
|
93
|
+
export interface SurveyChoice {
|
|
94
|
+
surveyId: string
|
|
95
|
+
label: string
|
|
96
|
+
feedbackThemes?: SurveyFeedbackTheme[]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function SurveyResponsesScreen({
|
|
100
|
+
surveyId,
|
|
101
|
+
surveys,
|
|
102
|
+
}: {
|
|
103
|
+
surveyId: string
|
|
104
|
+
surveys?: SurveyChoice[]
|
|
105
|
+
}) {
|
|
106
|
+
const [selected, setSelected] = useState(surveyId)
|
|
107
|
+
const picker =
|
|
108
|
+
surveys && surveys.length > 1 ? (
|
|
109
|
+
<SurveyPicker surveys={surveys} selected={selected} onSelect={setSelected} />
|
|
110
|
+
) : null
|
|
111
|
+
// Keyed by survey, so switching starts clean: no page, comparison, filter or
|
|
112
|
+
// loaded responses carry over from one survey to another.
|
|
113
|
+
return (
|
|
114
|
+
<SurveyResultsView
|
|
115
|
+
key={selected}
|
|
116
|
+
surveyId={selected}
|
|
117
|
+
picker={picker}
|
|
118
|
+
feedbackThemes={surveys?.find((survey) => survey.surveyId === selected)?.feedbackThemes ?? []}
|
|
119
|
+
/>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function SurveyPicker({
|
|
124
|
+
surveys,
|
|
125
|
+
selected,
|
|
126
|
+
onSelect,
|
|
127
|
+
}: {
|
|
128
|
+
surveys: SurveyChoice[]
|
|
129
|
+
selected: string
|
|
130
|
+
onSelect: (surveyId: string) => void
|
|
131
|
+
}) {
|
|
132
|
+
return (
|
|
133
|
+
<label className="svy-control">
|
|
134
|
+
<span className="kicker">Survey</span>
|
|
135
|
+
<select
|
|
136
|
+
className="selectbox"
|
|
137
|
+
value={selected}
|
|
138
|
+
onChange={(event) => onSelect(event.target.value)}
|
|
139
|
+
>
|
|
140
|
+
{surveys.map((survey) => (
|
|
141
|
+
<option key={survey.surveyId} value={survey.surveyId}>
|
|
142
|
+
{survey.label}
|
|
143
|
+
</option>
|
|
144
|
+
))}
|
|
145
|
+
</select>
|
|
146
|
+
</label>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function SurveyResultsView({
|
|
151
|
+
surveyId,
|
|
152
|
+
picker,
|
|
153
|
+
feedbackThemes,
|
|
154
|
+
}: {
|
|
155
|
+
surveyId: string
|
|
156
|
+
picker: ReactNode
|
|
157
|
+
feedbackThemes: SurveyFeedbackTheme[]
|
|
158
|
+
}) {
|
|
93
159
|
const [state, setState] = useState<SurveyResponsesState>({ kind: 'loading' })
|
|
94
160
|
const [offset, setOffset] = useState(0)
|
|
95
161
|
const [labelSaveFailed, setLabelSaveFailed] = useState(false)
|
|
@@ -189,6 +255,8 @@ export function SurveyResponsesScreen({ surveyId }: { surveyId: string }) {
|
|
|
189
255
|
labelSaveFailed={labelSaveFailed}
|
|
190
256
|
everyone={everyone}
|
|
191
257
|
onLoadEveryone={(total) => void loadEveryone(total)}
|
|
258
|
+
picker={picker}
|
|
259
|
+
feedbackThemes={feedbackThemes}
|
|
192
260
|
/>
|
|
193
261
|
)
|
|
194
262
|
}
|
|
@@ -201,6 +269,18 @@ function withAnswerLabel(
|
|
|
201
269
|
): SurveyResults {
|
|
202
270
|
return {
|
|
203
271
|
...data,
|
|
272
|
+
responses: data.responses.map((response) =>
|
|
273
|
+
response.id !== responseId
|
|
274
|
+
? response
|
|
275
|
+
: {
|
|
276
|
+
...response,
|
|
277
|
+
answers: response.answers?.map((answer) =>
|
|
278
|
+
answer.questionId !== questionId
|
|
279
|
+
? answer
|
|
280
|
+
: { ...answer, label, labelSource: 'owner' },
|
|
281
|
+
),
|
|
282
|
+
},
|
|
283
|
+
),
|
|
204
284
|
freeText: data.freeText.map((entry) =>
|
|
205
285
|
entry.responseId === responseId && entry.questionId === questionId
|
|
206
286
|
? // A successful owner save stores source 'owner' server-side; the
|
|
@@ -224,6 +304,8 @@ export function SurveyResponsesBody({
|
|
|
224
304
|
onLoadEveryone,
|
|
225
305
|
compareBy = '',
|
|
226
306
|
commentMood = '',
|
|
307
|
+
picker = null,
|
|
308
|
+
feedbackThemes = [],
|
|
227
309
|
}: {
|
|
228
310
|
surveyId: string
|
|
229
311
|
state: SurveyResponsesState
|
|
@@ -239,30 +321,40 @@ export function SurveyResponsesBody({
|
|
|
239
321
|
compareBy?: string
|
|
240
322
|
/** The comment mood filter to open with; the screen starts with all readers. */
|
|
241
323
|
commentMood?: Mood | ''
|
|
324
|
+
/** The survey switcher, shown in the header when there is more than one survey. */
|
|
325
|
+
picker?: ReactNode
|
|
326
|
+
feedbackThemes?: SurveyFeedbackTheme[]
|
|
242
327
|
}) {
|
|
243
328
|
const canSeeVerbatims = canViewSurveyVerbatims(role)
|
|
244
329
|
const canLabel = canLabelSurveyAnswers(role)
|
|
245
330
|
|
|
246
|
-
if (state.kind === 'loading')
|
|
331
|
+
if (state.kind === 'loading')
|
|
332
|
+
return <StatusPage picker={picker} title="Loading survey responses…" />
|
|
247
333
|
if (state.kind === 'forbidden')
|
|
248
|
-
return
|
|
334
|
+
return (
|
|
335
|
+
<StatusPage picker={picker} title="You do not have permission to view survey responses." />
|
|
336
|
+
)
|
|
249
337
|
if (state.kind === 'unavailable')
|
|
250
|
-
return <StatusPage title="Survey reporting is not configured for this site." />
|
|
338
|
+
return <StatusPage picker={picker} title="Survey reporting is not configured for this site." />
|
|
251
339
|
if (state.kind === 'auth_failed')
|
|
252
340
|
return (
|
|
253
341
|
<StatusPage
|
|
342
|
+
picker={picker}
|
|
254
343
|
title="Survey reporting rejected this site’s credentials."
|
|
255
344
|
detail="The signing secret shared with the reporting upstream is missing or does not match."
|
|
256
345
|
retry={onRetry}
|
|
257
346
|
/>
|
|
258
347
|
)
|
|
259
348
|
if (state.kind === 'malformed')
|
|
260
|
-
return
|
|
349
|
+
return (
|
|
350
|
+
<StatusPage picker={picker} title="Masthead received invalid survey data." retry={onRetry} />
|
|
351
|
+
)
|
|
261
352
|
if (state.kind === 'not_found')
|
|
262
|
-
return <StatusPage title="The configured survey could not be found." />
|
|
353
|
+
return <StatusPage picker={picker} title="The configured survey could not be found." />
|
|
263
354
|
if (state.kind === 'retryable')
|
|
264
355
|
return (
|
|
265
356
|
<StatusPage
|
|
357
|
+
picker={picker}
|
|
266
358
|
title="Survey responses could not be loaded."
|
|
267
359
|
detail="The request did not reach the reporting upstream, or it failed before replying."
|
|
268
360
|
retry={onRetry}
|
|
@@ -285,12 +377,15 @@ export function SurveyResponsesBody({
|
|
|
285
377
|
: `${total} ${total === 1 ? 'response' : 'responses'} · ${surveyStatusLabel(data.survey.status)}`}
|
|
286
378
|
</div>
|
|
287
379
|
</div>
|
|
288
|
-
<
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
380
|
+
<div className="svy-head-actions">
|
|
381
|
+
{picker}
|
|
382
|
+
<a
|
|
383
|
+
className="btn"
|
|
384
|
+
href={`/admin/api/surveys/${encodeURIComponent(surveyId)}/responses.csv`}
|
|
385
|
+
>
|
|
386
|
+
Export CSV
|
|
387
|
+
</a>
|
|
388
|
+
</div>
|
|
294
389
|
</div>
|
|
295
390
|
|
|
296
391
|
{total === 0 ? (
|
|
@@ -309,6 +404,8 @@ export function SurveyResponsesBody({
|
|
|
309
404
|
initialCompareBy={compareBy}
|
|
310
405
|
/>
|
|
311
406
|
|
|
407
|
+
{feedbackThemes.length > 0 && <FeedbackThemes themes={feedbackThemes} />}
|
|
408
|
+
|
|
312
409
|
{canSeeVerbatims ? (
|
|
313
410
|
<Comments
|
|
314
411
|
data={data}
|
|
@@ -336,6 +433,51 @@ export function SurveyResponsesBody({
|
|
|
336
433
|
)
|
|
337
434
|
}
|
|
338
435
|
|
|
436
|
+
// Existing site configuration supplies reviewed statuses; the survey payload stays unchanged.
|
|
437
|
+
function FeedbackThemes({ themes }: { themes: SurveyFeedbackTheme[] }) {
|
|
438
|
+
const states: Record<SurveyFeedbackTheme['state'], string> = {
|
|
439
|
+
open: 'Open',
|
|
440
|
+
partly_shipped: 'Partly shipped',
|
|
441
|
+
shipped: 'Shipped',
|
|
442
|
+
unknown: 'Not verified',
|
|
443
|
+
}
|
|
444
|
+
return (
|
|
445
|
+
<section className="svy-section" aria-labelledby="survey-feedback-themes">
|
|
446
|
+
<div className="svy-section-head">
|
|
447
|
+
<div className="kicker">Recurring feedback</div>
|
|
448
|
+
<h2 className="card-title" id="survey-feedback-themes">
|
|
449
|
+
You said → we shipped
|
|
450
|
+
</h2>
|
|
451
|
+
<p className="page-sub">
|
|
452
|
+
Reviewed delivery notes for recurring reader themes. These are dated updates, not live
|
|
453
|
+
issue statuses or counts of survey answers. Open work stays visible.
|
|
454
|
+
</p>
|
|
455
|
+
</div>
|
|
456
|
+
<div className="svy-grid">
|
|
457
|
+
{themes.map((theme) => (
|
|
458
|
+
<article className="card" key={theme.id} data-feedback-theme={theme.id}>
|
|
459
|
+
<div className="kicker">{states[theme.state]}</div>
|
|
460
|
+
<h3 className="svy-q-prompt">{theme.title}</h3>
|
|
461
|
+
<p className="svy-takeaway">{theme.summary}</p>
|
|
462
|
+
<p className="svy-note">
|
|
463
|
+
Reviewed <time dateTime={theme.reviewedAt}>{theme.reviewedAt.slice(0, 10)}</time>
|
|
464
|
+
</p>
|
|
465
|
+
{theme.evidence.length > 0 && (
|
|
466
|
+
<ul className="svy-note">
|
|
467
|
+
{theme.evidence.map((link) => (
|
|
468
|
+
<li key={link.href}>
|
|
469
|
+
<a href={link.href}>{link.label}</a>
|
|
470
|
+
</li>
|
|
471
|
+
))}
|
|
472
|
+
</ul>
|
|
473
|
+
)}
|
|
474
|
+
</article>
|
|
475
|
+
))}
|
|
476
|
+
</div>
|
|
477
|
+
</section>
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
|
|
339
481
|
// ── Headline ────────────────────────────────────────────────────────────
|
|
340
482
|
|
|
341
483
|
// The first scale question is the survey's headline number. Leading with the
|
|
@@ -816,15 +958,31 @@ function Comments({
|
|
|
816
958
|
|
|
817
959
|
// Non-owners never receive technical or unlabeled entries (the route filters
|
|
818
960
|
// server-side), so for them this feed is already the general thread.
|
|
961
|
+
const writtenAnswers: WrittenAnswer[] = [...data.freeText]
|
|
962
|
+
for (const response of everyone.kind === 'ready' ? everyone.responses : data.responses) {
|
|
963
|
+
for (const answer of response.answers ?? []) {
|
|
964
|
+
if (answer.writtenValue === undefined) continue
|
|
965
|
+
writtenAnswers.push({
|
|
966
|
+
questionId: answer.questionId,
|
|
967
|
+
responseId: response.id,
|
|
968
|
+
submittedAt: response.submittedAt,
|
|
969
|
+
value: answer.writtenValue,
|
|
970
|
+
label: answer.label,
|
|
971
|
+
labelSource: answer.labelSource,
|
|
972
|
+
classificationStatus: answer.classificationStatus,
|
|
973
|
+
nextAttemptAt: answer.nextAttemptAt,
|
|
974
|
+
})
|
|
975
|
+
}
|
|
976
|
+
}
|
|
819
977
|
const visibleFreeText =
|
|
820
|
-
labelFilter === 'all' ?
|
|
978
|
+
labelFilter === 'all' ? writtenAnswers : writtenAnswers.filter((e) => e.label === labelFilter)
|
|
821
979
|
const writtenByResponse = new Map<string, WrittenAnswer[]>()
|
|
822
980
|
for (const entry of visibleFreeText) {
|
|
823
981
|
const entries = writtenByResponse.get(entry.responseId) ?? []
|
|
824
982
|
entries.push(entry)
|
|
825
983
|
writtenByResponse.set(entry.responseId, entries)
|
|
826
984
|
}
|
|
827
|
-
const wroteSomething = new Set(
|
|
985
|
+
const wroteSomething = new Set(writtenAnswers.map((entry) => entry.responseId))
|
|
828
986
|
|
|
829
987
|
// A filter answers "what do unhappy readers say", so it searches every
|
|
830
988
|
// response, never just the page on screen. The written answers already
|
|
@@ -949,13 +1107,15 @@ function Comments({
|
|
|
949
1107
|
const mood = moodOf(response)
|
|
950
1108
|
const others = (response.answers ?? []).filter(
|
|
951
1109
|
(answer) =>
|
|
952
|
-
!textQuestionIds.has(answer.questionId) &&
|
|
1110
|
+
!textQuestionIds.has(answer.questionId) &&
|
|
1111
|
+
answer.writtenValue === undefined &&
|
|
1112
|
+
answer.questionId !== headline?.id,
|
|
953
1113
|
)
|
|
954
1114
|
return (
|
|
955
1115
|
<article className="card svy-comment" key={response.id}>
|
|
956
1116
|
{(writtenByResponse.get(response.id) ?? []).map((entry) => (
|
|
957
1117
|
<div className="svy-comment-entry" key={`${response.id}:${entry.questionId}`}>
|
|
958
|
-
{textQuestions.length > 1 ? (
|
|
1118
|
+
{textQuestions.length > 1 || !textQuestionIds.has(entry.questionId) ? (
|
|
959
1119
|
<div className="kicker">{promptById.get(entry.questionId)}</div>
|
|
960
1120
|
) : null}
|
|
961
1121
|
<blockquote className="svy-quote">{entry.value}</blockquote>
|
|
@@ -984,6 +1144,21 @@ function Comments({
|
|
|
984
1144
|
>
|
|
985
1145
|
General
|
|
986
1146
|
</button>
|
|
1147
|
+
{!entry.label && entry.classificationStatus ? (
|
|
1148
|
+
<span className="svy-meta" role="status">
|
|
1149
|
+
{entry.classificationStatus === 'pending'
|
|
1150
|
+
? 'Classification in progress. Reload to check the result.'
|
|
1151
|
+
: entry.classificationStatus === 'manual_review'
|
|
1152
|
+
? 'Choose a label manually.'
|
|
1153
|
+
: entry.classificationStatus === 'retry'
|
|
1154
|
+
? `Classification failed. ${entry.nextAttemptAt ? `Retry eligible after ${formatTime(new Date(entry.nextAttemptAt).toISOString())}. ` : ''}Reload to retry, or choose a label.`
|
|
1155
|
+
: entry.classificationStatus === 'expired'
|
|
1156
|
+
? 'Classification timed out. Reload to schedule a retry, or choose a label.'
|
|
1157
|
+
: entry.classificationStatus === 'unavailable'
|
|
1158
|
+
? 'Classification status unavailable. Reload or choose a label.'
|
|
1159
|
+
: 'Awaiting classification. Reload to check, or choose a label.'}
|
|
1160
|
+
</span>
|
|
1161
|
+
) : null}
|
|
987
1162
|
{entry.labelSource === 'auto' ? (
|
|
988
1163
|
<span
|
|
989
1164
|
className="chip is-quiet"
|
|
@@ -1114,14 +1289,19 @@ function StatusPage({
|
|
|
1114
1289
|
title,
|
|
1115
1290
|
detail,
|
|
1116
1291
|
retry,
|
|
1292
|
+
picker = null,
|
|
1117
1293
|
}: {
|
|
1118
1294
|
title: string
|
|
1119
1295
|
detail?: string
|
|
1120
1296
|
retry?: () => void
|
|
1297
|
+
picker?: ReactNode
|
|
1121
1298
|
}) {
|
|
1122
1299
|
return (
|
|
1123
1300
|
<div className="page" role="status">
|
|
1124
|
-
<
|
|
1301
|
+
<div className="page-head">
|
|
1302
|
+
<h1 className="page-title">Survey responses</h1>
|
|
1303
|
+
{picker}
|
|
1304
|
+
</div>
|
|
1125
1305
|
<p className="page-sub">{title}</p>
|
|
1126
1306
|
{detail ? <p className="page-sub">{detail}</p> : null}
|
|
1127
1307
|
{retry ? (
|
|
@@ -25,7 +25,7 @@ import { PodcastListeningScreen } from './PodcastListeningScreen.js'
|
|
|
25
25
|
import { SettingsScreen } from './SettingsScreen.js'
|
|
26
26
|
import { SocialShareScreen } from './SocialShareScreen.js'
|
|
27
27
|
import { SubscriptionsScreen } from './SubscriptionsScreen.js'
|
|
28
|
-
import { SurveyResponsesScreen } from './SurveyResponsesScreen.js'
|
|
28
|
+
import { type SurveyChoice, SurveyResponsesScreen } from './SurveyResponsesScreen.js'
|
|
29
29
|
import { TopicsScreen } from './TopicsScreen.js'
|
|
30
30
|
|
|
31
31
|
// Thin wrappers that supply the openDoc seam for screens that need it.
|
|
@@ -47,12 +47,14 @@ export function createCmsScreens({
|
|
|
47
47
|
primaryTopic,
|
|
48
48
|
contentMetadataSelects = [],
|
|
49
49
|
surveyId,
|
|
50
|
+
surveys,
|
|
50
51
|
}: {
|
|
51
52
|
allowInlineAuthorCreation?: boolean
|
|
52
53
|
primaryCategories?: Array<{ slug: string; label: string }>
|
|
53
54
|
primaryTopic?: PrimaryTopicControl
|
|
54
55
|
contentMetadataSelects?: ContentMetadataSelectField[]
|
|
55
56
|
surveyId?: string
|
|
57
|
+
surveys?: SurveyChoice[]
|
|
56
58
|
} = {}): ScreenRegistry {
|
|
57
59
|
return {
|
|
58
60
|
content: () => (
|
|
@@ -73,7 +75,9 @@ export function createCmsScreens({
|
|
|
73
75
|
topics: () => <TopicsScreen />,
|
|
74
76
|
subscriptions: () => <SubscriptionsScreen />,
|
|
75
77
|
...(surveyId
|
|
76
|
-
? {
|
|
78
|
+
? {
|
|
79
|
+
'survey-responses': () => <SurveyResponsesScreen surveyId={surveyId} surveys={surveys} />,
|
|
80
|
+
}
|
|
77
81
|
: {}),
|
|
78
82
|
'social-share': () => (
|
|
79
83
|
<SocialShareScreen allowInlineAuthorCreation={allowInlineAuthorCreation} />
|