@evoke-platform/ui-components 1.6.0-testing.1 → 1.6.0-testing.11
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/published/components/core/SwipeableDrawer/SwipeableDrawer.d.ts +4 -0
- package/dist/published/components/core/SwipeableDrawer/SwipeableDrawer.js +8 -0
- package/dist/published/components/core/SwipeableDrawer/index.d.ts +3 -0
- package/dist/published/components/core/SwipeableDrawer/index.js +3 -0
- package/dist/published/components/core/index.d.ts +2 -1
- package/dist/published/components/core/index.js +2 -1
- package/dist/published/components/custom/BuilderGrid/BuilderGrid.js +27 -27
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +1 -1
- package/dist/published/components/custom/Form/FormComponents/ImageComponent/Image.js +2 -2
- package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/RepeatableField.js +87 -57
- package/dist/published/components/custom/Form/tests/Form.test.js +1 -1
- package/dist/published/components/custom/FormField/Select/Select.js +17 -5
- package/dist/published/components/custom/HistoryLog/HistoryData.d.ts +1 -0
- package/dist/published/components/custom/HistoryLog/HistoryData.js +14 -6
- package/dist/published/components/custom/HistoryLog/HistoryLoading.d.ts +4 -1
- package/dist/published/components/custom/HistoryLog/HistoryLoading.js +14 -8
- package/dist/published/components/custom/HistoryLog/index.d.ts +2 -0
- package/dist/published/components/custom/HistoryLog/index.js +4 -4
- package/dist/published/components/custom/ResponsiveOverflow/ResponsiveOverflow.d.ts +33 -0
- package/dist/published/components/custom/ResponsiveOverflow/ResponsiveOverflow.js +139 -0
- package/dist/published/components/custom/ResponsiveOverflow/ResponsiveOverflow.test.d.ts +1 -0
- package/dist/published/components/custom/ResponsiveOverflow/ResponsiveOverflow.test.js +100 -0
- package/dist/published/components/custom/ResponsiveOverflow/index.d.ts +4 -0
- package/dist/published/components/custom/ResponsiveOverflow/index.js +3 -0
- package/dist/published/components/custom/index.d.ts +1 -0
- package/dist/published/components/custom/index.js +1 -0
- package/dist/published/index.d.ts +1 -1
- package/dist/published/index.js +1 -1
- package/dist/published/stories/ResponsiveOverflow.stories.d.ts +8 -0
- package/dist/published/stories/ResponsiveOverflow.stories.js +91 -0
- package/package.json +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { SwipeableDrawer as MuiSwipeableDrawer } from '@mui/material';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import UIThemeProvider from '../../../theme';
|
|
4
|
+
const SwipeableDrawer = (props) => {
|
|
5
|
+
return (React.createElement(UIThemeProvider, null,
|
|
6
|
+
React.createElement(MuiSwipeableDrawer, { ...props })));
|
|
7
|
+
};
|
|
8
|
+
export default SwipeableDrawer;
|
|
@@ -37,6 +37,7 @@ export { Skeleton } from './Skeleton';
|
|
|
37
37
|
export { Snackbar } from './Snackbar';
|
|
38
38
|
export * from './StaticDatePicker';
|
|
39
39
|
export { Step, StepButton, StepConnector, StepContent, StepIcon, StepLabel, Stepper } from './Stepper';
|
|
40
|
+
export { SwipeableDrawer } from './SwipeableDrawer';
|
|
40
41
|
export { Switch } from './Switch';
|
|
41
42
|
export { Table } from './Table';
|
|
42
43
|
export { Tab, Tabs } from './Tabs';
|
|
@@ -47,7 +48,7 @@ export { Tooltip } from './Tooltip';
|
|
|
47
48
|
export { Typography } from './Typography';
|
|
48
49
|
export { TabContext, TabList, TabPanel, TreeItem, TreeView } from '@mui/lab';
|
|
49
50
|
export { CardActionArea, CardActions, CardContent, CardHeader, CardMedia, Input, InputAdornment, InputLabel, ListItemButton, ListItemText, MenuList, SvgIcon, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, TableSortLabel, } from '@mui/material';
|
|
50
|
-
export { useGridApiRef } from '@mui/x-data-grid';
|
|
51
|
+
export { GridToolbarContainer, GridToolbarQuickFilter, useGridApiRef } from '@mui/x-data-grid';
|
|
51
52
|
export { TreeItem as RichTreeItem, RichTreeView, TreeItem2Content, TreeItem2DragAndDropOverlay, TreeItem2GroupTransition, TreeItem2Icon, TreeItem2IconContainer, TreeItem2Label, TreeItem2Provider, TreeItem2Root, useTreeItem2, } from '@mui/x-tree-view';
|
|
52
53
|
export type { GridSize } from '@mui/material';
|
|
53
54
|
export type { GridCellParams, GridColDef, GridEventListener, GridFilterModel, GridInitialState, GridRowParams, GridSortModel, GridValueFormatterParams, GridValueGetterParams, } from '@mui/x-data-grid';
|
|
@@ -37,6 +37,7 @@ export { Skeleton } from './Skeleton';
|
|
|
37
37
|
export { Snackbar } from './Snackbar';
|
|
38
38
|
export * from './StaticDatePicker';
|
|
39
39
|
export { Step, StepButton, StepConnector, StepContent, StepIcon, StepLabel, Stepper } from './Stepper';
|
|
40
|
+
export { SwipeableDrawer } from './SwipeableDrawer';
|
|
40
41
|
export { Switch } from './Switch';
|
|
41
42
|
export { Table } from './Table';
|
|
42
43
|
export { Tab, Tabs } from './Tabs';
|
|
@@ -48,5 +49,5 @@ export { Typography } from './Typography';
|
|
|
48
49
|
//TODO: Review following components. They also need theme control:
|
|
49
50
|
export { TabContext, TabList, TabPanel, TreeItem, TreeView } from '@mui/lab';
|
|
50
51
|
export { CardActionArea, CardActions, CardContent, CardHeader, CardMedia, Input, InputAdornment, InputLabel, ListItemButton, ListItemText, MenuList, SvgIcon, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, TableSortLabel, } from '@mui/material';
|
|
51
|
-
export { useGridApiRef } from '@mui/x-data-grid';
|
|
52
|
+
export { GridToolbarContainer, GridToolbarQuickFilter, useGridApiRef } from '@mui/x-data-grid';
|
|
52
53
|
export { TreeItem as RichTreeItem, RichTreeView, TreeItem2Content, TreeItem2DragAndDropOverlay, TreeItem2GroupTransition, TreeItem2Icon, TreeItem2IconContainer, TreeItem2Label, TreeItem2Provider, TreeItem2Root, useTreeItem2, } from '@mui/x-tree-view';
|
|
@@ -17,7 +17,32 @@ const BuilderGrid = (props) => {
|
|
|
17
17
|
borderBottom: 'none',
|
|
18
18
|
boxShadow: 'rgba(145, 158, 171, 0.2) 0px 8px 16px',
|
|
19
19
|
} },
|
|
20
|
-
React.createElement(MuiDataGrid, { autoPageSize: !disablePagination, hideFooterPagination: disablePagination, hideFooter: disablePagination, onMenuOpen: (env) => setAnchorEl(env.target), loading: loading, rows: rows,
|
|
20
|
+
React.createElement(MuiDataGrid, { autoPageSize: !disablePagination, hideFooterPagination: disablePagination, hideFooter: disablePagination, onMenuOpen: (env) => setAnchorEl(env.target), loading: loading, rows: rows, getRowId: (row) => row.id, disableColumnMenu: true, initialState: {
|
|
21
|
+
sorting: {
|
|
22
|
+
sortModel: initialSort ? [initialSort] : [],
|
|
23
|
+
},
|
|
24
|
+
}, componentsProps: {
|
|
25
|
+
panel: {
|
|
26
|
+
anchorEl: anchorEl,
|
|
27
|
+
placement: 'bottom-end',
|
|
28
|
+
sx: {
|
|
29
|
+
'& .MuiPaper-root': {
|
|
30
|
+
borderRadius: '6px',
|
|
31
|
+
boxShadow: '0px 24px 48px rgba(145, 158, 171, 0.4)',
|
|
32
|
+
padding: '8px',
|
|
33
|
+
stop: -120,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
}, slots: {
|
|
38
|
+
toolbar: hideToolbar ? null : toolbar,
|
|
39
|
+
noResultsOverlay: () => {
|
|
40
|
+
return React.createElement(Box, null);
|
|
41
|
+
},
|
|
42
|
+
noRowsOverlay: () => {
|
|
43
|
+
return noRowsOverlay ? noRowsOverlay : null;
|
|
44
|
+
},
|
|
45
|
+
}, rowHeight: 60, columnHeaderHeight: 62, ...rest, sx: {
|
|
21
46
|
border: 'none',
|
|
22
47
|
'& .MuiDataGrid-toolbarContainer': {
|
|
23
48
|
padding: '0',
|
|
@@ -64,32 +89,7 @@ const BuilderGrid = (props) => {
|
|
|
64
89
|
},
|
|
65
90
|
height: disablePagination && !loading ? 'auto' : 'calc(100vh - 240px)',
|
|
66
91
|
...rest.sx,
|
|
67
|
-
}
|
|
68
|
-
sorting: {
|
|
69
|
-
sortModel: initialSort ? [initialSort] : [],
|
|
70
|
-
},
|
|
71
|
-
}, componentsProps: {
|
|
72
|
-
panel: {
|
|
73
|
-
anchorEl: anchorEl,
|
|
74
|
-
placement: 'bottom-end',
|
|
75
|
-
sx: {
|
|
76
|
-
'& .MuiPaper-root': {
|
|
77
|
-
borderRadius: '6px',
|
|
78
|
-
boxShadow: '0px 24px 48px rgba(145, 158, 171, 0.4)',
|
|
79
|
-
padding: '8px',
|
|
80
|
-
stop: -120,
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
}, slots: {
|
|
85
|
-
toolbar: hideToolbar ? null : toolbar,
|
|
86
|
-
noResultsOverlay: () => {
|
|
87
|
-
return React.createElement(Box, null);
|
|
88
|
-
},
|
|
89
|
-
noRowsOverlay: () => {
|
|
90
|
-
return noRowsOverlay ? noRowsOverlay : null;
|
|
91
|
-
},
|
|
92
|
-
}, rowHeight: 60, columnHeaderHeight: 62 }))) : (React.createElement(React.Fragment, null, error ? (React.createElement(Box, { sx: {
|
|
92
|
+
} }))) : (React.createElement(React.Fragment, null, error ? (React.createElement(Box, { sx: {
|
|
93
93
|
backgroundColor: '#fff',
|
|
94
94
|
borderRadius: '6px',
|
|
95
95
|
display: 'flex',
|
|
@@ -186,7 +186,7 @@ const customSelector = (props) => {
|
|
|
186
186
|
return opts.find((o) => option === o.name)?.label || option;
|
|
187
187
|
}
|
|
188
188
|
return option.label;
|
|
189
|
-
}, isOptionEqualToValue: (option, value) => {
|
|
189
|
+
}, getOptionKey: (option) => option.name, isOptionEqualToValue: (option, value) => {
|
|
190
190
|
if (typeof option === 'string') {
|
|
191
191
|
return option === value;
|
|
192
192
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { BackupOutlined, ClearRounded } from '@mui/icons-material';
|
|
2
1
|
import React, { useEffect, useState } from 'react';
|
|
3
2
|
import { useDropzone } from 'react-dropzone';
|
|
3
|
+
import { ClearRounded, UploadCloud } from '../../../../../icons';
|
|
4
4
|
import { CardMedia, IconButton, Typography } from '../../../../core';
|
|
5
5
|
import { Box, Grid } from '../../../../layout';
|
|
6
6
|
export function blobToDataUrl(blob) {
|
|
@@ -84,7 +84,7 @@ export const Image = (props) => {
|
|
|
84
84
|
React.createElement("input", { ...getInputProps({ id }), multiple: false }),
|
|
85
85
|
React.createElement(Grid, { container: true, sx: { width: '100%' } },
|
|
86
86
|
React.createElement(Grid, { item: true, xs: 12, sx: { display: 'flex', justifyContent: 'center', paddingBottom: '5px' } },
|
|
87
|
-
React.createElement(
|
|
87
|
+
React.createElement(UploadCloud, { sx: { color: '#919EAB', width: '50px', height: '30px' } })),
|
|
88
88
|
React.createElement(Grid, { item: true, xs: 12 },
|
|
89
89
|
React.createElement(Typography, { variant: "body2", sx: { color: '#212B36', textAlign: 'center' } },
|
|
90
90
|
"Drag and drop or",
|
|
@@ -7,6 +7,7 @@ import sift from 'sift';
|
|
|
7
7
|
import { Edit, TrashCan } from '../../../../../icons';
|
|
8
8
|
import { Button, IconButton, Skeleton, Snackbar, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Tooltip, Typography, } from '../../../../core';
|
|
9
9
|
import { Box } from '../../../../layout';
|
|
10
|
+
import { getReadableQuery } from '../../../CriteriaBuilder';
|
|
10
11
|
import { getPrefixedUrl, normalizeDateTime, retrieveCustomErrorMessage } from '../../utils';
|
|
11
12
|
import { ActionDialog } from './ActionDialog';
|
|
12
13
|
import { DocumentViewerCell } from './DocumentViewerCell';
|
|
@@ -37,6 +38,7 @@ const RepeatableField = (props) => {
|
|
|
37
38
|
const [relatedObject, setRelatedObject] = useState();
|
|
38
39
|
const [hasCreateAction, setHasCreateAction] = useState(false);
|
|
39
40
|
const [users, setUsers] = useState();
|
|
41
|
+
const [criteriaObjects, setCriteriaObjects] = useState([]);
|
|
40
42
|
const [openDialog, setOpenDialog] = useState(false);
|
|
41
43
|
const [dialogType, setDialogType] = useState();
|
|
42
44
|
const [selectedRow, setSelectedRow] = useState();
|
|
@@ -50,9 +52,7 @@ const RepeatableField = (props) => {
|
|
|
50
52
|
const [error, setError] = useState(false);
|
|
51
53
|
const DEFAULT_CREATE_ACTION = '_create';
|
|
52
54
|
const { instanceChanges } = useNotification();
|
|
53
|
-
const
|
|
54
|
-
if (openDialog)
|
|
55
|
-
return;
|
|
55
|
+
const fetchRelatedObject = useCallback(async () => {
|
|
56
56
|
let relatedObject;
|
|
57
57
|
if (property.objectId) {
|
|
58
58
|
try {
|
|
@@ -80,31 +80,86 @@ const RepeatableField = (props) => {
|
|
|
80
80
|
catch (err) {
|
|
81
81
|
console.error(error);
|
|
82
82
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
clearTimeout(timeout);
|
|
83
|
+
}
|
|
84
|
+
relatedObject && checkCreateAccess(relatedObject);
|
|
85
|
+
}, [apiServices, property]);
|
|
86
|
+
const fetchRelatedInstances = useCallback(async () => {
|
|
87
|
+
if (openDialog)
|
|
88
|
+
return;
|
|
89
|
+
if (property.objectId && property.relatedPropertyId && instance?.id) {
|
|
90
|
+
const filterProperty = `${property.relatedPropertyId}.id`;
|
|
91
|
+
const filter = { where: { [filterProperty]: instance?.id }, limit: 100 };
|
|
92
|
+
const objectId = property.objectId;
|
|
93
|
+
try {
|
|
94
|
+
const timeout = setTimeout(() => {
|
|
96
95
|
setLoading(false);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
}, 300);
|
|
97
|
+
setLoading(true);
|
|
98
|
+
const instances = await apiServices.get(getPrefixedUrl(`/objects/${objectId}/instances`), {
|
|
99
|
+
params: { filter: JSON.stringify(filter) },
|
|
100
|
+
});
|
|
101
|
+
clearTimeout(timeout);
|
|
102
|
+
setLoading(false);
|
|
103
|
+
if (instances) {
|
|
104
|
+
setRelatedInstances(instances);
|
|
103
105
|
}
|
|
104
106
|
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
setError(true);
|
|
109
|
+
}
|
|
105
110
|
}
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
}, [apiServices, property]);
|
|
112
|
+
const fetchCriteriaObjects = useCallback(async () => {
|
|
113
|
+
let objectIds = [];
|
|
114
|
+
const criteriaProperties = relatedObject?.properties?.filter((property) => property.type === 'criteria' && property.objectId) ?? [];
|
|
115
|
+
if (tableViewLayout) {
|
|
116
|
+
objectIds = criteriaProperties
|
|
117
|
+
.filter((p) => tableViewLayout.properties.some((column) => column.id === p.id))
|
|
118
|
+
.map((property) => property.objectId);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
objectIds = criteriaProperties.map((p) => p.objectId);
|
|
122
|
+
}
|
|
123
|
+
const objects = [];
|
|
124
|
+
for (const objectId of new Set(objectIds)) {
|
|
125
|
+
try {
|
|
126
|
+
const criteriaObject = await apiServices.get(getPrefixedUrl(`/objects/${objectId}/effective`), {
|
|
127
|
+
params: { fields: ['id', 'name', 'properties'] },
|
|
128
|
+
});
|
|
129
|
+
objects.push(criteriaObject);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
console.error(`Error fetching criteria object with ID ${objectId}:`, error);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
setCriteriaObjects(objects);
|
|
136
|
+
}, [apiServices, relatedObject, tableViewLayout]);
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
(async () => {
|
|
139
|
+
try {
|
|
140
|
+
const users = await apiServices.get(getPrefixedUrl(`/users`));
|
|
141
|
+
setUsers(users);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
console.error(error);
|
|
145
|
+
}
|
|
146
|
+
})();
|
|
147
|
+
}, [apiServices]);
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
fetchRelatedObject();
|
|
150
|
+
fetchRelatedInstances();
|
|
151
|
+
}, [fetchRelatedInstances, fetchRelatedObject, reloadOnErrorTrigger, instance]);
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
if (relatedObject)
|
|
154
|
+
fetchCriteriaObjects();
|
|
155
|
+
}, [fetchCriteriaObjects, relatedObject]);
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (relatedObject?.rootObjectId) {
|
|
158
|
+
const callback = () => fetchRelatedInstances();
|
|
159
|
+
instanceChanges?.subscribe(relatedObject?.rootObjectId, callback);
|
|
160
|
+
return () => instanceChanges?.unsubscribe(relatedObject?.rootObjectId, callback);
|
|
161
|
+
}
|
|
162
|
+
}, [instanceChanges, relatedObject]);
|
|
108
163
|
const retrieveCriteria = (relatedObjProperty, action, object) => {
|
|
109
164
|
let property;
|
|
110
165
|
if (action.parameters) {
|
|
@@ -171,27 +226,6 @@ const RepeatableField = (props) => {
|
|
|
171
226
|
});
|
|
172
227
|
}
|
|
173
228
|
};
|
|
174
|
-
useEffect(() => {
|
|
175
|
-
(async () => {
|
|
176
|
-
try {
|
|
177
|
-
const users = await apiServices.get(getPrefixedUrl(`/users`));
|
|
178
|
-
setUsers(users);
|
|
179
|
-
}
|
|
180
|
-
catch (error) {
|
|
181
|
-
console.error(error);
|
|
182
|
-
}
|
|
183
|
-
})();
|
|
184
|
-
}, [apiServices]);
|
|
185
|
-
useEffect(() => {
|
|
186
|
-
fetchRelatedInstances();
|
|
187
|
-
}, [fetchRelatedInstances, reloadOnErrorTrigger, instance]);
|
|
188
|
-
useEffect(() => {
|
|
189
|
-
if (relatedObject?.rootObjectId) {
|
|
190
|
-
const callback = () => fetchRelatedInstances();
|
|
191
|
-
instanceChanges?.subscribe(relatedObject?.rootObjectId, callback);
|
|
192
|
-
return () => instanceChanges?.unsubscribe(relatedObject?.rootObjectId, callback);
|
|
193
|
-
}
|
|
194
|
-
}, [instanceChanges, relatedObject]);
|
|
195
229
|
const deleteRow = (id) => {
|
|
196
230
|
setDialogType('delete');
|
|
197
231
|
setSelectedRow(id);
|
|
@@ -334,28 +368,24 @@ const RepeatableField = (props) => {
|
|
|
334
368
|
};
|
|
335
369
|
const getValue = (relatedInstance, propertyId, propertyType) => {
|
|
336
370
|
const value = get(relatedInstance, propertyId);
|
|
337
|
-
// If the property is not date-like then just return the
|
|
338
|
-
// value found at the given path.
|
|
339
|
-
if (!['date', 'date-time', 'time'].includes(propertyType)) {
|
|
340
|
-
return value;
|
|
341
|
-
}
|
|
342
371
|
// If the date-like value is empty then there is no need to format.
|
|
343
372
|
if (!value) {
|
|
344
373
|
return value;
|
|
345
374
|
}
|
|
346
|
-
// At this point it has been asserted that there is a value
|
|
347
|
-
// and since the property is date-like the value must be
|
|
348
|
-
// a string.
|
|
349
|
-
const stringValue = value;
|
|
350
375
|
if (propertyType === 'date') {
|
|
351
|
-
return DateTime.fromISO(
|
|
376
|
+
return DateTime.fromISO(value).toLocaleString(DateTime.DATE_SHORT);
|
|
352
377
|
}
|
|
353
378
|
if (propertyType === 'date-time') {
|
|
354
|
-
return DateTime.fromISO(
|
|
379
|
+
return DateTime.fromISO(value).toLocaleString(DateTime.DATETIME_SHORT);
|
|
355
380
|
}
|
|
356
381
|
if (propertyType === 'time') {
|
|
357
|
-
return DateTime.fromISO(
|
|
382
|
+
return DateTime.fromISO(value).toLocaleString(DateTime.TIME_SIMPLE);
|
|
383
|
+
}
|
|
384
|
+
if (propertyType === 'criteria' && typeof value === 'object') {
|
|
385
|
+
const property = relatedObject?.properties?.find((p) => p.id === propertyId);
|
|
386
|
+
return getReadableQuery(value, criteriaObjects.find((o) => o.id === property?.objectId)?.properties ?? []);
|
|
358
387
|
}
|
|
388
|
+
return value;
|
|
359
389
|
};
|
|
360
390
|
const columns = retrieveViewLayout();
|
|
361
391
|
return loading ? (React.createElement(React.Fragment, null,
|
|
@@ -6,7 +6,7 @@ import { isEqual } from 'lodash';
|
|
|
6
6
|
import { http, HttpResponse } from 'msw';
|
|
7
7
|
import { setupServer } from 'msw/node';
|
|
8
8
|
import React from 'react';
|
|
9
|
-
import { it } from 'vitest';
|
|
9
|
+
import { expect, it } from 'vitest';
|
|
10
10
|
import Form from '../Common/Form';
|
|
11
11
|
import { licenseObject, npLicense, npSpecialtyType1, npSpecialtyType2, rnLicense, rnSpecialtyType1, rnSpecialtyType2, specialtyObject, specialtyTypeObject, } from './test-data';
|
|
12
12
|
const removePoppers = () => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createFilterOptions, List, ListSubheader } from '@mui/material';
|
|
2
|
+
import { uniq } from 'lodash';
|
|
2
3
|
import React, { forwardRef, useEffect, useRef, useState } from 'react';
|
|
3
4
|
import { Clear } from '../../../../icons';
|
|
4
5
|
import { Autocomplete, FormControl, FormControlLabel, IconButton, Radio, RadioGroup, TextField, Typography, } from '../../../core';
|
|
@@ -15,7 +16,7 @@ const Select = (props) => {
|
|
|
15
16
|
const [isOtherFocused, setIsOtherFocused] = useState(false);
|
|
16
17
|
const [value, setValue] = useState(defaultValue);
|
|
17
18
|
const [inputValue, setInputValue] = useState(!selectOptions?.some((option) => (typeof option === 'string' && option === defaultValue) ||
|
|
18
|
-
option.value === defaultValue)
|
|
19
|
+
option.value === defaultValue) && property.type !== 'array'
|
|
19
20
|
? defaultValue
|
|
20
21
|
: '');
|
|
21
22
|
const [errorState, setErrorState] = useState();
|
|
@@ -27,11 +28,14 @@ const Select = (props) => {
|
|
|
27
28
|
otherInputRef.current.focus();
|
|
28
29
|
}
|
|
29
30
|
}, [isOther, value]);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
setValue(defaultValue);
|
|
33
|
+
}, [defaultValue]);
|
|
30
34
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
35
|
const handleChange = (event, selected) => {
|
|
32
36
|
if (Array.isArray(selected)) {
|
|
33
37
|
const newValues = selected.map((option) => option.value ?? option);
|
|
34
|
-
setValue(newValues);
|
|
38
|
+
setValue(uniq(newValues));
|
|
35
39
|
onChange && onChange(property.id, newValues, property);
|
|
36
40
|
}
|
|
37
41
|
else {
|
|
@@ -144,6 +148,7 @@ const Select = (props) => {
|
|
|
144
148
|
filtered.push({
|
|
145
149
|
value: inputValue,
|
|
146
150
|
label: `Add "${inputValue}"`,
|
|
151
|
+
isCustomValue: true,
|
|
147
152
|
});
|
|
148
153
|
}
|
|
149
154
|
return filtered;
|
|
@@ -151,12 +156,19 @@ const Select = (props) => {
|
|
|
151
156
|
? (option, value) => isOptionEqualToValue(option, value)
|
|
152
157
|
: undefined, getOptionLabel: getOptionLabel && !isCombobox
|
|
153
158
|
? (option) => getOptionLabel(option)
|
|
154
|
-
: (option) =>
|
|
159
|
+
: (option) => {
|
|
160
|
+
if (typeof option === 'string')
|
|
161
|
+
return option;
|
|
162
|
+
// If the option is a custom value, return the value without the prepended "Add" text.
|
|
163
|
+
if (option.isCustomValue)
|
|
164
|
+
return option.value;
|
|
165
|
+
return option.label ?? '';
|
|
166
|
+
}, renderOption: renderOption
|
|
155
167
|
? (props, option, state) => renderOption(props, option, state)
|
|
156
|
-
:
|
|
168
|
+
: (props, option) => React.createElement("li", { ...props }, option.label ?? option), ListboxComponent: ListboxComponent, disableCloseOnSelect: disableCloseOnSelect, sx: {
|
|
157
169
|
'& button.MuiButtonBase-root': {
|
|
158
170
|
visibility: 'visible',
|
|
159
171
|
},
|
|
160
|
-
}, ...(isCombobox ? { selectOnFocus: true, handleHomeEndKeys: true, freeSolo: true } : {}), ...(additionalProps ?? {}) }));
|
|
172
|
+
}, forcePopupIcon: true, ...(isCombobox ? { selectOnFocus: true, handleHomeEndKeys: true, freeSolo: true } : {}), ...(additionalProps ?? {}) }));
|
|
161
173
|
};
|
|
162
174
|
export default Select;
|
|
@@ -19,8 +19,7 @@ const styles = {
|
|
|
19
19
|
color: '#637381',
|
|
20
20
|
},
|
|
21
21
|
timelineConnector: {
|
|
22
|
-
padding: '1px 0px 24px
|
|
23
|
-
borderLeft: '2px rgba(145, 158, 171, 0.24) solid',
|
|
22
|
+
padding: '1px 0px 24px 0px',
|
|
24
23
|
marginLeft: '5px',
|
|
25
24
|
},
|
|
26
25
|
historyData: {
|
|
@@ -30,15 +29,24 @@ const styles = {
|
|
|
30
29
|
},
|
|
31
30
|
};
|
|
32
31
|
const HistoricalData = (props) => {
|
|
33
|
-
const { records, documentHistory, object, referencedObjects } = props;
|
|
32
|
+
const { records, documentHistory, object, referencedObjects, hideTimeline } = props;
|
|
34
33
|
const getPastDocumentVersion = (history) => {
|
|
35
34
|
const documentVersions = documentHistory?.[history.subject?.id ?? 'unknown'] ?? [];
|
|
36
35
|
const currentVersion = documentVersions?.map((v) => v.timestamp).indexOf(history.timestamp);
|
|
37
36
|
return currentVersion ? documentVersions[currentVersion - 1] : undefined;
|
|
38
37
|
};
|
|
39
|
-
return (React.createElement(Box, { sx:
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
return (React.createElement(Box, { sx: {
|
|
39
|
+
...styles.timelineConnector,
|
|
40
|
+
borderLeft: !hideTimeline ? '2px rgba(145, 158, 171, 0.24) solid' : undefined,
|
|
41
|
+
} }, records.map((r) => (React.createElement(Box, { margin: '16px 0px', key: `${r.timestamp}-${nanoid()}` },
|
|
42
|
+
React.createElement(Box, { sx: {
|
|
43
|
+
display: 'flex',
|
|
44
|
+
justifyContent: 'space-between',
|
|
45
|
+
flexDirection: { xs: 'column', sm: 'row' },
|
|
46
|
+
alignItems: { xs: 'flex-start', sm: 'center' },
|
|
47
|
+
gap: 0,
|
|
48
|
+
} },
|
|
49
|
+
React.createElement(Box, { sx: { display: 'flex', maxWidth: { sx: '100%', sm: '60%' }, alignContent: 'flex-start' } },
|
|
42
50
|
React.createElement(Typography, { sx: { fontSize: '12px', color: '#637381' } },
|
|
43
51
|
React.createElement(Typography, { component: 'span', sx: { fontWeight: 600, fontSize: '12px' } }, r.user.name ?? 'Unknown User'),
|
|
44
52
|
"\u00A0",
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
+
import { Circle } from '@mui/icons-material';
|
|
1
2
|
import React from 'react';
|
|
2
|
-
import { Box } from '../../layout';
|
|
3
3
|
import { Skeleton } from '../../core';
|
|
4
|
-
import {
|
|
4
|
+
import { Box } from '../../layout';
|
|
5
5
|
const styles = {
|
|
6
6
|
timelineConnector: {
|
|
7
7
|
padding: '1px 0px 5px 22px',
|
|
8
|
-
borderLeft: '2px rgba(145, 158, 171, 0.24) solid',
|
|
9
8
|
marginLeft: '5px',
|
|
10
9
|
},
|
|
11
10
|
headerSkeleton: {
|
|
@@ -18,20 +17,27 @@ const styles = {
|
|
|
18
17
|
borderRadius: '7px',
|
|
19
18
|
},
|
|
20
19
|
};
|
|
21
|
-
const HistoryLoading = () => {
|
|
20
|
+
const HistoryLoading = (props) => {
|
|
21
|
+
const { hideTimeline } = props;
|
|
22
22
|
return (React.createElement(React.Fragment, { key: 'history-log-loading' },
|
|
23
23
|
React.createElement(Box, { sx: { display: 'flex', alignItems: 'center' } },
|
|
24
|
-
React.createElement(Circle, { color: "primary", sx: { fontSize: '12px', marginRight: '12px' } }),
|
|
24
|
+
!hideTimeline && React.createElement(Circle, { color: "primary", sx: { fontSize: '12px', marginRight: '12px' } }),
|
|
25
25
|
React.createElement(Skeleton, { variant: "text", sx: styles.headerSkeleton })),
|
|
26
|
-
React.createElement(Box, { sx:
|
|
26
|
+
React.createElement(Box, { sx: {
|
|
27
|
+
...styles.timelineConnector,
|
|
28
|
+
borderLeft: !hideTimeline ? '2px rgba(145, 158, 171, 0.24) solid' : undefined,
|
|
29
|
+
} },
|
|
27
30
|
React.createElement(Box, { margin: '4px 0px 24px 0px' },
|
|
28
31
|
React.createElement(Skeleton, { variant: "text", sx: { ...styles.dataSkeleton, width: '120px' } }),
|
|
29
32
|
React.createElement(Skeleton, { variant: "text", sx: { ...styles.dataSkeleton, width: '65%' } }),
|
|
30
33
|
React.createElement(Skeleton, { variant: "text", sx: { ...styles.dataSkeleton, width: '75%' } }))),
|
|
31
34
|
React.createElement(Box, { sx: { display: 'flex', alignItems: 'center' } },
|
|
32
|
-
React.createElement(Circle, { color: "primary", sx: { fontSize: '12px', marginRight: '12px' } }),
|
|
35
|
+
!hideTimeline && React.createElement(Circle, { color: "primary", sx: { fontSize: '12px', marginRight: '12px' } }),
|
|
33
36
|
React.createElement(Skeleton, { variant: "text", sx: styles.headerSkeleton })),
|
|
34
|
-
React.createElement(Box, { sx:
|
|
37
|
+
React.createElement(Box, { sx: {
|
|
38
|
+
...styles.timelineConnector,
|
|
39
|
+
borderLeft: !hideTimeline ? '2px rgba(145, 158, 171, 0.24) solid' : undefined,
|
|
40
|
+
} },
|
|
35
41
|
React.createElement(Box, { margin: '4px 0px 20px 0px' },
|
|
36
42
|
React.createElement(Skeleton, { variant: "text", sx: { ...styles.dataSkeleton, width: '120px' } }),
|
|
37
43
|
React.createElement(Skeleton, { variant: "text", sx: { ...styles.dataSkeleton, width: '55%' } })))));
|
|
@@ -10,6 +10,8 @@ export type HistoryLogProps = {
|
|
|
10
10
|
loading?: boolean;
|
|
11
11
|
/** The title displayed above the timeline */
|
|
12
12
|
title?: string;
|
|
13
|
+
/** Whether to hide the timeline in the history log */
|
|
14
|
+
hideTimeline?: boolean;
|
|
13
15
|
};
|
|
14
16
|
/**
|
|
15
17
|
* Renders a timeline of the instance's history log.
|
|
@@ -20,7 +20,7 @@ const { format } = require('small-date');
|
|
|
20
20
|
* @returns {JSX.Element} A timeline view representing the instance's history.
|
|
21
21
|
*/
|
|
22
22
|
export const HistoryLog = (props) => {
|
|
23
|
-
const { object, history, loading, title } = props;
|
|
23
|
+
const { object, history, loading, title, hideTimeline } = props;
|
|
24
24
|
const [historyMap, setHistoryMap] = useState({});
|
|
25
25
|
const [documentHistory, setDocumentHistory] = useState({});
|
|
26
26
|
const [referencedObjects, setReferencedObjects] = useState([]);
|
|
@@ -91,7 +91,7 @@ export const HistoryLog = (props) => {
|
|
|
91
91
|
if (records.length) {
|
|
92
92
|
return (React.createElement(React.Fragment, { key: date },
|
|
93
93
|
React.createElement(Box, { sx: { display: 'flex', alignItems: 'center' } },
|
|
94
|
-
React.createElement(Circle, { color: "primary", sx: { fontSize: '12px', marginRight: '12px' } }),
|
|
94
|
+
!hideTimeline && (React.createElement(Circle, { color: "primary", sx: { fontSize: '12px', marginRight: '12px' } })),
|
|
95
95
|
React.createElement(Typography, { sx: { fontWeight: 600, fontSize: '16px' } },
|
|
96
96
|
format(new Date(), 'yyyy-MM-dd') === date
|
|
97
97
|
? 'Today'
|
|
@@ -99,14 +99,14 @@ export const HistoryLog = (props) => {
|
|
|
99
99
|
' ',
|
|
100
100
|
"\u00A0"),
|
|
101
101
|
React.createElement(Typography, { sx: { fontWeight: 600, fontSize: '16px', color: '#637381' } }, format(new Date(date + ' 00:00:000'), 'MMM dd, yyyy'))),
|
|
102
|
-
React.createElement(HistoricalData, { object: object, records: records, documentHistory: documentHistory, referencedObjects: referencedObjects })));
|
|
102
|
+
React.createElement(HistoricalData, { object: object, records: records, documentHistory: documentHistory, referencedObjects: referencedObjects, hideTimeline: hideTimeline })));
|
|
103
103
|
}
|
|
104
104
|
return null;
|
|
105
105
|
}),
|
|
106
106
|
!loading && filteredHistory && Object.values(filteredHistory).every((v) => !v.length) && (React.createElement(Box, { width: '100%', display: 'grid', justifyContent: 'center', marginTop: '60px' },
|
|
107
107
|
React.createElement(Typography, { fontSize: '20px', fontWeight: 700 }, "You Have No History"),
|
|
108
108
|
React.createElement(Typography, { fontSize: '14px', fontWeight: 400 }, "Try modifying the history type."))),
|
|
109
|
-
loading && React.createElement(HistoryLoading,
|
|
109
|
+
loading && React.createElement(HistoryLoading, { hideTimeline: hideTimeline }),
|
|
110
110
|
React.createElement(Snackbar, { open: showSnackbar, handleClose: () => setShowSnackbar(false), message: 'Error occurred when loading referenced objects', error: true })));
|
|
111
111
|
};
|
|
112
112
|
export default HistoryLog;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface ResponsiveOverflowProps {
|
|
3
|
+
/**
|
|
4
|
+
* Children elements to be displayed horizontally
|
|
5
|
+
*/
|
|
6
|
+
children: React.ReactNode[];
|
|
7
|
+
/**
|
|
8
|
+
* Text for the overflow button. Default is "More"
|
|
9
|
+
*/
|
|
10
|
+
moreButtonText?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Custom button element to replace the default "More" button
|
|
13
|
+
*/
|
|
14
|
+
customMoreButton?: React.ReactNode;
|
|
15
|
+
/**
|
|
16
|
+
* Maximum number of children to be displayed before overflowing
|
|
17
|
+
*/
|
|
18
|
+
maxVisible?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Function to render each overflow menu item. Receives the overflowed child and its index.
|
|
21
|
+
*/
|
|
22
|
+
renderOverflowMenuItem?: (item: React.ReactNode) => React.ReactNode;
|
|
23
|
+
/**
|
|
24
|
+
* If true, the container will take full width
|
|
25
|
+
*/
|
|
26
|
+
fullWidth?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* ResponsiveOverflow is a container component that displays children horizontally,
|
|
30
|
+
* automatically moving overflow items into a dropdown menu when there isn't enough space.
|
|
31
|
+
*/
|
|
32
|
+
export declare const ResponsiveOverflow: (props: ResponsiveOverflowProps) => React.JSX.Element;
|
|
33
|
+
export default ResponsiveOverflow;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { KeyboardArrowDownRounded } from '@mui/icons-material';
|
|
2
|
+
import { Box, Menu, MenuItem } from '@mui/material';
|
|
3
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { Button } from '../../core';
|
|
5
|
+
const styles = {
|
|
6
|
+
container: {
|
|
7
|
+
display: 'flex',
|
|
8
|
+
alignItems: 'center',
|
|
9
|
+
flex: '1 1 auto',
|
|
10
|
+
minWidth: 0,
|
|
11
|
+
overflow: 'hidden',
|
|
12
|
+
justifyContent: 'flex-end',
|
|
13
|
+
},
|
|
14
|
+
moreButton: {
|
|
15
|
+
color: '#637381',
|
|
16
|
+
fontSize: '14px',
|
|
17
|
+
fontWeight: 700,
|
|
18
|
+
minWidth: 'unset',
|
|
19
|
+
'& .MuiButton-endIcon': { margin: '0px' },
|
|
20
|
+
},
|
|
21
|
+
menuPaper: {
|
|
22
|
+
border: '0.5px solid #919EAB52',
|
|
23
|
+
borderRadius: '8px',
|
|
24
|
+
minWidth: '150px',
|
|
25
|
+
boxShadow: '0px 8px 16px 0px #0000001F',
|
|
26
|
+
},
|
|
27
|
+
horizontalItems: {
|
|
28
|
+
display: 'inline-block',
|
|
29
|
+
verticalAlign: 'top',
|
|
30
|
+
whiteSpace: 'nowrap',
|
|
31
|
+
},
|
|
32
|
+
fullWidthItems: {
|
|
33
|
+
padding: '0 4px',
|
|
34
|
+
flexGrow: 1,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* ResponsiveOverflow is a container component that displays children horizontally,
|
|
39
|
+
* automatically moving overflow items into a dropdown menu when there isn't enough space.
|
|
40
|
+
*/
|
|
41
|
+
export const ResponsiveOverflow = (props) => {
|
|
42
|
+
const { children, customMoreButton, maxVisible, renderOverflowMenuItem, moreButtonText, fullWidth } = props;
|
|
43
|
+
const containerRef = useRef(null);
|
|
44
|
+
const itemRefs = useRef([]);
|
|
45
|
+
const moreButtonBoxRef = useRef(null);
|
|
46
|
+
const [overflowItems, setOverflowItems] = useState([]);
|
|
47
|
+
const [anchorEl, setAnchorEl] = useState(null);
|
|
48
|
+
// Initialize refs array with correct length
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
itemRefs.current = itemRefs.current.slice(0, children.length);
|
|
51
|
+
handleResize();
|
|
52
|
+
}, [children]);
|
|
53
|
+
const handleResize = useCallback(() => {
|
|
54
|
+
if (!containerRef.current)
|
|
55
|
+
return;
|
|
56
|
+
// Reset all items to visible before measuring
|
|
57
|
+
itemRefs.current.forEach((item) => {
|
|
58
|
+
if (item) {
|
|
59
|
+
item.style.display = 'inline-block';
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
const availableWidth = containerRef.current.getBoundingClientRect().width;
|
|
63
|
+
const moreButtonWidth = moreButtonBoxRef.current?.getBoundingClientRect().width ?? 0;
|
|
64
|
+
let usedWidth = 0;
|
|
65
|
+
let maxFit = children.length;
|
|
66
|
+
// Determine which items fit, accounting for overflow button width
|
|
67
|
+
for (let i = 0; i < itemRefs.current.length; i++) {
|
|
68
|
+
const item = itemRefs.current[i];
|
|
69
|
+
if (!item) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const itemWidth = item.getBoundingClientRect().width;
|
|
73
|
+
if (usedWidth + itemWidth + moreButtonWidth > availableWidth) {
|
|
74
|
+
maxFit = i;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
usedWidth += itemWidth;
|
|
78
|
+
}
|
|
79
|
+
// If maxVisible is set, limit the number of visible items
|
|
80
|
+
if (typeof maxVisible === 'number') {
|
|
81
|
+
maxFit = Math.min(maxFit, maxVisible);
|
|
82
|
+
}
|
|
83
|
+
const newOverflow = [];
|
|
84
|
+
itemRefs.current.forEach((item, idx) => {
|
|
85
|
+
if (!item) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
else if (idx >= maxFit) {
|
|
89
|
+
item.style.display = 'none';
|
|
90
|
+
if (children[idx]) {
|
|
91
|
+
newOverflow.push(children[idx]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
item.style.display = 'inline-block';
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
setOverflowItems(newOverflow);
|
|
99
|
+
// Close menu if nothing is overflowing
|
|
100
|
+
if (newOverflow.length === 0) {
|
|
101
|
+
setAnchorEl(null);
|
|
102
|
+
}
|
|
103
|
+
}, [children, maxVisible]);
|
|
104
|
+
// Set up resize observer
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
107
|
+
requestAnimationFrame(handleResize);
|
|
108
|
+
});
|
|
109
|
+
if (containerRef.current) {
|
|
110
|
+
resizeObserver.observe(containerRef.current);
|
|
111
|
+
}
|
|
112
|
+
handleResize();
|
|
113
|
+
return () => {
|
|
114
|
+
if (containerRef.current) {
|
|
115
|
+
resizeObserver.unobserve(containerRef.current);
|
|
116
|
+
}
|
|
117
|
+
resizeObserver.disconnect();
|
|
118
|
+
};
|
|
119
|
+
}, [handleResize]);
|
|
120
|
+
const handleMenuOpen = (e) => setAnchorEl(e.currentTarget);
|
|
121
|
+
const handleMenuClose = () => setAnchorEl(null);
|
|
122
|
+
return (React.createElement(Box, { ref: containerRef, sx: styles.container },
|
|
123
|
+
children.map((item, index) => (React.createElement(Box, { ref: (el) => (itemRefs.current[index] = el), key: index, sx: { ...styles.horizontalItems, ...(fullWidth ? styles.fullWidthItems : {}) } }, item))),
|
|
124
|
+
!!overflowItems.length && (React.createElement(React.Fragment, null,
|
|
125
|
+
React.createElement(Box, { ref: moreButtonBoxRef, onClick: anchorEl ? handleMenuClose : handleMenuOpen }, customMoreButton ?? (React.createElement(Button, { variant: "text", sx: styles.moreButton, size: "small", endIcon: React.createElement(KeyboardArrowDownRounded, null) }, moreButtonText ?? 'More'))),
|
|
126
|
+
React.createElement(Menu, { anchorEl: anchorEl, open: Boolean(anchorEl), onClose: handleMenuClose, disablePortal: false, disableScrollLock: true, keepMounted: true, anchorOrigin: {
|
|
127
|
+
vertical: 'bottom',
|
|
128
|
+
horizontal: 'right',
|
|
129
|
+
}, transformOrigin: {
|
|
130
|
+
vertical: 'top',
|
|
131
|
+
horizontal: 'right',
|
|
132
|
+
}, sx: { '& .MuiPaper-root': styles.menuPaper }, onClick: handleMenuClose }, overflowItems.map((item, index) => {
|
|
133
|
+
if (renderOverflowMenuItem) {
|
|
134
|
+
return renderOverflowMenuItem(item);
|
|
135
|
+
}
|
|
136
|
+
return (React.createElement(MenuItem, { key: `overflow-item-${index}`, onClick: handleMenuClose }, item));
|
|
137
|
+
}))))));
|
|
138
|
+
};
|
|
139
|
+
export default ResponsiveOverflow;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { vi } from 'vitest';
|
|
6
|
+
import Button from '../../core/Button/Button';
|
|
7
|
+
import ResponsiveOverflow from './ResponsiveOverflow';
|
|
8
|
+
let mockResizeCallback = null;
|
|
9
|
+
const originalResizeObserver = global.ResizeObserver;
|
|
10
|
+
const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect;
|
|
11
|
+
describe('ResponsiveOverflow', () => {
|
|
12
|
+
// Helper function to simulate resize events
|
|
13
|
+
const simulateResize = (width) => {
|
|
14
|
+
Element.prototype.getBoundingClientRect = vi.fn(() => ({
|
|
15
|
+
width,
|
|
16
|
+
}));
|
|
17
|
+
if (mockResizeCallback) {
|
|
18
|
+
mockResizeCallback([], {});
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
mockResizeCallback = null;
|
|
23
|
+
global.ResizeObserver = class ResizeObserver {
|
|
24
|
+
constructor(callback) {
|
|
25
|
+
mockResizeCallback = callback;
|
|
26
|
+
}
|
|
27
|
+
observe() { }
|
|
28
|
+
unobserve() { }
|
|
29
|
+
disconnect() { }
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
global.ResizeObserver = originalResizeObserver;
|
|
34
|
+
Element.prototype.getBoundingClientRect = originalGetBoundingClientRect;
|
|
35
|
+
});
|
|
36
|
+
it('renders children correctly', () => {
|
|
37
|
+
render(React.createElement(ResponsiveOverflow, null,
|
|
38
|
+
React.createElement(Button, null, "Button 1"),
|
|
39
|
+
React.createElement(Button, null, "Button 2"),
|
|
40
|
+
React.createElement(Button, null, "Button 3")));
|
|
41
|
+
expect(screen.getByText('Button 1')).toBeInTheDocument();
|
|
42
|
+
expect(screen.getByText('Button 2')).toBeInTheDocument();
|
|
43
|
+
expect(screen.getByText('Button 3')).toBeInTheDocument();
|
|
44
|
+
});
|
|
45
|
+
it('displays custom button text when overflowing', async () => {
|
|
46
|
+
render(React.createElement(ResponsiveOverflow, { moreButtonText: "Show More" },
|
|
47
|
+
React.createElement(Button, null, "Button 1"),
|
|
48
|
+
React.createElement(Button, null, "Button 2"),
|
|
49
|
+
React.createElement(Button, null, "Button 3"),
|
|
50
|
+
React.createElement(Button, null, "Button 4")));
|
|
51
|
+
simulateResize(80); // Narrow width to force overflow
|
|
52
|
+
await screen.findByRole('button', { name: 'Show More' });
|
|
53
|
+
});
|
|
54
|
+
it('displays custom more button when overflowing', async () => {
|
|
55
|
+
const customButton = React.createElement(Button, { "data-testid": "custom-more" }, "Custom More");
|
|
56
|
+
render(React.createElement(ResponsiveOverflow, { customMoreButton: customButton },
|
|
57
|
+
React.createElement(Button, null, "Button 1"),
|
|
58
|
+
React.createElement(Button, null, "Button 2"),
|
|
59
|
+
React.createElement(Button, null, "Button 3"),
|
|
60
|
+
React.createElement(Button, null, "Button 4")));
|
|
61
|
+
simulateResize(80);
|
|
62
|
+
await screen.findByRole('button', { name: 'Custom More' });
|
|
63
|
+
});
|
|
64
|
+
it('shows More button when triggered by ResizeObserver', async () => {
|
|
65
|
+
render(React.createElement(ResponsiveOverflow, { moreButtonText: "More" },
|
|
66
|
+
React.createElement(Button, null, "Button 1"),
|
|
67
|
+
React.createElement(Button, null, "Button 2"),
|
|
68
|
+
React.createElement(Button, null, "Button 3"),
|
|
69
|
+
React.createElement(Button, null, "Button 4"),
|
|
70
|
+
React.createElement(Button, null, "Button 5"),
|
|
71
|
+
React.createElement(Button, null, "Button 6"),
|
|
72
|
+
React.createElement(Button, null, "Button 7"),
|
|
73
|
+
React.createElement(Button, null, "Button 8"),
|
|
74
|
+
React.createElement(Button, null, "Button 9"),
|
|
75
|
+
React.createElement(Button, null, "Button 10")));
|
|
76
|
+
simulateResize(80);
|
|
77
|
+
const moreBtn = await screen.findByRole('button', { name: 'More' });
|
|
78
|
+
await screen.findByRole('button', { name: 'Button 1' });
|
|
79
|
+
await userEvent.click(moreBtn);
|
|
80
|
+
// Button 9 should be in the overflow menu as a menuitem
|
|
81
|
+
await screen.findByRole('menuitem', { name: 'Button 9' });
|
|
82
|
+
});
|
|
83
|
+
it('handles dynamic resizing from wide to narrow conditions', async () => {
|
|
84
|
+
render(React.createElement(ResponsiveOverflow, { moreButtonText: "More" },
|
|
85
|
+
React.createElement(Button, null, "Button 1"),
|
|
86
|
+
React.createElement(Button, null, "Button 2"),
|
|
87
|
+
React.createElement(Button, null, "Button 3"),
|
|
88
|
+
React.createElement(Button, null, "Button 4"),
|
|
89
|
+
React.createElement(Button, null, "Button 5"),
|
|
90
|
+
React.createElement(Button, null, "Button 6")));
|
|
91
|
+
simulateResize(800);
|
|
92
|
+
// With wide container, all buttons should fit
|
|
93
|
+
// More button should not be visible
|
|
94
|
+
expect(screen.queryByRole('button', { name: 'More' })).not.toBeInTheDocument();
|
|
95
|
+
// Now simulate narrow conditions - force overflow
|
|
96
|
+
simulateResize(200);
|
|
97
|
+
// With narrow container, More button should now be visible
|
|
98
|
+
await screen.findByRole('button', { name: 'More' });
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -9,5 +9,6 @@ export { HistoryLog } from './HistoryLog';
|
|
|
9
9
|
export { MenuBar } from './Menubar';
|
|
10
10
|
export { MultiSelect } from './MultiSelect';
|
|
11
11
|
export { RepeatableField } from './RepeatableField';
|
|
12
|
+
export { ResponsiveOverflow } from './ResponsiveOverflow';
|
|
12
13
|
export { RichTextViewer } from './RichTextViewer';
|
|
13
14
|
export { UserAvatar } from './UserAvatar';
|
|
@@ -8,5 +8,6 @@ export { HistoryLog } from './HistoryLog';
|
|
|
8
8
|
export { MenuBar } from './Menubar';
|
|
9
9
|
export { MultiSelect } from './MultiSelect';
|
|
10
10
|
export { RepeatableField } from './RepeatableField';
|
|
11
|
+
export { ResponsiveOverflow } from './ResponsiveOverflow';
|
|
11
12
|
export { RichTextViewer } from './RichTextViewer';
|
|
12
13
|
export { UserAvatar } from './UserAvatar';
|
|
@@ -2,7 +2,7 @@ export { ClickAwayListener, createTheme, darken, lighten, styled, Toolbar } from
|
|
|
2
2
|
export { CalendarPicker, DateTimePicker, MonthPicker, PickersDay, StaticDateTimePicker, StaticTimePicker, TimePicker, YearPicker, } from '@mui/x-date-pickers';
|
|
3
3
|
export * from './colors';
|
|
4
4
|
export * from './components/core';
|
|
5
|
-
export { BuilderGrid, CriteriaBuilder, DataGrid, ErrorComponent, Form, FormField, getReadableQuery, HistoryLog, MenuBar, MultiSelect, RepeatableField, RichTextViewer, UserAvatar, } from './components/custom';
|
|
5
|
+
export { BuilderGrid, CriteriaBuilder, DataGrid, ErrorComponent, Form, FormField, getReadableQuery, HistoryLog, MenuBar, MultiSelect, RepeatableField, ResponsiveOverflow, RichTextViewer, UserAvatar, } from './components/custom';
|
|
6
6
|
export type { FormRef } from './components/custom';
|
|
7
7
|
export { NumericFormat } from './components/custom/FormField/InputFieldComponent';
|
|
8
8
|
export { Box, Container, Grid, Stack } from './components/layout';
|
package/dist/published/index.js
CHANGED
|
@@ -2,7 +2,7 @@ export { ClickAwayListener, createTheme, darken, lighten, styled, Toolbar } from
|
|
|
2
2
|
export { CalendarPicker, DateTimePicker, MonthPicker, PickersDay, StaticDateTimePicker, StaticTimePicker, TimePicker, YearPicker, } from '@mui/x-date-pickers';
|
|
3
3
|
export * from './colors';
|
|
4
4
|
export * from './components/core';
|
|
5
|
-
export { BuilderGrid, CriteriaBuilder, DataGrid, ErrorComponent, Form, FormField, getReadableQuery, HistoryLog, MenuBar, MultiSelect, RepeatableField, RichTextViewer, UserAvatar, } from './components/custom';
|
|
5
|
+
export { BuilderGrid, CriteriaBuilder, DataGrid, ErrorComponent, Form, FormField, getReadableQuery, HistoryLog, MenuBar, MultiSelect, RepeatableField, ResponsiveOverflow, RichTextViewer, UserAvatar, } from './components/custom';
|
|
6
6
|
export { NumericFormat } from './components/custom/FormField/InputFieldComponent';
|
|
7
7
|
export { Box, Container, Grid, Stack } from './components/layout';
|
|
8
8
|
export * from './theme';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Meta, Story } from '@storybook/react';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { ResponsiveOverflowProps } from '../components/custom/ResponsiveOverflow';
|
|
4
|
+
declare const _default: Meta<import("@storybook/react").Args>;
|
|
5
|
+
export default _default;
|
|
6
|
+
export declare const ResizableDemo: () => React.JSX.Element;
|
|
7
|
+
export declare const WithLinks: Story<ResponsiveOverflowProps>;
|
|
8
|
+
export declare const WithCustomMoreButton: Story<ResponsiveOverflowProps>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { KeyboardArrowDownRounded } from '@mui/icons-material';
|
|
2
|
+
import { Box, Paper, Typography } from '@mui/material';
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import Button from '../components/core/Button/Button';
|
|
5
|
+
import Link from '../components/core/Link/Link';
|
|
6
|
+
import ResponsiveOverflow from '../components/custom/ResponsiveOverflow';
|
|
7
|
+
export default {
|
|
8
|
+
title: 'Custom/ResponsiveOverflow',
|
|
9
|
+
component: ResponsiveOverflow,
|
|
10
|
+
parameters: {
|
|
11
|
+
componentSubtitle: 'A container that handles overflow with a dropdown menu',
|
|
12
|
+
},
|
|
13
|
+
argTypes: {
|
|
14
|
+
moreButtonText: {
|
|
15
|
+
control: 'text',
|
|
16
|
+
description: 'Text for the overflow button',
|
|
17
|
+
defaultValue: 'More',
|
|
18
|
+
},
|
|
19
|
+
customMoreButton: {
|
|
20
|
+
control: false,
|
|
21
|
+
description: 'Custom button element to replace the default More button',
|
|
22
|
+
},
|
|
23
|
+
children: {
|
|
24
|
+
control: false,
|
|
25
|
+
description: 'React elements to display in the container',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
const Template = (args) => React.createElement(ResponsiveOverflow, { ...args });
|
|
30
|
+
// Interactive demo with resizable container
|
|
31
|
+
export const ResizableDemo = () => {
|
|
32
|
+
const [width, setWidth] = useState(800);
|
|
33
|
+
const handleChange = (event) => {
|
|
34
|
+
setWidth(Number(event.target.value));
|
|
35
|
+
};
|
|
36
|
+
return (React.createElement(Box, null,
|
|
37
|
+
React.createElement(Typography, { variant: "h6", gutterBottom: true }, "Resize Container"),
|
|
38
|
+
React.createElement(Box, { sx: { mb: 2 } },
|
|
39
|
+
React.createElement(Typography, { variant: "body2", gutterBottom: true },
|
|
40
|
+
"Container Width: ",
|
|
41
|
+
width,
|
|
42
|
+
"px"),
|
|
43
|
+
React.createElement("input", { type: "range", min: 200, max: 1000, value: width, onChange: handleChange,
|
|
44
|
+
// eslint-disable-next-line no-inline-styles/no-inline-styles
|
|
45
|
+
style: { width: '100%' }, "aria-label": "Adjust container width", title: "Drag to resize container" })),
|
|
46
|
+
React.createElement(Paper, { elevation: 1, sx: {
|
|
47
|
+
width: `${width}px`,
|
|
48
|
+
padding: 2,
|
|
49
|
+
transition: 'width 0.3s ease',
|
|
50
|
+
backgroundColor: '#f5f5f5',
|
|
51
|
+
} },
|
|
52
|
+
React.createElement(ResponsiveOverflow, { moreButtonText: "More" },
|
|
53
|
+
React.createElement(Button, { variant: "contained", color: "primary" }, "Dashboard"),
|
|
54
|
+
React.createElement(Button, { variant: "contained" }, "Applications"),
|
|
55
|
+
React.createElement(Button, { variant: "contained" }, "Inspections"),
|
|
56
|
+
React.createElement(Button, { variant: "contained" }, "Reports"),
|
|
57
|
+
React.createElement(Button, { variant: "contained" }, "Licenses"),
|
|
58
|
+
React.createElement(Button, { variant: "contained" }, "Permits"),
|
|
59
|
+
React.createElement(Button, { variant: "contained" }, "Requests"),
|
|
60
|
+
React.createElement(Button, { variant: "contained" }, "Locations")))));
|
|
61
|
+
};
|
|
62
|
+
export const WithLinks = Template.bind({});
|
|
63
|
+
WithLinks.args = {
|
|
64
|
+
moreButtonText: 'More',
|
|
65
|
+
children: [
|
|
66
|
+
React.createElement(Link, { key: "1" }, "Applications"),
|
|
67
|
+
React.createElement(Link, { key: "2" }, "Inspections"),
|
|
68
|
+
React.createElement(Link, { key: "3" }, "Licenses"),
|
|
69
|
+
React.createElement(Link, { key: "4" }, "Permits"),
|
|
70
|
+
React.createElement(Link, { key: "5" }, "Requests"),
|
|
71
|
+
React.createElement(Link, { key: "6" }, "Compliance"),
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
export const WithCustomMoreButton = Template.bind({});
|
|
75
|
+
WithCustomMoreButton.args = {
|
|
76
|
+
customMoreButton: (React.createElement(Button, { variant: "contained", endIcon: React.createElement(KeyboardArrowDownRounded, null) }, "More Options")),
|
|
77
|
+
children: [
|
|
78
|
+
React.createElement(Button, { variant: "contained", color: "primary", key: "1" }, "Dashboard"),
|
|
79
|
+
React.createElement(Button, { variant: "contained", key: "2" }, "Applications"),
|
|
80
|
+
React.createElement(Button, { variant: "contained", key: "3" }, "Inspections"),
|
|
81
|
+
React.createElement(Button, { variant: "contained", key: "4" }, "Reports"),
|
|
82
|
+
React.createElement(Button, { variant: "contained", key: "5" }, "Licenses"),
|
|
83
|
+
React.createElement(Button, { variant: "contained", key: "6" }, "Permits"),
|
|
84
|
+
React.createElement(Button, { variant: "contained", key: "7" }, "Requests"),
|
|
85
|
+
React.createElement(Button, { variant: "contained", key: "8" }, "Documents"),
|
|
86
|
+
React.createElement(Button, { variant: "contained", key: "9" }, "Locations"),
|
|
87
|
+
React.createElement(Button, { variant: "contained", key: "10" }, "Calendar"),
|
|
88
|
+
React.createElement(Button, { variant: "contained", key: "11" }, "Compliance"),
|
|
89
|
+
React.createElement(Button, { variant: "contained", key: "12" }, "Notifications"),
|
|
90
|
+
],
|
|
91
|
+
};
|