diff --git a/CHANGELOG b/CHANGELOG index aa7fc4838fe902dfe321a5cb6f4be0ed0ed9b535..6edee4e15a79bb1a874f04def49b292e7a9b0c14 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,8 +3,18 @@ minerva-front (19.0.0~alpha.0) stable; urgency=medium -- Piotr Gawron <piotr.gawron@uni.lu> Fri, 18 Oct 2024 13:00:00 +0200 +minerva-front (18.0.2) stable; urgency=medium + * Bug fix: Terms of Service modal is not hidden by Select project modal when + login via ORCID (#305) + * Bug fix: when downloading map there was missing spinner indicating the + download is in progress (#297) + + -- Piotr Gawron <piotr.gawron@uni.lu> Wed, 30 Oct 2024 13:00:00 +0200 + minerva-front (18.0.1) stable; urgency=medium * Bug fix: show cookie baner only when cookie baner link is provided (#304) + * Bug fix: when link to submap is provided add submap name (#303) + * Bug fix: some old maps could not be opened (#311) -- Piotr Gawron <piotr.gawron@uni.lu> Thu, 24 Oct 2024 13:00:00 +0200 diff --git a/src/components/FunctionalArea/MapNavigation/MapNavigation.component.test.tsx b/src/components/FunctionalArea/MapNavigation/MapNavigation.component.test.tsx index 4fae37530de53dd219cd0d5118bbe12aded2d3ae..2af7f3fb90190b2ffa0dc1f822138ceab27c2396 100644 --- a/src/components/FunctionalArea/MapNavigation/MapNavigation.component.test.tsx +++ b/src/components/FunctionalArea/MapNavigation/MapNavigation.component.test.tsx @@ -9,11 +9,9 @@ import { getReduxWrapperWithStore, } from '@/utils/testing/getReduxWrapperWithStore'; import { act, render, screen, within } from '@testing-library/react'; +import { HISTAMINE_MAP_ID, MAIN_MAP_ID } from '@/constants/mocks'; import { MapNavigation } from './MapNavigation.component'; -const MAIN_MAP_ID = 5053; -const HISTAMINE_MAP_ID = 5052; - const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState); diff --git a/src/components/FunctionalArea/Modal/ToSModal/ToSModal.component.tsx b/src/components/FunctionalArea/Modal/ToSModal/ToSModal.component.tsx index 7d3d4e766253a5b8f061f9dc0758f65f4e10df2e..789e9fa033716df99f2660e2df9dc8db9e62d59a 100644 --- a/src/components/FunctionalArea/Modal/ToSModal/ToSModal.component.tsx +++ b/src/components/FunctionalArea/Modal/ToSModal/ToSModal.component.tsx @@ -3,7 +3,7 @@ import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { Button } from '@/shared/Button'; import { getSessionValid, logout, updateUser } from '@/redux/user/user.thunks'; -import { closeModal } from '@/redux/modal/modal.slice'; +import { closeModal, openSelectProjectModal } from '@/redux/modal/modal.slice'; import { userSelector } from '@/redux/user/user.selectors'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { termsOfServiceValSelector } from '@/redux/configuration/configuration.selectors'; @@ -15,13 +15,14 @@ export const ToSModal: React.FC = () => { const termsOfService = useAppSelector(termsOfServiceValSelector); const updateUserTosHandler = async (): Promise<void> => { - // eslint-disable-next-line no-console - console.log('update'); if (userData) { const user = { ...userData, termsOfUseConsent: true }; await dispatch(updateUser(user)); await dispatch(getSessionValid()); dispatch(closeModal()); + if (userData.orcidId && userData.orcidId !== '') { + dispatch(openSelectProjectModal()); + } } }; diff --git a/src/components/Map/Drawer/BioEntityDrawer/AssociatedSubmap/AssociatedSubmap.component.tsx b/src/components/Map/Drawer/BioEntityDrawer/AssociatedSubmap/AssociatedSubmap.component.tsx index c14fbdd9a32d7b94b30633de79fcc71c688d7fb4..659ff1931abd873664b21b77246ef41e1eba86d2 100644 --- a/src/components/Map/Drawer/BioEntityDrawer/AssociatedSubmap/AssociatedSubmap.component.tsx +++ b/src/components/Map/Drawer/BioEntityDrawer/AssociatedSubmap/AssociatedSubmap.component.tsx @@ -19,7 +19,7 @@ export const AssociatedSubmap = (): React.ReactNode => { data-testid="associated-submap" className="flex flex-row flex-nowrap items-center justify-between" > - <p>Associated Submap: </p> + <p>Associated Submap: {relatedSubmap.name}</p> <Button className="max-h-8" variantStyles="ghost" onClick={openSubmap}> Open submap </Button> diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.test.tsx index 33686dced99968f69a96162f98bc1546b749b0b4..64d82fabf6a64324b2f08d52f52c4166541434d6 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.test.tsx @@ -11,6 +11,7 @@ import { } from '@/utils/testing/getReduxWrapperWithStore'; import { render, screen } from '@testing-library/react'; import { MockStoreEnhanced } from 'redux-mock-store'; +import { HISTAMINE_MAP_ID, MAIN_MAP_ID, PRKN_SUBSTRATES_MAP_ID } from '@/constants/mocks'; import { BioEntitiesAccordion } from './BioEntitiesAccordion.component'; const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { @@ -95,10 +96,22 @@ describe('BioEntitiesAccordion - component', () => { }, }); - expect(screen.getByText('Content (10)')).toBeInTheDocument(); - expect(screen.getByText('Core PD map (6)')).toBeInTheDocument(); - expect(screen.getByText('Histamine signaling (2)')).toBeInTheDocument(); - expect(screen.getByText('PRKN substrates (2)')).toBeInTheDocument(); + const countHistamine = bioEntitiesContentFixture.filter( + content => content.bioEntity.model === HISTAMINE_MAP_ID, + ).length; + const countCore = bioEntitiesContentFixture.filter( + content => content.bioEntity.model === MAIN_MAP_ID, + ).length; + const countPrkn = bioEntitiesContentFixture.filter( + content => content.bioEntity.model === PRKN_SUBSTRATES_MAP_ID, + ).length; + + const countAll = bioEntitiesContentFixture.length; + + expect(screen.getByText(`Content (${countAll})`)).toBeInTheDocument(); + expect(screen.getByText(`Core PD map (${countCore})`)).toBeInTheDocument(); + expect(screen.getByText(`Histamine signaling (${countHistamine})`)).toBeInTheDocument(); + expect(screen.getByText(`PRKN substrates (${countPrkn})`)).toBeInTheDocument(); }); it('should fire toggleIsContentTabOpened on accordion item button click', () => { diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.test.tsx index bcb63ba44430a300465929d2bc330f3c2d242848..b54bea432a06a379ce412848858e9715321eee26 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.test.tsx @@ -62,6 +62,11 @@ const renderComponent = ( }; describe('PinsListItem - component ', () => { + drugsFixture[0].targets[0].targetParticipants[0].link = 'https://example.com/plugin.js'; + drugsFixture[0].targets[0].targetParticipants[1].link = 'https://example.com/plugin.js'; + chemicalsFixture[0].targets[0].targetParticipants[0].link = 'https://example.com/plugin.js'; + chemicalsFixture[0].targets[0].targetParticipants[1].link = 'https://example.com/plugin.js'; + it('should display full name of pin', () => { renderComponent(DRUGS_PIN.name, DRUGS_PIN.pin, 'drugs', BIO_ENTITY, INITIAL_STORE_STATE); diff --git a/src/components/Map/Drawer/SubmapsDrawer/SubmapItem/DownloadSubmap/DownloadSubmap.component.tsx b/src/components/Map/Drawer/SubmapsDrawer/SubmapItem/DownloadSubmap/DownloadSubmap.component.tsx index 01587d62f8bbd459f9aff42cd0f7502c1051ec8a..316528128b6a0dec97b0fd16761961f47b2ed60e 100644 --- a/src/components/Map/Drawer/SubmapsDrawer/SubmapItem/DownloadSubmap/DownloadSubmap.component.tsx +++ b/src/components/Map/Drawer/SubmapsDrawer/SubmapItem/DownloadSubmap/DownloadSubmap.component.tsx @@ -2,18 +2,34 @@ import { formatsHandlersSelector } from '@/redux/configuration/configuration.sel import { Button } from '@/shared/Button'; import { useSelect } from 'downshift'; import { useSelector } from 'react-redux'; +import Image from 'next/image'; +import spinnerIcon from '@/assets/vectors/icons/spinner.svg'; +import { useState } from 'react'; +import { downloadFileFromUrl } from '@/redux/export/export.utils'; import { SUBMAP_DOWNLOAD_HANDLERS_NAMES } from './DownloadSubmap.constants'; import { useGetSubmapDownloadUrl } from './utils/useGetSubmapDownloadUrl'; -export const DownloadSubmap = (): JSX.Element => { +export const DownloadSubmap = (): React.ReactNode => { const formatsHandlers = useSelector(formatsHandlersSelector); const formatsHandlersItems = Object.entries(formatsHandlers); const getSubmapDownloadUrl = useGetSubmapDownloadUrl(); - const { isOpen, getToggleButtonProps, getMenuProps } = useSelect({ + const [isDownloading, setIsDownloading] = useState<boolean>(false); + + const { isOpen, getToggleButtonProps, getMenuProps, closeMenu } = useSelect({ items: formatsHandlersItems, }); + const downloadSubmap = (handler: string) => { + return function () { + closeMenu(); + setIsDownloading(true); + downloadFileFromUrl(getSubmapDownloadUrl({ handler })).finally(function () { + setIsDownloading(false); + }); + }; + }; + return ( <div className="relative"> <Button @@ -22,6 +38,15 @@ export const DownloadSubmap = (): JSX.Element => { className="mr-4" {...getToggleButtonProps()} > + {isDownloading && ( + <Image + src={spinnerIcon} + alt="spinner icon" + height={12} + width={12} + className="mr-5 animate-spin" + /> + )} Download </Button> <ul @@ -34,14 +59,13 @@ export const DownloadSubmap = (): JSX.Element => { {isOpen && formatsHandlersItems.map(([formatId, handler]) => ( <li key={formatId}> - <a - className="flex flex-col border-t px-4 py-2 shadow-sm" - href={getSubmapDownloadUrl({ handler })} - target="_blank" - download + <Button + variantStyles="ghost" + className="flex w-full flex-col border-t px-4 py-2 shadow-sm" + onClick={downloadSubmap(handler)} > - <span>{SUBMAP_DOWNLOAD_HANDLERS_NAMES[formatId]}</span> - </a> + {SUBMAP_DOWNLOAD_HANDLERS_NAMES[formatId]} + </Button> </li> ))} </ul> diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.test.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.test.ts index e00535c5e895bb9b34d0563fb0132387abe31dcb..aae76f614efd91b111eb16846f5a3ef92ae9f222 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.test.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.test.ts @@ -209,8 +209,6 @@ describe('handleAliasResults - util', () => { 'entityNumber/addNumbersToEntityNumberData', 'project/getBioEntityById/fulfilled', 'entityNumber/addNumbersToEntityNumberData', - 'reactions/getByIds/pending', - 'reactions/getByIds/fulfilled', 'project/getMultiBioEntity/fulfilled', 'drawer/selectTab', 'drawer/openBioEntityDrawerById', diff --git a/src/constants/mocks.ts b/src/constants/mocks.ts index d7156f1ea3552ebde078a9d4dd4a84070f50f26e..11af1d0a150d3e7c2adcf79f5aff2f6359e3a9a6 100644 --- a/src/constants/mocks.ts +++ b/src/constants/mocks.ts @@ -1,2 +1,4 @@ -// eslint-disable-next-line no-magic-numbers -export const MODEL_IDS_MOCK = [5052, 5053, 5054]; +export const MAIN_MAP_ID = 5053; +export const HISTAMINE_MAP_ID = 5052; +export const PRKN_SUBSTRATES_MAP_ID = 5054; +export const MODEL_IDS_MOCK = [HISTAMINE_MAP_ID, MAIN_MAP_ID, PRKN_SUBSTRATES_MAP_ID]; diff --git a/src/models/bioEntitySchema.ts b/src/models/bioEntitySchema.ts index ee025b2c5d30a12083dff6af22f8f1d13733e729..83f7f0c7eab111cbeea3ee399eeb56d8ff87035e 100644 --- a/src/models/bioEntitySchema.ts +++ b/src/models/bioEntitySchema.ts @@ -34,7 +34,7 @@ export const bioEntitySchema = z.object({ .number() .optional() .transform(height => height ?? ZERO), - visibilityLevel: z.string(), + visibilityLevel: z.string().nullable(), transparencyLevel: z.string().nullable().optional(), synonyms: z.array(z.string()), formerSymbols: z.array(z.string()).nullable().optional(), @@ -46,13 +46,13 @@ export const bioEntitySchema = z.object({ activity: z.boolean().optional(), structuralState: z.optional(structuralStateSchema.nullable()), hypothetical: z.boolean().nullable().optional(), - boundaryCondition: z.boolean().optional(), - constant: z.boolean().optional(), + boundaryCondition: z.boolean().optional().nullable(), + constant: z.boolean().optional().nullable(), initialAmount: z.number().nullable().optional(), initialConcentration: z.number().nullable().optional(), charge: z.number().nullable().optional(), substanceUnits: z.string().nullable().optional(), - onlySubstanceUnits: z.boolean().optional(), + onlySubstanceUnits: z.boolean().optional().nullable(), modificationResidues: z.optional(z.array(modificationResiduesSchema)), complex: z.number().nullable().optional(), compartment: z.number().nullable().optional(), diff --git a/src/models/compartmentPathwaySchema.ts b/src/models/compartmentPathwaySchema.ts index 368ff17fc1f0fc77251f2bec1b917ec1f44c2858..1259fa8708e3d6514b69e113b52c97cd5c118a21 100644 --- a/src/models/compartmentPathwaySchema.ts +++ b/src/models/compartmentPathwaySchema.ts @@ -32,7 +32,7 @@ export const compartmentPathwayDetailsSchema = z.object({ formula: z.null(), fullName: z.string().nullable(), glyph: z.any(), - hierarchyVisibilityLevel: z.string(), + hierarchyVisibilityLevel: z.string().nullable(), homomultimer: z.null(), hypothetical: z.null(), id: z.number().gt(-1), diff --git a/src/models/mapBackground.ts b/src/models/mapBackground.ts index a8a9605280785e7071cd94256022177803ba9808..fe6c6e9e1319bad70bd7f627192caea855fd1b6c 100644 --- a/src/models/mapBackground.ts +++ b/src/models/mapBackground.ts @@ -8,7 +8,7 @@ export const mapBackground = z.object({ creator: z.object({ login: z.string() }), status: z.string(), progress: z.number(), - description: z.null(), + description: z.string().nullable(), order: z.number(), images: z.array( z.object({ diff --git a/src/models/overlayLeftBioEntitySchema.ts b/src/models/overlayLeftBioEntitySchema.ts index bd0431d48ebe6109f44d23fc08c8879bdf6d14b0..69daea535bd37e94817339700b6830a390f628aa 100644 --- a/src/models/overlayLeftBioEntitySchema.ts +++ b/src/models/overlayLeftBioEntitySchema.ts @@ -18,7 +18,7 @@ export const overlayLeftBioEntitySchema = z.object({ fontColor: colorSchema.optional(), fillColor: colorSchema.optional(), borderColor: colorSchema, - visibilityLevel: z.string(), + visibilityLevel: z.string().nullable(), transparencyLevel: z.string(), notes: z.string(), symbol: z.string().nullable(), @@ -40,10 +40,10 @@ export const overlayLeftBioEntitySchema = z.object({ initialAmount: z.unknown().nullable(), charge: z.unknown(), initialConcentration: z.number().nullable().optional(), - onlySubstanceUnits: z.unknown(), + onlySubstanceUnits: z.boolean().nullable().optional(), homodimer: z.number().optional(), hypothetical: z.unknown(), - boundaryCondition: z.boolean().optional(), + boundaryCondition: z.boolean().optional().nullable(), constant: z.boolean().nullable().optional(), modificationResidues: z.unknown(), substanceUnits: z.boolean().nullable().optional(), diff --git a/src/models/overlayLeftReactionSchema.ts b/src/models/overlayLeftReactionSchema.ts index b7663c41b56d7c54cc93982db5fe009c55960149..c5d887af14d79c296a733c7233402a27163b41e3 100644 --- a/src/models/overlayLeftReactionSchema.ts +++ b/src/models/overlayLeftReactionSchema.ts @@ -17,7 +17,7 @@ export const overlayLeftReactionSchema = z.object({ upperBound: z.null(), subsystem: z.null(), geneProteinReaction: z.null(), - visibilityLevel: z.string(), + visibilityLevel: z.string().nullable(), z: z.number(), synonyms: z.array(z.unknown()), model: z.number(), diff --git a/src/models/reaction.ts b/src/models/reaction.ts index 51c7c51b014b35f76668730099639a929fb251cc..18a438f72386e9f790394dca2cf37114a9d24c29 100644 --- a/src/models/reaction.ts +++ b/src/models/reaction.ts @@ -6,7 +6,7 @@ import { referenceSchema } from './referenceSchema'; export const reactionSchema = z.object({ centerPoint: positionSchema, - hierarchyVisibilityLevel: z.string(), + hierarchyVisibilityLevel: z.string().nullable(), id: z.number(), kineticLaw: z.null(), lines: z.array(reactionLineSchema), diff --git a/src/redux/export/export.utils.ts b/src/redux/export/export.utils.ts index 60cba1fd90f1fe16a31e44a266f7bc8dd41206a6..921c381286d677a9ee075eaa0b8afd91cb4f6ccb 100644 --- a/src/redux/export/export.utils.ts +++ b/src/redux/export/export.utils.ts @@ -1,3 +1,5 @@ +import axios from 'axios'; + export const downloadFileFromBlob = (data: string, filename: string): void => { const url = window.URL.createObjectURL(new Blob([data])); const link = document.createElement('a'); @@ -7,3 +9,27 @@ export const downloadFileFromBlob = (data: string, filename: string): void => { link.click(); link.remove(); }; + +export const downloadFileFromUrl = async (url: string): Promise<void> => { + const genericAxios = axios.create(); + + const response = await genericAxios.get(url, { + withCredentials: true, + responseType: 'arraybuffer', + }); + + const content = Buffer.from(response.data, 'binary'); + + let filename = 'file.xml'; + if (response.headers && response.headers['content-type'] === 'application/zip') { + filename = 'file.zip'; + } + + const tmpUrl = window.URL.createObjectURL(new Blob([content], { type: 'application/zip' })); + const link = document.createElement('a'); + link.href = tmpUrl; + link.setAttribute('download', filename); + document.body.appendChild(link); + link.click(); + link.remove(); +}; diff --git a/src/redux/modal/modal.reducers.ts b/src/redux/modal/modal.reducers.ts index 236f7f809f2b3d01bca4d20b095ad25917dba6a8..a3704696a08a6a8531a19b5e0d23cecc1ea095ae 100644 --- a/src/redux/modal/modal.reducers.ts +++ b/src/redux/modal/modal.reducers.ts @@ -3,6 +3,13 @@ import { PayloadAction } from '@reduxjs/toolkit'; import { ErrorData } from '@/utils/error-report/ErrorData'; import { ModalState, OpenEditOverlayModalAction } from './modal.types'; +const getOpenedModel = (state: ModalState): ModalName | null => { + if (state.isOpen) { + return state.modalName; + } + return null; +}; + export const openModalReducer = (state: ModalState, action: PayloadAction<ModalName>): void => { state.isOpen = true; state.modalName = action.payload; @@ -74,9 +81,11 @@ export const openAccessDeniedModalReducer = (state: ModalState): void => { }; export const openSelectProjectModalReducer = (state: ModalState): void => { - state.isOpen = true; - state.modalName = 'select-project'; - state.modalTitle = 'Select project!'; + if (getOpenedModel(state) !== 'terms-of-service') { + state.isOpen = true; + state.modalName = 'select-project'; + state.modalTitle = 'Select project!'; + } }; export const setOverviewImageIdReducer = ( diff --git a/src/redux/user/user.thunks.ts b/src/redux/user/user.thunks.ts index 9b016dd81526e58f529f591b18c39c7d9f451bf0..89e404dadd301142b1cbbe4668fb59db73c434d9 100644 --- a/src/redux/user/user.thunks.ts +++ b/src/redux/user/user.thunks.ts @@ -138,7 +138,7 @@ export const updateUser = createAsyncThunk<undefined, User, ThunkConfig>( }, ); - validateDataUsingZodSchema(newUser, userSchema); + validateDataUsingZodSchema(newUser.data, userSchema); showToast({ type: 'success', message: 'ToS agreement registered' }); } catch (error) { diff --git a/src/utils/search/getElementsByCoordinates.ts b/src/utils/search/getElementsByCoordinates.ts index 70e392de47fcf2be6aabd9ee800069b60c682a58..fa54f1de0978145ce1d676a4663bca699216e570 100644 --- a/src/utils/search/getElementsByCoordinates.ts +++ b/src/utils/search/getElementsByCoordinates.ts @@ -25,7 +25,10 @@ export const getFirstVisibleParent = async ({ apiPath.getElementById(parentId, bioEntity.model), ); const parent = parentResponse.data; - if (parseInt(parent.visibilityLevel, 10) > Math.ceil(considerZoomLevel)) { + if ( + parent.visibilityLevel !== null && + parseInt(parent.visibilityLevel, 10) > Math.ceil(considerZoomLevel) + ) { return getFirstVisibleParent({ bioEntity: parent, considerZoomLevel, @@ -70,8 +73,9 @@ export const getElementsByPoint = async ({ ); const element = elementResponse.data; if ( + element.visibilityLevel != null && parseInt(element.visibilityLevel, 10) - (ONE - FRACTIONAL_ZOOM_AT_WHICH_IMAGE_LAYER_CHANGE) > - (considerZoomLevel || Number.MAX_SAFE_INTEGER) + (considerZoomLevel || Number.MAX_SAFE_INTEGER) ) { const visibleParent = await getFirstVisibleParent({ bioEntity: element,