1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-18 20:59:44 +02:00
planka/client/src/components/CardModal/CardModal.jsx

590 lines
22 KiB
React
Raw Normal View History

2022-06-20 18:27:39 +02:00
import React, { useCallback, useRef } from 'react';
2019-08-31 04:07:25 +05:00
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Button, Grid, Icon, Modal } from 'semantic-ui-react';
import { usePopup } from '../../lib/popup';
import { Markdown } from '../../lib/custom-ui';
2019-08-31 04:07:25 +05:00
import { startStopwatch, stopStopwatch } from '../../utils/stopwatch';
2019-08-31 04:07:25 +05:00
import NameField from './NameField';
import DescriptionEdit from './DescriptionEdit';
2019-08-31 04:07:25 +05:00
import Tasks from './Tasks';
2020-04-21 05:04:34 +05:00
import Attachments from './Attachments';
import AttachmentAddZone from './AttachmentAddZone';
import AttachmentAddStep from './AttachmentAddStep';
2022-08-04 13:31:14 +02:00
import Activities from './Activities';
2019-08-31 04:07:25 +05:00
import User from '../User';
import Label from '../Label';
import DueDate from '../DueDate';
import Stopwatch from '../Stopwatch';
import BoardMembershipsStep from '../BoardMembershipsStep';
import LabelsStep from '../LabelsStep';
import DueDateEditStep from '../DueDateEditStep';
import StopwatchEditStep from '../StopwatchEditStep';
import CardMoveStep from '../CardMoveStep';
import DeleteStep from '../DeleteStep';
2019-08-31 04:07:25 +05:00
import styles from './CardModal.module.scss';
2019-08-31 04:07:25 +05:00
const CardModal = React.memo(
({
name,
description,
dueDate,
stopwatch,
2019-08-31 04:07:25 +05:00
isSubscribed,
2022-08-04 13:31:14 +02:00
isActivitiesFetching,
isAllActivitiesFetched,
isActivitiesDetailsVisible,
isActivitiesDetailsFetching,
2020-05-05 01:30:06 +05:00
listId,
boardId,
projectId,
2019-08-31 04:07:25 +05:00
users,
labels,
tasks,
2020-04-21 05:04:34 +05:00
attachments,
2022-08-04 13:31:14 +02:00
activities,
2020-05-05 01:30:06 +05:00
allProjectsToLists,
allBoardMemberships,
2019-08-31 04:07:25 +05:00
allLabels,
canEdit,
canEditCommentActivities,
2022-08-04 13:31:14 +02:00
canEditAllCommentActivities,
2019-08-31 04:07:25 +05:00
onUpdate,
2020-05-05 01:30:06 +05:00
onMove,
onTransfer,
2019-08-31 04:07:25 +05:00
onDelete,
onUserAdd,
onUserRemove,
2020-05-05 01:30:06 +05:00
onBoardFetch,
2019-08-31 04:07:25 +05:00
onLabelAdd,
onLabelRemove,
onLabelCreate,
onLabelUpdate,
2023-01-09 12:17:06 +01:00
onLabelMove,
2019-08-31 04:07:25 +05:00
onLabelDelete,
onTaskCreate,
onTaskUpdate,
onTaskMove,
2019-08-31 04:07:25 +05:00
onTaskDelete,
2020-04-21 05:04:34 +05:00
onAttachmentCreate,
onAttachmentUpdate,
onAttachmentDelete,
2022-08-04 13:31:14 +02:00
onActivitiesFetch,
onActivitiesDetailsToggle,
onCommentActivityCreate,
onCommentActivityUpdate,
onCommentActivityDelete,
2019-08-31 04:07:25 +05:00
onClose,
}) => {
const [t] = useTranslation();
2022-06-20 18:27:39 +02:00
const isGalleryOpened = useRef(false);
const handleToggleStopwatchClick = useCallback(() => {
onUpdate({
stopwatch: stopwatch.startedAt ? stopStopwatch(stopwatch) : startStopwatch(stopwatch),
});
}, [stopwatch, onUpdate]);
2019-08-31 04:07:25 +05:00
const handleNameUpdate = useCallback(
2020-03-25 00:15:47 +05:00
(newName) => {
2019-08-31 04:07:25 +05:00
onUpdate({
name: newName,
});
},
[onUpdate],
);
const handleDescriptionUpdate = useCallback(
2020-03-25 00:15:47 +05:00
(newDescription) => {
2019-08-31 04:07:25 +05:00
onUpdate({
description: newDescription,
});
},
[onUpdate],
);
const handleDueDateUpdate = useCallback(
2020-03-25 00:15:47 +05:00
(newDueDate) => {
2019-08-31 04:07:25 +05:00
onUpdate({
dueDate: newDueDate,
2019-08-31 04:07:25 +05:00
});
},
[onUpdate],
);
const handleStopwatchUpdate = useCallback(
(newStopwatch) => {
2019-08-31 04:07:25 +05:00
onUpdate({
stopwatch: newStopwatch,
2019-08-31 04:07:25 +05:00
});
},
[onUpdate],
);
2020-04-23 03:02:53 +05:00
const handleCoverUpdate = useCallback(
(newCoverAttachmentId) => {
onUpdate({
coverAttachmentId: newCoverAttachmentId,
});
},
[onUpdate],
);
const handleToggleSubscriptionClick = useCallback(() => {
2019-08-31 04:07:25 +05:00
onUpdate({
isSubscribed: !isSubscribed,
});
}, [isSubscribed, onUpdate]);
2022-06-20 18:27:39 +02:00
const handleGalleryOpen = useCallback(() => {
isGalleryOpened.current = true;
}, []);
const handleGalleryClose = useCallback(() => {
isGalleryOpened.current = false;
}, []);
const handleClose = useCallback(() => {
if (isGalleryOpened.current) {
return;
}
onClose();
}, [onClose]);
const AttachmentAddPopup = usePopup(AttachmentAddStep);
const BoardMembershipsPopup = usePopup(BoardMembershipsStep);
const LabelsPopup = usePopup(LabelsStep);
const DueDateEditPopup = usePopup(DueDateEditStep);
const StopwatchEditPopup = usePopup(StopwatchEditStep);
const CardMovePopup = usePopup(CardMoveStep);
const DeletePopup = usePopup(DeleteStep);
2020-03-25 00:15:47 +05:00
const userIds = users.map((user) => user.id);
const labelIds = labels.map((label) => label.id);
2019-08-31 04:07:25 +05:00
const contentNode = (
<Grid className={styles.grid}>
<Grid.Row className={styles.headerPadding}>
<Grid.Column width={16} className={styles.headerPadding}>
<div className={styles.headerWrapper}>
<Icon name="list alternate outline" className={styles.moduleIcon} />
<div className={styles.headerTitleWrapper}>
{canEdit ? (
<NameField defaultValue={name} onUpdate={handleNameUpdate} />
) : (
<div className={styles.headerTitle}>{name}</div>
)}
</div>
</div>
</Grid.Column>
</Grid.Row>
<Grid.Row className={styles.modalPadding}>
<Grid.Column width={canEdit ? 12 : 16} className={styles.contentPadding}>
{(users.length > 0 || labels.length > 0 || dueDate || stopwatch) && (
<div className={styles.moduleWrapper}>
{users.length > 0 && (
<div className={styles.attachments}>
<div className={styles.text}>
{t('common.members', {
context: 'title',
})}
</div>
{users.map((user) => (
<span key={user.id} className={styles.attachment}>
{canEdit ? (
<BoardMembershipsPopup
items={allBoardMemberships}
currentUserIds={userIds}
onUserSelect={onUserAdd}
onUserDeselect={onUserRemove}
2019-08-31 04:07:25 +05:00
>
<User name={user.name} avatarUrl={user.avatarUrl} />
</BoardMembershipsPopup>
) : (
<User name={user.name} avatarUrl={user.avatarUrl} />
)}
</span>
))}
{canEdit && (
<BoardMembershipsPopup
items={allBoardMemberships}
currentUserIds={userIds}
onUserSelect={onUserAdd}
onUserDeselect={onUserRemove}
>
2023-11-17 14:34:10 +01:00
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
<button
type="button"
className={classNames(styles.attachment, styles.dueDate)}
2019-08-31 04:07:25 +05:00
>
<Icon name="add" size="small" className={styles.addAttachment} />
</button>
</BoardMembershipsPopup>
)}
</div>
)}
{labels.length > 0 && (
<div className={styles.attachments}>
<div className={styles.text}>
{t('common.labels', {
context: 'title',
})}
</div>
{labels.map((label) => (
<span key={label.id} className={styles.attachment}>
{canEdit ? (
<LabelsPopup
key={label.id}
items={allLabels}
currentIds={labelIds}
onSelect={onLabelAdd}
onDeselect={onLabelRemove}
onCreate={onLabelCreate}
onUpdate={onLabelUpdate}
2023-01-09 12:17:06 +01:00
onMove={onLabelMove}
onDelete={onLabelDelete}
>
<Label name={label.name} color={label.color} />
</LabelsPopup>
) : (
<Label name={label.name} color={label.color} />
)}
</span>
))}
{canEdit && (
<LabelsPopup
items={allLabels}
currentIds={labelIds}
onSelect={onLabelAdd}
onDeselect={onLabelRemove}
onCreate={onLabelCreate}
onUpdate={onLabelUpdate}
2023-01-09 12:17:06 +01:00
onMove={onLabelMove}
onDelete={onLabelDelete}
>
2023-11-17 14:34:10 +01:00
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
<button
type="button"
className={classNames(styles.attachment, styles.dueDate)}
>
<Icon name="add" size="small" className={styles.addAttachment} />
</button>
</LabelsPopup>
2019-08-31 04:07:25 +05:00
)}
</div>
)}
{dueDate && (
<div className={styles.attachments}>
<div className={styles.text}>
{t('common.dueDate', {
context: 'title',
})}
</div>
<span className={styles.attachment}>
{canEdit ? (
<DueDateEditPopup defaultValue={dueDate} onUpdate={handleDueDateUpdate}>
<DueDate value={dueDate} />
</DueDateEditPopup>
) : (
<DueDate value={dueDate} />
)}
</span>
</div>
)}
{stopwatch && (
<div className={styles.attachments}>
<div className={styles.text}>
{t('common.stopwatch', {
context: 'title',
})}
</div>
<span className={styles.attachment}>
{canEdit ? (
<StopwatchEditPopup
defaultValue={stopwatch}
onUpdate={handleStopwatchUpdate}
>
<Stopwatch startedAt={stopwatch.startedAt} total={stopwatch.total} />
</StopwatchEditPopup>
) : (
<Stopwatch startedAt={stopwatch.startedAt} total={stopwatch.total} />
)}
</span>
{canEdit && (
2023-11-17 14:34:10 +01:00
// eslint-disable-next-line jsx-a11y/control-has-associated-label
<button
type="button"
className={classNames(styles.attachment, styles.dueDate)}
2023-11-17 14:34:10 +01:00
onClick={handleToggleStopwatchClick}
>
<Icon
name={stopwatch.startedAt ? 'pause' : 'play'}
size="small"
className={styles.addAttachment}
/>
</button>
)}
</div>
)}
</div>
)}
{(description || canEdit) && (
<div className={styles.contentModule}>
<div className={styles.moduleWrapper}>
<Icon name="align justify" className={styles.moduleIcon} />
<div className={styles.moduleHeader}>{t('common.description')}</div>
{canEdit ? (
<DescriptionEdit defaultValue={description} onUpdate={handleDescriptionUpdate}>
{description ? (
<button
type="button"
className={classNames(styles.descriptionText, styles.cursorPointer)}
>
<Markdown linkStopPropagation linkTarget="_blank">
{description}
</Markdown>
</button>
) : (
<button type="button" className={styles.descriptionButton}>
<span className={styles.descriptionButtonText}>
{t('action.addMoreDetailedDescription')}
</span>
</button>
)}
</DescriptionEdit>
) : (
<div className={styles.descriptionText}>
<Markdown linkStopPropagation linkTarget="_blank">
{description}
</Markdown>
</div>
)}
2019-08-31 04:07:25 +05:00
</div>
</div>
)}
{(tasks.length > 0 || canEdit) && (
<div className={styles.contentModule}>
<div className={styles.moduleWrapper}>
<Icon name="check square outline" className={styles.moduleIcon} />
<div className={styles.moduleHeader}>{t('common.tasks')}</div>
<Tasks
items={tasks}
canEdit={canEdit}
onCreate={onTaskCreate}
onUpdate={onTaskUpdate}
onMove={onTaskMove}
onDelete={onTaskDelete}
/>
2020-04-21 05:04:34 +05:00
</div>
</div>
)}
{attachments.length > 0 && (
<div className={styles.contentModule}>
<div className={styles.moduleWrapper}>
<Icon name="attach" className={styles.moduleIcon} />
<div className={styles.moduleHeader}>{t('common.attachments')}</div>
<Attachments
items={attachments}
canEdit={canEdit}
onUpdate={onAttachmentUpdate}
onDelete={onAttachmentDelete}
onCoverUpdate={handleCoverUpdate}
2022-06-20 18:27:39 +02:00
onGalleryOpen={handleGalleryOpen}
onGalleryClose={handleGalleryClose}
/>
</div>
</div>
)}
2022-08-04 13:31:14 +02:00
<Activities
items={activities}
isFetching={isActivitiesFetching}
isAllFetched={isAllActivitiesFetched}
isDetailsVisible={isActivitiesDetailsVisible}
isDetailsFetching={isActivitiesDetailsFetching}
canEdit={canEditCommentActivities}
2022-08-04 13:31:14 +02:00
canEditAllComments={canEditAllCommentActivities}
onFetch={onActivitiesFetch}
onDetailsToggle={onActivitiesDetailsToggle}
onCommentCreate={onCommentActivityCreate}
onCommentUpdate={onCommentActivityUpdate}
onCommentDelete={onCommentActivityDelete}
/>
</Grid.Column>
{canEdit && (
<Grid.Column width={4} className={styles.sidebarPadding}>
<div className={styles.actions}>
<span className={styles.actionsTitle}>{t('action.addToCard')}</span>
<BoardMembershipsPopup
items={allBoardMemberships}
currentUserIds={userIds}
onUserSelect={onUserAdd}
onUserDeselect={onUserRemove}
>
<Button fluid className={styles.actionButton}>
<Icon name="user outline" className={styles.actionIcon} />
{t('common.members')}
</Button>
</BoardMembershipsPopup>
<LabelsPopup
items={allLabels}
currentIds={labelIds}
onSelect={onLabelAdd}
onDeselect={onLabelRemove}
onCreate={onLabelCreate}
onUpdate={onLabelUpdate}
2023-01-09 12:17:06 +01:00
onMove={onLabelMove}
onDelete={onLabelDelete}
>
<Button fluid className={styles.actionButton}>
<Icon name="bookmark outline" className={styles.actionIcon} />
{t('common.labels')}
</Button>
</LabelsPopup>
<DueDateEditPopup defaultValue={dueDate} onUpdate={handleDueDateUpdate}>
<Button fluid className={styles.actionButton}>
<Icon name="calendar check outline" className={styles.actionIcon} />
{t('common.dueDate', {
context: 'title',
})}
</Button>
</DueDateEditPopup>
<StopwatchEditPopup defaultValue={stopwatch} onUpdate={handleStopwatchUpdate}>
<Button fluid className={styles.actionButton}>
<Icon name="clock outline" className={styles.actionIcon} />
{t('common.stopwatch')}
</Button>
</StopwatchEditPopup>
<AttachmentAddPopup onCreate={onAttachmentCreate}>
<Button fluid className={styles.actionButton}>
<Icon name="attach" className={styles.actionIcon} />
{t('common.attachment')}
</Button>
</AttachmentAddPopup>
</div>
<div className={styles.actions}>
<span className={styles.actionsTitle}>{t('common.actions')}</span>
<Button
fluid
className={styles.actionButton}
onClick={handleToggleSubscriptionClick}
>
<Icon name="paper plane outline" className={styles.actionIcon} />
{isSubscribed ? t('action.unsubscribe') : t('action.subscribe')}
</Button>
<CardMovePopup
projectsToLists={allProjectsToLists}
defaultPath={{
projectId,
boardId,
listId,
}}
onMove={onMove}
onTransfer={onTransfer}
onBoardFetch={onBoardFetch}
>
<Button
fluid
className={styles.actionButton}
onClick={handleToggleSubscriptionClick}
>
<Icon name="share square outline" className={styles.actionIcon} />
{t('action.move')}
2019-08-31 04:07:25 +05:00
</Button>
</CardMovePopup>
<DeletePopup
title="common.deleteCard"
content="common.areYouSureYouWantToDeleteThisCard"
buttonContent="action.deleteCard"
onConfirm={onDelete}
>
<Button fluid className={styles.actionButton}>
<Icon name="trash alternate outline" className={styles.actionIcon} />
{t('action.delete')}
</Button>
</DeletePopup>
</div>
</Grid.Column>
)}
</Grid.Row>
</Grid>
);
return (
2022-07-26 13:53:30 +02:00
<Modal open closeIcon centered={false} onClose={handleClose} className={styles.wrapper}>
{canEdit ? (
<AttachmentAddZone onCreate={onAttachmentCreate}>{contentNode}</AttachmentAddZone>
) : (
contentNode
)}
2019-08-31 04:07:25 +05:00
</Modal>
);
},
);
CardModal.propTypes = {
name: PropTypes.string.isRequired,
description: PropTypes.string,
dueDate: PropTypes.instanceOf(Date),
stopwatch: PropTypes.object, // eslint-disable-line react/forbid-prop-types
2019-08-31 04:07:25 +05:00
isSubscribed: PropTypes.bool.isRequired,
2022-08-04 13:31:14 +02:00
isActivitiesFetching: PropTypes.bool.isRequired,
isAllActivitiesFetched: PropTypes.bool.isRequired,
isActivitiesDetailsVisible: PropTypes.bool.isRequired,
isActivitiesDetailsFetching: PropTypes.bool.isRequired,
2020-05-05 01:30:06 +05:00
listId: PropTypes.string.isRequired,
boardId: PropTypes.string.isRequired,
projectId: PropTypes.string.isRequired,
2019-08-31 04:07:25 +05:00
/* eslint-disable react/forbid-prop-types */
users: PropTypes.array.isRequired,
labels: PropTypes.array.isRequired,
tasks: PropTypes.array.isRequired,
2020-04-21 05:04:34 +05:00
attachments: PropTypes.array.isRequired,
2022-08-04 13:31:14 +02:00
activities: PropTypes.array.isRequired,
2020-05-05 01:30:06 +05:00
allProjectsToLists: PropTypes.array.isRequired,
allBoardMemberships: PropTypes.array.isRequired,
2019-08-31 04:07:25 +05:00
allLabels: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
canEdit: PropTypes.bool.isRequired,
canEditCommentActivities: PropTypes.bool.isRequired,
2022-08-04 13:31:14 +02:00
canEditAllCommentActivities: PropTypes.bool.isRequired,
2019-08-31 04:07:25 +05:00
onUpdate: PropTypes.func.isRequired,
2020-05-05 01:30:06 +05:00
onMove: PropTypes.func.isRequired,
onTransfer: PropTypes.func.isRequired,
2019-08-31 04:07:25 +05:00
onDelete: PropTypes.func.isRequired,
onUserAdd: PropTypes.func.isRequired,
onUserRemove: PropTypes.func.isRequired,
2020-05-05 01:30:06 +05:00
onBoardFetch: PropTypes.func.isRequired,
2019-08-31 04:07:25 +05:00
onLabelAdd: PropTypes.func.isRequired,
onLabelRemove: PropTypes.func.isRequired,
onLabelCreate: PropTypes.func.isRequired,
onLabelUpdate: PropTypes.func.isRequired,
2023-01-09 12:17:06 +01:00
onLabelMove: PropTypes.func.isRequired,
2019-08-31 04:07:25 +05:00
onLabelDelete: PropTypes.func.isRequired,
onTaskCreate: PropTypes.func.isRequired,
onTaskUpdate: PropTypes.func.isRequired,
onTaskMove: PropTypes.func.isRequired,
2019-08-31 04:07:25 +05:00
onTaskDelete: PropTypes.func.isRequired,
2020-04-21 05:04:34 +05:00
onAttachmentCreate: PropTypes.func.isRequired,
onAttachmentUpdate: PropTypes.func.isRequired,
onAttachmentDelete: PropTypes.func.isRequired,
2022-08-04 13:31:14 +02:00
onActivitiesFetch: PropTypes.func.isRequired,
onActivitiesDetailsToggle: PropTypes.func.isRequired,
onCommentActivityCreate: PropTypes.func.isRequired,
onCommentActivityUpdate: PropTypes.func.isRequired,
onCommentActivityDelete: PropTypes.func.isRequired,
2019-08-31 04:07:25 +05:00
onClose: PropTypes.func.isRequired,
};
CardModal.defaultProps = {
description: undefined,
dueDate: undefined,
stopwatch: undefined,
2019-08-31 04:07:25 +05:00
};
export default CardModal;