@actuate-media/cms-admin 0.16.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.
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +1 -1
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/lib/useApiData.test.d.ts +2 -0
- package/dist/__tests__/lib/useApiData.test.d.ts.map +1 -0
- package/dist/__tests__/lib/useApiData.test.js +84 -0
- package/dist/__tests__/lib/useApiData.test.js.map +1 -0
- package/dist/__tests__/views/dashboard.test.d.ts +2 -0
- package/dist/__tests__/views/dashboard.test.d.ts.map +1 -0
- package/dist/__tests__/views/dashboard.test.js +138 -0
- package/dist/__tests__/views/dashboard.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +50 -17
- package/dist/views/Dashboard.js.map +1 -1
- package/package.json +1 -1
- package/src/AdminRoot.tsx +1 -0
- package/src/__tests__/lib/useApiData.test.ts +105 -0
- package/src/__tests__/views/dashboard.test.tsx +180 -0
- package/src/views/Dashboard.tsx +127 -86
package/src/views/Dashboard.tsx
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* >= 1024 (desktop): stat cards 5-col, main grid is `1fr 320px`
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { useMemo, useState } from 'react'
|
|
18
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
19
19
|
import {
|
|
20
20
|
FileText,
|
|
21
21
|
File as FileIcon,
|
|
@@ -119,6 +119,24 @@ function collectionLabel(col: CollectionMeta, plural = true): string {
|
|
|
119
119
|
return col.labels?.singular ?? fallback
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Resolve the correct editor route for a recent/queued document.
|
|
124
|
+
*
|
|
125
|
+
* Post-type documents MUST open in the section-based post editor
|
|
126
|
+
* (`/posts/:type/:id/edit`). Routing them through the generic
|
|
127
|
+
* `/:collection/:id` handler lands them in the legacy `DocumentEdit`
|
|
128
|
+
* view, which has no concept of the section model — opening + saving
|
|
129
|
+
* there silently strips a post's `sections` data. The canonical Pages
|
|
130
|
+
* collection opens in the full page editor (which wires up preview).
|
|
131
|
+
* Everything else falls through to the existing generic collection
|
|
132
|
+
* route, which already resolves correctly.
|
|
133
|
+
*/
|
|
134
|
+
function editPathForDoc(col: CollectionMeta | undefined, collection: string, id: string): string {
|
|
135
|
+
if (col?.type === 'post') return `/posts/${collection}/${id}/edit`
|
|
136
|
+
if (collection === 'pages') return `/pages/${id}/edit`
|
|
137
|
+
return `/${collection}/${id}`
|
|
138
|
+
}
|
|
139
|
+
|
|
122
140
|
function relativeTime(dateStr: string): string {
|
|
123
141
|
const then = new Date(dateStr).getTime()
|
|
124
142
|
const diff = Date.now() - then
|
|
@@ -252,8 +270,18 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
252
270
|
const { data: contentHealthData, loading: contentHealthLoading } =
|
|
253
271
|
useApiData<ContentHealthData>('/stats/content-health')
|
|
254
272
|
|
|
255
|
-
|
|
256
|
-
|
|
273
|
+
// A slow ticker (60s) so the greeting, calendar date, and "x minutes
|
|
274
|
+
// ago" relative times stay correct on a dashboard left open for hours
|
|
275
|
+
// — without it, a tab opened at 11:58am would read "Good morning" all
|
|
276
|
+
// afternoon and show stale activity timestamps.
|
|
277
|
+
const [now, setNow] = useState(() => Date.now())
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
const id = setInterval(() => setNow(Date.now()), 60_000)
|
|
280
|
+
return () => clearInterval(id)
|
|
281
|
+
}, [])
|
|
282
|
+
|
|
283
|
+
const greeting = useMemo(() => timeOfDayGreeting(), [now])
|
|
284
|
+
const dateStr = useMemo(() => todayDateString(), [now])
|
|
257
285
|
const userName = useMemo(() => firstNameForGreeting(session), [session])
|
|
258
286
|
|
|
259
287
|
const collections = useMemo(() => resolveCollections(config), [config])
|
|
@@ -396,9 +424,12 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
396
424
|
relTime: relativeTime(d.updatedAt),
|
|
397
425
|
avatar: authorAvatar(d.author),
|
|
398
426
|
statusInfo: statusBadge(d.status),
|
|
427
|
+
editPath: editPathForDoc(col, d.collection, d.id),
|
|
399
428
|
}
|
|
400
429
|
})
|
|
401
|
-
|
|
430
|
+
// `now` keeps the relative timestamps fresh on a long-open tab.
|
|
431
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
432
|
+
}, [stats, collections, activityLimit, now])
|
|
402
433
|
|
|
403
434
|
// ── Publishing queue (scheduled docs only) ──────────────────────────────
|
|
404
435
|
const publishQueue = useMemo(() => {
|
|
@@ -415,9 +446,11 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
415
446
|
type: col ? collectionLabel(col, false) : d.collection,
|
|
416
447
|
date: relativeTime(d.updatedAt),
|
|
417
448
|
author: d.author,
|
|
449
|
+
editPath: editPathForDoc(col, d.collection, d.id),
|
|
418
450
|
}
|
|
419
451
|
})
|
|
420
|
-
|
|
452
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
453
|
+
}, [stats, collections, now])
|
|
421
454
|
|
|
422
455
|
// ── Content health (sourced from /stats/content-health) ─────────────────
|
|
423
456
|
// Persisted by the nightly content-health cron. The four issue rows
|
|
@@ -618,12 +651,14 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
618
651
|
<h2 className="text-foreground text-sm font-semibold">Recent Activity</h2>
|
|
619
652
|
<p className="text-muted-foreground mt-0.5 text-xs">Last 7 days across all content</p>
|
|
620
653
|
</div>
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
654
|
+
{(stats?.recentDocuments?.length ?? 0) > 8 && (
|
|
655
|
+
<button
|
|
656
|
+
className="text-xs font-medium text-violet-600 hover:underline dark:text-violet-400"
|
|
657
|
+
onClick={() => setActivityLimit((n) => (n >= 20 ? 8 : 20))}
|
|
658
|
+
>
|
|
659
|
+
{activityLimit >= 20 ? 'Show less' : 'Show more'}
|
|
660
|
+
</button>
|
|
661
|
+
)}
|
|
627
662
|
</header>
|
|
628
663
|
{activity.length === 0 ? (
|
|
629
664
|
<EmptyState
|
|
@@ -634,38 +669,40 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
634
669
|
) : (
|
|
635
670
|
<ul role="list" className="divide-border divide-y">
|
|
636
671
|
{activity.map((it) => (
|
|
637
|
-
<li
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
<div
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
<
|
|
652
|
-
<
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
<
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
672
|
+
<li key={it.id}>
|
|
673
|
+
<button
|
|
674
|
+
type="button"
|
|
675
|
+
onClick={() => nav(it.editPath)}
|
|
676
|
+
className="hover:bg-accent/50 focus-visible:bg-accent/50 w-full cursor-pointer px-4 py-3 text-left transition-colors"
|
|
677
|
+
>
|
|
678
|
+
<div className="flex min-w-0 items-start gap-3">
|
|
679
|
+
<div
|
|
680
|
+
className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[11px] font-bold text-white"
|
|
681
|
+
style={{ background: it.avatar.color }}
|
|
682
|
+
aria-hidden
|
|
683
|
+
>
|
|
684
|
+
{it.avatar.initials}
|
|
685
|
+
</div>
|
|
686
|
+
<div className="min-w-0 flex-1">
|
|
687
|
+
<p className="text-foreground truncate text-sm leading-snug">
|
|
688
|
+
<span className="font-semibold">
|
|
689
|
+
“{it.title || 'Untitled'}”
|
|
690
|
+
</span>{' '}
|
|
691
|
+
<span className="text-muted-foreground">— {it.typeLabel}</span>
|
|
692
|
+
</p>
|
|
693
|
+
<div className="text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-[11px]">
|
|
694
|
+
<span
|
|
695
|
+
className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium ${it.statusInfo.cls}`}
|
|
696
|
+
>
|
|
697
|
+
{it.statusInfo.label}
|
|
698
|
+
</span>
|
|
699
|
+
<span className="max-w-[120px] truncate">{it.author}</span>
|
|
700
|
+
<span aria-hidden>·</span>
|
|
701
|
+
<span>{it.relTime}</span>
|
|
702
|
+
</div>
|
|
666
703
|
</div>
|
|
667
704
|
</div>
|
|
668
|
-
</
|
|
705
|
+
</button>
|
|
669
706
|
</li>
|
|
670
707
|
))}
|
|
671
708
|
</ul>
|
|
@@ -691,7 +728,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
691
728
|
// link to the first scheduled item — that's where authors
|
|
692
729
|
// typically need to land to reschedule or cancel.
|
|
693
730
|
const first = publishQueue[0]
|
|
694
|
-
if (first) nav(
|
|
731
|
+
if (first) nav(first.editPath)
|
|
695
732
|
}}
|
|
696
733
|
>
|
|
697
734
|
Manage
|
|
@@ -707,21 +744,23 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
707
744
|
) : (
|
|
708
745
|
<ul role="list" className="divide-border divide-y">
|
|
709
746
|
{publishQueue.map((q) => (
|
|
710
|
-
<li
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
<
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
747
|
+
<li key={q.id}>
|
|
748
|
+
<button
|
|
749
|
+
type="button"
|
|
750
|
+
onClick={() => nav(q.editPath)}
|
|
751
|
+
className="hover:bg-accent/50 focus-visible:bg-accent/50 flex w-full min-w-0 cursor-pointer items-center gap-2.5 px-4 py-2.5 text-left transition-colors"
|
|
752
|
+
>
|
|
753
|
+
<span className="h-2 w-2 shrink-0 rounded-full bg-amber-500" aria-hidden />
|
|
754
|
+
<div className="min-w-0 flex-1">
|
|
755
|
+
<p className="text-foreground truncate text-sm font-medium">{q.title}</p>
|
|
756
|
+
<p className="text-muted-foreground truncate text-[11px]">
|
|
757
|
+
{q.date} · {q.author}
|
|
758
|
+
</p>
|
|
759
|
+
</div>
|
|
760
|
+
<span className="border-border bg-background text-muted-foreground shrink-0 rounded border px-1.5 py-0.5 text-[10px] capitalize">
|
|
761
|
+
{q.type}
|
|
762
|
+
</span>
|
|
763
|
+
</button>
|
|
725
764
|
</li>
|
|
726
765
|
))}
|
|
727
766
|
</ul>
|
|
@@ -786,36 +825,38 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
786
825
|
) : (
|
|
787
826
|
<ul role="list" className="divide-border divide-y">
|
|
788
827
|
{contentHealth.issues.map((iss, i) => (
|
|
789
|
-
<li
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
<span
|
|
795
|
-
className={`h-1.5 w-1.5 shrink-0 rounded-full ${
|
|
796
|
-
iss.tone === 'err'
|
|
797
|
-
? 'bg-red-500'
|
|
798
|
-
: iss.tone === 'warn'
|
|
799
|
-
? 'bg-amber-500'
|
|
800
|
-
: 'bg-muted-foreground'
|
|
801
|
-
}`}
|
|
802
|
-
aria-hidden
|
|
803
|
-
/>
|
|
804
|
-
<span className="text-muted-foreground flex-1 truncate text-xs">
|
|
805
|
-
{iss.label}
|
|
806
|
-
</span>
|
|
807
|
-
<span
|
|
808
|
-
className={`shrink-0 text-sm font-semibold ${
|
|
809
|
-
iss.tone === 'err'
|
|
810
|
-
? 'text-red-600 dark:text-red-400'
|
|
811
|
-
: iss.tone === 'warn'
|
|
812
|
-
? 'text-amber-600 dark:text-amber-400'
|
|
813
|
-
: 'text-muted-foreground'
|
|
814
|
-
}`}
|
|
828
|
+
<li key={i}>
|
|
829
|
+
<button
|
|
830
|
+
type="button"
|
|
831
|
+
onClick={() => nav('/seo')}
|
|
832
|
+
className="hover:bg-accent/50 focus-visible:bg-accent/50 flex w-full items-center gap-2.5 px-4 py-2 text-left transition-colors"
|
|
815
833
|
>
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
834
|
+
<span
|
|
835
|
+
className={`h-1.5 w-1.5 shrink-0 rounded-full ${
|
|
836
|
+
iss.tone === 'err'
|
|
837
|
+
? 'bg-red-500'
|
|
838
|
+
: iss.tone === 'warn'
|
|
839
|
+
? 'bg-amber-500'
|
|
840
|
+
: 'bg-muted-foreground'
|
|
841
|
+
}`}
|
|
842
|
+
aria-hidden
|
|
843
|
+
/>
|
|
844
|
+
<span className="text-muted-foreground flex-1 truncate text-xs">
|
|
845
|
+
{iss.label}
|
|
846
|
+
</span>
|
|
847
|
+
<span
|
|
848
|
+
className={`shrink-0 text-sm font-semibold ${
|
|
849
|
+
iss.tone === 'err'
|
|
850
|
+
? 'text-red-600 dark:text-red-400'
|
|
851
|
+
: iss.tone === 'warn'
|
|
852
|
+
? 'text-amber-600 dark:text-amber-400'
|
|
853
|
+
: 'text-muted-foreground'
|
|
854
|
+
}`}
|
|
855
|
+
>
|
|
856
|
+
{iss.count}
|
|
857
|
+
</span>
|
|
858
|
+
<ChevronRight className="text-muted-foreground/60 h-3.5 w-3.5 shrink-0" />
|
|
859
|
+
</button>
|
|
819
860
|
</li>
|
|
820
861
|
))}
|
|
821
862
|
</ul>
|