Skip to content

Commit

Permalink
feat(procedures): implemented gdpr form (#13558)
Browse files Browse the repository at this point in the history
ref:MANAGER-15317

Signed-off-by: Omar ALKABOUSS MOUSSANA <[email protected]>
  • Loading branch information
oalkabouss authored Oct 17, 2024
1 parent 9b7d52d commit 6916a19
Show file tree
Hide file tree
Showing 12 changed files with 642 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const PageLayout: FunctionComponent<Props> = ({ children }) => (
<div className="inline-block pb-6 md:pb-12">
<img src={ovhCloudLogo} alt="ovh-cloud-logo" className="app-logo" />
</div>
<div className="flex justify-center app-content lg:w-8/12 mx-auto min-h-[500px] sm:shadow sm:border-none border-t-[1px] border-gray-300 px-6">
<div className="flex justify-center app-content lg:w-8/12 mx-auto min-h-[500px] sm:shadow-[0_0_6px_0_rgba(40,89,192,0.2)] sm:border-none border-t-[1px] border-gray-300 px-6">
<div className="md:p-8 w-full">{children}</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,18 @@ vi.mock('./rgdpIntroduction/RGDPIntroduction.component', () => ({
RGDPIntroduction: () => <div>RGDPIntroduction</div>,
}));

vi.mock('./rgdpForm/RGDPForm.component', () => ({
RGDPForm: () => <div>RGDPForm</div>,
}));

describe('RGDP Component', () => {
it('renders the component correctly', () => {
render(<RGDP />);

const introductionElement = screen.getByText('RGDPIntroduction');
const formElement = screen.getByText('RGDPForm');

expect(introductionElement).toBeInTheDocument();
expect(formElement).toBeInTheDocument();
});
});
2 changes: 2 additions & 0 deletions packages/manager/apps/procedures/src/pages/rgdp/RGDP.page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React from 'react';
import { PageLayout } from '@/components/PageLayout/PageLayout.component';
import { RGDPIntroduction } from './rgdpIntroduction/RGDPIntroduction.component';
import { RGDPForm } from './rgdpForm/RGDPForm.component';

export default function RGDP() {
return (
<PageLayout>
<RGDPIntroduction />
<RGDPForm />
</PageLayout>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import {
ODS_THEME_COLOR_INTENT,
ODS_THEME_TYPOGRAPHY_LEVEL,
} from '@ovhcloud/ods-common-theming';
import { ODS_BUTTON_TYPE } from '@ovhcloud/ods-components';
import { OsdsButton, OsdsText } from '@ovhcloud/ods-components/react';
import React, { FunctionComponent, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { GDPRFormValues } from '@/types/gdpr.type';
import { TextField } from './TextField/TextField.component';
import { SelectField } from './SelectField/SelectField.component';
import { TextAreaField } from './TextAreaField/TextAreaField.component';
import {
EmailRegex,
GDPRSubjectValues,
TextInputRegex,
} from './RGDPForm.constants';
import './RGDPForm.style.css';

export const RGDPForm: FunctionComponent = () => {
const { t } = useTranslation('rgdp');
const {
handleSubmit,
watch,
control,
formState: { isSubmitting, isValid, touchedFields },
trigger,
} = useForm<GDPRFormValues>({ mode: 'onBlur' });

const onSubmit = (data: GDPRFormValues) => {
// TODO: Handle API call & ConfirmModal
console.log(data);
};

const email = watch('email');

useEffect(() => {
if (touchedFields.confirmEmail) {
trigger('confirmEmail');
}
}, [email]);

return (
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<div className="my-10">
<TextField
label={t('rgdp_form_field_label_firstname')}
name="firstName"
required={t('rgdp_form_validation_message_required')}
pattern={{
value: TextInputRegex,
message: t('rgdp_form_validation_message_invalid_format'),
}}
control={control}
/>

<TextField
label={t('rgdp_form_field_label_surname')}
name="surname"
required={t('rgdp_form_validation_message_required')}
pattern={{
value: TextInputRegex,
message: t('rgdp_form_validation_message_invalid_format'),
}}
control={control}
/>

<TextField
label={t('rgdp_form_field_label_email')}
helper={t('rgdp_form_field_helper_email')}
name="email"
required={t('rgdp_form_validation_message_required')}
pattern={{
value: EmailRegex,
message: t('rgdp_form_validation_message_invalid_format'),
}}
control={control}
/>

<TextField
label={t('rgdp_form_field_label_confirm_email')}
name="confirmEmail"
validate={(value) =>
value === email || t('rgdp_form_validation_message_email_match')
}
control={control}
/>

<TextField
label={`${t('rgdp_form_field_label_nic')} (${t(
'rgdp_form_field_optional',
)})`}
name="nicHandle"
pattern={{
value: TextInputRegex,
message: t('rgdp_form_validation_message_invalid_format'),
}}
control={control}
/>

<div className="flex flex-col md:flex-row gap-2 flex-wrap md:items-center mb-8">
<OsdsText
level={ODS_THEME_TYPOGRAPHY_LEVEL.heading}
color={ODS_THEME_COLOR_INTENT.primary}
>
{t('rgdp_form_field_label_subject')}
</OsdsText>

<SelectField
name="messageSubject"
required={t('rgdp_form_validation_message_required')}
control={control}
placeholder={t('rgdp_form_field_placeholder_subject')}
options={GDPRSubjectValues.map((value) => ({
value,
label: t(`rgdp_form_subject_${value}`),
}))}
/>

<OsdsText
level={ODS_THEME_TYPOGRAPHY_LEVEL.heading}
color={ODS_THEME_COLOR_INTENT.primary}
>
{t('rgdp_form_field_label_subject_detail')}
</OsdsText>
</div>

<TextAreaField
label={t('rgdp_form_field_label_request_description')}
name="requestDescription"
required={t('rgdp_form_validation_message_required')}
control={control}
/>
</div>

<OsdsButton
type={ODS_BUTTON_TYPE.submit}
color={ODS_THEME_COLOR_INTENT.primary}
disabled={!isValid || isSubmitting || undefined}
inline={true}
>
{t('rgdp_form_submit')}
</OsdsButton>
</form>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const GDPRSubjectValues = [
'opposition_right',
'rectification_right',
'access_right',
'erasure_right',
'limitation_right',
'portability_right',
'payment_method_remove',
'other_request',
];

export const EmailRegex = /^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9]{2}(?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
export const TextInputRegex = /^[^<>]*$/;
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* TODO:
It is not clean to apply CSS for ODS components. It was added because Tailwind CSS overrides the border of ODS inputs.
The best solution is to configure Tailwind CSS with this option: corePlugins: {
preflight: false,
},
However,
some components on the MFA page are not in ODS. Consider converting these components to ODS in the future.
*/

osds-input[size='md'] {
border-width: var(--ods-size-input-md-border-width);
}

osds-input[color='primary'] {
border-color: var(--ods-color-primary-200);
}

osds-input[color='error'] {
border-color: var(--ods-color-error-500);
}

.ods-error {
border-color: var(--ods-color-error-500) !important;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { render, screen, act, waitFor } from '@testing-library/react';
import React from 'react';
import { describe, it, expect, vi } from 'vitest';
import * as OdsComponentModule from '@ovhcloud/ods-components/react';
import { OsdsInput, OsdsTextArea } from '@ovhcloud/ods-components';
import { RGDPForm } from './RGDPForm.component';
import { GDPRFormValues } from '@/types/gdpr.type';

const getOsdsElementByFormName = <T,>(fieldName: keyof GDPRFormValues) =>
(screen.queryByTestId(`field_id_${fieldName}`) as unknown) as T;

vi.mock('@ovhcloud/ods-components/react', async (importOriginal) => {
const module: typeof OdsComponentModule = await importOriginal();
return {
...module,
OsdsFormField: ({ children, error }: any) => (
<div>
{children}
<p>{error}</p>
</div>
),
OsdsInput: ({ ...props }: any) => (
<module.OsdsInput {...props} data-testid={props.id} />
),
OsdsButton: ({ ...props }: any) => <button {...props} />,
OsdsSelect: ({ ...props }: any) => (
<module.OsdsSelect {...props} data-testid={props.id} />
),
OsdsTextarea: ({ ...props }: any) => (
<module.OsdsTextarea {...props} data-testid={props.id} />
),
};
});

describe('RGDPForm', () => {
const renderForm = () => render(<RGDPForm />);

it('Should render the form fields correctly when the form is displayed', async () => {
const { getByText } = renderForm();

await waitFor(() => {
expect(getByText('rgdp_form_field_label_firstname:')).toBeInTheDocument();
expect(getByText('rgdp_form_field_label_surname:')).toBeInTheDocument();
expect(getByText('rgdp_form_field_label_email:')).toBeInTheDocument();
expect(
getByText('rgdp_form_field_label_confirm_email:'),
).toBeInTheDocument();
expect(getByText('rgdp_form_field_label_subject')).toBeInTheDocument();
expect(
getByText('rgdp_form_field_label_subject_detail'),
).toBeInTheDocument();
expect(
getByText('rgdp_form_field_label_request_description:'),
).toBeInTheDocument();
});
});

it('Should disable the submit button when required fields are not filled', async () => {
const { getByRole } = renderForm();
const submitBtn = getByRole('button', { name: 'rgdp_form_submit' });
expect(submitBtn).toBeDisabled();
});

it('Should show errors on multiple required fields when blurred with empty values', async () => {
const { queryAllByText } = renderForm();

const surnameInput = getOsdsElementByFormName<OsdsInput>('surname');
const firstNameInput = getOsdsElementByFormName<OsdsInput>('firstName');
const requestDescriptionText = getOsdsElementByFormName<OsdsTextArea>(
'requestDescription',
);

await act(async () => {
surnameInput.value = '';
firstNameInput.value = '';
requestDescriptionText.value = '';

surnameInput.odsInputBlur.emit();
firstNameInput.odsInputBlur.emit();
requestDescriptionText.odsBlur.emit();
});

await waitFor(() => {
const errorMessages = queryAllByText(
'rgdp_form_validation_message_required',
);
expect(errorMessages).toHaveLength(3);
});
});

it('Should show pattern validation errors on multiple fields when invalid characters are entered', async () => {
const { queryAllByText } = renderForm();

const surnameInput = getOsdsElementByFormName<OsdsInput>('surname');
const firstNameInput = getOsdsElementByFormName<OsdsInput>('firstName');
const nicInput = getOsdsElementByFormName<OsdsInput>('nicHandle');

await act(async () => {
surnameInput.value = '<';
firstNameInput.value = '>';
nicInput.value = '<';

surnameInput.odsValueChange.emit();
firstNameInput.odsValueChange.emit();
nicInput.odsValueChange.emit();

surnameInput.odsInputBlur.emit();
firstNameInput.odsInputBlur.emit();
nicInput.odsInputBlur.emit();
});

await waitFor(() => {
const errorMessages = queryAllByText(
'rgdp_form_validation_message_invalid_format',
);
expect(errorMessages).toHaveLength(3);
});
});

it('Should show an error when an invalid email is entered', async () => {
const { getByText } = renderForm();

const emailInput = getOsdsElementByFormName<OsdsInput>('email');

await act(async () => {
emailInput.value = 'invalidEmail';
emailInput.odsValueChange.emit();
emailInput.odsInputBlur.emit();
});

await waitFor(() => {
expect(
getByText('rgdp_form_validation_message_invalid_format'),
).toBeInTheDocument();
});
});

it('Should validate email fields and show an error if they don t match', async () => {
const { getByText } = renderForm();

const emailInput = getOsdsElementByFormName<OsdsInput>('email');
const confirmEmailInput = getOsdsElementByFormName<OsdsInput>(
'confirmEmail',
);

await act(async () => {
emailInput.value = '[email protected]';
confirmEmailInput.value = '[email protected]';

emailInput.odsValueChange.emit();
confirmEmailInput.odsValueChange.emit();

emailInput.odsInputBlur.emit();
confirmEmailInput.odsInputBlur.emit();
});

await waitFor(() => {
expect(
getByText('rgdp_form_validation_message_email_match'),
).toBeInTheDocument();
});
});
});
Loading

0 comments on commit 6916a19

Please sign in to comment.