Skip to content
This repository was archived by the owner on Mar 14, 2025. It is now read-only.

Commit 32d7626

Browse files
authored
Merge pull request #114 from hantbk/feat/attachment
feat: add attachment management with modal for creating and displayin…
2 parents 0051db5 + 65bcb7f commit 32d7626

File tree

7 files changed

+651
-9
lines changed

7 files changed

+651
-9
lines changed

backend/src/validations/cardValidation.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ const addChecklistItem = async (req, res, next) => {
8383
const addAttachment = async (req, res, next) => {
8484
const correctCondition = Joi.object({
8585
link: Joi.string().required().uri().trim().strict(),
86-
name: Joi.string().optional().trim().strict()
86+
name: Joi.string().allow('').optional().trim().strict()
8787
})
8888

8989
try {

frontend/src/components/Modal/ActiveCard/ActiveCard.jsx

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import Divider from '@mui/material/Divider'
99
import PersonOutlineOutlinedIcon from '@mui/icons-material/PersonOutlineOutlined'
1010
import LocalOfferOutlinedIcon from '@mui/icons-material/LocalOfferOutlined'
1111
import WatchLaterOutlinedIcon from '@mui/icons-material/WatchLaterOutlined'
12-
import AttachFileOutlinedIcon from '@mui/icons-material/AttachFileOutlined'
13-
import TaskAltOutlinedIcon from '@mui/icons-material/TaskAltOutlined'
1412
import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined'
1513
import AutoFixHighOutlinedIcon from '@mui/icons-material/AutoFixHighOutlined'
1614
import AspectRatioOutlinedIcon from '@mui/icons-material/AspectRatioOutlined'
@@ -55,6 +53,8 @@ import CardLabelSection from './CardLabelSection'
5553

5654
import { styled } from '@mui/material/styles'
5755
import { Popover } from '@mui/material'
56+
import AttachmentModal from './AttachmentModal'
57+
import CardAttachmentSection from './CardAttachmentSection'
5858

5959
const SidebarItem = styled(Box)(({ theme }) => ({
6060
display: 'flex',
@@ -189,6 +189,21 @@ function ActiveCard() {
189189
dispatch(updateCardInBoard(updatedChecklists))
190190
}
191191

192+
const onAttachmentCreated = (updatedAttachments) => {
193+
// Gọi onUpdateCardAttachments bên trong onAttachmentCreated
194+
onUpdateCardAttachments(updatedAttachments);
195+
196+
dispatch(updateCurrentActiveCard(updatedAttachments));
197+
dispatch(updateCardInBoard(updatedAttachments));
198+
};
199+
200+
const onUpdateCardAttachments = (updatedAttachments) => {
201+
202+
dispatch(updateCurrentActiveCard(updatedAttachments));
203+
dispatch(updateCardInBoard(updatedAttachments));
204+
};
205+
206+
192207
const [anchorPopoverElement, setAnchorPopoverElement] = useState(null)
193208
const isOpenPopover = Boolean(anchorPopoverElement)
194209
const popoverId = isOpenPopover ? 'labels-popover' : undefined
@@ -343,6 +358,18 @@ function ActiveCard() {
343358
/>
344359
</Box>
345360

361+
{ /* Attachment Section */}
362+
<Box sx={{ mb: 3 }}>
363+
{/* Render CardAttachmentSection only if there are attachments */}
364+
{activeCard?.attachments?.length > 0 && (
365+
<CardAttachmentSection
366+
cardId={activeCard?._id}
367+
cardAttachmentProp={activeCard?.attachments}
368+
handleUpdateCardAttachments={onUpdateCardAttachments}
369+
/>
370+
)}
371+
</Box>
372+
346373
{/* Checklist Section */}
347374
<Box sx={{ mb: 3 }}>
348375
<CardChecklistSection
@@ -405,10 +432,13 @@ function ActiveCard() {
405432
<VisuallyHiddenInput type="file" onChange={onUploadCardCover} />
406433
</SidebarItem>
407434

408-
<SidebarItem>
409-
<AttachFileOutlinedIcon fontSize="small" />
410-
Attachment
411-
</SidebarItem>
435+
<AttachmentModal
436+
cardId={activeCard?._id}
437+
attachments={activeCard?.attachments || []}
438+
onAttachmentCreated={onAttachmentCreated}
439+
/>
440+
441+
412442
<SidebarItem onClick={handleTogglePopover}>
413443
<LocalOfferOutlinedIcon fontSize="small" />
414444
Labels
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import React, { useState } from 'react';
2+
import {
3+
Button,
4+
Modal,
5+
Box,
6+
TextField,
7+
Typography,
8+
CircularProgress,
9+
List,
10+
ListItem,
11+
ListItemText,
12+
IconButton,
13+
} from '@mui/material';
14+
import { createAttachmentAPI } from '~/apis';
15+
import { toast } from 'react-toastify';
16+
import { styled } from '@mui/material/styles';
17+
import AddIcon from '@mui/icons-material/Add';
18+
import DeleteIcon from '@mui/icons-material/Delete';
19+
20+
const SidebarItem = styled(Box)(({ theme }) => ({
21+
display: 'flex',
22+
alignItems: 'center',
23+
gap: '6px',
24+
cursor: 'pointer',
25+
fontSize: '14px',
26+
fontWeight: '600',
27+
color: theme.palette.mode === 'dark' ? '#90caf9' : '#172b4d',
28+
backgroundColor: theme.palette.mode === 'dark' ? '#2f3542' : '#091e420f',
29+
padding: '10px',
30+
borderRadius: '4px',
31+
'&:hover': {
32+
backgroundColor:
33+
theme.palette.mode === 'dark' ? '#33485D' : theme.palette.grey[300],
34+
},
35+
}));
36+
37+
const AddAttachmentModal = ({ cardId, attachments, onAddAttachmentCreated }) => {
38+
const [isModalOpen, setIsModalOpen] = useState(false);
39+
const [link, setLink] = useState('');
40+
const [name, setName] = useState('');
41+
const [loading, setLoading] = useState(false);
42+
43+
// Mở Modal
44+
const handleOpen = () => setIsModalOpen(true);
45+
46+
// Đóng Modal
47+
const handleClose = () => {
48+
setIsModalOpen(false);
49+
setLink('');
50+
setName('');
51+
};
52+
53+
// Xử lý tạo tệp đính kèm
54+
const handleCreateAttachment = async () => {
55+
if (!link.trim()) {
56+
toast.error('Link is required!');
57+
return;
58+
}
59+
60+
setLoading(true);
61+
62+
try {
63+
const newAttachment = {
64+
link: link.trim(),
65+
name: name.trim() || link.trim().split('/').pop(),
66+
};
67+
68+
const response = await createAttachmentAPI(cardId, newAttachment);
69+
70+
onAddAttachmentCreated(response);
71+
toast.success('Attachment created successfully!');
72+
handleClose();
73+
} catch (error) {
74+
toast.error('Failed to create attachment!');
75+
} finally {
76+
setLoading(false);
77+
}
78+
};
79+
80+
return (
81+
<>
82+
<SidebarItem onClick={handleOpen}>
83+
<AddIcon fontSize="small" />
84+
Add
85+
</SidebarItem>
86+
87+
<Modal
88+
open={isModalOpen}
89+
onClose={handleClose}
90+
aria-labelledby="modal-title"
91+
aria-describedby="modal-description"
92+
>
93+
<Box
94+
sx={{
95+
position: 'absolute',
96+
top: '50%',
97+
left: '50%',
98+
transform: 'translate(-50%, -50%)',
99+
width: 400,
100+
bgcolor: 'background.paper',
101+
boxShadow: 24,
102+
p: 4,
103+
borderRadius: 2,
104+
}}
105+
>
106+
<Typography id="modal-title" variant="h6" component="h2" mb={2}>
107+
Add Attachment
108+
</Typography>
109+
110+
<TextField
111+
fullWidth
112+
margin="normal"
113+
label="Search or paste a link"
114+
variant="outlined"
115+
value={link}
116+
onChange={(e) => setLink(e.target.value)}
117+
required
118+
/>
119+
120+
<TextField
121+
fullWidth
122+
margin="normal"
123+
label="Display text (optional)"
124+
variant="outlined"
125+
value={name}
126+
onChange={(e) => setName(e.target.value)}
127+
/>
128+
129+
<Box
130+
display="flex"
131+
justifyContent="space-between"
132+
alignItems="center"
133+
mt={2}
134+
>
135+
<Button variant="outlined" onClick={handleClose}>
136+
Cancel
137+
</Button>
138+
139+
<Button
140+
variant="contained"
141+
color="primary"
142+
onClick={handleCreateAttachment}
143+
disabled={loading}
144+
>
145+
{loading ? <CircularProgress size={24} color="inherit" /> : 'Create'}
146+
</Button>
147+
</Box>
148+
149+
<Typography variant="subtitle1" mt={3}>
150+
Existing Attachments
151+
</Typography>
152+
153+
<List
154+
sx={{
155+
maxHeight: 200, // Chiều cao tối đa của danh sách
156+
overflowY: 'auto', // Bật thanh cuộn dọc
157+
border: '1px solid',
158+
borderColor: 'divider',
159+
borderRadius: 1,
160+
mt: 2,
161+
p: 1,
162+
bgcolor: 'background.paper',
163+
}}
164+
>
165+
{attachments.map((attachment) => (
166+
<ListItem
167+
key={attachment._id}
168+
secondaryAction={
169+
<IconButton edge="end" aria-label="delete">
170+
<DeleteIcon />
171+
</IconButton>
172+
}
173+
>
174+
<ListItemText
175+
primary={attachment.name}
176+
secondary={attachment.link}
177+
/>
178+
</ListItem>
179+
))}
180+
</List>
181+
</Box>
182+
</Modal>
183+
</>
184+
);
185+
};
186+
187+
export default AddAttachmentModal;

0 commit comments

Comments
 (0)