diff --git a/lib/DonorsList/DonorsContainer.js b/lib/DonorsList/DonorsContainer.js
new file mode 100644
index 00000000..37765fe4
--- /dev/null
+++ b/lib/DonorsList/DonorsContainer.js
@@ -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);
+ }
+ }, [donorOrganizationIds, handleFetchDonors]);
+
+ const donorsMap = donors.reduce((acc, contact) => {
+ acc[contact.id] = contact;
+
+ return acc;
+ }, {});
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+ );
+}
+
+DonorsContainer.propTypes = {
+ name: PropTypes.string.isRequired,
+ donorOrganizationIds: PropTypes.arrayOf(PropTypes.string),
+};
+
+DonorsContainer.defaultProps = {
+ donorOrganizationIds: [],
+};
+
+export default DonorsContainer;
diff --git a/lib/DonorsList/DonorsList.js b/lib/DonorsList/DonorsList.js
new file mode 100644
index 00000000..a9855ada
--- /dev/null
+++ b/lib/DonorsList/DonorsList.js
@@ -0,0 +1,165 @@
+import React, { useMemo } from 'react';
+import {
+ map,
+ sortBy,
+} from 'lodash';
+import PropTypes from 'prop-types';
+import {
+ FormattedMessage,
+ useIntl,
+} from 'react-intl';
+
+import {
+ Button,
+ Icon,
+ MultiColumnList,
+} from '@folio/stripes/components';
+import {
+ Pluggable,
+ useStripes,
+} from '@folio/stripes/core';
+
+import { acqRowFormatter } from '../utils';
+
+const columnMapping = {
+ name: ,
+ code: ,
+ unassignDonor: null,
+};
+
+const visibleColumns = [
+ 'name',
+ 'code',
+ 'unassignDonor',
+];
+
+const getResultsFormatter = ({
+ intl,
+ fields,
+}) => ({
+ name: donor => donor.name,
+ code: donor => donor.code,
+ unassignDonor: (donor) => (
+
+ ),
+});
+
+const getDonorUrl = (orgId) => {
+ if (orgId) {
+ return `/organizations/view/${orgId}`;
+ }
+
+ return undefined;
+};
+
+const AddDonorButton = ({ fetchDonors, fields, stripes, name }) => {
+ const addDonors = (contacts = []) => {
+ const addedContactIds = new Set(fields.value);
+ const newContactsIds = map(contacts.filter(({ id }) => !addedContactIds.has(id)), 'id');
+
+ if (newContactsIds.length) {
+ fetchDonors([...addedContactIds, ...newContactsIds]);
+ newContactsIds.forEach(contactId => fields.push(contactId));
+ }
+ };
+
+ return (
+ }
+ searchButtonStyle="default"
+ disableRecordCreation
+ stripes={stripes}
+ selectVendor={addDonors}
+ isDonorsEnabled
+ >
+
+
+
+
+ );
+};
+
+AddDonorButton.propTypes = {
+ fetchDonors: PropTypes.func.isRequired,
+ fields: PropTypes.object,
+ stripes: PropTypes.object,
+ name: PropTypes.string.isRequired,
+};
+
+const alignRowProps = { alignLastColToEnd: true };
+
+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()]);
+
+ 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 (
+ <>
+
+
+
+ >
+ );
+};
+
+DonorsList.propTypes = {
+ fetchDonors: PropTypes.func.isRequired,
+ fields: PropTypes.object,
+ donorsMap: PropTypes.object,
+ id: PropTypes.string.isRequired,
+};
+
+export default DonorsList;
diff --git a/lib/DonorsList/hooks/index.js b/lib/DonorsList/hooks/index.js
new file mode 100644
index 00000000..eed71fe5
--- /dev/null
+++ b/lib/DonorsList/hooks/index.js
@@ -0,0 +1 @@
+export { useFetchDonors } from './useFetchDonors';
diff --git a/lib/DonorsList/hooks/useFetchDonors/index.js b/lib/DonorsList/hooks/useFetchDonors/index.js
new file mode 100644
index 00000000..eed71fe5
--- /dev/null
+++ b/lib/DonorsList/hooks/useFetchDonors/index.js
@@ -0,0 +1 @@
+export { useFetchDonors } from './useFetchDonors';
diff --git a/lib/DonorsList/hooks/useFetchDonors/useFetchDonors.js b/lib/DonorsList/hooks/useFetchDonors/useFetchDonors.js
new file mode 100644
index 00000000..ee08a023
--- /dev/null
+++ b/lib/DonorsList/hooks/useFetchDonors/useFetchDonors.js
@@ -0,0 +1,36 @@
+import { useMutation } from 'react-query';
+
+import { useOkapiKy } from '@folio/stripes/core';
+
+import { VENDORS_API } from '../../../constants';
+import { batchRequest } from '../../../utils';
+
+const buildQueryByIds = (itemsChunk) => {
+ const query = itemsChunk
+ .map(chunkId => `id==${chunkId}`)
+ .join(' or ');
+
+ return query || '';
+};
+
+export const useFetchDonors = () => {
+ const ky = useOkapiKy();
+
+ const { isLoading, mutateAsync } = useMutation(
+ ({ donorOrganizationIds }) => {
+ return batchRequest(
+ ({ params: searchParams }) => ky
+ .get(VENDORS_API, { searchParams })
+ .json()
+ .then(({ organizations }) => organizations),
+ donorOrganizationIds,
+ buildQueryByIds,
+ );
+ },
+ );
+
+ return ({
+ fetchDonorsMutation: mutateAsync,
+ isLoading,
+ });
+};
diff --git a/lib/DonorsList/index.js b/lib/DonorsList/index.js
new file mode 100644
index 00000000..66ae435f
--- /dev/null
+++ b/lib/DonorsList/index.js
@@ -0,0 +1 @@
+export { default as DonorsList } from './DonorsContainer';
diff --git a/lib/index.js b/lib/index.js
index ea8509f8..7c0140a3 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -16,6 +16,7 @@ export * from './Currency';
export * from './CurrencyExchangeRateFields';
export * from './CurrencySymbol';
export * from './DeleteHoldingsModal';
+export * from './DonorsList';
export * from './DragDropMCL';
export * from './DynamicSelection';
export * from './DynamicSelectionFilter';
diff --git a/translations/stripes-acq-components/en.json b/translations/stripes-acq-components/en.json
index 5b2d35a1..6d8cf536 100644
--- a/translations/stripes-acq-components/en.json
+++ b/translations/stripes-acq-components/en.json
@@ -187,5 +187,8 @@
"acquisition_method.other": "Other",
"acquisition_method.purchase": "Purchase",
"acquisition_method.purchaseAtVendorSystem": "Purchase at vendor system",
- "acquisition_method.technical": "Technical"
+ "acquisition_method.technical": "Technical",
+ "donors.button.addDonor": "Add donor",
+ "donors.column.code": "Code",
+ "donors.column.name": "Name"
}