@kyro-cms/admin 0.9.0 → 0.9.1
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 +11960 -11006
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +67 -65
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +563 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.js +12183 -11238
- package/dist/index.js.map +1 -1
- package/package.json +15 -11
- package/src/components/ActionBar.tsx +27 -14
- package/src/components/Admin.tsx +1 -1
- package/src/components/ApiKeysManager.tsx +5 -5
- package/src/components/AutoForm.tsx +585 -369
- package/src/components/BrandingHub.tsx +7 -4
- package/src/components/CreateView.tsx +2 -0
- package/src/components/DetailView.tsx +71 -56
- package/src/components/DeveloperCenter.tsx +8 -6
- package/src/components/FieldRenderer.tsx +94 -19
- package/src/components/ListView.tsx +33 -20
- package/src/components/MediaGallery.tsx +219 -194
- package/src/components/PluginsManager.tsx +197 -70
- package/src/components/RestPlayground.tsx +7 -7
- package/src/components/SessionsManager.tsx +1 -1
- package/src/components/SettingsPage.tsx +22 -0
- package/src/components/Sidebar.astro +13 -41
- package/src/components/UserManagement.tsx +153 -15
- package/src/components/UserMenu.tsx +30 -4
- package/src/components/VersionHistoryPanel.tsx +112 -119
- package/src/components/WebhookManager.tsx +6 -4
- package/src/components/blocks/ArrayBlock.tsx +6 -23
- package/src/components/blocks/BlockEditModal.tsx +82 -309
- package/src/components/blocks/CardBlock.tsx +35 -0
- package/src/components/blocks/ChildBlocksTree.tsx +57 -31
- package/src/components/blocks/GenericBlock.tsx +44 -0
- package/src/components/blocks/HeadingSubheadingBlock.tsx +32 -0
- package/src/components/blocks/HeroBlock.tsx +5 -14
- package/src/components/blocks/RichTextBlock.tsx +5 -5
- package/src/components/blocks/index.ts +5 -3
- package/src/components/fields/AccordionField.tsx +2 -2
- package/src/components/fields/ArrayField.tsx +1 -1
- package/src/components/fields/ArrayLayout.tsx +120 -29
- package/src/components/fields/BlocksField.tsx +430 -50
- package/src/components/fields/CardField.tsx +73 -0
- package/src/components/fields/CheckboxField.tsx +7 -3
- package/src/components/fields/DateField.tsx +4 -1
- package/src/components/fields/GroupLayout.tsx +2 -2
- package/src/components/fields/HeadingSubheadingField.tsx +43 -0
- package/src/components/fields/ListField.tsx +2 -2
- package/src/components/fields/NumberField.tsx +4 -1
- package/src/components/fields/RelationshipField.tsx +153 -87
- package/src/components/fields/RichTextField.tsx +781 -0
- package/src/components/fields/SecretField.tsx +102 -0
- package/src/components/fields/SelectField.tsx +19 -6
- package/src/components/fields/TabsLayout.tsx +19 -9
- package/src/components/fields/TextField.tsx +4 -1
- package/src/components/fields/UploadField.tsx +122 -56
- package/src/components/fields/extensions/blockComponents.tsx +103 -174
- package/src/components/fields/extensions/blocksStore.ts +8 -1
- package/src/components/fields/index.ts +4 -2
- package/src/components/ui/PageHeader.tsx +5 -5
- package/src/components/ui/SlidePanel.tsx +8 -3
- package/src/components/ui/icons.tsx +109 -109
- package/src/components/users/UserDetail.tsx +79 -16
- package/src/hooks/useAutoFormState.ts +125 -62
- package/src/integration.ts +148 -46
- package/src/kyro-cms.d.ts +7 -2
- package/src/layouts/AuthLayout.astro +14 -2
- package/src/lib/autoform-store.ts +85 -52
- package/src/lib/change-source.ts +9 -0
- package/src/lib/config.ts +104 -8
- package/src/lib/globals.ts +44 -9
- package/src/lib/normalize-upload-fields.ts +41 -0
- package/src/lib/paths.ts +2 -2
- package/src/lib/resolve-field-value.ts +110 -0
- package/src/lib/shim/use-sync-external-store-with-selector.js +30 -0
- package/src/lib/shim/use-sync-external-store.js +1 -0
- package/src/lib/stores/index.ts +1 -0
- package/src/lib/useResourceManager.ts +4 -4
- package/src/lib/vite-shim-plugin.ts +100 -0
- package/src/pages/[collection]/[id].astro +1 -1
- package/src/pages/preview/[collection]/[id].astro +4 -4
- package/src/pages/settings/[slug].astro +2 -2
- package/src/styles/main.css +60 -54
- package/README.md +0 -46
- package/dist/EditorClient-Q23UXR37.cjs +0 -468
- package/dist/EditorClient-Q23UXR37.cjs.map +0 -1
- package/dist/EditorClient-T5PASFNR.js +0 -466
- package/dist/EditorClient-T5PASFNR.js.map +0 -1
- package/dist/chunk-3BGDYKTD.cjs +0 -348
- package/dist/chunk-3BGDYKTD.cjs.map +0 -1
- package/dist/chunk-EEFXLQVT.js +0 -3
- package/dist/chunk-EEFXLQVT.js.map +0 -1
- package/src/components/blocks/ButtonBlock.tsx +0 -64
- package/src/components/blocks/ColumnsBlock.tsx +0 -55
- package/src/components/blocks/DividerBlock.tsx +0 -43
- package/src/components/blocks/LinkBlock.tsx +0 -65
- package/src/components/blocks/VStackBlock.tsx +0 -29
- package/src/components/fields/EditorClient.tsx +0 -535
- package/src/components/fields/PortableTextField.tsx +0 -155
- package/src/components/fields/PortableTextRenderer.tsx +0 -68
|
@@ -11,12 +11,16 @@ interface CheckboxFieldComponentProps {
|
|
|
11
11
|
|
|
12
12
|
export default function CheckboxField({
|
|
13
13
|
field,
|
|
14
|
-
value
|
|
14
|
+
value,
|
|
15
15
|
onChange,
|
|
16
16
|
error,
|
|
17
17
|
disabled,
|
|
18
18
|
}: CheckboxFieldComponentProps) {
|
|
19
|
-
const isReadOnly =
|
|
19
|
+
const isReadOnly =
|
|
20
|
+
typeof field.admin?.readOnly === "function"
|
|
21
|
+
? false
|
|
22
|
+
: Boolean(field.admin?.readOnly);
|
|
23
|
+
const checked = value ?? false;
|
|
20
24
|
|
|
21
25
|
return (
|
|
22
26
|
<FieldLayout
|
|
@@ -27,7 +31,7 @@ export default function CheckboxField({
|
|
|
27
31
|
<label className="flex items-center gap-2.5 cursor-pointer group py-0.5">
|
|
28
32
|
<input
|
|
29
33
|
type="checkbox"
|
|
30
|
-
checked={
|
|
34
|
+
checked={checked}
|
|
31
35
|
onChange={(e) => onChange?.(e.target.checked)}
|
|
32
36
|
disabled={disabled || isReadOnly}
|
|
33
37
|
className={`w-4 h-4 rounded border-[var(--kyro-border)] text-[var(--kyro-primary)] focus:ring-[var(--kyro-primary)] transition-all ${
|
|
@@ -16,7 +16,10 @@ export default function DateField({
|
|
|
16
16
|
error,
|
|
17
17
|
disabled,
|
|
18
18
|
}: DateFieldComponentProps) {
|
|
19
|
-
const isReadOnly =
|
|
19
|
+
const isReadOnly =
|
|
20
|
+
typeof field.admin?.readOnly === "function"
|
|
21
|
+
? false
|
|
22
|
+
: Boolean(field.admin?.readOnly);
|
|
20
23
|
|
|
21
24
|
return (
|
|
22
25
|
<FieldLayout
|
|
@@ -22,10 +22,10 @@ export function GroupLayout({
|
|
|
22
22
|
|
|
23
23
|
return (
|
|
24
24
|
<div className="kyro-form-group border border-[var(--kyro-border)] rounded-[var(--kyro-radius-lg)] p-6 bg-[var(--kyro-surface-accent)]/30">
|
|
25
|
-
<h3 className="text-sm font-bold
|
|
25
|
+
<h3 className="text-sm font-bold tracking-widest text-[var(--kyro-text-primary)] mb-6 border-b border-[var(--kyro-border)] pb-2 inline-block">
|
|
26
26
|
{field.label || field.name}
|
|
27
27
|
</h3>
|
|
28
|
-
<div className="space-y-6">
|
|
28
|
+
<div className={field.admin?.inline ? "flex items-start gap-4" : "space-y-6"}>
|
|
29
29
|
{(field as Field & { fields?: Field[] }).fields.map((f: Field) =>
|
|
30
30
|
renderField(f, groupData, onChange),
|
|
31
31
|
)}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
interface HeadingSubheadingFieldProps {
|
|
4
|
+
heading?: string;
|
|
5
|
+
subheading?: string;
|
|
6
|
+
onChange: (field: string, value: string) => void;
|
|
7
|
+
compact?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const HeadingSubheadingField: React.FC<HeadingSubheadingFieldProps> = ({
|
|
11
|
+
heading = "",
|
|
12
|
+
subheading = "",
|
|
13
|
+
onChange,
|
|
14
|
+
compact = false,
|
|
15
|
+
}) => {
|
|
16
|
+
const inputClass = compact
|
|
17
|
+
? "w-full px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
18
|
+
: "w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent";
|
|
19
|
+
|
|
20
|
+
const textareaClass = compact
|
|
21
|
+
? "w-full px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] min-h-[50px] resize-none focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
22
|
+
: "w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] min-h-[80px] resize-none focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent";
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className={compact ? "space-y-2" : "space-y-3"}>
|
|
26
|
+
<input
|
|
27
|
+
type="text"
|
|
28
|
+
value={heading}
|
|
29
|
+
onChange={(e) => onChange("heading", e.target.value)}
|
|
30
|
+
className={`${inputClass} font-bold text-base`}
|
|
31
|
+
placeholder="Heading..."
|
|
32
|
+
/>
|
|
33
|
+
<textarea
|
|
34
|
+
value={subheading}
|
|
35
|
+
onChange={(e) => onChange("subheading", e.target.value)}
|
|
36
|
+
className={textareaClass}
|
|
37
|
+
placeholder="Subheading..."
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default HeadingSubheadingField;
|
|
@@ -32,13 +32,13 @@ export const ListField: React.FC<ListFieldProps> = ({
|
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
const inputClass = compact
|
|
35
|
-
? "w-full px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
35
|
+
? "w-full px-2.5 py-1.5 border border-[var(--kyro-border)] rounded-md bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
36
36
|
: "w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent";
|
|
37
37
|
|
|
38
38
|
return (
|
|
39
39
|
<div className={compact ? "space-y-1.5" : "space-y-2"}>
|
|
40
40
|
{items.length === 0 ? (
|
|
41
|
-
<div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-
|
|
41
|
+
<div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-md">
|
|
42
42
|
No items. Type below to add.
|
|
43
43
|
</div>
|
|
44
44
|
) : (
|
|
@@ -16,7 +16,10 @@ export default function NumberField({
|
|
|
16
16
|
error,
|
|
17
17
|
disabled,
|
|
18
18
|
}: NumberFieldComponentProps) {
|
|
19
|
-
const isReadOnly =
|
|
19
|
+
const isReadOnly =
|
|
20
|
+
typeof field.admin?.readOnly === "function"
|
|
21
|
+
? false
|
|
22
|
+
: Boolean(field.admin?.readOnly);
|
|
20
23
|
|
|
21
24
|
return (
|
|
22
25
|
<FieldLayout
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useState, useRef } from "react";
|
|
1
|
+
import { useEffect, useState, useRef, useCallback } from "react";
|
|
2
2
|
import { Search, X, ChevronDown, Loader2 } from "../ui/icons";
|
|
3
3
|
import { apiGet, buildSearchQuery } from "../../lib/api";
|
|
4
4
|
|
|
@@ -15,12 +15,33 @@ interface RelationshipFieldProps {
|
|
|
15
15
|
placeholder?: string;
|
|
16
16
|
};
|
|
17
17
|
};
|
|
18
|
-
value?:
|
|
19
|
-
onChange?: (value:
|
|
18
|
+
value?: unknown;
|
|
19
|
+
onChange?: (value: unknown) => void;
|
|
20
20
|
error?: string;
|
|
21
21
|
disabled?: boolean;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
interface ResolvedDoc {
|
|
25
|
+
id: string;
|
|
26
|
+
relationTo?: string;
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getLabel(opt: Record<string, unknown>): string {
|
|
31
|
+
const mainTabs = opt?.mainTabs as Record<string, unknown> | undefined;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
(opt?.title as string) ||
|
|
35
|
+
(mainTabs?.title as string) ||
|
|
36
|
+
(opt?.name as string) ||
|
|
37
|
+
(opt?.label as string) ||
|
|
38
|
+
(opt?.email as string) ||
|
|
39
|
+
(opt?.filename as string) ||
|
|
40
|
+
(opt?.slug as string) ||
|
|
41
|
+
"Untitled"
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
24
45
|
export function RelationshipField({
|
|
25
46
|
field,
|
|
26
47
|
value,
|
|
@@ -30,28 +51,85 @@ export function RelationshipField({
|
|
|
30
51
|
}: RelationshipFieldProps) {
|
|
31
52
|
const [isOpen, setIsOpen] = useState(false);
|
|
32
53
|
const [search, setSearch] = useState("");
|
|
33
|
-
const [options, setOptions] = useState<
|
|
54
|
+
const [options, setOptions] = useState<ResolvedDoc[]>([]);
|
|
34
55
|
const [loading, setLoading] = useState(false);
|
|
56
|
+
const [selectedDocs, setSelectedDocs] = useState<ResolvedDoc[]>([]);
|
|
57
|
+
const fetchedIdsRef = useRef<Set<string>>(new Set());
|
|
35
58
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
59
|
+
const onChangeRef = useRef<(value: unknown) => void>(() => {});
|
|
60
|
+
onChangeRef.current = onChange || (() => {});
|
|
36
61
|
|
|
37
62
|
const isMultiple = field.hasMany;
|
|
38
63
|
const relationTo = Array.isArray(field.relationTo)
|
|
39
64
|
? field.relationTo
|
|
40
65
|
: [field.relationTo];
|
|
41
|
-
const
|
|
66
|
+
const isPolymorphic = relationTo.length > 1;
|
|
67
|
+
const [activeRelation, setActiveRelation] = useState(relationTo[0]);
|
|
68
|
+
|
|
69
|
+
const extractIds = useCallback((): string[] => {
|
|
70
|
+
if (!value) return [];
|
|
71
|
+
const items = isMultiple
|
|
72
|
+
? Array.isArray(value) ? value : []
|
|
73
|
+
: value ? [value] : [];
|
|
74
|
+
return items.map((item) => {
|
|
75
|
+
if (typeof item === "object" && item !== null) {
|
|
76
|
+
return (item as { value?: string }).value || (item as { id?: string }).id || "";
|
|
77
|
+
}
|
|
78
|
+
return String(item);
|
|
79
|
+
}).filter(Boolean);
|
|
80
|
+
}, [value, isMultiple]);
|
|
42
81
|
|
|
43
|
-
const
|
|
82
|
+
const fetchSelectedDocs = useCallback((ids: string[]) => {
|
|
83
|
+
if (ids.length === 0) return;
|
|
84
|
+
ids.forEach((id) => {
|
|
85
|
+
if (fetchedIdsRef.current.has(id)) return;
|
|
86
|
+
fetchedIdsRef.current.add(id);
|
|
87
|
+
const rel = isPolymorphic
|
|
88
|
+
? (() => {
|
|
89
|
+
if (!value) return activeRelation;
|
|
90
|
+
const items = isMultiple
|
|
91
|
+
? Array.isArray(value) ? value : []
|
|
92
|
+
: [value];
|
|
93
|
+
const match = items.find((item) => {
|
|
94
|
+
if (typeof item === "object" && item !== null) {
|
|
95
|
+
return (item as { value?: string }).value === id || (item as { id?: string }).id === id;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
});
|
|
99
|
+
return match && typeof match === "object" ? (match as { relationTo?: string }).relationTo || activeRelation : activeRelation;
|
|
100
|
+
})()
|
|
101
|
+
: activeRelation;
|
|
102
|
+
apiGet<Record<string, unknown>>(`/api/${rel}/${id}`)
|
|
103
|
+
.then((response) => {
|
|
104
|
+
const doc = (response as any).data || response;
|
|
105
|
+
if (!doc || typeof doc !== "object") return;
|
|
106
|
+
|
|
107
|
+
setSelectedDocs((prev) => {
|
|
108
|
+
if (prev.some((d) => d.id === id)) return prev;
|
|
109
|
+
return [...prev, { ...doc, id: String((doc as any).id), relationTo: rel }];
|
|
110
|
+
});
|
|
111
|
+
})
|
|
112
|
+
.catch(() => {});
|
|
113
|
+
});
|
|
114
|
+
}, [isPolymorphic, value, activeRelation, isMultiple]);
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
const ids = extractIds();
|
|
118
|
+
fetchSelectedDocs(ids);
|
|
119
|
+
}, [extractIds, fetchSelectedDocs]);
|
|
120
|
+
|
|
121
|
+
const fetchOptions = useCallback((query: string = "") => {
|
|
44
122
|
setLoading(true);
|
|
45
123
|
const searchFields = ["title", "name", "label", "email"];
|
|
46
|
-
const url = `/api/${
|
|
124
|
+
const url = `/api/${activeRelation}?${buildSearchQuery(query, searchFields)}`;
|
|
47
125
|
|
|
48
|
-
apiGet(url)
|
|
126
|
+
apiGet<{ docs?: Record<string, unknown>[] }>(url)
|
|
49
127
|
.then((data) => {
|
|
50
128
|
setOptions((prev) => {
|
|
51
129
|
const existingIds = new Set(prev.map((o) => o.id));
|
|
52
|
-
const newDocs = (data.docs || []).filter(
|
|
53
|
-
|
|
54
|
-
|
|
130
|
+
const newDocs: ResolvedDoc[] = (data.docs || []).filter(
|
|
131
|
+
(d) => !existingIds.has(d.id as string),
|
|
132
|
+
).map((d) => ({ ...d, id: d.id as string }));
|
|
55
133
|
return [...prev, ...newDocs];
|
|
56
134
|
});
|
|
57
135
|
setLoading(false);
|
|
@@ -59,39 +137,14 @@ const newDocs = (data.docs || []).filter(
|
|
|
59
137
|
.catch(() => {
|
|
60
138
|
setLoading(false);
|
|
61
139
|
});
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const fetchSelectedItems = () => {
|
|
65
|
-
const items: (string | Record<string, unknown>)[] = isMultiple
|
|
66
|
-
? Array.isArray(value)
|
|
67
|
-
? value
|
|
68
|
-
: []
|
|
69
|
-
: value
|
|
70
|
-
? [value]
|
|
71
|
-
: [];
|
|
72
|
-
items.forEach((itemId) => {
|
|
73
|
-
const id = typeof itemId === "object" ? itemId?.id : itemId;
|
|
74
|
-
if (id && !options.some((o) => o.id === id)) {
|
|
75
|
-
apiGet(`/api/${targetCollection}/${id}`)
|
|
76
|
-
.then((doc) => {
|
|
77
|
-
setOptions((prev) => [...prev, doc]);
|
|
78
|
-
})
|
|
79
|
-
.catch(() => {});
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
useEffect(() => {
|
|
85
|
-
if (value) {
|
|
86
|
-
fetchSelectedItems();
|
|
87
|
-
}
|
|
88
|
-
}, [value, targetCollection]);
|
|
140
|
+
}, [activeRelation]);
|
|
89
141
|
|
|
90
142
|
useEffect(() => {
|
|
91
143
|
if (isOpen) {
|
|
144
|
+
setOptions([]);
|
|
92
145
|
fetchOptions(search);
|
|
93
146
|
}
|
|
94
|
-
}, [isOpen,
|
|
147
|
+
}, [isOpen, activeRelation]);
|
|
95
148
|
|
|
96
149
|
useEffect(() => {
|
|
97
150
|
const handleClickOutside = (event: MouseEvent) => {
|
|
@@ -106,24 +159,14 @@ const fetchSelectedItems = () => {
|
|
|
106
159
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
107
160
|
}, []);
|
|
108
161
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
opt?.email ||
|
|
115
|
-
opt?.filename ||
|
|
116
|
-
opt?.slug ||
|
|
117
|
-
String(opt?.id) ||
|
|
118
|
-
"Untitled"
|
|
119
|
-
);
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
const getValueId = (val: string | Record<string, unknown>) => {
|
|
123
|
-
return val?.id || val;
|
|
162
|
+
const getValueId = (val: unknown): string => {
|
|
163
|
+
if (typeof val === "object" && val !== null) {
|
|
164
|
+
return (val as { value?: string }).value || (val as { id?: string }).id || "";
|
|
165
|
+
}
|
|
166
|
+
return String(val);
|
|
124
167
|
};
|
|
125
168
|
|
|
126
|
-
const isSelected = (opt: Record<string, unknown>) => {
|
|
169
|
+
const isSelected = (opt: Record<string, unknown>): boolean => {
|
|
127
170
|
const optId = opt.id;
|
|
128
171
|
if (!value) return false;
|
|
129
172
|
if (isMultiple && Array.isArray(value)) {
|
|
@@ -135,60 +178,56 @@ const fetchSelectedItems = () => {
|
|
|
135
178
|
const handleSelect = (opt: Record<string, unknown>) => {
|
|
136
179
|
const optId = opt.id;
|
|
137
180
|
if (isMultiple) {
|
|
138
|
-
const current = Array.isArray(value) ? value : [];
|
|
181
|
+
const current: unknown[] = Array.isArray(value) ? value : [];
|
|
139
182
|
if (isSelected(opt)) {
|
|
140
|
-
|
|
183
|
+
onChangeRef.current?.(current.filter((v) => getValueId(v) !== optId));
|
|
141
184
|
} else {
|
|
142
|
-
|
|
185
|
+
const newItem = isPolymorphic
|
|
186
|
+
? { relationTo: activeRelation, value: optId }
|
|
187
|
+
: optId;
|
|
188
|
+
onChangeRef.current?.([...current, newItem]);
|
|
143
189
|
}
|
|
144
190
|
} else {
|
|
145
191
|
if (isSelected(opt)) {
|
|
146
|
-
|
|
192
|
+
onChangeRef.current?.(null);
|
|
147
193
|
} else {
|
|
148
|
-
|
|
194
|
+
const newItem = isPolymorphic
|
|
195
|
+
? { relationTo: activeRelation, value: optId }
|
|
196
|
+
: optId;
|
|
197
|
+
onChangeRef.current?.(newItem);
|
|
149
198
|
setIsOpen(false);
|
|
150
199
|
setSearch("");
|
|
151
200
|
}
|
|
152
201
|
}
|
|
153
202
|
};
|
|
154
203
|
|
|
155
|
-
const handleClear = () => {
|
|
156
|
-
onChange?.(isMultiple ? [] : null);
|
|
157
|
-
};
|
|
158
|
-
|
|
159
204
|
const renderSelectedItems = () => {
|
|
160
205
|
if (!value) return null;
|
|
161
206
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
} else {
|
|
166
|
-
items = value ? [value] : [];
|
|
167
|
-
}
|
|
207
|
+
const items: unknown[] = isMultiple
|
|
208
|
+
? Array.isArray(value) ? value : []
|
|
209
|
+
: value ? [value] : [];
|
|
168
210
|
|
|
169
211
|
return (
|
|
170
212
|
<div className="flex flex-wrap gap-1.5 mt-2">
|
|
171
|
-
{items.map((item
|
|
172
|
-
const rawId =
|
|
173
|
-
const
|
|
174
|
-
const label =
|
|
213
|
+
{items.map((item) => {
|
|
214
|
+
const rawId = getValueId(item);
|
|
215
|
+
const doc = selectedDocs.find((d) => d.id === rawId);
|
|
216
|
+
const label = doc ? getLabel(doc) : rawId.slice(0, 12);
|
|
217
|
+
const rel = isPolymorphic && doc ? doc.relationTo : null;
|
|
175
218
|
return (
|
|
176
219
|
<span
|
|
177
|
-
key={
|
|
220
|
+
key={rawId}
|
|
178
221
|
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)]"
|
|
179
222
|
>
|
|
223
|
+
{rel && <span className="opacity-60 mr-0.5">{rel}:</span>}
|
|
180
224
|
{label}
|
|
181
225
|
{!disabled && (
|
|
182
226
|
<button
|
|
183
227
|
type="button"
|
|
184
228
|
onClick={() => {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
(items as (string | Record<string, unknown>)[]).filter((_: unknown, i: number) => i !== idx),
|
|
188
|
-
);
|
|
189
|
-
} else {
|
|
190
|
-
onChange?.(null);
|
|
191
|
-
}
|
|
229
|
+
const filtered = items.filter((value) => getValueId(value) !== rawId);
|
|
230
|
+
onChangeRef.current?.(isMultiple ? filtered : (filtered[0] ?? null));
|
|
192
231
|
}}
|
|
193
232
|
className="hover:opacity-70"
|
|
194
233
|
>
|
|
@@ -213,6 +252,28 @@ onChange?.(
|
|
|
213
252
|
</label>
|
|
214
253
|
)}
|
|
215
254
|
<div ref={containerRef} className="relative">
|
|
255
|
+
{isPolymorphic && (
|
|
256
|
+
<div className="flex gap-1 mb-1.5">
|
|
257
|
+
{relationTo.map((rel) => (
|
|
258
|
+
<button
|
|
259
|
+
key={rel}
|
|
260
|
+
type="button"
|
|
261
|
+
onClick={() => {
|
|
262
|
+
setActiveRelation(rel);
|
|
263
|
+
setOptions([]);
|
|
264
|
+
setSearch("");
|
|
265
|
+
}}
|
|
266
|
+
className={`px-2 py-0.5 text-[10px] font-bold rounded transition-colors ${
|
|
267
|
+
activeRelation === rel
|
|
268
|
+
? "kyro-btn-primary"
|
|
269
|
+
: "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-border)]"
|
|
270
|
+
}`}
|
|
271
|
+
>
|
|
272
|
+
{rel}
|
|
273
|
+
</button>
|
|
274
|
+
))}
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
216
277
|
<div className="relative">
|
|
217
278
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
218
279
|
<input
|
|
@@ -225,10 +286,15 @@ onChange?.(
|
|
|
225
286
|
}}
|
|
226
287
|
onFocus={() => setIsOpen(true)}
|
|
227
288
|
placeholder={
|
|
228
|
-
field.admin?.placeholder || `Search ${
|
|
289
|
+
field.admin?.placeholder || `Search ${activeRelation}...`
|
|
290
|
+
}
|
|
291
|
+
disabled={
|
|
292
|
+
disabled ||
|
|
293
|
+
(typeof field.admin?.readOnly === "function"
|
|
294
|
+
? false
|
|
295
|
+
: Boolean(field.admin?.readOnly))
|
|
229
296
|
}
|
|
230
|
-
|
|
231
|
-
className="w-full pl-9 pr-10 py-2 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent disabled:opacity-50"
|
|
297
|
+
className="w-full pl-9 pr-10 py-2 border border-[var(--kyro-border)] rounded-md bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent disabled:opacity-50"
|
|
232
298
|
/>
|
|
233
299
|
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
|
234
300
|
{loading ? (
|
|
@@ -242,7 +308,7 @@ onChange?.(
|
|
|
242
308
|
</div>
|
|
243
309
|
|
|
244
310
|
{isOpen && (
|
|
245
|
-
<div className="
|
|
311
|
+
<div className="relative z-20 w-full mt-1 border border-[var(--kyro-border)] rounded-lg shadow-lg bg-[var(--kyro-surface)] max-h-64 overflow-auto">
|
|
246
312
|
{loading ? (
|
|
247
313
|
<div className="p-4 text-center text-sm text-[var(--kyro-text-muted)]">
|
|
248
314
|
Loading...
|
|
@@ -272,7 +338,7 @@ onChange?.(
|
|
|
272
338
|
</span>
|
|
273
339
|
)}
|
|
274
340
|
</div>
|
|
275
|
-
{opt.slug && (
|
|
341
|
+
{"slug" in opt && typeof opt.slug === "string" && (
|
|
276
342
|
<div className="text-xs text-[var(--kyro-text-muted)]">
|
|
277
343
|
{opt.slug}
|
|
278
344
|
</div>
|