@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.
Files changed (100) hide show
  1. package/dist/index.cjs +11960 -11006
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +67 -65
  4. package/dist/index.css.map +1 -1
  5. package/dist/index.d.cts +563 -0
  6. package/dist/index.d.ts +7 -7
  7. package/dist/index.js +12183 -11238
  8. package/dist/index.js.map +1 -1
  9. package/package.json +15 -11
  10. package/src/components/ActionBar.tsx +27 -14
  11. package/src/components/Admin.tsx +1 -1
  12. package/src/components/ApiKeysManager.tsx +5 -5
  13. package/src/components/AutoForm.tsx +585 -369
  14. package/src/components/BrandingHub.tsx +7 -4
  15. package/src/components/CreateView.tsx +2 -0
  16. package/src/components/DetailView.tsx +71 -56
  17. package/src/components/DeveloperCenter.tsx +8 -6
  18. package/src/components/FieldRenderer.tsx +94 -19
  19. package/src/components/ListView.tsx +33 -20
  20. package/src/components/MediaGallery.tsx +219 -194
  21. package/src/components/PluginsManager.tsx +197 -70
  22. package/src/components/RestPlayground.tsx +7 -7
  23. package/src/components/SessionsManager.tsx +1 -1
  24. package/src/components/SettingsPage.tsx +22 -0
  25. package/src/components/Sidebar.astro +13 -41
  26. package/src/components/UserManagement.tsx +153 -15
  27. package/src/components/UserMenu.tsx +30 -4
  28. package/src/components/VersionHistoryPanel.tsx +112 -119
  29. package/src/components/WebhookManager.tsx +6 -4
  30. package/src/components/blocks/ArrayBlock.tsx +6 -23
  31. package/src/components/blocks/BlockEditModal.tsx +82 -309
  32. package/src/components/blocks/CardBlock.tsx +35 -0
  33. package/src/components/blocks/ChildBlocksTree.tsx +57 -31
  34. package/src/components/blocks/GenericBlock.tsx +44 -0
  35. package/src/components/blocks/HeadingSubheadingBlock.tsx +32 -0
  36. package/src/components/blocks/HeroBlock.tsx +5 -14
  37. package/src/components/blocks/RichTextBlock.tsx +5 -5
  38. package/src/components/blocks/index.ts +5 -3
  39. package/src/components/fields/AccordionField.tsx +2 -2
  40. package/src/components/fields/ArrayField.tsx +1 -1
  41. package/src/components/fields/ArrayLayout.tsx +120 -29
  42. package/src/components/fields/BlocksField.tsx +430 -50
  43. package/src/components/fields/CardField.tsx +73 -0
  44. package/src/components/fields/CheckboxField.tsx +7 -3
  45. package/src/components/fields/DateField.tsx +4 -1
  46. package/src/components/fields/GroupLayout.tsx +2 -2
  47. package/src/components/fields/HeadingSubheadingField.tsx +43 -0
  48. package/src/components/fields/ListField.tsx +2 -2
  49. package/src/components/fields/NumberField.tsx +4 -1
  50. package/src/components/fields/RelationshipField.tsx +153 -87
  51. package/src/components/fields/RichTextField.tsx +781 -0
  52. package/src/components/fields/SecretField.tsx +102 -0
  53. package/src/components/fields/SelectField.tsx +19 -6
  54. package/src/components/fields/TabsLayout.tsx +19 -9
  55. package/src/components/fields/TextField.tsx +4 -1
  56. package/src/components/fields/UploadField.tsx +122 -56
  57. package/src/components/fields/extensions/blockComponents.tsx +103 -174
  58. package/src/components/fields/extensions/blocksStore.ts +8 -1
  59. package/src/components/fields/index.ts +4 -2
  60. package/src/components/ui/PageHeader.tsx +5 -5
  61. package/src/components/ui/SlidePanel.tsx +8 -3
  62. package/src/components/ui/icons.tsx +109 -109
  63. package/src/components/users/UserDetail.tsx +79 -16
  64. package/src/hooks/useAutoFormState.ts +125 -62
  65. package/src/integration.ts +148 -46
  66. package/src/kyro-cms.d.ts +7 -2
  67. package/src/layouts/AuthLayout.astro +14 -2
  68. package/src/lib/autoform-store.ts +85 -52
  69. package/src/lib/change-source.ts +9 -0
  70. package/src/lib/config.ts +104 -8
  71. package/src/lib/globals.ts +44 -9
  72. package/src/lib/normalize-upload-fields.ts +41 -0
  73. package/src/lib/paths.ts +2 -2
  74. package/src/lib/resolve-field-value.ts +110 -0
  75. package/src/lib/shim/use-sync-external-store-with-selector.js +30 -0
  76. package/src/lib/shim/use-sync-external-store.js +1 -0
  77. package/src/lib/stores/index.ts +1 -0
  78. package/src/lib/useResourceManager.ts +4 -4
  79. package/src/lib/vite-shim-plugin.ts +100 -0
  80. package/src/pages/[collection]/[id].astro +1 -1
  81. package/src/pages/preview/[collection]/[id].astro +4 -4
  82. package/src/pages/settings/[slug].astro +2 -2
  83. package/src/styles/main.css +60 -54
  84. package/README.md +0 -46
  85. package/dist/EditorClient-Q23UXR37.cjs +0 -468
  86. package/dist/EditorClient-Q23UXR37.cjs.map +0 -1
  87. package/dist/EditorClient-T5PASFNR.js +0 -466
  88. package/dist/EditorClient-T5PASFNR.js.map +0 -1
  89. package/dist/chunk-3BGDYKTD.cjs +0 -348
  90. package/dist/chunk-3BGDYKTD.cjs.map +0 -1
  91. package/dist/chunk-EEFXLQVT.js +0 -3
  92. package/dist/chunk-EEFXLQVT.js.map +0 -1
  93. package/src/components/blocks/ButtonBlock.tsx +0 -64
  94. package/src/components/blocks/ColumnsBlock.tsx +0 -55
  95. package/src/components/blocks/DividerBlock.tsx +0 -43
  96. package/src/components/blocks/LinkBlock.tsx +0 -65
  97. package/src/components/blocks/VStackBlock.tsx +0 -29
  98. package/src/components/fields/EditorClient.tsx +0 -535
  99. package/src/components/fields/PortableTextField.tsx +0 -155
  100. package/src/components/fields/PortableTextRenderer.tsx +0 -68
@@ -1,5 +1,6 @@
1
1
  import React, { useState, useEffect } from "react";
2
2
  import { apiGet, apiPatch } from "../lib/api";
3
+ import { toast } from "../lib/stores";
3
4
  import {
4
5
  Palette,
5
6
  Tag,
@@ -25,7 +26,7 @@ export function BrandingHub() {
25
26
  useEffect(() => {
26
27
  const fetchBranding = async () => {
27
28
  try {
28
- const result = await apiGet("/api/globals/site");
29
+ const result = await apiGet("/api/globals/site-settings");
29
30
  const data = result.data || result;
30
31
  if (data && Object.keys(data).length > 0) {
31
32
  if (data.siteName) setSiteName(data.siteName);
@@ -44,19 +45,21 @@ export function BrandingHub() {
44
45
  const handleSave = async () => {
45
46
  setSaving(true);
46
47
  try {
47
- await apiPatch("/api/globals/site", {
48
+ await apiPatch("/api/globals/site-settings", {
48
49
  siteName,
49
50
  adminTitle,
50
51
  primaryColor,
51
52
  dashboardGreeting,
52
53
  });
53
54
  setSaved(true);
54
- setTimeout(() => setSaved(false), 3000);
55
+ toast.success("Branding updated");
55
56
  document.documentElement.style.setProperty(
56
57
  "--kyro-primary",
57
58
  primaryColor,
58
59
  );
60
+ setTimeout(() => window.location.reload(), 800);
59
61
  } catch (e) {
62
+ toast.error("Failed to save branding");
60
63
  console.error(e);
61
64
  } finally {
62
65
  setSaving(false);
@@ -92,7 +95,7 @@ export function BrandingHub() {
92
95
  disabled={saving}
93
96
  className={`flex items-center gap-2 px-8 py-3 rounded-2xl font-bold text-sm shadow-xl transition-all active:scale-95 ${saved
94
97
  ? "bg-green-500 text-white"
95
- : "bg-[var(--kyro-primary)] text-white hover:shadow-[var(--kyro-primary)]"
98
+ : "kyro-btn-primary hover:shadow-[var(--kyro-primary)]"
96
99
  }`}
97
100
  >
98
101
  {saving ? (
@@ -5,6 +5,7 @@ import { AutoForm } from "./AutoForm";
5
5
  import { Spinner } from "./ui/Spinner";
6
6
  import { PageHeader } from "./ui/PageHeader";
7
7
  import { adminPath } from "../lib/paths";
8
+ import { toast } from "../lib/stores";
8
9
 
9
10
 
10
11
  interface CreateViewProps {
@@ -34,6 +35,7 @@ export function CreateView({
34
35
  try {
35
36
  setSaving(true);
36
37
  await apiPost(`/api/${collection.slug}`, data);
38
+ toast.success(`${collection.singularLabel || collection.label || "Document"} created`);
37
39
  onSuccess();
38
40
  } catch (err) {
39
41
  onError(err instanceof Error ? err.message : "Failed to create");
@@ -14,6 +14,7 @@ import { useUIStore } from "../lib/stores";
14
14
  import { PageHeader } from "./ui/PageHeader";
15
15
  import { Badge } from "./ui/Badge";
16
16
  import { adminPath } from "../lib/paths";
17
+ import { resolveFieldValue } from "../lib/resolve-field-value";
17
18
 
18
19
 
19
20
  interface DetailViewProps {
@@ -94,10 +95,10 @@ export function DetailView({
94
95
  const docData = result.data || {};
95
96
  setData(docData);
96
97
  setOriginalData(docData);
97
- setStatus((result.status || "draft") as DocumentStatus);
98
- setCreatedAt(result.createdAt || null);
99
- setUpdatedAt(result.updatedAt || null);
100
- setPublishedAt(result.publishedAt || null);
98
+ setStatus(((docData as any)?.publishStatus || result.status || "draft") as DocumentStatus);
99
+ setCreatedAt(result.createdAt || (docData.createdAt as string) || null);
100
+ setUpdatedAt(result.updatedAt || (docData.updatedAt as string) || null);
101
+ setPublishedAt(result.publishedAt || (docData.publishedAt as string) || null);
101
102
  } catch {
102
103
  onError("Failed to load document");
103
104
  } finally {
@@ -134,7 +135,7 @@ export function DetailView({
134
135
  ? `/api/globals/${slug}`
135
136
  : `/api/${slug}/${documentId}`;
136
137
 
137
- const result = (await apiPatch(endpoint, data) as { data?: Record<string, unknown> });
138
+ const result = (await apiPatch(endpoint, data, { autoToast: false }) as { data?: Record<string, unknown> });
138
139
  const savedData = (result && (result.data || result)) || data;
139
140
 
140
141
  if (!isAutosave) {
@@ -143,6 +144,7 @@ export function DetailView({
143
144
  }
144
145
 
145
146
  setData(savedData);
147
+ setStatus((savedData as any)?.publishStatus || status);
146
148
  setSaveStatus("saved");
147
149
  setUpdatedAt(new Date().toISOString());
148
150
 
@@ -151,7 +153,8 @@ export function DetailView({
151
153
  setTimeout(() => setJustSaved(false), 3000);
152
154
 
153
155
  if (!isAutosave) {
154
- addToast?.("success", "Saved successfully");
156
+ const isDraft = status === "draft" || (savedData as any)?.publishStatus === "draft";
157
+ addToast?.(isDraft ? "warning" : "success", isDraft ? "Draft saved" : "Updated");
155
158
  }
156
159
 
157
160
  setTimeout(() => {
@@ -173,7 +176,7 @@ export function DetailView({
173
176
  const handlePublish = async () => {
174
177
  try {
175
178
  setSaving(true);
176
- await apiPost(`/api/${slug}/${documentId}/publish`);
179
+ await apiPost(`/api/${slug}/${documentId}/publish`, undefined, { autoToast: false });
177
180
  setStatus("published");
178
181
  setPublishedAt(new Date().toISOString());
179
182
  addToast?.("success", "Published successfully");
@@ -188,10 +191,12 @@ export function DetailView({
188
191
  const handleUnpublish = async () => {
189
192
  try {
190
193
  setSaving(true);
191
- await apiPost(`/api/${slug}/${documentId}/unpublish`);
194
+ await apiPost(`/api/${slug}/${documentId}/unpublish`, undefined, { autoToast: false });
192
195
  setStatus("draft");
196
+ addToast?.("warning", "Document unpublished");
193
197
  } catch {
194
198
  onError("Failed to unpublish");
199
+ addToast?.("error", "Failed to unpublish");
195
200
  } finally {
196
201
  setSaving(false);
197
202
  }
@@ -199,14 +204,15 @@ export function DetailView({
199
204
 
200
205
  const handleDuplicate = async () => {
201
206
  try {
202
- console.log(`[Duplicate] Calling /api/${slug}/${documentId}/duplicate`);
203
- const result = await apiPost(`/api/${slug}/${documentId}/duplicate`);
204
- console.log("[Duplicate] Success:", result);
205
- onError("Document duplicated successfully");
206
- } catch (err: unknown) {
207
- console.error("[Duplicate] Error:", err);
208
- const message = err instanceof Error ? err.message : "Failed to duplicate document";
209
- onError(message);
207
+ setSaving(true);
208
+ await apiPost(`/api/${slug}/${documentId}/duplicate`, undefined, { autoToast: false });
209
+ addToast?.("success", "Document duplicated");
210
+ } catch (err: unknown) {
211
+ const message = err instanceof Error ? err.message : "Failed to duplicate document";
212
+ onError(message);
213
+ addToast?.("error", message);
214
+ } finally {
215
+ setSaving(false);
210
216
  }
211
217
  };
212
218
 
@@ -218,12 +224,12 @@ export function DetailView({
218
224
  onConfirm: async () => {
219
225
  try {
220
226
  setDeleting(true);
221
- await apiDelete(`/api/${slug}/${documentId}`);
227
+ await apiDelete(`/api/${slug}/${documentId}`, { autoToast: false });
222
228
  onDelete?.();
223
- } catch (err: unknown) {
224
- console.error("[Delete] Error:", err);
225
- const message = err instanceof Error ? err.message : "Failed to delete document";
226
- alert({ title: "Error", message });
229
+ addToast?.("error", "Document deleted");
230
+ } catch (err: unknown) {
231
+ const message = err instanceof Error ? err.message : "Failed to delete document";
232
+ addToast?.("error", message);
227
233
  } finally {
228
234
  setDeleting(false);
229
235
  }
@@ -263,7 +269,7 @@ export function DetailView({
263
269
  { label: mode === "global" ? "Edit" : documentId ? "Edit" : "New" }
264
270
  ]}
265
271
  title={
266
- (mode === "global" ? label : (data[collection?.admin?.useAsTitle || "title"] as string || data.name as string || documentId || `New ${collection?.singularLabel || label}`))
272
+ (mode === "global" ? label : ((resolveFieldValue(collection?.fields as any, data, collection?.admin?.useAsTitle || "title") as string) || data.name as string || documentId || `New ${collection?.singularLabel || label}`))
267
273
  }
268
274
  metadata={[
269
275
  <Badge
@@ -341,41 +347,50 @@ export function DetailView({
341
347
  Delete
342
348
  </button>
343
349
  )}
344
- <button
345
- type="button"
346
- onClick={() => handleSave(false)}
347
- disabled={saving}
348
- className="kyro-btn kyro-btn-lg kyro-btn-primary shadow-xl flex items-center gap-2"
349
- >
350
- {saving ? (
351
- <svg
352
- className="w-4 h-4 animate-spin"
353
- viewBox="0 0 24 24"
354
- fill="none"
355
- stroke="currentColor"
356
- strokeWidth="2"
357
- >
358
- <path d="M21 12a9 9 0 1 1-6.219-8.56" />
350
+ {status === "published" && !hasChanges && !saving ? (
351
+ <span className="inline-flex items-center gap-2 px-6 py-2.5 rounded-lg text-xs font-bold bg-green-100 text-green-700 border border-green-200 cursor-not-allowed shadow-xl">
352
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
353
+ <path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z" />
359
354
  </svg>
360
- ) : (
361
- <svg
362
- className="w-4 h-4"
363
- viewBox="0 0 24 24"
364
- fill="none"
365
- stroke="currentColor"
366
- strokeWidth="2"
367
- >
368
- <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
369
- <polyline points="17 21 17 13 7 13 7 21" />
370
- <polyline points="7 3 7 8 15 8" />
371
- </svg>
372
- )}
373
- {saving
374
- ? "Saving..."
375
- : mode === "global"
376
- ? "Save Configuration"
377
- : "Save Document"}
378
- </button>
355
+ Published
356
+ </span>
357
+ ) : (
358
+ <button
359
+ type="button"
360
+ onClick={() => handleSave(false)}
361
+ disabled={saving}
362
+ className="kyro-btn kyro-btn-lg kyro-btn-primary shadow-xl flex items-center gap-2"
363
+ >
364
+ {saving ? (
365
+ <svg
366
+ className="w-4 h-4 animate-spin"
367
+ viewBox="0 0 24 24"
368
+ fill="none"
369
+ stroke="currentColor"
370
+ strokeWidth="2"
371
+ >
372
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
373
+ </svg>
374
+ ) : (
375
+ <svg
376
+ className="w-4 h-4"
377
+ viewBox="0 0 24 24"
378
+ fill="none"
379
+ stroke="currentColor"
380
+ strokeWidth="2"
381
+ >
382
+ <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
383
+ <polyline points="17 21 17 13 7 13 7 21" />
384
+ <polyline points="7 3 7 8 15 8" />
385
+ </svg>
386
+ )}
387
+ {saving
388
+ ? "Saving..."
389
+ : mode === "global"
390
+ ? "Save Configuration"
391
+ : "Save Document"}
392
+ </button>
393
+ )}
379
394
  </div>
380
395
  )}
381
396
  </div>
@@ -17,7 +17,7 @@ import {
17
17
  import CodeMirror from "@uiw/react-codemirror";
18
18
  import { json } from "@codemirror/lang-json";
19
19
  import { aura } from "@uiw/codemirror-theme-aura";
20
- import { useUIStore } from "../lib/stores";
20
+ import { useUIStore, toast } from "../lib/stores";
21
21
  import { Modal, ModalContent, ModalActions } from "./ui/Modal";
22
22
  import { PageHeader } from "./ui/PageHeader";
23
23
  import { Badge } from "./ui/Badge";
@@ -70,9 +70,10 @@ export function DeveloperCenter({ collections }: { collections: Record<string, u
70
70
  loadKeys();
71
71
  setShowCreateModal(false);
72
72
  setNewKeyName("");
73
+ toast.success("API key generated");
73
74
  } catch (e) {
74
75
  console.error(e);
75
- alert({ title: "Error", message: "Failed to generate API key" });
76
+ toast.error("Failed to generate API key");
76
77
  }
77
78
  };
78
79
 
@@ -85,9 +86,10 @@ export function DeveloperCenter({ collections }: { collections: Record<string, u
85
86
  try {
86
87
  await apiDelete(`/api/keys/${id}`);
87
88
  loadKeys();
89
+ toast.success("API key revoked");
88
90
  } catch (e) {
89
91
  console.error(e);
90
- alert({ title: "Error", message: "Failed to revoke API key" });
92
+ toast.error("Failed to revoke API key");
91
93
  }
92
94
  }
93
95
  });
@@ -180,7 +182,7 @@ export function DeveloperCenter({ collections }: { collections: Record<string, u
180
182
  className="p-1.5 hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-all text-[var(--kyro-text-secondary)]"
181
183
  onClick={() => {
182
184
  navigator.clipboard.writeText(key.key);
183
- alert({ title: "Success", message: "API key copied to clipboard" });
185
+ toast.success("API key copied to clipboard");
184
186
  }}
185
187
  >
186
188
  <Copy className="w-4 h-4" />
@@ -311,7 +313,7 @@ export function DeveloperCenter({ collections }: { collections: Record<string, u
311
313
  type="button"
312
314
  onClick={handleRunTest}
313
315
  disabled={exploring || !testEndpoint}
314
- className="px-8 py-4 bg-[var(--kyro-primary)] text-white rounded-[1.5rem] font-bold text-sm shadow-xl disabled:opacity-50 disabled:cursor-not-allowed hover:scale-[1.02] transition-all flex items-center gap-3 shrink-0"
316
+ className="kyro-btn kyro-btn-primary px-8 py-4 rounded-[1.5rem] font-bold text-sm shadow-xl disabled:opacity-50 disabled:cursor-not-allowed hover:scale-[1.02] transition-all flex items-center gap-3 shrink-0"
315
317
  >
316
318
  {exploring ? (
317
319
  <RefreshCcw className="w-4 h-4 animate-spin" />
@@ -398,7 +400,7 @@ export function DeveloperCenter({ collections }: { collections: Record<string, u
398
400
  <button
399
401
  type="button"
400
402
  onClick={confirmGenerateKey}
401
- className="px-8 py-3 rounded-xl font-bold text-sm bg-[var(--kyro-primary)] text-white hover:opacity-90 shadow-lg shadow-[var(--kyro-primary)]/20 transition-all"
403
+ className="kyro-btn kyro-btn-primary px-8 py-3 rounded-xl font-bold text-sm hover:opacity-90 shadow-lg shadow-[var(--kyro-primary)]/20 transition-all"
402
404
  >
403
405
  Generate Token
404
406
  </button>
@@ -9,10 +9,15 @@ import DateField from "./fields/DateField";
9
9
  import { MarkdownField } from "./fields/MarkdownField";
10
10
  import TextField from "./fields/TextField";
11
11
  import { BlocksField } from "./fields/BlocksField";
12
- import PortableTextField from "./fields/PortableTextField";
12
+ import { RichTextField } from "./fields";
13
13
  import { ListField } from "./fields/ListField";
14
14
  import RelationshipField from "./fields/RelationshipField";
15
+ import SecretField from "./fields/SecretField";
15
16
  import FieldLayout from "./fields/FieldLayout";
17
+ import ArrayField from "./fields/ArrayField";
18
+ import { GroupLayout } from "./fields/GroupLayout";
19
+ import { ArrayLayout } from "./fields/ArrayLayout";
20
+ import { setChangeSource } from "../lib/change-source";
16
21
 
17
22
  interface FieldRendererProps {
18
23
  field: Field;
@@ -29,7 +34,12 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
29
34
  error,
30
35
  disabled,
31
36
  }) => {
32
- if (field.admin?.hidden) return null;
37
+ if (field.hidden === true || field.admin?.hidden === true) return null;
38
+
39
+ const onChangeKeystroke = (val: unknown) => {
40
+ setChangeSource("keystroke");
41
+ onChange(val);
42
+ };
33
43
 
34
44
  switch (field.type) {
35
45
  case "text":
@@ -39,7 +49,7 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
39
49
  <TextField
40
50
  field={field as any}
41
51
  value={value}
42
- onChange={onChange}
52
+ onChange={onChangeKeystroke}
43
53
  error={error}
44
54
  disabled={disabled}
45
55
  />
@@ -49,7 +59,7 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
49
59
  <TextField
50
60
  field={{ ...field, variant: "textarea" } as any}
51
61
  value={value}
52
- onChange={onChange}
62
+ onChange={onChangeKeystroke}
53
63
  error={error}
54
64
  disabled={disabled}
55
65
  />
@@ -59,7 +69,17 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
59
69
  <TextField
60
70
  field={{ ...field, variant: "password" } as any}
61
71
  value={value}
62
- onChange={onChange}
72
+ onChange={onChangeKeystroke}
73
+ error={error}
74
+ disabled={disabled}
75
+ />
76
+ );
77
+ case "secret":
78
+ return (
79
+ <SecretField
80
+ field={field as any}
81
+ value={value as string | null | undefined}
82
+ onChange={onChange as (value: string) => void}
63
83
  error={error}
64
84
  disabled={disabled}
65
85
  />
@@ -105,19 +125,11 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
105
125
  />
106
126
  );
107
127
  case "richtext":
108
- return (field as any).hasBlocks === false ? (
109
- <PortableTextField
110
- field={field as any}
111
- value={value}
112
- onChange={onChange}
113
- disabled={disabled}
114
- error={error}
115
- />
116
- ) : (
117
- <BlocksField
118
- field={field as any}
128
+ return (
129
+ <RichTextField
130
+ field={field}
119
131
  value={value}
120
- onChange={onChange}
132
+ onChange={onChangeKeystroke}
121
133
  disabled={disabled}
122
134
  error={error}
123
135
  />
@@ -127,7 +139,7 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
127
139
  <MarkdownField
128
140
  field={field as any}
129
141
  value={value}
130
- onChange={onChange}
142
+ onChange={onChangeKeystroke}
131
143
  disabled={disabled}
132
144
  error={error}
133
145
  />
@@ -137,7 +149,7 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
137
149
  <CodeField
138
150
  field={field as any}
139
151
  value={value}
140
- onChange={onChange}
152
+ onChange={onChangeKeystroke}
141
153
  disabled={disabled}
142
154
  error={error}
143
155
  />
@@ -174,6 +186,69 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
174
186
  />
175
187
  </FieldLayout>
176
188
  );
189
+ case "array":
190
+ return (
191
+ <ArrayLayout
192
+ field={field}
193
+ value={Array.isArray(value) ? value : []}
194
+ onChange={onChange}
195
+ disabled={disabled}
196
+ renderField={(nestedField, parentData, onNestedChange) => {
197
+ const nestedValue = parentData[nestedField.name];
198
+ return (
199
+ <FieldRenderer
200
+ key={nestedField.name}
201
+ field={nestedField}
202
+ value={nestedValue}
203
+ onChange={(val) => {
204
+ onNestedChange({
205
+ ...parentData,
206
+ [nestedField.name]: val,
207
+ });
208
+ }}
209
+ disabled={disabled}
210
+ error={error}
211
+ />
212
+ );
213
+ }}
214
+ />
215
+ );
216
+ case "blocks":
217
+ return (
218
+ <BlocksField
219
+ field={field as any}
220
+ value={value}
221
+ onChange={onChange}
222
+ disabled={disabled}
223
+ error={error}
224
+ />
225
+ );
226
+ case "group":
227
+ return (
228
+ <GroupLayout
229
+ field={field}
230
+ value={value as Record<string, unknown> | null}
231
+ onChange={onChange}
232
+ renderField={(nestedField, parentData, onNestedChange) => {
233
+ const nestedValue = parentData[nestedField.name];
234
+ return (
235
+ <FieldRenderer
236
+ key={nestedField.name}
237
+ field={nestedField}
238
+ value={nestedValue}
239
+ onChange={(val) => {
240
+ onNestedChange({
241
+ ...parentData,
242
+ [nestedField.name]: val,
243
+ });
244
+ }}
245
+ disabled={disabled}
246
+ error={error}
247
+ />
248
+ );
249
+ }}
250
+ />
251
+ );
177
252
  case "color":
178
253
  return (
179
254
  <FieldLayout field={field} error={error}>
@@ -4,7 +4,7 @@ import { Shimmer } from "./ui/Shimmer";
4
4
  import { Plus } from "./ui/icons";
5
5
  import { apiGet, apiDelete, withCacheBust } from "../lib/api";
6
6
 
7
- import { useAuthStore } from "../lib/stores";
7
+ import { useAuthStore, toast } from "../lib/stores";
8
8
  import { useUIStore } from "../lib/stores";
9
9
  import { adminPath as ADMIN_BASE } from "../lib/paths";
10
10
  import { PageHeader } from "./ui/PageHeader";
@@ -12,6 +12,7 @@ import { Badge } from "./ui/Badge";
12
12
 
13
13
 
14
14
  import type { CollectionConfig, Field } from "@kyro-cms/core";
15
+ import { resolveFieldValue } from "../lib/resolve-field-value";
15
16
 
16
17
  type FieldConfig = Field;
17
18
 
@@ -114,7 +115,7 @@ export function ListView({
114
115
  function flattenFields(fields: FieldConfig[]): FieldConfig[] {
115
116
  const result: FieldConfig[] = [];
116
117
  for (const field of fields || []) {
117
- if (!field.name || field.admin?.hidden || field.name === "id") continue;
118
+ if (!field.name || field.hidden === true || field.admin?.hidden || field.name === "id") continue;
118
119
  if (field.type === "tabs" && field.tabs) {
119
120
  for (const tab of field.tabs) {
120
121
  if (tab.fields) {
@@ -197,7 +198,21 @@ export function ListView({
197
198
  }, []);
198
199
 
199
200
  const displayFields = useMemo(
200
- () => allFields.filter((f): f is typeof f & { name: string } => !!f.name && visibleColumns.has(f.name)),
201
+ () => {
202
+ const fields = allFields.filter((f): f is typeof f & { name: string } => !!f.name && visibleColumns.has(f.name));
203
+ if (visibleColumns.has("publishStatus")) {
204
+ fields.push({
205
+ name: "publishStatus",
206
+ type: "select",
207
+ label: "Status",
208
+ options: [
209
+ { value: "draft", label: "Draft" },
210
+ { value: "published", label: "Published" },
211
+ ],
212
+ } as any);
213
+ }
214
+ return fields;
215
+ },
201
216
  [allFields, visibleColumns],
202
217
  );
203
218
 
@@ -211,21 +226,8 @@ export function ListView({
211
226
 
212
227
  function extractFieldValue(doc: any, field: FieldConfig): any {
213
228
  if (!field.name) return null;
214
- if (doc[field.name] !== undefined && doc[field.name] !== null) {
215
- return doc[field.name];
216
- }
217
- if (field.type === "group" && typeof doc[field.name] === "object") {
218
- const firstFieldName = field.fields?.[0]?.name;
219
- if (
220
- firstFieldName &&
221
- doc[field.name][firstFieldName] !== undefined
222
- ) {
223
- return doc[field.name][firstFieldName];
224
- }
225
- const firstKey = Object.keys(doc[field.name] || {})[0];
226
- if (firstKey) return doc[field.name][firstKey];
227
- }
228
- return null;
229
+ const val = resolveFieldValue(collection.fields as any, doc, field.name);
230
+ return val ?? null;
229
231
  }
230
232
 
231
233
  const fetchDocs = useCallback(async () => {
@@ -303,9 +305,10 @@ export function ListView({
303
305
  }
304
306
  setSelectedIds(new Set());
305
307
  fetchDocs();
308
+ toast.success("Documents deleted");
306
309
  } catch (error) {
307
310
  console.error("Bulk delete failed:", error);
308
- alert({ title: "Error", message: "Failed to delete some documents" });
311
+ toast.error("Failed to delete some documents");
309
312
  }
310
313
  }
311
314
  });
@@ -320,9 +323,10 @@ export function ListView({
320
323
  try {
321
324
  await apiDelete(`/api/${collectionSlug}/${id}`);
322
325
  fetchDocs();
326
+ toast.success("Document deleted");
323
327
  } catch (error) {
324
328
  console.error("Delete failed:", error);
325
- alert({ title: "Error", message: "Failed to delete document" });
329
+ toast.error("Failed to delete document");
326
330
  }
327
331
  }
328
332
  });
@@ -891,6 +895,15 @@ function formatCellValue(value: any, type?: string): string {
891
895
  });
892
896
  }
893
897
 
898
+ if (Array.isArray(value)) {
899
+ return value.map(item => {
900
+ if (item && typeof item === "object") {
901
+ return item.title || item.name || item.email || item.filename || item.url || JSON.stringify(item).slice(0, 30);
902
+ }
903
+ return String(item ?? "").slice(0, 30);
904
+ }).filter(Boolean).join(", ");
905
+ }
906
+
894
907
  if (typeof value === "object") {
895
908
  if (value.title) return value.title;
896
909
  if (value.name) return value.name;