@actuate-media/cms-admin 0.65.0 → 0.67.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 (138) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/AdminRoot.d.ts.map +1 -1
  3. package/dist/AdminRoot.js +7 -4
  4. package/dist/AdminRoot.js.map +1 -1
  5. package/dist/__tests__/components/inspector-pane.test.d.ts +2 -0
  6. package/dist/__tests__/components/inspector-pane.test.d.ts.map +1 -0
  7. package/dist/__tests__/components/inspector-pane.test.js +31 -0
  8. package/dist/__tests__/components/inspector-pane.test.js.map +1 -0
  9. package/dist/__tests__/lib/pages-tree.test.d.ts +2 -0
  10. package/dist/__tests__/lib/pages-tree.test.d.ts.map +1 -0
  11. package/dist/__tests__/lib/pages-tree.test.js +173 -0
  12. package/dist/__tests__/lib/pages-tree.test.js.map +1 -0
  13. package/dist/__tests__/views/dashboard.test.js +7 -3
  14. package/dist/__tests__/views/dashboard.test.js.map +1 -1
  15. package/dist/__tests__/views/media-seo-audit.render.test.d.ts +2 -0
  16. package/dist/__tests__/views/media-seo-audit.render.test.d.ts.map +1 -0
  17. package/dist/__tests__/views/media-seo-audit.render.test.js +102 -0
  18. package/dist/__tests__/views/media-seo-audit.render.test.js.map +1 -0
  19. package/dist/__tests__/views/onboarding-block.render.test.d.ts +2 -0
  20. package/dist/__tests__/views/onboarding-block.render.test.d.ts.map +1 -0
  21. package/dist/__tests__/views/onboarding-block.render.test.js +146 -0
  22. package/dist/__tests__/views/onboarding-block.render.test.js.map +1 -0
  23. package/dist/__tests__/views/pages-list-view.test.js +77 -1
  24. package/dist/__tests__/views/pages-list-view.test.js.map +1 -1
  25. package/dist/actuate-admin.css +1 -1
  26. package/dist/components/script-tags/ScriptTagEditorPane.d.ts +26 -0
  27. package/dist/components/script-tags/ScriptTagEditorPane.d.ts.map +1 -0
  28. package/dist/components/script-tags/ScriptTagEditorPane.js +160 -0
  29. package/dist/components/script-tags/ScriptTagEditorPane.js.map +1 -0
  30. package/dist/components/ui/InspectorPane.d.ts +51 -0
  31. package/dist/components/ui/InspectorPane.d.ts.map +1 -0
  32. package/dist/components/ui/InspectorPane.js +42 -0
  33. package/dist/components/ui/InspectorPane.js.map +1 -0
  34. package/dist/components/ui/RowActions.d.ts +19 -0
  35. package/dist/components/ui/RowActions.d.ts.map +1 -0
  36. package/dist/components/ui/RowActions.js +16 -0
  37. package/dist/components/ui/RowActions.js.map +1 -0
  38. package/dist/components/ui/index.d.ts +4 -0
  39. package/dist/components/ui/index.d.ts.map +1 -1
  40. package/dist/components/ui/index.js +2 -0
  41. package/dist/components/ui/index.js.map +1 -1
  42. package/dist/layout/Header.js +1 -1
  43. package/dist/layout/Header.js.map +1 -1
  44. package/dist/layout/Layout.js +8 -38
  45. package/dist/layout/Layout.js.map +1 -1
  46. package/dist/lib/page-editor-service.d.ts +11 -2
  47. package/dist/lib/page-editor-service.d.ts.map +1 -1
  48. package/dist/lib/page-editor-service.js +8 -2
  49. package/dist/lib/page-editor-service.js.map +1 -1
  50. package/dist/lib/pages-service.d.ts +9 -0
  51. package/dist/lib/pages-service.d.ts.map +1 -1
  52. package/dist/lib/pages-service.js +14 -0
  53. package/dist/lib/pages-service.js.map +1 -1
  54. package/dist/lib/pages-tree.d.ts +44 -0
  55. package/dist/lib/pages-tree.d.ts.map +1 -0
  56. package/dist/lib/pages-tree.js +106 -0
  57. package/dist/lib/pages-tree.js.map +1 -0
  58. package/dist/lib/posts-service.d.ts +11 -0
  59. package/dist/lib/posts-service.d.ts.map +1 -1
  60. package/dist/lib/posts-service.js +13 -0
  61. package/dist/lib/posts-service.js.map +1 -1
  62. package/dist/lib/seo-service.d.ts +3 -0
  63. package/dist/lib/seo-service.d.ts.map +1 -1
  64. package/dist/lib/seo-service.js.map +1 -1
  65. package/dist/views/Dashboard.d.ts.map +1 -1
  66. package/dist/views/Dashboard.js +40 -3
  67. package/dist/views/Dashboard.js.map +1 -1
  68. package/dist/views/MediaBrowser.d.ts.map +1 -1
  69. package/dist/views/MediaBrowser.js +55 -2
  70. package/dist/views/MediaBrowser.js.map +1 -1
  71. package/dist/views/MediaSeoAudit.d.ts +25 -0
  72. package/dist/views/MediaSeoAudit.d.ts.map +1 -0
  73. package/dist/views/MediaSeoAudit.js +97 -0
  74. package/dist/views/MediaSeoAudit.js.map +1 -0
  75. package/dist/views/Pages/PagesListView.d.ts.map +1 -1
  76. package/dist/views/Pages/PagesListView.js +145 -16
  77. package/dist/views/Pages/PagesListView.js.map +1 -1
  78. package/dist/views/Pages/PagesTreeTable.d.ts +35 -0
  79. package/dist/views/Pages/PagesTreeTable.d.ts.map +1 -0
  80. package/dist/views/Pages/PagesTreeTable.js +28 -0
  81. package/dist/views/Pages/PagesTreeTable.js.map +1 -0
  82. package/dist/views/Posts/PostsListView.d.ts.map +1 -1
  83. package/dist/views/Posts/PostsListView.js +66 -9
  84. package/dist/views/Posts/PostsListView.js.map +1 -1
  85. package/dist/views/ScriptTags.d.ts +5 -1
  86. package/dist/views/ScriptTags.d.ts.map +1 -1
  87. package/dist/views/ScriptTags.js +32 -7
  88. package/dist/views/ScriptTags.js.map +1 -1
  89. package/dist/views/dashboard/OnboardingBlock.d.ts +29 -0
  90. package/dist/views/dashboard/OnboardingBlock.d.ts.map +1 -0
  91. package/dist/views/dashboard/OnboardingBlock.js +71 -0
  92. package/dist/views/dashboard/OnboardingBlock.js.map +1 -0
  93. package/dist/views/page-editor/PageSectionEditor.d.ts +7 -1
  94. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  95. package/dist/views/page-editor/PageSectionEditor.js +3 -3
  96. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  97. package/dist/views/page-editor/PageSettingsModal.d.ts +1 -1
  98. package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -1
  99. package/dist/views/page-editor/PageSettingsModal.js +42 -3
  100. package/dist/views/page-editor/PageSettingsModal.js.map +1 -1
  101. package/dist/views/seo/TechnicalTab.d.ts.map +1 -1
  102. package/dist/views/seo/TechnicalTab.js +15 -0
  103. package/dist/views/seo/TechnicalTab.js.map +1 -1
  104. package/package.json +3 -3
  105. package/src/AdminRoot.tsx +7 -3
  106. package/src/__tests__/components/inspector-pane.test.tsx +64 -0
  107. package/src/__tests__/lib/pages-tree.test.ts +197 -0
  108. package/src/__tests__/views/dashboard.test.tsx +7 -3
  109. package/src/__tests__/views/media-seo-audit.render.test.tsx +117 -0
  110. package/src/__tests__/views/onboarding-block.render.test.tsx +193 -0
  111. package/src/__tests__/views/pages-list-view.test.tsx +93 -1
  112. package/src/components/script-tags/ScriptTagEditorPane.tsx +474 -0
  113. package/src/components/ui/InspectorPane.tsx +129 -0
  114. package/src/components/ui/RowActions.tsx +50 -0
  115. package/src/components/ui/index.ts +12 -0
  116. package/src/layout/Header.tsx +2 -2
  117. package/src/layout/Layout.tsx +8 -80
  118. package/src/lib/page-editor-service.ts +16 -2
  119. package/src/lib/pages-service.ts +18 -0
  120. package/src/lib/pages-tree.ts +145 -0
  121. package/src/lib/posts-service.ts +26 -0
  122. package/src/lib/seo-service.ts +3 -0
  123. package/src/views/Dashboard.tsx +286 -214
  124. package/src/views/MediaBrowser.tsx +673 -512
  125. package/src/views/MediaSeoAudit.tsx +320 -0
  126. package/src/views/Pages/PagesListView.tsx +459 -185
  127. package/src/views/Pages/PagesTreeTable.tsx +339 -0
  128. package/src/views/Posts/PostsListView.tsx +135 -10
  129. package/src/views/ScriptTags.tsx +204 -129
  130. package/src/views/dashboard/OnboardingBlock.tsx +233 -0
  131. package/src/views/page-editor/PageSectionEditor.tsx +9 -2
  132. package/src/views/page-editor/PageSettingsModal.tsx +75 -2
  133. package/src/views/seo/TechnicalTab.tsx +15 -0
  134. package/dist/views/ScriptTagEditor.d.ts +0 -6
  135. package/dist/views/ScriptTagEditor.d.ts.map +0 -1
  136. package/dist/views/ScriptTagEditor.js +0 -111
  137. package/dist/views/ScriptTagEditor.js.map +0 -1
  138. package/src/views/ScriptTagEditor.tsx +0 -353
@@ -0,0 +1,193 @@
1
+ // @vitest-environment happy-dom
2
+ import { describe, expect, it, vi } from 'vitest'
3
+ import { fireEvent, render, screen } from '@testing-library/react'
4
+ import {
5
+ OnboardingBlock,
6
+ deriveOnboardingSteps,
7
+ shouldShowOnboarding,
8
+ type OnboardingSignals,
9
+ } from '../../views/dashboard/OnboardingBlock.js'
10
+
11
+ function signals(over: Partial<OnboardingSignals> = {}): OnboardingSignals {
12
+ return {
13
+ postsAndPagesCount: 0,
14
+ mediaCount: 0,
15
+ seoScore: 0,
16
+ userCount: 1,
17
+ hasApiTraffic: false,
18
+ ...over,
19
+ }
20
+ }
21
+
22
+ describe('deriveOnboardingSteps', () => {
23
+ it('marks only the workspace step done on a brand-new site', () => {
24
+ const steps = deriveOnboardingSteps(signals())
25
+ expect(steps).toHaveLength(6)
26
+ expect(steps.filter((s) => s.done).map((s) => s.id)).toEqual(['workspace'])
27
+ })
28
+
29
+ it('derives each step from its dashboard signal', () => {
30
+ const steps = deriveOnboardingSteps(
31
+ signals({
32
+ postsAndPagesCount: 3,
33
+ mediaCount: 2,
34
+ seoScore: 71,
35
+ userCount: 4,
36
+ hasApiTraffic: true,
37
+ }),
38
+ )
39
+ expect(steps.every((s) => s.done)).toBe(true)
40
+ })
41
+
42
+ it('treats a lone user and zero SEO score as incomplete steps', () => {
43
+ const steps = deriveOnboardingSteps(signals({ userCount: 1, seoScore: 0 }))
44
+ const byId = Object.fromEntries(steps.map((s) => [s.id, s.done]))
45
+ expect(byId['invite']).toBe(false)
46
+ expect(byId['seo']).toBe(false)
47
+ expect(byId['api-key']).toBe(false)
48
+ })
49
+ })
50
+
51
+ describe('shouldShowOnboarding', () => {
52
+ it('shows only when both posts+pages and media are empty', () => {
53
+ expect(shouldShowOnboarding(signals())).toBe(true)
54
+ expect(shouldShowOnboarding(signals({ postsAndPagesCount: 1 }))).toBe(false)
55
+ expect(shouldShowOnboarding(signals({ mediaCount: 1 }))).toBe(false)
56
+ })
57
+ })
58
+
59
+ describe('OnboardingBlock', () => {
60
+ it('renders the welcome banner with the derived progress count', () => {
61
+ render(<OnboardingBlock signals={signals()} onNavigate={() => {}} />)
62
+
63
+ expect(screen.getByText(/Welcome to ActuateCMS/)).toBeTruthy()
64
+ // 1 of 6 done (workspace only) on a fresh site.
65
+ expect(screen.getByText(/1\/6 steps complete/)).toBeTruthy()
66
+
67
+ const bar = screen.getByRole('progressbar', { name: 'Onboarding progress' })
68
+ expect(bar.getAttribute('aria-valuenow')).toBe('1')
69
+ expect(bar.getAttribute('aria-valuemax')).toBe('6')
70
+ })
71
+
72
+ it('updates the progress count as steps derive complete', () => {
73
+ render(
74
+ <OnboardingBlock signals={signals({ userCount: 3, seoScore: 50 })} onNavigate={() => {}} />,
75
+ )
76
+
77
+ expect(screen.getByText(/3\/6 steps complete/)).toBeTruthy()
78
+ expect(
79
+ screen
80
+ .getByRole('progressbar', { name: 'Onboarding progress' })
81
+ .getAttribute('aria-valuenow'),
82
+ ).toBe('3')
83
+ })
84
+
85
+ it('renders all six checklist labels with accessible completion state', () => {
86
+ render(<OnboardingBlock signals={signals()} onNavigate={() => {}} />)
87
+
88
+ for (const label of [
89
+ 'Create your workspace',
90
+ 'Create your first Post or Page',
91
+ 'Upload media assets',
92
+ 'Configure SEO defaults',
93
+ 'Get your API key',
94
+ 'Invite team members',
95
+ ]) {
96
+ expect(screen.getByText(label)).toBeTruthy()
97
+ }
98
+
99
+ // Completion is announced via text, not color alone.
100
+ expect(screen.getAllByText('(completed)')).toHaveLength(1)
101
+ expect(screen.getAllByText('(not completed)')).toHaveLength(5)
102
+ })
103
+
104
+ it('navigates through quick links via onNavigate', () => {
105
+ const onNavigate = vi.fn()
106
+ render(<OnboardingBlock signals={signals()} onNavigate={onNavigate} />)
107
+
108
+ fireEvent.click(screen.getByRole('button', { name: /Invite team/ }))
109
+ expect(onNavigate).toHaveBeenCalledWith('/settings?tab=users')
110
+
111
+ fireEvent.click(screen.getByRole('button', { name: /API Reference/ }))
112
+ expect(onNavigate).toHaveBeenCalledWith('/settings?tab=api-keys')
113
+
114
+ fireEvent.click(screen.getByRole('button', { name: /SEO defaults/ }))
115
+ expect(onNavigate).toHaveBeenCalledWith('/settings?tab=seo')
116
+ })
117
+ })
118
+
119
+ // ── Dashboard integration: visibility rule ─────────────────────────────────
120
+
121
+ const responses: Record<string, unknown> = {}
122
+ const emptyResult = { data: null, loading: false, error: null, exhausted: false, refetch: () => {} }
123
+
124
+ vi.mock('../../lib/useApiData.js', () => ({
125
+ useApiData: (endpoint: string | null) => responses[endpoint ?? ''] ?? emptyResult,
126
+ }))
127
+
128
+ const { Dashboard } = await import('../../views/Dashboard.js')
129
+
130
+ const baseStats = {
131
+ totalDocuments: 0,
132
+ totalMedia: 0,
133
+ totalUsers: 1,
134
+ formCount: 0,
135
+ avgSeoScore: 0,
136
+ webhookCount: 0,
137
+ webhookActiveCount: 0,
138
+ collectionCounts: {} as Record<string, number>,
139
+ statusCounts: {},
140
+ recentDocuments: [] as unknown[],
141
+ }
142
+
143
+ function statsResult(over: Partial<typeof baseStats> = {}) {
144
+ return {
145
+ data: { ...baseStats, ...over },
146
+ loading: false,
147
+ error: null,
148
+ exhausted: false,
149
+ refetch: () => {},
150
+ }
151
+ }
152
+
153
+ describe('Dashboard onboarding visibility', () => {
154
+ it('replaces the activity grid with onboarding when the site is empty', () => {
155
+ responses['/stats'] = statsResult()
156
+
157
+ render(<Dashboard config={{ collections: [] }} session={{}} onNavigate={() => {}} />)
158
+
159
+ expect(screen.getByText(/Welcome to ActuateCMS/)).toBeTruthy()
160
+ expect(screen.queryByText('Recent Activity')).toBeNull()
161
+ expect(screen.queryByText('Publishing Queue')).toBeNull()
162
+ expect(screen.queryByText('Content Health')).toBeNull()
163
+ // The Delivery API card below the block stays.
164
+ expect(screen.getByText('Delivery API')).toBeTruthy()
165
+ delete responses['/stats']
166
+ })
167
+
168
+ it('renders the normal dashboard when posts or pages exist', () => {
169
+ responses['/stats'] = statsResult({ collectionCounts: { posts: 2 } })
170
+
171
+ render(
172
+ <Dashboard
173
+ config={{ collections: [{ slug: 'posts', type: 'post' }] }}
174
+ session={{}}
175
+ onNavigate={() => {}}
176
+ />,
177
+ )
178
+
179
+ expect(screen.queryByText(/Welcome to ActuateCMS/)).toBeNull()
180
+ expect(screen.getByText('Recent Activity')).toBeTruthy()
181
+ delete responses['/stats']
182
+ })
183
+
184
+ it('renders the normal dashboard when only media exists', () => {
185
+ responses['/stats'] = statsResult({ totalMedia: 5 })
186
+
187
+ render(<Dashboard config={{ collections: [] }} session={{}} onNavigate={() => {}} />)
188
+
189
+ expect(screen.queryByText(/Welcome to ActuateCMS/)).toBeNull()
190
+ expect(screen.getByText('Recent Activity')).toBeTruthy()
191
+ delete responses['/stats']
192
+ })
193
+ })
@@ -1,6 +1,6 @@
1
1
  // @vitest-environment happy-dom
2
2
  import { afterEach, describe, expect, it, vi } from 'vitest'
3
- import { render, screen, fireEvent, waitFor } from '@testing-library/react'
3
+ import { cleanup, render, screen, fireEvent, waitFor } from '@testing-library/react'
4
4
 
5
5
  // Keep the pure shaping helpers (applyPagesQuery/groupPages) real — the
6
6
  // list view relies on them for in-memory filtering — and stub only the
@@ -14,11 +14,24 @@ vi.mock('../../lib/pages-service.js', async (importOriginal) => {
14
14
  }
15
15
  })
16
16
 
17
+ // This happy-dom environment doesn't expose a functional localStorage, which
18
+ // the view reads for the persisted List/Tree mode.
19
+ const storage = new Map<string, string>()
20
+ vi.stubGlobal('localStorage', {
21
+ getItem: (k: string) => storage.get(k) ?? null,
22
+ setItem: (k: string, v: string) => void storage.set(k, String(v)),
23
+ removeItem: (k: string) => void storage.delete(k),
24
+ clear: () => storage.clear(),
25
+ })
26
+
17
27
  const { PagesListView } = await import('../../views/Pages/PagesListView.js')
18
28
  import type { Page, PageTag, PagesDataset } from '../../lib/pages-service.js'
19
29
 
20
30
  afterEach(() => {
31
+ cleanup()
21
32
  fetchPagesDataset.mockReset()
33
+ // The view persists the List/Tree choice — clear it so tests stay isolated.
34
+ storage.clear()
22
35
  })
23
36
 
24
37
  function page(id: string, over: Partial<Page> = {}): Page {
@@ -117,3 +130,82 @@ describe('PagesListView', () => {
117
130
  expect(onNavigate).toHaveBeenCalledWith('/pages/about/edit')
118
131
  })
119
132
  })
133
+
134
+ describe('PagesListView tree mode', () => {
135
+ function hierarchyDataset() {
136
+ return dataset({
137
+ pages: [
138
+ page('about', { title: 'About', path: '/about' }),
139
+ page('team', { title: 'Team', path: '/about/team' }),
140
+ page('blog', { title: 'Blog', path: '/blog' }),
141
+ ],
142
+ })
143
+ }
144
+
145
+ it('nests children and toggles expand/collapse', async () => {
146
+ fetchPagesDataset.mockResolvedValue(hierarchyDataset())
147
+ render(<PagesListView onNavigate={() => {}} />)
148
+ await screen.findByText('About')
149
+
150
+ fireEvent.click(screen.getByRole('button', { name: 'Tree view' }))
151
+
152
+ // Root with children starts expanded → child visible; tree subtitle.
153
+ expect(screen.getByText(/3 pages total/)).toBeTruthy()
154
+ expect(screen.getByText('Team')).toBeTruthy()
155
+
156
+ fireEvent.click(screen.getByRole('button', { name: 'Collapse About' }))
157
+ expect(screen.queryByText('Team')).toBeNull()
158
+
159
+ fireEvent.click(screen.getByRole('button', { name: 'Expand About' }))
160
+ expect(screen.getByText('Team')).toBeTruthy()
161
+ })
162
+
163
+ it('flattens the tree while a status filter is active (expand state ignored)', async () => {
164
+ fetchPagesDataset.mockResolvedValue(
165
+ dataset({
166
+ pages: [
167
+ page('about', { title: 'About', path: '/about' }),
168
+ page('team', { title: 'Team', path: '/about/team', status: 'DRAFT' }),
169
+ ],
170
+ }),
171
+ )
172
+ render(<PagesListView onNavigate={() => {}} />)
173
+ await screen.findByText('About')
174
+
175
+ fireEvent.click(screen.getByRole('button', { name: 'Tree view' }))
176
+ fireEvent.click(screen.getByRole('button', { name: 'Collapse About' }))
177
+ expect(screen.queryByText('Team')).toBeNull()
178
+
179
+ // DRAFT filter → the collapsed child surfaces as a depth-0 match.
180
+ fireEvent.change(screen.getByDisplayValue('All statuses'), { target: { value: 'DRAFT' } })
181
+ expect(await screen.findByText('Team')).toBeTruthy()
182
+ expect(screen.queryByText('About')).toBeNull()
183
+ // Flattened rows carry no expand affordance.
184
+ expect(screen.queryByRole('button', { name: /Expand|Collapse/ })).toBeNull()
185
+ })
186
+
187
+ it('routes "Add sub-page" to /pages/new with the parent preselected', async () => {
188
+ const onNavigate = vi.fn()
189
+ fetchPagesDataset.mockResolvedValue(hierarchyDataset())
190
+ render(<PagesListView onNavigate={onNavigate} />)
191
+ await screen.findByText('About')
192
+
193
+ fireEvent.click(screen.getByRole('button', { name: 'Tree view' }))
194
+ fireEvent.click(screen.getByRole('button', { name: 'Add sub-page under "Blog"' }))
195
+ expect(onNavigate).toHaveBeenCalledWith('/pages/new?parent=blog')
196
+ })
197
+
198
+ it('remembers the tree preference via localStorage', async () => {
199
+ fetchPagesDataset.mockResolvedValue(hierarchyDataset())
200
+ const first = render(<PagesListView onNavigate={() => {}} />)
201
+ await screen.findByText('About')
202
+ fireEvent.click(screen.getByRole('button', { name: 'Tree view' }))
203
+ expect(storage.get('actuate.pages.viewMode')).toBe('tree')
204
+ first.unmount()
205
+
206
+ fetchPagesDataset.mockResolvedValue(hierarchyDataset())
207
+ render(<PagesListView onNavigate={() => {}} />)
208
+ // Mounts straight into tree mode → tree subtitle.
209
+ expect(await screen.findByText(/3 pages total/)).toBeTruthy()
210
+ })
211
+ })