Загрузка данных
import { Dispatch, MutableRefObject, SetStateAction, useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { communicator } from '@core/comm';
import { useAppSelect } from '@hooks/useAppSelector';
import { Contract } from '@modules/contracts';
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 { Widget } from 'types/Widgets';
import type { WidgetProperties } from '../properties/types';
import type { ChartContainerProps } from '../types';
interface UseChartComponentFacadeReturn {
dropDownOpen: boolean;
setDropdownOpen: Dispatch<SetStateAction<boolean>>;
currentInstrument: string;
isWidgetHeaderContextMenuOpen: boolean;
setIsWidgetHeaderContextMenuOpen: Dispatch<SetStateAction<boolean>>;
onDropInstruments: (val: string, withUpdate?: boolean) => void;
addInstrumentFromModal: (instruments: Contract[]) => void;
isOver: boolean;
}
export default function useChartComponentFacade(props: ChartContainerProps): UseChartComponentFacadeReturn {
const { widgetId } = props;
const [isOver, setIsOver] = useState<boolean>(false);
const chartContainerRef = useRef<HTMLDivElement | null>(null) as MutableRefObject<HTMLDivElement>;
const dispatch = useDispatch();
const indicativeData = props.widgetContentProps?.indicativeData;
const chartPrevStateRef = useRef<object>({});
// используется для определения источника отображаемого в виджете инструмента (из привязки или нет)
const isInstrumentChangedFromBinding = useRef<boolean>(false);
const prevTimeframe = useRef<string>('');
const lastIntervalChangeTime = useRef<number | null>(null);
const timeDiffOfIntervalChangeTime = useRef<number | null>(null);
const [isWidgetHeaderContextMenuOpen, setIsWidgetHeaderContextMenuOpen] = useState(false);
const setChartIsReady = useCallback((isReady: boolean) => {
setChartIsReadyFromUseState(isReady);
isChartIsReadyFromRef.current = isReady;
}, []);
const language = useAppSelect((state) => state.localisation.localisation);
const { chartState } = useAppSelect((state) => state.widgets.widgets.find(({ id }) => id === widgetId))
?.widgetContentProps as WidgetProperties;
const [currentInstrument, setCurrentInstrument] = useState(chartState.savedInstrument ?? DEFAULT_SYMBOL);
const { triggerRelatedWidgetsToUpdate, getMasterInstrumentFromPublicContext } = useWidgetsBind({
widgetId,
});
// Функция работает внутри интервала, поэтому используем не хуковый доступ к стору
const saveContentProps = (
val: Partial<WidgetProperties['chartState']> & { withUpdate?: boolean; cleanIndicativeData?: boolean },
): void => {
// если вызываем функцию с withUpdate, то оправляем в стор и делаем запрос на изменения
const { withUpdate, cleanIndicativeData, ...chartStateVal } = val;
const oldProps = (getState().widgets.widgets.find(({ id }) => id === widgetId) as Widget)?.widgetContentProps;
if (oldProps) {
dispatch(
addContentPropsToWidget({
id: widgetId,
withoutSend: !withUpdate,
widgetContentProps: {
chartState: { ...oldProps.chartState, ...chartStateVal },
// при смене инструмента очищаем индикативные данные, переданные из виджета Индикативные котировки
indicativeData: cleanIndicativeData ? undefined : oldProps.indicativeData,
},
}),
);
}
};
useEffect(() => {
triggerRelatedWidgetsToUpdate(currentInstrument.current);
const savedActiveChart = chartIsReady ? savedTvWidget?.activeChart?.() : false;
if (!chartIsReady || !tvWidgetRef.current || !savedTvWidget || !savedActiveChart) {
return;
}
if (currentInstrument.current !== savedActiveChart.symbol()) {
savedActiveChart.setSymbol(currentInstrument.current);
}
// [currentInstrument.current] в зависимостях
// т.к. здесь не нужен ререндер потому что достаточно прокинуть в TV новое значение
// eslint-disable-next-line react-hooks/exhaustive-deps -- TODO Разобраться с зависимостями
}, [currentInstrument.current, chartIsReady]);
const onInstrumentChange = useCallback(
(newVal: string, withUpdate?: boolean, unbind = true): void => {
const prev = currentInstrument.current;
currentInstrument.current = newVal;
if (unbind) {
dispatch(unbindWidgets({ widgetId }));
}
// при смене инструмента очищаем индикативные данные виджета график
// т.к. логика для графика индикатива построена на наличии в widgetContentProps данных indicativeData
saveContentProps({ savedInstrument: newVal, withUpdate, cleanIndicativeData: true });
setIsWidgetHeaderContextMenuOpen(false);
onInstrumentChangeCallback(prev, newVal);
},
// eslint-disable-next-line react-hooks/exhaustive-deps -- TODO Разобраться с зависимостями
[onInstrumentChangeCallback, chartIsReady],
);
const addInstrumentFromModal = useCallback(
(instruments: Pick<Contract, 'issKey'>[]) => {
const instr = instruments[0];
isInstrumentChangedFromBinding.current = false;
if (instr.issKey !== null) {
onInstrumentChange(instr.issKey);
}
},
[onInstrumentChange],
);
const onInstrumentChangeFromBind = useCallback(
(instrumentId: string) => {
isInstrumentChangedFromBinding.current = true;
onInstrumentChange(instrumentId, true, false);
},
[onInstrumentChange],
);
const onDropInstruments = useCallback((val: string, withUpdate?: boolean): void => {
isInstrumentChangedFromBinding.current = false;
onInstrumentChange(val, withUpdate);
// eslint-disable-next-line react-hooks/exhaustive-deps -- TODO Разобраться с зависимостями
}, []);
useChartPublicContext({
setCurrInstrument: onInstrumentChangeFromBind,
widgetId,
getMasterInstrumentFromPublicContext,
});
const [dropDownOpen, setDropdownOpen] = useState(false);
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]);
/** Переключение инструмента и вида графика при indicativeData */
useEffect(() => {
const savedActiveChart = chartIsReady ? savedTvWidget?.activeChart?.() : false;
if (!chartIsReady || !tvWidgetRef.current || !savedTvWidget || !savedActiveChart) {
return;
}
onInstrumentChangeCallback(
chartState?.savedInstrument ?? (DEFAULT_OPTIONS.symbol as string),
currentInstrument.current,
);
if (indicativeData) {
savedActiveChart.setSymbol(indicativeData.key);
savedActiveChart.setChartType(SeriesStyle.Line);
savedActiveChart.setResolution('60' as ResolutionString);
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- TODO Разобраться с зависимостями
}, [chartIsReady, savedTvWidget, indicativeData]);
return {
chartContainerRef,
savedTvWidget,
dropDownOpen,
setDropdownOpen,
currentInstrument,
handlerSaveAsExcel,
isWidgetHeaderContextMenuOpen,
setIsWidgetHeaderContextMenuOpen,
onDropInstruments,
addInstrumentFromModal,
isOver,
};
}
[{
"resource": "/c:/Users/ShkinderDV/moex-terminal-front/src/widgets/Chart/hooks/useChartComponentFacade.tsx",
"owner": "typescript",
"code": "2304",
"severity": 8,
"message": "Cannot find name 'setChartIsReadyFromUseState'.",
"source": "ts",
"startLineNumber": 53,
"startColumn": 5,
"endLineNumber": 53,
"endColumn": 32,
"modelVersionId": 187,
"origin": "extHost1"
}]