From 5681126100d7a8ad06a81cf3c0cd2db4841a1ae4 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Thu, 6 Jun 2024 16:17:02 +0200 Subject: [PATCH] feat: rich text and user mention components (DHIS2-15522) (#1525) * feat: add rich text editor/parser components "Moved" from d2-ui-rich-text. * feat: moved RichText and UserMention out of Intepretation components RichText has the Jest tests from the old d2-ui implementation. * feat: add inputHeight prop to control textarea rows This allows for customization of the textarea height. * fix: respect new lines in preview mode DHIS2-15536 * fix: use same styles for text in edit and preview mode DHIS2-15537 * fix: add prop for controlling autofocus DHIS2-15541 * fix: keep toolbar fixed in edit/preview mode DHIS2-15539 * fix: add prop for controlling resizable textarea DHIS2-15540 * fix: make textarea resize according to parent DHIS2-15535 * fix: show search results for the same query (DHIS2-15542) When the same query is run multiple times, data does not change and the useEffect does not trigger. By looking at fetching instead, we can set data every time the query is completed. * fix: port help text in the rich text editor fix (DHIS2-15781) * fix: position caret between bold/italic markers DHIS2-17344 * feat(rich-text): enable heading and lists (DHIS2-17357) * fix: fix user list positioning with wrapping text (DHIS2-17347) * fix: close user list when clicking outside of it (DHIS2-17345) * fix: add a space and put caret after smileys * fix: close user list when clicking outside (DHIS2-17345) This fix is to solve the issue of a click on a list item not selecting the user. Also, a fix for the list positioning when there are newlines in the text. * fix: position the caret between bold/italic markers (DHIS2-17344) This fix is for when using Ctrl + B/I, now the same callback used when clicking the B/I buttons in the toolbar is used. * fix: avoid sending \n in the user search (DHIS2-17523) They cause the user search request to fail with a 400 error. --------- Co-authored-by: Jen Jones Arnesen --- i18n/en.pot | 66 ++--- package.json | 2 +- src/components/AboutAOUnit/AboutAOUnit.js | 8 +- .../InterpretationModal/CommentAddForm.js | 2 +- .../InterpretationModal/CommentUpdateForm.js | 7 +- .../InterpretationsUnit/InterpretationForm.js | 11 +- .../InterpretationUpdateForm.js | 2 +- .../Interpretations/common/Message/Message.js | 2 +- .../common/Message/MessageEditorContainer.js | 1 + .../common/RichTextEditor/index.js | 1 - .../Interpretations/common/index.js | 1 - .../Editor/Editor.js} | 99 +++++--- .../RichText/Editor/__tests__/Editor.spec.js | 47 ++++ .../Editor/__tests__/convertCtrlKey.spec.js | 230 ++++++++++++++++++ .../Editor}/markdownHandler.js | 18 +- .../Editor/styles/Editor.style.js} | 25 +- src/components/RichText/Parser/MdParser.js | 125 ++++++++++ src/components/RichText/Parser/Parser.js | 28 +++ .../Parser/__tests__/MdParser.spec.js | 166 +++++++++++++ .../RichText/Parser/__tests__/Parser.spec.js | 43 ++++ src/components/RichText/index.js | 3 + .../common => }/UserMention/UserList.js | 0 .../UserMention/UserMentionWrapper.js | 42 ++-- .../styles/UserMentionWrapper.style.js | 7 +- .../UserMention/useUserSearchResults.js | 4 +- src/index.js | 2 + yarn.lock | 43 ++-- 27 files changed, 854 insertions(+), 131 deletions(-) delete mode 100644 src/components/Interpretations/common/RichTextEditor/index.js rename src/components/{Interpretations/common/RichTextEditor/RichTextEditor.js => RichText/Editor/Editor.js} (78%) create mode 100644 src/components/RichText/Editor/__tests__/Editor.spec.js create mode 100644 src/components/RichText/Editor/__tests__/convertCtrlKey.spec.js rename src/components/{Interpretations/common/RichTextEditor => RichText/Editor}/markdownHandler.js (86%) rename src/components/{Interpretations/common/RichTextEditor/styles/RichTextEditor.style.js => RichText/Editor/styles/Editor.style.js} (81%) create mode 100644 src/components/RichText/Parser/MdParser.js create mode 100644 src/components/RichText/Parser/Parser.js create mode 100644 src/components/RichText/Parser/__tests__/MdParser.spec.js create mode 100644 src/components/RichText/Parser/__tests__/Parser.spec.js create mode 100644 src/components/RichText/index.js rename src/components/{Interpretations/common => }/UserMention/UserList.js (100%) rename src/components/{Interpretations/common => }/UserMention/UserMentionWrapper.js (88%) rename src/components/{Interpretations/common => }/UserMention/styles/UserMentionWrapper.style.js (88%) rename src/components/{Interpretations/common => }/UserMention/useUserSearchResults.js (94%) diff --git a/i18n/en.pot b/i18n/en.pot index bb2f03c33..936f74c49 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -427,39 +427,6 @@ msgstr "Could not update interpretation" msgid "Enter interpretation text" msgstr "Enter interpretation text" -msgid "Bold text" -msgstr "Bold text" - -msgid "Italic text" -msgstr "Italic text" - -msgid "Link to a URL" -msgstr "Link to a URL" - -msgid "Mention a user" -msgstr "Mention a user" - -msgid "Add emoji" -msgstr "Add emoji" - -msgid "Preview" -msgstr "Preview" - -msgid "Back to write mode" -msgstr "Back to write mode" - -msgid "Too many results. Try refining the search." -msgstr "Too many results. Try refining the search." - -msgid "Search for a user" -msgstr "Search for a user" - -msgid "Searching for \"{{- searchText}}\"" -msgstr "Searching for \"{{- searchText}}\"" - -msgid "No results found" -msgstr "No results found" - msgid "Not available offline" msgstr "Not available offline" @@ -882,6 +849,27 @@ msgstr "Financial Years" msgid "Years" msgstr "Years" +msgid "Bold text" +msgstr "Bold text" + +msgid "Italic text" +msgstr "Italic text" + +msgid "Link to a URL" +msgstr "Link to a URL" + +msgid "Mention a user" +msgstr "Mention a user" + +msgid "Add emoji" +msgstr "Add emoji" + +msgid "Preview" +msgstr "Preview" + +msgid "Back to write mode" +msgstr "Back to write mode" + msgid "Interpretations and details" msgstr "Interpretations and details" @@ -912,6 +900,18 @@ msgstr "Could not load translations" msgid "Retry" msgstr "Retry" +msgid "Too many results. Try refining the search." +msgstr "Too many results. Try refining the search." + +msgid "Search for a user" +msgstr "Search for a user" + +msgid "Searching for \"{{- searchText}}\"" +msgstr "Searching for \"{{- searchText}}\"" + +msgid "No results found" +msgstr "No results found" + msgid "Series" msgstr "Series" diff --git a/package.json b/package.json index 68bdee192..57d596dfc 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,6 @@ "styled-jsx": "^4.0.1" }, "dependencies": { - "@dhis2/d2-ui-rich-text": "^7.4.1", "@dhis2/multi-calendar-dates": "1.0.0", "@dnd-kit/core": "^6.0.7", "@dnd-kit/sortable": "^7.0.2", @@ -71,6 +70,7 @@ "d3-color": "^1.2.3", "highcharts": "^10.3.3", "lodash": "^4.17.21", + "markdown-it": "^13.0.1", "mathjs": "^9.4.2", "react-beautiful-dnd": "^10.1.1", "resize-observer-polyfill": "^1.5.1" diff --git a/src/components/AboutAOUnit/AboutAOUnit.js b/src/components/AboutAOUnit/AboutAOUnit.js index 0d734efc1..1e2d77ab1 100644 --- a/src/components/AboutAOUnit/AboutAOUnit.js +++ b/src/components/AboutAOUnit/AboutAOUnit.js @@ -4,7 +4,6 @@ import { useTimeZoneConversion, } from '@dhis2/app-runtime' import i18n from '@dhis2/d2-i18n' -import { Parser as RichTextParser } from '@dhis2/d2-ui-rich-text' import { Button, CircularLoader, @@ -29,6 +28,7 @@ import React, { useImperativeHandle, } from 'react' import { formatList } from '../../modules/list.js' +import { RichTextParser } from '../RichText/index.js' import styles from './styles/AboutAOUnit.style.js' import { getTranslatedString, AOTypeMap } from './utils.js' @@ -191,7 +191,7 @@ const AboutAOUnit = forwardRef(({ type, id, renderId }, ref) => { )} {data && (
-

{ {data.ao.displayDescription} ) : ( - i18n.t('No description') +

{i18n.t('No description')}

)} -

+

diff --git a/src/components/Interpretations/InterpretationModal/CommentAddForm.js b/src/components/Interpretations/InterpretationModal/CommentAddForm.js index 9cae2d4a7..0b8f98498 100644 --- a/src/components/Interpretations/InterpretationModal/CommentAddForm.js +++ b/src/components/Interpretations/InterpretationModal/CommentAddForm.js @@ -3,8 +3,8 @@ import i18n from '@dhis2/d2-i18n' import { Button } from '@dhis2/ui' import PropTypes from 'prop-types' import React, { useRef, useState } from 'react' +import { RichTextEditor } from '../../RichText/index.js' import { - RichTextEditor, MessageEditorContainer, MessageButtonStrip, MessageInput, diff --git a/src/components/Interpretations/InterpretationModal/CommentUpdateForm.js b/src/components/Interpretations/InterpretationModal/CommentUpdateForm.js index ea9d3812e..dc90d2175 100644 --- a/src/components/Interpretations/InterpretationModal/CommentUpdateForm.js +++ b/src/components/Interpretations/InterpretationModal/CommentUpdateForm.js @@ -3,11 +3,8 @@ import i18n from '@dhis2/d2-i18n' import { Button, spacers, colors } from '@dhis2/ui' import PropTypes from 'prop-types' import React, { useState, useRef } from 'react' -import { - MessageEditorContainer, - RichTextEditor, - MessageButtonStrip, -} from '../common/index.js' +import { RichTextEditor } from '../../RichText/index.js' +import { MessageEditorContainer, MessageButtonStrip } from '../common/index.js' export const CommentUpdateForm = ({ interpretationId, diff --git a/src/components/Interpretations/InterpretationsUnit/InterpretationForm.js b/src/components/Interpretations/InterpretationsUnit/InterpretationForm.js index 2b7e9d02c..c6467a453 100644 --- a/src/components/Interpretations/InterpretationsUnit/InterpretationForm.js +++ b/src/components/Interpretations/InterpretationsUnit/InterpretationForm.js @@ -3,11 +3,8 @@ import i18n from '@dhis2/d2-i18n' import { Button, Input } from '@dhis2/ui' import PropTypes from 'prop-types' import React, { useRef, useState } from 'react' -import { - RichTextEditor, - MessageEditorContainer, - MessageButtonStrip, -} from '../common/index.js' +import { RichTextEditor } from '../../RichText/index.js' +import { MessageEditorContainer, MessageButtonStrip } from '../common/index.js' export const InterpretationForm = ({ type, @@ -46,7 +43,7 @@ export const InterpretationForm = ({ dataTest="interpretation-form" > {showRichTextEditor ? ( -

+ <> -
+ ) : ( setShowRichTextEditor(true)} diff --git a/src/components/Interpretations/common/Interpretation/InterpretationUpdateForm.js b/src/components/Interpretations/common/Interpretation/InterpretationUpdateForm.js index cf900fdf1..9891d7052 100644 --- a/src/components/Interpretations/common/Interpretation/InterpretationUpdateForm.js +++ b/src/components/Interpretations/common/Interpretation/InterpretationUpdateForm.js @@ -3,9 +3,9 @@ import i18n from '@dhis2/d2-i18n' import { Button, spacers, colors } from '@dhis2/ui' import PropTypes from 'prop-types' import React, { useState } from 'react' +import { RichTextEditor } from '../../../RichText/index.js' import { MessageEditorContainer, - RichTextEditor, MessageButtonStrip, InterpretationSharingLink, } from '../index.js' diff --git a/src/components/Interpretations/common/Message/Message.js b/src/components/Interpretations/common/Message/Message.js index 016e8b9d0..94bd37b5a 100644 --- a/src/components/Interpretations/common/Message/Message.js +++ b/src/components/Interpretations/common/Message/Message.js @@ -1,9 +1,9 @@ import { useTimeZoneConversion } from '@dhis2/app-runtime' -import { Parser as RichTextParser } from '@dhis2/d2-ui-rich-text' import { UserAvatar, spacers, colors } from '@dhis2/ui' import moment from 'moment' import PropTypes from 'prop-types' import React from 'react' +import { RichTextParser } from '../../../RichText/index.js' const Message = ({ children, text, created, username }) => { const { fromServerDate } = useTimeZoneConversion() diff --git a/src/components/Interpretations/common/Message/MessageEditorContainer.js b/src/components/Interpretations/common/Message/MessageEditorContainer.js index b4c7ff338..4de5bec5d 100644 --- a/src/components/Interpretations/common/Message/MessageEditorContainer.js +++ b/src/components/Interpretations/common/Message/MessageEditorContainer.js @@ -19,6 +19,7 @@ const MessageEditorContainer = ({ children, currentUser, dataTest }) => ( } .editor { flex-grow: 1; + height: 100%; } `}
diff --git a/src/components/Interpretations/common/RichTextEditor/index.js b/src/components/Interpretations/common/RichTextEditor/index.js deleted file mode 100644 index 31c0113ca..000000000 --- a/src/components/Interpretations/common/RichTextEditor/index.js +++ /dev/null @@ -1 +0,0 @@ -export { RichTextEditor } from './RichTextEditor.js' diff --git a/src/components/Interpretations/common/index.js b/src/components/Interpretations/common/index.js index 562614fb1..d3473298f 100644 --- a/src/components/Interpretations/common/index.js +++ b/src/components/Interpretations/common/index.js @@ -1,4 +1,3 @@ export * from './Interpretation/index.js' export * from './Message/index.js' -export * from './RichTextEditor/index.js' export * from './getInterpretationAccess.js' diff --git a/src/components/Interpretations/common/RichTextEditor/RichTextEditor.js b/src/components/RichText/Editor/Editor.js similarity index 78% rename from src/components/Interpretations/common/RichTextEditor/RichTextEditor.js rename to src/components/RichText/Editor/Editor.js index e8ad9216d..6fdbf558e 100644 --- a/src/components/Interpretations/common/RichTextEditor/RichTextEditor.js +++ b/src/components/RichText/Editor/Editor.js @@ -1,10 +1,9 @@ import i18n from '@dhis2/d2-i18n' -import { Parser as RichTextParser } from '@dhis2/d2-ui-rich-text' import { Button, Popover, Tooltip, - Field, + Help, IconAt24, IconFaceAdd24, IconLink24, @@ -12,9 +11,11 @@ import { IconTextItalic24, colors, } from '@dhis2/ui' +import cx from 'classnames' import PropTypes from 'prop-types' import React, { forwardRef, useRef, useEffect, useState } from 'react' -import { UserMentionWrapper } from '../UserMention/UserMentionWrapper.js' +import { UserMentionWrapper } from '../../UserMention/UserMentionWrapper.js' +import { Parser } from '../Parser/Parser.js' import { convertCtrlKey, insertMarkdown, @@ -33,22 +34,22 @@ import { toolbarClasses, tooltipAnchorClasses, emojisPopoverClasses, -} from './styles/RichTextEditor.style.js' +} from './styles/Editor.style.js' const EmojisPopover = ({ onInsertMarkdown, onClose, reference }) => (
  • onInsertMarkdown(EMOJI_SMILEY_FACE)}> - {emojis[EMOJI_SMILEY_FACE]} + {emojis[EMOJI_SMILEY_FACE]}
  • onInsertMarkdown(EMOJI_SAD_FACE)}> - {emojis[EMOJI_SAD_FACE]} + {emojis[EMOJI_SAD_FACE]}
  • onInsertMarkdown(EMOJI_THUMBS_UP)}> - {emojis[EMOJI_THUMBS_UP]} + {emojis[EMOJI_THUMBS_UP]}
  • onInsertMarkdown(EMOJI_THUMBS_DOWN)}> - {emojis[EMOJI_THUMBS_DOWN]} + {emojis[EMOJI_THUMBS_DOWN]}
@@ -190,29 +191,59 @@ Toolbar.propTypes = { disabled: PropTypes.bool, } -export const RichTextEditor = forwardRef( +export const Editor = forwardRef( ( - { value, disabled, inputPlaceholder, onChange, errorText, helpText }, + { + value, + disabled, + inputPlaceholder, + onChange, + errorText, + helpText, + initialFocus, + resizable, + }, externalRef ) => { const [previewMode, setPreviewMode] = useState(false) const internalRef = useRef() const textareaRef = externalRef || internalRef + const caretPosRef = useRef(undefined) - useEffect(() => textareaRef.current?.focus(), [textareaRef]) + const insertMarkdownCallback = (text, caretPos) => { + caretPosRef.current = caretPos + onChange(text) + textareaRef.current.focus() + } + + useEffect(() => { + if (initialFocus) { + textareaRef.current?.focus() + } + }, [initialFocus, textareaRef]) + + useEffect(() => { + if (caretPosRef.current) { + textareaRef.current?.setSelectionRange( + caretPosRef.current, + caretPosRef.current + ) + + caretPosRef.current = undefined + } + }, [value, textareaRef]) return ( -
+
{ insertMarkdown( markdown, textareaRef.current, - (text, caretPos) => { - onChange(text) - textareaRef.current.focus() - textareaRef.current.selectionEnd = caretPos - } + insertMarkdownCallback ) if (markdown === MENTION) { @@ -231,20 +262,18 @@ export const RichTextEditor = forwardRef( /> {previewMode ? (
- {value} + {value}
) : ( - +