drf-react-by-schema 0.0.1 → 0.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.
@@ -0,0 +1,1401 @@
1
+ import React, { useEffect, useState, useReducer, useRef } from 'react';
2
+ import moment from 'moment';
3
+ // import Mask from 'string-mask';
4
+ import {
5
+ GridApi,
6
+ DataGrid,
7
+ GridEditInputCell,
8
+ GridRowModes,
9
+ GridToolbarContainer,
10
+ GridToolbarColumnsButton,
11
+ GridToolbarFilterButton,
12
+ GridToolbarDensitySelector,
13
+ GridToolbarExport,
14
+ GridToolbarQuickFilter,
15
+ GridFooter,
16
+ GridFooterContainer,
17
+ GridActionsCellItem,
18
+ gridVisibleSortedRowIdsSelector,
19
+ useGridApiContext,
20
+ gridStringOrNumberComparator,
21
+ getGridNumericOperators,
22
+ getGridDateOperators,
23
+ GridFilterOperator
24
+ } from '@mui/x-data-grid';
25
+ import * as Yup from 'yup';
26
+ import { Link } from 'react-router-dom';
27
+ import Box from '@mui/material/Box';
28
+ import CircularProgress from '@mui/material/CircularProgress';
29
+ import TextField from '@mui/material/TextField';
30
+ import Button from '@mui/material/Button';
31
+ import Menu from '@mui/material/Menu';
32
+ import MenuItem from '@mui/material/MenuItem';
33
+ import SyncIcon from '@mui/icons-material/Sync';
34
+ import ExpandIcon from '@mui/icons-material/Expand';
35
+ import AddIcon from '@mui/icons-material/Add';
36
+ import EditIcon from '@mui/icons-material/Edit';
37
+ import ClearIcon from '@mui/icons-material/Clear';
38
+ import CheckIcon from '@mui/icons-material/Check';
39
+ import UndoIcon from '@mui/icons-material/Undo';
40
+ import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete';
41
+ import { NumericFormat, PatternFormat } from 'react-number-format';
42
+ import Dialog from '@mui/material/Dialog';
43
+ import DialogActions from '@mui/material/DialogActions';
44
+ import DialogContent from '@mui/material/DialogContent';
45
+ import DialogTitle from '@mui/material/DialogTitle';
46
+ import Snackbar from '@mui/material/Snackbar';
47
+ import Alert from '@mui/material/Alert';
48
+
49
+ import { Layout } from '../styles';
50
+ import { getAutoComplete } from '../api';
51
+ import {
52
+ Item,
53
+ Schema,
54
+ Choice,
55
+ Id,
56
+ GridEnrichedBySchemaColDef,
57
+ reducer,
58
+ getTmpId,
59
+ isTmpId,
60
+ emptyByType,
61
+ buildGenericYupValidationSchema
62
+ } from '../utils';
63
+ import { SxProps } from '@mui/material';
64
+ import { ObjectShape } from 'yup/lib/object';
65
+
66
+ const filter = createFilterOptions();
67
+ const stringMask = require('string-mask');
68
+
69
+ // returns width of the biggest row inside a column
70
+ function maxOfCol (colIndex: number) {
71
+ const invisibleContainer = document.createElement('div');
72
+
73
+ invisibleContainer.style.visibility = 'hidden';
74
+ invisibleContainer.style.zIndex = '-9999999999';
75
+ invisibleContainer.style.position = 'absolute';
76
+ invisibleContainer.style.fontSize = '14px';
77
+ invisibleContainer.style.top = '0';
78
+ invisibleContainer.style.left = '0';
79
+ document.body.append(invisibleContainer);
80
+ const widths: any[] = [];
81
+ document.querySelectorAll(
82
+ `[aria-colindex='${colIndex}']`
83
+ ).forEach(cell => {
84
+ const invisibleCell = document.createElement('div');
85
+ invisibleCell.innerHTML = cell.innerHTML;
86
+ invisibleCell.style.width = 'max-content';
87
+ invisibleCell.style.maxWidth = 'none';
88
+ invisibleCell.style.minWidth = 'none';
89
+ invisibleContainer.append(invisibleCell);
90
+ widths.push(Math.ceil(invisibleCell.clientWidth));
91
+ });
92
+ let max = Math.max(...widths);
93
+ if (max !== 0 && max < 50) {
94
+ max = 50;
95
+ }
96
+ invisibleContainer.remove();
97
+ return max;
98
+ }
99
+
100
+ type ResizeType = 'condense' | 'maxContent' | 'fitScreen';
101
+
102
+ function resizeColumns (
103
+ columns: GridEnrichedBySchemaColDef[],
104
+ resizeType: ResizeType,
105
+ apiRef: React.MutableRefObject<GridApi>
106
+ ) {
107
+ const cols = [...columns];
108
+ cols.forEach((col, index: number) => {
109
+ if (resizeType === 'fitScreen') {
110
+ delete col.width;
111
+ col.minWidth = 80;
112
+ if (col.isIndexField) {
113
+ col.flex = 1;
114
+ }
115
+ } else if (resizeType === 'maxContent') {
116
+ const maxColWidth = maxOfCol(index);
117
+ delete col.flex;
118
+ delete col.minWidth;
119
+ col.width = maxColWidth + 22;
120
+ } else {
121
+ col.width = 0;
122
+ }
123
+ });
124
+ return cols;
125
+ }
126
+
127
+ type CustomToolbarProps = {
128
+ preparedColumns: GridEnrichedBySchemaColDef[],
129
+ setPreparedColumns: (p:null | GridEnrichedBySchemaColDef[]) => void
130
+ };
131
+ const CustomToolbar = ({
132
+ preparedColumns,
133
+ setPreparedColumns
134
+ }:CustomToolbarProps) => {
135
+ const apiRef = useGridApiContext();
136
+ const [resizeMenuAnchorEl, setResizeMenuAnchorEl] = useState<any>(null);
137
+ const isResizeMenuOpen = Boolean(resizeMenuAnchorEl);
138
+ const openResizeMenu = (event:React.MouseEvent) => {
139
+ setResizeMenuAnchorEl(event.currentTarget);
140
+ };
141
+ const closeResizeMenu = () => {
142
+ setResizeMenuAnchorEl(null);
143
+ };
144
+ return (
145
+ <GridToolbarContainer sx={{ justifyContent: 'space-between' }}>
146
+ <div style={{ display: 'flex', flexWrap: 'wrap' }}>
147
+ <GridToolbarColumnsButton sx={{ ml: '10px', fontSize: '13px' }} />
148
+ <GridToolbarFilterButton sx={{ ml: '10px', fontSize: '13px' }} />
149
+ <GridToolbarDensitySelector sx={{ ml: '10px', fontSize: '13px' }} />
150
+ <Button onClick={openResizeMenu} sx={{ ml: '0px', fontSize: '13px' }}>
151
+ <ExpandIcon sx={{ transform: 'rotate(90deg)', mr: '6px' }} />
152
+ Ajustar
153
+ </Button>
154
+ <Menu
155
+ anchorEl={resizeMenuAnchorEl}
156
+ open={isResizeMenuOpen}
157
+ onClose={closeResizeMenu}
158
+ >
159
+ <MenuItem
160
+ onClick={() => {
161
+ closeResizeMenu();
162
+ setPreparedColumns(resizeColumns(
163
+ preparedColumns,
164
+ 'fitScreen',
165
+ apiRef
166
+ ));
167
+ }}
168
+ >
169
+ Ajustar à tela
170
+ </MenuItem>
171
+ <MenuItem
172
+ onClick={() => {
173
+ closeResizeMenu();
174
+ setPreparedColumns(resizeColumns(
175
+ preparedColumns,
176
+ 'maxContent',
177
+ apiRef
178
+ ));
179
+ }}
180
+ >
181
+ Ajustar ao conteúdo
182
+ </MenuItem>
183
+ </Menu>
184
+ <GridToolbarExport sx={{ ml: '0px', fontSize: '13px' }} />
185
+ </div>
186
+ <div>
187
+ <GridToolbarQuickFilter />
188
+ </div>
189
+ </GridToolbarContainer>
190
+ );
191
+ };
192
+
193
+ type SelectEditInputCellProps = {
194
+ field: string,
195
+ id: number | string,
196
+ value?: any,
197
+ column: GridEnrichedBySchemaColDef,
198
+ type: string,
199
+ optionsAC: { current: Record<string, Item[]> | null },
200
+ isIndexField: boolean,
201
+ multiple?: boolean,
202
+ sx?: SxProps
203
+ };
204
+ function SelectEditInputCell ({
205
+ id,
206
+ value,
207
+ field,
208
+ column,
209
+ type,
210
+ optionsAC,
211
+ isIndexField,
212
+ multiple = false,
213
+ sx = {}
214
+ }: SelectEditInputCellProps) {
215
+ // TODO: allow edit option label, as in formautocomplete!
216
+ const apiRef = useGridApiContext();
217
+
218
+ const handleChange = async (newValue:any) => {
219
+ await apiRef.current.setEditCellValue({ id, field, value: newValue });
220
+ apiRef.current.stopCellEditMode({ id, field });
221
+ };
222
+
223
+ const labelKey = (['field', 'nested object'].includes(type) || isIndexField)
224
+ ? 'label'
225
+ : 'display_name';
226
+ const valueKey = (['field', 'nested object'].includes(type) || isIndexField)
227
+ ? 'id'
228
+ : 'value';
229
+
230
+ let creatableProps = {};
231
+ if (column.creatable || isIndexField) {
232
+ creatableProps = {
233
+ freesolo: 'true',
234
+ filterOptions: (options:Record<string, any>[], params:any) => {
235
+ const filtered = filter(options, params);
236
+ const inputValue = (params.inputValue)
237
+ ? params.inputValue
238
+ : '';
239
+ const inputValueLower = inputValue.trim().toLowerCase();
240
+ // Suggest the creation of a new value
241
+ const isExisting = options.some(option => inputValueLower === option[labelKey].trim().toLowerCase());
242
+ if (inputValue !== '' && !isExisting) {
243
+ filtered.push({
244
+ inputValue,
245
+ [labelKey]: `Criar "${inputValue}"`
246
+ });
247
+ }
248
+ return filtered;
249
+ },
250
+ handleHomeEndKeys: true,
251
+ getOptionLabel: (option:string | Record<string, any>) => {
252
+ // Value selected with enter, right from the input
253
+ if (typeof option === 'string') {
254
+ return option;
255
+ }
256
+ // Criar "xxx" option created dynamically
257
+ if (option.inputValue) {
258
+ return option.inputValue;
259
+ }
260
+ // Regular option
261
+ return option[labelKey];
262
+ },
263
+ renderOption: (props:any, option:Record<string, any>) => {
264
+ return (<li key={option[valueKey]} {...props}>{option[labelKey]}</li>);
265
+ }
266
+ };
267
+ };
268
+
269
+ return (
270
+ <Autocomplete
271
+ key={field}
272
+ id={field}
273
+ value={value}
274
+ options={optionsAC.current && optionsAC.current[field] ? optionsAC.current[field] : []}
275
+ selectOnFocus
276
+ autoHighlight
277
+ multiple={multiple}
278
+ isOptionEqualToValue={(option, value) => {
279
+ return (option[labelKey] === value[labelKey]);
280
+ }}
281
+ getOptionLabel={(option) => {
282
+ return option[labelKey];
283
+ }}
284
+ onChange={(e, value) => {
285
+ if (!column.creatable && !isIndexField) {
286
+ handleChange(value);
287
+ return;
288
+ }
289
+ let newValue = value;
290
+ if (typeof newValue === 'string') {
291
+ const tmpId = getTmpId();
292
+ newValue = {
293
+ [valueKey]: tmpId,
294
+ [labelKey]: newValue
295
+ };
296
+ }
297
+ if (newValue && newValue.inputValue) {
298
+ const tmpId = getTmpId();
299
+ newValue = {
300
+ [valueKey]: tmpId,
301
+ [labelKey]: newValue.inputValue
302
+ };
303
+ }
304
+ handleChange(newValue);
305
+ }}
306
+ fullWidth
307
+ renderInput={params => (
308
+ <TextField
309
+ {...params}
310
+ sx={sx}
311
+ />
312
+ )}
313
+
314
+ {...creatableProps}
315
+ />
316
+ );
317
+ }
318
+
319
+ type GridDecimalInputProps = {
320
+ field: string,
321
+ id: number | string,
322
+ value?: any,
323
+ column: object
324
+ };
325
+ function GridDecimalInput ({
326
+ id,
327
+ value,
328
+ field
329
+ }:GridDecimalInputProps) {
330
+ const apiRef = useGridApiContext();
331
+ const decimalScale = 2;
332
+ const disableCurrency = true;
333
+ const handleChange = async (newValue:any) => {
334
+ await apiRef.current.setEditCellValue({ id, field, value: newValue });
335
+ apiRef.current.stopCellEditMode({ id, field });
336
+ };
337
+ return (
338
+ <NumericFormat
339
+ key={field}
340
+ id={field}
341
+ onValueChange={(values, sourceInfo) => {
342
+ handleChange(values.value);
343
+ }}
344
+ value={value}
345
+ thousandSeparator='.'
346
+ decimalSeparator=','
347
+ decimalScale={decimalScale}
348
+ fixedDecimalScale={true}
349
+ valueIsNumericString
350
+ prefix={disableCurrency ? '' : 'R$ '}
351
+ customInput={TextField}
352
+ />
353
+ );
354
+ }
355
+
356
+ type GridPatternInputProps = {
357
+ field: string,
358
+ id: number | string,
359
+ value?: any,
360
+ patternFormat?: string
361
+ };
362
+ function GridPatternInput ({
363
+ id,
364
+ value,
365
+ field,
366
+ patternFormat = 'cpf'
367
+ }:GridPatternInputProps) {
368
+ const apiRef = useGridApiContext();
369
+ const handleChange = async (newValue:any) => {
370
+ await apiRef.current.setEditCellValue({ id, field, value: newValue });
371
+ apiRef.current.stopCellEditMode({ id, field });
372
+ };
373
+ return (
374
+ <PatternFormat
375
+ key={field}
376
+ id={field}
377
+ onValueChange={(values, sourceInfo) => {
378
+ handleChange(values.value);
379
+ }}
380
+ value={value}
381
+ valueIsNumericString
382
+ format={patternFormat}
383
+ mask="_"
384
+ customInput={TextField}
385
+ />
386
+ );
387
+ }
388
+
389
+ type FooterToolbarProps = {
390
+ name: string,
391
+ setRowModesModel: (p:any) => any,
392
+ dataGrid: { data:Record<string, any>[] },
393
+ setDataGrid: (p:any) => any,
394
+ emptyItem: { current:Record<string, any> },
395
+ indexField: string,
396
+ isEditable: boolean
397
+ };
398
+ function FooterToolbar ({
399
+ name,
400
+ setRowModesModel,
401
+ dataGrid,
402
+ setDataGrid,
403
+ emptyItem,
404
+ indexField,
405
+ isEditable
406
+ }:FooterToolbarProps) {
407
+ const handleClick = () => {
408
+ const id = getTmpId();
409
+ emptyItem.current.id = id;
410
+ const newData = [
411
+ { ...emptyItem.current },
412
+ ...dataGrid.data
413
+ ];
414
+ setDataGrid({
415
+ data: newData
416
+ });
417
+ setRowModesModel((oldModel:any) => ({
418
+ ...oldModel,
419
+ [id]: { mode: GridRowModes.Edit, fieldToFocus: indexField }
420
+ }));
421
+ // Ugly hack to scroll to top, since scroll to cell is only available in Pro
422
+ const el = document.querySelector(`.dataGrid_${name} .MuiDataGrid-virtualScroller`);
423
+ // console.log(el, name);
424
+ if (el) {
425
+ el.scrollTop = 0;
426
+ setTimeout(() => {
427
+ el.scrollTop = 0;
428
+ }, 10);
429
+ }
430
+ };
431
+
432
+ return (
433
+ <GridFooterContainer>
434
+ {isEditable &&
435
+ <Button
436
+ color="primary"
437
+ startIcon={<AddIcon />}
438
+ onClick={handleClick}
439
+ sx={{ ml: 2 }}
440
+ >
441
+ Adicionar
442
+ </Button>
443
+ }
444
+ <GridFooter
445
+ sx={
446
+ (isEditable)
447
+ ? { border: 'none' }
448
+ : { width: '100%' }
449
+ }
450
+ />
451
+ </GridFooterContainer>
452
+ );
453
+ }
454
+
455
+
456
+ // FILTROS "ENTRE":
457
+
458
+ const SUBMIT_FILTER_STROKE_TIME = 500;
459
+ type InputIntervalProps = {
460
+ applyValue: (p: any) => void,
461
+ focusElementRef?: null | ((p: any) => void) | {
462
+ current: any
463
+ },
464
+ item: {
465
+ columnField: string,
466
+ id: number | string,
467
+ operatorValue: string,
468
+ value: any
469
+ },
470
+ type: string
471
+ };
472
+ function InputInterval ({
473
+ item,
474
+ applyValue,
475
+ focusElementRef = null,
476
+ type
477
+ }:InputIntervalProps) {
478
+ const filterTimeout = useRef<any>();
479
+ const [filterValueState, setFilterValueState] = React.useState(item.value ?? '');
480
+
481
+ const [applying, setIsApplying] = React.useState(false);
482
+
483
+ React.useEffect(() => {
484
+ return () => {
485
+ clearTimeout(filterTimeout.current);
486
+ };
487
+ }, []);
488
+
489
+ React.useEffect(() => {
490
+ const itemValue = item.value ?? [undefined, undefined];
491
+ setFilterValueState(itemValue);
492
+ }, [item.value]);
493
+
494
+ const updateFilterValue = (lowerBound:any, upperBound:any) => {
495
+ clearTimeout(filterTimeout.current);
496
+ setFilterValueState([lowerBound, upperBound]);
497
+
498
+ setIsApplying(true);
499
+ filterTimeout.current = setTimeout(() => {
500
+ setIsApplying(false);
501
+ applyValue({ ...item, value: [lowerBound, upperBound] });
502
+ }, SUBMIT_FILTER_STROKE_TIME);
503
+ };
504
+
505
+ const handleUpperFilterChange = (event:React.ChangeEvent<HTMLInputElement> | { target: { value: string }}) => {
506
+ const newUpperBound = event.target?.value;
507
+ updateFilterValue(filterValueState[0], newUpperBound);
508
+ };
509
+ const handleLowerFilterChange = (event:React.ChangeEvent<HTMLInputElement> | { target: { value: string }}) => {
510
+ const newLowerBound = event.target?.value;
511
+ updateFilterValue(newLowerBound, filterValueState[1]);
512
+ };
513
+
514
+ return (
515
+ <Box
516
+ sx={{
517
+ display: 'inline-flex',
518
+ flexDirection: 'row',
519
+ alignItems: 'end',
520
+ height: 48,
521
+ pl: '20px'
522
+ }}
523
+ >
524
+ {type === 'number' &&
525
+ <>
526
+ <TextField
527
+ name="lower-bound-input"
528
+ placeholder="De"
529
+ label="De"
530
+ variant="standard"
531
+ value={Number(filterValueState[0])}
532
+ onChange={handleLowerFilterChange}
533
+ type="number"
534
+ inputRef={focusElementRef}
535
+ sx={{ mr: 2, minWidth: 130 }}
536
+ />
537
+ <TextField
538
+ name="upper-bound-input"
539
+ placeholder="Até"
540
+ label="Até"
541
+ variant="standard"
542
+ value={Number(filterValueState[1])}
543
+ onChange={handleUpperFilterChange}
544
+ type="number"
545
+ sx={{ minWidth: 130 }}
546
+ InputProps={applying ? { endAdornment: <SyncIcon /> } : {}}
547
+ />
548
+ </>
549
+ }
550
+ {type === 'float' &&
551
+ <>
552
+ <NumericFormat
553
+ name="lower-bound-input"
554
+ placeholder="De"
555
+ label="De"
556
+ variant="standard"
557
+ value={Number(filterValueState[0])}
558
+ onValueChange={(values, sourceInfo) => {
559
+ handleLowerFilterChange({ target: { value: values.value } });
560
+ }}
561
+ thousandSeparator='.'
562
+ decimalSeparator=','
563
+ decimalScale={2}
564
+ fixedDecimalScale={true}
565
+ valueIsNumericString
566
+ inputRef={focusElementRef}
567
+ sx={{ mr: 2, minWidth: 130 }}
568
+ customInput={TextField}
569
+ />
570
+ <NumericFormat
571
+ name="upper-bound-input"
572
+ placeholder="Até"
573
+ label="Até"
574
+ variant="standard"
575
+ value={Number(filterValueState[1])}
576
+ onValueChange={(values, sourceInfo) => {
577
+ handleUpperFilterChange({ target: { value: values.value } });
578
+ }}
579
+ thousandSeparator='.'
580
+ decimalSeparator=','
581
+ decimalScale={2}
582
+ fixedDecimalScale={true}
583
+ valueIsNumericString
584
+ InputProps={applying ? { endAdornment: <SyncIcon /> } : {}}
585
+ sx={{ minWidth: 130 }}
586
+ customInput={TextField}
587
+ />
588
+ </>
589
+ }
590
+ {type === 'date' &&
591
+ <>
592
+ <TextField
593
+ name="lower-bound-input"
594
+ label="De"
595
+ variant="standard"
596
+ value={filterValueState[0] || ''}
597
+ onChange={handleLowerFilterChange}
598
+ type="date"
599
+ inputRef={focusElementRef}
600
+ InputLabelProps={{ shrink: true }}
601
+ sx={{ mr: 2, minWidth: 130 }}
602
+ />
603
+ <TextField
604
+ name="upper-bound-input"
605
+ label="Até"
606
+ variant="standard"
607
+ value={filterValueState[1] || ''}
608
+ onChange={handleUpperFilterChange}
609
+ type="date"
610
+ InputProps={applying ? { endAdornment: <SyncIcon /> } : {}}
611
+ InputLabelProps={{ shrink: true }}
612
+ sx={{ minWidth: 130 }}
613
+ />
614
+ </>
615
+ }
616
+ </Box>
617
+ );
618
+ }
619
+
620
+ function InputNumberInterval (props: any) {
621
+ return (
622
+ <InputInterval
623
+ {...props}
624
+ type="number"
625
+ />
626
+ );
627
+ }
628
+ function InputDateInterval (props: any) {
629
+ return (
630
+ <InputInterval
631
+ {...props}
632
+ type="date"
633
+ />
634
+ );
635
+ }
636
+ function InputFloatInterval (props: any) {
637
+ return (
638
+ <InputInterval
639
+ {...props}
640
+ type="float"
641
+ />
642
+ );
643
+ }
644
+
645
+ const quantityOnlyOperators = ({ type }:{ type: string }) => {
646
+ const builtInFilters = (type === 'date')
647
+ ? getGridDateOperators()
648
+ : getGridNumericOperators();
649
+ let InputComponent = InputNumberInterval;
650
+ if (type === 'date') {
651
+ InputComponent = InputDateInterval;
652
+ }
653
+ if (type === 'float') {
654
+ InputComponent = InputFloatInterval;
655
+ }
656
+ return [
657
+ ...builtInFilters,
658
+ {
659
+ label: 'entre',
660
+ value: 'entre',
661
+ getApplyFilterFn: (filterItem: any) => {
662
+ if (!Array.isArray(filterItem.value) || filterItem.value.length !== 2) {
663
+ return null;
664
+ }
665
+ if (filterItem.value[0] === null || filterItem.value[1] === null) {
666
+ return null;
667
+ }
668
+
669
+ return ({ value }: { value: any }) => {
670
+ return (
671
+ value !== null &&
672
+ filterItem.value[0] <= value &&
673
+ value <= filterItem.value[1]
674
+ );
675
+ };
676
+ },
677
+ InputComponent
678
+ }
679
+ ] as GridFilterOperator[];
680
+ };
681
+
682
+ type FConfirmDialogProps = {
683
+ open: boolean,
684
+ onClose: (p: any) => void,
685
+ onConfirm: (p: any) => void
686
+ };
687
+ const FConfirmDialog = ({
688
+ open,
689
+ onClose,
690
+ onConfirm
691
+ }: FConfirmDialogProps) => {
692
+ return (
693
+ <Dialog open={open} onClose={onClose}>
694
+ <DialogTitle>
695
+ Confirmar
696
+ </DialogTitle>
697
+ <DialogContent>
698
+ Tem certeza de que você quer remover este item?
699
+ </DialogContent>
700
+ <DialogActions>
701
+ <Button
702
+ onClick={onClose}
703
+ >
704
+ Cancelar
705
+ </Button>
706
+ <Button
707
+ onClick={onConfirm}
708
+ >
709
+ Remover
710
+ </Button>
711
+ </DialogActions>
712
+ </Dialog>
713
+ );
714
+ }
715
+ const ConfirmDialog = React.memo(FConfirmDialog);
716
+
717
+ type DataGridBySchemaEditableProps = {
718
+ schema: Schema,
719
+ data: Item[],
720
+ columns: GridEnrichedBySchemaColDef[],
721
+ model: string,
722
+ fieldKey?: string,
723
+ labelKey?: string,
724
+ index?: number,
725
+ name?: string,
726
+ updateUrl: string,
727
+ indexField?: string,
728
+ addExistingModel?: string,
729
+ indexFieldMinWidth?: number,
730
+ indexFieldBasePath?: string,
731
+ stateToLink?: object,
732
+ minWidth?: number,
733
+ onEditRelatedModelSave?: (p: any) => Id | { data: Item } | { errors: Item },
734
+ onDeleteRelatedModel?: (p: any) => any,
735
+ customColumnOperations?: (p: any) => GridEnrichedBySchemaColDef,
736
+ customLinkDestination?: (p: any) => string,
737
+ onProcessRow?: (p: any) => void,
738
+ onDataChange?: (p: any) => void,
739
+ isEditable?: boolean,
740
+ sx?: SxProps,
741
+ isAutoHeight?: boolean,
742
+ defaultValues?: Item,
743
+ hideFooterPagination?: boolean,
744
+ setVisibleRows?: (p: any) => void
745
+ };
746
+ const DataGridBySchemaEditable = React.forwardRef((
747
+ {
748
+ schema,
749
+ data,
750
+ columns,
751
+ model,
752
+ fieldKey,
753
+ labelKey = 'nome',
754
+ index,
755
+ name = Math.floor(Math.random() * 1000000).toString(),
756
+ updateUrl,
757
+ indexField = 'nome',
758
+ addExistingModel,
759
+ indexFieldMinWidth = 350,
760
+ indexFieldBasePath = '',
761
+ stateToLink = {},
762
+ minWidth = 80,
763
+ onEditRelatedModelSave,
764
+ onDeleteRelatedModel,
765
+ customColumnOperations,
766
+ customLinkDestination,
767
+ onProcessRow,
768
+ onDataChange,
769
+ isEditable = false,
770
+ sx = { mr: 2 },
771
+ isAutoHeight = false,
772
+ defaultValues = {},
773
+ hideFooterPagination = false,
774
+ setVisibleRows,
775
+ ...other
776
+ }: DataGridBySchemaEditableProps,
777
+ ref
778
+ ) => {
779
+ const initialSnackBar = {
780
+ open: false,
781
+ msg: '',
782
+ severity: 'info'
783
+ };
784
+ const [snackBar, setSnackBar] = useReducer(reducer, initialSnackBar);
785
+ const [dataGrid, setDataGrid] = useState<{ data: Item[] }>({ data: [] });
786
+ const [preparedColumns, setPreparedColumns] = useState<null | GridEnrichedBySchemaColDef[]>(null);
787
+ const [dataGridLoading, setDataGridLoading] = useState(false);
788
+ const [rowModesModel, setRowModesModel] = useState<Record<string, any>>({});
789
+ const [dialogOpen, setDialogOpen] = useState(false);
790
+ const optionsAC = useRef<Record<string, Item[]> | null>(null);
791
+ const emptyItem = useRef<Item>({});
792
+ const yupValidationSchema = useRef<Yup.AnySchema | null>(null);
793
+ const processingRow = useRef<number | string | null>(null);
794
+ const idToRemove = useRef<Id | null>(null);
795
+
796
+ const updateOptionsAC = async () => {
797
+ optionsAC.current = {};
798
+ for (const col of columns) {
799
+ if (!schema[col.field]) {
800
+ continue;
801
+ }
802
+ if (['field', 'nested object'].includes(schema[col.field].type) && !(col.field in optionsAC.current)) {
803
+ const options = await getAutoComplete(col.field);
804
+ optionsAC.current[col.field] = options;
805
+ continue;
806
+ }
807
+ if (schema[col.field].type === 'choice' && !(col.field in optionsAC.current)) {
808
+ optionsAC.current[col.field] = schema[col.field].choices as Choice[];
809
+ continue;
810
+ }
811
+ if (col.field === indexField && addExistingModel && !optionsAC.current[col.field]) {
812
+ const options = await getAutoComplete(addExistingModel);
813
+ optionsAC.current[col.field] = options;
814
+ }
815
+ }
816
+ };
817
+
818
+ const initColumns = () => {
819
+ let cols:GridEnrichedBySchemaColDef[] = [];
820
+ if (isEditable) {
821
+ cols.push({
822
+ field: 'actions',
823
+ headerName: '',
824
+ type: 'actions',
825
+ getActions: ({ id }: { id: Id }) => {
826
+ const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit;
827
+ if (isInEditMode) {
828
+ return [
829
+ <GridActionsCellItem
830
+ key={`save_${id}`}
831
+ icon={<CheckIcon />}
832
+ label="Salvar"
833
+ onClick={handleSaveClick(id)}
834
+ color="success"
835
+ />,
836
+ <GridActionsCellItem
837
+ key={`cancel_${id}`}
838
+ icon={<UndoIcon />}
839
+ label="Cancelar"
840
+ onClick={handleCancelClick(id)}
841
+ color="inherit"
842
+ />
843
+ ];
844
+ }
845
+
846
+ return [
847
+ <GridActionsCellItem
848
+ key={`edit_${id}`}
849
+ icon={<EditIcon />}
850
+ label="Edit"
851
+ onClick={handleEditClick(id)}
852
+ color="inherit"
853
+ />,
854
+ <GridActionsCellItem
855
+ key={`remove_${id}`}
856
+ icon={<ClearIcon />}
857
+ label="Delete"
858
+ onClick={handleDeleteClick(id)}
859
+ color="error"
860
+ />
861
+ ];
862
+ }
863
+ });
864
+ }
865
+ cols = [...cols, ...columns];
866
+ cols = cols.map(col => {
867
+ if (!schema[col.field]) {
868
+ return col;
869
+ }
870
+ const isIndexField = (indexField !== '' && (col.field === indexField));
871
+ let column = col;
872
+ if (isIndexField) {
873
+ col.isIndexField = true;
874
+ }
875
+ column.editable = (isEditable) && (
876
+ !schema[col.field].read_only || ['field', 'nested object'].includes(schema[col.field].type)
877
+ );
878
+ switch (schema[col.field].type) {
879
+ case 'date':
880
+ column.type = 'date';
881
+ column.valueFormatter = params => params.value
882
+ ? moment(params.value).format('DD/MM/YYYY')
883
+ : '';
884
+ column.filterOperators = quantityOnlyOperators({ type: 'date' });
885
+ break;
886
+ case 'datetime':
887
+ column.type = 'dateTime';
888
+ column.minWidth = 180;
889
+ column.valueFormatter = params => params.value
890
+ ? moment(params.value).format('DD/MM/YYYY HH:mm')
891
+ : '';
892
+ break;
893
+ case 'nested object':
894
+ column.minWidth = 150;
895
+ if (isEditable) {
896
+ column.valueFormatter = params => {
897
+ return (!params.value) ? '' : params.value.label;
898
+ };
899
+ column.filterable = false;
900
+ column.sortComparator = (v1, v2, param1, param2) => {
901
+ return gridStringOrNumberComparator(v1.label, v2.label, param1, param2);
902
+ };
903
+ column.renderEditCell = (params) => <SelectEditInputCell
904
+ {...params}
905
+ column={column}
906
+ type={schema[col.field].type}
907
+ optionsAC={optionsAC}
908
+ isIndexField={isIndexField}
909
+ />;
910
+ break;
911
+ }
912
+
913
+ column.valueGetter = params => {
914
+ return (!params.value) ? '' : params.value.label;
915
+ };
916
+ break;
917
+ case 'field':
918
+ column.orderable = false;
919
+
920
+ if (isEditable) {
921
+ column.minWidth = 300;
922
+ column.valueFormatter = params => {
923
+ return (!params.value || !Array.isArray(params.value))
924
+ ? ''
925
+ : params.value.map(val => val.label).join(', ');
926
+ };
927
+ column.filterable = false;
928
+ column.renderEditCell = (params) => <SelectEditInputCell
929
+ {...params}
930
+ column={column}
931
+ type={schema[col.field].type}
932
+ optionsAC={optionsAC}
933
+ isIndexField={isIndexField}
934
+ multiple={true}
935
+ />;
936
+ break;
937
+ }
938
+
939
+ column.valueGetter = params => {
940
+ return (!params.value || !Array.isArray(params.value))
941
+ ? ''
942
+ : params.value.map(val => val.label).join(', ');
943
+ };
944
+ break;
945
+ case 'choice':
946
+ if (isEditable) {
947
+ column.minWidth = 150;
948
+ column.valueFormatter = params => {
949
+ return (!params.value) ? '' : params.value.display_name;
950
+ };
951
+ column.filterable = false;
952
+ column.sortComparator = (v1, v2, param1, param2) => {
953
+ return gridStringOrNumberComparator(v1.display_name, v2.display_name, param1, param2);
954
+ };
955
+ column.renderEditCell = (params) => <SelectEditInputCell
956
+ {...params}
957
+ column={column}
958
+ type={schema[col.field].type}
959
+ optionsAC={optionsAC}
960
+ isIndexField={isIndexField}
961
+ />;
962
+ break;
963
+ }
964
+
965
+ column.valueGetter = params => {
966
+ return (!params.value) ? '' : params.value.display_name;
967
+ };
968
+ break;
969
+ case 'decimal':
970
+ case 'float':
971
+ column.type = 'number';
972
+ column.valueFormatter = params => {
973
+ return (!params.value)
974
+ ? ''
975
+ : parseFloat(params.value).toLocaleString('pt-BR', {
976
+ minimumFractionDigits: 2,
977
+ maximumFractionDigits: 2
978
+ });
979
+ };
980
+ if (isEditable) {
981
+ column.renderEditCell = (params) => <GridDecimalInput
982
+ {...params}
983
+ column={column}
984
+ />;
985
+ }
986
+ column.filterOperators = quantityOnlyOperators({ type: 'float' });
987
+ break;
988
+ case 'number':
989
+ case 'integer':
990
+ column.type = 'number';
991
+ column.filterOperators = quantityOnlyOperators({ type: 'number' });
992
+ break;
993
+ }
994
+
995
+ if (indexFieldMinWidth && !column.minWidth) {
996
+ column.minWidth = (col.field === indexField) ? indexFieldMinWidth : minWidth;
997
+ }
998
+ if (indexField) {
999
+ if (col.field === indexField) {
1000
+ column.flex = 1;
1001
+ column.renderCell = params => {
1002
+ if (customLinkDestination) {
1003
+ return (
1004
+ <Link to={`${customLinkDestination(params)}`} state={stateToLink}>
1005
+ {params.formattedValue}
1006
+ </Link>
1007
+ );
1008
+ }
1009
+
1010
+ if (
1011
+ !indexFieldBasePath ||
1012
+ (
1013
+ ['field', 'nested object'].includes(schema[col.field].type) &&
1014
+ (!params.row[col.field] || !params.row[col.field].id)
1015
+ )
1016
+ ) {
1017
+ return (<>{params.formattedValue}</>);
1018
+ }
1019
+
1020
+ const targetId = (['field', 'nested object'].includes(schema[col.field].type))
1021
+ ? params.row[col.field].id.toString()
1022
+ : params.row.id.toString();
1023
+
1024
+ return (
1025
+ <Link to={`${indexFieldBasePath}${targetId}`} state={stateToLink}>
1026
+ {params.formattedValue}
1027
+ </Link>
1028
+ );
1029
+ };
1030
+ if (
1031
+ isEditable &&
1032
+ optionsAC.current &&
1033
+ col.field in optionsAC.current &&
1034
+ addExistingModel
1035
+ ) {
1036
+ column.renderEditCell = (params) => {
1037
+ if (!isTmpId(params.id)) {
1038
+ return (
1039
+ <GridEditInputCell
1040
+ {...params}
1041
+ />
1042
+ );
1043
+ }
1044
+ return (
1045
+ <SelectEditInputCell
1046
+ {...params}
1047
+ column={column}
1048
+ type={schema[col.field].type}
1049
+ optionsAC={optionsAC}
1050
+ isIndexField={true}
1051
+ />
1052
+ );
1053
+ };
1054
+ }
1055
+ }
1056
+ }
1057
+
1058
+ // Custom column operations:
1059
+ if (customColumnOperations) {
1060
+ column = customColumnOperations(column);
1061
+ }
1062
+
1063
+ // format numbers:
1064
+ if (column.patternFormat) {
1065
+ column.valueFormatter = params => {
1066
+ if (!params.value || typeof params.value !== 'string') {
1067
+ return params.value;
1068
+ }
1069
+ const formattedValue = new stringMask.Mask(column.patternFormat, {}).apply(params.value);
1070
+ return formattedValue;
1071
+ };
1072
+ if (isEditable) {
1073
+ column.renderEditCell = params => <GridPatternInput
1074
+ {...params}
1075
+ patternFormat={column.patternFormat}
1076
+ />;
1077
+ }
1078
+ }
1079
+
1080
+ // Finally!
1081
+ return column;
1082
+ });
1083
+ setPreparedColumns(cols);
1084
+ };
1085
+
1086
+ const handleDialogClose = () => {
1087
+ setDialogOpen(false);
1088
+ };
1089
+
1090
+ const handleRowEditStart = (params: any, event: any) => {
1091
+ event.defaultMuiPrevented = true;
1092
+ };
1093
+
1094
+ const handleRowEditStop = (params: any, event: any) => {
1095
+ event.defaultMuiPrevented = true;
1096
+ };
1097
+
1098
+ const handleEditClick = (id: Id) => () => {
1099
+ setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } });
1100
+ };
1101
+
1102
+ const handleSaveClick = (id: Id) => () => {
1103
+ setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } });
1104
+ };
1105
+
1106
+ const handleDeleteClick = (id: Id) => () => {
1107
+ idToRemove.current = id;
1108
+ setDialogOpen(true);
1109
+ };
1110
+
1111
+ const handleDeleteSave = () => {
1112
+ const newData = dataGrid.data.filter(row => row.id !== idToRemove.current);
1113
+ setDataGrid({
1114
+ ...dataGrid,
1115
+ data: newData
1116
+ });
1117
+ if (onDeleteRelatedModel) {
1118
+ onDeleteRelatedModel({
1119
+ relatedModel: model,
1120
+ relatedModelId: idToRemove.current
1121
+ });
1122
+ }
1123
+
1124
+ // Reflect changes to the outside, for example for doing global calculations over multiple rows:
1125
+ if (onDataChange) {
1126
+ onDataChange(newData);
1127
+ }
1128
+ idToRemove.current = null;
1129
+ setDialogOpen(false);
1130
+ };
1131
+
1132
+ const handleCancelClick = (id: Id) => () => {
1133
+ setRowModesModel({
1134
+ ...rowModesModel,
1135
+ [id]: { mode: GridRowModes.View, ignoreModifications: true }
1136
+ });
1137
+
1138
+ const editedRow = dataGrid.data.find(row => row.id === id);
1139
+ if (editedRow && editedRow.isNew) {
1140
+ setDataGrid({
1141
+ ...dataGrid,
1142
+ data: dataGrid.data.filter(row => row.id !== id)
1143
+ });
1144
+ }
1145
+ };
1146
+
1147
+ useEffect(() => {
1148
+ if (!columns) {
1149
+ setDataGrid({ data: [] });
1150
+ return;
1151
+ }
1152
+
1153
+ if (isEditable) {
1154
+ for (const col of columns) {
1155
+ if (schema[col.field].model_default) {
1156
+ emptyItem.current[col.field] = schema[col.field].model_default;
1157
+ continue;
1158
+ }
1159
+ if (Object.prototype.hasOwnProperty.call(defaultValues, col.field)) {
1160
+ emptyItem.current[col.field] = defaultValues[col.field];
1161
+ continue;
1162
+ }
1163
+ emptyItem.current[col.field] = (schema[col.field])
1164
+ ? emptyByType(schema[col.field])
1165
+ : null;
1166
+ }
1167
+ }
1168
+
1169
+ const skipFields = [];
1170
+ if (indexField && addExistingModel) {
1171
+ skipFields.push(indexField);
1172
+ }
1173
+ yupValidationSchema.current = buildGenericYupValidationSchema({
1174
+ data: emptyItem.current,
1175
+ schema,
1176
+ skipFields
1177
+ });
1178
+
1179
+ setDataGrid({
1180
+ data
1181
+ });
1182
+ }, [data]);
1183
+
1184
+ useEffect(() => {
1185
+ if (optionsAC.current) {
1186
+ initColumns();
1187
+ return;
1188
+ }
1189
+
1190
+ updateOptionsAC().then(() => {
1191
+ initColumns();
1192
+ });
1193
+ }, [rowModesModel]);
1194
+
1195
+ const processRowUpdate = async (newRow: Item) => {
1196
+ if (!preparedColumns || !yupValidationSchema.current) {
1197
+ return false;
1198
+ }
1199
+ setDataGridLoading(true);
1200
+ const indexCol = preparedColumns.find(col => col.field === indexField);
1201
+ processingRow.current = newRow.id;
1202
+ await yupValidationSchema.current.validate(newRow);
1203
+ const onlyAddExisting = (
1204
+ indexField &&
1205
+ isTmpId(newRow.id) &&
1206
+ (newRow[indexField] && !isTmpId(newRow[indexField].id)) &&
1207
+ (!indexCol || !indexCol.valueFormatter)
1208
+ );
1209
+ const createNewItem = (indexField && isTmpId(newRow.id) && newRow[indexField] && isTmpId(newRow[indexField].id));
1210
+ if (onlyAddExisting) {
1211
+ const row:Item = {};
1212
+ row.id_to_add = newRow[indexField].id;
1213
+ for (const [key, value] of Object.entries(newRow[indexField])) {
1214
+ if (key === 'id') {
1215
+ row[key] = newRow.id;
1216
+ continue;
1217
+ }
1218
+ row[key] = value;
1219
+ }
1220
+ newRow = { ...row };
1221
+ }
1222
+ if (createNewItem && newRow[indexField]) {
1223
+ if (newRow[indexField].inputValue) {
1224
+ newRow[indexField] = newRow[indexField].inputValue;
1225
+ }
1226
+ if (addExistingModel) {
1227
+ newRow[indexField] = newRow[indexField].label;
1228
+ }
1229
+ }
1230
+
1231
+ if (onEditRelatedModelSave) {
1232
+ const response = await onEditRelatedModelSave({
1233
+ relatedModel: model,
1234
+ relatedModelId: newRow.id,
1235
+ newRow,
1236
+ schema,
1237
+ onlyAddExisting
1238
+ });
1239
+
1240
+ const responseAsId = response as 'number';
1241
+ const responseAsData = response as { data: Item };
1242
+ const responseAsErrors = response as { errors: Item }
1243
+
1244
+ if (
1245
+ (onlyAddExisting && !responseAsErrors.errors) ||
1246
+ (parseInt(responseAsId) && (createNewItem || !onlyAddExisting))
1247
+ ) {
1248
+ updateOptionsAC();
1249
+ const data = [...dataGrid.data];
1250
+ for (const i in data) {
1251
+ if (data[i].id === newRow.id) {
1252
+ data[i] = onlyAddExisting ? responseAsData.data : newRow;
1253
+ if (isTmpId(newRow.id)) {
1254
+ const newId = parseInt(responseAsId);
1255
+ data[i].id = newId || responseAsData.data.id;
1256
+ }
1257
+
1258
+ // This is for cases where users want to do custom operations after saving the row,
1259
+ // like for example make calculations among columns.
1260
+ if (onProcessRow) {
1261
+ onProcessRow(data[i]);
1262
+ }
1263
+
1264
+ // Reflect the changes to the outside, for example for global calculations over all data
1265
+ if (onDataChange) {
1266
+ onDataChange(data);
1267
+ }
1268
+
1269
+ setDataGrid({ data });
1270
+ break;
1271
+ }
1272
+ }
1273
+ setDataGridLoading(false);
1274
+ return newRow;
1275
+ }
1276
+ }
1277
+ setDataGridLoading(false);
1278
+ return false;
1279
+ };
1280
+
1281
+ const customPaddings = (isAutoHeight)
1282
+ ? {
1283
+ '&.MuiDataGrid-root--densityCompact .MuiDataGrid-cell': { py: '8px' },
1284
+ '&.MuiDataGrid-root--densityStandard .MuiDataGrid-cell': { py: '15px' },
1285
+ '&.MuiDataGrid-root--densityComfortable .MuiDataGrid-cell': { py: '22px' }
1286
+ }
1287
+ : undefined;
1288
+
1289
+ return (
1290
+ <Box className={`dataGrid_${name}`} sx={{ height: '100%' }}>
1291
+ {preparedColumns === null
1292
+ ? <Box sx={Layout.loadingBox}>
1293
+ <CircularProgress />
1294
+ </Box>
1295
+ : <DataGrid
1296
+ rows={dataGrid.data}
1297
+ columns={preparedColumns}
1298
+ onStateChange={(state) => {
1299
+ if (setVisibleRows) {
1300
+ const newRows = gridVisibleSortedRowIdsSelector(state);
1301
+ setVisibleRows(newRows);
1302
+ }
1303
+ }}
1304
+ editMode="row"
1305
+ loading={dataGridLoading}
1306
+ hideFooterPagination={hideFooterPagination}
1307
+ getRowHeight={() => {
1308
+ if (isAutoHeight) {
1309
+ return 'auto';
1310
+ }
1311
+ return null;
1312
+ }}
1313
+ isCellEditable={params => (
1314
+ rowModesModel[params.row.id as string]?.mode === GridRowModes.Edit &&
1315
+ (
1316
+ !isTmpId(params.row.id) ||
1317
+ (
1318
+ isTmpId(params.row.id) &&
1319
+ (
1320
+ params.field === indexField ||
1321
+ !optionsAC.current ||
1322
+ !Object.prototype.hasOwnProperty.call(optionsAC.current, indexField) ||
1323
+ (
1324
+ preparedColumns.find(col => col.field === indexField) &&
1325
+ Object.prototype.hasOwnProperty.call(preparedColumns.find(col => col.field === indexField), 'valueFormatter')
1326
+ )
1327
+ )
1328
+ )
1329
+ )
1330
+ ) as boolean}
1331
+ rowModesModel={rowModesModel}
1332
+ onRowEditStart={handleRowEditStart}
1333
+ onRowEditStop={handleRowEditStop}
1334
+ processRowUpdate={processRowUpdate}
1335
+ onProcessRowUpdateError={(e) => {
1336
+ setDataGridLoading(false);
1337
+ if (processingRow.current) {
1338
+ setRowModesModel({
1339
+ ...rowModesModel,
1340
+ [processingRow.current]: { mode: GridRowModes.Edit }
1341
+ });
1342
+ }
1343
+ const msg = `Erro em "${e.path}": ${e.errors}`;
1344
+ setSnackBar({
1345
+ open: true,
1346
+ severity: 'error',
1347
+ msg
1348
+ });
1349
+ console.log(e);
1350
+ }}
1351
+ components={{ Toolbar: CustomToolbar, Footer: FooterToolbar }}
1352
+ componentsProps={
1353
+ {
1354
+ toolbar: {
1355
+ preparedColumns,
1356
+ setPreparedColumns,
1357
+ showQuickFilter: true,
1358
+ quickFilterProps: { debounceMs: 500 }
1359
+ },
1360
+ footer: {
1361
+ name,
1362
+ setRowModesModel,
1363
+ dataGrid,
1364
+ setDataGrid,
1365
+ emptyItem,
1366
+ indexField,
1367
+ isEditable
1368
+ },
1369
+ filterPanel: {
1370
+ sx: {
1371
+ '& .MuiDataGrid-filterFormValueInput': { width: 300 }
1372
+ }
1373
+ }
1374
+ }
1375
+ }
1376
+ experimentalFeatures={{ newEditingApi: isEditable }}
1377
+ sx={customPaddings}
1378
+ />
1379
+ }
1380
+ <ConfirmDialog
1381
+ open={dialogOpen}
1382
+ onClose={handleDialogClose}
1383
+ onConfirm={handleDeleteSave}
1384
+ />
1385
+ <Snackbar
1386
+ open={snackBar.open}
1387
+ autoHideDuration={5000}
1388
+ onClose={() => { setSnackBar({ open: false }); }}
1389
+ anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
1390
+ >
1391
+ <Alert severity={snackBar.severity}>
1392
+ {snackBar.msg}
1393
+ </Alert>
1394
+ </Snackbar>
1395
+ </Box>
1396
+ );
1397
+ });
1398
+
1399
+ DataGridBySchemaEditable.displayName = 'DataGridBySchemaEditable';
1400
+
1401
+ export default DataGridBySchemaEditable;