mirror of
https://github.com/plankanban/planka.git
synced 2025-07-18 20:59:44 +02:00
feat: Add selector for card names and update task completion logic
This commit is contained in:
parent
b22dba0d11
commit
5e073a3648
5 changed files with 110 additions and 18 deletions
|
@ -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(
|
||||
|
|
|
@ -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 (
|
||||
<a
|
||||
|
@ -43,11 +53,11 @@ const Linkify = React.memo(({ children, linkStopPropagation, ...props }) => {
|
|||
rel={isSameSite ? undefined : 'noreferrer'}
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
{isSameSite ? url.pathname : content}
|
||||
{isSameSite ? linkContent : content}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
[handleLinkClick],
|
||||
[handleLinkClick, cardNamesById],
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -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 }) => {
|
|||
>
|
||||
<span className={styles.checkboxWrapper}>
|
||||
<Checkbox
|
||||
checked={task.isCompleted}
|
||||
disabled={!task.isPersisted || !canToggle}
|
||||
checked={isCompleted}
|
||||
disabled={isToggleDisabled}
|
||||
className={styles.checkbox}
|
||||
onChange={handleToggleChange}
|
||||
/>
|
||||
|
@ -138,9 +161,7 @@ const Task = React.memo(({ id, index }) => {
|
|||
className={classNames(styles.text, canEdit && styles.textEditable)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<span
|
||||
className={classNames(styles.task, task.isCompleted && styles.taskCompleted)}
|
||||
>
|
||||
<span className={classNames(styles.task, isCompleted && styles.taskCompleted)}>
|
||||
<Linkify linkStopPropagation>{task.name}</Linkify>
|
||||
</span>
|
||||
</span>
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue