@actuate-media/cms-admin 0.15.0 → 0.16.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.
Files changed (308) hide show
  1. package/dist/AdminRoot.d.ts.map +1 -1
  2. package/dist/AdminRoot.js +148 -2
  3. package/dist/AdminRoot.js.map +1 -1
  4. package/dist/__tests__/components/posts-badges.test.d.ts +2 -0
  5. package/dist/__tests__/components/posts-badges.test.d.ts.map +1 -0
  6. package/dist/__tests__/components/posts-badges.test.js +68 -0
  7. package/dist/__tests__/components/posts-badges.test.js.map +1 -0
  8. package/dist/__tests__/layout/sidebar-posts.test.d.ts +2 -0
  9. package/dist/__tests__/layout/sidebar-posts.test.d.ts.map +1 -0
  10. package/dist/__tests__/layout/sidebar-posts.test.js +162 -0
  11. package/dist/__tests__/layout/sidebar-posts.test.js.map +1 -0
  12. package/dist/__tests__/lib/page-editor-service.test.d.ts +2 -0
  13. package/dist/__tests__/lib/page-editor-service.test.d.ts.map +1 -0
  14. package/dist/__tests__/lib/page-editor-service.test.js +137 -0
  15. package/dist/__tests__/lib/page-editor-service.test.js.map +1 -0
  16. package/dist/__tests__/lib/pages-service.test.d.ts +2 -0
  17. package/dist/__tests__/lib/pages-service.test.d.ts.map +1 -0
  18. package/dist/__tests__/lib/pages-service.test.js +140 -0
  19. package/dist/__tests__/lib/pages-service.test.js.map +1 -0
  20. package/dist/__tests__/lib/post-editor-service.test.d.ts +2 -0
  21. package/dist/__tests__/lib/post-editor-service.test.d.ts.map +1 -0
  22. package/dist/__tests__/lib/post-editor-service.test.js +185 -0
  23. package/dist/__tests__/lib/post-editor-service.test.js.map +1 -0
  24. package/dist/__tests__/lib/posts-service.test.d.ts +2 -0
  25. package/dist/__tests__/lib/posts-service.test.d.ts.map +1 -0
  26. package/dist/__tests__/lib/posts-service.test.js +244 -0
  27. package/dist/__tests__/lib/posts-service.test.js.map +1 -0
  28. package/dist/__tests__/lib/useApiData.test.d.ts +2 -0
  29. package/dist/__tests__/lib/useApiData.test.d.ts.map +1 -0
  30. package/dist/__tests__/lib/useApiData.test.js +84 -0
  31. package/dist/__tests__/lib/useApiData.test.js.map +1 -0
  32. package/dist/__tests__/views/dashboard.test.d.ts +2 -0
  33. package/dist/__tests__/views/dashboard.test.d.ts.map +1 -0
  34. package/dist/__tests__/views/dashboard.test.js +138 -0
  35. package/dist/__tests__/views/dashboard.test.js.map +1 -0
  36. package/dist/__tests__/views/posts-list-view.test.d.ts +2 -0
  37. package/dist/__tests__/views/posts-list-view.test.d.ts.map +1 -0
  38. package/dist/__tests__/views/posts-list-view.test.js +98 -0
  39. package/dist/__tests__/views/posts-list-view.test.js.map +1 -0
  40. package/dist/actuate-admin.css +1 -1
  41. package/dist/components/pages/tag-badges.d.ts +21 -0
  42. package/dist/components/pages/tag-badges.d.ts.map +1 -0
  43. package/dist/components/pages/tag-badges.js +56 -0
  44. package/dist/components/pages/tag-badges.js.map +1 -0
  45. package/dist/components/posts/badges.d.ts +25 -0
  46. package/dist/components/posts/badges.d.ts.map +1 -0
  47. package/dist/components/posts/badges.js +123 -0
  48. package/dist/components/posts/badges.js.map +1 -0
  49. package/dist/components/posts/icons.d.ts +12 -0
  50. package/dist/components/posts/icons.d.ts.map +1 -0
  51. package/dist/components/posts/icons.js +26 -0
  52. package/dist/components/posts/icons.js.map +1 -0
  53. package/dist/layout/Layout.js +36 -8
  54. package/dist/layout/Layout.js.map +1 -1
  55. package/dist/layout/Sidebar.d.ts +9 -0
  56. package/dist/layout/Sidebar.d.ts.map +1 -1
  57. package/dist/layout/Sidebar.js +100 -48
  58. package/dist/layout/Sidebar.js.map +1 -1
  59. package/dist/lib/page-editor-service.d.ts +45 -0
  60. package/dist/lib/page-editor-service.d.ts.map +1 -0
  61. package/dist/lib/page-editor-service.js +153 -0
  62. package/dist/lib/page-editor-service.js.map +1 -0
  63. package/dist/lib/pages-service.d.ts +143 -0
  64. package/dist/lib/pages-service.d.ts.map +1 -0
  65. package/dist/lib/pages-service.js +444 -0
  66. package/dist/lib/pages-service.js.map +1 -0
  67. package/dist/lib/post-editor-service.d.ts +57 -0
  68. package/dist/lib/post-editor-service.d.ts.map +1 -0
  69. package/dist/lib/post-editor-service.js +234 -0
  70. package/dist/lib/post-editor-service.js.map +1 -0
  71. package/dist/lib/posts-service.d.ts +73 -0
  72. package/dist/lib/posts-service.d.ts.map +1 -0
  73. package/dist/lib/posts-service.js +245 -0
  74. package/dist/lib/posts-service.js.map +1 -0
  75. package/dist/lib/preview-link.d.ts +32 -0
  76. package/dist/lib/preview-link.d.ts.map +1 -0
  77. package/dist/lib/preview-link.js +40 -0
  78. package/dist/lib/preview-link.js.map +1 -0
  79. package/dist/router/index.d.ts +1 -0
  80. package/dist/router/index.d.ts.map +1 -1
  81. package/dist/router/index.js +9 -1
  82. package/dist/router/index.js.map +1 -1
  83. package/dist/views/Dashboard.d.ts.map +1 -1
  84. package/dist/views/Dashboard.js +64 -20
  85. package/dist/views/Dashboard.js.map +1 -1
  86. package/dist/views/Pages/PageTagManager.d.ts +14 -0
  87. package/dist/views/Pages/PageTagManager.d.ts.map +1 -0
  88. package/dist/views/Pages/PageTagManager.js +145 -0
  89. package/dist/views/Pages/PageTagManager.js.map +1 -0
  90. package/dist/views/Pages/PagesListView.d.ts +18 -0
  91. package/dist/views/Pages/PagesListView.d.ts.map +1 -0
  92. package/dist/views/Pages/PagesListView.js +287 -0
  93. package/dist/views/Pages/PagesListView.js.map +1 -0
  94. package/dist/views/Posts/NewPostPicker.d.ts +16 -0
  95. package/dist/views/Posts/NewPostPicker.d.ts.map +1 -0
  96. package/dist/views/Posts/NewPostPicker.js +59 -0
  97. package/dist/views/Posts/NewPostPicker.js.map +1 -0
  98. package/dist/views/Posts/NewPostTypeInstructions.d.ts +18 -0
  99. package/dist/views/Posts/NewPostTypeInstructions.d.ts.map +1 -0
  100. package/dist/views/Posts/NewPostTypeInstructions.js +147 -0
  101. package/dist/views/Posts/NewPostTypeInstructions.js.map +1 -0
  102. package/dist/views/Posts/PostTypesView.d.ts +26 -0
  103. package/dist/views/Posts/PostTypesView.d.ts.map +1 -0
  104. package/dist/views/Posts/PostTypesView.js +52 -0
  105. package/dist/views/Posts/PostTypesView.js.map +1 -0
  106. package/dist/views/Posts/PostsListView.d.ts +25 -0
  107. package/dist/views/Posts/PostsListView.d.ts.map +1 -0
  108. package/dist/views/Posts/PostsListView.js +239 -0
  109. package/dist/views/Posts/PostsListView.js.map +1 -0
  110. package/dist/views/Posts/post-type-presets.d.ts +32 -0
  111. package/dist/views/Posts/post-type-presets.d.ts.map +1 -0
  112. package/dist/views/Posts/post-type-presets.js +72 -0
  113. package/dist/views/Posts/post-type-presets.js.map +1 -0
  114. package/dist/views/page-builder/Widgets.d.ts +13 -0
  115. package/dist/views/page-builder/Widgets.d.ts.map +1 -0
  116. package/dist/views/page-builder/Widgets.js +15 -0
  117. package/dist/views/page-builder/Widgets.js.map +1 -0
  118. package/dist/views/page-editor/AddSectionModal.d.ts +20 -0
  119. package/dist/views/page-editor/AddSectionModal.d.ts.map +1 -0
  120. package/dist/views/page-editor/AddSectionModal.js +22 -0
  121. package/dist/views/page-editor/AddSectionModal.js.map +1 -0
  122. package/dist/views/page-editor/EditorCanvas.d.ts +18 -0
  123. package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -0
  124. package/dist/views/page-editor/EditorCanvas.js +64 -0
  125. package/dist/views/page-editor/EditorCanvas.js.map +1 -0
  126. package/dist/views/page-editor/EditorTopBar.d.ts +30 -0
  127. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -0
  128. package/dist/views/page-editor/EditorTopBar.js +42 -0
  129. package/dist/views/page-editor/EditorTopBar.js.map +1 -0
  130. package/dist/views/page-editor/PagePreview.d.ts +13 -0
  131. package/dist/views/page-editor/PagePreview.d.ts.map +1 -0
  132. package/dist/views/page-editor/PagePreview.js +37 -0
  133. package/dist/views/page-editor/PagePreview.js.map +1 -0
  134. package/dist/views/page-editor/PageSectionEditor.d.ts +16 -0
  135. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -0
  136. package/dist/views/page-editor/PageSectionEditor.js +272 -0
  137. package/dist/views/page-editor/PageSectionEditor.js.map +1 -0
  138. package/dist/views/page-editor/PageSettingsModal.d.ts +12 -0
  139. package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -0
  140. package/dist/views/page-editor/PageSettingsModal.js +52 -0
  141. package/dist/views/page-editor/PageSettingsModal.js.map +1 -0
  142. package/dist/views/page-editor/SectionInspector.d.ts +17 -0
  143. package/dist/views/page-editor/SectionInspector.d.ts.map +1 -0
  144. package/dist/views/page-editor/SectionInspector.js +98 -0
  145. package/dist/views/page-editor/SectionInspector.js.map +1 -0
  146. package/dist/views/page-editor/SectionsPanel.d.ts +15 -0
  147. package/dist/views/page-editor/SectionsPanel.d.ts.map +1 -0
  148. package/dist/views/page-editor/SectionsPanel.js +43 -0
  149. package/dist/views/page-editor/SectionsPanel.js.map +1 -0
  150. package/dist/views/page-editor/ValidationSummary.d.ts +9 -0
  151. package/dist/views/page-editor/ValidationSummary.d.ts.map +1 -0
  152. package/dist/views/page-editor/ValidationSummary.js +10 -0
  153. package/dist/views/page-editor/ValidationSummary.js.map +1 -0
  154. package/dist/views/page-editor/section-icons.d.ts +4 -0
  155. package/dist/views/page-editor/section-icons.d.ts.map +1 -0
  156. package/dist/views/page-editor/section-icons.js +20 -0
  157. package/dist/views/page-editor/section-icons.js.map +1 -0
  158. package/dist/views/page-editor/section-types.d.ts +11 -0
  159. package/dist/views/page-editor/section-types.d.ts.map +1 -0
  160. package/dist/views/page-editor/section-types.js +10 -0
  161. package/dist/views/page-editor/section-types.js.map +1 -0
  162. package/dist/views/page-editor/sections/ArticleBodySection.d.ts +13 -0
  163. package/dist/views/page-editor/sections/ArticleBodySection.d.ts.map +1 -0
  164. package/dist/views/page-editor/sections/ArticleBodySection.js +23 -0
  165. package/dist/views/page-editor/sections/ArticleBodySection.js.map +1 -0
  166. package/dist/views/page-editor/sections/AuthorBioSection.d.ts +8 -0
  167. package/dist/views/page-editor/sections/AuthorBioSection.d.ts.map +1 -0
  168. package/dist/views/page-editor/sections/AuthorBioSection.js +21 -0
  169. package/dist/views/page-editor/sections/AuthorBioSection.js.map +1 -0
  170. package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts +5 -0
  171. package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts.map +1 -0
  172. package/dist/views/page-editor/sections/ByTheNumbersSection.js +10 -0
  173. package/dist/views/page-editor/sections/ByTheNumbersSection.js.map +1 -0
  174. package/dist/views/page-editor/sections/CtaBannerSection.d.ts +5 -0
  175. package/dist/views/page-editor/sections/CtaBannerSection.d.ts.map +1 -0
  176. package/dist/views/page-editor/sections/CtaBannerSection.js +10 -0
  177. package/dist/views/page-editor/sections/CtaBannerSection.js.map +1 -0
  178. package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts +5 -0
  179. package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts.map +1 -0
  180. package/dist/views/page-editor/sections/FeatureHighlightSection.js +18 -0
  181. package/dist/views/page-editor/sections/FeatureHighlightSection.js.map +1 -0
  182. package/dist/views/page-editor/sections/GallerySection.d.ts +6 -0
  183. package/dist/views/page-editor/sections/GallerySection.d.ts.map +1 -0
  184. package/dist/views/page-editor/sections/GallerySection.js +17 -0
  185. package/dist/views/page-editor/sections/GallerySection.js.map +1 -0
  186. package/dist/views/page-editor/sections/HeroBannerSection.d.ts +5 -0
  187. package/dist/views/page-editor/sections/HeroBannerSection.d.ts.map +1 -0
  188. package/dist/views/page-editor/sections/HeroBannerSection.js +11 -0
  189. package/dist/views/page-editor/sections/HeroBannerSection.js.map +1 -0
  190. package/dist/views/page-editor/sections/MissionSection.d.ts +6 -0
  191. package/dist/views/page-editor/sections/MissionSection.d.ts.map +1 -0
  192. package/dist/views/page-editor/sections/MissionSection.js +12 -0
  193. package/dist/views/page-editor/sections/MissionSection.js.map +1 -0
  194. package/dist/views/page-editor/sections/PullQuoteSection.d.ts +6 -0
  195. package/dist/views/page-editor/sections/PullQuoteSection.d.ts.map +1 -0
  196. package/dist/views/page-editor/sections/PullQuoteSection.js +13 -0
  197. package/dist/views/page-editor/sections/PullQuoteSection.js.map +1 -0
  198. package/dist/views/page-editor/sections/RelatedPostsSection.d.ts +12 -0
  199. package/dist/views/page-editor/sections/RelatedPostsSection.d.ts.map +1 -0
  200. package/dist/views/page-editor/sections/RelatedPostsSection.js +23 -0
  201. package/dist/views/page-editor/sections/RelatedPostsSection.js.map +1 -0
  202. package/dist/views/page-editor/sections/SectionRenderer.d.ts +22 -0
  203. package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -0
  204. package/dist/views/page-editor/sections/SectionRenderer.js +52 -0
  205. package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -0
  206. package/dist/views/page-editor/sections/index.d.ts +14 -0
  207. package/dist/views/page-editor/sections/index.d.ts.map +1 -0
  208. package/dist/views/page-editor/sections/index.js +12 -0
  209. package/dist/views/page-editor/sections/index.js.map +1 -0
  210. package/dist/views/page-editor/sections/parts.d.ts +55 -0
  211. package/dist/views/page-editor/sections/parts.d.ts.map +1 -0
  212. package/dist/views/page-editor/sections/parts.js +60 -0
  213. package/dist/views/page-editor/sections/parts.js.map +1 -0
  214. package/dist/views/page-editor/viewports.d.ts +14 -0
  215. package/dist/views/page-editor/viewports.d.ts.map +1 -0
  216. package/dist/views/page-editor/viewports.js +12 -0
  217. package/dist/views/page-editor/viewports.js.map +1 -0
  218. package/dist/views/post-editor/PostEditorCanvas.d.ts +21 -0
  219. package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -0
  220. package/dist/views/post-editor/PostEditorCanvas.js +58 -0
  221. package/dist/views/post-editor/PostEditorCanvas.js.map +1 -0
  222. package/dist/views/post-editor/PostFieldsModal.d.ts +12 -0
  223. package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -0
  224. package/dist/views/post-editor/PostFieldsModal.js +53 -0
  225. package/dist/views/post-editor/PostFieldsModal.js.map +1 -0
  226. package/dist/views/post-editor/PostHeader.d.ts +15 -0
  227. package/dist/views/post-editor/PostHeader.d.ts.map +1 -0
  228. package/dist/views/post-editor/PostHeader.js +27 -0
  229. package/dist/views/post-editor/PostHeader.js.map +1 -0
  230. package/dist/views/post-editor/PostHeaderPanel.d.ts +11 -0
  231. package/dist/views/post-editor/PostHeaderPanel.d.ts.map +1 -0
  232. package/dist/views/post-editor/PostHeaderPanel.js +21 -0
  233. package/dist/views/post-editor/PostHeaderPanel.js.map +1 -0
  234. package/dist/views/post-editor/PostPreview.d.ts +14 -0
  235. package/dist/views/post-editor/PostPreview.d.ts.map +1 -0
  236. package/dist/views/post-editor/PostPreview.js +53 -0
  237. package/dist/views/post-editor/PostPreview.js.map +1 -0
  238. package/dist/views/post-editor/PostSectionEditor.d.ts +17 -0
  239. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -0
  240. package/dist/views/post-editor/PostSectionEditor.js +290 -0
  241. package/dist/views/post-editor/PostSectionEditor.js.map +1 -0
  242. package/dist/views/post-editor/PostTemplateEditor.d.ts +9 -0
  243. package/dist/views/post-editor/PostTemplateEditor.d.ts.map +1 -0
  244. package/dist/views/post-editor/PostTemplateEditor.js +144 -0
  245. package/dist/views/post-editor/PostTemplateEditor.js.map +1 -0
  246. package/package.json +2 -2
  247. package/src/AdminRoot.tsx +214 -2
  248. package/src/__tests__/components/posts-badges.test.tsx +77 -0
  249. package/src/__tests__/layout/sidebar-posts.test.ts +179 -0
  250. package/src/__tests__/lib/page-editor-service.test.ts +173 -0
  251. package/src/__tests__/lib/pages-service.test.ts +157 -0
  252. package/src/__tests__/lib/post-editor-service.test.ts +215 -0
  253. package/src/__tests__/lib/posts-service.test.ts +279 -0
  254. package/src/__tests__/lib/useApiData.test.ts +105 -0
  255. package/src/__tests__/views/dashboard.test.tsx +180 -0
  256. package/src/__tests__/views/posts-list-view.test.ts +110 -0
  257. package/src/components/pages/tag-badges.tsx +104 -0
  258. package/src/components/posts/badges.tsx +170 -0
  259. package/src/components/posts/icons.ts +40 -0
  260. package/src/layout/Layout.tsx +66 -10
  261. package/src/layout/Sidebar.tsx +116 -50
  262. package/src/lib/page-editor-service.ts +233 -0
  263. package/src/lib/pages-service.ts +599 -0
  264. package/src/lib/post-editor-service.ts +342 -0
  265. package/src/lib/posts-service.ts +366 -0
  266. package/src/lib/preview-link.ts +63 -0
  267. package/src/router/index.ts +10 -1
  268. package/src/views/Dashboard.tsx +142 -89
  269. package/src/views/Pages/PageTagManager.tsx +436 -0
  270. package/src/views/Pages/PagesListView.tsx +944 -0
  271. package/src/views/Posts/NewPostPicker.tsx +147 -0
  272. package/src/views/Posts/NewPostTypeInstructions.tsx +344 -0
  273. package/src/views/Posts/PostTypesView.tsx +194 -0
  274. package/src/views/Posts/PostsListView.tsx +869 -0
  275. package/src/views/Posts/post-type-presets.ts +105 -0
  276. package/src/views/page-builder/Widgets.tsx +44 -0
  277. package/src/views/page-editor/AddSectionModal.tsx +56 -0
  278. package/src/views/page-editor/EditorCanvas.tsx +127 -0
  279. package/src/views/page-editor/EditorTopBar.tsx +233 -0
  280. package/src/views/page-editor/PagePreview.tsx +96 -0
  281. package/src/views/page-editor/PageSectionEditor.tsx +476 -0
  282. package/src/views/page-editor/PageSettingsModal.tsx +154 -0
  283. package/src/views/page-editor/SectionInspector.tsx +599 -0
  284. package/src/views/page-editor/SectionsPanel.tsx +270 -0
  285. package/src/views/page-editor/ValidationSummary.tsx +53 -0
  286. package/src/views/page-editor/section-icons.ts +33 -0
  287. package/src/views/page-editor/section-types.ts +27 -0
  288. package/src/views/page-editor/sections/ArticleBodySection.tsx +57 -0
  289. package/src/views/page-editor/sections/AuthorBioSection.tsx +61 -0
  290. package/src/views/page-editor/sections/ByTheNumbersSection.tsx +41 -0
  291. package/src/views/page-editor/sections/CtaBannerSection.tsx +29 -0
  292. package/src/views/page-editor/sections/FeatureHighlightSection.tsx +63 -0
  293. package/src/views/page-editor/sections/GallerySection.tsx +54 -0
  294. package/src/views/page-editor/sections/HeroBannerSection.tsx +44 -0
  295. package/src/views/page-editor/sections/MissionSection.tsx +22 -0
  296. package/src/views/page-editor/sections/PullQuoteSection.tsx +27 -0
  297. package/src/views/page-editor/sections/RelatedPostsSection.tsx +55 -0
  298. package/src/views/page-editor/sections/SectionRenderer.tsx +102 -0
  299. package/src/views/page-editor/sections/index.ts +13 -0
  300. package/src/views/page-editor/sections/parts.tsx +113 -0
  301. package/src/views/page-editor/viewports.ts +25 -0
  302. package/src/views/post-editor/PostEditorCanvas.tsx +123 -0
  303. package/src/views/post-editor/PostFieldsModal.tsx +196 -0
  304. package/src/views/post-editor/PostHeader.tsx +87 -0
  305. package/src/views/post-editor/PostHeaderPanel.tsx +107 -0
  306. package/src/views/post-editor/PostPreview.tsx +114 -0
  307. package/src/views/post-editor/PostSectionEditor.tsx +506 -0
  308. package/src/views/post-editor/PostTemplateEditor.tsx +347 -0
@@ -0,0 +1,173 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ addSection,
4
+ createSection,
5
+ duplicateSection,
6
+ emptyPage,
7
+ moveSection,
8
+ removeSection,
9
+ reorderSections,
10
+ toggleSectionVisibility,
11
+ updateSectionContent,
12
+ updateSectionMeta,
13
+ updateSectionSettings,
14
+ validatePage,
15
+ type EditorPage,
16
+ type PageSection,
17
+ } from '../../lib/page-editor-service.js'
18
+ import { SECTION_TYPES, SECTION_TYPE_LIST } from '../../views/page-editor/section-types.js'
19
+
20
+ function basePage(sections: PageSection[]): EditorPage {
21
+ return { ...emptyPage(), title: 'About', slug: 'about', path: '/about', sections }
22
+ }
23
+
24
+ describe('createSection', () => {
25
+ it('seeds content + settings from the registry and a unique id', () => {
26
+ const a = createSection('hero')
27
+ const b = createSection('hero')
28
+ expect(a.sectionType).toBe('hero')
29
+ expect(a.name).toBe(SECTION_TYPES.hero.name)
30
+ expect(a.visible).toBe(true)
31
+ expect(a.id).not.toBe(b.id)
32
+ expect(a.content.heading).toBe(SECTION_TYPES.hero.defaultContent.heading)
33
+ })
34
+
35
+ it('deep-clones defaults so sections never share references', () => {
36
+ const a = createSection('stats')
37
+ const b = createSection('stats')
38
+ ;(a.content.stats as unknown[]).push({ value: 'x' })
39
+ expect((b.content.stats as unknown[]).length).toBe(
40
+ (SECTION_TYPES.stats.defaultContent.stats as unknown[]).length,
41
+ )
42
+ })
43
+ })
44
+
45
+ describe('section array helpers', () => {
46
+ const s1 = createSection('hero')
47
+ const s2 = createSection('text')
48
+ const s3 = createSection('cta')
49
+ const list = [s1, s2, s3]
50
+
51
+ it('addSection appends by default and inserts at an index', () => {
52
+ const extra = createSection('stats')
53
+ expect(addSection(list, extra).map((s) => s.id)).toEqual([s1.id, s2.id, s3.id, extra.id])
54
+ expect(addSection(list, extra, 1).map((s) => s.id)).toEqual([s1.id, extra.id, s2.id, s3.id])
55
+ })
56
+
57
+ it('removeSection drops the matching id', () => {
58
+ expect(removeSection(list, s2.id).map((s) => s.id)).toEqual([s1.id, s3.id])
59
+ })
60
+
61
+ it('moveSection relocates and clamps', () => {
62
+ expect(moveSection(list, s1.id, 2).map((s) => s.id)).toEqual([s2.id, s3.id, s1.id])
63
+ expect(moveSection(list, s1.id, 99).map((s) => s.id)).toEqual([s2.id, s3.id, s1.id])
64
+ })
65
+
66
+ it('reorderSections respects explicit order and never drops sections', () => {
67
+ expect(reorderSections(list, [s3.id, s1.id, s2.id]).map((s) => s.id)).toEqual([
68
+ s3.id,
69
+ s1.id,
70
+ s2.id,
71
+ ])
72
+ // missing ids are appended defensively rather than lost
73
+ expect(reorderSections(list, [s2.id]).map((s) => s.id)).toEqual([s2.id, s1.id, s3.id])
74
+ })
75
+
76
+ it('duplicateSection inserts a new-id copy right after the original', () => {
77
+ const out = duplicateSection(list, s2.id)
78
+ expect(out).toHaveLength(4)
79
+ expect(out[2]!.id).not.toBe(s2.id)
80
+ expect(out[2]!.name).toBe(`${s2.name} (Copy)`)
81
+ expect(out[2]!.sectionType).toBe(s2.sectionType)
82
+ })
83
+
84
+ it('toggleSectionVisibility flips only the target', () => {
85
+ const out = toggleSectionVisibility(list, s1.id)
86
+ expect(out.find((s) => s.id === s1.id)!.visible).toBe(false)
87
+ expect(out.find((s) => s.id === s2.id)!.visible).toBe(true)
88
+ })
89
+
90
+ it('updateSectionContent / settings / meta patch immutably', () => {
91
+ const c = updateSectionContent(list, s1.id, { heading: 'New' })
92
+ expect(c.find((s) => s.id === s1.id)!.content.heading).toBe('New')
93
+ expect(list.find((s) => s.id === s1.id)!.content.heading).not.toBe('New')
94
+
95
+ const st = updateSectionSettings(list, s1.id, { paddingY: 'sm' })
96
+ expect(st.find((s) => s.id === s1.id)!.settings.paddingY).toBe('sm')
97
+
98
+ const m = updateSectionMeta(list, s1.id, { name: 'Renamed' })
99
+ expect(m.find((s) => s.id === s1.id)!.name).toBe('Renamed')
100
+ })
101
+ })
102
+
103
+ describe('validatePage', () => {
104
+ it('requires title and path', () => {
105
+ const page = basePage([])
106
+ page.title = ''
107
+ page.path = ''
108
+ const { errors } = validatePage(page, false)
109
+ expect(errors).toContain('Page title is required.')
110
+ expect(errors).toContain('Page path is required.')
111
+ })
112
+
113
+ it('requires at least one visible section to publish', () => {
114
+ const hidden = { ...createSection('hero'), visible: false }
115
+ const { errors } = validatePage(basePage([hidden]), true)
116
+ expect(errors.some((e) => e.includes('at least one visible section'))).toBe(true)
117
+ })
118
+
119
+ it('flags missing required section fields on publish', () => {
120
+ const hero = createSection('hero')
121
+ hero.content.heading = '' // required
122
+ const { errors } = validatePage(basePage([hero]), true)
123
+ expect(errors.some((e) => e.includes('Heading'))).toBe(true)
124
+ })
125
+
126
+ it('flags invalid button URLs and missing image alt on publish', () => {
127
+ const feature = createSection('feature')
128
+ feature.content.heading = 'Has heading'
129
+ feature.content.buttonLabel = 'Learn more'
130
+ feature.content.buttonUrl = 'not a url'
131
+ feature.content.imageUrl = 'https://x/img.png'
132
+ feature.content.imageAlt = ''
133
+ const { errors } = validatePage(basePage([feature]), true)
134
+ expect(errors.some((e) => e.includes('not a valid URL'))).toBe(true)
135
+ expect(errors.some((e) => e.includes('alt text'))).toBe(true)
136
+ })
137
+
138
+ it('accepts a valid page and warns on missing SEO', () => {
139
+ const hero = createSection('hero')
140
+ const { errors, warnings } = validatePage(basePage([hero]), true)
141
+ expect(errors).toEqual([])
142
+ expect(warnings.length).toBeGreaterThan(0)
143
+ })
144
+
145
+ it('does not run publish-only rules for draft saves', () => {
146
+ const { errors } = validatePage(basePage([]), false)
147
+ expect(errors.some((e) => e.includes('visible section'))).toBe(false)
148
+ })
149
+ })
150
+
151
+ describe('section type registry', () => {
152
+ it('every listed type has a renderable id, defaults, fields, and a scope', () => {
153
+ const renderable = new Set([
154
+ 'hero',
155
+ 'text',
156
+ 'feature',
157
+ 'stats',
158
+ 'cta',
159
+ 'article-body',
160
+ 'quote',
161
+ 'author-bio',
162
+ 'related-posts',
163
+ 'gallery',
164
+ ])
165
+ expect(SECTION_TYPE_LIST.length).toBe(renderable.size)
166
+ for (const def of SECTION_TYPE_LIST) {
167
+ expect(renderable.has(def.id)).toBe(true)
168
+ expect(def.fields.length).toBeGreaterThan(0)
169
+ expect(def.defaultContent).toBeTruthy()
170
+ expect(def.scope.length).toBeGreaterThan(0)
171
+ }
172
+ })
173
+ })
@@ -0,0 +1,157 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { applyPagesQuery, groupPages, type Page, type PageTag } from '../../lib/pages-service.js'
3
+
4
+ /**
5
+ * Unit coverage for the Pages filtering/grouping logic. These are the
6
+ * pure functions the PagesListView depends on — kept network-free so the
7
+ * search/tag/status/sort/group contracts are locked independent of the
8
+ * REST layer.
9
+ */
10
+
11
+ function page(partial: Partial<Page> & Pick<Page, 'id' | 'title'>): Page {
12
+ return {
13
+ id: partial.id,
14
+ title: partial.title,
15
+ path: partial.path ?? `/${partial.title.toLowerCase()}`,
16
+ slug: partial.slug ?? partial.title.toLowerCase(),
17
+ parentPageId: null,
18
+ tags: partial.tags ?? [],
19
+ primaryTagId: partial.primaryTagId ?? partial.tags?.[0] ?? null,
20
+ authorId: partial.authorId ?? 'u1',
21
+ authorName: partial.authorName ?? 'Maria K.',
22
+ authorInitials: 'MK',
23
+ status: partial.status ?? 'PUBLISHED',
24
+ publishDate: null,
25
+ scheduledDate: null,
26
+ lastModified: partial.lastModified ?? '2026-05-01',
27
+ seoScore: partial.seoScore ?? null,
28
+ seoTitle: null,
29
+ seoDescription: null,
30
+ templateId: null,
31
+ isHomepage: partial.isHomepage ?? false,
32
+ isSystemPage: false,
33
+ createdAt: '2026-01-01',
34
+ updatedAt: partial.lastModified ?? '2026-05-01',
35
+ }
36
+ }
37
+
38
+ const TAGS: PageTag[] = [
39
+ {
40
+ id: 'core',
41
+ docId: 'd1',
42
+ name: 'Core',
43
+ slug: 'core',
44
+ description: null,
45
+ color: 'purple',
46
+ pageCount: 0,
47
+ sortOrder: 0,
48
+ active: true,
49
+ createdAt: null,
50
+ updatedAt: null,
51
+ },
52
+ {
53
+ id: 'services',
54
+ docId: 'd2',
55
+ name: 'Services',
56
+ slug: 'services',
57
+ description: null,
58
+ color: 'green',
59
+ pageCount: 0,
60
+ sortOrder: 1,
61
+ active: true,
62
+ createdAt: null,
63
+ updatedAt: null,
64
+ },
65
+ ]
66
+
67
+ const PAGES: Page[] = [
68
+ page({ id: '1', title: 'About', tags: ['core'], status: 'PUBLISHED', seoScore: 88 }),
69
+ page({
70
+ id: '2',
71
+ title: 'Services',
72
+ tags: ['core', 'services'],
73
+ status: 'PUBLISHED',
74
+ seoScore: 85,
75
+ }),
76
+ page({
77
+ id: '3',
78
+ title: 'SEO Services',
79
+ tags: ['services'],
80
+ status: 'DRAFT',
81
+ seoScore: 44,
82
+ authorId: 'u2',
83
+ authorName: 'Sarah M.',
84
+ }),
85
+ page({ id: '4', title: 'Home', tags: [], status: 'PUBLISHED', isHomepage: true, seoScore: 92 }),
86
+ ]
87
+
88
+ describe('applyPagesQuery', () => {
89
+ it('defaults to title ascending', () => {
90
+ const out = applyPagesQuery(PAGES, {})
91
+ expect(out.map((p) => p.title)).toEqual(['About', 'Home', 'SEO Services', 'Services'])
92
+ })
93
+
94
+ it('filters by status', () => {
95
+ const out = applyPagesQuery(PAGES, { status: 'DRAFT' })
96
+ expect(out.map((p) => p.id)).toEqual(['3'])
97
+ })
98
+
99
+ it('filters by tag with OR semantics', () => {
100
+ const out = applyPagesQuery(PAGES, { tagIds: ['services'] })
101
+ expect(out.map((p) => p.id).sort()).toEqual(['2', '3'])
102
+ })
103
+
104
+ it('matches multiple selected tags (union)', () => {
105
+ const out = applyPagesQuery(PAGES, { tagIds: ['core', 'services'] })
106
+ expect(out.map((p) => p.id).sort()).toEqual(['1', '2', '3'])
107
+ })
108
+
109
+ it('searches title, path, and tags', () => {
110
+ expect(applyPagesQuery(PAGES, { search: 'seo' }).map((p) => p.id)).toEqual(['3'])
111
+ expect(
112
+ applyPagesQuery(PAGES, { search: 'core' })
113
+ .map((p) => p.id)
114
+ .sort(),
115
+ ).toEqual(['1', '2'])
116
+ })
117
+
118
+ it('sorts by seo descending', () => {
119
+ const out = applyPagesQuery(PAGES, { sortBy: 'seo', sortDirection: 'desc' })
120
+ expect(out[0]?.id).toBe('4') // Home, 92
121
+ })
122
+ })
123
+
124
+ describe('groupPages', () => {
125
+ it('returns a single bucket for groupBy none', () => {
126
+ const groups = groupPages(PAGES, 'none', TAGS)
127
+ expect(groups).toHaveLength(1)
128
+ expect(groups[0]?.pages).toHaveLength(4)
129
+ })
130
+
131
+ it('groups by primary tag and bucket-labels untagged pages', () => {
132
+ const groups = groupPages(PAGES, 'tag', TAGS)
133
+ const byKey = new Map(groups.map((g) => [g.key, g]))
134
+ // About + Services have primary tag 'core'; SEO Services 'services';
135
+ // Home has no tags → Untagged.
136
+ expect(
137
+ byKey
138
+ .get('core')
139
+ ?.pages.map((p) => p.id)
140
+ .sort(),
141
+ ).toEqual(['1', '2'])
142
+ expect(byKey.get('services')?.pages.map((p) => p.id)).toEqual(['3'])
143
+ expect(byKey.get('__untagged')?.pages.map((p) => p.id)).toEqual(['4'])
144
+ })
145
+
146
+ it('never duplicates a page across tag groups', () => {
147
+ const groups = groupPages(PAGES, 'tag', TAGS)
148
+ const total = groups.reduce((n, g) => n + g.pages.length, 0)
149
+ expect(total).toBe(PAGES.length)
150
+ })
151
+
152
+ it('groups by status', () => {
153
+ const groups = groupPages(PAGES, 'status', TAGS)
154
+ const published = groups.find((g) => g.key === 'PUBLISHED')
155
+ expect(published?.pages).toHaveLength(3)
156
+ })
157
+ })
@@ -0,0 +1,215 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ // `cmsApi` is the only I/O dependency; mock it so the service is tested
4
+ // hermetically. Responses follow the SDK `{ data, error }` envelope.
5
+ const cmsApiMock = vi.fn()
6
+ vi.mock('../../lib/api.js', () => ({
7
+ cmsApi: (...args: unknown[]) => cmsApiMock(...args),
8
+ }))
9
+
10
+ const {
11
+ validatePost,
12
+ validateTemplate,
13
+ emptyPostFromTemplate,
14
+ fetchTemplateForEditor,
15
+ savePostDraft,
16
+ } = await import('../../lib/post-editor-service.js')
17
+
18
+ type EditorPostT = Awaited<ReturnType<typeof emptyPostFromTemplate>>
19
+
20
+ function basePost(over: Partial<EditorPostT> = {}): EditorPostT {
21
+ return {
22
+ id: 'p1',
23
+ postType: 'blog',
24
+ title: 'Hello',
25
+ slug: 'hello',
26
+ excerpt: '',
27
+ featuredImage: '',
28
+ body: '',
29
+ category: '',
30
+ status: 'DRAFT',
31
+ publishDate: null,
32
+ sections: [],
33
+ seoTitle: '',
34
+ seoDescription: '',
35
+ publishedAt: null,
36
+ updatedAt: null,
37
+ ...over,
38
+ }
39
+ }
40
+
41
+ beforeEach(() => {
42
+ cmsApiMock.mockReset()
43
+ })
44
+
45
+ describe('validatePost', () => {
46
+ it('requires title + slug', () => {
47
+ const r = validatePost(basePost({ title: '', slug: '' }), false)
48
+ expect(r.errors).toContain('Post title is required.')
49
+ expect(r.errors).toContain('Post slug is required.')
50
+ })
51
+
52
+ it('does not run section validation for draft saves', () => {
53
+ const r = validatePost(
54
+ basePost({
55
+ sections: [
56
+ {
57
+ id: 's1',
58
+ sectionType: 'quote',
59
+ name: 'Quote',
60
+ visible: true,
61
+ content: {},
62
+ settings: {},
63
+ },
64
+ ],
65
+ }),
66
+ false,
67
+ )
68
+ expect(r.errors).toEqual([])
69
+ })
70
+
71
+ it('runs section + SEO checks at publish time', () => {
72
+ const r = validatePost(
73
+ basePost({
74
+ sections: [
75
+ // missing required `quote` field → blocking section error
76
+ {
77
+ id: 's1',
78
+ sectionType: 'quote',
79
+ name: 'Quote',
80
+ visible: true,
81
+ content: {},
82
+ settings: {},
83
+ },
84
+ ],
85
+ }),
86
+ true,
87
+ )
88
+ expect(r.errors.some((e) => e.startsWith('Quote:'))).toBe(true)
89
+ expect(r.warnings.some((w) => w.includes('SEO title'))).toBe(true)
90
+ })
91
+
92
+ it('skips hidden sections during publish validation', () => {
93
+ const r = validatePost(
94
+ basePost({
95
+ seoTitle: 'T',
96
+ seoDescription: 'D',
97
+ sections: [
98
+ {
99
+ id: 's1',
100
+ sectionType: 'quote',
101
+ name: 'Quote',
102
+ visible: false,
103
+ content: {},
104
+ settings: {},
105
+ },
106
+ ],
107
+ }),
108
+ true,
109
+ )
110
+ expect(r.errors).toEqual([])
111
+ })
112
+ })
113
+
114
+ describe('validateTemplate', () => {
115
+ it('validates only visible section content', () => {
116
+ const r = validateTemplate({
117
+ docId: null,
118
+ postType: 'blog',
119
+ header: {
120
+ layout: 'centered',
121
+ showFeaturedImage: true,
122
+ showExcerpt: true,
123
+ showAuthor: true,
124
+ showDate: true,
125
+ showCategory: true,
126
+ showReadingTime: false,
127
+ },
128
+ sections: [
129
+ { id: 's1', sectionType: 'quote', name: 'Quote', visible: true, content: {}, settings: {} },
130
+ ],
131
+ })
132
+ expect(r.errors.some((e) => e.startsWith('Quote:'))).toBe(true)
133
+ })
134
+ })
135
+
136
+ describe('emptyPostFromTemplate', () => {
137
+ it('seeds sections from the type template with fresh ids', async () => {
138
+ // fetchTemplateForEditor lists the post-templates collection.
139
+ cmsApiMock.mockResolvedValueOnce({
140
+ data: {
141
+ docs: [
142
+ {
143
+ id: 'tmpl-1',
144
+ slug: 'blog',
145
+ data: {
146
+ header: { layout: 'left' },
147
+ sections: [
148
+ {
149
+ id: 'orig',
150
+ sectionType: 'article-body',
151
+ name: 'Body',
152
+ visible: true,
153
+ content: {},
154
+ settings: {},
155
+ },
156
+ ],
157
+ },
158
+ },
159
+ ],
160
+ },
161
+ })
162
+ const post = await emptyPostFromTemplate('blog')
163
+ expect(post.id).toBeNull()
164
+ expect(post.postType).toBe('blog')
165
+ expect(post.sections).toHaveLength(1)
166
+ expect(post.sections[0]!.sectionType).toBe('article-body')
167
+ // cloned: a fresh id, not the template's section id
168
+ expect(post.sections[0]!.id).not.toBe('orig')
169
+ })
170
+
171
+ it('starts empty when the type has no template', async () => {
172
+ cmsApiMock.mockResolvedValueOnce({ data: { docs: [] } })
173
+ const post = await emptyPostFromTemplate('news')
174
+ expect(post.sections).toEqual([])
175
+ })
176
+ })
177
+
178
+ describe('fetchTemplateForEditor', () => {
179
+ it('returns an unsaved default when no template document matches', async () => {
180
+ cmsApiMock.mockResolvedValueOnce({ data: { docs: [] } })
181
+ const t = await fetchTemplateForEditor('blog')
182
+ expect(t.docId).toBeNull()
183
+ expect(t.header.layout).toBe('centered')
184
+ expect(t.sections).toEqual([])
185
+ })
186
+ })
187
+
188
+ describe('savePostDraft', () => {
189
+ it('PUTs the post fields + sections for an existing post', async () => {
190
+ cmsApiMock.mockResolvedValueOnce({
191
+ data: { id: 'p1', title: 'Hello', slug: 'hello', status: 'DRAFT', data: { sections: [] } },
192
+ })
193
+ await savePostDraft(basePost({ body: 'Body copy', category: 'News' }))
194
+ expect(cmsApiMock).toHaveBeenCalledTimes(1)
195
+ const [endpoint, init] = cmsApiMock.mock.calls[0]!
196
+ expect(endpoint).toBe('/collections/blog/p1')
197
+ expect(init.method).toBe('PUT')
198
+ const body = JSON.parse(init.body as string)
199
+ expect(body.title).toBe('Hello')
200
+ expect(body.body).toBe('Body copy')
201
+ expect(body.category).toBe('News')
202
+ expect(Array.isArray(body.sections)).toBe(true)
203
+ })
204
+
205
+ it('POSTs to create when the post has no id', async () => {
206
+ cmsApiMock.mockResolvedValueOnce({
207
+ data: { id: 'new1', title: 'Hello', slug: 'hello', status: 'DRAFT', data: {} },
208
+ })
209
+ const out = await savePostDraft(basePost({ id: null }))
210
+ const [endpoint, init] = cmsApiMock.mock.calls[0]!
211
+ expect(endpoint).toBe('/collections/blog')
212
+ expect(init.method).toBe('POST')
213
+ expect(out.id).toBe('new1')
214
+ })
215
+ })