@actuate-media/cms-admin 0.23.0 → 0.25.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__/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/page-builder-block-renderer.render.test.d.ts +2 -0
- package/dist/__tests__/views/page-builder-block-renderer.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/page-builder-block-renderer.render.test.js +48 -0
- package/dist/__tests__/views/page-builder-block-renderer.render.test.js.map +1 -0
- package/dist/__tests__/views/page-builder-form-picker.render.test.d.ts +2 -0
- package/dist/__tests__/views/page-builder-form-picker.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/page-builder-form-picker.render.test.js +44 -0
- package/dist/__tests__/views/page-builder-form-picker.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/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +44 -21
- package/dist/layout/Sidebar.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 +4 -24
- package/dist/views/FormSubmissions.js.map +1 -1
- package/dist/views/page-builder/BlockEditor.d.ts.map +1 -1
- package/dist/views/page-builder/BlockEditor.js +2 -1
- package/dist/views/page-builder/BlockEditor.js.map +1 -1
- package/dist/views/page-builder/RelationshipField.d.ts +15 -0
- package/dist/views/page-builder/RelationshipField.d.ts.map +1 -0
- package/dist/views/page-builder/RelationshipField.js +65 -0
- package/dist/views/page-builder/RelationshipField.js.map +1 -0
- package/dist/views/page-builder/block-renderers/FormPreview.d.ts +6 -0
- package/dist/views/page-builder/block-renderers/FormPreview.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/FormPreview.js +44 -6
- package/dist/views/page-builder/block-renderers/FormPreview.js.map +1 -1
- package/dist/views/page-builder/canvas/BlockRenderer.d.ts.map +1 -1
- package/dist/views/page-builder/canvas/BlockRenderer.js +3 -9
- package/dist/views/page-builder/canvas/BlockRenderer.js.map +1 -1
- package/package.json +2 -2
- package/src/AdminRoot.tsx +4 -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/page-builder-block-renderer.render.test.tsx +66 -0
- package/src/__tests__/views/page-builder-form-picker.render.test.tsx +54 -0
- package/src/components/forms/EntriesTable.tsx +197 -0
- package/src/layout/Sidebar.tsx +75 -17
- package/src/views/FormEntries.tsx +449 -0
- package/src/views/FormSubmissions.tsx +4 -149
- package/src/views/page-builder/BlockEditor.tsx +7 -10
- package/src/views/page-builder/RelationshipField.tsx +113 -0
- package/src/views/page-builder/block-renderers/FormPreview.tsx +85 -33
- package/src/views/page-builder/canvas/BlockRenderer.tsx +10 -12
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
4
|
+
import type { FieldDefinition } from '@actuate-media/cms-core'
|
|
5
|
+
import { cmsApi } from '../../lib/api.js'
|
|
6
|
+
|
|
7
|
+
const INPUT_CLASS =
|
|
8
|
+
'w-full px-3 py-2 text-sm bg-background border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring'
|
|
9
|
+
const LABEL_CLASS = 'text-sm font-medium text-foreground mb-1 block'
|
|
10
|
+
|
|
11
|
+
interface RelationshipOption {
|
|
12
|
+
id: string
|
|
13
|
+
label: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface RelationshipFieldProps {
|
|
17
|
+
definition: FieldDefinition
|
|
18
|
+
value: unknown
|
|
19
|
+
onChange: (value: unknown) => void
|
|
20
|
+
label: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Relationship picker for page-builder block fields (e.g. the form block's
|
|
25
|
+
* `formId`). Loads the target collection via the admin API and renders a
|
|
26
|
+
* labelled dropdown. If the collection can't be loaded it degrades to a plain
|
|
27
|
+
* id input so the field is never a dead end.
|
|
28
|
+
*/
|
|
29
|
+
export function RelationshipField({ definition, value, onChange, label }: RelationshipFieldProps) {
|
|
30
|
+
const relationTo = useMemo(() => {
|
|
31
|
+
if (definition.type !== 'relationship') return ''
|
|
32
|
+
return Array.isArray(definition.relationTo)
|
|
33
|
+
? (definition.relationTo[0] ?? '')
|
|
34
|
+
: definition.relationTo
|
|
35
|
+
}, [definition])
|
|
36
|
+
|
|
37
|
+
const [options, setOptions] = useState<RelationshipOption[]>([])
|
|
38
|
+
const [loading, setLoading] = useState(false)
|
|
39
|
+
const [failed, setFailed] = useState(false)
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!relationTo) return
|
|
43
|
+
let cancelled = false
|
|
44
|
+
setLoading(true)
|
|
45
|
+
setFailed(false)
|
|
46
|
+
cmsApi<Array<Record<string, unknown>>>(`/${relationTo}`)
|
|
47
|
+
.then((res) => {
|
|
48
|
+
if (cancelled) return
|
|
49
|
+
if (Array.isArray(res.data)) {
|
|
50
|
+
setOptions(
|
|
51
|
+
res.data
|
|
52
|
+
.map((item) => ({
|
|
53
|
+
id: String(item.id ?? ''),
|
|
54
|
+
label: String(item.name ?? item.title ?? item.slug ?? item.id ?? 'Untitled'),
|
|
55
|
+
}))
|
|
56
|
+
.filter((option) => option.id),
|
|
57
|
+
)
|
|
58
|
+
} else {
|
|
59
|
+
setFailed(true)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
.catch(() => {
|
|
63
|
+
if (!cancelled) setFailed(true)
|
|
64
|
+
})
|
|
65
|
+
.finally(() => {
|
|
66
|
+
if (!cancelled) setLoading(false)
|
|
67
|
+
})
|
|
68
|
+
return () => {
|
|
69
|
+
cancelled = true
|
|
70
|
+
}
|
|
71
|
+
}, [relationTo])
|
|
72
|
+
|
|
73
|
+
const current = (value as string) ?? ''
|
|
74
|
+
const noun = relationTo === 'forms' ? 'a form' : 'an item'
|
|
75
|
+
|
|
76
|
+
if (failed) {
|
|
77
|
+
return (
|
|
78
|
+
<div>
|
|
79
|
+
<label className={LABEL_CLASS}>{label}</label>
|
|
80
|
+
<input
|
|
81
|
+
type="text"
|
|
82
|
+
value={current}
|
|
83
|
+
onChange={(e) => onChange(e.target.value || undefined)}
|
|
84
|
+
placeholder={`Enter ${relationTo} id…`}
|
|
85
|
+
className={INPUT_CLASS}
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div>
|
|
93
|
+
<label className={LABEL_CLASS}>{label}</label>
|
|
94
|
+
<select
|
|
95
|
+
value={current}
|
|
96
|
+
onChange={(e) => onChange(e.target.value || undefined)}
|
|
97
|
+
disabled={loading}
|
|
98
|
+
aria-label={label}
|
|
99
|
+
className={INPUT_CLASS}
|
|
100
|
+
>
|
|
101
|
+
<option value="">{loading ? 'Loading…' : `Select ${noun}…`}</option>
|
|
102
|
+
{options.map((option) => (
|
|
103
|
+
<option key={option.id} value={option.id}>
|
|
104
|
+
{option.label}
|
|
105
|
+
</option>
|
|
106
|
+
))}
|
|
107
|
+
</select>
|
|
108
|
+
{!loading && options.length === 0 && (
|
|
109
|
+
<p className="text-muted-foreground mt-1 text-xs">No {relationTo} found.</p>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
@@ -1,17 +1,76 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
3
4
|
import { FileText, Mail, Send } from 'lucide-react'
|
|
5
|
+
import { cmsApi } from '../../../lib/api.js'
|
|
4
6
|
import type { BlockPreviewProps } from './index.js'
|
|
5
7
|
|
|
8
|
+
interface PreviewField {
|
|
9
|
+
key: string
|
|
10
|
+
label: string
|
|
11
|
+
type: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface PreviewForm {
|
|
15
|
+
name: string
|
|
16
|
+
fields: PreviewField[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Canvas preview for the form block. When a form is linked we fetch its real
|
|
21
|
+
* name + fields so the editor sees what will render; otherwise we fall back to
|
|
22
|
+
* a generic skeleton. Network failures degrade to the skeleton rather than
|
|
23
|
+
* breaking the canvas.
|
|
24
|
+
*/
|
|
6
25
|
export function FormPreview({ data, variant = 'contact' }: BlockPreviewProps) {
|
|
7
26
|
const formId = (data.formId as string) || ''
|
|
27
|
+
const [form, setForm] = useState<PreviewForm | null>(null)
|
|
28
|
+
const [loading, setLoading] = useState(false)
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!formId) {
|
|
32
|
+
setForm(null)
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
let cancelled = false
|
|
36
|
+
setLoading(true)
|
|
37
|
+
cmsApi<{ name?: string; fields?: PreviewField[] }>(`/forms/${encodeURIComponent(formId)}`)
|
|
38
|
+
.then((res) => {
|
|
39
|
+
if (cancelled) return
|
|
40
|
+
if (res.data?.name) {
|
|
41
|
+
setForm({ name: res.data.name, fields: res.data.fields ?? [] })
|
|
42
|
+
} else {
|
|
43
|
+
setForm(null)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
.finally(() => {
|
|
47
|
+
if (!cancelled) setLoading(false)
|
|
48
|
+
})
|
|
49
|
+
return () => {
|
|
50
|
+
cancelled = true
|
|
51
|
+
}
|
|
52
|
+
}, [formId])
|
|
53
|
+
|
|
54
|
+
if (!formId) {
|
|
55
|
+
return (
|
|
56
|
+
<div className="border-border bg-muted/30 rounded-md border border-dashed p-6 text-center">
|
|
57
|
+
<FileText size={20} className="text-muted-foreground mx-auto" />
|
|
58
|
+
<p className="text-muted-foreground mt-2 text-sm">No form selected</p>
|
|
59
|
+
<p className="text-muted-foreground/70 text-xs">Pick a form in the block settings</p>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const heading = form?.name ?? (loading ? 'Loading form…' : 'Contact Form')
|
|
65
|
+
const fields = form?.fields ?? []
|
|
8
66
|
|
|
9
67
|
if (variant === 'newsletter') {
|
|
10
68
|
return (
|
|
11
69
|
<div className="border-border rounded-md border p-6 text-center">
|
|
12
70
|
<Mail size={24} className="text-muted-foreground mx-auto" />
|
|
13
|
-
<p className="text-foreground mt-3 text-sm font-medium">
|
|
14
|
-
|
|
71
|
+
<p className="text-foreground mt-3 text-sm font-medium">
|
|
72
|
+
{form?.name ?? 'Newsletter Signup'}
|
|
73
|
+
</p>
|
|
15
74
|
<div className="mx-auto mt-4 flex max-w-xs gap-2">
|
|
16
75
|
<div className="border-border bg-input-background h-9 flex-1 rounded-md border" />
|
|
17
76
|
<div className="bg-primary flex h-9 items-center rounded-md px-3">
|
|
@@ -22,42 +81,35 @@ export function FormPreview({ data, variant = 'contact' }: BlockPreviewProps) {
|
|
|
22
81
|
)
|
|
23
82
|
}
|
|
24
83
|
|
|
25
|
-
if (variant === 'custom') {
|
|
26
|
-
return (
|
|
27
|
-
<div className="border-border rounded-md border p-5">
|
|
28
|
-
<div className="flex items-center gap-2">
|
|
29
|
-
<FileText size={16} className="text-muted-foreground" />
|
|
30
|
-
<p className="text-foreground text-sm font-medium">
|
|
31
|
-
{formId ? `Form: ${formId}` : 'Custom Form'}
|
|
32
|
-
</p>
|
|
33
|
-
</div>
|
|
34
|
-
<div className="mt-3 flex flex-col gap-2">
|
|
35
|
-
<div className="border-border bg-input-background h-8 rounded-md border" />
|
|
36
|
-
<div className="border-border bg-input-background h-8 rounded-md border" />
|
|
37
|
-
<div className="border-border bg-input-background h-20 rounded-md border" />
|
|
38
|
-
</div>
|
|
39
|
-
<div className="mt-3">
|
|
40
|
-
<span className="bg-primary text-primary-foreground inline-block rounded-md px-4 py-2 text-sm">
|
|
41
|
-
Submit
|
|
42
|
-
</span>
|
|
43
|
-
</div>
|
|
44
|
-
</div>
|
|
45
|
-
)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
84
|
return (
|
|
49
85
|
<div className="border-border rounded-md border p-5">
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
86
|
+
<div className="flex items-center gap-2">
|
|
87
|
+
<FileText size={16} className="text-muted-foreground" />
|
|
88
|
+
<p className="text-foreground text-sm font-medium">{heading}</p>
|
|
89
|
+
</div>
|
|
90
|
+
<div className="mt-3 flex flex-col gap-2">
|
|
91
|
+
{fields.length > 0 ? (
|
|
92
|
+
fields.slice(0, 5).map((field) => (
|
|
93
|
+
<div key={field.key}>
|
|
94
|
+
<p className="text-muted-foreground mb-1 text-xs">{field.label || field.key}</p>
|
|
95
|
+
<div
|
|
96
|
+
className={`border-border bg-input-background rounded-md border ${
|
|
97
|
+
field.type === 'textarea' ? 'h-16' : 'h-8'
|
|
98
|
+
}`}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
))
|
|
102
|
+
) : (
|
|
103
|
+
<>
|
|
104
|
+
<div className="border-border bg-input-background h-8 rounded-md border" />
|
|
105
|
+
<div className="border-border bg-input-background h-8 rounded-md border" />
|
|
106
|
+
<div className="border-border bg-input-background h-20 rounded-md border" />
|
|
107
|
+
</>
|
|
108
|
+
)}
|
|
55
109
|
</div>
|
|
56
|
-
<div className="border-border bg-input-background mt-2 h-8 rounded-md border" />
|
|
57
|
-
<div className="border-border bg-input-background mt-2 h-20 rounded-md border" />
|
|
58
110
|
<div className="mt-3">
|
|
59
111
|
<span className="bg-primary text-primary-foreground inline-block rounded-md px-4 py-2 text-sm">
|
|
60
|
-
Send Message
|
|
112
|
+
{variant === 'custom' ? 'Submit' : 'Send Message'}
|
|
61
113
|
</span>
|
|
62
114
|
</div>
|
|
63
115
|
</div>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react'
|
|
4
|
-
import { Box, Type, Image, Layout, Star } from 'lucide-react'
|
|
5
4
|
import type { BlockNode } from '@actuate-media/cms-core'
|
|
5
|
+
import { blockRenderers, FallbackPreview } from '../block-renderers/index.js'
|
|
6
6
|
|
|
7
7
|
export interface BlockRendererProps {
|
|
8
8
|
node: BlockNode
|
|
@@ -10,13 +10,6 @@ export interface BlockRendererProps {
|
|
|
10
10
|
onSelectNode: (id: string | null) => void
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const BLOCK_ICONS: Record<string, typeof Box> = {
|
|
14
|
-
hero: Star,
|
|
15
|
-
text: Type,
|
|
16
|
-
image: Image,
|
|
17
|
-
layout: Layout,
|
|
18
|
-
}
|
|
19
|
-
|
|
20
13
|
function getBlockLabel(blockType: string): string {
|
|
21
14
|
return blockType.charAt(0).toUpperCase() + blockType.slice(1)
|
|
22
15
|
}
|
|
@@ -26,7 +19,7 @@ export function BlockRenderer({ node, selectedNodeId, onSelectNode }: BlockRende
|
|
|
26
19
|
const isSelected = selectedNodeId === node.id
|
|
27
20
|
const blockType = node.settings.blockType
|
|
28
21
|
const label = getBlockLabel(blockType)
|
|
29
|
-
const
|
|
22
|
+
const Preview = blockRenderers[blockType]
|
|
30
23
|
|
|
31
24
|
const handleClick = (e: React.MouseEvent) => {
|
|
32
25
|
e.stopPropagation()
|
|
@@ -36,6 +29,7 @@ export function BlockRenderer({ node, selectedNodeId, onSelectNode }: BlockRende
|
|
|
36
29
|
return (
|
|
37
30
|
<div
|
|
38
31
|
data-node-id={node.id}
|
|
32
|
+
data-block-type={blockType}
|
|
39
33
|
className={`relative min-h-[48px] transition-shadow ${
|
|
40
34
|
isSelected ? 'ring-primary ring-2 ring-offset-2' : hovered ? 'ring-primary/50 ring-1' : ''
|
|
41
35
|
}`}
|
|
@@ -49,9 +43,13 @@ export function BlockRenderer({ node, selectedNodeId, onSelectNode }: BlockRende
|
|
|
49
43
|
</span>
|
|
50
44
|
)}
|
|
51
45
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
{/* Previews are display-only on the canvas; the wrapper owns selection. */}
|
|
47
|
+
<div className="pointer-events-none">
|
|
48
|
+
{Preview ? (
|
|
49
|
+
<Preview data={node.data} variant={node.settings.variant} />
|
|
50
|
+
) : (
|
|
51
|
+
<FallbackPreview blockType={blockType} data={node.data} />
|
|
52
|
+
)}
|
|
55
53
|
</div>
|
|
56
54
|
</div>
|
|
57
55
|
)
|