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

Merge branch 'feature/project-info-tab' into 'development'

feat(project info): initialised project info drawer

See merge request !111
parents ba85bc5c 5bdfb65b
No related branches found
No related tags found
2 merge requests!223reset the pin numbers before search results are fetch (so the results will be...,!111feat(project info): initialised project info drawer
Pipeline #84739 passed
Showing
with 349 additions and 15 deletions
......@@ -104,10 +104,10 @@ describe('Drawer - component', () => {
});
expect(screen.queryByTestId('reaction-drawer')).not.toBeInTheDocument();
store.dispatch(getReactionsByIds([id]));
store.dispatch(openReactionDrawerById(id));
await act(() => {
store.dispatch(getReactionsByIds([id]));
store.dispatch(openReactionDrawerById(id));
});
await waitFor(() => expect(screen.getByTestId('reaction-drawer')).toBeInTheDocument());
});
});
......
......@@ -8,6 +8,7 @@ import { SubmapsDrawer } from './SubmapsDrawer';
import { OverlaysDrawer } from './OverlaysDrawer';
import { BioEntityDrawer } from './BioEntityDrawer/BioEntityDrawer.component';
import { ExportDrawer } from './ExportDrawer';
import { ProjectInfoDrawer } from './ProjectInfoDrawer';
export const Drawer = (): JSX.Element => {
const { isOpen, drawerName } = useAppSelector(drawerSelector);
......@@ -25,6 +26,7 @@ export const Drawer = (): JSX.Element => {
{isOpen && drawerName === 'reaction' && <ReactionDrawer />}
{isOpen && drawerName === 'overlays' && <OverlaysDrawer />}
{isOpen && drawerName === 'bio-entity' && <BioEntityDrawer />}
{isOpen && drawerName === 'project-info' && <ProjectInfoDrawer />}
{isOpen && drawerName === 'export' && <ExportDrawer />}
</div>
);
......
import { act } from 'react-dom/test-utils';
import { render, screen } from '@testing-library/react';
import {
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import { projectFixture } from '@/models/fixtures/projectFixture';
import { StoreType } from '@/redux/store';
import { MODEL_WITH_DESCRIPTION } from '@/models/mocks/modelsMock';
import { ProjectInfoDrawer } from './ProjectInfoDrawer.component';
const MOCKED_STORE: InitialStoreState = {
project: {
data: { ...projectFixture },
loading: 'idle',
error: new Error(),
},
models: {
data: [MODEL_WITH_DESCRIPTION],
loading: 'idle',
error: new Error(),
},
};
const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStore);
return (
render(
<Wrapper>
<ProjectInfoDrawer />
</Wrapper>,
),
{
store,
}
);
};
describe('ProjectInfoDrawer', () => {
it('should render the project name', () => {
renderComponent(MOCKED_STORE);
expect(screen.getByText(projectFixture.name)).toBeInTheDocument();
});
it('should render the version', () => {
renderComponent(MOCKED_STORE);
expect(screen.getByText(projectFixture.version)).toBeInTheDocument();
});
it.skip('should render number of publications', () => {});
it.skip('should open publications modal when publications link is clicked', () => {});
it('should render the manual link', () => {
renderComponent(MOCKED_STORE);
const manualLink = screen.getByText(/Manual/i);
expect(manualLink).toBeInTheDocument();
expect(manualLink).toHaveAttribute('href', 'https://minerva.pages.uni.lu/doc/');
});
it('should render the disease link with name and href', async () => {
await act(() => {
renderComponent(MOCKED_STORE);
});
const diseaseLink = screen.getByText(/Disease:/i);
expect(diseaseLink).toBeInTheDocument();
const linkelement = screen.getByRole('link', { name: projectFixture.diseaseName });
expect(linkelement).toBeInTheDocument();
expect(linkelement).toHaveAttribute('href', projectFixture.disease.link);
});
it('should fetch diesease name when diseaseId is provided', async () => {
await act(() => {
renderComponent(MOCKED_STORE);
});
const organismLink = screen.getByText(/Organism:/i);
expect(organismLink).toBeInTheDocument();
const linkelement = screen.getByRole('link', { name: projectFixture.organismName });
expect(linkelement).toBeInTheDocument();
expect(linkelement).toHaveAttribute('href', projectFixture.organism.link);
});
it('should render the source file download button', () => {
renderComponent(MOCKED_STORE);
const downloadButton = screen.getByRole('link', { name: /Download source file/i });
expect(downloadButton).toBeInTheDocument();
expect(downloadButton).toHaveAttribute(
'href',
'localhost/projects/pdmap_appu_test:downloadSource',
);
expect(downloadButton).toHaveAttribute('download', 'sourceFile.txt');
});
it('should render the description when it exists', () => {
renderComponent(MOCKED_STORE);
const desc = screen.getByTestId('project-description');
expect(desc.innerHTML).toContain(
'For information on content, functionalities and referencing the Parkinson\'s disease map, click <a href="http://pdmap.uni.lu" target="_blank">here</a>',
);
});
it.skip('should not render the description when it does not exist', () => {
renderComponent();
const descriptionElement = screen.queryByText('This is the project description.');
expect(descriptionElement).not.toBeInTheDocument();
});
});
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import {
diseaseNameSelector,
projectNameSelector,
versionSelector,
organismNameSelector,
diseaseLinkSelector,
organismLinkSelector,
} from '@/redux/project/project.selectors';
import { DrawerHeading } from '@/shared/DrawerHeading';
import { apiPath } from '@/redux/apiPath';
import { LinkButton } from '@/shared/LinkButton';
import { mainMapModelDescriptionSelector } from '@/redux/models/models.selectors';
import './ProjectInfoDrawer.styles.css';
export const ProjectInfoDrawer = (): JSX.Element => {
const diseaseName = useAppSelector(diseaseNameSelector);
const diseaseLink = useAppSelector(diseaseLinkSelector);
const organismLink = useAppSelector(organismLinkSelector);
const organismName = useAppSelector(organismNameSelector);
const projectName = useAppSelector(projectNameSelector);
const version = useAppSelector(versionSelector);
const description = useAppSelector(mainMapModelDescriptionSelector);
const sourceDownloadLink = window.location.hostname + apiPath.getSourceFile();
return (
<div data-testid="export-drawer" className="h-full max-h-full">
<DrawerHeading title="Project info" />
<div className="h-[calc(100%-93px)] max-h-[calc(100%-93px)] overflow-y-auto px-6">
<p className="mt-6">
Name: <span className="font-semibold">{projectName}</span>
</p>
<p className="mt-4">
version: <span className="font-semibold">{version}</span>
</p>
<div className="mt-4">Data:</div>
<ul className="list-disc pl-6 ">
<li className="mt-2 text-hyperlink-blue">(21) publications</li>
<li className="mt-2 text-hyperlink-blue">
<a
href="https://minerva.pages.uni.lu/doc/"
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
Manual
</a>
</li>
<li className="mt-2 text-hyperlink-blue">
<span className="text-black">Disease: </span>
<a
href={diseaseLink}
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
{diseaseName}
</a>
</li>
<li className="mt-2 text-hyperlink-blue">
<span className="text-black">Organism: </span>
<a
href={organismLink}
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
{organismName}
</a>
</li>
</ul>
<LinkButton className="mt-6" href={sourceDownloadLink} download="sourceFile.txt">
Download source file
</LinkButton>
{description && (
<div
data-testid="project-description"
className="anchor-tag mt-7 rounded-lg bg-cultured px-4 py-2"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: description }}
/>
)}
</div>
</div>
);
};
.anchor-tag a {
@apply text-hyperlink-blue;
@apply hover:underline;
}
export { ProjectInfoDrawer } from './ProjectInfoDrawer.component';
import { ZOD_SEED } from '@/constants';
// eslint-disable-next-line import/no-extraneous-dependencies
import { createFixture } from 'zod-fixture';
import { projectSchema } from '../project';
import { projectSchema } from '../projectSchema';
export const projectFixture = createFixture(projectSchema, {
seed: ZOD_SEED,
......
......@@ -475,3 +475,22 @@ export const CORE_PD_MODEL_MOCK: MapModel = {
minZoom: 2,
maxZoom: 9,
};
export const MODEL_WITH_DESCRIPTION: MapModel = {
idObject: 5056,
width: 1975.0,
height: 1950.0,
defaultCenterX: null,
defaultCenterY: null,
description:
'For information on content, functionalities and referencing the Parkinson\'s disease map, click <a href="http://pdmap.uni.lu" target="_blank">here</a>\n\n.',
name: 'MTOR AMPK signaling',
defaultZoomLevel: null,
tileSize: 256,
references: [],
authors: [],
creationDate: null,
modificationDates: [],
minZoom: 2,
maxZoom: 5,
};
......@@ -6,8 +6,9 @@ import { overviewImageView } from './overviewImageView';
export const projectSchema = z.object({
version: z.string(),
disease,
diseaseName: z.string(),
organism,
idObject: z.number(),
organismName: z.string(),
status: z.string(),
directory: z.string(),
progress: z.number(),
......@@ -15,7 +16,9 @@ export const projectSchema = z.object({
logEntries: z.boolean(),
name: z.string(),
sharedInMinervaNet: z.boolean(),
owner: z.string(),
owner: z.object({
login: z.string(),
}),
projectId: z.string(),
creationDate: z.string(),
mapCanvasType: z.string(),
......
......@@ -44,4 +44,7 @@ export const apiPath = {
getCompartmentPathwayDetails: (ids: number[]): string =>
`projects/${PROJECT_ID}/models/*/bioEntities/elements/?id=${ids.join(',')}`,
sendCompartmentPathwaysIds: (): string => `projects/${PROJECT_ID}/models/*/bioEntities/elements/`,
getSourceFile: (): string => `/projects/${PROJECT_ID}:downloadSource`,
getMesh: (meshId: string): string => `mesh/${meshId}`,
getTaxonomy: (taxonomyId: string): string => `taxonomy/${taxonomyId}`,
};
......@@ -34,3 +34,7 @@ export const modelByIdSelector = createSelector(
const MAIN_MAP = 0;
export const mainMapModelSelector = createSelector(modelsDataSelector, models => models[MAIN_MAP]);
export const mainMapModelDescriptionSelector = createSelector(
modelsDataSelector,
models => models[MAIN_MAP].description,
);
......@@ -4,14 +4,14 @@ import {
ToolkitStoreWithSingleSlice,
createStoreInstanceUsingSliceReducer,
} from '@/utils/createStoreInstanceUsingSliceReducer';
import { mockNetworkResponse } from '@/utils/mockNetworkResponse';
import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse';
import { HttpStatusCode } from 'axios';
import { apiPath } from '../apiPath';
import projectReducer from './project.slice';
import { getProjectById } from './project.thunks';
import { ProjectState } from './project.types';
const mockedAxiosClient = mockNetworkResponse();
const mockedAxiosClient = mockNetworkNewAPIResponse();
const INITIAL_STATE: ProjectState = {
data: undefined,
......
......@@ -36,3 +36,30 @@ export const projectIdSelector = createSelector(
projectDataSelector,
projectData => projectData?.projectId,
);
export const projectNameSelector = createSelector(
projectDataSelector,
projectData => projectData?.name,
);
export const diseaseNameSelector = createSelector(
projectDataSelector,
projectData => projectData?.diseaseName,
);
export const diseaseLinkSelector = createSelector(
projectDataSelector,
projectData => projectData?.disease.link,
);
export const organismLinkSelector = createSelector(
projectDataSelector,
projectData => projectData?.organism.link,
);
export const organismNameSelector = createSelector(
projectDataSelector,
projectData => projectData?.organismName,
);
export const versionSelector = createSelector(projectDataSelector, state => state?.version);
import { projectSchema } from '@/models/project';
import { axiosInstance } from '@/services/api/utils/axiosInstance';
import { projectSchema } from '@/models/projectSchema';
import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance';
import { Project } from '@/types/models';
import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema';
import { createAsyncThunk } from '@reduxjs/toolkit';
......@@ -8,7 +8,7 @@ import { apiPath } from '../apiPath';
export const getProjectById = createAsyncThunk(
'project/getProjectById',
async (id: string): Promise<Project | undefined> => {
const response = await axiosInstance.get<Project>(apiPath.getProjectById(id));
const response = await axiosInstanceNewAPI.get<Project>(apiPath.getProjectById(id));
const isDataValid = validateDataUsingZodSchema(response.data, projectSchema);
......
import { render, screen } from '@testing-library/react';
import { LinkButton } from './LinkButton.component';
describe('LinkButton', () => {
it('renders without crashing', () => {
render(<LinkButton>Test</LinkButton>);
expect(screen.getByText('Test')).toBeInTheDocument();
});
it('applies the primary variant by default', () => {
render(<LinkButton>Test</LinkButton>);
const button = screen.getByText('Test');
expect(button).toHaveClass(
'bg-primary-500 text-white-pearl hover:bg-primary-600 active:bg-primary-700 disabled:bg-greyscale-700',
);
});
it('applies additional classes passed in', () => {
// eslint-disable-next-line tailwindcss/no-custom-classname
render(<LinkButton className="extra-class">Test</LinkButton>);
const button = screen.getByText('Test');
expect(button).toHaveClass('extra-class');
});
it('passes through additional props to the anchor element', () => {
render(<LinkButton data-testid="my-button">Test</LinkButton>);
const button = screen.getByTestId('my-button');
expect(button).toBeInTheDocument();
});
});
import { twMerge } from 'tailwind-merge';
const variants = {
primary: {
link: 'bg-primary-500 text-white-pearl hover:bg-primary-600 active:bg-primary-700 disabled:bg-greyscale-700',
},
} as const;
type VariantStyle = keyof typeof variants;
type LinkButtonProps = {
variant?: VariantStyle;
children: React.ReactNode;
} & React.AnchorHTMLAttributes<HTMLAnchorElement>;
export const LinkButton = ({
variant = 'primary',
className = '',
children,
...props
}: LinkButtonProps): JSX.Element => {
return (
<a
className={twMerge(
'group flex w-fit items-center rounded-e rounded-s px-3 py-2 text-xs font-bold',
variants[variant].link,
className,
)}
{...props}
>
{children}
</a>
);
};
export { LinkButton } from './LinkButton.component';
......@@ -29,7 +29,7 @@ import {
overviewImageLinkModel,
} from '@/models/overviewImageLink';
import { overviewImageView } from '@/models/overviewImageView';
import { projectSchema } from '@/models/project';
import { projectSchema } from '@/models/projectSchema';
import { reactionSchema } from '@/models/reaction';
import { reactionLineSchema } from '@/models/reactionLineSchema';
import { referenceSchema } from '@/models/referenceSchema';
......
......@@ -9,13 +9,14 @@ import { modelsDataSelector } from '@/redux/models/models.selectors';
import { overlaysDataSelector } from '@/redux/overlays/overlays.selectors';
import { projectDataSelector } from '@/redux/project/project.selectors';
import { initDataLoadingInitialized } from '@/redux/root/init.selectors';
import { mockNetworkResponse } from '@/utils/mockNetworkResponse';
import { mockNetworkNewAPIResponse, mockNetworkResponse } from '@/utils/mockNetworkResponse';
import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
import { renderHook, waitFor } from '@testing-library/react';
import { HttpStatusCode } from 'axios';
import * as hook from './useInitializeStore';
const mockedAxiosClient = mockNetworkResponse();
const mockedAxiosNEWApiClient = mockNetworkNewAPIResponse();
describe('useInitializeStore - hook', () => {
describe('when fired', () => {
......@@ -24,7 +25,7 @@ describe('useInitializeStore - hook', () => {
mockedAxiosClient
.onGet(apiPath.getAllOverlaysByProjectIdQuery(PROJECT_ID, { publicOverlay: true }))
.reply(HttpStatusCode.Ok, overlaysFixture);
mockedAxiosClient
mockedAxiosNEWApiClient
.onGet(apiPath.getProjectById(PROJECT_ID))
.reply(HttpStatusCode.Ok, projectFixture);
mockedAxiosClient
......
......@@ -29,6 +29,7 @@ const config: Config = {
purple: '#6400e3',
pink: '#f1009f',
'cetacean-blue': '#070130',
'hyperlink-blue': '#0048ff',
},
height: {
'calc-drawer': 'calc(100% - 104px)',
......
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