Skip to content

Commit 0c31922

Browse files
authored
UISACQCOMP-169: add donor filters component (#732)
* UISACQCOMP-169: add donor filters component * handle clear all functionality * test: add test coverage * tests: add test coverage * update donors list behavior * fix: export names
1 parent fb7b054 commit 0c31922

File tree

8 files changed

+212
-1
lines changed

8 files changed

+212
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* View the list of donors. Refs UISACQCOMP-166.
88
* Added `indexRef` and `inputRef` props to `<SingleSearchForm>`. Refs UISACQCOMP-167.
99
* Extend Donors component functionality. Refs UISACQCOMP-168.
10+
* Add Donors Filter component. Refs UISACQCOMP-169.
1011

1112
## [5.0.0](https://github.com/folio-org/stripes-acq-components/tree/v5.0.0) (2023-10-12)
1213
[Full Changelog](https://github.com/folio-org/stripes-acq-components/compare/v4.0.2...v5.0.0)

lib/Donors/DonorsLookup.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const DonorsLookup = ({
2222
onAddDonors,
2323
searchLabel,
2424
visibleColumns,
25+
searchButtonStyle,
2526
}) => {
2627
const stripes = useStripes();
2728

@@ -32,7 +33,7 @@ export const DonorsLookup = ({
3233
type="find-organization"
3334
dataKey="organization"
3435
searchLabel={searchLabel}
35-
searchButtonStyle="default"
36+
searchButtonStyle={searchButtonStyle}
3637
disableRecordCreation
3738
stripes={stripes}
3839
selectVendor={onAddDonors}
@@ -59,9 +60,11 @@ DonorsLookup.propTypes = {
5960
searchLabel: PropTypes.node,
6061
visibleColumns: PropTypes.arrayOf(PropTypes.string),
6162
columnWidths: PropTypes.object,
63+
searchButtonStyle: PropTypes.string,
6264
};
6365

6466
DonorsLookup.defaultProps = {
6567
searchLabel: <FormattedMessage id="stripes-acq-components.donors.button.addDonor" />,
6668
visibleColumns: pluginVisibleColumns,
69+
searchButtonStyle: 'default',
6770
};

lib/Donors/DonorsLookup.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ describe('DonorsLookup', () => {
4141
const addDonorsButton = screen.getByText('Add donor');
4242

4343
expect(addDonorsButton).toBeDefined();
44+
4445
await user.click(addDonorsButton);
4546
expect(mockOnAddDonors).toHaveBeenCalledWith([mockVendorData]);
4647
});
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { map, sortBy, uniqBy } from 'lodash';
2+
import {
3+
useCallback,
4+
useEffect,
5+
useMemo,
6+
useState,
7+
} from 'react';
8+
import PropTypes from 'prop-types';
9+
import { FormattedMessage, useIntl } from 'react-intl';
10+
11+
import { MultiSelection } from '@folio/stripes/components';
12+
13+
import {
14+
DonorsLookup,
15+
useFetchDonors,
16+
} from '../Donors';
17+
import { FilterAccordion } from '../FilterAccordion';
18+
19+
export const PluggableDonorsFilter = ({
20+
id,
21+
activeFilters,
22+
closedByDefault,
23+
disabled,
24+
labelId,
25+
name,
26+
onChange,
27+
}) => {
28+
const { donors } = useFetchDonors(activeFilters);
29+
const [selectedDonors, setSelectedDonors] = useState([]);
30+
31+
useEffect(() => {
32+
// set initial selected donors from active filters prop
33+
if (activeFilters?.length && !selectedDonors?.length) {
34+
setSelectedDonors(donors);
35+
}
36+
}, [activeFilters, donors, selectedDonors]);
37+
38+
const onSelectDonor = useCallback((values) => {
39+
const updatedDonorIds = uniqBy([...donors, ...values], 'id');
40+
41+
setSelectedDonors(updatedDonorIds);
42+
onChange({
43+
name,
44+
values: map(updatedDonorIds, 'id'),
45+
});
46+
}, [donors, name, onChange]);
47+
48+
const onRemove = useCallback((donor) => {
49+
const updatedDonorIds = selectedDonors.filter(({ id: donorId }) => donorId !== donor.value);
50+
51+
setSelectedDonors(updatedDonorIds);
52+
onChange({
53+
name,
54+
values: map(updatedDonorIds, 'id'),
55+
});
56+
}, [name, onChange, selectedDonors]);
57+
58+
const intl = useIntl();
59+
const label = intl.formatMessage({ id: labelId });
60+
const dataOptions = useMemo(() => {
61+
return selectedDonors.map(donor => ({ value: donor.id, label: donor.name }));
62+
}, [selectedDonors]);
63+
64+
const onClearAll = () => {
65+
onChange({
66+
name,
67+
values: [],
68+
});
69+
setSelectedDonors([]);
70+
};
71+
72+
return (
73+
<FilterAccordion
74+
activeFilters={activeFilters}
75+
closedByDefault={closedByDefault}
76+
disabled={disabled}
77+
id={id}
78+
labelId={labelId}
79+
name={name}
80+
onChange={onClearAll}
81+
>
82+
<MultiSelection
83+
id="input-tag"
84+
aria-label={label}
85+
disabled
86+
dataOptions={sortBy(dataOptions, ['value'])}
87+
value={sortBy(dataOptions, ['value'])}
88+
onRemove={onRemove}
89+
/>
90+
<DonorsLookup
91+
searchButtonStyle="link"
92+
onAddDonors={onSelectDonor}
93+
searchLabel={<FormattedMessage id="stripes-acq-components.filter.donor.lookup" />}
94+
/>
95+
</FilterAccordion>
96+
);
97+
};
98+
99+
PluggableDonorsFilter.propTypes = {
100+
activeFilters: PropTypes.arrayOf(PropTypes.string),
101+
closedByDefault: PropTypes.bool,
102+
disabled: PropTypes.bool,
103+
id: PropTypes.string,
104+
labelId: PropTypes.string.isRequired,
105+
name: PropTypes.string.isRequired,
106+
onChange: PropTypes.func.isRequired,
107+
};
108+
109+
PluggableDonorsFilter.defaultProps = {
110+
closedByDefault: true,
111+
disabled: false,
112+
};
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { QueryClient, QueryClientProvider } from 'react-query';
2+
import { render } from '@testing-library/react';
3+
import user from '@testing-library/user-event';
4+
5+
import { PluggableDonorsFilter } from './PluggableDonorsFilter';
6+
7+
const mockVendorData = { id: '1', name: 'Amazon' };
8+
9+
jest.mock('../Donors', () => ({
10+
...jest.requireActual('../Donors'),
11+
useFetchDonors: jest.fn(() => ({
12+
isLoading: false,
13+
donors: [{ id: '1', name: 'Amazon' }],
14+
})),
15+
DonorsLookup: jest.fn(({ children, ...rest }) => {
16+
return (
17+
<div>
18+
{children}
19+
<button
20+
type="button"
21+
id={rest?.name}
22+
onClick={() => rest?.onAddDonors([mockVendorData])}
23+
>
24+
Add donor
25+
</button>
26+
</div>
27+
);
28+
}),
29+
}));
30+
31+
const labelId = 'ui-orders.orderDetails.assignedTo';
32+
33+
const queryClient = new QueryClient();
34+
35+
const wrapper = ({ children }) => (
36+
<QueryClientProvider client={queryClient}>
37+
{children}
38+
</QueryClientProvider>
39+
);
40+
41+
const renderFilter = (props) => (render(
42+
<PluggableDonorsFilter
43+
id="donor"
44+
activeFilters={[]}
45+
labelId={labelId}
46+
name={mockVendorData.name}
47+
onChange={() => {}}
48+
{...props}
49+
/>,
50+
{ wrapper },
51+
));
52+
53+
describe('PluggableDonorsFilter', () => {
54+
it('should render component', () => {
55+
const { getAllByText } = renderFilter();
56+
57+
expect(getAllByText(labelId)).toHaveLength(2);
58+
});
59+
60+
it('should add donor', () => {
61+
const mockOnAddDonors = jest.fn();
62+
const { getByText } = renderFilter({ onChange: mockOnAddDonors });
63+
64+
const addDonorsButton = getByText('Add donor');
65+
66+
expect(addDonorsButton).toBeInTheDocument();
67+
user.click(addDonorsButton);
68+
expect(mockOnAddDonors).toHaveBeenCalledWith({
69+
name: mockVendorData.name,
70+
values: [mockVendorData.id],
71+
});
72+
});
73+
74+
it('should clear all donors', () => {
75+
const mockOnAddDonors = jest.fn();
76+
const { getAllByRole } = renderFilter({
77+
onChange: mockOnAddDonors,
78+
activeFilters: [mockVendorData.id],
79+
});
80+
81+
const clearAllButton = getAllByRole('button')[1];
82+
83+
expect(clearAllButton).toBeInTheDocument();
84+
user.click(clearAllButton);
85+
expect(mockOnAddDonors).toHaveBeenCalledWith({
86+
name: mockVendorData.name,
87+
values: [],
88+
});
89+
});
90+
});

lib/DonorsFilter/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { PluggableDonorsFilter } from './PluggableDonorsFilter';

lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export * from './CurrencyExchangeRateFields';
1717
export * from './CurrencySymbol';
1818
export * from './DeleteHoldingsModal';
1919
export * from './Donors';
20+
export * from './DonorsFilter';
2021
export * from './DragDropMCL';
2122
export * from './DynamicSelection';
2223
export * from './DynamicSelectionFilter';

translations/stripes-acq-components/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@
107107
"organization": "Organization",
108108
"filter.organization.lookup": "Organization look-up",
109109
"filter.organization.lookupNoSupport": "Organization look-up is not supported",
110+
"filter.donor.lookup": "Donor look-up",
111+
"filter.donor.lookupNoSupport": "Donor look-up is not supported",
110112
"filter.user.lookup": "User look-up",
111113
"filter.user.lookupNoSupport": "user look-up is not supported",
112114
"filter.expenseClass": "Expense class",

0 commit comments

Comments
 (0)