1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-03-15 12:29:16 +01:00

(feat) machines custom banner frontend and frontend test

This commit is contained in:
Karen 2023-01-25 11:24:41 +01:00 committed by Sylvain
parent 4dde127203
commit a1eaa2eae7
11 changed files with 180 additions and 48 deletions

View File

@ -1,17 +1,21 @@
import * as React from 'react';
import { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { FieldValues } from 'react-hook-form/dist/types/fields';
import { Control, FormState, UseFormRegister } from 'react-hook-form';
import { Control, FormState, UseFormRegister, UseFormGetValues } from 'react-hook-form';
import { FormSwitch } from '../form/form-switch';
import { FormRichText } from '../form/form-rich-text';
import { FormInput } from '../form/form-input';
import { SettingName } from '../../models/setting';
export type EditorialKeys = 'active_text_block' | 'text_block' | 'active_cta' | 'cta_label' | 'cta_url';
interface EditorialBlockFormProps<TFieldValues> {
register: UseFormRegister<TFieldValues>,
control: Control<TFieldValues>,
formState: FormState<TFieldValues>,
info?: string
keys: Record<EditorialKeys, SettingName>,
getValues?: UseFormGetValues<FieldValues>,
}
// regular expression to validate the input fields
@ -20,21 +24,23 @@ const urlRegex = /^(https?:\/\/)([^.]+)\.(.{2,30})(\/.*)*\/?$/;
/**
* Allows to create a formatted text and optional cta button in a form block, to be included in a resource form managed by react-hook-form.
*/
export const EditorialBlockForm = <TFieldValues extends FieldValues>({ register, control, formState, info }: EditorialBlockFormProps<TFieldValues>) => {
export const EditorialBlockForm = <TFieldValues extends FieldValues>({ register, control, formState, info, keys, getValues }: EditorialBlockFormProps<TFieldValues>) => {
const { t } = useTranslation('admin');
const [isActiveTextBlock, setIsActiveTextBlock] = useState<boolean>(false);
const [isActiveCta, setIsActiveCta] = useState<boolean>(false);
/** Set correct values for switches when formState changes */
useEffect(() => {
setIsActiveTextBlock(control._formValues[keys.active_text_block]);
setIsActiveCta(control._formValues[keys.active_cta]);
}, [control._formValues]);
/** Callback triggered when the text block switch has changed. */
const toggleTextBlockSwitch = (value: boolean) => {
setIsActiveTextBlock(value);
};
const toggleTextBlockSwitch = (value: boolean) => setIsActiveTextBlock(value);
/** Callback triggered when the CTA switch has changed. */
const toggleTextBlockCta = (value: boolean) => {
setIsActiveCta(value);
};
const toggleTextBlockCta = (value: boolean) => setIsActiveCta(value);
return (
<>
@ -43,34 +49,41 @@ export const EditorialBlockForm = <TFieldValues extends FieldValues>({ register,
{info && <p className="description">{info}</p>}
</header>
<div className="content">
<FormSwitch id="active_text_block" control={control}
<div className="content" data-testid="editorial-block-form">
<FormSwitch id={keys.active_text_block} control={control}
onChange={toggleTextBlockSwitch} formState={formState}
defaultValue={isActiveTextBlock}
label={t('app.admin.editorial_block_form.switch')} />
{/* TODO: error message if empty */}
<FormRichText id="text_block"
<FormRichText id={keys.text_block}
label={t('app.admin.editorial_block_form.content')}
control={control}
formState={formState}
heading
limit={280}
rules={{ required: isActiveTextBlock }}
rules={{ required: { value: isActiveTextBlock, message: t('app.admin.editorial_block_form.content_is_required') } }}
disabled={!isActiveTextBlock} />
{isActiveTextBlock && <>
<FormSwitch id="active_cta" control={control}
<FormSwitch id={keys.active_cta} control={control}
onChange={toggleTextBlockCta} formState={formState}
label={t('app.admin.editorial_block_form.cta_switch')} />
{isActiveCta && <>
<FormInput id="cta_label"
<FormInput id={keys.cta_label}
register={register}
rules={{ required: isActiveCta }}
formState={formState}
getValues={getValues}
rules={{ required: { value: isActiveCta, message: t('app.admin.editorial_block_form.label_is_required') } }}
maxLength={40}
label={t('app.admin.editorial_block_form.cta_label')} />
<FormInput id="cta_url"
<FormInput id={keys.cta_url}
register={register}
rules={{ required: isActiveCta, pattern: urlRegex }}
formState={formState}
rules={{
required: { value: isActiveCta, message: t('app.admin.editorial_block_form.url_is_required') },
pattern: { value: urlRegex, message: t('app.admin.editorial_block_form.url_must_be_safe') }
}}
label={t('app.admin.editorial_block_form.cta_url')} />
</>}
</>}

View File

@ -3,13 +3,14 @@ import { IApplication } from '../../models/application';
import { Loader } from '../base/loader';
import { react2angular } from 'react2angular';
import { FabButton } from '../base/fab-button';
import { SettingValue } from '../../models/setting';
declare const Application: IApplication;
interface EditorialBlockProps {
text: string,
cta?: string,
url?: string
text: SettingValue,
cta?: SettingValue,
url?: SettingValue
}
/**
@ -18,12 +19,12 @@ interface EditorialBlockProps {
export const EditorialBlock: React.FC<EditorialBlockProps> = ({ text, cta, url }) => {
/** Link to url from props */
const linkTo = (): void => {
window.location.href = url;
window.location.href = url as string;
};
return (
<div className={`editorial-block ${cta?.length > 25 ? 'long-cta' : ''}`}>
<div dangerouslySetInnerHTML={{ __html: text }}></div>
<div className={`editorial-block ${(cta as string)?.length > 25 ? 'long-cta' : ''}`}>
<div dangerouslySetInnerHTML={{ __html: text as string }}></div>
{cta && <FabButton className='is-main' onClick={linkTo}>{cta}</FabButton>}
</div>
);

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import React, { useEffect } from 'react';
import { IApplication } from '../../models/application';
import { Loader } from '../base/loader';
import { react2angular } from 'react2angular';
@ -6,29 +6,52 @@ import { ErrorBoundary } from '../base/error-boundary';
import { useTranslation } from 'react-i18next';
import { useForm, SubmitHandler } from 'react-hook-form';
import { FabButton } from '../base/fab-button';
import { EditorialBlockForm } from '../editorial-block/editorial-block-form';
import { EditorialKeys, EditorialBlockForm } from '../editorial-block/editorial-block-form';
import SettingAPI from '../../api/setting';
import SettingLib from '../../lib/setting';
import { SettingName, SettingValue, machineBannerSettings } from '../../models/setting';
declare const Application: IApplication;
interface MachinesSettingsProps {
onError: (message: string) => void,
onSuccess: (message: string) => void,
beforeSubmit?: (data: Record<SettingName, SettingValue>) => void,
}
/**
* Machines settings
*/
export const MachinesSettings: React.FC<MachinesSettingsProps> = () => {
export const MachinesSettings: React.FC<MachinesSettingsProps> = ({ onError, onSuccess, beforeSubmit }) => {
const { t } = useTranslation('admin');
const { register, control, formState, handleSubmit } = useForm();
const { register, control, formState, handleSubmit, reset, getValues } = useForm();
/**
* Callback triggered when the form is submitted: save the settings
*/
const onSubmit: SubmitHandler<any> = (data) => {
console.log(data);
/** Link Machines Banner Setting Names to generic keys expected by the Editorial Form */
const bannerKeys: Record<EditorialKeys, SettingName> = {
active_text_block: 'machines_banner_active',
text_block: 'machines_banner_text',
active_cta: 'machines_banner_cta_active',
cta_label: 'machines_banner_cta_label',
cta_url: 'machines_banner_cta_url'
};
/** Callback triggered when the form is submitted: save the settings */
const onSubmit: SubmitHandler<Record<SettingName, SettingValue>> = (data) => {
if (typeof beforeSubmit === 'function') beforeSubmit(data);
SettingAPI.bulkUpdate(SettingLib.objectToBulkMap(data)).then(() => {
onSuccess(t('app.admin.machines_settings.successfully_saved'));
}, reason => {
onError(reason);
});
};
/** On component mount, fetch existing Machines Banner Settings from API, and populate form with these values. */
useEffect(() => {
SettingAPI.query(machineBannerSettings)
.then(settings => reset(SettingLib.bulkMapToObject(settings)))
.catch(onError);
}, []);
return (
<div className="machines-settings">
<header>
@ -40,6 +63,8 @@ export const MachinesSettings: React.FC<MachinesSettingsProps> = () => {
<EditorialBlockForm register={register}
control={control}
formState={formState}
getValues={getValues}
keys={bannerKeys}
info={t('app.admin.machines_settings.generic_text_block_info')} />
</div>
</form>

View File

@ -243,9 +243,12 @@ export const trainingSettings = [
'trainings_invalidation_rule_period'
] as const;
export const bannersSettings = [
export const machineBannerSettings = [
'machines_banner_active',
'machines_banner_text',
'machines_banner_button'
'machines_banner_cta_active',
'machines_banner_cta_label',
'machines_banner_cta_url'
] as const;
export const allSettings = [
@ -275,7 +278,7 @@ export const allSettings = [
...displaySettings,
...storeSettings,
...trainingSettings,
...bannersSettings
...machineBannerSettings
] as const;
export type SettingName = typeof allSettings[number];

View File

@ -161,6 +161,7 @@
&-error {
margin-top: 0.4rem;
color: var(--alert);
font-weight: normal;
}
&-warning {
margin-top: 0.4rem;

View File

@ -6,4 +6,9 @@
&.is-disabled {
opacity: 0.5;
}
&.is-incorrect {
.fab-text-editor {
border-color: var(--alert);
}
}
}

View File

@ -12,7 +12,7 @@
<uib-tabset justified="true" active="tabs.active">
<uib-tab heading="{{ 'app.admin.machines.machines_settings' | translate }}" index="1" select="selectTab()">
<machines-settings on-error="onError" on-success="on-success"></machines-settings>
<machines-settings on-error="onError" on-success="onSuccess"></machines-settings>
</uib-tab>
<uib-tab heading="{{ 'app.admin.machines.all_machines' | translate }}" index="0" select="selectTab()">

View File

@ -22,6 +22,7 @@ en:
cta_label: "Button label"
cta_url: "url"
save: "Save"
successfully_saved: "Your banner was successfully saved."
machine_categories_list:
machine_categories: "Machines categories"
add_a_machine_category: "Add a machine category"
@ -2391,8 +2392,13 @@ en:
date: "Changed at"
operator: "By"
editorial_block_form:
title: "Editorial text block"
switch: "Display editorial block"
content: "Content"
content_is_required: "You must provide a content. If you wish to disable the banner, toggle the switch above this field."
label_is_required: "You must provide a label. If you wish to disable the button, toggle the switch above this field."
url_is_required: "You must provide a link for your button."
url_must_be_safe: "The button link should start with http://www or https://www ."
title: "Banner"
switch: "Display the banner"
cta_switch: "Display a button"
cta_label: "Button label"
cta_url: "url"
cta_url: "Button link"

View File

@ -749,16 +749,34 @@ export const settings: Array<Setting> = [
localized: 'Nom de la TVA'
},
{
name: 'machines_banner_text',
value: 'false',
name: 'machines_banner_active',
value: 'true',
last_update: '2022-12-23T14:39:12+0100',
localized: 'Text of the banner in Machines List'
localized: 'Custom banner is active'
},
{
name: 'machines_banner_button',
value: 'false',
name: 'machines_banner_text',
value: 'Test for machines Banner Content',
last_update: '2022-12-23T14:39:12+0100',
localized: 'Button of the banner in Machines List'
localized: 'Text of the custom banner'
},
{
name: 'machines_banner_cta_active',
value: 'true',
last_update: '2022-12-23T14:39:12+0100',
localized: 'Custom banner has a button'
},
{
name: 'machines_banner_cta_label',
value: 'Test for machines Banner Button',
last_update: '2022-12-23T14:39:12+0100',
localized: 'Label'
},
{
name: 'machines_banner_cta_url',
value: 'https://www.sleede.com/',
last_update: '2022-12-23T14:39:12+0100',
localized: 'Url'
}
];

View File

@ -0,0 +1,21 @@
import { render, screen } from '@testing-library/react';
import { EditorialBlock } from '../../../../app/frontend/src/javascript/components/editorial-block/editorial-block';
import { settings } from '../../__fixtures__/settings';
// Editorial Block
describe('Editorial Block', () => {
test('should render the correct block', async () => {
const machinesBannerText = settings.find((setting) => setting.name === 'machines_banner_text').value;
const machinesBannerCtaActive = settings.find((setting) => setting.name === 'machines_banner_cta_active').value;
const machinesBannerCtaLabel = settings.find((setting) => setting.name === 'machines_banner_cta_label').value;
const machinesBannerCtaUrl = settings.find((setting) => setting.name === 'machines_banner_cta_url').value;
render(<EditorialBlock
text={machinesBannerText}
cta={machinesBannerCtaActive && machinesBannerCtaLabel}
url={machinesBannerCtaActive && machinesBannerCtaUrl} />);
expect(screen.getByText('Test for machines Banner Content')).toBeDefined();
expect(screen.getByText('Test for machines Banner Button')).toBeDefined();
});
});

View File

@ -0,0 +1,39 @@
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { MachinesSettings } from '../../../../app/frontend/src/javascript/components/machines/machines-settings';
import { tiptapEvent } from '../../__lib__/tiptap';
// Machines Settings
describe('Machines Settings', () => {
test('should render the correct form', async () => {
const onError = jest.fn();
const onSuccess = jest.fn();
render(<MachinesSettings onError={onError} onSuccess={onSuccess}/>);
await waitFor(() => screen.getByTestId('editorial-block-form'));
expect(screen.getByLabelText(/app.admin.editorial_block_form.content/)).toBeDefined();
expect(screen.getByLabelText(/app.admin.editorial_block_form.cta_label/)).toBeDefined();
expect(screen.getByLabelText(/app.admin.editorial_block_form.cta_url/)).toBeDefined();
});
test('create a banner', async () => {
const onError = jest.fn();
const onSuccess = jest.fn();
const beforeSubmit = jest.fn();
render(<MachinesSettings onError={onError} onSuccess={onSuccess} beforeSubmit={beforeSubmit}/>);
await waitFor(() => screen.getByTestId('editorial-block-form'));
await tiptapEvent.type(screen.getByLabelText(/app.admin.editorial_block_form.content/), 'Lorem ipsum dolor sit amet');
fireEvent.change(screen.getByLabelText(/app.admin.editorial_block_form.cta_label/), { target: { value: 'Button Label 1' } });
fireEvent.change(screen.getByLabelText(/app.admin.editorial_block_form.cta_url/), { target: { value: 'https://www.sleede.com/' } });
fireEvent.click(screen.getByRole('button', { name: /app.admin.machines_settings.save/ }));
const expected = {
machines_banner_active: true,
machines_banner_text: '<p>Lorem ipsum dolor sit amet</p>',
machines_banner_cta_active: true,
machines_banner_cta_label: 'Button Label 1',
machines_banner_cta_url: 'https://www.sleede.com/'
};
await waitFor(() => expect(onSuccess).toHaveBeenCalledTimes(1));
expect(beforeSubmit).toHaveBeenCalledWith(expect.objectContaining(expected));
});
});