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:
parent
4dde127203
commit
a1eaa2eae7
@ -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')} />
|
||||
</>}
|
||||
</>}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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];
|
||||
|
@ -161,6 +161,7 @@
|
||||
&-error {
|
||||
margin-top: 0.4rem;
|
||||
color: var(--alert);
|
||||
font-weight: normal;
|
||||
}
|
||||
&-warning {
|
||||
margin-top: 0.4rem;
|
||||
|
@ -6,4 +6,9 @@
|
||||
&.is-disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
&.is-incorrect {
|
||||
.fab-text-editor {
|
||||
border-color: var(--alert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()">
|
||||
|
@ -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"
|
||||
|
@ -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'
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
39
test/frontend/components/machines/machines-settings.test.tsx
Normal file
39
test/frontend/components/machines/machines-settings.test.tsx
Normal 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));
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user