@actuate-media/cms-admin 0.19.0 → 0.20.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 +9 -5
- package/dist/AdminRoot.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/seo/Drawer.d.ts +11 -0
- package/dist/components/seo/Drawer.d.ts.map +1 -0
- package/dist/components/seo/Drawer.js +13 -0
- package/dist/components/seo/Drawer.js.map +1 -0
- package/dist/components/seo/RedirectEditorDrawer.d.ts +9 -0
- package/dist/components/seo/RedirectEditorDrawer.d.ts.map +1 -0
- package/dist/components/seo/RedirectEditorDrawer.js +94 -0
- package/dist/components/seo/RedirectEditorDrawer.js.map +1 -0
- package/dist/components/seo/SeoCopilotDrawer.d.ts +10 -0
- package/dist/components/seo/SeoCopilotDrawer.d.ts.map +1 -0
- package/dist/components/seo/SeoCopilotDrawer.js +34 -0
- package/dist/components/seo/SeoCopilotDrawer.js.map +1 -0
- package/dist/components/seo/SeoEditorDrawer.d.ts +8 -0
- package/dist/components/seo/SeoEditorDrawer.d.ts.map +1 -0
- package/dist/components/seo/SeoEditorDrawer.js +111 -0
- package/dist/components/seo/SeoEditorDrawer.js.map +1 -0
- package/dist/components/seo/SeoIssueFixDrawer.d.ts +8 -0
- package/dist/components/seo/SeoIssueFixDrawer.d.ts.map +1 -0
- package/dist/components/seo/SeoIssueFixDrawer.js +111 -0
- package/dist/components/seo/SeoIssueFixDrawer.js.map +1 -0
- package/dist/components/seo/primitives.d.ts +53 -0
- package/dist/components/seo/primitives.d.ts.map +1 -0
- package/dist/components/seo/primitives.js +100 -0
- package/dist/components/seo/primitives.js.map +1 -0
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +3 -0
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/lib/seo-service.d.ts +322 -0
- package/dist/lib/seo-service.d.ts.map +1 -0
- package/dist/lib/seo-service.js +295 -0
- package/dist/lib/seo-service.js.map +1 -0
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +4 -1
- package/dist/router/index.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +36 -25
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/Posts/NewPostTypeInstructions.js +2 -2
- package/dist/views/Posts/post-type-presets.d.ts +5 -0
- package/dist/views/Posts/post-type-presets.d.ts.map +1 -1
- package/dist/views/Posts/post-type-presets.js +10 -64
- package/dist/views/Posts/post-type-presets.js.map +1 -1
- package/dist/views/SEO.d.ts.map +1 -1
- package/dist/views/SEO.js +89 -132
- package/dist/views/SEO.js.map +1 -1
- package/dist/views/seo/AuditTab.d.ts +6 -0
- package/dist/views/seo/AuditTab.d.ts.map +1 -0
- package/dist/views/seo/AuditTab.js +57 -0
- package/dist/views/seo/AuditTab.js.map +1 -0
- package/dist/views/seo/ContentTab.d.ts +5 -0
- package/dist/views/seo/ContentTab.d.ts.map +1 -0
- package/dist/views/seo/ContentTab.js +27 -0
- package/dist/views/seo/ContentTab.js.map +1 -0
- package/dist/views/seo/OverviewTab.d.ts +8 -0
- package/dist/views/seo/OverviewTab.d.ts.map +1 -0
- package/dist/views/seo/OverviewTab.js +35 -0
- package/dist/views/seo/OverviewTab.js.map +1 -0
- package/dist/views/seo/RedirectsTab.d.ts +2 -0
- package/dist/views/seo/RedirectsTab.d.ts.map +1 -0
- package/dist/views/seo/RedirectsTab.js +124 -0
- package/dist/views/seo/RedirectsTab.js.map +1 -0
- package/dist/views/seo/TechnicalTab.d.ts +2 -0
- package/dist/views/seo/TechnicalTab.d.ts.map +1 -0
- package/dist/views/seo/TechnicalTab.js +102 -0
- package/dist/views/seo/TechnicalTab.js.map +1 -0
- package/dist/views/seo/useSeoResource.d.ts +8 -0
- package/dist/views/seo/useSeoResource.d.ts.map +1 -0
- package/dist/views/seo/useSeoResource.js +42 -0
- package/dist/views/seo/useSeoResource.js.map +1 -0
- package/package.json +2 -2
- package/src/AdminRoot.tsx +9 -5
- package/src/components/seo/Drawer.tsx +61 -0
- package/src/components/seo/RedirectEditorDrawer.tsx +235 -0
- package/src/components/seo/SeoCopilotDrawer.tsx +76 -0
- package/src/components/seo/SeoEditorDrawer.tsx +347 -0
- package/src/components/seo/SeoIssueFixDrawer.tsx +244 -0
- package/src/components/seo/primitives.tsx +237 -0
- package/src/layout/Sidebar.tsx +3 -0
- package/src/lib/seo-service.ts +623 -0
- package/src/router/index.ts +4 -1
- package/src/views/MediaBrowser.tsx +89 -67
- package/src/views/Posts/NewPostTypeInstructions.tsx +2 -2
- package/src/views/Posts/post-type-presets.ts +15 -64
- package/src/views/SEO.tsx +186 -863
- package/src/views/seo/AuditTab.tsx +229 -0
- package/src/views/seo/ContentTab.tsx +164 -0
- package/src/views/seo/OverviewTab.tsx +257 -0
- package/src/views/seo/RedirectsTab.tsx +392 -0
- package/src/views/seo/TechnicalTab.tsx +303 -0
- package/src/views/seo/useSeoResource.ts +52 -0
|
@@ -30,7 +30,6 @@ import { sortByRelevance, type SortConfig, toggleSort } from '../lib/search.js'
|
|
|
30
30
|
import { useApiData } from '../lib/useApiData.js'
|
|
31
31
|
import { cmsApi } from '../lib/api.js'
|
|
32
32
|
import { FolderTree, type FolderSelection } from '../components/FolderTree.js'
|
|
33
|
-
import { FocalPointPicker } from '../components/FocalPointPicker.js'
|
|
34
33
|
|
|
35
34
|
interface MediaItem {
|
|
36
35
|
id: number | string
|
|
@@ -120,8 +119,6 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
120
119
|
const [editAlt, setEditAlt] = useState('')
|
|
121
120
|
const [editTitle, setEditTitle] = useState('')
|
|
122
121
|
const [editFilename, setEditFilename] = useState('')
|
|
123
|
-
const [focalX, setFocalX] = useState(0.5)
|
|
124
|
-
const [focalY, setFocalY] = useState(0.5)
|
|
125
122
|
const [saving, setSaving] = useState(false)
|
|
126
123
|
const [aiGenerating, setAiGenerating] = useState<string | null>(null)
|
|
127
124
|
const [uploading, setUploading] = useState(false)
|
|
@@ -157,8 +154,6 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
157
154
|
setEditAlt(item.altTag ?? '')
|
|
158
155
|
setEditTitle(item.title ?? '')
|
|
159
156
|
setEditFilename(item.name)
|
|
160
|
-
setFocalX((item as any).focalPointX ?? 0.5)
|
|
161
|
-
setFocalY((item as any).focalPointY ?? 0.5)
|
|
162
157
|
}
|
|
163
158
|
|
|
164
159
|
const closeDetail = () => {
|
|
@@ -187,8 +182,6 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
187
182
|
alt: editAlt,
|
|
188
183
|
title: editTitle,
|
|
189
184
|
filename: editFilename,
|
|
190
|
-
focalX,
|
|
191
|
-
focalY,
|
|
192
185
|
}),
|
|
193
186
|
})
|
|
194
187
|
setSaving(false)
|
|
@@ -244,18 +237,38 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
244
237
|
return
|
|
245
238
|
}
|
|
246
239
|
|
|
247
|
-
|
|
240
|
+
if (!activeItem) {
|
|
241
|
+
setAiGenerating(null)
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const res = await cmsApi<{ alt?: string; title?: string; alternatives?: string[] }>(
|
|
246
|
+
'/ai/media-metadata',
|
|
247
|
+
{ method: 'POST', body: JSON.stringify({ mediaId: activeItem.id }) },
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if (res.error) {
|
|
251
|
+
// AI plugin/provider not configured (e.g. local dev): fall back to a
|
|
252
|
+
// basic filename-derived suggestion so the action still helps.
|
|
253
|
+
const base = (activeItem.name ?? '')
|
|
254
|
+
.replace(/\.\w+$/, '')
|
|
255
|
+
.replace(/[-_]/g, ' ')
|
|
256
|
+
.trim()
|
|
257
|
+
if (field === 'alt') {
|
|
258
|
+
setEditAlt(base ? `Image of ${base}` : '')
|
|
259
|
+
} else {
|
|
260
|
+
setEditTitle(base.replace(/\b\w/g, (c) => c.toUpperCase()))
|
|
261
|
+
}
|
|
262
|
+
toast.info('AI unavailable — generated a basic suggestion from the filename')
|
|
263
|
+
setAiGenerating(null)
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
248
267
|
if (field === 'alt') {
|
|
249
|
-
|
|
250
|
-
setEditAlt(generated)
|
|
268
|
+
setEditAlt(res.data?.alt ?? '')
|
|
251
269
|
toast.success('Alt tag generated by AI')
|
|
252
|
-
} else
|
|
253
|
-
|
|
254
|
-
activeItem?.name
|
|
255
|
-
.replace(/[-_]/g, ' ')
|
|
256
|
-
.replace(/\.\w+$/, '')
|
|
257
|
-
.replace(/\b\w/g, (c) => c.toUpperCase()) ?? ''
|
|
258
|
-
setEditTitle(generated)
|
|
270
|
+
} else {
|
|
271
|
+
setEditTitle(res.data?.title ?? '')
|
|
259
272
|
toast.success('Title generated by AI')
|
|
260
273
|
}
|
|
261
274
|
setAiGenerating(null)
|
|
@@ -533,7 +546,7 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
533
546
|
>
|
|
534
547
|
{viewMode === 'grid' ? (
|
|
535
548
|
<div
|
|
536
|
-
className={`grid h-full gap-
|
|
549
|
+
className={`grid h-full auto-rows-min content-start gap-3 overflow-y-auto p-2 sm:gap-4 sm:p-3 ${
|
|
537
550
|
panelOpen
|
|
538
551
|
? 'grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'
|
|
539
552
|
: 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-8'
|
|
@@ -542,10 +555,17 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
542
555
|
{filteredAndSorted.map((item) => {
|
|
543
556
|
const isActive = activeItem?.id === item.id
|
|
544
557
|
const hasIssues = !item.altTag || !item.title || item.usedOn?.length === 0
|
|
558
|
+
const typeLabel = (
|
|
559
|
+
item.name.includes('.')
|
|
560
|
+
? item.name.split('.').pop()!
|
|
561
|
+
: item.format || item.type || ''
|
|
562
|
+
)
|
|
563
|
+
.replace(/^.*\//, '')
|
|
564
|
+
.toUpperCase()
|
|
545
565
|
return (
|
|
546
566
|
<div
|
|
547
567
|
key={item.id}
|
|
548
|
-
className={`group relative
|
|
568
|
+
className={`group relative flex cursor-pointer flex-col overflow-hidden rounded-lg border-2 transition-all ${
|
|
549
569
|
isActive
|
|
550
570
|
? 'border-blue-500 ring-2 ring-blue-200'
|
|
551
571
|
: selectedMedia.includes(item.id)
|
|
@@ -556,45 +576,61 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
556
576
|
draggable
|
|
557
577
|
onDragStart={(e) => handleDragStart(e, item.id)}
|
|
558
578
|
>
|
|
559
|
-
<div className="
|
|
560
|
-
<
|
|
561
|
-
|
|
562
|
-
<div className="absolute inset-0 bg-linear-to-t from-black/60 to-transparent opacity-0 transition-opacity group-hover:opacity-100">
|
|
563
|
-
<div className="absolute right-0 bottom-0 left-0 p-2">
|
|
564
|
-
<p className="truncate text-xs font-medium text-white">{item.name}</p>
|
|
565
|
-
<p className="text-xs text-white/80">{item.size}</p>
|
|
579
|
+
<div className="relative aspect-square w-full overflow-hidden">
|
|
580
|
+
<div className="flex h-full w-full items-center justify-center bg-gray-100">
|
|
581
|
+
<MediaPreview item={item} />
|
|
566
582
|
</div>
|
|
567
|
-
|
|
568
|
-
|
|
583
|
+
{hasIssues && (
|
|
584
|
+
<div
|
|
585
|
+
className="absolute top-1.5 left-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-yellow-500"
|
|
586
|
+
title="Needs attention"
|
|
587
|
+
>
|
|
588
|
+
<AlertTriangle className="h-3 w-3 text-white" />
|
|
589
|
+
</div>
|
|
590
|
+
)}
|
|
569
591
|
<div
|
|
570
|
-
className="absolute top-1.5
|
|
571
|
-
|
|
592
|
+
className="absolute top-1.5 right-1.5 opacity-0 transition-opacity group-hover:opacity-100"
|
|
593
|
+
onClick={(e) => handleCheckbox(e, item.id)}
|
|
572
594
|
>
|
|
573
|
-
<
|
|
595
|
+
<div
|
|
596
|
+
className={`flex h-5 w-5 items-center justify-center rounded border-2 transition-colors ${
|
|
597
|
+
selectedMedia.includes(item.id)
|
|
598
|
+
? 'border-blue-600 bg-blue-600'
|
|
599
|
+
: 'border-gray-400 bg-white/80'
|
|
600
|
+
}`}
|
|
601
|
+
>
|
|
602
|
+
{selectedMedia.includes(item.id) && (
|
|
603
|
+
<svg
|
|
604
|
+
className="h-3 w-3 text-white"
|
|
605
|
+
fill="none"
|
|
606
|
+
viewBox="0 0 12 12"
|
|
607
|
+
>
|
|
608
|
+
<path
|
|
609
|
+
d="M10 3L4.5 8.5L2 6"
|
|
610
|
+
stroke="currentColor"
|
|
611
|
+
strokeWidth="2"
|
|
612
|
+
strokeLinecap="round"
|
|
613
|
+
strokeLinejoin="round"
|
|
614
|
+
/>
|
|
615
|
+
</svg>
|
|
616
|
+
)}
|
|
617
|
+
</div>
|
|
574
618
|
</div>
|
|
575
|
-
|
|
576
|
-
<div
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
<div
|
|
581
|
-
className={`flex h-5 w-5 items-center justify-center rounded border-2 transition-colors ${
|
|
582
|
-
selectedMedia.includes(item.id)
|
|
583
|
-
? 'border-blue-600 bg-blue-600'
|
|
584
|
-
: 'border-gray-400 bg-white/80'
|
|
619
|
+
</div>
|
|
620
|
+
<div className="flex items-center gap-2 border-t border-gray-200 bg-white px-2 py-1.5">
|
|
621
|
+
<span
|
|
622
|
+
className={`mt-0.5 h-2 w-2 shrink-0 self-start rounded-full ${
|
|
623
|
+
hasIssues ? 'bg-yellow-500' : 'bg-green-500'
|
|
585
624
|
}`}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
/>
|
|
596
|
-
</svg>
|
|
597
|
-
)}
|
|
625
|
+
title={hasIssues ? 'Needs attention' : 'SEO complete'}
|
|
626
|
+
/>
|
|
627
|
+
<div className="min-w-0 flex-1">
|
|
628
|
+
<p className="truncate text-xs font-medium text-gray-900">
|
|
629
|
+
{item.name}
|
|
630
|
+
</p>
|
|
631
|
+
<p className="truncate text-xs text-gray-500">
|
|
632
|
+
{[typeLabel, item.size].filter(Boolean).join(' · ')}
|
|
633
|
+
</p>
|
|
598
634
|
</div>
|
|
599
635
|
</div>
|
|
600
636
|
</div>
|
|
@@ -872,20 +908,6 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
872
908
|
</div>
|
|
873
909
|
</div>
|
|
874
910
|
|
|
875
|
-
{isImageMedia(activeItem) && activeItem.url && (
|
|
876
|
-
<div className="border-b border-gray-200 p-4">
|
|
877
|
-
<FocalPointPicker
|
|
878
|
-
imageUrl={activeItem.url}
|
|
879
|
-
focalX={focalX}
|
|
880
|
-
focalY={focalY}
|
|
881
|
-
onChange={(x, y) => {
|
|
882
|
-
setFocalX(x)
|
|
883
|
-
setFocalY(y)
|
|
884
|
-
}}
|
|
885
|
-
/>
|
|
886
|
-
</div>
|
|
887
|
-
)}
|
|
888
|
-
|
|
889
911
|
<div className="border-b border-gray-200 p-4">
|
|
890
912
|
<h4 className="mb-3 text-xs font-semibold tracking-wide text-gray-500 uppercase">
|
|
891
913
|
Used On {activeItem.usedOn && `(${activeItem.usedOn.length})`}
|
|
@@ -11,8 +11,8 @@ import { POST_TYPE_PRESETS, type PresetColor, type QuickPreset } from './post-ty
|
|
|
11
11
|
* /posts/types/new — Quick Post Type gallery + snippet generator.
|
|
12
12
|
*
|
|
13
13
|
* Two ways to start:
|
|
14
|
-
* 1. Pick a Quick Post Type preset (
|
|
15
|
-
*
|
|
14
|
+
* 1. Pick a Quick Post Type preset (Blog) — pre-fills the form and
|
|
15
|
+
* tailored extra fields.
|
|
16
16
|
* 2. Fill the form from scratch.
|
|
17
17
|
*
|
|
18
18
|
* Either way the page emits a paste-ready `actuate.config.ts` snippet.
|
|
@@ -10,6 +10,11 @@ import type { PostTypeIconName } from '../../components/posts/icons.js'
|
|
|
10
10
|
* Presets are NOT a runtime data model — they only seed the snippet.
|
|
11
11
|
* Collections stay code-defined; this just saves the developer from
|
|
12
12
|
* hand-authoring common field sets.
|
|
13
|
+
*
|
|
14
|
+
* Only the canonical **Blog** post type ships as a built-in preset. Other
|
|
15
|
+
* content types (case studies, portfolio, team, …) are available as opt-in
|
|
16
|
+
* collection factories in `@actuate-media/cms-core` (`collections/presets.ts`)
|
|
17
|
+
* rather than shipped here as defaults.
|
|
13
18
|
*/
|
|
14
19
|
|
|
15
20
|
export type PresetColor = 'purple' | 'green' | 'orange' | 'blue' | 'red' | 'pink'
|
|
@@ -34,72 +39,18 @@ export interface QuickPreset {
|
|
|
34
39
|
|
|
35
40
|
export const POST_TYPE_PRESETS: QuickPreset[] = [
|
|
36
41
|
{
|
|
37
|
-
key: '
|
|
38
|
-
label: '
|
|
39
|
-
singular: '
|
|
40
|
-
plural: '
|
|
41
|
-
slug: '
|
|
42
|
-
description: '
|
|
43
|
-
color: 'green',
|
|
44
|
-
icon: 'briefcase',
|
|
45
|
-
extraFields: [
|
|
46
|
-
"client: { type: 'text', label: 'Client Name' },",
|
|
47
|
-
"industry: { type: 'text', label: 'Industry' },",
|
|
48
|
-
"results: { type: 'richText', label: 'Results & Outcomes' },",
|
|
49
|
-
],
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
key: 'news',
|
|
53
|
-
label: 'News',
|
|
54
|
-
singular: 'News Item',
|
|
55
|
-
plural: 'News',
|
|
56
|
-
slug: 'news',
|
|
57
|
-
description: 'Company announcements, press releases, and industry news.',
|
|
58
|
-
color: 'orange',
|
|
59
|
-
icon: 'newspaper',
|
|
60
|
-
extraFields: ["source: { type: 'text', label: 'Source' },"],
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
key: 'portfolio',
|
|
64
|
-
label: 'Portfolio',
|
|
65
|
-
singular: 'Portfolio Item',
|
|
66
|
-
plural: 'Portfolio',
|
|
67
|
-
slug: 'portfolio',
|
|
68
|
-
description: 'Showcase projects, work samples, and creative pieces.',
|
|
69
|
-
color: 'blue',
|
|
70
|
-
icon: 'image',
|
|
71
|
-
extraFields: [
|
|
72
|
-
"projectUrl: { type: 'url', label: 'Project URL' },",
|
|
73
|
-
"gallery: { type: 'media', label: 'Gallery' },",
|
|
74
|
-
],
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
key: 'events',
|
|
78
|
-
label: 'Events',
|
|
79
|
-
singular: 'Event',
|
|
80
|
-
plural: 'Events',
|
|
81
|
-
slug: 'events',
|
|
82
|
-
description: 'Webinars, conferences, and scheduled happenings.',
|
|
83
|
-
color: 'pink',
|
|
84
|
-
icon: 'calendar',
|
|
85
|
-
extraFields: [
|
|
86
|
-
"startDate: { type: 'date', label: 'Start Date', includeTime: true },",
|
|
87
|
-
"endDate: { type: 'date', label: 'End Date', includeTime: true },",
|
|
88
|
-
"location: { type: 'text', label: 'Location' },",
|
|
89
|
-
],
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
key: 'team',
|
|
93
|
-
label: 'Team Members',
|
|
94
|
-
singular: 'Team Member',
|
|
95
|
-
plural: 'Team',
|
|
96
|
-
slug: 'team',
|
|
97
|
-
description: 'Staff bios, headshots, and role information.',
|
|
42
|
+
key: 'blog',
|
|
43
|
+
label: 'Blog',
|
|
44
|
+
singular: 'Post',
|
|
45
|
+
plural: 'Posts',
|
|
46
|
+
slug: 'blog',
|
|
47
|
+
description: 'Long-form articles and blog posts with author, category, and tags.',
|
|
98
48
|
color: 'purple',
|
|
99
|
-
icon: '
|
|
49
|
+
icon: 'newspaper',
|
|
100
50
|
extraFields: [
|
|
101
|
-
"
|
|
102
|
-
"
|
|
51
|
+
"author: { type: 'text', label: 'Author' },",
|
|
52
|
+
"category: { type: 'text', label: 'Category' },",
|
|
53
|
+
"tags: { type: 'array', label: 'Tags', fields: { value: { type: 'text', label: 'Tag' } } },",
|
|
103
54
|
],
|
|
104
55
|
},
|
|
105
56
|
]
|