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,