Add playbook select and project field validation

This commit is contained in:
Marliana Lara
2019-08-23 10:24:35 -04:00
parent e19035079e
commit 156d03fa45
8 changed files with 350 additions and 157 deletions

View File

@@ -4,6 +4,12 @@ class Projects extends Base {
constructor(http) { constructor(http) {
super(http); super(http);
this.baseUrl = '/api/v2/projects/'; this.baseUrl = '/api/v2/projects/';
this.readPlaybooks = this.readPlaybooks.bind(this);
}
readPlaybooks(id) {
return this.http.get(`${this.baseUrl}${id}/playbooks/`);
} }
} }

View File

@@ -17,13 +17,16 @@ class AnsibleSelect extends React.Component {
} }
render() { render() {
const { value, data, i18n } = this.props; const { id, data, i18n, isValid, onBlur, value } = this.props;
return ( return (
<FormSelect <FormSelect
id={id}
value={value} value={value}
onChange={this.onSelectChange} onChange={this.onSelectChange}
onBlur={onBlur}
aria-label={i18n._(t`Select Input`)} aria-label={i18n._(t`Select Input`)}
isValid={isValid}
> >
{data.map(datum => ( {data.map(datum => (
<FormSelectOption <FormSelectOption
@@ -40,10 +43,15 @@ class AnsibleSelect extends React.Component {
AnsibleSelect.defaultProps = { AnsibleSelect.defaultProps = {
data: [], data: [],
isValid: true,
onBlur: () => {},
}; };
AnsibleSelect.propTypes = { AnsibleSelect.propTypes = {
data: PropTypes.arrayOf(PropTypes.object), data: PropTypes.arrayOf(PropTypes.object),
id: PropTypes.string.isRequired,
isValid: PropTypes.bool,
onBlur: PropTypes.func,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
}; };

View File

@@ -19,6 +19,7 @@ describe('<AnsibleSelect />', () => {
test('initially renders succesfully', async () => { test('initially renders succesfully', async () => {
mountWithContexts( mountWithContexts(
<AnsibleSelect <AnsibleSelect
id="bar"
value="foo" value="foo"
name="bar" name="bar"
onChange={() => {}} onChange={() => {}}
@@ -31,6 +32,7 @@ describe('<AnsibleSelect />', () => {
const spy = jest.spyOn(_AnsibleSelect.prototype, 'onSelectChange'); const spy = jest.spyOn(_AnsibleSelect.prototype, 'onSelectChange');
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<AnsibleSelect <AnsibleSelect
id="bar"
value="foo" value="foo"
name="bar" name="bar"
onChange={() => {}} onChange={() => {}}
@@ -45,6 +47,7 @@ describe('<AnsibleSelect />', () => {
test('Returns correct select options', () => { test('Returns correct select options', () => {
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<AnsibleSelect <AnsibleSelect
id="bar"
value="foo" value="foo"
name="bar" name="bar"
onChange={() => {}} onChange={() => {}}

View File

@@ -186,6 +186,7 @@ class Lookup extends React.Component {
columns, columns,
multiple, multiple,
name, name,
onBlur,
required, required,
i18n, i18n,
} = this.props; } = this.props;
@@ -209,7 +210,7 @@ class Lookup extends React.Component {
return ( return (
<Fragment> <Fragment>
<InputGroup> <InputGroup onBlur={onBlur}>
<Button <Button
aria-label="Search" aria-label="Search"
id={id} id={id}

View File

@@ -4,7 +4,7 @@ import { withRouter, Redirect } from 'react-router-dom';
import { CardBody } from '@patternfly/react-core'; import { CardBody } from '@patternfly/react-core';
import ContentError from '@components/ContentError'; import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading'; import ContentLoading from '@components/ContentLoading';
import { JobTemplatesAPI } from '@api'; import { JobTemplatesAPI, ProjectsAPI } from '@api';
import { JobTemplate } from '@types'; import { JobTemplate } from '@types';
import JobTemplateForm from '../shared/JobTemplateForm'; import JobTemplateForm from '../shared/JobTemplateForm';
@@ -21,6 +21,7 @@ class JobTemplateEdit extends Component {
contentError: null, contentError: null,
formSubmitError: null, formSubmitError: null,
relatedCredentials: [], relatedCredentials: [],
relatedProjectPlaybooks: [],
}; };
const { const {
@@ -31,6 +32,9 @@ class JobTemplateEdit extends Component {
this.handleCancel = this.handleCancel.bind(this); this.handleCancel = this.handleCancel.bind(this);
this.handleSubmit = this.handleSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this);
this.loadRelatedCredentials = this.loadRelatedCredentials.bind(this); this.loadRelatedCredentials = this.loadRelatedCredentials.bind(this);
this.loadRelatedProjectPlaybooks = this.loadRelatedProjectPlaybooks.bind(
this
);
this.submitLabels = this.submitLabels.bind(this); this.submitLabels = this.submitLabels.bind(this);
} }
@@ -41,15 +45,13 @@ class JobTemplateEdit extends Component {
async loadRelated() { async loadRelated() {
this.setState({ contentError: null, hasContentLoading: true }); this.setState({ contentError: null, hasContentLoading: true });
try { try {
const [ const [relatedCredentials, relatedProjectPlaybooks] = await Promise.all([
relatedCredentials,
// relatedProjectPlaybooks,
] = await Promise.all([
this.loadRelatedCredentials(), this.loadRelatedCredentials(),
// this.loadRelatedProjectPlaybooks(), this.loadRelatedProjectPlaybooks(),
]); ]);
this.setState({ this.setState({
relatedCredentials, relatedCredentials,
relatedProjectPlaybooks,
}); });
} catch (contentError) { } catch (contentError) {
this.setState({ contentError }); this.setState({ contentError });
@@ -86,7 +88,20 @@ class JobTemplateEdit extends Component {
} }
} }
async handleSubmit(values, newLabels = [], removedLabels = []) { async loadRelatedProjectPlaybooks() {
const {
template: { project },
} = this.props;
try {
const { data: playbooks = [] } = await ProjectsAPI.readPlaybooks(project);
this.setState({ relatedProjectPlaybooks: playbooks });
return playbooks;
} catch (err) {
throw err;
}
}
async handleSubmit(values) {
const { const {
template: { id }, template: { id },
history, history,
@@ -95,14 +110,16 @@ class JobTemplateEdit extends Component {
this.setState({ formSubmitError: null }); this.setState({ formSubmitError: null });
try { try {
await JobTemplatesAPI.update(id, { ...values }); await JobTemplatesAPI.update(id, { ...values });
await Promise.all([this.submitLabels(newLabels, removedLabels)]); await Promise.all([
this.submitLabels(values.newLabels, values.removedLabels),
]);
history.push(this.detailsUrl); history.push(this.detailsUrl);
} catch (formSubmitError) { } catch (formSubmitError) {
this.setState({ formSubmitError }); this.setState({ formSubmitError });
} }
} }
async submitLabels(newLabels, removedLabels) { async submitLabels(newLabels = [], removedLabels = []) {
const { const {
template: { id }, template: { id },
} = this.props; } = this.props;
@@ -131,7 +148,12 @@ class JobTemplateEdit extends Component {
render() { render() {
const { template } = this.props; const { template } = this.props;
const { contentError, formSubmitError, hasContentLoading } = this.state; const {
contentError,
formSubmitError,
hasContentLoading,
relatedProjectPlaybooks,
} = this.state;
const canEdit = template.summary_fields.user_capabilities.edit; const canEdit = template.summary_fields.user_capabilities.edit;
if (hasContentLoading) { if (hasContentLoading) {
@@ -152,6 +174,7 @@ class JobTemplateEdit extends Component {
template={template} template={template}
handleCancel={this.handleCancel} handleCancel={this.handleCancel}
handleSubmit={this.handleSubmit} handleSubmit={this.handleSubmit}
relatedProjectPlaybooks={relatedProjectPlaybooks}
/> />
{formSubmitError ? <div> error </div> : null} {formSubmitError ? <div> error </div> : null}
</CardBody> </CardBody>

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Formik, Field } from 'formik'; import { withFormik, Field } from 'formik';
import { Form, FormGroup, Tooltip, Card } from '@patternfly/react-core'; import { Form, FormGroup, Tooltip, Card } from '@patternfly/react-core';
import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons'; import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons';
import ContentError from '@components/ContentError'; import ContentError from '@components/ContentError';
@@ -18,7 +18,7 @@ import styled from 'styled-components';
import { JobTemplate } from '@types'; import { JobTemplate } from '@types';
import InventoriesLookup from './InventoriesLookup'; import InventoriesLookup from './InventoriesLookup';
import ProjectLookup from './ProjectLookup'; import ProjectLookup from './ProjectLookup';
import { LabelsAPI } from '@api'; import { LabelsAPI, ProjectsAPI } from '@api';
const QuestionCircleIcon = styled(PFQuestionCircleIcon)` const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
margin-left: 10px; margin-left: 10px;
@@ -47,6 +47,7 @@ class JobTemplateForm extends Component {
summary_fields: { summary_fields: {
inventory: null, inventory: null,
labels: { results: [] }, labels: { results: [] },
project: null,
}, },
}, },
}; };
@@ -61,14 +62,21 @@ class JobTemplateForm extends Component {
removedLabels: [], removedLabels: [],
project: props.template.summary_fields.project, project: props.template.summary_fields.project,
inventory: props.template.summary_fields.inventory, inventory: props.template.summary_fields.inventory,
relatedProjectPlaybooks: props.relatedProjectPlaybooks,
}; };
this.handleNewLabel = this.handleNewLabel.bind(this); this.handleNewLabel = this.handleNewLabel.bind(this);
this.loadLabels = this.loadLabels.bind(this); this.loadLabels = this.loadLabels.bind(this);
this.removeLabel = this.removeLabel.bind(this); this.removeLabel = this.removeLabel.bind(this);
this.handleProjectValidation = this.handleProjectValidation.bind(this);
this.loadRelatedProjectPlaybooks = this.loadRelatedProjectPlaybooks.bind(
this
);
} }
componentDidMount() { async componentDidMount() {
this.loadLabels(QSConfig); const { validateField } = this.props;
await this.loadLabels(QSConfig);
validateField('project');
} }
async loadLabels(QueryConfig) { async loadLabels(QueryConfig) {
@@ -101,7 +109,7 @@ class JobTemplateForm extends Component {
handleNewLabel(label) { handleNewLabel(label) {
const { newLabels } = this.state; const { newLabels } = this.state;
const { template } = this.props; const { template, setFieldValue } = this.props;
const isIncluded = newLabels.some(newLabel => newLabel.name === label.name); const isIncluded = newLabels.some(newLabel => newLabel.name === label.name);
if (isIncluded) { if (isIncluded) {
const filteredLabels = newLabels.filter( const filteredLabels = newLabels.filter(
@@ -109,6 +117,13 @@ class JobTemplateForm extends Component {
); );
this.setState({ newLabels: filteredLabels }); this.setState({ newLabels: filteredLabels });
} else if (typeof label === 'string') { } else if (typeof label === 'string') {
setFieldValue('newLabels', [
...newLabels,
{
name: label,
organization: template.summary_fields.inventory.organization_id,
},
]);
this.setState({ this.setState({
newLabels: [ newLabels: [
...newLabels, ...newLabels,
@@ -119,6 +134,10 @@ class JobTemplateForm extends Component {
], ],
}); });
} else { } else {
setFieldValue('newLabels', [
...newLabels,
{ name: label.name, associate: true, id: label.id },
]);
this.setState({ this.setState({
newLabels: [ newLabels: [
...newLabels, ...newLabels,
@@ -130,13 +149,20 @@ class JobTemplateForm extends Component {
removeLabel(label) { removeLabel(label) {
const { removedLabels, newLabels } = this.state; const { removedLabels, newLabels } = this.state;
const { template } = this.props; const { template, setFieldValue } = this.props;
const isAssociatedLabel = template.summary_fields.labels.results.some( const isAssociatedLabel = template.summary_fields.labels.results.some(
tempLabel => tempLabel.id === label.id tempLabel => tempLabel.id === label.id
); );
if (isAssociatedLabel) { if (isAssociatedLabel) {
setFieldValue(
'removedLabels',
removedLabels.concat({
disassociate: true,
id: label.id,
})
);
this.setState({ this.setState({
removedLabels: removedLabels.concat({ removedLabels: removedLabels.concat({
disassociate: true, disassociate: true,
@@ -147,10 +173,34 @@ class JobTemplateForm extends Component {
const filteredLabels = newLabels.filter( const filteredLabels = newLabels.filter(
newLabel => newLabel.name !== label.name newLabel => newLabel.name !== label.name
); );
setFieldValue('newLabels', filteredLabels);
this.setState({ newLabels: filteredLabels }); this.setState({ newLabels: filteredLabels });
} }
} }
async loadRelatedProjectPlaybooks(project) {
try {
const { data: playbooks = [] } = await ProjectsAPI.readPlaybooks(project);
this.setState({ relatedProjectPlaybooks: playbooks });
} catch (contentError) {
this.setState({ contentError });
}
}
handleProjectValidation() {
const { i18n, touched } = this.props;
const { project } = this.state;
return () => {
if (!project && touched.project) {
return i18n._(t`Select a value for this field`);
}
if (project && project.status === 'never updated') {
return i18n._(t`This project needs to be updated`);
}
return undefined;
};
}
render() { render() {
const { const {
loadedLabels, loadedLabels,
@@ -158,10 +208,15 @@ class JobTemplateForm extends Component {
hasContentLoading, hasContentLoading,
inventory, inventory,
project, project,
newLabels, relatedProjectPlaybooks = [],
removedLabels,
} = this.state; } = this.state;
const { handleCancel, handleSubmit, i18n, template } = this.props; const {
handleCancel,
handleSubmit,
handleBlur,
i18n,
template,
} = this.props;
const jobTypeOptions = [ const jobTypeOptions = [
{ {
value: '', value: '',
@@ -177,6 +232,28 @@ class JobTemplateForm extends Component {
isDisabled: false, isDisabled: false,
}, },
]; ];
const playbookOptions = relatedProjectPlaybooks
.map(playbook => {
return {
value: playbook,
key: playbook,
label: playbook,
isDisabled: false,
};
})
.reduce(
(arr, playbook) => {
return arr.concat(playbook);
},
[
{
value: '',
key: '',
label: i18n._(t`Choose a playbook`),
isDisabled: false,
},
]
);
if (hasContentLoading) { if (hasContentLoading) {
return ( return (
@@ -195,129 +272,180 @@ class JobTemplateForm extends Component {
} }
return ( return (
<Formik <Form autoComplete="off" onSubmit={handleSubmit}>
initialValues={{ <FormRow>
name: template.name, <FormField
description: template.description, id="template-name"
job_type: template.job_type, name="name"
inventory: template.inventory || '', type="text"
project: template.project || '', label={i18n._(t`Name`)}
playbook: template.playbook, validate={required(null, i18n)}
labels: template.summary_fields.labels.results, isRequired
}} />
onSubmit={values => { <FormField
handleSubmit(values, newLabels, removedLabels); id="template-description"
}} name="description"
render={formik => ( type="text"
<Form autoComplete="off" onSubmit={formik.handleSubmit}> label={i18n._(t`Description`)}
<FormRow> />
<FormField <Field
id="template-name" name="job_type"
name="name" validate={required(null, i18n)}
type="text" onBlur={handleBlur}
label={i18n._(t`Name`)} render={({ form, field }) => {
validate={required(null, i18n)} const isValid =
isRequired form && (!form.touched[field.name] || !form.errors[field.name]);
/> return (
<FormField <FormGroup
id="template-description" fieldId="template-job-type"
name="description" helperTextInvalid={form.errors.job_type}
type="text" isRequired
label={i18n._(t`Description`)} isValid={isValid}
/> label={i18n._(t`Job Type`)}
<Field
name="job_type"
validate={required(null, i18n)}
render={({ field }) => (
<FormGroup
fieldId="template-job-type"
isRequired
label={i18n._(t`Job Type`)}
>
<Tooltip
position="right"
content={i18n._(t`For job templates, select run to execute
the playbook. Select check to only check playbook syntax,
test environment setup, and report problems without
executing the playbook.`)}
>
<QuestionCircleIcon />
</Tooltip>
<AnsibleSelect data={jobTypeOptions} {...field} />
</FormGroup>
)}
/>
<Field
name="inventory"
validate={required(null, i18n)}
render={({ form }) => (
<InventoriesLookup
value={inventory}
tooltip={i18n._(t`Select the inventory containing the hosts
you want this job to manage.`)}
onChange={value => {
form.setFieldValue('inventory', value.id);
this.setState({ inventory: value });
}}
required
/>
)}
/>
<Field
name="project"
validate={required(null, i18n)}
render={({ form }) => (
<ProjectLookup
value={project}
tooltip={i18n._(t`Select the project containing the playbook
you want this job to execute.`)}
onChange={value => {
form.setFieldValue('project', value.id);
this.setState({ project: value });
}}
required
/>
)}
/>
<FormField
id="template-playbook"
name="playbook"
type="text"
label={i18n._(t`Playbook`)}
tooltip={i18n._(
t`Select the playbook to be executed by this job.`
)}
isRequired
validate={required(null, i18n)}
/>
</FormRow>
<FormRow>
<FormGroup label={i18n._(t`Labels`)} fieldId="template-labels">
<Tooltip
position="right"
content={i18n._(
t`Optional labels that describe this job template, such as 'dev' or 'test'. Labels can be used to group and filter job templates and completed jobs.`
)}
> >
<QuestionCircleIcon /> <Tooltip
</Tooltip> position="right"
<MultiSelect content={i18n._(t`For job templates, select run to execute
onAddNewItem={this.handleNewLabel} the playbook. Select check to only check playbook syntax,
onRemoveItem={this.removeLabel} test environment setup, and report problems without
associatedItems={template.summary_fields.labels.results} executing the playbook.`)}
options={loadedLabels} >
<QuestionCircleIcon />
</Tooltip>
<AnsibleSelect
isValid={isValid}
id="job_type"
data={jobTypeOptions}
{...field}
/>
</FormGroup>
);
}}
/>
<Field
name="inventory"
validate={required(null, i18n)}
render={({ form }) => (
<InventoriesLookup
value={inventory}
tooltip={i18n._(t`Select the inventory containing the hosts
you want this job to manage.`)}
onChange={value => {
form.setFieldValue('inventory', value.id);
this.setState({ inventory: value });
}}
required
/>
)}
/>
<Field
name="project"
validate={this.handleProjectValidation()}
render={({ form }) => {
const isValid = form && !form.errors.project;
return (
<ProjectLookup
helperTextInvalid={form.errors.project}
isValid={isValid}
value={project}
onBlur={handleBlur}
tooltip={i18n._(t`Select the project containing the playbook
you want this job to execute.`)}
onChange={value => {
this.loadRelatedProjectPlaybooks(value.id);
form.setFieldValue('project', value.id);
form.setFieldTouched('project');
this.setState({ project: value });
}}
required
/> />
</FormGroup> );
</FormRow> }}
<FormActionGroup />
onCancel={handleCancel} <Field
onSubmit={formik.handleSubmit} name="playbook"
validate={required(i18n._(t`Select a value for this field`), i18n)}
onBlur={handleBlur}
render={({ field, form }) => {
const isValid =
form && (!form.touched[field.name] || !form.errors[field.name]);
return (
<FormGroup
fieldId="template-playbook"
helperTextInvalid={form.errors.playbook}
isRequired
isValid={isValid}
label={i18n._(t`Playbook`)}
>
<Tooltip
position="right"
content={i18n._(
t`Select the playbook to be executed by this job.`
)}
>
<QuestionCircleIcon />
</Tooltip>
<AnsibleSelect
id="playbook"
data={playbookOptions}
isValid={isValid}
form={form}
{...field}
/>
</FormGroup>
);
}}
/>
</FormRow>
<FormRow>
<FormGroup label={i18n._(t`Labels`)} fieldId="template-labels">
<Tooltip
position="right"
content={i18n._(
t`Optional labels that describe this job template, such as 'dev' or 'test'. Labels can be used to group and filter job templates and completed jobs.`
)}
>
<QuestionCircleIcon />
</Tooltip>
<MultiSelect
onAddNewItem={this.handleNewLabel}
onRemoveItem={this.removeLabel}
associatedItems={template.summary_fields.labels.results}
options={loadedLabels}
/> />
</Form> </FormGroup>
)} </FormRow>
/> <FormActionGroup onCancel={handleCancel} onSubmit={handleSubmit} />
</Form>
); );
} }
} }
const FormikApp = withFormik({
mapPropsToValues(props) {
const { template = {} } = props;
const {
name = '',
description = '',
job_type = '',
inventory = '',
playbook = '',
project = '',
summary_fields = { labels: { results: [] } },
} = { ...template };
return {
name: name || '',
description: description || '',
job_type: job_type || '',
inventory: inventory || '',
project: project || '',
playbook: playbook || '',
labels: summary_fields.labels.results,
};
},
handleSubmit: (values, bag) => bag.props.handleSubmit(values),
})(JobTemplateForm);
export { JobTemplateForm as _JobTemplateForm }; export { JobTemplateForm as _JobTemplateForm };
export default withI18n()(withRouter(JobTemplateForm)); export default withI18n()(withRouter(FormikApp));

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import { sleep } from '@testUtils/testUtils'; import { sleep } from '@testUtils/testUtils';
import { shallow } from 'enzyme';
import JobTemplateForm, { _JobTemplateForm } from './JobTemplateForm'; import JobTemplateForm, { _JobTemplateForm } from './JobTemplateForm';
import { LabelsAPI } from '@api'; import { LabelsAPI } from '@api';
@@ -41,12 +42,15 @@ describe('<JobTemplateForm />', () => {
test('initially renders successfully', async done => { test('initially renders successfully', async done => {
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<JobTemplateForm shallow(
template={mockData} <JobTemplateForm
handleSubmit={jest.fn()} template={mockData}
handleCancel={jest.fn()} handleSubmit={jest.fn()}
/> handleCancel={jest.fn()}
/>
).get(0)
); );
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0); await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
expect(LabelsAPI.read).toHaveBeenCalled(); expect(LabelsAPI.read).toHaveBeenCalled();
expect( expect(
@@ -60,11 +64,13 @@ describe('<JobTemplateForm />', () => {
test('should update form values on input changes', async done => { test('should update form values on input changes', async done => {
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<JobTemplateForm shallow(
template={mockData} <JobTemplateForm
handleSubmit={jest.fn()} template={mockData}
handleCancel={jest.fn()} handleSubmit={jest.fn()}
/> handleCancel={jest.fn()}
/>
).get(0)
); );
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0); await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
const form = wrapper.find('Formik'); const form = wrapper.find('Formik');
@@ -90,7 +96,7 @@ describe('<JobTemplateForm />', () => {
name: 'project', name: 'project',
}); });
expect(form.state('values').project).toEqual(4); expect(form.state('values').project).toEqual(4);
wrapper.find('input#template-playbook').simulate('change', { wrapper.find('AnsibleSelect[name="playbook"]').simulate('change', {
target: { value: 'new baz type', name: 'playbook' }, target: { value: 'new baz type', name: 'playbook' },
}); });
expect(form.state('values').playbook).toEqual('new baz type'); expect(form.state('values').playbook).toEqual('new baz type');

View File

@@ -17,12 +17,23 @@ const loadProjects = async params => ProjectsAPI.read(params);
class ProjectLookup extends React.Component { class ProjectLookup extends React.Component {
render() { render() {
const { value, tooltip, onChange, required, i18n } = this.props; const {
helperTextInvalid,
i18n,
isValid,
onChange,
required,
tooltip,
value,
onBlur,
} = this.props;
return ( return (
<FormGroup <FormGroup
fieldId="project-lookup" fieldId="project"
helperTextInvalid={helperTextInvalid}
isRequired={required} isRequired={required}
isValid={isValid}
label={i18n._(t`Project`)} label={i18n._(t`Project`)}
> >
{tooltip && ( {tooltip && (
@@ -31,10 +42,11 @@ class ProjectLookup extends React.Component {
</Tooltip> </Tooltip>
)} )}
<Lookup <Lookup
id="project-lookup" id="project"
lookupHeader={i18n._(t`Projects`)} lookupHeader={i18n._(t`Projects`)}
name="project" name="project"
value={value} value={value}
onBlur={onBlur}
onLookupSave={onChange} onLookupSave={onChange}
getItems={loadProjects} getItems={loadProjects}
required={required} required={required}
@@ -47,15 +59,21 @@ class ProjectLookup extends React.Component {
ProjectLookup.propTypes = { ProjectLookup.propTypes = {
value: Project, value: Project,
tooltip: string, helperTextInvalid: string,
isValid: bool,
onBlur: func,
onChange: func.isRequired, onChange: func.isRequired,
required: bool, required: bool,
tooltip: string,
}; };
ProjectLookup.defaultProps = { ProjectLookup.defaultProps = {
value: null, helperTextInvalid: '',
tooltip: '', isValid: true,
required: false, required: false,
tooltip: '',
value: null,
onBlur: () => {},
}; };
export default withI18n()(ProjectLookup); export default withI18n()(ProjectLookup);