@qwickapps/react-framework 1.5.7 → 1.5.9

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 (211) hide show
  1. package/dist/components/AccessibilityChecker.d.ts.map +1 -1
  2. package/dist/components/Html.d.ts +1 -1
  3. package/dist/components/Html.d.ts.map +1 -1
  4. package/dist/components/Logo.d.ts.map +1 -1
  5. package/dist/components/Markdown.d.ts +2 -2
  6. package/dist/components/Markdown.d.ts.map +1 -1
  7. package/dist/components/SafeSpan.d.ts +1 -1
  8. package/dist/components/SafeSpan.d.ts.map +1 -1
  9. package/dist/components/base/ModelView.d.ts +1 -1
  10. package/dist/components/base/ModelView.d.ts.map +1 -1
  11. package/dist/components/blocks/Article.d.ts +1 -1
  12. package/dist/components/blocks/Article.d.ts.map +1 -1
  13. package/dist/components/blocks/CardListGrid.d.ts.map +1 -1
  14. package/dist/components/blocks/Code.d.ts.map +1 -1
  15. package/dist/components/blocks/Content.d.ts.map +1 -1
  16. package/dist/components/blocks/CoverImageHeader.d.ts.map +1 -1
  17. package/dist/components/blocks/FeatureCard.d.ts.map +1 -1
  18. package/dist/components/blocks/FeatureGrid.d.ts.map +1 -1
  19. package/dist/components/blocks/Footer.d.ts.map +1 -1
  20. package/dist/components/blocks/Image.d.ts.map +1 -1
  21. package/dist/components/blocks/PageBannerHeader.d.ts.map +1 -1
  22. package/dist/components/blocks/ProductCard.d.ts.map +1 -1
  23. package/dist/components/blocks/Section.d.ts.map +1 -1
  24. package/dist/components/blocks/Text.d.ts +8 -1
  25. package/dist/components/blocks/Text.d.ts.map +1 -1
  26. package/dist/components/buttons/Button.d.ts.map +1 -1
  27. package/dist/components/buttons/PaletteSwitcher.d.ts.map +1 -1
  28. package/dist/components/buttons/ThemeSwitcher.d.ts.map +1 -1
  29. package/dist/components/forms/FormBlock.d.ts +1 -1
  30. package/dist/components/forms/FormBlock.d.ts.map +1 -1
  31. package/dist/components/forms/SchemaFormRenderer.d.ts +28 -0
  32. package/dist/components/forms/SchemaFormRenderer.d.ts.map +1 -0
  33. package/dist/components/forms/index.d.ts +2 -0
  34. package/dist/components/forms/index.d.ts.map +1 -1
  35. package/dist/components/index.d.ts +1 -0
  36. package/dist/components/index.d.ts.map +1 -1
  37. package/dist/components/input/ChoiceInputField.d.ts.map +1 -1
  38. package/dist/components/input/HtmlInputField.d.ts.map +1 -1
  39. package/dist/components/layout/CollapsibleLayout/CollapsibleLayout.d.ts.map +1 -1
  40. package/dist/components/layout/GridLayout.d.ts +5 -0
  41. package/dist/components/layout/GridLayout.d.ts.map +1 -1
  42. package/dist/components/plugins/DataTable.d.ts +57 -0
  43. package/dist/components/plugins/DataTable.d.ts.map +1 -0
  44. package/dist/components/plugins/StatCard.d.ts +44 -0
  45. package/dist/components/plugins/StatCard.d.ts.map +1 -0
  46. package/dist/components/plugins/index.d.ts +13 -0
  47. package/dist/components/plugins/index.d.ts.map +1 -0
  48. package/dist/components/shared/createSerializableView.d.ts.map +1 -1
  49. package/dist/hooks/useBaseProps.d.ts +1161 -12
  50. package/dist/hooks/useBaseProps.d.ts.map +1 -1
  51. package/dist/index.esm.js +5468 -5216
  52. package/dist/index.js +5572 -5317
  53. package/dist/palettes/manifest.json +19 -19
  54. package/dist/schemas/transformers/ReactNodeTransformer.d.ts.map +1 -1
  55. package/dist/utils/iconMap.d.ts.map +1 -1
  56. package/package.json +6 -5
  57. package/src/components/AccessibilityChecker.tsx +10 -7
  58. package/src/components/ErrorBoundary.tsx +3 -3
  59. package/src/components/Html.tsx +17 -12
  60. package/src/components/Logo.tsx +1 -8
  61. package/src/components/Markdown.tsx +12 -12
  62. package/src/components/ResponsiveMenu.tsx +1 -1
  63. package/src/components/SafeSpan.tsx +10 -10
  64. package/src/components/Scaffold.tsx +4 -4
  65. package/src/components/base/ModelView.tsx +2 -2
  66. package/src/components/blocks/Article.tsx +8 -8
  67. package/src/components/blocks/CardListGrid.tsx +1 -3
  68. package/src/components/blocks/Code.tsx +10 -8
  69. package/src/components/blocks/Content.tsx +2 -4
  70. package/src/components/blocks/CoverImageHeader.tsx +3 -4
  71. package/src/components/blocks/FeatureCard.tsx +2 -4
  72. package/src/components/blocks/FeatureGrid.tsx +2 -4
  73. package/src/components/blocks/Footer.tsx +2 -4
  74. package/src/components/blocks/Image.tsx +10 -7
  75. package/src/components/blocks/PageBannerHeader.tsx +3 -4
  76. package/src/components/blocks/ProductCard.tsx +8 -5
  77. package/src/components/blocks/Section.tsx +8 -6
  78. package/src/components/blocks/Text.tsx +22 -14
  79. package/src/components/buttons/Button.tsx +11 -9
  80. package/src/components/buttons/PaletteSwitcher.tsx +6 -8
  81. package/src/components/buttons/ThemeSwitcher.tsx +8 -9
  82. package/src/components/forms/Captcha.tsx +1 -1
  83. package/src/components/forms/FormBlock.tsx +3 -5
  84. package/src/components/forms/FormCheckbox.tsx +1 -1
  85. package/src/components/forms/FormField.tsx +1 -1
  86. package/src/components/forms/FormSelect.tsx +1 -1
  87. package/src/components/forms/SchemaFormRenderer.tsx +268 -0
  88. package/src/components/forms/__tests__/SchemaFormRenderer.test.tsx +212 -0
  89. package/src/components/forms/index.ts +3 -0
  90. package/src/components/index.ts +1 -0
  91. package/src/components/input/ChoiceInputField.tsx +2 -1
  92. package/src/components/input/HtmlInputField.tsx +14 -9
  93. package/src/components/input/TextField.tsx +1 -1
  94. package/src/components/layout/CollapsibleLayout/CollapsibleLayout.tsx +6 -8
  95. package/src/components/layout/GridLayout.tsx +4 -0
  96. package/src/components/plugins/DataTable.tsx +259 -0
  97. package/src/components/plugins/StatCard.tsx +122 -0
  98. package/src/components/plugins/__tests__/DataTable.test.tsx +158 -0
  99. package/src/components/plugins/index.ts +14 -0
  100. package/src/components/shared/createSerializableView.tsx +8 -6
  101. package/src/hooks/useBaseProps.ts +1 -1
  102. package/src/schemas/transformers/ReactNodeTransformer.ts +13 -10
  103. package/src/utils/iconMap.tsx +143 -83
  104. package/dist/palettes/palette-autumn.1.4.9.css +0 -172
  105. package/dist/palettes/palette-autumn.1.4.9.min.css +0 -1
  106. package/dist/palettes/palette-autumn.1.5.0.css +0 -172
  107. package/dist/palettes/palette-autumn.1.5.0.min.css +0 -1
  108. package/dist/palettes/palette-autumn.1.5.1.css +0 -172
  109. package/dist/palettes/palette-autumn.1.5.1.min.css +0 -1
  110. package/dist/palettes/palette-autumn.1.5.2.css +0 -172
  111. package/dist/palettes/palette-autumn.1.5.2.min.css +0 -1
  112. package/dist/palettes/palette-autumn.1.5.4.css +0 -172
  113. package/dist/palettes/palette-autumn.1.5.4.min.css +0 -1
  114. package/dist/palettes/palette-autumn.1.5.5.css +0 -172
  115. package/dist/palettes/palette-autumn.1.5.5.min.css +0 -1
  116. package/dist/palettes/palette-autumn.1.5.6.css +0 -172
  117. package/dist/palettes/palette-autumn.1.5.6.min.css +0 -1
  118. package/dist/palettes/palette-autumn.1.5.7.css +0 -172
  119. package/dist/palettes/palette-autumn.1.5.7.min.css +0 -1
  120. package/dist/palettes/palette-cosmic.1.4.9.css +0 -172
  121. package/dist/palettes/palette-cosmic.1.4.9.min.css +0 -1
  122. package/dist/palettes/palette-cosmic.1.5.0.css +0 -172
  123. package/dist/palettes/palette-cosmic.1.5.0.min.css +0 -1
  124. package/dist/palettes/palette-cosmic.1.5.1.css +0 -172
  125. package/dist/palettes/palette-cosmic.1.5.1.min.css +0 -1
  126. package/dist/palettes/palette-cosmic.1.5.2.css +0 -172
  127. package/dist/palettes/palette-cosmic.1.5.2.min.css +0 -1
  128. package/dist/palettes/palette-cosmic.1.5.4.css +0 -172
  129. package/dist/palettes/palette-cosmic.1.5.4.min.css +0 -1
  130. package/dist/palettes/palette-cosmic.1.5.5.css +0 -172
  131. package/dist/palettes/palette-cosmic.1.5.5.min.css +0 -1
  132. package/dist/palettes/palette-cosmic.1.5.6.css +0 -172
  133. package/dist/palettes/palette-cosmic.1.5.6.min.css +0 -1
  134. package/dist/palettes/palette-cosmic.1.5.7.css +0 -172
  135. package/dist/palettes/palette-cosmic.1.5.7.min.css +0 -1
  136. package/dist/palettes/palette-default.1.4.9.css +0 -178
  137. package/dist/palettes/palette-default.1.4.9.min.css +0 -1
  138. package/dist/palettes/palette-default.1.5.0.css +0 -178
  139. package/dist/palettes/palette-default.1.5.0.min.css +0 -1
  140. package/dist/palettes/palette-default.1.5.1.css +0 -178
  141. package/dist/palettes/palette-default.1.5.1.min.css +0 -1
  142. package/dist/palettes/palette-default.1.5.2.css +0 -178
  143. package/dist/palettes/palette-default.1.5.2.min.css +0 -1
  144. package/dist/palettes/palette-default.1.5.4.css +0 -178
  145. package/dist/palettes/palette-default.1.5.4.min.css +0 -1
  146. package/dist/palettes/palette-default.1.5.5.css +0 -178
  147. package/dist/palettes/palette-default.1.5.5.min.css +0 -1
  148. package/dist/palettes/palette-default.1.5.6.css +0 -178
  149. package/dist/palettes/palette-default.1.5.6.min.css +0 -1
  150. package/dist/palettes/palette-default.1.5.7.css +0 -178
  151. package/dist/palettes/palette-default.1.5.7.min.css +0 -1
  152. package/dist/palettes/palette-ocean.1.4.9.css +0 -172
  153. package/dist/palettes/palette-ocean.1.4.9.min.css +0 -1
  154. package/dist/palettes/palette-ocean.1.5.0.css +0 -172
  155. package/dist/palettes/palette-ocean.1.5.0.min.css +0 -1
  156. package/dist/palettes/palette-ocean.1.5.1.css +0 -172
  157. package/dist/palettes/palette-ocean.1.5.1.min.css +0 -1
  158. package/dist/palettes/palette-ocean.1.5.2.css +0 -172
  159. package/dist/palettes/palette-ocean.1.5.2.min.css +0 -1
  160. package/dist/palettes/palette-ocean.1.5.4.css +0 -172
  161. package/dist/palettes/palette-ocean.1.5.4.min.css +0 -1
  162. package/dist/palettes/palette-ocean.1.5.5.css +0 -172
  163. package/dist/palettes/palette-ocean.1.5.5.min.css +0 -1
  164. package/dist/palettes/palette-ocean.1.5.6.css +0 -172
  165. package/dist/palettes/palette-ocean.1.5.6.min.css +0 -1
  166. package/dist/palettes/palette-ocean.1.5.7.css +0 -172
  167. package/dist/palettes/palette-ocean.1.5.7.min.css +0 -1
  168. package/dist/palettes/palette-spring.1.4.9.css +0 -160
  169. package/dist/palettes/palette-spring.1.4.9.min.css +0 -1
  170. package/dist/palettes/palette-spring.1.5.0.css +0 -160
  171. package/dist/palettes/palette-spring.1.5.0.min.css +0 -1
  172. package/dist/palettes/palette-spring.1.5.1.css +0 -160
  173. package/dist/palettes/palette-spring.1.5.1.min.css +0 -1
  174. package/dist/palettes/palette-spring.1.5.2.css +0 -160
  175. package/dist/palettes/palette-spring.1.5.2.min.css +0 -1
  176. package/dist/palettes/palette-spring.1.5.4.css +0 -166
  177. package/dist/palettes/palette-spring.1.5.4.min.css +0 -1
  178. package/dist/palettes/palette-spring.1.5.5.css +0 -166
  179. package/dist/palettes/palette-spring.1.5.5.min.css +0 -1
  180. package/dist/palettes/palette-spring.1.5.6.css +0 -166
  181. package/dist/palettes/palette-spring.1.5.6.min.css +0 -1
  182. package/dist/palettes/palette-spring.1.5.7.css +0 -166
  183. package/dist/palettes/palette-spring.1.5.7.min.css +0 -1
  184. package/dist/palettes/palette-winter.1.4.9.css +0 -172
  185. package/dist/palettes/palette-winter.1.4.9.min.css +0 -1
  186. package/dist/palettes/palette-winter.1.5.0.css +0 -172
  187. package/dist/palettes/palette-winter.1.5.0.min.css +0 -1
  188. package/dist/palettes/palette-winter.1.5.1.css +0 -172
  189. package/dist/palettes/palette-winter.1.5.1.min.css +0 -1
  190. package/dist/palettes/palette-winter.1.5.2.css +0 -172
  191. package/dist/palettes/palette-winter.1.5.2.min.css +0 -1
  192. package/dist/palettes/palette-winter.1.5.4.css +0 -172
  193. package/dist/palettes/palette-winter.1.5.4.min.css +0 -1
  194. package/dist/palettes/palette-winter.1.5.5.css +0 -172
  195. package/dist/palettes/palette-winter.1.5.5.min.css +0 -1
  196. package/dist/palettes/palette-winter.1.5.6.css +0 -172
  197. package/dist/palettes/palette-winter.1.5.6.min.css +0 -1
  198. package/dist/palettes/palette-winter.1.5.7.css +0 -172
  199. package/dist/palettes/palette-winter.1.5.7.min.css +0 -1
  200. /package/dist/palettes/{palette-autumn.1.5.3.css → palette-autumn.1.5.9.css} +0 -0
  201. /package/dist/palettes/{palette-autumn.1.5.3.min.css → palette-autumn.1.5.9.min.css} +0 -0
  202. /package/dist/palettes/{palette-cosmic.1.5.3.css → palette-cosmic.1.5.9.css} +0 -0
  203. /package/dist/palettes/{palette-cosmic.1.5.3.min.css → palette-cosmic.1.5.9.min.css} +0 -0
  204. /package/dist/palettes/{palette-default.1.5.3.css → palette-default.1.5.9.css} +0 -0
  205. /package/dist/palettes/{palette-default.1.5.3.min.css → palette-default.1.5.9.min.css} +0 -0
  206. /package/dist/palettes/{palette-ocean.1.5.3.css → palette-ocean.1.5.9.css} +0 -0
  207. /package/dist/palettes/{palette-ocean.1.5.3.min.css → palette-ocean.1.5.9.min.css} +0 -0
  208. /package/dist/palettes/{palette-spring.1.5.3.css → palette-spring.1.5.9.css} +0 -0
  209. /package/dist/palettes/{palette-spring.1.5.3.min.css → palette-spring.1.5.9.min.css} +0 -0
  210. /package/dist/palettes/{palette-winter.1.5.3.css → palette-winter.1.5.9.css} +0 -0
  211. /package/dist/palettes/{palette-winter.1.5.3.min.css → palette-winter.1.5.9.min.css} +0 -0
@@ -0,0 +1,268 @@
1
+ /**
2
+ * SchemaFormRenderer - Dynamic form generation from @qwickapps/schema models
3
+ *
4
+ * Reads @Editor metadata from Model classes and generates Material-UI form fields.
5
+ * Maps field_type to appropriate input components with validation.
6
+ *
7
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
+ */
9
+
10
+ import React, { useState, useCallback } from 'react';
11
+ import {
12
+ TextField,
13
+ FormControlLabel,
14
+ Switch,
15
+ Box,
16
+ Typography,
17
+ FormHelperText,
18
+ Alert,
19
+ } from '@mui/material';
20
+ import { Model, FieldType } from '@qwickapps/schema';
21
+ import type { FieldDefinition } from '@qwickapps/schema';
22
+
23
+ export interface SchemaFormRendererProps<T extends Model> {
24
+ /** Model class to generate form from */
25
+ modelClass: new () => T;
26
+
27
+ /** Current form data */
28
+ value: Partial<T>;
29
+
30
+ /** Called when any field changes */
31
+ onChange: (data: Partial<T>) => void;
32
+
33
+ /** Show validation errors */
34
+ showValidation?: boolean;
35
+
36
+ /** Validation errors from Model.validate() */
37
+ validationErrors?: string[];
38
+
39
+ /** Read-only mode */
40
+ readOnly?: boolean;
41
+ }
42
+
43
+ /**
44
+ * Render a single form field based on its editor configuration
45
+ */
46
+ function renderField<T extends Model>(
47
+ field: FieldDefinition,
48
+ value: unknown,
49
+ onChange: (name: string, value: unknown) => void,
50
+ readOnly: boolean
51
+ ): React.ReactNode {
52
+ const { name, required, editor } = field;
53
+
54
+ if (!editor) {
55
+ return null;
56
+ }
57
+
58
+ const { field_type, label, description, placeholder, validation } = editor;
59
+ const fieldValue = value ?? '';
60
+
61
+ const commonProps = {
62
+ fullWidth: true,
63
+ margin: 'normal' as const,
64
+ required,
65
+ disabled: readOnly,
66
+ label,
67
+ helperText: description,
68
+ };
69
+
70
+ switch (field_type) {
71
+ case FieldType.TEXT:
72
+ case FieldType.EMAIL:
73
+ return (
74
+ <TextField
75
+ {...commonProps}
76
+ type={field_type === FieldType.EMAIL ? 'email' : 'text'}
77
+ value={fieldValue}
78
+ onChange={(e) => onChange(name, e.target.value)}
79
+ placeholder={placeholder}
80
+ inputProps={{
81
+ minLength: validation?.min,
82
+ maxLength: validation?.max,
83
+ }}
84
+ />
85
+ );
86
+
87
+ case FieldType.TEXTAREA:
88
+ return (
89
+ <TextField
90
+ {...commonProps}
91
+ multiline
92
+ rows={4}
93
+ value={fieldValue}
94
+ onChange={(e) => onChange(name, e.target.value)}
95
+ placeholder={placeholder}
96
+ inputProps={{
97
+ minLength: validation?.min,
98
+ maxLength: validation?.max,
99
+ }}
100
+ />
101
+ );
102
+
103
+ case FieldType.NUMBER:
104
+ return (
105
+ <TextField
106
+ {...commonProps}
107
+ type="number"
108
+ value={fieldValue}
109
+ onChange={(e) => {
110
+ const val = e.target.value;
111
+ onChange(name, val === '' ? undefined : parseFloat(val));
112
+ }}
113
+ placeholder={placeholder}
114
+ inputProps={{
115
+ min: validation?.min,
116
+ max: validation?.max,
117
+ }}
118
+ />
119
+ );
120
+
121
+ case FieldType.BOOLEAN:
122
+ return (
123
+ <FormControlLabel
124
+ control={
125
+ <Switch
126
+ checked={!!fieldValue}
127
+ onChange={(e) => onChange(name, e.target.checked)}
128
+ disabled={readOnly}
129
+ />
130
+ }
131
+ label={
132
+ <Box>
133
+ <Typography variant="body2" fontWeight={required ? 600 : 400}>
134
+ {label}
135
+ </Typography>
136
+ {description && (
137
+ <Typography variant="caption" color="text.secondary">
138
+ {description}
139
+ </Typography>
140
+ )}
141
+ </Box>
142
+ }
143
+ />
144
+ );
145
+
146
+ case FieldType.DATE_TIME:
147
+ return (
148
+ <TextField
149
+ {...commonProps}
150
+ type="datetime-local"
151
+ value={fieldValue}
152
+ onChange={(e) => onChange(name, e.target.value)}
153
+ InputLabelProps={{ shrink: true }}
154
+ />
155
+ );
156
+
157
+ case FieldType.COLOR:
158
+ return (
159
+ <Box>
160
+ <TextField
161
+ {...commonProps}
162
+ type="color"
163
+ value={fieldValue || '#000000'}
164
+ onChange={(e) => onChange(name, e.target.value)}
165
+ />
166
+ </Box>
167
+ );
168
+
169
+ case FieldType.IMAGE:
170
+ return (
171
+ <Box>
172
+ <Typography variant="body2" fontWeight={required ? 600 : 400}>
173
+ {label}
174
+ </Typography>
175
+ <TextField
176
+ fullWidth
177
+ margin="normal"
178
+ type="url"
179
+ value={fieldValue}
180
+ onChange={(e) => onChange(name, e.target.value)}
181
+ placeholder={placeholder || 'https://example.com/image.jpg'}
182
+ helperText={description}
183
+ disabled={readOnly}
184
+ />
185
+ </Box>
186
+ );
187
+
188
+ case FieldType.FORM:
189
+ // Nested form - would need recursive rendering
190
+ return (
191
+ <Alert severity="info" sx={{ my: 2 }}>
192
+ Nested form for {name} (not yet implemented)
193
+ </Alert>
194
+ );
195
+
196
+ case FieldType.MODEL_REPEATER:
197
+ // Array of nested forms
198
+ return (
199
+ <Alert severity="info" sx={{ my: 2 }}>
200
+ Array field {name} (not yet implemented)
201
+ </Alert>
202
+ );
203
+
204
+ default:
205
+ return (
206
+ <TextField
207
+ {...commonProps}
208
+ value={fieldValue}
209
+ onChange={(e) => onChange(name, e.target.value)}
210
+ placeholder={placeholder}
211
+ />
212
+ );
213
+ }
214
+ }
215
+
216
+ /**
217
+ * SchemaFormRenderer Component
218
+ */
219
+ export function SchemaFormRenderer<T extends Model>({
220
+ modelClass,
221
+ value,
222
+ onChange,
223
+ showValidation = false,
224
+ validationErrors = [],
225
+ readOnly = false,
226
+ }: SchemaFormRendererProps<T>) {
227
+ const schema = modelClass.getSchema();
228
+
229
+ const handleFieldChange = useCallback(
230
+ (fieldName: string, fieldValue: any) => {
231
+ onChange({
232
+ ...value,
233
+ [fieldName]: fieldValue,
234
+ });
235
+ },
236
+ [value, onChange]
237
+ );
238
+
239
+ return (
240
+ <Box>
241
+ {showValidation && validationErrors.length > 0 && (
242
+ <Alert severity="error" sx={{ mb: 2 }}>
243
+ <Typography variant="body2" fontWeight={600} gutterBottom>
244
+ Please fix the following errors:
245
+ </Typography>
246
+ <ul style={{ margin: 0, paddingLeft: 20 }}>
247
+ {validationErrors.map((error, idx) => (
248
+ <li key={idx}>
249
+ <Typography variant="body2">{error}</Typography>
250
+ </li>
251
+ ))}
252
+ </ul>
253
+ </Alert>
254
+ )}
255
+
256
+ {schema.fields.map((field) => (
257
+ <Box key={field.name}>
258
+ {renderField<T>(
259
+ field,
260
+ (value as any)[field.name],
261
+ handleFieldChange,
262
+ readOnly
263
+ )}
264
+ </Box>
265
+ ))}
266
+ </Box>
267
+ );
268
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * SchemaFormRenderer Tests
3
+ *
4
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
5
+ */
6
+
7
+ import { render, screen, fireEvent } from '@testing-library/react';
8
+ import { SchemaFormRenderer } from '../SchemaFormRenderer';
9
+ import { Model, Field, Editor, FieldType, Schema } from '@qwickapps/schema';
10
+
11
+ // Test Model with various field types
12
+ @Schema()
13
+ class TestModel extends Model {
14
+ @Field()
15
+ @Editor({ field_type: FieldType.TEXT, label: 'Text Field', description: 'Enter text' })
16
+ textField?: string;
17
+
18
+ @Field()
19
+ @Editor({ field_type: FieldType.EMAIL, label: 'Email Field' })
20
+ emailField?: string;
21
+
22
+ @Field()
23
+ @Editor({ field_type: FieldType.TEXTAREA, label: 'Textarea Field' })
24
+ textareaField?: string;
25
+
26
+ @Field()
27
+ @Editor({ field_type: FieldType.NUMBER, label: 'Number Field' })
28
+ numberField?: number;
29
+
30
+ @Field()
31
+ @Editor({ field_type: FieldType.BOOLEAN, label: 'Boolean Field', description: 'Toggle me' })
32
+ booleanField?: boolean;
33
+
34
+ @Field()
35
+ @Editor({ field_type: FieldType.DATE_TIME, label: 'DateTime Field' })
36
+ dateTimeField?: string;
37
+
38
+ @Field()
39
+ @Editor({ field_type: FieldType.COLOR, label: 'Color Field' })
40
+ colorField?: string;
41
+ }
42
+
43
+ describe('SchemaFormRenderer', () => {
44
+ const mockOnChange = jest.fn();
45
+
46
+ beforeEach(() => {
47
+ mockOnChange.mockClear();
48
+ });
49
+
50
+ it('renders all field types from schema', () => {
51
+ render(
52
+ <SchemaFormRenderer
53
+ modelClass={TestModel}
54
+ value={{}}
55
+ onChange={mockOnChange}
56
+ />
57
+ );
58
+
59
+ expect(screen.getByLabelText(/Text Field/i)).toBeInTheDocument();
60
+ expect(screen.getByLabelText(/Email Field/i)).toBeInTheDocument();
61
+ expect(screen.getByLabelText(/Textarea Field/i)).toBeInTheDocument();
62
+ expect(screen.getByLabelText(/Number Field/i)).toBeInTheDocument();
63
+ expect(screen.getByLabelText(/Boolean Field/i)).toBeInTheDocument();
64
+ expect(screen.getByLabelText(/DateTime Field/i)).toBeInTheDocument();
65
+ expect(screen.getByLabelText(/Color Field/i)).toBeInTheDocument();
66
+ });
67
+
68
+ it('displays field values correctly', () => {
69
+ const value = {
70
+ textField: 'test value',
71
+ emailField: 'test@example.com',
72
+ numberField: 42,
73
+ booleanField: true,
74
+ };
75
+
76
+ render(
77
+ <SchemaFormRenderer
78
+ modelClass={TestModel}
79
+ value={value}
80
+ onChange={mockOnChange}
81
+ />
82
+ );
83
+
84
+ expect(screen.getByDisplayValue('test value')).toBeInTheDocument();
85
+ expect(screen.getByDisplayValue('test@example.com')).toBeInTheDocument();
86
+ expect(screen.getByDisplayValue('42')).toBeInTheDocument();
87
+ expect(screen.getByRole('checkbox', { name: /Boolean Field/i })).toBeChecked();
88
+ });
89
+
90
+ it('calls onChange when text field changes', () => {
91
+ render(
92
+ <SchemaFormRenderer
93
+ modelClass={TestModel}
94
+ value={{}}
95
+ onChange={mockOnChange}
96
+ />
97
+ );
98
+
99
+ const textField = screen.getByLabelText(/Text Field/i);
100
+ fireEvent.change(textField, { target: { value: 'new value' } });
101
+
102
+ expect(mockOnChange).toHaveBeenCalledWith({ textField: 'new value' });
103
+ });
104
+
105
+ it('calls onChange when number field changes', () => {
106
+ render(
107
+ <SchemaFormRenderer
108
+ modelClass={TestModel}
109
+ value={{}}
110
+ onChange={mockOnChange}
111
+ />
112
+ );
113
+
114
+ const numberField = screen.getByLabelText(/Number Field/i);
115
+ fireEvent.change(numberField, { target: { value: '123' } });
116
+
117
+ expect(mockOnChange).toHaveBeenCalledWith({ numberField: 123 });
118
+ });
119
+
120
+ it('handles empty number field correctly (returns undefined, not NaN)', () => {
121
+ render(
122
+ <SchemaFormRenderer
123
+ modelClass={TestModel}
124
+ value={{ numberField: 42 }}
125
+ onChange={mockOnChange}
126
+ />
127
+ );
128
+
129
+ const numberField = screen.getByLabelText(/Number Field/i);
130
+ fireEvent.change(numberField, { target: { value: '' } });
131
+
132
+ expect(mockOnChange).toHaveBeenCalledWith({ numberField: undefined });
133
+ });
134
+
135
+ it('calls onChange when boolean field changes', () => {
136
+ render(
137
+ <SchemaFormRenderer
138
+ modelClass={TestModel}
139
+ value={{}}
140
+ onChange={mockOnChange}
141
+ />
142
+ );
143
+
144
+ const booleanField = screen.getByRole('checkbox', { name: /Boolean Field/i });
145
+ fireEvent.click(booleanField);
146
+
147
+ expect(mockOnChange).toHaveBeenCalledWith({ booleanField: true });
148
+ });
149
+
150
+ it('displays validation errors when showValidation is true', () => {
151
+ const errors = ['Field A is required', 'Field B must be positive'];
152
+
153
+ render(
154
+ <SchemaFormRenderer
155
+ modelClass={TestModel}
156
+ value={{}}
157
+ onChange={mockOnChange}
158
+ showValidation={true}
159
+ validationErrors={errors}
160
+ />
161
+ );
162
+
163
+ expect(screen.getByText(/Please fix the following errors/i)).toBeInTheDocument();
164
+ expect(screen.getByText('Field A is required')).toBeInTheDocument();
165
+ expect(screen.getByText('Field B must be positive')).toBeInTheDocument();
166
+ });
167
+
168
+ it('does not display validation errors when showValidation is false', () => {
169
+ const errors = ['Field A is required'];
170
+
171
+ render(
172
+ <SchemaFormRenderer
173
+ modelClass={TestModel}
174
+ value={{}}
175
+ onChange={mockOnChange}
176
+ showValidation={false}
177
+ validationErrors={errors}
178
+ />
179
+ );
180
+
181
+ expect(screen.queryByText(/Please fix the following errors/i)).not.toBeInTheDocument();
182
+ });
183
+
184
+ it('disables all fields when readOnly is true', () => {
185
+ render(
186
+ <SchemaFormRenderer
187
+ modelClass={TestModel}
188
+ value={{}}
189
+ onChange={mockOnChange}
190
+ readOnly={true}
191
+ />
192
+ );
193
+
194
+ expect(screen.getByLabelText(/Text Field/i)).toBeDisabled();
195
+ expect(screen.getByLabelText(/Email Field/i)).toBeDisabled();
196
+ expect(screen.getByLabelText(/Number Field/i)).toBeDisabled();
197
+ expect(screen.getByRole('checkbox', { name: /Boolean Field/i })).toBeDisabled();
198
+ });
199
+
200
+ it('displays field descriptions as helper text', () => {
201
+ render(
202
+ <SchemaFormRenderer
203
+ modelClass={TestModel}
204
+ value={{}}
205
+ onChange={mockOnChange}
206
+ />
207
+ );
208
+
209
+ expect(screen.getByText('Enter text')).toBeInTheDocument();
210
+ expect(screen.getByText('Toggle me')).toBeInTheDocument();
211
+ });
212
+ });
@@ -17,3 +17,6 @@ export type { FormCheckboxProps } from './FormCheckbox';
17
17
 
18
18
  export { default as Captcha } from './Captcha';
19
19
  export type { CaptchaProps, CaptchaProvider } from './Captcha';
20
+
21
+ export { SchemaFormRenderer } from './SchemaFormRenderer';
22
+ export type { SchemaFormRendererProps } from './SchemaFormRenderer';
@@ -23,6 +23,7 @@ export * from './forms';
23
23
  export * from './input';
24
24
  export * from './layout';
25
25
  export * from './pages';
26
+ export * from './plugins';
26
27
  export { default as Scaffold } from './Scaffold';
27
28
  export type { ScaffoldProps, AppBarProps } from './Scaffold';
28
29
  export type { MenuItem } from './menu/MenuItem';
@@ -18,7 +18,8 @@ import {
18
18
  Button,
19
19
  Typography
20
20
  } from '@mui/material';
21
- import { Add as AddIcon } from '@mui/icons-material';
21
+ import Add from "@mui/icons-material/Add";
22
+ const AddIcon = Add;
22
23
  import HtmlInputField from './HtmlInputField';
23
24
  // import ChoiceInputFieldModel from '../../schemas/ChoiceInputFieldSchema';
24
25
  import { createSerializableView, SerializableComponent } from '../shared/createSerializableView';
@@ -23,15 +23,20 @@ import {
23
23
  Alert,
24
24
  ButtonGroup,
25
25
  } from '@mui/material';
26
- import {
27
- FormatBold as BoldIcon,
28
- FormatItalic as ItalicIcon,
29
- FormatUnderlined as UnderlineIcon,
30
- Code as CodeIcon,
31
- Visibility as PreviewIcon,
32
- VisibilityOff as EditIcon,
33
- Help as HelpIcon
34
- } from '@mui/icons-material';
26
+ import FormatBold from '@mui/icons-material/FormatBold';
27
+ import FormatItalic from '@mui/icons-material/FormatItalic';
28
+ import FormatUnderlined from '@mui/icons-material/FormatUnderlined';
29
+ import Code from '@mui/icons-material/Code';
30
+ import Visibility from '@mui/icons-material/Visibility';
31
+ import VisibilityOff from '@mui/icons-material/VisibilityOff';
32
+ import Help from '@mui/icons-material/Help';
33
+ const BoldIcon = FormatBold;
34
+ const ItalicIcon = FormatItalic;
35
+ const UnderlineIcon = FormatUnderlined;
36
+ const CodeIcon = Code;
37
+ const PreviewIcon = Visibility;
38
+ const EditIcon = VisibilityOff;
39
+ const HelpIcon = Help;
35
40
  import SafeSpan from '../SafeSpan';
36
41
  import sanitizeHtml from 'sanitize-html';
37
42
  // import HtmlInputFieldModel from '../../schemas/HtmlInputFieldSchema';
@@ -55,7 +55,7 @@ export const TextField = React.forwardRef<HTMLDivElement, TextFieldProps>((props
55
55
  }
56
56
 
57
57
  // Mark as QwickApp component
58
- (TextField as Record<string, unknown>)[QWICKAPP_COMPONENT] = true;
58
+ Object.assign(TextField, { [QWICKAPP_COMPONENT]: true });
59
59
 
60
60
  return (
61
61
  <MuiTextField
@@ -26,10 +26,10 @@ import {
26
26
  SxProps,
27
27
  Theme,
28
28
  } from '@mui/material';
29
- import {
30
- ExpandMore as ExpandMoreIcon,
31
- ExpandLess as ExpandLessIcon,
32
- } from '@mui/icons-material';
29
+ import ExpandMore from '@mui/icons-material/ExpandMore';
30
+ import ExpandLess from '@mui/icons-material/ExpandLess';
31
+ const ExpandMoreIcon = ExpandMore;
32
+ const ExpandLessIcon = ExpandLess;
33
33
  import { QWICKAPP_COMPONENT, useBaseProps, useDataBinding } from '../../../hooks';
34
34
  import CollapsibleLayoutModel from '../../../schemas/CollapsibleLayoutSchema';
35
35
  import {
@@ -167,7 +167,7 @@ function CollapsibleLayoutView({
167
167
  const { styleProps, htmlProps, restProps: otherProps } = useBaseProps(restProps);
168
168
 
169
169
  // Mark as QwickApp component
170
- (CollapsibleLayoutView as Record<string, unknown>)[QWICKAPP_COMPONENT] = true;
170
+ Object.assign(CollapsibleLayoutView, { [QWICKAPP_COMPONENT]: true });
171
171
 
172
172
  // Determine controlled vs uncontrolled usage
173
173
  const controlled = collapsedProp !== undefined;
@@ -500,9 +500,7 @@ function CollapsibleLayout(props: CollapsibleLayoutProps) {
500
500
  // Always call hooks unconditionally
501
501
  const bindingResult = useDataBinding<CollapsibleLayoutModel>(
502
502
  dataSource || '',
503
- restProps as Partial<CollapsibleLayoutModel>,
504
- CollapsibleLayoutModel.getSchema(),
505
- { cache: true, cacheTTL: 300000, strict: false, ...bindingOptions }
503
+ restProps as Partial<CollapsibleLayoutModel>
506
504
  );
507
505
 
508
506
  // If no dataSource, use traditional props
@@ -44,6 +44,10 @@ export interface GridLayoutProps extends ViewProps {
44
44
  maxHeight?: string;
45
45
  /** Maximum grid container width */
46
46
  maxWidth?: string;
47
+ /** MUI sx prop for advanced styling (explicit override for type resolution) */
48
+ sx?: import('@mui/material/styles').SxProps<import('@mui/material/styles').Theme>;
49
+ /** Inline CSS styles (explicit override for type resolution) */
50
+ style?: React.CSSProperties;
47
51
  }
48
52
 
49
53
  /**