Загрузка данных
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: [],
};
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);
},
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;
}
},
},
});
export const {
addWorkspace,
removeWorkspace,
setCurrentWorkspace,
setWorkspaceName,
togglePinWorkspace,
setClosedWorkspace,
setWorkspaces,
openWorkspace,
addToFavorites,
removeFromFavorites,
updateWorkspace,
changeCurrentWorkspace,
updateLayout,
} = workspacesSlice.actions;
export default workspacesSlice.reducer;
import { createSlice } from '@reduxjs/toolkit';
import { TSelectedTableType } from '@components/VirtualTable';
import { WIDGETS_CAN_BE_MULTIPLE_MASTERS, Z_INDEX } from '@configs/appConfig';
import { bind, isWidgetHasMaster, pinch, unbind } from '@utils/bind';
import { OrderbookWidgetConfig, Widget } from 'types/Widgets';
import type { PayloadAction } from '@reduxjs/toolkit';
export interface WidgetsState {
widgets: Widget[];
widgetsConfig: {
glass: OrderbookWidgetConfig;
};
}
const initialState: WidgetsState = {
widgets: [],
widgetsConfig: {
glass: {
view: 'four-col-3',
showYield: false,
showSpread: false,
choosenInstrument: '',
},
},
};
export const columnKeys = {
1: 'firstColumns',
2: 'secondColumns',
3: 'thirdColumns',
4: 'forthColumns',
};
export type WidgetChangePayload<T extends Exclude<keyof Widget, 'id'>> = Pick<Widget, 'id' | T> & {
withoutSend?: boolean;
} & { widgetTableColSizes?: Record<string, unknown> } & {
moved?: boolean;
};
const getWidgetIndex = (widgetId: Widget['id'], widgets: Widget[]): number =>
widgets.findIndex(({ id }) => id === widgetId);
const widgetsSlice = createSlice({
name: 'workspace',
// `createSlice` will infer the state type from the `initialState` argument
initialState,
reducers: {
addWidget: (state, { payload }: PayloadAction<Widget | Widget[]>) => {
if (Array.isArray(payload)) {
if (payload.length > 0) {
state.widgets = [...state.widgets, ...payload];
}
} else {
state.widgets = [...state.widgets, payload];
}
},
removeWidget: (state, { payload }: PayloadAction<Widget['id'] | Widget['id'][]>) => {
const widgetsIdList = Array.isArray(payload) ? payload : [payload];
// Поиск виджетов с удалённым мастером
state.widgets = state.widgets.filter(({ id }) => !widgetsIdList.includes(id));
// Поиск и удаление связи с удалённым мастер виджетом
const slaveWidgets = state.widgets.filter((w) =>
widgetsIdList.some((widgetId) => isWidgetHasMaster(w, widgetId)),
);
slaveWidgets.forEach((slaveWidget) => {
widgetsIdList.forEach((masterId) => unbind(slaveWidget, masterId));
});
},
setZIndexForWidget: (state, { payload }: PayloadAction<WidgetChangePayload<'zIndex'>>) => {
const changedWidgetIndex = getWidgetIndex(payload.id, state.widgets);
if (changedWidgetIndex === -1) {
return;
}
state.widgets[changedWidgetIndex].zIndex = payload.zIndex;
state.widgets[changedWidgetIndex].withoutSend = false;
},
setSizesForWidget: (state, { payload }: PayloadAction<WidgetChangePayload<'sizes'>>) => {
const changedWidgetIndex = getWidgetIndex(payload.id, state.widgets);
state.widgets[changedWidgetIndex].withoutSend = !!payload.withoutSend;
state.widgets[changedWidgetIndex].sizes = payload.sizes;
},
setWidgetSizeLimits: (state, { payload }: PayloadAction<WidgetChangePayload<'sizeLimits'>>) => {
const changedWidgetIndex = getWidgetIndex(payload.id, state.widgets);
if (!state.widgets[changedWidgetIndex]?.sizeLimits) {
return;
}
state.widgets[changedWidgetIndex].sizeLimits = payload.sizeLimits;
},
setPositionForWidget: (state, { payload }: PayloadAction<WidgetChangePayload<'position'>>) => {
const changedWidgetIndex = getWidgetIndex(payload.id, state.widgets);
state.widgets[changedWidgetIndex].withoutSend = !!payload.withoutSend;
state.widgets[changedWidgetIndex].position = {
...state.widgets[changedWidgetIndex].position,
x: payload.position.x,
y: payload.position.y,
};
state.widgets[changedWidgetIndex].moved = !!payload.moved;
},
addContentPropsToWidget: (state, { payload }: PayloadAction<WidgetChangePayload<'widgetContentProps'>>) => {
const changedWidgetIndex = getWidgetIndex(payload.id, state.widgets);
if (changedWidgetIndex !== -1) {
if (!payload.withoutSend) {
// делаем запрос после обновления стора
state.widgets[changedWidgetIndex].withoutSend = false;
}
state.widgets[changedWidgetIndex].widgetContentProps = {
...state.widgets[changedWidgetIndex].widgetContentProps,
...payload.widgetContentProps,
};
}
},
updateWidgetContentProps: (
state,
{
payload,
}: PayloadAction<{
id: number;
withoutSend?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- конкретный тип задается при dispatch
updater: (draft: any) => void;
}>,
) => {
const changedWidgetIndex = getWidgetIndex(payload.id, state.widgets);
if (changedWidgetIndex !== -1) {
if (!payload.withoutSend) {
// делаем запрос после обновления стора
state.widgets[changedWidgetIndex].withoutSend = false;
}
payload.updater(state.widgets[changedWidgetIndex].widgetContentProps ?? {});
}
},
setColumnsWidthToWidget: (
state,
{
payload,
}: PayloadAction<{
widgetId: number;
widgetTableColSizes: {
1?: string[];
2?: string[];
3?: string[];
4?: string[];
};
selectedTableType: TSelectedTableType;
}>,
) => {
const changedWidgetIndex = getWidgetIndex(payload.widgetId, state.widgets);
const columnKey = columnKeys[payload.selectedTableType];
const columns = state.widgets[changedWidgetIndex].widgetContentProps.state[columnKey].map(
(column: Record<string, unknown>) => ({
...column,
}),
);
let index = 0;
const { selectedTableType } = payload;
const tableColSizes = payload.widgetTableColSizes[selectedTableType];
if (tableColSizes) {
columns.forEach((column: Record<string, unknown>) => {
if (!column.hidden && tableColSizes[index]) {
// eslint-disable-next-line no-param-reassign -- в редьюсере можно
column.width = tableColSizes[index];
index += 1;
}
});
}
state.widgets[changedWidgetIndex].widgetContentProps.state[columnKey] = columns;
},
setWidgetName: (state, { payload }: PayloadAction<WidgetChangePayload<'name'>>) => {
const changedWidgetIndex = getWidgetIndex(payload.id, state.widgets);
if (changedWidgetIndex > -1) {
state.widgets[changedWidgetIndex].name = payload.name;
}
},
setWidgetExpand: (
state,
{ payload }: PayloadAction<WidgetChangePayload<'isExpand' | 'beforeExpandParams' | 'position' | 'sizes'>>,
) => {
const changedWidgetIndex = getWidgetIndex(payload.id, state.widgets);
if (changedWidgetIndex > -1) {
state.widgets[changedWidgetIndex].isExpand = payload.isExpand;
state.widgets[changedWidgetIndex].beforeExpandParams = payload.beforeExpandParams;
state.widgets[changedWidgetIndex].position = payload.position;
state.widgets[changedWidgetIndex].sizes = payload.sizes;
}
},
setWidgetParentWorkspaceId: (
state,
{ payload }: PayloadAction<WidgetChangePayload<'workspaceId'> | WidgetChangePayload<'workspaceId'>[]>,
) => {
if ((payload as WidgetChangePayload<'workspaceId'>[]).length > 0) {
(payload as WidgetChangePayload<'workspaceId'>[]).forEach(({ id, workspaceId }) => {
const changedWidgetIndex = getWidgetIndex(id, state.widgets);
if (changedWidgetIndex > -1) {
state.widgets[changedWidgetIndex].workspaceId = workspaceId;
}
});
} else {
const { id, workspaceId } = payload as WidgetChangePayload<'workspaceId'>;
const changedWidgetIndex = getWidgetIndex(id, state.widgets);
if (changedWidgetIndex > -1) {
state.widgets[changedWidgetIndex].workspaceId = workspaceId;
}
}
},
addWidgetBinding: (
state,
{
payload,
}: PayloadAction<{
masterId: Widget['id'];
idList: Widget['id'][];
}>,
) => {
// предотвращает бесконечный цикл подписок
const masterWidget = state.widgets.find((w) => w.id === payload.masterId);
if (masterWidget && masterWidget?.master && payload.idList.includes(masterWidget?.master)) {
unbind(masterWidget, masterWidget?.master);
}
state.widgets
.filter((w) => !payload.idList.includes(w.id) && isWidgetHasMaster(w, payload.masterId))
.forEach((w) => unbind(w, payload.masterId));
const canAllMastersBeMultipleMasters = (current: Widget) => {
const canAllAlreadyMastersBeMultipleMasters = state.widgets
.filter((w) => current.master === w.id || current.masters?.includes(w.id))
.every(({ type }) => WIDGETS_CAN_BE_MULTIPLE_MASTERS.includes(type));
const canCurrentMasterBeMultipleMaster =
!!masterWidget?.type && WIDGETS_CAN_BE_MULTIPLE_MASTERS.includes(masterWidget.type);
return canAllAlreadyMastersBeMultipleMasters && canCurrentMasterBeMultipleMaster;
};
state.widgets
.filter((w) => payload.idList.includes(w.id))
.forEach((w) => bind(w, payload.masterId, canAllMastersBeMultipleMasters(w)));
},
addWidgetBindingProperties: (
state,
{
payload,
}: PayloadAction<{
widgetId: Widget['id'];
masterWidgetId: Widget['id'];
externalProperties: Record<string, string>;
}>,
) => {
const changedWidgetIndex = getWidgetIndex(payload.widgetId, state.widgets);
if (changedWidgetIndex === -1) {
return;
}
if (payload.masterWidgetId) {
if (state.widgets[changedWidgetIndex].externalProperties) {
state.widgets[changedWidgetIndex].externalProperties = [payload.externalProperties];
} else {
state.widgets[changedWidgetIndex].externalProperties = [payload.externalProperties];
}
} else {
state.widgets[changedWidgetIndex].externalProperties = null;
}
},
// Фактически не используется
setBinding: (state, { payload }: PayloadAction<{ widgetId: number; master: number | null; masters: number[] }>) => {
const changedWidgetIndex = getWidgetIndex(payload.widgetId, state.widgets);
if (changedWidgetIndex > -1) {
state.widgets[changedWidgetIndex].master = payload.master;
state.widgets[changedWidgetIndex].masters = payload.masters;
}
},
unbindWidgets: (state, { payload }: PayloadAction<{ widgetId: Widget['id'] }>) => {
if (payload.widgetId) {
const changedWidgetIndex = getWidgetIndex(payload.widgetId, state.widgets);
if (changedWidgetIndex > -1) {
state.widgets[changedWidgetIndex].master = null;
state.widgets[changedWidgetIndex].masters = [];
}
} else {
state.widgets.forEach((item) => {
// eslint-disable-next-line no-param-reassign -- в редьюсере можно
item.master = null;
// eslint-disable-next-line no-param-reassign -- в редьюсере можно
item.masters = [];
});
}
},
markAsActiveMaster: (state, { payload }: PayloadAction<{ widgetId: Widget['id'] }>) => {
state.widgets.filter((w) => isWidgetHasMaster(w, payload.widgetId)).forEach((w) => pinch(w, payload.widgetId));
},
setWidgetFocus: (state, { payload }: PayloadAction<{ widgetId: Widget['id']; inFocus: boolean }>) => {
const changedWidgetIndex = getWidgetIndex(payload.widgetId, state.widgets);
if (changedWidgetIndex > -1) {
state.widgets[changedWidgetIndex].inFocus = payload.inFocus;
}
},
optimizeWidgetsZIndexes: (
state,
{ payload }: PayloadAction<{ workspaceId: number; widgetIdWithMaxZIndex: Widget['id']; basicIndex?: number }>,
) => {
const { workspaceId, widgetIdWithMaxZIndex, basicIndex = Z_INDEX.WIDGET_BASE } = payload;
const widgetWithMaxZIndex = state.widgets.find(
(widget) => widget.workspaceId === workspaceId && widget.id === widgetIdWithMaxZIndex,
);
if (!widgetWithMaxZIndex) {
return;
}
const changedWidgets = state.widgets
.filter((widget) => widget.workspaceId === workspaceId && widget.id !== widgetWithMaxZIndex.id)
.sort((a, b) => a.zIndex - b.zIndex)
.concat(widgetWithMaxZIndex);
changedWidgets.forEach((widget, index) => {
// eslint-disable-next-line no-param-reassign -- в редьюсере можно
widget.zIndex = index + basicIndex;
// eslint-disable-next-line no-param-reassign -- в редьюсере можно
widget.withoutSend = false;
});
},
},
});
export const {
addWidget,
removeWidget,
setZIndexForWidget,
setPositionForWidget,
setSizesForWidget,
setWidgetSizeLimits,
addContentPropsToWidget,
setColumnsWidthToWidget,
setWidgetName,
setWidgetExpand,
setWidgetParentWorkspaceId,
addWidgetBinding,
addWidgetBindingProperties,
unbindWidgets,
markAsActiveMaster,
setWidgetFocus,
updateWidgetContentProps,
optimizeWidgetsZIndexes,
} = widgetsSlice.actions;
export { initialState as widgetsInitialState };
export default widgetsSlice.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 };
}
},
createValuesToPublicContext: (state, { payload }: PayloadAction<PublicContextItem[]>) => {
if (payload.length > 0) {
state.publicContext = [...payload];
} else {
state.publicContext = [];
}
},
},
});
export const { addValuesToPublicContext, createValuesToPublicContext } = publicContextSlice.actions;
export default publicContextSlice.reducer;
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[];
}
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 type { Workspace } from './Workspace';
import type { ViewType } from '@widgets/Orderbook/types';
import type { Position } from 'react-rnd';
export interface WidgetPosition {
x: number;
y: number;
widgetNumber?: number;
maxX?: number;
maxY?: number;
}
export enum WidgetContentType {
testIFrame = 'testIFrame',
graphic = 'graphic',
news = 'news',
newsIss = 'newsIss',
corporateActions = 'corporateActions',
glass = 'glass',
instruments = 'instruments',
noTradeChat = 'noTradeChat',
HHI = 'HHI',
contacts = 'contacts',
aboutInstruments = 'aboutInstruments',
fixings = 'fixings',
FUTOI = 'FUTOI',
curves = 'curves',
// Виджет Карта рынка облигаций
// для совестимости с предыдущей версией названия из /tb/api/v1/users/current -> widgetTypes
profitabilityAndDuration = 'profitabilityAndDuration',
widgetTurnovers = 'widgetTurnovers',
tradingResult = 'tradingResult',
quotesNTPro = 'quotesNTPro',
instruction = 'instruction',
macroData = 'macroData',
otcTurnover = 'otcTurnover',
techSupport = 'techSupport',
screener = 'screener',
review = 'review',
helpCenter = 'helpCenter',
ordersJournal = 'ordersJournal',
indicativeQuotes = 'indicativeQuotes',
swapCalculator = 'swapCalculator',
spfiPrices = 'spfiPrices',
issuerCard = 'issuerCard',
ntbOrdersAndDeals = 'ntbOrdersAndDeals',
ntbPositions = 'ntbPositions',
ntbLogisticAuto = 'ntbLogisticAuto',
ntbLogisticFreight = 'ntbLogisticFreight',
ntbLogisticContainers = 'ntbLogisticContainers',
ntbAnaliticsCommodities = 'ntbAnaliticsCommodities',
ntbAnalyticsContracts = 'ntbAnalyticsContracts',
tradeJournal = 'tradeJournal',
draftBrokerSpfi = 'draftBrokerSpfi',
tradeJournalDetails = 'tradeJournalDetails',
ntbIndexes = 'ntbIndexes',
depositCcpOrderTables = 'depositCcpOrderTables',
depositCcpTradeTables = 'depositCcpTradeTables',
depositCcpReferenceTables = 'depositCcpReferenceTables',
depositCcpRiskTables = 'depositCcpRiskTables',
userAdministration = 'userAdministration',
mxtAccountPositions = 'mxtAccountPositions',
}
export interface WidgetSizes {
/** 'px' */
minHeight?: string;
/** '%' */
height: string;
/** 'px' */
minWidth?: string;
/** '%' */
width: string;
/** '%' */
restWidth?: number;
}
export interface WidgetSizeLimits {
minHeight: number; // px
minWidth: number; // px
maxWidth?: number; // px
maxHeight?: number; // px
}
export interface BeforeWidgetExpandParams {
position: Position;
sizes: WidgetSizes;
}
export interface ICreateWidgetByDragProps {
sizes?: WidgetSizes;
position?: WidgetPosition;
}
export interface Widget {
name: string;
sizes: WidgetSizes;
sizeLimits?: WidgetSizeLimits;
zIndex: number;
id: number;
workspaceId: Workspace['id'];
position: WidgetPosition;
type: WidgetContentType;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: типипзировать
widgetContentProps: Record<string, any>;
isExpand: boolean;
beforeExpandParams: BeforeWidgetExpandParams | null;
/** @deprecated Для множественной связки используется более универсальное поле masters */
master?: number | null;
/** Для определения мастеров при связке (binding) виджетов. */
masters?: number[] | null;
externalProperties?: { [key: string]: string | number }[] | null;
withoutSend?: boolean;
moved?: boolean;
inFocus?: boolean;
/**
*
*/
isStacked?: boolean;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: типипзировать
export interface WidgetContentBasicProps<T = any> {
widgetId: Widget['id'];
widgetContentProps?: T;
onWidgetContentClick?: () => void;
refWrapper?: React.MutableRefObject<HTMLDivElement | null>;
/** undefined - не отображаем, null - дизейблим */
handlerSaveAsExcel?: (() => void) | null;
isMobileView?: boolean;
style?: Record<string, string>;
className?: string;
// максимальное число toast-ов в рамках виджета
maxToastsCount?: number;
}
export interface PackedPropsProperties {
key: string;
value: string | number | boolean | undefined | null;
}
export interface WidgetPackedProps {
widgetId: number;
properties: PackedPropsProperties[];
isRemoval?: boolean;
}
export interface OrderbookWidgetConfig {
view: ViewType;
showYield: boolean;
showSpread: boolean;
choosenInstrument: string;
}
import { AxiosResponse } from 'axios';
import { call, put } from 'typed-redux-saga';
import { storageController } from '@api/controllers/customStorage';
import { isShowMarketProfileModal, openMarketProfileModalAndUpdateStorage } from '@components/MarketProfileModal/utils';
import { TOURS_DATA } from '@modules/tours';
import { addStorageData } from '@store/slices/cashedData';
import { StorageDataType } from 'types/StorageData';
import type { SagaGenerator } from 'typed-redux-saga';
export function* initStorageData(): SagaGenerator<void> {
try {
const response: AxiosResponse<StorageDataType> = yield* call(() => storageController.getAllCustomFields());
const res = response.data;
yield* put(addStorageData(res));
if (isShowMarketProfileModal(res)) {
yield* call(openMarketProfileModalAndUpdateStorage);
} else {
const introTour = TOURS_DATA?.find((el) => el.id === 'intro-tour');
if (introTour?.onStart) {
yield* call([introTour, introTour.onStart], res);
}
}
} catch {
// Ошибка при получении данных в storage — не блокирует
}
}
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from '@store/store';
import { createFieldSelector } from '@store/utils/createFieldSelector';
import { Permissions } from 'types/User';
export const userPersonalDataSelector = createFieldSelector((state) => state.userSlice.personalData);
export const userInfoSelector = createFieldSelector((state) => state.userSlice.info);
export const userPrivacySettingsSelector = createFieldSelector((state) => state.userSlice.privacySettings);
export const permissionsSelector = (state: RootState) => state.userSlice.permissions;
export const isSPFITraderSelector = (state: RootState) => permissionsSelector(state).includes(Permissions.SPFI_USER);
export const isSPFIBrokerSelector = (state: RootState) => permissionsSelector(state).includes(Permissions.SPFI_BROCKER);
// TODO: Сейчас есть 2 правила. Нужно будет разобраться
export const isTraderSelector = (state: RootState) => permissionsSelector(state).includes(Permissions.TRADER);
export const checkMxtTrading = (state: RootState) => permissionsSelector(state).includes(Permissions.MXT_TRADING);
export const isAnalyticSelector = (state: RootState) => state.userSlice.info?.bookbuilderRoles.includes('Аналитик');
export const isTraderRoleSelector = (state: RootState) => state.userSlice.info?.bookbuilderRoles.includes('Трейдер');
export const isContributorSelector = (state: RootState) => permissionsSelector(state).includes(Permissions.CONTRIBUTOR);
export const isAgroTraderSelector = (state: RootState) => permissionsSelector(state).includes(Permissions.AGRO_TRADER);
const isAgroViewerSelector = (state: RootState) => permissionsSelector(state).includes(Permissions.AGRO_VIEWER);
export const isNtbUserSelector = (state: RootState) => {
const permissions = permissionsSelector(state);
return permissions.includes(Permissions.AGRO_VIEWER) || permissions.includes(Permissions.AGRO_TRADER);
};
export const profileEditModeSelector = (state: RootState) => state.userSlice.editMode;
export const userAvatarSelector = (state: RootState) => state.userSlice.avatarUrl;
export const userEmailSelector = (state: RootState): string => state.userSlice.info?.email || '';
export const userFullNameSelector = (state: RootState): string => state.userSlice.info?.fullName || '';
export const userTRIdSelector = (state: RootState): string | null => state.userSlice.info?.trId || null;
/** deprecated, По возможности не использовать этот селектор, а обращаться к ролевой модели */
const isTradePermission = (state: RootState) => state.userSlice.info?.isTradePermission;
export const formOptionsSelector = createFieldSelector((state) => state.userSlice.formOptions);
export const userTradingAccessesSelector = (state: RootState) => state.userSlice.userTradingAccesses;
export const tradeTimePermissionsSelector = (state: RootState) =>
state.userSlice.userTradingAccesses?.tradeTimePermissions;
/** Map, где ключ - key инструмента, а значение - информация о торгах (статус, время) */
export const tradeTimePermissionsMapSelector = createSelector(
[tradeTimePermissionsSelector],
(permissions) => new Map(permissions?.map((p) => [p.key, p])),
);
import { createSelector } from '@reduxjs/toolkit';
import { shallowEqual } from 'react-redux';
import { RootState } from '@store/store';
import { createFieldSelector } from '@store/utils/createFieldSelector';
import { WidgetContentType } from 'types/Widgets';
export const widgetsSelector = (state: RootState) => state.widgets.widgets;
export const widgetSizesSelector = (widgetId: number) => (state: RootState) =>
state.widgets.widgets.find(({ id }) => id === widgetId)?.sizes;
export const createWidgetsSelectorByWorkspaceId = (workspaceId: number) =>
createSelector([(state: RootState) => state.widgets.widgets], (widgets) =>
widgets.filter((w) => w.workspaceId === workspaceId),
);
export const widgetByIdSelector = (id: number) => (state: RootState) =>
state.widgets.widgets.find((item) => item.id === id);
export const masterWidgetSelector = (widgetId: number) => (state: RootState) => {
const masterId = widgetByIdSelector(widgetId)(state)?.master;
return masterId ? widgetByIdSelector(masterId)(state) : undefined;
};
export const widgetsByWorkspaceIdAndTypeSelector = (workspaceId: number, type: WidgetContentType) =>
createSelector([widgetsSelector], (widgets) =>
widgets.filter((item) => item.workspaceId === workspaceId && item.type === type),
);
export const getSlaveWidgetsByIdSelector = (widgetId: number) =>
createSelector(
[widgetsSelector],
(widgets) =>
widgets.filter((elem) => elem?.master === widgetId || elem?.masters?.includes(widgetId)).map((elem) => elem.id),
{
memoizeOptions: {
maxSize: 10,
resultEqualityCheck: shallowEqual,
},
},
);
import { createSelector } from '@reduxjs/toolkit';
import isEqual from 'lodash/isEqual';
import { RootState } from '@store/store';
import { createFieldSelector } from '@store/utils/createFieldSelector';
import { getTheNewestWidget } from '@utils/getTheNewestWidget';
import { WidgetContentType } from 'types/Widgets';
import { Workspace } from 'types/Workspace';
import { widgetsSelector } from './widgets';
export const workspacesSelector = (state: RootState) => state.workspaces.workspaces;
export const currentWorkspaceIdSelector = (state: RootState) =>
state.workspaces.workspaces.find(({ isCurrent }) => isCurrent)?.id;
export const workspaceWidgetsSelector = createSelector(
[widgetsSelector, currentWorkspaceIdSelector],
(wgts, currentWorkspaceId) => wgts.filter(({ workspaceId }) => workspaceId === currentWorkspaceId),
{
memoizeOptions: {
maxSize: 10,
resultEqualityCheck: isEqual,
},
},
);
export const savedWorkspacesSelector = createSelector(workspacesSelector, (workspaces) =>
workspaces.filter((item) => item.saved),
);
export const closedWorkspacesSelector = createSelector(workspacesSelector, (workspaces) =>
workspaces.filter((item) => !item.saved),
);
export function workspaceByIdSelector(id: number): (state: RootState) => Workspace | undefined;
export function workspaceByIdSelector<K extends keyof Workspace>(
id: number,
field: K,
): (state: RootState) => Workspace[K] | undefined;
export function workspaceByIdSelector(id: number, field?: keyof Workspace) {
return (state: RootState) => {
const workspace = state.workspaces.workspaces.find((w) => w.id === id);
if (field !== undefined) {
return workspace?.[field];
}
return workspace;
};
}
export const selectIsCurrentWorkspaceNew = (state: RootState) =>
state.workspaces.workspaces.find(({ isCurrent }) => isCurrent)?.isNew ?? false;
export const currentWorkspaceSelector = (state: RootState) =>
state.workspaces.workspaces.find(({ isCurrent }) => isCurrent);
export const currentWorkspaceLayoutSelector = createSelector(
currentWorkspaceSelector,
(workspace) => workspace?.layout,
);
export const workspaceByNameSelector = (name: string) =>
createFieldSelector((state: RootState) => workspacesSelector(state).find((w) => w.name === name));
export const workspacesNamesSelector = createSelector(workspacesSelector, (workspaces) =>
workspaces.map((w) => w.name),
);
export const newestWidgetIdSelector = createSelector(
[widgetsSelector, (_: RootState, type: WidgetContentType) => type],
(widgets, type) => getTheNewestWidget(widgets, type)?.id,
);
/* eslint-disable no-continue */
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Widget, WidgetPackedProps } from 'types/Widgets';
export function packWidgetProps(value: Record<string, any>, propertyPrefix?: string): WidgetPackedProps {
const result = Object.keys(value)
.map((key) => {
const val = value[key];
const isSaveAsString = typeof val === 'string' && !Number.isNaN(Number(val)) && val !== '';
// если есть префикс, значит функция вызвана в рекурсии и нужно
// сосласться на предыдущее проперти, таким образом "зашифровав"
// объект
const propertyPrefixKey = isSaveAsString ? `${propertyPrefix}_${key}_asString` : `${propertyPrefix}_${key}`;
const newKey = propertyPrefix ? propertyPrefixKey : key;
// если значение - объект(объектом в том числе является и массив)
// то уходим в рекурсию
const newValue =
typeof val === 'object' && val !== undefined && val !== null ? packWidgetProps(val, newKey) : val;
// если значение получено из рекурсии надо сделать массив
// объектом
if (typeof newValue === 'object' && newValue !== undefined && newValue !== null) {
return Object.keys(newValue)
.map((k) => ({ ...newValue[k] }))
.reduce(
(acc, curr) => ({
...acc,
...curr,
}),
{},
);
}
// бэк не умеет работать с null значением нпрямую, поэтому с фронта
// передаем его строкой
return {
[newKey]: newValue !== null && newValue !== undefined ? newValue : 'null',
};
})
.flat(Infinity);
// если нет префикса, значит возвращаемся из первого вызова, и нужно свернуть
// массив для передачи на бэк, в противном случае возвращаем массив
return !propertyPrefix
? result.reduce(
(acc, curr) => {
Object.entries(curr).forEach(([key, val]) => {
if (key === 'id') {
acc.widgetId = val;
} else {
acc.properties.push({ key, value: val });
}
});
return acc;
},
{ widgetId: undefined, properties: [] },
)
: result;
}
function convertTypesFromString(value: WidgetPackedProps['properties']): WidgetPackedProps['properties'] {
return Object.entries(value).reduce(
(acc, curr) => {
const [, v] = curr;
const { key } = v;
const val = v.value;
if (typeof val !== 'string') {
return { ...acc, [key as string]: val };
}
if (val === 'false') {
return { ...acc, [key as string]: false };
}
if (val === 'true') {
return { ...acc, [key as string]: true };
}
if (val === 'undefined') {
return { ...acc, [key as string]: undefined };
}
if (val === 'null') {
return { ...acc, [key as string]: null };
}
// не используем тут parseFloat или parseInt, так как он преобразует
// строковые значения в числа, если строки содержат в себе любое число
const numberVal = Number(val);
if (!Number.isNaN(numberVal) && val !== '' && !key.includes('asString')) {
return { ...acc, [key as string]: numberVal };
}
return { ...acc, [key as string]: val };
},
[] as WidgetPackedProps['properties'],
);
}
function unpackObject(value: Record<string, any> | any[]): any {
const result = Array.isArray(value) ? [] : ({} as any);
const valuesEntries = Object.entries(value).map(([key, val]) => [key.split('_'), val]);
valuesEntries.forEach(([keys, val]) => {
const [firstKey] = keys as string[];
// если ключей больше 1, значит перед нами объект и мы должны
// найти все ключи которые содержат в себе firstKey, объединить
// их в объект и уйти в рекурсию
const isArray = !Number.isNaN(Number(keys[1]));
if (keys.length > 1 && result[firstKey] === undefined) {
const middler = valuesEntries
.filter(([k]) => k[0] === firstKey)
.reduce(
(acc, curr) => {
const [k, v] = curr;
const newKey = k.join('_').replace(`${firstKey}_`, '');
acc[newKey] = v;
return acc;
},
isArray ? [] : ({} as any),
);
result[firstKey] = unpackObject(middler);
} else if (result[firstKey] === undefined) {
result[firstKey] = val;
}
});
return result;
}
export function unpackWidgetProps(v: WidgetPackedProps): Widget {
const { widgetId, properties } = v;
// бэк отдает нам все значение в виде строк, поэтом нам нужно
// преобразовать их в исходные типы данных
const convertedProperties = Object.entries(convertTypesFromString(properties as any)).reduce((acc, curr) => {
const [key, value] = curr;
acc[key.replace('_asString', '')] = value;
return acc;
}, {} as any);
/*
Так как бэк не понимает, что некоторые проперти, которые в одном случае могли бы быть
объектам, могут быть null(например beforeExpandParams), то нам необходимо отфильтровать
пустые значения если есть не пустые ключи со схожим началом в ключах это и проделывается
в блоке кода на строках 130-164
*/
const propertiesKeys = Object.keys(convertedProperties) as (keyof typeof convertedProperties)[];
const cleanProperties = {} as typeof convertedProperties;
for (let i = 0; i < propertiesKeys.length; i += 1) {
const key = propertiesKeys[i] as string;
const val = convertedProperties[key];
if (val === null) {
const spitted = (key as string).split('_');
const sameKeys = propertiesKeys.filter((k) => (k as string).includes(key));
// если нет схожик ключей - значит параметр уникален
// и не имеет более глубоких дублеров
if (sameKeys.length === 0) {
cleanProperties[key] = val;
continue;
}
let flag = true;
for (let j = 0; j < sameKeys.length; j += 1) {
// если все части исходного ключа содержатся в дублере,
// то исходный ключ не должен идти в конечный результат
if (spitted.every((item) => sameKeys.includes(item))) {
flag = false;
break;
}
}
if (flag) {
cleanProperties[key] = val;
}
} else {
cleanProperties[key] = val;
}
}
const unpackedProperties = unpackObject(cleanProperties);
return {
id: widgetId,
...unpackedProperties,
};
}