Загрузка данных
import isEqual from 'lodash/isEqual';
import api from '@api/index';
import { isRemovalFlag } from '@store/controllers/StateDiff/utils/addRemovalFlag';
import { PublicContextItem } from '@store/slices/publicContext';
import { packWidgetProps } from '@utils/widgetPropsProceeders';
import { convertPxToPercents } from '@utils/widgetSizesNPositionProceeders';
import { getStore } from '../../store';
import type {
ApiCallBack,
ApiController,
ApiControllerKeys,
CollapseDiffStateType,
DiffResult,
DiffType,
MainDiffResult,
PartialWidget,
PartialWorkspace,
} from './types';
import type { RootState } from '@store/store';
import type { AxiosResponse } from 'axios';
import type { Widget, WidgetPackedProps } from 'types/Widgets';
import type { Workspace } from 'types/Workspace';
class StateDiffController {
private previosStoreState: RootState = {} as RootState;
private prevState: RootState = {} as RootState;
private currentDiff: MainDiffResult[] | undefined = undefined;
private apiController: ApiController = {
workspaces: api.workspaceController,
widgets: {
delete: api.widgetsController.delete,
update: api.widgetPropertiesController.update,
},
publicContext: { update: api.widgetsController.update, delete: undefined },
};
public init = (state: RootState): this => {
this.previosStoreState = state;
this.prevState = state;
return this;
};
public async saveDiff(): Promise<void> {
if (!this.currentDiff) {
// eslint-disable-next-line no-console -- Видимо нужно для отладки
console.error('TERMINAL STATE DIFF CONTROLLER: current diff is undefined, call getDiff before call saveDiff');
return;
}
this.currentDiff.forEach((elemDiff) => {
const { result, type } = elemDiff as MainDiffResult;
if (
type === 'workspaces' &&
result.type === 'update' &&
(result.diff as PartialWorkspace[])[0].name !==
this.prevState.workspaces.workspaces.find((el) => el.id === (result.diff as PartialWorkspace[])[0].id)?.name
) {
return;
}
if (
type === 'workspaces' &&
result.type === 'update' &&
(result.diff as PartialWorkspace[])[0].isCurrent !==
this.prevState.workspaces.workspaces.find((el) => el.id === (result.diff as PartialWorkspace[])[0].id)
?.isCurrent
) {
return;
}
if (
type === 'workspaces' &&
result.type === 'update' &&
(result.diff as PartialWorkspace[])[0].saved !==
this.prevState.workspaces.workspaces.find((el) => el.id === (result.diff as PartialWorkspace[])[0].id)?.saved
) {
return;
}
if (type === 'nothing' || result.type === 'nothing') {
return;
}
const apiCallback = [elemDiff.type, elemDiff.result.type].includes('nothing')
? undefined
: this.getApiCallback(elemDiff.type as ApiControllerKeys, elemDiff.result.type as CollapseDiffStateType);
if (!apiCallback) {
return;
}
if (result.type === 'update' && type === 'widgets') {
if ('length' in result.diff) {
result.diff.forEach((item) => {
const { isRemoval, ...itemWithoutRemovalFlag } = item;
const widget = getStore().getState().widgets.widgets.filter((w) => w.id === itemWithoutRemovalFlag.id)[0];
if ('withoutSend' in widget) {
if (!widget.withoutSend) {
const packedProps = convertPxToPercents(packWidgetProps(itemWithoutRemovalFlag));
(
apiCallback as unknown as (
data: ReturnType<typeof packWidgetProps>,
) => Promise<AxiosResponse<WidgetPackedProps[]>>
)({ ...packedProps, isRemoval: !!isRemoval });
}
} else {
const packedProps = convertPxToPercents(packWidgetProps(itemWithoutRemovalFlag));
(
apiCallback as unknown as (
data: ReturnType<typeof packWidgetProps>,
) => Promise<AxiosResponse<WidgetPackedProps[]>>
)({ ...packedProps, isRemoval: !!isRemoval });
}
});
}
return;
}
if (Array.isArray(result.diff)) {
(result.diff as Workspace[] | Widget[]).forEach((item) =>
(apiCallback as (data: typeof item) => Promise<AxiosResponse<undefined>>)(item),
);
} else {
(apiCallback as (data: typeof result.diff) => Promise<AxiosResponse<undefined>>)(result.diff as Workspace);
}
});
this.currentDiff = undefined;
this.prevState = this.previosStoreState;
}
public getDiff = (state: RootState): this => {
let type: DiffType | 'nothing' = 'nothing';
const result: DiffResult = {
type: 'nothing',
diff: [],
};
if (
!this.previosStoreState ||
(this.previosStoreState.widgets.widgets.length === 0 && state.widgets.widgets.length !== 0) ||
(this.previosStoreState.publicContext.publicContext.length === 0 &&
state.publicContext.publicContext.length !== 0) ||
(this.previosStoreState.workspaces.workspaces.find((item) => item.id === -1) &&
!state.workspaces.workspaces.find((item) => item.id === -1))
) {
this.previosStoreState = state;
const res: MainDiffResult = {
type: 'nothing',
result: { type: 'nothing', diff: [] },
};
this.currentDiff = [res];
return this;
}
const stateEntries = Object.entries(state) as [keyof RootState, RootState[keyof RootState]][];
const diffProceeder: Partial<Record<keyof RootState, (s: RootState) => DiffResult>> = {
workspaces: this.getWorkspaceDiff,
widgets: this.getWidgetsDiff,
publicContext: this.getPublicContextDiff,
};
this.currentDiff = [];
for (let i = 0; i < stateEntries.length; i += 1) {
const [key, value] = stateEntries[i];
if (!isEqual(value, this.previosStoreState[key])) {
type = key;
if (diffProceeder[key]) {
const diff = (diffProceeder[key] as (s: RootState) => DiffResult)(state);
result.type = diff.type;
result.diff = diff.diff;
this.currentDiff.push({
type,
result: { ...result },
});
}
}
}
this.previosStoreState = state;
return this;
};
private getWorkspaceDiff = (state: RootState): DiffResult => {
const { workspaces } = state;
const prevWorkspaces = this.previosStoreState.workspaces;
const newItemsIds: Workspace['id'][] = [];
if (
prevWorkspaces.workspaces.filter((item) => item.id !== -1).length >
workspaces.workspaces.filter((item) => item.id !== -1).length
) {
const existId = workspaces.workspaces.map(({ id }) => id);
return {
type: 'delete',
diff: prevWorkspaces.workspaces.filter(({ id }) => id > 1).find(({ id }) => !existId.includes(id)) as Workspace,
};
}
if (prevWorkspaces.workspaces.length < workspaces.workspaces.length) {
const lastExistIds = prevWorkspaces.workspaces.map(({ id }) => id);
workspaces.workspaces
.map(({ id }) => id)
.filter((id) => !lastExistIds.includes(id))
.forEach((id) => newItemsIds.push(id));
}
const diff: PartialWorkspace[] = [];
workspaces.workspaces.forEach((item, index) => {
if (newItemsIds.includes(item.id)) {
return undefined;
}
const prevItem = prevWorkspaces.workspaces[index];
const keys = Object.keys(item).filter((key) => key !== 'id') as (keyof Workspace)[];
for (let i = 0; i < keys.length; i += 1) {
const key = keys[i];
if (item[key] !== prevItem[key] && item.id > 0) {
const diffItemIndex = diff.findIndex(({ id }) => id === item.id);
if (diffItemIndex !== -1) {
diff[diffItemIndex] = item;
} else {
diff.push(item);
}
}
}
return undefined;
});
const type = diff.length > 0 ? 'update' : 'nothing';
return {
type,
diff,
};
};
private getWidgetsDiff = (state: RootState): DiffResult => {
const { widgets: newWidgets } = state.widgets;
const prevWidgets = this.previosStoreState.widgets.widgets;
// Исключаем случай с добавлением виджета
const widgets = newWidgets.filter(({ id }) => prevWidgets.some((item) => item.id === id));
if (widgets.length < prevWidgets.length) {
const existIds = widgets.map(({ id }) => id);
return {
type: 'delete',
diff: prevWidgets.filter(({ id }) => id > 1).filter(({ id }) => !existIds.includes(id)),
};
}
const diff: PartialWidget[] = [];
widgets.forEach((widget, index) => {
const prevWidget = prevWidgets[index];
const keys = Object.keys(widget).filter((key) => key !== 'id') as Exclude<keyof Widget, 'id'>[];
for (let i = 0; i < keys.length; i += 1) {
const key = keys[i];
if (prevWidget[key] !== widget[key]) {
const diffItemIndex = diff.findIndex(({ id }) => id === widget.id);
if (diffItemIndex !== -1) {
(diff[diffItemIndex][key] as Widget[keyof Widget]) = widget[key];
} else {
const isNeedToRemoveProperties =
key === 'widgetContentProps' && isRemovalFlag(widget[key], prevWidget[key]);
diff.push({ id: widget.id, [key]: widget[key], isRemoval: isNeedToRemoveProperties });
}
}
}
});
const type = diff.length > 0 ? 'update' : 'nothing';
return {
type,
diff,
};
};
private getPublicContextDiff = (state: RootState): DiffResult => {
const { widgets } = state.widgets;
const { publicContext } = state.publicContext;
const prevPublicContext = this.previosStoreState.publicContext.publicContext;
const diff: PublicContextItem[] = [];
publicContext.forEach((item) => {
const findInd = prevPublicContext.findIndex((elem) => elem.id === item.id);
if (findInd === -1 || !isEqual(item, prevPublicContext[findInd])) {
const resultPublicProps = Array.from(item.publicProperties || []).reduce(
(acc, curr, ind) => ({ ...acc, [ind]: JSON.stringify(curr) }),
{},
);
const findWidget = widgets.find((elem) => elem.id === item.id);
diff.push({
id: item.id,
color: item.color,
name: findWidget?.name ?? '',
workspaceId: findWidget?.workspaceId ?? 0,
publicProperties: resultPublicProps,
} as PublicContextItem);
}
});
return {
type: 'update',
diff,
};
};
private getApiCallback<T extends ApiControllerKeys, R extends CollapseDiffStateType>(
mainType: T,
type: R,
): ApiCallBack<T, R> | undefined {
if (!this.apiController[mainType as keyof ApiController]) {
return undefined;
}
return this.apiController[mainType as keyof ApiController][type as CollapseDiffStateType] as ApiCallBack<T, R>;
}
}
export const stateDiffController = new StateDiffController();
import debounce from 'lodash/debounce';
import { initApiManagers } from '@api/core';
import { initModalServices } from '@modules/ModalService/initModalCore';
import { stateDiffController } from './controllers/StateDiff';
import { setupBookbuilderStore } from './bookbuilder/setupStore'; // не менять порядок импортов
import { setupStore } from './setupStore';
import type { AppStore as AppStoreTraderadar , RootState as RootStateTraderadar } from './setupStore';
export type AppStore = AppStoreTraderadar;
export type RootState = RootStateTraderadar;
let store: AppStore;
export enum STORE_TYPE {
BOOKBUILDER = 'BOOKBUILDER',
TRADERADAR = 'TRADERADAR',
}
export const initStoreForTests = (preloadedState?: Partial<RootState>) => { // инициализация стора только для тестов
store = setupStore(preloadedState);
return store;
}
export const initStore = (type: STORE_TYPE = STORE_TYPE.TRADERADAR) => {
if (!store) {
switch(type) {
case STORE_TYPE.BOOKBUILDER: {
store = setupBookbuilderStore() as AppStore;
break;
}
case STORE_TYPE.TRADERADAR: {
store = setupStore();
break;
}
default:
return;
}
stateDiffController.init(store.getState());
const saveState = debounce(() => stateDiffController.getDiff(store.getState()).saveDiff(), 200);
store.subscribe(() => {
saveState();
});
initApiManagers(store);
initModalServices(store.dispatch);
}
}
export const getStore = () => store
import React, { FC, lazy, Suspense } from 'react';
import { Provider } from 'react-redux';
import { getStore, initStore, STORE_TYPE } from '@store/store';
import { useAuthFacade } from './useAuthFacade';
import '@components/ComplexContextMenu/dropdown.scss';
import type { AuthStatusResponse } from 'types/Auth';
const Terminal = lazy(() => import('./terminal'));
type AuthProps = { initAuthData: AuthStatusResponse };
initStore(STORE_TYPE.TRADERADAR); // инициализация стора для traderadar
// Из-за большого количества ссылок на этом этапе грузится достаточно большой бандл
// TODO: отрефакторить
const AuthWithStore: FC<AuthProps> = ({ initAuthData }) => {
const isAuthenticated = useAuthFacade(initAuthData);
if (!isAuthenticated) {
return;
}
return (
<Suspense>
<Terminal />
</Suspense>
);
};
const Auth: FC<AuthProps> = (props) => (
<Provider store={getStore()}>
<AuthWithStore {...props} />
</Provider>
);
export default Auth;
import React, { FC, lazy, Suspense, useEffect } from 'react';
import { Provider } from 'react-redux';
import { Route, Routes } from 'react-router-dom';
import { getStore, initStore, STORE_TYPE } from '@store/store';
import { BookbuilderAuthErrorScreen } from '@terminal/components/BookbuilderAuthErrorScreen/BookbuilderAuthErrorScreen';
import { IframeLoadingScreen } from '@terminal/components/IframeLoadingScreen';
import { useTerminalFacade } from '@terminal/hooks/useTerminalFacade';
import { ChatUIConfigProvider } from '@widgets/NoTradeChat/context/ChatUIConfig';
import { useBookbuilderAuthFacade } from './useBookbuilderAuthFacade';
const BookbuilderTerminal = lazy(() => import('./terminal/Bookbuilder'));
const BookBuilderCreateChatTerminal = lazy(() => import('./terminal/BookBuilderCreateChat'));
initStore(STORE_TYPE.BOOKBUILDER); // инициализация стора для bookbuilder
const BookbuilderWithCriticalData: FC = () => {
const { criticalDataIsReady } = useTerminalFacade();
if (!criticalDataIsReady) {
return null;
}
return (
<ChatUIConfigProvider isIframeView>
<Routes>
<Route
path="create"
element={(
<Suspense>
<BookBuilderCreateChatTerminal />
</Suspense>)}
/>
<Route
path="*"
element={(
<Suspense>
<BookbuilderTerminal />
</Suspense>)}
/>
</Routes>
</ChatUIConfigProvider>
);
};
const BookbuilderAuthWithStore: FC = () => {
const { isReady, error } = useBookbuilderAuthFacade();
useEffect(() => {
document.documentElement.dataset.theme = 'bookBilder';
}, []);
if (error) {
return <BookbuilderAuthErrorScreen />;
}
if (!isReady) {
return <IframeLoadingScreen />;
}
return <BookbuilderWithCriticalData />;
};
const BookbuilderAuth: FC = () => (
<Provider store={getStore()}>
<BookbuilderAuthWithStore />
</Provider>
);
export default BookbuilderAuth;