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-172: create Privileged donor contacts modal and list #736

Merged
merged 8 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -9,6 +9,7 @@
* Extend Donors component functionality. Refs UISACQCOMP-168.
* 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.

## [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
28 changes: 14 additions & 14 deletions lib/Donors/DonorsContainer.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { map, noop, sortBy } from 'lodash';
import {
keyBy,
map,
noop,
sortBy,
} from 'lodash';
import PropTypes from 'prop-types';
import { useMemo } from 'react';
import { useIntl } from 'react-intl';
Expand Down Expand Up @@ -28,23 +33,18 @@ export function DonorsContainer({
const intl = useIntl();
const canViewOrganizations = stripes.hasPerm('ui-organizations.view');

const donorsMap = useMemo(() => {
return donors.reduce((acc, contact) => {
acc[contact.id] = contact;

return acc;
}, {});
}, [donors]);
const donorsMap = useMemo(() => keyBy(donors, 'id'), [donors]);

const listOfDonors = useMemo(() => (fields.value || [])
.map((contactId, _index) => {
.reduce((acc, contactId, _index) => {
const contact = donorsMap?.[contactId];

return {
...(contact || { isDeleted: true }),
_index,
};
}), [donorsMap, fields.value]);
if (contact?.id) {
acc.push({ ...contact, _index });
}

return acc;
}, []), [donorsMap, fields.value]);

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

Expand Down
121 changes: 121 additions & 0 deletions lib/PrivilegedDonorContacts/ContactsContainer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import {
keyBy,
map,
sortBy,
} from 'lodash';
import PropTypes from 'prop-types';
import { useMemo } from 'react';
import { useIntl } from 'react-intl';

import { useCategories } from '../hooks';
import { acqRowFormatter } from '../utils';
import { defaultContainerVisibleColumns } from './constants';
import { PrivilegedDonorContactsList } from './PrivilegedDonorContactsList';
import { PrivilegedDonorContactsLookup } from './PrivilegedDonorContactsLookup';
import {
getContactsUrl,
getResultsFormatter,
} from './utils';

export function ContactsContainer({
columnMapping,
columnWidths,
contacts,
fields,
formatter,
id,
orgId,
setContactIds,
searchLabel,
showTriggerButton,
visibleColumns,
...rest
}) {
const intl = useIntl();
const { categories } = useCategories();

const contactsMap = useMemo(() => keyBy(contacts, 'id'), [contacts]);

const listOfDonors = useMemo(() => (fields.value || [])
.reduce((acc, contactId, _index) => {
const contact = contactsMap?.[contactId];

if (contact?.id) {
acc.push({ ...contact, _index });
}

return acc;
}, []), [contactsMap, fields.value]);

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

const resultsFormatter = useMemo(() => {
return formatter || getResultsFormatter({ intl, fields, categoriesDict: categories });
}, [categories, fields, formatter, intl]);

const anchoredRowFormatter = ({ rowProps, ...restRowProps }) => {
return acqRowFormatter({
...restRowProps,
rowProps: {
...rowProps,
to: getContactsUrl(orgId, restRowProps.rowData?.id),
},
});
};

const onAddContacts = (values = []) => {
const addedDonorIds = new Set(fields.value);
const newDonorsIds = map(values.filter(({ id: donorId }) => !addedDonorIds.has(donorId)), 'id');

if (newDonorsIds.length) {
setContactIds([...addedDonorIds, ...newDonorsIds]);
usavkov-epam marked this conversation as resolved.
Show resolved Hide resolved
newDonorsIds.forEach(contactId => fields.push(contactId));
}
};

return (
<>
<PrivilegedDonorContactsList
id={id}
visibleColumns={visibleColumns}
contentData={contentData}
formatter={resultsFormatter}
columnMapping={columnMapping}
columnWidths={columnWidths}
rowFormatter={anchoredRowFormatter}
{...rest}
/>
<br />
{
showTriggerButton && (
<PrivilegedDonorContactsLookup
onAddContacts={onAddContacts}
name={id}
searchLabel={searchLabel}
columnWidths={columnWidths}
orgId={orgId}
/>
)
}
</>
);
}

ContactsContainer.propTypes = {
columnMapping: PropTypes.object,
columnWidths: PropTypes.object,
contacts: PropTypes.arrayOf(PropTypes.object),
fields: PropTypes.object,
formatter: PropTypes.object,
id: PropTypes.string,
orgId: PropTypes.string,
searchLabel: PropTypes.node,
setContactIds: PropTypes.func.isRequired,
showTriggerButton: PropTypes.bool,
visibleColumns: PropTypes.arrayOf(PropTypes.string),
};

ContactsContainer.defaultProps = {
showTriggerButton: true,
visibleColumns: defaultContainerVisibleColumns,
};
119 changes: 119 additions & 0 deletions lib/PrivilegedDonorContacts/ContactsContainer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { MemoryRouter } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import user from '@testing-library/user-event';

import stripesFinalForm from '@folio/stripes/final-form';

import { useCategories } from '../hooks';
import { ContactsContainer } from './ContactsContainer';

const mockVendor = { id: '1', name: 'Amazon' };

jest.mock('./PrivilegedDonorContactsList', () => ({
PrivilegedDonorContactsList: jest.fn(({ contentData }) => {
return (
<div>
{contentData.map(({ name }) => (
<div key={name}>{name}</div>
))}
</div>
);
}),
}));

jest.mock('./PrivilegedDonorContactsLookup', () => ({
PrivilegedDonorContactsLookup: jest.fn(({ onAddContacts }) => {
return (
<div>
<button
type="button"
onClick={() => onAddContacts([mockVendor])}
>
Add donor
</button>
</div>
);
}),
}));

const setContactIds = jest.fn();

jest.mock('../hooks', () => ({
useCategories: jest.fn().mockReturnValue({
categories: [],
isLoading: false,
}),
}));

const defaultProps = {
columnMapping: {},
columnWidths: {},
contacts: [],
fields: {
value: [
'1',
'2',
],
},
formatter: {},
id: 'donors',
setContactIds,
searchLabel: 'Search',
showTriggerButton: true,
visibleColumns: ['name'],
};

const renderForm = (props = {}) => (
<form>
<ContactsContainer
{...defaultProps}
{...props}
/>
<button type="submit">Submit</button>
</form>
);

const FormCmpt = stripesFinalForm({})(renderForm);

const renderComponent = (props = {}) => (render(
<MemoryRouter>
<FormCmpt onSubmit={() => { }} {...props} />
</MemoryRouter>,
));

describe('ContactsContainer', () => {
beforeEach(() => {
useCategories.mockClear().mockReturnValue({
donors: [],
isLoading: false,
});
});

it('should render component', () => {
renderComponent();

expect(screen.getByText('Add donor')).toBeDefined();
});

it('should call `setContactIds` when `onAddContacts` is called', () => {
renderComponent({
donors: [mockVendor],
fields: {
value: [],
push: jest.fn(),
},
});

const addDonorsButton = screen.getByText('Add donor');

expect(addDonorsButton).toBeDefined();
user.click(addDonorsButton);
expect(setContactIds).toHaveBeenCalled();
});

it('should not render `DonorsLookup` when `showTriggerButton` is false', () => {
renderComponent({ showTriggerButton: false });

expect(screen.queryByText('Add donor')).toBeNull();
});
});
61 changes: 61 additions & 0 deletions lib/PrivilegedDonorContacts/PrivilegedDonorContacts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { noop } from 'lodash';
import PropTypes from 'prop-types';
import { useEffect, useState } from 'react';
import { FieldArray } from 'react-final-form-arrays';

import {
Col,
Row,
} from '@folio/stripes/components';

import { defaultColumnMapping } from './constants';
import { useFetchPrivilegedContacts } from './hooks';
import { ContactsContainer } from './ContactsContainer';

export function PrivilegedDonorContacts({ name, privilegedContactIds, onChange, ...rest }) {
const [contactIds, setContactIds] = useState(privilegedContactIds);
const { contacts, isLoading } = useFetchPrivilegedContacts(contactIds, { keepPreviousData: true });

useEffect(() => {
setContactIds(privilegedContactIds);
}, [privilegedContactIds]);

const onSetContactIds = (values = []) => {
setContactIds(values);
onChange(values);
};

return (
<Row>
<Col xs={12}>
<FieldArray
name={name}
id={name}
component={ContactsContainer}
setContactIds={onSetContactIds}
contacts={contacts}
loading={isLoading}
{...rest}
/>
</Col>
</Row>
);
}

PrivilegedDonorContacts.propTypes = {
columnMapping: PropTypes.object,
columnWidths: PropTypes.object,
privilegedContactIds: PropTypes.arrayOf(PropTypes.string),
name: PropTypes.string,
onChange: PropTypes.func,
searchLabel: PropTypes.node,
showTriggerButton: PropTypes.bool,
visibleColumns: PropTypes.arrayOf(PropTypes.string),
};

PrivilegedDonorContacts.defaultProps = {
columnMapping: defaultColumnMapping,
privilegedContactIds: [],
name: 'privilegedContacts',
onChange: noop,
};
Loading
Loading