Using the code provided below. Try to figure out the reason why don't my validation rules work for Input that has a prop inputValue with prechats?.fullName?.label
import { TNullable } from '../Constants/interfaces';
import {
  IValidationResult,
  IValidationRule,
} from '../hooks/useValidation/interfaces';

/**
 * Сравнивает два числа или длину строки и число на <=
 * @param value число или строка которую будут проверять
 * @param count число с которым будет сравниваться
 * @returns true/false
 */
export const isLessOrEquivalentlyThan = (
  value: TNullable<string | number | undefined>,
  count: number,
): boolean => isEquivalently(value, count) || isLessThan(value, count);

/**
 * Сравнивает два числа или длину строки и число на ===
 * @param value число или строка которую будут проверять
 * @param count число с которым будет сравниваться
 * @returns true/false
 */
export const isEquivalently = (
  value: TNullable<string | number | undefined>,
  count: number,
): boolean => {
  if (typeof value === 'number') return value === count;
  if (typeof value === 'string') return value.length === count;
  return false;
};

/**
 * Сравнивает два числа или длину строки и число на <
 * @param value число или строка которую будут проверять
 * @param count число с которым будет сравниваться
 * @returns true/false
 */
export const isLessThan = (
  value: TNullable<string | number | undefined>,
  count: number,
): boolean => {
  if (typeof value === 'number') return value < count;
  if (typeof value === 'string') return value.length < count;
  return false;
};

/**
 * Проверит пустой ли объект.
 *
 * @param {any} value Значение для проверки.
 */
export function isEmpty(value: any): value is null | undefined {
  if (value == null || value === undefined || typeof value === 'boolean') {
    return true;
  } else if (Array.isArray(value)) {
    return value.length === 0;
  } else if (typeof value === 'object') {
    for (const prop in value) {
      if (Object.prototype.hasOwnProperty.call(value, prop) !== undefined) {
        return false;
      }
    }

    return JSON.stringify(value) === JSON.stringify({});
  } else if (typeof value === 'string') {
    return value.trim().length === 0;
  } else if (typeof value === 'number') {
    return value === 0 || isNaN(value);
  } else if (value === undefined) {
    return true;
  }

  return false;
}

/**
 * Валидирует поля формы по переданным правилам.
 *
 * @param {any} form Поля формы.
 * @param {IValidationRule[]} rules Правила валидации.
 */
export const validate = (
  form: any,
  rules: IValidationRule[],
): IValidationResult => {
  let isValid = true;
  const errors = {};

  function validateField(keyRules: IValidationRule[], key: string): void {
    keyRules.forEach((rule: IValidationRule) => {
      // Если поле обязательное или непустое, то проверяем, иначе считаем валидным.
      if (rule.isRequired === true || !isEmpty(form[key])) {
        const [isFieldValid, options] = rule.rule(form[key], key, form);

        if (!isFieldValid) {
          const errorText =
            // eslint-disable-next-line @typescript-eslint/prefer-optional-chain
            options != null && options.templateText !== undefined
              ? options.templateText
              : rule.errorText;

          errors[key] = !isEmpty(errors[key]) ? [...errors[key]] : [];
          isValid = false;
          errors[key].push(errorText);
        }
      }
    });
  }

  if (!isEmpty(form) && !isEmpty(rules)) {
    for (const key in form) {
      const keyRules = rules.filter(
        (rule: IValidationRule) => rule.fieldName === key,
      );

      if (isEmpty(keyRules)) continue;

      errors[key] = null;

      validateField(keyRules, key);
    }
  }

  return { isValid, errors };
};

import { useState } from 'react';
import { TUseValidation } from '../Hooks.interfaces';
import { IValidationErrors, IValidationRule } from './interfaces';
import { validate } from '../../Utils/validate';

/**
 * Хук для валидации.
 */
export const useValidation: TUseValidation = function <T>(
  customValidationRules: IValidationRule[],
) {
  const [errors, setErrors] = useState<IValidationErrors>({});
  const [isValid, setIsValid] = useState<boolean>(true);

  /**
   * Валидирует все указанные поля формы.
   * Возвращает результат в виде boolean.
   *
   * @param {T} form Данные формы.
   * @param {Function} [onError] Действие, выполняемое после валидации при ошибке.
   */
  function handleValidateFields(form: T, onError?: () => void): boolean {
    const { isValid, errors } = validate(form, customValidationRules);

    setIsValid(isValid);
    setErrors(errors);

    !isValid && onError != null && onError();

    return isValid;
  }

  /**
   * Валидирует указанное поле и обновляет объект ошибок.
   *
   * @param {{[key: string]: any}} field Объект, содержащий поле для валидации.
   */
  function handleValidateField(field: Partial<T>) {
    return function () {
      const { isValid, errors: newErrors } = validate(
        field,
        customValidationRules,
      );

      setErrors((prevState) => ({ ...prevState, ...newErrors }));

      return isValid;
    };
  }

  /**
   * Очищает список ошибок
   */
  const handleClearErrors = (): void => {
    setErrors({});
  };

  return {
    isValid,
    onValidateFields: handleValidateFields,
    onValidateField: handleValidateField,
    errors,
    onClearErrors: handleClearErrors,
  };
};
import * as React from 'react';
import { useStore } from 'effector-react';
import debounce from 'lodash.debounce';
import {
  Button,
  Drawer,
  FunctionButton,
  Input,
  Trash,
} from '@prof_interfaces/prof_interfaces';
import {
  isEmpty,
  NOTIFICATIONS,
  showFixErrorsNotification,
  TitleForRequiredField,
  useValidation,
} from '@/shared';
import { EChannelGroups } from '@/entities/Channel/interfaces';
import {
  $editChannelFormData,
  $editModalVisible,
  editChannelEv,
  editChannelName,
  hideEditModal,
  showConfirmationModal,
} from '../EditChannel.store';
import { SButtonGroup, SColumn, SContent, SFooter } from '../styles';
import { formValidationRules } from '../consts';
import type { TValidationFields } from '../interfaces';

import { BasicSettingsForm } from './BasicSettingsForm';
import { WidgetSettingsForm } from './WidgetSettingsForm';
import { ConfirmationModal } from './ConfirmationModal';
import { AppearanceSettingsForm } from './AppearanceSettingsForm';
import { OtherSettingsForm } from './OtherSettingsForm';

/**
 * Дровер изменения/удаления канала
 * */
export const EditChannelModal: React.FC = () => {
  const visible = useStore($editModalVisible);
  const editFormData = useStore($editChannelFormData);
  const { channelSettings, channelTypeId } = editFormData;

  const { errors, onValidateFields, onClearErrors } =
    useValidation<TValidationFields>(formValidationRules);

  const handleClose = (): void => {
    hideEditModal();
  };

  const handleSave = (): void => {
    const result: TValidationFields = {
      ...editFormData,
      ...channelSettings,
      ...channelSettings?.bar,
      ...channelSettings?.bar?.triggerMessage,
      ...channelSettings?.files,
      ...channelSettings?.customersFilter,
      ...channelSettings?.prechats
    };

    if (channelSettings?.schedule) {
      result.fieldFrom = channelSettings.schedule.fieldFrom;
      result.fieldTo = channelSettings.schedule.fieldTo;
    }

    const isValid = onValidateFields(result);

    if (isValid) {
      editChannelEv();
    } else {
      showFixErrorsNotification(NOTIFICATIONS.Error.fixErrorsBeforeSaving);
    }
  };

  const footer = (
    <SFooter>
      <SButtonGroup>
        <Button onClick={handleSave}>Сохранить</Button>
        <Button variant="secondary" onClick={handleClose}>
          Отмена
        </Button>
      </SButtonGroup>

      <FunctionButton
        icon={<Trash />}
        onClick={() => {
          showConfirmationModal();
        }}
      >
        Удалить
      </FunctionButton>
    </SFooter>
  );

  React.useEffect(() => {
    onClearErrors();
  }, [visible]);

  return (
    <>
      <Drawer
        isOpen={visible}
        title="Редактирование канала"
        width={440}
        footer={footer}
        onClose={handleClose}
        closeOnEscape={false}
      >
        {channelTypeId !== EChannelGroups.WEB_CHAT ? (
          <SContent>
            <Input
              label={<TitleForRequiredField>Название</TitleForRequiredField>}
              inputValue={editFormData.name}
              assistive={errors.name?.[0]}
              isError={!isEmpty(errors.name)}
              onChange={debounce((e) => {
                editChannelName({ name: e.target.value });
              }, 250)}
            />
          </SContent>
        ) : (
          <SColumn>
            <BasicSettingsForm
              name={editFormData.name}
              enabled={channelSettings?.enabled}
              welcomeMessage={channelSettings?.welcomeMessage}
              schedule={editFormData?.channelSettings?.schedule}
              errors={errors}
            />
            <WidgetSettingsForm
              prechats={channelSettings?.prechats}
              bar={channelSettings?.bar}
              behaviour={channelSettings?.behaviour}
              errors={errors}
            />
            <AppearanceSettingsForm appearance={channelSettings?.appearance} />
            <OtherSettingsForm
              files={channelSettings?.files}
              filter={channelSettings?.customersFilter}
              errors={errors}
            />
          </SColumn>
        )}
      </Drawer>

      <ConfirmationModal name={editFormData.name} />
    </>
  );
};
import * as React from 'react';
import debounce from 'lodash.debounce';
import {
  FieldWrapper,
  Input,
  Switch,
  TextArea,
} from '@prof_interfaces/prof_interfaces';
import { CollapsePanel, isEmpty, TitleForRequiredField } from '@/shared';
import {
  SCollapsePanelForm,
  SCollapsePanelFormTitle,
  SFormContent,
  SFormContentItem,
  SFontStyleGrey,
  SInputDelayWrapper,
  STitleForm,
} from '../styles';
import { editChannelWidget } from '../EditChannel.store';
import { IWidgetSettingsForm } from '../interfaces';
import { useEffect, useState } from 'react';

/**
 * форма настроек виджета
 * @param bar Настройки отвечающие за бар
 * @param prechats настройки канала, отвечающие за отображение пречат-полей
 * @param behaviour Настройки канала определяющие поведение веб-чата
 * @param errors ошибки полей валидации
 */
export const WidgetSettingsForm: React.FC<IWidgetSettingsForm> = ({
  bar,
  prechats,
  behaviour,
  errors,
}) => {
  const [openedPanel, setOpenedPanel] = useState<boolean>(true);

  useEffect(() => {
    console.log('errors: ', errors)
    if (!isEmpty(errors.triggerMessage) || !isEmpty(errors.delay) || !isEmpty(errors.fullname)) {
      setOpenedPanel(true);
    }
  }, [errors]);

  return (
    <SCollapsePanelForm>
      <CollapsePanel
        openWhenClickOnTitle
        arrowSide="right"
        title={
          <SCollapsePanelFormTitle>Настройки виджета</SCollapsePanelFormTitle>
        }
        isOpen={openedPanel}
        onOpenChange={setOpenedPanel}
      >
        <SFormContent>
          <SFormContentItem>
            <STitleForm>Пречат поля</STitleForm>
            <Switch
              label="Отображать"
              defaultChecked={prechats?.enabled}
              size="medium"
              onChange={(value) => {
                editChannelWidget({
                  prechats: {
                    enabled: value,
                  },
                });
              }}
            />
          </SFormContentItem>

          <SFormContentItem>
            <STitleForm>Поле «Отдел»</STitleForm>
            <Switch
              label="Включено"
              defaultChecked={prechats?.skillGroup?.enabled}
              size="small"
              disabled={!prechats?.enabled}
              onChange={(value) => {
                editChannelWidget({
                  prechats: {
                    skillGroup: {
                      enabled: value,
                    },
                  },
                });
              }}
            />
            <Switch
              label="Обязательное"
              defaultChecked={prechats?.skillGroup?.required}
              size="small"
              disabled={!prechats?.enabled}
              onChange={(value) => {
                editChannelWidget({
                  prechats: {
                    skillGroup: {
                      required: value,
                    },
                  },
                });
              }}
            />
            <Input
              inputValue={prechats?.skillGroup?.label}
              disabled={!prechats?.enabled}
              onChange={debounce((e) => {
                editChannelWidget({
                  prechats: {
                    skillGroup: {
                      label: e.target.value,
                    },
                  },
                });
              }, 250)}
            />
          </SFormContentItem>

          <SFormContentItem>
            <STitleForm>Поле «Имя»</STitleForm>
            <Switch
              label="Включено"
              defaultChecked={prechats?.fullName?.enabled}
              size="small"
              disabled={!prechats?.enabled}
              onChange={(value) => {
                editChannelWidget({
                  prechats: {
                    fullName: {
                      enabled: value,
                    },
                  },
                });
              }}
            />
            <Switch
              label="Обязательное"
              defaultChecked={prechats?.fullName?.required}
              size="small"
              disabled={!prechats?.enabled}
              onChange={(value) => {
                editChannelWidget({
                  prechats: {
                    fullName: {
                      required: value,
                    },
                  },
                });
              }}
            />
            <Input
              inputValue={prechats?.fullName?.label}
              disabled={!prechats?.enabled}
              assistive={errors.fullname?.[0]}
              isError={!isEmpty(errors.fullname)}
              onChange={debounce((e) => {
                editChannelWidget({
                  prechats: {
                    fullName: {
                      label: e.target.value,
                    },
                  },
                });
              }, 250)}
            />
          </SFormContentItem>

          <SFormContentItem>
            <STitleForm>Поле «Телефон»</STitleForm>
            <Switch
              label="Включено"
              defaultChecked={prechats?.phone?.enabled}
              size="small"
              disabled={!prechats?.enabled}
              onChange={(value) => {
                editChannelWidget({
                  prechats: {
                    phone: {
                      enabled: value,
                    },
                  },
                });
              }}
            />
            <Switch
              label="Обязательное"
              defaultChecked={prechats?.phone?.required}
              size="small"
              disabled={!prechats?.enabled}
              onChange={(value) => {
                editChannelWidget({
                  prechats: {
                    phone: {
                      required: value,
                    },
                  },
                });
              }}
            />
            <Input
              inputValue={prechats?.phone?.label}
              disabled={!prechats?.enabled}
              onChange={debounce((e) => {
                editChannelWidget({
                  prechats: {
                    phone: {
                      label: e.target.value,
                    },
                  },
                });
              }, 250)}
            />
          </SFormContentItem>

          <SFormContentItem>
            <STitleForm>Бар виджета</STitleForm>
            <FieldWrapper assistive="Альтернативные каналы будут отображаться независимо от настроек скилл-группы">
              <Switch
                label="Показывать все альтернативные каналы"
                defaultChecked={behaviour?.allAlternativeChannelsEnabled}
                size="small"
                onChange={(value) => {
                  editChannelWidget({
                    behaviour: {
                      allAlternativeChannelsEnabled: value,
                    },
                  });
                }}
              />
            </FieldWrapper>
          </SFormContentItem>

          <SFormContentItem>
            <STitleForm>Текст бара</STitleForm>
            <Switch
              label="Отображать"
              defaultChecked={bar?.text?.enabled}
              size="small"
              onChange={(value) => {
                editChannelWidget({
                  bar: {
                    text: {
                      enabled: value,
                    },
                  },
                });
              }}
            />
            <Input
              inputValue={bar?.text?.value}
              placeholder="Введите текст"
              disabled={!bar?.text?.enabled}
              onChange={debounce((e) => {
                editChannelWidget({
                  bar: {
                    text: {
                      value: e.target.value,
                    },
                  },
                });
              }, 250)}
            />
          </SFormContentItem>

          <SFormContentItem>
            <STitleForm>Триггерное сообщение</STitleForm>
            <Switch
              label="Отображать"
              defaultChecked={bar?.triggerMessage?.enabled}
              size="small"
              onChange={(value) => {
                editChannelWidget({
                  bar: {
                    triggerMessage: {
                      enabled: value,
                    },
                  },
                });
              }}
            />
            <TextArea
              label={
                <TitleForRequiredField>Текст сообщения</TitleForRequiredField>
              }
              placeholder="Введи текст сообщения"
              defaultValue={bar?.triggerMessage?.text}
              onChange={debounce((e) => {
                editChannelWidget({
                  bar: {
                    triggerMessage: {
                      text: e.target.value,
                    },
                  },
                });
              }, 250)}
              disabled={!bar?.triggerMessage?.enabled}
              assistive={errors.triggerMessage?.[0]}
              isError={!isEmpty(errors.triggerMessage)}
            />
            <SInputDelayWrapper>
              <Input
                label={
                  <TitleForRequiredField>Отображать</TitleForRequiredField>
                }
                type="number"
                inputValue={String(bar?.triggerMessage?.delay)}
                disabled={!bar?.triggerMessage?.enabled}
                action={<SFontStyleGrey>сек.</SFontStyleGrey>}
                assistive={errors.delay?.[0]}
                isError={!isEmpty(errors.delay)}
                clearable={false}
                className="input-number"
                onChange={(e) => {
                  editChannelWidget({
                    bar: {
                      triggerMessage: {
                        delay: !isEmpty(e.target.value)
                          ? +e.target.value
                          : undefined,
                      },
                    },
                  });
                }}
              />
            </SInputDelayWrapper>
          </SFormContentItem>
        </SFormContent>
      </CollapsePanel>
    </SCollapsePanelForm>
  );
};
import { IValidationRule } from '@/shared/hooks/useValidation/interfaces';
import {
  NOTIFICATIONS,
  PATTERN_FOR_FIELDS,
  VALIDATION_NOTIFICATION_TEXT,
  isEmpty,
  isLessOrEquivalentlyThan,
} from '@/shared';

import type { TSchedule } from './interfaces';

export const formValidationRules: IValidationRule[] = [
  {
    fieldName: 'name',
    isRequired: true,
    rule: (name?: string) => [Boolean(name?.length)],
    errorText: NOTIFICATIONS.Error.isEmptyErrorText,
  },
  {
    fieldName: 'name',
    rule: (name: string) => [PATTERN_FOR_FIELDS.test(name)],
    errorText: VALIDATION_NOTIFICATION_TEXT,
  },
  {
    fieldName: 'name',
    rule: (name: string) => [name.length <= 255],
    errorText: NOTIFICATIONS.Error.isLengthTextMustBeLessThen(255),
  },
  {
    fieldName: 'welcomeMessage',
    isRequired: true,
    rule: (welcomeMessage?: string) => [Boolean(welcomeMessage?.length)],
    errorText: NOTIFICATIONS.Error.isEmptyErrorText,
  },
  {
    fieldName: 'welcomeMessage',
    rule: (welcomeMessage: string) => [PATTERN_FOR_FIELDS.test(welcomeMessage)],
    errorText: VALIDATION_NOTIFICATION_TEXT,
  },
  {
    fieldName: 'welcomeMessage',
    rule: (welcomeMessage: string) => [welcomeMessage.length <= 150],
    errorText: NOTIFICATIONS.Error.isLengthTextMustBeLessThen(150),
  },
  {
    rule: ({
      fieldFrom,
      fieldTo,
      from,
      fromMinutes,
      to,
      toMinutes,
    }: Required<TSchedule>) => [
      fieldFrom.includes('_') ||
        fieldTo.includes('_') ||
        from * 60 + fromMinutes !== to * 60 + toMinutes,
    ],
    fieldName: 'schedule',
    errorText: 'Границы совпадают. Необходимо поправить границы диапазона',
    isRequired: true,
  },
  {
    rule: ({
      fieldFrom,
      fieldTo,
      from,
      fromMinutes,
      to,
      toMinutes,
    }: Required<TSchedule>) => [
      fieldFrom.includes('_') ||
        fieldTo.includes('_') ||
        from * 60 + fromMinutes < to * 60 + toMinutes,
    ],
    fieldName: 'schedule',
    errorText:
      'Время начала отображения должно быть меньше времени окончания отображения',
    isRequired: true,
  },
  {
    rule: (fieldFrom: string) => [!fieldFrom.includes('_')],
    fieldName: 'fieldFrom',
    errorText: NOTIFICATIONS.Error.valueMustBeInFollowingFormat('00:00'),
    isRequired: true,
  },
  {
    rule: (fieldTo: string) => [!fieldTo.includes('_')],
    fieldName: 'fieldTo',
    errorText: NOTIFICATIONS.Error.valueMustBeInFollowingFormat('00:00'),
    isRequired: true,
  },
  {
    rule: (fieldTo: string) => [+fieldTo.replace(':', '.') <= 24],
    fieldName: 'fieldTo',
    errorText:
      'Время окончания отображения виджета не должно быть больше 24:00',
    isRequired: true,
  },
  {
    fieldName: 'triggerMessage',
    isRequired: true,
    rule: (triggerMessage) => [
      triggerMessage.enabled ? Boolean(triggerMessage?.text?.length) : true,
    ],
    errorText: NOTIFICATIONS.Error.isEmptyErrorText,
  },
  {
    fieldName: 'triggerMessage',
    rule: (triggerMessage) => [
      triggerMessage.enabled
        ? PATTERN_FOR_FIELDS.test(triggerMessage?.text)
        : true,
    ],
    errorText: VALIDATION_NOTIFICATION_TEXT,
  },
  {
    fieldName: 'triggerMessage',
    rule: (triggerMessage) => [
      triggerMessage.enabled ? triggerMessage?.text?.length <= 150 : true,
    ],
    errorText: NOTIFICATIONS.Error.isLengthTextMustBeLessThen(150),
  },
  {
    fieldName: 'delay',
    isRequired: true,
    rule: (delay, key, form) => {
      return [
        form.bar.triggerMessage.enabled ? Boolean(delay !== undefined) : true,
      ];
    },
    errorText: NOTIFICATIONS.Error.isEmptyErrorText,
  },
  {
    fieldName: 'allowedTypes',
    isRequired: true,
    rule: (allowedTypes?: string[]) => [Boolean(allowedTypes?.length)],
    errorText: NOTIFICATIONS.Error.isEmptyErrorText,
  },
  {
    fieldName: 'limit',
    isRequired: true,
    rule: (limit: string) => [Boolean(limit !== undefined)],
    errorText: NOTIFICATIONS.Error.isEmptyErrorText,
  },
  {
    fieldName: 'size',
    isRequired: true,
    rule: (size: string) => [Boolean(size !== undefined)],
    errorText: NOTIFICATIONS.Error.isEmptyErrorText,
  },
  {
    fieldName: 'greetings',
    rule: (greetings: string, _, { filtration: isSwitchActive }) => [
      isSwitchActive === false || !isEmpty(greetings),
    ],
    isRequired: true,
    errorText: NOTIFICATIONS.Error.isEmptyErrorText,
  },
  {
    fieldName: 'greetings',
    rule: (greetings: string) => [PATTERN_FOR_FIELDS.test(greetings)],
    errorText: VALIDATION_NOTIFICATION_TEXT,
  },
  {
    fieldName: 'greetings',
    rule: (greetings: string) => [greetings.length <= 250],
    errorText: NOTIFICATIONS.Error.isLengthTextMustBeLessThen(250),
  },
  {
    fieldName: 'template',
    rule: (text: string, _, { filtration: isSwitchActive }) => [
      isSwitchActive === false || !isEmpty(text),
    ],
    isRequired: true,
    errorText: 'Укажите текст шаблона',
  },
  {
    fieldName: 'template',
    rule: (text: string) => [isLessOrEquivalentlyThan(text.length, 4000)],
    errorText: NOTIFICATIONS.Error.isLengthTextMustBeLessThen(4000),
  },
  {
    fieldName: 'fullname',
    rule: (fullname: string) => {
      console.log('fullname: ', fullname);
      if (fullname === '' || fullname === undefined) return [true];
      let result;
      if (fullname !== undefined) {
        result = [PATTERN_FOR_FIELDS.test(fullname)];
      }
      return result;
    },
    isRequired: true,
    errorText: VALIDATION_NOTIFICATION_TEXT,
  },
  {
    fieldName: 'fullname',
    rule: (fullname: string) => [fullname.length <= 50],
    errorText: NOTIFICATIONS.Error.isLengthTextMustBeLessThen(50),
  },
];