From c1ceeb8030b7ed9c1aa9dd836c9534ef7947eaba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anne=20L=27H=C3=B4te?= Date: Thu, 7 Nov 2024 19:53:55 +0100 Subject: [PATCH] feat(router): Refactor navigation logic --- client/src/components/tiles/datasets.jsx | 2 +- client/src/components/tiles/openalex.jsx | 2 +- client/src/components/tiles/publications.jsx | 2 +- client/src/pages/affiliations.jsx | 172 +++++++-- client/src/pages/filters.jsx | 364 ++++++------------- client/src/router.jsx | 34 +- client/src/utils/strings.jsx | 3 + 7 files changed, 279 insertions(+), 300 deletions(-) diff --git a/client/src/components/tiles/datasets.jsx b/client/src/components/tiles/datasets.jsx index 78e452ec..19fb291a 100644 --- a/client/src/components/tiles/datasets.jsx +++ b/client/src/components/tiles/datasets.jsx @@ -4,7 +4,7 @@ export default function DatasetsTile() {

- + 🗃 Find the datasets affiliated to your institution

diff --git a/client/src/components/tiles/openalex.jsx b/client/src/components/tiles/openalex.jsx index f883282b..9c163295 100644 --- a/client/src/components/tiles/openalex.jsx +++ b/client/src/components/tiles/openalex.jsx @@ -4,7 +4,7 @@ export default function OpenalexTile() {

- + ✏️ Improve ROR matching in OpenAlex - Provide your feedback!

diff --git a/client/src/components/tiles/publications.jsx b/client/src/components/tiles/publications.jsx index 1be5e75e..ea8c3c5f 100644 --- a/client/src/components/tiles/publications.jsx +++ b/client/src/components/tiles/publications.jsx @@ -4,7 +4,7 @@ export default function PublicationsTile() {

- + 📑 Find the publications affiliated to your institution

diff --git a/client/src/pages/affiliations.jsx b/client/src/pages/affiliations.jsx index b43240a3..45eb7732 100644 --- a/client/src/pages/affiliations.jsx +++ b/client/src/pages/affiliations.jsx @@ -1,17 +1,29 @@ -import { Col, Container, Row, Spinner } from '@dataesr/dsfr-plus'; +import { + Badge, + Col, + Container, + Row, + SegmentedControl, + SegmentedElement, + Spinner, + Tag, + TagGroup, + Title, +} from '@dataesr/dsfr-plus'; import { useQuery } from '@tanstack/react-query'; -import PropTypes from 'prop-types'; import { useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; +import Ribbon from '../components/ribbon'; import DatasetsTile from '../components/tiles/datasets'; import OpenalexTile from '../components/tiles/openalex'; import PublicationsTile from '../components/tiles/publications'; import { status } from '../config'; import useToast from '../hooks/useToast'; import { getAffiliationsCorrections } from '../utils/curations'; +import { normalize } from '../utils/strings'; +import { isRor } from '../utils/ror'; import { getWorks } from '../utils/works'; -import Filters from './filters'; import Datasets from './views/datasets'; import Openalex from './ror-openalex/openalex'; import Publications from './views/publications'; @@ -19,7 +31,14 @@ import Publications from './views/publications'; import 'primereact/resources/primereact.min.css'; import 'primereact/resources/themes/lara-light-indigo/theme.css'; -export default function Affiliations({ isSticky, setIsSticky }) { +const { + VITE_APP_NAME, + VITE_APP_TAG_LIMIT, + VITE_HEADER_TAG, + VITE_HEADER_TAG_COLOR, +} = import.meta.env; + +export default function Affiliations() { const [searchParams] = useSearchParams(); const [affiliations, setAffiliations] = useState([]); @@ -37,11 +56,6 @@ export default function Affiliations({ isSticky, setIsSticky }) { cacheTime: 60 * (60 * 1000), // 1h }); - const sendQuery = async (_options) => { - await setOptions(_options); - refetch(); - }; - const tagPublications = (publications, action) => { const publicationsIds = publications.map((publication) => publication.id); data?.publications?.results @@ -58,9 +72,9 @@ export default function Affiliations({ isSticky, setIsSticky }) { setSelectedDatasets([]); }; - const tagAffiliations = (affiliations, action) => { + const tagAffiliations = (_affiliations, action) => { if (action !== status.excluded.id) { - const worksIds = affiliations + const worksIds = _affiliations .map((affiliation) => affiliation.works) .flat(); data?.publications?.results @@ -70,9 +84,9 @@ export default function Affiliations({ isSticky, setIsSticky }) { ?.filter((dataset) => worksIds.includes(dataset.id)) .map((dataset) => (dataset.status = action)); } - const affiliationIds = affiliations.map((affiliation) => affiliation.id); + const affiliationIds = _affiliations.map((affiliation) => affiliation.id); setAffiliations( - affiliations + _affiliations ?.filter((affiliation) => affiliationIds.includes(affiliation.id)) .map((affiliation) => (affiliation.status = action)), ); @@ -95,6 +109,47 @@ export default function Affiliations({ isSticky, setIsSticky }) { setAllOpenalexCorrections(getAffiliationsCorrections(newAffiliations)); }; + useEffect(() => { + const queryParams = { + datasets: searchParams.get('datasets') === 'true', + endYear: searchParams.get('endYear', '2023'), + startYear: searchParams.get('startYear', '2023'), + view: searchParams.get('view', ''), + }; + queryParams.affiliationStrings = []; + queryParams.deletedAffiliations = []; + queryParams.rors = []; + queryParams.rorExclusions = []; + searchParams.getAll('affiliations').forEach((item) => { + if (isRor(item)) { + queryParams.rors.push(item); + } else { + queryParams.affiliationStrings.push(normalize(item)); + } + }); + searchParams.getAll('deletedAffiliations').forEach((item) => { + if (isRor(item)) { + queryParams.rorExclusions.push(item); + } else { + queryParams.deletedAffiliations.push(normalize(item)); + } + }); + if ( + queryParams.affiliationStrings.length === 0 + && queryParams.rors.length === 0 + ) { + console.error( + `You must provide at least one affiliation longer than ${VITE_APP_TAG_LIMIT} letters.`, + ); + return; + } + setOptions(queryParams); + }, [searchParams]); + + useEffect(() => { + if (Object.keys(options).length > 0) refetch(); + }, [options, refetch]); + useEffect(() => { setAffiliations(data?.affiliations ?? []); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -103,12 +158,74 @@ export default function Affiliations({ isSticky, setIsSticky }) { return ( // TODO: Find a cleaner way to display the spinner and views <> - + + + + + + {VITE_APP_NAME} + {VITE_HEADER_TAG && ( + <Badge + className="fr-ml-1w" + color={VITE_HEADER_TAG_COLOR} + size="sm" + > + {VITE_HEADER_TAG} + </Badge> + )} + + + + + { + // setIsOpen(true); + e.preventDefault(); + }} + > + + + {`${options.startYear} - ${options.endYear}`} + + {/* {tagsDisplayed.map((tag) => ( + + {tag.label} + + ))} */} + + + + setSearchParams({ ...options, view })} + > + + + + + + + + + {isFetching && ( @@ -152,7 +269,9 @@ export default function Affiliations({ isSticky, setIsSticky }) { )} - {!isFetching && isFetched && searchParams.get('view') === 'openalex' && ( + {!isFetching + && isFetched + && searchParams.get('view') === 'openalex' && ( )} - {!isFetching && isFetched && searchParams.get('view') === 'publications' && ( + {!isFetching + && isFetched + && searchParams.get('view') === 'publications' && ( )} - {!isFetching && isFetched && searchParams.get('view') === 'datasets' && ( + {!isFetching + && isFetched + && searchParams.get('view') === 'datasets' && ( ); } - -Affiliations.propTypes = { - isSticky: PropTypes.bool.isRequired, - setIsSticky: PropTypes.func.isRequired, -}; diff --git a/client/src/pages/filters.jsx b/client/src/pages/filters.jsx index dbaa3a19..29d4fd8b 100644 --- a/client/src/pages/filters.jsx +++ b/client/src/pages/filters.jsx @@ -1,5 +1,4 @@ import { - Badge, Button, Checkbox, Col, @@ -7,29 +6,17 @@ import { Modal, ModalContent, Row, - SegmentedControl, - SegmentedElement, Select, SelectOption, - Tag, - TagGroup, TextInput, - Title, } from '@dataesr/dsfr-plus'; -import PropTypes from 'prop-types'; import { useEffect, useState } from 'react'; -import { useSearchParams } from 'react-router-dom'; +import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; -import Ribbon from '../components/ribbon'; import TagInput from '../components/tag-input'; import { getRorData, isRor } from '../utils/ror'; -const { - VITE_APP_NAME, - VITE_APP_TAG_LIMIT, - VITE_HEADER_TAG, - VITE_HEADER_TAG_COLOR, -} = import.meta.env; +const { VITE_APP_TAG_LIMIT } = import.meta.env; const START_YEAR = 2010; // Generate an array of objects with all years from START_YEAR @@ -37,14 +24,9 @@ const years = [...Array(new Date().getFullYear() - START_YEAR + 1).keys()] .map((year) => (year + START_YEAR).toString()) .map((year) => ({ label: year, value: year })); -const normalizeStr = (x) => x.replaceAll(',', ' ').replaceAll(' ', ' '); - -export default function Filters({ - isFetched, - isSticky, - sendQuery, - setIsSticky, -}) { +export default function Filters() { + const { pathname, search } = useLocation(); + const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const [currentSearchParams, setCurrentSearchParams] = useState({}); @@ -64,9 +46,9 @@ export default function Filters({ if (searchParams.size < 4) { // Set default params values const searchParamsTmp = { - affiliations: searchParams.get('affiliations') ?? [], + affiliations: searchParams.getAll('affiliations') ?? [], datasets: searchParams.get('datasets') ?? false, - deletedAffiliations: searchParams.get('deletedAffiliations') ?? [], + deletedAffiliations: searchParams.getAll('deletedAffiliations') ?? [], endYear: searchParams.get('endYear') ?? '2023', startYear: searchParams.get('startYear') ?? '2023', view: searchParams.get('view') ?? 'openalex', @@ -226,29 +208,7 @@ export default function Filters({ } setMessageType(''); setMessage(''); - const queryParams = { ...currentSearchParams }; - queryParams.affiliationStrings = tags - .filter((tag) => !tag.disable && tag.type === 'affiliationString') - .map((tag) => normalizeStr(tag.label)); - queryParams.rors = tags - .filter((tag) => !tag.disable && tag.type === 'rorId') - .map((tag) => tag.label); - queryParams.rorExclusions = rorExclusions - .split(' ') - .filter((item) => item?.length ?? false); - if ( - queryParams.affiliationStrings.length === 0 - && queryParams.rors.length === 0 - ) { - setMessageType('error'); - setMessage( - `You must provide at least one affiliation longer than ${VITE_APP_TAG_LIMIT} letters.`, - ); - return; - } - sendQuery(queryParams); - setIsOpen(false); - setIsSticky(true); + navigate(`/${pathname.split('/')[1]}/results${search}`); }; const NB_TAGS_STICKY = 2; @@ -260,212 +220,6 @@ export default function Filters({ return ( <> - {isSticky ? ( - - - - setIsSticky(false)} - > - - {VITE_APP_NAME} - {VITE_HEADER_TAG && ( - <Badge - className="fr-ml-1w" - color={VITE_HEADER_TAG_COLOR} - size="sm" - > - {VITE_HEADER_TAG} - </Badge> - )} - - - - - { - setIsOpen(true); - e.preventDefault(); - }} - > - - - {`${currentSearchParams.startYear} - ${currentSearchParams.endYear}`} - - {tagsDisplayed.map((tag) => ( - - {tag.label} - - ))} - - - - setSearchParams({ ...currentSearchParams, view })} - > - - - - - - - - - - ) : ( - <> - - - - { - setIsOpen(true); - e.preventDefault(); - }} - setGetRorChildren={setGetRorChildren} - tags={tags} - /> - - - - - - - - - - - - - setSearchParams({ - ...currentSearchParams, - datasets: e.target.checked, - })} - /> - - - - - - - setRorExclusions(e.target.value)} - value={rorExclusions} - /> - - - - - - - {isFetched && false && ( - - - - setSearchParams({ ...currentSearchParams, view })} - > - - - - - - - - )} - - )} setIsOpen(false)} size="xl"> @@ -562,13 +316,101 @@ export default function Filters({ + + + + { + setIsOpen(true); + e.preventDefault(); + }} + setGetRorChildren={setGetRorChildren} + tags={tags} + /> + + + + + + + + + + + + + setSearchParams({ + ...currentSearchParams, + datasets: e.target.checked, + })} + /> + + + + + + + setRorExclusions(e.target.value)} + value={rorExclusions} + /> + + + + + + ); } - -Filters.propTypes = { - isFetched: PropTypes.bool.isRequired, - isSticky: PropTypes.bool.isRequired, - sendQuery: PropTypes.func.isRequired, - setIsSticky: PropTypes.func.isRequired, -}; diff --git a/client/src/router.jsx b/client/src/router.jsx index 56a70f64..c43d3ccc 100644 --- a/client/src/router.jsx +++ b/client/src/router.jsx @@ -1,9 +1,10 @@ import { useState } from 'react'; -import { Route, Routes } from 'react-router-dom'; +import { Navigate, Route, Routes } from 'react-router-dom'; import Layout from './layout'; -import Home from './pages/home'; import Affiliations from './pages/affiliations'; +import Filters from './pages/filters'; +import Home from './pages/home'; import Mentions from './pages/mentions'; export default function Router() { @@ -14,24 +15,39 @@ export default function Router() { }> } /> } + /> + } /> + + } /> } + /> + } /> + + } /> } + /> + } /> + + } /> - } /> + } /> ); diff --git a/client/src/utils/strings.jsx b/client/src/utils/strings.jsx index 15ae05d2..a6330b22 100644 --- a/client/src/utils/strings.jsx +++ b/client/src/utils/strings.jsx @@ -1,3 +1,5 @@ +const normalize = (str) => str.replaceAll(',', ' ').replaceAll(' ', ' '); + // See https://stackoverflow.com/a/18391901 const defaultDiacriticsRemovalMap = [ { base: 'A', letters: '\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F' }, @@ -108,5 +110,6 @@ const removeDiacritics = (str) => str.replace(/[^\u0000-\u007E]/g, (a) => diacri .replaceAll(/ +/g, ' '); export { + normalize, removeDiacritics, };