Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* Add Order email templates settings with token picker and Handlebars backend-rendered preview. Refs UIOR-1492, UIOR-1493, UIOR-1495.
* Do not show metadata fields in "Version history" for PO and PO Lines. Refs UIOR-1585.
* Preserve the "Create inventory" and "Receiving workflow" fields values according to the form value precedence rules. Refs UIOR-1582.
* Show which export integration applies to a PO line. Refs UIOR-1556.

## [9.0.5](https://github.com/folio-org/ui-orders/tree/v9.0.5) (2026-07-20)
[Full Changelog](https://github.com/folio-org/ui-orders/compare/v9.0.4...v9.0.5)
Expand Down
20 changes: 20 additions & 0 deletions src/common/POLFields/DetailsFields/AutomaticExportInfo.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* Two lines, then an ellipsis. `anywhere` lets a long name without spaces wrap
instead of being clipped mid-word. */
.exportVia {
display: -webkit-box;
overflow: hidden;
margin-bottom: 1rem;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
overflow-wrap: anywhere;
}

.warningButton {
color: var(--warn);
}

.list {
margin: 0 0 0.75rem;
padding-inline-start: 1.1rem;
}
105 changes: 105 additions & 0 deletions src/common/POLFields/DetailsFields/AutomaticExportInfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import PropTypes from 'prop-types';
import { FormattedMessage, useIntl } from 'react-intl';

import {
IconButton,
InfoPopover,
} from '@folio/stripes/components';

import {
formatConfigLabel,
getApplicableOrderingIntegrations,
} from '../../../components/Utils/toggleAutomaticExport';
import css from './AutomaticExportInfo.css';

// Hint for the "Automatic export" checkbox. placement="label" renders an icon
// next to the label (warning: no match, info popover: several matches),
// placement="below" renders the single matching integration as a text line.
// Without integrationConfigs (order templates) there is nothing to match.
const AutomaticExportInfo = ({
acquisitionMethod,
automaticExport = false,
integrationConfigs,
isLoading = false,
manualOrder = false,
placement = 'below',
vendorAccount,
}) => {
const intl = useIntl();

if (manualOrder || !automaticExport || isLoading || !integrationConfigs) return null;

const applicableIntegrations = getApplicableOrderingIntegrations({
vendorAccount,
acquisitionMethod,
integrationConfigs,
});

if (placement === 'below') {
if (applicableIntegrations.length !== 1) return null;

const label = formatConfigLabel(applicableIntegrations[0]);

return (
<div className={css.exportVia} title={label}>
<FormattedMessage
id="ui-orders.poLine.automaticExport.exportVia"
values={{ name: label }}
/>
</div>
);
}

if (applicableIntegrations.length === 0) {
return (
<InfoPopover
content={<FormattedMessage id="ui-orders.poLine.automaticExport.noIntegration" />}
renderTrigger={({ open, ref, toggle }) => (
<IconButton
ref={ref}
icon="exclamation-circle"
className={css.warningButton}
onClick={toggle}
aria-label={intl.formatMessage({ id: 'ui-orders.poLine.automaticExport.noIntegration' })}
aria-haspopup="dialog"
aria-expanded={open}
/>
)}
/>
);
}

if (applicableIntegrations.length > 1) {
return (
<InfoPopover
content={(
<>
<strong>
<FormattedMessage id="ui-orders.poLine.automaticExport.multipleTitle" />
</strong>
<ul className={css.list}>
{applicableIntegrations.map((config) => (
<li key={config.id}>{formatConfigLabel(config)}</li>
))}
</ul>
<FormattedMessage id="ui-orders.poLine.automaticExport.multipleHint" />
</>
)}
/>
);
}

return null;
};

AutomaticExportInfo.propTypes = {
acquisitionMethod: PropTypes.string,
automaticExport: PropTypes.bool,
integrationConfigs: PropTypes.arrayOf(PropTypes.object),
isLoading: PropTypes.bool,
manualOrder: PropTypes.bool,
placement: PropTypes.oneOf(['label', 'below']),
vendorAccount: PropTypes.string,
};

export default AutomaticExportInfo;
158 changes: 158 additions & 0 deletions src/common/POLFields/DetailsFields/AutomaticExportInfo.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { render, screen } from '@folio/jest-config-stripes/testing-library/react';
import userEvent from '@folio/jest-config-stripes/testing-library/user-event';

import { integrationConfig } from 'fixtures';
import AutomaticExportInfo from './AutomaticExportInfo';

const acquisitionMethod = integrationConfig
.exportTypeSpecificParameters
.vendorEdiOrdersExportConfig
.ediConfig
.defaultAcquisitionMethods[0];
const vendorAccount = integrationConfig
.exportTypeSpecificParameters
.vendorEdiOrdersExportConfig
.ediConfig
.accountNoList[0];

const secondConfig = {
...integrationConfig,
id: 'second-config',
exportTypeSpecificParameters: {
vendorEdiOrdersExportConfig: {
...integrationConfig.exportTypeSpecificParameters.vendorEdiOrdersExportConfig,
configName: 'secondConfig',
transmissionMethod: 'Email',
},
},
};

const defaultProps = {
acquisitionMethod,
automaticExport: true,
integrationConfigs: [integrationConfig],
vendorAccount,
};

const renderAutomaticExportInfo = (props = {}) => render(
<AutomaticExportInfo
{...defaultProps}
{...props}
/>,
);

describe('AutomaticExportInfo', () => {
describe('placement "below"', () => {
it('should render the single matching integration with its full label as title', () => {
renderAutomaticExportInfo({ placement: 'below' });

const hint = screen.getByText('ui-orders.poLine.automaticExport.exportVia');

expect(hint).toBeInTheDocument();
expect(hint.closest('[title]')).toHaveAttribute('title', 'testConfig');
});

it('should include the transmission method in the label', () => {
renderAutomaticExportInfo({
placement: 'below',
integrationConfigs: [secondConfig],
});

expect(screen.getByText('ui-orders.poLine.automaticExport.exportVia').closest('[title]'))
.toHaveAttribute('title', 'secondConfig (Email)');
});

it('should render nothing when no integration matches', () => {
const { container } = renderAutomaticExportInfo({
placement: 'below',
integrationConfigs: [],
});

expect(container).toBeEmptyDOMElement();
});

it('should render nothing when several integrations match', () => {
const { container } = renderAutomaticExportInfo({
placement: 'below',
integrationConfigs: [integrationConfig, secondConfig],
});

expect(container).toBeEmptyDOMElement();
});
});

describe('placement "label"', () => {
it('should render a warning icon when no integration matches', async () => {
renderAutomaticExportInfo({
placement: 'label',
integrationConfigs: [],
});

const warning = screen.getByRole('button', { name: 'ui-orders.poLine.automaticExport.noIntegration' });

await userEvent.click(warning);

expect(warning).toHaveAttribute('aria-expanded', 'true');
expect(screen.getByText('ui-orders.poLine.automaticExport.noIntegration')).toBeInTheDocument();
});

it('should render an info popover listing all matching integrations', async () => {
renderAutomaticExportInfo({
placement: 'label',
integrationConfigs: [integrationConfig, secondConfig],
});

await userEvent.click(screen.getByRole('button'));

expect(screen.getByText('ui-orders.poLine.automaticExport.multipleTitle')).toBeInTheDocument();
expect(screen.getByText('testConfig')).toBeInTheDocument();
expect(screen.getByText('secondConfig (Email)')).toBeInTheDocument();
expect(screen.getByText('ui-orders.poLine.automaticExport.multipleHint')).toBeInTheDocument();
});

it('should render nothing when exactly one integration matches', () => {
const { container } = renderAutomaticExportInfo({ placement: 'label' });

expect(container).toBeEmptyDOMElement();
});
});

it('should render nothing when automatic export is off', () => {
const { container } = renderAutomaticExportInfo({
placement: 'label',
automaticExport: false,
integrationConfigs: [],
});

expect(container).toBeEmptyDOMElement();
});

it('should render nothing without integration configs (order templates)', () => {
const { container } = renderAutomaticExportInfo({
placement: 'label',
integrationConfigs: undefined,
});

expect(container).toBeEmptyDOMElement();
});

it('should render nothing while the integration configs are loading', () => {
const { container } = renderAutomaticExportInfo({
placement: 'label',
isLoading: true,
integrationConfigs: [],
});

expect(container).toBeEmptyDOMElement();
});

it('should render nothing for a manual order', () => {
const { container } = renderAutomaticExportInfo({
placement: 'label',
manualOrder: true,
integrationConfigs: [],
});

expect(container).toBeEmptyDOMElement();
});
});
4 changes: 3 additions & 1 deletion src/common/POLFields/DetailsFields/FieldAutomaticExport.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import {

import { POL_FORM_FIELDS } from '../../constants';

const FieldAutomaticExport = ({ isManualOrder = false, ...props }) => {
const FieldAutomaticExport = ({ isManualOrder = false, exportInfo = null, ...props }) => {
const label = (
<>
<FormattedMessage id="ui-orders.poLine.automaticExport" />
{isManualOrder && <InfoPopover content={<FormattedMessage id="ui-orders.poLine.manualPO.info" />} />}
{exportInfo}
</>
);

Expand All @@ -32,6 +33,7 @@ const FieldAutomaticExport = ({ isManualOrder = false, ...props }) => {
};

FieldAutomaticExport.propTypes = {
exportInfo: PropTypes.node,
isManualOrder: PropTypes.bool,
};

Expand Down
13 changes: 13 additions & 0 deletions src/common/POLFields/DetailsFields/FieldAutomaticExport.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,17 @@ describe('FieldAutomaticExport', () => {

expect(screen.getByText('ui-orders.poLine.automaticExport')).toBeInTheDocument();
});

it('should render the manual order info for a manual order', () => {
renderFieldAutomaticExport({ isManualOrder: true });

expect(screen.getByRole('button')).toBeInTheDocument();
expect(screen.getByRole('checkbox', { name: /ui-orders.poLine.automaticExport/ })).toBeInTheDocument();
});

it('should render the export info inside the label', () => {
renderFieldAutomaticExport({ exportInfo: <span>export info</span> });

expect(screen.getByRole('checkbox', { name: /export info/ })).toBeInTheDocument();
});
});
1 change: 1 addition & 0 deletions src/common/POLFields/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export { default as FieldCheckInItems } from './DetailsFields/FieldCheckInItems'
export { default as FieldCancellationRestrictionNote } from './DetailsFields/FieldCancellationRestrictionNote';
export { default as FieldPOLineDescription } from './DetailsFields/FieldPOLineDescription';
export { default as FieldAutomaticExport } from './DetailsFields/FieldAutomaticExport';
export { default as AutomaticExportInfo } from './DetailsFields/AutomaticExportInfo';

// vendor
export { default as FieldVendorInstructions } from './VendorFields/FieldVendorInstructions';
Expand Down
Loading
Loading