@kyro-cms/admin 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +11960 -11006
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +67 -65
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +563 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.js +12183 -11238
- package/dist/index.js.map +1 -1
- package/package.json +15 -11
- package/src/components/ActionBar.tsx +27 -14
- package/src/components/Admin.tsx +1 -1
- package/src/components/ApiKeysManager.tsx +5 -5
- package/src/components/AutoForm.tsx +585 -369
- package/src/components/BrandingHub.tsx +7 -4
- package/src/components/CreateView.tsx +2 -0
- package/src/components/DetailView.tsx +71 -56
- package/src/components/DeveloperCenter.tsx +8 -6
- package/src/components/FieldRenderer.tsx +94 -19
- package/src/components/ListView.tsx +33 -20
- package/src/components/MediaGallery.tsx +219 -194
- package/src/components/PluginsManager.tsx +197 -70
- package/src/components/RestPlayground.tsx +7 -7
- package/src/components/SessionsManager.tsx +1 -1
- package/src/components/SettingsPage.tsx +22 -0
- package/src/components/Sidebar.astro +13 -41
- package/src/components/UserManagement.tsx +153 -15
- package/src/components/UserMenu.tsx +30 -4
- package/src/components/VersionHistoryPanel.tsx +112 -119
- package/src/components/WebhookManager.tsx +6 -4
- package/src/components/blocks/ArrayBlock.tsx +6 -23
- package/src/components/blocks/BlockEditModal.tsx +82 -309
- package/src/components/blocks/CardBlock.tsx +35 -0
- package/src/components/blocks/ChildBlocksTree.tsx +57 -31
- package/src/components/blocks/GenericBlock.tsx +44 -0
- package/src/components/blocks/HeadingSubheadingBlock.tsx +32 -0
- package/src/components/blocks/HeroBlock.tsx +5 -14
- package/src/components/blocks/RichTextBlock.tsx +5 -5
- package/src/components/blocks/index.ts +5 -3
- package/src/components/fields/AccordionField.tsx +2 -2
- package/src/components/fields/ArrayField.tsx +1 -1
- package/src/components/fields/ArrayLayout.tsx +120 -29
- package/src/components/fields/BlocksField.tsx +430 -50
- package/src/components/fields/CardField.tsx +73 -0
- package/src/components/fields/CheckboxField.tsx +7 -3
- package/src/components/fields/DateField.tsx +4 -1
- package/src/components/fields/GroupLayout.tsx +2 -2
- package/src/components/fields/HeadingSubheadingField.tsx +43 -0
- package/src/components/fields/ListField.tsx +2 -2
- package/src/components/fields/NumberField.tsx +4 -1
- package/src/components/fields/RelationshipField.tsx +153 -87
- package/src/components/fields/RichTextField.tsx +781 -0
- package/src/components/fields/SecretField.tsx +102 -0
- package/src/components/fields/SelectField.tsx +19 -6
- package/src/components/fields/TabsLayout.tsx +19 -9
- package/src/components/fields/TextField.tsx +4 -1
- package/src/components/fields/UploadField.tsx +122 -56
- package/src/components/fields/extensions/blockComponents.tsx +103 -174
- package/src/components/fields/extensions/blocksStore.ts +8 -1
- package/src/components/fields/index.ts +4 -2
- package/src/components/ui/PageHeader.tsx +5 -5
- package/src/components/ui/SlidePanel.tsx +8 -3
- package/src/components/ui/icons.tsx +109 -109
- package/src/components/users/UserDetail.tsx +79 -16
- package/src/hooks/useAutoFormState.ts +125 -62
- package/src/integration.ts +148 -46
- package/src/kyro-cms.d.ts +7 -2
- package/src/layouts/AuthLayout.astro +14 -2
- package/src/lib/autoform-store.ts +85 -52
- package/src/lib/change-source.ts +9 -0
- package/src/lib/config.ts +104 -8
- package/src/lib/globals.ts +44 -9
- package/src/lib/normalize-upload-fields.ts +41 -0
- package/src/lib/paths.ts +2 -2
- package/src/lib/resolve-field-value.ts +110 -0
- package/src/lib/shim/use-sync-external-store-with-selector.js +30 -0
- package/src/lib/shim/use-sync-external-store.js +1 -0
- package/src/lib/stores/index.ts +1 -0
- package/src/lib/useResourceManager.ts +4 -4
- package/src/lib/vite-shim-plugin.ts +100 -0
- package/src/pages/[collection]/[id].astro +1 -1
- package/src/pages/preview/[collection]/[id].astro +4 -4
- package/src/pages/settings/[slug].astro +2 -2
- package/src/styles/main.css +60 -54
- package/README.md +0 -46
- package/dist/EditorClient-Q23UXR37.cjs +0 -468
- package/dist/EditorClient-Q23UXR37.cjs.map +0 -1
- package/dist/EditorClient-T5PASFNR.js +0 -466
- package/dist/EditorClient-T5PASFNR.js.map +0 -1
- package/dist/chunk-3BGDYKTD.cjs +0 -348
- package/dist/chunk-3BGDYKTD.cjs.map +0 -1
- package/dist/chunk-EEFXLQVT.js +0 -3
- package/dist/chunk-EEFXLQVT.js.map +0 -1
- package/src/components/blocks/ButtonBlock.tsx +0 -64
- package/src/components/blocks/ColumnsBlock.tsx +0 -55
- package/src/components/blocks/DividerBlock.tsx +0 -43
- package/src/components/blocks/LinkBlock.tsx +0 -65
- package/src/components/blocks/VStackBlock.tsx +0 -29
- package/src/components/fields/EditorClient.tsx +0 -535
- package/src/components/fields/PortableTextField.tsx +0 -155
- package/src/components/fields/PortableTextRenderer.tsx +0 -68
|
@@ -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
|
-
|
|
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
|
-
: "
|
|
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
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
<
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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={
|
|
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={
|
|
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={
|
|
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 (
|
|
109
|
-
<
|
|
110
|
-
field={field
|
|
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={
|
|
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={
|
|
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={
|
|
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
|
-
() =>
|
|
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
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|