Fix #3815 Page turns unresponsive for feeds with heavy html tags (#3852)

This commit is contained in:
Sachin Chaurasiya 2022-04-06 21:11:46 +05:30 committed by GitHub
parent bd4071bd64
commit a1e2b27f39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1206 additions and 159 deletions

View File

@ -10,7 +10,7 @@
"updatedAt": 1647046363996,
"updatedBy": "anonymous",
"resolved": false,
"message": "Added **tags**: `Tier.Tier2`",
"message": "Updated **columns** : <span class=\"diff-added\">account_category_code, </span>account_type_code, account_type_desc, <span class=\"diff-removed\">account_category_code</span><span class=\"diff-added\">account_type_desc_eng</span>, <span class=\"diff-removed\">eca_ind</span><span class=\"diff-added\">ban_type_desc</span>, <span class=\"diff-removed\">bill_production_ind</span><span class=\"diff-added\">ban_type_key</span>, <span class=\"diff-removed\">collection_waive_ind</span><span class=\"diff-added\">bill_production_ind</span>, <span class=\"diff-removed\">vip_status</span><span class=\"diff-added\">business_type</span>, <span class=\"diff-removed\">ban_type_desc</span><span class=\"diff-added\">check_in_crm</span>, <span class=\"diff-removed\">non_commercial_ind</span><span class=\"diff-added\">collection_waive_ind</span>, <span class=\"diff-removed\">legacy_account_category, </span>corporate_category, <span class=\"diff-removed\">ban_type_key</span><span class=\"diff-added\">currency_key</span>, <span class=\"diff-removed\">segment_key</span><span class=\"diff-added\">eca_ind</span>, <span class=\"diff-removed\">check_in_crm</span><span class=\"diff-added\">legacy_account_category</span>, <span class=\"diff-removed\">currency_key</span><span class=\"diff-added\">legal_person_ind</span>, <span class=\"diff-removed\">legal_person_ind</span><span class=\"diff-added\">non_commercial_ind</span>, <span class=\"diff-removed\">account_type_desc_eng</span><span class=\"diff-added\">segment_key</span>, <span class=\"diff-removed\">business_type</span><span class=\"diff-added\">vip_status</span>",
"postsCount": 0,
"posts": []
},
@ -701,4 +701,4 @@
"posts": []
}
]
}
}

View File

@ -44,7 +44,6 @@
"immutable": "^4.0.0-rc.14",
"jquery": "^3.5.0",
"markdown-draft-js": "^2.3.0",
"markdown-to-jsx": "^7.1.6",
"mobx": "6.0.1",
"mobx-react": "6.1.4",
"moment": "^2.29.1",
@ -65,6 +64,7 @@
"react-flow-renderer": "^9.6.8",
"react-google-login": "^5.2.2",
"react-js-pagination": "^3.0.3",
"react-markdown": "^8.0.2",
"react-oidc": "^1.0.3",
"react-quill": "^1.3.5",
"react-router-dom": "^5.2.0",
@ -74,6 +74,8 @@
"react-tippy": "^1.4.0",
"reactjs-localstorage": "^1.0.1",
"recharts": "^1.8.5",
"rehype-raw": "^6.1.1",
"remark-gfm": "^3.0.1",
"resolve": "1.15.0",
"resolve-url-loader": "3.1.1",
"slick-carousel": "^1.8.1",

View File

@ -28,6 +28,10 @@ jest.mock('../../auth-provider/AuthProvider', () => {
};
});
jest.mock('../common/rich-text-editor/RichTextEditorPreviewer', () => {
return jest.fn().mockReturnValue(<p>RichTextEditorPreviewer</p>);
});
jest.mock('../../axiosAPIs/glossaryAPI', () => ({
addGlossaries: jest.fn().mockImplementation(() => Promise.resolve()),
}));

View File

@ -36,6 +36,10 @@ jest.mock('../../axiosAPIs/glossaryAPI', () => ({
addGlossaries: jest.fn().mockImplementation(() => Promise.resolve()),
}));
jest.mock('../common/rich-text-editor/RichTextEditorPreviewer', () => {
return jest.fn().mockReturnValue(<p>RichTextEditorPreviewer</p>);
});
const mockOnCancel = jest.fn();
const mockOnSave = jest.fn();

View File

@ -46,6 +46,10 @@ jest.mock('../../auth-provider/AuthProvider', () => {
};
});
jest.mock('../common/rich-text-editor/RichTextEditorPreviewer', () => {
return jest.fn().mockReturnValue(<p>RichTextEditorPreviewer</p>);
});
const mockUserTeam = [
{
description: 'description',

View File

@ -30,6 +30,10 @@ window.ResizeObserver = jest.fn().mockImplementation(() => ({
disconnect: jest.fn(),
}));
jest.mock('../common/rich-text-editor/RichTextEditorPreviewer', () => {
return jest.fn().mockReturnValue(<p>RichTextEditorPreviewer</p>);
});
const mockLineageData = {
entity: {
id: 'efcc334a-41c8-483e-b779-464a88a7ece3',

View File

@ -627,8 +627,7 @@
padding: 2px 4px;
}
.ql-snow .ql-editor pre.ql-syntax {
background-color: #23241f;
color: #f8f8f2;
background-color: #ebeef1;
overflow: visible;
}
.ql-snow .ql-editor img {

View File

@ -51,6 +51,10 @@ jest.mock('../../components/common/non-admin-action/NonAdminAction', () => {
));
});
jest.mock('../common/rich-text-editor/RichTextEditorPreviewer', () => {
return jest.fn().mockReturnValue(<p>RichTextEditorPreviewer</p>);
});
const mockProps = {
glossary: mockedGlossaries[0],
isHasAccess: true,

View File

@ -54,6 +54,10 @@ jest.mock('../../components/common/non-admin-action/NonAdminAction', () => {
));
});
jest.mock('../common/rich-text-editor/RichTextEditorPreviewer', () => {
return jest.fn().mockReturnValue(<p>RichTextEditorPreviewer</p>);
});
const mockProps = {
assetData: mockedAssetData,
isHasAccess: true,

View File

@ -0,0 +1,105 @@
/*
* Copyright 2021 Collate
* Licensed 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 {
findByTestId,
fireEvent,
queryByTestId,
render,
} from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { BlurLayout } from './BlurLayout';
const markdown =
// eslint-disable-next-line max-len
'Updated **columns** : <span class="diff-added">account_category_code, </span>account_type_code, account_type_desc, <span class="diff-removed">account_category_code</span><span class="diff-added">account_type_desc_eng</span>, <span class="diff-removed">eca_ind</span><span class="diff-added">ban_type_desc</span>, <span class="diff-removed">bill_production_ind</span><span class="diff-added">ban_type_key</span>, <span class="diff-removed">collection_waive_ind</span><span class="diff-added">bill_production_ind</span>, <span class="diff-removed">vip_status</span><span class="diff-added">business_type</span>, <span class="diff-removed">ban_type_desc</span><span class="diff-added">check_in_crm</span>, <span class="diff-removed">non_commercial_ind</span><span class="diff-added">collection_waive_ind</span>, <span class="diff-removed">legacy_account_category, </span>corporate_category, <span class="diff-removed">ban_type_key</span><span class="diff-added">currency_key</span>, <span class="diff-removed">segment_key</span><span class="diff-added">eca_ind</span>, <span class="diff-removed">check_in_crm</span><span class="diff-added">legacy_account_category</span>, <span class="diff-removed">currency_key</span><span class="diff-added">legal_person_ind</span>, <span class="diff-removed">legal_person_ind</span><span class="diff-added">non_commercial_ind</span>, <span class="diff-removed">account_type_desc_eng</span><span class="diff-added">segment_key</span>, <span class="diff-removed">business_type</span><span class="diff-added">vip_status</span>';
const displayMoreHandler = jest.fn();
const mockProp = {
enableSeeMoreVariant: true,
markdown,
displayMoreText: true,
blurClasses: '',
displayMoreHandler,
};
jest.mock('./RichTextEditorPreviewer', () => ({
MAX_LENGTH: 300,
}));
describe('Test BlurLayout Component', () => {
it('Should render the Layout Component', async () => {
const { container } = render(<BlurLayout {...mockProp} />, {
wrapper: MemoryRouter,
});
const blurLayout = await findByTestId(container, 'blur-layout');
const displayButton = await findByTestId(container, 'display-button');
expect(blurLayout).toBeInTheDocument();
expect(displayButton).toBeInTheDocument();
});
it('Should not render the Layout Component if markdown length is less that MAX_LENGTH', async () => {
const { container } = render(<BlurLayout {...mockProp} markdown="" />, {
wrapper: MemoryRouter,
});
const blurLayout = queryByTestId(container, 'blur-layout');
const displayButton = queryByTestId(container, 'display-button');
expect(blurLayout).not.toBeInTheDocument();
expect(displayButton).not.toBeInTheDocument();
});
it('Should not render the Layout Component if enableSeeMoreVariant is false', async () => {
const { container } = render(
<BlurLayout {...mockProp} enableSeeMoreVariant={false} />,
{
wrapper: MemoryRouter,
}
);
const blurLayout = queryByTestId(container, 'blur-layout');
const displayButton = queryByTestId(container, 'display-button');
expect(blurLayout).not.toBeInTheDocument();
expect(displayButton).not.toBeInTheDocument();
});
it('Should call displayMoreHandler on display button click', async () => {
const { container } = render(<BlurLayout {...mockProp} />, {
wrapper: MemoryRouter,
});
const blurLayout = await findByTestId(container, 'blur-layout');
const displayButton = await findByTestId(container, 'display-button');
expect(blurLayout).toBeInTheDocument();
expect(displayButton).toBeInTheDocument();
fireEvent.click(displayButton);
expect(displayMoreHandler).toBeCalled();
});
});

View File

@ -0,0 +1,64 @@
/*
* Copyright 2021 Collate
* Licensed 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 classNames from 'classnames';
import React, { FC } from 'react';
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
import { MAX_LENGTH } from './RichTextEditorPreviewer';
interface BlurLayoutProp {
markdown: string;
enableSeeMoreVariant: boolean;
displayMoreText: boolean;
blurClasses: string;
displayMoreHandler: () => void;
}
export const BlurLayout: FC<BlurLayoutProp> = ({
enableSeeMoreVariant,
markdown,
displayMoreText,
blurClasses,
displayMoreHandler,
}: BlurLayoutProp) => {
const getBlurClass = () => {
return !displayMoreText ? blurClasses : '';
};
return enableSeeMoreVariant && markdown.length > MAX_LENGTH ? (
<div
className={classNames(
'tw-absolute tw-flex tw-h-full tw-w-full tw-inset-x-0 tw-pointer-events-none',
getBlurClass(),
{
'tw-top-0 tw-bottom-0': !displayMoreText,
' tw--bottom-4': displayMoreText,
}
)}
data-testid="blur-layout">
<p
className="tw-cursor-pointer tw-self-end tw-pointer-events-auto tw-text-primary tw-mx-auto"
data-testid="display-button"
onClick={displayMoreHandler}>
<span className="tw-flex tw-items-center tw-gap-2">
<SVGIcons
alt="expand-collapse"
className={classNames({ 'rotate-inverse': displayMoreText })}
icon={Icons.CHEVRON_DOWN}
width="32"
/>
</span>
</p>
</div>
) : null;
};

View File

@ -11,7 +11,7 @@
* limitations under the License.
*/
import { Attributes, HtmlHTMLAttributes, ReactNode } from 'react';
import { ReactNode } from 'react';
export type editorRef = ReactNode | HTMLElement | string;
export enum Format {
@ -27,8 +27,11 @@ export type EditorProp = {
customOptions?: ReactNode[];
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface ElementProp extends HtmlHTMLAttributes<any>, Attributes {
class: string;
className: string;
export interface PreviewerProp {
markdown: string;
className?: string;
blurClasses?: string;
maxHtClass?: string;
maxLen?: number;
enableSeeMoreVariant?: boolean;
}

View File

@ -0,0 +1,56 @@
/*
* Copyright 2021 Collate
* Licensed 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 { findByTestId, render } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import RichTextEditorPreviewer from './RichTextEditorPreviewer';
const mockProp = {
markdown: '',
className: '',
blurClasses: 'see-more-blur',
maxHtClass: 'tw-h-24',
maxLen: 300,
enableSeeMoreVariant: true,
};
jest.mock('react-markdown', () => {
return jest.fn().mockImplementation(() => {
return <p data-testid="markdown-parser">markdown parser</p>;
});
});
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(<RichTextEditorPreviewer {...mockProp} />, {
wrapper: MemoryRouter,
});
const viewerContainer = await findByTestId(container, 'viewer-container');
expect(viewerContainer).toBeInTheDocument();
const markdownParser = await findByTestId(container, 'markdown-parser');
expect(markdownParser).toBeInTheDocument();
});
});

View File

@ -12,13 +12,15 @@
*/
import classNames from 'classnames';
import Markdown from 'markdown-to-jsx';
import React, { useEffect, useState } from 'react';
import { Paragraph, UnOrderedList } from '../../../utils/MarkdownUtils';
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
import { ElementProp } from './RichTextEditor.interface';
// Markdown Parser and plugin imports
import MarkdownParser from 'react-markdown';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';
import { BlurLayout } from './BlurLayout';
import { PreviewerProp } from './RichTextEditor.interface';
const MAX_LENGTH = 300;
export const MAX_LENGTH = 300;
const RichTextEditorPreviewer = ({
markdown = '',
@ -27,21 +29,23 @@ const RichTextEditorPreviewer = ({
maxHtClass = 'tw-h-24',
maxLen = MAX_LENGTH,
enableSeeMoreVariant = true,
}: {
markdown: string;
className?: string;
blurClasses?: string;
maxHtClass?: string;
maxLen?: number;
enableSeeMoreVariant?: boolean;
}) => {
}: PreviewerProp) => {
const [content, setContent] = useState<string>('');
const [displayMoreText, setDisplayMoreText] = useState(false);
useEffect(() => {
const modifiedContent = markdown
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>');
const setModifiedContent = (markdownValue: string) => {
const modifiedContent = markdownValue
.replace(/&lt;/g, '<')
.replace(/&gt/g, '>');
setContent(modifiedContent);
};
const displayMoreHandler = () => {
setDisplayMoreText((pre) => !pre);
};
useEffect(() => {
setModifiedContent(markdown);
}, [markdown]);
return (
@ -55,90 +59,55 @@ const RichTextEditorPreviewer = ({
{
'tw-mb-5': displayMoreText,
}
)}>
<Markdown
options={{
overrides: {
h1: {
component: Paragraph,
},
h2: {
component: Paragraph,
},
h3: {
component: Paragraph,
},
h4: {
component: Paragraph,
},
h5: {
component: Paragraph,
},
h6: {
component: Paragraph,
},
ul: {
component: UnOrderedList,
props: {
className: 'tw-ml-3',
},
},
},
/**
* Custom React CreateElement Implementation
* @param type - Element type
* @param props - Element Props
* @param children - Elemeny children
* @returns React element of {type} with {props} and {children}
*/
createElement(type, props, children) {
const {
className,
/** disabling eslint rule because class is reserverd keyword
* and here we have to give alias that is classes */
// eslint-disable-next-line react/prop-types
class: classes,
...restProps
} = props as ElementProp;
const modifiedProps = {
...restProps,
/** react does not accept class attribute,
* hence we need to escape class and pass className attribute
*/
className: `${className ? className : ''} ${
classes ? classes : ''
}`,
};
return React.createElement(type, modifiedProps, children);
},
}}>
{content}
</Markdown>
{enableSeeMoreVariant && markdown.length > MAX_LENGTH && (
<div
className={classNames(
'tw-absolute tw-flex tw-h-full tw-w-full tw-inset-x-0 tw-pointer-events-none',
!displayMoreText ? blurClasses : null,
{
'tw-top-0 tw-bottom-0': !displayMoreText,
' tw--bottom-4': displayMoreText,
}
)}>
<p
className="tw-cursor-pointer tw-self-end tw-pointer-events-auto tw-text-primary tw-mx-auto"
onClick={() => setDisplayMoreText((pre) => !pre)}>
<span className="tw-flex tw-items-center tw-gap-2">
<SVGIcons
alt="expand-collapse"
className={classNames({ 'rotate-inverse': displayMoreText })}
icon={Icons.CHEVRON_DOWN}
width="32"
/>
</span>
</p>
</div>
)}
data-testid="viewer-container">
<MarkdownParser
sourcePos
components={{
h1: 'p',
h2: 'p',
ul: ({ children, ...props }) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { ordered, ...rest } = props;
return (
<ul className="tw-ml-3" {...rest}>
{children}
</ul>
);
},
ol: ({ children, ...props }) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { ordered, ...rest } = props;
return (
<ol className="tw-ml-3" {...rest} style={{ listStyle: 'auto' }}>
{children}
</ol>
);
},
code: ({ children, ...props }) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { inline, ...rest } = props;
return (
<code {...rest} className="tw-my">
{children}
</code>
);
},
}}
rehypePlugins={[rehypeRaw]}
remarkPlugins={[remarkGfm]}>
{content}
</MarkdownParser>
<BlurLayout
blurClasses={blurClasses}
displayMoreHandler={displayMoreHandler}
displayMoreText={displayMoreText}
enableSeeMoreVariant={enableSeeMoreVariant}
markdown={content}
/>
</div>
);
};

View File

@ -31,6 +31,10 @@ const tagsWithTerm = [
{ tagFQN: `tags:term`, source: 'Glossary' },
];
jest.mock('../common/rich-text-editor/RichTextEditorPreviewer', () => {
return jest.fn().mockReturnValue(<p>RichTextEditorPreviewer</p>);
});
describe('Test TagsViewer Component', () => {
it('Component should render', () => {
const { container } = render(<TagsViewer sizeCap={-1} tags={tags} />);

View File

@ -898,7 +898,8 @@ body .profiler-graph .recharts-active-dot circle {
.entity-feed-list {
grid-template-columns: 200px auto 200px;
}
.activity-feed-card-text code {
code {
padding: 2px 3px;
border-radius: 4px;
background: #ebeef1;

View File

@ -14,10 +14,13 @@
import classNames from 'classnames';
import { diffArrays, diffWordsWithSpace } from 'diff';
import { isEmpty, isUndefined, uniqueId } from 'lodash';
import Markdown from 'markdown-to-jsx';
import React, { Fragment } from 'react';
import ReactDOMServer from 'react-dom/server';
// 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 { FQN_SEPARATOR_CHAR } from '../constants/char.constants';
import { DESCRIPTIONLENGTH, getTeamDetailsPath } from '../constants/constants';
import { ChangeType } from '../enums/entity.enum';
@ -27,7 +30,6 @@ import {
FieldChange,
} from '../generated/entity/services/databaseService';
import { TagLabel } from '../generated/type/tagLabel';
import { Paragraph, Span, UnOrderedList } from './MarkdownUtils';
import { isValidJSONString } from './StringsUtils';
import { getEntityLink, getOwnerFromId } from './TableUtils';
@ -38,49 +40,62 @@ const parseMarkdown = (
_isNewLine: boolean
) => {
return (
<Markdown
options={{
overrides: {
h1: {
component: Paragraph,
<Fragment>
<MarkdownParser
sourcePos
components={{
h1: 'p',
h2: 'p',
ul: ({ children, ...props }) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { ordered, ...rest } = props;
return (
<ul className={classNames('tw-ml-3', className)} {...rest}>
{children}
</ul>
);
},
h2: {
component: Paragraph,
ol: ({ children, ...props }) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { ordered, ...rest } = props;
return (
<ol className="tw-ml-3" {...rest} style={{ listStyle: 'auto' }}>
{children}
</ol>
);
},
h3: {
component: Paragraph,
code: ({ children, ...props }) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { inline, ...rest } = props;
return (
<code {...rest} className="tw-my">
{children}
</code>
);
},
h4: {
component: Paragraph,
p: ({ children, ...props }) => {
return (
<p className={className} {...props}>
{children}
</p>
);
},
h5: {
component: Paragraph,
span: ({ children, ...props }) => {
return (
<span className={className} {...props}>
{children}
</span>
);
},
h6: {
component: Paragraph,
},
ul: {
component: UnOrderedList,
props: {
className: `${className} tw-ml-3`,
},
},
p: {
component: Paragraph,
props: {
className: `${className}`,
},
},
span: {
component: Span,
props: {
className: `${className}`,
},
},
},
}}>
{content}
</Markdown>
}}
rehypePlugins={[rehypeRaw]}
remarkPlugins={[remarkGfm]}>
{content}
</MarkdownParser>
</Fragment>
);
};

View File

@ -51,6 +51,15 @@ module.exports = {
},
},
},
// .mjs files to be handled
{
test: /\.m?js/,
include: path.resolve(__dirname, 'node_modules/kleur'),
resolve: {
fullySpecified: false,
},
},
// .ts and .tsx files to be handled by ts-loader
{
test: /\.(ts|tsx)$/,

View File

@ -52,6 +52,15 @@ module.exports = {
},
},
},
// .mjs files to be handled
{
test: /\.m?js/,
include: path.resolve(__dirname, 'node_modules/kleur'),
resolve: {
fullySpecified: false,
},
},
// .ts and .tsx files to be handled by ts-loader
{
test: /\.(ts|tsx)$/,

File diff suppressed because it is too large Load Diff