Skip to content
This repository has been archived by the owner on Dec 7, 2021. It is now read-only.

Commit

Permalink
feat: Save partial project progress during project creation (#769)
Browse files Browse the repository at this point in the history
This adds functionality to persist partial project information when creating a new project. Right now when creating a new connection inline within the create project flow and returning to the create project screen your partial project information is lost. Partial form progress is now saved into local storage and bound when returning to the form.

Resolves #758
  • Loading branch information
wbreza authored Apr 17, 2019
1 parent b13eaf6 commit 5574cd9
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 60 deletions.
51 changes: 36 additions & 15 deletions src/react/components/pages/projectSettings/projectForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ describe("Project Form Component", () => {
const appSettings = MockFactory.appSettings();
const connections = MockFactory.createTestConnections();
let wrapper: ReactWrapper<IProjectFormProps, IProjectFormState> = null;
let onSubmitHandler: jest.Mock = null;
let onCancelHandler: jest.Mock = null;
const onSubmitHandler = jest.fn();
const onChangeHandler = jest.fn();
const onCancelHandler = jest.fn();

function createComponent(props: IProjectFormProps) {
return mount(
Expand All @@ -33,13 +34,16 @@ describe("Project Form Component", () => {

describe("Completed project", () => {
beforeEach(() => {
onSubmitHandler = jest.fn();
onCancelHandler = jest.fn();
onChangeHandler.mockClear();
onSubmitHandler.mockClear();
onCancelHandler.mockClear();

wrapper = createComponent({
project,
connections,
appSettings,
onSubmit: onSubmitHandler,
onChange: onChangeHandler,
onCancel: onCancelHandler,
});
});
Expand Down Expand Up @@ -76,10 +80,14 @@ describe("Project Form Component", () => {

const form = wrapper.find("form");
form.simulate("submit");
expect(onSubmitHandler).toBeCalledWith({

const expectedProject = {
...project,
name: newName,
});
};

expect(onChangeHandler).toBeCalled();
expect(onSubmitHandler).toBeCalledWith(expectedProject);
});

it("starting project should update description upon submission", () => {
Expand All @@ -92,10 +100,14 @@ describe("Project Form Component", () => {

const form = wrapper.find("form");
form.simulate("submit");
expect(onSubmitHandler).toBeCalledWith({

const expectedProject = {
...project,
description: newDescription,
});
};

expect(onChangeHandler).toBeCalledWith(expect.objectContaining(project));
expect(onSubmitHandler).toBeCalledWith(expectedProject);
});

it("starting project should update source connection ID upon submission", () => {
Expand All @@ -109,11 +121,14 @@ describe("Project Form Component", () => {
expect(wrapper.state().formData.sourceConnection).toEqual(newConnection);
const form = wrapper.find("form");
form.simulate("submit");
expect(onSubmitHandler).toBeCalledWith({

const expectedProject = {
...project,
sourceConnection: connections[1],
});
};

expect(onChangeHandler).toBeCalledWith(expect.objectContaining(project));
expect(onSubmitHandler).toBeCalledWith(expectedProject);
});

it("starting project should update target connection ID upon submission", () => {
Expand All @@ -125,13 +140,17 @@ describe("Project Form Component", () => {
wrapper.find("select#root_targetConnection").simulate("change", { target: { value: newConnection.id } });
expect(wrapper.state().formData.targetConnection).toEqual(newConnection);
wrapper.find("form").simulate("submit");
expect(onSubmitHandler).toBeCalledWith({

const expectedProject = {
...project,
targetConnection: connections[1],
});
};

expect(onChangeHandler).toBeCalledWith(expect.objectContaining(project));
expect(onSubmitHandler).toBeCalledWith(expectedProject);
});

it("starting project should call onChangeHandler on submission", () => {
it("starting project should call onSubmitHandler on submission", () => {
const form = wrapper.find("form");
form.simulate("submit");
expect(onSubmitHandler).toBeCalledWith({
Expand All @@ -155,6 +174,7 @@ describe("Project Form Component", () => {

const form = wrapper.find("form");
form.simulate("submit");
expect(onChangeHandler).toBeCalledWith(expect.objectContaining(project));
expect(onSubmitHandler).toBeCalledWith(
expect.objectContaining({
name: newName,
Expand Down Expand Up @@ -187,6 +207,7 @@ describe("Project Form Component", () => {
appSettings,
connections: newConnections,
onSubmit: onSubmitHandler,
onChange: onChangeHandler,
onCancel: onCancelHandler,
});
// Source Connection should have all connections
Expand All @@ -202,13 +223,12 @@ describe("Project Form Component", () => {

describe("Empty Project", () => {
beforeEach(() => {
onSubmitHandler = jest.fn();
onCancelHandler = jest.fn();
wrapper = createComponent({
project: null,
appSettings,
connections,
onSubmit: onSubmitHandler,
onChange: onChangeHandler,
onCancel: onCancelHandler,
});
});
Expand Down Expand Up @@ -239,6 +259,7 @@ describe("Project Form Component", () => {
appSettings,
connections,
onSubmit: onSubmitHandler,
onChange: onChangeHandler,
onCancel: onCancelHandler,
});
const newTagName = "My new tag";
Expand Down
10 changes: 9 additions & 1 deletion src/react/components/pages/projectSettings/projectForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import Form, { FormValidation, ISubmitEvent } from "react-jsonschema-form";
import Form, { FormValidation, ISubmitEvent, IChangeEvent } from "react-jsonschema-form";
import { ITagsInputProps, TagEditorModal, TagsInput } from "vott-react";
import { addLocValues, strings } from "../../../../common/strings";
import { IConnection, IProject, ITag, IAppSettings } from "../../../../models/applicationState";
Expand Down Expand Up @@ -28,6 +28,7 @@ export interface IProjectFormProps extends React.Props<ProjectForm> {
connections: IConnection[];
appSettings: IAppSettings;
onSubmit: (project: IProject) => void;
onChange?: (project: IProject) => void;
onCancel?: () => void;
}

Expand Down Expand Up @@ -97,6 +98,7 @@ export default class ProjectForm extends React.Component<IProjectFormProps, IPro
schema={this.state.formSchema}
uiSchema={this.state.uiSchema}
formData={this.state.formData}
onChange={this.onFormChange}
onSubmit={this.onFormSubmit}>
<div>
<button className="btn btn-success mr-1" type="submit">{strings.projectSettings.save}</button>
Expand Down Expand Up @@ -184,6 +186,12 @@ export default class ProjectForm extends React.Component<IProjectFormProps, IPro
return errors;
}

private onFormChange = (changeEvent: IChangeEvent<IProject>) => {
if (this.props.onChange) {
this.props.onChange(changeEvent.formData);
}
}

private onFormSubmit(args: ISubmitEvent<IProject>) {
const project: IProject = {
...args.formData,
Expand Down
140 changes: 107 additions & 33 deletions src/react/components/pages/projectSettings/projectSettingsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@ import { Provider } from "react-redux";
import { BrowserRouter as Router } from "react-router-dom";
import MockFactory from "../../../../common/mockFactory";
import createReduxStore from "../../../../redux/store/store";
import ProjectSettingsPage, { IProjectSettingsPageProps } from "./projectSettingsPage";
import ProjectSettingsPage, { IProjectSettingsPageProps, IProjectSettingsPageState } from "./projectSettingsPage";

jest.mock("../../../../services/projectService");
import ProjectService from "../../../../services/projectService";
import { IAppSettings } from "../../../../models/applicationState";
import { IAppSettings, IProject } from "../../../../models/applicationState";
import ProjectMetrics from "./projectMetrics";
import ProjectForm, { IProjectFormProps } from "./projectForm";

jest.mock("./projectMetrics", () => () => {
return (
<div className="project-settings-page-metrics">
Dummy Project Metrics
</div>
);
},
return (
<div className="project-settings-page-metrics">
Dummy Project Metrics
</div>
);
},
);

describe("Project settings page", () => {
Expand All @@ -33,12 +34,29 @@ describe("Project settings page", () => {
);
}

const localStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
};

beforeAll(() => {
Object.defineProperty(global, "_localStorage", {
value: localStorageMock,
writable: false,
});
});

beforeEach(() => {
localStorageMock.getItem.mockClear();
localStorageMock.setItem.mockClear();
localStorageMock.removeItem.mockClear();

projectServiceMock = ProjectService as jest.Mocked<typeof ProjectService>;
projectServiceMock.prototype.load = jest.fn((project) => ({...project}));
projectServiceMock.prototype.load = jest.fn((project) => ({ ...project }));
});

it("Form submission calls save project action", (done) => {
it("Form submission calls save project action", async () => {
const store = createReduxStore(MockFactory.initialState());
const props = MockFactory.projectSettingsProps();
const saveProjectSpy = jest.spyOn(props.projectActions, "saveProject");
Expand All @@ -47,14 +65,12 @@ describe("Project settings page", () => {

const wrapper = createComponent(store, props);
wrapper.find("form").simulate("submit");
await MockFactory.flushUi();

setImmediate(() => {
expect(saveProjectSpy).toBeCalled();
done();
});
expect(saveProjectSpy).toBeCalled();
});

it("Throws an error when a user tries to create a duplicate project", async (done) => {
it("Throws an error when a user tries to create a duplicate project", async () => {
const project = MockFactory.createTestProject("1");
project.id = "25";
const initialStateOverride = {
Expand All @@ -78,18 +94,16 @@ describe("Project settings page", () => {
},
});
wrapper.find("form").simulate("submit");
setImmediate(async () => {
// expect(saveProjectSpy).toBeCalled();
expect(saveProjectSpy.mockRejectedValue).not.toBeNull();
done();
});
await MockFactory.flushUi();

expect(saveProjectSpy.mockRejectedValue).not.toBeNull();
});

it("calls save project when user creates a unique project", (done) => {
it("calls save project when user creates a unique project", async () => {
const initialState = MockFactory.initialState();

// New Project should not have id or security token set by default
const project = {...initialState.recentProjects[0]};
const project = { ...initialState.recentProjects[0] };
project.id = null;
project.name = "Brand New Project";
project.securityToken = "";
Expand All @@ -106,20 +120,20 @@ describe("Project settings page", () => {
const wrapper = createComponent(store, props);
wrapper.find("form").simulate("submit");

setImmediate(() => {
// New security token was created for new project
expect(saveAppSettingsSpy).toBeCalled();
const appSettings = saveAppSettingsSpy.mock.calls[0][0] as IAppSettings;
expect(appSettings.securityTokens.length).toEqual(initialState.appSettings.securityTokens.length + 1);
await MockFactory.flushUi();

// New project was saved with new security token
expect(saveProjectSpy).toBeCalledWith({
...project,
securityToken: `${project.name} Token`,
});
// New security token was created for new project
expect(saveAppSettingsSpy).toBeCalled();
const appSettings = saveAppSettingsSpy.mock.calls[0][0] as IAppSettings;
expect(appSettings.securityTokens.length).toEqual(initialState.appSettings.securityTokens.length + 1);

done();
// New project was saved with new security token
expect(saveProjectSpy).toBeCalledWith({
...project,
securityToken: `${project.name} Token`,
});

expect(localStorage.removeItem).toBeCalledWith("projectForm");
});

it("render ProjectMetrics", () => {
Expand All @@ -146,4 +160,64 @@ describe("Project settings page", () => {
expect(projectMetrics).toHaveLength(0);
});
});

describe("Persisting project form", () => {
let wrapper: ReactWrapper = null;

function initPersistProjectFormTest() {
const store = createReduxStore(MockFactory.initialState());
const props = MockFactory.projectSettingsProps();
props.match.url = "/projects/create";
wrapper = createComponent(store, props);
}

it("Loads partial project from local storage", () => {
const partialProject: IProject = {
...{} as any,
name: "partial project",
description: "partial project description",
tags: [
{ name: "tag-1", color: "#ff0000" },
{ name: "tag-3", color: "#ffff00" },
],
};

localStorageMock.getItem.mockImplementationOnce(() => JSON.stringify(partialProject));

initPersistProjectFormTest();
const projectSettingsPage = wrapper
.find(ProjectSettingsPage)
.childAt(0) as ReactWrapper<IProjectSettingsPageProps, IProjectSettingsPageState>;

expect(localStorage.getItem).toBeCalledWith("projectForm");
expect(projectSettingsPage.state().project).toEqual(partialProject);
});

it("Stores partial project in local storage", () => {
initPersistProjectFormTest();
const partialProject: IProject = {
...{} as any,
name: "partial project",
};

const projectForm = wrapper.find(ProjectForm) as ReactWrapper<IProjectFormProps>;
projectForm.props().onChange(partialProject);

expect(localStorage.setItem).toBeCalledWith("projectForm", JSON.stringify(partialProject));
});

it("Does NOT store empty project in local storage", () => {
initPersistProjectFormTest();
const emptyProject: IProject = {
...{} as any,
sourceConnection: {},
targetConnection: {},
exportFormat: {},
};
const projectForm = wrapper.find(ProjectForm) as ReactWrapper<IProjectFormProps>;
projectForm.props().onChange(emptyProject);

expect(localStorage.setItem).not.toBeCalled();
});
});
});
Loading

0 comments on commit 5574cd9

Please sign in to comment.