@actuate-media/cms-admin 0.69.0 → 0.70.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 +19 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +4 -1
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/lib/pages-tree.test.js +40 -1
- package/dist/__tests__/lib/pages-tree.test.js.map +1 -1
- package/dist/__tests__/views/form-entries.render.test.js +14 -4
- package/dist/__tests__/views/form-entries.render.test.js.map +1 -1
- package/dist/__tests__/views/form-submissions.render.test.js +14 -4
- package/dist/__tests__/views/form-submissions.render.test.js.map +1 -1
- package/dist/__tests__/views/media-seo-audit.render.test.js +26 -0
- package/dist/__tests__/views/media-seo-audit.render.test.js.map +1 -1
- package/dist/__tests__/views/page-builder-template-seed.render.test.d.ts +2 -0
- package/dist/__tests__/views/page-builder-template-seed.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/page-builder-template-seed.render.test.js +48 -0
- package/dist/__tests__/views/page-builder-template-seed.render.test.js.map +1 -0
- package/dist/__tests__/views/page-templates.render.test.js +21 -0
- package/dist/__tests__/views/page-templates.render.test.js.map +1 -1
- package/dist/__tests__/views/redirects-pane.render.test.d.ts +2 -0
- package/dist/__tests__/views/redirects-pane.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/redirects-pane.render.test.js +121 -0
- package/dist/__tests__/views/redirects-pane.render.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/components/SEOPanel.js +3 -3
- package/dist/components/SEOPanel.js.map +1 -1
- package/dist/components/TipTapEditor.d.ts.map +1 -1
- package/dist/components/TipTapEditor.js +11 -9
- package/dist/components/TipTapEditor.js.map +1 -1
- package/dist/components/collections/EntryDetailPane.js +1 -1
- package/dist/components/collections/EntryDetailPane.js.map +1 -1
- package/dist/components/forms/EntriesTable.d.ts +3 -1
- package/dist/components/forms/EntriesTable.d.ts.map +1 -1
- package/dist/components/forms/EntriesTable.js +5 -2
- package/dist/components/forms/EntriesTable.js.map +1 -1
- package/dist/components/forms/FormEntryDetailPane.d.ts +10 -0
- package/dist/components/forms/FormEntryDetailPane.d.ts.map +1 -0
- package/dist/components/forms/FormEntryDetailPane.js +137 -0
- package/dist/components/forms/FormEntryDetailPane.js.map +1 -0
- package/dist/components/forms/FormSchemaPane.js +1 -1
- package/dist/components/forms/FormSchemaPane.js.map +1 -1
- package/dist/components/script-tags/ScriptTagEditorPane.js +1 -1
- package/dist/components/script-tags/ScriptTagEditorPane.js.map +1 -1
- package/dist/components/seo/RedirectEditorPane.d.ts +12 -0
- package/dist/components/seo/RedirectEditorPane.d.ts.map +1 -0
- package/dist/components/seo/RedirectEditorPane.js +89 -0
- package/dist/components/seo/RedirectEditorPane.js.map +1 -0
- package/dist/components/seo/SeoEditorPane.js +1 -1
- package/dist/components/seo/SeoEditorPane.js.map +1 -1
- package/dist/components/ui/DataTable.js +2 -2
- package/dist/components/ui/DataTable.js.map +1 -1
- package/dist/components/ui/Skeleton.js +3 -3
- package/dist/components/ui/Skeleton.js.map +1 -1
- package/dist/components/ui/Toggle.js +1 -1
- package/dist/components/ui/Toggle.js.map +1 -1
- package/dist/fields/BlockBuilderField.js +1 -1
- package/dist/fields/BlockBuilderField.js.map +1 -1
- package/dist/fields/MediaField.js +1 -1
- package/dist/fields/MediaField.js.map +1 -1
- package/dist/fields/PropInput.js +1 -1
- package/dist/fields/PropInput.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/layout/Header.js +1 -1
- package/dist/layout/Header.js.map +1 -1
- package/dist/lib/pages-service.d.ts +7 -0
- package/dist/lib/pages-service.d.ts.map +1 -1
- package/dist/lib/pages-service.js +11 -0
- package/dist/lib/pages-service.js.map +1 -1
- package/dist/lib/pages-tree.d.ts +14 -0
- package/dist/lib/pages-tree.d.ts.map +1 -1
- package/dist/lib/pages-tree.js +51 -0
- package/dist/lib/pages-tree.js.map +1 -1
- package/dist/views/DocumentEdit.js +1 -1
- package/dist/views/DocumentEdit.js.map +1 -1
- package/dist/views/FormEditor.js +5 -7
- package/dist/views/FormEditor.js.map +1 -1
- package/dist/views/FormEntries.d.ts.map +1 -1
- package/dist/views/FormEntries.js +18 -14
- package/dist/views/FormEntries.js.map +1 -1
- package/dist/views/FormSubmissions.d.ts.map +1 -1
- package/dist/views/FormSubmissions.js +18 -15
- package/dist/views/FormSubmissions.js.map +1 -1
- package/dist/views/Forms.js +5 -5
- package/dist/views/Forms.js.map +1 -1
- package/dist/views/HiddenLoginNotFound.d.ts +6 -0
- package/dist/views/HiddenLoginNotFound.d.ts.map +1 -1
- package/dist/views/HiddenLoginNotFound.js +6 -0
- package/dist/views/HiddenLoginNotFound.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +3 -2
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/MediaSeoAudit.d.ts +4 -1
- package/dist/views/MediaSeoAudit.d.ts.map +1 -1
- package/dist/views/MediaSeoAudit.js +12 -2
- package/dist/views/MediaSeoAudit.js.map +1 -1
- package/dist/views/PageEditor.d.ts +4 -0
- package/dist/views/PageEditor.d.ts.map +1 -1
- package/dist/views/PageEditor.js +4 -0
- package/dist/views/PageEditor.js.map +1 -1
- package/dist/views/Pages/PagesListView.d.ts.map +1 -1
- package/dist/views/Pages/PagesListView.js +52 -17
- package/dist/views/Pages/PagesListView.js.map +1 -1
- package/dist/views/Pages/PagesTreeTable.d.ts +11 -1
- package/dist/views/Pages/PagesTreeTable.d.ts.map +1 -1
- package/dist/views/Pages/PagesTreeTable.js +40 -9
- package/dist/views/Pages/PagesTreeTable.js.map +1 -1
- package/dist/views/Pages.d.ts +4 -0
- package/dist/views/Pages.d.ts.map +1 -1
- package/dist/views/Pages.js +4 -0
- package/dist/views/Pages.js.map +1 -1
- package/dist/views/Redirects.d.ts +4 -0
- package/dist/views/Redirects.d.ts.map +1 -1
- package/dist/views/Redirects.js +4 -0
- package/dist/views/Redirects.js.map +1 -1
- package/dist/views/ScriptTags.d.ts.map +1 -1
- package/dist/views/ScriptTags.js +4 -3
- package/dist/views/ScriptTags.js.map +1 -1
- package/dist/views/page-builder/PageBuilder.d.ts +3 -1
- package/dist/views/page-builder/PageBuilder.d.ts.map +1 -1
- package/dist/views/page-builder/PageBuilder.js +24 -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 +30 -4
- package/dist/views/page-builder/PageTemplates.js.map +1 -1
- package/dist/views/seo/ContentTab.js +6 -6
- package/dist/views/seo/ContentTab.js.map +1 -1
- package/dist/views/seo/RedirectsTab.d.ts.map +1 -1
- package/dist/views/seo/RedirectsTab.js +23 -8
- package/dist/views/seo/RedirectsTab.js.map +1 -1
- package/package.json +1 -1
- package/src/AdminRoot.tsx +11 -1
- package/src/__tests__/lib/pages-tree.test.ts +49 -0
- package/src/__tests__/views/form-entries.render.test.tsx +16 -4
- package/src/__tests__/views/form-submissions.render.test.tsx +16 -4
- package/src/__tests__/views/media-seo-audit.render.test.tsx +34 -0
- package/src/__tests__/views/page-builder-template-seed.render.test.tsx +59 -0
- package/src/__tests__/views/page-templates.render.test.tsx +40 -0
- package/src/__tests__/views/redirects-pane.render.test.tsx +156 -0
- package/src/components/SEOPanel.tsx +9 -9
- package/src/components/TipTapEditor.tsx +11 -9
- package/src/components/collections/EntryDetailPane.tsx +1 -1
- package/src/components/forms/EntriesTable.tsx +12 -3
- package/src/components/forms/{EntryDetailDrawer.tsx → FormEntryDetailPane.tsx} +112 -102
- package/src/components/forms/FormSchemaPane.tsx +1 -1
- package/src/components/script-tags/ScriptTagEditorPane.tsx +1 -1
- package/src/components/seo/RedirectEditorPane.tsx +271 -0
- package/src/components/seo/SeoEditorPane.tsx +1 -1
- package/src/components/ui/DataTable.tsx +3 -3
- package/src/components/ui/Skeleton.tsx +8 -8
- package/src/components/ui/Toggle.tsx +1 -1
- package/src/fields/BlockBuilderField.tsx +1 -1
- package/src/fields/MediaField.tsx +1 -1
- package/src/fields/PropInput.tsx +1 -1
- package/src/index.ts +3 -0
- package/src/layout/Header.tsx +2 -2
- package/src/lib/pages-service.ts +15 -0
- package/src/lib/pages-tree.ts +54 -0
- package/src/styles/actuate-tokens.css +4 -0
- package/src/styles/theme.css +9 -3
- package/src/views/DocumentEdit.tsx +4 -4
- package/src/views/FormEditor.tsx +10 -10
- package/src/views/FormEntries.tsx +81 -66
- package/src/views/FormSubmissions.tsx +78 -64
- package/src/views/Forms.tsx +16 -16
- package/src/views/HiddenLoginNotFound.tsx +6 -0
- package/src/views/MediaBrowser.tsx +14 -10
- package/src/views/MediaSeoAudit.tsx +39 -2
- package/src/views/PageEditor.tsx +4 -0
- package/src/views/Pages/PagesListView.tsx +99 -54
- package/src/views/Pages/PagesTreeTable.tsx +204 -109
- package/src/views/Pages.tsx +4 -0
- package/src/views/Redirects.tsx +4 -0
- package/src/views/ScriptTags.tsx +36 -42
- package/src/views/page-builder/PageBuilder.tsx +30 -2
- package/src/views/page-builder/PageTemplates.tsx +56 -15
- package/src/views/seo/ContentTab.tsx +10 -10
- package/src/views/seo/RedirectsTab.tsx +145 -109
- package/dist/components/forms/EntryDetailDrawer.d.ts +0 -11
- package/dist/components/forms/EntryDetailDrawer.d.ts.map +0 -1
- package/dist/components/forms/EntryDetailDrawer.js +0 -145
- package/dist/components/forms/EntryDetailDrawer.js.map +0 -1
- package/dist/components/seo/RedirectEditorDrawer.d.ts +0 -9
- package/dist/components/seo/RedirectEditorDrawer.d.ts.map +0 -1
- package/dist/components/seo/RedirectEditorDrawer.js +0 -95
- package/dist/components/seo/RedirectEditorDrawer.js.map +0 -1
- package/src/components/seo/RedirectEditorDrawer.tsx +0 -231
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import {
|
|
5
|
+
DndContext,
|
|
6
|
+
KeyboardSensor,
|
|
7
|
+
PointerSensor,
|
|
8
|
+
pointerWithin,
|
|
9
|
+
useDraggable,
|
|
10
|
+
useDroppable,
|
|
11
|
+
useSensor,
|
|
12
|
+
useSensors,
|
|
13
|
+
type DragEndEvent,
|
|
14
|
+
type DragStartEvent,
|
|
15
|
+
} from '@dnd-kit/core'
|
|
16
|
+
import { ChevronDown, Copy, GripVertical, PenLine, Plus, Trash2 } from 'lucide-react'
|
|
4
17
|
import type { Page } from '../../lib/pages-service.js'
|
|
5
18
|
import type { PageTreeRow } from '../../lib/pages-tree.js'
|
|
6
19
|
import { PostStatusBadge, SeoScoreIndicator } from '../../components/posts/badges.js'
|
|
20
|
+
import { Button } from '../../components/ui/Button.js'
|
|
7
21
|
import { RowActionButton, RowActions } from '../../components/ui/RowActions.js'
|
|
8
22
|
|
|
9
23
|
/**
|
|
@@ -16,6 +30,12 @@ import { RowActionButton, RowActions } from '../../components/ui/RowActions.js'
|
|
|
16
30
|
* renders at depth 0 and expand state is ignored — the caller passes
|
|
17
31
|
* pre-flattened rows, this component just skips the leaf spacers' tree
|
|
18
32
|
* semantics.
|
|
33
|
+
*
|
|
34
|
+
* Drag-reparent: rows can be dragged (grip handle) and dropped onto
|
|
35
|
+
* another row to become its child. There is intentionally no "drop to
|
|
36
|
+
* make top-level" zone — with `parentPageId` cleared, the tree falls back
|
|
37
|
+
* to path-prefix derivation, which would silently re-nest most pages, so
|
|
38
|
+
* un-nesting stays an editor operation. Disabled while flattened.
|
|
19
39
|
*/
|
|
20
40
|
|
|
21
41
|
interface PagesTreeTableProps {
|
|
@@ -33,6 +53,10 @@ interface PagesTreeTableProps {
|
|
|
33
53
|
onAddSubPage: (page: Page) => void
|
|
34
54
|
onDuplicate: (page: Page) => void
|
|
35
55
|
onDelete: (page: Page) => void
|
|
56
|
+
/** Drop handler: `source` becomes a child of `targetParent`. */
|
|
57
|
+
onReparent: (sourceId: string, targetParentId: string) => void
|
|
58
|
+
/** Validity gate for a drop (cycle/self/no-op checks live in the caller). */
|
|
59
|
+
canReparent: (sourceId: string, targetParentId: string) => boolean
|
|
36
60
|
onRetry: () => void
|
|
37
61
|
onClearFilters: () => void
|
|
38
62
|
onNewPage: () => void
|
|
@@ -55,122 +79,157 @@ export function PagesTreeTable({
|
|
|
55
79
|
onAddSubPage,
|
|
56
80
|
onDuplicate,
|
|
57
81
|
onDelete,
|
|
82
|
+
onReparent,
|
|
83
|
+
canReparent,
|
|
58
84
|
onRetry,
|
|
59
85
|
onClearFilters,
|
|
60
86
|
onNewPage,
|
|
61
87
|
}: PagesTreeTableProps) {
|
|
62
88
|
const allSelected = rows.length > 0 && rows.every((r) => selected.has(r.page.id))
|
|
89
|
+
const [draggingId, setDraggingId] = useState<string | null>(null)
|
|
90
|
+
|
|
91
|
+
// A small activation distance keeps plain clicks (title, checkbox,
|
|
92
|
+
// chevron) from starting a drag.
|
|
93
|
+
const sensors = useSensors(
|
|
94
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
|
|
95
|
+
useSensor(KeyboardSensor),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
const handleDragStart = (event: DragStartEvent) => {
|
|
99
|
+
setDraggingId(String(event.active.id))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const handleDragEnd = (event: DragEndEvent) => {
|
|
103
|
+
setDraggingId(null)
|
|
104
|
+
const sourceId = String(event.active.id)
|
|
105
|
+
const targetId = event.over ? String(event.over.id) : null
|
|
106
|
+
if (!targetId || targetId === sourceId) return
|
|
107
|
+
if (!canReparent(sourceId, targetId)) return
|
|
108
|
+
onReparent(sourceId, targetId)
|
|
109
|
+
}
|
|
63
110
|
|
|
64
111
|
return (
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
className="border-border accent-primary rounded"
|
|
86
|
-
/>
|
|
87
|
-
</th>
|
|
88
|
-
<th scope="col" className={`${TH_CLASS} min-w-[280px]`}>
|
|
89
|
-
Title
|
|
90
|
-
</th>
|
|
91
|
-
<th scope="col" className={TH_CLASS}>
|
|
92
|
-
Template
|
|
93
|
-
</th>
|
|
94
|
-
<th scope="col" className={TH_CLASS}>
|
|
95
|
-
Author
|
|
96
|
-
</th>
|
|
97
|
-
<th scope="col" className={TH_CLASS}>
|
|
98
|
-
Status
|
|
99
|
-
</th>
|
|
100
|
-
<th scope="col" className={TH_CLASS}>
|
|
101
|
-
Last Modified
|
|
102
|
-
</th>
|
|
103
|
-
<th scope="col" className={TH_CLASS}>
|
|
104
|
-
SEO
|
|
105
|
-
</th>
|
|
106
|
-
<th scope="col" className="px-3 py-2.5 text-right">
|
|
107
|
-
<span className="sr-only">Actions</span>
|
|
108
|
-
</th>
|
|
109
|
-
</tr>
|
|
110
|
-
</thead>
|
|
111
|
-
<tbody>
|
|
112
|
-
{loading && rows.length === 0 && <TreeLoadingRows />}
|
|
113
|
-
{!loading && error && (
|
|
114
|
-
<tr>
|
|
115
|
-
<td colSpan={8} className="px-3 py-12 text-center">
|
|
116
|
-
<div className="text-foreground mb-2 text-sm font-medium">{error}</div>
|
|
117
|
-
<button onClick={onRetry} className="text-primary text-sm hover:underline">
|
|
118
|
-
Try again
|
|
119
|
-
</button>
|
|
120
|
-
</td>
|
|
121
|
-
</tr>
|
|
122
|
-
)}
|
|
123
|
-
{!loading && !error && rows.length === 0 && (
|
|
112
|
+
<DndContext
|
|
113
|
+
sensors={sensors}
|
|
114
|
+
collisionDetection={pointerWithin}
|
|
115
|
+
onDragStart={handleDragStart}
|
|
116
|
+
onDragEnd={handleDragEnd}
|
|
117
|
+
onDragCancel={() => setDraggingId(null)}
|
|
118
|
+
>
|
|
119
|
+
<div className="overflow-x-auto">
|
|
120
|
+
<table className="w-full min-w-[800px]" role="table" aria-label="Pages tree">
|
|
121
|
+
<colgroup>
|
|
122
|
+
<col style={{ width: '40px' }} />
|
|
123
|
+
<col />
|
|
124
|
+
<col style={{ width: '130px' }} />
|
|
125
|
+
<col style={{ width: '150px' }} />
|
|
126
|
+
<col style={{ width: '120px' }} />
|
|
127
|
+
<col style={{ width: '115px' }} />
|
|
128
|
+
<col style={{ width: '70px' }} />
|
|
129
|
+
<col style={{ width: '170px' }} />
|
|
130
|
+
</colgroup>
|
|
131
|
+
<thead className="border-b border-[var(--bdr)]">
|
|
124
132
|
<tr>
|
|
125
|
-
<
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
{
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
133
|
+
<th scope="col" className="px-3 py-2.5 text-left">
|
|
134
|
+
<input
|
|
135
|
+
type="checkbox"
|
|
136
|
+
aria-label="Select all visible pages"
|
|
137
|
+
checked={allSelected}
|
|
138
|
+
onChange={(e) => onToggleAll(e.target.checked)}
|
|
139
|
+
className="accent-primary rounded border-[var(--bdr)]"
|
|
140
|
+
/>
|
|
141
|
+
</th>
|
|
142
|
+
<th scope="col" className={`${TH_CLASS} min-w-[280px]`}>
|
|
143
|
+
Title
|
|
144
|
+
</th>
|
|
145
|
+
<th scope="col" className={TH_CLASS}>
|
|
146
|
+
Template
|
|
147
|
+
</th>
|
|
148
|
+
<th scope="col" className={TH_CLASS}>
|
|
149
|
+
Author
|
|
150
|
+
</th>
|
|
151
|
+
<th scope="col" className={TH_CLASS}>
|
|
152
|
+
Status
|
|
153
|
+
</th>
|
|
154
|
+
<th scope="col" className={TH_CLASS}>
|
|
155
|
+
Last Modified
|
|
156
|
+
</th>
|
|
157
|
+
<th scope="col" className={TH_CLASS}>
|
|
158
|
+
SEO
|
|
159
|
+
</th>
|
|
160
|
+
<th scope="col" className="px-3 py-2.5 text-right">
|
|
161
|
+
<span className="sr-only">Actions</span>
|
|
162
|
+
</th>
|
|
153
163
|
</tr>
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
<
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
164
|
+
</thead>
|
|
165
|
+
<tbody>
|
|
166
|
+
{loading && rows.length === 0 && <TreeLoadingRows />}
|
|
167
|
+
{!loading && error && (
|
|
168
|
+
<tr>
|
|
169
|
+
<td colSpan={8} className="px-3 py-12 text-center">
|
|
170
|
+
<div className="mb-2 text-sm font-medium text-[var(--txt)]">{error}</div>
|
|
171
|
+
<Button variant="link" size="md" onClick={onRetry}>
|
|
172
|
+
Try again
|
|
173
|
+
</Button>
|
|
174
|
+
</td>
|
|
175
|
+
</tr>
|
|
176
|
+
)}
|
|
177
|
+
{!loading && !error && rows.length === 0 && (
|
|
178
|
+
<tr>
|
|
179
|
+
<td colSpan={8} className="px-3 py-16 text-center">
|
|
180
|
+
<div className="mb-2 text-sm font-medium text-[var(--txt)]">
|
|
181
|
+
{flattened ? 'No pages match your filters' : 'No pages yet'}
|
|
182
|
+
</div>
|
|
183
|
+
<div className="mb-4 text-sm text-[var(--sub)]">
|
|
184
|
+
{flattened
|
|
185
|
+
? 'Try clearing the search or status filter above.'
|
|
186
|
+
: 'Create your first page to get started.'}
|
|
187
|
+
</div>
|
|
188
|
+
{flattened ? (
|
|
189
|
+
<Button type="button" variant="link" size="md" onClick={onClearFilters}>
|
|
190
|
+
Clear filters
|
|
191
|
+
</Button>
|
|
192
|
+
) : (
|
|
193
|
+
<Button
|
|
194
|
+
type="button"
|
|
195
|
+
variant="primary"
|
|
196
|
+
size="sm"
|
|
197
|
+
className="shadow-sm"
|
|
198
|
+
onClick={onNewPage}
|
|
199
|
+
leftIcon={<Plus size={16} aria-hidden />}
|
|
200
|
+
>
|
|
201
|
+
New page
|
|
202
|
+
</Button>
|
|
203
|
+
)}
|
|
204
|
+
</td>
|
|
205
|
+
</tr>
|
|
206
|
+
)}
|
|
207
|
+
{!loading &&
|
|
208
|
+
!error &&
|
|
209
|
+
rows.map((row) => (
|
|
210
|
+
<TreeRow
|
|
211
|
+
key={row.page.id}
|
|
212
|
+
row={row}
|
|
213
|
+
isSelected={selected.has(row.page.id)}
|
|
214
|
+
isExpanded={expanded.has(row.page.id)}
|
|
215
|
+
draggable={!flattened}
|
|
216
|
+
isDropTarget={
|
|
217
|
+
draggingId !== null &&
|
|
218
|
+
draggingId !== row.page.id &&
|
|
219
|
+
canReparent(draggingId, row.page.id)
|
|
220
|
+
}
|
|
221
|
+
onToggleExpand={() => onToggleExpand(row.page.id)}
|
|
222
|
+
onToggleSelect={() => onToggleSelect(row.page.id)}
|
|
223
|
+
onOpen={() => onOpen(row.page)}
|
|
224
|
+
onAddSubPage={() => onAddSubPage(row.page)}
|
|
225
|
+
onDuplicate={() => onDuplicate(row.page)}
|
|
226
|
+
onDelete={() => onDelete(row.page)}
|
|
227
|
+
/>
|
|
228
|
+
))}
|
|
229
|
+
</tbody>
|
|
230
|
+
</table>
|
|
231
|
+
</div>
|
|
232
|
+
</DndContext>
|
|
174
233
|
)
|
|
175
234
|
}
|
|
176
235
|
|
|
@@ -178,6 +237,8 @@ function TreeRow({
|
|
|
178
237
|
row,
|
|
179
238
|
isSelected,
|
|
180
239
|
isExpanded,
|
|
240
|
+
draggable,
|
|
241
|
+
isDropTarget,
|
|
181
242
|
onToggleExpand,
|
|
182
243
|
onToggleSelect,
|
|
183
244
|
onOpen,
|
|
@@ -188,6 +249,9 @@ function TreeRow({
|
|
|
188
249
|
row: PageTreeRow
|
|
189
250
|
isSelected: boolean
|
|
190
251
|
isExpanded: boolean
|
|
252
|
+
draggable: boolean
|
|
253
|
+
/** true while another row is being dragged and this row is a valid parent */
|
|
254
|
+
isDropTarget: boolean
|
|
191
255
|
onToggleExpand: () => void
|
|
192
256
|
onToggleSelect: () => void
|
|
193
257
|
onOpen: () => void
|
|
@@ -196,10 +260,28 @@ function TreeRow({
|
|
|
196
260
|
onDelete: () => void
|
|
197
261
|
}) {
|
|
198
262
|
const { page, depth, hasChildren } = row
|
|
263
|
+
const {
|
|
264
|
+
attributes,
|
|
265
|
+
listeners,
|
|
266
|
+
setNodeRef: setDragRef,
|
|
267
|
+
isDragging,
|
|
268
|
+
} = useDraggable({ id: page.id, disabled: !draggable })
|
|
269
|
+
const { setNodeRef: setDropRef, isOver } = useDroppable({
|
|
270
|
+
id: page.id,
|
|
271
|
+
disabled: !draggable || !isDropTarget,
|
|
272
|
+
})
|
|
273
|
+
|
|
199
274
|
return (
|
|
200
275
|
<tr
|
|
276
|
+
ref={setDropRef}
|
|
201
277
|
className={`group h-14 border-b border-[var(--bdr)] transition-colors last:border-b-0 hover:bg-[var(--acc-l)] ${
|
|
202
278
|
isSelected ? 'bg-[var(--acc-l)]' : ''
|
|
279
|
+
} ${isDragging ? 'opacity-40' : ''} ${
|
|
280
|
+
isDropTarget && isOver
|
|
281
|
+
? 'bg-[var(--acc-l)] shadow-[inset_0_0_0_2px_var(--acc)]'
|
|
282
|
+
: isDropTarget
|
|
283
|
+
? 'shadow-[inset_0_0_0_1px_var(--acc-m)]'
|
|
284
|
+
: ''
|
|
203
285
|
}`}
|
|
204
286
|
>
|
|
205
287
|
<td className="px-3 align-middle">
|
|
@@ -208,13 +290,26 @@ function TreeRow({
|
|
|
208
290
|
aria-label={`Select "${page.title}"`}
|
|
209
291
|
checked={isSelected}
|
|
210
292
|
onChange={onToggleSelect}
|
|
211
|
-
className="
|
|
293
|
+
className="accent-primary rounded border-[var(--bdr)]"
|
|
212
294
|
/>
|
|
213
295
|
</td>
|
|
214
296
|
<td className="px-3 align-middle">
|
|
215
297
|
{/* Indentation is data-driven (24px per depth level), so it must be
|
|
216
298
|
an inline style rather than a class. */}
|
|
217
299
|
<div className="flex items-center" style={{ paddingLeft: depth * 24 }}>
|
|
300
|
+
{draggable && (
|
|
301
|
+
<button
|
|
302
|
+
type="button"
|
|
303
|
+
ref={setDragRef}
|
|
304
|
+
{...attributes}
|
|
305
|
+
{...listeners}
|
|
306
|
+
aria-label={`Move "${page.title}" — drag onto another page to nest it there`}
|
|
307
|
+
title="Drag to move under another page"
|
|
308
|
+
className="mr-1 flex h-5 w-5 shrink-0 cursor-grab items-center justify-center rounded-[5px] text-[var(--muted)] opacity-0 transition-opacity duration-[var(--motion-fast)] group-hover:opacity-100 hover:bg-[var(--acc-l)] hover:text-[var(--acc)] focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none active:cursor-grabbing"
|
|
309
|
+
>
|
|
310
|
+
<GripVertical size={13} aria-hidden />
|
|
311
|
+
</button>
|
|
312
|
+
)}
|
|
218
313
|
{hasChildren ? (
|
|
219
314
|
<button
|
|
220
315
|
type="button"
|
package/src/views/Pages.tsx
CHANGED
|
@@ -97,6 +97,10 @@ function buildApiUrl(folderSel: FolderSelection): string {
|
|
|
97
97
|
return `${base}&folderId=${folderSel.folderId}`
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* @deprecated Superseded by `PagesListView` (`views/Pages/PagesListView.tsx`),
|
|
102
|
+
* which `AdminRoot` routes to; will be removed in the next major.
|
|
103
|
+
*/
|
|
100
104
|
export function Pages({ onNavigate }: PagesProps) {
|
|
101
105
|
const [folderSel, setFolderSel] = useState<FolderSelection>({ type: 'smart', smart: 'all' })
|
|
102
106
|
const [sidebarOpen, setSidebarOpen] = useState(true)
|
package/src/views/Redirects.tsx
CHANGED
|
@@ -26,6 +26,10 @@ export interface RedirectsProps {
|
|
|
26
26
|
onNavigate?: (path: string) => void
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* @deprecated Superseded by `RedirectsTab` (`views/seo/RedirectsTab.tsx`)
|
|
31
|
+
* inside the SEO view; will be removed in the next major.
|
|
32
|
+
*/
|
|
29
33
|
export function Redirects({ onNavigate }: RedirectsProps) {
|
|
30
34
|
const { data, loading, error, refetch } = useApiData<any[]>('/redirects')
|
|
31
35
|
const [showAddDialog, setShowAddDialog] = useState(false)
|
package/src/views/ScriptTags.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { useMemo, useState } from 'react'
|
|
|
4
4
|
import { Code2, Plus, Loader2, AlertTriangle } from 'lucide-react'
|
|
5
5
|
import { toast } from 'sonner'
|
|
6
6
|
import { EnabledSwitch } from '../components/EnabledSwitch.js'
|
|
7
|
+
import { Button } from '../components/ui/Button.js'
|
|
7
8
|
import { StatPill } from '../components/ui/StatPill.js'
|
|
8
9
|
import { InspectorPane, SplitPaneLayout, SplitPaneList } from '../components/ui/InspectorPane.js'
|
|
9
10
|
import {
|
|
@@ -139,7 +140,7 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
139
140
|
<span className="text-destructive flex-1 text-sm">{error}</span>
|
|
140
141
|
<button
|
|
141
142
|
onClick={refetch}
|
|
142
|
-
className="border-destructive/40 text-destructive hover:bg-destructive/15 rounded-lg border px-3 py-1 text-sm transition-colors"
|
|
143
|
+
className="border-destructive/40 text-destructive hover:bg-destructive/15 rounded-lg border px-3 py-1 text-sm transition-colors focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
|
|
143
144
|
>
|
|
144
145
|
Retry
|
|
145
146
|
</button>
|
|
@@ -148,8 +149,8 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
148
149
|
|
|
149
150
|
<div className="mb-4 flex items-center justify-between">
|
|
150
151
|
<div>
|
|
151
|
-
<h1 className="
|
|
152
|
-
<p className="text-
|
|
152
|
+
<h1 className="mb-1 text-[var(--txt)]">Script Tags</h1>
|
|
153
|
+
<p className="text-sm text-[var(--sub)]">
|
|
153
154
|
Manage tracking codes, analytics, and custom scripts injected into your site
|
|
154
155
|
</p>
|
|
155
156
|
{tags.length > 0 && (
|
|
@@ -168,59 +169,52 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
168
169
|
</div>
|
|
169
170
|
)}
|
|
170
171
|
</div>
|
|
171
|
-
<
|
|
172
|
+
<Button
|
|
172
173
|
type="button"
|
|
174
|
+
variant="primary"
|
|
175
|
+
size="md"
|
|
173
176
|
onClick={() => setSelection({ mode: 'new' })}
|
|
174
|
-
|
|
177
|
+
leftIcon={<Plus size={16} aria-hidden />}
|
|
175
178
|
>
|
|
176
|
-
<Plus className="h-4 w-4" />
|
|
177
179
|
New Script
|
|
178
|
-
</
|
|
180
|
+
</Button>
|
|
179
181
|
</div>
|
|
180
182
|
</div>
|
|
181
183
|
|
|
182
184
|
<SplitPaneLayout>
|
|
183
185
|
<SplitPaneList className="p-3 pt-0 pr-6 sm:p-4 sm:pt-0 sm:pr-8">
|
|
184
186
|
{tags.length === 0 && !error ? (
|
|
185
|
-
<div className="
|
|
186
|
-
<Code2 className="
|
|
187
|
-
<h3 className="text-
|
|
188
|
-
<p className="
|
|
187
|
+
<div className="rounded-lg border border-[var(--bdr)] bg-[var(--card)] p-12 text-center">
|
|
188
|
+
<Code2 className="mx-auto mb-3 h-10 w-10 text-[var(--sub)]" />
|
|
189
|
+
<h3 className="text-sm font-medium text-[var(--txt)]">No script tags yet</h3>
|
|
190
|
+
<p className="mt-1 text-sm text-[var(--sub)]">
|
|
189
191
|
Add tracking codes like Google Analytics, Tag Manager, or Facebook Pixel.
|
|
190
192
|
</p>
|
|
191
|
-
<
|
|
193
|
+
<Button
|
|
192
194
|
type="button"
|
|
195
|
+
variant="primary"
|
|
196
|
+
size="md"
|
|
197
|
+
className="mt-4"
|
|
193
198
|
onClick={() => setSelection({ mode: 'new' })}
|
|
194
|
-
|
|
199
|
+
leftIcon={<Plus size={16} aria-hidden />}
|
|
195
200
|
>
|
|
196
|
-
<Plus className="h-4 w-4" />
|
|
197
201
|
Add Your First Script
|
|
198
|
-
</
|
|
202
|
+
</Button>
|
|
199
203
|
</div>
|
|
200
204
|
) : (
|
|
201
|
-
<div className="
|
|
205
|
+
<div className="overflow-hidden rounded-lg border border-[var(--bdr)] bg-[var(--card)]">
|
|
202
206
|
<table className="w-full text-sm" aria-label="Script tags">
|
|
203
207
|
<thead>
|
|
204
|
-
<tr className="
|
|
205
|
-
<th className="
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
<th className="text-muted-foreground px-4 py-3 text-left font-medium">
|
|
209
|
-
Location
|
|
210
|
-
</th>
|
|
211
|
-
<th className="text-muted-foreground px-4 py-3 text-center font-medium">
|
|
208
|
+
<tr className="bg-muted border-b border-[var(--bdr)]">
|
|
209
|
+
<th className="px-4 py-3 text-left font-medium text-[var(--sub)]">Script</th>
|
|
210
|
+
<th className="px-4 py-3 text-left font-medium text-[var(--sub)]">Location</th>
|
|
211
|
+
<th className="px-4 py-3 text-center font-medium text-[var(--sub)]">
|
|
212
212
|
Priority
|
|
213
213
|
</th>
|
|
214
|
-
<th className="
|
|
215
|
-
<th className="
|
|
216
|
-
|
|
217
|
-
</th>
|
|
218
|
-
<th className="text-muted-foreground px-4 py-3 text-center font-medium">
|
|
219
|
-
Active
|
|
220
|
-
</th>
|
|
221
|
-
<th className="text-muted-foreground px-4 py-3 text-left font-medium">
|
|
222
|
-
Updated
|
|
223
|
-
</th>
|
|
214
|
+
<th className="px-4 py-3 text-left font-medium text-[var(--sub)]">Scope</th>
|
|
215
|
+
<th className="px-4 py-3 text-left font-medium text-[var(--sub)]">Consent</th>
|
|
216
|
+
<th className="px-4 py-3 text-center font-medium text-[var(--sub)]">Active</th>
|
|
217
|
+
<th className="px-4 py-3 text-left font-medium text-[var(--sub)]">Updated</th>
|
|
224
218
|
</tr>
|
|
225
219
|
</thead>
|
|
226
220
|
<tbody>
|
|
@@ -229,8 +223,8 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
229
223
|
return (
|
|
230
224
|
<tr
|
|
231
225
|
key={tag.id}
|
|
232
|
-
className={`
|
|
233
|
-
isSelected ? 'bg-[var(--acc-l)]' : 'hover:bg-
|
|
226
|
+
className={`cursor-pointer border-b border-[var(--bdr)] transition-colors last:border-0 ${
|
|
227
|
+
isSelected ? 'bg-[var(--acc-l)]' : 'hover:bg-[var(--acc-l)]'
|
|
234
228
|
}`}
|
|
235
229
|
onClick={() => toggleRow(tag)}
|
|
236
230
|
>
|
|
@@ -242,29 +236,29 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
242
236
|
toggleRow(tag)
|
|
243
237
|
}}
|
|
244
238
|
aria-expanded={isSelected}
|
|
245
|
-
className="text-
|
|
239
|
+
className="text-left font-medium text-[var(--txt)] hover:text-[var(--acc)]"
|
|
246
240
|
>
|
|
247
241
|
{tag.name}
|
|
248
242
|
</button>
|
|
249
243
|
</td>
|
|
250
244
|
<td className="px-4 py-[11px]">
|
|
251
245
|
<span
|
|
252
|
-
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${PLACEMENT_COLORS[tag.placement] ?? 'bg-muted text-
|
|
246
|
+
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${PLACEMENT_COLORS[tag.placement] ?? 'bg-muted text-[var(--txt)]'}`}
|
|
253
247
|
>
|
|
254
248
|
{PLACEMENT_LABELS[tag.placement] ?? tag.placement}
|
|
255
249
|
</span>
|
|
256
250
|
</td>
|
|
257
|
-
<td className="
|
|
251
|
+
<td className="px-4 py-[11px] text-center font-mono text-[var(--sub)]">
|
|
258
252
|
{tag.priority}
|
|
259
253
|
</td>
|
|
260
|
-
<td className="
|
|
254
|
+
<td className="px-4 py-[11px] text-[var(--sub)]">{scopeLabel(tag)}</td>
|
|
261
255
|
<td className="px-4 py-[11px]">
|
|
262
256
|
{tag.consentCategory && tag.consentCategory !== 'none' ? (
|
|
263
257
|
<span className="rounded-[5px] border border-[var(--bdr)] bg-[var(--bg)] px-1.5 py-0.5 text-[11px] text-[var(--sub)]">
|
|
264
258
|
{CONSENT_LABELS[tag.consentCategory] ?? tag.consentCategory}
|
|
265
259
|
</span>
|
|
266
260
|
) : (
|
|
267
|
-
<span className="text-
|
|
261
|
+
<span className="text-xs text-[var(--sub)]">—</span>
|
|
268
262
|
)}
|
|
269
263
|
</td>
|
|
270
264
|
<td
|
|
@@ -278,7 +272,7 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
278
272
|
aria-label={`${tag.enabled ? 'Disable' : 'Enable'} ${tag.name}`}
|
|
279
273
|
/>
|
|
280
274
|
</td>
|
|
281
|
-
<td className="
|
|
275
|
+
<td className="px-4 py-[11px] text-xs text-[var(--sub)]">
|
|
282
276
|
{formatUpdated(tag.updatedAt)}
|
|
283
277
|
</td>
|
|
284
278
|
</tr>
|
|
@@ -21,6 +21,8 @@ import { AIGenerateDialog } from './AIGenerateDialog.js'
|
|
|
21
21
|
|
|
22
22
|
export interface PageBuilderProps {
|
|
23
23
|
documentId?: string
|
|
24
|
+
/** Seed a NEW page from a saved template (`/page-builder/new?template=<id>`). */
|
|
25
|
+
templateId?: string
|
|
24
26
|
collectionSlug: string
|
|
25
27
|
config: any
|
|
26
28
|
onNavigate: (path: string) => void
|
|
@@ -55,7 +57,13 @@ function initialSectionsCollapsed(): boolean {
|
|
|
55
57
|
return false
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
export function PageBuilder({
|
|
60
|
+
export function PageBuilder({
|
|
61
|
+
documentId,
|
|
62
|
+
templateId,
|
|
63
|
+
collectionSlug,
|
|
64
|
+
config,
|
|
65
|
+
onNavigate,
|
|
66
|
+
}: PageBuilderProps) {
|
|
59
67
|
const [loading, setLoading] = useState(!!documentId)
|
|
60
68
|
const [loadError, setLoadError] = useState<string | null>(null)
|
|
61
69
|
const [saving, setSaving] = useState(false)
|
|
@@ -100,6 +108,25 @@ export function PageBuilder({ documentId, collectionSlug, config, onNavigate }:
|
|
|
100
108
|
|
|
101
109
|
useEffect(() => {
|
|
102
110
|
if (!documentId) {
|
|
111
|
+
// A new page can be seeded from a saved template ("Use template" on
|
|
112
|
+
// the Templates grid). Load the template tree, then hand the canvas
|
|
113
|
+
// over as a dirty, unsaved draft.
|
|
114
|
+
if (templateId) {
|
|
115
|
+
let cancelledNew = false
|
|
116
|
+
setLoading(true)
|
|
117
|
+
cmsApi<{ name?: string; tree?: PageNode }>(`/page-templates/${templateId}`).then((res) => {
|
|
118
|
+
if (cancelledNew) return
|
|
119
|
+
if (res.error) {
|
|
120
|
+
toast.error(`Couldn't load template: ${res.error}`)
|
|
121
|
+
} else if (res.data?.tree) {
|
|
122
|
+
replaceTree(res.data.tree)
|
|
123
|
+
}
|
|
124
|
+
setLoading(false)
|
|
125
|
+
})
|
|
126
|
+
return () => {
|
|
127
|
+
cancelledNew = true
|
|
128
|
+
}
|
|
129
|
+
}
|
|
103
130
|
setLoading(false)
|
|
104
131
|
return
|
|
105
132
|
}
|
|
@@ -139,7 +166,8 @@ export function PageBuilder({ documentId, collectionSlug, config, onNavigate }:
|
|
|
139
166
|
return () => {
|
|
140
167
|
cancelled = true
|
|
141
168
|
}
|
|
142
|
-
|
|
169
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- replaceTree/setPageSettings are stable builder actions
|
|
170
|
+
}, [documentId, templateId, collectionSlug])
|
|
143
171
|
|
|
144
172
|
const handleSave = useCallback(async () => {
|
|
145
173
|
setSaving(true)
|