drf-react-by-schema 0.2.2 → 0.3.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,168 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import Box from '@mui/material/Box';
3
+ import { GridRowId } from '@mui/x-data-grid';
4
+
5
+ import DataGridBySchemaEditable from './DataGridBySchemaEditable';
6
+ import DataTotals from './DataTotals';
7
+ import { DRFReactBySchemaContext } from '../context/DRFReactBySchemaContext';
8
+ import { APIWrapperContext } from '../context/APIWrapperContext';
9
+ import { DataSchemaColumnsType, GridEnrichedBySchemaColDef, Id, Item } from '../utils';
10
+ import { getGenericModelList } from '../api';
11
+ import { SumRowsType } from './DataTotals';
12
+ import { Layout } from '../styles';
13
+
14
+ interface GenericRelatedModelListProps {
15
+ model: string;
16
+ id: Id;
17
+ relatedModel: string;
18
+ columnFields: string[];
19
+ creatableFields: string[];
20
+ hiddenFields: string[];
21
+ usuaria?: Item | null;
22
+ minWidthFields?: Record<string, number>;
23
+ indexField?: string;
24
+ addExistingModel?: string;
25
+ onProcessRow?: (p: any) => void;
26
+ sumRows?: SumRowsType;
27
+ customColumnOperations?: (column: GridEnrichedBySchemaColDef) => GridEnrichedBySchemaColDef;
28
+ isEditable?: boolean;
29
+ isAutoHeight?: boolean;
30
+ isInBatches?: boolean;
31
+ indexFieldBasePath?: string;
32
+ LinkComponent?: React.ReactNode;
33
+ };
34
+
35
+ export default function GenericRelatedModelList ({
36
+ model,
37
+ id,
38
+ relatedModel,
39
+ columnFields,
40
+ creatableFields,
41
+ hiddenFields,
42
+ usuaria,
43
+ minWidthFields,
44
+ indexField = '',
45
+ addExistingModel = '',
46
+ onProcessRow,
47
+ sumRows,
48
+ customColumnOperations,
49
+ isEditable = true,
50
+ isAutoHeight = false,
51
+ indexFieldBasePath,
52
+ LinkComponent
53
+ }:GenericRelatedModelListProps) {
54
+ const context = React.useContext(DRFReactBySchemaContext);
55
+ const apiContext = React.useContext(APIWrapperContext);
56
+ if (!context.serverEndPoint || !apiContext) {
57
+ console.error('Error: There is no endpoint defined in DRFReactBySchemaProvider!');
58
+ return (<></>);
59
+ }
60
+ const {
61
+ serverEndPoint,
62
+ isInBatches,
63
+ firstBatchLength,
64
+ } = context;
65
+ const {
66
+ onEditRelatedModelSave,
67
+ onDeleteRelatedModel
68
+ } = apiContext;
69
+
70
+ const [data, setData] = useState<DataSchemaColumnsType | boolean>(false);
71
+ const [visibleRows, setVisibleRows] = useState<GridRowId[]>([]);
72
+ const [hideFooterPagination, setHideFooterPagination] = useState(false);
73
+
74
+ const finalCustomColumnOperations = (column:GridEnrichedBySchemaColDef) => {
75
+ if (minWidthFields) {
76
+ if (Object.prototype.hasOwnProperty.call(minWidthFields, column.field)) {
77
+ column.minWidth = minWidthFields[column.field];
78
+ }
79
+ }
80
+
81
+ if (customColumnOperations) {
82
+ return customColumnOperations(column);
83
+ }
84
+
85
+ return column;
86
+ };
87
+
88
+ const loadObjectList = async () => {
89
+ const loadParams = {
90
+ model,
91
+ serverEndPoint,
92
+ id,
93
+ relatedModel,
94
+ columnFields,
95
+ hiddenFields,
96
+ creatableFields,
97
+ isInBatches
98
+ };
99
+ const loadedData = await getGenericModelList(loadParams);
100
+ if (loadedData) {
101
+ setData(loadedData);
102
+ if (isInBatches && loadedData.data.length === firstBatchLength) {
103
+ setHideFooterPagination(true);
104
+ getGenericModelList({
105
+ ...loadParams,
106
+ loadedSchema: loadedData.schema
107
+ }).then(lastBatchData => {
108
+ if (lastBatchData) {
109
+ setData({
110
+ ...loadedData,
111
+ data: [
112
+ ...loadedData.data,
113
+ ...lastBatchData.data
114
+ ]
115
+ });
116
+ }
117
+ setHideFooterPagination(false);
118
+ });
119
+ }
120
+ return;
121
+ }
122
+ console.log('error retrieving data!');
123
+ };
124
+
125
+ useEffect(() => {
126
+ loadObjectList();
127
+ }, []);
128
+
129
+ return (
130
+ <>
131
+ {typeof data !== 'boolean' && data.columns &&
132
+ <>
133
+ <Box sx={Layout.dataGridFixedHeight}>
134
+ <DataGridBySchemaEditable
135
+ data={data.data}
136
+ columns={data.columns}
137
+ schema={data.schema}
138
+ model={relatedModel}
139
+ indexField={indexField}
140
+ indexFieldBasePath={indexFieldBasePath}
141
+ addExistingModel={addExistingModel}
142
+ isEditable={isEditable}
143
+ modelParent={model}
144
+ modelParentId={id}
145
+ customColumnOperations={finalCustomColumnOperations}
146
+ setVisibleRows={setVisibleRows}
147
+ isAutoHeight={isAutoHeight}
148
+ hideFooterPagination={hideFooterPagination}
149
+ onProcessRow={onProcessRow}
150
+ onDataChange={newData => {
151
+ setData({
152
+ ...data,
153
+ data: newData
154
+ });
155
+ }}
156
+ LinkComponent={LinkComponent}
157
+ />
158
+ </Box>
159
+ <DataTotals
160
+ data={data.data}
161
+ sumRows={sumRows}
162
+ visibleRows={visibleRows}
163
+ />
164
+ </>
165
+ }
166
+ </>
167
+ );
168
+ }
@@ -0,0 +1,447 @@
1
+ import React, { useState, useReducer, useEffect, useRef } from 'react';
2
+ import {
3
+ Id,
4
+ Item,
5
+ reducer,
6
+ SchemaType,
7
+ populateValues,
8
+ buildGenericYupValidationSchema,
9
+ getTmpId,
10
+ isTmpId
11
+ } from '../utils';
12
+
13
+ import DialogJSONSchemaForm from '../components/DialogJSONSchemaForm';
14
+ import DialogActions from '../components/DialogActions';
15
+ import { DRFReactBySchemaContext, DRFReactBySchemaContextType } from './DRFReactBySchemaContext';
16
+ import {
17
+ isLoggedIn,
18
+ getAutoComplete,
19
+ getGenericModel,
20
+ updateDataBySchema,
21
+ addExistingRelatedModel,
22
+ deleteData,
23
+ getJSONSchema,
24
+ createOrUpdateJSONSchema
25
+ } from '../api';
26
+ import { AxiosResponse } from 'axios';
27
+ import {
28
+ APIWrapperContext,
29
+ DialogType,
30
+ LoadSinglePageDataProps,
31
+ OnDeleteRelatedModelType,
32
+ OnEditModelType,
33
+ OnEditRelatedModelType,
34
+ PageFormType,
35
+ SnackBarType
36
+ } from './APIWrapperContext';
37
+
38
+ interface APIWrapperProps {
39
+ setLoading: React.Dispatch<React.SetStateAction<boolean>>;
40
+ handleLoading: (p: boolean) => void;
41
+ setSnackBar: React.Dispatch<React.SetStateAction<SnackBarType>>;
42
+ setDialog: React.Dispatch<React.SetStateAction<DialogType>>;
43
+ children: React.ReactNode;
44
+ };
45
+
46
+ function APIWrapper ({
47
+ setLoading,
48
+ handleLoading,
49
+ setSnackBar,
50
+ setDialog,
51
+ children
52
+ }:APIWrapperProps) {
53
+ const { serverEndPoint } = DRFReactBySchemaContext
54
+ ? React.useContext(DRFReactBySchemaContext) as DRFReactBySchemaContextType
55
+ : { serverEndPoint: null };
56
+ if (!serverEndPoint) {
57
+ return (
58
+ <>
59
+ {children}
60
+ </>
61
+ );
62
+ }
63
+
64
+ const [usuaria, setUsuaria] = useState<Item | null>(null);
65
+ const [optionsAC, setOptionsAC] = useReducer(reducer, {});
66
+ const initialPageForm:PageFormType = {
67
+ id: '',
68
+ schema: null,
69
+ initialValues: null,
70
+ validationSchema: null
71
+ };
72
+ const [pageForm, setPageForm] = useReducer(reducer, initialPageForm);
73
+ const editModel = useRef<Item>({});
74
+ const jsonSchemaFormRef = useRef<any>(null);
75
+
76
+ useEffect(() => {
77
+ setUsuaria(null);
78
+ isLoggedIn(serverEndPoint).then(usuaria => {
79
+ setUsuaria(usuaria || { erro: 'token inválido' });
80
+ });
81
+ }, []);
82
+
83
+ useEffect(() => {
84
+ setPageForm(initialPageForm);
85
+ }, []);
86
+
87
+ const onTriggerSnackBar = ({ msg, severity = 'info' }:SnackBarType) => {
88
+ setSnackBar({
89
+ open: true,
90
+ msg,
91
+ severity
92
+ });
93
+ };
94
+
95
+ const loadSinglePageData = async ({
96
+ model,
97
+ objId,
98
+ objTitleField = 'nome',
99
+ optionsACModels,
100
+ basePath = '/',
101
+ formPath = null,
102
+ extraValidators = {}
103
+ }: LoadSinglePageDataProps) => {
104
+ setLoading(true);
105
+ if (objId === 'novo') {
106
+ objId = getTmpId();
107
+ }
108
+
109
+ const object = await getGenericModel({
110
+ model,
111
+ serverEndPoint,
112
+ id: isTmpId(objId) ? null : objId
113
+ });
114
+
115
+ if (object === false) {
116
+ setPageForm({ schema: false, id: '' });
117
+ console.log('Houve um erro ao tentar carregar os dados!');
118
+ return false;
119
+ }
120
+
121
+ setLoading(false);
122
+
123
+ populateOptionsAC(optionsACModels);
124
+
125
+ const values = populateInitialValues({
126
+ model,
127
+ id: objId,
128
+ extraValidators,
129
+ ...object
130
+ });
131
+
132
+ return values;
133
+ };
134
+
135
+ const onSubmit = async (
136
+ model:string,
137
+ id:Id,
138
+ data:Item,
139
+ e:React.SyntheticEvent<HTMLButtonElement, SubmitEvent>
140
+ ) => {
141
+ setLoading(true);
142
+ const response = await updateDataBySchema({
143
+ model,
144
+ modelObjectId: id,
145
+ serverEndPoint,
146
+ data,
147
+ schema: pageForm.schema
148
+ });
149
+ setLoading(false);
150
+ if (!['number', 'string'].includes(typeof response)) {
151
+ onTriggerSnackBar({
152
+ msg: 'Houve um problema ao salvar seus dados! Por favor, entre em contato',
153
+ severity: 'error'
154
+ });
155
+ console.log({
156
+ msg: 'Error saving model',
157
+ errors: response,
158
+ data
159
+ });
160
+ return false;
161
+ }
162
+
163
+ onTriggerSnackBar({
164
+ msg: (id)
165
+ ? 'Dados atualizados com sucesso!'
166
+ : 'Criado com sucesso!'
167
+ });
168
+
169
+ return response as Id;
170
+ };
171
+
172
+ const populateOptionsAC = (optionsACModels:string[]) => {
173
+ for (const model of optionsACModels) {
174
+ getAutoComplete({ model, serverEndPoint }).then(options => {
175
+ setOptionsAC({ [model]: options });
176
+ });
177
+ }
178
+ };
179
+
180
+ interface PopulateInitialValuesProps {
181
+ model: string;
182
+ id: Id;
183
+ data: Item;
184
+ schema: SchemaType;
185
+ extraValidators: Item;
186
+ isEditModel?: boolean;
187
+ };
188
+ const populateInitialValues = ({
189
+ model,
190
+ id,
191
+ isEditModel,
192
+ extraValidators,
193
+ ...object
194
+ }:PopulateInitialValuesProps) => {
195
+ const values = populateValues(object);
196
+ const yupSchema = buildGenericYupValidationSchema({ ...object, data: values, extraValidators });
197
+ setPageForm({
198
+ model,
199
+ id,
200
+ schema: object.schema,
201
+ initialValues: values,
202
+ validationSchema: yupSchema
203
+ });
204
+ return values;
205
+ };
206
+
207
+ const onEditModel = ({
208
+ fieldKey,
209
+ index,
210
+ model,
211
+ id,
212
+ labelKey,
213
+ setValue,
214
+ getValues
215
+ }: OnEditModelType) => {
216
+ setDialog({
217
+ open: true,
218
+ loading: true
219
+ });
220
+ getJSONSchema({ model, serverEndPoint, id }).then(data => {
221
+ const jsonSchemaSubmit = async (e:React.SyntheticEvent<HTMLElement, SubmitEvent>) => {
222
+ jsonSchemaFormRef.current.onSubmit(e);
223
+ return true;
224
+ };
225
+ setDialog({
226
+ loading: false,
227
+ title: 'Editar',
228
+ Body: <DialogJSONSchemaForm
229
+ jsonSchemaFormRef = {jsonSchemaFormRef}
230
+ schema = {data.serializer.schema}
231
+ uiSchema = {data.serializer.uiSchema}
232
+ formData = {data.formData}
233
+ onSubmit = {onEditModelSave}
234
+ />,
235
+ Actions: <DialogActions
236
+ setDialog = {setDialog}
237
+ handleSave = {jsonSchemaSubmit}
238
+ />
239
+ } as DialogType);
240
+ editModel.current = {
241
+ fieldKey,
242
+ index,
243
+ model,
244
+ id,
245
+ labelKey,
246
+ setValue,
247
+ getValues
248
+ };
249
+ });
250
+ };
251
+
252
+ const onEditModelSave = async ({ formData }:{ formData: Item }) => {
253
+ setDialog({ open: false });
254
+ setLoading(true);
255
+ const {
256
+ fieldKey,
257
+ index,
258
+ model,
259
+ id,
260
+ labelKey,
261
+ setValue,
262
+ getValues
263
+ } = editModel.current;
264
+ const newModelId = await createOrUpdateJSONSchema({
265
+ model,
266
+ serverEndPoint,
267
+ id,
268
+ formData
269
+ });
270
+ if (newModelId.errors) {
271
+ console.log(newModelId.errors);
272
+ onTriggerSnackBar({
273
+ msg: 'Houve um problema ao salvar a alteração! Por favor, entre em contato.',
274
+ severity: 'error'
275
+ });
276
+ return;
277
+ }
278
+ onTriggerSnackBar({
279
+ msg: 'Alterações salvas com sucesso!',
280
+ severity: 'info'
281
+ });
282
+ const targetKey = (fieldKey && index >= 0)
283
+ ? `${fieldKey}.${index}.${model}`
284
+ : model;
285
+ const newValue = {
286
+ ...getValues(targetKey),
287
+ ...formData,
288
+ label: formData[labelKey]
289
+ };
290
+ setValue(targetKey, newValue);
291
+ populateOptionsAC([model]);
292
+ setLoading(false);
293
+ };
294
+
295
+ const onDeleteModel = (model:string, id:Id, onSuccess:()=>void | null) => {
296
+ setDialog({
297
+ open: true,
298
+ loading: false,
299
+ title: 'Apagar',
300
+ Body: 'Tem certeza de que deseja apagar este item?',
301
+ Actions: <DialogActions
302
+ setDialog = {setDialog}
303
+ handleSave = {(e:React.SyntheticEvent) => {
304
+ return onDeleteModelSave(model, id, onSuccess);
305
+ }}
306
+ btnConfirm = "Sim, apagar"
307
+ />
308
+ });
309
+ };
310
+
311
+ const onDeleteModelSave = async (model:string, id:Id, onSuccess:()=>void | null) => {
312
+ setDialog({ open: false });
313
+ setLoading(true);
314
+ const ret = await deleteData(model, serverEndPoint, id);
315
+ if (ret !== false) {
316
+ onTriggerSnackBar({
317
+ msg: 'Apagado com com sucesso!',
318
+ severity: 'info'
319
+ });
320
+ if (onSuccess !== null) {
321
+ onSuccess();
322
+ }
323
+ return true;
324
+ }
325
+
326
+ setLoading(false);
327
+ onTriggerSnackBar({
328
+ msg: 'Houve um problema ao remover o item! Por favor, entre em contato.',
329
+ severity: 'error'
330
+ });
331
+ return false;
332
+ };
333
+
334
+ const onEditRelatedModelSave = async ({
335
+ model,
336
+ id,
337
+ relatedModel,
338
+ relatedModelId,
339
+ newRow,
340
+ schema,
341
+ onlyAddExisting
342
+ }:OnEditRelatedModelType) => {
343
+ const updateUrl = `${model}/${id}/${relatedModel}`;
344
+ if (onlyAddExisting) {
345
+ const response = await addExistingRelatedModel({
346
+ model,
347
+ id,
348
+ serverEndPoint,
349
+ data: {
350
+ onlyAddExisting: {
351
+ key: relatedModel,
352
+ value: newRow.id_to_add
353
+ }
354
+ }
355
+ });
356
+ if (Object.prototype.hasOwnProperty.call(response, 'errors')) {
357
+ console.log(response);
358
+ onTriggerSnackBar({
359
+ msg: 'Houve um problema ao salvar a alteração! Por favor, entre em contato.',
360
+ severity: 'error'
361
+ });
362
+ return false;
363
+ }
364
+ onTriggerSnackBar({
365
+ msg: 'Alterações salvas com sucesso!',
366
+ severity: 'info'
367
+ });
368
+ const object = await getGenericModel({
369
+ model,
370
+ id,
371
+ serverEndPoint,
372
+ relatedModel,
373
+ relatedModelId: newRow.id_to_add
374
+ });
375
+ return object;
376
+ }
377
+ // This is important for related data
378
+ if (schema[model] && !newRow[model]) {
379
+ newRow[model] = id;
380
+ }
381
+ const response = await updateDataBySchema({
382
+ model: relatedModel,
383
+ modelObjectId: newRow.id,
384
+ serverEndPoint,
385
+ data: newRow,
386
+ schema,
387
+ path: updateUrl
388
+ });
389
+ if (response && !Object.prototype.hasOwnProperty.call(response, 'errors')) {
390
+ onTriggerSnackBar({
391
+ msg: 'Alterações salvas com sucesso!',
392
+ severity: 'info'
393
+ });
394
+ return response as Id;
395
+ }
396
+ onTriggerSnackBar({
397
+ msg: 'Não foi possível salvar os dados. Confira os erros.',
398
+ severity: 'error'
399
+ });
400
+ return false;
401
+ };
402
+
403
+ const onDeleteRelatedModel = async ({
404
+ model,
405
+ id,
406
+ relatedModel,
407
+ relatedModelId
408
+ }:OnDeleteRelatedModelType) => {
409
+ const deleteUrl = `${model}/${id}/${relatedModel}`;
410
+ const response = await deleteData(deleteUrl, serverEndPoint, relatedModelId);
411
+ if (response) {
412
+ onTriggerSnackBar({
413
+ msg: 'Alterações salvas com sucesso!',
414
+ severity: 'info'
415
+ });
416
+ return response;
417
+ }
418
+ onTriggerSnackBar({
419
+ msg: 'Houve um problema ao remover o item! Por favor, entre em contato.',
420
+ severity: 'error'
421
+ });
422
+ return false;
423
+ };
424
+
425
+ return (
426
+ <APIWrapperContext.Provider
427
+ value = {{
428
+ usuaria,
429
+ onSubmit,
430
+ loadSinglePageData,
431
+ handleLoading,
432
+ optionsACState: [optionsAC, setOptionsAC],
433
+ pageFormState: [pageForm, setPageForm],
434
+ onEditModel,
435
+ onDeleteModel,
436
+ onEditRelatedModelSave,
437
+ onDeleteRelatedModel,
438
+ onTriggerSnackBar,
439
+ setDialog
440
+ }}
441
+ >
442
+ {children}
443
+ </APIWrapperContext.Provider>
444
+ );
445
+ }
446
+
447
+ export default React.memo(APIWrapper);
@@ -0,0 +1,84 @@
1
+ import React from 'react';
2
+ import { ItemSchemaColumnsType, Id, Item, SchemaType } from '../utils';
3
+
4
+ export interface LoadSinglePageDataProps {
5
+ model: string;
6
+ objId: Id;
7
+ objTitleField?: string;
8
+ optionsACModels: string[];
9
+ basePath?: string;
10
+ formPath?: string | null;
11
+ extraValidators: Item;
12
+ };
13
+
14
+ // export interface LoadingStateType {
15
+ // loading: boolean,
16
+ // setLoading: React.Dispatch<React.SetStateAction<boolean>>;
17
+ // };
18
+
19
+ export type OptionsACType = Record<string, Item[]>;
20
+
21
+ export interface PageFormType {
22
+ id: Id;
23
+ schema: SchemaType | null;
24
+ initialValues: Item | null;
25
+ validationSchema: Item | null;
26
+ };
27
+
28
+ export interface OnEditModelType {
29
+ fieldKey: string;
30
+ index: string;
31
+ model: string;
32
+ id: Id;
33
+ labelKey: string;
34
+ setValue: (p: any) => void;
35
+ getValues: (p: any) => any;
36
+ };
37
+
38
+ export interface OnEditRelatedModelType {
39
+ model: string;
40
+ id: Id;
41
+ relatedModel: string;
42
+ relatedModelId: Id;
43
+ newRow: Item;
44
+ schema: SchemaType;
45
+ onlyAddExisting: boolean;
46
+ };
47
+
48
+ export interface OnDeleteRelatedModelType {
49
+ model: string;
50
+ id: Id;
51
+ relatedModel: string;
52
+ relatedModelId: Id;
53
+ };
54
+
55
+ export interface SnackBarType {
56
+ open?: boolean;
57
+ msg?: string;
58
+ severity?: string
59
+ };
60
+
61
+ export interface DialogType {
62
+ open: boolean;
63
+ loading?: boolean;
64
+ title?: string;
65
+ Body?: React.ReactNode;
66
+ Actions?: React.ReactNode;
67
+ }
68
+
69
+ export interface APIWrapperContextType {
70
+ usuaria: Item | null;
71
+ onSubmit: (model: string, id: Id, data: Item, e: React.SyntheticEvent<HTMLButtonElement, SubmitEvent>) => Promise<false | Id>;
72
+ loadSinglePageData: (p: LoadSinglePageDataProps) => boolean | Item;
73
+ handleLoading: (p: boolean) => void;
74
+ optionsACState: [{ [x: string]: any; }, React.Dispatch<OptionsACType>];
75
+ pageFormState: [{ [x: string]: any; }, React.Dispatch<PageFormType>];
76
+ onEditModel: (p: OnEditModelType) => void;
77
+ onDeleteModel: (model:string, id:Id, onSuccess:()=>void | null) => void;
78
+ onEditRelatedModelSave: (p: OnEditRelatedModelType) => Promise<boolean | Id | ItemSchemaColumnsType>;
79
+ onDeleteRelatedModel: (p: OnDeleteRelatedModelType) => Promise<boolean>;
80
+ onTriggerSnackBar: (p: SnackBarType) => void;
81
+ setDialog: React.Dispatch<React.SetStateAction<DialogType>>;
82
+ };
83
+
84
+ export const APIWrapperContext = React.createContext<APIWrapperContextType | null>(null);