@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.
Files changed (77) hide show
  1. package/dist/index.cjs +32 -110
  2. package/dist/index.css +1 -1
  3. package/dist/index.d.cts +5 -5
  4. package/dist/index.d.ts +5 -5
  5. package/dist/index.js +32 -110
  6. package/package.json +11 -5
  7. package/src/components/AuthBridge.tsx +2 -2
  8. package/src/components/AutoForm.tsx +49 -57
  9. package/src/components/BrandingHub.tsx +1 -1
  10. package/src/components/DashboardMetrics.tsx +519 -290
  11. package/src/components/DetailView.tsx +5 -3
  12. package/src/components/FieldRenderer.tsx +59 -47
  13. package/src/components/ListView.tsx +25 -10
  14. package/src/components/MediaGallery.tsx +1 -1
  15. package/src/components/PluginsManager.tsx +39 -21
  16. package/src/components/Sidebar.astro +7 -14
  17. package/src/components/UserMenu.tsx +10 -9
  18. package/src/components/autoform/AutoFormApiView.tsx +1 -1
  19. package/src/components/autoform/AutoFormHeader.tsx +1 -1
  20. package/src/components/autoform/ErrorBoundary.tsx +1 -1
  21. package/src/components/blocks/AccordionBlock.tsx +8 -4
  22. package/src/components/blocks/ArrayBlock.tsx +4 -3
  23. package/src/components/blocks/BlockEditModal.tsx +4 -3
  24. package/src/components/blocks/CardBlock.tsx +10 -2
  25. package/src/components/blocks/ChildBlocksTree.tsx +19 -18
  26. package/src/components/blocks/CodeBlock.tsx +7 -2
  27. package/src/components/blocks/FileBlock.tsx +6 -2
  28. package/src/components/blocks/GenericBlock.tsx +1 -1
  29. package/src/components/blocks/HeadingBlock.tsx +6 -2
  30. package/src/components/blocks/HeadingSubheadingBlock.tsx +7 -2
  31. package/src/components/blocks/HeroBlock.tsx +12 -3
  32. package/src/components/blocks/ImageBlock.tsx +8 -2
  33. package/src/components/blocks/ListBlock.tsx +6 -2
  34. package/src/components/blocks/ParagraphBlock.tsx +2 -2
  35. package/src/components/blocks/RelationshipBlock.tsx +10 -2
  36. package/src/components/blocks/VideoBlock.tsx +7 -2
  37. package/src/components/fields/AccordionField.tsx +1 -1
  38. package/src/components/fields/ArrayLayout.tsx +55 -58
  39. package/src/components/fields/BlocksField.tsx +1 -1
  40. package/src/components/fields/ChildrenField.tsx +3 -2
  41. package/src/components/fields/CodeField.tsx +3 -2
  42. package/src/components/fields/ColumnsField.tsx +3 -2
  43. package/src/components/fields/DateField.tsx +2 -2
  44. package/src/components/fields/FieldLayout.tsx +3 -3
  45. package/src/components/fields/GroupLayout.tsx +2 -2
  46. package/src/components/fields/IconField.tsx +1 -1
  47. package/src/components/fields/MarkdownField.tsx +2 -2
  48. package/src/components/fields/NumberField.tsx +4 -4
  49. package/src/components/fields/RelationshipField.tsx +4 -1
  50. package/src/components/fields/RichTextField.tsx +200 -5
  51. package/src/components/fields/extensions/blockComponents.tsx +1 -1
  52. package/src/components/fields/extensions/blocksStore.ts +15 -12
  53. package/src/components/ui/Button.tsx +5 -2
  54. package/src/components/ui/Pagination.tsx +1 -1
  55. package/src/components/users/UserDetail.tsx +7 -7
  56. package/src/components/users/UserForm.tsx +3 -2
  57. package/src/components/users/UsersList.tsx +5 -4
  58. package/src/env.d.ts +4 -0
  59. package/src/fields/index.ts +1 -1
  60. package/src/hooks/useAutoFormState.ts +7 -6
  61. package/src/hooks/useQueue.ts +3 -2
  62. package/src/index.ts +0 -1
  63. package/src/integration.ts +50 -12
  64. package/src/kyro-cms.d.ts +5 -0
  65. package/src/lib/api.ts +1 -0
  66. package/src/lib/autoform-store.ts +4 -3
  67. package/src/lib/config.ts +6 -19
  68. package/src/lib/core-types.ts +78 -0
  69. package/src/lib/normalize-upload-fields.ts +17 -4
  70. package/src/pages/[collection]/index.astro +1 -1
  71. package/src/pages/users/index.astro +1 -1
  72. package/src/plugins/seo-admin.tsx +155 -0
  73. package/src/styles/main.css +2 -0
  74. package/src/vite-env.d.ts +10 -1
  75. package/src/components/fix_imports.cjs +0 -23
  76. package/src/components/fix_imports2.cjs +0 -19
  77. 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
- window.location.href = `${adminPath}/${slug}/${response.data.id}`;
236
+ navigate(`${adminPath}/${slug}/${response.data.id}`);
235
237
  } else {
236
- window.location.href = `${adminPath}/${slug}`;
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
- window.location.href = `${adminPath}/${slug}`;
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 { Field } from "@kyro-cms/core/client";
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: any;
26
- onChange: (value: any) => void;
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 any}
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 any}
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 any}
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 | null | undefined}
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 any}
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 any}
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 any}
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 any}
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 any}
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 any}
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 any}
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 any}
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 any}
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
- window.location.href = href;
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
- window.location.href = href;
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 (search) params.append("search", search);
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, search, sort, filters]);
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, search, sort, filters]);
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
- return item.title || item.name || item.email || item.filename || item.url || JSON.stringify(item).slice(0, 30);
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
- if (value.title) return value.title;
775
- if (value.name) return value.name;
776
- if (value.email) return value.email;
777
- return JSON.stringify(value).slice(0, 50);
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.location.reload();
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 React, { useState, useEffect } from "react";
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
- const { t } = useTranslation();
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: () => (window.location.href = "/admin/marketplace"),
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
- <button
226
- type="button"
227
- onClick={() => setShowConfigModal(plugin.id)}
228
- 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"
229
- disabled={!plugin.enabled}
230
- >
231
- <Settings className="w-3.5 h-3.5" />
232
- </button>
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
- <div className="p-12 text-center space-y-4">
257
- <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">
258
- <Settings className="w-10 h-10 text-[var(--kyro-primary)] opacity-40 animate-spin-slow" />
259
- </div>
260
- <div>
261
- <h4 className="text-xl font-bold mb-1">Advanced Configuration</h4>
262
- <p className="text-sm text-[var(--kyro-text-secondary)] opacity-50 italic">Settings for this plugin will be synthesized dynamically from its manifest.</p>
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
- </div>
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
- if (col.admin?.group) {
48
- const groupName = col.admin.group as string;
49
- if (!groupedCollections.has(groupName)) {
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
- window.location.href = `${adminPath}/users/${id}`;
68
+ navigate(`${adminPath}/users/${id}`);
68
69
  } else {
69
- window.location.href = `${adminPath}/users`;
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={() => window.location.href = `${adminPath}/roles`}
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={() => (window.location.href = `${adminPath}/keys`)}
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={() => (window.location.href = `${adminPath}/webhooks`)}
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={() => (window.location.href = `${adminPath}/sessions`)}
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={() => (window.location.href = `${adminPath}/audit`)}
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={() => (window.location.href = `${adminPath}/rest-playground`)}
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={() => (window.location.href = `${adminPath}/graphql`)}
128
+ onClick={() => (navigate(`${adminPath}/graphql`))}
128
129
  >
129
130
  {t("userMenu.graphqlPlayground", { defaultValue: "GraphQL Playground" })}
130
131
  </DropdownItem>
@@ -44,7 +44,7 @@ export function AutoFormApiView({
44
44
  </div>
45
45
  </div>
46
46
 
47
- {formData.id && (
47
+ {Boolean(formData.id) && (
48
48
  <div>
49
49
  <label className="text-[10px] font-bold tracking-[0.1em] text-[var(--kyro-text-secondary)] opacity-50 block mb-2">
50
50
  Document ID
@@ -179,7 +179,7 @@ export function AutoFormHeader({
179
179
  <span className="opacity-30">|</span>
180
180
  <button
181
181
  type="button"
182
- onClick={() => window.location.reload()}
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
@@ -45,7 +45,7 @@ export class ErrorBoundary extends Component<Props, State> {
45
45
  };
46
46
 
47
47
  handleReload = () => {
48
- window.location.reload();
48
+ window.dispatchEvent(new Event('kyro:soft-reload'));
49
49
  };
50
50
 
51
51
  render() {