RocketChat/Rocket.Chat

View on GitHub
packages/livechat/src/routes/LeaveMessage/index.tsx

Summary

Maintainability
D
1 day
Test Coverage
import type { FunctionalComponent } from 'preact';
import { useContext, useRef } from 'preact/hooks';
import type { JSXInternal } from 'preact/src/jsx';
import type { FieldValues, SubmitHandler } from 'react-hook-form';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';

import { Livechat } from '../../api';
import { Button } from '../../components/Button';
import { Form, FormField, SelectInput, TextInput } from '../../components/Form';
import { FormScrollShadow } from '../../components/Form/FormScrollShadow';
import { MultilineTextInput } from '../../components/Form/MultilineTextInput';
import MarkdownBlock from '../../components/MarkdownBlock';
import { ModalManager } from '../../components/Modal';
import Screen from '../../components/Screen';
import { createClassName } from '../../helpers/createClassName';
import { parseOfflineMessage } from '../../helpers/parseOfflineMessage';
import { sortArrayByColumn } from '../../helpers/sortArrayByColumn';
import { validateEmail } from '../../lib/email';
import { parentCall } from '../../lib/parentCall';
import { createToken } from '../../lib/random';
import { StoreContext } from '../../store';
import styles from './styles.scss';

const LeaveMessage: FunctionalComponent<{ path: string }> = () => {
    const {
        config: {
            departments = [],
            messages: { offlineMessage, offlineSuccessMessage, offlineUnavailableMessage },
            theme: { offlineTitle: title, offlineColor },
            settings: { displayOfflineForm },
        },
        iframe,
        loading,
        dispatch,
        alerts,
    } = useContext(StoreContext);
    const { t } = useTranslation();

    const topRef = useRef<HTMLDivElement>(null);
    const bottomRef = useRef<HTMLDivElement>(null);

    const {
        handleSubmit,
        formState: { errors, isDirty, isValid, isSubmitting },
        control,
    } = useForm({ mode: 'onChange' });

    const customOfflineTitle = iframe?.theme?.offlineTitle;

    type FormValues = { name: string; email: string; department?: string; message: string };

    const onSubmit = async ({ name, email, department, message }: FormValues) => {
        const fields = {
            name,
            email,
            ...(department && { department }),
            message,
        };

        await dispatch({ loading: true });

        try {
            // TODO: Remove intersection after ts refactor of parseOfflineMessage
            const payload = parseOfflineMessage(fields) as FormValues & { host: string };
            const text = await Livechat.sendOfflineMessage(payload);
            await ModalManager.alert({
                text: offlineSuccessMessage || text,
            });
            parentCall('callback', 'offline-form-submit', fields);
            return true;
        } catch (error: unknown) {
            const errorMessage = (error as { error: string })?.error;
            console.error(errorMessage);
            const alert = { id: createToken(), children: errorMessage, error: true, timeout: 5000 };
            await dispatch({ alerts: (alerts.push(alert), alerts) });
            return false;
        } finally {
            await dispatch({ loading: false });
        }
    };

    const defaultTitle = t('leave_a_message');
    const defaultMessage = t('we_are_not_online_right_now_please_leave_a_message');
    const defaultUnavailableMessage = t('offline_form_not_available');

    return (
        <Screen title={customOfflineTitle || title || defaultTitle} color={offlineColor} className={createClassName(styles, 'leave-message')}>
            {displayOfflineForm ? (
                <FormScrollShadow topRef={topRef} bottomRef={bottomRef}>
                    <Screen.Content full>
                        <div id='top' ref={topRef} style={{ height: '1px', width: '100%' }} />

                        <div className={createClassName(styles, 'leave-message__main-message')}>
                            <MarkdownBlock text={offlineMessage || defaultMessage} />
                        </div>

                        <Form
                            // The price of using react-hook-form on a preact project ¯\_(ツ)_/¯
                            onSubmit={handleSubmit(onSubmit as SubmitHandler<FieldValues>) as unknown as JSXInternal.GenericEventHandler<HTMLFormElement>}
                            id='leaveMessage'
                        >
                            <FormField required label={t('name')} error={errors.name?.message?.toString()}>
                                <Controller
                                    name='name'
                                    control={control}
                                    // defaultValue={guestName}
                                    rules={{ required: true }}
                                    render={({ field }) => (
                                        <TextInput placeholder={t('insert_your_field_here', { field: t('name') })} disabled={loading} {...field} />
                                    )}
                                />
                            </FormField>

                            <FormField required label={t('email')} error={errors.email?.message?.toString()}>
                                <Controller
                                    name='email'
                                    control={control}
                                    // defaultValue={guestEmail}
                                    rules={{
                                        required: true,
                                        validate: { checkEmail: (value) => validateEmail(value, { style: 'rfc' }) || t('invalid_email') },
                                    }}
                                    render={({ field }) => (
                                        <TextInput placeholder={t('insert_your_field_here', { field: t('email') })} disabled={loading} {...field} />
                                    )}
                                />
                            </FormField>

                            {departments?.some((dept) => dept.showOnOfflineForm) ? (
                                <FormField label={t('i_need_help_with')} error={errors.department?.message?.toString()}>
                                    <Controller
                                        name='department'
                                        control={control}
                                        render={({ field }) => (
                                            <SelectInput
                                                options={sortArrayByColumn(departments, 'name').map(({ _id, name }: { _id: string; name: string }) => ({
                                                    value: _id,
                                                    label: name,
                                                }))}
                                                placeholder={t('choose_an_option')}
                                                disabled={loading}
                                                {...field}
                                            />
                                        )}
                                    />
                                </FormField>
                            ) : null}
                            <FormField required label={t('message')} error={errors.message?.message?.toString()}>
                                <Controller
                                    name='message'
                                    control={control}
                                    rules={{ required: true }}
                                    render={({ field }) => (
                                        <MultilineTextInput rows={4} placeholder={t('write_your_message')} disabled={loading} {...field} />
                                    )}
                                />
                            </FormField>
                        </Form>
                        <div ref={bottomRef} id='bottom' style={{ height: '1px', width: '100%' }} />
                    </Screen.Content>
                </FormScrollShadow>
            ) : (
                <Screen.Content full>
                    <div className={createClassName(styles, 'leave-message__main-message')}>
                        <MarkdownBlock text={offlineUnavailableMessage || defaultUnavailableMessage} />
                    </div>
                </Screen.Content>
            )}
            <Screen.Footer>
                {displayOfflineForm ? (
                    <Button loading={loading} form='leaveMessage' submit full disabled={!isDirty || !isValid || loading || isSubmitting}>
                        {t('send')}
                    </Button>
                ) : null}
            </Screen.Footer>
        </Screen>
    );
};

export default LeaveMessage;