Skip to content
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

feat(procedures): implemented gdpr form #13558

Merged
merged 2 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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])?$/;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure for that regex ? I never see numbers in the tld part

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, i used Xander regex

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
Loading