@actuate-media/cms-admin 0.69.0 → 0.69.1
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 +6 -0
- package/dist/__tests__/views/form-entries.render.test.js +14 -4
- package/dist/__tests__/views/form-entries.render.test.js.map +1 -1
- package/dist/__tests__/views/form-submissions.render.test.js +14 -4
- package/dist/__tests__/views/form-submissions.render.test.js.map +1 -1
- package/dist/__tests__/views/media-seo-audit.render.test.js +26 -0
- package/dist/__tests__/views/media-seo-audit.render.test.js.map +1 -1
- package/dist/__tests__/views/redirects-pane.render.test.d.ts +2 -0
- package/dist/__tests__/views/redirects-pane.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/redirects-pane.render.test.js +121 -0
- package/dist/__tests__/views/redirects-pane.render.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/components/SEOPanel.js +3 -3
- package/dist/components/SEOPanel.js.map +1 -1
- package/dist/components/collections/EntryDetailPane.js +1 -1
- package/dist/components/collections/EntryDetailPane.js.map +1 -1
- package/dist/components/forms/EntriesTable.d.ts +3 -1
- package/dist/components/forms/EntriesTable.d.ts.map +1 -1
- package/dist/components/forms/EntriesTable.js +5 -2
- package/dist/components/forms/EntriesTable.js.map +1 -1
- package/dist/components/forms/FormEntryDetailPane.d.ts +10 -0
- package/dist/components/forms/FormEntryDetailPane.d.ts.map +1 -0
- package/dist/components/forms/FormEntryDetailPane.js +137 -0
- package/dist/components/forms/FormEntryDetailPane.js.map +1 -0
- package/dist/components/forms/FormSchemaPane.js +1 -1
- package/dist/components/forms/FormSchemaPane.js.map +1 -1
- package/dist/components/script-tags/ScriptTagEditorPane.js +1 -1
- package/dist/components/script-tags/ScriptTagEditorPane.js.map +1 -1
- package/dist/components/seo/RedirectEditorPane.d.ts +12 -0
- package/dist/components/seo/RedirectEditorPane.d.ts.map +1 -0
- package/dist/components/seo/RedirectEditorPane.js +89 -0
- package/dist/components/seo/RedirectEditorPane.js.map +1 -0
- package/dist/components/seo/SeoEditorPane.js +1 -1
- package/dist/components/seo/SeoEditorPane.js.map +1 -1
- package/dist/components/ui/DataTable.js +2 -2
- package/dist/components/ui/DataTable.js.map +1 -1
- package/dist/components/ui/Skeleton.js +3 -3
- package/dist/components/ui/Skeleton.js.map +1 -1
- package/dist/components/ui/Toggle.js +1 -1
- package/dist/components/ui/Toggle.js.map +1 -1
- package/dist/fields/BlockBuilderField.js +1 -1
- package/dist/fields/BlockBuilderField.js.map +1 -1
- package/dist/fields/MediaField.js +1 -1
- package/dist/fields/MediaField.js.map +1 -1
- package/dist/fields/PropInput.js +1 -1
- package/dist/fields/PropInput.js.map +1 -1
- package/dist/layout/Header.js +1 -1
- package/dist/layout/Header.js.map +1 -1
- package/dist/views/DocumentEdit.js +1 -1
- package/dist/views/DocumentEdit.js.map +1 -1
- package/dist/views/FormEntries.d.ts.map +1 -1
- package/dist/views/FormEntries.js +18 -14
- package/dist/views/FormEntries.js.map +1 -1
- package/dist/views/FormSubmissions.d.ts.map +1 -1
- package/dist/views/FormSubmissions.js +18 -15
- package/dist/views/FormSubmissions.js.map +1 -1
- package/dist/views/Forms.js +5 -5
- package/dist/views/Forms.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +1 -1
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/MediaSeoAudit.d.ts +4 -1
- package/dist/views/MediaSeoAudit.d.ts.map +1 -1
- package/dist/views/MediaSeoAudit.js +12 -2
- package/dist/views/MediaSeoAudit.js.map +1 -1
- package/dist/views/Pages/PagesListView.js +14 -14
- package/dist/views/Pages/PagesListView.js.map +1 -1
- package/dist/views/Pages/PagesTreeTable.d.ts.map +1 -1
- package/dist/views/Pages/PagesTreeTable.js +3 -3
- package/dist/views/Pages/PagesTreeTable.js.map +1 -1
- package/dist/views/ScriptTags.d.ts.map +1 -1
- package/dist/views/ScriptTags.js +3 -3
- package/dist/views/ScriptTags.js.map +1 -1
- package/dist/views/seo/ContentTab.js +6 -6
- package/dist/views/seo/ContentTab.js.map +1 -1
- package/dist/views/seo/RedirectsTab.d.ts.map +1 -1
- package/dist/views/seo/RedirectsTab.js +23 -8
- package/dist/views/seo/RedirectsTab.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/views/form-entries.render.test.tsx +16 -4
- package/src/__tests__/views/form-submissions.render.test.tsx +16 -4
- package/src/__tests__/views/media-seo-audit.render.test.tsx +34 -0
- package/src/__tests__/views/redirects-pane.render.test.tsx +156 -0
- package/src/components/SEOPanel.tsx +9 -9
- package/src/components/collections/EntryDetailPane.tsx +1 -1
- package/src/components/forms/EntriesTable.tsx +12 -3
- package/src/components/forms/{EntryDetailDrawer.tsx → FormEntryDetailPane.tsx} +112 -102
- package/src/components/forms/FormSchemaPane.tsx +1 -1
- package/src/components/script-tags/ScriptTagEditorPane.tsx +1 -1
- package/src/components/seo/RedirectEditorPane.tsx +271 -0
- package/src/components/seo/SeoEditorPane.tsx +1 -1
- package/src/components/ui/DataTable.tsx +3 -3
- package/src/components/ui/Skeleton.tsx +8 -8
- package/src/components/ui/Toggle.tsx +1 -1
- package/src/fields/BlockBuilderField.tsx +1 -1
- package/src/fields/MediaField.tsx +1 -1
- package/src/fields/PropInput.tsx +1 -1
- package/src/layout/Header.tsx +2 -2
- package/src/styles/actuate-tokens.css +4 -0
- package/src/styles/theme.css +9 -3
- package/src/views/DocumentEdit.tsx +4 -4
- package/src/views/FormEntries.tsx +81 -66
- package/src/views/FormSubmissions.tsx +78 -64
- package/src/views/Forms.tsx +16 -16
- package/src/views/MediaBrowser.tsx +6 -1
- package/src/views/MediaSeoAudit.tsx +39 -2
- package/src/views/Pages/PagesListView.tsx +46 -46
- package/src/views/Pages/PagesTreeTable.tsx +11 -8
- package/src/views/ScriptTags.tsx +26 -36
- package/src/views/seo/ContentTab.tsx +10 -10
- package/src/views/seo/RedirectsTab.tsx +145 -109
- package/dist/components/forms/EntryDetailDrawer.d.ts +0 -11
- package/dist/components/forms/EntryDetailDrawer.d.ts.map +0 -1
- package/dist/components/forms/EntryDetailDrawer.js +0 -145
- package/dist/components/forms/EntryDetailDrawer.js.map +0 -1
- package/dist/components/seo/RedirectEditorDrawer.d.ts +0 -9
- package/dist/components/seo/RedirectEditorDrawer.d.ts.map +0 -1
- package/dist/components/seo/RedirectEditorDrawer.js +0 -95
- package/dist/components/seo/RedirectEditorDrawer.js.map +0 -1
- package/src/components/seo/RedirectEditorDrawer.tsx +0 -231
|
@@ -6,9 +6,9 @@
|
|
|
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
|
|
10
|
-
* row click (marking the entry read). Loading / empty /
|
|
11
|
-
* states are all handled.
|
|
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.
|
|
12
12
|
*/
|
|
13
13
|
import { useCallback, useEffect, useState } from 'react'
|
|
14
14
|
import { ArrowLeft, Download, Inbox, Search } from 'lucide-react'
|
|
@@ -25,9 +25,10 @@ import {
|
|
|
25
25
|
type FormSubmission,
|
|
26
26
|
} from '../lib/forms-service.js'
|
|
27
27
|
import { emitFormsChanged, onFormsChanged } from '../lib/forms-events.js'
|
|
28
|
-
import {
|
|
28
|
+
import { FormEntryDetailPane } from '../components/forms/FormEntryDetailPane.js'
|
|
29
29
|
import { EntriesTable } from '../components/forms/EntriesTable.js'
|
|
30
30
|
import { ConfirmDialog } from '../components/ui/ConfirmDialog.js'
|
|
31
|
+
import { InspectorPane, SplitPaneLayout, SplitPaneList } from '../components/ui/InspectorPane.js'
|
|
31
32
|
import {
|
|
32
33
|
FormsEmptyState,
|
|
33
34
|
FormsErrorState,
|
|
@@ -109,7 +110,6 @@ export function FormSubmissions({ formId, onNavigate }: FormSubmissionsProps) {
|
|
|
109
110
|
const [page, setPage] = useState(1)
|
|
110
111
|
const [selected, setSelected] = useState<Set<string>>(new Set())
|
|
111
112
|
const [activeEntry, setActiveEntry] = useState<string | null>(null)
|
|
112
|
-
const [drawerOpen, setDrawerOpen] = useState(false)
|
|
113
113
|
const [confirmBulkDelete, setConfirmBulkDelete] = useState(false)
|
|
114
114
|
|
|
115
115
|
// Form metadata for the title (loads once).
|
|
@@ -177,9 +177,9 @@ export function FormSubmissions({ formId, onNavigate }: FormSubmissionsProps) {
|
|
|
177
177
|
}, [load, loadCounts])
|
|
178
178
|
|
|
179
179
|
const openEntry = useCallback((id: string) => {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
// Reflect read state immediately in the row; the
|
|
180
|
+
// Toggle: clicking the open row again collapses the pane.
|
|
181
|
+
setActiveEntry((cur) => (cur === id ? null : id))
|
|
182
|
+
// Reflect read state immediately in the row; the pane persists it.
|
|
183
183
|
setEntries((cur) => cur.map((e) => (e.id === id ? { ...e, unread: false } : e)))
|
|
184
184
|
}, [])
|
|
185
185
|
|
|
@@ -377,64 +377,78 @@ export function FormSubmissions({ formId, onNavigate }: FormSubmissionsProps) {
|
|
|
377
377
|
</div>
|
|
378
378
|
)}
|
|
379
379
|
|
|
380
|
-
{
|
|
381
|
-
|
|
382
|
-
)
|
|
383
|
-
<
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
selected={selected}
|
|
413
|
-
allSelected={allSelected}
|
|
414
|
-
onToggleSelectAll={toggleSelectAll}
|
|
415
|
-
onToggleSelect={toggleSelect}
|
|
416
|
-
onOpen={openEntry}
|
|
417
|
-
onToggleStar={toggleStar}
|
|
418
|
-
footer={
|
|
419
|
-
<FormsPagination
|
|
420
|
-
page={page}
|
|
421
|
-
pageSize={PAGE_SIZE}
|
|
422
|
-
total={total}
|
|
423
|
-
onPageChange={setPage}
|
|
424
|
-
noun="submission"
|
|
425
|
-
label="Submissions pagination"
|
|
380
|
+
{/* The page scrolls, so the split area takes a viewport-derived height
|
|
381
|
+
to keep the pane's body/footer independently scrollable. */}
|
|
382
|
+
<SplitPaneLayout className="h-[calc(100vh-340px)] min-h-[480px]">
|
|
383
|
+
<SplitPaneList>
|
|
384
|
+
{error ? (
|
|
385
|
+
<FormsErrorState message={error} onRetry={load} />
|
|
386
|
+
) : loading ? (
|
|
387
|
+
<FormsLoading label="Loading submissions…" />
|
|
388
|
+
) : showEmpty ? (
|
|
389
|
+
<FormsEmptyState
|
|
390
|
+
icon={<Inbox className="h-6 w-6" aria-hidden />}
|
|
391
|
+
title={isFiltered ? 'No submissions match your filters' : 'No submissions yet'}
|
|
392
|
+
description={
|
|
393
|
+
isFiltered
|
|
394
|
+
? 'Try a different search term or status filter.'
|
|
395
|
+
: 'Submissions will appear here once your form starts receiving entries.'
|
|
396
|
+
}
|
|
397
|
+
action={
|
|
398
|
+
isFiltered ? (
|
|
399
|
+
<button
|
|
400
|
+
type="button"
|
|
401
|
+
onClick={() => {
|
|
402
|
+
setSearch('')
|
|
403
|
+
setFilter('all')
|
|
404
|
+
setPage(1)
|
|
405
|
+
}}
|
|
406
|
+
className="text-primary text-sm font-medium hover:underline"
|
|
407
|
+
>
|
|
408
|
+
Clear filters
|
|
409
|
+
</button>
|
|
410
|
+
) : undefined
|
|
411
|
+
}
|
|
426
412
|
/>
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
413
|
+
) : (
|
|
414
|
+
<EntriesTable
|
|
415
|
+
entries={entries}
|
|
416
|
+
selected={selected}
|
|
417
|
+
allSelected={allSelected}
|
|
418
|
+
onToggleSelectAll={toggleSelectAll}
|
|
419
|
+
onToggleSelect={toggleSelect}
|
|
420
|
+
onOpen={openEntry}
|
|
421
|
+
onToggleStar={toggleStar}
|
|
422
|
+
activeId={activeEntry}
|
|
423
|
+
footer={
|
|
424
|
+
<FormsPagination
|
|
425
|
+
page={page}
|
|
426
|
+
pageSize={PAGE_SIZE}
|
|
427
|
+
total={total}
|
|
428
|
+
onPageChange={setPage}
|
|
429
|
+
noun="submission"
|
|
430
|
+
label="Submissions pagination"
|
|
431
|
+
/>
|
|
432
|
+
}
|
|
433
|
+
/>
|
|
434
|
+
)}
|
|
435
|
+
</SplitPaneList>
|
|
430
436
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
437
|
+
<InspectorPane open={activeEntry !== null} width={400} aria-label="Entry details">
|
|
438
|
+
{activeEntry && (
|
|
439
|
+
<FormEntryDetailPane
|
|
440
|
+
key={activeEntry}
|
|
441
|
+
entryId={activeEntry}
|
|
442
|
+
onClose={() => setActiveEntry(null)}
|
|
443
|
+
onChanged={refresh}
|
|
444
|
+
onDeleted={() => {
|
|
445
|
+
setActiveEntry(null)
|
|
446
|
+
refresh()
|
|
447
|
+
}}
|
|
448
|
+
/>
|
|
449
|
+
)}
|
|
450
|
+
</InspectorPane>
|
|
451
|
+
</SplitPaneLayout>
|
|
438
452
|
|
|
439
453
|
<ConfirmDialog
|
|
440
454
|
open={confirmBulkDelete}
|
package/src/views/Forms.tsx
CHANGED
|
@@ -166,8 +166,8 @@ export function Forms({ onNavigate }: FormsProps) {
|
|
|
166
166
|
<div className="space-y-5 p-6 pb-4">
|
|
167
167
|
<header className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
168
168
|
<div>
|
|
169
|
-
<h1 className="text-
|
|
170
|
-
<p className="
|
|
169
|
+
<h1 className="text-[var(--txt)]">Forms</h1>
|
|
170
|
+
<p className="mt-1 text-sm text-[var(--sub)]">
|
|
171
171
|
Manage forms and view incoming submissions
|
|
172
172
|
</p>
|
|
173
173
|
</div>
|
|
@@ -196,7 +196,7 @@ export function Forms({ onNavigate }: FormsProps) {
|
|
|
196
196
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
197
197
|
<div className="relative w-full sm:max-w-xs">
|
|
198
198
|
<Search
|
|
199
|
-
className="
|
|
199
|
+
className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-[var(--sub)]"
|
|
200
200
|
aria-hidden
|
|
201
201
|
/>
|
|
202
202
|
<input
|
|
@@ -208,7 +208,7 @@ export function Forms({ onNavigate }: FormsProps) {
|
|
|
208
208
|
setPage(1)
|
|
209
209
|
}}
|
|
210
210
|
aria-label="Search forms"
|
|
211
|
-
className="
|
|
211
|
+
className="bg-input-background w-full rounded-lg border border-[var(--bdr)] py-2 pr-3 pl-9 text-sm text-[var(--txt)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
|
|
212
212
|
/>
|
|
213
213
|
</div>
|
|
214
214
|
<div className="flex gap-1" role="group" aria-label="Filter by status">
|
|
@@ -224,7 +224,7 @@ export function Forms({ onNavigate }: FormsProps) {
|
|
|
224
224
|
className={`rounded-full px-3 py-1.5 text-sm font-medium transition-colors ${
|
|
225
225
|
filter === f.id
|
|
226
226
|
? 'bg-primary text-primary-foreground'
|
|
227
|
-
: 'text-
|
|
227
|
+
: 'text-[var(--sub)] hover:bg-[var(--acc-l)] hover:text-[var(--txt)]'
|
|
228
228
|
}`}
|
|
229
229
|
>
|
|
230
230
|
{f.label}
|
|
@@ -326,11 +326,11 @@ function FormsTable({
|
|
|
326
326
|
footer?: ReactNode
|
|
327
327
|
}) {
|
|
328
328
|
return (
|
|
329
|
-
<div className="
|
|
329
|
+
<div className="rounded-lg border border-[var(--bdr)] bg-[var(--card)]">
|
|
330
330
|
<div className="overflow-x-auto">
|
|
331
331
|
<table className="w-full min-w-[760px] text-sm" aria-label="Forms">
|
|
332
332
|
<thead>
|
|
333
|
-
<tr className="border-
|
|
333
|
+
<tr className="border-b border-[var(--bdr)] text-left text-xs text-[var(--sub)]">
|
|
334
334
|
<th scope="col" className="px-4 py-2.5 font-medium">
|
|
335
335
|
Form
|
|
336
336
|
</th>
|
|
@@ -363,8 +363,8 @@ function FormsTable({
|
|
|
363
363
|
return (
|
|
364
364
|
<tr
|
|
365
365
|
key={form.id}
|
|
366
|
-
className={`border-
|
|
367
|
-
isSelected ? 'bg-[var(--acc-l)]' : 'hover:bg-
|
|
366
|
+
className={`border-b border-[var(--bdr)] transition-colors last:border-b-0 ${
|
|
367
|
+
isSelected ? 'bg-[var(--acc-l)]' : 'hover:bg-[var(--acc-l)]'
|
|
368
368
|
} ${muted ? 'opacity-60' : ''}`}
|
|
369
369
|
>
|
|
370
370
|
<td className="px-4 py-3">
|
|
@@ -373,10 +373,10 @@ function FormsTable({
|
|
|
373
373
|
onClick={() => onOpenForm(form.id)}
|
|
374
374
|
className="block max-w-xs text-left"
|
|
375
375
|
>
|
|
376
|
-
<span className="
|
|
376
|
+
<span className="block truncate font-medium text-[var(--txt)] hover:underline">
|
|
377
377
|
{form.name}
|
|
378
378
|
</span>
|
|
379
|
-
<span className="
|
|
379
|
+
<span className="block truncate text-xs text-[var(--sub)]">
|
|
380
380
|
/{form.slug}
|
|
381
381
|
{form.description ? ` · ${form.description}` : ''}
|
|
382
382
|
</span>
|
|
@@ -387,7 +387,7 @@ function FormsTable({
|
|
|
387
387
|
{form.fields.slice(0, 4).map((field) => (
|
|
388
388
|
<FieldTypeBadge key={field.id} type={field.type} />
|
|
389
389
|
))}
|
|
390
|
-
<span className="text-
|
|
390
|
+
<span className="text-xs text-[var(--sub)]">{fieldCount} fields</span>
|
|
391
391
|
</div>
|
|
392
392
|
</td>
|
|
393
393
|
<td className="px-4 py-3">
|
|
@@ -397,15 +397,15 @@ function FormsTable({
|
|
|
397
397
|
<button
|
|
398
398
|
type="button"
|
|
399
399
|
onClick={() => onOpenEntries(form.id)}
|
|
400
|
-
className="
|
|
400
|
+
className="inline-flex items-center gap-1.5 rounded font-medium text-[var(--txt)] tabular-nums hover:text-[var(--acc)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
|
|
401
401
|
aria-label={`View ${form.totalEntries ?? 0} entries for ${form.name}`}
|
|
402
402
|
>
|
|
403
403
|
{form.totalEntries ?? 0}
|
|
404
404
|
<UnreadBadge count={unread} />
|
|
405
|
-
<ChevronRight className="
|
|
405
|
+
<ChevronRight className="h-3.5 w-3.5 text-[var(--sub)]" aria-hidden />
|
|
406
406
|
</button>
|
|
407
407
|
</td>
|
|
408
|
-
<td className="
|
|
408
|
+
<td className="px-4 py-3 whitespace-nowrap text-[var(--sub)]">
|
|
409
409
|
{formatRelative(form.lastEntryAt)}
|
|
410
410
|
</td>
|
|
411
411
|
<td className="px-4 py-3 text-center">
|
|
@@ -421,7 +421,7 @@ function FormsTable({
|
|
|
421
421
|
type="button"
|
|
422
422
|
onClick={() => onOpenSchema(form)}
|
|
423
423
|
aria-expanded={isSelected}
|
|
424
|
-
className="
|
|
424
|
+
className="inline-flex items-center gap-1.5 rounded-md border border-[var(--bdr)] px-2.5 py-1.5 text-xs font-medium text-[var(--txt)] transition-colors hover:bg-[var(--acc-l)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
|
|
425
425
|
>
|
|
426
426
|
<Settings2 className="h-3.5 w-3.5" aria-hidden />
|
|
427
427
|
Schema
|
|
@@ -569,7 +569,12 @@ export function MediaBrowser({ onNavigate, currentPath }: MediaBrowserProps) {
|
|
|
569
569
|
</div>
|
|
570
570
|
|
|
571
571
|
{activeTab === 'audit' ? (
|
|
572
|
-
<MediaSeoAudit
|
|
572
|
+
<MediaSeoAudit
|
|
573
|
+
items={mediaItems}
|
|
574
|
+
onEdit={handleAuditEdit}
|
|
575
|
+
error={error}
|
|
576
|
+
onRetry={refetch}
|
|
577
|
+
/>
|
|
573
578
|
) : (
|
|
574
579
|
<div className="flex min-h-0 flex-1 gap-4 overflow-hidden">
|
|
575
580
|
{sidebarOpen && (
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { Check, FileImage, ImageIcon, SquarePen, TriangleAlert, X } from 'lucide-react'
|
|
4
4
|
import { useMemo, useState } from 'react'
|
|
5
|
+
import type { KeyboardEvent } from 'react'
|
|
5
6
|
import { FilterPill } from '../components/ui/FilterPill.js'
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -27,6 +28,9 @@ export interface MediaSeoAuditProps {
|
|
|
27
28
|
items: MediaSeoAuditItem[]
|
|
28
29
|
/** Fired when a row / Edit button is activated — opens the item in the Library tab. */
|
|
29
30
|
onEdit: (item: MediaSeoAuditItem) => void
|
|
31
|
+
/** Media fetch error — renders a retryable error state instead of an empty audit. */
|
|
32
|
+
error?: string | null
|
|
33
|
+
onRetry?: () => void
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
type AuditFilter = 'all' | 'alt' | 'title' | 'poor'
|
|
@@ -144,7 +148,7 @@ function StatCard({
|
|
|
144
148
|
)
|
|
145
149
|
}
|
|
146
150
|
|
|
147
|
-
export function MediaSeoAudit({ items, onEdit }: MediaSeoAuditProps) {
|
|
151
|
+
export function MediaSeoAudit({ items, onEdit, error, onRetry }: MediaSeoAuditProps) {
|
|
148
152
|
const [filter, setFilter] = useState<AuditFilter>('all')
|
|
149
153
|
|
|
150
154
|
const stats = useMemo(() => {
|
|
@@ -174,6 +178,30 @@ export function MediaSeoAudit({ items, onEdit }: MediaSeoAuditProps) {
|
|
|
174
178
|
}
|
|
175
179
|
}, [items, filter])
|
|
176
180
|
|
|
181
|
+
if (error && items.length === 0) {
|
|
182
|
+
return (
|
|
183
|
+
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
184
|
+
<div
|
|
185
|
+
role="alert"
|
|
186
|
+
className="flex flex-col items-center justify-center rounded-[10px] border border-[var(--bdr)] bg-[var(--card)] px-4 py-14 text-center"
|
|
187
|
+
>
|
|
188
|
+
<TriangleAlert size={24} className="mb-3 text-[var(--err)]" aria-hidden />
|
|
189
|
+
<p className="text-sm font-medium text-[var(--txt)]">Couldn't load media</p>
|
|
190
|
+
<p className="mt-1 max-w-sm text-xs text-[var(--sub)]">{error}</p>
|
|
191
|
+
{onRetry && (
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
onClick={onRetry}
|
|
195
|
+
className="mt-4 rounded-[7px] border border-[var(--bdr)] bg-[var(--card)] px-3.5 py-1.5 text-[12.5px] font-medium text-[var(--txt)] transition-colors hover:border-[var(--acc)] hover:text-[var(--acc)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
|
|
196
|
+
>
|
|
197
|
+
Retry
|
|
198
|
+
</button>
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
177
205
|
return (
|
|
178
206
|
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
179
207
|
<div className="mb-[18px] grid grid-cols-2 gap-2.5 lg:grid-cols-4">
|
|
@@ -253,8 +281,17 @@ export function MediaSeoAudit({ items, onEdit }: MediaSeoAuditProps) {
|
|
|
253
281
|
return (
|
|
254
282
|
<tr
|
|
255
283
|
key={item.id}
|
|
284
|
+
tabIndex={0}
|
|
256
285
|
onClick={() => onEdit(item)}
|
|
257
|
-
|
|
286
|
+
onKeyDown={(e: KeyboardEvent<HTMLTableRowElement>) => {
|
|
287
|
+
if (e.target !== e.currentTarget) return
|
|
288
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
289
|
+
e.preventDefault()
|
|
290
|
+
onEdit(item)
|
|
291
|
+
}
|
|
292
|
+
}}
|
|
293
|
+
aria-label={`Edit SEO metadata for ${item.name}`}
|
|
294
|
+
className="cursor-pointer border-b border-[var(--bdr)] transition-colors last:border-b-0 hover:bg-[var(--acc-l)] focus-visible:bg-[var(--acc-l)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none focus-visible:ring-inset"
|
|
258
295
|
>
|
|
259
296
|
<td className="px-3 py-[9px] align-middle">
|
|
260
297
|
<div className="flex items-center gap-2.5">
|