Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • minerva/frontend
1 result
Show changes
Commits on Source (26)
Showing
with 782 additions and 125 deletions
......@@ -11,6 +11,7 @@
"dependencies": {
"@next/font": "^13.5.2",
"@reduxjs/toolkit": "^1.9.6",
"@tanstack/react-table": "^8.11.7",
"@types/node": "20.6.2",
"@types/openlayers": "^4.6.20",
"@types/react": "18.2.21",
......@@ -2038,6 +2039,37 @@
"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": {
"version": "9.3.3",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz",
......@@ -15447,6 +15479,19 @@
"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": {
"version": "9.3.3",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz",
......
......@@ -25,6 +25,7 @@
"dependencies": {
"@next/font": "^13.5.2",
"@reduxjs/toolkit": "^1.9.6",
"@tanstack/react-table": "^8.11.7",
"@types/node": "20.6.2",
"@types/openlayers": "^4.6.20",
"@types/react": "18.2.21",
......
import { MouseEvent } from 'react';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { MAIN_MAP } from '@/redux/map/map.constants';
import { mapModelIdSelector, mapOpenedMapsSelector } from '@/redux/map/map.selectors';
import { closeMap, closeMapAndSetMainMapActive, setActiveMap } from '@/redux/map/map.slice';
import { OppenedMap } from '@/redux/map/map.types';
import { Button } from '@/shared/Button';
import { Icon } from '@/shared/Icon';
import { MAIN_MAP } from '@/redux/map/map.constants';
import { MouseEvent } from 'react';
import { twMerge } from 'tailwind-merge';
export const MapNavigation = (): JSX.Element => {
......
import { MODAL_INITIAL_STATE } from '@/redux/modal/modal.constants';
import { modalSelector } from '@/redux/modal/modal.selector';
import { StoreType } from '@/redux/store';
import {
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import { render, screen } from '@testing-library/react';
import { Modal } from './Modal.component';
const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStore);
return (
render(
<Wrapper>
<Modal />
</Wrapper>,
),
{
store,
}
);
};
describe('Modal - Component', () => {
describe('when modal is hidden', () => {
beforeEach(() => {
renderComponent({
modal: {
...MODAL_INITIAL_STATE,
isOpen: false,
modalTitle: 'Modal Hidden Title',
},
});
});
it('should modal have hidden class', () => {
const modalElement = screen.getByRole('modal');
expect(modalElement).toBeInTheDocument();
expect(modalElement).toHaveClass('hidden');
});
});
describe('when modal is shown', () => {
let store: StoreType;
beforeEach(() => {
const { store: newStore } = renderComponent({
modal: {
...MODAL_INITIAL_STATE,
isOpen: true,
modalTitle: 'Modal Opened Title',
},
});
store = newStore;
});
it('should modal NOT have hidden class', () => {
const modalElement = screen.getByRole('modal');
expect(modalElement).toBeInTheDocument();
expect(modalElement).not.toHaveClass('hidden');
});
it('shows modal title', () => {
expect(screen.getByText('Modal Opened Title', { exact: false })).toBeInTheDocument();
});
it('shows modal close button', () => {
expect(screen.getByLabelText('close button')).toBeInTheDocument();
});
it('closes modal on close button click', () => {
const closeButton = screen.getByLabelText('close button');
closeButton.click();
const { isOpen } = modalSelector(store.getState());
expect(isOpen).toBeFalsy();
});
});
});
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { modalSelector } from '@/redux/modal/modal.selector';
import { closeModal } from '@/redux/modal/modal.slice';
import { Icon } from '@/shared/Icon';
import dynamic from 'next/dynamic';
import { twMerge } from 'tailwind-merge';
import { LoginModal } from './LoginModal';
import { MODAL_ROLE } from './Modal.constants';
import { OverviewImagesModal } from './OverviewImagesModal';
import { PublicationsModal } from './PublicationsModal';
import { ModalLayout } from './ModalLayout';
import { EditOverlayModal } from './EditOverlayModal';
const MolArtModal = dynamic(
......@@ -16,41 +13,31 @@ const MolArtModal = dynamic(
);
export const Modal = (): React.ReactNode => {
const dispatch = useAppDispatch();
const { isOpen, modalName, modalTitle } = useAppSelector(modalSelector);
const handleCloseModal = (): void => {
dispatch(closeModal());
};
const { isOpen, modalName } = useAppSelector(modalSelector);
return (
<div
className={twMerge(
'absolute left-0 top-0 z-10 h-full w-full bg-cetacean-blue/[.48]',
isOpen ? '' : 'hidden',
<>
{isOpen && modalName === 'overview-images' && (
<ModalLayout>
<OverviewImagesModal />
</ModalLayout>
)}
{isOpen && modalName === 'mol-art' && (
<ModalLayout>
<MolArtModal />
</ModalLayout>
)}
{isOpen && modalName === 'login' && (
<ModalLayout>
<LoginModal />
</ModalLayout>
)}
{isOpen && modalName === 'publications' && <PublicationsModal />}
{isOpen && modalName === 'edit-overlay' && (
<ModalLayout>
<EditOverlayModal />
</ModalLayout>
)}
role={MODAL_ROLE}
>
<div className="flex h-full w-full items-center justify-center">
<div
className={twMerge(
'flex h-5/6 w-10/12 flex-col overflow-hidden rounded-lg',
modalName === 'login' && 'h-auto w-[400px]',
modalName === 'edit-overlay' && 'h-auto w-[450px]',
)}
>
<div className="flex items-center justify-between bg-white p-[24px] text-xl">
<div>{modalTitle}</div>
<button type="button" onClick={handleCloseModal} aria-label="close button">
<Icon name="close" className="fill-font-500" />
</button>
</div>
{isOpen && modalName === 'overview-images' && <OverviewImagesModal />}
{isOpen && modalName === 'mol-art' && <MolArtModal />}
{isOpen && modalName === 'login' && <LoginModal />}
{isOpen && modalName === 'edit-overlay' && <EditOverlayModal />}
</div>
</div>
</div>
</>
);
};
import { twMerge } from 'tailwind-merge';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { modalSelector } from '@/redux/modal/modal.selector';
import { closeModal } from '@/redux/modal/modal.slice';
import { Icon } from '@/shared/Icon';
import { MODAL_ROLE } from './ModalLayout.constants';
type ModalLayoutProps = {
children: React.ReactNode;
};
export const ModalLayout = ({ children }: ModalLayoutProps): JSX.Element => {
const dispatch = useAppDispatch();
const { modalName, modalTitle } = useAppSelector(modalSelector);
const handleCloseModal = (): void => {
dispatch(closeModal());
};
return (
<div
className={twMerge('absolute left-0 top-0 z-10 h-full w-full bg-cetacean-blue/[.48]')}
role={MODAL_ROLE}
>
<div className="flex h-full w-full items-center justify-center">
<div
className={twMerge(
'flex h-5/6 w-10/12 flex-col overflow-hidden rounded-lg',
modalName === 'login' && 'h-auto w-[400px]',
)}
>
<div className="flex items-center justify-between bg-white p-[24px] text-xl">
<div>{modalTitle}</div>
<button type="button" onClick={handleCloseModal} aria-label="close button">
<Icon name="close" className="fill-font-500" />
</button>
</div>
{children}
</div>
</div>
</div>
);
};
export { ModalLayout } from './ModalLayout.component';
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', () => {});
describe('submaps filter', () => {
it('should render submaps filter', () => {});
it('should have no default submap selected on init on submaps filter', () => {});
it('should display publications for selected submap', () => {});
it('should display publications related to selected submap when submap is selected', () => {});
it('should display publications related to selected submap when user searches for publications using search input', () => {});
it('should display publications related to selected submap when user sorts publications by clicking on column header', () => {});
});
});
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { getPublications } from '@/redux/publications/publications.thunks';
import { useEffect, useMemo } from 'react';
import { publicationsListDataSelector } from '@/redux/publications/publications.selectors';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { modelsNameMapSelector } from '@/redux/models/models.selectors';
import { FIRST_ARRAY_ELEMENT } from '@/constants/common';
import { LoadingIndicator } from '@/shared/LoadingIndicator';
import {
PublicationsTable,
PublicationsTableData,
} from './PublicationsTable/PublicationsTable.component';
import { PublicationsModalLayout } from './PublicationsModalLayout';
export const PublicationsModal = (): JSX.Element => {
const dispatch = useAppDispatch();
const data = useAppSelector(publicationsListDataSelector);
const mapsNames = useAppSelector(modelsNameMapSelector);
const parsedData: PublicationsTableData[] | undefined = useMemo(() => {
const dd = data?.map(item => ({
pubmedId: item.publication.article.pubmedId,
title: item.publication.article.title,
authors: item.publication.article.authors,
journal: item.publication.article.journal,
year: item.publication.article.year,
elementsOnMap: '{link to element on map}',
submaps: mapsNames[item.elements[FIRST_ARRAY_ELEMENT].modelId],
}));
return dd || [];
}, [data, mapsNames]);
useEffect(() => {
if (!data) {
dispatch(getPublications({ params: {} }));
}
}, [data, dispatch]);
return (
<PublicationsModalLayout>
<div className="flex w-full flex-1 flex-col items-center justify-center overflow-hidden bg-white">
{data ? <PublicationsTable data={parsedData} /> : <LoadingIndicator />}
</div>
</PublicationsModalLayout>
);
};
import { twMerge } from 'tailwind-merge';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { closeModal } from '@/redux/modal/modal.slice';
import { Icon } from '@/shared/Icon';
import { filteredSizeSelector } from '@/redux/publications/publications.selectors';
import { MODAL_ROLE } from './PublicationsModalLayout.constants';
import { PublicationsSearch } from '../PublicationsSearch';
type ModalLayoutProps = {
children: React.ReactNode;
};
export const PublicationsModalLayout = ({ children }: ModalLayoutProps): JSX.Element => {
const dispatch = useAppDispatch();
const numberOfPublications = useAppSelector(filteredSizeSelector);
const handleCloseModal = (): void => {
dispatch(closeModal());
};
return (
<div
className={twMerge('absolute left-0 top-0 z-10 h-full w-full bg-cetacean-blue/[.48]')}
role={MODAL_ROLE}
>
<div className="flex h-full w-full items-center justify-center">
<div className={twMerge('flex h-5/6 w-10/12 flex-col overflow-hidden rounded-lg')}>
<div className="flex items-center justify-between bg-white p-[24px] text-xl">
<div className="font-semibold">
<div>Publications ({numberOfPublications} results)</div>
</div>
<div className="flex flex-row flex-nowrap items-center">
<PublicationsSearch />
<button type="button" onClick={handleCloseModal} aria-label="close button">
<Icon name="close" className="fill-font-500" />
</button>
</div>
</div>
{children}
</div>
</div>
</div>
);
};
export { PublicationsModalLayout } from './PublicationsModalLayout.component';
import { ChangeEvent, useCallback, useEffect, useState } from 'react';
import lensIcon from '@/assets/vectors/icons/lens.svg';
import { useDebounce } from 'use-debounce';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { getPublications } from '@/redux/publications/publications.thunks';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import {
isLoadingSelector,
selectedModelIdSelector,
sortColumnSelector,
sortOrderSelector,
} from '@/redux/publications/publications.selectors';
import Image from 'next/image';
import { setPublicationSearchValue } from '@/redux/publications/publications.slice';
import { DEFAULT_PAGE_SIZE } from '../PublicationsTable/PublicationsTable.constants';
const DEFAULT_DELAY = 500;
export const PublicationsSearch = (): JSX.Element => {
const dispatch = useAppDispatch();
const isLoading = useAppSelector(isLoadingSelector);
const [value, setValue] = useState('');
const [debouncedValue] = useDebounce<string>(value, DEFAULT_DELAY);
const sortColumn = useAppSelector(sortColumnSelector);
const sortOrder = useAppSelector(sortOrderSelector);
const selectedId = useAppSelector(selectedModelIdSelector);
const handleChange = (event: ChangeEvent<HTMLInputElement>): void => {
setValue(event.target.value);
};
const handleSearch = useCallback((): void => {
dispatch(
getPublications({
params: {
page: 0,
length: DEFAULT_PAGE_SIZE,
sortColumn,
sortOrder,
search: debouncedValue,
},
modelId: selectedId,
}),
);
}, [debouncedValue, dispatch, selectedId, sortColumn, sortOrder]);
useEffect(() => {
dispatch(setPublicationSearchValue(debouncedValue));
handleSearch();
}, [debouncedValue, dispatch, handleSearch]);
return (
<div className="relative mr-4">
<input
value={value}
name="search-input"
aria-label="search-input"
data-testid="search-input"
onChange={handleChange}
disabled={isLoading}
className="h-9 w-72 rounded-[64px] border border-transparent bg-cultured px-4 py-2.5 text-xs font-medium text-font-400 outline-none hover:border-greyscale-600 focus:border-greyscale-600"
/>
<button disabled={isLoading} type="button" className="bg-transparent">
<Image
src={lensIcon}
alt="lens icon"
height={16}
width={16}
className="absolute right-4 top-2.5"
/>
</button>
</div>
);
};
export { PublicationsSearch } from './PublicationsSearch.component';
import { useSelect } from 'downshift';
import { twMerge } from 'tailwind-merge';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { modelsIdsAndNamesSelector } from '@/redux/models/models.selectors';
import {
searchValueSelector,
sortColumnSelector,
sortOrderSelector,
} from '@/redux/publications/publications.selectors';
import { getPublications } from '@/redux/publications/publications.thunks';
import { setSelectedModelId } from '@/redux/publications/publications.slice';
import { Icon } from '@/shared/Icon';
import { DEFAULT_PAGE_SIZE } from '../PublicationsTable.constants';
export const FilterBySubmapHeader = (): JSX.Element => {
const dispatch = useAppDispatch();
const models = useAppSelector(modelsIdsAndNamesSelector);
const sortColumn = useAppSelector(sortColumnSelector);
const sortOrder = useAppSelector(sortOrderSelector);
const searchValue = useAppSelector(searchValueSelector);
const handleChange = (modelId: number | undefined): void => {
const newModelId = modelId ? String(modelId) : undefined;
dispatch(setSelectedModelId(newModelId));
dispatch(
getPublications({
params: {
page: 0,
length: DEFAULT_PAGE_SIZE,
sortColumn,
sortOrder,
search: searchValue,
},
modelId: newModelId,
}),
);
};
const {
isOpen,
selectedItem,
getToggleButtonProps,
getMenuProps,
highlightedIndex,
getItemProps,
} = useSelect({
items: models,
initialSelectedItem: null,
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => handleChange(newSelectedItem?.id),
});
return (
<div className="relative">
<div
className="flex cursor-pointer flex-row items-center justify-between bg-white px-3"
{...getToggleButtonProps()}
data-testid="background-dropdown-button"
>
<span data-testid="background-dropdown-button-name" className="truncate font-semibold">
{selectedItem?.name || 'Submaps'}
</span>
<Icon
name="chevron-down"
className={twMerge('arrow-button h-6 w-6 fill-primary-500', isOpen && 'rotate-180')}
/>
</div>
<ul
{...getMenuProps()}
className={twMerge(
'absolute top-full mt-2 h-60 w-full overflow-y-scroll bg-white shadow-lg',
!isOpen && 'hidden',
)}
data-testid="background-dropdown-list"
>
{isOpen &&
models.map((item, index) => (
<li
key={item.id}
{...getItemProps({ item, index })}
className={twMerge(
'w-full truncate border-t text-left font-normal',
highlightedIndex === index && 'text-primary-500',
selectedItem?.id === item.id && 'font-bold',
'flex flex-col px-4 py-2 shadow-sm',
)}
>
{item.name}
</li>
))}
</ul>
</div>
);
};
/* eslint-disable no-magic-numbers */
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { act } from 'react-dom/test-utils';
import { mockNetworkResponse } from '@/utils/mockNetworkResponse';
import {
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import { StoreType } from '@/redux/store';
import { render, screen } from '@testing-library/react';
import { PUBLICATIONS_INITIAL_STATE_MOCK } from '@/redux/publications/publications.mock';
import { DEFAULT_ERROR } from '@/constants/errors';
import { MODELS_MOCK } from '@/models/mocks/modelsMock';
import { FIRST_ARRAY_ELEMENT, ZERO } from '@/constants/common';
import { FilterBySubmapHeader } from './FilterBySubmapHeader.component';
mockNetworkResponse();
const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState);
return (
render(
<Wrapper>
<FilterBySubmapHeader />
</Wrapper>,
),
{
store,
}
);
};
describe('FilterBySubmapHeader - component', () => {
describe('render', () => {
it('should render closed dropdown', async () => {
renderComponent({
publications: PUBLICATIONS_INITIAL_STATE_MOCK,
models: { data: MODELS_MOCK, loading: 'idle', error: DEFAULT_ERROR },
});
const ulTag = await screen.findByTestId('background-dropdown-list');
const listItems = screen.queryAllByRole('listitem');
expect(listItems).toHaveLength(ZERO);
expect(ulTag).toBeInTheDocument();
expect(ulTag).toHaveClass('hidden');
});
it('should render available submaps on dropdown open', async () => {
renderComponent({
publications: PUBLICATIONS_INITIAL_STATE_MOCK,
models: { data: MODELS_MOCK, loading: 'idle', error: DEFAULT_ERROR },
});
const button = await screen.findByTestId('background-dropdown-button');
await act(() => {
button.click();
});
const ulTag = await screen.findByTestId('background-dropdown-list');
expect(ulTag).not.toHaveClass('hidden');
const listItems = await screen.findAllByRole('option');
expect(listItems).toHaveLength(MODELS_MOCK.length);
expect(listItems[FIRST_ARRAY_ELEMENT]).toHaveTextContent(
MODELS_MOCK[FIRST_ARRAY_ELEMENT].name,
);
});
it('should display no value selected initially', () => {
renderComponent({
publications: PUBLICATIONS_INITIAL_STATE_MOCK,
models: { data: MODELS_MOCK, loading: 'idle', error: DEFAULT_ERROR },
});
const button = screen.getByTestId('background-dropdown-button-name');
expect(button).toHaveTextContent('Submaps');
});
it('should display selected submap name in toggle button', async () => {
renderComponent({
publications: PUBLICATIONS_INITIAL_STATE_MOCK,
models: { data: MODELS_MOCK, loading: 'idle', error: DEFAULT_ERROR },
});
const button = await screen.findByTestId('background-dropdown-button');
await act(() => {
button.click();
});
const listItems = screen.getAllByRole('option');
const selectedItem = listItems[FIRST_ARRAY_ELEMENT];
await act(() => {
selectedItem.click();
});
const buttonName = screen.getByTestId('background-dropdown-button-name');
expect(buttonName).toHaveTextContent(MODELS_MOCK[FIRST_ARRAY_ELEMENT].name);
});
});
describe('on submap selection', () => {
it('should dispatch setSelectedModelId action', async () => {
const mockStore = configureMockStore([thunk]);
const store = mockStore({
publications: PUBLICATIONS_INITIAL_STATE_MOCK,
models: { data: MODELS_MOCK, loading: 'idle', error: DEFAULT_ERROR },
});
render(
<Provider store={store}>
<FilterBySubmapHeader />
</Provider>,
);
const button = await screen.findByTestId('background-dropdown-button');
await act(() => {
button.click();
});
const listItems = screen.getAllByRole('option');
const selectedItem = listItems[FIRST_ARRAY_ELEMENT];
await act(() => {
selectedItem.click();
});
const actions = store.getActions();
expect(actions).toHaveLength(3); // 2 - getPublications (pending, fulfilled), 1 - setSelectedModelId
expect(actions[FIRST_ARRAY_ELEMENT].type).toBe('publications/setSelectedModelId');
const selectedModelId = actions[FIRST_ARRAY_ELEMENT].payload;
expect(selectedModelId).toBe(String(MODELS_MOCK[FIRST_ARRAY_ELEMENT].idObject));
});
it('should dispatch getPublications action', async () => {
const mockStore = configureMockStore([thunk]);
const store = mockStore({
publications: PUBLICATIONS_INITIAL_STATE_MOCK,
models: { data: MODELS_MOCK, loading: 'idle', error: DEFAULT_ERROR },
});
render(
<Provider store={store}>
<FilterBySubmapHeader />
</Provider>,
);
const button = await screen.findByTestId('background-dropdown-button');
await act(() => {
button.click();
});
const listItems = screen.getAllByRole('option');
const selectedItem = listItems[FIRST_ARRAY_ELEMENT];
await act(() => {
selectedItem.click();
});
const actions = store.getActions();
expect(actions).toHaveLength(3); // 2 - getPublications (pending, fulfilled), 1 - setSelectedModelId
expect(actions[1].type).toBe('publications/getPublications/pending');
});
});
});
import { ONE, ZERO } from '@/constants/common';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import {
totalSizeSelector,
paginationSelector,
isLoadingSelector,
sortColumnSelector,
sortOrderSelector,
selectedModelIdSelector,
searchValueSelector,
} from '@/redux/publications/publications.selectors';
import { getPublications } from '@/redux/publications/publications.thunks';
import { Button } from '@/shared/Button';
import {
PaginationState,
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
OnChangeFn,
} from '@tanstack/react-table';
import { useState } from 'react';
import { SortByHeader } from './SortByHeader';
import { DEFAULT_PAGE_SIZE } from './PublicationsTable.constants';
import { FilterBySubmapHeader } from './FilterBySubmapHeader/FilterBySubmapHeader.component';
export type PublicationsTableData = {
pubmedId: string;
title: string;
authors: string[];
journal: string;
year: number;
elementsOnMap: string;
submaps: string;
};
const columnHelper = createColumnHelper<PublicationsTableData>();
const columns = [
columnHelper.accessor(row => row.pubmedId, {
id: 'pubmedId',
header: () => <SortByHeader columnName="pubmedId">Pubmed ID</SortByHeader>,
size: 128,
}),
columnHelper.accessor(row => row.title, {
id: 'title',
header: () => <SortByHeader columnName="title">Title</SortByHeader>,
size: 288,
}),
columnHelper.accessor(row => row.authors, {
id: 'authors',
header: () => <SortByHeader columnName="authors">Authors</SortByHeader>,
size: 200,
}),
columnHelper.accessor(row => row.journal, {
id: 'journal',
header: () => <SortByHeader columnName="journal">Journal</SortByHeader>,
size: 168,
}),
columnHelper.accessor(row => row.year, {
id: 'year',
header: () => <SortByHeader columnName="year">Year</SortByHeader>,
size: 80,
}),
// eslint-disable-next-line @typescript-eslint/no-unused-vars
columnHelper.accessor(row => row.elementsOnMap, { header: 'Elements on map', size: 176 }),
// eslint-disable-next-line @typescript-eslint/no-unused-vars
columnHelper.accessor(row => row.submaps, {
id: 'submaps',
header: () => <FilterBySubmapHeader />,
size: 144,
}),
];
type PublicationsTableProps = {
data: PublicationsTableData[];
};
export const PublicationsTable = ({ data }: PublicationsTableProps): JSX.Element => {
const dispatch = useAppDispatch();
const pagesCount = useAppSelector(totalSizeSelector);
const isPublicationsLoading = useAppSelector(isLoadingSelector);
const sortColumn = useAppSelector(sortColumnSelector);
const sortOrder = useAppSelector(sortOrderSelector);
const selectedId = useAppSelector(selectedModelIdSelector);
const searchValue = useAppSelector(searchValueSelector);
const reduxPagination = useAppSelector(paginationSelector);
const [pagination, setPagination] = useState(reduxPagination);
const onPaginationChange: OnChangeFn<PaginationState> = updater => {
/** updating state this way is forced by table library */
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const nextState = updater(pagination);
dispatch(
getPublications({
params: {
page: nextState.pageIndex,
length: DEFAULT_PAGE_SIZE,
sortColumn,
sortOrder,
search: searchValue,
},
modelId: selectedId,
}),
);
setPagination(nextState);
};
const table = useReactTable({
state: {
pagination,
},
columns,
data,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
pageCount: pagesCount,
// onPaginationChange: setPagination,
onPaginationChange,
});
return (
<div className="flex max-h-full w-full flex-col items-center justify-center bg-white p-6">
<div className="w-full overflow-auto">
<table className="w-full min-w-[1184px] table-auto overflow-auto text-sm">
<thead className="sticky top-0 bg-white-pearl">
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id} className="border-y ">
{headerGroup.headers.map(header => (
<th
key={header.id}
className="whitespace-nowrap py-2.5"
style={{ width: header.getSize() }}
>
{flexRender(header.column.columnDef.header, header.getContext())}
</th>
))}
</tr>
))}
</thead>
<tbody>
{data &&
table.getRowModel().rows.map(row => (
<tr key={row.id} className="even:bg-lotion">
{row.getVisibleCells().map(cell => (
<td
key={cell.id}
className="p-3"
style={{
width: cell.column.getSize(),
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<div className="flex w-full flex-row justify-end border-t">
<div className="mt-6 flex flex-row items-center">
<Button
variantStyles="quiet"
className="text-primary-500"
onClick={() => table.setPageIndex(ZERO)}
disabled={isPublicationsLoading}
>
First page
</Button>
<Button
variantStyles="secondary"
onClick={() => table.previousPage()}
disabled={isPublicationsLoading}
>
Previous page
</Button>
<div className="mx-4 text-sm font-semibold">
Page {table.getState().pagination.pageIndex + ONE} out of {table.getPageCount()}
</div>
<Button
variantStyles="secondary"
onClick={() => table.nextPage()}
disabled={isPublicationsLoading}
>
Next page
</Button>
<Button
variantStyles="quiet"
className="text-primary-500"
onClick={() => table.setPageIndex(table.getPageCount() - ONE)}
disabled={isPublicationsLoading}
>
Last page
</Button>
</div>
</div>
</div>
);
};
export const DEFAULT_PAGE_SIZE = 10;