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

feat(publications): filter by model selector

parent e4366cb2
No related branches found
No related tags found
3 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
Pipeline #85117 passed
Showing
with 333 additions and 32 deletions
......@@ -17,4 +17,13 @@ describe('Publications Modal - component', () => {
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', () => {});
});
});
......@@ -31,8 +31,10 @@ export const PublicationsModal = (): JSX.Element => {
}, [data, mapsNames]);
useEffect(() => {
dispatch(getPublications({}));
}, [dispatch]);
if (!data) {
dispatch(getPublications({ params: {} }));
}
}, [data, dispatch]);
return (
<div className="flex w-full flex-1 flex-col items-center justify-center overflow-hidden bg-white">
......
......@@ -18,7 +18,7 @@ export const PublicationsSearch = (): JSX.Element => {
};
useEffect(() => {
dispatch(getPublications({ search: debouncedValue }));
dispatch(getPublications({ params: { search: debouncedValue } }));
}, [dispatch, debouncedValue]);
return (
......
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 { 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 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,
// TODO
// search: get search from redux
},
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');
});
});
});
......@@ -7,6 +7,7 @@ import {
isLoadingSelector,
sortColumnSelector,
sortOrderSelector,
selectedModelIdSelector,
} from '@/redux/publications/publications.selectors';
import { getPublications } from '@/redux/publications/publications.thunks';
import { Button } from '@/shared/Button';
......@@ -21,6 +22,7 @@ import {
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;
......@@ -63,7 +65,11 @@ const columns = [
// 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, { header: 'Submaps', size: 144 }),
columnHelper.accessor(row => row.submaps, {
id: 'submaps',
header: () => <FilterBySubmapHeader />,
size: 144,
}),
];
type PublicationsTableProps = {
......@@ -76,6 +82,7 @@ export const PublicationsTable = ({ data }: PublicationsTableProps): JSX.Element
const isPublicationsLoading = useAppSelector(isLoadingSelector);
const sortColumn = useAppSelector(sortColumnSelector);
const sortOrder = useAppSelector(sortOrderSelector);
const selectedId = useAppSelector(selectedModelIdSelector);
const reduxPagination = useAppSelector(paginationSelector);
const [pagination, setPagination] = useState(reduxPagination);
......@@ -89,14 +96,18 @@ export const PublicationsTable = ({ data }: PublicationsTableProps): JSX.Element
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const nextState = updater(pagination);
dispatch(
getPublications({
page: nextState.pageIndex,
length: DEFAULT_PAGE_SIZE,
sortColumn,
sortOrder,
// TODO
// search: get search from redux
params: {
page: nextState.pageIndex,
length: DEFAULT_PAGE_SIZE,
sortColumn,
sortOrder,
// TODO
// search: get search from redux
},
modelId: selectedId,
}),
);
setPagination(nextState);
......
......@@ -31,12 +31,14 @@ export const SortByHeader = ({ columnName, children }: SortByHeaderProps): JSX.E
dispatch(
getPublications({
page: 0,
length: DEFAULT_PAGE_SIZE,
sortColumn: columnName,
sortOrder: newSortDirection,
// TODO
// search: get search from redux
params: {
page: 0,
length: DEFAULT_PAGE_SIZE,
sortColumn: columnName,
sortOrder: newSortDirection,
// TODO
// search: get search from redux
},
}),
);
};
......
import { PROJECT_ID } from '@/constants';
import { PerfectSearchParams } from '@/types/search';
import { Point } from '@/types/map';
export type GetPublicationsParams = {
page?: number;
sortColumn?: string;
sortOrder?: string;
level?: number;
length?: number;
search?: string;
};
import { GetPublicationsParams, PublicationsQueryParams } from './publications/publications.types';
const getPublicationsURLSearchParams = ({
page,
......@@ -18,7 +10,7 @@ const getPublicationsURLSearchParams = ({
level,
length,
search,
}: GetPublicationsParams): URLSearchParams => {
}: PublicationsQueryParams): URLSearchParams => {
const params = new URLSearchParams();
if (page) params.append('page', page.toString());
if (sortColumn) params.append('sortColumn', sortColumn);
......@@ -74,7 +66,7 @@ export const apiPath = {
getSourceFile: (): string => `/projects/${PROJECT_ID}:downloadSource`,
getMesh: (meshId: string): string => `mesh/${meshId}`,
getTaxonomy: (taxonomyId: string): string => `taxonomy/${taxonomyId}`,
getPublications: (params: GetPublicationsParams, modelId = '*'): string =>
getPublications: ({ params, modelId = '*' }: GetPublicationsParams): string =>
`/projects/${PROJECT_ID}/models/${modelId}/publications/?${getPublicationsURLSearchParams(
params,
)}`,
......
......@@ -24,6 +24,10 @@ export const modelsNameMapSelector = createSelector(modelsDataSelector, models =
),
);
export const modelsIdsAndNamesSelector = createSelector(modelsDataSelector, models =>
models.map(({ idObject, name }) => ({ id: idObject, name })),
);
export const currentModelIdSelector = createSelector(
currentModelSelector,
model => model?.idObject || MODEL_ID_DEFAULT,
......
......@@ -6,4 +6,5 @@ export const PUBLICATIONS_INITIAL_STATE_MOCK: PublicationsState = {
error: { name: '', message: '' },
sortColumn: '',
sortOrder: 'asc',
selectedModelId: undefined,
};
......@@ -25,3 +25,10 @@ export const setSortOrderAndColumnReducer = (
state.sortColumn = action.payload.sortColumn;
state.sortOrder = action.payload.sortOrder;
};
export const setSelectedModelIdReducer = (
state: PublicationsState,
action: PayloadAction<string | undefined>,
): void => {
state.selectedModelId = action.payload;
};
......@@ -41,3 +41,8 @@ export const sortOrderSelector = createSelector(
publicationsSelector,
publications => publications.sortOrder,
);
export const selectedModelIdSelector = createSelector(
publicationsSelector,
publications => publications.selectedModelId,
);
import { createSlice } from '@reduxjs/toolkit';
import { PublicationsState } from './publications.types';
import { getPublicationsReducer, setSortOrderAndColumnReducer } from './publications.reducers';
import {
getPublicationsReducer,
setSortOrderAndColumnReducer,
setSelectedModelIdReducer,
} from './publications.reducers';
const initialState: PublicationsState = {
data: undefined,
......@@ -15,12 +19,13 @@ const publicationsSlice = createSlice({
initialState,
reducers: {
setSortOrderAndColumn: setSortOrderAndColumnReducer,
setSelectedModelId: setSelectedModelIdReducer,
},
extraReducers: builder => {
getPublicationsReducer(builder);
},
});
export const { setSortOrderAndColumn } = publicationsSlice.actions;
export const { setSortOrderAndColumn, setSelectedModelId } = publicationsSlice.actions;
export default publicationsSlice.reducer;
......@@ -3,7 +3,8 @@ 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';
import { GetPublicationsParams } from './publications.types';
import { apiPath } from '../apiPath';
export const getPublications = createAsyncThunk(
'publications/getPublications',
......
......@@ -7,13 +7,19 @@ export type SortOrder = 'asc' | 'desc';
export type PublicationsState = FetchDataState<PublicationsResponse> & {
sortColumn: SortColumn;
sortOrder: SortOrder;
selectedModelId?: string;
};
export type GetPublicationsParams = {
start?: number;
export type PublicationsQueryParams = {
page?: number;
sortColumn?: string;
sortOrder?: string;
level?: number;
length?: number;
search?: string;
};
export type GetPublicationsParams = {
params: PublicationsQueryParams;
modelId?: string;
};
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