diff --git a/CHANGELOG.md b/CHANGELOG.md index acb5149f..ab1fef11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Change history for ui-organizations ## 6.0.0 (IN PROGRESS) + +* *BREAKING* Display all versions in change log in fourth pane. Refs UIORGS-355. +* Show in version history record view, which fields have been edited. Refs UIORGS-356. +* Adapt organization metadata fields to version history mechanism. Refs UIORGS-359. * *BREAKING* Add number generator for vendor code including settings page. Refs UIORGS-336, UIORGS-337. ## [5.2.0](https://github.com/folio-org/ui-organizations/tree/v5.2.0) (2024-10-31) diff --git a/package.json b/package.json index 05d77ebc..8cce3209 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "home": "/organizations", "okapiInterfaces": { "acquisition-methods": "1.0", + "acquisition-organization-events": "1.0", "banking-information": "1.0", "data-export-spring": "1.0 2.0", "configuration": "2.0", @@ -68,6 +69,7 @@ "replaces": ["ui-organizations.third-party-services"], "visible": false, "subPermissions": [ + "acquisition.organization.events.get", "acquisitions-units.memberships.collection.get", "acquisitions-units.units.collection.get", "configuration.entries.collection.get", diff --git a/src/ContactPeople/ContactPerson/index.js b/src/ContactPeople/ContactPerson/index.js index 01188197..1ea57a2f 100644 --- a/src/ContactPeople/ContactPerson/index.js +++ b/src/ContactPeople/ContactPerson/index.js @@ -1 +1,2 @@ export { default } from './ContactPerson'; +export { default as ContactPersonSection } from './ContactPersonSection'; diff --git a/src/Organizations/OrganizationDetails/OrganizationDetails.js b/src/Organizations/OrganizationDetails/OrganizationDetails.js index d0990cf8..fca690ff 100644 --- a/src/Organizations/OrganizationDetails/OrganizationDetails.js +++ b/src/Organizations/OrganizationDetails/OrganizationDetails.js @@ -39,10 +39,12 @@ import { TagsPane, useAcqRestrictions, useModalToggle, + VersionHistoryButton, } from '@folio/stripes-acq-components'; import { NOTES_ROUTE, + ORGANIZATION_VERSIONS_VIEW_ROUTE, ORGANIZATIONS_ROUTE, } from '../../common/constants'; import { @@ -135,6 +137,13 @@ const OrganizationDetails = ({ if (isDetailsPaneInFocus) paneTitleRef.current.focus(); }, [isDetailsPaneInFocus]); + const openVersionHistory = useCallback(() => { + history.push({ + pathname: ORGANIZATION_VERSIONS_VIEW_ROUTE.replace(':id', organization.id), + search: location.search, + }); + }, [history, location.search, organization.id]); + const getActionMenu = useCallback( ({ onToggle }) => { return ( @@ -200,6 +209,7 @@ const OrganizationDetails = ({ tagsQuantity={get(organization, 'tags.tagList', []).length} tagsToggle={toggleTagsPane} /> + ); diff --git a/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/OrganizationBankingInfoForm.test.js b/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/OrganizationBankingInfoForm.test.js index 9708bcbf..c1183c73 100644 --- a/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/OrganizationBankingInfoForm.test.js +++ b/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/OrganizationBankingInfoForm.test.js @@ -92,7 +92,7 @@ describe('OrganizationBankingInfoForm', () => { await user.click(screen.getAllByText('stripes-components.selection.controlLabel')[1]); bankingAccountTypes.forEach(({ name }) => { - expect(screen.getByText(name)).toBeInTheDocument(); + expect(screen.getAllByText(name)[0]).toBeInTheDocument(); }); }); @@ -102,10 +102,10 @@ describe('OrganizationBankingInfoForm', () => { renderOrganizationBankingInfoForm(); await addField(); - await user.click(screen.getAllByText('stripes-components.selection.controlLabel')[0]); + await user.click(screen.getByRole('button', { name: 'ui-organizations.data.bankingInformation.addressCategory' })); categories.forEach(({ value }) => { - expect(within(screen.getByTestId('banking-information-card')).getByText(value)).toBeInTheDocument(); + expect(screen.getAllByText(value)[0]).toBeInTheDocument(); }); }); diff --git a/src/Organizations/OrganizationVersion/OrganizationVersion.js b/src/Organizations/OrganizationVersion/OrganizationVersion.js new file mode 100644 index 00000000..c6551cf6 --- /dev/null +++ b/src/Organizations/OrganizationVersion/OrganizationVersion.js @@ -0,0 +1,122 @@ +import get from 'lodash/get'; +import { + memo, + useCallback, + useMemo, +} from 'react'; +import ReactRouterPropTypes from 'react-router-prop-types'; + +import { + useOrganization, + VersionHistoryPane, + VersionView, + VersionViewContextProvider, +} from '@folio/stripes-acq-components'; +import { TitleManager } from '@folio/stripes/core'; + +import { + ORGANIZATION_VERSIONS_VIEW_ROUTE, + ORGANIZATIONS_ROUTE, + VIEW_ORG_DETAILS, +} from '../../common/constants'; +import { HIDDEN_FIELDS_FOR_ORGANIZATION_VERSION_HISTORY } from '../constants'; +import { getOrganizationFieldsLabelMap } from './getOrganizationFieldsLabelMap'; +import { + useOrganizationVersions, + useSelectedOrganizationVersion, +} from './hooks'; +import { OrganizationVersionView } from './OrganizationVersionView'; + +const OrganizationVersion = ({ + history, + location, + match, +}) => { + const { id: organizationId, versionId } = match.params; + const snapshotPath = 'organizationSnapshot.map'; + + const { + isLoading: isOrganizationLoading, + organization, + } = useOrganization(organizationId); + + const onHistoryClose = useCallback(() => history.push({ + pathname: `${VIEW_ORG_DETAILS}${organizationId}`, + search: location.search, + }), [history, location.search, organizationId]); + + const onVersionClose = useCallback(() => history.push({ + pathname: ORGANIZATIONS_ROUTE, + search: location.search, + }), [history, location.search]); + + const onSelectVersion = useCallback((_versionId) => { + history.push({ + pathname: `${ORGANIZATION_VERSIONS_VIEW_ROUTE.replace(':id', organizationId)}/${_versionId}`, + search: location.search, + }); + }, [history, location.search, organizationId]); + + const { + versions, + isLoading: isHistoryLoading, + } = useOrganizationVersions(organizationId, { + onSuccess: ({ organizationAuditEvents }) => { + if (!versionId && organizationAuditEvents[0]?.id) onSelectVersion(organizationAuditEvents[0].id); + }, + }); + + const { + isLoading: isOrganizationVersionLoading, + selectedVersion, + } = useSelectedOrganizationVersion({ versionId, versions, snapshotPath }); + + const isVersionLoading = ( + isOrganizationLoading + || isHistoryLoading + || isOrganizationVersionLoading + ); + + const labelsMap = useMemo(() => getOrganizationFieldsLabelMap(), []); + + return ( + + + + + + + + + ); +}; + +OrganizationVersion.propTypes = { + history: ReactRouterPropTypes.history, + location: ReactRouterPropTypes.location, + match: ReactRouterPropTypes.match, +}; + +export default memo(OrganizationVersion); diff --git a/src/Organizations/OrganizationVersion/OrganizationVersion.test.js b/src/Organizations/OrganizationVersion/OrganizationVersion.test.js new file mode 100644 index 00000000..594c149e --- /dev/null +++ b/src/Organizations/OrganizationVersion/OrganizationVersion.test.js @@ -0,0 +1,141 @@ +import { + QueryClient, + QueryClientProvider, +} from 'react-query'; +import { + MemoryRouter, + Route, + Switch, + withRouter, +} from 'react-router-dom'; + +import { + render, + screen, +} from '@folio/jest-config-stripes/testing-library/react'; +import user from '@folio/jest-config-stripes/testing-library/user-event'; +import { useOkapiKy } from '@folio/stripes/core'; +import { + AUDIT_ACQ_EVENTS_API, + useOrganization, +} from '@folio/stripes-acq-components'; + +import { + organization, + organizationAuditEvent, +} from 'fixtures'; +import { ORGANIZATION_VERSIONS_VIEW_ROUTE } from '../../common/constants'; +import OrganizationVersion from './OrganizationVersion'; + +jest.mock('@folio/stripes-acq-components', () => ({ + ...jest.requireActual('@folio/stripes-acq-components'), + useOrganization: jest.fn(() => ({})), +})); + +const { organizationSnapshot, ...auditEvent } = organizationAuditEvent; + +const latestSnapshot = { + ...organizationSnapshot.map, + edition: 'Second edition', +}; +const originalSnapshot = { ...organizationSnapshot }; + +const versions = [ + { + ...auditEvent, + id: 'testAuditEventId', + organizationSnapshot: { map: latestSnapshot }, + }, + { + ...auditEvent, + organizationSnapshot: { map: originalSnapshot }, + }, +]; + +const kyMock = { + get: jest.fn((url) => ({ + json: async () => { + const result = {}; + + if (url.startsWith(`${AUDIT_ACQ_EVENTS_API}/organization/`)) { + result.organizationAuditEvents = versions; + } + + return Promise.resolve({ + isLoading: false, + ...result, + }); + }, + })), +}; + +const queryClient = new QueryClient(); +const wrapper = ({ children }) => ( + + + {children} + + +); + +const Component = withRouter(OrganizationVersion); +const mockDefaultContent = 'Hello world'; + +const renderOrganizationVersion = (props = {}) => render( + + ( + + )} + /> + ( +
{mockDefaultContent}
+ )} + /> +
, + { wrapper }, +); + +describe('OrganizationVersion', () => { + beforeEach(() => { + useOkapiKy.mockReturnValue(kyMock); + useOrganization.mockReturnValue({ + isLoading: false, + organization, + }); + }); + + it('should close version view when \'Version close\' button was clicked', async () => { + renderOrganizationVersion(); + + await screen.findAllByRole('button', { name: 'stripes-acq-components.versionHistory.card.select.tooltip' }) + .then(async ([selectVersionBtn]) => user.click(selectVersionBtn)); + + await screen.findAllByRole('button', { name: 'stripes-components.closeItem' }) + .then(async ([closeVersionBtn]) => user.click(closeVersionBtn)); + + expect(screen.queryByText(organization.name)).not.toBeInTheDocument(); + expect(screen.getByText(mockDefaultContent)).toBeInTheDocument(); + }); + + it('should close version view when \'History close\' button was clicked', async () => { + renderOrganizationVersion(); + + await screen.findAllByRole('button', { name: 'stripes-acq-components.versionHistory.card.select.tooltip' }) + .then(async ([selectVersionBtn]) => user.click(selectVersionBtn)); + + await screen.findAllByRole('button', { name: 'stripes-components.closeItem' }) + .then(async ([_, closeHistoryBtn]) => user.click(closeHistoryBtn)); + + expect(screen.queryByText(organization.name)).not.toBeInTheDocument(); + expect(screen.getByText(mockDefaultContent)).toBeInTheDocument(); + }); +}); diff --git a/src/Organizations/OrganizationVersion/OrganizationVersionView/OrganizationVersionView.js b/src/Organizations/OrganizationVersion/OrganizationVersionView/OrganizationVersionView.js new file mode 100644 index 00000000..d96763da --- /dev/null +++ b/src/Organizations/OrganizationVersion/OrganizationVersionView/OrganizationVersionView.js @@ -0,0 +1,147 @@ +import PropTypes from 'prop-types'; +import { useRef } from 'react'; + +import { + Accordion, + AccordionSet, + AccordionStatus, + checkScope, + Col, + collapseAllSections, + ExpandAllButton, + expandAllSections, + HasCommand, + Row, +} from '@folio/stripes/components'; + +import { + ORGANIZATION_SECTION_LABELS, + ORGANIZATION_SECTIONS, +} from '../../constants'; +import { + OrganizationAccountsVersionView, + OrganizationContactInfoVersionView, + OrganizationContactPeopleVersionView, + OrganizationInterfacesVersionView, + OrganizationSummaryVersionView, + OrganizationVendorInfoVersionView, + OrganizationVendorTermsVersionView, +} from '../components'; + +const initialAccordionStatus = { + [ORGANIZATION_SECTIONS.summarySection]: true, + [ORGANIZATION_SECTIONS.contactInformationSection]: false, + [ORGANIZATION_SECTIONS.contactPeopleSection]: true, + [ORGANIZATION_SECTIONS.interfacesSection]: false, + [ORGANIZATION_SECTIONS.vendorInformationSection]: false, + [ORGANIZATION_SECTIONS.vendorTermsSection]: false, + [ORGANIZATION_SECTIONS.integrationDetailsSection]: false, + [ORGANIZATION_SECTIONS.accountsSection]: false, + [ORGANIZATION_SECTIONS.agreements]: false, +}; + +export const OrganizationVersionView = ({ version }) => { + const accordionStatusRef = useRef(); + + const shortcuts = [ + { + name: 'expandAllSections', + handler: (e) => expandAllSections(e, accordionStatusRef), + }, + { + name: 'collapseAllSections', + handler: (e) => collapseAllSections(e, accordionStatusRef), + }, + ]; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + { + version?.isVendor && ( + <> + + + + + + + + + + + + + ) + } + + + + ); +}; + +OrganizationVersionView.propTypes = { + version: PropTypes.object, +}; diff --git a/src/Organizations/OrganizationVersion/OrganizationVersionView/OrganizationVersionView.test.js b/src/Organizations/OrganizationVersion/OrganizationVersionView/OrganizationVersionView.test.js new file mode 100644 index 00000000..63b2450f --- /dev/null +++ b/src/Organizations/OrganizationVersion/OrganizationVersionView/OrganizationVersionView.test.js @@ -0,0 +1,68 @@ +import { + QueryClient, + QueryClientProvider, +} from 'react-query'; +import { MemoryRouter } from 'react-router-dom'; + +import { + render, + screen, +} from '@folio/jest-config-stripes/testing-library/react'; +import { useCategories } from '@folio/stripes-acq-components'; + +import { + organization, + organizationAuditEvent, +} from 'fixtures'; +import { ORGANIZATION_VERSIONS_VIEW_ROUTE } from '../../../common/constants'; +import { OrganizationVersionView } from './OrganizationVersionView'; + +jest.mock('@folio/stripes-acq-components', () => ({ + ...jest.requireActual('@folio/stripes-acq-components'), + useCategories: jest.fn(), +})); + +const { organizationSnapshot } = organizationAuditEvent; + +const queryClient = new QueryClient(); +const wrapper = ({ children }) => ( + + + {children} + + +); + +const renderOrganizationVersionView = (props = {}) => render( + , + { wrapper }, +); + +describe('OrganizationVersion', () => { + beforeEach(() => { + useCategories.mockReturnValue({ categories: [{ id: 'f52ceea4-8e35-404b-9ebd-5c7db6613195', value: 'cat' }] }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render version history view', async () => { + renderOrganizationVersionView(); + + expect(screen.getByText('ui-organizations.summary')).toBeInTheDocument(); + expect(screen.getByText('ui-organizations.contactInformation')).toBeInTheDocument(); + expect(screen.getByText('ui-organizations.contactPeople')).toBeInTheDocument(); + expect(screen.getByText('ui-organizations.interface')).toBeInTheDocument(); + expect(screen.getByText('ui-organizations.vendorInformation')).toBeInTheDocument(); + expect(screen.getByText('ui-organizations.vendorTerms')).toBeInTheDocument(); + expect(screen.getByText('ui-organizations.accounts')).toBeInTheDocument(); + }); +}); diff --git a/src/Organizations/OrganizationVersion/OrganizationVersionView/index.js b/src/Organizations/OrganizationVersion/OrganizationVersionView/index.js new file mode 100644 index 00000000..90186b5f --- /dev/null +++ b/src/Organizations/OrganizationVersion/OrganizationVersionView/index.js @@ -0,0 +1 @@ +export { OrganizationVersionView } from './OrganizationVersionView'; diff --git a/src/Organizations/OrganizationVersion/components/OrganizationAccountsVersionView/OrganizationAccountsVersionView.js b/src/Organizations/OrganizationVersion/components/OrganizationAccountsVersionView/OrganizationAccountsVersionView.js new file mode 100644 index 00000000..b09ebc50 --- /dev/null +++ b/src/Organizations/OrganizationVersion/components/OrganizationAccountsVersionView/OrganizationAccountsVersionView.js @@ -0,0 +1,122 @@ +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; + +import { + Col, + Row, +} from '@folio/stripes/components'; +import { + PAYMENT_METHOD_LABELS, + VersionKeyValue, +} from '@folio/stripes-acq-components'; + +import css from '../../../OrganizationDetails/OrganizationAccounts/OrganizationAccount/OrganizationAccount.css'; + +export const OrganizationAccountsVersionView = ({ name, version }) => { + if (!version?.accounts?.length) { + return ( +

+ +

+ ); + } + + return ( + <> + {version?.accounts?.map((account, indx) => { + return ( + + + } + value={account?.name} + /> + + + + } + value={account?.accountNo} + /> + + + + } + value={account?.description} + /> + + + + } + value={account?.appSystemNo} + /> + + + + } + value={PAYMENT_METHOD_LABELS[account?.paymentMethod]} + /> + + + + } + value={account?.contactInfo} + /> + + + + } + value={account?.libraryCode} + /> + + + + } + value={account?.libraryEdiCode} + /> + + + + } + value={account?.notes} + /> + + + + } + value={account?.acqUnits} + multiple + /> + + + ); + })} + + ); +}; + +OrganizationAccountsVersionView.propTypes = { + name: PropTypes.string.isRequired, + version: PropTypes.object, +}; diff --git a/src/Organizations/OrganizationVersion/components/OrganizationAccountsVersionView/index.js b/src/Organizations/OrganizationVersion/components/OrganizationAccountsVersionView/index.js new file mode 100644 index 00000000..8b3dfc55 --- /dev/null +++ b/src/Organizations/OrganizationVersion/components/OrganizationAccountsVersionView/index.js @@ -0,0 +1 @@ +export { OrganizationAccountsVersionView } from './OrganizationAccountsVersionView'; diff --git a/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/OrganizationContactInfoVersionView.js b/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/OrganizationContactInfoVersionView.js new file mode 100644 index 00000000..1e8f8c0f --- /dev/null +++ b/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/OrganizationContactInfoVersionView.js @@ -0,0 +1,98 @@ +import cloneDeep from 'lodash/cloneDeep'; +import get from 'lodash/get'; +import PropTypes from 'prop-types'; +import { useMemo } from 'react'; + +import { + Accordion, + Layout, +} from '@folio/stripes/components'; +import { useCategories } from '@folio/stripes-acq-components'; + +import { + UNCATEGORIZED_ID, + UNCATEGORIZED_VALUE, +} from '../../../../common/constants'; +import { + hydrateAddresses, + mixCategories, +} from '../../../../common/utils'; +import { + ContactAddressesVersionView, + ContactPersonEmailsVersionView, + ContactPersonPhonesVersionView, + ContactPersonURLsVersionView, +} from './components'; + +const setInitialArrayFieldPaths = (obj, paths) => { + return paths.reduce((result, path) => { + const target = get(result, path, []); + + target.forEach((item, indx) => { + item._initialFieldPath = `${path}[${indx}]`; + }); + + return result; + }, cloneDeep(obj ?? {})); +}; + +export const OrganizationContactInfoVersionView = ({ version: currentVersion }) => { + /* + Entities are grouped by categories, so the order in the arrays is not guaranteed. + So we need to keep the initial field paths + */ + const version = setInitialArrayFieldPaths(currentVersion, ['addresses', 'emails', 'phoneNumbers', 'urls']); + + const { categories } = useCategories(); + + const groups = useMemo(() => [ + ...(categories ?? []), + { + id: UNCATEGORIZED_ID, + value: UNCATEGORIZED_VALUE, + }, + ], [categories]); + + const data = useMemo(() => groups.reduce((acc, { id }) => { + const filterCb = ({ categories: entityCategories = [] }) => { + return id === UNCATEGORIZED_ID ? entityCategories.length === 0 : entityCategories.includes(id); + }; + + const addresses = (version?.addresses || []).filter(filterCb); + const emails = (version?.emails || []).filter(filterCb); + const phoneNumbers = (version?.phoneNumbers || []).filter(filterCb); + const urls = (version?.urls || []).filter(filterCb); + + if (addresses.length || emails.length || phoneNumbers.length || urls.length) { + acc[id] = { + addresses: hydrateAddresses(categories, addresses), + emails: mixCategories(categories, emails), + phoneNumbers: mixCategories(categories, phoneNumbers), + urls: mixCategories(categories, urls), + }; + } + + return acc; + }, {}), [categories, groups, version]); + + return ( + + {groups.map((category) => data[category.id] && ( + + + + + + + ))} + + ); +}; + +OrganizationContactInfoVersionView.propTypes = { + version: PropTypes.object, +}; diff --git a/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/components/ContactAddressesVersionView.js b/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/components/ContactAddressesVersionView.js new file mode 100644 index 00000000..aac1908b --- /dev/null +++ b/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/components/ContactAddressesVersionView.js @@ -0,0 +1,97 @@ +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; + +import { + Card, + Col, + Row, +} from '@folio/stripes/components'; +import { VersionKeyValue } from '@folio/stripes-acq-components'; + +import { ContactCardHeaderVersionView } from './ContactCardHeaderVersionView'; + +export const ContactAddressesVersionView = ({ addresses }) => { + return ( + + + {addresses?.map((address) => { + return ( + + )} + > + + + } + value={address?.addressLine1} + /> + + + } + value={address?.addressLine2} + /> + + + } + value={address?.city} + /> + + + } + value={address?.stateRegion} + /> + + + } + value={address?.zipCode} + /> + + + } + value={address?.country} + /> + + + } + value={address?.language} + /> + + + } + value={address?.categories} + multiple + /> + + + + ); + })} + + + ); +}; + +ContactAddressesVersionView.propTypes = { + addresses: PropTypes.arrayOf(PropTypes.object), +}; diff --git a/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/components/ContactCardHeaderVersionView/ContactCardHeaderVersionView.css b/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/components/ContactCardHeaderVersionView/ContactCardHeaderVersionView.css new file mode 100644 index 00000000..ac0e1af8 --- /dev/null +++ b/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/components/ContactCardHeaderVersionView/ContactCardHeaderVersionView.css @@ -0,0 +1,7 @@ +.cardHeader { + margin: 0; + + & .mark { + background-color: mark; + } +} diff --git a/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/components/ContactCardHeaderVersionView/ContactCardHeaderVersionView.js b/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/components/ContactCardHeaderVersionView/ContactCardHeaderVersionView.js new file mode 100644 index 00000000..ac903107 --- /dev/null +++ b/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/components/ContactCardHeaderVersionView/ContactCardHeaderVersionView.js @@ -0,0 +1,28 @@ +import PropTypes from 'prop-types'; +import { useContext } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { VersionViewContext } from '@folio/stripes-acq-components'; + +import css from './ContactCardHeaderVersionView.css'; + +export const ContactCardHeaderVersionView = ({ isPrimary, name }) => { + const versionContext = useContext(VersionViewContext); + + const isUpdated = versionContext?.paths?.includes(name); + + const headerStart = ( +

+ + + +

+ ); + + return headerStart; +}; + +ContactCardHeaderVersionView.propTypes = { + isPrimary: PropTypes.bool.isRequired, + name: PropTypes.string.isRequired, +}; diff --git a/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/components/ContactCardHeaderVersionView/index.js b/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/components/ContactCardHeaderVersionView/index.js new file mode 100644 index 00000000..f958e7ee --- /dev/null +++ b/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/components/ContactCardHeaderVersionView/index.js @@ -0,0 +1 @@ +export { ContactCardHeaderVersionView } from './ContactCardHeaderVersionView'; diff --git a/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/components/ContactPersonEmailsVersionView.js b/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/components/ContactPersonEmailsVersionView.js new file mode 100644 index 00000000..80d17f0e --- /dev/null +++ b/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/components/ContactPersonEmailsVersionView.js @@ -0,0 +1,82 @@ +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; + +import { + Card, + Col, + Row, +} from '@folio/stripes/components'; +import { + LANG_LABEL_BY_CODE, + VersionKeyValue, +} from '@folio/stripes-acq-components'; + +import { ContactPersonSection } from '../../../../../ContactPeople/ContactPerson'; +import { ContactCardHeaderVersionView } from './ContactCardHeaderVersionView'; + +export const ContactPersonEmailsVersionView = ({ emails }) => { + if (!emails?.length) return null; + + const renderBody = () => ( + + + {emails?.map((email) => { + return ( + + )} + > + + + } + value={email?.value} + /> + + + } + value={email?.description} + /> + + + } + value={LANG_LABEL_BY_CODE[email?.language] || email?.language} + /> + + + } + value={email.categories} + multiple + /> + + + + ); + })} + + + ); + + return ( + } + renderBody={renderBody} + /> + ); +}; + +ContactPersonEmailsVersionView.propTypes = { + emails: PropTypes.arrayOf(PropTypes.object), +}; diff --git a/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/components/ContactPersonPhonesVersionView.js b/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/components/ContactPersonPhonesVersionView.js new file mode 100644 index 00000000..672d1333 --- /dev/null +++ b/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/components/ContactPersonPhonesVersionView.js @@ -0,0 +1,82 @@ +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; + +import { + Card, + Col, + Row, +} from '@folio/stripes/components'; +import { + LANG_LABEL_BY_CODE, + VersionKeyValue, +} from '@folio/stripes-acq-components'; + +import { ContactPersonSection } from '../../../../../ContactPeople/ContactPerson'; +import { ContactCardHeaderVersionView } from './ContactCardHeaderVersionView'; + +export const ContactPersonPhonesVersionView = ({ phones }) => { + if (!phones?.length) return null; + + const renderBody = () => ( + + + {phones?.map((phone) => { + return ( + + )} + > + + + } + value={phone?.phoneNumber} + /> + + + } + value={phone?.type} + /> + + + } + value={LANG_LABEL_BY_CODE[phone?.language] || phone?.language} + /> + + + } + value={phone.categories} + multiple + /> + + + + ); + })} + + + ); + + return ( + } + renderBody={renderBody} + /> + ); +}; + +ContactPersonPhonesVersionView.propTypes = { + phones: PropTypes.arrayOf(PropTypes.object), +}; diff --git a/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/components/ContactPersonURLsVersionView.js b/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/components/ContactPersonURLsVersionView.js new file mode 100644 index 00000000..e4c4e8a7 --- /dev/null +++ b/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/components/ContactPersonURLsVersionView.js @@ -0,0 +1,90 @@ +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; + +import { + Card, + Col, + Row, + TextLink, +} from '@folio/stripes/components'; +import { + LANG_LABEL_BY_CODE, + VersionKeyValue, +} from '@folio/stripes-acq-components'; + +import { ContactPersonSection } from '../../../../../ContactPeople/ContactPerson'; +import { ContactCardHeaderVersionView } from './ContactCardHeaderVersionView'; + +export const ContactPersonURLsVersionView = ({ urls }) => { + if (!urls?.length) return null; + + const renderBody = () => ( + + + {urls?.map((url) => { + return ( + + )} + > + + + } + > + + {url.value} + + + + + } + value={url?.description} + /> + + + } + value={LANG_LABEL_BY_CODE[url?.language] || url?.language} + /> + + + } + value={url.categories} + multiple + /> + + + + ); + })} + + + ); + + return ( + } + renderBody={renderBody} + /> + ); +}; + +ContactPersonURLsVersionView.propTypes = { + urls: PropTypes.arrayOf(PropTypes.object), +}; diff --git a/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/components/index.js b/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/components/index.js new file mode 100644 index 00000000..6810bca3 --- /dev/null +++ b/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/components/index.js @@ -0,0 +1,4 @@ +export { ContactAddressesVersionView } from './ContactAddressesVersionView'; +export { ContactPersonEmailsVersionView } from './ContactPersonEmailsVersionView'; +export { ContactPersonPhonesVersionView } from './ContactPersonPhonesVersionView'; +export { ContactPersonURLsVersionView } from './ContactPersonURLsVersionView'; diff --git a/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/index.js b/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/index.js new file mode 100644 index 00000000..bc291929 --- /dev/null +++ b/src/Organizations/OrganizationVersion/components/OrganizationContactInfoVersionView/index.js @@ -0,0 +1 @@ +export { OrganizationContactInfoVersionView } from './OrganizationContactInfoVersionView'; diff --git a/src/Organizations/OrganizationVersion/components/OrganizationContactPeopleVersionView/OrganizationContactPeopleVersionView.js b/src/Organizations/OrganizationVersion/components/OrganizationContactPeopleVersionView/OrganizationContactPeopleVersionView.js new file mode 100644 index 00000000..2e40d058 --- /dev/null +++ b/src/Organizations/OrganizationVersion/components/OrganizationContactPeopleVersionView/OrganizationContactPeopleVersionView.js @@ -0,0 +1,79 @@ +import find from 'lodash/find'; +import get from 'lodash/get'; +import PropTypes from 'prop-types'; +import { useMemo } from 'react'; +import { + FormattedMessage, + useIntl, +} from 'react-intl'; + +import { + MultiColumnList, + NoValue, +} from '@folio/stripes/components'; +import { useCategories } from '@folio/stripes-acq-components'; + +import { Ellipsis } from '../../../../common/components'; +import { useVersionWrappedRowFormatter } from '../../../../common/hooks'; +import { transformCategoryIdsToLables } from '../../../../common/utils'; + +const visibleColumns = [ + 'name', + 'categories', + 'email', + 'phone', + 'status', + 'notes', +]; + +const columnMapping = { + name: , + categories: , + email: , + phone: , + status: , + notes: , +}; + +const getResultsFormatter = ({ intl, categories }) => ({ + name: ({ isDeleted, firstName, lastName }) => ( + isDeleted + ? intl.formatMessage({ id: 'ui-organizations.contactPeople.removedContact' }) + : `${lastName}, ${firstName}` + ), + categories: ({ categories: vendorCategories = [] }) => { + return transformCategoryIdsToLables(categories, vendorCategories) || ; + }, + email: c => get(find(c.emails, 'isPrimary'), 'value', '') || , + phone: c => get(find(c.phoneNumbers, 'isPrimary'), 'phoneNumber', '') || , + status: c => , + notes: c => {c.notes}, +}); + +export const OrganizationContactPeopleVersionView = ({ name, version }) => { + const intl = useIntl(); + + const { categories } = useCategories(); + + const rowFormatter = useVersionWrappedRowFormatter({ name }); + + const resultsFormatter = useMemo(() => { + return getResultsFormatter({ intl, categories }); + }, [intl, categories]); + + return ( + + ); +}; + +OrganizationContactPeopleVersionView.propTypes = { + name: PropTypes.string.isRequired, + version: PropTypes.object, +}; diff --git a/src/Organizations/OrganizationVersion/components/OrganizationContactPeopleVersionView/index.js b/src/Organizations/OrganizationVersion/components/OrganizationContactPeopleVersionView/index.js new file mode 100644 index 00000000..72ff74a1 --- /dev/null +++ b/src/Organizations/OrganizationVersion/components/OrganizationContactPeopleVersionView/index.js @@ -0,0 +1 @@ +export { OrganizationContactPeopleVersionView } from './OrganizationContactPeopleVersionView'; diff --git a/src/Organizations/OrganizationVersion/components/OrganizationInterfacesVersionView/OrganizationInterfacesVersionView.js b/src/Organizations/OrganizationVersion/components/OrganizationInterfacesVersionView/OrganizationInterfacesVersionView.js new file mode 100644 index 00000000..b7a2570a --- /dev/null +++ b/src/Organizations/OrganizationVersion/components/OrganizationInterfacesVersionView/OrganizationInterfacesVersionView.js @@ -0,0 +1,63 @@ +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; + +import { + Col, + MultiColumnList, + NoValue, + Row, + TextLink, +} from '@folio/stripes/components'; + +import { useVersionWrappedRowFormatter } from '../../../../common/hooks'; + +const columnMapping = { + interfaceName: , + interfaceUrl: , +}; + +const visibleColumns = ['interfaceName', 'interfaceUrl']; + +const resultsFormatter = { + interfaceName: ({ name }) => name, + interfaceUrl: (item) => ( + item.uri + ? ( + + {item.uri} + + ) + : + ), +}; + +export const OrganizationInterfacesVersionView = ({ + name, + version, +}) => { + const rowFormatter = useVersionWrappedRowFormatter({ name }); + + return ( + + + + + + ); +}; + +OrganizationInterfacesVersionView.propTypes = { + name: PropTypes.string.isRequired, + version: PropTypes.object, +}; diff --git a/src/Organizations/OrganizationVersion/components/OrganizationInterfacesVersionView/index.js b/src/Organizations/OrganizationVersion/components/OrganizationInterfacesVersionView/index.js new file mode 100644 index 00000000..06da8449 --- /dev/null +++ b/src/Organizations/OrganizationVersion/components/OrganizationInterfacesVersionView/index.js @@ -0,0 +1 @@ +export { OrganizationInterfacesVersionView } from './OrganizationInterfacesVersionView'; diff --git a/src/Organizations/OrganizationVersion/components/OrganizationSummaryVersionView/OrganizationSummaryVersionView.js b/src/Organizations/OrganizationVersion/components/OrganizationSummaryVersionView/OrganizationSummaryVersionView.js new file mode 100644 index 00000000..2385fd1a --- /dev/null +++ b/src/Organizations/OrganizationVersion/components/OrganizationSummaryVersionView/OrganizationSummaryVersionView.js @@ -0,0 +1,130 @@ +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; + +import { + Col, + NoValue, + Row, +} from '@folio/stripes/components'; +import { ViewMetaData } from '@folio/stripes/smart-components'; +import { + LANG_LABEL_BY_CODE, + VersionCheckbox, + VersionKeyValue, +} from '@folio/stripes-acq-components'; + +import { ORGANIZATION_SECTIONS } from '../../../constants'; + +export const OrganizationSummaryVersionView = ({ version }) => { + return ( + <> + + + {version?.metadata && ( + + )} + + + + + + } + value={version?.name} + /> + + + + } + value={version?.code} + /> + + + + } + value={version?.erpCode || } + /> + + + + } + value={version?.status && } + /> + + + + } + value={LANG_LABEL_BY_CODE[version?.language] || version?.language || } + /> + + + + } + value={version?.organizationTypes?.join(', ') || } + multiple + /> + + + + } + value={version?.acqUnits} + multiple + /> + + + + } + value={version?.description || } + /> + + + + } + /> + + + + } + /> + + + + } + value={version?.alternativeNames || } + multiple + /> + + + + ); +}; + +OrganizationSummaryVersionView.propTypes = { + version: PropTypes.object, +}; diff --git a/src/Organizations/OrganizationVersion/components/OrganizationSummaryVersionView/index.js b/src/Organizations/OrganizationVersion/components/OrganizationSummaryVersionView/index.js new file mode 100644 index 00000000..bb3ea69e --- /dev/null +++ b/src/Organizations/OrganizationVersion/components/OrganizationSummaryVersionView/index.js @@ -0,0 +1 @@ +export { OrganizationSummaryVersionView } from './OrganizationSummaryVersionView'; diff --git a/src/Organizations/OrganizationVersion/components/OrganizationVendorInfoVersionView/OrganizationVendorInfoVersionView.js b/src/Organizations/OrganizationVersion/components/OrganizationVendorInfoVersionView/OrganizationVendorInfoVersionView.js new file mode 100644 index 00000000..dfe40ec7 --- /dev/null +++ b/src/Organizations/OrganizationVersion/components/OrganizationVendorInfoVersionView/OrganizationVendorInfoVersionView.js @@ -0,0 +1,135 @@ +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; + +import { + PAYMENT_METHOD_LABELS, + VersionCheckbox, + VersionKeyValue, +} from '@folio/stripes-acq-components'; +import { + Col, + Row, +} from '@folio/stripes/components'; + +export const OrganizationVendorInfoVersionView = ({ version }) => { + return ( + + + } + value={PAYMENT_METHOD_LABELS[version?.paymentMethod]} + /> + + + + } + value={version?.vendorCurrenciesValue} + multiple + /> + + + +
+ + + + } + value={version?.claimingInterval} + /> + + + + } + value={version?.discountPercent} + /> + + + + } + value={version?.expectedActivationInterval} + /> + + + + } + value={version?.expectedInvoiceInterval} + /> + + + + } + value={version?.expectedReceiptInterval} + /> + + + + } + value={version?.renewalActivationInterval} + /> + + + + } + value={version?.subscriptionInterval} + /> + + + + } + /> + + + +
+ + + + } + value={version?.taxId} + /> + + + + } + value={version?.taxPercentage} + /> + + + + } + /> + +
+ ); +}; + +OrganizationVendorInfoVersionView.propTypes = { + version: PropTypes.object, +}; diff --git a/src/Organizations/OrganizationVersion/components/OrganizationVendorInfoVersionView/index.js b/src/Organizations/OrganizationVersion/components/OrganizationVendorInfoVersionView/index.js new file mode 100644 index 00000000..745e857b --- /dev/null +++ b/src/Organizations/OrganizationVersion/components/OrganizationVendorInfoVersionView/index.js @@ -0,0 +1 @@ +export { OrganizationVendorInfoVersionView } from './OrganizationVendorInfoVersionView'; diff --git a/src/Organizations/OrganizationVersion/components/OrganizationVendorTermsVersionView/OrganizationVendorTermsVersionView.js b/src/Organizations/OrganizationVersion/components/OrganizationVendorTermsVersionView/OrganizationVendorTermsVersionView.js new file mode 100644 index 00000000..87eb069c --- /dev/null +++ b/src/Organizations/OrganizationVersion/components/OrganizationVendorTermsVersionView/OrganizationVendorTermsVersionView.js @@ -0,0 +1,63 @@ +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; + +import { + Col, + NoValue, + Row, +} from '@folio/stripes/components'; +import { VersionKeyValue } from '@folio/stripes-acq-components'; + +export const OrganizationVendorTermsVersionView = ({ name, version }) => { + if (!version?.agreements?.length) { + return ( +

+ +

+ ); + } + + return ( + <> + {version?.agreements?.map((agreement, indx) => { + return ( + + + } + value={agreement?.name} + /> + + + } + value={agreement?.discount !== undefined ? `${agreement?.discount}%` : } + /> + + + } + value={agreement?.referenceUrl} + /> + + + } + value={agreement?.notes} + /> + + + ); + })} + + ); +}; + +OrganizationVendorTermsVersionView.propTypes = { + name: PropTypes.string.isRequired, + version: PropTypes.object, +}; diff --git a/src/Organizations/OrganizationVersion/components/OrganizationVendorTermsVersionView/index.js b/src/Organizations/OrganizationVersion/components/OrganizationVendorTermsVersionView/index.js new file mode 100644 index 00000000..057611e4 --- /dev/null +++ b/src/Organizations/OrganizationVersion/components/OrganizationVendorTermsVersionView/index.js @@ -0,0 +1 @@ +export { OrganizationVendorTermsVersionView } from './OrganizationVendorTermsVersionView'; diff --git a/src/Organizations/OrganizationVersion/components/index.js b/src/Organizations/OrganizationVersion/components/index.js new file mode 100644 index 00000000..de2d6fa9 --- /dev/null +++ b/src/Organizations/OrganizationVersion/components/index.js @@ -0,0 +1,7 @@ +export { OrganizationAccountsVersionView } from './OrganizationAccountsVersionView'; +export { OrganizationContactInfoVersionView } from './OrganizationContactInfoVersionView'; +export { OrganizationContactPeopleVersionView } from './OrganizationContactPeopleVersionView'; +export { OrganizationInterfacesVersionView } from './OrganizationInterfacesVersionView'; +export { OrganizationSummaryVersionView } from './OrganizationSummaryVersionView'; +export { OrganizationVendorInfoVersionView } from './OrganizationVendorInfoVersionView'; +export { OrganizationVendorTermsVersionView } from './OrganizationVendorTermsVersionView'; diff --git a/src/Organizations/OrganizationVersion/getOrganizationFieldsLabelMap.js b/src/Organizations/OrganizationVersion/getOrganizationFieldsLabelMap.js new file mode 100644 index 00000000..9da5b70f --- /dev/null +++ b/src/Organizations/OrganizationVersion/getOrganizationFieldsLabelMap.js @@ -0,0 +1,153 @@ +export const getOrganizationFieldsLabelMap = () => { + return { + 'tags': 'stripes-acq-components.label.tags', + 'tags.tagList': 'stripes-acq-components.label.tags', + 'tags.tagList[\\d]': 'stripes-acq-components.label.tags', + + 'metadata.createdByUserId': 'ui-organizations.metadata.createdByUserId', + 'metadata.createdDate': 'ui-organizations.metadata.createdDate', + 'metadata.updatedByUserId': 'ui-organizations.metadata.updatedByUserId', + 'metadata.updatedDate': 'ui-organizations.metadata.updatedDate', + + 'name': 'ui-organizations.summary.name', + 'code': 'ui-organizations.summary.code', + 'description': 'ui-organizations.summary.description', + 'exportToAccounting': 'ui-organizations.vendorInfo.exportToAccounting', + 'status': 'ui-organizations.summary.organizationStatus', + 'organizationTypes': 'ui-organizations.summary.type', + 'organizationTypes[\\d]': 'ui-organizations.summary.type', + 'language': 'ui-organizations.summary.defaultLanguage', + 'aliases': 'ui-organizations.summary.alternativeNames', + 'aliases[\\d]': 'ui-organizations.summary.alternativeNames', + 'aliases[\\d].value': 'ui-organizations.search.aliases', + 'aliases[\\d].description': 'ui-organizations.versionHistory.field.alias.description', + + 'addresses': 'ui-organizations.data.contactTypes.address', + 'addresses[\\d]': 'ui-organizations.data.contactTypes.address', + 'addresses[\\d].addressLine1': 'ui-organizations.contactPeople.addressLine1', + 'addresses[\\d].addressLine2': 'ui-organizations.contactPeople.addressLine2', + 'addresses[\\d].city': 'ui-organizations.contactPeople.city', + 'addresses[\\d].stateRegion': 'ui-organizations.contactPeople.stateRegion', + 'addresses[\\d].zipCode': 'ui-organizations.contactPeople.zipCode', + 'addresses[\\d].country': 'ui-organizations.contactPeople.country', + 'addresses[\\d].isPrimary': 'ui-organizations.versionHistory.field.address.isPrimary', + 'addresses[\\d].categories': 'ui-organizations.versionHistory.field.address.categories', + 'addresses[\\d].categories[\\d]': 'ui-organizations.versionHistory.field.address.categories', + 'addresses[\\d].language': 'ui-organizations.versionHistory.field.address.language', + 'phoneNumbers': 'ui-organizations.contactPeople.phoneNumbers', + 'phoneNumbers[\\d]': 'ui-organizations.contactPeople.phoneNumbers', + 'phoneNumbers[\\d].phoneNumber': 'ui-organizations.contactPeople.phoneNumber', + 'phoneNumbers[\\d].categories': 'ui-organizations.versionHistory.field.phoneNumbers.categories', + 'phoneNumbers[\\d].categories[\\d]': 'ui-organizations.versionHistory.field.phoneNumbers.categories', + 'phoneNumbers[\\d].type': 'ui-organizations.versionHistory.field.phoneNumbers.type', + 'phoneNumbers[\\d].isPrimary': 'ui-organizations.versionHistory.field.phoneNumbers.isPrimary', + 'phoneNumbers[\\d].language': 'ui-organizations.contactPeople.language', + 'emails': 'ui-organizations.contactPeople.emails', + 'emails[\\d]': 'ui-organizations.contactPeople.emails', + 'emails[\\d].value': 'ui-organizations.contactPeople.emailAddress', + 'emails[\\d].description': 'ui-organizations.versionHistory.field.emails.description', + 'emails[\\d].isPrimary': 'ui-organizations.versionHistory.field.emails.isPrimary', + 'emails[\\d].categories': 'ui-organizations.versionHistory.field.emails.categories', + 'emails[\\d].categories[\\d]': 'ui-organizations.versionHistory.field.emails.categories', + 'emails[\\d].language': 'ui-organizations.versionHistory.field.emails.language', + 'urls': 'ui-organizations.contactPeople.urls', + 'urls[\\d]': 'ui-organizations.contactPeople.urls', + 'urls[\\d].value': 'ui-organizations.contactPeople.url', + 'urls[\\d].description': 'ui-organizations.versionHistory.field.urls.description', + 'urls[\\d].language': 'ui-organizations.versionHistory.field.urls.language', + 'urls[\\d].isPrimary': 'ui-organizations.versionHistory.field.urls.isPrimary', + 'urls[\\d].categories': 'ui-organizations.versionHistory.field.urls.categories', + 'urls[\\d].categories[\\d]': 'ui-organizations.versionHistory.field.urls.categories', + + 'contacts': 'ui-organizations.contactPeople', + 'contacts[\\d]': 'ui-organizations.contactPeople', + 'privilegedContacts': 'ui-organizations.privilegedDonorInformation', + 'agreements': 'ui-organizations.linkedAgreements.section', + 'agreements[\\d]': 'ui-organizations.linkedAgreements.section', + 'agreements[\\d].name': 'ui-organizations.versionHistory.field.agreements.name', + 'agreements[\\d].discount': 'ui-organizations.agreement.discount', + 'agreements[\\d].referenceUrl': 'ui-organizations.agreement.referenceUrl', + 'agreements[\\d].notes': 'ui-organizations.versionHistory.field.agreements.notes', + 'erpCode': 'ui-organizations.summary.accountingCode', + 'paymentMethod': 'ui-organizations.accounts.paymentMethod', + 'accessProvider': 'ui-organizations.versionHistory.field.accessProvider', + 'governmental': 'ui-organizations.versionHistory.field.governmental', + 'licensor': 'ui-organizations.versionHistory.field.governmental', + 'materialSupplier': 'ui-organizations.versionHistory.field.materialSupplier', + 'vendorCurrencies': 'ui-organizations.vendorInfo.vendorCurrencies', + 'vendorCurrencies[\\d]': 'ui-organizations.vendorInfo.vendorCurrencies', + 'claimingInterval': 'ui-organizations.vendorInfo.claimingInterval', + 'discountPercent': 'ui-organizations.vendorInfo.discountPercent', + 'expectedActivationInterval': 'ui-organizations.vendorInfo.expectedActivationInterval', + 'expectedInvoiceInterval': 'ui-organizations.vendorInfo.expectedInvoiceInterval', + 'renewalActivationInterval': 'ui-organizations.vendorInfo.renewalActivationInterval', + 'subscriptionInterval': 'ui-organizations.vendorInfo.subscriptionInterval', + 'expectedReceiptInterval': 'ui-organizations.vendorInfo.expectedReceiptInterval', + 'taxId': 'ui-organizations.vendorInfo.taxID', + 'liableForVat': 'ui-organizations.vendorInfo.liableForVAT', + 'taxPercentage': 'ui-organizations.vendorInfo.taxPercentage', + 'interfaces': 'ui-organizations.interface', + 'interfaces[\\d]': 'ui-organizations.interface', + + 'edi': 'ui-organizations.integration.edi', + 'edi.vendorEdiCode': 'ui-organizations.integration.edi.vendorEDICode', + 'edi.vendorEdiType': 'ui-organizations.integration.edi.vendorEDIType', + 'edi.libEdiCode': 'ui-organizations.integration.edi.libraryEDICode', + 'edi.libEdiType': 'ui-organizations.integration.edi.libraryEDIType', + 'edi.prorateTax': 'ui-organizations.versionHistory.field.edi.prorateTax', + 'edi.prorateFees': 'ui-organizations.versionHistory.field.edi.prorateFees', + 'edi.ediNamingConvention': 'ui-organizations.integration.edi.ediNamingConvention', + 'edi.sendAcctNum': 'integration.edi.sendAccountNumber', + 'edi.supportOrder': 'ui-organizations.integration.edi.orders', + 'edi.supportInvoice': 'ui-organizations.integration.edi.invoices', + 'edi.notes': 'ui-organizations.versionHistory.field.edi.notes', + 'edi.ediFtp': 'ui-organizations.integration.ftp', + 'edi.ediFtp.ftpFormat': 'ui-organizations.integration.ftp.ftpFormat', + 'edi.ediFtp.serverAddress': 'ui-organizations.integration.ftp.serverAddress', + 'edi.ediFtp.username': 'ui-organizations.integration.ftp.username', + 'edi.ediFtp.password': 'ui-organizations.edit.password', // NOSONAR - it's a label mapping, not the actual password + 'edi.ediFtp.ftpMode': 'ui-organizations.integration.ftp.ftpMode', + 'edi.ediFtp.ftpConnMode': 'ui-organizations.integration.ftp.ftpConnectionMode', + 'edi.ediFtp.ftpPort': 'ui-organizations.integration.ftp.ftpPort', + 'edi.ediFtp.orderDirectory': 'ui-organizations.integration.ftp.orderDirectory', + 'edi.ediFtp.invoiceDirectory': 'ui-organizations.integration.ftp.invoiceDirectory', + 'edi.ediFtp.notes': 'ui-organizations.integration.ftp.notes', + 'edi.ediJob': 'ui-organizations.integration.scheduling', + 'edi.ediJob.scheduleEdi': 'ui-organizations.integration.scheduling.scheduleEDI', + 'edi.ediJob.schedulingDate': 'ui-organizations.integration.scheduling.scheduleDate', + 'edi.ediJob.time': 'ui-organizations.integration.scheduling.scheduleTime', + 'edi.ediJob.isMonday': 'ui-organizations.integration.scheduling.scheduleWeekdays.MONDAY', + 'edi.ediJob.isTuesday': 'ui-organizations.integration.scheduling.scheduleWeekdays.TUESDAY', + 'edi.ediJob.isWednesday': 'ui-organizations.integration.scheduling.scheduleWeekdays.WEDNESDAY', + 'edi.ediJob.isThursday': 'ui-organizations.integration.scheduling.scheduleWeekdays.THURSDAY', + 'edi.ediJob.isFriday': 'ui-organizations.integration.scheduling.scheduleWeekdays.FRIDAY', + 'edi.ediJob.isSaturday': 'ui-organizations.integration.scheduling.scheduleWeekdays.SATURDAY', + 'edi.ediJob.isSunday': 'ui-organizations.integration.scheduling.scheduleWeekdays.SUNDAY', + 'edi.ediJob.sendToEmails': 'ui-organizations.versionHistory.field.edi.ediJob.sendToEmails', + 'edi.ediJob.notifyAllEdi': 'ui-organizations.versionHistory.field.edi.ediJob.notifyAllEdi', + 'edi.ediJob.notifyInvoiceOnly': 'ui-organizations.versionHistory.field.edi.ediJob.notifyInvoiceOnly', + 'edi.ediJob.notifyErrorOnly': 'ui-organizations.versionHistory.field.edi.ediJob.notifyErrorOnly', + 'edi.ediJob.schedulingNotes': 'ui-organizations.versionHistory.field.edi.ediJob.schedulingNotes', + + 'accounts': 'ui-organizations.accounts', + 'accounts[\\d]': 'ui-organizations.accounts', + 'accounts[\\d].name': 'ui-organizations.accounts.name', + 'accounts[\\d].accountNo': 'ui-organizations.accounts.accountNumber', + 'accounts[\\d].description': 'ui-organizations.accounts.description', + 'accounts[\\d].appSystemNo': 'ui-organizations.accounts.payable', + 'accounts[\\d].paymentMethod': 'ui-organizations.accounts.paymentMethod', + 'accounts[\\d].accountStatus': 'ui-organizations.accounts.account.accountStatus', + 'accounts[\\d].contactInfo': 'ui-organizations.accounts.account.contactInfo', + 'accounts[\\d].libraryCode': 'ui-organizations.accounts.libraryCode', + 'accounts[\\d].libraryEdiCode': 'ui-organizations.accounts.libraryEDICode', + 'accounts[\\d].notes': 'ui-organizations.accounts.notes', + 'accounts[\\d].acqUnitIds': 'ui-organizations.versionHistory.field.accounts.acqUnitsIds', + 'accounts[\\d].acqUnitIds[\\d]': 'ui-organizations.versionHistory.field.accounts.acqUnitsIds', + + 'isVendor': 'ui-organizations.summary.isVendor', + 'isDonor': 'ui-organizations.summary.isDonor', + 'sanCode': 'ui-organizations.versionHistory.field.sanCode', + 'acqUnitIds': 'stripes-acq-components.label.acqUnits', + 'acqUnitIds[\\d]': 'stripes-acq-components.label.acqUnits', + }; +}; diff --git a/src/Organizations/OrganizationVersion/hooks/index.js b/src/Organizations/OrganizationVersion/hooks/index.js new file mode 100644 index 00000000..98d80689 --- /dev/null +++ b/src/Organizations/OrganizationVersion/hooks/index.js @@ -0,0 +1,2 @@ +export { useOrganizationVersions } from './useOrganizationVersions'; +export { useSelectedOrganizationVersion } from './useSelectedOrganizationVersion'; diff --git a/src/Organizations/OrganizationVersion/hooks/useOrganizationVersions/index.js b/src/Organizations/OrganizationVersion/hooks/useOrganizationVersions/index.js new file mode 100644 index 00000000..ecff7cfd --- /dev/null +++ b/src/Organizations/OrganizationVersion/hooks/useOrganizationVersions/index.js @@ -0,0 +1 @@ +export { useOrganizationVersions } from './useOrganizationVersions'; diff --git a/src/Organizations/OrganizationVersion/hooks/useOrganizationVersions/useOrganizationVersions.js b/src/Organizations/OrganizationVersion/hooks/useOrganizationVersions/useOrganizationVersions.js new file mode 100644 index 00000000..ba5d1ccc --- /dev/null +++ b/src/Organizations/OrganizationVersion/hooks/useOrganizationVersions/useOrganizationVersions.js @@ -0,0 +1,34 @@ +import { useQuery } from 'react-query'; + +import { + useNamespace, + useOkapiKy, +} from '@folio/stripes/core'; + +import { AUDIT_ACQ_EVENTS_API } from '@folio/stripes-acq-components'; + +const DEFAULT_DATA = []; + +export const useOrganizationVersions = (organizationId, options = {}) => { + const ky = useOkapiKy(); + const [namespace] = useNamespace({ key: 'organization-versions' }); + + const searchParams = { + sortBy: 'event_date', + sortOrder: 'desc', + }; + + const { isLoading, data } = useQuery( + [namespace, organizationId], + ({ signal }) => ky.get(`${AUDIT_ACQ_EVENTS_API}/organization/${organizationId}`, { signal, searchParams }).json(), + { + enabled: Boolean(organizationId), + ...options, + }, + ); + + return { + isLoading, + versions: data?.organizationAuditEvents || DEFAULT_DATA, + }; +}; diff --git a/src/Organizations/OrganizationVersion/hooks/useOrganizationVersions/useOrganizationVersions.test.js b/src/Organizations/OrganizationVersion/hooks/useOrganizationVersions/useOrganizationVersions.test.js new file mode 100644 index 00000000..06743fe7 --- /dev/null +++ b/src/Organizations/OrganizationVersion/hooks/useOrganizationVersions/useOrganizationVersions.test.js @@ -0,0 +1,53 @@ +import { + QueryClient, + QueryClientProvider, +} from 'react-query'; + +import { + renderHook, + waitFor, +} from '@folio/jest-config-stripes/testing-library/react'; +import { useOkapiKy } from '@folio/stripes/core'; + +import { useOrganizationVersions } from './useOrganizationVersions'; + +const queryClient = new QueryClient(); +const wrapper = ({ children }) => ( + + {children} + +); + +describe('useOrganizationVersions', () => { + const mockGet = jest.fn(); + + beforeEach(() => { + useOkapiKy.mockReturnValue({ get: mockGet }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return default data when organizationId is not provided', async () => { + const { result } = renderHook(() => useOrganizationVersions(), { wrapper }); + + await waitFor(() => expect(result.current.isLoading).toBeFalsy()); + + expect(result.current.versions).toEqual([]); + }); + + it('should fetch and return organization versions', async () => { + const mockData = { organizationAuditEvents: [{ id: '1', name: 'Version 1' }] }; + + mockGet.mockReturnValueOnce({ + json: () => Promise.resolve(mockData), + }); + + const { result } = renderHook(() => useOrganizationVersions('org-1'), { wrapper }); + + await waitFor(() => expect(result.current.isLoading).toBeFalsy()); + + expect(result.current.versions).toEqual(mockData.organizationAuditEvents); + }); +}); diff --git a/src/Organizations/OrganizationVersion/hooks/useSelectedOrganizationVersion/index.js b/src/Organizations/OrganizationVersion/hooks/useSelectedOrganizationVersion/index.js new file mode 100644 index 00000000..ebb54868 --- /dev/null +++ b/src/Organizations/OrganizationVersion/hooks/useSelectedOrganizationVersion/index.js @@ -0,0 +1 @@ +export { useSelectedOrganizationVersion } from './useSelectedOrganizationVersion'; diff --git a/src/Organizations/OrganizationVersion/hooks/useSelectedOrganizationVersion/useSelectedOrganizationVersion.js b/src/Organizations/OrganizationVersion/hooks/useSelectedOrganizationVersion/useSelectedOrganizationVersion.js new file mode 100644 index 00000000..be89fbf3 --- /dev/null +++ b/src/Organizations/OrganizationVersion/hooks/useSelectedOrganizationVersion/useSelectedOrganizationVersion.js @@ -0,0 +1,143 @@ +import filter from 'lodash/fp/filter'; +import flatMap from 'lodash/fp/flatMap'; +import flow from 'lodash/fp/flow'; +import get from 'lodash/fp/get'; +import keyBy from 'lodash/fp/keyBy'; +import uniq from 'lodash/fp/uniq'; +import { useMemo } from 'react'; +import { useIntl } from 'react-intl'; +import { useQuery } from 'react-query'; + +import { + useNamespace, + useOkapiKy, +} from '@folio/stripes/core'; +import { getFullName } from '@folio/stripes/util'; +import { + fetchAcqUnitsByIds, + getVersionMetadata, + useOrganization, + useUsersBatch, +} from '@folio/stripes-acq-components'; +import { currenciesByCode } from '@folio/stripes/components'; + +import { + useContactsByIds, + useInterfacesByIds, +} from '../../../../common/hooks'; + +const getUniqItems = (arr) => ( + flow( + uniq, + filter(Boolean), + )(arr) +); + +export const useSelectedOrganizationVersion = ({ versionId, versions, snapshotPath }, options = {}) => { + const intl = useIntl(); + const ky = useOkapiKy(); + const [namespace] = useNamespace({ key: 'organization-version-data' }); + + const deletedRecordLabel = intl.formatMessage({ id: 'stripes-acq-components.versionHistory.deletedRecord' }); + + const currentVersion = useMemo(() => ( + versions?.find(({ id }) => id === versionId) + ), [versionId, versions]); + + const versionSnapshot = useMemo(() => ( + get(snapshotPath, currentVersion) + ), [snapshotPath, currentVersion]); + + const { + organization, + isLoading: isOrganizationLoading, + } = useOrganization(currentVersion?.organizationId); + + const metadata = useMemo(() => getVersionMetadata(currentVersion, organization), [currentVersion, organization]); + const createdByUserId = metadata?.createdByUserId; + + const versionUserIds = useMemo(() => getUniqItems([createdByUserId]), [createdByUserId]); + const { + users, + isLoading: isUsersLoading, + } = useUsersBatch(versionUserIds); + + const { + contacts, + isLoading: isContactsLoading, + } = useContactsByIds(versionSnapshot?.contacts); + + const { + interfaces, + isLoading: isInterfacesLoading, + } = useInterfacesByIds(versionSnapshot?.interfaces); + + const { + isLoading: isVersionDataLoading, + data = {}, + } = useQuery({ + queryKey: [namespace, versionId, versionSnapshot?.id], + queryFn: async () => { + const acqUnitsIds = [ + ...get('acqUnitIds', versionSnapshot, []), + ...flow( + get('accounts'), + flatMap('acqUnitIds'), + uniq, + )(versionSnapshot), + ]; + + const [acqUnitsMap] = await Promise.all([ + fetchAcqUnitsByIds(ky)(acqUnitsIds).then(keyBy('id')), + ]); + + const vendorCurrenciesValue = versionSnapshot?.vendorCurrencies?.map(currency => { + const currencyInfo = currenciesByCode[currency]; + + return currencyInfo ? `${currencyInfo.currency} (${currencyInfo.code})` : currency; + }).join(', '); + + return { + ...versionSnapshot, + accounts: versionSnapshot?.accounts?.map((account) => ({ + ...account, + acqUnits: account?.acqUnitIds?.map((acqUnitId) => acqUnitsMap[acqUnitId]?.name || deletedRecordLabel), + })), + acqUnits: acqUnitsIds.map(acqUnitsId => acqUnitsMap[acqUnitsId]?.name || deletedRecordLabel).join(', '), + alternativeNames: versionSnapshot?.aliases?.map(({ value }) => value).join(', '), + vendorCurrenciesValue, + metadata, + }; + }, + enabled: Boolean(versionId && organization?.id), + ...options, + }); + + const selectedVersion = useMemo(() => { + const versionUsersMap = keyBy('id', users); + + const createdByUser = versionUsersMap[createdByUserId] + ? getFullName(versionUsersMap[createdByUserId]) + : deletedRecordLabel; + + return { + ...data, + createdByUser: createdByUserId && createdByUser, + contactsList: contacts, + interfacesList: interfaces, + }; + }, [users, createdByUserId, deletedRecordLabel, data, contacts, interfaces]); + + const isLoading = ( + isOrganizationLoading + || isUsersLoading + || isVersionDataLoading + || isContactsLoading + || isInterfacesLoading + ); + + return { + isLoading, + selectedVersion, + }; +}; diff --git a/src/Organizations/OrganizationVersion/hooks/useSelectedOrganizationVersion/useSelectedOrganizationVersion.test.js b/src/Organizations/OrganizationVersion/hooks/useSelectedOrganizationVersion/useSelectedOrganizationVersion.test.js new file mode 100644 index 00000000..752aff62 --- /dev/null +++ b/src/Organizations/OrganizationVersion/hooks/useSelectedOrganizationVersion/useSelectedOrganizationVersion.test.js @@ -0,0 +1,89 @@ +import get from 'lodash/get'; +import { + QueryClient, + QueryClientProvider, +} from 'react-query'; + +import { + renderHook, + waitFor, +} from '@folio/jest-config-stripes/testing-library/react'; +import { + fetchAcqUnitsByIds, + getVersionMetadata, + useOrganization, + useUsersBatch, +} from '@folio/stripes-acq-components'; + +import { organizationAuditEvent } from 'fixtures'; +import { + useContactsByIds, + useInterfacesByIds, +} from '../../../../common/hooks'; +import { useSelectedOrganizationVersion } from './useSelectedOrganizationVersion'; + +jest.mock('@folio/stripes-acq-components', () => ({ + ...jest.requireActual('@folio/stripes-acq-components'), + fetchAcqUnitsByIds: jest.fn(), + getVersionMetadata: jest.fn(), + useOrganization: jest.fn(), + useUsersBatch: jest.fn(), +})); +jest.mock('../../../../common/hooks', () => ({ + ...jest.requireActual('../../../../common/hooks'), + useContactsByIds: jest.fn(), + useInterfacesByIds: jest.fn(), +})); + +const versionId = organizationAuditEvent.id; +const versions = [organizationAuditEvent]; +const snapshotPath = 'organizationSnapshot.map'; + +const contacts = [{ id: 'contact1' }]; +const interfaces = [{ id: 'interface1' }]; +const organization = { id: 'org1' }; +const users = [{ id: 'user1', personal: { firstName: 'John', lastName: 'Doe' } }]; + +const queryClient = new QueryClient(); +const wrapper = ({ children }) => ( + + {children} + +); + +describe('useSelectedOrganizationVersion', () => { + beforeEach(() => { + fetchAcqUnitsByIds.mockReturnValue(() => Promise.resolve([{ id: 'acq-unit-id' }])); + useOrganization.mockReturnValue({ organization, isLoading: false }); + useUsersBatch.mockReturnValue({ users, isLoading: false }); + useContactsByIds.mockReturnValue({ contacts, isLoading: false }); + useInterfacesByIds.mockReturnValue({ interfaces, isLoading: false }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return selected version data', async () => { + const { result } = renderHook( + () => useSelectedOrganizationVersion({ versionId, versions, snapshotPath }), + { wrapper }, + ); + + await waitFor(() => expect(result.current.isLoading).toBeFalsy()); + + expect(result.current.selectedVersion).toEqual({ + ...get(organizationAuditEvent, snapshotPath), + accounts: get(organizationAuditEvent, `${snapshotPath}.accounts`).map(acc => ({ + ...acc, + acqUnits: [], + })), + acqUnits: '', + alternativeNames: 'Amazon', + contactsList: contacts, + interfacesList: interfaces, + vendorCurrenciesValue: 'US Dollar (USD)', + metadata: getVersionMetadata(organizationAuditEvent, organization), + }); + }); +}); diff --git a/src/Organizations/OrganizationVersion/index.js b/src/Organizations/OrganizationVersion/index.js new file mode 100644 index 00000000..8a312a96 --- /dev/null +++ b/src/Organizations/OrganizationVersion/index.js @@ -0,0 +1 @@ +export { default as OrganizationVersion } from './OrganizationVersion'; diff --git a/src/Organizations/OrganizationsList/OrganizationsList.js b/src/Organizations/OrganizationsList/OrganizationsList.js index d054d860..ce3135fa 100644 --- a/src/Organizations/OrganizationsList/OrganizationsList.js +++ b/src/Organizations/OrganizationsList/OrganizationsList.js @@ -44,10 +44,12 @@ import { } from '@folio/plugin-find-organization'; import { + ORGANIZATION_VERSIONS_VIEW_ROUTE, ORGANIZATIONS_ROUTE, VIEW_ORG_DETAILS, } from '../../common/constants'; import { OrganizationDetailsContainer } from '../OrganizationDetails'; +import { OrganizationVersion } from '../OrganizationVersion'; import OrganizationsListLastMenu from './OrganizationsListLastMenu'; const resultsPaneTitle = ; @@ -251,9 +253,16 @@ const OrganizationsList = ({ + + ); diff --git a/src/Organizations/constants.js b/src/Organizations/constants.js index 9f59e372..360b0828 100644 --- a/src/Organizations/constants.js +++ b/src/Organizations/constants.js @@ -46,3 +46,5 @@ export const MAP_FIELD_ACCORDION = { }; export const BANKING_INFORMATION_FIELD_NAME = 'bankingInformation'; + +export const HIDDEN_FIELDS_FOR_ORGANIZATION_VERSION_HISTORY = []; diff --git a/src/common/constants/routes.js b/src/common/constants/routes.js index 455f24f9..c7e40458 100644 --- a/src/common/constants/routes.js +++ b/src/common/constants/routes.js @@ -1,3 +1,4 @@ export const NOTES_ROUTE = '/organizations/notes'; export const ORGANIZATIONS_ROUTE = '/organizations'; export const VIEW_ORG_DETAILS = '/organizations/view/'; +export const ORGANIZATION_VERSIONS_VIEW_ROUTE = `${VIEW_ORG_DETAILS}:id/versions`; diff --git a/src/common/constants/vendorCategories.js b/src/common/constants/vendorCategories.js index 33812d25..c9d26ab0 100644 --- a/src/common/constants/vendorCategories.js +++ b/src/common/constants/vendorCategories.js @@ -1,3 +1,4 @@ +export const UNCATEGORIZED_ID = 'uncategorized'; export const UNCATEGORIZED_VALUE = 'Uncategorized'; export const VENDOR_DEFAULT_CATEGORIES = { diff --git a/src/common/hooks/index.js b/src/common/hooks/index.js index c22e5b3b..14bfc2c0 100644 --- a/src/common/hooks/index.js +++ b/src/common/hooks/index.js @@ -3,11 +3,14 @@ export * from './useBankingAccountTypes'; export * from './useBankingInformationMutation'; export * from './useBankingInformationSettings'; export * from './useCategories'; +export * from './useContactsByIds'; export * from './useEventEmitter'; export * from './useIntegrationConfig'; export * from './useIntegrationConfigMutation'; +export * from './useInterfacesByIds'; export * from './useLinkedAgreements'; export * from './useOrganizationBankingInformation'; export * from './useTranslatedCategories'; export * from './useTypes'; export * from './useVendorCodeGeneratorSettings'; +export * from './useVersionWrappedRowFormatter'; diff --git a/src/common/hooks/useCategories/useCategories.js b/src/common/hooks/useCategories/useCategories.js index 8cd3878b..a4349b2f 100644 --- a/src/common/hooks/useCategories/useCategories.js +++ b/src/common/hooks/useCategories/useCategories.js @@ -4,7 +4,10 @@ import { useNamespace, useOkapiKy, } from '@folio/stripes/core'; -import { LIMIT_MAX } from '@folio/stripes-acq-components'; +import { + ALL_RECORDS_CQL, + LIMIT_MAX, +} from '@folio/stripes-acq-components'; import { CATEGORIES_API } from '../../constants'; import { useTranslatedCategories } from '../useTranslatedCategories'; @@ -17,7 +20,7 @@ export const useCategories = (options = {}) => { const searchParams = { limit: LIMIT_MAX, - query: 'cql.allRecords=1', + query: ALL_RECORDS_CQL, }; const { @@ -26,7 +29,7 @@ export const useCategories = (options = {}) => { isLoading, } = useQuery( [namespace], - () => ky.get(CATEGORIES_API, { searchParams }).json(), + ({ signal }) => ky.get(CATEGORIES_API, { searchParams, signal }).json(), options, ); diff --git a/src/common/hooks/useContactsByIds/index.js b/src/common/hooks/useContactsByIds/index.js new file mode 100644 index 00000000..b8a06c06 --- /dev/null +++ b/src/common/hooks/useContactsByIds/index.js @@ -0,0 +1 @@ +export { useContactsByIds } from './useContactsByIds'; diff --git a/src/common/hooks/useContactsByIds/useContactsByIds.js b/src/common/hooks/useContactsByIds/useContactsByIds.js new file mode 100644 index 00000000..74639eed --- /dev/null +++ b/src/common/hooks/useContactsByIds/useContactsByIds.js @@ -0,0 +1,46 @@ +import { useQuery } from 'react-query'; + +import { + useNamespace, + useOkapiKy, +} from '@folio/stripes/core'; +import { batchFetch } from '@folio/stripes-acq-components'; + +import { CONTACTS_API } from '../../constants'; + +const DEFAULT_DATA = []; + +export const useContactsByIds = (contactIds, options = {}) => { + const { + enabled = true, + ...queryOptions + } = options; + + const ky = useOkapiKy(); + const [namespace] = useNamespace({ key: 'contacts' }); + + const { + data, + isFetching, + isLoading, + } = useQuery({ + queryKey: [namespace, contactIds], + queryFn: ({ signal }) => batchFetch( + { + GET: ({ params: searchParams }) => { + return ky.get(CONTACTS_API, { searchParams, signal }).json().then(({ contacts }) => contacts); + }, + }, + contactIds, + ), + enabled: Boolean(enabled && contactIds?.length), + ...queryOptions, + }); + + return ({ + contacts: data || DEFAULT_DATA, + totalRecords: data?.length, + isFetching, + isLoading, + }); +}; diff --git a/src/common/hooks/useInterfacesByIds/index.js b/src/common/hooks/useInterfacesByIds/index.js new file mode 100644 index 00000000..e389bcf9 --- /dev/null +++ b/src/common/hooks/useInterfacesByIds/index.js @@ -0,0 +1 @@ +export { useInterfacesByIds } from './useInterfacesByIds'; diff --git a/src/common/hooks/useInterfacesByIds/useInterfacesByIds.js b/src/common/hooks/useInterfacesByIds/useInterfacesByIds.js new file mode 100644 index 00000000..a11d5452 --- /dev/null +++ b/src/common/hooks/useInterfacesByIds/useInterfacesByIds.js @@ -0,0 +1,46 @@ +import { useQuery } from 'react-query'; + +import { + useNamespace, + useOkapiKy, +} from '@folio/stripes/core'; +import { batchFetch } from '@folio/stripes-acq-components'; + +import { INTERFACES_API } from '../../constants'; + +const DEFAULT_DATA = []; + +export const useInterfacesByIds = (interfaceIds, options = {}) => { + const { + enabled = true, + ...queryOptions + } = options; + + const ky = useOkapiKy(); + const [namespace] = useNamespace({ key: 'interfaces' }); + + const { + data, + isFetching, + isLoading, + } = useQuery({ + queryKey: [namespace, interfaceIds], + queryFn: ({ signal }) => batchFetch( + { + GET: ({ params: searchParams }) => { + return ky.get(INTERFACES_API, { searchParams, signal }).json().then(({ interfaces }) => interfaces); + }, + }, + interfaceIds, + ), + enabled: Boolean(enabled && interfaceIds?.length), + ...queryOptions, + }); + + return { + interfaces: data || DEFAULT_DATA, + totalRecords: data?.length, + isFetching, + isLoading, + }; +}; diff --git a/src/common/hooks/useVersionWrappedRowFormatter/index.js b/src/common/hooks/useVersionWrappedRowFormatter/index.js new file mode 100644 index 00000000..dcc8b135 --- /dev/null +++ b/src/common/hooks/useVersionWrappedRowFormatter/index.js @@ -0,0 +1 @@ +export { useVersionWrappedRowFormatter } from './useVersionWrappedRowFormatter'; diff --git a/src/common/hooks/useVersionWrappedRowFormatter/styles.css b/src/common/hooks/useVersionWrappedRowFormatter/styles.css new file mode 100644 index 00000000..69d7cc00 --- /dev/null +++ b/src/common/hooks/useVersionWrappedRowFormatter/styles.css @@ -0,0 +1,6 @@ +.mark { + &.version-wrapped.version-row-formatted { + background-color: mark; + margin: 0.15rem 0; + } +} diff --git a/src/common/hooks/useVersionWrappedRowFormatter/useVersionWrappedRowFormatter.js b/src/common/hooks/useVersionWrappedRowFormatter/useVersionWrappedRowFormatter.js new file mode 100644 index 00000000..d997e92a --- /dev/null +++ b/src/common/hooks/useVersionWrappedRowFormatter/useVersionWrappedRowFormatter.js @@ -0,0 +1,55 @@ +import { + useCallback, + useContext, +} from 'react'; + +import { DefaultMCLRowFormatter } from '@folio/stripes/components'; +import { VersionViewContext } from '@folio/stripes-acq-components'; + +import css from './styles.css'; + +const getVersionWrappedRowFormatter = ({ + baseRowFormatter = DefaultMCLRowFormatter, + row, + name, + paths, +}) => { + const { + rowClass, + rowIndex, + ...props + } = row; + + const isUpdated = paths?.includes(`${name}[${rowIndex}]`); + + return baseRowFormatter({ + ...props, + rowClass: [ + css['version-wrapped'], + css['version-row-formatted'], + rowClass, + isUpdated ? css.mark : '', + ].join(' '), + rowIndex, + }); +}; + +export const useVersionWrappedRowFormatter = ({ + baseRowFormatter, + name, +}) => { + const versionContext = useContext(VersionViewContext); + + const rowFormatter = useCallback((row) => { + if (!versionContext || !name) return baseRowFormatter; + + return getVersionWrappedRowFormatter({ + baseRowFormatter, + row, + name, + paths: versionContext.paths, + }); + }, [baseRowFormatter, name, versionContext]); + + return rowFormatter; +}; diff --git a/src/common/hooks/useVersionWrappedRowFormatter/useVersionWrappedRowFormatter.test.js b/src/common/hooks/useVersionWrappedRowFormatter/useVersionWrappedRowFormatter.test.js new file mode 100644 index 00000000..f8a9b43b --- /dev/null +++ b/src/common/hooks/useVersionWrappedRowFormatter/useVersionWrappedRowFormatter.test.js @@ -0,0 +1,41 @@ +import { renderHook } from '@folio/jest-config-stripes/testing-library/react'; + +import { VersionViewContext } from '@folio/stripes-acq-components'; + +import { useVersionWrappedRowFormatter } from './useVersionWrappedRowFormatter'; + +const versionViewContext = { paths: ['testName[0]'] }; + +const getWrapper = (contextValue = {}) => ({ children }) => ( + + {children} + +); + +const mockBaseRowFormatter = ({ rowClass }) => ({ rowClass }); + +describe('useVersionWrappedRowFormatter', () => { + it('should return version wrapped row formatter if versionContext and name are provided', () => { + const { result } = renderHook(() => useVersionWrappedRowFormatter({ + baseRowFormatter: mockBaseRowFormatter, + name: 'testName', + }), { wrapper: getWrapper() }); + + const rowFormatter = result.current; + const row = { rowClass: 'testClass', rowIndex: 0 }; + + expect(rowFormatter(row).rowClass.includes('mark')).toBeTruthy(); + }); + + it('should not add mark class if row is not updated', () => { + const { result } = renderHook(() => useVersionWrappedRowFormatter({ + baseRowFormatter: mockBaseRowFormatter, + name: 'testName', + }), { wrapper: getWrapper({ paths: ['otherName[0]'] }) }); + + const rowFormatter = result.current; + const row = { rowClass: 'testClass', rowIndex: 0 }; + + expect(rowFormatter(row).rowClass.includes('mark')).toBeFalsy(); + }); +}); diff --git a/test/jest/fixtures/index.js b/test/jest/fixtures/index.js index 7a39fa0b..5a07a642 100644 --- a/test/jest/fixtures/index.js +++ b/test/jest/fixtures/index.js @@ -2,4 +2,5 @@ export * from './contact'; export * from './integrationConfig'; export * from './interface'; export * from './organization'; +export * from './organizationAuditEvent'; export * from './organizationTypes'; diff --git a/test/jest/fixtures/organizationAuditEvent.js b/test/jest/fixtures/organizationAuditEvent.js new file mode 100644 index 00000000..b2ab1a53 --- /dev/null +++ b/test/jest/fixtures/organizationAuditEvent.js @@ -0,0 +1,14 @@ +import { organization } from './organization'; + +export const organizationAuditEvent = { + id: '3635ff84-ede6-4786-95e7-00a4801115ba', + action: 'Edit', + organizationId: '6e5bb3d1-cba7-47a8-87c2-eb9f3b1598fc', + userId: 'dd88964f-22f2-5579-898f-86920b2c1d71', + eventDate: '2024-11-14T08:48:48.340+00:00', + actionDate: '2024-11-14T08:48:48.335+00:00', + organizationSnapshot: { + map: { ...organization }, + empty: false, + }, +}; diff --git a/translations/ui-organizations/en.json b/translations/ui-organizations/en.json index d8422eea..344ccf13 100644 --- a/translations/ui-organizations/en.json +++ b/translations/ui-organizations/en.json @@ -439,6 +439,45 @@ "save.error.creds": "Error while saving credentials", "save.error.assignInterface": "Error while assigning interface to the organization", + "versionHistory.field.metadata.createdByUserId": "Created by", + "versionHistory.field.metadata.createdDate": "Created date", + "versionHistory.field.metadata.updatedByUserId": "Updated by", + "versionHistory.field.metadata.updatedDate": "Updated date", + "versionHistory.field.alias.description": "Alias description", + "versionHistory.field.sanCode": "SAN Code", + "versionHistory.field.accounts.acqUnitsIds": "Account acquisition units", + "versionHistory.field.accessProvider": "Access provider", + "versionHistory.field.governmental": "Governmental", + "versionHistory.field.licensor": "Licensor", + "versionHistory.field.edi.notes": "Notes (EDI)", + "versionHistory.field.edi.prorateTax": "Prorate tax", + "versionHistory.field.edi.prorateFees": "Prorate fees", + "versionHistory.field.edi.ediJob.sendToEmails": "Send to emails", + "versionHistory.field.edi.ediJob.notifyAllEdi": "Notify all", + "versionHistory.field.edi.ediJob.notifyInvoiceOnly": "Notify invoice only", + "versionHistory.field.edi.ediJob.notifyErrorOnly": "Notify error only", + "versionHistory.field.edi.ediJob.schedulingNotes": "Scheduling notes", + "versionHistory.field.address.isPrimary": "Primary (Address)", + "versionHistory.field.address.categories": "Categories (Address)", + "versionHistory.field.address.language": "Language (Address)", + "versionHistory.field.phoneNumbers.isPrimary": "Primary (Phone number)", + "versionHistory.field.phoneNumbers.categories": "Categories (Phone number)", + "versionHistory.field.phoneNumbers.language": "Language (Phone number)", + "versionHistory.field.phoneNumbers.type": "Type (Phone number)", + "versionHistory.field.emails.description": "Description (Email)", + "versionHistory.field.emails.isPrimary": "Primary (Email)", + "versionHistory.field.emails.categories": "Categories (Email)", + "versionHistory.field.emails.language": "Language (Email)", + "versionHistory.field.urls.description": "Description (URL)", + "versionHistory.field.urls.language": "Language (URL)", + "versionHistory.field.urls.isPrimary": "Primary (URL)", + "versionHistory.field.urls.categories": "Categories (URL)", + "versionHistory.field.agreements.name": "Name (Agreement)", + "versionHistory.field.agreements.notes": "Notes (Agreement)", + "versionHistory.field.accounts.name": "Name (Account)", + "versionHistory.field.accounts.description": "Description (Account)", + "versionHistory.field.accounts.notes": "Notes (Account)", + "settings.categories": "Categories", "settings.categories.cannotDeleteTermHeader": "Cannot delete category", "settings.categories.cannotDeleteTermMessage": "This category cannot be deleted, as it is in use by one or more records.",