mirror of
https://github.com/ansible/awx.git
synced 2026-03-20 02:17:37 -02:30
Adds Multiselect functionality to labels on JTs
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import { encodeQueryString } from '@util/qs';
|
import {
|
||||||
|
encodeQueryString
|
||||||
|
} from '@util/qs';
|
||||||
|
|
||||||
const defaultHttp = axios.create({
|
const defaultHttp = axios.create({
|
||||||
xsrfCookieName: 'csrftoken',
|
xsrfCookieName: 'csrftoken',
|
||||||
@@ -25,7 +27,9 @@ class Base {
|
|||||||
}
|
}
|
||||||
|
|
||||||
read(params) {
|
read(params) {
|
||||||
return this.http.get(this.baseUrl, { params });
|
return this.http.get(this.baseUrl, {
|
||||||
|
params
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
readDetail(id) {
|
readDetail(id) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import InstanceGroups from './models/InstanceGroups';
|
|||||||
import Inventories from './models/Inventories';
|
import Inventories from './models/Inventories';
|
||||||
import JobTemplates from './models/JobTemplates';
|
import JobTemplates from './models/JobTemplates';
|
||||||
import Jobs from './models/Jobs';
|
import Jobs from './models/Jobs';
|
||||||
|
import Labels from './models/Labels';
|
||||||
import Me from './models/Me';
|
import Me from './models/Me';
|
||||||
import Organizations from './models/Organizations';
|
import Organizations from './models/Organizations';
|
||||||
import Root from './models/Root';
|
import Root from './models/Root';
|
||||||
@@ -17,6 +18,7 @@ const InstanceGroupsAPI = new InstanceGroups();
|
|||||||
const InventoriesAPI = new Inventories();
|
const InventoriesAPI = new Inventories();
|
||||||
const JobTemplatesAPI = new JobTemplates();
|
const JobTemplatesAPI = new JobTemplates();
|
||||||
const JobsAPI = new Jobs();
|
const JobsAPI = new Jobs();
|
||||||
|
const LabelsAPI = new Labels();
|
||||||
const MeAPI = new Me();
|
const MeAPI = new Me();
|
||||||
const OrganizationsAPI = new Organizations();
|
const OrganizationsAPI = new Organizations();
|
||||||
const RootAPI = new Root();
|
const RootAPI = new Root();
|
||||||
@@ -32,6 +34,7 @@ export {
|
|||||||
InventoriesAPI,
|
InventoriesAPI,
|
||||||
JobTemplatesAPI,
|
JobTemplatesAPI,
|
||||||
JobsAPI,
|
JobsAPI,
|
||||||
|
LabelsAPI,
|
||||||
MeAPI,
|
MeAPI,
|
||||||
OrganizationsAPI,
|
OrganizationsAPI,
|
||||||
RootAPI,
|
RootAPI,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class JobTemplates extends InstanceGroupsMixin(Base) {
|
|||||||
|
|
||||||
this.launch = this.launch.bind(this);
|
this.launch = this.launch.bind(this);
|
||||||
this.readLaunch = this.readLaunch.bind(this);
|
this.readLaunch = this.readLaunch.bind(this);
|
||||||
|
this.updateLabels = this.updateLabels.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
launch(id, data) {
|
launch(id, data) {
|
||||||
@@ -17,6 +18,10 @@ class JobTemplates extends InstanceGroupsMixin(Base) {
|
|||||||
readLaunch(id) {
|
readLaunch(id) {
|
||||||
return this.http.get(`${this.baseUrl}${id}/launch/`);
|
return this.http.get(`${this.baseUrl}${id}/launch/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateLabels(id, data) {
|
||||||
|
return this.http.post(`${this.baseUrl}${id}/labels/`, data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default JobTemplates;
|
export default JobTemplates;
|
||||||
|
|||||||
10
awx/ui_next/src/api/models/Labels.js
Normal file
10
awx/ui_next/src/api/models/Labels.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import Base from '../Base';
|
||||||
|
|
||||||
|
class Labels extends Base {
|
||||||
|
constructor(http) {
|
||||||
|
super(http);
|
||||||
|
this.baseUrl = '/api/v2/labels/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Labels;
|
||||||
202
awx/ui_next/src/components/MultiSelect/MultiSelect.jsx
Normal file
202
awx/ui_next/src/components/MultiSelect/MultiSelect.jsx
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Chip, ChipGroup } from '@components/Chip';
|
||||||
|
import {
|
||||||
|
Dropdown as PFDropdown,
|
||||||
|
DropdownItem,
|
||||||
|
TextInput as PFTextInput,
|
||||||
|
DropdownToggle,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const InputGroup = styled.div`
|
||||||
|
border: 1px solid black;
|
||||||
|
margin-top: 2px;
|
||||||
|
`;
|
||||||
|
const TextInput = styled(PFTextInput)`
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 8px;
|
||||||
|
`;
|
||||||
|
const Dropdown = styled(PFDropdown)`
|
||||||
|
width: 100%;
|
||||||
|
.pf-c-dropdown__toggle.pf-m-plain {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
display: block;
|
||||||
|
.pf-c-dropdown__menu {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
&& button[disabled] {
|
||||||
|
color: var(--pf-c-button--m-plain--Color);
|
||||||
|
pointer-events: initial;
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: var(--pf-global--disabled-color--200);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
class MultiSelect extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
associatedItems: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
})
|
||||||
|
).isRequired,
|
||||||
|
onAddNewItem: PropTypes.func.isRequired,
|
||||||
|
onRemoveItem: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.myRef = React.createRef();
|
||||||
|
this.state = {
|
||||||
|
input: '',
|
||||||
|
chipItems: [],
|
||||||
|
isExpanded: false,
|
||||||
|
};
|
||||||
|
this.handleAddItem = this.handleAddItem.bind(this);
|
||||||
|
this.handleInputChange = this.handleInputChange.bind(this);
|
||||||
|
this.handleSelection = this.handleSelection.bind(this);
|
||||||
|
this.removeChip = this.removeChip.bind(this);
|
||||||
|
this.handleClick = this.handleClick.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.renderChips();
|
||||||
|
document.addEventListener('mousedown', this.handleClick, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick(e, option) {
|
||||||
|
if (this.node && this.node.contains(e.target)) {
|
||||||
|
if (option) {
|
||||||
|
this.handleSelection(e, option);
|
||||||
|
}
|
||||||
|
this.setState({ isExpanded: true });
|
||||||
|
} else {
|
||||||
|
this.setState({ isExpanded: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderChips() {
|
||||||
|
const { associatedItems } = this.props;
|
||||||
|
const items = associatedItems.map(item => ({
|
||||||
|
name: item.name,
|
||||||
|
id: item.id,
|
||||||
|
organization: item.organization,
|
||||||
|
}));
|
||||||
|
this.setState({
|
||||||
|
chipItems: items,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSelection(e, item) {
|
||||||
|
const { chipItems } = this.state;
|
||||||
|
const { onAddNewItem } = this.props;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
chipItems: chipItems.concat({ name: item.name, id: item.id }),
|
||||||
|
});
|
||||||
|
onAddNewItem(item);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAddItem(event) {
|
||||||
|
const { input, chipItems } = this.state;
|
||||||
|
const { onAddNewItem } = this.props;
|
||||||
|
const newChip = { name: input, id: Math.random() };
|
||||||
|
if (event.key === 'Tab') {
|
||||||
|
this.setState({
|
||||||
|
chipItems: chipItems.concat(newChip),
|
||||||
|
isExpanded: false,
|
||||||
|
input: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
onAddNewItem(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInputChange(e) {
|
||||||
|
this.setState({ input: e, isExpanded: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
removeChip(e, item) {
|
||||||
|
const { onRemoveItem } = this.props;
|
||||||
|
const { chipItems } = this.state;
|
||||||
|
const chips = chipItems.filter(chip => chip.name !== item.name);
|
||||||
|
|
||||||
|
this.setState({ chipItems: chips });
|
||||||
|
onRemoveItem(item);
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { options } = this.props;
|
||||||
|
const { chipItems, input, isExpanded } = this.state;
|
||||||
|
|
||||||
|
const list = options.map(option => (
|
||||||
|
<Fragment key={option.id}>
|
||||||
|
{option.name.includes(input) ? (
|
||||||
|
<DropdownItem
|
||||||
|
component="button"
|
||||||
|
isDisabled={chipItems.some(item => item.id === option.id)}
|
||||||
|
value={option.name}
|
||||||
|
onClick={e => {
|
||||||
|
this.handleClick(e, option);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option.name}
|
||||||
|
</DropdownItem>
|
||||||
|
) : null}
|
||||||
|
</Fragment>
|
||||||
|
));
|
||||||
|
|
||||||
|
const chips = (
|
||||||
|
<ChipGroup>
|
||||||
|
{chipItems &&
|
||||||
|
chipItems.map(item => (
|
||||||
|
<Chip
|
||||||
|
key={item.id}
|
||||||
|
onClick={e => {
|
||||||
|
this.removeChip(e, item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</ChipGroup>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<InputGroup>
|
||||||
|
<div
|
||||||
|
ref={node => {
|
||||||
|
this.node = node;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
aria-label="labels"
|
||||||
|
value={input}
|
||||||
|
onClick={() => this.setState({ isExpanded: true })}
|
||||||
|
onChange={this.handleInputChange}
|
||||||
|
onKeyDown={this.handleAddItem}
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
type="button"
|
||||||
|
isPlain
|
||||||
|
value={chipItems}
|
||||||
|
toggle={<DropdownToggle isPlain>Labels</DropdownToggle>}
|
||||||
|
// Above is not rendered but is a required prop from Patternfly
|
||||||
|
isOpen={isExpanded}
|
||||||
|
dropdownItems={list}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div css="margin: 10px">{chips}</div>
|
||||||
|
</InputGroup>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default MultiSelect;
|
||||||
4
awx/ui_next/src/components/MultiSelect/index.js
Normal file
4
awx/ui_next/src/components/MultiSelect/index.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export {
|
||||||
|
default
|
||||||
|
}
|
||||||
|
from './MultiSelect';
|
||||||
@@ -13,6 +13,11 @@ describe('<JobTemplateAdd />', () => {
|
|||||||
name: '',
|
name: '',
|
||||||
playbook: '',
|
playbook: '',
|
||||||
project: '',
|
project: '',
|
||||||
|
summary_fields: {
|
||||||
|
user_capabilities: {
|
||||||
|
edit: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -21,14 +21,29 @@ class JobTemplateEdit extends Component {
|
|||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleSubmit(values) {
|
async handleSubmit(values, newLabels, removedLabels) {
|
||||||
const {
|
const {
|
||||||
template: { id, type },
|
template: { id, type },
|
||||||
history,
|
history,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const disassociatedLabels = removedLabels
|
||||||
|
? removedLabels.forEach(removedLabel =>
|
||||||
|
JobTemplatesAPI.updateLabels(id, removedLabel)
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
const associatedLabels = newLabels
|
||||||
|
? newLabels.forEach(newLabel =>
|
||||||
|
JobTemplatesAPI.updateLabels(id, newLabel)
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await JobTemplatesAPI.update(id, { ...values });
|
await Promise.all([
|
||||||
|
JobTemplatesAPI.update(id, { ...values }),
|
||||||
|
disassociatedLabels,
|
||||||
|
associatedLabels,
|
||||||
|
]);
|
||||||
history.push(`/templates/${type}/${id}/details`);
|
history.push(`/templates/${type}/${id}/details`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.setState({ error });
|
this.setState({ error });
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ describe('<JobTemplateEdit />', () => {
|
|||||||
user_capabilities: {
|
user_capabilities: {
|
||||||
edit: true,
|
edit: true,
|
||||||
},
|
},
|
||||||
|
labels: {
|
||||||
|
results: [{ name: 'Sushi', id: 1 }, { name: 'Major', id: 2 }],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,17 @@ 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 { Formik, Field } from 'formik';
|
||||||
import { Form, FormGroup, Tooltip } from '@patternfly/react-core';
|
import {
|
||||||
|
Form,
|
||||||
|
FormGroup,
|
||||||
|
Tooltip,
|
||||||
|
PageSection,
|
||||||
|
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 AnsibleSelect from '@components/AnsibleSelect';
|
import AnsibleSelect from '@components/AnsibleSelect';
|
||||||
|
import MultiSelect from '@components/MultiSelect';
|
||||||
import FormActionGroup from '@components/FormActionGroup';
|
import FormActionGroup from '@components/FormActionGroup';
|
||||||
import FormField from '@components/FormField';
|
import FormField from '@components/FormField';
|
||||||
import FormRow from '@components/FormRow';
|
import FormRow from '@components/FormRow';
|
||||||
@@ -14,10 +22,16 @@ import { required } from '@util/validators';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { JobTemplate } from '@types';
|
import { JobTemplate } from '@types';
|
||||||
import InventoriesLookup from './InventoriesLookup';
|
import InventoriesLookup from './InventoriesLookup';
|
||||||
|
import { LabelsAPI } from '@api';
|
||||||
|
|
||||||
const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
|
const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
`;
|
`;
|
||||||
|
const QSConfig = {
|
||||||
|
page: 1,
|
||||||
|
page_size: 200,
|
||||||
|
order_by: 'name',
|
||||||
|
};
|
||||||
|
|
||||||
class JobTemplateForm extends Component {
|
class JobTemplateForm extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@@ -36,22 +50,107 @@ class JobTemplateForm extends Component {
|
|||||||
playbook: '',
|
playbook: '',
|
||||||
summary_fields: {
|
summary_fields: {
|
||||||
inventory: null,
|
inventory: null,
|
||||||
|
labels: { results: [] },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
hasContentLoading: true,
|
||||||
|
contentError: false,
|
||||||
|
loadedLabels: [],
|
||||||
|
newLabels: [],
|
||||||
|
removedLabels: [],
|
||||||
inventory: props.template.summary_fields.inventory,
|
inventory: props.template.summary_fields.inventory,
|
||||||
};
|
};
|
||||||
|
this.handleNewLabel = this.handleNewLabel.bind(this);
|
||||||
|
this.loadLabels = this.loadLabels.bind(this);
|
||||||
|
this.disassociateLabel = this.disassociateLabel.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.loadLabels(QSConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadLabels(QueryConfig) {
|
||||||
|
const { loadedLabels } = this.state;
|
||||||
|
this.setState({ contentError: null, hasContentLoading: true });
|
||||||
|
try {
|
||||||
|
const { data } = await LabelsAPI.read(QueryConfig);
|
||||||
|
const labels = [...data.results];
|
||||||
|
this.setState({ loadedLabels: loadedLabels.concat(labels) });
|
||||||
|
if (data.next && data.next.includes('page=2')) {
|
||||||
|
this.loadLabels({
|
||||||
|
page: 2,
|
||||||
|
page_size: 200,
|
||||||
|
order_by: 'name',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.setState({ contentError: err });
|
||||||
|
} finally {
|
||||||
|
this.setState({ hasContentLoading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNewLabel(label) {
|
||||||
|
const { newLabels } = this.state;
|
||||||
|
const { template } = this.props;
|
||||||
|
const isIncluded = newLabels.some(newLabel => newLabel.name === label.name);
|
||||||
|
if (isIncluded) {
|
||||||
|
const filteredLabels = newLabels.filter(
|
||||||
|
newLabel => newLabel.name !== label
|
||||||
|
);
|
||||||
|
this.setState({ newLabels: filteredLabels });
|
||||||
|
} else if (typeof label === 'string') {
|
||||||
|
this.setState({
|
||||||
|
newLabels: [
|
||||||
|
...newLabels,
|
||||||
|
{
|
||||||
|
name: label,
|
||||||
|
organization: template.summary_fields.inventory.organization_id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
newLabels: [...newLabels, { associate: true, id: label.id }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disassociateLabel(label) {
|
||||||
|
const { removedLabels, newLabels } = this.state;
|
||||||
|
const isNewCreatedLabel = newLabels.some(
|
||||||
|
newLabel => newLabel === label.name
|
||||||
|
);
|
||||||
|
if (isNewCreatedLabel) {
|
||||||
|
const filteredLabels = newLabels.filter(
|
||||||
|
newLabel => newLabel !== label.name
|
||||||
|
);
|
||||||
|
this.setState({ newLabels: filteredLabels });
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
removedLabels: removedLabels.concat({
|
||||||
|
disassociate: true,
|
||||||
|
id: label.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {
|
||||||
|
loadedLabels,
|
||||||
|
contentError,
|
||||||
|
hasContentLoading,
|
||||||
|
inventory,
|
||||||
|
newLabels,
|
||||||
|
removedLabels,
|
||||||
|
} = this.state;
|
||||||
const { handleCancel, handleSubmit, i18n, template } = this.props;
|
const { handleCancel, handleSubmit, i18n, template } = this.props;
|
||||||
const { inventory } = this.state;
|
|
||||||
|
|
||||||
const jobTypeOptions = [
|
const jobTypeOptions = [
|
||||||
{
|
{
|
||||||
value: '',
|
value: '',
|
||||||
@@ -68,6 +167,15 @@ class JobTemplateForm extends Component {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (!hasContentLoading && contentError) {
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card className="awx-c-card">
|
||||||
|
<ContentError error={contentError} />
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
@@ -77,8 +185,11 @@ class JobTemplateForm extends Component {
|
|||||||
inventory: template.inventory,
|
inventory: template.inventory,
|
||||||
project: template.project,
|
project: template.project,
|
||||||
playbook: template.playbook,
|
playbook: template.playbook,
|
||||||
|
labels: template.summary_fields.labels.results,
|
||||||
|
}}
|
||||||
|
onSubmit={values => {
|
||||||
|
handleSubmit(values, newLabels, removedLabels);
|
||||||
}}
|
}}
|
||||||
onSubmit={handleSubmit}
|
|
||||||
render={formik => (
|
render={formik => (
|
||||||
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||||
<FormRow>
|
<FormRow>
|
||||||
@@ -156,9 +267,31 @@ class JobTemplateForm extends Component {
|
|||||||
validate={required(null, i18n)}
|
validate={required(null, i18n)}
|
||||||
/>
|
/>
|
||||||
</FormRow>
|
</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>
|
||||||
|
<Field
|
||||||
|
render={() => (
|
||||||
|
<MultiSelect
|
||||||
|
onAddNewItem={this.handleNewLabel}
|
||||||
|
onRemoveItem={this.disassociateLabel}
|
||||||
|
associatedItems={template.summary_fields.labels.results}
|
||||||
|
options={loadedLabels}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</FormRow>
|
||||||
<FormActionGroup
|
<FormActionGroup
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
onSubmit={formik.handleSubmit}
|
onSubmit={values => handleSubmit(values)}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
@@ -166,5 +299,5 @@ class JobTemplateForm extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export { JobTemplateForm as _JobTemplateForm };
|
||||||
export default withI18n()(withRouter(JobTemplateForm));
|
export default withI18n()(withRouter(JobTemplateForm));
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ describe('<JobTemplateForm />', () => {
|
|||||||
id: 2,
|
id: 2,
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
},
|
},
|
||||||
|
labels: { results: [{ name: 'Sushi', id: 1 }, { name: 'Major', id: 2 }] },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user