diff --git a/awx/ui_next/src/api/models/JobTemplates.js b/awx/ui_next/src/api/models/JobTemplates.js
index 0e2eba8079..03647de4b0 100644
--- a/awx/ui_next/src/api/models/JobTemplates.js
+++ b/awx/ui_next/src/api/models/JobTemplates.js
@@ -19,6 +19,10 @@ class JobTemplates extends SchedulesMixin(
this.readWebhookKey = this.readWebhookKey.bind(this);
}
+ copyTemplate(id, data) {
+ return this.http.post(`${this.baseUrl}${id}/copy/`, data);
+ }
+
launch(id, data) {
return this.http.post(`${this.baseUrl}${id}/launch/`, data);
}
diff --git a/awx/ui_next/src/screens/Template/TemplateList/CopyButton.jsx b/awx/ui_next/src/screens/Template/TemplateList/CopyButton.jsx
new file mode 100644
index 0000000000..50725ad232
--- /dev/null
+++ b/awx/ui_next/src/screens/Template/TemplateList/CopyButton.jsx
@@ -0,0 +1,54 @@
+import React, { useCallback, useEffect } from 'react';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+
+import { Button, Tooltip } from '@patternfly/react-core';
+import { CopyIcon } from '@patternfly/react-icons';
+import useRequest, { useDismissableError } from '@util/useRequest';
+import AlertModal from '@components/AlertModal';
+import ErrorDetail from '@components/ErrorDetail';
+
+function CopyButton({ i18n, itemName, copyItem, disableButtons }) {
+ const { isLoading, error, request: copyTemplateToAPI } = useRequest(
+ useCallback(async () => {
+ await copyItem();
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []),
+ {}
+ );
+
+ useEffect(() => {
+ if (isLoading) {
+ return disableButtons(true);
+ }
+ return disableButtons(false);
+ }, [isLoading, disableButtons]);
+
+ const { dismissError } = useDismissableError(error);
+
+ return (
+ <>
+
+
+
+ dismissError}
+ >
+ {i18n._(t`Failed to copy ${itemName}.`)}
+
+
+ >
+ );
+}
+export default withI18n()(CopyButton);
diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx
index 1f7b5554a4..21d7dc0206 100644
--- a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx
+++ b/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx
@@ -227,6 +227,7 @@ function TemplateList({ i18n }) {
detailUrl={`/templates/${template.type}/${template.id}`}
onSelect={() => handleSelect(template)}
isSelected={selected.some(row => row.id === template.id)}
+ fetchTemplates={fetchTemplates}
/>
)}
emptyStateControls={(canAddJT || canAddWFJT) && addButton}
diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx
index 991f9b91c0..c82216421f 100644
--- a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx
+++ b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import {
Button,
@@ -18,11 +18,14 @@ import {
PencilAltIcon,
RocketIcon,
} from '@patternfly/react-icons';
+import { timeOfDay } from '@util/dates';
+import { JobTemplatesAPI } from '@api';
import LaunchButton from '@components/LaunchButton';
import Sparkline from '@components/Sparkline';
import { toTitleCase } from '@util/strings';
import styled from 'styled-components';
+import CopyButton from './CopyButton';
const DataListAction = styled(_DataListAction)`
align-items: center;
@@ -31,7 +34,15 @@ const DataListAction = styled(_DataListAction)`
grid-template-columns: repeat(2, 40px);
`;
-function TemplateListItem({ i18n, template, isSelected, onSelect, detailUrl }) {
+function TemplateListItem({
+ i18n,
+ template,
+ isSelected,
+ onSelect,
+ detailUrl,
+ fetchTemplates,
+}) {
+ const [disableButtons, setDisableButtons] = useState(false);
const labelId = `check-action-${template.id}`;
const canLaunch = template.summary_fields.user_capabilities.start;
@@ -40,11 +51,11 @@ function TemplateListItem({ i18n, template, isSelected, onSelect, detailUrl }) {
(!template.summary_fields.project ||
(!template.summary_fields.inventory &&
!template.ask_inventory_on_launch));
-
return (
{({ handleLaunch }) => (