Загрузка данных
diff --git a/src/components/InstrumentSearch/components/Wrapper/index.tsx b/src/components/InstrumentSearch/components/Wrapper/index.tsx
index b4ffc3827b1b020b1a6097d23c45576c77ed3043..1ee12b805534f162d71da020d8fd7144f7165730 100644
--- a/src/components/InstrumentSearch/components/Wrapper/index.tsx
+++ b/src/components/InstrumentSearch/components/Wrapper/index.tsx
@@ -10,13 +10,14 @@ type IWrapper = {
children: ReactNode;
setOpen: (b: boolean) => void;
isOpen: boolean;
+ headerName?: string;
};
/** Компонент обёртка для модального окна */
-export const Wrapper: React.FC<IWrapper> = ({ children, setOpen, isOpen }) => (
+export const Wrapper: React.FC<IWrapper> = ({ children, setOpen, isOpen, headerName }) => (
<Modal isOpen={isOpen}>
<div className={styles.modalWrapper}>
<header>
- <TextInput>Поиск инструментов</TextInput>
+ <TextInput>{ headerName ?? 'Поиск инструментов' }</TextInput>
<div
onClick={() => {
setOpen(false);
diff --git a/src/components/InstrumentSearch/search.tsx b/src/components/InstrumentSearch/search.tsx
index c142a84b776ca2a529c68a19529b69e8450f598a..aed5205283aa8fe41eb1a85ce31a004236d6e85a 100644
--- a/src/components/InstrumentSearch/search.tsx
+++ b/src/components/InstrumentSearch/search.tsx
@@ -14,6 +14,7 @@ export const InstrumentSearch: FC<InstrumentSearchType> = ({
addInstruments,
customActionsFooterHandlers,
isNewScaleDisabled,
+ headerName,
}) => {
const { filters, actions, table } = useData(variant, setOpen, addInstruments);
const tableProps = { ...table, widgetId };
@@ -22,6 +23,7 @@ export const InstrumentSearch: FC<InstrumentSearchType> = ({
<Wrapper
setOpen={setOpen}
isOpen={isOpen}
+ headerName={headerName}
>
<SearchInput {...filters} />
<Filters {...filters} />
diff --git a/src/components/InstrumentSearch/types/hooks.ts b/src/components/InstrumentSearch/types/hooks.ts
index aa7d44a7ceb4efd3eb4f2c64608f7db2634ae8ab..aa6057d6535678ec67392e426c2fddfc04d5287e 100644
--- a/src/components/InstrumentSearch/types/hooks.ts
+++ b/src/components/InstrumentSearch/types/hooks.ts
@@ -80,6 +80,7 @@ export type InstrumentSearchType = {
handleNewPanel: (instrument: Contract) => void;
};
isNewScaleDisabled?: boolean;
+ headerName?: string;
};
export type InstrumentType = Record<AllFiltersVariants, string | number | null | boolean>;
diff --git a/src/widgets/Chart/__tests__/dataSourceProvide.test.ts b/src/widgets/Chart/__tests__/dataSourceProvide.test.ts
index 2bbe173ff5427a9f87f8aad867603f29a5641f83..878e00daa82fa129c9b0ebd41f7cd646ce86de46 100644
--- a/src/widgets/Chart/__tests__/dataSourceProvide.test.ts
+++ b/src/widgets/Chart/__tests__/dataSourceProvide.test.ts
@@ -85,6 +85,7 @@ const runRealtimeTick = async (): Promise<void> => {
await flushPromises();
};
+
describe('DataSourceProvider', () => {
beforeEach(() => {
jest.clearAllMocks();
@@ -204,26 +205,6 @@ describe('DataSourceProvider', () => {
);
});
- it('should not request history data for default symbol', async () => {
- const provider = new DataSourceProvider();
- const dataSource = provider.getDataSource();
-
- const result = await dataSource(Timeframes['1m'], DEFAULT_SYMBOL);
-
- expect(result).toBeNull();
- expect(mockRequestBars).not.toHaveBeenCalled();
- });
-
- it('should not request history data for default symbol with surrounding spaces', async () => {
- const provider = new DataSourceProvider();
- const dataSource = provider.getDataSource();
-
- const result = await dataSource(Timeframes['1m'], ` ${DEFAULT_SYMBOL} `);
-
- expect(result).toBeNull();
- expect(mockRequestBars).not.toHaveBeenCalled();
- });
-
it('should not request history data for empty symbol', async () => {
const provider = new DataSourceProvider();
const dataSource = provider.getDataSource();
@@ -589,6 +570,37 @@ describe('DataSourceProvider', () => {
expect(mockUpdate).toHaveBeenCalledWith('MOEX:LKOH', realtimeCandle);
});
+ it('loaded data for default symbol should be valid', async () => {
+ const firstStart = Math.floor(Date.parse('2026-07-30T04:00:00Z') / 1000);
+ const secondStart = Math.floor(Date.parse('2026-07-31T04:00:00Z') / 1000);
+
+ const historyData = [
+ ...Array.from({ length: 15 }, (_, hour) => createHourBar(firstStart, hour)),
+ ...Array.from({ length: 14 }, (_, hour) => createHourBar(secondStart, hour)),
+ ];
+
+ mockRequestBars.mockResolvedValue(historyData);
+
+ const provider = new DataSourceProvider();
+ const dataSource = provider.getDataSource();
+
+ const result = await dataSource(Timeframes['4h'], DEFAULT_SYMBOL.symbolId);
+
+ expect(result?.map(({ time }) => new Date(time * 1000).toISOString())).toEqual([
+ "2026-07-30T04:00:00.000Z",
+ "2026-07-30T08:00:00.000Z",
+ "2026-07-30T12:00:00.000Z",
+ "2026-07-30T16:00:00.000Z",
+ "2026-07-31T04:00:00.000Z",
+ "2026-07-31T08:00:00.000Z",
+ "2026-07-31T12:00:00.000Z",
+ "2026-07-31T16:00:00.000Z",
+ ]);
+
+ expect(result?.length).toBeGreaterThan(7);
+ expect(mockRequestBars).toHaveBeenCalled();
+ });
+
it('should reset realtime convolution for a new trading session', async () => {
const sessionStart = Math.floor(Date.parse('2026-07-30T03:00:00Z') / 1000);
const historyData = Array.from({ length: 18 }, (_, hour) => createHourBar(sessionStart, hour));
@@ -654,25 +666,6 @@ describe('DataSourceProvider', () => {
expect(mockUpdate).toHaveBeenCalledWith('moex:sber', mockBar);
});
- it('should not request realtime data for default symbol', async () => {
- const provider = new DataSourceProvider();
- const mockUpdate = jest.fn();
-
- const unsubscribe = provider.startRealtime({
- getSymbols: () => [DEFAULT_SYMBOL],
- getTimeframe: () => Timeframes['1m'],
- update: mockUpdate,
- periodMs: 1000,
- });
-
- await runRealtimeTick();
-
- unsubscribe();
-
- expect(mockRequestRealtimeBars).not.toHaveBeenCalled();
- expect(mockUpdate).not.toHaveBeenCalled();
- });
-
it('should not request realtime data for empty symbol', async () => {
const provider = new DataSourceProvider();
diff --git a/src/widgets/Chart/__tests__/useChartPublicContext.test.tsx b/src/widgets/Chart/__tests__/useChartPublicContext.test.tsx
index 0b0f2057249d256e59239aeba58b38632c27e8f9..2b9711f0c62a608eb2112bcac73e7e776d8a4b2e 100644
--- a/src/widgets/Chart/__tests__/useChartPublicContext.test.tsx
+++ b/src/widgets/Chart/__tests__/useChartPublicContext.test.tsx
@@ -39,7 +39,7 @@ describe('useChartPublicContext', () => {
const mockContracts = [
{
- issKey: DEFAULT_SYMBOL,
+ issKey: DEFAULT_SYMBOL.symbolId,
displayName: 'Инструмент по умолчанию',
symbol: 'DEFAULT',
// ... other contract properties
@@ -219,11 +219,7 @@ describe('useChartPublicContext', () => {
// Assert
expect(mockSetCurrInstrument).toHaveBeenCalledTimes(1);
- expect(mockSetCurrInstrument).toHaveBeenCalledWith({
- symbolId: DEFAULT_SYMBOL,
- symbol: 'DEFAULT',
- symbolName: 'Инструмент по умолчанию',
- });
+ expect(mockSetCurrInstrument).toHaveBeenCalledWith(DEFAULT_SYMBOL);
});
it('should resolve instrument when widget is not found', () => {
diff --git a/src/widgets/Chart/components/MoexChart/components/CompareModal.tsx b/src/widgets/Chart/components/MoexChart/components/CompareModal.tsx
index 6f48a72671648ca543bca0daaaabee539c5cdc91..98ffd54a8ffe7c3a11bb8b4f926ff61eea2c8be6 100644
--- a/src/widgets/Chart/components/MoexChart/components/CompareModal.tsx
+++ b/src/widgets/Chart/components/MoexChart/components/CompareModal.tsx
@@ -82,6 +82,7 @@ export const CompareModal = ({ compareManager, widgetId, isOpen, setOpen }: Comp
handleNewScale,
handleNewPanel,
}}
+ headerName='Сравнить инструменты'
/>
);
};
diff --git a/src/widgets/Chart/components/MoexChart/dataSourceProvide.ts b/src/widgets/Chart/components/MoexChart/dataSourceProvide.ts
index b27ddb1faa10bc5584fc85c8e08415fc06e1d2f8..3ca863b40b5a5130537a4dbecac3b35c90efe8b0 100644
--- a/src/widgets/Chart/components/MoexChart/dataSourceProvide.ts
+++ b/src/widgets/Chart/components/MoexChart/dataSourceProvide.ts
@@ -3,7 +3,6 @@ import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import { moexChartTimeConverter, moexChartToIssTimeframe } from '@utils/chartToReqTimeConverter';
-import { DEFAULT_SYMBOL } from '@widgets/Chart/const';
import { requestBars, requestRealtimeBars } from '../../requestBars';
import { ChartIndicativeData } from '../../types';
@@ -21,7 +20,7 @@ interface RealtimeState {
function getRequestSymbol(symbolRaw?: string): string | undefined {
const symbol = String(symbolRaw ?? '').trim();
- if (!symbol || symbol === DEFAULT_SYMBOL) {
+ if (!symbol) {
return undefined;
}
diff --git a/src/widgets/Chart/const.ts b/src/widgets/Chart/const.ts
index e7bfbfac9c95acf7c2e8e3713f79d94c08f72dfa..eeffc33b560c98dc0e7b320cccae8c0cb8b2ce98 100644
--- a/src/widgets/Chart/const.ts
+++ b/src/widgets/Chart/const.ts
@@ -1 +1,5 @@
-export const DEFAULT_SYMBOL = '';
+export const DEFAULT_SYMBOL = {
+ symbolId: 'INDICES:SNDX:IMOEX',
+ symbol: "IMOEX",
+ symbolName: "Индекс МосБиржи"
+};
diff --git a/src/widgets/Chart/hooks/useChartComponentFacade.tsx b/src/widgets/Chart/hooks/useChartComponentFacade.tsx
index 9fd119f338c3712b8ae09525d4448beb7a60bfd7..4213d5932b3cf8d3316e48d68bad0e62e88a3a63 100644
--- a/src/widgets/Chart/hooks/useChartComponentFacade.tsx
+++ b/src/widgets/Chart/hooks/useChartComponentFacade.tsx
@@ -17,7 +17,7 @@ import type { WidgetProperties } from '../properties/types';
import type { ChartContainerProps, SelectedInstrument } from '../types';
import type { SymbolInfoInput } from '@mx-charts/charts';
-import type { Dispatch, SetStateAction } from 'react';
+import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
import type { Widget } from 'types/Widgets';
interface UseChartComponentFacadeReturn {
@@ -36,67 +36,23 @@ type SaveContentPropsOptions = Partial<WidgetProperties['chartState']> & {
cleanIndicativeData?: boolean;
};
-export default function useChartComponentFacade(props: ChartContainerProps): UseChartComponentFacadeReturn {
- const { widgetId } = props;
-
+function useInstrumentChange({
+ saveContentProps,
+ setIsWidgetHeaderContextMenuOpen,
+ currentSymbolInfoRef,
+ setCurrentSymbolInfo,
+ triggerRelatedWidgetsToUpdateRef,
+ widgetId
+}: {
+ saveContentProps: (value: SaveContentPropsOptions) => void,
+ setIsWidgetHeaderContextMenuOpen: (value: boolean) => void,
+ currentSymbolInfoRef: MutableRefObject<SymbolInfoInput>,
+ setCurrentSymbolInfo: (value: SymbolInfoInput) => void,
+ triggerRelatedWidgetsToUpdateRef: MutableRefObject<(key: string) => void>,
+ widgetId: number
+}) {
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;
-
- // TODO временно реф из-за непредсказуемого изменения если используется useState,
- // нужен рефакторинг и вернуть обратно useState
- const currentSymbolInfoRef = useRef<SymbolInfoInput>({
- symbolId: widgetProperties?.chartState?.savedInstrument ?? DEFAULT_SYMBOL,
- symbol: widgetProperties?.chartState?.savedSymbol,
- symbolName: widgetProperties?.chartState?.savedSymbolName,
- });
-
- const [currentSymbolInfo, setCurrentSymbolInfo] = useState<SymbolInfoInput>(currentSymbolInfoRef.current);
-
- const { triggerRelatedWidgetsToUpdate, getMasterInstrumentFromPublicContext } = useWidgetsBind({
- widgetId,
- });
-
- const triggerRelatedWidgetsToUpdateRef = useRef(triggerRelatedWidgetsToUpdate);
-
- useEffect(() => {
- triggerRelatedWidgetsToUpdateRef.current = triggerRelatedWidgetsToUpdate;
- }, [triggerRelatedWidgetsToUpdate]);
-
- const saveContentProps = useCallback(
- (value: SaveContentPropsOptions): void => {
- const { withUpdate, cleanIndicativeData, ...chartStateValue } = value;
-
- const oldProps = (getStore().getState().widgets.widgets.find(({ id }) => id === widgetId) as Widget | undefined)
- ?.widgetContentProps;
-
- if (!oldProps) {
- return;
- }
-
- dispatch(
- addContentPropsToWidget({
- id: widgetId,
- withoutSend: !withUpdate,
- widgetContentProps: {
- chartState: {
- ...oldProps.chartState,
- ...chartStateValue,
- },
- // при смене инструмента очищаем индикативные данные, переданные из виджета Индикативные котировки
- indicativeData: cleanIndicativeData ? undefined : oldProps.indicativeData,
- },
- }),
- );
- },
- [dispatch, widgetId],
- );
+ const currentSymbolInfoRefCrunch = currentSymbolInfoRef
const onInstrumentChange = useCallback(
(symbolInfo: SymbolInfoInput, withUpdate?: boolean, unbind = true): void => {
@@ -108,7 +64,7 @@ export default function useChartComponentFacade(props: ChartContainerProps): Use
setIsWidgetHeaderContextMenuOpen(false);
- const currentValue = currentSymbolInfoRef.current;
+ const currentValue = currentSymbolInfoRefCrunch.current;
const symbolIdChanged = currentValue.symbolId !== symbolId;
const symbol = symbolInfo.symbol?.trim() || (symbolIdChanged ? undefined : currentValue.symbol);
@@ -127,7 +83,7 @@ export default function useChartComponentFacade(props: ChartContainerProps): Use
return;
}
- currentSymbolInfoRef.current = nextSymbolInfo;
+ currentSymbolInfoRefCrunch.current = nextSymbolInfo;
setCurrentSymbolInfo(nextSymbolInfo);
if (symbolIdChanged && unbind) {
@@ -144,14 +100,14 @@ export default function useChartComponentFacade(props: ChartContainerProps): Use
cleanIndicativeData: symbolIdChanged,
});
- if (symbolIdChanged) {
- triggerRelatedWidgetsToUpdateRef.current(symbolId);
- }
- },
- [dispatch, saveContentProps, widgetId],
- );
+ if (symbolIdChanged) {
+ triggerRelatedWidgetsToUpdateRef.current(symbolId);
+ }
+ },
+ [dispatch, saveContentProps, widgetId, currentSymbolInfoRefCrunch, setCurrentSymbolInfo, setIsWidgetHeaderContextMenuOpen, triggerRelatedWidgetsToUpdateRef ],
+ );
- const addInstrumentFromModal = useCallback(
+ const addInstrumentFromModal = useCallback(
(instruments: SelectedInstrument[]): void => {
const instrument = instruments[0];
@@ -182,6 +138,86 @@ export default function useChartComponentFacade(props: ChartContainerProps): Use
[onInstrumentChange],
);
+ return {addInstrumentFromModal, onInstrumentChangeFromBind, onDropInstrument, onInstrumentChange}
+}
+
+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;
+
+ // TODO временно реф из-за непредсказуемого изменения если используется useState,
+ // нужен рефакторинг и вернуть обратно useState
+ const currentSymbolInfoRef = useRef<SymbolInfoInput>(DEFAULT_SYMBOL)
+
+ const savedInstrument = widgetProperties?.chartState?.savedInstrument
+
+ if(savedInstrument){
+ currentSymbolInfoRef.current = {
+ symbolId: savedInstrument,
+ symbol: widgetProperties?.chartState?.savedSymbol,
+ symbolName: widgetProperties?.chartState?.savedSymbolName,
+ };
+ }
+
+ const [currentSymbolInfo, setCurrentSymbolInfo] = useState<SymbolInfoInput>(currentSymbolInfoRef.current);
+
+ const { triggerRelatedWidgetsToUpdate, getMasterInstrumentFromPublicContext } = useWidgetsBind({
+ widgetId,
+ });
+
+ const triggerRelatedWidgetsToUpdateRef = useRef(triggerRelatedWidgetsToUpdate);
+
+ useEffect(() => {
+ triggerRelatedWidgetsToUpdateRef.current = triggerRelatedWidgetsToUpdate;
+ }, [triggerRelatedWidgetsToUpdate]);
+
+ const saveContentProps = useCallback(
+ (value: SaveContentPropsOptions): void => {
+ const { withUpdate, cleanIndicativeData, ...chartStateValue } = value;
+
+ const oldProps = (getStore().getState().widgets.widgets.find(({ id }) => id === widgetId) as Widget | undefined)
+ ?.widgetContentProps;
+
+ if (!oldProps) {
+ return;
+ }
+
+ dispatch(
+ addContentPropsToWidget({
+ id: widgetId,
+ withoutSend: !withUpdate,
+ widgetContentProps: {
+ chartState: {
+ ...oldProps.chartState,
+ ...chartStateValue,
+ },
+ // при смене инструмента очищаем индикативные данные, переданные из виджета Индикативные котировки
+ indicativeData: cleanIndicativeData ? undefined : oldProps.indicativeData,
+ },
+ }),
+ );
+ },
+ [dispatch, widgetId],
+ );
+
+ const { addInstrumentFromModal, onInstrumentChangeFromBind, onDropInstrument, onInstrumentChange } = useInstrumentChange({
+ saveContentProps,
+ setIsWidgetHeaderContextMenuOpen,
+ currentSymbolInfoRef,
+ setCurrentSymbolInfo,
+ triggerRelatedWidgetsToUpdateRef,
+ widgetId
+ })
+
useChartPublicContext({
setCurrInstrument: onInstrumentChangeFromBind,
widgetId,
diff --git a/src/widgets/Chart/hooks/useChartPublicContext.ts b/src/widgets/Chart/hooks/useChartPublicContext.ts
index 6a71316e05c1e8a0d0cc7ab261306a3c5e165490..a50be700ce7b824360688d648ff5f1a792a9b15c 100644
--- a/src/widgets/Chart/hooks/useChartPublicContext.ts
+++ b/src/widgets/Chart/hooks/useChartPublicContext.ts
@@ -42,13 +42,7 @@ export function useChartPublicContext({
const fieldValue = getMasterInstrumentFromPublicContext();
if (!fieldValue && widget?.master) {
- const defaultInstrument = instruments.find((instrument) => instrument.issKey === DEFAULT_SYMBOL);
-
- setCurrInstrument({
- symbolId: DEFAULT_SYMBOL,
- symbol: defaultInstrument?.symbol ?? undefined,
- symbolName: defaultInstrument?.displayName ?? undefined,
- });
+ setCurrInstrument(DEFAULT_SYMBOL);
return;
}