Skip to content

Commit

Permalink
UISACQCOMP-166: configure the plugin via props (#726)
Browse files Browse the repository at this point in the history
* UISACQCOMP-166: configure the plugin via props

* refactor: move logic to component container

* test: fix failing tests

* test: add test cases

* refactor: change component name

* remove isRequired attribute from name prop

* refactor: default props

* refactor: add default formatter

* change default export to named export

* remove default formatter

* update DonorsList with providing default formatter

* fix formatter
  • Loading branch information
alisher-epam authored Nov 9, 2023
1 parent 0b6d10c commit be0e6f9
Show file tree
Hide file tree
Showing 20 changed files with 424 additions and 207 deletions.
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

0 comments on commit be0e6f9

Please sign in to comment.