diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.jsx index 3656c73c02..e47e1fcf95 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.jsx @@ -1,13 +1,22 @@ -import React, { useState } from 'react'; +import React, { useState, useCallback, useEffect } from 'react'; import { useHistory } from 'react-router-dom'; import { Card, PageSection } from '@patternfly/react-core'; import { CardBody } from '../../../components/Card'; -import { WorkflowJobTemplatesAPI, OrganizationsAPI } from '../../../api'; +import { + WorkflowJobTemplatesAPI, + OrganizationsAPI, + UsersAPI, +} from '../../../api'; import WorkflowJobTemplateForm from '../shared/WorkflowJobTemplateForm'; +import { useConfig } from '../../../contexts/Config'; +import useRequest from '../../../util/useRequest'; +import ContentError from '../../../components/ContentError'; +import ContentLoading from '../../../components/ContentLoading'; function WorkflowJobTemplateAdd() { + const { me = {} } = useConfig(); const history = useHistory(); const [formSubmitError, setFormSubmitError] = useState(null); @@ -60,6 +69,33 @@ function WorkflowJobTemplateAdd() { history.push(`/templates`); }; + const { + isLoading, + request: fetchUserRole, + result: { orgAdminResults, isOrgAdmin }, + error: contentError, + } = useRequest( + useCallback(async () => { + const { + data: { results, count }, + } = await UsersAPI.readAdminOfOrganizations(me?.id); + return { isOrgAdmin: count > 0, orgAdminResults: results }; + }, [me.id]), + { isOrgAdmin: false, orgAdminResults: null } + ); + + useEffect(() => { + fetchUserRole(); + }, [fetchUserRole]); + + if (contentError) { + return ; + } + + if (isLoading || !orgAdminResults) { + return ; + } + return ( @@ -68,6 +104,7 @@ function WorkflowJobTemplateAdd() { handleCancel={handleCancel} handleSubmit={handleSubmit} submitError={formSubmitError} + isOrgAdmin={isOrgAdmin} /> diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.test.jsx index 2cfd5e0093..6b09435fab 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.test.jsx @@ -7,8 +7,12 @@ import { OrganizationsAPI, LabelsAPI, ExecutionEnvironmentsAPI, + UsersAPI, } from '../../../api'; -import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import { + mountWithContexts, + waitForElement, +} from '../../../../testUtils/enzymeHelpers'; import WorkflowJobTemplateAdd from './WorkflowJobTemplateAdd'; @@ -17,6 +21,7 @@ jest.mock('../../../api/models/Organizations'); jest.mock('../../../api/models/Labels'); jest.mock('../../../api/models/Inventories'); jest.mock('../../../api/models/ExecutionEnvironments'); +jest.mock('../../../api/models/Users'); describe('', () => { let wrapper; @@ -40,6 +45,10 @@ describe('', () => { data: { results: [{ id: 1, name: 'Foo', image: 'localhost.com' }] }, }); + UsersAPI.readAdminOfOrganizations.mockResolvedValue({ + data: { count: 0, results: [] }, + }); + await act(async () => { history = createMemoryHistory({ initialEntries: ['/templates/workflow_job_template/add'], @@ -67,6 +76,7 @@ describe('', () => { } ); }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); }); }); afterEach(async () => { diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.jsx index dd47118316..96a0be82df 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.jsx @@ -1,12 +1,21 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; import { CardBody } from '../../../components/Card'; import { getAddedAndRemoved } from '../../../util/lists'; -import { WorkflowJobTemplatesAPI, OrganizationsAPI } from '../../../api'; +import { + WorkflowJobTemplatesAPI, + OrganizationsAPI, + UsersAPI, +} from '../../../api'; import { WorkflowJobTemplateForm } from '../shared'; +import { useConfig } from '../../../contexts/Config'; +import useRequest from '../../../util/useRequest'; +import ContentError from '../../../components/ContentError'; +import ContentLoading from '../../../components/ContentLoading'; function WorkflowJobTemplateEdit({ template }) { + const { me = {} } = useConfig(); const history = useHistory(); const [formSubmitError, setFormSubmitError] = useState(null); @@ -69,6 +78,33 @@ function WorkflowJobTemplateEdit({ template }) { history.push(`/templates/workflow_job_template/${template.id}/details`); }; + const { + isLoading, + request: fetchUserRole, + result: { orgAdminResults, isOrgAdmin }, + error: contentError, + } = useRequest( + useCallback(async () => { + const { + data: { results, count }, + } = await UsersAPI.readAdminOfOrganizations(me?.id); + return { isOrgAdmin: count > 0, orgAdminResults: results }; + }, [me.id]), + { isOrgAdmin: false, orgAdminResults: null } + ); + + useEffect(() => { + fetchUserRole(); + }, [fetchUserRole]); + + if (contentError) { + return ; + } + + if (isLoading || !orgAdminResults) { + return ; + } + return ( ); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.test.jsx index 1f760bb302..f85e2f213f 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.test.jsx @@ -7,8 +7,12 @@ import { OrganizationsAPI, LabelsAPI, ExecutionEnvironmentsAPI, + UsersAPI, } from '../../../api'; -import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import { + mountWithContexts, + waitForElement, +} from '../../../../testUtils/enzymeHelpers'; import WorkflowJobTemplateEdit from './WorkflowJobTemplateEdit'; jest.mock('../../../api/models/WorkflowJobTemplates'); @@ -16,6 +20,7 @@ jest.mock('../../../api/models/Labels'); jest.mock('../../../api/models/Organizations'); jest.mock('../../../api/models/Inventories'); jest.mock('../../../api/models/ExecutionEnvironments'); +jest.mock('../../../api/models/Users'); const mockTemplate = { id: 6, @@ -74,6 +79,10 @@ describe('', () => { }, }); + UsersAPI.readAdminOfOrganizations.mockResolvedValue({ + data: { count: 1, results: [{ id: 1 }] }, + }); + await act(async () => { history = createMemoryHistory({ initialEntries: ['/templates/workflow_job_template/6/edit'], @@ -95,6 +104,7 @@ describe('', () => { }, } ); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); }); }); @@ -241,6 +251,7 @@ describe('', () => { } ); }); + await waitForElement(newWrapper, 'ContentLoading', el => el.length === 0); OrganizationsAPI.read.mockRejectedValue({ response: { config: { diff --git a/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx index 01fd9378c0..a1891d26f0 100644 --- a/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx @@ -43,6 +43,7 @@ function WorkflowJobTemplateForm({ handleCancel, i18n, submitError, + isOrgAdmin, }) { const { setFieldValue } = useFormikContext(); const [enableWebhooks, setEnableWebhooks] = useState( @@ -55,7 +56,9 @@ function WorkflowJobTemplateForm({ ); const [labelsField, , labelsHelpers] = useField('labels'); const [limitField, limitMeta, limitHelpers] = useField('limit'); - const [organizationField, organizationMeta] = useField('organization'); + const [organizationField, organizationMeta, organizationHelpers] = useField( + 'organization' + ); const [scmField, , scmHelpers] = useField('scm_branch'); const [, webhookServiceMeta, webhookServiceHelpers] = useField( 'webhook_service' @@ -119,9 +122,14 @@ function WorkflowJobTemplateForm({ /> organizationHelpers.setTouched()} onChange={onOrganizationChange} value={organizationField.value} - isValid={!organizationMeta.error} + touched={organizationMeta.touched} + error={organizationMeta.error} + required={isOrgAdmin} + autoPopulate={isOrgAdmin} /> <> ', () => { expect(wrapper.length).toBe(1); }); + test('organization is a required field for organization admins', async () => { + await act(async () => { + wrapper = mountWithContexts( + ( + + )} + />, + { + context: { + router: { + history, + route: { + location: history.location, + match: { params: { id: 6 } }, + }, + }, + }, + } + ); + }); + + wrapper.update(); + expect( + wrapper.find('FormGroup[label="Organization"]').prop('isRequired') + ).toBeTruthy(); + }); + test('all the fields render successfully', () => { const fields = [ 'FormField[name="name"]', @@ -140,6 +174,9 @@ describe('', () => { expect(wrapper.find(`${field}`).length).toBe(1); }; fields.map((field, index) => assertField(field, index)); + expect( + wrapper.find('FormGroup[label="Organization"]').prop('isRequired') + ).toBeFalsy(); }); test('changing inputs should update values', async () => {