@prenta/admin 1.2.0 → 1.3.1
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 +12 -0
- package/design-system/README.md +1 -1
- package/design-system/tokens.css +3 -3
- package/design-system/tokens.json +2 -2
- package/design-system/tokens.scss +1 -3
- package/dist/__tests__/lib/page-audit-input.test.js +81 -0
- package/dist/__tests__/lib/page-audit-input.test.js.map +1 -1
- package/dist/__tests__/lib/version-history.test.js +18 -0
- package/dist/__tests__/lib/version-history.test.js.map +1 -1
- package/dist/__tests__/no-server-barrel-imports.test.js +3 -4
- package/dist/__tests__/no-server-barrel-imports.test.js.map +1 -1
- package/dist/__tests__/views/editor-top-bar.render.test.js +5 -0
- package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
- package/dist/__tests__/views/post-section-editor.test.js +38 -0
- package/dist/__tests__/views/post-section-editor.test.js.map +1 -1
- package/dist/__tests__/views/post-tab.render.test.d.ts +2 -0
- package/dist/__tests__/views/post-tab.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/post-tab.render.test.js +86 -0
- package/dist/__tests__/views/post-tab.render.test.js.map +1 -0
- package/dist/components/CanvasEditTip.js +1 -1
- package/dist/components/CanvasEditTip.js.map +1 -1
- package/dist/components/seo/SeoEditorPane.d.ts +9 -1
- package/dist/components/seo/SeoEditorPane.d.ts.map +1 -1
- package/dist/components/seo/SeoEditorPane.js +5 -2
- package/dist/components/seo/SeoEditorPane.js.map +1 -1
- package/dist/hooks/usePageAudit.d.ts +4 -1
- package/dist/hooks/usePageAudit.d.ts.map +1 -1
- package/dist/hooks/usePageAudit.js +3 -3
- package/dist/hooks/usePageAudit.js.map +1 -1
- package/dist/lib/page-audit-input.d.ts +26 -1
- package/dist/lib/page-audit-input.d.ts.map +1 -1
- package/dist/lib/page-audit-input.js +48 -2
- package/dist/lib/page-audit-input.js.map +1 -1
- package/dist/lib/version-history.d.ts +5 -0
- package/dist/lib/version-history.d.ts.map +1 -1
- package/dist/lib/version-history.js +18 -0
- package/dist/lib/version-history.js.map +1 -1
- package/dist/prenta-admin.css +1 -1
- package/dist/views/page-editor/EditorTopBar.js +1 -1
- package/dist/views/page-editor/EditorTopBar.js.map +1 -1
- package/dist/views/post-editor/PostEditorCanvas.d.ts +14 -1
- package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -1
- package/dist/views/post-editor/PostEditorCanvas.js +2 -2
- package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
- package/dist/views/post-editor/PostFieldsForm.d.ts +39 -0
- package/dist/views/post-editor/PostFieldsForm.d.ts.map +1 -0
- package/dist/views/post-editor/PostFieldsForm.js +85 -0
- package/dist/views/post-editor/PostFieldsForm.js.map +1 -0
- package/dist/views/post-editor/PostFieldsModal.d.ts +2 -14
- package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -1
- package/dist/views/post-editor/PostFieldsModal.js +20 -81
- package/dist/views/post-editor/PostFieldsModal.js.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.js +122 -34
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
- package/dist/views/post-editor/PostTab.d.ts +17 -0
- package/dist/views/post-editor/PostTab.d.ts.map +1 -0
- package/dist/views/post-editor/PostTab.js +23 -0
- package/dist/views/post-editor/PostTab.js.map +1 -0
- package/dist/views/settings/useAppearanceSettings.js +1 -1
- package/dist/views/settings/useAppearanceSettings.js.map +1 -1
- package/package.json +6 -6
- package/src/__tests__/lib/page-audit-input.test.ts +99 -0
- package/src/__tests__/lib/version-history.test.ts +20 -0
- package/src/__tests__/no-server-barrel-imports.test.ts +3 -4
- package/src/__tests__/views/editor-top-bar.render.test.tsx +6 -0
- package/src/__tests__/views/post-section-editor.test.tsx +59 -0
- package/src/__tests__/views/post-tab.render.test.tsx +116 -0
- package/src/components/CanvasEditTip.tsx +2 -2
- package/src/components/seo/SeoEditorPane.tsx +16 -1
- package/src/hooks/usePageAudit.ts +9 -2
- package/src/lib/page-audit-input.ts +67 -1
- package/src/lib/version-history.ts +23 -0
- package/src/styles/prenta-tokens.css +3 -3
- package/src/views/page-editor/EditorTopBar.tsx +2 -2
- package/src/views/post-editor/PostEditorCanvas.tsx +27 -5
- package/src/views/post-editor/PostFieldsForm.tsx +322 -0
- package/src/views/post-editor/PostFieldsModal.tsx +30 -281
- package/src/views/post-editor/PostSectionEditor.tsx +204 -59
- package/src/views/post-editor/PostTab.tsx +46 -0
- package/src/views/settings/useAppearanceSettings.ts +1 -1
|
@@ -10,7 +10,16 @@ import { EditorRail, RailHeader } from '../../components/EditorRail.js'
|
|
|
10
10
|
import { EditorRailTabs, type RailTab, type RailTabId } from '../../components/EditorRailTabs.js'
|
|
11
11
|
import { SeoEditorPane } from '../../components/seo/SeoEditorPane.js'
|
|
12
12
|
import { ErrorBoundary } from '../../components/ErrorBoundary.js'
|
|
13
|
-
import {
|
|
13
|
+
import { VersionHistoryPanel } from '../page-editor/VersionHistoryPanel.js'
|
|
14
|
+
import { ReviewBar } from '../page-editor/ReviewBar.js'
|
|
15
|
+
import {
|
|
16
|
+
fetchPageVersions,
|
|
17
|
+
restorePageVersion,
|
|
18
|
+
type PageVersion,
|
|
19
|
+
} from '../../lib/version-history.js'
|
|
20
|
+
import { changedSectionIds, diffVersions } from '../../lib/version-diff.js'
|
|
21
|
+
import { scoreBand, type PageAuditResult } from '@prenta/core/seo/page-audit'
|
|
22
|
+
import { usePageAudit } from '../../hooks/usePageAudit.js'
|
|
14
23
|
import {
|
|
15
24
|
emptyPostFromTemplate,
|
|
16
25
|
fetchPostForEditor,
|
|
@@ -41,13 +50,25 @@ import { EditorTopBar, type SaveState } from '../page-editor/EditorTopBar.js'
|
|
|
41
50
|
import { SectionsList } from '../page-editor/SectionsPanel.js'
|
|
42
51
|
import { SectionEditForm } from '../page-editor/SectionEditForm.js'
|
|
43
52
|
import { ValidationSummary } from '../page-editor/ValidationSummary.js'
|
|
44
|
-
import { buildEditorContentHtml
|
|
53
|
+
import { buildEditorContentHtml } from '../../lib/editor-seo-content.js'
|
|
45
54
|
import { snapshotBaseline, type ReflowBaseline } from '../../lib/reflow-baseline.js'
|
|
46
55
|
import { ReflowContext } from '../page-editor/reflow-context.js'
|
|
47
56
|
import type { Zoom } from '../../lib/breakpoints.js'
|
|
48
57
|
import { inspectorIdFromFieldPath } from '../../lib/preview-bridge.js'
|
|
49
58
|
import { PostEditorCanvas } from './PostEditorCanvas.js'
|
|
50
59
|
import { PostFieldsModal, type PostFieldConfig } from './PostFieldsModal.js'
|
|
60
|
+
import { PostTab } from './PostTab.js'
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The lower of the two audit scores — the tab dot should reflect the half that
|
|
64
|
+
* needs work, not an average that hides it. Same three-valued contract as the
|
|
65
|
+
* page editor: undefined = not reported yet, null = gated off, number = score.
|
|
66
|
+
*/
|
|
67
|
+
function weakestAuditScore(audit: PageAuditResult | null): number | null | undefined {
|
|
68
|
+
if (!audit) return undefined
|
|
69
|
+
const scores = [audit.seo.score, audit.geo.score].filter((s): s is number => s !== null)
|
|
70
|
+
return scores.length ? Math.min(...scores) : null
|
|
71
|
+
}
|
|
51
72
|
|
|
52
73
|
/** The slice of the resolved CMS config this editor reads. */
|
|
53
74
|
interface EditorConfig {
|
|
@@ -127,6 +148,18 @@ export function PostSectionEditor({
|
|
|
127
148
|
const [railTab, setRailTab] = useState<RailTabId>('sections')
|
|
128
149
|
const seoOpen = railTab === 'seo'
|
|
129
150
|
const [historyOpen, setHistoryOpen] = useState(false)
|
|
151
|
+
const [versions, setVersions] = useState<PageVersion[]>([])
|
|
152
|
+
const [versionsLoading, setVersionsLoading] = useState(false)
|
|
153
|
+
const [versionsError, setVersionsError] = useState<string | null>(null)
|
|
154
|
+
/** Index into `versions`; 0 is the newest, which is the current content. */
|
|
155
|
+
const [versionIndex, setVersionIndex] = useState(0)
|
|
156
|
+
const [restoring, setRestoring] = useState(false)
|
|
157
|
+
/**
|
|
158
|
+
* Live audit reported by the SEO pane, built from unsaved form state. Null
|
|
159
|
+
* until that pane has mounted — `audit` below falls back to the saved-record
|
|
160
|
+
* audit so the header chip and tab dot always have a number.
|
|
161
|
+
*/
|
|
162
|
+
const [liveAudit, setLiveAudit] = useState<PageAuditResult | null>(null)
|
|
130
163
|
const [confirmPublish, setConfirmPublish] = useState(false)
|
|
131
164
|
const [scheduleOpen, setScheduleOpen] = useState(false)
|
|
132
165
|
const [shareOpen, setShareOpen] = useState(false)
|
|
@@ -179,6 +212,20 @@ export function PostSectionEditor({
|
|
|
179
212
|
)
|
|
180
213
|
const publishReady = Boolean(post) && publishCheck.errors.length === 0
|
|
181
214
|
|
|
215
|
+
const { audit: savedAudit } = usePageAudit({
|
|
216
|
+
entityType: 'post',
|
|
217
|
+
entityId: post?.id ?? null,
|
|
218
|
+
sections: post?.sections ?? [],
|
|
219
|
+
slug: post?.slug ?? '',
|
|
220
|
+
fallback: { title: post?.seoTitle, description: post?.seoDescription },
|
|
221
|
+
post: {
|
|
222
|
+
title: post?.title,
|
|
223
|
+
bodyHtml: post?.body,
|
|
224
|
+
featuredImage: post?.featuredImage,
|
|
225
|
+
},
|
|
226
|
+
})
|
|
227
|
+
const audit = liveAudit ?? savedAudit
|
|
228
|
+
|
|
182
229
|
const railTabs = useMemo<RailTab[]>(
|
|
183
230
|
() => [
|
|
184
231
|
{
|
|
@@ -187,13 +234,19 @@ export function PostSectionEditor({
|
|
|
187
234
|
title: 'Post content',
|
|
188
235
|
count: post?.sections.length ?? 0,
|
|
189
236
|
},
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
237
|
+
{
|
|
238
|
+
id: 'seo',
|
|
239
|
+
label: 'SEO & GEO',
|
|
240
|
+
title: 'Search & AI readiness',
|
|
241
|
+
score: weakestAuditScore(audit),
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
id: 'page',
|
|
245
|
+
label: 'Post',
|
|
246
|
+
title: 'Post settings',
|
|
247
|
+
},
|
|
195
248
|
],
|
|
196
|
-
[post?.sections.length],
|
|
249
|
+
[post?.sections.length, audit],
|
|
197
250
|
)
|
|
198
251
|
const activeRailTab = railTabs.find((tab) => tab.id === railTab) ?? railTabs[0]!
|
|
199
252
|
|
|
@@ -218,17 +271,6 @@ export function PostSectionEditor({
|
|
|
218
271
|
[canvasEl, selectedId, baseline, viewport],
|
|
219
272
|
)
|
|
220
273
|
|
|
221
|
-
const liveSeoScore = useMemo(() => {
|
|
222
|
-
if (!post) return 0
|
|
223
|
-
return computeLiveSeoScore({
|
|
224
|
-
title: post.title,
|
|
225
|
-
slug: post.slug,
|
|
226
|
-
seoTitle: post.seoTitle,
|
|
227
|
-
seoDescription: post.seoDescription,
|
|
228
|
-
sections: post.sections,
|
|
229
|
-
bodyHtml: post.body,
|
|
230
|
-
})
|
|
231
|
-
}, [post])
|
|
232
274
|
const publishDisabledReason = publishCheck.errors[0]
|
|
233
275
|
|
|
234
276
|
// ─── Load ──────────────────────────────────────────────────────────
|
|
@@ -437,23 +479,77 @@ export function PostSectionEditor({
|
|
|
437
479
|
}
|
|
438
480
|
}, [post, documentId, postType, onNavigate])
|
|
439
481
|
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
|
|
482
|
+
// ─── Version history ─────────────────────────────────────────────────
|
|
483
|
+
// History is a MODE, not a dialog: it overlays the rail and drives the
|
|
484
|
+
// canvas, so the version being reviewed is editor state rather than
|
|
485
|
+
// something owned by a panel that can be unmounted.
|
|
486
|
+
|
|
487
|
+
const loadVersions = useCallback(
|
|
488
|
+
async (postId: string) => {
|
|
489
|
+
setVersionsLoading(true)
|
|
490
|
+
setVersionsError(null)
|
|
491
|
+
try {
|
|
492
|
+
setVersions(await fetchPageVersions(postType, postId))
|
|
493
|
+
} catch (err) {
|
|
494
|
+
setVersions([])
|
|
495
|
+
setVersionsError(err instanceof Error ? err.message : 'Failed to load version history')
|
|
496
|
+
} finally {
|
|
497
|
+
setVersionsLoading(false)
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
[postType],
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
const openHistory = useCallback(() => {
|
|
443
504
|
if (!post?.id) return
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
505
|
+
setHistoryOpen(true)
|
|
506
|
+
setVersionIndex(0)
|
|
507
|
+
void loadVersions(post.id)
|
|
508
|
+
}, [post?.id, loadVersions])
|
|
509
|
+
|
|
510
|
+
const closeHistory = useCallback(() => {
|
|
511
|
+
setHistoryOpen(false)
|
|
512
|
+
setVersionIndex(0)
|
|
513
|
+
}, [])
|
|
514
|
+
|
|
515
|
+
const reviewing = historyOpen && versionIndex > 0
|
|
516
|
+
const reviewVersion = reviewing ? (versions[versionIndex] ?? null) : null
|
|
517
|
+
|
|
518
|
+
const reviewChanges = useMemo(
|
|
519
|
+
() =>
|
|
520
|
+
reviewVersion
|
|
521
|
+
? diffVersions(reviewVersion.sections, versions[versionIndex + 1]?.sections)
|
|
522
|
+
: [],
|
|
523
|
+
[reviewVersion, versions, versionIndex],
|
|
524
|
+
)
|
|
525
|
+
const reviewChangedIds = useMemo(() => changedSectionIds(reviewChanges), [reviewChanges])
|
|
526
|
+
|
|
527
|
+
const handleRestoreVersion = useCallback(
|
|
528
|
+
async (version: PageVersion) => {
|
|
529
|
+
if (!post?.id) return
|
|
530
|
+
setRestoring(true)
|
|
531
|
+
try {
|
|
532
|
+
await restorePageVersion(postType, post.id, version.id)
|
|
533
|
+
const restored = await fetchPostForEditor(postType, post.id)
|
|
534
|
+
acceptPost(restored)
|
|
447
535
|
setSelectedId(null)
|
|
448
|
-
setRailTab('sections')
|
|
449
536
|
setDirty(false)
|
|
450
537
|
setSaveState('idle')
|
|
451
538
|
setValidation(null)
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
toast.
|
|
455
|
-
})
|
|
456
|
-
|
|
539
|
+
closeHistory()
|
|
540
|
+
void loadVersions(post.id)
|
|
541
|
+
toast.success('Version restored')
|
|
542
|
+
} catch (err) {
|
|
543
|
+
toast.error(err instanceof Error ? err.message : 'Failed to restore version')
|
|
544
|
+
} finally {
|
|
545
|
+
setRestoring(false)
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
[post?.id, postType, acceptPost, closeHistory, loadVersions],
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
const effectiveCanEdit = canEdit && !reviewing
|
|
552
|
+
const canRestoreVersion = canEdit
|
|
457
553
|
|
|
458
554
|
const handleSeoSaved = useCallback(() => {
|
|
459
555
|
if (!post?.id) return
|
|
@@ -590,8 +686,8 @@ export function PostSectionEditor({
|
|
|
590
686
|
dirty={dirty}
|
|
591
687
|
saveState={saveState}
|
|
592
688
|
publishing={publishing}
|
|
593
|
-
canEdit={
|
|
594
|
-
canPublish={canPublish &&
|
|
689
|
+
canEdit={effectiveCanEdit}
|
|
690
|
+
canPublish={canPublish && effectiveCanEdit}
|
|
595
691
|
publishReady={publishReady}
|
|
596
692
|
publishDisabledReason={publishDisabledReason}
|
|
597
693
|
viewport={viewport}
|
|
@@ -605,11 +701,15 @@ export function PostSectionEditor({
|
|
|
605
701
|
onShare={post.id ? handleShare : undefined}
|
|
606
702
|
hasPublicSiteUrl={Boolean(publicSiteUrl)}
|
|
607
703
|
matchesAdminOrigin={matchesAdminOrigin}
|
|
608
|
-
|
|
704
|
+
historyOpen={historyOpen}
|
|
705
|
+
onHistory={post.id ? () => (historyOpen ? closeHistory() : openHistory()) : undefined}
|
|
609
706
|
onOpenSettings={() => setFieldsOpen(true)}
|
|
610
707
|
onOpenSeo={handleToggleSeo}
|
|
611
708
|
seoOpen={seoOpen}
|
|
612
|
-
seoScore={
|
|
709
|
+
seoScore={audit?.seo.score ?? undefined}
|
|
710
|
+
seoScoreBand={
|
|
711
|
+
typeof audit?.seo.score === 'number' ? scoreBand(audit.seo.score) : undefined
|
|
712
|
+
}
|
|
613
713
|
onSaveDraft={handleSaveDraft}
|
|
614
714
|
onPublish={requestPublish}
|
|
615
715
|
onSchedule={post.id ? requestSchedule : undefined}
|
|
@@ -667,27 +767,44 @@ export function PostSectionEditor({
|
|
|
667
767
|
<SectionsList
|
|
668
768
|
sections={post.sections}
|
|
669
769
|
selectedId={selectedId}
|
|
670
|
-
canEdit={
|
|
770
|
+
canEdit={effectiveCanEdit}
|
|
671
771
|
onSelect={handleSelectSection}
|
|
672
772
|
onToggleVisibility={handleToggleVisibility}
|
|
673
|
-
renderEditor={
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
773
|
+
renderEditor={
|
|
774
|
+
reviewing
|
|
775
|
+
? undefined
|
|
776
|
+
: (section) => (
|
|
777
|
+
<SectionEditForm
|
|
778
|
+
section={section}
|
|
779
|
+
canEdit={effectiveCanEdit}
|
|
780
|
+
onContentChange={handleContentChange}
|
|
781
|
+
onMetaChange={handleMetaChange}
|
|
782
|
+
postBody={post.body}
|
|
783
|
+
onPostBodyChange={handlePostBodyChange}
|
|
784
|
+
/>
|
|
785
|
+
)
|
|
786
|
+
}
|
|
683
787
|
/>
|
|
788
|
+
) : railTab === 'page' ? (
|
|
789
|
+
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
790
|
+
<PostTab
|
|
791
|
+
post={post}
|
|
792
|
+
canEdit={effectiveCanEdit}
|
|
793
|
+
fields={config?.collections?.[postType]?.fields}
|
|
794
|
+
onChange={handleFields}
|
|
795
|
+
/>
|
|
796
|
+
</div>
|
|
684
797
|
) : (
|
|
685
798
|
<SeoEditorPane
|
|
686
799
|
entityType="post"
|
|
687
800
|
entityId={post.id}
|
|
688
801
|
slug={post.slug}
|
|
689
802
|
auditSections={post.sections}
|
|
803
|
+
onAuditChange={setLiveAudit}
|
|
690
804
|
contentHtml={seoContentHtml}
|
|
805
|
+
title={post.title}
|
|
806
|
+
bodyHtml={post.body}
|
|
807
|
+
featuredImage={post.featuredImage}
|
|
691
808
|
draftMeta={{
|
|
692
809
|
seoTitle: post.seoTitle,
|
|
693
810
|
seoDescription: post.seoDescription,
|
|
@@ -698,15 +815,39 @@ export function PostSectionEditor({
|
|
|
698
815
|
/>
|
|
699
816
|
)}
|
|
700
817
|
</div>
|
|
818
|
+
|
|
819
|
+
{historyOpen && (
|
|
820
|
+
<VersionHistoryPanel
|
|
821
|
+
versions={versions}
|
|
822
|
+
loading={versionsLoading}
|
|
823
|
+
error={versionsError}
|
|
824
|
+
selectedIndex={versionIndex}
|
|
825
|
+
onSelect={setVersionIndex}
|
|
826
|
+
onClose={closeHistory}
|
|
827
|
+
onRestore={handleRestoreVersion}
|
|
828
|
+
restoring={restoring}
|
|
829
|
+
canRestore={canRestoreVersion}
|
|
830
|
+
/>
|
|
831
|
+
)}
|
|
701
832
|
</>
|
|
702
833
|
)}
|
|
703
834
|
</EditorRail>
|
|
704
835
|
|
|
705
836
|
<div className="flex-1 overflow-hidden">
|
|
706
837
|
<PostEditorCanvas
|
|
707
|
-
sections={post.sections}
|
|
838
|
+
sections={reviewVersion ? reviewVersion.sections : post.sections}
|
|
708
839
|
header={header}
|
|
709
|
-
context={
|
|
840
|
+
context={
|
|
841
|
+
reviewVersion
|
|
842
|
+
? {
|
|
843
|
+
...context,
|
|
844
|
+
title: reviewVersion.title ?? context.title,
|
|
845
|
+
excerpt: reviewVersion.excerpt ?? context.excerpt,
|
|
846
|
+
body: reviewVersion.body ?? context.body,
|
|
847
|
+
featuredImageUrl: reviewVersion.featuredImage ?? context.featuredImageUrl,
|
|
848
|
+
}
|
|
849
|
+
: context
|
|
850
|
+
}
|
|
710
851
|
selectedId={selectedId}
|
|
711
852
|
viewport={viewport}
|
|
712
853
|
zoom={zoom}
|
|
@@ -714,26 +855,30 @@ export function PostSectionEditor({
|
|
|
714
855
|
onCanvasElement={setCanvasEl}
|
|
715
856
|
onSelect={handleSelectSection}
|
|
716
857
|
structuralTypes={structuralTypes}
|
|
858
|
+
reviewing={reviewing}
|
|
859
|
+
changedSectionIds={reviewChangedIds}
|
|
860
|
+
reviewBar={
|
|
861
|
+
reviewVersion ? (
|
|
862
|
+
<ReviewBar
|
|
863
|
+
version={reviewVersion}
|
|
864
|
+
changedCount={reviewChangedIds.size}
|
|
865
|
+
onRestore={() => void handleRestoreVersion(reviewVersion)}
|
|
866
|
+
onBackToCurrent={() => setVersionIndex(0)}
|
|
867
|
+
restoring={restoring}
|
|
868
|
+
canRestore={canRestoreVersion}
|
|
869
|
+
/>
|
|
870
|
+
) : null
|
|
871
|
+
}
|
|
717
872
|
/>
|
|
718
873
|
</div>
|
|
719
874
|
</div>
|
|
720
875
|
</ReflowContext.Provider>
|
|
721
876
|
</div>
|
|
722
877
|
|
|
723
|
-
{post.id && (
|
|
724
|
-
<VersionHistory
|
|
725
|
-
collectionSlug={postType}
|
|
726
|
-
documentId={post.id}
|
|
727
|
-
open={historyOpen}
|
|
728
|
-
onClose={() => setHistoryOpen(false)}
|
|
729
|
-
onRestore={handleRestored}
|
|
730
|
-
/>
|
|
731
|
-
)}
|
|
732
|
-
|
|
733
878
|
<PostFieldsModal
|
|
734
879
|
open={fieldsOpen}
|
|
735
880
|
post={post}
|
|
736
|
-
canEdit={
|
|
881
|
+
canEdit={effectiveCanEdit}
|
|
737
882
|
fields={config?.collections?.[postType]?.fields}
|
|
738
883
|
onClose={() => setFieldsOpen(false)}
|
|
739
884
|
onSave={handleFields}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import type { EditorPost } from '../../lib/post-editor-service.js'
|
|
5
|
+
import { PostFieldsForm, type PostFieldConfig } from './PostFieldsForm.js'
|
|
6
|
+
|
|
7
|
+
export interface PostTabProps {
|
|
8
|
+
post: EditorPost
|
|
9
|
+
canEdit: boolean
|
|
10
|
+
fields?: Record<string, PostFieldConfig | undefined>
|
|
11
|
+
onChange: (patch: Partial<EditorPost>) => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Rail tab 3 — post identity fields only.
|
|
16
|
+
*
|
|
17
|
+
* Title, slug, excerpt, featured image, category, and tags. Layout, header
|
|
18
|
+
* chrome, and template controls stay design-owned on the type template.
|
|
19
|
+
* Address / 301 / robots / author / danger-zone belong to pages, not posts.
|
|
20
|
+
*/
|
|
21
|
+
export function PostTab({ post, canEdit, fields, onChange }: PostTabProps) {
|
|
22
|
+
const [slugTouched, setSlugTouched] = useState(Boolean(post.slug))
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="p-3">
|
|
26
|
+
<PostFieldsForm
|
|
27
|
+
idPrefix="post"
|
|
28
|
+
values={{
|
|
29
|
+
title: post.title,
|
|
30
|
+
slug: post.slug,
|
|
31
|
+
excerpt: post.excerpt,
|
|
32
|
+
featuredImage: post.featuredImage,
|
|
33
|
+
category: post.category,
|
|
34
|
+
tags: post.tags,
|
|
35
|
+
}}
|
|
36
|
+
canEdit={canEdit}
|
|
37
|
+
fields={fields}
|
|
38
|
+
postType={post.postType}
|
|
39
|
+
bodyHtml={post.body}
|
|
40
|
+
slugTouched={slugTouched}
|
|
41
|
+
onSlugTouched={() => setSlugTouched(true)}
|
|
42
|
+
onChange={(patch) => onChange(patch)}
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
)
|
|
46
|
+
}
|