yaml-admin-front 0.0.3 → 0.0.4
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 -2
- package/src/YMLAdmin.jsx +85 -28
- package/src/common/axios.jsx +187 -31
- package/src/common/field.jsx +77 -0
- package/src/common/fileUploader.jsx +81 -0
- package/src/component/ClickableImageField.jsx +155 -0
- package/src/component/SafeImageField.jsx +116 -0
- package/src/layout/MyAppBar.jsx +0 -1
- package/src/layout/MyMenu.jsx +5 -5
- package/src/layout/SubMenu.jsx +2 -2
- package/src/login/authprovider.jsx +32 -31
- package/src/section/DynamicCreate.jsx +90 -0
- package/src/section/DynamicEdit.jsx +90 -0
- package/src/section/DynamicList.jsx +230 -0
- package/src/section/DynamicShow.jsx +85 -0
- package/src/common/client.jsx +0 -78
package/package.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yaml-admin-front",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "React components for yaml-admin front (library)",
|
|
6
6
|
"main": "src/index.js",
|
|
7
|
-
"scripts": {
|
|
7
|
+
"scripts": {
|
|
8
|
+
"publish": "npm publish -w yaml-admin-front --access public"
|
|
9
|
+
},
|
|
8
10
|
"dependencies": {
|
|
9
11
|
"@iconify/react": "^6.0.0",
|
|
10
12
|
"axios": "^1.11.0",
|
package/src/YMLAdmin.jsx
CHANGED
|
@@ -1,27 +1,61 @@
|
|
|
1
|
-
import { Admin, Resource, ListGuesser } from "react-admin";
|
|
1
|
+
import { Admin, Resource, ListGuesser, CreateBase, fetchUtils, CustomRoutes } from "react-admin";
|
|
2
2
|
import jsonServerProvider from "ra-data-json-server";
|
|
3
|
+
import { Route } from "react-router-dom";
|
|
3
4
|
import YAML from 'yaml';
|
|
4
|
-
import LoginPage from './login/LoginPage';
|
|
5
5
|
import MyLayout from './layout/MyLayout'
|
|
6
|
+
import DynamicList from './section/DynamicList';
|
|
7
|
+
import DynamicCreate from './section/DynamicCreate';
|
|
8
|
+
import DynamicEdit from './section/DynamicEdit';
|
|
9
|
+
import DynamicShow from './section/DynamicShow';
|
|
6
10
|
import { useState, useEffect } from 'react';
|
|
7
11
|
import { Icon } from '@iconify/react';
|
|
8
12
|
import { AdminProvider } from './AdminContext';
|
|
9
13
|
import authProvider from './login/authProvider';
|
|
10
|
-
import { setApiHost} from './common/axios';
|
|
14
|
+
import { setApiHost } from './common/axios';
|
|
15
|
+
import fileUploader from './common/fileUploader';
|
|
11
16
|
|
|
12
|
-
const
|
|
13
|
-
|
|
17
|
+
const httpClient = (url, options = {}) => {
|
|
18
|
+
if (!options.headers) {
|
|
19
|
+
options.headers = new Headers({ Accept: 'application/json' });
|
|
20
|
+
}
|
|
21
|
+
const token = localStorage.getItem('token');
|
|
22
|
+
options.headers.set('x-access-token', token);
|
|
23
|
+
return fetchUtils.fetchJson(url, options);
|
|
24
|
+
}
|
|
14
25
|
|
|
15
|
-
const YMLAdmin = ({ adminYaml }) => {
|
|
26
|
+
const YMLAdmin = ({ adminYaml, i18nProvider, custom }) => {
|
|
16
27
|
const [yml, setYml] = useState(null);
|
|
28
|
+
const [dataProvider, setDataProvider] = useState(null);
|
|
29
|
+
|
|
17
30
|
useEffect(() => {
|
|
18
31
|
const loadYamlFile = async () => {
|
|
19
32
|
try {
|
|
20
33
|
const json = YAML.parse(adminYaml);
|
|
21
34
|
setYml(json);
|
|
22
|
-
|
|
35
|
+
const api_host = json['api-host'].uri;
|
|
36
|
+
const privateEntityMap = {}
|
|
37
|
+
Object.entries(json.entity).map(([key, val])=>{
|
|
38
|
+
val.fields.map((field)=>{
|
|
39
|
+
if(field.private) {
|
|
40
|
+
privateEntityMap[key] = {
|
|
41
|
+
...privateEntityMap[key],
|
|
42
|
+
[field.name]: field.private
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if(json.upload?.local) {
|
|
49
|
+
setDataProvider(fileUploader(jsonServerProvider(api_host, httpClient), true, privateEntityMap));
|
|
50
|
+
} else if(json.upload?.s3) {
|
|
51
|
+
setDataProvider(fileUploader(jsonServerProvider(api_host, httpClient), false, privateEntityMap));
|
|
52
|
+
} else {
|
|
53
|
+
setDataProvider(jsonServerProvider(api_host, httpClient));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setApiHost(api_host);
|
|
23
57
|
} catch (error) {
|
|
24
|
-
console.error('YAML
|
|
58
|
+
console.error('YAML file load error', error);
|
|
25
59
|
}
|
|
26
60
|
};
|
|
27
61
|
|
|
@@ -29,26 +63,49 @@ const YMLAdmin = ({ adminYaml }) => {
|
|
|
29
63
|
}, []);
|
|
30
64
|
|
|
31
65
|
return (
|
|
32
|
-
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
<>
|
|
67
|
+
{dataProvider && <AdminProvider initialYml={yml} width="1250px">
|
|
68
|
+
<Admin
|
|
69
|
+
dashboard={undefined}
|
|
70
|
+
layout={MyLayout}
|
|
71
|
+
authProvider={authProvider}
|
|
72
|
+
i18nProvider={i18nProvider}
|
|
73
|
+
dataProvider={dataProvider}>
|
|
74
|
+
{yml?.entity && Object.keys(yml.entity).map(name => {
|
|
75
|
+
const entity = yml.entity[name];
|
|
76
|
+
const IconComponent = entity?.icon
|
|
77
|
+
? () => <Icon icon={entity.icon} width="1.25rem" height="1.25rem" />
|
|
78
|
+
: undefined;
|
|
79
|
+
|
|
80
|
+
if (entity.custom)
|
|
81
|
+
return <Resource key={name} name={name} options={{ label: entity.label }} icon={IconComponent}/>
|
|
82
|
+
else
|
|
83
|
+
return (
|
|
84
|
+
<Resource key={name} name={name}
|
|
85
|
+
options={{ label: entity.label }}
|
|
86
|
+
icon={IconComponent}
|
|
87
|
+
list={(props => <DynamicList {...props} custom={custom} />)}
|
|
88
|
+
create={(props => <DynamicCreate {...props} custom={custom} />)}
|
|
89
|
+
edit={(props => <DynamicEdit {...props} custom={custom} />)}
|
|
90
|
+
show={(props => <DynamicShow {...props} custom={custom} />)}
|
|
91
|
+
/>
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
})}
|
|
95
|
+
|
|
96
|
+
{/* <CustomRoutes>
|
|
97
|
+
{yml?.entity && Object.keys(yml.entity).map(name => {
|
|
98
|
+
const entity = yml.entity[name];
|
|
99
|
+
if (entity.custom)
|
|
100
|
+
return (
|
|
101
|
+
<Route path={`/${name}`} element={customEntity(name, 'entire')} />
|
|
102
|
+
)
|
|
103
|
+
})}
|
|
104
|
+
</CustomRoutes> */}
|
|
105
|
+
</Admin>
|
|
106
|
+
</AdminProvider>
|
|
107
|
+
}
|
|
108
|
+
</>
|
|
52
109
|
)
|
|
53
110
|
};
|
|
54
111
|
|
package/src/common/axios.jsx
CHANGED
|
@@ -4,6 +4,14 @@ import axios from 'axios';
|
|
|
4
4
|
|
|
5
5
|
const axiosInstance = axios.create({});
|
|
6
6
|
|
|
7
|
+
export const updateToken = async (query) => {
|
|
8
|
+
if (query && query.token) {
|
|
9
|
+
sessionStorage.setItem('token', query.token);
|
|
10
|
+
localStorage.setItem('token', query.token);
|
|
11
|
+
axios.defaults.headers.common['x-access-token'] = `${query.token}`;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
7
15
|
export const setApiHost = (host) => {
|
|
8
16
|
let base = host ?? import.meta.env.VITE_HOST_API ?? 'http://localhost:6911';
|
|
9
17
|
if (base && !base.startsWith('http')) {
|
|
@@ -24,6 +32,7 @@ export default axiosInstance;
|
|
|
24
32
|
|
|
25
33
|
// ----------------------------------------------------------------------
|
|
26
34
|
|
|
35
|
+
|
|
27
36
|
export const fetcher = async (args) => {
|
|
28
37
|
const [url, config] = Array.isArray(args) ? args : [args];
|
|
29
38
|
|
|
@@ -33,46 +42,193 @@ export const fetcher = async (args) => {
|
|
|
33
42
|
};
|
|
34
43
|
|
|
35
44
|
export const postFetcher = async (url, config, param) => {
|
|
45
|
+
if (!url.startsWith('http'))
|
|
46
|
+
url = `${import.meta.env.VITE_HOST_API}${url}`;
|
|
36
47
|
const res = await axiosInstance.post(url, param, { ...config });
|
|
37
48
|
|
|
38
49
|
return res.data;
|
|
39
50
|
};
|
|
40
51
|
|
|
41
|
-
export const putFetcher = async (url, config, param) => {
|
|
42
|
-
const res = await axiosInstance.put(url, param, { ...config });
|
|
43
|
-
|
|
44
|
-
return res.data;
|
|
45
|
-
};
|
|
46
52
|
|
|
47
53
|
export const uploadFile = (blob_path, file_name) => new Promise((resolve, reject) => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
54
|
+
const reader = new FileReader();
|
|
55
|
+
reader.readAsArrayBuffer(blob_path);
|
|
56
|
+
reader.onload = async () => {
|
|
57
|
+
const ext = file_name.substring(file_name.lastIndexOf('.') + 1, file_name.length);
|
|
58
|
+
const res = await fetcher(`/api/media/url/put/${ext}`)
|
|
59
|
+
fetch(res.upload_url, {
|
|
60
|
+
method: "PUT",
|
|
61
|
+
headers: {
|
|
62
|
+
'Content-Type': res.contentType
|
|
63
|
+
},
|
|
64
|
+
body: reader.result
|
|
65
|
+
})
|
|
66
|
+
.then(res2 => {
|
|
67
|
+
resolve(res.key)
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
reader.onerror = reject;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export const uploadFileSecure = (blob_path, file_name) => new Promise((resolve, reject) => {
|
|
74
|
+
const reader = new FileReader();
|
|
75
|
+
reader.readAsArrayBuffer(blob_path);
|
|
76
|
+
reader.onload = async () => {
|
|
77
|
+
const ext = file_name.substring(file_name.lastIndexOf('.') + 1, file_name.length);
|
|
78
|
+
const res = await fetcher(`/api/media/url/secure/put/${ext}`)
|
|
79
|
+
fetch(res.upload_url, {
|
|
80
|
+
method: "PUT",
|
|
81
|
+
headers: {
|
|
82
|
+
'Content-Type': res.contentType
|
|
83
|
+
},
|
|
84
|
+
body: reader.result
|
|
85
|
+
}).then(res2 => {
|
|
86
|
+
resolve(res.key)
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
reader.onerror = reject;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export const uploadFileSecureProgress = async (file, onProgress) => {
|
|
93
|
+
const CHUNK_SIZE = 50 * 1024 * 1024; // 진행상황 업데이트 위해 분할크기 축소
|
|
94
|
+
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
|
|
95
|
+
let uploadedSize = 0;
|
|
96
|
+
|
|
97
|
+
const ext = file.name.split('.').pop();
|
|
98
|
+
|
|
99
|
+
// uploadId
|
|
100
|
+
let initRes
|
|
101
|
+
try {
|
|
102
|
+
initRes = await fetcher(`/api/media/url/secure/init/${ext}`);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
throw new Error("업로드 초기화 실패");
|
|
66
105
|
}
|
|
106
|
+
const { uploadId, key } = initRes;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const partRequests = Array.from({ length: totalChunks }, (_, i) =>
|
|
110
|
+
postFetcher(`/api/media/url/secure/part`, {}, { key, uploadId, partNumber: i + 1 })
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// part uploadUrl
|
|
114
|
+
const partResponses = await Promise.all(partRequests);
|
|
115
|
+
|
|
116
|
+
/*
|
|
117
|
+
* uploadedParts = { PartNumber: chunk index, ETag: ETag }
|
|
118
|
+
*/
|
|
119
|
+
const uploadedParts = [];
|
|
120
|
+
|
|
121
|
+
// 분할 업로드 진행상황 표시를 위해 순차 업로드
|
|
122
|
+
for (let partIndex = 0; partIndex < partResponses.length; partIndex++) {
|
|
123
|
+
const start = partIndex * CHUNK_SIZE;
|
|
124
|
+
const end = Math.min(start + CHUNK_SIZE, file.size);
|
|
125
|
+
const chunk = file.slice(start, end);
|
|
126
|
+
|
|
127
|
+
// eslint-disable-next-line no-await-in-loop
|
|
128
|
+
const uploadResponse = await fetchWithRetry(partResponses[partIndex].uploadUrl, { method: "PUT", body: chunk });
|
|
129
|
+
uploadedSize += chunk.size;
|
|
130
|
+
// 퍼센트 업데이트
|
|
131
|
+
onProgress(Math.round((uploadedSize / file.size) * 100));
|
|
132
|
+
|
|
133
|
+
const eTag = uploadResponse.headers.get("ETag");
|
|
134
|
+
uploadedParts.push({ PartNumber: partIndex + 1, ETag: eTag.replace(/"/g, '') });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 분할 업로드 완료, 파일 병합 요청
|
|
138
|
+
await postFetcher(`/api/media/url/secure/complete`, {}, { key, uploadId, parts: uploadedParts })
|
|
139
|
+
|
|
140
|
+
return key;
|
|
141
|
+
} catch (e) {
|
|
142
|
+
const abort = await postFetcher(`/api/media/url/secure/abort`, {}, { key, uploadId });
|
|
143
|
+
if (abort && abort.r) {
|
|
144
|
+
throw new Error("파일 업로드 실패");
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
throw new Error(`업로드 실패 key: ${key}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export const uploadFileWithBase64String = (base64string, file_name) => new Promise((resolve, reject) => {
|
|
153
|
+
const ext = file_name.substring(file_name.lastIndexOf('.') + 1, file_name.length);
|
|
154
|
+
fetcher(`/api/media/url/put/${ext}`).then(res => {
|
|
155
|
+
let byteString = base64string;
|
|
156
|
+
if (base64string.startsWith('data:'))
|
|
157
|
+
byteString = atob(base64string.split(',')[1]);
|
|
158
|
+
const ab = new ArrayBuffer(byteString.length);
|
|
159
|
+
const ia = new Uint8Array(ab);
|
|
160
|
+
for (let i = 0; i < byteString.length; i++) {
|
|
161
|
+
ia[i] = byteString.charCodeAt(i);
|
|
162
|
+
}
|
|
163
|
+
fetch(res.upload_url, {
|
|
164
|
+
method: "PUT",
|
|
165
|
+
headers: {
|
|
166
|
+
'Content-Type': res.contentType
|
|
167
|
+
},
|
|
168
|
+
body: ab
|
|
169
|
+
})
|
|
170
|
+
.then(res2 => {
|
|
171
|
+
resolve(res.key)
|
|
172
|
+
});
|
|
173
|
+
})
|
|
67
174
|
});
|
|
68
175
|
|
|
69
|
-
export const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
176
|
+
export const useGetFetcher = (path) => {
|
|
177
|
+
const url = path.startsWith('http') ? path : `${import.meta.env.VITE_HOST_API}${path}`
|
|
178
|
+
const { data, isLoading, error, isValidating } = useSWR(url, fetcher);
|
|
179
|
+
|
|
180
|
+
const memoizedValue = useMemo(
|
|
181
|
+
() => ({
|
|
182
|
+
data,
|
|
183
|
+
isLoading,
|
|
184
|
+
error,
|
|
185
|
+
isValidating
|
|
186
|
+
}),
|
|
187
|
+
[data, error, isLoading, isValidating]
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
return memoizedValue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function fetchWithRetry(url, options = {}, retries = 0) {
|
|
194
|
+
try {
|
|
195
|
+
return await fetch(url, options);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (retries < 3) {
|
|
198
|
+
console.log(`요청 실패. 재시도${retries + 1}회 : ${url}`);
|
|
199
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
200
|
+
return fetchWithRetry(url, options, retries + 1);
|
|
201
|
+
} else {
|
|
202
|
+
throw new Error("파일 업로드 실패");
|
|
203
|
+
}
|
|
77
204
|
}
|
|
205
|
+
}
|
|
206
|
+
export const uploadFileLocal = (blob_path, file_name) => new Promise((resolve, reject) => {
|
|
207
|
+
const reader = new FileReader();
|
|
208
|
+
reader.readAsArrayBuffer(blob_path);
|
|
209
|
+
reader.onload = async () => {
|
|
210
|
+
const ext = file_name.substring(file_name.lastIndexOf('.') + 1, file_name.length);
|
|
211
|
+
const url = `/api/local/media/upload?ext=${ext}&name=${encodeURIComponent(file_name)}`
|
|
212
|
+
const res = await axiosInstance.put(url, reader.result, {
|
|
213
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
214
|
+
transformRequest: [(data) => data]
|
|
215
|
+
});
|
|
216
|
+
resolve(res.data.key)
|
|
217
|
+
};
|
|
218
|
+
reader.onerror = reject;
|
|
78
219
|
});
|
|
220
|
+
|
|
221
|
+
export const uploadFileLocalSecure = (blob_path, file_name) => new Promise((resolve, reject) => {
|
|
222
|
+
const reader = new FileReader();
|
|
223
|
+
reader.readAsArrayBuffer(blob_path);
|
|
224
|
+
reader.onload = async () => {
|
|
225
|
+
const ext = file_name.substring(file_name.lastIndexOf('.') + 1, file_name.length);
|
|
226
|
+
const url = `/api/local/media/upload/secure?ext=${ext}&name=${encodeURIComponent(file_name)}`
|
|
227
|
+
const res = await axiosInstance.put(url, reader.result, {
|
|
228
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
229
|
+
transformRequest: [(data) => data]
|
|
230
|
+
});
|
|
231
|
+
resolve(res.data.key)
|
|
232
|
+
};
|
|
233
|
+
reader.onerror = reject;
|
|
234
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
TextField, NumberField, ReferenceField, DateField, BooleanField,
|
|
4
|
+
ReferenceInput, AutocompleteInput, TextInput,
|
|
5
|
+
SelectInput, FunctionField, ImageInput, ImageField,
|
|
6
|
+
} from 'react-admin';
|
|
7
|
+
import { Avatar } from '@mui/material';
|
|
8
|
+
import ClickableImageField from '../component/ClickableImageField';
|
|
9
|
+
import SafeImageField from '../component/SafeImageField';
|
|
10
|
+
|
|
11
|
+
export const getFieldShow = (field, isList = false) => {
|
|
12
|
+
if (!field || field.type == 'password') return null;
|
|
13
|
+
if (field.type == 'string' || field.key){
|
|
14
|
+
return <TextField key={field.name} label={field.label} source={field.name} />
|
|
15
|
+
} else if (field.type == 'integer')
|
|
16
|
+
return <NumberField key={field.name} label={field.label} source={field.name} />
|
|
17
|
+
else if (field.type == 'select')
|
|
18
|
+
return <FunctionField key={field.name} label={field.label} source={field.name}
|
|
19
|
+
render={record => field.select_values.find(m => field.name == record[field.name])?.label} />
|
|
20
|
+
else if (field.type == 'reference')
|
|
21
|
+
return <ReferenceField key={field.name} link="show" label={field.label} source={field.name} reference={field.reference_entity}>
|
|
22
|
+
<TextField source={field.reference_name} />
|
|
23
|
+
</ReferenceField>
|
|
24
|
+
else if (field.type == 'date')
|
|
25
|
+
return <DateField key={field.name} label={field.label} source={field.name} />
|
|
26
|
+
else if (field.type == 'boolean')
|
|
27
|
+
return <BooleanField key={field.name} label={field.label} source={field.name} />
|
|
28
|
+
else if (field.type == 'objectId')
|
|
29
|
+
return <TextField key={field.name} label={field.label} source={field.name} />
|
|
30
|
+
else if (field.type == 'image') {
|
|
31
|
+
if(field.avatar)
|
|
32
|
+
return <FunctionField label={field.label} render={record =>
|
|
33
|
+
<Avatar alt="Natacha" src={record[field.name].image_preview}
|
|
34
|
+
sx={isList ? {width: 100, height: 100} : {width: 256, height: 256}}/>
|
|
35
|
+
} />
|
|
36
|
+
else
|
|
37
|
+
return <ClickableImageField key={field.name} label={field.label} source={field.name}
|
|
38
|
+
width={isList ? "100px" : "200px"} height={isList ? "100px" : "200px"}/>
|
|
39
|
+
}
|
|
40
|
+
else
|
|
41
|
+
return <TextField key={field.name} label={field.label} source={field.name} />
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const required = (message = 'ra.validation.required') =>
|
|
45
|
+
value => value ? undefined : message;
|
|
46
|
+
const validateRequire = [required()];
|
|
47
|
+
|
|
48
|
+
export const getFieldEdit = (field, search = false) => {
|
|
49
|
+
if (!field)
|
|
50
|
+
return null;
|
|
51
|
+
const { type, autogenerate } = field
|
|
52
|
+
if (autogenerate && !search) return null
|
|
53
|
+
if (type == 'reference')
|
|
54
|
+
return <ReferenceInput key={field.name} label={field?.label} source={field.name} reference={field?.reference_entity} alwaysOn>
|
|
55
|
+
<AutocompleteInput sx={{ width: '300px' }} label={field?.label} optionText={field?.reference_name}
|
|
56
|
+
filterToQuery={(searchText) => ({ [field?.reference_name || 'q']: searchText })}
|
|
57
|
+
validate={field.required && !search && validateRequire}
|
|
58
|
+
/>
|
|
59
|
+
</ReferenceInput>
|
|
60
|
+
else if (field?.type == 'select')
|
|
61
|
+
return <SelectInput key={field.name} label={field?.label} source={field.name} alwaysOn
|
|
62
|
+
choices={field?.select_values}
|
|
63
|
+
optionText="name" optionValue="label"
|
|
64
|
+
validate={field.required && !search && validateRequire}
|
|
65
|
+
/>
|
|
66
|
+
else if (field?.type == 'image') {
|
|
67
|
+
return <ImageInput key={field.name} source={field.name} label={field.label} accept="image/*" placeholder={<p>{field.label}</p>}
|
|
68
|
+
validate={field.required && !search && validateRequire}>
|
|
69
|
+
<SafeImageField source={'src'} title={'title'} />
|
|
70
|
+
</ImageInput>
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
return <TextInput key={field.name} label={field?.label} source={field.name} alwaysOn
|
|
74
|
+
validate={field.required && !search && validateRequire}
|
|
75
|
+
/>
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { uploadFile, uploadFileLocal, uploadFileLocalSecure, uploadFileSecure } from '../common/axios'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* react-admin v5 uses a method-based dataProvider (getList/getOne/create/update/...).
|
|
5
|
+
* This wrapper intercepts create/update to upload any fields containing `rawFile`,
|
|
6
|
+
* replaces them with `{ src, title }`, and then delegates to the original provider.
|
|
7
|
+
*/
|
|
8
|
+
const fileUploader = (provider, isLocal = false, privateEntityMap = {}) => {
|
|
9
|
+
const isPlainObject = (value) => value && typeof value === 'object' && !Array.isArray(value);
|
|
10
|
+
const replaceFileField = async (entity_name, filed_name, value) => {
|
|
11
|
+
if (value && value.rawFile && value.title) {
|
|
12
|
+
let key
|
|
13
|
+
let isSecure = false
|
|
14
|
+
if(privateEntityMap[entity_name] && privateEntityMap[entity_name][filed_name]) {
|
|
15
|
+
isSecure = true
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if(isLocal) {
|
|
19
|
+
if(isSecure) {
|
|
20
|
+
key = await uploadFileLocalSecure(value.rawFile, value.title);
|
|
21
|
+
} else {
|
|
22
|
+
key = await uploadFileLocal(value.rawFile, value.title);
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
if(isSecure) {
|
|
26
|
+
key = await uploadFileSecure(value.rawFile, value.title);
|
|
27
|
+
} else {
|
|
28
|
+
key = await uploadFile(value.rawFile, value.title);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (key) {
|
|
33
|
+
const next = { ...value };
|
|
34
|
+
next.src = key;
|
|
35
|
+
delete next.rawFile;
|
|
36
|
+
return next;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const deepProcessData = async (entity_name, data) => {
|
|
43
|
+
if (Array.isArray(data)) {
|
|
44
|
+
const processed = await Promise.all(data.map(item => deepProcessData(entity_name, item)));
|
|
45
|
+
return processed;
|
|
46
|
+
}
|
|
47
|
+
if (!isPlainObject(data)) return data;
|
|
48
|
+
|
|
49
|
+
const entries = await Promise.all(Object.entries(data).map(async ([key, val]) => {
|
|
50
|
+
if (val && val.rawFile) {
|
|
51
|
+
const replaced = await replaceFileField(entity_name, key, val);
|
|
52
|
+
return [key, replaced];
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(val) || isPlainObject(val)) {
|
|
55
|
+
const nested = await deepProcessData(entity_name, val);
|
|
56
|
+
return [key, nested];
|
|
57
|
+
}
|
|
58
|
+
return [key, val];
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
return Object.fromEntries(entries);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
...provider,
|
|
66
|
+
async create(resource, params) {
|
|
67
|
+
const nextData = await deepProcessData(resource, params?.data ?? {});
|
|
68
|
+
return provider.create(resource, { ...params, data: nextData });
|
|
69
|
+
},
|
|
70
|
+
async update(resource, params) {
|
|
71
|
+
try {
|
|
72
|
+
const nextData = await deepProcessData(resource, params?.data ?? {});
|
|
73
|
+
return provider.update(resource, { ...params, data: nextData });
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.error('update error', e)
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export default fileUploader;
|