From cf52a00bd7b9a6a1d357d7709d823866d19b0a14 Mon Sep 17 00:00:00 2001 From: Maksim Eltyshev Date: Tue, 8 Jul 2025 17:13:22 +0200 Subject: [PATCH] ref: Refactoring --- .../components/cards/Card/TaskList/Task.jsx | 28 ++++++- .../cards/Card/TaskList/TaskList.jsx | 9 +-- client/src/components/common/Linkify.jsx | 77 +++++++++++-------- .../task-lists/TaskList/Task/Task.jsx | 19 +++-- .../task-lists/TaskList/TaskList.jsx | 35 ++++----- client/src/selectors/cards.js | 13 ---- 6 files changed, 101 insertions(+), 80 deletions(-) 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 49a6c1e9..368b73c8 100644 --- a/client/src/components/cards/Card/TaskList/TaskList.jsx +++ b/client/src/components/cards/Card/TaskList/TaskList.jsx @@ -17,16 +17,13 @@ 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 selectCardById = useMemo(() => selectors.makeSelectCardById(), []); - const selectListById = useMemo(() => selectors.makeSelectListById(), []); - const completedTasksTotal = useSelector((state) => tasks.reduce((result, task) => { if (task.isCompleted) { @@ -53,6 +50,8 @@ const TaskList = React.memo(({ id }) => { }, 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 f1d4f5cb..e0304891 100644 --- a/client/src/components/common/Linkify.jsx +++ b/client/src/components/common/Linkify.jsx @@ -3,16 +3,44 @@ * 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 cardNamesById = useSelector(selectors.selectCardNamesById); + 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) => { @@ -20,44 +48,27 @@ const Linkify = React.memo(({ children, linkStopPropagation, ...props }) => { 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; - let linkContent = content; - if (isSameSite) { - const { pathname } = url; - const match = pathname.match(/^\/cards\/([^/]+)$/); - linkContent = cardNamesById[match?.[1]] || pathname; - } - - return ( - - {isSameSite ? linkContent : content} - - ); - }, - [handleLinkClick, cardNamesById], + ({ 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 be5b88b4..9458afea 100755 --- a/client/src/components/task-lists/TaskList/Task/Task.jsx +++ b/client/src/components/task-lists/TaskList/Task/Task.jsx @@ -28,19 +28,22 @@ 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 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; } @@ -73,21 +76,13 @@ 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, isToggleDisabled]); + }, [id, task.isCompleted, dispatch]); const handleUserSelect = useCallback( (userId) => { @@ -108,6 +103,10 @@ 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) { setIsEditNameOpened(true); diff --git a/client/src/components/task-lists/TaskList/TaskList.jsx b/client/src/components/task-lists/TaskList/TaskList.jsx index 08081c8a..8ff3d8c4 100755 --- a/client/src/components/task-lists/TaskList/TaskList.jsx +++ b/client/src/components/task-lists/TaskList/TaskList.jsx @@ -23,31 +23,14 @@ 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)); - const canEdit = useSelector((state) => { - const { listId } = selectors.selectCurrentCard(state); - const list = selectListById(state, listId); - - if (isListArchiveOrTrash(list)) { - return false; - } - - const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state); - return !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR; - }); - - const [t] = useTranslation(); - const [isAddOpened, setIsAddOpened] = useState(false); - const [, , setIsClosableActive] = useContext(ClosableContext); - // TODO: move to selector? - const selectCardById = useMemo(() => selectors.makeSelectCardById(), []); - const completedTasksTotal = useSelector((state) => tasks.reduce((result, task) => { if (task.isCompleted) { @@ -74,6 +57,22 @@ const TaskList = React.memo(({ id }) => { }, 0), ); + const canEdit = useSelector((state) => { + const { listId } = selectors.selectCurrentCard(state); + const list = selectListById(state, listId); + + if (isListArchiveOrTrash(list)) { + return false; + } + + const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state); + return !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR; + }); + + const [t] = useTranslation(); + const [isAddOpened, setIsAddOpened] = useState(false); + const [, , setIsClosableActive] = useContext(ClosableContext); + const handleAddClick = useCallback(() => { setIsAddOpened(true); }, []); diff --git a/client/src/selectors/cards.js b/client/src/selectors/cards.js index b9d2875b..eb203b1a 100644 --- a/client/src/selectors/cards.js +++ b/client/src/selectors/cards.js @@ -32,18 +32,6 @@ 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, @@ -479,7 +467,6 @@ export default { selectCardById, makeSelectCardIndexById, selectCardIndexById, - selectCardNamesById, makeSelectUserIdsByCardId, selectUserIdsByCardId, makeSelectLabelIdsByCardId,