@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.
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 +1 -2
  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 +10 -10
  62. package/src/components/ResponsiveMenu.tsx +1 -1
  63. package/src/components/SafeSpan.tsx +9 -9
  64. package/src/components/Scaffold.tsx +4 -4
  65. package/src/components/base/ModelView.tsx +2 -2
  66. package/src/components/blocks/Article.tsx +7 -7
  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 +8 -5
  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 +6 -4
  78. package/src/components/blocks/Text.tsx +15 -7
  79. package/src/components/buttons/Button.tsx +8 -6
  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.8.css} +0 -0
  201. /package/dist/palettes/{palette-autumn.1.5.3.min.css → palette-autumn.1.5.8.min.css} +0 -0
  202. /package/dist/palettes/{palette-cosmic.1.5.3.css → palette-cosmic.1.5.8.css} +0 -0
  203. /package/dist/palettes/{palette-cosmic.1.5.3.min.css → palette-cosmic.1.5.8.min.css} +0 -0
  204. /package/dist/palettes/{palette-default.1.5.3.css → palette-default.1.5.8.css} +0 -0
  205. /package/dist/palettes/{palette-default.1.5.3.min.css → palette-default.1.5.8.min.css} +0 -0
  206. /package/dist/palettes/{palette-ocean.1.5.3.css → palette-ocean.1.5.8.css} +0 -0
  207. /package/dist/palettes/{palette-ocean.1.5.3.min.css → palette-ocean.1.5.8.min.css} +0 -0
  208. /package/dist/palettes/{palette-spring.1.5.3.css → palette-spring.1.5.8.css} +0 -0
  209. /package/dist/palettes/{palette-spring.1.5.3.min.css → palette-spring.1.5.8.min.css} +0 -0
  210. /package/dist/palettes/{palette-winter.1.5.3.css → palette-winter.1.5.8.css} +0 -0
  211. /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';
@@ -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
  /**
@@ -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
+ }