/*! * 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 ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { Draggable } from 'react-beautiful-dnd'; import { Button, Checkbox, Icon } from 'semantic-ui-react'; import { useDidUpdate } from '../../../../lib/hooks'; import selectors from '../../../../selectors'; import entryActions from '../../../../entry-actions'; import { usePopupInClosableContext } from '../../../../hooks'; import { isListArchiveOrTrash } from '../../../../utils/record-helpers'; import { BoardMembershipRoles } from '../../../../constants/Enums'; import { ClosableContext } from '../../../../contexts'; import EditName from './EditName'; import SelectAssigneeStep from './SelectAssigneeStep'; import ActionsStep from './ActionsStep'; import Linkify from '../../../common/Linkify'; import UserAvatar from '../../../users/UserAvatar'; import styles from './Task.module.scss'; const Task = React.memo(({ id, index }) => { const selectTaskById = useMemo(() => selectors.makeSelectTaskById(), []); const selectListById = useMemo(() => selectors.makeSelectListById(), []); const task = useSelector((state) => selectTaskById(state, id)); const { canEdit, canToggle } = useSelector((state) => { const { listId } = selectors.selectCurrentCard(state); const list = selectListById(state, listId); if (isListArchiveOrTrash(list)) { return { canEdit: false, canToggle: false, }; } const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state); const isEditor = !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR; return { canEdit: isEditor, canToggle: isEditor, }; }, shallowEqual); const dispatch = useDispatch(); const [isEditNameOpened, setIsEditNameOpened] = useState(false); const [, , setIsClosableActive] = useContext(ClosableContext); const handleToggleChange = useCallback(() => { dispatch( entryActions.updateTask(id, { isCompleted: !task.isCompleted, }), ); }, [id, task.isCompleted, dispatch]); const handleUserSelect = useCallback( (userId) => { dispatch( entryActions.updateTask(id, { assigneeUserId: userId, }), ); }, [id, dispatch], ); const handleUserDeselect = useCallback(() => { dispatch( entryActions.updateTask(id, { assigneeUserId: null, }), ); }, [id, dispatch]); const isEditable = task.isPersisted && canEdit; const handleClick = useCallback(() => { if (isEditable) { setIsEditNameOpened(true); } }, [isEditable]); const handleNameEdit = useCallback(() => { setIsEditNameOpened(true); }, []); const handleEditNameClose = useCallback(() => { setIsEditNameOpened(false); }, []); useDidUpdate(() => { setIsClosableActive(isEditNameOpened); }, [isEditNameOpened]); const SelectAssigneePopup = usePopupInClosableContext(SelectAssigneeStep); const ActionsPopup = usePopupInClosableContext(ActionsStep); return ( {({ innerRef, draggableProps, dragHandleProps }, { isDragging }) => { const contentNode = (
{isEditNameOpened ? ( ) : (
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} {task.name} {(task.assigneeUserId || isEditable) && (
{isEditable ? ( <> {task.assigneeUserId ? ( ) : ( )} ) : ( )}
)}
)}
); return isDragging ? ReactDOM.createPortal(contentNode, document.body) : contentNode; }}
); }); Task.propTypes = { id: PropTypes.string.isRequired, index: PropTypes.number.isRequired, }; export default Task;