diff --git a/static/app/views/projectInstall/createProject.spec.tsx b/static/app/views/projectInstall/createProject.spec.tsx index 59514e23d5bd96..2ddbb7e9ed8540 100644 --- a/static/app/views/projectInstall/createProject.spec.tsx +++ b/static/app/views/projectInstall/createProject.spec.tsx @@ -10,12 +10,14 @@ import { userEvent, waitFor, } from 'sentry-test/reactTestingLibrary'; +import selectEvent from 'sentry-test/selectEvent'; import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; import OrganizationStore from 'sentry/stores/organizationStore'; import TeamStore from 'sentry/stores/teamStore'; import type {Organization} from 'sentry/types/organization'; import {CreateProject} from 'sentry/views/projectInstall/createProject'; +import * as useValidateChannelModule from 'sentry/views/projectInstall/useValidateChannel'; jest.mock('sentry/actionCreators/indicator'); @@ -77,17 +79,24 @@ describe('CreateProject', () => { access: ['team:admin', 'team:write', 'team:read'], }); + const integration = OrganizationIntegrationsFixture({ + name: "Moo Deng's Workspace", + }); + beforeEach(() => { TeamStore.reset(); TeamStore.loadUserTeams([teamNoAccess]); MockApiClient.addMockResponse({ url: `/organizations/org-slug/integrations/?integrationType=messaging`, - body: [ - OrganizationIntegrationsFixture({ - name: "Moo Deng's Workspace", - }), - ], + body: [integration], + }); + + MockApiClient.addMockResponse({ + url: `/organizations/org-slug/integrations/${integration.id}/channels/`, + body: { + results: [], + }, }); }); @@ -428,6 +437,102 @@ describe('CreateProject', () => { expect(projectCreationMockRequest).toHaveBeenCalled(); }); + it('should disable submit button when channel validation fails and integration is selected', async () => { + renderFrameworkModalMockRequests({ + organization, + teamSlug: teamWithAccess.slug, + }); + + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/integrations/${integration.id}/channel-validate/`, + body: {valid: false, detail: 'Channel not found'}, + }); + + render(, {organization}); + + await userEvent.click(screen.getByTestId('platform-apple-ios')); + expect(screen.getByRole('button', {name: 'Create Project'})).toBeEnabled(); + await userEvent.click( + screen.getByRole('checkbox', { + name: /Notify via integration/, + }) + ); + await selectEvent.create(screen.getByLabelText('channel'), '#custom-channel', { + waitForElement: false, + createOptionText: '#custom-channel', + }); + expect(await screen.findByText('Channel not found')).toBeInTheDocument(); + expect(screen.getByRole('button', {name: 'Create Project'})).toBeDisabled(); + await userEvent.hover(screen.getByRole('button', {name: 'Create Project'})); + expect(await screen.findByText('Channel not found')).toBeInTheDocument(); + await userEvent.click(screen.getByLabelText('Clear choices')); + await userEvent.hover(screen.getByRole('button', {name: 'Create Project'})); + expect( + await screen.findByText(/provide an integration channel/) + ).toBeInTheDocument(); + }); + + it('should NOT disable submit button when channel validation fails but integration is unchecked', async () => { + renderFrameworkModalMockRequests({ + organization, + teamSlug: teamWithAccess.slug, + }); + + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/integrations/${integration.id}/channel-validate/`, + body: {valid: false, detail: 'Channel not found'}, + }); + + render(, {organization}); + + await userEvent.click(screen.getByTestId('platform-apple-ios')); + await userEvent.click( + screen.getByRole('checkbox', { + name: /Notify via integration/, + }) + ); + await selectEvent.create(screen.getByLabelText('channel'), '#custom-channel', { + waitForElement: false, + createOptionText: '#custom-channel', + }); + expect(await screen.findByText('Channel not found')).toBeInTheDocument(); + expect(screen.getByRole('button', {name: 'Create Project'})).toBeDisabled(); + await userEvent.click( + screen.getByRole('checkbox', { + name: /Notify via integration/, + }) + ); + await waitFor(() => { + expect(screen.getByRole('button', {name: 'Create Project'})).toBeEnabled(); + }); + }); + + it('should show validating tooltip and disable button while validating channel', async () => { + renderFrameworkModalMockRequests({ + organization, + teamSlug: teamWithAccess.slug, + }); + + jest.spyOn(useValidateChannelModule, 'useValidateChannel').mockReturnValue({ + isFetching: true, + clear: jest.fn(), + error: undefined, + }); + + render(, {organization}); + await userEvent.click(screen.getByTestId('platform-apple-ios')); + await userEvent.click( + screen.getByRole('checkbox', { + name: /Notify via integration/, + }) + ); + expect(screen.getByRole('button', {name: 'Create Project'})).toBeDisabled(); + await userEvent.hover(screen.getByRole('button', {name: 'Create Project'})); + expect( + await screen.findByText(/Validating integration channel/) + ).toBeInTheDocument(); + }); + it('should create an issue alert rule by default', async () => { const {projectCreationMockRequest} = renderFrameworkModalMockRequests({ organization, diff --git a/static/app/views/projectInstall/createProject.tsx b/static/app/views/projectInstall/createProject.tsx index 63492cee188a22..6b4a365cd25c1e 100644 --- a/static/app/views/projectInstall/createProject.tsx +++ b/static/app/views/projectInstall/createProject.tsx @@ -45,6 +45,7 @@ import {useTeams} from 'sentry/utils/useTeams'; import { MultipleCheckboxOptions, useCreateNotificationAction, + type IntegrationChannel, } from 'sentry/views/projectInstall/issueAlertNotificationOptions'; import type { AlertRuleOptions, @@ -53,6 +54,7 @@ import type { import IssueAlertOptions, { getRequestDataFragment, } from 'sentry/views/projectInstall/issueAlertOptions'; +import {useValidateChannel} from 'sentry/views/projectInstall/useValidateChannel'; import {makeProjectsPathname} from 'sentry/views/projects/pathname'; type FormData = { @@ -82,7 +84,7 @@ function getMissingValues({ isOrgMemberWithNoAccess: boolean; notificationProps: { actions?: string[]; - channel?: string; + channel?: IntegrationChannel; }; projectName: string; team: string | undefined; @@ -130,6 +132,7 @@ function getSubmitTooltipText({ if (isMissingPlatform) { return t('Please select a platform'); } + return t('Please select a team'); } @@ -141,6 +144,7 @@ export function CreateProject() { const location = useLocation(); const canUserCreateProject = useCanCreateProject(); const createProjectAndRules = useCreateProjectAndRules(); + const {teams} = useTeams(); const accessTeams = teams.filter((team: Team) => team.access.includes('team:admin')); const referrer = decodeScalar(location.query.referrer); @@ -163,6 +167,12 @@ export function CreateProject() { createNotificationActionParam ); + const validateChannel = useValidateChannel({ + channel: notificationProps.channel, + integrationId: notificationProps.integration?.id, + enabled: false, + }); + const defaultTeam = accessTeams?.[0]?.slug; const initialData: FormData = useMemo(() => { @@ -204,18 +214,26 @@ export function CreateProject() { platform: formData.platform, }); + const isNotifyingViaIntegration = + alertRuleConfig.shouldCreateRule && + notificationProps.actions?.includes(MultipleCheckboxOptions.INTEGRATION); + const formErrorCount = [ missingValues.isMissingPlatform, missingValues.isMissingTeam, missingValues.isMissingProjectName, missingValues.isMissingAlertThreshold, missingValues.isMissingMessagingIntegrationChannel, + isNotifyingViaIntegration && validateChannel.error, ].filter(value => value).length; - const submitTooltipText = getSubmitTooltipText({ - ...missingValues, - formErrorCount, - }); + const submitTooltipText = + isNotifyingViaIntegration && validateChannel.error + ? validateChannel.error + : getSubmitTooltipText({ + ...missingValues, + formErrorCount, + }); const updateFormData = useCallback( (field: K, value: FormData[K]) => { @@ -557,16 +575,25 @@ export function CreateProject() {