@actuate-media/cms-admin 0.68.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 +18 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +31 -11
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/hooks/useIdleLogout.test.js +57 -2
- package/dist/__tests__/hooks/useIdleLogout.test.js.map +1 -1
- package/dist/__tests__/router/query-string.test.d.ts +2 -0
- package/dist/__tests__/router/query-string.test.d.ts.map +1 -0
- package/dist/__tests__/router/query-string.test.js +44 -0
- package/dist/__tests__/router/query-string.test.js.map +1 -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/__tests__/views/users-selected-deeplink.render.test.d.ts +2 -0
- package/dist/__tests__/views/users-selected-deeplink.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/users-selected-deeplink.render.test.js +91 -0
- package/dist/__tests__/views/users-selected-deeplink.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/hooks/useIdleLogout.d.ts +21 -6
- package/dist/hooks/useIdleLogout.d.ts.map +1 -1
- package/dist/hooks/useIdleLogout.js +41 -8
- package/dist/hooks/useIdleLogout.js.map +1 -1
- package/dist/layout/Header.js +1 -1
- package/dist/layout/Header.js.map +1 -1
- package/dist/router/index.d.ts +7 -0
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +23 -4
- package/dist/router/index.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 +4 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +28 -6
- 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/SEO.d.ts.map +1 -1
- package/dist/views/SEO.js +2 -1
- package/dist/views/SEO.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/Settings.d.ts +4 -1
- package/dist/views/Settings.d.ts.map +1 -1
- package/dist/views/Settings.js +4 -3
- package/dist/views/Settings.js.map +1 -1
- package/dist/views/Users.d.ts +4 -1
- package/dist/views/Users.d.ts.map +1 -1
- package/dist/views/Users.js +22 -2
- package/dist/views/Users.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/dist/views/settings/SecurityTab.d.ts.map +1 -1
- package/dist/views/settings/SecurityTab.js +2 -1
- package/dist/views/settings/SecurityTab.js.map +1 -1
- package/package.json +2 -2
- package/src/AdminRoot.tsx +53 -12
- package/src/__tests__/hooks/useIdleLogout.test.tsx +103 -1
- package/src/__tests__/router/query-string.test.ts +54 -0
- 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/__tests__/views/users-selected-deeplink.render.test.tsx +102 -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/hooks/useIdleLogout.ts +65 -13
- package/src/layout/Header.tsx +2 -2
- package/src/router/index.ts +25 -4
- 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 +34 -6
- 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/SEO.tsx +2 -1
- package/src/views/ScriptTags.tsx +26 -36
- package/src/views/Settings.tsx +7 -2
- package/src/views/Users.tsx +23 -2
- package/src/views/seo/ContentTab.tsx +10 -10
- package/src/views/seo/RedirectsTab.tsx +145 -109
- package/src/views/settings/SecurityTab.tsx +2 -1
- 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
|
@@ -435,7 +435,7 @@ function Section({
|
|
|
435
435
|
<div className="overflow-hidden rounded-lg border border-[var(--border)]">
|
|
436
436
|
<button
|
|
437
437
|
onClick={() => onToggle(id)}
|
|
438
|
-
className="flex w-full items-center justify-between px-4 py-2.5 text-left text-sm font-medium text-[var(--foreground)] transition-colors
|
|
438
|
+
className="hover:bg-muted flex w-full items-center justify-between px-4 py-2.5 text-left text-sm font-medium text-[var(--foreground)] transition-colors"
|
|
439
439
|
>
|
|
440
440
|
<span className="flex items-center gap-2">
|
|
441
441
|
{icon}
|
|
@@ -646,7 +646,7 @@ export function SEOPanel({
|
|
|
646
646
|
expanded={expandedSections.includes('analysis')}
|
|
647
647
|
onToggle={toggleSection}
|
|
648
648
|
badge={
|
|
649
|
-
<span className="ml-1 inline-flex items-center rounded-full
|
|
649
|
+
<span className="bg-muted ml-1 inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium text-[var(--muted-foreground)]">
|
|
650
650
|
{goodCount}/{checks.length}
|
|
651
651
|
</span>
|
|
652
652
|
}
|
|
@@ -673,26 +673,26 @@ export function SEOPanel({
|
|
|
673
673
|
onToggle={toggleSection}
|
|
674
674
|
>
|
|
675
675
|
<div className="grid grid-cols-2 gap-3">
|
|
676
|
-
<div className="rounded-lg
|
|
676
|
+
<div className="bg-muted rounded-lg p-2.5">
|
|
677
677
|
<p className="text-xs text-[var(--muted-foreground)]">Flesch Score</p>
|
|
678
678
|
<p className={`text-lg font-medium ${fleschColor}`}>{readability.fleschScore}</p>
|
|
679
679
|
<p className={`text-[10px] ${fleschColor}`}>{fleschLabel}</p>
|
|
680
680
|
</div>
|
|
681
|
-
<div className="rounded-lg
|
|
681
|
+
<div className="bg-muted rounded-lg p-2.5">
|
|
682
682
|
<p className="text-xs text-[var(--muted-foreground)]">Word Count</p>
|
|
683
683
|
<p className="text-lg font-medium text-[var(--foreground)]">
|
|
684
684
|
{readability.wordCount}
|
|
685
685
|
</p>
|
|
686
686
|
<p className="text-[10px] text-[var(--muted-foreground)]">words</p>
|
|
687
687
|
</div>
|
|
688
|
-
<div className="rounded-lg
|
|
688
|
+
<div className="bg-muted rounded-lg p-2.5">
|
|
689
689
|
<p className="text-xs text-[var(--muted-foreground)]">Avg. Sentence</p>
|
|
690
690
|
<p className="text-lg font-medium text-[var(--foreground)]">
|
|
691
691
|
{readability.avgSentenceLength.toFixed(1)}
|
|
692
692
|
</p>
|
|
693
693
|
<p className="text-[10px] text-[var(--muted-foreground)]">words/sentence</p>
|
|
694
694
|
</div>
|
|
695
|
-
<div className="rounded-lg
|
|
695
|
+
<div className="bg-muted rounded-lg p-2.5">
|
|
696
696
|
<p className="text-xs text-[var(--muted-foreground)]">Reading Time</p>
|
|
697
697
|
<p className="text-lg font-medium text-[var(--foreground)]">
|
|
698
698
|
{readability.readingTime}
|
|
@@ -907,9 +907,9 @@ export function SEOPanel({
|
|
|
907
907
|
<p className="mb-2 text-xs font-medium text-[var(--muted-foreground)]">
|
|
908
908
|
Social Preview
|
|
909
909
|
</p>
|
|
910
|
-
<div className="overflow-hidden rounded-lg border border-[var(--border)]
|
|
910
|
+
<div className="bg-muted overflow-hidden rounded-lg border border-[var(--border)]">
|
|
911
911
|
{seoData.ogImage ? (
|
|
912
|
-
<div className="flex aspect-video items-center justify-center overflow-hidden
|
|
912
|
+
<div className="bg-muted flex aspect-video items-center justify-center overflow-hidden">
|
|
913
913
|
<img
|
|
914
914
|
src={seoData.ogImage}
|
|
915
915
|
alt="OG preview"
|
|
@@ -917,7 +917,7 @@ export function SEOPanel({
|
|
|
917
917
|
/>
|
|
918
918
|
</div>
|
|
919
919
|
) : (
|
|
920
|
-
<div className="flex aspect-video items-center justify-center
|
|
920
|
+
<div className="bg-muted flex aspect-video items-center justify-center text-sm text-[var(--muted-foreground)]">
|
|
921
921
|
No OG image set
|
|
922
922
|
</div>
|
|
923
923
|
)}
|
|
@@ -171,7 +171,7 @@ export function EntryDetailPane({
|
|
|
171
171
|
<button
|
|
172
172
|
type="button"
|
|
173
173
|
onClick={onOpenEditor}
|
|
174
|
-
className="flex w-full items-center justify-center gap-1.5 rounded-[7px]
|
|
174
|
+
className="bg-primary text-primary-foreground flex w-full items-center justify-center gap-1.5 rounded-[7px] px-3 py-2 text-[13px] font-semibold transition-colors duration-[var(--motion-fast)] hover:opacity-90 focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
|
|
175
175
|
>
|
|
176
176
|
<ExternalLink size={14} aria-hidden />
|
|
177
177
|
Open full editor
|
|
@@ -32,6 +32,8 @@ export interface EntriesTableProps {
|
|
|
32
32
|
onToggleSelect: (id: string) => void
|
|
33
33
|
onOpen: (id: string) => void
|
|
34
34
|
onToggleStar: (entry: FormSubmission) => void
|
|
35
|
+
/** Entry currently open in the inline detail pane (drives aria-expanded). */
|
|
36
|
+
activeId?: string | null
|
|
35
37
|
/** Show a Form column (used by the cross-form All Entries inbox). */
|
|
36
38
|
showForm?: boolean
|
|
37
39
|
/** Navigate to a form's submissions when its name is clicked (All Entries). */
|
|
@@ -50,6 +52,7 @@ export function EntriesTable({
|
|
|
50
52
|
onToggleSelect,
|
|
51
53
|
onOpen,
|
|
52
54
|
onToggleStar,
|
|
55
|
+
activeId = null,
|
|
53
56
|
showForm = false,
|
|
54
57
|
onOpenForm,
|
|
55
58
|
ariaLabel = 'Submissions',
|
|
@@ -129,11 +132,17 @@ export function EntriesTable({
|
|
|
129
132
|
aria-label="Unread"
|
|
130
133
|
/>
|
|
131
134
|
)}
|
|
132
|
-
<
|
|
133
|
-
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
onClick={(e) => {
|
|
138
|
+
e.stopPropagation()
|
|
139
|
+
onOpen(entry.id)
|
|
140
|
+
}}
|
|
141
|
+
aria-expanded={entry.id === activeId}
|
|
142
|
+
className="text-foreground focus-visible:ring-ring truncate rounded text-left font-medium hover:underline focus-visible:ring-2 focus-visible:outline-none"
|
|
134
143
|
>
|
|
135
144
|
{name}
|
|
136
|
-
</
|
|
145
|
+
</button>
|
|
137
146
|
</div>
|
|
138
147
|
{entry.senderEmail && (
|
|
139
148
|
<span className="text-muted-foreground block truncate text-xs">
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Form entry detail pane — inline InspectorPane replacement for the old
|
|
5
|
+
* `EntryDetailDrawer` overlay. Shows a single submission rendered against the
|
|
6
|
+
* schema version it was captured with, plus sender, attribution, and
|
|
7
|
+
* per-entry actions (star, archive, mark unread, delete). Opening an unread
|
|
8
|
+
* entry marks it read. Rendered beside the entries table (400px split pane).
|
|
8
9
|
*/
|
|
9
10
|
import { useCallback, useEffect, useState } from 'react'
|
|
10
11
|
import { Archive, ArchiveRestore, Loader2, Mail, Star, Trash2 } from 'lucide-react'
|
|
@@ -19,8 +20,16 @@ import {
|
|
|
19
20
|
} from '../../lib/forms-service.js'
|
|
20
21
|
import { emitFormsChanged } from '../../lib/forms-events.js'
|
|
21
22
|
import { ConfirmDialog } from '../ui/ConfirmDialog.js'
|
|
22
|
-
import {
|
|
23
|
-
|
|
23
|
+
import {
|
|
24
|
+
InspectorPaneBody,
|
|
25
|
+
InspectorPaneFooter,
|
|
26
|
+
InspectorPaneHeader,
|
|
27
|
+
InspectorPaneSection,
|
|
28
|
+
} from '../ui/InspectorPane.js'
|
|
29
|
+
import { FormsErrorState } from './primitives.js'
|
|
30
|
+
|
|
31
|
+
const iconBtn =
|
|
32
|
+
'flex h-7 w-7 shrink-0 items-center justify-center rounded-[6px] text-[var(--muted)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--bg)] hover:text-[var(--txt)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none disabled:opacity-60'
|
|
24
33
|
|
|
25
34
|
function formatDateTime(iso: string | null | undefined): string {
|
|
26
35
|
if (!iso) return '—'
|
|
@@ -59,25 +68,23 @@ function attributionRows(entry: FormSubmission): AttrRow[] {
|
|
|
59
68
|
].filter((r) => r.value)
|
|
60
69
|
}
|
|
61
70
|
|
|
62
|
-
export interface
|
|
63
|
-
entryId: string
|
|
64
|
-
|
|
65
|
-
onOpenChange: (open: boolean) => void
|
|
71
|
+
export interface FormEntryDetailPaneProps {
|
|
72
|
+
entryId: string
|
|
73
|
+
onClose: () => void
|
|
66
74
|
/** Called after a mutation (star/archive/read/delete) so the list refreshes. */
|
|
67
75
|
onChanged?: () => void
|
|
68
76
|
/** Called specifically after a delete so the caller can drop the row/selection. */
|
|
69
77
|
onDeleted?: (id: string) => void
|
|
70
78
|
}
|
|
71
79
|
|
|
72
|
-
export function
|
|
80
|
+
export function FormEntryDetailPane({
|
|
73
81
|
entryId,
|
|
74
|
-
|
|
75
|
-
onOpenChange,
|
|
82
|
+
onClose,
|
|
76
83
|
onChanged,
|
|
77
84
|
onDeleted,
|
|
78
|
-
}:
|
|
85
|
+
}: FormEntryDetailPaneProps) {
|
|
79
86
|
const [detail, setDetail] = useState<EntryDetail | null>(null)
|
|
80
|
-
const [loading, setLoading] = useState(
|
|
87
|
+
const [loading, setLoading] = useState(true)
|
|
81
88
|
const [error, setError] = useState<string | null>(null)
|
|
82
89
|
const [busy, setBusy] = useState(false)
|
|
83
90
|
const [confirmDelete, setConfirmDelete] = useState(false)
|
|
@@ -87,9 +94,8 @@ export function EntryDetailDrawer({
|
|
|
87
94
|
onChanged?.()
|
|
88
95
|
}, [onChanged])
|
|
89
96
|
|
|
90
|
-
// Load the entry (and its schema) when the
|
|
97
|
+
// Load the entry (and its schema) when the pane opens, marking it read.
|
|
91
98
|
useEffect(() => {
|
|
92
|
-
if (!open || !entryId) return
|
|
93
99
|
let cancelled = false
|
|
94
100
|
setLoading(true)
|
|
95
101
|
setError(null)
|
|
@@ -117,11 +123,10 @@ export function EntryDetailDrawer({
|
|
|
117
123
|
return () => {
|
|
118
124
|
cancelled = true
|
|
119
125
|
}
|
|
120
|
-
}, [
|
|
126
|
+
}, [entryId, notifyChanged])
|
|
121
127
|
|
|
122
128
|
const mutate = useCallback(
|
|
123
129
|
async (patch: { unread?: boolean; starred?: boolean; archived?: boolean }) => {
|
|
124
|
-
if (!entryId) return
|
|
125
130
|
setBusy(true)
|
|
126
131
|
const res = await updateEntry(entryId, patch)
|
|
127
132
|
setBusy(false)
|
|
@@ -136,7 +141,6 @@ export function EntryDetailDrawer({
|
|
|
136
141
|
)
|
|
137
142
|
|
|
138
143
|
const handleDelete = useCallback(async () => {
|
|
139
|
-
if (!entryId) return
|
|
140
144
|
setBusy(true)
|
|
141
145
|
const res = await deleteEntry(entryId)
|
|
142
146
|
setBusy(false)
|
|
@@ -147,8 +151,7 @@ export function EntryDetailDrawer({
|
|
|
147
151
|
toast.success('Submission deleted')
|
|
148
152
|
notifyChanged()
|
|
149
153
|
onDeleted?.(entryId)
|
|
150
|
-
|
|
151
|
-
}, [entryId, notifyChanged, onDeleted, onOpenChange])
|
|
154
|
+
}, [entryId, notifyChanged, onDeleted])
|
|
152
155
|
|
|
153
156
|
const entry = detail?.entry
|
|
154
157
|
const senderName = entry?.senderName || 'Anonymous'
|
|
@@ -156,114 +159,122 @@ export function EntryDetailDrawer({
|
|
|
156
159
|
|
|
157
160
|
return (
|
|
158
161
|
<>
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
162
|
+
<InspectorPaneHeader onClose={onClose} closeLabel="Close entry details">
|
|
163
|
+
<div className="flex items-start justify-between gap-2">
|
|
164
|
+
<div className="min-w-0">
|
|
165
|
+
<div className="truncate text-[13.5px] font-semibold text-[var(--txt)]">
|
|
166
|
+
{entry ? `Entry${entry.entryNumber ? ` #${entry.entryNumber}` : ''}` : 'Entry'}
|
|
167
|
+
</div>
|
|
168
|
+
{entry && (
|
|
169
|
+
<div className="mt-0.5 truncate text-[11.5px] text-[var(--muted)]">
|
|
170
|
+
{formatDateTime(entry.submittedAt)}
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
{entry && (
|
|
175
|
+
<div className="flex shrink-0 gap-1">
|
|
167
176
|
<button
|
|
168
177
|
type="button"
|
|
169
|
-
className={
|
|
178
|
+
className={iconBtn}
|
|
170
179
|
aria-pressed={entry.starred}
|
|
171
180
|
aria-label={entry.starred ? 'Unstar entry' : 'Star entry'}
|
|
172
181
|
disabled={busy}
|
|
173
182
|
onClick={() => void mutate({ starred: !entry.starred })}
|
|
174
183
|
>
|
|
175
184
|
<Star
|
|
176
|
-
size={
|
|
185
|
+
size={16}
|
|
177
186
|
aria-hidden
|
|
178
|
-
className={entry.starred ? 'fill-
|
|
187
|
+
className={entry.starred ? 'fill-[var(--warn)] text-[var(--warn)]' : ''}
|
|
179
188
|
/>
|
|
180
189
|
</button>
|
|
181
190
|
<button
|
|
182
191
|
type="button"
|
|
183
|
-
className={
|
|
192
|
+
className={iconBtn}
|
|
184
193
|
aria-pressed={entry.archived}
|
|
185
194
|
aria-label={entry.archived ? 'Unarchive entry' : 'Archive entry'}
|
|
186
195
|
disabled={busy}
|
|
187
196
|
onClick={() => void mutate({ archived: !entry.archived })}
|
|
188
197
|
>
|
|
189
198
|
{entry.archived ? (
|
|
190
|
-
<ArchiveRestore size={
|
|
199
|
+
<ArchiveRestore size={16} aria-hidden />
|
|
191
200
|
) : (
|
|
192
|
-
<Archive size={
|
|
201
|
+
<Archive size={16} aria-hidden />
|
|
193
202
|
)}
|
|
194
203
|
</button>
|
|
195
|
-
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
<button
|
|
202
|
-
type="button"
|
|
203
|
-
className="text-destructive hover:bg-destructive/10 focus-visible:ring-ring inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:opacity-60"
|
|
204
|
-
disabled={busy}
|
|
205
|
-
onClick={() => setConfirmDelete(true)}
|
|
206
|
-
>
|
|
207
|
-
<Trash2 size={16} aria-hidden />
|
|
208
|
-
Delete
|
|
209
|
-
</button>
|
|
210
|
-
<button
|
|
211
|
-
type="button"
|
|
212
|
-
className="border-border text-foreground hover:bg-accent focus-visible:ring-ring inline-flex items-center gap-2 rounded-md border px-3 py-2 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:opacity-60"
|
|
213
|
-
disabled={busy}
|
|
214
|
-
onClick={() => void mutate({ unread: !entry.unread })}
|
|
215
|
-
>
|
|
216
|
-
<Mail size={16} aria-hidden />
|
|
217
|
-
Mark {entry.unread ? 'read' : 'unread'}
|
|
218
|
-
</button>
|
|
219
|
-
</>
|
|
220
|
-
) : undefined
|
|
221
|
-
}
|
|
222
|
-
>
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
</InspectorPaneHeader>
|
|
208
|
+
|
|
209
|
+
<InspectorPaneBody>
|
|
223
210
|
{loading ? (
|
|
224
211
|
<div className="flex h-40 items-center justify-center" role="status" aria-live="polite">
|
|
225
|
-
<Loader2 className="
|
|
212
|
+
<Loader2 className="h-5 w-5 animate-spin text-[var(--muted)]" aria-hidden />
|
|
226
213
|
<span className="sr-only">Loading entry…</span>
|
|
227
214
|
</div>
|
|
228
215
|
) : error ? (
|
|
229
|
-
<
|
|
216
|
+
<div className="px-4 py-3.5">
|
|
217
|
+
<FormsErrorState message={error} />
|
|
218
|
+
</div>
|
|
230
219
|
) : entry ? (
|
|
231
|
-
|
|
232
|
-
<
|
|
233
|
-
<
|
|
220
|
+
<>
|
|
221
|
+
<InspectorPaneSection title="Sender">
|
|
222
|
+
<div className="text-[12.5px] font-medium text-[var(--txt)]">{senderName}</div>
|
|
234
223
|
{entry.senderEmail && (
|
|
235
224
|
<a
|
|
236
225
|
href={`mailto:${entry.senderEmail}`}
|
|
237
|
-
className="text-
|
|
226
|
+
className="block truncate text-[12px] text-[var(--acc)] hover:underline"
|
|
238
227
|
>
|
|
239
228
|
{entry.senderEmail}
|
|
240
229
|
</a>
|
|
241
230
|
)}
|
|
242
231
|
{entry.senderPhone && (
|
|
243
|
-
<
|
|
232
|
+
<div className="text-[12px] text-[var(--sub)]">{entry.senderPhone}</div>
|
|
244
233
|
)}
|
|
245
|
-
</
|
|
234
|
+
</InspectorPaneSection>
|
|
246
235
|
|
|
247
236
|
<FieldValues entry={entry} fields={detail.fields} />
|
|
248
237
|
|
|
249
238
|
{attrRows.length > 0 && (
|
|
250
|
-
<
|
|
251
|
-
<
|
|
252
|
-
Attribution
|
|
253
|
-
</h4>
|
|
254
|
-
<dl className="divide-border divide-y text-sm">
|
|
239
|
+
<InspectorPaneSection title="Attribution">
|
|
240
|
+
<dl>
|
|
255
241
|
{attrRows.map((row) => (
|
|
256
|
-
<div key={row.label} className="flex justify-between gap-4 py-1
|
|
257
|
-
<dt className="text-muted
|
|
258
|
-
<dd className="
|
|
242
|
+
<div key={row.label} className="flex justify-between gap-4 py-1 text-[12px]">
|
|
243
|
+
<dt className="shrink-0 text-[var(--muted)]">{row.label}</dt>
|
|
244
|
+
<dd className="min-w-0 truncate text-right text-[var(--sub)]">{row.value}</dd>
|
|
259
245
|
</div>
|
|
260
246
|
))}
|
|
261
247
|
</dl>
|
|
262
|
-
</
|
|
248
|
+
</InspectorPaneSection>
|
|
263
249
|
)}
|
|
264
|
-
|
|
250
|
+
</>
|
|
265
251
|
) : null}
|
|
266
|
-
</
|
|
252
|
+
</InspectorPaneBody>
|
|
253
|
+
|
|
254
|
+
{entry && (
|
|
255
|
+
<InspectorPaneFooter>
|
|
256
|
+
<div className="flex items-center gap-2">
|
|
257
|
+
<button
|
|
258
|
+
type="button"
|
|
259
|
+
className="flex min-w-0 flex-1 items-center justify-center gap-1.5 rounded-[7px] border border-[var(--bdr)] px-3 py-2 text-[12.5px] font-medium text-[var(--txt)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--bg)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none disabled:opacity-60"
|
|
260
|
+
disabled={busy}
|
|
261
|
+
onClick={() => void mutate({ unread: !entry.unread })}
|
|
262
|
+
>
|
|
263
|
+
<Mail size={14} aria-hidden />
|
|
264
|
+
Mark {entry.unread ? 'read' : 'unread'}
|
|
265
|
+
</button>
|
|
266
|
+
<button
|
|
267
|
+
type="button"
|
|
268
|
+
className="flex shrink-0 items-center justify-center gap-1.5 rounded-[7px] border border-[var(--bdr)] px-3 py-2 text-[12.5px] font-medium text-[var(--err)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--err-l)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none disabled:opacity-60"
|
|
269
|
+
disabled={busy}
|
|
270
|
+
onClick={() => setConfirmDelete(true)}
|
|
271
|
+
>
|
|
272
|
+
<Trash2 size={14} aria-hidden />
|
|
273
|
+
Delete
|
|
274
|
+
</button>
|
|
275
|
+
</div>
|
|
276
|
+
</InspectorPaneFooter>
|
|
277
|
+
)}
|
|
267
278
|
|
|
268
279
|
<ConfirmDialog
|
|
269
280
|
open={confirmDelete}
|
|
@@ -288,25 +299,24 @@ function FieldValues({ entry, fields }: { entry: FormSubmission; fields: FormFie
|
|
|
288
299
|
? inputFields.map((f) => ({ key: f.key, label: f.label }))
|
|
289
300
|
: Object.keys(entry.data ?? {}).map((k) => ({ key: k, label: k }))
|
|
290
301
|
|
|
291
|
-
if (rows.length === 0) {
|
|
292
|
-
return <p className="text-muted-foreground text-sm">No field data captured.</p>
|
|
293
|
-
}
|
|
294
|
-
|
|
295
302
|
return (
|
|
296
|
-
<
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
303
|
+
<InspectorPaneSection title="Submission">
|
|
304
|
+
{rows.length === 0 ? (
|
|
305
|
+
<p className="text-[12px] text-[var(--muted)]">No field data captured.</p>
|
|
306
|
+
) : (
|
|
307
|
+
<dl>
|
|
308
|
+
{rows.map((row) => (
|
|
309
|
+
<div key={row.key} className="mb-2.5 last:mb-0">
|
|
310
|
+
<dt className="text-[10.5px] font-semibold tracking-[0.5px] text-[var(--muted)] uppercase">
|
|
311
|
+
{row.label}
|
|
312
|
+
</dt>
|
|
313
|
+
<dd className="mt-0.5 text-[12.5px] break-words whitespace-pre-wrap text-[var(--txt)]">
|
|
314
|
+
{displayValue(entry.data?.[row.key])}
|
|
315
|
+
</dd>
|
|
316
|
+
</div>
|
|
317
|
+
))}
|
|
318
|
+
</dl>
|
|
319
|
+
)}
|
|
320
|
+
</InspectorPaneSection>
|
|
311
321
|
)
|
|
312
322
|
}
|
|
@@ -122,7 +122,7 @@ export function FormSchemaPane({
|
|
|
122
122
|
<Tabs.Trigger
|
|
123
123
|
key={t.id}
|
|
124
124
|
value={t.id}
|
|
125
|
-
className="
|
|
125
|
+
className="-mb-px border-b-2 border-transparent px-2.5 py-2 text-[12.5px] font-medium whitespace-nowrap text-[var(--sub)] transition-colors hover:text-[var(--txt)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none data-[state=active]:border-[var(--acc)] data-[state=active]:text-[var(--txt)]"
|
|
126
126
|
>
|
|
127
127
|
{t.label}
|
|
128
128
|
</Tabs.Trigger>
|
|
@@ -434,7 +434,7 @@ export function ScriptTagEditorPane({ tag, onClose, onChanged }: ScriptTagEditor
|
|
|
434
434
|
type="button"
|
|
435
435
|
onClick={() => void handleSave()}
|
|
436
436
|
disabled={saving}
|
|
437
|
-
className="w-full rounded-[7px]
|
|
437
|
+
className="bg-primary text-primary-foreground w-full rounded-[7px] px-3 py-2 text-[13px] font-semibold transition-colors duration-[var(--motion-fast)] hover:opacity-90 focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
438
438
|
>
|
|
439
439
|
{saving ? 'Saving…' : 'Save Script'}
|
|
440
440
|
</button>
|