diff --git a/awx/ui/src/components/Lookup/Lookup.js b/awx/ui/src/components/Lookup/Lookup.js
index 80267ca1e4..20d8e65ca8 100644
--- a/awx/ui/src/components/Lookup/Lookup.js
+++ b/awx/ui/src/components/Lookup/Lookup.js
@@ -8,6 +8,7 @@ import {
oneOfType,
shape,
node,
+ object,
} from 'prop-types';
import { withRouter } from 'react-router-dom';
import { useField } from 'formik';
@@ -222,7 +223,7 @@ Lookup.propTypes = {
header: string,
modalDescription: oneOfType([string, node]),
onChange: func.isRequired,
- value: oneOfType([Item, arrayOf(Item)]),
+ value: oneOfType([Item, arrayOf(Item), object]),
multiple: bool,
required: bool,
onBlur: func,
diff --git a/awx/ui/src/components/Lookup/ProjectLookup.js b/awx/ui/src/components/Lookup/ProjectLookup.js
index 55749fcc25..3bd02d9289 100644
--- a/awx/ui/src/components/Lookup/ProjectLookup.js
+++ b/awx/ui/src/components/Lookup/ProjectLookup.js
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect } from 'react';
-import { node, string, func, bool } from 'prop-types';
+import { node, string, func, bool, object, oneOfType } from 'prop-types';
import { withRouter } from 'react-router-dom';
import { t } from '@lingui/macro';
import { FormGroup } from '@patternfly/react-core';
@@ -184,7 +184,7 @@ ProjectLookup.propTypes = {
onChange: func.isRequired,
required: bool,
tooltip: string,
- value: Project,
+ value: oneOfType([Project, object]),
isOverrideDisabled: bool,
validate: func,
fieldName: string,
diff --git a/awx/ui/src/components/RelatedTemplateList/RelatedTemplateList.js b/awx/ui/src/components/RelatedTemplateList/RelatedTemplateList.js
index 98f890ed12..f13208e4f0 100644
--- a/awx/ui/src/components/RelatedTemplateList/RelatedTemplateList.js
+++ b/awx/ui/src/components/RelatedTemplateList/RelatedTemplateList.js
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect } from 'react';
-import { useLocation } from 'react-router-dom';
+import { useParams, useLocation } from 'react-router-dom';
import { t, Plural } from '@lingui/macro';
import { Card } from '@patternfly/react-core';
@@ -14,7 +14,12 @@ import PaginatedTable, {
ToolbarDeleteButton,
getSearchableKeys,
} from 'components/PaginatedTable';
-import { getQSConfig, parseQueryString, mergeParams } from 'util/qs';
+import {
+ getQSConfig,
+ parseQueryString,
+ mergeParams,
+ encodeQueryString,
+} from 'util/qs';
import useWsTemplates from 'hooks/useWsTemplates';
import useSelected from 'hooks/useSelected';
import useExpanded from 'hooks/useExpanded';
@@ -29,7 +34,8 @@ const QS_CONFIG = getQSConfig('template', {
order_by: 'name',
});
-function RelatedTemplateList({ searchParams }) {
+function RelatedTemplateList({ searchParams, projectName = null }) {
+ const { id: projectId } = useParams();
const location = useLocation();
const { addToast, Toast, toastProps } = useToast();
@@ -122,9 +128,18 @@ function RelatedTemplateList({ searchParams }) {
const canAddJT =
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
- const addButton = (
-
- );
+ let linkTo = '';
+
+ if (projectName) {
+ const qs = encodeQueryString({
+ project_id: projectId,
+ project_name: projectName,
+ });
+ linkTo = `/templates/job_template/add/?${qs}`;
+ } else {
+ linkTo = '/templates/job_template/add';
+ }
+ const addButton = ;
const deleteDetailsRequests = relatedResourceDeleteRequests.template(
selected[0]
diff --git a/awx/ui/src/screens/Project/Project.js b/awx/ui/src/screens/Project/Project.js
index a3156ca151..0b2205c3d9 100644
--- a/awx/ui/src/screens/Project/Project.js
+++ b/awx/ui/src/screens/Project/Project.js
@@ -174,7 +174,12 @@ function Project({ setBreadcrumb }) {
)}
-
+
{project?.scm_type && project.scm_type !== '' && (
diff --git a/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.js b/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.js
index 0c91b6cea4..f4d0f4f49a 100644
--- a/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.js
+++ b/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.js
@@ -9,6 +9,32 @@ function JobTemplateAdd() {
const [formSubmitError, setFormSubmitError] = useState(null);
const history = useHistory();
+ const projectParams = {
+ project_id: null,
+ project_name: null,
+ };
+ history.location.search
+ .replace(/^\?/, '')
+ .split('&')
+ .map((s) => s.split('='))
+ .forEach(([key, val]) => {
+ if (!(key in projectParams)) {
+ return;
+ }
+ projectParams[key] = decodeURIComponent(val);
+ });
+
+ let projectValues = null;
+
+ if (
+ Object.values(projectParams).filter((item) => item !== null).length === 2
+ ) {
+ projectValues = {
+ id: projectParams.project_id,
+ name: projectParams.project_name,
+ };
+ }
+
const handleSubmit = async (values) => {
const {
labels,
@@ -35,7 +61,11 @@ function JobTemplateAdd() {
execution_environment: values.execution_environment?.id,
});
await Promise.all([
- submitLabels(id, values.project.summary_fields.organization.id, labels),
+ submitLabels(
+ id,
+ values.project.summary_fields?.organization.id,
+ labels
+ ),
submitInstanceGroups(id, instanceGroups),
submitCredentials(id, credentials),
]);
@@ -92,6 +122,7 @@ function JobTemplateAdd() {
handleCancel={handleCancel}
handleSubmit={handleSubmit}
submitError={formSubmitError}
+ projectValues={projectValues}
isOverrideDisabledLookup
/>
diff --git a/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.js b/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.js
index 91b0a623ed..46527f444a 100644
--- a/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.js
+++ b/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.js
@@ -257,4 +257,33 @@ describe('', () => {
});
expect(history.location.pathname).toEqual('/templates');
});
+
+ test('should parse and pre-fill project field from query params', async () => {
+ const history = createMemoryHistory({
+ initialEntries: [
+ '/templates/job_template/add/add?project_id=6&project_name=Demo%20Project',
+ ],
+ });
+ let wrapper;
+ await act(async () => {
+ wrapper = mountWithContexts(, {
+ context: { router: { history } },
+ });
+ });
+ await waitForElement(wrapper, 'EmptyStateBody', (el) => el.length === 0);
+ expect(wrapper.find('input#project').prop('value')).toEqual('Demo Project');
+ expect(ProjectsAPI.readPlaybooks).toBeCalledWith('6');
+ });
+
+ test('should not call ProjectsAPI.readPlaybooks if there is no project', async () => {
+ const history = createMemoryHistory({
+ initialEntries: ['/templates/job_template/add'],
+ });
+ await act(async () =>
+ mountWithContexts(, {
+ context: { router: history },
+ })
+ );
+ expect(ProjectsAPI.readPlaybooks).not.toBeCalled();
+ });
});
diff --git a/awx/ui/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.js b/awx/ui/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.js
index f0e02ebd12..eac6275959 100644
--- a/awx/ui/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.js
+++ b/awx/ui/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.js
@@ -392,60 +392,4 @@ describe('', () => {
'/templates/job_template/1/details'
);
});
- test('should not call ProjectsAPI.readPlaybooks if there is no project', async () => {
- const history = createMemoryHistory({});
- const noProjectTemplate = {
- id: 1,
- name: 'Foo',
- description: 'Bar',
- job_type: 'run',
- inventory: 2,
- playbook: 'Baz',
- type: 'job_template',
- forks: 0,
- limit: '',
- verbosity: '0',
- job_slice_count: 1,
- timeout: 0,
- job_tags: '',
- skip_tags: '',
- diff_mode: false,
- allow_callbacks: false,
- allow_simultaneous: false,
- use_fact_cache: false,
- host_config_key: '',
- summary_fields: {
- user_capabilities: {
- edit: true,
- },
- labels: {
- results: [
- { name: 'Sushi', id: 1 },
- { name: 'Major', id: 2 },
- ],
- },
- inventory: {
- id: 2,
- name: 'Demo Inventory',
- organization_id: 1,
- },
- credentials: [
- { id: 1, kind: 'cloud', name: 'Foo' },
- { id: 2, kind: 'ssh', name: 'Bar' },
- ],
- webhook_credential: {
- id: 7,
- name: 'webhook credential',
- kind: 'github_token',
- credential_type_id: 12,
- },
- },
- };
- await act(async () =>
- mountWithContexts(, {
- context: { router: { history } },
- })
- );
- expect(ProjectsAPI.readPlaybooks).not.toBeCalled();
- });
});
diff --git a/awx/ui/src/screens/Template/shared/JobTemplateForm.js b/awx/ui/src/screens/Template/shared/JobTemplateForm.js
index b7f39d05fa..a2cdc04f63 100644
--- a/awx/ui/src/screens/Template/shared/JobTemplateForm.js
+++ b/awx/ui/src/screens/Template/shared/JobTemplateForm.js
@@ -64,7 +64,7 @@ function JobTemplateForm({
Boolean(template?.host_config_key)
);
const [enableWebhooks, setEnableWebhooks] = useState(
- Boolean(template.webhook_service)
+ Boolean(template?.webhook_service)
);
const isMounted = useIsMounted();
const brandName = useBrandName();
@@ -646,7 +646,7 @@ JobTemplateForm.defaultProps = {
};
const FormikApp = withFormik({
- mapPropsToValues({ template = {} }) {
+ mapPropsToValues({ projectValues = {}, template = {} }) {
const {
summary_fields = {
labels: { results: [] },
@@ -684,7 +684,7 @@ const FormikApp = withFormik({
limit: template.limit || '',
name: template.name || '',
playbook: template.playbook || '',
- project: summary_fields?.project || null,
+ project: summary_fields?.project || projectValues || null,
scm_branch: template.scm_branch || '',
skip_tags: template.skip_tags || '',
timeout: template.timeout || 0,