diff --git a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.jsx b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.jsx
index 5f36cd7938..bec336f4e6 100644
--- a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.jsx
+++ b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.jsx
@@ -18,7 +18,7 @@ import PaginatedDataList, {
import { getQSConfig, parseQueryString } from '@util/qs';
import AddDropDownButton from '@components/AddDropDownButton';
-import ProjectTemplatesListItem from '../../Template/TemplateList/TemplateListItem';
+import ProjectTemplatesListItem from './ProjectJobTemplatesListItem';
// The type value in const QS_CONFIG below does not have a space between job_template and
// workflow_job_template so the params sent to the API match what the api expects.
@@ -238,7 +238,7 @@ function ProjectJobTemplatesList({ i18n }) {
itemsToDelete={selected}
pluralizedItemName="Templates"
/>,
- (canAddJT || canAddWFJT) && addButton,
+ ...(canAddJT || canAddWFJT ? [addButton] : []),
]}
/>
)}
diff --git a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx
new file mode 100644
index 0000000000..ea46bb967b
--- /dev/null
+++ b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx
@@ -0,0 +1,126 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import {
+ Button,
+ DataListAction as _DataListAction,
+ DataListCell,
+ DataListCheck,
+ DataListItem,
+ DataListItemRow,
+ DataListItemCells,
+ Tooltip,
+} from '@patternfly/react-core';
+import { t } from '@lingui/macro';
+import { withI18n } from '@lingui/react';
+import {
+ ExclamationTriangleIcon,
+ PencilAltIcon,
+ RocketIcon,
+} from '@patternfly/react-icons';
+
+import LaunchButton from '@components/LaunchButton';
+import Sparkline from '@components/Sparkline';
+import { toTitleCase } from '@util/strings';
+import styled from 'styled-components';
+
+const DataListAction = styled(_DataListAction)`
+ align-items: center;
+ display: grid;
+ grid-gap: 16px;
+ grid-template-columns: repeat(2, 40px);
+`;
+
+function ProjectJobTemplateListItem({
+ i18n,
+ template,
+ isSelected,
+ onSelect,
+ detailUrl,
+}) {
+ const labelId = `check-action-${template.id}`;
+ const canLaunch = template.summary_fields.user_capabilities.start;
+
+ const missingResourceIcon =
+ template.type === 'job_template' &&
+ (!template.summary_fields.project ||
+ (!template.summary_fields.inventory &&
+ !template.ask_inventory_on_launch));
+
+ return (
+
+
+
+
+
+
+ {template.name}
+
+
+ {missingResourceIcon && (
+
+
+
+
+
+ )}
+ ,
+
+ {toTitleCase(template.type)}
+ ,
+
+
+ ,
+ ]}
+ />
+
+ {canLaunch && template.type === 'job_template' && (
+
+
+ {({ handleLaunch }) => (
+
+ )}
+
+
+ )}
+ {template.summary_fields.user_capabilities.edit && (
+
+
+
+ )}
+
+
+
+ );
+}
+
+export { ProjectJobTemplateListItem as _ProjectJobTemplateListItem };
+export default withI18n()(ProjectJobTemplateListItem);
diff --git a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.test.jsx b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.test.jsx
new file mode 100644
index 0000000000..f94fc1a6e2
--- /dev/null
+++ b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.test.jsx
@@ -0,0 +1,189 @@
+import React from 'react';
+
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import { createMemoryHistory } from 'history';
+import ProjectJobTemplatesListItem from './ProjectJobTemplatesListItem';
+
+describe('', () => {
+ test('launch button shown to users with start capabilities', () => {
+ const wrapper = mountWithContexts(
+
+ );
+ expect(wrapper.find('LaunchButton').exists()).toBeTruthy();
+ });
+ test('launch button hidden from users without start capabilities', () => {
+ const wrapper = mountWithContexts(
+
+ );
+ expect(wrapper.find('LaunchButton').exists()).toBeFalsy();
+ });
+ test('edit button shown to users with edit capabilities', () => {
+ const wrapper = mountWithContexts(
+
+ );
+ expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
+ });
+ test('edit button hidden from users without edit capabilities', () => {
+ const wrapper = mountWithContexts(
+
+ );
+ expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
+ });
+ test('missing resource icon is shown.', () => {
+ const wrapper = mountWithContexts(
+
+ );
+ expect(wrapper.find('ExclamationTriangleIcon').exists()).toBe(true);
+ });
+ test('missing resource icon is not shown when there is a project and an inventory.', () => {
+ const wrapper = mountWithContexts(
+
+ );
+ expect(wrapper.find('ExclamationTriangleIcon').exists()).toBe(false);
+ });
+ test('missing resource icon is not shown when inventory is prompt_on_launch, and a project', () => {
+ const wrapper = mountWithContexts(
+
+ );
+ expect(wrapper.find('ExclamationTriangleIcon').exists()).toBe(false);
+ });
+ test('missing resource icon is not shown type is workflow_job_template', () => {
+ const wrapper = mountWithContexts(
+
+ );
+ expect(wrapper.find('ExclamationTriangleIcon').exists()).toBe(false);
+ });
+ test('clicking on template from project templates list navigates properly', () => {
+ const history = createMemoryHistory({
+ initialEntries: ['/projects/1/job_templates'],
+ });
+ const wrapper = mountWithContexts(
+ ,
+ { context: { router: { history } } }
+ );
+ wrapper.find('Link').simulate('click', { button: 0 });
+ expect(history.location.pathname).toEqual(
+ '/templates/job_template/2/details'
+ );
+ });
+});
diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx
index 86c506912e..b13eb92298 100644
--- a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx
+++ b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Link, useLocation } from 'react-router-dom';
+import { Link } from 'react-router-dom';
import {
Button,
DataListAction as _DataListAction,
@@ -40,12 +40,6 @@ function TemplateListItem({ i18n, template, isSelected, onSelect, detailUrl }) {
(!template.summary_fields.inventory &&
!template.ask_inventory_on_launch));
- // const location = useLocation();
-
- // if (location.pathname.startsWith('/projects')) {
- // detailUrl = `/templates/job_template/${template.id}/details`;
- // }
-
return (
diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.test.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.test.jsx
index d834478668..df84a79cec 100644
--- a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.test.jsx
+++ b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.test.jsx
@@ -1,5 +1,4 @@
import React from 'react';
-import { Route } from 'react-router-dom';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import { createMemoryHistory } from 'history';
@@ -167,83 +166,24 @@ describe('', () => {
initialEntries: ['/templates'],
});
const wrapper = mountWithContexts(
- (
-
- )}
- />,
- {
- context: {
- router: {
- history,
- route: {
- location: history.location,
- match: {
- params: { id: 1 },
- },
+ ,
+ { context: { router: { history } } }
);
wrapper.find('Link').simulate('click', { button: 0 });
expect(history.location.pathname).toEqual(
'/templates/job_template/1/details'
);
});
- test('clicking on template from project templates list navigates properly', () => {
- const history = createMemoryHistory({
- initialEntries: ['/projects/1/job_templates'],
- });
- const wrapper = mountWithContexts(
- (
-
- )}
- />,
- {
- context: {
- router: {
- history,
- route: {
- location: history.location,
- match: {
- params: { id: 1 },
- },
- },
- },
- },
- }
- );
- wrapper.find('Link').simulate('click', { button: 0 });
- expect(history.location.pathname).toEqual(
- '/templates/job_template/2/details'
- );
- });
});