Merge pull request #1571 from lustsazeus-lab/fix/issue-165-feature-bounty
feat: improve card communication with reply and mentions
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import keyBy from 'lodash/keyBy';
|
||||
import React, { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
@@ -16,6 +17,7 @@ import entryActions from '../../../entry-actions';
|
||||
import { usePopupInClosableContext } from '../../../hooks';
|
||||
import { startStopwatch, stopStopwatch } from '../../../utils/stopwatch';
|
||||
import { isUsableMarkdownElement } from '../../../utils/element-helpers';
|
||||
import { mentionTextToMarkup } from '../../../utils/mentions';
|
||||
import { BoardMembershipRoles, CardTypes, ListTypes } from '../../../constants/Enums';
|
||||
import { CardTypeIcons } from '../../../constants/Icons';
|
||||
import { ClosableContext } from '../../../contexts';
|
||||
@@ -50,6 +52,7 @@ const ProjectContent = React.memo(() => {
|
||||
|
||||
const card = useSelector(selectors.selectCurrentCard);
|
||||
const board = useSelector(selectors.selectCurrentBoard);
|
||||
const boardMemberships = useSelector(selectors.selectMembershipsForCurrentBoard);
|
||||
const userIds = useSelector(selectors.selectUserIdsForCurrentCard);
|
||||
const labelIds = useSelector(selectors.selectLabelIdsForCurrentCard);
|
||||
const attachmentIds = useSelector(selectors.selectAttachmentIdsForCurrentCard);
|
||||
@@ -142,6 +145,15 @@ const ProjectContent = React.memo(() => {
|
||||
}, shallowEqual);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const userByUsername = useMemo(
|
||||
() =>
|
||||
keyBy(
|
||||
boardMemberships.flatMap(({ user }) => (user.username ? user : [])),
|
||||
({ username }) => username.toLowerCase(),
|
||||
),
|
||||
[boardMemberships],
|
||||
);
|
||||
|
||||
const [t] = useTranslation();
|
||||
const [descriptionDraft, setDescriptionDraft] = useState(null);
|
||||
const [isEditDescriptionOpened, setIsEditDescriptionOpened] = useState(false);
|
||||
@@ -169,11 +181,11 @@ const ProjectContent = React.memo(() => {
|
||||
(description) => {
|
||||
dispatch(
|
||||
entryActions.updateCurrentCard({
|
||||
description,
|
||||
description: description && mentionTextToMarkup(description, userByUsername),
|
||||
}),
|
||||
);
|
||||
},
|
||||
[dispatch],
|
||||
[dispatch, userByUsername],
|
||||
);
|
||||
|
||||
const handleDueCompletionChange = useCallback(() => {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import keyBy from 'lodash/keyBy';
|
||||
import React, { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
@@ -16,6 +17,7 @@ import selectors from '../../../selectors';
|
||||
import entryActions from '../../../entry-actions';
|
||||
import { usePopupInClosableContext } from '../../../hooks';
|
||||
import { isUsableMarkdownElement } from '../../../utils/element-helpers';
|
||||
import { mentionTextToMarkup } from '../../../utils/mentions';
|
||||
import { BoardMembershipRoles, CardTypes, ListTypes } from '../../../constants/Enums';
|
||||
import { CardTypeIcons } from '../../../constants/Icons';
|
||||
import { ClosableContext } from '../../../contexts';
|
||||
@@ -46,6 +48,7 @@ const StoryContent = React.memo(() => {
|
||||
|
||||
const card = useSelector(selectors.selectCurrentCard);
|
||||
const board = useSelector(selectors.selectCurrentBoard);
|
||||
const boardMemberships = useSelector(selectors.selectMembershipsForCurrentBoard);
|
||||
const userIds = useSelector(selectors.selectUserIdsForCurrentCard);
|
||||
const labelIds = useSelector(selectors.selectLabelIdsForCurrentCard);
|
||||
const attachmentIds = useSelector(selectors.selectAttachmentIdsForCurrentCard);
|
||||
@@ -137,6 +140,15 @@ const StoryContent = React.memo(() => {
|
||||
}, shallowEqual);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const userByUsername = useMemo(
|
||||
() =>
|
||||
keyBy(
|
||||
boardMemberships.flatMap(({ user }) => (user.username ? user : [])),
|
||||
({ username }) => username.toLowerCase(),
|
||||
),
|
||||
[boardMemberships],
|
||||
);
|
||||
|
||||
const [t] = useTranslation();
|
||||
const [descriptionDraft, setDescriptionDraft] = useState(null);
|
||||
const [isEditDescriptionOpened, setIsEditDescriptionOpened] = useState(false);
|
||||
@@ -164,11 +176,11 @@ const StoryContent = React.memo(() => {
|
||||
(description) => {
|
||||
dispatch(
|
||||
entryActions.updateCurrentCard({
|
||||
description,
|
||||
description: description && mentionTextToMarkup(description, userByUsername),
|
||||
}),
|
||||
);
|
||||
},
|
||||
[dispatch],
|
||||
[dispatch, userByUsername],
|
||||
);
|
||||
|
||||
const handleRestoreClick = useCallback(() => {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import keyBy from 'lodash/keyBy';
|
||||
import React, { useCallback, useState, useRef, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Mention, MentionsInput } from 'react-mentions';
|
||||
@@ -24,7 +25,7 @@ const DEFAULT_DATA = {
|
||||
text: '',
|
||||
};
|
||||
|
||||
const Add = React.memo(() => {
|
||||
const Add = React.memo(({ initialText, onInitialTextConsumed }) => {
|
||||
const boardMemberships = useSelector(selectors.selectMembershipsForCurrentBoard);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
@@ -138,6 +139,19 @@ const Add = React.memo(() => {
|
||||
textInputRef.current.focus();
|
||||
}, [selectTextFieldState]);
|
||||
|
||||
useDidUpdate(() => {
|
||||
if (!initialText) {
|
||||
return;
|
||||
}
|
||||
|
||||
setData({
|
||||
text: initialText,
|
||||
});
|
||||
setIsOpened(true);
|
||||
selectTextField();
|
||||
onInitialTextConsumed();
|
||||
}, [initialText, onInitialTextConsumed, selectTextField, setData]);
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<div ref={textFieldRef} className={styles.field}>
|
||||
@@ -188,4 +202,14 @@ const Add = React.memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
Add.propTypes = {
|
||||
initialText: PropTypes.string,
|
||||
onInitialTextConsumed: PropTypes.func,
|
||||
};
|
||||
|
||||
Add.defaultProps = {
|
||||
initialText: undefined,
|
||||
onInitialTextConsumed: () => {},
|
||||
};
|
||||
|
||||
export default Add;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { Comment, Loader } from 'semantic-ui-react';
|
||||
@@ -45,6 +45,15 @@ const Comments = React.memo(() => {
|
||||
});
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [replyText, setReplyText] = useState();
|
||||
|
||||
const handleReply = useCallback((username) => {
|
||||
setReplyText(`@${username} `);
|
||||
}, []);
|
||||
|
||||
const handleReplyTextConsumed = useCallback(() => {
|
||||
setReplyText(undefined);
|
||||
}, []);
|
||||
|
||||
const [inViewRef] = useInView({
|
||||
threshold: 1,
|
||||
@@ -57,11 +66,11 @@ const Comments = React.memo(() => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{cadAdd && <Add />}
|
||||
{cadAdd && <Add initialText={replyText} onInitialTextConsumed={handleReplyTextConsumed} />}
|
||||
<div className={styles.itemsWrapper}>
|
||||
<Comment.Group className={styles.items}>
|
||||
{commentIds.map((commentId) => (
|
||||
<Item key={commentId} id={commentId} />
|
||||
<Item key={commentId} id={commentId} canReply={cadAdd} onReply={handleReply} />
|
||||
))}
|
||||
</Comment.Group>
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@ import UserAvatar from '../../users/UserAvatar';
|
||||
|
||||
import styles from './Item.module.scss';
|
||||
|
||||
const Item = React.memo(({ id }) => {
|
||||
const Item = React.memo(({ id, canReply, onReply }) => {
|
||||
const selectCommentById = useMemo(() => selectors.makeSelectCommentById(), []);
|
||||
const selectUserById = useMemo(() => selectors.makeSelectUserById(), []);
|
||||
const selectListById = useMemo(() => selectors.makeSelectListById(), []);
|
||||
@@ -83,6 +83,12 @@ const Item = React.memo(({ id }) => {
|
||||
setIsEditOpened(true);
|
||||
}, []);
|
||||
|
||||
const handleReplyClick = useCallback(() => {
|
||||
if (user.username) {
|
||||
onReply(user.username);
|
||||
}
|
||||
}, [onReply, user.username]);
|
||||
|
||||
const handleEditClose = useCallback(() => {
|
||||
setIsEditOpened(false);
|
||||
}, []);
|
||||
@@ -117,8 +123,18 @@ const Item = React.memo(({ id }) => {
|
||||
<span className={styles.date}>
|
||||
<TimeAgo date={comment.createdAt} />
|
||||
</span>
|
||||
{(canEdit || canDelete) && (
|
||||
{(canReply || canEdit || canDelete) && (
|
||||
<span className={styles.actions}>
|
||||
{canReply && user.username && (
|
||||
<Comment.Action
|
||||
as="button"
|
||||
content={t('action.reply', {
|
||||
defaultValue: 'Reply',
|
||||
})}
|
||||
disabled={!comment.isPersisted}
|
||||
onClick={handleReplyClick}
|
||||
/>
|
||||
)}
|
||||
{canEdit && (
|
||||
<Comment.Action
|
||||
as="button"
|
||||
@@ -153,6 +169,13 @@ const Item = React.memo(({ id }) => {
|
||||
|
||||
Item.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
canReply: PropTypes.bool,
|
||||
onReply: PropTypes.func,
|
||||
};
|
||||
|
||||
Item.defaultProps = {
|
||||
canReply: false,
|
||||
onReply: () => {},
|
||||
};
|
||||
|
||||
export default Item;
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import keyBy from 'lodash/keyBy';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Form } from 'semantic-ui-react';
|
||||
@@ -13,6 +14,7 @@ import { useClickAwayListener } from '../../../lib/hooks';
|
||||
import selectors from '../../../selectors';
|
||||
import entryActions from '../../../entry-actions';
|
||||
import { useNestedRef } from '../../../hooks';
|
||||
import { mentionMarkupToText, mentionTextToMarkup } from '../../../utils/mentions';
|
||||
import MarkdownEditor from '../MarkdownEditor';
|
||||
|
||||
import styles from './EditMarkdown.module.scss';
|
||||
@@ -21,10 +23,20 @@ const MAX_LENGTH = 1048576;
|
||||
|
||||
const EditMarkdown = React.memo(({ defaultValue, draftValue, onUpdate, onClose }) => {
|
||||
const defaultMode = useSelector((state) => selectors.selectCurrentUser(state).defaultEditorMode);
|
||||
const boardMemberships = useSelector(selectors.selectMembershipsForCurrentBoard);
|
||||
|
||||
const userByUsername = useMemo(
|
||||
() =>
|
||||
keyBy(
|
||||
boardMemberships.map(({ user }) => user),
|
||||
(user) => user.username.toLowerCase(),
|
||||
),
|
||||
[boardMemberships],
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
const [value, setValue] = useState(() => draftValue || defaultValue || '');
|
||||
const [value, setValue] = useState(() => mentionMarkupToText(draftValue || defaultValue || ''));
|
||||
|
||||
const fieldRef = useRef(null);
|
||||
const [submitButtonRef, handleSubmitButtonRef] = useNestedRef();
|
||||
@@ -44,14 +56,15 @@ const EditMarkdown = React.memo(({ defaultValue, draftValue, onUpdate, onClose }
|
||||
const isExceeded = value.length > MAX_LENGTH;
|
||||
|
||||
const submit = useCallback(() => {
|
||||
const cleanValue = value.trim() || null;
|
||||
const valueWithMarkup = mentionTextToMarkup(value, userByUsername);
|
||||
const cleanValue = valueWithMarkup.trim() || null;
|
||||
|
||||
if (!isExceeded && cleanValue !== defaultValue) {
|
||||
onUpdate(cleanValue);
|
||||
}
|
||||
|
||||
onClose(isExceeded ? cleanValue : null);
|
||||
}, [onUpdate, onClose, defaultValue, value, isExceeded]);
|
||||
}, [onUpdate, onClose, defaultValue, value, isExceeded, userByUsername]);
|
||||
|
||||
const handleChange = useCallback((nextValue) => {
|
||||
setValue(nextValue);
|
||||
|
||||
Reference in New Issue
Block a user