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


import { useCallback } from 'react';
import { useDispatch } from 'react-redux';

import { workspaceController } from '@api/controllers/workspace';
import { CREATE_NEW_WORKSPACE_ID, DEFAULT_NEGOTIATION_WORKSPACE } from '@configs/appConfig';
import { useAppSelect } from '@hooks/useAppSelector';
import { widgetsSelector } from '@store/selectors/widgets';
import { currentWorkspaceIdSelector, workspacesSelector } from '@store/selectors/workspaces';
import { removeWidget } from '@store/slices/widgets';
import {
  addToFavorites,
  addWorkspace,
  removeFromFavorites,
  removeWorkspace,
  setClosedWorkspace,
  setCurrentWorkspace,
  setWorkspaceName,
  updateWorkspace,
} from '@store/slices/workspaces';
import { Workspace } from 'types/Workspace';

import { toasts } from '../services/toasts';

export const useWorkspaceActions = () => {
  const dispatch = useDispatch();
  const widgets = useAppSelect(widgetsSelector);
  const currentWorkspaceId = useAppSelect(currentWorkspaceIdSelector);
  const workspaces = useAppSelect(workspacesSelector);

  const deleteWorkspace = useCallback(
    async (id: number): Promise<void> => {
      if (id === currentWorkspaceId) {
        const openWorkspaces = workspaces.filter((elem) => elem.id !== DEFAULT_NEGOTIATION_WORKSPACE && elem.saved);
        const removedWorkspaceIndex = openWorkspaces.findIndex((workspace) => id === workspace.id);
        const previosWorkspace = openWorkspaces[removedWorkspaceIndex - 1];
        const nextWorkspace = openWorkspaces[removedWorkspaceIndex + 1];
        if (previosWorkspace) {
          dispatch(setCurrentWorkspace(previosWorkspace.id));
          await workspaceController.update({ ...previosWorkspace, isCurrent: true });
        } else if (nextWorkspace) {
          dispatch(setCurrentWorkspace(nextWorkspace.id));
          await workspaceController.update({ ...nextWorkspace, isCurrent: true });
        }
      }
      dispatch(removeWorkspace(id));
      dispatch(removeWidget(widgets.filter(({ workspaceId }) => workspaceId === id).map((widget) => widget.id)));
    },
    [currentWorkspaceId, dispatch, widgets, workspaces],
  );

  const closeWorkspace = useCallback(
    async (id: number): Promise<void> => {
      const openWorkspaces = workspaces.filter((elem) => elem.id !== DEFAULT_NEGOTIATION_WORKSPACE && elem.saved);

      if (id === currentWorkspaceId) {
        const removedWorkspaceIndex = openWorkspaces.findIndex((workspace) => id === workspace.id);
        const previosWorkspace = openWorkspaces[removedWorkspaceIndex - 1];
        const nextWorkspace = openWorkspaces[removedWorkspaceIndex + 1];
        if (previosWorkspace) {
          dispatch(setCurrentWorkspace(previosWorkspace.id));
          await workspaceController.update({ ...previosWorkspace, isCurrent: true });
        } else if (nextWorkspace) {
          dispatch(setCurrentWorkspace(nextWorkspace.id));
          await workspaceController.update({ ...nextWorkspace, isCurrent: true });
        }
      }
      dispatch(setClosedWorkspace({ id, saved: false }));
      const workspace = workspaces.find((el) => el.id === id);
      if (workspace && workspace.id !== CREATE_NEW_WORKSPACE_ID) {
        try {
          const res = await workspaceController.update({ ...workspace, saved: false, isCurrent: false });
          if (res.data.id) {
            dispatch(updateWorkspace(res.data));
          }
        } catch {
          /** */
        }
      }
    },
    [currentWorkspaceId, dispatch, workspaces],
  );

  const makeNewWorkspace = useCallback((): void => {
    const newWorkspace: Workspace = {
      name: 'Новый рабочий стол',
      id: CREATE_NEW_WORKSPACE_ID,
      isCurrent: true,
      saved: true,
      pinned: false,
      position: workspaces.length,
      isNew: true,
      isAddWidgets: true,
    };
    dispatch(addWorkspace(newWorkspace));
    dispatch(setCurrentWorkspace(CREATE_NEW_WORKSPACE_ID));
  }, [dispatch, workspaces.length]);

  const renameWorkspace = useCallback(
    async (id: number, name: string) => {
      const workspace = workspaces.find((w) => w.id === id);
      if (workspace) {
        try {
          await workspaceController.update({ ...workspace, name });
          dispatch(setWorkspaceName({ id, name }));
          toasts.renameWrokspace.success();
        } catch (error) {
          toasts.renameWrokspace.error();
        }
      }
    },
    [dispatch, workspaces],
  );

  const addWorkspaceToFavorites = useCallback(
    async (id: number, name: string) => {
      const workspace = workspaces.find((w) => w.id === id);
      if (workspace) {
        try {
          await workspaceController.update({ ...workspace, name, favorite: true });
          dispatch(addToFavorites({ id, name }));
          toasts.addToFavorites.success(name);
        } catch (error) {
          toasts.addToFavorites.error(name);
        }
      }
    },
    [dispatch, workspaces],
  );

  const removeWorkspaceFormFavorites = useCallback(
    (id: number) => {
      dispatch(removeFromFavorites(id));
    },
    [dispatch],
  );

  return {
    deleteWorkspace,
    closeWorkspace,
    makeNewWorkspace,
    renameWorkspace,
    addWorkspaceToFavorites,
    removeWorkspaceFormFavorites,
  };
};

















import { DragEndEvent } from '@dnd-kit/core';
import { arrayMove } from '@dnd-kit/sortable';
import { useCallback, useMemo, useState } from 'react';
import { useDispatch } from 'react-redux';

import { workspaceController } from '@api/controllers/workspace';
import { CREATE_NEW_WORKSPACE_ID, DEFAULT_NEGOTIATION_WORKSPACE } from '@configs/appConfig';
import { useAppSelect } from '@hooks/useAppSelector';
import { useLocalisation } from '@hooks/useLocalisation';
import { changeCurrentWorkspace, removeWorkspace, setWorkspaces } from '@store/slices/workspaces';
import { useSaveProfileConfirm } from '@terminal/hooks/useSaveProfileConfirm';
import { useWorkspaceActions } from '@terminal/hooks/useWorkspaceActions';

type UseWorkspaceTabParams = {
  onClick?: (id: number) => void;
};

export const useWorkspaceTabs = ({ onClick }: UseWorkspaceTabParams) => {
  const [hoveredIndex, setHoveredIndex] = useState<number>(-1);
  const dispatch = useDispatch();
  const workspacesFromStore = useAppSelect((state) => state.workspaces.workspaces);
  const currentWorkspace = useAppSelect((state) => state.workspaces.workspaces?.find((item) => !!item.isCurrent));

  const { makeNewWorkspace } = useWorkspaceActions();

  const { confirm } = useSaveProfileConfirm();

  const localisation = useLocalisation();

  const workspaces = useMemo(
    () => workspacesFromStore.filter((w) => w.saved && w.name !== localisation.helpCenter),
    [localisation.helpCenter, workspacesFromStore],
  );

  const workspaceTabs = useMemo(() => {
    const editableWorkspaces = workspaces.filter((elem) => elem.id !== DEFAULT_NEGOTIATION_WORKSPACE);
    const pinnedWorkspaces = editableWorkspaces.filter((elem) => elem.pinned);
    const alsoWorkspaces = editableWorkspaces.filter((elem) => !elem.pinned).sort((a, b) => a.position - b.position);

    // Элементы для отображения во вкладках
    return [...pinnedWorkspaces, ...alsoWorkspaces];
  }, [workspaces]);

  const defaultWorkspace = workspaces.find((elem) => elem.id === DEFAULT_NEGOTIATION_WORKSPACE);

  const handleClickTab = useCallback(
    async (id?: number) => {
      const findedWorkspace = workspaces.find((el) => el.id === id);
      if (id && currentWorkspace && findedWorkspace?.id) {
        dispatch(changeCurrentWorkspace(id));
        // Если переходим со стартовой страницы, то после перехода закрываем ее с задержкой,
        // чтобы правильно обработать двойной клик по вкладке
        if (currentWorkspace.id === CREATE_NEW_WORKSPACE_ID) {
          setTimeout(() => dispatch(removeWorkspace(CREATE_NEW_WORKSPACE_ID)), 300);
        }
        onClick?.(id);
        await Promise.all([
          currentWorkspace.id !== CREATE_NEW_WORKSPACE_ID &&
            workspaceController.update({ ...currentWorkspace, isCurrent: false }),
          workspaceController.update({ ...findedWorkspace, isCurrent: true }),
        ]);
      }
    },
    [workspaces, currentWorkspace, dispatch, onClick],
  );

  const moveWorkspaceTab = useCallback(
    (event: DragEndEvent) => {
      const { active, over } = event;

      if (active.id !== over?.id) {
        const oldIndex = workspacesFromStore.findIndex((w) => w.id === active.id);
        const newIndex = workspacesFromStore.findIndex((w) => w.id === over?.id);
        const movedWorkspaces = arrayMove(workspacesFromStore, oldIndex, newIndex);

        const editableWorkspaces = movedWorkspaces.filter((w) => w.id !== DEFAULT_NEGOTIATION_WORKSPACE);
        const pinnedWorkspaces = editableWorkspaces.filter((w) => w.pinned);
        const otherWorkspaces = editableWorkspaces.filter((w) => !w.pinned);
        const reorderedWorkspaces = [...pinnedWorkspaces, ...otherWorkspaces].map((w, i) => ({
          ...w,
          position: i,
        }));
        const newWorkspaces = defaultWorkspace ? [defaultWorkspace, ...reorderedWorkspaces] : reorderedWorkspaces;
        dispatch(setWorkspaces(newWorkspaces));
      }
    },
    [defaultWorkspace, dispatch, workspacesFromStore],
  );

  const handlePlusBtnClick = async () => {
    const confirmed = await confirm();
    if (confirmed) {
      makeNewWorkspace();
    }
  };

  const showPlusBtn = workspaces.findIndex((elem) => elem.isNew) === -1;

  return {
    currentWorkspace,
    workspaceTabs,
    defaultWorkspace,
    showPlusBtn,
    moveWorkspaceTab,
    handleClickTab,
    hoveredIndex,
    changeHoveredIndex: setHoveredIndex,
    handlePlusBtnClick,
  };
};


















import classNames from 'classnames';
import React, { useEffect } from 'react';

import { IconButton } from '@components/IconButton';
import { ChevronLeftRounded } from '@components/Icons/ChevronLeftRounded';
import { ChevronRightRounded } from '@components/Icons/ChevronRightRounded';

import { useWorkspaceActions } from '@terminal/hooks/useWorkspaceActions';

import { Button } from '@uikit/Button';

import { WorkspaceTab } from '../WorkspaceTab';

import { DndWrapper } from './components/DndWrapper';
import { useScroll } from './hooks/useScroll';
import { useWorkspaceTabs } from './hooks/useWorkspaceTabs';
import styles from './workspacePanel.module.scss';

// TODO: убрать, т.к. здесь он не используется, но импортируется в модуле NewsFeedsHeader
export type Coords = {
  left: number;
  top: number;
};

export const WorkspacePanel = () => {
  const { setTabRef, scroll, scrollRef, showLeft, showRight, handleWheel, isWheeling, scrollToTab } = useScroll();

  const { closeWorkspace } = useWorkspaceActions();

  const {
    currentWorkspace,
    workspaceTabs,
    defaultWorkspace,
    hoveredIndex,
    showPlusBtn,
    handleClickTab,
    moveWorkspaceTab,
    changeHoveredIndex,
    handlePlusBtnClick,
  } = useWorkspaceTabs({ onClick: scrollToTab });

  useEffect(() => {
    scrollToTab(currentWorkspace?.id);
  }, [scrollToTab, currentWorkspace?.id, currentWorkspace?.pinned]);

  return (
    <div
      className={styles.workspaceTabPanel}
      data-unit="workspace-panel"
      onWheel={handleWheel}
    >
      {showLeft && (
        <div className={classNames(styles.nav, styles['nav-left'])}>
          <IconButton
            size="large"
            icon={<ChevronLeftRounded />}
            onClick={() => scroll('left')}
            className={styles.navButton}
          />
        </div>
      )}
      <div
        className={styles.tabsContainer}
        ref={scrollRef}
      >
        {defaultWorkspace && (
          <WorkspaceTab
            id={defaultWorkspace?.id}
            name={defaultWorkspace?.name}
            index={-1}
            onClick={handleClickTab}
            selected={defaultWorkspace?.isCurrent}
            onCloseWorkspace={closeWorkspace}
            setTabRef={setTabRef}
          />
        )}
        <DndWrapper
          items={workspaceTabs}
          scrollContainerRef={scrollRef}
          onDragEnd={moveWorkspaceTab}
        >
          {workspaceTabs.map((elem, index, items) => (
            <WorkspaceTab
              key={elem.id}
              id={elem.id}
              name={elem.name}
              index={index}
              onClick={handleClickTab}
              editable
              selected={elem.isCurrent}
              onCloseWorkspace={closeWorkspace}
              pinned={elem.pinned}
              withDivider={
                index !== 0 &&
                !elem.isCurrent &&
                !items[index - 1].isCurrent &&
                index !== hoveredIndex &&
                index !== hoveredIndex + 1
              }
              onChangeHoverIndex={changeHoveredIndex}
              withTooltip={!isWheeling}
              setTabRef={setTabRef}
            />
          ))}
        </DndWrapper>
        {showPlusBtn && (
          <div className={classNames(styles.mover)}>
            <Button
              variant="unfilled-secondary"
              Icon="plus"
              iconEnabled="start"
              onClick={handlePlusBtnClick}
              className={styles.plusIcon}
            />
          </div>
        )}
      </div>
      {showRight && (
        <div className={classNames(styles.nav, styles['nav-right'])}>
          <IconButton
            size="large"
            icon={<ChevronRightRounded />}
            onClick={() => scroll('right')}
            className={styles.navButton}
          />
          {showPlusBtn && (
            <Button
              variant="unfilled-secondary"
              Icon="plus"
              iconEnabled="start"
              onClick={handlePlusBtnClick}
              className={styles.plusIcon}
            />
          )}
        </div>
      )}
    </div>
  );
};


















import React, { FC, forwardRef, useEffect, useRef } from 'react';

import { newsSettingsController } from '@api/controllers/newsController';
import { ChevronRight } from '@components/Icons/ChevronRight';
import { useDragInstrument } from '@hooks/dnd';
import { useAppSelect } from '@hooks/useAppSelector';
import { useLocalisation } from '@hooks/useLocalisation';
import { useManageSidebar } from '@hooks/useManageSidebar';
import { useValueAtPreviousRender } from '@hooks/usePrevious';
import { setNewsFeedSettings } from '@store/slices/newsSettings';
import { setOpenNewsId } from '@store/slices/newsSlice';
import { setClosedWorkspace, setCurrentWorkspace } from '@store/slices/workspaces';
import { getStore } from '@store/store';
import { highlight } from '@utils/highlight';
import createWidget from '@utils/createWidget';
import { newWorkspaceName } from '@utils/newWorkspaceName';
import { WidgetContentType } from 'types/Widgets';
import createWorkspace from '@utils/createWorkspace';

import { ItemListType } from '../type/type';

import styles from './itemList.module.scss';

export interface ItemListProps {
  setCurrentDivSource: React.Dispatch<React.SetStateAction<HTMLDivElement | undefined>>;
  currentDivSource: HTMLDivElement | undefined;
  shiftRight: boolean;
  className?: string;
  closeList?: () => void;
  selectItemHandler: (item: ItemListType | null) => void;
  itemProp: ItemListType;
}

export const ItemList: FC<ItemListProps> = forwardRef(
  ({
    selectItemHandler,
    className = '',
    currentDivSource,
    setCurrentDivSource,
    shiftRight,
    closeList,
    itemProp,
  }: ItemListProps): JSX.Element => {
    const { name, type, widget, findKey, workspace, instrument, instrumentProps, key: newsId } = itemProp;
    const refSource = useRef(null);
    const workspaceStore = useAppSelect((state) => state.workspaces);
    const newsSettings = useAppSelect((state) => state.newsSettings.settings);
    const isExtended = useAppSelect((state) => state.newsSettings.settings?.viewMode === 'EXTENDED');
    const workspaceId = workspaceStore.workspaces.find(({ isCurrent }) => isCurrent)?.id as number;
    const localisation = useLocalisation();
    const isCurrentWorkspaceNews: boolean = useAppSelect(
      (state) => !!state.workspaces.workspaces?.find((item) => item.name === localisation.news && item.isCurrent),
    );
    const currentWorkspace = useAppSelect((state) => state.workspaces.workspaces?.find((item) => item.isCurrent));
    const workspaceNews = useAppSelect((state) =>
      state.workspaces.workspaces?.find((item) => item.name === localisation.news && item.isAddWidgets === false),
    );

    async function findWorkspace() {
      if (workspaceId === -1) {
        const { id } = await createWorkspace({
          name: newWorkspaceName(localisation.newWorkspace, workspaceStore),
          isNew: false,
          pinned: false,
        });
        return id;
      }
      return workspaceId;
    }

    const onClickHandler = async () => {
      const avalibleWorkspace = workspaceStore.workspaces.find((item) => item.id === workspace);
      if (workspace) {
        if (avalibleWorkspace) {
          getStore().dispatch(setCurrentWorkspace(avalibleWorkspace.id));
        } else {
          // eslint-disable-next-line no-console
          console.warn('Workspace not avalibal');
        }
        if (closeList) {closeList();}
      } else if (widget) {
        const id = await findWorkspace();
        createWidget([
          {
            type: widget as WidgetContentType,
            workspaceId: id,
          },
        ]);
        if (closeList) {closeList();}
      } else if (type === 'news' && newsId) {
        if (!newsSettings) {
          (async () => {
            const { data } = await newsSettingsController.getSettings();
            getStore().dispatch(setNewsFeedSettings(data));
          })();
        }

        getStore().dispatch(setOpenNewsId(newsId));
        if (!isExtended) {
          if (!isCurrentWorkspaceNews && workspaceNews) {
            getStore().dispatch(setCurrentWorkspace(workspaceNews.id));
            getStore().dispatch(
              setClosedWorkspace({
                id: workspaceNews.id,
                saved: true,
              }),
            );
          } else if (!isCurrentWorkspaceNews) {
            createWorkspace({
              name: localisation.news,
              isAddWidgets: false,
              isNew: false,
            });
          }
        } else if (isCurrentWorkspaceNews) {
          createWorkspace({
            name: newWorkspaceName(localisation.newWorkspace, workspaceStore),
            isNew: false,
            pinned: false,
          }).then((res) => {
            createWidget([
              {
                workspaceId: res.id as unknown as number,
                type: WidgetContentType.news,
                position: { x: 0, y: 0 },
              },
            ]);
          });
        } else if (currentWorkspace?.id) {
          createWidget([
            {
              workspaceId: currentWorkspace.id,
              type: WidgetContentType.news,
            },
          ]);
        }
        if (closeList) {closeList();}
      } else {
        setCurrentDivSource(refSource.current as unknown as HTMLDivElement);
        selectItemHandler(itemProp);
      }
    };

    const { dragRef, opacity, isDragging } = useDragInstrument({
      ...instrumentProps,
    });
    const { close: closeSidebar } = useManageSidebar();

    // Закрытие списка если начато перетаскивание инструмента
    const previousRenderDraggingStatus = useValueAtPreviousRender(isDragging);
    useEffect(() => {
      // начато перетаскивание инструмента
      if (isDragging && !previousRenderDraggingStatus) {
        closeList?.();
        closeSidebar();
      }
    }, [isDragging, previousRenderDraggingStatus, closeList, closeSidebar]);

    return (
      <div
        className={styles.itemList}
        onClick={onClickHandler}
        ref={refSource}
      >
        <div
          style={{ opacity }}
          className={`${styles.wrapper} ${
            refSource.current === currentDivSource ? styles['wrapper-checked'] : ''
          } ${className} ${shiftRight ? styles['wrapper-shift'] : ''}`}
          ref={dragRef}
        >
          <div className={styles['wrapper-name']}>
            <div className={`${styles.name} ${type === 'news' ? styles['name-news'] : ''}`}>
              {highlight(name, findKey)}
            </div>
            {type !== 'news' && <div className={styles.type}>{highlight(type, findKey)}</div>}
          </div>
          {instrument && (
            <div className={styles['wrapper-instrument']}>
              <div className={styles.instrument}>{highlight(instrument, findKey)}</div>
              <div className={styles['chevron-icon']}>
                <ChevronRight />
              </div>
            </div>
          )}
          {itemProp.issuerId && (
            <div className={styles['wrapper-instrument']}>
              <div className={styles['chevron-icon']}>
                <ChevronRight />
              </div>
            </div>
          )}
        </div>
      </div>
    );
  },
);


















import React, { FC } from 'react';

import { workspaceController } from '@api/controllers/workspace';
import Icons from '@components/Icons';

import { DEFAULT_NEGOTIATION_WORKSPACE } from '@configs/appConfig';
import { useAppSelect } from '@hooks/useAppSelector';
import { useLocalisation } from '@hooks/useLocalisation';
import { setClosedWorkspace, setCurrentWorkspace } from '@store/slices/workspaces';
import { getStore } from '@store/store';

import { DropdownItems, WorkspaceItem } from '../MenuItems/WorkspaceItem';

const { LaptopChromebookIcon } = Icons;

const menuItemsTest = [
  {
    name: 'Мои рабочие столы',
    icon: <LaptopChromebookIcon style={{ color: '#C7C7D1' }} />,
    dropdownItems: [
      {
        name: 'Новый рабочий стол',
      },
    ],
  },
  // {
  //   name: "Рекомендуемые рабочие столы",
  //   icon: <LaptopChromebookIcon style={{ color: "#C7C7D1" }} />,
  //   dropdownItems: [
  //     {
  //       name: "Новости Prime",
  //     },
  //     {
  //       name: "Акции",
  //     },
  //     {
  //       name: "Облигации",
  //     },
  //     {
  //       name: "Валютный рынок",
  //     },
  //     {
  //       name: "Рынок метталлов",
  //     },
  //     {
  //       name: "Фьючерсы",
  //     },
  //   ],
  // },
];

interface WorkspaceMenuProps {
  selectedItem: string;
  setSelectedItem: React.Dispatch<React.SetStateAction<string>>;
}

export const WorkspaceMenu: FC<WorkspaceMenuProps> = function ({ selectedItem, setSelectedItem }) {
  const localisation = useLocalisation();
  const workspaces = useAppSelect((state) => state.workspaces.workspaces);
  const workspacesMenu: DropdownItems[] = workspaces
    .filter((w) => w.id !== DEFAULT_NEGOTIATION_WORKSPACE)
    // TRADERADAR-2032. Возможны проблемы при работе с английским языком
    .filter((w) => w.name !== localisation.helpCenter)
    .map((workspace) => ({
      id: workspace.id,
      name: workspace.name,
      onClick: async (id) => {
        if (id) {
          getStore().dispatch(setCurrentWorkspace(id));
          getStore().dispatch(setClosedWorkspace({ id, saved: true }));
          await workspaceController.update({ ...workspace, isCurrent: true, saved: true });
        }
      },
    }));

  return (
    <>
      {menuItemsTest.map((menuItem) => {
        const isMyWorkSpaceMenu = menuItem.name === 'Мои рабочие столы';
        return (
          <WorkspaceItem
            key={menuItem.name}
            contextMenu
            name={menuItem.name}
            icon={menuItem.icon}
            selected={selectedItem === menuItem.name}
            setSelectedItem={setSelectedItem}
            dropdownItems={isMyWorkSpaceMenu ? workspacesMenu : menuItem.dropdownItems}
            chevronIcon={isMyWorkSpaceMenu ? !!workspacesMenu.length : true}
            disabled={isMyWorkSpaceMenu ? !workspacesMenu.length : false}
          />
        );
      })}
    </>
  );
};


















import { useCallback } from 'react';
import { useDispatch } from 'react-redux';

import { LIBRARY_WORKSPACES_NAMES } from '@configs/systemWorkspaceConfigs';
import { useAppSelect } from '@hooks/useAppSelector';
import { useLocalisation } from '@hooks/useLocalisation';
import { useWorkspaceCreator } from '@hooks/useWorkspaceCreator';
import { workspacesNamesSelector } from '@store/selectors/workspaces';
import { setCurrentWorkspace } from '@store/slices/workspaces';
import { createNameForNewWorkspace } from '@terminal/desktop/components/Header/utils';
import createWidget from '@utils/createWidget';
import createWorkspace from '@utils/createWorkspace';
import getDefaultWorkspaceConfig from '@utils/getDefaultWorkspaceConfig';
import { newWorkspaceName } from '@utils/newWorkspaceName';
import { LibraryWorkspaceType, SystemWorkspaceConfig } from 'types/Workspace';

export const useWorkspaceCreate = () => {
  const dispatch = useDispatch();
  const localisation = useLocalisation();
  const workspaceStore = useAppSelect((state) => state.workspaces);
  const workspacesNames = useAppSelect(workspacesNamesSelector);

  const createWorkspaceFromConfig = (type: SystemWorkspaceConfig, name: string) => {
    createWorkspace({
      name: newWorkspaceName(name, workspaceStore),
      isNew: false,
    }).then((res) => {
      dispatch(setCurrentWorkspace(res.id));
      const widgets = getDefaultWorkspaceConfig(type, res.id);
      createWidget(widgets);
    });
  };

  const createEmptyWorkspace = async () => {
    try {
      const workspace = await createWorkspace({
        name: createNameForNewWorkspace(localisation.newWorkspace, workspacesNames),
        isNew: false,
        pinned: false,
      });
      return workspace;
    } catch (error) {
      console.log(error);
    }
  };

  const createWorkspaceThroughImport = useWorkspaceCreator(null);

  const createWorkspaceFromJson = useCallback(
    (id: LibraryWorkspaceType) => {
      fetch(`/workspaces/${id}.json`)
        .then((result) => result.text())
        .then((workspace) => {
          const dataJSON = JSON.parse(workspace);
          createWorkspaceThroughImport(
            dataJSON.workspace,
            dataJSON.widgets_properties,
            LIBRARY_WORKSPACES_NAMES.get(id),
          );
        });
    },
    [createWorkspaceThroughImport],
  );

  return { createWorkspaceFromConfig, createEmptyWorkspace, createWorkspaceFromJson, workspaceStore };
};


















import { useMemo } from 'react';

import { LIBRARY_WORKSPACES_NAMES } from '@configs/systemWorkspaceConfigs';
import { useAppSelect } from '@hooks/useAppSelector';
import { useProtectedWorkspaces } from '@hooks/useProtectedWorkspaces';
import { isSPFIBrokerSelector, isSPFITraderSelector } from '@store/selectors/user';
import { workspacesSelector } from '@store/selectors/workspaces';

import { useFavoriteWorkspaces } from './hooks/useFavoriteWorkspaces';
import { useWorkspaceCreate } from './hooks/useWorkspaceCreate';
import { DefaultWorkspaceTile } from './types';

export function useStartPage() {
  const { isAvailableWorkspaceByWorkspaceType } = useProtectedWorkspaces();
  const isSpfiTrader = useAppSelect(isSPFITraderSelector);
  const isSpfiBroker = useAppSelect(isSPFIBrokerSelector);
  const workspaces = useAppSelect(workspacesSelector);

  const currencyWorkspace = useMemo(() => {
    if (isSpfiBroker) {
      return 'spfiBroker';
    }

    if (isSpfiTrader) {
      return 'spfiScreen';
    }

    return 'spfiView';
  }, [isSpfiTrader, isSpfiBroker]);

  const { createEmptyWorkspace, createWorkspaceFromJson } = useWorkspaceCreate();

  // TODO: Обновлять при изменении конфигов рабочих столов
  const startPageDefaultWorkspaces = [
    isAvailableWorkspaceByWorkspaceType('depositsCcp') && {
      label: LIBRARY_WORKSPACES_NAMES.get('depositsCcp'),
      onClick: () => createWorkspaceFromJson('depositsCcp'),
      empty: false,
      variant: 'quadBlock',
    },
    isAvailableWorkspaceByWorkspaceType('Currency') && {
      label: LIBRARY_WORKSPACES_NAMES.get(currencyWorkspace),
      onClick: () => createWorkspaceFromJson(currencyWorkspace),
      empty: false,
      variant: 'solid',
    },
    isAvailableWorkspaceByWorkspaceType('Bonds') && {
      label: 'Облигации',
      onClick: () => createWorkspaceFromJson('bondsLargeScreen'),
      empty: false,
      variant: 'leftTripleBlock',
    },
    !isAvailableWorkspaceByWorkspaceType('NTBStock') && {
      label: LIBRARY_WORKSPACES_NAMES.get('corporateInformationCenter'),
      onClick: () => createWorkspaceFromJson('corporateInformationCenter'),
      empty: false,
      variant: 'solid',
    },
    isAvailableWorkspaceByWorkspaceType('NTBStock') && {
      label: 'Товарный рынок',
      onClick: () => createWorkspaceFromJson('ntb'),
      empty: false,
      variant: 'rightTripleBlock',
    },
    isAvailableWorkspaceByWorkspaceType('NTBStock') && {
      label: 'Товарные индексы',
      onClick: () => createWorkspaceFromJson('ntbIndexes'),
      empty: false,
      variant: 'rightTripleBlock',
    },
    isAvailableWorkspaceByWorkspaceType('NTBStock') && {
      label: 'Логистика',
      onClick: () => createWorkspaceFromJson('ntbLogistic'),
      empty: false,
      variant: 'rightTripleBlock',
    },
    isAvailableWorkspaceByWorkspaceType('Stocks') && {
      label: 'Акции и Индексы',
      onClick: () => createWorkspaceFromJson('stocksAndIndexes'),
      empty: false,
      variant: 'quadBlock',
    },
    isAvailableWorkspaceByWorkspaceType('Currency') && {
      label: 'Валютный рынок',
      empty: false,
      onClick: () => createWorkspaceFromJson('currencyMarket'),
      variant: 'doubleBlock',
    },
    isAvailableWorkspaceByWorkspaceType('Talks') && {
      label: 'MOEX Dealing',
      onClick: () => createWorkspaceFromJson('dealingFunctionality'),
      empty: false,
      variant: 'rightTripleBlock',
    },
  ].filter(Boolean) as DefaultWorkspaceTile[];

  const favoriteWorkspaces = useFavoriteWorkspaces();

  const isRecentWorkspacesExist = useMemo(() => !workspaces.some(({ saved }) => !saved), [workspaces]);

  return {
    startPageDefaultWorkspaces,
    onAddEmptyClick: createEmptyWorkspace,
    isRecentWorkspacesExist,
    favoriteWorkspaces,
  };
}


















import React from 'react';

import { useDispatch } from 'react-redux';

import { TOURS_DATA } from '@modules/tours';
import { openStartPageWidgetsModal } from '@store/slices/modals';
import { Button } from '@uikit/Button';

import { MiniTile } from './components/MiniTile';
import { StartPageModal } from './components/StartPageModal/types';
import styles from './startPage.module.scss';
import { useStartPage } from './useStartPage';

const StartPage = function () {
  const { startPageDefaultWorkspaces, onAddEmptyClick, favoriteWorkspaces, isRecentWorkspacesExist } = useStartPage();
  const dispatch = useDispatch();

  const recentWorkspacesButton = (
    <Button
      tooltipProps={{
        title: isRecentWorkspacesExist ? 'Нет закрытых рабочих столов' : null,
      }}
      disabled={isRecentWorkspacesExist}
      onClick={() => dispatch(openStartPageWidgetsModal(StartPageModal.RECENT_WORKSPACES))}
      className={styles['startpage-block-subtitle-button']}
      variant="unfilled-primary"
      text="Недавно закрытые"
    />
  );

  return (
    <div className={styles['startpage-block']}>
      <div className={styles['startpage-block-create-container']}>
        <div
          className={styles['startpage-block-create']}
          onClick={onAddEmptyClick}
          data-intro-tour={TOURS_DATA[0].steps[0].target.split('-')[1]}
        >
          <div className={styles['startpage-block-create-circle']}>+</div>
          <div className={styles['startpage-block-create-bottom']}>
            <div className={styles['startpage-block-create-bottom-title']}>Создать свой рабочий стол</div>
            <div className={styles['startpage-block-create-bottom-text']}>
              Создайте рабочий стол со своими настройками
            </div>
          </div>
        </div>
        {favoriteWorkspaces.length === 0 && recentWorkspacesButton}
      </div>
      {favoriteWorkspaces.length > 0 && (
        <div className={styles['startpage-block-item']}>
          <div className={styles['startpage-block-subtitle']}>
            <p className={styles['startpage-block-subtitle-text']}>Избранные рабочие столы</p>
            {recentWorkspacesButton}
          </div>
          <div className={styles['startpage-block-body']}>
            <div className={styles['body-tile-row']}>
              {favoriteWorkspaces.map((item) => (
                <MiniTile
                  key={item.id}
                  onClick={item.onClick}
                  label={item.label}
                  variant={item.variant}
                />
              ))}
            </div>
          </div>
        </div>
      )}
      <div className={styles['startpage-block-item']}>
        <div className={styles['startpage-block-subtitle']}>
          <p className={styles['startpage-block-subtitle-text']}>Шаблонные рабочие столы</p>
          <Button
            onClick={() => dispatch(openStartPageWidgetsModal(StartPageModal.LIBRARY_WORKSPACES))}
            className={styles['startpage-block-subtitle-button']}
            variant="unfilled-primary"
            text="Открыть библиотеку"
          />
        </div>
        <div className={styles['startpage-block-body']}>
          <div className={styles['body-tile-row']}>
            {startPageDefaultWorkspaces.map((item) =>
              !item.empty ? (
                <MiniTile
                  id={item.id ?? ''}
                  key={item.label}
                  onClick={item.onClick}
                  label={item.label}
                  variant={item.variant}
                />
              ) : null,
            )}
          </div>
        </div>
      </div>
    </div>
  );
};

export default StartPage;


















import isEqual from 'lodash/isEqual';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';

import {
  CREATE_NEW_WORKSPACE_ID,
  DISTANCE_BETWEEN_WIDGET,
  DISTANCE_MAGNETIZATION_WIDGET_START,
} from '@configs/appConfig';
import features from '@features';
import { useAppSelect } from '@hooks/useAppSelector';
import { useLocalisation } from '@hooks/useLocalisation';

import { useSetBiggestZIndex } from '@hooks/useSetBiggestZIndex';
import { savedWorkspacesSelector, workspaceWidgetsSelector } from '@store/selectors/workspaces';
import { setPositionForWidget, setSizesForWidget, WidgetChangePayload } from '@store/slices/widgets';
import { addWorkspace, setCurrentWorkspace } from '@store/slices/workspaces';
import { getStore } from '@store/store';
import getWorkspaceSizes from '@utils/getWorkspaceSizes';
import { type Widget, type WidgetSizes } from 'types/Widgets';
import { Workspace } from 'types/Workspace';

import { exportJSONFile } from '../../../utils/exportJSONFile';

import { widgetDistanceConnect } from './widgetDistanceConnect';
import { widgetResizeConnect } from './widgetResizeConnect';

import type { DraggableData, Position, RndDragEvent, RndResizeCallback } from 'react-rnd';

type OnResizeCb = (...args: Parameters<RndResizeCallback>) => boolean;

interface WidgetConnectorCoordinates {
  x: number;
  y: number;
}

interface UseWorkspaceFacadeResult {
  widgets: Widget[];
  setZIndexForWidget(widgetId: Widget['id']): void;
  setDraggingWidgetId: (widgetId: number) => void;
  onWidgetDrag(event: RndDragEvent, data: DraggableData): void;
  onWidgetDragStop(e: DraggableData): void;
  onWidgetResize: OnResizeCb;
  widgetConnectorCoordinates: Partial<WidgetConnectorCoordinates> | undefined;
  onWidgetResizeStop: RndResizeCallback;
  onWidgetResizeStart(widgetId: Widget['id']): void;
  isCurrentWorkspaceNew: boolean | undefined;
  isShowCurrentWidgets: boolean;
  exportJSONFile(item: number, name: string): void;
  workspacesArr: Workspace[];
  idOfMovingOrResizingWidget: number | null;
}

export function useWorkspaceFacade(): UseWorkspaceFacadeResult {
  const dispatch = useDispatch();
  const workspacesArr = useAppSelect(savedWorkspacesSelector);
  const isCurrentWorkspaceNew = useAppSelect(
    ({ workspaces }) => workspaces.workspaces.find(({ isCurrent }) => isCurrent)?.isNew,
  );

  const { setZIndexForWidget } = useSetBiggestZIndex();

  const isShowCurrentWidgets: boolean = useAppSelect(
    (state) => state.workspaces.workspaces.find(({ isCurrent, code }) => isCurrent && !code)?.isAddWidgets ?? false,
  );

  const widgets = useAppSelect(workspaceWidgetsSelector);

  // Использование одновременно и стейта и реф для:
  // 1) Уменьшения пересоздания коллбеков (не надо прокидывать в зависимости useCallback)
  // 2) стейт - для перерендеров там где нужно (вне фасада)
  const [draggingWidgetId, setDraggingWidgetId] = useState<Widget['id'] | undefined>();
  const draggingWidgetIdRef = useRef<number | undefined>(undefined);

  const [widgetConnectorCoordinates, setWidgetConnectorCoordinates] = useState<
    Partial<WidgetConnectorCoordinates> | undefined
  >();
  const newSizesForWidget = useRef<{
    id: number;
    sizes: WidgetSizes;
  }>();
  const newPositionForWidget = useRef<{
    id: number;
    position: Position;
  }>();

  const [idOfWidgetInResizeProcess, setIdOfWidgetInResizeProcess] = useState<number | null>(null);

  const setOnlyNewWidgetConnectorCoordinates = useCallback(
    (connectorCoordinates: Partial<WidgetConnectorCoordinates> | undefined) => {
      if (!isEqual(widgetConnectorCoordinates, connectorCoordinates)) {
        setWidgetConnectorCoordinates(connectorCoordinates);
      }
    },
    [widgetConnectorCoordinates],
  );

  useEffect(() => {
    if (workspacesArr.length === 0) {
      const newWorkspace: Workspace = {
        name: 'Новый рабочий стол',
        id: CREATE_NEW_WORKSPACE_ID,
        isCurrent: true,
        saved: true,
        pinned: false,
        position: getStore().getState().workspaces.workspaces.length,
        isNew: true,
        isAddWidgets: true,
      };
      if (features.meshLayout) {
        newWorkspace.layout = {
          version: '1.0',
          rootId: 'pane-0',
          items: [{ id: 'pane-0', widgetId: '', size: { width: 0, height: 0 } }],
        };
      }
      dispatch(addWorkspace(newWorkspace));
      dispatch(setCurrentWorkspace(CREATE_NEW_WORKSPACE_ID));
    }
  }, [dispatch, workspacesArr]);

  const { getConnectionSideCoordinateFromDC } = useMemo(widgetDistanceConnect, []);
  const { getParams } = useMemo(widgetResizeConnect, []);

  const onWidgetDrag = useCallback(
    (event: RndDragEvent, data: DraggableData): void => {
      const { x, y } = data;
      const { height, width } = data.node.getBoundingClientRect();

      event.preventDefault();

      const coord = getConnectionSideCoordinateFromDC(data);

      const workspaceSizes = getWorkspaceSizes();
      // прилипание виджетов между виджетом и стороной
      if (
        y >= 0 &&
        y <= DISTANCE_MAGNETIZATION_WIDGET_START &&
        coord?.y !== undefined &&
        coord?.y < DISTANCE_MAGNETIZATION_WIDGET_START
      ) {
        if (widgetConnectorCoordinates) {
          setOnlyNewWidgetConnectorCoordinates({ x: coord.x, y: 1 });
        }
        return;
      }

      if (
        y + height >= workspaceSizes.height - DISTANCE_MAGNETIZATION_WIDGET_START &&
        y + height <= workspaceSizes.height &&
        coord &&
        coord?.y !== undefined &&
        workspaceSizes.height - (coord?.y as number) <= DISTANCE_MAGNETIZATION_WIDGET_START
      ) {
        setOnlyNewWidgetConnectorCoordinates({
          x: coord.x,
          y: workspaceSizes.height - 1,
        });
        return;
      }

      if (
        x + width >= workspaceSizes.width - DISTANCE_MAGNETIZATION_WIDGET_START &&
        x + width <= workspaceSizes.width &&
        coord &&
        coord?.x !== undefined &&
        workspaceSizes.width - (coord?.x as number) <= DISTANCE_MAGNETIZATION_WIDGET_START
      ) {
        setOnlyNewWidgetConnectorCoordinates({
          x: workspaceSizes.width - 1,
          y: coord.y,
        });
        return;
      }

      if (
        x >= 0 &&
        x <= DISTANCE_MAGNETIZATION_WIDGET_START &&
        coord?.x !== undefined &&
        coord?.x <= DISTANCE_MAGNETIZATION_WIDGET_START
      ) {
        setOnlyNewWidgetConnectorCoordinates({
          x: 1,
          y: coord.y,
        });
        return;
      }

      if (x >= 0 && x <= DISTANCE_MAGNETIZATION_WIDGET_START && y >= 0 && y <= DISTANCE_MAGNETIZATION_WIDGET_START) {
        // проверка на прилипание по углам
        setOnlyNewWidgetConnectorCoordinates({ x: 1, y: 1 });
        return;
      }

      if (
        x >= 0 &&
        x <= DISTANCE_MAGNETIZATION_WIDGET_START &&
        y + height >= workspaceSizes.height - DISTANCE_MAGNETIZATION_WIDGET_START &&
        y + height <= workspaceSizes.height
      ) {
        setOnlyNewWidgetConnectorCoordinates({
          x: 1,
          y: workspaceSizes.height - 1,
        });
        return;
      }

      if (
        x + width >= workspaceSizes.width - DISTANCE_MAGNETIZATION_WIDGET_START &&
        x + width <= workspaceSizes.width &&
        y >= 0 &&
        y <= DISTANCE_MAGNETIZATION_WIDGET_START
      ) {
        setOnlyNewWidgetConnectorCoordinates({
          x: workspaceSizes.width - 1,
          y: 1,
        });
        return;
      }

      if (
        x + width >= workspaceSizes.width - DISTANCE_MAGNETIZATION_WIDGET_START &&
        x + width <= workspaceSizes.width &&
        y + height >= workspaceSizes.height - DISTANCE_MAGNETIZATION_WIDGET_START &&
        y + height <= workspaceSizes.height
      ) {
        setOnlyNewWidgetConnectorCoordinates({
          x: workspaceSizes.width - 1,
          y: workspaceSizes.height - 1,
        });
        return;
      }

      if (x >= 0 && x <= DISTANCE_MAGNETIZATION_WIDGET_START) {
        setOnlyNewWidgetConnectorCoordinates({ x: 1 });
        return;
      }

      if (
        x + width >= workspaceSizes.width - DISTANCE_MAGNETIZATION_WIDGET_START &&
        x + width <= workspaceSizes.width
      ) {
        setOnlyNewWidgetConnectorCoordinates({ x: workspaceSizes.width - 1 });
        return;
      }
      if (y >= 0 && y <= DISTANCE_MAGNETIZATION_WIDGET_START) {
        setOnlyNewWidgetConnectorCoordinates({ y: 1 });
        return;
      }
      if (
        y + height >= workspaceSizes.height - DISTANCE_MAGNETIZATION_WIDGET_START &&
        y + height <= workspaceSizes.height
      ) {
        setOnlyNewWidgetConnectorCoordinates({ y: workspaceSizes.height - 1 });
        return;
      }

      // проверка на прилипание к другим виджетам
      if (coord) {
        const coordConnector = Object.entries(coord).filter((item) => item[0] === 'x' || item[0] === 'y');
        const coordRes = coordConnector.reduce((acc, cur) => ({ ...acc, [cur[0]]: cur[1] }), {});
        setOnlyNewWidgetConnectorCoordinates(coordRes);
        return;
      }

      if (
        (x > DISTANCE_MAGNETIZATION_WIDGET_START ||
          x + width < workspaceSizes.width - DISTANCE_MAGNETIZATION_WIDGET_START ||
          y > DISTANCE_MAGNETIZATION_WIDGET_START ||
          y + height < workspaceSizes.height - DISTANCE_MAGNETIZATION_WIDGET_START) &&
        !coord
      ) {
        setOnlyNewWidgetConnectorCoordinates(undefined);
      }
    },
    [getConnectionSideCoordinateFromDC, setOnlyNewWidgetConnectorCoordinates, widgetConnectorCoordinates],
  );

  // Использование ref чтобы избежать ререндера коллбека из-за изменения зависимости widgets
  const dragWidgetRef = useRef<Widget | undefined>(undefined);
  const draggableWidgetId = draggingWidgetIdRef.current;
  useEffect(() => {
    const draggedWidget = widgets.find(({ id }) => draggableWidgetId === id);
    dragWidgetRef.current = draggedWidget;
  }, [widgets, draggableWidgetId]);

  const onWidgetDragStop = useCallback(
    (data: DraggableData): void => {
      const { width, height } = getWorkspaceSizes();
      if (
        draggingWidgetIdRef.current &&
        widgetConnectorCoordinates &&
        widgetConnectorCoordinates.x &&
        widgetConnectorCoordinates.x !== 1 &&
        widgetConnectorCoordinates.x !== width - 1
      ) {
        const position = getConnectionSideCoordinateFromDC(data)
          ? getConnectionSideCoordinateFromDC(data)?.position
          : undefined;

        if (position) {
          dispatch(setPositionForWidget({ id: draggingWidgetIdRef.current, position, moved: true }));
        }
      } else if (
        draggingWidgetIdRef.current &&
        widgetConnectorCoordinates &&
        widgetConnectorCoordinates.y &&
        widgetConnectorCoordinates.y !== 1 &&
        widgetConnectorCoordinates.y !== height - 1
      ) {
        const position = getConnectionSideCoordinateFromDC(data)
          ? getConnectionSideCoordinateFromDC(data)?.position
          : undefined;

        if (position) {
          dispatch(setPositionForWidget({ id: draggingWidgetIdRef.current, position, moved: true }));
        }
      } else if (draggingWidgetIdRef.current && widgetConnectorCoordinates) {
        const position = {
          x: data.x,
          y: data.y,
        };
        const currentDraggableWidget = dragWidgetRef.current;
        if (currentDraggableWidget) {
          if (widgetConnectorCoordinates?.x) {
            const newX =
              widgetConnectorCoordinates.x > 1
                ? width - parseFloat(currentDraggableWidget.sizes.width) - DISTANCE_BETWEEN_WIDGET
                : DISTANCE_BETWEEN_WIDGET;
            position.x = newX;
          }
          if (widgetConnectorCoordinates?.y) {
            const newY =
              widgetConnectorCoordinates.y > 1
                ? height - parseFloat(currentDraggableWidget.sizes.height) - DISTANCE_BETWEEN_WIDGET
                : DISTANCE_BETWEEN_WIDGET;
            position.y = newY;
          }
          dispatch(setPositionForWidget({ id: draggingWidgetIdRef.current, position, moved: true }));
        }
      }

      // Убираем текущий виджет из активного драг состояния
      draggingWidgetIdRef.current = undefined;
      setDraggingWidgetId(undefined);

      setWidgetConnectorCoordinates(undefined);
    },
    [dispatch, getConnectionSideCoordinateFromDC, widgetConnectorCoordinates],
  );

  const onWidgetResize: OnResizeCb = useCallback(
    (e, direction, refToElem, delta, pos) => {
      const resizeId = parseInt(refToElem.id, 10);
      const directions = ['top', 'right', 'bottom', 'left', 'topLeft', 'topRight', 'bottomLeft', 'bottomRight'];

      if (!directions.includes(direction)) {
        if (widgetConnectorCoordinates) {
          setWidgetConnectorCoordinates(undefined);
        }
        return false;
      }

      const params = getParams({
        dir: direction,
        refToElem,
        pos,
      });
      const { x, y, width, height } = refToElem.getBoundingClientRect();
      const transformSizeParam = (param: number): string => `${param}px`;

      newSizesForWidget.current = {
        id: resizeId,
        sizes: {
          width: transformSizeParam(width),
          height: transformSizeParam(height),
        },
      };
      newPositionForWidget.current = {
        id: resizeId,
        position: { x, y },
      };

      if (!params) {
        if (widgetConnectorCoordinates) {
          setWidgetConnectorCoordinates(undefined);
        }
        return false;
      }

      const { position, sizes, closest, typeX, typeY } = params;

      newSizesForWidget.current = {
        id: resizeId,
        sizes,
      };
      newPositionForWidget.current = {
        id: resizeId,
        position,
      };

      const getLineCoords = (direct: typeof direction): { x?: number; y?: number } | undefined => {
        let result: { x?: number | undefined; y?: number | undefined } | undefined;
        const lineTop = () => {
          if (typeY) {
            result = {
              ...result,
              y: typeY === 'closest' ? closest.position.y + parseFloat(closest.sizes.height) + 2 : closest.position.y,
            };
          }
        };
        const lineBottom = () => {
          if (typeY) {
            result = {
              ...result,
              y: typeY === 'closest' ? closest.position.y : closest.position.y + parseFloat(closest.sizes.height),
            };
          }
        };
        const lineLeft = () => {
          if (typeX) {
            result = {
              ...result,
              x: typeX === 'closest' ? closest.position.x + parseFloat(closest.sizes.width) : closest.position.x,
            };
          }
        };
        const lineRight = () => {
          if (typeX) {
            result = {
              ...result,
              x: typeX === 'closest' ? closest.position.x : closest.position.x + parseFloat(closest.sizes.width),
            };
          }
        };
        switch (direct) {
          case 'top':
            lineTop();
            break;
          case 'bottom':
            lineBottom();
            break;
          case 'left':
            lineLeft();
            break;
          case 'right':
            lineRight();
            break;
          case 'topLeft':
            lineLeft();
            lineTop();
            break;
          case 'topRight':
            lineTop();
            lineRight();
            break;
          case 'bottomLeft':
            lineBottom();
            lineLeft();
            break;
          case 'bottomRight':
            lineBottom();
            lineRight();
            break;
          default:
            break;
        }
        return result;
      };

      const variant = getLineCoords(direction);

      if (variant) {
        setOnlyNewWidgetConnectorCoordinates(variant);
      } else {
        setOnlyNewWidgetConnectorCoordinates(undefined);
      }

      return false;
    },
    [getParams, setOnlyNewWidgetConnectorCoordinates, widgetConnectorCoordinates],
  );

  const onWidgetResizeStop: RndResizeCallback = useCallback(() => {
    setIdOfWidgetInResizeProcess(null);
    if (widgetConnectorCoordinates) {
      dispatch(setSizesForWidget(newSizesForWidget.current as unknown as WidgetChangePayload<'sizes'>));
      dispatch(
        setPositionForWidget({
          ...(newPositionForWidget.current as unknown as WidgetChangePayload<'position'>),
          moved: true,
        }),
      );
      setWidgetConnectorCoordinates(undefined);
    }
  }, [dispatch, widgetConnectorCoordinates]);

  const onWidgetResizeStart = useCallback((widgetId: number) => {
    setIdOfWidgetInResizeProcess(widgetId);
  }, []);

  const setDraggingWidgetIdCallback = useCallback((widgetId: number) => {
    setDraggingWidgetId(widgetId);
    draggingWidgetIdRef.current = widgetId;
  }, []);

  return {
    widgets,
    setZIndexForWidget,
    setDraggingWidgetId: setDraggingWidgetIdCallback,
    onWidgetDrag,
    onWidgetDragStop,
    onWidgetResize,
    widgetConnectorCoordinates,
    onWidgetResizeStop,
    isCurrentWorkspaceNew,
    isShowCurrentWidgets,
    exportJSONFile,
    workspacesArr,
    onWidgetResizeStart,
    idOfMovingOrResizingWidget: draggingWidgetId ?? idOfWidgetInResizeProcess,
  };
}


















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 { 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 { SaveWorkspaceActionType } from '../SaveWorkspaceModal/types';

import { WorkspaceMenuItemsKeys } from './const';
import { useWidgetsByWorkspaceId } from './hooks/useWidgetsByWorkspaceId';

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 widgets = useWidgetsByWorkspaceId(workspaceId);

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

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

    createWorkspace({
      name: newWorkspaceName,
      isNew: false,
    })
      .then(({ id }) => {
        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;
        });
        createWidget(newWidgets);
        toasts.duplicateWorkspace.success(newWorkspaceName);
      })
      .catch(() => {
        toasts.duplicateWorkspace.error(newWorkspaceName);
      });
  }, [widgets, 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>
  );
};


















import React from 'react';

import { useAppSelect } from '@hooks/useAppSelector';
import { useSystemWorkspaces } from '@hooks/useSystemWorkspaces';
import { workspaceByIdSelector } from '@store/selectors/workspaces';

import { WorkspaceContextMenu, WorkspaceContextMenuProps } from './WorkspaceContextMenu';

type WorkspaceContextMenuWrapperProps = Omit<WorkspaceContextMenuProps, 'workspaceId'> & {
  workspaceId?: number;
  disabled?: boolean;
};

export const WorkspaceContextMenuWrapper = ({
  children,
  workspaceId,
  disabled,
  ...otherProps
}: WorkspaceContextMenuWrapperProps) => {
  const { isSystemWorkspace } = useSystemWorkspaces({ workspaceId });

  const disableContextMenu = disabled || isSystemWorkspace;

  if (!workspaceId || disableContextMenu) {
    return children;
  }
  return (
    <WorkspaceContextMenu
      workspaceId={workspaceId}
      {...otherProps}
    >
      {children}
    </WorkspaceContextMenu>
  );
};

















import classNames from 'classnames/bind';
import React, { FC, useEffect, useRef, useState } from 'react';

import { WorkspaceContextMenuWrapper, WorkspaceMenuItemsKeys } from '@terminal/desktop/components/WorkspaceContextMenu';
import { wrapBetaInSpan } from '@utils/wrapBetaInSpan';
import { WidgetContentType } from 'types/Widgets';

import styles from './dropdownitem.module.scss';

const cx = classNames.bind(styles);

interface ItemProps {
  name: string;
  title?: string;
  subtitle?: string;
  descripIcon?: FC;
  id?: number;
  updated?: string;
  onItemClick?: (id?: number, type?: WidgetContentType) => void;
  type?: WidgetContentType;
  itemStyles?: React.CSSProperties;
  selected?: boolean;
  contextMenu?: boolean;
  disabled?: boolean;
  needTip?: boolean;
}

export const DropdownItem: FC<ItemProps> = function ({
  name,
  title = '',
  subtitle,
  descripIcon,
  id,
  updated = '',
  onItemClick,
  type,
  itemStyles = {} as React.CSSProperties,
  selected = false,
  contextMenu = false,
  disabled = false,
  needTip,
}) {
  const [tooLong, setTooLong] = useState(false);
  const nameRef = useRef<HTMLSpanElement | null>(null);

  const DescripIcon = descripIcon as FC;

  useEffect(() => {
    if (needTip && nameRef.current) {
      const nameWidth = nameRef.current.offsetWidth;
      if (nameWidth > 220) {
        setTooLong(true);
      }
    }
  }, [needTip]);

  const ref = useRef<HTMLDivElement | null>(null);

  return (
    <WorkspaceContextMenuWrapper
      workspaceId={id}
      disabled={!contextMenu}
      hiddenItems={[WorkspaceMenuItemsKeys.Close]}
    >
      <div
        className={cx({
          'dropdown-menu-item': !selected,
          'dropdown-menu-item-selected': selected,
          'dropdown-menu-item-disabled': disabled,
        })}
        onClick={onItemClick && !disabled ? () => onItemClick(id, type) : undefined}
        style={itemStyles}
        title={tooLong ? name : ''}
        ref={ref}
      >
        <div
          style={{
            display: 'flex',
            paddingBottom: '4px',
            pointerEvents: 'none',
          }}
        >
          <span className={cx('menu-item-name', [type])}>
            <span className={styles['menu-item-title']}>{title}</span>
            <div className={styles['menu-item-main-info-container']}>
              {descripIcon && <DescripIcon />}
              <div className={styles['menu-item-main-info']}>
                <span
                  ref={nameRef}
                  className={cx(styles.name, { [styles.tooLong]: tooLong })}
                >
                  {wrapBetaInSpan(name)}
                </span>
                {subtitle && <div className={styles['menu-item-subtitle']}>{subtitle}</div>}
              </div>
            </div>
          </span>
        </div>
        <span className={styles['menu-item-updated']}>{updated}</span>
      </div>
    </WorkspaceContextMenuWrapper>
  );
};


















import dayjs, { Dayjs } from 'dayjs';
import React, { useMemo, useState } from 'react';

import { DatePickerMad } from '@components/DatePicker';
import { useAppSelect } from '@hooks/useAppSelector';

import { ModalContent } from '../ModalContent';
import { WorkspaceCard } from '../WorkspaceCard';

import styles from './recentWorkspaces.module.scss';
import { useWorkspace } from './useWorkspace';
import { getDateDescription, getDateTitle } from './utils/dates';
import { formatDate } from './utils/formatDate';
import { groupWorkspacesByDate } from './utils/groupWorkspacesByDate';
import { closedWorkspacesSelector } from '@store/selectors/workspaces';

export const RecentWorkspaces = () => {
  const [date, setDate] = useState<Dayjs | null>(null);
  const workspaces = useAppSelect(closedWorkspacesSelector);
  const { handleSelectWorkspace } = useWorkspace();

  const groupedWorkspaces = useMemo(() => groupWorkspacesByDate(workspaces), [workspaces]);

  const filteredWorkspaces = useMemo(
    () => groupedWorkspaces.filter(([closedDate]) => !date || closedDate === formatDate(date)),
    [date, groupedWorkspaces],
  );

  return (
    <ModalContent
      headerTitle="Выберите дату"
      headerContent={
        <DatePickerMad<Dayjs | null>
          value={date}
          onChange={setDate}
          format="DD.MM.YYYY"
          popupClassName={styles.picker}
          disabledDate={(current) => current.isAfter(dayjs(), 'day')}
        />
      }
      sections={filteredWorkspaces}
      getDataFromSection={([groupDate, group]) => ({
        key: groupDate,
        title: getDateTitle(groupDate),
        content: group.map((workspace) => (
          <WorkspaceCard
            key={workspace.id}
            title={workspace.name}
            onClick={() => handleSelectWorkspace(workspace.id, workspace)}
            description={getDateDescription(groupDate, workspace.closedAt ?? '')}
          />
        )),
      })}
    />
  );
};

















import { useMemo } from 'react';

import { useDispatch } from 'react-redux';

import { workspaceController } from '@api/controllers/workspace';
import { CREATE_NEW_WORKSPACE_ID } from '@configs/appConfig';
import { useAppSelect } from '@hooks/useAppSelector';
import { openWorkspace, setClosedWorkspace } from '@store/slices/workspaces';

import { TILE_VARIANTS } from '../const';
import { FavoriteWorkspaceTile } from '../types';
import { workspacesSelector } from '@store/selectors/workspaces';
import { widgetsSelector } from '@store/selectors/widgets';

export const useFavoriteWorkspaces = () => {
  const dispatch = useDispatch();
  const widgets = useAppSelect(widgetsSelector);
  const workspaces = useAppSelect(workspacesSelector);

  const favoriteWorkspaces: FavoriteWorkspaceTile[] = useMemo(() => {
    const getTileVariant = (workspaceId: number) => {
      const widgetsQty = widgets.filter((w) => w.workspaceId === workspaceId).length;

      let tileVariantIndex = 0;
      if (widgetsQty <= 1) {
        tileVariantIndex = 0;
      } else if (widgetsQty === 3) {
        // 2 и 3 варианты имеют три блока,
        // поэтому для раб. столов с тремя виджетами вариант определяем на основании четности id
        tileVariantIndex = workspaceId % 2 ? 2 : 3;
      } else if (widgetsQty < TILE_VARIANTS.length) {
        tileVariantIndex = widgetsQty;
      } else {
        tileVariantIndex = TILE_VARIANTS.length - 1;
      }

      return TILE_VARIANTS[tileVariantIndex];
    };

    const currentWorkspace = workspaces.find((w) => w.isCurrent);

    return workspaces
      .filter((w) => w.favorite)
      .map((w) => ({
        id: w.id,
        label: w.name,
        onClick: () => {
          dispatch(openWorkspace(w.id));
          dispatch(setClosedWorkspace({ id: CREATE_NEW_WORKSPACE_ID, saved: false }));
          Promise.all([
            currentWorkspace &&
              currentWorkspace.id !== CREATE_NEW_WORKSPACE_ID &&
              workspaceController.update({ ...currentWorkspace, isCurrent: false }),
            workspaceController.update({ ...w, isCurrent: true }),
          ]);
        },
        variant: getTileVariant(w.id),
      }));
  }, [dispatch, widgets, workspaces]);

  return favoriteWorkspaces;
};