@actuate-media/cms-admin 0.21.1 → 0.23.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.
Files changed (83) hide show
  1. package/dist/__tests__/components/forms-pagination.render.test.d.ts +2 -0
  2. package/dist/__tests__/components/forms-pagination.render.test.d.ts.map +1 -0
  3. package/dist/__tests__/components/forms-pagination.render.test.js +33 -0
  4. package/dist/__tests__/components/forms-pagination.render.test.js.map +1 -0
  5. package/dist/__tests__/layout/sidebar-forms-badge.render.test.d.ts +2 -0
  6. package/dist/__tests__/layout/sidebar-forms-badge.render.test.d.ts.map +1 -0
  7. package/dist/__tests__/layout/sidebar-forms-badge.render.test.js +30 -0
  8. package/dist/__tests__/layout/sidebar-forms-badge.render.test.js.map +1 -0
  9. package/dist/__tests__/layout/sidebar-submenu.render.test.js +8 -2
  10. package/dist/__tests__/layout/sidebar-submenu.render.test.js.map +1 -1
  11. package/dist/__tests__/views/form-submissions.render.test.d.ts +2 -0
  12. package/dist/__tests__/views/form-submissions.render.test.d.ts.map +1 -0
  13. package/dist/__tests__/views/form-submissions.render.test.js +110 -0
  14. package/dist/__tests__/views/form-submissions.render.test.js.map +1 -0
  15. package/dist/__tests__/views/forms-list.render.test.d.ts +2 -0
  16. package/dist/__tests__/views/forms-list.render.test.d.ts.map +1 -0
  17. package/dist/__tests__/views/forms-list.render.test.js +122 -0
  18. package/dist/__tests__/views/forms-list.render.test.js.map +1 -0
  19. package/dist/actuate-admin.css +1 -1
  20. package/dist/components/forms/Drawer.d.ts +13 -0
  21. package/dist/components/forms/Drawer.d.ts.map +1 -0
  22. package/dist/components/forms/Drawer.js +13 -0
  23. package/dist/components/forms/Drawer.js.map +1 -0
  24. package/dist/components/forms/EmbedPanel.d.ts +7 -0
  25. package/dist/components/forms/EmbedPanel.d.ts.map +1 -0
  26. package/dist/components/forms/EmbedPanel.js +91 -0
  27. package/dist/components/forms/EmbedPanel.js.map +1 -0
  28. package/dist/components/forms/EntryDetailDrawer.d.ts +11 -0
  29. package/dist/components/forms/EntryDetailDrawer.d.ts.map +1 -0
  30. package/dist/components/forms/EntryDetailDrawer.js +145 -0
  31. package/dist/components/forms/EntryDetailDrawer.js.map +1 -0
  32. package/dist/components/forms/FieldsPanel.d.ts +8 -0
  33. package/dist/components/forms/FieldsPanel.d.ts.map +1 -0
  34. package/dist/components/forms/FieldsPanel.js +123 -0
  35. package/dist/components/forms/FieldsPanel.js.map +1 -0
  36. package/dist/components/forms/FormSchemaDrawer.d.ts +9 -0
  37. package/dist/components/forms/FormSchemaDrawer.d.ts.map +1 -0
  38. package/dist/components/forms/FormSchemaDrawer.js +96 -0
  39. package/dist/components/forms/FormSchemaDrawer.js.map +1 -0
  40. package/dist/components/forms/NotificationsPanel.d.ts +6 -0
  41. package/dist/components/forms/NotificationsPanel.d.ts.map +1 -0
  42. package/dist/components/forms/NotificationsPanel.js +80 -0
  43. package/dist/components/forms/NotificationsPanel.js.map +1 -0
  44. package/dist/components/forms/primitives.d.ts +57 -0
  45. package/dist/components/forms/primitives.d.ts.map +1 -0
  46. package/dist/components/forms/primitives.js +131 -0
  47. package/dist/components/forms/primitives.js.map +1 -0
  48. package/dist/layout/Sidebar.d.ts +5 -0
  49. package/dist/layout/Sidebar.d.ts.map +1 -1
  50. package/dist/layout/Sidebar.js +43 -3
  51. package/dist/layout/Sidebar.js.map +1 -1
  52. package/dist/lib/forms-events.d.ts +17 -0
  53. package/dist/lib/forms-events.d.ts.map +1 -0
  54. package/dist/lib/forms-events.js +20 -0
  55. package/dist/lib/forms-events.js.map +1 -0
  56. package/dist/lib/forms-service.d.ts +114 -0
  57. package/dist/lib/forms-service.d.ts.map +1 -0
  58. package/dist/lib/forms-service.js +210 -0
  59. package/dist/lib/forms-service.js.map +1 -0
  60. package/dist/views/FormSubmissions.d.ts.map +1 -1
  61. package/dist/views/FormSubmissions.js +224 -77
  62. package/dist/views/FormSubmissions.js.map +1 -1
  63. package/dist/views/Forms.d.ts.map +1 -1
  64. package/dist/views/Forms.js +135 -17
  65. package/dist/views/Forms.js.map +1 -1
  66. package/package.json +1 -1
  67. package/src/__tests__/components/forms-pagination.render.test.tsx +49 -0
  68. package/src/__tests__/layout/sidebar-forms-badge.render.test.tsx +50 -0
  69. package/src/__tests__/layout/sidebar-submenu.render.test.tsx +9 -2
  70. package/src/__tests__/views/form-submissions.render.test.tsx +129 -0
  71. package/src/__tests__/views/forms-list.render.test.tsx +141 -0
  72. package/src/components/forms/Drawer.tsx +70 -0
  73. package/src/components/forms/EmbedPanel.tsx +173 -0
  74. package/src/components/forms/EntryDetailDrawer.tsx +312 -0
  75. package/src/components/forms/FieldsPanel.tsx +385 -0
  76. package/src/components/forms/FormSchemaDrawer.tsx +185 -0
  77. package/src/components/forms/NotificationsPanel.tsx +240 -0
  78. package/src/components/forms/primitives.tsx +300 -0
  79. package/src/layout/Sidebar.tsx +72 -6
  80. package/src/lib/forms-events.ts +32 -0
  81. package/src/lib/forms-service.ts +343 -0
  82. package/src/views/FormSubmissions.tsx +529 -394
  83. package/src/views/Forms.tsx +381 -106
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Forms service — typed client over the cms-core Forms REST endpoints.
3
+ *
4
+ * Every Forms admin component reads/writes through this module (never raw
5
+ * `cmsApi` / `fetch`), so the network contract lives in one place. Read
6
+ * fetchers re-surface `cmsApi`'s `{ error }` as a thrown error (so the
7
+ * resource hook can show error/retry UI); mutations return `{ error? }`.
8
+ *
9
+ * The endpoint paths here are the contract implemented by cms-core
10
+ * `packages/cms-core/src/api/handlers.ts` (PR 2). Domain types are re-used
11
+ * from `@actuate-media/cms-core` so the admin can never drift from the server.
12
+ */
13
+ import { cmsApi } from './api.js'
14
+ import type {
15
+ FormDefinition,
16
+ FormSchemaVersion,
17
+ FormField,
18
+ FormNotificationSettings,
19
+ FormEmbedSettings,
20
+ FormStatus,
21
+ FormStats,
22
+ FormsSidebarCount,
23
+ FetchFormsParams,
24
+ FormSubmission,
25
+ FetchFormEntriesParams,
26
+ EntryCounts,
27
+ } from '@actuate-media/cms-core'
28
+
29
+ export type {
30
+ FormDefinition,
31
+ FormSchemaVersion,
32
+ FormField,
33
+ FormFieldType,
34
+ FormFieldOption,
35
+ FieldValidationRules,
36
+ FormNotificationSettings,
37
+ FormEmbedSettings,
38
+ FormStatus,
39
+ FormStats,
40
+ FormsSidebarCount,
41
+ FormSubmission,
42
+ FormSubmissionFile,
43
+ FetchFormEntriesParams,
44
+ EntryCounts,
45
+ SpamStatus,
46
+ } from '@actuate-media/cms-core'
47
+
48
+ /**
49
+ * `cmsApi` never throws — it returns `{ error, status }` on a network/HTTP
50
+ * failure. Read (GET) fetchers must re-surface that as a thrown error so the
51
+ * resource hook can distinguish "request failed" from "empty result".
52
+ */
53
+ function throwIfError(res: { error?: string }): void {
54
+ if (res.error) throw new Error(res.error)
55
+ }
56
+
57
+ /**
58
+ * A form as returned by `GET /forms` — the typed definition plus the legacy
59
+ * numeric aliases the list endpoint adds for backward compatibility.
60
+ */
61
+ export interface FormListItem extends FormDefinition {
62
+ /** `fields.length` precomputed by the server. */
63
+ fieldCount?: number
64
+ /** Alias of `totalEntries`. */
65
+ submissions?: number
66
+ }
67
+
68
+ export interface FormSchemaResult {
69
+ fields: FormField[]
70
+ activeVersionId: string | null
71
+ versions: FormSchemaVersion[]
72
+ }
73
+
74
+ // ─── Forms list / stats / sidebar ────────────────────────────────────────
75
+
76
+ function buildFormsQuery(params: FetchFormsParams): string {
77
+ const qs = new URLSearchParams()
78
+ if (params.search) qs.set('search', params.search)
79
+ if (params.status) qs.set('status', params.status)
80
+ if (typeof params.notifyEnabled === 'boolean') {
81
+ qs.set('notifyEnabled', String(params.notifyEnabled))
82
+ }
83
+ if (params.sortBy) qs.set('sortBy', params.sortBy)
84
+ if (params.sortDirection) qs.set('sortDirection', params.sortDirection)
85
+ if (params.page) qs.set('page', String(params.page))
86
+ if (params.pageSize) qs.set('pageSize', String(params.pageSize))
87
+ const str = qs.toString()
88
+ return str ? `?${str}` : ''
89
+ }
90
+
91
+ export async function fetchForms(params: FetchFormsParams = {}): Promise<FormListItem[]> {
92
+ const res = await cmsApi<FormListItem[]>(`/forms${buildFormsQuery(params)}`)
93
+ throwIfError(res)
94
+ return res.data ?? []
95
+ }
96
+
97
+ export async function fetchFormById(id: string): Promise<FormDefinition | null> {
98
+ const res = await cmsApi<FormDefinition>(`/forms/${encodeURIComponent(id)}`)
99
+ throwIfError(res)
100
+ return res.data ?? null
101
+ }
102
+
103
+ const EMPTY_STATS: FormStats = {
104
+ totalForms: 0,
105
+ activeForms: 0,
106
+ totalEntries: 0,
107
+ unreadEntries: 0,
108
+ }
109
+
110
+ export async function fetchFormStats(): Promise<FormStats> {
111
+ const res = await cmsApi<FormStats>('/forms/stats')
112
+ throwIfError(res)
113
+ return res.data ?? EMPTY_STATS
114
+ }
115
+
116
+ export async function fetchFormsSidebarCounts(): Promise<FormsSidebarCount[]> {
117
+ const res = await cmsApi<FormsSidebarCount[]>('/forms/sidebar-counts')
118
+ throwIfError(res)
119
+ return res.data ?? []
120
+ }
121
+
122
+ // ─── Form mutations ──────────────────────────────────────────────────────
123
+
124
+ export interface CreateFormPayload {
125
+ name: string
126
+ slug?: string
127
+ description?: string
128
+ status?: FormStatus
129
+ template?: string
130
+ fields?: FormField[]
131
+ }
132
+
133
+ export async function createForm(
134
+ payload: CreateFormPayload,
135
+ ): Promise<{ data?: FormDefinition; error?: string }> {
136
+ const res = await cmsApi<FormDefinition>('/forms', {
137
+ method: 'POST',
138
+ body: JSON.stringify(payload),
139
+ })
140
+ return { data: res.data, error: res.error }
141
+ }
142
+
143
+ export async function updateForm(
144
+ id: string,
145
+ payload: Partial<Pick<FormDefinition, 'name' | 'slug' | 'description' | 'status'>>,
146
+ ): Promise<{ data?: FormDefinition; error?: string }> {
147
+ const res = await cmsApi<FormDefinition>(`/forms/${encodeURIComponent(id)}`, {
148
+ method: 'PATCH',
149
+ body: JSON.stringify(payload),
150
+ })
151
+ return { data: res.data, error: res.error }
152
+ }
153
+
154
+ export async function duplicateForm(
155
+ id: string,
156
+ ): Promise<{ data?: FormDefinition; error?: string }> {
157
+ const res = await cmsApi<FormDefinition>(`/forms/${encodeURIComponent(id)}/duplicate`, {
158
+ method: 'POST',
159
+ })
160
+ return { data: res.data, error: res.error }
161
+ }
162
+
163
+ /** Move a form to the archived state (hidden from embed pickers, no new submissions). */
164
+ export async function archiveForm(id: string): Promise<{ error?: string }> {
165
+ return updateForm(id, { status: 'archived' })
166
+ }
167
+
168
+ /** Restore an archived form back to draft. */
169
+ export async function restoreForm(id: string): Promise<{ error?: string }> {
170
+ return updateForm(id, { status: 'draft' })
171
+ }
172
+
173
+ export async function deleteForm(
174
+ id: string,
175
+ opts: { force?: boolean } = {},
176
+ ): Promise<{ error?: string }> {
177
+ const qs = opts.force ? '?force=true' : ''
178
+ const res = await cmsApi(`/forms/${encodeURIComponent(id)}${qs}`, { method: 'DELETE' })
179
+ return { error: res.error }
180
+ }
181
+
182
+ // ─── Schema ──────────────────────────────────────────────────────────────
183
+
184
+ export async function fetchFormSchema(id: string): Promise<FormSchemaResult> {
185
+ const res = await cmsApi<FormSchemaResult>(`/forms/${encodeURIComponent(id)}/schema`)
186
+ throwIfError(res)
187
+ return res.data ?? { fields: [], activeVersionId: null, versions: [] }
188
+ }
189
+
190
+ export async function saveFormSchema(
191
+ id: string,
192
+ fields: FormField[],
193
+ notes?: string,
194
+ ): Promise<{ data?: { form: FormDefinition; version: FormSchemaVersion }; error?: string }> {
195
+ const res = await cmsApi<{ form: FormDefinition; version: FormSchemaVersion }>(
196
+ `/forms/${encodeURIComponent(id)}/schema`,
197
+ { method: 'PUT', body: JSON.stringify({ fields, notes }) },
198
+ )
199
+ return { data: res.data, error: res.error }
200
+ }
201
+
202
+ // ─── Notifications ─────────────────────────────────────────────────────────
203
+
204
+ export async function fetchNotificationSettings(id: string): Promise<FormNotificationSettings> {
205
+ const res = await cmsApi<FormNotificationSettings>(
206
+ `/forms/${encodeURIComponent(id)}/notifications`,
207
+ )
208
+ throwIfError(res)
209
+ return res.data ?? { enabled: false, recipients: [] }
210
+ }
211
+
212
+ export async function updateNotificationSettings(
213
+ id: string,
214
+ payload: Partial<FormNotificationSettings>,
215
+ ): Promise<{ data?: FormNotificationSettings; error?: string }> {
216
+ const res = await cmsApi<FormNotificationSettings>(
217
+ `/forms/${encodeURIComponent(id)}/notifications`,
218
+ { method: 'PUT', body: JSON.stringify(payload) },
219
+ )
220
+ return { data: res.data, error: res.error }
221
+ }
222
+
223
+ /** Lightweight notify on/off toggle used by the forms-list switch. */
224
+ export async function setNotifyEnabled(
225
+ id: string,
226
+ enabled: boolean,
227
+ ): Promise<{ data?: { notifyEnabled: boolean }; error?: string }> {
228
+ const res = await cmsApi<{ notifyEnabled: boolean }>(`/forms/${encodeURIComponent(id)}/notify`, {
229
+ method: 'PUT',
230
+ body: JSON.stringify({ enabled }),
231
+ })
232
+ return { data: res.data, error: res.error }
233
+ }
234
+
235
+ // ─── Embed ───────────────────────────────────────────────────────────────
236
+
237
+ export async function fetchEmbedSettings(id: string): Promise<FormEmbedSettings> {
238
+ const res = await cmsApi<FormEmbedSettings>(`/forms/${encodeURIComponent(id)}/embed`)
239
+ throwIfError(res)
240
+ return res.data ?? {}
241
+ }
242
+
243
+ export async function updateEmbedSettings(
244
+ id: string,
245
+ payload: Partial<FormEmbedSettings>,
246
+ ): Promise<{ data?: FormEmbedSettings; error?: string }> {
247
+ const res = await cmsApi<FormEmbedSettings>(`/forms/${encodeURIComponent(id)}/embed`, {
248
+ method: 'PUT',
249
+ body: JSON.stringify(payload),
250
+ })
251
+ return { data: res.data, error: res.error }
252
+ }
253
+
254
+ // ─── Entries (submissions) ─────────────────────────────────────────────────
255
+
256
+ /** A page of submissions as returned by `GET /forms/entries`. */
257
+ export interface EntriesPage {
258
+ entries: FormSubmission[]
259
+ total: number
260
+ page: number
261
+ pageSize: number
262
+ }
263
+
264
+ /** Patch shape accepted by the entry mutation endpoints. */
265
+ export interface EntryPatch {
266
+ unread?: boolean
267
+ starred?: boolean
268
+ archived?: boolean
269
+ }
270
+
271
+ function buildEntriesQuery(params: FetchFormEntriesParams): string {
272
+ const qs = new URLSearchParams()
273
+ if (params.search) qs.set('search', params.search)
274
+ if (params.formId) qs.set('formId', params.formId)
275
+ if (typeof params.unread === 'boolean') qs.set('unread', String(params.unread))
276
+ if (typeof params.starred === 'boolean') qs.set('starred', String(params.starred))
277
+ if (typeof params.archived === 'boolean') qs.set('archived', String(params.archived))
278
+ if (params.spamStatus) qs.set('spamStatus', params.spamStatus)
279
+ if (params.dateFrom) qs.set('dateFrom', params.dateFrom)
280
+ if (params.dateTo) qs.set('dateTo', params.dateTo)
281
+ if (params.thisWeek) qs.set('thisWeek', 'true')
282
+ if (params.sortBy) qs.set('sortBy', params.sortBy)
283
+ if (params.sortDirection) qs.set('sortDirection', params.sortDirection)
284
+ if (params.page) qs.set('page', String(params.page))
285
+ if (params.pageSize) qs.set('pageSize', String(params.pageSize))
286
+ const str = qs.toString()
287
+ return str ? `?${str}` : ''
288
+ }
289
+
290
+ const EMPTY_PAGE: EntriesPage = { entries: [], total: 0, page: 1, pageSize: 25 }
291
+
292
+ export async function fetchEntries(params: FetchFormEntriesParams = {}): Promise<EntriesPage> {
293
+ const res = await cmsApi<EntriesPage>(`/forms/entries${buildEntriesQuery(params)}`)
294
+ throwIfError(res)
295
+ return res.data ?? EMPTY_PAGE
296
+ }
297
+
298
+ const EMPTY_COUNTS: EntryCounts = { total: 0, unread: 0, starred: 0, thisWeek: 0, byForm: {} }
299
+
300
+ export async function fetchEntryCounts(params: FetchFormEntriesParams = {}): Promise<EntryCounts> {
301
+ const res = await cmsApi<EntryCounts>(`/forms/entries/counts${buildEntriesQuery(params)}`)
302
+ throwIfError(res)
303
+ return res.data ?? EMPTY_COUNTS
304
+ }
305
+
306
+ /** A single entry rendered against the schema version it was captured with. */
307
+ export interface EntryDetail {
308
+ entry: FormSubmission
309
+ fields: FormField[]
310
+ }
311
+
312
+ export async function fetchEntry(id: string): Promise<EntryDetail | null> {
313
+ const res = await cmsApi<EntryDetail>(`/forms/entries/${encodeURIComponent(id)}`)
314
+ throwIfError(res)
315
+ return res.data ?? null
316
+ }
317
+
318
+ export async function updateEntry(
319
+ id: string,
320
+ patch: EntryPatch,
321
+ ): Promise<{ data?: FormSubmission; error?: string }> {
322
+ const res = await cmsApi<FormSubmission>(`/forms/entries/${encodeURIComponent(id)}`, {
323
+ method: 'PATCH',
324
+ body: JSON.stringify(patch),
325
+ })
326
+ return { data: res.data, error: res.error }
327
+ }
328
+
329
+ export async function deleteEntry(id: string): Promise<{ error?: string }> {
330
+ const res = await cmsApi(`/forms/entries/${encodeURIComponent(id)}`, { method: 'DELETE' })
331
+ return { error: res.error }
332
+ }
333
+
334
+ export async function bulkUpdateEntries(
335
+ ids: string[],
336
+ patch: EntryPatch,
337
+ ): Promise<{ data?: { updated: number }; error?: string }> {
338
+ const res = await cmsApi<{ updated: number }>('/forms/entries/bulk', {
339
+ method: 'POST',
340
+ body: JSON.stringify({ ids, patch }),
341
+ })
342
+ return { data: res.data, error: res.error }
343
+ }