@qwickapps/react-framework 1.5.7 → 1.5.8
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 +1 -2
- 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 +10 -10
- package/src/components/ResponsiveMenu.tsx +1 -1
- package/src/components/SafeSpan.tsx +9 -9
- package/src/components/Scaffold.tsx +4 -4
- package/src/components/base/ModelView.tsx +2 -2
- package/src/components/blocks/Article.tsx +7 -7
- 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 +8 -5
- package/src/components/blocks/PageBannerHeader.tsx +3 -4
- package/src/components/blocks/ProductCard.tsx +8 -5
- package/src/components/blocks/Section.tsx +6 -4
- package/src/components/blocks/Text.tsx +15 -7
- package/src/components/buttons/Button.tsx +8 -6
- 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.8.css} +0 -0
- /package/dist/palettes/{palette-autumn.1.5.3.min.css → palette-autumn.1.5.8.min.css} +0 -0
- /package/dist/palettes/{palette-cosmic.1.5.3.css → palette-cosmic.1.5.8.css} +0 -0
- /package/dist/palettes/{palette-cosmic.1.5.3.min.css → palette-cosmic.1.5.8.min.css} +0 -0
- /package/dist/palettes/{palette-default.1.5.3.css → palette-default.1.5.8.css} +0 -0
- /package/dist/palettes/{palette-default.1.5.3.min.css → palette-default.1.5.8.min.css} +0 -0
- /package/dist/palettes/{palette-ocean.1.5.3.css → palette-ocean.1.5.8.css} +0 -0
- /package/dist/palettes/{palette-ocean.1.5.3.min.css → palette-ocean.1.5.8.min.css} +0 -0
- /package/dist/palettes/{palette-spring.1.5.3.css → palette-spring.1.5.8.css} +0 -0
- /package/dist/palettes/{palette-spring.1.5.3.min.css → palette-spring.1.5.8.min.css} +0 -0
- /package/dist/palettes/{palette-winter.1.5.3.css → palette-winter.1.5.8.css} +0 -0
- /package/dist/palettes/{palette-winter.1.5.3.min.css → palette-winter.1.5.8.min.css} +0 -0
|
@@ -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
|
/**
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DataTable - Sortable, filterable table component for plugin management pages
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent table UI with sorting, filtering, pagination, and bulk actions.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* <DataTable
|
|
9
|
+
* columns={[
|
|
10
|
+
* { key: 'email', label: 'Email', sortable: true },
|
|
11
|
+
* { key: 'status', label: 'Status', render: (val) => <Badge>{val}</Badge> }
|
|
12
|
+
* ]}
|
|
13
|
+
* data={users}
|
|
14
|
+
* onRowClick={(user) => navigate(`/users/${user.id}`)}
|
|
15
|
+
* bulkActions={[
|
|
16
|
+
* { label: 'Delete', onClick: (rows) => handleDelete(rows), variant: 'danger' }
|
|
17
|
+
* ]}
|
|
18
|
+
* />
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import React, { useState, useMemo } from 'react';
|
|
23
|
+
|
|
24
|
+
export interface Column<T> {
|
|
25
|
+
/** Column key (must match data property) */
|
|
26
|
+
key: keyof T;
|
|
27
|
+
|
|
28
|
+
/** Display label */
|
|
29
|
+
label: string;
|
|
30
|
+
|
|
31
|
+
/** Enable sorting for this column */
|
|
32
|
+
sortable?: boolean;
|
|
33
|
+
|
|
34
|
+
/** Custom render function */
|
|
35
|
+
render?: (value: unknown, row: T) => React.ReactNode;
|
|
36
|
+
|
|
37
|
+
/** Column width (CSS value) */
|
|
38
|
+
width?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface DataTableProps<T> {
|
|
42
|
+
/** Column definitions */
|
|
43
|
+
columns: Column<T>[];
|
|
44
|
+
|
|
45
|
+
/** Table data */
|
|
46
|
+
data: T[];
|
|
47
|
+
|
|
48
|
+
/** Row click handler */
|
|
49
|
+
onRowClick?: (row: T) => void;
|
|
50
|
+
|
|
51
|
+
/** Bulk action buttons */
|
|
52
|
+
bulkActions?: Array<{
|
|
53
|
+
label: string;
|
|
54
|
+
onClick: (selectedRows: T[]) => void;
|
|
55
|
+
variant?: 'primary' | 'secondary' | 'danger';
|
|
56
|
+
}>;
|
|
57
|
+
|
|
58
|
+
/** Enable row selection */
|
|
59
|
+
selectable?: boolean;
|
|
60
|
+
|
|
61
|
+
/** Empty state message */
|
|
62
|
+
emptyMessage?: string;
|
|
63
|
+
|
|
64
|
+
/** Loading state */
|
|
65
|
+
loading?: boolean;
|
|
66
|
+
|
|
67
|
+
/** Row key extractor */
|
|
68
|
+
getRowKey?: (row: T, index: number) => string | number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function DataTable<T = Record<string, unknown>>({
|
|
72
|
+
columns,
|
|
73
|
+
data,
|
|
74
|
+
onRowClick,
|
|
75
|
+
bulkActions = [],
|
|
76
|
+
selectable = bulkActions.length > 0,
|
|
77
|
+
emptyMessage = 'No data available',
|
|
78
|
+
loading = false,
|
|
79
|
+
getRowKey = (row: T, index: number) => {
|
|
80
|
+
// Type-safe check for id property
|
|
81
|
+
const rowObj = row as Record<string, unknown>;
|
|
82
|
+
const hasValidId = 'id' in rowObj &&
|
|
83
|
+
(typeof rowObj.id === 'string' || typeof rowObj.id === 'number');
|
|
84
|
+
return hasValidId ? (rowObj.id as string | number) : index;
|
|
85
|
+
},
|
|
86
|
+
}: DataTableProps<T>) {
|
|
87
|
+
const [selectedRows, setSelectedRows] = useState<Set<string | number>>(new Set());
|
|
88
|
+
const [sortColumn, setSortColumn] = useState<keyof T | null>(null);
|
|
89
|
+
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
|
90
|
+
|
|
91
|
+
// Sorted data
|
|
92
|
+
const sortedData = useMemo(() => {
|
|
93
|
+
if (!sortColumn) return data;
|
|
94
|
+
|
|
95
|
+
return [...data].sort((a, b) => {
|
|
96
|
+
const aVal = a[sortColumn];
|
|
97
|
+
const bVal = b[sortColumn];
|
|
98
|
+
|
|
99
|
+
if (aVal === bVal) return 0;
|
|
100
|
+
|
|
101
|
+
const comparison = aVal < bVal ? -1 : 1;
|
|
102
|
+
return sortDirection === 'asc' ? comparison : -comparison;
|
|
103
|
+
});
|
|
104
|
+
}, [data, sortColumn, sortDirection]);
|
|
105
|
+
|
|
106
|
+
const handleSort = (column: keyof T) => {
|
|
107
|
+
if (sortColumn === column) {
|
|
108
|
+
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
|
109
|
+
} else {
|
|
110
|
+
setSortColumn(column);
|
|
111
|
+
setSortDirection('asc');
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const handleSelectAll = () => {
|
|
116
|
+
if (selectedRows.size === data.length) {
|
|
117
|
+
setSelectedRows(new Set());
|
|
118
|
+
} else {
|
|
119
|
+
setSelectedRows(new Set(data.map((row, index) => getRowKey(row, index))));
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const handleSelectRow = (rowKey: string | number) => {
|
|
124
|
+
const newSelection = new Set(selectedRows);
|
|
125
|
+
if (newSelection.has(rowKey)) {
|
|
126
|
+
newSelection.delete(rowKey);
|
|
127
|
+
} else {
|
|
128
|
+
newSelection.add(rowKey);
|
|
129
|
+
}
|
|
130
|
+
setSelectedRows(newSelection);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const getSelectedRowData = () => {
|
|
134
|
+
return data.filter((row, index) =>
|
|
135
|
+
selectedRows.has(getRowKey(row, index))
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
if (loading) {
|
|
140
|
+
return (
|
|
141
|
+
<div className="animate-pulse space-y-2">
|
|
142
|
+
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
|
143
|
+
<div className="h-16 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
|
144
|
+
<div className="h-16 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (data.length === 0) {
|
|
150
|
+
return (
|
|
151
|
+
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
|
152
|
+
{emptyMessage}
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div className="space-y-4">
|
|
159
|
+
{/* Bulk Actions */}
|
|
160
|
+
{selectable && selectedRows.size > 0 && bulkActions.length > 0 && (
|
|
161
|
+
<div className="flex items-center gap-3 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
|
162
|
+
<span className="text-sm font-medium text-blue-900 dark:text-blue-100">
|
|
163
|
+
{selectedRows.size} selected
|
|
164
|
+
</span>
|
|
165
|
+
{bulkActions.map((action, index) => (
|
|
166
|
+
<button
|
|
167
|
+
key={index}
|
|
168
|
+
onClick={() => action.onClick(getSelectedRowData())}
|
|
169
|
+
className={`
|
|
170
|
+
px-3 py-1.5 rounded text-sm font-medium
|
|
171
|
+
${action.variant === 'danger'
|
|
172
|
+
? 'bg-red-600 hover:bg-red-700 text-white'
|
|
173
|
+
: 'bg-blue-600 hover:bg-blue-700 text-white'}
|
|
174
|
+
`}
|
|
175
|
+
>
|
|
176
|
+
{action.label}
|
|
177
|
+
</button>
|
|
178
|
+
))}
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
|
|
182
|
+
{/* Table */}
|
|
183
|
+
<div className="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700">
|
|
184
|
+
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
185
|
+
<thead className="bg-gray-50 dark:bg-gray-800">
|
|
186
|
+
<tr>
|
|
187
|
+
{selectable && (
|
|
188
|
+
<th className="w-12 px-4 py-3">
|
|
189
|
+
<input
|
|
190
|
+
type="checkbox"
|
|
191
|
+
checked={selectedRows.size === data.length && data.length > 0}
|
|
192
|
+
onChange={handleSelectAll}
|
|
193
|
+
className="rounded"
|
|
194
|
+
/>
|
|
195
|
+
</th>
|
|
196
|
+
)}
|
|
197
|
+
{columns.map((column) => (
|
|
198
|
+
<th
|
|
199
|
+
key={String(column.key)}
|
|
200
|
+
style={{ width: column.width }}
|
|
201
|
+
className={`
|
|
202
|
+
px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider
|
|
203
|
+
${column.sortable ? 'cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700' : ''}
|
|
204
|
+
`}
|
|
205
|
+
onClick={() => column.sortable && handleSort(column.key)}
|
|
206
|
+
>
|
|
207
|
+
<div className="flex items-center gap-2">
|
|
208
|
+
{column.label}
|
|
209
|
+
{column.sortable && sortColumn === column.key && (
|
|
210
|
+
<span>{sortDirection === 'asc' ? '↑' : '↓'}</span>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
</th>
|
|
214
|
+
))}
|
|
215
|
+
</tr>
|
|
216
|
+
</thead>
|
|
217
|
+
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
|
218
|
+
{sortedData.map((row, rowIndex) => {
|
|
219
|
+
const rowKey = getRowKey(row, rowIndex);
|
|
220
|
+
const isSelected = selectedRows.has(rowKey);
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<tr
|
|
224
|
+
key={rowKey}
|
|
225
|
+
className={`
|
|
226
|
+
${onRowClick ? 'cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800' : ''}
|
|
227
|
+
${isSelected ? 'bg-blue-50 dark:bg-blue-900/20' : ''}
|
|
228
|
+
`}
|
|
229
|
+
onClick={() => onRowClick?.(row)}
|
|
230
|
+
>
|
|
231
|
+
{selectable && (
|
|
232
|
+
<td className="px-4 py-3" onClick={(e) => e.stopPropagation()}>
|
|
233
|
+
<input
|
|
234
|
+
type="checkbox"
|
|
235
|
+
checked={isSelected}
|
|
236
|
+
onChange={() => handleSelectRow(rowKey)}
|
|
237
|
+
className="rounded"
|
|
238
|
+
/>
|
|
239
|
+
</td>
|
|
240
|
+
)}
|
|
241
|
+
{columns.map((column) => (
|
|
242
|
+
<td
|
|
243
|
+
key={String(column.key)}
|
|
244
|
+
className="px-4 py-3 text-sm text-gray-900 dark:text-gray-100"
|
|
245
|
+
>
|
|
246
|
+
{column.render
|
|
247
|
+
? column.render(row[column.key], row)
|
|
248
|
+
: String(row[column.key] ?? '')}
|
|
249
|
+
</td>
|
|
250
|
+
))}
|
|
251
|
+
</tr>
|
|
252
|
+
);
|
|
253
|
+
})}
|
|
254
|
+
</tbody>
|
|
255
|
+
</table>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
}
|