Загрузка данных
import { Contract } from '@modules/contracts/types';
import { ChartIndicativeData } from '../types';
import type { Timeframes } from 'moex-chart';
export type WidgetProperties = {
chartState: {
savedInstrument: Contract['issKey'] | null;
interval: string;
savedData?: string;
};
indicativeData?: ChartIndicativeData;
moexChartState?: {
timeframe: Timeframes;
savedData?: string;
};
};
import type { WidgetContentBasicProps } from 'types/Widgets';
import type { WidgetProperties } from './properties/types';
export type ChartIndicativeData = {
secId: string;
instrumentName: string;
settlement: string;
firmName: string;
key: string;
};
export type ChartContainerProps = WidgetContentBasicProps<WidgetProperties>;
import type { Workspace } from './Workspace';
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',
tradeJournal = 'tradeJournal',
draftBrokerSpfi = 'draftBrokerSpfi',
tradeJournalDetails = 'tradeJournalDetails',
ntbIndexes = 'ntbIndexes',
depositCcpOrderTables = 'depositCcpOrderTables',
depositCcpTradeTables = 'depositCcpTradeTables',
depositCcpReferenceTables = 'depositCcpReferenceTables',
depositCcpRiskTables = 'depositCcpRiskTables',
userAdministration = 'userAdministration',
}
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;
}
export interface PackedPropsProperties {
key: string;
value: string | number | boolean | undefined | null;
}
export interface WidgetPackedProps {
widgetId: number;
properties: PackedPropsProperties[];
isRemoval?: boolean;
}
export interface GlassWidgetConfig {
view: 'four-col-1' | 'four-col-2' | 'four-col-3';
showYield: boolean;
showSpread: boolean;
choosenInstrument: string;
}
import { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { communicator } from '@core/comm';
import { useAppSelect } from '@hooks/useAppSelector';
import { CORPACTIONS_OPEN_EXESTED_WIDGET_EVENT, HIGHLIGHT_WIDGET_EVENT } from '@modules/widgets/shared';
import { addContentPropsToWidget, unbindWidgets } from '@store/slices/widgets';
import { getState } from '@store/store';
import { useWidgetsBind } from '@utils/hooks/useWidgetsBind';
import { DEFAULT_SYMBOL } from '../const';
import { useChartPublicContext } from './useChartPublicContext';
import type { WidgetProperties } from '../properties/types';
import type { ChartContainerProps } from '../types';
import type { Contract } from '@modules/contracts';
import type { Dispatch, SetStateAction } from 'react';
import type { Widget } from 'types/Widgets';
interface UseChartComponentFacadeReturn {
dropDownOpen: boolean;
setDropdownOpen: Dispatch<SetStateAction<boolean>>;
currentInstrument: string;
isWidgetHeaderContextMenuOpen: boolean;
setIsWidgetHeaderContextMenuOpen: Dispatch<SetStateAction<boolean>>;
onDropInstruments: (val: string, withUpdate?: boolean) => void;
addInstrumentFromModal: (instruments: Pick<Contract, 'issKey'>[]) => void;
isOver: boolean;
}
type SaveContentPropsOptions = Partial<WidgetProperties['chartState']> & {
withUpdate?: boolean;
cleanIndicativeData?: boolean;
};
export default function useChartComponentFacade(props: ChartContainerProps): UseChartComponentFacadeReturn {
const { widgetId } = props;
const dispatch = useDispatch();
const [isOver, setIsOver] = useState(false);
const [dropDownOpen, setDropdownOpen] = useState(false);
const [isWidgetHeaderContextMenuOpen, setIsWidgetHeaderContextMenuOpen] = useState(false);
const widgetProperties = useAppSelect(
(state) => state.widgets.widgets.find(({ id }) => id === widgetId)?.widgetContentProps,
) as WidgetProperties | undefined;
const initialInstrument = widgetProperties?.chartState?.savedInstrument ?? DEFAULT_SYMBOL;
// TODO временно реф из-за непредсказуемого изменения если используется useState,
// нужен рефакторинг и вернуть обратно useState
const currentInstrumentRef = useRef(initialInstrument);
const [currentInstrument, setCurrentInstrument] = useState(initialInstrument);
const { triggerRelatedWidgetsToUpdate, getMasterInstrumentFromPublicContext } = useWidgetsBind({
widgetId,
});
const triggerRelatedWidgetsToUpdateRef = useRef(triggerRelatedWidgetsToUpdate);
useEffect(() => {
triggerRelatedWidgetsToUpdateRef.current = triggerRelatedWidgetsToUpdate;
}, [triggerRelatedWidgetsToUpdate]);
const saveContentProps = useCallback(
(val: SaveContentPropsOptions): void => {
const { withUpdate, cleanIndicativeData, ...chartStateVal } = val;
const oldProps = (getState().widgets.widgets.find(({ id }) => id === widgetId) as Widget | undefined)
?.widgetContentProps;
if (!oldProps) {
return;
}
dispatch(
addContentPropsToWidget({
id: widgetId,
withoutSend: !withUpdate,
widgetContentProps: {
chartState: {
...oldProps.chartState,
...chartStateVal,
},
// при смене инструмента очищаем индикативные данные, переданные из виджета Индикативные котировки
indicativeData: cleanIndicativeData ? undefined : oldProps.indicativeData,
},
}),
);
},
[dispatch, widgetId],
);
const onInstrumentChange = useCallback(
(newVal: string, withUpdate?: boolean, unbind = true): void => {
if (!newVal) {
return;
}
setIsWidgetHeaderContextMenuOpen(false);
if (currentInstrumentRef.current === newVal) {
return;
}
currentInstrumentRef.current = newVal;
setCurrentInstrument(newVal);
if (unbind) {
dispatch(unbindWidgets({ widgetId }));
}
// при смене инструмента очищаем индикативные данные виджета график
// т.к. логика для графика индикатива построена на наличии в widgetContentProps данных indicativeData
saveContentProps({
savedInstrument: newVal,
withUpdate,
cleanIndicativeData: true,
});
triggerRelatedWidgetsToUpdateRef.current(newVal);
},
[dispatch, saveContentProps, widgetId],
);
const addInstrumentFromModal = useCallback(
(instruments: Pick<Contract, 'issKey'>[]) => {
const issKey = instruments[0]?.issKey;
if (!issKey) {
return;
}
onInstrumentChange(issKey);
},
[onInstrumentChange],
);
const onInstrumentChangeFromBind = useCallback(
(instrumentId: string) => {
onInstrumentChange(instrumentId, true, false);
},
[onInstrumentChange],
);
const onDropInstruments = useCallback(
(val: string, withUpdate?: boolean): void => {
onInstrumentChange(val, withUpdate);
},
[onInstrumentChange],
);
useChartPublicContext({
setCurrInstrument: onInstrumentChangeFromBind,
widgetId,
getMasterInstrumentFromPublicContext,
});
useEffect(() => {
triggerRelatedWidgetsToUpdateRef.current(currentInstrumentRef.current);
}, []);
useEffect(() => {
const unsubscribe = communicator.listen(
{
messageType: CORPACTIONS_OPEN_EXESTED_WIDGET_EVENT,
},
(message) => {
const issKey = (message as Record<number, string>)[widgetId];
if (issKey) {
addInstrumentFromModal([{ issKey }]);
}
},
);
const unsubscribeHighlighter = communicator.listen(
{
messageType: HIGHLIGHT_WIDGET_EVENT,
},
(message) => {
const typedMessage = message as Record<number, boolean>;
if (Object.keys(typedMessage).includes(String(widgetId))) {
setIsOver(typedMessage[widgetId]);
}
},
);
return () => {
unsubscribe();
unsubscribeHighlighter();
};
}, [addInstrumentFromModal, widgetId]);
return {
dropDownOpen,
setDropdownOpen,
currentInstrument,
isWidgetHeaderContextMenuOpen,
setIsWidgetHeaderContextMenuOpen,
onDropInstruments,
addInstrumentFromModal,
isOver,
};
}
import React, { FC, lazy, useContext, useState } from 'react';
import { DNDWrapper } from '@components/DNDWrapper';
import { InstrumentSearch } from '@components/InstrumentSearch';
import WidgetContentWrapper from '@components/WidgetContentWrapper';
import WidgetHeader from '@components/WidgetHeader';
import { useDropInstrument } from '@hooks/dnd';
import { WidgetEnvContext } from '@terminal/desktop/workspaces/default/components/Widget/context';
import { useWidgetHeaderName } from '@utils/useWidgetName';
import useChartComponentFacade from './hooks/useChartComponentFacade';
import type { ChartContainerProps } from './types';
const MoexChart = lazy(() => import('./components/MoexChart/MoexChart'));
export const Chart: FC<ChartContainerProps> = function (props): JSX.Element {
const indicativeData = props.widgetContentProps?.indicativeData;
const {
dropDownOpen,
setDropdownOpen,
currentInstrument,
setIsWidgetHeaderContextMenuOpen,
isWidgetHeaderContextMenuOpen,
onDropInstruments,
addInstrumentFromModal,
isOver,
} = useChartComponentFacade(props);
/* на дроп обновляем название бумаги в сторе
и делаем апдейт на бэк, чтобы сохранить изменения
при обновлении страницы */
const { dropRef } = useDropInstrument((dragData) => {
onDropInstruments(dragData.properties.issKey, true);
});
const [isOpenEmptyAction, setIsOpenEmptyAction] = useState<boolean>(false);
const openInstrumentModal = () => {
setIsOpenEmptyAction(true);
setIsWidgetHeaderContextMenuOpen(false);
};
// TODO: Зависит от сеток
const { isDraggedOver } = useContext(WidgetEnvContext);
const contractsInstrumentName = useWidgetHeaderName(currentInstrument);
const instrumentName = indicativeData
? `${indicativeData.instrumentName} ${indicativeData.settlement} - ${indicativeData.firmName}`
: contractsInstrumentName;
return (
<DNDWrapper
ref={dropRef}
isOver={isOver}
canDrop
>
<WidgetHeader
{...props}
dropdownOpen={dropDownOpen}
setDropdownOpen={setDropdownOpen}
itemsSearchIcon={[true]}
handlerSaveAsExcel={null}
addToWidgetNamePrefix={instrumentName}
setIsOpenContextMenuFromWidget={setIsWidgetHeaderContextMenuOpen}
isOpenContextMenuFromWidget={isWidgetHeaderContextMenuOpen}
openInstrumentsModal={openInstrumentModal}
/>
<WidgetContentWrapper {...props}>
<div
style={{
display: 'flex',
flexDirection: 'column',
height: '100%',
pointerEvents: isDraggedOver ? 'none' : 'inherit',
}}
>
<MoexChart
symbol={currentInstrument}
symbolName={instrumentName || currentInstrument}
indicativeData={indicativeData}
widgetId={props.widgetId}
addInstrumentFromModal={addInstrumentFromModal}
/>
{isOpenEmptyAction && (
<InstrumentSearch
setOpen={setIsOpenEmptyAction}
isOpen={isOpenEmptyAction}
variant="single"
widgetId={props.widgetId}
// withNRD={false}
addInstruments={addInstrumentFromModal}
/>
)}
</div>
</WidgetContentWrapper>
</DNDWrapper>
);
};