@questpie/admin 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +108 -0
- package/CHANGELOG.md +10 -0
- package/README.md +556 -0
- package/STATUS.md +917 -0
- package/VALIDATION.md +602 -0
- package/components.json +24 -0
- package/dist/__tests__/setup.mjs +38 -0
- package/dist/__tests__/test-utils.mjs +45 -0
- package/dist/__tests__/vitest.d.mjs +3 -0
- package/dist/components/admin-app.mjs +69 -0
- package/dist/components/fields/array-field.mjs +190 -0
- package/dist/components/fields/checkbox-field.mjs +34 -0
- package/dist/components/fields/custom-field.mjs +32 -0
- package/dist/components/fields/date-field.mjs +41 -0
- package/dist/components/fields/datetime-field.mjs +42 -0
- package/dist/components/fields/email-field.mjs +37 -0
- package/dist/components/fields/embedded-collection.mjs +253 -0
- package/dist/components/fields/field-types.mjs +1 -0
- package/dist/components/fields/field-utils.mjs +10 -0
- package/dist/components/fields/field-wrapper.mjs +34 -0
- package/dist/components/fields/index.mjs +23 -0
- package/dist/components/fields/json-field.mjs +243 -0
- package/dist/components/fields/locale-badge.mjs +16 -0
- package/dist/components/fields/number-field.mjs +39 -0
- package/dist/components/fields/password-field.mjs +37 -0
- package/dist/components/fields/relation-field.mjs +104 -0
- package/dist/components/fields/relation-picker.mjs +229 -0
- package/dist/components/fields/relation-select.mjs +188 -0
- package/dist/components/fields/rich-text-editor/index.mjs +897 -0
- package/dist/components/fields/select-field.mjs +41 -0
- package/dist/components/fields/switch-field.mjs +34 -0
- package/dist/components/fields/text-field.mjs +38 -0
- package/dist/components/fields/textarea-field.mjs +38 -0
- package/dist/components/index.mjs +59 -0
- package/dist/components/primitives/checkbox-input.mjs +127 -0
- package/dist/components/primitives/date-input.mjs +303 -0
- package/dist/components/primitives/index.mjs +12 -0
- package/dist/components/primitives/number-input.mjs +104 -0
- package/dist/components/primitives/select-input.mjs +177 -0
- package/dist/components/primitives/tag-input.mjs +135 -0
- package/dist/components/primitives/text-input.mjs +39 -0
- package/dist/components/primitives/textarea-input.mjs +37 -0
- package/dist/components/primitives/toggle-input.mjs +31 -0
- package/dist/components/primitives/types.mjs +12 -0
- package/dist/components/ui/accordion.mjs +55 -0
- package/dist/components/ui/avatar.mjs +54 -0
- package/dist/components/ui/badge.mjs +34 -0
- package/dist/components/ui/button.mjs +48 -0
- package/dist/components/ui/card.mjs +58 -0
- package/dist/components/ui/checkbox.mjs +21 -0
- package/dist/components/ui/combobox.mjs +163 -0
- package/dist/components/ui/dialog.mjs +95 -0
- package/dist/components/ui/dropdown-menu.mjs +138 -0
- package/dist/components/ui/field.mjs +113 -0
- package/dist/components/ui/input-group.mjs +82 -0
- package/dist/components/ui/input.mjs +17 -0
- package/dist/components/ui/label.mjs +15 -0
- package/dist/components/ui/popover.mjs +56 -0
- package/dist/components/ui/scroll-area.mjs +38 -0
- package/dist/components/ui/select.mjs +100 -0
- package/dist/components/ui/separator.mjs +16 -0
- package/dist/components/ui/sheet.mjs +90 -0
- package/dist/components/ui/sidebar.mjs +387 -0
- package/dist/components/ui/skeleton.mjs +14 -0
- package/dist/components/ui/spinner.mjs +16 -0
- package/dist/components/ui/switch.mjs +22 -0
- package/dist/components/ui/table.mjs +68 -0
- package/dist/components/ui/tabs.mjs +48 -0
- package/dist/components/ui/textarea.mjs +15 -0
- package/dist/components/ui/tooltip.mjs +44 -0
- package/dist/config/component-registry.mjs +38 -0
- package/dist/config/index.mjs +129 -0
- package/dist/hooks/admin-provider.mjs +70 -0
- package/dist/hooks/index.mjs +7 -0
- package/dist/hooks/store.mjs +178 -0
- package/dist/hooks/use-auth.mjs +76 -0
- package/dist/hooks/use-collection-db.mjs +146 -0
- package/dist/hooks/use-collection.mjs +112 -0
- package/dist/hooks/use-global.mjs +46 -0
- package/dist/hooks/use-mobile.mjs +20 -0
- package/dist/lib/utils.mjs +10 -0
- package/dist/styles/index.css +336 -0
- package/dist/styles/index.mjs +1 -0
- package/dist/utils/index.mjs +9 -0
- package/dist/views/auth/auth-layout.mjs +52 -0
- package/dist/views/auth/forgot-password-form.mjs +148 -0
- package/dist/views/auth/index.mjs +6 -0
- package/dist/views/auth/login-form.mjs +156 -0
- package/dist/views/auth/reset-password-form.mjs +184 -0
- package/dist/views/collection/auto-form-fields.mjs +525 -0
- package/dist/views/collection/collection-form.mjs +91 -0
- package/dist/views/collection/collection-list.mjs +76 -0
- package/dist/views/collection/form-field.mjs +42 -0
- package/dist/views/collection/index.mjs +6 -0
- package/dist/views/common/index.mjs +4 -0
- package/dist/views/common/locale-switcher.mjs +39 -0
- package/dist/views/common/version-history.mjs +272 -0
- package/dist/views/index.mjs +9 -0
- package/dist/views/layout/admin-layout.mjs +40 -0
- package/dist/views/layout/admin-router.mjs +95 -0
- package/dist/views/layout/admin-sidebar.mjs +63 -0
- package/dist/views/layout/index.mjs +5 -0
- package/package.json +276 -0
- package/src/__tests__/setup.ts +44 -0
- package/src/__tests__/test-utils.tsx +49 -0
- package/src/__tests__/vitest.d.ts +9 -0
- package/src/components/admin-app.tsx +221 -0
- package/src/components/fields/array-field.tsx +237 -0
- package/src/components/fields/checkbox-field.tsx +47 -0
- package/src/components/fields/custom-field.tsx +50 -0
- package/src/components/fields/date-field.tsx +65 -0
- package/src/components/fields/datetime-field.tsx +67 -0
- package/src/components/fields/email-field.tsx +51 -0
- package/src/components/fields/embedded-collection.tsx +315 -0
- package/src/components/fields/field-types.ts +162 -0
- package/src/components/fields/field-utils.ts +6 -0
- package/src/components/fields/field-wrapper.tsx +52 -0
- package/src/components/fields/index.ts +66 -0
- package/src/components/fields/json-field.tsx +440 -0
- package/src/components/fields/locale-badge.tsx +15 -0
- package/src/components/fields/number-field.tsx +57 -0
- package/src/components/fields/password-field.tsx +51 -0
- package/src/components/fields/relation-field.tsx +243 -0
- package/src/components/fields/relation-picker.tsx +402 -0
- package/src/components/fields/relation-select.tsx +327 -0
- package/src/components/fields/rich-text-editor/index.tsx +1337 -0
- package/src/components/fields/select-field.tsx +61 -0
- package/src/components/fields/switch-field.tsx +47 -0
- package/src/components/fields/text-field.tsx +55 -0
- package/src/components/fields/textarea-field.tsx +55 -0
- package/src/components/index.ts +40 -0
- package/src/components/primitives/checkbox-input.tsx +193 -0
- package/src/components/primitives/date-input.tsx +401 -0
- package/src/components/primitives/index.ts +24 -0
- package/src/components/primitives/number-input.tsx +132 -0
- package/src/components/primitives/select-input.tsx +296 -0
- package/src/components/primitives/tag-input.tsx +200 -0
- package/src/components/primitives/text-input.tsx +49 -0
- package/src/components/primitives/textarea-input.tsx +46 -0
- package/src/components/primitives/toggle-input.tsx +36 -0
- package/src/components/primitives/types.ts +235 -0
- package/src/components/ui/accordion.tsx +72 -0
- package/src/components/ui/avatar.tsx +106 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +53 -0
- package/src/components/ui/card.tsx +94 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/combobox.tsx +290 -0
- package/src/components/ui/dialog.tsx +151 -0
- package/src/components/ui/dropdown-menu.tsx +254 -0
- package/src/components/ui/field.tsx +227 -0
- package/src/components/ui/input-group.tsx +149 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/label.tsx +18 -0
- package/src/components/ui/popover.tsx +88 -0
- package/src/components/ui/scroll-area.tsx +53 -0
- package/src/components/ui/select.tsx +192 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +127 -0
- package/src/components/ui/sidebar.tsx +723 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/spinner.tsx +10 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/table.tsx +99 -0
- package/src/components/ui/tabs.tsx +82 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +70 -0
- package/src/config/component-registry.ts +190 -0
- package/src/config/index.ts +1099 -0
- package/src/hooks/README.md +269 -0
- package/src/hooks/admin-provider.tsx +110 -0
- package/src/hooks/index.ts +41 -0
- package/src/hooks/store.ts +248 -0
- package/src/hooks/use-auth.ts +168 -0
- package/src/hooks/use-collection-db.ts +209 -0
- package/src/hooks/use-collection.ts +156 -0
- package/src/hooks/use-global.ts +69 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/index.css +340 -0
- package/src/utils/index.ts +6 -0
- package/src/views/auth/auth-layout.tsx +77 -0
- package/src/views/auth/forgot-password-form.tsx +192 -0
- package/src/views/auth/index.ts +21 -0
- package/src/views/auth/login-form.tsx +229 -0
- package/src/views/auth/reset-password-form.tsx +232 -0
- package/src/views/collection/auto-form-fields.tsx +982 -0
- package/src/views/collection/collection-form.tsx +186 -0
- package/src/views/collection/collection-list.tsx +223 -0
- package/src/views/collection/form-field.tsx +52 -0
- package/src/views/collection/index.ts +15 -0
- package/src/views/common/index.ts +8 -0
- package/src/views/common/locale-switcher.tsx +45 -0
- package/src/views/common/version-history.tsx +406 -0
- package/src/views/index.ts +25 -0
- package/src/views/layout/admin-layout.tsx +117 -0
- package/src/views/layout/admin-router.tsx +206 -0
- package/src/views/layout/admin-sidebar.tsx +185 -0
- package/src/views/layout/index.ts +12 -0
- package/tsconfig.json +13 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsdown.config.ts +13 -0
- package/vitest.config.ts +29 -0
|
@@ -0,0 +1,982 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AutoFormFields Component
|
|
3
|
+
*
|
|
4
|
+
* Automatically generates form fields from:
|
|
5
|
+
* 1. CMS schema (Drizzle columns)
|
|
6
|
+
* 2. Admin config (field overrides, layout, sections, tabs)
|
|
7
|
+
*
|
|
8
|
+
* Replaces manual field definitions!
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as React from "react";
|
|
12
|
+
import { useFormContext } from "react-hook-form";
|
|
13
|
+
import { FormField } from "./form-field";
|
|
14
|
+
import type { FormFieldProps } from "./form-field";
|
|
15
|
+
import { RelationSelect } from "../../components/fields/relation-select";
|
|
16
|
+
import { RelationPicker } from "../../components/fields/relation-picker";
|
|
17
|
+
import { RichTextEditor } from "../../components/fields/rich-text-editor";
|
|
18
|
+
import { EmbeddedCollectionField } from "../../components/fields/embedded-collection";
|
|
19
|
+
import { ArrayField } from "../../components/fields/array-field";
|
|
20
|
+
import {
|
|
21
|
+
Accordion,
|
|
22
|
+
AccordionContent,
|
|
23
|
+
AccordionItem,
|
|
24
|
+
AccordionTrigger,
|
|
25
|
+
} from "../../components/ui/accordion";
|
|
26
|
+
import {
|
|
27
|
+
Tabs,
|
|
28
|
+
TabsContent,
|
|
29
|
+
TabsList,
|
|
30
|
+
TabsTrigger,
|
|
31
|
+
} from "../../components/ui/tabs";
|
|
32
|
+
import { cn } from "../../utils";
|
|
33
|
+
import { useAdminContext } from "../../hooks/admin-provider";
|
|
34
|
+
import type { Questpie } from "questpie";
|
|
35
|
+
import type {
|
|
36
|
+
EditConfig,
|
|
37
|
+
FieldConfig,
|
|
38
|
+
FieldLayout,
|
|
39
|
+
SectionConfig,
|
|
40
|
+
SectionLayout,
|
|
41
|
+
TabConfig,
|
|
42
|
+
} from "../../config";
|
|
43
|
+
import type { ComponentRegistry } from "../../config/component-registry";
|
|
44
|
+
|
|
45
|
+
type ValueResolver<T> = T | ((values: any) => T);
|
|
46
|
+
type CollectionConfig = {
|
|
47
|
+
edit?: EditConfig;
|
|
48
|
+
fields?: Record<string, FieldConfig>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export interface AutoFormFieldsProps<T extends Questpie<any>, K extends string> {
|
|
52
|
+
/**
|
|
53
|
+
* CMS instance (for schema introspection)
|
|
54
|
+
*/
|
|
55
|
+
cms?: T;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Collection name
|
|
59
|
+
*/
|
|
60
|
+
collection: K;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Admin config for this collection
|
|
64
|
+
*/
|
|
65
|
+
config?: CollectionConfig;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Component registry for custom field types
|
|
69
|
+
*/
|
|
70
|
+
registry?: ComponentRegistry;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Custom field renderer (fallback)
|
|
74
|
+
*/
|
|
75
|
+
renderField?: (
|
|
76
|
+
fieldName: string,
|
|
77
|
+
fieldConfig?: FieldConfig,
|
|
78
|
+
) => React.ReactNode;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Field name prefix for nested fields
|
|
82
|
+
*/
|
|
83
|
+
fieldPrefix?: string;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* All collection configs (for embedded collections)
|
|
87
|
+
*/
|
|
88
|
+
allCollectionsConfig?: Record<string, CollectionConfig>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get default fields for a collection
|
|
93
|
+
* This would come from CMS schema introspection
|
|
94
|
+
*/
|
|
95
|
+
function getDefaultFields(collection: string): string[] {
|
|
96
|
+
// TODO: Extract from CMS schema
|
|
97
|
+
// For now, return common pattern
|
|
98
|
+
const commonFields = {
|
|
99
|
+
barbers: ["name", "email", "phone", "bio", "avatar", "isActive"],
|
|
100
|
+
services: ["name", "description", "duration", "price", "isActive"],
|
|
101
|
+
appointments: [
|
|
102
|
+
"customerId",
|
|
103
|
+
"barberId",
|
|
104
|
+
"serviceId",
|
|
105
|
+
"scheduledAt",
|
|
106
|
+
"status",
|
|
107
|
+
"notes",
|
|
108
|
+
],
|
|
109
|
+
reviews: ["appointmentId", "rating", "comment"],
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return commonFields[collection as keyof typeof commonFields] || [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getFormValues(form: any, fieldPrefix?: string) {
|
|
116
|
+
if (!form?.watch) return {};
|
|
117
|
+
const values = fieldPrefix ? form.watch(fieldPrefix) : form.watch();
|
|
118
|
+
return values ?? {};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getFullFieldName(fieldName: string, fieldPrefix?: string) {
|
|
122
|
+
return fieldPrefix ? `${fieldPrefix}.${fieldName}` : fieldName;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function resolveValue<T>(
|
|
126
|
+
value: ValueResolver<T> | undefined,
|
|
127
|
+
formValues: any,
|
|
128
|
+
defaultValue: T,
|
|
129
|
+
): T {
|
|
130
|
+
if (value === undefined) return defaultValue;
|
|
131
|
+
if (typeof value === "function") {
|
|
132
|
+
return (value as (values: any) => T)(formValues);
|
|
133
|
+
}
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function resolveOptions(
|
|
138
|
+
options:
|
|
139
|
+
| Array<{ label: string; value: any }>
|
|
140
|
+
| ((values: any) => Array<{ label: string; value: any }>)
|
|
141
|
+
| undefined,
|
|
142
|
+
formValues: any,
|
|
143
|
+
) {
|
|
144
|
+
if (!options) return undefined;
|
|
145
|
+
if (typeof options === "function") {
|
|
146
|
+
return options(formValues);
|
|
147
|
+
}
|
|
148
|
+
return options;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function resolveRelationTarget(relationConfig?: FieldConfig["relation"]) {
|
|
152
|
+
return relationConfig?.targetCollection;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
type FieldContext = {
|
|
156
|
+
fieldName: string;
|
|
157
|
+
fullFieldName: string;
|
|
158
|
+
collection: string;
|
|
159
|
+
fieldConfig?: FieldConfig;
|
|
160
|
+
fieldValue: any;
|
|
161
|
+
label: string;
|
|
162
|
+
description?: string;
|
|
163
|
+
placeholder?: string;
|
|
164
|
+
options?: Array<{ label: string; value: any }>;
|
|
165
|
+
isVisible: boolean;
|
|
166
|
+
isReadOnly: boolean;
|
|
167
|
+
isDisabled: boolean;
|
|
168
|
+
isRequired: boolean;
|
|
169
|
+
isLocalized: boolean;
|
|
170
|
+
locale?: string;
|
|
171
|
+
fieldError?: string;
|
|
172
|
+
updateValue: (nextValue: any) => void;
|
|
173
|
+
type?: string;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
function getFieldContext({
|
|
177
|
+
fieldName,
|
|
178
|
+
fieldConfig,
|
|
179
|
+
collection,
|
|
180
|
+
form,
|
|
181
|
+
fieldPrefix,
|
|
182
|
+
locale,
|
|
183
|
+
}: {
|
|
184
|
+
fieldName: string;
|
|
185
|
+
fieldConfig?: FieldConfig;
|
|
186
|
+
collection: string;
|
|
187
|
+
form: any;
|
|
188
|
+
fieldPrefix?: string;
|
|
189
|
+
locale?: string;
|
|
190
|
+
}): FieldContext {
|
|
191
|
+
const formValues = getFormValues(form, fieldPrefix);
|
|
192
|
+
const fullFieldName = getFullFieldName(fieldName, fieldPrefix);
|
|
193
|
+
const fieldState = form?.getFieldState
|
|
194
|
+
? form.getFieldState(fullFieldName)
|
|
195
|
+
: undefined;
|
|
196
|
+
const fieldError = fieldState?.error?.message;
|
|
197
|
+
const fieldValue = formValues[fieldName];
|
|
198
|
+
|
|
199
|
+
const label = fieldConfig?.label || fieldName;
|
|
200
|
+
const description = fieldConfig?.description;
|
|
201
|
+
const placeholder = fieldConfig?.placeholder;
|
|
202
|
+
|
|
203
|
+
const isVisible = resolveValue(fieldConfig?.visible, formValues, true);
|
|
204
|
+
const isReadOnly = resolveValue(fieldConfig?.readOnly, formValues, false);
|
|
205
|
+
const isDisabled = resolveValue(fieldConfig?.disabled, formValues, false);
|
|
206
|
+
const isRequired = resolveValue(fieldConfig?.required, formValues, false);
|
|
207
|
+
const options = resolveOptions(fieldConfig?.options, formValues);
|
|
208
|
+
const isLocalized = !!fieldConfig?.localized;
|
|
209
|
+
|
|
210
|
+
const type = fieldConfig?.type;
|
|
211
|
+
|
|
212
|
+
const updateValue = (nextValue: any) => {
|
|
213
|
+
if (form?.setValue) {
|
|
214
|
+
form.setValue(fullFieldName, nextValue, {
|
|
215
|
+
shouldDirty: true,
|
|
216
|
+
shouldTouch: true,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
fieldName,
|
|
223
|
+
fullFieldName,
|
|
224
|
+
collection,
|
|
225
|
+
fieldConfig,
|
|
226
|
+
fieldValue,
|
|
227
|
+
label,
|
|
228
|
+
description,
|
|
229
|
+
placeholder,
|
|
230
|
+
options,
|
|
231
|
+
isVisible,
|
|
232
|
+
isReadOnly,
|
|
233
|
+
isDisabled,
|
|
234
|
+
isRequired,
|
|
235
|
+
isLocalized,
|
|
236
|
+
locale,
|
|
237
|
+
fieldError,
|
|
238
|
+
updateValue,
|
|
239
|
+
type,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function buildComponentProps(context: FieldContext) {
|
|
244
|
+
return {
|
|
245
|
+
key: context.fullFieldName,
|
|
246
|
+
name: context.fullFieldName,
|
|
247
|
+
value: context.fieldValue,
|
|
248
|
+
onChange: context.updateValue,
|
|
249
|
+
label: context.label,
|
|
250
|
+
description: context.description,
|
|
251
|
+
placeholder: context.placeholder,
|
|
252
|
+
required: context.isRequired,
|
|
253
|
+
disabled: context.isDisabled,
|
|
254
|
+
readOnly: context.isReadOnly,
|
|
255
|
+
error: context.fieldError,
|
|
256
|
+
localized: context.isLocalized,
|
|
257
|
+
locale: context.locale,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function buildRelationProps(context: FieldContext) {
|
|
262
|
+
return {
|
|
263
|
+
label: context.label,
|
|
264
|
+
required: context.isRequired,
|
|
265
|
+
disabled: context.isDisabled,
|
|
266
|
+
readOnly: context.isReadOnly,
|
|
267
|
+
placeholder: context.placeholder,
|
|
268
|
+
error: context.fieldError,
|
|
269
|
+
localized: context.isLocalized,
|
|
270
|
+
locale: context.locale,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function buildFormFieldProps(context: FieldContext): FormFieldProps {
|
|
275
|
+
return {
|
|
276
|
+
name: context.fullFieldName,
|
|
277
|
+
label: context.label,
|
|
278
|
+
description: context.description,
|
|
279
|
+
placeholder: context.placeholder,
|
|
280
|
+
required: context.isRequired,
|
|
281
|
+
disabled: context.isDisabled || context.isReadOnly,
|
|
282
|
+
localized: context.isLocalized,
|
|
283
|
+
locale: context.locale,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function renderConfigError(message: string) {
|
|
288
|
+
return (
|
|
289
|
+
<div className="rounded-md border border-destructive/40 bg-destructive/5 p-3 text-sm text-destructive">
|
|
290
|
+
{message}
|
|
291
|
+
</div>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function renderFormField(
|
|
296
|
+
formFieldProps: FormFieldProps,
|
|
297
|
+
type?: FormFieldProps["type"],
|
|
298
|
+
extra?: Partial<FormFieldProps>,
|
|
299
|
+
) {
|
|
300
|
+
return <FormField {...formFieldProps} type={type} {...extra} />;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function renderCustomComponent({
|
|
304
|
+
context,
|
|
305
|
+
registry,
|
|
306
|
+
componentProps,
|
|
307
|
+
}: {
|
|
308
|
+
context: FieldContext;
|
|
309
|
+
registry?: ComponentRegistry;
|
|
310
|
+
componentProps: ReturnType<typeof buildComponentProps>;
|
|
311
|
+
}) {
|
|
312
|
+
const component = context.fieldConfig?.component;
|
|
313
|
+
if (!component) return null;
|
|
314
|
+
const CustomComponent =
|
|
315
|
+
typeof component === "string"
|
|
316
|
+
? (
|
|
317
|
+
registry?.fields as
|
|
318
|
+
| Record<string, React.ComponentType<any>>
|
|
319
|
+
| undefined
|
|
320
|
+
)?.[component] || registry?.custom?.[component]
|
|
321
|
+
: component;
|
|
322
|
+
if (!CustomComponent) return null;
|
|
323
|
+
return <CustomComponent {...componentProps} />;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function renderRelationField({
|
|
327
|
+
context,
|
|
328
|
+
relationProps,
|
|
329
|
+
formFieldProps,
|
|
330
|
+
}: {
|
|
331
|
+
context: FieldContext;
|
|
332
|
+
relationProps: ReturnType<typeof buildRelationProps>;
|
|
333
|
+
formFieldProps: FormFieldProps;
|
|
334
|
+
}) {
|
|
335
|
+
const relationConfig = context.fieldConfig?.relation;
|
|
336
|
+
const shouldRender = context.type === "relation" || !!relationConfig;
|
|
337
|
+
if (!shouldRender) return null;
|
|
338
|
+
|
|
339
|
+
if (!relationConfig) {
|
|
340
|
+
return renderConfigError(
|
|
341
|
+
`Missing relation config for "${context.fieldName}". Define relation.targetCollection.`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const targetCollection = resolveRelationTarget(relationConfig);
|
|
346
|
+
|
|
347
|
+
if (!targetCollection) {
|
|
348
|
+
return renderConfigError(
|
|
349
|
+
`Missing relation.targetCollection for "${context.fieldName}".`,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const baseRelationProps = {
|
|
354
|
+
key: context.fullFieldName,
|
|
355
|
+
name: context.fullFieldName,
|
|
356
|
+
targetCollection,
|
|
357
|
+
filter: relationConfig?.filter,
|
|
358
|
+
renderFormFields: undefined,
|
|
359
|
+
...relationProps,
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const isMultiple =
|
|
363
|
+
relationConfig?.mode === "picker" || Array.isArray(context.fieldValue);
|
|
364
|
+
|
|
365
|
+
if (isMultiple) {
|
|
366
|
+
return (
|
|
367
|
+
<RelationPicker
|
|
368
|
+
{...baseRelationProps}
|
|
369
|
+
value={context.fieldValue || []}
|
|
370
|
+
onChange={context.updateValue}
|
|
371
|
+
orderable={relationConfig?.orderable}
|
|
372
|
+
/>
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return (
|
|
377
|
+
<RelationSelect
|
|
378
|
+
{...baseRelationProps}
|
|
379
|
+
value={context.fieldValue}
|
|
380
|
+
onChange={context.updateValue}
|
|
381
|
+
/>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function renderEmbeddedField({
|
|
386
|
+
context,
|
|
387
|
+
registry,
|
|
388
|
+
allCollectionsConfig,
|
|
389
|
+
componentProps,
|
|
390
|
+
}: {
|
|
391
|
+
context: FieldContext;
|
|
392
|
+
registry?: ComponentRegistry;
|
|
393
|
+
allCollectionsConfig?: Record<string, CollectionConfig>;
|
|
394
|
+
componentProps: ReturnType<typeof buildComponentProps>;
|
|
395
|
+
}) {
|
|
396
|
+
const embeddedConfig = context.fieldConfig?.embedded;
|
|
397
|
+
if (!embeddedConfig) return null;
|
|
398
|
+
|
|
399
|
+
const embeddedCollection = embeddedConfig.collection;
|
|
400
|
+
const embeddedCollectionConfig = embeddedCollection
|
|
401
|
+
? allCollectionsConfig?.[embeddedCollection]
|
|
402
|
+
: undefined;
|
|
403
|
+
const EmbeddedComponent = (registry?.fields?.embedded ||
|
|
404
|
+
EmbeddedCollectionField) as React.ComponentType<any>;
|
|
405
|
+
|
|
406
|
+
return (
|
|
407
|
+
<EmbeddedComponent
|
|
408
|
+
{...componentProps}
|
|
409
|
+
value={context.fieldValue || []}
|
|
410
|
+
collection={embeddedCollection}
|
|
411
|
+
mode={embeddedConfig?.mode}
|
|
412
|
+
orderable={embeddedConfig?.orderable}
|
|
413
|
+
rowLabel={embeddedConfig?.rowLabel}
|
|
414
|
+
renderFields={(index: number) =>
|
|
415
|
+
embeddedCollection ? (
|
|
416
|
+
<AutoFormFields
|
|
417
|
+
collection={embeddedCollection}
|
|
418
|
+
config={embeddedCollectionConfig}
|
|
419
|
+
registry={registry}
|
|
420
|
+
fieldPrefix={`${context.fullFieldName}.${index}`}
|
|
421
|
+
allCollectionsConfig={allCollectionsConfig}
|
|
422
|
+
/>
|
|
423
|
+
) : null
|
|
424
|
+
}
|
|
425
|
+
/>
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function renderArrayField({
|
|
430
|
+
context,
|
|
431
|
+
registry,
|
|
432
|
+
componentProps,
|
|
433
|
+
}: {
|
|
434
|
+
context: FieldContext;
|
|
435
|
+
registry?: ComponentRegistry;
|
|
436
|
+
componentProps: ReturnType<typeof buildComponentProps>;
|
|
437
|
+
}) {
|
|
438
|
+
const arrayConfig = context.fieldConfig?.array;
|
|
439
|
+
const shouldRender = context.type === "array" || !!arrayConfig;
|
|
440
|
+
if (!shouldRender) return null;
|
|
441
|
+
|
|
442
|
+
const ArrayComponent = (registry?.fields?.array ||
|
|
443
|
+
ArrayField) as React.ComponentType<any>;
|
|
444
|
+
const config = arrayConfig || {};
|
|
445
|
+
|
|
446
|
+
return (
|
|
447
|
+
<ArrayComponent
|
|
448
|
+
{...componentProps}
|
|
449
|
+
value={context.fieldValue || []}
|
|
450
|
+
placeholder={config.placeholder ?? context.placeholder}
|
|
451
|
+
itemType={config.itemType}
|
|
452
|
+
options={config.options || context.options}
|
|
453
|
+
orderable={config.orderable}
|
|
454
|
+
minItems={config.minItems}
|
|
455
|
+
maxItems={config.maxItems}
|
|
456
|
+
/>
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function renderRichTextField({
|
|
461
|
+
context,
|
|
462
|
+
registry,
|
|
463
|
+
componentProps,
|
|
464
|
+
}: {
|
|
465
|
+
context: FieldContext;
|
|
466
|
+
registry?: ComponentRegistry;
|
|
467
|
+
componentProps: ReturnType<typeof buildComponentProps>;
|
|
468
|
+
}) {
|
|
469
|
+
if (context.type !== "richText") return null;
|
|
470
|
+
const RichTextComponent = registry?.fields?.richText || RichTextEditor;
|
|
471
|
+
return (
|
|
472
|
+
<RichTextComponent {...componentProps} {...context.fieldConfig?.richText} />
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function renderPrimitiveField({
|
|
477
|
+
context,
|
|
478
|
+
formFieldProps,
|
|
479
|
+
}: {
|
|
480
|
+
context: FieldContext;
|
|
481
|
+
formFieldProps: FormFieldProps;
|
|
482
|
+
}) {
|
|
483
|
+
if (!context.type) {
|
|
484
|
+
return renderConfigError(
|
|
485
|
+
`Missing field type for "${context.fieldName}". Define it in admin config.`,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
if (context.type === "boolean") {
|
|
489
|
+
return renderFormField(formFieldProps, "switch");
|
|
490
|
+
}
|
|
491
|
+
if (context.type === "select" && context.options) {
|
|
492
|
+
return renderFormField(formFieldProps, "select", {
|
|
493
|
+
options: context.options,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
return renderFormField(
|
|
497
|
+
formFieldProps,
|
|
498
|
+
context.type as FormFieldProps["type"],
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Render a single field with conditional logic
|
|
504
|
+
*/
|
|
505
|
+
function FieldRenderer({
|
|
506
|
+
fieldName,
|
|
507
|
+
fieldConfig,
|
|
508
|
+
collection,
|
|
509
|
+
registry,
|
|
510
|
+
fieldPrefix,
|
|
511
|
+
allCollectionsConfig,
|
|
512
|
+
}: {
|
|
513
|
+
fieldName: string;
|
|
514
|
+
fieldConfig?: FieldConfig;
|
|
515
|
+
collection: string;
|
|
516
|
+
registry?: ComponentRegistry;
|
|
517
|
+
fieldPrefix?: string;
|
|
518
|
+
allCollectionsConfig?: Record<string, CollectionConfig>;
|
|
519
|
+
}) {
|
|
520
|
+
// Get current form values for conditional rendering
|
|
521
|
+
const form = useFormContext() as any;
|
|
522
|
+
const { locale } = useAdminContext<any>();
|
|
523
|
+
const context = getFieldContext({
|
|
524
|
+
fieldName,
|
|
525
|
+
fieldConfig,
|
|
526
|
+
collection,
|
|
527
|
+
form,
|
|
528
|
+
fieldPrefix,
|
|
529
|
+
locale,
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
if (!context.isVisible) return null;
|
|
533
|
+
|
|
534
|
+
const componentProps = buildComponentProps(context);
|
|
535
|
+
const relationProps = buildRelationProps(context);
|
|
536
|
+
const formFieldProps = buildFormFieldProps(context);
|
|
537
|
+
|
|
538
|
+
const renderers = [
|
|
539
|
+
() =>
|
|
540
|
+
renderCustomComponent({
|
|
541
|
+
context,
|
|
542
|
+
registry,
|
|
543
|
+
componentProps,
|
|
544
|
+
}),
|
|
545
|
+
() =>
|
|
546
|
+
renderRelationField({
|
|
547
|
+
context,
|
|
548
|
+
relationProps,
|
|
549
|
+
formFieldProps,
|
|
550
|
+
}),
|
|
551
|
+
() =>
|
|
552
|
+
renderEmbeddedField({
|
|
553
|
+
context,
|
|
554
|
+
registry,
|
|
555
|
+
allCollectionsConfig,
|
|
556
|
+
componentProps,
|
|
557
|
+
}),
|
|
558
|
+
() =>
|
|
559
|
+
renderArrayField({
|
|
560
|
+
context,
|
|
561
|
+
registry,
|
|
562
|
+
componentProps,
|
|
563
|
+
}),
|
|
564
|
+
() =>
|
|
565
|
+
renderRichTextField({
|
|
566
|
+
context,
|
|
567
|
+
registry,
|
|
568
|
+
componentProps,
|
|
569
|
+
}),
|
|
570
|
+
() =>
|
|
571
|
+
renderPrimitiveField({
|
|
572
|
+
context,
|
|
573
|
+
formFieldProps,
|
|
574
|
+
}),
|
|
575
|
+
];
|
|
576
|
+
|
|
577
|
+
for (const render of renderers) {
|
|
578
|
+
const node = render();
|
|
579
|
+
if (node) return node;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const gridColumnClasses: Record<number, string> = {
|
|
586
|
+
1: "grid-cols-1",
|
|
587
|
+
2: "grid-cols-2",
|
|
588
|
+
3: "grid-cols-3",
|
|
589
|
+
4: "grid-cols-4",
|
|
590
|
+
5: "grid-cols-5",
|
|
591
|
+
6: "grid-cols-6",
|
|
592
|
+
7: "grid-cols-7",
|
|
593
|
+
8: "grid-cols-8",
|
|
594
|
+
9: "grid-cols-9",
|
|
595
|
+
10: "grid-cols-10",
|
|
596
|
+
11: "grid-cols-11",
|
|
597
|
+
12: "grid-cols-12",
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
function getGridColumnsClass(columns?: number, prefix?: "sm" | "md" | "lg") {
|
|
601
|
+
if (!columns) return "";
|
|
602
|
+
const base = gridColumnClasses[columns];
|
|
603
|
+
if (!base) return "";
|
|
604
|
+
return prefix ? `${prefix}:${base}` : base;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function getGapStyle(gap?: number) {
|
|
608
|
+
if (gap === undefined) return undefined;
|
|
609
|
+
return `${gap * 0.25}rem`;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function normalizeSectionFields(
|
|
613
|
+
fields: string[] | FieldLayout[],
|
|
614
|
+
): FieldLayout[] {
|
|
615
|
+
if (!fields?.length) return [];
|
|
616
|
+
if (typeof fields[0] === "string") {
|
|
617
|
+
return (fields as string[]).map((field) => ({ field }));
|
|
618
|
+
}
|
|
619
|
+
return fields as FieldLayout[];
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function resolveSpan(span: string | number | undefined, columns: number) {
|
|
623
|
+
if (!span) return undefined;
|
|
624
|
+
if (typeof span === "number") return Math.max(1, Math.min(columns, span));
|
|
625
|
+
if (span === "full") return columns;
|
|
626
|
+
const fractions: Record<string, number> = {
|
|
627
|
+
"1/2": 0.5,
|
|
628
|
+
"1/3": 1 / 3,
|
|
629
|
+
"2/3": 2 / 3,
|
|
630
|
+
"1/4": 0.25,
|
|
631
|
+
"3/4": 0.75,
|
|
632
|
+
};
|
|
633
|
+
const fraction = fractions[span];
|
|
634
|
+
if (!fraction) return undefined;
|
|
635
|
+
return Math.max(1, Math.round(columns * fraction));
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* AutoFormFields Component
|
|
640
|
+
*/
|
|
641
|
+
export function AutoFormFields<T extends Questpie<any>, K extends string>({
|
|
642
|
+
cms: _cms,
|
|
643
|
+
collection,
|
|
644
|
+
config,
|
|
645
|
+
registry,
|
|
646
|
+
renderField,
|
|
647
|
+
fieldPrefix,
|
|
648
|
+
allCollectionsConfig,
|
|
649
|
+
}: AutoFormFieldsProps<T, K>): React.ReactElement {
|
|
650
|
+
const form = useFormContext() as any;
|
|
651
|
+
const formValues = getFormValues(form, fieldPrefix);
|
|
652
|
+
|
|
653
|
+
// Get field order
|
|
654
|
+
const fieldOrder =
|
|
655
|
+
config?.edit?.fields || getDefaultFields(collection as string);
|
|
656
|
+
|
|
657
|
+
const excludedFields = new Set(config?.edit?.exclude || []);
|
|
658
|
+
const sidebarFieldSet = new Set(config?.edit?.sidebar?.fields || []);
|
|
659
|
+
|
|
660
|
+
const isFieldExcluded = React.useCallback(
|
|
661
|
+
(fieldName: string) => {
|
|
662
|
+
if (excludedFields.has(fieldName)) return true;
|
|
663
|
+
const fieldConfig = config?.fields?.[fieldName];
|
|
664
|
+
if (fieldConfig?.hidden) return true;
|
|
665
|
+
return false;
|
|
666
|
+
},
|
|
667
|
+
[config?.fields, excludedFields],
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
const renderFieldNode = React.useCallback(
|
|
671
|
+
(fieldName: string) => {
|
|
672
|
+
if (isFieldExcluded(fieldName)) return null;
|
|
673
|
+
const fieldConfig = config?.fields?.[fieldName];
|
|
674
|
+
const fullFieldName = getFullFieldName(fieldName, fieldPrefix);
|
|
675
|
+
if (renderField) {
|
|
676
|
+
return renderField(fullFieldName, fieldConfig);
|
|
677
|
+
}
|
|
678
|
+
return (
|
|
679
|
+
<FieldRenderer
|
|
680
|
+
key={fullFieldName}
|
|
681
|
+
fieldName={fieldName}
|
|
682
|
+
fieldConfig={fieldConfig}
|
|
683
|
+
collection={collection as string}
|
|
684
|
+
registry={registry}
|
|
685
|
+
fieldPrefix={fieldPrefix}
|
|
686
|
+
allCollectionsConfig={allCollectionsConfig}
|
|
687
|
+
/>
|
|
688
|
+
);
|
|
689
|
+
},
|
|
690
|
+
[
|
|
691
|
+
collection,
|
|
692
|
+
config?.fields,
|
|
693
|
+
fieldPrefix,
|
|
694
|
+
isFieldExcluded,
|
|
695
|
+
registry,
|
|
696
|
+
renderField,
|
|
697
|
+
allCollectionsConfig,
|
|
698
|
+
],
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
const renderFieldLayout = React.useCallback(
|
|
702
|
+
(field: FieldLayout, layout: SectionLayout, columns: number) => {
|
|
703
|
+
const fieldNode = renderFieldNode(field.field);
|
|
704
|
+
if (!fieldNode) return null;
|
|
705
|
+
|
|
706
|
+
const style: React.CSSProperties = {};
|
|
707
|
+
if (layout === "columns" || layout === "grid") {
|
|
708
|
+
const span = resolveSpan(field.span, columns);
|
|
709
|
+
if (span) {
|
|
710
|
+
style.gridColumn = `span ${span} / span ${span}`;
|
|
711
|
+
}
|
|
712
|
+
if (field.rowSpan) {
|
|
713
|
+
style.gridRow = `span ${field.rowSpan} / span ${field.rowSpan}`;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (field.width) {
|
|
718
|
+
style.width = field.width;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return (
|
|
722
|
+
<div
|
|
723
|
+
key={field.field}
|
|
724
|
+
style={style}
|
|
725
|
+
className={cn(
|
|
726
|
+
layout === "inline" ? "min-w-[180px] flex-1" : "min-w-0",
|
|
727
|
+
)}
|
|
728
|
+
>
|
|
729
|
+
{fieldNode}
|
|
730
|
+
</div>
|
|
731
|
+
);
|
|
732
|
+
},
|
|
733
|
+
[renderFieldNode],
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
const renderFieldsContainer = React.useCallback(
|
|
737
|
+
(
|
|
738
|
+
fields: FieldLayout[],
|
|
739
|
+
layout: SectionLayout,
|
|
740
|
+
columns: number,
|
|
741
|
+
grid?: SectionConfig["grid"],
|
|
742
|
+
) => {
|
|
743
|
+
const normalizedFields = fields.filter(
|
|
744
|
+
(field) => !isFieldExcluded(field.field),
|
|
745
|
+
);
|
|
746
|
+
if (!normalizedFields.length) return null;
|
|
747
|
+
|
|
748
|
+
if (layout === "inline") {
|
|
749
|
+
return (
|
|
750
|
+
<div
|
|
751
|
+
className="flex flex-wrap gap-4"
|
|
752
|
+
style={{ gap: getGapStyle(grid?.gap) }}
|
|
753
|
+
>
|
|
754
|
+
{normalizedFields.map((field) =>
|
|
755
|
+
renderFieldLayout(field, layout, columns),
|
|
756
|
+
)}
|
|
757
|
+
</div>
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (layout === "columns" || layout === "grid") {
|
|
762
|
+
const baseClass = getGridColumnsClass(columns);
|
|
763
|
+
const gridClassName = cn(
|
|
764
|
+
"grid gap-4",
|
|
765
|
+
baseClass,
|
|
766
|
+
getGridColumnsClass(grid?.responsive?.sm, "sm"),
|
|
767
|
+
getGridColumnsClass(grid?.responsive?.md, "md"),
|
|
768
|
+
getGridColumnsClass(grid?.responsive?.lg, "lg"),
|
|
769
|
+
);
|
|
770
|
+
const style: React.CSSProperties = {
|
|
771
|
+
gap: getGapStyle(grid?.gap),
|
|
772
|
+
...(baseClass
|
|
773
|
+
? {}
|
|
774
|
+
: {
|
|
775
|
+
gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
|
|
776
|
+
}),
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
return (
|
|
780
|
+
<div className={gridClassName} style={style}>
|
|
781
|
+
{normalizedFields.map((field) =>
|
|
782
|
+
renderFieldLayout(field, layout, columns),
|
|
783
|
+
)}
|
|
784
|
+
</div>
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return (
|
|
789
|
+
<div className="space-y-4">
|
|
790
|
+
{normalizedFields.map((field) =>
|
|
791
|
+
renderFieldLayout(field, layout, columns),
|
|
792
|
+
)}
|
|
793
|
+
</div>
|
|
794
|
+
);
|
|
795
|
+
},
|
|
796
|
+
[isFieldExcluded, renderFieldLayout],
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
const renderSection = React.useCallback(
|
|
800
|
+
(section: SectionConfig, index: number, excludedSet?: Set<string>) => {
|
|
801
|
+
const isVisible = resolveValue(section.visible, formValues, true);
|
|
802
|
+
|
|
803
|
+
if (!isVisible) return null;
|
|
804
|
+
|
|
805
|
+
const sectionFields = normalizeSectionFields(section.fields).filter(
|
|
806
|
+
(field) => !excludedSet?.has(field.field),
|
|
807
|
+
);
|
|
808
|
+
|
|
809
|
+
if (!sectionFields.length) return null;
|
|
810
|
+
|
|
811
|
+
const layout = section.layout ?? "auto";
|
|
812
|
+
const columns =
|
|
813
|
+
layout === "columns"
|
|
814
|
+
? section.columns || section.grid?.columns || 2
|
|
815
|
+
: section.grid?.columns || section.columns || 1;
|
|
816
|
+
const content = renderFieldsContainer(
|
|
817
|
+
sectionFields,
|
|
818
|
+
layout,
|
|
819
|
+
columns,
|
|
820
|
+
section.grid,
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
if (!content) return null;
|
|
824
|
+
|
|
825
|
+
const header =
|
|
826
|
+
section.title || section.description ? (
|
|
827
|
+
<div>
|
|
828
|
+
{section.title && (
|
|
829
|
+
<h3 className="text-lg font-semibold">{section.title}</h3>
|
|
830
|
+
)}
|
|
831
|
+
{section.description && (
|
|
832
|
+
<p className="text-sm text-muted-foreground">
|
|
833
|
+
{section.description}
|
|
834
|
+
</p>
|
|
835
|
+
)}
|
|
836
|
+
</div>
|
|
837
|
+
) : null;
|
|
838
|
+
|
|
839
|
+
if (section.collapsible) {
|
|
840
|
+
const value = `section-${index}`;
|
|
841
|
+
return (
|
|
842
|
+
<Accordion
|
|
843
|
+
key={value}
|
|
844
|
+
defaultValue={section.defaultOpen ? [value] : []}
|
|
845
|
+
>
|
|
846
|
+
<AccordionItem value={value}>
|
|
847
|
+
<AccordionTrigger>{section.title || "Section"}</AccordionTrigger>
|
|
848
|
+
<AccordionContent className="space-y-3 pt-2">
|
|
849
|
+
{section.description && (
|
|
850
|
+
<p className="text-sm text-muted-foreground">
|
|
851
|
+
{section.description}
|
|
852
|
+
</p>
|
|
853
|
+
)}
|
|
854
|
+
{content}
|
|
855
|
+
</AccordionContent>
|
|
856
|
+
</AccordionItem>
|
|
857
|
+
</Accordion>
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
return (
|
|
862
|
+
<div key={index} className={cn("space-y-4", section.className)}>
|
|
863
|
+
{header}
|
|
864
|
+
{content}
|
|
865
|
+
</div>
|
|
866
|
+
);
|
|
867
|
+
},
|
|
868
|
+
[formValues, renderFieldsContainer],
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
const renderSections = React.useCallback(
|
|
872
|
+
(sections: SectionConfig[], excludedSet?: Set<string>) => {
|
|
873
|
+
const renderedSections = sections
|
|
874
|
+
.map((section, index) => renderSection(section, index, excludedSet))
|
|
875
|
+
.filter(Boolean);
|
|
876
|
+
|
|
877
|
+
if (!renderedSections.length) return null;
|
|
878
|
+
|
|
879
|
+
return <div className="space-y-6">{renderedSections}</div>;
|
|
880
|
+
},
|
|
881
|
+
[renderSection],
|
|
882
|
+
);
|
|
883
|
+
|
|
884
|
+
const renderTabs = React.useCallback(
|
|
885
|
+
(tabs: TabConfig[], excludedSet?: Set<string>) => {
|
|
886
|
+
const visibleTabs = tabs.filter((tab) =>
|
|
887
|
+
resolveValue(tab.visible, formValues, true),
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
if (!visibleTabs.length) return null;
|
|
891
|
+
|
|
892
|
+
const defaultTab = visibleTabs[0]?.id;
|
|
893
|
+
|
|
894
|
+
return (
|
|
895
|
+
<Tabs defaultValue={defaultTab}>
|
|
896
|
+
<TabsList variant="line">
|
|
897
|
+
{visibleTabs.map((tab) => (
|
|
898
|
+
<TabsTrigger key={tab.id} value={tab.id}>
|
|
899
|
+
{tab.label}
|
|
900
|
+
</TabsTrigger>
|
|
901
|
+
))}
|
|
902
|
+
</TabsList>
|
|
903
|
+
{visibleTabs.map((tab) => (
|
|
904
|
+
<TabsContent key={tab.id} value={tab.id}>
|
|
905
|
+
{tab.sections
|
|
906
|
+
? renderSections(tab.sections, excludedSet)
|
|
907
|
+
: renderFieldsContainer(
|
|
908
|
+
normalizeSectionFields(tab.fields || []).filter(
|
|
909
|
+
(field) => !excludedSet?.has(field.field),
|
|
910
|
+
),
|
|
911
|
+
"auto",
|
|
912
|
+
1,
|
|
913
|
+
)}
|
|
914
|
+
</TabsContent>
|
|
915
|
+
))}
|
|
916
|
+
</Tabs>
|
|
917
|
+
);
|
|
918
|
+
},
|
|
919
|
+
[formValues, renderFieldsContainer, renderSections],
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
const visibleFields = fieldOrder.filter(
|
|
923
|
+
(fieldName) => !isFieldExcluded(fieldName),
|
|
924
|
+
);
|
|
925
|
+
|
|
926
|
+
const defaultContent = config?.edit?.tabs
|
|
927
|
+
? renderTabs(config.edit.tabs, undefined)
|
|
928
|
+
: config?.edit?.sections
|
|
929
|
+
? renderSections(config.edit.sections, undefined)
|
|
930
|
+
: renderFieldsContainer(normalizeSectionFields(visibleFields), "auto", 1);
|
|
931
|
+
|
|
932
|
+
const isSidebarLayout =
|
|
933
|
+
config?.edit?.layout === "with-sidebar" && sidebarFieldSet.size > 0;
|
|
934
|
+
|
|
935
|
+
if (isSidebarLayout) {
|
|
936
|
+
const sidebarFields = Array.from(sidebarFieldSet).filter(
|
|
937
|
+
(fieldName) => !isFieldExcluded(fieldName),
|
|
938
|
+
);
|
|
939
|
+
const sidebarContent = renderFieldsContainer(
|
|
940
|
+
normalizeSectionFields(sidebarFields),
|
|
941
|
+
"auto",
|
|
942
|
+
1,
|
|
943
|
+
);
|
|
944
|
+
const mainContent = config?.edit?.tabs
|
|
945
|
+
? renderTabs(config.edit.tabs, sidebarFieldSet)
|
|
946
|
+
: config?.edit?.sections
|
|
947
|
+
? renderSections(config.edit.sections, sidebarFieldSet)
|
|
948
|
+
: renderFieldsContainer(
|
|
949
|
+
normalizeSectionFields(
|
|
950
|
+
visibleFields.filter(
|
|
951
|
+
(fieldName) => !sidebarFieldSet.has(fieldName),
|
|
952
|
+
),
|
|
953
|
+
),
|
|
954
|
+
"auto",
|
|
955
|
+
1,
|
|
956
|
+
);
|
|
957
|
+
|
|
958
|
+
const sidebarPosition = config?.edit?.sidebar?.position || "right";
|
|
959
|
+
const sidebarWidth = config?.edit?.sidebar?.width || "280px";
|
|
960
|
+
|
|
961
|
+
return (
|
|
962
|
+
<div
|
|
963
|
+
className={cn(
|
|
964
|
+
"flex flex-col gap-6 lg:flex-row",
|
|
965
|
+
sidebarPosition === "left" ? "lg:flex-row-reverse" : "",
|
|
966
|
+
)}
|
|
967
|
+
>
|
|
968
|
+
<div className="min-w-0 flex-1">{mainContent}</div>
|
|
969
|
+
{sidebarContent && (
|
|
970
|
+
<aside
|
|
971
|
+
className="rounded-md border bg-card p-4"
|
|
972
|
+
style={{ width: sidebarWidth }}
|
|
973
|
+
>
|
|
974
|
+
<div className="space-y-4">{sidebarContent}</div>
|
|
975
|
+
</aside>
|
|
976
|
+
)}
|
|
977
|
+
</div>
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
return <div>{defaultContent}</div>;
|
|
982
|
+
}
|