Adding new markdown editor (#53)

* Adding new markdown editor

* minor changes

* adding heading option

* fixed selection issue

* added italic and unordered options

* styling changes

* removed commented code

* minor style changes

* removed headings from markdown

* minor change
This commit is contained in:
Sachin Chaurasiya 2021-08-12 00:31:19 +05:30 committed by GitHub
parent 4464d81857
commit ae8e01c655
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1465 additions and 216 deletions

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,7 @@
"builtin-status-codes": "^3.0.0",
"cookie-storage": "^6.1.0",
"core-js": "^3.10.1",
"draft-js": "^0.11.7",
"eslint": "^6.6.0",
"eslint-config-react-app": "^5.2.1",
"eslint-loader": "3.0.3",
@ -34,10 +35,13 @@
"html-react-parser": "^1.2.6",
"https-browserify": "^1.0.0",
"ieee754": "^1.2.1",
"immutable": "^4.0.0-rc.14",
"jquery": "^3.5.0",
"markdown-draft-js": "^2.3.0",
"mobx": "6.0.1",
"mobx-react": "6.1.4",
"oidc-client": "^1.11.5",
"path-browserify": "^1.0.1",
"popper.js": "1.16.1",
"postcss": "^8.3.2",
"process": "^0.11.10",
@ -45,13 +49,17 @@
"react": "^16.14.0",
"react-bootstrap": "^1.6.0",
"react-dom": "^16.14.0",
"react-draft-wysiwyg": "^1.14.7",
"react-js-pagination": "^3.0.3",
"react-markdown": "^6.0.3",
"react-oidc": "^1.0.3",
"react-router-dom": "^5.2.0",
"react-select": "^3.1.1",
"react-syntax-highlighter": "^15.4.4",
"react-tippy": "^1.4.0",
"recharts": "^1.8.5",
"redoc": "https://github.com/deuex-solutions/redoc/tarball/master",
"remark-gfm": "^1.0.0",
"resolve": "1.15.0",
"resolve-url-loader": "3.1.1",
"slate": "^0.59.0",

View File

@ -17,12 +17,12 @@
import classnames from 'classnames';
import React, { FunctionComponent, useRef, useState } from 'react';
import { stringToDOMElement } from '../../../utils/StringsUtils';
// import { stringToDOMElement } from '../../../utils/StringsUtils';
import { Button } from '../../buttons/Button/Button';
import { MarkdownWithPreview } from '../../common/editor/MarkdownWithPreview';
import MarkdownWithPreview from '../../common/editor/MarkdownWithPreview';
type MarkdownRef = {
fetchUpdatedHTML: () => string;
type EditorContentRef = {
getEditorContent: () => string;
};
type Props = {
@ -38,7 +38,7 @@ type Props = {
export const ModalWithMarkdownEditor: FunctionComponent<Props> = ({
isExpandable = false,
header,
placeholder,
// placeholder,
value,
onSave,
// onSuggest,
@ -46,7 +46,7 @@ export const ModalWithMarkdownEditor: FunctionComponent<Props> = ({
}: Props) => {
const [expanded, setExpanded] = useState<boolean>(false);
const markdownRef = useRef<MarkdownRef>();
const markdownRef = useRef<EditorContentRef>();
const getContainerClasses = () => {
return classnames(
@ -57,8 +57,7 @@ export const ModalWithMarkdownEditor: FunctionComponent<Props> = ({
const handleSaveData = () => {
if (markdownRef.current) {
const updatedHTML = markdownRef.current.fetchUpdatedHTML();
onSave(stringToDOMElement(updatedHTML).textContent ? updatedHTML : '');
onSave(markdownRef.current?.getEditorContent() ?? '');
}
};
@ -99,11 +98,7 @@ export const ModalWithMarkdownEditor: FunctionComponent<Props> = ({
)}
</div>
<div className="tw-modal-body tw-pt-0 tw-pb-1">
<MarkdownWithPreview
editorRef={(Ref: MarkdownRef) => (markdownRef.current = Ref)}
placeholder={placeholder}
value={value}
/>
<MarkdownWithPreview ref={markdownRef} value={value} />
</div>
<div className="tw-modal-footer">
<Button

View File

@ -16,126 +16,109 @@
*/
import React, {
FunctionComponent,
useCallback,
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
} from 'react';
import { deserializeData } from '../../../utils/EditorUtils';
import { stringToDOMElement, stringToHTML } from '../../../utils/StringsUtils';
import MarkdownEditor from './Editor';
import { isValidJSONString } from '../../../utils/StringsUtils';
import RichTextEditor from '../rich-text-editor/RichTextEditor';
import { editorRef } from '../rich-text-editor/RichTextEditor.interface';
import RichTextEditorPreviewer from '../rich-text-editor/RichTextEditorPreviewer';
type MarkdownWithPreview = {
fetchUpdatedHTML: () => string;
type EditorContentRef = {
getEditorContent: (value: string) => string;
};
type Props = {
value: string;
placeholder: string;
editorRef: Function;
};
const getDeserializedNodes = (strElm: string): Array<Node> => {
const domElm = stringToDOMElement(`<div>${strElm}</div>`);
const MarkdownWithPreview = forwardRef<editorRef, Props>(
({ value }: Props, ref) => {
const [activeTab, setActiveTab] = useState<number>(1);
const [preview, setPreview] = useState<string>('');
const [initValue, setInitValue] = useState<string>(value ?? '');
return deserializeData(domElm.childNodes[0]);
};
export const MarkdownWithPreview: FunctionComponent<Props> = ({
editorRef,
placeholder,
value,
}: Props) => {
const [activeTab, setActiveTab] = useState<number>(1);
const [preview, setPreview] = useState<string>('');
const [initValue, setInitValue] = useState<Array<Node>>(
getDeserializedNodes(value || ' ')
);
const markdownRef = useRef<MarkdownWithPreview>();
const getTabClasses = (tab: number, activeTab: number) => {
return (
'tw-gh-tabs tw-cursor-pointer' + (activeTab === tab ? ' active' : '')
);
};
const getPreviewHTML = useCallback(() => {
return stringToHTML(preview);
}, [preview]);
const updateInternalValue = () => {
if (markdownRef.current) {
const updatedHTML = markdownRef.current.fetchUpdatedHTML();
setInitValue(
getDeserializedNodes(
stringToDOMElement(updatedHTML).textContent ? updatedHTML : ''
)
const editorRef = useRef<EditorContentRef>();
const getTabClasses = (tab: number, activeTab: number) => {
return (
'tw-gh-tabs tw-cursor-pointer' + (activeTab === tab ? ' active' : '')
);
setPreview(updatedHTML);
}
};
};
const handleSaveData = () => {
if (markdownRef.current) {
const updatedHTML = markdownRef.current.fetchUpdatedHTML();
const updateInternalValue = () => {
if (editorRef.current) {
setInitValue(editorRef.current?.getEditorContent('markdown'));
setPreview(editorRef.current?.getEditorContent('markdown'));
}
};
return stringToDOMElement(updatedHTML).textContent ? updatedHTML : '';
} else {
return '';
}
};
useEffect(() => {
if (typeof editorRef === 'function') {
editorRef({
fetchUpdatedHTML: () => {
return handleSaveData();
},
});
}
}, []);
const getPreview = () => {
if (preview.length < 1) {
return 'Nothing to preview';
}
return (
<>
<div className="tw-bg-transparent">
<nav className="tw-flex tw-flex-row tw-gh-tabs-container tw-px-6">
<p
className={getTabClasses(1, activeTab)}
data-testid="tab"
onClick={(e) => {
e.stopPropagation();
setActiveTab(1);
}}>
{'Write '}
</p>
<p
className={getTabClasses(2, activeTab)}
data-testid="tab"
onClick={(e) => {
e.stopPropagation();
setActiveTab(2);
updateInternalValue();
}}>
{'View '}
</p>
</nav>
return <RichTextEditorPreviewer markdown={preview} />;
};
useImperativeHandle(ref, () => ({
getEditorContent() {
return activeTab === 2
? initValue
: editorRef.current?.getEditorContent('markdown');
},
}));
useEffect(() => {
setInitValue(value ?? '');
}, [value]);
return (
<div>
<div className="tw-bg-transparent">
<nav className="tw-flex tw-flex-row tw-gh-tabs-container tw-px-6">
<p
className={getTabClasses(1, activeTab)}
data-testid="tab"
onClick={(e) => {
e.stopPropagation();
setActiveTab(1);
}}>
{'Write '}
</p>
<p
className={getTabClasses(2, activeTab)}
data-testid="tab"
onClick={(e) => {
e.stopPropagation();
setActiveTab(2);
updateInternalValue();
}}>
{'View '}
</p>
</nav>
</div>
<div className="tw-py-5">
{activeTab === 1 && (
<RichTextEditor
format={isValidJSONString(initValue) ? 'json' : 'markdown'}
initvalue={initValue}
ref={editorRef}
/>
)}
{activeTab === 2 && (
<div className="editor-wrapper tw-flex tw-flex-col tw-flex-1 tw-overflow-y-auto tw-p-3 tw-pl-6 tw-min-h-32 tw-border tw-border-gray-300 tw-rounded tw-max-h-none">
{getPreview()}
</div>
)}
</div>
</div>
<div className="tw-py-5">
{activeTab === 1 && (
<MarkdownEditor
className=""
contentListClasses="tw-z-9999"
editorRef={(ref) => (markdownRef.current = ref)}
initValue={initValue}
placeholder={placeholder}
/>
)}
{activeTab === 2 && (
<div className="editor-wrapper tw-flex tw-flex-col tw-flex-1 tw-overflow-y-auto tw-p-3 tw-min-h-32 tw-border tw-border-gray-300 tw-rounded tw-max-h-none">
{getPreviewHTML()}
</div>
)}
</div>
</>
);
};
);
}
);
MarkdownWithPreview.displayName = 'MarkdownWithPreview';
export default MarkdownWithPreview;

View File

@ -0,0 +1,32 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ReactNode } from 'react';
export type editorRef = ReactNode | HTMLElement | string;
export enum Format {
JSON = 'json',
MARKDOWN = 'markdown',
}
export type EditorProp = {
format: 'json' | 'markdown';
initvalue?: string;
suggestionList?: { text: string; value: string; url: string }[];
mentionTrigger?: string;
readonly?: boolean;
customOptions?: ReactNode[];
};

View File

@ -0,0 +1,142 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { convertFromRaw, convertToRaw, EditorState } from 'draft-js';
import { draftToMarkdown, markdownToDraft } from 'markdown-draft-js';
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useState,
} from 'react';
import { Editor } from 'react-draft-wysiwyg';
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
import { EditorProp, editorRef, Format } from './RichTextEditor.interface';
import { Bold, Italic, Link, ULLIST } from './ToolBarOptions';
const getIntialContent = (format: string, content?: string) => {
/*eslint-disable */
if (content) {
switch (format) {
case Format.MARKDOWN:
const rawData = markdownToDraft(content, {
remarkablePreset: 'commonmark',
remarkableOptions: {
disable: {
inline: ['links', 'emphasis'],
block: ['heading', 'code', 'list'],
},
enable: {
block: 'table',
core: ['abbr'],
},
},
preserveNewlines: true,
});
const modifiedBlock = rawData.blocks.filter((data: any) => {
if (data.text) {
return data;
}
});
const state = convertFromRaw({ ...rawData, blocks: modifiedBlock });
return EditorState.createWithContent(state);
case Format.JSON:
const jsonData = convertFromRaw(JSON.parse(content));
return EditorState.createWithContent(jsonData);
default:
return EditorState.createEmpty();
}
} else {
return EditorState.createEmpty();
}
};
const RichTextEditor = forwardRef<editorRef, EditorProp>(
(
{
format = 'markdown',
initvalue,
readonly = false,
customOptions,
}: EditorProp,
ref
) => {
const [editorState, setEditorState] = useState(
getIntialContent(format, initvalue)
);
const onEditorStateChange = (newState: typeof editorState) => {
setEditorState(newState);
};
useImperativeHandle(ref, () => ({
getEditorContent(format: 'json' | 'markdown') {
switch (format) {
case Format.MARKDOWN:
return draftToMarkdown(
convertToRaw(editorState.getCurrentContent())
);
case Format.JSON:
default:
return convertToRaw(editorState.getCurrentContent());
}
},
}));
useEffect(() => {
setEditorState(getIntialContent(format, initvalue));
}, [initvalue, format]);
return (
<>
<div className="tw-min-h-32 tw-border tw-border-gray-300 tw-rounded tw-overflow-y-auto">
<Editor
editorClassName="tw-px-1 tw-min-h-32"
editorState={editorState}
readOnly={readonly}
toolbar={{
options: [],
}}
toolbarClassName="tw-py-2 tw-border tw-border-gray-300"
toolbarCustomButtons={
customOptions ?? [
<Bold key="bold" />,
<Italic key="italic" />,
<Link key="link" />,
<ULLIST key="ulList" />,
]
}
toolbarHidden={readonly}
wrapperClassName="editor-wrapper"
onEditorStateChange={onEditorStateChange}
/>
</div>
<p className="tw-pt-2">Using headings in markdown is not allowed</p>
</>
);
}
);
RichTextEditor.displayName = 'RichTextEditor';
export default RichTextEditor;

View File

@ -0,0 +1,49 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useEffect, useState } from 'react';
import ReactMarkdown from 'react-markdown';
import gfm from 'remark-gfm';
/*eslint-disable */
const RichTextEditorPreviewer = ({ markdown }: { markdown: string }) => {
const [content, setContent] = useState<string>('');
useEffect(() => {
setContent(markdown);
}, [markdown]);
return (
<div className="content-container">
<ReactMarkdown
children={content
.replaceAll(/&lt;/g, '<')
.replaceAll(/&gt;/g, '>')
.replaceAll('\\', '')}
components={{
h1: 'p',
h2: 'p',
h3: 'p',
h4: 'p',
h5: 'p',
h6: 'p',
}}
remarkPlugins={[gfm]}
/>
</div>
);
};
export default RichTextEditorPreviewer;

View File

@ -0,0 +1,333 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EditorState, Modifier, SelectionState } from 'draft-js';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import PopOver from '../popover/PopOver';
/*eslint-disable */
const getSelectedText = (editorState) => {
const selection = editorState.getSelection();
const anchorKey = selection.getAnchorKey();
const currentContent = editorState.getCurrentContent();
const currentBlock = currentContent.getBlockForKey(anchorKey);
const start = selection.getStartOffset();
const end = selection.getEndOffset();
const selectedText = currentBlock.getText().slice(start, end);
return selectedText;
};
const updateEditorSelection = (eState, offsetDiff) => {
const selection = eState.getSelection();
const newFocusOffset = selection.focusOffset + offsetDiff;
const newSelection = new SelectionState({
anchorKey: selection.anchorKey,
anchorOffset: newFocusOffset,
focusKey: selection.focusKey,
focusOffset: newFocusOffset,
});
const newEditorState = EditorState.forceSelection(eState, newSelection);
return EditorState.push(newEditorState, newEditorState.getCurrentContent());
};
export class Bold extends Component {
static propTypes = {
onChange: PropTypes.func,
editorState: PropTypes.object,
};
makeBold = () => {
const { editorState, onChange } = this.props;
const selectedText = getSelectedText(editorState);
const contentState = Modifier.replaceText(
editorState.getCurrentContent(),
editorState.getSelection(),
`${
selectedText.startsWith('**') && selectedText.endsWith('**')
? selectedText.replaceAll('**', '')
: `**${selectedText}**`
}`,
editorState.getCurrentInlineStyle()
);
const eState = EditorState.push(
editorState,
contentState,
'insert-characters'
);
onChange(updateEditorSelection(eState, -2));
};
render() {
return (
<div className="rdw-option-wrapper tw-font-bold" onClick={this.makeBold}>
<PopOver
position="bottom"
size="small"
title="Add bold text"
trigger="mouseenter">
<p>B</p>
</PopOver>
</div>
);
}
}
export class Link extends Component {
static propTypes = {
onChange: PropTypes.func,
editorState: PropTypes.object,
};
makeLink = () => {
const { editorState, onChange } = this.props;
const selectedText = getSelectedText(editorState);
const contentState = Modifier.replaceText(
editorState.getCurrentContent(),
editorState.getSelection(),
`${
selectedText.startsWith('[') && selectedText.endsWith(')')
? selectedText.replace(/ *\([^)]*\) */g, '').replace(/[\])}[{(]/g, '')
: `[${selectedText}](url)`
}`,
editorState.getCurrentInlineStyle()
);
const eState = EditorState.push(
editorState,
contentState,
'insert-characters'
);
onChange(updateEditorSelection(eState, -6));
};
render() {
return (
<div className="rdw-option-wrapper " onClick={this.makeLink}>
<PopOver
position="bottom"
size="small"
title="Add link"
trigger="mouseenter">
<i className="fas fa-link" />
</PopOver>
</div>
);
}
}
export class Italic extends Component {
static propTypes = {
onChange: PropTypes.func,
editorState: PropTypes.object,
};
makeItalic = () => {
const { editorState, onChange } = this.props;
const selectedText = getSelectedText(editorState);
const contentState = Modifier.replaceText(
editorState.getCurrentContent(),
editorState.getSelection(),
`${
selectedText.startsWith('_') && selectedText.endsWith('_')
? selectedText.replaceAll('_', '')
: `_${selectedText}_`
}`,
editorState.getCurrentInlineStyle()
);
const eState = EditorState.push(
editorState,
contentState,
'insert-characters'
);
onChange(updateEditorSelection(eState, -1));
};
render() {
return (
<div className="rdw-option-wrapper " onClick={this.makeItalic}>
<PopOver
position="bottom"
size="small"
title="Add italic text"
trigger="mouseenter">
<i className="fas fa-italic" />
</PopOver>
</div>
);
}
}
export class Heading extends Component {
static propTypes = {
onChange: PropTypes.func,
editorState: PropTypes.object,
};
makeHeading = () => {
const { editorState, onChange } = this.props;
const selectedText = getSelectedText(editorState);
const contentState = Modifier.replaceText(
editorState.getCurrentContent(),
editorState.getSelection(),
`${
selectedText.startsWith('### ')
? selectedText.replaceAll('### ', '')
: `### ${selectedText}`
}`,
editorState.getCurrentInlineStyle()
);
const eState = EditorState.push(
editorState,
contentState,
'insert-characters'
);
onChange(updateEditorSelection(eState, 0));
};
render() {
return (
<div className="rdw-option-wrapper" onClick={this.makeHeading}>
<PopOver
position="bottom"
size="small"
title="Add header text"
trigger="mouseenter">
<p>H</p>
</PopOver>
</div>
);
}
}
export class ULLIST extends Component {
static propTypes = {
onChange: PropTypes.func,
editorState: PropTypes.object,
};
makeLIST = () => {
const { editorState, onChange } = this.props;
const selectedText = getSelectedText(editorState);
const selection = editorState.getSelection();
const currentKey = selection.getStartKey();
const currentBlock = editorState
.getCurrentContent()
.getBlockForKey(currentKey);
const text = selectedText.startsWith('- ')
? selectedText.replaceAll('- ', '')
: `${
selection.anchorOffset > 0 && selectedText.length <= 0
? `\n\n- ${selectedText}`
: `- ${selectedText}`
}`;
const contentState = Modifier.replaceText(
editorState.getCurrentContent(),
editorState.getSelection(),
text,
editorState.getCurrentInlineStyle()
);
onChange(EditorState.push(editorState, contentState, 'insert-characters'));
};
render() {
return (
<div className="rdw-option-wrapper " onClick={this.makeLIST}>
<PopOver
position="bottom"
size="small"
title="Add unordered list"
trigger="mouseenter">
<i className="fas fa-list-ul" />
</PopOver>
</div>
);
}
}
export class OLLIST extends Component {
static propTypes = {
onChange: PropTypes.func,
editorState: PropTypes.object,
};
makeLIST = () => {
const { editorState, onChange } = this.props;
const selectedText = getSelectedText(editorState);
const selection = editorState.getSelection();
const currentKey = selection.getStartKey();
const currentBlock = editorState
.getCurrentContent()
.getBlockForKey(currentKey);
const textArr = currentBlock.getText().split('\n') || [];
const lastText = textArr[textArr.length - 1];
const match = lastText.match(/(\d+)/)?.[0];
let len = 0;
for (const txt of textArr) {
len += txt.length;
if (len >= selection.focusOffset) {
const index = textArr.indexOf(txt);
break;
}
len++;
}
const newSelection = new SelectionState({
anchorKey: selection.anchorKey,
anchorOffset: textArr.join(',').length,
focusKey: selection.focusKey,
focusOffset: textArr.join(',').length,
});
const contentState = Modifier.replaceText(
editorState.getCurrentContent(),
newSelection,
`${
selection.anchorOffset > 0
? `\n${match ? parseInt(match) + 1 : 1}. `
: `${match ? parseInt(match) + 1 : 1}. `
}`,
editorState.getCurrentInlineStyle()
);
onChange(EditorState.push(editorState, contentState, 'insert-characters'));
};
render() {
return (
<div className="rdw-option-wrapper " onClick={this.makeLIST}>
<PopOver
position="bottom"
size="small"
title="Add unordered list"
trigger="mouseenter">
<i className="fas fa-list-ol" />
</PopOver>
</div>
);
}
}

View File

@ -17,8 +17,8 @@
import { isNil } from 'lodash';
import React, { FunctionComponent } from 'react';
import { stringToHTML } from '../../../utils/StringsUtils';
import Tag from '../../tags/tags';
import RichTextEditorPreviewer from '../rich-text-editor/RichTextEditorPreviewer';
type Props = {
description: string;
@ -37,7 +37,7 @@ const TableDataCardBody: FunctionComponent<Props> = ({
return (
<>
<div className="tw-mb-1 description-text">
{stringToHTML(description)}
<RichTextEditorPreviewer markdown={description} />
</div>
<p className="tw-py-1">
{extraInfo.map(({ key, value }, i) =>
@ -63,7 +63,7 @@ const TableDataCardBody: FunctionComponent<Props> = ({
<Tag
className="tw-border-none tw-bg-gray-200"
key={index}
tag={`#${tag}`}
tag={`#${tag.startsWith('Tier.Tier') ? tag.split('.')[1] : tag}`}
type="contained"
/>
))}

View File

@ -32,10 +32,10 @@ import {
getTableFQNFromColumnFQN,
isEven,
} from '../../utils/CommonUtils';
import { stringToHTML } from '../../utils/StringsUtils';
import SVGIcons from '../../utils/SvgUtils';
import { getConstraintIcon } from '../../utils/TableUtils';
import { getTagCategories, getTaglist } from '../../utils/TagsUtils';
import RichTextEditorPreviewer from '../common/rich-text-editor/RichTextEditorPreviewer';
// import { EditSchemaColumnModal } from '../Modals/EditSchemaColumnModal/EditSchemaColumnModal';
import { ModalWithMarkdownEditor } from '../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
import TagsContainer from '../tags-container/tags-container';
@ -197,6 +197,9 @@ const SchemaTable: FunctionComponent<Props> = ({
...columns.slice(editColumn.index + 1),
];
onUpdate(updatedColumns);
setEditColumn(undefined);
} else {
setEditColumn(undefined);
}
};
useEffect(() => {
@ -262,11 +265,15 @@ const SchemaTable: FunctionComponent<Props> = ({
<td className="tw-group tableBody-cell tw-relative">
<div>
<div
className="child-inline tw-cursor-pointer hover:tw-underline"
className="tw-cursor-pointer hover:tw-underline"
data-testid="description"
id={`column-description-${index}`}
onClick={() => handleEditColumn(column, index)}>
{stringToHTML(column.description) || (
{column.description ? (
<RichTextEditorPreviewer
markdown={column.description}
/>
) : (
<span className="tw-no-description">
No description added
</span>

View File

@ -32,6 +32,7 @@ import { getServiceById } from '../../axiosAPIs/serviceAPI';
import { getDatabaseTables } from '../../axiosAPIs/tableAPI';
import NextPrevious from '../../components/common/next-previous/NextPrevious';
import PopOver from '../../components/common/popover/PopOver';
import RichTextEditorPreviewer from '../../components/common/rich-text-editor/RichTextEditorPreviewer';
import TitleBreadcrumb from '../../components/common/title-breadcrumb/title-breadcrumb.component';
import { TitleBreadcrumbProps } from '../../components/common/title-breadcrumb/title-breadcrumb.interface';
import PageContainer from '../../components/containers/PageContainer';
@ -47,7 +48,6 @@ import {
import useToastContext from '../../hooks/useToastContext';
import { getCurrentUserId, isEven } from '../../utils/CommonUtils';
import { serviceTypeLogo } from '../../utils/ServiceUtils';
import { stringToHTML } from '../../utils/StringsUtils';
import SVGIcons from '../../utils/SvgUtils';
import { getUsagePercentile } from '../../utils/TableUtils';
import { getTableTags } from '../../utils/TagsUtils';
@ -242,9 +242,11 @@ const DatabaseDetails: FunctionComponent = () => {
</button>
</div>
</div>
<div className="tw-px-3 tw-py-2 tw-overflow-y-auto">
<div className="tw-px-3 tw-pl-5 tw-py-2 tw-overflow-y-auto">
<div data-testid="description" id="description" />
{stringToHTML(description.trim()) || (
{description.trim() ? (
<RichTextEditorPreviewer markdown={description} />
) : (
<span className="tw-no-description">
No description added
</span>
@ -295,7 +297,11 @@ const DatabaseDetails: FunctionComponent = () => {
</Link>
</td>
<td className="tableBody-cell">
{stringToHTML(table.description) || (
{table.description ? (
<RichTextEditorPreviewer
markdown={table.description}
/>
) : (
<span className="tw-no-description">
No description added
</span>

View File

@ -31,6 +31,7 @@ import {
removeFollower,
} from '../../axiosAPIs/tableAPI';
import PopOver from '../../components/common/popover/PopOver';
import RichTextEditorPreviewer from '../../components/common/rich-text-editor/RichTextEditorPreviewer';
import TitleBreadcrumb from '../../components/common/title-breadcrumb/title-breadcrumb.component';
import { TitleBreadcrumbProps } from '../../components/common/title-breadcrumb/title-breadcrumb.interface';
import PageContainer from '../../components/containers/PageContainer';
@ -53,7 +54,6 @@ import {
getTableFQNFromColumnFQN,
} from '../../utils/CommonUtils';
import { serviceTypeLogo } from '../../utils/ServiceUtils';
import { stringToHTML } from '../../utils/StringsUtils';
import SVGIcons from '../../utils/SvgUtils';
import {
getTagsWithoutTier,
@ -201,6 +201,8 @@ const MyDataDetailsPage = () => {
setDescription(updatedHTML);
setIsEdit(false);
});
} else {
setIsEdit(false);
}
};
@ -442,12 +444,18 @@ const MyDataDetailsPage = () => {
</div>
</div>
<div className="tw-px-3 tw-py-2 tw-overflow-y-auto">
<div data-testid="description" id="description" />
{stringToHTML(description?.trim()) || (
<span className="tw-no-description">
No description added
</span>
)}
<div
className="tw-pl-3"
data-testid="description"
id="description">
{description?.trim() ? (
<RichTextEditorPreviewer markdown={description} />
) : (
<span className="tw-no-description">
No description added
</span>
)}
</div>
{isEdit && (
<ModalWithMarkdownEditor
header={`Edit description for ${name}`}

View File

@ -24,6 +24,7 @@ import { Link, useParams } from 'react-router-dom';
import { getDatabases } from '../../axiosAPIs/databaseAPI';
import { getServiceByFQN, updateService } from '../../axiosAPIs/serviceAPI';
import NextPrevious from '../../components/common/next-previous/NextPrevious';
import RichTextEditorPreviewer from '../../components/common/rich-text-editor/RichTextEditorPreviewer';
import TitleBreadcrumb from '../../components/common/title-breadcrumb/title-breadcrumb.component';
import { TitleBreadcrumbProps } from '../../components/common/title-breadcrumb/title-breadcrumb.interface';
import PageContainer from '../../components/containers/PageContainer';
@ -33,7 +34,6 @@ import { pagingObject } from '../../constants/constants';
import useToastContext from '../../hooks/useToastContext';
import { isEven } from '../../utils/CommonUtils';
import { serviceTypeLogo } from '../../utils/ServiceUtils';
import { stringToHTML } from '../../utils/StringsUtils';
import SVGIcons from '../../utils/SvgUtils';
import { getUsagePercentile } from '../../utils/TableUtils';
@ -156,9 +156,11 @@ const ServicePage: FunctionComponent = () => {
</button>
</div>
</div>
<div className="tw-px-3 tw-py-2 tw-overflow-y-auto">
<div className="tw-px-3 tw-pl-5 tw-py-2 tw-overflow-y-auto">
<div data-testid="description" id="description" />
{stringToHTML(description.trim()) || (
{description.trim() ? (
<RichTextEditorPreviewer markdown={description} />
) : (
<span className="tw-no-description">
No description added
</span>
@ -207,7 +209,11 @@ const ServicePage: FunctionComponent = () => {
</Link>
</td>
<td className="tableBody-cell">
{stringToHTML(database.description?.trim()) || (
{database.description?.trim() ? (
<RichTextEditorPreviewer
markdown={database.description}
/>
) : (
<span className="tw-no-description">
No description added
</span>

View File

@ -23,14 +23,14 @@ import React, {
useRef,
useState,
} from 'react';
import { MarkdownWithPreview } from '../../components/common/editor/MarkdownWithPreview';
import MarkdownWithPreview from '../../components/common/editor/MarkdownWithPreview';
import { TagsCategory } from './tagsTypes';
type FormProp = {
saveData: (value: {}) => void;
initialData: TagsCategory;
};
type MarkdownRef = {
fetchUpdatedHTML: () => string;
type EditorContentRef = {
getEditorContent: () => string;
};
const Form: React.FC<FormProp> = forwardRef(
({ saveData, initialData }, ref): JSX.Element => {
@ -39,7 +39,7 @@ const Form: React.FC<FormProp> = forwardRef(
description: initialData.description,
categoryType: initialData.categoryType,
});
const markdownRef = useRef<MarkdownRef>();
const markdownRef = useRef<EditorContentRef>();
const onChangeHadler = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
@ -54,7 +54,7 @@ const Form: React.FC<FormProp> = forwardRef(
useImperativeHandle(ref, () => ({
fetchMarkDownData() {
return markdownRef.current?.fetchUpdatedHTML();
return markdownRef.current?.getEditorContent();
},
}));
@ -81,8 +81,8 @@ const Form: React.FC<FormProp> = forwardRef(
name="categoryType"
value={data.categoryType}
onChange={onChangeHadler}>
<option value="DESCRIPTIVE">Descriptive </option>
<option value="CLASSIFICATION">Classification</option>
<option value="Descriptive">Descriptive </option>
<option value="Classification">Classification</option>
</select>
</div>
)}
@ -105,11 +105,7 @@ const Form: React.FC<FormProp> = forwardRef(
<label className="tw-form-label required-field">
Description
</label>
<MarkdownWithPreview
editorRef={(Ref: MarkdownRef) => (markdownRef.current = Ref)}
placeholder="Description"
value={data.description}
/>
<MarkdownWithPreview ref={markdownRef} value={data.description} />
</div>
</div>
</div>

View File

@ -27,6 +27,7 @@ import {
updateTagCategory,
} from '../../axiosAPIs/tagAPI';
import { Button } from '../../components/buttons/Button/Button';
import RichTextEditorPreviewer from '../../components/common/rich-text-editor/RichTextEditorPreviewer';
import PageContainer from '../../components/containers/PageContainer';
import Loader from '../../components/Loader/Loader';
import FormModal from '../../components/Modals/FormModal';
@ -34,7 +35,6 @@ import { ModalWithMarkdownEditor } from '../../components/Modals/ModalWithMarkdo
import TagsContainer from '../../components/tags-container/tags-container';
import Tags from '../../components/tags/tags';
import { isEven } from '../../utils/CommonUtils';
import { stringToDOMElement } from '../../utils/StringsUtils';
import SVGIcons from '../../utils/SvgUtils';
import { getTagCategories, getTaglist } from '../../utils/TagsUtils';
import Form from './Form';
@ -154,12 +154,6 @@ const TagsPage = () => {
setEditTag(undefined);
};
const getDescription = (description: string) => {
const desc = stringToDOMElement(description).textContent;
return desc && desc.length > 1 ? desc : 'No description added';
};
useEffect(() => {
fetchCategories();
}, []);
@ -237,10 +231,18 @@ const TagsPage = () => {
</button>
</div>
</div>
<div className="tw-px-3 tw-py-2 tw-overflow-y-auto">
<div className="tw-px-3 tw-pl-5 tw-py-2 tw-overflow-y-auto">
{currentCategory && (
<div data-testid="description" id="description">
{getDescription(currentCategory.description)}
{currentCategory.description.trim() ? (
<RichTextEditorPreviewer
markdown={currentCategory.description}
/>
) : (
<span className="tw-no-description">
No description added
</span>
)}
{isEditCategory && (
<ModalWithMarkdownEditor
header={`Edit description for ${currentCategory.name}`}
@ -286,8 +288,16 @@ const TagsPage = () => {
setIsEditTag(true);
setEditTag(tag);
}}>
<div className="child-inline tw-cursor-pointer hover:tw-underline">
{getDescription(tag.description)}
<div className="tw-cursor-pointer hover:tw-underline">
{tag.description ? (
<RichTextEditorPreviewer
markdown={tag.description}
/>
) : (
<span className="tw-no-description">
No description added
</span>
)}
<button className="tw-opacity-0 tw-ml-1 group-hover:tw-opacity-100 focus:tw-outline-none">
<SVGIcons
alt="edit"
@ -375,7 +385,7 @@ const TagsPage = () => {
initialData={{
name: '',
description: '',
categoryType: 'DESCRIPTIVE',
categoryType: 'Descriptive',
}}
onCancel={() => setIsAddingCategory(false)}
onSave={(data) => createCategory(data)}

View File

@ -19,3 +19,7 @@
/// <reference types="react-scripts" />
declare module 'classnames';
declare module 'react-js-pagination';
declare module 'draft-js';
declare module 'react-draft-wysiwyg';
declare module 'markdown-draft-js';
declare module 'react-syntax-highlighter';

View File

@ -219,4 +219,13 @@
.tw-form-label {
@apply tw-block tw-text-body tw-font-normal tw-text-grey-body tw-mb-2;
}
body .content-container h1,
body .content-container h2 {
@apply tw-text-h3;
}
body .editor-wrapper ul,
body #description ul,
body .content-container ul {
@apply tw-list-disc;
}
}

View File

@ -680,3 +680,15 @@
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
blockquote {
border-left: 5px solid #f1f1f1;
padding-left: 5px;
}
strong {
font-weight: bold;
}
body .rdw-editor-main pre {
background: none;
}

View File

@ -45,3 +45,21 @@ export const ordinalize = (num: number): string => {
return num + ordinalSuffix;
};
export const getJSONFromString = (data: string): string | null => {
try {
// Format string if possible and return valid JSON
return JSON.parse(data);
} catch (e) {
// Invalid JSON, return null
return null;
}
};
export const isValidJSONString = (data?: string): boolean => {
if (data) {
return Boolean(getJSONFromString(data));
}
return false;
};

View File

@ -6,7 +6,7 @@
"incremental": true,
"target": "ES5",
"module": "esnext",
"lib": ["dom", "dom.iterable", "ES2020.Promise"],
"lib": ["dom", "dom.iterable", "ES2020.Promise", "es2021"],
"allowJs": true,
"jsx": "react",
"declaration": true,

View File

@ -115,6 +115,7 @@ module.exports = {
fallback: {
http: require.resolve('stream-http'),
https: require.resolve('https-browserify'),
path: require.resolve('path-browserify'),
},
},

View File

@ -116,6 +116,7 @@ module.exports = {
fallback: {
http: require.resolve('stream-http'),
https: require.resolve('https-browserify'),
path: require.resolve('path-browserify'),
},
},