trithuc-mvc-react 1.5.8 → 1.6.2
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/api/index.js +9 -0
- package/components/DataManagement/DataTable.jsx +10 -3
- package/components/DataManagement/EditorForm.jsx +15 -11
- package/components/DataManagement/FormField.jsx +15 -1
- package/components/DataManagement/upload/UploadAvatar.jsx +147 -0
- package/components/DataManagement/upload/UploadMultiFile.jsx +233 -0
- package/components/DataManagement/upload/UploadMultipleFile.jsx +60 -0
- package/components/DataManagement/upload/UploadSingleFile.jsx +129 -0
- package/package.json +1 -1
- package/utils/bytesToSize.js +11 -0
- package/utils/formFields.jsx +47 -0
- package/utils/index.js +32 -0
package/api/index.js
CHANGED
|
@@ -60,3 +60,12 @@ export const exportExcel = async ({ tableName,data }) => {
|
|
|
60
60
|
});
|
|
61
61
|
return res.data;
|
|
62
62
|
};
|
|
63
|
+
|
|
64
|
+
export const uploadFile = async (formData) => {
|
|
65
|
+
const res = await api.post("/Handler/fileUploader.ashx", formData, {
|
|
66
|
+
headers: {
|
|
67
|
+
"Content-Type": "multipart/form-data"
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
return res.data;
|
|
71
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Table, TableBody, TableCell, TableContainer, TableRow, useMediaQuery, useTheme } from "@mui/material";
|
|
2
2
|
import TablePaginationCustom from "../table/TablePagination";
|
|
3
|
-
import { useMemo, useState } from "react";
|
|
3
|
+
import { useEffect, useMemo, useState } from "react";
|
|
4
4
|
|
|
5
5
|
import { TableHead } from "./TableHead";
|
|
6
6
|
import { useMutation, useQuery, useQueryClient } from "react-query";
|
|
@@ -30,10 +30,11 @@ const DataTable = () => {
|
|
|
30
30
|
pageSize: rowsPerPage,
|
|
31
31
|
data: dataSearch
|
|
32
32
|
}),
|
|
33
|
-
keepPreviousData: true,
|
|
33
|
+
// keepPreviousData: true,
|
|
34
34
|
onSuccess: ({ PermissionModel, status }) => {
|
|
35
35
|
if (status) {
|
|
36
|
-
setPermission(PermissionModel);
|
|
36
|
+
// setPermission(PermissionModel);
|
|
37
|
+
console.log("LOAD LAI PermissionModel");
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
});
|
|
@@ -94,12 +95,18 @@ const DataTable = () => {
|
|
|
94
95
|
const { rows, total } = useMemo(() => {
|
|
95
96
|
let rows = data?.data ?? [];
|
|
96
97
|
let total = data?.total ?? 0;
|
|
98
|
+
|
|
97
99
|
return {
|
|
98
100
|
rows: rows,
|
|
99
101
|
total
|
|
100
102
|
};
|
|
101
103
|
}, [data]);
|
|
102
104
|
|
|
105
|
+
useEffect(()=>{
|
|
106
|
+
let PermissionModel = data?.PermissionModel;
|
|
107
|
+
PermissionModel&&setPermission(PermissionModel);
|
|
108
|
+
},[rows])
|
|
109
|
+
|
|
103
110
|
const handleChangePage = (event, newPage) => {
|
|
104
111
|
setPage(newPage);
|
|
105
112
|
};
|
|
@@ -15,14 +15,14 @@ import { yupResolver } from "@hookform/resolvers/yup";
|
|
|
15
15
|
EditorForm.propTypes = {
|
|
16
16
|
fields: PropTypes.array
|
|
17
17
|
};
|
|
18
|
-
function EditorForm({ fields,
|
|
18
|
+
function EditorForm({ fields, submitRef }) {
|
|
19
19
|
const queryClient = useQueryClient();
|
|
20
20
|
const { tableName, selectedEditItem, setOpenEditorDialog, validationSchema } = useDataTable();
|
|
21
21
|
|
|
22
22
|
const methods = useForm({ defaultValues: {}, resolver: yupResolver(validationSchema) });
|
|
23
23
|
const theme = useTheme();
|
|
24
24
|
const downXl = useMediaQuery(theme.breakpoints.down("xl"));
|
|
25
|
-
const elementSize = downXl ? "small": "medium";
|
|
25
|
+
const elementSize = downXl ? "small" : "medium";
|
|
26
26
|
useEffect(() => {
|
|
27
27
|
if (selectedEditItem) {
|
|
28
28
|
methods.setValue("Id", selectedEditItem.Id);
|
|
@@ -37,6 +37,8 @@ function EditorForm({ fields, submitRef }) {
|
|
|
37
37
|
methods.setValue(keyValueLabel, selectedEditItem[keyValueLabel]);
|
|
38
38
|
} else if (type === "date") {
|
|
39
39
|
methods.setValue(field, selectedEditItem[field]);
|
|
40
|
+
} else if (type === "file") {
|
|
41
|
+
methods.setValue(field, selectedEditItem[field] ? JSON.parse(selectedEditItem[field]) : []);
|
|
40
42
|
} else {
|
|
41
43
|
methods.setValue(field, selectedEditItem[field]);
|
|
42
44
|
}
|
|
@@ -67,20 +69,22 @@ function EditorForm({ fields, submitRef }) {
|
|
|
67
69
|
}
|
|
68
70
|
});
|
|
69
71
|
const onSubmit = (data) => {
|
|
70
|
-
fields
|
|
71
|
-
|
|
72
|
-
.forEach(({ field }) => {
|
|
72
|
+
fields.reduce((data, { type, field, datas, keyValueLabel, keyValue, keyLabel }) => {
|
|
73
|
+
if (type === "date") {
|
|
73
74
|
if (data[field]) {
|
|
74
75
|
data[field] = moment(data[field]).toDate();
|
|
75
76
|
}
|
|
76
|
-
})
|
|
77
|
-
fields
|
|
78
|
-
.filter(({ type }) => type === "autocomplete")
|
|
79
|
-
.forEach(({ field, datas, keyValueLabel, keyValue, keyLabel }) => {
|
|
77
|
+
} else if (type === "autocomplete") {
|
|
80
78
|
if (data[field] && !data[keyValueLabel] && keyValueLabel) {
|
|
81
79
|
data[keyValueLabel] = datas.find((item) => item[keyValue] == data[field])?.[keyLabel];
|
|
82
80
|
}
|
|
83
|
-
})
|
|
81
|
+
} else if (type === "file") {
|
|
82
|
+
if (data[field]) {
|
|
83
|
+
data[field] = JSON.stringify(data[field]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return data;
|
|
87
|
+
}, data);
|
|
84
88
|
|
|
85
89
|
saveMutation.mutate({
|
|
86
90
|
tableName,
|
|
@@ -103,7 +107,7 @@ function EditorForm({ fields, submitRef }) {
|
|
|
103
107
|
childrenFields,
|
|
104
108
|
datas,
|
|
105
109
|
loading = false,
|
|
106
|
-
onChange = () => {},
|
|
110
|
+
onChange = () => { },
|
|
107
111
|
keyLabel,
|
|
108
112
|
keyValue,
|
|
109
113
|
keyValueLabel,
|
|
@@ -19,6 +19,7 @@ import { DatePicker } from "@mui/x-date-pickers";
|
|
|
19
19
|
import { useCallback, useEffect, } from "react";
|
|
20
20
|
import moment from "moment/moment";
|
|
21
21
|
import { DEFAULT_DATE_FORMAT } from "../../constants";
|
|
22
|
+
import UploadMultipleFile from "./upload/UploadMultipleFile";
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
FormField.propTypes = {
|
|
@@ -235,7 +236,6 @@ function FormField({
|
|
|
235
236
|
}}
|
|
236
237
|
/>
|
|
237
238
|
);
|
|
238
|
-
|
|
239
239
|
case "trangThaiXuLy":
|
|
240
240
|
return (
|
|
241
241
|
<Controller
|
|
@@ -262,6 +262,20 @@ function FormField({
|
|
|
262
262
|
}}
|
|
263
263
|
/>
|
|
264
264
|
);
|
|
265
|
+
case "file":
|
|
266
|
+
return (
|
|
267
|
+
<Controller
|
|
268
|
+
name={name}
|
|
269
|
+
control={control}
|
|
270
|
+
render={({ field, fieldState: { error } }) => {
|
|
271
|
+
return (
|
|
272
|
+
<>
|
|
273
|
+
<UploadMultipleFile {...field} name="DinhKem" />
|
|
274
|
+
</>
|
|
275
|
+
);
|
|
276
|
+
}}
|
|
277
|
+
/>
|
|
278
|
+
);
|
|
265
279
|
}
|
|
266
280
|
}
|
|
267
281
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { isString } from 'lodash';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { Icon, addIcon } from '@iconify/react';
|
|
4
|
+
import { useDropzone } from 'react-dropzone';
|
|
5
|
+
import AddAPhotoIcon from "@mui/icons-material/AddAPhoto";
|
|
6
|
+
// material
|
|
7
|
+
import { alpha, styled } from '@mui/material/styles';
|
|
8
|
+
import { Box, Typography, Paper } from '@mui/material';
|
|
9
|
+
// utils
|
|
10
|
+
|
|
11
|
+
function fData(number) {
|
|
12
|
+
// return numeral(number).format("0.0 b");
|
|
13
|
+
return number;
|
|
14
|
+
}
|
|
15
|
+
// ----------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
const RootStyle = styled('div')(({ theme }) => ({
|
|
18
|
+
width: 144,
|
|
19
|
+
height: 144,
|
|
20
|
+
margin: 'auto',
|
|
21
|
+
borderRadius: '50%',
|
|
22
|
+
padding: theme.spacing(1),
|
|
23
|
+
border: `1px dashed ${theme.palette.grey[500_32]}`
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
const DropZoneStyle = styled('div')({
|
|
27
|
+
zIndex: 0,
|
|
28
|
+
width: '100%',
|
|
29
|
+
height: '100%',
|
|
30
|
+
outline: 'none',
|
|
31
|
+
display: 'flex',
|
|
32
|
+
overflow: 'hidden',
|
|
33
|
+
borderRadius: '50%',
|
|
34
|
+
position: 'relative',
|
|
35
|
+
alignItems: 'center',
|
|
36
|
+
justifyContent: 'center',
|
|
37
|
+
'& > *': { width: '100%', height: '100%' },
|
|
38
|
+
'&:hover': {
|
|
39
|
+
cursor: 'pointer',
|
|
40
|
+
'& .placeholder': {
|
|
41
|
+
zIndex: 9
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const PlaceholderStyle = styled('div')(({ theme }) => ({
|
|
47
|
+
display: 'flex',
|
|
48
|
+
position: 'absolute',
|
|
49
|
+
alignItems: 'center',
|
|
50
|
+
flexDirection: 'column',
|
|
51
|
+
justifyContent: 'center',
|
|
52
|
+
color: theme.palette.text.secondary,
|
|
53
|
+
backgroundColor: theme.palette.background.neutral,
|
|
54
|
+
transition: theme.transitions.create('opacity', {
|
|
55
|
+
easing: theme.transitions.easing.easeInOut,
|
|
56
|
+
duration: theme.transitions.duration.shorter
|
|
57
|
+
}),
|
|
58
|
+
'&:hover': { opacity: 0.72 }
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
// ------------------------------------------100/*/----------------------------
|
|
62
|
+
|
|
63
|
+
UploadAvatar.propTypes = {
|
|
64
|
+
error: PropTypes.bool,
|
|
65
|
+
file: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
|
66
|
+
caption: PropTypes.node,
|
|
67
|
+
sx: PropTypes.object
|
|
68
|
+
};
|
|
69
|
+
const ShowRejectionItems = ({ fileRejections }) => (
|
|
70
|
+
<Paper
|
|
71
|
+
variant="outlined"
|
|
72
|
+
sx={{
|
|
73
|
+
py: 1,
|
|
74
|
+
px: 2,
|
|
75
|
+
my: 2,
|
|
76
|
+
borderColor: "error.light",
|
|
77
|
+
bgcolor: (theme) => alpha(theme.palette.error.main, 0.08)
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
{fileRejections.map(({ file, errors }) => {
|
|
81
|
+
const { path, size } = file;
|
|
82
|
+
return (
|
|
83
|
+
<Box key={path} sx={{ my: 1 }}>
|
|
84
|
+
<Typography variant="subtitle2" noWrap>
|
|
85
|
+
{path} - {fData(size)}
|
|
86
|
+
</Typography>
|
|
87
|
+
{errors.map((e) => (
|
|
88
|
+
<Typography key={e.code} variant="caption" component="p">
|
|
89
|
+
- {e.message}
|
|
90
|
+
</Typography>
|
|
91
|
+
))}
|
|
92
|
+
</Box>
|
|
93
|
+
);
|
|
94
|
+
})}
|
|
95
|
+
</Paper>
|
|
96
|
+
);
|
|
97
|
+
export default function UploadAvatar({ error, file, caption, sx, ...other }) {
|
|
98
|
+
const { getRootProps, getInputProps, isDragActive, isDragReject, fileRejections } = useDropzone({
|
|
99
|
+
multiple: false,
|
|
100
|
+
...other
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
// console.log(file);
|
|
105
|
+
return (
|
|
106
|
+
<>
|
|
107
|
+
<RootStyle sx={sx}>
|
|
108
|
+
<DropZoneStyle
|
|
109
|
+
{...getRootProps()}
|
|
110
|
+
sx={{
|
|
111
|
+
...(isDragActive && { opacity: 0.72 }),
|
|
112
|
+
...((isDragReject || error) && {
|
|
113
|
+
color: "error.main",
|
|
114
|
+
borderColor: "error.light",
|
|
115
|
+
bgcolor: "error.lighter"
|
|
116
|
+
})
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
<input {...getInputProps()} />
|
|
120
|
+
|
|
121
|
+
{file && <Box component="img" alt="avatar" src={isString(file) ? file : file.preview} sx={{ zIndex: 8, objectFit: "cover" }} />}
|
|
122
|
+
|
|
123
|
+
<PlaceholderStyle
|
|
124
|
+
className="placeholder"
|
|
125
|
+
sx={{
|
|
126
|
+
...(file && {
|
|
127
|
+
opacity: 0,
|
|
128
|
+
color: "common.white",
|
|
129
|
+
bgcolor: "grey.900",
|
|
130
|
+
"&:hover": { opacity: 0.72 }
|
|
131
|
+
})
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
<Box icon={AddAPhotoIcon} sx={{ width: 24, height: 24, mb: 1 }} >
|
|
135
|
+
<AddAPhotoIcon />
|
|
136
|
+
</Box>
|
|
137
|
+
<Typography variant="caption">Chọn hình</Typography>
|
|
138
|
+
</PlaceholderStyle>
|
|
139
|
+
</DropZoneStyle>
|
|
140
|
+
</RootStyle>
|
|
141
|
+
|
|
142
|
+
{caption}
|
|
143
|
+
|
|
144
|
+
{fileRejections.length > 0 && <ShowRejectionItems fileRejections={fileRejections} />}
|
|
145
|
+
</>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { isString } from "lodash";
|
|
2
|
+
import PropTypes from "prop-types";
|
|
3
|
+
// import { Icon } from "@iconify/react";
|
|
4
|
+
import { useDropzone } from "react-dropzone";
|
|
5
|
+
|
|
6
|
+
import CloseIcon from "@mui/icons-material/Close";
|
|
7
|
+
import TextSnippetOutlinedIcon from "@mui/icons-material/TextSnippetOutlined";
|
|
8
|
+
// import { motion, AnimatePresence } from 'framer-motion';
|
|
9
|
+
// material
|
|
10
|
+
import { alpha, styled } from "@mui/material/styles";
|
|
11
|
+
import {
|
|
12
|
+
Box,
|
|
13
|
+
List,
|
|
14
|
+
Stack,
|
|
15
|
+
Paper,
|
|
16
|
+
Button,
|
|
17
|
+
ListItem,
|
|
18
|
+
Typography,
|
|
19
|
+
ListItemIcon,
|
|
20
|
+
ListItemText,
|
|
21
|
+
ListItemSecondaryAction,
|
|
22
|
+
IconButton,
|
|
23
|
+
Avatar
|
|
24
|
+
} from "@mui/material";
|
|
25
|
+
// utils
|
|
26
|
+
import { bytesToSize, fData } from "../../../utils";
|
|
27
|
+
import FileUploadOutlinedIcon from "@mui/icons-material/FileUploadOutlined";
|
|
28
|
+
//
|
|
29
|
+
// import { MIconButton } from '../@material-extend';
|
|
30
|
+
// import { varFadeInRight } from "../animate";
|
|
31
|
+
// import { UploadIllustration } from '../../assets';
|
|
32
|
+
|
|
33
|
+
// ----------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
const DropZoneStyle = styled("div")(({ theme }) => ({
|
|
36
|
+
outline: "none",
|
|
37
|
+
display: "flex",
|
|
38
|
+
textAlign: "center",
|
|
39
|
+
alignItems: "center",
|
|
40
|
+
flexDirection: "column",
|
|
41
|
+
justifyContent: "center",
|
|
42
|
+
padding: theme.spacing(5, 1),
|
|
43
|
+
borderRadius: theme.shape.borderRadius,
|
|
44
|
+
backgroundColor: theme.palette.background.neutral,
|
|
45
|
+
border: `1px dashed ${theme.palette.grey[500_32]}`,
|
|
46
|
+
"&:hover": { opacity: 0.72, cursor: "pointer" },
|
|
47
|
+
[theme.breakpoints.up("md")]: { textAlign: "left", flexDirection: "row" }
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
// ----------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
UploadMultiFile.propTypes = {
|
|
53
|
+
error: PropTypes.bool,
|
|
54
|
+
showPreview: PropTypes.bool,
|
|
55
|
+
files: PropTypes.array,
|
|
56
|
+
onRemove: PropTypes.func,
|
|
57
|
+
onRemoveAll: PropTypes.func,
|
|
58
|
+
sx: PropTypes.object
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export default function UploadMultiFile({ error, showPreview = false, files, onRemove, onRemoveAll, sx, ...other }) {
|
|
62
|
+
const hasFile = files && files.length > 0;
|
|
63
|
+
const { getRootProps, getInputProps, isDragActive, isDragReject, fileRejections } = useDropzone({
|
|
64
|
+
...other
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const ShowRejectionItems = () => (
|
|
68
|
+
<Paper
|
|
69
|
+
variant="outlined"
|
|
70
|
+
sx={{
|
|
71
|
+
py: 1,
|
|
72
|
+
px: 2,
|
|
73
|
+
mt: 3,
|
|
74
|
+
borderColor: "error.light",
|
|
75
|
+
bgcolor: (theme) => alpha(theme.palette.error.main, 0.08)
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
{fileRejections.map(({ file, errors }) => {
|
|
79
|
+
const { path, size } = file;
|
|
80
|
+
return (
|
|
81
|
+
<Box key={path} sx={{ my: 1 }}>
|
|
82
|
+
<Typography variant="subtitle2" noWrap>
|
|
83
|
+
{path} - {fData(size)}
|
|
84
|
+
</Typography>
|
|
85
|
+
{errors.map((e) => (
|
|
86
|
+
<Typography key={e.code} variant="caption" component="p">
|
|
87
|
+
- {e.message}
|
|
88
|
+
</Typography>
|
|
89
|
+
))}
|
|
90
|
+
</Box>
|
|
91
|
+
);
|
|
92
|
+
})}
|
|
93
|
+
</Paper>
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<Box sx={{ width: "100%", ...sx }}>
|
|
98
|
+
<DropZoneStyle
|
|
99
|
+
{...getRootProps()}
|
|
100
|
+
sx={{
|
|
101
|
+
...(isDragActive && { opacity: 0.72 }),
|
|
102
|
+
...((isDragReject || error) && {
|
|
103
|
+
color: "error.main",
|
|
104
|
+
borderColor: "error.light",
|
|
105
|
+
bgcolor: "error.lighter"
|
|
106
|
+
})
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
<input {...getInputProps()} />
|
|
110
|
+
|
|
111
|
+
{/* <UploadIllustration sx={{ width: 220 }} /> */}
|
|
112
|
+
<Stack direction={"row"} alignItems={"center"}>
|
|
113
|
+
<Avatar>
|
|
114
|
+
<FileUploadOutlinedIcon />
|
|
115
|
+
</Avatar>
|
|
116
|
+
<Box sx={{ p: 1, ml: { md: 2 } }}>
|
|
117
|
+
<Typography gutterBottom variant="h5">
|
|
118
|
+
Click hoặc kéo và thả file
|
|
119
|
+
</Typography>
|
|
120
|
+
|
|
121
|
+
{/* <Typography variant="body2" sx={{ color: "text.secondary" }}>
|
|
122
|
+
Drop files here or click
|
|
123
|
+
<Typography variant="body2" component="span" sx={{ color: "primary.main", textDecoration: "underline" }}>
|
|
124
|
+
browse
|
|
125
|
+
</Typography>
|
|
126
|
+
thorough your machine
|
|
127
|
+
</Typography> */}
|
|
128
|
+
</Box>
|
|
129
|
+
</Stack>
|
|
130
|
+
</DropZoneStyle>
|
|
131
|
+
|
|
132
|
+
{fileRejections.length > 0 && <ShowRejectionItems />}
|
|
133
|
+
|
|
134
|
+
<List disablePadding sx={{ ...(hasFile && { my: 3 }) }}>
|
|
135
|
+
{files?.map((file) => {
|
|
136
|
+
let name, size;
|
|
137
|
+
if (file.tenFile) {
|
|
138
|
+
name = file.tenFile;
|
|
139
|
+
size = file.Size;
|
|
140
|
+
} else {
|
|
141
|
+
name = file.name;
|
|
142
|
+
size = bytesToSize(file.size);
|
|
143
|
+
}
|
|
144
|
+
const { preview, tenFile, Size } = file;
|
|
145
|
+
const key = isString(file) ? file : name;
|
|
146
|
+
|
|
147
|
+
if (showPreview) {
|
|
148
|
+
return (
|
|
149
|
+
<ListItem
|
|
150
|
+
key={key}
|
|
151
|
+
// {...varFadeInRight}
|
|
152
|
+
sx={{
|
|
153
|
+
p: 0,
|
|
154
|
+
m: 0.5,
|
|
155
|
+
width: 80,
|
|
156
|
+
height: 80,
|
|
157
|
+
borderRadius: 1.5,
|
|
158
|
+
overflow: "hidden",
|
|
159
|
+
position: "relative",
|
|
160
|
+
display: "inline-flex"
|
|
161
|
+
}}
|
|
162
|
+
>
|
|
163
|
+
<Paper
|
|
164
|
+
variant="outlined"
|
|
165
|
+
component="img"
|
|
166
|
+
src={isString(file) ? file : preview}
|
|
167
|
+
sx={{ width: "100%", height: "100%", objectFit: "cover", position: "absolute" }}
|
|
168
|
+
/>
|
|
169
|
+
<Box sx={{ top: 6, right: 6, position: "absolute" }}>
|
|
170
|
+
<IconButton
|
|
171
|
+
size="small"
|
|
172
|
+
onClick={() => onRemove(file)}
|
|
173
|
+
sx={{
|
|
174
|
+
p: "2px",
|
|
175
|
+
color: "common.white",
|
|
176
|
+
bgcolor: (theme) => alpha(theme.palette.grey[900], 0.72),
|
|
177
|
+
"&:hover": {
|
|
178
|
+
bgcolor: (theme) => alpha(theme.palette.grey[900], 0.48)
|
|
179
|
+
}
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
182
|
+
<CloseIcon />
|
|
183
|
+
{/* <Icon icon={closeFill} /> */}
|
|
184
|
+
</IconButton>
|
|
185
|
+
</Box>
|
|
186
|
+
</ListItem>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<ListItem
|
|
192
|
+
key={key}
|
|
193
|
+
// {...varFadeInRight}
|
|
194
|
+
sx={{
|
|
195
|
+
my: 1,
|
|
196
|
+
py: 0.75,
|
|
197
|
+
px: 2,
|
|
198
|
+
borderRadius: 1,
|
|
199
|
+
border: (theme) => `solid 1px ${theme.palette.divider}`,
|
|
200
|
+
bgcolor: "background.paper"
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
<ListItemIcon>
|
|
204
|
+
{/* <Icon icon={fileFill} width={28} height={28} /> */}
|
|
205
|
+
<TextSnippetOutlinedIcon color="info" />
|
|
206
|
+
</ListItemIcon>
|
|
207
|
+
<ListItemText
|
|
208
|
+
primary={isString(file) ? file : name}
|
|
209
|
+
secondary={isString(file) ? "" : size}
|
|
210
|
+
primaryTypographyProps={{ variant: "subtitle2" }}
|
|
211
|
+
secondaryTypographyProps={{ variant: "caption" }}
|
|
212
|
+
/>
|
|
213
|
+
<ListItemSecondaryAction>
|
|
214
|
+
<IconButton edge="end" size="small" onClick={() => onRemove(file)}>
|
|
215
|
+
<CloseIcon />
|
|
216
|
+
</IconButton>
|
|
217
|
+
</ListItemSecondaryAction>
|
|
218
|
+
</ListItem>
|
|
219
|
+
);
|
|
220
|
+
})}
|
|
221
|
+
</List>
|
|
222
|
+
|
|
223
|
+
{hasFile && (
|
|
224
|
+
<Stack direction="row" justifyContent="flex-end">
|
|
225
|
+
<Button onClick={onRemoveAll} sx={{ mr: 1.5 }} color="error">
|
|
226
|
+
Gỡ hết
|
|
227
|
+
</Button>
|
|
228
|
+
{/* <Button variant="contained">Upload files</Button> */}
|
|
229
|
+
</Stack>
|
|
230
|
+
)}
|
|
231
|
+
</Box>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { CardContent, CardHeader, FormHelperText, LinearProgress, Card } from "@mui/material";
|
|
2
|
+
|
|
3
|
+
import { Controller, useFormContext } from "react-hook-form";
|
|
4
|
+
import UploadMultiFile from "trithuc-mvc-react/components/DataManagement/upload/UploadMultiFile";
|
|
5
|
+
import { useCallback, useState } from "react";
|
|
6
|
+
import { saveFilesToServer } from "@/utils";
|
|
7
|
+
|
|
8
|
+
const UploadMultipleFile = ({ name, label }) => {
|
|
9
|
+
const { control, getValues, setValue } = useFormContext();
|
|
10
|
+
const [isLoadingUpfile, setIsLoadingUpfile] = useState(false);
|
|
11
|
+
|
|
12
|
+
const handleDrop = useCallback(
|
|
13
|
+
async (acceptedFiles) => {
|
|
14
|
+
const olds = getValues(name) ?? [];
|
|
15
|
+
setIsLoadingUpfile(true);
|
|
16
|
+
const data = await saveFilesToServer(acceptedFiles);
|
|
17
|
+
setValue(name, [...olds, ...data]);
|
|
18
|
+
setIsLoadingUpfile(false);
|
|
19
|
+
},
|
|
20
|
+
[setValue]
|
|
21
|
+
);
|
|
22
|
+
const handleRemoveAll = () => {
|
|
23
|
+
setValue(name, []);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const handleRemove = (file) => {
|
|
27
|
+
const filteredItems = getValues(name).filter((_file) => _file.urlFile !== file.urlFile);
|
|
28
|
+
setValue(name, filteredItems);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Controller
|
|
33
|
+
name={name}
|
|
34
|
+
control={control}
|
|
35
|
+
render={({ field, fieldState: { error } }) => {
|
|
36
|
+
return (
|
|
37
|
+
<>
|
|
38
|
+
|
|
39
|
+
<UploadMultiFile
|
|
40
|
+
maxSize={3145728}
|
|
41
|
+
accept="*"
|
|
42
|
+
files={field.value}
|
|
43
|
+
onDrop={handleDrop}
|
|
44
|
+
onRemove={handleRemove}
|
|
45
|
+
onRemoveAll={handleRemoveAll}
|
|
46
|
+
error={Boolean(error)}
|
|
47
|
+
/>
|
|
48
|
+
{isLoadingUpfile && <LinearProgress />}
|
|
49
|
+
{error?.message && (
|
|
50
|
+
<FormHelperText error sx={{ px: 2 }}>
|
|
51
|
+
{error.message}
|
|
52
|
+
</FormHelperText>
|
|
53
|
+
)}
|
|
54
|
+
</>
|
|
55
|
+
);
|
|
56
|
+
}}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
export default UploadMultipleFile ;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { isString } from "lodash";
|
|
2
|
+
import PropTypes from "prop-types";
|
|
3
|
+
import { useDropzone } from "react-dropzone";
|
|
4
|
+
// material
|
|
5
|
+
import { alpha, styled } from "@mui/material/styles";
|
|
6
|
+
import { Paper, Box, Typography } from "@mui/material";
|
|
7
|
+
// utils
|
|
8
|
+
// import { fData } from '../../utils/formatNumber';
|
|
9
|
+
//
|
|
10
|
+
// import { UploadIllustration } from '../../assets';
|
|
11
|
+
import { fData } from "../../utils";
|
|
12
|
+
|
|
13
|
+
// ----------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
const DropZoneStyle = styled("div")(({ theme }) => ({
|
|
16
|
+
outline: "none",
|
|
17
|
+
display: "flex",
|
|
18
|
+
overflow: "hidden",
|
|
19
|
+
textAlign: "center",
|
|
20
|
+
position: "relative",
|
|
21
|
+
alignItems: "center",
|
|
22
|
+
flexDirection: "column",
|
|
23
|
+
justifyContent: "center",
|
|
24
|
+
padding: theme.spacing(5, 0),
|
|
25
|
+
borderRadius: theme.shape.borderRadius,
|
|
26
|
+
transition: theme.transitions.create("padding"),
|
|
27
|
+
backgroundColor: theme.palette.background.neutral,
|
|
28
|
+
border: `1px dashed ${theme.palette.grey[500_32]}`,
|
|
29
|
+
"&:hover": {
|
|
30
|
+
opacity: 0.72,
|
|
31
|
+
cursor: "pointer"
|
|
32
|
+
},
|
|
33
|
+
[theme.breakpoints.up("md")]: { textAlign: "left", flexDirection: "row" }
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
// ----------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
UploadSingleFile.propTypes = {
|
|
39
|
+
error: PropTypes.bool,
|
|
40
|
+
file: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
|
41
|
+
sx: PropTypes.object
|
|
42
|
+
};
|
|
43
|
+
const ShowRejectionItems = ({ fileRejections }) => (
|
|
44
|
+
<Paper
|
|
45
|
+
variant="outlined"
|
|
46
|
+
sx={{
|
|
47
|
+
py: 1,
|
|
48
|
+
px: 2,
|
|
49
|
+
mt: 3,
|
|
50
|
+
borderColor: "error.light",
|
|
51
|
+
bgcolor: (theme) => alpha(theme.palette.error.main, 0.08)
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
{fileRejections.map(({ file, errors }) => {
|
|
55
|
+
const { path, size } = file;
|
|
56
|
+
return (
|
|
57
|
+
<Box key={path} sx={{ my: 1 }}>
|
|
58
|
+
<Typography variant="subtitle2" noWrap>
|
|
59
|
+
{path} - {fData(size)}
|
|
60
|
+
</Typography>
|
|
61
|
+
{errors.map((e) => (
|
|
62
|
+
<Typography key={e.code} variant="caption" component="p">
|
|
63
|
+
- {e.message}
|
|
64
|
+
</Typography>
|
|
65
|
+
))}
|
|
66
|
+
</Box>
|
|
67
|
+
);
|
|
68
|
+
})}
|
|
69
|
+
</Paper>
|
|
70
|
+
);
|
|
71
|
+
export default function UploadSingleFile({ error, file, sx, ...other }) {
|
|
72
|
+
const { getRootProps, getInputProps, isDragActive, isDragReject, fileRejections } = useDropzone({
|
|
73
|
+
multiple: false,
|
|
74
|
+
...other
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Box sx={{ width: "100%", ...sx }}>
|
|
79
|
+
<DropZoneStyle
|
|
80
|
+
{...getRootProps()}
|
|
81
|
+
sx={{
|
|
82
|
+
...(isDragActive && { opacity: 0.72 }),
|
|
83
|
+
...((isDragReject || error) && {
|
|
84
|
+
color: "error.main",
|
|
85
|
+
borderColor: "error.light",
|
|
86
|
+
bgcolor: "error.lighter"
|
|
87
|
+
}),
|
|
88
|
+
...(file && { padding: "12% 0" })
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
<input {...getInputProps()} />
|
|
92
|
+
|
|
93
|
+
{/* <UploadIllustration sx={{ width: 220 }} /> */}
|
|
94
|
+
|
|
95
|
+
<Box sx={{ p: 3, ml: { md: 2 } }}>
|
|
96
|
+
<Typography gutterBottom variant="h5">
|
|
97
|
+
Drop or Select file
|
|
98
|
+
</Typography>
|
|
99
|
+
|
|
100
|
+
<Typography variant="body2" sx={{ color: "text.secondary" }}>
|
|
101
|
+
Drop files here or click
|
|
102
|
+
<Typography variant="body2" component="span" sx={{ color: "primary.main", textDecoration: "underline" }}>
|
|
103
|
+
browse
|
|
104
|
+
</Typography>
|
|
105
|
+
thorough your machine
|
|
106
|
+
</Typography>
|
|
107
|
+
</Box>
|
|
108
|
+
|
|
109
|
+
{file && (
|
|
110
|
+
<Box
|
|
111
|
+
component="img"
|
|
112
|
+
alt="file preview"
|
|
113
|
+
src={isString(file) ? file : file.preview}
|
|
114
|
+
sx={{
|
|
115
|
+
top: 8,
|
|
116
|
+
borderRadius: 1,
|
|
117
|
+
objectFit: "cover",
|
|
118
|
+
position: "absolute",
|
|
119
|
+
width: "calc(100% - 16px)",
|
|
120
|
+
height: "calc(100% - 16px)"
|
|
121
|
+
}}
|
|
122
|
+
/>
|
|
123
|
+
)}
|
|
124
|
+
</DropZoneStyle>
|
|
125
|
+
|
|
126
|
+
{/* {fileRejections.length > 0 && <ShowRejectionItems fileRejections={fileRejections} />} */}
|
|
127
|
+
</Box>
|
|
128
|
+
);
|
|
129
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export default (bytes, decimals = 2) => {
|
|
2
|
+
if (bytes === 0) return '0 Bytes';
|
|
3
|
+
|
|
4
|
+
const k = 1024;
|
|
5
|
+
const dm = decimals < 0 ? 0 : decimals;
|
|
6
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
7
|
+
|
|
8
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
9
|
+
|
|
10
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
|
11
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import moment from "moment";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns an object with default values for the given fields.
|
|
5
|
+
*
|
|
6
|
+
* @param {Array<{ fieldName: string, defaultValue?: string, type?: string }>} fields - The list of fields.
|
|
7
|
+
* @returns {Record<string, string[] | null | string>} - The object with default values.
|
|
8
|
+
*/
|
|
9
|
+
export const getDefaultValues = (fields) => {
|
|
10
|
+
return fields.reduce((acc, { fieldName, defaultValue = "", type = "text" }) => {
|
|
11
|
+
switch (type) {
|
|
12
|
+
case "file":
|
|
13
|
+
acc[fieldName] = [];
|
|
14
|
+
break;
|
|
15
|
+
case "date":
|
|
16
|
+
acc[fieldName] = null;
|
|
17
|
+
break;
|
|
18
|
+
default:
|
|
19
|
+
acc[fieldName] = defaultValue;
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
return acc;
|
|
23
|
+
}, {});
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Converts field values in a form data object based on their type.
|
|
28
|
+
* @param {Array<{ fieldName: string, type: string }>} fields - The list of fields to handle.
|
|
29
|
+
* @param {Object} formData - The form data object to update.
|
|
30
|
+
* @returns {void}
|
|
31
|
+
*/
|
|
32
|
+
export const handleFieldTypesBeforeSubmit = (fields, formData) => {
|
|
33
|
+
const updatedFormData = { ...formData };
|
|
34
|
+
|
|
35
|
+
for (const { fieldName, type } of fields) {
|
|
36
|
+
switch (type) {
|
|
37
|
+
case "date":
|
|
38
|
+
updatedFormData[fieldName] = moment(formData[fieldName]).toDate();
|
|
39
|
+
break;
|
|
40
|
+
case "file":
|
|
41
|
+
updatedFormData[fieldName] = JSON.stringify(formData[fieldName]);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return updatedFormData;
|
|
47
|
+
};
|
package/utils/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import moment from "moment";
|
|
2
|
+
import bytesToSize from "./bytesToSize";
|
|
3
|
+
import { uploadFile } from "trithuc-mvc-react/api";
|
|
4
|
+
export { bytesToSize };
|
|
5
|
+
export * from "./formFields";
|
|
6
|
+
export function fDateTime(date) {
|
|
7
|
+
return moment(date).format("DD/MM/yyyy HH:mm");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function fData(number) {
|
|
11
|
+
// return numeral(number).format("0.0 b");
|
|
12
|
+
return number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const saveFilesToServer = async (files) => {
|
|
16
|
+
try {
|
|
17
|
+
const formData = new FormData();
|
|
18
|
+
files.forEach((file, i) => {
|
|
19
|
+
formData.append(`file${i}`, file);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const data = await uploadFile(formData);
|
|
23
|
+
|
|
24
|
+
return data.map((item) => {
|
|
25
|
+
return JSON.parse(item);
|
|
26
|
+
});
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.log(error);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
|