@actuate-media/cms-admin 0.65.0 → 0.66.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 (53) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/actuate-admin.css +1 -1
  3. package/dist/components/ui/RowActions.d.ts +19 -0
  4. package/dist/components/ui/RowActions.d.ts.map +1 -0
  5. package/dist/components/ui/RowActions.js +16 -0
  6. package/dist/components/ui/RowActions.js.map +1 -0
  7. package/dist/components/ui/index.d.ts +2 -0
  8. package/dist/components/ui/index.d.ts.map +1 -1
  9. package/dist/components/ui/index.js +1 -0
  10. package/dist/components/ui/index.js.map +1 -1
  11. package/dist/layout/Header.js +1 -1
  12. package/dist/layout/Header.js.map +1 -1
  13. package/dist/layout/Layout.js +8 -38
  14. package/dist/layout/Layout.js.map +1 -1
  15. package/dist/lib/pages-service.d.ts +9 -0
  16. package/dist/lib/pages-service.d.ts.map +1 -1
  17. package/dist/lib/pages-service.js +14 -0
  18. package/dist/lib/pages-service.js.map +1 -1
  19. package/dist/lib/posts-service.d.ts +11 -0
  20. package/dist/lib/posts-service.d.ts.map +1 -1
  21. package/dist/lib/posts-service.js +13 -0
  22. package/dist/lib/posts-service.js.map +1 -1
  23. package/dist/lib/seo-service.d.ts +3 -0
  24. package/dist/lib/seo-service.d.ts.map +1 -1
  25. package/dist/lib/seo-service.js.map +1 -1
  26. package/dist/views/Dashboard.d.ts.map +1 -1
  27. package/dist/views/Dashboard.js +9 -1
  28. package/dist/views/Dashboard.js.map +1 -1
  29. package/dist/views/MediaBrowser.d.ts.map +1 -1
  30. package/dist/views/MediaBrowser.js +28 -1
  31. package/dist/views/MediaBrowser.js.map +1 -1
  32. package/dist/views/Pages/PagesListView.d.ts.map +1 -1
  33. package/dist/views/Pages/PagesListView.js +59 -10
  34. package/dist/views/Pages/PagesListView.js.map +1 -1
  35. package/dist/views/Posts/PostsListView.d.ts.map +1 -1
  36. package/dist/views/Posts/PostsListView.js +66 -9
  37. package/dist/views/Posts/PostsListView.js.map +1 -1
  38. package/dist/views/seo/TechnicalTab.d.ts.map +1 -1
  39. package/dist/views/seo/TechnicalTab.js +15 -0
  40. package/dist/views/seo/TechnicalTab.js.map +1 -1
  41. package/package.json +3 -3
  42. package/src/components/ui/RowActions.tsx +50 -0
  43. package/src/components/ui/index.ts +2 -0
  44. package/src/layout/Header.tsx +2 -2
  45. package/src/layout/Layout.tsx +8 -80
  46. package/src/lib/pages-service.ts +18 -0
  47. package/src/lib/posts-service.ts +26 -0
  48. package/src/lib/seo-service.ts +3 -0
  49. package/src/views/Dashboard.tsx +48 -18
  50. package/src/views/MediaBrowser.tsx +32 -1
  51. package/src/views/Pages/PagesListView.tsx +115 -10
  52. package/src/views/Posts/PostsListView.tsx +135 -10
  53. package/src/views/seo/TechnicalTab.tsx +15 -0
@@ -6,7 +6,10 @@ import {
6
6
  ChevronLeft,
7
7
  ChevronRight,
8
8
  ChevronUp,
9
+ Copy,
9
10
  Loader2,
11
+ PenLine,
12
+ Plus,
10
13
  Search,
11
14
  Settings,
12
15
  Trash2,
@@ -16,6 +19,8 @@ import { toast } from 'sonner'
16
19
  import {
17
20
  bulkDeletePosts,
18
21
  bulkUpdatePosts,
22
+ deletePost,
23
+ duplicatePost,
19
24
  fetchPosts,
20
25
  type Post,
21
26
  type PostStatus,
@@ -29,6 +34,7 @@ import {
29
34
  paletteFor,
30
35
  } from '../../components/posts/badges.js'
31
36
  import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'
37
+ import { RowActionButton, RowActions } from '../../components/ui/RowActions.js'
32
38
 
33
39
  /**
34
40
  * All Posts management surface — table with type tabs, controls row,
@@ -69,6 +75,7 @@ export function PostsListView({ onNavigate }: PostsListViewProps) {
69
75
  const [error, setError] = useState<string | null>(null)
70
76
  const [refreshToken, setRefreshToken] = useState(0)
71
77
  const [pendingBulk, setPendingBulk] = useState<null | 'delete' | 'publish' | 'draft'>(null)
78
+ const [rowDelete, setRowDelete] = useState<Post | null>(null)
72
79
 
73
80
  const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
74
81
  useEffect(() => {
@@ -132,6 +139,14 @@ export function PostsListView({ onNavigate }: PostsListViewProps) {
132
139
  () => (data?.types ?? []).reduce((sum, t) => sum + t.postCount, 0),
133
140
  [data?.types],
134
141
  )
142
+
143
+ // Header CTA target: one Post Type → its editor, several → the picker,
144
+ // none → the Post Type setup instructions.
145
+ const newPostPath = useMemo(() => {
146
+ const typeList = data?.types ?? []
147
+ if (typeList.length === 1) return `/${typeList[0]!.slug}/new`
148
+ return typeList.length > 1 ? '/posts/new' : '/posts/types/new'
149
+ }, [data?.types])
135
150
  const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE))
136
151
  const posts = data?.posts ?? []
137
152
  const types = data?.types ?? []
@@ -210,16 +225,69 @@ export function PostsListView({ onNavigate }: PostsListViewProps) {
210
225
  setRefreshToken((t) => t + 1)
211
226
  }
212
227
 
228
+ // ─── Row actions ──────────────────────────────────────────────────
229
+
230
+ const runDuplicate = async (p: Post) => {
231
+ const promise = duplicatePost(p.postTypeId, p.id).then((r) => {
232
+ if (r.error) throw new Error(r.error)
233
+ return r
234
+ })
235
+ toast.promise(promise, {
236
+ loading: `Duplicating "${p.title}"…`,
237
+ success: `"${p.title}" duplicated`,
238
+ error: (e: Error) => e.message || 'Duplicate failed',
239
+ })
240
+ try {
241
+ await promise
242
+ setRefreshToken((t) => t + 1)
243
+ } catch {
244
+ /* surfaced via toast.promise */
245
+ }
246
+ }
247
+
248
+ const runRowDelete = async (p: Post) => {
249
+ const promise = deletePost(p.postTypeId, p.id).then((r) => {
250
+ if (r.error) throw new Error(r.error)
251
+ })
252
+ toast.promise(promise, {
253
+ loading: `Deleting "${p.title}"…`,
254
+ success: `"${p.title}" moved to trash`,
255
+ error: (e: Error) => e.message || 'Delete failed',
256
+ })
257
+ try {
258
+ await promise
259
+ setSelected((s) => {
260
+ const next = new Set(s)
261
+ next.delete(p.id)
262
+ return next
263
+ })
264
+ setRefreshToken((t) => t + 1)
265
+ } catch {
266
+ /* surfaced via toast.promise */
267
+ }
268
+ }
269
+
213
270
  // ─── Render ───────────────────────────────────────────────────────
214
271
 
215
272
  return (
216
273
  <div className="flex h-full flex-col gap-4 p-4 sm:p-6">
217
- {/* Header */}
218
- <div className="flex flex-col gap-1">
219
- <h1 className="text-foreground">Posts</h1>
220
- <p className="text-muted-foreground text-sm">
221
- {totalAll} total post{totalAll === 1 ? '' : 's'} across all types
222
- </p>
274
+ {/* Header — the "+ New Post" primary action sits on the right, per
275
+ the design's `.page-hdr` / `.hdr-r` layout. */}
276
+ <div className="flex flex-wrap items-start justify-between gap-3">
277
+ <div className="flex flex-col gap-1">
278
+ <h1 className="text-foreground">Posts</h1>
279
+ <p className="text-muted-foreground text-sm">
280
+ {totalAll} total post{totalAll === 1 ? '' : 's'} across all types
281
+ </p>
282
+ </div>
283
+ <button
284
+ type="button"
285
+ onClick={() => onNavigate(newPostPath)}
286
+ className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium shadow-sm transition-colors focus:outline-none focus-visible:ring-2"
287
+ >
288
+ <Plus className="h-3.5 w-3.5" aria-hidden />
289
+ New Post
290
+ </button>
223
291
  </div>
224
292
 
225
293
  {/* Type tabs */}
@@ -269,6 +337,7 @@ export function PostsListView({ onNavigate }: PostsListViewProps) {
269
337
  <col style={{ width: '130px' }} />
270
338
  <col style={{ width: '120px' }} />
271
339
  <col style={{ width: '90px' }} />
340
+ <col style={{ width: '110px' }} />
272
341
  </colgroup>
273
342
  <thead className="border-border bg-muted/40 border-b">
274
343
  <tr className="text-muted-foreground text-xs">
@@ -335,13 +404,16 @@ export function PostsListView({ onNavigate }: PostsListViewProps) {
335
404
  onSort={handleSort}
336
405
  />
337
406
  </th>
407
+ <th scope="col" className="px-3 py-2.5 text-right">
408
+ <span className="sr-only">Actions</span>
409
+ </th>
338
410
  </tr>
339
411
  </thead>
340
412
  <tbody>
341
413
  {loading && posts.length === 0 && <LoadingRows />}
342
414
  {!loading && error && (
343
415
  <tr>
344
- <td colSpan={7} className="px-3 py-12 text-center">
416
+ <td colSpan={8} className="px-3 py-12 text-center">
345
417
  <div className="text-foreground mb-2 text-sm font-medium">{error}</div>
346
418
  <button
347
419
  onClick={() => setRefreshToken((t) => t + 1)}
@@ -368,6 +440,8 @@ export function PostsListView({ onNavigate }: PostsListViewProps) {
368
440
  selected={selected.has(p.id)}
369
441
  onSelect={() => toggleOne(p.id)}
370
442
  onOpen={() => onNavigate(`/posts/${p.postTypeId}/${p.id}/edit`)}
443
+ onDuplicate={() => void runDuplicate(p)}
444
+ onDelete={() => setRowDelete(p)}
371
445
  />
372
446
  ))}
373
447
  </tbody>
@@ -384,6 +458,19 @@ export function PostsListView({ onNavigate }: PostsListViewProps) {
384
458
  />
385
459
  </div>
386
460
 
461
+ <ConfirmDialog
462
+ open={rowDelete !== null}
463
+ title={`Delete "${rowDelete?.title ?? ''}"?`}
464
+ description="The post moves to trash and can be restored for 30 days."
465
+ confirmLabel="Delete"
466
+ destructive
467
+ onClose={() => setRowDelete(null)}
468
+ onConfirm={() => {
469
+ if (rowDelete) void runRowDelete(rowDelete)
470
+ setRowDelete(null)
471
+ }}
472
+ />
473
+
387
474
  <ConfirmDialog
388
475
  open={pendingBulk === 'delete'}
389
476
  title={`Delete ${selected.size} post${selected.size === 1 ? '' : 's'}?`}
@@ -669,11 +756,15 @@ function PostRow({
669
756
  selected,
670
757
  onSelect,
671
758
  onOpen,
759
+ onDuplicate,
760
+ onDelete,
672
761
  }: {
673
762
  post: Post
674
763
  selected: boolean
675
764
  onSelect: () => void
676
765
  onOpen: () => void
766
+ onDuplicate: () => void
767
+ onDelete: () => void
677
768
  }) {
678
769
  const p = paletteFor(post.postTypeAccentColor)
679
770
  return (
@@ -693,7 +784,19 @@ function PostRow({
693
784
  </td>
694
785
  <td className="px-3 py-3">
695
786
  <button type="button" onClick={onOpen} className="flex w-full items-center gap-3 text-left">
696
- <span aria-hidden className={`block h-9 w-9 shrink-0 rounded-md ${p.bg.split(' ')[0]}`} />
787
+ {post.imageUrl ? (
788
+ <img
789
+ src={post.imageUrl}
790
+ alt=""
791
+ loading="lazy"
792
+ className="block h-9 w-9 shrink-0 rounded-md object-cover"
793
+ />
794
+ ) : (
795
+ <span
796
+ aria-hidden
797
+ className={`block h-9 w-9 shrink-0 rounded-md ${p.bg.split(' ')[0]}`}
798
+ />
799
+ )}
697
800
  <span className="min-w-0">
698
801
  <span className="text-foreground group-hover:text-primary block truncate text-sm font-medium">
699
802
  {post.title}
@@ -727,6 +830,28 @@ function PostRow({
727
830
  <td className="px-3 py-3">
728
831
  <SeoScoreIndicator score={post.seoScore} />
729
832
  </td>
833
+ <td className="px-3 py-3">
834
+ <RowActions>
835
+ <RowActionButton aria-label={`Edit "${post.title}"`} title="Edit" onClick={onOpen}>
836
+ <PenLine size={13} aria-hidden />
837
+ </RowActionButton>
838
+ <RowActionButton
839
+ aria-label={`Duplicate "${post.title}"`}
840
+ title="Duplicate"
841
+ onClick={onDuplicate}
842
+ >
843
+ <Copy size={12} aria-hidden />
844
+ </RowActionButton>
845
+ <RowActionButton
846
+ aria-label={`Delete "${post.title}"`}
847
+ title="Delete"
848
+ danger
849
+ onClick={onDelete}
850
+ >
851
+ <Trash2 size={12} aria-hidden />
852
+ </RowActionButton>
853
+ </RowActions>
854
+ </td>
730
855
  </tr>
731
856
  )
732
857
  }
@@ -737,7 +862,7 @@ function LoadingRows() {
737
862
  <>
738
863
  {rows.map((_, i) => (
739
864
  <tr key={i} className="border-border border-b last:border-b-0">
740
- {Array.from({ length: 7 }).map((__, j) => (
865
+ {Array.from({ length: 8 }).map((__, j) => (
741
866
  <td key={j} className="px-3 py-3">
742
867
  <div className="bg-muted h-4 w-full animate-pulse rounded" />
743
868
  </td>
@@ -759,7 +884,7 @@ function EmptyRow({
759
884
  }) {
760
885
  return (
761
886
  <tr>
762
- <td colSpan={7} className="px-3 py-16 text-center">
887
+ <td colSpan={8} className="px-3 py-16 text-center">
763
888
  <div className="text-foreground mb-2 text-sm font-medium">
764
889
  {filtersActive ? 'No posts match the current filters.' : 'No posts yet.'}
765
890
  </div>
@@ -40,6 +40,21 @@ const CRAWL_TOGGLES: { key: keyof CrawlSettings; label: string; help: string }[]
40
40
  label: 'Include posts in sitemap',
41
41
  help: 'Add published posts to the sitemap.',
42
42
  },
43
+ {
44
+ key: 'sitemapEnabled',
45
+ label: 'Enable XML sitemap',
46
+ help: 'Serve the sitemap at /sitemap.xml.',
47
+ },
48
+ {
49
+ key: 'autoRegenerateSitemapOnPublish',
50
+ label: 'Auto-regenerate sitemap',
51
+ help: 'Rebuild the sitemap automatically when content is published.',
52
+ },
53
+ {
54
+ key: 'pingSearchEnginesOnPublish',
55
+ label: 'Ping search engines on publish',
56
+ help: 'Notify Google and Bing when the sitemap changes (production only).',
57
+ },
43
58
  ]
44
59
 
45
60
  function Toggle({