diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fd58c275..2cee3b27f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## 7.1.0 (IN PROGRESS) * Display the “Record deleted” label in version history only if the UUID no longer exists. Refs UIOR-1355. +* Add the "Save & keep editing" button to the PO form. Refs UIOR-1325. +* Add the "Save & keep editing" button to the PO Line form. Refs UIOR-1351. ## [7.0.4](https://github.com/folio-org/ui-orders/tree/v7.0.4) (2024-12-31) [Full Changelog](https://github.com/folio-org/ui-orders/compare/v7.0.3...v7.0.4) diff --git a/src/common/constants/constants.js b/src/common/constants/constants.js index f03f7a096..b0fa0fd47 100644 --- a/src/common/constants/constants.js +++ b/src/common/constants/constants.js @@ -71,3 +71,5 @@ export const CENTRAL_ORDERING_DEFAULT_RECEIVING_SEARCH = { centralDefault: 'Central default', activeAffiliationDefault: 'Active affiliation default', }; + +export const SUBMIT_ACTION_FIELD = '__submitAction__'; diff --git a/src/common/hooks/useOrder/useOrder.js b/src/common/hooks/useOrder/useOrder.js index a0fba85d0..60bfc6d7d 100644 --- a/src/common/hooks/useOrder/useOrder.js +++ b/src/common/hooks/useOrder/useOrder.js @@ -12,7 +12,11 @@ export const useOrder = (orderId) => { query: `id==${orderId}`, }; - const { isLoading, data } = useQuery( + const { + data, + isLoading, + refetch, + } = useQuery( ['ui-orders', 'order', orderId], async () => { try { @@ -29,5 +33,6 @@ export const useOrder = (orderId) => { return ({ order: data, isLoading, + refetch, }); }; diff --git a/src/common/hooks/useOrderLine/useOrderLine.js b/src/common/hooks/useOrderLine/useOrderLine.js index 0edb65dbf..800abe732 100644 --- a/src/common/hooks/useOrderLine/useOrderLine.js +++ b/src/common/hooks/useOrderLine/useOrderLine.js @@ -7,20 +7,32 @@ import { import { LINES_API } from '@folio/stripes-acq-components'; -export const useOrderLine = (lineId) => { - const ky = useOkapiKy(); - const [namespace] = useNamespace({ key: 'order-versions' }); +export const useOrderLine = (lineId, options = {}) => { + const { + enabled = true, + tenantId, + ...queryOptions + } = options; - const { isLoading, data } = useQuery( - [namespace, lineId], - async () => ky.get(`${LINES_API}/${lineId}`).json(), - { - enabled: Boolean(lineId), - }, - ); + const [namespace] = useNamespace({ key: 'purchase-order-line' }); + const ky = useOkapiKy({ tenant: tenantId }); + + const { + data, + isFetching, + isLoading, + refetch, + } = useQuery({ + queryKey: [namespace, lineId, tenantId], + queryFn: ({ signal }) => ky.get(`${LINES_API}/${lineId}`, { signal }).json(), + enabled: enabled && Boolean(lineId), + ...queryOptions, + }); return ({ orderLine: data, + isFetching, isLoading, + refetch, }); }; diff --git a/src/components/LayerCollection/LayerPO.js b/src/components/LayerCollection/LayerPO.js index 57585af11..cfbdb3043 100644 --- a/src/components/LayerCollection/LayerPO.js +++ b/src/components/LayerCollection/LayerPO.js @@ -1,12 +1,15 @@ -import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import get from 'lodash/get'; import PropTypes from 'prop-types'; -import { get } from 'lodash'; +import { + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; import { FormattedMessage } from 'react-intl'; import { LoadingView } from '@folio/stripes/components'; -import { - stripesConnect, -} from '@folio/stripes/core'; +import { stripesConnect } from '@folio/stripes/core'; import { baseManifest, ORDER_STATUSES, @@ -16,9 +19,15 @@ import { useShowCallout, } from '@folio/stripes-acq-components'; +import { SUBMIT_ACTION_FIELD } from '../../common/constants'; import { - createOrEditOrderResource, -} from '../Utils/orderResource'; + useHandleOrderUpdateError, + useOrder, +} from '../../common/hooks'; +import { SUBMIT_ACTION } from '../PurchaseOrder/constants'; +import POForm from '../PurchaseOrder/POForm'; +import { UpdateOrderErrorModal } from '../PurchaseOrder/UpdateOrderErrorModal'; +import { createOrEditOrderResource } from '../Utils/orderResource'; import { ADDRESSES, ORDER, @@ -27,9 +36,6 @@ import { ORDER_TEMPLATES, USERS, } from '../Utils/resources'; -import { useHandleOrderUpdateError } from '../../common/hooks/useHandleOrderUpdateError'; -import POForm from '../PurchaseOrder/POForm'; -import { UpdateOrderErrorModal } from '../PurchaseOrder/UpdateOrderErrorModal'; const NEW_ORDER = { reEncumber: true, @@ -53,8 +59,17 @@ function LayerPO({ const [isLoading, setIsLoading] = useState(true); const [updateOrderError, setUpdateOrderError] = useState(); const [isErrorsModalOpened, toggleErrorsModal] = useModalToggle(); - const order = id ? resources?.order?.records[0] : NEW_ORDER; + const instanceId = location.state?.instanceId; + const instanceTenantId = location.state?.instanceTenantId; + + const { + order: fetchedOrder, + isLoading: isOrderLoading, + refetch, + } = useOrder(id); + + const order = id ? fetchedOrder : NEW_ORDER; useEffect(() => { memoizedMutator.orderNumber.reset(); @@ -73,33 +88,56 @@ function LayerPO({ setUpdateOrderError(errors); }, [toggleErrorsModal]); - const updatePO = useCallback(values => { + const updatePO = useCallback((values) => { setIsLoading(true); - setSavingValues(values); - return createOrEditOrderResource(values, memoizedMutator.order) - .then(savedOrder => { + const { [SUBMIT_ACTION_FIELD]: submitAction, ...data } = values; + + setSavingValues(data); + + return createOrEditOrderResource(data, mutator.order) + .then((savedOrder) => { sendCallout({ message: , }); - history.push({ - pathname: instanceId ? `/orders/view/${savedOrder.id}/po-line/create` : `/orders/view/${savedOrder.id}`, - search: location.search, - state: instanceId ? { instanceId, instanceTenantId: location.state?.instanceTenantId } : {}, - }); + + return savedOrder; + }) + .then(async (savedOrder) => { + setSavingValues(null); + + switch (submitAction) { + case SUBMIT_ACTION.saveAndKeepEditing: + await refetch(); + + history.push({ + pathname: `/orders/edit/${savedOrder.id}`, + search: location.search, + }); + break; + case SUBMIT_ACTION.saveAndClose: + default: + history.push({ + pathname: instanceId ? `/orders/view/${savedOrder.id}/po-line/create` : `/orders/view/${savedOrder.id}`, + search: location.search, + state: instanceId ? { instanceId, instanceTenantId } : {}, + }); + break; + } }) .catch(async e => { - setIsLoading(false); await handleErrorResponse(e, openOrderErrorModalShow); - }); + }) + .finally(() => setIsLoading(false)); }, [ handleErrorResponse, history, instanceId, location.search, - location.state?.instanceTenantId, - memoizedMutator.order, + instanceTenantId, + mutator.order, openOrderErrorModalShow, + refetch, sendCallout, ]); @@ -116,7 +154,14 @@ function LayerPO({ [history, id, location.search, instanceId], ); - if (isLoading || !order) return ; + if (isLoading || isOrderLoading || !order) { + return ( + + ); + } const { poNumber, poNumberPrefix, poNumberSuffix } = order; const generatedNumber = get(resources, 'orderNumber.records.0.poNumber'); @@ -152,7 +197,10 @@ function LayerPO({ } LayerPO.manifest = Object.freeze({ - order: ORDER, + order: { + ...ORDER, + fetch: false, + }, addresses: ADDRESSES, users: { ...USERS, diff --git a/src/components/LayerCollection/LayerPO.test.js b/src/components/LayerCollection/LayerPO.test.js index 47aecb364..8e9566996 100644 --- a/src/components/LayerCollection/LayerPO.test.js +++ b/src/components/LayerCollection/LayerPO.test.js @@ -1,6 +1,10 @@ import { MemoryRouter } from 'react-router'; -import { render, screen, waitFor } from '@folio/jest-config-stripes/testing-library/react'; +import { + render, + screen, + waitFor, +} from '@folio/jest-config-stripes/testing-library/react'; import { ORDER_TYPES } from '@folio/stripes-acq-components'; import { @@ -12,23 +16,26 @@ import { history, location, } from 'fixtures/routerMocks'; +import { SUBMIT_ACTION_FIELD } from '../../common/constants'; +import { useOrder } from '../../common/hooks'; +import { SUBMIT_ACTION } from '../PurchaseOrder/constants'; import POForm from '../PurchaseOrder/POForm'; import LayerPO from './LayerPO'; +jest.mock('../../common/hooks', () => ({ + ...jest.requireActual('../../common/hooks'), + useOrder: jest.fn(), +})); jest.mock('../PurchaseOrder/POForm', () => jest.fn().mockReturnValue('POForm')); const defaultProps = { resourses: { - order: { - records: [order], - }, orderNumber: { records: [{ poNumber: '10000' }], }, }, mutator: { order: { - GET: jest.fn().mockResolvedValue([order]), POST: jest.fn().mockResolvedValue([order]), PUT: jest.fn().mockResolvedValue([order]), }, @@ -73,9 +80,14 @@ const renderLayerPO = (props = {}) => render( describe('LayerPO', () => { beforeEach(() => { - defaultProps.mutator.order.POST.mockClear(); - history.push.mockClear(); - POForm.mockClear(); + useOrder.mockReturnValue({ + order, + refetch: jest.fn(), + }); + }); + + afterEach(() => { + jest.clearAllMocks(); }); it('should render PO form', async () => { @@ -101,7 +113,22 @@ describe('LayerPO', () => { orderType: ORDER_TYPES.ongoing, })); - expect(history.push).toHaveBeenCalled(); + expect(history.push).toHaveBeenCalledWith(expect.objectContaining({ + pathname: expect.stringMatching(/orders\/view/), + })); + }); + + it('should keep edit form opened if saveAndKeepEditing action was selected', async () => { + renderLayerPO(); + + await waitFor(() => POForm.mock.calls[0][0].onSubmit({ + orderType: ORDER_TYPES.ongoing, + [SUBMIT_ACTION_FIELD]: SUBMIT_ACTION.saveAndKeepEditing, + })); + + expect(history.push).toHaveBeenCalledWith(expect.objectContaining({ + pathname: expect.stringMatching(/orders\/edit/), + })); }); describe('Create from inventory', () => { @@ -120,6 +147,7 @@ describe('LayerPO', () => { await waitFor(() => POForm.mock.calls[0][0].onSubmit({ orderType: ORDER_TYPES.ongoing, + [SUBMIT_ACTION_FIELD]: SUBMIT_ACTION.saveAndClose, })); expect(history.push).toHaveBeenCalledWith(expect.objectContaining({ @@ -130,7 +158,7 @@ describe('LayerPO', () => { }); it('should throw an error if the order update was failed ', async () => { - defaultProps.mutator.order.POST.mockRejectedValue({}); + defaultProps.mutator.order.POST.mockRejectedValueOnce({}); renderLayerPO(); diff --git a/src/components/LayerCollection/LayerPOLine.js b/src/components/LayerCollection/LayerPOLine.js index ffc79d00d..c2c0d80bb 100644 --- a/src/components/LayerCollection/LayerPOLine.js +++ b/src/components/LayerCollection/LayerPOLine.js @@ -25,32 +25,34 @@ import { LoadingView, } from '@folio/stripes/components'; import { - baseManifest, DICT_CONTRIBUTOR_NAME_TYPES, DICT_IDENTIFIER_TYPES, getConfigSetting, LIMIT_MAX, materialTypesManifest, ORDER_FORMATS, + ResponseErrorsContainer, sourceValues, useCentralOrderingContext, useIntegrationConfigs, useLocationsQuery, useModalToggle, + useOrganization, useShowCallout, - VENDORS_API, } from '@folio/stripes-acq-components'; import { ERROR_CODES, - WORKFLOW_STATUS, + SUBMIT_ACTION_FIELD, VALIDATION_ERRORS, + WORKFLOW_STATUS, } from '../../common/constants'; import { useInstance, useLinesLimit, useOpenOrderSettings, useOrder, + useOrderLine, useTitleMutation, } from '../../common/hooks'; import { @@ -61,6 +63,7 @@ import { import DuplicateLinesModal from '../../common/DuplicateLinesModal'; import { DISCOUNT_TYPE, + SUBMIT_ACTION, } from '../POLine/const'; import { cloneOrder, @@ -76,7 +79,6 @@ import { ORDER_NUMBER, ORDER_TEMPLATES, ORDERS, - VALIDATE_ISBN, } from '../Utils/resources'; import { POLineForm } from '../POLine'; import LinesLimit from '../PurchaseOrder/LinesLimit'; @@ -96,6 +98,24 @@ const parseErrorMessage = (code) => { const FIELD_ARRAYS_TO_HYDRATE = ['locations']; +const handleVendorLoadingError = async (response, sendCallout) => { + const { handler } = await ResponseErrorsContainer.create(response); + + sendCallout({ + message: , + type: 'error', + }); + + const message = handler.getError().message; + + if (message) { + sendCallout({ + message: , + type: 'error', + }); + } +}; + function LayerPOLine({ history, location: { search, state: locationState }, @@ -105,47 +125,88 @@ function LayerPOLine({ stripes, }) { const intl = useIntl(); - const [isLinesLimitExceededModalOpened, setLinesLimitExceededModalOpened] = useState(false); + const sendCallout = useShowCallout(); + const { isCentralOrderingEnabled } = useCentralOrderingContext(); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const memoizedMutator = useMemo(() => mutator, []); + const [isDeletePiecesOpened, toggleDeletePieces] = useModalToggle(); const [isNotUniqueOpen, toggleNotUnique] = useModalToggle(); const [isDifferentAccountModalOpened, toggleDifferentAccountModal] = useModalToggle(); + + const [isLinesLimitExceededModalOpened, setIsLinesLimitExceededModalOpened] = useState(false); const [accountNumbers, setAccountNumbers] = useState([]); const [savingValues, setSavingValues] = useState(); - const sendCallout = useShowCallout(); - const [isLoading, setIsLoading] = useState(false); - const { isFetching: isOpenOrderSettingsFetching, openOrderSettings } = useOpenOrderSettings(); - const { isOpenOrderEnabled, isDuplicateCheckDisabled } = openOrderSettings; - const [isValidateDuplicateLines, setValidateDuplicateLines] = useState(); + const [isProcessing, setIsProcessing] = useState(false); + const [isValidateDuplicateLines, setIsValidateDuplicateLines] = useState(); const [duplicateLines, setDuplicateLines] = useState(); - const { isLoading: isOrderLoading, order } = useOrder(id); - const createInventory = get(resources, ['createInventory', 'records']); - const createInventorySetting = useMemo( - () => getCreateInventorySetting(createInventory), - [createInventory], - ); + const [poLines, setPoLines] = useState(); - const poLine = poLines?.find((u) => u.id === lineId); - const [vendor, setVendor] = useState(); - const { isLoading: isLinesLimitLoading, linesLimit } = useLinesLimit(!(lineId || poLine)); - const [isCreateAnotherChecked, setCreateAnotherChecked] = useState(locationState?.isCreateAnotherChecked); - const { isFetching: isConfigsFetching, integrationConfigs } = useIntegrationConfigs({ organizationId: vendor?.id }); - const { instance, isLoading: isInstanceLoading } = useInstance( - locationState?.instanceId, - { - tenantId: locationState?.instanceTenantId, - }, - ); + + const locationStateInstanceId = locationState?.instanceId; + const isCreateFromInstance = Boolean(locationStateInstanceId); + const { mutateTitle } = useTitleMutation(); - const { isCentralOrderingEnabled } = useCentralOrderingContext(); + /* Queries */ + const { + orderLine: poLine, + isLoading: isOrderLineLoading, + refetch, + } = useOrderLine(lineId); + + const { + isFetching: isOpenOrderSettingsFetching, + openOrderSettings, + } = useOpenOrderSettings(); + + const { + isLoading: isOrderLoading, + order, + } = useOrder(id); + + const { + organization: vendor, + isLoading: isVendorLoading, + } = useOrganization( + order?.vendor, + { onError: ({ response }) => handleVendorLoadingError(response, sendCallout) }, + ); + + const { + instance, + isLoading: isInstanceLoading, + } = useInstance(locationStateInstanceId, { tenantId: locationState?.instanceTenantId }); + + const { + isLoading: isLinesLimitLoading, + linesLimit, + } = useLinesLimit(!(lineId || poLine)); + + const { + isFetching: isConfigsFetching, + integrationConfigs, + } = useIntegrationConfigs({ organizationId: vendor?.id }); const { isLoading: isLocationsLoading, locations, } = useLocationsQuery({ consortium: isCentralOrderingEnabled }); + /* */ - // eslint-disable-next-line react-hooks/exhaustive-deps - const memoizedMutator = useMemo(() => mutator, []); + const { isOpenOrderEnabled, isDuplicateCheckDisabled } = openOrderSettings; + const { isApprovalRequired } = getConfigSetting(get(resources, 'approvalsSetting.records', {})); + const createInventory = resources?.createInventory?.records; + const isOrderApproved = isApprovalRequired ? order?.approved : true; + const differentAccountsModalLabel = intl.formatMessage({ id: 'ui-orders.differentAccounts.title' }); + const createInventorySetting = useMemo(() => getCreateInventorySetting(createInventory), [createInventory]); + + const isSaveAndOpenButtonVisible = ( + isOpenOrderEnabled && + isOrderApproved && + order?.workflowStatus === WORKFLOW_STATUS.pending + ); useEffect(() => { setPoLines(); @@ -159,16 +220,16 @@ function LayerPOLine({ }, [id, memoizedMutator.poLines]); useEffect(() => { - setValidateDuplicateLines(!isDuplicateCheckDisabled); + setIsValidateDuplicateLines(!isDuplicateCheckDisabled); }, [isDuplicateCheckDisabled]); const openLineLimitExceededModal = useCallback(line => { - setLinesLimitExceededModalOpened(true); + setIsLinesLimitExceededModalOpened(true); setSavingValues(line); }, []); const closeLineLimitExceededModal = useCallback(() => { - setLinesLimitExceededModalOpened(false); + setIsLinesLimitExceededModalOpened(false); setSavingValues(); }, []); @@ -214,48 +275,43 @@ function LayerPOLine({ [intl, openLineLimitExceededModal, sendCallout, toggleDeletePieces], ); - const openOrder = useCallback( - (saveAndOpen, newLine = {}) => { - if (saveAndOpen) { - const exportAccountNumbers = getExportAccountNumbers([...order.compositePoLines, newLine]); + const openOrder = useCallback((newLine = {}) => { + const exportAccountNumbers = getExportAccountNumbers([...order.compositePoLines, newLine]); - if (!order.manualPo && exportAccountNumbers.length > 1) { - setAccountNumbers(exportAccountNumbers); + if (!order.manualPo && exportAccountNumbers.length > 1) { + setAccountNumbers(exportAccountNumbers); - // eslint-disable-next-line prefer-promise-reject-errors - return Promise.reject({ validationError: VALIDATION_ERRORS.differentAccount }); - } + // eslint-disable-next-line prefer-promise-reject-errors + return Promise.reject({ validationError: VALIDATION_ERRORS.differentAccount }); + } - return updateOrderResource(order, memoizedMutator.lineOrder, { - workflowStatus: WORKFLOW_STATUS.open, - }) - .then(() => { - sendCallout({ - message: ( - - ), - type: 'success', - }); - }) - .catch(errorResponse => { - sendCallout({ - message: ( - - ), - type: 'error', - }); - throw errorResponse; - }); - } else return Promise.resolve(); - }, - [memoizedMutator.lineOrder, order, sendCallout], - ); + return updateOrderResource(order, mutator.lineOrder, { + workflowStatus: WORKFLOW_STATUS.open, + }) + .then(() => { + sendCallout({ + message: ( + + ), + type: 'success', + }); + }) + .catch(errorResponse => { + sendCallout({ + message: ( + + ), + type: 'error', + }); + throw errorResponse; + }); + }, [mutator.lineOrder, order, sendCallout]); const formatPOLineBeforeSaving = (line) => { switch (line.orderFormat) { @@ -267,41 +323,61 @@ function LayerPOLine({ }; const submitPOLine = useCallback(async (lineValues) => { - const { saveAndOpen, isAcknowledged, ...line } = lineValues; - let savedLine; + const { + [SUBMIT_ACTION_FIELD]: submitAction, + isAcknowledged, + ...line + } = lineValues; - setIsLoading(true); + let savedLine; + setIsProcessing(true); setSavingValues(lineValues); - try { - setIsLoading(true); + try { if (isValidateDuplicateLines) { - setValidateDuplicateLines(false); + setIsValidateDuplicateLines(false); await validateDuplicateLines(line, mutator, resources); } const newLine = formatPOLineBeforeSaving(cloneDeep(line)); - savedLine = await memoizedMutator.poLines.POST(newLine); + savedLine = await mutator.poLines.POST(newLine); - await openOrder(saveAndOpen, savedLine); + let pathname = isCreateFromInstance + ? `/orders/view/${id}/po-line/view/${savedLine.id}` + : `/orders/view/${id}`; - sendCallout({ - message: , - type: 'success', - }); + switch (submitAction) { + case SUBMIT_ACTION.saveAndOpen: { + await openOrder(savedLine); + + if (isCreateFromInstance) { + pathname = `/inventory/view/${locationStateInstanceId}`; + } - let pathname = isCreateAnotherChecked - ? `/orders/view/${id}/po-line/create` - : `/orders/view/${id}/po-line/view/${savedLine.id}`; + break; + } + case SUBMIT_ACTION.saveAndCreateAnother: { + pathname = `/orders/view/${id}/po-line/create`; + break; + } + case SUBMIT_ACTION.saveAndKeepEditing: { + await refetch(); - if (locationState?.instanceId) { - pathname = saveAndOpen ? `/inventory/view/${locationState.instanceId}` : `/orders/view/${id}`; + pathname = `/orders/view/${id}/po-line/edit/${savedLine.id}`; + break; + } + case SUBMIT_ACTION.saveAndClose: + default: + break; } - const state = isCreateAnotherChecked ? { isCreateAnotherChecked: true } : {}; + sendCallout({ + message: , + type: 'success', + }); setSavingValues(); @@ -319,7 +395,6 @@ function LayerPOLine({ return history.push({ pathname, search, - state, }); } catch (e) { if (e?.validationError === VALIDATION_ERRORS.duplicateLines) { @@ -327,8 +402,9 @@ function LayerPOLine({ return toggleNotUnique(); } - if (saveAndOpen && savedLine) { - await memoizedMutator.poLines.DELETE(savedLine); + + if (submitAction === SUBMIT_ACTION.saveAndOpen && savedLine) { + await mutator.poLines.DELETE(savedLine); } if (e?.validationError === VALIDATION_ERRORS.differentAccount) { @@ -337,21 +413,22 @@ function LayerPOLine({ return handleErrorResponse(e, line); } finally { - setIsLoading(false); + setIsProcessing(false); } - }, - - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - handleErrorResponse, - history, - id, - isCreateAnotherChecked, + }, [ isValidateDuplicateLines, - memoizedMutator.poLines, - openOrder, - search, + mutator, + isCreateFromInstance, + id, sendCallout, + history, + search, + resources, + openOrder, + locationStateInstanceId, + refetch, + mutateTitle, + handleErrorResponse, toggleNotUnique, toggleDifferentAccountModal, ]); @@ -359,12 +436,12 @@ function LayerPOLine({ const createNewOrder = useCallback( async () => { closeLineLimitExceededModal(); - setIsLoading(true); + setIsProcessing(true); try { const newOrder = await cloneOrder( order, - memoizedMutator.lineOrder, + mutator.lineOrder, memoizedMutator.orderNumber, savingValues && [savingValues], ); @@ -374,7 +451,7 @@ function LayerPOLine({ search, }); } catch (e) { - setIsLoading(false); + setIsProcessing(false); sendCallout({ message: , type: 'error', @@ -384,7 +461,7 @@ function LayerPOLine({ [ closeLineLimitExceededModal, history, - memoizedMutator.lineOrder, + mutator.lineOrder, memoizedMutator.orderNumber, order, savingValues, @@ -405,21 +482,21 @@ function LayerPOLine({ }, [history, id, lineId, search, locationState]); const updatePOLine = useCallback(async (hydratedLine) => { - const { saveAndOpen, ...data } = hydratedLine; + const { [SUBMIT_ACTION_FIELD]: submitAction, ...data } = hydratedLine; - setIsLoading(true); + setIsProcessing(true); setSavingValues(hydratedLine); if (isValidateDuplicateLines) { try { - setValidateDuplicateLines(false); + setIsValidateDuplicateLines(false); await validateDuplicateLines(hydratedLine, mutator, resources); } catch (e) { if (e?.validationError === VALIDATION_ERRORS.duplicateLines) { setDuplicateLines(e.duplicateLines); - setIsLoading(false); + setIsProcessing(false); return toggleNotUnique(); } @@ -430,8 +507,7 @@ function LayerPOLine({ delete line.metadata; - return memoizedMutator.poLines.PUT(line) - .then(() => openOrder(saveAndOpen)) + return mutator.poLines.PUT(line) .then(() => { sendCallout({ message: ( @@ -442,10 +518,27 @@ function LayerPOLine({ ), type: 'success', }); - setTimeout(onCancel); + }) + .then(async () => { + switch (submitAction) { + case SUBMIT_ACTION.saveAndOpen: { + await openOrder(); + onCancel(); + break; + } + case SUBMIT_ACTION.saveAndKeepEditing: + await refetch(); + break; + case SUBMIT_ACTION.saveAndClose: + default: + onCancel(); + break; + } + + setIsProcessing(false); }) .catch((e) => { - setIsLoading(false); + setIsProcessing(false); if (e?.validationError === VALIDATION_ERRORS.differentAccount) { return toggleDifferentAccountModal(); @@ -453,16 +546,16 @@ function LayerPOLine({ return handleErrorResponse(e, line); }); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - handleErrorResponse, + }, [ isValidateDuplicateLines, - memoizedMutator.poLines, + mutator, + resources, + toggleNotUnique, + sendCallout, + refetch, onCancel, openOrder, - sendCallout, - toggleNotUnique, + handleErrorResponse, toggleDifferentAccountModal, ]); @@ -506,7 +599,7 @@ function LayerPOLine({ newObj.eresource.accessProvider = vendor.id; newObj.physical.materialSupplier = vendor.id; - if (vendor.discountPercent) { + if (vendor?.discountPercent) { newObj.cost.discountType = DISCOUNT_TYPE.percentage; newObj.cost.discount = vendor.discountPercent; } @@ -515,54 +608,6 @@ function LayerPOLine({ return newObj; }, [createInventorySetting.eresource, createInventorySetting.physical, order, stripes.currency, vendor]); - const vendorId = order?.vendor; - - useEffect( - () => { - if (vendorId) { - memoizedMutator.orderVendor.GET({ path: `${VENDORS_API}/${vendorId}` }) - .then( - setVendor, - errorResponse => { - setVendor({}); - - let response; - - try { - response = JSON.parse(errorResponse?.message); - } catch (parsingException) { - response = errorResponse; - } - - sendCallout({ - message: , - type: 'error', - }); - - const message = response?.errors?.[0]?.message; - - if (message) { - sendCallout({ - message: , - type: 'error', - }); - } - }, - ); - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [memoizedMutator.orderVendor, vendorId], - ); - - const { isApprovalRequired } = getConfigSetting( - get(resources, 'approvalsSetting.records', {}), - ); - const isOrderApproved = isApprovalRequired ? order?.approved : true; - const isSaveAndOpenButtonVisible = - isOpenOrderEnabled && - isOrderApproved && - order?.workflowStatus === WORKFLOW_STATUS.pending; const isntLoaded = !( get(resources, 'createInventory.hasLoaded') && !isOrderLoading && @@ -578,16 +623,23 @@ function LayerPOLine({ !isConfigsFetching && !isOpenOrderSettingsFetching && !isInstanceLoading && - !isLocationsLoading + !isLocationsLoading && + !isOrderLineLoading && + !isVendorLoading ); - if (isLoading || isntLoaded) return ; + if (isProcessing || isntLoaded) { + return ( + + ); + } const initialValues = lineId ? poLine : getCreatePOLIneInitialValues; const onSubmit = lineId ? updatePOLine : submitPOLine; - const differentAccountsModalLabel = intl.formatMessage({ id: 'ui-orders.differentAccounts.title' }); - return ( <> { toggleNotUnique(); - setValidateDuplicateLines(false); + setIsValidateDuplicateLines(false); onSubmit(savingValues); }} onCancel={() => { toggleNotUnique(); - setValidateDuplicateLines(true); + setIsValidateDuplicateLines(true); }} /> ) @@ -665,11 +714,6 @@ LayerPOLine.manifest = Object.freeze({ approvalsSetting: APPROVALS_SETTING, [DICT_CONTRIBUTOR_NAME_TYPES]: CONTRIBUTOR_NAME_TYPES, poLines: ORDER_LINES, - orderVendor: { - ...baseManifest, - accumulate: true, - fetch: false, - }, createInventory: CREATE_INVENTORY, orderTemplates: { ...ORDER_TEMPLATES, @@ -680,7 +724,6 @@ LayerPOLine.manifest = Object.freeze({ accumulate: false, fetch: true, }, - validateISBN: VALIDATE_ISBN, convertToIsbn13: CONVERT_TO_ISBN13, [DICT_IDENTIFIER_TYPES]: IDENTIFIER_TYPES, orderNumber: ORDER_NUMBER, diff --git a/src/components/LayerCollection/LayerPOLine.test.js b/src/components/LayerCollection/LayerPOLine.test.js index 305d56f01..f82a98858 100644 --- a/src/components/LayerCollection/LayerPOLine.test.js +++ b/src/components/LayerCollection/LayerPOLine.test.js @@ -12,6 +12,7 @@ import { } from '@folio/jest-config-stripes/testing-library/react'; import { useLocationsQuery, + useOrganization, useShowCallout, } from '@folio/stripes-acq-components'; @@ -25,9 +26,15 @@ import { location, match, } from 'fixtures/routerMocks'; -import { useOrder } from '../../common/hooks'; +import { SUBMIT_ACTION_FIELD } from '../../common/constants'; +import { + useOrder, + useOrderLine, +} from '../../common/hooks'; import ModalDeletePieces from '../ModalDeletePieces'; +import { SUBMIT_ACTION } from '../POLine/const'; import POLineForm from '../POLine/POLineForm'; +import { updateOrderResource } from '../Utils/orderResource'; import LayerPOLine from './LayerPOLine'; jest.mock('@folio/stripes-acq-components', () => ({ @@ -35,12 +42,14 @@ jest.mock('@folio/stripes-acq-components', () => ({ useCentralOrderingContext: jest.fn(() => ({ isCentralOrderingEnabled: false })), useIntegrationConfigs: jest.fn().mockReturnValue({ integrationConfigs: [], isLoading: false }), useLocationsQuery: jest.fn(), + useOrganization: jest.fn(), useShowCallout: jest.fn(), })); jest.mock('../../common/hooks', () => ({ useOpenOrderSettings: jest.fn().mockReturnValue({ isFetching: false, openOrderSettings: {} }), useLinesLimit: jest.fn().mockReturnValue({ isLoading: false, linesLimit: 1 }), useOrder: jest.fn(), + useOrderLine: jest.fn(), useInstance: jest.fn().mockReturnValue({ isLoading: false, instance: {} }), useTitleMutation: jest.fn().mockReturnValue({ mutateTitle: jest.fn().mockReturnValue(() => Promise.resolve()) }), })); @@ -50,13 +59,16 @@ jest.mock('../../common/utils', () => ({ ...jest.requireActual('../../common/utils'), validateDuplicateLines: jest.fn().mockReturnValue(Promise.resolve()), })); +jest.mock('../Utils/orderResource', () => ({ + ...jest.requireActual('../Utils/orderResource'), + updateOrderResource: jest.fn(() => Promise.resolve()), +})); const queryClient = new QueryClient(); const defaultProps = { mutator: { lineOrder: { - GET: jest.fn().mockResolvedValue(order), POST: jest.fn().mockResolvedValue(order), PUT: jest.fn().mockResolvedValue(order), }, @@ -71,9 +83,6 @@ const defaultProps = { PUT: jest.fn().mockResolvedValue(orderLine), POST: jest.fn().mockResolvedValue(orderLine), }, - orderVendor: { - GET: jest.fn().mockResolvedValue(vendor), - }, createInventory: { GET: jest.fn().mockResolvedValue(), }, @@ -83,9 +92,6 @@ const defaultProps = { materialTypes: { GET: jest.fn().mockResolvedValue(), }, - validateISBN: { - GET: jest.fn().mockResolvedValue(), - }, identifierTypes: { GET: jest.fn().mockResolvedValue(), }, @@ -97,10 +103,6 @@ const defaultProps = { createInventory: { hasLoaded: true, }, - lineOrder: { - hasLoaded: true, - records: [order], - }, approvalsSetting: { hasLoaded: true, }, @@ -128,7 +130,6 @@ const defaultProps = { history, }; -// eslint-disable-next-line react/prop-types const wrapper = ({ children }) => ( @@ -146,22 +147,26 @@ const renderLayerPOLine = (props = {}) => render( ); const mockShowCallout = jest.fn(); +const refetchOrderLine = jest.fn(); describe('LayerPOLine', () => { beforeEach(() => { - POLineForm.mockClear(); - history.push.mockClear(); - defaultProps.mutator.poLines.PUT.mockClear(); - defaultProps.mutator.poLines.POST.mockClear(); - useOrder - .mockClear() - .mockReturnValue({ isLoading: false, order }); - useLocationsQuery - .mockClear() - .mockReturnValue({ locations: [location] }); - useShowCallout - .mockClear() - .mockReturnValue(mockShowCallout); + useOrder.mockReturnValue({ + isLoading: false, + order: { ...order, compositePoLines: [] }, + }); + useOrderLine.mockReturnValue({ + isLoading: false, + orderLine, + refetch: refetchOrderLine, + }); + useOrganization.mockReturnValue({ organization: vendor }); + useLocationsQuery.mockReturnValue({ locations: [location] }); + useShowCallout.mockReturnValue(mockShowCallout); + }); + + afterEach(() => { + jest.clearAllMocks(); }); it('should render POLineForm', async () => { @@ -180,7 +185,7 @@ describe('LayerPOLine', () => { }, } }); - await waitFor(() => POLineForm.mock.calls[0][0].onSubmit({ saveAndOpen: false, isAcknowledged: true })); + await waitFor(() => POLineForm.mock.calls[0][0].onSubmit({ isAcknowledged: true })); expect(defaultProps.mutator.poLines.POST).toHaveBeenCalled(); }); @@ -190,12 +195,89 @@ describe('LayerPOLine', () => { await waitFor(() => POLineForm.mock.calls[0][0].onSubmit({ ...orderLine, - saveAndOpen: true, + [SUBMIT_ACTION_FIELD]: SUBMIT_ACTION.saveAndOpen, })); expect(defaultProps.mutator.poLines.PUT).toHaveBeenCalled(); }); + describe('Alternative submit actions', () => { + const submitWithType = (submitAction) => () => { + return POLineForm.mock.calls[0][0].onSubmit({ + ...orderLine, + [SUBMIT_ACTION_FIELD]: submitAction, + }); + }; + + describe('Add PO Line', () => { + it('should create POLine and keep the edit form opened', async () => { + renderLayerPOLine({ + match: { + ...match, + params: { id: order.id }, + }, + }); + + await waitFor(submitWithType(SUBMIT_ACTION.saveAndKeepEditing)); + + expect(defaultProps.mutator.poLines.POST).toHaveBeenCalled(); + expect(history.push).toHaveBeenCalledWith(expect.objectContaining({ + pathname: expect.stringMatching(/orders\/view\/.*\/po-line\/edit\/.*/), + })); + }); + + it('should create POLine and open the form for another PO Line', async () => { + renderLayerPOLine({ + match: { + ...match, + params: { id: order.id }, + }, + }); + + await waitFor(submitWithType(SUBMIT_ACTION.saveAndCreateAnother)); + + expect(defaultProps.mutator.poLines.POST).toHaveBeenCalled(); + expect(history.push).toHaveBeenCalledWith(expect.objectContaining({ + pathname: expect.stringMatching(/orders\/view\/.*\/po-line\/create/), + })); + }); + + it('should create POLine and open the order', async () => { + renderLayerPOLine({ + match: { + ...match, + params: { id: order.id }, + }, + }); + + await waitFor(submitWithType(SUBMIT_ACTION.saveAndOpen)); + + expect(defaultProps.mutator.poLines.POST).toHaveBeenCalled(); + expect(updateOrderResource).toHaveBeenCalled(); + }); + }); + + describe('Update PO Line', () => { + it('should update POLine and keep the edit form opened', async () => { + renderLayerPOLine(); + + await waitFor(submitWithType(SUBMIT_ACTION.saveAndKeepEditing)); + + expect(defaultProps.mutator.poLines.PUT).toHaveBeenCalled(); + expect(refetchOrderLine).toHaveBeenCalled(); + }); + + it('should update POLine and open the order', async () => { + renderLayerPOLine(); + + await waitFor(submitWithType(SUBMIT_ACTION.saveAndOpen)); + + expect(defaultProps.mutator.poLines.PUT).toHaveBeenCalled(); + expect(updateOrderResource).toHaveBeenCalled(); + }); + }); + }); + it('should call onCancel if cancelling', async () => { renderLayerPOLine(); @@ -298,7 +380,7 @@ describe('LayerPOLine', () => { renderLayerPOLine(); - await waitFor(() => POLineForm.mock.calls[0][0].onSubmit({ saveAndOpen: true })); + await waitFor(() => POLineForm.mock.calls[0][0].onSubmit({ [SUBMIT_ACTION_FIELD]: SUBMIT_ACTION.saveAndOpen })); const modal = await screen.findByText(/ui-orders.differentAccounts.title/i); @@ -310,6 +392,15 @@ describe('LayerPOLine', () => { ['someError', 'error message'], ['genericError', 'Invalid token'], ])('should handle \'%s\' error', async (code, message) => { + useOrganization.mockImplementationOnce(async (_id, { onError }) => { + await onError({ + clone: jest.fn().mockReturnThis(), + json: jest.fn().mockResolvedValue({ errors: [{ message: '' }] }), + }); + + return { organization: null }; + }); + // eslint-disable-next-line prefer-promise-reject-errors defaultProps.mutator.poLines.PUT.mockImplementation(() => Promise.reject({ errors: [{ diff --git a/src/components/POLine/POLineForm.css b/src/components/POLine/POLineForm.css deleted file mode 100644 index 4aba2d2aa..000000000 --- a/src/components/POLine/POLineForm.css +++ /dev/null @@ -1,3 +0,0 @@ -.createAnotherCheckbox { - margin-right: 10px; -}; diff --git a/src/components/POLine/POLineForm.js b/src/components/POLine/POLineForm.js index 80a160b79..4920fd2c8 100644 --- a/src/components/POLine/POLineForm.js +++ b/src/components/POLine/POLineForm.js @@ -30,7 +30,6 @@ import { AccordionSet, AccordionStatus, Button, - Checkbox, checkScope, Col, collapseAllSections, @@ -44,6 +43,7 @@ import { Pane, PaneFooter, PaneMenu, + Paneset, Row, Selection, } from '@folio/stripes/components'; @@ -57,7 +57,10 @@ import { ViewMetaData, } from '@folio/stripes/smart-components'; -import { ENTITY_TYPE_PO_LINE } from '../../common/constants'; +import { + ENTITY_TYPE_PO_LINE, + SUBMIT_ACTION_FIELD, +} from '../../common/constants'; import { useErrorAccordionStatus, useFundDistributionValidation, @@ -89,6 +92,7 @@ import { INITIAL_SECTIONS, MAP_FIELD_ACCORDION, POL_TEMPLATE_FIELDS_MAP, + SUBMIT_ACTION, } from './const'; import getMaterialTypesForSelect from '../Utils/getMaterialTypesForSelect'; import getIdentifierTypesForSelect from '../Utils/getIdentifierTypesForSelect'; @@ -97,10 +101,8 @@ import getOrderTemplatesForSelect from '../Utils/getOrderTemplatesForSelect'; import { ifDisabledToChangePaymentInfo } from '../PurchaseOrder/util'; import getOrderTemplateValue from '../Utils/getOrderTemplateValue'; import calculateEstimatedPrice from './calculateEstimatedPrice'; -import { createPOLDataFromInstance } from './Item/util'; import { useManageDonorOrganizationIds } from './hooks'; - -import styles from './POLineForm.css'; +import { createPOLDataFromInstance } from './Item/util'; const GAME_CHANGER_FIELDS = ['isPackage', 'orderFormat', 'checkinItems', 'packagePoLineId', 'instanceId']; const GAME_CHANGER_TIMEOUT = 50; @@ -123,8 +125,6 @@ function POLineForm({ enableSaveBtn, linesLimit, locations, - isCreateAnotherChecked = false, - toggleCreateAnother, integrationConfigs = [], instance, isCreateFromInstance = false, @@ -138,7 +138,6 @@ function POLineForm({ const identifierTypes = getIdentifierTypesForSelect(parentResources); const lineId = get(initialValues, 'id'); - const saveBtnLabelId = isCreateAnotherChecked ? 'ui-orders.buttons.line.save' : 'stripes-components.saveAndClose'; const initialDonorOrganizationIds = get(initialValues, 'donorOrganizationIds', []); const fundDistribution = get(formValues, 'fundDistribution', []); const lineLocations = get(formValues, 'locations', []); @@ -299,67 +298,99 @@ function POLineForm({ ) ); - const submitAndOpen = useCallback(() => { - change('saveAndOpen', true); + const onSaveAndClose = useCallback(() => { + change(SUBMIT_ACTION_FIELD, SUBMIT_ACTION.saveAndClose); + handleSubmit(); + }, [change, handleSubmit]); + + const onSaveAndCreateAnother = useCallback(() => { + change(SUBMIT_ACTION_FIELD, SUBMIT_ACTION.saveAndCreateAnother); + handleSubmit(); + }, [change, handleSubmit]); + + const onSaveAndOpen = useCallback(() => { + change(SUBMIT_ACTION_FIELD, SUBMIT_ACTION.saveAndOpen); handleSubmit(); }, [change, handleSubmit]); - const submit = useCallback(() => { - change('saveAndOpen', false); + const onSaveAndKeepEditing = useCallback(() => { + change(SUBMIT_ACTION_FIELD, SUBMIT_ACTION.saveAndKeepEditing); handleSubmit(); }, [change, handleSubmit]); const getPaneFooter = () => { + const isSubmitBtnDisabled = !enableSaveBtn && (pristine || submitting); + const start = ( - - {([btnLabel]) => ( + + - )} - + + ); - const buttonSaveStyle = isSaveAndOpenButtonVisible ? 'default mega' : 'primary mega'; - const end = ( - <> + + + + + {!isCreateFromInstance && !lineId && (linesLimit > 1) && ( - } - checked={isCreateAnotherChecked} - onChange={e => toggleCreateAnother(e.target.checked)} - className={styles.createAnotherCheckbox} - inline - /> + + + )} - - {isSaveAndOpenButtonVisible && ( + + + + + {isSaveAndOpenButtonVisible && ( + + + )} - + ); return ( @@ -491,223 +522,225 @@ function POLineForm({ isWithinScope={checkScope} scope={document.body} > - - - {({ status }) => ( -
- - - - - - - - - - - - - - - - {([translatedLabel]) => ( - - )} - - - - - - - - } - id={ACCORDION_ID.itemDetails} - > - {metadata && } - - - - } - id={ACCORDION_ID.lineDetails} - > - - - } + + + + {({ status }) => ( + + + + + + + + + + + + + + + + + {([translatedLabel]) => ( + + )} + + + + + + + - - - {isOngoing(order.orderType) && ( } - id={ACCORDION_ID.ongoingOrder} + label={} + id={ACCORDION_ID.itemDetails} > - } + + - )} - } - id={ACCORDION_ID.vendor} - > - - - } - id={ACCORDION_ID.costDetails} - > - - - } - id={ACCORDION_ID.fundDistribution} - > - - - } - id={ACCORDION_ID.location} - > - - - {showPhresources && ( } - id={ACCORDION_ID.physical} + label={} + id={ACCORDION_ID.lineDetails} > - - )} - {showEresources && ( } - id={ACCORDION_ID.eresources} + id={ACCORDION_ID.donorsInformation} + label={} > - + + {isOngoing(order.orderType) && ( + } + id={ACCORDION_ID.ongoingOrder} + > + + + )} + } + id={ACCORDION_ID.vendor} + > + - )} - {showOther && ( } - id={ACCORDION_ID.other} + label={} + id={ACCORDION_ID.costDetails} > - - )} - - - - - - - -
- )} -
-
+ } + id={ACCORDION_ID.fundDistribution} + > + + + } + id={ACCORDION_ID.location} + > + + + {showPhresources && ( + } + id={ACCORDION_ID.physical} + > + + + )} + {showEresources && ( + } + id={ACCORDION_ID.eresources} + > + + + )} + {showOther && ( + } + id={ACCORDION_ID.other} + > + + + )} + + + + + + + + + )} + + + ); } @@ -737,8 +770,6 @@ POLineForm.propTypes = { libraryId: PropTypes.string.isRequired, tenantId: PropTypes.string, })).isRequired, - isCreateAnotherChecked: PropTypes.bool, - toggleCreateAnother: PropTypes.func.isRequired, integrationConfigs: PropTypes.arrayOf(PropTypes.object), instance: PropTypes.object, isCreateFromInstance: PropTypes.bool, diff --git a/src/components/POLine/POLineForm.test.js b/src/components/POLine/POLineForm.test.js index e9a14cb7c..cd7ee455b 100644 --- a/src/components/POLine/POLineForm.test.js +++ b/src/components/POLine/POLineForm.test.js @@ -123,8 +123,6 @@ const defaultProps = { }], }, }, - toggleCreateAnother: jest.fn(), - isCreateAnotherChecked: false, linesLimit: 3, values: { orderFormat: 'P/E Mix', @@ -221,7 +219,7 @@ describe('POLineForm', () => { renderPOLineForm(); await waitFor(() => { - expect(screen.getByText('ui-orders.buttons.line.createAnother')).toBeInTheDocument(); + expect(screen.getByText('ui-orders.buttons.line.saveAndCreateAnother')).toBeInTheDocument(); }); }); @@ -229,7 +227,7 @@ describe('POLineForm', () => { renderPOLineForm({ isCreateFromInstance: true }); await waitFor(() => { - expect(screen.queryByText('ui-orders.buttons.line.createAnother')).not.toBeInTheDocument(); + expect(screen.queryByText('ui-orders.buttons.line.saveAndCreateAnother')).not.toBeInTheDocument(); }); }); }); diff --git a/src/components/POLine/const.js b/src/components/POLine/const.js index ec05028e6..abb43d59f 100644 --- a/src/components/POLine/const.js +++ b/src/components/POLine/const.js @@ -87,3 +87,10 @@ export const ACCOUNT_STATUS = { INACTIVE: 'Inactive', PENDING: 'Pending', }; + +export const SUBMIT_ACTION = { + saveAndClose: 'saveAndClose', + saveAndCreateAnother: 'saveAndCreateAnother', + saveAndKeepEditing: 'saveAndKeepEditing', + saveAndOpen: 'saveAndOpen', +}; diff --git a/src/components/PurchaseOrder/PO.js b/src/components/PurchaseOrder/PO.js index 39fa39820..99a2d450a 100644 --- a/src/components/PurchaseOrder/PO.js +++ b/src/components/PurchaseOrder/PO.js @@ -1,9 +1,18 @@ /* eslint-disable max-lines */ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { FormattedMessage, useIntl } from 'react-intl'; import PropTypes from 'prop-types'; +import get from 'lodash/get'; +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { + FormattedMessage, + useIntl, +} from 'react-intl'; import ReactRouterPropTypes from 'react-router-prop-types'; -import { get } from 'lodash'; import { IfPermission, @@ -28,22 +37,23 @@ import { AccordionSet, AccordionStatus, Button, - Col, checkScope, + Col, collapseAllSections, ConfirmationModal, Dropdown, DropdownMenu, + ErrorModal, ExpandAllButton, expandAllSections, HasCommand, Icon, Loading, LoadingPane, + MenuSection, Pane, PaneMenu, Row, - ErrorModal, } from '@folio/stripes/components'; import { ColumnManagerMenu, @@ -99,15 +109,15 @@ import { ORDERS, } from '../Utils/resources'; import CloseOrderModal from './CloseOrder'; -import OpenOrderConfirmationModal from './OpenOrderConfirmationModal'; -import LineListing from './LineListing'; -import LinesLimit from './LinesLimit'; -import POInvoicesContainer from './POInvoices'; import { LINE_LISTING_COLUMN_MAPPING } from './constants'; import { getPOActionMenu } from './getPOActionMenu'; import { useOrderMutation } from './hooks'; +import LineListing from './LineListing'; +import LinesLimit from './LinesLimit'; import { OngoingOrderInfoView } from './OngoingOrderInfo'; +import OpenOrderConfirmationModal from './OpenOrderConfirmationModal'; import { PODetailsView } from './PODetails'; +import POInvoicesContainer from './POInvoices'; import { SummaryView } from './Summary'; import { UnopenOrderConfirmationModal } from './UnopenOrderConfirmationModal'; import { UpdateOrderErrorModal } from './UpdateOrderErrorModal'; @@ -600,19 +610,22 @@ const PO = ({ buttonProps={{ buttonStyle: 'primary' }} > - - - + + + + + + (prev ? undefined : template?.hiddenFields || {})); }, [template]); + const onSaveAndKeepEditing = useCallback(() => { + change(SUBMIT_ACTION_FIELD, SUBMIT_ACTION.saveAndKeepEditing); + handleSubmit(); + }, [change, handleSubmit]); + + const onSaveAndClose = useCallback(() => { + change(SUBMIT_ACTION_FIELD, SUBMIT_ACTION.saveAndClose); + handleSubmit(); + }, [change, handleSubmit]); + const firstMenu = useMemo(() => { return ( @@ -161,34 +181,58 @@ const POForm = ({ ), [hiddenFields, template, toggleForceVisibility]); const getPaneFooter = useCallback((id, label) => { + const isSubmitBtnDisabled = pristine || submitting; + const start = ( - - {(btnLabel) => ( - - )} - + + + + {(btnLabel) => ( + + )} + + + ); const end = ( - - {btnLabel => ( - + + {!instanceId && ( + + + )} - + + + + {btnLabel => ( + + )} + + + ); return ( @@ -197,7 +241,14 @@ const POForm = ({ renderEnd={end} /> ); - }, [handleSubmit, onCancel, pristine, submitting]); + }, [ + instanceId, + onCancel, + onSaveAndClose, + onSaveAndKeepEditing, + pristine, + submitting, + ]); const onChangeTemplate = useCallback((value) => { const templateValue = getOrderTemplateValue(parentResources, value); @@ -246,9 +297,9 @@ const POForm = ({ ? : ; - const buttonLabelId = instanceId ? 'ui-orders.paneMenu.addPOLine' : 'ui-orders.paneMenu.saveOrder'; + const buttonLabelId = instanceId ? 'ui-orders.paneMenu.addPOLine' : 'stripes-components.saveAndClose'; const paneFooter = initialValues.id - ? getPaneFooter('clickable-update-purchase-order', 'ui-orders.paneMenu.saveOrder') + ? getPaneFooter('clickable-update-purchase-order', 'stripes-components.saveAndClose') : getPaneFooter('clickable-create-new-purchase-order', buttonLabelId); const prefixesSetting = get(parentResources, 'prefixesSetting.records', []) @@ -296,108 +347,106 @@ const POForm = ({ } return ( -
- - - - - {({ status }) => ( -
- - - - - - - - - - - - - - - - - - - - - + + + + {({ status }) => ( + + + + + + + + + + + + + + + + + + + + + + + } + > + + + {isOngoing(formValues.orderType) && ( + + )} + } > - } - > - - - {isOngoing(formValues.orderType) && ( - - )} - } - > - - - - - - - - - - )} - - - -
-
+ + + + + + + + + )} + + + + ); }; diff --git a/src/components/PurchaseOrder/constants.js b/src/components/PurchaseOrder/constants.js index 2de65e830..7664e4e6c 100644 --- a/src/components/PurchaseOrder/constants.js +++ b/src/components/PurchaseOrder/constants.js @@ -32,3 +32,8 @@ export const MAP_FIELD_ACCORDION = { orderType: ACCORDION_ID.purchaseOrder, notes: ACCORDION_ID.purchaseOrder, }; + +export const SUBMIT_ACTION = { + saveAndClose: 'saveAndClose', + saveAndKeepEditing: 'saveAndKeepEditing', +}; diff --git a/translations/ui-orders/en.json b/translations/ui-orders/en.json index 3bc900440..833e45301 100644 --- a/translations/ui-orders/en.json +++ b/translations/ui-orders/en.json @@ -23,9 +23,8 @@ "button.printLine": "Print order line", "buttons.line.cancel": "Cancel", "buttons.line.close": "Close", - "buttons.line.save": "Save", "buttons.line.saveAndOpen": "Save & open order", - "buttons.line.createAnother": "Create another", + "buttons.line.saveAndCreateAnother": "Save & create another", "buttons.line.submit": "Submit", "buttons.line.changeInstance": "Change instance connection", "canceled": "Canceled", @@ -496,7 +495,6 @@ "paneMenu.addPOLine": "Add POL", "paneMenu.createPurchaseOrder": "Create purchase order", "paneMenu.editOrder": "Edit order", - "paneMenu.saveOrder": "Save & close", "paneBlock.approveBtn": "Approve", "payment_status.awaitingPayment": "Awaiting payment", "payment_status.cancelled": "Cancelled",