@kyro-cms/admin 0.1.8 → 0.2.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/README.md +3 -0
- package/package.json +6 -3
- package/src/components/Admin.tsx +2 -1
- package/src/components/ApiKeysManager.tsx +45 -49
- package/src/components/AuditLogsPage.tsx +9 -4
- package/src/components/BrandingHub.tsx +24 -32
- package/src/components/CreateView.tsx +6 -13
- package/src/components/DetailView.tsx +8 -30
- package/src/components/DeveloperCenter.tsx +31 -27
- package/src/components/EnhancedListView.tsx +5 -6
- package/src/components/ListView.tsx +21 -19
- package/src/components/LoginPage.tsx +3 -17
- package/src/components/MediaGallery.tsx +103 -105
- package/src/components/UserManagement.tsx +28 -18
- package/src/components/WebhookManager.tsx +43 -56
- package/src/components/fields/RelationshipBlockField.tsx +4 -7
- package/src/components/fields/RelationshipField.tsx +4 -10
- package/src/components/fields/UploadField.tsx +37 -44
- package/src/components/ui/CommandPalette.tsx +1 -0
package/README.md
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Admin dashboard for Kyro CMS — a React-based admin interface built with Astro.
|
|
4
4
|
|
|
5
|
+
Uses `@kyro-cms/utils` for API calls, date formatting, and validation.
|
|
6
|
+
|
|
5
7
|
## Features
|
|
6
8
|
|
|
7
9
|
- **Multi-Database Support** — SQLite, PostgreSQL, MySQL, MongoDB
|
|
8
10
|
- **Authentication** — JWT-based login/logout with auto-selecting auth adapter
|
|
11
|
+
- **Shared Utilities** — Uses @kyro-cms/utils for consistent API handling
|
|
9
12
|
- **Collection Management** — Create, edit, and manage content collections
|
|
10
13
|
- **User Management** — Manage users, roles, and permissions
|
|
11
14
|
- **Settings** — Configure CMS settings, globals, and plugins
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kyro-cms/admin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
5
8
|
"type": "module",
|
|
6
9
|
"description": "Admin dashboard for Kyro CMS",
|
|
7
10
|
"main": "./src/index.ts",
|
|
@@ -44,8 +47,8 @@
|
|
|
44
47
|
"@dnd-kit/sortable": "^10.0.0",
|
|
45
48
|
"@dnd-kit/utilities": "^3.2.2",
|
|
46
49
|
"@graphiql/react": "^0.37.3",
|
|
47
|
-
"@kyro-cms/core": "
|
|
48
|
-
"@kyro-cms/utils": "
|
|
50
|
+
"@kyro-cms/core": "^0.2.0",
|
|
51
|
+
"@kyro-cms/utils": "^0.2.0",
|
|
49
52
|
"@platejs/dnd": "^53.0.0",
|
|
50
53
|
"@portabletext/editor": "^6.6.3",
|
|
51
54
|
"@portabletext/react": "^6.0.3",
|
package/src/components/Admin.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect } from "react";
|
|
2
|
+
import { apiPost } from "@kyro-cms/utils/lib/api";
|
|
2
3
|
import type { CollectionConfig, GlobalConfig } from "@kyro-cms/core/client";
|
|
3
4
|
import { ListView } from "./ListView";
|
|
4
5
|
import { DetailView } from "./DetailView";
|
|
@@ -141,7 +142,7 @@ export function Admin({ config, theme = "light", onThemeChange }: AdminProps) {
|
|
|
141
142
|
|
|
142
143
|
const handleLogout = async () => {
|
|
143
144
|
try {
|
|
144
|
-
await
|
|
145
|
+
await apiPost("/api/auth/logout");
|
|
145
146
|
} catch {
|
|
146
147
|
} finally {
|
|
147
148
|
localStorage.removeItem("kyro_token");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { apiGet, apiPost, apiDelete } from "@kyro-cms/utils/lib/api";
|
|
2
3
|
import {
|
|
3
4
|
Key,
|
|
4
5
|
Plus,
|
|
@@ -44,17 +45,14 @@ export function ApiKeysManager() {
|
|
|
44
45
|
const loadKeys = async () => {
|
|
45
46
|
setLoading(true);
|
|
46
47
|
try {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
})),
|
|
56
|
-
);
|
|
57
|
-
}
|
|
48
|
+
const data = await apiGet("/api/keys");
|
|
49
|
+
setKeys(
|
|
50
|
+
data.map((k: any) => ({
|
|
51
|
+
...k,
|
|
52
|
+
key: k.key,
|
|
53
|
+
keyPrefix: k.keyPrefix || k.key?.substring(0, 8) || "",
|
|
54
|
+
})),
|
|
55
|
+
);
|
|
58
56
|
} catch (e) {
|
|
59
57
|
console.error(e);
|
|
60
58
|
} finally {
|
|
@@ -73,23 +71,12 @@ export function ApiKeysManager() {
|
|
|
73
71
|
}
|
|
74
72
|
|
|
75
73
|
try {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (res.ok) {
|
|
83
|
-
const created = await res.json();
|
|
84
|
-
setNewKey(created);
|
|
85
|
-
setShowCreateModal(false);
|
|
86
|
-
setNewKeyName("");
|
|
87
|
-
setCreateError("");
|
|
88
|
-
loadKeys();
|
|
89
|
-
} else {
|
|
90
|
-
const error = await res.json();
|
|
91
|
-
setCreateError(error.error || "Failed to create API key");
|
|
92
|
-
}
|
|
74
|
+
const created = await apiPost("/api/keys", { name: newKeyName });
|
|
75
|
+
setNewKey(created);
|
|
76
|
+
setShowCreateModal(false);
|
|
77
|
+
setNewKeyName("");
|
|
78
|
+
setCreateError("");
|
|
79
|
+
loadKeys();
|
|
93
80
|
} catch (e) {
|
|
94
81
|
console.error(e);
|
|
95
82
|
setCreateError("Failed to create API key");
|
|
@@ -105,13 +92,8 @@ export function ApiKeysManager() {
|
|
|
105
92
|
if (!deleteKeyId) return;
|
|
106
93
|
|
|
107
94
|
try {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
loadKeys();
|
|
111
|
-
} else {
|
|
112
|
-
setAlertMessage("Failed to delete API key");
|
|
113
|
-
setShowAlertModal(true);
|
|
114
|
-
}
|
|
95
|
+
await apiDelete(`/api/keys/${deleteKeyId}`);
|
|
96
|
+
loadKeys();
|
|
115
97
|
} catch (e) {
|
|
116
98
|
console.error(e);
|
|
117
99
|
setAlertMessage("Failed to delete API key");
|
|
@@ -136,7 +118,8 @@ export function ApiKeysManager() {
|
|
|
136
118
|
<h1 className="text-4xl font-black tracking-tighter text-[var(--kyro-text-primary)]">
|
|
137
119
|
API <span className="text-[var(--kyro-primary)]">Keys</span>
|
|
138
120
|
</h1>
|
|
139
|
-
<button
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
140
123
|
onClick={() => setShowHelpModal(true)}
|
|
141
124
|
className="p-2 rounded-lg text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] hover:bg-[var(--kyro-surface-accent)] transition-all"
|
|
142
125
|
title="Learn how API keys work"
|
|
@@ -148,7 +131,8 @@ export function ApiKeysManager() {
|
|
|
148
131
|
Secure tokens for authenticating API requests.
|
|
149
132
|
</p>
|
|
150
133
|
</div>
|
|
151
|
-
<button
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
152
136
|
onClick={() => {
|
|
153
137
|
setNewKeyName("");
|
|
154
138
|
setCreateError("");
|
|
@@ -187,7 +171,8 @@ export function ApiKeysManager() {
|
|
|
187
171
|
-H "Authorization: ApiKey kyro_xxx"
|
|
188
172
|
</div>
|
|
189
173
|
</div>
|
|
190
|
-
<button
|
|
174
|
+
<button
|
|
175
|
+
type="button"
|
|
191
176
|
onClick={() => {
|
|
192
177
|
navigator.clipboard.writeText(
|
|
193
178
|
'curl -X GET https://yoursite.com/api/posts -H "Authorization: ApiKey YOUR_KEY"',
|
|
@@ -276,7 +261,8 @@ export function ApiKeysManager() {
|
|
|
276
261
|
<code className="flex-1 p-4 bg-[var(--kyro-bg)] border border-green-500/30 rounded-xl font-mono text-sm break-all">
|
|
277
262
|
{newKey.key}
|
|
278
263
|
</code>
|
|
279
|
-
<button
|
|
264
|
+
<button
|
|
265
|
+
type="button"
|
|
280
266
|
onClick={() => copyToClipboard(newKey.key!, newKey.id)}
|
|
281
267
|
className="p-4 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl hover:opacity-90 flex-shrink-0"
|
|
282
268
|
title="Copy to clipboard"
|
|
@@ -290,7 +276,8 @@ export function ApiKeysManager() {
|
|
|
290
276
|
</div>
|
|
291
277
|
</div>
|
|
292
278
|
</div>
|
|
293
|
-
<button
|
|
279
|
+
<button
|
|
280
|
+
type="button"
|
|
294
281
|
onClick={() => setNewKey(null)}
|
|
295
282
|
className="mt-6 text-sm font-bold text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)]"
|
|
296
283
|
>
|
|
@@ -319,7 +306,8 @@ export function ApiKeysManager() {
|
|
|
319
306
|
<p className="text-sm text-[var(--kyro-text-secondary)] opacity-60 mb-6">
|
|
320
307
|
Create your first API key to authenticate with the API.
|
|
321
308
|
</p>
|
|
322
|
-
<button
|
|
309
|
+
<button
|
|
310
|
+
type="button"
|
|
323
311
|
onClick={() => setShowCreateModal(true)}
|
|
324
312
|
className="px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-full font-black text-sm hover:opacity-90 transition-all"
|
|
325
313
|
>
|
|
@@ -363,7 +351,8 @@ export function ApiKeysManager() {
|
|
|
363
351
|
</div>
|
|
364
352
|
</div>
|
|
365
353
|
<div className="flex items-center gap-2">
|
|
366
|
-
<button
|
|
354
|
+
<button
|
|
355
|
+
type="button"
|
|
367
356
|
onClick={() =>
|
|
368
357
|
copyToClipboard(
|
|
369
358
|
key.key || `${key.keyPrefix}...`,
|
|
@@ -381,7 +370,8 @@ export function ApiKeysManager() {
|
|
|
381
370
|
{copiedId === key.id ? "Copied!" : "Copy"}
|
|
382
371
|
</span>
|
|
383
372
|
</button>
|
|
384
|
-
<button
|
|
373
|
+
<button
|
|
374
|
+
type="button"
|
|
385
375
|
onClick={() => handleDeleteKey(key.id)}
|
|
386
376
|
className="p-3 text-red-500 bg-red-500/10 rounded-xl hover:bg-red-500/20"
|
|
387
377
|
title="Delete API key"
|
|
@@ -433,13 +423,15 @@ export function ApiKeysManager() {
|
|
|
433
423
|
</div>
|
|
434
424
|
</ModalContent>
|
|
435
425
|
<ModalActions>
|
|
436
|
-
<button
|
|
426
|
+
<button
|
|
427
|
+
type="button"
|
|
437
428
|
onClick={() => setShowCreateModal(false)}
|
|
438
429
|
className="px-4 py-2 rounded-lg font-medium text-sm border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)] transition-colors"
|
|
439
430
|
>
|
|
440
431
|
Cancel
|
|
441
432
|
</button>
|
|
442
|
-
<button
|
|
433
|
+
<button
|
|
434
|
+
type="button"
|
|
443
435
|
onClick={handleCreateKey}
|
|
444
436
|
className="px-4 py-2 rounded-lg font-medium text-sm bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90 transition-colors"
|
|
445
437
|
>
|
|
@@ -468,13 +460,15 @@ export function ApiKeysManager() {
|
|
|
468
460
|
</div>
|
|
469
461
|
</ModalContent>
|
|
470
462
|
<ModalActions>
|
|
471
|
-
<button
|
|
463
|
+
<button
|
|
464
|
+
type="button"
|
|
472
465
|
onClick={() => setShowDeleteModal(false)}
|
|
473
466
|
className="px-4 py-2 rounded-lg font-medium text-sm border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)] transition-colors"
|
|
474
467
|
>
|
|
475
468
|
Keep Key
|
|
476
469
|
</button>
|
|
477
|
-
<button
|
|
470
|
+
<button
|
|
471
|
+
type="button"
|
|
478
472
|
onClick={confirmDeleteKey}
|
|
479
473
|
className="px-4 py-2 rounded-lg font-medium text-sm bg-red-500 text-white hover:bg-red-600 transition-colors"
|
|
480
474
|
>
|
|
@@ -529,7 +523,8 @@ export function ApiKeysManager() {
|
|
|
529
523
|
</div>
|
|
530
524
|
</ModalContent>
|
|
531
525
|
<ModalActions>
|
|
532
|
-
<button
|
|
526
|
+
<button
|
|
527
|
+
type="button"
|
|
533
528
|
onClick={() => setShowHelpModal(false)}
|
|
534
529
|
className="px-4 py-2 rounded-lg font-medium text-sm bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90 transition-colors"
|
|
535
530
|
>
|
|
@@ -550,7 +545,8 @@ export function ApiKeysManager() {
|
|
|
550
545
|
</p>
|
|
551
546
|
</ModalContent>
|
|
552
547
|
<ModalActions>
|
|
553
|
-
<button
|
|
548
|
+
<button
|
|
549
|
+
type="button"
|
|
554
550
|
onClick={() => setShowAlertModal(false)}
|
|
555
551
|
className="px-4 py-2 rounded-lg font-medium text-sm bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90 transition-colors"
|
|
556
552
|
>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { apiGet } from "@kyro-cms/utils/lib/api";
|
|
2
3
|
import { Modal } from "./ui/Modal";
|
|
3
4
|
|
|
4
5
|
interface AuditLog {
|
|
@@ -283,7 +284,8 @@ export function AuditLogsPage() {
|
|
|
283
284
|
</select>
|
|
284
285
|
|
|
285
286
|
{(search || action || successFilter) && (
|
|
286
|
-
<button
|
|
287
|
+
<button
|
|
288
|
+
type="button"
|
|
287
289
|
onClick={() => {
|
|
288
290
|
setSearch("");
|
|
289
291
|
setAction("");
|
|
@@ -491,7 +493,8 @@ export function AuditLogsPage() {
|
|
|
491
493
|
</span>
|
|
492
494
|
</p>
|
|
493
495
|
<div className="flex items-center gap-2">
|
|
494
|
-
<button
|
|
496
|
+
<button
|
|
497
|
+
type="button"
|
|
495
498
|
onClick={() => fetchLogs(page - 1)}
|
|
496
499
|
disabled={page <= 1}
|
|
497
500
|
className={`px-4 py-2 rounded-xl text-sm font-bold transition-all ${
|
|
@@ -505,7 +508,8 @@ export function AuditLogsPage() {
|
|
|
505
508
|
{Array.from({ length: Math.min(totalPages, 7) }, (_, i) => {
|
|
506
509
|
const p = i + 1;
|
|
507
510
|
return (
|
|
508
|
-
<button
|
|
511
|
+
<button
|
|
512
|
+
type="button"
|
|
509
513
|
key={p}
|
|
510
514
|
onClick={() => fetchLogs(p)}
|
|
511
515
|
className={`px-3.5 py-2 rounded-xl text-sm font-bold transition-all ${
|
|
@@ -518,7 +522,8 @@ export function AuditLogsPage() {
|
|
|
518
522
|
</button>
|
|
519
523
|
);
|
|
520
524
|
})}
|
|
521
|
-
<button
|
|
525
|
+
<button
|
|
526
|
+
type="button"
|
|
522
527
|
onClick={() => fetchLogs(page + 1)}
|
|
523
528
|
disabled={page >= totalPages}
|
|
524
529
|
className={`px-4 py-2 rounded-xl text-sm font-bold transition-all ${
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { apiGet, apiPatch } from "@kyro-cms/utils/lib/api";
|
|
2
3
|
import {
|
|
3
4
|
Palette,
|
|
4
5
|
Tag,
|
|
@@ -24,17 +25,14 @@ export function BrandingHub() {
|
|
|
24
25
|
useEffect(() => {
|
|
25
26
|
const fetchBranding = async () => {
|
|
26
27
|
try {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (data
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (data.dashboardGreeting)
|
|
36
|
-
setDashboardGreeting(data.dashboardGreeting);
|
|
37
|
-
}
|
|
28
|
+
const result = await apiGet("/api/globals/site");
|
|
29
|
+
const data = result.data || result;
|
|
30
|
+
if (data && Object.keys(data).length > 0) {
|
|
31
|
+
if (data.siteName) setSiteName(data.siteName);
|
|
32
|
+
if (data.adminTitle) setAdminTitle(data.adminTitle);
|
|
33
|
+
if (data.primaryColor) setPrimaryColor(data.primaryColor);
|
|
34
|
+
if (data.dashboardGreeting)
|
|
35
|
+
setDashboardGreeting(data.dashboardGreeting);
|
|
38
36
|
}
|
|
39
37
|
} catch (err) {
|
|
40
38
|
console.error("Failed to load branding:", err);
|
|
@@ -46,26 +44,18 @@ export function BrandingHub() {
|
|
|
46
44
|
const handleSave = async () => {
|
|
47
45
|
setSaving(true);
|
|
48
46
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
adminTitle,
|
|
55
|
-
primaryColor,
|
|
56
|
-
dashboardGreeting,
|
|
57
|
-
}),
|
|
47
|
+
await apiPatch("/api/globals/site", {
|
|
48
|
+
siteName,
|
|
49
|
+
adminTitle,
|
|
50
|
+
primaryColor,
|
|
51
|
+
dashboardGreeting,
|
|
58
52
|
});
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
);
|
|
66
|
-
} else {
|
|
67
|
-
throw new Error("Failed to save");
|
|
68
|
-
}
|
|
53
|
+
setSaved(true);
|
|
54
|
+
setTimeout(() => setSaved(false), 3000);
|
|
55
|
+
document.documentElement.style.setProperty(
|
|
56
|
+
"--kyro-primary",
|
|
57
|
+
primaryColor,
|
|
58
|
+
);
|
|
69
59
|
} catch (e) {
|
|
70
60
|
console.error(e);
|
|
71
61
|
} finally {
|
|
@@ -96,7 +86,8 @@ export function BrandingHub() {
|
|
|
96
86
|
</p>
|
|
97
87
|
</div>
|
|
98
88
|
<div className="flex items-center gap-3">
|
|
99
|
-
<button
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
100
91
|
onClick={handleSave}
|
|
101
92
|
disabled={saving}
|
|
102
93
|
className={`flex items-center gap-2 px-8 py-3 rounded-2xl font-black text-sm shadow-xl transition-all active:scale-95 ${
|
|
@@ -187,7 +178,8 @@ export function BrandingHub() {
|
|
|
187
178
|
</label>
|
|
188
179
|
<div className="grid grid-cols-6 gap-3">
|
|
189
180
|
{colors.map((c) => (
|
|
190
|
-
<button
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
191
183
|
key={c.name}
|
|
192
184
|
onClick={() => setPrimaryColor(c.hex)}
|
|
193
185
|
className={`aspect-square rounded-xl transition-all border-4 ${primaryColor === c.hex ? "border-white ring-2 ring-[var(--kyro-primary)]" : "border-transparent opacity-60 hover:opacity-100"}`}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
|
+
import { apiPost } from "@kyro-cms/utils/lib/api";
|
|
2
3
|
import type { KyroConfig, CollectionConfig } from "@kyro-cms/core/client";
|
|
3
4
|
import { AutoForm } from "./AutoForm";
|
|
4
5
|
import { Spinner } from "./ui/Spinner";
|
|
@@ -28,17 +29,7 @@ export function CreateView({
|
|
|
28
29
|
e.preventDefault();
|
|
29
30
|
try {
|
|
30
31
|
setSaving(true);
|
|
31
|
-
|
|
32
|
-
method: "POST",
|
|
33
|
-
headers: { "Content-Type": "application/json" },
|
|
34
|
-
body: JSON.stringify(data),
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
if (!response.ok) {
|
|
38
|
-
const error = await response.json();
|
|
39
|
-
throw new Error(error.message || "Failed to create");
|
|
40
|
-
}
|
|
41
|
-
|
|
32
|
+
await apiPost(`/api/${collection.slug}`, data);
|
|
42
33
|
onSuccess();
|
|
43
34
|
} catch (err) {
|
|
44
35
|
onError(err instanceof Error ? err.message : "Failed to create");
|
|
@@ -64,14 +55,16 @@ export function CreateView({
|
|
|
64
55
|
</button>
|
|
65
56
|
<h2 className="kyro-detail-title">Create {label}</h2>
|
|
66
57
|
<div className="kyro-detail-actions">
|
|
67
|
-
<button
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
68
60
|
className="kyro-btn kyro-btn-secondary kyro-btn-md"
|
|
69
61
|
onClick={onCancel}
|
|
70
62
|
disabled={saving}
|
|
71
63
|
>
|
|
72
64
|
Cancel
|
|
73
65
|
</button>
|
|
74
|
-
<button
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
75
68
|
className="kyro-btn kyro-btn-primary kyro-btn-md"
|
|
76
69
|
onClick={handleSubmit}
|
|
77
70
|
disabled={saving}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useRef } from "react";
|
|
2
|
+
import { apiGet, apiPatch, apiPost, apiDelete } from "@kyro-cms/utils/lib/api";
|
|
2
3
|
import type {
|
|
3
4
|
KyroConfig,
|
|
4
5
|
CollectionConfig,
|
|
@@ -107,9 +108,7 @@ export function DetailView({
|
|
|
107
108
|
const loadDocument = async () => {
|
|
108
109
|
try {
|
|
109
110
|
setLoading(true);
|
|
110
|
-
const
|
|
111
|
-
if (!response.ok) throw new Error("Failed to load document");
|
|
112
|
-
const result = await response.json();
|
|
111
|
+
const result = await apiGet(`/api/${slug}/${documentId}`);
|
|
113
112
|
const docData = result.data || {};
|
|
114
113
|
setData(docData);
|
|
115
114
|
setOriginalData(docData);
|
|
@@ -127,9 +126,7 @@ export function DetailView({
|
|
|
127
126
|
const loadGlobal = async () => {
|
|
128
127
|
try {
|
|
129
128
|
setLoading(true);
|
|
130
|
-
const
|
|
131
|
-
if (!response.ok) throw new Error("Failed to load global");
|
|
132
|
-
const result = await response.json();
|
|
129
|
+
const result = await apiGet(`/api/globals/${slug}`);
|
|
133
130
|
const globalData = result.data || {};
|
|
134
131
|
setData(globalData);
|
|
135
132
|
setOriginalData(globalData);
|
|
@@ -151,13 +148,7 @@ export function DetailView({
|
|
|
151
148
|
? `/api/globals/${slug}`
|
|
152
149
|
: `/api/${slug}/${documentId}`;
|
|
153
150
|
|
|
154
|
-
|
|
155
|
-
method: "PATCH",
|
|
156
|
-
headers: { "Content-Type": "application/json" },
|
|
157
|
-
body: JSON.stringify(data),
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
if (!response.ok) throw new Error("Failed to save");
|
|
151
|
+
await apiPatch(endpoint, data);
|
|
161
152
|
|
|
162
153
|
if (!isAutosave) {
|
|
163
154
|
setOriginalData(data);
|
|
@@ -193,10 +184,7 @@ export function DetailView({
|
|
|
193
184
|
const handlePublish = async () => {
|
|
194
185
|
try {
|
|
195
186
|
setSaving(true);
|
|
196
|
-
|
|
197
|
-
method: "POST",
|
|
198
|
-
});
|
|
199
|
-
if (!response.ok) throw new Error("Failed to publish");
|
|
187
|
+
await apiPost(`/api/${slug}/${documentId}/publish`);
|
|
200
188
|
setStatus("published");
|
|
201
189
|
setPublishedAt(new Date().toISOString());
|
|
202
190
|
addToast("success", "Published successfully");
|
|
@@ -211,10 +199,7 @@ export function DetailView({
|
|
|
211
199
|
const handleUnpublish = async () => {
|
|
212
200
|
try {
|
|
213
201
|
setSaving(true);
|
|
214
|
-
|
|
215
|
-
method: "POST",
|
|
216
|
-
});
|
|
217
|
-
if (!response.ok) throw new Error("Failed to unpublish");
|
|
202
|
+
await apiPost(`/api/${slug}/${documentId}/unpublish`);
|
|
218
203
|
setStatus("draft");
|
|
219
204
|
} catch {
|
|
220
205
|
onError("Failed to unpublish");
|
|
@@ -225,10 +210,7 @@ export function DetailView({
|
|
|
225
210
|
|
|
226
211
|
const handleDuplicate = async () => {
|
|
227
212
|
try {
|
|
228
|
-
|
|
229
|
-
method: "POST",
|
|
230
|
-
});
|
|
231
|
-
if (!response.ok) throw new Error("Failed to duplicate");
|
|
213
|
+
await apiPost(`/api/${slug}/${documentId}/duplicate`);
|
|
232
214
|
onError("Document duplicated successfully");
|
|
233
215
|
} catch {
|
|
234
216
|
onError("Failed to duplicate document");
|
|
@@ -238,11 +220,7 @@ export function DetailView({
|
|
|
238
220
|
const handleDelete = async () => {
|
|
239
221
|
try {
|
|
240
222
|
setDeleting(true);
|
|
241
|
-
|
|
242
|
-
method: "DELETE",
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
if (!response.ok) throw new Error("Failed to delete");
|
|
223
|
+
await apiDelete(`/api/${slug}/${documentId}`);
|
|
246
224
|
onDelete?.();
|
|
247
225
|
} catch {
|
|
248
226
|
onError("Failed to delete document");
|