@actuate-media/cms-admin 0.22.0 → 0.24.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/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +4 -0
- package/dist/AdminRoot.js.map +1 -1
- 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-submenu.render.test.d.ts +2 -0
- package/dist/__tests__/layout/sidebar-forms-submenu.render.test.d.ts.map +1 -0
- package/dist/__tests__/layout/sidebar-forms-submenu.render.test.js +28 -0
- package/dist/__tests__/layout/sidebar-forms-submenu.render.test.js.map +1 -0
- package/dist/__tests__/views/form-entries.render.test.d.ts +2 -0
- package/dist/__tests__/views/form-entries.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/form-entries.render.test.js +112 -0
- package/dist/__tests__/views/form-entries.render.test.js.map +1 -0
- 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/actuate-admin.css +1 -1
- package/dist/components/forms/EntriesTable.d.ts +28 -0
- package/dist/components/forms/EntriesTable.d.ts.map +1 -0
- package/dist/components/forms/EntriesTable.js +26 -0
- package/dist/components/forms/EntriesTable.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/NotificationsPanel.js +1 -1
- package/dist/components/forms/NotificationsPanel.js.map +1 -1
- package/dist/components/forms/primitives.d.ts +15 -0
- package/dist/components/forms/primitives.d.ts.map +1 -1
- package/dist/components/forms/primitives.js +36 -1
- package/dist/components/forms/primitives.js.map +1 -1
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +44 -21
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/lib/forms-service.d.ts +36 -2
- package/dist/lib/forms-service.d.ts.map +1 -1
- package/dist/lib/forms-service.js +66 -0
- package/dist/lib/forms-service.js.map +1 -1
- package/dist/views/FormEntries.d.ts +5 -0
- package/dist/views/FormEntries.d.ts.map +1 -0
- package/dist/views/FormEntries.js +211 -0
- package/dist/views/FormEntries.js.map +1 -0
- package/dist/views/FormSubmissions.d.ts.map +1 -1
- package/dist/views/FormSubmissions.js +204 -77
- package/dist/views/FormSubmissions.js.map +1 -1
- package/dist/views/Forms.d.ts.map +1 -1
- package/dist/views/Forms.js +26 -10
- package/dist/views/Forms.js.map +1 -1
- package/package.json +1 -1
- package/src/AdminRoot.tsx +4 -0
- package/src/__tests__/components/forms-pagination.render.test.tsx +49 -0
- package/src/__tests__/layout/sidebar-forms-submenu.render.test.tsx +38 -0
- package/src/__tests__/views/form-entries.render.test.tsx +133 -0
- package/src/__tests__/views/form-submissions.render.test.tsx +129 -0
- package/src/components/forms/EntriesTable.tsx +197 -0
- package/src/components/forms/EntryDetailDrawer.tsx +312 -0
- package/src/components/forms/NotificationsPanel.tsx +1 -1
- package/src/components/forms/primitives.tsx +101 -1
- package/src/layout/Sidebar.tsx +75 -17
- package/src/lib/forms-service.ts +99 -0
- package/src/views/FormEntries.tsx +449 -0
- package/src/views/FormSubmissions.tsx +391 -401
- package/src/views/Forms.tsx +145 -107
package/src/views/Forms.tsx
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { ChevronRight, ClipboardList, Plus, Search, Settings2 } from 'lucide-react'
|
|
11
11
|
import * as Switch from '@radix-ui/react-switch'
|
|
12
|
-
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
12
|
+
import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'
|
|
13
13
|
import { toast } from 'sonner'
|
|
14
14
|
import { sortByRelevance } from '../lib/search.js'
|
|
15
15
|
import {
|
|
@@ -28,11 +28,14 @@ import {
|
|
|
28
28
|
FormsEmptyState,
|
|
29
29
|
FormsErrorState,
|
|
30
30
|
FormsLoading,
|
|
31
|
+
FormsPagination,
|
|
31
32
|
SummaryChip,
|
|
32
33
|
UnreadBadge,
|
|
33
34
|
btnPrimary,
|
|
34
35
|
} from '../components/forms/primitives.js'
|
|
35
36
|
|
|
37
|
+
const PAGE_SIZE = 10
|
|
38
|
+
|
|
36
39
|
export interface FormsProps {
|
|
37
40
|
onNavigate?: (path: string) => void
|
|
38
41
|
}
|
|
@@ -72,6 +75,7 @@ export function Forms({ onNavigate }: FormsProps) {
|
|
|
72
75
|
const [error, setError] = useState<string | null>(null)
|
|
73
76
|
const [search, setSearch] = useState('')
|
|
74
77
|
const [filter, setFilter] = useState<StatusFilter>('all')
|
|
78
|
+
const [page, setPage] = useState(1)
|
|
75
79
|
const [schemaForm, setSchemaForm] = useState<FormListItem | null>(null)
|
|
76
80
|
const [schemaOpen, setSchemaOpen] = useState(false)
|
|
77
81
|
|
|
@@ -121,6 +125,18 @@ export function Forms({ onNavigate }: FormsProps) {
|
|
|
121
125
|
return results
|
|
122
126
|
}, [forms, filter, search])
|
|
123
127
|
|
|
128
|
+
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE))
|
|
129
|
+
|
|
130
|
+
// Keep the page in range when the filtered set shrinks (filters, refresh).
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (page > totalPages) setPage(totalPages)
|
|
133
|
+
}, [page, totalPages])
|
|
134
|
+
|
|
135
|
+
const paged = useMemo(
|
|
136
|
+
() => filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE),
|
|
137
|
+
[filtered, page],
|
|
138
|
+
)
|
|
139
|
+
|
|
124
140
|
const openSchema = useCallback((form: FormListItem) => {
|
|
125
141
|
setSchemaForm(form)
|
|
126
142
|
setSchemaOpen(true)
|
|
@@ -176,7 +192,10 @@ export function Forms({ onNavigate }: FormsProps) {
|
|
|
176
192
|
type="search"
|
|
177
193
|
placeholder="Search forms…"
|
|
178
194
|
value={search}
|
|
179
|
-
onChange={(e) =>
|
|
195
|
+
onChange={(e) => {
|
|
196
|
+
setSearch(e.target.value)
|
|
197
|
+
setPage(1)
|
|
198
|
+
}}
|
|
180
199
|
aria-label="Search forms"
|
|
181
200
|
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"
|
|
182
201
|
/>
|
|
@@ -186,7 +205,10 @@ export function Forms({ onNavigate }: FormsProps) {
|
|
|
186
205
|
<button
|
|
187
206
|
key={f.id}
|
|
188
207
|
type="button"
|
|
189
|
-
onClick={() =>
|
|
208
|
+
onClick={() => {
|
|
209
|
+
setFilter(f.id)
|
|
210
|
+
setPage(1)
|
|
211
|
+
}}
|
|
190
212
|
aria-pressed={filter === f.id}
|
|
191
213
|
className={`rounded-full px-3 py-1.5 text-sm font-medium transition-colors ${
|
|
192
214
|
filter === f.id
|
|
@@ -226,6 +248,7 @@ export function Forms({ onNavigate }: FormsProps) {
|
|
|
226
248
|
onClick={() => {
|
|
227
249
|
setSearch('')
|
|
228
250
|
setFilter('all')
|
|
251
|
+
setPage(1)
|
|
229
252
|
}}
|
|
230
253
|
className="text-primary text-sm font-medium hover:underline"
|
|
231
254
|
>
|
|
@@ -235,11 +258,21 @@ export function Forms({ onNavigate }: FormsProps) {
|
|
|
235
258
|
/>
|
|
236
259
|
) : (
|
|
237
260
|
<FormsTable
|
|
238
|
-
forms={
|
|
261
|
+
forms={paged}
|
|
239
262
|
onOpenForm={(id) => onNavigate?.(`/forms/${id}/edit`)}
|
|
240
263
|
onOpenEntries={(id) => onNavigate?.(`/forms/${id}/submissions`)}
|
|
241
264
|
onOpenSchema={openSchema}
|
|
242
265
|
onNotifyToggle={handleNotifyToggle}
|
|
266
|
+
footer={
|
|
267
|
+
<FormsPagination
|
|
268
|
+
page={page}
|
|
269
|
+
pageSize={PAGE_SIZE}
|
|
270
|
+
total={filtered.length}
|
|
271
|
+
onPageChange={setPage}
|
|
272
|
+
noun="form"
|
|
273
|
+
label="Forms pagination"
|
|
274
|
+
/>
|
|
275
|
+
}
|
|
243
276
|
/>
|
|
244
277
|
)}
|
|
245
278
|
|
|
@@ -259,117 +292,122 @@ function FormsTable({
|
|
|
259
292
|
onOpenEntries,
|
|
260
293
|
onOpenSchema,
|
|
261
294
|
onNotifyToggle,
|
|
295
|
+
footer,
|
|
262
296
|
}: {
|
|
263
297
|
forms: FormListItem[]
|
|
264
298
|
onOpenForm: (id: string) => void
|
|
265
299
|
onOpenEntries: (id: string) => void
|
|
266
300
|
onOpenSchema: (form: FormListItem) => void
|
|
267
301
|
onNotifyToggle: (form: FormListItem, next: boolean) => void
|
|
302
|
+
footer?: ReactNode
|
|
268
303
|
}) {
|
|
269
304
|
return (
|
|
270
|
-
<div className="border-border bg-card
|
|
271
|
-
<
|
|
272
|
-
<
|
|
273
|
-
<
|
|
274
|
-
<
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
<
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
<
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
<
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
<
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
<
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
<
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
<
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
305
|
+
<div className="border-border bg-card rounded-lg border">
|
|
306
|
+
<div className="overflow-x-auto">
|
|
307
|
+
<table className="w-full min-w-[760px] text-sm" aria-label="Forms">
|
|
308
|
+
<thead>
|
|
309
|
+
<tr className="border-border text-muted-foreground border-b text-left text-xs">
|
|
310
|
+
<th scope="col" className="px-4 py-2.5 font-medium">
|
|
311
|
+
Form
|
|
312
|
+
</th>
|
|
313
|
+
<th scope="col" className="px-4 py-2.5 font-medium">
|
|
314
|
+
Fields
|
|
315
|
+
</th>
|
|
316
|
+
<th scope="col" className="px-4 py-2.5 font-medium">
|
|
317
|
+
Status
|
|
318
|
+
</th>
|
|
319
|
+
<th scope="col" className="px-4 py-2.5 font-medium">
|
|
320
|
+
Entries
|
|
321
|
+
</th>
|
|
322
|
+
<th scope="col" className="px-4 py-2.5 font-medium">
|
|
323
|
+
Last Entry
|
|
324
|
+
</th>
|
|
325
|
+
<th scope="col" className="px-4 py-2.5 text-center font-medium">
|
|
326
|
+
Notify
|
|
327
|
+
</th>
|
|
328
|
+
<th scope="col" className="px-4 py-2.5 text-right font-medium">
|
|
329
|
+
<span className="sr-only">Schema</span>
|
|
330
|
+
</th>
|
|
331
|
+
</tr>
|
|
332
|
+
</thead>
|
|
333
|
+
<tbody>
|
|
334
|
+
{forms.map((form) => {
|
|
335
|
+
const fieldCount = form.fieldCount ?? form.fields.length
|
|
336
|
+
const unread = form.unreadEntries ?? 0
|
|
337
|
+
const muted = form.status === 'archived'
|
|
338
|
+
return (
|
|
339
|
+
<tr
|
|
340
|
+
key={form.id}
|
|
341
|
+
className={`border-border hover:bg-accent/40 border-b transition-colors last:border-b-0 ${muted ? 'opacity-60' : ''}`}
|
|
342
|
+
>
|
|
343
|
+
<td className="px-4 py-3">
|
|
344
|
+
<button
|
|
345
|
+
type="button"
|
|
346
|
+
onClick={() => onOpenForm(form.id)}
|
|
347
|
+
className="block max-w-xs text-left"
|
|
348
|
+
>
|
|
349
|
+
<span className="text-foreground block truncate font-medium hover:underline">
|
|
350
|
+
{form.name}
|
|
351
|
+
</span>
|
|
352
|
+
<span className="text-muted-foreground block truncate text-xs">
|
|
353
|
+
/{form.slug}
|
|
354
|
+
{form.description ? ` · ${form.description}` : ''}
|
|
355
|
+
</span>
|
|
356
|
+
</button>
|
|
357
|
+
</td>
|
|
358
|
+
<td className="px-4 py-3">
|
|
359
|
+
<div className="flex flex-wrap items-center gap-1">
|
|
360
|
+
{form.fields.slice(0, 4).map((field) => (
|
|
361
|
+
<FieldTypeBadge key={field.id} type={field.type} />
|
|
362
|
+
))}
|
|
363
|
+
<span className="text-muted-foreground text-xs">{fieldCount} fields</span>
|
|
364
|
+
</div>
|
|
365
|
+
</td>
|
|
366
|
+
<td className="px-4 py-3">
|
|
367
|
+
<FormStatusBadge status={form.status} />
|
|
368
|
+
</td>
|
|
369
|
+
<td className="px-4 py-3">
|
|
370
|
+
<button
|
|
371
|
+
type="button"
|
|
372
|
+
onClick={() => onOpenEntries(form.id)}
|
|
373
|
+
className="text-foreground hover:text-primary focus-visible:ring-ring inline-flex items-center gap-1.5 rounded font-medium tabular-nums focus-visible:ring-2 focus-visible:outline-none"
|
|
374
|
+
aria-label={`View ${form.totalEntries ?? 0} entries for ${form.name}`}
|
|
375
|
+
>
|
|
376
|
+
{form.totalEntries ?? 0}
|
|
377
|
+
<UnreadBadge count={unread} />
|
|
378
|
+
<ChevronRight className="text-muted-foreground h-3.5 w-3.5" aria-hidden />
|
|
379
|
+
</button>
|
|
380
|
+
</td>
|
|
381
|
+
<td className="text-muted-foreground px-4 py-3 whitespace-nowrap">
|
|
382
|
+
{formatRelative(form.lastEntryAt)}
|
|
383
|
+
</td>
|
|
384
|
+
<td className="px-4 py-3 text-center">
|
|
385
|
+
<Switch.Root
|
|
386
|
+
checked={form.notifyEnabled ?? false}
|
|
387
|
+
onCheckedChange={(v) => onNotifyToggle(form, v)}
|
|
388
|
+
aria-label={`Notifications for ${form.name}`}
|
|
389
|
+
className="bg-destructive data-[state=checked]:bg-success focus-visible:ring-ring relative inline-flex h-[18px] w-8 shrink-0 rounded-full transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
|
390
|
+
>
|
|
391
|
+
<Switch.Thumb className="bg-background block h-3.5 w-3.5 translate-x-0.5 rounded-full shadow-sm transition-transform data-[state=checked]:translate-x-[14px]" />
|
|
392
|
+
</Switch.Root>
|
|
393
|
+
</td>
|
|
394
|
+
<td className="px-4 py-3 text-right">
|
|
395
|
+
<button
|
|
396
|
+
type="button"
|
|
397
|
+
onClick={() => onOpenSchema(form)}
|
|
398
|
+
className="border-border text-foreground hover:bg-accent focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
|
399
|
+
>
|
|
400
|
+
<Settings2 className="h-3.5 w-3.5" aria-hidden />
|
|
401
|
+
Schema
|
|
402
|
+
</button>
|
|
403
|
+
</td>
|
|
404
|
+
</tr>
|
|
405
|
+
)
|
|
406
|
+
})}
|
|
407
|
+
</tbody>
|
|
408
|
+
</table>
|
|
409
|
+
</div>
|
|
410
|
+
{footer}
|
|
373
411
|
</div>
|
|
374
412
|
)
|
|
375
413
|
}
|