-
Notifications
You must be signed in to change notification settings - Fork 99
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(mrc): add order config/summary component
ref: MANAGER-16647 Signed-off-by: David Arsène <[email protected]>
- Loading branch information
Showing
10 changed files
with
414 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
130 changes: 130 additions & 0 deletions
130
packages/manager-react-components/src/components/order/Order.component.spec.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import React, { fireEvent } from '@testing-library/react'; | ||
import { vi, vitest } from 'vitest'; | ||
import { Order } from './Order.component'; | ||
import { render } from '../../utils/test.provider'; | ||
|
||
describe('<Order> tests suite', () => { | ||
// Mock global window.open | ||
vi.stubGlobal('open', vi.fn()); | ||
|
||
const onCancelSpy = vi.fn(); | ||
const onValidateSpy = vi.fn(); | ||
const onFinishSpy = vi.fn(); | ||
const onClickLinkSpy = vi.fn(); | ||
const orderLink = 'https://order-link'; | ||
|
||
afterEach(() => { | ||
vitest.resetAllMocks(); | ||
}); | ||
|
||
const renderComponent = ( | ||
isValid: boolean, | ||
link: string, | ||
productName: string, | ||
) => | ||
render( | ||
<Order> | ||
<Order.Configuration | ||
isValid={isValid} | ||
onCancel={onCancelSpy} | ||
onConfirm={onValidateSpy} | ||
> | ||
<p>Order steps</p> | ||
</Order.Configuration> | ||
<Order.Summary | ||
onFinish={onFinishSpy} | ||
orderLink={link} | ||
onClickLink={onClickLinkSpy} | ||
productName={productName} | ||
></Order.Summary> | ||
</Order>, | ||
); | ||
|
||
it.each([{ valid: true }, { valid: false }])( | ||
'when order configuration validity is $valid confirm button disabled attribute should be $valid', | ||
({ valid }) => { | ||
const { getByTestId, getByText } = renderComponent(valid, null, null); | ||
|
||
expect(getByText('Order steps')).toBeVisible(); | ||
|
||
const orderButton = getByTestId('cta-order-configuration-order'); | ||
expect(orderButton).toHaveAttribute('label', 'Commander'); | ||
expect(orderButton).toHaveAttribute('is-disabled', `${!valid}`); | ||
}, | ||
); | ||
|
||
it('confirm button should be enabled and clickable when order configuration is valid', () => { | ||
const { getByTestId } = renderComponent(true, null, null); | ||
|
||
fireEvent.click(getByTestId('cta-order-configuration-order')); | ||
|
||
expect(onValidateSpy).toHaveBeenCalled(); | ||
}); | ||
|
||
it('should cancel order configuration when cancel button is clicked', () => { | ||
const { getByTestId } = renderComponent(false, null, null); | ||
|
||
fireEvent.click(getByTestId('cta-order-configuration-cancel')); | ||
|
||
expect(onCancelSpy).toHaveBeenCalled(); | ||
}); | ||
|
||
it('should open order link and display order summary when order configuration is confirmed ', () => { | ||
vi.spyOn(window, 'open'); | ||
const { getByTestId, queryByText } = renderComponent(true, orderLink, null); | ||
|
||
fireEvent.click(getByTestId('cta-order-configuration-order')); | ||
|
||
// order configuration is hidden | ||
expect(queryByText('Order steps')).not.toBeInTheDocument(); | ||
|
||
expect(getByTestId('order-summary-title')).toBeVisible(); | ||
expect(getByTestId('order-summary-link')).toBeVisible(); | ||
|
||
expect(window.open).toHaveBeenCalledTimes(1); | ||
expect(window.open).toHaveBeenCalledWith( | ||
orderLink, | ||
'_blank', | ||
'noopener,noreferrer', | ||
); | ||
}); | ||
|
||
it('should open order link when order link is clicked', () => { | ||
vi.spyOn(window, 'open'); | ||
const { getByTestId } = renderComponent(true, orderLink, null); | ||
|
||
fireEvent.click(getByTestId('cta-order-configuration-order')); | ||
fireEvent.click(getByTestId('order-summary-link')); | ||
|
||
expect(onClickLinkSpy).toHaveBeenCalled(); | ||
}); | ||
|
||
it('should close order summary when finish button is clicked', () => { | ||
vi.spyOn(window, 'open'); | ||
const { getByTestId } = renderComponent(true, orderLink, null); | ||
|
||
fireEvent.click(getByTestId('cta-order-configuration-order')); | ||
fireEvent.click(getByTestId('cta-order-summary-finish')); | ||
|
||
expect(onFinishSpy).toHaveBeenCalled(); | ||
expect(getByTestId('cta-order-configuration-order')).toBeVisible(); | ||
}); | ||
|
||
it.each([{ productName: '' }, { productName: 'OVHcloud product' }])( | ||
'should display given product name with value $productName', | ||
({ productName }) => { | ||
vi.spyOn(window, 'open'); | ||
const { getByTestId, getByText } = renderComponent( | ||
true, | ||
orderLink, | ||
productName, | ||
); | ||
|
||
fireEvent.click(getByTestId('cta-order-configuration-order')); | ||
fireEvent.click(getByTestId('order-summary-link')); | ||
|
||
const product = productName || 'service'; | ||
expect(getByText(`Commande de votre ${product} initiée`)).toBeVisible(); | ||
}, | ||
); | ||
}); |
12 changes: 12 additions & 0 deletions
12
packages/manager-react-components/src/components/order/Order.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import React, { PropsWithChildren } from 'react'; | ||
import { OrderContextProvider } from './Order.context'; | ||
import { OrderConfiguration } from './OrderConfiguration.component'; | ||
import { OrderSummary } from './OrderSummary.component'; | ||
import './translations'; | ||
|
||
export const Order = ({ children }: PropsWithChildren) => { | ||
return <OrderContextProvider>{children}</OrderContextProvider>; | ||
}; | ||
|
||
Order.Configuration = OrderConfiguration; | ||
Order.Summary = OrderSummary; |
32 changes: 32 additions & 0 deletions
32
packages/manager-react-components/src/components/order/Order.context.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import React, { createContext, useContext, useMemo, useState } from 'react'; | ||
|
||
export type TOrderContext = { | ||
setIsOrderInitialized: (isOrderInitialized: boolean) => void; | ||
isOrderInitialized: boolean; | ||
}; | ||
|
||
const OrderContext = createContext<TOrderContext>({} as TOrderContext); | ||
|
||
export const OrderContextProvider = ({ children }: React.PropsWithChildren) => { | ||
const [isOrderInitialized, setIsOrderInitialized] = useState<boolean>(false); | ||
|
||
const context = useMemo<TOrderContext>( | ||
() => ({ | ||
isOrderInitialized, | ||
setIsOrderInitialized, | ||
}), | ||
[isOrderInitialized], | ||
); | ||
|
||
return ( | ||
<OrderContext.Provider value={context}>{children}</OrderContext.Provider> | ||
); | ||
}; | ||
|
||
export const useOrderContext = (): TOrderContext => { | ||
const context = useContext(OrderContext); | ||
if (context === undefined) { | ||
throw new Error('Order-related components must be used within <Order>'); | ||
} | ||
return context; | ||
}; |
57 changes: 57 additions & 0 deletions
57
packages/manager-react-components/src/components/order/Order.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import React, { ComponentType } from 'react'; | ||
|
||
import { OdsText } from '@ovhcloud/ods-components/react'; | ||
import { Meta, StoryObj } from '@storybook/react'; | ||
import { Order } from './Order.component'; | ||
|
||
function renderComponent(args) { | ||
return ( | ||
<Order> | ||
<Order.Configuration | ||
onCancel={args.onCancel} | ||
onConfirm={args.onConfirm} | ||
isValid={args.isValid} | ||
> | ||
<p> | ||
<OdsText preset="code" className="italic"> | ||
...|order configuration steps| ... | ||
</OdsText> | ||
</p> | ||
</Order.Configuration> | ||
<Order.Summary | ||
onFinish={args.onFinish} | ||
orderLink={args.orderLink} | ||
onClickLink={args.onClickLink} | ||
productName={args.productName} | ||
></Order.Summary> | ||
</Order> | ||
); | ||
} | ||
|
||
type Story = StoryObj<typeof Order> & | ||
StoryObj<typeof Order.Configuration> & | ||
StoryObj<typeof Order.Summary>; | ||
|
||
export const DemoOrder: Story = { | ||
args: { | ||
isValid: true, | ||
orderLink: 'https://www.ovh.com', | ||
productName: '', | ||
onCancel: () => {}, | ||
onConfirm: () => {}, | ||
onFinish: () => {}, | ||
onClickLink: () => {}, | ||
}, | ||
render: renderComponent, | ||
}; | ||
|
||
const meta: Meta = { | ||
title: 'Components/Order', | ||
component: Order, | ||
subcomponents: { | ||
'Order.Summary': Order.Summary as ComponentType<unknown>, | ||
'Order.Configuration': Order.Configuration as ComponentType<unknown>, | ||
}, | ||
}; | ||
|
||
export default meta; |
61 changes: 61 additions & 0 deletions
61
packages/manager-react-components/src/components/order/OrderConfiguration.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { | ||
ODS_BUTTON_COLOR, | ||
ODS_BUTTON_ICON_ALIGNMENT, | ||
ODS_BUTTON_SIZE, | ||
ODS_BUTTON_VARIANT, | ||
ODS_ICON_NAME, | ||
} from '@ovhcloud/ods-components'; | ||
import { OdsButton } from '@ovhcloud/ods-components/react'; | ||
import React, { ReactNode } from 'react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { useOrderContext } from './Order.context'; | ||
|
||
export type TOrderConfiguration = { | ||
children: ReactNode; | ||
onCancel: () => void; | ||
onConfirm: () => void; | ||
isValid: boolean; | ||
}; | ||
|
||
export const OrderConfiguration: React.FC<TOrderConfiguration> = ({ | ||
children, | ||
onCancel, | ||
onConfirm, | ||
isValid, | ||
}: TOrderConfiguration): JSX.Element => { | ||
const { isOrderInitialized, setIsOrderInitialized } = useOrderContext(); | ||
const { t } = useTranslation('order'); | ||
|
||
if (isOrderInitialized) { | ||
return <></>; | ||
} | ||
|
||
return ( | ||
<> | ||
{children} | ||
<div className="flex flex-row gap-4"> | ||
<OdsButton | ||
size={ODS_BUTTON_SIZE.md} | ||
variant={ODS_BUTTON_VARIANT.ghost} | ||
color={ODS_BUTTON_COLOR.primary} | ||
onClick={onCancel} | ||
label={t('order_configuration_cancel')} | ||
data-testid="cta-order-configuration-cancel" | ||
/> | ||
<OdsButton | ||
size={ODS_BUTTON_SIZE.md} | ||
color={ODS_BUTTON_COLOR.primary} | ||
isDisabled={!isValid} | ||
onClick={() => { | ||
onConfirm(); | ||
setIsOrderInitialized(true); | ||
}} | ||
icon={ODS_ICON_NAME.externalLink} | ||
iconAlignment={ODS_BUTTON_ICON_ALIGNMENT.left} | ||
label={t('order_configuration_order')} | ||
data-testid="cta-order-configuration-order" | ||
/> | ||
</div> | ||
</> | ||
); | ||
}; |
83 changes: 83 additions & 0 deletions
83
packages/manager-react-components/src/components/order/OrderSummary.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { | ||
ODS_BUTTON_COLOR, | ||
ODS_BUTTON_SIZE, | ||
ODS_TEXT_PRESET, | ||
} from '@ovhcloud/ods-components'; | ||
import { OdsButton, OdsText } from '@ovhcloud/ods-components/react'; | ||
import { Trans, useTranslation } from 'react-i18next'; | ||
import React, { useEffect } from 'react'; | ||
import { Links, LinkType } from '../typography'; | ||
import { useOrderContext } from './Order.context'; | ||
|
||
export type TOrderSummary = { | ||
onFinish: () => void; | ||
onClickLink?: () => void; | ||
orderLink: string; | ||
productName?: string; | ||
}; | ||
|
||
export const OrderSummary: React.FC<TOrderSummary> = ({ | ||
onFinish, | ||
onClickLink, | ||
orderLink, | ||
productName, | ||
}: TOrderSummary): JSX.Element => { | ||
const { t } = useTranslation('order'); | ||
const { isOrderInitialized, setIsOrderInitialized } = useOrderContext(); | ||
|
||
useEffect(() => { | ||
if (orderLink && isOrderInitialized) { | ||
window.open(orderLink, '_blank', 'noopener,noreferrer'); | ||
} | ||
}, [orderLink, isOrderInitialized]); | ||
|
||
if (!isOrderInitialized) { | ||
return <></>; | ||
} | ||
|
||
// set default label if no product name provided | ||
const product = productName || t('order_summary_product_default_label'); | ||
|
||
return ( | ||
<div className="flex flex-col gap-8"> | ||
<div className="flex flex-col gap-4"> | ||
<OdsText | ||
preset={ODS_TEXT_PRESET.heading2} | ||
data-testid="order-summary-title" | ||
> | ||
{t('order_summary_order_initiated_title', { product })} | ||
</OdsText> | ||
<OdsText preset={ODS_TEXT_PRESET.paragraph}> | ||
<Trans | ||
t={t} | ||
i18nKey="order_summary_order_initiated_subtitle" | ||
components={{ | ||
OrderLink: ( | ||
<Links | ||
type={LinkType.external} | ||
target="_blank" | ||
href={orderLink} | ||
data-testid="order-summary-link" | ||
onClickReturn={onClickLink} | ||
/> | ||
), | ||
}} | ||
></Trans> | ||
</OdsText> | ||
<OdsText preset={ODS_TEXT_PRESET.paragraph}> | ||
{t('order_summary_order_initiated_info', { product })} | ||
</OdsText> | ||
</div> | ||
<OdsButton | ||
size={ODS_BUTTON_SIZE.md} | ||
color={ODS_BUTTON_COLOR.primary} | ||
data-testid="cta-order-summary-finish" | ||
onClick={() => { | ||
onFinish(); | ||
setIsOrderInitialized(false); | ||
}} | ||
label={t('order_summary_finish')} | ||
/> | ||
</div> | ||
); | ||
}; |
2 changes: 2 additions & 0 deletions
2
packages/manager-react-components/src/components/order/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './Order.component'; | ||
export * from './Order.context'; |
9 changes: 9 additions & 0 deletions
9
packages/manager-react-components/src/components/order/translations/Messages_fr_FR.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"order_configuration_cancel": "Annuler", | ||
"order_configuration_order": "Commander", | ||
"order_summary_finish": "Terminer", | ||
"order_summary_product_default_label": "service", | ||
"order_summary_order_initiated_title": "Commande de votre {{product}} initiée", | ||
"order_summary_order_initiated_subtitle": "Si vous n'avez pas pu finaliser votre commande, merci de la compléter en cliquant sur le <OrderLink label=\"lien suivant\">{{label}}</OrderLink>", | ||
"order_summary_order_initiated_info": "Nous vous informerons de la disponibilité de votre {{product}} par e-mail." | ||
} |
Oops, something went wrong.