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 => (