@judo/components 0.1.1-alpha.3

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.
@@ -0,0 +1,324 @@
1
+ import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button } from '@mui/material';
2
+ import {
3
+ GridSortModel,
4
+ GridRowModel,
5
+ DataGrid,
6
+ GridRowParams,
7
+ GridColumns,
8
+ GridSortItem,
9
+ GridSelectionModel,
10
+ GridToolbarContainer,
11
+ GridRowId,
12
+ } from '@mui/x-data-grid';
13
+ import { useSnackbar } from 'notistack';
14
+ import { useEffect, useRef, useState } from 'react';
15
+ import type { JudoStored, QueryCustomizer } from '@judo/data-api-common';
16
+ import type { Filter, FilterOption } from '@judo/components-api';
17
+ import { errorHandling } from '@judo/utilities';
18
+ import { CustomTablePagination } from '../CustomTablePagination';
19
+ import { useFilterDialog } from './DialogContext';
20
+
21
+ interface RangeDialogProps<T extends JudoStored<T>, U extends QueryCustomizer<T>> {
22
+ resolve: (value: any) => void;
23
+ open: boolean;
24
+ handleClose: () => void;
25
+ single?: boolean;
26
+ columns: GridColumns<T>;
27
+ defaultSortField: GridSortItem;
28
+ rangeCall: (queryCustomizer: U) => Promise<Array<T>>;
29
+ alreadySelectedItems: GridSelectionModel | GridRowId;
30
+ initalQueryCustomizer: U;
31
+ filterOptions: FilterOption[];
32
+ }
33
+
34
+ export const RangeDialog = <T extends JudoStored<T>, U extends QueryCustomizer<T>>({
35
+ resolve,
36
+ open,
37
+ handleClose,
38
+ single = false,
39
+ columns,
40
+ defaultSortField,
41
+ rangeCall,
42
+ alreadySelectedItems,
43
+ initalQueryCustomizer,
44
+ filterOptions,
45
+ }: RangeDialogProps<T, U>) => {
46
+ const { openFilterDialog } = useFilterDialog();
47
+ const { enqueueSnackbar } = useSnackbar();
48
+
49
+ const descriptionElementRef = useRef<HTMLElement>(null);
50
+ const [isLoading, setIsLoading] = useState<boolean>(false);
51
+ const [rowCount, setRowCount] = useState<number>(0);
52
+ const [sortModel, setSortModel] = useState<GridSortModel>([defaultSortField]);
53
+ const [lastItem, setLastItem] = useState<T>();
54
+ const [firstItem, setFirstItem] = useState<T>();
55
+ const [isNextButtonEnabled, setIsNextButtonEnabled] = useState<boolean>(true);
56
+ const [page, setPage] = useState<number>(0);
57
+ const [data, setData] = useState<GridRowModel<T>[]>([]);
58
+ const [selectionModel, setSelectionModel] = useState<GridSelectionModel | GridRowId | undefined>(
59
+ alreadySelectedItems ?? (single ? undefined : []),
60
+ );
61
+ const [selectedItems, setSelectedItems] = useState<T[] | T | undefined>([]);
62
+ const [filters, setFilters] = useState<Filter[]>([]);
63
+ const [queryCustomizer, setQueryCustomizer] = useState<U>({
64
+ ...initalQueryCustomizer,
65
+ _seek: {
66
+ limit: 6,
67
+ },
68
+ });
69
+
70
+ const handlePageChange = async (isNext: boolean) => {
71
+ setQueryCustomizer((prevQueryCustomizer) => {
72
+ return {
73
+ ...prevQueryCustomizer,
74
+ _seek: {
75
+ limit: isNext ? 6 : 5,
76
+ reverse: !isNext,
77
+ lastItem: isNext ? lastItem : firstItem,
78
+ },
79
+ };
80
+ });
81
+
82
+ setIsNextButtonEnabled(!isNext);
83
+ };
84
+
85
+ const fetchData = async () => {
86
+ setIsLoading(true);
87
+ try {
88
+ const res = await rangeCall(queryCustomizer);
89
+
90
+ if (res.length > 5) {
91
+ setIsNextButtonEnabled(true);
92
+ res.pop();
93
+ } else if (queryCustomizer._seek?.limit === 6) {
94
+ setIsNextButtonEnabled(false);
95
+ }
96
+
97
+ setData(res);
98
+ setFirstItem(res[0]);
99
+ setLastItem(res[res.length - 1]);
100
+ setRowCount(res.length || 0);
101
+ } catch (error) {
102
+ errorHandling(error, enqueueSnackbar);
103
+ }
104
+ setIsLoading(false);
105
+ };
106
+
107
+ const handleFiltersChange = (newFilters: Filter[]) => {
108
+ setPage(0);
109
+ setFilters(newFilters);
110
+
111
+ // @ts-ignore
112
+ setQueryCustomizer((prevQueryCustomizer) => {
113
+ const tempQueryCustomizer = { ...prevQueryCustomizer };
114
+
115
+ filterOptions.forEach(
116
+ (filter) =>
117
+ // @ts-ignore
118
+ (tempQueryCustomizer[filter.attributeName] = mapFiltersToQueryCustomizerProperty(
119
+ newFilters,
120
+ filter.attributeName,
121
+ )),
122
+ );
123
+
124
+ return {
125
+ ...prevQueryCustomizer,
126
+ _seek: {
127
+ lastItem: undefined,
128
+ limit: 6,
129
+ reverse: undefined,
130
+ },
131
+ ...tempQueryCustomizer,
132
+ };
133
+ });
134
+ };
135
+
136
+ const handleSortModelChange = (newModel: GridSortModel) => {
137
+ setPage(0);
138
+ setSortModel(newModel);
139
+
140
+ const { field, sort } = newModel[0];
141
+
142
+ setQueryCustomizer((prevQueryCustomizer) => {
143
+ return {
144
+ ...prevQueryCustomizer,
145
+ _orderBy: [{ attribute: field, descending: sort === 'desc' }],
146
+ };
147
+ });
148
+ };
149
+
150
+ // useEffect(() => {
151
+ // setPage(0);
152
+ // const { field, sort } = sortModel[0];
153
+
154
+ // setQueryCustomizer((prevQueryCustomizer) => {
155
+ // const tempQueryCustomizer = { ...prevQueryCustomizer };
156
+
157
+ // filterOptions.forEach(
158
+ // (filter) =>
159
+ // // @ts-ignore
160
+ // (tempQueryCustomizer[filter.attributeName] = mapFiltersToQueryCustomizerProperty(
161
+ // filters,
162
+ // filter.attributeName,
163
+ // )),
164
+ // );
165
+
166
+ // return {
167
+ // ...prevQueryCustomizer,
168
+ // _seek: {
169
+ // lastItem: undefined,
170
+ // limit: 6,
171
+ // reverse: undefined,
172
+ // },
173
+ // _orderBy: [{ attribute: field, descending: sort === 'desc' }],
174
+ // ...tempQueryCustomizer,
175
+ // };
176
+ // });
177
+ // }, [sortModel, filters]);
178
+
179
+ useEffect(() => {
180
+ fetchData();
181
+ }, [queryCustomizer]);
182
+
183
+ useEffect(() => {
184
+ if (open) {
185
+ const { current: descriptionElement } = descriptionElementRef;
186
+ if (descriptionElement !== null) {
187
+ descriptionElement.focus();
188
+ }
189
+ }
190
+ }, [open]);
191
+
192
+ const cancel = () => {
193
+ handleClose();
194
+ resolve(undefined);
195
+ };
196
+
197
+ const ok = () => {
198
+ handleClose();
199
+ resolve(selectedItems);
200
+ };
201
+
202
+ const handleOnSelection = (newSelectionModel: GridSelectionModel) => {
203
+ if (!Array.isArray(selectionModel)) return;
204
+
205
+ // added new items
206
+ if (newSelectionModel.length > selectionModel.length) {
207
+ const diff = newSelectionModel.length - selectionModel.length;
208
+ const newItemsId = [...newSelectionModel].slice(diff * -1);
209
+ const newItems = data.filter((value) => newItemsId.indexOf(value.__identifier as GridRowId) !== -1);
210
+ setSelectedItems((prevSelectedItems) => {
211
+ if (!Array.isArray(prevSelectedItems)) return;
212
+
213
+ return [...prevSelectedItems, ...newItems];
214
+ });
215
+ }
216
+
217
+ // removed items
218
+ if (newSelectionModel.length < selectionModel.length) {
219
+ const removedItemsId = selectionModel.filter((value) => newSelectionModel.indexOf(value) === -1);
220
+ setSelectedItems((prevSelectedItems) => {
221
+ if (!Array.isArray(prevSelectedItems)) return;
222
+
223
+ return [...prevSelectedItems.filter((value) => removedItemsId.indexOf(value.__identifier as GridRowId) === -1)];
224
+ });
225
+ }
226
+
227
+ setSelectionModel(newSelectionModel);
228
+ };
229
+
230
+ const handleSingleOnSelection = (newSelectionModel: GridSelectionModel) => {
231
+ if (Array.isArray(selectionModel)) return;
232
+
233
+ if (newSelectionModel.length === 0) {
234
+ setSelectionModel('');
235
+ setSelectedItems(undefined);
236
+ return;
237
+ }
238
+
239
+ const lastId = newSelectionModel[newSelectionModel.length - 1];
240
+
241
+ setSelectionModel(lastId);
242
+ setSelectedItems(data.find((value) => value.__identifier === lastId));
243
+ };
244
+
245
+ const handleIsRowSelectable = (params: GridRowParams<T>) => {
246
+ if (alreadySelectedItems) {
247
+ if (!Array.isArray(alreadySelectedItems)) throw Error('Range dialog gets wrong alreadySelectedItems.');
248
+
249
+ return !alreadySelectedItems.includes(params.id);
250
+ }
251
+
252
+ return true;
253
+ };
254
+
255
+ return (
256
+ <Dialog open={open} onClose={cancel} scroll="paper" fullWidth={true} maxWidth={'sm'}>
257
+ <DialogTitle id="scroll-dialog-title">Select</DialogTitle>
258
+ <DialogContent dividers={true}>
259
+ <DialogContentText id="scroll-dialog-description" ref={descriptionElementRef} tabIndex={-1}>
260
+ <DataGrid
261
+ sx={
262
+ single
263
+ ? {
264
+ '.MuiDataGrid-columnHeaderCheckbox .MuiDataGrid-columnHeaderTitleContainer': {
265
+ display: 'none',
266
+ },
267
+ }
268
+ : undefined
269
+ }
270
+ autoHeight
271
+ getRowId={(row: T) => row.__identifier as GridRowId}
272
+ loading={isLoading}
273
+ paginationMode="server"
274
+ rows={data}
275
+ rowCount={rowCount}
276
+ sortingOrder={['desc', 'asc']}
277
+ sortingMode="server"
278
+ sortModel={sortModel}
279
+ onSortModelChange={handleSortModelChange}
280
+ checkboxSelection
281
+ onSelectionModelChange={!single ? handleOnSelection : handleSingleOnSelection}
282
+ isRowSelectable={!single ? handleIsRowSelectable : undefined}
283
+ selectionModel={selectionModel}
284
+ hideFooterSelectedRowCount={single}
285
+ columns={columns}
286
+ keepNonExistentRowsSelected
287
+ components={{
288
+ Toolbar: () => (
289
+ <GridToolbarContainer>
290
+ <Button
291
+ variant="outlined"
292
+ onClick={async () => {
293
+ const newFilters = await openFilterDialog(filterOptions, filters);
294
+
295
+ if (newFilters) {
296
+ handleFiltersChange(newFilters);
297
+ }
298
+ }}
299
+ disabled={isLoading}
300
+ >
301
+ Set filters {filters.length !== 0 ? '(' + filters.length + ')' : ''}
302
+ </Button>
303
+ </GridToolbarContainer>
304
+ ),
305
+ Pagination: () => (
306
+ <CustomTablePagination
307
+ pageChange={handlePageChange}
308
+ isNextButtonEnabled={isNextButtonEnabled}
309
+ page={page}
310
+ setPage={setPage}
311
+ rowPerPage={5}
312
+ />
313
+ ),
314
+ }}
315
+ />
316
+ </DialogContentText>
317
+ </DialogContent>
318
+ <DialogActions>
319
+ <Button onClick={cancel}>Cancel</Button>
320
+ <Button onClick={ok}>Ok</Button>
321
+ </DialogActions>
322
+ </Dialog>
323
+ );
324
+ };
@@ -0,0 +1,5 @@
1
+ export * from './ConfirmationDialog';
2
+ export * from './DialogContext';
3
+ export * from './FilterDialog';
4
+ export * from './PageDialog';
5
+ export * from './RangeDialog';
package/src/index.tsx ADDED
@@ -0,0 +1,9 @@
1
+ export * from './dialog';
2
+ export * from './table';
3
+ export * from './AggregationInput';
4
+ export * from './CustomBreadcrumb';
5
+ export * from './CustomLink';
6
+ export * from './CustomTablePagination';
7
+ export * from './DropdownButton';
8
+ export * from './PageHeader';
9
+ export * from './TrinaryLogicCombobox';
@@ -0,0 +1 @@
1
+ export * from './table-row-actions';
@@ -0,0 +1,86 @@
1
+ import type { ColumnActionsProvider, ColumnsActionsOptions, TableRowAction } from '@judo/utilities';
2
+ import { exists } from '@judo/utilities';
3
+ import { MoreHoriz } from '@mui/icons-material';
4
+ import { Button } from '@mui/material';
5
+ import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
6
+ import { DropdownButton } from '../DropdownButton';
7
+
8
+ export const columnsActionCalculator: ColumnActionsProvider<any> = (
9
+ actions: TableRowAction<any>[],
10
+ options?: ColumnsActionsOptions,
11
+ ): GridColDef[] => {
12
+ if (!exists(actions) || actions.length === 0) {
13
+ return [];
14
+ }
15
+
16
+ let shownActionsNumber = 1;
17
+ if (options?.shownActions) {
18
+ shownActionsNumber = actions.length < options.shownActions ? actions.length : options.shownActions;
19
+ }
20
+
21
+ if (shownActionsNumber < 0) {
22
+ return standaloneActions(actions, options);
23
+ } else if (shownActionsNumber === 0) {
24
+ return [];
25
+ } else if (shownActionsNumber === 1) {
26
+ return dropdownActions(actions, options);
27
+ } else {
28
+ const sliceNumber = actions.length === shownActionsNumber ? shownActionsNumber : shownActionsNumber - 1;
29
+ const standaloneRowActions = actions.slice(0, sliceNumber);
30
+ const dropdownRowActions = actions.slice(sliceNumber);
31
+
32
+ return [...standaloneActions(standaloneRowActions, options), ...dropdownActions(dropdownRowActions, options)];
33
+ }
34
+ };
35
+
36
+ const standaloneActions: ColumnActionsProvider<unknown> = (
37
+ actions: TableRowAction<unknown>[],
38
+ options?: ColumnsActionsOptions,
39
+ ): GridColDef[] => {
40
+ return actions.map((action, index) => {
41
+ return {
42
+ field: action.label + index,
43
+ headerName: '',
44
+ align: 'center',
45
+ type: 'actions',
46
+ renderCell: (params: GridRenderCellParams) => {
47
+ return (
48
+ <Button variant="text" onClick={() => action.action(params.row)}>
49
+ {action.icon}
50
+ {(options?.showLabel ?? true) && action.label}
51
+ </Button>
52
+ );
53
+ },
54
+ };
55
+ });
56
+ };
57
+
58
+ const dropdownActions: ColumnActionsProvider<unknown> = (actions: TableRowAction<unknown>[]): GridColDef[] => {
59
+ if (actions.length === 0) return [];
60
+
61
+ return [
62
+ {
63
+ field: 'actions',
64
+ headerName: '',
65
+ align: 'center',
66
+ type: 'actions',
67
+ renderCell: (params: GridRenderCellParams) => {
68
+ return (
69
+ <DropdownButton
70
+ variant="text"
71
+ showDropdownIcon={false}
72
+ menuItems={actions.map((action) => {
73
+ return {
74
+ label: action.label,
75
+ startIcon: action.icon,
76
+ onClick: () => action.action(params.row),
77
+ };
78
+ })}
79
+ >
80
+ <MoreHoriz />
81
+ </DropdownButton>
82
+ );
83
+ },
84
+ },
85
+ ];
86
+ };