@actuate-media/cms-admin 0.78.0 → 0.79.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/CHANGELOG.md +11 -0
- package/dist/__tests__/views/form-editor.render.test.js +1 -0
- package/dist/__tests__/views/form-editor.render.test.js.map +1 -1
- package/dist/__tests__/views/forms-list.render.test.js +2 -0
- package/dist/__tests__/views/forms-list.render.test.js.map +1 -1
- package/dist/__tests__/views/redirects-pane.render.test.js +2 -0
- package/dist/__tests__/views/redirects-pane.render.test.js.map +1 -1
- package/dist/components/forms/FormSchemaPane.js +1 -1
- package/dist/components/forms/FormSchemaPane.js.map +1 -1
- package/dist/components/forms/NotificationsPanel.d.ts +4 -1
- package/dist/components/forms/NotificationsPanel.d.ts.map +1 -1
- package/dist/components/forms/NotificationsPanel.js +66 -12
- package/dist/components/forms/NotificationsPanel.js.map +1 -1
- package/dist/components/forms/ShortcodeGlossary.d.ts +5 -0
- package/dist/components/forms/ShortcodeGlossary.d.ts.map +1 -0
- package/dist/components/forms/ShortcodeGlossary.js +62 -0
- package/dist/components/forms/ShortcodeGlossary.js.map +1 -0
- package/dist/components/ui/ReauthDialog.d.ts +17 -0
- package/dist/components/ui/ReauthDialog.d.ts.map +1 -0
- package/dist/components/ui/ReauthDialog.js +34 -0
- package/dist/components/ui/ReauthDialog.js.map +1 -0
- package/dist/components/ui/index.d.ts +2 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +1 -0
- package/dist/components/ui/index.js.map +1 -1
- package/dist/lib/forms-service.d.ts +9 -0
- package/dist/lib/forms-service.d.ts.map +1 -1
- package/dist/lib/forms-service.js +5 -0
- package/dist/lib/forms-service.js.map +1 -1
- package/dist/lib/seo-service.d.ts +4 -0
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js +1 -0
- package/dist/lib/seo-service.js.map +1 -1
- package/dist/views/ApiKeys.d.ts.map +1 -1
- package/dist/views/ApiKeys.js +22 -23
- package/dist/views/ApiKeys.js.map +1 -1
- package/dist/views/FormEditor.js +1 -1
- package/dist/views/FormEditor.js.map +1 -1
- package/dist/views/seo/RedirectsTab.d.ts.map +1 -1
- package/dist/views/seo/RedirectsTab.js +3 -2
- package/dist/views/seo/RedirectsTab.js.map +1 -1
- package/dist/views/users/UserDetailDrawer.d.ts.map +1 -1
- package/dist/views/users/UserDetailDrawer.js +2 -30
- package/dist/views/users/UserDetailDrawer.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/views/form-editor.render.test.tsx +1 -0
- package/src/__tests__/views/forms-list.render.test.tsx +2 -0
- package/src/__tests__/views/redirects-pane.render.test.tsx +2 -0
- package/src/components/forms/FormSchemaPane.tsx +1 -1
- package/src/components/forms/NotificationsPanel.tsx +150 -20
- package/src/components/forms/ShortcodeGlossary.tsx +146 -0
- package/src/components/ui/ReauthDialog.tsx +93 -0
- package/src/components/ui/index.ts +2 -0
- package/src/lib/forms-service.ts +12 -0
- package/src/lib/seo-service.ts +5 -0
- package/src/views/ApiKeys.tsx +48 -41
- package/src/views/FormEditor.tsx +1 -1
- package/src/views/seo/RedirectsTab.tsx +3 -1
- package/src/views/users/UserDetailDrawer.tsx +1 -76
package/src/views/ApiKeys.tsx
CHANGED
|
@@ -17,6 +17,7 @@ import { type FormEvent, useState } from 'react'
|
|
|
17
17
|
import { toast } from 'sonner'
|
|
18
18
|
import { useApiData } from '../lib/useApiData.js'
|
|
19
19
|
import { cmsApi } from '../lib/api.js'
|
|
20
|
+
import { ReauthDialog } from '../components/ui/ReauthDialog.js'
|
|
20
21
|
import { SettingsCard } from './settings/components.js'
|
|
21
22
|
|
|
22
23
|
const DIALOG_INPUT_CLASS =
|
|
@@ -80,26 +81,11 @@ export function ApiKeys({ embedded = false }: ApiKeysProps = {}) {
|
|
|
80
81
|
const { data, loading, error, refetch } = useApiData<ApiKeyRecord[]>('/api-keys')
|
|
81
82
|
const [showCreate, setShowCreate] = useState(false)
|
|
82
83
|
const [createdKey, setCreatedKey] = useState<{ key: string; record: ApiKeyRecord } | null>(null)
|
|
84
|
+
const [pendingDelete, setPendingDelete] = useState<{ id: string; name: string } | null>(null)
|
|
83
85
|
|
|
84
86
|
const keys = data ?? []
|
|
85
87
|
const rootPadding = embedded ? '' : 'p-4 sm:p-6'
|
|
86
88
|
|
|
87
|
-
const handleRevoke = async (id: string, name: string) => {
|
|
88
|
-
if (!confirm(`Revoke API key "${name}"? This cannot be undone.`)) return
|
|
89
|
-
const password = prompt('Confirm your password to revoke this API key:')
|
|
90
|
-
if (!password) return
|
|
91
|
-
const res = await cmsApi(`/api-keys/${id}`, {
|
|
92
|
-
method: 'DELETE',
|
|
93
|
-
headers: { 'x-reauth-password': password },
|
|
94
|
-
})
|
|
95
|
-
if (res.error) {
|
|
96
|
-
toast.error(res.error)
|
|
97
|
-
} else {
|
|
98
|
-
toast.success('API key revoked')
|
|
99
|
-
refetch()
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
89
|
if (loading) {
|
|
104
90
|
return (
|
|
105
91
|
<div className={`flex h-64 items-center justify-center ${embedded ? '' : 'p-4 sm:p-6'}`}>
|
|
@@ -193,10 +179,9 @@ export function ApiKeys({ embedded = false }: ApiKeysProps = {}) {
|
|
|
193
179
|
</thead>
|
|
194
180
|
<tbody className="divide-border divide-y">
|
|
195
181
|
{keys.map((k) => {
|
|
196
|
-
const revoked = !!k.revokedAt
|
|
197
182
|
const expired = k.expiresAt && new Date(k.expiresAt).getTime() < Date.now()
|
|
198
183
|
return (
|
|
199
|
-
<tr key={k.id} className={
|
|
184
|
+
<tr key={k.id} className={expired ? 'opacity-60' : undefined}>
|
|
200
185
|
<td className="px-4 py-3 text-sm">
|
|
201
186
|
<div className="text-foreground font-medium">{k.name}</div>
|
|
202
187
|
{k.user && (
|
|
@@ -218,11 +203,7 @@ export function ApiKeys({ embedded = false }: ApiKeysProps = {}) {
|
|
|
218
203
|
{formatDate(k.expiresAt)}
|
|
219
204
|
</td>
|
|
220
205
|
<td className="px-4 py-3">
|
|
221
|
-
{
|
|
222
|
-
<span className="bg-destructive/10 text-destructive inline-flex rounded-full px-2 py-0.5 text-xs font-medium">
|
|
223
|
-
Revoked
|
|
224
|
-
</span>
|
|
225
|
-
) : expired ? (
|
|
206
|
+
{expired ? (
|
|
226
207
|
<span className="bg-warning/10 text-warning inline-flex rounded-full px-2 py-0.5 text-xs font-medium">
|
|
227
208
|
Expired
|
|
228
209
|
</span>
|
|
@@ -233,17 +214,15 @@ export function ApiKeys({ embedded = false }: ApiKeysProps = {}) {
|
|
|
233
214
|
)}
|
|
234
215
|
</td>
|
|
235
216
|
<td className="px-4 py-3 text-right">
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
</button>
|
|
246
|
-
)}
|
|
217
|
+
<button
|
|
218
|
+
type="button"
|
|
219
|
+
onClick={() => setPendingDelete({ id: k.id, name: k.name })}
|
|
220
|
+
className="hover:bg-destructive/10 rounded p-1.5 transition-colors"
|
|
221
|
+
aria-label={`Delete ${k.name}`}
|
|
222
|
+
title="Delete"
|
|
223
|
+
>
|
|
224
|
+
<Trash2 className="text-destructive h-4 w-4" aria-hidden="true" />
|
|
225
|
+
</button>
|
|
247
226
|
</td>
|
|
248
227
|
</tr>
|
|
249
228
|
)
|
|
@@ -266,6 +245,33 @@ export function ApiKeys({ embedded = false }: ApiKeysProps = {}) {
|
|
|
266
245
|
/>
|
|
267
246
|
|
|
268
247
|
{createdKey && <RevealKeyDialog created={createdKey} onClose={() => setCreatedKey(null)} />}
|
|
248
|
+
|
|
249
|
+
<ReauthDialog
|
|
250
|
+
open={!!pendingDelete}
|
|
251
|
+
onClose={() => setPendingDelete(null)}
|
|
252
|
+
title="Delete API key"
|
|
253
|
+
description={
|
|
254
|
+
pendingDelete
|
|
255
|
+
? `Permanently delete “${pendingDelete.name}”? Clients using this key will fail immediately. This cannot be undone.`
|
|
256
|
+
: 'Permanently delete this API key? This cannot be undone.'
|
|
257
|
+
}
|
|
258
|
+
confirmLabel="Delete key"
|
|
259
|
+
destructive
|
|
260
|
+
onSubmit={async (password) => {
|
|
261
|
+
if (!pendingDelete) return
|
|
262
|
+
const res = await cmsApi(`/api-keys/${pendingDelete.id}`, {
|
|
263
|
+
method: 'DELETE',
|
|
264
|
+
headers: { 'x-reauth-password': password },
|
|
265
|
+
})
|
|
266
|
+
if (res.error) {
|
|
267
|
+
toast.error(res.error)
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
toast.success('API key deleted')
|
|
271
|
+
setPendingDelete(null)
|
|
272
|
+
refetch()
|
|
273
|
+
}}
|
|
274
|
+
/>
|
|
269
275
|
</div>
|
|
270
276
|
)
|
|
271
277
|
}
|
|
@@ -442,24 +448,25 @@ function CreateApiKeyDialog({ open, onClose, onCreated }: CreateApiKeyDialogProp
|
|
|
442
448
|
/>
|
|
443
449
|
</div>
|
|
444
450
|
|
|
445
|
-
<div>
|
|
451
|
+
<div className="border-border bg-muted/30 space-y-2 rounded-md border p-3">
|
|
446
452
|
<label
|
|
447
453
|
htmlFor="apikey-password"
|
|
448
|
-
className="text-foreground
|
|
454
|
+
className="text-foreground block text-sm font-medium"
|
|
449
455
|
>
|
|
450
|
-
Confirm
|
|
456
|
+
Confirm it's you
|
|
451
457
|
</label>
|
|
458
|
+
<p className="text-muted-foreground text-sm">
|
|
459
|
+
Creating an API key is a sensitive action. Re-enter your password to continue.
|
|
460
|
+
</p>
|
|
452
461
|
<input
|
|
453
462
|
id="apikey-password"
|
|
454
463
|
type="password"
|
|
455
464
|
value={password}
|
|
456
465
|
onChange={(e) => setPassword(e.target.value)}
|
|
466
|
+
autoComplete="current-password"
|
|
457
467
|
className={DIALOG_INPUT_CLASS}
|
|
458
468
|
required
|
|
459
469
|
/>
|
|
460
|
-
<p className="text-muted-foreground mt-1 text-sm">
|
|
461
|
-
Creating an API key is a sensitive action and requires re-authentication.
|
|
462
|
-
</p>
|
|
463
470
|
</div>
|
|
464
471
|
|
|
465
472
|
<div className="flex items-center justify-end gap-2 pt-2">
|
|
@@ -509,7 +516,7 @@ function RevealKeyDialog({
|
|
|
509
516
|
<div className="border-warning/30 bg-warning/10 mb-4 flex items-start gap-2 rounded-lg border p-4">
|
|
510
517
|
<AlertTriangle className="text-warning mt-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
|
|
511
518
|
<p className="text-foreground text-xs">
|
|
512
|
-
We only store a hash of this key. If you lose it, you'll need to
|
|
519
|
+
We only store a hash of this key. If you lose it, you'll need to delete it and
|
|
513
520
|
create a new one.
|
|
514
521
|
</p>
|
|
515
522
|
</div>
|
package/src/views/FormEditor.tsx
CHANGED
|
@@ -314,7 +314,7 @@ function ExistingFormEditor({
|
|
|
314
314
|
|
|
315
315
|
<Tabs.Content value="notifications" tabIndex={-1}>
|
|
316
316
|
<div className="border-border bg-card rounded-lg border p-4">
|
|
317
|
-
<NotificationsPanel formId={formId} />
|
|
317
|
+
<NotificationsPanel formId={formId} fields={fields} />
|
|
318
318
|
</div>
|
|
319
319
|
</Tabs.Content>
|
|
320
320
|
|
|
@@ -53,12 +53,13 @@ function OverviewCards({ onRefetchKey }: { onRefetchKey: number }) {
|
|
|
53
53
|
if (!data) return null
|
|
54
54
|
const cards = [
|
|
55
55
|
{ label: 'Active redirects', value: data.activeRedirects },
|
|
56
|
+
{ label: 'Never hit', value: data.neverHitRedirects },
|
|
56
57
|
{ label: 'Stale redirects', value: data.staleRedirects },
|
|
57
58
|
{ label: '404s tracked', value: data.notFoundCount },
|
|
58
59
|
{ label: 'Chains & loops', value: data.chainsAndLoops },
|
|
59
60
|
]
|
|
60
61
|
return (
|
|
61
|
-
<div className="grid grid-cols-2 gap-4 lg:grid-cols-
|
|
62
|
+
<div className="grid grid-cols-2 gap-4 lg:grid-cols-3 xl:grid-cols-5">
|
|
62
63
|
{cards.map((c) => (
|
|
63
64
|
<div key={c.label} className="border-border bg-card rounded-lg border p-4">
|
|
64
65
|
<p className="text-muted-foreground text-sm">{c.label}</p>
|
|
@@ -470,6 +471,7 @@ export function RedirectsTab() {
|
|
|
470
471
|
label={r.enabled ? 'Enabled' : 'Disabled'}
|
|
471
472
|
tone={r.enabled ? 'good' : 'neutral'}
|
|
472
473
|
/>
|
|
474
|
+
{r.neverHit && <SeoStatusBadge label="Never hit" tone="neutral" />}
|
|
473
475
|
{r.stale && <SeoStatusBadge label="Stale" tone="fair" />}
|
|
474
476
|
</span>
|
|
475
477
|
</td>
|
|
@@ -39,6 +39,7 @@ import { Button } from '../../components/ui/Button.js'
|
|
|
39
39
|
import { Skeleton } from '../../components/ui/Skeleton.js'
|
|
40
40
|
import { EmptyState } from '../../components/ui/EmptyState.js'
|
|
41
41
|
import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'
|
|
42
|
+
import { ReauthDialog } from '../../components/ui/ReauthDialog.js'
|
|
42
43
|
|
|
43
44
|
// ---------------------------------------------------------------------------
|
|
44
45
|
// Types (mirror cms-core serialized shapes)
|
|
@@ -151,82 +152,6 @@ const STATUS_META: Record<UserDetail['status'], { dot: string; text: string; lab
|
|
|
151
152
|
},
|
|
152
153
|
}
|
|
153
154
|
|
|
154
|
-
// ---------------------------------------------------------------------------
|
|
155
|
-
// Reauth dialog
|
|
156
|
-
// ---------------------------------------------------------------------------
|
|
157
|
-
|
|
158
|
-
function ReauthDialog({
|
|
159
|
-
open,
|
|
160
|
-
onClose,
|
|
161
|
-
onSubmit,
|
|
162
|
-
}: {
|
|
163
|
-
open: boolean
|
|
164
|
-
onClose: () => void
|
|
165
|
-
onSubmit: (password: string) => Promise<void>
|
|
166
|
-
}) {
|
|
167
|
-
const [password, setPassword] = useState('')
|
|
168
|
-
const [busy, setBusy] = useState(false)
|
|
169
|
-
|
|
170
|
-
const submit = async () => {
|
|
171
|
-
if (!password || busy) return
|
|
172
|
-
setBusy(true)
|
|
173
|
-
try {
|
|
174
|
-
await onSubmit(password)
|
|
175
|
-
setPassword('')
|
|
176
|
-
} finally {
|
|
177
|
-
setBusy(false)
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return (
|
|
182
|
-
<Modal
|
|
183
|
-
open={open}
|
|
184
|
-
onClose={() => {
|
|
185
|
-
setPassword('')
|
|
186
|
-
onClose()
|
|
187
|
-
}}
|
|
188
|
-
title="Confirm it's you"
|
|
189
|
-
actions={
|
|
190
|
-
<>
|
|
191
|
-
<Button
|
|
192
|
-
variant="ghost"
|
|
193
|
-
onClick={() => {
|
|
194
|
-
setPassword('')
|
|
195
|
-
onClose()
|
|
196
|
-
}}
|
|
197
|
-
>
|
|
198
|
-
Cancel
|
|
199
|
-
</Button>
|
|
200
|
-
<Button variant="primary" loading={busy} disabled={!password || busy} onClick={submit}>
|
|
201
|
-
Confirm
|
|
202
|
-
</Button>
|
|
203
|
-
</>
|
|
204
|
-
}
|
|
205
|
-
>
|
|
206
|
-
<p className="text-muted-foreground mb-3 text-sm">
|
|
207
|
-
This is a sensitive action. Re-enter your password to continue.
|
|
208
|
-
</p>
|
|
209
|
-
<label
|
|
210
|
-
htmlFor="reauth-password"
|
|
211
|
-
className="text-card-foreground mb-1 block text-sm font-medium"
|
|
212
|
-
>
|
|
213
|
-
Your password
|
|
214
|
-
</label>
|
|
215
|
-
<input
|
|
216
|
-
id="reauth-password"
|
|
217
|
-
type="password"
|
|
218
|
-
value={password}
|
|
219
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
220
|
-
onKeyDown={(e) => {
|
|
221
|
-
if (e.key === 'Enter') submit()
|
|
222
|
-
}}
|
|
223
|
-
autoComplete="current-password"
|
|
224
|
-
className="border-border bg-input-background text-foreground focus:ring-brand w-full rounded-md border px-3 py-2 text-sm focus:ring-2 focus:outline-none"
|
|
225
|
-
/>
|
|
226
|
-
</Modal>
|
|
227
|
-
)
|
|
228
|
-
}
|
|
229
|
-
|
|
230
155
|
// ---------------------------------------------------------------------------
|
|
231
156
|
// Transfer content modal
|
|
232
157
|
// ---------------------------------------------------------------------------
|