@kyro-cms/admin 0.11.2 → 0.11.4
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 -21
- package/dist/index.d.cts +567 -0
- package/dist/index.d.ts +567 -0
- package/dist/index.js +24 -21
- package/package.json +1 -1
- package/src/components/AutoForm.tsx +4 -0
- package/src/components/FieldRenderer.tsx +16 -2
- package/src/components/ListView.tsx +2 -2
- package/src/components/fields/ArrayLayout.tsx +2 -2
- package/src/components/fields/SelectField.tsx +65 -1
- package/src/components/fields/UploadField.tsx +40 -69
- package/src/integration.ts +7 -4
- package/src/lib/normalize-upload-fields.ts +1 -1
package/package.json
CHANGED
|
@@ -25,6 +25,10 @@ interface FieldRendererProps {
|
|
|
25
25
|
onChange: (value: any) => void;
|
|
26
26
|
error?: string;
|
|
27
27
|
disabled?: boolean;
|
|
28
|
+
formData?: Record<string, unknown>;
|
|
29
|
+
siblingData?: Record<string, unknown>;
|
|
30
|
+
collectionSlug?: string;
|
|
31
|
+
globalSlug?: string;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
@@ -33,6 +37,10 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
33
37
|
onChange,
|
|
34
38
|
error,
|
|
35
39
|
disabled,
|
|
40
|
+
formData,
|
|
41
|
+
siblingData,
|
|
42
|
+
collectionSlug,
|
|
43
|
+
globalSlug,
|
|
36
44
|
}) => {
|
|
37
45
|
if (field.hidden === true || field.admin?.hidden === true) return null;
|
|
38
46
|
|
|
@@ -109,9 +117,13 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
109
117
|
<SelectField
|
|
110
118
|
field={field as any}
|
|
111
119
|
value={value}
|
|
112
|
-
onChange={
|
|
113
|
-
disabled={disabled}
|
|
120
|
+
onChange={onChangeKeystroke}
|
|
114
121
|
error={error}
|
|
122
|
+
disabled={disabled}
|
|
123
|
+
formData={formData}
|
|
124
|
+
siblingData={siblingData}
|
|
125
|
+
collectionSlug={collectionSlug}
|
|
126
|
+
globalSlug={globalSlug}
|
|
115
127
|
/>
|
|
116
128
|
);
|
|
117
129
|
case "date":
|
|
@@ -244,6 +256,8 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
244
256
|
}}
|
|
245
257
|
disabled={disabled}
|
|
246
258
|
error={error}
|
|
259
|
+
collectionSlug={collectionSlug}
|
|
260
|
+
globalSlug={globalSlug}
|
|
247
261
|
/>
|
|
248
262
|
);
|
|
249
263
|
}}
|
|
@@ -638,8 +638,8 @@ export function ListView({
|
|
|
638
638
|
{displayFields.map((field) => {
|
|
639
639
|
const rawValue = extractFieldValue(doc, field);
|
|
640
640
|
const cellValue =
|
|
641
|
-
field.type === "select" && rawValue
|
|
642
|
-
? field.options
|
|
641
|
+
field.type === "select" && rawValue && Array.isArray(field.options)
|
|
642
|
+
? field.options.find((o: any) => o.value === rawValue)
|
|
643
643
|
?.label || rawValue
|
|
644
644
|
: formatCellValue(rawValue, field.type);
|
|
645
645
|
return (
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
} from "@dnd-kit/sortable";
|
|
18
18
|
import { CSS } from "@dnd-kit/utilities";
|
|
19
19
|
|
|
20
|
-
const SIMPLE_TYPES = new Set(["text", "
|
|
20
|
+
const SIMPLE_TYPES = new Set(["text", "number", "checkbox", "select", "radio", "color", "email", "password"]);
|
|
21
21
|
|
|
22
22
|
interface ArrayLayoutProps {
|
|
23
23
|
field: Field;
|
|
@@ -102,7 +102,7 @@ function SortableArrayItem({
|
|
|
102
102
|
<span className="text-[10px] font-bold text-[var(--kyro-text-muted)] pt-2.5 min-w-[18px] text-center">
|
|
103
103
|
{index + 1}
|
|
104
104
|
</span>
|
|
105
|
-
<div className="flex-1 flex items-start gap-1.5
|
|
105
|
+
<div className={`flex-1 min-w-0 ${((field as any).fields || []).length >= 3 ? "flex flex-col gap-1.5" : "flex items-start gap-1.5"}`}>
|
|
106
106
|
{((field as any).fields || []).map((f: Field) => (
|
|
107
107
|
<div key={f.name} className="flex-1 min-w-0">
|
|
108
108
|
{renderField(f, item, onChangeItem)}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
1
2
|
import type { SelectField as SelectFieldType } from "@kyro-cms/core/client";
|
|
2
3
|
import FieldLayout from "./FieldLayout";
|
|
3
4
|
import { collections } from "../../lib/config";
|
|
5
|
+
import { fetchWithAuth } from "../../lib/api";
|
|
6
|
+
import { apiPath } from "../../lib/paths";
|
|
4
7
|
|
|
5
8
|
interface SelectFieldComponentProps {
|
|
6
9
|
field: SelectFieldType;
|
|
@@ -8,6 +11,10 @@ interface SelectFieldComponentProps {
|
|
|
8
11
|
onChange?: (value: string | string[] | undefined) => void;
|
|
9
12
|
error?: string;
|
|
10
13
|
disabled?: boolean;
|
|
14
|
+
formData?: Record<string, unknown>;
|
|
15
|
+
siblingData?: Record<string, unknown>;
|
|
16
|
+
collectionSlug?: string;
|
|
17
|
+
globalSlug?: string;
|
|
11
18
|
}
|
|
12
19
|
|
|
13
20
|
export default function SelectField({
|
|
@@ -16,14 +23,71 @@ export default function SelectField({
|
|
|
16
23
|
onChange,
|
|
17
24
|
error,
|
|
18
25
|
disabled,
|
|
26
|
+
formData,
|
|
27
|
+
siblingData,
|
|
28
|
+
collectionSlug,
|
|
29
|
+
globalSlug,
|
|
19
30
|
}: SelectFieldComponentProps) {
|
|
20
31
|
const isReadOnly =
|
|
21
32
|
typeof field.admin?.readOnly === "function"
|
|
22
33
|
? false
|
|
23
34
|
: Boolean(field.admin?.readOnly);
|
|
24
35
|
|
|
36
|
+
const [dynamicOptions, setDynamicOptions] = useState<Array<{ label: string; value: string }> | null>(null);
|
|
37
|
+
|
|
38
|
+
// Debounce stringify for formData to prevent infinite loops or excessive refetches
|
|
39
|
+
const formDataStr = JSON.stringify(formData || {});
|
|
40
|
+
const siblingDataStr = JSON.stringify(siblingData || {});
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (field.options !== "__KYRO_DYNAMIC_OPTIONS__") return;
|
|
44
|
+
|
|
45
|
+
const fetchOptions = async () => {
|
|
46
|
+
try {
|
|
47
|
+
let url = "";
|
|
48
|
+
if (collectionSlug) {
|
|
49
|
+
url = `${apiPath}/${collectionSlug}/dynamic-options/${field.name}`;
|
|
50
|
+
} else if (globalSlug) {
|
|
51
|
+
url = `${apiPath}/globals/${globalSlug}/dynamic-options/${field.name}`;
|
|
52
|
+
} else {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const res = await fetchWithAuth(url, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: { "Content-Type": "application/json" },
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
data: JSON.parse(formDataStr),
|
|
61
|
+
siblingData: JSON.parse(siblingDataStr)
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (res.ok) {
|
|
66
|
+
const json = await res.json();
|
|
67
|
+
if (json.options) {
|
|
68
|
+
setDynamicOptions(json.options);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error("Failed to fetch dynamic options:", err);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const timer = setTimeout(fetchOptions, 300);
|
|
77
|
+
return () => clearTimeout(timer);
|
|
78
|
+
}, [formDataStr, siblingDataStr, field.name, field.options, collectionSlug, globalSlug]);
|
|
79
|
+
|
|
25
80
|
// Resolve dynamic options at runtime if configured
|
|
26
|
-
let options =
|
|
81
|
+
let options: Array<{ label: string; value: string }> = dynamicOptions || [];
|
|
82
|
+
|
|
83
|
+
if (field.options !== "__KYRO_DYNAMIC_OPTIONS__") {
|
|
84
|
+
if (typeof field.options === "function") {
|
|
85
|
+
options = field.options({ data: formData || {}, siblingData: siblingData || {} });
|
|
86
|
+
} else if (Array.isArray(field.options)) {
|
|
87
|
+
options = field.options;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
27
91
|
if (field.dynamicOptions === "collections") {
|
|
28
92
|
options = Object.keys(collections)
|
|
29
93
|
.filter((slug) => slug !== "media")
|
|
@@ -347,46 +347,6 @@ export function UploadField({
|
|
|
347
347
|
);
|
|
348
348
|
};
|
|
349
349
|
|
|
350
|
-
if (value) {
|
|
351
|
-
return (
|
|
352
|
-
<div className="space-y-2">
|
|
353
|
-
{isMultiple ? (
|
|
354
|
-
<div className="grid grid-cols-2 gap-2">
|
|
355
|
-
{currentValue.map((img: any, i: number) =>
|
|
356
|
-
renderImagePreview(img, i),
|
|
357
|
-
)}
|
|
358
|
-
{canAddMore && (
|
|
359
|
-
<button
|
|
360
|
-
type="button"
|
|
361
|
-
onClick={() => {
|
|
362
|
-
setSelectedItems([]);
|
|
363
|
-
setShowPicker(true);
|
|
364
|
-
}}
|
|
365
|
-
disabled={disabled}
|
|
366
|
-
className="flex items-center justify-center h-12 border-2 border-dashed border-[var(--kyro-border)] rounded-lg text-sm text-[var(--kyro-text-secondary)] hover:border-[var(--kyro-border-active)] cursor-pointer transition-colors"
|
|
367
|
-
>
|
|
368
|
-
+ Add {fieldLabel}
|
|
369
|
-
</button>
|
|
370
|
-
)}
|
|
371
|
-
</div>
|
|
372
|
-
) : (
|
|
373
|
-
renderImagePreview(value)
|
|
374
|
-
)}
|
|
375
|
-
<input
|
|
376
|
-
ref={inputRef}
|
|
377
|
-
type="file"
|
|
378
|
-
accept="image/*"
|
|
379
|
-
onChange={(e) => {
|
|
380
|
-
const file = e.target.files?.[0];
|
|
381
|
-
if (file) uploadFile(file);
|
|
382
|
-
}}
|
|
383
|
-
disabled={disabled}
|
|
384
|
-
className="hidden"
|
|
385
|
-
/>
|
|
386
|
-
</div>
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
350
|
return (
|
|
391
351
|
<div className="space-y-2 relative">
|
|
392
352
|
<input
|
|
@@ -400,35 +360,46 @@ export function UploadField({
|
|
|
400
360
|
disabled={disabled}
|
|
401
361
|
className="hidden"
|
|
402
362
|
/>
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
>
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
363
|
+
|
|
364
|
+
{currentValue.length > 0 && (
|
|
365
|
+
<div className={isMultiple ? "grid grid-cols-2 gap-2" : "space-y-2"}>
|
|
366
|
+
{currentValue.map((img: any, i: number) =>
|
|
367
|
+
renderImagePreview(img, i),
|
|
368
|
+
)}
|
|
369
|
+
</div>
|
|
370
|
+
)}
|
|
371
|
+
|
|
372
|
+
{(!currentValue.length || canAddMore) && (
|
|
373
|
+
<div className="flex gap-2 flex-wrap">
|
|
374
|
+
<button
|
|
375
|
+
type="button"
|
|
376
|
+
onClick={() => inputRef.current?.click()}
|
|
377
|
+
disabled={disabled}
|
|
378
|
+
className="px-3 py-1.5 text-xs font-semibold rounded border border-dashed border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] cursor-pointer hover:border-[var(--kyro-border-active)] transition-colors"
|
|
379
|
+
>
|
|
380
|
+
+ Upload {fieldLabel}
|
|
381
|
+
</button>
|
|
382
|
+
<button
|
|
383
|
+
type="button"
|
|
384
|
+
onClick={() => {
|
|
385
|
+
setSelectedItems([]);
|
|
386
|
+
setShowPicker(true);
|
|
387
|
+
}}
|
|
388
|
+
disabled={disabled}
|
|
389
|
+
className="px-3 py-1.5 text-xs font-semibold rounded border border-[var(--kyro-border)] bg-[var(--kyro-surface)] text-[var(--kyro-text-secondary)] cursor-pointer hover:border-[var(--kyro-border-active)] transition-colors"
|
|
390
|
+
>
|
|
391
|
+
Library
|
|
392
|
+
</button>
|
|
393
|
+
<button
|
|
394
|
+
type="button"
|
|
395
|
+
onClick={() => setShowUrlInput(!showUrlInput)}
|
|
396
|
+
disabled={disabled}
|
|
397
|
+
className="px-3 py-1.5 text-xs font-semibold rounded border border-[var(--kyro-border)] bg-[var(--kyro-surface)] text-[var(--kyro-text-secondary)] cursor-pointer hover:border-[var(--kyro-border-active)] transition-colors"
|
|
398
|
+
>
|
|
399
|
+
URL
|
|
400
|
+
</button>
|
|
401
|
+
</div>
|
|
402
|
+
)}
|
|
432
403
|
|
|
433
404
|
{showUrlInput && (
|
|
434
405
|
<div className="flex gap-2 items-center">
|
package/src/integration.ts
CHANGED
|
@@ -79,14 +79,17 @@ export function kyroAdmin(options: KyroAdminOptions = {}): AstroIntegration {
|
|
|
79
79
|
import { parentPort } from 'worker_threads';
|
|
80
80
|
import('${pathToFileURL(tmpFile).href}').then(mod => {
|
|
81
81
|
const cfg = mod.default || mod;
|
|
82
|
-
const serialize = (obj) => {
|
|
82
|
+
const serialize = (obj, key) => {
|
|
83
83
|
if (obj === null || obj === undefined) return obj;
|
|
84
|
-
if (typeof obj === 'function')
|
|
85
|
-
|
|
84
|
+
if (typeof obj === 'function') {
|
|
85
|
+
if (key === 'options') return "__KYRO_DYNAMIC_OPTIONS__";
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
if (Array.isArray(obj)) return obj.map((v) => serialize(v));
|
|
86
89
|
if (typeof obj === 'object') {
|
|
87
90
|
const result = {};
|
|
88
91
|
for (const [k, v] of Object.entries(obj)) {
|
|
89
|
-
const sv = serialize(v);
|
|
92
|
+
const sv = serialize(v, k);
|
|
90
93
|
if (sv !== undefined) result[k] = sv;
|
|
91
94
|
}
|
|
92
95
|
return result;
|
|
@@ -25,7 +25,7 @@ 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 <=
|
|
28
|
+
if (hasId && hasMediaField && keys.length <= 25) {
|
|
29
29
|
return obj.id;
|
|
30
30
|
}
|
|
31
31
|
|