@kyro-cms/admin 0.12.17 → 0.12.18
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 +24 -24
- package/dist/index.js +19 -19
- package/package.json +6 -5
- package/src/components/AutoForm.tsx +4 -2
- package/src/components/DashboardMetrics.tsx +40 -25
- package/src/components/fields/ArrayLayout.tsx +131 -12
- package/src/components/fields/RelationshipField.tsx +6 -6
- package/src/components/fields/TabsLayout.tsx +5 -1
- package/src/hooks/useAutoFormState.ts +19 -6
- package/src/integration.ts +2 -1
- package/src/lib/autoform-store.ts +23 -9
- package/src/lib/config.ts +18 -6
- package/src/pages/index.astro +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kyro-cms/admin",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.18",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=22"
|
|
6
6
|
},
|
|
@@ -73,6 +73,8 @@
|
|
|
73
73
|
"@dnd-kit/sortable": "^10.0.0",
|
|
74
74
|
"@dnd-kit/utilities": "^3.2.2",
|
|
75
75
|
"@graphiql/react": "^0.37.3",
|
|
76
|
+
"@kyro-cms/astro": "0.12.18",
|
|
77
|
+
"@kyro-cms/core": "0.12.18",
|
|
76
78
|
"@tailwindcss/vite": "^4.0.0",
|
|
77
79
|
"@tiptap/extension-color": "^3.23.6",
|
|
78
80
|
"@tiptap/extension-highlight": "^3.23.6",
|
|
@@ -109,8 +111,7 @@
|
|
|
109
111
|
"tailwindcss": "^4.0.0",
|
|
110
112
|
"unstorage": "^1.17.5",
|
|
111
113
|
"use-sync-external-store": "1.5.0",
|
|
112
|
-
"zustand": "^5.0.3"
|
|
113
|
-
"@kyro-cms/core": "0.12.17"
|
|
114
|
+
"zustand": "^5.0.3"
|
|
114
115
|
},
|
|
115
116
|
"devDependencies": {
|
|
116
117
|
"@astrojs/check": "^0.9.9",
|
|
@@ -124,9 +125,9 @@
|
|
|
124
125
|
"vitest": "^4.1.4"
|
|
125
126
|
},
|
|
126
127
|
"peerDependencies": {
|
|
128
|
+
"@kyro-cms/core": "0.12.18",
|
|
127
129
|
"react": "^19.0.0",
|
|
128
|
-
"react-dom": "^19.0.0"
|
|
129
|
-
"@kyro-cms/core": "0.12.17"
|
|
130
|
+
"react-dom": "^19.0.0"
|
|
130
131
|
},
|
|
131
132
|
"repository": {
|
|
132
133
|
"type": "git",
|
|
@@ -864,11 +864,13 @@ export function AutoForm({
|
|
|
864
864
|
case "tabs":
|
|
865
865
|
return (
|
|
866
866
|
<TabsLayout
|
|
867
|
-
key={field.name || `tabs
|
|
867
|
+
key={field.name || `tabs-layout`}
|
|
868
868
|
field={field}
|
|
869
869
|
formData={formData}
|
|
870
870
|
onTabDataChange={(tabData) => {
|
|
871
|
-
|
|
871
|
+
if (field.name) {
|
|
872
|
+
setField(field.name, tabData);
|
|
873
|
+
}
|
|
872
874
|
}}
|
|
873
875
|
renderField={(f, parentData, onChange) => renderField(f, parentData, onChange)}
|
|
874
876
|
/>
|
|
@@ -431,32 +431,34 @@ export const DashboardMetrics: React.FC<DashboardMetricsProps> = ({ isEcommerce
|
|
|
431
431
|
|
|
432
432
|
const currency = analyticsData?.currencyCode || "USD";
|
|
433
433
|
|
|
434
|
+
const productCount =
|
|
435
|
+
(data?.collectionCounts?.["products"] || 0) +
|
|
436
|
+
(data?.collectionCounts?.["food-menu"] || 0) +
|
|
437
|
+
(data?.collectionCounts?.["menu"] || 0);
|
|
438
|
+
|
|
434
439
|
const ecommerceCards = [
|
|
435
440
|
{
|
|
436
441
|
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>,
|
|
437
442
|
gradient: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)",
|
|
438
|
-
value: analyticsData?.totalRevenue
|
|
443
|
+
value: analyticsData?.totalRevenue !== undefined
|
|
439
444
|
? new Intl.NumberFormat("en-US", { style: "currency", currency }).format(analyticsData.totalRevenue)
|
|
440
445
|
: "$0",
|
|
441
446
|
label: "Total Revenue",
|
|
442
|
-
subtext: "From
|
|
443
|
-
trend: { value: "+12.4%", up: true },
|
|
447
|
+
subtext: "From system orders",
|
|
444
448
|
},
|
|
445
449
|
{
|
|
446
450
|
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /></svg>,
|
|
447
451
|
gradient: "linear-gradient(135deg, #ec4899 0%, #f43f5e 100%)",
|
|
448
|
-
value:
|
|
449
|
-
label: "Products",
|
|
450
|
-
subtext: "Active inventory items",
|
|
451
|
-
trend: { value: "+3 new", up: true },
|
|
452
|
+
value: productCount,
|
|
453
|
+
label: "Products / Menu Items",
|
|
454
|
+
subtext: "Active inventory & menu items",
|
|
452
455
|
},
|
|
453
456
|
{
|
|
454
457
|
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" /></svg>,
|
|
455
458
|
gradient: "linear-gradient(135deg, #10b981 0%, #06b6d4 100%)",
|
|
456
|
-
value: data?.collectionCounts?.["orders"] || 0,
|
|
459
|
+
value: analyticsData?.totalOrders !== undefined ? analyticsData.totalOrders : (data?.collectionCounts?.["orders"] || 0),
|
|
457
460
|
label: "Orders",
|
|
458
461
|
subtext: "Total orders placed",
|
|
459
|
-
trend: { value: "+8 today", up: true },
|
|
460
462
|
},
|
|
461
463
|
{
|
|
462
464
|
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /></svg>,
|
|
@@ -464,7 +466,6 @@ export const DashboardMetrics: React.FC<DashboardMetricsProps> = ({ isEcommerce
|
|
|
464
466
|
value: data?.collectionCounts?.["customers"] || 0,
|
|
465
467
|
label: "Customers",
|
|
466
468
|
subtext: "Registered shoppers",
|
|
467
|
-
trend: { value: "+5 this week", up: true },
|
|
468
469
|
},
|
|
469
470
|
{
|
|
470
471
|
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" /></svg>,
|
|
@@ -475,7 +476,26 @@ export const DashboardMetrics: React.FC<DashboardMetricsProps> = ({ isEcommerce
|
|
|
475
476
|
},
|
|
476
477
|
];
|
|
477
478
|
|
|
478
|
-
|
|
479
|
+
// Dynamically extract top active custom collections if present
|
|
480
|
+
const activeCustomCollections = data?.collectionCounts
|
|
481
|
+
? Object.entries(data.collectionCounts)
|
|
482
|
+
.filter(([slug, count]) => count > 0 && !["users", "audit_logs", "media"].includes(slug))
|
|
483
|
+
.sort((a, b) => b[1] - a[1])
|
|
484
|
+
.slice(0, 3)
|
|
485
|
+
: [];
|
|
486
|
+
|
|
487
|
+
const dynamicCollectionCards = activeCustomCollections.map(([slug, count]) => {
|
|
488
|
+
const formattedLabel = slug.split("-").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
489
|
+
return {
|
|
490
|
+
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /></svg>,
|
|
491
|
+
gradient: "linear-gradient(135deg, #0ea5e9 0%, #6366f1 100%)",
|
|
492
|
+
value: count,
|
|
493
|
+
label: formattedLabel,
|
|
494
|
+
subtext: "Active entries",
|
|
495
|
+
};
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const baseCmsCards = [
|
|
479
499
|
{
|
|
480
500
|
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" 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" /></svg>,
|
|
481
501
|
gradient: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)",
|
|
@@ -490,6 +510,9 @@ export const DashboardMetrics: React.FC<DashboardMetricsProps> = ({ isEcommerce
|
|
|
490
510
|
label: "Media Files",
|
|
491
511
|
subtext: "Images, videos & docs",
|
|
492
512
|
},
|
|
513
|
+
];
|
|
514
|
+
|
|
515
|
+
const trailingCmsCards = [
|
|
493
516
|
{
|
|
494
517
|
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" 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" /></svg>,
|
|
495
518
|
gradient: "linear-gradient(135deg, #0ea5e9 0%, #06b6d4 100%)",
|
|
@@ -497,22 +520,14 @@ export const DashboardMetrics: React.FC<DashboardMetricsProps> = ({ isEcommerce
|
|
|
497
520
|
label: "Team Members",
|
|
498
521
|
subtext: "Active user accounts",
|
|
499
522
|
},
|
|
500
|
-
{
|
|
501
|
-
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>,
|
|
502
|
-
gradient: "linear-gradient(135deg, #10b981 0%, #06b6d4 100%)",
|
|
503
|
-
value: data?.totalWebhooks || 0,
|
|
504
|
-
label: "Webhooks",
|
|
505
|
-
subtext: "Active integrations",
|
|
506
|
-
},
|
|
507
|
-
{
|
|
508
|
-
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" 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" /></svg>,
|
|
509
|
-
gradient: "linear-gradient(135deg, #f59e0b 0%, #f97316 100%)",
|
|
510
|
-
value: data?.totalApiKeys || 0,
|
|
511
|
-
label: "API Keys",
|
|
512
|
-
subtext: "Developer access tokens",
|
|
513
|
-
},
|
|
514
523
|
];
|
|
515
524
|
|
|
525
|
+
const cmsCards = [
|
|
526
|
+
...baseCmsCards,
|
|
527
|
+
...dynamicCollectionCards,
|
|
528
|
+
...trailingCmsCards,
|
|
529
|
+
].slice(0, 5);
|
|
530
|
+
|
|
516
531
|
const cards = isEcommerce ? ecommerceCards : cmsCards;
|
|
517
532
|
|
|
518
533
|
return (
|
|
@@ -2,6 +2,7 @@ import React from "react";
|
|
|
2
2
|
import type { Field } from "@kyro-cms/core/client";
|
|
3
3
|
import RelationshipField from "./RelationshipField";
|
|
4
4
|
import { ChevronDown, ChevronUp, GripVertical } from "../ui/icons";
|
|
5
|
+
import { apiGet } from "../../lib/api";
|
|
5
6
|
import {
|
|
6
7
|
DndContext,
|
|
7
8
|
closestCenter,
|
|
@@ -20,6 +21,32 @@ import { useTranslation } from "react-i18next";
|
|
|
20
21
|
|
|
21
22
|
const SIMPLE_TYPES = new Set(["text", "number", "checkbox", "select", "radio", "color", "email", "password"]);
|
|
22
23
|
|
|
24
|
+
function extractLabelFromObj(obj: unknown): string | null {
|
|
25
|
+
if (!obj || typeof obj !== "object") return null;
|
|
26
|
+
const o = obj as Record<string, unknown>;
|
|
27
|
+
const tabs = o.tabs as Record<string, unknown> | undefined;
|
|
28
|
+
const label =
|
|
29
|
+
(o.title as string) ||
|
|
30
|
+
(tabs?.title as string) ||
|
|
31
|
+
(o.name as string) ||
|
|
32
|
+
(o.label as string) ||
|
|
33
|
+
(o.email as string) ||
|
|
34
|
+
(o.filename as string) ||
|
|
35
|
+
(o.slug as string);
|
|
36
|
+
if (label && typeof label === "string") return label;
|
|
37
|
+
if (o.value && typeof o.value === "object") return extractLabelFromObj(o.value);
|
|
38
|
+
if (o.doc && typeof o.doc === "object") return extractLabelFromObj(o.doc);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getFallbackLabel(field: Field, index: number): string {
|
|
43
|
+
const rawLabel = field.label || field.name || "Item";
|
|
44
|
+
if (/item$/i.test(rawLabel.trim())) {
|
|
45
|
+
return `${rawLabel} ${index + 1}`;
|
|
46
|
+
}
|
|
47
|
+
return `${rawLabel} Item ${index + 1}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
23
50
|
interface ArrayLayoutProps {
|
|
24
51
|
field: Field;
|
|
25
52
|
value: unknown[];
|
|
@@ -148,7 +175,7 @@ function SortableArrayItem({
|
|
|
148
175
|
|
|
149
176
|
<div className="flex-1 min-w-0">
|
|
150
177
|
<span className="text-xs font-medium text-[var(--kyro-text-primary)] truncate block">
|
|
151
|
-
{getItemLabel(item) ||
|
|
178
|
+
{getItemLabel(item) || getFallbackLabel(field, index)}
|
|
152
179
|
</span>
|
|
153
180
|
</div>
|
|
154
181
|
|
|
@@ -197,11 +224,13 @@ export function ArrayLayout({
|
|
|
197
224
|
disabled,
|
|
198
225
|
}: ArrayLayoutProps) {
|
|
199
226
|
const items = (Array.isArray(value) ? value : []) as Record<string, unknown>[];
|
|
200
|
-
const fields = (field as Field & { fields?: { name?: string; type?: string; relationTo?: string; label?: string }[] }).fields || [];
|
|
227
|
+
const fields = (field as Field & { fields?: { name?: string; type?: string; relationTo?: string | string[]; label?: string }[] }).fields || [];
|
|
201
228
|
const firstField = fields[0];
|
|
202
229
|
const labelField = firstField?.name || "user";
|
|
203
230
|
const isRelationship = firstField?.type === "relationship";
|
|
204
231
|
const [openIndex, setOpenIndex] = React.useState<number | null>(0);
|
|
232
|
+
const [relLabels, setRelLabels] = React.useState<Record<string, string>>({});
|
|
233
|
+
const fetchedRelIds = React.useRef<Set<string>>(new Set());
|
|
205
234
|
|
|
206
235
|
// Sync stable IDs and heal bad data
|
|
207
236
|
React.useEffect(() => {
|
|
@@ -229,27 +258,117 @@ export function ArrayLayout({
|
|
|
229
258
|
}
|
|
230
259
|
}, [value, onChange]);
|
|
231
260
|
|
|
261
|
+
React.useEffect(() => {
|
|
262
|
+
const fieldsTyped = (field as Field & { fields?: Field[] }).fields || [];
|
|
263
|
+
items.forEach((item) => {
|
|
264
|
+
if (!item || typeof item !== "object") return;
|
|
265
|
+
fieldsTyped.forEach((f) => {
|
|
266
|
+
if (f.type === "relationship" && f.name) {
|
|
267
|
+
const val = item[f.name];
|
|
268
|
+
if (!val) return;
|
|
269
|
+
|
|
270
|
+
let id = "";
|
|
271
|
+
let rel = Array.isArray(f.relationTo) ? f.relationTo[0] : f.relationTo;
|
|
272
|
+
|
|
273
|
+
if (typeof val === "string") {
|
|
274
|
+
id = val;
|
|
275
|
+
} else if (typeof val === "object" && val !== null) {
|
|
276
|
+
const vObj = val as Record<string, unknown>;
|
|
277
|
+
if (vObj.relationTo && typeof vObj.relationTo === "string") {
|
|
278
|
+
rel = vObj.relationTo;
|
|
279
|
+
}
|
|
280
|
+
if (vObj.id && typeof vObj.id === "string") {
|
|
281
|
+
id = vObj.id;
|
|
282
|
+
} else if (vObj.value && typeof vObj.value === "string") {
|
|
283
|
+
id = vObj.value;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (id && rel && !fetchedRelIds.current.has(id)) {
|
|
288
|
+
fetchedRelIds.current.add(id);
|
|
289
|
+
apiGet<Record<string, unknown>>(`/api/${rel}/${id}`)
|
|
290
|
+
.then((res) => {
|
|
291
|
+
const doc = (res as any)?.data || res;
|
|
292
|
+
const label = extractLabelFromObj(doc);
|
|
293
|
+
if (label) {
|
|
294
|
+
setRelLabels((prev) => ({ ...prev, [id]: label }));
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
.catch(() => {});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
}, [items, field]);
|
|
303
|
+
|
|
232
304
|
function getItemLabel(item: Record<string, unknown>): string {
|
|
233
|
-
for (const key of ["label", "title", "name"]) {
|
|
305
|
+
for (const key of ["label", "title", "name", "externalUrl", "url"]) {
|
|
234
306
|
const val = item[key];
|
|
235
|
-
if (val && typeof val === "string") return val;
|
|
307
|
+
if (val && typeof val === "string" && val.trim() !== "") return val;
|
|
236
308
|
}
|
|
237
|
-
|
|
309
|
+
|
|
310
|
+
const fieldsTyped = (field as Field & { fields?: Field[] }).fields || [];
|
|
311
|
+
const DISCRIMINATOR_NAMES = new Set(["linkType", "type", "blockType", "kind", "mode"]);
|
|
312
|
+
|
|
238
313
|
for (const f of fieldsTyped) {
|
|
314
|
+
if (!f.name) continue;
|
|
315
|
+
const val = item[f.name];
|
|
316
|
+
if (!val) continue;
|
|
317
|
+
|
|
239
318
|
if (f.type === "text" || f.type === "textarea") {
|
|
240
|
-
|
|
241
|
-
if (val && typeof val === "string") return val;
|
|
319
|
+
if (typeof val === "string" && val.trim() !== "") return val;
|
|
242
320
|
}
|
|
321
|
+
|
|
322
|
+
if (f.type === "select" && !DISCRIMINATOR_NAMES.has(f.name)) {
|
|
323
|
+
if (typeof val === "string" && val.trim() !== "") return val;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (f.type === "relationship") {
|
|
327
|
+
const directLabel = extractLabelFromObj(val);
|
|
328
|
+
if (directLabel) return directLabel;
|
|
329
|
+
|
|
330
|
+
let id = "";
|
|
331
|
+
if (typeof val === "string") {
|
|
332
|
+
id = val;
|
|
333
|
+
} else if (typeof val === "object" && val !== null) {
|
|
334
|
+
const vObj = val as Record<string, unknown>;
|
|
335
|
+
if (vObj.id && typeof vObj.id === "string") id = vObj.id;
|
|
336
|
+
else if (vObj.value && typeof vObj.value === "string") id = vObj.value;
|
|
337
|
+
}
|
|
338
|
+
if (id && relLabels[id]) {
|
|
339
|
+
return relLabels[id];
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
243
343
|
if (f.type === "group" && (f as Field & { fields?: Field[] }).fields) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
344
|
+
const groupObj = val as Record<string, unknown> | undefined;
|
|
345
|
+
if (groupObj && typeof groupObj === "object") {
|
|
346
|
+
for (const inner of (f as Field & { fields?: Field[] }).fields || []) {
|
|
347
|
+
if (!inner.name) continue;
|
|
348
|
+
const innerVal = groupObj[inner.name];
|
|
349
|
+
if (inner.type === "text" || inner.type === "textarea") {
|
|
350
|
+
if (typeof innerVal === "string" && innerVal.trim() !== "") return innerVal;
|
|
351
|
+
}
|
|
352
|
+
if (inner.type === "select" && !DISCRIMINATOR_NAMES.has(inner.name)) {
|
|
353
|
+
if (typeof innerVal === "string" && innerVal.trim() !== "") return innerVal;
|
|
354
|
+
}
|
|
355
|
+
if (inner.type === "relationship") {
|
|
356
|
+
const directLabel = extractLabelFromObj(innerVal);
|
|
357
|
+
if (directLabel) return directLabel;
|
|
358
|
+
let id = "";
|
|
359
|
+
if (typeof innerVal === "string") id = innerVal;
|
|
360
|
+
else if (typeof innerVal === "object" && innerVal !== null) {
|
|
361
|
+
const vObj = innerVal as Record<string, unknown>;
|
|
362
|
+
if (vObj.id && typeof vObj.id === "string") id = vObj.id;
|
|
363
|
+
else if (vObj.value && typeof vObj.value === "string") id = vObj.value;
|
|
364
|
+
}
|
|
365
|
+
if (id && relLabels[id]) return relLabels[id];
|
|
366
|
+
}
|
|
249
367
|
}
|
|
250
368
|
}
|
|
251
369
|
}
|
|
252
370
|
}
|
|
371
|
+
|
|
253
372
|
return "";
|
|
254
373
|
}
|
|
255
374
|
|
|
@@ -326,14 +326,14 @@ export function RelationshipField({
|
|
|
326
326
|
items={tagIds}
|
|
327
327
|
strategy={horizontalListSortingStrategy}
|
|
328
328
|
>
|
|
329
|
-
{items.map((item) => {
|
|
329
|
+
{items.map((item, idx) => {
|
|
330
330
|
const rawId = getValueId(item);
|
|
331
331
|
const doc = selectedDocs.find((d) => d.id === rawId);
|
|
332
|
-
const label = doc ? getLabel(doc) : rawId.slice(0, 12);
|
|
332
|
+
const label = doc ? getLabel(doc) : (rawId ? rawId.slice(0, 12) : "Item");
|
|
333
333
|
const rel = isPolymorphic && doc ? doc.relationTo : null;
|
|
334
334
|
return (
|
|
335
335
|
<SortableTag
|
|
336
|
-
key={rawId}
|
|
336
|
+
key={`${rawId}-${idx}`}
|
|
337
337
|
id={rawId}
|
|
338
338
|
label={label}
|
|
339
339
|
relation={rel}
|
|
@@ -353,14 +353,14 @@ export function RelationshipField({
|
|
|
353
353
|
|
|
354
354
|
return (
|
|
355
355
|
<div className="flex flex-wrap gap-1.5 mt-2">
|
|
356
|
-
{items.map((item) => {
|
|
356
|
+
{items.map((item, idx) => {
|
|
357
357
|
const rawId = getValueId(item);
|
|
358
358
|
const doc = selectedDocs.find((d) => d.id === rawId);
|
|
359
|
-
const label = doc ? getLabel(doc) : rawId.slice(0, 12);
|
|
359
|
+
const label = doc ? getLabel(doc) : (rawId ? rawId.slice(0, 12) : "Item");
|
|
360
360
|
const rel = isPolymorphic && doc ? doc.relationTo : null;
|
|
361
361
|
return (
|
|
362
362
|
<span
|
|
363
|
-
key={rawId}
|
|
363
|
+
key={`${rawId}-${idx}`}
|
|
364
364
|
className="inline-flex items-center gap-1 px-2 py-1 text-xs rounded-md bg-[var(--kyro-sidebar-active)]/10 text-[var(--kyro-sidebar-active)]"
|
|
365
365
|
>
|
|
366
366
|
{rel && <span className="opacity-60 mr-0.5">{rel}:</span>}
|
|
@@ -53,7 +53,11 @@ export function TabsLayout({
|
|
|
53
53
|
</div>
|
|
54
54
|
<div className="space-y-6">
|
|
55
55
|
{currentTab?.fields.map((f: Field) =>
|
|
56
|
-
renderField(
|
|
56
|
+
renderField(
|
|
57
|
+
f,
|
|
58
|
+
field.name ? tabData : formData,
|
|
59
|
+
field.name ? onTabDataChange : undefined as any,
|
|
60
|
+
),
|
|
57
61
|
)}
|
|
58
62
|
</div>
|
|
59
63
|
|
|
@@ -447,9 +447,17 @@ const persistBrowserDraft = useCallback(
|
|
|
447
447
|
const titleStr = titleValue ? String(titleValue) : "";
|
|
448
448
|
|
|
449
449
|
if (titleStr && (!formData.metaTitle || formData.metaTitle === formData._lastMetaTitle)) {
|
|
450
|
-
|
|
450
|
+
if (formData.metaTitle !== titleStr) {
|
|
451
|
+
useAutoFormStore.setState((state) => ({
|
|
452
|
+
formData: {
|
|
453
|
+
...state.formData,
|
|
454
|
+
metaTitle: titleStr,
|
|
455
|
+
_lastMetaTitle: titleStr,
|
|
456
|
+
},
|
|
457
|
+
}));
|
|
458
|
+
}
|
|
451
459
|
}
|
|
452
|
-
}, [formData, config?.fields
|
|
460
|
+
}, [formData.title, config?.fields]);
|
|
453
461
|
|
|
454
462
|
interface FieldConfig {
|
|
455
463
|
name?: string;
|
|
@@ -471,13 +479,18 @@ const persistBrowserDraft = useCallback(
|
|
|
471
479
|
|
|
472
480
|
const sourceValue = resolveFieldValue(fields, formData, sourceField);
|
|
473
481
|
|
|
474
|
-
if (isSlugLocked && typeof sourceValue === "string") {
|
|
482
|
+
if (isSlugLocked && typeof sourceValue === "string" && sourceValue) {
|
|
475
483
|
const newSlug = slugifyText(sourceValue);
|
|
476
|
-
if (newSlug !== formData.slug) {
|
|
477
|
-
|
|
484
|
+
if (newSlug && newSlug !== formData.slug) {
|
|
485
|
+
useAutoFormStore.setState((state) => ({
|
|
486
|
+
formData: {
|
|
487
|
+
...state.formData,
|
|
488
|
+
slug: newSlug,
|
|
489
|
+
},
|
|
490
|
+
}));
|
|
478
491
|
}
|
|
479
492
|
}
|
|
480
|
-
}, [formData, isSlugLocked, config?.fields
|
|
493
|
+
}, [formData.title, isSlugLocked, config?.fields]);
|
|
481
494
|
|
|
482
495
|
// Auto-save effect — only starts timers on keystroke-originated changes.
|
|
483
496
|
// Local save fires after 1.5s of inactivity, server save after 8s.
|
package/src/integration.ts
CHANGED
|
@@ -150,7 +150,7 @@ export function kyroAdmin(options: KyroAdminOptions = {}): AstroIntegration {
|
|
|
150
150
|
];
|
|
151
151
|
for (const col of (configResult.collections || [])) {
|
|
152
152
|
if (col.seo) {
|
|
153
|
-
const tabsField = col.fields?.find((f: any) => f.type === 'tabs'
|
|
153
|
+
const tabsField = col.fields?.find((f: any) => f.type === 'tabs');
|
|
154
154
|
if (tabsField?.tabs) {
|
|
155
155
|
// Only add if not already injected (avoid duplicates on hot-reload)
|
|
156
156
|
if (!tabsField.tabs.find((t: any) => t.label === 'SEO Settings')) {
|
|
@@ -258,6 +258,7 @@ export default debug;
|
|
|
258
258
|
alias: {
|
|
259
259
|
"kyro:config": resolvedConfig,
|
|
260
260
|
},
|
|
261
|
+
dedupe: ["react", "react-dom"],
|
|
261
262
|
},
|
|
262
263
|
optimizeDeps: {
|
|
263
264
|
include: [
|
|
@@ -211,6 +211,7 @@ export const useAutoFormStore = create<AutoFormStore>()(
|
|
|
211
211
|
|
|
212
212
|
// Field update actions
|
|
213
213
|
setField: (field: string, value: unknown) => {
|
|
214
|
+
if (!field || field === "undefined") return;
|
|
214
215
|
const state = get();
|
|
215
216
|
const newDirty = new Set(state.dirtyFields);
|
|
216
217
|
// Mark dirty if value differs from last saved baseline (normalized to strip full media details)
|
|
@@ -223,18 +224,22 @@ export const useAutoFormStore = create<AutoFormStore>()(
|
|
|
223
224
|
} else {
|
|
224
225
|
newDirty.delete(field);
|
|
225
226
|
}
|
|
227
|
+
const nextFormData = { ...state.formData };
|
|
228
|
+
delete nextFormData.undefined;
|
|
229
|
+
delete nextFormData["undefined"];
|
|
230
|
+
nextFormData[field] = value;
|
|
226
231
|
set({
|
|
227
|
-
formData:
|
|
228
|
-
...state.formData,
|
|
229
|
-
[field]: value,
|
|
230
|
-
},
|
|
232
|
+
formData: nextFormData,
|
|
231
233
|
dirtyFields: newDirty,
|
|
232
234
|
hasUnsavedChanges: newDirty.size > 0,
|
|
233
235
|
});
|
|
234
236
|
},
|
|
235
237
|
|
|
236
238
|
setFormData: (data: Record<string, unknown>) => {
|
|
237
|
-
|
|
239
|
+
const cleanData = { ...data };
|
|
240
|
+
delete cleanData.undefined;
|
|
241
|
+
delete cleanData["undefined"];
|
|
242
|
+
set({ formData: cleanData });
|
|
238
243
|
},
|
|
239
244
|
|
|
240
245
|
setNestedField: (path: string, value: unknown) => {
|
|
@@ -253,6 +258,8 @@ export const useAutoFormStore = create<AutoFormStore>()(
|
|
|
253
258
|
}
|
|
254
259
|
|
|
255
260
|
current[keys[keys.length - 1]] = value;
|
|
261
|
+
delete newFormData.undefined;
|
|
262
|
+
delete newFormData["undefined"];
|
|
256
263
|
|
|
257
264
|
return { formData: newFormData };
|
|
258
265
|
});
|
|
@@ -363,17 +370,24 @@ export const useAutoFormStore = create<AutoFormStore>()(
|
|
|
363
370
|
data: Record<string, unknown>,
|
|
364
371
|
lastSaved?: Record<string, unknown>,
|
|
365
372
|
) => {
|
|
373
|
+
const cleanData = { ...data };
|
|
374
|
+
delete cleanData.undefined;
|
|
375
|
+
delete cleanData["undefined"];
|
|
376
|
+
const cleanLastSaved = lastSaved ? { ...lastSaved } : cleanData;
|
|
377
|
+
delete cleanLastSaved.undefined;
|
|
378
|
+
delete cleanLastSaved["undefined"];
|
|
379
|
+
|
|
366
380
|
const state = get();
|
|
367
381
|
if (
|
|
368
|
-
state.formData ===
|
|
369
|
-
state.lastSavedData ===
|
|
382
|
+
state.formData === cleanData &&
|
|
383
|
+
state.lastSavedData === cleanLastSaved &&
|
|
370
384
|
!state.hasUnsavedChanges
|
|
371
385
|
) {
|
|
372
386
|
return;
|
|
373
387
|
}
|
|
374
388
|
set({
|
|
375
|
-
formData:
|
|
376
|
-
lastSavedData:
|
|
389
|
+
formData: cleanData,
|
|
390
|
+
lastSavedData: cleanLastSaved,
|
|
377
391
|
hasUnsavedChanges: false,
|
|
378
392
|
dirtyFields: new Set<string>(),
|
|
379
393
|
});
|
package/src/lib/config.ts
CHANGED
|
@@ -98,7 +98,7 @@ function updateFieldByPath(
|
|
|
98
98
|
if (field.fields && Array.isArray(field.fields)) {
|
|
99
99
|
return updateFieldByPath(field.fields, remainingPath, updates);
|
|
100
100
|
}
|
|
101
|
-
|
|
101
|
+
|
|
102
102
|
// For tabs fields, traverse into all tabs
|
|
103
103
|
if (field.type === "tabs" && field.tabs && Array.isArray(field.tabs)) {
|
|
104
104
|
for (const tab of field.tabs) {
|
|
@@ -116,7 +116,7 @@ function updateFieldByPath(
|
|
|
116
116
|
const blockSlug = remainingPath.split(".")[0];
|
|
117
117
|
const restOfPath = remainingPath.split(".").slice(1).join(".");
|
|
118
118
|
if (!restOfPath) return false;
|
|
119
|
-
|
|
119
|
+
|
|
120
120
|
for (const block of field.blocks) {
|
|
121
121
|
if (block.slug === blockSlug && block.fields && Array.isArray(block.fields)) {
|
|
122
122
|
return updateFieldByPath(block.fields, restOfPath, updates);
|
|
@@ -124,7 +124,7 @@ function updateFieldByPath(
|
|
|
124
124
|
}
|
|
125
125
|
return false;
|
|
126
126
|
}
|
|
127
|
-
|
|
127
|
+
|
|
128
128
|
// For array fields, look in the nested fields
|
|
129
129
|
if (field.type === "array" && field.fields && Array.isArray(field.fields)) {
|
|
130
130
|
return updateFieldByPath(field.fields, remainingPath, updates);
|
|
@@ -136,9 +136,10 @@ function updateFieldByPath(
|
|
|
136
136
|
return true;
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Pass through structural flat wrappers (unnamed tabs, rows, collapsibles)
|
|
142
|
+
for (const field of fields) {
|
|
142
143
|
const isFlatStructuralField =
|
|
143
144
|
!field.name ||
|
|
144
145
|
field.type === "tabs" ||
|
|
@@ -158,6 +159,17 @@ function updateFieldByPath(
|
|
|
158
159
|
}
|
|
159
160
|
}
|
|
160
161
|
}
|
|
162
|
+
|
|
163
|
+
// Target field does not exist in this container.
|
|
164
|
+
// If there is no remaining path, this is the terminal target container — append the new field here!
|
|
165
|
+
if (!remainingPath) {
|
|
166
|
+
fields.push({
|
|
167
|
+
name: currentPart,
|
|
168
|
+
...updates,
|
|
169
|
+
});
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
161
173
|
return false;
|
|
162
174
|
}
|
|
163
175
|
|
package/src/pages/index.astro
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import AdminLayout from "../layouts/AdminLayout.astro";
|
|
3
|
-
import i18next from
|
|
3
|
+
import i18next from "../lib/i18n";
|
|
4
4
|
import { collections } from "../lib/config";
|
|
5
5
|
const authCollections = ["users", "audit_logs"];
|
|
6
6
|
import { adminPath, apiPath } from "../lib/paths";
|
|
@@ -13,7 +13,7 @@ const authItems = authCollections.map((slug) => ({
|
|
|
13
13
|
}));
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
<AdminLayout title={i18next.t("pages." + "Dashboard".replace(/ /g, ""), { defaultValue: "Dashboard" })}>
|
|
16
|
+
<AdminLayout title={i18next.t("pages." + "Dashboard".replace(/ /g, ""), { defaultValue: "Dashboard" })}>
|
|
17
17
|
<div class="flex-1 overflow-y-auto space-y-8">
|
|
18
18
|
<!-- Header -->
|
|
19
19
|
<div class="surface-tile p-6 flex items-center justify-between gap-8">
|
|
@@ -31,7 +31,7 @@ const authItems = authCollections.map((slug) => ({
|
|
|
31
31
|
type="text"
|
|
32
32
|
id="header-search-input"
|
|
33
33
|
placeholder="Search anything..."
|
|
34
|
-
class="w-full bg-[var(--kyro-surface-accent)] border border-transparent rounded-2xl py-3 pl-16 pr-8 text-
|
|
34
|
+
class="w-full bg-[var(--kyro-surface-accent)] border border-transparent rounded-2xl py-3 pl-16 pr-8 text-md font-medium focus:outline-none focus:bg-[var(--kyro-surface)] focus:border-[var(--kyro-border)] transition-all shadow-inner text-[var(--kyro-text-primary)] placeholder-[var(--kyro-text-muted)]"
|
|
35
35
|
autocomplete="off"
|
|
36
36
|
/>
|
|
37
37
|
<div
|
|
@@ -51,8 +51,8 @@ const authItems = authCollections.map((slug) => ({
|
|
|
51
51
|
</div>
|
|
52
52
|
|
|
53
53
|
<!-- CMS Metrics -->
|
|
54
|
-
<DashboardMetrics client:load isEcommerce={!!collections[
|
|
55
|
-
{(!!collections[
|
|
54
|
+
<DashboardMetrics client:load isEcommerce={!!collections["orders"] || !!collections["products"]} />
|
|
55
|
+
{(!!collections["orders"] || !!collections["products"]) && <RevenueChart client:load />}
|
|
56
56
|
|
|
57
57
|
<!-- Quick Links Section -->
|
|
58
58
|
<div class="surface-tile overflow-hidden mt-8">
|