@actuate-media/cms-admin 0.66.0 → 0.68.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/CHANGELOG.md +37 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +36 -9
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/components/inspector-pane.test.d.ts +2 -0
- package/dist/__tests__/components/inspector-pane.test.d.ts.map +1 -0
- package/dist/__tests__/components/inspector-pane.test.js +31 -0
- package/dist/__tests__/components/inspector-pane.test.js.map +1 -0
- package/dist/__tests__/hooks/useIdleLogout.test.d.ts +2 -0
- package/dist/__tests__/hooks/useIdleLogout.test.d.ts.map +1 -0
- package/dist/__tests__/hooks/useIdleLogout.test.js +64 -0
- package/dist/__tests__/hooks/useIdleLogout.test.js.map +1 -0
- package/dist/__tests__/lib/pages-tree.test.d.ts +2 -0
- package/dist/__tests__/lib/pages-tree.test.d.ts.map +1 -0
- package/dist/__tests__/lib/pages-tree.test.js +173 -0
- package/dist/__tests__/lib/pages-tree.test.js.map +1 -0
- package/dist/__tests__/lib/post-types-service.test.d.ts +2 -0
- package/dist/__tests__/lib/post-types-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/post-types-service.test.js +88 -0
- package/dist/__tests__/lib/post-types-service.test.js.map +1 -0
- package/dist/__tests__/views/collection-list-nav.render.test.js +43 -5
- package/dist/__tests__/views/collection-list-nav.render.test.js.map +1 -1
- package/dist/__tests__/views/dashboard.test.js +7 -3
- package/dist/__tests__/views/dashboard.test.js.map +1 -1
- package/dist/__tests__/views/forms-list.render.test.js +25 -6
- package/dist/__tests__/views/forms-list.render.test.js.map +1 -1
- package/dist/__tests__/views/media-seo-audit.render.test.d.ts +2 -0
- package/dist/__tests__/views/media-seo-audit.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/media-seo-audit.render.test.js +102 -0
- package/dist/__tests__/views/media-seo-audit.render.test.js.map +1 -0
- package/dist/__tests__/views/onboarding-block.render.test.d.ts +2 -0
- package/dist/__tests__/views/onboarding-block.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/onboarding-block.render.test.js +146 -0
- package/dist/__tests__/views/onboarding-block.render.test.js.map +1 -0
- package/dist/__tests__/views/page-builder-sections-panel.render.test.d.ts +2 -0
- package/dist/__tests__/views/page-builder-sections-panel.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/page-builder-sections-panel.render.test.js +121 -0
- package/dist/__tests__/views/page-builder-sections-panel.render.test.js.map +1 -0
- package/dist/__tests__/views/page-templates.render.test.d.ts +2 -0
- package/dist/__tests__/views/page-templates.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/page-templates.render.test.js +105 -0
- package/dist/__tests__/views/page-templates.render.test.js.map +1 -0
- package/dist/__tests__/views/pages-list-view.test.js +77 -1
- package/dist/__tests__/views/pages-list-view.test.js.map +1 -1
- package/dist/__tests__/views/post-type-editor.render.test.d.ts +2 -0
- package/dist/__tests__/views/post-type-editor.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/post-type-editor.render.test.js +102 -0
- package/dist/__tests__/views/post-type-editor.render.test.js.map +1 -0
- package/dist/__tests__/views/security-settings.render.test.js +85 -4
- package/dist/__tests__/views/security-settings.render.test.js.map +1 -1
- package/dist/__tests__/views/seo-content-pane.render.test.d.ts +2 -0
- package/dist/__tests__/views/seo-content-pane.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/seo-content-pane.render.test.js +117 -0
- package/dist/__tests__/views/seo-content-pane.render.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/components/collections/EntryDetailPane.d.ts +10 -0
- package/dist/components/collections/EntryDetailPane.d.ts.map +1 -0
- package/dist/components/collections/EntryDetailPane.js +92 -0
- package/dist/components/collections/EntryDetailPane.js.map +1 -0
- package/dist/components/forms/FieldsPanel.js +1 -1
- package/dist/components/forms/FieldsPanel.js.map +1 -1
- package/dist/components/forms/FormSchemaPane.d.ts +8 -0
- package/dist/components/forms/FormSchemaPane.d.ts.map +1 -0
- package/dist/components/forms/FormSchemaPane.js +95 -0
- package/dist/components/forms/FormSchemaPane.js.map +1 -0
- package/dist/components/script-tags/ScriptTagEditorPane.d.ts +26 -0
- package/dist/components/script-tags/ScriptTagEditorPane.d.ts.map +1 -0
- package/dist/components/script-tags/ScriptTagEditorPane.js +160 -0
- package/dist/components/script-tags/ScriptTagEditorPane.js.map +1 -0
- package/dist/components/seo/SeoEditorPane.d.ts +9 -0
- package/dist/components/seo/SeoEditorPane.d.ts.map +1 -0
- package/dist/components/seo/SeoEditorPane.js +175 -0
- package/dist/components/seo/SeoEditorPane.js.map +1 -0
- package/dist/components/ui/Badge.d.ts +1 -1
- package/dist/components/ui/Button.d.ts +1 -1
- package/dist/components/ui/InspectorPane.d.ts +51 -0
- package/dist/components/ui/InspectorPane.d.ts.map +1 -0
- package/dist/components/ui/InspectorPane.js +42 -0
- package/dist/components/ui/InspectorPane.js.map +1 -0
- package/dist/components/ui/StatusBadge.d.ts +1 -1
- package/dist/components/ui/index.d.ts +2 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +1 -0
- package/dist/components/ui/index.js.map +1 -1
- package/dist/hooks/useIdleLogout.d.ts +18 -0
- package/dist/hooks/useIdleLogout.d.ts.map +1 -0
- package/dist/hooks/useIdleLogout.js +66 -0
- package/dist/hooks/useIdleLogout.js.map +1 -0
- package/dist/lib/api.d.ts +1 -0
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +7 -1
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/page-editor-service.d.ts +11 -2
- package/dist/lib/page-editor-service.d.ts.map +1 -1
- package/dist/lib/page-editor-service.js +8 -2
- package/dist/lib/page-editor-service.js.map +1 -1
- package/dist/lib/pages-tree.d.ts +44 -0
- package/dist/lib/pages-tree.d.ts.map +1 -0
- package/dist/lib/pages-tree.js +106 -0
- package/dist/lib/pages-tree.js.map +1 -0
- package/dist/lib/post-types-service.d.ts +44 -0
- package/dist/lib/post-types-service.d.ts.map +1 -0
- package/dist/lib/post-types-service.js +83 -0
- package/dist/lib/post-types-service.js.map +1 -0
- package/dist/views/CollectionList.d.ts.map +1 -1
- package/dist/views/CollectionList.js +23 -4
- package/dist/views/CollectionList.js.map +1 -1
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +31 -2
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/Forms.d.ts.map +1 -1
- package/dist/views/Forms.js +25 -21
- package/dist/views/Forms.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +28 -2
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/MediaSeoAudit.d.ts +25 -0
- package/dist/views/MediaSeoAudit.d.ts.map +1 -0
- package/dist/views/MediaSeoAudit.js +97 -0
- package/dist/views/MediaSeoAudit.js.map +1 -0
- package/dist/views/Pages/PagesListView.d.ts.map +1 -1
- package/dist/views/Pages/PagesListView.js +90 -10
- package/dist/views/Pages/PagesListView.js.map +1 -1
- package/dist/views/Pages/PagesTreeTable.d.ts +35 -0
- package/dist/views/Pages/PagesTreeTable.d.ts.map +1 -0
- package/dist/views/Pages/PagesTreeTable.js +28 -0
- package/dist/views/Pages/PagesTreeTable.js.map +1 -0
- package/dist/views/Posts/PostTypeEditor.d.ts +16 -0
- package/dist/views/Posts/PostTypeEditor.d.ts.map +1 -0
- package/dist/views/Posts/PostTypeEditor.js +159 -0
- package/dist/views/Posts/PostTypeEditor.js.map +1 -0
- package/dist/views/Posts/PostTypesView.d.ts +3 -2
- package/dist/views/Posts/PostTypesView.d.ts.map +1 -1
- package/dist/views/Posts/PostTypesView.js +1 -1
- package/dist/views/Posts/PostTypesView.js.map +1 -1
- package/dist/views/ScriptTags.d.ts +5 -1
- package/dist/views/ScriptTags.d.ts.map +1 -1
- package/dist/views/ScriptTags.js +32 -7
- package/dist/views/ScriptTags.js.map +1 -1
- package/dist/views/dashboard/OnboardingBlock.d.ts +29 -0
- package/dist/views/dashboard/OnboardingBlock.d.ts.map +1 -0
- package/dist/views/dashboard/OnboardingBlock.js +71 -0
- package/dist/views/dashboard/OnboardingBlock.js.map +1 -0
- package/dist/views/page-builder/BuilderSectionsPanel.d.ts +20 -0
- package/dist/views/page-builder/BuilderSectionsPanel.d.ts.map +1 -0
- package/dist/views/page-builder/BuilderSectionsPanel.js +105 -0
- package/dist/views/page-builder/BuilderSectionsPanel.js.map +1 -0
- package/dist/views/page-builder/PageBuilder.d.ts.map +1 -1
- package/dist/views/page-builder/PageBuilder.js +51 -2
- package/dist/views/page-builder/PageBuilder.js.map +1 -1
- package/dist/views/page-builder/PageTemplates.d.ts.map +1 -1
- package/dist/views/page-builder/PageTemplates.js +113 -6
- package/dist/views/page-builder/PageTemplates.js.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.d.ts +7 -1
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.js +3 -3
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/page-editor/PageSettingsModal.d.ts +1 -1
- package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -1
- package/dist/views/page-editor/PageSettingsModal.js +42 -3
- package/dist/views/page-editor/PageSettingsModal.js.map +1 -1
- package/dist/views/seo/ContentTab.d.ts.map +1 -1
- package/dist/views/seo/ContentTab.js +22 -6
- package/dist/views/seo/ContentTab.js.map +1 -1
- package/dist/views/settings/AccessProtectionCard.d.ts +5 -1
- package/dist/views/settings/AccessProtectionCard.d.ts.map +1 -1
- package/dist/views/settings/AccessProtectionCard.js +13 -10
- package/dist/views/settings/AccessProtectionCard.js.map +1 -1
- package/dist/views/settings/SecurityTab.d.ts.map +1 -1
- package/dist/views/settings/SecurityTab.js +3 -3
- package/dist/views/settings/SecurityTab.js.map +1 -1
- package/dist/views/settings/SessionPolicyCard.d.ts +1 -2
- package/dist/views/settings/SessionPolicyCard.d.ts.map +1 -1
- package/dist/views/settings/SessionPolicyCard.js +12 -5
- package/dist/views/settings/SessionPolicyCard.js.map +1 -1
- package/dist/views/settings/useSecuritySettings.d.ts +13 -1
- package/dist/views/settings/useSecuritySettings.d.ts.map +1 -1
- package/dist/views/settings/useSecuritySettings.js +47 -4
- package/dist/views/settings/useSecuritySettings.js.map +1 -1
- package/package.json +2 -2
- package/src/AdminRoot.tsx +43 -10
- package/src/__tests__/components/inspector-pane.test.tsx +64 -0
- package/src/__tests__/hooks/useIdleLogout.test.tsx +86 -0
- package/src/__tests__/lib/pages-tree.test.ts +197 -0
- package/src/__tests__/lib/post-types-service.test.ts +112 -0
- package/src/__tests__/views/collection-list-nav.render.test.tsx +51 -5
- package/src/__tests__/views/dashboard.test.tsx +7 -3
- package/src/__tests__/views/forms-list.render.test.tsx +34 -6
- package/src/__tests__/views/media-seo-audit.render.test.tsx +117 -0
- package/src/__tests__/views/onboarding-block.render.test.tsx +193 -0
- package/src/__tests__/views/page-builder-sections-panel.render.test.tsx +138 -0
- package/src/__tests__/views/page-templates.render.test.tsx +142 -0
- package/src/__tests__/views/pages-list-view.test.tsx +93 -1
- package/src/__tests__/views/post-type-editor.render.test.tsx +116 -0
- package/src/__tests__/views/security-settings.render.test.tsx +111 -4
- package/src/__tests__/views/seo-content-pane.render.test.tsx +142 -0
- package/src/components/collections/EntryDetailPane.tsx +182 -0
- package/src/components/forms/FieldsPanel.tsx +3 -1
- package/src/components/forms/FormSchemaPane.tsx +192 -0
- package/src/components/script-tags/ScriptTagEditorPane.tsx +474 -0
- package/src/components/seo/SeoEditorPane.tsx +533 -0
- package/src/components/ui/InspectorPane.tsx +129 -0
- package/src/components/ui/index.ts +10 -0
- package/src/hooks/useIdleLogout.ts +77 -0
- package/src/lib/api.ts +8 -2
- package/src/lib/page-editor-service.ts +16 -2
- package/src/lib/pages-tree.ts +145 -0
- package/src/lib/post-types-service.ts +129 -0
- package/src/views/CollectionList.tsx +158 -110
- package/src/views/Dashboard.tsx +238 -196
- package/src/views/Forms.tsx +154 -126
- package/src/views/MediaBrowser.tsx +641 -511
- package/src/views/MediaSeoAudit.tsx +320 -0
- package/src/views/Pages/PagesListView.tsx +352 -183
- package/src/views/Pages/PagesTreeTable.tsx +339 -0
- package/src/views/Posts/PostTypeEditor.tsx +568 -0
- package/src/views/Posts/PostTypesView.tsx +5 -4
- package/src/views/ScriptTags.tsx +204 -129
- package/src/views/dashboard/OnboardingBlock.tsx +233 -0
- package/src/views/page-builder/BuilderSectionsPanel.tsx +304 -0
- package/src/views/page-builder/PageBuilder.tsx +69 -2
- package/src/views/page-builder/PageTemplates.tsx +340 -57
- package/src/views/page-editor/PageSectionEditor.tsx +9 -2
- package/src/views/page-editor/PageSettingsModal.tsx +75 -2
- package/src/views/seo/ContentTab.tsx +113 -80
- package/src/views/settings/AccessProtectionCard.tsx +83 -53
- package/src/views/settings/SecurityTab.tsx +23 -2
- package/src/views/settings/SessionPolicyCard.tsx +45 -9
- package/src/views/settings/useSecuritySettings.ts +78 -22
- package/dist/components/forms/FormSchemaDrawer.d.ts +0 -9
- package/dist/components/forms/FormSchemaDrawer.d.ts.map +0 -1
- package/dist/components/forms/FormSchemaDrawer.js +0 -99
- package/dist/components/forms/FormSchemaDrawer.js.map +0 -1
- package/dist/components/seo/SeoEditorDrawer.d.ts +0 -8
- package/dist/components/seo/SeoEditorDrawer.d.ts.map +0 -1
- package/dist/components/seo/SeoEditorDrawer.js +0 -172
- package/dist/components/seo/SeoEditorDrawer.js.map +0 -1
- package/dist/views/ScriptTagEditor.d.ts +0 -6
- package/dist/views/ScriptTagEditor.d.ts.map +0 -1
- package/dist/views/ScriptTagEditor.js +0 -111
- package/dist/views/ScriptTagEditor.js.map +0 -1
- package/src/components/forms/FormSchemaDrawer.tsx +0 -192
- package/src/components/seo/SeoEditorDrawer.tsx +0 -529
- package/src/views/ScriptTagEditor.tsx +0 -353
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useMemo, useState } from 'react'
|
|
4
|
+
import { AlertTriangle, Layers, Pencil, RefreshCw, Trash2 } from 'lucide-react'
|
|
5
|
+
import { toast } from 'sonner'
|
|
6
|
+
import { cmsApi } from '../../lib/api.js'
|
|
4
7
|
import { useApiData } from '../../lib/useApiData.js'
|
|
8
|
+
import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'
|
|
9
|
+
import { EmptyState } from '../../components/ui/EmptyState.js'
|
|
10
|
+
import { FilterPill } from '../../components/ui/FilterPill.js'
|
|
11
|
+
import { Input } from '../../components/ui/Input.js'
|
|
12
|
+
import { Modal } from '../../components/ui/Modal.js'
|
|
13
|
+
import { Button } from '../../components/ui/Button.js'
|
|
14
|
+
import { RowActions, RowActionButton } from '../../components/ui/RowActions.js'
|
|
15
|
+
import { SearchInput } from '../../components/ui/SearchInput.js'
|
|
16
|
+
import { Select } from '../../components/ui/Select.js'
|
|
17
|
+
import { Skeleton } from '../../components/ui/Skeleton.js'
|
|
5
18
|
|
|
6
19
|
interface PageTemplate {
|
|
7
20
|
id: string
|
|
@@ -10,6 +23,25 @@ interface PageTemplate {
|
|
|
10
23
|
category?: string
|
|
11
24
|
builtIn?: boolean
|
|
12
25
|
updatedAt?: string
|
|
26
|
+
thumbnail?: string | null
|
|
27
|
+
tree?: { children?: unknown[] } | null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type CategoryId = 'content' | 'landing' | 'utility'
|
|
31
|
+
|
|
32
|
+
const CATEGORIES: Record<CategoryId, { label: string; dot: string; text: string }> = {
|
|
33
|
+
content: { label: 'Content', dot: 'bg-[var(--acc)]', text: 'text-[var(--acc)]' },
|
|
34
|
+
landing: { label: 'Landing', dot: 'bg-[var(--suc)]', text: 'text-[var(--suc)]' },
|
|
35
|
+
utility: { label: 'Utility', dot: 'bg-[var(--warn)]', text: 'text-[var(--warn)]' },
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function categoryOf(template: PageTemplate): CategoryId {
|
|
39
|
+
const cat = template.category
|
|
40
|
+
return cat === 'landing' || cat === 'utility' ? cat : 'content'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sectionCountOf(template: PageTemplate): number {
|
|
44
|
+
return Array.isArray(template.tree?.children) ? template.tree.children.length : 0
|
|
13
45
|
}
|
|
14
46
|
|
|
15
47
|
export interface PageTemplatesProps {
|
|
@@ -18,94 +50,345 @@ export interface PageTemplatesProps {
|
|
|
18
50
|
|
|
19
51
|
export function PageTemplates({ onNavigate }: PageTemplatesProps) {
|
|
20
52
|
const { data, loading, error, refetch } = useApiData<PageTemplate[]>('/page-templates')
|
|
21
|
-
const templates = data ?? []
|
|
53
|
+
const templates = useMemo(() => data ?? [], [data])
|
|
22
54
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
)
|
|
55
|
+
const [search, setSearch] = useState('')
|
|
56
|
+
const [category, setCategory] = useState<CategoryId | 'all'>('all')
|
|
57
|
+
const [editTarget, setEditTarget] = useState<PageTemplate | null>(null)
|
|
58
|
+
const [deleteTarget, setDeleteTarget] = useState<PageTemplate | null>(null)
|
|
59
|
+
|
|
60
|
+
const filtered = useMemo(() => {
|
|
61
|
+
const q = search.trim().toLowerCase()
|
|
62
|
+
return templates.filter((t) => {
|
|
63
|
+
const matchesQuery =
|
|
64
|
+
!q ||
|
|
65
|
+
(t.name ?? '').toLowerCase().includes(q) ||
|
|
66
|
+
(t.description ?? '').toLowerCase().includes(q)
|
|
67
|
+
const matchesCategory = category === 'all' || categoryOf(t) === category
|
|
68
|
+
return matchesQuery && matchesCategory
|
|
69
|
+
})
|
|
70
|
+
}, [templates, search, category])
|
|
71
|
+
|
|
72
|
+
async function confirmDelete() {
|
|
73
|
+
if (!deleteTarget) return
|
|
74
|
+
const res = await cmsApi(`/page-templates/${deleteTarget.id}`, { method: 'DELETE' })
|
|
75
|
+
if (res.error) {
|
|
76
|
+
toast.error(res.error)
|
|
77
|
+
} else {
|
|
78
|
+
toast.success('Template deleted')
|
|
79
|
+
refetch()
|
|
80
|
+
}
|
|
30
81
|
}
|
|
31
82
|
|
|
32
83
|
return (
|
|
33
84
|
<div className="p-4 pr-8">
|
|
34
85
|
{error && (
|
|
35
|
-
<div className="
|
|
36
|
-
<AlertTriangle className="
|
|
37
|
-
<span className="
|
|
86
|
+
<div className="mb-4 flex items-center gap-3 rounded-[var(--r)] border border-[var(--bdr)] bg-[var(--card)] p-3">
|
|
87
|
+
<AlertTriangle className="h-5 w-5 shrink-0 text-[var(--muted)]" aria-hidden />
|
|
88
|
+
<span className="flex-1 text-sm text-[var(--txt)]">{error}</span>
|
|
38
89
|
<button
|
|
39
90
|
type="button"
|
|
40
91
|
onClick={refetch}
|
|
41
|
-
className="
|
|
92
|
+
className="rounded-md border border-[var(--bdr)] px-3 py-1 text-sm text-[var(--txt)] hover:bg-[var(--acc-l)]"
|
|
42
93
|
>
|
|
43
94
|
Retry
|
|
44
95
|
</button>
|
|
45
96
|
</div>
|
|
46
97
|
)}
|
|
47
98
|
|
|
48
|
-
<div className="mb-4 flex items-center justify-between">
|
|
99
|
+
<div className="mb-4 flex items-center justify-between gap-4">
|
|
49
100
|
<div>
|
|
50
101
|
<h1 className="text-foreground mb-1">Page Templates</h1>
|
|
51
|
-
<p className="text-
|
|
102
|
+
<p className="text-sm text-[var(--sub)]">
|
|
52
103
|
{templates.length} saved template{templates.length === 1 ? '' : 's'}
|
|
53
104
|
</p>
|
|
54
105
|
</div>
|
|
55
106
|
<button
|
|
56
107
|
type="button"
|
|
57
108
|
onClick={refetch}
|
|
58
|
-
className="
|
|
109
|
+
className="inline-flex items-center gap-2 rounded-md border border-[var(--bdr)] px-3 py-2 text-sm font-medium text-[var(--txt)] transition-colors duration-[var(--motion-fast)] hover:border-[var(--acc-m)] hover:bg-[var(--acc-l)] hover:text-[var(--acc)]"
|
|
59
110
|
>
|
|
60
|
-
<RefreshCw className="h-4 w-4" />
|
|
111
|
+
<RefreshCw className="h-4 w-4" aria-hidden />
|
|
61
112
|
Refresh
|
|
62
113
|
</button>
|
|
63
114
|
</div>
|
|
64
115
|
|
|
65
|
-
{templates.length
|
|
66
|
-
<div className="
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
116
|
+
{!loading && templates.length > 0 && (
|
|
117
|
+
<div className="mb-4 flex flex-wrap items-center gap-2">
|
|
118
|
+
<div className="max-w-xs min-w-52 flex-1">
|
|
119
|
+
<SearchInput value={search} onChange={setSearch} placeholder="Search templates…" />
|
|
120
|
+
</div>
|
|
121
|
+
<div className="flex gap-1.5" role="group" aria-label="Filter templates by category">
|
|
122
|
+
<FilterPill active={category === 'all'} onClick={() => setCategory('all')}>
|
|
123
|
+
All
|
|
124
|
+
</FilterPill>
|
|
125
|
+
{(Object.keys(CATEGORIES) as CategoryId[]).map((cat) => (
|
|
126
|
+
<FilterPill key={cat} active={category === cat} onClick={() => setCategory(cat)}>
|
|
127
|
+
{CATEGORIES[cat].label}
|
|
128
|
+
</FilterPill>
|
|
129
|
+
))}
|
|
130
|
+
</div>
|
|
131
|
+
<span className="ml-auto text-[12.5px] whitespace-nowrap text-[var(--muted)]">
|
|
132
|
+
{filtered.length} of {templates.length}
|
|
133
|
+
</span>
|
|
79
134
|
</div>
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
</p>
|
|
92
|
-
</div>
|
|
93
|
-
{template.builtIn && (
|
|
94
|
-
<span className="bg-muted text-muted-foreground rounded-full px-2 py-0.5 text-xs">
|
|
95
|
-
Built-in
|
|
96
|
-
</span>
|
|
97
|
-
)}
|
|
98
|
-
</div>
|
|
99
|
-
<div className="text-muted-foreground flex items-center justify-between text-sm">
|
|
100
|
-
<span>{template.category ?? 'content'}</span>
|
|
101
|
-
<span>
|
|
102
|
-
{template.updatedAt ? new Date(template.updatedAt).toLocaleDateString() : ''}
|
|
103
|
-
</span>
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
{loading ? (
|
|
138
|
+
<div
|
|
139
|
+
className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3"
|
|
140
|
+
role="status"
|
|
141
|
+
aria-live="polite"
|
|
142
|
+
aria-label="Loading page templates"
|
|
143
|
+
>
|
|
144
|
+
{Array.from({ length: 6 }, (_, i) => (
|
|
145
|
+
<Skeleton key={i} variant="card" />
|
|
106
146
|
))}
|
|
107
147
|
</div>
|
|
148
|
+
) : templates.length === 0 ? (
|
|
149
|
+
<EmptyState
|
|
150
|
+
icon={<Layers size={24} aria-hidden />}
|
|
151
|
+
title="No page templates yet"
|
|
152
|
+
description="Built-in templates are seeded by the CMS when the templates API is available."
|
|
153
|
+
actionLabel="View Saved Sections"
|
|
154
|
+
onAction={() => onNavigate?.('/saved-sections')}
|
|
155
|
+
/>
|
|
156
|
+
) : filtered.length === 0 ? (
|
|
157
|
+
<p className="py-12 text-center text-sm text-[var(--muted)]">
|
|
158
|
+
No templates match your search.
|
|
159
|
+
</p>
|
|
160
|
+
) : (
|
|
161
|
+
<ul
|
|
162
|
+
className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3"
|
|
163
|
+
aria-label="Page templates"
|
|
164
|
+
>
|
|
165
|
+
{filtered.map((template) => (
|
|
166
|
+
<TemplateCard
|
|
167
|
+
key={template.id}
|
|
168
|
+
template={template}
|
|
169
|
+
onEdit={() => setEditTarget(template)}
|
|
170
|
+
onDelete={() => setDeleteTarget(template)}
|
|
171
|
+
/>
|
|
172
|
+
))}
|
|
173
|
+
</ul>
|
|
108
174
|
)}
|
|
175
|
+
|
|
176
|
+
<TemplateEditModal
|
|
177
|
+
template={editTarget}
|
|
178
|
+
onClose={() => setEditTarget(null)}
|
|
179
|
+
onSaved={() => {
|
|
180
|
+
setEditTarget(null)
|
|
181
|
+
refetch()
|
|
182
|
+
}}
|
|
183
|
+
/>
|
|
184
|
+
|
|
185
|
+
<ConfirmDialog
|
|
186
|
+
open={deleteTarget !== null}
|
|
187
|
+
onClose={() => setDeleteTarget(null)}
|
|
188
|
+
onConfirm={() => void confirmDelete()}
|
|
189
|
+
title="Delete template?"
|
|
190
|
+
description={`"${deleteTarget?.name ?? 'This template'}" will be permanently removed. Pages already created from it are not affected.`}
|
|
191
|
+
confirmLabel="Delete"
|
|
192
|
+
destructive
|
|
193
|
+
/>
|
|
109
194
|
</div>
|
|
110
195
|
)
|
|
111
196
|
}
|
|
197
|
+
|
|
198
|
+
function TemplateCard({
|
|
199
|
+
template,
|
|
200
|
+
onEdit,
|
|
201
|
+
onDelete,
|
|
202
|
+
}: {
|
|
203
|
+
template: PageTemplate
|
|
204
|
+
onEdit: () => void
|
|
205
|
+
onDelete: () => void
|
|
206
|
+
}) {
|
|
207
|
+
const cat = CATEGORIES[categoryOf(template)]
|
|
208
|
+
const sections = sectionCountOf(template)
|
|
209
|
+
const name = template.name ?? 'Untitled template'
|
|
210
|
+
const updated = template.updatedAt ? new Date(template.updatedAt).toLocaleDateString() : null
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<li className="group relative flex flex-col overflow-hidden rounded-[var(--r)] border border-[var(--bdr)] bg-[var(--card)] shadow-[var(--shd)] transition-[border-color,box-shadow,transform] duration-[var(--motion-fast)] focus-within:border-[var(--acc-m)] focus-within:shadow-[var(--shd2)] hover:-translate-y-0.5 hover:border-[var(--acc-m)] hover:shadow-[var(--shd2)] motion-reduce:hover:translate-y-0">
|
|
214
|
+
<TemplatePreview template={template} sections={sections} />
|
|
215
|
+
|
|
216
|
+
<div className="flex flex-1 flex-col p-4 pb-0">
|
|
217
|
+
<div className="flex items-start justify-between gap-3">
|
|
218
|
+
<h2 className="text-[15px] leading-snug font-medium text-[var(--txt)]">{name}</h2>
|
|
219
|
+
{template.builtIn && (
|
|
220
|
+
<span className="shrink-0 rounded-full border border-[var(--bdr)] bg-[var(--bg)] px-2 py-0.5 text-[10.5px] font-medium whitespace-nowrap text-[var(--sub)]">
|
|
221
|
+
Built-in
|
|
222
|
+
</span>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
<p className="mt-2 min-h-10 text-[12.5px] leading-relaxed text-[var(--sub)]">
|
|
226
|
+
{template.description ?? 'No description provided.'}
|
|
227
|
+
</p>
|
|
228
|
+
|
|
229
|
+
<div className="relative mt-3 flex h-[46px] items-center justify-between border-t border-[var(--bdr)] py-2.5">
|
|
230
|
+
<span
|
|
231
|
+
className={`inline-flex items-center gap-1.5 text-[11.5px] font-medium ${cat.text}`}
|
|
232
|
+
>
|
|
233
|
+
<span aria-hidden className={`h-[7px] w-[7px] shrink-0 rounded-full ${cat.dot}`} />
|
|
234
|
+
{cat.label}
|
|
235
|
+
</span>
|
|
236
|
+
<span className="text-[11.5px] text-[var(--muted)] transition-opacity duration-[var(--motion-fast)] group-focus-within:opacity-0 group-hover:opacity-0">
|
|
237
|
+
{sections} section{sections === 1 ? '' : 's'}
|
|
238
|
+
{updated ? ` · ${updated}` : ''}
|
|
239
|
+
</span>
|
|
240
|
+
{!template.builtIn && (
|
|
241
|
+
<RowActions className="absolute top-1/2 right-0 -translate-y-1/2">
|
|
242
|
+
<RowActionButton aria-label={`Edit ${name}`} onClick={onEdit}>
|
|
243
|
+
<Pencil size={14} aria-hidden />
|
|
244
|
+
</RowActionButton>
|
|
245
|
+
<RowActionButton aria-label={`Delete ${name}`} danger onClick={onDelete}>
|
|
246
|
+
<Trash2 size={14} aria-hidden />
|
|
247
|
+
</RowActionButton>
|
|
248
|
+
</RowActions>
|
|
249
|
+
)}
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
</li>
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Card preview area. Uses the stored thumbnail when one exists; otherwise
|
|
258
|
+
* renders an honest wireframe placeholder built from the template's section
|
|
259
|
+
* count (a hero band plus one bar per section) — no fake screenshots.
|
|
260
|
+
*/
|
|
261
|
+
function TemplatePreview({ template, sections }: { template: PageTemplate; sections: number }) {
|
|
262
|
+
if (template.thumbnail) {
|
|
263
|
+
return (
|
|
264
|
+
<div className="h-28 shrink-0 overflow-hidden border-b border-[var(--bdr)] bg-[var(--bg)]">
|
|
265
|
+
<img
|
|
266
|
+
src={template.thumbnail}
|
|
267
|
+
alt={`Preview of ${template.name ?? 'template'}`}
|
|
268
|
+
className="h-full w-full object-cover"
|
|
269
|
+
/>
|
|
270
|
+
</div>
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const bars = Math.max(0, Math.min(sections - 1, 3))
|
|
275
|
+
return (
|
|
276
|
+
<div
|
|
277
|
+
aria-hidden
|
|
278
|
+
className="flex h-28 shrink-0 flex-col gap-1.5 border-b border-[var(--bdr)] bg-[var(--bg)] p-3"
|
|
279
|
+
>
|
|
280
|
+
<div className="h-9 rounded-[5px] bg-[var(--acc-l)]" />
|
|
281
|
+
{Array.from({ length: bars }, (_, i) => (
|
|
282
|
+
<div
|
|
283
|
+
key={i}
|
|
284
|
+
className="h-3.5 rounded-[4px] bg-[var(--bdr)]"
|
|
285
|
+
style={{ width: `${88 - i * 14}%` }}
|
|
286
|
+
/>
|
|
287
|
+
))}
|
|
288
|
+
</div>
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function TemplateEditModal({
|
|
293
|
+
template,
|
|
294
|
+
onClose,
|
|
295
|
+
onSaved,
|
|
296
|
+
}: {
|
|
297
|
+
template: PageTemplate | null
|
|
298
|
+
onClose: () => void
|
|
299
|
+
onSaved: () => void
|
|
300
|
+
}) {
|
|
301
|
+
// Keyed remount seeds the form state from the selected template each time
|
|
302
|
+
// the modal opens, without effect-based state syncing.
|
|
303
|
+
if (!template) return null
|
|
304
|
+
return (
|
|
305
|
+
<TemplateEditForm key={template.id} template={template} onClose={onClose} onSaved={onSaved} />
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function TemplateEditForm({
|
|
310
|
+
template,
|
|
311
|
+
onClose,
|
|
312
|
+
onSaved,
|
|
313
|
+
}: {
|
|
314
|
+
template: PageTemplate
|
|
315
|
+
onClose: () => void
|
|
316
|
+
onSaved: () => void
|
|
317
|
+
}) {
|
|
318
|
+
const [name, setName] = useState(template.name ?? '')
|
|
319
|
+
const [description, setDescription] = useState(template.description ?? '')
|
|
320
|
+
const [category, setCategory] = useState<CategoryId>(categoryOf(template))
|
|
321
|
+
const [saving, setSaving] = useState(false)
|
|
322
|
+
|
|
323
|
+
async function handleSave() {
|
|
324
|
+
if (!name.trim()) {
|
|
325
|
+
toast.error('Template name is required')
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
setSaving(true)
|
|
329
|
+
const res = await cmsApi(`/page-templates/${template.id}`, {
|
|
330
|
+
method: 'PUT',
|
|
331
|
+
body: JSON.stringify({
|
|
332
|
+
name: name.trim(),
|
|
333
|
+
description: description.trim() || null,
|
|
334
|
+
category,
|
|
335
|
+
}),
|
|
336
|
+
})
|
|
337
|
+
setSaving(false)
|
|
338
|
+
if (res.error) {
|
|
339
|
+
toast.error(res.error)
|
|
340
|
+
} else {
|
|
341
|
+
toast.success('Template updated')
|
|
342
|
+
onSaved()
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
<Modal
|
|
348
|
+
open
|
|
349
|
+
onClose={onClose}
|
|
350
|
+
title="Edit template"
|
|
351
|
+
actions={
|
|
352
|
+
<>
|
|
353
|
+
<Button variant="ghost" onClick={onClose} disabled={saving}>
|
|
354
|
+
Cancel
|
|
355
|
+
</Button>
|
|
356
|
+
<Button variant="primary" onClick={() => void handleSave()} loading={saving}>
|
|
357
|
+
Save changes
|
|
358
|
+
</Button>
|
|
359
|
+
</>
|
|
360
|
+
}
|
|
361
|
+
>
|
|
362
|
+
<div className="flex flex-col gap-4">
|
|
363
|
+
<label className="flex flex-col gap-1.5">
|
|
364
|
+
<span className="text-sm font-medium text-[var(--txt)]">Name</span>
|
|
365
|
+
<Input
|
|
366
|
+
value={name}
|
|
367
|
+
onChange={(e) => setName(e.target.value)}
|
|
368
|
+
placeholder="Template name"
|
|
369
|
+
/>
|
|
370
|
+
</label>
|
|
371
|
+
<label className="flex flex-col gap-1.5">
|
|
372
|
+
<span className="text-sm font-medium text-[var(--txt)]">Description</span>
|
|
373
|
+
<textarea
|
|
374
|
+
value={description}
|
|
375
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
376
|
+
rows={3}
|
|
377
|
+
placeholder="What this template is for…"
|
|
378
|
+
className="w-full resize-y rounded-[var(--r-control)] border border-[var(--bdr)] bg-[var(--input-background)] px-3 py-2 text-sm text-[var(--txt)] placeholder:text-[var(--muted)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
|
|
379
|
+
/>
|
|
380
|
+
</label>
|
|
381
|
+
<label className="flex flex-col gap-1.5">
|
|
382
|
+
<span className="text-sm font-medium text-[var(--txt)]">Category</span>
|
|
383
|
+
<Select value={category} onChange={(e) => setCategory(e.target.value as CategoryId)}>
|
|
384
|
+
{(Object.keys(CATEGORIES) as CategoryId[]).map((cat) => (
|
|
385
|
+
<option key={cat} value={cat}>
|
|
386
|
+
{CATEGORIES[cat].label}
|
|
387
|
+
</option>
|
|
388
|
+
))}
|
|
389
|
+
</Select>
|
|
390
|
+
</label>
|
|
391
|
+
</div>
|
|
392
|
+
</Modal>
|
|
393
|
+
)
|
|
394
|
+
}
|
|
@@ -48,6 +48,12 @@ import { resolveDefaultSitePreview } from '../../lib/editor-preview.js'
|
|
|
48
48
|
interface PageSectionEditorProps {
|
|
49
49
|
/** undefined when creating a new page (`/pages/new`) */
|
|
50
50
|
documentId?: string
|
|
51
|
+
/**
|
|
52
|
+
* Parent page to preselect on a new page — set when arriving via
|
|
53
|
+
* "Add sub-page" in the Pages tree view (`/pages/new?parent=<id>`).
|
|
54
|
+
* Ignored when `documentId` is present.
|
|
55
|
+
*/
|
|
56
|
+
initialParentId?: string | null
|
|
51
57
|
config?: any
|
|
52
58
|
session?: any
|
|
53
59
|
onNavigate: (path: string) => void
|
|
@@ -73,6 +79,7 @@ function deriveCan(session: any): { canEdit: boolean; canPublish: boolean } {
|
|
|
73
79
|
|
|
74
80
|
export function PageSectionEditor({
|
|
75
81
|
documentId,
|
|
82
|
+
initialParentId,
|
|
76
83
|
config,
|
|
77
84
|
session,
|
|
78
85
|
onNavigate,
|
|
@@ -136,7 +143,7 @@ export function PageSectionEditor({
|
|
|
136
143
|
}
|
|
137
144
|
|
|
138
145
|
if (!documentId) {
|
|
139
|
-
setPage(emptyPage())
|
|
146
|
+
setPage(emptyPage(initialParentId ?? null))
|
|
140
147
|
setLoading(false)
|
|
141
148
|
return
|
|
142
149
|
}
|
|
@@ -160,7 +167,7 @@ export function PageSectionEditor({
|
|
|
160
167
|
return () => {
|
|
161
168
|
cancelled = true
|
|
162
169
|
}
|
|
163
|
-
}, [documentId, schemaError])
|
|
170
|
+
}, [documentId, initialParentId, schemaError])
|
|
164
171
|
|
|
165
172
|
// ─── Unsaved-changes guard (browser unload) ──────────────────────────
|
|
166
173
|
useEffect(() => {
|
|
@@ -5,6 +5,7 @@ import { normalizeSlug, validateSlug } from '@actuate-media/cms-core/client'
|
|
|
5
5
|
import { Modal } from '../../components/ui/Modal.js'
|
|
6
6
|
import { Button } from '../../components/ui/Button.js'
|
|
7
7
|
import { SeoCharCount } from '../../components/seo/SeoCharCount.js'
|
|
8
|
+
import { cmsApi } from '../../lib/api.js'
|
|
8
9
|
import type { EditorPage } from '../../lib/page-editor-service.js'
|
|
9
10
|
|
|
10
11
|
const SEO_TITLE_MIN = 30
|
|
@@ -26,7 +27,12 @@ const INPUT =
|
|
|
26
27
|
|
|
27
28
|
const slugify = normalizeSlug
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
interface ParentOption {
|
|
31
|
+
id: string
|
|
32
|
+
title: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Edits page-level fields: title, slug, path, parent page, and SEO. */
|
|
30
36
|
export function PageSettingsModal({
|
|
31
37
|
open,
|
|
32
38
|
page,
|
|
@@ -37,21 +43,52 @@ export function PageSettingsModal({
|
|
|
37
43
|
const [title, setTitle] = useState(page.title)
|
|
38
44
|
const [slug, setSlug] = useState(page.slug)
|
|
39
45
|
const [path, setPath] = useState(page.path)
|
|
46
|
+
const [parentPageId, setParentPageId] = useState(page.parentPageId ?? '')
|
|
40
47
|
const [seoTitle, setSeoTitle] = useState(page.seoTitle)
|
|
41
48
|
const [seoDescription, setSeoDescription] = useState(page.seoDescription)
|
|
42
49
|
// Auto-derive slug/path from title until the user edits slug manually.
|
|
43
50
|
const [slugTouched, setSlugTouched] = useState(Boolean(page.slug))
|
|
51
|
+
const [parentOptions, setParentOptions] = useState<ParentOption[] | null>(null)
|
|
44
52
|
|
|
45
53
|
useEffect(() => {
|
|
46
54
|
if (!open) return
|
|
47
55
|
setTitle(page.title)
|
|
48
56
|
setSlug(page.slug)
|
|
49
57
|
setPath(page.path)
|
|
58
|
+
setParentPageId(page.parentPageId ?? '')
|
|
50
59
|
setSeoTitle(page.seoTitle)
|
|
51
60
|
setSeoDescription(page.seoDescription)
|
|
52
61
|
setSlugTouched(Boolean(page.slug))
|
|
53
62
|
}, [open, page])
|
|
54
63
|
|
|
64
|
+
// Load the parent-page choices lazily the first time the modal opens.
|
|
65
|
+
// The page itself is excluded (a page can't be its own parent); deeper
|
|
66
|
+
// cycle protection lives in the tree builder, which breaks bad chains.
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!open || parentOptions !== null) return
|
|
69
|
+
let cancelled = false
|
|
70
|
+
cmsApi<{ docs: Array<{ id: string; title: string | null }> }>(
|
|
71
|
+
'/collections/pages?pageSize=500&sort=updatedAt&order=desc',
|
|
72
|
+
)
|
|
73
|
+
.then((res) => {
|
|
74
|
+
if (cancelled) return
|
|
75
|
+
const docs = res.data?.docs ?? []
|
|
76
|
+
const options = docs
|
|
77
|
+
.filter((d) => d.id !== page.id)
|
|
78
|
+
.map((d) => ({ id: d.id, title: d.title ?? '(untitled)' }))
|
|
79
|
+
.sort((a, b) => a.title.localeCompare(b.title))
|
|
80
|
+
setParentOptions(options)
|
|
81
|
+
})
|
|
82
|
+
.catch(() => {
|
|
83
|
+
// Leave the picker in its loading state note; the rest of the
|
|
84
|
+
// settings form stays fully usable.
|
|
85
|
+
if (!cancelled) setParentOptions([])
|
|
86
|
+
})
|
|
87
|
+
return () => {
|
|
88
|
+
cancelled = true
|
|
89
|
+
}
|
|
90
|
+
}, [open, parentOptions, page.id])
|
|
91
|
+
|
|
55
92
|
function handleTitle(next: string) {
|
|
56
93
|
setTitle(next)
|
|
57
94
|
if (!slugTouched) {
|
|
@@ -65,7 +102,14 @@ export function PageSettingsModal({
|
|
|
65
102
|
|
|
66
103
|
function handleSave() {
|
|
67
104
|
if (slugError) return
|
|
68
|
-
onSave({
|
|
105
|
+
onSave({
|
|
106
|
+
title,
|
|
107
|
+
slug,
|
|
108
|
+
path: path || (slug ? `/${slug}` : '/'),
|
|
109
|
+
parentPageId: parentPageId || null,
|
|
110
|
+
seoTitle,
|
|
111
|
+
seoDescription,
|
|
112
|
+
})
|
|
69
113
|
onClose()
|
|
70
114
|
}
|
|
71
115
|
|
|
@@ -135,6 +179,35 @@ export function PageSettingsModal({
|
|
|
135
179
|
/>
|
|
136
180
|
</div>
|
|
137
181
|
</div>
|
|
182
|
+
<div>
|
|
183
|
+
<label className={LABEL} htmlFor="ps-parent">
|
|
184
|
+
Parent page
|
|
185
|
+
</label>
|
|
186
|
+
<select
|
|
187
|
+
id="ps-parent"
|
|
188
|
+
className={INPUT}
|
|
189
|
+
value={parentPageId}
|
|
190
|
+
disabled={!canEdit || parentOptions === null}
|
|
191
|
+
onChange={(e) => setParentPageId(e.target.value)}
|
|
192
|
+
>
|
|
193
|
+
<option value="">None — top level</option>
|
|
194
|
+
{/* Keep a preselected parent (e.g. from "Add sub-page") valid
|
|
195
|
+
while the options load or if it's missing from the list. */}
|
|
196
|
+
{parentPageId && !(parentOptions ?? []).some((p) => p.id === parentPageId) && (
|
|
197
|
+
<option value={parentPageId}>
|
|
198
|
+
{parentOptions === null ? 'Loading…' : 'Current parent'}
|
|
199
|
+
</option>
|
|
200
|
+
)}
|
|
201
|
+
{(parentOptions ?? []).map((p) => (
|
|
202
|
+
<option key={p.id} value={p.id}>
|
|
203
|
+
{p.title}
|
|
204
|
+
</option>
|
|
205
|
+
))}
|
|
206
|
+
</select>
|
|
207
|
+
<p className="text-muted-foreground mt-1 text-xs">
|
|
208
|
+
Nests this page under a parent in the Pages tree view.
|
|
209
|
+
</p>
|
|
210
|
+
</div>
|
|
138
211
|
<div>
|
|
139
212
|
<div className="mb-1 flex items-center justify-between">
|
|
140
213
|
<label className={`${LABEL} mb-0`} htmlFor="ps-seo-title">
|