@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,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StatCard - Display a single metric with optional trend indicator
|
|
3
|
+
*
|
|
4
|
+
* Used in plugin status widgets to show key metrics at a glance.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* <StatCard
|
|
9
|
+
* label="Active Connections"
|
|
10
|
+
* value={42}
|
|
11
|
+
* unit="connections"
|
|
12
|
+
* trend={{ value: 12, direction: 'up' }}
|
|
13
|
+
* status="healthy"
|
|
14
|
+
* />
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import React from 'react';
|
|
19
|
+
|
|
20
|
+
export interface StatCardProps {
|
|
21
|
+
/** Label describing the metric */
|
|
22
|
+
label: string;
|
|
23
|
+
|
|
24
|
+
/** Current value */
|
|
25
|
+
value: number | string;
|
|
26
|
+
|
|
27
|
+
/** Optional unit (e.g., "MB", "requests/sec") */
|
|
28
|
+
unit?: string;
|
|
29
|
+
|
|
30
|
+
/** Optional suffix (alias for unit, for backward compatibility) */
|
|
31
|
+
suffix?: string;
|
|
32
|
+
|
|
33
|
+
/** Optional trend indicator */
|
|
34
|
+
trend?: {
|
|
35
|
+
value: number;
|
|
36
|
+
direction: 'up' | 'down' | 'stable';
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Status indicator */
|
|
40
|
+
status?: 'healthy' | 'warning' | 'error' | 'info';
|
|
41
|
+
|
|
42
|
+
/** Optional click handler */
|
|
43
|
+
onClick?: () => void;
|
|
44
|
+
|
|
45
|
+
/** Optional icon */
|
|
46
|
+
icon?: React.ReactNode;
|
|
47
|
+
|
|
48
|
+
/** Optional sub-value displayed below the label */
|
|
49
|
+
subValue?: string;
|
|
50
|
+
|
|
51
|
+
/** Optional custom color for the icon/accent */
|
|
52
|
+
color?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const StatCard: React.FC<StatCardProps> = ({
|
|
56
|
+
label,
|
|
57
|
+
value,
|
|
58
|
+
unit,
|
|
59
|
+
suffix,
|
|
60
|
+
trend,
|
|
61
|
+
status = 'info',
|
|
62
|
+
onClick,
|
|
63
|
+
icon,
|
|
64
|
+
subValue,
|
|
65
|
+
color,
|
|
66
|
+
}) => {
|
|
67
|
+
// Use suffix as fallback for unit (backward compatibility)
|
|
68
|
+
const displayUnit = unit || suffix;
|
|
69
|
+
const statusColors = {
|
|
70
|
+
healthy: 'text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-900/20',
|
|
71
|
+
warning: 'text-yellow-600 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/20',
|
|
72
|
+
error: 'text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20',
|
|
73
|
+
info: 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const trendIcons = {
|
|
77
|
+
up: '↑',
|
|
78
|
+
down: '↓',
|
|
79
|
+
stable: '→',
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
className={`
|
|
85
|
+
rounded-lg border p-4
|
|
86
|
+
${statusColors[status]}
|
|
87
|
+
${onClick ? 'cursor-pointer hover:shadow-md transition-shadow' : ''}
|
|
88
|
+
`}
|
|
89
|
+
onClick={onClick}
|
|
90
|
+
role={onClick ? 'button' : undefined}
|
|
91
|
+
tabIndex={onClick ? 0 : undefined}
|
|
92
|
+
>
|
|
93
|
+
<div className="flex items-start justify-between">
|
|
94
|
+
<div className="flex-1">
|
|
95
|
+
<p className="text-sm font-medium opacity-80">{label}</p>
|
|
96
|
+
<div className="mt-1 flex items-baseline gap-2">
|
|
97
|
+
<p className="text-2xl font-semibold">
|
|
98
|
+
{value}
|
|
99
|
+
</p>
|
|
100
|
+
{displayUnit && (
|
|
101
|
+
<span className="text-sm opacity-70">{displayUnit}</span>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
{subValue && (
|
|
105
|
+
<p className="text-xs opacity-70 mt-1">{subValue}</p>
|
|
106
|
+
)}
|
|
107
|
+
{trend && (
|
|
108
|
+
<div className="mt-2 flex items-center gap-1 text-sm">
|
|
109
|
+
<span>{trendIcons[trend.direction]}</span>
|
|
110
|
+
<span>{trend.value}%</span>
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
{icon && (
|
|
115
|
+
<div className="ml-4 text-2xl opacity-70" style={color ? { color } : undefined}>
|
|
116
|
+
{icon}
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DataTable tests
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { render, screen } from '@testing-library/react';
|
|
9
|
+
import '@testing-library/jest-dom';
|
|
10
|
+
import { DataTable, type Column, type DataTableProps } from '../DataTable';
|
|
11
|
+
|
|
12
|
+
describe('DataTable', () => {
|
|
13
|
+
interface TestRow {
|
|
14
|
+
id: number;
|
|
15
|
+
name: string;
|
|
16
|
+
email: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const mockData: TestRow[] = [
|
|
20
|
+
{ id: 1, name: 'Alice', email: 'alice@example.com' },
|
|
21
|
+
{ id: 2, name: 'Bob', email: 'bob@example.com' },
|
|
22
|
+
{ id: 3, name: 'Charlie', email: 'charlie@example.com' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const mockColumns: Column<TestRow>[] = [
|
|
26
|
+
{ key: 'id', label: 'ID', sortable: true },
|
|
27
|
+
{ key: 'name', label: 'Name', sortable: true },
|
|
28
|
+
{ key: 'email', label: 'Email', sortable: true },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
describe('Type Safety', () => {
|
|
32
|
+
it('should accept generic type parameter correctly', () => {
|
|
33
|
+
const props: DataTableProps<TestRow> = {
|
|
34
|
+
columns: mockColumns,
|
|
35
|
+
data: mockData,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
expect(props.columns).toBeDefined();
|
|
39
|
+
expect(props.data).toBeDefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should have type-safe column keys', () => {
|
|
43
|
+
// This test verifies that TypeScript enforces column keys match the data type
|
|
44
|
+
const validColumn: Column<TestRow> = { key: 'name', label: 'Name' };
|
|
45
|
+
expect(validColumn.key).toBe('name');
|
|
46
|
+
|
|
47
|
+
// TypeScript should prevent invalid keys at compile time
|
|
48
|
+
// @ts-expect-error - invalid key should cause type error
|
|
49
|
+
const invalidColumn: Column<TestRow> = { key: 'invalid', label: 'Invalid' };
|
|
50
|
+
expect(invalidColumn).toBeDefined(); // Just to use the variable
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should handle getRowKey with type-safe id extraction', () => {
|
|
54
|
+
// Test the default getRowKey behavior
|
|
55
|
+
const { container } = render(
|
|
56
|
+
<DataTable<TestRow> columns={mockColumns} data={mockData} />
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(container).toBeInTheDocument();
|
|
60
|
+
// The component should render rows using ids as keys
|
|
61
|
+
// This verifies the type-safe id extraction works
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should handle custom getRowKey function', () => {
|
|
65
|
+
const customGetRowKey = (row: TestRow, index: number) => `row-${row.id}-${index}`;
|
|
66
|
+
|
|
67
|
+
render(
|
|
68
|
+
<DataTable<TestRow>
|
|
69
|
+
columns={mockColumns}
|
|
70
|
+
data={mockData}
|
|
71
|
+
getRowKey={customGetRowKey}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Component should render with custom keys
|
|
76
|
+
expect(screen.getByText('Alice')).toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should handle rows without id property using index fallback', () => {
|
|
80
|
+
interface RowWithoutId {
|
|
81
|
+
name: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const dataWithoutId: RowWithoutId[] = [
|
|
85
|
+
{ name: 'Alice' },
|
|
86
|
+
{ name: 'Bob' },
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const columnsWithoutId: Column<RowWithoutId>[] = [
|
|
90
|
+
{ key: 'name', label: 'Name' },
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const { container } = render(
|
|
94
|
+
<DataTable<RowWithoutId> columns={columnsWithoutId} data={dataWithoutId} />
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(container).toBeInTheDocument();
|
|
98
|
+
expect(screen.getByText('Alice')).toBeInTheDocument();
|
|
99
|
+
expect(screen.getByText('Bob')).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('Component Rendering', () => {
|
|
104
|
+
it('should render empty message when data is empty', () => {
|
|
105
|
+
render(
|
|
106
|
+
<DataTable<TestRow>
|
|
107
|
+
columns={mockColumns}
|
|
108
|
+
data={[]}
|
|
109
|
+
emptyMessage="No users found"
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
expect(screen.getByText('No users found')).toBeInTheDocument();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should render loading state', () => {
|
|
117
|
+
const { container } = render(
|
|
118
|
+
<DataTable<TestRow> columns={mockColumns} data={mockData} loading={true} />
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Check for loading skeleton (has animate-pulse class)
|
|
122
|
+
const loadingSkeleton = container.querySelector('.animate-pulse');
|
|
123
|
+
expect(loadingSkeleton).toBeInTheDocument();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should render column headers', () => {
|
|
127
|
+
render(<DataTable<TestRow> columns={mockColumns} data={mockData} />);
|
|
128
|
+
|
|
129
|
+
expect(screen.getByText('ID')).toBeInTheDocument();
|
|
130
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
131
|
+
expect(screen.getByText('Email')).toBeInTheDocument();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should render data rows', () => {
|
|
135
|
+
render(<DataTable<TestRow> columns={mockColumns} data={mockData} />);
|
|
136
|
+
|
|
137
|
+
expect(screen.getByText('Alice')).toBeInTheDocument();
|
|
138
|
+
expect(screen.getByText('Bob')).toBeInTheDocument();
|
|
139
|
+
expect(screen.getByText('Charlie')).toBeInTheDocument();
|
|
140
|
+
expect(screen.getByText('alice@example.com')).toBeInTheDocument();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should use custom render function when provided', () => {
|
|
144
|
+
const columnsWithCustomRender: Column<TestRow>[] = [
|
|
145
|
+
{
|
|
146
|
+
key: 'name',
|
|
147
|
+
label: 'Name',
|
|
148
|
+
render: (value) => <strong>{String(value)}</strong>,
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
render(<DataTable<TestRow> columns={columnsWithCustomRender} data={mockData} />);
|
|
153
|
+
|
|
154
|
+
const strongElement = screen.getByText('Alice').closest('strong');
|
|
155
|
+
expect(strongElement).toBeInTheDocument();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic UI Components
|
|
3
|
+
*
|
|
4
|
+
* Reusable UI primitives for building admin interfaces.
|
|
5
|
+
* Server-specific plugin patterns are in @qwickapps/server/ui
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { StatCard } from './StatCard.js';
|
|
11
|
+
export type { StatCardProps } from './StatCard.js';
|
|
12
|
+
|
|
13
|
+
export { DataTable } from './DataTable.js';
|
|
14
|
+
export type { DataTableProps, Column } from './DataTable.js';
|
|
@@ -154,11 +154,11 @@ export function createSerializableView<P extends ViewProps>(
|
|
|
154
154
|
|
|
155
155
|
// Attach static properties for serialization
|
|
156
156
|
const component = SerializableViewComponent as unknown as SerializableComponent<P>;
|
|
157
|
-
|
|
157
|
+
|
|
158
158
|
// Component identification
|
|
159
159
|
component.tagName = tagName;
|
|
160
160
|
component.version = version;
|
|
161
|
-
component[QWICKAPP_COMPONENT]
|
|
161
|
+
Object.assign(component, { [QWICKAPP_COMPONENT]: QWICKAPP_COMPONENT });
|
|
162
162
|
|
|
163
163
|
// Serialization methods
|
|
164
164
|
component.fromJson = function fromJson(data: unknown): ReactElement {
|
|
@@ -178,8 +178,9 @@ export function createSerializableView<P extends ViewProps>(
|
|
|
178
178
|
if (childrenStrategy.mode === 'content-prop') {
|
|
179
179
|
const propName = childrenStrategy.propName || 'content';
|
|
180
180
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
181
|
-
const
|
|
182
|
-
const
|
|
181
|
+
const typedComponentData = (componentData as Record<string, unknown>) || {};
|
|
182
|
+
const { children, ...rest } = typedComponentData;
|
|
183
|
+
const contentData = { ...rest, [propName]: typedComponentData[propName] || '' };
|
|
183
184
|
return React.createElement(component as ComponentType<P>, contentData as P);
|
|
184
185
|
} else {
|
|
185
186
|
// For react-children strategy, recursively deserialize children
|
|
@@ -197,8 +198,9 @@ export function createSerializableView<P extends ViewProps>(
|
|
|
197
198
|
if (childrenStrategy.mode === 'content-prop') {
|
|
198
199
|
const propName = childrenStrategy.propName || 'content';
|
|
199
200
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
200
|
-
const
|
|
201
|
-
const
|
|
201
|
+
const typedProps = (props as Record<string, unknown>) || {};
|
|
202
|
+
const { children, ...rest } = typedProps;
|
|
203
|
+
const contentValue = typedProps[propName] ?? toText(typedProps.children as ReactNode);
|
|
202
204
|
|
|
203
205
|
// Clean props for content-prop serialization
|
|
204
206
|
const cleanProps: Record<string, unknown> = {};
|
|
@@ -264,4 +264,4 @@ export function useBaseProps<T extends BaseComponentProps>(props: T) {
|
|
|
264
264
|
/**
|
|
265
265
|
* Type helper for components using base props
|
|
266
266
|
*/
|
|
267
|
-
export type WithBaseProps<P =
|
|
267
|
+
export type WithBaseProps<P = object> = P & BaseComponentProps;
|
|
@@ -185,7 +185,8 @@ export class ReactNodeTransformer {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
// Use Html component to render HTML content safely
|
|
188
|
-
|
|
188
|
+
const typedProps = props as Record<string, unknown>;
|
|
189
|
+
return createElement(Html, { key, children: typedProps.children });
|
|
189
190
|
}
|
|
190
191
|
|
|
191
192
|
/**
|
|
@@ -217,22 +218,24 @@ export class ReactNodeTransformer {
|
|
|
217
218
|
* @returns Text content or null
|
|
218
219
|
*/
|
|
219
220
|
private static extractTextContent(props: unknown): string | null {
|
|
220
|
-
if (!props) return null;
|
|
221
|
+
if (!props || typeof props !== 'object') return null;
|
|
221
222
|
|
|
222
|
-
|
|
223
|
-
|
|
223
|
+
const typedProps = props as Record<string, unknown>;
|
|
224
|
+
|
|
225
|
+
if (typeof typedProps.children === 'string') {
|
|
226
|
+
return typedProps.children;
|
|
224
227
|
}
|
|
225
228
|
|
|
226
|
-
if (
|
|
227
|
-
return
|
|
229
|
+
if (typedProps.title && typeof typedProps.title === 'string') {
|
|
230
|
+
return typedProps.title;
|
|
228
231
|
}
|
|
229
232
|
|
|
230
|
-
if (
|
|
231
|
-
return
|
|
233
|
+
if (typedProps.label && typeof typedProps.label === 'string') {
|
|
234
|
+
return typedProps.label;
|
|
232
235
|
}
|
|
233
236
|
|
|
234
|
-
if (
|
|
235
|
-
return
|
|
237
|
+
if (typedProps.text && typeof typedProps.text === 'string') {
|
|
238
|
+
return typedProps.text;
|
|
236
239
|
}
|
|
237
240
|
|
|
238
241
|
return null;
|
package/src/utils/iconMap.tsx
CHANGED
|
@@ -14,89 +14,108 @@
|
|
|
14
14
|
|
|
15
15
|
import React from 'react';
|
|
16
16
|
|
|
17
|
-
// Material UI Icons -
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
17
|
+
// Material UI Icons - Direct imports to avoid loading the entire barrel index
|
|
18
|
+
// This prevents EMFILE errors when Node.js tries to parse thousands of icon exports
|
|
19
|
+
import AccountCircle from '@mui/icons-material/AccountCircle';
|
|
20
|
+
import Add from '@mui/icons-material/Add';
|
|
21
|
+
import Architecture from '@mui/icons-material/Architecture';
|
|
22
|
+
import ArrowBack from '@mui/icons-material/ArrowBack';
|
|
23
|
+
import ArrowForward from '@mui/icons-material/ArrowForward';
|
|
24
|
+
import Article from '@mui/icons-material/Article';
|
|
25
|
+
import AttachMoney from '@mui/icons-material/AttachMoney';
|
|
26
|
+
import Autorenew from '@mui/icons-material/Autorenew';
|
|
27
|
+
import Block from '@mui/icons-material/Block';
|
|
28
|
+
import Book from '@mui/icons-material/Book';
|
|
29
|
+
import Business from '@mui/icons-material/Business';
|
|
30
|
+
import Check from '@mui/icons-material/Check';
|
|
31
|
+
import CheckCircle from '@mui/icons-material/CheckCircle';
|
|
32
|
+
import Close from '@mui/icons-material/Close';
|
|
33
|
+
import Cloud from '@mui/icons-material/Cloud';
|
|
34
|
+
import CloudDownload from '@mui/icons-material/CloudDownload';
|
|
35
|
+
import CloudUpload from '@mui/icons-material/CloudUpload';
|
|
36
|
+
import Code from '@mui/icons-material/Code';
|
|
37
|
+
import Computer from '@mui/icons-material/Computer';
|
|
38
|
+
import Construction from '@mui/icons-material/Construction';
|
|
39
|
+
import Dashboard from '@mui/icons-material/Dashboard';
|
|
40
|
+
import Delete from '@mui/icons-material/Delete';
|
|
41
|
+
import Download from '@mui/icons-material/Download';
|
|
42
|
+
import Edit from '@mui/icons-material/Edit';
|
|
43
|
+
import Email from '@mui/icons-material/Email';
|
|
44
|
+
import Explore from '@mui/icons-material/Explore';
|
|
45
|
+
import Favorite from '@mui/icons-material/Favorite';
|
|
46
|
+
import Group from '@mui/icons-material/Group';
|
|
47
|
+
import Help from '@mui/icons-material/Help';
|
|
48
|
+
import HelpOutline from '@mui/icons-material/HelpOutline';
|
|
49
|
+
import Home from '@mui/icons-material/Home';
|
|
50
|
+
import Info from '@mui/icons-material/Info';
|
|
51
|
+
import InsertPhoto from '@mui/icons-material/InsertPhoto';
|
|
52
|
+
import IntegrationInstructions from '@mui/icons-material/IntegrationInstructions';
|
|
53
|
+
import Inventory from '@mui/icons-material/Inventory';
|
|
54
|
+
import Inventory2 from '@mui/icons-material/Inventory2';
|
|
55
|
+
import Key from '@mui/icons-material/Key';
|
|
56
|
+
import Layers from '@mui/icons-material/Layers';
|
|
57
|
+
import LibraryBooks from '@mui/icons-material/LibraryBooks';
|
|
58
|
+
import LocalOffer from '@mui/icons-material/LocalOffer';
|
|
59
|
+
import Lock from '@mui/icons-material/Lock';
|
|
60
|
+
import LockOpen from '@mui/icons-material/LockOpen';
|
|
61
|
+
import Login from '@mui/icons-material/Login';
|
|
62
|
+
import Logout from '@mui/icons-material/Logout';
|
|
63
|
+
import ManageAccounts from '@mui/icons-material/ManageAccounts';
|
|
64
|
+
import Memory from '@mui/icons-material/Memory';
|
|
65
|
+
import Menu from '@mui/icons-material/Menu';
|
|
66
|
+
import Notifications from '@mui/icons-material/Notifications';
|
|
67
|
+
import People from '@mui/icons-material/People';
|
|
68
|
+
import Person from '@mui/icons-material/Person';
|
|
69
|
+
import PersonSearch from '@mui/icons-material/PersonSearch';
|
|
70
|
+
import Phone from '@mui/icons-material/Phone';
|
|
71
|
+
import PhotoLibrary from '@mui/icons-material/PhotoLibrary';
|
|
72
|
+
import PlayArrow from '@mui/icons-material/PlayArrow';
|
|
73
|
+
import Psychology from '@mui/icons-material/Psychology';
|
|
74
|
+
import Refresh from '@mui/icons-material/Refresh';
|
|
75
|
+
import Rocket from '@mui/icons-material/Rocket';
|
|
76
|
+
import RotateRight from '@mui/icons-material/RotateRight';
|
|
77
|
+
import Route from '@mui/icons-material/Route';
|
|
78
|
+
import Save from '@mui/icons-material/Save';
|
|
79
|
+
import Search from '@mui/icons-material/Search';
|
|
80
|
+
import Security from '@mui/icons-material/Security';
|
|
81
|
+
import Send from '@mui/icons-material/Send';
|
|
82
|
+
import Settings from '@mui/icons-material/Settings';
|
|
83
|
+
import Share from '@mui/icons-material/Share';
|
|
84
|
+
import Shield from '@mui/icons-material/Shield';
|
|
85
|
+
import ShoppingCart from '@mui/icons-material/ShoppingCart';
|
|
86
|
+
import Speed from '@mui/icons-material/Speed';
|
|
87
|
+
import Star from '@mui/icons-material/Star';
|
|
88
|
+
import Storage from '@mui/icons-material/Storage';
|
|
89
|
+
import SupportAgent from '@mui/icons-material/SupportAgent';
|
|
90
|
+
import Sync from '@mui/icons-material/Sync';
|
|
91
|
+
import TrendingUp from '@mui/icons-material/TrendingUp';
|
|
92
|
+
import Tune from '@mui/icons-material/Tune';
|
|
93
|
+
import VerifiedUser from '@mui/icons-material/VerifiedUser';
|
|
94
|
+
import Visibility from '@mui/icons-material/Visibility';
|
|
95
|
+
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
|
96
|
+
import VpnKey from '@mui/icons-material/VpnKey';
|
|
97
|
+
import Work from '@mui/icons-material/Work';
|
|
98
|
+
import WorkspacePremium from '@mui/icons-material/WorkspacePremium';
|
|
99
|
+
// Additional icons for components
|
|
100
|
+
import Accessibility from '@mui/icons-material/Accessibility';
|
|
101
|
+
import BrokenImage from '@mui/icons-material/BrokenImage';
|
|
102
|
+
import Circle from '@mui/icons-material/Circle';
|
|
103
|
+
import ContentCopy from '@mui/icons-material/ContentCopy';
|
|
104
|
+
import DarkMode from '@mui/icons-material/DarkMode';
|
|
105
|
+
import Error from '@mui/icons-material/Error';
|
|
106
|
+
import ExpandLess from '@mui/icons-material/ExpandLess';
|
|
107
|
+
import ExpandMore from '@mui/icons-material/ExpandMore';
|
|
108
|
+
import FormatBold from '@mui/icons-material/FormatBold';
|
|
109
|
+
import FormatItalic from '@mui/icons-material/FormatItalic';
|
|
110
|
+
import FormatUnderlined from '@mui/icons-material/FormatUnderlined';
|
|
111
|
+
import Launch from '@mui/icons-material/Launch';
|
|
112
|
+
import LightMode from '@mui/icons-material/LightMode';
|
|
113
|
+
import MoreVert from '@mui/icons-material/MoreVert';
|
|
114
|
+
import Palette from '@mui/icons-material/Palette';
|
|
115
|
+
import RadioButtonUnchecked from '@mui/icons-material/RadioButtonUnchecked';
|
|
116
|
+
import Schedule from '@mui/icons-material/Schedule';
|
|
117
|
+
import SettingsSystemDaydream from '@mui/icons-material/SettingsSystemDaydream';
|
|
118
|
+
import Warning from '@mui/icons-material/Warning';
|
|
100
119
|
|
|
101
120
|
/**
|
|
102
121
|
* Icon mapping entry with both Material-UI component and emoji representation
|
|
@@ -235,6 +254,47 @@ export const iconMap: Record<string, IconMapping> = {
|
|
|
235
254
|
person_search: { emoji: '🔍', component: PersonSearch },
|
|
236
255
|
user: { emoji: '👤', component: Person }, // alias
|
|
237
256
|
users: { emoji: '👥', component: People }, // alias
|
|
257
|
+
|
|
258
|
+
// === UI & Interaction ===
|
|
259
|
+
accessibility: { emoji: '♿', component: Accessibility },
|
|
260
|
+
broken_image: { emoji: '🖼️', component: BrokenImage },
|
|
261
|
+
brokenimage: { emoji: '🖼️', component: BrokenImage }, // alias
|
|
262
|
+
circle: { emoji: '⭕', component: Circle },
|
|
263
|
+
content_copy: { emoji: '📋', component: ContentCopy },
|
|
264
|
+
contentcopy: { emoji: '📋', component: ContentCopy }, // alias
|
|
265
|
+
copy: { emoji: '📋', component: ContentCopy }, // alias
|
|
266
|
+
dark_mode: { emoji: '🌙', component: DarkMode },
|
|
267
|
+
darkmode: { emoji: '🌙', component: DarkMode }, // alias
|
|
268
|
+
error: { emoji: '❌', component: Error },
|
|
269
|
+
expand_less: { emoji: '▲', component: ExpandLess },
|
|
270
|
+
expandless: { emoji: '▲', component: ExpandLess }, // alias
|
|
271
|
+
expand_more: { emoji: '▼', component: ExpandMore },
|
|
272
|
+
expandmore: { emoji: '▼', component: ExpandMore }, // alias
|
|
273
|
+
format_bold: { emoji: '𝐁', component: FormatBold },
|
|
274
|
+
formatbold: { emoji: '𝐁', component: FormatBold }, // alias
|
|
275
|
+
bold: { emoji: '𝐁', component: FormatBold }, // alias
|
|
276
|
+
format_italic: { emoji: '𝐼', component: FormatItalic },
|
|
277
|
+
formatitalic: { emoji: '𝐼', component: FormatItalic }, // alias
|
|
278
|
+
italic: { emoji: '𝐼', component: FormatItalic }, // alias
|
|
279
|
+
format_underlined: { emoji: 'U̲', component: FormatUnderlined },
|
|
280
|
+
formatunderlined: { emoji: 'U̲', component: FormatUnderlined }, // alias
|
|
281
|
+
underline: { emoji: 'U̲', component: FormatUnderlined }, // alias
|
|
282
|
+
launch: { emoji: '🚀', component: Launch },
|
|
283
|
+
open: { emoji: '🚀', component: Launch }, // alias
|
|
284
|
+
light_mode: { emoji: '☀️', component: LightMode },
|
|
285
|
+
lightmode: { emoji: '☀️', component: LightMode }, // alias
|
|
286
|
+
more_vert: { emoji: '⋮', component: MoreVert },
|
|
287
|
+
morevert: { emoji: '⋮', component: MoreVert }, // alias
|
|
288
|
+
more: { emoji: '⋮', component: MoreVert }, // alias
|
|
289
|
+
palette: { emoji: '🎨', component: Palette },
|
|
290
|
+
radio_button_unchecked: { emoji: '○', component: RadioButtonUnchecked },
|
|
291
|
+
radiobuttonunchecked: { emoji: '○', component: RadioButtonUnchecked }, // alias
|
|
292
|
+
schedule: { emoji: '📅', component: Schedule },
|
|
293
|
+
calendar: { emoji: '📅', component: Schedule }, // alias
|
|
294
|
+
settings_system_daydream: { emoji: '💻', component: SettingsSystemDaydream },
|
|
295
|
+
settingssystemdaydream: { emoji: '💻', component: SettingsSystemDaydream }, // alias
|
|
296
|
+
system: { emoji: '💻', component: SettingsSystemDaydream }, // alias
|
|
297
|
+
warning: { emoji: '⚠️', component: Warning },
|
|
238
298
|
};
|
|
239
299
|
|
|
240
300
|
/**
|