Global Form UI Cleanup (#413)
* 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:
Родитель
f573b20d68
Коммит
5c2a60111f
|
@ -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;
|
||||
|
|
Загрузка…
Ссылка в новой задаче