diff --git a/lib/Donors/DonorsForm.js b/lib/Donors/DonorsForm.js
new file mode 100644
index 00000000..e43052dc
--- /dev/null
+++ b/lib/Donors/DonorsForm.js
@@ -0,0 +1,62 @@
+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 { useDonorContacts } from './FindDonors/hooks';
+import DonorsFormList from './DonorsFormList';
+
+function DonorsForm({ name, donorOrganizationIds }) {
+ const [donors, setDonors] = useState([]);
+ const { fetchDonorsMutation, isLoading } = useDonorContacts();
+
+ const handleFetchDonors = useCallback(ids => {
+ fetchDonorsMutation({ donorOrganizationIds: ids })
+ .then((data) => {
+ setDonors(data);
+ });
+ }, [fetchDonorsMutation]);
+
+ useEffect(() => {
+ handleFetchDonors(donorOrganizationIds);
+ }, [donorOrganizationIds, handleFetchDonors]);
+
+ const donorsMap = donors.reduce((acc, contact) => {
+ acc[contact.id] = contact;
+
+ return acc;
+ }, {});
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+ );
+}
+
+DonorsForm.propTypes = {
+ name: PropTypes.string.isRequired,
+ donorOrganizationIds: PropTypes.arrayOf(PropTypes.string),
+};
+
+DonorsForm.defaultProps = {
+ donorOrganizationIds: [],
+};
+
+export default DonorsForm;
diff --git a/lib/Donors/DonorsFormList.js b/lib/Donors/DonorsFormList.js
new file mode 100644
index 00000000..7eeba8c6
--- /dev/null
+++ b/lib/Donors/DonorsFormList.js
@@ -0,0 +1,157 @@
+import React, { useMemo } from 'react';
+import PropTypes from 'prop-types';
+import {
+ map,
+ sortBy,
+} from 'lodash';
+import {
+ FormattedMessage,
+ useIntl,
+} from 'react-intl';
+
+import {
+ Button,
+ Icon,
+ MultiColumnList,
+} from '@folio/stripes/components';
+import { useStripes } from '@folio/stripes/core';
+
+import FindDonors from './FindDonors/FindDonors';
+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) => {
+ return `/organizations/view/${orgId}`;
+};
+
+const AddDonorButton = ({ fetchDonors, fields, stripes }) => {
+ 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}
+ addDonors={addDonors}
+ >
+
+
+
+
+ );
+};
+
+AddDonorButton.propTypes = {
+ fetchDonors: PropTypes.func.isRequired,
+ fields: PropTypes.object,
+ stripes: PropTypes.object,
+};
+
+const alignRowProps = { alignLastColToEnd: true };
+
+const DonorsFormList = ({ fetchDonors, fields, donorsMap, orgId, categoriesDict }) => {
+ const intl = useIntl();
+ const stripes = useStripes();
+ const contacts = (fields.value || [])
+ .map((contactId, _index) => {
+ const contact = donorsMap?.[contactId];
+
+ return {
+ ...(contact || { isDeleted: true }),
+ _index,
+ };
+ });
+ const contentData = sortBy(contacts, [({ lastName }) => lastName?.toLowerCase()]);
+
+ const anchoredRowFormatter = ({ rowProps, ...rest }) => {
+ // console.log(rowProps, rest)
+ return acqRowFormatter({
+ ...rest,
+ rowProps: {
+ ...rowProps,
+ to: getDonorUrl(rest.rowData.id),
+ },
+ });
+ };
+
+ const resultsFormatter = useMemo(() => {
+ return getResultsFormatter({ intl, categoriesDict, fields });
+ }, [categoriesDict, fields, intl]);
+
+ return (
+ <>
+
+
+
+ >
+ );
+};
+
+DonorsFormList.propTypes = {
+ fetchDonors: PropTypes.func.isRequired,
+ fields: PropTypes.object,
+ orgId: PropTypes.string,
+ categoriesDict: PropTypes.arrayOf(PropTypes.object),
+ donorsMap: PropTypes.object,
+};
+
+export default DonorsFormList;
diff --git a/lib/Donors/FindDonors/DonorsListFilters/DonorsListFilters.js b/lib/Donors/FindDonors/DonorsListFilters/DonorsListFilters.js
new file mode 100644
index 00000000..9374400d
--- /dev/null
+++ b/lib/Donors/FindDonors/DonorsListFilters/DonorsListFilters.js
@@ -0,0 +1,56 @@
+import PropTypes from 'prop-types';
+import { useCallback } from 'react';
+
+import { AccordionSet } from '@folio/stripes/components';
+// import { TypeFilter } from '@folio/plugin-find-organization';
+
+import { AcqTagsFilter } from '../../../AcqTagsFilter';
+import { AcqCheckboxFilter } from '../../../AcqCheckboxFilter';
+import {
+ DONOR_FILTERS,
+ DONOR_STATUS_OPTIONS,
+} from '../constants';
+
+export const DonorsListFilters = ({
+ activeFilters,
+ applyFilters,
+ disabled,
+}) => {
+ const adaptedApplyFilters = useCallback(({ name, values }) => applyFilters(name, values), [applyFilters]);
+
+ return (
+
+ {/* */}
+
+
+
+
+
+ );
+};
+
+DonorsListFilters.propTypes = {
+ activeFilters: PropTypes.object.isRequired,
+ applyFilters: PropTypes.func.isRequired,
+ disabled: PropTypes.bool,
+};
diff --git a/lib/Donors/FindDonors/DonorsListFilters/index.js b/lib/Donors/FindDonors/DonorsListFilters/index.js
new file mode 100644
index 00000000..9863de0d
--- /dev/null
+++ b/lib/Donors/FindDonors/DonorsListFilters/index.js
@@ -0,0 +1 @@
+export { DonorsListFilters } from './DonorsListFilters';
diff --git a/lib/Donors/FindDonors/FindDonors.js b/lib/Donors/FindDonors/FindDonors.js
new file mode 100644
index 00000000..893543f3
--- /dev/null
+++ b/lib/Donors/FindDonors/FindDonors.js
@@ -0,0 +1,122 @@
+import { useCallback, useState } from 'react';
+import PropTypes from 'prop-types';
+import { FormattedMessage } from 'react-intl';
+import { noop } from 'lodash';
+
+import { FindRecords } from '../../FindRecords';
+import { PLUGIN_RESULT_COUNT_INCREMENT } from '../../constants';
+import { DonorsListFilters } from './DonorsListFilters';
+import { useDonors } from './hooks';
+
+const idPrefix = 'uiFindDonors-';
+const modalLabel = ;
+const resultsPaneTitle = ;
+
+const columnWidths = {
+ isChecked: '8%',
+ code: '30%',
+ name: '40%',
+};
+const visibleColumns = ['name', 'code'];
+const columnMapping = {
+ name: ,
+ code: ,
+};
+
+const INIT_PAGINATION = { limit: PLUGIN_RESULT_COUNT_INCREMENT, offset: 0 };
+
+const FindDonors = ({
+ addDonors,
+ isMultiSelect,
+ renderNewContactBtn,
+ ...rest
+}) => {
+ const [totalCount, setTotalCount] = useState(0);
+ const [records, setRecords] = useState([]);
+ const [searchParams, setSearchParams] = useState({});
+ const [isLoading, setIsLoading] = useState(false);
+ const [pagination, setPagination] = useState(INIT_PAGINATION);
+
+ const { fetchDonors } = useDonors();
+
+ const refreshRecords = useCallback((filters) => {
+ setIsLoading(true);
+
+ setRecords([]);
+ setTotalCount(0);
+ setPagination(INIT_PAGINATION);
+ setSearchParams(filters);
+
+ fetchDonors({ ...INIT_PAGINATION, searchParams: filters })
+ .then(({ organizations, totalRecords }) => {
+ setTotalCount(totalRecords);
+ setRecords(organizations);
+ })
+ .finally(() => setIsLoading(false));
+ }, [fetchDonors]);
+
+ const onNeedMoreData = useCallback((newPagination) => {
+ setIsLoading(true);
+
+ fetchDonors({ ...newPagination, searchParams })
+ .then(({ organizations }) => {
+ setPagination(newPagination);
+ setRecords(organizations);
+ })
+ .finally(() => setIsLoading(false));
+ }, [fetchDonors, searchParams]);
+
+ const renderFilters = useCallback((activeFilters, applyFilters) => {
+ return (
+
+ );
+ }, [isLoading]);
+
+ return (
+
+ );
+};
+
+FindDonors.propTypes = {
+ disabled: PropTypes.bool,
+ marginBottom0: PropTypes.bool,
+ marginTop0: PropTypes.bool,
+ searchButtonStyle: PropTypes.string,
+ searchLabel: PropTypes.node,
+ addDonors: PropTypes.func,
+ renderNewContactBtn: PropTypes.func,
+ isMultiSelect: PropTypes.bool,
+};
+
+FindDonors.defaultProps = {
+ disabled: false,
+ searchLabel: ,
+ addDonors: noop,
+ renderNewContactBtn: noop,
+ isMultiSelect: true,
+};
+
+export default FindDonors;
diff --git a/lib/Donors/FindDonors/constants.js b/lib/Donors/FindDonors/constants.js
new file mode 100644
index 00000000..326135fe
--- /dev/null
+++ b/lib/Donors/FindDonors/constants.js
@@ -0,0 +1,30 @@
+import { FormattedMessage } from 'react-intl';
+
+export const FINANCE_FUNDS_API = 'finance/funds';
+
+export const DONOR_FILTERS = {
+ IS_VENDOR: 'isVendor',
+ TAGS: 'tags',
+ TYPES: 'organizationTypes',
+};
+
+export const DONOR_STATUSES = {
+ ACTIVE: 'true',
+ INACTIVE: 'false',
+};
+
+export const DONOR_STATUS_OPTIONS = [
+ {
+ value: DONOR_STATUSES.ACTIVE,
+ label: ,
+ },
+ {
+ value: DONOR_STATUSES.INACTIVE,
+ label: ,
+ },
+];
+
+export const DONORS_SORT_MAP = {
+ name: 'name',
+ code: 'code',
+};
diff --git a/lib/Donors/FindDonors/hooks/index.js b/lib/Donors/FindDonors/hooks/index.js
new file mode 100644
index 00000000..8413d293
--- /dev/null
+++ b/lib/Donors/FindDonors/hooks/index.js
@@ -0,0 +1,2 @@
+export { useDonors } from './useDonors';
+export { useDonorContacts } from './useDonorContacts';
diff --git a/lib/Donors/FindDonors/hooks/useDonorContacts/index.js b/lib/Donors/FindDonors/hooks/useDonorContacts/index.js
new file mode 100644
index 00000000..81bc25f1
--- /dev/null
+++ b/lib/Donors/FindDonors/hooks/useDonorContacts/index.js
@@ -0,0 +1 @@
+export { useDonorContacts } from './useDonorContacts';
diff --git a/lib/Donors/FindDonors/hooks/useDonorContacts/useDonorContacts.js b/lib/Donors/FindDonors/hooks/useDonorContacts/useDonorContacts.js
new file mode 100644
index 00000000..d0b8b443
--- /dev/null
+++ b/lib/Donors/FindDonors/hooks/useDonorContacts/useDonorContacts.js
@@ -0,0 +1,36 @@
+import { useMutation } from 'react-query';
+
+import { useOkapiKy } from '@folio/stripes/core';
+
+import { batchRequest } from '../../../../utils';
+import { VENDORS_API } from '../../../../constants';
+
+const buildQueryByIds = (itemsChunk) => {
+ const query = itemsChunk
+ .map(chunkId => `id==${chunkId}`)
+ .join(' or ');
+
+ return query || '';
+};
+
+export const useDonorContacts = () => {
+ 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/Donors/FindDonors/hooks/useDonors/index.js b/lib/Donors/FindDonors/hooks/useDonors/index.js
new file mode 100644
index 00000000..9f4b89f4
--- /dev/null
+++ b/lib/Donors/FindDonors/hooks/useDonors/index.js
@@ -0,0 +1 @@
+export { useDonors } from './useDonors';
diff --git a/lib/Donors/FindDonors/hooks/useDonors/useDonors.js b/lib/Donors/FindDonors/hooks/useDonors/useDonors.js
new file mode 100644
index 00000000..c5cec978
--- /dev/null
+++ b/lib/Donors/FindDonors/hooks/useDonors/useDonors.js
@@ -0,0 +1,88 @@
+import { useMutation } from 'react-query';
+
+import { useOkapiKy } from '@folio/stripes/core';
+
+import {
+ ASC_DIRECTION,
+ buildFilterQuery,
+ connectQuery,
+} from '../../../../AcqList';
+import {
+ PLUGIN_RESULT_COUNT_INCREMENT,
+ VENDORS_API,
+} from '../../../../constants';
+import {
+ DONOR_FILTERS,
+ DONORS_SORT_MAP,
+} from '../../constants';
+
+const DEFAULT_QUERY = 'status=="active" and isDonor=true';
+
+const buildCustomFilterQuery = filter => values => {
+ const value = values.map(val => `"${val}"`).join(' or ');
+
+ return `${filter}=(${value})`;
+};
+
+const buildCustomSortingQuery = ({ sorting, sortingDirection } = {}) => {
+ if (sorting) {
+ const sortIndex = (DONORS_SORT_MAP[sorting] || sorting)
+ .split(' ')
+ .map(value => `${value}/sort.${sortingDirection || ASC_DIRECTION}`)
+ .join(' ');
+
+ return `sortby ${sortIndex}`;
+ }
+
+ return '';
+};
+
+const buildDonorsQuery = searchParams => {
+ const mainQuery = buildFilterQuery(
+ searchParams,
+ (query) => `name="${query}*" or code="${query}*"`,
+ {
+ [DONOR_FILTERS.IS_VENDOR]: buildCustomFilterQuery(DONOR_FILTERS.IS_VENDOR),
+ [DONOR_FILTERS.TAGS]: buildCustomFilterQuery(DONOR_FILTERS.TAGS),
+ [DONOR_FILTERS.TYPES]: buildCustomFilterQuery(DONOR_FILTERS.TYPES),
+ },
+ );
+
+ const sortingQuery = buildCustomSortingQuery(searchParams);
+ const filtersQuery = mainQuery ? `${mainQuery} and ${DEFAULT_QUERY}` : DEFAULT_QUERY;
+
+ return connectQuery(filtersQuery, sortingQuery);
+};
+
+export const useDonors = () => {
+ const ky = useOkapiKy();
+
+ const {
+ mutateAsync: fetchDonors,
+ } = useMutation({
+ mutationFn: async ({
+ searchParams = {},
+ limit = PLUGIN_RESULT_COUNT_INCREMENT,
+ offset = 0,
+ }) => {
+ const donorsQuery = buildDonorsQuery(searchParams);
+ const builtSearchParams = {
+ query: donorsQuery,
+ limit,
+ offset,
+ };
+
+ const {
+ organizations = [],
+ totalRecords,
+ } = await ky.get(VENDORS_API, { searchParams: { ...builtSearchParams } }).json();
+
+ return {
+ organizations,
+ totalRecords,
+ };
+ },
+ });
+
+ return { fetchDonors };
+};
diff --git a/lib/Donors/index.js b/lib/Donors/index.js
new file mode 100644
index 00000000..f53891a8
--- /dev/null
+++ b/lib/Donors/index.js
@@ -0,0 +1 @@
+export { default as Donors } from './DonorsForm';
diff --git a/lib/index.js b/lib/index.js
index ea8509f8..0760f77a 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 './Donors';
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..caac0e64 100644
--- a/translations/stripes-acq-components/en.json
+++ b/translations/stripes-acq-components/en.json
@@ -187,5 +187,16 @@
"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.noFindDonorPlugin": "no find-contact plugin",
+ "donors.button.unassign": "Unassign",
+ "donors.column.name": "Name",
+ "donors.column.code": "Code",
+ "donors.filter.isVendor": "Is vendor",
+ "donors.filter.isVendor.active": "Active",
+ "donors.filter.isVendor.inactive": "Inactive",
+ "donors.modal.title": "Add donors",
+ "donors.modal.subTitle": "Donors"
}