mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-07-23 17:30:35 +00:00
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:
parent
4464d81857
commit
ae8e01c655
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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[];
|
||||
};
|
@ -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;
|
@ -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(/</g, '<')
|
||||
.replaceAll(/>/g, '>')
|
||||
.replaceAll('\\', '')}
|
||||
components={{
|
||||
h1: 'p',
|
||||
h2: 'p',
|
||||
h3: 'p',
|
||||
h4: 'p',
|
||||
h5: 'p',
|
||||
h6: 'p',
|
||||
}}
|
||||
remarkPlugins={[gfm]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RichTextEditorPreviewer;
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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"
|
||||
/>
|
||||
))}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}`}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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)}
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -115,6 +115,7 @@ module.exports = {
|
||||
fallback: {
|
||||
http: require.resolve('stream-http'),
|
||||
https: require.resolve('https-browserify'),
|
||||
path: require.resolve('path-browserify'),
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -116,6 +116,7 @@ module.exports = {
|
||||
fallback: {
|
||||
http: require.resolve('stream-http'),
|
||||
https: require.resolve('https-browserify'),
|
||||
path: require.resolve('path-browserify'),
|
||||
},
|
||||
},
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user