Skip to content

Commit

Permalink
feat(mrc): add order config/summary component
Browse files Browse the repository at this point in the history
ref: MANAGER-16647

Signed-off-by: David Arsène <[email protected]>
  • Loading branch information
darsene committed Jan 27, 2025
1 parent 1a6368e commit c53e69a
Show file tree
Hide file tree
Showing 10 changed files with 414 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/manager-react-components/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ export * from './ManagerText/ManagerText';

export * from './pci-maintenance-banner';
export * from './region/region.component';
export * from './order';
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();
},
);
});
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;
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;
};
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;
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>
</>
);
};
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>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './Order.component';
export * from './Order.context';
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."
}
Loading

0 comments on commit c53e69a

Please sign in to comment.