@actuate-media/cms-admin 0.14.1 → 0.16.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 +148 -2
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/components/posts-badges.test.d.ts +2 -0
- package/dist/__tests__/components/posts-badges.test.d.ts.map +1 -0
- package/dist/__tests__/components/posts-badges.test.js +68 -0
- package/dist/__tests__/components/posts-badges.test.js.map +1 -0
- package/dist/__tests__/layout/sidebar-posts.test.d.ts +2 -0
- package/dist/__tests__/layout/sidebar-posts.test.d.ts.map +1 -0
- package/dist/__tests__/layout/sidebar-posts.test.js +162 -0
- package/dist/__tests__/layout/sidebar-posts.test.js.map +1 -0
- package/dist/__tests__/lib/page-editor-service.test.d.ts +2 -0
- package/dist/__tests__/lib/page-editor-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/page-editor-service.test.js +137 -0
- package/dist/__tests__/lib/page-editor-service.test.js.map +1 -0
- package/dist/__tests__/lib/pages-service.test.d.ts +2 -0
- package/dist/__tests__/lib/pages-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/pages-service.test.js +140 -0
- package/dist/__tests__/lib/pages-service.test.js.map +1 -0
- package/dist/__tests__/lib/post-editor-service.test.d.ts +2 -0
- package/dist/__tests__/lib/post-editor-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/post-editor-service.test.js +185 -0
- package/dist/__tests__/lib/post-editor-service.test.js.map +1 -0
- package/dist/__tests__/lib/posts-service.test.d.ts +2 -0
- package/dist/__tests__/lib/posts-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/posts-service.test.js +244 -0
- package/dist/__tests__/lib/posts-service.test.js.map +1 -0
- package/dist/__tests__/views/posts-list-view.test.d.ts +2 -0
- package/dist/__tests__/views/posts-list-view.test.d.ts.map +1 -0
- package/dist/__tests__/views/posts-list-view.test.js +98 -0
- package/dist/__tests__/views/posts-list-view.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/components/pages/tag-badges.d.ts +21 -0
- package/dist/components/pages/tag-badges.d.ts.map +1 -0
- package/dist/components/pages/tag-badges.js +56 -0
- package/dist/components/pages/tag-badges.js.map +1 -0
- package/dist/components/posts/badges.d.ts +25 -0
- package/dist/components/posts/badges.d.ts.map +1 -0
- package/dist/components/posts/badges.js +123 -0
- package/dist/components/posts/badges.js.map +1 -0
- package/dist/components/posts/icons.d.ts +12 -0
- package/dist/components/posts/icons.d.ts.map +1 -0
- package/dist/components/posts/icons.js +26 -0
- package/dist/components/posts/icons.js.map +1 -0
- package/dist/layout/Header.d.ts +14 -1
- package/dist/layout/Header.d.ts.map +1 -1
- package/dist/layout/Header.js +10 -3
- package/dist/layout/Header.js.map +1 -1
- package/dist/layout/Layout.d.ts.map +1 -1
- package/dist/layout/Layout.js +78 -2
- package/dist/layout/Layout.js.map +1 -1
- package/dist/layout/Sidebar.d.ts +16 -0
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +176 -49
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/lib/page-editor-service.d.ts +45 -0
- package/dist/lib/page-editor-service.d.ts.map +1 -0
- package/dist/lib/page-editor-service.js +153 -0
- package/dist/lib/page-editor-service.js.map +1 -0
- package/dist/lib/pages-service.d.ts +143 -0
- package/dist/lib/pages-service.d.ts.map +1 -0
- package/dist/lib/pages-service.js +444 -0
- package/dist/lib/pages-service.js.map +1 -0
- package/dist/lib/post-editor-service.d.ts +57 -0
- package/dist/lib/post-editor-service.d.ts.map +1 -0
- package/dist/lib/post-editor-service.js +234 -0
- package/dist/lib/post-editor-service.js.map +1 -0
- package/dist/lib/posts-service.d.ts +73 -0
- package/dist/lib/posts-service.d.ts.map +1 -0
- package/dist/lib/posts-service.js +245 -0
- package/dist/lib/posts-service.js.map +1 -0
- package/dist/lib/preview-link.d.ts +32 -0
- package/dist/lib/preview-link.d.ts.map +1 -0
- package/dist/lib/preview-link.js +40 -0
- package/dist/lib/preview-link.js.map +1 -0
- package/dist/router/index.d.ts +1 -0
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +9 -1
- package/dist/router/index.js.map +1 -1
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +147 -62
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/Pages/PageTagManager.d.ts +14 -0
- package/dist/views/Pages/PageTagManager.d.ts.map +1 -0
- package/dist/views/Pages/PageTagManager.js +145 -0
- package/dist/views/Pages/PageTagManager.js.map +1 -0
- package/dist/views/Pages/PagesListView.d.ts +18 -0
- package/dist/views/Pages/PagesListView.d.ts.map +1 -0
- package/dist/views/Pages/PagesListView.js +287 -0
- package/dist/views/Pages/PagesListView.js.map +1 -0
- package/dist/views/Posts/NewPostPicker.d.ts +16 -0
- package/dist/views/Posts/NewPostPicker.d.ts.map +1 -0
- package/dist/views/Posts/NewPostPicker.js +59 -0
- package/dist/views/Posts/NewPostPicker.js.map +1 -0
- package/dist/views/Posts/NewPostTypeInstructions.d.ts +18 -0
- package/dist/views/Posts/NewPostTypeInstructions.d.ts.map +1 -0
- package/dist/views/Posts/NewPostTypeInstructions.js +147 -0
- package/dist/views/Posts/NewPostTypeInstructions.js.map +1 -0
- package/dist/views/Posts/PostTypesView.d.ts +26 -0
- package/dist/views/Posts/PostTypesView.d.ts.map +1 -0
- package/dist/views/Posts/PostTypesView.js +52 -0
- package/dist/views/Posts/PostTypesView.js.map +1 -0
- package/dist/views/Posts/PostsListView.d.ts +25 -0
- package/dist/views/Posts/PostsListView.d.ts.map +1 -0
- package/dist/views/Posts/PostsListView.js +239 -0
- package/dist/views/Posts/PostsListView.js.map +1 -0
- package/dist/views/Posts/post-type-presets.d.ts +32 -0
- package/dist/views/Posts/post-type-presets.d.ts.map +1 -0
- package/dist/views/Posts/post-type-presets.js +72 -0
- package/dist/views/Posts/post-type-presets.js.map +1 -0
- package/dist/views/page-builder/Widgets.d.ts +13 -0
- package/dist/views/page-builder/Widgets.d.ts.map +1 -0
- package/dist/views/page-builder/Widgets.js +15 -0
- package/dist/views/page-builder/Widgets.js.map +1 -0
- package/dist/views/page-editor/AddSectionModal.d.ts +20 -0
- package/dist/views/page-editor/AddSectionModal.d.ts.map +1 -0
- package/dist/views/page-editor/AddSectionModal.js +22 -0
- package/dist/views/page-editor/AddSectionModal.js.map +1 -0
- package/dist/views/page-editor/EditorCanvas.d.ts +18 -0
- package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -0
- package/dist/views/page-editor/EditorCanvas.js +64 -0
- package/dist/views/page-editor/EditorCanvas.js.map +1 -0
- package/dist/views/page-editor/EditorTopBar.d.ts +30 -0
- package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -0
- package/dist/views/page-editor/EditorTopBar.js +42 -0
- package/dist/views/page-editor/EditorTopBar.js.map +1 -0
- package/dist/views/page-editor/PagePreview.d.ts +13 -0
- package/dist/views/page-editor/PagePreview.d.ts.map +1 -0
- package/dist/views/page-editor/PagePreview.js +37 -0
- package/dist/views/page-editor/PagePreview.js.map +1 -0
- package/dist/views/page-editor/PageSectionEditor.d.ts +16 -0
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -0
- package/dist/views/page-editor/PageSectionEditor.js +272 -0
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -0
- package/dist/views/page-editor/PageSettingsModal.d.ts +12 -0
- package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -0
- package/dist/views/page-editor/PageSettingsModal.js +52 -0
- package/dist/views/page-editor/PageSettingsModal.js.map +1 -0
- package/dist/views/page-editor/SectionInspector.d.ts +17 -0
- package/dist/views/page-editor/SectionInspector.d.ts.map +1 -0
- package/dist/views/page-editor/SectionInspector.js +98 -0
- package/dist/views/page-editor/SectionInspector.js.map +1 -0
- package/dist/views/page-editor/SectionsPanel.d.ts +15 -0
- package/dist/views/page-editor/SectionsPanel.d.ts.map +1 -0
- package/dist/views/page-editor/SectionsPanel.js +43 -0
- package/dist/views/page-editor/SectionsPanel.js.map +1 -0
- package/dist/views/page-editor/ValidationSummary.d.ts +9 -0
- package/dist/views/page-editor/ValidationSummary.d.ts.map +1 -0
- package/dist/views/page-editor/ValidationSummary.js +10 -0
- package/dist/views/page-editor/ValidationSummary.js.map +1 -0
- package/dist/views/page-editor/section-icons.d.ts +4 -0
- package/dist/views/page-editor/section-icons.d.ts.map +1 -0
- package/dist/views/page-editor/section-icons.js +20 -0
- package/dist/views/page-editor/section-icons.js.map +1 -0
- package/dist/views/page-editor/section-types.d.ts +11 -0
- package/dist/views/page-editor/section-types.d.ts.map +1 -0
- package/dist/views/page-editor/section-types.js +10 -0
- package/dist/views/page-editor/section-types.js.map +1 -0
- package/dist/views/page-editor/sections/ArticleBodySection.d.ts +13 -0
- package/dist/views/page-editor/sections/ArticleBodySection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/ArticleBodySection.js +23 -0
- package/dist/views/page-editor/sections/ArticleBodySection.js.map +1 -0
- package/dist/views/page-editor/sections/AuthorBioSection.d.ts +8 -0
- package/dist/views/page-editor/sections/AuthorBioSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/AuthorBioSection.js +21 -0
- package/dist/views/page-editor/sections/AuthorBioSection.js.map +1 -0
- package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts +5 -0
- package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/ByTheNumbersSection.js +10 -0
- package/dist/views/page-editor/sections/ByTheNumbersSection.js.map +1 -0
- package/dist/views/page-editor/sections/CtaBannerSection.d.ts +5 -0
- package/dist/views/page-editor/sections/CtaBannerSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/CtaBannerSection.js +10 -0
- package/dist/views/page-editor/sections/CtaBannerSection.js.map +1 -0
- package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts +5 -0
- package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/FeatureHighlightSection.js +18 -0
- package/dist/views/page-editor/sections/FeatureHighlightSection.js.map +1 -0
- package/dist/views/page-editor/sections/GallerySection.d.ts +6 -0
- package/dist/views/page-editor/sections/GallerySection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/GallerySection.js +17 -0
- package/dist/views/page-editor/sections/GallerySection.js.map +1 -0
- package/dist/views/page-editor/sections/HeroBannerSection.d.ts +5 -0
- package/dist/views/page-editor/sections/HeroBannerSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/HeroBannerSection.js +11 -0
- package/dist/views/page-editor/sections/HeroBannerSection.js.map +1 -0
- package/dist/views/page-editor/sections/MissionSection.d.ts +6 -0
- package/dist/views/page-editor/sections/MissionSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/MissionSection.js +12 -0
- package/dist/views/page-editor/sections/MissionSection.js.map +1 -0
- package/dist/views/page-editor/sections/PullQuoteSection.d.ts +6 -0
- package/dist/views/page-editor/sections/PullQuoteSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/PullQuoteSection.js +13 -0
- package/dist/views/page-editor/sections/PullQuoteSection.js.map +1 -0
- package/dist/views/page-editor/sections/RelatedPostsSection.d.ts +12 -0
- package/dist/views/page-editor/sections/RelatedPostsSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/RelatedPostsSection.js +23 -0
- package/dist/views/page-editor/sections/RelatedPostsSection.js.map +1 -0
- package/dist/views/page-editor/sections/SectionRenderer.d.ts +22 -0
- package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -0
- package/dist/views/page-editor/sections/SectionRenderer.js +52 -0
- package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -0
- package/dist/views/page-editor/sections/index.d.ts +14 -0
- package/dist/views/page-editor/sections/index.d.ts.map +1 -0
- package/dist/views/page-editor/sections/index.js +12 -0
- package/dist/views/page-editor/sections/index.js.map +1 -0
- package/dist/views/page-editor/sections/parts.d.ts +55 -0
- package/dist/views/page-editor/sections/parts.d.ts.map +1 -0
- package/dist/views/page-editor/sections/parts.js +60 -0
- package/dist/views/page-editor/sections/parts.js.map +1 -0
- package/dist/views/page-editor/viewports.d.ts +14 -0
- package/dist/views/page-editor/viewports.d.ts.map +1 -0
- package/dist/views/page-editor/viewports.js +12 -0
- package/dist/views/page-editor/viewports.js.map +1 -0
- package/dist/views/post-editor/PostEditorCanvas.d.ts +21 -0
- package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -0
- package/dist/views/post-editor/PostEditorCanvas.js +58 -0
- package/dist/views/post-editor/PostEditorCanvas.js.map +1 -0
- package/dist/views/post-editor/PostFieldsModal.d.ts +12 -0
- package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -0
- package/dist/views/post-editor/PostFieldsModal.js +53 -0
- package/dist/views/post-editor/PostFieldsModal.js.map +1 -0
- package/dist/views/post-editor/PostHeader.d.ts +15 -0
- package/dist/views/post-editor/PostHeader.d.ts.map +1 -0
- package/dist/views/post-editor/PostHeader.js +27 -0
- package/dist/views/post-editor/PostHeader.js.map +1 -0
- package/dist/views/post-editor/PostHeaderPanel.d.ts +11 -0
- package/dist/views/post-editor/PostHeaderPanel.d.ts.map +1 -0
- package/dist/views/post-editor/PostHeaderPanel.js +21 -0
- package/dist/views/post-editor/PostHeaderPanel.js.map +1 -0
- package/dist/views/post-editor/PostPreview.d.ts +14 -0
- package/dist/views/post-editor/PostPreview.d.ts.map +1 -0
- package/dist/views/post-editor/PostPreview.js +53 -0
- package/dist/views/post-editor/PostPreview.js.map +1 -0
- package/dist/views/post-editor/PostSectionEditor.d.ts +17 -0
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -0
- package/dist/views/post-editor/PostSectionEditor.js +290 -0
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -0
- package/dist/views/post-editor/PostTemplateEditor.d.ts +9 -0
- package/dist/views/post-editor/PostTemplateEditor.d.ts.map +1 -0
- package/dist/views/post-editor/PostTemplateEditor.js +144 -0
- package/dist/views/post-editor/PostTemplateEditor.js.map +1 -0
- package/package.json +2 -2
- package/src/AdminRoot.tsx +213 -2
- package/src/__tests__/components/posts-badges.test.tsx +77 -0
- package/src/__tests__/layout/sidebar-posts.test.ts +179 -0
- package/src/__tests__/lib/page-editor-service.test.ts +173 -0
- package/src/__tests__/lib/pages-service.test.ts +157 -0
- package/src/__tests__/lib/post-editor-service.test.ts +215 -0
- package/src/__tests__/lib/posts-service.test.ts +279 -0
- package/src/__tests__/views/posts-list-view.test.ts +110 -0
- package/src/components/pages/tag-badges.tsx +104 -0
- package/src/components/posts/badges.tsx +170 -0
- package/src/components/posts/icons.ts +40 -0
- package/src/layout/Header.tsx +90 -43
- package/src/layout/Layout.tsx +166 -1
- package/src/layout/Sidebar.tsx +287 -101
- package/src/lib/page-editor-service.ts +233 -0
- package/src/lib/pages-service.ts +599 -0
- package/src/lib/post-editor-service.ts +342 -0
- package/src/lib/posts-service.ts +366 -0
- package/src/lib/preview-link.ts +63 -0
- package/src/router/index.ts +10 -1
- package/src/views/Dashboard.tsx +270 -131
- package/src/views/Pages/PageTagManager.tsx +436 -0
- package/src/views/Pages/PagesListView.tsx +944 -0
- package/src/views/Posts/NewPostPicker.tsx +147 -0
- package/src/views/Posts/NewPostTypeInstructions.tsx +344 -0
- package/src/views/Posts/PostTypesView.tsx +194 -0
- package/src/views/Posts/PostsListView.tsx +869 -0
- package/src/views/Posts/post-type-presets.ts +105 -0
- package/src/views/page-builder/Widgets.tsx +44 -0
- package/src/views/page-editor/AddSectionModal.tsx +56 -0
- package/src/views/page-editor/EditorCanvas.tsx +127 -0
- package/src/views/page-editor/EditorTopBar.tsx +233 -0
- package/src/views/page-editor/PagePreview.tsx +96 -0
- package/src/views/page-editor/PageSectionEditor.tsx +476 -0
- package/src/views/page-editor/PageSettingsModal.tsx +154 -0
- package/src/views/page-editor/SectionInspector.tsx +599 -0
- package/src/views/page-editor/SectionsPanel.tsx +270 -0
- package/src/views/page-editor/ValidationSummary.tsx +53 -0
- package/src/views/page-editor/section-icons.ts +33 -0
- package/src/views/page-editor/section-types.ts +27 -0
- package/src/views/page-editor/sections/ArticleBodySection.tsx +57 -0
- package/src/views/page-editor/sections/AuthorBioSection.tsx +61 -0
- package/src/views/page-editor/sections/ByTheNumbersSection.tsx +41 -0
- package/src/views/page-editor/sections/CtaBannerSection.tsx +29 -0
- package/src/views/page-editor/sections/FeatureHighlightSection.tsx +63 -0
- package/src/views/page-editor/sections/GallerySection.tsx +54 -0
- package/src/views/page-editor/sections/HeroBannerSection.tsx +44 -0
- package/src/views/page-editor/sections/MissionSection.tsx +22 -0
- package/src/views/page-editor/sections/PullQuoteSection.tsx +27 -0
- package/src/views/page-editor/sections/RelatedPostsSection.tsx +55 -0
- package/src/views/page-editor/sections/SectionRenderer.tsx +102 -0
- package/src/views/page-editor/sections/index.ts +13 -0
- package/src/views/page-editor/sections/parts.tsx +113 -0
- package/src/views/page-editor/viewports.ts +25 -0
- package/src/views/post-editor/PostEditorCanvas.tsx +123 -0
- package/src/views/post-editor/PostFieldsModal.tsx +196 -0
- package/src/views/post-editor/PostHeader.tsx +87 -0
- package/src/views/post-editor/PostHeaderPanel.tsx +107 -0
- package/src/views/post-editor/PostPreview.tsx +114 -0
- package/src/views/post-editor/PostSectionEditor.tsx +506 -0
- package/src/views/post-editor/PostTemplateEditor.tsx +347 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { AlertTriangle, ArrowLeft, Layout, Loader2, Save } from 'lucide-react';
|
|
5
|
+
import { toast } from 'sonner';
|
|
6
|
+
import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js';
|
|
7
|
+
import { ErrorBoundary } from '../../components/ErrorBoundary.js';
|
|
8
|
+
import { addSection, createSection, duplicateSection, fetchTemplateForEditor, reorderSections, removeSection, saveTemplate, toggleSectionVisibility, updateSectionContent, updateSectionMeta, updateSectionSettings, validateTemplate, } from '../../lib/post-editor-service.js';
|
|
9
|
+
import { DEFAULT_VIEWPORT } from '../page-editor/viewports.js';
|
|
10
|
+
import { SectionsPanel } from '../page-editor/SectionsPanel.js';
|
|
11
|
+
import { SectionInspector } from '../page-editor/SectionInspector.js';
|
|
12
|
+
import { AddSectionModal } from '../page-editor/AddSectionModal.js';
|
|
13
|
+
import { PostEditorCanvas } from './PostEditorCanvas.js';
|
|
14
|
+
import { PostHeaderPanel } from './PostHeaderPanel.js';
|
|
15
|
+
function deriveCanEdit(session) {
|
|
16
|
+
const role = session?.user?.role;
|
|
17
|
+
if (!role)
|
|
18
|
+
return true;
|
|
19
|
+
return ['ADMIN', 'EDITOR'].includes(role);
|
|
20
|
+
}
|
|
21
|
+
const SAMPLE_CONTEXT = {
|
|
22
|
+
title: 'Sample Post Title',
|
|
23
|
+
excerpt: 'A short summary that previews how the header excerpt will look.',
|
|
24
|
+
body: 'This is sample body copy.\n\nIt shows how the Article Body section renders the post body once an author writes a post with this template.',
|
|
25
|
+
publishDate: new Date().toISOString(),
|
|
26
|
+
category: 'Category',
|
|
27
|
+
author: { name: 'Author Name', bio: 'A short example author bio for the preview.' },
|
|
28
|
+
};
|
|
29
|
+
export function PostTemplateEditor({ postType, config, session, onNavigate, }) {
|
|
30
|
+
const [template, setTemplate] = useState(null);
|
|
31
|
+
const [loading, setLoading] = useState(true);
|
|
32
|
+
const [loadError, setLoadError] = useState(null);
|
|
33
|
+
// selection: a section id, the special 'header' panel, or nothing.
|
|
34
|
+
const [selected, setSelected] = useState(null);
|
|
35
|
+
const [viewport, setViewport] = useState(DEFAULT_VIEWPORT);
|
|
36
|
+
const [dirty, setDirty] = useState(false);
|
|
37
|
+
const [saving, setSaving] = useState(false);
|
|
38
|
+
const [addOpen, setAddOpen] = useState(false);
|
|
39
|
+
const [deleteTargetId, setDeleteTargetId] = useState(null);
|
|
40
|
+
const [leaveTarget, setLeaveTarget] = useState(null);
|
|
41
|
+
const canEdit = useMemo(() => deriveCanEdit(session), [session]);
|
|
42
|
+
const typeLabel = useMemo(() => {
|
|
43
|
+
const col = config?.collections?.[postType];
|
|
44
|
+
return col?.labels?.singular ?? postType;
|
|
45
|
+
}, [config, postType]);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
let cancelled = false;
|
|
48
|
+
setLoading(true);
|
|
49
|
+
setLoadError(null);
|
|
50
|
+
fetchTemplateForEditor(postType)
|
|
51
|
+
.then((t) => {
|
|
52
|
+
if (!cancelled)
|
|
53
|
+
setTemplate(t);
|
|
54
|
+
})
|
|
55
|
+
.catch((err) => {
|
|
56
|
+
if (!cancelled)
|
|
57
|
+
setLoadError(err instanceof Error ? err.message : 'Failed to load template');
|
|
58
|
+
})
|
|
59
|
+
.finally(() => {
|
|
60
|
+
if (!cancelled)
|
|
61
|
+
setLoading(false);
|
|
62
|
+
});
|
|
63
|
+
return () => {
|
|
64
|
+
cancelled = true;
|
|
65
|
+
};
|
|
66
|
+
}, [postType]);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!dirty)
|
|
69
|
+
return;
|
|
70
|
+
function onBeforeUnload(e) {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
e.returnValue = '';
|
|
73
|
+
}
|
|
74
|
+
window.addEventListener('beforeunload', onBeforeUnload);
|
|
75
|
+
return () => window.removeEventListener('beforeunload', onBeforeUnload);
|
|
76
|
+
}, [dirty]);
|
|
77
|
+
const mutateSections = useCallback((fn) => {
|
|
78
|
+
setTemplate((prev) => (prev ? { ...prev, sections: fn(prev.sections) } : prev));
|
|
79
|
+
setDirty(true);
|
|
80
|
+
}, []);
|
|
81
|
+
const handleHeaderChange = useCallback((patch) => {
|
|
82
|
+
setTemplate((prev) => (prev ? { ...prev, header: { ...prev.header, ...patch } } : prev));
|
|
83
|
+
setDirty(true);
|
|
84
|
+
}, []);
|
|
85
|
+
const handleAdd = useCallback((type) => {
|
|
86
|
+
const section = createSection(type);
|
|
87
|
+
mutateSections((s) => addSection(s, section));
|
|
88
|
+
setSelected({ kind: 'section', id: section.id });
|
|
89
|
+
setAddOpen(false);
|
|
90
|
+
}, [mutateSections]);
|
|
91
|
+
const selectedSectionId = selected?.kind === 'section' ? selected.id : null;
|
|
92
|
+
const handleSave = useCallback(async () => {
|
|
93
|
+
if (!template)
|
|
94
|
+
return;
|
|
95
|
+
const result = validateTemplate(template);
|
|
96
|
+
if (result.errors.length > 0) {
|
|
97
|
+
toast.error(result.errors[0]);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
setSaving(true);
|
|
101
|
+
try {
|
|
102
|
+
const saved = await saveTemplate(template);
|
|
103
|
+
setTemplate(saved);
|
|
104
|
+
setDirty(false);
|
|
105
|
+
toast.success('Template saved');
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
toast.error(err instanceof Error ? err.message : 'Failed to save template');
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
setSaving(false);
|
|
112
|
+
}
|
|
113
|
+
}, [template]);
|
|
114
|
+
const guardedNavigate = useCallback((path) => {
|
|
115
|
+
if (dirty)
|
|
116
|
+
setLeaveTarget(path);
|
|
117
|
+
else
|
|
118
|
+
onNavigate(path);
|
|
119
|
+
}, [dirty, onNavigate]);
|
|
120
|
+
if (loading) {
|
|
121
|
+
return (_jsxs("div", { className: "bg-background flex h-full items-center justify-center", children: [_jsx(Loader2, { className: "text-muted-foreground h-6 w-6 animate-spin", "aria-hidden": true }), _jsx("span", { className: "sr-only", children: "Loading template editor\u2026" })] }));
|
|
122
|
+
}
|
|
123
|
+
if (loadError || !template) {
|
|
124
|
+
return (_jsx("div", { className: "bg-background flex h-full items-center justify-center p-6", children: _jsxs("div", { className: "border-destructive/30 bg-destructive/5 flex max-w-md items-center gap-3 rounded-lg border p-4", children: [_jsx(AlertTriangle, { className: "text-destructive shrink-0", size: 20, "aria-hidden": true }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-foreground text-sm font-medium", children: "Failed to load template" }), _jsx("p", { className: "text-muted-foreground mt-1 text-sm", children: loadError ?? 'Not found.' }), _jsx("button", { type: "button", onClick: () => onNavigate('/posts/types'), className: "text-primary mt-2 text-sm font-medium hover:underline", children: "Back to Post Types" })] })] }) }));
|
|
125
|
+
}
|
|
126
|
+
const selectedSection = template.sections.find((s) => s.id === selectedSectionId) ?? null;
|
|
127
|
+
return (_jsxs(ErrorBoundary, { children: [_jsxs("div", { className: "bg-background flex h-full flex-col overflow-hidden", children: [_jsxs("header", { className: "bg-card border-border flex h-14 shrink-0 items-center justify-between gap-4 border-b px-4", children: [_jsxs("nav", { "aria-label": "Breadcrumb", className: "flex min-w-0 items-center gap-1.5 text-sm", children: [_jsxs("button", { type: "button", onClick: () => guardedNavigate('/posts/types'), className: "text-muted-foreground hover:text-foreground inline-flex shrink-0 items-center gap-1.5", children: [_jsx(ArrowLeft, { className: "h-4 w-4", "aria-hidden": true }), "Post Types"] }), _jsx("span", { className: "text-muted-foreground", "aria-hidden": true, children: "/" }), _jsxs("span", { className: "text-foreground truncate font-medium", children: [typeLabel, " Template"] }), dirty && (_jsx("span", { className: "text-muted-foreground ml-2 text-xs", "aria-live": "polite", children: "Unsaved changes" }))] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("button", { type: "button", onClick: () => setSelected({ kind: 'header' }), className: `inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition-colors ${selected?.kind === 'header'
|
|
128
|
+
? 'border-primary bg-accent text-foreground'
|
|
129
|
+
: 'border-border text-foreground hover:bg-accent'}`, children: [_jsx(Layout, { className: "h-4 w-4", "aria-hidden": true }), "Header"] }), _jsxs("button", { type: "button", onClick: handleSave, disabled: !canEdit || saving || !dirty, className: "bg-primary text-primary-foreground hover:bg-primary/90 inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50", children: [saving ? (_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin", "aria-hidden": true })) : (_jsx(Save, { className: "h-3.5 w-3.5", "aria-hidden": true })), "Save"] })] })] }), _jsxs("div", { className: "flex flex-1 overflow-hidden", children: [_jsx(SectionsPanel, { sections: template.sections, selectedId: selectedSectionId, canEdit: canEdit, onSelect: (id) => setSelected({ kind: 'section', id }), onReorder: (ids) => mutateSections((s) => reorderSections(s, ids)), onToggleVisibility: (id) => mutateSections((s) => toggleSectionVisibility(s, id)), onDuplicate: (id) => mutateSections((s) => duplicateSection(s, id)), onRequestDelete: (id) => setDeleteTargetId(id), onAdd: () => setAddOpen(true) }), _jsx("div", { className: "flex-1 overflow-hidden", children: _jsx(PostEditorCanvas, { sections: template.sections, header: template.header, context: SAMPLE_CONTEXT, selectedId: selectedSectionId, viewport: viewport, onSelect: (id) => setSelected({ kind: 'section', id }), onAddSection: () => setAddOpen(true) }) }), selected?.kind === 'header' && (_jsx(PostHeaderPanel, { header: template.header, canEdit: canEdit, onClose: () => setSelected(null), onChange: handleHeaderChange })), selectedSection && (_jsx(SectionInspector, { section: selectedSection, canEdit: canEdit, onClose: () => setSelected(null), onContentChange: (patch) => mutateSections((s) => updateSectionContent(s, selectedSection.id, patch)), onSettingsChange: (patch) => mutateSections((s) => updateSectionSettings(s, selectedSection.id, patch)), onMetaChange: (patch) => mutateSections((s) => updateSectionMeta(s, selectedSection.id, patch)), onToggleVisibility: () => mutateSections((s) => toggleSectionVisibility(s, selectedSection.id)) }))] })] }), _jsx(AddSectionModal, { open: addOpen, scope: "post", onClose: () => setAddOpen(false), onAdd: handleAdd }), _jsx(ConfirmDialog, { open: deleteTargetId !== null, onClose: () => setDeleteTargetId(null), onConfirm: () => {
|
|
130
|
+
if (deleteTargetId) {
|
|
131
|
+
mutateSections((s) => removeSection(s, deleteTargetId));
|
|
132
|
+
if (selectedSectionId === deleteTargetId)
|
|
133
|
+
setSelected(null);
|
|
134
|
+
}
|
|
135
|
+
setDeleteTargetId(null);
|
|
136
|
+
}, title: "Delete section?", description: "This removes the section from the template. You can undo by leaving without saving.", confirmLabel: "Delete", destructive: true }), _jsx(ConfirmDialog, { open: leaveTarget !== null, onClose: () => setLeaveTarget(null), onConfirm: () => {
|
|
137
|
+
const target = leaveTarget;
|
|
138
|
+
setLeaveTarget(null);
|
|
139
|
+
setDirty(false);
|
|
140
|
+
if (target)
|
|
141
|
+
onNavigate(target);
|
|
142
|
+
}, title: "Discard unsaved changes?", description: "You have unsaved changes that will be lost if you leave now.", confirmLabel: "Leave", cancelLabel: "Stay", destructive: true })] }));
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=PostTemplateEditor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PostTemplateEditor.js","sourceRoot":"","sources":["../../../src/views/post-editor/PostTemplateEditor.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@actuate-media/cms-admin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/actuate-media/actuatecms.git",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"tailwindcss": "^4.0.0",
|
|
88
88
|
"typescript": "^5.7.0",
|
|
89
89
|
"vitest": "^3.0.0",
|
|
90
|
-
"@actuate-media/cms-core": "0.
|
|
90
|
+
"@actuate-media/cms-core": "0.27.0",
|
|
91
91
|
"@actuate-media/component-blocks": "0.2.0"
|
|
92
92
|
},
|
|
93
93
|
"scripts": {
|
package/src/AdminRoot.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState, useMemo, useEffect, useRef } from 'react'
|
|
3
|
+
import { useState, useMemo, useEffect, useRef, useCallback } from 'react'
|
|
4
|
+
import { toast } from 'sonner'
|
|
4
5
|
import { Layout } from './layout/Layout.js'
|
|
5
6
|
import { useAdminRouter } from './router/index.js'
|
|
6
7
|
import { ensureCsrfToken } from './lib/api.js'
|
|
@@ -26,8 +27,20 @@ import { ThemeProvider } from './components/ThemeProvider.js'
|
|
|
26
27
|
import { LocaleProvider } from './components/LocaleProvider.js'
|
|
27
28
|
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts.js'
|
|
28
29
|
import { PageBuilder } from './views/page-builder/PageBuilder.js'
|
|
30
|
+
import { PageSectionEditor } from './views/page-editor/PageSectionEditor.js'
|
|
31
|
+
import { PagePreview } from './views/page-editor/PagePreview.js'
|
|
29
32
|
import { SavedSections } from './views/page-builder/SavedSections.js'
|
|
30
33
|
import { PageTemplates } from './views/page-builder/PageTemplates.js'
|
|
34
|
+
import { Widgets } from './views/page-builder/Widgets.js'
|
|
35
|
+
import { PagesListView } from './views/Pages/PagesListView.js'
|
|
36
|
+
import { PageTagManager } from './views/Pages/PageTagManager.js'
|
|
37
|
+
import { PostsListView } from './views/Posts/PostsListView.js'
|
|
38
|
+
import { PostTypesView } from './views/Posts/PostTypesView.js'
|
|
39
|
+
import { NewPostPicker } from './views/Posts/NewPostPicker.js'
|
|
40
|
+
import { NewPostTypeInstructions } from './views/Posts/NewPostTypeInstructions.js'
|
|
41
|
+
import { PostSectionEditor } from './views/post-editor/PostSectionEditor.js'
|
|
42
|
+
import { PostTemplateEditor } from './views/post-editor/PostTemplateEditor.js'
|
|
43
|
+
import { PostPreview } from './views/post-editor/PostPreview.js'
|
|
31
44
|
|
|
32
45
|
export interface AdminRootProps {
|
|
33
46
|
config: any
|
|
@@ -58,9 +71,45 @@ function AdminShell({
|
|
|
58
71
|
onLogin,
|
|
59
72
|
captchaConfig,
|
|
60
73
|
}: AdminRootProps) {
|
|
61
|
-
const { currentPath, navigate, matchRoute } = useAdminRouter(basePath, initialPath)
|
|
74
|
+
const { currentPath, navigate, matchRoute, buildHref } = useAdminRouter(basePath, initialPath)
|
|
62
75
|
const [shortcutHelpOpen, setShortcutHelpOpen] = useState(false)
|
|
63
76
|
|
|
77
|
+
// Open a preview route in a new browser tab so it behaves like a normal,
|
|
78
|
+
// shareable page (its own URL, no admin chrome — see the standalone branch
|
|
79
|
+
// below). The blank tab is opened synchronously inside the click gesture so
|
|
80
|
+
// popup blockers permit it; `prepare` (e.g. saving an unsaved draft) runs
|
|
81
|
+
// before we point the tab at the URL, guaranteeing the preview reflects the
|
|
82
|
+
// latest saved content. If `prepare` rejects, the tab is closed.
|
|
83
|
+
const openPreview = useCallback(
|
|
84
|
+
(path: string, prepare?: () => Promise<boolean | void> | boolean | void) => {
|
|
85
|
+
if (typeof window === 'undefined') return
|
|
86
|
+
const tab = window.open('about:blank', '_blank')
|
|
87
|
+
if (!tab) {
|
|
88
|
+
toast.error('Enable pop-ups for this site to open the preview in a new tab.')
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
const go = (ok: boolean | void) => {
|
|
92
|
+
if (ok === false) {
|
|
93
|
+
tab?.close()
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
if (tab && !tab.closed) {
|
|
97
|
+
// Sever the opener link for safety, then load the real URL.
|
|
98
|
+
try {
|
|
99
|
+
tab.opener = null
|
|
100
|
+
} catch {
|
|
101
|
+
/* cross-origin guard — never throws for same-origin admin */
|
|
102
|
+
}
|
|
103
|
+
tab.location.replace(buildHref(path))
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
Promise.resolve(prepare?.())
|
|
107
|
+
.then(go)
|
|
108
|
+
.catch(() => tab?.close())
|
|
109
|
+
},
|
|
110
|
+
[buildHref],
|
|
111
|
+
)
|
|
112
|
+
|
|
64
113
|
useBranding(config)
|
|
65
114
|
useAdminPageTitle(config, currentPath)
|
|
66
115
|
|
|
@@ -178,6 +227,142 @@ function AdminShell({
|
|
|
178
227
|
return <MediaBrowser onNavigate={navigate} />
|
|
179
228
|
}
|
|
180
229
|
|
|
230
|
+
// ─── Posts area ────────────────────────────────────────────────
|
|
231
|
+
// These take priority over the per-collection `/<slug>` matcher
|
|
232
|
+
// below so a consumer-defined `posts` collection can't accidentally
|
|
233
|
+
// shadow the aggregate Posts view.
|
|
234
|
+
if (matchRoute('/posts/types/new')) {
|
|
235
|
+
return <NewPostTypeInstructions onNavigate={navigate} />
|
|
236
|
+
}
|
|
237
|
+
// Per-type template editor — must match before `/posts/types/:slug/edit`
|
|
238
|
+
// is irrelevant (distinct last segment) but stays grouped with the type
|
|
239
|
+
// routes so it can't be shadowed by the generic `/posts/:type/*` block.
|
|
240
|
+
const postTypeTemplate = matchRoute('/posts/types/:slug/template')
|
|
241
|
+
if (postTypeTemplate?.slug) {
|
|
242
|
+
return (
|
|
243
|
+
<PostTemplateEditor
|
|
244
|
+
postType={postTypeTemplate.slug}
|
|
245
|
+
config={config}
|
|
246
|
+
session={session}
|
|
247
|
+
onNavigate={navigate}
|
|
248
|
+
/>
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
const postTypeEdit = matchRoute('/posts/types/:slug/edit')
|
|
252
|
+
if (postTypeEdit?.slug) {
|
|
253
|
+
// V1 falls through to the per-collection list view; future
|
|
254
|
+
// PR introduces a dedicated PostTypeEditor with field-level
|
|
255
|
+
// metadata. The list view is the natural per-type management
|
|
256
|
+
// surface today (it already shows posts in that type).
|
|
257
|
+
return (
|
|
258
|
+
<CollectionList collectionSlug={postTypeEdit.slug} config={config} onNavigate={navigate} />
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
if (matchRoute('/posts/types')) {
|
|
262
|
+
return <PostTypesView onNavigate={navigate} />
|
|
263
|
+
}
|
|
264
|
+
if (matchRoute('/posts/new')) {
|
|
265
|
+
return <NewPostPicker onNavigate={navigate} />
|
|
266
|
+
}
|
|
267
|
+
if (matchRoute('/posts')) {
|
|
268
|
+
return <PostsListView onNavigate={navigate} />
|
|
269
|
+
}
|
|
270
|
+
// Section-based Post editor. These `/posts/:type/*` patterns are tested
|
|
271
|
+
// AFTER every `/posts/types/*` route above so `:type` cannot swallow the
|
|
272
|
+
// reserved "types" segment.
|
|
273
|
+
const postNew = matchRoute('/posts/:type/new')
|
|
274
|
+
if (postNew?.type) {
|
|
275
|
+
return (
|
|
276
|
+
<PostSectionEditor
|
|
277
|
+
postType={postNew.type}
|
|
278
|
+
config={config}
|
|
279
|
+
session={session}
|
|
280
|
+
onNavigate={navigate}
|
|
281
|
+
onOpenPreview={openPreview}
|
|
282
|
+
/>
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
const postPreview = matchRoute('/posts/:type/:id/preview')
|
|
286
|
+
if (postPreview?.type && postPreview?.id) {
|
|
287
|
+
return (
|
|
288
|
+
<PostPreview
|
|
289
|
+
postType={postPreview.type}
|
|
290
|
+
documentId={postPreview.id}
|
|
291
|
+
session={session}
|
|
292
|
+
onNavigate={navigate}
|
|
293
|
+
/>
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
const postEdit = matchRoute('/posts/:type/:id/edit')
|
|
297
|
+
if (postEdit?.type && postEdit?.id) {
|
|
298
|
+
return (
|
|
299
|
+
<PostSectionEditor
|
|
300
|
+
postType={postEdit.type}
|
|
301
|
+
documentId={postEdit.id}
|
|
302
|
+
config={config}
|
|
303
|
+
session={session}
|
|
304
|
+
onNavigate={navigate}
|
|
305
|
+
onOpenPreview={openPreview}
|
|
306
|
+
/>
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ─── Pages area ────────────────────────────────────────────────
|
|
311
|
+
// Ordering matters: tag routes and /pages/new must be tested before
|
|
312
|
+
// the `/pages/:id` matcher, otherwise `:id` swallows "tags"/"new".
|
|
313
|
+
if (matchRoute('/pages/tags/new')) {
|
|
314
|
+
return <PageTagManager onNavigate={navigate} startCreating />
|
|
315
|
+
}
|
|
316
|
+
const pageTagEdit = matchRoute('/pages/tags/:id/edit')
|
|
317
|
+
if (pageTagEdit?.id) {
|
|
318
|
+
// The manager is a single CRUD surface; deep-linking to a tag just
|
|
319
|
+
// opens the manager (inline edit lives per-row).
|
|
320
|
+
return <PageTagManager onNavigate={navigate} />
|
|
321
|
+
}
|
|
322
|
+
if (matchRoute('/pages/tags')) {
|
|
323
|
+
return <PageTagManager onNavigate={navigate} />
|
|
324
|
+
}
|
|
325
|
+
if (matchRoute('/pages/new')) {
|
|
326
|
+
return (
|
|
327
|
+
<PageSectionEditor
|
|
328
|
+
config={config}
|
|
329
|
+
session={session}
|
|
330
|
+
onNavigate={navigate}
|
|
331
|
+
onOpenPreview={openPreview}
|
|
332
|
+
/>
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
const pagePreview = matchRoute('/pages/:id/preview')
|
|
336
|
+
if (pagePreview?.id) {
|
|
337
|
+
return <PagePreview documentId={pagePreview.id} onNavigate={navigate} />
|
|
338
|
+
}
|
|
339
|
+
const pageEdit = matchRoute('/pages/:id/edit')
|
|
340
|
+
if (pageEdit?.id) {
|
|
341
|
+
return (
|
|
342
|
+
<PageSectionEditor
|
|
343
|
+
documentId={pageEdit.id}
|
|
344
|
+
config={config}
|
|
345
|
+
session={session}
|
|
346
|
+
onNavigate={navigate}
|
|
347
|
+
onOpenPreview={openPreview}
|
|
348
|
+
/>
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
const pageEditBare = matchRoute('/pages/:id')
|
|
352
|
+
if (pageEditBare?.id) {
|
|
353
|
+
return (
|
|
354
|
+
<PageSectionEditor
|
|
355
|
+
documentId={pageEditBare.id}
|
|
356
|
+
config={config}
|
|
357
|
+
session={session}
|
|
358
|
+
onNavigate={navigate}
|
|
359
|
+
/>
|
|
360
|
+
)
|
|
361
|
+
}
|
|
362
|
+
if (matchRoute('/pages')) {
|
|
363
|
+
return <PagesListView onNavigate={navigate} />
|
|
364
|
+
}
|
|
365
|
+
|
|
181
366
|
if (matchRoute('/forms/new')) {
|
|
182
367
|
return <FormEditor onNavigate={navigate} />
|
|
183
368
|
}
|
|
@@ -225,6 +410,10 @@ function AdminShell({
|
|
|
225
410
|
return <PageTemplates onNavigate={navigate} />
|
|
226
411
|
}
|
|
227
412
|
|
|
413
|
+
if (matchRoute('/widgets')) {
|
|
414
|
+
return <Widgets onNavigate={navigate} />
|
|
415
|
+
}
|
|
416
|
+
|
|
228
417
|
if (matchRoute('/users')) {
|
|
229
418
|
return <Users onNavigate={navigate} />
|
|
230
419
|
}
|
|
@@ -264,10 +453,21 @@ function AdminShell({
|
|
|
264
453
|
'script-tags',
|
|
265
454
|
'saved-sections',
|
|
266
455
|
'page-templates',
|
|
456
|
+
'widgets',
|
|
267
457
|
'users',
|
|
268
458
|
'settings',
|
|
269
459
|
'page-builder',
|
|
270
460
|
'collections',
|
|
461
|
+
// `pages` is reserved so the Pages list/editor/tag routes above
|
|
462
|
+
// own the `/pages/*` namespace and can't be shadowed by a
|
|
463
|
+
// consumer-defined `pages` collection. The per-collection list is
|
|
464
|
+
// still reachable via `/collections/pages`.
|
|
465
|
+
'pages',
|
|
466
|
+
// `posts` is reserved so the aggregate Posts area is reachable
|
|
467
|
+
// even if a consumer defines a collection literally called
|
|
468
|
+
// `posts`. The per-type collection list is still available via
|
|
469
|
+
// `/collections/posts`.
|
|
470
|
+
'posts',
|
|
271
471
|
])
|
|
272
472
|
|
|
273
473
|
for (const [slug, def] of collectionMap) {
|
|
@@ -354,6 +554,17 @@ function AdminShell({
|
|
|
354
554
|
)
|
|
355
555
|
}
|
|
356
556
|
|
|
557
|
+
// Preview routes render as standalone pages (no sidebar/header) so a
|
|
558
|
+
// new-tab preview looks like the real published page rather than an
|
|
559
|
+
// embedded admin view. Everything else gets the full admin chrome.
|
|
560
|
+
if (currentPath.endsWith('/preview')) {
|
|
561
|
+
return (
|
|
562
|
+
<div className="bg-background h-full min-h-screen">
|
|
563
|
+
<ErrorBoundary>{renderView()}</ErrorBoundary>
|
|
564
|
+
</div>
|
|
565
|
+
)
|
|
566
|
+
}
|
|
567
|
+
|
|
357
568
|
return (
|
|
358
569
|
<Layout config={config} session={session} currentPath={currentPath} onNavigate={navigate}>
|
|
359
570
|
<ErrorBoundary>{renderView()}</ErrorBoundary>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { render, screen } from '@testing-library/react'
|
|
4
|
+
import {
|
|
5
|
+
PostStatusBadge,
|
|
6
|
+
PostTypeBadge,
|
|
7
|
+
SeoScoreIndicator,
|
|
8
|
+
seoScorePalette,
|
|
9
|
+
paletteFor,
|
|
10
|
+
} from '../../components/posts/badges.js'
|
|
11
|
+
|
|
12
|
+
describe('seoScorePalette', () => {
|
|
13
|
+
it('returns gray for null', () => {
|
|
14
|
+
expect(seoScorePalette(null)).toBe('gray')
|
|
15
|
+
})
|
|
16
|
+
it.each([
|
|
17
|
+
[80, 'green'],
|
|
18
|
+
[100, 'green'],
|
|
19
|
+
[79, 'orange'],
|
|
20
|
+
[60, 'orange'],
|
|
21
|
+
[59, 'red'],
|
|
22
|
+
[1, 'red'],
|
|
23
|
+
[0, 'gray'],
|
|
24
|
+
])('score %i → %s', (input, expected) => {
|
|
25
|
+
expect(seoScorePalette(input)).toBe(expected)
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('paletteFor', () => {
|
|
30
|
+
it('falls back to gray for unknown colors', () => {
|
|
31
|
+
const p = paletteFor('chartreuse')
|
|
32
|
+
expect(p.bg).toContain('muted')
|
|
33
|
+
})
|
|
34
|
+
it('returns gray for null', () => {
|
|
35
|
+
const p = paletteFor(null)
|
|
36
|
+
expect(p.bg).toContain('muted')
|
|
37
|
+
})
|
|
38
|
+
it('returns purple tokens for "purple"', () => {
|
|
39
|
+
const p = paletteFor('purple')
|
|
40
|
+
expect(p.bg).toMatch(/violet/)
|
|
41
|
+
expect(p.border).toMatch(/violet/)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('SeoScoreIndicator', () => {
|
|
46
|
+
it('renders a dash for missing score', () => {
|
|
47
|
+
render(<SeoScoreIndicator score={null} />)
|
|
48
|
+
expect(screen.getByText('—')).toBeTruthy()
|
|
49
|
+
})
|
|
50
|
+
it('renders the numeric score when present', () => {
|
|
51
|
+
render(<SeoScoreIndicator score={88} />)
|
|
52
|
+
expect(screen.getByText('88')).toBeTruthy()
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('PostStatusBadge', () => {
|
|
57
|
+
it('renders the human label for each status', () => {
|
|
58
|
+
render(<PostStatusBadge status="PUBLISHED" />)
|
|
59
|
+
expect(screen.getByText('Published')).toBeTruthy()
|
|
60
|
+
})
|
|
61
|
+
it('uses an aria-label so screen readers get a clean Status: foo announcement', () => {
|
|
62
|
+
const { container } = render(<PostStatusBadge status="SCHEDULED" />)
|
|
63
|
+
expect(container.querySelector('[aria-label="Status: Scheduled"]')).toBeTruthy()
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('PostTypeBadge', () => {
|
|
68
|
+
it('renders as a static span when no onClick is provided', () => {
|
|
69
|
+
const { container } = render(<PostTypeBadge label="Blog" color="purple" />)
|
|
70
|
+
expect(container.querySelector('span')?.textContent).toBe('Blog')
|
|
71
|
+
expect(container.querySelector('button')).toBeNull()
|
|
72
|
+
})
|
|
73
|
+
it('renders as a button when onClick is provided', () => {
|
|
74
|
+
render(<PostTypeBadge label="Blog" color="purple" onClick={() => {}} />)
|
|
75
|
+
expect(screen.getByRole('button', { name: 'Blog' })).toBeTruthy()
|
|
76
|
+
})
|
|
77
|
+
})
|