yaml-admin-front 0.0.19 → 0.0.21
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/package.json +4 -1
- package/src/YMLAdmin.jsx +2 -1
- package/src/common/actionParser.jsx +59 -0
- package/src/common/field.jsx +166 -35
- package/src/component/EntityTreeView.jsx +156 -0
- package/src/layout/MyMenu.jsx +3 -3
- package/src/section/Component.jsx +36 -0
- package/src/section/ComponentLayout.jsx +44 -0
- package/src/section/DashboardLayout.jsx +50 -0
- package/src/section/DynamicCreate.jsx +25 -3
- package/src/section/DynamicEdit.jsx +28 -5
- package/src/section/DynamicLayout.jsx +21 -0
- package/src/section/DynamicList.jsx +74 -38
- package/src/section/DynamicShow.jsx +33 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yaml-admin-front",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "React components for yaml-admin front (library)",
|
|
@@ -10,11 +10,14 @@
|
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@iconify/react": "^6.0.0",
|
|
13
|
+
"@mui/x-tree-view": "^8.11.2",
|
|
14
|
+
"apexcharts": "^5.3.5",
|
|
13
15
|
"axios": "^1.11.0",
|
|
14
16
|
"js-yaml": "^4.1.0",
|
|
15
17
|
"ra-data-json-server": "^5.10.1",
|
|
16
18
|
"react": "^19.1.1",
|
|
17
19
|
"react-admin": "^5.10.1",
|
|
20
|
+
"react-apexcharts": "^1.7.0",
|
|
18
21
|
"react-dom": "^19.1.1",
|
|
19
22
|
"yaml": "^2.8.1"
|
|
20
23
|
},
|
package/src/YMLAdmin.jsx
CHANGED
|
@@ -13,6 +13,7 @@ import { AdminProvider } from './AdminContext';
|
|
|
13
13
|
import authProvider from './login/authProvider';
|
|
14
14
|
import { setApiHost } from './common/axios';
|
|
15
15
|
import fileUploader from './common/fileUploader';
|
|
16
|
+
import DashboardLayout from './section/DashboardLayout';
|
|
16
17
|
|
|
17
18
|
const httpClient = (url, options = {}) => {
|
|
18
19
|
if (!options.headers) {
|
|
@@ -67,7 +68,7 @@ const YMLAdmin = ({ adminYaml, i18nProvider, custom, theme, layout }) => {
|
|
|
67
68
|
{dataProvider && <AdminProvider initialYml={yml} width="1250px">
|
|
68
69
|
<Admin
|
|
69
70
|
theme={{...defaultTheme, ...theme}}
|
|
70
|
-
dashboard={undefined}
|
|
71
|
+
dashboard={yml?.front?.dashboard ? DashboardLayout : undefined}
|
|
71
72
|
layout={layout || MyLayout}
|
|
72
73
|
authProvider={authProvider}
|
|
73
74
|
i18nProvider={i18nProvider}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
- type: body
|
|
3
|
+
crud : list
|
|
4
|
+
entity: item
|
|
5
|
+
filter:
|
|
6
|
+
- name: region_id
|
|
7
|
+
value: $arg0
|
|
8
|
+
sort:
|
|
9
|
+
- name: seq
|
|
10
|
+
desc: false
|
|
11
|
+
* @param {*} action
|
|
12
|
+
* @param {*} args
|
|
13
|
+
* @returns
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const act = (action, args, {navigate}) => {
|
|
17
|
+
if (!action || !action.type) return action
|
|
18
|
+
if (action.type === 'body') {
|
|
19
|
+
let { crud, entity, filter, sort } = action
|
|
20
|
+
let url = `/${entity}`
|
|
21
|
+
|
|
22
|
+
const filterObject = {}
|
|
23
|
+
if (Array.isArray(filter)) {
|
|
24
|
+
filter.forEach((f) => {
|
|
25
|
+
if (!f || !f.name) return
|
|
26
|
+
let value = f.value
|
|
27
|
+
if (typeof value === 'string' && value.startsWith('$arg')) {
|
|
28
|
+
const index = parseInt(value.replace('$arg', ''), 10)
|
|
29
|
+
if (Array.isArray(args)) value = args[index]
|
|
30
|
+
else if (args && typeof args === 'object') value = args[index]
|
|
31
|
+
}
|
|
32
|
+
filterObject[f.name] = value
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const params = new URLSearchParams()
|
|
37
|
+
if (Object.keys(filterObject).length > 0) {
|
|
38
|
+
params.set('filter', JSON.stringify(filterObject))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// sort: 첫 번째 정렬 조건만 사용 (react-admin과 호환: sort, order)
|
|
42
|
+
if (Array.isArray(sort) && sort.length > 0 && sort[0]?.name) {
|
|
43
|
+
params.set('sort', sort[0].name)
|
|
44
|
+
params.set('order', sort[0].desc ? 'DESC' : 'ASC')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const qs = params.toString()
|
|
48
|
+
const fullUrl = qs ? `${url}?${qs}` : url
|
|
49
|
+
|
|
50
|
+
if (typeof navigate === 'function') {
|
|
51
|
+
navigate(fullUrl)
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
return fullUrl
|
|
55
|
+
}
|
|
56
|
+
return action
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { act }
|
package/src/common/field.jsx
CHANGED
|
@@ -1,90 +1,221 @@
|
|
|
1
1
|
import {
|
|
2
2
|
TextField, NumberField, ReferenceField, DateField, BooleanField,
|
|
3
3
|
ReferenceInput, AutocompleteInput, TextInput,
|
|
4
|
-
SelectInput, FunctionField, ImageInput, ImageField, FileInput, FileField
|
|
4
|
+
SelectInput, FunctionField, ImageInput, ImageField, FileInput, FileField,
|
|
5
|
+
ArrayInput, ArrayField, SingleFieldList, Datagrid, SimpleFormIterator, BooleanInput,
|
|
6
|
+
DateInput, NumberInput,
|
|
5
7
|
} from 'react-admin';
|
|
6
8
|
import { Avatar } from '@mui/material';
|
|
7
9
|
import ClickableImageField from '../component/ClickableImageField';
|
|
8
10
|
import SafeImageField from '../component/SafeImageField';
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
/**
|
|
13
|
+
*
|
|
14
|
+
* @param {*} field example {{
|
|
15
|
+
"name": "lock_list",
|
|
16
|
+
"label": "mylabel",
|
|
17
|
+
"type": "array",
|
|
18
|
+
"fields": [
|
|
19
|
+
{
|
|
20
|
+
"name": "member_no",
|
|
21
|
+
"type": "reference",
|
|
22
|
+
"reference_entity": "member",
|
|
23
|
+
"reference_match": "member_no",
|
|
24
|
+
"reference_name": "name"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "reg_date",
|
|
28
|
+
"type": "date"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}}
|
|
32
|
+
* @param {*} field_path example "lock_list.member_no"
|
|
33
|
+
* @returns example {{
|
|
34
|
+
"name": "member_no",
|
|
35
|
+
"type": "reference",
|
|
36
|
+
"reference_entity": "member",
|
|
37
|
+
"reference_match": "member_no",
|
|
38
|
+
"reference_name": "name"
|
|
39
|
+
}}
|
|
40
|
+
*/
|
|
41
|
+
const findChildField = (field, field_path) => {
|
|
42
|
+
let field_path_array = field_path.split('.')
|
|
43
|
+
if (field_path_array.length == 1)
|
|
44
|
+
return field
|
|
45
|
+
else {
|
|
46
|
+
let child_field_name = field_path_array[1]
|
|
47
|
+
let field_path_rest = field_path_array.slice(1).join('.')
|
|
48
|
+
return findChildField(field.fields.find(f => f.name == child_field_name), field_path_rest)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const getFieldShow = ({ field, isList, crud_field }) => {
|
|
53
|
+
let label = crud_field?.label || field.label
|
|
11
54
|
if (!field || field.type == 'password') return null;
|
|
12
|
-
if (field.type == 'string' || field.key){
|
|
13
|
-
return <TextField key={field.name} label={
|
|
55
|
+
if (field.type == 'string' || field.key) {
|
|
56
|
+
return <TextField key={field.name} label={label} source={field.name} />
|
|
14
57
|
} else if (field.type == 'integer')
|
|
15
|
-
return <NumberField key={field.name} label={
|
|
58
|
+
return <NumberField key={field.name} label={label} source={field.name} />
|
|
59
|
+
else if (field.type == 'length')
|
|
60
|
+
return <FunctionField key={field.name} label={label} render={record =>
|
|
61
|
+
<>
|
|
62
|
+
{record[field.name]?.length}
|
|
63
|
+
</>
|
|
64
|
+
} />
|
|
16
65
|
else if (field.type == 'select')
|
|
17
|
-
return <FunctionField key={field.name} label={
|
|
66
|
+
return <FunctionField key={field.name} label={label} source={field.name}
|
|
18
67
|
render={record => field.select_values.find(m => m.name == record[field.name])?.label} />
|
|
19
68
|
else if (field.type == 'reference')
|
|
20
|
-
return <ReferenceField key={field.name} link="show" label={
|
|
21
|
-
<TextField source={field.reference_name} />
|
|
69
|
+
return <ReferenceField key={field.name} link="show" label={label} source={field.name} reference={field.reference_entity}>
|
|
70
|
+
{!field.reference_format && <TextField source={field.reference_name} />}
|
|
71
|
+
{field.reference_format && (() => {
|
|
72
|
+
// Extract field names from the format string
|
|
73
|
+
// e.g. "${name}(${phone})(${user_type})" => ['name', 'phone', 'user_type']
|
|
74
|
+
const matches = [...field.reference_format.matchAll(/\$\{(\w+)\}/g)];
|
|
75
|
+
const fieldNames = matches.map(m => m[1]);
|
|
76
|
+
// Build a label string for TextField
|
|
77
|
+
// e.g. "${name}(${phone})(${user_type})" => "{name}({phone})({user_type})"
|
|
78
|
+
// We'll use FunctionField to render the formatted string
|
|
79
|
+
return (
|
|
80
|
+
<FunctionField
|
|
81
|
+
render={record => {
|
|
82
|
+
let str = field.reference_format;
|
|
83
|
+
fieldNames.forEach(fn => {
|
|
84
|
+
str = str.replace(`\$\{${fn}\}`, record?.[fn] ?? '');
|
|
85
|
+
});
|
|
86
|
+
return str;
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
})()
|
|
91
|
+
}
|
|
22
92
|
</ReferenceField>
|
|
23
93
|
else if (field.type == 'date')
|
|
24
|
-
return <DateField key={field.name} label={
|
|
94
|
+
return <DateField key={field.name} label={label} source={field.name} showTime={field.showtime} />
|
|
25
95
|
else if (field.type == 'boolean')
|
|
26
|
-
return <BooleanField key={field.name} label={
|
|
96
|
+
return <BooleanField key={field.name} label={label} source={field.name} />
|
|
27
97
|
else if (field.type == 'objectId')
|
|
28
|
-
return <TextField key={field.name} label={
|
|
29
|
-
else if (field.type == '
|
|
30
|
-
|
|
98
|
+
return <TextField key={field.name} label={label} source={field.name} />
|
|
99
|
+
else if (field.type == 'array') {
|
|
100
|
+
if (crud_field?.name?.includes('.')) {
|
|
101
|
+
let child_field = findChildField(field, crud_field.name)
|
|
102
|
+
return <ArrayField key={field.name} source={field.name} label={label}>
|
|
103
|
+
<SingleFieldList linkType={false}>
|
|
104
|
+
{getFieldShow({ field: child_field, isList })}
|
|
105
|
+
</SingleFieldList>
|
|
106
|
+
</ArrayField>
|
|
107
|
+
} else {
|
|
108
|
+
if (isList)
|
|
109
|
+
return <FunctionField key={field.name} label={label} render={record =>
|
|
110
|
+
record?.[field.name]?.length || 0
|
|
111
|
+
} />
|
|
112
|
+
else {
|
|
113
|
+
return <ArrayField label={label} source={field.name} >
|
|
114
|
+
<Datagrid bulkActionButtons={false} rowClick={false}>
|
|
115
|
+
{field.fields.map(m => getFieldShow({ field: m, isList }))}
|
|
116
|
+
</Datagrid>
|
|
117
|
+
</ArrayField>
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} else if (field.type == 'file') {
|
|
121
|
+
return <FunctionField key={field.name} label={label} render={record =>
|
|
31
122
|
<a href={record?.[field.name]?.image_preview} target="_blank">{record?.[field.name]?.title || 'Download'}</a>
|
|
32
123
|
} />
|
|
33
124
|
} else if (field.type == 'image') {
|
|
34
|
-
if(field.avatar)
|
|
35
|
-
return <FunctionField label={
|
|
36
|
-
<Avatar alt="Natacha" src={record[field.name].image_preview}
|
|
37
|
-
|
|
125
|
+
if (field.avatar)
|
|
126
|
+
return <FunctionField label={label} render={record =>
|
|
127
|
+
<Avatar alt="Natacha" src={record[field.name].image_preview}
|
|
128
|
+
sx={isList ? { width: 100, height: 100 } : { width: 256, height: 256 }} />
|
|
38
129
|
} />
|
|
39
|
-
else
|
|
40
|
-
return <ClickableImageField key={field.name} label={
|
|
41
|
-
width={isList ? "100px" : "200px"} height={isList ? "100px" : "200px"}/>
|
|
42
|
-
}
|
|
130
|
+
else
|
|
131
|
+
return <ClickableImageField key={field.name} label={label} source={field.name}
|
|
132
|
+
width={isList ? "100px" : "200px"} height={isList ? "100px" : "200px"} />
|
|
133
|
+
}
|
|
43
134
|
else
|
|
44
|
-
return <TextField key={field.name} label={
|
|
135
|
+
return <TextField key={field.name} label={label} source={field.name} />
|
|
45
136
|
}
|
|
46
137
|
|
|
47
138
|
const required = (message = 'ra.validation.required') =>
|
|
48
139
|
value => value ? undefined : message;
|
|
49
140
|
const validateRequire = [required()];
|
|
50
141
|
|
|
51
|
-
export const getFieldEdit = (field, search = false,
|
|
142
|
+
export const getFieldEdit = ({field, search = false, globalFilter = {}, label = null, crud_field}) => {
|
|
52
143
|
if (!field)
|
|
53
144
|
return null;
|
|
54
145
|
const { type, autogenerate } = field
|
|
55
146
|
if (autogenerate && !search) return null
|
|
56
|
-
|
|
147
|
+
|
|
57
148
|
if (type == 'reference') {
|
|
58
|
-
return <ReferenceInput key={field.name} label={field?.label} source={field.name} reference={field?.reference_entity}
|
|
59
|
-
alwaysOn={
|
|
149
|
+
return <ReferenceInput key={field.name} label={field?.label} source={field.name} reference={field?.reference_entity}
|
|
150
|
+
alwaysOn={globalFilter[field.name] ? false : true}
|
|
151
|
+
filter={globalFilter}
|
|
152
|
+
defaultValue={crud_field?.default}
|
|
60
153
|
>
|
|
61
154
|
<AutocompleteInput sx={{ width: '300px' }} label={field?.label} optionText={field?.reference_name}
|
|
62
|
-
filterToQuery={(searchText) => ({ [field?.reference_name || 'q']: searchText })}
|
|
155
|
+
filterToQuery={(searchText) => ({ [field?.reference_name || 'q']: searchText })}
|
|
63
156
|
validate={field.required && !search && validateRequire}
|
|
64
|
-
defaultValue={
|
|
65
|
-
|
|
157
|
+
defaultValue={globalFilter[field.name]}
|
|
158
|
+
/>
|
|
66
159
|
</ReferenceInput>
|
|
67
160
|
} else if (field?.type == 'select')
|
|
68
161
|
return <SelectInput key={field.name} label={field?.label} source={field.name} alwaysOn
|
|
69
162
|
choices={field?.select_values}
|
|
70
163
|
optionText="label" optionValue="name"
|
|
71
164
|
validate={field.required && !search && validateRequire}
|
|
165
|
+
defaultValue={crud_field?.default}
|
|
166
|
+
/>
|
|
167
|
+
else if (field?.type == 'integer') {
|
|
168
|
+
return <NumberInput key={field.name} label={field?.label} source={field.name} alwaysOn
|
|
169
|
+
validate={field.required && !search && validateRequire}
|
|
170
|
+
defaultValue={crud_field?.default}
|
|
72
171
|
/>
|
|
172
|
+
}
|
|
73
173
|
else if (field?.type == 'image') {
|
|
74
|
-
return <ImageInput key={field.name} source={field.name} label={field.label} accept="image/*" placeholder={<p>{field.label}</p>}
|
|
174
|
+
return <ImageInput key={field.name} source={field.name} label={label || field.label} accept="image/*" placeholder={<p>{field.label}</p>}
|
|
75
175
|
validate={field.required && !search && validateRequire}>
|
|
76
176
|
<SafeImageField source={'src'} title={'title'} />
|
|
77
177
|
</ImageInput>
|
|
78
178
|
}
|
|
79
179
|
else if (field?.type == 'file') {
|
|
80
|
-
return <FileInput key={field.name} source={field.name} placeholder={<p>{field.label}</p>}
|
|
81
|
-
|
|
82
|
-
<FileField source="src" title="title"/>
|
|
180
|
+
return <FileInput key={field.name} source={field.name} placeholder={<p>{field.label}</p>}
|
|
181
|
+
validate={field.required && !search && validateRequire}>
|
|
182
|
+
<FileField source="src" title="title" />
|
|
83
183
|
</FileInput>
|
|
84
184
|
}
|
|
85
|
-
else {
|
|
86
|
-
return <
|
|
185
|
+
else if (field?.type == 'boolean') {
|
|
186
|
+
return <BooleanInput key={field.name} label={field?.label} source={field.name} alwaysOn
|
|
187
|
+
validate={field.required && !search && validateRequire}
|
|
188
|
+
defaultValue={crud_field?.default}
|
|
189
|
+
/>
|
|
190
|
+
}
|
|
191
|
+
else if (field?.type == 'date') {
|
|
192
|
+
return <DateInput key={field.name} label={field?.label} source={field.name} alwaysOn
|
|
193
|
+
showTime={field.showtime}
|
|
87
194
|
validate={field.required && !search && validateRequire}
|
|
195
|
+
defaultValue={crud_field?.default}
|
|
196
|
+
/>
|
|
197
|
+
}
|
|
198
|
+
else if (field.type == 'array') {
|
|
199
|
+
return (<ArrayInput key={field.name} source={field.name} label={field.label} alwaysOn>
|
|
200
|
+
<SimpleFormIterator>
|
|
201
|
+
{field.fields && field.fields.map(subField => {
|
|
202
|
+
// recursively call getFieldEdit to render the sub fields
|
|
203
|
+
return getFieldEdit({
|
|
204
|
+
field:subField,
|
|
205
|
+
search,
|
|
206
|
+
globalFilter,
|
|
207
|
+
label:subField.label,
|
|
208
|
+
crud_field //TODO : crud_field should be child of the field
|
|
209
|
+
})
|
|
210
|
+
})}
|
|
211
|
+
</SimpleFormIterator>
|
|
212
|
+
</ArrayInput>
|
|
213
|
+
);
|
|
214
|
+
} else {
|
|
215
|
+
return <TextInput key={field.name} label={field?.label} source={field.name} alwaysOn
|
|
216
|
+
required={!search && field?.type != 'password' && field.required}
|
|
217
|
+
validate={field.required && field?.type != 'password' && !search && validateRequire}
|
|
218
|
+
defaultValue={crud_field?.default}
|
|
88
219
|
/>
|
|
89
220
|
}
|
|
90
221
|
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useCallback, useState, useRef } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
useRefresh,
|
|
4
|
+
} from 'react-admin';
|
|
5
|
+
import { useLocation, useNavigate } from 'react-router-dom';
|
|
6
|
+
import { useAdminContext } from '../AdminContext';
|
|
7
|
+
import { postFetcher, fetcher } from '../common/axios.jsx';
|
|
8
|
+
import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView';
|
|
9
|
+
import { TreeItem } from '@mui/x-tree-view/TreeItem';
|
|
10
|
+
import { Box, Paper } from '@mui/material';
|
|
11
|
+
import { act } from '../common/actionParser';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {object} component
|
|
15
|
+
* {
|
|
16
|
+
"component": "tree",
|
|
17
|
+
"entity": "region",
|
|
18
|
+
"key": "id",
|
|
19
|
+
"parent_key": "parent_id",
|
|
20
|
+
"label": "name"
|
|
21
|
+
}
|
|
22
|
+
* @param {*} component
|
|
23
|
+
* @returns
|
|
24
|
+
*/
|
|
25
|
+
export const EntityTreeView = ({ component, custom, ...props }) => {
|
|
26
|
+
const navigate = useNavigate()
|
|
27
|
+
const refresh = useRefresh();
|
|
28
|
+
const yml = useAdminContext();
|
|
29
|
+
const [list, setList] = useState([])
|
|
30
|
+
const fetchedKeysRef = useRef(new Set())
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
let {entity, key, parent_key, label, sort} = component
|
|
34
|
+
const custon_filter = custom?.globalFilterDelegate(entity) || {}
|
|
35
|
+
let url = `/${entity}?${parent_key}=`
|
|
36
|
+
if(custon_filter) {
|
|
37
|
+
url += `&${Object.keys(custon_filter).map(key => `${key}=${custon_filter[key]}`).join('&')}`
|
|
38
|
+
}
|
|
39
|
+
if(sort) {
|
|
40
|
+
url += `&_sort=${sort.map(s => `${s.name}`).join(',')}`
|
|
41
|
+
url += `&_order=${sort.map(s => `${s.desc ? 'DESC' : 'ASC'}`).join(',')}`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fetcher(url).then(res => {
|
|
45
|
+
|
|
46
|
+
if(Array.isArray(res)) {
|
|
47
|
+
setList(res)
|
|
48
|
+
res.map(m=>{
|
|
49
|
+
fetchChild(m)
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
}, [component, custom])
|
|
54
|
+
|
|
55
|
+
const findNode = useCallback((node, targetKeyValue) => {
|
|
56
|
+
if(node[component.key] == targetKeyValue) {
|
|
57
|
+
return node
|
|
58
|
+
}
|
|
59
|
+
if(Array.isArray(node.list)) {
|
|
60
|
+
for(let n of node.list) {
|
|
61
|
+
let r = findNode(n, targetKeyValue)
|
|
62
|
+
if(r) {
|
|
63
|
+
return r
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null
|
|
68
|
+
}, [component])
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
const updateNodeChildren = useCallback((nodes, targetKeyValue, children) => {
|
|
72
|
+
if(!Array.isArray(nodes)) return nodes
|
|
73
|
+
return nodes.map(node => {
|
|
74
|
+
if(node[component.key] === targetKeyValue) {
|
|
75
|
+
return { ...node, list: children }
|
|
76
|
+
}
|
|
77
|
+
if(Array.isArray(node.list)) {
|
|
78
|
+
const updated = updateNodeChildren(node.list, targetKeyValue, children)
|
|
79
|
+
if(updated !== node.list) {
|
|
80
|
+
return { ...node, list: updated }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return node
|
|
84
|
+
})
|
|
85
|
+
}, [component])
|
|
86
|
+
|
|
87
|
+
const fetchChild = useCallback((item) => {
|
|
88
|
+
let {entity, key, parent_key, label, sort} = component
|
|
89
|
+
const custon_filter = custom?.globalFilterDelegate(entity) || {}
|
|
90
|
+
let key_value = item[component.key]
|
|
91
|
+
if(fetchedKeysRef.current.has(key_value)) return
|
|
92
|
+
let url = `/${entity}?${parent_key}=${key_value}`
|
|
93
|
+
if(custon_filter) {
|
|
94
|
+
url += `&${Object.keys(custon_filter).map(key => `${key}=${custon_filter[key]}`).join('&')}`
|
|
95
|
+
}
|
|
96
|
+
if(sort) {
|
|
97
|
+
url += `&_sort=${sort.map(s => `${s.name}`).join(',')}`
|
|
98
|
+
url += `&_order=${sort.map(s => `${s.desc ? 'DESC' : 'ASC'}`).join(',')}`
|
|
99
|
+
}
|
|
100
|
+
fetcher(url).then(res => {
|
|
101
|
+
let children = Array.isArray(res) ? res : []
|
|
102
|
+
// remove self references and duplicates
|
|
103
|
+
children = children.filter(c => c?.[component.key] !== key_value)
|
|
104
|
+
setList(prev => updateNodeChildren(prev, key_value, children))
|
|
105
|
+
fetchedKeysRef.current.add(key_value)
|
|
106
|
+
// recursively prefetch deeper children
|
|
107
|
+
children.forEach(child => fetchChild(child))
|
|
108
|
+
})
|
|
109
|
+
}, [component, custom, updateNodeChildren])
|
|
110
|
+
|
|
111
|
+
const itemClick = useCallback((event, nodeId) => {
|
|
112
|
+
let theNode = findNode({list}, nodeId)
|
|
113
|
+
let isPeer = !theNode.list || theNode.list.length == 0
|
|
114
|
+
if(isPeer) {
|
|
115
|
+
if(component.peer_click?.action) {
|
|
116
|
+
let args = []
|
|
117
|
+
component.argment.forEach(arg => {
|
|
118
|
+
args.push(theNode[arg.name])
|
|
119
|
+
})
|
|
120
|
+
for(let action of component.peer_click.action) {
|
|
121
|
+
act(action, args, {
|
|
122
|
+
navigate
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
}, [component, custom, list, findNode])
|
|
129
|
+
|
|
130
|
+
const renderTree = (item, visited) => {
|
|
131
|
+
const id = item[component.key]
|
|
132
|
+
if(visited?.has(id)) return null
|
|
133
|
+
const nextVisited = visited ? new Set(visited) : new Set()
|
|
134
|
+
nextVisited.add(id)
|
|
135
|
+
return (
|
|
136
|
+
<TreeItem key={id} itemId={`${id}`} label={<span >{item[component.label]}</span>} >
|
|
137
|
+
{item.list?.map(child => renderTree(child, nextVisited))}
|
|
138
|
+
</TreeItem>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<Box sx={{ minHeight: 352, minWidth: 250 }}>
|
|
144
|
+
<SimpleTreeView onItemClick={itemClick}>
|
|
145
|
+
{list.map(item => {
|
|
146
|
+
return (
|
|
147
|
+
renderTree(item)
|
|
148
|
+
)
|
|
149
|
+
})}
|
|
150
|
+
</SimpleTreeView>
|
|
151
|
+
</Box>
|
|
152
|
+
)
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
export default EntityTreeView;
|
package/src/layout/MyMenu.jsx
CHANGED
|
@@ -21,7 +21,7 @@ const MyMenu = () => {
|
|
|
21
21
|
let r = yml.entity[m]
|
|
22
22
|
r.name = m
|
|
23
23
|
return r
|
|
24
|
-
}).filter(f=>f.category == m.name)
|
|
24
|
+
}).filter(f=>f.category == m.name && f.hidden !== true)
|
|
25
25
|
})
|
|
26
26
|
return list
|
|
27
27
|
}, [yml]);
|
|
@@ -31,13 +31,13 @@ const MyMenu = () => {
|
|
|
31
31
|
let r = yml.entity[m]
|
|
32
32
|
r.name = m
|
|
33
33
|
return r
|
|
34
|
-
}).filter(f=>!f.category)
|
|
34
|
+
}).filter(f=>!f.category && f.hidden !== true)
|
|
35
35
|
return list || [];
|
|
36
36
|
}, [yml]);
|
|
37
37
|
|
|
38
38
|
return (
|
|
39
39
|
<Menu>
|
|
40
|
-
{
|
|
40
|
+
{yml?.front?.dashboard && <Menu.DashboardItem />}
|
|
41
41
|
|
|
42
42
|
{noCartegoryList.map(m => <Menu.ResourceItem key={m.name} name={m.name} />)}
|
|
43
43
|
{categoryList.map(c => {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React, { useMemo, useState, useCallback, useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
useRefresh,
|
|
4
|
+
} from 'react-admin';
|
|
5
|
+
|
|
6
|
+
import { useNavigate } from 'react-router-dom';
|
|
7
|
+
import { useAdminContext } from '../AdminContext';
|
|
8
|
+
import Chart from "react-apexcharts";
|
|
9
|
+
import { fetcher } from '../common/axios';
|
|
10
|
+
|
|
11
|
+
export const ComponentLayout = ({ component, custom, ...props }) => {
|
|
12
|
+
const navigate = useNavigate()
|
|
13
|
+
const refresh = useRefresh();
|
|
14
|
+
const yml = useAdminContext();
|
|
15
|
+
const [data, setData] = useState(null);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
fetcher(`/api/chart/${component.id}`).then(res => {
|
|
19
|
+
setData(res);
|
|
20
|
+
});
|
|
21
|
+
}, [component.id]);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div>
|
|
25
|
+
{data && <Chart
|
|
26
|
+
height={component.height || 300}
|
|
27
|
+
options={data.options}
|
|
28
|
+
series={data.series}
|
|
29
|
+
type={component?.type}
|
|
30
|
+
/>}
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
export default ComponentLayout;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React, { useMemo, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
useRefresh,
|
|
4
|
+
} from 'react-admin';
|
|
5
|
+
|
|
6
|
+
import { useNavigate } from 'react-router-dom';
|
|
7
|
+
import { useAdminContext } from '../AdminContext';
|
|
8
|
+
import { Box, Grid, Card, CardContent, CardHeader } from '@mui/material';
|
|
9
|
+
import Component from './Component';
|
|
10
|
+
import { useTheme } from '@mui/material/styles';
|
|
11
|
+
import useMediaQuery from '@mui/material/useMediaQuery';
|
|
12
|
+
|
|
13
|
+
// 컨테이너 근처에서
|
|
14
|
+
|
|
15
|
+
//Custom Import Start
|
|
16
|
+
|
|
17
|
+
//Custom Import End
|
|
18
|
+
|
|
19
|
+
export const ComponentLayout = ({ components, custom, ...props }) => {
|
|
20
|
+
const navigate = useNavigate()
|
|
21
|
+
const refresh = useRefresh();
|
|
22
|
+
const yml = useAdminContext();
|
|
23
|
+
const theme = useTheme();
|
|
24
|
+
const mdUp = useMediaQuery(theme.breakpoints.up('md'));
|
|
25
|
+
return (
|
|
26
|
+
<Box padding={2} sx={{width:1200}}>
|
|
27
|
+
<Grid container spacing={2}>
|
|
28
|
+
{components?.map((component, index) => {
|
|
29
|
+
return <Grid item key={index} size={component.size || 4} >
|
|
30
|
+
<Card>
|
|
31
|
+
<CardHeader title={component.label} />
|
|
32
|
+
<CardContent>
|
|
33
|
+
<Component component={component} />
|
|
34
|
+
</CardContent>
|
|
35
|
+
</Card>
|
|
36
|
+
</Grid>
|
|
37
|
+
})}
|
|
38
|
+
</Grid>
|
|
39
|
+
</Box>
|
|
40
|
+
)
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
export default ComponentLayout;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React, { useMemo, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
AutocompleteInput,
|
|
4
|
+
ChipField,
|
|
5
|
+
Datagrid,
|
|
6
|
+
DateField,
|
|
7
|
+
EditButton,
|
|
8
|
+
Filter,
|
|
9
|
+
FunctionField,
|
|
10
|
+
Show,
|
|
11
|
+
SimpleShowLayout,
|
|
12
|
+
NumberField,
|
|
13
|
+
ReferenceArrayField,
|
|
14
|
+
ReferenceField,
|
|
15
|
+
ReferenceInput,
|
|
16
|
+
SaveButton,
|
|
17
|
+
SelectInput,
|
|
18
|
+
SingleFieldList,
|
|
19
|
+
TextField,
|
|
20
|
+
TextInput,
|
|
21
|
+
Toolbar,
|
|
22
|
+
useRecordContext,
|
|
23
|
+
useRefresh,
|
|
24
|
+
useResourceContext,
|
|
25
|
+
BooleanField,
|
|
26
|
+
} from 'react-admin';
|
|
27
|
+
|
|
28
|
+
import { useNavigate } from 'react-router-dom';
|
|
29
|
+
import { useAdminContext } from '../AdminContext';
|
|
30
|
+
import { getFieldShow } from '../common/field';
|
|
31
|
+
import ComponentLayout from "./ComponentLayout";
|
|
32
|
+
//Custom Import Start
|
|
33
|
+
|
|
34
|
+
//Custom Import End
|
|
35
|
+
|
|
36
|
+
export const DashboardLayout = ({ custom, ...props }) => {
|
|
37
|
+
const navigate = useNavigate()
|
|
38
|
+
const refresh = useRefresh();
|
|
39
|
+
const yml = useAdminContext();
|
|
40
|
+
|
|
41
|
+
// Custom List Code Start
|
|
42
|
+
|
|
43
|
+
//Custom List Code End
|
|
44
|
+
return (
|
|
45
|
+
<ComponentLayout components={yml?.front?.dashboard} />
|
|
46
|
+
)
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
export default DashboardLayout;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import { useEffect, useMemo } from 'react';
|
|
2
|
+
import { useEffect, useMemo, useCallback } from 'react';
|
|
3
3
|
import {
|
|
4
4
|
AutocompleteInput,
|
|
5
5
|
Create,
|
|
@@ -41,6 +41,21 @@ export const DynamicCreate = ({custom, ...props}) => {
|
|
|
41
41
|
return yml.entity[resource].fields
|
|
42
42
|
}, [yml, resource])
|
|
43
43
|
|
|
44
|
+
const api_generate = useMemo(() => {
|
|
45
|
+
return yml.entity[resource].api_generate || {}
|
|
46
|
+
}, [yml, resource])
|
|
47
|
+
|
|
48
|
+
const checkApiGenerateContain = useCallback((name) => {
|
|
49
|
+
if(!api_generate)
|
|
50
|
+
return true;
|
|
51
|
+
if(api_generate[name])
|
|
52
|
+
return false;
|
|
53
|
+
if(name.includes('.') && api_generate[name.split('.')[0]]) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}, [api_generate])
|
|
58
|
+
|
|
44
59
|
const crud = useMemo(() => {
|
|
45
60
|
return yml.entity[resource].crud || {
|
|
46
61
|
show: true,
|
|
@@ -64,8 +79,15 @@ export const DynamicCreate = ({custom, ...props}) => {
|
|
|
64
79
|
|
|
65
80
|
//Custom Create SimpleForm Property End
|
|
66
81
|
>
|
|
67
|
-
{fields.filter(field => crud.create == true || crud.create.map(a=>a.name).includes(field.name) )
|
|
68
|
-
|
|
82
|
+
{fields.filter(field => crud.create == true || crud.create.map(a=>a.name).includes(field.name) )
|
|
83
|
+
//exclude field by api_generate
|
|
84
|
+
.filter(field => checkApiGenerateContain(field.name))
|
|
85
|
+
.map(field => {
|
|
86
|
+
return getFieldEdit({field,
|
|
87
|
+
search:false,
|
|
88
|
+
globalFilter:custom?.globalFilterDelegate(resource),
|
|
89
|
+
crud_field:crud.create == true ? null : crud.create.find(a=>a.name == field.name)
|
|
90
|
+
})
|
|
69
91
|
})}
|
|
70
92
|
|
|
71
93
|
{/* Custom Create Start */}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import { useEffect, useMemo } from 'react';
|
|
2
|
+
import { useEffect, useMemo, useCallback } from 'react';
|
|
3
3
|
import {
|
|
4
4
|
AutocompleteInput,
|
|
5
5
|
Edit,
|
|
@@ -33,7 +33,7 @@ const EditToolbar = props => (
|
|
|
33
33
|
);
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
export const DynamicEdit = props => {
|
|
36
|
+
export const DynamicEdit = ({custom, ...props}) => {
|
|
37
37
|
const { permissions } = usePermissions();
|
|
38
38
|
const yml = useAdminContext();
|
|
39
39
|
const resource = useResourceContext(props);
|
|
@@ -42,6 +42,10 @@ export const DynamicEdit = props => {
|
|
|
42
42
|
return yml.entity[resource].fields
|
|
43
43
|
}, [yml, resource])
|
|
44
44
|
|
|
45
|
+
const api_generate = useMemo(() => {
|
|
46
|
+
return yml.entity[resource].api_generate || {}
|
|
47
|
+
}, [yml, resource])
|
|
48
|
+
|
|
45
49
|
const crud = useMemo(() => {
|
|
46
50
|
return yml.entity[resource].crud || {
|
|
47
51
|
show: true,
|
|
@@ -54,12 +58,23 @@ export const DynamicEdit = props => {
|
|
|
54
58
|
}
|
|
55
59
|
}, [yml, resource])
|
|
56
60
|
|
|
61
|
+
const checkApiGenerateContain = useCallback((name) => {
|
|
62
|
+
if(!api_generate)
|
|
63
|
+
return true;
|
|
64
|
+
if(api_generate[name])
|
|
65
|
+
return false;
|
|
66
|
+
if(name.includes('.') && api_generate[name.split('.')[0]]) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}, [api_generate])
|
|
71
|
+
|
|
57
72
|
//Custom Create Code Start
|
|
58
73
|
|
|
59
74
|
//Custom Create Code End
|
|
60
75
|
|
|
61
76
|
return (
|
|
62
|
-
<Edit title={<DynamicTitle />} {...props} mutationMode='optimistic' redirect="
|
|
77
|
+
<Edit title={<DynamicTitle />} {...props} mutationMode='optimistic' redirect="edit"
|
|
63
78
|
//Custom Create Property Start
|
|
64
79
|
|
|
65
80
|
//Custom Create Property End
|
|
@@ -69,8 +84,16 @@ export const DynamicEdit = props => {
|
|
|
69
84
|
|
|
70
85
|
//Custom Create SimpleForm Property End
|
|
71
86
|
>
|
|
72
|
-
{fields.filter(field =>
|
|
73
|
-
|
|
87
|
+
{fields.filter(field => crud.edit == true || crud.edit.map(a=>a.name).includes(field.name))
|
|
88
|
+
//exclude field by api_generate
|
|
89
|
+
.filter(field => checkApiGenerateContain(field.name))
|
|
90
|
+
.map(field => {
|
|
91
|
+
return getFieldEdit({
|
|
92
|
+
field,
|
|
93
|
+
search:false,
|
|
94
|
+
globalFilter:custom?.globalFilterDelegate(resource) || {},
|
|
95
|
+
crud_field:crud.edit == true ? null : crud.edit.find(a=>a.name == field.name)
|
|
96
|
+
})
|
|
74
97
|
})}
|
|
75
98
|
|
|
76
99
|
{/* Custom Create Start */}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
import { Box, Stack, Grid } from '@mui/material';
|
|
3
|
+
import EntityTreeView from '../component/EntityTreeView';
|
|
4
|
+
|
|
5
|
+
const DynamicLayout = ({ entity, custom, children }) => {
|
|
6
|
+
return (
|
|
7
|
+
<Stack direction={'row'} spacing={1}>
|
|
8
|
+
{entity.layout?.left && <Box padding={1}>
|
|
9
|
+
{entity.layout.left.map((component, index) => {
|
|
10
|
+
return <EntityTreeView key={index} component={component} custom={custom} />
|
|
11
|
+
})}
|
|
12
|
+
</Box>}
|
|
13
|
+
<Box width={'100%'}>
|
|
14
|
+
{children}
|
|
15
|
+
</Box>
|
|
16
|
+
</Stack>
|
|
17
|
+
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default DynamicLayout;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import DownloadIcon from '@mui/icons-material/Download';
|
|
3
3
|
import UploadIcon from '@mui/icons-material/Upload';
|
|
4
4
|
import moment from 'moment';
|
|
5
|
-
import React, { useMemo } from 'react';
|
|
5
|
+
import React, { useMemo, useCallback } from 'react';
|
|
6
6
|
import {
|
|
7
7
|
Button,
|
|
8
8
|
CreateButton,
|
|
@@ -22,7 +22,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
|
|
22
22
|
import { useAdminContext } from '../AdminContext';
|
|
23
23
|
import { postFetcher } from '../common/axios.jsx';
|
|
24
24
|
import { getFieldEdit, getFieldShow } from '../common/field';
|
|
25
|
-
|
|
25
|
+
import DynamicLayout from './DynamicLayout';
|
|
26
26
|
//Custom Import Start
|
|
27
27
|
|
|
28
28
|
//Custom Import End
|
|
@@ -39,7 +39,7 @@ const EditToolbar = props => (
|
|
|
39
39
|
</Toolbar>
|
|
40
40
|
);
|
|
41
41
|
|
|
42
|
-
const DynamicFilter = ({
|
|
42
|
+
const DynamicFilter = ({ custom, ...props }) => {
|
|
43
43
|
const yml = useAdminContext();
|
|
44
44
|
const resource = useResourceContext(props);
|
|
45
45
|
const yml_entity = useMemo(() => {
|
|
@@ -51,7 +51,12 @@ const DynamicFilter = ({defaultValueByFieldName, ...props}) => {
|
|
|
51
51
|
{
|
|
52
52
|
yml_entity.crud?.search?.map(m => {
|
|
53
53
|
const field = yml_entity.fields.find(f => f.name == m.name)
|
|
54
|
-
return getFieldEdit(
|
|
54
|
+
return getFieldEdit({
|
|
55
|
+
field,
|
|
56
|
+
search:true,
|
|
57
|
+
globalFilter:custom?.globalFilterDelegate(resource) || {},
|
|
58
|
+
crud_field:m
|
|
59
|
+
})
|
|
55
60
|
})
|
|
56
61
|
}
|
|
57
62
|
{
|
|
@@ -63,7 +68,7 @@ const DynamicFilter = ({defaultValueByFieldName, ...props}) => {
|
|
|
63
68
|
)
|
|
64
69
|
};
|
|
65
70
|
|
|
66
|
-
const ListActions = ({crud, custom, ...props}) => {
|
|
71
|
+
const ListActions = ({ crud, custom, ...props }) => {
|
|
67
72
|
const resource = useResourceContext(props);
|
|
68
73
|
const fileInputRef = React.createRef();
|
|
69
74
|
const notify = useNotify();
|
|
@@ -71,17 +76,17 @@ const ListActions = ({crud, custom, ...props}) => {
|
|
|
71
76
|
const location = useLocation()
|
|
72
77
|
|
|
73
78
|
const convertFileToBase64 = async file => {
|
|
74
|
-
|
|
75
|
-
if(file){
|
|
79
|
+
|
|
80
|
+
if (file) {
|
|
76
81
|
const arrayBuffer = await file.arrayBuffer(); // ArrayBuffer 얻기
|
|
77
82
|
const uint8Array = new Uint8Array(arrayBuffer); // Uint8Array로 변환
|
|
78
|
-
|
|
83
|
+
|
|
79
84
|
// Uint8Array를 문자열로 변환
|
|
80
85
|
const binaryString = uint8Array.reduce((acc, byte) => acc + String.fromCharCode(byte), '');
|
|
81
|
-
|
|
86
|
+
|
|
82
87
|
// Base64 인코딩
|
|
83
88
|
return btoa(binaryString);
|
|
84
|
-
} else
|
|
89
|
+
} else {
|
|
85
90
|
return null
|
|
86
91
|
}
|
|
87
92
|
};
|
|
@@ -90,7 +95,7 @@ const ListActions = ({crud, custom, ...props}) => {
|
|
|
90
95
|
const file = files[0];
|
|
91
96
|
|
|
92
97
|
const base64 = await convertFileToBase64(file)
|
|
93
|
-
await postFetcher(`/excel/${resource}/import`, {}, {base64}).then(res => {
|
|
98
|
+
await postFetcher(`/excel/${resource}/import`, {}, { base64 }).then(res => {
|
|
94
99
|
if (res && res.r) {
|
|
95
100
|
notify(
|
|
96
101
|
res.msg,
|
|
@@ -125,13 +130,13 @@ const ListActions = ({crud, custom, ...props}) => {
|
|
|
125
130
|
//url에서 filter paremeters를 가져와서 export
|
|
126
131
|
const params = new URLSearchParams(location.search); // Query String 파싱
|
|
127
132
|
let filter = params.get("filter")
|
|
128
|
-
if(filter)
|
|
133
|
+
if (filter)
|
|
129
134
|
filter = JSON.parse(filter)
|
|
130
135
|
else
|
|
131
136
|
filter = {}
|
|
132
137
|
const globalFilter = custom?.globalFilterDelegate(resource)
|
|
133
138
|
let mergedFilter = {}
|
|
134
|
-
if(globalFilter) {
|
|
139
|
+
if (globalFilter) {
|
|
135
140
|
mergedFilter = { ...filter, ...globalFilter }
|
|
136
141
|
}
|
|
137
142
|
postFetcher(`/excel/${resource}/export`, {}, { filter: mergedFilter }).then(r => {
|
|
@@ -172,14 +177,14 @@ const ListActions = ({crud, custom, ...props}) => {
|
|
|
172
177
|
style={{ display: 'none' }}
|
|
173
178
|
onChange={e => handleImportFiles(e.target.files)}
|
|
174
179
|
/>
|
|
175
|
-
<Button onClick={handleImportClick} startIcon={<UploadIcon />} label='Import'/>
|
|
180
|
+
<Button onClick={handleImportClick} startIcon={<UploadIcon />} label='Import' />
|
|
176
181
|
</>}
|
|
177
|
-
{crud?.export && <Button onClick={handleExportClick} startIcon={<DownloadIcon />} label='Export'/>}
|
|
182
|
+
{crud?.export && <Button onClick={handleExportClick} startIcon={<DownloadIcon />} label='Export' />}
|
|
178
183
|
</TopToolbar>
|
|
179
184
|
);
|
|
180
185
|
};
|
|
181
186
|
|
|
182
|
-
export const DynamicList = ({custom, ...props}) => {
|
|
187
|
+
export const DynamicList = ({ custom, ...props }) => {
|
|
183
188
|
const navigate = useNavigate()
|
|
184
189
|
const refresh = useRefresh();
|
|
185
190
|
const yml = useAdminContext();
|
|
@@ -201,37 +206,68 @@ export const DynamicList = ({custom, ...props}) => {
|
|
|
201
206
|
return yml.entity[resource].fields
|
|
202
207
|
}, [yml, resource])
|
|
203
208
|
|
|
209
|
+
const findField = useCallback((name) => {
|
|
210
|
+
let name_array = name.split('.')[0]
|
|
211
|
+
let r = fields.find(f => f.name == name_array)
|
|
212
|
+
if(!r)
|
|
213
|
+
r = fields.find(f => f.name == name)
|
|
214
|
+
return r;
|
|
215
|
+
}, [fields])
|
|
216
|
+
|
|
217
|
+
const shouldShowFields = useCallback((name) => {
|
|
218
|
+
|
|
219
|
+
if (fields.map(a => a.name).includes(name))
|
|
220
|
+
return true
|
|
221
|
+
|
|
222
|
+
return findField(name) != null
|
|
223
|
+
|
|
224
|
+
return false
|
|
225
|
+
|
|
226
|
+
}, [fields])
|
|
204
227
|
//Custom List Code Start
|
|
205
228
|
|
|
206
229
|
//Custom List Code End
|
|
207
230
|
return (
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
//Custom List Action End
|
|
217
|
-
>
|
|
218
|
-
{
|
|
219
|
-
//Custom List Body Start
|
|
231
|
+
<DynamicLayout entity={yml.entity[resource]} custom={custom}>
|
|
232
|
+
<List {...props} filters={<DynamicFilter custom={custom} />} mutationMode='optimistic'
|
|
233
|
+
exporter={false}
|
|
234
|
+
sort={{ field: 'id', order: 'DESC' }}
|
|
235
|
+
perPage={30}
|
|
236
|
+
actions={<ListActions crud={crud} custom={custom} />}
|
|
237
|
+
filter={custom?.globalFilterDelegate(resource) || {}}
|
|
238
|
+
//Custom List Action Start
|
|
220
239
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
<Datagrid rowClick="show" bulkActionButtons={true}>
|
|
240
|
+
//Custom List Action End
|
|
241
|
+
>
|
|
224
242
|
{
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
243
|
+
//Custom List Body Start
|
|
244
|
+
|
|
245
|
+
//Custom List Body End
|
|
228
246
|
}
|
|
247
|
+
<Datagrid rowClick={crud.show ? "show" : false}
|
|
248
|
+
bulkActionButtons={crud.delete ? true : false}
|
|
249
|
+
>
|
|
250
|
+
{crud.list == true && fields.map(m => {
|
|
251
|
+
return getFieldShow({
|
|
252
|
+
field: m,
|
|
253
|
+
isList: true
|
|
254
|
+
})
|
|
255
|
+
})}
|
|
256
|
+
{crud.list != true && crud.list.filter(f => f.name).filter(f => shouldShowFields(f.name)).map(crud_field => {
|
|
257
|
+
let m = findField(crud_field.name)
|
|
258
|
+
return getFieldShow({
|
|
259
|
+
crud_field,
|
|
260
|
+
field: m,
|
|
261
|
+
isList: true
|
|
262
|
+
})
|
|
263
|
+
})}
|
|
229
264
|
//Custom List Start
|
|
230
265
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
266
|
+
//Custom List End
|
|
267
|
+
{crud.edit && <EditButton />}
|
|
268
|
+
</Datagrid>
|
|
269
|
+
</List>
|
|
270
|
+
</DynamicLayout>
|
|
235
271
|
)
|
|
236
272
|
};
|
|
237
273
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import React, { useMemo } from 'react';
|
|
1
|
+
import React, { useMemo, useCallback } from 'react';
|
|
3
2
|
import {
|
|
4
3
|
AutocompleteInput,
|
|
5
4
|
ChipField,
|
|
@@ -49,17 +48,17 @@ const ShowContent = ({ customFunc }) => {
|
|
|
49
48
|
)
|
|
50
49
|
};
|
|
51
50
|
|
|
52
|
-
export const DynamicShow = ({custom, ...props}) => {
|
|
51
|
+
export const DynamicShow = ({ custom, ...props }) => {
|
|
53
52
|
const navigate = useNavigate()
|
|
54
53
|
const refresh = useRefresh();
|
|
55
54
|
const yml = useAdminContext();
|
|
56
|
-
const resource = useResourceContext(props);
|
|
57
|
-
|
|
55
|
+
const resource = useResourceContext(props);
|
|
56
|
+
|
|
58
57
|
const fields = useMemo(() => {
|
|
59
58
|
return yml.entity[resource].fields
|
|
60
59
|
}, [yml, resource])
|
|
61
60
|
|
|
62
|
-
const customFunc = useMemo(()=> {
|
|
61
|
+
const customFunc = useMemo(() => {
|
|
63
62
|
return custom?.entity?.[resource]?.show
|
|
64
63
|
}, [yml, resource])
|
|
65
64
|
|
|
@@ -75,6 +74,22 @@ export const DynamicShow = ({custom, ...props}) => {
|
|
|
75
74
|
}
|
|
76
75
|
}, [yml, resource])
|
|
77
76
|
|
|
77
|
+
const findField = useCallback((name) => {
|
|
78
|
+
let name_array = name.split('.')[0]
|
|
79
|
+
let r = fields.find(f => f.name == name_array)
|
|
80
|
+
return r;
|
|
81
|
+
}, [fields])
|
|
82
|
+
|
|
83
|
+
const shouldShowFields = useCallback((name) => {
|
|
84
|
+
|
|
85
|
+
if (fields.map(a => a.name).includes(name))
|
|
86
|
+
return true
|
|
87
|
+
|
|
88
|
+
return findField(name) != null
|
|
89
|
+
|
|
90
|
+
return false
|
|
91
|
+
|
|
92
|
+
}, [fields])
|
|
78
93
|
// Custom List Code Start
|
|
79
94
|
|
|
80
95
|
//Custom List Code End
|
|
@@ -82,8 +97,18 @@ export const DynamicShow = ({custom, ...props}) => {
|
|
|
82
97
|
<Show title={<DynamicTitle />} {...props} >
|
|
83
98
|
<SimpleShowLayout>
|
|
84
99
|
{customFunc && <ShowContent customFunc={customFunc} fields={fields} />}
|
|
85
|
-
{!customFunc &&
|
|
86
|
-
return getFieldShow(
|
|
100
|
+
{!customFunc && crud.show == true && fields.map(m => {
|
|
101
|
+
return getFieldShow({
|
|
102
|
+
field: m,
|
|
103
|
+
})
|
|
104
|
+
})}
|
|
105
|
+
|
|
106
|
+
{!customFunc && crud.show != true && crud.show.filter(f => f.name).filter(f => shouldShowFields(f.name)).map(crud_field => {
|
|
107
|
+
let m = findField(crud_field.name)
|
|
108
|
+
return getFieldShow({
|
|
109
|
+
crud_field,
|
|
110
|
+
field: m,
|
|
111
|
+
})
|
|
87
112
|
})}
|
|
88
113
|
//Custom Show Start
|
|
89
114
|
|