Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UISACQCOMP-171 Implement "Find location" components #737

Merged
merged 21 commits into from
Dec 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* Add Donors Filter component. Refs UISACQCOMP-169.
* Optimize acquisition memberships query to improve performance. Refs UISACQCOMP-170.
* Create Privileged donor contacts modal and list. Refs UISACQCOMP-172.
* Implement "Find location" lookup. Refs UISACQCOMP-171.

## [5.0.0](https://github.com/folio-org/stripes-acq-components/tree/v5.0.0) (2023-10-12)
[Full Changelog](https://github.com/folio-org/stripes-acq-components/compare/v4.0.2...v5.0.0)
Expand Down
82 changes: 82 additions & 0 deletions lib/FindLocation/FindLocation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import PropTypes from 'prop-types';
import { useCallback, useRef, useState } from 'react';

import { Button } from '@folio/stripes/components';

import { FindLocationLookup } from './FindLocationLookup';

export const FindLocation = (props) => {
const {
id,
disabled,
marginBottom0,
onClose,
renderTrigger,
searchButtonStyle,
searchLabel,
triggerless,
...rest
} = props;

const modalTriggerRef = useRef();
const [openModal, setOpenModal] = useState(false);

const onModalClose = useCallback((data) => {
setOpenModal(false);
onClose?.(data);
}, [onClose]);

const renderDefaultTrigger = useCallback(() => {
return (
<Button
id={id}
buttonRef={modalTriggerRef}
buttonStyle={searchButtonStyle}
disabled={disabled}
key="searchButton"
marginBottom0={marginBottom0}
onClick={() => setOpenModal(true)}
>
{searchLabel}
</Button>
);
}, [
disabled,
id,
marginBottom0,
searchButtonStyle,
searchLabel,
]);

const renderTriggerButton = useCallback(() => {
return renderTrigger
? renderTrigger({
buttonRef: modalTriggerRef,
onClick: () => setOpenModal(true),
})
: renderDefaultTrigger();
}, [renderDefaultTrigger, renderTrigger]);

return (
<>
{!triggerless && renderTriggerButton()}
{(openModal || triggerless) && (
<FindLocationLookup
onClose={onModalClose}
{...rest}
/>
)}
</>
);
};

FindLocation.propTypes = {
id: PropTypes.string.isRequired,
disabled: PropTypes.bool,
marginBottom0: PropTypes.bool,
onClose: PropTypes.func,
renderTrigger: PropTypes.func,
searchButtonStyle: PropTypes.string,
searchLabel: PropTypes.string,
triggerless: PropTypes.bool,
};
125 changes: 125 additions & 0 deletions lib/FindLocation/FindLocation.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import user from '@testing-library/user-event';
import { act, render, screen } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from 'react-query';

import {
campus,
institution,
library,
location,
} from '../../test/jest/fixtures';
import {
useCampuses,
useInstitutions,
useLibraries,
} from '../hooks';
import { fetchAllRecords } from '../utils';
import { FindLocation } from './FindLocation';

jest.mock('../AcqList', () => ({
...jest.requireActual('../AcqList'),
useFiltersToogle: () => ({ isFiltersOpened: true, toggleFilters: jest.fn }),
}));
jest.mock('../hooks', () => ({
...jest.requireActual('../hooks'),
useInstitutions: jest.fn(),
useCampuses: jest.fn(),
useLibraries: jest.fn(),
}));
jest.mock('../utils', () => ({
...jest.requireActual('../utils'),
fetchAllRecords: jest.fn(),
}));

const defaultProps = {
id: 'lookup',
searchLabel: 'Search locations',
onClose: jest.fn(),
onRecordsSelect: jest.fn(),
};

const queryClient = new QueryClient();

// eslint-disable-next-line react/prop-types
const wrapper = ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);

const renderFindLocation = (props = {}) => render(
<FindLocation
{...defaultProps}
{...props}
/>,
{ wrapper },
);

describe('FindLocation', () => {
beforeEach(() => {
useInstitutions
.mockClear()
.mockReturnValue({ institutions: [institution] });
useCampuses
.mockClear()
.mockReturnValue({ campuses: [campus] });
useLibraries
.mockClear()
.mockReturnValue({ libraries: [library] });
fetchAllRecords
.mockClear()
.mockReturnValue([location]);
});

it('should render locations lookup trigger', () => {
renderFindLocation();

expect(screen.getByText(defaultProps.searchLabel)).toBeInTheDocument();
});

it('should open locations lookup when trigger clicked', async () => {
renderFindLocation();

await act(async () => user.click(screen.getByText(defaultProps.searchLabel)));

expect(screen.getByText('stripes-acq-components.find-location.modal.label')).toBeInTheDocument();
});

it('should open triggerless locations lookup', async () => {
renderFindLocation({ triggerless: true });

expect(screen.getByText('stripes-acq-components.find-location.modal.label')).toBeInTheDocument();
});

it('should open locations lookup by click on custom trigger', async () => {
const triggerLabel = 'Custom trigger';

renderFindLocation({
renderTrigger: ({ buttonRef, onClick }) => (
<button
type="button"
ref={buttonRef}
onClick={onClick}
>
{triggerLabel}
</button>
),
});

expect(screen.getByText(triggerLabel)).toBeInTheDocument();

await act(async () => user.click(screen.getByText(triggerLabel)));

expect(screen.getByText('stripes-acq-components.find-location.modal.label')).toBeInTheDocument();
});

it('should close the modal on dismiss', async () => {
renderFindLocation();

await act(async () => user.click(screen.getByText(defaultProps.searchLabel)));
await act(async () => user.click(screen.getByText('stripes-acq-components.button.close')));

expect(defaultProps.onClose).toHaveBeenCalled();
expect(screen.queryByText('stripes-acq-components.find-location.modal.label')).not.toBeInTheDocument();
});
});
58 changes: 58 additions & 0 deletions lib/FindLocation/FindLocationFilters/CampusesFilter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import PropTypes from 'prop-types';

import { MultiSelectionFilter } from '@folio/stripes/smart-components';

import { FilterAccordion } from '../../FilterAccordion';

export const CampusesFilter = ({
activeFilters,
closedByDefault,
disabled,
id,
labelId,
name,
onChange,
options,
}) => {
return (
<FilterAccordion
id={id}
activeFilters={activeFilters}
closedByDefault={closedByDefault}
disabled={disabled}
labelId={labelId}
name={name}
onChange={onChange}
>
<MultiSelectionFilter
ariaLabelledBy={`accordion-toggle-button-${id}`}
dataOptions={options}
disabled={disabled}
id="campuses-filter"
name={name}
onChange={onChange}
selectedValues={activeFilters}
/>
</FilterAccordion>
);
};

CampusesFilter.propTypes = {
activeFilters: PropTypes.arrayOf(PropTypes.string),
closedByDefault: PropTypes.bool,
disabled: PropTypes.bool,
id: PropTypes.string.isRequired,
labelId: PropTypes.string,
name: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
options: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.string,
value: PropTypes.string,
})).isRequired,
};

CampusesFilter.defaultProps = {
closedByDefault: false,
disabled: false,
labelId: 'stripes-acq-components.filter.campus',
};
106 changes: 106 additions & 0 deletions lib/FindLocation/FindLocationFilters/FindLocationFilters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import PropTypes from 'prop-types';
import { useCallback, useMemo } from 'react';

import { AccordionSet } from '@folio/stripes/components';

import { AcqCheckboxFilter } from '../../AcqCheckboxFilter';
import {
ASSIGNED_FILTER_OPTIONS,
FILTERS,
} from '../configs';
import { CampusesFilter } from './CampusesFilter';
import { InstitutionsFilter } from './InstitutionsFilter';
import { LibrariesFilter } from './LibrariesFilter';

const getSelectionFilterOptions = ({ id, name, code }) => ({
label: [name, code && `(${code})`].join(' '),
value: id,
});

export const FindLocationFilters = ({
activeFilters,
applyFilters,
campuses,
disabled,
institutions,
isMultiSelect,
libraries,
}) => {
const adaptedApplyFilters = useCallback(({ name, values }) => applyFilters(name, values), [applyFilters]);

const institutionOptions = useMemo(() => institutions.map(getSelectionFilterOptions), [institutions]);
const campusesOptions = useMemo(() => campuses.map(getSelectionFilterOptions), [campuses]);
const librariesOptions = useMemo(() => libraries.map(getSelectionFilterOptions), [libraries]);

return (
<AccordionSet>
<InstitutionsFilter
id={FILTERS.institutions}
activeFilters={activeFilters[FILTERS.institutions]}
name={FILTERS.institutions}
onChange={adaptedApplyFilters}
disabled={disabled}
options={institutionOptions}
/>

<CampusesFilter
id={FILTERS.campuses}
activeFilters={activeFilters[FILTERS.campuses]}
name={FILTERS.campuses}
onChange={adaptedApplyFilters}
disabled={disabled}
options={campusesOptions}
/>

<LibrariesFilter
id={FILTERS.libraries}
activeFilters={activeFilters[FILTERS.libraries]}
name={FILTERS.libraries}
onChange={adaptedApplyFilters}
disabled={disabled}
options={librariesOptions}
/>

{isMultiSelect && (
<AcqCheckboxFilter
closedByDefault={false}
id={FILTERS.isAssigned}
activeFilters={activeFilters[FILTERS.isAssigned]}
labelId="stripes-acq-components.find-location.results.column.assignment"
name={FILTERS.isAssigned}
onChange={adaptedApplyFilters}
disabled={disabled}
options={ASSIGNED_FILTER_OPTIONS}
/>
)}
</AccordionSet>
);
};

FindLocationFilters.defaultProps = {
campuses: [],
institutions: [],
libraries: [],
};

FindLocationFilters.propTypes = {
activeFilters: PropTypes.object.isRequired,
applyFilters: PropTypes.func.isRequired,
campuses: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
code: PropTypes.string,
})),
disabled: PropTypes.bool,
institutions: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
code: PropTypes.string,
})),
isMultiSelect: PropTypes.bool,
libraries: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
code: PropTypes.string,
})),
};
Loading
Loading