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 = `
+
+
+ `;
+
+ 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\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 (
-
- );
- },
- 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}
-