diff --git a/client/src/pages/affiliations.jsx b/client/src/pages/affiliations.jsx index 45eb7732..bc6f8096 100644 --- a/client/src/pages/affiliations.jsx +++ b/client/src/pages/affiliations.jsx @@ -21,11 +21,11 @@ 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 { normalize } from '../utils/strings'; import { getWorks } from '../utils/works'; +import Openalex from './openalex-ror/openalex'; import Datasets from './views/datasets'; -import Openalex from './ror-openalex/openalex'; import Publications from './views/publications'; import 'primereact/resources/primereact.min.css'; diff --git a/client/src/pages/datasetsTab.jsx b/client/src/pages/datasets/datasetsTab.jsx similarity index 97% rename from client/src/pages/datasetsTab.jsx rename to client/src/pages/datasets/datasetsTab.jsx index 3b734f08..48322808 100644 --- a/client/src/pages/datasetsTab.jsx +++ b/client/src/pages/datasets/datasetsTab.jsx @@ -2,13 +2,13 @@ import { Button, Col, Row } from '@dataesr/dsfr-plus'; import PropTypes from 'prop-types'; import { useEffect, useState } from 'react'; -import Gauge from '../components/gauge'; -import { datasources, status } from '../config'; +import Gauge from '../../components/gauge'; +import { datasources, status } from '../../config'; import { normalizeName, renderButtonDataset, renderButtons, -} from '../utils/works'; +} from '../../utils/works'; import DatasetsView from './datasetsView'; export default function DatasetsTab({ diff --git a/client/src/pages/datasetsView.jsx b/client/src/pages/datasets/datasetsView.jsx similarity index 99% rename from client/src/pages/datasetsView.jsx rename to client/src/pages/datasets/datasetsView.jsx index 6d74f21d..c82e8a70 100644 --- a/client/src/pages/datasetsView.jsx +++ b/client/src/pages/datasets/datasetsView.jsx @@ -13,7 +13,7 @@ import { linkedORCIDTemplate, statusRowFilterTemplate, statusTemplate, -} from '../utils/templates'; +} from '../../utils/templates'; export default function DatasetsView({ filteredAffiliationName, diff --git a/client/src/pages/datasetsYearlyDistribution.jsx b/client/src/pages/datasets/datasetsYearlyDistribution.jsx similarity index 98% rename from client/src/pages/datasetsYearlyDistribution.jsx rename to client/src/pages/datasets/datasetsYearlyDistribution.jsx index 097723d3..65e79604 100644 --- a/client/src/pages/datasetsYearlyDistribution.jsx +++ b/client/src/pages/datasets/datasetsYearlyDistribution.jsx @@ -7,7 +7,7 @@ import HighchartsReact from 'highcharts-react-official'; import PropTypes from 'prop-types'; import { useSearchParams } from 'react-router-dom'; -import { range } from '../utils/works'; +import { range } from '../../utils/works'; export default function DatasetsYearlyDistribution({ allDatasets, field, subfield = undefined }) { const [searchParams] = useSearchParams(); diff --git a/client/src/pages/filters.jsx b/client/src/pages/datasets/search.jsx similarity index 99% rename from client/src/pages/filters.jsx rename to client/src/pages/datasets/search.jsx index 29d4fd8b..cce65510 100644 --- a/client/src/pages/filters.jsx +++ b/client/src/pages/datasets/search.jsx @@ -13,8 +13,8 @@ import { import { useEffect, useState } from 'react'; import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; -import TagInput from '../components/tag-input'; -import { getRorData, isRor } from '../utils/ror'; +import TagInput from '../../components/tag-input'; +import { getRorData, isRor } from '../../utils/ror'; const { VITE_APP_TAG_LIMIT } = import.meta.env; @@ -24,7 +24,7 @@ const years = [...Array(new Date().getFullYear() - START_YEAR + 1).keys()] .map((year) => (year + START_YEAR).toString()) .map((year) => ({ label: year, value: year })); -export default function Filters() { +export default function Search() { const { pathname, search } = useLocation(); const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); diff --git a/client/src/pages/openalex-ror/search.jsx b/client/src/pages/openalex-ror/search.jsx new file mode 100644 index 00000000..cce65510 --- /dev/null +++ b/client/src/pages/openalex-ror/search.jsx @@ -0,0 +1,416 @@ +import { + Button, + Checkbox, + Col, + Container, + Modal, + ModalContent, + Row, + Select, + SelectOption, + TextInput, +} from '@dataesr/dsfr-plus'; +import { useEffect, useState } from 'react'; +import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; + +import TagInput from '../../components/tag-input'; +import { getRorData, isRor } from '../../utils/ror'; + +const { VITE_APP_TAG_LIMIT } = import.meta.env; + +const START_YEAR = 2010; +// Generate an array of objects with all years from START_YEAR +const years = [...Array(new Date().getFullYear() - START_YEAR + 1).keys()] + .map((year) => (year + START_YEAR).toString()) + .map((year) => ({ label: year, value: year })); + +export default function Search() { + const { pathname, search } = useLocation(); + const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); + + const [currentSearchParams, setCurrentSearchParams] = useState({}); + const [deletedAffiliations, setDeletedAffiliations] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isOpen, setIsOpen] = useState(false); + const [getRorChildren, setGetRorChildren] = useState(false); + const [message, setMessage] = useState(''); + const [messageType, setMessageType] = useState(''); + const [onInputAffiliationsHandler, setOnInputAffiliationsHandler] = useState(false); + const [rorExclusions, setRorExclusions] = useState(''); + const [searchedAffiliations, setSearchedAffiliations] = useState([]); + const [tags, setTags] = useState([]); + + useEffect(() => { + const getData = async () => { + if (searchParams.size < 4) { + // Set default params values + const searchParamsTmp = { + affiliations: searchParams.getAll('affiliations') ?? [], + datasets: searchParams.get('datasets') ?? false, + deletedAffiliations: searchParams.getAll('deletedAffiliations') ?? [], + endYear: searchParams.get('endYear') ?? '2023', + startYear: searchParams.get('startYear') ?? '2023', + view: searchParams.get('view') ?? 'openalex', + }; + setSearchParams(searchParamsTmp); + setTags([]); + } else { + setIsLoading(true); + const affiliations = searchParams.getAll('affiliations') || []; + const deletedAffiliations1 = searchParams.getAll('deletedAffiliations') || []; + + setCurrentSearchParams({ + affiliations, + datasets: searchParams.get('datasets') === 'true', + deletedAffiliations: deletedAffiliations1, + endYear: searchParams.get('endYear', '2023'), + startYear: searchParams.get('startYear', '2023'), + view: searchParams.get('view', ''), + }); + + const newSearchedAffiliations = affiliations.filter( + (affiliation) => !searchedAffiliations.includes(affiliation), + ); + if (newSearchedAffiliations.length > 0) { + setSearchedAffiliations(affiliations); + } + const newDeletedAffiliations = deletedAffiliations1.filter( + (affiliation) => !deletedAffiliations.includes(affiliation), + ) + + deletedAffiliations.filter( + (affiliation) => !deletedAffiliations1.includes(affiliation), + ); + if (newDeletedAffiliations.length > 0) { + setDeletedAffiliations(deletedAffiliations1); + } + + setIsLoading(false); + } + }; + getData(); + }, [ + deletedAffiliations, + getRorChildren, + searchedAffiliations, + searchParams, + setSearchParams, + ]); + + useEffect(() => { + const getData = async () => { + setIsLoading(true); + const filteredSearchedAffiliation = searchedAffiliations.filter( + (affiliation) => !deletedAffiliations.includes(affiliation), + ); + const queries = filteredSearchedAffiliation.map((affiliation) => getRorData(affiliation, getRorChildren)); + let rorNames = await Promise.all(queries); + rorNames = rorNames.filter( + (rorName) => !deletedAffiliations.includes(rorName), + ); + + const allTags = []; + const knownTags = {}; + + filteredSearchedAffiliation.forEach((affiliation) => { + const label = affiliation + .replace('https://ror.org/', '') + .replace('ror.org/', ''); + if (isRor(label)) { + allTags.push({ + disable: label.length < VITE_APP_TAG_LIMIT, + label, + source: 'user', + type: 'rorId', + }); + } else { + allTags.push({ + disable: affiliation.length < VITE_APP_TAG_LIMIT, + label: affiliation, + source: 'user', + type: 'affiliationString', + }); + } + knownTags[label.toLowerCase()] = 1; + }); + + rorNames.flat().forEach((rorElt) => { + if (knownTags[rorElt.rorId.toLowerCase()] === undefined) { + if (!deletedAffiliations.includes(rorElt.rorId)) { + allTags.push({ + disable: rorElt.rorId.length < VITE_APP_TAG_LIMIT, + label: rorElt.rorId, + source: 'ror', + type: 'rorId', + }); + knownTags[rorElt.rorId.toLowerCase()] = 1; + } + } + + rorElt.names.forEach((rorName) => { + if (knownTags[rorName.toLowerCase()] === undefined) { + if (!deletedAffiliations.includes(rorName)) { + const isDangerous = rorName.length < 4; + allTags.push({ + disable: rorName.length < VITE_APP_TAG_LIMIT, + isDangerous, + label: rorName, + rorId: rorElt.rorId, + source: 'ror', + type: 'affiliationString', + }); + knownTags[rorName.toLowerCase()] = 1; + } + } + }); + }); + + setTags(allTags); + setIsLoading(false); + }; + + getData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [deletedAffiliations, getRorChildren, searchedAffiliations]); + + const onTagsChange = async (_affiliations, _deletedAffiliations) => { + const affiliations = _affiliations + .filter((affiliation) => affiliation.source === 'user') + .map((affiliation) => affiliation.label); + const deletedAffiliations1 = [ + ...new Set( + _deletedAffiliations + .map((affiliation) => affiliation.label) + .concat(currentSearchParams.deletedAffiliations || []), + ), + ].filter( + (item) => !_affiliations.map((affiliation) => affiliation.label).includes(item), + ); + setSearchParams({ + ...currentSearchParams, + affiliations, + deletedAffiliations: deletedAffiliations1, + }); + }; + + const checkAndSendQuery = () => { + if (onInputAffiliationsHandler) { + setMessageType('error'); + setMessage( + "Don't forget to validate the Affiliations input by pressing the return key.", + ); + return; + } + if (searchedAffiliations.length === 0) { + setMessageType('error'); + setMessage('You must provide at least one affiliation.'); + return; + } + setMessageType(''); + setMessage(''); + navigate(`/${pathname.split('/')[1]}/results${search}`); + }; + + const NB_TAGS_STICKY = 2; + const tagsDisplayed = tags.slice(0, NB_TAGS_STICKY); + + if (tags.length > NB_TAGS_STICKY) { + tagsDisplayed.push({ label: '...' }); + } + + return ( + <> + setIsOpen(false)} size="xl"> + + + + + + + + + + + + + setSearchParams({ + ...currentSearchParams, + datasets: e.target.checked, + })} + /> + + + + + + + + setRorExclusions(e.target.value)} + value={rorExclusions} + /> + + + + + + + + + + + + { + setIsOpen(true); + e.preventDefault(); + }} + setGetRorChildren={setGetRorChildren} + tags={tags} + /> + + + + + + + + + + + + + setSearchParams({ + ...currentSearchParams, + datasets: e.target.checked, + })} + /> + + + + + + + setRorExclusions(e.target.value)} + value={rorExclusions} + /> + + + + + + + + ); +} diff --git a/client/src/pages/publicationsTab.jsx b/client/src/pages/publications/publicationsTab.jsx similarity index 96% rename from client/src/pages/publicationsTab.jsx rename to client/src/pages/publications/publicationsTab.jsx index bc61d755..590d39e4 100644 --- a/client/src/pages/publicationsTab.jsx +++ b/client/src/pages/publications/publicationsTab.jsx @@ -6,9 +6,9 @@ import { } from '@dataesr/dsfr-plus'; import PublicationsView from './publicationsView'; -import Gauge from '../components/gauge'; -import { datasources, status } from '../config'; -import { normalizeName, renderButtons } from '../utils/works'; +import Gauge from '../../components/gauge'; +import { datasources, status } from '../../config'; +import { normalizeName, renderButtons } from '../../utils/works'; export default function PublicationsTab({ publications, publishers, selectedPublications, setSelectedPublications, tagPublications, types, years }) { const [filteredAffiliationName, setFilteredAffiliationName] = useState(''); diff --git a/client/src/pages/publicationsView.jsx b/client/src/pages/publications/publicationsView.jsx similarity index 99% rename from client/src/pages/publicationsView.jsx rename to client/src/pages/publications/publicationsView.jsx index 1974f850..bedb5efc 100644 --- a/client/src/pages/publicationsView.jsx +++ b/client/src/pages/publications/publicationsView.jsx @@ -13,7 +13,7 @@ import { datasourceTemplate, statusRowFilterTemplate, statusTemplate, -} from '../utils/templates'; +} from '../../utils/templates'; export default function PublicationsView({ filteredAffiliationName, diff --git a/client/src/pages/publications/search.jsx b/client/src/pages/publications/search.jsx new file mode 100644 index 00000000..cce65510 --- /dev/null +++ b/client/src/pages/publications/search.jsx @@ -0,0 +1,416 @@ +import { + Button, + Checkbox, + Col, + Container, + Modal, + ModalContent, + Row, + Select, + SelectOption, + TextInput, +} from '@dataesr/dsfr-plus'; +import { useEffect, useState } from 'react'; +import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; + +import TagInput from '../../components/tag-input'; +import { getRorData, isRor } from '../../utils/ror'; + +const { VITE_APP_TAG_LIMIT } = import.meta.env; + +const START_YEAR = 2010; +// Generate an array of objects with all years from START_YEAR +const years = [...Array(new Date().getFullYear() - START_YEAR + 1).keys()] + .map((year) => (year + START_YEAR).toString()) + .map((year) => ({ label: year, value: year })); + +export default function Search() { + const { pathname, search } = useLocation(); + const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); + + const [currentSearchParams, setCurrentSearchParams] = useState({}); + const [deletedAffiliations, setDeletedAffiliations] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isOpen, setIsOpen] = useState(false); + const [getRorChildren, setGetRorChildren] = useState(false); + const [message, setMessage] = useState(''); + const [messageType, setMessageType] = useState(''); + const [onInputAffiliationsHandler, setOnInputAffiliationsHandler] = useState(false); + const [rorExclusions, setRorExclusions] = useState(''); + const [searchedAffiliations, setSearchedAffiliations] = useState([]); + const [tags, setTags] = useState([]); + + useEffect(() => { + const getData = async () => { + if (searchParams.size < 4) { + // Set default params values + const searchParamsTmp = { + affiliations: searchParams.getAll('affiliations') ?? [], + datasets: searchParams.get('datasets') ?? false, + deletedAffiliations: searchParams.getAll('deletedAffiliations') ?? [], + endYear: searchParams.get('endYear') ?? '2023', + startYear: searchParams.get('startYear') ?? '2023', + view: searchParams.get('view') ?? 'openalex', + }; + setSearchParams(searchParamsTmp); + setTags([]); + } else { + setIsLoading(true); + const affiliations = searchParams.getAll('affiliations') || []; + const deletedAffiliations1 = searchParams.getAll('deletedAffiliations') || []; + + setCurrentSearchParams({ + affiliations, + datasets: searchParams.get('datasets') === 'true', + deletedAffiliations: deletedAffiliations1, + endYear: searchParams.get('endYear', '2023'), + startYear: searchParams.get('startYear', '2023'), + view: searchParams.get('view', ''), + }); + + const newSearchedAffiliations = affiliations.filter( + (affiliation) => !searchedAffiliations.includes(affiliation), + ); + if (newSearchedAffiliations.length > 0) { + setSearchedAffiliations(affiliations); + } + const newDeletedAffiliations = deletedAffiliations1.filter( + (affiliation) => !deletedAffiliations.includes(affiliation), + ) + + deletedAffiliations.filter( + (affiliation) => !deletedAffiliations1.includes(affiliation), + ); + if (newDeletedAffiliations.length > 0) { + setDeletedAffiliations(deletedAffiliations1); + } + + setIsLoading(false); + } + }; + getData(); + }, [ + deletedAffiliations, + getRorChildren, + searchedAffiliations, + searchParams, + setSearchParams, + ]); + + useEffect(() => { + const getData = async () => { + setIsLoading(true); + const filteredSearchedAffiliation = searchedAffiliations.filter( + (affiliation) => !deletedAffiliations.includes(affiliation), + ); + const queries = filteredSearchedAffiliation.map((affiliation) => getRorData(affiliation, getRorChildren)); + let rorNames = await Promise.all(queries); + rorNames = rorNames.filter( + (rorName) => !deletedAffiliations.includes(rorName), + ); + + const allTags = []; + const knownTags = {}; + + filteredSearchedAffiliation.forEach((affiliation) => { + const label = affiliation + .replace('https://ror.org/', '') + .replace('ror.org/', ''); + if (isRor(label)) { + allTags.push({ + disable: label.length < VITE_APP_TAG_LIMIT, + label, + source: 'user', + type: 'rorId', + }); + } else { + allTags.push({ + disable: affiliation.length < VITE_APP_TAG_LIMIT, + label: affiliation, + source: 'user', + type: 'affiliationString', + }); + } + knownTags[label.toLowerCase()] = 1; + }); + + rorNames.flat().forEach((rorElt) => { + if (knownTags[rorElt.rorId.toLowerCase()] === undefined) { + if (!deletedAffiliations.includes(rorElt.rorId)) { + allTags.push({ + disable: rorElt.rorId.length < VITE_APP_TAG_LIMIT, + label: rorElt.rorId, + source: 'ror', + type: 'rorId', + }); + knownTags[rorElt.rorId.toLowerCase()] = 1; + } + } + + rorElt.names.forEach((rorName) => { + if (knownTags[rorName.toLowerCase()] === undefined) { + if (!deletedAffiliations.includes(rorName)) { + const isDangerous = rorName.length < 4; + allTags.push({ + disable: rorName.length < VITE_APP_TAG_LIMIT, + isDangerous, + label: rorName, + rorId: rorElt.rorId, + source: 'ror', + type: 'affiliationString', + }); + knownTags[rorName.toLowerCase()] = 1; + } + } + }); + }); + + setTags(allTags); + setIsLoading(false); + }; + + getData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [deletedAffiliations, getRorChildren, searchedAffiliations]); + + const onTagsChange = async (_affiliations, _deletedAffiliations) => { + const affiliations = _affiliations + .filter((affiliation) => affiliation.source === 'user') + .map((affiliation) => affiliation.label); + const deletedAffiliations1 = [ + ...new Set( + _deletedAffiliations + .map((affiliation) => affiliation.label) + .concat(currentSearchParams.deletedAffiliations || []), + ), + ].filter( + (item) => !_affiliations.map((affiliation) => affiliation.label).includes(item), + ); + setSearchParams({ + ...currentSearchParams, + affiliations, + deletedAffiliations: deletedAffiliations1, + }); + }; + + const checkAndSendQuery = () => { + if (onInputAffiliationsHandler) { + setMessageType('error'); + setMessage( + "Don't forget to validate the Affiliations input by pressing the return key.", + ); + return; + } + if (searchedAffiliations.length === 0) { + setMessageType('error'); + setMessage('You must provide at least one affiliation.'); + return; + } + setMessageType(''); + setMessage(''); + navigate(`/${pathname.split('/')[1]}/results${search}`); + }; + + const NB_TAGS_STICKY = 2; + const tagsDisplayed = tags.slice(0, NB_TAGS_STICKY); + + if (tags.length > NB_TAGS_STICKY) { + tagsDisplayed.push({ label: '...' }); + } + + return ( + <> + setIsOpen(false)} size="xl"> + + + + + + + + + + + + + setSearchParams({ + ...currentSearchParams, + datasets: e.target.checked, + })} + /> + + + + + + + + setRorExclusions(e.target.value)} + value={rorExclusions} + /> + + + + + + + + + + + + { + setIsOpen(true); + e.preventDefault(); + }} + setGetRorChildren={setGetRorChildren} + tags={tags} + /> + + + + + + + + + + + + + setSearchParams({ + ...currentSearchParams, + datasets: e.target.checked, + })} + /> + + + + + + + setRorExclusions(e.target.value)} + value={rorExclusions} + /> + + + + + + + + ); +} diff --git a/client/src/pages/views/datasets.jsx b/client/src/pages/views/datasets.jsx index 6b8deedb..affa7eb7 100644 --- a/client/src/pages/views/datasets.jsx +++ b/client/src/pages/views/datasets.jsx @@ -9,8 +9,8 @@ import { Title, } from '@dataesr/dsfr-plus'; import ActionsDatasets from '../actions/actionsDatasets'; -import DatasetsTab from '../datasetsTab'; -import DatasetsYearlyDistribution from '../datasetsYearlyDistribution'; +import DatasetsTab from '../datasets/datasetsTab'; +import DatasetsYearlyDistribution from '../datasets/datasetsYearlyDistribution'; import AffiliationsTab from '../affiliationsTab'; import ActionsAffiliations from '../actions/actionsAffiliations'; diff --git a/client/src/pages/views/publications.jsx b/client/src/pages/views/publications.jsx index 03376041..aa57e202 100644 --- a/client/src/pages/views/publications.jsx +++ b/client/src/pages/views/publications.jsx @@ -12,7 +12,7 @@ import { useState } from 'react'; import ActionsAffiliations from '../actions/actionsAffiliations'; import ActionsPublications from '../actions/actionsPublications'; import AffiliationsTab from '../affiliationsTab'; -import PublicationsTab from '../publicationsTab'; +import PublicationsTab from '../publications/publicationsTab'; export default function Publications({ allAffiliations, diff --git a/client/src/router.jsx b/client/src/router.jsx index c43d3ccc..1ab8e9d1 100644 --- a/client/src/router.jsx +++ b/client/src/router.jsx @@ -3,12 +3,14 @@ import { Navigate, Route, Routes } from 'react-router-dom'; import Layout from './layout'; import Affiliations from './pages/affiliations'; -import Filters from './pages/filters'; +import DatasetsSearch from './pages/datasets/search'; import Home from './pages/home'; import Mentions from './pages/mentions'; +import OpenalexRorSearch from './pages/openalex-ror/search'; +import PublicationsSearch from './pages/publications/search'; export default function Router() { - const [isSticky, setIsSticky] = useState(false); + const [isSticky] = useState(false); return ( @@ -18,33 +20,33 @@ export default function Router() { path="/openalex-ror" element={} /> - } /> + } /> + } /> } /> - } /> + } /> + } /> } /> - } /> + } /> + } /> } />