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

feat(publications): search bar && layout modal

parent 8fc1b463
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 #85146 passed
Showing
with 208 additions and 156 deletions
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';
const MolArtModal = dynamic(
() => import('./MolArtModal/MolArtModal.component').then(mod => mod.MolArtModal),
......@@ -16,40 +12,26 @@ 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>
)}
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>
{isOpen && modalName === 'overview-images' && <OverviewImagesModal />}
{isOpen && modalName === 'mol-art' && <MolArtModal />}
{isOpen && modalName === 'login' && <LoginModal />}
{isOpen && modalName === 'publications' && <PublicationsModal />}
</div>
</div>
</div>
{isOpen && modalName === 'publications' && <PublicationsModal />}
</>
);
};
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';
import Image from 'next/image';
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 spinnerIcon from '@/assets/vectors/icons/spinner.svg';
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();
......@@ -37,19 +37,10 @@ export const PublicationsModal = (): JSX.Element => {
}, [data, dispatch]);
return (
<div className="flex w-full flex-1 flex-col items-center justify-center overflow-hidden bg-white">
{/* <PublicationsSearch /> */}
{data ? (
<PublicationsTable data={parsedData} />
) : (
<Image
src={spinnerIcon}
alt="spinner icon"
height={40}
width={40}
className="animate-spin"
/>
)}
</div>
<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 const MODAL_ROLE = 'modal';
export { PublicationsModalLayout } from './PublicationsModalLayout.component';
import { ChangeEvent, useEffect, useState } from 'react';
import { ChangeEvent, useCallback, useEffect, useState } from 'react';
import lensIcon from '@/assets/vectors/icons/lens.svg';
import { useDebounce } from '@/hooks/useDebounce';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { getPublications } from '@/redux/publications/publications.thunks';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { isLoadingSelector } from '@/redux/publications/publications.selectors';
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';
export const PublicationsSearch = (): JSX.Element => {
const dispatch = useAppDispatch();
const isLoading = useAppSelector(isLoadingSelector);
const [value, setValue] = useState('');
const debouncedValue = useDebounce<string>(value);
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(getPublications({ params: { search: debouncedValue } }));
}, [dispatch, debouncedValue]);
dispatch(setPublicationSearchValue(debouncedValue));
handleSearch();
}, [debouncedValue, dispatch, handleSearch]);
return (
<div className="mt-5">
<div className="relative mr-4">
<input
value={value}
name="search-input"
......
......@@ -3,7 +3,11 @@ 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 {
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';
......@@ -14,6 +18,7 @@ export const FilterBySubmapHeader = (): JSX.Element => {
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;
......@@ -27,8 +32,7 @@ export const FilterBySubmapHeader = (): JSX.Element => {
length: DEFAULT_PAGE_SIZE,
sortColumn,
sortOrder,
// TODO
// search: get search from redux
search: searchValue,
},
modelId: newModelId,
}),
......
......@@ -8,6 +8,7 @@ import {
sortColumnSelector,
sortOrderSelector,
selectedModelIdSelector,
searchValueSelector,
} from '@/redux/publications/publications.selectors';
import { getPublications } from '@/redux/publications/publications.thunks';
import { Button } from '@/shared/Button';
......@@ -83,14 +84,11 @@ export const PublicationsTable = ({ data }: PublicationsTableProps): JSX.Element
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);
// useEffect(() => {
// dispatch(getPublications({ page: pagination.pageIndex, length: DEFAULT_PAGE_SIZE }));
// }, [pagination, dispatch]);
const onPaginationChange: OnChangeFn<PaginationState> = updater => {
/** updating state this way is forced by table library */
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
......@@ -104,8 +102,7 @@ export const PublicationsTable = ({ data }: PublicationsTableProps): JSX.Element
length: DEFAULT_PAGE_SIZE,
sortColumn,
sortOrder,
// TODO
// search: get search from redux
search: searchValue,
},
modelId: selectedId,
}),
......
......@@ -3,7 +3,10 @@ import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { setSortOrderAndColumn } from '@/redux/publications/publications.slice';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { Icon } from '@/shared/Icon';
import { sortColumnSelector } from '@/redux/publications/publications.selectors';
import {
searchValueSelector,
sortColumnSelector,
} from '@/redux/publications/publications.selectors';
import { SortColumn, SortOrder } from '@/redux/publications/publications.types';
import { getPublications } from '@/redux/publications/publications.thunks';
import { DEFAULT_PAGE_SIZE } from '../PublicationsTable.constants';
......@@ -16,6 +19,7 @@ type SortByHeaderProps = {
export const SortByHeader = ({ columnName, children }: SortByHeaderProps): JSX.Element => {
const activeColumn = useAppSelector(sortColumnSelector);
const [sortDirection, setSortDirection] = useState<SortOrder | undefined>();
const searchValue = useAppSelector(searchValueSelector);
const dispatch = useAppDispatch();
// if columnName is the same as the current sortColumn, then sort in the opposite direction
......@@ -36,8 +40,7 @@ export const SortByHeader = ({ columnName, children }: SortByHeaderProps): JSX.E
length: DEFAULT_PAGE_SIZE,
sortColumn: columnName,
sortOrder: newSortDirection,
// TODO
// search: get search from redux
search: searchValue,
},
}),
);
......
......@@ -6,5 +6,6 @@ export const PUBLICATIONS_INITIAL_STATE_MOCK: PublicationsState = {
error: { name: '', message: '' },
sortColumn: '',
sortOrder: 'asc',
searchValue: '',
selectedModelId: undefined,
};
......@@ -32,3 +32,10 @@ export const setSelectedModelIdReducer = (
): void => {
state.selectedModelId = action.payload;
};
export const setSearchValueReducer = (
state: PublicationsState,
action: PayloadAction<string>,
): void => {
state.searchValue = action.payload;
};
......@@ -46,3 +46,8 @@ export const selectedModelIdSelector = createSelector(
publicationsSelector,
publications => publications.selectedModelId,
);
export const searchValueSelector = createSelector(
publicationsSelector,
publications => publications.searchValue,
);
......@@ -4,6 +4,7 @@ import {
getPublicationsReducer,
setSortOrderAndColumnReducer,
setSelectedModelIdReducer,
setSearchValueReducer,
} from './publications.reducers';
const initialState: PublicationsState = {
......@@ -12,6 +13,7 @@ const initialState: PublicationsState = {
error: { name: '', message: '' },
sortColumn: '',
sortOrder: 'asc',
searchValue: '',
};
const publicationsSlice = createSlice({
......@@ -20,12 +22,14 @@ const publicationsSlice = createSlice({
reducers: {
setSortOrderAndColumn: setSortOrderAndColumnReducer,
setSelectedModelId: setSelectedModelIdReducer,
setPublicationSearchValue: setSearchValueReducer,
},
extraReducers: builder => {
getPublicationsReducer(builder);
},
});
export const { setSortOrderAndColumn, setSelectedModelId } = publicationsSlice.actions;
export const { setSortOrderAndColumn, setSelectedModelId, setPublicationSearchValue } =
publicationsSlice.actions;
export default publicationsSlice.reducer;
......@@ -8,6 +8,7 @@ export type PublicationsState = FetchDataState<PublicationsResponse> & {
sortColumn: SortColumn;
sortOrder: SortOrder;
selectedModelId?: string;
searchValue: string;
};
export type PublicationsQueryParams = {
......
import Image from 'next/image';
import spinnerIcon from '@/assets/vectors/icons/spinner.svg';
type LoadingIndicatorProps = {
height?: number;
width?: number;
};
const DEFAULT_HEIGHT = 16;
const DEFAULT_WIDTH = 16;
export const LoadingIndicator = ({
height = DEFAULT_HEIGHT,
width = DEFAULT_WIDTH,
}: LoadingIndicatorProps): JSX.Element => (
<Image
src={spinnerIcon}
alt="spinner icon"
height={height}
width={width}
className="animate-spin"
/>
);
export { LoadingIndicator } from './LoadingIndicator.component';
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