/*! * 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 { Gallery, Item as GalleryItem } from 'react-photoswipe-gallery'; 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 { isUsableMarkdownElement } from '../../../../utils/element-helpers'; import { BoardMembershipRoles, CardTypes, ListTypes } from '../../../../constants/Enums'; import { CardTypeIcons } from '../../../../constants/Icons'; import { ClosableContext } from '../../../../contexts'; import Thumbnail from './Thumbnail'; import NameField from '../NameField'; import CustomFieldGroups from '../CustomFieldGroups'; import Communication from '../Communication'; import CreationDetailsStep from '../CreationDetailsStep'; import SelectCardTypeStep from '../../SelectCardTypeStep'; import MoveCardStep from '../../MoveCardStep'; import Markdown from '../../../common/Markdown'; 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 Attachments from '../../../attachments/Attachments'; import AddAttachmentStep from '../../../attachments/AddAttachmentStep'; import AddCustomFieldGroupStep from '../../../custom-field-groups/AddCustomFieldGroupStep'; import styles from './StoryContent.module.scss'; const StoryContent = React.memo(({ onClose }) => { const selectListById = useMemo(() => selectors.makeSelectListById(), []); const selectPrevListById = useMemo(() => selectors.makeSelectListById(), []); const selectAttachmentById = useMemo(() => selectors.makeSelectAttachmentById(), []); 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 imageAttachmentIdsExceptCover = useSelector( selectors.selectImageAttachmentIdsExceptCoverForCurrentCard, ); 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 coverAttachment = useSelector((state) => selectAttachmentById(state, card.coverAttachmentId), ); const isInArchiveList = list.type === ListTypes.ARCHIVE; const isInTrashList = list.type === ListTypes.TRASH; const { canEditType, canEditName, canEditDescription, canSubscribe, canJoin, canDuplicate, canMove, canRestore, canArchive, canDelete, canUseLists, canUseMembers, canUseLabels, 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, canSubscribe: isMember, canJoin: false, canDuplicate: false, canMove: false, canRestore: isEditor, canArchive: isEditor, canDelete: isEditor, canUseLists: isEditor, canUseMembers: false, canUseLabels: false, canAddAttachment: false, canAddCustomFieldGroup: false, }; } return { canEditType: isEditor, canEditName: isEditor, canEditDescription: isEditor, canSubscribe: isMember, canJoin: isEditor, canDuplicate: isEditor, canMove: isEditor, canRestore: null, canArchive: isEditor, canDelete: isEditor, canUseLists: isEditor, canUseMembers: isEditor, canUseLabels: isEditor, canAddAttachment: isEditor, canAddCustomFieldGroup: isEditor, }; }, shallowEqual); const dispatch = useDispatch(); const [t] = useTranslation(); const [descriptionDraft, setDescriptionDraft] = useState(null); const [isEditDescriptionOpened, setIsEditDescriptionOpened] = useState(false); const [activateClosable, deactivateClosable, 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 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); }, []); const handleBeforeGalleryOpen = useCallback( (gallery) => { activateClosable(); gallery.on('destroy', () => { deactivateClosable(); }); }, [activateClosable, deactivateClosable], ); 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 AddAttachmentPopup = usePopupInClosableContext(AddAttachmentStep); const AddCustomFieldGroupPopup = usePopupInClosableContext(AddCustomFieldGroupStep); const MoveCardPopup = usePopupInClosableContext(MoveCardStep); const ConfirmationPopup = usePopupInClosableContext(ConfirmationStep); return (
{canEditName ? ( ) : (
{card.name}
)}
{ const paddingX = viewportSize.x / 20; const paddingY = viewportSize.y / 20; return { top: paddingX, bottom: paddingX, left: paddingY, right: paddingY, }; }, }} onBeforeOpen={handleBeforeGalleryOpen} > {(board.alwaysDisplayCardCreator || labelIds.length > 0 || coverAttachment) && (
{coverAttachment && (
{({ ref, open }) => ( /* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions */ {coverAttachment.name} )}
)} {board.alwaysDisplayCardCreator && (
)} {labelIds.length > 0 && (
{labelIds.map((labelId) => ( {canUseLabels ? ( ) : ( )} ))} {canUseLabels && ( )}
)}
)} {(card.description || canEditDescription) && (
{canEditDescription && (isEditDescriptionOpened ? ( ) : ( <> {descriptionDraft && ( {t('common.unsavedChanges')} )} {card.description ? ( /* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
{card.description}
) : ( )} ))} {!canEditDescription && (
{card.description}
)} {imageAttachmentIdsExceptCover.length > 0 && (
{imageAttachmentIdsExceptCover.map((attachmentId) => ( ))}
)}
)}
{attachmentIds.length > 0 && (
{t('common.attachments')}
)}
{t('common.list')}
{canUseLists ? ( ) : ( {list.name || t(`common.${list.type}`)} )}
{(canUseMembers || canUseLabels || canAddAttachment || canAddCustomFieldGroup) && (
{t('action.addToCard')} {canUseLabels && ( )} {canAddAttachment && ( )} {canAddCustomFieldGroup && ( )} {canUseMembers && ( )}
)} {((!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 && ( )}
)}
); }); StoryContent.propTypes = { onClose: PropTypes.func.isRequired, }; export default StoryContent;