diff --git a/client/src/components/cards/Card/TaskList/Task.jsx b/client/src/components/cards/Card/TaskList/Task.jsx index 5603bf00..731aa7ea 100644 --- a/client/src/components/cards/Card/TaskList/Task.jsx +++ b/client/src/components/cards/Card/TaskList/Task.jsx @@ -9,17 +9,43 @@ import classNames from 'classnames'; import { useSelector } from 'react-redux'; import selectors from '../../../../selectors'; +import { ListTypes } from '../../../../constants/Enums'; import Linkify from '../../../common/Linkify'; import styles from './Task.module.scss'; const Task = React.memo(({ id }) => { const selectTaskById = useMemo(() => selectors.makeSelectTaskById(), []); + const selectCardById = useMemo(() => selectors.makeSelectCardById(), []); + const selectListById = useMemo(() => selectors.makeSelectListById(), []); const task = useSelector((state) => selectTaskById(state, id)); + const isCompleted = useSelector((state) => { + if (task.isCompleted) { + return true; + } + + const regex = /\/cards\/([^/]+)/g; + const matches = task.name.matchAll(regex); + + // eslint-disable-next-line no-restricted-syntax + for (const [, cardId] of matches) { + const card = selectCardById(state, cardId); + + if (card) { + const list = selectListById(state, card.listId); + + if (list && list.type === ListTypes.CLOSED) { + return true; + } + } + } + return false; + }); + return ( -
  • +
  • {task.name}
  • ); diff --git a/client/src/components/cards/Card/TaskList/TaskList.jsx b/client/src/components/cards/Card/TaskList/TaskList.jsx index e9497848..368b73c8 100644 --- a/client/src/components/cards/Card/TaskList/TaskList.jsx +++ b/client/src/components/cards/Card/TaskList/TaskList.jsx @@ -9,6 +9,7 @@ import classNames from 'classnames'; import { useSelector } from 'react-redux'; import { Progress } from 'semantic-ui-react'; import { useToggle } from '../../../../lib/hooks'; +import { ListTypes } from '../../../../constants/Enums'; import selectors from '../../../../selectors'; import Task from './Task'; @@ -16,18 +17,41 @@ import Task from './Task'; import styles from './TaskList.module.scss'; const TaskList = React.memo(({ id }) => { + const selectCardById = useMemo(() => selectors.makeSelectCardById(), []); + const selectListById = useMemo(() => selectors.makeSelectListById(), []); const selectTasksByTaskListId = useMemo(() => selectors.makeSelectTasksByTaskListId(), []); const tasks = useSelector((state) => selectTasksByTaskListId(state, id)); - const [isOpened, toggleOpened] = useToggle(); - // TODO: move to selector? - const completedTasksTotal = useMemo( - () => tasks.reduce((result, task) => (task.isCompleted ? result + 1 : result), 0), - [tasks], + const completedTasksTotal = useSelector((state) => + tasks.reduce((result, task) => { + if (task.isCompleted) { + return result + 1; + } + + const regex = /\/cards\/([^/]+)/g; + const matches = task.name.matchAll(regex); + + // eslint-disable-next-line no-restricted-syntax + for (const [, cardId] of matches) { + const card = selectCardById(state, cardId); + + if (card) { + const list = selectListById(state, card.listId); + + if (list && list.type === ListTypes.CLOSED) { + return result + 1; + } + } + } + + return result; + }, 0), ); + const [isOpened, toggleOpened] = useToggle(); + const handleToggleClick = useCallback( (event) => { event.stopPropagation(); diff --git a/client/src/components/common/Linkify.jsx b/client/src/components/common/Linkify.jsx index 86df52ec..e0304891 100644 --- a/client/src/components/common/Linkify.jsx +++ b/client/src/components/common/Linkify.jsx @@ -3,51 +3,72 @@ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; import LinkifyReact from 'linkify-react'; import history from '../../history'; +import selectors from '../../selectors'; +import matchPaths from '../../utils/match-paths'; +import Paths from '../../constants/Paths'; const Linkify = React.memo(({ children, linkStopPropagation, ...props }) => { + const selectCardById = useMemo(() => selectors.makeSelectCardById(), []); + + const url = useMemo(() => { + try { + return new URL(children, window.location); + } catch { + return null; + } + }, [children]); + + const isSameSite = !!url && url.origin === window.location.origin; + + const cardsPathMatch = useMemo(() => { + if (!isSameSite) { + return null; + } + + return matchPaths(url.pathname, [Paths.CARDS]); + }, [url.pathname, isSameSite]); + + const card = useSelector((state) => { + if (!cardsPathMatch) { + return null; + } + + return selectCardById(state, cardsPathMatch.params.id); + }); + const handleLinkClick = useCallback( (event) => { if (linkStopPropagation) { event.stopPropagation(); } - if (!event.target.getAttribute('target')) { + if (isSameSite) { event.preventDefault(); history.push(event.target.href); } }, - [linkStopPropagation], + [linkStopPropagation, isSameSite], ); const linkRenderer = useCallback( - ({ attributes: { href, ...linkProps }, content }) => { - let url; - try { - url = new URL(href, window.location); - } catch { - /* empty */ - } - - const isSameSite = !!url && url.origin === window.location.origin; - - return ( - - {isSameSite ? url.pathname : content} - - ); - }, - [handleLinkClick], + ({ attributes: { href, ...linkProps }, content }) => ( + + {card ? card.name : content} + + ), + [isSameSite, card, handleLinkClick], ); return ( diff --git a/client/src/components/task-lists/TaskList/Task/Task.jsx b/client/src/components/task-lists/TaskList/Task/Task.jsx index b1671f79..9458afea 100755 --- a/client/src/components/task-lists/TaskList/Task/Task.jsx +++ b/client/src/components/task-lists/TaskList/Task/Task.jsx @@ -16,7 +16,7 @@ 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 { BoardMembershipRoles, ListTypes } from '../../../../constants/Enums'; import { ClosableContext } from '../../../../contexts'; import EditName from './EditName'; import SelectAssigneeStep from './SelectAssigneeStep'; @@ -28,10 +28,30 @@ import styles from './Task.module.scss'; const Task = React.memo(({ id, index }) => { const selectTaskById = useMemo(() => selectors.makeSelectTaskById(), []); + const selectCardById = useMemo(() => selectors.makeSelectCardById(), []); const selectListById = useMemo(() => selectors.makeSelectListById(), []); const task = useSelector((state) => selectTaskById(state, id)); + const isLinkedCardCompleted = useSelector((state) => { + const regex = /\/cards\/([^/]+)/g; + const matches = task.name.matchAll(regex); + + // eslint-disable-next-line no-restricted-syntax + for (const [, cardId] of matches) { + const card = selectCardById(state, cardId); + + if (card) { + const list = selectListById(state, card.listId); + + if (list && list.type === ListTypes.CLOSED) { + return true; + } + } + } + return false; + }); + const { canEdit, canToggle } = useSelector((state) => { const { listId } = selectors.selectCurrentCard(state); const list = selectListById(state, listId); @@ -84,6 +104,8 @@ const Task = React.memo(({ id, index }) => { }, [id, dispatch]); const isEditable = task.isPersisted && canEdit; + const isCompleted = task.isCompleted || isLinkedCardCompleted; + const isToggleDisabled = !task.isPersisted || !canToggle || isLinkedCardCompleted; const handleClick = useCallback(() => { if (isEditable) { @@ -122,8 +144,8 @@ const Task = React.memo(({ id, index }) => { > @@ -138,9 +160,7 @@ const Task = React.memo(({ id, index }) => { className={classNames(styles.text, canEdit && styles.textEditable)} onClick={handleClick} > - + {task.name} diff --git a/client/src/components/task-lists/TaskList/TaskList.jsx b/client/src/components/task-lists/TaskList/TaskList.jsx index 25e7be7d..8ff3d8c4 100755 --- a/client/src/components/task-lists/TaskList/TaskList.jsx +++ b/client/src/components/task-lists/TaskList/TaskList.jsx @@ -14,7 +14,7 @@ import { useDidUpdate } from '../../../lib/hooks'; import selectors from '../../../selectors'; import { isListArchiveOrTrash } from '../../../utils/record-helpers'; import DroppableTypes from '../../../constants/DroppableTypes'; -import { BoardMembershipRoles } from '../../../constants/Enums'; +import { BoardMembershipRoles, ListTypes } from '../../../constants/Enums'; import { ClosableContext } from '../../../contexts'; import Task from './Task'; import AddTask from './AddTask'; @@ -23,12 +23,40 @@ import styles from './TaskList.module.scss'; const TaskList = React.memo(({ id }) => { const selectTaskListById = useMemo(() => selectors.makeSelectTaskListById(), []); + const selectCardById = useMemo(() => selectors.makeSelectCardById(), []); const selectListById = useMemo(() => selectors.makeSelectListById(), []); const selectTasksByTaskListId = useMemo(() => selectors.makeSelectTasksByTaskListId(), []); const taskList = useSelector((state) => selectTaskListById(state, id)); const tasks = useSelector((state) => selectTasksByTaskListId(state, id)); + // TODO: move to selector? + const completedTasksTotal = useSelector((state) => + tasks.reduce((result, task) => { + if (task.isCompleted) { + return result + 1; + } + + const regex = /\/cards\/([^/]+)/g; + const matches = task.name.matchAll(regex); + + // eslint-disable-next-line no-restricted-syntax + for (const [, cardId] of matches) { + const card = selectCardById(state, cardId); + + if (card) { + const list = selectListById(state, card.listId); + + if (list && list.type === ListTypes.CLOSED) { + return result + 1; + } + } + } + + return result; + }, 0), + ); + const canEdit = useSelector((state) => { const { listId } = selectors.selectCurrentCard(state); const list = selectListById(state, listId); @@ -45,12 +73,6 @@ const TaskList = React.memo(({ id }) => { const [isAddOpened, setIsAddOpened] = useState(false); const [, , setIsClosableActive] = useContext(ClosableContext); - // TODO: move to selector? - const completedTasksTotal = useMemo( - () => tasks.reduce((result, task) => (task.isCompleted ? result + 1 : result), 0), - [tasks], - ); - const handleAddClick = useCallback(() => { setIsAddOpened(true); }, []);