-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
UISACQCOMP-166: view list of donors (#724)
* UISACQCOMP-166: view list of donors * tests: add test coverages * tests: add test coverage and Changelog.md * refactor: change the order of import files * add column width for better table view * refactor: add config properties for plugin to implement donors * fix: add missing IS_DONOR filter prop * improve code quality regarding to the comments * refactor: upgrade mutation to query for better caching * fix: failing tests * update callback names for better readability * improve and optimize the code * update state initializer * tests: add test cases for the button * remove unused packages and imports * tests: add test cases
- Loading branch information
1 parent
fc2f33d
commit 5bbcf17
Showing
16 changed files
with
585 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { map } from 'lodash'; | ||
import PropTypes from 'prop-types'; | ||
import { FormattedMessage } from 'react-intl'; | ||
|
||
import { Pluggable } from '@folio/stripes/core'; | ||
|
||
import { | ||
initialFilters, | ||
modalLabel, | ||
pluginVisibleColumns, | ||
resultsPaneTitle, | ||
searchableIndexes, | ||
visibleFilters, | ||
} from './constants'; | ||
|
||
const AddDonorButton = ({ onAddDonors, fields, stripes, name }) => { | ||
const addDonors = (donors = []) => { | ||
const addedDonorIds = new Set(fields.value); | ||
const newDonorsIds = map(donors.filter(({ id }) => !addedDonorIds.has(id)), 'id'); | ||
|
||
if (newDonorsIds.length) { | ||
onAddDonors([...addedDonorIds, ...newDonorsIds]); | ||
newDonorsIds.forEach(contactId => fields.push(contactId)); | ||
} | ||
}; | ||
|
||
return ( | ||
<Pluggable | ||
id={`${name}-plugin`} | ||
aria-haspopup="true" | ||
type="find-organization" | ||
dataKey="organization" | ||
searchLabel={<FormattedMessage id="stripes-acq-components.donors.button.addDonor" />} | ||
searchButtonStyle="default" | ||
disableRecordCreation | ||
stripes={stripes} | ||
selectVendor={addDonors} | ||
modalLabel={modalLabel} | ||
resultsPaneTitle={resultsPaneTitle} | ||
visibleColumns={pluginVisibleColumns} | ||
initialFilters={initialFilters} | ||
searchableIndexes={searchableIndexes} | ||
visibleFilters={visibleFilters} | ||
isMultiSelect | ||
> | ||
<span data-test-add-donor> | ||
<FormattedMessage id="stripes-acq-components.donors.noFindOrganizationPlugin" /> | ||
</span> | ||
</Pluggable> | ||
); | ||
}; | ||
|
||
AddDonorButton.propTypes = { | ||
onAddDonors: PropTypes.func.isRequired, | ||
fields: PropTypes.object, | ||
stripes: PropTypes.object, | ||
name: PropTypes.string.isRequired, | ||
}; | ||
|
||
export default AddDonorButton; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { render, screen } from '@testing-library/react'; | ||
import user from '@testing-library/user-event'; | ||
|
||
import AddDonorButton from './AddDonorButton'; | ||
|
||
const mockVendorData = { id: '1', name: 'Amazon' }; | ||
|
||
jest.mock('@folio/stripes/core', () => ({ | ||
...jest.requireActual('@folio/stripes/core'), | ||
Pluggable: jest.fn(({ children, ...rest }) => { | ||
return ( | ||
<div> | ||
{children} | ||
<button | ||
type="button" | ||
id={rest?.name} | ||
onClick={() => rest?.selectVendor([mockVendorData])} | ||
> | ||
Add donor | ||
</button> | ||
</div> | ||
); | ||
}), | ||
})); | ||
|
||
const mockOnAddDonors = jest.fn(); | ||
|
||
const defaultProps = { | ||
onAddDonors: mockOnAddDonors, | ||
fields: { | ||
name: 'donors', | ||
}, | ||
name: 'donors', | ||
}; | ||
|
||
const renderComponent = (props = defaultProps) => (render( | ||
<AddDonorButton {...props} />, | ||
)); | ||
|
||
describe('AddDonorButton', () => { | ||
it('should render component', async () => { | ||
renderComponent({ | ||
fields: { | ||
name: 'donors', | ||
push: jest.fn(), | ||
}, | ||
name: 'donors', | ||
onAddDonors: mockOnAddDonors, | ||
}); | ||
|
||
const addDonorsButton = screen.getByText('Add donor'); | ||
|
||
expect(addDonorsButton).toBeDefined(); | ||
|
||
await user.click(addDonorsButton); | ||
|
||
expect(mockOnAddDonors).toHaveBeenCalledWith([mockVendorData.id]); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import React, { useState } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { FieldArray } from 'react-final-form-arrays'; | ||
|
||
import { | ||
Col, | ||
Loading, | ||
Row, | ||
} from '@folio/stripes/components'; | ||
|
||
import DonorsList from './DonorsList'; | ||
import { useFetchDonors } from './hooks'; | ||
|
||
function DonorsContainer({ name, donorOrganizationIds }) { | ||
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 />; | ||
} | ||
|
||
return ( | ||
<Row> | ||
<Col xs={12}> | ||
<FieldArray | ||
name={name} | ||
id={name} | ||
component={DonorsList} | ||
setDonorIds={setDonorIds} | ||
donorsMap={donorsMap} | ||
/> | ||
</Col> | ||
</Row> | ||
); | ||
} | ||
|
||
DonorsContainer.propTypes = { | ||
name: PropTypes.string.isRequired, | ||
donorOrganizationIds: PropTypes.arrayOf(PropTypes.string), | ||
}; | ||
|
||
DonorsContainer.defaultProps = { | ||
donorOrganizationIds: [], | ||
}; | ||
|
||
export default DonorsContainer; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import { MemoryRouter } from 'react-router-dom'; | ||
import { render, screen } from '@testing-library/react'; | ||
|
||
import stripesFinalForm from '@folio/stripes/final-form'; | ||
|
||
import DonorsContainer from './DonorsContainer'; | ||
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: [], | ||
isLoading: false, | ||
}), | ||
})); | ||
|
||
const defaultProps = { | ||
name: 'donors', | ||
donorOrganizationIds: [], | ||
}; | ||
|
||
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('stripes-components.tableEmpty')).toBeDefined(); | ||
}); | ||
|
||
it('should render Loading component', () => { | ||
useFetchDonors.mockClear().mockReturnValue({ | ||
donors: [], | ||
isLoading: true, | ||
}); | ||
|
||
renderComponent(); | ||
|
||
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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import React, { useMemo } from 'react'; | ||
import { sortBy } from 'lodash'; | ||
import PropTypes from 'prop-types'; | ||
import { useIntl } from 'react-intl'; | ||
|
||
import { | ||
Button, | ||
Icon, | ||
MultiColumnList, | ||
TextLink, | ||
} from '@folio/stripes/components'; | ||
import { useStripes } from '@folio/stripes/core'; | ||
|
||
import AddDonorButton from './AddDonorButton'; | ||
import { | ||
alignRowProps, | ||
columnMapping, | ||
columnWidths, | ||
visibleColumns, | ||
} from './constants'; | ||
|
||
const getDonorUrl = (orgId) => { | ||
if (orgId) { | ||
return `/organizations/view/${orgId}`; | ||
} | ||
|
||
return undefined; | ||
}; | ||
|
||
const getResultsFormatter = ({ | ||
canViewOrganizations, | ||
fields, | ||
intl, | ||
}) => ({ | ||
name: donor => <TextLink to={getDonorUrl(canViewOrganizations && donor.id)}>{donor.name}</TextLink>, | ||
code: donor => donor.code, | ||
unassignDonor: donor => ( | ||
<Button | ||
align="end" | ||
aria-label={intl.formatMessage({ id: 'stripes-acq-components.donors.button.unassign' })} | ||
buttonStyle="fieldControl" | ||
type="button" | ||
onClick={(e) => { | ||
e.preventDefault(); | ||
fields.remove(donor._index); | ||
}} | ||
> | ||
<Icon icon="times-circle" /> | ||
</Button> | ||
), | ||
}); | ||
|
||
const DonorsList = ({ setDonorIds, fields, donorsMap, id }) => { | ||
const intl = useIntl(); | ||
const stripes = useStripes(); | ||
const canViewOrganizations = stripes.hasPerm('ui-organizations.view'); | ||
|
||
const donors = useMemo(() => (fields.value || []) | ||
.map((contactId, _index) => { | ||
const contact = donorsMap?.[contactId]; | ||
|
||
return { | ||
...(contact || { isDeleted: true }), | ||
_index, | ||
}; | ||
}), [donorsMap, fields.value]); | ||
|
||
const contentData = useMemo(() => sortBy(donors, [({ lastName }) => lastName?.toLowerCase()]), [donors]); | ||
|
||
const resultsFormatter = useMemo(() => { | ||
return getResultsFormatter({ intl, fields, canViewOrganizations }); | ||
}, [canViewOrganizations, fields, intl]); | ||
|
||
return ( | ||
<> | ||
<MultiColumnList | ||
id={id} | ||
columnMapping={columnMapping} | ||
contentData={contentData} | ||
formatter={resultsFormatter} | ||
rowProps={alignRowProps} | ||
visibleColumns={visibleColumns} | ||
columnWidths={columnWidths} | ||
/> | ||
<br /> | ||
<AddDonorButton | ||
onAddDonors={setDonorIds} | ||
fields={fields} | ||
stripes={stripes} | ||
name={id} | ||
/> | ||
</> | ||
); | ||
}; | ||
|
||
DonorsList.propTypes = { | ||
setDonorIds: PropTypes.func.isRequired, | ||
fields: PropTypes.object, | ||
donorsMap: PropTypes.object, | ||
id: PropTypes.string.isRequired, | ||
}; | ||
|
||
export default DonorsList; |
Oops, something went wrong.