Store value improvements (#3663)

This commit is contained in:
Hetu Nandu 2021-03-24 10:39:47 +05:30 committed by GitHub
parent 56f22edbe8
commit 9a4c317f20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 308 additions and 47 deletions

View File

@ -94,7 +94,7 @@ jobs:
- name: Run the jest tests
if: github.event_name == 'pull_request'
uses: hetunandu/Jest-Coverage-Diff@feature/improve-comment-ui
uses: hetunandu/Jest-Coverage-Diff@fix/new-delete-file
with:
fullCoverageDiff: false
runCommand: cd app/client && REACT_APP_ENVIRONMENT=${{steps.vars.outputs.REACT_APP_ENVIRONMENT}} yarn run test:unit

View File

@ -259,11 +259,18 @@ export const setAppMode = (payload: APP_MODE): ReduxAction<APP_MODE> => {
};
};
export const updateAppStore = (
export const updateAppTransientStore = (
payload: Record<string, unknown>,
): ReduxAction<Record<string, unknown>> => ({
type: ReduxActionTypes.UPDATE_APP_TRANSIENT_STORE,
payload,
});
export const updateAppPersistentStore = (
payload: Record<string, unknown>,
): ReduxAction<Record<string, unknown>> => {
return {
type: ReduxActionTypes.UPDATE_APP_STORE,
type: ReduxActionTypes.UPDATE_APP_PERSISTENT_STORE,
payload,
};
};

View File

@ -223,7 +223,6 @@ export default function Dropdown(props: DropdownProps) {
},
[onSelect],
);
console.log({ height: props.height });
return (
<DropdownContainer
tabIndex={0}

View File

@ -10,7 +10,7 @@ const APP_STORE_NAMESPACE = "APPSMITH_LOCAL_STORE";
export const getAppStoreName = (appId: string) =>
`${APP_STORE_NAMESPACE}-${appId}`;
export const getAppStore = (appId: string) => {
export const getPersistentAppStore = (appId: string) => {
const appStoreName = getAppStoreName(appId);
let storeString = "{}";
// Check if localStorage exists

View File

@ -318,7 +318,8 @@ export const ReduxActionTypes: { [key: string]: string } = {
SET_APP_MODE: "SET_APP_MODE",
TOGGLE_PROPERTY_PANE_WIDGET_NAME_EDIT:
"TOGGLE_PROPERTY_PANE_WIDGET_NAME_EDIT",
UPDATE_APP_STORE: "UPDATE_APP_STORE",
UPDATE_APP_PERSISTENT_STORE: "UPDATE_APP_PERSISTENT_STORE",
UPDATE_APP_TRANSIENT_STORE: "UPDATE_APP_TRANSIENT_STORE",
SET_ACTION_TO_EXECUTE_ON_PAGELOAD: "SET_ACTION_TO_EXECUTE_ON_PAGELOAD",
TOGGLE_ACTION_EXECUTE_ON_LOAD_SUCCESS:
"TOGGLE_ACTION_EXECUTE_ON_LOAD_SUCCESS",

View File

@ -59,7 +59,7 @@ export interface DataTreeWidget extends WidgetProps {
ENTITY_TYPE: ENTITY_TYPE.WIDGET;
}
export interface DataTreeAppsmith extends AppDataState {
export interface DataTreeAppsmith extends Omit<AppDataState, "store"> {
ENTITY_TYPE: ENTITY_TYPE.APPSMITH;
store: Record<string, unknown>;
}
@ -192,7 +192,12 @@ export class DataTreeFactory {
});
dataTree.pageList = pageList;
dataTree.appsmith = { ...appData } as DataTreeAppsmith;
dataTree.appsmith = {
...appData,
// combine both persistent and transient state with the transient state
// taking precedence in case the key is the same
store: { ...appData.store.persistent, ...appData.store.transient },
} as DataTreeAppsmith;
(dataTree.appsmith as DataTreeAppsmith).ENTITY_TYPE = ENTITY_TYPE.APPSMITH;
return dataTree;
}

View File

@ -24,11 +24,16 @@ export type UrlDataState = {
fullPath: string;
};
export type AppStoreState = {
transient: Record<string, unknown>;
persistent: Record<string, unknown>;
};
export type AppDataState = {
mode?: APP_MODE;
user: AuthUserState;
URL: UrlDataState;
store: Record<string, unknown>;
store: AppStoreState;
};
const initialState: AppDataState = {
@ -47,7 +52,10 @@ const initialState: AppDataState = {
hash: "",
fullPath: "",
},
store: {},
store: {
transient: {},
persistent: {},
},
};
const appReducer = createReducer(initialState, {
@ -78,13 +86,28 @@ const appReducer = createReducer(initialState, {
URL: action.payload,
};
},
[ReduxActionTypes.UPDATE_APP_STORE]: (
[ReduxActionTypes.UPDATE_APP_TRANSIENT_STORE]: (
state: AppDataState,
action: ReduxAction<Record<string, unknown>>,
) => {
return {
...state,
store: action.payload,
store: {
...state.store,
transient: action.payload,
},
};
},
[ReduxActionTypes.UPDATE_APP_PERSISTENT_STORE]: (
state: AppDataState,
action: ReduxAction<Record<string, unknown>>,
) => {
return {
...state,
store: {
...state.store,
persistent: action.payload,
},
};
},
});

View File

@ -22,7 +22,6 @@ import {
takeEvery,
takeLatest,
} from "redux-saga/effects";
import { getDynamicBindings, isDynamicValue } from "utils/DynamicBindingUtils";
import {
ActionDescription,
RunActionPayload,
@ -57,6 +56,7 @@ import ActionAPI, {
} from "api/ActionAPI";
import {
getAction,
getAppStoreData,
getCurrentPageNameByActionId,
isActionDirty,
isActionSaving,
@ -67,7 +67,10 @@ import { validateResponse } from "sagas/ErrorSagas";
import { TypeOptions } from "react-toastify";
import { PLUGIN_TYPE_API } from "constants/ApiEditorConstants";
import { DEFAULT_EXECUTE_ACTION_TIMEOUT_MS } from "constants/ApiConstants";
import { updateAppStore } from "actions/pageActions";
import {
updateAppPersistentStore,
updateAppTransientStore,
} from "actions/pageActions";
import { getAppStoreName } from "constants/AppConstants";
import downloadjs from "downloadjs";
import { getType, Types } from "utils/TypeHelpers";
@ -94,7 +97,7 @@ import {
ERROR_FAIL_ON_PAGE_LOAD_ACTIONS,
ERROR_WIDGET_DOWNLOAD,
} from "constants/messages";
import { EMPTY_RESPONSE } from "../components/editorComponents/ApiResponseView";
import { EMPTY_RESPONSE } from "components/editorComponents/ApiResponseView";
import localStorage from "utils/localStorage";
import { getWidgetByName } from "./selectors";
@ -175,18 +178,32 @@ function* navigateActionSaga(
}
function* storeValueLocally(
action: { key: string; value: string },
action: { key: string; value: string; persist: boolean },
event: ExecuteActionPayloadEvent,
) {
try {
const appId = yield select(getCurrentApplicationId);
const appStoreName = getAppStoreName(appId);
const existingStore = yield localStorage.getItem(appStoreName) || "{}";
const storeObj = JSON.parse(existingStore);
storeObj[action.key] = action.value;
const storeString = JSON.stringify(storeObj);
yield localStorage.setItem(appStoreName, storeString);
yield put(updateAppStore(storeObj));
if (action.persist) {
const appId = yield select(getCurrentApplicationId);
const appStoreName = getAppStoreName(appId);
const existingStore = localStorage.getItem(appStoreName) || "{}";
const parsedStore = JSON.parse(existingStore);
parsedStore[action.key] = action.value;
const storeString = JSON.stringify(parsedStore);
yield localStorage.setItem(appStoreName, storeString);
yield put(updateAppPersistentStore(parsedStore));
} else {
const existingStore = yield select(getAppStoreData);
const newTransientStore = {
...existingStore.transient,
[action.key]: action.value,
};
yield put(updateAppTransientStore(newTransientStore));
}
// Wait for an evaluation before completing this trigger effect
// This makes this trigger work in sync and not trigger
// another effect till the values are reflected in
// the dataTree
yield take(ReduxActionTypes.SET_EVALUATED_TREE);
if (event.callback) event.callback({ success: true });
} catch (err) {
if (event.callback) event.callback({ success: false });
@ -389,18 +406,6 @@ export function* evaluateActionParams(
return mapToPropList(actionParams);
}
export function extractBindingsFromAction(action: Action) {
const bindings: string[] = [];
action.dynamicBindingPathList.forEach((a) => {
const value = _.get(action, a.key);
if (isDynamicValue(value)) {
const { jsSnippets } = getDynamicBindings(value);
bindings.push(...jsSnippets.filter((jsSnippet) => !!jsSnippet));
}
});
return bindings;
}
export function* executeActionSaga(
apiAction: RunActionPayload,
event: ExecuteActionPayloadEvent,

View File

@ -246,7 +246,8 @@ const EVALUATE_REDUX_ACTIONS = [
// App Data
ReduxActionTypes.SET_APP_MODE,
ReduxActionTypes.FETCH_USER_DETAILS_SUCCESS,
ReduxActionTypes.UPDATE_APP_STORE,
ReduxActionTypes.UPDATE_APP_PERSISTENT_STORE,
ReduxActionTypes.UPDATE_APP_TRANSIENT_STORE,
// Widgets
ReduxActionTypes.UPDATE_LAYOUT,
ReduxActionTypes.UPDATE_WIDGET_PROPERTY,

View File

@ -22,7 +22,7 @@ import {
fetchPageList,
fetchPublishedPage,
setAppMode,
updateAppStore,
updateAppPersistentStore,
} from "actions/pageActions";
import { fetchDatasources } from "actions/datasourceActions";
import { fetchPlugins } from "actions/pluginActions";
@ -31,7 +31,7 @@ import { fetchApplication } from "actions/applicationActions";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { getCurrentApplication } from "selectors/applicationSelectors";
import { APP_MODE } from "reducers/entityReducers/appReducer";
import { getAppStore } from "constants/AppConstants";
import { getPersistentAppStore } from "constants/AppConstants";
import { getDefaultPageId } from "./selectors";
import { populatePageDSLsSaga } from "./PageSagas";
import log from "loglevel";
@ -48,6 +48,7 @@ function* initializeEditorSaga(
const { applicationId, pageId } = initializeEditorAction.payload;
try {
yield put(setAppMode(APP_MODE.EDIT));
yield put(updateAppPersistentStore(getPersistentAppStore(applicationId)));
yield put({ type: ReduxActionTypes.START_EVALUATION });
yield all([
put(fetchPageList(applicationId, APP_MODE.EDIT)),
@ -115,8 +116,6 @@ function* initializeEditorSaga(
return;
}
yield put(updateAppStore(getAppStore(applicationId)));
const currentApplication = yield select(getCurrentApplication);
const appName = currentApplication ? currentApplication.name : "";
@ -150,6 +149,7 @@ export function* initializeAppViewerSaga(
) {
const { applicationId, pageId } = action.payload;
yield put(setAppMode(APP_MODE.PUBLISHED));
yield put(updateAppPersistentStore(getPersistentAppStore(applicationId)));
yield put({ type: ReduxActionTypes.START_EVALUATION });
yield all([
// TODO (hetu) Remove spl view call for fetch actions
@ -185,7 +185,6 @@ export function* initializeAppViewerSaga(
return;
}
yield put(updateAppStore(getAppStore(applicationId)));
const defaultPageId = yield select(getDefaultPageId);
const toLoadPageId = pageId || defaultPageId;
@ -212,7 +211,6 @@ export function* initializeAppViewerSaga(
}
yield put(setAppMode(APP_MODE.PUBLISHED));
yield put(updateAppStore(getAppStore(applicationId)));
yield put({
type: ReduxActionTypes.INITIALIZE_PAGE_VIEWER_SUCCESS,

View File

@ -338,7 +338,7 @@ export function* logoutSaga() {
if (isValidResponse) {
AnalyticsUtil.reset();
yield put(logoutUserSuccess());
localStorage.removeItem("THEME");
localStorage.clear();
yield put(flushErrorsAndRedirect(AUTH_LOGIN_URL));
}
} catch (error) {

View File

@ -12,6 +12,7 @@ import { find } from "lodash";
import ImageAlt from "assets/images/placeholder-image.svg";
import { CanvasWidgetsReduxState } from "../reducers/entityReducers/canvasWidgetsReducer";
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
import { AppStoreState } from "reducers/entityReducers/appReducer";
export const getEntities = (state: AppState): AppState["entities"] =>
state.entities;
@ -282,6 +283,9 @@ export const isActionDirty = (id: string) =>
export const getAppData = (state: AppState) => state.entities.app;
export const getAppStoreData = (state: AppState): AppStoreState =>
state.entities.app.store;
export const getCanvasWidgets = (state: AppState): CanvasWidgetsReduxState =>
state.entities.canvasWidgets;

View File

@ -0,0 +1,29 @@
import myLocalStorage from "utils/localStorage";
describe("local storage", () => {
it("calls getItem", () => {
jest.spyOn(window.localStorage.__proto__, "getItem");
window.localStorage.__proto__.getItem = jest.fn();
myLocalStorage.getItem("myTestKey");
expect(localStorage.getItem).toBeCalledWith("myTestKey");
});
it("calls setItem", () => {
jest.spyOn(window.localStorage.__proto__, "setItem");
window.localStorage.__proto__.setItem = jest.fn();
myLocalStorage.setItem("myTestKey", "testValue");
expect(localStorage.setItem).toBeCalledWith("myTestKey", "testValue");
});
it("calls removeItem", () => {
jest.spyOn(window.localStorage.__proto__, "removeItem");
window.localStorage.__proto__.removeItem = jest.fn();
myLocalStorage.removeItem("myTestKey");
expect(localStorage.removeItem).toBeCalledWith("myTestKey");
});
it("calls clear", () => {
jest.spyOn(window.localStorage.__proto__, "clear");
window.localStorage.__proto__.clear = jest.fn();
myLocalStorage.clear();
expect(localStorage.clear).toBeCalled();
});
});

View File

@ -52,6 +52,14 @@ const getLocalStorage = () => {
}
};
const clear = () => {
try {
storage.clear();
} catch (e) {
handleError(e);
}
};
const isSupported = () => !!window.localStorage;
return {
@ -59,6 +67,7 @@ const getLocalStorage = () => {
setItem,
removeItem,
isSupported,
clear,
};
};

View File

@ -1,4 +1,6 @@
import { getAllPaths } from "./evaluationUtils";
import { addFunctions, getAllPaths } from "./evaluationUtils";
import { DataTree, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
import { PluginType } from "entities/Action";
describe("getAllPaths", () => {
it("getsAllPaths", () => {
@ -39,3 +41,177 @@ describe("getAllPaths", () => {
expect(actual).toStrictEqual(result);
});
});
describe("Add functions", () => {
it("adds functions correctly", () => {
const dataTree: DataTree = {
action1: {
actionId: "123",
data: {},
config: {},
pluginType: PluginType.API,
dynamicBindingPathList: [],
name: "action1",
bindingPaths: {},
isLoading: false,
run: {},
ENTITY_TYPE: ENTITY_TYPE.ACTION,
},
};
const dataTreeWithFunctions = addFunctions(dataTree);
expect(dataTreeWithFunctions.actionPaths).toStrictEqual([
"action1.run",
"navigateTo",
"showAlert",
"showModal",
"closeModal",
"storeValue",
"download",
"copyToClipboard",
"resetWidget",
]);
// Action run
const onSuccess = "() => {successRun()}";
const onError = "() => {failureRun()}";
const actionParams = "{ param1: value1 }";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const actionRunResponse = dataTreeWithFunctions.action1.run(
onSuccess,
onError,
actionParams,
);
expect(actionRunResponse).toStrictEqual({
type: "RUN_ACTION",
payload: {
actionId: "123",
onSuccess: `{{${onSuccess}}}`,
onError: `{{${onError}}}`,
params: actionParams,
},
});
// Navigate To
const pageNameOrUrl = "www.google.com";
const params = "{ param1: value1 }";
const target = "NEW_WINDOW";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const navigateToResponse = dataTreeWithFunctions.navigateTo(
pageNameOrUrl,
params,
target,
);
expect(navigateToResponse).toStrictEqual({
type: "NAVIGATE_TO",
payload: {
pageNameOrUrl,
params,
target,
},
});
// Show alert
const message = "Alert message";
const style = "info";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const showAlertResponse = dataTreeWithFunctions.showAlert(message, style);
expect(showAlertResponse).toStrictEqual({
type: "SHOW_ALERT",
payload: {
message,
style,
},
});
// Show Modal
const modalName = "Modal 1";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const showModalResponse = dataTreeWithFunctions.showModal(modalName);
expect(showModalResponse).toStrictEqual({
type: "SHOW_MODAL_BY_NAME",
payload: {
modalName,
},
});
// Close Modal
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const closeModalResponse = dataTreeWithFunctions.closeModal(modalName);
expect(closeModalResponse).toStrictEqual({
type: "CLOSE_MODAL",
payload: {
modalName,
},
});
// Store value
const key = "some";
const value = "thing";
const persist = false;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const storeValueResponse = dataTreeWithFunctions.storeValue(
key,
value,
persist,
);
expect(storeValueResponse).toStrictEqual({
type: "STORE_VALUE",
payload: {
key,
value,
persist,
},
});
// Download
const data = "file";
const name = "downloadedFile.txt";
const type = "text";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const downloadResponse = dataTreeWithFunctions.download(data, name, type);
expect(downloadResponse).toStrictEqual({
type: "DOWNLOAD",
payload: {
data,
name,
type,
},
});
// copy to clipboard
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const copyToClipboardResponse = dataTreeWithFunctions.copyToClipboard(data);
expect(copyToClipboardResponse).toStrictEqual({
type: "COPY_TO_CLIPBOARD",
payload: {
data,
options: { debug: undefined, format: undefined },
},
});
// reset widget
const widgetName = "widget1";
const resetChildren = true;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const resetWidgetResponse = dataTreeWithFunctions.resetWidget(
widgetName,
resetChildren,
);
expect(resetWidgetResponse).toStrictEqual({
type: "RESET_WIDGET_META_RECURSIVE_BY_NAME",
payload: {
widgetName,
resetChildren,
},
});
});
});

View File

@ -424,10 +424,14 @@ export const addFunctions = (dataTree: Readonly<DataTree>): DataTree => {
};
withFunction.actionPaths.push("closeModal");
withFunction.storeValue = function(key: string, value: string) {
withFunction.storeValue = function(
key: string,
value: string,
persist = true,
) {
return {
type: "STORE_VALUE",
payload: { key, value },
payload: { key, value, persist },
};
};
withFunction.actionPaths.push("storeValue");