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


import api from '@api/index';
import { setWidgetParentWorkspaceId } from '@store/slices/widgets';
import { addWorkspace, 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(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 { 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 [];
  }

  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 { workspaceController } from '@api/controllers/workspace';

export const exportJSONFile = (item: number, name: string) => {
  workspaceController.export(item).then((response) => {
    const json = JSON.stringify(response.data);
    const blob = new Blob([json], { type: ' text/plain;charset=utf-8 ' });
    const link = document.createElement('a');
    link.href = window.URL.createObjectURL(blob);
    link.download = `${name}.json`;
    link.click();
  });
};


















import { useCallback } from 'react';

import { alertManager } from '@terminal/components/Alerts/alertManager';
import { useAppSelect } from '@hooks/useAppSelector';
import { userInfoSelector } from '@store/selectors/user';
import { workspacesNamesSelector } from '@store/selectors/workspaces';
import { createNameForNewWorkspace } from '@terminal/desktop/components/Header/utils';
import createWidget from '@utils/createWidget';
import createWorkspace from '@utils/createWorkspace';
import { deepClone } from '@utils/deepClone';
import { isRemovedWidget } from '@utils/isRemovedWidget';
import { unpackWidgetProps } from '@utils/widgetPropsProceeders';
import { convertPercentToPx } from '@utils/widgetSizesNPositionProceeders';
import { User } from 'types/User';
import { PackedPropsProperties, WidgetContentType } from 'types/Widgets';

type WidgetProperty = {
  key: string;
  value: string;
};

type Widget = {
  id: number | string;
  properties?: WidgetProperty[];
};

const getFilteredWidgets = (widgets: Widget[]) => {
  const hiddenWidgetTypes: Set<string> = new Set([
  ]);

  const isBannedWidget = (item: Widget) =>
    item.properties?.some((p) => p.key === 'type' && hiddenWidgetTypes.has(p.value));

  return widgets.filter((item) => !isBannedWidget(item) && !isRemovedWidget(item.properties));
};

export const useWorkspaceCreator = (externalUser: User | null) => {
  const workspacesNames = useAppSelect(workspacesNamesSelector);
  const userInfo = useAppSelect(userInfoSelector());

  const loadWorkspaceWidgets = useCallback(
    (workspace: JSON, widgetsArray: JSON, workspaceName?: string): void => {
      const workspaceImport = JSON.parse(JSON.stringify(workspace));
      const newWorkspaceName = createNameForNewWorkspace(workspaceName ?? workspaceImport.name, workspacesNames);
      const widgets: Widget[] = JSON.parse(JSON.stringify(widgetsArray));

      // TODO 11175 убрать после доработки торгового функционала
      const widgetsFiltered = getFilteredWidgets(widgets);

      const widgetsProtectedFlag = widgetsFiltered.some((widget: Widget) =>
        widget?.properties?.some(
          (prop: { key: string; value: string }) =>
            prop.key === 'type' && !(externalUser || userInfo)?.widgetTypes.includes(prop.value as WidgetContentType),
        ),
      );

      if (widgetsProtectedFlag) {
        alertManager.notify({
          message: `Импорт рабочего стола невозможен, так как в нем содержатся данные или функционал, 
          к которым у вас нет доступа.`,
          type: 'error',
          timeout: 5000,
        });
        return;
      }

      createWorkspace({
        name: newWorkspaceName,
        isNew: false,
      }).then(({ id }) => {
        const newWidgets = widgetsFiltered.map((item: Record<string, unknown>) => {
          const newWidget = deepClone(item);
          delete (newWidget as unknown as Record<string, unknown>).id;
          const unpackedWidget = unpackWidgetProps(
            convertPercentToPx({
              widgetId: newWidget.widgetId as number,
              properties: newWidget.properties as PackedPropsProperties[],
            }),
          );
          unpackedWidget.workspaceId = id;
          if (!unpackedWidget.isExpand && unpackedWidget.beforeExpandParams) {
            unpackedWidget.beforeExpandParams = null;
          }
          if (!unpackedWidget.widgetContentProps) {
            unpackedWidget.widgetContentProps = {};
          }
          // Удаляет id списка инструментов при импорте рабочего стола
          delete unpackedWidget?.widgetContentProps?.widgetQuotesState?.instrumentListId;

          return unpackedWidget;
        });
        createWidget(newWidgets);
      });
    },
    [externalUser, userInfo, workspacesNames],
  );

  return loadWorkspaceWidgets;
};