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


import type { StorageDataType } from './StorageData';
import type { SplitLayoutState } from '@terminal/desktop/workspaces/mesh/types';

export type SystemWorkspaceConfig =
  | 'currencyMarket'
  | 'negotiations'
  | 'bonds'
  | 'monitor'
  | 'monitor2'
  | 'stocks'
  | 'NTBStock';

export type WorkspaceType = 'Bonds' | 'Currency' | 'Talks' | 'Stocks' | 'NTBStock';

export interface Workspace {
  id: number;
  name: string;
  isCurrent: boolean;
  position: number;
  isAddWidgets: boolean;
  saved?: boolean;
  pinned?: boolean;
  isNew?: boolean;
  code?: string;
  favorite?: boolean;
  closedAt?: string;
  layout?: SplitLayoutState;
}

export interface WorkspaceState {
  workspaces: Workspace[];
  loadStates?: Partial<Record<Workspace['id'], WorkspaceLoadState>>;
}

export interface WorkspaceLoadState {
  status: 'loading' | 'loaded' | 'error';
  requestId: number | null;
  error: string | null;
}

export enum WorkspaceGroups {
  Currency = 'currency',
  Ntb = 'ntb',
  Spfi = 'spfi',
  Stocks = 'stocks',
  Bonds = 'bonds',
  Market = 'market',
  Trading = 'trading',
  Cci = 'cci',
  DepositCcp = 'depositCcp',
}

export type LibraryWorkspace = {
  id: LibraryWorkspaceType;
  name: string;
  groups: WorkspaceGroups[];
};

/** Все типы столов, используемых в библиотеке шаблонов */
export type LibraryWorkspaceType =
  | WorkspaceType
  | 'corporateInformationCenter'
  | 'spfiScreen'
  | 'spfiView'
  | 'spfiBroker'
  | 'spfiScreenExtended'
  | 'bondsLargeScreen'
  | 'bondsComparison'
  | 'bondsMonitoringLargeScreen'
  | 'municipalBonds'
  | 'corporateBonds'
  | 'govermentBonds'
  | 'currencyBonds'
  | 'repoMidSizeScreen'
  | 'ntb'
  | 'ntbIndexes'
  | 'ntbLogistic'
  | 'dealingFunctionality'
  | 'marketOverview'
  | 'instrumentListsByAllMarkets'
  | 'stocksAndIndexes'
  | 'currencyMarket'
  | 'depositsCcp';

export type WorkspaceAutoCreateConfig = {
  checkPermission: (permissions: string[]) => boolean;
  workspaceJsonName: LibraryWorkspaceType;
  storageKey: keyof StorageDataType;
  featureFlag: boolean;
};


















import { createSlice } from '@reduxjs/toolkit';

import { CREATE_NEW_WORKSPACE_ID } from '@configs/appConfig';

import type { PayloadAction } from '@reduxjs/toolkit';
import type { Workspace, WorkspaceState } from 'types/Workspace';

const initialState: WorkspaceState = {
  workspaces: [],
  loadStates: {},
};

type WorkspaceChangePayload<T extends Exclude<keyof Workspace, 'id'>> = Pick<Workspace, 'id' | T>;

const workspacesSlice = createSlice({
  name: 'workspace',
  // `createSlice` will infer the state type from the `initialState` argument
  initialState,
  reducers: {
    addWorkspace: (state, { payload }: PayloadAction<Workspace | Workspace[]>): void => {
      state.workspaces = state.workspaces.filter((workspace: Workspace) => workspace.id !== -1);
      if ((payload as Workspace[]).length && (payload as Workspace[]).length > 0) {
        state.workspaces = [...(payload as Workspace[])];
      } else {
        state.workspaces.push(payload as Workspace);
      }
    },
    removeWorkspace: (state, { payload }: PayloadAction<Workspace['id']>): void => {
      state.workspaces = state.workspaces.filter((workspace: Workspace) => workspace.id !== payload);
      if (state.loadStates) {
        delete state.loadStates[payload];
      }
    },
    setWorkspaceName: (state, { payload }: PayloadAction<WorkspaceChangePayload<'name'>>) => {
      const changedWorkspaceIndex = state.workspaces.findIndex(({ id }) => payload.id === id);
      state.workspaces[changedWorkspaceIndex].name = payload.name;
    },
    setCurrentWorkspace: (state, { payload }: PayloadAction<Workspace['id']>) => {
      if (payload !== CREATE_NEW_WORKSPACE_ID) {
        state.workspaces = state.workspaces.filter((workspace: Workspace) => workspace.id !== CREATE_NEW_WORKSPACE_ID);
      }
      state.workspaces = state.workspaces.map((item) => ({
        ...item,
        isCurrent: payload === item.id,
      }));
    },
    togglePinWorkspace: (state, { payload }: PayloadAction<WorkspaceChangePayload<never>>) => {
      const changedWorkspace = state.workspaces.find(({ id }) => payload.id === id);
      if (changedWorkspace) {
        changedWorkspace.pinned = !changedWorkspace.pinned;
      }
    },
    setClosedWorkspace: (state, { payload }: PayloadAction<WorkspaceChangePayload<'saved'>>) => {
      const changedWorkspace = state.workspaces.find(({ id }) => payload.id === id);
      if (changedWorkspace) {
        changedWorkspace.saved = payload.saved;
      }
    },
    setWorkspaces: (state, { payload }: PayloadAction<Workspace[]>) => {
      state.workspaces = payload;
    },
    openWorkspace: (state, { payload }: PayloadAction<Workspace['id']>) => {
      const changedWorkspace = state.workspaces.find(({ id }) => payload === id);
      if (changedWorkspace) {
        changedWorkspace.saved = true;
      }
      state.workspaces = state.workspaces.map((item) => ({
        ...item,
        isCurrent: payload === item.id,
      }));
    },
    addToFavorites: (state, { payload }: PayloadAction<Partial<WorkspaceChangePayload<'name'>>>) => {
      const changedWorkspace = state.workspaces.find(({ id }) => id === payload.id);
      if (changedWorkspace) {
        changedWorkspace.favorite = true;
        if (payload.name) {
          changedWorkspace.name = payload.name;
        }
      }
    },
    removeFromFavorites: (state, { payload }: PayloadAction<Workspace['id']>) => {
      const changedWorkspace = state.workspaces.find(({ id }) => payload === id);
      if (changedWorkspace) {
        changedWorkspace.favorite = false;
      }
    },
    updateWorkspace: (state, { payload }: PayloadAction<Workspace>) => {
      const workspaceIndex = state.workspaces.findIndex((w) => w.id === payload.id);
      if (workspaceIndex !== -1) {
        state.workspaces[workspaceIndex] = payload;
      }
    },
    changeCurrentWorkspace: (state, { payload }: PayloadAction<Workspace['id']>) => {
      state.workspaces = state.workspaces.map((item) => ({
        ...item,
        isCurrent: payload === item.id,
      }));
      const workspace = state.workspaces.find((w) => payload === w.id);
      if (workspace) {
        workspace.saved = true;
      }
    },
    updateLayout: (
      state,
      { payload: { layout, id: payloadId } }: PayloadAction<Partial<WorkspaceChangePayload<'layout'>>>,
    ) => {
      const changedWorkspace = state.workspaces.find(({ id }) => id === payloadId);
      if (changedWorkspace && layout) {
        changedWorkspace.layout = layout;
      }
    },
    startWorkspaceLoading: (
      state,
      { payload }: PayloadAction<{ workspaceId: Workspace['id']; requestId: number }>,
    ) => {
      state.loadStates ??= {};
      state.loadStates[payload.workspaceId] = {
        status: 'loading',
        requestId: payload.requestId,
        error: null,
      };
    },
    completeWorkspaceLoading: (
      state,
      { payload }: PayloadAction<{ workspaceId: Workspace['id']; requestId: number }>,
    ) => {
      const loadState = state.loadStates?.[payload.workspaceId];
      if (loadState?.requestId === payload.requestId) {
        loadState.status = 'loaded';
        loadState.error = null;
      }
    },
    failWorkspaceLoading: (
      state,
      { payload }: PayloadAction<{ workspaceId: Workspace['id']; requestId: number; error: string }>,
    ) => {
      const loadState = state.loadStates?.[payload.workspaceId];
      if (loadState?.requestId === payload.requestId) {
        loadState.status = 'error';
        loadState.error = payload.error;
      }
    },
    markWorkspaceLoaded: (state, { payload }: PayloadAction<Workspace['id']>) => {
      state.loadStates ??= {};
      state.loadStates[payload] = {
        status: 'loaded',
        requestId: null,
        error: null,
      };
    },
  },
});

export const {
  addWorkspace,
  removeWorkspace,
  setCurrentWorkspace,
  setWorkspaceName,
  togglePinWorkspace,
  setClosedWorkspace,
  setWorkspaces,
  openWorkspace,
  addToFavorites,
  removeFromFavorites,
  updateWorkspace,
  changeCurrentWorkspace,
  updateLayout,
  startWorkspaceLoading,
  completeWorkspaceLoading,
  failWorkspaceLoading,
  markWorkspaceLoaded,
} = workspacesSlice.actions;

export default workspacesSlice.reducer;


















import { createSlice, PayloadAction } from '@reduxjs/toolkit';

export interface PublicContextItem {
  id: number;
  color?: string | null;
  publicProperties?: { name: string | null; value: string | number | null }[];
  name?: string | null;
  workspaceId?: number;
  isRemoval?: boolean;
}

interface publicContextState {
  publicContext: PublicContextItem[];
}

const initialState: publicContextState = {
  publicContext: [],
};

const publicContextSlice = createSlice({
  name: 'publicContext',
  initialState,
  reducers: {
    addValuesToPublicContext(state, { payload }: PayloadAction<PublicContextItem>) {
      const index = state.publicContext.findIndex((item) => item.id === payload.id);

      if (index === -1) {
        state.publicContext.push(payload);
      } else {
        state.publicContext[index] = { ...state.publicContext[index], ...payload };
      }
    },

    addValuesToPublicContextBatch(state, { payload }: PayloadAction<PublicContextItem[]>) {
      const items = new Map(state.publicContext.map((item) => [item.id, item]));

      payload.forEach((item) => {
        items.set(item.id, { ...items.get(item.id), ...item });
      });

      state.publicContext = Array.from(items.values());
    },

    createValuesToPublicContext: (state, { payload }: PayloadAction<PublicContextItem[]>) => {
      if (payload.length > 0) {
        state.publicContext = [...payload];
      } else {
        state.publicContext = [];
      }
    },
  },
});

export const { addValuesToPublicContext, addValuesToPublicContextBatch, createValuesToPublicContext } =
  publicContextSlice.actions;

export default publicContextSlice.reducer;


















import chunk from 'lodash/chunk';

import api from '@api/index';
import { CREATE_NEW_WORKSPACE_ID, WIDGET_PROPS_REQUEST_CHUNK_SIZE } from '@configs/appConfig';
import { stateDiffController } from '@store/controllers/StateDiff';
import { addValuesToPublicContextBatch } from '@store/slices/publicContext';
import { addWidget } from '@store/slices/widgets';
import {
  completeWorkspaceLoading,
  failWorkspaceLoading,
  startWorkspaceLoading,
} from '@store/slices/workspaces';
import { getStore } from '@store/store';
import { isRemovedWidget } from '@utils/isRemovedWidget';
import { robustRequest } from '@utils/robustRequest';
import { unpackWidgetProps } from '@utils/widgetPropsProceeders';
import { convertPercentToPx } from '@utils/widgetSizesNPositionProceeders';

import type { PublicContextItem } from '@store/slices/publicContext';
import type { AppStore } from '@store/store';
import type { Widget } from 'types/Widgets';
import type { Workspace } from 'types/Workspace';

type WorkspaceWidgetInfo = Pick<Widget, 'id' | 'name' | 'workspaceId'> &
  Pick<PublicContextItem, 'color' | 'isRemoval'> & {
    publicProperties?: Record<string, string> | string[];
  };

interface WorkspaceData {
  widgets: Widget[];
  publicContext: PublicContextItem[];
}

interface PendingRequest {
  requestId: number;
  promise: Promise<void>;
}

const pendingRequests = new WeakMap<AppStore, Map<Workspace['id'], PendingRequest>>();
let nextRequestId = 0;

export async function readWorkspacesData(workspaceIds: Workspace['id'][]): Promise<WorkspaceData> {
  const response = await robustRequest(() => api.widgetsController.read(workspaceIds));
  const widgetInfo = Object.values(response.data).flat() as WorkspaceWidgetInfo[];
  const widgetIds = Array.from(new Set(widgetInfo.map(({ id }) => id)));
  const chunks = chunk(widgetIds, WIDGET_PROPS_REQUEST_CHUNK_SIZE);

  const publicContext: PublicContextItem[] = widgetInfo.map(({ publicProperties, ...item }) => ({
    ...item,
    publicProperties: Object.values(publicProperties ?? {}).map(
      (value) => JSON.parse(value) as NonNullable<PublicContextItem['publicProperties']>[number],
    ),
  }));

  const properties = await Promise.all(
    chunks.map((ids) => robustRequest(() => api.widgetPropertiesController.read(ids))),
  );

  const widgets = properties
    .flatMap(({ data }) => data)
    .filter((item) => !isRemovedWidget(item.properties))
    .map((item) => {
      const widget = unpackWidgetProps(convertPercentToPx(item));

      if (!widget.isExpand && widget.beforeExpandParams) {
        widget.beforeExpandParams = null;
      }

      if (!widget.widgetContentProps) {
        widget.widgetContentProps = {};
      }

      return widget;
    });

  return { widgets, publicContext };
}

function isCurrentRequest(store: AppStore, workspaceId: Workspace['id'], requestId: number): boolean {
  const state = store.getState();
  const loadState = state.workspaces.loadStates?.[workspaceId];

  return (
    getStore() === store &&
    state.workspaces.workspaces.some(({ id }) => id === workspaceId) &&
    loadState?.status === 'loading' &&
    loadState.requestId === requestId
  );
}

async function loadWorkspace(store: AppStore, workspaceId: Workspace['id'], requestId: number): Promise<void> {
  try {
    if (!isCurrentRequest(store, workspaceId, requestId)) {
      throw new Error('Workspace loading is no longer current');
    }

    const data = await readWorkspacesData([workspaceId]);

    if (!isCurrentRequest(store, workspaceId, requestId)) {
      throw new Error('Workspace loading is no longer current');
    }

    const state = store.getState();
    const widgetIds = new Set(state.widgets.widgets.map(({ id }) => id));
    const publicContextIds = new Set(state.publicContext.publicContext.map(({ id }) => id));
    const widgets = data.widgets.filter(({ id }) => !widgetIds.has(id));
    const publicContext = data.publicContext.filter(({ id }) => !publicContextIds.has(id));

    stateDiffController.acceptLoadedData(widgets, publicContext);

    if (widgets.length > 0) {
      store.dispatch(addWidget(widgets));
    }

    if (publicContext.length > 0) {
      store.dispatch(addValuesToPublicContextBatch(publicContext));
    }

    store.dispatch(completeWorkspaceLoading({ workspaceId, requestId }));
  } catch (error) {
    if (isCurrentRequest(store, workspaceId, requestId)) {
      store.dispatch(
        failWorkspaceLoading({
          workspaceId,
          requestId,
          error: error instanceof Error ? error.message : 'Unable to load workspace',
        }),
      );
    }

    throw error;
  }
}

export function ensureWorkspaceLoaded(workspaceId: Workspace['id']): Promise<void> {
  const store = getStore();
  const state = store.getState();

  if (!state.workspaces.workspaces.some(({ id }) => id === workspaceId)) {
    return Promise.reject(new Error('Workspace does not exist'));
  }

  if (workspaceId === CREATE_NEW_WORKSPACE_ID || state.workspaces.loadStates?.[workspaceId]?.status === 'loaded') {
    return Promise.resolve();
  }

  const requests = pendingRequests.get(store) ?? new Map<Workspace['id'], PendingRequest>();

  pendingRequests.set(store, requests);

  const pending = requests.get(workspaceId);
  const loadState = state.workspaces.loadStates?.[workspaceId];

  if (pending && loadState?.status === 'loading' && loadState.requestId === pending.requestId) {
    return pending.promise;
  }

  nextRequestId += 1;
  const requestId = nextRequestId;

  store.dispatch(startWorkspaceLoading({ workspaceId, requestId }));

  const promise = Promise.resolve()
    .then(() => loadWorkspace(store, workspaceId, requestId))
    .finally(() => {
      if (requests.get(workspaceId)?.requestId === requestId) {
        requests.delete(workspaceId);
      }
    });

  requests.set(workspaceId, { requestId, promise });

  return promise;
}


















import { all, call, put, select } from 'typed-redux-saga';

import api from '@api/index';
import { CREATE_NEW_WORKSPACE_ID, TRADE_PERMISSION } from '@configs/appConfig';
import { permissionsSelector } from '@store/selectors/user';
import { currentWorkspaceIdSelector } from '@store/selectors/workspaces';
import { metricsAction } from '@store/slices/core';
import { createValuesToPublicContext } from '@store/slices/publicContext';
import { addWidget } from '@store/slices/widgets';
import { addWorkspace, markWorkspaceLoaded } from '@store/slices/workspaces';
import { alertManager } from '@terminal/components/Alerts/alertManager';
import { ensureWorkspaceLoaded, readWorkspacesData } from '@utils/loadWorkspaceWidgets';
import { robustRequest } from '@utils/robustRequest';
import { MetricsAction } from 'types/MetricsActions';
import { Workspace } from 'types/Workspace';

export function* loadCurrentWorkspaceSaga() {
  const workspaceId = yield* select(currentWorkspaceIdSelector);

  if (workspaceId === undefined || workspaceId === CREATE_NEW_WORKSPACE_ID) {
    return;
  }

  try {
    yield* call(ensureWorkspaceLoaded, workspaceId);
  } catch {
    const currentWorkspaceId = yield* select(currentWorkspaceIdSelector);

    if (currentWorkspaceId === workspaceId) {
      alertManager.notify({
        message: 'Не удалось загрузить рабочий стол. Попробуйте открыть его повторно.',
        type: 'error',
        timeout: 5000,
      });
    }
  }
}

export function* fullWorkspacesUploadSaga({ loadCurrentOnly = false }: { loadCurrentOnly?: boolean } = {}) {
  const permissions = yield* select(permissionsSelector);
  const isCanTrade = permissions.includes(TRADE_PERMISSION);

  const workspacesShortInfoResponse = yield* call(() => robustRequest(api.workspaceController.read));
  yield put(metricsAction(MetricsAction.LOADING_SCREEN_TIME_WORKSPACES));
  const workspaces: Workspace[] = workspacesShortInfoResponse.data;
  workspaces
    .sort((a, b) => a.position - b.position)
    .map(({ saved, pinned, code, ...workspace }) => ({
      saved: saved || false,
      pinned: pinned || false,
      code: code ?? null,
      ...workspace,
    }));
  if (workspaces.length > 0) {
    let workspacesForStore = workspaces;
    if (!isCanTrade) {
      workspacesForStore = workspaces.filter(({ name }) => name !== 'Переговоры');
    }
    yield put(addWorkspace(workspacesForStore));
  } else {
    return;
  }

  if (loadCurrentOnly) {
    yield* call(loadCurrentWorkspaceSaga);
    return;
  }

  const workspacesIdList = workspaces.map(({ id }) => id);

  // Получение подробной информации по виджетам
  // Предположительно можно выполнить параллельно с запросом на проперти
  const { widgets, publicContext } = yield* call(readWorkspacesData, workspacesIdList);

  yield put(createValuesToPublicContext(publicContext));

  if (widgets.length > 0) {
    yield put(addWidget(widgets));
  }

  yield* all(workspacesIdList.map((workspaceId) => put(markWorkspaceLoaded(workspaceId))));
}


















import { all, takeEvery, takeLeading } from 'typed-redux-saga';

import { requestLastReceivedMessages } from '@store/actions/chats';
import {
  initMetrics,
  initNotifications,
  metricsAction,
  metricTimer,
  requestAppCriticalData,
  requestDesktopCriticalData,
  requestMobileCriticalData,
} from '@store/slices/core';
import { changeCurrentWorkspace, openWorkspace, setCurrentWorkspace } from '@store/slices/workspaces';

import { initLastMessagesSaga } from '../chats/initChat';
import { initMetricsSaga, sendMetricsSaga, sendTimerMetricAction } from '../metrics';
import { initNotificationsSaga } from '../notifications';
import { loadCurrentWorkspaceSaga } from '../workspaces';

import { criticalDesktopReceiverSaga } from './desktop';
import { criticalMobileReceiverSaga } from './mobile';
import { criticalTerminalDataReceiverSaga } from './terminal';

export function* coreSagas() {
  yield all([
    takeLeading(initMetrics, initMetricsSaga),
    takeEvery(metricsAction, sendMetricsSaga),
    takeEvery(metricTimer, sendTimerMetricAction),
    takeLeading(requestAppCriticalData, criticalTerminalDataReceiverSaga),
    takeLeading(requestMobileCriticalData, criticalMobileReceiverSaga),
    takeLeading(requestDesktopCriticalData, criticalDesktopReceiverSaga),
    takeEvery(
      [setCurrentWorkspace.type, openWorkspace.type, changeCurrentWorkspace.type],
      loadCurrentWorkspaceSaga,
    ),
    takeLeading(initNotifications, initNotificationsSaga),
    takeLeading(requestLastReceivedMessages, initLastMessagesSaga),
  ]);
}


















import { all, call, fork, join, put, select } from 'typed-redux-saga';

import { instrumentListsController } from '@api/controllers/instrumentLists';
import { profileController } from '@api/controllers/profileController';
import api from '@api/index';
import { getIsSpfiTrader } from '@api/utils/getIsSpfiTrader';
import { wsEndpointStompClient } from '@api/websokets/classes/WSEndpointStompClient';
import { systemNotificationController } from '@api/websokets/systemNotification/systemNotificationController';

import { initStorageData } from '@store/sagas/initStorageData';
// import { addNSDCost } from '@store/slices/cashedData';
import { desktopCriticalDataReceived, metricsAction } from '@store/slices/core';
import { setFirms } from '@store/slices/firms';
import { setIntrumentLists } from '@store/slices/instrumentLists';
import { setAvatarUrl, setPersonalData, setPrivacySettings } from '@store/slices/user';
import { getStore, RootState } from '@store/store';

import { MetricsAction } from 'types/MetricsActions';

import { contractsRequestSaga } from '../basic/contracts';
import { newsInitSaga } from '../basic/news';
import { chatsCacheSyncSaga } from '../chats/initChat';
import { customersAndChatsSaga } from '../customersAndChats';
import { initChatAvatars } from '../initAvatars';
import { initSurveys } from '../surveys/init';
import { requestAndPut } from '../utils';
import { fullWorkspacesUploadSaga } from '../workspaces';

/** В этот момент пропадает экран загрузки и пользователь видит терминал */
export function* backgroundDesktopReceiverSaga() {
  yield* fork(async () => {
    try {
      const res = await profileController.getAvatar();
      getStore().dispatch(setAvatarUrl(res.data ? URL.createObjectURL(res.data) : null));
    } catch {
      getStore().dispatch(setAvatarUrl(null));
    }
  });

  yield* fork(newsInitSaga);
  yield* fork(initChatAvatars);

  yield* fork(initStorageData);
  yield* fork(chatsCacheSyncSaga);
  yield* fork(initSurveys);
}

export function* criticalDesktopReceiverSaga() {
  if (yield* select((state: RootState) => state.coreSlice.desktopCriticalDataReceived)) {
    return;
  }

  // Активируем соединение для большого количества виджетов
  wsEndpointStompClient.activate();

  // Активация контроллера системных уведомлений
  const isSpfiTrader = yield* select(getIsSpfiTrader);
  systemNotificationController.activate(isSpfiTrader);

  const customersAndChatsFork = yield* fork(customersAndChatsSaga);
  const workspacesFork = yield* fork(fullWorkspacesUploadSaga, { loadCurrentOnly: true });

  // eslint-disable-next-line func-names -- в сагах нормально
  const contractsAndTradeFork = yield* fork(function* () {
    yield call(contractsRequestSaga);
  });

  // eslint-disable-next-line func-names -- в сагах нормально
  const massRequestsFork = yield* fork(function* () {
    yield all([
      // Нужно в виджете контакты, входящий звонок, список переговоров
      requestAndPut(setFirms, api.getFirms, MetricsAction.LOADING_SCREEN_TIME_CATA_CACHE_FIRMS),
      // Отключено в рамках TRADERADAR-12666 Отключение источника НРД
      // requestAndPut(addNSDCost, api.getNSDCost, MetricsAction.LOADING_SCREEN_TIME_DATA_NSDCOST),
      requestAndPut(
        setIntrumentLists,
        instrumentListsController.getInstrumentLists,
        MetricsAction.LOADING_SCREEN_TIME_LISTINSTRUMENTS,
      ),
      // TODO: убрать отсюда когда появится статус загрузки
      requestAndPut(
        setPersonalData,
        profileController.getPersonalData,
        MetricsAction.LOADING_SCREEN_TIME_USERS_PERSONAL_DATA,
      ),
      // TODO: убрать отсюда когда появится статус загрузки
      requestAndPut(
        setPrivacySettings,
        profileController.getPrivacySettings,
        MetricsAction.LOADING_SCREEN_TIME_USERS_PRIVACY_SETTINGS,
      ),
    ]);
  });

  yield* join([customersAndChatsFork, workspacesFork, contractsAndTradeFork, massRequestsFork]);

  yield put(metricsAction(MetricsAction.LOADING_SCREEN_TIME_END));
  yield put(desktopCriticalDataReceived());
  yield* call(backgroundDesktopReceiverSaga);
}


















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 acceptLoadedData(widgets: Widget[], publicContext: PublicContextItem[]): void {
    const updateBaseline = (state: RootState): RootState => {
      if (!state.widgets || !state.publicContext) {
        return state;
      }

      const widgetIds = new Set(state.widgets.widgets.map(({ id }) => id));
      const publicContextIds = new Set(state.publicContext.publicContext.map(({ id }) => id));

      return {
        ...state,
        widgets: {
          ...state.widgets,
          widgets: [...state.widgets.widgets, ...widgets.filter(({ id }) => !widgetIds.has(id))],
        },
        publicContext: {
          ...state.publicContext,
          publicContext: [
            ...state.publicContext.publicContext,
            ...publicContext.filter(({ id }) => !publicContextIds.has(id)),
          ],
        },
      };
    };

    this.previosStoreState = updateBaseline(this.previosStoreState);
    this.prevState = updateBaseline(this.prevState);
  }

  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 api from '@api/index';
import { setWidgetParentWorkspaceId } from '@store/slices/widgets';
import { addWorkspace, markWorkspaceLoaded, setCurrentWorkspace } from '@store/slices/workspaces';
import { getStore } from '@store/store';

import type { RequiredWithPartial } from 'types/utilityTypes';
import type { Workspace } from 'types/Workspace';

type CreateWorkspaceArgs = RequiredWithPartial<Omit<Workspace, 'id' | 'isCurrent' | 'position'>, 'isAddWidgets'>;
type CreateWorkspaceResult = Pick<Workspace, 'id'>;

export default async function createWorkspace(params: CreateWorkspaceArgs): Promise<CreateWorkspaceResult> {
  const position = getStore().getState().workspaces.workspaces.length;
  const newWorkspace: Workspace = {
    ...params,
    id: 0,
    isCurrent: true,
    saved: true,
    pinned: false,
    position: getStore().getState().workspaces.workspaces.length,
    isNew: params.isNew ?? true,
    isAddWidgets: params.isAddWidgets ?? true,
  };

  const { workspaceController } = api;

  await workspaceController
    .create({
      name: params.name,
      position,
      isCurrent: true,
      isNew: params.isNew ?? true,
      isAddWidgets: params.isAddWidgets ?? true,
      favorite: false,
    })
    .then((res) => {
      const { id } = res.data;
      newWorkspace.id = id;
      getStore().dispatch(markWorkspaceLoaded(id));
      getStore().dispatch(addWorkspace(newWorkspace));
      getStore().dispatch(setCurrentWorkspace(id));
      const widgets = getStore()
        .getState()
        .widgets.widgets.filter(({ workspaceId }) => newWorkspace.id === workspaceId)
        .map((widget) => ({
          id: widget.id,
          workspaceId: id,
        }));
      if (widgets.length > 0) {
        getStore().dispatch(setWidgetParentWorkspaceId(widgets));
      }
    });

  return {
    id: newWorkspace.id,
  };
}


















import isNil from 'lodash/isNil';

import api from '@api/index';
import {
  BIND_ICON_COLORS,
  MOBILE_SIZE,
  WIDGET_DEFAULT_SIZE_LIMITS,
  WIDGET_DEFAULT_SIZES,
  Z_INDEX,
} from '@configs/appConfig';

import { widgetPropertiesMap } from '@modules/widgetProperties';
import { createChatWidget } from '@store/slices/chatSlice';
import { addValuesToPublicContext } from '@store/slices/publicContext';
import { setCreatedWidgetId } from '@store/slices/searchNews';
import { addWidget } from '@store/slices/widgets';
import { getStore } from '@store/store';

import { Widget, WidgetContentType } from 'types/Widgets';

import { getLimits } from '../widgets/utils/limits';
import { getWidgetSizes } from '../widgets/utils/sizes';

import { createBindMap } from './createBindMap';
import getDefaultPositionForWidget from './getDefaultPositionForWidget';
import { getIssKeyFromWidgets } from './getIssKeyFromWidgets';
import getWorkspaceSizes from './getWorkspaceSizes';
import { ensureWorkspaceLoaded } from './loadWorkspaceWidgets';
import { packWidgetProps } from './widgetPropsProceeders';
import { convertPxToPercents } from './widgetSizesNPositionProceeders';

import type { PartialWithRequired } from 'types/utilityTypes';

type CreateWidgetArgs = PartialWithRequired<Widget, 'workspaceId' | 'type'>;

type CreatedWidgetData = Pick<Widget, 'id' | 'name' | 'workspaceId'>;

type BuildNewWidgetsProps = {
  data: CreatedWidgetData[];
  items: CreateWidgetArgs[];
  basicWidgetZIndex: number;
  isMobileView: boolean;
  workspaceSizes: { width: number; height: number };
  center: boolean;
};

function getBasicWidgetZIndex(items: CreateWidgetArgs[]): number {
  const currentWidgets = getStore()
    .getState()
    .widgets.widgets.filter((item) => items[0]?.workspaceId === item?.workspaceId);

  return currentWidgets.length > 0
    ? currentWidgets.map((item) => item.zIndex).sort((a, b) => b - a)[0] + 1
    : Z_INDEX.WIDGET_BASE;
}

function isAddingWidgetsAllowed(workspaceId: Widget['workspaceId']): boolean {
  const workspace = getStore()
    .getState()
    .workspaces.workspaces.find(({ id }) => id === workspaceId);

  return Boolean(workspace?.isAddWidgets);
}

function isWidgetsLimitExceeded(workspaceId: Widget['workspaceId']): boolean {
  const widgetsOnWorkspace = getStore()
    .getState()
    .widgets.widgets.filter((widget) => widget.workspaceId === workspaceId);

  return widgetsOnWorkspace.length > 49;
}

function buildNewWidgets({
  data,
  items,
  basicWidgetZIndex,
  isMobileView,
  workspaceSizes,
  center,
}: BuildNewWidgetsProps): Widget[] {
  return data.map(({ id, name, workspaceId }, index) => {
    const argItem = items[index];
    const newWidgetZIndex = basicWidgetZIndex + 1 + index;
    const widgetSizes = getWidgetSizes(argItem.type, isMobileView, workspaceSizes);
    return {
      ...argItem,
      id,
      name,
      workspaceId,
      type: argItem.type,
      sizes: argItem.sizes ?? widgetSizes ?? WIDGET_DEFAULT_SIZES,
      sizeLimits: argItem.sizeLimits || (getLimits(argItem.type) ?? WIDGET_DEFAULT_SIZE_LIMITS),
      zIndex: argItem.zIndex ?? newWidgetZIndex,
      position:
        argItem.position ?? getDefaultPositionForWidget(widgetSizes ?? WIDGET_DEFAULT_SIZES, workspaceId, center),
      widgetContentProps: argItem.widgetContentProps ?? widgetPropertiesMap[argItem.type] ?? {},
      isExpand: argItem.isExpand ?? false,
      beforeExpandParams: argItem.beforeExpandParams ?? null,
    };
  });
}

function createOldToNewWidgetIdMap(items: CreateWidgetArgs[], newWidgets: Widget[]): Record<number, number> {
  const oldToNewWidgetIdMap: Record<number, number> = {};

  // Что-то страшное. Если количество не совпадает то не можем смаппить
  if (newWidgets.length === items.length) {
    newWidgets.forEach((widget, index) => {
      const oldId = items[index].id;
      if (typeof oldId !== 'undefined') {
        oldToNewWidgetIdMap[oldId] = widget.id;
      }
    });
  }

  return oldToNewWidgetIdMap;
}

function updateWidgetMasters(widgets: Widget[], oldToNewWidgetIdMap: Record<number, number>): Widget[] {
  return widgets.map((widget) => {
    const master = isNil(widget.master) ? widget.master : (oldToNewWidgetIdMap[widget.master] ?? widget.master);
    const masters = isNil(widget.masters) ? widget.masters : widget.masters?.map((m) => oldToNewWidgetIdMap[m] ?? m);
    return {
      ...widget,
      master: master || null,
      masters: masters || [],
    };
  });
}

function dispatchCreatedWidgetSideEffects(newWidgets: Widget[]): void {
  const firstWidget = newWidgets[0];
  // Запись этого ID должна быть максильно близко к записи виджетов в стор
  if (firstWidget.type === WidgetContentType.news) {
    getStore().dispatch(setCreatedWidgetId(firstWidget.id));
  }
  if (firstWidget.type === WidgetContentType.noTradeChat) {
    getStore().dispatch(createChatWidget(String(firstWidget.id)));
  }
}

function updateWidgetPropertiesAndPublicContext(newWidgets: Widget[]): void {
  const { widgetPropertiesController } = api;
  const bindMap = createBindMap(newWidgets);
  let masterIndex = 0;
  // Создать новые связки убрав предыдущие
  // diff контроллер к этому моменту ещё не прошёлся по новыми виджетам
  // И добавление пропертей ничего не даст. Плюс он не анализирует добавление виджетов
  newWidgets.forEach((widget) => {
    const { properties } = packWidgetProps({
      master: widget.master,
      masters: widget.masters,
    });
    widgetPropertiesController.update({
      widgetId: widget.id,
      properties,
    });

    const masterPublicContext = getStore()
      .getState()
      .publicContext.publicContext?.find(({ id }) => id === widget.id);
    if (!widget.masters?.length && !masterPublicContext && Object.keys(bindMap).includes(widget.id.toString())) {
      const instrumentId = getIssKeyFromWidgets(widget);
      getStore().dispatch(
        addValuesToPublicContext({
          id: widget.id,
          color: BIND_ICON_COLORS[masterIndex],
          publicProperties: [{ name: 'instrId', value: instrumentId }],
        }),
      );
      masterIndex += 1;
    }
  });
}

// Фактически результат нигде не используется
export default async function createWidget(items: CreateWidgetArgs[], center = false): Promise<Widget[]> {
  const currentWorkspaceId = items.map(({ workspaceId }) => workspaceId)[0];

  if (!isAddingWidgetsAllowed(currentWorkspaceId)) {
    return [];
  }

  const workspaceIds = Array.from(new Set(items.map(({ workspaceId }) => workspaceId)));

  await Promise.all(workspaceIds.map((workspaceId) => ensureWorkspaceLoaded(workspaceId)));

  if (!isAddingWidgetsAllowed(currentWorkspaceId)) {
    return [];
  }

  if (isWidgetsLimitExceeded(currentWorkspaceId)) {
    // eslint-disable-next-line no-alert -- нужный alert
    alert('Виджетов может быть максимум 50 на одной странице!');
    return [];
  }

  const isMobileView = window.screen.width <= MOBILE_SIZE || window.innerWidth <= MOBILE_SIZE;
  const workspaceSizes = getWorkspaceSizes();
  const { widgetsController, widgetPropertiesController } = api;
  const basicWidgetZIndex = getBasicWidgetZIndex(items);

  const res = await widgetsController.create(
    items.map(({ name, workspaceId }) => ({
      workspaceId,
      name: name ?? '',
    })),
  );

  if (!res.data) {
    return [];
  }

  const newWidgets = buildNewWidgets({
    data: res.data,
    items,
    basicWidgetZIndex,
    isMobileView,
    workspaceSizes,
    center,
  });

  await widgetPropertiesController.create(newWidgets.map((widget) => convertPxToPercents(packWidgetProps(widget))));

  const oldToNewWidgetIdMap = createOldToNewWidgetIdMap(items, newWidgets);
  const newWidgetsWithUpdatedMasters = updateWidgetMasters(newWidgets, oldToNewWidgetIdMap);

  dispatchCreatedWidgetSideEffects(newWidgetsWithUpdatedMasters);
  getStore().dispatch(addWidget(newWidgetsWithUpdatedMasters));
  updateWidgetPropertiesAndPublicContext(newWidgetsWithUpdatedMasters);

  return newWidgetsWithUpdatedMasters;
}


















import React, { FC, PropsWithChildren, useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';

import { MAX_FAVORITE_WORKSPACES } from '@configs/appConfig';
import { useAppSelect } from '@hooks/useAppSelector';
import { isFavoritesLimitExceedSelector } from '@store/selectors/modals';
import { workspaceByIdSelector } from '@store/selectors/workspaces';
import { openSaveWorkspaceModal } from '@store/slices/modals';
import { togglePinWorkspace } from '@store/slices/workspaces';
import { getStore } from '@store/store';
import { createNameForNewWorkspace } from '@terminal/desktop/components/Header/utils';
import { useWorkspaceActions } from '@terminal/hooks/useWorkspaceActions';

import { toasts } from '@terminal/services/toasts';
import { exportJSONFile } from '@terminal/utils/exportJSONFile';
import { ContextMenu } from '@uikit/ContextMenu';
import { ContextMenuItem } from '@uikit/ContextMenu/types';
import createWidget from '@utils/createWidget';
import createWorkspace from '@utils/createWorkspace';
import { deepClone } from '@utils/deepClone';
import { ensureWorkspaceLoaded } from '@utils/loadWorkspaceWidgets';

import { SaveWorkspaceActionType } from '../SaveWorkspaceModal/types';

import { WorkspaceMenuItemsKeys } from './const';

import type { Widget } from 'types/Widgets';
import type { Workspace } from 'types/Workspace';

export type WorkspaceContextMenuProps = PropsWithChildren<{
  workspaceId: Workspace['id'];
  disabled?: boolean;
  onOpenChange?: (open: boolean) => void;
  hiddenItems?: WorkspaceMenuItemsKeys[];
  open?: boolean;
}>;

export const WorkspaceContextMenu: FC<WorkspaceContextMenuProps> = function ({
  workspaceId,
  disabled,
  onOpenChange,
  children,
  hiddenItems,
  open,
}): JSX.Element | null {
  const workspace = useAppSelect(workspaceByIdSelector(workspaceId));
  const workspacesNames = useAppSelect((state) => state.workspaces.workspaces).map((item) => item.name);
  const dispatch = useDispatch();

  const isFavoritesLimitExceed = useAppSelect(isFavoritesLimitExceedSelector(MAX_FAVORITE_WORKSPACES));
  const { deleteWorkspace, closeWorkspace, removeWorkspaceFormFavorites } = useWorkspaceActions();

  const name = workspace?.name ?? '';
  const code = workspace?.code;
  const pinned = workspace?.pinned ?? false;
  const favorite = workspace?.favorite ?? false;

  const onDuplicateBtnClick = useCallback(async () => {
    const newWorkspaceName = createNameForNewWorkspace(name, workspacesNames);

    try {
      await ensureWorkspaceLoaded(workspaceId);

      const widgets = getStore()
        .getState()
        .widgets.widgets.filter((widget) => widget.workspaceId === workspaceId);

      const { id } = await createWorkspace({
        name: newWorkspaceName,
        isNew: false,
      });

      const newWidgets = widgets.map((item) => {
        const newWidget = deepClone(item) as Omit<Widget, 'id'> & Partial<Pick<Widget, 'id'>>;
        delete newWidget.id;
        newWidget.workspaceId = id;
        // Удаляет id списка инструментов при дублировании рабочего стола
        delete newWidget?.widgetContentProps?.widgetQuotesState?.instrumentListId;
        return newWidget;
      });

      await createWidget(newWidgets);
      toasts.duplicateWorkspace.success(newWorkspaceName);
    } catch {
      toasts.duplicateWorkspace.error(newWorkspaceName);
    }
  }, [workspaceId, name, workspacesNames]);

  const onPinnedWorkspaceClick = useCallback(() => {
    dispatch(togglePinWorkspace({ id: workspaceId }));
  }, [dispatch, workspaceId]);

  const addToFavorites = useCallback(() => {
    dispatch(openSaveWorkspaceModal({ workspaceId, type: SaveWorkspaceActionType.Favorite }));
  }, [dispatch, workspaceId]);

  const removeFromFavorites = useCallback(() => {
    removeWorkspaceFormFavorites(workspaceId);
  }, [removeWorkspaceFormFavorites, workspaceId]);

  const renameWorkspace = useCallback(() => {
    dispatch(openSaveWorkspaceModal({ workspaceId, type: SaveWorkspaceActionType.Rename }));
  }, [dispatch, workspaceId]);

  const menuItems: ContextMenuItem[] = useMemo(
    () =>
      [
        !code && {
          key: WorkspaceMenuItemsKeys.Rename,
          label: 'Редактировать название',
          onClick: renameWorkspace,
        },
        { key: WorkspaceMenuItemsKeys.Copy, label: 'Дублировать вкладку', onClick: onDuplicateBtnClick },
        !code && {
          key: WorkspaceMenuItemsKeys.Pin,
          label: pinned ? 'Открепить вкладку' : 'Закрепить вкладку',
          onClick: onPinnedWorkspaceClick,
        },
        {
          key: WorkspaceMenuItemsKeys.Favorite,
          label: favorite ? 'Удалить из избранных' : 'Добавить в избранные',
          onClick: favorite ? removeFromFavorites : addToFavorites,
          disabled: !favorite && isFavoritesLimitExceed,
        },
        {
          key: WorkspaceMenuItemsKeys.Delete,
          label: 'Удалить рабочий стол',
          onClick: () => deleteWorkspace(workspaceId),
        },
        {
          key: WorkspaceMenuItemsKeys.Export,
          label: 'Выгрузить рабочий стол в файл',
          onClick: () => exportJSONFile(workspaceId, name),
        },
        {
          key: WorkspaceMenuItemsKeys.Close,
          label: 'Закрыть',
          onClick: () => closeWorkspace?.(workspaceId),
        },
      ]
        .filter((item) => !!item)
        .filter((item) => !hiddenItems?.includes(item.key)),
    [
      addToFavorites,
      closeWorkspace,
      code,
      deleteWorkspace,
      favorite,
      hiddenItems,
      isFavoritesLimitExceed,
      name,
      onDuplicateBtnClick,
      onPinnedWorkspaceClick,
      pinned,
      removeFromFavorites,
      renameWorkspace,
      workspaceId,
    ],
  );

  if (!workspace) {
    return null;
  }

  return (
    <ContextMenu
      onOpenChange={onOpenChange}
      items={menuItems}
      disabled={disabled}
      zIndex={10001}
      open={open}
    >
      {children}
    </ContextMenu>
  );
};