@questpie/admin 0.0.1 → 1.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.
Files changed (250) hide show
  1. package/README.md +439 -424
  2. package/dist/auth-layout-M8K8_q5R.mjs +181 -0
  3. package/dist/auth-layout-M8K8_q5R.mjs.map +1 -0
  4. package/dist/bulk-upload-dialog-D7w7W1Hl.mjs +273 -0
  5. package/dist/bulk-upload-dialog-D7w7W1Hl.mjs.map +1 -0
  6. package/dist/{components/ui/card.mjs → card-BKHjBQfw.mjs} +8 -8
  7. package/dist/card-BKHjBQfw.mjs.map +1 -0
  8. package/dist/client/styles/index.css +434 -0
  9. package/dist/client-DbpZKSgH.d.mts +13585 -0
  10. package/dist/client-DbpZKSgH.d.mts.map +1 -0
  11. package/dist/client-njX1rZmi.mjs +22612 -0
  12. package/dist/client-njX1rZmi.mjs.map +1 -0
  13. package/dist/client.d.mts +3 -0
  14. package/dist/client.mjs +13 -0
  15. package/dist/content-locales-provider-BXvuIgfg.mjs +1650 -0
  16. package/dist/content-locales-provider-BXvuIgfg.mjs.map +1 -0
  17. package/dist/dashboard-page-B4PGEdc2.mjs +2500 -0
  18. package/dist/dashboard-page-B4PGEdc2.mjs.map +1 -0
  19. package/dist/dashboard-page-mCY0pgZv.mjs +3 -0
  20. package/dist/dropzone-Do3awXKd.mjs +634 -0
  21. package/dist/dropzone-Do3awXKd.mjs.map +1 -0
  22. package/dist/{views/auth/forgot-password-form.mjs → forgot-password-page-Bcp-An4Y.mjs} +87 -14
  23. package/dist/forgot-password-page-Bcp-An4Y.mjs.map +1 -0
  24. package/dist/forgot-password-page-CEwsdLwn.mjs +3 -0
  25. package/dist/index-B9Xwk4hi.d.mts +2753 -0
  26. package/dist/index-B9Xwk4hi.d.mts.map +1 -0
  27. package/dist/index.d.mts +3 -0
  28. package/dist/index.mjs +13 -0
  29. package/dist/login-page-BUnpCbCa.mjs +3 -0
  30. package/dist/login-page-CP4gA-dl.mjs +298 -0
  31. package/dist/login-page-CP4gA-dl.mjs.map +1 -0
  32. package/dist/preview-utils-BKQ9-TMa.mjs +65 -0
  33. package/dist/preview-utils-BKQ9-TMa.mjs.map +1 -0
  34. package/dist/{views/auth/reset-password-form.mjs → reset-password-page-BqfDmLxA.mjs} +111 -14
  35. package/dist/reset-password-page-BqfDmLxA.mjs.map +1 -0
  36. package/dist/reset-password-page-CufHz3h3.mjs +3 -0
  37. package/dist/runtime-6VZM878K.mjs +69 -0
  38. package/dist/runtime-6VZM878K.mjs.map +1 -0
  39. package/dist/saved-views.types-BMsz5mCy.d.mts +42 -0
  40. package/dist/saved-views.types-BMsz5mCy.d.mts.map +1 -0
  41. package/dist/server.d.mts +250 -0
  42. package/dist/server.d.mts.map +1 -0
  43. package/dist/server.mjs +832 -0
  44. package/dist/server.mjs.map +1 -0
  45. package/dist/setup-page-BNNzt_Z6.mjs +3 -0
  46. package/dist/setup-page-YAP_fzqh.mjs +264 -0
  47. package/dist/setup-page-YAP_fzqh.mjs.map +1 -0
  48. package/dist/shared.d.mts +57 -0
  49. package/dist/shared.d.mts.map +1 -0
  50. package/dist/shared.mjs +3 -0
  51. package/dist/{hooks/use-auth.mjs → use-auth-BoLmWtmU.mjs} +42 -30
  52. package/dist/use-auth-BoLmWtmU.mjs.map +1 -0
  53. package/package.json +48 -198
  54. package/.turbo/turbo-build.log +0 -108
  55. package/CHANGELOG.md +0 -10
  56. package/STATUS.md +0 -917
  57. package/VALIDATION.md +0 -602
  58. package/components.json +0 -24
  59. package/dist/__tests__/setup.mjs +0 -38
  60. package/dist/__tests__/test-utils.mjs +0 -45
  61. package/dist/__tests__/vitest.d.mjs +0 -3
  62. package/dist/components/admin-app.mjs +0 -69
  63. package/dist/components/fields/array-field.mjs +0 -190
  64. package/dist/components/fields/checkbox-field.mjs +0 -34
  65. package/dist/components/fields/custom-field.mjs +0 -32
  66. package/dist/components/fields/date-field.mjs +0 -41
  67. package/dist/components/fields/datetime-field.mjs +0 -42
  68. package/dist/components/fields/email-field.mjs +0 -37
  69. package/dist/components/fields/embedded-collection.mjs +0 -253
  70. package/dist/components/fields/field-types.mjs +0 -1
  71. package/dist/components/fields/field-utils.mjs +0 -10
  72. package/dist/components/fields/field-wrapper.mjs +0 -34
  73. package/dist/components/fields/index.mjs +0 -23
  74. package/dist/components/fields/json-field.mjs +0 -243
  75. package/dist/components/fields/locale-badge.mjs +0 -16
  76. package/dist/components/fields/number-field.mjs +0 -39
  77. package/dist/components/fields/password-field.mjs +0 -37
  78. package/dist/components/fields/relation-field.mjs +0 -104
  79. package/dist/components/fields/relation-picker.mjs +0 -229
  80. package/dist/components/fields/relation-select.mjs +0 -188
  81. package/dist/components/fields/rich-text-editor/index.mjs +0 -897
  82. package/dist/components/fields/select-field.mjs +0 -41
  83. package/dist/components/fields/switch-field.mjs +0 -34
  84. package/dist/components/fields/text-field.mjs +0 -38
  85. package/dist/components/fields/textarea-field.mjs +0 -38
  86. package/dist/components/index.mjs +0 -59
  87. package/dist/components/primitives/checkbox-input.mjs +0 -127
  88. package/dist/components/primitives/date-input.mjs +0 -303
  89. package/dist/components/primitives/index.mjs +0 -12
  90. package/dist/components/primitives/number-input.mjs +0 -104
  91. package/dist/components/primitives/select-input.mjs +0 -177
  92. package/dist/components/primitives/tag-input.mjs +0 -135
  93. package/dist/components/primitives/text-input.mjs +0 -39
  94. package/dist/components/primitives/textarea-input.mjs +0 -37
  95. package/dist/components/primitives/toggle-input.mjs +0 -31
  96. package/dist/components/primitives/types.mjs +0 -12
  97. package/dist/components/ui/accordion.mjs +0 -55
  98. package/dist/components/ui/avatar.mjs +0 -54
  99. package/dist/components/ui/badge.mjs +0 -34
  100. package/dist/components/ui/button.mjs +0 -48
  101. package/dist/components/ui/checkbox.mjs +0 -21
  102. package/dist/components/ui/combobox.mjs +0 -163
  103. package/dist/components/ui/dialog.mjs +0 -95
  104. package/dist/components/ui/dropdown-menu.mjs +0 -138
  105. package/dist/components/ui/field.mjs +0 -113
  106. package/dist/components/ui/input-group.mjs +0 -82
  107. package/dist/components/ui/input.mjs +0 -17
  108. package/dist/components/ui/label.mjs +0 -15
  109. package/dist/components/ui/popover.mjs +0 -56
  110. package/dist/components/ui/scroll-area.mjs +0 -38
  111. package/dist/components/ui/select.mjs +0 -100
  112. package/dist/components/ui/separator.mjs +0 -16
  113. package/dist/components/ui/sheet.mjs +0 -90
  114. package/dist/components/ui/sidebar.mjs +0 -387
  115. package/dist/components/ui/skeleton.mjs +0 -14
  116. package/dist/components/ui/spinner.mjs +0 -16
  117. package/dist/components/ui/switch.mjs +0 -22
  118. package/dist/components/ui/table.mjs +0 -68
  119. package/dist/components/ui/tabs.mjs +0 -48
  120. package/dist/components/ui/textarea.mjs +0 -15
  121. package/dist/components/ui/tooltip.mjs +0 -44
  122. package/dist/config/component-registry.mjs +0 -38
  123. package/dist/config/index.mjs +0 -129
  124. package/dist/hooks/admin-provider.mjs +0 -70
  125. package/dist/hooks/index.mjs +0 -7
  126. package/dist/hooks/store.mjs +0 -178
  127. package/dist/hooks/use-collection-db.mjs +0 -146
  128. package/dist/hooks/use-collection.mjs +0 -112
  129. package/dist/hooks/use-global.mjs +0 -46
  130. package/dist/hooks/use-mobile.mjs +0 -20
  131. package/dist/lib/utils.mjs +0 -10
  132. package/dist/styles/index.css +0 -336
  133. package/dist/styles/index.mjs +0 -1
  134. package/dist/utils/index.mjs +0 -9
  135. package/dist/views/auth/auth-layout.mjs +0 -52
  136. package/dist/views/auth/index.mjs +0 -6
  137. package/dist/views/auth/login-form.mjs +0 -156
  138. package/dist/views/collection/auto-form-fields.mjs +0 -525
  139. package/dist/views/collection/collection-form.mjs +0 -91
  140. package/dist/views/collection/collection-list.mjs +0 -76
  141. package/dist/views/collection/form-field.mjs +0 -42
  142. package/dist/views/collection/index.mjs +0 -6
  143. package/dist/views/common/index.mjs +0 -4
  144. package/dist/views/common/locale-switcher.mjs +0 -39
  145. package/dist/views/common/version-history.mjs +0 -272
  146. package/dist/views/index.mjs +0 -9
  147. package/dist/views/layout/admin-layout.mjs +0 -40
  148. package/dist/views/layout/admin-router.mjs +0 -95
  149. package/dist/views/layout/admin-sidebar.mjs +0 -63
  150. package/dist/views/layout/index.mjs +0 -5
  151. package/src/__tests__/setup.ts +0 -44
  152. package/src/__tests__/test-utils.tsx +0 -49
  153. package/src/__tests__/vitest.d.ts +0 -9
  154. package/src/components/admin-app.tsx +0 -221
  155. package/src/components/fields/array-field.tsx +0 -237
  156. package/src/components/fields/checkbox-field.tsx +0 -47
  157. package/src/components/fields/custom-field.tsx +0 -50
  158. package/src/components/fields/date-field.tsx +0 -65
  159. package/src/components/fields/datetime-field.tsx +0 -67
  160. package/src/components/fields/email-field.tsx +0 -51
  161. package/src/components/fields/embedded-collection.tsx +0 -315
  162. package/src/components/fields/field-types.ts +0 -162
  163. package/src/components/fields/field-utils.ts +0 -6
  164. package/src/components/fields/field-wrapper.tsx +0 -52
  165. package/src/components/fields/index.ts +0 -66
  166. package/src/components/fields/json-field.tsx +0 -440
  167. package/src/components/fields/locale-badge.tsx +0 -15
  168. package/src/components/fields/number-field.tsx +0 -57
  169. package/src/components/fields/password-field.tsx +0 -51
  170. package/src/components/fields/relation-field.tsx +0 -243
  171. package/src/components/fields/relation-picker.tsx +0 -402
  172. package/src/components/fields/relation-select.tsx +0 -327
  173. package/src/components/fields/rich-text-editor/index.tsx +0 -1337
  174. package/src/components/fields/select-field.tsx +0 -61
  175. package/src/components/fields/switch-field.tsx +0 -47
  176. package/src/components/fields/text-field.tsx +0 -55
  177. package/src/components/fields/textarea-field.tsx +0 -55
  178. package/src/components/index.ts +0 -40
  179. package/src/components/primitives/checkbox-input.tsx +0 -193
  180. package/src/components/primitives/date-input.tsx +0 -401
  181. package/src/components/primitives/index.ts +0 -24
  182. package/src/components/primitives/number-input.tsx +0 -132
  183. package/src/components/primitives/select-input.tsx +0 -296
  184. package/src/components/primitives/tag-input.tsx +0 -200
  185. package/src/components/primitives/text-input.tsx +0 -49
  186. package/src/components/primitives/textarea-input.tsx +0 -46
  187. package/src/components/primitives/toggle-input.tsx +0 -36
  188. package/src/components/primitives/types.ts +0 -235
  189. package/src/components/ui/accordion.tsx +0 -72
  190. package/src/components/ui/avatar.tsx +0 -106
  191. package/src/components/ui/badge.tsx +0 -48
  192. package/src/components/ui/button.tsx +0 -53
  193. package/src/components/ui/card.tsx +0 -94
  194. package/src/components/ui/checkbox.tsx +0 -27
  195. package/src/components/ui/combobox.tsx +0 -290
  196. package/src/components/ui/dialog.tsx +0 -151
  197. package/src/components/ui/dropdown-menu.tsx +0 -254
  198. package/src/components/ui/field.tsx +0 -227
  199. package/src/components/ui/input-group.tsx +0 -149
  200. package/src/components/ui/input.tsx +0 -20
  201. package/src/components/ui/label.tsx +0 -18
  202. package/src/components/ui/popover.tsx +0 -88
  203. package/src/components/ui/scroll-area.tsx +0 -53
  204. package/src/components/ui/select.tsx +0 -192
  205. package/src/components/ui/separator.tsx +0 -23
  206. package/src/components/ui/sheet.tsx +0 -127
  207. package/src/components/ui/sidebar.tsx +0 -723
  208. package/src/components/ui/skeleton.tsx +0 -13
  209. package/src/components/ui/spinner.tsx +0 -10
  210. package/src/components/ui/switch.tsx +0 -32
  211. package/src/components/ui/table.tsx +0 -99
  212. package/src/components/ui/tabs.tsx +0 -82
  213. package/src/components/ui/textarea.tsx +0 -18
  214. package/src/components/ui/tooltip.tsx +0 -70
  215. package/src/config/component-registry.ts +0 -190
  216. package/src/config/index.ts +0 -1099
  217. package/src/hooks/README.md +0 -269
  218. package/src/hooks/admin-provider.tsx +0 -110
  219. package/src/hooks/index.ts +0 -41
  220. package/src/hooks/store.ts +0 -248
  221. package/src/hooks/use-auth.ts +0 -168
  222. package/src/hooks/use-collection-db.ts +0 -209
  223. package/src/hooks/use-collection.ts +0 -156
  224. package/src/hooks/use-global.ts +0 -69
  225. package/src/hooks/use-mobile.ts +0 -21
  226. package/src/lib/utils.ts +0 -6
  227. package/src/styles/index.css +0 -340
  228. package/src/utils/index.ts +0 -6
  229. package/src/views/auth/auth-layout.tsx +0 -77
  230. package/src/views/auth/forgot-password-form.tsx +0 -192
  231. package/src/views/auth/index.ts +0 -21
  232. package/src/views/auth/login-form.tsx +0 -229
  233. package/src/views/auth/reset-password-form.tsx +0 -232
  234. package/src/views/collection/auto-form-fields.tsx +0 -982
  235. package/src/views/collection/collection-form.tsx +0 -186
  236. package/src/views/collection/collection-list.tsx +0 -223
  237. package/src/views/collection/form-field.tsx +0 -52
  238. package/src/views/collection/index.ts +0 -15
  239. package/src/views/common/index.ts +0 -8
  240. package/src/views/common/locale-switcher.tsx +0 -45
  241. package/src/views/common/version-history.tsx +0 -406
  242. package/src/views/index.ts +0 -25
  243. package/src/views/layout/admin-layout.tsx +0 -117
  244. package/src/views/layout/admin-router.tsx +0 -206
  245. package/src/views/layout/admin-sidebar.tsx +0 -185
  246. package/src/views/layout/index.ts +0 -12
  247. package/tsconfig.json +0 -13
  248. package/tsconfig.tsbuildinfo +0 -1
  249. package/tsdown.config.ts +0 -13
  250. package/vitest.config.ts +0 -29
@@ -1,982 +0,0 @@
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
- }