Загрузка данных
import React, { FC, lazy, useContext, useEffect, 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 { useAppSelect } from '@hooks/useAppSelector';
import { useChangeProperties } from '@modules/widgetProperties';
import { getIsShowMoexChartInChart } from '@store/selectors/cachedData';
import { WidgetEnvContext } from '@terminal/desktop/workspaces/default/components/Widget/context';
import { useWidgetHeaderName } from '@utils/useWidgetName';
import useChartComponentFacade from './hooks/useChartComponentFacade';
import DEFAULT_PROPERTIES from './properties/defaultProperties';
import type { WidgetProperties } from './properties/types';
import type { ChartContainerProps } from './types';
const LazyTREntry = lazy(() => import('./components/MoexChart/MoexChart'));
export const Chart: FC<ChartContainerProps> = function (props): JSX.Element {
const indicativeData = props.widgetContentProps?.indicativeData;
const { updateProperties } = useChangeProperties<WidgetProperties>();
const isShowMoexChartInChart = useAppSelect(getIsShowMoexChartInChart);
const [isMoexChartShow, setIsMoexChartShow] = useState(
// Доработка пошла хотфиксом, поэтому не стал исправлять все рабочие столы
props.widgetContentProps?.isMoexChartShow ?? DEFAULT_PROPERTIES.isMoexChartShow,
);
const {
chartContainerRef,
dropDownOpen,
setDropdownOpen,
currentInstrument,
handlerSaveAsExcel,
setIsWidgetHeaderContextMenuOpen,
isWidgetHeaderContextMenuOpen,
onDropInstruments,
addInstrumentFromModal,
isOver,
} = useChartComponentFacade(props);
/* на дроп обновляем название бумаги в сторе
и делаем апдейт на бэк, чтобы сохранить изменения
при обновлении страницы */
const { dropRef, isDragged } = 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.current);
const instrumentName = indicativeData
? `${indicativeData.instrumentName} ${indicativeData.settlement} - ${indicativeData.firmName}`
: contractsInstrumentName;
useEffect(() => {
updateProperties((state) => {
state.isMoexChartShow = isMoexChartShow;
});
}, [isMoexChartShow, updateProperties]);
return (
<DNDWrapper
ref={dropRef}
isOver={isOver}
canDrop
>
<WidgetHeader
{...props}
dropdownOpen={dropDownOpen}
setDropdownOpen={setDropdownOpen}
itemsSearchIcon={[true]}
handlerSaveAsExcel={indicativeData ? null : handlerSaveAsExcel}
addToWidgetNamePrefix={instrumentName}
setIsOpenContextMenuFromWidget={setIsWidgetHeaderContextMenuOpen}
isOpenContextMenuFromWidget={isWidgetHeaderContextMenuOpen}
openInstrumentsModal={openInstrumentModal}
/>
<WidgetContentWrapper {...props}>
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
{isShowMoexChartInChart && (
<LazyTREntry
fullName={currentInstrument.current}
widgetId={props.widgetId}
/>
)}
<div
className="TVChartContainer"
style={{
width: '100%',
height: '100%',
pointerEvents: isDragged || isDraggedOver ? 'none' : 'inherit',
display: !isMoexChartShow ? 'block' : 'none',
}}
ref={chartContainerRef}
/>
{isOpenEmptyAction && (
<InstrumentSearch
setOpen={setIsOpenEmptyAction}
isOpen={isOpenEmptyAction}
variant="single"
widgetId={props.widgetId}
// withNRD={false}
addInstruments={addInstrumentFromModal}
/>
)}
</div>
</WidgetContentWrapper>
</DNDWrapper>
);
};
import classNames from 'classnames';
import React, { Component, PropsWithChildren } from 'react';
import { WidgetToastContainer } from '@components/Toast';
import features from '@features';
import styles from './widgetContentWrapper.module.scss';
import type { WidgetContentBasicProps } from 'types/Widgets';
type WidgetContentWrapperProps = PropsWithChildren<Record<string, unknown> & WidgetContentBasicProps>;
interface WidgetContentWrapperState {
error: boolean;
}
class WidgetContentWrapper extends Component<WidgetContentWrapperProps, WidgetContentWrapperState> {
render() {
const {
children,
onWidgetContentClick,
scrollable = true,
refWrapper,
style,
widgetId,
containsIframe,
isIframeView,
} = this.props;
return (
<div
onClick={onWidgetContentClick}
className={classNames(styles['widget-content-wrapper'], {
'-not-scroll': !scrollable,
[styles['widget-content-wrapper-contains-iframe']]: features.enableBookBuilder && containsIframe,
[styles['widget-content-wrapper-inside-iframe']]: features.enableBookBuilder && isIframeView,
})}
style={style}
ref={refWrapper}
>
{children}
<WidgetToastContainer widgetId={widgetId} />
</div>
);
}
}
export default WidgetContentWrapper;
import { Dropdown, Input, MenuProps } from 'antd';
import classNames from 'classnames';
import React, { FC, MouseEvent, ReactNode, useEffect, useState } from 'react';
import { ContextMenu } from '@components/ContextMenu';
import { IconButton } from '@components/IconButton';
import Icons from '@components/Icons';
import './widgetHeader.scss';
import { CloseIcon } from '@components/Icons/CloseIcon';
import { WidgetBindIcon } from '@components/Icons/WidgetBindIcon';
import { InputSearch } from '@components/Input';
import { TextInput } from '@components/TextMad';
import { Footnote } from '@components/TextUI';
import { MacroDataWidgetMenu } from '@components/WidgetHeader/components/MacroDataWidgetMenu';
import { bindingWidgets } from '@components/WidgetHeader/config';
import { DRUG_HEADER_CLASSNAME, NO_DRAG_CLASSNAME } from '@configs/appConfig';
import { useAppSelect } from '@hooks/useAppSelector';
import { useWidgetNamePrefix } from '@hooks/useWidgetNamePrefix';
import colors from '@styles/colors.module.scss';
import { CheckboxValue } from '@uikit/Checkbox';
import Typography from '@uikit/Typography';
import { wrapBetaInSpan } from '@utils/wrapBetaInSpan';
import { FixingData } from '@widgets/Fixings/useFixingsFacade';
import { useChatUIConfig } from '@widgets/NoTradeChat/context/ChatUIConfig';
import { FromNoTradeChat } from 'types/NoTradeChat';
import { isDefined } from 'types/utils';
import { WidgetContentType } from 'types/Widgets';
import { DefaultMenuProps, DefaultWidgetMenu } from './components/DefaultWidgetMenu';
import { FilterSwitch, FilterSwitchProps } from './components/FilterSwitch';
import { useDropdownPosition } from './hooks/useDropdownPosition';
import useWidgetBind from './hooks/useWidgetBind';
import { useWidgetHeaderFacade } from './hooks/useWidgetHeaderFacade';
import styles from './widgetHeader.module.scss';
import type { WidgetContentBasicProps } from 'types/Widgets';
const { MoreVertIcon, FilterTableIcon } = Icons;
export interface WidgetHeaderProps extends WidgetContentBasicProps {
editWidget?(e: MouseEvent): void;
/* Необходимые элементы для отображения элемента меню "Настроить" */
items?: MenuProps['items'];
itemsSearchIcon?: MenuProps['items'] | boolean[];
addToWidgetNamePrefix?: string;
dropDownRender?(menu: React.ReactNode): JSX.Element;
dropDownRenderSearchIcon?(menu: React.ReactNode): JSX.Element;
/* Отображение элемента меню "Настроить". По дефолту включено */
showWidgetSetting?: boolean;
dropdownOpen?: boolean;
setDropdownOpen?: React.Dispatch<React.SetStateAction<boolean>>;
ticketFixingsWidget?: FixingData | null;
visible?: boolean;
setVisible?: React.Dispatch<React.SetStateAction<boolean>>;
fromNoTradeChat?: FromNoTradeChat;
setFilterToolState?: React.Dispatch<
React.SetStateAction<{
isOpenFilterBlock: boolean;
isOpenFilterTools: boolean;
activeFilterCount: number;
}>
>;
filterToolState?: {
isOpenFilterBlock: boolean;
isOpenFilterTools: boolean;
activeFilterCount: number;
} | null;
/** undefined - не отображаем, null - дизейблим */
handlerSaveAsExcel?: (() => void) | null;
setIsLinkedWidget?: React.Dispatch<React.SetStateAction<boolean>>;
setIsOpenedSearchBlock?: React.Dispatch<React.SetStateAction<boolean>>;
isOpenedSearchBlock?: boolean;
widgetType?: WidgetContentType;
handleHeaderSearch?: (value: string) => void;
headerSearchValue?: string;
className?: string;
filterCheckedList?: CheckboxValue[];
isAbleToExpand?: boolean;
children?: React.ReactNode;
isOpenContextMenuFromWidget?: boolean;
setIsOpenContextMenuFromWidget?: React.Dispatch<React.SetStateAction<boolean>>;
/** Связать с другим виджетом */
hideBindIcon?: boolean;
openInstrumentsModal?: () => void;
menuItems?: ReactNode[];
filterSwitch?: FilterSwitchProps;
/** Компонент поиска содержимого из таблицы */
tableSearchComponent?: React.JSX.Element | undefined;
/** Кнопки, размещаемые в хедере справа */
rightBtns?: ReactNode;
/** Информация, размещаемая под названием виджета (например, торговый логин пользователя) */
details?: string;
/** Вместо троеточия будет просто крестик, по клику на который можно будет закрыть виджет */
closeIconInsteadOfDotsWithDropdown?: boolean;
}
const WidgetHeader: FC<WidgetHeaderProps> = function (props): JSX.Element {
const {
widgetId,
items = [],
itemsSearchIcon = [],
dropDownRender,
dropDownRenderSearchIcon,
addToWidgetNamePrefix = '',
showWidgetSetting = true,
dropdownOpen,
setDropdownOpen,
ticketFixingsWidget = null,
visible = false,
setVisible,
fromNoTradeChat,
setFilterToolState,
filterToolState = null,
handlerSaveAsExcel,
setIsLinkedWidget,
setIsOpenedSearchBlock,
isOpenedSearchBlock,
handleHeaderSearch,
className,
filterCheckedList,
isAbleToExpand,
children,
isOpenContextMenuFromWidget,
setIsOpenContextMenuFromWidget,
hideBindIcon,
openInstrumentsModal,
headerSearchValue,
menuItems,
filterSwitch,
tableSearchComponent,
rightBtns,
details,
closeIconInsteadOfDotsWithDropdown,
} = props;
const {
type,
name,
isEditMode,
inputRef,
inputValue,
onInputChange,
onEditBtnClick,
onExpandBtnClick,
deleteWidget,
isExpand,
event,
isMoreBtnDropdownOpen,
setIsMoreBtnDropdownOpen,
onMoreBtnClick,
resetEvent,
} = useWidgetHeaderFacade(props);
const { itemsBind, bindCount, colorIconBind, isBindDisabled } = useWidgetBind({
widgetId,
type,
setIsLinkedWidget,
});
const { namePrefix } = useWidgetNamePrefix(type);
const [dropwdownSearchIcon] = useDropdownPosition({
isOpen: dropdownOpen ?? false,
});
const uiConfig = useChatUIConfig();
const widgetType = useAppSelect((state) => state.widgets.widgets.find((it) => it.id === widgetId))?.type;
const isCanBindWidget = widgetType && bindingWidgets.includes(widgetType);
const [linkedWidgets, setLinkedWidgets] = useState(false);
const defaultMenuProps: DefaultMenuProps = {
onEditBtnClick,
fromNoTradeChat,
setIsMoreBtnDropdownOpen,
showWidgetSetting,
dropDownRender,
items,
visible,
setIsOpenedSearchBlock,
setVisible,
dropDownRenderSearchIcon,
itemsSearchIcon,
dropwdownSearchIcon,
dropdownOpen,
setDropdownOpen,
handlerSaveAsExcel,
onExpandBtnClick,
isExpand,
deleteWidget,
linkedWidgets,
setLinkedWidgets,
itemsBind,
bindCount,
isCanBindWidget,
filterCheckedList,
openInstrumentsModal,
};
const dropdownMenuMap: Partial<Record<WidgetContentType, JSX.Element>> = {
[WidgetContentType.macroData]: (
<MacroDataWidgetMenu
deleteWidget={deleteWidget}
items={items}
setVisible={setVisible}
visible={visible}
handlerSaveAsExcel={handlerSaveAsExcel}
setFilterToolState={setFilterToolState}
resetEvent={resetEvent}
dropDownRender={dropDownRender}
/>
),
[WidgetContentType.news]: (
<DefaultWidgetMenu
defaultMenuProps={{ ...defaultMenuProps, isNewsWidget: true }}
isAbleToExpand={isAbleToExpand}
/>
),
};
useEffect(() => {
if (!isMoreBtnDropdownOpen) {
setLinkedWidgets(false);
}
}, [isMoreBtnDropdownOpen]);
const onSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!handleHeaderSearch) {
return;
}
const { value } = e.target;
handleHeaderSearch(value);
};
if (children) {
// используется в виджете IncomingCall
return (
<div
style={{ display: `${isOpenedSearchBlock ? 'none' : ''}` }}
className={classNames(styles['workspace-widget-content-header-children'], DRUG_HEADER_CLASSNAME, className)}
>
{children}
</div>
);
}
const shouldDisplaySearchBar = !!filterToolState?.isOpenFilterBlock;
const haveCustomSearchBar = !!tableSearchComponent;
return (
<div
style={{ display: `${isOpenedSearchBlock ? 'none' : ''}` }}
className={classNames(
styles['workspace-widget-content-header'],
{
[styles['no-cursor-move']]: uiConfig?.isIframeView,
[styles['workspace-widget-content-header_large']]: Boolean(details),
},
DRUG_HEADER_CLASSNAME,
className,
)}
>
{/* Компонент поиска содержимого из таблицы */}
{haveCustomSearchBar && tableSearchComponent}
{!haveCustomSearchBar && shouldDisplaySearchBar && (
<div className="virtual-table-search-top">
<div className="virtual-table-search-input">
<InputSearch
placeholder="Поиск"
value={headerSearchValue}
onChange={onSearchChange}
/>
</div>
<div className={styles.flexGapSmall}>
<div
onClick={() => {
if (setFilterToolState) {
setFilterToolState((prevState) => ({
...prevState,
isOpenFilterTools: !filterToolState?.isOpenFilterTools,
}));
}
}}
className="virtual-table-search-icon virtual-table-search-icon-filter"
>
<FilterTableIcon style={filterToolState?.isOpenFilterTools ? { color: '#6C7FE0' } : {}} />
{filterToolState.activeFilterCount > 0 && !filterToolState?.isOpenFilterTools && (
<div className="virtual-table-search-icon-count">{filterToolState.activeFilterCount}</div>
)}
</div>
<div
onMouseDown={(e) => e.stopPropagation()}
onClick={() => {
if (handleHeaderSearch) {
handleHeaderSearch('');
}
if (setFilterToolState) {
setFilterToolState((prevState) => ({
...prevState,
isOpenFilterBlock: false,
isOpenFilterTools: false,
}));
}
}}
className="virtual-table-search-icon virtual-table-search-icon-close"
>
<CloseIcon />
</div>
</div>
</div>
)}
{!haveCustomSearchBar && !shouldDisplaySearchBar && (
<div className={styles['workspace-widget-content-header_container']}>
<div className={styles['title-container']}>
<div className={styles.title}>
<TextInput
style={{
textOverflow: 'ellipsis',
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
}}
>
{wrapBetaInSpan(String(namePrefix))}
{wrapBetaInSpan(addToWidgetNamePrefix)}
</TextInput>
<TextInput style={{ display: 'flex', color: colors['text-interface-on-color'] }}>
{name && !isEditMode ? `- ${name}` : ''}
</TextInput>
{!hideBindIcon && (
<Dropdown
disabled={isBindDisabled}
placement="bottomLeft"
arrow={{ pointAtCenter: true }}
menu={{ items: itemsBind }}
trigger={['click']}
>
<div
style={{ color: colorIconBind || 'grey' }}
className={classNames(styles['left-btn-container'], NO_DRAG_CLASSNAME)}
>
<button
style={{ color: colorIconBind || 'grey' }}
type="button"
>
<WidgetBindIcon />
</button>
{bindCount || ''}
</div>
</Dropdown>
)}
{ticketFixingsWidget && <Footnote style={{ display: 'flex' }}>{ticketFixingsWidget.ticker}</Footnote>}
{isEditMode && (
<>
<Footnote style={{ display: 'flex' }}>-</Footnote>
<Input
style={{ display: 'flex', height: '21px' }}
ref={inputRef}
value={inputValue}
onChange={onInputChange}
/>
</>
)}
</div>
{details && (
<Typography.Paragraph.XS
text={details}
className={styles.subtitle}
/>
)}
</div>
<div className={styles['right-btn-container']}>
{/** TODO Убрать когда точно будем знать что это нам не нужно */}
{filterToolState && filterToolState?.activeFilterCount > 0 && (
<div
aria-disabled
onClick={() => {
if (setFilterToolState) {
setFilterToolState((prevState) => ({
...prevState,
isOpenFilterTools: true,
isOpenFilterBlock: true,
}));
}
}}
className={classNames(
'virtual-table-search-icon',
'virtual-table-search-icon-filter',
'virtual-table-search-icon-filter-close',
)}
>
<FilterTableIcon />
{filterToolState.activeFilterCount > 0 && (
<div className="virtual-table-search-icon-count">{filterToolState.activeFilterCount}</div>
)}
</div>
)}
{rightBtns}
{filterSwitch && <FilterSwitch {...filterSwitch} />}
{closeIconInsteadOfDotsWithDropdown ? (
<IconButton
className={classNames(NO_DRAG_CLASSNAME, styles.closeIcon)}
icon={<CloseIcon />}
onClick={deleteWidget}
/>
) : (
<button
type="button"
className={NO_DRAG_CLASSNAME}
onClick={onMoreBtnClick}
>
<MoreVertIcon />
</button>
)}
<ContextMenu
event={event}
openContextMenu={
isDefined(isOpenContextMenuFromWidget) ? isOpenContextMenuFromWidget : isMoreBtnDropdownOpen
}
setOpenContextMenu={
isDefined(setIsOpenContextMenuFromWidget) ? setIsOpenContextMenuFromWidget : setIsMoreBtnDropdownOpen
}
setDropdownOpen={setDropdownOpen}
setVisible={setVisible}
>
{dropdownMenuMap[widgetType as WidgetContentType] ?? (
<DefaultWidgetMenu
defaultMenuProps={defaultMenuProps}
isAbleToExpand={isAbleToExpand}
menuItems={menuItems}
/>
)}
</ContextMenu>
</div>
</div>
)}
</div>
);
};
export default WidgetHeader;
import isEqual from 'lodash/isEqual';
import { Dispatch, MutableRefObject, SetStateAction, useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { candlesController } from '@api/controllers/candles';
import { uiConfig } from '@configs/manager';
import { communicator } from '@core/comm';
import { useAppSelect } from '@hooks/useAppSelector';
import {
ChartingLibraryWidgetOptions,
IChartingLibraryWidget,
ResolutionString,
widget,
} from '@modules/charting_library/charting_library';
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 { chartToReqTimeConverter } from '@utils/chartToReqTimeConverter';
import { useWidgetsBind } from '@utils/hooks/useWidgetsBind';
import { DAY_LINE_ON_CHART_BOARDS, DEFAULT_SYMBOL, MINUTE_CANDLES_ON_CHART_BOARDS } from '../const';
import useDatafeed from '../datafeed';
import { ResolutionMap } from '../types';
import { isIndicativeTicker } from '../utils/isIndicativeTicker';
import { mapResolutionIntervals } from '../utils/resolutionIntervals';
import { formatToDateTime } from '../utils/utils';
import { useChartPublicContext } from './useChartPublicContext';
import type { WidgetProperties } from '../properties/types';
import type { ChartContainerProps } from '../types';
import type { Candles } from 'types/Candles';
import type { Widget } from 'types/Widgets';
enum SeriesStyle {
Bars = 0,
Candles = 1,
Line = 2,
Area = 3,
HeikenAshi = 8,
HollowCandles = 9,
Baseline = 10,
HiLo = 12,
Column = 13,
Renko = 4,
Kagi = 5,
PointAndFigure = 6,
LineBreak = 7,
}
interface UseChartComponentFacadeReturn {
chartContainerRef: MutableRefObject<HTMLDivElement>;
savedTvWidget: IChartingLibraryWidget | null;
dropDownOpen: boolean;
setDropdownOpen: Dispatch<SetStateAction<boolean>>;
currentInstrument: MutableRefObject<string>;
handlerSaveAsExcel: (() => void) | undefined;
isWidgetHeaderContextMenuOpen: boolean;
setIsWidgetHeaderContextMenuOpen: Dispatch<SetStateAction<boolean>>;
onDropInstruments: (val: string, withUpdate?: boolean) => void;
addInstrumentFromModal: (instruments: Contract[]) => void;
isOver: boolean;
}
const DEFAULT_OPTIONS = {
symbol: DEFAULT_SYMBOL,
interval: '1' as ResolutionString,
datafeedUrl: 'https://demo_feed.tradingview.com',
libraryPath: `${uiConfig.staticContextPath || ''}/charting_library/`,
chartsStorageUrl: 'https://saveload.tradingview.com',
chartsStorageApiVersion: '1.1' as const,
clientId: 'tradingview.com',
userId: 'public_user_id',
fullscreen: false,
autosize: true,
studiesOverrides: {},
};
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 tvWidgetRef = useRef<IChartingLibraryWidget | null>(null);
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 [savedTvWidget, setTvWidget] = useState<IChartingLibraryWidget | null>(null);
/**
* Копия chartIsReady тк некоторые useCallback хранят старое значение переменной.
* Полностью не переходим на реф тк стейт используется в useEffect.
* Рекомендуется использовать реф, за исключением если требуется срабатывать useEffect на chartIsReady.
*/
const isChartIsReadyFromRef = useRef<boolean>(false);
const [chartIsReady, setChartIsReadyFromUseState] = useState(false);
const [isWidgetHeaderContextMenuOpen, setIsWidgetHeaderContextMenuOpen] = useState(false);
const setChartIsReady = useCallback((isReady: boolean) => {
setChartIsReadyFromUseState(isReady);
isChartIsReadyFromRef.current = isReady;
}, []);
useEffect(() => {
tvWidgetRef.current = savedTvWidget;
}, [savedTvWidget]);
const language = useAppSelect((state) => state.localisation.localisation);
const { chartState } = useAppSelect((state) => state.widgets.widgets.find(({ id }) => id === widgetId))
?.widgetContentProps as WidgetProperties;
// TODO временно реф из-за непредсказуемого изменения если используется useState,
// нужен рефакторинг и вернуть обратно useState
const currentInstrument = useRef<NonNullable<Contract['issKey']>>(
(chartState?.savedInstrument ?? DEFAULT_OPTIONS.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,
},
}),
);
}
};
const handlerSaveAsExcel = () => {
const chart = savedTvWidget?.activeChart();
const visibleRange = chart ? chart.getVisibleRange() : null;
const checkedInterval = chart?.resolution();
// Интервал из которого фактически будут формироваться свечи на графике
const interval = mapResolutionIntervals((checkedInterval ?? DEFAULT_OPTIONS.interval) as keyof ResolutionMap);
const timezoneTitle = tvWidgetRef.current?.activeChart().getTimezoneApi().getTimezone()?.title;
let timezoneAsNumber;
if (timezoneTitle === 'Биржа') {
timezoneAsNumber = 3;
} else if (timezoneTitle && timezoneTitle.includes(':')) {
// если часовой пояс с дробным числом в формате UTC то по договоренности с бэком берем только целую часть
timezoneAsNumber = Number(
timezoneTitle
.split('UTC' as string)[1]
.split(')')[0]
.split(':')[0],
);
} else {
timezoneAsNumber = timezoneTitle ? Number(timezoneTitle.split('UTC')[1].split(')')[0]) : 3;
}
if (visibleRange) {
const startDateTime = formatToDateTime(visibleRange.from);
const endDateTime = formatToDateTime(visibleRange.to);
const data: Candles = {
secId: currentInstrument.current.replace(/:/g, '.'),
from: startDateTime,
till: endDateTime,
interval: chartToReqTimeConverter(interval ?? DEFAULT_OPTIONS.interval),
value: chartToReqTimeConverter(checkedInterval ?? DEFAULT_OPTIONS.interval),
offset: timezoneAsNumber,
};
candlesController
.upload(data)
.then((response) => {
// eslint-disable-next-line @typescript-eslint/no-shadow -- TODO исправить reasign
const { data, headers } = response;
const blob = new Blob([data]);
const fileName = headers['content-disposition']?.match(/filename="(.+)"/)?.[1] || 'default_filename';
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = fileName;
link.click();
})
.catch((error) => {
// eslint-disable-next-line no-console -- Обработка исключения
console.error('Error uploading data:', error);
});
} else {
// eslint-disable-next-line no-console -- Обработка исключения
console.error('Chart is undefined or visible range is not available.');
}
};
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 onInstrumentChangeCallback = useCallback((prevInstr: string, newInstr: string) => {
if (prevInstr === newInstr || !isChartIsReadyFromRef.current) {
return;
}
const newBoardInLowerCase = newInstr.split(':')[1]?.toLowerCase() ?? '';
const prevBoardInLowerCase = prevInstr.split(':')[1]?.toLowerCase() ?? '';
// для двух специфичных случаев точечно изменяем тип линии и таймфрейм
if (
((prevInstr === DEFAULT_SYMBOL ||
!DAY_LINE_ON_CHART_BOARDS.includes(prevBoardInLowerCase) ||
!MINUTE_CANDLES_ON_CHART_BOARDS.includes(prevBoardInLowerCase)) &&
DAY_LINE_ON_CHART_BOARDS.includes(newBoardInLowerCase)) ||
(MINUTE_CANDLES_ON_CHART_BOARDS.includes(prevBoardInLowerCase) &&
DAY_LINE_ON_CHART_BOARDS.includes(newBoardInLowerCase))
) {
tvWidgetRef.current?.activeChart().setChartType(SeriesStyle.Line);
tvWidgetRef.current?.activeChart().setResolution('1D' as ResolutionString);
}
if (
MINUTE_CANDLES_ON_CHART_BOARDS.includes(newBoardInLowerCase) &&
DAY_LINE_ON_CHART_BOARDS.includes(prevBoardInLowerCase)
) {
tvWidgetRef.current?.activeChart().setChartType(SeriesStyle.Candles);
tvWidgetRef.current?.activeChart().setResolution('1' as ResolutionString);
}
// для индикатива при выборе из Поиска инструментов и после перезагрузки устанавливаем
// нужные настройки графика
if (isIndicativeTicker(newInstr)) {
tvWidgetRef.current?.activeChart().setChartType(SeriesStyle.Line);
tvWidgetRef.current?.activeChart().setResolution('60' as ResolutionString);
}
}, []);
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 { datafeed, windowIntervalRequester } = useDatafeed(
tvWidgetRef,
prevTimeframe,
timeDiffOfIntervalChangeTime,
isInstrumentChangedFromBinding,
indicativeData,
);
const createWidget = () => {
if (!chartContainerRef.current) {
return;
}
const todayData = new Date();
const halfYearAgo = new Date();
halfYearAgo.setMonth(halfYearAgo.getMonth() - 6);
const threeMonthsAgo = new Date();
threeMonthsAgo.setDate(threeMonthsAgo.getDate() - 90);
const oneDayAgo = new Date();
oneDayAgo.setDate(oneDayAgo.getDate() - 1);
const thisYearStartDate = new Date();
thisYearStartDate.setMonth(0);
thisYearStartDate.setDate(1);
const diffBetween = todayData.getTime() - thisYearStartDate.getTime();
const dayDiff = diffBetween / 1000 / 3600 / 24;
const minutesDiff = diffBetween / 1000 / 60;
const ytlResolution = `${dayDiff > 0 ? dayDiff : minutesDiff}${dayDiff > 0 ? 'D' : ''}` as ResolutionString;
const widgetOptions: ChartingLibraryWidgetOptions = {
symbol: currentInstrument.current ?? chartState?.savedInstrument ?? (DEFAULT_OPTIONS.symbol as string),
// BEWARE: no trailing slash is expected in feed URL
// tslint:disable-next-line:no-any
datafeed,
interval: (chartState?.interval ?? DEFAULT_OPTIONS.interval) as ChartingLibraryWidgetOptions['interval'],
container: chartContainerRef.current,
library_path: DEFAULT_OPTIONS.libraryPath as string,
locale: language,
timezone: 'Europe/Moscow',
charts_storage_url: DEFAULT_OPTIONS.chartsStorageUrl,
charts_storage_api_version: DEFAULT_OPTIONS.chartsStorageApiVersion,
client_id: DEFAULT_OPTIONS.clientId,
time_frames: [
{ text: '1D', resolution: '60' as ResolutionString, description: '1d' },
{ text: '5d', resolution: '60' as ResolutionString, description: '5d' },
{ text: '1M', resolution: 'D' as ResolutionString, description: '1M' },
{ text: '6M', resolution: 'D' as ResolutionString, description: '6M' },
{
text: ytlResolution,
resolution: '7D' as ResolutionString,
description: 'YTL',
title: 'YTD',
},
{ text: '1Y', resolution: '7D' as ResolutionString, description: '6M' },
{ text: '5Y', resolution: '1M' as ResolutionString, description: '6M' },
{
text: '1000y',
resolution: '7D' as ResolutionString,
description: 'All',
title: 'All',
},
],
user_id: DEFAULT_OPTIONS.userId,
fullscreen: DEFAULT_OPTIONS.fullscreen,
autosize: DEFAULT_OPTIONS.autosize,
symbol_search_request_delay: 1000,
enabled_features: ['study_templates', 'hide_left_toolbar_by_default', 'header_compare', 'header_indicators'],
disabled_features: ['use_localstorage_for_settings'],
custom_css_url: 'css/customstyles.css',
theme: 'Dark',
save_load_adapter: {
charts: [],
studyTemplates: [],
drawingTemplates: [],
getAllCharts() {
return Promise.resolve(this.charts);
},
removeChart(id: number) {
for (let i = 0; i < this.charts.length; i += 1) {
if (this.charts[i].id === id) {
this.charts.splice(i, 1);
return Promise.resolve();
}
}
return Promise.reject();
},
saveChart(chartData: Record<string, unknown>) {
if (!chartData.id) {
// eslint-disable-next-line no-param-reassign -- Ругается.
chartData.id = Math.random().toString();
} else {
this.removeChart(chartData.id);
}
// eslint-disable-next-line no-param-reassign -- Ругается.
chartData.timestamp = new Date().valueOf();
this.charts.push(chartData);
return Promise.resolve(chartData.id);
},
getChartContent(id: number) {
for (let i = 0; i < this.charts.length; i += 1) {
if (this.charts[i].id === id) {
return Promise.resolve(this.charts[i].content);
}
}
// eslint-disable-next-line no-console -- Обработка исключения
console.error('error');
return Promise.reject();
},
removeStudyTemplate(studyTemplateData: Record<string, unknown>) {
for (let i = 0; i < this.studyTemplates.length; i += 1) {
if (this.studyTemplates[i].name === studyTemplateData.name) {
this.studyTemplates.splice(i, 1);
return Promise.resolve();
}
}
return Promise.reject();
},
getStudyTemplateContent(studyTemplateData: Record<string, unknown>) {
for (let i = 0; i < this.studyTemplates.length; i += 1) {
if (this.studyTemplates[i].name === studyTemplateData.name) {
return Promise.resolve(this.studyTemplates[i]);
}
}
// eslint-disable-next-line no-console -- Обработка исключения
console.error('st: error');
return Promise.reject();
},
saveStudyTemplate(studyTemplateData: Record<string, unknown>) {
for (let i = 0; i < this.studyTemplates.length; i += 1) {
if (this.studyTemplates[i].name === studyTemplateData.name) {
this.studyTemplates.splice(i, 1);
break;
}
}
this.studyTemplates.push(studyTemplateData);
return Promise.resolve();
},
getAllStudyTemplates() {
return Promise.resolve(this.studyTemplates);
},
removeDrawingTemplate(toolName: string, templateName: string) {
for (let i = 0; i < this.drawingTemplates.length; i += 1) {
if (this.drawingTemplates[i].name === templateName) {
this.drawingTemplates.splice(i, 1);
return Promise.resolve();
}
}
return Promise.reject();
},
loadDrawingTemplate(toolName: string, templateName: string) {
for (let i = 0; i < this.drawingTemplates.length; i += 1) {
if (this.drawingTemplates[i].name === templateName) {
return Promise.resolve(this.drawingTemplates[i].content);
}
}
// eslint-disable-next-line no-console -- Обработка исключения
console.error('drawing: error');
return Promise.reject();
},
saveDrawingTemplate(toolName: string, templateName: string, content: unknown) {
for (let i = 0; i < this.drawingTemplates.length; i += 1) {
if (this.drawingTemplates[i].name === templateName) {
this.drawingTemplates.splice(i, 1);
break;
}
}
this.drawingTemplates.push({ name: templateName, content });
return Promise.resolve();
},
getDrawingTemplates() {
return Promise.resolve(this.drawingTemplates.map((template: Record<string, unknown>) => template.name));
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Ругается.
} as any,
overrides: {
'mainSeriesProperties.lineStyle.color': '#576DDB',
'paneProperties.legendProperties.showSeriesTitle': false,
},
};
if (chartState && chartState.savedData) {
try {
widgetOptions.saved_data = JSON.parse(chartState.savedData);
} catch {
widgetOptions.saved_data = undefined;
}
}
// eslint-disable-next-line new-cap -- Ругается
const tvWidget = new widget(widgetOptions);
tvWidget.onChartReady(() => {
setChartIsReady(true);
tvWidget.chart().getTimezoneApi().setTimezone('Europe/Moscow');
tvWidget
?.activeChart()
?.onIntervalChanged()
?.subscribe(null, (newInterval) => {
const oldProps = (getState().widgets.widgets.find(({ id }) => id === widgetId) as Widget)?.widgetContentProps;
prevTimeframe.current = oldProps.chartState.interval;
const now = Date.now();
if (lastIntervalChangeTime.current) {
timeDiffOfIntervalChangeTime.current = now - lastIntervalChangeTime.current;
}
lastIntervalChangeTime.current = now;
if (oldProps) {
dispatch(
addContentPropsToWidget({
id: widgetId,
widgetContentProps: {
chartState: {
...oldProps.chartState,
interval: newInterval,
},
},
}),
);
}
});
// tvWidget.chart().getTimezoneApi().setTimezone("Europe/Moscow");
tvWidget
.activeChart()
.onSymbolChanged()
.subscribe(null, () => {
// если инструмент из индикативныз котировок то его не надо отдельно созранять через onInstrumentChange
if (indicativeData) {
return;
}
if (currentInstrument.current === tvWidget.activeChart().symbol()) {
return;
}
// если инструмент не из привязки то обновляем значение и отвязываем привязку
if (!isInstrumentChangedFromBinding.current) {
onInstrumentChange(tvWidget.activeChart().symbol(), false, true);
}
});
});
setTvWidget(tvWidget);
const counter = setInterval(() => {
if (!chartContainerRef.current || !tvWidgetRef.current) {
if (!chartContainerRef.current) {
clearInterval(counter);
}
return;
}
tvWidget?.save((state) => {
if (chartPrevStateRef.current && !isEqual(state, chartPrevStateRef.current)) {
chartPrevStateRef.current = state;
saveContentProps({
interval: tvWidget.symbolInterval().interval,
savedData: JSON.stringify(state),
});
}
});
}, 1000);
};
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]);
useEffect(
() => () => {
Object.values(windowIntervalRequester).forEach(clearInterval);
},
[windowIntervalRequester],
);
useEffect(
() => () => {
if (savedTvWidget !== null) {
savedTvWidget.remove();
setTvWidget(null);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps -- TODO Разобраться с зависимостями
[],
);
useEffect(() => {
if (savedTvWidget?.getLanguage() === language) {
return;
}
createWidget();
// eslint-disable-next-line react-hooks/exhaustive-deps -- TODO Разобраться с зависимостями
}, [language]);
/** Переключение инструмента и вида графика при 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,
};
}
declare global {
interface Window {
CONTEXT_PATH: string | undefined;
STATIC_CONTEXT_PATH: string | undefined;
FORMALIZATION_CONTEXT_PATH: string | undefined;
CURRENT_BACKEND_HOST: string;
CURRENT_BACKEND_REST_HOST: string;
CURRENT_BACKEND_WS_HOST: string;
WS_QUERY_ACCESS_TOKEN_ENABLED: 'true' | 'false';
ROCKET_CHAT_URL: string;
DISPLAY_MARKETING_NOTIFICATION: string | undefined;
TIME_REPEAT_ANIMATION_CHATS: string | undefined;
FEATURE_WEBPUSH: 'true' | 'false';
WEBPUSH_PUBLIC_KEY: string | undefined;
FEATURE_FLAG_1: string | undefined;
FEATURE_FLAG_2: string | undefined;
FEATURE_FLAG_3: string | undefined;
SPFI_HOST: string;
SPFI_AUTH_HOST: string;
}
}
type UIConfig = {
contextPath: string;
staticContextPath: string;
formalizationContextPath: string;
api: UIConfigAPI;
showMarketingNotification: boolean;
timeRepeatChatAnimation: number;
featureWebpush: boolean;
webpushPublicKey: string;
featureFlag1: string;
featureFlag2: string;
featureFlag3: string;
};
type UIConfigAPI = {
wsHost: string;
restHost: string;
spfiHost: string;
spfiAuthHost: string;
wsQueryAccessTokenEnable: boolean;
};
interface IUIConfigManager {
backendRestUrl: string;
spfiHostUrl: string;
spfiAuthHostUrl: string;
backendWSUrl: string;
needToAddTokenQueryParamForWS: boolean;
contextPath: string;
formalizationContextPath: string;
staticContextPath: string;
showMarketingNotification: boolean;
timeRepeatChatAnimation: number;
featureWebpushEnabled: boolean;
webpushPublicKey: string;
featureFlag1: string;
featureFlag2: string;
featureFlag3: string;
}
const extractStringParam = (property: keyof Window): string => {
let value = window[property] ?? '';
if (typeof value !== 'string') {
value = value.toString();
}
return value;
};
/** Изоляция скрипта конфигурации и повышение надёжности */
class UIConfigManager implements IUIConfigManager {
private config: UIConfig;
private isLegacyConfig: boolean;
private static safeExtractWSTokenInjectParamRaw() {
return extractStringParam('WS_QUERY_ACCESS_TOKEN_ENABLED');
}
private static isLegacyAPIConfig() {
const rawValue = UIConfigManager.safeExtractWSTokenInjectParamRaw();
const isModernConfig = rawValue === 'true' || rawValue === 'false';
return !isModernConfig;
}
private extractWSQueryInjectParam() {
if (this.isLegacyConfig) {
return false;
}
if (typeof window.WS_QUERY_ACCESS_TOKEN_ENABLED === 'undefined') {
return false;
}
if (window.WS_QUERY_ACCESS_TOKEN_ENABLED === 'true') {
return true;
}
return false;
}
private extractRestHost(): string {
return this.isLegacyConfig ? '' : extractStringParam('CURRENT_BACKEND_REST_HOST');
}
private static extractSPFIHost(): string {
return extractStringParam('SPFI_HOST');
}
private static extractSPFIAuthHost(): string {
return extractStringParam('SPFI_AUTH_HOST');
}
private static extractFeatureWebPush(): boolean {
const webpushEnabled = extractStringParam('FEATURE_WEBPUSH');
return webpushEnabled === 'true';
}
private extractWSHost(): string {
const key: keyof Window = this.isLegacyConfig ? 'CURRENT_BACKEND_HOST' : 'CURRENT_BACKEND_WS_HOST';
return extractStringParam(key);
}
private extractAPIConfig(): UIConfigAPI {
return {
restHost: this.extractRestHost(),
spfiHost: UIConfigManager.extractSPFIHost(),
spfiAuthHost: UIConfigManager.extractSPFIAuthHost(),
wsHost: this.extractWSHost(),
wsQueryAccessTokenEnable: this.extractWSQueryInjectParam(),
};
}
private extractVariables(): UIConfig {
const showMarketingNotification = extractStringParam('DISPLAY_MARKETING_NOTIFICATION');
return {
api: this.extractAPIConfig(),
contextPath: extractStringParam('CONTEXT_PATH'),
formalizationContextPath: extractStringParam('FORMALIZATION_CONTEXT_PATH'),
staticContextPath: extractStringParam('STATIC_CONTEXT_PATH'),
showMarketingNotification: showMarketingNotification === 'true',
timeRepeatChatAnimation: Number(extractStringParam('TIME_REPEAT_ANIMATION_CHATS')),
featureWebpush: UIConfigManager.extractFeatureWebPush(),
webpushPublicKey: extractStringParam('WEBPUSH_PUBLIC_KEY'),
featureFlag1: extractStringParam('FEATURE_FLAG_1'),
featureFlag2: extractStringParam('FEATURE_FLAG_1'),
featureFlag3: extractStringParam('FEATURE_FLAG_1'),
};
}
get backendRestUrl() {
return this.config.api.restHost;
}
get spfiHostUrl() {
return this.config.api.spfiHost;
}
get spfiAuthHostUrl() {
return this.config.api.spfiAuthHost;
}
get backendWSUrl() {
return this.config.api.wsHost;
}
get needToAddTokenQueryParamForWS() {
return this.config.api.wsQueryAccessTokenEnable;
}
get contextPath() {
return this.config.contextPath;
}
get formalizationContextPath() {
return this.config.formalizationContextPath;
}
get staticContextPath() {
return this.config.staticContextPath;
}
get showMarketingNotification() {
return this.config.showMarketingNotification;
}
get timeRepeatAnimationChats() {
return this.config.timeRepeatChatAnimation;
}
get timeRepeatChatAnimation() {
return this.config.timeRepeatChatAnimation;
}
get featureWebpushEnabled() {
return this.config.featureWebpush;
}
get webpushPublicKey() {
return this.config.webpushPublicKey;
}
get featureFlag1() {
return this.config.featureFlag1;
}
get featureFlag2() {
return this.config.featureFlag2;
}
get featureFlag3() {
return this.config.featureFlag3;
}
constructor() {
this.isLegacyConfig = UIConfigManager.isLegacyAPIConfig();
this.config = this.extractVariables();
}
}
export const uiConfig: IUIConfigManager = new UIConfigManager();