@kyro-cms/admin 0.12.7 → 0.12.8
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 +23 -23
- package/dist/index.d.cts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +18 -18
- package/package.json +6 -5
- package/src/components/AuthBridge.tsx +2 -2
- package/src/components/AutoForm.tsx +14 -12
- package/src/components/BrandingHub.tsx +1 -1
- package/src/components/DetailView.tsx +5 -3
- package/src/components/ListView.tsx +15 -5
- package/src/components/MediaGallery.tsx +1 -1
- package/src/components/PluginsManager.tsx +2 -1
- package/src/components/UserMenu.tsx +10 -9
- package/src/components/autoform/AutoFormApiView.tsx +1 -1
- package/src/components/autoform/AutoFormHeader.tsx +1 -1
- package/src/components/autoform/ErrorBoundary.tsx +1 -1
- package/src/components/fields/IconField.tsx +1 -1
- package/src/components/ui/Button.tsx +5 -2
- package/src/components/users/UserDetail.tsx +7 -7
- package/src/components/users/UserForm.tsx +3 -2
- package/src/components/users/UsersList.tsx +5 -4
- package/src/env.d.ts +4 -0
- package/src/hooks/useQueue.ts +3 -2
- package/src/kyro-cms.d.ts +5 -0
- package/src/lib/autoform-store.ts +4 -3
- package/src/lib/normalize-upload-fields.ts +17 -4
- package/src/pages/[collection]/index.astro +1 -1
- package/src/pages/users/index.astro +1 -1
- package/src/plugins/seo-admin.tsx +1 -1
- package/src/components/fix_imports.cjs +0 -23
- package/src/components/fix_imports2.cjs +0 -19
- package/src/components/replace_svgs.cjs +0 -63
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kyro-cms/admin",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.8",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=22"
|
|
6
6
|
},
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
"prepare": "npm run build"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@astrojs/node": "^
|
|
65
|
-
"@astrojs/react": "^
|
|
64
|
+
"@astrojs/node": "^11.0.2",
|
|
65
|
+
"@astrojs/react": "^6.0.1",
|
|
66
66
|
"@codemirror/autocomplete": "^6.20.3",
|
|
67
67
|
"@codemirror/lang-cpp": "^6.0.3",
|
|
68
68
|
"@codemirror/lang-css": "^6.3.1",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"@dnd-kit/sortable": "^10.0.0",
|
|
82
82
|
"@dnd-kit/utilities": "^3.2.2",
|
|
83
83
|
"@graphiql/react": "^0.37.3",
|
|
84
|
-
"@kyro-cms/core": "
|
|
84
|
+
"@kyro-cms/core": "^0.12.7",
|
|
85
85
|
"@tailwindcss/vite": "^4.0.0",
|
|
86
86
|
"@tiptap/extension-color": "^3.23.6",
|
|
87
87
|
"@tiptap/extension-highlight": "^3.23.6",
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
"@uiw/codemirror-theme-dracula": "^4.25.9",
|
|
101
101
|
"@uiw/codemirror-theme-github": "^4.25.9",
|
|
102
102
|
"@uiw/react-codemirror": "^4.25.9",
|
|
103
|
-
"astro": "^
|
|
103
|
+
"astro": "^7.1.1",
|
|
104
104
|
"astro-loading-indicator": "^0.8.0",
|
|
105
105
|
"dotenv": "^17.4.2",
|
|
106
106
|
"graphiql": "^5.2.2",
|
|
@@ -128,6 +128,7 @@
|
|
|
128
128
|
"i18next-parser": "^9.4.0",
|
|
129
129
|
"tsup": "^8.5.1",
|
|
130
130
|
"typescript": "^5.7.3",
|
|
131
|
+
"vite": "^8.1.5",
|
|
131
132
|
"vitest": "^4.1.4"
|
|
132
133
|
},
|
|
133
134
|
"peerDependencies": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
|
-
import { useAuthStore } from "../lib/stores";
|
|
2
|
+
import { useAuthStore, type AuthUser, type Permissions } from "../lib/stores";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* AuthBridge
|
|
@@ -24,7 +24,7 @@ export function AuthBridge() {
|
|
|
24
24
|
|
|
25
25
|
function populate(detail: { user: unknown; permissions: unknown }) {
|
|
26
26
|
if (detail?.user) {
|
|
27
|
-
setUser(detail.user, detail.permissions ?? null);
|
|
27
|
+
setUser(detail.user as AuthUser, (detail.permissions ?? null) as Permissions | null);
|
|
28
28
|
} else {
|
|
29
29
|
// Unauthenticated – set loading to false so components don't hang
|
|
30
30
|
setLoading(false);
|
|
@@ -10,6 +10,8 @@ import type { DeclarativeCondition } from "../lib/core-types";
|
|
|
10
10
|
type View = "edit" | "version" | "api";
|
|
11
11
|
import { globals, collections } from "../lib/config";
|
|
12
12
|
import { resolveUrl, apiGet, apiDelete, fetchWithAuth } from "../lib/api";
|
|
13
|
+
import { EmptyState } from "./ui/EmptyState";
|
|
14
|
+
import { navigate } from "astro:transitions/client";
|
|
13
15
|
import { Shimmer } from "./ui/Shimmer";
|
|
14
16
|
import { normalizeUploadFields } from "../lib/normalize-upload-fields";
|
|
15
17
|
import { useAutoFormStore } from "../lib/autoform-store";
|
|
@@ -426,11 +428,11 @@ export function AutoForm({
|
|
|
426
428
|
onConfirm: async () => {
|
|
427
429
|
await handleSaveDraft();
|
|
428
430
|
await new Promise((r) => setTimeout(r, 1000));
|
|
429
|
-
|
|
431
|
+
navigate(`${ADMIN_BASE}/${collectionSlug}/new`);
|
|
430
432
|
},
|
|
431
433
|
});
|
|
432
434
|
} else {
|
|
433
|
-
|
|
435
|
+
navigate(`${ADMIN_BASE}/${collectionSlug}/new`);
|
|
434
436
|
}
|
|
435
437
|
};
|
|
436
438
|
|
|
@@ -451,9 +453,9 @@ export function AutoForm({
|
|
|
451
453
|
const result = await response.json();
|
|
452
454
|
onActionSuccess?.("Document duplicated successfully");
|
|
453
455
|
if (result.data?.id) {
|
|
454
|
-
|
|
456
|
+
navigate(`${ADMIN_BASE}/${collectionSlug}/${result.data.id}`);
|
|
455
457
|
} else {
|
|
456
|
-
|
|
458
|
+
navigate(`${ADMIN_BASE}/${collectionSlug}`);
|
|
457
459
|
}
|
|
458
460
|
} else {
|
|
459
461
|
const err = await response.json();
|
|
@@ -496,7 +498,7 @@ export function AutoForm({
|
|
|
496
498
|
autoSaveSkipRef.current = true;
|
|
497
499
|
try {
|
|
498
500
|
await apiDelete(`/api/${collectionSlug}/${formData.id}`);
|
|
499
|
-
|
|
501
|
+
navigate(`${ADMIN_BASE}/${collectionSlug}`);
|
|
500
502
|
} catch (err) {
|
|
501
503
|
toast.error((err as Error).message || "Failed to delete document");
|
|
502
504
|
} finally {
|
|
@@ -543,7 +545,7 @@ export function AutoForm({
|
|
|
543
545
|
setLocalSaveStatus("saving");
|
|
544
546
|
|
|
545
547
|
try {
|
|
546
|
-
const data = normalizeUploadFields({ ...formData }) as Record<string, unknown>;
|
|
548
|
+
const data = normalizeUploadFields({ ...formData }, true) as Record<string, unknown>;
|
|
547
549
|
const isPost = isNewDoc && !globalSlug;
|
|
548
550
|
|
|
549
551
|
const response = isPost
|
|
@@ -574,7 +576,7 @@ export function AutoForm({
|
|
|
574
576
|
}
|
|
575
577
|
if (isPost) {
|
|
576
578
|
setTimeout(() => {
|
|
577
|
-
|
|
579
|
+
navigate(`${ADMIN_BASE}/${collectionSlug}/${result.data.id}`);
|
|
578
580
|
}, 800);
|
|
579
581
|
}
|
|
580
582
|
} else {
|
|
@@ -606,7 +608,7 @@ export function AutoForm({
|
|
|
606
608
|
|
|
607
609
|
if (isNewDoc && !globalSlug) {
|
|
608
610
|
// Create then immediately publish
|
|
609
|
-
const data = normalizeUploadFields({ ...formData }) as Record<string, unknown>;
|
|
611
|
+
const data = normalizeUploadFields({ ...formData }, true) as Record<string, unknown>;
|
|
610
612
|
const response = await fetchWithAuth(`/api/${collectionSlug}`, {
|
|
611
613
|
method: "POST",
|
|
612
614
|
headers: { "Content-Type": "application/json" },
|
|
@@ -627,7 +629,7 @@ export function AutoForm({
|
|
|
627
629
|
}
|
|
628
630
|
|
|
629
631
|
// Save and publish (X-Draft: false writes to main doc + versions table)
|
|
630
|
-
const data = normalizeUploadFields(dataToPublish) as Record<string, unknown>;
|
|
632
|
+
const data = normalizeUploadFields(dataToPublish, true) as Record<string, unknown>;
|
|
631
633
|
const response = await saveDocument(data, false);
|
|
632
634
|
|
|
633
635
|
if (response?.ok) {
|
|
@@ -643,7 +645,7 @@ export function AutoForm({
|
|
|
643
645
|
|
|
644
646
|
if (isNewDoc && !globalSlug && dataToPublish.id) {
|
|
645
647
|
setTimeout(() => {
|
|
646
|
-
|
|
648
|
+
navigate(`${ADMIN_BASE}/${collectionSlug}/${dataToPublish.id}`);
|
|
647
649
|
}, 800);
|
|
648
650
|
}
|
|
649
651
|
} else {
|
|
@@ -653,7 +655,7 @@ export function AutoForm({
|
|
|
653
655
|
const error = await response?.json().catch(() => ({}));
|
|
654
656
|
toast.warning("Document saved as draft. Publishing failed: " + (error?.error || "Unknown error"));
|
|
655
657
|
setTimeout(() => {
|
|
656
|
-
|
|
658
|
+
navigate(`${ADMIN_BASE}/${collectionSlug}/${dataToPublish.id}`);
|
|
657
659
|
}, 1200);
|
|
658
660
|
return;
|
|
659
661
|
}
|
|
@@ -679,7 +681,7 @@ export function AutoForm({
|
|
|
679
681
|
|
|
680
682
|
try {
|
|
681
683
|
const data = {
|
|
682
|
-
...normalizeUploadFields({ ...formData }) as Record<string, unknown>,
|
|
684
|
+
...normalizeUploadFields({ ...formData }, true) as Record<string, unknown>,
|
|
683
685
|
_schedulePublishAt: scheduledFor,
|
|
684
686
|
};
|
|
685
687
|
|
|
@@ -59,7 +59,7 @@ export function BrandingHub() {
|
|
|
59
59
|
"--kyro-primary",
|
|
60
60
|
primaryColor,
|
|
61
61
|
);
|
|
62
|
-
setTimeout(() => window.
|
|
62
|
+
setTimeout(() => window.dispatchEvent(new Event('kyro:soft-reload')), 800);
|
|
63
63
|
} catch (e) {
|
|
64
64
|
toast.error("Failed to save branding");
|
|
65
65
|
console.error(e);
|
|
@@ -14,6 +14,8 @@ import { useAutoFormStore } from "../lib/autoform-store";
|
|
|
14
14
|
import { PageHeader } from "./ui/PageHeader";
|
|
15
15
|
import { Badge } from "./ui/Badge";
|
|
16
16
|
import { SplitButton } from "./ui/SplitButton";
|
|
17
|
+
import { Lock, FileText, ChevronLeft, Save } from "lucide-react";
|
|
18
|
+
import { navigate } from "astro:transitions/client";
|
|
17
19
|
import { useTranslation } from "react-i18next";
|
|
18
20
|
import "../lib/i18n";
|
|
19
21
|
import { adminPath } from "../lib/paths";
|
|
@@ -231,9 +233,9 @@ export function DetailView({
|
|
|
231
233
|
const response = await apiPost(`/api/${slug}/${documentId}/duplicate`, undefined, { autoToast: false }) as { data?: { id?: string } };
|
|
232
234
|
toast.success(t("toast.duplicated", { defaultValue: "Document duplicated" }));
|
|
233
235
|
if (response?.data?.id) {
|
|
234
|
-
|
|
236
|
+
navigate(`${adminPath}/${slug}/${response.data.id}`);
|
|
235
237
|
} else {
|
|
236
|
-
|
|
238
|
+
navigate(`${adminPath}/${slug}`);
|
|
237
239
|
}
|
|
238
240
|
} catch (e) {
|
|
239
241
|
toast.error((e as Error).message || t("toast.duplicateError", { defaultValue: "Failed to duplicate document" }));
|
|
@@ -290,7 +292,7 @@ export function DetailView({
|
|
|
290
292
|
try {
|
|
291
293
|
setIsDeleting(true);
|
|
292
294
|
await apiDelete(`/api/${slug}/${documentId}`);
|
|
293
|
-
|
|
295
|
+
navigate(`${adminPath}/${slug}`);
|
|
294
296
|
} catch (e) {
|
|
295
297
|
setIsDeleting(false);
|
|
296
298
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Search, Filter, Columns3, X, Trash2, Archive, ChevronUp, Edit2 } from "./ui/icons";
|
|
2
2
|
import { useState, useEffect, useMemo, useCallback, useRef } from "react";
|
|
3
3
|
import { Spinner } from "./ui/Spinner";
|
|
4
|
+
import { navigate } from "astro:transitions/client";
|
|
4
5
|
import { Shimmer } from "./ui/Shimmer";
|
|
5
6
|
import { Plus } from "./ui/icons";
|
|
6
7
|
import { apiGet, apiDelete, withCacheBust } from "../lib/api";
|
|
@@ -78,7 +79,7 @@ export function ListView({
|
|
|
78
79
|
import("astro:transitions/client").then(({ navigate }) => {
|
|
79
80
|
navigate(href);
|
|
80
81
|
}).catch(() => {
|
|
81
|
-
|
|
82
|
+
navigate(href);
|
|
82
83
|
});
|
|
83
84
|
}
|
|
84
85
|
};
|
|
@@ -92,7 +93,7 @@ export function ListView({
|
|
|
92
93
|
import("astro:transitions/client").then(({ navigate }) => {
|
|
93
94
|
navigate(href);
|
|
94
95
|
}).catch(() => {
|
|
95
|
-
|
|
96
|
+
navigate(href);
|
|
96
97
|
});
|
|
97
98
|
}
|
|
98
99
|
};
|
|
@@ -104,7 +105,16 @@ export function ListView({
|
|
|
104
105
|
const [limit, setLimit] = useState(10);
|
|
105
106
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
|
106
107
|
const [search, setSearch] = useState("");
|
|
108
|
+
const [debouncedSearch, setDebouncedSearch] = useState("");
|
|
107
109
|
const [filters, setFilters] = useState<FilterConfig[]>([]);
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
const handler = setTimeout(() => {
|
|
113
|
+
setDebouncedSearch(search);
|
|
114
|
+
setPage(1); // Reset to page 1 on new search
|
|
115
|
+
}, 300);
|
|
116
|
+
return () => clearTimeout(handler);
|
|
117
|
+
}, [search]);
|
|
108
118
|
const { confirm, alert } = useUIStore();
|
|
109
119
|
|
|
110
120
|
const addFilter = () => {
|
|
@@ -262,7 +272,7 @@ export function ListView({
|
|
|
262
272
|
depth: "1",
|
|
263
273
|
});
|
|
264
274
|
|
|
265
|
-
if (
|
|
275
|
+
if (debouncedSearch) params.append("search", debouncedSearch);
|
|
266
276
|
if (sort) params.append("sort", sort.field);
|
|
267
277
|
if (sort) params.append("order", sort.direction);
|
|
268
278
|
if (filters.length > 0) {
|
|
@@ -280,7 +290,7 @@ export function ListView({
|
|
|
280
290
|
} finally {
|
|
281
291
|
setLoading(false);
|
|
282
292
|
}
|
|
283
|
-
}, [collectionSlug, page, limit,
|
|
293
|
+
}, [collectionSlug, page, limit, debouncedSearch, sort, filters]);
|
|
284
294
|
|
|
285
295
|
// Initial fetch only if not provided with initialDocs or if empty
|
|
286
296
|
useEffect(() => {
|
|
@@ -297,7 +307,7 @@ export function ListView({
|
|
|
297
307
|
return;
|
|
298
308
|
}
|
|
299
309
|
fetchDocs();
|
|
300
|
-
}, [page, limit,
|
|
310
|
+
}, [page, limit, debouncedSearch, sort, filters]);
|
|
301
311
|
|
|
302
312
|
const handleSelectAll = () => {
|
|
303
313
|
if (selectedIds.size === docs.length) {
|
|
@@ -1256,7 +1256,7 @@ export function MediaGallery({
|
|
|
1256
1256
|
}).then(() => {
|
|
1257
1257
|
setShowStorageConfigModal(false);
|
|
1258
1258
|
setStorageConfigured(true);
|
|
1259
|
-
window.
|
|
1259
|
+
window.dispatchEvent(new Event('kyro:soft-reload'));
|
|
1260
1260
|
}).catch(() => {
|
|
1261
1261
|
toast.error("Failed to configure storage");
|
|
1262
1262
|
});
|
|
@@ -16,6 +16,7 @@ import { Modal, ModalContent, ModalActions } from "./ui/Modal";
|
|
|
16
16
|
import { PageHeader } from "./ui/PageHeader";
|
|
17
17
|
import { Badge } from "./ui/Badge";
|
|
18
18
|
import { useTranslation } from "react-i18next";
|
|
19
|
+
import { navigate } from "astro:transitions/client";
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
interface Plugin {
|
|
@@ -97,7 +98,7 @@ export function PluginsManager() {
|
|
|
97
98
|
actions={[
|
|
98
99
|
{
|
|
99
100
|
label: "Marketplace",
|
|
100
|
-
onClick: () => (
|
|
101
|
+
onClick: () => (navigate("/admin/marketplace")),
|
|
101
102
|
icon: Plus,
|
|
102
103
|
},
|
|
103
104
|
]}
|
|
@@ -4,6 +4,7 @@ import { User, Shield, Key, Webhook, Clock, FileText, ExternalLink, HelpCircle,
|
|
|
4
4
|
import { useAuthStore } from "../lib/stores";
|
|
5
5
|
import { apiGet } from "../lib/api";
|
|
6
6
|
import { useTranslation } from "react-i18next";
|
|
7
|
+
import { navigate } from 'astro:transitions/client';
|
|
7
8
|
|
|
8
9
|
interface UserMenuProps {
|
|
9
10
|
adminPath: string;
|
|
@@ -64,9 +65,9 @@ export function UserMenu({ adminPath }: UserMenuProps) {
|
|
|
64
65
|
onClick={() => {
|
|
65
66
|
const id = currentUser?.id;
|
|
66
67
|
if (id) {
|
|
67
|
-
|
|
68
|
+
navigate(`${adminPath}/users/${id}`);
|
|
68
69
|
} else {
|
|
69
|
-
|
|
70
|
+
navigate(`${adminPath}/users`);
|
|
70
71
|
}
|
|
71
72
|
}}
|
|
72
73
|
>
|
|
@@ -75,7 +76,7 @@ export function UserMenu({ adminPath }: UserMenuProps) {
|
|
|
75
76
|
|
|
76
77
|
<DropdownItem
|
|
77
78
|
icon={<Shield className="w-4 h-4" />}
|
|
78
|
-
onClick={() =>
|
|
79
|
+
onClick={() => navigate(`${adminPath}/roles`)}
|
|
79
80
|
>
|
|
80
81
|
{t("userMenu.permissions", { defaultValue: "Permissions" })}
|
|
81
82
|
</DropdownItem>
|
|
@@ -90,33 +91,33 @@ export function UserMenu({ adminPath }: UserMenuProps) {
|
|
|
90
91
|
|
|
91
92
|
<DropdownItem
|
|
92
93
|
icon={<Key className="w-4 h-4" />}
|
|
93
|
-
onClick={() => (
|
|
94
|
+
onClick={() => (navigate(`${adminPath}/keys`))}
|
|
94
95
|
>
|
|
95
96
|
{t("userMenu.apiKeys", { defaultValue: "API Keys" })}
|
|
96
97
|
</DropdownItem>
|
|
97
98
|
|
|
98
99
|
<DropdownItem
|
|
99
100
|
icon={<Webhook className="w-4 h-4" />}
|
|
100
|
-
onClick={() => (
|
|
101
|
+
onClick={() => (navigate(`${adminPath}/webhooks`))}
|
|
101
102
|
>
|
|
102
103
|
{t("userMenu.webHooks", { defaultValue: "Web Hooks" })}
|
|
103
104
|
</DropdownItem>
|
|
104
105
|
<DropdownItem
|
|
105
106
|
icon={<Clock className="w-4 h-4" />}
|
|
106
|
-
onClick={() => (
|
|
107
|
+
onClick={() => (navigate(`${adminPath}/sessions`))}
|
|
107
108
|
>
|
|
108
109
|
{t("userMenu.sessions", { defaultValue: "Sessions" })}
|
|
109
110
|
</DropdownItem>
|
|
110
111
|
<DropdownItem
|
|
111
112
|
icon={<FileText className="w-4 h-4" />}
|
|
112
|
-
onClick={() => (
|
|
113
|
+
onClick={() => (navigate(`${adminPath}/audit`))}
|
|
113
114
|
>
|
|
114
115
|
{t("userMenu.auditLogs", { defaultValue: "Audit Logs" })}
|
|
115
116
|
</DropdownItem>
|
|
116
117
|
|
|
117
118
|
<DropdownItem
|
|
118
119
|
icon={<Terminal className="w-4 h-4" />}
|
|
119
|
-
onClick={() => (
|
|
120
|
+
onClick={() => (navigate(`${adminPath}/rest-playground`))}
|
|
120
121
|
>
|
|
121
122
|
{t("userMenu.apiExplorer", { defaultValue: "API Explorer" })}
|
|
122
123
|
</DropdownItem>
|
|
@@ -124,7 +125,7 @@ export function UserMenu({ adminPath }: UserMenuProps) {
|
|
|
124
125
|
{(apiAccess === null || apiAccess?.graphqlEnabled) && (
|
|
125
126
|
<DropdownItem
|
|
126
127
|
icon={<Zap className="w-4 h-4" />}
|
|
127
|
-
onClick={() => (
|
|
128
|
+
onClick={() => (navigate(`${adminPath}/graphql`))}
|
|
128
129
|
>
|
|
129
130
|
{t("userMenu.graphqlPlayground", { defaultValue: "GraphQL Playground" })}
|
|
130
131
|
</DropdownItem>
|
|
@@ -179,7 +179,7 @@ export function AutoFormHeader({
|
|
|
179
179
|
<span className="opacity-30">|</span>
|
|
180
180
|
<button
|
|
181
181
|
type="button"
|
|
182
|
-
onClick={() => window.
|
|
182
|
+
onClick={() => window.dispatchEvent(new Event('kyro:soft-reload'))}
|
|
183
183
|
className="text-[var(--kyro-danger)] hover:underline"
|
|
184
184
|
>
|
|
185
185
|
Reload server version
|
|
@@ -43,7 +43,7 @@ export default function IconField({
|
|
|
43
43
|
type="text"
|
|
44
44
|
value={normalizedValue}
|
|
45
45
|
onChange={(e) => onChange?.(e.target.value)}
|
|
46
|
-
placeholder={field.admin?.placeholder || "e.g., activity"}
|
|
46
|
+
placeholder={(field.admin?.placeholder as string) || "e.g., activity"}
|
|
47
47
|
disabled={disabled || isReadOnly}
|
|
48
48
|
required={field.required}
|
|
49
49
|
className={`kyro-form-input ${disabled || isReadOnly ? "opacity-70 bg-[var(--kyro-bg-secondary)] cursor-not-allowed" : ""}`}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
|
2
2
|
|
|
3
|
+
export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost';
|
|
4
|
+
export type ButtonSize = 'sm' | 'md' | 'lg';
|
|
5
|
+
|
|
3
6
|
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
4
|
-
variant?:
|
|
5
|
-
size?:
|
|
7
|
+
variant?: ButtonVariant;
|
|
8
|
+
size?: ButtonSize;
|
|
6
9
|
loading?: boolean;
|
|
7
10
|
children: ReactNode;
|
|
8
11
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
2
|
import { apiGet } from "../../lib/api";
|
|
3
3
|
import { useUIStore, toast } from "../../lib/stores";
|
|
4
|
-
import { X } from "../ui/icons";
|
|
5
4
|
import { UploadField } from "../fields/UploadField";
|
|
6
5
|
import { useTranslation } from "react-i18next";
|
|
6
|
+
import { navigate } from 'astro:transitions/client';
|
|
7
7
|
|
|
8
8
|
interface User {
|
|
9
9
|
id: string;
|
|
@@ -43,8 +43,8 @@ export function UserDetail({ user, apiPath, adminPath }: UserDetailProps) {
|
|
|
43
43
|
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
|
|
44
44
|
const [avatarMedia, setAvatarMedia] = useState<any>(user.avatar ? { id: user.avatar } : null);
|
|
45
45
|
const [saving, setSaving] = useState(false);
|
|
46
|
-
const { confirm
|
|
47
|
-
const [
|
|
46
|
+
const { confirm } = useUIStore();
|
|
47
|
+
const [, setDeleting] = useState(false);
|
|
48
48
|
const [locking, setLocking] = useState(false);
|
|
49
49
|
const [isLocked, setIsLocked] = useState(user.locked || false);
|
|
50
50
|
|
|
@@ -90,7 +90,7 @@ export function UserDetail({ user, apiPath, adminPath }: UserDetailProps) {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
if (Object.keys(body).length === 0) {
|
|
93
|
-
|
|
93
|
+
navigate(adminPath + "/users");
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
96
96
|
|
|
@@ -105,7 +105,7 @@ export function UserDetail({ user, apiPath, adminPath }: UserDetailProps) {
|
|
|
105
105
|
|
|
106
106
|
if (res.ok) {
|
|
107
107
|
toast.success("User updated");
|
|
108
|
-
|
|
108
|
+
navigate(adminPath + "/users");
|
|
109
109
|
} else {
|
|
110
110
|
toast.error(data.error || "Failed to save user");
|
|
111
111
|
}
|
|
@@ -162,7 +162,7 @@ export function UserDetail({ user, apiPath, adminPath }: UserDetailProps) {
|
|
|
162
162
|
});
|
|
163
163
|
if (res.ok) {
|
|
164
164
|
toast.success("User deleted");
|
|
165
|
-
|
|
165
|
+
navigate(adminPath + "/users");
|
|
166
166
|
} else {
|
|
167
167
|
toast.error("Failed to delete user");
|
|
168
168
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
2
|
import { useTranslation } from "react-i18next";
|
|
3
|
+
import { navigate } from 'astro:transitions/client';
|
|
3
4
|
|
|
4
5
|
interface UserFormProps {
|
|
5
6
|
mode: "create" | "edit";
|
|
@@ -38,7 +39,7 @@ export function UserForm({ mode, apiPath, adminPath, user }: UserFormProps) {
|
|
|
38
39
|
type: "success" | "error";
|
|
39
40
|
} | null>(null);
|
|
40
41
|
|
|
41
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
42
|
+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
42
43
|
e.preventDefault();
|
|
43
44
|
setLoading(true);
|
|
44
45
|
setMessage(null);
|
|
@@ -71,7 +72,7 @@ export function UserForm({ mode, apiPath, adminPath, user }: UserFormProps) {
|
|
|
71
72
|
type: "success",
|
|
72
73
|
});
|
|
73
74
|
setTimeout(() => {
|
|
74
|
-
|
|
75
|
+
navigate(`${adminPath}/users`);
|
|
75
76
|
}, 1000);
|
|
76
77
|
} else {
|
|
77
78
|
setMessage({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Plus, Lock, CheckCircle2, Edit2, Trash2, XCircle, X } from "../ui/icons";
|
|
2
|
-
import
|
|
2
|
+
import { useState } from "react";
|
|
3
3
|
import { useUIStore } from "../../lib/stores";
|
|
4
|
+
import { navigate } from 'astro:transitions/client';
|
|
4
5
|
|
|
5
6
|
interface User {
|
|
6
7
|
id: string;
|
|
@@ -39,8 +40,8 @@ export function UsersList({
|
|
|
39
40
|
}: UsersListProps) {
|
|
40
41
|
const [users, setUsers] = useState<User[]>(initialUsers);
|
|
41
42
|
const [totalUsers, setTotalUsers] = useState(initialTotal);
|
|
42
|
-
const { confirm
|
|
43
|
-
const [
|
|
43
|
+
const { confirm } = useUIStore();
|
|
44
|
+
const [, setDeleting] = useState(false);
|
|
44
45
|
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
|
45
46
|
|
|
46
47
|
const handleDeleteClick = (user: User) => {
|
|
@@ -168,7 +169,7 @@ export function UsersList({
|
|
|
168
169
|
key={user.id}
|
|
169
170
|
className="group hover:bg-gray-50/50 transition-colors cursor-pointer"
|
|
170
171
|
onClick={() =>
|
|
171
|
-
(
|
|
172
|
+
navigate(`${adminPath}/users/${user.id}`)
|
|
172
173
|
}
|
|
173
174
|
>
|
|
174
175
|
<td className="px-8 py-5">
|
package/src/env.d.ts
CHANGED
|
@@ -2,6 +2,10 @@ declare const __KYRO_API_PATH__: string;
|
|
|
2
2
|
declare const __KYRO_ADMIN_PATH__: string;
|
|
3
3
|
declare const __KYRO_ADMIN_AUTH_DISABLED__: boolean;
|
|
4
4
|
|
|
5
|
+
declare module 'astro:transitions/client' {
|
|
6
|
+
export function navigate(href: string, options?: any): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
5
9
|
declare namespace App {
|
|
6
10
|
interface Locals {
|
|
7
11
|
user?: {
|
package/src/hooks/useQueue.ts
CHANGED
|
@@ -14,7 +14,6 @@ type QueueTask = (fn: QueuedFunction, options?: QueueTaskOptions) => void;
|
|
|
14
14
|
* Only the last task in the queue is ever processed; all prior pending tasks are discarded.
|
|
15
15
|
* This prevents race conditions where multiple autosave fetches could run in parallel.
|
|
16
16
|
*
|
|
17
|
-
* Inspired by Payload's useQueues hook.
|
|
18
17
|
*
|
|
19
18
|
* @returns {queueTask} A function used to queue a task for execution.
|
|
20
19
|
*/
|
|
@@ -40,7 +39,9 @@ export function useQueue(): { queueTask: QueueTask } {
|
|
|
40
39
|
isProcessing.current = true;
|
|
41
40
|
|
|
42
41
|
try {
|
|
43
|
-
|
|
42
|
+
if (latestTask) {
|
|
43
|
+
await latestTask();
|
|
44
|
+
}
|
|
44
45
|
} catch (err) {
|
|
45
46
|
console.error("Error in queued function:", err);
|
|
46
47
|
} finally {
|
package/src/kyro-cms.d.ts
CHANGED
|
@@ -62,6 +62,8 @@ admin?: {
|
|
|
62
62
|
export interface JSONField extends FieldConfig { type: 'json' }
|
|
63
63
|
export interface ImageField extends FieldConfig { type: 'image' }
|
|
64
64
|
export interface UploadField extends FieldConfig { type: 'upload' }
|
|
65
|
+
export interface IconField extends FieldConfig { type: 'icon' }
|
|
66
|
+
export interface SecretField extends FieldConfig { type: 'secret' }
|
|
65
67
|
export interface RelationshipField extends FieldConfig { type: 'relationship'; relationTo: string }
|
|
66
68
|
export interface BlocksField extends FieldConfig { type: 'blocks'; blocks: Block[] }
|
|
67
69
|
export interface ArrayField extends FieldConfig { type: 'array'; fields: FieldConfig[] }
|
|
@@ -181,6 +183,9 @@ admin?: {
|
|
|
181
183
|
newValue?: unknown;
|
|
182
184
|
[key: string]: unknown;
|
|
183
185
|
}
|
|
186
|
+
|
|
187
|
+
export const richTextStyles: string;
|
|
188
|
+
export function renderRichText(content: unknown, options?: unknown): string;
|
|
184
189
|
}
|
|
185
190
|
|
|
186
191
|
declare module '@kyro-cms/core/client' {
|
|
@@ -3,7 +3,7 @@ import { persist, createJSONStorage } from "zustand/middleware";
|
|
|
3
3
|
import type { StateStorage } from "zustand/middleware";
|
|
4
4
|
import { createStorage } from "unstorage";
|
|
5
5
|
import indexedbDriver from "unstorage/drivers/indexedb";
|
|
6
|
-
import type { Version } from "@kyro-cms/core/client";
|
|
6
|
+
import type { Version, VersionDiff } from "@kyro-cms/core/client";
|
|
7
7
|
import { deepEqual, isEmpty } from "./deep-equal";
|
|
8
8
|
import { normalizeUploadFields } from "./normalize-upload-fields";
|
|
9
9
|
|
|
@@ -247,8 +247,9 @@ export const useAutoFormStore = create<AutoFormStore>()(
|
|
|
247
247
|
if (current[keys[i]] === undefined) {
|
|
248
248
|
current[keys[i]] = {};
|
|
249
249
|
}
|
|
250
|
-
|
|
251
|
-
current
|
|
250
|
+
const nextVal = current[keys[i]] as Record<string, unknown>;
|
|
251
|
+
current[keys[i]] = { ...nextVal };
|
|
252
|
+
current = current[keys[i]] as Record<string, unknown>;
|
|
252
253
|
}
|
|
253
254
|
|
|
254
255
|
current[keys[keys.length - 1]] = value;
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
* Without normalization, the server stores the full object as a JSON string,
|
|
8
8
|
* which causes 404s on the next load when trying to fetch the media.
|
|
9
9
|
*/
|
|
10
|
-
export function normalizeUploadFields(value: unknown): unknown {
|
|
10
|
+
export function normalizeUploadFields(value: unknown, isRoot = false): unknown {
|
|
11
11
|
if (value === null || value === undefined || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
12
12
|
return value;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
if (Array.isArray(value)) {
|
|
16
|
-
return value.map(normalizeUploadFields);
|
|
16
|
+
return value.map(v => normalizeUploadFields(v, false));
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
if (typeof value === "object") {
|
|
@@ -25,14 +25,27 @@ export function normalizeUploadFields(value: unknown): unknown {
|
|
|
25
25
|
const hasId = "id" in obj && (typeof obj.id === "string" || obj.id === null);
|
|
26
26
|
const hasMediaField = "url" in obj && ("filename" in obj || "mimeType" in obj);
|
|
27
27
|
|
|
28
|
-
if (hasId && hasMediaField && keys.length <= 25) {
|
|
28
|
+
if (!isRoot && hasId && hasMediaField && keys.length <= 25) {
|
|
29
|
+
return obj.id;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Heuristic for polymorphic relationships: { relationTo: '...', value: { id: '...' } }
|
|
33
|
+
if (!isRoot && "relationTo" in obj && "value" in obj && typeof obj.value === "object" && obj.value !== null) {
|
|
34
|
+
const valObj = obj.value as Record<string, unknown>;
|
|
35
|
+
if ("id" in valObj && (typeof valObj.id === "string" || typeof valObj.id === "number")) {
|
|
36
|
+
return { relationTo: obj.relationTo, value: valObj.id };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Heuristic for regular populated relationships: must have id, createdAt, updatedAt
|
|
41
|
+
if (!isRoot && hasId && "createdAt" in obj && "updatedAt" in obj) {
|
|
29
42
|
return obj.id;
|
|
30
43
|
}
|
|
31
44
|
|
|
32
45
|
// Recursively normalize nested objects (tabs, groups, blocks, arrays)
|
|
33
46
|
const result: Record<string, unknown> = {};
|
|
34
47
|
for (const key of keys) {
|
|
35
|
-
result[key] = normalizeUploadFields(obj[key]);
|
|
48
|
+
result[key] = normalizeUploadFields(obj[key], false);
|
|
36
49
|
}
|
|
37
50
|
return result;
|
|
38
51
|
}
|