diff --git a/openmetadata-ui/src/main/resources/ui/cypress/integration/Pages/Service.spec.js b/openmetadata-ui/src/main/resources/ui/cypress/integration/Pages/Service.spec.js index ebebbf36920..e9174409507 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/integration/Pages/Service.spec.js +++ b/openmetadata-ui/src/main/resources/ui/cypress/integration/Pages/Service.spec.js @@ -23,10 +23,10 @@ const updateService = () => { .type(service.newDescription); cy.get('[data-testid="save"]').click(); cy.get( - '[data-testid="description"] > [data-testid="viewer-container"] > p' + '[data-testid="description"] > [data-testid="viewer-container"] > [data-testid="markdown-parser"] > :nth-child(1) > .toastui-editor-contents > p' ).contains(service.newDescription); cy.get(':nth-child(1) > .link-title').click(); - cy.get('[data-testid="viewer-container"] > p').contains( + cy.get('.toastui-editor-contents > p').contains( service.newDescription ); }; diff --git a/openmetadata-ui/src/main/resources/ui/package.json b/openmetadata-ui/src/main/resources/ui/package.json index e8a959c8587..83867d7cfdd 100644 --- a/openmetadata-ui/src/main/resources/ui/package.json +++ b/openmetadata-ui/src/main/resources/ui/package.json @@ -25,7 +25,7 @@ "@okta/okta-auth-js": "^6.4.0", "@okta/okta-react": "^6.4.3", "@rjsf/core": "^4.1.1", - "@toast-ui/react-editor": "^3.1.3", + "@toast-ui/react-editor": "^3.1.8", "antd": "^4.20.6", "antlr4": "4.9.2", "autoprefixer": "^9.8.6", diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/markdown.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/markdown.svg new file mode 100644 index 00000000000..849c55aeb19 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/markdown.svg @@ -0,0 +1,4 @@ + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/EditorToolBar.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/EditorToolBar.ts new file mode 100644 index 00000000000..705d0fe45b7 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/EditorToolBar.ts @@ -0,0 +1,45 @@ +import MarkdownIcon from '../../../assets/svg/markdown.svg'; + +/** + * Read more : https://nhn.github.io/tui.editor/latest/tutorial-example15-customizing-toolbar-buttons + * @returns HTMLElement for toolbar + */ +const markdownButton = (): HTMLButtonElement => { + const button = document.createElement('button'); + + button.className = 'toastui-editor-toolbar-icons markdown-icon'; + button.style.backgroundImage = 'none'; + button.style.margin = '0'; + button.style.marginTop = '4px'; + button.innerHTML = ` + + markdown-icon + `; + + return button; +}; + +export const EDITOR_TOOLBAR_ITEMS = [ + 'heading', + 'bold', + 'italic', + 'strike', + 'ul', + 'ol', + 'link', + 'hr', + 'quote', + 'code', + 'codeblock', + { + name: 'Markdown Guide', + el: markdownButton(), + tooltip: 'Markdown Guide', + }, +]; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditor.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditor.tsx index 38595b2c397..6b3787bcde1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditor.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditor.tsx @@ -15,6 +15,7 @@ import { Editor, Viewer } from '@toast-ui/react-editor'; import classNames from 'classnames'; +import { uniqueId } from 'lodash'; import React, { createRef, forwardRef, @@ -22,6 +23,7 @@ import React, { useImperativeHandle, useState, } from 'react'; +import { EDITOR_TOOLBAR_ITEMS } from './EditorToolBar'; import './RichTextEditor.css'; import { editorRef, RichTextEditorProp } from './RichTextEditor.interface'; @@ -32,7 +34,7 @@ const RichTextEditor = forwardRef( previewStyle = 'tab', editorType = 'markdown', previewHighlight = false, - useCommandShortcut = false, + useCommandShortcut = true, extendedAutolinks = true, hideModeSwitch = true, initialValue = '', @@ -75,6 +77,7 @@ const RichTextEditor = forwardRef( @@ -82,7 +85,7 @@ const RichTextEditor = forwardRef(
( previewHighlight={previewHighlight} previewStyle={previewStyle} ref={richTextEditorRef} - toolbarItems={[['bold', 'italic', 'ul', 'ol', 'link']]} + toolbarItems={[EDITOR_TOOLBAR_ITEMS]} useCommandShortcut={useCommandShortcut} onChange={onChangeHandler} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditorPreviewer.less b/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditorPreviewer.less new file mode 100644 index 00000000000..1fedefff00b --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditorPreviewer.less @@ -0,0 +1,9 @@ +body { + .toastui-editor-contents { + p { + margin-top: 0px; + margin-bottom: 10px; + color: #37352f; + } + } +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditorPreviewer.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditorPreviewer.test.tsx index ab74a80122e..a0690207393 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditorPreviewer.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditorPreviewer.test.tsx @@ -16,8 +16,12 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import RichTextEditorPreviewer from './RichTextEditorPreviewer'; +const mockDescription = + // eslint-disable-next-line max-len + '**Headings**\n\n# H1\n## H2\n### H3\n\n***\n**Bold**\n\n**bold text**\n\n\n***\n**Italic**\n\n*italic*\n\n***\n**BlockQuote**\n\n> blockquote\n\n***\n**Ordered List**\n\n1. First item\n2. Second item\n3. Third item\n\n\n***\n**Unordered List**\n\n- First item\n- Second item\n- Third item\n\n\n***\n**Code**\n\n`code`\n\n\n***\n**Horizontal Rule**\n\n---\n\n\n***\n**Link**\n[title](https://www.example.com)\n\n\n***\n**Image**\n\n![alt text](https://github.com/open-metadata/OpenMetadata/blob/main/docs/.gitbook/assets/openmetadata-banner.png?raw=true)\n\n\n***\n**Table**\n\n| Syntax | Description |\n| ----------- | ----------- |\n| Header | Title |\n| Paragraph | Text |\n***\n\n**Fenced Code Block**\n\n```\n{\n "firstName": "John",\n "lastName": "Smith",\n "age": 25\n}\n```\n\n\n***\n**Strikethrough**\n~~The world is flat.~~\n'; + const mockProp = { - markdown: '', + markdown: mockDescription, className: '', blurClasses: 'see-more-blur', maxHtClass: 'tw-h-24', @@ -25,20 +29,6 @@ const mockProp = { enableSeeMoreVariant: true, }; -jest.mock('react-markdown', () => { - return jest.fn().mockImplementation(() => { - return

markdown parser

; - }); -}); - -jest.mock('rehype-raw', () => { - return jest.fn(); -}); - -jest.mock('remark-gfm', () => { - return jest.fn(); -}); - describe('Test RichTextEditor Previewer Component', () => { it('Should render RichTextEditorViewer Component', async () => { const { container } = render(, { @@ -53,4 +43,190 @@ describe('Test RichTextEditor Previewer Component', () => { expect(markdownParser).toBeInTheDocument(); }); + + it('Should render bold markdown content', async () => { + const { container } = render(, { + wrapper: MemoryRouter, + }); + + const markdownParser = await findByTestId(container, 'markdown-parser'); + + const boldMarkdown = markdownParser.querySelectorAll('strong'); + + expect(boldMarkdown).toHaveLength(boldMarkdown.length); + + expect(markdownParser).toBeInTheDocument(); + }); + + it('Should render strikethrough markdown content', async () => { + const { container } = render(, { + wrapper: MemoryRouter, + }); + + const markdownParser = await findByTestId(container, 'markdown-parser'); + + const strikeThroughMarkdown = markdownParser.querySelector('del'); + + expect(strikeThroughMarkdown).toBeInTheDocument(); + + expect(markdownParser).toBeInTheDocument(); + }); + + it('Should render headings markdown content', async () => { + const { container } = render(, { + wrapper: MemoryRouter, + }); + + const markdownParser = await findByTestId(container, 'markdown-parser'); + + const heading1 = markdownParser.querySelector('h1'); + const heading2 = markdownParser.querySelector('h2'); + const heading3 = markdownParser.querySelector('h3'); + + expect(heading1).toBeInTheDocument(); + expect(heading2).toBeInTheDocument(); + expect(heading3).toBeInTheDocument(); + + expect(markdownParser).toBeInTheDocument(); + }); + + it('Should render italic markdown content', async () => { + const { container } = render(, { + wrapper: MemoryRouter, + }); + + const markdownParser = await findByTestId(container, 'markdown-parser'); + + const italicMarkdown = markdownParser.querySelector('em'); + + expect(italicMarkdown).toBeInTheDocument(); + + expect(markdownParser).toBeInTheDocument(); + }); + + it('Should render blockquote markdown content', async () => { + const { container } = render(, { + wrapper: MemoryRouter, + }); + + const markdownParser = await findByTestId(container, 'markdown-parser'); + + const blockquoteMarkdown = markdownParser.querySelector('blockquote'); + + expect(blockquoteMarkdown).toBeInTheDocument(); + + expect(markdownParser).toBeInTheDocument(); + }); + + it('Should render ordered list markdown content', async () => { + const { container } = render(, { + wrapper: MemoryRouter, + }); + + const markdownParser = await findByTestId(container, 'markdown-parser'); + + const orderedList = markdownParser.querySelector('ol'); + + expect(orderedList).toBeInTheDocument(); + + expect(markdownParser).toBeInTheDocument(); + }); + + it('Should render unordered list markdown content', async () => { + const { container } = render(, { + wrapper: MemoryRouter, + }); + + const markdownParser = await findByTestId(container, 'markdown-parser'); + + const unorderedList = markdownParser.querySelector('ul'); + + expect(unorderedList).toBeInTheDocument(); + + expect(markdownParser).toBeInTheDocument(); + }); + + it('Should render code markdown content', async () => { + const { container } = render(, { + wrapper: MemoryRouter, + }); + + const markdownParser = await findByTestId(container, 'markdown-parser'); + + const code = markdownParser.querySelector('code'); + + expect(code).toBeInTheDocument(); + + expect(markdownParser).toBeInTheDocument(); + }); + + it('Should render code block markdown content', async () => { + const { container } = render(, { + wrapper: MemoryRouter, + }); + + const markdownParser = await findByTestId(container, 'markdown-parser'); + + const codeBlock = markdownParser.querySelector('pre'); + + expect(codeBlock).toBeInTheDocument(); + + expect(markdownParser).toBeInTheDocument(); + }); + + it('Should render horizontal rule markdown content', async () => { + const { container } = render(, { + wrapper: MemoryRouter, + }); + + const markdownParser = await findByTestId(container, 'markdown-parser'); + + const horizontalRule = markdownParser.querySelector('hr'); + + expect(horizontalRule).toBeInTheDocument(); + + expect(markdownParser).toBeInTheDocument(); + }); + + it('Should render link markdown content', async () => { + const { container } = render(, { + wrapper: MemoryRouter, + }); + + const markdownParser = await findByTestId(container, 'markdown-parser'); + + const link = markdownParser.querySelector('a'); + + expect(link).toBeInTheDocument(); + + expect(markdownParser).toBeInTheDocument(); + }); + + it('Should render image markdown content', async () => { + const { container } = render(, { + wrapper: MemoryRouter, + }); + + const markdownParser = await findByTestId(container, 'markdown-parser'); + + const image = markdownParser.querySelector('img'); + + expect(image).toBeInTheDocument(); + + expect(markdownParser).toBeInTheDocument(); + }); + + it('Should render table markdown content', async () => { + const { container } = render(, { + wrapper: MemoryRouter, + }); + + const markdownParser = await findByTestId(container, 'markdown-parser'); + + const table = markdownParser.querySelector('table'); + + expect(table).toBeInTheDocument(); + + expect(markdownParser).toBeInTheDocument(); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditorPreviewer.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditorPreviewer.tsx index 13ed40b3ea8..366d7437512 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditorPreviewer.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditorPreviewer.tsx @@ -11,16 +11,13 @@ * limitations under the License. */ +import { Viewer } from '@toast-ui/react-editor'; import classNames from 'classnames'; +import { uniqueId } from 'lodash'; import React, { useEffect, useState } from 'react'; -// Markdown Parser and plugin imports -import MarkdownParser from 'react-markdown'; -import { Link } from 'react-router-dom'; -import rehypeRaw from 'rehype-raw'; -import remarkGfm from 'remark-gfm'; -import { isExternalUrl } from '../../../utils/StringsUtils'; import { BlurLayout } from './BlurLayout'; import { PreviewerProp } from './RichTextEditor.interface'; +import './RichTextEditorPreviewer.less'; export const MAX_LENGTH = 300; @@ -33,21 +30,14 @@ const RichTextEditorPreviewer = ({ enableSeeMoreVariant = true, }: PreviewerProp) => { const [content, setContent] = useState(''); - const [displayMoreText, setDisplayMoreText] = useState(false); - - const setModifiedContent = (markdownValue: string) => { - const modifiedContent = markdownValue - .replace(/</g, '<') - .replace(/>/g, '>'); - setContent(modifiedContent); - }; + const [displayMoreText, setDisplayMoreText] = useState(false); const displayMoreHandler = () => { setDisplayMoreText((pre) => !pre); }; useEffect(() => { - setModifiedContent(markdown); + setContent(markdown); }, [markdown]); return ( @@ -63,54 +53,10 @@ const RichTextEditorPreviewer = ({ } )} data-testid="viewer-container"> - { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { ordered, ...rest } = props; +
+ +
- return ( -
    - {children} -
- ); - }, - ol: ({ children, ...props }) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { ordered, ...rest } = props; - - return ( -
    - {children} -
- ); - }, - code: ({ children, ...props }) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { inline, ...rest } = props; - - return ( - - {children} - - ); - }, - a: ({ children, ...props }) => { - const href = props.href; - if (isExternalUrl(href)) { - return {children}; - } else { - return {children}; - } - }, - }} - rehypePlugins={[rehypeRaw]} - remarkPlugins={[remarkGfm]}> - {content} -