diff --git a/package.json b/package.json
index 074f30f22..44765a650 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"exif-js": "^2.3.0",
"file-type": "^14.6.2",
"jszip": "^3.5.0",
+ "localforage": "^1.9.0",
"lodash": "^4.17.20",
"ol": "^5.3.3",
"path-to-regexp": "^6.2.0",
diff --git a/src/common/webStorage.ts b/src/common/webStorage.ts
new file mode 100644
index 000000000..4569cf39c
--- /dev/null
+++ b/src/common/webStorage.ts
@@ -0,0 +1,53 @@
+import * as localforage from "localforage";
+import { toast } from "react-toastify";
+
+class WebStorage {
+ private isStorageReady: boolean = false;
+
+ constructor() {
+ localforage.config({
+ name: "FOTT-webStorage",
+ driver: [localforage.INDEXEDDB, localforage.LOCALSTORAGE],
+ });
+ }
+
+ private async waitForStorageReady() {
+ if (!this.isStorageReady) {
+ try {
+ await localforage.ready();
+ this.isStorageReady = true;
+ } catch (error) {
+ toast.error(`No usable storage driver is found.`);
+ }
+ }
+ }
+
+ public async getItem(key: string) {
+ try {
+ await this.waitForStorageReady();
+ return await localforage.getItem(key);
+ } catch (err) {
+ toast.error(`WebStorage getItem("${key}") error: ${err}`);
+ }
+ }
+
+ public async setItem(key: string, value: any) {
+ try {
+ await this.waitForStorageReady();
+ await localforage.setItem(key, value);
+ } catch (err) {
+ toast.error(`WebStorage setItem("${key}") error: ${err}`);
+ }
+ }
+
+ public async removeItem(key: string) {
+ try {
+ await this.waitForStorageReady();
+ await localforage.removeItem(key);
+ } catch (err) {
+ toast.error(`WebStorage removeItem("${key}") error: ${err}`);
+ }
+ }
+}
+
+export const webStorage = new WebStorage();
diff --git a/src/index.tsx b/src/index.tsx
index 62e240d27..a967198ba 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -16,36 +16,38 @@ import { registerIcons } from "./registerIcons";
import registerProviders from "./registerProviders";
import registerMixins from "./registerMixins";
-registerIcons();
-registerMixins();
-registerProviders();
-const defaultState: IApplicationState = initialState;
-const store = createReduxStore(defaultState, true);
+(async () => {
+ registerIcons();
+ registerMixins();
+ registerProviders();
+ const defaultState: IApplicationState = initialState;
+ const store = await createReduxStore(defaultState, true);
-let noFocusOutline = true;
-document.body.classList.add("no-focus-outline");
+ let noFocusOutline = true;
+ document.body.classList.add("no-focus-outline");
-document.body.addEventListener("mousedown", () => {
- if (!noFocusOutline) {
- noFocusOutline = true;
- document.body.classList.add("no-focus-outline");
- }
-});
+ document.body.addEventListener("mousedown", () => {
+ if (!noFocusOutline) {
+ noFocusOutline = true;
+ document.body.classList.add("no-focus-outline");
+ }
+ });
-document.body.addEventListener("keydown", (event) => {
- if (event.keyCode === 9 && noFocusOutline) {
- noFocusOutline = false;
- document.body.classList.remove("no-focus-outline");
- }
-});
+ document.body.addEventListener("keydown", (event) => {
+ if (event.keyCode === 9 && noFocusOutline) {
+ noFocusOutline = false;
+ document.body.classList.remove("no-focus-outline");
+ }
+ });
-ReactDOM.render(
-
-
-
- , document.getElementById("rootdiv"));
+ ReactDOM.render(
+
+
+
+ , document.getElementById("rootdiv"));
-// If you want your app to work offline and load faster, you can change
-// unregister() to register() below. Note this comes with some pitfalls.
-// Learn more about service workers: https://bit.ly/CRA-PWA
-serviceWorker.unregister();
+ // If you want your app to work offline and load faster, you can change
+ // unregister() to register() below. Note this comes with some pitfalls.
+ // Learn more about service workers: https://bit.ly/CRA-PWA
+ serviceWorker.unregister();
+})();
diff --git a/src/react/components/pages/appSettings/appSettingsPage.test.tsx b/src/react/components/pages/appSettings/appSettingsPage.test.tsx
index 5e8c75fa2..79257252d 100644
--- a/src/react/components/pages/appSettings/appSettingsPage.test.tsx
+++ b/src/react/components/pages/appSettings/appSettingsPage.test.tsx
@@ -35,8 +35,8 @@ describe("App Settings Page", () => {
toast.success = jest.fn(() => 2);
});
- it("renders correctly", () => {
- const store = createStore();
+ it("renders correctly", async () => {
+ const store = await createStore();
const wrapper = createComponent(store);
expect(wrapper.find(AppSettingsForm).length).toEqual(1);
expect(wrapper.find("button#toggleDevTools").length).toEqual(1);
@@ -45,7 +45,7 @@ describe("App Settings Page", () => {
it("Saves app settings when click on save button", async () => {
const appSettings = MockFactory.appSettings();
- const store = createStore(appSettings);
+ const store = await createStore(appSettings);
const props = createProps();
const saveAppSettingsSpy = jest.spyOn(props.actions, "saveAppSettings");
const goBackSpy = jest.spyOn(props.history, "goBack");
@@ -59,8 +59,8 @@ describe("App Settings Page", () => {
expect(goBackSpy).toBeCalled();
});
- it("Navigates the user back to the previous page on cancel", () => {
- const store = createStore();
+ it("Navigates the user back to the previous page on cancel", async () => {
+ const store = await createStore();
const props = createProps();
const goBackSpy = jest.spyOn(props.history, "goBack");
@@ -106,7 +106,7 @@ describe("App Settings Page", () => {
};
}
- function createStore(appSettings: IAppSettings = null): Store {
+ async function createStore(appSettings: IAppSettings = null): Promise> {
const initialState: IApplicationState = {
currentProject: null,
appSettings: appSettings || MockFactory.appSettings(),
@@ -114,6 +114,6 @@ describe("App Settings Page", () => {
recentProjects: [],
};
- return createReduxStore(initialState);
+ return await createReduxStore(initialState);
}
});
diff --git a/src/react/components/pages/connections/connectionsPage.test.tsx b/src/react/components/pages/connections/connectionsPage.test.tsx
index aa3fc874b..1fc1227d9 100644
--- a/src/react/components/pages/connections/connectionsPage.test.tsx
+++ b/src/react/components/pages/connections/connectionsPage.test.tsx
@@ -320,6 +320,6 @@ function createProps(route: string): IConnectionPageProps {
};
}
-function createStore(state?: IApplicationState): Store {
- return createReduxStore(state);
+async function createStore(state?: IApplicationState): Promise> {
+ return await createReduxStore(state);
}
diff --git a/src/react/components/pages/editorPage/editorPage.test.tsx b/src/react/components/pages/editorPage/editorPage.test.tsx
index b168f13d7..901fa9be3 100644
--- a/src/react/components/pages/editorPage/editorPage.test.tsx
+++ b/src/react/components/pages/editorPage/editorPage.test.tsx
@@ -529,7 +529,7 @@ describe("Editor Page Component", () => {
});
});
-function createStore(project: IProject, setCurrentProject: boolean = false): Store {
+async function createStore(project: IProject, setCurrentProject: boolean = false): Promise> {
const initialState: IApplicationState = {
currentProject: setCurrentProject ? project : null,
appSettings: MockFactory.appSettings(),
@@ -537,7 +537,7 @@ function createStore(project: IProject, setCurrentProject: boolean = false): Sto
recentProjects: [project],
};
- return createReduxStore(initialState);
+ return await createReduxStore(initialState);
}
async function waitForSelectedAsset(wrapper: ReactWrapper) {
diff --git a/src/react/components/pages/homepage/homePage.test.tsx b/src/react/components/pages/homepage/homePage.test.tsx
index b9e298e16..8e13a44f2 100644
--- a/src/react/components/pages/homepage/homePage.test.tsx
+++ b/src/react/components/pages/homepage/homePage.test.tsx
@@ -53,12 +53,12 @@ describe("Homepage Component", () => {
toast.info = jest.fn(() => 2);
});
- beforeEach(() => {
+ beforeEach(async () => {
const projectServiceMock = ProjectService as jest.Mocked;
projectServiceMock.prototype.load = jest.fn((project) => Promise.resolve(project));
projectServiceMock.prototype.delete = jest.fn(() => Promise.resolve());
- store = createStore(recentProjects);
+ store = await createStore(recentProjects);
props = createProps();
deleteProjectSpy = jest.spyOn(props.actions, "deleteProject");
closeProjectSpy = jest.spyOn(props.actions, "closeProject");
@@ -174,7 +174,7 @@ describe("Homepage Component", () => {
};
}
- function createStore(recentProjects: IProject[]): Store {
+ async function createStore(recentProjects: IProject[]): Promise> {
const initialState: IApplicationState = {
currentProject: null,
appSettings: MockFactory.appSettings(),
@@ -183,6 +183,6 @@ describe("Homepage Component", () => {
appError: null,
};
- return createReduxStore(initialState);
+ return await createReduxStore(initialState);
}
});
diff --git a/src/react/components/pages/projectSettings/projectSettingsPage.test.tsx b/src/react/components/pages/projectSettings/projectSettingsPage.test.tsx
index ffca0e18a..ecbd70894 100644
--- a/src/react/components/pages/projectSettings/projectSettingsPage.test.tsx
+++ b/src/react/components/pages/projectSettings/projectSettingsPage.test.tsx
@@ -5,6 +5,7 @@ import { mount, ReactWrapper } from "enzyme";
import React from "react";
import { Provider } from "react-redux";
import { BrowserRouter as Router } from "react-router-dom";
+import localforage from "localforage";
import MockFactory from "../../../../common/mockFactory";
import createReduxStore from "../../../../redux/store/store";
import ProjectSettingsPage, { IProjectSettingsPageProps, IProjectSettingsPageState } from "./projectSettingsPage";
@@ -134,7 +135,7 @@ describe("Project settings page", () => {
securityToken: `${project.name} Token`,
});
- expect(localStorage.removeItem).toBeCalledWith("projectForm");
+ expect(localforage.removeItem).toBeCalledWith("projectForm");
});
describe("project does not exists", () => {
@@ -181,7 +182,7 @@ describe("Project settings page", () => {
.find(ProjectSettingsPage)
.childAt(0) as ReactWrapper;
- expect(localStorage.getItem).toBeCalledWith("projectForm");
+ expect(localforage.getItem).toBeCalledWith("projectForm");
expect(projectSettingsPage.state().project).toEqual(partialProject);
});
@@ -195,7 +196,7 @@ describe("Project settings page", () => {
const projectForm = wrapper.find(ProjectForm) as ReactWrapper;
projectForm.props().onChange(partialProject);
- expect(localStorage.setItem).toBeCalledWith("projectForm", JSON.stringify(partialProject));
+ expect(localforage.setItem).toBeCalledWith("projectForm", JSON.stringify(partialProject));
});
it("Does NOT store empty project in local storage", () => {
@@ -209,7 +210,7 @@ describe("Project settings page", () => {
const projectForm = wrapper.find(ProjectForm) as ReactWrapper;
projectForm.props().onChange(emptyProject);
- expect(localStorage.setItem).not.toBeCalled();
+ expect(localforage.setItem).not.toBeCalled();
});
});
});
diff --git a/src/react/components/pages/projectSettings/projectSettingsPage.tsx b/src/react/components/pages/projectSettings/projectSettingsPage.tsx
index d4e8337fe..06b46e55c 100644
--- a/src/react/components/pages/projectSettings/projectSettingsPage.tsx
+++ b/src/react/components/pages/projectSettings/projectSettingsPage.tsx
@@ -161,8 +161,8 @@ export default class ProjectSettingsPage extends React.Component {
- const projectJson = getStorageItem(constants.projectFormTempKey);
+ private newProjectSetting = async (): Promise => {
+ const projectJson = await getStorageItem(constants.projectFormTempKey);
if (projectJson) {
this.setState({ project: JSON.parse(projectJson) });
}
diff --git a/src/react/components/pages/train/trainPage.tsx b/src/react/components/pages/train/trainPage.tsx
index 14bfd6e84..61eebd4d0 100644
--- a/src/react/components/pages/train/trainPage.tsx
+++ b/src/react/components/pages/train/trainPage.tsx
@@ -29,6 +29,7 @@ import TrainPanel from "./trainPanel";
import {ITrainRecordProps} from "./trainRecord";
import TrainTable from "./trainTable";
import { getAPIVersion } from "../../../../common/utils";
+import { webStorage } from "../../../../common/webStorage";
export interface ITrainPageProps extends RouteComponentProps, React.Props {
connections: IConnection[];
@@ -258,11 +259,11 @@ export default class TrainPage extends React.Component {
- const storedTrainInputs = JSON.parse(window.localStorage.getItem("trainPage_inputs"));
+ private checkAndUpdateInputsInLocalStorage = async (projectId: string) => {
+ const storedTrainInputs = JSON.parse(await webStorage.getItem("trainPage_inputs") as string);
if (storedTrainInputs?.id !== projectId) {
- window.localStorage.removeItem("trainPage_inputs");
+ await webStorage.removeItem("trainPage_inputs");
UseLocalStorage.setItem("trainPage_inputs", "projectId", projectId);
}
@@ -339,8 +340,8 @@ export default class TrainPage extends React.Component {
this.setState({
isTraining: false,
diff --git a/src/react/components/shell/mainContentRouter.test.tsx b/src/react/components/shell/mainContentRouter.test.tsx
index 8825ea53b..3a7069fb8 100644
--- a/src/react/components/shell/mainContentRouter.test.tsx
+++ b/src/react/components/shell/mainContentRouter.test.tsx
@@ -55,6 +55,6 @@ describe("Main Content Router", () => {
});
});
-function createStore(state?: IApplicationState): Store {
- return createReduxStore(state);
+async function createStore(state?: IApplicationState): Promise> {
+ return await createReduxStore(state);
}
diff --git a/src/redux/actions/connectionActions.ts b/src/redux/actions/connectionActions.ts
index 69ffd32a3..3fd494be0 100644
--- a/src/redux/actions/connectionActions.ts
+++ b/src/redux/actions/connectionActions.ts
@@ -6,6 +6,8 @@ import { ActionTypes } from "./actionTypes";
import { IPayloadAction, createPayloadAction } from "./actionCreators";
import { Dispatch } from "redux";
import ConnectionService from "../../services/connectionService";
+import { webStorage } from "../../common/webStorage";
+import { constants } from "../../common/constants";
/**
* Actions to be performed in relation to connections
@@ -35,6 +37,12 @@ export function saveConnection(connection: IConnection): (dispatch: Dispatch) =>
return async (dispatch: Dispatch) => {
const connectionService = new ConnectionService();
await connectionService.save(connection);
+ const projectJson = await webStorage.getItem(constants.projectFormTempKey);
+ if (projectJson) {
+ const project = JSON.parse(projectJson as string);
+ project.sourceConnection = connection;
+ await webStorage.setItem(constants.projectFormTempKey, JSON.stringify(project));
+ }
dispatch(saveConnectionAction(connection));
return Promise.resolve(connection);
};
diff --git a/src/redux/middleware/localStorage.ts b/src/redux/middleware/localStorage.ts
index bea277b71..9d0f1cb78 100644
--- a/src/redux/middleware/localStorage.ts
+++ b/src/redux/middleware/localStorage.ts
@@ -3,6 +3,7 @@
import { Middleware, Dispatch, AnyAction, MiddlewareAPI } from "redux";
import { constants } from "../../common/constants";
+import { webStorage } from "../../common/webStorage";
export interface ILocalStorageMiddlewareOptions {
paths: string[];
@@ -24,26 +25,44 @@ export function createLocalStorage(config: ILocalStorageMiddlewareOptions): Midd
};
}
-export function mergeInitialState(state: any, paths: string[]) {
+export async function mergeInitialState(state: any, paths: string[]) {
const initialState = { ...state };
- paths.forEach((path) => {
- const json = getStorageItem(path);
+
+ for (const path of paths) {
+ const isArray = Array.isArray(initialState[path]);
+
+ let legacyItem = isArray ? [] : {};
+ let item = isArray ? [] : {};
+
+ const json = (await getStorageItem(path)) as string;
if (json) {
- initialState[path] = JSON.parse(json);
+ item = JSON.parse(json);
+ }
+
+ const legacyJson = localStorage.getItem(`${constants.version}_${path}`);
+ if (legacyJson) {
+ legacyItem = JSON.parse(legacyJson);
+ localStorage.removeItem(`${constants.version}_${path}`);
+ }
+
+ if (isArray) {
+ initialState[path] = [].concat(legacyItem, item);
+ } else {
+ initialState[path] = { ...item, ...legacyItem };
}
- });
+ }
return initialState;
}
export function setStorageItem(key: string, value: string) {
- localStorage.setItem(`${constants.version}_${key}`, value);
+ webStorage.setItem(`${constants.version}_${key}`, value);
}
-export function getStorageItem(key: string) {
- return localStorage.getItem(`${constants.version}_${key}`);
+export async function getStorageItem(key: string): Promise {
+ return (await webStorage.getItem(`${constants.version}_${key}`)) as string;
}
export function removeStorageItem(key: string) {
- return localStorage.removeItem(`${constants.version}_${key}`);
+ return webStorage.removeItem(`${constants.version}_${key}`);
}
diff --git a/src/redux/reducers/connectionsReducer.ts b/src/redux/reducers/connectionsReducer.ts
index 033a7d503..013b2bc8d 100644
--- a/src/redux/reducers/connectionsReducer.ts
+++ b/src/redux/reducers/connectionsReducer.ts
@@ -1,11 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
-import { constants } from "../../common/constants";
import { ActionTypes } from "../actions/actionTypes";
import { IConnection } from "../../models/applicationState";
import { AnyAction } from "../actions/actionCreators";
-import { getStorageItem, setStorageItem } from "../middleware/localStorage";
/**
* Reducer for application connections. Actions handled:
@@ -22,12 +20,6 @@ export const reducer = (state: IConnection[] = [], action: AnyAction): IConnecti
switch (action.type) {
case ActionTypes.SAVE_CONNECTION_SUCCESS:
- const projectJson = getStorageItem(constants.projectFormTempKey);
- if (projectJson) {
- const project = JSON.parse(projectJson);
- project.sourceConnection = action.payload;
- setStorageItem(constants.projectFormTempKey, JSON.stringify(project));
- }
return [
{ ...action.payload },
...state.filter((connection) => connection.id !== action.payload.id),
diff --git a/src/redux/store/store.ts b/src/redux/store/store.ts
index 9c6ba5f82..4dbc94018 100644
--- a/src/redux/store/store.ts
+++ b/src/redux/store/store.ts
@@ -13,9 +13,9 @@ import { Env } from "../../common/environment";
* @param initialState - Initial state of application
* @param useLocalStorage - Whether or not to use localStorage middleware
*/
-export default function createReduxStore(
+export default async function createReduxStore(
initialState?: IApplicationState,
- useLocalStorage: boolean = false): Store {
+ useLocalStorage: boolean = false): Promise {
const paths: string[] = ["appSettings", "connections", "recentProjects", "prebuiltSettings"];
let middlewares = [thunk];
@@ -39,9 +39,11 @@ export default function createReduxStore(
];
}
+ const mergedInitialState = await mergeInitialState(initialState, paths);
+
return createStore(
rootReducer,
- useLocalStorage ? mergeInitialState(initialState, paths) : initialState,
+ useLocalStorage ? mergedInitialState : initialState,
applyMiddleware(...middlewares),
);
}
diff --git a/src/services/useLocalStorage.ts b/src/services/useLocalStorage.ts
index 1145df156..7c841f11d 100644
--- a/src/services/useLocalStorage.ts
+++ b/src/services/useLocalStorage.ts
@@ -1,7 +1,9 @@
+import { webStorage } from "../common/webStorage";
+
export default class UseLocalStorage {
- public static setItem(name: string, key: string, value: string) {
+ public static setItem = async (name: string, key: string, value: string) => {
// Get the existing data
- const existingData = window.localStorage.getItem(name);
+ const existingData = await webStorage.getItem(name) as string;
// If no existing data, create an {}
// Otherwise, convert the localStorage string to an {}
@@ -11,6 +13,6 @@ export default class UseLocalStorage {
newLsData[key] = value;
// Save back to localStorage
- window.localStorage.setItem(name, JSON.stringify(newLsData));
+ webStorage.setItem(name, JSON.stringify(newLsData));
}
}
diff --git a/yarn.lock b/yarn.lock
index b8eab85e4..9536be798 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8012,6 +8012,13 @@ levn@^0.3.0, levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"
+lie@3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
+ integrity sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=
+ dependencies:
+ immediate "~3.0.5"
+
lie@~3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
@@ -8086,6 +8093,13 @@ loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4
emojis-list "^3.0.0"
json5 "^1.0.1"
+localforage@^1.9.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.9.0.tgz#f3e4d32a8300b362b4634cc4e066d9d00d2f09d1"
+ integrity sha512-rR1oyNrKulpe+VM9cYmcFn6tsHuokyVHFaCM3+osEmxaHTbEk8oQu6eGDfS6DQLWi/N67XRmB8ECG37OES368g==
+ dependencies:
+ lie "3.1.1"
+
localized-strings@^0.2.0:
version "0.2.4"
resolved "https://registry.yarnpkg.com/localized-strings/-/localized-strings-0.2.4.tgz#9d61c06b60cc7b5edf7c46e6c7f2d1ecb84aeb2c"