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

feat: add attachment management with modal for creating and displayin… #114

Merged
merged 1 commit into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/src/validations/cardValidation.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const addChecklistItem = async (req, res, next) => {
const addAttachment = async (req, res, next) => {
const correctCondition = Joi.object({
link: Joi.string().required().uri().trim().strict(),
name: Joi.string().optional().trim().strict()
name: Joi.string().allow('').optional().trim().strict()
})

try {
Expand Down
42 changes: 36 additions & 6 deletions frontend/src/components/Modal/ActiveCard/ActiveCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import Divider from '@mui/material/Divider'
import PersonOutlineOutlinedIcon from '@mui/icons-material/PersonOutlineOutlined'
import LocalOfferOutlinedIcon from '@mui/icons-material/LocalOfferOutlined'
import WatchLaterOutlinedIcon from '@mui/icons-material/WatchLaterOutlined'
import AttachFileOutlinedIcon from '@mui/icons-material/AttachFileOutlined'
import TaskAltOutlinedIcon from '@mui/icons-material/TaskAltOutlined'
import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined'
import AutoFixHighOutlinedIcon from '@mui/icons-material/AutoFixHighOutlined'
import AspectRatioOutlinedIcon from '@mui/icons-material/AspectRatioOutlined'
Expand Down Expand Up @@ -55,6 +53,8 @@ import CardLabelSection from './CardLabelSection'

import { styled } from '@mui/material/styles'
import { Popover } from '@mui/material'
import AttachmentModal from './AttachmentModal'
import CardAttachmentSection from './CardAttachmentSection'

const SidebarItem = styled(Box)(({ theme }) => ({
display: 'flex',
Expand Down Expand Up @@ -189,6 +189,21 @@ function ActiveCard() {
dispatch(updateCardInBoard(updatedChecklists))
}

const onAttachmentCreated = (updatedAttachments) => {
// Gọi onUpdateCardAttachments bên trong onAttachmentCreated
onUpdateCardAttachments(updatedAttachments);

dispatch(updateCurrentActiveCard(updatedAttachments));
dispatch(updateCardInBoard(updatedAttachments));
};

const onUpdateCardAttachments = (updatedAttachments) => {

dispatch(updateCurrentActiveCard(updatedAttachments));
dispatch(updateCardInBoard(updatedAttachments));
};


const [anchorPopoverElement, setAnchorPopoverElement] = useState(null)
const isOpenPopover = Boolean(anchorPopoverElement)
const popoverId = isOpenPopover ? 'labels-popover' : undefined
Expand Down Expand Up @@ -343,6 +358,18 @@ function ActiveCard() {
/>
</Box>

{ /* Attachment Section */}
<Box sx={{ mb: 3 }}>
{/* Render CardAttachmentSection only if there are attachments */}
{activeCard?.attachments?.length > 0 && (
<CardAttachmentSection
cardId={activeCard?._id}
cardAttachmentProp={activeCard?.attachments}
handleUpdateCardAttachments={onUpdateCardAttachments}
/>
)}
</Box>

{/* Checklist Section */}
<Box sx={{ mb: 3 }}>
<CardChecklistSection
Expand Down Expand Up @@ -405,10 +432,13 @@ function ActiveCard() {
<VisuallyHiddenInput type="file" onChange={onUploadCardCover} />
</SidebarItem>

<SidebarItem>
<AttachFileOutlinedIcon fontSize="small" />
Attachment
</SidebarItem>
<AttachmentModal
cardId={activeCard?._id}
attachments={activeCard?.attachments || []}
onAttachmentCreated={onAttachmentCreated}
/>


<SidebarItem onClick={handleTogglePopover}>
<LocalOfferOutlinedIcon fontSize="small" />
Labels
Expand Down
187 changes: 187 additions & 0 deletions frontend/src/components/Modal/ActiveCard/AddAttachmentModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import React, { useState } from 'react';
import {
Button,
Modal,
Box,
TextField,
Typography,
CircularProgress,
List,
ListItem,
ListItemText,
IconButton,
} from '@mui/material';
import { createAttachmentAPI } from '~/apis';
import { toast } from 'react-toastify';
import { styled } from '@mui/material/styles';
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/Delete';

const SidebarItem = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
gap: '6px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '600',
color: theme.palette.mode === 'dark' ? '#90caf9' : '#172b4d',
backgroundColor: theme.palette.mode === 'dark' ? '#2f3542' : '#091e420f',
padding: '10px',
borderRadius: '4px',
'&:hover': {
backgroundColor:
theme.palette.mode === 'dark' ? '#33485D' : theme.palette.grey[300],
},
}));

const AddAttachmentModal = ({ cardId, attachments, onAddAttachmentCreated }) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [link, setLink] = useState('');
const [name, setName] = useState('');
const [loading, setLoading] = useState(false);

// Mở Modal
const handleOpen = () => setIsModalOpen(true);

// Đóng Modal
const handleClose = () => {
setIsModalOpen(false);
setLink('');
setName('');
};

// Xử lý tạo tệp đính kèm
const handleCreateAttachment = async () => {
if (!link.trim()) {
toast.error('Link is required!');
return;
}

setLoading(true);

try {
const newAttachment = {
link: link.trim(),
name: name.trim() || link.trim().split('/').pop(),
};

const response = await createAttachmentAPI(cardId, newAttachment);

onAddAttachmentCreated(response);
toast.success('Attachment created successfully!');
handleClose();
} catch (error) {
toast.error('Failed to create attachment!');
} finally {
setLoading(false);
}
};

return (
<>
<SidebarItem onClick={handleOpen}>
<AddIcon fontSize="small" />
Add
</SidebarItem>

<Modal
open={isModalOpen}
onClose={handleClose}
aria-labelledby="modal-title"
aria-describedby="modal-description"
>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
boxShadow: 24,
p: 4,
borderRadius: 2,
}}
>
<Typography id="modal-title" variant="h6" component="h2" mb={2}>
Add Attachment
</Typography>

<TextField
fullWidth
margin="normal"
label="Search or paste a link"
variant="outlined"
value={link}
onChange={(e) => setLink(e.target.value)}
required
/>

<TextField
fullWidth
margin="normal"
label="Display text (optional)"
variant="outlined"
value={name}
onChange={(e) => setName(e.target.value)}
/>

<Box
display="flex"
justifyContent="space-between"
alignItems="center"
mt={2}
>
<Button variant="outlined" onClick={handleClose}>
Cancel
</Button>

<Button
variant="contained"
color="primary"
onClick={handleCreateAttachment}
disabled={loading}
>
{loading ? <CircularProgress size={24} color="inherit" /> : 'Create'}
</Button>
</Box>

<Typography variant="subtitle1" mt={3}>
Existing Attachments
</Typography>

<List
sx={{
maxHeight: 200, // Chiều cao tối đa của danh sách
overflowY: 'auto', // Bật thanh cuộn dọc
border: '1px solid',
borderColor: 'divider',
borderRadius: 1,
mt: 2,
p: 1,
bgcolor: 'background.paper',
}}
>
{attachments.map((attachment) => (
<ListItem
key={attachment._id}
secondaryAction={
<IconButton edge="end" aria-label="delete">
<DeleteIcon />
</IconButton>
}
>
<ListItemText
primary={attachment.name}
secondary={attachment.link}
/>
</ListItem>
))}
</List>
</Box>
</Modal>
</>
);
};

export default AddAttachmentModal;
Loading