Skip to content

Commit

Permalink
UIORGS-390 Implement event emmiter to handle address categories change
Browse files Browse the repository at this point in the history
  • Loading branch information
usavkov-epam committed Nov 14, 2023
1 parent 1ba9085 commit 2e0e557
Show file tree
Hide file tree
Showing 18 changed files with 198 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import { Field } from 'react-final-form';
import { useMemo } from 'react';
import { Field, useForm } from 'react-final-form';
import { FormattedMessage } from 'react-intl';

import {
Expand All @@ -11,14 +12,25 @@ import {
import { FieldSelectionFinal } from '@folio/stripes-acq-components';

import { FieldIsPrimary } from '../../../../common/components';
import { getAddressCategoryIdsSet } from '../../getAddressCategoryIdsSet';

export const BankingInformationField = ({
categoriesOptions,
bankingAccountTypeOptions,
categories,

Check failure on line 19 in src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationField/BankingInformationField.js

View workflow job for this annotation

GitHub Actions / github-actions-ci

'categories' is missing in props validation

Check failure on line 19 in src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationField/BankingInformationField.js

View workflow job for this annotation

GitHub Actions / github-actions-ci

'categories' is missing in props validation
fields,
index,
name,
}) => {
const { getFieldState } = useForm();

// TODO: category id instead of address
const initCategoryId = getFieldState(`${name}.addressId`)?.initial;

const addresses = getFieldState('addresses')?.value;
const addressCategoryIdsSet = useMemo(() => {
return getAddressCategoryIdsSet(addresses);
}, [addresses]);

const cardHeader = (
<FieldIsPrimary
fields={fields}
Expand All @@ -28,6 +40,16 @@ export const BankingInformationField = ({
/>
);

const categoriesOptions = useMemo(() => {
return categories.reduce((acc, { id, value }) => {

Check failure on line 44 in src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationField/BankingInformationField.js

View workflow job for this annotation

GitHub Actions / github-actions-ci

'categories.reduce' is missing in props validation

Check failure on line 44 in src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationField/BankingInformationField.js

View workflow job for this annotation

GitHub Actions / github-actions-ci

'categories.reduce' is missing in props validation
if (addressCategoryIdsSet.has(id) || id === initCategoryId) {
acc.push({ label: value, value: id });
}

return acc;
}, []);
}, [addressCategoryIdsSet, categories]);

Check warning on line 51 in src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationField/BankingInformationField.js

View workflow job for this annotation

GitHub Actions / github-actions-ci

React Hook useMemo has a missing dependency: 'initCategoryId'. Either include it or remove the dependency array

Check warning on line 51 in src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationField/BankingInformationField.js

View workflow job for this annotation

GitHub Actions / github-actions-ci

React Hook useMemo has a missing dependency: 'initCategoryId'. Either include it or remove the dependency array

return (
<Card headerStart={cardHeader}>
<Row>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useEffect } from 'react';
import { useForm } from 'react-final-form';

import { RepeatableFieldWithValidation } from '@folio/stripes-acq-components';

import { EVENT_EMITTER_EVENTS } from '../../../../common/constants';
import { useEventEmitter } from '../../../../common/hooks';
import { getAddressCategoryIdsSet } from '../../getAddressCategoryIdsSet';

export const BankingInformationFieldArray = (props) => {
const { fields } = props;

Check failure on line 11 in src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationFieldArray/BankingInformationFieldArray.js

View workflow job for this annotation

GitHub Actions / github-actions-ci

'fields' is missing in props validation

Check failure on line 11 in src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationFieldArray/BankingInformationFieldArray.js

View workflow job for this annotation

GitHub Actions / github-actions-ci

'fields' is missing in props validation
const eventEmitter = useEventEmitter();
const { change, getFieldState } = useForm();

/*
Handles organization addresses categories change.
Resets banking information address category fields without initial value.
*/
useEffect(() => {
const eventType = EVENT_EMITTER_EVENTS.ADDRESS_CATEGORY_CHANGED;
const callback = () => {
const addressesCategoriesIdsMap = getAddressCategoryIdsSet(getFieldState('addresses').value);

fields.forEach(field => {

Check failure on line 24 in src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationFieldArray/BankingInformationFieldArray.js

View workflow job for this annotation

GitHub Actions / github-actions-ci

'fields.forEach' is missing in props validation

Check failure on line 24 in src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationFieldArray/BankingInformationFieldArray.js

View workflow job for this annotation

GitHub Actions / github-actions-ci

'fields.forEach' is missing in props validation
// TODO: change addres to categoty (id)
const fieldName = `${field}.addressId`;
const { initial, value } = getFieldState(fieldName);

if (!addressesCategoriesIdsMap.has(value) && value !== initial) {
change(fieldName, undefined);
}
});
};

eventEmitter.on(eventType, callback);

return () => {
eventEmitter.off(eventType, callback);
};
}, [change, eventEmitter, fields, getFieldState]);

return <RepeatableFieldWithValidation {...props} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { BankingInformationFieldArray } from './BankingInformationFieldArray'

Check failure on line 1 in src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationFieldArray/index.js

View workflow job for this annotation

GitHub Actions / github-actions-ci

Missing semicolon

Check failure on line 1 in src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationFieldArray/index.js

View workflow job for this annotation

GitHub Actions / github-actions-ci

Missing semicolon
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useMemo } from 'react';
import { FormattedMessage } from 'react-intl';
import { FieldArray } from 'react-final-form-arrays';
import { FormattedMessage } from 'react-intl';

import { Loading } from '@folio/stripes/components';
import { RepeatableFieldWithValidation } from '@folio/stripes-acq-components';

import {
useBankingAccountTypes,
Expand All @@ -16,6 +15,7 @@ import {
import { validatePrimary } from '../../../common/validation';
import { BANKING_INFORMATION_FIELD_NAME } from '../../constants';
import { BankingInformationField } from './BankingInformationField';
import { BankingInformationFieldArray } from './BankingInformationFieldArray';

const renderField = (props) => (name, index, fields) => (
<BankingInformationField
Expand Down Expand Up @@ -44,13 +44,6 @@ export const OrganizationBankingInfoForm = () => {
}));
}, [bankingAccountTypes]);

const categoriesOptions = useMemo(() => {
return categories.map(({ id, value }) => ({
label: value,
value: id,
}));
}, [categories]);

const isLoading = isBankingAccountTypesFetching || isCategoriesFetching;

if (isLoading) {
Expand All @@ -60,12 +53,15 @@ export const OrganizationBankingInfoForm = () => {
return (
<FieldArray
addLabel={<FormattedMessage id="ui-organizations.button.bankingInformation.add" />}
component={RepeatableFieldWithValidation}
component={BankingInformationFieldArray}
id="bankingInformation"
name={BANKING_INFORMATION_FIELD_NAME}
onAdd={createAddNewItem()}
onRemove={removeItem}
renderField={renderField({ categoriesOptions, bankingAccountTypeOptions })}
renderField={renderField({
bankingAccountTypeOptions,
categories,
})}
validate={validatePrimary}
/>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import memoize from 'lodash/memoize';

export const getAddressCategoryIdsSet = memoize((addresses = []) => {
return addresses.reduce((acc, address) => {
address.categories?.forEach(categoryId => acc.add(categoryId));

return acc;
}, new Set());
})

Check failure on line 9 in src/Organizations/OrganizationForm/getAddressCategoryIdsSet.js

View workflow job for this annotation

GitHub Actions / github-actions-ci

Missing semicolon

Check failure on line 9 in src/Organizations/OrganizationForm/getAddressCategoryIdsSet.js

View workflow job for this annotation

GitHub Actions / github-actions-ci

Missing semicolon
23 changes: 20 additions & 3 deletions src/Utils/CategoryDropdown/CategoryDropdown.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import React, { useCallback, useMemo } from 'react';
import find from 'lodash/find';
import PropTypes from 'prop-types';
import { find } from 'lodash';
import { useCallback, useMemo } from 'react';
import { useForm } from 'react-final-form';
import { FormattedMessage } from 'react-intl';

import { OptionSegment } from '@folio/stripes/components';
import { FieldMultiSelectionFinal } from '@folio/stripes-acq-components';

import { filterCategories } from './utils';

function CategoryDropdown({ dropdownVendorCategories, name, withLabel, ariaLabelledBy }) {
function CategoryDropdown({
dropdownVendorCategories,
name,
withLabel,
ariaLabelledBy,
onChange: onChangeProp,
}) {
const { change } = useForm();

const fieldName = name ? `${name}.categories` : 'categories';
const toString = useCallback((option) => (
option ? `${fieldName}-${option}` : option
Expand All @@ -35,6 +44,12 @@ function CategoryDropdown({ dropdownVendorCategories, name, withLabel, ariaLabel
return dropdownVendorCategories.map(item => item.id) || [];
}, [dropdownVendorCategories]);

const onChange = useCallback((value) => {
change(fieldName, value);

if (onChangeProp) onChangeProp(value);
}, [onChangeProp]);

Check warning on line 51 in src/Utils/CategoryDropdown/CategoryDropdown.js

View workflow job for this annotation

GitHub Actions / github-actions-ci

React Hook useCallback has missing dependencies: 'change' and 'fieldName'. Either include them or remove the dependency array

Check warning on line 51 in src/Utils/CategoryDropdown/CategoryDropdown.js

View workflow job for this annotation

GitHub Actions / github-actions-ci

React Hook useCallback has missing dependencies: 'change' and 'fieldName'. Either include them or remove the dependency array

return (
<FieldMultiSelectionFinal
label={withLabel ? <FormattedMessage id="ui-organizations.data.contactTypes.categories" /> : undefined}
Expand All @@ -44,6 +59,7 @@ function CategoryDropdown({ dropdownVendorCategories, name, withLabel, ariaLabel
itemToString={toString}
formatter={formatter}
filter={filterItems}
onChange={onChange}
/>
);
}
Expand All @@ -53,6 +69,7 @@ CategoryDropdown.propTypes = {
name: PropTypes.string,
withLabel: PropTypes.bool,
ariaLabelledBy: PropTypes.string,
onChange: PropTypes.func,
};

CategoryDropdown.defaultProps = {
Expand Down
11 changes: 10 additions & 1 deletion src/common/components/AddressInfo/AddressInfo.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import { useCallback } from 'react';
import {
FormattedMessage,
injectIntl,
Expand All @@ -21,6 +21,8 @@ import {
} from '@folio/stripes-acq-components';

import CategoryDropdown from '../../../Utils/CategoryDropdown';
import { EVENT_EMITTER_EVENTS } from '../../constants';
import { useEventEmitter } from '../../hooks';
import {
createAddNewItem,
removeItem,
Expand All @@ -36,6 +38,12 @@ const AddressInfo = ({
dropdownVendorCategories,
intl,
}) => {
const eventEmitter = useEventEmitter();

const onCategoryChange = useCallback(() => {
eventEmitter.emit(EVENT_EMITTER_EVENTS.ADDRESS_CATEGORY_CHANGED);
}, [eventEmitter]);

const countriesOptions = countries.map(c => ({
label: intl.formatMessage({ id: `stripes-components.countries.${c.alpha2}` }),
value: c.alpha3,
Expand Down Expand Up @@ -161,6 +169,7 @@ const AddressInfo = ({
ariaLabelledBy="addressFormCategoriesLabel"
dropdownVendorCategories={dropdownVendorCategories}
name={name}
onChange={onCategoryChange}
/>
</Col>
</Row>
Expand Down
3 changes: 3 additions & 0 deletions src/common/constants/events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const EVENT_EMITTER_EVENTS = {
ADDRESS_CATEGORY_CHANGED: 'ADDRESS_CATEGORY_CHANGED',
};
1 change: 1 addition & 0 deletions src/common/constants/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './api';
export * from './categories';
export * from './events';
export * from './interfaces';
export * from './organization';
export * from './organizationTypes';
Expand Down
1 change: 1 addition & 0 deletions src/common/hooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './useBankingAccountTypes';
export * from './useBankingInformationMutation';
export * from './useBankingInformationSettings';
export * from './useCategories';
export * from './useEventEmitter';
export * from './useIntegrationConfig';
export * from './useIntegrationConfigMutation';
export * from './useLinkedAgreements';
Expand Down
3 changes: 2 additions & 1 deletion src/common/hooks/useCategories/useCategories.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { useTranslatedCategories } from '../useTranslatedCategories';

const DEFAULT_DATA = [];

export const useCategories = () => {
export const useCategories = (options = {}) => {
const ky = useOkapiKy();
const [namespace] = useNamespace('categories');

Expand All @@ -27,6 +27,7 @@ export const useCategories = () => {
} = useQuery(
[namespace],
() => ky.get(CATEGORIES_API, { searchParams }).json(),
options,
);

const [translatedCategories] = useTranslatedCategories(data?.categories);
Expand Down
1 change: 1 addition & 0 deletions src/common/hooks/useEventEmitter/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useEventEmitter } from './useEventEmitter';
7 changes: 7 additions & 0 deletions src/common/hooks/useEventEmitter/useEventEmitter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { EventEmitter } from '../../utils';

const eventEmitter = new EventEmitter();

export const useEventEmitter = () => {
return eventEmitter;
};
12 changes: 12 additions & 0 deletions src/common/hooks/useEventEmitter/useEventEmitter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { renderHook } from '@folio/jest-config-stripes/testing-library/react';

import { EventEmitter } from '../../utils';
import { useEventEmitter } from './useEventEmitter';

describe('useEventEmitter', () => {
it('should return event emitter instance', async () => {
const { result } = renderHook(() => useEventEmitter());

expect(result.current).toBeInstanceOf(EventEmitter);
});
});
19 changes: 19 additions & 0 deletions src/common/utils/EventEmitter/EventEmitter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export class EventEmitter {
constructor() {
this.eventTarget = new EventTarget();
}

on(eventName, callback) {
this.eventTarget.addEventListener(eventName, callback);
}

off(eventName, callback) {
this.eventTarget.removeEventListener(eventName, callback);
}

emit(eventName, data) {
const event = new CustomEvent(eventName, { detail: data });

this.eventTarget.dispatchEvent(event);
}
}
36 changes: 36 additions & 0 deletions src/common/utils/EventEmitter/EventEmitter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { EventEmitter } from './EventEmitter';

const EVENT_TYPE = 'test-event-type';
const callback = jest.fn();
const payload = 'Test payload';

describe('EventEmitter', () => {
let emitter;

beforeEach(() => {
emitter = new EventEmitter();
callback.mockClear();
});

it('should add and invoke event listeners', () => {
emitter.on(EVENT_TYPE, callback);
emitter.emit(EVENT_TYPE, payload);

expect(callback).toHaveBeenCalledWith(expect.objectContaining({ detail: payload }));
});

it('should remove event listeners', () => {
emitter.on(EVENT_TYPE, callback);
emitter.off(EVENT_TYPE, callback);
emitter.emit(EVENT_TYPE, payload);

expect(callback).not.toHaveBeenCalled();
});

it('should emit events with the correct data', () => {
emitter.on(EVENT_TYPE, callback);
emitter.emit(EVENT_TYPE, payload);

expect(callback).toHaveBeenCalledWith(expect.objectContaining({ detail: payload }));
});
});
1 change: 1 addition & 0 deletions src/common/utils/EventEmitter/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { EventEmitter } from './EventEmitter';
1 change: 1 addition & 0 deletions src/common/utils/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './category';
export * from './createItem';
export * from './EventEmitter';
export * from './getArrayItemsChanges';
export * from './getResourceData';
export * from './hydrateContactInfo';
Expand Down

0 comments on commit 2e0e557

Please sign in to comment.