1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-18 12:49:43 +02:00

ref: Refactoring

This commit is contained in:
Maksim Eltyshev 2025-07-08 17:13:22 +02:00
parent 5e073a3648
commit cf52a00bd7
6 changed files with 101 additions and 80 deletions

View file

@ -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 (
<li className={classNames(styles.wrapper, task.isCompleted && styles.wrapperCompleted)}>
<li className={classNames(styles.wrapper, isCompleted && styles.wrapperCompleted)}>
<Linkify linkStopPropagation>{task.name}</Linkify>
</li>
);

View file

@ -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();

View file

@ -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 (
<a
{...linkProps} // eslint-disable-line react/jsx-props-no-spreading
href={href}
target={isSameSite ? undefined : '_blank'}
rel={isSameSite ? undefined : 'noreferrer'}
onClick={handleLinkClick}
>
{isSameSite ? linkContent : content}
</a>
);
},
[handleLinkClick, cardNamesById],
({ attributes: { href, ...linkProps }, content }) => (
<a
{...linkProps} // eslint-disable-line react/jsx-props-no-spreading
href={href}
target={isSameSite ? undefined : '_blank'}
rel={isSameSite ? undefined : 'noreferrer'}
onClick={handleLinkClick}
>
{card ? card.name : content}
</a>
),
[isSameSite, card, handleLinkClick],
);
return (

View file

@ -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);

View file

@ -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);
}, []);

View file

@ -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,