diff --git a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx
index 19030a798e..f08c8951cf 100644
--- a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx
+++ b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx
@@ -1,13 +1,19 @@
-import React from 'react';
-import { Link } from 'react-router-dom';
+import React, { useState } from 'react';
+import { Link, useHistory } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Project } from '@types';
import { Config } from '@contexts/Config';
+
import { Button, List, ListItem } from '@patternfly/react-core';
+import AlertModal from '@components/AlertModal';
import { CardBody, CardActionsRow } from '@components/Card';
+import ContentLoading from '@components/ContentLoading';
+import DeleteButton from '@components/DeleteButton';
import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
+import ErrorDetail from '@components/ErrorDetail';
import { CredentialChip } from '@components/Chip';
+import { ProjectsAPI } from '@api';
import { toTitleCase } from '@util/strings';
function ProjectDetail({ project, i18n }) {
@@ -30,6 +36,20 @@ function ProjectDetail({ project, i18n }) {
scm_url,
summary_fields,
} = project;
+ const [deletionError, setDeletionError] = useState(null);
+ const [hasContentLoading, setHasContentLoading] = useState(false);
+ const history = useHistory();
+
+ const handleDelete = async () => {
+ setHasContentLoading(true);
+ try {
+ await ProjectsAPI.destroy(id);
+ history.push(`/projects`);
+ } catch (error) {
+ setDeletionError(error);
+ }
+ setHasContentLoading(false);
+ };
let optionsList = '';
if (
@@ -54,6 +74,10 @@ function ProjectDetail({ project, i18n }) {
);
}
+ if (hasContentLoading) {
+ return ;
+ }
+
return (
@@ -138,7 +162,29 @@ function ProjectDetail({ project, i18n }) {
{i18n._(t`Edit`)}
)}
+ {summary_fields.user_capabilities &&
+ summary_fields.user_capabilities.delete && (
+
+ {i18n._(t`Delete`)}
+
+ )}
+ {/* Update delete modal to show dependencies https://github.com/ansible/awx/issues/5546 */}
+ {deletionError && (
+ setDeletionError(null)}
+ >
+ {i18n._(t`Failed to delete project.`)}
+
+
+ )}
);
}
diff --git a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx
index d0eb66ca66..33f8e71254 100644
--- a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx
+++ b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx
@@ -1,8 +1,12 @@
import React from 'react';
+import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
+import { ProjectsAPI } from '@api';
import ProjectDetail from './ProjectDetail';
+jest.mock('@api');
+
describe('', () => {
const mockProject = {
id: 1,
@@ -122,6 +126,7 @@ describe('', () => {
test('should hide options label when all project options return false', () => {
const mockOptions = {
+ scm_type: '',
scm_clean: false,
scm_delete_on_update: false,
scm_update_on_launch: false,
@@ -135,7 +140,7 @@ describe('', () => {
expect(wrapper.find('Detail[label="Options"]').length).toBe(0);
});
- test('should render with missing summary fields', async done => {
+ test('should render with missing summary fields', async () => {
const wrapper = mountWithContexts(
);
@@ -144,10 +149,9 @@ describe('', () => {
'Detail[label="Name"]',
el => el.length === 1
);
- done();
});
- test('should show edit button for users with edit permission', async done => {
+ test('should show edit button for users with edit permission', async () => {
const wrapper = mountWithContexts();
const editButton = await waitForElement(
wrapper,
@@ -155,10 +159,9 @@ describe('', () => {
);
expect(editButton.text()).toEqual('Edit');
expect(editButton.prop('to')).toBe(`/projects/${mockProject.id}/edit`);
- done();
});
- test('should hide edit button for users without edit permission', async done => {
+ test('should hide edit button for users without edit permission', async () => {
const wrapper = mountWithContexts(
', () => {
expect(wrapper.find('ProjectDetail Button[aria-label="edit"]').length).toBe(
0
);
- done();
});
test('edit button should navigate to project edit', () => {
@@ -189,4 +191,37 @@ describe('', () => {
.simulate('click', { button: 0 });
expect(history.location.pathname).toEqual('/projects/1/edit');
});
+
+ test('expected api calls are made for delete', async () => {
+ const wrapper = mountWithContexts();
+ await waitForElement(wrapper, 'ProjectDetail Button[aria-label="Delete"]');
+ await act(async () => {
+ wrapper.find('DeleteButton').invoke('onConfirm')();
+ });
+ expect(ProjectsAPI.destroy).toHaveBeenCalledTimes(1);
+ });
+
+ test('Error dialog shown for failed deletion', async () => {
+ ProjectsAPI.destroy.mockImplementationOnce(() =>
+ Promise.reject(new Error())
+ );
+ const wrapper = mountWithContexts();
+ await waitForElement(wrapper, 'ProjectDetail Button[aria-label="Delete"]');
+ await act(async () => {
+ wrapper.find('DeleteButton').invoke('onConfirm')();
+ });
+ await waitForElement(
+ wrapper,
+ 'Modal[title="Error!"]',
+ el => el.length === 1
+ );
+ await act(async () => {
+ wrapper.find('Modal[title="Error!"]').invoke('onClose')();
+ });
+ await waitForElement(
+ wrapper,
+ 'Modal[title="Error!"]',
+ el => el.length === 0
+ );
+ });
});