@actuate-media/cms-admin 0.36.0 → 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.
- package/dist/__tests__/views/appearance-settings.render.test.js +4 -4
- package/dist/__tests__/views/updates-tab.render.test.d.ts +2 -0
- package/dist/__tests__/views/updates-tab.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/updates-tab.render.test.js +132 -0
- package/dist/__tests__/views/updates-tab.render.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/views/ApiKeys.d.ts.map +1 -1
- package/dist/views/ApiKeys.js +14 -13
- package/dist/views/ApiKeys.js.map +1 -1
- package/dist/views/Profile.d.ts.map +1 -1
- package/dist/views/Profile.js +1 -1
- package/dist/views/Profile.js.map +1 -1
- package/dist/views/Settings.d.ts.map +1 -1
- package/dist/views/Settings.js +16 -119
- package/dist/views/Settings.js.map +1 -1
- package/dist/views/Users.js +1 -1
- package/dist/views/Users.js.map +1 -1
- package/dist/views/settings/AISettingsTab.js +40 -42
- package/dist/views/settings/AISettingsTab.js.map +1 -1
- package/dist/views/settings/AdminThemeCard.js +1 -1
- package/dist/views/settings/AiFeaturesCard.d.ts.map +1 -1
- package/dist/views/settings/AiFeaturesCard.js +2 -3
- package/dist/views/settings/AiFeaturesCard.js.map +1 -1
- package/dist/views/settings/BrandPreviewCard.js +1 -1
- package/dist/views/settings/BrandVoiceCard.d.ts.map +1 -1
- package/dist/views/settings/BrandVoiceCard.js +2 -4
- package/dist/views/settings/BrandVoiceCard.js.map +1 -1
- package/dist/views/settings/BrandingCard.d.ts.map +1 -1
- package/dist/views/settings/BrandingCard.js +34 -19
- package/dist/views/settings/BrandingCard.js.map +1 -1
- package/dist/views/settings/GeneralSettingsTab.js +1 -1
- package/dist/views/settings/LanguageRegionCard.d.ts.map +1 -1
- package/dist/views/settings/LanguageRegionCard.js +2 -1
- package/dist/views/settings/LanguageRegionCard.js.map +1 -1
- package/dist/views/settings/MonthlyUsageCard.d.ts.map +1 -1
- package/dist/views/settings/MonthlyUsageCard.js +3 -3
- package/dist/views/settings/MonthlyUsageCard.js.map +1 -1
- package/dist/views/settings/ProviderConnectionCard.js +1 -1
- package/dist/views/settings/ProviderConnectionCard.js.map +1 -1
- package/dist/views/settings/SecurityStatusCard.d.ts.map +1 -1
- package/dist/views/settings/SecurityStatusCard.js +2 -3
- package/dist/views/settings/SecurityStatusCard.js.map +1 -1
- package/dist/views/settings/SecurityTab.js +1 -1
- package/dist/views/settings/SeoRobotsDefaultsCard.d.ts.map +1 -1
- package/dist/views/settings/SeoRobotsDefaultsCard.js +3 -4
- package/dist/views/settings/SeoRobotsDefaultsCard.js.map +1 -1
- package/dist/views/settings/SeoSettingsTab.d.ts.map +1 -1
- package/dist/views/settings/SeoSettingsTab.js +5 -8
- package/dist/views/settings/SeoSettingsTab.js.map +1 -1
- package/dist/views/settings/SiteIndexStatusCard.d.ts.map +1 -1
- package/dist/views/settings/SiteIndexStatusCard.js +3 -2
- package/dist/views/settings/SiteIndexStatusCard.js.map +1 -1
- package/dist/views/settings/SiteInformationCard.d.ts.map +1 -1
- package/dist/views/settings/SiteInformationCard.js +4 -4
- package/dist/views/settings/SiteInformationCard.js.map +1 -1
- package/dist/views/settings/TypographyMotionCard.js +1 -1
- package/dist/views/settings/UpdatesTab.d.ts +6 -0
- package/dist/views/settings/UpdatesTab.d.ts.map +1 -0
- package/dist/views/settings/UpdatesTab.js +80 -0
- package/dist/views/settings/UpdatesTab.js.map +1 -0
- package/dist/views/settings/components.d.ts +51 -1
- package/dist/views/settings/components.d.ts.map +1 -1
- package/dist/views/settings/components.js +33 -0
- package/dist/views/settings/components.js.map +1 -1
- package/dist/views/settings/securityPrimitives.d.ts +11 -3
- package/dist/views/settings/securityPrimitives.d.ts.map +1 -1
- package/dist/views/settings/securityPrimitives.js +10 -3
- package/dist/views/settings/securityPrimitives.js.map +1 -1
- package/dist/views/settings/useUpdates.d.ts +67 -0
- package/dist/views/settings/useUpdates.d.ts.map +1 -0
- package/dist/views/settings/useUpdates.js +127 -0
- package/dist/views/settings/useUpdates.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/views/appearance-settings.render.test.tsx +4 -4
- package/src/__tests__/views/updates-tab.render.test.tsx +161 -0
- package/src/views/ApiKeys.tsx +190 -154
- package/src/views/Profile.tsx +4 -2
- package/src/views/Settings.tsx +26 -451
- package/src/views/Users.tsx +1 -1
- package/src/views/settings/AISettingsTab.tsx +64 -76
- package/src/views/settings/AdminThemeCard.tsx +1 -1
- package/src/views/settings/AiFeaturesCard.tsx +6 -12
- package/src/views/settings/BrandPreviewCard.tsx +1 -1
- package/src/views/settings/BrandVoiceCard.tsx +6 -19
- package/src/views/settings/BrandingCard.tsx +179 -180
- package/src/views/settings/GeneralSettingsTab.tsx +1 -1
- package/src/views/settings/LanguageRegionCard.tsx +5 -7
- package/src/views/settings/MonthlyUsageCard.tsx +6 -10
- package/src/views/settings/ProviderConnectionCard.tsx +1 -1
- package/src/views/settings/SecurityStatusCard.tsx +8 -11
- package/src/views/settings/SecurityTab.tsx +1 -1
- package/src/views/settings/SeoRobotsDefaultsCard.tsx +7 -14
- package/src/views/settings/SeoSettingsTab.tsx +17 -26
- package/src/views/settings/SiteIndexStatusCard.tsx +10 -22
- package/src/views/settings/SiteInformationCard.tsx +3 -9
- package/src/views/settings/TypographyMotionCard.tsx +1 -1
- package/src/views/settings/UpdatesTab.tsx +545 -0
- package/src/views/settings/components.tsx +123 -1
- package/src/views/settings/securityPrimitives.tsx +14 -9
- package/src/views/settings/useUpdates.ts +183 -0
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AlertTriangle,
|
|
5
|
+
ArrowUpCircle,
|
|
6
|
+
CheckCircle2,
|
|
7
|
+
Database,
|
|
8
|
+
ExternalLink,
|
|
9
|
+
Eye,
|
|
10
|
+
EyeOff,
|
|
11
|
+
GitPullRequest,
|
|
12
|
+
Loader2,
|
|
13
|
+
Lock,
|
|
14
|
+
Puzzle,
|
|
15
|
+
RefreshCw,
|
|
16
|
+
ShieldAlert,
|
|
17
|
+
} from 'lucide-react'
|
|
18
|
+
import { useState, type ReactNode } from 'react'
|
|
19
|
+
|
|
20
|
+
import { SettingsCard, SettingsControlRow, SettingsSaveBar } from './components.js'
|
|
21
|
+
import {
|
|
22
|
+
useUpdates,
|
|
23
|
+
type UpdateChangeType,
|
|
24
|
+
type UpdateChangelogItem,
|
|
25
|
+
type UpdateInfo,
|
|
26
|
+
} from './useUpdates.js'
|
|
27
|
+
|
|
28
|
+
const SEVERITY: Record<
|
|
29
|
+
NonNullable<UpdateInfo['severity']>,
|
|
30
|
+
{ banner: string; text: string; label: string }
|
|
31
|
+
> = {
|
|
32
|
+
patch: { banner: 'border-brand/30 bg-brand/10', text: 'text-brand', label: 'Patch' },
|
|
33
|
+
minor: { banner: 'border-warning/30 bg-warning/10', text: 'text-warning', label: 'Minor' },
|
|
34
|
+
major: {
|
|
35
|
+
banner: 'border-destructive/30 bg-destructive/10',
|
|
36
|
+
text: 'text-destructive',
|
|
37
|
+
label: 'Major',
|
|
38
|
+
},
|
|
39
|
+
security: {
|
|
40
|
+
banner: 'border-destructive/40 bg-destructive/15',
|
|
41
|
+
text: 'text-destructive',
|
|
42
|
+
label: 'Security',
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const CHANGE_TYPE: Record<UpdateChangeType, { label: string; className: string }> = {
|
|
47
|
+
feature: { label: 'Feature', className: 'border-brand/30 bg-brand/10 text-brand' },
|
|
48
|
+
improvement: {
|
|
49
|
+
label: 'Improvement',
|
|
50
|
+
className: 'border-border bg-muted text-muted-foreground',
|
|
51
|
+
},
|
|
52
|
+
fix: { label: 'Fix', className: 'border-border bg-muted text-muted-foreground' },
|
|
53
|
+
breaking: {
|
|
54
|
+
label: 'Breaking',
|
|
55
|
+
className: 'border-destructive/30 bg-destructive/10 text-destructive',
|
|
56
|
+
},
|
|
57
|
+
security: {
|
|
58
|
+
label: 'Security',
|
|
59
|
+
className: 'border-destructive/40 bg-destructive/15 text-destructive',
|
|
60
|
+
},
|
|
61
|
+
deprecation: { label: 'Deprecated', className: 'border-warning/30 bg-warning/10 text-warning' },
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const ghInputClass =
|
|
65
|
+
'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'
|
|
66
|
+
|
|
67
|
+
/** Group a flat changelog list into per-version buckets (order preserved). */
|
|
68
|
+
function groupByVersion(
|
|
69
|
+
changelog: UpdateChangelogItem[],
|
|
70
|
+
): Array<{ version: string; date: string; items: UpdateChangelogItem[] }> {
|
|
71
|
+
const groups: Array<{ version: string; date: string; items: UpdateChangelogItem[] }> = []
|
|
72
|
+
for (const item of changelog) {
|
|
73
|
+
const last = groups[groups.length - 1]
|
|
74
|
+
if (last && last.version === item.version) {
|
|
75
|
+
last.items.push(item)
|
|
76
|
+
} else {
|
|
77
|
+
groups.push({ version: item.version, date: item.date, items: [item] })
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return groups
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function ChangeTypeBadge({ type }: { type?: UpdateChangeType }) {
|
|
84
|
+
if (!type) return null
|
|
85
|
+
const meta = CHANGE_TYPE[type]
|
|
86
|
+
return (
|
|
87
|
+
<span
|
|
88
|
+
className={`inline-flex shrink-0 items-center rounded border px-1.5 py-0.5 text-xs font-medium ${meta.className}`}
|
|
89
|
+
>
|
|
90
|
+
{meta.label}
|
|
91
|
+
</span>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A bordered notice row used for migration / breaking-change callouts. */
|
|
96
|
+
function Notice({
|
|
97
|
+
tone,
|
|
98
|
+
icon,
|
|
99
|
+
title,
|
|
100
|
+
children,
|
|
101
|
+
}: {
|
|
102
|
+
tone: 'warning' | 'destructive'
|
|
103
|
+
icon: ReactNode
|
|
104
|
+
title: string
|
|
105
|
+
children?: ReactNode
|
|
106
|
+
}) {
|
|
107
|
+
const toneClass =
|
|
108
|
+
tone === 'destructive'
|
|
109
|
+
? 'border-destructive/30 bg-destructive/10 text-destructive'
|
|
110
|
+
: 'border-warning/30 bg-warning/10 text-warning'
|
|
111
|
+
return (
|
|
112
|
+
<div className={`mt-3 flex items-start gap-2.5 rounded-md border p-3 ${toneClass}`}>
|
|
113
|
+
<span className="mt-0.5 shrink-0" aria-hidden="true">
|
|
114
|
+
{icon}
|
|
115
|
+
</span>
|
|
116
|
+
<div className="min-w-0 text-sm">
|
|
117
|
+
<p className="font-medium">{title}</p>
|
|
118
|
+
{children && <div className="text-foreground/80 mt-1 space-y-1">{children}</div>}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface UpdatesTabProps {
|
|
125
|
+
/** Current admin role — gates the write actions (config + Create PR). */
|
|
126
|
+
role?: string
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function UpdatesTab({ role }: UpdatesTabProps) {
|
|
130
|
+
const {
|
|
131
|
+
info,
|
|
132
|
+
checking,
|
|
133
|
+
hasChecked,
|
|
134
|
+
error,
|
|
135
|
+
applying,
|
|
136
|
+
prResult,
|
|
137
|
+
canManage,
|
|
138
|
+
form,
|
|
139
|
+
setField,
|
|
140
|
+
dirty,
|
|
141
|
+
repoError,
|
|
142
|
+
hasErrors,
|
|
143
|
+
saveState,
|
|
144
|
+
save,
|
|
145
|
+
reset,
|
|
146
|
+
check,
|
|
147
|
+
apply,
|
|
148
|
+
} = useUpdates({ role })
|
|
149
|
+
|
|
150
|
+
const [showToken, setShowToken] = useState(false)
|
|
151
|
+
|
|
152
|
+
const severity = info?.updateAvailable ? SEVERITY[info.severity ?? 'patch'] : null
|
|
153
|
+
const changelogGroups = info?.changelog ? groupByVersion(info.changelog) : []
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<>
|
|
157
|
+
{/* Current version */}
|
|
158
|
+
<SettingsCard
|
|
159
|
+
title="Actuate CMS"
|
|
160
|
+
description="Keep your CMS up to date with the latest features and security patches."
|
|
161
|
+
actions={
|
|
162
|
+
<button
|
|
163
|
+
type="button"
|
|
164
|
+
onClick={check}
|
|
165
|
+
disabled={checking}
|
|
166
|
+
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"
|
|
167
|
+
>
|
|
168
|
+
<RefreshCw size={16} className={checking ? 'animate-spin' : ''} aria-hidden="true" />
|
|
169
|
+
{checking ? 'Checking…' : 'Check for Updates'}
|
|
170
|
+
</button>
|
|
171
|
+
}
|
|
172
|
+
>
|
|
173
|
+
<p className="text-muted-foreground text-sm">
|
|
174
|
+
Current version:{' '}
|
|
175
|
+
<span className="text-foreground font-mono font-medium">{info?.current ?? '…'}</span>
|
|
176
|
+
</p>
|
|
177
|
+
</SettingsCard>
|
|
178
|
+
|
|
179
|
+
{/* Error */}
|
|
180
|
+
{error && (
|
|
181
|
+
<div
|
|
182
|
+
role="alert"
|
|
183
|
+
className="border-destructive/30 bg-destructive/10 text-destructive flex items-center gap-3 rounded-lg border p-3 text-sm"
|
|
184
|
+
>
|
|
185
|
+
<AlertTriangle size={20} className="shrink-0" aria-hidden="true" />
|
|
186
|
+
<span className="flex-1">{error}</span>
|
|
187
|
+
<button
|
|
188
|
+
type="button"
|
|
189
|
+
onClick={check}
|
|
190
|
+
className="border-destructive/40 rounded-md border px-3 py-1 transition-colors hover:opacity-80"
|
|
191
|
+
>
|
|
192
|
+
Retry
|
|
193
|
+
</button>
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
|
|
197
|
+
{/* Up to date */}
|
|
198
|
+
{hasChecked && info && !info.updateAvailable && !error && (
|
|
199
|
+
<div className="border-success/30 bg-success/10 flex items-center gap-3 rounded-lg border p-4">
|
|
200
|
+
<CheckCircle2 size={24} className="text-success shrink-0" aria-hidden="true" />
|
|
201
|
+
<div>
|
|
202
|
+
<h3 className="text-foreground font-medium">You're up to date!</h3>
|
|
203
|
+
<p className="text-muted-foreground mt-0.5 text-sm">
|
|
204
|
+
Actuate CMS <span className="font-mono">{info.current}</span> is the latest version.
|
|
205
|
+
</p>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
|
|
210
|
+
{/* Update available */}
|
|
211
|
+
{info?.updateAvailable && severity && (
|
|
212
|
+
<div className={`rounded-lg border p-4 ${severity.banner}`}>
|
|
213
|
+
<div className="flex items-start gap-3">
|
|
214
|
+
{info.severity === 'security' ? (
|
|
215
|
+
<ShieldAlert
|
|
216
|
+
size={24}
|
|
217
|
+
className={`mt-0.5 shrink-0 ${severity.text}`}
|
|
218
|
+
aria-hidden="true"
|
|
219
|
+
/>
|
|
220
|
+
) : (
|
|
221
|
+
<ArrowUpCircle
|
|
222
|
+
size={24}
|
|
223
|
+
className={`mt-0.5 shrink-0 ${severity.text}`}
|
|
224
|
+
aria-hidden="true"
|
|
225
|
+
/>
|
|
226
|
+
)}
|
|
227
|
+
<div className="flex-1">
|
|
228
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
229
|
+
<h3 className={`font-medium ${severity.text}`}>
|
|
230
|
+
{info.severity === 'security' ? 'Security update available' : 'Update available'}
|
|
231
|
+
</h3>
|
|
232
|
+
<span
|
|
233
|
+
className={`inline-flex items-center rounded border px-2 py-0.5 text-xs font-medium ${severity.banner} ${severity.text}`}
|
|
234
|
+
>
|
|
235
|
+
{severity.label}
|
|
236
|
+
</span>
|
|
237
|
+
</div>
|
|
238
|
+
<p className="text-muted-foreground mt-1 text-sm">
|
|
239
|
+
<span className="font-mono">{info.current}</span> →{' '}
|
|
240
|
+
<span className="text-foreground font-mono font-medium">{info.latest}</span>
|
|
241
|
+
{info.releaseDate && (
|
|
242
|
+
<span className="ml-2 text-xs opacity-70">Released {info.releaseDate}</span>
|
|
243
|
+
)}
|
|
244
|
+
</p>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
{info.hasMigrations && (
|
|
249
|
+
<Notice
|
|
250
|
+
tone="warning"
|
|
251
|
+
icon={<Database size={16} />}
|
|
252
|
+
title="Database migrations required"
|
|
253
|
+
>
|
|
254
|
+
<p>
|
|
255
|
+
Run{' '}
|
|
256
|
+
<code className="bg-muted rounded px-1 font-mono text-xs">
|
|
257
|
+
npx prisma migrate deploy
|
|
258
|
+
</code>{' '}
|
|
259
|
+
after upgrading. On Vercel this runs automatically at build time.
|
|
260
|
+
</p>
|
|
261
|
+
</Notice>
|
|
262
|
+
)}
|
|
263
|
+
|
|
264
|
+
{info.breakingChanges && info.breakingChanges.length > 0 && (
|
|
265
|
+
<Notice
|
|
266
|
+
tone="destructive"
|
|
267
|
+
icon={<AlertTriangle size={16} />}
|
|
268
|
+
title="Breaking changes in this release"
|
|
269
|
+
>
|
|
270
|
+
<ul className="list-disc space-y-1 pl-4">
|
|
271
|
+
{info.breakingChanges.map((c, i) => (
|
|
272
|
+
<li key={i}>{c}</li>
|
|
273
|
+
))}
|
|
274
|
+
</ul>
|
|
275
|
+
</Notice>
|
|
276
|
+
)}
|
|
277
|
+
|
|
278
|
+
<div className="mt-4 flex flex-wrap items-center gap-3">
|
|
279
|
+
{canManage ? (
|
|
280
|
+
<button
|
|
281
|
+
type="button"
|
|
282
|
+
onClick={apply}
|
|
283
|
+
disabled={applying}
|
|
284
|
+
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"
|
|
285
|
+
>
|
|
286
|
+
{applying ? (
|
|
287
|
+
<>
|
|
288
|
+
<Loader2 size={16} className="animate-spin" aria-hidden="true" />
|
|
289
|
+
Creating PR…
|
|
290
|
+
</>
|
|
291
|
+
) : (
|
|
292
|
+
<>
|
|
293
|
+
<GitPullRequest size={16} aria-hidden="true" />
|
|
294
|
+
Create Update PR
|
|
295
|
+
</>
|
|
296
|
+
)}
|
|
297
|
+
</button>
|
|
298
|
+
) : (
|
|
299
|
+
<span className="text-muted-foreground inline-flex items-center gap-1.5 text-xs">
|
|
300
|
+
<Lock size={14} aria-hidden="true" />
|
|
301
|
+
Ask an administrator to apply this update.
|
|
302
|
+
</span>
|
|
303
|
+
)}
|
|
304
|
+
{canManage && (
|
|
305
|
+
<span className="text-muted-foreground text-xs">
|
|
306
|
+
Opens a pull request on your repository
|
|
307
|
+
</span>
|
|
308
|
+
)}
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
{info.updateCommand && (
|
|
312
|
+
<div className="border-border bg-card mt-3 rounded-md border p-3">
|
|
313
|
+
<p className="text-muted-foreground mb-1 text-xs">Or update manually:</p>
|
|
314
|
+
<code className="bg-muted text-foreground block rounded px-2 py-1.5 font-mono text-xs select-all">
|
|
315
|
+
{info.updateCommand}
|
|
316
|
+
</code>
|
|
317
|
+
</div>
|
|
318
|
+
)}
|
|
319
|
+
</div>
|
|
320
|
+
)}
|
|
321
|
+
|
|
322
|
+
{/* Changelog */}
|
|
323
|
+
{info?.updateAvailable && changelogGroups.length > 0 && (
|
|
324
|
+
<SettingsCard
|
|
325
|
+
title="Changelog"
|
|
326
|
+
description={
|
|
327
|
+
info.source === 'npm'
|
|
328
|
+
? 'Version history from npm. Connect the update server for full release notes.'
|
|
329
|
+
: "What's new since your current version."
|
|
330
|
+
}
|
|
331
|
+
>
|
|
332
|
+
<div className="max-h-80 space-y-4 overflow-y-auto pr-1">
|
|
333
|
+
{changelogGroups.map((group) => (
|
|
334
|
+
<div
|
|
335
|
+
key={group.version}
|
|
336
|
+
className="border-border border-b pb-3 last:border-0 last:pb-0"
|
|
337
|
+
>
|
|
338
|
+
<div className="mb-1.5 flex items-baseline gap-3">
|
|
339
|
+
<span className="text-foreground font-mono text-sm font-medium">
|
|
340
|
+
{group.version}
|
|
341
|
+
</span>
|
|
342
|
+
{group.date && (
|
|
343
|
+
<span className="text-muted-foreground text-xs">{group.date}</span>
|
|
344
|
+
)}
|
|
345
|
+
</div>
|
|
346
|
+
<ul className="space-y-1.5">
|
|
347
|
+
{group.items.map((item, i) => (
|
|
348
|
+
<li key={i} className="flex items-start gap-2 text-sm">
|
|
349
|
+
<ChangeTypeBadge type={item.type} />
|
|
350
|
+
<span className="text-muted-foreground min-w-0">{item.summary}</span>
|
|
351
|
+
</li>
|
|
352
|
+
))}
|
|
353
|
+
</ul>
|
|
354
|
+
</div>
|
|
355
|
+
))}
|
|
356
|
+
</div>
|
|
357
|
+
</SettingsCard>
|
|
358
|
+
)}
|
|
359
|
+
|
|
360
|
+
{/* Plugin compatibility */}
|
|
361
|
+
{info?.updateAvailable && info.pluginCompatibility && info.pluginCompatibility.length > 0 && (
|
|
362
|
+
<SettingsCard
|
|
363
|
+
title="Plugin compatibility"
|
|
364
|
+
description="Latest published versions of the official plugins for this release."
|
|
365
|
+
>
|
|
366
|
+
<ul className="grid gap-2 sm:grid-cols-2">
|
|
367
|
+
{info.pluginCompatibility.map((plugin) => (
|
|
368
|
+
<li
|
|
369
|
+
key={plugin.name}
|
|
370
|
+
className="border-border flex items-center justify-between gap-3 rounded-md border px-3 py-2 text-sm"
|
|
371
|
+
>
|
|
372
|
+
<span className="text-foreground inline-flex min-w-0 items-center gap-2">
|
|
373
|
+
<Puzzle size={14} className="text-muted-foreground shrink-0" aria-hidden="true" />
|
|
374
|
+
<span className="truncate font-mono text-xs">
|
|
375
|
+
{plugin.name.replace('@actuate-media/', '')}
|
|
376
|
+
</span>
|
|
377
|
+
</span>
|
|
378
|
+
<span className="text-muted-foreground inline-flex shrink-0 items-center gap-1.5">
|
|
379
|
+
{plugin.minVersion && (
|
|
380
|
+
<span className="font-mono text-xs">{plugin.minVersion}</span>
|
|
381
|
+
)}
|
|
382
|
+
<CheckCircle2 size={14} className="text-success" aria-hidden="true" />
|
|
383
|
+
</span>
|
|
384
|
+
</li>
|
|
385
|
+
))}
|
|
386
|
+
</ul>
|
|
387
|
+
</SettingsCard>
|
|
388
|
+
)}
|
|
389
|
+
|
|
390
|
+
{/* PR created */}
|
|
391
|
+
{prResult && (
|
|
392
|
+
<div className="border-success/30 bg-success/10 rounded-lg border p-4">
|
|
393
|
+
<div className="flex items-start gap-3">
|
|
394
|
+
<GitPullRequest size={20} className="text-success mt-0.5 shrink-0" aria-hidden="true" />
|
|
395
|
+
<div>
|
|
396
|
+
<h3 className="text-foreground font-medium">Pull Request Created</h3>
|
|
397
|
+
<p className="text-muted-foreground mt-1 text-sm">
|
|
398
|
+
PR #{prResult.prNumber} has been created on your repository. Review and merge it to
|
|
399
|
+
apply the update, then run{' '}
|
|
400
|
+
<code className="bg-muted rounded px-1 font-mono text-xs">
|
|
401
|
+
npx prisma migrate deploy
|
|
402
|
+
</code>
|
|
403
|
+
.
|
|
404
|
+
</p>
|
|
405
|
+
<a
|
|
406
|
+
href={prResult.prUrl}
|
|
407
|
+
target="_blank"
|
|
408
|
+
rel="noopener noreferrer"
|
|
409
|
+
className="text-brand mt-2 inline-flex items-center gap-1.5 text-sm font-medium hover:underline"
|
|
410
|
+
>
|
|
411
|
+
<ExternalLink size={16} aria-hidden="true" />
|
|
412
|
+
View Pull Request
|
|
413
|
+
</a>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
)}
|
|
418
|
+
|
|
419
|
+
{/* GitHub integration — admins only */}
|
|
420
|
+
{canManage && (
|
|
421
|
+
<SettingsCard
|
|
422
|
+
title="GitHub Integration"
|
|
423
|
+
description="Connect your repository to enable one-click update PRs. Credentials are encrypted at rest (AES-256-GCM)."
|
|
424
|
+
>
|
|
425
|
+
<div className="space-y-4">
|
|
426
|
+
<SettingsControlRow
|
|
427
|
+
label="Repository"
|
|
428
|
+
htmlFor="updates-gh-repo"
|
|
429
|
+
align="start"
|
|
430
|
+
description={
|
|
431
|
+
<>
|
|
432
|
+
e.g.{' '}
|
|
433
|
+
<code className="bg-muted rounded px-1 font-mono">
|
|
434
|
+
actuate-media/my-client-site
|
|
435
|
+
</code>
|
|
436
|
+
</>
|
|
437
|
+
}
|
|
438
|
+
control={
|
|
439
|
+
<div className="w-72 max-w-full">
|
|
440
|
+
<input
|
|
441
|
+
id="updates-gh-repo"
|
|
442
|
+
type="text"
|
|
443
|
+
value={form.githubRepo}
|
|
444
|
+
onChange={(e) => setField('githubRepo', e.target.value)}
|
|
445
|
+
placeholder="owner/repo"
|
|
446
|
+
aria-invalid={Boolean(repoError)}
|
|
447
|
+
aria-describedby={repoError ? 'updates-gh-repo-error' : undefined}
|
|
448
|
+
className={ghInputClass}
|
|
449
|
+
/>
|
|
450
|
+
{repoError && (
|
|
451
|
+
<p id="updates-gh-repo-error" className="text-destructive mt-1 text-xs">
|
|
452
|
+
{repoError}
|
|
453
|
+
</p>
|
|
454
|
+
)}
|
|
455
|
+
</div>
|
|
456
|
+
}
|
|
457
|
+
/>
|
|
458
|
+
|
|
459
|
+
<SettingsControlRow
|
|
460
|
+
label={
|
|
461
|
+
<span className="inline-flex items-center gap-2">
|
|
462
|
+
GitHub token
|
|
463
|
+
{info?.hasGithubToken && !form.githubToken && (
|
|
464
|
+
<span className="text-success text-xs font-normal">Saved</span>
|
|
465
|
+
)}
|
|
466
|
+
</span>
|
|
467
|
+
}
|
|
468
|
+
htmlFor="updates-gh-token"
|
|
469
|
+
align="start"
|
|
470
|
+
description={
|
|
471
|
+
<>
|
|
472
|
+
Needs <code className="bg-muted rounded px-1 font-mono">repo</code> scope. Create
|
|
473
|
+
one at{' '}
|
|
474
|
+
<a
|
|
475
|
+
href="https://github.com/settings/tokens"
|
|
476
|
+
target="_blank"
|
|
477
|
+
rel="noopener noreferrer"
|
|
478
|
+
className="text-brand hover:underline"
|
|
479
|
+
>
|
|
480
|
+
github.com/settings/tokens
|
|
481
|
+
</a>
|
|
482
|
+
.
|
|
483
|
+
</>
|
|
484
|
+
}
|
|
485
|
+
control={
|
|
486
|
+
<div className="relative w-72 max-w-full">
|
|
487
|
+
<input
|
|
488
|
+
id="updates-gh-token"
|
|
489
|
+
type={showToken ? 'text' : 'password'}
|
|
490
|
+
value={form.githubToken}
|
|
491
|
+
onChange={(e) => setField('githubToken', e.target.value)}
|
|
492
|
+
placeholder={
|
|
493
|
+
info?.hasGithubToken ? '••••••••••••••••' : 'ghp_… or github_pat_…'
|
|
494
|
+
}
|
|
495
|
+
className={`${ghInputClass} pr-10`}
|
|
496
|
+
/>
|
|
497
|
+
<button
|
|
498
|
+
type="button"
|
|
499
|
+
onClick={() => setShowToken((s) => !s)}
|
|
500
|
+
aria-label={showToken ? 'Hide token' : 'Show token'}
|
|
501
|
+
className="text-muted-foreground hover:text-foreground absolute top-1/2 right-2 -translate-y-1/2 p-1"
|
|
502
|
+
>
|
|
503
|
+
{showToken ? (
|
|
504
|
+
<EyeOff size={16} aria-hidden="true" />
|
|
505
|
+
) : (
|
|
506
|
+
<Eye size={16} aria-hidden="true" />
|
|
507
|
+
)}
|
|
508
|
+
</button>
|
|
509
|
+
</div>
|
|
510
|
+
}
|
|
511
|
+
/>
|
|
512
|
+
</div>
|
|
513
|
+
|
|
514
|
+
<SettingsSaveBar
|
|
515
|
+
dirty={dirty}
|
|
516
|
+
canEdit={canManage}
|
|
517
|
+
hasErrors={hasErrors}
|
|
518
|
+
saveState={saveState}
|
|
519
|
+
onSave={save}
|
|
520
|
+
onReset={reset}
|
|
521
|
+
/>
|
|
522
|
+
</SettingsCard>
|
|
523
|
+
)}
|
|
524
|
+
|
|
525
|
+
{/* Help */}
|
|
526
|
+
<SettingsCard title="How Updates Work">
|
|
527
|
+
<ol className="text-muted-foreground space-y-1.5 text-sm">
|
|
528
|
+
{[
|
|
529
|
+
'Click “Check for Updates” to see if a new version is available.',
|
|
530
|
+
'Add your GitHub token and repository above (admins only). They’re encrypted at rest — never stored in plaintext.',
|
|
531
|
+
'Click “Create Update PR” to open a pull request that bumps your CMS packages automatically.',
|
|
532
|
+
'Review and merge the PR, then deploy. Database migrations run automatically via prisma migrate deploy.',
|
|
533
|
+
].map((step, i) => (
|
|
534
|
+
<li key={i} className="flex items-start gap-2">
|
|
535
|
+
<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">
|
|
536
|
+
{i + 1}
|
|
537
|
+
</span>
|
|
538
|
+
<span>{step}</span>
|
|
539
|
+
</li>
|
|
540
|
+
))}
|
|
541
|
+
</ol>
|
|
542
|
+
</SettingsCard>
|
|
543
|
+
</>
|
|
544
|
+
)
|
|
545
|
+
}
|
|
@@ -2,9 +2,131 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Dialog from '@radix-ui/react-dialog'
|
|
4
4
|
import { AlertTriangle, Check, Loader2, Lock } from 'lucide-react'
|
|
5
|
-
import type
|
|
5
|
+
import { useId, type ReactNode } from 'react'
|
|
6
6
|
import type { SettingsSourceMetadata, ValidationIssue } from './useGeneralSettings.js'
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* The canonical Settings section card: a bordered `bg-card` surface with a
|
|
10
|
+
* header (title + optional description) and an optional right-aligned actions
|
|
11
|
+
* slot. This is the single source of truth for the Settings design language —
|
|
12
|
+
* every tab's sections render through it so spacing, padding, typography, and
|
|
13
|
+
* the title/description rhythm stay identical across General, Appearance,
|
|
14
|
+
* Security, SEO, AI, and beyond. Match the Appearance tab exactly: `p-6`,
|
|
15
|
+
* `text-lg` medium title, `text-sm` muted description.
|
|
16
|
+
*/
|
|
17
|
+
export function SettingsCard({
|
|
18
|
+
title,
|
|
19
|
+
description,
|
|
20
|
+
actions,
|
|
21
|
+
children,
|
|
22
|
+
className,
|
|
23
|
+
headingLevel: Heading = 'h3',
|
|
24
|
+
}: {
|
|
25
|
+
title: ReactNode
|
|
26
|
+
description?: ReactNode
|
|
27
|
+
/** Right-aligned controls in the header (e.g. a Refresh or Add button). */
|
|
28
|
+
actions?: ReactNode
|
|
29
|
+
children: ReactNode
|
|
30
|
+
className?: string
|
|
31
|
+
/** Override the heading element for correct document outline nesting. */
|
|
32
|
+
headingLevel?: 'h2' | 'h3'
|
|
33
|
+
}) {
|
|
34
|
+
const headingId = useId()
|
|
35
|
+
return (
|
|
36
|
+
<section
|
|
37
|
+
aria-labelledby={headingId}
|
|
38
|
+
className={`border-border bg-card rounded-lg border p-6 ${className ?? ''}`}
|
|
39
|
+
>
|
|
40
|
+
<header className="mb-4 flex items-start justify-between gap-3">
|
|
41
|
+
<div className="min-w-0">
|
|
42
|
+
<Heading id={headingId} className="text-foreground text-lg font-medium">
|
|
43
|
+
{title}
|
|
44
|
+
</Heading>
|
|
45
|
+
{description && <p className="text-muted-foreground mt-0.5 text-sm">{description}</p>}
|
|
46
|
+
</div>
|
|
47
|
+
{actions && <div className="flex shrink-0 items-center gap-2">{actions}</div>}
|
|
48
|
+
</header>
|
|
49
|
+
{children}
|
|
50
|
+
</section>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A sub-section inside a {@link SettingsCard}: a top divider plus a
|
|
56
|
+
* `text-base` medium label and optional description. Use for grouped controls
|
|
57
|
+
* within a single card (e.g. "Admin branding" under "Branding").
|
|
58
|
+
*/
|
|
59
|
+
export function SettingsSubsection({
|
|
60
|
+
title,
|
|
61
|
+
description,
|
|
62
|
+
children,
|
|
63
|
+
className,
|
|
64
|
+
}: {
|
|
65
|
+
title: ReactNode
|
|
66
|
+
description?: ReactNode
|
|
67
|
+
children?: ReactNode
|
|
68
|
+
className?: string
|
|
69
|
+
}) {
|
|
70
|
+
return (
|
|
71
|
+
<div className={`border-border border-t pt-4 ${className ?? ''}`}>
|
|
72
|
+
<div className="text-foreground text-base font-medium">{title}</div>
|
|
73
|
+
{description && <p className="text-muted-foreground mt-0.5 text-sm">{description}</p>}
|
|
74
|
+
{children}
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A single setting row: an optional leading visual plus a label and
|
|
81
|
+
* description on the left, and a control aligned to the right edge. This is the
|
|
82
|
+
* uniform rhythm for compact controls (upload buttons, segmented toggles)
|
|
83
|
+
* inside a {@link SettingsCard} — mirror it everywhere so rows line up instead
|
|
84
|
+
* of stacking controls beneath their copy.
|
|
85
|
+
*/
|
|
86
|
+
export function SettingsControlRow({
|
|
87
|
+
label,
|
|
88
|
+
description,
|
|
89
|
+
control,
|
|
90
|
+
leading,
|
|
91
|
+
htmlFor,
|
|
92
|
+
align = 'center',
|
|
93
|
+
className,
|
|
94
|
+
}: {
|
|
95
|
+
label: ReactNode
|
|
96
|
+
description?: ReactNode
|
|
97
|
+
/** Right-aligned control(s) for this row. */
|
|
98
|
+
control: ReactNode
|
|
99
|
+
/** Optional visual (e.g. a logo thumbnail) shown before the label. */
|
|
100
|
+
leading?: ReactNode
|
|
101
|
+
/** When set, the label renders as a `<label>` bound to this control id. */
|
|
102
|
+
htmlFor?: string
|
|
103
|
+
/** Vertical alignment of the two columns. Defaults to centered. */
|
|
104
|
+
align?: 'center' | 'start'
|
|
105
|
+
className?: string
|
|
106
|
+
}) {
|
|
107
|
+
const labelClass = 'text-foreground block text-base font-medium'
|
|
108
|
+
return (
|
|
109
|
+
<div
|
|
110
|
+
className={`flex justify-between gap-4 ${align === 'center' ? 'items-center' : 'items-start'} ${className ?? ''}`}
|
|
111
|
+
>
|
|
112
|
+
<div className="flex min-w-0 items-center gap-4">
|
|
113
|
+
{leading}
|
|
114
|
+
<div className="min-w-0">
|
|
115
|
+
{htmlFor ? (
|
|
116
|
+
<label htmlFor={htmlFor} className={labelClass}>
|
|
117
|
+
{label}
|
|
118
|
+
</label>
|
|
119
|
+
) : (
|
|
120
|
+
<span className={labelClass}>{label}</span>
|
|
121
|
+
)}
|
|
122
|
+
{description && <p className="text-muted-foreground mt-0.5 text-sm">{description}</p>}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
<div className="flex shrink-0 items-center gap-2">{control}</div>
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
8
130
|
const SOURCE_LABEL: Record<SettingsSourceMetadata['source'], string> = {
|
|
9
131
|
config: 'Config-managed',
|
|
10
132
|
environment: 'Environment-managed',
|