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 + ); + }); });