@actuate-media/cms-admin 0.36.1 → 0.37.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 (38) hide show
  1. package/dist/__tests__/views/appearance-settings.render.test.js +4 -4
  2. package/dist/__tests__/views/updates-tab.render.test.d.ts +2 -0
  3. package/dist/__tests__/views/updates-tab.render.test.d.ts.map +1 -0
  4. package/dist/__tests__/views/updates-tab.render.test.js +132 -0
  5. package/dist/__tests__/views/updates-tab.render.test.js.map +1 -0
  6. package/dist/actuate-admin.css +1 -1
  7. package/dist/views/Settings.d.ts.map +1 -1
  8. package/dist/views/Settings.js +4 -112
  9. package/dist/views/Settings.js.map +1 -1
  10. package/dist/views/settings/AdminThemeCard.js +1 -1
  11. package/dist/views/settings/BrandPreviewCard.js +1 -1
  12. package/dist/views/settings/BrandingCard.d.ts.map +1 -1
  13. package/dist/views/settings/BrandingCard.js +34 -19
  14. package/dist/views/settings/BrandingCard.js.map +1 -1
  15. package/dist/views/settings/TypographyMotionCard.js +1 -1
  16. package/dist/views/settings/UpdatesTab.d.ts +6 -0
  17. package/dist/views/settings/UpdatesTab.d.ts.map +1 -0
  18. package/dist/views/settings/UpdatesTab.js +80 -0
  19. package/dist/views/settings/UpdatesTab.js.map +1 -0
  20. package/dist/views/settings/components.d.ts +20 -0
  21. package/dist/views/settings/components.d.ts.map +1 -1
  22. package/dist/views/settings/components.js +11 -0
  23. package/dist/views/settings/components.js.map +1 -1
  24. package/dist/views/settings/useUpdates.d.ts +67 -0
  25. package/dist/views/settings/useUpdates.d.ts.map +1 -0
  26. package/dist/views/settings/useUpdates.js +127 -0
  27. package/dist/views/settings/useUpdates.js.map +1 -0
  28. package/package.json +2 -2
  29. package/src/__tests__/views/appearance-settings.render.test.tsx +4 -4
  30. package/src/__tests__/views/updates-tab.render.test.tsx +161 -0
  31. package/src/views/Settings.tsx +2 -438
  32. package/src/views/settings/AdminThemeCard.tsx +1 -1
  33. package/src/views/settings/BrandPreviewCard.tsx +1 -1
  34. package/src/views/settings/BrandingCard.tsx +179 -180
  35. package/src/views/settings/TypographyMotionCard.tsx +1 -1
  36. package/src/views/settings/UpdatesTab.tsx +545 -0
  37. package/src/views/settings/components.tsx +51 -0
  38. package/src/views/settings/useUpdates.ts +183 -0
@@ -3,16 +3,9 @@
3
3
  import * as Tabs from '@radix-ui/react-tabs'
4
4
  import {
5
5
  Bot,
6
- Eye,
7
- EyeOff,
8
6
  Loader2,
9
7
  AlertTriangle,
10
8
  Download,
11
- CheckCircle2,
12
- ArrowUpCircle,
13
- ExternalLink,
14
- RefreshCw,
15
- GitPullRequest,
16
9
  Layers,
17
10
  Users as UsersIcon,
18
11
  KeyRound,
@@ -28,6 +21,7 @@ import { AISettingsTab } from './settings/AISettingsTab.js'
28
21
  import { AppearanceTab } from './settings/AppearanceTab.js'
29
22
  import { SecurityTab } from './settings/SecurityTab.js'
30
23
  import { SettingsCard } from './settings/components.js'
24
+ import { UpdatesTab } from './settings/UpdatesTab.js'
31
25
  import { Users as UsersView } from './Users.js'
32
26
  import { ApiKeys as ApiKeysView } from './ApiKeys.js'
33
27
 
@@ -330,7 +324,7 @@ export function Settings({
330
324
  </Tabs.Content>
331
325
 
332
326
  <Tabs.Content value="updates" className="space-y-4">
333
- <UpdatesPanel />
327
+ <UpdatesTab role={session?.user?.role} />
334
328
  </Tabs.Content>
335
329
  </Tabs.Root>
336
330
 
@@ -349,433 +343,3 @@ export function Settings({
349
343
  </div>
350
344
  )
351
345
  }
352
-
353
- // ---------------------------------------------------------------------------
354
- // Updates Panel
355
- // ---------------------------------------------------------------------------
356
-
357
- interface UpdateInfo {
358
- current: string
359
- latest: string
360
- updateAvailable: boolean
361
- severity?: 'patch' | 'minor' | 'major'
362
- releaseDate?: string
363
- changelog?: Array<{ version: string; date: string; summary: string }>
364
- updateCommand?: string
365
- hasGithubToken?: boolean
366
- githubRepo?: string
367
- }
368
-
369
- function UpdatesPanel() {
370
- const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null)
371
- const [checking, setChecking] = useState(false)
372
- const [checkError, setCheckError] = useState('')
373
- const [applying, setApplying] = useState(false)
374
- const [prResult, setPrResult] = useState<{ prUrl: string; prNumber: number } | null>(null)
375
- const [hasChecked, setHasChecked] = useState(false)
376
-
377
- // GitHub config
378
- const [ghToken, setGhToken] = useState('')
379
- const [ghRepo, setGhRepo] = useState('')
380
- const [showGhToken, setShowGhToken] = useState(false)
381
- const [savingConfig, setSavingConfig] = useState(false)
382
- const [configSaved, setConfigSaved] = useState(false)
383
-
384
- const checkForUpdates = async () => {
385
- setChecking(true)
386
- setCheckError('')
387
- setPrResult(null)
388
-
389
- try {
390
- const res = await cmsApi<UpdateInfo>('/updates/check')
391
- if (res.error) {
392
- setCheckError(res.error)
393
- } else if (res.data) {
394
- setUpdateInfo(res.data)
395
- if (res.data.githubRepo) setGhRepo(res.data.githubRepo)
396
- }
397
- } catch {
398
- setCheckError('Unable to check for updates. Please try again.')
399
- } finally {
400
- setChecking(false)
401
- setHasChecked(true)
402
- }
403
- }
404
-
405
- const saveGitHubConfig = async () => {
406
- setSavingConfig(true)
407
- setConfigSaved(false)
408
- try {
409
- const res = await cmsApi('/updates/config', {
410
- method: 'PUT',
411
- body: JSON.stringify({
412
- ...(ghToken ? { githubToken: ghToken } : {}),
413
- ...(ghRepo ? { githubRepo: ghRepo } : {}),
414
- }),
415
- })
416
- if (res.error) {
417
- toast.error(res.error)
418
- } else {
419
- toast.success('GitHub configuration saved and encrypted.')
420
- setConfigSaved(true)
421
- setGhToken('')
422
- if (updateInfo) {
423
- setUpdateInfo({ ...updateInfo, hasGithubToken: true, githubRepo: ghRepo })
424
- }
425
- }
426
- } catch {
427
- toast.error('Failed to save configuration.')
428
- } finally {
429
- setSavingConfig(false)
430
- }
431
- }
432
-
433
- const applyUpdate = async () => {
434
- if (!updateInfo?.latest) return
435
- setApplying(true)
436
-
437
- try {
438
- const res = await cmsApi<{ prUrl: string; prNumber: number }>('/updates/apply', {
439
- method: 'POST',
440
- body: JSON.stringify({ targetVersion: updateInfo.latest }),
441
- })
442
-
443
- if (res.error) {
444
- toast.error(res.error)
445
- } else if (res.data) {
446
- setPrResult(res.data)
447
- toast.success('Update PR created successfully!')
448
- }
449
- } catch {
450
- toast.error('Failed to create update PR. Check your GitHub configuration.')
451
- } finally {
452
- setApplying(false)
453
- }
454
- }
455
-
456
- useEffect(() => {
457
- checkForUpdates()
458
- }, [])
459
-
460
- // Semantic severity tokens so update banners match the rest of the admin in
461
- // both light and dark mode (no raw palette colors). Concrete keys (not a
462
- // string-index Record) keep the lookup non-nullable.
463
- const severityColors = {
464
- patch: { banner: 'border-brand/30 bg-brand/10', text: 'text-brand', label: 'Patch' },
465
- minor: { banner: 'border-warning/30 bg-warning/10', text: 'text-warning', label: 'Minor' },
466
- major: {
467
- banner: 'border-destructive/30 bg-destructive/10',
468
- text: 'text-destructive',
469
- label: 'Major',
470
- },
471
- }
472
- const sev = severityColors[updateInfo?.severity ?? 'patch']
473
- const ghInputClass =
474
- 'w-full rounded-md border border-border bg-input-background px-3 py-2 text-base text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none'
475
-
476
- return (
477
- <>
478
- {/* Current Version Card */}
479
- <SettingsCard
480
- title="Actuate CMS"
481
- description="Keep your CMS up to date with the latest features and security patches."
482
- actions={
483
- <button
484
- type="button"
485
- onClick={checkForUpdates}
486
- disabled={checking}
487
- className="border-border text-foreground hover:bg-accent inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium transition-colors disabled:opacity-50"
488
- >
489
- <RefreshCw size={16} className={checking ? 'animate-spin' : ''} aria-hidden="true" />
490
- {checking ? 'Checking…' : 'Check for Updates'}
491
- </button>
492
- }
493
- >
494
- <p className="text-muted-foreground text-sm">
495
- Current version:{' '}
496
- <span className="text-foreground font-mono font-medium">
497
- {updateInfo?.current ?? '…'}
498
- </span>
499
- </p>
500
- </SettingsCard>
501
-
502
- {/* Error State */}
503
- {checkError && (
504
- <div
505
- role="alert"
506
- className="border-destructive/30 bg-destructive/10 text-destructive flex items-center gap-3 rounded-lg border p-3 text-sm"
507
- >
508
- <AlertTriangle size={20} className="shrink-0" aria-hidden="true" />
509
- <span className="flex-1">{checkError}</span>
510
- <button
511
- onClick={checkForUpdates}
512
- className="border-destructive/40 rounded-md border px-3 py-1 transition-colors hover:opacity-80"
513
- >
514
- Retry
515
- </button>
516
- </div>
517
- )}
518
-
519
- {/* Up to Date */}
520
- {hasChecked && updateInfo && !updateInfo.updateAvailable && !checkError && (
521
- <div className="border-success/30 bg-success/10 text-success flex items-center gap-3 rounded-lg border p-4">
522
- <CheckCircle2 size={24} className="shrink-0" aria-hidden="true" />
523
- <div>
524
- <h3 className="text-foreground font-medium">You&apos;re up to date!</h3>
525
- <p className="text-muted-foreground mt-0.5 text-sm">
526
- Actuate CMS <span className="font-mono">{updateInfo.current}</span> is the latest
527
- version.
528
- </p>
529
- </div>
530
- </div>
531
- )}
532
-
533
- {/* Update Available */}
534
- {updateInfo?.updateAvailable && (
535
- <>
536
- <div className={`rounded-lg border p-4 ${sev.banner}`}>
537
- <div className="flex items-start gap-3">
538
- <ArrowUpCircle
539
- size={24}
540
- className={`mt-0.5 shrink-0 ${sev.text}`}
541
- aria-hidden="true"
542
- />
543
- <div className="flex-1">
544
- <div className="flex items-center gap-2">
545
- <h3 className={`font-medium ${sev.text}`}>Update Available</h3>
546
- <span
547
- className={`inline-flex items-center rounded border px-2 py-0.5 text-xs font-medium ${sev.banner} ${sev.text}`}
548
- >
549
- {sev.label}
550
- </span>
551
- </div>
552
- <p className="text-muted-foreground mt-1 text-sm">
553
- <span className="font-mono">{updateInfo.current}</span> &rarr;{' '}
554
- <span className="text-foreground font-mono font-medium">{updateInfo.latest}</span>
555
- {updateInfo.releaseDate && (
556
- <span className="ml-2 text-xs opacity-70">
557
- Released {updateInfo.releaseDate}
558
- </span>
559
- )}
560
- </p>
561
- </div>
562
- </div>
563
-
564
- <div className="mt-4 flex items-center gap-3">
565
- <button
566
- type="button"
567
- onClick={applyUpdate}
568
- disabled={applying}
569
- className="bg-brand text-brand-foreground inline-flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-opacity hover:opacity-90 disabled:opacity-50"
570
- >
571
- {applying ? (
572
- <>
573
- <Loader2 size={16} className="animate-spin" aria-hidden="true" />
574
- Creating PR…
575
- </>
576
- ) : (
577
- <>
578
- <GitPullRequest size={16} aria-hidden="true" />
579
- Create Update PR
580
- </>
581
- )}
582
- </button>
583
- <span className="text-muted-foreground text-xs">
584
- Opens a pull request on your repository
585
- </span>
586
- </div>
587
-
588
- {updateInfo.updateCommand && (
589
- <div className="border-border bg-card mt-3 rounded-md border p-3">
590
- <p className="text-muted-foreground mb-1 text-xs">Or update manually:</p>
591
- <code className="bg-muted text-foreground block rounded px-2 py-1.5 font-mono text-xs select-all">
592
- {updateInfo.updateCommand}
593
- </code>
594
- </div>
595
- )}
596
- </div>
597
-
598
- {/* Changelog */}
599
- {updateInfo.changelog && updateInfo.changelog.length > 0 && (
600
- <SettingsCard title="Changelog">
601
- <div className="max-h-64 space-y-2 overflow-y-auto">
602
- {updateInfo.changelog.map((entry) => (
603
- <div key={entry.version} className="flex items-baseline gap-3 text-sm">
604
- <span className="text-muted-foreground w-14 shrink-0 font-mono text-xs">
605
- {entry.version}
606
- </span>
607
- <span className="text-muted-foreground w-20 shrink-0 text-xs">
608
- {entry.date}
609
- </span>
610
- <span className="text-foreground">{entry.summary}</span>
611
- </div>
612
- ))}
613
- </div>
614
- </SettingsCard>
615
- )}
616
- </>
617
- )}
618
-
619
- {/* PR Created */}
620
- {prResult && (
621
- <div className="border-success/30 bg-success/10 rounded-lg border p-4">
622
- <div className="flex items-start gap-3">
623
- <GitPullRequest size={20} className="text-success mt-0.5 shrink-0" aria-hidden="true" />
624
- <div>
625
- <h3 className="text-foreground font-medium">Pull Request Created</h3>
626
- <p className="text-muted-foreground mt-1 text-sm">
627
- PR #{prResult.prNumber} has been created on your repository. Review and merge it to
628
- apply the update, then run{' '}
629
- <code className="bg-muted rounded px-1 font-mono text-xs">
630
- npx prisma migrate deploy
631
- </code>
632
- .
633
- </p>
634
- <a
635
- href={prResult.prUrl}
636
- target="_blank"
637
- rel="noopener noreferrer"
638
- className="text-brand mt-2 inline-flex items-center gap-1.5 text-sm font-medium hover:underline"
639
- >
640
- <ExternalLink size={16} aria-hidden="true" />
641
- View Pull Request
642
- </a>
643
- </div>
644
- </div>
645
- </div>
646
- )}
647
-
648
- {/* GitHub Configuration */}
649
- <SettingsCard
650
- title="GitHub Integration"
651
- description="Connect your repository to enable one-click update PRs. Credentials are encrypted at rest (AES-256-GCM)."
652
- >
653
- <div className="space-y-4">
654
- <div>
655
- <label
656
- htmlFor="updates-gh-repo"
657
- className="text-foreground mb-1 block text-sm font-medium"
658
- >
659
- Repository
660
- </label>
661
- <input
662
- id="updates-gh-repo"
663
- type="text"
664
- value={ghRepo}
665
- onChange={(e) => setGhRepo(e.target.value)}
666
- placeholder="owner/repo"
667
- className={ghInputClass}
668
- />
669
- <p className="text-muted-foreground mt-1 text-sm">
670
- e.g.{' '}
671
- <code className="bg-muted rounded px-1 font-mono">actuate-media/my-client-site</code>
672
- </p>
673
- </div>
674
- <div>
675
- <label
676
- htmlFor="updates-gh-token"
677
- className="text-foreground mb-1 block text-sm font-medium"
678
- >
679
- GitHub Token
680
- {updateInfo?.hasGithubToken && !ghToken && (
681
- <span className="text-success ml-2 text-xs font-normal">Saved</span>
682
- )}
683
- </label>
684
- <div className="relative">
685
- <input
686
- id="updates-gh-token"
687
- type={showGhToken ? 'text' : 'password'}
688
- value={ghToken}
689
- onChange={(e) => setGhToken(e.target.value)}
690
- placeholder={
691
- updateInfo?.hasGithubToken ? '••••••••••••••••' : 'ghp_... or github_pat_...'
692
- }
693
- className={`${ghInputClass} pr-10`}
694
- />
695
- <button
696
- type="button"
697
- onClick={() => setShowGhToken(!showGhToken)}
698
- aria-label={showGhToken ? 'Hide token' : 'Show token'}
699
- className="text-muted-foreground hover:text-foreground absolute top-1/2 right-2 -translate-y-1/2 p-1"
700
- >
701
- {showGhToken ? (
702
- <EyeOff size={16} aria-hidden="true" />
703
- ) : (
704
- <Eye size={16} aria-hidden="true" />
705
- )}
706
- </button>
707
- </div>
708
- <p className="text-muted-foreground mt-1 text-sm">
709
- Needs <code className="bg-muted rounded px-1 font-mono">repo</code> scope. Create at{' '}
710
- <a
711
- href="https://github.com/settings/tokens"
712
- target="_blank"
713
- rel="noopener noreferrer"
714
- className="text-brand hover:underline"
715
- >
716
- github.com/settings/tokens
717
- </a>
718
- </p>
719
- </div>
720
-
721
- <button
722
- type="button"
723
- onClick={saveGitHubConfig}
724
- disabled={savingConfig || (!ghToken && !ghRepo)}
725
- className="bg-brand text-brand-foreground inline-flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-opacity hover:opacity-90 disabled:opacity-50"
726
- >
727
- {savingConfig ? (
728
- <>
729
- <Loader2 size={16} className="animate-spin" aria-hidden="true" />
730
- Encrypting &amp; Saving…
731
- </>
732
- ) : (
733
- 'Save Configuration'
734
- )}
735
- </button>
736
- </div>
737
- </SettingsCard>
738
-
739
- {/* Help Text */}
740
- <SettingsCard title="How Updates Work">
741
- <ul className="text-muted-foreground space-y-1.5 text-sm">
742
- <li className="flex items-start gap-2">
743
- <span className="bg-brand/10 text-brand mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-[10px] font-medium">
744
- 1
745
- </span>
746
- <span>
747
- Click &quot;Check for Updates&quot; to see if a new version is available on npm.
748
- </span>
749
- </li>
750
- <li className="flex items-start gap-2">
751
- <span className="bg-brand/10 text-brand mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-[10px] font-medium">
752
- 2
753
- </span>
754
- <span>
755
- Add your GitHub token and repository above. They&apos;re encrypted at rest — never
756
- stored in plaintext.
757
- </span>
758
- </li>
759
- <li className="flex items-start gap-2">
760
- <span className="bg-brand/10 text-brand mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-[10px] font-medium">
761
- 3
762
- </span>
763
- <span>
764
- Click &quot;Create Update PR&quot; to open a pull request that bumps your CMS packages
765
- automatically.
766
- </span>
767
- </li>
768
- <li className="flex items-start gap-2">
769
- <span className="bg-brand/10 text-brand mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-[10px] font-medium">
770
- 4
771
- </span>
772
- <span>
773
- Review and merge the PR, then deploy. Database migrations run automatically via{' '}
774
- <code className="bg-muted rounded px-1 font-mono">prisma migrate deploy</code>.
775
- </span>
776
- </li>
777
- </ul>
778
- </SettingsCard>
779
- </>
780
- )
781
- }
@@ -66,7 +66,7 @@ export function AdminThemeCard({ s, canEdit }: { s: UseAppearanceSettings; canEd
66
66
  return (
67
67
  <section className="border-border bg-card rounded-lg border p-6">
68
68
  <header className="mb-4">
69
- <h3 className="text-foreground text-lg font-medium">Admin Theme</h3>
69
+ <h3 className="text-foreground text-lg font-medium">Admin theme</h3>
70
70
  <p className="text-muted-foreground mt-0.5 text-sm">
71
71
  Controls the default look of the admin. Individual users can override the color mode.
72
72
  </p>
@@ -60,7 +60,7 @@ export function BrandPreviewCard({ s }: { s: UseAppearanceSettings }) {
60
60
  <section className="border-border bg-card rounded-lg border p-6">
61
61
  <header className="mb-4 flex items-center justify-between gap-2">
62
62
  <div>
63
- <h3 className="text-foreground text-lg font-medium">Brand Preview</h3>
63
+ <h3 className="text-foreground text-lg font-medium">Brand preview</h3>
64
64
  <p className="text-muted-foreground mt-0.5 text-sm">
65
65
  How your brand renders across the admin and public site.
66
66
  </p>