-
Notifications
You must be signed in to change notification settings - Fork 212
[OMS] Add cancel button and modal @W-18998059 #2775
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f72ed98
2b1c0c8
fcf9398
492be37
ce28431
c5bc7a2
0f886ec
e3c0c81
cf7b4db
0fcfed7
5dc7a73
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| /* | ||
| * Copyright (c) 2023, salesforce.com, inc. | ||
| * All rights reserved. | ||
| * SPDX-License-Identifier: BSD-3-Clause | ||
| * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
| */ | ||
|
|
||
| import React from 'react' | ||
| import PropTypes from 'prop-types' | ||
| import {FormattedMessage} from 'react-intl' | ||
| import { | ||
| Modal, | ||
| ModalOverlay, | ||
| ModalContent, | ||
| ModalHeader, | ||
| ModalFooter, | ||
| ModalBody, | ||
| ModalCloseButton, | ||
| Button, | ||
| Text | ||
| } from '@chakra-ui/react' | ||
|
|
||
| /** | ||
| * A Modal for requesting order cancellation | ||
| */ | ||
| const CancelOrderModal = ({isOpen, onClose, order, onRequestCancellation, ...props}) => { | ||
| const handleCancelOrder = () => { | ||
| if (!onRequestCancellation) { | ||
| console.error('Cancel order modal: onRequestCancellation is required') | ||
| return | ||
| } | ||
|
|
||
| onRequestCancellation(order) | ||
| onClose() | ||
| } | ||
|
|
||
| return ( | ||
| <Modal isOpen={isOpen} onClose={onClose} isCentered {...props}> | ||
| <ModalOverlay /> | ||
| <ModalContent> | ||
| <ModalHeader> | ||
| <FormattedMessage | ||
| defaultMessage="Request Cancellation" | ||
| id="cancel_order_modal.title.request_cancellation" | ||
| /> | ||
| </ModalHeader> | ||
| <ModalCloseButton /> | ||
| <ModalBody> | ||
| {/* TODO: Add order details here W-18998712 */} | ||
| <Text> | ||
| <FormattedMessage | ||
| defaultMessage="This is a blank modal for canceling the order." | ||
| id="cancel_order_modal.content.placeholder" | ||
| /> | ||
| </Text> | ||
| </ModalBody> | ||
| <ModalFooter> | ||
| <Button variant="solid" onClick={handleCancelOrder}> | ||
| <FormattedMessage | ||
| defaultMessage="Request Cancellation" | ||
| id="cancel_order_modal.button.confirm" | ||
| /> | ||
| </Button> | ||
| </ModalFooter> | ||
| </ModalContent> | ||
| </Modal> | ||
| ) | ||
| } | ||
|
|
||
| CancelOrderModal.propTypes = { | ||
| /** | ||
| * Whether the modal is open | ||
| */ | ||
| isOpen: PropTypes.bool.isRequired, | ||
| /** | ||
| * Callback to close the modal | ||
| */ | ||
| onClose: PropTypes.func.isRequired, | ||
| /** | ||
| * Order object for cancellation | ||
| */ | ||
| order: PropTypes.object.isRequired, | ||
| /** | ||
| * Callback when user confirms cancellation request | ||
| */ | ||
| onRequestCancellation: PropTypes.func.isRequired | ||
| } | ||
|
|
||
| export default CancelOrderModal | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,221 @@ | ||
| /* | ||
| * Copyright (c) 2023, salesforce.com, inc. | ||
| * All rights reserved. | ||
| * SPDX-License-Identifier: BSD-3-Clause | ||
| * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
| */ | ||
| import React from 'react' | ||
| import CancelOrderModal from '@salesforce/retail-react-app/app/components/cancel-order-modal/index' | ||
| import {Box, useDisclosure} from '@salesforce/retail-react-app/app/components/shared/ui' | ||
| import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils' | ||
| import userEvent from '@testing-library/user-event' | ||
| import {screen} from '@testing-library/react' | ||
|
|
||
| const mockOrder = { | ||
| orderNo: '00028011', | ||
| status: 'completed', | ||
| creationDate: '2023-02-15T10:15:00.000Z', | ||
| currency: 'USD', | ||
| productItems: [ | ||
| { | ||
| productId: 'test-product-1', | ||
| quantity: 2, | ||
| name: 'Test Product 1' | ||
| } | ||
| ] | ||
| } | ||
|
|
||
| const MockedComponent = (props) => { | ||
| const modalProps = useDisclosure() | ||
|
|
||
| return ( | ||
| <Box> | ||
| <button onClick={modalProps.onOpen}>Open Cancel Modal</button> | ||
| <CancelOrderModal {...modalProps} {...props} /> | ||
| </Box> | ||
| ) | ||
| } | ||
|
|
||
| afterEach(() => { | ||
| jest.resetModules() | ||
| }) | ||
|
|
||
| test('renders cancel order modal when open', async () => { | ||
| renderWithProviders(<CancelOrderModal isOpen={true} onClose={jest.fn()} order={mockOrder} />) | ||
| expect(screen.getByRole('dialog')).toBeInTheDocument() | ||
| expect(screen.getAllByText(/request cancellation/i)).toHaveLength(2) | ||
| }) | ||
|
|
||
| test('does not render modal when closed', () => { | ||
| renderWithProviders(<MockedComponent order={mockOrder} />) | ||
| expect(screen.queryByRole('dialog')).not.toBeInTheDocument() | ||
| expect(screen.queryByText(/request cancellation/i)).not.toBeInTheDocument() | ||
| }) | ||
|
|
||
| test('renders modal with correct header text', async () => { | ||
| renderWithProviders(<CancelOrderModal isOpen={true} onClose={jest.fn()} order={mockOrder} />) | ||
| const dialog = screen.getByRole('dialog') | ||
| expect(dialog).toBeInTheDocument() | ||
| expect(screen.getAllByText(/request cancellation/i)).toHaveLength(2) | ||
| }) | ||
|
|
||
| test('renders modal with correct body content', async () => { | ||
| const user = userEvent.setup() | ||
| renderWithProviders(<MockedComponent order={mockOrder} />) | ||
|
|
||
| // Open the modal | ||
| const trigger = screen.getByText(/open cancel modal/i) | ||
| await user.click(trigger) | ||
|
|
||
| // Check body content | ||
| expect(screen.getByText(/this is a blank modal for canceling the order/i)).toBeInTheDocument() | ||
| }) | ||
|
|
||
| test('renders request cancellation button', async () => { | ||
| const user = userEvent.setup() | ||
| renderWithProviders(<MockedComponent order={mockOrder} />) | ||
|
|
||
| // Open the modal | ||
| const trigger = screen.getByText(/open cancel modal/i) | ||
| await user.click(trigger) | ||
|
|
||
| // Check for request cancellation button | ||
| const requestButton = screen.getByRole('button', {name: /request cancellation/i}) | ||
| expect(requestButton).toBeInTheDocument() | ||
| }) | ||
|
|
||
| test('renders close button (X)', async () => { | ||
| const user = userEvent.setup() | ||
| renderWithProviders(<MockedComponent order={mockOrder} />) | ||
|
|
||
| // Open the modal | ||
| const trigger = screen.getByText(/open cancel modal/i) | ||
| await user.click(trigger) | ||
|
|
||
| // Check for close button | ||
| const closeButton = screen.getByRole('button', {name: /close/i}) | ||
| expect(closeButton).toBeInTheDocument() | ||
| }) | ||
|
|
||
| test('calls onClose when close button is clicked', async () => { | ||
| const user = userEvent.setup() | ||
| const onClose = jest.fn() | ||
| renderWithProviders(<MockedComponent order={mockOrder} onClose={onClose} />) | ||
|
|
||
| // Open the modal | ||
| renderWithProviders(<CancelOrderModal isOpen={true} onClose={onClose} order={mockOrder} />) | ||
|
|
||
| // Click close button | ||
| const closeButton = screen.getByRole('button', {name: /close/i}) | ||
| await user.click(closeButton) | ||
|
|
||
| expect(onClose).toHaveBeenCalledTimes(1) | ||
| }) | ||
|
|
||
| test('calls onRequestCancellation when request cancellation button is clicked', async () => { | ||
| const user = userEvent.setup() | ||
| const onRequestCancellation = jest.fn() | ||
| const onClose = jest.fn() | ||
|
|
||
| renderWithProviders( | ||
| <CancelOrderModal | ||
| isOpen={true} | ||
| onClose={onClose} | ||
| order={mockOrder} | ||
| onRequestCancellation={onRequestCancellation} | ||
| /> | ||
| ) | ||
|
|
||
| // Click request cancellation button | ||
| const requestButton = screen.getByRole('button', {name: /request cancellation/i}) | ||
| await user.click(requestButton) | ||
|
|
||
| expect(onRequestCancellation).toHaveBeenCalledTimes(1) | ||
| expect(onRequestCancellation).toHaveBeenCalledWith(mockOrder) | ||
| }) | ||
|
|
||
| test('calls onClose when request cancellation button is clicked', async () => { | ||
| const user = userEvent.setup() | ||
| const onRequestCancellation = jest.fn() | ||
| const onClose = jest.fn() | ||
|
|
||
| renderWithProviders( | ||
| <CancelOrderModal | ||
| isOpen={true} | ||
| onClose={onClose} | ||
| order={mockOrder} | ||
| onRequestCancellation={onRequestCancellation} | ||
| /> | ||
| ) | ||
|
|
||
| // Click request cancellation button | ||
| const requestButton = screen.getByRole('button', {name: /request cancellation/i}) | ||
| await user.click(requestButton) | ||
|
|
||
| expect(onClose).toHaveBeenCalledTimes(1) | ||
| }) | ||
|
|
||
| test('component works correctly with all required props provided', async () => { | ||
| const onRequestCancellation = jest.fn() | ||
| const onClose = jest.fn() | ||
|
|
||
| renderWithProviders( | ||
| <CancelOrderModal | ||
| isOpen={true} | ||
| onClose={onClose} | ||
| order={mockOrder} | ||
| onRequestCancellation={onRequestCancellation} | ||
| /> | ||
| ) | ||
|
|
||
| expect(screen.getByRole('dialog')).toBeInTheDocument() | ||
| expect(screen.getByRole('button', {name: /request cancellation/i})).toBeInTheDocument() | ||
| }) | ||
|
|
||
| test('onRequestCancellation is called with correct order parameter', async () => { | ||
| const user = userEvent.setup() | ||
| const onRequestCancellation = jest.fn() | ||
| const onClose = jest.fn() | ||
|
|
||
| renderWithProviders( | ||
| <CancelOrderModal | ||
| isOpen={true} | ||
| onClose={onClose} | ||
| order={mockOrder} | ||
| onRequestCancellation={onRequestCancellation} | ||
| /> | ||
| ) | ||
|
|
||
| const requestButton = screen.getByRole('button', {name: /request cancellation/i}) | ||
| await user.click(requestButton) | ||
|
|
||
| // Verify the callback is called with the exact order object | ||
| expect(onRequestCancellation).toHaveBeenCalledWith(mockOrder) | ||
| expect(onRequestCancellation).toHaveBeenCalledTimes(1) | ||
| }) | ||
|
|
||
| test('both onRequestCancellation and onClose are called in correct order', async () => { | ||
| const user = userEvent.setup() | ||
| const onRequestCancellation = jest.fn() | ||
| const onClose = jest.fn() | ||
| const callOrder = [] | ||
|
|
||
| // Track call order | ||
| onRequestCancellation.mockImplementation(() => callOrder.push('onRequestCancellation')) | ||
| onClose.mockImplementation(() => callOrder.push('onClose')) | ||
|
|
||
| renderWithProviders( | ||
| <CancelOrderModal | ||
| isOpen={true} | ||
| onClose={onClose} | ||
| order={mockOrder} | ||
| onRequestCancellation={onRequestCancellation} | ||
| /> | ||
| ) | ||
|
|
||
| const requestButton = screen.getByRole('button', {name: /request cancellation/i}) | ||
| await user.click(requestButton) | ||
|
|
||
| // Verify both functions are called and in the correct order | ||
| expect(callOrder).toEqual(['onRequestCancellation', 'onClose']) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,8 +19,9 @@ import { | |
| Divider, | ||
| Grid, | ||
| SimpleGrid, | ||
| Skeleton | ||
| } from '@salesforce/retail-react-app/app/components/shared/ui' | ||
| Skeleton, | ||
| useDisclosure | ||
| } from '@chakra-ui/react' | ||
| import {getCreditCardIcon} from '@salesforce/retail-react-app/app/utils/cc-utils' | ||
| import {useOrder, useProducts} from '@salesforce/commerce-sdk-react' | ||
| import Link from '@salesforce/retail-react-app/app/components/link' | ||
|
|
@@ -31,6 +32,7 @@ import CartItemVariantImage from '@salesforce/retail-react-app/app/components/it | |
| import CartItemVariantName from '@salesforce/retail-react-app/app/components/item-variant/item-name' | ||
| import CartItemVariantAttributes from '@salesforce/retail-react-app/app/components/item-variant/item-attributes' | ||
| import CartItemVariantPrice from '@salesforce/retail-react-app/app/components/item-variant/item-price' | ||
| import CancelOrderModal from '@salesforce/retail-react-app/app/components/cancel-order-modal' | ||
| import PropTypes from 'prop-types' | ||
| const onClient = typeof window !== 'undefined' | ||
|
|
||
|
|
@@ -110,6 +112,12 @@ const AccountOrderDetail = () => { | |
| const history = useHistory() | ||
| const {formatMessage, formatDate} = useIntl() | ||
|
|
||
| const { | ||
| isOpen: isCancelModalOpen, | ||
| onOpen: onCancelModalOpen, | ||
| onClose: onCancelModalClose | ||
| } = useDisclosure() | ||
|
|
||
| const {data: order, isLoading: isOrderLoading} = useOrder( | ||
| { | ||
| parameters: {orderNo: params.orderNo} | ||
|
|
@@ -156,12 +164,21 @@ const AccountOrderDetail = () => { | |
| </Box> | ||
|
|
||
| <Stack spacing={[1, 2]}> | ||
| <Heading as="h1" fontSize={['lg', '2xl']} tabIndex="0" ref={headingRef}> | ||
| <FormattedMessage | ||
| defaultMessage="Order Details" | ||
| id="account_order_detail.title.order_details" | ||
| /> | ||
| </Heading> | ||
| <Flex justify="space-between" align="center"> | ||
| <Heading as="h1" fontSize={['lg', '2xl']} tabIndex="0" ref={headingRef}> | ||
| <FormattedMessage | ||
| defaultMessage="Order Details" | ||
| id="account_order_detail.title.order_details" | ||
| /> | ||
| </Heading> | ||
| {/* TODO: addcancel order elligibility logic */} | ||
| <Button variant="link" size="sm" onClick={onCancelModalOpen}> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not all orders are cancellable, right? How do we make sure this doesn't show up at all times?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I haven't added that logic bit yet. I recently learned that SOM has a field called QuantityAvailableToCancel on each orderItemSummary. I will incorporate that in the next iteration, and add a todo comment to address that here if that works?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Other than quantity (why would quantity matter when canceling an order?), I'd imagine the order status also matters? I am good with doing it in the next iteration. Thanks.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question, I'll follow up on that |
||
| <FormattedMessage | ||
| defaultMessage="Cancel order" | ||
| id="account_order_detail.button.cancel_order" | ||
| /> | ||
| </Button> | ||
| </Flex> | ||
|
|
||
| {!isLoading ? ( | ||
| <Stack | ||
|
|
@@ -398,6 +415,16 @@ const AccountOrderDetail = () => { | |
| )} | ||
| </Stack> | ||
| </Stack> | ||
|
|
||
| <CancelOrderModal | ||
| isOpen={isCancelModalOpen} | ||
| onClose={onCancelModalClose} | ||
| order={order} | ||
| onRequestCancellation={(order) => { | ||
| // TODO: Add cancellation logic here | ||
| console.log('Requesting cancellation for order:', order?.orderNo) | ||
| }} | ||
| /> | ||
| </Stack> | ||
| ) | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can safely remove this check now that it's marked as isRequired.