diff --git a/client/src/components/cards/Card/TaskList/TaskList.jsx b/client/src/components/cards/Card/TaskList/TaskList.jsx index e9497848..49a6c1e9 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'; @@ -23,9 +24,33 @@ const TaskList = React.memo(({ id }) => { const [isOpened, toggleOpened] = useToggle(); // TODO: move to selector? - const completedTasksTotal = useMemo( - () => tasks.reduce((result, task) => (task.isCompleted ? result + 1 : result), 0), - [tasks], + const selectCardById = useMemo(() => selectors.makeSelectCardById(), []); + const selectListById = useMemo(() => selectors.makeSelectListById(), []); + + 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 handleToggleClick = useCallback( diff --git a/client/src/components/common/Linkify.jsx b/client/src/components/common/Linkify.jsx index 86df52ec..f1d4f5cb 100644 --- a/client/src/components/common/Linkify.jsx +++ b/client/src/components/common/Linkify.jsx @@ -5,11 +5,15 @@ import React, { useCallback } 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'; const Linkify = React.memo(({ children, linkStopPropagation, ...props }) => { + const cardNamesById = useSelector(selectors.selectCardNamesById); + const handleLinkClick = useCallback( (event) => { if (linkStopPropagation) { @@ -34,6 +38,12 @@ const Linkify = React.memo(({ children, linkStopPropagation, ...props }) => { } const isSameSite = !!url && url.origin === window.location.origin; + let linkContent = content; + if (isSameSite) { + const { pathname } = url; + const match = pathname.match(/^\/cards\/([^/]+)$/); + linkContent = cardNamesById[match?.[1]] || pathname; + } return ( { rel={isSameSite ? undefined : 'noreferrer'} onClick={handleLinkClick} > - {isSameSite ? url.pathname : content} + {isSameSite ? linkContent : content} ); }, - [handleLinkClick], + [handleLinkClick, cardNamesById], ); 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..be5b88b4 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'; @@ -29,9 +29,26 @@ import styles from './Task.module.scss'; const Task = React.memo(({ id, index }) => { const selectTaskById = useMemo(() => selectors.makeSelectTaskById(), []); const selectListById = useMemo(() => selectors.makeSelectListById(), []); + const selectCardById = useMemo(() => selectors.makeSelectCardById(), []); 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); @@ -56,13 +73,21 @@ const Task = React.memo(({ id, index }) => { const [isEditNameOpened, setIsEditNameOpened] = useState(false); const [, , setIsClosableActive] = useContext(ClosableContext); + const isEditable = task.isPersisted && canEdit; + const isCompleted = task.isCompleted || isLinkedCardCompleted; + const isToggleDisabled = !task.isPersisted || !canToggle || isLinkedCardCompleted; + const handleToggleChange = useCallback(() => { + if (isToggleDisabled) { + return; + } + dispatch( entryActions.updateTask(id, { isCompleted: !task.isCompleted, }), ); - }, [id, task.isCompleted, dispatch]); + }, [id, task.isCompleted, dispatch, isToggleDisabled]); const handleUserSelect = useCallback( (userId) => { @@ -83,8 +108,6 @@ const Task = React.memo(({ id, index }) => { ); }, [id, dispatch]); - const isEditable = task.isPersisted && canEdit; - const handleClick = useCallback(() => { if (isEditable) { setIsEditNameOpened(true); @@ -122,8 +145,8 @@ const Task = React.memo(({ id, index }) => { > @@ -138,9 +161,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..08081c8a 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'; @@ -46,9 +46,32 @@ const TaskList = React.memo(({ id }) => { const [, , setIsClosableActive] = useContext(ClosableContext); // TODO: move to selector? - const completedTasksTotal = useMemo( - () => tasks.reduce((result, task) => (task.isCompleted ? result + 1 : result), 0), - [tasks], + const selectCardById = useMemo(() => selectors.makeSelectCardById(), []); + + 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 handleAddClick = useCallback(() => { diff --git a/client/src/selectors/cards.js b/client/src/selectors/cards.js index eb203b1a..b9d2875b 100644 --- a/client/src/selectors/cards.js +++ b/client/src/selectors/cards.js @@ -32,6 +32,18 @@ export const makeSelectCardById = () => export const selectCardById = makeSelectCardById(); +export const selectCardNamesById = createSelector(orm, ({ Card }) => + Card.all() + .toModelArray() + .reduce( + (result, cardModel) => ({ + ...result, + [cardModel.id]: cardModel.name, + }), + {}, + ), +); + export const makeSelectCardIndexById = () => createSelector( orm, @@ -467,6 +479,7 @@ export default { selectCardById, makeSelectCardIndexById, selectCardIndexById, + selectCardNamesById, makeSelectUserIdsByCardId, selectUserIdsByCardId, makeSelectLabelIdsByCardId,