From f3e7f18dc3cfd6127236b15f5b99dc65612157d0 Mon Sep 17 00:00:00 2001 From: Asim Regmi <54924215+asimregmi@users.noreply.github.com> Date: Tue, 24 Oct 2023 15:16:51 -0500 Subject: [PATCH] Task/WP-72: Highlight matching search terms (#873) * task/WP-72 highlighted search queries/term in job history infinitescrolltable * fixed formatting * removed highlight and added bold * remove css properties and replaced with * used local css modules instead of global * Changed implementation of HighlightSearchTerm component to be used by Jobs.jsx * added useMemo hook and improved HighlightSearchTerm perf * removed useMemo hook * added unit tests for HighlightSearchTerm component * updated comment and fixed linting --------- Co-authored-by: Shayan Khan Co-authored-by: Chandra Y --- client/src/components/Jobs/Jobs.jsx | 49 +++++++++++----- .../HighlightSearchTerm.jsx | 39 +++++++++++++ .../HighlightSearchTerm.module.scss | 3 + .../HighlightSearchTerm.test.js | 57 +++++++++++++++++++ .../_common/HighlightSearchTerm/index.js | 3 + .../InfiniteScrollTable.jsx | 1 + client/src/components/_common/index.js | 1 + 7 files changed, 138 insertions(+), 15 deletions(-) create mode 100644 client/src/components/_common/HighlightSearchTerm/HighlightSearchTerm.jsx create mode 100644 client/src/components/_common/HighlightSearchTerm/HighlightSearchTerm.module.scss create mode 100644 client/src/components/_common/HighlightSearchTerm/HighlightSearchTerm.test.js create mode 100644 client/src/components/_common/HighlightSearchTerm/index.js diff --git a/client/src/components/Jobs/Jobs.jsx b/client/src/components/Jobs/Jobs.jsx index 1945d1bf7..c10f6abcb 100644 --- a/client/src/components/Jobs/Jobs.jsx +++ b/client/src/components/Jobs/Jobs.jsx @@ -5,6 +5,7 @@ import { Link, useLocation } from 'react-router-dom'; import { AppIcon, InfiniteScrollTable, + HighlightSearchTerm, Message, SectionMessage, Section, @@ -90,8 +91,6 @@ function JobsView({ original: { id, uuid, name }, }, }) => { - const query = queryStringParser.parse(useLocation().search); - // TODOv3: dropV2Jobs const jobsPathname = uuid ? `/jobs/${uuid}` : `/jobsv2/${id}`; return ( @@ -105,11 +104,11 @@ function JobsView({ }} className="wb-link" > - View Details + {query.query_string ? View Details : 'View Details'} ); }, - [] + [query] ); if (error) { @@ -133,15 +132,24 @@ function JobsView({ { Header: 'Job Name', accessor: 'name', - Cell: (el) => ( - - {el.value} - - ), + Cell: (el) => { + return ( + + {query.query_string ? ( + + ) : ( + el.value + )} + + ); + }, }, { Header: 'Job Status', @@ -183,12 +191,20 @@ function JobsView({ // TODOv3: dropV2Jobs if (el.row.original.uuid) { const outputLocation = getOutputPath(el.row.original); + return outputLocation && !hideDataFiles ? ( - {outputLocation} + {query.query_string ? ( + + ) : ( + outputLocation + )} ) : null; } else { @@ -232,7 +248,10 @@ function JobsView({ } getRowProps={rowProps} - columnMemoProps={[version]} /* TODOv3: dropV2Jobs. */ + columnMemoProps={[ + version, + query, + ]} /* TODOv3: dropV2Jobs. Refactor version prop. */ /> ); diff --git a/client/src/components/_common/HighlightSearchTerm/HighlightSearchTerm.jsx b/client/src/components/_common/HighlightSearchTerm/HighlightSearchTerm.jsx new file mode 100644 index 000000000..592085e29 --- /dev/null +++ b/client/src/components/_common/HighlightSearchTerm/HighlightSearchTerm.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styles from './HighlightSearchTerm.module.scss'; + +const HighlightSearchTerm = ({ searchTerm, content }) => { + if (!searchTerm) { + return <>{content}; + } + + const searchTermRegex = new RegExp(`(${searchTerm})`, 'gi'); + + const highlightParts = () => { + const parts = content.split(searchTermRegex); + return parts.map((part, i) => { + const isSearchTerm = part.match(searchTermRegex); + return isSearchTerm ? ( + + {part} + + ) : ( + part + ); + }); + }; + + return <>{highlightParts()}; +}; + +HighlightSearchTerm.propTypes = { + searchTerm: PropTypes.string, + content: PropTypes.string, +}; + +HighlightSearchTerm.defaultProps = { + searchTerm: '', + content: '', +}; + +export default HighlightSearchTerm; diff --git a/client/src/components/_common/HighlightSearchTerm/HighlightSearchTerm.module.scss b/client/src/components/_common/HighlightSearchTerm/HighlightSearchTerm.module.scss new file mode 100644 index 000000000..a9bb77c2b --- /dev/null +++ b/client/src/components/_common/HighlightSearchTerm/HighlightSearchTerm.module.scss @@ -0,0 +1,3 @@ +.highlight { + font-weight: bold; +} diff --git a/client/src/components/_common/HighlightSearchTerm/HighlightSearchTerm.test.js b/client/src/components/_common/HighlightSearchTerm/HighlightSearchTerm.test.js new file mode 100644 index 000000000..11060513f --- /dev/null +++ b/client/src/components/_common/HighlightSearchTerm/HighlightSearchTerm.test.js @@ -0,0 +1,57 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { + toBeInTheDocument, + toHaveClass, + toBeNull, +} from '@testing-library/jest-dom'; + +import HighlightSearchTerm from './HighlightSearchTerm'; + +describe('HighlightSearchTerm Component', () => { + it('renders content when searchTerm is not provided', () => { + const { getByText } = render(); + expect(getByText('Lorem ipsum')).toBeInTheDocument(); + }); + + it('renders without highlighting when searchTerm in content do not match', () => { + const { getByText } = render( + + ); + expect(getByText('Lorem ipsum dolor sit amet')).toBeInTheDocument(); + expect(document.querySelector('.highlight')).toBeNull(); + }); + + it('renders content when searchTerm is not provided', () => { + const { getByText } = render(); + expect(getByText('Lorem ipsum')).toBeInTheDocument(); + }); + + it('renders content with searchTerm highlighted', () => { + const { getByText } = render( + + ); + const highlightedText = getByText('ipsum'); + expect(highlightedText).toHaveClass('highlight'); + }); + + it('renders content with multiple searchTerm occurrences highlighted', () => { + const { getAllByText } = render( + + ); + const highlightedText = getAllByText('ipsum'); + expect(highlightedText.length).toBe(5); + highlightedText.forEach((element) => { + expect(element).toHaveClass('highlight'); + }); + }); +}); diff --git a/client/src/components/_common/HighlightSearchTerm/index.js b/client/src/components/_common/HighlightSearchTerm/index.js new file mode 100644 index 000000000..ccb240cb7 --- /dev/null +++ b/client/src/components/_common/HighlightSearchTerm/index.js @@ -0,0 +1,3 @@ +import HighlightSearchTerm from './HighlightSearchTerm'; + +export default HighlightSearchTerm; diff --git a/client/src/components/_common/InfiniteScrollTable/InfiniteScrollTable.jsx b/client/src/components/_common/InfiniteScrollTable/InfiniteScrollTable.jsx index a3bbe85e5..d9f673df1 100644 --- a/client/src/components/_common/InfiniteScrollTable/InfiniteScrollTable.jsx +++ b/client/src/components/_common/InfiniteScrollTable/InfiniteScrollTable.jsx @@ -128,6 +128,7 @@ InfiniteScrollTable.propTypes = { noDataText: rowContentPropType, getRowProps: PropTypes.func, columnMemoProps: PropTypes.arrayOf(PropTypes.any), + cell: PropTypes.object, }; InfiniteScrollTable.defaultProps = { onInfiniteScroll: (offset) => {}, diff --git a/client/src/components/_common/index.js b/client/src/components/_common/index.js index 3812495d2..ec663f8ca 100644 --- a/client/src/components/_common/index.js +++ b/client/src/components/_common/index.js @@ -16,6 +16,7 @@ export { default as Icon } from './Icon'; export { default as Message } from './Message'; export { default as InlineMessage } from './InlineMessage'; export { default as SectionMessage } from './SectionMessage'; +export { default as HighlightSearchTerm } from './HighlightSearchTerm'; export { default as Sidebar } from './Sidebar'; export { default as DescriptionList } from './DescriptionList'; export { default as DropdownSelector } from './DropdownSelector';