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


import isEqual from 'lodash/isEqual';

import api from '@api/index';
import { isRemovalFlag } from '@store/controllers/StateDiff/utils/addRemovalFlag';
import { PublicContextItem } from '@store/slices/publicContext';
import { packWidgetProps } from '@utils/widgetPropsProceeders';
import { convertPxToPercents } from '@utils/widgetSizesNPositionProceeders';

import { getStore } from '../../store';

import type {
  ApiCallBack,
  ApiController,
  ApiControllerKeys,
  CollapseDiffStateType,
  DiffResult,
  DiffType,
  MainDiffResult,
  PartialWidget,
  PartialWorkspace,
} from './types';
import type { RootState } from '@store/store';
import type { AxiosResponse } from 'axios';
import type { Widget, WidgetPackedProps } from 'types/Widgets';
import type { Workspace } from 'types/Workspace';

class StateDiffController {
  private previosStoreState: RootState = {} as RootState;

  private prevState: RootState = {} as RootState;

  private currentDiff: MainDiffResult[] | undefined = undefined;

  private apiController: ApiController = {
    workspaces: api.workspaceController,
    widgets: {
      delete: api.widgetsController.delete,
      update: api.widgetPropertiesController.update,
    },
    publicContext: { update: api.widgetsController.update, delete: undefined },
  };

  public init = (state: RootState): this => {
    this.previosStoreState = state;
    this.prevState = state;
    return this;
  };

  public async saveDiff(): Promise<void> {
    if (!this.currentDiff) {
      // eslint-disable-next-line no-console -- Видимо нужно для отладки
      console.error('TERMINAL STATE DIFF CONTROLLER: current diff is undefined, call getDiff before call saveDiff');
      return;
    }
    this.currentDiff.forEach((elemDiff) => {
      const { result, type } = elemDiff as MainDiffResult;

      if (
        type === 'workspaces' &&
        result.type === 'update' &&
        (result.diff as PartialWorkspace[])[0].name !==
          this.prevState.workspaces.workspaces.find((el) => el.id === (result.diff as PartialWorkspace[])[0].id)?.name
      ) {
        return;
      }
      if (
        type === 'workspaces' &&
        result.type === 'update' &&
        (result.diff as PartialWorkspace[])[0].isCurrent !==
          this.prevState.workspaces.workspaces.find((el) => el.id === (result.diff as PartialWorkspace[])[0].id)
            ?.isCurrent
      ) {
        return;
      }
      if (
        type === 'workspaces' &&
        result.type === 'update' &&
        (result.diff as PartialWorkspace[])[0].saved !==
          this.prevState.workspaces.workspaces.find((el) => el.id === (result.diff as PartialWorkspace[])[0].id)?.saved
      ) {
        return;
      }
      if (type === 'nothing' || result.type === 'nothing') {
        return;
      }

      const apiCallback = [elemDiff.type, elemDiff.result.type].includes('nothing')
        ? undefined
        : this.getApiCallback(elemDiff.type as ApiControllerKeys, elemDiff.result.type as CollapseDiffStateType);

      if (!apiCallback) {
        return;
      }
      if (result.type === 'update' && type === 'widgets') {
        if ('length' in result.diff) {
          result.diff.forEach((item) => {
            const { isRemoval, ...itemWithoutRemovalFlag } = item;
            const widget = getStore().getState().widgets.widgets.filter((w) => w.id === itemWithoutRemovalFlag.id)[0];
            if ('withoutSend' in widget) {
              if (!widget.withoutSend) {
                const packedProps = convertPxToPercents(packWidgetProps(itemWithoutRemovalFlag));
                (
                  apiCallback as unknown as (
                    data: ReturnType<typeof packWidgetProps>,
                  ) => Promise<AxiosResponse<WidgetPackedProps[]>>
                )({ ...packedProps, isRemoval: !!isRemoval });
              }
            } else {
              const packedProps = convertPxToPercents(packWidgetProps(itemWithoutRemovalFlag));
              (
                apiCallback as unknown as (
                  data: ReturnType<typeof packWidgetProps>,
                ) => Promise<AxiosResponse<WidgetPackedProps[]>>
              )({ ...packedProps, isRemoval: !!isRemoval });
            }
          });
        }
        return;
      }
      if (Array.isArray(result.diff)) {
        (result.diff as Workspace[] | Widget[]).forEach((item) =>
          (apiCallback as (data: typeof item) => Promise<AxiosResponse<undefined>>)(item),
        );
      } else {
        (apiCallback as (data: typeof result.diff) => Promise<AxiosResponse<undefined>>)(result.diff as Workspace);
      }
    });
    this.currentDiff = undefined;
    this.prevState = this.previosStoreState;
  }

  public getDiff = (state: RootState): this => {
    let type: DiffType | 'nothing' = 'nothing';
    const result: DiffResult = {
      type: 'nothing',
      diff: [],
    };
    if (
      !this.previosStoreState ||
      (this.previosStoreState.widgets.widgets.length === 0 && state.widgets.widgets.length !== 0) ||
      (this.previosStoreState.publicContext.publicContext.length === 0 &&
        state.publicContext.publicContext.length !== 0) ||
      (this.previosStoreState.workspaces.workspaces.find((item) => item.id === -1) &&
        !state.workspaces.workspaces.find((item) => item.id === -1))
    ) {
      this.previosStoreState = state;
      const res: MainDiffResult = {
        type: 'nothing',
        result: { type: 'nothing', diff: [] },
      };
      this.currentDiff = [res];
      return this;
    }

    const stateEntries = Object.entries(state) as [keyof RootState, RootState[keyof RootState]][];

    const diffProceeder: Partial<Record<keyof RootState, (s: RootState) => DiffResult>> = {
      workspaces: this.getWorkspaceDiff,
      widgets: this.getWidgetsDiff,
      publicContext: this.getPublicContextDiff,
    };
    this.currentDiff = [];
    for (let i = 0; i < stateEntries.length; i += 1) {
      const [key, value] = stateEntries[i];

      if (!isEqual(value, this.previosStoreState[key])) {
        type = key;

        if (diffProceeder[key]) {
          const diff = (diffProceeder[key] as (s: RootState) => DiffResult)(state);
          result.type = diff.type;
          result.diff = diff.diff;
          this.currentDiff.push({
            type,
            result: { ...result },
          });
        }
      }
    }

    this.previosStoreState = state;

    return this;
  };

  private getWorkspaceDiff = (state: RootState): DiffResult => {
    const { workspaces } = state;
    const prevWorkspaces = this.previosStoreState.workspaces;
    const newItemsIds: Workspace['id'][] = [];
    if (
      prevWorkspaces.workspaces.filter((item) => item.id !== -1).length >
      workspaces.workspaces.filter((item) => item.id !== -1).length
    ) {
      const existId = workspaces.workspaces.map(({ id }) => id);
      return {
        type: 'delete',
        diff: prevWorkspaces.workspaces.filter(({ id }) => id > 1).find(({ id }) => !existId.includes(id)) as Workspace,
      };
    }
    if (prevWorkspaces.workspaces.length < workspaces.workspaces.length) {
      const lastExistIds = prevWorkspaces.workspaces.map(({ id }) => id);
      workspaces.workspaces
        .map(({ id }) => id)
        .filter((id) => !lastExistIds.includes(id))
        .forEach((id) => newItemsIds.push(id));
    }
    const diff: PartialWorkspace[] = [];

    workspaces.workspaces.forEach((item, index) => {
      if (newItemsIds.includes(item.id)) {
        return undefined;
      }
      const prevItem = prevWorkspaces.workspaces[index];
      const keys = Object.keys(item).filter((key) => key !== 'id') as (keyof Workspace)[];
      for (let i = 0; i < keys.length; i += 1) {
        const key = keys[i];
        if (item[key] !== prevItem[key] && item.id > 0) {
          const diffItemIndex = diff.findIndex(({ id }) => id === item.id);
          if (diffItemIndex !== -1) {
            diff[diffItemIndex] = item;
          } else {
            diff.push(item);
          }
        }
      }
      return undefined;
    });

    const type = diff.length > 0 ? 'update' : 'nothing';

    return {
      type,
      diff,
    };
  };

  private getWidgetsDiff = (state: RootState): DiffResult => {
    const { widgets: newWidgets } = state.widgets;
    const prevWidgets = this.previosStoreState.widgets.widgets;

    // Исключаем случай с добавлением виджета
    const widgets = newWidgets.filter(({ id }) => prevWidgets.some((item) => item.id === id));

    if (widgets.length < prevWidgets.length) {
      const existIds = widgets.map(({ id }) => id);
      return {
        type: 'delete',
        diff: prevWidgets.filter(({ id }) => id > 1).filter(({ id }) => !existIds.includes(id)),
      };
    }

    const diff: PartialWidget[] = [];

    widgets.forEach((widget, index) => {
      const prevWidget = prevWidgets[index];
      const keys = Object.keys(widget).filter((key) => key !== 'id') as Exclude<keyof Widget, 'id'>[];

      for (let i = 0; i < keys.length; i += 1) {
        const key = keys[i];
        if (prevWidget[key] !== widget[key]) {
          const diffItemIndex = diff.findIndex(({ id }) => id === widget.id);
          if (diffItemIndex !== -1) {
            (diff[diffItemIndex][key] as Widget[keyof Widget]) = widget[key];
          } else {
            const isNeedToRemoveProperties =
              key === 'widgetContentProps' && isRemovalFlag(widget[key], prevWidget[key]);
            diff.push({ id: widget.id, [key]: widget[key], isRemoval: isNeedToRemoveProperties });
          }
        }
      }
    });

    const type = diff.length > 0 ? 'update' : 'nothing';

    return {
      type,
      diff,
    };
  };

  private getPublicContextDiff = (state: RootState): DiffResult => {
    const { widgets } = state.widgets;
    const { publicContext } = state.publicContext;
    const prevPublicContext = this.previosStoreState.publicContext.publicContext;
    const diff: PublicContextItem[] = [];
    publicContext.forEach((item) => {
      const findInd = prevPublicContext.findIndex((elem) => elem.id === item.id);
      if (findInd === -1 || !isEqual(item, prevPublicContext[findInd])) {
        const resultPublicProps = Array.from(item.publicProperties || []).reduce(
          (acc, curr, ind) => ({ ...acc, [ind]: JSON.stringify(curr) }),
          {},
        );
        const findWidget = widgets.find((elem) => elem.id === item.id);

        diff.push({
          id: item.id,
          color: item.color,
          name: findWidget?.name ?? '',
          workspaceId: findWidget?.workspaceId ?? 0,
          publicProperties: resultPublicProps,
        } as PublicContextItem);
      }
    });

    return {
      type: 'update',
      diff,
    };
  };

  private getApiCallback<T extends ApiControllerKeys, R extends CollapseDiffStateType>(
    mainType: T,
    type: R,
  ): ApiCallBack<T, R> | undefined {
    if (!this.apiController[mainType as keyof ApiController]) {
      return undefined;
    }
    return this.apiController[mainType as keyof ApiController][type as CollapseDiffStateType] as ApiCallBack<T, R>;
  }
}

export const stateDiffController = new StateDiffController();
















import debounce from 'lodash/debounce';

import { initApiManagers } from '@api/core';

import { initModalServices } from '@modules/ModalService/initModalCore';

import { stateDiffController } from './controllers/StateDiff';
import { setupBookbuilderStore } from './bookbuilder/setupStore'; // не менять порядок импортов
import { setupStore } from './setupStore';

import type { AppStore as AppStoreTraderadar , RootState as RootStateTraderadar } from './setupStore';

export type AppStore = AppStoreTraderadar;
export type RootState = RootStateTraderadar;

let store: AppStore;

export enum STORE_TYPE {
  BOOKBUILDER = 'BOOKBUILDER',
  TRADERADAR = 'TRADERADAR',
}

export const initStoreForTests = (preloadedState?: Partial<RootState>) => { // инициализация стора только для тестов
  store = setupStore(preloadedState);
  return store;
}

export const initStore = (type: STORE_TYPE = STORE_TYPE.TRADERADAR) => {
  if (!store) {
    switch(type) {
      case STORE_TYPE.BOOKBUILDER: {
        store = setupBookbuilderStore() as AppStore;
        break;
      }
      case STORE_TYPE.TRADERADAR: {
        store = setupStore();
        break;
      }
      default:
        return;
    }
    stateDiffController.init(store.getState());

    const saveState = debounce(() => stateDiffController.getDiff(store.getState()).saveDiff(), 200);

    store.subscribe(() => {
      saveState();
    });

    initApiManagers(store);
    initModalServices(store.dispatch);
  }
}

export const getStore = () => store


















import React, { FC, lazy, Suspense } from 'react';
import { Provider } from 'react-redux';

import { getStore, initStore, STORE_TYPE } from '@store/store';

import { useAuthFacade } from './useAuthFacade';

import '@components/ComplexContextMenu/dropdown.scss';

import type { AuthStatusResponse } from 'types/Auth';

const Terminal = lazy(() => import('./terminal'));

type AuthProps = { initAuthData: AuthStatusResponse };

initStore(STORE_TYPE.TRADERADAR); // инициализация стора для traderadar
// Из-за большого количества ссылок на этом этапе грузится достаточно большой бандл
// TODO: отрефакторить
const AuthWithStore: FC<AuthProps> = ({ initAuthData }) => {
  const isAuthenticated = useAuthFacade(initAuthData);

  if (!isAuthenticated) {
    return;
  }

  return (
    <Suspense>
      <Terminal />
    </Suspense>
  );
};

const Auth: FC<AuthProps> = (props) => (
  <Provider store={getStore()}>
    <AuthWithStore {...props} />
  </Provider>
);

export default Auth;


















import React, { FC, lazy, Suspense, useEffect } from 'react';
import { Provider } from 'react-redux';
import { Route, Routes } from 'react-router-dom';



import { getStore, initStore, STORE_TYPE } from '@store/store';
import { BookbuilderAuthErrorScreen } from '@terminal/components/BookbuilderAuthErrorScreen/BookbuilderAuthErrorScreen';
import { IframeLoadingScreen } from '@terminal/components/IframeLoadingScreen';
import { useTerminalFacade } from '@terminal/hooks/useTerminalFacade';
import { ChatUIConfigProvider } from '@widgets/NoTradeChat/context/ChatUIConfig';

import { useBookbuilderAuthFacade } from './useBookbuilderAuthFacade';


const BookbuilderTerminal = lazy(() => import('./terminal/Bookbuilder'));
const BookBuilderCreateChatTerminal = lazy(() => import('./terminal/BookBuilderCreateChat'));

initStore(STORE_TYPE.BOOKBUILDER); // инициализация стора для bookbuilder

const BookbuilderWithCriticalData: FC = () => {
  const { criticalDataIsReady } = useTerminalFacade();

  if (!criticalDataIsReady) {
    return null;
  }

  return (
    <ChatUIConfigProvider isIframeView>
      <Routes>
        <Route
          path="create"
          element={(
          <Suspense>
            <BookBuilderCreateChatTerminal />
          </Suspense>)}
        />
        <Route
          path="*"
          element={(
          <Suspense>
            <BookbuilderTerminal />
          </Suspense>)}
        />
      </Routes>
    </ChatUIConfigProvider>
  );
};


const BookbuilderAuthWithStore: FC = () => {
  const { isReady, error } = useBookbuilderAuthFacade();
  
  useEffect(() => {
      document.documentElement.dataset.theme = 'bookBilder';
  }, []);

  if (error) {
    return <BookbuilderAuthErrorScreen />;
  }
  if (!isReady) {
    return <IframeLoadingScreen />;
  }

  return <BookbuilderWithCriticalData />;
};

const BookbuilderAuth: FC = () => (
  <Provider store={getStore()}>
    <BookbuilderAuthWithStore />
  </Provider>
);

export default BookbuilderAuth;