diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx
index 19ca4d3467..0640dab906 100644
--- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx
+++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx
@@ -1,4 +1,4 @@
-import React, { Component } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react';
@@ -15,6 +15,8 @@ import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading';
import AnsibleSelect from '@components/AnsibleSelect';
import { TagMultiSelect } from '@components/MultiSelect';
+import useRequest from '@util/useRequest';
+
import FormActionGroup from '@components/FormActionGroup';
import FormField, {
CheckboxField,
@@ -40,287 +42,362 @@ import { JobTemplatesAPI, ProjectsAPI } from '@api';
import LabelSelect from './LabelSelect';
import PlaybookSelect from './PlaybookSelect';
-class JobTemplateForm extends Component {
- static propTypes = {
- template: JobTemplate,
- handleCancel: PropTypes.func.isRequired,
- handleSubmit: PropTypes.func.isRequired,
- submitError: PropTypes.shape({}),
- };
+function JobTemplateForm({
+ template,
+ validateField,
+ handleCancel,
+ handleSubmit,
+ handleBlur,
+ setFieldValue,
+ submitError,
+ i18n,
+ touched,
+}) {
+ const [contentError, setContentError] = useState(false);
+ const [project, setProject] = useState(null);
+ const [inventory, setInventory] = useState(
+ template?.summary_fields?.inventory
+ );
+ const [allowCallbacks, setAllowCallbacks] = useState(
+ Boolean(template?.host_config_key)
+ );
- static defaultProps = {
- template: {
- name: '',
- description: '',
- job_type: 'run',
- inventory: undefined,
- project: undefined,
- playbook: '',
- summary_fields: {
- inventory: null,
- labels: { results: [] },
- project: null,
- credentials: [],
- },
- isNew: true,
- },
- submitError: null,
- };
-
- constructor(props) {
- super(props);
- this.state = {
- hasContentLoading: true,
- contentError: false,
- project: props.template.summary_fields.project,
- inventory: props.template.summary_fields.inventory,
- allowCallbacks: !!props.template.host_config_key,
- };
- this.handleProjectValidation = this.handleProjectValidation.bind(this);
- this.loadRelatedInstanceGroups = this.loadRelatedInstanceGroups.bind(this);
- this.handleProjectUpdate = this.handleProjectUpdate.bind(this);
- this.setContentError = this.setContentError.bind(this);
- this.fetchProject = this.fetchProject.bind(this);
- }
-
- componentDidMount() {
- const { validateField } = this.props;
- this.setState({ contentError: null, hasContentLoading: true });
- // TODO: determine when LabelSelect has finished loading labels
- Promise.all([this.loadRelatedInstanceGroups(), this.fetchProject()]).then(
- () => {
- this.setState({ hasContentLoading: false });
+ const {
+ request: fetchProject,
+ error: projectContentError,
+ contentLoading: hasProjectLoading,
+ } = useRequest(
+ useCallback(async () => {
+ let projectData;
+ if (template?.project) {
+ projectData = await ProjectsAPI.readDetail(template?.project);
validateField('project');
+ setProject(projectData.data);
}
- );
- }
-
- async fetchProject() {
- const { project } = this.state;
- if (project && project.id) {
- try {
- const { data: projectData } = await ProjectsAPI.readDetail(project.id);
- this.setState({ project: projectData });
- } catch (err) {
- this.setState({ contentError: err });
+ }, [template, validateField])
+ );
+ const {
+ request: loadRelatedInstanceGroups,
+ error: instanceGroupError,
+ contentLoading: instanceGroupLoading,
+ } = useRequest(
+ useCallback(async () => {
+ if (!template?.id) {
+ return;
}
- }
- }
-
- async loadRelatedInstanceGroups() {
- const { setFieldValue, template } = this.props;
- if (!template.id) {
- return;
- }
- try {
const { data } = await JobTemplatesAPI.readInstanceGroups(template.id);
setFieldValue('initialInstanceGroups', data.results);
setFieldValue('instanceGroups', [...data.results]);
- } catch (err) {
- this.setState({ contentError: err });
+ }, [setFieldValue, template])
+ );
+
+ useEffect(() => {
+ fetchProject();
+ }, [fetchProject]);
+
+ useEffect(() => {
+ loadRelatedInstanceGroups();
+ }, [loadRelatedInstanceGroups]);
+
+ const handleProjectValidation = () => {
+ if (!project && touched.project) {
+ return i18n._(t`Select a value for this field`);
}
- }
+ if (project && project.status === 'never updated') {
+ return i18n._(t`This project needs to be updated`);
+ }
+ return undefined;
+ };
- handleProjectValidation() {
- const { i18n, touched } = this.props;
- const { project } = this.state;
- return () => {
- if (!project && touched.project) {
- return i18n._(t`Select a value for this field`);
- }
- if (project && project.status === 'never updated') {
- return i18n._(t`This project needs to be updated`);
- }
- return undefined;
- };
- }
-
- handleProjectUpdate(project) {
- const { setFieldValue } = this.props;
- setFieldValue('project', project.id);
+ const handleProjectUpdate = newProject => {
+ setProject(newProject);
+ setFieldValue('project', newProject.id);
setFieldValue('playbook', 0);
setFieldValue('scm_branch', '');
- this.setState({ project });
+ };
+
+ const jobTypeOptions = [
+ {
+ value: '',
+ key: '',
+ label: i18n._(t`Choose a job type`),
+ isDisabled: true,
+ },
+ { value: 'run', key: 'run', label: i18n._(t`Run`), isDisabled: false },
+ {
+ value: 'check',
+ key: 'check',
+ label: i18n._(t`Check`),
+ isDisabled: false,
+ },
+ ];
+ const verbosityOptions = [
+ { value: '0', key: '0', label: i18n._(t`0 (Normal)`) },
+ { value: '1', key: '1', label: i18n._(t`1 (Verbose)`) },
+ { value: '2', key: '2', label: i18n._(t`2 (More Verbose)`) },
+ { value: '3', key: '3', label: i18n._(t`3 (Debug)`) },
+ { value: '4', key: '4', label: i18n._(t`4 (Connection Debug)`) },
+ ];
+ let callbackUrl;
+ if (template?.related) {
+ const { origin } = document.location;
+ const path = template.related.callback || `${template.url}callback`;
+ callbackUrl = `${origin}${path}`;
}
- setContentError(contentError) {
- this.setState({ contentError });
+ if (instanceGroupLoading || hasProjectLoading) {
+ return