/*! * Copyright (c) 2024 PLANKA Software GmbH * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md */ import React, { useCallback, useContext, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { useTranslation } from 'react-i18next'; import { Button, Grid, Icon } from 'semantic-ui-react'; import { useDidUpdate } from '../../../lib/hooks'; import selectors from '../../../selectors'; import entryActions from '../../../entry-actions'; import { usePopupInClosableContext } from '../../../hooks'; import { startStopwatch, stopStopwatch } from '../../../utils/stopwatch'; import { isUsableMarkdownElement } from '../../../utils/element-helpers'; import { BoardMembershipRoles, CardTypes, ListTypes } from '../../../constants/Enums'; import { CardTypeIcons } from '../../../constants/Icons'; import { ClosableContext } from '../../../contexts'; import NameField from './NameField'; import TaskLists from './TaskLists'; import CustomFieldGroups from './CustomFieldGroups'; import Communication from './Communication'; import CreationDetailsStep from './CreationDetailsStep'; import DueDateChip from '../DueDateChip'; import StopwatchChip from '../StopwatchChip'; import SelectCardTypeStep from '../SelectCardTypeStep'; import EditDueDateStep from '../EditDueDateStep'; import EditStopwatchStep from '../EditStopwatchStep'; import MoveCardStep from '../MoveCardStep'; import ExpandableMarkdown from '../../common/ExpandableMarkdown'; import EditMarkdown from '../../common/EditMarkdown'; import ConfirmationStep from '../../common/ConfirmationStep'; import UserAvatar from '../../users/UserAvatar'; import BoardMembershipsStep from '../../board-memberships/BoardMembershipsStep'; import LabelChip from '../../labels/LabelChip'; import LabelsStep from '../../labels/LabelsStep'; import ListsStep from '../../lists/ListsStep'; import AddTaskListStep from '../../task-lists/AddTaskListStep'; import Attachments from '../../attachments/Attachments'; import AddAttachmentStep from '../../attachments/AddAttachmentStep'; import AddCustomFieldGroupStep from '../../custom-field-groups/AddCustomFieldGroupStep'; import styles from './ProjectContent.module.scss'; const ProjectContent = React.memo(({ onClose }) => { const selectListById = useMemo(() => selectors.makeSelectListById(), []); const selectPrevListById = useMemo(() => selectors.makeSelectListById(), []); const card = useSelector(selectors.selectCurrentCard); const board = useSelector(selectors.selectCurrentBoard); const userIds = useSelector(selectors.selectUserIdsForCurrentCard); const labelIds = useSelector(selectors.selectLabelIdsForCurrentCard); const attachmentIds = useSelector(selectors.selectAttachmentIdsForCurrentCard); const isJoined = useSelector(selectors.selectIsCurrentUserInCurrentCard); const list = useSelector((state) => selectListById(state, card.listId)); // TODO: check availability? const prevList = useSelector( (state) => card.prevListId && selectPrevListById(state, card.prevListId), ); const isInArchiveList = list.type === ListTypes.ARCHIVE; const isInTrashList = list.type === ListTypes.TRASH; const { canEditType, canEditName, canEditDescription, canEditDueDate, canEditStopwatch, canSubscribe, canJoin, canDuplicate, canMove, canRestore, canArchive, canDelete, canUseLists, canUseMembers, canUseLabels, canAddTaskList, canAddAttachment, canAddCustomFieldGroup, } = useSelector((state) => { const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state); let isMember = false; let isEditor = false; if (boardMembership) { isMember = true; isEditor = boardMembership.role === BoardMembershipRoles.EDITOR; } if (isInArchiveList || isInTrashList) { return { canEditType: false, canEditName: false, canEditDescription: false, canEditDueDate: false, canEditStopwatch: false, canSubscribe: isMember, canJoin: false, canDuplicate: false, canMove: false, canRestore: isEditor, canArchive: isEditor, canDelete: isEditor, canUseLists: isEditor, canUseMembers: false, canUseLabels: false, canAddTaskList: false, canAddAttachment: false, canAddCustomFieldGroup: false, }; } return { canEditType: isEditor, canEditName: isEditor, canEditDescription: isEditor, canEditDueDate: isEditor, canEditStopwatch: isEditor, canSubscribe: isMember, canJoin: isEditor, canDuplicate: isEditor, canMove: isEditor, canRestore: null, canArchive: isEditor, canDelete: isEditor, canUseLists: isEditor, canUseMembers: isEditor, canUseLabels: isEditor, canAddTaskList: isEditor, canAddAttachment: isEditor, canAddCustomFieldGroup: isEditor, }; }, shallowEqual); const dispatch = useDispatch(); const [t] = useTranslation(); const [descriptionDraft, setDescriptionDraft] = useState(null); const [isEditDescriptionOpened, setIsEditDescriptionOpened] = useState(false); const [, , setIsClosableActive] = useContext(ClosableContext); const handleListSelect = useCallback( (listId) => { dispatch(entryActions.moveCurrentCard(listId)); }, [dispatch], ); const handleTypeSelect = useCallback( (type) => { dispatch( entryActions.updateCurrentCard({ type, }), ); }, [dispatch], ); const handleNameUpdate = useCallback( (name) => { dispatch( entryActions.updateCurrentCard({ name, }), ); }, [dispatch], ); const handleDescriptionUpdate = useCallback( (description) => { dispatch( entryActions.updateCurrentCard({ description, }), ); }, [dispatch], ); const handleToggleStopwatchClick = useCallback(() => { dispatch( entryActions.updateCurrentCard({ stopwatch: card.stopwatch.startedAt ? stopStopwatch(card.stopwatch) : startStopwatch(card.stopwatch), }), ); }, [card.stopwatch, dispatch]); const handleDuplicateClick = useCallback(() => { dispatch( entryActions.duplicateCurrentCard({ name: `${card.name} (${t('common.copy', { context: 'inline', })})`, }), ); onClose(); }, [onClose, card.name, dispatch, t]); const handleRestoreClick = useCallback(() => { dispatch(entryActions.moveCurrentCard(card.prevListId, undefined, true)); }, [card.prevListId, dispatch]); const handleArchiveConfirm = useCallback(() => { dispatch(entryActions.moveCurrentCardToArchive()); }, [dispatch]); const handleDeleteConfirm = useCallback(() => { if (isInTrashList) { dispatch(entryActions.deleteCurrentCard()); } else { dispatch(entryActions.moveCurrentCardToTrash()); } }, [isInTrashList, dispatch]); const handleUserSelect = useCallback( (userId) => { dispatch(entryActions.addUserToCurrentCard(userId)); }, [dispatch], ); const handleUserDeselect = useCallback( (userId) => { dispatch(entryActions.removeUserFromCurrentCard(userId)); }, [dispatch], ); const handleLabelSelect = useCallback( (labelId) => { dispatch(entryActions.addLabelToCurrentCard(labelId)); }, [dispatch], ); const handleLabelDeselect = useCallback( (labelId) => { dispatch(entryActions.removeLabelFromCurrentCard(labelId)); }, [dispatch], ); const handleCustomFieldGroupCreate = useCallback( (data) => { dispatch(entryActions.createCustomFieldGroupInCurrentCard(data)); }, [dispatch], ); const handleToggleJointClick = useCallback(() => { if (isJoined) { dispatch(entryActions.removeCurrentUserFromCurrentCard()); } else { dispatch(entryActions.addCurrentUserToCurrentCard()); } }, [isJoined, dispatch]); const handleToggleSubscriptionClick = useCallback(() => { dispatch( entryActions.updateCurrentCard({ isSubscribed: !card.isSubscribed, }), ); }, [card.isSubscribed, dispatch]); const handleEditDescriptionClick = useCallback((event) => { if (window.getSelection().toString() || isUsableMarkdownElement(event.target)) { return; } setIsEditDescriptionOpened(true); }, []); const handleEditDescriptionClose = useCallback((nextDescriptionDraft) => { setDescriptionDraft(nextDescriptionDraft); setIsEditDescriptionOpened(false); }, []); useDidUpdate(() => { if (!canEditDescription) { setIsEditDescriptionOpened(false); } }, [canEditDescription]); useDidUpdate(() => { setIsClosableActive(isEditDescriptionOpened); }, [isEditDescriptionOpened]); const CreationDetailsPopup = usePopupInClosableContext(CreationDetailsStep); const BoardMembershipsPopup = usePopupInClosableContext(BoardMembershipsStep); const LabelsPopup = usePopupInClosableContext(LabelsStep); const ListsPopup = usePopupInClosableContext(ListsStep); const SelectCardTypePopup = usePopupInClosableContext(SelectCardTypeStep); const EditDueDatePopup = usePopupInClosableContext(EditDueDateStep); const EditStopwatchPopup = usePopupInClosableContext(EditStopwatchStep); const AddTaskListPopup = usePopupInClosableContext(AddTaskListStep); const AddAttachmentPopup = usePopupInClosableContext(AddAttachmentStep); const AddCustomFieldGroupPopup = usePopupInClosableContext(AddCustomFieldGroupStep); const MoveCardPopup = usePopupInClosableContext(MoveCardStep); const ConfirmationPopup = usePopupInClosableContext(ConfirmationStep); return (
{canEditName ? ( ) : (
{card.name}
)}
{(card.dueDate || card.stopwatch || board.alwaysDisplayCardCreator || userIds.length > 0 || labelIds.length > 0) && (
{board.alwaysDisplayCardCreator && (
{t('common.creator', { context: 'title', })}
)} {userIds.length > 0 && (
{t('common.members', { context: 'title', })}
{userIds.map((userId) => ( {canUseMembers ? ( ) : ( )} ))} {canUseMembers && ( )}
)} {labelIds.length > 0 && (
{t('common.labels', { context: 'title', })}
{labelIds.map((labelId) => ( {canUseLabels ? ( ) : ( )} ))} {canUseLabels && ( )}
)} {card.dueDate && (
{t('common.dueDate', { context: 'title', })}
{canEditDueDate ? ( ) : ( )}
)} {card.stopwatch && (
{t('common.stopwatch', { context: 'title', })}
{canEditStopwatch ? ( ) : ( )} {canEditStopwatch && ( )}
)}
)} {(card.description || canEditDescription) && (
{t('common.description')} {canEditDescription && !isEditDescriptionOpened && descriptionDraft && ( {t('common.unsavedChanges')} )}
{canEditDescription && ( <> {isEditDescriptionOpened && ( )} {!isEditDescriptionOpened && (card.description ? ( /* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
{card.description}
) : ( ))} )} {!canEditDescription && {card.description}}
)} {attachmentIds.length > 0 && (
{t('common.attachments')}
)}
{t('common.list')}
{canUseLists ? ( ) : ( {list.name || t(`common.${list.type}`)} )}
{(canEditDueDate || canEditStopwatch || canUseMembers || canUseLabels || canAddTaskList || canAddAttachment || canAddCustomFieldGroup) && (
{t('action.addToCard')} {canUseMembers && ( )} {canUseLabels && ( )} {canEditDueDate && ( )} {canEditStopwatch && ( )} {canAddTaskList && ( )} {canAddAttachment && ( )} {canAddCustomFieldGroup && ( )}
)} {((!board.limitCardTypesToDefaultOne && canEditType) || canSubscribe || canJoin || canDuplicate || canMove || (canRestore && (isInArchiveList || isInTrashList)) || (canArchive && !isInArchiveList) || canDelete) && (
{t('common.actions')} {canJoin && ( )} {canSubscribe && ( )} {!board.limitCardTypesToDefaultOne && canEditType && ( )} {canDuplicate && ( )} {canMove && ( )} {canRestore && (isInArchiveList || isInTrashList) && ( )} {canArchive && !isInArchiveList && ( )} {canDelete && ( )}
)}
); }); ProjectContent.propTypes = { onClose: PropTypes.func.isRequired, }; export default ProjectContent;