@actuate-media/cms-admin 0.44.0 → 0.46.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 (46) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/__tests__/components/tag-input.render.test.d.ts +2 -0
  3. package/dist/__tests__/components/tag-input.render.test.d.ts.map +1 -0
  4. package/dist/__tests__/components/tag-input.render.test.js +86 -0
  5. package/dist/__tests__/components/tag-input.render.test.js.map +1 -0
  6. package/dist/__tests__/lib/post-editor-service.test.js +26 -0
  7. package/dist/__tests__/lib/post-editor-service.test.js.map +1 -1
  8. package/dist/__tests__/views/post-fields-modal.render.test.d.ts +2 -0
  9. package/dist/__tests__/views/post-fields-modal.render.test.d.ts.map +1 -0
  10. package/dist/__tests__/views/post-fields-modal.render.test.js +86 -0
  11. package/dist/__tests__/views/post-fields-modal.render.test.js.map +1 -0
  12. package/dist/__tests__/views/section-inspector-repeater.render.test.d.ts +2 -0
  13. package/dist/__tests__/views/section-inspector-repeater.render.test.d.ts.map +1 -0
  14. package/dist/__tests__/views/section-inspector-repeater.render.test.js +68 -0
  15. package/dist/__tests__/views/section-inspector-repeater.render.test.js.map +1 -0
  16. package/dist/actuate-admin.css +1 -1
  17. package/dist/components/TagInput.d.ts +16 -0
  18. package/dist/components/TagInput.d.ts.map +1 -0
  19. package/dist/components/TagInput.js +105 -0
  20. package/dist/components/TagInput.js.map +1 -0
  21. package/dist/lib/post-editor-service.d.ts +8 -0
  22. package/dist/lib/post-editor-service.d.ts.map +1 -1
  23. package/dist/lib/post-editor-service.js +25 -0
  24. package/dist/lib/post-editor-service.js.map +1 -1
  25. package/dist/views/page-editor/SectionInspector.d.ts +9 -0
  26. package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
  27. package/dist/views/page-editor/SectionInspector.js +38 -1
  28. package/dist/views/page-editor/SectionInspector.js.map +1 -1
  29. package/dist/views/post-editor/PostFieldsModal.d.ts +17 -2
  30. package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -1
  31. package/dist/views/post-editor/PostFieldsModal.js +13 -4
  32. package/dist/views/post-editor/PostFieldsModal.js.map +1 -1
  33. package/dist/views/post-editor/PostSectionEditor.d.ts +2 -0
  34. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
  35. package/dist/views/post-editor/PostSectionEditor.js +1 -1
  36. package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
  37. package/package.json +2 -2
  38. package/src/__tests__/components/tag-input.render.test.tsx +103 -0
  39. package/src/__tests__/lib/post-editor-service.test.ts +28 -0
  40. package/src/__tests__/views/post-fields-modal.render.test.tsx +126 -0
  41. package/src/__tests__/views/section-inspector-repeater.render.test.tsx +90 -0
  42. package/src/components/TagInput.tsx +196 -0
  43. package/src/lib/post-editor-service.ts +28 -0
  44. package/src/views/page-editor/SectionInspector.tsx +40 -2
  45. package/src/views/post-editor/PostFieldsModal.tsx +79 -10
  46. package/src/views/post-editor/PostSectionEditor.tsx +11 -2
@@ -4,13 +4,27 @@ import { useEffect, useState } from 'react'
4
4
  import { Modal } from '../../components/ui/Modal.js'
5
5
  import { Button } from '../../components/ui/Button.js'
6
6
  import { MediaPickerModal } from '../../components/MediaPickerModal.js'
7
+ import { TagInput } from '../../components/TagInput.js'
7
8
  import { RichTextField } from '../../fields/RichTextField.js'
8
9
  import type { EditorPost } from '../../lib/post-editor-service.js'
9
10
 
11
+ /**
12
+ * The slice of a collection's field config the modal reads to pick controls:
13
+ * a `select` category renders a dropdown (structured vocabulary), an `array`
14
+ * tags field renders the chip input with autocomplete.
15
+ */
16
+ export interface PostFieldConfig {
17
+ type?: string
18
+ options?: Array<{ label: string; value: string }>
19
+ maxRows?: number
20
+ }
21
+
10
22
  interface PostFieldsModalProps {
11
23
  open: boolean
12
24
  post: EditorPost
13
25
  canEdit: boolean
26
+ /** Field definitions from the post type's collection config. */
27
+ fields?: Record<string, PostFieldConfig | undefined>
14
28
  onClose: () => void
15
29
  onSave: (patch: Partial<EditorPost>) => void
16
30
  }
@@ -27,19 +41,35 @@ function slugify(value: string): string {
27
41
  .replace(/^-+|-+$/g, '')
28
42
  }
29
43
 
30
- /** Edits post-level fields: title, slug, excerpt, featured image, category, body, SEO. */
31
- export function PostFieldsModal({ open, post, canEdit, onClose, onSave }: PostFieldsModalProps) {
44
+ /** Edits post-level fields: title, slug, excerpt, featured image, category, tags, body, SEO. */
45
+ export function PostFieldsModal({
46
+ open,
47
+ post,
48
+ canEdit,
49
+ fields,
50
+ onClose,
51
+ onSave,
52
+ }: PostFieldsModalProps) {
32
53
  const [title, setTitle] = useState(post.title)
33
54
  const [slug, setSlug] = useState(post.slug)
34
55
  const [excerpt, setExcerpt] = useState(post.excerpt)
35
56
  const [featuredImage, setFeaturedImage] = useState(post.featuredImage)
36
57
  const [category, setCategory] = useState(post.category)
58
+ const [tags, setTags] = useState<string[]>(post.tags)
37
59
  const [body, setBody] = useState(post.body)
38
60
  const [seoTitle, setSeoTitle] = useState(post.seoTitle)
39
61
  const [seoDescription, setSeoDescription] = useState(post.seoDescription)
40
62
  const [slugTouched, setSlugTouched] = useState(Boolean(post.slug))
41
63
  const [mediaOpen, setMediaOpen] = useState(false)
42
64
 
65
+ const categoryField = fields?.category
66
+ const categoryOptions =
67
+ categoryField?.type === 'select' && Array.isArray(categoryField.options)
68
+ ? categoryField.options
69
+ : null
70
+ const tagsField = fields?.tags
71
+ const hasTags = tagsField?.type === 'array'
72
+
43
73
  useEffect(() => {
44
74
  if (!open) return
45
75
  setTitle(post.title)
@@ -47,6 +77,7 @@ export function PostFieldsModal({ open, post, canEdit, onClose, onSave }: PostFi
47
77
  setExcerpt(post.excerpt)
48
78
  setFeaturedImage(post.featuredImage)
49
79
  setCategory(post.category)
80
+ setTags(post.tags)
50
81
  setBody(post.body)
51
82
  setSeoTitle(post.seoTitle)
52
83
  setSeoDescription(post.seoDescription)
@@ -59,7 +90,7 @@ export function PostFieldsModal({ open, post, canEdit, onClose, onSave }: PostFi
59
90
  }
60
91
 
61
92
  function handleSave() {
62
- onSave({ title, slug, excerpt, featuredImage, category, body, seoTitle, seoDescription })
93
+ onSave({ title, slug, excerpt, featuredImage, category, tags, body, seoTitle, seoDescription })
63
94
  onClose()
64
95
  }
65
96
 
@@ -112,16 +143,54 @@ export function PostFieldsModal({ open, post, canEdit, onClose, onSave }: PostFi
112
143
  <label className={LABEL} htmlFor="pf-category">
113
144
  Category
114
145
  </label>
115
- <input
116
- id="pf-category"
117
- className={INPUT}
118
- value={category}
146
+ {categoryOptions ? (
147
+ <select
148
+ id="pf-category"
149
+ className={INPUT}
150
+ value={category}
151
+ disabled={!canEdit}
152
+ onChange={(e) => setCategory(e.target.value)}
153
+ >
154
+ <option value="">No category</option>
155
+ {categoryOptions.map((o) => (
156
+ <option key={o.value} value={o.value}>
157
+ {o.label}
158
+ </option>
159
+ ))}
160
+ {/* Keep a legacy free-text value selectable so opening the
161
+ modal never silently discards it. */}
162
+ {category && !categoryOptions.some((o) => o.value === category) && (
163
+ <option value={category}>{category}</option>
164
+ )}
165
+ </select>
166
+ ) : (
167
+ <input
168
+ id="pf-category"
169
+ className={INPUT}
170
+ value={category}
171
+ disabled={!canEdit}
172
+ placeholder="e.g. Product"
173
+ onChange={(e) => setCategory(e.target.value)}
174
+ />
175
+ )}
176
+ </div>
177
+ </div>
178
+ {hasTags && (
179
+ <div>
180
+ <label className={LABEL} htmlFor="pf-tags">
181
+ Tags
182
+ </label>
183
+ <TagInput
184
+ id="pf-tags"
185
+ value={tags}
186
+ onChange={setTags}
187
+ collection={post.postType}
188
+ field="tags"
119
189
  disabled={!canEdit}
120
- placeholder="e.g. Product"
121
- onChange={(e) => setCategory(e.target.value)}
190
+ maxTags={tagsField?.maxRows}
122
191
  />
123
192
  </div>
124
- </div>
193
+ )}
125
194
  <div>
126
195
  <label className={LABEL} htmlFor="pf-excerpt">
127
196
  Excerpt
@@ -47,11 +47,19 @@ import { SectionInspector } from '../page-editor/SectionInspector.js'
47
47
  import { AddSectionModal } from '../page-editor/AddSectionModal.js'
48
48
  import { ValidationSummary } from '../page-editor/ValidationSummary.js'
49
49
  import { PostEditorCanvas } from './PostEditorCanvas.js'
50
- import { PostFieldsModal } from './PostFieldsModal.js'
50
+ import { PostFieldsModal, type PostFieldConfig } from './PostFieldsModal.js'
51
51
 
52
52
  /** The slice of the resolved CMS config this editor reads. */
53
53
  interface EditorConfig {
54
- collections?: Record<string, { labels?: { singular?: string }; urlPrefix?: string } | undefined>
54
+ collections?: Record<
55
+ string,
56
+ | {
57
+ labels?: { singular?: string }
58
+ urlPrefix?: string
59
+ fields?: Record<string, PostFieldConfig | undefined>
60
+ }
61
+ | undefined
62
+ >
55
63
  seo?: { siteUrl?: string }
56
64
  }
57
65
 
@@ -576,6 +584,7 @@ export function PostSectionEditor({
576
584
  open={fieldsOpen}
577
585
  post={post}
578
586
  canEdit={canEdit}
587
+ fields={config?.collections?.[postType]?.fields}
579
588
  onClose={() => setFieldsOpen(false)}
580
589
  onSave={handleFields}
581
590
  />