Repository: folio-org/ui-receiving
Pull request #651: UIREC-448 Implement removeFromPackage button to allow user to "Unlink" title
Author: login: azizjonnurov
Created: 2025-04-18T11:57:35Z | Merged: 2025-04-29T14:02:24Z

<!--
  If you have a relevant JIRA issue number, please put it in the issue title.
  Example: MODORDERS-70 Orders schema updates
-->

## Purpose
<!--
  Why are you making this change? There is nothing more important
  to provide to the reviewer and to future readers than the cause
  that gave rise to this pull request. Be careful to avoid circular
  statements like "the purpose is to update the schema." and
  instead provide an explanation like "there is more data to be provided and stored for Purchase Orders
  which is currently missing in the schema"

  The purpose may seem self-evident to you now, but the standard to
  hold yourself to should be "can a developer parachuting into this
  project reconstruct the necessary context merely by reading this
  section."

  If you have a relevant JIRA issue, add a link directly to the issue URL here.
  Example: https://issues.folio.org/browse/MODORDERS-70
 -->
https://folio-org.atlassian.net/browse/UIREC-448

## Screenshots
<!-- OPTIONAL
 One picture is literally worth a thousand words. When the feature is
 an interaction, an animated GIF or video is best. Most of the time it helps to
 include "before" and "after" screenshots to quickly demonstrate the
 value of the feature.

 Here are some great tools to help you record gifs:

 Windows: https://getsharex.com/
 Mac: https://gifox.io/
-->
![image](https://github.com/user-attachments/assets/6b55e38b-f617-43d8-a0e3-eabb90a00a81)
![image](https://github.com/user-attachments/assets/5502dfe1-df2e-49e4-ba3e-5caba7469573)
![image](https://github.com/user-attachments/assets/9b0fe15a-c19c-4a75-8fcc-17a7721dae27)
![image](https://github.com/user-attachments/assets/1c0b825d-452a-47c3-9218-483f963b618c)

<!-- OPTIONAL
#### TODOS and Open Questions
- [ ] Use GitHub checklists. When solved, check the box and explain the answer.
-->

<!-- OPTIONAL
## Learning
  Help out not only your reviewer, but also your fellow developer!
  Sometimes there are key pieces of information that you used to come up
  with your solution. Don't let all that hard work go to waste! A
  pull request is a *perfect opportunity to share the learning that
  you did. Add links to blog posts, patterns, libraries or addons used
  to solve this problem.
-->

## Pre-Merge Checklist
Before merging this PR, please go through the following list and take appropriate actions.
- [x] I've added appropriate record to the CHANGELOG.md
- Does this PR meet or exceed the expected quality standards?
  - [x] Code coverage on new code is 80% or greater
  - [x] Duplications on new code is 3% or less
  - [x] There are no major code smells or security issues
- Does this introduce breaking changes?
  - [ ] If any API-related changes - okapi interfaces and permissions are reviewed/changed correspondingly
  - [x] There are no breaking changes in this PR.

If there are breaking changes, please **STOP** and consider the following:

- What other modules will these changes impact?
- Do JIRAs exist to update the impacted modules?
  - [ ] If not, please create them
  - [ ] Do they contain the appropriate level of detail?  Which endpoints/schemas changed, etc.
  - [ ] Do they have all they appropriate links to blocked/related issues?
- Are the JIRAs under active development?
  - [ ] If not, contact the project's PO and make sure they're aware of the urgency.
- Do PRs exist for these changes?
  - [ ] If so, have they been approved?

Ideally all of the PRs involved in breaking changes would be merged in the same day to avoid breaking the folio-testing environment.  Communication is paramount if that is to be achieved, especially as the number of intermodule and inter-team dependencies increase.

While it's helpful for reviewers to help identify potential problems, ensuring that it's safe to merge is ultimately the responsibility of the PR assignee.



Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7709600d..10d8c31d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
 ## 8.0.0 (IN PROGRESS)
 
 * *BREAKING* Update `orders` and `order-lines` interfaces to `13.0` and `4.0` accordingly. Refs UIREC-447.
+* Allow user to "Unlink" title from package POL view. Refs UIREC-448.
 
 ## [7.0.2](https://github.com/folio-org/ui-receiving/tree/v7.0.2) (2025-04-14)
 [Full Changelog](https://github.com/folio-org/ui-receiving/compare/v7.0.1...v7.0.2)
diff --git a/src/ReceivingList/ReceivingList.js b/src/ReceivingList/ReceivingList.js
index c232801b..336a37a8 100644
--- a/src/ReceivingList/ReceivingList.js
+++ b/src/ReceivingList/ReceivingList.js
@@ -100,6 +100,7 @@ const ReceivingList = ({
   onNeedMoreData,
   pagination,
   query,
+  refreshList,
   resetData,
   tenantId,
   titles,
@@ -156,6 +157,13 @@ const ReceivingList = ({
     />
   );
 
+  const renderTitleDetailsContainer = useCallback((props) => (
+    <TitleDetailsContainer
+      {...props}
+      refreshList={refreshList}
+    />
+  ), [refreshList]);
+
   const shortcuts = [
     {
       name: 'new',
@@ -274,7 +282,7 @@ const ReceivingList = ({
 
         <Route
           path={`${match.path}/:id/view`}
-          component={TitleDetailsContainer}
+          render={renderTitleDetailsContainer}
         />
       </PersistedPaneset>
     </HasCommand>
@@ -291,6 +299,7 @@ ReceivingList.propTypes = {
   onNeedMoreData: PropTypes.func.isRequired,
   pagination: PropTypes.object.isRequired,
   query: PropTypes.string,
+  refreshList: PropTypes.func.isRequired,
   resetData: PropTypes.func.isRequired,
   tenantId: PropTypes.string.isRequired,
   titles: PropTypes.arrayOf(PropTypes.object),
diff --git a/src/ReceivingList/ReceivingListContainer.js b/src/ReceivingList/ReceivingListContainer.js
index 225ebde2..d9efbc6c 100644
--- a/src/ReceivingList/ReceivingListContainer.js
+++ b/src/ReceivingList/ReceivingListContainer.js
@@ -103,7 +103,7 @@ const ReceivingListContainer = () => {
     return { orderLinesMap };
   }, [invalidReferenceMessage, isCentralOrderingEnabled, isTargetTenantCentral, stripes]);
 
-  const { pagination, changePage } = usePagination({ limit: RESULT_COUNT_INCREMENT, offset: 0 });
+  const { pagination, changePage, refreshPage } = usePagination({ limit: RESULT_COUNT_INCREMENT, offset: 0 });
   const {
     isFetching,
     query,
@@ -122,17 +122,18 @@ const ReceivingListContainer = () => {
 
   return (
     <ReceivingList
+      crossTenant={crossTenant}
+      filtersStorageKey={filtersStorageKey}
+      isLoading={isFetching}
       key={targetTenantId}
       onNeedMoreData={changePage}
-      resetData={resetData}
-      titlesCount={totalRecords}
-      isLoading={isFetching}
-      titles={titles}
       pagination={pagination}
       query={query}
-      crossTenant={crossTenant}
+      refreshList={refreshPage}
+      resetData={resetData}
       tenantId={targetTenantId}
-      filtersStorageKey={filtersStorageKey}
+      titles={titles}
+      titlesCount={totalRecords}
     />
   );
 };
diff --git a/src/TitleDetails/TitleDetails.js b/src/TitleDetails/TitleDetails.js
index 412e111a..8ee3340a 100644
--- a/src/TitleDetails/TitleDetails.js
+++ b/src/TitleDetails/TitleDetails.js
@@ -28,10 +28,11 @@ import {
   ExpandAllButton,
   expandAllSections,
   HasCommand,
+  MenuSection,
   MessageBanner,
   Pane,
-  PaneMenu,
   Row,
+  Icon,
 } from '@folio/stripes/components';
 import {
   IfPermission,
@@ -55,7 +56,11 @@ import {
   useModalToggle,
 } from '@folio/stripes-acq-components';
 
-import { ConfirmReceivingModal } from '../common/components';
+import {
+  ConfirmReceivingModal,
+  RemoveFromPackageModals,
+} from '../common/components';
+import { useRemoveFromPackage } from '../common/hooks';
 import {
   CENTRAL_RECEIVING_PIECE_CREATE_ROUTE,
   CENTRAL_RECEIVING_PIECE_EDIT_ROUTE,
@@ -103,13 +108,14 @@ const TitleDetails = ({
   order,
   piecesExistence,
   poLine,
+  refreshList,
   title,
   vendorsMap = {},
 }) => {
   const intl = useIntl();
   const stripes = useStripes();
-  const [isAcknowledgeNote, toggleAcknowledgeNote] = useModalToggle();
   const [confirmAcknowledgeNote, setConfirmAcknowledgeNote] = useState();
+  const [isAcknowledgeNote, toggleAcknowledgeNote] = useModalToggle();
   const [isConfirmReceiving, toggleConfirmReceiving] = useModalToggle();
   const confirmReceivingPromise = useRef({});
   const accordionStatusRef = useRef();
@@ -266,6 +272,11 @@ const TitleDetails = ({
     toggleConfirmReceiving();
   };
 
+  const onDeleteSuccess = () => {
+    onClose();
+    refreshList();
+  };
+
   const hasReceive = Boolean(piecesExistence?.[EXPECTED_PIECES_SEARCH_VALUE]);
 
   const [isExpectedPiecesLoading, setExpectedPiecesLoading] = useState(false);
@@ -294,6 +305,13 @@ const TitleDetails = ({
     searchQuery: unreceivablePiecesSearchQuery,
   } = useFilters(noop);
   const { filters: boundItemsFilters } = useFilters(noop, { [MENU_FILTERS.bound]: ['true'] });
+  const {
+    isRemoveFromPackageOpen,
+    toggleRemoveFromPackageModal,
+    isRemoveHoldingsOpen,
+    toggleRemoveHoldingsModal,
+    onConfirmRemoveFromPackage,
+  } = useRemoveFromPackage({ id: titleId, onSuccess: onDeleteSuccess });
 
   const expectedPiecesProtectedActions = useMemo(() => ({
     [EXPECTED_PIECES_ACTION_NAMES.addPiece]: (
@@ -376,19 +394,43 @@ const TitleDetails = ({
     />
   );
 
-  const lastMenu = (
-    <PaneMenu>
+  const renderActionMenu = () => (
+    <MenuSection id="receiving-title-actions">
       <IfPermission perm="ui-receiving.edit">
-        <Button
-          onClick={onEdit}
-          marginBottom0
-          disabled={isRestrictedByAcqUnit}
-          buttonStyle="primary"
-        >
-          <FormattedMessage id="ui-receiving.title.details.button.edit" />
-        </Button>
+        <FormattedMessage id="ui-receiving.title.details.button.edit">
+          {ariaLabel => (
+            <Button
+              onClick={onEdit}
+              disabled={isRestrictedByAcqUnit}
+              aria-label={ariaLabel}
+              buttonStyle="dropdownItem"
+              marginBottom0
+            >
+              <Icon size="small" icon="edit">
+                <FormattedMessage id="ui-receiving.title.details.button.edit" />
+              </Icon>
+            </Button>
+          )}
+        </FormattedMessage>
+      </IfPermission>
+
+      <IfPermission perm="ui-receiving.delete">
+        <FormattedMessage id="ui-receiving.title.paneTitle.removeFromPackage">
+          {ariaLabel => (
+            <Button
+              onClick={toggleRemoveFromPackageModal}
+              aria-label={ariaLabel}
+              buttonStyle="dropdownItem"
+              marginBottom0
+            >
+              <Icon size="small" icon="cancel">
+                <FormattedMessage id="ui-receiving.title.paneTitle.removeFromPackage" />
+              </Icon>
+            </Button>
+          )}
+        </FormattedMessage>
       </IfPermission>
-    </PaneMenu>
+    </MenuSection>
   );
 
   return (
@@ -398,13 +440,13 @@ const TitleDetails = ({
       scope={document.body}
     >
       <Pane
+        actionMenu={renderActionMenu}
         id="pane-title-details"
         defaultWidth="fill"
         dismissible
         paneTitle={title.title}
         paneSub={poLineNumber}
         onClose={onClose}
-        lastMenu={lastMenu}
       >
         <TitleManager record={title.title} />
         <AccordionStatus ref={accordionStatusRef}>
@@ -617,6 +659,14 @@ const TitleDetails = ({
           }}
         />
 
+        <RemoveFromPackageModals
+          isRemoveFromPackageOpen={isRemoveFromPackageOpen}
+          isRemoveHoldingsOpen={isRemoveHoldingsOpen}
+          onConfirmRemoveFromPackage={onConfirmRemoveFromPackage}
+          toggleRemoveFromPackageModal={toggleRemoveFromPackageModal}
+          toggleRemoveHoldingsModal={toggleRemoveHoldingsModal}
+        />
+
         <ConfirmReceivingModal
           open={isConfirmReceiving}
           onCancel={onCancelReceiving}
@@ -636,6 +686,7 @@ TitleDetails.propTypes = {
   order: PropTypes.object.isRequired,
   piecesExistence: PropTypes.object,
   poLine: PropTypes.object.isRequired,
+  refreshList: PropTypes.object.isRequired,
   title: PropTypes.object.isRequired,
   vendorsMap: PropTypes.object.isRequired,
 };
diff --git a/src/TitleDetails/TitleDetailsContainer.js b/src/TitleDetails/TitleDetailsContainer.js
index 2a4bc8ab..58145f3c 100644
--- a/src/TitleDetails/TitleDetailsContainer.js
+++ b/src/TitleDetails/TitleDetailsContainer.js
@@ -1,3 +1,4 @@
+import PropTypes from 'prop-types';
 import {
   useCallback,
   useState,
@@ -39,6 +40,7 @@ const TitleDetailsContainer = ({
   history,
   location,
   match,
+  refreshList,
 }) => {
   const titleId = match.params.id;
 
@@ -158,6 +160,7 @@ const TitleDetailsContainer = ({
       order={order || DEFAULT_DATA_OBJECT}
       piecesExistence={piecesExistence}
       poLine={poLine || DEFAULT_DATA_OBJECT}
+      refreshList={refreshList}
       title={title || DEFAULT_DATA_OBJECT}
       vendorsMap={vendorsMap}
     />
@@ -168,6 +171,7 @@ TitleDetailsContainer.propTypes = {
   history: ReactRouterPropTypes.history.isRequired,
   match: ReactRouterPropTypes.match.isRequired,
   location: ReactRouterPropTypes.location.isRequired,
+  refreshList: PropTypes.func.isRequired,
 };
 
 export default TitleDetailsContainer;
diff --git a/src/TitleForm/TitleForm.js b/src/TitleForm/TitleForm.js
index 1c13156e..dc74f9a6 100644
--- a/src/TitleForm/TitleForm.js
+++ b/src/TitleForm/TitleForm.js
@@ -10,6 +10,7 @@ import {
   Accordion,
   AccordionSet,
   AccordionStatus,
+  Button,
   Checkbox,
   checkScope,
   Col,
@@ -37,14 +38,16 @@ import {
   validateRequiredPositiveNumber,
 } from '@folio/stripes-acq-components';
 
+import { RemoveFromPackageModals } from '../common/components';
+import { useRemoveFromPackage } from '../common/hooks';
 import {
   CENTRAL_RECEIVING_ROUTE,
   RECEIVING_ROUTE,
 } from '../constants';
 import { useReceivingSearchContext } from '../contexts';
 import { SECTIONS } from './constants';
-import ProductIdDetailsForm from './ProductIdDetailsForm';
 import ContributorsForm from './ContributorsForm';
+import ProductIdDetailsForm from './ProductIdDetailsForm';
 
 const ALLOWED_YEAR_LENGTH = 4;
 const ASSIGN_ACQ_UNITS_PERM = 'titles.acquisitions-units-assignments.assign';
@@ -76,9 +79,27 @@ const TitleForm = ({
 
   const { isCentralRouting } = useReceivingSearchContext();
 
+  const {
+    isRemoveFromPackageOpen,
+    isRemoveHoldingsOpen,
+    onConfirmRemoveFromPackage,
+    toggleRemoveFromPackageModal,
+    toggleRemoveHoldingsModal,
+  } = useRemoveFromPackage({ id, onSuccess: onCancel });
+
   const isEditMode = Boolean(id);
   const disabled = (isEditMode && restrictions?.protectUpdate) || isRestrictionsLoading;
 
+  const lastMenu = (
+    <Button
+      onClick={toggleRemoveFromPackageModal}
+      buttonStyle="primary paneHeaderNewButton"
+      marginBottom0
+    >
+      <FormattedMessage id="ui-receiving.title.paneTitle.removeFromPackage" />
+    </Button>
+  );
+
   const paneFooter = (
     <FormFooter
       handleSubmit={handleSubmit}
@@ -145,6 +166,7 @@ const TitleForm = ({
             onClose={onCancel}
             paneTitle={paneTitle}
             footer={paneFooter}
+            lastMenu={isEditMode && lastMenu}
           >
             <Row>
               <Col
@@ -380,6 +402,15 @@ const TitleForm = ({
                 </AccordionStatus>
               </Col>
             </Row>
+
+            <RemoveFromPackageModals
+              isRemoveFromPackageOpen={isRemoveFromPackageOpen}
+              isRemoveHoldingsOpen={isRemoveHoldingsOpen}
+              onConfirmRemoveFromPackage={onConfirmRemoveFromPackage}
+              toggleRemoveFromPackageModal={toggleRemoveFromPackageModal}
+              toggleRemoveHoldingsModal={toggleRemoveHoldingsModal}
+            />
+
           </Pane>
         </Paneset>
       </HasCommand>
diff --git a/src/common/components/RemoveFromPackageModals/RemoveFromPackageModals.js b/src/common/components/RemoveFromPackageModals/RemoveFromPackageModals.js
new file mode 100644
index 00000000..4871686e
--- /dev/null
+++ b/src/common/components/RemoveFromPackageModals/RemoveFromPackageModals.js
@@ -0,0 +1,49 @@
+import PropTypes from 'prop-types';
+import { FormattedMessage } from 'react-intl';
+
+import { ConfirmationModal } from '@folio/stripes/components';
+import { DeleteHoldingsModal } from '@folio/stripes-acq-components';
+
+export function RemoveFromPackageModals({
+  isRemoveFromPackageOpen,
+  isRemoveHoldingsOpen,
+  onConfirmRemoveFromPackage,
+  toggleRemoveFromPackageModal,
+  toggleRemoveHoldingsModal,
+}) {
+  return (
+    <>
+      <ConfirmationModal
+        open={isRemoveFromPackageOpen}
+        heading={<FormattedMessage id="ui-receiving.title.paneTitle.removeFromPackage" />}
+        message={<FormattedMessage id="ui-receiving.title.confirmationModal.removeFromPackage.message" />}
+        onConfirm={() => onConfirmRemoveFromPackage()}
+        onCancel={toggleRemoveFromPackageModal}
+        confirmLabel={<FormattedMessage id="ui-receiving.title.confirmationModal.removeFromPackage.confirm" />}
+      />
+
+      {isRemoveHoldingsOpen && (
+        <DeleteHoldingsModal
+          message={<FormattedMessage id="ui-receiving.title.confirmationModal.removeHolding.message" />}
+          onCancel={toggleRemoveHoldingsModal}
+          onConfirm={() => {
+            onConfirmRemoveFromPackage({ deleteHoldings: true });
+            toggleRemoveHoldingsModal();
+          }}
+          onKeepHoldings={() => {
+            onConfirmRemoveFromPackage({ deleteHoldings: false });
+            toggleRemoveHoldingsModal();
+          }}
+        />
+      )}
+    </>
+  );
+}
+
+RemoveFromPackageModals.propTypes = {
+  isRemoveFromPackageOpen: PropTypes.bool,
+  isRemoveHoldingsOpen: PropTypes.bool,
+  onConfirmRemoveFromPackage: PropTypes.func.isRequired,
+  toggleRemoveFromPackageModal: PropTypes.func.isRequired,
+  toggleRemoveHoldingsModal: PropTypes.func.isRequired,
+};
diff --git a/src/common/components/RemoveFromPackageModals/RemoveFromPackageModals.test.js b/src/common/components/RemoveFromPackageModals/RemoveFromPackageModals.test.js
new file mode 100644
index 00000000..cb343f43
--- /dev/null
+++ b/src/common/components/RemoveFromPackageModals/RemoveFromPackageModals.test.js
@@ -0,0 +1,97 @@
+import {
+  render,
+  screen,
+} from '@folio/jest-config-stripes/testing-library/react';
+import userEvent from '@folio/jest-config-stripes/testing-library/user-event';
+
+import { RemoveFromPackageModals } from './RemoveFromPackageModals';
+
+const isRemoveFromPackageOpen = false;
+const isRemoveHoldingsOpen = false;
+const onConfirmRemoveFromPackage = jest.fn();
+const toggleRemoveFromPackageModal = jest.fn();
+const toggleRemoveHoldingsModal = jest.fn();
+
+const defaultProps = {
+  isRemoveFromPackageOpen,
+  isRemoveHoldingsOpen,
+  onConfirmRemoveFromPackage,
+  toggleRemoveFromPackageModal,
+  toggleRemoveHoldingsModal,
+};
+
+describe('Render RemoveFromPackageModals', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('should render the modal', async () => {
+    render(<RemoveFromPackageModals {...defaultProps} isRemoveFromPackageOpen />);
+
+    expect(screen.getByText('ui-receiving.title.paneTitle.removeFromPackage')).toBeDefined();
+    expect(screen.getByText('ui-receiving.title.confirmationModal.removeFromPackage.message')).toBeDefined();
+    expect(screen.getByText('ui-receiving.title.confirmationModal.removeFromPackage.confirm')).toBeDefined();
+  });
+
+  it('should not render the modal', async () => {
+    render(<RemoveFromPackageModals {...defaultProps} isRemoveFromPackageOpen={false} />);
+
+    expect(screen.queryByText('ui-receiving.title.paneTitle.removeFromPackage')).toBeNull();
+    expect(screen.queryByText('ui-receiving.title.confirmationModal.removeFromPackage.message')).toBeNull();
+  });
+
+  it('should call onConfirm on click', async () => {
+    render(<RemoveFromPackageModals {...defaultProps} isRemoveFromPackageOpen />);
+
+    const button = screen.getByText('ui-receiving.title.confirmationModal.removeFromPackage.confirm');
+
+    await userEvent.click(button);
+
+    expect(onConfirmRemoveFromPackage).toHaveBeenCalledTimes(1);
+  });
+
+  it('should display second modal on confirm if displayDeleteHoldingsConfirmation is true', async () => {
+    render(<RemoveFromPackageModals {...defaultProps} isRemoveFromPackageOpen isRemoveHoldingsOpen />);
+
+    const button = screen.getByText('ui-receiving.title.confirmationModal.removeFromPackage.confirm');
+
+    await userEvent.click(button);
+
+    expect(screen.getByRole('heading', { level: 1, name: 'stripes-acq-components.holdings.deleteModal.heading' })).toBeDefined();
+    expect(screen.getByText('ui-receiving.title.confirmationModal.removeHolding.message')).toBeDefined();
+  });
+
+  it('should call keepHoldings', async () => {
+    render(<RemoveFromPackageModals {...defaultProps} isRemoveFromPackageOpen isRemoveHoldingsOpen />);
+
+    const button = screen.getByText('ui-receiving.title.confirmationModal.removeFromPackage.confirm');
+
+    await userEvent.click(button);
+
+    expect(screen.getByRole('heading', { level: 1, name: 'stripes-acq-components.holdings.deleteModal.heading' })).toBeDefined();
+    expect(screen.getByText('ui-receiving.title.confirmationModal.removeHolding.message')).toBeDefined();
+
+    const keepHoldings = screen.getByText('stripes-acq-components.holdings.deleteModal.keepHoldings');
+
+    await userEvent.click(keepHoldings);
+
+    expect(onConfirmRemoveFromPackage).toHaveBeenCalledWith({ deleteHoldings: false });
+  });
+
+  it('should call deleteHoldings', async () => {
+    render(<RemoveFromPackageModals {...defaultProps} isRemoveFromPackageOpen isRemoveHoldingsOpen />);
+
+    const button = screen.getByText('ui-receiving.title.confirmationModal.removeFromPackage.confirm');
+
+    await userEvent.click(button);
+
+    expect(screen.getByRole('heading', { level: 1, name: 'stripes-acq-components.holdings.deleteModal.heading' })).toBeDefined();
+    expect(screen.getByText('ui-receiving.title.confirmationModal.removeHolding.message')).toBeDefined();
+
+    const deleteHoldings = screen.getByRole('button', { name: 'stripes-acq-components.holdings.deleteModal.heading' });
+
+    await userEvent.click(deleteHoldings);
+
+    expect(onConfirmRemoveFromPackage).toHaveBeenCalledWith({ deleteHoldings: true });
+  });
+});
diff --git a/src/common/components/RemoveFromPackageModals/index.js b/src/common/components/RemoveFromPackageModals/index.js
new file mode 100644
index 00000000..1859f303
--- /dev/null
+++ b/src/common/components/RemoveFromPackageModals/index.js
@@ -0,0 +1 @@
+export { RemoveFromPackageModals } from './RemoveFromPackageModals';
diff --git a/src/common/components/index.js b/src/common/components/index.js
index 4ae95488..ec0916a5 100644
--- a/src/common/components/index.js
+++ b/src/common/components/index.js
@@ -5,3 +5,4 @@ export * from './FieldClaimingDate';
 export * from './LineLocationsView';
 export * from './NumberGeneratorModal';
 export * from './NumberGeneratorButton';
+export * from './RemoveFromPackageModals';
diff --git a/src/common/constants/errorCodes.js b/src/common/constants/errorCodes.js
index ddfc8640..5bc7a49c 100644
--- a/src/common/constants/errorCodes.js
+++ b/src/common/constants/errorCodes.js
@@ -2,6 +2,7 @@ export const ERROR_CODES = {
   cannotFindPiecesWithLatestStatusToProcess: 'cannotFindPiecesWithLatestStatusToProcess',
   createItemForPieceIsNotAllowedError: 'createItemForPieceIsNotAllowedError',
   createPiecePendingOrderError: 'createPiecePendingOrderError',
+  existingHoldingsForDeleteConfirmation: 'existingHoldingsForDeleteConfirmation',
   barcodeIsNotUnique: 'barcodeIsNotUnique',
   forbiddenDeleteSystemValues: 'forbiddenDeleteSystemValues',
   forbiddenDeleteUsedValue: 'forbiddenDeleteUsedValue',
diff --git a/src/common/hooks/index.js b/src/common/hooks/index.js
index 8b984843..2e97f9e2 100644
--- a/src/common/hooks/index.js
+++ b/src/common/hooks/index.js
@@ -11,6 +11,8 @@ export * from './usePiecesExpect';
 export * from './usePiecesRequests';
 export * from './useReceive';
 export * from './useReceivingTenantIdsAndLocations';
+export * from './useRemoveFromPackage';
 export * from './useTitle';
 export * from './useTitleHydratedPieces';
+export * from './useTitleMutation';
 export * from './useUnreceive';
diff --git a/src/common/hooks/useRemoveFromPackage/index.js b/src/common/hooks/useRemoveFromPackage/index.js
new file mode 100644
index 00000000..6ad15c20
--- /dev/null
+++ b/src/common/hooks/useRemoveFromPackage/index.js
@@ -0,0 +1 @@
+export { useRemoveFromPackage } from './useRemoveFromPackage';
diff --git a/src/common/hooks/useRemoveFromPackage/useRemoveFromPackage.js b/src/common/hooks/useRemoveFromPackage/useRemoveFromPackage.js
new file mode 100644
index 00000000..b582c9f3
--- /dev/null
+++ b/src/common/hooks/useRemoveFromPackage/useRemoveFromPackage.js
@@ -0,0 +1,52 @@
+import get from 'lodash/get';
+import { useCallback } from 'react';
+
+import {
+  useModalToggle,
+  useShowCallout,
+  ResponseErrorsContainer,
+} from '@folio/stripes-acq-components';
+
+import { ERROR_CODES } from '../../constants';
+import { useTitleMutation } from '../useTitleMutation';
+
+export function useRemoveFromPackage({ id, onSuccess }) {
+  const [isRemoveFromPackageOpen, toggleRemoveFromPackageModal] = useModalToggle();
+  const [isRemoveHoldingsOpen, toggleRemoveHoldingsModal] = useModalToggle();
+  const showCallout = useShowCallout();
+  const { deleteTitle } = useTitleMutation();
+
+  const onConfirmRemoveFromPackage = useCallback(async (searchParams = {}) => {
+    try {
+      await deleteTitle({ id, searchParams });
+      onSuccess();
+      showCallout({ messageId: 'ui-receiving.title.confirmationModal.removeFromPackage.success' });
+    } catch (error) {
+      const { handler } = await ResponseErrorsContainer.create(error.response);
+      const { code, message } = handler.getError();
+      const errorCode = get(ERROR_CODES, code);
+
+      toggleRemoveFromPackageModal();
+      if (errorCode === ERROR_CODES.existingHoldingsForDeleteConfirmation) {
+        toggleRemoveHoldingsModal();
+      } else {
+        showCallout({ messageId: message, type: 'error' });
+      }
+    }
+  }, [
+    deleteTitle,
+    id,
+    onSuccess,
+    showCallout,
+    toggleRemoveFromPackageModal,
+    toggleRemoveHoldingsModal,
+  ]);
+
+  return {
+    isRemoveFromPackageOpen,
+    isRemoveHoldingsOpen,
+    onConfirmRemoveFromPackage,
+    toggleRemoveFromPackageModal,
+    toggleRemoveHoldingsModal,
+  };
+}
diff --git a/src/common/hooks/useRemoveFromPackage/useRemoveFromPackage.test.js b/src/common/hooks/useRemoveFromPackage/useRemoveFromPackage.test.js
new file mode 100644
index 00000000..1c7f9730
--- /dev/null
+++ b/src/common/hooks/useRemoveFromPackage/useRemoveFromPackage.test.js
@@ -0,0 +1,112 @@
+import {
+  act,
+  renderHook,
+} from '@folio/jest-config-stripes/testing-library/react';
+import {
+  useModalToggle,
+  useShowCallout,
+  ResponseErrorsContainer,
+} from '@folio/stripes-acq-components';
+
+import { ERROR_CODES } from '../../constants';
+import { useTitleMutation } from '../useTitleMutation';
+import { useRemoveFromPackage } from './useRemoveFromPackage';
+
+jest.mock('@folio/stripes-acq-components', () => ({
+  ...jest.requireActual('@folio/stripes-acq-components'),
+  useModalToggle: jest.fn(),
+  useShowCallout: jest.fn(),
+  ResponseErrorsContainer: {
+    create: jest.fn(),
+  },
+}));
+
+jest.mock('../useTitleMutation', () => ({
+  useTitleMutation: jest.fn(),
+}));
+
+const toggleRemoveFromPackageModal = jest.fn();
+const toggleRemoveHoldingsModal = jest.fn();
+const showCallout = jest.fn();
+const deleteTitle = jest.fn();
+
+describe('useRemoveFromPackage', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+
+    useModalToggle
+      .mockReturnValueOnce([false, toggleRemoveFromPackageModal])
+      .mockReturnValueOnce([false, toggleRemoveHoldingsModal]);
+
+    useShowCallout.mockReturnValue(showCallout);
+    useTitleMutation.mockReturnValue({ deleteTitle });
+  });
+
+  it('calls deleteTitle and onSuccess on successful deletion', async () => {
+    const onSuccess = jest.fn();
+
+    deleteTitle.mockResolvedValueOnce();
+
+    const { result } = renderHook(() => useRemoveFromPackage({ id: 'title-id', onSuccess }));
+
+    await act(async () => {
+      await result.current.onConfirmRemoveFromPackage();
+    });
+
+    expect(deleteTitle).toHaveBeenCalledWith({ id: 'title-id', searchParams: {} });
+    expect(onSuccess).toHaveBeenCalled();
+    expect(showCallout).toHaveBeenCalledWith({
+      messageId: 'ui-receiving.title.confirmationModal.removeFromPackage.success',
+    });
+  });
+
+  it('opens holdings modal on existingHoldingsForDeleteConfirmation error code', async () => {
+    const onSuccess = jest.fn();
+
+    ResponseErrorsContainer.create.mockResolvedValue({
+      handler: {
+        getError: () => ({
+          code: ERROR_CODES.existingHoldingsForDeleteConfirmation,
+          message: 'some.message.code',
+        }),
+      },
+    });
+
+    deleteTitle.mockRejectedValueOnce({ response: {} });
+
+    const { result } = renderHook(() => useRemoveFromPackage({ id: 'title-id', onSuccess }));
+
+    await act(async () => {
+      await result.current.onConfirmRemoveFromPackage();
+    });
+
+    expect(toggleRemoveFromPackageModal).toHaveBeenCalled();
+    expect(toggleRemoveHoldingsModal).toHaveBeenCalled();
+  });
+
+  it('shows error callout on unknown error code', async () => {
+    const onSuccess = jest.fn();
+
+    ResponseErrorsContainer.create.mockResolvedValue({
+      handler: {
+        getError: () => ({
+          code: 'someOtherError',
+          message: 'ui-error.unknown',
+        }),
+      },
+    });
+
+    deleteTitle.mockRejectedValueOnce({ response: {} });
+
+    const { result } = renderHook(() => useRemoveFromPackage({ id: 'title-id', onSuccess }));
+
+    await act(async () => {
+      await result.current.onConfirmRemoveFromPackage();
+    });
+
+    expect(showCallout).toHaveBeenCalledWith({
+      messageId: 'ui-error.unknown',
+      type: 'error',
+    });
+  });
+});
diff --git a/src/common/hooks/useTitleMutation/index.js b/src/common/hooks/useTitleMutation/index.js
new file mode 100644
index 00000000..f47d61be
--- /dev/null
+++ b/src/common/hooks/useTitleMutation/index.js
@@ -0,0 +1 @@
+export { useTitleMutation } from './useTitleMutation';
diff --git a/src/common/hooks/useTitleMutation/useTitleMutation.js b/src/common/hooks/useTitleMutation/useTitleMutation.js
new file mode 100644
index 00000000..2022dd01
--- /dev/null
+++ b/src/common/hooks/useTitleMutation/useTitleMutation.js
@@ -0,0 +1,22 @@
+import { useMutation } from 'react-query';
+
+import { useOkapiKy } from '@folio/stripes/core';
+
+import { TITLES_API } from '../../constants';
+
+export const useTitleMutation = (options = {}) => {
+  const { tenantId } = options;
+
+  const ky = useOkapiKy({ tenant: tenantId });
+
+  const deleteTitleMutationFn = ({ id, searchParams = {} }) => {
+    return ky.delete(`${TITLES_API}/${id}`, { searchParams });
+  };
+
+  const { mutateAsync: deleteTitle, isLoading } = useMutation({ mutationFn: deleteTitleMutationFn });
+
+  return {
+    deleteTitle,
+    isLoading,
+  };
+};
diff --git a/translations/ui-receiving/en.json b/translations/ui-receiving/en.json
index 3096e0e2..c322e6c3 100644
--- a/translations/ui-receiving/en.json
+++ b/translations/ui-receiving/en.json
@@ -87,6 +87,11 @@
   "titles.search": "Search",
   "titles.orderStatus": "Order status",
   "title.paneTitle.create": "New title",
+  "title.paneTitle.removeFromPackage": "Remove from package",
+  "title.confirmationModal.removeFromPackage.message": "Removing this title from the package will delete all related pieces and any related inventory Holdings and Items.",
+  "title.confirmationModal.removeFromPackage.confirm": "Confirm",
+  "title.confirmationModal.removeFromPackage.success": "Related pieces and any related inventory Holdings and Items have been deleted.",
+  "title.confirmationModal.removeHolding.message": "Related Holdings do not reference any other pieces or items. Do you want to delete them?",
   "title.section.itemDetails": "Item details",
   "title.section.lineDetails": "PO line details",
   "titles.lineNumber": "POL number",
@@ -173,34 +178,34 @@
   "bind.pieces.modal.delete.message": "There are requests on the following pieces: <b>{barcodes}</b>. Please <b>delete requests</b> before proceeding.",
   "bind.pieces.create.success.noBarcode": "Item created successfully",
   "bind.pieces.modal.button.not.transfer": "Do not transfer",
-  "piece.accordion.itemDetails":"Item details",
-  "piece.accordion.originalItemDetails":"Item details (original)",
-  "piece.accordion.pieceDetails":"Piece details",
-  "piece.accordion.statusChangeLog":"Status log",
+  "piece.accordion.itemDetails": "Item details",
+  "piece.accordion.originalItemDetails": "Item details (original)",
+  "piece.accordion.pieceDetails": "Piece details",
+  "piece.accordion.statusChangeLog": "Status log",
   "piece.accessionNumber": "Accession number",
-  "piece.actions.addItem":"Add item",
+  "piece.actions.addItem": "Add item",
   "piece.actions.savePiece.error": "The piece was <b>not saved</b>",
   "piece.actions.savePiece.success": "The piece was successfully <b>saved</b>",
-  "piece.actions.cancel":"Cancel",
+  "piece.actions.cancel": "Cancel",
   "piece.actions.checkInItem.error": "Receiving error",
   "piece.actions.checkInItem.success": "The piece <b>{enumeration}</b> was successfully <b>received</b>",
   "piece.actions.confirm": "Continue",
-  "piece.actions.delete":"Delete",
+  "piece.actions.delete": "Delete",
   "piece.actions.delete.success": "The piece <b>{enumeration}</b> was successfully <b>deleted</b>",
   "piece.actions.delete.error": "The piece <b>{enumeration}</b> was not deleted",
   "piece.actions.delete.deleteItem": "Delete item",
   "piece.actions.delete.deleteHoldings": "Delete Holdings",
   "piece.actions.delete.deleteHoldingsAndItem": "Delete Holdings and Item",
-  "piece.actions.edit":"Edit",
-  "piece.actions.edit.keepHoldings":"Keep Holdings",
-  "piece.actions.edit.deleteHoldings.message":"This piece is connected to records in inventory. After this edit there will be no other pieces OR items connected to the related Holdings record. After making this change would you like FOLIO to delete the Holdings?",
+  "piece.actions.edit": "Edit",
+  "piece.actions.edit.keepHoldings": "Keep Holdings",
+  "piece.actions.edit.deleteHoldings.message": "This piece is connected to records in inventory. After this edit there will be no other pieces OR items connected to the related Holdings record. After making this change would you like FOLIO to delete the Holdings?",
   "piece.actions.load.error": "Failed to load pieces",
-  "piece.actions.quickReceive":"Quick receive",
-  "piece.actions.createAnother":"Create another",
+  "piece.actions.quickReceive": "Quick receive",
+  "piece.actions.createAnother": "Create another",
   "piece.actions.unreceive.confirm": "Confirm",
-  "piece.actions.unreceive.heading":"Unreceiving piece",
-  "piece.actions.unreceive.message":"Are you sure you want to unreceive piece? It will return to a status of Expected and any associated item will be set as On-Order.",
-  "piece.actions.unreceive":"Unreceive",
+  "piece.actions.unreceive.heading": "Unreceiving piece",
+  "piece.actions.unreceive.message": "Are you sure you want to unreceive piece? It will return to a status of Expected and any associated item will be set as On-Order.",
+  "piece.actions.unreceive": "Unreceive",
   "piece.actions.unreceivePiece.error": "The piece <b>{enumeration}</b> was not unreceived",
   "piece.actions.unreceivePiece.success": "The piece <b>{enumeration}</b> was successfully <b>unreceived</b>",
   "piece.actions.selectAll": "Select all pieces",
@@ -254,13 +259,13 @@
   "piece.receivingNoteModal.title": "Receiving note",
   "piece.request": "Request",
   "piece.request.isOpened": "Yes",
-  "piece.status":"Status",
-  "piece.statusChangeLog.column.date":"Date",
-  "piece.statusChangeLog.column.interval":"Interval",
-  "piece.statusChangeLog.column.status":"Status change",
-  "piece.statusChangeLog.column.updatedBy":"Updated by",
-  "piece.sendClaim.withIntegration.message":"This will generate a claim job which can be viewed in Export manager. Continue?",
-  "piece.sendClaim.withoutIntegration.message":"No claiming integration exists for this organization. Piece will be set to \"Claim sent\" status. Continue?",
+  "piece.status": "Status",
+  "piece.statusChangeLog.column.date": "Date",
+  "piece.statusChangeLog.column.interval": "Interval",
+  "piece.statusChangeLog.column.status": "Status change",
+  "piece.statusChangeLog.column.updatedBy": "Updated by",
+  "piece.sendClaim.withIntegration.message": "This will generate a claim job which can be viewed in Export manager. Continue?",
+  "piece.sendClaim.withoutIntegration.message": "No claiming integration exists for this organization. Piece will be set to \"Claim sent\" status. Continue?",
   "piece.supplement": "Supplement",
   "piece.title": "Title",
   "piece.delete.confirm": "Confirm",


Reviews:
user: login: usavkov-epam
body: It looks good, though I’ve added some comments in areas that could be improved.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T14:19:40Z
user: login: azizjonnurov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T11:38:05Z
user: login: azizjonnurov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T11:38:30Z
user: login: usavkov-epam
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T11:46:18Z
user: login: azizjonnurov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T12:40:42Z
user: login: usavkov-epam
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T12:46:20Z
user: login: Dmytro-Melnyshyn
state: APPROVED
author_association: NONE
submitted_at: 2025-04-29T06:23:54Z

Review comments:
diff_hunk: @@ -200,13 +205,20 @@ const TitleDetails = ({
     });
   }, [history, isCentralRouting, location.search, titleId]);
 
-  const goToReceiveList = useCallback(() => {
+  const goToReceiveListItem = useCallback(() => {
path: src/TitleDetails/TitleDetails.js
user: login: usavkov-epam
body: The name a bit confuses me. What is receive list item?
created_at: 2025-04-23T13:32:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -200,13 +205,20 @@ const TitleDetails = ({
     });
   }, [history, isCentralRouting, location.search, titleId]);
 
-  const goToReceiveList = useCallback(() => {
+  const goToReceiveListItem = useCallback(() => {
     history.push({
       pathname: `${isCentralRouting ? CENTRAL_RECEIVING_ROUTE : RECEIVING_ROUTE}/receive/${titleId}`,
       search: location.search,
     });
   }, [titleId, history, isCentralRouting, location.search]);
 
+  const goToReceiveList = useCallback(() => {
+    history.push({
+      pathname: `${isCentralRouting ? CENTRAL_RECEIVING_ROUTE : RECEIVING_ROUTE}/receive`,
path: src/TitleDetails/TitleDetails.js
user: login: usavkov-epam
body: What does a user expect to see when navigating that route?
created_at: 2025-04-23T13:36:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -376,19 +395,43 @@ const TitleDetails = ({
     />
   );
 
-  const lastMenu = (
-    <PaneMenu>
+  const renderActionMenu = () => (
+    <MenuSection id="receiving-actions">
path: src/TitleDetails/TitleDetails.js
user: login: usavkov-epam
body: It's more `receiving-title-actions`
created_at: 2025-04-23T13:38:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,82 @@
+import PropTypes from 'prop-types';
+import { FormattedMessage } from 'react-intl';
+
+import {
+  Modal,
+  ConfirmationModal,
+  Button,
+} from '@folio/stripes/components';
+import { ModalFooter } from '@folio/stripes-acq-components';
+
+export function RemoveFromPackageModals({
+  isRemoveFromPackageOpen,
+  isRemoveHoldingsOpen,
+  onConfirmRemoveFromPackage,
+  toggleRemoveFromPackageModal,
+  toggleRemoveHoldingsModal,
+}) {
+  const footer = (
+    <ModalFooter
+      renderStart={
+        <Button marginBottom0 onClick={toggleRemoveHoldingsModal}>
+          <FormattedMessage id="ui-receiving.title.edit.cancel" />
+        </Button>
+      }
+      renderEnd={
+        <>
+          <Button
+            buttonStyle="primary"
+            marginBottom0
+            onClick={() => {
+              onConfirmRemoveFromPackage({ deleteHoldings: false });
+              toggleRemoveHoldingsModal();
+            }}
+          >
+            <FormattedMessage id="ui-receiving.title.confirmationModal.removeHolding.keepHoldings" />
+          </Button>
+          <Button
+            buttonStyle="primary"
+            marginBottom0
+            onClick={() => {
+              onConfirmRemoveFromPackage({ deleteHoldings: true });
+              toggleRemoveHoldingsModal();
+            }}
+          >
+            <FormattedMessage id="ui-receiving.title.confirmationModal.removeHolding.deleteHoldings" />
+          </Button>
+        </>
+      }
+    />
+  );
+
+  return (
+    <>
+      <ConfirmationModal
+        open={isRemoveFromPackageOpen}
+        heading={<FormattedMessage id="ui-receiving.title.paneTitle.removeFromPackage" />}
+        message={<FormattedMessage id="ui-receiving.title.confirmationModal.removeFromPackage.message" />}
+        onConfirm={() => onConfirmRemoveFromPackage()}
+        onCancel={toggleRemoveFromPackageModal}
+        confirmLabel={<FormattedMessage id="ui-receiving.title.confirmationModal.removeFromPackage.confirm" />}
+      />
+
+      <Modal
path: src/common/components/RemoveFromPackageModals/RemoveFromPackageModals.js
user: login: usavkov-epam
body: There is `DeleteHoldingsModal` component in `stripes-acq-components`, can you reuse it?
created_at: 2025-04-23T13:56:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,22 @@
+import { useMutation } from 'react-query';
+
+import { useOkapiKy } from '@folio/stripes/core';
+
+import { TITLES_API } from '../../constants';
+
+export const useTitleMutation = (options = {}) => {
+  const { tenantId } = options;
+
+  const ky = useOkapiKy({ tenant: tenantId });
+
+  const deleteTitleMutationFn = ({ id, searchParams = {} }) => {
+    return ky.delete(`${TITLES_API}/${id}`, { searchParams });
+  };
+
+  const { mutateAsync: deleteTitle, isLoading: isDeleting } = useMutation({ mutationFn: deleteTitleMutationFn });
path: src/common/hooks/useTitleMutation/useTitleMutation.js
user: login: usavkov-epam
body: Let's keep it more consistent with other hooks, use `isLoading` as it is
created_at: 2025-04-23T13:58:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,49 @@
+import { useCallback } from 'react';
+
+import { useModalToggle, useShowCallout } from '@folio/stripes-acq-components';
+
+import { useTitleMutation } from '../useTitleMutation';
+
+const ERROR_CODES = {
+  emptyHoldings: 'existingHoldingsForDeleteConfirmation',
path: src/common/hooks/useRemoveFromPackage/useRemoveFromPackage.js
user: login: usavkov-epam
body: Please keep it near the all error code const: `src/common/constants/errorCodes.js`
created_at: 2025-04-23T13:59:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,49 @@
+import { useCallback } from 'react';
+
+import { useModalToggle, useShowCallout } from '@folio/stripes-acq-components';
path: src/common/hooks/useRemoveFromPackage/useRemoveFromPackage.js
user: login: usavkov-epam
body: If there is more than one import, please split them into separate lines.
created_at: 2025-04-23T14:00:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,49 @@
+import { useCallback } from 'react';
+
+import { useModalToggle, useShowCallout } from '@folio/stripes-acq-components';
+
+import { useTitleMutation } from '../useTitleMutation';
+
+const ERROR_CODES = {
+  emptyHoldings: 'existingHoldingsForDeleteConfirmation',
+};
+
+export function useRemoveFromPackage({ id, onSuccess }) {
+  const [isRemoveFromPackageOpen, toggleRemoveFromPackageModal] = useModalToggle();
+  const [isRemoveHoldingsOpen, toggleRemoveHoldingsModal] = useModalToggle();
+  const showCallout = useShowCallout();
+  const { deleteTitle } = useTitleMutation();
+
+  const onConfirmRemoveFromPackage = useCallback(async (searchParams = {}) => {
+    try {
+      await deleteTitle({ id, searchParams });
+      onSuccess();
+      showCallout({ messageId: 'ui-receiving.title.confirmationModal.removeFromPackage.success' });
+    } catch (error) {
+      const { errors } = await error.response.json();
+      const { message, code } = errors[0];
path: src/common/hooks/useRemoveFromPackage/useRemoveFromPackage.js
user: login: usavkov-epam
body: Could you please use `ResponseErrorsContainer` for that purpose? It's more safer and prevent exceptions if there is an error during converting a response to JSON ([ref](https://github.com/folio-org/stripes-acq-components/blob/6180443bf37966a6408e35237bab539f6a481c62/lib/utils/errorHandling/ResponseErrorsContainer.js))

```
const { handler } = await ResponseErrorsContainer.create(error?.response);
const { message, code } = handler.getError();
```

The "Strategy" pattern is particularly useful for handling error codes, and this utility was created for that purpose. You can find an example of how it works [here](https://github.com/folio-org/ui-orders/blob/f50bcd4d0b96c6ac9cfa44d71d803256e72696c8/src/components/Utils/order/showUpdateOrderError.js#L5).
created_at: 2025-04-23T14:17:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -200,13 +205,20 @@ const TitleDetails = ({
     });
   }, [history, isCentralRouting, location.search, titleId]);
 
-  const goToReceiveList = useCallback(() => {
+  const goToReceiveListItem = useCallback(() => {
     history.push({
       pathname: `${isCentralRouting ? CENTRAL_RECEIVING_ROUTE : RECEIVING_ROUTE}/receive/${titleId}`,
       search: location.search,
     });
   }, [titleId, history, isCentralRouting, location.search]);
 
+  const goToReceiveList = useCallback(() => {
+    history.push({
+      pathname: `${isCentralRouting ? CENTRAL_RECEIVING_ROUTE : RECEIVING_ROUTE}/receive`,
path: src/TitleDetails/TitleDetails.js
user: login: azizjonnurov
body: Was using it to hide detailed view, removed it and using onClose instead
created_at: 2025-04-24T11:38:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -200,13 +205,20 @@ const TitleDetails = ({
     });
   }, [history, isCentralRouting, location.search, titleId]);
 
-  const goToReceiveList = useCallback(() => {
+  const goToReceiveListItem = useCallback(() => {
path: src/TitleDetails/TitleDetails.js
user: login: azizjonnurov
body: Left it as it was
created_at: 2025-04-24T11:38:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,9 +83,37 @@ const TitleForm = ({
 
   const { isCentralRouting } = useReceivingSearchContext();
 
+  const onClose = useCallback(
path: src/TitleForm/TitleForm.js
user: login: usavkov-epam
body: Does this callback does the same thing as the `onCancel` form container?
created_at: 2025-04-28T11:46:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,9 +83,37 @@ const TitleForm = ({
 
   const { isCentralRouting } = useReceivingSearchContext();
 
+  const onClose = useCallback(
path: src/TitleForm/TitleForm.js
user: login: azizjonnurov
body: Used onCancel instead
created_at: 2025-04-28T12:40:42Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-18T12:00:09Z
body: ## Jest Unit Test Results
317 tests  +9   315 ✅ +9   3m 21s ⏱️ -6s
 99 suites +2     2 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit 7e56b038. ± Comparison against base commit 1696f30d.

[test-results]:data:application/gzip;base64,H4sIACh3D2gC/1WMyw6DIBQFf8Ww7gIqAvZnGgVuQqrS8FiZ/nsvtBW7nDk5sxNwi43k1rFLR2J2qcI4IpkcpuT8hnylZcYtlbVn8kf3mLWuamjq4Z7lcwiY3IKCHsKG4MPXhLy1ZoH/5Me0YuVTsPK5p/26uoRApB3ETHvFsC0EKM41gDKzEFQJbpiBnlGmJXm9Aazz50kGAQAA

:recycle: This comment has been updated with latest results.
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-04-28T12:41:45Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=org.folio%3Aui-receiving&pullRequest=651) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=org.folio%3Aui-receiving&pullRequest=651&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=org.folio%3Aui-receiving&pullRequest=651&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=org.folio%3Aui-receiving&pullRequest=651&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [90.9% Coverage on New Code](https://sonarcloud.io/component_measures?id=org.folio%3Aui-receiving&pullRequest=651&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=org.folio%3Aui-receiving&pullRequest=651&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=org.folio%3Aui-receiving&pullRequest=651)


author_association: NONE