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


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 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 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 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 { all, call, put, select } from 'typed-redux-saga';

import api from '@api/index';
import { TRADE_PERMISSION } from '@configs/appConfig';
import { permissionsSelector } from '@store/selectors/user';
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 { readWorkspacesData } from '@utils/loadWorkspaceWidgets';
import { robustRequest } from '@utils/robustRequest';
import { MetricsAction } from 'types/MetricsActions';
import { Workspace } from 'types/Workspace';

export function* fullWorkspacesUploadSaga() {
  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;
  }

  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))));
}