Skip to content

Commit f5fd3c3

Browse files
authored
@W-18998712 Cancel order modal #2841 (#2841)
1 parent 8bd87a8 commit f5fd3c3

File tree

14 files changed

+1449
-248
lines changed

14 files changed

+1449
-248
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright (c) 2025, salesforce.com, inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: BSD-3-Clause
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
import {defineMessages} from 'react-intl'
9+
10+
// Messages for the cancel order modal
11+
export const messages = defineMessages({
12+
requestCancellation: {
13+
defaultMessage: 'Request Cancellation',
14+
id: 'cancel_order_modal.heading.request_cancellation'
15+
},
16+
selectReason: {
17+
defaultMessage: 'Select a cancellation reason',
18+
id: 'cancel_order_modal.dropdown.select_reason'
19+
},
20+
confirmationText: {
21+
defaultMessage:
22+
"We'll send you an email confirmation once your cancellation request has been processed.",
23+
id: 'cancel_order_modal.text.confirmation'
24+
},
25+
close: {
26+
defaultMessage: 'Close',
27+
id: 'cancel_order_modal.button.close'
28+
},
29+
noOrderProvided: {
30+
defaultMessage: 'No order provided',
31+
id: 'cancel_order_modal.message.no_order_provided'
32+
},
33+
// Cancellation reason labels
34+
itemPriceTooHigh: {
35+
defaultMessage: 'Item price too high',
36+
id: 'cancel_order_modal.reason.item_price_too_high'
37+
},
38+
shippingCostTooHigh: {
39+
defaultMessage: 'Shipping cost too high',
40+
id: 'cancel_order_modal.reason.shipping_cost_too_high'
41+
},
42+
itemNotArriveOnTime: {
43+
defaultMessage: 'Item(s) would not arrive on time',
44+
id: 'cancel_order_modal.reason.item_not_arrive_on_time'
45+
},
46+
orderCreatedByMistake: {
47+
defaultMessage: 'Order created by mistake',
48+
id: 'cancel_order_modal.reason.order_created_by_mistake'
49+
},
50+
changedMind: {
51+
defaultMessage: 'Changed my mind',
52+
id: 'cancel_order_modal.reason.changed_mind'
53+
},
54+
noLongerNeeded: {
55+
defaultMessage: 'No longer needed',
56+
id: 'cancel_order_modal.reason.no_longer_needed'
57+
},
58+
financialReasons: {
59+
defaultMessage: 'Financial reasons',
60+
id: 'cancel_order_modal.reason.financial_reasons'
61+
},
62+
other: {
63+
defaultMessage: 'Other',
64+
id: 'cancel_order_modal.reason.other'
65+
}
66+
})
67+
68+
export const CANCELLATION_REASONS = [
69+
{id: 'select_reason', messageKey: 'selectReason', isDefault: true}, // Default "no reason" option
70+
{id: 'item_price_too_high', messageKey: 'itemPriceTooHigh'},
71+
{id: 'shipping_cost_too_high', messageKey: 'shippingCostTooHigh'},
72+
{id: 'item_not_arrive_on_time', messageKey: 'itemNotArriveOnTime'},
73+
{id: 'order_created_by_mistake', messageKey: 'orderCreatedByMistake'},
74+
{id: 'changed_mind', messageKey: 'changedMind'},
75+
{id: 'no_longer_needed', messageKey: 'noLongerNeeded'},
76+
{id: 'financial_reasons', messageKey: 'financialReasons'},
77+
{id: 'other', messageKey: 'other'}
78+
]
Lines changed: 170 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
/*
2-
* Copyright (c) 2023, salesforce.com, inc.
2+
* Copyright (c) 2025, salesforce.com, inc.
33
* All rights reserved.
44
* SPDX-License-Identifier: BSD-3-Clause
55
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8-
import React from 'react'
8+
import React, {useState} from 'react'
99
import PropTypes from 'prop-types'
10-
import {FormattedMessage} from 'react-intl'
10+
import {FormattedMessage, useIntl} from 'react-intl'
1111
import {
1212
Modal,
1313
ModalOverlay,
@@ -17,73 +17,200 @@ import {
1717
ModalBody,
1818
ModalCloseButton,
1919
Button,
20-
Text
20+
Menu,
21+
MenuButton,
22+
MenuList,
23+
MenuItem,
24+
Box,
25+
Stack,
26+
useBreakpointValue
2127
} from '@chakra-ui/react'
28+
import {ChevronDownIcon} from '@chakra-ui/icons'
29+
import {useProducts} from '@salesforce/commerce-sdk-react'
30+
import ProductList from '@salesforce/retail-react-app/app/components/product-list'
31+
import {
32+
messages,
33+
CANCELLATION_REASONS
34+
} from '@salesforce/retail-react-app/app/components/cancel-order-modal/constants'
35+
36+
const onClient = typeof window !== 'undefined'
2237

2338
/**
24-
* A Modal for requesting order cancellation
39+
* Modal component for requesting order cancellation.
40+
* Displays order products and provides a dropdown for selecting cancellation reasons.
41+
*
42+
* @param {boolean} isOpen - Controls modal visibility
43+
* @param {Function} onClose - Callback to close the modal
44+
* @param {Object} order - Order object containing productItems and currency
45+
* @param {Function} onCancel - Callback fired when cancellation is requested (order, reason)
46+
* @returns {JSX.Element} Modal component with order content or "No order provided" message
2547
*/
26-
const CancelOrderModal = ({isOpen, onClose, order, onRequestCancellation, ...props}) => {
27-
const handleCancelOrder = () => {
28-
if (!onRequestCancellation) {
29-
console.error('Cancel order modal: onRequestCancellation is required')
30-
return
48+
const CancelOrderModal = ({isOpen, onClose, order, onCancel}) => {
49+
const intl = useIntl()
50+
const [selectedReason, setSelectedReason] = useState('')
51+
52+
// Fetch product data for order items
53+
const productIds = order?.productItems?.map((product) => product.productId) || []
54+
const {data: products, isLoading} = useProducts(
55+
{
56+
parameters: {
57+
ids: productIds.join(','),
58+
allImages: true
59+
}
60+
},
61+
{
62+
enabled: !!productIds.length && onClient,
63+
select: (result) => {
64+
return result?.data?.reduce((result, item) => {
65+
const key = item.id
66+
result[key] = item
67+
return result
68+
}, {})
69+
}
70+
}
71+
)
72+
73+
// Merge product data with order items
74+
const variants =
75+
order?.productItems?.map((item) => {
76+
const product = products?.[item.productId]
77+
return {
78+
...(product ? product : {}),
79+
isProductUnavailable: !product,
80+
...item
81+
}
82+
}) || []
83+
84+
// For responsive sizing
85+
const modalSize = useBreakpointValue({base: 'full', md: '2xl'})
86+
const buttonSize = useBreakpointValue({base: 'lg', md: 'md'})
87+
88+
const cancellationReasons = CANCELLATION_REASONS.map((reason) => ({
89+
id: reason.id,
90+
label: intl.formatMessage(messages[reason.messageKey])
91+
}))
92+
93+
const getCancellationReasonDisplayText = () => {
94+
if (selectedReason) {
95+
return cancellationReasons.find((reason) => reason.id === selectedReason)?.label
3196
}
97+
return intl.formatMessage(messages.selectReason)
98+
}
3299

33-
onRequestCancellation(order)
100+
const handleCancel = () => {
101+
onCancel(order, selectedReason)
34102
onClose()
35103
}
36104

37105
return (
38-
<Modal isOpen={isOpen} onClose={onClose} isCentered {...props}>
106+
<Modal
107+
isOpen={isOpen}
108+
onClose={onClose}
109+
size={modalSize}
110+
isCentered
111+
scrollBehavior="inside"
112+
>
39113
<ModalOverlay />
40114
<ModalContent>
41115
<ModalHeader>
42-
<FormattedMessage
43-
defaultMessage="Request Cancellation"
44-
id="cancel_order_modal.title.request_cancellation"
45-
/>
116+
<FormattedMessage {...messages.requestCancellation} />
46117
</ModalHeader>
47118
<ModalCloseButton />
48-
<ModalBody>
49-
{/* TODO: Add order details here W-18998712 */}
50-
<Text>
51-
<FormattedMessage
52-
defaultMessage="This is a blank modal for canceling the order."
53-
id="cancel_order_modal.content.placeholder"
54-
/>
55-
</Text>
119+
<ModalBody pb={6}>
120+
{order ? (
121+
!isLoading ? (
122+
<ProductList
123+
variants={variants}
124+
currency={order.currency}
125+
imageWidth="20"
126+
padding={4}
127+
spacing={2}
128+
/>
129+
) : (
130+
<Box textAlign="center" color="gray.500" fontSize="md" py={8}>
131+
Loading products...
132+
</Box>
133+
)
134+
) : (
135+
<Box textAlign="center" color="gray.500" fontSize="md" py={8}>
136+
<FormattedMessage {...messages.noOrderProvided} />
137+
</Box>
138+
)}
56139
</ModalBody>
140+
57141
<ModalFooter>
58-
<Button variant="solid" onClick={handleCancelOrder}>
59-
<FormattedMessage
60-
defaultMessage="Request Cancellation"
61-
id="cancel_order_modal.button.confirm"
62-
/>
63-
</Button>
142+
<Stack
143+
direction={['column', 'row']}
144+
spacing={4}
145+
w="full"
146+
justify="space-between"
147+
align="center"
148+
>
149+
{/* Cancellation Reason Dropdown */}
150+
<Box w="full" flex={1}>
151+
<Menu>
152+
<MenuButton
153+
as={Button}
154+
rightIcon={<ChevronDownIcon />}
155+
variant="outline"
156+
size={buttonSize}
157+
w="full"
158+
textAlign="left"
159+
justifyContent="space-between"
160+
fontWeight="normal"
161+
color={selectedReason ? 'black' : 'gray.500'}
162+
isDisabled={!order}
163+
>
164+
{getCancellationReasonDisplayText()}
165+
</MenuButton>
166+
<MenuList maxH="72" overflowY="auto">
167+
{cancellationReasons.map((reason) => (
168+
<MenuItem
169+
key={reason.id}
170+
onClick={() => setSelectedReason(reason.id)}
171+
bg={
172+
selectedReason === reason.id ? 'blue.50' : undefined
173+
}
174+
color={
175+
selectedReason === reason.id
176+
? 'blue.600'
177+
: reason.isDefault
178+
? 'gray.500'
179+
: undefined
180+
}
181+
fontWeight={
182+
selectedReason === reason.id ? 'medium' : undefined
183+
}
184+
>
185+
{reason.label}
186+
</MenuItem>
187+
))}
188+
</MenuList>
189+
</Menu>
190+
</Box>
191+
192+
{/* Request Cancellation Button */}
193+
<Button
194+
colorScheme="blue"
195+
onClick={handleCancel}
196+
size={buttonSize}
197+
w={['full', 'auto']}
198+
isDisabled={!order}
199+
>
200+
<FormattedMessage {...messages.requestCancellation} />
201+
</Button>
202+
</Stack>
64203
</ModalFooter>
65204
</ModalContent>
66205
</Modal>
67206
)
68207
}
69208

70209
CancelOrderModal.propTypes = {
71-
/**
72-
* Whether the modal is open
73-
*/
74210
isOpen: PropTypes.bool.isRequired,
75-
/**
76-
* Callback to close the modal
77-
*/
78211
onClose: PropTypes.func.isRequired,
79-
/**
80-
* Order object for cancellation
81-
*/
82212
order: PropTypes.object.isRequired,
83-
/**
84-
* Callback when user confirms cancellation request
85-
*/
86-
onRequestCancellation: PropTypes.func.isRequired
213+
onCancel: PropTypes.func.isRequired
87214
}
88215

89216
export default CancelOrderModal

0 commit comments

Comments
 (0)