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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yaml-admin-front",
3
- "version": "0.0.19",
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 }
@@ -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
- export const getFieldShow = (field, isList = false) => {
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={field.label} source={field.name} />
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={field.label} source={field.name} />
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={field.label} source={field.name}
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={field.label} source={field.name} reference={field.reference_entity}>
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={field.label} source={field.name} showTime={field.showtime} />
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={field.label} source={field.name} />
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={field.label} source={field.name} />
29
- else if (field.type == 'file') {
30
- return <FunctionField key={field.name} label={field.label} render={record =>
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={field.label} render={record =>
36
- <Avatar alt="Natacha" src={record[field.name].image_preview}
37
- sx={isList ? {width: 100, height: 100} : {width: 256, height: 256}}/>
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={field.label} source={field.name}
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={field.label} source={field.name} />
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, defaultValueByFieldName = {}) => {
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={defaultValueByFieldName[field.name] ? false : true}
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={defaultValueByFieldName[field.name]}
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
- validate={field.required && !search && validateRequire}>
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 <TextInput key={field.name} label={field?.label} source={field.name} alwaysOn
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;
@@ -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
- {/* <Menu.DashboardItem /> */}
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) ).map(field => {
68
- return getFieldEdit(field, false, custom?.globalFilterDelegate(resource))
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="list"
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 => crud.edit == true || crud.edit.map(a=>a.name).includes(field.name) ).map(field => {
73
- return getFieldEdit(field)
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 = ({defaultValueByFieldName, ...props}) => {
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(field, true, defaultValueByFieldName)
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
- <List {...props} filters={<DynamicFilter defaultValueByFieldName={custom?.globalFilterDelegate(resource)}/>} mutationMode='optimistic'
209
- exporter={false}
210
- sort={{ field: 'id', order: 'DESC' }}
211
- perPage={30}
212
- actions={<ListActions crud={crud} custom={custom} />}
213
- filter={custom?.globalFilterDelegate(resource) || {}}
214
- //Custom List Action Start
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
- //Custom List Body End
222
- }
223
- <Datagrid rowClick="show" bulkActionButtons={true}>
240
+ //Custom List Action End
241
+ >
224
242
  {
225
- fields.filter(field => crud.list == true || crud.list.map(a=>a.name).includes(field.name) ).map(m => {
226
- return getFieldShow(m, true, custom?.globalFilterDelegate(resource))
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
- //Custom List End
232
- <EditButton />
233
- </Datagrid>
234
- </List>
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 && fields.filter(field => crud.show == true || crud.show.map(a=>a.name).includes(field.name) ).map(m=>{
86
- return getFieldShow(m)
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