@kyro-cms/admin 0.11.4 → 0.11.5
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/index.cjs +19 -19
- package/dist/index.css +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +19 -19
- package/package.json +1 -1
- package/src/components/AutoForm.tsx +47 -27
- package/src/components/CreateView.tsx +1 -0
- package/src/components/DashboardMetrics.tsx +280 -0
- package/src/components/DetailView.tsx +22 -20
- package/src/components/MediaGallery.tsx +80 -123
- package/src/components/SessionsManager.tsx +5 -0
- package/src/components/Sidebar.astro +36 -17
- package/src/components/autoform/AutoFormHeader.tsx +5 -2
- package/src/components/fields/TabsLayout.tsx +1 -1
- package/src/components/ui/CommandPaletteWrapper.tsx +2 -3
- package/src/components/ui/ImageFocalEditor.tsx +299 -0
- package/src/components/ui/Modal.tsx +1 -1
- package/src/components/ui/PageHeader.tsx +5 -2
- package/src/integration.ts +32 -0
- package/src/layouts/AdminLayout.astro +1 -1
- package/src/lib/globals.ts +53 -9
- package/src/pages/index.astro +34 -113
- package/src/pages/media.astro +1 -1
- package/src/pages/settings/[slug].astro +19 -1
- package/src/styles/main.css +50 -38
package/package.json
CHANGED
|
@@ -167,6 +167,7 @@ export function AutoForm({
|
|
|
167
167
|
const scheduleRef = useRef<HTMLDivElement>(null);
|
|
168
168
|
const [showSchedulePicker, setShowSchedulePicker] = useState(false);
|
|
169
169
|
const [localSaveStatus, setLocalSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle");
|
|
170
|
+
const [isDuplicating, setIsDuplicating] = useState(false);
|
|
170
171
|
const [now, setNow] = useState(Date.now());
|
|
171
172
|
const disabled = propDisabled;
|
|
172
173
|
const [clientLoading, setClientLoading] = useState(false);
|
|
@@ -401,37 +402,54 @@ export function AutoForm({
|
|
|
401
402
|
};
|
|
402
403
|
|
|
403
404
|
const handleDuplicate = () => {
|
|
405
|
+
if (!formData.id) {
|
|
406
|
+
toast.error("Please save the document before duplicating.");
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const performDuplicate = async () => {
|
|
411
|
+
try {
|
|
412
|
+
setIsDuplicating(true);
|
|
413
|
+
const response = await fetchWithAuth(`/api/${collectionSlug}/${formData.id}/duplicate`, {
|
|
414
|
+
method: "POST",
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
if (response.ok) {
|
|
418
|
+
const result = await response.json();
|
|
419
|
+
onActionSuccess?.("Document duplicated successfully");
|
|
420
|
+
if (result.data?.id) {
|
|
421
|
+
window.location.href = `${ADMIN_BASE}/${collectionSlug}/${result.data.id}`;
|
|
422
|
+
} else {
|
|
423
|
+
window.location.href = `${ADMIN_BASE}/${collectionSlug}`;
|
|
424
|
+
}
|
|
425
|
+
} else {
|
|
426
|
+
const err = await response.json();
|
|
427
|
+
toast.error(err.error || "Failed to duplicate");
|
|
428
|
+
}
|
|
429
|
+
} catch (e) {
|
|
430
|
+
toast.error("Failed to duplicate document");
|
|
431
|
+
} finally {
|
|
432
|
+
setIsDuplicating(false);
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
if (hasUnsavedChanges) {
|
|
437
|
+
confirm({
|
|
438
|
+
title: "Unsaved Changes",
|
|
439
|
+
message: "You have unsaved changes. Please save the document before duplicating.",
|
|
440
|
+
onConfirm: async () => {
|
|
441
|
+
await handleSaveDraft();
|
|
442
|
+
await performDuplicate();
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
404
448
|
confirm({
|
|
405
449
|
title: "Duplicate Document",
|
|
406
450
|
message: "Are you sure you want to duplicate this document?",
|
|
407
451
|
onConfirm: async () => {
|
|
408
|
-
|
|
409
|
-
const { id, createdAt, updatedAt, ...duplicateData } = formData;
|
|
410
|
-
const normalizedData = normalizeUploadFields(duplicateData) as Record<string, unknown>;
|
|
411
|
-
const response = await fetchWithAuth(`/api/${collectionSlug}`, {
|
|
412
|
-
method: "POST",
|
|
413
|
-
headers: { "Content-Type": "application/json" },
|
|
414
|
-
body: JSON.stringify({
|
|
415
|
-
...normalizedData,
|
|
416
|
-
title: `${duplicateData.title || duplicateData.name || "Copy"} (Copy)`,
|
|
417
|
-
slug: `${duplicateData.slug || "copy"}-${Date.now()}`,
|
|
418
|
-
status: "draft",
|
|
419
|
-
}),
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
if (response.ok) {
|
|
423
|
-
const result = await response.json();
|
|
424
|
-
onActionSuccess?.("Document duplicated successfully");
|
|
425
|
-
if (result.data?.id) {
|
|
426
|
-
window.location.href = `${ADMIN_BASE}/${collectionSlug}/${result.data.id}`;
|
|
427
|
-
}
|
|
428
|
-
} else {
|
|
429
|
-
const error = await response.json();
|
|
430
|
-
toast.error(error.error || "Failed to duplicate document");
|
|
431
|
-
}
|
|
432
|
-
} catch (err) {
|
|
433
|
-
toast.error("Failed to duplicate document");
|
|
434
|
-
}
|
|
452
|
+
await performDuplicate();
|
|
435
453
|
},
|
|
436
454
|
});
|
|
437
455
|
};
|
|
@@ -532,6 +550,7 @@ export function AutoForm({
|
|
|
532
550
|
toast.error("Failed to save document");
|
|
533
551
|
setTimeout(() => setLocalSaveStatus("idle"), 3000);
|
|
534
552
|
} finally {
|
|
553
|
+
window.dispatchEvent(new CustomEvent("kyro:global-save-end"));
|
|
535
554
|
autoSaveSkipRef.current = false;
|
|
536
555
|
}
|
|
537
556
|
};
|
|
@@ -987,6 +1006,7 @@ export function AutoForm({
|
|
|
987
1006
|
documentStatus={documentStatus || "draft"}
|
|
988
1007
|
hasUnpublishedChanges={hasUnpublishedChanges}
|
|
989
1008
|
localSaveStatus={localSaveStatus}
|
|
1009
|
+
isDuplicating={isDuplicating}
|
|
990
1010
|
handleCreateNew={handleCreateNew}
|
|
991
1011
|
handleDuplicate={handleDuplicate}
|
|
992
1012
|
handleUnpublish={handleUnpublish}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import { apiGet } from "../lib/api";
|
|
3
|
+
|
|
4
|
+
interface MetricsData {
|
|
5
|
+
totalDocuments: number;
|
|
6
|
+
totalMedia: number;
|
|
7
|
+
totalUsers: number;
|
|
8
|
+
totalWebhooks: number;
|
|
9
|
+
totalApiKeys: number;
|
|
10
|
+
totalStoredRecords: number;
|
|
11
|
+
collectionCounts: Record<string, number>;
|
|
12
|
+
collections: number;
|
|
13
|
+
|
|
14
|
+
timestamp: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatNumber(n: number): string {
|
|
18
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
19
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
|
20
|
+
return n.toLocaleString();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function SkeletonCard() {
|
|
24
|
+
return (
|
|
25
|
+
<div className="p-5 bg-[var(--kyro-surface-accent)] rounded-xl border border-[var(--kyro-border)] animate-pulse">
|
|
26
|
+
<div className="w-10 h-10 rounded-lg bg-[var(--kyro-bg-secondary)] mb-3" />
|
|
27
|
+
<div className="h-7 w-16 bg-[var(--kyro-bg-secondary)] rounded mb-2" />
|
|
28
|
+
<div className="h-4 w-24 bg-[var(--kyro-bg-secondary)] rounded" />
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function MetricCard({
|
|
34
|
+
icon,
|
|
35
|
+
iconBg,
|
|
36
|
+
iconColor,
|
|
37
|
+
value,
|
|
38
|
+
label,
|
|
39
|
+
subtext,
|
|
40
|
+
}: {
|
|
41
|
+
icon: React.ReactNode;
|
|
42
|
+
iconBg: string;
|
|
43
|
+
iconColor: string;
|
|
44
|
+
value: string | number;
|
|
45
|
+
label: string;
|
|
46
|
+
subtext?: string;
|
|
47
|
+
}) {
|
|
48
|
+
return (
|
|
49
|
+
<div className="kyro-metric-card group p-5 bg-[var(--kyro-surface-accent)] rounded-lg border border-transparent hover:border-[var(--kyro-border)] transition-all hover:shadow-xs">
|
|
50
|
+
<div
|
|
51
|
+
className="w-10 h-10 rounded-lg flex items-center justify-center mb-3"
|
|
52
|
+
style={{ background: iconBg, color: iconColor }}
|
|
53
|
+
>
|
|
54
|
+
{icon}
|
|
55
|
+
</div>
|
|
56
|
+
<h4 className="text-2xl font-bold text-[var(--kyro-text-primary)] tracking-tight mb-0.5">
|
|
57
|
+
{typeof value === "number" ? formatNumber(value) : value}
|
|
58
|
+
</h4>
|
|
59
|
+
<p className="text-xs font-medium text-[var(--kyro-text-secondary)]">
|
|
60
|
+
{label}
|
|
61
|
+
</p>
|
|
62
|
+
{subtext && (
|
|
63
|
+
<p className="text-[10px] text-[var(--kyro-text-muted)] mt-1">
|
|
64
|
+
{subtext}
|
|
65
|
+
</p>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
export function DashboardMetrics() {
|
|
74
|
+
const [data, setData] = useState<MetricsData | null>(null);
|
|
75
|
+
const [loading, setLoading] = useState(true);
|
|
76
|
+
const [error, setError] = useState<string | null>(null);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
apiGet<MetricsData>("/api/metrics", { autoToast: false })
|
|
80
|
+
.then((res) => {
|
|
81
|
+
setData(res);
|
|
82
|
+
setError(null);
|
|
83
|
+
})
|
|
84
|
+
.catch((err) => {
|
|
85
|
+
console.error("Failed to load metrics:", err);
|
|
86
|
+
setError("Unable to load metrics");
|
|
87
|
+
})
|
|
88
|
+
.finally(() => setLoading(false));
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
if (error) {
|
|
92
|
+
return (
|
|
93
|
+
<div className="surface-tile overflow-hidden">
|
|
94
|
+
<div className="p-6 border-b border-[var(--kyro-border)]">
|
|
95
|
+
<h2 className="text-xl font-bold tracking-tight text-[var(--kyro-text-primary)]">
|
|
96
|
+
CMS Metrics
|
|
97
|
+
</h2>
|
|
98
|
+
</div>
|
|
99
|
+
<div className="p-6">
|
|
100
|
+
<p className="text-sm text-[var(--kyro-text-muted)]">{error}</p>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div className="surface-tile overflow-hidden">
|
|
108
|
+
<div className="p-6 border-b border-[var(--kyro-border)]">
|
|
109
|
+
<div className="flex items-center justify-between">
|
|
110
|
+
<div>
|
|
111
|
+
<h2 className="text-xl font-bold tracking-tight text-[var(--kyro-text-primary)] flex items-center gap-2">
|
|
112
|
+
<svg
|
|
113
|
+
className="w-5 h-5 text-[var(--kyro-primary)]"
|
|
114
|
+
fill="none"
|
|
115
|
+
stroke="currentColor"
|
|
116
|
+
viewBox="0 0 24 24"
|
|
117
|
+
>
|
|
118
|
+
<path
|
|
119
|
+
strokeLinecap="round"
|
|
120
|
+
strokeLinejoin="round"
|
|
121
|
+
strokeWidth="2"
|
|
122
|
+
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
|
123
|
+
/>
|
|
124
|
+
</svg>
|
|
125
|
+
CMS Metrics
|
|
126
|
+
</h2>
|
|
127
|
+
<p className="text-xs text-[var(--kyro-text-secondary)] font-medium mt-1">
|
|
128
|
+
Real-time content, performance, and system health overview
|
|
129
|
+
</p>
|
|
130
|
+
</div>
|
|
131
|
+
{data && (
|
|
132
|
+
<span className="text-[10px] text-[var(--kyro-text-muted)] font-mono">
|
|
133
|
+
{new Date(data.timestamp).toLocaleTimeString()}
|
|
134
|
+
</span>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{/* Primary Metrics */}
|
|
140
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4 p-6">
|
|
141
|
+
{loading ? (
|
|
142
|
+
<>
|
|
143
|
+
<SkeletonCard />
|
|
144
|
+
<SkeletonCard />
|
|
145
|
+
<SkeletonCard />
|
|
146
|
+
<SkeletonCard />
|
|
147
|
+
<SkeletonCard />
|
|
148
|
+
</>
|
|
149
|
+
) : data ? (
|
|
150
|
+
<>
|
|
151
|
+
<MetricCard
|
|
152
|
+
icon={
|
|
153
|
+
<svg
|
|
154
|
+
className="w-5 h-5"
|
|
155
|
+
fill="none"
|
|
156
|
+
stroke="currentColor"
|
|
157
|
+
viewBox="0 0 24 24"
|
|
158
|
+
>
|
|
159
|
+
<path
|
|
160
|
+
strokeLinecap="round"
|
|
161
|
+
strokeLinejoin="round"
|
|
162
|
+
strokeWidth="2"
|
|
163
|
+
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
|
164
|
+
/>
|
|
165
|
+
</svg>
|
|
166
|
+
}
|
|
167
|
+
iconBg="rgba(99, 102, 241, 0.1)"
|
|
168
|
+
iconColor="#6366f1"
|
|
169
|
+
value={data.totalDocuments}
|
|
170
|
+
label="Documents"
|
|
171
|
+
subtext={`Across ${data.collections} collection${data.collections !== 1 ? "s" : ""}`}
|
|
172
|
+
/>
|
|
173
|
+
|
|
174
|
+
{data.totalMedia !== undefined && (
|
|
175
|
+
<MetricCard
|
|
176
|
+
icon={
|
|
177
|
+
<svg
|
|
178
|
+
className="w-5 h-5"
|
|
179
|
+
fill="none"
|
|
180
|
+
stroke="currentColor"
|
|
181
|
+
viewBox="0 0 24 24"
|
|
182
|
+
>
|
|
183
|
+
<path
|
|
184
|
+
strokeLinecap="round"
|
|
185
|
+
strokeLinejoin="round"
|
|
186
|
+
strokeWidth="2"
|
|
187
|
+
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
188
|
+
/>
|
|
189
|
+
</svg>
|
|
190
|
+
}
|
|
191
|
+
iconBg="rgba(168, 85, 247, 0.1)"
|
|
192
|
+
iconColor="#a855f7"
|
|
193
|
+
value={data.totalMedia}
|
|
194
|
+
label="Media Files"
|
|
195
|
+
subtext="Images, videos & docs"
|
|
196
|
+
/>
|
|
197
|
+
)}
|
|
198
|
+
|
|
199
|
+
{data.totalUsers !== undefined && (
|
|
200
|
+
<MetricCard
|
|
201
|
+
icon={
|
|
202
|
+
<svg
|
|
203
|
+
className="w-5 h-5"
|
|
204
|
+
fill="none"
|
|
205
|
+
stroke="currentColor"
|
|
206
|
+
viewBox="0 0 24 24"
|
|
207
|
+
>
|
|
208
|
+
<path
|
|
209
|
+
strokeLinecap="round"
|
|
210
|
+
strokeLinejoin="round"
|
|
211
|
+
strokeWidth="2"
|
|
212
|
+
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
|
|
213
|
+
/>
|
|
214
|
+
</svg>
|
|
215
|
+
}
|
|
216
|
+
iconBg="rgba(14, 165, 233, 0.1)"
|
|
217
|
+
iconColor="#0ea5e9"
|
|
218
|
+
value={data.totalUsers}
|
|
219
|
+
label="Team Members"
|
|
220
|
+
subtext="Active user accounts"
|
|
221
|
+
/>
|
|
222
|
+
)}
|
|
223
|
+
|
|
224
|
+
{data.totalWebhooks !== undefined && (
|
|
225
|
+
<MetricCard
|
|
226
|
+
icon={
|
|
227
|
+
<svg
|
|
228
|
+
className="w-5 h-5"
|
|
229
|
+
fill="none"
|
|
230
|
+
stroke="currentColor"
|
|
231
|
+
viewBox="0 0 24 24"
|
|
232
|
+
>
|
|
233
|
+
<path
|
|
234
|
+
strokeLinecap="round"
|
|
235
|
+
strokeLinejoin="round"
|
|
236
|
+
strokeWidth="2"
|
|
237
|
+
d="M13 10V3L4 14h7v7l9-11h-7z"
|
|
238
|
+
/>
|
|
239
|
+
</svg>
|
|
240
|
+
}
|
|
241
|
+
iconBg="rgba(34, 197, 94, 0.1)"
|
|
242
|
+
iconColor="#22c55e"
|
|
243
|
+
value={data.totalWebhooks}
|
|
244
|
+
label="Webhooks"
|
|
245
|
+
subtext="Active integrations"
|
|
246
|
+
/>
|
|
247
|
+
)}
|
|
248
|
+
|
|
249
|
+
{data.totalApiKeys !== undefined && (
|
|
250
|
+
<MetricCard
|
|
251
|
+
icon={
|
|
252
|
+
<svg
|
|
253
|
+
className="w-5 h-5"
|
|
254
|
+
fill="none"
|
|
255
|
+
stroke="currentColor"
|
|
256
|
+
viewBox="0 0 24 24"
|
|
257
|
+
>
|
|
258
|
+
<path
|
|
259
|
+
strokeLinecap="round"
|
|
260
|
+
strokeLinejoin="round"
|
|
261
|
+
strokeWidth="2"
|
|
262
|
+
d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"
|
|
263
|
+
/>
|
|
264
|
+
</svg>
|
|
265
|
+
}
|
|
266
|
+
iconBg="rgba(245, 158, 11, 0.1)"
|
|
267
|
+
iconColor="#f59e0b"
|
|
268
|
+
value={data.totalApiKeys}
|
|
269
|
+
label="API Keys"
|
|
270
|
+
subtext="Developer access tokens"
|
|
271
|
+
/>
|
|
272
|
+
)}
|
|
273
|
+
</>
|
|
274
|
+
) : null}
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
@@ -45,8 +45,9 @@ export function DetailView({
|
|
|
45
45
|
const [originalData, setOriginalData] = useState<Record<string, unknown>>({});
|
|
46
46
|
const [loading, setLoading] = useState(true);
|
|
47
47
|
const [saving, setSaving] = useState(false);
|
|
48
|
+
const [isDuplicating, setIsDuplicating] = useState(false);
|
|
49
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
48
50
|
const [saveStatus, setSaveStatus] = useState<SaveStatus>("idle");
|
|
49
|
-
const [deleting, setDeleting] = useState(false);
|
|
50
51
|
const [status, setStatus] = useState<DocumentStatus>("draft");
|
|
51
52
|
const [createdAt, setCreatedAt] = useState<string | null>(null);
|
|
52
53
|
const [updatedAt, setUpdatedAt] = useState<string | null>(null);
|
|
@@ -213,15 +214,18 @@ export function DetailView({
|
|
|
213
214
|
|
|
214
215
|
const handleDuplicate = async () => {
|
|
215
216
|
try {
|
|
216
|
-
|
|
217
|
-
await apiPost(`/api/${slug}/${documentId}/duplicate`, undefined, { autoToast: false });
|
|
217
|
+
setIsDuplicating(true);
|
|
218
|
+
const response = await apiPost(`/api/${slug}/${documentId}/duplicate`, undefined, { autoToast: false }) as { data?: { id?: string } };
|
|
218
219
|
toast.success("Document duplicated");
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
220
|
+
if (response?.data?.id) {
|
|
221
|
+
window.location.href = `${adminPath}/${slug}/${response.data.id}`;
|
|
222
|
+
} else {
|
|
223
|
+
window.location.href = `${adminPath}/${slug}`;
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {
|
|
226
|
+
toast.error((e as Error).message || "Failed to duplicate document");
|
|
223
227
|
} finally {
|
|
224
|
-
|
|
228
|
+
setIsDuplicating(false);
|
|
225
229
|
}
|
|
226
230
|
};
|
|
227
231
|
|
|
@@ -232,15 +236,11 @@ export function DetailView({
|
|
|
232
236
|
variant: "danger",
|
|
233
237
|
onConfirm: async () => {
|
|
234
238
|
try {
|
|
235
|
-
|
|
236
|
-
await apiDelete(`/api/${slug}/${documentId}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const message = err instanceof Error ? err.message : "Failed to delete document";
|
|
241
|
-
toast.error(message);
|
|
242
|
-
} finally {
|
|
243
|
-
setDeleting(false);
|
|
239
|
+
setIsDeleting(true);
|
|
240
|
+
await apiDelete(`/api/${slug}/${documentId}`);
|
|
241
|
+
window.location.href = `${adminPath}/${slug}`;
|
|
242
|
+
} catch (e) {
|
|
243
|
+
setIsDeleting(false);
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
});
|
|
@@ -350,9 +350,10 @@ export function DetailView({
|
|
|
350
350
|
<button
|
|
351
351
|
type="button"
|
|
352
352
|
onClick={handleDeleteTrigger}
|
|
353
|
-
|
|
353
|
+
disabled={isDeleting || saving}
|
|
354
|
+
className="kyro-btn kyro-btn-sm text-[var(--kyro-danger)] hover:bg-[var(--kyro-danger)]/10 w-full justify-start mt-2"
|
|
354
355
|
>
|
|
355
|
-
Delete
|
|
356
|
+
{isDeleting ? "Deleting..." : "Delete Document"}
|
|
356
357
|
</button>
|
|
357
358
|
)}
|
|
358
359
|
<SplitButton
|
|
@@ -436,9 +437,10 @@ export function DetailView({
|
|
|
436
437
|
<button
|
|
437
438
|
type="button"
|
|
438
439
|
onClick={handleDuplicate}
|
|
440
|
+
disabled={isDuplicating || saving}
|
|
439
441
|
className="kyro-btn kyro-btn-sm kyro-btn-ghost w-full justify-start"
|
|
440
442
|
>
|
|
441
|
-
Duplicate Document
|
|
443
|
+
{isDuplicating ? "Duplicating..." : "Duplicate Document"}
|
|
442
444
|
</button>
|
|
443
445
|
<button
|
|
444
446
|
type="button"
|