@visns-studio/visns-components 6.0.4 → 6.1.0
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/package.json +1 -1
- package/src/components/DataGrid.jsx +37 -12
- package/src/components/Navigation.jsx +59 -3
- package/src/components/auth/Login.jsx +203 -175
- package/src/components/columns/ColumnRenderers.jsx +34 -0
- package/src/components/generic/GenericDashboard.jsx +181 -1
- package/src/components/generic/GenericFormBuilder.jsx +735 -37
- package/src/components/generic/GenericIndex.jsx +15 -5
- package/src/components/generic/OutstandingRuleEditor.jsx +313 -0
- package/src/components/styles/DataGrid.module.scss +38 -22
- package/src/components/styles/Form.module.scss +32 -33
- package/src/components/styles/GenericDashboard.module.scss +155 -0
- package/src/components/styles/GenericDetail.module.scss +28 -30
- package/src/components/styles/GenericDynamic.module.scss +7 -21
- package/src/components/styles/GenericEditableTable.module.scss +5 -22
- package/src/components/styles/GenericFormBuilder.module.scss +605 -30
- package/src/components/styles/GenericIndex.module.scss +17 -32
- package/src/components/styles/GenericMain.module.scss +18 -6
- package/src/components/styles/GenericQuote.module.scss +7 -22
- package/src/components/styles/GenericReport.module.scss +5 -37
- package/src/components/styles/Login.module.scss +415 -239
- package/src/components/styles/Navigation.module.scss +124 -35
- package/src/components/styles/Profile.module.scss +5 -26
- package/src/components/styles/QuickAction.module.scss +34 -29
- package/src/components/styles/_controls.scss +139 -0
- package/src/components/styles/global.css +54 -15
|
@@ -152,6 +152,40 @@ export const renderBooleanColumn = ({
|
|
|
152
152
|
render: ({ data }) => {
|
|
153
153
|
const rawValue = data[column.id];
|
|
154
154
|
const formattedValue = formatCellContent(rawValue, { ...column, type: 'boolean' });
|
|
155
|
+
|
|
156
|
+
// Coloured pill variant — opt in via column.badge, matching the
|
|
157
|
+
// option renderer. A yes/no column rendered as plain text reads
|
|
158
|
+
// the same as every other cell, so the state has to be read
|
|
159
|
+
// rather than scanned. Without column.badge nothing changes.
|
|
160
|
+
if (column.badge) {
|
|
161
|
+
const on = Boolean(rawValue) && rawValue !== '0';
|
|
162
|
+
const palette = on
|
|
163
|
+
? (column.badgeOn || { bg: '#eaf6ef', fg: '#1f6b41' })
|
|
164
|
+
: (column.badgeOff || { bg: '#f1f1ee', fg: '#6b7688' });
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<CellWithTooltip value={rawValue} columnType="boolean">
|
|
168
|
+
<span
|
|
169
|
+
style={{
|
|
170
|
+
display: 'inline-block',
|
|
171
|
+
padding: '2px 10px',
|
|
172
|
+
borderRadius: '999px',
|
|
173
|
+
fontSize: '11px',
|
|
174
|
+
fontWeight: 600,
|
|
175
|
+
lineHeight: 1.6,
|
|
176
|
+
letterSpacing: '0.02em',
|
|
177
|
+
textTransform: 'uppercase',
|
|
178
|
+
whiteSpace: 'nowrap',
|
|
179
|
+
backgroundColor: palette.bg,
|
|
180
|
+
color: palette.fg,
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
{formattedValue}
|
|
184
|
+
</span>
|
|
185
|
+
</CellWithTooltip>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
155
189
|
return (
|
|
156
190
|
<CellWithTooltip value={rawValue} columnType="boolean">
|
|
157
191
|
<span>{formattedValue}</span>
|
|
@@ -575,6 +575,9 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
|
575
575
|
)
|
|
576
576
|
);
|
|
577
577
|
const [data, setData] = useState({});
|
|
578
|
+
// Per-widget load state ('loading' | 'ready' | 'failed') keyed by widget.id.
|
|
579
|
+
// Tracked per widget so one slow/failed request only affects its own tile.
|
|
580
|
+
const [widgetStatus, setWidgetStatus] = useState({});
|
|
578
581
|
const [dropdowns, setDropdowns] = useState({});
|
|
579
582
|
const [filters, setFilters] = useState({});
|
|
580
583
|
const [editMode, setEditMode] = useState(false);
|
|
@@ -631,8 +634,23 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
|
631
634
|
updateDashboardSetting(dashboardSetting);
|
|
632
635
|
}, [dashboardSetting]);
|
|
633
636
|
|
|
637
|
+
// A widget only fetches when it has both a url and a method — anything else
|
|
638
|
+
// never loads, so it must never be marked 'loading'.
|
|
639
|
+
const widgetFetches = (widget) => Boolean(widget.api?.url && widget.api?.method);
|
|
640
|
+
|
|
634
641
|
const fetchData = async (appliedFilters = {}) => {
|
|
635
642
|
try {
|
|
643
|
+
const loadingStatus = dashboardSetting.widgets.reduce(
|
|
644
|
+
(acc, widget) => {
|
|
645
|
+
acc[widget.id] = widgetFetches(widget)
|
|
646
|
+
? 'loading'
|
|
647
|
+
: 'ready';
|
|
648
|
+
return acc;
|
|
649
|
+
},
|
|
650
|
+
{}
|
|
651
|
+
);
|
|
652
|
+
setWidgetStatus((prev) => ({ ...prev, ...loadingStatus }));
|
|
653
|
+
|
|
636
654
|
const promises = dashboardSetting.widgets.map(async (widget) => {
|
|
637
655
|
const widgetFilters = appliedFilters[widget.id] || {};
|
|
638
656
|
|
|
@@ -714,9 +732,40 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
|
714
732
|
},
|
|
715
733
|
{}
|
|
716
734
|
);
|
|
735
|
+
// Resolve each widget's status from the same settled results: a
|
|
736
|
+
// rejected promise or an API-error payload is a failure, anything
|
|
737
|
+
// else has finished loading.
|
|
738
|
+
const nextStatus = dashboardSetting.widgets.reduce(
|
|
739
|
+
(acc, widget, index) => {
|
|
740
|
+
if (!widgetFetches(widget)) {
|
|
741
|
+
acc[widget.id] = 'ready';
|
|
742
|
+
return acc;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const result = results[index];
|
|
746
|
+
acc[widget.id] =
|
|
747
|
+
result?.status === 'rejected' ||
|
|
748
|
+
result?.value?.data?.value
|
|
749
|
+
? 'failed'
|
|
750
|
+
: 'ready';
|
|
751
|
+
return acc;
|
|
752
|
+
},
|
|
753
|
+
{}
|
|
754
|
+
);
|
|
755
|
+
|
|
717
756
|
setData(fetchedData);
|
|
757
|
+
setWidgetStatus((prev) => ({ ...prev, ...nextStatus }));
|
|
718
758
|
} catch (error) {
|
|
719
759
|
console.error('Error fetching dashboard data:', error);
|
|
760
|
+
// Never leave a widget stuck on a skeleton — a hard failure of the
|
|
761
|
+
// whole fetch marks everything still in flight as failed.
|
|
762
|
+
setWidgetStatus((prev) =>
|
|
763
|
+
Object.keys(prev).reduce((acc, widgetId) => {
|
|
764
|
+
acc[widgetId] =
|
|
765
|
+
prev[widgetId] === 'loading' ? 'failed' : prev[widgetId];
|
|
766
|
+
return acc;
|
|
767
|
+
}, {})
|
|
768
|
+
);
|
|
720
769
|
}
|
|
721
770
|
};
|
|
722
771
|
|
|
@@ -1085,6 +1134,137 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
|
1085
1134
|
);
|
|
1086
1135
|
};
|
|
1087
1136
|
|
|
1137
|
+
// Placeholder shaped like the widget's real content. Every block reserves
|
|
1138
|
+
// the dimensions the loaded content occupies so nothing shifts on arrival.
|
|
1139
|
+
const renderWidgetSkeleton = (widget) => {
|
|
1140
|
+
const chartHeight = widget.height || '600px';
|
|
1141
|
+
const block = (extraClass, style, key) => (
|
|
1142
|
+
<div
|
|
1143
|
+
key={key}
|
|
1144
|
+
className={`${styles.skeletonBlock} ${extraClass}`}
|
|
1145
|
+
style={style}
|
|
1146
|
+
/>
|
|
1147
|
+
);
|
|
1148
|
+
|
|
1149
|
+
const renderBody = () => {
|
|
1150
|
+
switch (widget.type) {
|
|
1151
|
+
case 'counter':
|
|
1152
|
+
return (
|
|
1153
|
+
<>
|
|
1154
|
+
<div className={styles.skeletonCounter}>
|
|
1155
|
+
{block(styles.skeletonCounterValue)}
|
|
1156
|
+
</div>
|
|
1157
|
+
{widget.button && (
|
|
1158
|
+
<div className={styles.skeletonButtonRow}>
|
|
1159
|
+
{block(styles.skeletonButton)}
|
|
1160
|
+
</div>
|
|
1161
|
+
)}
|
|
1162
|
+
</>
|
|
1163
|
+
);
|
|
1164
|
+
case 'pie':
|
|
1165
|
+
return (
|
|
1166
|
+
<div
|
|
1167
|
+
className={styles.skeletonChart}
|
|
1168
|
+
style={{ height: chartHeight }}
|
|
1169
|
+
>
|
|
1170
|
+
{block(styles.skeletonCircle)}
|
|
1171
|
+
</div>
|
|
1172
|
+
);
|
|
1173
|
+
case 'bar':
|
|
1174
|
+
case 'line':
|
|
1175
|
+
return (
|
|
1176
|
+
<div
|
|
1177
|
+
className={`${styles.skeletonChart} ${styles.skeletonChartStrips}`}
|
|
1178
|
+
style={{ height: chartHeight }}
|
|
1179
|
+
>
|
|
1180
|
+
{[70, 45, 88, 58, 34].map((width, index) =>
|
|
1181
|
+
block(
|
|
1182
|
+
styles.skeletonStrip,
|
|
1183
|
+
{ width: `${width}%` },
|
|
1184
|
+
`${widget.id}-skeleton-strip-${index}`
|
|
1185
|
+
)
|
|
1186
|
+
)}
|
|
1187
|
+
</div>
|
|
1188
|
+
);
|
|
1189
|
+
case 'table':
|
|
1190
|
+
return (
|
|
1191
|
+
<div
|
|
1192
|
+
className={styles.skeletonTable}
|
|
1193
|
+
style={
|
|
1194
|
+
widget.height
|
|
1195
|
+
? {
|
|
1196
|
+
maxHeight: widget.height,
|
|
1197
|
+
overflow: 'hidden',
|
|
1198
|
+
}
|
|
1199
|
+
: undefined
|
|
1200
|
+
}
|
|
1201
|
+
>
|
|
1202
|
+
{block(styles.skeletonTableHeader)}
|
|
1203
|
+
{[0, 1, 2, 3, 4].map((index) =>
|
|
1204
|
+
block(
|
|
1205
|
+
styles.skeletonTableRow,
|
|
1206
|
+
undefined,
|
|
1207
|
+
`${widget.id}-skeleton-row-${index}`
|
|
1208
|
+
)
|
|
1209
|
+
)}
|
|
1210
|
+
</div>
|
|
1211
|
+
);
|
|
1212
|
+
case 'list':
|
|
1213
|
+
return (
|
|
1214
|
+
<div className={styles.skeletonList}>
|
|
1215
|
+
{[0, 1, 2, 3].map((index) =>
|
|
1216
|
+
block(
|
|
1217
|
+
styles.skeletonListRow,
|
|
1218
|
+
undefined,
|
|
1219
|
+
`${widget.id}-skeleton-list-${index}`
|
|
1220
|
+
)
|
|
1221
|
+
)}
|
|
1222
|
+
</div>
|
|
1223
|
+
);
|
|
1224
|
+
case 'timeline':
|
|
1225
|
+
return block(styles.skeletonTimeline, {
|
|
1226
|
+
height: widget.height || '500px',
|
|
1227
|
+
});
|
|
1228
|
+
default:
|
|
1229
|
+
return block(styles.skeletonGeneric);
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
|
|
1233
|
+
return (
|
|
1234
|
+
<div
|
|
1235
|
+
className={styles.skeleton}
|
|
1236
|
+
role="status"
|
|
1237
|
+
aria-busy="true"
|
|
1238
|
+
aria-label={`Loading ${widget.title || 'widget'}`}
|
|
1239
|
+
>
|
|
1240
|
+
{renderBody()}
|
|
1241
|
+
</div>
|
|
1242
|
+
);
|
|
1243
|
+
};
|
|
1244
|
+
|
|
1245
|
+
// Skeletons show only until a widget has data for the first time: a
|
|
1246
|
+
// background auto-refresh must never blank a value someone is reading.
|
|
1247
|
+
const renderWidgetBody = (widget) => {
|
|
1248
|
+
const status = widgetStatus[widget.id];
|
|
1249
|
+
const hasLoadedOnce = data[widget.id] !== undefined;
|
|
1250
|
+
|
|
1251
|
+
if (!hasLoadedOnce) {
|
|
1252
|
+
if (status === 'loading') {
|
|
1253
|
+
return renderWidgetSkeleton(widget);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if (status === 'failed') {
|
|
1257
|
+
return (
|
|
1258
|
+
<div className={styles.noData}>
|
|
1259
|
+
Couldn't load this widget.
|
|
1260
|
+
</div>
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
return renderWidget(widget);
|
|
1266
|
+
};
|
|
1267
|
+
|
|
1088
1268
|
const renderWidget = (widget) => {
|
|
1089
1269
|
const widgetData = data[widget.id] || [];
|
|
1090
1270
|
|
|
@@ -1914,7 +2094,7 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
|
1914
2094
|
)}
|
|
1915
2095
|
</div>
|
|
1916
2096
|
{renderFilters(widget)}
|
|
1917
|
-
<div>{
|
|
2097
|
+
<div>{renderWidgetBody(widget)}</div>
|
|
1918
2098
|
{editMode && (
|
|
1919
2099
|
<div className={styles.widgetTools}>
|
|
1920
2100
|
<div className={styles.resizeTools}>
|