@prenta/admin 0.96.0 → 0.97.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/__tests__/lib/page-editor-service.test.js +1 -0
  3. package/dist/__tests__/lib/page-editor-service.test.js.map +1 -1
  4. package/dist/__tests__/lib/post-editor-service.test.js +27 -0
  5. package/dist/__tests__/lib/post-editor-service.test.js.map +1 -1
  6. package/dist/__tests__/views/post-section-editor.test.js +1 -0
  7. package/dist/__tests__/views/post-section-editor.test.js.map +1 -1
  8. package/dist/__tests__/views/version-history-panel.render.test.js +23 -2
  9. package/dist/__tests__/views/version-history-panel.render.test.js.map +1 -1
  10. package/dist/lib/post-editor-service.d.ts.map +1 -1
  11. package/dist/lib/post-editor-service.js +7 -0
  12. package/dist/lib/post-editor-service.js.map +1 -1
  13. package/dist/prenta-admin.css +1 -1
  14. package/dist/views/Posts/PostsListView.d.ts.map +1 -1
  15. package/dist/views/Posts/PostsListView.js +11 -1
  16. package/dist/views/Posts/PostsListView.js.map +1 -1
  17. package/dist/views/page-editor/SectionEditForm.d.ts.map +1 -1
  18. package/dist/views/page-editor/SectionEditForm.js +6 -1
  19. package/dist/views/page-editor/SectionEditForm.js.map +1 -1
  20. package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
  21. package/dist/views/page-editor/SectionInspector.js +5 -1
  22. package/dist/views/page-editor/SectionInspector.js.map +1 -1
  23. package/dist/views/page-editor/sections/DividerSection.d.ts +14 -0
  24. package/dist/views/page-editor/sections/DividerSection.d.ts.map +1 -0
  25. package/dist/views/page-editor/sections/DividerSection.js +19 -0
  26. package/dist/views/page-editor/sections/DividerSection.js.map +1 -0
  27. package/dist/views/page-editor/sections/EmbedSection.d.ts.map +1 -1
  28. package/dist/views/page-editor/sections/EmbedSection.js +11 -3
  29. package/dist/views/page-editor/sections/EmbedSection.js.map +1 -1
  30. package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -1
  31. package/dist/views/page-editor/sections/SectionRenderer.js +4 -0
  32. package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -1
  33. package/dist/views/page-editor/sections/WidgetEmbedSection.d.ts.map +1 -1
  34. package/dist/views/page-editor/sections/WidgetEmbedSection.js +1 -1
  35. package/dist/views/page-editor/sections/WidgetEmbedSection.js.map +1 -1
  36. package/package.json +5 -5
  37. package/src/__tests__/lib/page-editor-service.test.ts +1 -0
  38. package/src/__tests__/lib/post-editor-service.test.ts +30 -0
  39. package/src/__tests__/views/post-section-editor.test.tsx +1 -0
  40. package/src/__tests__/views/version-history-panel.render.test.tsx +26 -2
  41. package/src/lib/post-editor-service.ts +7 -0
  42. package/src/views/Posts/PostsListView.tsx +17 -1
  43. package/src/views/page-editor/SectionEditForm.tsx +9 -1
  44. package/src/views/page-editor/SectionInspector.tsx +7 -2
  45. package/src/views/page-editor/sections/DividerSection.tsx +31 -0
  46. package/src/views/page-editor/sections/EmbedSection.tsx +11 -5
  47. package/src/views/page-editor/sections/SectionRenderer.tsx +4 -0
  48. package/src/views/page-editor/sections/WidgetEmbedSection.tsx +4 -1
@@ -111,6 +111,12 @@ export interface ValidationResult {
111
111
  * key not declared as a field on the collection, so a post-type collection
112
112
  * missing e.g. `sections` would discard all section content on save with no
113
113
  * error. Mirrors the Page Editor's `missingPageFields` guard.
114
+ *
115
+ * Keep this list in sync with the keys `toWriteBody` sends: a key that is
116
+ * written but not guarded is exactly the silent data loss this guard exists
117
+ * to prevent. `title`/`slug` are omitted deliberately — they're required by
118
+ * every collection shape, and a post type without them cannot be listed in
119
+ * the first place.
114
120
  */
115
121
  const REQUIRED_POST_FIELDS = [
116
122
  'sections',
@@ -118,6 +124,7 @@ const REQUIRED_POST_FIELDS = [
118
124
  'featuredImage',
119
125
  'body',
120
126
  'category',
127
+ 'tags',
121
128
  'publishDate',
122
129
  'metaTitle',
123
130
  'metaDescription',
@@ -60,6 +60,17 @@ type Dir = 'asc' | 'desc'
60
60
 
61
61
  const PAGE_SIZE = 10
62
62
 
63
+ /**
64
+ * The table is `table-fixed`, so the flexible Title column only gets whatever
65
+ * the min-width leaves after the seven fixed columns (810px). The previous
66
+ * 760px was *below* that sum, so at ≤810px of table width the Title column
67
+ * collapsed to zero and the post title vanished entirely — the row kept its
68
+ * checkbox, type, author, status, date, SEO and actions, and showed no title
69
+ * at all. Keep ≥240px for the title and let the table scroll sideways
70
+ * instead. Mirrors `tableMinWidth` in the Pages list, which hit this first.
71
+ */
72
+ const TABLE_MIN_WIDTH = 810 + 240
73
+
63
74
  export function PostsListView({ onNavigate }: PostsListViewProps) {
64
75
  const [activeType, setActiveType] = useState<string>('all')
65
76
  const [search, setSearch] = useState('')
@@ -331,7 +342,12 @@ export function PostsListView({ onNavigate }: PostsListViewProps) {
331
342
  {/* Both-axis scroll: tall lists scroll vertically; narrow viewports
332
343
  keep column widths and scroll sideways instead of crushing. */}
333
344
  <div className="min-h-0 flex-1 overflow-auto">
334
- <table className="w-full min-w-[760px] table-fixed" role="table" aria-label="Posts">
345
+ <table
346
+ className="w-full table-fixed"
347
+ style={{ minWidth: TABLE_MIN_WIDTH }}
348
+ role="table"
349
+ aria-label="Posts"
350
+ >
335
351
  <colgroup>
336
352
  <col style={{ width: '40px' }} />
337
353
  <col />
@@ -3,6 +3,7 @@
3
3
  import { ExternalLink, Lock } from 'lucide-react'
4
4
  import { RichTextField } from '../../fields/RichTextField.js'
5
5
  import type { PageSection } from '../../lib/page-editor-service.js'
6
+ import { visibleFields } from '@prenta/core/page-sections'
6
7
  import { ContentField, FIELD_INPUT, FIELD_LABEL } from './section-fields.js'
7
8
  import { getSectionType } from './section-types.js'
8
9
 
@@ -75,7 +76,14 @@ export function SectionEditForm({
75
76
  section.sectionType === 'article-body' &&
76
77
  bodySource === 'field' &&
77
78
  onPostBodyChange !== undefined
78
- const contentFields = def?.fields.filter((f) => !(editPostBodyField && f.key === 'body')) ?? []
79
+ // `visibleFields` first, so a type offering alternative inputs for one job
80
+ // (see the video section's source switch) shows only the box that is live.
81
+ // The validator reads the same helper, so it can never require a field the
82
+ // editor is not drawing.
83
+ const contentFields =
84
+ visibleFields(def?.fields ?? [], section.content).filter(
85
+ (f) => !(editPostBodyField && f.key === 'body'),
86
+ ) ?? []
79
87
 
80
88
  return (
81
89
  <div className="border-border bg-muted/40 border-t px-3 py-3.5">
@@ -3,6 +3,7 @@
3
3
  import { useEffect } from 'react'
4
4
  import { Eye, EyeOff, Lock, X } from 'lucide-react'
5
5
  import { InspectorExpandButton, ResizableAside } from '../../components/ui/ResizableAside.js'
6
+ import { visibleFields } from '@prenta/core/page-sections'
6
7
  import { ContentField, FIELD_INPUT, FIELD_LABEL } from './section-fields.js'
7
8
  import { RichTextField } from '../../fields/RichTextField.js'
8
9
  import type { PageSection } from '../../lib/page-editor-service.js'
@@ -66,8 +67,12 @@ export function SectionInspector({
66
67
  section.sectionType === 'article-body' &&
67
68
  bodySource === 'field' &&
68
69
  onPostBodyChange !== undefined
69
- const contentFields =
70
- def?.fields.filter((field) => !(editPostBodyField && field.key === 'body')) ?? []
70
+ // Same visibility rule as the inline form and the validator — see
71
+ // `visibleFields`. The template inspector must not offer a box the page
72
+ // editor hides, or the two surfaces disagree about the same section.
73
+ const contentFields = visibleFields(def?.fields ?? [], section.content).filter(
74
+ (field) => !(editPostBodyField && field.key === 'body'),
75
+ )
71
76
 
72
77
  // Escape closes the inspector.
73
78
  useEffect(() => {
@@ -0,0 +1,31 @@
1
+ import type { PageSection } from '../../../lib/page-editor-service.js'
2
+ import { str } from './parts.js'
3
+
4
+ /**
5
+ * Canvas body for the `divider` section.
6
+ *
7
+ * `space` deliberately renders an empty box rather than nothing: on the public
8
+ * page the section wrapper's padding IS the break, but on the canvas a section
9
+ * that draws nothing at all reads as broken. A faint dashed outline says "this
10
+ * is intentional whitespace" without pretending to be what ships — the canvas
11
+ * shows structure, and empty space is the structure here.
12
+ */
13
+ export function DividerSection({ section }: { section: PageSection }) {
14
+ const variant = str(section.content, 'variant') || 'line'
15
+
16
+ if (variant === 'space') {
17
+ return (
18
+ <div className="mx-auto max-w-3xl px-6">
19
+ <div className="border-border text-muted-foreground flex h-10 items-center justify-center rounded-md border border-dashed text-xs">
20
+ Space
21
+ </div>
22
+ </div>
23
+ )
24
+ }
25
+
26
+ return (
27
+ <div className="mx-auto max-w-3xl px-6">
28
+ <hr className="border-0 border-t border-current opacity-15" aria-hidden />
29
+ </div>
30
+ )
31
+ }
@@ -1,5 +1,5 @@
1
1
  import { PlayCircle } from 'lucide-react'
2
- import { parseEmbed, EMBED_PROVIDER_LABEL } from '@prenta/core/page-sections'
2
+ import { resolveVideo, EMBED_PROVIDER_LABEL } from '@prenta/core/page-sections'
3
3
  import type { PageSection } from '../../../lib/page-editor-service.js'
4
4
  import { str } from './parts.js'
5
5
 
@@ -17,10 +17,18 @@ const RATIO_CLASS: Record<string, string> = {
17
17
  */
18
18
  export function EmbedSection({ section }: { section: PageSection }) {
19
19
  const c = section.content
20
- const parsed = parseEmbed(str(c, 'url'))
20
+ const video = resolveVideo(c)
21
21
  const ratioClass = RATIO_CLASS[str(c, 'ratio', '16:9')] ?? RATIO_CLASS['16:9']
22
22
  const title = str(c, 'title')
23
23
  const caption = str(c, 'caption')
24
+ // Naming the resolved source is the only confirmation an editor gets that
25
+ // the link they pasted, or the file they picked, actually resolved — the
26
+ // canvas never loads the real video either way.
27
+ const label = !video
28
+ ? 'No valid video set'
29
+ : video.kind === 'hosted'
30
+ ? 'Uploaded video'
31
+ : `${EMBED_PROVIDER_LABEL[video.provider]} video`
24
32
 
25
33
  return (
26
34
  <figure className="mx-auto max-w-3xl px-6">
@@ -28,9 +36,7 @@ export function EmbedSection({ section }: { section: PageSection }) {
28
36
  className={`bg-muted text-muted-foreground relative flex w-full flex-col items-center justify-center gap-2 overflow-hidden rounded-xl ${ratioClass}`}
29
37
  >
30
38
  <PlayCircle className="h-10 w-10" aria-hidden />
31
- <p className="text-foreground text-sm font-medium">
32
- {parsed ? `${EMBED_PROVIDER_LABEL[parsed.provider]} video` : 'No valid video URL'}
33
- </p>
39
+ <p className="text-foreground text-sm font-medium">{label}</p>
34
40
  {title && <p className="text-muted-foreground px-4 text-center text-xs">{title}</p>}
35
41
  </div>
36
42
  {caption && (
@@ -18,6 +18,7 @@ import { RelatedPostsSection } from './RelatedPostsSection.js'
18
18
  import { GallerySection } from './GallerySection.js'
19
19
  import { EmbedSection } from './EmbedSection.js'
20
20
  import { WidgetEmbedSection } from './WidgetEmbedSection.js'
21
+ import { DividerSection } from './DividerSection.js'
21
22
 
22
23
  /**
23
24
  * Section types with a real canvas body component below. Anything else —
@@ -38,6 +39,7 @@ const CANVAS_RENDERED_TYPES: ReadonlySet<string> = new Set([
38
39
  'gallery',
39
40
  'embed',
40
41
  'widget-embed',
42
+ 'divider',
41
43
  ])
42
44
 
43
45
  /** True when the canvas renders this type for real (not as a placeholder). */
@@ -126,6 +128,8 @@ function BuiltInSectionBody({
126
128
  return <EmbedSection section={section} />
127
129
  case 'widget-embed':
128
130
  return <WidgetEmbedSection section={section} />
131
+ case 'divider':
132
+ return <DividerSection section={section} />
129
133
  default:
130
134
  // Config-declared custom types and unmanaged sections with no supplied
131
135
  // renderer: a neutral placeholder keeps the canvas reflecting the
@@ -20,10 +20,13 @@ export function WidgetEmbedSection({ section }: { section: PageSection }) {
20
20
  style={{ minHeight: `${minHeight}px` }}
21
21
  >
22
22
  <Puzzle className="h-8 w-8" aria-hidden />
23
+ {/* Naming the recognised provider is the only feedback an editor gets
24
+ that their pasted snippet parsed — the widget itself never loads in
25
+ the canvas, so "it looks blank" is the expected state either way. */}
23
26
  <p className="text-foreground text-sm font-medium">
24
27
  {parsed
25
28
  ? `${WIDGET_PROVIDER_LABEL[parsed.provider]} widget`
26
- : 'Configure widget provider + ids'}
29
+ : 'Paste embed code to load a widget'}
27
30
  </p>
28
31
  {title && <p className="text-muted-foreground px-4 text-center text-xs">{title}</p>}
29
32
  </div>