diff --git a/packages/sections/src/common/Literature/Body.jsx b/packages/sections/src/common/Literature/Body.jsx index feb5c8889..b69cd5625 100644 --- a/packages/sections/src/common/Literature/Body.jsx +++ b/packages/sections/src/common/Literature/Body.jsx @@ -1,62 +1,71 @@ import { useEffect, useState } from "react"; +import { + LiteratureProvider, + useLiterature, + useLiteratureDispatch, + defaultLiteratureState, +} from "./LiteratureContext"; +import { fetchSimilarEntities } from "./requests"; import { Box } from "@mui/material"; -import { useSetRecoilState, useRecoilValue, useResetRecoilState, RecoilRoot } from "recoil"; import { SectionItem } from "ui"; import PublicationsList from "./PublicationsList"; import Description from "./Description"; -import { literatureState, updateLiteratureState, fetchSimilarEntities } from "./atoms"; import Entities from "./Entities"; import Category from "./Category"; import CountInfo from "./CountInfo"; import { DateFilter } from "./DateFilter"; -function LiteratureList({ id, name, entity, BODY_QUERY, definition }) { - const [requestObj, setRequestObj] = useState({}); +// type LiteratureListParameter = { +// id: string, +// name: string, +// entity: any, // ADD LATER +// BODY_QUERY: string, +// definition: any, // ADD LATER +// }; - const setLiteratureUpdate = useSetRecoilState(updateLiteratureState); - const resetLiteratureState = useResetRecoilState(literatureState); +function LiteratureList( + { id, name, entity, BODY_QUERY, definition }) { + // { id, name, entity, BODY_QUERY, definition }: LiteratureListParameter) { - const bibliographyState = useRecoilValue(literatureState); - const { category, startYear, startMonth, endYear, endMonth } = bibliographyState; + const [requestObj, setRequestObj] = useState({}); + const literature = useLiterature(); + const { category, startYear, startMonth, endYear, endMonth } = literature; + const literatureDispatch = useLiteratureDispatch(); - useEffect( - () => { - async function startRequest() { - const inintRequest = await fetchSimilarEntities({ - id, - query: BODY_QUERY, - category, - startYear, - startMonth, - endYear, - endMonth, - }); - setRequestObj(inintRequest); - const data = inintRequest.data[entity]; - const update = { - entities: data.similarEntities, - litsIds: data.literatureOcurrences?.rows?.map(({ pmid }) => ({ - id: pmid, - status: "ready", - publication: null, - })), - litsCount: data.literatureOcurrences?.filteredCount, - earliestPubYear: data.literatureOcurrences?.earliestPubYear, - cursor: data.literatureOcurrences?.cursor, - id, - query: BODY_QUERY, - globalEntity: entity, - }; - setLiteratureUpdate(update); - } - startRequest(); - return function cleanUp() { - resetLiteratureState(); + useEffect(() => { + async function startRequest() { + const initRequest = await fetchSimilarEntities({ + id, + query: BODY_QUERY, + category, + startYear, + startMonth, + endYear, + endMonth, + }); + setRequestObj(initRequest); + const data = initRequest.data[entity]; + const update = { + entities: data.similarEntities, + litsIds: data.literatureOcurrences?.rows?.map(({ pmid }) => pmid), + litsCount: data.literatureOcurrences?.filteredCount, + earliestPubYear: data.literatureOcurrences?.earliestPubYear, + cursor: data.literatureOcurrences?.cursor, + id, + query: BODY_QUERY, + globalEntity: entity, }; - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); + literatureDispatch({ type: 'stateUpdate', value: update }); + } + startRequest(); + // return function cleanUp() { + // literatureDispatch({ + // type: 'stateUpdate', + // value: defaultLiteratureState + // }); + // }; + }, []); + return ( + - + /> + ); } -export default Body; +export default Body; \ No newline at end of file diff --git a/packages/sections/src/common/Literature/Category.jsx b/packages/sections/src/common/Literature/Category.jsx index 8dd350c99..8569d5a44 100644 --- a/packages/sections/src/common/Literature/Category.jsx +++ b/packages/sections/src/common/Literature/Category.jsx @@ -1,12 +1,8 @@ -import { InputLabel, FormGroup, Checkbox, FormControlLabel } from "@mui/material"; -import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil"; -import { - selectedCategoriesState, - loadingEntitiesState, - literatureState, - fetchSimilarEntities, - updateLiteratureState, -} from "./atoms"; +import { InputLabel, FormGroup, Checkbox, FormControlLabel } + from "@mui/material"; +import { useLiterature, useSelectedCategories, useLiteratureDispatch } + from "./LiteratureContext"; +import { fetchSimilarEntities } from "./requests"; const toggleValue = (selected, categories) => { const isChecked = categories.indexOf(selected) !== -1; @@ -21,11 +17,10 @@ const categories = [ ]; export default function Category() { - const category = useRecoilValue(selectedCategoriesState); - const setLiteratureUpdate = useSetRecoilState(updateLiteratureState); - const [loadingEntities, setLoadingEntities] = useRecoilState(loadingEntitiesState); - const bibliographyState = useRecoilValue(literatureState); + const literature = useLiterature(); + const category = useSelectedCategories(); + const literatureDispatch = useLiteratureDispatch(); const handleChange = async event => { const { @@ -39,12 +34,10 @@ export default function Category() { endMonth, startYear, startMonth, - } = bibliographyState; - const { - target: { name: clicked }, - } = event; + } = literature; + const { target: { name: clicked } } = event; const newCategories = toggleValue(clicked, bibliographyCategory); - setLoadingEntities(true); + literatureDispatch({ type: 'loadingEntities', value: true }); const request = await fetchSimilarEntities({ query, id, @@ -57,7 +50,6 @@ export default function Category() { startMonth, }); const data = request.data[globalEntity]; - const update = { entities: data.similarEntities, earliestPubYear: data.literatureOcurrences?.earliestPubYear, @@ -65,7 +57,7 @@ export default function Category() { loadingEntities: false, category: newCategories, }; - setLiteratureUpdate(update); + literatureDispatch({ type: 'stateUpdate', value: update }); }; return ( @@ -88,7 +80,7 @@ export default function Category() { onChange={handleChange} name={name} color="primary" - disabled={loadingEntities} + disabled={literature.loadingEntities} /> } label={label} diff --git a/packages/sections/src/common/Literature/CountInfo.jsx b/packages/sections/src/common/Literature/CountInfo.jsx index 241aae271..62f2f6d94 100644 --- a/packages/sections/src/common/Literature/CountInfo.jsx +++ b/packages/sections/src/common/Literature/CountInfo.jsx @@ -1,7 +1,6 @@ import { InputLabel, Box } from "@mui/material"; import { makeStyles } from "@mui/styles"; -import { useRecoilValue } from "recoil"; -import { litsCountState, loadingEntitiesState, tablePageSizeState } from "./atoms"; +import { useLiterature } from "./LiteratureContext"; const useStyles = makeStyles(() => ({ resultCount: { @@ -11,14 +10,12 @@ const useStyles = makeStyles(() => ({ })); function CountInfo() { + const { pageSize, litsCount, loadingEntities } = useLiterature(); const classes = useStyles(); - const pageSize = useRecoilValue(tablePageSizeState); - const count = useRecoilValue(litsCountState); - const loadingEntities = useRecoilValue(loadingEntitiesState); - const getLabelText = () => { if (loadingEntities) return "Loading count..."; - return `Showing ${count > pageSize ? pageSize : count} of ${count} results`; + return `Showing ${ + litsCount > pageSize ? pageSize : litsCount} of ${litsCount} results`; }; return ( @@ -28,4 +25,4 @@ function CountInfo() { ); } -export default CountInfo; +export default CountInfo; \ No newline at end of file diff --git a/packages/sections/src/common/Literature/DateFilter.tsx b/packages/sections/src/common/Literature/DateFilter.jsx similarity index 71% rename from packages/sections/src/common/Literature/DateFilter.tsx rename to packages/sections/src/common/Literature/DateFilter.jsx index 3e9e7a595..28cc7927a 100644 --- a/packages/sections/src/common/Literature/DateFilter.tsx +++ b/packages/sections/src/common/Literature/DateFilter.jsx @@ -1,13 +1,8 @@ import { useEffect, useState } from "react"; import { FormControl, FormGroup, InputLabel, Slider } from "@mui/material"; -import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil"; -import { - fetchSimilarEntities, - literatureState, - loadingEntitiesState, - updateLiteratureState, -} from "./atoms"; import { styled } from "@mui/material/styles"; +import { useLiterature, useLiteratureDispatch } from "./LiteratureContext"; +import { fetchSimilarEntities } from "./requests"; const OTSlider = styled(Slider)({ root: { @@ -29,7 +24,7 @@ const DateIndicator = styled("span")({ maxWidth: 80, }); -const monthsBtwnDates = (startDate: Date, endDate: Date) => +const monthsBtwnDates = (startDate, endDate) => Math.max( (endDate.getFullYear() - startDate.getFullYear()) * 12 + (endDate.getMonth() - startDate.getMonth()), @@ -37,30 +32,31 @@ const monthsBtwnDates = (startDate: Date, endDate: Date) => ); export function DateFilter() { - const [filterDate, setFilterDate] = useState([0, 0]); + + const [filterDate, setFilterDate] = useState([0, 0]); const [numberOfMonths, setNumberOfMonths] = useState(0); const [pubYear, setPubYear] = useState(0); - const setLiteratureUpdate = useSetRecoilState(updateLiteratureState); - const [_, setLoadingEntities] = useRecoilState(loadingEntitiesState); + const literature = useLiterature(); + const literatureDispatch = useLiteratureDispatch(); + const { query, id, category, earliestPubYear, selectedEntities, - cursor, globalEntity, litsIds, pageSize, litsCount, loadingEntities, - } = useRecoilValue(literatureState); + } = literature; - function getDateFromYear(year: number) { + function getDateFromYear(year) { return new Date(year, 0, 1, 1, 1, 1, 1); } - const sumMonthsSinceYear = (year: number) => (value: number) => { + const sumMonthsSinceYear = year => value => { const from = getDateFromYear(year); const date = new Date(from.setMonth(from.getMonth() + value)); return date; @@ -74,11 +70,8 @@ export function DateFilter() { if (earliestPubYear && earliestPubYear !== pubYear) { const earliestDate = getDateFromYear(earliestPubYear); const limit = monthsBtwnDates(earliestDate, new Date()); - const lowerLimit = getLowerLimit(earliestDate); - const higherLimit = getHigherLimit(earliestDate, limit); - setFilterDate([lowerLimit, higherLimit]); setNumberOfMonths(limit); setPubYear(earliestPubYear); @@ -89,15 +82,17 @@ export function DateFilter() { } }, [earliestPubYear]); - function getHigherLimit(earliestDate: Date, limit: number) { + function getHigherLimit(earliestDate, limit) { const oldHigherDate = oldSelectedDate(filterDate[1]); const newHighFilter = monthsBtwnDates(earliestDate, oldHigherDate); const higherLimit = - filterDate[1] > 0 && newHighFilter > 0 && newHighFilter < limit ? newHighFilter : limit; + filterDate[1] > 0 && newHighFilter > 0 && newHighFilter < limit + ? newHighFilter + : limit; return higherLimit; } - function getLowerLimit(earliestDate: Date) { + function getLowerLimit(earliestDate) { if (filterDate[0] == 0) return 0; const oldLowerDate = oldSelectedDate(filterDate[0]); const newLowerFilter = monthsBtwnDates(earliestDate, oldLowerDate); @@ -105,29 +100,15 @@ export function DateFilter() { return lowerLimit; } - const handleChange = async (values: { - startYear: number; - startMonth: number; - endYear: number; - endMonth: number; - }) => { - setLoadingEntities(true); - const entities = selectedEntities as any[]; + const handleChange = async values => { + literatureDispatch({ type: 'loadingEntities', value: true }); const request = await fetchSimilarEntities({ query, id, category, - entities, + entities: selectedEntities, //selectedEntities as any[], cursor: null, - earliestPubYear, - globalEntity, - selectedEntities, - litsIds, - page: 0, - pageSize, - litsCount, - loadingEntities, - ...values, + ...values, // values has startYear, startMonth, endYear, endMonth }); const data = request.data[globalEntity]; const update = { @@ -137,38 +118,31 @@ export function DateFilter() { entities: data.similarEntities, loadingEntities: false, category, - litsIds: data.literatureOcurrences?.rows?.map(({ pmid }: { pmid: any }) => ({ - id: pmid, - status: "ready", - publication: null, - })), + litsIds: data.literatureOcurrences?.rows?.map(({ pmid }) => pmid), litsCount: data.literatureOcurrences?.filteredCount, earliestPubYear: data.literatureOcurrences?.earliestPubYear, globalEntity, selectedEntities, page: 0, pageSize, - ...values, + ...values, // values has startYear, startMonth, endYear, endMonth }; - setLiteratureUpdate(update); + literatureDispatch({ type: 'stateUpdate', value: update }); }; - const valueLabelFormat = (value: number | number[]) => { + const valueLabelFormat = (value) => { if (earliestPubYear) { - const labelDate = selectedDate(value as number); + const labelDate = selectedDate(value); return `${labelDate.getFullYear()}-${labelDate.getMonth() + 1}`; } return "YYYY-MM"; }; - const handleDateRangeChange = (_event: Event, value: number[] | number, _activeThumb: number) => { - setFilterDate(value as number[]); + const handleDateRangeChange = (_event, value, _activeThumb) => { + setFilterDate(value); }; - const handleDateRangeChangeCommitted = ( - _event: Event | React.SyntheticEvent, - value: number | number[] - ) => { + const handleDateRangeChangeCommitted = (_event, value) => { if (Array.isArray(value)) { const startDate = selectedDate(value[0]); const endDate = selectedDate(value[1]); diff --git a/packages/sections/src/common/Literature/Entities.jsx b/packages/sections/src/common/Literature/Entities.jsx index 3d7a119f2..7657fefc0 100644 --- a/packages/sections/src/common/Literature/Entities.jsx +++ b/packages/sections/src/common/Literature/Entities.jsx @@ -1,14 +1,7 @@ import { Chip, Grow } from "@mui/material"; import { makeStyles } from "@mui/styles"; -import { useRecoilState, useSetRecoilState, useRecoilValue } from "recoil"; -import { - entitiesState, - selectedEntitiesState, - fetchSimilarEntities, - literatureState, - loadingEntitiesState, - updateLiteratureState, -} from "./atoms"; +import { useLiterature, useLiteratureDispatch } from "./LiteratureContext"; +import { fetchSimilarEntities } from "./requests"; const useStyles = makeStyles(theme => ({ root: { @@ -25,11 +18,14 @@ const useStyles = makeStyles(theme => ({ })); function EntitiesToSelect({ id }) { - const entities = useRecoilValue(entitiesState); - const bibliographyState = useRecoilValue(literatureState); - const setLiteratureUpdate = useSetRecoilState(updateLiteratureState); - const [selectedChips, setSelectedChips] = useRecoilState(selectedEntitiesState); - const [loadingEntities, setLoadingEntities] = useRecoilState(loadingEntitiesState); + + const literature = useLiterature(); + const { + entities, + selectedEntities: selectedChips, + loadingEntities, + } = literature; + const literatureDispatch = useLiteratureDispatch(); const handleSelectChip = async e => { const { @@ -41,7 +37,7 @@ function EntitiesToSelect({ id }) { endMonth, startYear, startMonth, - } = bibliographyState; + } = literature; const newChips = [ ...selectedChips, { @@ -52,8 +48,8 @@ function EntitiesToSelect({ id }) { }, }, ]; - setSelectedChips(newChips); - setLoadingEntities(true); + literatureDispatch({ type: 'selectedEntities', value: newChips }); + literatureDispatch({ type: 'loadingEntities', value: true }); const request = await fetchSimilarEntities({ query, id: bibliographyId, @@ -67,18 +63,14 @@ function EntitiesToSelect({ id }) { const data = request.data[globalEntity]; const update = { entities: data.similarEntities, - litsIds: data.literatureOcurrences?.rows?.map(({ pmid }) => ({ - id: pmid, - status: "ready", - publication: null, - })), + litsIds: data.literatureOcurrences?.rows?.map(({ pmid }) => pmid), litsCount: data.literatureOcurrences?.filteredCount, earliestPubYear: data.literatureOcurrences?.earliestPubYear, cursor: data.literatureOcurrences?.cursor, loadingEntities: false, page: 0, }; - setLiteratureUpdate(update); + literatureDispatch({ type: 'stateUpdate', value: update }); }; const validateEntity = entity => { @@ -121,12 +113,14 @@ function EntitiesToSelect({ id }) { } export default function Entities({ name, id }) { + const classes = useStyles(); - - const setLiteratureUpdate = useSetRecoilState(updateLiteratureState); - const bibliographyState = useRecoilValue(literatureState); - const [loadingEntities, setLoadingEntities] = useRecoilState(loadingEntitiesState); - const [selectedChips, setSelectedChips] = useRecoilState(selectedEntitiesState); + const literature = useLiterature(); + const { + selectedEntities: selectedChips, + loadingEntities, + } = literature; + const literatureDispatch = useLiteratureDispatch(); const handleDeleteChip = async index => { const { @@ -138,10 +132,11 @@ export default function Entities({ name, id }) { endMonth, startYear, startMonth, - } = bibliographyState; - const newChips = [...selectedChips.slice(0, index), ...selectedChips.slice(index + 1)]; - setSelectedChips(newChips); - setLoadingEntities(true); + } = literature; + const newChips = + [...selectedChips.slice(0, index), ...selectedChips.slice(index + 1)]; + literatureDispatch({ type: 'selectedEntities', value: newChips }); + literatureDispatch({ type: 'loadingEntities', value: true }); const request = await fetchSimilarEntities({ query, id: bibliographyId, @@ -155,18 +150,14 @@ export default function Entities({ name, id }) { const data = request.data[globalEntity]; const update = { entities: data.similarEntities, - litsIds: data.literatureOcurrences?.rows?.map(({ pmid }) => ({ - id: pmid, - status: "ready", - publication: null, - })), + litsIds: data.literatureOcurrences?.rows?.map(({ pmid }) => pmid), litsCount: data.literatureOcurrences?.filteredCount, earliestPubYear: data.literatureOcurrences?.earliestPubYear, cursor: data.literatureOcurrences?.cursor, loadingEntities: false, page: 0, }; - setLiteratureUpdate(update); + literatureDispatch({ type: 'stateUpdate', value: update }); }; return ( diff --git a/packages/sections/src/common/Literature/LiteratureContext.tsx b/packages/sections/src/common/Literature/LiteratureContext.tsx new file mode 100644 index 000000000..ad6bf864a --- /dev/null +++ b/packages/sections/src/common/Literature/LiteratureContext.tsx @@ -0,0 +1,120 @@ +import isEmpty from "lodash/isEmpty"; +import { createContext, useContext, useReducer } from 'react'; +import type { LiteratureStateType, DetailsStateType } from './types'; +import { getPage } from "ui"; + +export const defaultLiteratureState: LiteratureStateType = { + id: "", + cursor: "", + category: ["disease", "drug", "target"], + query: null, + globalEntity: null, + entities: [], + selectedEntities: [], + startYear: null, + startMonth: null, + endYear: null, + endMonth: null, + earliestPubYear: 0, + litsIds: [], + page: 0, + pageSize: 5, + litsCount: 0, + loadingEntities: false, +}; + +const LiteratureContext = createContext(null as any); +const LiteratureDispatchContext = createContext(null as any); + +const DetailsContext = createContext(null as any); +const DetailsDispatchContext = createContext(null as any); + +function literatureReducer(literatureState: LiteratureStateType, action: any) { + console.log(`LITERATURE REDUCER: ${action.type}`); + switch (action.type) { + case 'loadingEntities': + return { + ...literatureState, + loadingEntities: action.value, + }; + case 'tablePageSize': + return { + ...literatureState, + pageSize: action.value, + }; + case 'selectedEntities': + return { + ...literatureState, + selectedEntities: action.value, + }; + case 'stateUpdate': + return { + ...literatureState, + ...action.value, + }; + default: + throw Error('invalid action type'); + } +} + +function detailsReducer(detailsState: DetailsStateType, action: any) { + console.log(`DETAILS REDUCER: ${action.type}`); + switch (action.type) { + case 'addDetails': + return { ...detailsState, ...action.value }; + case 'setToLoading': { + const newObj = { ...detailsState }; + for (const id of action.value) { + newObj[id] = 'loading'; + } + return newObj; + } + } +} + +export function LiteratureProvider({ children }) { + + const [literature, literatureDispatch] = + useReducer(literatureReducer, defaultLiteratureState); + + const [details, detailsDispatch] = useReducer(detailsReducer, {}); + + return ( + + + + + {children} + + + + + ); + +} + +export function useLiterature() { + return useContext(LiteratureContext); +} + +export function useLiteratureDispatch() { + return useContext(LiteratureDispatchContext); +} + +export function useSelectedCategories() { + const { category } = useLiterature(); + return [...category].sort(); +} + +export function useDisplayedPublications() { + const { page, pageSize, litsIds } = useLiterature(); + return isEmpty(litsIds) ? [] : getPage(litsIds, page, pageSize); +} + +export function useDetails() { + return useContext(DetailsContext); +} + +export function useDetailsDispatch() { + return useContext(DetailsDispatchContext); +} \ No newline at end of file diff --git a/packages/sections/src/common/Literature/PublicationsList.jsx b/packages/sections/src/common/Literature/PublicationsList.jsx deleted file mode 100644 index eec522374..000000000 --- a/packages/sections/src/common/Literature/PublicationsList.jsx +++ /dev/null @@ -1,242 +0,0 @@ -import { useEffect } from "react"; -import { useRecoilState, useRecoilValue, useSetRecoilState, useRecoilCallback } from "recoil"; -import { Box, Grid, Fade, Skeleton } from "@mui/material"; -import { makeStyles } from "@mui/styles"; -import { PublicationWrapper, Table } from "ui"; -import Loader from "./Loader"; - -import { - litsIdsState, - loadingEntitiesState, - displayedPublications, - literaturesEuropePMCQuery, - parsePublications, - tablePageState, - litsCountState, - litsCursorState, - literatureState, - fetchSimilarEntities, - updateLiteratureState, - tablePageSizeState, -} from "./atoms"; - -const useStyles = makeStyles(() => ({ - root: { - marginTop: 0, - }, -})); - -function SkeletonRow() { - return ( - - - - {/* */} - - {/* */} - - - - - - - - - - - ); -} - -function PublicationsList({ hideSearch = false }) { - const classes = useStyles(); - const lits = useRecoilValue(litsIdsState); - const [loadingEntities, setLoadingEntities] = useRecoilState(loadingEntitiesState); - const count = useRecoilValue(litsCountState); - const cursor = useRecoilValue(litsCursorState); - const displayedPubs = useRecoilValue(displayedPublications); - const bibliographyState = useRecoilValue(literatureState); - const setLiteratureUpdate = useSetRecoilState(updateLiteratureState); - const page = useRecoilValue(tablePageState); - const pageSize = useRecoilValue(tablePageSizeState); - - // function to request 'ready' literatures ids - const syncLiteraturesState = useRecoilCallback(({ snapshot, set }) => async () => { - const AllLits = await snapshot.getPromise(litsIdsState); - const readyForRequest = AllLits.filter(x => x.status === "ready").map(x => x.id); - - if (readyForRequest.length === 0) return; - const queryResult = await snapshot.getPromise( - literaturesEuropePMCQuery({ - literaturesIds: readyForRequest, - }) - ); - - const parsedPublications = parsePublications(queryResult); - - const mapedResults = new Map(parsedPublications.map(key => [key.europePmcId, key])); - - const updatedPublications = AllLits.map(x => { - const publication = mapedResults.get(x.id); - if (x.status === "loaded") return x; - const status = publication ? "loaded" : "missing"; - return { ...x, status, publication }; - }); - set(litsIdsState, updatedPublications); - }); - - useEffect( - () => { - if (lits.length !== 0) syncLiteraturesState(); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [lits] - ); - - const handleRowsPerPageChange = useRecoilCallback(({ snapshot }) => async newPageSize => { - const pageSizeInt = Number(newPageSize); - const expected = pageSizeInt * page + pageSizeInt; - if (expected > lits.length && cursor !== null) { - const { - query, - id, - category, - selectedEntities, - cursor: newCursor, - globalEntity, - endYear, - endMonth, - startYear, - startMonth, - } = bibliographyState; - setLoadingEntities(true); - const request = await fetchSimilarEntities({ - query, - id, - category, - entities: selectedEntities, - cursor: newCursor, - page: 0, - endYear, - endMonth, - startYear, - startMonth, - }); - setLoadingEntities(false); - const data = request.data[globalEntity]; - const loadedPublications = await snapshot.getPromise(litsIdsState); - const newLits = data.literatureOcurrences?.rows?.map(({ pmid }) => ({ - id: pmid, - status: "ready", - publication: null, - })); - const update = { - litsIds: [...loadedPublications, ...newLits], - cursor: data.literatureOcurrences?.cursor, - page: 0, - pageSize: pageSizeInt, - }; - setLiteratureUpdate(update); - } else { - setLiteratureUpdate({ page: 0, pageSize: pageSizeInt }); - } - }); - - const handlePageChange = useRecoilCallback(({ snapshot }) => async newPage => { - const newPageInt = Number(newPage); - if (pageSize * newPageInt + pageSize > lits.length && cursor !== null) { - const { - query, - id, - category, - selectedEntities, - cursor: newCursor, - globalEntity, - endYear, - endMonth, - startYear, - startMonth, - } = bibliographyState; - setLoadingEntities(true); - const request = await fetchSimilarEntities({ - query, - id, - category, - entities: selectedEntities, - cursor: newCursor, - endYear, - endMonth, - startYear, - startMonth, - }); - setLoadingEntities(false); - const data = request.data[globalEntity]; - const loadedPublications = await snapshot.getPromise(litsIdsState); - const newLits = data.literatureOcurrences?.rows?.map(({ pmid }) => ({ - id: pmid, - status: "ready", - publication: null, - })); - const update = { - litsIds: [...loadedPublications, ...newLits], - cursor: data.literatureOcurrences?.cursor, - page: newPageInt, - }; - setLiteratureUpdate(update); - } else { - setLiteratureUpdate({ page: newPageInt }); - } - }); - - const columns = [ - { - id: "publications", - label: " ", - renderCell: ({ publication, status }) => { - if (status === "ready") return ; - if (status === "missing") return null; - return ( - - ); - }, - filterValue: ({ row: publication }) => - `${publication.journal.journal?.title} ${publication?.title} ${publication?.year} - ${publication.authors - .reduce((acc, author) => { - if (author.fullName) acc.push(author.fullName); - return acc; - }, []) - .join(" ")}`, - }, - ]; - - if (loadingEntities) - return ; - - return ( - - ); -} - -export default PublicationsList; diff --git a/packages/sections/src/common/Literature/PublicationsList.tsx b/packages/sections/src/common/Literature/PublicationsList.tsx new file mode 100644 index 000000000..a0eb9fb90 --- /dev/null +++ b/packages/sections/src/common/Literature/PublicationsList.tsx @@ -0,0 +1,254 @@ +import { useEffect } from "react"; +import { Box, Grid, Fade, Skeleton } from "@mui/material"; +import { makeStyles } from "@mui/styles"; +import { PublicationWrapper, Table } from "ui"; +import Loader from "./Loader"; +import type { PublicationType } from "./types"; +import { + useDisplayedPublications, + useLiterature, + useLiteratureDispatch, + useDetails, + useDetailsDispatch, +} from "./LiteratureContext"; +import { fetchSimilarEntities, literaturesEuropePMCQuery } from "./requests"; +import { DetailsStateType } from "./types"; + +const useStyles = makeStyles(() => ({ + root: { + marginTop: 0, + }, +})); + + + +function parsePublications(publications: PublicationType[]): DetailsStateType { + const obj: DetailsStateType = {}; + for (const pub of publications) { + obj[pub.id] = { + source: pub.source, + patentDetails: pub.patentDetails, + europePmcId: pub.id, + fullTextOpen: !!(pub.inEPMC === "Y" || pub.inPMC === "Y"), + title: pub.title, + year: pub.pubYear, + abstract: pub.abstractText, + openAccess: pub.isOpenAccess !== "N", + authors: pub.authorList?.author || [], + journal: { + ...pub.journalInfo, + page: pub.pageInfo, + }, + }; + } + return obj; +} + +function SkeletonRow() { + return ( + + + + {/* */} + + {/* */} + + + + + + + + + + + ); +} + +function PublicationsList({ hideSearch = false }) { + + const classes = useStyles(); + const literature = useLiterature(); + const { + loadingEntities, + litsCount: count, + cursor, + page, + pageSize, + litsIds, + } = literature; + const details = useDetails(); + const displayedPubs = useDisplayedPublications(); + const literatureDispatch = useLiteratureDispatch(); + const detailsDispatch = useDetailsDispatch(); + + // get publications details from Europe PMC + useEffect(() => { + const fetchFunction = async() => { + const missingDetails = litsIds.filter((id: string) => !details[id]); + if (missingDetails.length === 0) return; + detailsDispatch({ + type: 'setToLoading', + value: missingDetails, + }) + const queryResult = await literaturesEuropePMCQuery({ + literaturesIds: missingDetails + }); + detailsDispatch({ + type: 'addDetails', + value: parsePublications(queryResult), + }); + }; + fetchFunction().catch(console.error); + }, [literature]); + + const handleRowsPerPageChange = async (newPageSize: string) => { + const pageSizeInt = Number(newPageSize); + const expected = pageSizeInt * page + pageSizeInt; + if (expected > litsIds.length && cursor !== null) { + const { + query, + id, + category, + selectedEntities, + cursor: newCursor, + globalEntity, + endYear, + endMonth, + startYear, + startMonth, + } = literature; + literatureDispatch({ type: 'loadingEntities', value: true }); + const request = await fetchSimilarEntities({ + query, + id, + category, + entities: selectedEntities, + cursor: newCursor, + endYear, + endMonth, + startYear, + startMonth, + }); + literatureDispatch({ type: 'loadingEntities', value: false }); + const data = request.data[globalEntity]; + const newLitsIds = + data.literatureOcurrences?.rows?.map(({ pmid }) => pmid) ?? []; + const update = { + litsIds: [...litsIds, ...newLitsIds], + cursor: data.literatureOcurrences?.cursor, + page: 0, + pageSize: pageSizeInt, + }; + literatureDispatch({ type: 'stateUpdate', value: update }); + } else { + literatureDispatch({ + type: 'stateUpdate', + value: { page: 0, pageSize: pageSizeInt } + }); + } + }; + + const handlePageChange = async (newPage: string) => { + const newPageInt = Number(newPage); + if (pageSize * newPageInt + pageSize > litsIds.length && cursor !== null) { + const { + query, + id, + category, + selectedEntities, + cursor: newCursor, + globalEntity, + endYear, + endMonth, + startYear, + startMonth, + } = literature; + literatureDispatch({ type: 'loadingEntities', value: true }); + const request = await fetchSimilarEntities({ + query, + id, + category, + entities: selectedEntities, + cursor: newCursor, + endYear, + endMonth, + startYear, + startMonth, + }); + literatureDispatch({ type: 'loadingEntities', value: false }); + const data = request.data[globalEntity]; + const newLitsIds = + data.literatureOcurrences?.rows?.map(({ pmid }) => pmid) ?? []; + const update = { + litsIds: [...litsIds, ...newLitsIds], + cursor: data.literatureOcurrences?.cursor, + page: newPageInt, + }; + literatureDispatch({ type: 'stateUpdate', value: update }); + } else { + literatureDispatch({ + type: 'stateUpdate', + value: ({ page: newPageInt }) + }); + } + }; + + const columns = [ + { + id: "publications", + label: " ", + renderCell(id) { + const det = details[id]; + if (det === 'loading') { + return ; + } else if (!det) { + return null; + } else { + return ( + + ) + } + }, + filterValue: ({ row: publication }) => + `${publication.journal.journal?.title} ${publication?.title} ${publication?.year} + ${publication.authors + .reduce((acc, author) => { + if (author.fullName) acc.push(author.fullName); + return acc; + }, []) + .join(" ")}`, + }, + ]; + + if (loadingEntities) + return ; + + return ( +
+ ); +} + +export default PublicationsList; \ No newline at end of file diff --git a/packages/sections/src/common/Literature/atoms.ts b/packages/sections/src/common/Literature/atoms.ts deleted file mode 100644 index 184d64acd..000000000 --- a/packages/sections/src/common/Literature/atoms.ts +++ /dev/null @@ -1,338 +0,0 @@ -import isEmpty from "lodash/isEmpty"; -import { atom, selectorFamily, selector, DefaultValue } from "recoil"; -import { getPage } from "ui"; -import client from "../../client"; -import { europePmcBiblioSearchPOSTQuery } from "../../utils/urls"; - -// ------------------------------------------ -// Helpers -// ------------------------------------------ - -type AuthorListType = { - fullName: string; - firstName: string; - lastName: string; - initials: string; - authorId: { - type: string; - value: string; - }; - authorAffiliationDetailsList: { - authorAffiliation: { - affiliation: string; - }[]; - }; -}; - -type JournalInfoType = { - issue: string; - volume: string; - journalIssueId: number; - dateOfPublication: string; - monthOfPublication: number; - yearOfPublication: number; - printPublicationDate: string; - journal: { - title: string; - medlineAbbreviation: string; - isoabbreviation: string; - nlmid: string; - issn: string; - essn: string; - }; -}; - -type PublicationType = { - source: string; - patentDetails: any; - id: string; - inEPMC: string; - inPMC: string; - title: string; - pubYear: string; - abstractText: string; - isOpenAccess: string; - authorList: { - author: AuthorListType; - }; - journalInfo: JournalInfoType; - pageInfo: string; -}; - -type RowType = { - source: string; - patentDetails: any; - europePmcId: string; - fullTextOpen: boolean; - title: string; - year: string; - abstract: string; - openAccess: boolean; - authors: AuthorListType; - journal: JournalInfoType & { page: string }; -}; - -export const parsePublications = (publications: PublicationType[]) => - publications.map(pub => { - const row: RowType = { - source: pub.source, - patentDetails: pub.patentDetails, - europePmcId: pub.id, - fullTextOpen: !!(pub.inEPMC === "Y" || pub.inPMC === "Y"), - title: pub.title, - year: pub.pubYear, - abstract: pub.abstractText, - openAccess: pub.isOpenAccess !== "N", - authors: pub.authorList?.author || [], - journal: { - ...pub.journalInfo, - page: pub.pageInfo, - }, - }; - return row; - }); - -type LiteratureStateType = { - id: string; - cursor: string | null; - threshold?: number; - size?: number; - category: string[]; - query: any | null; - globalEntity: any | null; - entities: any[]; - selectedEntities: any[] | DefaultValue; - startYear: number | null; - startMonth: number | null; - endYear: number | null; - endMonth: number | null; - earliestPubYear: number; - litsIds: - | { - id: string; - status: string; - publication: null; - }[] - | DefaultValue; - page: number; - pageSize: number | DefaultValue; - litsCount: number; - loadingEntities: boolean | DefaultValue; -}; - -// ------------------------------------------ -// ATOMS -// ------------------------------------------ - -const defaultState: LiteratureStateType = { - id: "", - cursor: "", - category: ["disease", "drug", "target"], - query: null, - globalEntity: null, - entities: [], - selectedEntities: [], - startYear: null, - startMonth: null, - endYear: null, - endMonth: null, - earliestPubYear: 0, - litsIds: [], - page: 0, - pageSize: 5, - litsCount: 0, - loadingEntities: false, -}; - -export const literatureState = atom({ - key: "literatureState", - default: defaultState, -}); - -// ------------------------------------------ -// SELECTORS -// ------------------------------------------ -export const loadingEntitiesState = selector({ - key: "loadingEntitiesState", - get: ({ get }) => { - const { loadingEntities } = get(literatureState); - return loadingEntities; - }, - set: ({ set, get }, newStatus) => { - const currentState = get(literatureState); - const newState: LiteratureStateType = { - ...currentState, - loadingEntities: newStatus, - }; - return set(literatureState, newState); - }, -}); - -export const selectedCategoriesState = selector({ - key: "selectedCategoriesState", - get: ({ get }) => { - const { category } = get(literatureState); - const sortedCategories = [...category].sort(); - return sortedCategories; - }, -}); - -export const litsCursorState = selector({ - key: "litsCursorState", - get: ({ get }) => { - const { cursor } = get(literatureState); - return cursor; - }, -}); - -export const tablePageState = selector({ - key: "tablePageState", - get: ({ get }) => { - const { page } = get(literatureState); - return page; - }, -}); - -export const tablePageSizeState = selector({ - key: "tablePageSizeState", - get: ({ get }) => { - const { pageSize } = get(literatureState); - return pageSize; - }, - set: ({ set, get }, newPageSize) => { - const currentState = get(literatureState); - const newState: LiteratureStateType = { - ...currentState, - pageSize: newPageSize, - }; - return set(literatureState, newState); - }, -}); - -export const litsCountState = selector({ - key: "litsCountState", - get: ({ get }) => { - const { litsCount } = get(literatureState); - return litsCount; - }, -}); - -export const litsIdsState = selector({ - key: "litsIdsState", - get: ({ get }) => { - const { litsIds } = get(literatureState); - return litsIds; - }, - set: ({ set, get }, newValue) => { - const currentState = get(literatureState); - const newState: LiteratureStateType = { - ...currentState, - litsIds: newValue, - }; - return set(literatureState, newState); - }, -}); - -export const displayedPublications = selector({ - key: "displayedPublications", - get: ({ get }) => { - const page = get(tablePageState); - const pageSize = get(tablePageSizeState); - const publications = get(litsIdsState); - if (isEmpty(publications)) return []; - const rows = getPage(publications, page, pageSize); - return rows; - }, -}); - -export const entitiesState = selector({ - key: "entitiesState", - get: ({ get }) => { - const { entities } = get(literatureState); - return entities; - }, -}); - -export const selectedEntitiesState = selector({ - key: "selectedEntitiesState", - get: ({ get }) => { - const { selectedEntities } = get(literatureState); - return selectedEntities; - }, - set: ({ set, get }, selectedEntities) => { - const currentState = get(literatureState); - const newState: LiteratureStateType = { ...currentState, selectedEntities }; - return set(literatureState, newState); - }, -}); - -export const updateLiteratureState = selector({ - key: "updateLiteratureState", - get: ({ get }) => get(literatureState), - set: ({ set, get }, stateUpdate) => { - const currentState = get(literatureState); - return set(literatureState, { ...currentState, ...stateUpdate }); - }, -}); - -// ------------------------------------------ -// Requests -// ------------------------------------------ - -const fetchLiteraturesFromPMC = async ({ - baseUrl, - requestOptions, -}: { - baseUrl: string; - requestOptions: any; -}) => fetch(baseUrl, requestOptions).then(response => response.json()); - -export const literaturesEuropePMCQuery = selectorFamily({ - key: "literaturesEuropePMCQuery", - get: - ({ literaturesIds }: { literaturesIds: string[] }) => - async () => { - if (literaturesIds.length === 0) return []; - const { baseUrl, requestOptions } = europePmcBiblioSearchPOSTQuery(literaturesIds); - const response = await fetchLiteraturesFromPMC({ - baseUrl, - requestOptions, - }); - if (response.error) { - throw response.error; - } - return response.resultList?.result; - }, -}); - -export const fetchSimilarEntities = ({ - id = "", - threshold = 0.5, - size = 15, - query, - cursor = null, - category = [], - entities = [], - startYear = null, - startMonth = null, - endYear = null, - endMonth = null, -}: LiteratureStateType) => { - const entityNames = category.length === 0 ? null : category; - const ids = entities.map(c => c.object.id); - return client.query({ - query, - variables: { - cursor, - id, - ids, - startYear, - startMonth, - endYear, - endMonth, - threshold, - size, - entityNames, - }, - }); -}; diff --git a/packages/sections/src/common/Literature/requests.ts b/packages/sections/src/common/Literature/requests.ts new file mode 100644 index 000000000..3d38ffa50 --- /dev/null +++ b/packages/sections/src/common/Literature/requests.ts @@ -0,0 +1,50 @@ +import client from "../../client"; +import { europePmcBiblioSearchPOSTQuery } from "../../utils/urls"; + +export async function literaturesEuropePMCQuery({ literaturesIds }) { + if (literaturesIds.length === 0) return []; + const { baseUrl, requestOptions } = + europePmcBiblioSearchPOSTQuery(literaturesIds); + const response = + await fetch(baseUrl, requestOptions).then(response => response.json()); + if (response.error) throw response.error; + return response.resultList?.result; +}; + +export const fetchSimilarEntities = ({ + id = "", + threshold = 0.5, + size = 15, + query, + cursor = null, + category = [], + entities = [], + startYear = null, + startMonth = null, + endYear = null, + endMonth = null, +// DO NOT GIVE PARAM A TYPE FOR NOW SINCE TS COMPLAINS IF ANY MISSING PROPS - THE +// TYPE DEFN SAYS THEY CAN BE NULL BUT THIS IS NOT THE SAME AS MISSING (UNDEFINED) +// }: LiteratureStateType) => { +}) => { + const entityNames = category.length === 0 ? null : category; + // USE SQUARE BRACKETS TO STOP TYPESCRIPT COMPLAINING: + // https://stackoverflow.com/questions/44147937/property-does-not-exist-on-type-never + // const ids = entities.map(c => c.object.id); + const ids = entities.map(c => c['object']['id']); + return client.query({ + query, + variables: { + cursor, + id, + ids, + startYear, + startMonth, + endYear, + endMonth, + threshold, + size, + entityNames, + }, + }); +}; \ No newline at end of file diff --git a/packages/sections/src/common/Literature/types.ts b/packages/sections/src/common/Literature/types.ts new file mode 100644 index 000000000..b956fd80d --- /dev/null +++ b/packages/sections/src/common/Literature/types.ts @@ -0,0 +1,93 @@ +// !! NOTES !! +// - FOR NOW HAVE REPLACED RECOIL'S DefaultValue WITH null + + +type AuthorListType = { + fullName: string; + firstName: string; + lastName: string; + initials: string; + authorId: { + type: string; + value: string; + }; + authorAffiliationDetailsList: { + authorAffiliation: { + affiliation: string; + }[]; + }; + }; + + type JournalInfoType = { + issue: string; + volume: string; + journalIssueId: number; + dateOfPublication: string; + monthOfPublication: number; + yearOfPublication: number; + printPublicationDate: string; + journal: { + title: string; + medlineAbbreviation: string; + isoabbreviation: string; + nlmid: string; + issn: string; + essn: string; + }; + }; + + export type PublicationType = { + source: string; + patentDetails: any; + id: string; + inEPMC: string; + inPMC: string; + title: string; + pubYear: string; + abstractText: string; + isOpenAccess: string; + authorList: { + author: AuthorListType; + }; + journalInfo: JournalInfoType; + pageInfo: string; + }; + + export type RowType = { + source: string; + patentDetails: any; + europePmcId: string; + fullTextOpen: boolean; + title: string; + year: string; + abstract: string; + openAccess: boolean; + authors: AuthorListType; + journal: JournalInfoType & { page: string }; + }; + + export type LiteratureStateType = { + id: string; + cursor: string | null; + threshold?: number; + size?: number; + category: string[]; + query: any | null; + globalEntity: any | null; + entities: any[]; + selectedEntities: any[] | null; // DefaultValue; + startYear: number | null; + startMonth: number | null; + endYear: number | null; + endMonth: number | null; + earliestPubYear: number; + litsIds: string[]; + page: number; + pageSize: number | null; // DefaultValue; + litsCount: number; + loadingEntities: boolean | null; // DefaultValue; + }; + + export type DetailsStateType = { + [index: string]: undefined | 'loading' | RowType; + }; \ No newline at end of file