2024-01-08 05:39:47 +00:00
|
|
|
import type { FocusState } from "reducers/uiReducers/focusHistoryReducer";
|
|
|
|
|
import type { StrictEffect } from "redux-saga/effects";
|
|
|
|
|
import { call, put, select } from "redux-saga/effects";
|
|
|
|
|
import { getCurrentFocusInfo } from "selectors/focusHistorySelectors";
|
|
|
|
|
import type { FocusEntityInfo } from "navigation/FocusEntity";
|
|
|
|
|
import {
|
|
|
|
|
FocusEntity,
|
|
|
|
|
FocusStoreHierarchy,
|
|
|
|
|
identifyEntityFromPath,
|
|
|
|
|
} from "navigation/FocusEntity";
|
|
|
|
|
import type { FocusElementConfig } from "navigation/FocusElements";
|
|
|
|
|
import { FocusElementConfigType } from "navigation/FocusElements";
|
|
|
|
|
import {
|
|
|
|
|
removeFocusHistory,
|
|
|
|
|
storeFocusHistory,
|
|
|
|
|
} from "actions/focusHistoryActions";
|
|
|
|
|
import type { AppsmithLocationState } from "utils/history";
|
|
|
|
|
import type { Action } from "entities/Action";
|
2024-08-06 14:52:22 +00:00
|
|
|
import { getAction, getPlugin } from "ee/selectors/entitiesSelector";
|
2024-01-08 05:39:47 +00:00
|
|
|
import type { Plugin } from "api/PluginApi";
|
|
|
|
|
import { getCurrentGitBranch } from "selectors/gitSyncSelectors";
|
2024-08-06 14:52:22 +00:00
|
|
|
import { getIDETypeByUrl } from "ee/entities/IDE/utils";
|
|
|
|
|
import { getIDEFocusStrategy } from "ee/navigation/FocusStrategy";
|
|
|
|
|
import { IDE_TYPE } from "ee/entities/IDE/constants";
|
2024-01-08 05:39:47 +00:00
|
|
|
|
|
|
|
|
export interface FocusPath {
|
|
|
|
|
key: string;
|
|
|
|
|
entityInfo: FocusEntityInfo;
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-24 08:52:40 +00:00
|
|
|
export type FocusElementsConfigList = {
|
|
|
|
|
[key in FocusEntity]?: FocusElementConfig[];
|
|
|
|
|
};
|
|
|
|
|
|
2024-01-08 05:39:47 +00:00
|
|
|
export interface FocusStrategy {
|
2024-01-24 08:52:40 +00:00
|
|
|
focusElements: FocusElementsConfigList;
|
2024-01-08 05:39:47 +00:00
|
|
|
/** based on the route change, what states need to be set in the upcoming route **/
|
|
|
|
|
getEntitiesForSet: (
|
|
|
|
|
previousPath: string,
|
|
|
|
|
currentPath: string,
|
|
|
|
|
state: AppsmithLocationState,
|
2024-07-31 15:41:28 +00:00
|
|
|
// TODO: Fix this the next time the file is edited
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2024-01-08 05:39:47 +00:00
|
|
|
) => Generator<any, Array<FocusPath>, any>;
|
|
|
|
|
/** based on the route change, what states need to be stored for the previous route **/
|
2024-02-27 05:12:27 +00:00
|
|
|
getEntitiesForStore: (
|
|
|
|
|
path: string,
|
|
|
|
|
currentPath: string,
|
2024-07-31 15:41:28 +00:00
|
|
|
// TODO: Fix this the next time the file is edited
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2024-02-27 05:12:27 +00:00
|
|
|
) => Generator<any, Array<FocusPath>, any>;
|
2024-01-08 05:39:47 +00:00
|
|
|
/** For entities with hierarchy, return the parent entity path for storing its state **/
|
|
|
|
|
getEntityParentUrl: (
|
|
|
|
|
entityInfo: FocusEntityInfo,
|
|
|
|
|
parentEntity: FocusEntity,
|
|
|
|
|
) => string;
|
|
|
|
|
/** Define a wait (saga) before we start setting states **/
|
|
|
|
|
waitForPathLoad: (
|
|
|
|
|
currentPath: string,
|
|
|
|
|
previousPath: string,
|
2024-07-31 15:41:28 +00:00
|
|
|
// TODO: Fix this the next time the file is edited
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2024-01-08 05:39:47 +00:00
|
|
|
) => Generator<any, void, any>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Context switching works by restoring the states of ui elements to as they were
|
|
|
|
|
* the last time the user was on a particular URL.
|
|
|
|
|
*
|
|
|
|
|
* To do this, there are two simple steps
|
|
|
|
|
* 1. When leaving an url, store the ui or url states
|
|
|
|
|
* 2. When entering an url, restore stored ui or url states, or defaults
|
|
|
|
|
*
|
|
|
|
|
* @param currentPath
|
|
|
|
|
* @param previousPath
|
|
|
|
|
* @param state
|
|
|
|
|
*/
|
|
|
|
|
class FocusRetention {
|
|
|
|
|
private focusStrategy: FocusStrategy;
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
this.focusStrategy = getIDEFocusStrategy(IDE_TYPE.None);
|
|
|
|
|
this.updateFocusStrategy = this.updateFocusStrategy.bind(this);
|
|
|
|
|
this.storeStateOfPath = this.storeStateOfPath.bind(this);
|
|
|
|
|
this.setStateOfPath = this.setStateOfPath.bind(this);
|
|
|
|
|
this.getState = this.getState.bind(this);
|
|
|
|
|
this.setState = this.setState.bind(this);
|
2024-04-05 12:26:17 +00:00
|
|
|
this.handleRemoveFocusHistory = this.handleRemoveFocusHistory.bind(this);
|
2024-01-08 05:39:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public *onRouteChange(
|
|
|
|
|
currentPath: string,
|
|
|
|
|
previousPath: string,
|
|
|
|
|
state: AppsmithLocationState,
|
|
|
|
|
) {
|
|
|
|
|
this.updateFocusStrategy(currentPath);
|
2024-09-18 16:35:28 +00:00
|
|
|
|
2024-01-08 05:39:47 +00:00
|
|
|
/* STORE THE UI STATE OF PREVIOUS URL */
|
|
|
|
|
if (previousPath) {
|
|
|
|
|
const toStore: Array<FocusPath> = yield call(
|
|
|
|
|
this.focusStrategy.getEntitiesForStore,
|
|
|
|
|
previousPath,
|
2024-02-27 05:12:27 +00:00
|
|
|
currentPath,
|
2024-01-08 05:39:47 +00:00
|
|
|
);
|
2024-09-18 16:35:28 +00:00
|
|
|
|
2024-01-08 05:39:47 +00:00
|
|
|
for (const storePath of toStore) {
|
|
|
|
|
yield call(this.storeStateOfPath, storePath, previousPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-09-18 16:35:28 +00:00
|
|
|
|
2024-01-08 05:39:47 +00:00
|
|
|
/* RESTORE THE UI STATE OF THE NEW URL */
|
|
|
|
|
yield call(this.focusStrategy.waitForPathLoad, currentPath, previousPath);
|
|
|
|
|
const setPaths: Array<FocusPath> = yield call(
|
|
|
|
|
this.focusStrategy.getEntitiesForSet,
|
|
|
|
|
previousPath,
|
|
|
|
|
currentPath,
|
|
|
|
|
state,
|
|
|
|
|
);
|
2024-09-18 16:35:28 +00:00
|
|
|
|
2024-01-08 05:39:47 +00:00
|
|
|
for (const setPath of setPaths) {
|
|
|
|
|
yield call(this.setStateOfPath, setPath.key, setPath.entityInfo);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-05 12:26:17 +00:00
|
|
|
public *handleRemoveFocusHistory(url: string) {
|
2024-01-08 05:39:47 +00:00
|
|
|
const branch: string | undefined = yield select(getCurrentGitBranch);
|
|
|
|
|
const removeKeys: string[] = [];
|
2024-05-08 17:27:27 +00:00
|
|
|
const focusEntityInfo = identifyEntityFromPath(url);
|
2024-09-18 16:35:28 +00:00
|
|
|
|
2024-01-08 05:39:47 +00:00
|
|
|
removeKeys.push(`${url}#${branch}`);
|
2024-05-08 17:27:27 +00:00
|
|
|
|
|
|
|
|
const parentElement = FocusStoreHierarchy[focusEntityInfo.entity];
|
2024-09-18 16:35:28 +00:00
|
|
|
|
2024-01-08 05:39:47 +00:00
|
|
|
if (parentElement) {
|
|
|
|
|
const parentPath = this.focusStrategy.getEntityParentUrl(
|
2024-05-08 17:27:27 +00:00
|
|
|
focusEntityInfo,
|
2024-01-08 05:39:47 +00:00
|
|
|
parentElement,
|
|
|
|
|
);
|
2024-09-18 16:35:28 +00:00
|
|
|
|
2024-01-08 05:39:47 +00:00
|
|
|
removeKeys.push(`${parentPath}#${branch}`);
|
|
|
|
|
}
|
2024-09-18 16:35:28 +00:00
|
|
|
|
2024-01-08 05:39:47 +00:00
|
|
|
for (const key of removeKeys) {
|
|
|
|
|
yield put(removeFocusHistory(key));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private updateFocusStrategy(currentPath: string) {
|
|
|
|
|
const ideType = getIDETypeByUrl(currentPath);
|
2024-09-18 16:35:28 +00:00
|
|
|
|
2024-01-08 05:39:47 +00:00
|
|
|
this.focusStrategy = getIDEFocusStrategy(ideType);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected *storeStateOfPath(
|
|
|
|
|
focusPath: FocusPath,
|
|
|
|
|
fromPath: string,
|
|
|
|
|
): Generator<StrictEffect, void, FocusState | undefined> {
|
|
|
|
|
const selectors =
|
|
|
|
|
this.focusStrategy.focusElements[focusPath.entityInfo.entity];
|
2024-09-18 16:35:28 +00:00
|
|
|
|
2024-01-24 08:52:40 +00:00
|
|
|
if (!selectors) return;
|
2024-09-18 16:35:28 +00:00
|
|
|
|
2024-07-31 15:41:28 +00:00
|
|
|
// TODO: Fix this the next time the file is edited
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2024-01-08 05:39:47 +00:00
|
|
|
const state: Record<string, any> = {};
|
2024-09-18 16:35:28 +00:00
|
|
|
|
2024-01-08 05:39:47 +00:00
|
|
|
for (const selectorInfo of selectors) {
|
|
|
|
|
state[selectorInfo.name] = yield call(
|
|
|
|
|
this.getState,
|
|
|
|
|
selectorInfo,
|
|
|
|
|
fromPath,
|
|
|
|
|
);
|
2024-09-18 16:35:28 +00:00
|
|
|
|
2024-02-27 05:12:27 +00:00
|
|
|
if (selectorInfo.persist) {
|
|
|
|
|
this.persistState(
|
|
|
|
|
focusPath.key,
|
|
|
|
|
selectorInfo,
|
|
|
|
|
state[selectorInfo.name],
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-01-08 05:39:47 +00:00
|
|
|
}
|
2024-09-18 16:35:28 +00:00
|
|
|
|
2024-01-08 05:39:47 +00:00
|
|
|
yield put(
|
|
|
|
|
storeFocusHistory(focusPath.key, {
|
|
|
|
|
entityInfo: focusPath.entityInfo,
|
|
|
|
|
state,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected *setStateOfPath(key: string, entityInfo: FocusEntityInfo) {
|
|
|
|
|
const focusHistory: FocusState = yield select(getCurrentFocusInfo, key);
|
|
|
|
|
|
|
|
|
|
const selectors = this.focusStrategy.focusElements[entityInfo.entity];
|
2024-09-18 16:35:28 +00:00
|
|
|
|
2024-01-24 08:52:40 +00:00
|
|
|
if (!selectors) return;
|
2024-01-08 05:39:47 +00:00
|
|
|
|
|
|
|
|
if (focusHistory) {
|
|
|
|
|
for (const selectorInfo of selectors) {
|
|
|
|
|
yield call(
|
|
|
|
|
this.setState,
|
|
|
|
|
selectorInfo,
|
|
|
|
|
focusHistory.state[selectorInfo.name],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
const subType: string | undefined = yield call(
|
|
|
|
|
this.getEntitySubType,
|
|
|
|
|
entityInfo,
|
|
|
|
|
);
|
2024-09-18 16:35:28 +00:00
|
|
|
|
2024-01-08 05:39:47 +00:00
|
|
|
for (const selectorInfo of selectors) {
|
2024-02-27 05:12:27 +00:00
|
|
|
const { defaultValue, persist, subTypes } = selectorInfo;
|
|
|
|
|
const persistedState = this.retrievePersistState(key, selectorInfo);
|
2024-09-18 16:35:28 +00:00
|
|
|
|
2024-02-27 05:12:27 +00:00
|
|
|
if (persist && persistedState) {
|
|
|
|
|
yield call(this.setState, selectorInfo, persistedState);
|
|
|
|
|
} else if (subType && subTypes && subType in subTypes) {
|
2024-01-08 05:39:47 +00:00
|
|
|
yield call(
|
|
|
|
|
this.setState,
|
|
|
|
|
selectorInfo,
|
|
|
|
|
subTypes[subType].defaultValue,
|
|
|
|
|
);
|
|
|
|
|
} else if (defaultValue !== undefined) {
|
|
|
|
|
if (typeof defaultValue === "function") {
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
const stateDefaultValue: unknown = yield select(defaultValue);
|
2024-09-18 16:35:28 +00:00
|
|
|
|
2024-01-08 05:39:47 +00:00
|
|
|
yield call(this.setState, selectorInfo, stateDefaultValue);
|
|
|
|
|
} else {
|
|
|
|
|
yield call(this.setState, selectorInfo, defaultValue);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private *getEntitySubType(entityInfo: FocusEntityInfo) {
|
|
|
|
|
if ([FocusEntity.API, FocusEntity.QUERY].includes(entityInfo.entity)) {
|
|
|
|
|
const action: Action | undefined = yield select(getAction, entityInfo.id);
|
2024-09-18 16:35:28 +00:00
|
|
|
|
2024-01-08 05:39:47 +00:00
|
|
|
if (action) {
|
|
|
|
|
const plugin: Plugin = yield select(getPlugin, action.pluginId);
|
2024-09-18 16:35:28 +00:00
|
|
|
|
2024-01-08 05:39:47 +00:00
|
|
|
return plugin.packageName;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private *getState(config: FocusElementConfig, previousURL: string): unknown {
|
|
|
|
|
if (config.type === FocusElementConfigType.Redux) {
|
|
|
|
|
return yield select(config.selector);
|
|
|
|
|
} else if (config.type === FocusElementConfigType.URL) {
|
|
|
|
|
return config.selector(previousURL);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
private *setState(config: FocusElementConfig, value: unknown): unknown {
|
|
|
|
|
if (config.type === FocusElementConfigType.Redux) {
|
|
|
|
|
yield put(config.setter(value));
|
|
|
|
|
} else if (config.type === FocusElementConfigType.URL) {
|
|
|
|
|
config.setter(value);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-27 05:12:27 +00:00
|
|
|
|
|
|
|
|
persistState(key: string, config: FocusElementConfig, value: unknown) {
|
|
|
|
|
localStorage.setItem(
|
|
|
|
|
`FocusHistory.${key}.${config.name}`,
|
|
|
|
|
JSON.stringify(value),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
retrievePersistState(key: string, config: FocusElementConfig) {
|
|
|
|
|
const state = localStorage.getItem(`FocusHistory.${key}.${config.name}`);
|
2024-09-18 16:35:28 +00:00
|
|
|
|
2024-02-27 05:12:27 +00:00
|
|
|
if (state) {
|
|
|
|
|
return JSON.parse(state);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-08 05:39:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default new FocusRetention();
|