@kyro-cms/admin 0.3.2 → 0.3.5
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/EditorClient-XEUOVAAC.js +466 -0
- package/dist/EditorClient-XEUOVAAC.js.map +1 -0
- package/dist/EditorClient-YLCGVDXY.cjs +468 -0
- package/dist/EditorClient-YLCGVDXY.cjs.map +1 -0
- package/dist/chunk-7KPIUCGT.js +384 -0
- package/dist/chunk-7KPIUCGT.js.map +1 -0
- package/dist/chunk-GOACG6R7.cjs +473 -0
- package/dist/chunk-GOACG6R7.cjs.map +1 -0
- package/dist/index.cjs +14861 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +1661 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +563 -0
- package/dist/index.js +14784 -0
- package/dist/index.js.map +1 -0
- package/package.json +19 -19
- package/src/components/ActionBar.tsx +7 -43
- package/src/components/Admin.tsx +138 -277
- package/src/components/ApiKeysManager.tsx +428 -419
- package/src/components/AuditLogsPage.tsx +35 -39
- package/src/components/AuthBridge.tsx +51 -0
- package/src/components/AutoForm.tsx +495 -1230
- package/src/components/BrandingHub.tsx +18 -19
- package/src/components/BulkActionsBar.tsx +1 -1
- package/src/components/CreateView.tsx +22 -36
- package/src/components/Dashboard.tsx +60 -84
- package/src/components/DetailView.tsx +113 -91
- package/src/components/DeveloperCenter.tsx +200 -198
- package/src/components/FieldRenderer.tsx +206 -0
- package/src/components/GraphQLPlayground.tsx +340 -480
- package/src/components/ListView.tsx +828 -254
- package/src/components/LoginPage.tsx +3 -4
- package/src/components/MarketplaceManager.tsx +254 -0
- package/src/components/MediaGallery.tsx +856 -1192
- package/src/components/PluginsManager.tsx +277 -0
- package/src/components/RestPlayground.tsx +398 -560
- package/src/components/SessionsManager.tsx +211 -0
- package/src/components/Sidebar.astro +179 -151
- package/src/components/ThemeProvider.tsx +7 -161
- package/src/components/UserManagement.tsx +162 -146
- package/src/components/UserMenu.tsx +110 -0
- package/src/components/WebhookManager.tsx +305 -367
- package/src/components/blocks/AccordionBlock.tsx +4 -4
- package/src/components/blocks/ArrayBlock.tsx +3 -3
- package/src/components/blocks/BlockEditModal.tsx +8 -8
- package/src/components/blocks/BlockWrapper.tsx +61 -0
- package/src/components/blocks/ButtonBlock.tsx +4 -4
- package/src/components/blocks/ChildBlocksTree.tsx +23 -25
- package/src/components/blocks/CodeBlock.tsx +15 -15
- package/src/components/blocks/ColumnsBlock.tsx +6 -44
- package/src/components/blocks/DividerBlock.tsx +3 -3
- package/src/components/blocks/FileBlock.tsx +4 -4
- package/src/components/blocks/HeadingBlock.tsx +6 -38
- package/src/components/blocks/HeroBlock.tsx +4 -4
- package/src/components/blocks/ImageBlock.tsx +4 -4
- package/src/components/blocks/LinkBlock.tsx +4 -4
- package/src/components/blocks/ListBlock.tsx +3 -3
- package/src/components/blocks/ParagraphBlock.tsx +12 -42
- package/src/components/blocks/RelationshipBlock.tsx +4 -4
- package/src/components/blocks/RichTextBlock.tsx +4 -4
- package/src/components/blocks/VStackBlock.tsx +5 -37
- package/src/components/blocks/VideoBlock.tsx +4 -4
- package/src/components/blocks/types.ts +11 -0
- package/src/components/fields/AccordionField.tsx +1 -1
- package/src/components/fields/ArrayField.tsx +2 -2
- package/src/components/fields/ArrayLayout.tsx +93 -0
- package/src/components/fields/BlocksField.tsx +122 -111
- package/src/components/fields/ButtonField.tsx +1 -1
- package/src/components/fields/CheckboxField.tsx +14 -15
- package/src/components/fields/ChildrenField.tsx +2 -2
- package/src/components/fields/CodeField.tsx +3 -3
- package/src/components/fields/ColumnsField.tsx +2 -2
- package/src/components/fields/DateField.tsx +13 -26
- package/src/components/fields/EditorClient.tsx +26 -28
- package/src/components/fields/FieldLayout.tsx +52 -0
- package/src/components/fields/GroupLayout.tsx +35 -0
- package/src/components/fields/JSONField.tsx +7 -7
- package/src/components/fields/LinkField.tsx +1 -1
- package/src/components/fields/MarkdownField.tsx +1 -1
- package/src/components/fields/NumberField.tsx +13 -26
- package/src/components/fields/PortableTextField.tsx +4 -4
- package/src/components/fields/PortableTextRenderer.tsx +1 -1
- package/src/components/fields/RelationshipBlockField.tsx +31 -23
- package/src/components/fields/RelationshipField.tsx +14 -14
- package/src/components/fields/SelectField.tsx +17 -26
- package/src/components/fields/TabsLayout.tsx +69 -0
- package/src/components/fields/TextField.tsx +85 -38
- package/src/components/fields/UploadField.tsx +71 -41
- package/src/components/fields/VideoField.tsx +1 -1
- package/src/components/fields/extensions/blockComponents.tsx +2 -2
- package/src/components/fields/extensions/blocksStore.ts +207 -193
- package/src/components/fields/types.ts +22 -0
- package/src/components/layout/Layout.tsx +1 -1
- package/src/components/ui/ActionMenu.tsx +63 -0
- package/src/components/ui/Badge.tsx +59 -5
- package/src/components/ui/BlockDrawer.tsx +4 -5
- package/src/components/ui/CommandPalette.tsx +58 -36
- package/src/components/ui/CommandPaletteWrapper.tsx +18 -17
- package/src/components/ui/Dropdown.tsx +18 -16
- package/src/components/ui/EmptyState.tsx +25 -0
- package/src/components/ui/GlobalModal.tsx +49 -0
- package/src/components/ui/IconButton.tsx +44 -0
- package/src/components/ui/Modal.tsx +19 -20
- package/src/components/ui/PageHeader.tsx +158 -0
- package/src/components/ui/Pagination.tsx +61 -0
- package/src/components/ui/PromptModal.tsx +1 -1
- package/src/components/ui/SearchInput.tsx +57 -0
- package/src/components/ui/SeoPreview.tsx +31 -0
- package/src/components/ui/SessionModal.tsx +0 -0
- package/src/components/ui/SlidePanel.tsx +2 -0
- package/src/components/ui/Toast.tsx +65 -122
- package/src/components/ui/Toaster.tsx +18 -0
- package/src/components/ui/icons.tsx +112 -0
- package/src/components/users/UserDetail.tsx +290 -0
- package/src/components/users/UserForm.tsx +242 -0
- package/src/components/users/UsersList.tsx +338 -0
- package/src/env.d.ts +13 -13
- package/src/fields/index.ts +2 -1
- package/src/global.d.ts +7 -0
- package/src/hooks/data.ts +2 -9
- package/src/hooks/useAsyncData.ts +36 -0
- package/src/hooks/useAutoFormState.ts +527 -0
- package/src/hooks/useSelection.ts +49 -0
- package/src/hooks/useSession.ts +0 -0
- package/src/index.ts +11 -1
- package/src/integration.ts +86 -11
- package/src/kyro-cms.d.ts +209 -0
- package/src/layouts/AdminLayout.astro +128 -11
- package/src/layouts/AuthLayout.astro +21 -5
- package/src/lib/api.ts +175 -55
- package/src/lib/autoform-store.ts +435 -0
- package/src/lib/config.ts +82 -34
- package/src/lib/createRegistry.ts +29 -0
- package/src/lib/default-kyro-config.ts +4 -0
- package/src/lib/globals.ts +50 -0
- package/src/lib/media-utils.ts +18 -0
- package/src/lib/object-utils.ts +77 -0
- package/src/lib/paths.ts +61 -0
- package/src/lib/stores/index.ts +370 -0
- package/src/lib/types.ts +43 -0
- package/src/lib/useResourceManager.ts +105 -0
- package/src/pages/403.astro +67 -0
- package/src/pages/[collection]/[id].astro +14 -180
- package/src/pages/[collection]/index.astro +11 -6
- package/src/pages/api-explorer.astro +173 -0
- package/src/pages/audit/index.astro +2 -0
- package/src/pages/auth/login.astro +122 -0
- package/src/pages/auth/register.astro +167 -0
- package/src/pages/graphql-explorer.astro +59 -0
- package/src/pages/{admin/graphql.astro → graphql.astro} +51 -17
- package/src/pages/index.astro +577 -0
- package/src/pages/index_ALT.astro +3 -0
- package/src/pages/keys.astro +11 -0
- package/src/pages/marketplace.astro +11 -0
- package/src/pages/media.astro +3 -0
- package/src/pages/plugins.astro +8 -0
- package/src/pages/preview/[collection]/[id].astro +188 -123
- package/src/pages/rest-playground.astro +62 -0
- package/src/pages/roles/index.astro +183 -76
- package/src/pages/sessions.astro +8 -0
- package/src/pages/settings/[slug].astro +92 -114
- package/src/pages/settings/index.astro +5 -3
- package/src/pages/users/[id].astro +25 -154
- package/src/pages/users/index.astro +19 -130
- package/src/pages/users/new.astro +9 -86
- package/src/pages/webhooks.astro +11 -0
- package/src/routes.ts +80 -0
- package/src/styles/main.css +119 -79
- package/src/theme/tokens.ts +1 -0
- package/src/vite-env.d.ts +14 -0
- package/src/collections/auth/index.ts +0 -155
- package/src/collections/portfolio/index.ts +0 -343
- package/src/components/ApiExplorer.tsx +0 -325
- package/src/components/EnhancedListView.tsx +0 -889
- package/src/components/GraphQLExplorer.tsx +0 -675
- package/src/components/Icons.tsx +0 -23
- package/src/components/StatusBadge.tsx +0 -76
- package/src/lib/MediaService.ts +0 -541
- package/src/lib/auth/sqlite-adapter.ts +0 -319
- package/src/lib/dataStore.ts +0 -226
- package/src/lib/db/adapter.ts +0 -54
- package/src/lib/db/drizzle-mysql-adapter.ts +0 -194
- package/src/lib/db/drizzle-mysql-auth-adapter.ts +0 -327
- package/src/lib/db/drizzle-postgres-adapter.ts +0 -202
- package/src/lib/db/drizzle-postgres-auth-adapter.ts +0 -304
- package/src/lib/db/drizzle-sqlite-adapter.ts +0 -227
- package/src/lib/db/drizzle-sqlite-auth-adapter.ts +0 -548
- package/src/lib/db/index.ts +0 -449
- package/src/lib/db/mongodb-adapter.ts +0 -207
- package/src/lib/db/mongodb-auth-adapter.ts +0 -305
- package/src/lib/db/schema/mysql-auth.ts +0 -113
- package/src/lib/db/schema/mysql-content.ts +0 -20
- package/src/lib/db/schema/postgres-auth.ts +0 -116
- package/src/lib/db/schema/postgres-content.ts +0 -35
- package/src/lib/db/schema/postgres-media.ts +0 -52
- package/src/lib/db/schema/postgres-settings.ts +0 -11
- package/src/lib/db/schema/sqlite-auth.ts +0 -112
- package/src/lib/db/schema/sqlite-content.ts +0 -20
- package/src/lib/db/version-adapter.ts +0 -248
- package/src/lib/graphql/index.ts +0 -1
- package/src/lib/graphql/schema.ts +0 -443
- package/src/lib/rate-limit.ts +0 -267
- package/src/lib/storage.ts +0 -374
- package/src/lib/store.ts +0 -85
- package/src/middleware.ts +0 -177
- package/src/pages/admin/api-explorer.astro +0 -98
- package/src/pages/admin/graphql-explorer.astro +0 -40
- package/src/pages/admin/index.astro +0 -286
- package/src/pages/admin/keys.astro +0 -8
- package/src/pages/admin/rest-playground.astro +0 -44
- package/src/pages/admin/webhooks.astro +0 -8
- package/src/pages/api/[collection]/[id]/publish.ts +0 -52
- package/src/pages/api/[collection]/[id]/unpublish.ts +0 -42
- package/src/pages/api/[collection]/[id]/versions.ts +0 -66
- package/src/pages/api/[collection]/[id].ts +0 -213
- package/src/pages/api/[collection]/index.ts +0 -209
- package/src/pages/api/auth/[id].ts +0 -121
- package/src/pages/api/auth/audit-logs.ts +0 -57
- package/src/pages/api/auth/login.ts +0 -211
- package/src/pages/api/auth/logout.ts +0 -66
- package/src/pages/api/auth/me.ts +0 -36
- package/src/pages/api/auth/refresh.ts +0 -119
- package/src/pages/api/auth/register.ts +0 -188
- package/src/pages/api/auth/users.ts +0 -97
- package/src/pages/api/collections.ts +0 -59
- package/src/pages/api/globals/[slug].ts +0 -42
- package/src/pages/api/graphql.ts +0 -90
- package/src/pages/api/health.ts +0 -426
- package/src/pages/api/keys/[id].ts +0 -26
- package/src/pages/api/keys/index.ts +0 -75
- package/src/pages/api/media/[id].ts +0 -309
- package/src/pages/api/media/folders.ts +0 -609
- package/src/pages/api/media/index.ts +0 -146
- package/src/pages/api/media/resize.ts +0 -267
- package/src/pages/api/search.ts +0 -82
- package/src/pages/api/slug-availability.ts +0 -70
- package/src/pages/api/storage-config.ts +0 -20
- package/src/pages/api/storage-status.ts +0 -206
- package/src/pages/api/upload.ts +0 -334
- package/src/pages/api/webhooks/index.ts +0 -71
- package/src/pages/login.astro +0 -82
- package/src/pages/register.astro +0 -102
|
@@ -5,6 +5,8 @@ import type {
|
|
|
5
5
|
Field,
|
|
6
6
|
Block,
|
|
7
7
|
} from "@kyro-cms/core/client";
|
|
8
|
+
|
|
9
|
+
type View = "edit" | "version" | "api";
|
|
8
10
|
import { UploadField } from "./fields/UploadField";
|
|
9
11
|
import { CodeField } from "./fields";
|
|
10
12
|
import NumberField from "./fields/NumberField";
|
|
@@ -12,18 +14,31 @@ import CheckboxField from "./fields/CheckboxField";
|
|
|
12
14
|
import SelectField from "./fields/SelectField";
|
|
13
15
|
import DateField from "./fields/DateField";
|
|
14
16
|
import { MarkdownField } from "./fields/MarkdownField";
|
|
17
|
+
import TextField from "./fields/TextField";
|
|
15
18
|
import { globals, collections } from "../lib/config";
|
|
16
19
|
import { slugifyText } from "../lib/slugify";
|
|
20
|
+
import { resolveUrl, apiDelete, fetchWithAuth } from "../lib/api";
|
|
21
|
+
import { useAutoFormStore } from "../lib/autoform-store";
|
|
22
|
+
import { useAutoFormState } from "../hooks/useAutoFormState";
|
|
23
|
+
import { useUIStore } from "../lib/stores";
|
|
24
|
+
|
|
25
|
+
import { adminPath as ADMIN_BASE, apiPath as API_BASE } from "../lib/paths";
|
|
17
26
|
|
|
18
27
|
import { BlocksField } from "./fields/BlocksField";
|
|
19
28
|
import PortableTextField from "./fields/PortableTextField";
|
|
20
29
|
import { ConfirmModal, Modal as UIModal } from "./ui/Modal";
|
|
30
|
+
import { ListField } from "./fields/ListField";
|
|
31
|
+
import { RelationshipBlockField } from "./fields/RelationshipBlockField";
|
|
32
|
+
import { FieldRenderer } from "./FieldRenderer";
|
|
33
|
+
import { TabsLayout } from "./fields/TabsLayout";
|
|
34
|
+
import { GroupLayout } from "./fields/GroupLayout";
|
|
35
|
+
import { ArrayLayout } from "./fields/ArrayLayout";
|
|
21
36
|
|
|
22
37
|
interface AutoFormProps {
|
|
23
38
|
config: CollectionConfig | GlobalConfig;
|
|
24
|
-
data?: Record<string,
|
|
39
|
+
data?: Record<string, unknown>;
|
|
25
40
|
errors?: Record<string, string>;
|
|
26
|
-
onChange?: (data: Record<string,
|
|
41
|
+
onChange?: (data: Record<string, unknown>) => void;
|
|
27
42
|
disabled?: boolean;
|
|
28
43
|
collectionSlug?: string;
|
|
29
44
|
globalSlug?: string;
|
|
@@ -31,8 +46,8 @@ interface AutoFormProps {
|
|
|
31
46
|
layout?: "split" | "single";
|
|
32
47
|
onActionSuccess?: (message: string) => void;
|
|
33
48
|
onActionError?: (message: string) => void;
|
|
34
|
-
documentStatus?: "draft" | "published" | "scheduled" | "archived";
|
|
35
49
|
justSaved?: boolean;
|
|
50
|
+
documentStatus?: string;
|
|
36
51
|
}
|
|
37
52
|
|
|
38
53
|
export function AutoForm({
|
|
@@ -47,7 +62,6 @@ export function AutoForm({
|
|
|
47
62
|
layout = "split",
|
|
48
63
|
onActionSuccess,
|
|
49
64
|
onActionError,
|
|
50
|
-
documentStatus,
|
|
51
65
|
justSaved,
|
|
52
66
|
}: AutoFormProps) {
|
|
53
67
|
// Resolve the "live" config to preserve functions (admin.condition) lost during prop serialization
|
|
@@ -58,321 +72,117 @@ export function AutoForm({
|
|
|
58
72
|
: propConfig;
|
|
59
73
|
const config = activeConfig || propConfig;
|
|
60
74
|
|
|
61
|
-
// Helper to extract default values from config recursively
|
|
62
|
-
function getDefaults(fields: any[], prefix = ""): Record<string, any> {
|
|
63
|
-
const defaults: Record<string, any> = {};
|
|
64
|
-
for (const field of fields || []) {
|
|
65
|
-
if (field.defaultValue !== undefined) {
|
|
66
|
-
const key = prefix + field.name;
|
|
67
|
-
defaults[key] = field.defaultValue;
|
|
68
|
-
// Also set nested defaults for groups
|
|
69
|
-
if (field.type === "group" && field.fields) {
|
|
70
|
-
for (const subField of field.fields) {
|
|
71
|
-
if (subField.defaultValue !== undefined) {
|
|
72
|
-
defaults[prefix + field.name + "." + subField.name] =
|
|
73
|
-
subField.defaultValue;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
if (field.fields && Array.isArray(field.fields)) {
|
|
79
|
-
Object.assign(defaults, getDefaults(field.fields, field.name + "."));
|
|
80
|
-
}
|
|
81
|
-
if (field.tabs) {
|
|
82
|
-
for (const tab of field.tabs) {
|
|
83
|
-
if (tab.fields) {
|
|
84
|
-
Object.assign(defaults, getDefaults(tab.fields, prefix));
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return defaults;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Helper to flatten nested object with dot notation keys
|
|
93
|
-
function flattenObject(
|
|
94
|
-
obj: Record<string, any>,
|
|
95
|
-
prefix = "",
|
|
96
|
-
): Record<string, any> {
|
|
97
|
-
const result: Record<string, any> = {};
|
|
98
|
-
for (const key in obj) {
|
|
99
|
-
const newKey = prefix ? `${prefix}.${key}` : key;
|
|
100
|
-
const val = obj[key];
|
|
101
|
-
if (
|
|
102
|
-
val !== null &&
|
|
103
|
-
typeof val === "object" &&
|
|
104
|
-
!Array.isArray(val) &&
|
|
105
|
-
// Only recurse into plain objects, not Dates, Maps, or other class instances
|
|
106
|
-
(val.constructor === Object || !val.constructor)
|
|
107
|
-
) {
|
|
108
|
-
Object.assign(result, flattenObject(val, newKey));
|
|
109
|
-
} else {
|
|
110
|
-
result[newKey] = val;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return result;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Helper to unflatten dot notation keys back to nested object
|
|
117
|
-
function unflattenObject(flat: Record<string, any>): Record<string, any> {
|
|
118
|
-
const result: Record<string, any> = {};
|
|
119
|
-
for (const key in flat) {
|
|
120
|
-
const parts = key.split(".");
|
|
121
|
-
let current = result;
|
|
122
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
123
|
-
if (!current[parts[i]]) {
|
|
124
|
-
current[parts[i]] = {};
|
|
125
|
-
}
|
|
126
|
-
current = current[parts[i]];
|
|
127
|
-
}
|
|
128
|
-
current[parts[parts.length - 1]] = flat[key];
|
|
129
|
-
}
|
|
130
|
-
return result;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Merge initial data with defaults from config
|
|
134
|
-
const [formData, setFormData] = useState<Record<string, any>>({});
|
|
135
|
-
|
|
136
|
-
useEffect(() => {
|
|
137
|
-
try {
|
|
138
|
-
const configDefaults = config ? getDefaults(config.fields) : {};
|
|
139
|
-
const flatInitialData = flattenObject(initialData || {});
|
|
140
|
-
const mergedFlatData = { ...configDefaults, ...flatInitialData };
|
|
141
|
-
const mergedInitialData = unflattenObject(mergedFlatData);
|
|
142
|
-
setFormData(mergedInitialData);
|
|
143
|
-
} catch (e) {
|
|
144
|
-
console.error("Critical error in AutoForm data initialization:", e);
|
|
145
|
-
// Fallback to raw initialData if flattening fails
|
|
146
|
-
setFormData(initialData || {});
|
|
147
|
-
}
|
|
148
|
-
}, [initialData, config]);
|
|
149
|
-
const [activeTab, setActiveTab] = useState(0);
|
|
150
|
-
const [isSlugLocked, setIsSlugLocked] = useState(true);
|
|
151
|
-
const [view, setView] = useState<"edit" | "version" | "api">("edit");
|
|
152
|
-
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
153
|
-
const [versions, setVersions] = useState<any[]>([]);
|
|
154
|
-
const [loadingVersions, setLoadingVersions] = useState(false);
|
|
155
|
-
const [showPreview, setShowPreview] = useState(false);
|
|
156
|
-
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
157
|
-
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
|
158
|
-
const [loadingFields, setLoadingFields] = useState<Record<string, boolean>>(
|
|
159
|
-
{},
|
|
160
|
-
);
|
|
161
|
-
const menuRef = useRef<HTMLDivElement>(null);
|
|
162
|
-
const [compareMode, setCompareMode] = useState(false);
|
|
163
|
-
const [compareSelected, setCompareSelected] = useState<string[]>([]);
|
|
164
|
-
const [compareDiffs, setCompareDiffs] = useState<any[]>([]);
|
|
165
|
-
const [loadingDiffs, setLoadingDiffs] = useState(false);
|
|
166
|
-
const [confirmModal, setConfirmModal] = useState<{
|
|
167
|
-
open: boolean;
|
|
168
|
-
title: string;
|
|
169
|
-
message: string;
|
|
170
|
-
onConfirm: () => void;
|
|
171
|
-
danger?: boolean;
|
|
172
|
-
}>({ open: false, title: "", message: "", onConfirm: () => {} });
|
|
173
|
-
const [alertModal, setAlertModal] = useState<{
|
|
174
|
-
open: boolean;
|
|
175
|
-
title: string;
|
|
176
|
-
message: string;
|
|
177
|
-
}>({ open: false, title: "", message: "" });
|
|
178
|
-
const [lastSavedData, setLastSavedData] = useState<Record<string, any>>({});
|
|
179
|
-
const [isAutoSaving, setIsAutoSaving] = useState(false);
|
|
180
|
-
const [autoSaveStatus, setAutoSaveStatus] = useState<
|
|
181
|
-
"idle" | "saving" | "saved" | "error"
|
|
182
|
-
>("idle");
|
|
183
|
-
const autoSaveTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
184
|
-
const lastAutoSaveTimeRef = useRef<number>(0);
|
|
185
|
-
const autoSaveSkipRef = useRef<boolean>(false);
|
|
186
75
|
|
|
187
|
-
const
|
|
76
|
+
const { confirm, alert } = useUIStore();
|
|
188
77
|
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
// Track unsaved changes (compare against last saved state)
|
|
200
|
-
useEffect(() => {
|
|
201
|
-
const isDifferent =
|
|
202
|
-
JSON.stringify(formData) !== JSON.stringify(lastSavedData);
|
|
203
|
-
setHasUnsavedChanges(isDifferent);
|
|
204
|
-
}, [formData, lastSavedData]);
|
|
205
|
-
|
|
206
|
-
// Auto-generate slug from configured source field if locked
|
|
207
|
-
useEffect(() => {
|
|
208
|
-
const slugField = config.fields.find(
|
|
209
|
-
(f: any) => f.name === "slug" && f.admin?.autoGenerate,
|
|
210
|
-
);
|
|
211
|
-
if (!slugField?.admin?.autoGenerate) return;
|
|
212
|
-
const sourceField: string = slugField.admin.autoGenerate;
|
|
213
|
-
if (isSlugLocked && formData[sourceField]) {
|
|
214
|
-
const newSlug = slugifyText(formData[sourceField]);
|
|
215
|
-
if (newSlug !== formData.slug) {
|
|
216
|
-
setFormData((prev) => ({ ...prev, slug: newSlug }));
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}, [
|
|
220
|
-
formData.title,
|
|
221
|
-
formData.name,
|
|
222
|
-
formData.label,
|
|
78
|
+
const {
|
|
79
|
+
formData,
|
|
80
|
+
lastSavedData,
|
|
81
|
+
hasUnsavedChanges,
|
|
82
|
+
isAutoSaving,
|
|
83
|
+
autoSaveStatus,
|
|
84
|
+
sidebarCollapsed,
|
|
85
|
+
setSidebarCollapsed,
|
|
86
|
+
activeTab,
|
|
87
|
+
setActiveTab,
|
|
223
88
|
isSlugLocked,
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
};
|
|
269
|
-
}, [formData]);
|
|
89
|
+
setIsSlugLocked,
|
|
90
|
+
view,
|
|
91
|
+
setView,
|
|
92
|
+
isDropdownOpen,
|
|
93
|
+
setIsDropdownOpen,
|
|
94
|
+
versions,
|
|
95
|
+
loadingVersions,
|
|
96
|
+
showPreview,
|
|
97
|
+
setShowPreview,
|
|
98
|
+
isMenuOpen,
|
|
99
|
+
setIsMenuOpen,
|
|
100
|
+
loadingFields,
|
|
101
|
+
setLoadingFields,
|
|
102
|
+
compareMode,
|
|
103
|
+
setCompareMode,
|
|
104
|
+
compareSelected,
|
|
105
|
+
setCompareSelected,
|
|
106
|
+
compareDiffs,
|
|
107
|
+
setCompareDiffs,
|
|
108
|
+
loadingDiffs,
|
|
109
|
+
setLoadingDiffs,
|
|
110
|
+
setField,
|
|
111
|
+
setFormData,
|
|
112
|
+
markSaved,
|
|
113
|
+
setLastSavedData,
|
|
114
|
+
setAutoSaveStatus,
|
|
115
|
+
fetchVersions,
|
|
116
|
+
saveDocument,
|
|
117
|
+
publishDocument,
|
|
118
|
+
clearDraftArtifacts,
|
|
119
|
+
autoSaveSkipRef,
|
|
120
|
+
lastAutoSaveTimeRef,
|
|
121
|
+
documentStatus,
|
|
122
|
+
hasUnpublishedChanges,
|
|
123
|
+
versionsEnabled,
|
|
124
|
+
} = useAutoFormState({
|
|
125
|
+
config,
|
|
126
|
+
initialData,
|
|
127
|
+
collectionSlug,
|
|
128
|
+
globalSlug,
|
|
129
|
+
onChange,
|
|
130
|
+
onActionSuccess,
|
|
131
|
+
onActionError,
|
|
132
|
+
});
|
|
270
133
|
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
if (JSON.stringify(formData) === JSON.stringify(lastSavedData)) return;
|
|
274
|
-
|
|
275
|
-
setIsAutoSaving(true);
|
|
276
|
-
setAutoSaveStatus("saving");
|
|
277
|
-
|
|
278
|
-
try {
|
|
279
|
-
const { id, createdAt, updatedAt, ...rest } = formData;
|
|
280
|
-
const saveData = {
|
|
281
|
-
...rest,
|
|
282
|
-
_changeDescription: "Auto-saved",
|
|
283
|
-
status: formData.status === "published" ? "draft" : formData.status,
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
const response = await fetch(`/api/${collectionSlug}/${formData.id}`, {
|
|
287
|
-
method: "PATCH",
|
|
288
|
-
credentials: "include",
|
|
289
|
-
headers: { "Content-Type": "application/json" },
|
|
290
|
-
body: JSON.stringify(saveData),
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
if (response.ok) {
|
|
294
|
-
const result = await response.json();
|
|
295
|
-
setLastSavedData(result.data || formData);
|
|
296
|
-
lastAutoSaveTimeRef.current = Date.now();
|
|
297
|
-
setAutoSaveStatus("saved");
|
|
298
|
-
fetchVersions();
|
|
299
|
-
setTimeout(() => setAutoSaveStatus("idle"), 2000);
|
|
300
|
-
} else {
|
|
301
|
-
setAutoSaveStatus("error");
|
|
302
|
-
setTimeout(() => setAutoSaveStatus("idle"), 3000);
|
|
303
|
-
}
|
|
304
|
-
} catch (err) {
|
|
305
|
-
console.error("Auto-save failed:", err);
|
|
306
|
-
setAutoSaveStatus("error");
|
|
307
|
-
setTimeout(() => setAutoSaveStatus("idle"), 3000);
|
|
308
|
-
} finally {
|
|
309
|
-
setIsAutoSaving(false);
|
|
310
|
-
}
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
// Sync to hidden input for Astro form submission
|
|
314
|
-
useEffect(() => {
|
|
315
|
-
const hiddenInput = document.getElementById(
|
|
316
|
-
"form-data",
|
|
317
|
-
) as HTMLInputElement;
|
|
318
|
-
if (hiddenInput) {
|
|
319
|
-
hiddenInput.value = JSON.stringify(formData);
|
|
320
|
-
}
|
|
321
|
-
onChange?.(formData);
|
|
322
|
-
}, [formData, onChange]);
|
|
323
|
-
|
|
324
|
-
useEffect(() => {
|
|
325
|
-
if (formData.id) fetchVersions();
|
|
326
|
-
}, [formData.id]);
|
|
134
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
135
|
+
const disabled = propDisabled;
|
|
327
136
|
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
137
|
+
const handleRestoreVersion = (versionId: string) => {
|
|
138
|
+
confirm({
|
|
139
|
+
title: "Restore Version",
|
|
140
|
+
message: "Are you sure you want to restore this version? This will overwrite your current changes.",
|
|
141
|
+
onConfirm: async () => {
|
|
142
|
+
try {
|
|
143
|
+
const url = globalSlug
|
|
144
|
+
? resolveUrl(`/api/globals/${globalSlug}/versions/${versionId}/restore`)
|
|
145
|
+
: resolveUrl(`/api/${collectionSlug}/${formData.id}/versions/${versionId}/restore`);
|
|
146
|
+
|
|
147
|
+
// Try RESTful URL first
|
|
148
|
+
let resp = await fetchWithAuth(url, { method: "POST" });
|
|
149
|
+
|
|
150
|
+
// Fallback to legacy action-based URL for Collections if needed
|
|
151
|
+
if (!resp.ok && collectionSlug) {
|
|
152
|
+
resp = await fetchWithAuth(
|
|
153
|
+
resolveUrl(`/api/${collectionSlug}/${formData.id}/versions`),
|
|
154
|
+
{
|
|
155
|
+
method: "POST",
|
|
156
|
+
headers: { "Content-Type": "application/json" },
|
|
157
|
+
body: JSON.stringify({ versionId, action: "restore" }),
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
}
|
|
342
161
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
{
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
},
|
|
358
|
-
);
|
|
359
|
-
const result = await resp.json();
|
|
360
|
-
if (result.data) {
|
|
361
|
-
setFormData(result.data);
|
|
362
|
-
setView("edit");
|
|
363
|
-
fetchVersions();
|
|
162
|
+
const result = await resp.json();
|
|
163
|
+
if (result.data) {
|
|
164
|
+
setFormData(result.data);
|
|
165
|
+
useAutoFormStore.getState().loadDocument(result.data, result.data);
|
|
166
|
+
onActionSuccess?.("Version restored successfully");
|
|
167
|
+
fetchVersions();
|
|
168
|
+
setView("edit");
|
|
169
|
+
} else {
|
|
170
|
+
alert({ title: "Error", message: result.error || "Failed to restore version" });
|
|
171
|
+
}
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error("Failed to restore version:", err);
|
|
174
|
+
alert({ title: "Error", message: "Failed to restore version" });
|
|
175
|
+
}
|
|
364
176
|
}
|
|
365
|
-
}
|
|
366
|
-
console.error("Restore failed:", e);
|
|
367
|
-
}
|
|
177
|
+
});
|
|
368
178
|
};
|
|
369
179
|
|
|
370
180
|
const handleCompareVersions = async () => {
|
|
371
181
|
if (compareSelected.length !== 2) return;
|
|
372
182
|
setLoadingDiffs(true);
|
|
373
183
|
try {
|
|
374
|
-
const resp = await
|
|
375
|
-
`/api/${collectionSlug}/${formData.id}/versions?compareA=${compareSelected[0]}&compareB=${compareSelected[1]}
|
|
184
|
+
const resp = await fetchWithAuth(
|
|
185
|
+
resolveUrl(`/api/${collectionSlug}/${formData.id}/versions?compareA=${compareSelected[0]}&compareB=${compareSelected[1]}`),
|
|
376
186
|
);
|
|
377
187
|
const data = await resp.json();
|
|
378
188
|
setCompareDiffs(data.diffs || []);
|
|
@@ -401,7 +211,7 @@ export function AutoForm({
|
|
|
401
211
|
// Cmd/Ctrl + S = Publish
|
|
402
212
|
if ((e.metaKey || e.ctrlKey) && e.key === "s") {
|
|
403
213
|
e.preventDefault();
|
|
404
|
-
(document.getElementById("btn-save") as
|
|
214
|
+
(document.getElementById("btn-save") as HTMLButtonElement | null)?.click();
|
|
405
215
|
}
|
|
406
216
|
// Cmd/Ctrl + P = Toggle Preview
|
|
407
217
|
if ((e.metaKey || e.ctrlKey) && e.key === "p") {
|
|
@@ -422,6 +232,13 @@ export function AutoForm({
|
|
|
422
232
|
return () => window.removeEventListener("keydown", handleShortcuts);
|
|
423
233
|
}, []);
|
|
424
234
|
|
|
235
|
+
// Listen for external "View History" trigger from ActionBar
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
const handler = () => setView("version");
|
|
238
|
+
window.addEventListener("kyro:show-version-history", handler);
|
|
239
|
+
return () => window.removeEventListener("kyro:show-version-history", handler);
|
|
240
|
+
}, []);
|
|
241
|
+
|
|
425
242
|
useEffect(() => {
|
|
426
243
|
const handleClickOutside = (e: MouseEvent) => {
|
|
427
244
|
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
|
@@ -431,59 +248,59 @@ export function AutoForm({
|
|
|
431
248
|
if (isMenuOpen) {
|
|
432
249
|
document.addEventListener("mousedown", handleClickOutside);
|
|
433
250
|
return () =>
|
|
434
|
-
document.
|
|
251
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
435
252
|
}
|
|
436
253
|
}, [isMenuOpen]);
|
|
437
254
|
|
|
438
255
|
const handleCreateNew = () => {
|
|
439
256
|
if (hasUnsavedChanges) {
|
|
440
|
-
|
|
441
|
-
open: true,
|
|
257
|
+
confirm({
|
|
442
258
|
title: "Unsaved Changes",
|
|
443
259
|
message: "You have unsaved changes. Save before creating new?",
|
|
444
260
|
onConfirm: async () => {
|
|
445
|
-
(document.getElementById("btn-save") as
|
|
261
|
+
(document.getElementById("btn-save") as HTMLButtonElement | null)?.click();
|
|
446
262
|
await new Promise((r) => setTimeout(r, 1000));
|
|
447
|
-
window.location.href =
|
|
263
|
+
window.location.href = `${ADMIN_BASE}/${collectionSlug}/new`;
|
|
448
264
|
},
|
|
449
265
|
});
|
|
450
266
|
} else {
|
|
451
|
-
window.location.href =
|
|
267
|
+
window.location.href = `${ADMIN_BASE}/${collectionSlug}/new`;
|
|
452
268
|
}
|
|
453
269
|
};
|
|
454
270
|
|
|
455
271
|
const handleDuplicate = () => {
|
|
456
|
-
|
|
457
|
-
open: true,
|
|
272
|
+
confirm({
|
|
458
273
|
title: "Duplicate Document",
|
|
459
|
-
message: "
|
|
274
|
+
message: "Are you sure you want to duplicate this document?",
|
|
460
275
|
onConfirm: async () => {
|
|
461
|
-
const { id, createdAt, updatedAt, status, ...rest } = formData;
|
|
462
|
-
const duplicateData = {
|
|
463
|
-
...rest,
|
|
464
|
-
title: `${rest.title || rest.name || "Untitled"} (Copy)`,
|
|
465
|
-
};
|
|
466
276
|
try {
|
|
467
|
-
const
|
|
277
|
+
const { id, createdAt, updatedAt, ...duplicateData } = formData;
|
|
278
|
+
const response = await fetchWithAuth(`/api/${collectionSlug}`, {
|
|
468
279
|
method: "POST",
|
|
469
|
-
credentials: "include",
|
|
470
280
|
headers: { "Content-Type": "application/json" },
|
|
471
|
-
body: JSON.stringify(
|
|
281
|
+
body: JSON.stringify({
|
|
282
|
+
...duplicateData,
|
|
283
|
+
title: `${duplicateData.title || duplicateData.name || "Copy"} (Copy)`,
|
|
284
|
+
slug: `${duplicateData.slug || "copy"}-${Date.now()}`,
|
|
285
|
+
status: "draft",
|
|
286
|
+
}),
|
|
472
287
|
});
|
|
288
|
+
|
|
473
289
|
if (response.ok) {
|
|
474
290
|
const result = await response.json();
|
|
475
|
-
|
|
291
|
+
onActionSuccess?.("Document duplicated successfully");
|
|
292
|
+
if (result.data?.id) {
|
|
293
|
+
window.location.href = `${ADMIN_BASE}/${collectionSlug}/${result.data.id}`;
|
|
294
|
+
}
|
|
476
295
|
} else {
|
|
477
296
|
const error = await response.json();
|
|
478
|
-
|
|
479
|
-
open: true,
|
|
297
|
+
alert({
|
|
480
298
|
title: "Error",
|
|
481
299
|
message: error.error || "Failed to duplicate document",
|
|
482
300
|
});
|
|
483
301
|
}
|
|
484
302
|
} catch (err) {
|
|
485
|
-
|
|
486
|
-
open: true,
|
|
303
|
+
alert({
|
|
487
304
|
title: "Error",
|
|
488
305
|
message: "Failed to duplicate document",
|
|
489
306
|
});
|
|
@@ -493,61 +310,34 @@ export function AutoForm({
|
|
|
493
310
|
};
|
|
494
311
|
|
|
495
312
|
const handleDelete = () => {
|
|
496
|
-
|
|
497
|
-
open: true,
|
|
313
|
+
confirm({
|
|
498
314
|
title: "Delete Document",
|
|
499
|
-
message: "Delete this document? This cannot be undone.",
|
|
500
|
-
|
|
501
|
-
onConfirm: () => {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
{
|
|
512
|
-
method: "DELETE",
|
|
513
|
-
credentials: "include",
|
|
514
|
-
},
|
|
515
|
-
);
|
|
516
|
-
if (response.ok) {
|
|
517
|
-
window.location.href = `/${collectionSlug}`;
|
|
518
|
-
} else {
|
|
519
|
-
const error = await response.json();
|
|
520
|
-
setAlertModal({
|
|
521
|
-
open: true,
|
|
522
|
-
title: "Error",
|
|
523
|
-
message: error.error || "Failed to delete document",
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
} catch (err) {
|
|
527
|
-
setAlertModal({
|
|
528
|
-
open: true,
|
|
529
|
-
title: "Error",
|
|
530
|
-
message: "Failed to delete document",
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
},
|
|
534
|
-
});
|
|
315
|
+
message: "Delete this document? This cannot be undone. Are you absolutely sure?",
|
|
316
|
+
variant: "danger",
|
|
317
|
+
onConfirm: async () => {
|
|
318
|
+
try {
|
|
319
|
+
await apiDelete(`/api/${collectionSlug}/${formData.id}`);
|
|
320
|
+
window.location.href = `${ADMIN_BASE}/${collectionSlug}`;
|
|
321
|
+
} catch (err) {
|
|
322
|
+
alert({
|
|
323
|
+
title: "Error",
|
|
324
|
+
message: (err as Error).message || "Failed to delete document",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
535
327
|
},
|
|
536
328
|
});
|
|
537
329
|
};
|
|
538
330
|
|
|
539
331
|
const handleUnpublish = () => {
|
|
540
|
-
|
|
541
|
-
open: true,
|
|
332
|
+
confirm({
|
|
542
333
|
title: "Unpublish Document",
|
|
543
334
|
message: "Unpublish this document?",
|
|
544
335
|
onConfirm: async () => {
|
|
545
336
|
try {
|
|
546
|
-
const response = await
|
|
547
|
-
`/api/${collectionSlug}/${formData.id}/unpublish
|
|
337
|
+
const response = await fetchWithAuth(
|
|
338
|
+
resolveUrl(`/api/${collectionSlug}/${formData.id}/unpublish`),
|
|
548
339
|
{
|
|
549
340
|
method: "POST",
|
|
550
|
-
credentials: "include",
|
|
551
341
|
},
|
|
552
342
|
);
|
|
553
343
|
if (response.ok) {
|
|
@@ -555,15 +345,13 @@ export function AutoForm({
|
|
|
555
345
|
location.reload();
|
|
556
346
|
} else {
|
|
557
347
|
const error = await response.json();
|
|
558
|
-
|
|
559
|
-
open: true,
|
|
348
|
+
alert({
|
|
560
349
|
title: "Error",
|
|
561
350
|
message: error.error || "Failed to unpublish",
|
|
562
351
|
});
|
|
563
352
|
}
|
|
564
353
|
} catch (err) {
|
|
565
|
-
|
|
566
|
-
open: true,
|
|
354
|
+
alert({
|
|
567
355
|
title: "Error",
|
|
568
356
|
message: "Failed to unpublish",
|
|
569
357
|
});
|
|
@@ -572,17 +360,14 @@ export function AutoForm({
|
|
|
572
360
|
});
|
|
573
361
|
};
|
|
574
362
|
|
|
575
|
-
const handleFieldChange = (fieldName: string, value:
|
|
576
|
-
|
|
577
|
-
...prev,
|
|
578
|
-
[fieldName]: value,
|
|
579
|
-
}));
|
|
363
|
+
const handleFieldChange = (fieldName: string, value: unknown) => {
|
|
364
|
+
setField(fieldName, value);
|
|
580
365
|
};
|
|
581
366
|
|
|
582
367
|
const renderField = (
|
|
583
368
|
field: Field,
|
|
584
|
-
parentData?: Record<string,
|
|
585
|
-
onParentChange?: (val:
|
|
369
|
+
parentData?: Record<string, unknown>,
|
|
370
|
+
onParentChange?: (val: unknown) => void,
|
|
586
371
|
): React.ReactNode => {
|
|
587
372
|
if (field.admin?.hidden) return null;
|
|
588
373
|
|
|
@@ -606,7 +391,7 @@ export function AutoForm({
|
|
|
606
391
|
const value = currentData[field.name!];
|
|
607
392
|
const error = errors[field.name!];
|
|
608
393
|
|
|
609
|
-
const onFieldChange = (val:
|
|
394
|
+
const onFieldChange = (val: unknown) => {
|
|
610
395
|
if (onParentChange) {
|
|
611
396
|
onParentChange({ ...currentData, [field.name!]: val });
|
|
612
397
|
} else {
|
|
@@ -614,47 +399,47 @@ export function AutoForm({
|
|
|
614
399
|
}
|
|
615
400
|
};
|
|
616
401
|
|
|
617
|
-
|
|
402
|
+
if (field.type === "row" && "fields" in field) {
|
|
403
|
+
const rowFields = (field as Field & { fields?: Field[] }).fields;
|
|
618
404
|
return (
|
|
619
405
|
<div
|
|
620
406
|
key={field.name || `row-${Math.random()}`}
|
|
621
407
|
className="kyro-form-row flex gap-6 items-end"
|
|
622
408
|
>
|
|
623
|
-
{
|
|
624
|
-
const fAdmin = f.admin;
|
|
625
|
-
const actionUrl = fAdmin?.action;
|
|
409
|
+
{rowFields?.map((f: Field) => {
|
|
410
|
+
const fAdmin = f.admin || {};
|
|
411
|
+
const actionUrl = fAdmin?.action as string | undefined;
|
|
626
412
|
|
|
627
413
|
if (f.type === "button" && actionUrl) {
|
|
628
|
-
const siblingEmailField =
|
|
414
|
+
const siblingEmailField = rowFields?.find(
|
|
629
415
|
(ff: Field) => ff.type === "email",
|
|
630
416
|
);
|
|
631
417
|
return (
|
|
632
418
|
<div key={f.name} className="flex-shrink-0">
|
|
633
419
|
<button
|
|
634
420
|
type="button"
|
|
635
|
-
disabled={disabled}
|
|
636
421
|
onClick={async () => {
|
|
637
|
-
const rowName = field.name;
|
|
638
|
-
const emailFieldName = siblingEmailField?.name;
|
|
639
|
-
let emailValue = formData[emailFieldName];
|
|
640
|
-
if (!emailValue && rowName) {
|
|
641
|
-
emailValue = formData[rowName]?.[emailFieldName];
|
|
422
|
+
const rowName = field.name as string | undefined;
|
|
423
|
+
const emailFieldName = siblingEmailField?.name as string | undefined;
|
|
424
|
+
let emailValue = emailFieldName ? formData[emailFieldName] : undefined;
|
|
425
|
+
if (!emailValue && rowName && typeof rowName === "string" && emailFieldName) {
|
|
426
|
+
emailValue = (formData[rowName] as Record<string, unknown>)?.[emailFieldName] as string | undefined;
|
|
642
427
|
}
|
|
643
428
|
if (!emailValue) return;
|
|
644
429
|
|
|
645
430
|
setLoadingFields((prev) => ({
|
|
646
431
|
...prev,
|
|
647
|
-
[f.name
|
|
432
|
+
[f.name as string]: true,
|
|
648
433
|
}));
|
|
649
434
|
try {
|
|
650
|
-
const response = await
|
|
651
|
-
method: fAdmin.method || "POST",
|
|
435
|
+
const response = await fetchWithAuth(resolveUrl(actionUrl), {
|
|
436
|
+
method: (fAdmin.method as string) || "POST",
|
|
652
437
|
headers: { "Content-Type": "application/json" },
|
|
653
438
|
body: JSON.stringify({ email: emailValue }),
|
|
654
439
|
});
|
|
655
|
-
let result;
|
|
440
|
+
let result: { success?: boolean; message?: string; error?: string } = {};
|
|
656
441
|
try {
|
|
657
|
-
result = await response.json();
|
|
442
|
+
result = await response.json() as typeof result;
|
|
658
443
|
} catch {
|
|
659
444
|
result = {};
|
|
660
445
|
}
|
|
@@ -668,22 +453,22 @@ export function AutoForm({
|
|
|
668
453
|
`Request failed (${response.status})`;
|
|
669
454
|
onActionError?.(errorMsg);
|
|
670
455
|
}
|
|
671
|
-
} catch (err:
|
|
456
|
+
} catch (err: unknown) {
|
|
672
457
|
onActionError?.(
|
|
673
|
-
err.message
|
|
458
|
+
err instanceof Error ? err.message : "Error connecting to server",
|
|
674
459
|
);
|
|
675
460
|
} finally {
|
|
676
461
|
setLoadingFields((prev) => ({
|
|
677
462
|
...prev,
|
|
678
|
-
[f.name
|
|
463
|
+
[f.name as string]: false,
|
|
679
464
|
}));
|
|
680
465
|
}
|
|
681
466
|
}}
|
|
682
467
|
//@ts-ignore
|
|
683
|
-
disabled={loadingFields[f.name
|
|
468
|
+
disabled={loadingFields[f.name as string] || disabled}
|
|
684
469
|
className="bg-[var(--kyro-primary)] text-white px-4 py-2 rounded-lg font-medium hover:opacity-90 transition-opacity disabled:opacity-50"
|
|
685
470
|
>
|
|
686
|
-
{loadingFields[f.name
|
|
471
|
+
{loadingFields[f.name as string] ? "Sending..." : f.label || "Click"}
|
|
687
472
|
</button>
|
|
688
473
|
</div>
|
|
689
474
|
);
|
|
@@ -694,7 +479,7 @@ export function AutoForm({
|
|
|
694
479
|
key={f.name}
|
|
695
480
|
className={f.type === "button" ? "flex-shrink-0" : "flex-1"}
|
|
696
481
|
style={
|
|
697
|
-
fAdmin?.width ? { width: fAdmin.width, flex: "none" } : {}
|
|
482
|
+
fAdmin?.width ? { width: fAdmin.width as string, flex: "none" } : {}
|
|
698
483
|
}
|
|
699
484
|
>
|
|
700
485
|
{renderField(f, parentData, onParentChange)}
|
|
@@ -706,621 +491,68 @@ export function AutoForm({
|
|
|
706
491
|
}
|
|
707
492
|
|
|
708
493
|
switch (field.type) {
|
|
709
|
-
case "tabs":
|
|
710
|
-
const fieldTabs = (field as any).tabs;
|
|
711
|
-
const currentTab = fieldTabs[activeTab] || fieldTabs[0];
|
|
712
|
-
|
|
494
|
+
case "tabs":
|
|
713
495
|
return (
|
|
714
|
-
<
|
|
496
|
+
<TabsLayout
|
|
715
497
|
key={field.name || `tabs-${Math.random()}`}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
className={`px-6 py-3 text-sm font-bold transition-all border-b-2 -mb-[1px] ${
|
|
724
|
-
activeTab === index
|
|
725
|
-
? "border-[var(--kyro-text-primary)] text-[var(--kyro-text-primary)]"
|
|
726
|
-
: "border-transparent text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)]"
|
|
727
|
-
}`}
|
|
728
|
-
onClick={() => setActiveTab(index)}
|
|
729
|
-
>
|
|
730
|
-
{tab.label}
|
|
731
|
-
</button>
|
|
732
|
-
))}
|
|
733
|
-
</div>
|
|
734
|
-
<div className="space-y-6">
|
|
735
|
-
{currentTab?.fields.map((f: Field) =>
|
|
736
|
-
renderField(f, parentData, onParentChange),
|
|
737
|
-
)}
|
|
738
|
-
</div>
|
|
739
|
-
|
|
740
|
-
{currentTab?.label === "SEO" && (
|
|
741
|
-
<div className="mt-12 pt-8 border-t border-[var(--kyro-border)]">
|
|
742
|
-
<h4 className="text-xs font-bold text-[var(--kyro-text-secondary)] uppercase tracking-[0.2em] mb-6 opacity-50">
|
|
743
|
-
Live Google Preview
|
|
744
|
-
</h4>
|
|
745
|
-
<SeoPreview
|
|
746
|
-
title={formData.metaTitle || formData.title || "Untitled"}
|
|
747
|
-
description={
|
|
748
|
-
formData.metaDescription || "Please enter a description..."
|
|
749
|
-
}
|
|
750
|
-
slug={formData.slug || "your-slug"}
|
|
751
|
-
/>
|
|
752
|
-
</div>
|
|
753
|
-
)}
|
|
754
|
-
</div>
|
|
755
|
-
);
|
|
756
|
-
}
|
|
757
|
-
case "text":
|
|
758
|
-
case "email":
|
|
759
|
-
const textValue = currentData[field.name!];
|
|
760
|
-
const isKeyHidden = String(textValue).startsWith("••");
|
|
761
|
-
|
|
762
|
-
return (
|
|
763
|
-
<div key={field.name} className="kyro-form-field">
|
|
764
|
-
<label className="kyro-form-label flex items-center justify-between">
|
|
765
|
-
<div className="flex items-center gap-2">
|
|
766
|
-
{field.label || field.name}
|
|
767
|
-
{field.required && (
|
|
768
|
-
<span className="kyro-form-label-required">*</span>
|
|
769
|
-
)}
|
|
770
|
-
</div>
|
|
771
|
-
{(field.admin?.autoGenerate || field.admin?.readOnly) && (
|
|
772
|
-
<button
|
|
773
|
-
type="button"
|
|
774
|
-
onClick={async (e) => {
|
|
775
|
-
e.preventDefault();
|
|
776
|
-
e.stopPropagation();
|
|
777
|
-
|
|
778
|
-
if (field.admin?.autoGenerate === "key") {
|
|
779
|
-
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
780
|
-
let suffix = "";
|
|
781
|
-
for (let i = 0; i < 32; i++) {
|
|
782
|
-
suffix +=
|
|
783
|
-
chars[Math.floor(Math.random() * chars.length)];
|
|
784
|
-
}
|
|
785
|
-
onFieldChange(`kyro_${suffix}`);
|
|
786
|
-
} else if (field.admin?.autoGenerate) {
|
|
787
|
-
onFieldChange(
|
|
788
|
-
slugifyText(
|
|
789
|
-
formData[field.admin!.autoGenerate as string] || "",
|
|
790
|
-
),
|
|
791
|
-
);
|
|
792
|
-
} else if (
|
|
793
|
-
field.admin?.readOnly &&
|
|
794
|
-
textValue &&
|
|
795
|
-
!isKeyHidden
|
|
796
|
-
) {
|
|
797
|
-
await navigator.clipboard.writeText(String(textValue));
|
|
798
|
-
const actualKey = textValue;
|
|
799
|
-
onFieldChange(actualKey + "__COPIED__");
|
|
800
|
-
setTimeout(
|
|
801
|
-
() => onFieldChange("••••••••••••••••••••••••••••••"),
|
|
802
|
-
100,
|
|
803
|
-
);
|
|
804
|
-
}
|
|
805
|
-
}}
|
|
806
|
-
className="p-1.5 rounded-lg text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] hover:bg-[var(--kyro-surface-accent)] transition-all"
|
|
807
|
-
title={
|
|
808
|
-
field.admin?.autoGenerate === "key"
|
|
809
|
-
? "Generate new key"
|
|
810
|
-
: field.admin?.autoGenerate
|
|
811
|
-
? `Generate from ${field.admin.autoGenerate}`
|
|
812
|
-
: "Copy to clipboard"
|
|
813
|
-
}
|
|
814
|
-
>
|
|
815
|
-
{field.admin?.autoGenerate === "key" ? (
|
|
816
|
-
<svg
|
|
817
|
-
width="14"
|
|
818
|
-
height="14"
|
|
819
|
-
viewBox="0 0 24 24"
|
|
820
|
-
fill="none"
|
|
821
|
-
stroke="currentColor"
|
|
822
|
-
strokeWidth="2"
|
|
823
|
-
>
|
|
824
|
-
<path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" />
|
|
825
|
-
</svg>
|
|
826
|
-
) : field.admin?.autoGenerate ? (
|
|
827
|
-
<svg
|
|
828
|
-
width="14"
|
|
829
|
-
height="14"
|
|
830
|
-
viewBox="0 0 24 24"
|
|
831
|
-
fill="none"
|
|
832
|
-
stroke="currentColor"
|
|
833
|
-
strokeWidth="2"
|
|
834
|
-
>
|
|
835
|
-
<path d="M12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" />
|
|
836
|
-
</svg>
|
|
837
|
-
) : (
|
|
838
|
-
<svg
|
|
839
|
-
width="14"
|
|
840
|
-
height="14"
|
|
841
|
-
viewBox="0 0 24 24"
|
|
842
|
-
fill="none"
|
|
843
|
-
stroke="currentColor"
|
|
844
|
-
strokeWidth="2"
|
|
845
|
-
>
|
|
846
|
-
<rect x="8" y="8" width="12" height="12" rx="2" />
|
|
847
|
-
<path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2" />
|
|
848
|
-
</svg>
|
|
849
|
-
)}
|
|
850
|
-
</button>
|
|
851
|
-
)}
|
|
852
|
-
</label>
|
|
853
|
-
{field.name === "slug" ? (
|
|
854
|
-
<div className="flex items-center gap-2">
|
|
855
|
-
<div className="relative flex-1">
|
|
856
|
-
<input
|
|
857
|
-
type="text"
|
|
858
|
-
className={`kyro-form-input pr-24 ${isSlugLocked ? "opacity-70 bg-[var(--kyro-bg-secondary)]" : ""}`}
|
|
859
|
-
value={value || ""}
|
|
860
|
-
onChange={(e) => onFieldChange(e.target.value)}
|
|
861
|
-
disabled={isSlugLocked || disabled}
|
|
862
|
-
/>
|
|
863
|
-
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1">
|
|
864
|
-
{!isSlugLocked && (
|
|
865
|
-
<button
|
|
866
|
-
type="button"
|
|
867
|
-
onClick={() =>
|
|
868
|
-
onFieldChange(
|
|
869
|
-
slugifyText(
|
|
870
|
-
formData[field.admin?.autoGenerate || "title"] ||
|
|
871
|
-
"",
|
|
872
|
-
),
|
|
873
|
-
)
|
|
874
|
-
}
|
|
875
|
-
className="p-1 text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)]"
|
|
876
|
-
>
|
|
877
|
-
<svg
|
|
878
|
-
width="12"
|
|
879
|
-
height="12"
|
|
880
|
-
viewBox="0 0 24 24"
|
|
881
|
-
fill="none"
|
|
882
|
-
stroke="currentColor"
|
|
883
|
-
strokeWidth="2.5"
|
|
884
|
-
>
|
|
885
|
-
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
|
|
886
|
-
<path d="M21 3v5h-5" />
|
|
887
|
-
</svg>
|
|
888
|
-
</button>
|
|
889
|
-
)}
|
|
890
|
-
<button
|
|
891
|
-
type="button"
|
|
892
|
-
onClick={() => setIsSlugLocked(!isSlugLocked)}
|
|
893
|
-
className={`p-1.5 rounded ${isSlugLocked ? "text-[var(--kyro-primary)]" : "text-[var(--kyro-text-secondary)]"}`}
|
|
894
|
-
>
|
|
895
|
-
{isSlugLocked ? (
|
|
896
|
-
<svg
|
|
897
|
-
width="12"
|
|
898
|
-
height="12"
|
|
899
|
-
viewBox="0 0 24 24"
|
|
900
|
-
fill="none"
|
|
901
|
-
stroke="currentColor"
|
|
902
|
-
strokeWidth="2.5"
|
|
903
|
-
>
|
|
904
|
-
<rect
|
|
905
|
-
x="3"
|
|
906
|
-
y="11"
|
|
907
|
-
width="18"
|
|
908
|
-
height="11"
|
|
909
|
-
rx="2"
|
|
910
|
-
ry="2"
|
|
911
|
-
/>
|
|
912
|
-
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
|
913
|
-
</svg>
|
|
914
|
-
) : (
|
|
915
|
-
<svg
|
|
916
|
-
width="12"
|
|
917
|
-
height="12"
|
|
918
|
-
viewBox="0 0 24 24"
|
|
919
|
-
fill="none"
|
|
920
|
-
stroke="currentColor"
|
|
921
|
-
strokeWidth="2.5"
|
|
922
|
-
>
|
|
923
|
-
<rect
|
|
924
|
-
x="3"
|
|
925
|
-
y="11"
|
|
926
|
-
width="18"
|
|
927
|
-
height="11"
|
|
928
|
-
rx="2"
|
|
929
|
-
ry="2"
|
|
930
|
-
/>
|
|
931
|
-
<path d="M7 11V7a5 5 0 0 1 9.9-1" />
|
|
932
|
-
</svg>
|
|
933
|
-
)}
|
|
934
|
-
</button>
|
|
935
|
-
</div>
|
|
936
|
-
</div>
|
|
937
|
-
</div>
|
|
938
|
-
) : (
|
|
939
|
-
<input
|
|
940
|
-
type={(field as any).variant === "url" ? "url" : "text"}
|
|
941
|
-
className="kyro-form-input"
|
|
942
|
-
value={value || ""}
|
|
943
|
-
onChange={(e) => onFieldChange(e.target.value)}
|
|
944
|
-
disabled={disabled}
|
|
945
|
-
/>
|
|
946
|
-
)}
|
|
947
|
-
{field.name?.toLowerCase().includes("metatitle") && (
|
|
948
|
-
<div className="flex items-center justify-between mt-1 text-[10px] font-bold uppercase tracking-wider">
|
|
949
|
-
<span
|
|
950
|
-
className={
|
|
951
|
-
(value?.length || 0) > 60
|
|
952
|
-
? "text-red-500"
|
|
953
|
-
: (value?.length || 0) >= 40
|
|
954
|
-
? "text-green-500"
|
|
955
|
-
: "text-amber-600"
|
|
956
|
-
}
|
|
957
|
-
>
|
|
958
|
-
{value?.length || 0} / 60 —{" "}
|
|
959
|
-
{(value?.length || 0) > 60
|
|
960
|
-
? "Too Long"
|
|
961
|
-
: (value?.length || 0) >= 40
|
|
962
|
-
? "Ideal"
|
|
963
|
-
: "Short"}
|
|
964
|
-
</span>
|
|
965
|
-
</div>
|
|
966
|
-
)}
|
|
967
|
-
{error && <p className="kyro-form-error">{error}</p>}
|
|
968
|
-
</div>
|
|
969
|
-
);
|
|
970
|
-
|
|
971
|
-
case "textarea":
|
|
972
|
-
return (
|
|
973
|
-
<div key={field.name} className="kyro-form-field">
|
|
974
|
-
<label className="kyro-form-label">
|
|
975
|
-
{field.label || field.name}
|
|
976
|
-
</label>
|
|
977
|
-
<textarea
|
|
978
|
-
className="kyro-form-input kyro-form-textarea"
|
|
979
|
-
value={value || ""}
|
|
980
|
-
onChange={(e) => onFieldChange(e.target.value)}
|
|
981
|
-
disabled={disabled}
|
|
982
|
-
rows={4}
|
|
983
|
-
/>
|
|
984
|
-
{field.name?.toLowerCase().includes("metadescription") && (
|
|
985
|
-
<div className="mt-1 text-[10px] font-bold uppercase tracking-wider">
|
|
986
|
-
<span
|
|
987
|
-
className={
|
|
988
|
-
(value?.length || 0) > 160
|
|
989
|
-
? "text-red-500"
|
|
990
|
-
: (value?.length || 0) >= 120
|
|
991
|
-
? "text-green-500"
|
|
992
|
-
: "text-amber-600"
|
|
993
|
-
}
|
|
994
|
-
>
|
|
995
|
-
{value?.length || 0} / 160 —{" "}
|
|
996
|
-
{(value?.length || 0) > 160
|
|
997
|
-
? "Too Long"
|
|
998
|
-
: (value?.length || 0) >= 120
|
|
999
|
-
? "Ideal"
|
|
1000
|
-
: "Short"}
|
|
1001
|
-
</span>
|
|
1002
|
-
</div>
|
|
1003
|
-
)}
|
|
1004
|
-
</div>
|
|
1005
|
-
);
|
|
1006
|
-
|
|
1007
|
-
case "richtext":
|
|
1008
|
-
return (field as any).hasBlocks === false ? (
|
|
1009
|
-
<PortableTextField
|
|
1010
|
-
key={field.name}
|
|
1011
|
-
field={field as any}
|
|
1012
|
-
value={value}
|
|
1013
|
-
onChange={(newValue: any) => onFieldChange(newValue)}
|
|
1014
|
-
disabled={disabled}
|
|
1015
|
-
error={error}
|
|
1016
|
-
/>
|
|
1017
|
-
) : (
|
|
1018
|
-
<BlocksField
|
|
1019
|
-
key={field.name}
|
|
1020
|
-
field={field as any}
|
|
1021
|
-
value={value}
|
|
1022
|
-
onChange={(newValue: any) => onFieldChange(newValue)}
|
|
1023
|
-
disabled={disabled}
|
|
1024
|
-
error={error}
|
|
498
|
+
field={field}
|
|
499
|
+
formData={formData}
|
|
500
|
+
onTabDataChange={(newTabData) => {
|
|
501
|
+
const updateTabData = useAutoFormStore.getState().updateTabData;
|
|
502
|
+
updateTabData(field.name as string, newTabData);
|
|
503
|
+
}}
|
|
504
|
+
renderField={renderField}
|
|
1025
505
|
/>
|
|
1026
506
|
);
|
|
1027
507
|
|
|
1028
508
|
case "group":
|
|
1029
|
-
if ("fields" in field) {
|
|
1030
|
-
const groupData = value || {};
|
|
1031
|
-
return (
|
|
1032
|
-
<div key={field.name} className="kyro-form-group">
|
|
1033
|
-
<h3 className="kyro-form-group-title">
|
|
1034
|
-
{field.label || field.name}
|
|
1035
|
-
</h3>
|
|
1036
|
-
<div className="kyro-form-group-fields">
|
|
1037
|
-
{(field as any).fields.map((f: Field) =>
|
|
1038
|
-
renderField(f, groupData, onFieldChange),
|
|
1039
|
-
)}
|
|
1040
|
-
</div>
|
|
1041
|
-
</div>
|
|
1042
|
-
);
|
|
1043
|
-
}
|
|
1044
|
-
return null;
|
|
1045
|
-
|
|
1046
|
-
case "array":
|
|
1047
|
-
if ("fields" in field) {
|
|
1048
|
-
const items = Array.isArray(value) ? value : [];
|
|
1049
|
-
const labelField = (field as any).fields?.[0]?.name || "user";
|
|
1050
|
-
const isRelationship =
|
|
1051
|
-
(field as any).fields?.[0]?.type === "relationship";
|
|
1052
|
-
return (
|
|
1053
|
-
<div key={field.name} className="kyro-form-field">
|
|
1054
|
-
<label className="kyro-form-label">
|
|
1055
|
-
{field.label || field.name}
|
|
1056
|
-
</label>
|
|
1057
|
-
{isRelationship ? (
|
|
1058
|
-
<RelationshipField
|
|
1059
|
-
field={{
|
|
1060
|
-
name: labelField,
|
|
1061
|
-
relationTo: (field as any).fields[0].relationTo,
|
|
1062
|
-
hasMany: true,
|
|
1063
|
-
label: (field as any).fields[0].label,
|
|
1064
|
-
}}
|
|
1065
|
-
value={items.map((i: any) => i[labelField]).filter(Boolean)}
|
|
1066
|
-
onChange={(newValue: any) => {
|
|
1067
|
-
const newItems = (newValue || []).map((id: string) => ({
|
|
1068
|
-
[labelField]: id,
|
|
1069
|
-
}));
|
|
1070
|
-
onFieldChange(newItems);
|
|
1071
|
-
}}
|
|
1072
|
-
disabled={disabled}
|
|
1073
|
-
/>
|
|
1074
|
-
) : (
|
|
1075
|
-
<div className="kyro-form-array">
|
|
1076
|
-
{items.map((item: any, index: number) => (
|
|
1077
|
-
<div key={index} className="kyro-form-array-item">
|
|
1078
|
-
<div className="flex justify-between mb-2">
|
|
1079
|
-
<span className="text-xs font-bold opacity-50">
|
|
1080
|
-
Item {index + 1}
|
|
1081
|
-
</span>
|
|
1082
|
-
<button
|
|
1083
|
-
type="button"
|
|
1084
|
-
className="text-red-500"
|
|
1085
|
-
onClick={() =>
|
|
1086
|
-
onFieldChange(items.filter((_, i) => i !== index))
|
|
1087
|
-
}
|
|
1088
|
-
>
|
|
1089
|
-
Remove
|
|
1090
|
-
</button>
|
|
1091
|
-
</div>
|
|
1092
|
-
{(field as any).fields.map((f: Field) =>
|
|
1093
|
-
renderField(f, item, (newItem) => {
|
|
1094
|
-
const newItems = [...items];
|
|
1095
|
-
newItems[index] = newItem;
|
|
1096
|
-
onFieldChange(newItems);
|
|
1097
|
-
}),
|
|
1098
|
-
)}
|
|
1099
|
-
</div>
|
|
1100
|
-
))}
|
|
1101
|
-
<button
|
|
1102
|
-
type="button"
|
|
1103
|
-
className="kyro-btn kyro-btn-secondary kyro-btn-sm"
|
|
1104
|
-
onClick={() => onFieldChange([...items, {}])}
|
|
1105
|
-
>
|
|
1106
|
-
Add Item
|
|
1107
|
-
</button>
|
|
1108
|
-
</div>
|
|
1109
|
-
)}
|
|
1110
|
-
</div>
|
|
1111
|
-
);
|
|
1112
|
-
}
|
|
1113
|
-
return null;
|
|
1114
|
-
|
|
1115
|
-
case "blocks":
|
|
1116
509
|
return (
|
|
1117
|
-
<
|
|
510
|
+
<GroupLayout
|
|
1118
511
|
key={field.name}
|
|
1119
|
-
field={field
|
|
1120
|
-
value={value}
|
|
1121
|
-
onChange={
|
|
1122
|
-
|
|
1123
|
-
error={error}
|
|
512
|
+
field={field}
|
|
513
|
+
value={value as Record<string, unknown> | null}
|
|
514
|
+
onChange={onFieldChange}
|
|
515
|
+
renderField={renderField}
|
|
1124
516
|
/>
|
|
1125
517
|
);
|
|
1126
518
|
|
|
1127
|
-
case "
|
|
1128
|
-
return (
|
|
1129
|
-
<div key={field.name} className="kyro-form-field">
|
|
1130
|
-
<label className="kyro-form-label">
|
|
1131
|
-
{field.label || field.name}
|
|
1132
|
-
{field.required && (
|
|
1133
|
-
<span className="kyro-form-label-required">*</span>
|
|
1134
|
-
)}
|
|
1135
|
-
</label>
|
|
1136
|
-
<NumberField
|
|
1137
|
-
field={field as any}
|
|
1138
|
-
value={value}
|
|
1139
|
-
onChange={(newValue) => onFieldChange(newValue)}
|
|
1140
|
-
disabled={disabled}
|
|
1141
|
-
error={error}
|
|
1142
|
-
/>
|
|
1143
|
-
</div>
|
|
1144
|
-
);
|
|
1145
|
-
|
|
1146
|
-
case "checkbox":
|
|
1147
|
-
return (
|
|
1148
|
-
<div key={field.name} className="kyro-form-field">
|
|
1149
|
-
<CheckboxField
|
|
1150
|
-
field={field as any}
|
|
1151
|
-
value={value}
|
|
1152
|
-
onChange={(newValue) => onFieldChange(newValue)}
|
|
1153
|
-
disabled={disabled}
|
|
1154
|
-
error={error}
|
|
1155
|
-
/>
|
|
1156
|
-
{error && <p className="kyro-form-error">{error}</p>}
|
|
1157
|
-
</div>
|
|
1158
|
-
);
|
|
1159
|
-
|
|
1160
|
-
case "select":
|
|
1161
|
-
return (
|
|
1162
|
-
<div key={field.name} className="kyro-form-field">
|
|
1163
|
-
<label className="kyro-form-label">
|
|
1164
|
-
{field.label || field.name}
|
|
1165
|
-
{field.required && (
|
|
1166
|
-
<span className="kyro-form-label-required">*</span>
|
|
1167
|
-
)}
|
|
1168
|
-
</label>
|
|
1169
|
-
<SelectField
|
|
1170
|
-
field={field as any}
|
|
1171
|
-
value={value}
|
|
1172
|
-
onChange={(newValue) => onFieldChange(newValue)}
|
|
1173
|
-
disabled={disabled}
|
|
1174
|
-
error={error}
|
|
1175
|
-
/>
|
|
1176
|
-
{error && <p className="kyro-form-error">{error}</p>}
|
|
1177
|
-
</div>
|
|
1178
|
-
);
|
|
1179
|
-
|
|
1180
|
-
case "date":
|
|
1181
|
-
return (
|
|
1182
|
-
<div key={field.name} className="kyro-form-field">
|
|
1183
|
-
<label className="kyro-form-label">
|
|
1184
|
-
{field.label || field.name}
|
|
1185
|
-
{field.required && (
|
|
1186
|
-
<span className="kyro-form-label-required">*</span>
|
|
1187
|
-
)}
|
|
1188
|
-
</label>
|
|
1189
|
-
<DateField
|
|
1190
|
-
field={field as any}
|
|
1191
|
-
value={value}
|
|
1192
|
-
onChange={(newValue) => onFieldChange(newValue)}
|
|
1193
|
-
disabled={disabled}
|
|
1194
|
-
error={error}
|
|
1195
|
-
/>
|
|
1196
|
-
{error && <p className="kyro-form-error">{error}</p>}
|
|
1197
|
-
</div>
|
|
1198
|
-
);
|
|
1199
|
-
|
|
1200
|
-
case "password":
|
|
1201
|
-
return (
|
|
1202
|
-
<div key={field.name} className="kyro-form-field">
|
|
1203
|
-
<label className="kyro-form-label flex items-center justify-between">
|
|
1204
|
-
<div className="flex items-center gap-2">
|
|
1205
|
-
{field.label || field.name}
|
|
1206
|
-
{field.required && (
|
|
1207
|
-
<span className="kyro-form-label-required">*</span>
|
|
1208
|
-
)}
|
|
1209
|
-
</div>
|
|
1210
|
-
</label>
|
|
1211
|
-
<input
|
|
1212
|
-
type="password"
|
|
1213
|
-
className="kyro-form-input"
|
|
1214
|
-
value={value || ""}
|
|
1215
|
-
onChange={(e) => onFieldChange(e.target.value)}
|
|
1216
|
-
disabled={disabled}
|
|
1217
|
-
placeholder={
|
|
1218
|
-
field.admin?.placeholder || `Enter ${field.label || field.name}`
|
|
1219
|
-
}
|
|
1220
|
-
/>
|
|
1221
|
-
{error && <p className="kyro-form-error">{error}</p>}
|
|
1222
|
-
</div>
|
|
1223
|
-
);
|
|
1224
|
-
|
|
1225
|
-
case "radio":
|
|
1226
|
-
return (
|
|
1227
|
-
<div key={field.name} className="kyro-form-field">
|
|
1228
|
-
<label className="kyro-form-label">
|
|
1229
|
-
{field.label || field.name}
|
|
1230
|
-
{field.required && (
|
|
1231
|
-
<span className="kyro-form-label-required">*</span>
|
|
1232
|
-
)}
|
|
1233
|
-
</label>
|
|
1234
|
-
<div className="kyro-form-radio-group">
|
|
1235
|
-
{((field as any).options || []).map((opt: any) => (
|
|
1236
|
-
<label key={opt.value} className="kyro-form-radio-label">
|
|
1237
|
-
<input
|
|
1238
|
-
type="radio"
|
|
1239
|
-
name={field.name}
|
|
1240
|
-
value={opt.value}
|
|
1241
|
-
checked={value === opt.value}
|
|
1242
|
-
onChange={() => onFieldChange(opt.value)}
|
|
1243
|
-
disabled={disabled}
|
|
1244
|
-
className="kyro-form-radio"
|
|
1245
|
-
/>
|
|
1246
|
-
<span>{opt.label || opt.value}</span>
|
|
1247
|
-
</label>
|
|
1248
|
-
))}
|
|
1249
|
-
</div>
|
|
1250
|
-
{error && <p className="kyro-form-error">{error}</p>}
|
|
1251
|
-
</div>
|
|
1252
|
-
);
|
|
1253
|
-
|
|
1254
|
-
case "color":
|
|
1255
|
-
return (
|
|
1256
|
-
<div key={field.name} className="kyro-form-field">
|
|
1257
|
-
<label className="kyro-form-label flex items-center gap-2">
|
|
1258
|
-
{field.label || field.name}
|
|
1259
|
-
{field.required && (
|
|
1260
|
-
<span className="kyro-form-label-required">*</span>
|
|
1261
|
-
)}
|
|
1262
|
-
{value && (
|
|
1263
|
-
<span
|
|
1264
|
-
className="w-5 h-5 rounded border border-[var(--kyro-border)] shrink-0"
|
|
1265
|
-
style={{ backgroundColor: value }}
|
|
1266
|
-
/>
|
|
1267
|
-
)}
|
|
1268
|
-
</label>
|
|
1269
|
-
<div className="flex items-center gap-3">
|
|
1270
|
-
<input
|
|
1271
|
-
type="color"
|
|
1272
|
-
value={value || "#000000"}
|
|
1273
|
-
onChange={(e) => onFieldChange(e.target.value)}
|
|
1274
|
-
disabled={disabled}
|
|
1275
|
-
className="kyro-form-input h-10 w-14 p-1 cursor-pointer"
|
|
1276
|
-
/>
|
|
1277
|
-
<input
|
|
1278
|
-
type="text"
|
|
1279
|
-
className="kyro-form-input font-mono uppercase"
|
|
1280
|
-
value={value || ""}
|
|
1281
|
-
onChange={(e) => onFieldChange(e.target.value)}
|
|
1282
|
-
disabled={disabled}
|
|
1283
|
-
placeholder="#000000"
|
|
1284
|
-
/>
|
|
1285
|
-
</div>
|
|
1286
|
-
{error && <p className="kyro-form-error">{error}</p>}
|
|
1287
|
-
</div>
|
|
1288
|
-
);
|
|
1289
|
-
|
|
1290
|
-
case "markdown":
|
|
519
|
+
case "array":
|
|
1291
520
|
return (
|
|
1292
|
-
<
|
|
521
|
+
<ArrayLayout
|
|
1293
522
|
key={field.name}
|
|
1294
|
-
field={field
|
|
1295
|
-
value={value
|
|
1296
|
-
onChange={
|
|
523
|
+
field={field}
|
|
524
|
+
value={value as unknown[]}
|
|
525
|
+
onChange={onFieldChange}
|
|
526
|
+
renderField={renderField}
|
|
1297
527
|
disabled={disabled}
|
|
1298
528
|
/>
|
|
1299
529
|
);
|
|
1300
530
|
|
|
531
|
+
|
|
1301
532
|
case "button": {
|
|
1302
|
-
const
|
|
533
|
+
const fieldName = field.name as string;
|
|
534
|
+
const isLoading = loadingFields[fieldName];
|
|
1303
535
|
return (
|
|
1304
|
-
<div key={
|
|
536
|
+
<div key={fieldName} className="kyro-form-field">
|
|
1305
537
|
<button
|
|
1306
538
|
type="button"
|
|
1307
539
|
disabled={isLoading || disabled}
|
|
1308
540
|
onClick={async () => {
|
|
1309
|
-
const action = field.admin?.action || (field as
|
|
541
|
+
const action = (field.admin?.action || (field as Record<string, unknown>).action) as string | undefined;
|
|
1310
542
|
const method =
|
|
1311
|
-
field.admin?.method || (field as
|
|
543
|
+
(field.admin?.method || (field as Record<string, unknown>).method || "POST") as string;
|
|
1312
544
|
if (action) {
|
|
1313
545
|
setLoadingFields((prev) => ({
|
|
1314
546
|
...prev,
|
|
1315
|
-
[
|
|
547
|
+
[fieldName]: true,
|
|
1316
548
|
}));
|
|
1317
549
|
try {
|
|
1318
|
-
const response = await
|
|
550
|
+
const response = await fetchWithAuth(action, {
|
|
1319
551
|
method,
|
|
1320
552
|
headers: { "Content-Type": "application/json" },
|
|
1321
553
|
body: JSON.stringify(formData),
|
|
1322
554
|
});
|
|
1323
|
-
|
|
555
|
+
await response.json();
|
|
1324
556
|
if (response.ok) {
|
|
1325
557
|
// handle result
|
|
1326
558
|
} else {
|
|
@@ -1331,7 +563,7 @@ export function AutoForm({
|
|
|
1331
563
|
} finally {
|
|
1332
564
|
setLoadingFields((prev) => ({
|
|
1333
565
|
...prev,
|
|
1334
|
-
[
|
|
566
|
+
[fieldName]: false,
|
|
1335
567
|
}));
|
|
1336
568
|
}
|
|
1337
569
|
}
|
|
@@ -1366,59 +598,76 @@ export function AutoForm({
|
|
|
1366
598
|
);
|
|
1367
599
|
}
|
|
1368
600
|
|
|
1369
|
-
case "relationship":
|
|
601
|
+
case "relationship-block":
|
|
1370
602
|
return (
|
|
1371
|
-
<
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
603
|
+
<div key={field.name} className="kyro-form-field">
|
|
604
|
+
<label className="kyro-form-label">
|
|
605
|
+
{field.label || field.name}
|
|
606
|
+
{field.required && (
|
|
607
|
+
<span className="kyro-form-label-required">*</span>
|
|
608
|
+
)}
|
|
609
|
+
</label>
|
|
610
|
+
<RelationshipBlockField
|
|
611
|
+
relationTo={field.relationTo as string}
|
|
612
|
+
hasMany={field.hasMany as boolean}
|
|
613
|
+
selectedIds={Array.isArray(value) ? value : value ? [value] : []}
|
|
614
|
+
onChange={(_field: string, newValue: unknown) => {
|
|
615
|
+
onFieldChange(newValue);
|
|
616
|
+
}}
|
|
617
|
+
compact
|
|
618
|
+
/>
|
|
619
|
+
{field.admin?.description ? (
|
|
620
|
+
<p className="kyro-form-help">{String(field.admin?.description)}</p>
|
|
621
|
+
) : null}
|
|
622
|
+
</div>
|
|
1391
623
|
);
|
|
1392
624
|
|
|
1393
|
-
|
|
1394
|
-
case "image":
|
|
1395
|
-
case "upload":
|
|
625
|
+
default:
|
|
1396
626
|
return (
|
|
1397
|
-
<
|
|
1398
|
-
key={field.name}
|
|
1399
|
-
field={field
|
|
627
|
+
<FieldRenderer
|
|
628
|
+
key={field.name || Math.random().toString()}
|
|
629
|
+
field={field}
|
|
1400
630
|
value={value}
|
|
1401
|
-
onChange={
|
|
631
|
+
onChange={onFieldChange}
|
|
632
|
+
error={error}
|
|
1402
633
|
disabled={disabled}
|
|
1403
634
|
/>
|
|
1404
635
|
);
|
|
1405
|
-
|
|
1406
|
-
default:
|
|
1407
|
-
return null;
|
|
1408
636
|
}
|
|
1409
637
|
};
|
|
1410
638
|
|
|
1411
639
|
const renderHeader = () => {
|
|
1412
|
-
const docTitle =
|
|
1413
|
-
|
|
640
|
+
const docTitle = String(
|
|
641
|
+
(formData.mainTabs as { title?: string })?.title ||
|
|
642
|
+
formData.title ||
|
|
643
|
+
formData.name ||
|
|
644
|
+
"Untitled",
|
|
645
|
+
);
|
|
646
|
+
// Use _status from the document (set by the new draft/publish system)
|
|
647
|
+
const docStatus = documentStatus ?? formData._status ?? formData.status ?? 'draft';
|
|
1414
648
|
const isNew = !formData.id;
|
|
1415
649
|
const lastModified = formData.updatedAt
|
|
1416
|
-
? new Date(formData.updatedAt).toLocaleString()
|
|
650
|
+
? new Date(formData.updatedAt as string).toLocaleString()
|
|
1417
651
|
: "Just now";
|
|
1418
652
|
const createdAt = formData.createdAt
|
|
1419
|
-
? new Date(formData.createdAt).toLocaleString()
|
|
653
|
+
? new Date(formData.createdAt as string).toLocaleString()
|
|
1420
654
|
: "Just now";
|
|
1421
655
|
|
|
656
|
+
// Status label shown in the header
|
|
657
|
+
const statusLabel = hasUnpublishedChanges
|
|
658
|
+
? docStatus === 'draft' && !formData._prevStatus
|
|
659
|
+
? 'Draft'
|
|
660
|
+
: 'Published (unpublished changes)'
|
|
661
|
+
: docStatus === 'published'
|
|
662
|
+
? 'Published'
|
|
663
|
+
: 'Draft';
|
|
664
|
+
|
|
665
|
+
const statusColor = docStatus === 'published' && !hasUnpublishedChanges
|
|
666
|
+
? 'bg-[var(--kyro-success)]'
|
|
667
|
+
: hasUnpublishedChanges
|
|
668
|
+
? 'bg-[var(--kyro-warning)]'
|
|
669
|
+
: 'bg-[var(--kyro-text-muted)]';
|
|
670
|
+
|
|
1422
671
|
return (
|
|
1423
672
|
<header className="surface-tile px-8 py-6 flex items-center justify-between sticky top-0 z-50 border-b border-[var(--kyro-border)] mb-8 bg-[var(--kyro-surface)] backdrop-blur-md">
|
|
1424
673
|
<div className="flex flex-col gap-1">
|
|
@@ -1446,9 +695,9 @@ export function AutoForm({
|
|
|
1446
695
|
<div className="flex items-center gap-4 text-[11px] font-medium tracking-wide opacity-60 ml-12">
|
|
1447
696
|
<span className="flex items-center gap-1.5 capitalize">
|
|
1448
697
|
<span
|
|
1449
|
-
className={`h-1.5 w-1.5 rounded-full ${
|
|
698
|
+
className={`h-1.5 w-1.5 rounded-full ${statusColor}`}
|
|
1450
699
|
/>
|
|
1451
|
-
{
|
|
700
|
+
{statusLabel}
|
|
1452
701
|
</span>
|
|
1453
702
|
{autoSaveStatus === "saving" && (
|
|
1454
703
|
<span className="flex items-center gap-1.5 text-[var(--kyro-text-muted)]">
|
|
@@ -1471,10 +720,10 @@ export function AutoForm({
|
|
|
1471
720
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
1472
721
|
/>
|
|
1473
722
|
</svg>
|
|
1474
|
-
Saving...
|
|
723
|
+
Saving draft...
|
|
1475
724
|
</span>
|
|
1476
725
|
)}
|
|
1477
|
-
{autoSaveStatus === "
|
|
726
|
+
{autoSaveStatus === "success" && (
|
|
1478
727
|
<span className="text-[var(--kyro-success)] flex items-center gap-1">
|
|
1479
728
|
<svg
|
|
1480
729
|
width="12"
|
|
@@ -1486,18 +735,25 @@ export function AutoForm({
|
|
|
1486
735
|
>
|
|
1487
736
|
<path d="M20 6L9 17l-5-5" />
|
|
1488
737
|
</svg>
|
|
1489
|
-
|
|
738
|
+
Draft saved
|
|
1490
739
|
</span>
|
|
1491
740
|
)}
|
|
1492
741
|
{autoSaveStatus === "error" && (
|
|
1493
|
-
<span className="text-[var(--kyro-danger)]">
|
|
742
|
+
<span className="text-[var(--kyro-danger)]">Draft save failed</span>
|
|
743
|
+
)}
|
|
744
|
+
{autoSaveStatus === "conflict" && (
|
|
745
|
+
<span className="text-[var(--kyro-danger)]">Conflict detected</span>
|
|
1494
746
|
)}
|
|
1495
747
|
{hasUnsavedChanges && autoSaveStatus !== "saving" && (
|
|
1496
748
|
<>
|
|
1497
749
|
<span className="opacity-30">—</span>
|
|
1498
750
|
<button
|
|
1499
751
|
type="button"
|
|
1500
|
-
onClick={() =>
|
|
752
|
+
onClick={async () => {
|
|
753
|
+
setFormData(lastSavedData);
|
|
754
|
+
markSaved();
|
|
755
|
+
await clearDraftArtifacts();
|
|
756
|
+
}}
|
|
1501
757
|
className="text-[var(--kyro-primary)] hover:underline"
|
|
1502
758
|
>
|
|
1503
759
|
Revert changes
|
|
@@ -1519,8 +775,8 @@ export function AutoForm({
|
|
|
1519
775
|
<button
|
|
1520
776
|
key={v}
|
|
1521
777
|
type="button"
|
|
1522
|
-
onClick={() => setView(v as
|
|
1523
|
-
className={`px-5 py-2 text-xs font-
|
|
778
|
+
onClick={() => setView(v as View)}
|
|
779
|
+
className={`px-5 py-2 text-xs font-bold rounded-lg transition-all ${view === v ? "bg-[var(--kyro-surface)] shadow-sm border border-[var(--kyro-border)] text-[var(--kyro-text-primary)]" : "text-[var(--kyro-text-secondary)] opacity-50 hover:opacity-100"}`}
|
|
1524
780
|
>
|
|
1525
781
|
{v.toUpperCase()}
|
|
1526
782
|
</button>
|
|
@@ -1547,7 +803,7 @@ export function AutoForm({
|
|
|
1547
803
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3" />
|
|
1548
804
|
</svg>
|
|
1549
805
|
{showPreview && (
|
|
1550
|
-
<span className="text-[10px] font-
|
|
806
|
+
<span className="text-[10px] font-bold tracking-widest pr-1">
|
|
1551
807
|
Active
|
|
1552
808
|
</span>
|
|
1553
809
|
)}
|
|
@@ -1577,6 +833,7 @@ export function AutoForm({
|
|
|
1577
833
|
id="btn-save"
|
|
1578
834
|
type="button"
|
|
1579
835
|
onClick={async () => {
|
|
836
|
+
autoSaveSkipRef.current = true;
|
|
1580
837
|
const hiddenInput = document.getElementById(
|
|
1581
838
|
"form-data",
|
|
1582
839
|
) as HTMLInputElement;
|
|
@@ -1593,48 +850,55 @@ export function AutoForm({
|
|
|
1593
850
|
|
|
1594
851
|
try {
|
|
1595
852
|
const data = JSON.parse(hiddenInput.value);
|
|
1596
|
-
const
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
body: JSON.stringify(data),
|
|
1606
|
-
});
|
|
853
|
+
const isPost = isNew && !globalSlug;
|
|
854
|
+
|
|
855
|
+
const response = isPost
|
|
856
|
+
? await fetchWithAuth(`/api/${collectionSlug}`, {
|
|
857
|
+
method: "POST",
|
|
858
|
+
headers: { "Content-Type": "application/json" },
|
|
859
|
+
body: JSON.stringify(data),
|
|
860
|
+
})
|
|
861
|
+
: await saveDocument(data);
|
|
1607
862
|
|
|
1608
863
|
if (response.ok) {
|
|
1609
864
|
const result = await response.json();
|
|
1610
|
-
|
|
865
|
+
setFormData(result.data || data);
|
|
866
|
+
setLastSavedData(result.data || data);
|
|
1611
867
|
lastAutoSaveTimeRef.current = Date.now();
|
|
1612
|
-
setAutoSaveStatus("
|
|
1613
|
-
|
|
868
|
+
setAutoSaveStatus("success");
|
|
869
|
+
await clearDraftArtifacts();
|
|
870
|
+
if (versionsEnabled) fetchVersions();
|
|
1614
871
|
setTimeout(() => setAutoSaveStatus("idle"), 2000);
|
|
1615
872
|
onActionSuccess?.(
|
|
1616
|
-
|
|
873
|
+
isPost ? "Document created successfully" : "Changes saved",
|
|
1617
874
|
);
|
|
1618
|
-
if (
|
|
875
|
+
if (globalSlug) {
|
|
876
|
+
setTimeout(() => {
|
|
877
|
+
window.location.reload();
|
|
878
|
+
}, 1000);
|
|
879
|
+
}
|
|
880
|
+
if (isPost) {
|
|
1619
881
|
setTimeout(() => {
|
|
1620
882
|
window.location.href = `/${collectionSlug}`;
|
|
1621
883
|
}, 800);
|
|
1622
884
|
}
|
|
1623
885
|
} else {
|
|
1624
886
|
const error = await response.json();
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
887
|
+
if (response.status === 409) {
|
|
888
|
+
setAutoSaveStatus("conflict");
|
|
889
|
+
}
|
|
890
|
+
alert({
|
|
891
|
+
title: response.status === 409 ? "Conflict detected" : "Error",
|
|
1628
892
|
message: error.error || "Failed to save",
|
|
1629
893
|
});
|
|
1630
894
|
}
|
|
1631
895
|
} catch (err) {
|
|
1632
|
-
|
|
1633
|
-
open: true,
|
|
896
|
+
alert({
|
|
1634
897
|
title: "Error",
|
|
1635
898
|
message: "Failed to save document",
|
|
1636
899
|
});
|
|
1637
900
|
} finally {
|
|
901
|
+
autoSaveSkipRef.current = false;
|
|
1638
902
|
if (btn) {
|
|
1639
903
|
btn.textContent = originalText;
|
|
1640
904
|
btn.removeAttribute("disabled");
|
|
@@ -1643,14 +907,15 @@ export function AutoForm({
|
|
|
1643
907
|
}}
|
|
1644
908
|
className="kyro-btn kyro-btn-primary px-6 py-2.5 text-xs rounded-xl shadow-lg transition-all"
|
|
1645
909
|
>
|
|
1646
|
-
{isNew ? "Create" : hasUnsavedChanges ? "Save Draft" : "Saved"}
|
|
910
|
+
{isNew ? (globalSlug ? "Save" : "Create") : hasUnsavedChanges ? (versionsEnabled ? "Save Draft" : "Save") : "Saved"}
|
|
1647
911
|
</button>
|
|
1648
912
|
|
|
1649
|
-
{!isNew &&
|
|
913
|
+
{!isNew && versionsEnabled && documentStatus === "draft" && (
|
|
1650
914
|
<button
|
|
1651
915
|
id="btn-publish"
|
|
1652
916
|
type="button"
|
|
1653
917
|
onClick={async () => {
|
|
918
|
+
autoSaveSkipRef.current = true;
|
|
1654
919
|
const btn = document.getElementById(
|
|
1655
920
|
"btn-publish",
|
|
1656
921
|
) as HTMLButtonElement;
|
|
@@ -1661,32 +926,51 @@ export function AutoForm({
|
|
|
1661
926
|
}
|
|
1662
927
|
|
|
1663
928
|
try {
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
{
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
929
|
+
if (hasUnsavedChanges) {
|
|
930
|
+
const saveResponse = await saveDocument(formData);
|
|
931
|
+
if (!saveResponse.ok) {
|
|
932
|
+
const saveError = await saveResponse.json().catch(() => ({}));
|
|
933
|
+
if (saveResponse.status === 409) {
|
|
934
|
+
setAutoSaveStatus("conflict");
|
|
935
|
+
}
|
|
936
|
+
alert({
|
|
937
|
+
title:
|
|
938
|
+
saveResponse.status === 409
|
|
939
|
+
? "Conflict detected"
|
|
940
|
+
: "Error",
|
|
941
|
+
message: saveError.error || "Failed to save latest draft before publishing",
|
|
942
|
+
});
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const saveResult = await saveResponse.json();
|
|
947
|
+
setFormData(saveResult.data || formData);
|
|
948
|
+
setLastSavedData(saveResult.data || formData);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const response = await publishDocument();
|
|
1671
952
|
|
|
1672
953
|
if (response.ok) {
|
|
954
|
+
await clearDraftArtifacts();
|
|
1673
955
|
onActionSuccess?.("Published successfully");
|
|
1674
956
|
location.reload();
|
|
1675
957
|
} else {
|
|
1676
958
|
const error = await response.json();
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
959
|
+
if (response.status === 409) {
|
|
960
|
+
setAutoSaveStatus("conflict");
|
|
961
|
+
}
|
|
962
|
+
alert({
|
|
963
|
+
title: response.status === 409 ? "Conflict detected" : "Error",
|
|
1680
964
|
message: error.error || "Failed to publish",
|
|
1681
965
|
});
|
|
1682
966
|
}
|
|
1683
967
|
} catch (err) {
|
|
1684
|
-
|
|
1685
|
-
open: true,
|
|
968
|
+
alert({
|
|
1686
969
|
title: "Error",
|
|
1687
970
|
message: "Failed to publish",
|
|
1688
971
|
});
|
|
1689
972
|
} finally {
|
|
973
|
+
autoSaveSkipRef.current = false;
|
|
1690
974
|
if (btn) {
|
|
1691
975
|
btn.textContent = originalText;
|
|
1692
976
|
btn.removeAttribute("disabled");
|
|
@@ -1695,7 +979,7 @@ export function AutoForm({
|
|
|
1695
979
|
}}
|
|
1696
980
|
className="px-6 py-2.5 text-xs font-bold rounded-xl border-2 border-[var(--kyro-border)] text-[var(--kyro-text-primary)] hover:border-[var(--kyro-primary)] hover:bg-[var(--kyro-primary)] hover:text-white transition-all"
|
|
1697
981
|
>
|
|
1698
|
-
Publish
|
|
982
|
+
{formData._prevStatus === 'published' ? 'Publish Changes' : 'Publish'}
|
|
1699
983
|
</button>
|
|
1700
984
|
)}
|
|
1701
985
|
|
|
@@ -1771,7 +1055,7 @@ export function AutoForm({
|
|
|
1771
1055
|
</svg>
|
|
1772
1056
|
Duplicate
|
|
1773
1057
|
</button>
|
|
1774
|
-
{
|
|
1058
|
+
{formData._status === "published" && (
|
|
1775
1059
|
<button
|
|
1776
1060
|
type="button"
|
|
1777
1061
|
onClick={() => {
|
|
@@ -1833,7 +1117,7 @@ export function AutoForm({
|
|
|
1833
1117
|
return (
|
|
1834
1118
|
<div className="w-full space-y-8">
|
|
1835
1119
|
<div className="surface-tile p-8 space-y-8">
|
|
1836
|
-
{config.fields.map((f) => renderField(f))}
|
|
1120
|
+
{config.fields.map((f: Field) => renderField(f))}
|
|
1837
1121
|
</div>
|
|
1838
1122
|
</div>
|
|
1839
1123
|
);
|
|
@@ -1842,29 +1126,28 @@ export function AutoForm({
|
|
|
1842
1126
|
// Default split layout
|
|
1843
1127
|
const showRightColumn = !sidebarCollapsed && !showPreview;
|
|
1844
1128
|
const hasSidebarFields =
|
|
1845
|
-
config.fields.some((f) => f.admin?.position === "sidebar") &&
|
|
1129
|
+
config.fields.some((f: Field) => f.admin?.position === "sidebar") &&
|
|
1846
1130
|
!showPreview;
|
|
1847
1131
|
|
|
1848
1132
|
return (
|
|
1849
1133
|
<div
|
|
1850
|
-
className={`w-full mx-auto grid gap-8 pb-32 transition-all duration-700 ${
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
}`}
|
|
1134
|
+
className={`w-full mx-auto grid gap-8 pb-32 transition-all duration-700 ${showPreview
|
|
1135
|
+
? "grid-cols-1 lg:grid-cols-2"
|
|
1136
|
+
: sidebarCollapsed || !hasSidebarFields
|
|
1137
|
+
? "grid-cols-1"
|
|
1138
|
+
: "grid-cols-1 lg:grid-cols-[1fr_380px]"
|
|
1139
|
+
}`}
|
|
1857
1140
|
>
|
|
1858
1141
|
<div className="space-y-8 animate-in fade-in slide-in-from-left-4 duration-500">
|
|
1859
1142
|
{config.tabs ? (
|
|
1860
|
-
renderField({ type: "tabs", tabs: config.tabs } as
|
|
1143
|
+
renderField({ type: "tabs", tabs: config.tabs } as Field)
|
|
1861
1144
|
) : (
|
|
1862
1145
|
<div className="surface-tile p-8 space-y-8">
|
|
1863
1146
|
{config.fields
|
|
1864
1147
|
.filter(
|
|
1865
|
-
(f) => !f.admin?.position || f.admin.position === "main",
|
|
1148
|
+
(f: Field) => !f.admin?.position || f.admin.position === "main",
|
|
1866
1149
|
)
|
|
1867
|
-
.map((f) => renderField(f))}
|
|
1150
|
+
.map((f: Field) => renderField(f))}
|
|
1868
1151
|
</div>
|
|
1869
1152
|
)}
|
|
1870
1153
|
</div>
|
|
@@ -1874,7 +1157,7 @@ export function AutoForm({
|
|
|
1874
1157
|
<div className="w-full h-full rounded-3xl border border-[var(--kyro-border)] bg-[var(--kyro-bg-secondary)] shadow-2xl overflow-hidden relative group">
|
|
1875
1158
|
<div className="absolute top-4 left-4 z-10 flex items-center gap-2">
|
|
1876
1159
|
<div className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
|
|
1877
|
-
<span className="text-[10px] font-
|
|
1160
|
+
<span className="text-[10px] font-bold tracking-widest text-white/60">
|
|
1878
1161
|
Live Preview Mode
|
|
1879
1162
|
</span>
|
|
1880
1163
|
</div>
|
|
@@ -1888,14 +1171,14 @@ export function AutoForm({
|
|
|
1888
1171
|
</div>
|
|
1889
1172
|
) : sidebarCollapsed ? null : (
|
|
1890
1173
|
<div className="space-y-6 animate-in fade-in slide-in-from-right-4 duration-500">
|
|
1891
|
-
{config.fields.some((f) => f.admin?.position === "sidebar") && (
|
|
1174
|
+
{config.fields.some((f: Field) => f.admin?.position === "sidebar") && (
|
|
1892
1175
|
<div className="surface-tile p-6 space-y-6">
|
|
1893
|
-
<h3 className="text-[10px] font-
|
|
1176
|
+
<h3 className="text-[10px] font-bold tracking-[0.2em] opacity-40">
|
|
1894
1177
|
Settings
|
|
1895
1178
|
</h3>
|
|
1896
1179
|
{config.fields
|
|
1897
|
-
.filter((f) => f.admin?.position === "sidebar")
|
|
1898
|
-
.map((f) => renderField(f))}
|
|
1180
|
+
.filter((f: Field) => f.admin?.position === "sidebar")
|
|
1181
|
+
.map((f: Field) => renderField(f))}
|
|
1899
1182
|
</div>
|
|
1900
1183
|
)}
|
|
1901
1184
|
</div>
|
|
@@ -1924,7 +1207,7 @@ export function AutoForm({
|
|
|
1924
1207
|
type="button"
|
|
1925
1208
|
onClick={handleCompareVersions}
|
|
1926
1209
|
disabled={loadingDiffs}
|
|
1927
|
-
className="px-3 py-1.5 rounded-lg bg-[var(--kyro-primary)] text-white text-[11px] font-bold
|
|
1210
|
+
className="px-3 py-1.5 rounded-lg bg-[var(--kyro-primary)] text-white text-[11px] font-bold tracking-wider hover:opacity-90 disabled:opacity-50"
|
|
1928
1211
|
>
|
|
1929
1212
|
{loadingDiffs ? "Comparing..." : "Compare"}
|
|
1930
1213
|
</button>
|
|
@@ -1936,11 +1219,10 @@ export function AutoForm({
|
|
|
1936
1219
|
setCompareSelected([]);
|
|
1937
1220
|
setCompareDiffs([]);
|
|
1938
1221
|
}}
|
|
1939
|
-
className={`px-3 py-1.5 rounded-lg text-[11px] font-bold
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
}`}
|
|
1222
|
+
className={`px-3 py-1.5 rounded-lg text-[11px] font-bold tracking-wider transition-all ${compareMode
|
|
1223
|
+
? "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)]"
|
|
1224
|
+
: "border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)]"
|
|
1225
|
+
}`}
|
|
1944
1226
|
>
|
|
1945
1227
|
{compareMode ? "Done" : "Compare"}
|
|
1946
1228
|
</button>
|
|
@@ -1950,7 +1232,7 @@ export function AutoForm({
|
|
|
1950
1232
|
{compareDiffs.length > 0 && (
|
|
1951
1233
|
<div className="border-b border-[var(--kyro-border)]">
|
|
1952
1234
|
<div className="px-6 py-3 flex items-center justify-between">
|
|
1953
|
-
<span className="text-[11px] font-bold text-[var(--kyro-text-primary)]
|
|
1235
|
+
<span className="text-[11px] font-bold text-[var(--kyro-text-primary)] tracking-wider">
|
|
1954
1236
|
{compareDiffs.length} change
|
|
1955
1237
|
{compareDiffs.length !== 1 ? "s" : ""}
|
|
1956
1238
|
</span>
|
|
@@ -2018,22 +1300,20 @@ export function AutoForm({
|
|
|
2018
1300
|
onClick={
|
|
2019
1301
|
compareMode ? () => toggleCompareSelection(v.id) : undefined
|
|
2020
1302
|
}
|
|
2021
|
-
className={`grid grid-cols-12 gap-3 px-6 py-3 items-center transition-all ${
|
|
2022
|
-
|
|
2023
|
-
?
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
} ${isDraftVersion ? "" : ""}`}
|
|
1303
|
+
className={`grid grid-cols-12 gap-3 px-6 py-3 items-center transition-all ${compareMode
|
|
1304
|
+
? isSelected
|
|
1305
|
+
? "bg-[var(--kyro-primary)]/5 cursor-pointer"
|
|
1306
|
+
: "hover:bg-[var(--kyro-bg-secondary)] cursor-pointer"
|
|
1307
|
+
: "hover:bg-[var(--kyro-bg-secondary)]"
|
|
1308
|
+
} ${isDraftVersion ? "" : ""}`}
|
|
2028
1309
|
>
|
|
2029
1310
|
<div className="col-span-1 flex items-center gap-2">
|
|
2030
1311
|
{compareMode ? (
|
|
2031
1312
|
<div
|
|
2032
|
-
className={`w-4 h-4 rounded-full border ${
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
}`}
|
|
1313
|
+
className={`w-4 h-4 rounded-full border ${isSelected
|
|
1314
|
+
? "border-[var(--kyro-primary)] bg-[var(--kyro-primary)]"
|
|
1315
|
+
: "border-[var(--kyro-border)]"
|
|
1316
|
+
}`}
|
|
2037
1317
|
>
|
|
2038
1318
|
{isSelected && (
|
|
2039
1319
|
<svg
|
|
@@ -2057,13 +1337,13 @@ export function AutoForm({
|
|
|
2057
1337
|
<div className="text-[13px] font-medium text-[var(--kyro-text-primary)] truncate flex items-center gap-2">
|
|
2058
1338
|
{v.changeDescription || "Snapshot"}
|
|
2059
1339
|
{isAutoSaved && (
|
|
2060
|
-
<span className="text-[9px] px-1.5 py-0.5 bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-secondary)] rounded font-bold
|
|
1340
|
+
<span className="text-[9px] px-1.5 py-0.5 bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-secondary)] rounded font-bold tracking-wider">
|
|
2061
1341
|
Auto
|
|
2062
1342
|
</span>
|
|
2063
1343
|
)}
|
|
2064
1344
|
</div>
|
|
2065
1345
|
<div className="text-[11px] text-[var(--kyro-text-muted)]">
|
|
2066
|
-
{new Date(v.createdAt).toLocaleString("en-US", {
|
|
1346
|
+
{new Date(v.createdAt as string).toLocaleString("en-US", {
|
|
2067
1347
|
month: "short",
|
|
2068
1348
|
day: "numeric",
|
|
2069
1349
|
hour: "2-digit",
|
|
@@ -2074,11 +1354,10 @@ export function AutoForm({
|
|
|
2074
1354
|
<div className="col-span-3">
|
|
2075
1355
|
{v.status && (
|
|
2076
1356
|
<span
|
|
2077
|
-
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-bold capitalize tracking-wider ${
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
}`}
|
|
1357
|
+
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-bold capitalize tracking-wider ${v.status === "published"
|
|
1358
|
+
? " text-[var(--kyro-success)]"
|
|
1359
|
+
: " text-[var(--kyro-warning)]"
|
|
1360
|
+
}`}
|
|
2082
1361
|
>
|
|
2083
1362
|
<span
|
|
2084
1363
|
className={`w-1.5 h-1.5 rounded-full ${v.status === "published" ? "bg-[var(--kyro-success)]" : "bg-[var(--kyro-warning)]"}`}
|
|
@@ -2095,7 +1374,7 @@ export function AutoForm({
|
|
|
2095
1374
|
<button
|
|
2096
1375
|
type="button"
|
|
2097
1376
|
onClick={() => handleRestoreVersion(v.id)}
|
|
2098
|
-
className="px-3 py-1.5 rounded-lg border border-[var(--kyro-border)] text-[11px] font-bold
|
|
1377
|
+
className="px-3 py-1.5 rounded-lg border border-[var(--kyro-border)] text-[11px] font-bold tracking-wider text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-primary)] hover:text-white hover:border-[var(--kyro-primary)] transition-all active:scale-95"
|
|
2099
1378
|
>
|
|
2100
1379
|
Restore
|
|
2101
1380
|
</button>
|
|
@@ -2114,7 +1393,7 @@ export function AutoForm({
|
|
|
2114
1393
|
<div className="w-full space-y-8 animate-in fade-in slide-in-from-bottom-4">
|
|
2115
1394
|
<div className="grid grid-cols-1 lg:grid-cols-[1fr_300px] gap-8">
|
|
2116
1395
|
<div className="surface-tile p-8 min-w-0">
|
|
2117
|
-
<h2 className="text-xl font-
|
|
1396
|
+
<h2 className="text-xl font-bold mb-6">Response Payload</h2>
|
|
2118
1397
|
<div className="bg-[#0f172a] p-6 rounded-2xl border border-white/5 overflow-x-auto max-h-[800px]">
|
|
2119
1398
|
<pre className="text-blue-300 text-xs font-mono whitespace-pre-wrap break-all">
|
|
2120
1399
|
{JSON.stringify(formData, null, 2)}
|
|
@@ -2124,11 +1403,11 @@ export function AutoForm({
|
|
|
2124
1403
|
|
|
2125
1404
|
<div className="space-y-6">
|
|
2126
1405
|
<div className="surface-tile p-8 space-y-6">
|
|
2127
|
-
<h2 className="text-xl font-
|
|
1406
|
+
<h2 className="text-xl font-bold mb-6">API Info</h2>
|
|
2128
1407
|
|
|
2129
1408
|
<div className="space-y-6">
|
|
2130
1409
|
<div>
|
|
2131
|
-
<label className="text-[10px] font-bold
|
|
1410
|
+
<label className="text-[10px] font-bold tracking-[0.1em] text-[var(--kyro-text-secondary)] opacity-50 block mb-2">
|
|
2132
1411
|
Reference Path
|
|
2133
1412
|
</label>
|
|
2134
1413
|
<div className="relative group">
|
|
@@ -2139,24 +1418,24 @@ export function AutoForm({
|
|
|
2139
1418
|
</div>
|
|
2140
1419
|
|
|
2141
1420
|
<div>
|
|
2142
|
-
<label className="text-[10px] font-bold
|
|
1421
|
+
<label className="text-[10px] font-bold tracking-[0.1em] text-[var(--kyro-text-secondary)] opacity-50 block mb-3">
|
|
2143
1422
|
Methods Allowed
|
|
2144
1423
|
</label>
|
|
2145
1424
|
<div className="flex gap-2">
|
|
2146
|
-
<span className="px-3 py-1.5 bg-green-500/10 text-green-500 rounded-lg font-
|
|
1425
|
+
<span className="px-3 py-1.5 bg-green-500/10 text-green-500 rounded-lg font-bold text-[9px] tracking-wider">
|
|
2147
1426
|
GET
|
|
2148
1427
|
</span>
|
|
2149
|
-
<span className="px-3 py-1.5 bg-amber-500/10 text-amber-500 rounded-lg font-
|
|
1428
|
+
<span className="px-3 py-1.5 bg-amber-500/10 text-amber-500 rounded-lg font-bold text-[9px] tracking-wider">
|
|
2150
1429
|
PATCH
|
|
2151
1430
|
</span>
|
|
2152
|
-
<span className="px-3 py-1.5 bg-red-500/10 text-red-500 rounded-lg font-
|
|
1431
|
+
<span className="px-3 py-1.5 bg-red-500/10 text-red-500 rounded-lg font-bold text-[9px] tracking-wider">
|
|
2153
1432
|
DELETE
|
|
2154
1433
|
</span>
|
|
2155
1434
|
</div>
|
|
2156
1435
|
</div>
|
|
2157
1436
|
|
|
2158
1437
|
<div className="pt-6 border-t border-[var(--kyro-border)]">
|
|
2159
|
-
<label className="text-[10px] font-bold
|
|
1438
|
+
<label className="text-[10px] font-bold tracking-[0.1em] text-[var(--kyro-text-secondary)] opacity-50 block mb-4">
|
|
2160
1439
|
Security Policy
|
|
2161
1440
|
</label>
|
|
2162
1441
|
<div className="space-y-3">
|
|
@@ -2206,7 +1485,7 @@ export function AutoForm({
|
|
|
2206
1485
|
</div>
|
|
2207
1486
|
|
|
2208
1487
|
<div className="pt-6 border-t border-[var(--kyro-border)]">
|
|
2209
|
-
<label className="text-[10px] font-bold
|
|
1488
|
+
<label className="text-[10px] font-bold tracking-[0.1em] text-[var(--kyro-text-secondary)] opacity-50 block mb-2">
|
|
2210
1489
|
Usage Help
|
|
2211
1490
|
</label>
|
|
2212
1491
|
<p className="text-[11px] text-[var(--kyro-text-secondary)] leading-relaxed">
|
|
@@ -2227,49 +1506,57 @@ export function AutoForm({
|
|
|
2227
1506
|
return (
|
|
2228
1507
|
<div className="flex flex-col h-full">
|
|
2229
1508
|
{layout !== "single" && renderHeader()}
|
|
1509
|
+
{layout === "single" && (
|
|
1510
|
+
<button
|
|
1511
|
+
id="btn-save"
|
|
1512
|
+
type="button"
|
|
1513
|
+
style={{ width: 0, height: 0, opacity: 0, padding: 0, margin: 0, border: 'none', position: 'absolute' }}
|
|
1514
|
+
onClick={async () => {
|
|
1515
|
+
console.log("[AutoForm] Hidden save button clicked");
|
|
1516
|
+
const hiddenInput = document.getElementById("form-data") as HTMLInputElement;
|
|
1517
|
+
if (!hiddenInput || !hiddenInput.value) {
|
|
1518
|
+
console.error("[AutoForm] #form-data input not found or empty");
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
try {
|
|
1522
|
+
const data = JSON.parse(hiddenInput.value);
|
|
1523
|
+
console.log("[AutoForm] Saving data:", data);
|
|
1524
|
+
const response = await saveDocument(data);
|
|
1525
|
+
if (response.ok) {
|
|
1526
|
+
const result = await response.json();
|
|
1527
|
+
const savedData = result.data || data;
|
|
1528
|
+
setFormData(savedData);
|
|
1529
|
+
setLastSavedData(savedData);
|
|
1530
|
+
onActionSuccess?.("Changes saved");
|
|
1531
|
+
// Trigger a refresh to ensure all global state is updated
|
|
1532
|
+
setTimeout(() => {
|
|
1533
|
+
window.location.reload();
|
|
1534
|
+
}, 1000); // Small delay to let the toast show
|
|
1535
|
+
} else {
|
|
1536
|
+
const errorData = await response.json().catch(() => ({}));
|
|
1537
|
+
console.error("Save global failed:", errorData);
|
|
1538
|
+
onActionError?.(errorData.error || "Save failed");
|
|
1539
|
+
}
|
|
1540
|
+
} catch (e) {
|
|
1541
|
+
console.error("Save error exception:", e);
|
|
1542
|
+
onActionError?.("Save failed: " + (e as Error).message);
|
|
1543
|
+
}
|
|
1544
|
+
}}
|
|
1545
|
+
/>
|
|
1546
|
+
)}
|
|
2230
1547
|
<main className="w-full">
|
|
2231
1548
|
{view === "edit" && renderEditView()}
|
|
2232
1549
|
{view === "version" && renderVersionView()}
|
|
2233
1550
|
{view === "api" && renderApiView()}
|
|
2234
1551
|
</main>
|
|
2235
|
-
<ConfirmModal
|
|
2236
|
-
open={confirmModal.open}
|
|
2237
|
-
onClose={() => setConfirmModal({ ...confirmModal, open: false })}
|
|
2238
|
-
onConfirm={() => {
|
|
2239
|
-
confirmModal.onConfirm();
|
|
2240
|
-
setConfirmModal({ ...confirmModal, open: false });
|
|
2241
|
-
}}
|
|
2242
|
-
title={confirmModal.title}
|
|
2243
|
-
message={confirmModal.message}
|
|
2244
|
-
variant={confirmModal.danger ? "danger" : "default"}
|
|
2245
|
-
/>
|
|
2246
|
-
<UIModal
|
|
2247
|
-
open={alertModal.open}
|
|
2248
|
-
onClose={() => setAlertModal({ ...alertModal, open: false })}
|
|
2249
|
-
title={alertModal.title}
|
|
2250
|
-
size="sm"
|
|
2251
|
-
footer={
|
|
2252
|
-
<button
|
|
2253
|
-
type="button"
|
|
2254
|
-
onClick={() => setAlertModal({ ...alertModal, open: false })}
|
|
2255
|
-
className="px-4 py-2 rounded-lg font-medium text-sm bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90 transition-colors"
|
|
2256
|
-
>
|
|
2257
|
-
OK
|
|
2258
|
-
</button>
|
|
2259
|
-
}
|
|
2260
|
-
>
|
|
2261
|
-
<p className="text-[var(--kyro-text-secondary)]">
|
|
2262
|
-
{alertModal.message}
|
|
2263
|
-
</p>
|
|
2264
|
-
</UIModal>
|
|
2265
1552
|
</div>
|
|
2266
1553
|
);
|
|
2267
1554
|
}
|
|
2268
1555
|
|
|
2269
1556
|
interface RelationshipFieldProps {
|
|
2270
|
-
field:
|
|
2271
|
-
value
|
|
2272
|
-
onChange
|
|
1557
|
+
field: Field;
|
|
1558
|
+
value?: unknown;
|
|
1559
|
+
onChange?: (value: unknown) => void;
|
|
2273
1560
|
disabled?: boolean;
|
|
2274
1561
|
error?: string;
|
|
2275
1562
|
}
|
|
@@ -2283,7 +1570,7 @@ function RelationshipField({
|
|
|
2283
1570
|
}: RelationshipFieldProps) {
|
|
2284
1571
|
const [isOpen, setIsOpen] = useState(false);
|
|
2285
1572
|
const [search, setSearch] = useState("");
|
|
2286
|
-
const [options, setOptions] = useState<
|
|
1573
|
+
const [options, setOptions] = useState<unknown[]>([]);
|
|
2287
1574
|
const [loading, setLoading] = useState(false);
|
|
2288
1575
|
|
|
2289
1576
|
const isMultiple = field.hasMany;
|
|
@@ -2293,10 +1580,10 @@ function RelationshipField({
|
|
|
2293
1580
|
|
|
2294
1581
|
const fetchOptions = () => {
|
|
2295
1582
|
setLoading(true);
|
|
2296
|
-
|
|
1583
|
+
fetchWithAuth(`/api/${targetCollection}?limit=50`)
|
|
2297
1584
|
.then((res) => res.json())
|
|
2298
1585
|
.then((data) => {
|
|
2299
|
-
setOptions(data.docs || []);
|
|
1586
|
+
setOptions((data.docs || []) as unknown[]);
|
|
2300
1587
|
setLoading(false);
|
|
2301
1588
|
})
|
|
2302
1589
|
.catch((err) => {
|
|
@@ -2309,38 +1596,43 @@ function RelationshipField({
|
|
|
2309
1596
|
fetchOptions();
|
|
2310
1597
|
}, [targetCollection]);
|
|
2311
1598
|
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
return
|
|
2315
|
-
|
|
1599
|
+
const getLabel = (opt: unknown) => {
|
|
1600
|
+
const o = opt as Record<string, unknown> | undefined;
|
|
1601
|
+
if (!o) return "";
|
|
1602
|
+
return String(
|
|
1603
|
+
o.title || o.name || o.label || o.filename || o.slug || o.id || "",
|
|
2316
1604
|
);
|
|
2317
1605
|
};
|
|
2318
1606
|
|
|
2319
|
-
const findOptionById = (id:
|
|
2320
|
-
return options
|
|
1607
|
+
const findOptionById = (id: unknown) => {
|
|
1608
|
+
return (options as Array<Record<string, unknown>>).find(
|
|
1609
|
+
(o) => o.id === id,
|
|
1610
|
+
);
|
|
2321
1611
|
};
|
|
2322
1612
|
|
|
2323
1613
|
const isSelected = (optId: string) => {
|
|
2324
1614
|
if (!value) return false;
|
|
2325
1615
|
if (isMultiple) {
|
|
2326
|
-
|
|
1616
|
+
const arr = Array.isArray(value) ? value : [];
|
|
1617
|
+
return (arr as Array<{ id?: string }>).some((v) => (v.id || v) === optId);
|
|
2327
1618
|
}
|
|
2328
|
-
return (value
|
|
1619
|
+
return ((value as { id?: string })?.id || value) === optId;
|
|
2329
1620
|
};
|
|
2330
1621
|
|
|
2331
|
-
const toggleSelection = (opt:
|
|
1622
|
+
const toggleSelection = (opt: { id?: string }) => {
|
|
2332
1623
|
if (isMultiple) {
|
|
2333
1624
|
const current = Array.isArray(value) ? value : [];
|
|
2334
|
-
|
|
2335
|
-
|
|
1625
|
+
const arr = current as Array<{ id?: string }>;
|
|
1626
|
+
if (isSelected(opt.id as string)) {
|
|
1627
|
+
onChange?.(arr.filter((item) => (item.id || item) !== opt.id));
|
|
2336
1628
|
} else {
|
|
2337
|
-
onChange([...
|
|
1629
|
+
onChange?.([...arr, opt.id]);
|
|
2338
1630
|
}
|
|
2339
1631
|
} else {
|
|
2340
|
-
if (isSelected(opt.id)) {
|
|
2341
|
-
onChange(null);
|
|
1632
|
+
if (isSelected(opt.id as string)) {
|
|
1633
|
+
onChange?.(null);
|
|
2342
1634
|
} else {
|
|
2343
|
-
onChange(opt.id);
|
|
1635
|
+
onChange?.(opt.id);
|
|
2344
1636
|
setIsOpen(false);
|
|
2345
1637
|
}
|
|
2346
1638
|
}
|
|
@@ -2350,28 +1642,30 @@ function RelationshipField({
|
|
|
2350
1642
|
if (!value) return null;
|
|
2351
1643
|
if (isMultiple && Array.isArray(value)) {
|
|
2352
1644
|
if (value.length === 0) return "None selected";
|
|
2353
|
-
|
|
1645
|
+
const arr = value as Array<{ id?: string }>;
|
|
1646
|
+
return arr
|
|
2354
1647
|
.map((v) => {
|
|
2355
1648
|
const id = v.id || v;
|
|
2356
1649
|
const opt = findOptionById(id);
|
|
2357
|
-
return opt ? getLabel(opt) : id;
|
|
1650
|
+
return opt ? getLabel(opt) : String(id);
|
|
2358
1651
|
})
|
|
2359
1652
|
.join(", ");
|
|
2360
1653
|
}
|
|
2361
|
-
const id = value.id || value;
|
|
1654
|
+
const id = (value as { id?: string }).id || value;
|
|
2362
1655
|
const opt = findOptionById(id);
|
|
2363
|
-
return opt ? getLabel(opt) : id;
|
|
1656
|
+
return opt ? getLabel(opt) : String(id);
|
|
2364
1657
|
};
|
|
2365
1658
|
|
|
2366
|
-
const filteredOptions = search
|
|
1659
|
+
const filteredOptions = (search
|
|
2367
1660
|
? (options || []).filter((opt) => {
|
|
1661
|
+
const o = opt as Record<string, unknown>;
|
|
2368
1662
|
const term = search.toLowerCase();
|
|
2369
1663
|
const searchableFields = ["title", "name", "label", "filename", "slug"];
|
|
2370
1664
|
return searchableFields.some(
|
|
2371
|
-
(key) =>
|
|
1665
|
+
(key) => o[key] && String(o[key]).toLowerCase().includes(term),
|
|
2372
1666
|
);
|
|
2373
1667
|
})
|
|
2374
|
-
: options || []
|
|
1668
|
+
: options || []) as Array<Record<string, unknown>>;
|
|
2375
1669
|
|
|
2376
1670
|
return (
|
|
2377
1671
|
<div className="kyro-form-field">
|
|
@@ -2405,9 +1699,9 @@ function RelationshipField({
|
|
|
2405
1699
|
</div>
|
|
2406
1700
|
</div>
|
|
2407
1701
|
|
|
2408
|
-
{field.admin?.description && !error
|
|
2409
|
-
<p className="kyro-form-help">{field.admin.description}</p>
|
|
2410
|
-
)}
|
|
1702
|
+
{(field.admin?.description && !error) ? (
|
|
1703
|
+
<p className="kyro-form-help">{String((field.admin as { description?: string }).description)}</p>
|
|
1704
|
+
) : null}
|
|
2411
1705
|
{error && <p className="kyro-form-error">{error}</p>}
|
|
2412
1706
|
|
|
2413
1707
|
{/* Modal */}
|
|
@@ -2437,19 +1731,22 @@ function RelationshipField({
|
|
|
2437
1731
|
No results found.
|
|
2438
1732
|
</div>
|
|
2439
1733
|
) : (
|
|
2440
|
-
filteredOptions.map((opt) =>
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
{
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
1734
|
+
filteredOptions.map((opt) => {
|
|
1735
|
+
const o = opt as { id?: string };
|
|
1736
|
+
return (
|
|
1737
|
+
<button
|
|
1738
|
+
key={String(o.id)}
|
|
1739
|
+
type="button"
|
|
1740
|
+
className={`kyro-relation-modal-item ${isSelected(String(o.id)) ? "selected" : ""}`}
|
|
1741
|
+
onClick={() => toggleSelection(o)}
|
|
1742
|
+
>
|
|
1743
|
+
<span>{getLabel(opt)}</span>
|
|
1744
|
+
<span className="kyro-relation-modal-item-id">
|
|
1745
|
+
{o.id ? `(${String(o.id).slice(0, 8)}...)` : ""}
|
|
1746
|
+
</span>
|
|
1747
|
+
</button>
|
|
1748
|
+
);
|
|
1749
|
+
})
|
|
2453
1750
|
)}
|
|
2454
1751
|
</div>
|
|
2455
1752
|
|
|
@@ -2474,35 +1771,3 @@ function stripHtml(html: string) {
|
|
|
2474
1771
|
if (typeof html !== "string") return "";
|
|
2475
1772
|
return html.replace(/<[^>]*>?/gm, "").trim();
|
|
2476
1773
|
}
|
|
2477
|
-
|
|
2478
|
-
const SeoPreview = ({
|
|
2479
|
-
title,
|
|
2480
|
-
description,
|
|
2481
|
-
slug,
|
|
2482
|
-
}: {
|
|
2483
|
-
title: string;
|
|
2484
|
-
description: string;
|
|
2485
|
-
slug: string;
|
|
2486
|
-
}) => (
|
|
2487
|
-
<div className="bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-lg p-6 max-w-2xl shadow-sm transition-colors duration-300">
|
|
2488
|
-
<div className="flex items-center gap-2 mb-2">
|
|
2489
|
-
<div className="w-7 h-7 bg-[var(--kyro-bg-secondary)] rounded-full flex items-center justify-center text-[10px] text-[var(--kyro-text-primary)] font-medium border border-[var(--kyro-border)]">
|
|
2490
|
-
K
|
|
2491
|
-
</div>
|
|
2492
|
-
<div className="flex flex-col">
|
|
2493
|
-
<span className="text-sm font-medium text-[var(--kyro-text-primary)] leading-tight">
|
|
2494
|
-
kyro-cms.com
|
|
2495
|
-
</span>
|
|
2496
|
-
<span className="text-[12px] text-[var(--kyro-text-secondary)] leading-tight opacity-80">
|
|
2497
|
-
https://kyro-cms.com › posts › {slug}
|
|
2498
|
-
</span>
|
|
2499
|
-
</div>
|
|
2500
|
-
</div>
|
|
2501
|
-
<h3 className="text-[20px] text-[#2563eb] dark:text-[#60a5fa] font-medium hover:underline cursor-pointer mb-1 leading-tight transition-colors">
|
|
2502
|
-
{title}
|
|
2503
|
-
</h3>
|
|
2504
|
-
<p className="text-[14px] text-[var(--kyro-text-secondary)] leading-relaxed line-clamp-2">
|
|
2505
|
-
{description}
|
|
2506
|
-
</p>
|
|
2507
|
-
</div>
|
|
2508
|
-
);
|