Skip to content
Snippets Groups Projects
Commit b9103e8d authored by Tadeusz Miesiąc's avatar Tadeusz Miesiąc
Browse files

feat(publications): initialised modal and fetching publications

parent ab449b1e
No related branches found
No related tags found
4 merge requests!223reset the pin numbers before search results are fetch (so the results will be...,!118Feature/publications search and layout,!117Feature/project info publications list submap select,!114Draft: Feature/project info publications modal + table
Showing
with 258 additions and 4 deletions
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@next/font": "^13.5.2", "@next/font": "^13.5.2",
"@reduxjs/toolkit": "^1.9.6", "@reduxjs/toolkit": "^1.9.6",
"@tanstack/react-table": "^8.11.7",
"@types/node": "20.6.2", "@types/node": "20.6.2",
"@types/openlayers": "^4.6.20", "@types/openlayers": "^4.6.20",
"@types/react": "18.2.21", "@types/react": "18.2.21",
...@@ -2018,6 +2019,37 @@ ...@@ -2018,6 +2019,37 @@
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/@tanstack/react-table": {
"version": "8.11.7",
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.11.7.tgz",
"integrity": "sha512-ZbzfMkLjxUTzNPBXJYH38pv2VpC9WUA+Qe5USSHEBz0dysDTv4z/ARI3csOed/5gmlmrPzVUN3UXGuUMbod3Jg==",
"dependencies": {
"@tanstack/table-core": "8.11.7"
},
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/@tanstack/table-core": {
"version": "8.11.7",
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.11.7.tgz",
"integrity": "sha512-N3ksnkbPbsF3PjubuZCB/etTqvctpXWRHIXTmYfJFnhynQKjeZu8BCuHvdlLPpumKbA+bjY4Ay9AELYLOXPWBg==",
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@testing-library/dom": { "node_modules/@testing-library/dom": {
"version": "9.3.3", "version": "9.3.3",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz",
...@@ -15341,6 +15373,19 @@ ...@@ -15341,6 +15373,19 @@
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"@tanstack/react-table": {
"version": "8.11.7",
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.11.7.tgz",
"integrity": "sha512-ZbzfMkLjxUTzNPBXJYH38pv2VpC9WUA+Qe5USSHEBz0dysDTv4z/ARI3csOed/5gmlmrPzVUN3UXGuUMbod3Jg==",
"requires": {
"@tanstack/table-core": "8.11.7"
}
},
"@tanstack/table-core": {
"version": "8.11.7",
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.11.7.tgz",
"integrity": "sha512-N3ksnkbPbsF3PjubuZCB/etTqvctpXWRHIXTmYfJFnhynQKjeZu8BCuHvdlLPpumKbA+bjY4Ay9AELYLOXPWBg=="
},
"@testing-library/dom": { "@testing-library/dom": {
"version": "9.3.3", "version": "9.3.3",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz",
......
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
"dependencies": { "dependencies": {
"@next/font": "^13.5.2", "@next/font": "^13.5.2",
"@reduxjs/toolkit": "^1.9.6", "@reduxjs/toolkit": "^1.9.6",
"@tanstack/react-table": "^8.11.7",
"@types/node": "20.6.2", "@types/node": "20.6.2",
"@types/openlayers": "^4.6.20", "@types/openlayers": "^4.6.20",
"@types/react": "18.2.21", "@types/react": "18.2.21",
......
...@@ -8,6 +8,7 @@ import { twMerge } from 'tailwind-merge'; ...@@ -8,6 +8,7 @@ import { twMerge } from 'tailwind-merge';
import { LoginModal } from './LoginModal'; import { LoginModal } from './LoginModal';
import { MODAL_ROLE } from './Modal.constants'; import { MODAL_ROLE } from './Modal.constants';
import { OverviewImagesModal } from './OverviewImagesModal'; import { OverviewImagesModal } from './OverviewImagesModal';
import { PublicationsModal } from './PublicationsModal';
const MolArtModal = dynamic( const MolArtModal = dynamic(
() => import('./MolArtModal/MolArtModal.component').then(mod => mod.MolArtModal), () => import('./MolArtModal/MolArtModal.component').then(mod => mod.MolArtModal),
...@@ -46,6 +47,7 @@ export const Modal = (): React.ReactNode => { ...@@ -46,6 +47,7 @@ export const Modal = (): React.ReactNode => {
{isOpen && modalName === 'overview-images' && <OverviewImagesModal />} {isOpen && modalName === 'overview-images' && <OverviewImagesModal />}
{isOpen && modalName === 'mol-art' && <MolArtModal />} {isOpen && modalName === 'mol-art' && <MolArtModal />}
{isOpen && modalName === 'login' && <LoginModal />} {isOpen && modalName === 'login' && <LoginModal />}
{isOpen && modalName === 'publications' && <PublicationsModal />}
</div> </div>
</div> </div>
</div> </div>
......
describe('Publications Modal - component', () => {
it('should render number of publications', () => {});
it('should render download csv button', () => {});
it('should trigger download on csv button click', () => {});
it('should render search input', () => {});
it('should be able to search publications by using search input', () => {});
it('should be able to sort publications by clicking on Pubmed ID column header', () => {});
it('should be able to sort publications by clicking on Title column header', () => {});
it('should be able to sort publications by clicking on Authors column header', () => {});
it('should be able to sort publications by clicking on Journal column header', () => {});
it('should be able to sort publications by clicking on Year column header', () => {});
it('should be able to sort publications by clicking on Elements on map column header', () => {});
it('should be able to sort publications by clicking on SUBMAPS on map column header', () => {});
it('should render publications list', () => {});
it('should render pagination', () => {});
it('should be able to navigate to next page', () => {});
it('should be able to navigate to previous page', () => {});
});
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { getPublications } from '@/redux/publications/publications.thunks';
import { useEffect } from 'react';
export const PublicationsModal = (): JSX.Element => {
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(getPublications({}));
}, [dispatch]);
return <div className="flex h-full w-full items-center justify-center bg-white">lol</div>;
};
export { PublicationsModal } from './PublicationsModal';
...@@ -12,8 +12,12 @@ import { apiPath } from '@/redux/apiPath'; ...@@ -12,8 +12,12 @@ import { apiPath } from '@/redux/apiPath';
import { LinkButton } from '@/shared/LinkButton'; import { LinkButton } from '@/shared/LinkButton';
import { mainMapModelDescriptionSelector } from '@/redux/models/models.selectors'; import { mainMapModelDescriptionSelector } from '@/redux/models/models.selectors';
import './ProjectInfoDrawer.styles.css'; import './ProjectInfoDrawer.styles.css';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { useEffect } from 'react';
import { openPublicationsModal } from '@/redux/modal/modal.slice';
export const ProjectInfoDrawer = (): JSX.Element => { export const ProjectInfoDrawer = (): JSX.Element => {
const dispatch = useAppDispatch();
const diseaseName = useAppSelector(diseaseNameSelector); const diseaseName = useAppSelector(diseaseNameSelector);
const diseaseLink = useAppSelector(diseaseLinkSelector); const diseaseLink = useAppSelector(diseaseLinkSelector);
const organismLink = useAppSelector(organismLinkSelector); const organismLink = useAppSelector(organismLinkSelector);
...@@ -24,6 +28,14 @@ export const ProjectInfoDrawer = (): JSX.Element => { ...@@ -24,6 +28,14 @@ export const ProjectInfoDrawer = (): JSX.Element => {
const sourceDownloadLink = window.location.hostname + apiPath.getSourceFile(); const sourceDownloadLink = window.location.hostname + apiPath.getSourceFile();
useEffect(() => {
// dispatch(getPublications());
}, [dispatch]);
const onPublicationsClick = (): void => {
dispatch(openPublicationsModal());
};
return ( return (
<div data-testid="export-drawer" className="h-full max-h-full"> <div data-testid="export-drawer" className="h-full max-h-full">
<DrawerHeading title="Project info" /> <DrawerHeading title="Project info" />
...@@ -36,13 +48,17 @@ export const ProjectInfoDrawer = (): JSX.Element => { ...@@ -36,13 +48,17 @@ export const ProjectInfoDrawer = (): JSX.Element => {
</p> </p>
<div className="mt-4">Data:</div> <div className="mt-4">Data:</div>
<ul className="list-disc pl-6 "> <ul className="list-disc pl-6 ">
<li className="mt-2 text-hyperlink-blue">(21) publications</li> <li className="mt-2 text-hyperlink-blue">
<button type="button" onClick={onPublicationsClick} className="text-sm font-semibold">
(21) publications
</button>
</li>
<li className="mt-2 text-hyperlink-blue"> <li className="mt-2 text-hyperlink-blue">
<a <a
href="https://minerva.pages.uni.lu/doc/" href="https://minerva.pages.uni.lu/doc/"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="hover:underline" className="font-semibold hover:underline"
> >
Manual Manual
</a> </a>
...@@ -53,7 +69,7 @@ export const ProjectInfoDrawer = (): JSX.Element => { ...@@ -53,7 +69,7 @@ export const ProjectInfoDrawer = (): JSX.Element => {
href={diseaseLink} href={diseaseLink}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="hover:underline" className="font-semibold hover:underline"
> >
{diseaseName} {diseaseName}
</a> </a>
...@@ -64,7 +80,7 @@ export const ProjectInfoDrawer = (): JSX.Element => { ...@@ -64,7 +80,7 @@ export const ProjectInfoDrawer = (): JSX.Element => {
href={organismLink} href={organismLink}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="hover:underline" className="font-semibold hover:underline"
> >
{organismName} {organismName}
</a> </a>
......
import { z } from 'zod';
import { publicationSchema } from './publicationsSchema';
export const publicationsResponseSchema = z.object({
data: z.array(publicationSchema),
totalSize: z.number(),
filteredSize: z.number(),
length: z.number(),
page: z.number(),
});
import { z } from 'zod';
import { targetElementSchema } from './targetElementSchema';
import { articleSchema } from './articleSchema';
export const publicationSchema = z.object({
elements: z.array(targetElementSchema),
publication: z.object({
article: articleSchema,
}),
});
...@@ -2,6 +2,33 @@ import { PROJECT_ID } from '@/constants'; ...@@ -2,6 +2,33 @@ import { PROJECT_ID } from '@/constants';
import { PerfectSearchParams } from '@/types/search'; import { PerfectSearchParams } from '@/types/search';
import { Point } from '@/types/map'; import { Point } from '@/types/map';
export type GetPublicationsParams = {
start?: number;
sortColumn?: string;
sortOrder?: string;
level?: number;
length?: number;
search?: string;
};
const getPublicationsURLSearchParams = ({
start,
sortColumn,
sortOrder,
level,
length,
search,
}: GetPublicationsParams): URLSearchParams => {
const params = new URLSearchParams();
if (start) params.append('start', start.toString());
if (sortColumn) params.append('sortColumn', sortColumn);
if (sortOrder) params.append('sortOrder', sortOrder);
if (level) params.append('level', level.toString());
if (length) params.append('length', length.toString());
if (search) params.append('search', search);
return params;
};
export const apiPath = { export const apiPath = {
getBioEntityContentsStringWithQuery: ({ getBioEntityContentsStringWithQuery: ({
searchQuery, searchQuery,
...@@ -47,4 +74,8 @@ export const apiPath = { ...@@ -47,4 +74,8 @@ export const apiPath = {
getSourceFile: (): string => `/projects/${PROJECT_ID}:downloadSource`, getSourceFile: (): string => `/projects/${PROJECT_ID}:downloadSource`,
getMesh: (meshId: string): string => `mesh/${meshId}`, getMesh: (meshId: string): string => `mesh/${meshId}`,
getTaxonomy: (taxonomyId: string): string => `taxonomy/${taxonomyId}`, getTaxonomy: (taxonomyId: string): string => `taxonomy/${taxonomyId}`,
getPublications: (params: GetPublicationsParams, modelId = '*'): string =>
`/projects/${PROJECT_ID}/models/${modelId}/publications/${getPublicationsURLSearchParams(
params,
)}`,
}; };
...@@ -50,3 +50,9 @@ export const setOverviewImageIdReducer = ( ...@@ -50,3 +50,9 @@ export const setOverviewImageIdReducer = (
imageId: action.payload, imageId: action.payload,
}; };
}; };
export const openPublicationsModalReducer = (state: ModalState): void => {
state.isOpen = true;
state.modalName = 'publications';
state.modalTitle = 'Publications';
};
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
openOverviewImagesModalByIdReducer, openOverviewImagesModalByIdReducer,
openMolArtModalByIdReducer, openMolArtModalByIdReducer,
setOverviewImageIdReducer, setOverviewImageIdReducer,
openPublicationsModalReducer,
} from './modal.reducers'; } from './modal.reducers';
const modalSlice = createSlice({ const modalSlice = createSlice({
...@@ -19,6 +20,7 @@ const modalSlice = createSlice({ ...@@ -19,6 +20,7 @@ const modalSlice = createSlice({
openMolArtModalById: openMolArtModalByIdReducer, openMolArtModalById: openMolArtModalByIdReducer,
setOverviewImageId: setOverviewImageIdReducer, setOverviewImageId: setOverviewImageIdReducer,
openLoginModal: openLoginModalReducer, openLoginModal: openLoginModalReducer,
openPublicationsModal: openPublicationsModalReducer,
}, },
}); });
...@@ -29,6 +31,7 @@ export const { ...@@ -29,6 +31,7 @@ export const {
setOverviewImageId, setOverviewImageId,
openMolArtModalById, openMolArtModalById,
openLoginModal, openLoginModal,
openPublicationsModal,
} = modalSlice.actions; } = modalSlice.actions;
export default modalSlice.reducer; export default modalSlice.reducer;
import { PublicationsState } from './publications.types';
export const PUBLICATIONS_INITIAL_STATE_MOCK: PublicationsState = {
loading: 'idle',
data: undefined,
error: { name: '', message: '' },
};
import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
import { PublicationsState } from './publications.types';
import { getPublications } from './publications.thunks';
export const getPublicationsReducer = (
builder: ActionReducerMapBuilder<PublicationsState>,
): void => {
builder.addCase(getPublications.pending, state => {
state.loading = 'pending';
});
builder.addCase(getPublications.fulfilled, (state, action) => {
state.data = action.payload || undefined;
state.loading = 'succeeded';
});
builder.addCase(getPublications.rejected, state => {
state.loading = 'failed';
// TODO to discuss manage state of failure
});
};
import { createSelector } from '@reduxjs/toolkit';
import { rootSelector } from '../root/root.selectors';
export const publicationsSelector = createSelector(rootSelector, state => state.publications);
export const publicationsDataSelector = createSelector(
publicationsSelector,
publications => publications?.data,
);
export const totalSizeSelector = createSelector(publicationsDataSelector, data => data?.totalSize);
export const filteredSizeSelector = createSelector(
publicationsDataSelector,
data => data?.filteredSize,
);
import { createSlice } from '@reduxjs/toolkit';
import { PublicationsState } from './publications.types';
import { getPublicationsReducer } from './publications.reducers';
const initialState: PublicationsState = {
data: undefined,
loading: 'idle',
error: { name: '', message: '' },
};
const publicationsSlice = createSlice({
name: 'publications',
initialState,
reducers: {},
extraReducers: builder => {
getPublicationsReducer(builder);
},
});
export default publicationsSlice.reducer;
import { createAsyncThunk } from '@reduxjs/toolkit';
import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema';
import { axiosInstance } from '@/services/api/utils/axiosInstance';
import { PublicationsResponse } from '@/types/models';
import { publicationsResponseSchema } from '@/models/publicationsResponseSchema';
import { GetPublicationsParams, apiPath } from '../apiPath';
export const getPublications = createAsyncThunk(
'publications/getPublications',
async (params: GetPublicationsParams): Promise<PublicationsResponse | undefined> => {
const response = await axiosInstance.get<PublicationsResponse>(apiPath.getPublications(params));
const isDataValid = validateDataUsingZodSchema(response.data, publicationsResponseSchema);
return isDataValid ? response.data : undefined;
},
);
import { FetchDataState } from '@/types/fetchDataState';
import { PublicationsResponse } from '@/types/models';
export type PublicationsState = FetchDataState<PublicationsResponse>;
export type GetPublicationsParams = {
start?: number;
sortColumn?: string;
sortOrder?: string;
level?: number;
length?: number;
search?: string;
};
...@@ -19,6 +19,7 @@ import { RootState } from '../store'; ...@@ -19,6 +19,7 @@ import { RootState } from '../store';
import { USER_INITIAL_STATE_MOCK } from '../user/user.mock'; import { USER_INITIAL_STATE_MOCK } from '../user/user.mock';
import { STATISTICS_STATE_INITIAL_MOCK } from '../statistics/statistics.mock'; import { STATISTICS_STATE_INITIAL_MOCK } from '../statistics/statistics.mock';
import { COMPARTMENT_PATHWAYS_INITIAL_STATE_MOCK } from '../compartmentPathways/compartmentPathways.mock'; import { COMPARTMENT_PATHWAYS_INITIAL_STATE_MOCK } from '../compartmentPathways/compartmentPathways.mock';
import { PUBLICATIONS_INITIAL_STATE_MOCK } from '../publications/publications.mock';
export const INITIAL_STORE_STATE_MOCK: RootState = { export const INITIAL_STORE_STATE_MOCK: RootState = {
search: SEARCH_STATE_INITIAL_MOCK, search: SEARCH_STATE_INITIAL_MOCK,
...@@ -41,4 +42,5 @@ export const INITIAL_STORE_STATE_MOCK: RootState = { ...@@ -41,4 +42,5 @@ export const INITIAL_STORE_STATE_MOCK: RootState = {
legend: LEGEND_INITIAL_STATE_MOCK, legend: LEGEND_INITIAL_STATE_MOCK,
statistics: STATISTICS_STATE_INITIAL_MOCK, statistics: STATISTICS_STATE_INITIAL_MOCK,
compartmentPathways: COMPARTMENT_PATHWAYS_INITIAL_STATE_MOCK, compartmentPathways: COMPARTMENT_PATHWAYS_INITIAL_STATE_MOCK,
publications: PUBLICATIONS_INITIAL_STATE_MOCK,
}; };
...@@ -26,6 +26,7 @@ import legendReducer from './legend/legend.slice'; ...@@ -26,6 +26,7 @@ import legendReducer from './legend/legend.slice';
import { mapListenerMiddleware } from './map/middleware/map.middleware'; import { mapListenerMiddleware } from './map/middleware/map.middleware';
import statisticsReducer from './statistics/statistics.slice'; import statisticsReducer from './statistics/statistics.slice';
import compartmentPathwaysReducer from './compartmentPathways/compartmentPathways.slice'; import compartmentPathwaysReducer from './compartmentPathways/compartmentPathways.slice';
import publicationsReducer from './publications/publications.slice';
export const reducers = { export const reducers = {
search: searchReducer, search: searchReducer,
...@@ -48,6 +49,7 @@ export const reducers = { ...@@ -48,6 +49,7 @@ export const reducers = {
legend: legendReducer, legend: legendReducer,
statistics: statisticsReducer, statistics: statisticsReducer,
compartmentPathways: compartmentPathwaysReducer, compartmentPathways: compartmentPathwaysReducer,
publications: publicationsReducer,
}; };
export const middlewares = [mapListenerMiddleware.middleware]; export const middlewares = [mapListenerMiddleware.middleware];
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment