@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.
- package/CHANGELOG.md +38 -0
- package/dist/__tests__/components/attribution-rows.test.d.ts +2 -0
- package/dist/__tests__/components/attribution-rows.test.d.ts.map +1 -0
- package/dist/__tests__/components/attribution-rows.test.js +82 -0
- package/dist/__tests__/components/attribution-rows.test.js.map +1 -0
- package/dist/__tests__/views/form-editor.render.test.d.ts +2 -0
- package/dist/__tests__/views/form-editor.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/form-editor.render.test.js +146 -0
- package/dist/__tests__/views/form-editor.render.test.js.map +1 -0
- package/dist/__tests__/views/form-entries.render.test.js +20 -0
- package/dist/__tests__/views/form-entries.render.test.js.map +1 -1
- package/dist/__tests__/views/form-submissions.render.test.js +12 -0
- package/dist/__tests__/views/form-submissions.render.test.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/forms/ExportMenu.d.ts +8 -0
- package/dist/components/forms/ExportMenu.d.ts.map +1 -0
- package/dist/components/forms/ExportMenu.js +32 -0
- package/dist/components/forms/ExportMenu.js.map +1 -0
- package/dist/components/forms/FormEntryDetailPane.d.ts +13 -0
- package/dist/components/forms/FormEntryDetailPane.d.ts.map +1 -1
- package/dist/components/forms/FormEntryDetailPane.js +74 -8
- package/dist/components/forms/FormEntryDetailPane.js.map +1 -1
- package/dist/components/forms/WebhooksPanel.d.ts.map +1 -1
- package/dist/components/forms/WebhooksPanel.js +1 -1
- package/dist/components/forms/WebhooksPanel.js.map +1 -1
- package/dist/lib/forms-service.d.ts +29 -3
- package/dist/lib/forms-service.d.ts.map +1 -1
- package/dist/lib/forms-service.js +37 -1
- package/dist/lib/forms-service.js.map +1 -1
- package/dist/views/FormEditor.d.ts.map +1 -1
- package/dist/views/FormEditor.js +211 -116
- package/dist/views/FormEditor.js.map +1 -1
- package/dist/views/FormEntries.d.ts.map +1 -1
- package/dist/views/FormEntries.js +8 -36
- package/dist/views/FormEntries.js.map +1 -1
- package/dist/views/FormSubmissions.d.ts.map +1 -1
- package/dist/views/FormSubmissions.js +7 -36
- package/dist/views/FormSubmissions.js.map +1 -1
- package/package.json +9 -9
- package/src/__tests__/components/attribution-rows.test.ts +93 -0
- package/src/__tests__/views/form-editor.render.test.tsx +186 -0
- package/src/__tests__/views/form-entries.render.test.tsx +29 -0
- package/src/__tests__/views/form-submissions.render.test.tsx +17 -0
- package/src/components/forms/ExportMenu.tsx +70 -0
- package/src/components/forms/FormEntryDetailPane.tsx +82 -8
- package/src/components/forms/WebhooksPanel.tsx +5 -1
- package/src/lib/forms-service.ts +61 -2
- package/src/views/FormEditor.tsx +655 -510
- package/src/views/FormEntries.tsx +7 -46
- 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,
|
|
6
|
-
* pagination, and inline detail pane as the per-form
|
|
7
|
-
* Form filter and a Form column so editors can
|
|
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 {
|
|
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
|
-
<
|
|
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
|
|
10
|
-
* opens on row click (marking the entry read). Loading /
|
|
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,
|
|
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
|
-
<
|
|
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
|
|