Загрузка данных


import { InterfaceType } from '@/shared/api/controller/client';
import { Schema } from '@shared/lib/Schema';
import { SchemaMessage } from '@shared/lib/SchemaMessage';
import { Validator } from '@shared/lib/Validator';
import { array, boolean, object, string } from 'yup';
import { OTHER_VALUES } from '../model/formNames';

const validator = new Validator();
const schemaMessage = new SchemaMessage();

export const createDeviceFormSchema = object().shape(
  {
    name: string().required(schemaMessage.requiredMessage),
    description: string().notRequired(),
    bidirectional: boolean().notRequired(),
    modelId: string().required(schemaMessage.requiredMessage),
    sipUri: Schema.sipUriSchema,
    ip: Schema.deviceIpV4.when('type', ([type]: InterfaceType[], schema) => {
      return schema;
    }),
    mac: Schema.deviceMac.when('type', ([type]: InterfaceType[], schema) => {
      if (type === 'av-lan') {
        return schema.notRequired().nullable();
      }
      return schema;
    }),
    controlPage: string().when('controlPage', (controlPage, schema) => {
      if (!controlPage) {
        return schema.notRequired().nullable();
      }

      return schema
        .matches(validator.controlPageRegExp, schemaMessage.wrongFormatMessage)
        .notRequired();
    }),
    hardwareId: Schema.hardwareId,
    type: Schema.interfaceType,
    ieee8021x: Schema.ieee8021x.when(
      'type',
      ([type]: InterfaceType[], schema) => {
        if (type === 'ethernet') {
          return schema.required(schemaMessage.requiredMessage);
        }

        return schema.notRequired().nullable();
      },
    ),
    protocolIp: Schema.protocolIp.when('type', {
      is: 'ethernet',
      then: (schema) => schema.required(schemaMessage.requiredMessage),
      otherwise: (schema) => schema.notRequired(),
    }),
    firmware: string().nullable(),
    serialNumber: string().nullable(),
    inventoryNumber: string().nullable(),
    ipAvLan: string().when('type', ([type]: InterfaceType[], schema) => {
      if (type === 'av-lan') {
        return schema
          .matches(validator.ipV4RegExp, schemaMessage.wrongFormatMessage)
          .required();
      }

      return schema.notRequired().nullable();
    }),
    localAddress: Schema.localAddressCreateSchema,
    port: Schema.portNotRequired,
    netInterface: boolean().optional(),
    disableMonitoring: boolean().optional(),
    netInterfaceInfo: array().when(
      [OTHER_VALUES.netInterface, OTHER_VALUES.type],
      ([netInterface, type], schema) => {
        if (netInterface && type === 'ethernet') {
          return schema.of(
            object({
              nameId: string().required(schemaMessage.requiredMessage),
              ip: Schema.ipV4NotRequired,
              mac: Schema.macNotRequired,
              subnetMask: Schema.ipV4NotRequired,
              gateway: Schema.ipV4NotRequired,
            }),
          );
        } else {
          return schema.optional().default([]);
        }
      },
    ),
  },
  [
    ['mac', 'mac'],
    ['port', 'port'],
    ['controlPage', 'controlPage'],
    ['localAddress', 'localAddress'],
  ],
);


import { useDeviceRepository } from '@/domains/equipment/repositories/device.repository';
import { normalizeSipUriForPayload } from '@/domains/equipment/repositories/helpers/normalizeSipUriForPayload';
import { usePreservedSipUri } from '@/domains/equipment/repositories/helpers/usePreservedSipUri';
import { DeviceModel } from '@/shared/api/controller/client';
import { yupResolver } from '@hookform/resolvers/yup';
import { QueryStatus } from '@reduxjs/toolkit/dist/query';
import { Button, Drawer, DrawerActions } from '@shared/components';
import { useCloseDrawer } from '@shared/hooks/drawer/useCloseDrawer';
import { useSelectDrawerType } from '@shared/hooks/drawer/useSelectDrawerType';
import {
  EventActionTypes,
  useSendClickStreamEvent,
} from '@shared/hooks/useSendClickStreamEvent';
import { DrawerTypes } from '@shared/model/drawer/drawer.types';
import { useSelectActiveController } from '@shared/store/сontrollers/сontrollers.selectors';
import { useSyncControlPage } from '@widgets/Monitoring/hooks';
import { memo, useEffect, useState } from 'react';
import {
  FormProvider,
  Resolver,
  SubmitHandler,
  useForm,
} from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { CreateDeviceForm } from '../CreateDeviceForm';
import { CreateDeviceFormData } from '../model/types';
import { createDeviceFormSchema } from '../validation/validator';

export const CreateDeviceDrawer = memo(() => {
  const { t } = useTranslation('common');
  const { sendEvent } = useSendClickStreamEvent(EventActionTypes.createDevice);

  const drawerType = useSelectDrawerType();
  const isVisible = drawerType[DrawerTypes.createDevice];
  const closeDrawer = useCloseDrawer();
  const activeController = useSelectActiveController();

  const [model, setModel] = useState<DeviceModel | null>(null);

  const { preserveSipUri, getPreservedSipUri, resetPreservedSipUri } =
    usePreservedSipUri();

  const {
    saveDevice,
    createDeviceStatus,
    isCreateDeviceLoading,
    resetCreateDevice,
  } = useDeviceRepository();

  const defaultValues: CreateDeviceFormData = {
    name: '',
    description: '',
    ip: '',
    mac: '',
    controlPage: '',
    typeName: '',
    manufacturerName: '',
    sipUri: '',
    hasSipUri: null,
    modelId: '',
    hardwareId: '',
    type: 'ethernet',
    bidirectional: false,
    ieee8021x: 'MAB',
    protocolIp: 'dhcp',
    firmware: null,
    serialNumber: null,
    inventoryNumber: null,
    ipAvLan: '',
    localAddress: undefined,
    port: undefined,
    netInterface: false,
    netInterfaceInfo: [],
    disableMonitoring: false,
  };

  const methods = useForm<CreateDeviceFormData>({
    mode: 'onBlur',
    reValidateMode: 'onChange',
    defaultValues,
    resolver: yupResolver(
      createDeviceFormSchema,
    ) as Resolver<CreateDeviceFormData>,
  });

  useSyncControlPage(defaultValues.ip, methods);

  useEffect(() => {
    const currentHasSipUri = methods.getValues('hasSipUri');

    if (currentHasSipUri) {
      preserveSipUri(methods.getValues('sipUri'));
    }

    if (model) {
      const modelHasSipUri = model.type?.hasSipUri ?? false;

      methods.setValue('manufacturerName', model.manufacturer?.name);
      methods.setValue('typeName', model.type?.name);
      methods.setValue('hasSipUri', modelHasSipUri);

      if (modelHasSipUri) {
        methods.setValue('sipUri', getPreservedSipUri());
      }

      return;
    }

    methods.setValue('modelId', '');
    methods.resetField('manufacturerName');
    methods.resetField('typeName');
    methods.setValue('hasSipUri', null);
  }, [model]);

  useEffect(() => {
    if (isVisible) {
      methods.reset(defaultValues);
      resetPreservedSipUri();
    }
  }, [isVisible]);

  useEffect(() => {
    if (createDeviceStatus === QueryStatus.fulfilled) {
      closeDrawer(DrawerTypes.createDevice);
      methods.reset(defaultValues);
      resetPreservedSipUri();
      resetCreateDevice();
      sendEvent();
    }
  }, [createDeviceStatus]);

  const submitHandler: SubmitHandler<CreateDeviceFormData> = (values) => {
    if (activeController?.id) {
      saveDevice(normalizeSipUriForPayload(values), activeController.id);
    }
  };

  const actions: DrawerActions = {
    actionPrimary: (
      <Button
        data-fui-tid={`${DrawerTypes.createDevice}-actionPrimary`}
        onClick={methods.handleSubmit(submitHandler)}
        loading={isCreateDeviceLoading}
        disabled={!methods.formState.isValid}
      >
        {t('buttons.save')}
      </Button>
    ),
  };

  return (
    <FormProvider {...methods}>
      <Drawer
        type={DrawerTypes.createDevice}
        actions={actions}
        title={t('drawerTitle.createDevice')}
      >
        <CreateDeviceForm setModel={setModel} />
      </Drawer>
    </FormProvider>
  );
});


{
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "dependencies": {
    "@babel/preset-env": "7.26.9",
    "@emotion/react": "11.11.3",
    "@emotion/styled": "11.11.0",
    "@hookform/resolvers": "3.1.1",
    "@mui/icons-material": "5.14.9",
    "@mui/material": "5.14.9",
    "@reduxjs/toolkit": "1.9.5",
    "@sber-friend/flamingo-charts": "4.10.0",
    "@sber-friend/flamingo-components": "4.10.0",
    "@sber-friend/flamingo-core": "4.10.0",
    "@sber-friend/flamingo-icons": "4.10.0",
    "@sber-friend/flamingo-pickers": "4.10.0",
    "@sbol/clickstream-agent": "0.9.9",
    "@tanstack/react-router": "1.29.2",
    "ajv": "8.18.0",
    "axios": "1.13.0",
    "d3-color": "3.1.0",
    "i18next": "23.2.8",
    "i18next-browser-languagedetector": "7.2.1",
    "i18next-http-backend": "2.5.2",
    "ip-regex": "4.3.0",
    "jest-fixed-jsdom": "0.0.9",
    "jwt-decode": "3.1.2",
    "klona": "2.0.6",
    "notistack": "3.0.1",
    "object-hash": "3.0.0",
    "plural-ru": "2.0.2",
    "postcss": "8.4.49",
    "react": "18.3.1",
    "react-dom": "^18.2.0",
    "react-hook-form": "7.45.4",
    "react-i18next": "13.0.1",
    "react-redux": "^8.1.2",
    "react-router-dom": "7.13.2",
    "react-use-websocket": "4.13.0",
    "react-virtuoso": "4.12.3",
    "rxjs": "8.0.0-alpha.14",
    "styled-components": "6.1.12",
    "typescript": "5.4.5",
    "use-debounce": "10.0.0",
    "yup": "1.3.3"
  },
  "devDependencies": {
    "@babel/core": "7.24.5",
    "@babel/plugin-proposal-class-properties": "7.18.6",
    "@babel/plugin-proposal-private-property-in-object": "7.21.11",
    "@babel/preset-env": "7.26.9",
    "@babel/preset-react": "7.24.1",
    "@babel/preset-typescript": "7.24.1",
    "@babel/register": "8.0.0-alpha.6",
    "@eslint/compat": "1.2.2",
    "@eslint/eslintrc": "3.1.0",
    "@eslint/js": "9.14.0",
    "@pmmmwh/react-refresh-webpack-plugin": "0.5.11",
    "@rtk-query/codegen-openapi": "1.0.0",
    "@svgr/webpack": "8.1.0",
    "@tanstack/router-devtools": "1.29.2",
    "@testing-library/dom": "10.3.0",
    "@testing-library/jest-dom": "6.4.5",
    "@testing-library/react": "16.0.1",
    "@testing-library/user-event": "14.5.2",
    "@types/jest": "29.5.10",
    "@types/node": "20.10.3",
    "@types/object-hash": "3.0.6",
    "@types/react": "18.2.41",
    "@types/react-dom": "18.2.17",
    "@types/react-input-mask": "3.0.5",
    "@types/webpack": "5.28.0",
    "@types/webpack-bundle-analyzer": "4.4.1",
    "@types/webpack-dev-server": "4.7.2",
    "@typescript-eslint/parser": "8.13.1-alpha.1",
    "@typescript-eslint/typescript-estree": "8.13.1-alpha.1",
    "@typescript-eslint/utils": "8.13.1-alpha.1",
    "autoprefixer": "9.4.5",
    "babel-jest": "27.5.1",
    "babel-loader": "8.2.3",
    "babel-preset-react-app": "10.0.1",
    "clean-webpack-plugin": "4.0.0",
    "copy-webpack-plugin": "10.2.4",
    "cross-env": "7.0.3",
    "css-loader": "6.8.1",
    "cssnano": "6.0.2",
    "dayjs": "1.11.11",
    "eslint": "9.14.0",
    "eslint-config-prettier": "9.0.0",
    "eslint-import-resolver-typescript": "3.6.3",
    "eslint-plugin-import": "2.31.0",
    "eslint-plugin-prettier": "5.0.0",
    "eslint-plugin-react": "7.29.4",
    "eslint-plugin-react-hooks": "4.3.0",
    "fork-ts-checker-webpack-plugin": "7.2.1",
    "globals": "15.12.0",
    "html-webpack-plugin": "5.5.4",
    "husky": "^8.0.0",
    "ignore-styles": "5.0.1",
    "jest": "29.7.0",
    "jest-environment-jsdom": "30.0.0",
    "jest-transform-stub": "2.0.0",
    "jsdom": "18.1.0",
    "lint-staged": "9.5.0",
    "merge": "2.1.1",
    "mini-css-extract-plugin": "2.7.6",
    "msw": "2.6.3",
    "postcss-loader": "8.1.1",
    "prettier": "3.2.5",
    "prettier-plugin-organize-imports": "4.1.0",
    "react-refresh": "0.9.0",
    "react-refresh-typescript": "2.0.9",
    "style-loader": "3.3.3",
    "ts-loader": "9.5.1",
    "ts-node": "10.9.1",
    "typescript-eslint": "8.13.0",
    "url-loader": "4.1.1",
    "webpack": "5.89.0",
    "webpack-bundle-analyzer": "4.10.2",
    "webpack-cli": "5.1.4",
    "webpack-dev-server": "4.15.1",
    "whatwg-fetch": "3.6.2"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "lint-staged": {
    "**/*.{js,jsx,ts,tsx}": [
      "npm run lint",
      "npm run prettier",
      "git add"
    ],
    "**/*.test.{js,ts,jsx,tsx}": [
      "npm run test",
      "git add"
    ]
  },
  "name": "smart-room-front",
  "scripts": {
    "dev": "cross-env NODE_ENV=development webpack serve --env mode=development",
    "build:dev": "cross-env NODE_ENV=development webpack --env mode=development",
    "build": "cross-env NODE_ENV=production webpack --env mode=production",
    "generate-auth-client": "npx @rtk-query/codegen-openapi src/shared/api/auth/openapi-config.ts",
    "generate-auth-equipment-client": "npx @rtk-query/codegen-openapi src/shared/api/auth/equipment/openapi-config.ts",
    "generate-auth-refresh-tokens-client": "npx @rtk-query/codegen-openapi src/shared/api/auth/refreshTokens/openapi-config.ts",
    "generate-auth-click-stream-client": "npx @rtk-query/codegen-openapi src/shared/api/auth/clickStream/openapi-config.ts",
    "generate-user-client": "npx @rtk-query/codegen-openapi src/shared/api/user/openapi-config.ts",
    "generate-controller-client": "npx @rtk-query/codegen-openapi src/shared/api/controller/openapi-config.ts",
    "generate-location-client": "npx @rtk-query/codegen-openapi src/shared/api/location/openapi-config.ts",
    "generate-task-client": "npx @rtk-query/codegen-openapi src/shared/api/task/openapi-config.ts",
    "generate-audit-client": "npx @rtk-query/codegen-openapi src/shared/api/audit/openapi-config.ts",
    "generate-stat-client": "npx @rtk-query/codegen-openapi src/shared/api/stat/openapi-config.ts",
    "generate-notifications-client": "npx @rtk-query/codegen-openapi src/shared/api/notifications/openapi-config.ts",
    "generate-ws-client": "npx @rtk-query/codegen-openapi src/shared/api/ws/openapi-config.ts",
    "test": "jest --passWithNoTests",
    "test:watch": "jest --watch",
    "prepare": "husky install",
    "lint": "eslint src/ -c eslint.config.mjs --fix",
    "prettier": "prettier -w --check"
  },
  "version": "1.1.15",
  "overrides": {
    "eslint": "9.14.0",
    "@typescript-eslint/utils": "8.13.1-alpha.1"
  },
  "msw": {
    "workerDirectory": [
      "public"
    ]
  }
}


Шаги воспроизведения при создании:

Создать ДУ типа Ethernet,
Заполнить обязательные поля,
Изменить тип ДУ например на avlan
Вернуть Ethernet
Фактический результат: возможность создать ДУ блокируется

Ожидаемый результат: Создать ДУ возможно