this.setState({ contentError: err })}
+ />
);
}}
@@ -416,15 +330,19 @@ class JobTemplateForm extends Component {
- {
+ setFieldValue('newLabels', newLabels);
+ }}
+ onRemovedLabelsChange={removedLabels => {
+ setFieldValue('removedLabels', removedLabels);
+ }}
+ onError={err => this.setState({ contentError: err })}
/>
@@ -485,8 +403,8 @@ class JobTemplateForm extends Component {
min="1"
label={i18n._(t`Job Slicing`)}
tooltip={i18n._(t`Divide the work done by this job template
- into the specified number of job slices, each running the
- same tasks against a portion of the inventory.`)}
+ into the specified number of job slices, each running the
+ same tasks against a portion of the inventory.`)}
/>
}
@@ -615,19 +530,15 @@ class JobTemplateForm extends Component {
id="option-concurrent"
name="allow_simultaneous"
label={i18n._(t`Concurrent Jobs`)}
- tooltip={i18n._(
- t`If enabled, simultaneous runs of this job template will
- be allowed.`
- )}
+ tooltip={i18n._(t`If enabled, simultaneous runs of this job
+ template will be allowed.`)}
/>
bag.props.handleSubmit(values),
diff --git a/awx/ui_next/src/screens/Template/shared/LabelSelect.jsx b/awx/ui_next/src/screens/Template/shared/LabelSelect.jsx
new file mode 100644
index 0000000000..4959a138e8
--- /dev/null
+++ b/awx/ui_next/src/screens/Template/shared/LabelSelect.jsx
@@ -0,0 +1,110 @@
+import React, { useState, useEffect } from 'react';
+import { func, arrayOf, number, shape, string } from 'prop-types';
+import MultiSelect from '@components/MultiSelect';
+import { LabelsAPI } from '@api';
+
+async function loadLabelOptions(setLabels, onError) {
+ let labels;
+ try {
+ const { data } = await LabelsAPI.read({
+ page: 1,
+ page_size: 200,
+ order_by: 'name',
+ });
+ labels = data.results;
+ setLabels(labels);
+ if (data.next && data.next.includes('page=2')) {
+ const {
+ data: { results },
+ } = await LabelsAPI.read({
+ page: 2,
+ page_size: 200,
+ order_by: 'name',
+ });
+ labels = labels.concat(results);
+ }
+ setLabels(labels);
+ } catch (err) {
+ onError(err);
+ }
+}
+
+function LabelSelect({
+ initialValues,
+ organizationId,
+ onNewLabelsChange,
+ onRemovedLabelsChange,
+ onError,
+}) {
+ const [options, setOptions] = useState([]);
+ // TODO: move newLabels into a prop?
+ const [newLabels, setNewLabels] = useState([]);
+ const [removedLabels, setRemovedLabels] = useState([]);
+ useEffect(() => {
+ loadLabelOptions(setOptions, onError);
+ }, []);
+
+ const handleNewLabel = label => {
+ const isIncluded = newLabels.some(l => l.name === label.name);
+ if (isIncluded) {
+ const filteredLabels = newLabels.filter(
+ newLabel => newLabel.name !== label
+ );
+ setNewLabels(filteredLabels);
+ } else {
+ const updatedNewLabels = newLabels.concat({
+ name: label.name,
+ associate: true,
+ id: label.id,
+ // TODO: can this be null? what happens if inventory > org id changes?
+ // organization: organizationId,
+ });
+ setNewLabels(updatedNewLabels);
+ onNewLabelsChange(updatedNewLabels);
+ }
+ };
+
+ const handleRemoveLabel = label => {
+ const isAssociatedLabel = initialValues.some(
+ l => l.id === label.id
+ );
+ if (isAssociatedLabel) {
+ const updatedRemovedLabels = removedLabels.concat({
+ id: label.id,
+ disassociate: true,
+ });
+ setRemovedLabels(updatedRemovedLabels);
+ onRemovedLabelsChange(updatedRemovedLabels);
+ } else {
+ const filteredLabels = newLabels.filter(l => l.name !== label.name);
+ setNewLabels(filteredLabels);
+ onNewLabelsChange(filteredLabels);
+ }
+ };
+
+ return (
+
+ );
+}
+LabelSelect.propTypes = {
+ initialValues: arrayOf(
+ shape({
+ id: number.isRequired,
+ name: string.isRequired,
+ })
+ ).isRequired,
+ organizationId: number,
+ onNewLabelsChange: func.isRequired,
+ onRemovedLabelsChange: func.isRequired,
+ onError: func.isRequired,
+};
+LabelSelect.defaultProps = {
+ organizationId: null,
+};
+
+export default LabelSelect;
diff --git a/awx/ui_next/src/screens/Template/shared/PlaybookSelect.jsx b/awx/ui_next/src/screens/Template/shared/PlaybookSelect.jsx
index 7b9c89f110..3e7d03c37e 100644
--- a/awx/ui_next/src/screens/Template/shared/PlaybookSelect.jsx
+++ b/awx/ui_next/src/screens/Template/shared/PlaybookSelect.jsx
@@ -8,6 +8,9 @@ import { ProjectsAPI } from '@api';
function PlaybookSelect({ projectId, isValid, form, field, onError, i18n }) {
const [options, setOptions] = useState([]);
useEffect(() => {
+ if (!projectId) {
+ return;
+ }
(async () => {
try {
const { data } = await ProjectsAPI.readPlaybooks(projectId);