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.
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ import DataGridBySchemaEditable from './components/DataGridBySchemaEditable';
2
+
3
+ export { DataGridBySchemaEditable }
@@ -0,0 +1,3 @@
1
+ import * as Layout from './layout';
2
+
3
+ export { Layout };
@@ -0,0 +1,104 @@
1
+ import theme from './theme';
2
+
3
+ export const content = {
4
+ flexGrow: 1,
5
+ pr: 3,
6
+ pl: 3,
7
+ pt: 2,
8
+ pb: 0
9
+ };
10
+
11
+ export const main = {
12
+ ...content,
13
+ ml: 4
14
+ };
15
+
16
+ export const flexRow = {
17
+ display: 'flex',
18
+ flexDirection: 'row',
19
+ justifyContent: 'space-between',
20
+ alignItems: 'center'
21
+ };
22
+
23
+ export const topBar = {
24
+ ...flexRow,
25
+ width: '100%',
26
+ pl: 3,
27
+ pr: 6
28
+ };
29
+
30
+ export const loadingBox = {
31
+ ...flexRow,
32
+ height: '100%',
33
+ backgroundColor: '#ccc',
34
+ justifyContent: 'center'
35
+ };
36
+
37
+ export const actionButtons = {
38
+ ...flexRow,
39
+ width: '100%',
40
+ alignItems: 'flex-start'
41
+ };
42
+
43
+ export const inLineForm = {
44
+ ...flexRow,
45
+ width: '100%',
46
+ justifyContent: 'flex-start'
47
+ };
48
+
49
+ export const flexColumn = {
50
+ ...flexRow,
51
+ flexDirection: 'column'
52
+ };
53
+
54
+ export const flexRowGrow = {
55
+ ...flexRow,
56
+ flexGrow: 1
57
+ };
58
+
59
+ export const topBarOverList = {
60
+ ...flexRowGrow,
61
+ mb: 2
62
+ };
63
+
64
+ export const dataGrid = {
65
+ height: 'calc(100vh - 180px)',
66
+ mt: 2
67
+ };
68
+
69
+ export const dataGridWithTabs = {
70
+ ...dataGrid,
71
+ mt: 0
72
+ };
73
+
74
+ export const dataGridFixedHeight = {
75
+ ...dataGrid,
76
+ height: 400
77
+ };
78
+
79
+ export const fullWidthButton = {
80
+ mt: 3,
81
+ width: '100%'
82
+ };
83
+
84
+ export const formCard = {
85
+ width: '100%',
86
+ backgroundColor: theme.palette.formCard.main,
87
+ mt: 2,
88
+ mb: 2
89
+ };
90
+
91
+ export const formCardContent = {
92
+ maxHeight: 350,
93
+ overflow: 'scroll'
94
+ };
95
+
96
+ export const metabaseAppEmbed = {
97
+ height: 1700
98
+ };
99
+
100
+ export const geoPicker = {
101
+ height: 350,
102
+ width: '100%',
103
+ mt: 0
104
+ };
@@ -0,0 +1,190 @@
1
+ import type {} from '@mui/x-data-grid/themeAugmentation';
2
+ import { createTheme } from '@mui/material/styles';
3
+ import { ptBR as corePtBR } from '@mui/material/locale';
4
+ import { ptBR } from '@mui/x-data-grid';
5
+ import { ptBR as pickersPtBR } from '@mui/x-date-pickers';
6
+
7
+ declare module '@mui/material/styles' {
8
+ // interface Theme {
9
+ // status: {
10
+ // danger: React.CSSProperties['color'];
11
+ // };
12
+ // }
13
+ interface Palette {
14
+ avatars: string[];
15
+ avatarsMore: string[];
16
+ topBarButton: Palette['primary'];
17
+ producao: Palette['primary'];
18
+ empreendimento: Palette['primary'];
19
+ comercializacao: Palette['primary'];
20
+ credito: Palette['primary'];
21
+ certificacao: Palette['primary'];
22
+ successButton: Palette['primary'];
23
+ selectedItem: Palette['primary'];
24
+ tableColumnHeader: Palette['primary'];
25
+ formCard: Palette['primary'];
26
+ }
27
+ // interface PaletteOptions {
28
+ // neutral: PaletteOptions['primary'];
29
+ // }
30
+ //
31
+ interface PaletteColor {
32
+ semaphoric?: string;
33
+ }
34
+ // interface SimplePaletteColorOptions {
35
+ // darker?: string;
36
+ // }
37
+ // interface ThemeOptions {
38
+ // status: {
39
+ // danger: React.CSSProperties['color'];
40
+ // };
41
+ // }
42
+ }
43
+
44
+
45
+ ptBR.components.MuiDataGrid.defaultProps.localeText = {
46
+ ...ptBR.components.MuiDataGrid.defaultProps.localeText,
47
+ toolbarQuickFilterPlaceholder: 'Buscar...',
48
+ toolbarQuickFilterLabel: 'Buscar',
49
+ toolbarQuickFilterDeleteIconLabel: 'Limpar busca'
50
+ };
51
+
52
+ const palette = {
53
+ avatars: ['#e60049', '#0bb4ff', '#50e991', '#e6d800', '#9b19f5', '#ffa300', '#dc0ab4', '#b3d4ff', '#00bfa0'],
54
+ avatarsMore: ['#023fa5', '#8e063b', '#d33f6a', '#11c638', '#ef9708', '#0fcfc0', '#f79cd4', '#7d87b9', '#bb7784', '#4a6fe3', '#8595e1', '#b5bbe3', '#e6afb9', '#e07b91', '#8dd593', '#c6dec7', '#ead3c6', '#f0b98d', '#9cded6', '#d5eae7', '#f3e1eb', '#f6c4e1', '#bec1d4', '#d6bcc0'],
55
+ topBarButton: {
56
+ main: '#ffffff'
57
+ },
58
+ producao: {
59
+ main: '#e60049',
60
+ contrastText: '#fff'
61
+ },
62
+ empreendimento: {
63
+ main: '#0bb4ff',
64
+ contrastText: '#fff'
65
+ },
66
+ comercializacao: {
67
+ main: '#ffa300',
68
+ contrastText: '#fff'
69
+ },
70
+ credito: {
71
+ main: '#dc0ab4',
72
+ contrastText: '#fff'
73
+ },
74
+ certificacao: {
75
+ main: '#9b19f5',
76
+ contrastText: '#fff'
77
+ },
78
+ background: {
79
+ default: '#D9D9D9'
80
+ },
81
+ primary: {
82
+ main: '#3949AB'
83
+ },
84
+ secondary: {
85
+ main: '#9ca4d5',
86
+ contrastText: '#fff'
87
+ },
88
+ successButton: {
89
+ main: '#CDDC39',
90
+ contrastText: '#fff'
91
+ },
92
+ success: {
93
+ main: '#0e0',
94
+ contrastText: '#fff',
95
+ semaphoric: '#0e0'
96
+ },
97
+ error: {
98
+ main: '#d32f2f',
99
+ semaphoric: '#f66'
100
+ },
101
+ warning: {
102
+ main: '#ee0',
103
+ semaphoric: '#ee0'
104
+ },
105
+ selectedItem: {
106
+ main: '#2962FF'
107
+ },
108
+ tableColumnHeader: {
109
+ main: '#ECEFF1'
110
+ },
111
+ formCard: {
112
+ main: '#FFF'
113
+ }
114
+ };
115
+
116
+ const theme = createTheme(
117
+ {
118
+ palette,
119
+ components: {
120
+ MuiButton: {
121
+ styleOverrides: {
122
+ contained: {
123
+ // borderRadius: 50
124
+ }
125
+ }
126
+ },
127
+ MuiTextField: {
128
+ styleOverrides: {
129
+ root: {
130
+ '&.Mui-required .MuiFormLabel-asterisk': {
131
+ // color: '#F00'
132
+ }
133
+ }
134
+ }
135
+ },
136
+ MuiListItemButton: {
137
+ styleOverrides: {
138
+ root: {
139
+ '&.Mui-selected, :hover': {
140
+ color: palette.selectedItem.main,
141
+ '& .MuiListItemIcon-root': {
142
+ color: palette.selectedItem.main
143
+ }
144
+ },
145
+ '&.Mui-selected:hover': {
146
+ color: palette.selectedItem.main,
147
+ '& .MuiListItemIcon-root': {
148
+ color: palette.selectedItem.main
149
+ }
150
+ },
151
+ '&.disabled': {
152
+ opacity: 0.5
153
+ }
154
+ }
155
+ }
156
+ },
157
+ MuiDataGrid: {
158
+ styleOverrides: {
159
+ root: {
160
+ backgroundColor: palette.formCard.main,
161
+ '& .MuiDataGrid-columnHeaderTitle': {
162
+ overflow: 'visible',
163
+ lineHeight: '1.43rem',
164
+ whiteSpace: 'normal'
165
+ }
166
+ },
167
+ cell: {
168
+ // backgroundColor: 'yellow'
169
+ },
170
+ columnHeader: {
171
+ backgroundColor: palette.tableColumnHeader.main
172
+ }
173
+ }
174
+ },
175
+ MuiTabs: {
176
+ styleOverrides: {
177
+ root: {
178
+ backgroundColor: palette.formCard.main,
179
+ marginTop: 20
180
+ }
181
+ }
182
+ }
183
+ }
184
+ },
185
+ corePtBR,
186
+ ptBR,
187
+ pickersPtBR
188
+ );
189
+
190
+ export default theme;
package/src/utils.ts ADDED
@@ -0,0 +1,321 @@
1
+ import * as Yup from 'yup';
2
+ import { GridActionsColDef, GridColDef } from '@mui/x-data-grid';
3
+
4
+ export type Id = string | number;
5
+ export type Item = Record<string, any>;
6
+ export type Schema = Record<string, Field>;
7
+ export interface Choice {
8
+ value: string | number,
9
+ display_name: string
10
+ };
11
+ type FieldChild = Record<string, Schema>;
12
+ export interface Field {
13
+ type: string,
14
+ child?: FieldChild,
15
+ children?: Schema,
16
+ model_default?: any,
17
+ model_required?: boolean
18
+ choices?: Choice[],
19
+ max_length?: number | string,
20
+ label: string,
21
+ read_only?: boolean
22
+ };
23
+ interface GridBySchemaColDef extends GridColDef {
24
+ isIndexField?: boolean,
25
+ creatable?: boolean,
26
+ orderable?: boolean,
27
+ patternFormat?: string
28
+ };
29
+ interface GridActionsBySchemaColDef extends GridActionsColDef {
30
+ isIndexField?: boolean,
31
+ creatable?: boolean,
32
+ orderable?: boolean,
33
+ patternFormat?: string
34
+ };
35
+ export type GridEnrichedBySchemaColDef = GridBySchemaColDef | GridActionsBySchemaColDef;
36
+
37
+ export const emptyByType: any = (field:Field, forDatabase:boolean = false) => {
38
+ if (field.model_default) {
39
+ return field.model_default;
40
+ }
41
+ switch (field.type) {
42
+ case 'nested object':
43
+ // emptyByType must be an empty object for the database, but must be null for the mui-autocomplete component. So I had to make this ugly hack here, when we're preparing to sendo to the database:
44
+ return forDatabase ? {} : null;
45
+ case 'field':
46
+ if (field.child) {
47
+ return [];
48
+ }
49
+ return (forDatabase)
50
+ ? undefined
51
+ : null;
52
+ case 'string':
53
+ case 'email':
54
+ return '';
55
+ case 'integer':
56
+ return 0;
57
+ case 'array':
58
+ return [];
59
+ default:
60
+ return null;
61
+ }
62
+ };
63
+
64
+ export const getChoiceByValue = (
65
+ value:number | string,
66
+ choices:Choice[] | undefined
67
+ ) => {
68
+ if (!choices) {
69
+ return null;
70
+ }
71
+ for (const choice of choices) {
72
+ if (choice.value === value) {
73
+ return choice.display_name;
74
+ }
75
+ }
76
+ };
77
+
78
+ export const populateValues = ({
79
+ model,
80
+ data,
81
+ schema
82
+ }: {
83
+ model:string,
84
+ data:Item,
85
+ schema:Schema
86
+ }) => {
87
+ const values:Record<string, any> = {};
88
+ for (const entry of Object.entries(schema)) {
89
+ const [key, field] = entry;
90
+ if (key === 'id' && isTmpId(data[key])) {
91
+ continue;
92
+ }
93
+
94
+ if (!data[key]) {
95
+ values[key] = emptyByType(field);
96
+ continue;
97
+ }
98
+
99
+ if (field.type === 'field' && field.child) {
100
+ if (Array.isArray(data[key])) {
101
+ const arValues = [];
102
+ for (const row of data[key]) {
103
+ const value = populateValues({
104
+ model,
105
+ data: row,
106
+ schema: field.child.children
107
+ });
108
+ arValues.push(value);
109
+ }
110
+ values[key] = arValues;
111
+ continue;
112
+ }
113
+
114
+ values[key] = populateValues({
115
+ model,
116
+ data: data[key],
117
+ schema: field.child.children
118
+ });
119
+ continue;
120
+ }
121
+
122
+ if (field.type === 'choice') {
123
+ values[key] = data[key]
124
+ ? {
125
+ value: data[key],
126
+ display_name: getChoiceByValue(data[key], field.choices)
127
+ }
128
+ : emptyByType(field);
129
+ continue;
130
+ }
131
+
132
+ values[key] = data[key];
133
+ }
134
+ // console.log(values);
135
+ return values;
136
+ };
137
+
138
+ const getYupValidator = (type:string) => {
139
+ let yupFunc;
140
+ try {
141
+ switch (type) {
142
+ case 'slug':
143
+ yupFunc = Yup.string();
144
+ break;
145
+ case 'email':
146
+ yupFunc = Yup.string().email('Este campo deve ser um e-mail válido.');
147
+ break;
148
+ case 'integer':
149
+ yupFunc = Yup.number().integer('Este campo deve ser um número inteiro');
150
+ break;
151
+ case 'choice':
152
+ yupFunc = Yup.object();
153
+ break;
154
+ case 'field':
155
+ yupFunc = Yup.mixed();
156
+ break;
157
+ case 'nested object':
158
+ yupFunc = Yup.object();
159
+ break;
160
+ case 'date':
161
+ yupFunc = Yup.date();
162
+ break;
163
+ case 'string':
164
+ yupFunc = Yup.string();
165
+ break;
166
+ case 'number':
167
+ yupFunc = Yup.number();
168
+ break;
169
+ case 'boolean':
170
+ yupFunc = Yup.boolean();
171
+ break;
172
+ case 'array':
173
+ yupFunc = Yup.array();
174
+ break;
175
+ case 'object':
176
+ yupFunc = Yup.object();
177
+ break;
178
+ default:
179
+ yupFunc = Yup.string();
180
+ break;
181
+ }
182
+ } catch (e) {
183
+ yupFunc = Yup.string();
184
+ }
185
+ return yupFunc.nullable();
186
+ };
187
+
188
+ export const buildGenericYupValidationSchema = ({
189
+ data,
190
+ schema,
191
+ many = false,
192
+ skipFields = [],
193
+ extraValidators = {}
194
+ }:{
195
+ data:Item,
196
+ schema:Schema,
197
+ many?:boolean,
198
+ skipFields?: string[],
199
+ extraValidators?:Item
200
+ }) => {
201
+ const yupValidator:Record<string, any> = {};
202
+ for (const entry of Object.entries(schema)) {
203
+ const [key, field] = entry;
204
+
205
+ if (!data || !(key in data) || key === 'id' || skipFields.includes(key)) {
206
+ continue;
207
+ }
208
+
209
+ // console.log({ key, field, data: data[key] });
210
+
211
+ // OneToMany or ManyToMany:
212
+ if (field.type === 'field' && field.child) {
213
+ yupValidator[key] = buildGenericYupValidationSchema({
214
+ schema: field.child.children,
215
+ many: true,
216
+ data: data[key],
217
+ extraValidators: Object.prototype.hasOwnProperty.call(extraValidators, key)
218
+ ? extraValidators[key]
219
+ : {}
220
+ });
221
+ continue;
222
+ }
223
+
224
+ // Nested Object:
225
+ if (field.type === 'nested object' && field.children) {
226
+ // yupValidator[key] = buildGenericYupValidationSchema({
227
+ // schema: field.children,
228
+ // many: false,
229
+ // data: data[key],
230
+ // extraValidators: Object.prototype.hasOwnProperty.call(extraValidators, key)
231
+ // ? extraValidators[key]
232
+ // : {}
233
+ // });
234
+ // if (!field.model_required) {
235
+ // yupValidator[key] = yupValidator[key].nullable();
236
+ // }
237
+ // continue;
238
+ }
239
+
240
+ yupValidator[key] = Object.prototype.hasOwnProperty.call(extraValidators, key)
241
+ ? extraValidators[key]
242
+ : getYupValidator(field.type);
243
+
244
+ if (field.model_required) {
245
+ yupValidator[key] = yupValidator[key].required('Este campo é obrigatório');
246
+ }
247
+ if (field.max_length) {
248
+ yupValidator[key] = yupValidator[key].max(parseInt(field.max_length as string), `Este campo só pode ter no máximo ${field.max_length} caracteres`);
249
+ }
250
+ }
251
+ // console.log({ yupValidator });
252
+ return (many)
253
+ ? Yup.array().of(Yup.object().shape(yupValidator))
254
+ : Yup.object().shape(yupValidator);
255
+ };
256
+
257
+ export const errorProps = ({
258
+ type,
259
+ errors,
260
+ fieldKey,
261
+ fieldKeyProp,
262
+ index
263
+ }: {
264
+ type:string,
265
+ errors:Item,
266
+ fieldKey:string,
267
+ fieldKeyProp:string,
268
+ index?:number
269
+ }) => {
270
+ let error;
271
+ let helperText;
272
+ if (index) {
273
+ const hasErrors = errors && errors[fieldKey] && errors[fieldKey][index] && Boolean(errors[fieldKey][index][fieldKeyProp]);
274
+ error = hasErrors;
275
+ helperText = hasErrors ? errors[fieldKey][index][fieldKeyProp].message : null;
276
+ return { error, helperText };
277
+ }
278
+
279
+ const hasErrors = errors && errors[fieldKey] && Boolean(errors[fieldKey][fieldKeyProp]);
280
+ error = hasErrors;
281
+ helperText = hasErrors ? errors[fieldKey][fieldKeyProp].message : null;
282
+ return { error, helperText };
283
+ };
284
+
285
+ export const getTmpId = () => {
286
+ return 'tmp' + Math.floor(Math.random() * 1000000);
287
+ };
288
+
289
+ export const isTmpId = (id:string | number | undefined | null) => {
290
+ if (!id) {
291
+ return true;
292
+ }
293
+ return id.toString().substr(0, 3) === 'tmp';
294
+ };
295
+
296
+ export const reducer = (state:Record<string, any>, newState:Record<string, any>) => {
297
+ return { ...state, ...newState };
298
+ };
299
+
300
+ export const getPatternFormat = (type:string) => {
301
+ let format = '';
302
+ switch (type) {
303
+ case 'telefone':
304
+ case 'fone':
305
+ case 'phone':
306
+ case 'contact':
307
+ case 'contato':
308
+ format = '(##)#####-####';
309
+ break;
310
+ case 'cpf':
311
+ format = '###.###.###-##';
312
+ break;
313
+ case 'cnpj':
314
+ format = '##.###.###/####-##';
315
+ break;
316
+ case 'cep':
317
+ format = '##.###-###';
318
+ break;
319
+ }
320
+ return format;
321
+ };