-
Notifications
You must be signed in to change notification settings - Fork 212
@W-18998088 Order tracker on order details page #2790
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 8 commits
2b1c0c8
7fcb156
f3f2032
3802d26
85e18ca
dce94e6
3f5686e
e311606
8a00b21
40b845d
ffa3dba
4d4b82a
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,169 @@ | ||
| /* | ||
| * Copyright (c) 2025, Salesforce, 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 {Box, useTheme, Text} from '@chakra-ui/react' | ||
| import PropTypes from 'prop-types' | ||
| import {useIntl} from 'react-intl' | ||
|
|
||
| const steps = ['Ordered', 'Dispatched', 'Out for delivery', 'Delivered'] | ||
|
|
||
| const OrderStatusBar = ({currentStepLabel}) => { | ||
| const theme = useTheme() | ||
| const intl = useIntl() | ||
|
|
||
| const getLocalizedMessage = (status) => { | ||
|
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. @sf-madhuri-uppu Why are we hard coding localized message for every step. This adds maintenance on our part. Is there any way to take the list of steps/texts from backend and translate the array on UI instead of each step ?
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. React Intl requires message IDs to be statically evaluable for extraction. Dynamic key generation like status_bar.${status.toLowerCase().replace(/\s+/g, '_')} is not static. I tried using a for loop earlier and it did not work so I had to use a switch case. The message itself is not hardcoded for every locale. We are only giving the step label as default messages. Those labels will be translated based on locale by React.intl |
||
| switch (status) { | ||
| case 'Ordered': | ||
| return intl.formatMessage({id: 'status_bar.ordered', defaultMessage: 'Ordered'}) | ||
| case 'Dispatched': | ||
| return intl.formatMessage({ | ||
| id: 'status_bar.dispatched', | ||
| defaultMessage: 'Dispatched' | ||
| }) | ||
| case 'Out for delivery': | ||
| return intl.formatMessage({ | ||
| id: 'status_bar.out_for_delivery', | ||
| defaultMessage: 'Out for delivery' | ||
| }) | ||
| case 'Delivered': | ||
| return intl.formatMessage({id: 'status_bar.delivered', defaultMessage: 'Delivered'}) | ||
| default: | ||
| return status | ||
| } | ||
| } | ||
|
|
||
| const n = steps.length | ||
sf-madhuri-uppu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| const svgWidth = 1080 | ||
| const svgHeight = 50 | ||
| const chevronWidth = 24 | ||
| const radius = 25 | ||
|
|
||
| // Dynamically calculate step width so all steps + chevrons fit in svgWidth | ||
| const stepWidth = (svgWidth + (n - 1) * chevronWidth) / n | ||
sf-madhuri-uppu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| let currentStep = steps.findIndex((step) => step === currentStepLabel) | ||
|
|
||
| if (currentStep === -1) currentStep = 0 | ||
|
|
||
| // Helper to get the x offset for each step (overlap chevrons) | ||
| const getStepOffset = (i) => i * (stepWidth - chevronWidth) | ||
|
|
||
| // Generate polygons/paths for each step | ||
| const renderStepShapes = () => { | ||
| const shapes = [] | ||
| for (let i = 0; i < n; i++) { | ||
sf-madhuri-uppu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| const x = getStepOffset(i) | ||
| let path, stepFill | ||
| if (i === 0) { | ||
| // First: rounded left, chevron right (chevron tip overlaps next step) | ||
| path = `M ${x + radius},0 | ||
| L ${x + stepWidth - chevronWidth},0 | ||
| L ${x + stepWidth},${svgHeight / 2} | ||
| L ${x + stepWidth - chevronWidth},${svgHeight} | ||
| L ${x + radius},${svgHeight} | ||
| A ${radius},${radius} 0 0 1 ${x},${svgHeight / 2} | ||
| A ${radius},${radius} 0 0 1 ${x + radius},0 | ||
| Z` | ||
| } else if (i === n - 1) { | ||
| // Last: chevron left, rounded right (no chevron tip on right) | ||
| path = `M ${x},0 | ||
| L ${x + stepWidth - radius},0 | ||
| A ${radius},${radius} 0 0 1 ${x + stepWidth},${svgHeight / 2} | ||
| A ${radius},${radius} 0 0 1 ${x + stepWidth - radius},${svgHeight} | ||
| L ${x},${svgHeight} | ||
| L ${x + chevronWidth},${svgHeight / 2} | ||
| Z` | ||
| } else { | ||
| // Middle: chevron left, chevron right (chevron tip overlaps next step) | ||
| path = `M ${x},0 | ||
| L ${x + stepWidth - chevronWidth},0 | ||
| L ${x + stepWidth},${svgHeight / 2} | ||
| L ${x + stepWidth - chevronWidth},${svgHeight} | ||
| L ${x},${svgHeight} | ||
| L ${x + chevronWidth},${svgHeight / 2} | ||
| Z` | ||
| } | ||
| // Color logic: completed steps = light teal, current step = dark blue, future steps = gray | ||
| if (i < currentStep) { | ||
| stepFill = theme.colors.teal[100] | ||
| } else if (i === currentStep) { | ||
| stepFill = theme.colors.blue[900] | ||
| } else { | ||
| stepFill = theme.colors.gray[200] | ||
| } | ||
| shapes.push(<path key={i} d={path} fill={stepFill} stroke="white" strokeWidth="2" />) | ||
| } | ||
| return shapes | ||
sf-madhuri-uppu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // Overlay text for each step (shift overlays for overlap) | ||
| const renderStepLabels = () => { | ||
| const labels = [] | ||
| for (let i = 0; i < n; i++) { | ||
| const x = getStepOffset(i) | ||
| // Text color logic: completed steps = dark text, current step = white, future steps = dark text | ||
| let labelColor | ||
| if (i < currentStep) { | ||
| labelColor = theme.colors.black[600] | ||
| } else if (i === currentStep) { | ||
| labelColor = 'white' | ||
| } else { | ||
| labelColor = theme.colors.black[600] | ||
| } | ||
| labels.push( | ||
| <Box | ||
| key={i} | ||
| position="absolute" | ||
| top={0} | ||
| left={`calc(${(x / svgWidth) * 100}% )`} | ||
| width={`calc(${(stepWidth / svgWidth) * 100}% )`} | ||
| height="100%" | ||
| display="flex" | ||
| alignItems="center" | ||
| justifyContent="center" | ||
| pointerEvents="none" | ||
| px={[1, 2]} // Add horizontal padding for text | ||
| > | ||
| <Text | ||
| color={labelColor} | ||
| fontWeight="medium" | ||
| fontSize={['xs', 'sm', 'md', 'lg']} | ||
| textAlign="center" | ||
| lineHeight="1.2" | ||
| wordBreak="break-word" | ||
| hyphens="auto" | ||
| maxW="100%" | ||
| > | ||
| {getLocalizedMessage(steps[i])} | ||
| </Text> | ||
| </Box> | ||
| ) | ||
| } | ||
| return labels | ||
| } | ||
|
|
||
| return ( | ||
| <Box position="relative" width="100%" maxWidth="1080px" height="50px"> | ||
| <svg | ||
|
||
| width="100%" | ||
| height="auto" | ||
| viewBox={`0 0 ${svgWidth} ${svgHeight}`} | ||
| style={{display: 'block'}} | ||
| preserveAspectRatio="none" | ||
| > | ||
| {renderStepShapes()} | ||
| </svg> | ||
| {renderStepLabels()} | ||
| </Box> | ||
| ) | ||
| } | ||
|
|
||
| OrderStatusBar.propTypes = { | ||
| currentStepLabel: PropTypes.string | ||
| } | ||
|
|
||
| export default OrderStatusBar | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| /* | ||
| * Copyright (c) 2025, Salesforce, 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 {screen} from '@testing-library/react' | ||
| import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils' | ||
| import OrderStatusBar from '.' | ||
|
|
||
| describe('OrderStatusBar', () => { | ||
| beforeEach(() => jest.clearAllMocks()) | ||
|
|
||
| test('renders all step labels with localized messages', () => { | ||
| renderWithProviders(<OrderStatusBar />) | ||
|
|
||
| expect(screen.getByText('Ordered')).toBeInTheDocument() | ||
| expect(screen.getByText('Dispatched')).toBeInTheDocument() | ||
| expect(screen.getByText('Out for delivery')).toBeInTheDocument() | ||
| expect(screen.getByText('Delivered')).toBeInTheDocument() | ||
| }) | ||
|
|
||
| test('renders step labels with responsive font sizes and word wrapping', () => { | ||
| renderWithProviders(<OrderStatusBar />) | ||
|
|
||
| const labels = screen.getAllByText(/Ordered|Dispatched|Out for delivery|Delivered/) | ||
| labels.forEach((label) => { | ||
| // Check that the component renders without errors | ||
| expect(label).toBeInTheDocument() | ||
| }) | ||
| }) | ||
|
|
||
| test('renders SVG element with correct attributes', () => { | ||
| renderWithProviders(<OrderStatusBar />) | ||
|
|
||
| const svg = document.querySelector('svg') | ||
| expect(svg).toBeInTheDocument() | ||
| expect(svg).toHaveAttribute('viewBox', '0 0 1080 50') | ||
| expect(svg).toHaveAttribute('width', '100%') | ||
| expect(svg).toHaveAttribute('preserveAspectRatio', 'none') | ||
| expect(svg).toHaveStyle({display: 'block'}) | ||
| }) | ||
|
|
||
| test('renders all step paths in SVG with correct count', () => { | ||
| renderWithProviders(<OrderStatusBar />) | ||
|
|
||
| const svg = document.querySelector('svg') | ||
| const paths = svg.querySelectorAll('path') | ||
| expect(paths).toHaveLength(4) // Should have 4 paths for 4 steps | ||
| }) | ||
|
|
||
| test('renders with different currentStepLabel props and updates colors accordingly', () => { | ||
| const {rerender} = renderWithProviders(<OrderStatusBar currentStepLabel="Ordered" />) | ||
| expect(screen.getByText('Ordered')).toBeInTheDocument() | ||
|
|
||
| rerender(<OrderStatusBar currentStepLabel="Dispatched" />) | ||
| expect(screen.getByText('Dispatched')).toBeInTheDocument() | ||
|
|
||
| rerender(<OrderStatusBar currentStepLabel="Delivered" />) | ||
| expect(screen.getByText('Delivered')).toBeInTheDocument() | ||
|
|
||
| rerender(<OrderStatusBar currentStepLabel="Invalid Step" />) | ||
| expect(screen.getByText('Ordered')).toBeInTheDocument() | ||
|
|
||
| rerender(<OrderStatusBar currentStepLabel="" />) | ||
| expect(screen.getByText('Ordered')).toBeInTheDocument() | ||
|
|
||
| rerender(<OrderStatusBar currentStepLabel={undefined} />) | ||
| expect(screen.getByText('Ordered')).toBeInTheDocument() | ||
|
|
||
| rerender(<OrderStatusBar currentStepLabel={['Ordered', 'Dispatched']} />) | ||
| expect(screen.getByText('Ordered')).toBeInTheDocument() | ||
| }) | ||
|
|
||
| test('renders container with correct positioning and dimensions', () => { | ||
| renderWithProviders(<OrderStatusBar />) | ||
|
|
||
| // The container is a Chakra UI Box component, so we check for the SVG instead | ||
| const svg = document.querySelector('svg') | ||
| expect(svg).toBeInTheDocument() | ||
| expect(svg).toHaveAttribute('width', '100%') | ||
| expect(svg).toHaveAttribute('viewBox', '0 0 1080 50') | ||
| }) | ||
|
|
||
| test('renders text labels with correct styling properties', () => { | ||
| renderWithProviders(<OrderStatusBar />) | ||
|
|
||
| const labels = screen.getAllByText(/Ordered|Dispatched|Out for delivery|Delivered/) | ||
| labels.forEach((label) => { | ||
| // Check that labels are rendered | ||
| expect(label).toBeInTheDocument() | ||
| }) | ||
| }) | ||
|
|
||
| test('renders step labels with proper positioning', () => { | ||
| renderWithProviders(<OrderStatusBar />) | ||
|
|
||
| // Check that all step labels are rendered | ||
| const labels = screen.getAllByText(/Ordered|Dispatched|Out for delivery|Delivered/) | ||
| expect(labels).toHaveLength(4) | ||
|
|
||
| // Check that each label is in the document | ||
| labels.forEach((label) => { | ||
| expect(label).toBeInTheDocument() | ||
| }) | ||
| }) | ||
|
|
||
| test('handles case-sensitive step label matching', () => { | ||
| // Test case-insensitive matching | ||
| const {rerender} = renderWithProviders(<OrderStatusBar currentStepLabel="ordered" />) | ||
| expect(screen.getByText('Ordered')).toBeInTheDocument() | ||
|
|
||
| // Test with different case | ||
| rerender(<OrderStatusBar currentStepLabel="DISPATCHED" />) | ||
| expect(screen.getByText('Dispatched')).toBeInTheDocument() | ||
|
|
||
| // Test with mixed case | ||
| rerender(<OrderStatusBar currentStepLabel="Out For Delivery" />) | ||
| expect(screen.getByText('Out for delivery')).toBeInTheDocument() | ||
| }) | ||
| }) |
Uh oh!
There was an error while loading. Please reload this page.