diff --git a/awx/ui_next/src/components/FieldWithPrompt/FieldWithPrompt.jsx b/awx/ui_next/src/components/FieldWithPrompt/FieldWithPrompt.jsx
index b0d27ccc6e..d505049cf0 100644
--- a/awx/ui_next/src/components/FieldWithPrompt/FieldWithPrompt.jsx
+++ b/awx/ui_next/src/components/FieldWithPrompt/FieldWithPrompt.jsx
@@ -23,6 +23,7 @@ function FieldWithPrompt({
promptId,
promptName,
tooltip,
+ isDisabled,
}) {
return (
@@ -39,6 +40,7 @@ function FieldWithPrompt({
{tooltip && }
diff --git a/awx/ui_next/src/components/Lookup/CredentialLookup.jsx b/awx/ui_next/src/components/Lookup/CredentialLookup.jsx
index c67e0087c6..a31fd8fd7d 100644
--- a/awx/ui_next/src/components/Lookup/CredentialLookup.jsx
+++ b/awx/ui_next/src/components/Lookup/CredentialLookup.jsx
@@ -33,6 +33,7 @@ function CredentialLookup({
history,
i18n,
tooltip,
+ isDisabled,
}) {
const {
result: { count, credentials, relatedSearchableKeys, searchableKeys },
@@ -108,6 +109,7 @@ function CredentialLookup({
onChange={onChange}
required={required}
qsConfig={QS_CONFIG}
+ isDisabled={isDisabled}
renderOptionsList={({ state, dispatch, canDelete }) => (
actionsResponse.data.actions?.GET[key].filterable),
+ canEdit: Boolean(actionsResponse.data.actions.POST),
};
}, [history.location]),
- { inventories: [], count: 0, relatedSearchableKeys: [], searchableKeys: [] }
+ {
+ inventories: [],
+ count: 0,
+ relatedSearchableKeys: [],
+ searchableKeys: [],
+ canEdit: false,
+ }
);
useEffect(() => {
fetchInventories();
}, [fetchInventories]);
- return (
+ return isPromptableField ? (
+ <>
+
+ (
+ dispatch({ type: 'SELECT_ITEM', item })}
+ deselectItem={item => dispatch({ type: 'DESELECT_ITEM', item })}
+ />
+ )}
+ />
+
+
+ >
+ ) : (
<>
(
+ props.isDisabled ? 'var(--pf-global--disabled-color--300)' : null};
`;
function Lookup(props) {
const {
@@ -43,6 +45,7 @@ function Lookup(props) {
renderOptionsList,
history,
i18n,
+ isDisabled,
} = props;
const [state, dispatch] = useReducer(
@@ -88,7 +91,8 @@ function Lookup(props) {
};
const { isModalOpen, selectedItems } = state;
- const canDelete = !required || (multiple && value.length > 1);
+ const canDelete =
+ (!required || (multiple && value.length > 1)) && !isDisabled;
let items = [];
if (multiple) {
items = value;
@@ -103,11 +107,11 @@ function Lookup(props) {
id={id}
onClick={() => dispatch({ type: 'TOGGLE_MODAL' })}
variant={ButtonVariant.control}
- isDisabled={isLoading}
+ isDisabled={isLoading || isDisabled}
>
-
+
{items.map(item =>
renderItemChip({
diff --git a/awx/ui_next/src/components/Lookup/ProjectLookup.jsx b/awx/ui_next/src/components/Lookup/ProjectLookup.jsx
index 5c8ec16dee..3858c6a7fb 100644
--- a/awx/ui_next/src/components/Lookup/ProjectLookup.jsx
+++ b/awx/ui_next/src/components/Lookup/ProjectLookup.jsx
@@ -32,7 +32,7 @@ function ProjectLookup({
history,
}) {
const {
- result: { projects, count, relatedSearchableKeys, searchableKeys },
+ result: { projects, count, relatedSearchableKeys, searchableKeys, canEdit },
request: fetchProjects,
error,
isLoading,
@@ -55,6 +55,7 @@ function ProjectLookup({
searchableKeys: Object.keys(
actionsResponse.data.actions?.GET || {}
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
+ canEdit: Boolean(actionsResponse.data.actions.POST),
};
}, [history.location.search, autocomplete]),
{
@@ -62,6 +63,7 @@ function ProjectLookup({
projects: [],
relatedSearchableKeys: [],
searchableKeys: [],
+ canEdit: false,
}
);
@@ -87,6 +89,7 @@ function ProjectLookup({
onChange={onChange}
required={required}
isLoading={isLoading}
+ isDisabled={!canEdit}
qsConfig={QS_CONFIG}
renderOptionsList={({ state, dispatch, canDelete }) => (
{
+ const newOptions = [];
if (value !== selections && options.length) {
- const syncedValue = value.map(item =>
- options.find(i => i.id === item.id)
- );
+ const syncedValue = value.map(item => {
+ const match = options.find(i => {
+ return i.id === item.id;
+ });
+ if (!match) {
+ newOptions.push(item);
+ }
+ return match || item;
+ });
setSelections(syncedValue);
}
+ if (newOptions.length > 0) {
+ setOptions(options.concat(newOptions));
+ }
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [value, options]);
@@ -27,7 +37,6 @@ export default function useSyncedSelectValue(value, onChange) {
onChange(selections.concat(item));
}
};
-
return {
selections: options.length ? addToStringToObjects(selections) : [],
onSelect,
diff --git a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx
index 1e48523d85..a51bbb355a 100644
--- a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx
+++ b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx
@@ -4,13 +4,11 @@ import { withRouter, Redirect } from 'react-router-dom';
import { CardBody } from '../../../components/Card';
import ContentError from '../../../components/ContentError';
import ContentLoading from '../../../components/ContentLoading';
-import { JobTemplatesAPI, ProjectsAPI } from '../../../api';
+import { JobTemplatesAPI } from '../../../api';
import { JobTemplate } from '../../../types';
import { getAddedAndRemoved } from '../../../util/lists';
import JobTemplateForm from '../shared/JobTemplateForm';
-const loadRelatedProjectPlaybooks = async project =>
- ProjectsAPI.readPlaybooks(project);
class JobTemplateEdit extends Component {
static propTypes = {
template: JobTemplate.isRequired,
@@ -43,17 +41,8 @@ class JobTemplateEdit extends Component {
}
async loadRelated() {
- const {
- template: { project },
- } = this.props;
this.setState({ contentError: null, hasContentLoading: true });
try {
- if (project) {
- const { data: playbook = [] } = await loadRelatedProjectPlaybooks(
- project
- );
- this.setState({ relatedProjectPlaybooks: playbook });
- }
const [relatedCredentials] = await this.loadRelatedCredentials();
this.setState({
relatedCredentials,
diff --git a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx
index 61b1467bcf..7790989aa9 100644
--- a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx
+++ b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx
@@ -12,6 +12,7 @@ import {
JobTemplatesAPI,
LabelsAPI,
ProjectsAPI,
+ InventoriesAPI,
} from '../../../api';
import JobTemplateEdit from './JobTemplateEdit';
@@ -181,6 +182,12 @@ JobTemplatesAPI.readCredentials.mockResolvedValue({
ProjectsAPI.readPlaybooks.mockResolvedValue({
data: mockRelatedProjectPlaybooks,
});
+InventoriesAPI.readOptions.mockResolvedValue({
+ data: { actions: { GET: {}, POST: {} } },
+});
+ProjectsAPI.readOptions.mockResolvedValue({
+ data: { actions: { GET: {}, POST: {} } },
+});
LabelsAPI.read.mockResolvedValue({ data: { results: [] } });
CredentialsAPI.read.mockResolvedValue({
data: {
diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx
index a7d540a7a7..ae68594205 100644
--- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx
+++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx
@@ -39,7 +39,7 @@ import {
ProjectLookup,
MultiCredentialsLookup,
} from '../../../components/Lookup';
-import { JobTemplatesAPI, ProjectsAPI } from '../../../api';
+import { JobTemplatesAPI } from '../../../api';
import LabelSelect from './LabelSelect';
import PlaybookSelect from './PlaybookSelect';
import WebhookSubForm from './WebhookSubForm';
@@ -100,18 +100,6 @@ function JobTemplateForm({
'webhook_credential'
);
- const {
- request: fetchProject,
- error: projectContentError,
- contentLoading: hasProjectLoading,
- } = useRequest(
- useCallback(async () => {
- if (template?.project) {
- await ProjectsAPI.readDetail(template?.project);
- }
- }, [template])
- );
-
const {
request: loadRelatedInstanceGroups,
error: instanceGroupError,
@@ -127,10 +115,6 @@ function JobTemplateForm({
}, [setFieldValue, template])
);
- useEffect(() => {
- fetchProject();
- }, [fetchProject]);
-
useEffect(() => {
loadRelatedInstanceGroups();
}, [loadRelatedInstanceGroups]);
@@ -204,16 +188,12 @@ function JobTemplateForm({
callbackUrl = `${origin}${path}`;
}
- if (instanceGroupLoading || hasProjectLoading) {
+ if (instanceGroupLoading) {
return ;
}
- if (contentError || instanceGroupError || projectContentError) {
- return (
-
- );
+ if (contentError || instanceGroupError) {
+ return ;
}
return (
@@ -254,17 +234,15 @@ function JobTemplateForm({
}}
/>
-
+ <>
inventoryHelpers.setTouched()}
onChange={value => {
inventoryHelpers.setValue(value ? value.id : null);
@@ -283,7 +261,7 @@ function JobTemplateForm({
{inventoryMeta.error}
)}
-
+ >
projectHelpers.setTouched()}
diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx
index a373fefe37..f6c6c3e486 100644
--- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx
+++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx
@@ -14,6 +14,7 @@ import {
ProjectsAPI,
CredentialsAPI,
CredentialTypesAPI,
+ InventoriesAPI,
} from '../../../api';
jest.mock('../../../api');
@@ -111,14 +112,23 @@ describe('', () => {
JobTemplatesAPI.updateWebhookKey.mockReturnValue({
data: { webhook_key: 'webhook key' },
});
- ProjectsAPI.readPlaybooks.mockReturnValue({
- data: ['debug.yml'],
+ JobTemplatesAPI.updateWebhookKey.mockReturnValue({
+ data: { webhook_key: 'webhook key' },
});
ProjectsAPI.readDetail.mockReturnValue({
name: 'foo',
id: 1,
allow_override: true,
});
+ ProjectsAPI.readPlaybooks.mockReturnValue({
+ data: ['debug.yml'],
+ });
+ InventoriesAPI.readOptions.mockResolvedValue({
+ data: { actions: { GET: {}, POST: {} } },
+ });
+ ProjectsAPI.readOptions.mockResolvedValue({
+ data: { actions: { GET: {}, POST: {} } },
+ });
});
afterEach(() => {
diff --git a/awx/ui_next/src/screens/Template/shared/PlaybookSelect.jsx b/awx/ui_next/src/screens/Template/shared/PlaybookSelect.jsx
index 78fd6e28f9..5a66cc0a78 100644
--- a/awx/ui_next/src/screens/Template/shared/PlaybookSelect.jsx
+++ b/awx/ui_next/src/screens/Template/shared/PlaybookSelect.jsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useEffect } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import { number, string, oneOfType } from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
@@ -7,6 +7,7 @@ import { ProjectsAPI } from '../../../api';
import useRequest from '../../../util/useRequest';
function PlaybookSelect({ projectId, isValid, field, onBlur, onError, i18n }) {
+ const [isDisabled, setIsDisabled] = useState(false);
const {
result: options,
request: fetchOptions,
@@ -18,6 +19,7 @@ function PlaybookSelect({ projectId, isValid, field, onBlur, onError, i18n }) {
return [];
}
const { data } = await ProjectsAPI.readPlaybooks(projectId);
+
const opts = (data || []).map(playbook => ({
value: playbook,
key: playbook,
@@ -42,18 +44,30 @@ function PlaybookSelect({ projectId, isValid, field, onBlur, onError, i18n }) {
useEffect(() => {
if (error) {
- onError(error);
+ if (error.response.status === 403) {
+ setIsDisabled(true);
+ } else {
+ onError(error);
+ }
}
}, [error, onError]);
+ const isDisabledData = [
+ {
+ value: field.value || '',
+ label: field.value || '',
+ key: 1,
+ isDisabled: true,
+ },
+ ];
return (
);
}
diff --git a/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx
index 4ea77786f9..bb13418aac 100644
--- a/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx
+++ b/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx
@@ -110,23 +110,21 @@ function WorkflowJobTemplateForm({
value={organizationField.value}
isValid={!organizationMeta.error}
/>
-
-
+ <>
inventoryHelpers.setTouched()}
onChange={value => {
inventoryHelpers.setValue(value);
}}
- required={askInventoryOnLaunchField.value}
+ required={!askInventoryOnLaunchField.value}
touched={inventoryMeta.touched}
error={inventoryMeta.error}
/>
@@ -139,8 +137,7 @@ function WorkflowJobTemplateForm({
{inventoryMeta.error}
)}
-
-
+ >
', () => {
let wrapper;
@@ -71,6 +75,15 @@ describe('', () => {
{ id: 2, name: 'Bar' },
],
});
+ CredentialTypesAPI.read.mockResolvedValue({
+ data: { results: [{ id: 1 }] },
+ });
+ InventoriesAPI.readOptions.mockResolvedValue({
+ data: { actions: { GET: {}, POST: {} } },
+ });
+ ProjectsAPI.readOptions.mockResolvedValue({
+ data: { actions: { GET: {}, POST: {} } },
+ });
history = createMemoryHistory({
initialEntries: ['/templates/workflow_job_template/6/edit'],