diff --git a/client/package-lock.json b/client/package-lock.json index 6f912566..b9891269 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -69,6 +69,7 @@ "react-i18next": "^15.5.1", "react-input-mask": "^2.0.4", "react-intersection-observer": "^9.16.0", + "react-mentions": "^4.4.10", "react-photoswipe-gallery": "^2.2.7", "react-redux": "^8.1.3", "react-router-dom": "^6.30.0", @@ -12296,6 +12297,31 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, + "node_modules/react-mentions": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/react-mentions/-/react-mentions-4.4.10.tgz", + "integrity": "sha512-JHiQlgF1oSZR7VYPjq32wy97z1w1oE4x10EuhKjPr4WUKhVzG1uFQhQjKqjQkbVqJrmahf+ldgBTv36NrkpKpA==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "7.4.5", + "invariant": "^2.2.4", + "prop-types": "^15.5.8", + "substyle": "^9.1.0" + }, + "peerDependencies": { + "react": ">=16.8.3", + "react-dom": ">=16.8.3" + } + }, + "node_modules/react-mentions/node_modules/@babel/runtime": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz", + "integrity": "sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.13.2" + } + }, "node_modules/react-onclickoutside": { "version": "6.13.2", "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.2.tgz", @@ -12677,6 +12703,12 @@ "node": ">=4" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, "node_modules/regexp-match-indices": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regexp-match-indices/-/regexp-match-indices-1.0.2.tgz", @@ -14183,6 +14215,19 @@ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==" }, + "node_modules/substyle": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/substyle/-/substyle-9.4.1.tgz", + "integrity": "sha512-VOngeq/W1/UkxiGzeqVvDbGDPM8XgUyJVWjrqeh+GgKqspEPiLYndK+XRcsKUHM5Muz/++1ctJ1QCF/OqRiKWA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.4", + "invariant": "^2.2.4" + }, + "peerDependencies": { + "react": ">=16.8.3" + } + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", diff --git a/client/package.json b/client/package.json index cf8921fd..c429160a 100755 --- a/client/package.json +++ b/client/package.json @@ -140,6 +140,7 @@ "react-i18next": "^15.5.1", "react-input-mask": "^2.0.4", "react-intersection-observer": "^9.16.0", + "react-mentions": "^4.4.10", "react-photoswipe-gallery": "^2.2.7", "react-redux": "^8.1.3", "react-router-dom": "^6.30.0", diff --git a/client/src/components/comments/Comments/Add.jsx b/client/src/components/comments/Comments/Add.jsx index 56e8b139..24cdf11d 100755 --- a/client/src/components/comments/Comments/Add.jsx +++ b/client/src/components/comments/Comments/Add.jsx @@ -3,16 +3,18 @@ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md */ -import React, { useCallback, useState } from 'react'; -import { useDispatch } from 'react-redux'; +import React, { useCallback, useState, useRef } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import { useTranslation } from 'react-i18next'; -import TextareaAutosize from 'react-textarea-autosize'; -import { Button, Form, TextArea } from 'semantic-ui-react'; +import { Mention, MentionsInput } from 'react-mentions'; +import { Button, Form } from 'semantic-ui-react'; import { useClickAwayListener, useDidUpdate, useToggle } from '../../../lib/hooks'; +import selectors from '../../../selectors'; import entryActions from '../../../entry-actions'; import { useEscapeInterceptor, useForm, useNestedRef } from '../../../hooks'; import { isModifierKeyPressed } from '../../../utils/event-helpers'; +import UserAvatar from '../../users/UserAvatar'; import styles from './Add.module.scss'; @@ -21,13 +23,16 @@ const DEFAULT_DATA = { }; const Add = React.memo(() => { + const boardMemberships = useSelector(selectors.selectMembershipsForCurrentBoard); + const dispatch = useDispatch(); const [t] = useTranslation(); + const [data, , setData] = useForm(DEFAULT_DATA); const [isOpened, setIsOpened] = useState(false); - const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA); const [selectTextFieldState, selectTextField] = useToggle(); - const [textFieldRef, handleTextFieldRef] = useNestedRef(); + const mentionsInputRef = useRef(null); + const textFieldRef = useRef(null); const [buttonRef, handleButtonRef] = useNestedRef(); const submit = useCallback(() => { @@ -47,6 +52,11 @@ const Add = React.memo(() => { }, [dispatch, data, setData, selectTextField, textFieldRef]); const handleEscape = useCallback(() => { + if (mentionsInputRef.current.isOpened()) { + mentionsInputRef.current.clearSuggestions(); + return; + } + setIsOpened(false); textFieldRef.current.blur(); }, [textFieldRef]); @@ -62,6 +72,15 @@ const Add = React.memo(() => { setIsOpened(true); }, []); + const handleFieldChange = useCallback( + (_, text) => { + setData({ + text, + }); + }, + [setData], + ); + const handleFieldKeyDown = useCallback( (event) => { if (isModifierKeyPressed(event) && event.key === 'Enter') { @@ -85,6 +104,16 @@ const Add = React.memo(() => { handleClickAwayCancel, ); + const suggestionRenderer = useCallback( + (entry, _, highlightedDisplay) => ( +
+ + {highlightedDisplay} +
+ ), + [], + ); + useDidUpdate(() => { if (isOpened) { activateEscapeInterceptor(); @@ -99,21 +128,39 @@ const Add = React.memo(() => { return (
-