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() {