Загрузка данных
import type { StorageDataType } from './StorageData';
import type { SplitLayoutState } from '@terminal/desktop/workspaces/mesh/types';
export type SystemWorkspaceConfig =
| 'currencyMarket'
| 'negotiations'
| 'bonds'
| 'monitor'
| 'monitor2'
| 'stocks'
| 'NTBStock';
export type WorkspaceType = 'Bonds' | 'Currency' | 'Talks' | 'Stocks' | 'NTBStock';
export interface Workspace {
id: number;
name: string;
isCurrent: boolean;
position: number;
isAddWidgets: boolean;
saved?: boolean;
pinned?: boolean;
isNew?: boolean;
code?: string;
favorite?: boolean;
closedAt?: string;
layout?: SplitLayoutState;
}
export interface WorkspaceState {
workspaces: Workspace[];
loadStates?: Partial<Record<Workspace['id'], WorkspaceLoadState>>;
}
export interface WorkspaceLoadState {
status: 'loading' | 'loaded' | 'error';
requestId: number | null;
error: string | null;
}
export enum WorkspaceGroups {
Currency = 'currency',
Ntb = 'ntb',
Spfi = 'spfi',
Stocks = 'stocks',
Bonds = 'bonds',
Market = 'market',
Trading = 'trading',
Cci = 'cci',
DepositCcp = 'depositCcp',
}
export type LibraryWorkspace = {
id: LibraryWorkspaceType;
name: string;
groups: WorkspaceGroups[];
};
/** Все типы столов, используемых в библиотеке шаблонов */
export type LibraryWorkspaceType =
| WorkspaceType
| 'corporateInformationCenter'
| 'spfiScreen'
| 'spfiView'
| 'spfiBroker'
| 'spfiScreenExtended'
| 'bondsLargeScreen'
| 'bondsComparison'
| 'bondsMonitoringLargeScreen'
| 'municipalBonds'
| 'corporateBonds'
| 'govermentBonds'
| 'currencyBonds'
| 'repoMidSizeScreen'
| 'ntb'
| 'ntbIndexes'
| 'ntbLogistic'
| 'dealingFunctionality'
| 'marketOverview'
| 'instrumentListsByAllMarkets'
| 'stocksAndIndexes'
| 'currencyMarket'
| 'depositsCcp';
export type WorkspaceAutoCreateConfig = {
checkPermission: (permissions: string[]) => boolean;
workspaceJsonName: LibraryWorkspaceType;
storageKey: keyof StorageDataType;
featureFlag: boolean;
};
import { createSlice } from '@reduxjs/toolkit';
import { CREATE_NEW_WORKSPACE_ID } from '@configs/appConfig';
import type { PayloadAction } from '@reduxjs/toolkit';
import type { Workspace, WorkspaceState } from 'types/Workspace';
const initialState: WorkspaceState = {
workspaces: [],
loadStates: {},
};
type WorkspaceChangePayload<T extends Exclude<keyof Workspace, 'id'>> = Pick<Workspace, 'id' | T>;
const workspacesSlice = createSlice({
name: 'workspace',
// `createSlice` will infer the state type from the `initialState` argument
initialState,
reducers: {
addWorkspace: (state, { payload }: PayloadAction<Workspace | Workspace[]>): void => {
state.workspaces = state.workspaces.filter((workspace: Workspace) => workspace.id !== -1);
if ((payload as Workspace[]).length && (payload as Workspace[]).length > 0) {
state.workspaces = [...(payload as Workspace[])];
} else {
state.workspaces.push(payload as Workspace);
}
},
removeWorkspace: (state, { payload }: PayloadAction<Workspace['id']>): void => {
state.workspaces = state.workspaces.filter((workspace: Workspace) => workspace.id !== payload);
if (state.loadStates) {
delete state.loadStates[payload];
}
},
setWorkspaceName: (state, { payload }: PayloadAction<WorkspaceChangePayload<'name'>>) => {
const changedWorkspaceIndex = state.workspaces.findIndex(({ id }) => payload.id === id);
state.workspaces[changedWorkspaceIndex].name = payload.name;
},
setCurrentWorkspace: (state, { payload }: PayloadAction<Workspace['id']>) => {
if (payload !== CREATE_NEW_WORKSPACE_ID) {
state.workspaces = state.workspaces.filter((workspace: Workspace) => workspace.id !== CREATE_NEW_WORKSPACE_ID);
}
state.workspaces = state.workspaces.map((item) => ({
...item,
isCurrent: payload === item.id,
}));
},
togglePinWorkspace: (state, { payload }: PayloadAction<WorkspaceChangePayload<never>>) => {
const changedWorkspace = state.workspaces.find(({ id }) => payload.id === id);
if (changedWorkspace) {
changedWorkspace.pinned = !changedWorkspace.pinned;
}
},
setClosedWorkspace: (state, { payload }: PayloadAction<WorkspaceChangePayload<'saved'>>) => {
const changedWorkspace = state.workspaces.find(({ id }) => payload.id === id);
if (changedWorkspace) {
changedWorkspace.saved = payload.saved;
}
},
setWorkspaces: (state, { payload }: PayloadAction<Workspace[]>) => {
state.workspaces = payload;
},
openWorkspace: (state, { payload }: PayloadAction<Workspace['id']>) => {
const changedWorkspace = state.workspaces.find(({ id }) => payload === id);
if (changedWorkspace) {
changedWorkspace.saved = true;
}
state.workspaces = state.workspaces.map((item) => ({
...item,
isCurrent: payload === item.id,
}));
},
addToFavorites: (state, { payload }: PayloadAction<Partial<WorkspaceChangePayload<'name'>>>) => {
const changedWorkspace = state.workspaces.find(({ id }) => id === payload.id);
if (changedWorkspace) {
changedWorkspace.favorite = true;
if (payload.name) {
changedWorkspace.name = payload.name;
}
}
},
removeFromFavorites: (state, { payload }: PayloadAction<Workspace['id']>) => {
const changedWorkspace = state.workspaces.find(({ id }) => payload === id);
if (changedWorkspace) {
changedWorkspace.favorite = false;
}
},
updateWorkspace: (state, { payload }: PayloadAction<Workspace>) => {
const workspaceIndex = state.workspaces.findIndex((w) => w.id === payload.id);
if (workspaceIndex !== -1) {
state.workspaces[workspaceIndex] = payload;
}
},
changeCurrentWorkspace: (state, { payload }: PayloadAction<Workspace['id']>) => {
state.workspaces = state.workspaces.map((item) => ({
...item,
isCurrent: payload === item.id,
}));
const workspace = state.workspaces.find((w) => payload === w.id);
if (workspace) {
workspace.saved = true;
}
},
updateLayout: (
state,
{ payload: { layout, id: payloadId } }: PayloadAction<Partial<WorkspaceChangePayload<'layout'>>>,
) => {
const changedWorkspace = state.workspaces.find(({ id }) => id === payloadId);
if (changedWorkspace && layout) {
changedWorkspace.layout = layout;
}
},
startWorkspaceLoading: (
state,
{ payload }: PayloadAction<{ workspaceId: Workspace['id']; requestId: number }>,
) => {
state.loadStates ??= {};
state.loadStates[payload.workspaceId] = {
status: 'loading',
requestId: payload.requestId,
error: null,
};
},
completeWorkspaceLoading: (
state,
{ payload }: PayloadAction<{ workspaceId: Workspace['id']; requestId: number }>,
) => {
const loadState = state.loadStates?.[payload.workspaceId];
if (loadState?.requestId === payload.requestId) {
loadState.status = 'loaded';
loadState.error = null;
}
},
failWorkspaceLoading: (
state,
{ payload }: PayloadAction<{ workspaceId: Workspace['id']; requestId: number; error: string }>,
) => {
const loadState = state.loadStates?.[payload.workspaceId];
if (loadState?.requestId === payload.requestId) {
loadState.status = 'error';
loadState.error = payload.error;
}
},
markWorkspaceLoaded: (state, { payload }: PayloadAction<Workspace['id']>) => {
state.loadStates ??= {};
state.loadStates[payload] = {
status: 'loaded',
requestId: null,
error: null,
};
},
},
});
export const {
addWorkspace,
removeWorkspace,
setCurrentWorkspace,
setWorkspaceName,
togglePinWorkspace,
setClosedWorkspace,
setWorkspaces,
openWorkspace,
addToFavorites,
removeFromFavorites,
updateWorkspace,
changeCurrentWorkspace,
updateLayout,
startWorkspaceLoading,
completeWorkspaceLoading,
failWorkspaceLoading,
markWorkspaceLoaded,
} = workspacesSlice.actions;
export default workspacesSlice.reducer;
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
export interface PublicContextItem {
id: number;
color?: string | null;
publicProperties?: { name: string | null; value: string | number | null }[];
name?: string | null;
workspaceId?: number;
isRemoval?: boolean;
}
interface publicContextState {
publicContext: PublicContextItem[];
}
const initialState: publicContextState = {
publicContext: [],
};
const publicContextSlice = createSlice({
name: 'publicContext',
initialState,
reducers: {
addValuesToPublicContext(state, { payload }: PayloadAction<PublicContextItem>) {
const index = state.publicContext.findIndex((item) => item.id === payload.id);
if (index === -1) {
state.publicContext.push(payload);
} else {
state.publicContext[index] = { ...state.publicContext[index], ...payload };
}
},
addValuesToPublicContextBatch(state, { payload }: PayloadAction<PublicContextItem[]>) {
const items = new Map(state.publicContext.map((item) => [item.id, item]));
payload.forEach((item) => {
items.set(item.id, { ...items.get(item.id), ...item });
});
state.publicContext = Array.from(items.values());
},
createValuesToPublicContext: (state, { payload }: PayloadAction<PublicContextItem[]>) => {
if (payload.length > 0) {
state.publicContext = [...payload];
} else {
state.publicContext = [];
}
},
},
});
export const { addValuesToPublicContext, addValuesToPublicContextBatch, createValuesToPublicContext } =
publicContextSlice.actions;
export default publicContextSlice.reducer;
import chunk from 'lodash/chunk';
import api from '@api/index';
import { CREATE_NEW_WORKSPACE_ID, WIDGET_PROPS_REQUEST_CHUNK_SIZE } from '@configs/appConfig';
import { stateDiffController } from '@store/controllers/StateDiff';
import { addValuesToPublicContextBatch } from '@store/slices/publicContext';
import { addWidget } from '@store/slices/widgets';
import {
completeWorkspaceLoading,
failWorkspaceLoading,
startWorkspaceLoading,
} from '@store/slices/workspaces';
import { getStore } from '@store/store';
import { isRemovedWidget } from '@utils/isRemovedWidget';
import { robustRequest } from '@utils/robustRequest';
import { unpackWidgetProps } from '@utils/widgetPropsProceeders';
import { convertPercentToPx } from '@utils/widgetSizesNPositionProceeders';
import type { PublicContextItem } from '@store/slices/publicContext';
import type { AppStore } from '@store/store';
import type { Widget } from 'types/Widgets';
import type { Workspace } from 'types/Workspace';
type WorkspaceWidgetInfo = Pick<Widget, 'id' | 'name' | 'workspaceId'> &
Pick<PublicContextItem, 'color' | 'isRemoval'> & {
publicProperties?: Record<string, string> | string[];
};
interface WorkspaceData {
widgets: Widget[];
publicContext: PublicContextItem[];
}
interface PendingRequest {
requestId: number;
promise: Promise<void>;
}
const pendingRequests = new WeakMap<AppStore, Map<Workspace['id'], PendingRequest>>();
let nextRequestId = 0;
export async function readWorkspacesData(workspaceIds: Workspace['id'][]): Promise<WorkspaceData> {
const response = await robustRequest(() => api.widgetsController.read(workspaceIds));
const widgetInfo = Object.values(response.data).flat() as WorkspaceWidgetInfo[];
const widgetIds = Array.from(new Set(widgetInfo.map(({ id }) => id)));
const chunks = chunk(widgetIds, WIDGET_PROPS_REQUEST_CHUNK_SIZE);
const publicContext: PublicContextItem[] = widgetInfo.map(({ publicProperties, ...item }) => ({
...item,
publicProperties: Object.values(publicProperties ?? {}).map(
(value) => JSON.parse(value) as NonNullable<PublicContextItem['publicProperties']>[number],
),
}));
const properties = await Promise.all(
chunks.map((ids) => robustRequest(() => api.widgetPropertiesController.read(ids))),
);
const widgets = properties
.flatMap(({ data }) => data)
.filter((item) => !isRemovedWidget(item.properties))
.map((item) => {
const widget = unpackWidgetProps(convertPercentToPx(item));
if (!widget.isExpand && widget.beforeExpandParams) {
widget.beforeExpandParams = null;
}
if (!widget.widgetContentProps) {
widget.widgetContentProps = {};
}
return widget;
});
return { widgets, publicContext };
}
function isCurrentRequest(store: AppStore, workspaceId: Workspace['id'], requestId: number): boolean {
const state = store.getState();
const loadState = state.workspaces.loadStates?.[workspaceId];
return (
getStore() === store &&
state.workspaces.workspaces.some(({ id }) => id === workspaceId) &&
loadState?.status === 'loading' &&
loadState.requestId === requestId
);
}
async function loadWorkspace(store: AppStore, workspaceId: Workspace['id'], requestId: number): Promise<void> {
try {
if (!isCurrentRequest(store, workspaceId, requestId)) {
throw new Error('Workspace loading is no longer current');
}
const data = await readWorkspacesData([workspaceId]);
if (!isCurrentRequest(store, workspaceId, requestId)) {
throw new Error('Workspace loading is no longer current');
}
const state = store.getState();
const widgetIds = new Set(state.widgets.widgets.map(({ id }) => id));
const publicContextIds = new Set(state.publicContext.publicContext.map(({ id }) => id));
const widgets = data.widgets.filter(({ id }) => !widgetIds.has(id));
const publicContext = data.publicContext.filter(({ id }) => !publicContextIds.has(id));
stateDiffController.acceptLoadedData(widgets, publicContext);
if (widgets.length > 0) {
store.dispatch(addWidget(widgets));
}
if (publicContext.length > 0) {
store.dispatch(addValuesToPublicContextBatch(publicContext));
}
store.dispatch(completeWorkspaceLoading({ workspaceId, requestId }));
} catch (error) {
if (isCurrentRequest(store, workspaceId, requestId)) {
store.dispatch(
failWorkspaceLoading({
workspaceId,
requestId,
error: error instanceof Error ? error.message : 'Unable to load workspace',
}),
);
}
throw error;
}
}
export function ensureWorkspaceLoaded(workspaceId: Workspace['id']): Promise<void> {
const store = getStore();
const state = store.getState();
if (!state.workspaces.workspaces.some(({ id }) => id === workspaceId)) {
return Promise.reject(new Error('Workspace does not exist'));
}
if (workspaceId === CREATE_NEW_WORKSPACE_ID || state.workspaces.loadStates?.[workspaceId]?.status === 'loaded') {
return Promise.resolve();
}
const requests = pendingRequests.get(store) ?? new Map<Workspace['id'], PendingRequest>();
pendingRequests.set(store, requests);
const pending = requests.get(workspaceId);
const loadState = state.workspaces.loadStates?.[workspaceId];
if (pending && loadState?.status === 'loading' && loadState.requestId === pending.requestId) {
return pending.promise;
}
nextRequestId += 1;
const requestId = nextRequestId;
store.dispatch(startWorkspaceLoading({ workspaceId, requestId }));
const promise = Promise.resolve()
.then(() => loadWorkspace(store, workspaceId, requestId))
.finally(() => {
if (requests.get(workspaceId)?.requestId === requestId) {
requests.delete(workspaceId);
}
});
requests.set(workspaceId, { requestId, promise });
return promise;
}
import { all, call, put, select } from 'typed-redux-saga';
import api from '@api/index';
import { CREATE_NEW_WORKSPACE_ID, TRADE_PERMISSION } from '@configs/appConfig';
import { permissionsSelector } from '@store/selectors/user';
import { currentWorkspaceIdSelector } from '@store/selectors/workspaces';
import { metricsAction } from '@store/slices/core';
import { createValuesToPublicContext } from '@store/slices/publicContext';
import { addWidget } from '@store/slices/widgets';
import { addWorkspace, markWorkspaceLoaded } from '@store/slices/workspaces';
import { alertManager } from '@terminal/components/Alerts/alertManager';
import { ensureWorkspaceLoaded, readWorkspacesData } from '@utils/loadWorkspaceWidgets';
import { robustRequest } from '@utils/robustRequest';
import { MetricsAction } from 'types/MetricsActions';
import { Workspace } from 'types/Workspace';
export function* loadCurrentWorkspaceSaga() {
const workspaceId = yield* select(currentWorkspaceIdSelector);
if (workspaceId === undefined || workspaceId === CREATE_NEW_WORKSPACE_ID) {
return;
}
try {
yield* call(ensureWorkspaceLoaded, workspaceId);
} catch {
const currentWorkspaceId = yield* select(currentWorkspaceIdSelector);
if (currentWorkspaceId === workspaceId) {
alertManager.notify({
message: 'Не удалось загрузить рабочий стол. Попробуйте открыть его повторно.',
type: 'error',
timeout: 5000,
});
}
}
}
export function* fullWorkspacesUploadSaga({ loadCurrentOnly = false }: { loadCurrentOnly?: boolean } = {}) {
const permissions = yield* select(permissionsSelector);
const isCanTrade = permissions.includes(TRADE_PERMISSION);
const workspacesShortInfoResponse = yield* call(() => robustRequest(api.workspaceController.read));
yield put(metricsAction(MetricsAction.LOADING_SCREEN_TIME_WORKSPACES));
const workspaces: Workspace[] = workspacesShortInfoResponse.data;
workspaces
.sort((a, b) => a.position - b.position)
.map(({ saved, pinned, code, ...workspace }) => ({
saved: saved || false,
pinned: pinned || false,
code: code ?? null,
...workspace,
}));
if (workspaces.length > 0) {
let workspacesForStore = workspaces;
if (!isCanTrade) {
workspacesForStore = workspaces.filter(({ name }) => name !== 'Переговоры');
}
yield put(addWorkspace(workspacesForStore));
} else {
return;
}
if (loadCurrentOnly) {
yield* call(loadCurrentWorkspaceSaga);
return;
}
const workspacesIdList = workspaces.map(({ id }) => id);
// Получение подробной информации по виджетам
// Предположительно можно выполнить параллельно с запросом на проперти
const { widgets, publicContext } = yield* call(readWorkspacesData, workspacesIdList);
yield put(createValuesToPublicContext(publicContext));
if (widgets.length > 0) {
yield put(addWidget(widgets));
}
yield* all(workspacesIdList.map((workspaceId) => put(markWorkspaceLoaded(workspaceId))));
}
import { all, takeEvery, takeLeading } from 'typed-redux-saga';
import { requestLastReceivedMessages } from '@store/actions/chats';
import {
initMetrics,
initNotifications,
metricsAction,
metricTimer,
requestAppCriticalData,
requestDesktopCriticalData,
requestMobileCriticalData,
} from '@store/slices/core';
import { changeCurrentWorkspace, openWorkspace, setCurrentWorkspace } from '@store/slices/workspaces';
import { initLastMessagesSaga } from '../chats/initChat';
import { initMetricsSaga, sendMetricsSaga, sendTimerMetricAction } from '../metrics';
import { initNotificationsSaga } from '../notifications';
import { loadCurrentWorkspaceSaga } from '../workspaces';
import { criticalDesktopReceiverSaga } from './desktop';
import { criticalMobileReceiverSaga } from './mobile';
import { criticalTerminalDataReceiverSaga } from './terminal';
export function* coreSagas() {
yield all([
takeLeading(initMetrics, initMetricsSaga),
takeEvery(metricsAction, sendMetricsSaga),
takeEvery(metricTimer, sendTimerMetricAction),
takeLeading(requestAppCriticalData, criticalTerminalDataReceiverSaga),
takeLeading(requestMobileCriticalData, criticalMobileReceiverSaga),
takeLeading(requestDesktopCriticalData, criticalDesktopReceiverSaga),
takeEvery(
[setCurrentWorkspace.type, openWorkspace.type, changeCurrentWorkspace.type],
loadCurrentWorkspaceSaga,
),
takeLeading(initNotifications, initNotificationsSaga),
takeLeading(requestLastReceivedMessages, initLastMessagesSaga),
]);
}
import { all, call, fork, join, put, select } from 'typed-redux-saga';
import { instrumentListsController } from '@api/controllers/instrumentLists';
import { profileController } from '@api/controllers/profileController';
import api from '@api/index';
import { getIsSpfiTrader } from '@api/utils/getIsSpfiTrader';
import { wsEndpointStompClient } from '@api/websokets/classes/WSEndpointStompClient';
import { systemNotificationController } from '@api/websokets/systemNotification/systemNotificationController';
import { initStorageData } from '@store/sagas/initStorageData';
// import { addNSDCost } from '@store/slices/cashedData';
import { desktopCriticalDataReceived, metricsAction } from '@store/slices/core';
import { setFirms } from '@store/slices/firms';
import { setIntrumentLists } from '@store/slices/instrumentLists';
import { setAvatarUrl, setPersonalData, setPrivacySettings } from '@store/slices/user';
import { getStore, RootState } from '@store/store';
import { MetricsAction } from 'types/MetricsActions';
import { contractsRequestSaga } from '../basic/contracts';
import { newsInitSaga } from '../basic/news';
import { chatsCacheSyncSaga } from '../chats/initChat';
import { customersAndChatsSaga } from '../customersAndChats';
import { initChatAvatars } from '../initAvatars';
import { initSurveys } from '../surveys/init';
import { requestAndPut } from '../utils';
import { fullWorkspacesUploadSaga } from '../workspaces';
/** В этот момент пропадает экран загрузки и пользователь видит терминал */
export function* backgroundDesktopReceiverSaga() {
yield* fork(async () => {
try {
const res = await profileController.getAvatar();
getStore().dispatch(setAvatarUrl(res.data ? URL.createObjectURL(res.data) : null));
} catch {
getStore().dispatch(setAvatarUrl(null));
}
});
yield* fork(newsInitSaga);
yield* fork(initChatAvatars);
yield* fork(initStorageData);
yield* fork(chatsCacheSyncSaga);
yield* fork(initSurveys);
}
export function* criticalDesktopReceiverSaga() {
if (yield* select((state: RootState) => state.coreSlice.desktopCriticalDataReceived)) {
return;
}
// Активируем соединение для большого количества виджетов
wsEndpointStompClient.activate();
// Активация контроллера системных уведомлений
const isSpfiTrader = yield* select(getIsSpfiTrader);
systemNotificationController.activate(isSpfiTrader);
const customersAndChatsFork = yield* fork(customersAndChatsSaga);
const workspacesFork = yield* fork(fullWorkspacesUploadSaga, { loadCurrentOnly: true });
// eslint-disable-next-line func-names -- в сагах нормально
const contractsAndTradeFork = yield* fork(function* () {
yield call(contractsRequestSaga);
});
// eslint-disable-next-line func-names -- в сагах нормально
const massRequestsFork = yield* fork(function* () {
yield all([
// Нужно в виджете контакты, входящий звонок, список переговоров
requestAndPut(setFirms, api.getFirms, MetricsAction.LOADING_SCREEN_TIME_CATA_CACHE_FIRMS),
// Отключено в рамках TRADERADAR-12666 Отключение источника НРД
// requestAndPut(addNSDCost, api.getNSDCost, MetricsAction.LOADING_SCREEN_TIME_DATA_NSDCOST),
requestAndPut(
setIntrumentLists,
instrumentListsController.getInstrumentLists,
MetricsAction.LOADING_SCREEN_TIME_LISTINSTRUMENTS,
),
// TODO: убрать отсюда когда появится статус загрузки
requestAndPut(
setPersonalData,
profileController.getPersonalData,
MetricsAction.LOADING_SCREEN_TIME_USERS_PERSONAL_DATA,
),
// TODO: убрать отсюда когда появится статус загрузки
requestAndPut(
setPrivacySettings,
profileController.getPrivacySettings,
MetricsAction.LOADING_SCREEN_TIME_USERS_PRIVACY_SETTINGS,
),
]);
});
yield* join([customersAndChatsFork, workspacesFork, contractsAndTradeFork, massRequestsFork]);
yield put(metricsAction(MetricsAction.LOADING_SCREEN_TIME_END));
yield put(desktopCriticalDataReceived());
yield* call(backgroundDesktopReceiverSaga);
}
import isEqual from 'lodash/isEqual';
import api from '@api/index';
import { isRemovalFlag } from '@store/controllers/StateDiff/utils/addRemovalFlag';
import { PublicContextItem } from '@store/slices/publicContext';
import { packWidgetProps } from '@utils/widgetPropsProceeders';
import { convertPxToPercents } from '@utils/widgetSizesNPositionProceeders';
import { getStore } from '../../store';
import type {
ApiCallBack,
ApiController,
ApiControllerKeys,
CollapseDiffStateType,
DiffResult,
DiffType,
MainDiffResult,
PartialWidget,
PartialWorkspace,
} from './types';
import type { RootState } from '@store/store';
import type { AxiosResponse } from 'axios';
import type { Widget, WidgetPackedProps } from 'types/Widgets';
import type { Workspace } from 'types/Workspace';
class StateDiffController {
private previosStoreState: RootState = {} as RootState;
private prevState: RootState = {} as RootState;
private currentDiff: MainDiffResult[] | undefined = undefined;
private apiController: ApiController = {
workspaces: api.workspaceController,
widgets: {
delete: api.widgetsController.delete,
update: api.widgetPropertiesController.update,
},
publicContext: { update: api.widgetsController.update, delete: undefined },
};
public init = (state: RootState): this => {
this.previosStoreState = state;
this.prevState = state;
return this;
};
public acceptLoadedData(widgets: Widget[], publicContext: PublicContextItem[]): void {
const updateBaseline = (state: RootState): RootState => {
if (!state.widgets || !state.publicContext) {
return state;
}
const widgetIds = new Set(state.widgets.widgets.map(({ id }) => id));
const publicContextIds = new Set(state.publicContext.publicContext.map(({ id }) => id));
return {
...state,
widgets: {
...state.widgets,
widgets: [...state.widgets.widgets, ...widgets.filter(({ id }) => !widgetIds.has(id))],
},
publicContext: {
...state.publicContext,
publicContext: [
...state.publicContext.publicContext,
...publicContext.filter(({ id }) => !publicContextIds.has(id)),
],
},
};
};
this.previosStoreState = updateBaseline(this.previosStoreState);
this.prevState = updateBaseline(this.prevState);
}
public async saveDiff(): Promise<void> {
if (!this.currentDiff) {
// eslint-disable-next-line no-console -- Видимо нужно для отладки
console.error('TERMINAL STATE DIFF CONTROLLER: current diff is undefined, call getDiff before call saveDiff');
return;
}
this.currentDiff.forEach((elemDiff) => {
const { result, type } = elemDiff as MainDiffResult;
if (
type === 'workspaces' &&
result.type === 'update' &&
(result.diff as PartialWorkspace[])[0].name !==
this.prevState.workspaces.workspaces.find((el) => el.id === (result.diff as PartialWorkspace[])[0].id)?.name
) {
return;
}
if (
type === 'workspaces' &&
result.type === 'update' &&
(result.diff as PartialWorkspace[])[0].isCurrent !==
this.prevState.workspaces.workspaces.find((el) => el.id === (result.diff as PartialWorkspace[])[0].id)
?.isCurrent
) {
return;
}
if (
type === 'workspaces' &&
result.type === 'update' &&
(result.diff as PartialWorkspace[])[0].saved !==
this.prevState.workspaces.workspaces.find((el) => el.id === (result.diff as PartialWorkspace[])[0].id)?.saved
) {
return;
}
if (type === 'nothing' || result.type === 'nothing') {
return;
}
const apiCallback = [elemDiff.type, elemDiff.result.type].includes('nothing')
? undefined
: this.getApiCallback(elemDiff.type as ApiControllerKeys, elemDiff.result.type as CollapseDiffStateType);
if (!apiCallback) {
return;
}
if (result.type === 'update' && type === 'widgets') {
if ('length' in result.diff) {
result.diff.forEach((item) => {
const { isRemoval, ...itemWithoutRemovalFlag } = item;
const widget = getStore().getState().widgets.widgets.filter((w) => w.id === itemWithoutRemovalFlag.id)[0];
if ('withoutSend' in widget) {
if (!widget.withoutSend) {
const packedProps = convertPxToPercents(packWidgetProps(itemWithoutRemovalFlag));
(
apiCallback as unknown as (
data: ReturnType<typeof packWidgetProps>,
) => Promise<AxiosResponse<WidgetPackedProps[]>>
)({ ...packedProps, isRemoval: !!isRemoval });
}
} else {
const packedProps = convertPxToPercents(packWidgetProps(itemWithoutRemovalFlag));
(
apiCallback as unknown as (
data: ReturnType<typeof packWidgetProps>,
) => Promise<AxiosResponse<WidgetPackedProps[]>>
)({ ...packedProps, isRemoval: !!isRemoval });
}
});
}
return;
}
if (Array.isArray(result.diff)) {
(result.diff as Workspace[] | Widget[]).forEach((item) =>
(apiCallback as (data: typeof item) => Promise<AxiosResponse<undefined>>)(item),
);
} else {
(apiCallback as (data: typeof result.diff) => Promise<AxiosResponse<undefined>>)(result.diff as Workspace);
}
});
this.currentDiff = undefined;
this.prevState = this.previosStoreState;
}
public getDiff = (state: RootState): this => {
let type: DiffType | 'nothing' = 'nothing';
const result: DiffResult = {
type: 'nothing',
diff: [],
};
if (
!this.previosStoreState ||
(this.previosStoreState.widgets.widgets.length === 0 && state.widgets.widgets.length !== 0) ||
(this.previosStoreState.publicContext.publicContext.length === 0 &&
state.publicContext.publicContext.length !== 0) ||
(this.previosStoreState.workspaces.workspaces.find((item) => item.id === -1) &&
!state.workspaces.workspaces.find((item) => item.id === -1))
) {
this.previosStoreState = state;
const res: MainDiffResult = {
type: 'nothing',
result: { type: 'nothing', diff: [] },
};
this.currentDiff = [res];
return this;
}
const stateEntries = Object.entries(state) as [keyof RootState, RootState[keyof RootState]][];
const diffProceeder: Partial<Record<keyof RootState, (s: RootState) => DiffResult>> = {
workspaces: this.getWorkspaceDiff,
widgets: this.getWidgetsDiff,
publicContext: this.getPublicContextDiff,
};
this.currentDiff = [];
for (let i = 0; i < stateEntries.length; i += 1) {
const [key, value] = stateEntries[i];
if (!isEqual(value, this.previosStoreState[key])) {
type = key;
if (diffProceeder[key]) {
const diff = (diffProceeder[key] as (s: RootState) => DiffResult)(state);
result.type = diff.type;
result.diff = diff.diff;
this.currentDiff.push({
type,
result: { ...result },
});
}
}
}
this.previosStoreState = state;
return this;
};
private getWorkspaceDiff = (state: RootState): DiffResult => {
const { workspaces } = state;
const prevWorkspaces = this.previosStoreState.workspaces;
const newItemsIds: Workspace['id'][] = [];
if (
prevWorkspaces.workspaces.filter((item) => item.id !== -1).length >
workspaces.workspaces.filter((item) => item.id !== -1).length
) {
const existId = workspaces.workspaces.map(({ id }) => id);
return {
type: 'delete',
diff: prevWorkspaces.workspaces.filter(({ id }) => id > 1).find(({ id }) => !existId.includes(id)) as Workspace,
};
}
if (prevWorkspaces.workspaces.length < workspaces.workspaces.length) {
const lastExistIds = prevWorkspaces.workspaces.map(({ id }) => id);
workspaces.workspaces
.map(({ id }) => id)
.filter((id) => !lastExistIds.includes(id))
.forEach((id) => newItemsIds.push(id));
}
const diff: PartialWorkspace[] = [];
workspaces.workspaces.forEach((item, index) => {
if (newItemsIds.includes(item.id)) {
return undefined;
}
const prevItem = prevWorkspaces.workspaces[index];
const keys = Object.keys(item).filter((key) => key !== 'id') as (keyof Workspace)[];
for (let i = 0; i < keys.length; i += 1) {
const key = keys[i];
if (item[key] !== prevItem[key] && item.id > 0) {
const diffItemIndex = diff.findIndex(({ id }) => id === item.id);
if (diffItemIndex !== -1) {
diff[diffItemIndex] = item;
} else {
diff.push(item);
}
}
}
return undefined;
});
const type = diff.length > 0 ? 'update' : 'nothing';
return {
type,
diff,
};
};
private getWidgetsDiff = (state: RootState): DiffResult => {
const { widgets: newWidgets } = state.widgets;
const prevWidgets = this.previosStoreState.widgets.widgets;
// Исключаем случай с добавлением виджета
const widgets = newWidgets.filter(({ id }) => prevWidgets.some((item) => item.id === id));
if (widgets.length < prevWidgets.length) {
const existIds = widgets.map(({ id }) => id);
return {
type: 'delete',
diff: prevWidgets.filter(({ id }) => id > 1).filter(({ id }) => !existIds.includes(id)),
};
}
const diff: PartialWidget[] = [];
widgets.forEach((widget, index) => {
const prevWidget = prevWidgets[index];
const keys = Object.keys(widget).filter((key) => key !== 'id') as Exclude<keyof Widget, 'id'>[];
for (let i = 0; i < keys.length; i += 1) {
const key = keys[i];
if (prevWidget[key] !== widget[key]) {
const diffItemIndex = diff.findIndex(({ id }) => id === widget.id);
if (diffItemIndex !== -1) {
(diff[diffItemIndex][key] as Widget[keyof Widget]) = widget[key];
} else {
const isNeedToRemoveProperties =
key === 'widgetContentProps' && isRemovalFlag(widget[key], prevWidget[key]);
diff.push({ id: widget.id, [key]: widget[key], isRemoval: isNeedToRemoveProperties });
}
}
}
});
const type = diff.length > 0 ? 'update' : 'nothing';
return {
type,
diff,
};
};
private getPublicContextDiff = (state: RootState): DiffResult => {
const { widgets } = state.widgets;
const { publicContext } = state.publicContext;
const prevPublicContext = this.previosStoreState.publicContext.publicContext;
const diff: PublicContextItem[] = [];
publicContext.forEach((item) => {
const findInd = prevPublicContext.findIndex((elem) => elem.id === item.id);
if (findInd === -1 || !isEqual(item, prevPublicContext[findInd])) {
const resultPublicProps = Array.from(item.publicProperties || []).reduce(
(acc, curr, ind) => ({ ...acc, [ind]: JSON.stringify(curr) }),
{},
);
const findWidget = widgets.find((elem) => elem.id === item.id);
diff.push({
id: item.id,
color: item.color,
name: findWidget?.name ?? '',
workspaceId: findWidget?.workspaceId ?? 0,
publicProperties: resultPublicProps,
} as PublicContextItem);
}
});
return {
type: 'update',
diff,
};
};
private getApiCallback<T extends ApiControllerKeys, R extends CollapseDiffStateType>(
mainType: T,
type: R,
): ApiCallBack<T, R> | undefined {
if (!this.apiController[mainType as keyof ApiController]) {
return undefined;
}
return this.apiController[mainType as keyof ApiController][type as CollapseDiffStateType] as ApiCallBack<T, R>;
}
}
export const stateDiffController = new StateDiffController();
import api from '@api/index';
import { setWidgetParentWorkspaceId } from '@store/slices/widgets';
import { addWorkspace, markWorkspaceLoaded, setCurrentWorkspace } from '@store/slices/workspaces';
import { getStore } from '@store/store';
import type { RequiredWithPartial } from 'types/utilityTypes';
import type { Workspace } from 'types/Workspace';
type CreateWorkspaceArgs = RequiredWithPartial<Omit<Workspace, 'id' | 'isCurrent' | 'position'>, 'isAddWidgets'>;
type CreateWorkspaceResult = Pick<Workspace, 'id'>;
export default async function createWorkspace(params: CreateWorkspaceArgs): Promise<CreateWorkspaceResult> {
const position = getStore().getState().workspaces.workspaces.length;
const newWorkspace: Workspace = {
...params,
id: 0,
isCurrent: true,
saved: true,
pinned: false,
position: getStore().getState().workspaces.workspaces.length,
isNew: params.isNew ?? true,
isAddWidgets: params.isAddWidgets ?? true,
};
const { workspaceController } = api;
await workspaceController
.create({
name: params.name,
position,
isCurrent: true,
isNew: params.isNew ?? true,
isAddWidgets: params.isAddWidgets ?? true,
favorite: false,
})
.then((res) => {
const { id } = res.data;
newWorkspace.id = id;
getStore().dispatch(markWorkspaceLoaded(id));
getStore().dispatch(addWorkspace(newWorkspace));
getStore().dispatch(setCurrentWorkspace(id));
const widgets = getStore()
.getState()
.widgets.widgets.filter(({ workspaceId }) => newWorkspace.id === workspaceId)
.map((widget) => ({
id: widget.id,
workspaceId: id,
}));
if (widgets.length > 0) {
getStore().dispatch(setWidgetParentWorkspaceId(widgets));
}
});
return {
id: newWorkspace.id,
};
}
import isNil from 'lodash/isNil';
import api from '@api/index';
import {
BIND_ICON_COLORS,
MOBILE_SIZE,
WIDGET_DEFAULT_SIZE_LIMITS,
WIDGET_DEFAULT_SIZES,
Z_INDEX,
} from '@configs/appConfig';
import { widgetPropertiesMap } from '@modules/widgetProperties';
import { createChatWidget } from '@store/slices/chatSlice';
import { addValuesToPublicContext } from '@store/slices/publicContext';
import { setCreatedWidgetId } from '@store/slices/searchNews';
import { addWidget } from '@store/slices/widgets';
import { getStore } from '@store/store';
import { Widget, WidgetContentType } from 'types/Widgets';
import { getLimits } from '../widgets/utils/limits';
import { getWidgetSizes } from '../widgets/utils/sizes';
import { createBindMap } from './createBindMap';
import getDefaultPositionForWidget from './getDefaultPositionForWidget';
import { getIssKeyFromWidgets } from './getIssKeyFromWidgets';
import getWorkspaceSizes from './getWorkspaceSizes';
import { ensureWorkspaceLoaded } from './loadWorkspaceWidgets';
import { packWidgetProps } from './widgetPropsProceeders';
import { convertPxToPercents } from './widgetSizesNPositionProceeders';
import type { PartialWithRequired } from 'types/utilityTypes';
type CreateWidgetArgs = PartialWithRequired<Widget, 'workspaceId' | 'type'>;
type CreatedWidgetData = Pick<Widget, 'id' | 'name' | 'workspaceId'>;
type BuildNewWidgetsProps = {
data: CreatedWidgetData[];
items: CreateWidgetArgs[];
basicWidgetZIndex: number;
isMobileView: boolean;
workspaceSizes: { width: number; height: number };
center: boolean;
};
function getBasicWidgetZIndex(items: CreateWidgetArgs[]): number {
const currentWidgets = getStore()
.getState()
.widgets.widgets.filter((item) => items[0]?.workspaceId === item?.workspaceId);
return currentWidgets.length > 0
? currentWidgets.map((item) => item.zIndex).sort((a, b) => b - a)[0] + 1
: Z_INDEX.WIDGET_BASE;
}
function isAddingWidgetsAllowed(workspaceId: Widget['workspaceId']): boolean {
const workspace = getStore()
.getState()
.workspaces.workspaces.find(({ id }) => id === workspaceId);
return Boolean(workspace?.isAddWidgets);
}
function isWidgetsLimitExceeded(workspaceId: Widget['workspaceId']): boolean {
const widgetsOnWorkspace = getStore()
.getState()
.widgets.widgets.filter((widget) => widget.workspaceId === workspaceId);
return widgetsOnWorkspace.length > 49;
}
function buildNewWidgets({
data,
items,
basicWidgetZIndex,
isMobileView,
workspaceSizes,
center,
}: BuildNewWidgetsProps): Widget[] {
return data.map(({ id, name, workspaceId }, index) => {
const argItem = items[index];
const newWidgetZIndex = basicWidgetZIndex + 1 + index;
const widgetSizes = getWidgetSizes(argItem.type, isMobileView, workspaceSizes);
return {
...argItem,
id,
name,
workspaceId,
type: argItem.type,
sizes: argItem.sizes ?? widgetSizes ?? WIDGET_DEFAULT_SIZES,
sizeLimits: argItem.sizeLimits || (getLimits(argItem.type) ?? WIDGET_DEFAULT_SIZE_LIMITS),
zIndex: argItem.zIndex ?? newWidgetZIndex,
position:
argItem.position ?? getDefaultPositionForWidget(widgetSizes ?? WIDGET_DEFAULT_SIZES, workspaceId, center),
widgetContentProps: argItem.widgetContentProps ?? widgetPropertiesMap[argItem.type] ?? {},
isExpand: argItem.isExpand ?? false,
beforeExpandParams: argItem.beforeExpandParams ?? null,
};
});
}
function createOldToNewWidgetIdMap(items: CreateWidgetArgs[], newWidgets: Widget[]): Record<number, number> {
const oldToNewWidgetIdMap: Record<number, number> = {};
// Что-то страшное. Если количество не совпадает то не можем смаппить
if (newWidgets.length === items.length) {
newWidgets.forEach((widget, index) => {
const oldId = items[index].id;
if (typeof oldId !== 'undefined') {
oldToNewWidgetIdMap[oldId] = widget.id;
}
});
}
return oldToNewWidgetIdMap;
}
function updateWidgetMasters(widgets: Widget[], oldToNewWidgetIdMap: Record<number, number>): Widget[] {
return widgets.map((widget) => {
const master = isNil(widget.master) ? widget.master : (oldToNewWidgetIdMap[widget.master] ?? widget.master);
const masters = isNil(widget.masters) ? widget.masters : widget.masters?.map((m) => oldToNewWidgetIdMap[m] ?? m);
return {
...widget,
master: master || null,
masters: masters || [],
};
});
}
function dispatchCreatedWidgetSideEffects(newWidgets: Widget[]): void {
const firstWidget = newWidgets[0];
// Запись этого ID должна быть максильно близко к записи виджетов в стор
if (firstWidget.type === WidgetContentType.news) {
getStore().dispatch(setCreatedWidgetId(firstWidget.id));
}
if (firstWidget.type === WidgetContentType.noTradeChat) {
getStore().dispatch(createChatWidget(String(firstWidget.id)));
}
}
function updateWidgetPropertiesAndPublicContext(newWidgets: Widget[]): void {
const { widgetPropertiesController } = api;
const bindMap = createBindMap(newWidgets);
let masterIndex = 0;
// Создать новые связки убрав предыдущие
// diff контроллер к этому моменту ещё не прошёлся по новыми виджетам
// И добавление пропертей ничего не даст. Плюс он не анализирует добавление виджетов
newWidgets.forEach((widget) => {
const { properties } = packWidgetProps({
master: widget.master,
masters: widget.masters,
});
widgetPropertiesController.update({
widgetId: widget.id,
properties,
});
const masterPublicContext = getStore()
.getState()
.publicContext.publicContext?.find(({ id }) => id === widget.id);
if (!widget.masters?.length && !masterPublicContext && Object.keys(bindMap).includes(widget.id.toString())) {
const instrumentId = getIssKeyFromWidgets(widget);
getStore().dispatch(
addValuesToPublicContext({
id: widget.id,
color: BIND_ICON_COLORS[masterIndex],
publicProperties: [{ name: 'instrId', value: instrumentId }],
}),
);
masterIndex += 1;
}
});
}
// Фактически результат нигде не используется
export default async function createWidget(items: CreateWidgetArgs[], center = false): Promise<Widget[]> {
const currentWorkspaceId = items.map(({ workspaceId }) => workspaceId)[0];
if (!isAddingWidgetsAllowed(currentWorkspaceId)) {
return [];
}
const workspaceIds = Array.from(new Set(items.map(({ workspaceId }) => workspaceId)));
await Promise.all(workspaceIds.map((workspaceId) => ensureWorkspaceLoaded(workspaceId)));
if (!isAddingWidgetsAllowed(currentWorkspaceId)) {
return [];
}
if (isWidgetsLimitExceeded(currentWorkspaceId)) {
// eslint-disable-next-line no-alert -- нужный alert
alert('Виджетов может быть максимум 50 на одной странице!');
return [];
}
const isMobileView = window.screen.width <= MOBILE_SIZE || window.innerWidth <= MOBILE_SIZE;
const workspaceSizes = getWorkspaceSizes();
const { widgetsController, widgetPropertiesController } = api;
const basicWidgetZIndex = getBasicWidgetZIndex(items);
const res = await widgetsController.create(
items.map(({ name, workspaceId }) => ({
workspaceId,
name: name ?? '',
})),
);
if (!res.data) {
return [];
}
const newWidgets = buildNewWidgets({
data: res.data,
items,
basicWidgetZIndex,
isMobileView,
workspaceSizes,
center,
});
await widgetPropertiesController.create(newWidgets.map((widget) => convertPxToPercents(packWidgetProps(widget))));
const oldToNewWidgetIdMap = createOldToNewWidgetIdMap(items, newWidgets);
const newWidgetsWithUpdatedMasters = updateWidgetMasters(newWidgets, oldToNewWidgetIdMap);
dispatchCreatedWidgetSideEffects(newWidgetsWithUpdatedMasters);
getStore().dispatch(addWidget(newWidgetsWithUpdatedMasters));
updateWidgetPropertiesAndPublicContext(newWidgetsWithUpdatedMasters);
return newWidgetsWithUpdatedMasters;
}
import React, { FC, PropsWithChildren, useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { MAX_FAVORITE_WORKSPACES } from '@configs/appConfig';
import { useAppSelect } from '@hooks/useAppSelector';
import { isFavoritesLimitExceedSelector } from '@store/selectors/modals';
import { workspaceByIdSelector } from '@store/selectors/workspaces';
import { openSaveWorkspaceModal } from '@store/slices/modals';
import { togglePinWorkspace } from '@store/slices/workspaces';
import { getStore } from '@store/store';
import { createNameForNewWorkspace } from '@terminal/desktop/components/Header/utils';
import { useWorkspaceActions } from '@terminal/hooks/useWorkspaceActions';
import { toasts } from '@terminal/services/toasts';
import { exportJSONFile } from '@terminal/utils/exportJSONFile';
import { ContextMenu } from '@uikit/ContextMenu';
import { ContextMenuItem } from '@uikit/ContextMenu/types';
import createWidget from '@utils/createWidget';
import createWorkspace from '@utils/createWorkspace';
import { deepClone } from '@utils/deepClone';
import { ensureWorkspaceLoaded } from '@utils/loadWorkspaceWidgets';
import { SaveWorkspaceActionType } from '../SaveWorkspaceModal/types';
import { WorkspaceMenuItemsKeys } from './const';
import type { Widget } from 'types/Widgets';
import type { Workspace } from 'types/Workspace';
export type WorkspaceContextMenuProps = PropsWithChildren<{
workspaceId: Workspace['id'];
disabled?: boolean;
onOpenChange?: (open: boolean) => void;
hiddenItems?: WorkspaceMenuItemsKeys[];
open?: boolean;
}>;
export const WorkspaceContextMenu: FC<WorkspaceContextMenuProps> = function ({
workspaceId,
disabled,
onOpenChange,
children,
hiddenItems,
open,
}): JSX.Element | null {
const workspace = useAppSelect(workspaceByIdSelector(workspaceId));
const workspacesNames = useAppSelect((state) => state.workspaces.workspaces).map((item) => item.name);
const dispatch = useDispatch();
const isFavoritesLimitExceed = useAppSelect(isFavoritesLimitExceedSelector(MAX_FAVORITE_WORKSPACES));
const { deleteWorkspace, closeWorkspace, removeWorkspaceFormFavorites } = useWorkspaceActions();
const name = workspace?.name ?? '';
const code = workspace?.code;
const pinned = workspace?.pinned ?? false;
const favorite = workspace?.favorite ?? false;
const onDuplicateBtnClick = useCallback(async () => {
const newWorkspaceName = createNameForNewWorkspace(name, workspacesNames);
try {
await ensureWorkspaceLoaded(workspaceId);
const widgets = getStore()
.getState()
.widgets.widgets.filter((widget) => widget.workspaceId === workspaceId);
const { id } = await createWorkspace({
name: newWorkspaceName,
isNew: false,
});
const newWidgets = widgets.map((item) => {
const newWidget = deepClone(item) as Omit<Widget, 'id'> & Partial<Pick<Widget, 'id'>>;
delete newWidget.id;
newWidget.workspaceId = id;
// Удаляет id списка инструментов при дублировании рабочего стола
delete newWidget?.widgetContentProps?.widgetQuotesState?.instrumentListId;
return newWidget;
});
await createWidget(newWidgets);
toasts.duplicateWorkspace.success(newWorkspaceName);
} catch {
toasts.duplicateWorkspace.error(newWorkspaceName);
}
}, [workspaceId, name, workspacesNames]);
const onPinnedWorkspaceClick = useCallback(() => {
dispatch(togglePinWorkspace({ id: workspaceId }));
}, [dispatch, workspaceId]);
const addToFavorites = useCallback(() => {
dispatch(openSaveWorkspaceModal({ workspaceId, type: SaveWorkspaceActionType.Favorite }));
}, [dispatch, workspaceId]);
const removeFromFavorites = useCallback(() => {
removeWorkspaceFormFavorites(workspaceId);
}, [removeWorkspaceFormFavorites, workspaceId]);
const renameWorkspace = useCallback(() => {
dispatch(openSaveWorkspaceModal({ workspaceId, type: SaveWorkspaceActionType.Rename }));
}, [dispatch, workspaceId]);
const menuItems: ContextMenuItem[] = useMemo(
() =>
[
!code && {
key: WorkspaceMenuItemsKeys.Rename,
label: 'Редактировать название',
onClick: renameWorkspace,
},
{ key: WorkspaceMenuItemsKeys.Copy, label: 'Дублировать вкладку', onClick: onDuplicateBtnClick },
!code && {
key: WorkspaceMenuItemsKeys.Pin,
label: pinned ? 'Открепить вкладку' : 'Закрепить вкладку',
onClick: onPinnedWorkspaceClick,
},
{
key: WorkspaceMenuItemsKeys.Favorite,
label: favorite ? 'Удалить из избранных' : 'Добавить в избранные',
onClick: favorite ? removeFromFavorites : addToFavorites,
disabled: !favorite && isFavoritesLimitExceed,
},
{
key: WorkspaceMenuItemsKeys.Delete,
label: 'Удалить рабочий стол',
onClick: () => deleteWorkspace(workspaceId),
},
{
key: WorkspaceMenuItemsKeys.Export,
label: 'Выгрузить рабочий стол в файл',
onClick: () => exportJSONFile(workspaceId, name),
},
{
key: WorkspaceMenuItemsKeys.Close,
label: 'Закрыть',
onClick: () => closeWorkspace?.(workspaceId),
},
]
.filter((item) => !!item)
.filter((item) => !hiddenItems?.includes(item.key)),
[
addToFavorites,
closeWorkspace,
code,
deleteWorkspace,
favorite,
hiddenItems,
isFavoritesLimitExceed,
name,
onDuplicateBtnClick,
onPinnedWorkspaceClick,
pinned,
removeFromFavorites,
renameWorkspace,
workspaceId,
],
);
if (!workspace) {
return null;
}
return (
<ContextMenu
onOpenChange={onOpenChange}
items={menuItems}
disabled={disabled}
zIndex={10001}
open={open}
>
{children}
</ContextMenu>
);
};