@kyro-cms/admin 0.12.6 → 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 +32 -110
- package/dist/index.css +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +32 -110
- package/package.json +11 -5
- package/src/components/AuthBridge.tsx +2 -2
- package/src/components/AutoForm.tsx +49 -57
- package/src/components/BrandingHub.tsx +1 -1
- package/src/components/DashboardMetrics.tsx +519 -290
- package/src/components/DetailView.tsx +5 -3
- package/src/components/FieldRenderer.tsx +59 -47
- package/src/components/ListView.tsx +25 -10
- package/src/components/MediaGallery.tsx +1 -1
- package/src/components/PluginsManager.tsx +39 -21
- package/src/components/Sidebar.astro +7 -14
- 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/blocks/AccordionBlock.tsx +8 -4
- package/src/components/blocks/ArrayBlock.tsx +4 -3
- package/src/components/blocks/BlockEditModal.tsx +4 -3
- package/src/components/blocks/CardBlock.tsx +10 -2
- package/src/components/blocks/ChildBlocksTree.tsx +19 -18
- package/src/components/blocks/CodeBlock.tsx +7 -2
- package/src/components/blocks/FileBlock.tsx +6 -2
- package/src/components/blocks/GenericBlock.tsx +1 -1
- package/src/components/blocks/HeadingBlock.tsx +6 -2
- package/src/components/blocks/HeadingSubheadingBlock.tsx +7 -2
- package/src/components/blocks/HeroBlock.tsx +12 -3
- package/src/components/blocks/ImageBlock.tsx +8 -2
- package/src/components/blocks/ListBlock.tsx +6 -2
- package/src/components/blocks/ParagraphBlock.tsx +2 -2
- package/src/components/blocks/RelationshipBlock.tsx +10 -2
- package/src/components/blocks/VideoBlock.tsx +7 -2
- package/src/components/fields/AccordionField.tsx +1 -1
- package/src/components/fields/ArrayLayout.tsx +55 -58
- package/src/components/fields/BlocksField.tsx +1 -1
- package/src/components/fields/ChildrenField.tsx +3 -2
- package/src/components/fields/CodeField.tsx +3 -2
- package/src/components/fields/ColumnsField.tsx +3 -2
- package/src/components/fields/DateField.tsx +2 -2
- package/src/components/fields/FieldLayout.tsx +3 -3
- package/src/components/fields/GroupLayout.tsx +2 -2
- package/src/components/fields/IconField.tsx +1 -1
- package/src/components/fields/MarkdownField.tsx +2 -2
- package/src/components/fields/NumberField.tsx +4 -4
- package/src/components/fields/RelationshipField.tsx +4 -1
- package/src/components/fields/RichTextField.tsx +200 -5
- package/src/components/fields/extensions/blockComponents.tsx +1 -1
- package/src/components/fields/extensions/blocksStore.ts +15 -12
- package/src/components/ui/Button.tsx +5 -2
- package/src/components/ui/Pagination.tsx +1 -1
- 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/fields/index.ts +1 -1
- package/src/hooks/useAutoFormState.ts +7 -6
- package/src/hooks/useQueue.ts +3 -2
- package/src/index.ts +0 -1
- package/src/integration.ts +50 -12
- package/src/kyro-cms.d.ts +5 -0
- package/src/lib/api.ts +1 -0
- package/src/lib/autoform-store.ts +4 -3
- package/src/lib/config.ts +6 -19
- package/src/lib/core-types.ts +78 -0
- 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 +155 -0
- package/src/styles/main.css +2 -0
- package/src/vite-env.d.ts +10 -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
|
@@ -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,5 +1,17 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
Field,
|
|
4
|
+
TextField as TextFieldType,
|
|
5
|
+
NumberField as NumberFieldType,
|
|
6
|
+
CheckboxField as CheckboxFieldType,
|
|
7
|
+
SelectField as SelectFieldType,
|
|
8
|
+
DateField as DateFieldType,
|
|
9
|
+
CodeField as CodeFieldType,
|
|
10
|
+
UploadField as UploadFieldType,
|
|
11
|
+
RelationshipField as RelationshipFieldType,
|
|
12
|
+
BlocksField as BlocksFieldType,
|
|
13
|
+
MarkdownField as MarkdownFieldType,
|
|
14
|
+
} from "@kyro-cms/core/client";
|
|
3
15
|
import { UploadField } from "./fields/UploadField";
|
|
4
16
|
import { CodeField } from "./fields";
|
|
5
17
|
import NumberField from "./fields/NumberField";
|
|
@@ -22,8 +34,8 @@ import { setChangeSource } from "../lib/change-source";
|
|
|
22
34
|
|
|
23
35
|
interface FieldRendererProps {
|
|
24
36
|
field: Field;
|
|
25
|
-
value:
|
|
26
|
-
onChange: (value:
|
|
37
|
+
value: unknown;
|
|
38
|
+
onChange: (value: unknown) => void;
|
|
27
39
|
error?: string;
|
|
28
40
|
disabled?: boolean;
|
|
29
41
|
formData?: Record<string, unknown>;
|
|
@@ -56,9 +68,9 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
56
68
|
case "url":
|
|
57
69
|
return (
|
|
58
70
|
<TextField
|
|
59
|
-
field={field as
|
|
60
|
-
value={value}
|
|
61
|
-
onChange={onChangeKeystroke}
|
|
71
|
+
field={field as TextFieldType}
|
|
72
|
+
value={(value as string) ?? undefined}
|
|
73
|
+
onChange={onChangeKeystroke as (value: string) => void}
|
|
62
74
|
error={error}
|
|
63
75
|
disabled={disabled}
|
|
64
76
|
/>
|
|
@@ -67,8 +79,8 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
67
79
|
return (
|
|
68
80
|
<IconField
|
|
69
81
|
field={field as any}
|
|
70
|
-
value={value}
|
|
71
|
-
onChange={onChangeKeystroke}
|
|
82
|
+
value={(value as string) ?? undefined}
|
|
83
|
+
onChange={onChangeKeystroke as (value: string) => void}
|
|
72
84
|
error={error}
|
|
73
85
|
disabled={disabled}
|
|
74
86
|
/>
|
|
@@ -76,9 +88,9 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
76
88
|
case "textarea":
|
|
77
89
|
return (
|
|
78
90
|
<TextField
|
|
79
|
-
field={{ ...field, variant: "textarea" } as
|
|
80
|
-
value={value}
|
|
81
|
-
onChange={onChangeKeystroke}
|
|
91
|
+
field={{ ...field, variant: "textarea" } as TextFieldType}
|
|
92
|
+
value={(value as string) ?? undefined}
|
|
93
|
+
onChange={onChangeKeystroke as (value: string) => void}
|
|
82
94
|
error={error}
|
|
83
95
|
disabled={disabled}
|
|
84
96
|
/>
|
|
@@ -86,9 +98,9 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
86
98
|
case "password":
|
|
87
99
|
return (
|
|
88
100
|
<TextField
|
|
89
|
-
field={{ ...field, variant: "password" } as
|
|
90
|
-
value={value}
|
|
91
|
-
onChange={onChangeKeystroke}
|
|
101
|
+
field={{ ...field, variant: "password" } as TextFieldType}
|
|
102
|
+
value={(value as string) ?? undefined}
|
|
103
|
+
onChange={onChangeKeystroke as (value: string) => void}
|
|
92
104
|
error={error}
|
|
93
105
|
disabled={disabled}
|
|
94
106
|
/>
|
|
@@ -97,7 +109,7 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
97
109
|
return (
|
|
98
110
|
<SecretField
|
|
99
111
|
field={field as any}
|
|
100
|
-
value={value as string
|
|
112
|
+
value={(value as string) ?? undefined}
|
|
101
113
|
onChange={onChange as (value: string) => void}
|
|
102
114
|
error={error}
|
|
103
115
|
disabled={disabled}
|
|
@@ -106,9 +118,9 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
106
118
|
case "number":
|
|
107
119
|
return (
|
|
108
120
|
<NumberField
|
|
109
|
-
field={field as
|
|
110
|
-
value={value}
|
|
111
|
-
onChange={onChange}
|
|
121
|
+
field={field as NumberFieldType}
|
|
122
|
+
value={(value as number) ?? undefined}
|
|
123
|
+
onChange={onChange as (value: number | undefined) => void}
|
|
112
124
|
disabled={disabled}
|
|
113
125
|
error={error}
|
|
114
126
|
/>
|
|
@@ -116,9 +128,9 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
116
128
|
case "checkbox":
|
|
117
129
|
return (
|
|
118
130
|
<CheckboxField
|
|
119
|
-
field={field as
|
|
120
|
-
value={value}
|
|
121
|
-
onChange={onChange}
|
|
131
|
+
field={field as CheckboxFieldType}
|
|
132
|
+
value={value as boolean}
|
|
133
|
+
onChange={onChange as (value: boolean) => void}
|
|
122
134
|
disabled={disabled}
|
|
123
135
|
error={error}
|
|
124
136
|
/>
|
|
@@ -126,9 +138,9 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
126
138
|
case "select":
|
|
127
139
|
return (
|
|
128
140
|
<SelectField
|
|
129
|
-
field={field as
|
|
130
|
-
value={value}
|
|
131
|
-
onChange={onChangeKeystroke}
|
|
141
|
+
field={field as SelectFieldType}
|
|
142
|
+
value={(value as string | string[]) ?? undefined}
|
|
143
|
+
onChange={onChangeKeystroke as (value: string | string[] | undefined) => void}
|
|
132
144
|
error={error}
|
|
133
145
|
disabled={disabled}
|
|
134
146
|
formData={formData}
|
|
@@ -140,9 +152,9 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
140
152
|
case "date":
|
|
141
153
|
return (
|
|
142
154
|
<DateField
|
|
143
|
-
field={field as
|
|
144
|
-
value={value}
|
|
145
|
-
onChange={onChange}
|
|
155
|
+
field={field as DateFieldType}
|
|
156
|
+
value={(value as string) ?? undefined}
|
|
157
|
+
onChange={onChange as (value: string | undefined) => void}
|
|
146
158
|
disabled={disabled}
|
|
147
159
|
error={error}
|
|
148
160
|
/>
|
|
@@ -151,7 +163,7 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
151
163
|
return (
|
|
152
164
|
<RichTextField
|
|
153
165
|
field={field}
|
|
154
|
-
value={value}
|
|
166
|
+
value={value as Record<string, any> | null}
|
|
155
167
|
onChange={onChangeKeystroke}
|
|
156
168
|
disabled={disabled}
|
|
157
169
|
error={error}
|
|
@@ -160,9 +172,9 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
160
172
|
case "markdown":
|
|
161
173
|
return (
|
|
162
174
|
<MarkdownField
|
|
163
|
-
field={field as
|
|
164
|
-
value={value}
|
|
165
|
-
onChange={onChangeKeystroke}
|
|
175
|
+
field={field as MarkdownFieldType}
|
|
176
|
+
value={(value as string) ?? undefined}
|
|
177
|
+
onChange={onChangeKeystroke as (value: string) => void}
|
|
166
178
|
disabled={disabled}
|
|
167
179
|
error={error}
|
|
168
180
|
/>
|
|
@@ -170,9 +182,9 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
170
182
|
case "code":
|
|
171
183
|
return (
|
|
172
184
|
<CodeField
|
|
173
|
-
field={field as
|
|
174
|
-
value={value}
|
|
175
|
-
onChange={onChangeKeystroke}
|
|
185
|
+
field={field as CodeFieldType}
|
|
186
|
+
value={(value as string) ?? undefined}
|
|
187
|
+
onChange={onChangeKeystroke as (value: string) => void}
|
|
176
188
|
disabled={disabled}
|
|
177
189
|
error={error}
|
|
178
190
|
/>
|
|
@@ -182,9 +194,9 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
182
194
|
return (
|
|
183
195
|
<FieldLayout field={field} error={error}>
|
|
184
196
|
<UploadField
|
|
185
|
-
field={field as
|
|
186
|
-
value={value}
|
|
187
|
-
onChange={onChange}
|
|
197
|
+
field={field as UploadFieldType}
|
|
198
|
+
value={value as Record<string, unknown> | Record<string, unknown>[] | null}
|
|
199
|
+
onChange={onChange as (value: Record<string, unknown> | Record<string, unknown>[] | null) => void}
|
|
188
200
|
disabled={disabled}
|
|
189
201
|
/>
|
|
190
202
|
</FieldLayout>
|
|
@@ -192,7 +204,7 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
192
204
|
case "relationship":
|
|
193
205
|
return (
|
|
194
206
|
<RelationshipField
|
|
195
|
-
field={field as
|
|
207
|
+
field={field as unknown as { name: string; label?: string; relationTo: string | string[]; hasMany?: boolean; required?: boolean; admin?: { description?: string; readOnly?: boolean; placeholder?: string; } }}
|
|
196
208
|
value={value}
|
|
197
209
|
onChange={onChange}
|
|
198
210
|
disabled={disabled}
|
|
@@ -203,8 +215,8 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
203
215
|
return (
|
|
204
216
|
<FieldLayout field={field} error={error}>
|
|
205
217
|
<ListField
|
|
206
|
-
items={Array.isArray(value) ? value : []}
|
|
207
|
-
onChange={onChange}
|
|
218
|
+
items={Array.isArray(value) ? (value as string[]) : []}
|
|
219
|
+
onChange={onChange as (items: string[]) => void}
|
|
208
220
|
compact
|
|
209
221
|
/>
|
|
210
222
|
</FieldLayout>
|
|
@@ -213,8 +225,8 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
213
225
|
return (
|
|
214
226
|
<ArrayLayout
|
|
215
227
|
field={field}
|
|
216
|
-
value={Array.isArray(value) ? value : []}
|
|
217
|
-
onChange={onChange}
|
|
228
|
+
value={Array.isArray(value) ? (value as unknown[]) : []}
|
|
229
|
+
onChange={onChange as (value: unknown[]) => void}
|
|
218
230
|
disabled={disabled}
|
|
219
231
|
renderField={(nestedField, parentData, onNestedChange) => {
|
|
220
232
|
const nestedValue = parentData[nestedField.name];
|
|
@@ -239,8 +251,8 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
239
251
|
case "blocks":
|
|
240
252
|
return (
|
|
241
253
|
<BlocksField
|
|
242
|
-
field={field as
|
|
243
|
-
value={value}
|
|
254
|
+
field={field as BlocksFieldType}
|
|
255
|
+
value={value as unknown[]}
|
|
244
256
|
onChange={onChangeKeystroke}
|
|
245
257
|
disabled={disabled}
|
|
246
258
|
error={error}
|
|
@@ -251,7 +263,7 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
251
263
|
<GroupLayout
|
|
252
264
|
field={field}
|
|
253
265
|
value={value as Record<string, unknown> | null}
|
|
254
|
-
onChange={onChange}
|
|
266
|
+
onChange={onChange as (value: Record<string, unknown>) => void}
|
|
255
267
|
renderField={(nestedField, parentData, onNestedChange) => {
|
|
256
268
|
const nestedValue = parentData[nestedField.name];
|
|
257
269
|
return (
|
|
@@ -280,7 +292,7 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
280
292
|
<div className="flex items-center gap-3">
|
|
281
293
|
<input
|
|
282
294
|
type="color"
|
|
283
|
-
value={value || "#000000"}
|
|
295
|
+
value={(value as string) || "#000000"}
|
|
284
296
|
onChange={(e) => onChange(e.target.value)}
|
|
285
297
|
disabled={disabled}
|
|
286
298
|
className="h-10 w-14 p-1 cursor-pointer bg-[var(--kyro-input-bg)] border border-[var(--kyro-input-border)] rounded-lg"
|
|
@@ -288,7 +300,7 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
288
300
|
<input
|
|
289
301
|
type="text"
|
|
290
302
|
className="kyro-form-input font-mono "
|
|
291
|
-
value={value || ""}
|
|
303
|
+
value={(value as string) || ""}
|
|
292
304
|
onChange={(e) => onChange(e.target.value)}
|
|
293
305
|
disabled={disabled}
|
|
294
306
|
placeholder="#000000"
|
|
@@ -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 = () => {
|
|
@@ -259,9 +269,10 @@ export function ListView({
|
|
|
259
269
|
const params = new URLSearchParams({
|
|
260
270
|
page: page.toString(),
|
|
261
271
|
limit: limit.toString(),
|
|
272
|
+
depth: "1",
|
|
262
273
|
});
|
|
263
274
|
|
|
264
|
-
if (
|
|
275
|
+
if (debouncedSearch) params.append("search", debouncedSearch);
|
|
265
276
|
if (sort) params.append("sort", sort.field);
|
|
266
277
|
if (sort) params.append("order", sort.direction);
|
|
267
278
|
if (filters.length > 0) {
|
|
@@ -279,7 +290,7 @@ export function ListView({
|
|
|
279
290
|
} finally {
|
|
280
291
|
setLoading(false);
|
|
281
292
|
}
|
|
282
|
-
}, [collectionSlug, page, limit,
|
|
293
|
+
}, [collectionSlug, page, limit, debouncedSearch, sort, filters]);
|
|
283
294
|
|
|
284
295
|
// Initial fetch only if not provided with initialDocs or if empty
|
|
285
296
|
useEffect(() => {
|
|
@@ -296,7 +307,7 @@ export function ListView({
|
|
|
296
307
|
return;
|
|
297
308
|
}
|
|
298
309
|
fetchDocs();
|
|
299
|
-
}, [page, limit,
|
|
310
|
+
}, [page, limit, debouncedSearch, sort, filters]);
|
|
300
311
|
|
|
301
312
|
const handleSelectAll = () => {
|
|
302
313
|
if (selectedIds.size === docs.length) {
|
|
@@ -764,17 +775,21 @@ function formatCellValue(value: any, type?: string, t?: any): string {
|
|
|
764
775
|
if (Array.isArray(value)) {
|
|
765
776
|
return value.map(item => {
|
|
766
777
|
if (item && typeof item === "object") {
|
|
767
|
-
|
|
778
|
+
const target = ('value' in item && typeof item.value === 'object' && item.value !== null) ? item.value : item;
|
|
779
|
+
const fallbackName = (target.firstName || target.lastName) ? `${target.firstName || ''} ${target.lastName || ''}`.trim() : undefined;
|
|
780
|
+
return target.fullName || fallbackName || target.title || target.name || target.label || target.email || target.filename || target.slug || target.id || JSON.stringify(target).slice(0, 30);
|
|
768
781
|
}
|
|
769
782
|
return String(item ?? "").slice(0, 30);
|
|
770
783
|
}).filter(Boolean).join(", ");
|
|
771
784
|
}
|
|
772
785
|
|
|
773
786
|
if (typeof value === "object") {
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
787
|
+
const target = ('value' in value && typeof value.value === 'object' && value.value !== null) ? value.value : value;
|
|
788
|
+
const fallbackName = (target.firstName || target.lastName) ? `${target.firstName || ''} ${target.lastName || ''}`.trim() : undefined;
|
|
789
|
+
const label = target.fullName || fallbackName || target.title || target.name || target.label || target.email || target.filename || target.slug;
|
|
790
|
+
if (label) return String(label);
|
|
791
|
+
if (target.id) return String(target.id);
|
|
792
|
+
return JSON.stringify(target).slice(0, 50);
|
|
778
793
|
}
|
|
779
794
|
return String(value).slice(0, 60);
|
|
780
795
|
}
|
|
@@ -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
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { pluginViews } from "virtual:kyro-plugins";
|
|
2
|
+
import React, { useState, useEffect, Suspense } from "react";
|
|
2
3
|
import {
|
|
3
4
|
Blocks,
|
|
4
5
|
Settings,
|
|
@@ -15,6 +16,8 @@ import { Modal, ModalContent, ModalActions } from "./ui/Modal";
|
|
|
15
16
|
import { PageHeader } from "./ui/PageHeader";
|
|
16
17
|
import { Badge } from "./ui/Badge";
|
|
17
18
|
import { useTranslation } from "react-i18next";
|
|
19
|
+
import { navigate } from "astro:transitions/client";
|
|
20
|
+
|
|
18
21
|
|
|
19
22
|
interface Plugin {
|
|
20
23
|
id: string;
|
|
@@ -32,13 +35,15 @@ interface ToggleError {
|
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
export function PluginsManager() {
|
|
35
|
-
|
|
38
|
+
const { t } = useTranslation();
|
|
36
39
|
const [plugins, setPlugins] = useState<Plugin[]>([]);
|
|
37
40
|
const [loading, setLoading] = useState(true);
|
|
38
41
|
const [searchQuery, setSearchQuery] = useState("");
|
|
39
42
|
const [showConfigModal, setShowConfigModal] = useState<string | null>(null);
|
|
40
43
|
const [error, setError] = useState<string | null>(null);
|
|
41
44
|
|
|
45
|
+
const PluginSettingsView = showConfigModal ? pluginViews[showConfigModal] : null;
|
|
46
|
+
|
|
42
47
|
const fetchPlugins = async () => {
|
|
43
48
|
try {
|
|
44
49
|
setLoading(true);
|
|
@@ -93,7 +98,7 @@ export function PluginsManager() {
|
|
|
93
98
|
actions={[
|
|
94
99
|
{
|
|
95
100
|
label: "Marketplace",
|
|
96
|
-
onClick: () => (
|
|
101
|
+
onClick: () => (navigate("/admin/marketplace")),
|
|
97
102
|
icon: Plus,
|
|
98
103
|
},
|
|
99
104
|
]}
|
|
@@ -120,8 +125,8 @@ export function PluginsManager() {
|
|
|
120
125
|
<div>
|
|
121
126
|
<h4 className="text-sm font-bold text-blue-500 mb-1">Developer-First Approach</h4>
|
|
122
127
|
<p className="text-xs text-blue-500/80 leading-relaxed">
|
|
123
|
-
Plugins inject core structural components like schemas, collections, and APIs.
|
|
124
|
-
To ensure version-controlled integrity, plugins are entirely managed via code in your <code className="bg-blue-500/20 px-1.5 py-0.5 rounded-md text-[10px] font-mono">kyro.config.ts</code> file.
|
|
128
|
+
Plugins inject core structural components like schemas, collections, and APIs.
|
|
129
|
+
To ensure version-controlled integrity, plugins are entirely managed via code in your <code className="bg-blue-500/20 px-1.5 py-0.5 rounded-md text-[10px] font-mono">kyro.config.ts</code> file.
|
|
125
130
|
This page serves as a read-only directory of the currently installed ecosystem.
|
|
126
131
|
</p>
|
|
127
132
|
</div>
|
|
@@ -222,14 +227,16 @@ export function PluginsManager() {
|
|
|
222
227
|
</div>
|
|
223
228
|
|
|
224
229
|
<div className="flex items-center gap-1.5">
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
230
|
+
{pluginViews[plugin.id] && (
|
|
231
|
+
<button
|
|
232
|
+
type="button"
|
|
233
|
+
onClick={() => setShowConfigModal(plugin.id)}
|
|
234
|
+
className="p-2 bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-lg hover:border-[var(--kyro-primary)] transition-all disabled:opacity-30 shadow-sm"
|
|
235
|
+
disabled={!plugin.enabled}
|
|
236
|
+
>
|
|
237
|
+
<Settings className="w-3.5 h-3.5" />
|
|
238
|
+
</button>
|
|
239
|
+
)}
|
|
233
240
|
</div>
|
|
234
241
|
</div>
|
|
235
242
|
</div>
|
|
@@ -253,15 +260,26 @@ export function PluginsManager() {
|
|
|
253
260
|
size="lg"
|
|
254
261
|
>
|
|
255
262
|
<ModalContent>
|
|
256
|
-
|
|
257
|
-
<
|
|
258
|
-
<
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
+
{PluginSettingsView ? (
|
|
264
|
+
<Suspense fallback={
|
|
265
|
+
<div className="p-12 flex flex-col items-center justify-center space-y-4">
|
|
266
|
+
<RefreshCw className="w-8 h-8 text-[var(--kyro-primary)] animate-spin opacity-50" />
|
|
267
|
+
<p className="text-sm text-[var(--kyro-text-secondary)]">Loading plugin settings...</p>
|
|
268
|
+
</div>
|
|
269
|
+
}>
|
|
270
|
+
<PluginSettingsView />
|
|
271
|
+
</Suspense>
|
|
272
|
+
) : (
|
|
273
|
+
<div className="p-12 text-center space-y-4">
|
|
274
|
+
<div className="w-20 h-20 mx-auto bg-[var(--kyro-surface-accent)] rounded-3xl flex items-center justify-center border border-[var(--kyro-border)] shadow-xl">
|
|
275
|
+
<Settings className="w-10 h-10 text-[var(--kyro-primary)] opacity-40 animate-spin-slow" />
|
|
276
|
+
</div>
|
|
277
|
+
<div>
|
|
278
|
+
<h4 className="text-xl font-bold mb-1">Custom Configuration</h4>
|
|
279
|
+
<p className="text-sm text-[var(--kyro-text-secondary)] opacity-50 italic">Settings for this plugin are managed by its own custom views and components.</p>
|
|
280
|
+
</div>
|
|
263
281
|
</div>
|
|
264
|
-
|
|
282
|
+
)}
|
|
265
283
|
</ModalContent>
|
|
266
284
|
<ModalActions>
|
|
267
285
|
<button
|
|
@@ -10,6 +10,7 @@ interface NavItem {
|
|
|
10
10
|
href: string;
|
|
11
11
|
label: string;
|
|
12
12
|
icon: string;
|
|
13
|
+
order?: number;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
const siteSettings = await getSiteSettings({ request: Astro.request });
|
|
@@ -33,7 +34,6 @@ const { title } = Astro.props;
|
|
|
33
34
|
const currentPath = Astro.url.pathname;
|
|
34
35
|
|
|
35
36
|
const groupedCollections = new Map<string, NavItem[]>();
|
|
36
|
-
const defaultCollections: NavItem[] = [];
|
|
37
37
|
|
|
38
38
|
nonAuthCollections
|
|
39
39
|
.filter((col) => col.slug !== "media") //removed here cos it's been added manually in the navSections.
|
|
@@ -42,22 +42,19 @@ nonAuthCollections
|
|
|
42
42
|
href: `${adminPath}/${col.slug}`,
|
|
43
43
|
label: col.label || col.slug,
|
|
44
44
|
icon: (col.admin?.icon as string) || "collection",
|
|
45
|
+
order: typeof col.admin?.order === 'number' ? col.admin.order : 999,
|
|
45
46
|
};
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
groupedCollections.set(groupName, []);
|
|
51
|
-
}
|
|
52
|
-
groupedCollections.get(groupName)!.push(item);
|
|
53
|
-
} else {
|
|
54
|
-
defaultCollections.push(item);
|
|
48
|
+
const groupName = (col.admin?.group as string) || "Content";
|
|
49
|
+
if (!groupedCollections.has(groupName)) {
|
|
50
|
+
groupedCollections.set(groupName, []);
|
|
55
51
|
}
|
|
52
|
+
groupedCollections.get(groupName)!.push(item);
|
|
56
53
|
});
|
|
57
54
|
|
|
58
55
|
const dynamicSections = Array.from(groupedCollections.entries()).map(([group, items]) => ({
|
|
59
56
|
label: group,
|
|
60
|
-
items: items,
|
|
57
|
+
items: items.sort((a, b) => (a.order || 999) - (b.order || 999)),
|
|
61
58
|
}));
|
|
62
59
|
|
|
63
60
|
const navSections: { label: string; items: NavItem[] }[] = [
|
|
@@ -73,10 +70,6 @@ const navSections: { label: string; items: NavItem[] }[] = [
|
|
|
73
70
|
],
|
|
74
71
|
},
|
|
75
72
|
...dynamicSections,
|
|
76
|
-
...(defaultCollections.length > 0 ? [{
|
|
77
|
-
label: "Content",
|
|
78
|
-
items: defaultCollections,
|
|
79
|
-
}] : []),
|
|
80
73
|
{
|
|
81
74
|
label: "Settings",
|
|
82
75
|
items: [
|
|
@@ -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
|