@actuate-media/cms-admin 0.70.0 → 0.71.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 +6 -0
- package/dist/__tests__/views/script-tags.render.test.d.ts +2 -0
- package/dist/__tests__/views/script-tags.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/script-tags.render.test.js +148 -0
- package/dist/__tests__/views/script-tags.render.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/components/script-tags/ScriptTagEditorPane.d.ts +10 -0
- package/dist/components/script-tags/ScriptTagEditorPane.d.ts.map +1 -1
- package/dist/components/script-tags/ScriptTagEditorPane.js +21 -2
- package/dist/components/script-tags/ScriptTagEditorPane.js.map +1 -1
- package/dist/views/ScriptTags.d.ts.map +1 -1
- package/dist/views/ScriptTags.js +121 -15
- package/dist/views/ScriptTags.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/views/script-tags.render.test.tsx +178 -0
- package/src/components/script-tags/ScriptTagEditorPane.tsx +40 -4
- package/src/views/ScriptTags.tsx +257 -23
package/src/views/ScriptTags.tsx
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useMemo, useState } from 'react'
|
|
4
|
-
import { Code2, Plus, Loader2, AlertTriangle } from 'lucide-react'
|
|
4
|
+
import { Code2, Copy, Plus, Loader2, AlertTriangle, Shield, Trash2 } from 'lucide-react'
|
|
5
5
|
import { toast } from 'sonner'
|
|
6
6
|
import { EnabledSwitch } from '../components/EnabledSwitch.js'
|
|
7
7
|
import { Button } from '../components/ui/Button.js'
|
|
8
|
+
import { ConfirmDialog } from '../components/ui/ConfirmDialog.js'
|
|
9
|
+
import { FilterPill } from '../components/ui/FilterPill.js'
|
|
10
|
+
import { RowActions, RowActionButton } from '../components/ui/RowActions.js'
|
|
11
|
+
import { SearchInput } from '../components/ui/SearchInput.js'
|
|
8
12
|
import { StatPill } from '../components/ui/StatPill.js'
|
|
9
13
|
import { InspectorPane, SplitPaneLayout, SplitPaneList } from '../components/ui/InspectorPane.js'
|
|
10
14
|
import {
|
|
15
|
+
CONSENT_COLORS,
|
|
11
16
|
CONSENT_LABELS,
|
|
17
|
+
NO_CONSENT_COLORS,
|
|
18
|
+
PLACEMENT_COLORS,
|
|
12
19
|
PLACEMENT_LABELS,
|
|
13
20
|
ScriptTagEditorPane,
|
|
14
21
|
type ScriptTag,
|
|
@@ -24,11 +31,21 @@ export interface ScriptTagsProps {
|
|
|
24
31
|
initialNew?: boolean
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
34
|
+
const LOCATION_FILTERS: Array<{ value: string; label: string }> = [
|
|
35
|
+
{ value: 'all', label: 'All' },
|
|
36
|
+
{ value: 'head', label: '<head>' },
|
|
37
|
+
{ value: 'body_open', label: 'Body start' },
|
|
38
|
+
{ value: 'body_close', label: 'Body end' },
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
const CONSENT_FILTERS: Array<{ value: string; label: string }> = [
|
|
42
|
+
{ value: 'all', label: 'All consent' },
|
|
43
|
+
{ value: 'none', label: 'No consent' },
|
|
44
|
+
{ value: 'analytics', label: 'Analytics' },
|
|
45
|
+
{ value: 'marketing', label: 'Marketing' },
|
|
46
|
+
{ value: 'functional', label: 'Functional' },
|
|
47
|
+
{ value: 'preferences', label: 'Preferences' },
|
|
48
|
+
]
|
|
32
49
|
|
|
33
50
|
function normalizeEnabled(value: unknown): boolean {
|
|
34
51
|
if (typeof value === 'boolean') return value
|
|
@@ -52,18 +69,27 @@ function scopeLabel(tag: ScriptTag): string {
|
|
|
52
69
|
return scope || 'Custom'
|
|
53
70
|
}
|
|
54
71
|
|
|
55
|
-
function
|
|
72
|
+
function formatDate(value: string): string {
|
|
56
73
|
const date = new Date(value)
|
|
57
74
|
if (Number.isNaN(date.getTime())) return '—'
|
|
58
75
|
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
|
|
59
76
|
}
|
|
60
77
|
|
|
78
|
+
function consentCategoryOf(tag: ScriptTag): string {
|
|
79
|
+
return tag.consentCategory && tag.consentCategory !== '' ? tag.consentCategory : 'none'
|
|
80
|
+
}
|
|
81
|
+
|
|
61
82
|
type Selection = { mode: 'edit'; id: string } | { mode: 'new' } | null
|
|
62
83
|
|
|
63
84
|
export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
64
85
|
const { data, loading, error, refetch } = useApiData<ScriptTag[]>('/script-tags')
|
|
65
86
|
const [togglingId, setTogglingId] = useState<string | null>(null)
|
|
66
87
|
const [enabledOverrides, setEnabledOverrides] = useState<Record<string, boolean>>({})
|
|
88
|
+
const [search, setSearch] = useState('')
|
|
89
|
+
const [locationFilter, setLocationFilter] = useState('all')
|
|
90
|
+
const [consentFilter, setConsentFilter] = useState('all')
|
|
91
|
+
const [checkedIds, setCheckedIds] = useState<Set<string>>(new Set())
|
|
92
|
+
const [pendingDelete, setPendingDelete] = useState<ScriptTag | null>(null)
|
|
67
93
|
const [selection, setSelection] = useState<Selection>(
|
|
68
94
|
initialNew
|
|
69
95
|
? { mode: 'new' }
|
|
@@ -81,6 +107,20 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
81
107
|
[data, enabledOverrides],
|
|
82
108
|
)
|
|
83
109
|
|
|
110
|
+
const filteredTags = useMemo(() => {
|
|
111
|
+
const query = search.trim().toLowerCase()
|
|
112
|
+
return tags.filter((tag) => {
|
|
113
|
+
if (locationFilter !== 'all' && tag.placement !== locationFilter) return false
|
|
114
|
+
if (consentFilter !== 'all' && consentCategoryOf(tag) !== consentFilter) return false
|
|
115
|
+
if (query && !tag.name.toLowerCase().includes(query)) return false
|
|
116
|
+
return true
|
|
117
|
+
})
|
|
118
|
+
}, [tags, locationFilter, consentFilter, search])
|
|
119
|
+
|
|
120
|
+
const consentGatedCount = tags.filter((t) => consentCategoryOf(t) !== 'none').length
|
|
121
|
+
const checkedVisible = filteredTags.filter((t) => checkedIds.has(t.id))
|
|
122
|
+
const allVisibleChecked = filteredTags.length > 0 && checkedVisible.length === filteredTags.length
|
|
123
|
+
|
|
84
124
|
const selectedTag =
|
|
85
125
|
selection?.mode === 'edit' ? (tags.find((t) => t.id === selection.id) ?? null) : null
|
|
86
126
|
const paneOpen = selection?.mode === 'new' || selectedTag !== null
|
|
@@ -91,6 +131,25 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
91
131
|
)
|
|
92
132
|
}
|
|
93
133
|
|
|
134
|
+
const toggleChecked = (id: string) => {
|
|
135
|
+
setCheckedIds((prev) => {
|
|
136
|
+
const next = new Set(prev)
|
|
137
|
+
if (next.has(id)) next.delete(id)
|
|
138
|
+
else next.add(id)
|
|
139
|
+
return next
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const toggleAllChecked = () => {
|
|
144
|
+
setCheckedIds(allVisibleChecked ? new Set() : new Set(filteredTags.map((t) => t.id)))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const clearFilters = () => {
|
|
148
|
+
setSearch('')
|
|
149
|
+
setLocationFilter('all')
|
|
150
|
+
setConsentFilter('all')
|
|
151
|
+
}
|
|
152
|
+
|
|
94
153
|
const toggleEnabled = async (tag: ScriptTag, nextEnabled: boolean) => {
|
|
95
154
|
setEnabledOverrides((prev) => ({ ...prev, [tag.id]: nextEnabled }))
|
|
96
155
|
setTogglingId(tag.id)
|
|
@@ -117,6 +176,48 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
117
176
|
toast.success(saved ? 'Tag enabled' : 'Tag disabled')
|
|
118
177
|
}
|
|
119
178
|
|
|
179
|
+
const duplicateTag = async (tag: ScriptTag) => {
|
|
180
|
+
const res = await cmsApi<ScriptTag>('/script-tags', {
|
|
181
|
+
method: 'POST',
|
|
182
|
+
body: JSON.stringify({
|
|
183
|
+
name: `${tag.name} (Copy)`,
|
|
184
|
+
code: tag.code,
|
|
185
|
+
placement: tag.placement,
|
|
186
|
+
scope: tag.scope,
|
|
187
|
+
targetPaths: tag.targetPaths,
|
|
188
|
+
priority: tag.priority,
|
|
189
|
+
enabled: false,
|
|
190
|
+
loadAsync: tag.loadAsync,
|
|
191
|
+
disableInPreview: tag.disableInPreview,
|
|
192
|
+
consentCategory: tag.consentCategory,
|
|
193
|
+
}),
|
|
194
|
+
})
|
|
195
|
+
if (res.error) {
|
|
196
|
+
toast.error(res.error)
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
toast.success('Script duplicated (created disabled)')
|
|
200
|
+
refetch()
|
|
201
|
+
if (res.data?.id) setSelection({ mode: 'edit', id: res.data.id })
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const deleteTag = async (tag: ScriptTag) => {
|
|
205
|
+
setPendingDelete(null)
|
|
206
|
+
const res = await cmsApi(`/script-tags/${tag.id}`, { method: 'DELETE' })
|
|
207
|
+
if (res.error) {
|
|
208
|
+
toast.error(res.error)
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
toast.success('Script deleted')
|
|
212
|
+
setCheckedIds((prev) => {
|
|
213
|
+
const next = new Set(prev)
|
|
214
|
+
next.delete(tag.id)
|
|
215
|
+
return next
|
|
216
|
+
})
|
|
217
|
+
if (selection?.mode === 'edit' && selection.id === tag.id) setSelection(null)
|
|
218
|
+
refetch()
|
|
219
|
+
}
|
|
220
|
+
|
|
120
221
|
const handlePaneChanged = (savedId?: string) => {
|
|
121
222
|
setEnabledOverrides({})
|
|
122
223
|
refetch()
|
|
@@ -151,7 +252,7 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
151
252
|
<div>
|
|
152
253
|
<h1 className="mb-1 text-[var(--txt)]">Script Tags</h1>
|
|
153
254
|
<p className="text-sm text-[var(--sub)]">
|
|
154
|
-
|
|
255
|
+
Inject scripts into the frontend — never into CMS rendering
|
|
155
256
|
</p>
|
|
156
257
|
{tags.length > 0 && (
|
|
157
258
|
<div className="mt-3 flex flex-wrap gap-1.5">
|
|
@@ -166,6 +267,11 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
166
267
|
label="Inactive"
|
|
167
268
|
valueClassName="text-[var(--muted)]"
|
|
168
269
|
/>
|
|
270
|
+
<StatPill
|
|
271
|
+
value={consentGatedCount}
|
|
272
|
+
label="Consent-gated"
|
|
273
|
+
icon={<Shield size={12} className="text-[var(--acc)]" aria-hidden />}
|
|
274
|
+
/>
|
|
169
275
|
</div>
|
|
170
276
|
)}
|
|
171
277
|
</div>
|
|
@@ -179,6 +285,52 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
179
285
|
New Script
|
|
180
286
|
</Button>
|
|
181
287
|
</div>
|
|
288
|
+
|
|
289
|
+
{tags.length > 0 && (
|
|
290
|
+
<div className="mb-3 flex flex-wrap items-center gap-2">
|
|
291
|
+
<div className="w-full max-w-[260px]">
|
|
292
|
+
<SearchInput value={search} onChange={setSearch} placeholder="Search scripts…" />
|
|
293
|
+
</div>
|
|
294
|
+
<div className="flex gap-1" role="group" aria-label="Filter by location">
|
|
295
|
+
{LOCATION_FILTERS.map((f) => (
|
|
296
|
+
<FilterPill
|
|
297
|
+
key={f.value}
|
|
298
|
+
active={locationFilter === f.value}
|
|
299
|
+
onClick={() => setLocationFilter(f.value)}
|
|
300
|
+
>
|
|
301
|
+
{f.label}
|
|
302
|
+
</FilterPill>
|
|
303
|
+
))}
|
|
304
|
+
</div>
|
|
305
|
+
<div className="ml-2 flex flex-wrap gap-1" role="group" aria-label="Filter by consent">
|
|
306
|
+
{CONSENT_FILTERS.map((f) => (
|
|
307
|
+
<FilterPill
|
|
308
|
+
key={f.value}
|
|
309
|
+
active={consentFilter === f.value}
|
|
310
|
+
onClick={() => setConsentFilter(f.value)}
|
|
311
|
+
>
|
|
312
|
+
{f.label}
|
|
313
|
+
</FilterPill>
|
|
314
|
+
))}
|
|
315
|
+
</div>
|
|
316
|
+
<span className="ml-auto text-xs text-[var(--muted)]" aria-live="polite">
|
|
317
|
+
{checkedIds.size > 0 ? (
|
|
318
|
+
<>
|
|
319
|
+
{checkedIds.size} selected ·{' '}
|
|
320
|
+
<button
|
|
321
|
+
type="button"
|
|
322
|
+
onClick={() => setCheckedIds(new Set())}
|
|
323
|
+
className="rounded-sm text-[var(--acc)] hover:underline focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
|
|
324
|
+
>
|
|
325
|
+
Clear
|
|
326
|
+
</button>
|
|
327
|
+
</>
|
|
328
|
+
) : (
|
|
329
|
+
`${filteredTags.length} script${filteredTags.length !== 1 ? 's' : ''}`
|
|
330
|
+
)}
|
|
331
|
+
</span>
|
|
332
|
+
</div>
|
|
333
|
+
)}
|
|
182
334
|
</div>
|
|
183
335
|
|
|
184
336
|
<SplitPaneLayout>
|
|
@@ -201,11 +353,39 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
201
353
|
Add Your First Script
|
|
202
354
|
</Button>
|
|
203
355
|
</div>
|
|
356
|
+
) : filteredTags.length === 0 ? (
|
|
357
|
+
<div className="rounded-lg border border-[var(--bdr)] bg-[var(--card)] p-12 text-center">
|
|
358
|
+
<Code2 className="mx-auto mb-3 h-10 w-10 text-[var(--sub)]" />
|
|
359
|
+
<h3 className="text-sm font-medium text-[var(--txt)]">
|
|
360
|
+
No scripts match your filters
|
|
361
|
+
</h3>
|
|
362
|
+
<p className="mt-1 text-sm text-[var(--sub)]">
|
|
363
|
+
Try a different search term or clear the filters.
|
|
364
|
+
</p>
|
|
365
|
+
<button
|
|
366
|
+
type="button"
|
|
367
|
+
onClick={clearFilters}
|
|
368
|
+
className="mt-3 rounded-sm text-sm text-[var(--acc)] hover:underline focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
|
|
369
|
+
>
|
|
370
|
+
Clear filters
|
|
371
|
+
</button>
|
|
372
|
+
</div>
|
|
204
373
|
) : (
|
|
205
374
|
<div className="overflow-hidden rounded-lg border border-[var(--bdr)] bg-[var(--card)]">
|
|
206
375
|
<table className="w-full text-sm" aria-label="Script tags">
|
|
207
376
|
<thead>
|
|
208
377
|
<tr className="bg-muted border-b border-[var(--bdr)]">
|
|
378
|
+
<th className="w-9 px-3 py-3">
|
|
379
|
+
<input
|
|
380
|
+
type="checkbox"
|
|
381
|
+
checked={allVisibleChecked}
|
|
382
|
+
onChange={toggleAllChecked}
|
|
383
|
+
aria-label={
|
|
384
|
+
allVisibleChecked ? 'Deselect all scripts' : 'Select all scripts'
|
|
385
|
+
}
|
|
386
|
+
className="h-[15px] w-[15px] cursor-pointer accent-[var(--acc)]"
|
|
387
|
+
/>
|
|
388
|
+
</th>
|
|
209
389
|
<th className="px-4 py-3 text-left font-medium text-[var(--sub)]">Script</th>
|
|
210
390
|
<th className="px-4 py-3 text-left font-medium text-[var(--sub)]">Location</th>
|
|
211
391
|
<th className="px-4 py-3 text-center font-medium text-[var(--sub)]">
|
|
@@ -214,20 +394,30 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
214
394
|
<th className="px-4 py-3 text-left font-medium text-[var(--sub)]">Scope</th>
|
|
215
395
|
<th className="px-4 py-3 text-left font-medium text-[var(--sub)]">Consent</th>
|
|
216
396
|
<th className="px-4 py-3 text-center font-medium text-[var(--sub)]">Active</th>
|
|
217
|
-
<th className="px-4 py-3 text-
|
|
397
|
+
<th className="px-4 py-3 text-right font-medium text-[var(--sub)]">Updated</th>
|
|
218
398
|
</tr>
|
|
219
399
|
</thead>
|
|
220
400
|
<tbody>
|
|
221
|
-
{
|
|
401
|
+
{filteredTags.map((tag) => {
|
|
222
402
|
const isSelected = selection?.mode === 'edit' && selection.id === tag.id
|
|
403
|
+
const consent = CONSENT_COLORS[consentCategoryOf(tag)] ?? NO_CONSENT_COLORS
|
|
223
404
|
return (
|
|
224
405
|
<tr
|
|
225
406
|
key={tag.id}
|
|
226
|
-
className={`cursor-pointer border-b border-[var(--bdr)] transition-colors last:border-0 ${
|
|
407
|
+
className={`group cursor-pointer border-b border-[var(--bdr)] transition-colors last:border-0 ${
|
|
227
408
|
isSelected ? 'bg-[var(--acc-l)]' : 'hover:bg-[var(--acc-l)]'
|
|
228
409
|
}`}
|
|
229
410
|
onClick={() => toggleRow(tag)}
|
|
230
411
|
>
|
|
412
|
+
<td className="px-3 py-[11px]" onClick={(e) => e.stopPropagation()}>
|
|
413
|
+
<input
|
|
414
|
+
type="checkbox"
|
|
415
|
+
checked={checkedIds.has(tag.id)}
|
|
416
|
+
onChange={() => toggleChecked(tag.id)}
|
|
417
|
+
aria-label={`Select ${tag.name}`}
|
|
418
|
+
className="h-[15px] w-[15px] cursor-pointer accent-[var(--acc)]"
|
|
419
|
+
/>
|
|
420
|
+
</td>
|
|
231
421
|
<td className="px-4 py-[11px]">
|
|
232
422
|
<button
|
|
233
423
|
type="button"
|
|
@@ -240,26 +430,33 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
240
430
|
>
|
|
241
431
|
{tag.name}
|
|
242
432
|
</button>
|
|
433
|
+
<div className="mt-0.5 text-[11.5px] text-[var(--muted)]">
|
|
434
|
+
Added {formatDate(tag.createdAt)}
|
|
435
|
+
</div>
|
|
243
436
|
</td>
|
|
244
437
|
<td className="px-4 py-[11px]">
|
|
245
438
|
<span
|
|
246
|
-
className={`inline-flex items-center rounded-
|
|
439
|
+
className={`inline-flex items-center rounded-[5px] px-2 py-[3px] text-[11.5px] font-medium ${PLACEMENT_COLORS[tag.placement] ?? 'bg-muted text-[var(--txt)]'}`}
|
|
247
440
|
>
|
|
248
441
|
{PLACEMENT_LABELS[tag.placement] ?? tag.placement}
|
|
249
442
|
</span>
|
|
250
443
|
</td>
|
|
251
|
-
<td className="px-4 py-[11px] text-center
|
|
252
|
-
|
|
444
|
+
<td className="px-4 py-[11px] text-center">
|
|
445
|
+
<span className="inline-flex h-6 w-6 items-center justify-center rounded-full border-[1.5px] border-[var(--bdr)] bg-[var(--bg)] text-[11.5px] font-semibold text-[var(--sub)]">
|
|
446
|
+
{tag.priority}
|
|
447
|
+
</span>
|
|
253
448
|
</td>
|
|
254
449
|
<td className="px-4 py-[11px] text-[var(--sub)]">{scopeLabel(tag)}</td>
|
|
255
450
|
<td className="px-4 py-[11px]">
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
451
|
+
<span
|
|
452
|
+
className={`inline-flex items-center gap-[3px] rounded-[4px] px-[7px] py-0.5 text-[11px] font-medium ${consent.badge}`}
|
|
453
|
+
>
|
|
454
|
+
<span
|
|
455
|
+
className={`inline-block h-[5px] w-[5px] rounded-full ${consent.dot}`}
|
|
456
|
+
aria-hidden
|
|
457
|
+
/>
|
|
458
|
+
{CONSENT_LABELS[consentCategoryOf(tag)] ?? tag.consentCategory}
|
|
459
|
+
</span>
|
|
263
460
|
</td>
|
|
264
461
|
<td
|
|
265
462
|
className="px-4 py-[11px] text-center"
|
|
@@ -272,8 +469,33 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
272
469
|
aria-label={`${tag.enabled ? 'Disable' : 'Enable'} ${tag.name}`}
|
|
273
470
|
/>
|
|
274
471
|
</td>
|
|
275
|
-
<td className="px-4 py-[11px]
|
|
276
|
-
|
|
472
|
+
<td className="px-4 py-[11px]">
|
|
473
|
+
<div className="flex items-center justify-end gap-2">
|
|
474
|
+
<RowActions>
|
|
475
|
+
<RowActionButton
|
|
476
|
+
aria-label={`Duplicate ${tag.name}`}
|
|
477
|
+
onClick={(e) => {
|
|
478
|
+
e.stopPropagation()
|
|
479
|
+
void duplicateTag(tag)
|
|
480
|
+
}}
|
|
481
|
+
>
|
|
482
|
+
<Copy size={12} aria-hidden />
|
|
483
|
+
</RowActionButton>
|
|
484
|
+
<RowActionButton
|
|
485
|
+
danger
|
|
486
|
+
aria-label={`Delete ${tag.name}`}
|
|
487
|
+
onClick={(e) => {
|
|
488
|
+
e.stopPropagation()
|
|
489
|
+
setPendingDelete(tag)
|
|
490
|
+
}}
|
|
491
|
+
>
|
|
492
|
+
<Trash2 size={12} aria-hidden />
|
|
493
|
+
</RowActionButton>
|
|
494
|
+
</RowActions>
|
|
495
|
+
<span className="text-xs whitespace-nowrap text-[var(--sub)]">
|
|
496
|
+
{formatDate(tag.updatedAt)}
|
|
497
|
+
</span>
|
|
498
|
+
</div>
|
|
277
499
|
</td>
|
|
278
500
|
</tr>
|
|
279
501
|
)
|
|
@@ -293,6 +515,18 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
293
515
|
/>
|
|
294
516
|
</InspectorPane>
|
|
295
517
|
</SplitPaneLayout>
|
|
518
|
+
|
|
519
|
+
<ConfirmDialog
|
|
520
|
+
open={pendingDelete !== null}
|
|
521
|
+
title="Delete script?"
|
|
522
|
+
description={`"${pendingDelete?.name ?? 'This script'}" will stop firing on your site immediately.`}
|
|
523
|
+
confirmLabel="Delete"
|
|
524
|
+
destructive
|
|
525
|
+
onConfirm={() => {
|
|
526
|
+
if (pendingDelete) void deleteTag(pendingDelete)
|
|
527
|
+
}}
|
|
528
|
+
onClose={() => setPendingDelete(null)}
|
|
529
|
+
/>
|
|
296
530
|
</div>
|
|
297
531
|
)
|
|
298
532
|
}
|