@actuate-media/cms-admin 0.76.7 → 0.78.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 (50) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/__tests__/components/attribution-rows.test.d.ts +2 -0
  3. package/dist/__tests__/components/attribution-rows.test.d.ts.map +1 -0
  4. package/dist/__tests__/components/attribution-rows.test.js +82 -0
  5. package/dist/__tests__/components/attribution-rows.test.js.map +1 -0
  6. package/dist/__tests__/views/form-editor.render.test.d.ts +2 -0
  7. package/dist/__tests__/views/form-editor.render.test.d.ts.map +1 -0
  8. package/dist/__tests__/views/form-editor.render.test.js +146 -0
  9. package/dist/__tests__/views/form-editor.render.test.js.map +1 -0
  10. package/dist/__tests__/views/form-entries.render.test.js +20 -0
  11. package/dist/__tests__/views/form-entries.render.test.js.map +1 -1
  12. package/dist/__tests__/views/form-submissions.render.test.js +12 -0
  13. package/dist/__tests__/views/form-submissions.render.test.js.map +1 -1
  14. package/dist/actuate-admin.css +1 -1
  15. package/dist/components/forms/ExportMenu.d.ts +8 -0
  16. package/dist/components/forms/ExportMenu.d.ts.map +1 -0
  17. package/dist/components/forms/ExportMenu.js +32 -0
  18. package/dist/components/forms/ExportMenu.js.map +1 -0
  19. package/dist/components/forms/FormEntryDetailPane.d.ts +13 -0
  20. package/dist/components/forms/FormEntryDetailPane.d.ts.map +1 -1
  21. package/dist/components/forms/FormEntryDetailPane.js +74 -8
  22. package/dist/components/forms/FormEntryDetailPane.js.map +1 -1
  23. package/dist/components/forms/WebhooksPanel.d.ts.map +1 -1
  24. package/dist/components/forms/WebhooksPanel.js +1 -1
  25. package/dist/components/forms/WebhooksPanel.js.map +1 -1
  26. package/dist/lib/forms-service.d.ts +29 -3
  27. package/dist/lib/forms-service.d.ts.map +1 -1
  28. package/dist/lib/forms-service.js +37 -1
  29. package/dist/lib/forms-service.js.map +1 -1
  30. package/dist/views/FormEditor.d.ts.map +1 -1
  31. package/dist/views/FormEditor.js +211 -116
  32. package/dist/views/FormEditor.js.map +1 -1
  33. package/dist/views/FormEntries.d.ts.map +1 -1
  34. package/dist/views/FormEntries.js +8 -36
  35. package/dist/views/FormEntries.js.map +1 -1
  36. package/dist/views/FormSubmissions.d.ts.map +1 -1
  37. package/dist/views/FormSubmissions.js +7 -36
  38. package/dist/views/FormSubmissions.js.map +1 -1
  39. package/package.json +9 -9
  40. package/src/__tests__/components/attribution-rows.test.ts +93 -0
  41. package/src/__tests__/views/form-editor.render.test.tsx +186 -0
  42. package/src/__tests__/views/form-entries.render.test.tsx +29 -0
  43. package/src/__tests__/views/form-submissions.render.test.tsx +17 -0
  44. package/src/components/forms/ExportMenu.tsx +70 -0
  45. package/src/components/forms/FormEntryDetailPane.tsx +82 -8
  46. package/src/components/forms/WebhooksPanel.tsx +5 -1
  47. package/src/lib/forms-service.ts +61 -2
  48. package/src/views/FormEditor.tsx +655 -510
  49. package/src/views/FormEntries.tsx +7 -46
  50. package/src/views/FormSubmissions.tsx +6 -46
@@ -2,13 +2,13 @@
2
2
 
3
3
  /**
4
4
  * All Entries — a cross-form submissions inbox. Same token-only table, search,
5
- * status pills (All / Unread / Starred / Archived), bulk actions, CSV export,
6
- * pagination, and inline detail pane as the per-form Submissions view, plus a
7
- * Form filter and a Form column so editors can triage every form from one
8
- * place.
5
+ * status pills (All / Unread / Starred / Archived), bulk actions, server-side
6
+ * CSV/Excel export, pagination, and inline detail pane as the per-form
7
+ * Submissions view, plus a Form filter and a Form column so editors can
8
+ * triage every form from one place.
9
9
  */
10
10
  import { useCallback, useEffect, useState } from 'react'
11
- import { Download, Inbox, Search } from 'lucide-react'
11
+ import { Inbox, Search } from 'lucide-react'
12
12
  import { toast } from 'sonner'
13
13
  import {
14
14
  bulkUpdateEntries,
@@ -25,6 +25,7 @@ import {
25
25
  import { emitFormsChanged, onFormsChanged } from '../lib/forms-events.js'
26
26
  import { FormEntryDetailPane } from '../components/forms/FormEntryDetailPane.js'
27
27
  import { EntriesTable } from '../components/forms/EntriesTable.js'
28
+ import { ExportMenu } from '../components/forms/ExportMenu.js'
28
29
  import { ConfirmDialog } from '../components/ui/ConfirmDialog.js'
29
30
  import { InspectorPane, SplitPaneLayout, SplitPaneList } from '../components/ui/InspectorPane.js'
30
31
  import {
@@ -33,7 +34,6 @@ import {
33
34
  FormsLoading,
34
35
  FormsPagination,
35
36
  SummaryChip,
36
- btnSecondary,
37
37
  } from '../components/forms/primitives.js'
38
38
 
39
39
  export interface FormEntriesProps {
@@ -64,37 +64,6 @@ function filterToParams(filter: StatusFilter): Partial<FetchFormEntriesParams> {
64
64
  }
65
65
  }
66
66
 
67
- function csvEscape(value: unknown): string {
68
- const s = value == null ? '' : typeof value === 'object' ? JSON.stringify(value) : String(value)
69
- return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s
70
- }
71
-
72
- function exportCsv(entries: FormSubmission[]) {
73
- const dataKeys = [...new Set(entries.flatMap((e) => Object.keys(e.data ?? {})))]
74
- const header = ['Entry', 'Form', 'Submitted', 'Name', 'Email', 'Phone', 'Status', ...dataKeys]
75
- const lines = [header.map(csvEscape).join(',')]
76
- for (const e of entries) {
77
- const row = [
78
- e.entryNumber ?? '',
79
- e.formName ?? '',
80
- e.submittedAt,
81
- e.senderName ?? '',
82
- e.senderEmail ?? '',
83
- e.senderPhone ?? '',
84
- e.unread ? 'unread' : 'read',
85
- ...dataKeys.map((k) => e.data?.[k]),
86
- ]
87
- lines.push(row.map(csvEscape).join(','))
88
- }
89
- const blob = new Blob([lines.join('\n')], { type: 'text/csv;charset=utf-8' })
90
- const url = URL.createObjectURL(blob)
91
- const a = document.createElement('a')
92
- a.href = url
93
- a.download = 'all-entries.csv'
94
- a.click()
95
- URL.revokeObjectURL(url)
96
- }
97
-
98
67
  export function FormEntries({ onNavigate }: FormEntriesProps) {
99
68
  const [entries, setEntries] = useState<FormSubmission[]>([])
100
69
  const [total, setTotal] = useState(0)
@@ -240,15 +209,7 @@ export function FormEntries({ onNavigate }: FormEntriesProps) {
240
209
  {total} {total === 1 ? 'submission' : 'submissions'} across your forms
241
210
  </p>
242
211
  </div>
243
- <button
244
- type="button"
245
- className={btnSecondary}
246
- onClick={() => exportCsv(entries)}
247
- disabled={entries.length === 0}
248
- >
249
- <Download className="h-4 w-4" aria-hidden />
250
- Export CSV
251
- </button>
212
+ <ExportMenu formId={scopedFormId} disabled={counts !== null && counts.total === 0} />
252
213
  </div>
253
214
 
254
215
  {counts && (
@@ -6,12 +6,12 @@
6
6
  *
7
7
  * Features: summary chips, search + status pills (All / Unread / Starred /
8
8
  * Archived), row selection with bulk actions (mark read, star, archive,
9
- * delete), per-row star toggle, CSV export, and an inline detail pane that
10
- * opens on row click (marking the entry read). Loading / empty /
11
- * filtered-empty / error states are all handled.
9
+ * delete), per-row star toggle, server-side CSV/Excel export, and an inline
10
+ * detail pane that opens on row click (marking the entry read). Loading /
11
+ * empty / filtered-empty / error states are all handled.
12
12
  */
13
13
  import { useCallback, useEffect, useState } from 'react'
14
- import { ArrowLeft, Download, Inbox, Search } from 'lucide-react'
14
+ import { ArrowLeft, Inbox, Search } from 'lucide-react'
15
15
  import { toast } from 'sonner'
16
16
  import {
17
17
  bulkUpdateEntries,
@@ -27,6 +27,7 @@ import {
27
27
  import { emitFormsChanged, onFormsChanged } from '../lib/forms-events.js'
28
28
  import { FormEntryDetailPane } from '../components/forms/FormEntryDetailPane.js'
29
29
  import { EntriesTable } from '../components/forms/EntriesTable.js'
30
+ import { ExportMenu } from '../components/forms/ExportMenu.js'
30
31
  import { ConfirmDialog } from '../components/ui/ConfirmDialog.js'
31
32
  import { InspectorPane, SplitPaneLayout, SplitPaneList } from '../components/ui/InspectorPane.js'
32
33
  import {
@@ -35,7 +36,6 @@ import {
35
36
  FormsLoading,
36
37
  FormsPagination,
37
38
  SummaryChip,
38
- btnSecondary,
39
39
  } from '../components/forms/primitives.js'
40
40
 
41
41
  export interface FormSubmissionsProps {
@@ -67,39 +67,8 @@ function filterToParams(filter: StatusFilter): Partial<FetchFormEntriesParams> {
67
67
  }
68
68
  }
69
69
 
70
- function csvEscape(value: unknown): string {
71
- const s = value == null ? '' : typeof value === 'object' ? JSON.stringify(value) : String(value)
72
- return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s
73
- }
74
-
75
- function exportCsv(entries: FormSubmission[], formSlug: string) {
76
- const dataKeys = [...new Set(entries.flatMap((e) => Object.keys(e.data ?? {})))]
77
- const header = ['Entry', 'Submitted', 'Name', 'Email', 'Phone', 'Status', ...dataKeys]
78
- const lines = [header.map(csvEscape).join(',')]
79
- for (const e of entries) {
80
- const row = [
81
- e.entryNumber ?? '',
82
- e.submittedAt,
83
- e.senderName ?? '',
84
- e.senderEmail ?? '',
85
- e.senderPhone ?? '',
86
- e.unread ? 'unread' : 'read',
87
- ...dataKeys.map((k) => e.data?.[k]),
88
- ]
89
- lines.push(row.map(csvEscape).join(','))
90
- }
91
- const blob = new Blob([lines.join('\n')], { type: 'text/csv;charset=utf-8' })
92
- const url = URL.createObjectURL(blob)
93
- const a = document.createElement('a')
94
- a.href = url
95
- a.download = `${formSlug || 'form'}-submissions.csv`
96
- a.click()
97
- URL.revokeObjectURL(url)
98
- }
99
-
100
70
  export function FormSubmissions({ formId, onNavigate }: FormSubmissionsProps) {
101
71
  const [formName, setFormName] = useState<string>('')
102
- const [formSlug, setFormSlug] = useState<string>('')
103
72
  const [entries, setEntries] = useState<FormSubmission[]>([])
104
73
  const [total, setTotal] = useState(0)
105
74
  const [counts, setCounts] = useState<EntryCounts | null>(null)
@@ -119,7 +88,6 @@ export function FormSubmissions({ formId, onNavigate }: FormSubmissionsProps) {
119
88
  .then((form) => {
120
89
  if (cancelled || !form) return
121
90
  setFormName(form.name)
122
- setFormSlug(form.slug)
123
91
  })
124
92
  .catch(() => {})
125
93
  return () => {
@@ -261,15 +229,7 @@ export function FormSubmissions({ formId, onNavigate }: FormSubmissionsProps) {
261
229
  {total} {total === 1 ? 'submission' : 'submissions'}
262
230
  </p>
263
231
  </div>
264
- <button
265
- type="button"
266
- className={btnSecondary}
267
- onClick={() => exportCsv(entries, formSlug)}
268
- disabled={entries.length === 0}
269
- >
270
- <Download className="h-4 w-4" aria-hidden />
271
- Export CSV
272
- </button>
232
+ <ExportMenu formId={formId} disabled={counts !== null && counts.total === 0} />
273
233
  </div>
274
234
  </div>
275
235