From 2d2aae6c03a6f1b2f02eb1551830933cbeefc82d Mon Sep 17 00:00:00 2001 From: dleadbetter Date: Mon, 23 Dec 2024 19:52:31 -0500 Subject: [PATCH] BASIRA #291 - Updating Search page to storage search history in session storage (via SearchHistory); Adding "/search_history" route and table to display recent searches --- client/src/App.js | 6 ++ client/src/components/NavBar.css | 3 + client/src/components/NavBar.js | 28 ++++++ client/src/components/RecordPage.js | 4 +- client/src/components/SearchFacets.js | 6 +- client/src/components/SearchHistory.js | 45 +++++++++ client/src/components/SearchLink.js | 8 +- client/src/i18n/en.json | 16 ++++ client/src/pages/Search.css | 6 +- client/src/pages/Search.js | 33 +++---- client/src/pages/SearchHistory.css | 3 + client/src/pages/SearchHistory.js | 122 +++++++++++++++++++++++++ client/src/services/SearchHistory.js | 40 ++++++++ 13 files changed, 285 insertions(+), 35 deletions(-) create mode 100644 client/src/components/NavBar.css create mode 100644 client/src/components/NavBar.js create mode 100644 client/src/components/SearchHistory.js create mode 100644 client/src/pages/SearchHistory.css create mode 100644 client/src/pages/SearchHistory.js create mode 100644 client/src/services/SearchHistory.js diff --git a/client/src/App.js b/client/src/App.js index c2f491a..bcf02b6 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -14,6 +14,7 @@ import PhysicalComponent from './pages/PhysicalComponent'; import Place from './pages/Place'; import Search from './pages/Search'; import SearchContextProvider from './components/SearchContextProvider'; +import SearchHistory from './pages/SearchHistory'; import VisualContext from './pages/VisualContext'; import './App.css'; @@ -25,6 +26,11 @@ const App = () => ( component={Search} exact /> + { + return ( + + +
+ + +
+ ); +}; + +export default NavBar; diff --git a/client/src/components/RecordPage.js b/client/src/components/RecordPage.js index 413db54..1537350 100644 --- a/client/src/components/RecordPage.js +++ b/client/src/components/RecordPage.js @@ -80,7 +80,9 @@ const RecordPage = (props: Props) => { - + diff --git a/client/src/components/SearchFacets.js b/client/src/components/SearchFacets.js index 5ddf8fc..2038d6b 100644 --- a/client/src/components/SearchFacets.js +++ b/client/src/components/SearchFacets.js @@ -8,11 +8,7 @@ import { } from '@performant-software/semantic-components'; import React, { useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { - useRange, - useRefinementList, - useToggleRefinement -} from 'react-instantsearch-hooks-web'; +import { useRange, useRefinementList, useToggleRefinement } from 'react-instantsearch-hooks-web'; import { Header, Segment } from 'semantic-ui-react'; import _ from 'underscore'; import useFacetLabels from '../hooks/FacetLabels'; diff --git a/client/src/components/SearchHistory.js b/client/src/components/SearchHistory.js new file mode 100644 index 0000000..07ccbde --- /dev/null +++ b/client/src/components/SearchHistory.js @@ -0,0 +1,45 @@ +// @flow + +import { useCallback, useEffect, useMemo } from 'react'; +import { useCurrentRefinements, useSearchBox } from 'react-instantsearch-hooks-web'; +import { useLocation } from 'react-router-dom'; +import _ from 'underscore'; +import SearchHistoryService from '../services/SearchHistory'; +import useFacetLabels from '../hooks/FacetLabels'; + +const SearchHistory = () => { + const currentRefinements = useCurrentRefinements(); + const { query } = useSearchBox(); + + const location = useLocation(); + const { getLabel } = useFacetLabels(); + + const { search: url } = location; + + /** + * Retrieves the label and facet value from the current refinements. + * + * @type {function(*): *} + */ + const transformItem = useCallback((item) => ( + _.map(item.refinements, (r) => `${getLabel(r.attribute)}: ${r.label}`) + ), [getLabel]); + + /** + * Transforms the list of items into an array of label/values. + */ + const items = useMemo(() => _.flatten(_.map(currentRefinements.items, transformItem)), [currentRefinements]); + + /** + * Saves the search whenever the URL is changed. + */ + useEffect(() => { + if (!(_.isEmpty(query) && _.isEmpty(items))) { + SearchHistoryService.saveSearch({ url, query, items, created: Date.now() }); + } + }, [location.search]); + + return null; +}; + +export default SearchHistory; \ No newline at end of file diff --git a/client/src/components/SearchLink.js b/client/src/components/SearchLink.js index c2fda2d..9c43e2d 100644 --- a/client/src/components/SearchLink.js +++ b/client/src/components/SearchLink.js @@ -6,7 +6,11 @@ import { Link } from 'react-router-dom'; import { Button } from 'semantic-ui-react'; import SearchContext from '../context/Search'; -const SearchLink = () => { +type Props = { + inverted?: boolean +}; + +const SearchLink = (props: Props) => { const { search } = useContext(SearchContext); const { t } = useTranslation(); @@ -16,7 +20,7 @@ const SearchLink = () => { basic content={t('SearchLink.buttons.back')} icon='arrow alternate circle left outline' - inverted + inverted={props.inverted} to={`/${search || ''}`} /> ); diff --git a/client/src/i18n/en.json b/client/src/i18n/en.json index 87c2909..978d827 100644 --- a/client/src/i18n/en.json +++ b/client/src/i18n/en.json @@ -531,6 +531,9 @@ "subjectCulturalContext": "Subject Cultural Context" } }, + "labels": { + "viewRecent": "View Recent" + }, "sort": { "artworkDate": { "label": "Artwork Date", @@ -553,6 +556,19 @@ "expand": "Expand All" } }, + "SearchHistory": { + "buttons": { + "clear": "Clear" + }, + "columns": { + "created": "Created", + "facets": "Facets", + "query": "Query" + }, + "messages": { + "copy": "Copied URL to clipboard" + } + }, "SearchLink": { "buttons": { "back": "Back to Search" diff --git a/client/src/pages/Search.css b/client/src/pages/Search.css index d4ba089..f66f015 100644 --- a/client/src/pages/Search.css +++ b/client/src/pages/Search.css @@ -1,7 +1,3 @@ -.search > .ui.menu { - border-radius: 0; -} - .search .item-collection { padding-bottom: 1em; } @@ -21,6 +17,6 @@ .search .stats-container { display: flex; - justify-content: flex-end; + justify-content: space-between; margin-top: 0.5em; } diff --git a/client/src/pages/Search.js b/client/src/pages/Search.js index 9ffca34..202c732 100644 --- a/client/src/pages/Search.js +++ b/client/src/pages/Search.js @@ -25,21 +25,17 @@ import { useStats } from 'react-instantsearch-hooks-web'; import { Link, useHistory, useLocation } from 'react-router-dom'; -import { - Container, - Grid, - Header, - Menu -} from 'semantic-ui-react'; +import { Container, Grid } from 'semantic-ui-react'; import _ from 'underscore'; import Banner from '../components/Banner'; -import LinksMenu from '../components/LinksMenu'; +import NavBar from '../components/NavBar'; import PageFooter from '../components/PageFooter'; import SearchContext from '../context/Search'; import SearchFacets from '../components/SearchFacets'; import SearchResultDescription from '../components/SearchResultDescription'; import SearchThumbnail from '../components/SearchThumbnail'; import searchClient from '../config/Search'; +import SearchHistory from '../components/SearchHistory'; import useFacetLabels from '../hooks/FacetLabels'; import './Search.css'; @@ -58,7 +54,7 @@ const Search = () => { */ const transformCurrentFacets = useCallback((items) => ( _.map(items, (item) => ({ ...item, label: getLabel(item.label) })) - ), []); + ), [getLabel]); /** * Set the search in the context when the location.search attribute changes. @@ -70,20 +66,7 @@ const Search = () => { className='search' fluid > - - -
- - -
+ { }} searchClient={searchClient} > + {
+ + { t('Search.labels.viewRecent') } + diff --git a/client/src/pages/SearchHistory.css b/client/src/pages/SearchHistory.css new file mode 100644 index 0000000..a6426ec --- /dev/null +++ b/client/src/pages/SearchHistory.css @@ -0,0 +1,3 @@ +.search-history > .ui.container > .search-list > .table > tbody > tr > td:first-child { + white-space: nowrap; +} \ No newline at end of file diff --git a/client/src/pages/SearchHistory.js b/client/src/pages/SearchHistory.js new file mode 100644 index 0000000..3b4b764 --- /dev/null +++ b/client/src/pages/SearchHistory.js @@ -0,0 +1,122 @@ +// @flow + +import { CurrentFacetLabels, EmbeddedList } from '@performant-software/semantic-components'; +import { useTimer } from '@performant-software/shared-components'; +import React, { useCallback, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { Button, Container, Popup } from 'semantic-ui-react'; +import _ from 'underscore'; +import { getDateTimeView } from '../utils/Date'; +import NavBar from '../components/NavBar'; +import SearchHistoryService from '../services/SearchHistory'; +import SearchLink from '../components/SearchLink'; +import './SearchHistory.css'; + +const TOOLTIP_DELAY_MS = 1500; + +const SearchHistory = () => { + const [items, setItems] = useState(_.sortBy(SearchHistoryService.getHistory(), 'created').reverse()); + const [copyItem, setCopyItem] = useState(); + + const { t } = useTranslation(); + const { clearTimer, setTimer } = useTimer(); + + /** + * Clears the search history. + * + * @type {(function(): void)|*} + */ + const onClear = useCallback(() => { + // Clear the items on the state + setItems([]); + + // Clear the items in session storage + SearchHistoryService.clearHistory(); + }, []); + + /** + * Copies the URL for the passed item and sets the selected item on the state. + * + * @type {(function(*): void)|*} + */ + const onCopy = useCallback((item) => { + const url = `${window.location.origin}/${item.url}`; + navigator.clipboard.writeText(url); + + setCopyItem(item); + + clearTimer(); + setTimer(() => setCopyItem(null), TOOLTIP_DELAY_MS); + }, []); + + return ( + + + + ({ + to: `/${item.url}` + }), + icon: 'arrow alternate circle right outline', + name: 'link' + }, { + name: 'copy', + render: (item) => ( + onCopy(item)} + open={copyItem === item} + trigger={( +