Skip to content

Commit 5a63395

Browse files
authored
UISACQCOMP-172: create Privileged donor contacts modal and list (#736)
* UISACQCOMP-172: create Privileged donor contacts modal and list * tests: add test cases * tests: add test coverages for hooks * tests: add test coverages * add selectedContactIds to filter out already added contacts * refactor code according to the comments * refactor: improve code quality * tests: fix failing test
1 parent 425a3ff commit 5a63395

31 files changed

+1140
-15
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* Extend Donors component functionality. Refs UISACQCOMP-168.
1010
* Add Donors Filter component. Refs UISACQCOMP-169.
1111
* Optimize acquisition memberships query to improve performance. Refs UISACQCOMP-170.
12+
* Create Privileged donor contacts modal and list. Refs UISACQCOMP-172.
1213

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

lib/Donors/DonorsContainer.js

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { map, noop, sortBy } from 'lodash';
1+
import {
2+
keyBy,
3+
map,
4+
noop,
5+
sortBy,
6+
} from 'lodash';
27
import PropTypes from 'prop-types';
38
import { useMemo } from 'react';
49
import { useIntl } from 'react-intl';
@@ -28,23 +33,18 @@ export function DonorsContainer({
2833
const intl = useIntl();
2934
const canViewOrganizations = stripes.hasPerm('ui-organizations.view');
3035

31-
const donorsMap = useMemo(() => {
32-
return donors.reduce((acc, contact) => {
33-
acc[contact.id] = contact;
34-
35-
return acc;
36-
}, {});
37-
}, [donors]);
36+
const donorsMap = useMemo(() => keyBy(donors, 'id'), [donors]);
3837

3938
const listOfDonors = useMemo(() => (fields.value || [])
40-
.map((contactId, _index) => {
39+
.reduce((acc, contactId, _index) => {
4140
const contact = donorsMap?.[contactId];
4241

43-
return {
44-
...(contact || { isDeleted: true }),
45-
_index,
46-
};
47-
}), [donorsMap, fields.value]);
42+
if (contact?.id) {
43+
acc.push({ ...contact, _index });
44+
}
45+
46+
return acc;
47+
}, []), [donorsMap, fields.value]);
4848

4949
const contentData = useMemo(() => sortBy(listOfDonors, [({ name }) => name?.toLowerCase()]), [listOfDonors]);
5050

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import {
2+
keyBy,
3+
map,
4+
sortBy,
5+
} from 'lodash';
6+
import PropTypes from 'prop-types';
7+
import { useMemo } from 'react';
8+
import { useIntl } from 'react-intl';
9+
10+
import { useCategories } from '../hooks';
11+
import { acqRowFormatter } from '../utils';
12+
import { defaultContainerVisibleColumns } from './constants';
13+
import { PrivilegedDonorContactsList } from './PrivilegedDonorContactsList';
14+
import { PrivilegedDonorContactsLookup } from './PrivilegedDonorContactsLookup';
15+
import {
16+
getContactsUrl,
17+
getResultsFormatter,
18+
} from './utils';
19+
20+
export function ContactsContainer({
21+
columnMapping,
22+
columnWidths,
23+
contacts,
24+
fields,
25+
formatter,
26+
id,
27+
orgId,
28+
setContactIds,
29+
searchLabel,
30+
showTriggerButton,
31+
visibleColumns,
32+
...rest
33+
}) {
34+
const intl = useIntl();
35+
const { categories } = useCategories();
36+
37+
const contactsMap = useMemo(() => keyBy(contacts, 'id'), [contacts]);
38+
39+
const listOfDonors = useMemo(() => (fields.value || [])
40+
.reduce((acc, contactId, _index) => {
41+
const contact = contactsMap?.[contactId];
42+
43+
if (contact?.id) {
44+
acc.push({ ...contact, _index });
45+
}
46+
47+
return acc;
48+
}, []), [contactsMap, fields.value]);
49+
50+
const contentData = useMemo(() => sortBy(listOfDonors, [({ lastName }) => lastName?.toLowerCase()]), [listOfDonors]);
51+
52+
const resultsFormatter = useMemo(() => {
53+
return formatter || getResultsFormatter({ intl, fields, categoriesDict: categories });
54+
}, [categories, fields, formatter, intl]);
55+
56+
const anchoredRowFormatter = ({ rowProps, ...restRowProps }) => {
57+
return acqRowFormatter({
58+
...restRowProps,
59+
rowProps: {
60+
...rowProps,
61+
to: getContactsUrl(orgId, restRowProps.rowData?.id),
62+
},
63+
});
64+
};
65+
66+
const onAddContacts = (values = []) => {
67+
const addedDonorIds = new Set(fields.value);
68+
const newDonorsIds = map(values.filter(({ id: donorId }) => !addedDonorIds.has(donorId)), 'id');
69+
70+
if (newDonorsIds.length) {
71+
setContactIds([...addedDonorIds, ...newDonorsIds]);
72+
newDonorsIds.forEach(contactId => fields.push(contactId));
73+
}
74+
};
75+
76+
return (
77+
<>
78+
<PrivilegedDonorContactsList
79+
id={id}
80+
visibleColumns={visibleColumns}
81+
contentData={contentData}
82+
formatter={resultsFormatter}
83+
columnMapping={columnMapping}
84+
columnWidths={columnWidths}
85+
rowFormatter={anchoredRowFormatter}
86+
{...rest}
87+
/>
88+
<br />
89+
{
90+
showTriggerButton && (
91+
<PrivilegedDonorContactsLookup
92+
onAddContacts={onAddContacts}
93+
name={id}
94+
searchLabel={searchLabel}
95+
columnWidths={columnWidths}
96+
orgId={orgId}
97+
/>
98+
)
99+
}
100+
</>
101+
);
102+
}
103+
104+
ContactsContainer.propTypes = {
105+
columnMapping: PropTypes.object,
106+
columnWidths: PropTypes.object,
107+
contacts: PropTypes.arrayOf(PropTypes.object),
108+
fields: PropTypes.object,
109+
formatter: PropTypes.object,
110+
id: PropTypes.string,
111+
orgId: PropTypes.string,
112+
searchLabel: PropTypes.node,
113+
setContactIds: PropTypes.func.isRequired,
114+
showTriggerButton: PropTypes.bool,
115+
visibleColumns: PropTypes.arrayOf(PropTypes.string),
116+
};
117+
118+
ContactsContainer.defaultProps = {
119+
showTriggerButton: true,
120+
visibleColumns: defaultContainerVisibleColumns,
121+
};
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { MemoryRouter } from 'react-router-dom';
2+
import { render, screen } from '@testing-library/react';
3+
import user from '@testing-library/user-event';
4+
5+
import stripesFinalForm from '@folio/stripes/final-form';
6+
7+
import { useCategories } from '../hooks';
8+
import { ContactsContainer } from './ContactsContainer';
9+
10+
const mockVendor = { id: '1', name: 'Amazon' };
11+
12+
jest.mock('./PrivilegedDonorContactsList', () => ({
13+
PrivilegedDonorContactsList: jest.fn(({ contentData }) => {
14+
return (
15+
<div>
16+
{contentData.map(({ name }) => (
17+
<div key={name}>{name}</div>
18+
))}
19+
</div>
20+
);
21+
}),
22+
}));
23+
24+
jest.mock('./PrivilegedDonorContactsLookup', () => ({
25+
PrivilegedDonorContactsLookup: jest.fn(({ onAddContacts }) => {
26+
return (
27+
<div>
28+
<button
29+
type="button"
30+
onClick={() => onAddContacts([mockVendor])}
31+
>
32+
Add donor
33+
</button>
34+
</div>
35+
);
36+
}),
37+
}));
38+
39+
const setContactIds = jest.fn();
40+
41+
jest.mock('../hooks', () => ({
42+
useCategories: jest.fn().mockReturnValue({
43+
categories: [],
44+
isLoading: false,
45+
}),
46+
}));
47+
48+
const defaultProps = {
49+
columnMapping: {},
50+
columnWidths: {},
51+
contacts: [],
52+
fields: {
53+
value: [
54+
'1',
55+
'2',
56+
],
57+
},
58+
formatter: {},
59+
id: 'donors',
60+
setContactIds,
61+
searchLabel: 'Search',
62+
showTriggerButton: true,
63+
visibleColumns: ['name'],
64+
};
65+
66+
const renderForm = (props = {}) => (
67+
<form>
68+
<ContactsContainer
69+
{...defaultProps}
70+
{...props}
71+
/>
72+
<button type="submit">Submit</button>
73+
</form>
74+
);
75+
76+
const FormCmpt = stripesFinalForm({})(renderForm);
77+
78+
const renderComponent = (props = {}) => (render(
79+
<MemoryRouter>
80+
<FormCmpt onSubmit={() => { }} {...props} />
81+
</MemoryRouter>,
82+
));
83+
84+
describe('ContactsContainer', () => {
85+
beforeEach(() => {
86+
useCategories.mockClear().mockReturnValue({
87+
donors: [],
88+
isLoading: false,
89+
});
90+
});
91+
92+
it('should render component', () => {
93+
renderComponent();
94+
95+
expect(screen.getByText('Add donor')).toBeDefined();
96+
});
97+
98+
it('should call `setContactIds` when `onAddContacts` is called', () => {
99+
renderComponent({
100+
donors: [mockVendor],
101+
fields: {
102+
value: [],
103+
push: jest.fn(),
104+
},
105+
});
106+
107+
const addDonorsButton = screen.getByText('Add donor');
108+
109+
expect(addDonorsButton).toBeDefined();
110+
user.click(addDonorsButton);
111+
expect(setContactIds).toHaveBeenCalled();
112+
});
113+
114+
it('should not render `DonorsLookup` when `showTriggerButton` is false', () => {
115+
renderComponent({ showTriggerButton: false });
116+
117+
expect(screen.queryByText('Add donor')).toBeNull();
118+
});
119+
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { noop } from 'lodash';
2+
import PropTypes from 'prop-types';
3+
import { useEffect, useState } from 'react';
4+
import { FieldArray } from 'react-final-form-arrays';
5+
6+
import {
7+
Col,
8+
Row,
9+
} from '@folio/stripes/components';
10+
11+
import { defaultColumnMapping } from './constants';
12+
import { useFetchPrivilegedContacts } from './hooks';
13+
import { ContactsContainer } from './ContactsContainer';
14+
15+
export function PrivilegedDonorContacts({ name, privilegedContactIds, onChange, ...rest }) {
16+
const [contactIds, setContactIds] = useState(privilegedContactIds);
17+
const { contacts, isLoading } = useFetchPrivilegedContacts(contactIds, { keepPreviousData: true });
18+
19+
useEffect(() => {
20+
setContactIds(privilegedContactIds);
21+
}, [privilegedContactIds]);
22+
23+
const onSetContactIds = (values = []) => {
24+
setContactIds(values);
25+
onChange(values);
26+
};
27+
28+
return (
29+
<Row>
30+
<Col xs={12}>
31+
<FieldArray
32+
name={name}
33+
id={name}
34+
component={ContactsContainer}
35+
setContactIds={onSetContactIds}
36+
contacts={contacts}
37+
loading={isLoading}
38+
{...rest}
39+
/>
40+
</Col>
41+
</Row>
42+
);
43+
}
44+
45+
PrivilegedDonorContacts.propTypes = {
46+
columnMapping: PropTypes.object,
47+
columnWidths: PropTypes.object,
48+
privilegedContactIds: PropTypes.arrayOf(PropTypes.string),
49+
name: PropTypes.string,
50+
onChange: PropTypes.func,
51+
searchLabel: PropTypes.node,
52+
showTriggerButton: PropTypes.bool,
53+
visibleColumns: PropTypes.arrayOf(PropTypes.string),
54+
};
55+
56+
PrivilegedDonorContacts.defaultProps = {
57+
columnMapping: defaultColumnMapping,
58+
privilegedContactIds: [],
59+
name: 'privilegedContacts',
60+
onChange: noop,
61+
};

0 commit comments

Comments
 (0)