@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.
- package/dist/components/AccessibilityChecker.d.ts.map +1 -1
- package/dist/components/Html.d.ts +1 -1
- package/dist/components/Html.d.ts.map +1 -1
- package/dist/components/Logo.d.ts.map +1 -1
- package/dist/components/Markdown.d.ts +2 -2
- package/dist/components/Markdown.d.ts.map +1 -1
- package/dist/components/SafeSpan.d.ts +1 -1
- package/dist/components/SafeSpan.d.ts.map +1 -1
- package/dist/components/base/ModelView.d.ts +1 -1
- package/dist/components/base/ModelView.d.ts.map +1 -1
- package/dist/components/blocks/Article.d.ts +1 -1
- package/dist/components/blocks/Article.d.ts.map +1 -1
- package/dist/components/blocks/CardListGrid.d.ts.map +1 -1
- package/dist/components/blocks/Code.d.ts.map +1 -1
- package/dist/components/blocks/Content.d.ts.map +1 -1
- package/dist/components/blocks/CoverImageHeader.d.ts.map +1 -1
- package/dist/components/blocks/FeatureCard.d.ts.map +1 -1
- package/dist/components/blocks/FeatureGrid.d.ts.map +1 -1
- package/dist/components/blocks/Footer.d.ts.map +1 -1
- package/dist/components/blocks/Image.d.ts.map +1 -1
- package/dist/components/blocks/PageBannerHeader.d.ts.map +1 -1
- package/dist/components/blocks/ProductCard.d.ts.map +1 -1
- package/dist/components/blocks/Section.d.ts.map +1 -1
- package/dist/components/blocks/Text.d.ts +8 -1
- package/dist/components/blocks/Text.d.ts.map +1 -1
- package/dist/components/buttons/Button.d.ts.map +1 -1
- package/dist/components/buttons/PaletteSwitcher.d.ts.map +1 -1
- package/dist/components/buttons/ThemeSwitcher.d.ts.map +1 -1
- package/dist/components/forms/FormBlock.d.ts +1 -1
- package/dist/components/forms/FormBlock.d.ts.map +1 -1
- package/dist/components/forms/SchemaFormRenderer.d.ts +28 -0
- package/dist/components/forms/SchemaFormRenderer.d.ts.map +1 -0
- package/dist/components/forms/index.d.ts +2 -0
- package/dist/components/forms/index.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/input/ChoiceInputField.d.ts.map +1 -1
- package/dist/components/input/HtmlInputField.d.ts.map +1 -1
- package/dist/components/layout/CollapsibleLayout/CollapsibleLayout.d.ts.map +1 -1
- package/dist/components/layout/GridLayout.d.ts +5 -0
- package/dist/components/layout/GridLayout.d.ts.map +1 -1
- package/dist/components/plugins/DataTable.d.ts +57 -0
- package/dist/components/plugins/DataTable.d.ts.map +1 -0
- package/dist/components/plugins/StatCard.d.ts +44 -0
- package/dist/components/plugins/StatCard.d.ts.map +1 -0
- package/dist/components/plugins/index.d.ts +13 -0
- package/dist/components/plugins/index.d.ts.map +1 -0
- package/dist/components/shared/createSerializableView.d.ts.map +1 -1
- package/dist/hooks/useBaseProps.d.ts +1161 -12
- package/dist/hooks/useBaseProps.d.ts.map +1 -1
- package/dist/index.esm.js +5468 -5216
- package/dist/index.js +5572 -5317
- package/dist/palettes/manifest.json +19 -19
- package/dist/schemas/transformers/ReactNodeTransformer.d.ts.map +1 -1
- package/dist/utils/iconMap.d.ts.map +1 -1
- package/package.json +6 -5
- package/src/components/AccessibilityChecker.tsx +10 -7
- package/src/components/ErrorBoundary.tsx +3 -3
- package/src/components/Html.tsx +17 -12
- package/src/components/Logo.tsx +1 -8
- package/src/components/Markdown.tsx +12 -12
- package/src/components/ResponsiveMenu.tsx +1 -1
- package/src/components/SafeSpan.tsx +10 -10
- package/src/components/Scaffold.tsx +4 -4
- package/src/components/base/ModelView.tsx +2 -2
- package/src/components/blocks/Article.tsx +8 -8
- package/src/components/blocks/CardListGrid.tsx +1 -3
- package/src/components/blocks/Code.tsx +10 -8
- package/src/components/blocks/Content.tsx +2 -4
- package/src/components/blocks/CoverImageHeader.tsx +3 -4
- package/src/components/blocks/FeatureCard.tsx +2 -4
- package/src/components/blocks/FeatureGrid.tsx +2 -4
- package/src/components/blocks/Footer.tsx +2 -4
- package/src/components/blocks/Image.tsx +10 -7
- package/src/components/blocks/PageBannerHeader.tsx +3 -4
- package/src/components/blocks/ProductCard.tsx +8 -5
- package/src/components/blocks/Section.tsx +8 -6
- package/src/components/blocks/Text.tsx +22 -14
- package/src/components/buttons/Button.tsx +11 -9
- package/src/components/buttons/PaletteSwitcher.tsx +6 -8
- package/src/components/buttons/ThemeSwitcher.tsx +8 -9
- package/src/components/forms/Captcha.tsx +1 -1
- package/src/components/forms/FormBlock.tsx +3 -5
- package/src/components/forms/FormCheckbox.tsx +1 -1
- package/src/components/forms/FormField.tsx +1 -1
- package/src/components/forms/FormSelect.tsx +1 -1
- package/src/components/forms/SchemaFormRenderer.tsx +268 -0
- package/src/components/forms/__tests__/SchemaFormRenderer.test.tsx +212 -0
- package/src/components/forms/index.ts +3 -0
- package/src/components/index.ts +1 -0
- package/src/components/input/ChoiceInputField.tsx +2 -1
- package/src/components/input/HtmlInputField.tsx +14 -9
- package/src/components/input/TextField.tsx +1 -1
- package/src/components/layout/CollapsibleLayout/CollapsibleLayout.tsx +6 -8
- package/src/components/layout/GridLayout.tsx +4 -0
- package/src/components/plugins/DataTable.tsx +259 -0
- package/src/components/plugins/StatCard.tsx +122 -0
- package/src/components/plugins/__tests__/DataTable.test.tsx +158 -0
- package/src/components/plugins/index.ts +14 -0
- package/src/components/shared/createSerializableView.tsx +8 -6
- package/src/hooks/useBaseProps.ts +1 -1
- package/src/schemas/transformers/ReactNodeTransformer.ts +13 -10
- package/src/utils/iconMap.tsx +143 -83
- package/dist/palettes/palette-autumn.1.4.9.css +0 -172
- package/dist/palettes/palette-autumn.1.4.9.min.css +0 -1
- package/dist/palettes/palette-autumn.1.5.0.css +0 -172
- package/dist/palettes/palette-autumn.1.5.0.min.css +0 -1
- package/dist/palettes/palette-autumn.1.5.1.css +0 -172
- package/dist/palettes/palette-autumn.1.5.1.min.css +0 -1
- package/dist/palettes/palette-autumn.1.5.2.css +0 -172
- package/dist/palettes/palette-autumn.1.5.2.min.css +0 -1
- package/dist/palettes/palette-autumn.1.5.4.css +0 -172
- package/dist/palettes/palette-autumn.1.5.4.min.css +0 -1
- package/dist/palettes/palette-autumn.1.5.5.css +0 -172
- package/dist/palettes/palette-autumn.1.5.5.min.css +0 -1
- package/dist/palettes/palette-autumn.1.5.6.css +0 -172
- package/dist/palettes/palette-autumn.1.5.6.min.css +0 -1
- package/dist/palettes/palette-autumn.1.5.7.css +0 -172
- package/dist/palettes/palette-autumn.1.5.7.min.css +0 -1
- package/dist/palettes/palette-cosmic.1.4.9.css +0 -172
- package/dist/palettes/palette-cosmic.1.4.9.min.css +0 -1
- package/dist/palettes/palette-cosmic.1.5.0.css +0 -172
- package/dist/palettes/palette-cosmic.1.5.0.min.css +0 -1
- package/dist/palettes/palette-cosmic.1.5.1.css +0 -172
- package/dist/palettes/palette-cosmic.1.5.1.min.css +0 -1
- package/dist/palettes/palette-cosmic.1.5.2.css +0 -172
- package/dist/palettes/palette-cosmic.1.5.2.min.css +0 -1
- package/dist/palettes/palette-cosmic.1.5.4.css +0 -172
- package/dist/palettes/palette-cosmic.1.5.4.min.css +0 -1
- package/dist/palettes/palette-cosmic.1.5.5.css +0 -172
- package/dist/palettes/palette-cosmic.1.5.5.min.css +0 -1
- package/dist/palettes/palette-cosmic.1.5.6.css +0 -172
- package/dist/palettes/palette-cosmic.1.5.6.min.css +0 -1
- package/dist/palettes/palette-cosmic.1.5.7.css +0 -172
- package/dist/palettes/palette-cosmic.1.5.7.min.css +0 -1
- package/dist/palettes/palette-default.1.4.9.css +0 -178
- package/dist/palettes/palette-default.1.4.9.min.css +0 -1
- package/dist/palettes/palette-default.1.5.0.css +0 -178
- package/dist/palettes/palette-default.1.5.0.min.css +0 -1
- package/dist/palettes/palette-default.1.5.1.css +0 -178
- package/dist/palettes/palette-default.1.5.1.min.css +0 -1
- package/dist/palettes/palette-default.1.5.2.css +0 -178
- package/dist/palettes/palette-default.1.5.2.min.css +0 -1
- package/dist/palettes/palette-default.1.5.4.css +0 -178
- package/dist/palettes/palette-default.1.5.4.min.css +0 -1
- package/dist/palettes/palette-default.1.5.5.css +0 -178
- package/dist/palettes/palette-default.1.5.5.min.css +0 -1
- package/dist/palettes/palette-default.1.5.6.css +0 -178
- package/dist/palettes/palette-default.1.5.6.min.css +0 -1
- package/dist/palettes/palette-default.1.5.7.css +0 -178
- package/dist/palettes/palette-default.1.5.7.min.css +0 -1
- package/dist/palettes/palette-ocean.1.4.9.css +0 -172
- package/dist/palettes/palette-ocean.1.4.9.min.css +0 -1
- package/dist/palettes/palette-ocean.1.5.0.css +0 -172
- package/dist/palettes/palette-ocean.1.5.0.min.css +0 -1
- package/dist/palettes/palette-ocean.1.5.1.css +0 -172
- package/dist/palettes/palette-ocean.1.5.1.min.css +0 -1
- package/dist/palettes/palette-ocean.1.5.2.css +0 -172
- package/dist/palettes/palette-ocean.1.5.2.min.css +0 -1
- package/dist/palettes/palette-ocean.1.5.4.css +0 -172
- package/dist/palettes/palette-ocean.1.5.4.min.css +0 -1
- package/dist/palettes/palette-ocean.1.5.5.css +0 -172
- package/dist/palettes/palette-ocean.1.5.5.min.css +0 -1
- package/dist/palettes/palette-ocean.1.5.6.css +0 -172
- package/dist/palettes/palette-ocean.1.5.6.min.css +0 -1
- package/dist/palettes/palette-ocean.1.5.7.css +0 -172
- package/dist/palettes/palette-ocean.1.5.7.min.css +0 -1
- package/dist/palettes/palette-spring.1.4.9.css +0 -160
- package/dist/palettes/palette-spring.1.4.9.min.css +0 -1
- package/dist/palettes/palette-spring.1.5.0.css +0 -160
- package/dist/palettes/palette-spring.1.5.0.min.css +0 -1
- package/dist/palettes/palette-spring.1.5.1.css +0 -160
- package/dist/palettes/palette-spring.1.5.1.min.css +0 -1
- package/dist/palettes/palette-spring.1.5.2.css +0 -160
- package/dist/palettes/palette-spring.1.5.2.min.css +0 -1
- package/dist/palettes/palette-spring.1.5.4.css +0 -166
- package/dist/palettes/palette-spring.1.5.4.min.css +0 -1
- package/dist/palettes/palette-spring.1.5.5.css +0 -166
- package/dist/palettes/palette-spring.1.5.5.min.css +0 -1
- package/dist/palettes/palette-spring.1.5.6.css +0 -166
- package/dist/palettes/palette-spring.1.5.6.min.css +0 -1
- package/dist/palettes/palette-spring.1.5.7.css +0 -166
- package/dist/palettes/palette-spring.1.5.7.min.css +0 -1
- package/dist/palettes/palette-winter.1.4.9.css +0 -172
- package/dist/palettes/palette-winter.1.4.9.min.css +0 -1
- package/dist/palettes/palette-winter.1.5.0.css +0 -172
- package/dist/palettes/palette-winter.1.5.0.min.css +0 -1
- package/dist/palettes/palette-winter.1.5.1.css +0 -172
- package/dist/palettes/palette-winter.1.5.1.min.css +0 -1
- package/dist/palettes/palette-winter.1.5.2.css +0 -172
- package/dist/palettes/palette-winter.1.5.2.min.css +0 -1
- package/dist/palettes/palette-winter.1.5.4.css +0 -172
- package/dist/palettes/palette-winter.1.5.4.min.css +0 -1
- package/dist/palettes/palette-winter.1.5.5.css +0 -172
- package/dist/palettes/palette-winter.1.5.5.min.css +0 -1
- package/dist/palettes/palette-winter.1.5.6.css +0 -172
- package/dist/palettes/palette-winter.1.5.6.min.css +0 -1
- package/dist/palettes/palette-winter.1.5.7.css +0 -172
- package/dist/palettes/palette-winter.1.5.7.min.css +0 -1
- /package/dist/palettes/{palette-autumn.1.5.3.css → palette-autumn.1.5.9.css} +0 -0
- /package/dist/palettes/{palette-autumn.1.5.3.min.css → palette-autumn.1.5.9.min.css} +0 -0
- /package/dist/palettes/{palette-cosmic.1.5.3.css → palette-cosmic.1.5.9.css} +0 -0
- /package/dist/palettes/{palette-cosmic.1.5.3.min.css → palette-cosmic.1.5.9.min.css} +0 -0
- /package/dist/palettes/{palette-default.1.5.3.css → palette-default.1.5.9.css} +0 -0
- /package/dist/palettes/{palette-default.1.5.3.min.css → palette-default.1.5.9.min.css} +0 -0
- /package/dist/palettes/{palette-ocean.1.5.3.css → palette-ocean.1.5.9.css} +0 -0
- /package/dist/palettes/{palette-ocean.1.5.3.min.css → palette-ocean.1.5.9.min.css} +0 -0
- /package/dist/palettes/{palette-spring.1.5.3.css → palette-spring.1.5.9.css} +0 -0
- /package/dist/palettes/{palette-spring.1.5.3.min.css → palette-spring.1.5.9.min.css} +0 -0
- /package/dist/palettes/{palette-winter.1.5.3.css → palette-winter.1.5.9.css} +0 -0
- /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';
|
package/src/components/index.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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
|
/**
|