@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.
- package/dist/__tests__/components/forms-pagination.render.test.d.ts +2 -0
- package/dist/__tests__/components/forms-pagination.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/forms-pagination.render.test.js +33 -0
- package/dist/__tests__/components/forms-pagination.render.test.js.map +1 -0
- package/dist/__tests__/layout/sidebar-forms-badge.render.test.d.ts +2 -0
- package/dist/__tests__/layout/sidebar-forms-badge.render.test.d.ts.map +1 -0
- package/dist/__tests__/layout/sidebar-forms-badge.render.test.js +30 -0
- package/dist/__tests__/layout/sidebar-forms-badge.render.test.js.map +1 -0
- package/dist/__tests__/layout/sidebar-submenu.render.test.js +8 -2
- package/dist/__tests__/layout/sidebar-submenu.render.test.js.map +1 -1
- package/dist/__tests__/views/form-submissions.render.test.d.ts +2 -0
- package/dist/__tests__/views/form-submissions.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/form-submissions.render.test.js +110 -0
- package/dist/__tests__/views/form-submissions.render.test.js.map +1 -0
- package/dist/__tests__/views/forms-list.render.test.d.ts +2 -0
- package/dist/__tests__/views/forms-list.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/forms-list.render.test.js +122 -0
- package/dist/__tests__/views/forms-list.render.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/components/forms/Drawer.d.ts +13 -0
- package/dist/components/forms/Drawer.d.ts.map +1 -0
- package/dist/components/forms/Drawer.js +13 -0
- package/dist/components/forms/Drawer.js.map +1 -0
- package/dist/components/forms/EmbedPanel.d.ts +7 -0
- package/dist/components/forms/EmbedPanel.d.ts.map +1 -0
- package/dist/components/forms/EmbedPanel.js +91 -0
- package/dist/components/forms/EmbedPanel.js.map +1 -0
- package/dist/components/forms/EntryDetailDrawer.d.ts +11 -0
- package/dist/components/forms/EntryDetailDrawer.d.ts.map +1 -0
- package/dist/components/forms/EntryDetailDrawer.js +145 -0
- package/dist/components/forms/EntryDetailDrawer.js.map +1 -0
- package/dist/components/forms/FieldsPanel.d.ts +8 -0
- package/dist/components/forms/FieldsPanel.d.ts.map +1 -0
- package/dist/components/forms/FieldsPanel.js +123 -0
- package/dist/components/forms/FieldsPanel.js.map +1 -0
- package/dist/components/forms/FormSchemaDrawer.d.ts +9 -0
- package/dist/components/forms/FormSchemaDrawer.d.ts.map +1 -0
- package/dist/components/forms/FormSchemaDrawer.js +96 -0
- package/dist/components/forms/FormSchemaDrawer.js.map +1 -0
- package/dist/components/forms/NotificationsPanel.d.ts +6 -0
- package/dist/components/forms/NotificationsPanel.d.ts.map +1 -0
- package/dist/components/forms/NotificationsPanel.js +80 -0
- package/dist/components/forms/NotificationsPanel.js.map +1 -0
- package/dist/components/forms/primitives.d.ts +57 -0
- package/dist/components/forms/primitives.d.ts.map +1 -0
- package/dist/components/forms/primitives.js +131 -0
- package/dist/components/forms/primitives.js.map +1 -0
- package/dist/layout/Sidebar.d.ts +5 -0
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +43 -3
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/lib/forms-events.d.ts +17 -0
- package/dist/lib/forms-events.d.ts.map +1 -0
- package/dist/lib/forms-events.js +20 -0
- package/dist/lib/forms-events.js.map +1 -0
- package/dist/lib/forms-service.d.ts +114 -0
- package/dist/lib/forms-service.d.ts.map +1 -0
- package/dist/lib/forms-service.js +210 -0
- package/dist/lib/forms-service.js.map +1 -0
- package/dist/views/FormSubmissions.d.ts.map +1 -1
- package/dist/views/FormSubmissions.js +224 -77
- package/dist/views/FormSubmissions.js.map +1 -1
- package/dist/views/Forms.d.ts.map +1 -1
- package/dist/views/Forms.js +135 -17
- package/dist/views/Forms.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/components/forms-pagination.render.test.tsx +49 -0
- package/src/__tests__/layout/sidebar-forms-badge.render.test.tsx +50 -0
- package/src/__tests__/layout/sidebar-submenu.render.test.tsx +9 -2
- package/src/__tests__/views/form-submissions.render.test.tsx +129 -0
- package/src/__tests__/views/forms-list.render.test.tsx +141 -0
- package/src/components/forms/Drawer.tsx +70 -0
- package/src/components/forms/EmbedPanel.tsx +173 -0
- package/src/components/forms/EntryDetailDrawer.tsx +312 -0
- package/src/components/forms/FieldsPanel.tsx +385 -0
- package/src/components/forms/FormSchemaDrawer.tsx +185 -0
- package/src/components/forms/NotificationsPanel.tsx +240 -0
- package/src/components/forms/primitives.tsx +300 -0
- package/src/layout/Sidebar.tsx +72 -6
- package/src/lib/forms-events.ts +32 -0
- package/src/lib/forms-service.ts +343 -0
- package/src/views/FormSubmissions.tsx +529 -394
- package/src/views/Forms.tsx +381 -106
|
@@ -0,0 +1,210 @@
|
|
|
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
|
+
/**
|
|
15
|
+
* `cmsApi` never throws — it returns `{ error, status }` on a network/HTTP
|
|
16
|
+
* failure. Read (GET) fetchers must re-surface that as a thrown error so the
|
|
17
|
+
* resource hook can distinguish "request failed" from "empty result".
|
|
18
|
+
*/
|
|
19
|
+
function throwIfError(res) {
|
|
20
|
+
if (res.error)
|
|
21
|
+
throw new Error(res.error);
|
|
22
|
+
}
|
|
23
|
+
// ─── Forms list / stats / sidebar ────────────────────────────────────────
|
|
24
|
+
function buildFormsQuery(params) {
|
|
25
|
+
const qs = new URLSearchParams();
|
|
26
|
+
if (params.search)
|
|
27
|
+
qs.set('search', params.search);
|
|
28
|
+
if (params.status)
|
|
29
|
+
qs.set('status', params.status);
|
|
30
|
+
if (typeof params.notifyEnabled === 'boolean') {
|
|
31
|
+
qs.set('notifyEnabled', String(params.notifyEnabled));
|
|
32
|
+
}
|
|
33
|
+
if (params.sortBy)
|
|
34
|
+
qs.set('sortBy', params.sortBy);
|
|
35
|
+
if (params.sortDirection)
|
|
36
|
+
qs.set('sortDirection', params.sortDirection);
|
|
37
|
+
if (params.page)
|
|
38
|
+
qs.set('page', String(params.page));
|
|
39
|
+
if (params.pageSize)
|
|
40
|
+
qs.set('pageSize', String(params.pageSize));
|
|
41
|
+
const str = qs.toString();
|
|
42
|
+
return str ? `?${str}` : '';
|
|
43
|
+
}
|
|
44
|
+
export async function fetchForms(params = {}) {
|
|
45
|
+
const res = await cmsApi(`/forms${buildFormsQuery(params)}`);
|
|
46
|
+
throwIfError(res);
|
|
47
|
+
return res.data ?? [];
|
|
48
|
+
}
|
|
49
|
+
export async function fetchFormById(id) {
|
|
50
|
+
const res = await cmsApi(`/forms/${encodeURIComponent(id)}`);
|
|
51
|
+
throwIfError(res);
|
|
52
|
+
return res.data ?? null;
|
|
53
|
+
}
|
|
54
|
+
const EMPTY_STATS = {
|
|
55
|
+
totalForms: 0,
|
|
56
|
+
activeForms: 0,
|
|
57
|
+
totalEntries: 0,
|
|
58
|
+
unreadEntries: 0,
|
|
59
|
+
};
|
|
60
|
+
export async function fetchFormStats() {
|
|
61
|
+
const res = await cmsApi('/forms/stats');
|
|
62
|
+
throwIfError(res);
|
|
63
|
+
return res.data ?? EMPTY_STATS;
|
|
64
|
+
}
|
|
65
|
+
export async function fetchFormsSidebarCounts() {
|
|
66
|
+
const res = await cmsApi('/forms/sidebar-counts');
|
|
67
|
+
throwIfError(res);
|
|
68
|
+
return res.data ?? [];
|
|
69
|
+
}
|
|
70
|
+
export async function createForm(payload) {
|
|
71
|
+
const res = await cmsApi('/forms', {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
body: JSON.stringify(payload),
|
|
74
|
+
});
|
|
75
|
+
return { data: res.data, error: res.error };
|
|
76
|
+
}
|
|
77
|
+
export async function updateForm(id, payload) {
|
|
78
|
+
const res = await cmsApi(`/forms/${encodeURIComponent(id)}`, {
|
|
79
|
+
method: 'PATCH',
|
|
80
|
+
body: JSON.stringify(payload),
|
|
81
|
+
});
|
|
82
|
+
return { data: res.data, error: res.error };
|
|
83
|
+
}
|
|
84
|
+
export async function duplicateForm(id) {
|
|
85
|
+
const res = await cmsApi(`/forms/${encodeURIComponent(id)}/duplicate`, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
});
|
|
88
|
+
return { data: res.data, error: res.error };
|
|
89
|
+
}
|
|
90
|
+
/** Move a form to the archived state (hidden from embed pickers, no new submissions). */
|
|
91
|
+
export async function archiveForm(id) {
|
|
92
|
+
return updateForm(id, { status: 'archived' });
|
|
93
|
+
}
|
|
94
|
+
/** Restore an archived form back to draft. */
|
|
95
|
+
export async function restoreForm(id) {
|
|
96
|
+
return updateForm(id, { status: 'draft' });
|
|
97
|
+
}
|
|
98
|
+
export async function deleteForm(id, opts = {}) {
|
|
99
|
+
const qs = opts.force ? '?force=true' : '';
|
|
100
|
+
const res = await cmsApi(`/forms/${encodeURIComponent(id)}${qs}`, { method: 'DELETE' });
|
|
101
|
+
return { error: res.error };
|
|
102
|
+
}
|
|
103
|
+
// ─── Schema ──────────────────────────────────────────────────────────────
|
|
104
|
+
export async function fetchFormSchema(id) {
|
|
105
|
+
const res = await cmsApi(`/forms/${encodeURIComponent(id)}/schema`);
|
|
106
|
+
throwIfError(res);
|
|
107
|
+
return res.data ?? { fields: [], activeVersionId: null, versions: [] };
|
|
108
|
+
}
|
|
109
|
+
export async function saveFormSchema(id, fields, notes) {
|
|
110
|
+
const res = await cmsApi(`/forms/${encodeURIComponent(id)}/schema`, { method: 'PUT', body: JSON.stringify({ fields, notes }) });
|
|
111
|
+
return { data: res.data, error: res.error };
|
|
112
|
+
}
|
|
113
|
+
// ─── Notifications ─────────────────────────────────────────────────────────
|
|
114
|
+
export async function fetchNotificationSettings(id) {
|
|
115
|
+
const res = await cmsApi(`/forms/${encodeURIComponent(id)}/notifications`);
|
|
116
|
+
throwIfError(res);
|
|
117
|
+
return res.data ?? { enabled: false, recipients: [] };
|
|
118
|
+
}
|
|
119
|
+
export async function updateNotificationSettings(id, payload) {
|
|
120
|
+
const res = await cmsApi(`/forms/${encodeURIComponent(id)}/notifications`, { method: 'PUT', body: JSON.stringify(payload) });
|
|
121
|
+
return { data: res.data, error: res.error };
|
|
122
|
+
}
|
|
123
|
+
/** Lightweight notify on/off toggle used by the forms-list switch. */
|
|
124
|
+
export async function setNotifyEnabled(id, enabled) {
|
|
125
|
+
const res = await cmsApi(`/forms/${encodeURIComponent(id)}/notify`, {
|
|
126
|
+
method: 'PUT',
|
|
127
|
+
body: JSON.stringify({ enabled }),
|
|
128
|
+
});
|
|
129
|
+
return { data: res.data, error: res.error };
|
|
130
|
+
}
|
|
131
|
+
// ─── Embed ───────────────────────────────────────────────────────────────
|
|
132
|
+
export async function fetchEmbedSettings(id) {
|
|
133
|
+
const res = await cmsApi(`/forms/${encodeURIComponent(id)}/embed`);
|
|
134
|
+
throwIfError(res);
|
|
135
|
+
return res.data ?? {};
|
|
136
|
+
}
|
|
137
|
+
export async function updateEmbedSettings(id, payload) {
|
|
138
|
+
const res = await cmsApi(`/forms/${encodeURIComponent(id)}/embed`, {
|
|
139
|
+
method: 'PUT',
|
|
140
|
+
body: JSON.stringify(payload),
|
|
141
|
+
});
|
|
142
|
+
return { data: res.data, error: res.error };
|
|
143
|
+
}
|
|
144
|
+
function buildEntriesQuery(params) {
|
|
145
|
+
const qs = new URLSearchParams();
|
|
146
|
+
if (params.search)
|
|
147
|
+
qs.set('search', params.search);
|
|
148
|
+
if (params.formId)
|
|
149
|
+
qs.set('formId', params.formId);
|
|
150
|
+
if (typeof params.unread === 'boolean')
|
|
151
|
+
qs.set('unread', String(params.unread));
|
|
152
|
+
if (typeof params.starred === 'boolean')
|
|
153
|
+
qs.set('starred', String(params.starred));
|
|
154
|
+
if (typeof params.archived === 'boolean')
|
|
155
|
+
qs.set('archived', String(params.archived));
|
|
156
|
+
if (params.spamStatus)
|
|
157
|
+
qs.set('spamStatus', params.spamStatus);
|
|
158
|
+
if (params.dateFrom)
|
|
159
|
+
qs.set('dateFrom', params.dateFrom);
|
|
160
|
+
if (params.dateTo)
|
|
161
|
+
qs.set('dateTo', params.dateTo);
|
|
162
|
+
if (params.thisWeek)
|
|
163
|
+
qs.set('thisWeek', 'true');
|
|
164
|
+
if (params.sortBy)
|
|
165
|
+
qs.set('sortBy', params.sortBy);
|
|
166
|
+
if (params.sortDirection)
|
|
167
|
+
qs.set('sortDirection', params.sortDirection);
|
|
168
|
+
if (params.page)
|
|
169
|
+
qs.set('page', String(params.page));
|
|
170
|
+
if (params.pageSize)
|
|
171
|
+
qs.set('pageSize', String(params.pageSize));
|
|
172
|
+
const str = qs.toString();
|
|
173
|
+
return str ? `?${str}` : '';
|
|
174
|
+
}
|
|
175
|
+
const EMPTY_PAGE = { entries: [], total: 0, page: 1, pageSize: 25 };
|
|
176
|
+
export async function fetchEntries(params = {}) {
|
|
177
|
+
const res = await cmsApi(`/forms/entries${buildEntriesQuery(params)}`);
|
|
178
|
+
throwIfError(res);
|
|
179
|
+
return res.data ?? EMPTY_PAGE;
|
|
180
|
+
}
|
|
181
|
+
const EMPTY_COUNTS = { total: 0, unread: 0, starred: 0, thisWeek: 0, byForm: {} };
|
|
182
|
+
export async function fetchEntryCounts(params = {}) {
|
|
183
|
+
const res = await cmsApi(`/forms/entries/counts${buildEntriesQuery(params)}`);
|
|
184
|
+
throwIfError(res);
|
|
185
|
+
return res.data ?? EMPTY_COUNTS;
|
|
186
|
+
}
|
|
187
|
+
export async function fetchEntry(id) {
|
|
188
|
+
const res = await cmsApi(`/forms/entries/${encodeURIComponent(id)}`);
|
|
189
|
+
throwIfError(res);
|
|
190
|
+
return res.data ?? null;
|
|
191
|
+
}
|
|
192
|
+
export async function updateEntry(id, patch) {
|
|
193
|
+
const res = await cmsApi(`/forms/entries/${encodeURIComponent(id)}`, {
|
|
194
|
+
method: 'PATCH',
|
|
195
|
+
body: JSON.stringify(patch),
|
|
196
|
+
});
|
|
197
|
+
return { data: res.data, error: res.error };
|
|
198
|
+
}
|
|
199
|
+
export async function deleteEntry(id) {
|
|
200
|
+
const res = await cmsApi(`/forms/entries/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
201
|
+
return { error: res.error };
|
|
202
|
+
}
|
|
203
|
+
export async function bulkUpdateEntries(ids, patch) {
|
|
204
|
+
const res = await cmsApi('/forms/entries/bulk', {
|
|
205
|
+
method: 'POST',
|
|
206
|
+
body: JSON.stringify({ ids, patch }),
|
|
207
|
+
});
|
|
208
|
+
return { data: res.data, error: res.error };
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=forms-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forms-service.js","sourceRoot":"","sources":["../../src/lib/forms-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormSubmissions.d.ts","sourceRoot":"","sources":["../../src/views/FormSubmissions.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"FormSubmissions.d.ts","sourceRoot":"","sources":["../../src/views/FormSubmissions.tsx"],"names":[],"mappings":"AAuCA,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CACpC;AAqED,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,oBAAoB,2CA+V3E"}
|
|
@@ -1,88 +1,235 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Per-form submissions (entries) — a compact, token-only table wired to the
|
|
5
|
+
* real entries API (`lib/forms-service.ts`). Replaces the previous card list.
|
|
6
|
+
*
|
|
7
|
+
* Features: summary chips, search + status pills (All / Unread / Starred /
|
|
8
|
+
* Archived), row selection with bulk actions (mark read, star, archive,
|
|
9
|
+
* delete), per-row star toggle, CSV export, and a detail drawer that opens on
|
|
10
|
+
* row click (marking the entry read). Loading / empty / filtered-empty / error
|
|
11
|
+
* states are all handled.
|
|
12
|
+
*/
|
|
13
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
14
|
+
import { ArrowLeft, Download, Inbox, Search, Star } from 'lucide-react';
|
|
5
15
|
import { toast } from 'sonner';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
import { bulkUpdateEntries, deleteEntry, fetchEntries, fetchEntryCounts, fetchFormById, updateEntry, } from '../lib/forms-service.js';
|
|
17
|
+
import { emitFormsChanged, onFormsChanged } from '../lib/forms-events.js';
|
|
18
|
+
import { EntryDetailDrawer } from '../components/forms/EntryDetailDrawer.js';
|
|
19
|
+
import { ConfirmDialog } from '../components/ui/ConfirmDialog.js';
|
|
20
|
+
import { FormsEmptyState, FormsErrorState, FormsLoading, FormsPagination, SummaryChip, btnGhostIcon, btnSecondary, } from '../components/forms/primitives.js';
|
|
21
|
+
const FILTERS = [
|
|
22
|
+
{ id: 'all', label: 'All' },
|
|
23
|
+
{ id: 'unread', label: 'Unread' },
|
|
24
|
+
{ id: 'starred', label: 'Starred' },
|
|
25
|
+
{ id: 'archived', label: 'Archived' },
|
|
26
|
+
];
|
|
27
|
+
const PAGE_SIZE = 25;
|
|
28
|
+
function filterToParams(filter) {
|
|
29
|
+
switch (filter) {
|
|
30
|
+
case 'unread':
|
|
31
|
+
return { unread: true };
|
|
32
|
+
case 'starred':
|
|
33
|
+
return { starred: true };
|
|
34
|
+
case 'archived':
|
|
35
|
+
return { archived: true };
|
|
36
|
+
default:
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function formatDateTime(iso) {
|
|
41
|
+
if (!iso)
|
|
42
|
+
return '—';
|
|
43
|
+
const d = new Date(iso);
|
|
44
|
+
if (Number.isNaN(d.getTime()))
|
|
45
|
+
return '—';
|
|
46
|
+
return d.toLocaleString(undefined, {
|
|
47
|
+
month: 'short',
|
|
48
|
+
day: 'numeric',
|
|
49
|
+
year: 'numeric',
|
|
50
|
+
hour: 'numeric',
|
|
51
|
+
minute: '2-digit',
|
|
52
|
+
});
|
|
21
53
|
}
|
|
22
|
-
function
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
return 'bg-orange-100 text-orange-800';
|
|
26
|
-
if (m === 'organic')
|
|
27
|
-
return 'bg-green-100 text-green-800';
|
|
28
|
-
if (m === 'social')
|
|
29
|
-
return 'bg-pink-100 text-pink-800';
|
|
30
|
-
if (m === 'email')
|
|
31
|
-
return 'bg-yellow-100 text-yellow-800';
|
|
32
|
-
if (m === '(none)')
|
|
33
|
-
return 'bg-gray-100 text-gray-600';
|
|
34
|
-
return 'bg-gray-100 text-gray-800';
|
|
54
|
+
function csvEscape(value) {
|
|
55
|
+
const s = value == null ? '' : typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
56
|
+
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
35
57
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
58
|
+
function exportCsv(entries, formSlug) {
|
|
59
|
+
const dataKeys = [...new Set(entries.flatMap((e) => Object.keys(e.data ?? {})))];
|
|
60
|
+
const header = ['Entry', 'Submitted', 'Name', 'Email', 'Phone', 'Status', ...dataKeys];
|
|
61
|
+
const lines = [header.map(csvEscape).join(',')];
|
|
62
|
+
for (const e of entries) {
|
|
63
|
+
const row = [
|
|
64
|
+
e.entryNumber ?? '',
|
|
65
|
+
e.submittedAt,
|
|
66
|
+
e.senderName ?? '',
|
|
67
|
+
e.senderEmail ?? '',
|
|
68
|
+
e.senderPhone ?? '',
|
|
69
|
+
e.unread ? 'unread' : 'read',
|
|
70
|
+
...dataKeys.map((k) => e.data?.[k]),
|
|
71
|
+
];
|
|
72
|
+
lines.push(row.map(csvEscape).join(','));
|
|
73
|
+
}
|
|
74
|
+
const blob = new Blob([lines.join('\n')], { type: 'text/csv;charset=utf-8' });
|
|
75
|
+
const url = URL.createObjectURL(blob);
|
|
76
|
+
const a = document.createElement('a');
|
|
77
|
+
a.href = url;
|
|
78
|
+
a.download = `${formSlug || 'form'}-submissions.csv`;
|
|
79
|
+
a.click();
|
|
80
|
+
URL.revokeObjectURL(url);
|
|
42
81
|
}
|
|
43
82
|
export function FormSubmissions({ formId, onNavigate }) {
|
|
44
|
-
const
|
|
45
|
-
const [
|
|
46
|
-
const [
|
|
47
|
-
const [
|
|
48
|
-
const [
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
83
|
+
const [formName, setFormName] = useState('');
|
|
84
|
+
const [formSlug, setFormSlug] = useState('');
|
|
85
|
+
const [entries, setEntries] = useState([]);
|
|
86
|
+
const [total, setTotal] = useState(0);
|
|
87
|
+
const [counts, setCounts] = useState(null);
|
|
88
|
+
const [loading, setLoading] = useState(true);
|
|
89
|
+
const [error, setError] = useState(null);
|
|
90
|
+
const [search, setSearch] = useState('');
|
|
91
|
+
const [filter, setFilter] = useState('all');
|
|
92
|
+
const [page, setPage] = useState(1);
|
|
93
|
+
const [selected, setSelected] = useState(new Set());
|
|
94
|
+
const [activeEntry, setActiveEntry] = useState(null);
|
|
95
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
96
|
+
const [confirmBulkDelete, setConfirmBulkDelete] = useState(false);
|
|
97
|
+
// Form metadata for the title (loads once).
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
let cancelled = false;
|
|
100
|
+
void fetchFormById(formId)
|
|
101
|
+
.then((form) => {
|
|
102
|
+
if (cancelled || !form)
|
|
103
|
+
return;
|
|
104
|
+
setFormName(form.name);
|
|
105
|
+
setFormSlug(form.slug);
|
|
106
|
+
})
|
|
107
|
+
.catch(() => { });
|
|
108
|
+
return () => {
|
|
109
|
+
cancelled = true;
|
|
110
|
+
};
|
|
111
|
+
}, [formId]);
|
|
112
|
+
const loadCounts = useCallback(() => {
|
|
113
|
+
void fetchEntryCounts({ formId })
|
|
114
|
+
.then(setCounts)
|
|
115
|
+
.catch(() => { });
|
|
116
|
+
}, [formId]);
|
|
117
|
+
const load = useCallback(async () => {
|
|
118
|
+
setLoading(true);
|
|
119
|
+
setError(null);
|
|
120
|
+
try {
|
|
121
|
+
const result = await fetchEntries({
|
|
122
|
+
formId,
|
|
123
|
+
search: search.trim() || undefined,
|
|
124
|
+
page,
|
|
125
|
+
pageSize: PAGE_SIZE,
|
|
126
|
+
...filterToParams(filter),
|
|
127
|
+
});
|
|
128
|
+
setEntries(result.entries);
|
|
129
|
+
setTotal(result.total);
|
|
130
|
+
setSelected(new Set());
|
|
61
131
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const uniqueSources = [...new Set(submissions.map((s) => s.attribution?.source ?? '(direct)'))];
|
|
65
|
-
const sourceSummary = useMemo(() => {
|
|
66
|
-
const counts = {};
|
|
67
|
-
for (const s of submissions) {
|
|
68
|
-
const key = `${s.attribution?.source ?? '(direct)'} / ${s.attribution?.medium ?? '(none)'}`;
|
|
69
|
-
counts[key] = (counts[key] ?? 0) + 1;
|
|
132
|
+
catch (e) {
|
|
133
|
+
setError(e instanceof Error ? e.message : 'Failed to load submissions');
|
|
70
134
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
135
|
+
finally {
|
|
136
|
+
setLoading(false);
|
|
137
|
+
}
|
|
138
|
+
}, [formId, search, filter, page]);
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
void load();
|
|
141
|
+
}, [load]);
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
loadCounts();
|
|
144
|
+
}, [loadCounts]);
|
|
145
|
+
// Refresh when another surface (drawer, sidebar) signals a change.
|
|
146
|
+
useEffect(() => onFormsChanged(() => {
|
|
147
|
+
loadCounts();
|
|
148
|
+
}), [loadCounts]);
|
|
149
|
+
const refresh = useCallback(() => {
|
|
150
|
+
void load();
|
|
151
|
+
loadCounts();
|
|
152
|
+
}, [load, loadCounts]);
|
|
153
|
+
const openEntry = useCallback((id) => {
|
|
154
|
+
setActiveEntry(id);
|
|
155
|
+
setDrawerOpen(true);
|
|
156
|
+
// Reflect read state immediately in the row; the drawer persists it.
|
|
157
|
+
setEntries((cur) => cur.map((e) => (e.id === id ? { ...e, unread: false } : e)));
|
|
158
|
+
}, []);
|
|
159
|
+
const toggleStar = useCallback(async (entry) => {
|
|
160
|
+
const next = !entry.starred;
|
|
161
|
+
setEntries((cur) => cur.map((e) => (e.id === entry.id ? { ...e, starred: next } : e)));
|
|
162
|
+
const res = await updateEntry(entry.id, { starred: next });
|
|
163
|
+
if (res.error) {
|
|
164
|
+
setEntries((cur) => cur.map((e) => (e.id === entry.id ? { ...e, starred: !next } : e)));
|
|
165
|
+
toast.error(res.error);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
emitFormsChanged();
|
|
169
|
+
}, []);
|
|
170
|
+
const allSelected = entries.length > 0 && selected.size === entries.length;
|
|
171
|
+
const toggleSelectAll = useCallback(() => {
|
|
172
|
+
setSelected((cur) => cur.size === entries.length ? new Set() : new Set(entries.map((e) => e.id)));
|
|
173
|
+
}, [entries]);
|
|
174
|
+
const toggleSelect = useCallback((id) => {
|
|
175
|
+
setSelected((cur) => {
|
|
176
|
+
const next = new Set(cur);
|
|
177
|
+
if (next.has(id))
|
|
178
|
+
next.delete(id);
|
|
179
|
+
else
|
|
180
|
+
next.add(id);
|
|
181
|
+
return next;
|
|
182
|
+
});
|
|
183
|
+
}, []);
|
|
184
|
+
const bulkPatch = useCallback(async (patch, label) => {
|
|
185
|
+
const ids = [...selected];
|
|
186
|
+
if (ids.length === 0)
|
|
187
|
+
return;
|
|
188
|
+
const res = await bulkUpdateEntries(ids, patch);
|
|
189
|
+
if (res.error) {
|
|
190
|
+
toast.error(res.error);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
toast.success(`${ids.length} ${ids.length === 1 ? 'entry' : 'entries'} ${label}`);
|
|
194
|
+
emitFormsChanged();
|
|
195
|
+
refresh();
|
|
196
|
+
}, [selected, refresh]);
|
|
197
|
+
const handleBulkDelete = useCallback(async () => {
|
|
198
|
+
const ids = [...selected];
|
|
199
|
+
if (ids.length === 0)
|
|
200
|
+
return;
|
|
201
|
+
const results = await Promise.allSettled(ids.map((id) => deleteEntry(id)));
|
|
202
|
+
const failed = results.filter((r) => r.status === 'rejected' || r.value.error).length;
|
|
203
|
+
if (failed > 0)
|
|
204
|
+
toast.error(`${failed} of ${ids.length} could not be deleted`);
|
|
205
|
+
else
|
|
206
|
+
toast.success(`${ids.length} ${ids.length === 1 ? 'entry' : 'entries'} deleted`);
|
|
207
|
+
emitFormsChanged();
|
|
208
|
+
refresh();
|
|
209
|
+
}, [selected, refresh]);
|
|
210
|
+
const showEmpty = !loading && !error && entries.length === 0;
|
|
211
|
+
const isFiltered = filter !== 'all' || search.trim().length > 0;
|
|
212
|
+
return (_jsxs("div", { className: "space-y-5 p-6", children: [_jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => onNavigate?.('/forms'), className: "text-muted-foreground hover:text-foreground mb-3 inline-flex items-center gap-2 text-sm transition-colors", children: [_jsx(ArrowLeft, { className: "h-4 w-4", "aria-hidden": true }), "Back to Forms"] }), _jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-foreground text-2xl font-medium", children: formName ? `${formName} — Submissions` : 'Submissions' }), _jsxs("p", { className: "text-muted-foreground mt-1 text-sm", children: [total, " ", total === 1 ? 'submission' : 'submissions'] })] }), _jsxs("button", { type: "button", className: btnSecondary, onClick: () => exportCsv(entries, formSlug), disabled: entries.length === 0, children: [_jsx(Download, { className: "h-4 w-4", "aria-hidden": true }), "Export CSV"] })] })] }), counts && (_jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(SummaryChip, { value: counts.total, label: "total" }), _jsx(SummaryChip, { value: counts.unread, label: "unread", tone: counts.unread > 0 ? 'destructive' : 'neutral' }), _jsx(SummaryChip, { value: counts.starred, label: "starred", tone: "warning" }), _jsx(SummaryChip, { value: counts.thisWeek, label: "this week" })] })), _jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between", children: [_jsxs("div", { className: "relative w-full sm:max-w-xs", children: [_jsx(Search, { className: "text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2", "aria-hidden": true }), _jsx("input", { type: "search", placeholder: "Search submissions\u2026", value: search, onChange: (e) => {
|
|
213
|
+
setSearch(e.target.value);
|
|
214
|
+
setPage(1);
|
|
215
|
+
}, "aria-label": "Search submissions", className: "border-border bg-input-background text-foreground focus-visible:ring-ring w-full rounded-lg border py-2 pr-3 pl-9 text-sm focus-visible:ring-2 focus-visible:outline-none" })] }), _jsx("div", { className: "flex gap-1", role: "group", "aria-label": "Filter submissions", children: FILTERS.map((f) => (_jsx("button", { type: "button", onClick: () => {
|
|
216
|
+
setFilter(f.id);
|
|
217
|
+
setPage(1);
|
|
218
|
+
}, "aria-pressed": filter === f.id, className: `rounded-full px-3 py-1.5 text-sm font-medium transition-colors ${filter === f.id
|
|
219
|
+
? 'bg-primary text-primary-foreground'
|
|
220
|
+
: 'text-muted-foreground hover:bg-accent hover:text-foreground'}`, children: f.label }, f.id))) })] }), selected.size > 0 && (_jsxs("div", { className: "border-border bg-accent/40 flex flex-wrap items-center gap-2 rounded-lg border px-3 py-2 text-sm", role: "region", "aria-label": "Bulk actions", children: [_jsxs("span", { className: "text-foreground font-medium", children: [selected.size, " selected"] }), _jsx("span", { className: "bg-border mx-1 h-4 w-px", "aria-hidden": true }), _jsx("button", { type: "button", className: "text-foreground hover:bg-accent rounded-md px-2 py-1 font-medium transition-colors", onClick: () => void bulkPatch({ unread: false }, 'marked read'), children: "Mark read" }), _jsx("button", { type: "button", className: "text-foreground hover:bg-accent rounded-md px-2 py-1 font-medium transition-colors", onClick: () => void bulkPatch({ starred: true }, 'starred'), children: "Star" }), _jsx("button", { type: "button", className: "text-foreground hover:bg-accent rounded-md px-2 py-1 font-medium transition-colors", onClick: () => void bulkPatch({ archived: filter !== 'archived' }, filter !== 'archived' ? 'archived' : 'unarchived'), children: filter === 'archived' ? 'Unarchive' : 'Archive' }), _jsx("button", { type: "button", className: "text-destructive hover:bg-destructive/10 rounded-md px-2 py-1 font-medium transition-colors", onClick: () => setConfirmBulkDelete(true), children: "Delete" }), _jsx("button", { type: "button", className: "text-muted-foreground hover:text-foreground ml-auto rounded-md px-2 py-1 font-medium transition-colors", onClick: () => setSelected(new Set()), children: "Clear" })] })), error ? (_jsx(FormsErrorState, { message: error, onRetry: load })) : loading ? (_jsx(FormsLoading, { label: "Loading submissions\u2026" })) : showEmpty ? (_jsx(FormsEmptyState, { icon: _jsx(Inbox, { className: "h-6 w-6", "aria-hidden": true }), title: isFiltered ? 'No submissions match your filters' : 'No submissions yet', description: isFiltered
|
|
221
|
+
? 'Try a different search term or status filter.'
|
|
222
|
+
: 'Submissions will appear here once your form starts receiving entries.', action: isFiltered ? (_jsx("button", { type: "button", onClick: () => {
|
|
223
|
+
setSearch('');
|
|
224
|
+
setFilter('all');
|
|
225
|
+
setPage(1);
|
|
226
|
+
}, className: "text-primary text-sm font-medium hover:underline", children: "Clear filters" })) : undefined })) : (_jsx(SubmissionsTable, { entries: entries, selected: selected, allSelected: allSelected, onToggleSelectAll: toggleSelectAll, onToggleSelect: toggleSelect, onOpen: openEntry, onToggleStar: toggleStar, footer: _jsx(FormsPagination, { page: page, pageSize: PAGE_SIZE, total: total, onPageChange: setPage, noun: "submission", label: "Submissions pagination" }) })), _jsx(EntryDetailDrawer, { entryId: activeEntry, open: drawerOpen, onOpenChange: setDrawerOpen, onChanged: refresh, onDeleted: () => refresh() }), _jsx(ConfirmDialog, { open: confirmBulkDelete, onClose: () => setConfirmBulkDelete(false), onConfirm: () => void handleBulkDelete(), title: `Delete ${selected.size} ${selected.size === 1 ? 'submission' : 'submissions'}?`, description: "This permanently removes the selected submissions and any attached files. This cannot be undone.", confirmLabel: "Delete", destructive: true })] }));
|
|
227
|
+
}
|
|
228
|
+
function SubmissionsTable({ entries, selected, allSelected, onToggleSelectAll, onToggleSelect, onOpen, onToggleStar, footer, }) {
|
|
229
|
+
return (_jsxs("div", { className: "border-border bg-card rounded-lg border", children: [_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full min-w-[720px] text-sm", "aria-label": "Submissions", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-border text-muted-foreground border-b text-left text-xs", children: [_jsx("th", { scope: "col", className: "w-10 px-4 py-2.5", children: _jsx("input", { type: "checkbox", checked: allSelected, onChange: onToggleSelectAll, "aria-label": "Select all submissions", className: "border-border text-primary focus-visible:ring-ring h-4 w-4 rounded" }) }), _jsx("th", { scope: "col", className: "px-4 py-2.5 font-medium", children: "Sender" }), _jsx("th", { scope: "col", className: "px-4 py-2.5 font-medium", children: "Submission" }), _jsx("th", { scope: "col", className: "px-4 py-2.5 font-medium whitespace-nowrap", children: "Submitted" }), _jsx("th", { scope: "col", className: "w-10 px-4 py-2.5 text-center font-medium", children: _jsx("span", { className: "sr-only", children: "Starred" }) })] }) }), _jsx("tbody", { children: entries.map((entry) => {
|
|
230
|
+
const isSelected = selected.has(entry.id);
|
|
231
|
+
const name = entry.senderName || 'Anonymous';
|
|
232
|
+
return (_jsxs("tr", { className: `border-border hover:bg-accent/40 group cursor-pointer border-b transition-colors last:border-b-0 ${isSelected ? 'bg-accent/30' : ''}`, onClick: () => onOpen(entry.id), children: [_jsx("td", { className: "px-4 py-3", onClick: (e) => e.stopPropagation(), children: _jsx("input", { type: "checkbox", checked: isSelected, onChange: () => onToggleSelect(entry.id), "aria-label": `Select submission from ${name}`, className: "border-border text-primary focus-visible:ring-ring h-4 w-4 rounded" }) }), _jsx("td", { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { className: "bg-muted text-muted-foreground flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-xs font-medium", "aria-hidden": true, children: name.charAt(0).toUpperCase() }), _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2", children: [entry.unread && (_jsx("span", { className: "bg-primary h-1.5 w-1.5 shrink-0 rounded-full", "aria-label": "Unread" })), _jsx("span", { className: `text-foreground truncate ${entry.unread ? 'font-semibold' : 'font-medium'}`, children: name })] }), entry.senderEmail && (_jsx("span", { className: "text-muted-foreground block truncate text-xs", children: entry.senderEmail }))] })] }) }), _jsx("td", { className: "px-4 py-3", children: _jsx("span", { className: "text-muted-foreground line-clamp-1 max-w-md", children: entry.preview || '—' }) }), _jsx("td", { className: "text-muted-foreground px-4 py-3 whitespace-nowrap", children: formatDateTime(entry.submittedAt) }), _jsx("td", { className: "px-4 py-3 text-center", onClick: (e) => e.stopPropagation(), children: _jsx("button", { type: "button", className: btnGhostIcon, "aria-pressed": entry.starred, "aria-label": entry.starred ? 'Unstar submission' : 'Star submission', onClick: () => onToggleStar(entry), children: _jsx(Star, { className: `h-4 w-4 ${entry.starred ? 'fill-warning text-warning' : ''}`, "aria-hidden": true }) }) })] }, entry.id));
|
|
233
|
+
}) })] }) }), footer] }));
|
|
87
234
|
}
|
|
88
235
|
//# sourceMappingURL=FormSubmissions.js.map
|