* Updated project form ui

* Cleaned up project form validation

* Cleaned up connection form UI

* Applied updated UI styles and validation to export form
This commit is contained in:
Wallace Breza 2018-12-20 16:05:03 -08:00 коммит произвёл GitHub
Родитель f573b20d68
Коммит 5c2a60111f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
15 изменённых файлов: 372 добавлений и 229 удалений

Просмотреть файл

@ -31,4 +31,26 @@ input[type=file] {
}
::-webkit-scrollbar-thumb:window-inactive {
background: $lighter-4;
}
.form-group {
&.is-invalid {
.invalid-feedback {
display: block;
}
.form-control {
border: solid 1px #ee5f5b;
}
}
&.is-valid {
.valid-feedback {
display: block;
}
.form-control {
border: solid 1px #62c462;
}
}
}

Просмотреть файл

@ -38,7 +38,7 @@ export default class ConnectionPicker extends React.Component<IConnectionPickerP
return (
<div className="input-group">
<select id={id} value={selectedValue} onChange={this.onChange} className="form-control">
<option value="">Select Connection</option>
<option>Select Connection</option>
{connections.map((connection) =>
<option key={connection.id} value={connection.id}>{connection.name}</option>)
}
@ -52,12 +52,10 @@ export default class ConnectionPicker extends React.Component<IConnectionPickerP
private onChange = (e) => {
const selectedConnection = this.props.connections
.find((connection) => connection.id === e.target.value);
.find((connection) => connection.id === e.target.value) || {};
if (selectedConnection) {
this.setState({
value: selectedConnection,
}, () => this.props.onChange(selectedConnection));
}
this.setState({
value: selectedConnection,
}, () => this.props.onChange(selectedConnection));
}
}

Просмотреть файл

@ -8,12 +8,6 @@ export default function CustomField(Widget: any, mapProps?: (props: FieldProps)
return function render(props: FieldProps) {
const { idSchema, schema, required } = props;
const widgetProps = mapProps ? mapProps(props) : props;
return (
<div>
<label className="control-label" htmlFor={idSchema.$id}>{schema.title}{required ? "*" : null}</label>
{schema.description}
<Widget {...widgetProps} />
</div>
);
return (<Widget {...widgetProps} />);
};
}

Просмотреть файл

@ -0,0 +1,28 @@
import React from "react";
import { FieldTemplateProps } from "react-jsonschema-form";
export default function CustomFieldTemplate(props: FieldTemplateProps) {
const { id, label, required, description, rawErrors, children } = props;
const classNames = ["form-group"];
if (rawErrors && rawErrors.length > 0) {
classNames.push("is-invalid");
} else {
classNames.push("is-valid");
}
return (
<div className={classNames.join(" ")}>
{ /* Render label for non-objects except for when an object has defined a ui:field template */}
{(props.schema.type !== "object" || (props.schema.type === "object" && props.uiSchema["ui:field"])) &&
<label htmlFor={id}>{label}{required ? "*" : null}</label>
}
{children}
{description && <small className="text-muted">{description}</small>}
{rawErrors && rawErrors.length > 0 &&
<div className="invalid-feedback">
{rawErrors.map((errorMessage, idx) => <div key={idx}>{label} {errorMessage}</div>)}
</div>
}
</div>
);
}

Просмотреть файл

@ -1,5 +1,4 @@
{
"title": "Connection Details",
"required": [
"name",
"providerType"

Просмотреть файл

@ -1,7 +1,8 @@
import React from "react";
import Form, { Widget } from "react-jsonschema-form";
import { IConnection } from "../../../../models/applicationState.js";
import Form, { Widget, IChangeEvent, FormValidation } from "react-jsonschema-form";
import { IConnection } from "../../../../models/applicationState";
import LocalFolderPicker from "../../common/localFolderPicker";
import CustomFieldTemplate from "../../common/customFieldTemplate";
// tslint:disable-next-line:no-var-requires
const formSchema = require("./connectionForm.json");
// tslint:disable-next-line:no-var-requires
@ -10,6 +11,7 @@ const uiSchema = require("./connectionForm.ui.json");
interface IConnectionFormProps extends React.Props<ConnectionForm> {
connection: IConnection;
onSubmit: (connection: IConnection) => void;
onCancel?: () => void;
}
interface IConnectionFormState {
@ -17,6 +19,7 @@ interface IConnectionFormState {
formSchema: any;
uiSchema: any;
formData: IConnection;
classNames: string[];
}
export default class ConnectionForm extends React.Component<IConnectionFormProps, IConnectionFormState> {
@ -28,6 +31,7 @@ export default class ConnectionForm extends React.Component<IConnectionFormProps
super(props, context);
this.state = {
classNames: ["needs-validation"],
formSchema: { ...formSchema },
uiSchema: { ...uiSchema },
providerName: this.props.connection ? this.props.connection.providerType : null,
@ -38,10 +42,12 @@ export default class ConnectionForm extends React.Component<IConnectionFormProps
this.bindForm(this.props.connection);
}
this.onFormCancel = this.onFormCancel.bind(this);
this.onFormValidate = this.onFormValidate.bind(this);
this.onFormChange = this.onFormChange.bind(this);
}
public componentDidUpdate(prevProps) {
public componentDidUpdate(prevProps: IConnectionFormProps) {
if (prevProps.connection !== this.props.connection) {
this.bindForm(this.props.connection);
}
@ -53,19 +59,41 @@ export default class ConnectionForm extends React.Component<IConnectionFormProps
<h3><i className="fas fa-plug fa-1x"></i><span className="px-2">Connection Settings</span></h3>
<div className="m-3 text-light">
<Form
className={this.state.classNames.join(" ")}
showErrorList={false}
liveValidate={true}
noHtml5Validate={true}
FieldTemplate={CustomFieldTemplate}
validate={this.onFormValidate}
widgets={this.widgets}
schema={this.state.formSchema}
uiSchema={this.state.uiSchema}
formData={this.state.formData}
onChange={this.onFormChange}
onSubmit={(form) => this.props.onSubmit(form.formData)}>
<div>
<button className="btn btn-success mr-1" type="submit">Save Connection</button>
<button className="btn btn-secondary btn-cancel"
type="button"
onClick={this.onFormCancel}>Cancel</button>
</div>
</Form>
</div>
</div>
);
}
private onFormChange = (args) => {
private onFormValidate(connection: IConnection, errors: FormValidation) {
if (this.state.classNames.indexOf("was-validated") === -1) {
this.setState({
classNames: [...this.state.classNames, "was-validated"],
});
}
return errors;
}
private onFormChange = (args: IChangeEvent<IConnection>) => {
const providerType = args.formData.providerType;
if (providerType !== this.state.providerName) {
@ -73,6 +101,12 @@ export default class ConnectionForm extends React.Component<IConnectionFormProps
}
}
private onFormCancel() {
if (this.props.onCancel) {
this.props.onCancel();
}
}
private bindForm(connection: IConnection, resetProviderOptions: boolean = false) {
const providerType = connection ? connection.providerType : null;
let newFormSchema: any = this.state.formSchema;

Просмотреть файл

@ -41,6 +41,7 @@ export default class ConnectionPage extends React.Component<IConnectionPageProps
};
this.onFormSubmit = this.onFormSubmit.bind(this);
this.onFormCancel = this.onFormCancel.bind(this);
this.onConnectionDelete = this.onConnectionDelete.bind(this);
}
@ -81,7 +82,8 @@ export default class ConnectionPage extends React.Component<IConnectionPageProps
<Route exact path="/connections/:connectionId" render={(props) =>
<ConnectionForm
connection={this.state.connection}
onSubmit={this.onFormSubmit} />
onSubmit={this.onFormSubmit}
onCancel={this.onFormCancel} />
} />
</div>
);
@ -102,6 +104,10 @@ export default class ConnectionPage extends React.Component<IConnectionPageProps
private onFormSubmit = async (connection: IConnection) => {
await this.props.actions.saveConnection(connection);
this.props.history.push("/connections");
this.props.history.goBack();
}
private onFormCancel() {
this.props.history.goBack();
}
}

Просмотреть файл

@ -1,6 +1,7 @@
import React from "react";
import Form from "react-jsonschema-form";
import Form, { FormValidation, ISubmitEvent, IChangeEvent } from "react-jsonschema-form";
import { IExportFormat } from "../../../../models/applicationState.js";
import CustomFieldTemplate from "../../common/customFieldTemplate";
// tslint:disable-next-line:no-var-requires
const formSchema = require("./exportForm.json");
// tslint:disable-next-line:no-var-requires
@ -9,9 +10,11 @@ const uiSchema = require("./exportForm.ui.json");
export interface IExportFormProps extends React.Props<ExportForm> {
settings: IExportFormat;
onSubmit: (exportFormat: IExportFormat) => void;
onCancel?: () => void;
}
export interface IExportFormState {
classNames: string[];
providerName: string;
formSchema: any;
uiSchema: any;
@ -23,6 +26,7 @@ export default class ExportForm extends React.Component<IExportFormProps, IExpor
super(props, context);
this.state = {
classNames: ["needs-validation"],
providerName: this.props.settings ? this.props.settings.providerType : null,
formSchema: { ...formSchema },
uiSchema: { ...uiSchema },
@ -33,11 +37,13 @@ export default class ExportForm extends React.Component<IExportFormProps, IExpor
this.bindForm(this.props.settings);
}
this.onFormChange = this.onFormChange.bind(this);
this.onFormSubmit = this.onFormSubmit.bind(this);
this.onFormValidate = this.onFormValidate.bind(this);
this.onFormChange = this.onFormChange.bind(this);
this.onFormCancel = this.onFormCancel.bind(this);
}
public componentDidUpdate(prevProps) {
public componentDidUpdate(prevProps: IExportFormProps) {
if (prevProps.settings !== this.props.settings) {
this.bindForm(this.props.settings);
}
@ -46,15 +52,28 @@ export default class ExportForm extends React.Component<IExportFormProps, IExpor
public render() {
return (
<Form
className={this.state.classNames.join(" ")}
showErrorList={false}
liveValidate={true}
noHtml5Validate={true}
FieldTemplate={CustomFieldTemplate}
validate={this.onFormValidate}
schema={this.state.formSchema}
uiSchema={this.state.uiSchema}
formData={this.state.formData}
onChange={this.onFormChange}
onSubmit={this.onFormSubmit} />
onSubmit={this.onFormSubmit}>
<div>
<button className="btn btn-success mr-1" type="submit">Save Export Settings</button>
<button className="btn btn-secondary btn-cancel"
type="button"
onClick={this.onFormCancel}>Cancel</button>
</div>
</Form>
);
}
private onFormChange = (args) => {
private onFormChange = (args: IChangeEvent<IExportFormat>) => {
const providerType = args.formData.providerType;
if (providerType !== this.state.providerName) {
@ -62,10 +81,26 @@ export default class ExportForm extends React.Component<IExportFormProps, IExpor
}
}
private onFormSubmit = (args) => {
private onFormValidate(exportFormat: IExportFormat, errors: FormValidation) {
if (this.state.classNames.indexOf("was-validated") === -1) {
this.setState({
classNames: [...this.state.classNames, "was-validated"],
});
}
return errors;
}
private onFormSubmit = (args: ISubmitEvent<IExportFormat>) => {
this.props.onSubmit(args.formData);
}
private onFormCancel() {
if (this.props.onCancel) {
this.props.onCancel();
}
}
private bindForm(exportFormat: IExportFormat, resetProviderOptions: boolean = false) {
const providerType = exportFormat ? exportFormat.providerType : null;
let newFormSchema: any = this.state.formSchema;

Просмотреть файл

@ -59,7 +59,7 @@ describe("Export Page", () => {
});
});
it("Calls save and export project actions on form submit", (done) => {
it("Calls save project actions on form submit", (done) => {
const testProject = MockFactory.createTestProject("TestProject");
const store = createStore(testProject, true);
const props = createProps(testProject.id);
@ -81,7 +81,7 @@ describe("Export Page", () => {
setImmediate(() => {
expect(saveProjectSpy).toBeCalled();
expect(exportProjectSpy).toBeCalled();
expect(exportProjectSpy).not.toBeCalled();
expect(props.history.goBack).toBeCalled();
const state = store.getState() as IApplicationState;

Просмотреть файл

@ -42,6 +42,7 @@ export default class ExportPage extends React.Component<IExportPageProps> {
}
this.onFormSubmit = this.onFormSubmit.bind(this);
this.onFormCancel = this.onFormCancel.bind(this);
}
public render() {
@ -53,7 +54,8 @@ export default class ExportPage extends React.Component<IExportPageProps> {
<div className="m-3 text-light">
<ExportForm
settings={exportFormat}
onSubmit={this.onFormSubmit} />
onSubmit={this.onFormSubmit}
onCancel={this.onFormCancel} />
</div>
</div>
);
@ -66,7 +68,10 @@ export default class ExportPage extends React.Component<IExportPageProps> {
};
await this.props.actions.saveProject(projectToUpdate);
await this.props.actions.exportProject(this.props.project);
this.props.history.goBack();
}
private onFormCancel() {
this.props.history.goBack();
}
}

Просмотреть файл

@ -7,10 +7,12 @@
},
"sourceConnection": {
"title": "Source Connection",
"description": "Where to load the assets from",
"type": "object"
},
"targetConnection": {
"title": "Target Connection",
"description": "Where to save the project and exported data",
"type": "object"
},
"description": {

Просмотреть файл

@ -1,15 +1,16 @@
import { mount } from "enzyme";
import { mount, ReactWrapper } from "enzyme";
import React from "react";
import { BrowserRouter as Router } from "react-router-dom";
import MockFactory from "../../../../common/mockFactory";
import { KeyCodes } from "../../common/tagsInput/tagsInput";
import ProjectForm, { IProjectFormProps } from "./projectForm";
import { IProject } from "../../../../models/applicationState";
import ProjectForm, { IProjectFormProps, IProjectFormState } from "./projectForm";
describe("Project Form Component", () => {
const project = MockFactory.createTestProject("TestProject");
const connections = MockFactory.createTestConnections();
let wrapper: ReactWrapper<IProjectFormProps, IProjectFormState> = null;
let onSubmitHandler: jest.Mock = null;
let onCancelHandler: jest.Mock = null;
function createComponent(props: IProjectFormProps) {
return mount(
@ -20,209 +21,173 @@ describe("Project Form Component", () => {
).find(ProjectForm).childAt(0);
}
it("starting project has initial state loaded correctly", () => {
const onSubmit = jest.fn();
const wrapper = createComponent({
project,
connections,
onSubmit,
});
const formData = wrapper.state().formData;
expect(formData.name).toEqual(project.name);
expect(formData.sourceConnection).toEqual(project.sourceConnection);
expect(formData.targetConnection).toEqual(project.targetConnection);
expect(formData.description).toEqual(project.description);
expect(project.tags.length).toBeGreaterThan(0);
expect(formData.tags).toEqual(project.tags);
});
it("starting project has correct initial rendering", () => {
const onSubmit = jest.fn();
const wrapper = createComponent({
project,
connections,
onSubmit,
});
expect(project.tags.length).toBeGreaterThan(0);
expect(wrapper.find(".tag-wrapper")).toHaveLength(project.tags.length);
});
it("starting project should update name upon submission", () => {
const onSubmit = jest.fn();
const wrapper = createComponent({
project,
connections,
onSubmit,
});
const newName = "My new name";
const currentName = wrapper.state().formData.name;
expect(currentName).not.toEqual(newName);
expect(currentName).toEqual(project.name);
wrapper.find("input#root_name").simulate("change", { target: { value: newName } });
expect(wrapper.state().formData.name).toEqual(newName);
const form = wrapper.find("form");
form.simulate("submit");
expect(onSubmit).toBeCalledWith({
...project,
name: newName,
});
});
it("starting project should update description upon submission", () => {
const onSubmit = jest.fn();
const wrapper = createComponent({
project,
connections,
onSubmit,
});
const newDescription = "My new description";
const currentDescription = wrapper.state().formData.description;
expect(currentDescription).not.toEqual(newDescription);
expect(currentDescription).toEqual(project.description);
wrapper.find("textarea#root_description").simulate("change", { target: { value: newDescription } });
expect(wrapper.state().formData.description).toEqual(newDescription);
const form = wrapper.find("form");
form.simulate("submit");
expect(onSubmit).toBeCalledWith({
...project,
description: newDescription,
});
});
it("starting project should update source connection ID upon submission", () => {
const onSubmit = jest.fn();
const wrapper = createComponent({
project,
connections,
onSubmit,
});
const newConnection = connections[1];
const currentConnectionId = wrapper.state().formData.sourceConnection.id;
expect(currentConnectionId).not.toEqual(newConnection.id);
expect(currentConnectionId).toEqual(project.sourceConnection.id);
expect(wrapper.find("select#root_sourceConnection").exists()).toBe(true);
wrapper.find("select#root_sourceConnection").simulate("change", { target: { value: newConnection.id } });
expect(wrapper.state().formData.sourceConnection).toEqual(newConnection);
const form = wrapper.find("form");
form.simulate("submit");
expect(onSubmit).toBeCalledWith({
...project,
sourceConnection: connections[1],
describe("Completed project", () => {
beforeEach(() => {
onSubmitHandler = jest.fn();
onCancelHandler = jest.fn();
wrapper = createComponent({
project,
connections,
onSubmit: onSubmitHandler,
onCancel: onCancelHandler,
});
});
});
it("starting project should update target connection ID upon submission", () => {
const onSubmit = jest.fn();
const wrapper = createComponent({
project,
connections,
onSubmit,
});
const newConnection = connections[1];
const currentConnectionId = wrapper.state().formData.targetConnection.id;
expect(currentConnectionId).not.toEqual(newConnection.id);
expect(currentConnectionId).toEqual(project.targetConnection.id);
expect(wrapper.find("select#root_targetConnection").exists()).toBe(true);
wrapper.find("select#root_targetConnection").simulate("change", { target: { value: newConnection.id } });
expect(wrapper.state().formData.targetConnection).toEqual(newConnection);
wrapper.find("form").simulate("submit");
expect(onSubmit).toBeCalledWith({
...project,
targetConnection: connections[1],
});
});
it("starting project should call onChangeHandler on submission", () => {
const onSubmit = jest.fn();
const wrapper = createComponent({
project,
connections,
onSubmit,
});
const form = wrapper.find("form");
form.simulate("submit");
expect(onSubmit).toBeCalledWith({
...project,
});
});
it("starting project should edit fields and submit project", () => {
const onSubmit = jest.fn();
const wrapper = createComponent({
project,
connections,
onSubmit,
it("starting project has initial state loaded correctly", () => {
const formData = wrapper.state().formData;
expect(formData.name).toEqual(project.name);
expect(formData.sourceConnection).toEqual(project.sourceConnection);
expect(formData.targetConnection).toEqual(project.targetConnection);
expect(formData.description).toEqual(project.description);
expect(project.tags.length).toBeGreaterThan(0);
expect(formData.tags).toEqual(project.tags);
});
const newName = "My new name";
const newConnection = connections[1];
const newDescription = "My new description";
const newTagName = "My new tag";
it("starting project has correct initial rendering", () => {
expect(project.tags.length).toBeGreaterThan(0);
expect(wrapper.find(".tag-wrapper")).toHaveLength(project.tags.length);
});
wrapper.find("input#root_name").simulate("change", { target: { value: newName } });
wrapper.find("select#root_sourceConnection").simulate("change", { target: { value: newConnection.id } });
wrapper.find("select#root_targetConnection").simulate("change", { target: { value: newConnection.id } });
wrapper.find("textarea#root_description").simulate("change", { target: { value: newDescription } });
wrapper.find("input.ReactTags__tagInputField").simulate("change", {target: {value: newTagName}});
wrapper.find("input.ReactTags__tagInputField").simulate("keyDown", {keyCode: KeyCodes.enter});
it("starting project should update name upon submission", () => {
const newName = "My new name";
const currentName = wrapper.state().formData.name;
expect(currentName).not.toEqual(newName);
expect(currentName).toEqual(project.name);
wrapper.find("input#root_name").simulate("change", { target: { value: newName } });
expect(wrapper.state().formData.name).toEqual(newName);
const form = wrapper.find("form");
form.simulate("submit");
expect(onSubmit).toBeCalledWith(
expect.objectContaining({
const form = wrapper.find("form");
form.simulate("submit");
expect(onSubmitHandler).toBeCalledWith({
...project,
name: newName,
sourceConnection: connections[1],
targetConnection: connections[1],
});
});
it("starting project should update description upon submission", () => {
const newDescription = "My new description";
const currentDescription = wrapper.state().formData.description;
expect(currentDescription).not.toEqual(newDescription);
expect(currentDescription).toEqual(project.description);
wrapper.find("textarea#root_description").simulate("change", { target: { value: newDescription } });
expect(wrapper.state().formData.description).toEqual(newDescription);
const form = wrapper.find("form");
form.simulate("submit");
expect(onSubmitHandler).toBeCalledWith({
...project,
description: newDescription,
tags: expect.arrayContaining([
...project.tags,
{
name: newTagName,
color: expect.stringMatching(/^#([0-9a-fA-F]{3}){1,2}$/i),
},
]),
}),
);
});
});
it("starting project should update source connection ID upon submission", () => {
const newConnection = connections[1];
const currentConnectionId = wrapper.state().formData.sourceConnection.id;
expect(currentConnectionId).not.toEqual(newConnection.id);
expect(currentConnectionId).toEqual(project.sourceConnection.id);
expect(wrapper.find("select#root_sourceConnection").exists()).toBe(true);
wrapper.find("select#root_sourceConnection").simulate("change", { target: { value: newConnection.id } });
expect(wrapper.state().formData.sourceConnection).toEqual(newConnection);
const form = wrapper.find("form");
form.simulate("submit");
expect(onSubmitHandler).toBeCalledWith({
...project,
sourceConnection: connections[1],
});
});
it("starting project should update target connection ID upon submission", () => {
const newConnection = connections[1];
const currentConnectionId = wrapper.state().formData.targetConnection.id;
expect(currentConnectionId).not.toEqual(newConnection.id);
expect(currentConnectionId).toEqual(project.targetConnection.id);
expect(wrapper.find("select#root_targetConnection").exists()).toBe(true);
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({
...project,
targetConnection: connections[1],
});
});
it("starting project should call onChangeHandler on submission", () => {
const form = wrapper.find("form");
form.simulate("submit");
expect(onSubmitHandler).toBeCalledWith({
...project,
});
});
it("starting project should edit fields and submit project", () => {
const newName = "My new name";
const newConnection = connections[1];
const newDescription = "My new description";
const newTagName = "My new tag";
wrapper.find("input#root_name").simulate("change", { target: { value: newName } });
wrapper.find("select#root_sourceConnection").simulate("change", { target: { value: newConnection.id } });
wrapper.find("select#root_targetConnection").simulate("change", { target: { value: newConnection.id } });
wrapper.find("textarea#root_description").simulate("change", { target: { value: newDescription } });
wrapper.find("input.ReactTags__tagInputField").simulate("change", { target: { value: newTagName } });
wrapper.find("input.ReactTags__tagInputField").simulate("keyDown", { keyCode: KeyCodes.enter });
const form = wrapper.find("form");
form.simulate("submit");
expect(onSubmitHandler).toBeCalledWith(
expect.objectContaining({
name: newName,
sourceConnection: connections[1],
targetConnection: connections[1],
description: newDescription,
tags: expect.arrayContaining([
...project.tags,
{
name: newTagName,
color: expect.stringMatching(/^#([0-9a-fA-F]{3}){1,2}$/i),
},
]),
}),
);
});
it("Canceling the form calls the specified onChange handler", () => {
const cancelButton = wrapper.find("form .btn-cancel");
cancelButton.simulate("click");
expect(onCancelHandler).toBeCalled();
});
});
it("empty project has initial state loaded correctly", () => {
const onSubmit = jest.fn();
const wrapper = createComponent({
project: null,
connections,
onSubmit,
describe("Empty Project", () => {
beforeEach(() => {
onSubmitHandler = jest.fn();
onCancelHandler = jest.fn();
wrapper = createComponent({
project: null,
connections,
onSubmit: onSubmitHandler,
onCancel: onCancelHandler,
});
});
it("Has initial state loaded correctly", () => {
const formData = wrapper.state().formData;
expect(formData.name).toBe(undefined);
expect(formData.sourceConnection).toEqual({});
expect(formData.targetConnection).toEqual({});
expect(formData.description).toBe(undefined);
expect(formData.tags).toBe(undefined);
});
const formData = wrapper.state().formData;
expect(formData.name).toBe(undefined);
expect(formData.sourceConnection).toEqual({});
expect(formData.targetConnection).toEqual({});
expect(formData.description).toBe(undefined);
expect(formData.tags).toBe(undefined);
});
it("empty project has correct initial rendering", () => {
const onSubmit = jest.fn();
const wrapper = createComponent({
project: null,
connections,
onSubmit,
it("Has correct initial rendering", () => {
expect(wrapper.find(".tag-wrapper")).toHaveLength(0);
});
expect(wrapper.find(".tag-wrapper")).toHaveLength(0);
});
it("empty project should not call onChangeHandler on submission because of empty required values", () => {
const onSubmit = jest.fn();
const wrapper = createComponent({
project: null,
connections,
onSubmit,
it("Should not call onChangeHandler on submission because of empty required values", () => {
const form = wrapper.find("form");
form.simulate("submit");
expect(onSubmitHandler).not.toBeCalled();
});
const form = wrapper.find("form");
form.simulate("submit");
expect(onSubmit).not.toBeCalled();
});
});

Просмотреть файл

@ -1,9 +1,10 @@
import React from "react";
import Form from "react-jsonschema-form";
import Form, { FormValidation, ISubmitEvent } from "react-jsonschema-form";
import { IConnection, IProject } from "../../../../models/applicationState.js";
import ConnectionPicker from "../../common/connectionPicker";
import TagsInput from "../../common/tagsInput/tagsInput";
import CustomField from "../../common/customField";
import CustomFieldTemplate from "../../common/customFieldTemplate";
// tslint:disable-next-line:no-var-requires
const formSchema = require("./projectForm.json");
// tslint:disable-next-line:no-var-requires
@ -19,6 +20,7 @@ export interface IProjectFormProps extends React.Props<ProjectForm> {
project: IProject;
connections: IConnection[];
onSubmit: (project: IProject) => void;
onCancel?: () => void;
}
/**
@ -28,6 +30,7 @@ export interface IProjectFormProps extends React.Props<ProjectForm> {
* uiSchema - json UI schema of form
*/
export interface IProjectFormState {
classNames: string[];
formData: any;
formSchema: any;
uiSchema: any;
@ -57,19 +60,23 @@ export default class ProjectForm extends React.Component<IProjectFormProps, IPro
constructor(props, context) {
super(props, context);
this.state = {
classNames: ["needs-validation"],
uiSchema: { ...uiSchema },
formSchema: { ...formSchema },
formData: {
...this.props.project,
},
};
this.onFormSubmit = this.onFormSubmit.bind(this);
this.onFormCancel = this.onFormCancel.bind(this);
this.onFormValidate = this.onFormValidate.bind(this);
}
/**
* Updates state if project from properties has changed
* @param prevProps - previously set properties
*/
public componentDidUpdate(prevProps) {
public componentDidUpdate(prevProps: IProjectFormProps) {
if (prevProps.project !== this.props.project) {
this.setState({
formData: { ...this.props.project },
@ -80,22 +87,58 @@ export default class ProjectForm extends React.Component<IProjectFormProps, IPro
public render() {
return (
<Form
className={this.state.classNames.join(" ")}
showErrorList={false}
liveValidate={true}
noHtml5Validate={true}
FieldTemplate={CustomFieldTemplate}
validate={this.onFormValidate}
fields={this.fields}
schema={this.state.formSchema}
uiSchema={this.state.uiSchema}
formData={this.state.formData}
onSubmit={this.onFormSubmit}>
<div>
<button className="btn btn-success mr-1" type="submit">Save Project</button>
<button className="btn btn-secondary btn-cancel"
type="button"
onClick={this.onFormCancel}>Cancel</button>
</div>
</Form>
);
}
private onFormValidate(project: IProject, errors: FormValidation) {
if (Object.keys(project.sourceConnection).length === 0) {
errors.sourceConnection.addError("is a required property");
}
if (Object.keys(project.targetConnection).length === 0) {
errors.targetConnection.addError("is a required property");
}
if (this.state.classNames.indexOf("was-validated") === -1) {
this.setState({
classNames: [...this.state.classNames, "was-validated"],
});
}
return errors;
}
/**
* Called when form is submitted
*/
private onFormSubmit(args) {
private onFormSubmit(args: ISubmitEvent<IProject>) {
const project: IProject = {
...args.formData,
};
this.props.onSubmit(project);
}
private onFormCancel() {
if (this.props.onCancel) {
this.props.onCancel();
}
}
}

Просмотреть файл

@ -39,6 +39,7 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
}
this.onFormSubmit = this.onFormSubmit.bind(this);
this.onFormCancel = this.onFormCancel.bind(this);
}
public render() {
@ -49,7 +50,8 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
<ProjectForm
project={this.props.project}
connections={this.props.connections}
onSubmit={this.onFormSubmit} />
onSubmit={this.onFormSubmit}
onCancel={this.onFormCancel} />
</div>
</div>
);
@ -60,7 +62,17 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
...formData,
};
const isNew = !(!!projectToUpdate.id);
await this.props.actions.saveProject(projectToUpdate);
if (isNew) {
this.props.history.push(`/projects/${this.props.project.id}/edit`);
} else {
this.props.history.goBack();
}
}
private onFormCancel() {
this.props.history.goBack();
}
}

Просмотреть файл

@ -11,7 +11,7 @@ export const reducer = (state: IProject = null, action: AnyAction): IProject =>
case ActionTypes.LOAD_PROJECT_SUCCESS:
return { ...action.payload };
case ActionTypes.SAVE_PROJECT_SUCCESS:
if (state && state.id === action.payload.id) {
if (!state || state.id === action.payload.id) {
return { ...action.payload };
} else {
return state;