@actuate-media/cms-admin 0.24.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.
Files changed (30) hide show
  1. package/dist/__tests__/views/page-builder-block-renderer.render.test.d.ts +2 -0
  2. package/dist/__tests__/views/page-builder-block-renderer.render.test.d.ts.map +1 -0
  3. package/dist/__tests__/views/page-builder-block-renderer.render.test.js +48 -0
  4. package/dist/__tests__/views/page-builder-block-renderer.render.test.js.map +1 -0
  5. package/dist/__tests__/views/page-builder-form-picker.render.test.d.ts +2 -0
  6. package/dist/__tests__/views/page-builder-form-picker.render.test.d.ts.map +1 -0
  7. package/dist/__tests__/views/page-builder-form-picker.render.test.js +44 -0
  8. package/dist/__tests__/views/page-builder-form-picker.render.test.js.map +1 -0
  9. package/dist/actuate-admin.css +1 -1
  10. package/dist/views/page-builder/BlockEditor.d.ts.map +1 -1
  11. package/dist/views/page-builder/BlockEditor.js +2 -1
  12. package/dist/views/page-builder/BlockEditor.js.map +1 -1
  13. package/dist/views/page-builder/RelationshipField.d.ts +15 -0
  14. package/dist/views/page-builder/RelationshipField.d.ts.map +1 -0
  15. package/dist/views/page-builder/RelationshipField.js +65 -0
  16. package/dist/views/page-builder/RelationshipField.js.map +1 -0
  17. package/dist/views/page-builder/block-renderers/FormPreview.d.ts +6 -0
  18. package/dist/views/page-builder/block-renderers/FormPreview.d.ts.map +1 -1
  19. package/dist/views/page-builder/block-renderers/FormPreview.js +44 -6
  20. package/dist/views/page-builder/block-renderers/FormPreview.js.map +1 -1
  21. package/dist/views/page-builder/canvas/BlockRenderer.d.ts.map +1 -1
  22. package/dist/views/page-builder/canvas/BlockRenderer.js +3 -9
  23. package/dist/views/page-builder/canvas/BlockRenderer.js.map +1 -1
  24. package/package.json +2 -2
  25. package/src/__tests__/views/page-builder-block-renderer.render.test.tsx +66 -0
  26. package/src/__tests__/views/page-builder-form-picker.render.test.tsx +54 -0
  27. package/src/views/page-builder/BlockEditor.tsx +7 -10
  28. package/src/views/page-builder/RelationshipField.tsx +113 -0
  29. package/src/views/page-builder/block-renderers/FormPreview.tsx +85 -33
  30. package/src/views/page-builder/canvas/BlockRenderer.tsx +10 -12
@@ -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">Newsletter Signup</p>
14
- {formId && <p className="text-muted-foreground mt-1 text-xs">Form: {formId}</p>}
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
- <p className="text-foreground text-sm font-medium">Contact Form</p>
51
- {formId && <p className="text-muted-foreground mt-1 text-xs">Form: {formId}</p>}
52
- <div className="mt-3 grid grid-cols-2 gap-2">
53
- <div className="border-border bg-input-background h-8 rounded-md border" />
54
- <div className="border-border bg-input-background h-8 rounded-md border" />
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 Icon = BLOCK_ICONS[blockType] ?? Box
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
- <div className="bg-muted/30 border-border flex items-center justify-center gap-2 rounded-md border border-dashed p-4">
53
- <Icon size={16} className="text-muted-foreground" />
54
- <span className="text-muted-foreground text-sm">{label}</span>
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
  )