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-166: configure the plugin via props #726

Merged
merged 12 commits into from
Nov 9, 2023
33 changes: 17 additions & 16 deletions lib/DonorsList/DonorsContainer.js → lib/Donors/Donors.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { FieldArray } from 'react-final-form-arrays';
import { useState } from 'react';

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

import DonorsList from './DonorsList';
import { defaultColumnMapping } from './constants';
import { DonorsContainer } from './DonorsContainer';
import { useFetchDonors } from './hooks';

function DonorsContainer({ name, donorOrganizationIds }) {
export function Donors({ name, donorOrganizationIds, ...rest }) {
const [donorIds, setDonorIds] = useState(donorOrganizationIds);
const { donors, isLoading } = useFetchDonors(donorIds);

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

return acc;
}, {});

if (isLoading) {
return <Loading />;
}
Expand All @@ -31,22 +26,28 @@ function DonorsContainer({ name, donorOrganizationIds }) {
<FieldArray
name={name}
id={name}
component={DonorsList}
component={DonorsContainer}
setDonorIds={setDonorIds}
donorsMap={donorsMap}
donors={donors}
{...rest}
/>
</Col>
</Row>
);
}

DonorsContainer.propTypes = {
name: PropTypes.string.isRequired,
Donors.propTypes = {
columnMapping: PropTypes.object,
columnWidths: PropTypes.object,
donorOrganizationIds: PropTypes.arrayOf(PropTypes.string),
name: PropTypes.string,
searchLabel: PropTypes.node,
showTriggerButton: PropTypes.bool,
visibleColumns: PropTypes.arrayOf(PropTypes.string),
};

DonorsContainer.defaultProps = {
Donors.defaultProps = {
donorOrganizationIds: [],
name: 'donorOrganizationIds',
columnMapping: defaultColumnMapping,
};

export default DonorsContainer;
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,14 @@ import { render, screen } from '@testing-library/react';

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

import DonorsContainer from './DonorsContainer';
import { Donors } from './Donors';
import { useFetchDonors } from './hooks';

jest.mock('@folio/stripes/components', () => ({
...jest.requireActual('@folio/stripes/components'),
Loading: jest.fn(() => 'Loading'),
}));

jest.mock('./DonorsList', () => jest.fn(({ donorsMap }) => {
if (!Object.values(donorsMap).length) {
return 'stripes-components.tableEmpty';
}

return Object.values(donorsMap).map(({ name }) => <div key={name}>{name}</div>);
}));

jest.mock('./hooks', () => ({
useFetchDonors: jest.fn().mockReturnValue({
donors: [],
Expand All @@ -33,7 +25,7 @@ const defaultProps = {

const renderForm = (props = {}) => (
<form>
<DonorsContainer
<Donors
{...defaultProps}
{...props}
/>
Expand All @@ -49,7 +41,7 @@ const renderComponent = (props = {}) => (render(
</MemoryRouter>,
));

describe('DonorsContainer', () => {
describe('Donors', () => {
beforeEach(() => {
useFetchDonors.mockClear().mockReturnValue({
donors: [],
Expand All @@ -73,17 +65,4 @@ describe('DonorsContainer', () => {

expect(screen.getByText('Loading')).toBeDefined();
});

it('should call `useFetchDonors` with `donorOrganizationIds`', () => {
const mockData = [{ name: 'Amazon', code: 'AMAZ', id: '1' }];

useFetchDonors.mockClear().mockReturnValue({
donors: mockData,
isLoading: false,
});

renderComponent({ donorOrganizationIds: ['1'] });

expect(screen.getByText(mockData[0].name)).toBeDefined();
});
});
116 changes: 116 additions & 0 deletions lib/Donors/DonorsContainer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { map, sortBy } from 'lodash';
import PropTypes from 'prop-types';
import { useMemo } from 'react';
import { useIntl } from 'react-intl';

import { useStripes } from '@folio/stripes/core';

import { defaultVisibleColumns } from './constants';
import { DonorsList } from './DonorsList';
import { DonorsLookup } from './DonorsLookup';
import {
getDonorsListFormatter,
getUnAssignDonorFormatter,
} from './utils';

export function DonorsContainer({
columnMapping,
columnWidths,
donors,
fields,
formatter,
id,
setDonorIds,
searchLabel,
showTriggerButton,
visibleColumns,
}) {
const stripes = useStripes();
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 listOfDonors = useMemo(() => (fields.value || [])
.map((contactId, _index) => {
const contact = donorsMap?.[contactId];

return {
...(contact || { isDeleted: true }),
_index,
};
}), [donorsMap, fields.value]);

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

const resultsFormatter = useMemo(() => {
const defaultFormatter = formatter || getDonorsListFormatter({ intl, fields, canViewOrganizations });

if (visibleColumns.includes('unassignDonor')) {
return {
...getUnAssignDonorFormatter({ intl, fields }),
...defaultFormatter,
};
}

return defaultFormatter;
}, [canViewOrganizations, fields, formatter, intl, visibleColumns]);

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

if (newDonorsIds.length) {
setDonorIds([...addedDonorIds, ...newDonorsIds]);
newDonorsIds.forEach(contactId => fields.push(contactId));
}
};

return (
<>
<DonorsList
id={id}
visibleColumns={visibleColumns}
contentData={contentData}
formatter={resultsFormatter}
columnMapping={columnMapping}
columnWidths={columnWidths}
/>
<br />
{
showTriggerButton && (
<DonorsLookup
onAddDonors={onAddDonors}
name={id}
searchLabel={searchLabel}
visibleColumns={visibleColumns}
/>
)
}
</>
);
}

DonorsContainer.propTypes = {
columnWidths: PropTypes.object,
columnMapping: PropTypes.object,
donors: PropTypes.arrayOf(PropTypes.object),
fields: PropTypes.object,
formatter: PropTypes.object,
id: PropTypes.string,
searchLabel: PropTypes.node,
setDonorIds: PropTypes.func.isRequired,
showTriggerButton: PropTypes.bool,
visibleColumns: PropTypes.arrayOf(PropTypes.string),
};

DonorsContainer.defaultProps = {
showTriggerButton: true,
visibleColumns: defaultVisibleColumns,
};
126 changes: 126 additions & 0 deletions lib/Donors/DonorsContainer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
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 { DonorsContainer } from './DonorsContainer';
import { useFetchDonors } from './hooks';

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

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

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

const setDonorIds = jest.fn();

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

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

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

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

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

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

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

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

it('should call `useFetchDonors` with `donorOrganizationIds`', () => {
const mockData = [{ name: 'Amazon', code: 'AMAZ', id: '1' }];

renderComponent({ donors: mockData });
expect(screen.getByText(mockData[0].name)).toBeDefined();
});

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

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

expect(addDonorsButton).toBeDefined();
await user.click(addDonorsButton);
expect(setDonorIds).toHaveBeenCalled();
});

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

expect(screen.queryByText('Add donor')).toBeNull();
});
});
Loading
Loading