diff --git a/frontend/dashboard/components/MakeCopyModal/MakeCopyModal.test.tsx b/frontend/dashboard/components/MakeCopyModal/MakeCopyModal.test.tsx index 635c17d9060..f27303cc447 100644 --- a/frontend/dashboard/components/MakeCopyModal/MakeCopyModal.test.tsx +++ b/frontend/dashboard/components/MakeCopyModal/MakeCopyModal.test.tsx @@ -12,6 +12,13 @@ import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; import { QueryKey } from 'app-shared/types/QueryKey'; import { PackagesRouter } from 'app-shared/navigation/PackagesRouter'; import { app, org } from '@studio/testing/testids'; +import { useUserOrgPermissionQuery } from '../../hooks/queries/useUserOrgPermissionsQuery'; + +jest.mock('../../hooks/queries/useUserOrgPermissionsQuery'); + +(useUserOrgPermissionQuery as jest.Mock).mockReturnValue({ + data: { canCreateOrgRepo: true }, +}); const mockServiceFullName: string = `${org}/${app}`; const mockUser: User = { diff --git a/frontend/dashboard/components/NewApplicationForm/NewApplicationForm.test.tsx b/frontend/dashboard/components/NewApplicationForm/NewApplicationForm.test.tsx index 1bbcc5a319a..ef510bada1f 100644 --- a/frontend/dashboard/components/NewApplicationForm/NewApplicationForm.test.tsx +++ b/frontend/dashboard/components/NewApplicationForm/NewApplicationForm.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import { NewApplicationForm, type NewApplicationFormProps, @@ -8,7 +8,11 @@ import { import { type User } from 'app-shared/types/Repository'; import { type Organization } from 'app-shared/types/Organization'; import userEvent from '@testing-library/user-event'; -import { textMock } from '../../../testing/mocks/i18nMock'; +import { textMock } from '@studio/testing/mocks/i18nMock'; +import type { ServicesContextProps } from 'app-shared/contexts/ServicesContext'; +import { renderWithProviders } from '../../testing/mocks'; +import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; +import { useUserOrgPermissionQuery } from '../../hooks/queries/useUserOrgPermissionsQuery'; const mockOnSubmit = jest.fn(); @@ -51,12 +55,18 @@ const defaultProps: NewApplicationFormProps = { actionableElement: mockCancelComponentButton, }; +jest.mock('../../hooks/queries/useUserOrgPermissionsQuery'); + +(useUserOrgPermissionQuery as jest.Mock).mockReturnValue({ + data: { canCreateOrgRepo: true }, +}); + describe('NewApplicationForm', () => { afterEach(jest.clearAllMocks); it('calls onSubmit when form is submitted with valid data', async () => { const user = userEvent.setup(); - render(); + renderNewApplicationForm(); const select = screen.getByLabelText(textMock('general.service_owner')); await user.click(select); @@ -81,7 +91,7 @@ describe('NewApplicationForm', () => { it('does not call onSubmit when form is submitted with invalid data', async () => { const user = userEvent.setup(); - render(); + renderNewApplicationForm(); const select = screen.getByLabelText(textMock('general.service_owner')); await user.click(select); @@ -96,4 +106,47 @@ describe('NewApplicationForm', () => { expect(mockOnSubmit).toHaveBeenCalledTimes(0); }); + + it('should notify the user if they lack permission to create a new application for the organization and disable the "Create" button', async () => { + const user = userEvent.setup(); + (useUserOrgPermissionQuery as jest.Mock).mockReturnValue({ + data: { canCreateOrgRepo: false }, + }); + renderNewApplicationForm(defaultProps); + + const serviceOwnerSelect = screen.getByLabelText(textMock('general.service_owner')); + await user.selectOptions(serviceOwnerSelect, mockOrg.username); + expect( + await screen.findByText(textMock('dashboard.missing_service_owner_rights_error_message')), + ).toBeInTheDocument(); + expect(screen.getByRole('button', { name: mockSubmitbuttonText })).toBeDisabled(); + }); + + it('should enable the "Create" button and not display an error if the user has permission to create an organization', async () => { + const user = userEvent.setup(); + (useUserOrgPermissionQuery as jest.Mock).mockReturnValue({ + data: { canCreateOrgRepo: true }, + }); + renderNewApplicationForm(defaultProps); + + const serviceOwnerSelect = screen.getByLabelText(textMock('general.service_owner')); + await user.selectOptions(serviceOwnerSelect, mockOrg.username); + expect( + screen.queryByText(textMock('dashboard.missing_service_owner_rights_error_message')), + ).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: mockSubmitbuttonText })).toBeEnabled(); + }); }); + +function renderNewApplicationForm( + newApplicationFormProps?: Partial, + services?: Partial, +) { + return renderWithProviders( + , + { + queries: services, + queryClient: createQueryClientMock(), + }, + ); +} diff --git a/frontend/dashboard/components/NewApplicationForm/NewApplicationForm.tsx b/frontend/dashboard/components/NewApplicationForm/NewApplicationForm.tsx index e3947330491..9d4c2f2feba 100644 --- a/frontend/dashboard/components/NewApplicationForm/NewApplicationForm.tsx +++ b/frontend/dashboard/components/NewApplicationForm/NewApplicationForm.tsx @@ -1,4 +1,4 @@ -import React, { type FormEvent, type ChangeEvent } from 'react'; +import React, { type FormEvent, type ChangeEvent, useState } from 'react'; import classes from './NewApplicationForm.module.css'; import { StudioButton, StudioSpinner } from '@studio/components'; import { useTranslation } from 'react-i18next'; @@ -11,6 +11,7 @@ import { SelectedContextType } from 'dashboard/context/HeaderContext'; import { type NewAppForm } from '../../types/NewAppForm'; import { useCreateAppFormValidation } from './hooks/useCreateAppFormValidation'; import { Link } from 'react-router-dom'; +import { useUserOrgPermissionQuery } from '../../hooks/queries/useUserOrgPermissionsQuery'; type CancelButton = { onClick: () => void; @@ -47,11 +48,14 @@ export const NewApplicationForm = ({ const { t } = useTranslation(); const selectedContext = useSelectedContext(); const { validateRepoOwnerName, validateRepoName } = useCreateAppFormValidation(); - const defaultSelectedOrgOrUser: string = selectedContext === SelectedContextType.Self || selectedContext === SelectedContextType.All ? user.login : selectedContext; + const [currentSelectedOrg, setCurrentSelectedOrg] = useState(defaultSelectedOrgOrUser); + const { data: userOrgPermission, isFetching } = useUserOrgPermissionQuery(currentSelectedOrg, { + enabled: Boolean(currentSelectedOrg), + }); const validateTextValue = (event: ChangeEvent) => { const { errorMessage: repoNameErrorMessage, isValid: isRepoNameValid } = validateRepoName( @@ -96,14 +100,22 @@ export const NewApplicationForm = ({ return isOrgValid && isRepoNameValid; }; + const createRepoAccessError: string = + !userOrgPermission?.canCreateOrgRepo && !isFetching + ? t('dashboard.missing_service_owner_rights_error_message') + : ''; + + const hasCreateRepoAccessError: boolean = Boolean(createRepoAccessError); + return (
) : ( <> - + {submitButtonText} diff --git a/frontend/dashboard/components/ServiceOwnerSelector/ServiceOwnerSelector.test.tsx b/frontend/dashboard/components/ServiceOwnerSelector/ServiceOwnerSelector.test.tsx index 91e02dcf786..d52bc705fbd 100644 --- a/frontend/dashboard/components/ServiceOwnerSelector/ServiceOwnerSelector.test.tsx +++ b/frontend/dashboard/components/ServiceOwnerSelector/ServiceOwnerSelector.test.tsx @@ -6,7 +6,7 @@ import { textMock } from '../../../testing/mocks/i18nMock'; import { user as mockUser } from 'app-shared/mocks/mocks'; import userEvent from '@testing-library/user-event'; -const defaultProps = { +const defaultProps: ServiceOwnerSelectorProps = { selectedOrgOrUser: 'userLogin', user: { ...mockUser, @@ -21,6 +21,7 @@ const defaultProps = { ], errorMessage: '', name: '', + onChange: () => {}, }; const renderServiceOwnerSelector = (props: Partial = {}) => { @@ -77,4 +78,16 @@ describe('ServiceOwnerSelector', () => { const select = screen.getByLabelText(textMock('general.service_owner')); expect(select).toHaveValue(defaultProps.user.login); }); + + it('should execute the onChange callback when service owner is changed', async () => { + const user = userEvent.setup(); + const selectedOrgOrUser = 'all'; + const onChangeMock = jest.fn(); + renderServiceOwnerSelector({ selectedOrgOrUser, onChange: onChangeMock }); + + const select = screen.getByLabelText(textMock('general.service_owner')); + await user.selectOptions(select, 'organizationUsername'); + expect(onChangeMock).toHaveBeenCalledWith('organizationUsername'); + expect(onChangeMock).toHaveBeenCalledTimes(1); + }); }); diff --git a/frontend/dashboard/components/ServiceOwnerSelector/ServiceOwnerSelector.tsx b/frontend/dashboard/components/ServiceOwnerSelector/ServiceOwnerSelector.tsx index 80f89c6c80f..bf6cabc0e2c 100644 --- a/frontend/dashboard/components/ServiceOwnerSelector/ServiceOwnerSelector.tsx +++ b/frontend/dashboard/components/ServiceOwnerSelector/ServiceOwnerSelector.tsx @@ -10,6 +10,7 @@ export type ServiceOwnerSelectorProps = { organizations: Organization[]; errorMessage?: string; name?: string; + onChange?: (org: string) => void; }; export const ServiceOwnerSelector = ({ @@ -18,6 +19,7 @@ export const ServiceOwnerSelector = ({ organizations, errorMessage, name, + onChange, }: ServiceOwnerSelectorProps) => { const { t } = useTranslation(); const serviceOwnerId: string = useId(); @@ -38,6 +40,7 @@ export const ServiceOwnerSelector = ({ name={name} id={serviceOwnerId} defaultValue={defaultValue} + onChange={(event) => onChange(event.target.value)} > {selectableOptions.map(({ value, label }) => (