Загрузка данных
import type { StorageDataType } from './StorageData';
import type { SplitLayoutState } from '@terminal/desktop/workspaces/mesh/types';
export type SystemWorkspaceConfig =
| 'currencyMarket'
| 'negotiations'
| 'bonds'
| 'monitor'
| 'monitor2'
| 'stocks'
| 'NTBStock';
export type WorkspaceType = 'Bonds' | 'Currency' | 'Talks' | 'Stocks' | 'NTBStock';
export interface Workspace {
id: number;
name: string;
isCurrent: boolean;
position: number;
isAddWidgets: boolean;
saved?: boolean;
pinned?: boolean;
isNew?: boolean;
code?: string;
favorite?: boolean;
closedAt?: string;
layout?: SplitLayoutState;
}
export interface WorkspaceState {
workspaces: Workspace[];
loadStates?: Partial<Record<Workspace['id'], WorkspaceLoadState>>;
}
export interface WorkspaceLoadState {
status: 'loading' | 'loaded' | 'error';
requestId: number | null;
error: string | null;
}
export enum WorkspaceGroups {
Currency = 'currency',
Ntb = 'ntb',
Spfi = 'spfi',
Stocks = 'stocks',
Bonds = 'bonds',
Market = 'market',
Trading = 'trading',
Cci = 'cci',
DepositCcp = 'depositCcp',
}
export type LibraryWorkspace = {
id: LibraryWorkspaceType;
name: string;
groups: WorkspaceGroups[];
};
/** Все типы столов, используемых в библиотеке шаблонов */
export type LibraryWorkspaceType =
| WorkspaceType
| 'corporateInformationCenter'
| 'spfiScreen'
| 'spfiView'
| 'spfiBroker'
| 'spfiScreenExtended'
| 'bondsLargeScreen'
| 'bondsComparison'
| 'bondsMonitoringLargeScreen'
| 'municipalBonds'
| 'corporateBonds'
| 'govermentBonds'
| 'currencyBonds'
| 'repoMidSizeScreen'
| 'ntb'
| 'ntbIndexes'
| 'ntbLogistic'
| 'dealingFunctionality'
| 'marketOverview'
| 'instrumentListsByAllMarkets'
| 'stocksAndIndexes'
| 'currencyMarket'
| 'depositsCcp';
export type WorkspaceAutoCreateConfig = {
checkPermission: (permissions: string[]) => boolean;
workspaceJsonName: LibraryWorkspaceType;
storageKey: keyof StorageDataType;
featureFlag: boolean;
};
import { createSlice } from '@reduxjs/toolkit';
import { CREATE_NEW_WORKSPACE_ID } from '@configs/appConfig';
import type { PayloadAction } from '@reduxjs/toolkit';
import type { Workspace, WorkspaceState } from 'types/Workspace';
const initialState: WorkspaceState = {
workspaces: [],
loadStates: {},
};
type WorkspaceChangePayload<T extends Exclude<keyof Workspace, 'id'>> = Pick<Workspace, 'id' | T>;
const workspacesSlice = createSlice({
name: 'workspace',
// `createSlice` will infer the state type from the `initialState` argument
initialState,
reducers: {
addWorkspace: (state, { payload }: PayloadAction<Workspace | Workspace[]>): void => {
state.workspaces = state.workspaces.filter((workspace: Workspace) => workspace.id !== -1);
if ((payload as Workspace[]).length && (payload as Workspace[]).length > 0) {
state.workspaces = [...(payload as Workspace[])];
} else {
state.workspaces.push(payload as Workspace);
}
},
removeWorkspace: (state, { payload }: PayloadAction<Workspace['id']>): void => {
state.workspaces = state.workspaces.filter((workspace: Workspace) => workspace.id !== payload);
if (state.loadStates) {
delete state.loadStates[payload];
}
},
setWorkspaceName: (state, { payload }: PayloadAction<WorkspaceChangePayload<'name'>>) => {
const changedWorkspaceIndex = state.workspaces.findIndex(({ id }) => payload.id === id);
state.workspaces[changedWorkspaceIndex].name = payload.name;
},
setCurrentWorkspace: (state, { payload }: PayloadAction<Workspace['id']>) => {
if (payload !== CREATE_NEW_WORKSPACE_ID) {
state.workspaces = state.workspaces.filter((workspace: Workspace) => workspace.id !== CREATE_NEW_WORKSPACE_ID);
}
state.workspaces = state.workspaces.map((item) => ({
...item,
isCurrent: payload === item.id,
}));
},
togglePinWorkspace: (state, { payload }: PayloadAction<WorkspaceChangePayload<never>>) => {
const changedWorkspace = state.workspaces.find(({ id }) => payload.id === id);
if (changedWorkspace) {
changedWorkspace.pinned = !changedWorkspace.pinned;
}
},
setClosedWorkspace: (state, { payload }: PayloadAction<WorkspaceChangePayload<'saved'>>) => {
const changedWorkspace = state.workspaces.find(({ id }) => payload.id === id);
if (changedWorkspace) {
changedWorkspace.saved = payload.saved;
}
},
setWorkspaces: (state, { payload }: PayloadAction<Workspace[]>) => {
state.workspaces = payload;
},
openWorkspace: (state, { payload }: PayloadAction<Workspace['id']>) => {
const changedWorkspace = state.workspaces.find(({ id }) => payload === id);
if (changedWorkspace) {
changedWorkspace.saved = true;
}
state.workspaces = state.workspaces.map((item) => ({
...item,
isCurrent: payload === item.id,
}));
},
addToFavorites: (state, { payload }: PayloadAction<Partial<WorkspaceChangePayload<'name'>>>) => {
const changedWorkspace = state.workspaces.find(({ id }) => id === payload.id);
if (changedWorkspace) {
changedWorkspace.favorite = true;
if (payload.name) {
changedWorkspace.name = payload.name;
}
}
},
removeFromFavorites: (state, { payload }: PayloadAction<Workspace['id']>) => {
const changedWorkspace = state.workspaces.find(({ id }) => payload === id);
if (changedWorkspace) {
changedWorkspace.favorite = false;
}
},
updateWorkspace: (state, { payload }: PayloadAction<Workspace>) => {
const workspaceIndex = state.workspaces.findIndex((w) => w.id === payload.id);
if (workspaceIndex !== -1) {
state.workspaces[workspaceIndex] = payload;
}
},
changeCurrentWorkspace: (state, { payload }: PayloadAction<Workspace['id']>) => {
state.workspaces = state.workspaces.map((item) => ({
...item,
isCurrent: payload === item.id,
}));
const workspace = state.workspaces.find((w) => payload === w.id);
if (workspace) {
workspace.saved = true;
}
},
updateLayout: (
state,
{ payload: { layout, id: payloadId } }: PayloadAction<Partial<WorkspaceChangePayload<'layout'>>>,
) => {
const changedWorkspace = state.workspaces.find(({ id }) => id === payloadId);
if (changedWorkspace && layout) {
changedWorkspace.layout = layout;
}
},
startWorkspaceLoading: (
state,
{ payload }: PayloadAction<{ workspaceId: Workspace['id']; requestId: number }>,
) => {
state.loadStates ??= {};
state.loadStates[payload.workspaceId] = {
status: 'loading',
requestId: payload.requestId,
error: null,
};
},
completeWorkspaceLoading: (
state,
{ payload }: PayloadAction<{ workspaceId: Workspace['id']; requestId: number }>,
) => {
const loadState = state.loadStates?.[payload.workspaceId];
if (loadState?.requestId === payload.requestId) {
loadState.status = 'loaded';
loadState.error = null;
}
},
failWorkspaceLoading: (
state,
{ payload }: PayloadAction<{ workspaceId: Workspace['id']; requestId: number; error: string }>,
) => {
const loadState = state.loadStates?.[payload.workspaceId];
if (loadState?.requestId === payload.requestId) {
loadState.status = 'error';
loadState.error = payload.error;
}
},
markWorkspaceLoaded: (state, { payload }: PayloadAction<Workspace['id']>) => {
state.loadStates ??= {};
state.loadStates[payload] = {
status: 'loaded',
requestId: null,
error: null,
};
},
},
});
export const {
addWorkspace,
removeWorkspace,
setCurrentWorkspace,
setWorkspaceName,
togglePinWorkspace,
setClosedWorkspace,
setWorkspaces,
openWorkspace,
addToFavorites,
removeFromFavorites,
updateWorkspace,
changeCurrentWorkspace,
updateLayout,
startWorkspaceLoading,
completeWorkspaceLoading,
failWorkspaceLoading,
markWorkspaceLoaded,
} = workspacesSlice.actions;
export default workspacesSlice.reducer;
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
export interface PublicContextItem {
id: number;
color?: string | null;
publicProperties?: { name: string | null; value: string | number | null }[];
name?: string | null;
workspaceId?: number;
isRemoval?: boolean;
}
interface publicContextState {
publicContext: PublicContextItem[];
}
const initialState: publicContextState = {
publicContext: [],
};
const publicContextSlice = createSlice({
name: 'publicContext',
initialState,
reducers: {
addValuesToPublicContext(state, { payload }: PayloadAction<PublicContextItem>) {
const index = state.publicContext.findIndex((item) => item.id === payload.id);
if (index === -1) {
state.publicContext.push(payload);
} else {
state.publicContext[index] = { ...state.publicContext[index], ...payload };
}
},
addValuesToPublicContextBatch(state, { payload }: PayloadAction<PublicContextItem[]>) {
const items = new Map(state.publicContext.map((item) => [item.id, item]));
payload.forEach((item) => {
items.set(item.id, { ...items.get(item.id), ...item });
});
state.publicContext = Array.from(items.values());
},
createValuesToPublicContext: (state, { payload }: PayloadAction<PublicContextItem[]>) => {
if (payload.length > 0) {
state.publicContext = [...payload];
} else {
state.publicContext = [];
}
},
},
});
export const { addValuesToPublicContext, addValuesToPublicContextBatch, createValuesToPublicContext } =
publicContextSlice.actions;
export default publicContextSlice.reducer;
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 acceptLoadedData(widgets: Widget[], publicContext: PublicContextItem[]): void {
const updateBaseline = (state: RootState): RootState => {
if (!state.widgets || !state.publicContext) {
return state;
}
const widgetIds = new Set(state.widgets.widgets.map(({ id }) => id));
const publicContextIds = new Set(state.publicContext.publicContext.map(({ id }) => id));
return {
...state,
widgets: {
...state.widgets,
widgets: [...state.widgets.widgets, ...widgets.filter(({ id }) => !widgetIds.has(id))],
},
publicContext: {
...state.publicContext,
publicContext: [
...state.publicContext.publicContext,
...publicContext.filter(({ id }) => !publicContextIds.has(id)),
],
},
};
};
this.previosStoreState = updateBaseline(this.previosStoreState);
this.prevState = updateBaseline(this.prevState);
}
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 chunk from 'lodash/chunk';
import api from '@api/index';
import { CREATE_NEW_WORKSPACE_ID, WIDGET_PROPS_REQUEST_CHUNK_SIZE } from '@configs/appConfig';
import { stateDiffController } from '@store/controllers/StateDiff';
import { addValuesToPublicContextBatch } from '@store/slices/publicContext';
import { addWidget } from '@store/slices/widgets';
import {
completeWorkspaceLoading,
failWorkspaceLoading,
startWorkspaceLoading,
} from '@store/slices/workspaces';
import { getStore } from '@store/store';
import { isRemovedWidget } from '@utils/isRemovedWidget';
import { robustRequest } from '@utils/robustRequest';
import { unpackWidgetProps } from '@utils/widgetPropsProceeders';
import { convertPercentToPx } from '@utils/widgetSizesNPositionProceeders';
import type { PublicContextItem } from '@store/slices/publicContext';
import type { AppStore } from '@store/store';
import type { Widget } from 'types/Widgets';
import type { Workspace } from 'types/Workspace';
type WorkspaceWidgetInfo = Pick<Widget, 'id' | 'name' | 'workspaceId'> &
Pick<PublicContextItem, 'color' | 'isRemoval'> & {
publicProperties?: Record<string, string> | string[];
};
interface WorkspaceData {
widgets: Widget[];
publicContext: PublicContextItem[];
}
interface PendingRequest {
requestId: number;
promise: Promise<void>;
}
const pendingRequests = new WeakMap<AppStore, Map<Workspace['id'], PendingRequest>>();
let nextRequestId = 0;
export async function readWorkspacesData(workspaceIds: Workspace['id'][]): Promise<WorkspaceData> {
const response = await robustRequest(() => api.widgetsController.read(workspaceIds));
const widgetInfo = Object.values(response.data).flat() as WorkspaceWidgetInfo[];
const widgetIds = Array.from(new Set(widgetInfo.map(({ id }) => id)));
const chunks = chunk(widgetIds, WIDGET_PROPS_REQUEST_CHUNK_SIZE);
const publicContext: PublicContextItem[] = widgetInfo.map(({ publicProperties, ...item }) => ({
...item,
publicProperties: Object.values(publicProperties ?? {}).map(
(value) => JSON.parse(value) as NonNullable<PublicContextItem['publicProperties']>[number],
),
}));
const properties = await Promise.all(
chunks.map((ids) => robustRequest(() => api.widgetPropertiesController.read(ids))),
);
const widgets = properties
.flatMap(({ data }) => data)
.filter((item) => !isRemovedWidget(item.properties))
.map((item) => {
const widget = unpackWidgetProps(convertPercentToPx(item));
if (!widget.isExpand && widget.beforeExpandParams) {
widget.beforeExpandParams = null;
}
if (!widget.widgetContentProps) {
widget.widgetContentProps = {};
}
return widget;
});
return { widgets, publicContext };
}
function isCurrentRequest(store: AppStore, workspaceId: Workspace['id'], requestId: number): boolean {
const state = store.getState();
const loadState = state.workspaces.loadStates?.[workspaceId];
return (
getStore() === store &&
state.workspaces.workspaces.some(({ id }) => id === workspaceId) &&
loadState?.status === 'loading' &&
loadState.requestId === requestId
);
}
async function loadWorkspace(store: AppStore, workspaceId: Workspace['id'], requestId: number): Promise<void> {
try {
if (!isCurrentRequest(store, workspaceId, requestId)) {
throw new Error('Workspace loading is no longer current');
}
const data = await readWorkspacesData([workspaceId]);
if (!isCurrentRequest(store, workspaceId, requestId)) {
throw new Error('Workspace loading is no longer current');
}
const state = store.getState();
const widgetIds = new Set(state.widgets.widgets.map(({ id }) => id));
const publicContextIds = new Set(state.publicContext.publicContext.map(({ id }) => id));
const widgets = data.widgets.filter(({ id }) => !widgetIds.has(id));
const publicContext = data.publicContext.filter(({ id }) => !publicContextIds.has(id));
stateDiffController.acceptLoadedData(widgets, publicContext);
if (widgets.length > 0) {
store.dispatch(addWidget(widgets));
}
if (publicContext.length > 0) {
store.dispatch(addValuesToPublicContextBatch(publicContext));
}
store.dispatch(completeWorkspaceLoading({ workspaceId, requestId }));
} catch (error) {
if (isCurrentRequest(store, workspaceId, requestId)) {
store.dispatch(
failWorkspaceLoading({
workspaceId,
requestId,
error: error instanceof Error ? error.message : 'Unable to load workspace',
}),
);
}
throw error;
}
}
export function ensureWorkspaceLoaded(workspaceId: Workspace['id']): Promise<void> {
const store = getStore();
const state = store.getState();
if (!state.workspaces.workspaces.some(({ id }) => id === workspaceId)) {
return Promise.reject(new Error('Workspace does not exist'));
}
if (workspaceId === CREATE_NEW_WORKSPACE_ID || state.workspaces.loadStates?.[workspaceId]?.status === 'loaded') {
return Promise.resolve();
}
const requests = pendingRequests.get(store) ?? new Map<Workspace['id'], PendingRequest>();
pendingRequests.set(store, requests);
const pending = requests.get(workspaceId);
const loadState = state.workspaces.loadStates?.[workspaceId];
if (pending && loadState?.status === 'loading' && loadState.requestId === pending.requestId) {
return pending.promise;
}
nextRequestId += 1;
const requestId = nextRequestId;
store.dispatch(startWorkspaceLoading({ workspaceId, requestId }));
const promise = Promise.resolve()
.then(() => loadWorkspace(store, workspaceId, requestId))
.finally(() => {
if (requests.get(workspaceId)?.requestId === requestId) {
requests.delete(workspaceId);
}
});
requests.set(workspaceId, { requestId, promise });
return promise;
}
import api from '@api/index';
import { setWidgetParentWorkspaceId } from '@store/slices/widgets';
import { addWorkspace, markWorkspaceLoaded, setCurrentWorkspace } from '@store/slices/workspaces';
import { getStore } from '@store/store';
import type { RequiredWithPartial } from 'types/utilityTypes';
import type { Workspace } from 'types/Workspace';
type CreateWorkspaceArgs = RequiredWithPartial<Omit<Workspace, 'id' | 'isCurrent' | 'position'>, 'isAddWidgets'>;
type CreateWorkspaceResult = Pick<Workspace, 'id'>;
export default async function createWorkspace(params: CreateWorkspaceArgs): Promise<CreateWorkspaceResult> {
const position = getStore().getState().workspaces.workspaces.length;
const newWorkspace: Workspace = {
...params,
id: 0,
isCurrent: true,
saved: true,
pinned: false,
position: getStore().getState().workspaces.workspaces.length,
isNew: params.isNew ?? true,
isAddWidgets: params.isAddWidgets ?? true,
};
const { workspaceController } = api;
await workspaceController
.create({
name: params.name,
position,
isCurrent: true,
isNew: params.isNew ?? true,
isAddWidgets: params.isAddWidgets ?? true,
favorite: false,
})
.then((res) => {
const { id } = res.data;
newWorkspace.id = id;
getStore().dispatch(markWorkspaceLoaded(id));
getStore().dispatch(addWorkspace(newWorkspace));
getStore().dispatch(setCurrentWorkspace(id));
const widgets = getStore()
.getState()
.widgets.widgets.filter(({ workspaceId }) => newWorkspace.id === workspaceId)
.map((widget) => ({
id: widget.id,
workspaceId: id,
}));
if (widgets.length > 0) {
getStore().dispatch(setWidgetParentWorkspaceId(widgets));
}
});
return {
id: newWorkspace.id,
};
}
import { all, call, put, select } from 'typed-redux-saga';
import api from '@api/index';
import { TRADE_PERMISSION } from '@configs/appConfig';
import { permissionsSelector } from '@store/selectors/user';
import { metricsAction } from '@store/slices/core';
import { createValuesToPublicContext } from '@store/slices/publicContext';
import { addWidget } from '@store/slices/widgets';
import { addWorkspace, markWorkspaceLoaded } from '@store/slices/workspaces';
import { readWorkspacesData } from '@utils/loadWorkspaceWidgets';
import { robustRequest } from '@utils/robustRequest';
import { MetricsAction } from 'types/MetricsActions';
import { Workspace } from 'types/Workspace';
export function* fullWorkspacesUploadSaga() {
const permissions = yield* select(permissionsSelector);
const isCanTrade = permissions.includes(TRADE_PERMISSION);
const workspacesShortInfoResponse = yield* call(() => robustRequest(api.workspaceController.read));
yield put(metricsAction(MetricsAction.LOADING_SCREEN_TIME_WORKSPACES));
const workspaces: Workspace[] = workspacesShortInfoResponse.data;
workspaces
.sort((a, b) => a.position - b.position)
.map(({ saved, pinned, code, ...workspace }) => ({
saved: saved || false,
pinned: pinned || false,
code: code ?? null,
...workspace,
}));
if (workspaces.length > 0) {
let workspacesForStore = workspaces;
if (!isCanTrade) {
workspacesForStore = workspaces.filter(({ name }) => name !== 'Переговоры');
}
yield put(addWorkspace(workspacesForStore));
} else {
return;
}
const workspacesIdList = workspaces.map(({ id }) => id);
// Получение подробной информации по виджетам
// Предположительно можно выполнить параллельно с запросом на проперти
const { widgets, publicContext } = yield* call(readWorkspacesData, workspacesIdList);
yield put(createValuesToPublicContext(publicContext));
if (widgets.length > 0) {
yield put(addWidget(widgets));
}
yield* all(workspacesIdList.map((workspaceId) => put(markWorkspaceLoaded(workspaceId))));
}