-
Notifications
You must be signed in to change notification settings - Fork 3
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: view list of donors #724
Changes from 8 commits
44033b8
ccae7e8
cb5be47
407e3a1
9afb133
acb730f
5474b1a
f12b1bc
e54369b
a4b13ff
b946cca
1b6cb98
98ef11b
c96feae
11bb6c9
32ec435
a989e49
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 = ({ fetchDonors, 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) { | ||
fetchDonors([...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 = { | ||
fetchDonors: PropTypes.func.isRequired, | ||
fields: PropTypes.object, | ||
stripes: PropTypes.object, | ||
name: PropTypes.string.isRequired, | ||
}; | ||
|
||
export default AddDonorButton; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import React, { useCallback, useEffect, 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 [donors, setDonors] = useState([]); | ||
const { fetchDonorsMutation, isLoading } = useFetchDonors(); | ||
|
||
const handleFetchDonors = useCallback(ids => { | ||
fetchDonorsMutation({ donorOrganizationIds: ids }) | ||
.then((data) => { | ||
setDonors(data); | ||
}); | ||
}, [fetchDonorsMutation]); | ||
|
||
useEffect(() => { | ||
if (donorOrganizationIds.length) { | ||
handleFetchDonors(donorOrganizationIds); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems you don't need extra function There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree, sorry the implementation was for mutation. I will remove it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. updated There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, I meant |
||
} | ||
}, [donorOrganizationIds, handleFetchDonors]); | ||
|
||
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} | ||
fetchDonors={handleFetchDonors} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be called like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. updated |
||
donorsMap={donorsMap} | ||
/> | ||
</Col> | ||
</Row> | ||
); | ||
} | ||
|
||
DonorsContainer.propTypes = { | ||
name: PropTypes.string.isRequired, | ||
donorOrganizationIds: PropTypes.arrayOf(PropTypes.string), | ||
}; | ||
|
||
DonorsContainer.defaultProps = { | ||
donorOrganizationIds: [], | ||
}; | ||
|
||
export default DonorsContainer; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
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({ | ||
fetchDonorsMutation: jest.fn(), | ||
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({ | ||
fetchDonorsMutation: jest.fn(), | ||
isLoading: false, | ||
}); | ||
}); | ||
|
||
it('should render component', () => { | ||
renderComponent(); | ||
|
||
expect(screen.getByText('stripes-components.tableEmpty')).toBeDefined(); | ||
}); | ||
|
||
it('should render Loading component', () => { | ||
useFetchDonors.mockClear().mockReturnValue({ | ||
fetchDonorsMutation: jest.fn(), | ||
isLoading: true, | ||
}); | ||
|
||
renderComponent(); | ||
|
||
expect(screen.getByText('Loading')).toBeDefined(); | ||
}); | ||
|
||
it('should call `fetchDonorsMutation` with `donorOrganizationIds`', () => { | ||
const mockData = [{ name: 'Amazon', code: 'AMAZ', id: '1' }]; | ||
const fetchDonorsMutationMock = jest.fn().mockReturnValue({ | ||
then: (cb) => cb(mockData), | ||
}); | ||
|
||
useFetchDonors.mockClear().mockReturnValue({ | ||
fetchDonorsMutation: fetchDonorsMutationMock, | ||
isLoading: false, | ||
}); | ||
|
||
renderComponent({ donorOrganizationIds: ['1'] }); | ||
|
||
expect(fetchDonorsMutationMock).toHaveBeenCalled(); | ||
expect(screen.getByText(mockData[0].name)).toBeDefined(); | ||
}); | ||
}); |
usavkov-epam marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import React, { useMemo } from 'react'; | ||
import { sortBy } from 'lodash'; | ||
import PropTypes from 'prop-types'; | ||
import { useIntl } from 'react-intl'; | ||
|
||
import { | ||
Button, | ||
Icon, | ||
MultiColumnList, | ||
} from '@folio/stripes/components'; | ||
import { useStripes } from '@folio/stripes/core'; | ||
|
||
import { acqRowFormatter } from '../utils'; | ||
import AddDonorButton from './AddDonorButton'; | ||
import { alignRowProps, columnMapping, columnWidths, visibleColumns } from './constants'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please, break imports into separate lines There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How could I miss this step again =( |
||
|
||
const getResultsFormatter = ({ | ||
intl, | ||
fields, | ||
}) => ({ | ||
name: donor => donor.name, | ||
code: donor => donor.code, | ||
unassignDonor: (donor) => ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please keep consistency in code style: two args in the callbacks before not wrapped in the |
||
<Button | ||
align="end" | ||
aria-label={intl.formatMessage({ id: 'stripes-acq-components.donors.button.unassign' })} | ||
buttonStyle="fieldControl" | ||
data-test-unassign-donor | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you use this attr. anywhere? |
||
type="button" | ||
onClick={(e) => { | ||
e.preventDefault(); | ||
fields.remove(donor._index); | ||
}} | ||
> | ||
<Icon icon="times-circle" /> | ||
</Button> | ||
), | ||
}); | ||
|
||
const getDonorUrl = (orgId) => { | ||
if (orgId) { | ||
return `/organizations/view/${orgId}`; | ||
} | ||
|
||
return undefined; | ||
}; | ||
|
||
const DonorsList = ({ fetchDonors, fields, donorsMap, id }) => { | ||
const intl = useIntl(); | ||
const stripes = useStripes(); | ||
const canViewOrganizations = stripes.hasPerm('ui-organizations.view'); | ||
const donors = (fields.value || []) | ||
.map((contactId, _index) => { | ||
const contact = donorsMap?.[contactId]; | ||
|
||
return { | ||
...(contact || { isDeleted: true }), | ||
_index, | ||
}; | ||
}); | ||
const contentData = sortBy(donors, [({ lastName }) => lastName?.toLowerCase()]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. probably makes sense to wrap in useMemo, because sorting is quite an expensive operation There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. updated |
||
|
||
const anchoredRowFormatter = ({ rowProps, ...rest }) => { | ||
return acqRowFormatter({ | ||
...rest, | ||
rowProps: { | ||
...rowProps, | ||
to: getDonorUrl(canViewOrganizations && rest.rowData.id), | ||
}, | ||
}); | ||
}; | ||
|
||
const resultsFormatter = useMemo(() => { | ||
return getResultsFormatter({ intl, fields }); | ||
}, [fields, intl]); | ||
|
||
return ( | ||
<> | ||
<MultiColumnList | ||
alisher-epam marked this conversation as resolved.
Show resolved
Hide resolved
usavkov-epam marked this conversation as resolved.
Show resolved
Hide resolved
|
||
id={id} | ||
columnMapping={columnMapping} | ||
contentData={contentData} | ||
formatter={resultsFormatter} | ||
rowFormatter={anchoredRowFormatter} | ||
rowProps={alignRowProps} | ||
visibleColumns={visibleColumns} | ||
columnWidths={columnWidths} | ||
/> | ||
<br /> | ||
<AddDonorButton | ||
fetchDonors={fetchDonors} | ||
fields={fields} | ||
stripes={stripes} | ||
name={id} | ||
/> | ||
</> | ||
); | ||
}; | ||
|
||
DonorsList.propTypes = { | ||
fetchDonors: PropTypes.func.isRequired, | ||
fields: PropTypes.object, | ||
donorsMap: PropTypes.object, | ||
id: PropTypes.string.isRequired, | ||
}; | ||
|
||
export default DonorsList; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { MemoryRouter } from 'react-router-dom'; | ||
import { render, screen } from '@testing-library/react'; | ||
|
||
import DonorsList from './DonorsList'; | ||
|
||
const mockFetchDonors = jest.fn(); | ||
|
||
const defaultProps = { | ||
fetchDonors: mockFetchDonors, | ||
fields: {}, | ||
donorsMap: {}, | ||
id: 'donors', | ||
}; | ||
|
||
const wrapper = ({ children }) => ( | ||
<MemoryRouter> | ||
{children} | ||
</MemoryRouter> | ||
); | ||
|
||
const renderComponent = (props = {}) => (render( | ||
<DonorsList | ||
{...defaultProps} | ||
{...props} | ||
/>, | ||
{ wrapper }, | ||
)); | ||
|
||
describe('DonorsList', () => { | ||
it('should render component', () => { | ||
renderComponent(); | ||
|
||
expect(screen.getByText('stripes-components.tableEmpty')).toBeDefined(); | ||
}); | ||
|
||
it('should render the list of donor organizations', () => { | ||
renderComponent({ | ||
fields: { | ||
value: [ | ||
'1', | ||
'2', | ||
], | ||
}, | ||
donorsMap: { | ||
1: { id: '1', name: 'Amazon' }, | ||
2: { id: '2', name: 'Google' }, | ||
}, | ||
}); | ||
|
||
expect(screen.getByText('Amazon')).toBeDefined(); | ||
expect(screen.getByText('Google')).toBeDefined(); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the
fetchDonors
should be called smth likeonAddDonor
, btn doesn't fetch anythingThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated