From d1fcb96ee2608f4b32a58812e70788aae1ff3d7b Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Tue, 5 Jan 2021 09:50:49 -0500 Subject: [PATCH] Adds sync button to project details page --- .../Project/ProjectDetail/ProjectDetail.jsx | 44 ++++++++++--------- .../ProjectDetail/ProjectDetail.test.jsx | 29 +++++++++++- .../Project/ProjectList/ProjectListItem.jsx | 19 ++------ .../Project/shared/ProjectSyncButton.jsx | 31 +++++++------ .../Project/shared/ProjectSyncButton.test.jsx | 8 +--- 5 files changed, 71 insertions(+), 60 deletions(-) diff --git a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx index 380196d950..4c92c9695e 100644 --- a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx +++ b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx @@ -19,6 +19,7 @@ import CredentialChip from '../../../components/CredentialChip'; import { ProjectsAPI } from '../../../api'; import { toTitleCase } from '../../../util/strings'; import useRequest, { useDismissableError } from '../../../util/useRequest'; +import ProjectSyncButton from '../shared/ProjectSyncButton'; function ProjectDetail({ project, i18n }) { const { @@ -148,27 +149,28 @@ function ProjectDetail({ project, i18n }) { /> - {summary_fields.user_capabilities && - summary_fields.user_capabilities.edit && ( - - )} - {summary_fields.user_capabilities && - summary_fields.user_capabilities.delete && ( - - {i18n._(t`Delete`)} - - )} + {summary_fields.user_capabilities?.edit && ( + + )} + {summary_fields.user_capabilities?.start && ( + + )} + {summary_fields.user_capabilities?.delete && ( + + {i18n._(t`Delete`)} + + )} {/* Update delete modal to show dependencies https://github.com/ansible/awx/issues/5546 */} {error && ( 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 3139e2b14c..52e45e7d28 100644 --- a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx @@ -9,7 +9,12 @@ import { ProjectsAPI } from '../../../api'; import ProjectDetail from './ProjectDetail'; jest.mock('../../../api'); - +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useRouteMatch: () => ({ + url: '/projects/1/details', + }), +})); describe('', () => { const mockProject = { id: 1, @@ -139,13 +144,19 @@ describe('', () => { ); }); - test('should show edit button for users with edit permission', async () => { + test('should show edit and sync button for users with edit permission', async () => { const wrapper = mountWithContexts(); const editButton = await waitForElement( wrapper, 'ProjectDetail Button[aria-label="edit"]' ); + + const syncButton = await waitForElement( + wrapper, + 'ProjectDetail Button[aria-label="Sync Project"]' + ); expect(editButton.text()).toEqual('Edit'); + expect(syncButton.text()).toEqual('Sync'); expect(editButton.prop('to')).toBe(`/projects/${mockProject.id}/edit`); }); @@ -166,6 +177,9 @@ describe('', () => { expect(wrapper.find('ProjectDetail Button[aria-label="edit"]').length).toBe( 0 ); + expect(wrapper.find('ProjectDetail Button[aria-label="sync"]').length).toBe( + 0 + ); }); test('edit button should navigate to project edit', () => { @@ -180,6 +194,17 @@ describe('', () => { expect(history.location.pathname).toEqual('/projects/1/edit'); }); + test('sync button should call api to syn project', async () => { + ProjectsAPI.readSync.mockResolvedValue({ data: { can_update: true } }); + const wrapper = mountWithContexts(); + await act(() => + wrapper + .find('ProjectDetail Button[aria-label="Sync Project"]') + .prop('onClick')(1) + ); + expect(ProjectsAPI.sync).toHaveBeenCalledTimes(1); + }); + test('expected api calls are made for delete', async () => { const wrapper = mountWithContexts(); await waitForElement(wrapper, 'ProjectDetail Button[aria-label="Delete"]'); diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx index b2539e5f87..dba55552d4 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx @@ -14,7 +14,7 @@ import { import { t } from '@lingui/macro'; import { Link } from 'react-router-dom'; -import { PencilAltIcon, SyncIcon } from '@patternfly/react-icons'; +import { PencilAltIcon } from '@patternfly/react-icons'; import styled from 'styled-components'; import { formatDateString, timeOfDay } from '../../../util/dates'; import { ProjectsAPI } from '../../../api'; @@ -153,23 +153,10 @@ function ProjectListItem({ aria-labelledby={labelId} id={labelId} > - {project.summary_fields.user_capabilities.start ? ( + {project.summary_fields.user_capabilities.start && ( - - {handleSync => ( - - )} - + - ) : ( - '' )} {project.summary_fields.user_capabilities.edit ? ( diff --git a/awx/ui_next/src/screens/Project/shared/ProjectSyncButton.jsx b/awx/ui_next/src/screens/Project/shared/ProjectSyncButton.jsx index b65aecae68..864142b046 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectSyncButton.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectSyncButton.jsx @@ -1,4 +1,8 @@ import React, { useCallback } from 'react'; +import { useRouteMatch } from 'react-router-dom'; +import { Button } from '@patternfly/react-core'; +import { SyncIcon } from '@patternfly/react-icons'; + import { number } from 'prop-types'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; @@ -8,28 +12,27 @@ import AlertModal from '../../../components/AlertModal'; import ErrorDetail from '../../../components/ErrorDetail'; import { ProjectsAPI } from '../../../api'; -function ProjectSyncButton({ i18n, children, projectId }) { +function ProjectSyncButton({ i18n, projectId }) { + const match = useRouteMatch(); + const { request: handleSync, error: syncError } = useRequest( useCallback(async () => { - const { data } = await ProjectsAPI.readSync(projectId); - if (data.can_update) { - await ProjectsAPI.sync(projectId); - } else { - throw new Error( - i18n._( - t`You don't have the necessary permissions to sync this project.` - ) - ); - } - }, [i18n, projectId]), + await ProjectsAPI.sync(projectId); + }, [projectId]), null ); const { error, dismissError } = useDismissableError(syncError); - + const isDetailsView = match.url.endsWith('/details'); return ( <> - {children(handleSync)} + {error && ( { let wrapper; - ProjectsAPI.readSync.mockResolvedValue({ - data: { - can_update: true, - }, - }); const children = handleSync => (