diff --git a/awx/ui_next/src/api/models/JobTemplates.js b/awx/ui_next/src/api/models/JobTemplates.js index 03d2ab099a..5447817eaa 100644 --- a/awx/ui_next/src/api/models/JobTemplates.js +++ b/awx/ui_next/src/api/models/JobTemplates.js @@ -75,7 +75,7 @@ class JobTemplates extends SchedulesMixin( return this.http.get(`${this.baseUrl}${id}/survey_spec/`); } - updateSurvey(id, survey = null) { + updateSurvey(id, survey) { return this.http.post(`${this.baseUrl}${id}/survey_spec/`, survey); } diff --git a/awx/ui_next/src/api/models/WorkflowJobTemplates.js b/awx/ui_next/src/api/models/WorkflowJobTemplates.js index e369d71cc9..3074608796 100644 --- a/awx/ui_next/src/api/models/WorkflowJobTemplates.js +++ b/awx/ui_next/src/api/models/WorkflowJobTemplates.js @@ -49,7 +49,21 @@ class WorkflowJobTemplates extends SchedulesMixin(NotificationsMixin(Base)) { } readAccessList(id, params) { - return this.http.get(`${this.baseUrl}${id}/access_list/`, { params }); + return this.http.get(`${this.baseUrl}${id}/access_list/`, { + params, + }); + } + + readSurvey(id) { + return this.http.get(`${this.baseUrl}${id}/survey_spec/`); + } + + updateSurvey(id, survey) { + return this.http.post(`${this.baseUrl}${id}/survey_spec/`, survey); + } + + destroySurvey(id) { + return this.http.delete(`${this.baseUrl}${id}/survey_spec/`); } } diff --git a/awx/ui_next/src/screens/Template/TemplateSurvey.jsx b/awx/ui_next/src/screens/Template/TemplateSurvey.jsx index aeef6eaf52..dae02aa02a 100644 --- a/awx/ui_next/src/screens/Template/TemplateSurvey.jsx +++ b/awx/ui_next/src/screens/Template/TemplateSurvey.jsx @@ -1,8 +1,8 @@ import React, { useState, useEffect, useCallback } from 'react'; -import { Switch, Route } from 'react-router-dom'; +import { Switch, Route, useParams } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { JobTemplatesAPI } from '@api'; +import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '@api'; import ContentError from '@components/ContentError'; import AlertModal from '@components/AlertModal'; import ErrorDetail from '@components/ErrorDetail'; @@ -12,6 +12,7 @@ import { SurveyList, SurveyQuestionAdd, SurveyQuestionEdit } from './Survey'; function TemplateSurvey({ template, i18n }) { const [surveyEnabled, setSurveyEnabled] = useState(template.survey_enabled); + const { templateType } = useParams(); const { result: survey, request: fetchSurvey, @@ -20,9 +21,12 @@ function TemplateSurvey({ template, i18n }) { setValue: setSurvey, } = useRequest( useCallback(async () => { - const { data } = await JobTemplatesAPI.readSurvey(template.id); + const { data } = + templateType === 'workflow_job_template' + ? await WorkflowJobTemplatesAPI.readSurvey(template.id) + : await JobTemplatesAPI.readSurvey(template.id); return data; - }, [template.id]) + }, [template.id, templateType]) ); useEffect(() => { fetchSurvey(); @@ -31,10 +35,17 @@ function TemplateSurvey({ template, i18n }) { const { request: updateSurvey, error: updateError } = useRequest( useCallback( async updatedSurvey => { - await JobTemplatesAPI.updateSurvey(template.id, updatedSurvey); + if (templateType === 'workflow_job_template') { + await WorkflowJobTemplatesAPI.updateSurvey( + template.id, + updatedSurvey + ); + } else { + await JobTemplatesAPI.updateSurvey(template.id, updatedSurvey); + } setSurvey(updatedSurvey); }, - [template.id, setSurvey] + [template.id, setSurvey, templateType] ) ); const updateSurveySpec = spec => { diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx index 06a1ee2802..9438fda421 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx @@ -22,6 +22,7 @@ import { import WorkflowJobTemplateDetail from './WorkflowJobTemplateDetail'; import WorkflowJobTemplateEdit from './WorkflowJobTemplateEdit'; import { Visualizer } from './WorkflowJobTemplateVisualizer'; +import TemplateSurvey from './TemplateSurvey'; class WorkflowJobTemplate extends Component { constructor(props) { @@ -147,6 +148,10 @@ class WorkflowJobTemplate extends Component { name: i18n._(t`Completed Jobs`), link: `${match.url}/completed_jobs`, }); + tabsArray.push({ + name: i18n._(t`Survey`), + link: `${match.url}/survey`, + }); tabsArray.forEach((tab, n) => { tab.id = n; @@ -284,6 +289,11 @@ class WorkflowJobTemplate extends Component { )} /> )} + {template && ( + + + + )} ', () => { const mockMe = { is_super_user: true, is_system_auditor: false, }; + let wrapper; + let history; beforeAll(() => { WorkflowJobTemplatesAPI.readDetail.mockResolvedValue({ data: { @@ -57,16 +64,18 @@ describe('', () => { }, }, }); - }); - test('calls api to get workflow job template data', async () => { - const history = createMemoryHistory({ - initialEntries: ['/templates/workflow_job_template/1'], + OrganizationsAPI.read.mockResolvedValue({ + data: { results: [{ id: 1, name: 'Org Foo' }] }, + }); + }); + beforeEach(() => { + history = createMemoryHistory({ + initialEntries: ['/templates/workflow_job_template/1/details'], }); - let wrapper; act(() => { wrapper = mountWithContexts( ( {}} me={mockMe} /> )} @@ -75,22 +84,59 @@ describe('', () => { context: { router: { history, - route: { - location: history.location, - match: { - params: { id: 1 }, - }, - }, }, }, } ); }); + }); + + test('calls api to get workflow job template data', async () => { expect(wrapper.find('WorkflowJobTemplate').length).toBe(1); expect(WorkflowJobTemplatesAPI.readDetail).toBeCalledWith('1'); wrapper.update(); await sleep(0); expect(WorkflowJobTemplatesAPI.readWebhookKey).toBeCalledWith('1'); expect(CredentialsAPI.readDetail).toBeCalledWith(1234567); + expect(OrganizationsAPI.read).toBeCalledWith({ + page_size: 1, + role_level: 'notification_admin_role', + }); + }); + + test('renders proper tabs', async () => { + const tabs = [ + 'Details', + 'Access', + 'Notifications', + 'Schedules', + 'Visualizer', + 'Completed Jobs', + 'Survey', + ]; + waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0); + wrapper.update(); + wrapper.find('TabContainer').forEach(tc => { + tabs.forEach(t => expect(tc.prop(`aria-label=[${t}]`))); + }); + }); + + test('Does not render Notifications tab', async () => { + OrganizationsAPI.read.mockResolvedValue({ + data: { results: [] }, + }); + const tabs = [ + 'Details', + 'Access', + 'Schedules', + 'Visualizer', + 'Completed Jobs', + 'Survey', + ]; + waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0); + wrapper.update(); + wrapper.find('TabContainer').forEach(tc => { + tabs.forEach(t => expect(tc.prop(`aria-label=[${t}]`))); + }); }); });