Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
4397a7a
feat(integration): Add new endpoint to list channels
priscilawebdev Oct 10, 2025
16769df
fix test
priscilawebdev Oct 10, 2025
280d89c
fix tests
priscilawebdev Oct 10, 2025
6739e23
fix type issue
priscilawebdev Oct 10, 2025
6f28480
Merge branch 'master' into priscila/feat/add-new-endpoint-to-list-int…
priscilawebdev Oct 13, 2025
adec286
feedback
priscilawebdev Oct 13, 2025
fb40b9a
fix types
priscilawebdev Oct 13, 2025
ba2245d
Merge branch 'master' into priscila/feat/add-new-endpoint-to-list-int…
priscilawebdev Oct 13, 2025
cfc551c
add int channels to silo url patterns
priscilawebdev Oct 13, 2025
981ee3c
feat(project-creation): Replace input text with select to list channels
priscilawebdev Oct 14, 2025
9609715
:hammer_and_wrench: apply pre-commit fixes
getsantry[bot] Oct 14, 2025
1488f2a
cursor feedback
priscilawebdev Oct 14, 2025
9405bbe
Merge branch 'priscila/feat/add-new-endpoint-to-list-integration-chan…
priscilawebdev Oct 14, 2025
f016f7f
fix integration initialization
priscilawebdev Oct 14, 2025
f1fadad
Merge branch 'master' into priscila/feat/add-new-endpoint-to-list-int…
priscilawebdev Oct 14, 2025
c41c910
Merge branch 'priscila/feat/add-new-endpoint-to-list-integration-chan…
priscilawebdev Oct 14, 2025
59e57b0
fix x
priscilawebdev Oct 14, 2025
796e708
:hammer_and_wrench: apply pre-commit fixes
getsantry[bot] Oct 14, 2025
705773e
fix test
priscilawebdev Oct 14, 2025
8785006
cursor feedback
priscilawebdev Oct 14, 2025
52c4a08
Merge branch 'priscila/feat/add-new-endpoint-to-list-integration-chan…
priscilawebdev Oct 14, 2025
e8db15e
Merge branch 'master' into priscila/feat/add-select-dropddown-for-int…
priscilawebdev Oct 15, 2025
6654245
remove icon warning
priscilawebdev Oct 15, 2025
698d801
Merge branch 'master' into priscila/feat/add-select-dropddown-for-int…
priscilawebdev Oct 20, 2025
7e40f21
Merge branch 'master' into priscila/feat/add-select-dropddown-for-int…
priscilawebdev Oct 21, 2025
8a88a65
update frontend
priscilawebdev Oct 21, 2025
0dd0396
make everything work with get
priscilawebdev Oct 21, 2025
c6696cb
Merge branch 'master' into priscila/feat/add-select-dropddown-for-int…
priscilawebdev Oct 21, 2025
b2ec59b
cursor feedback
priscilawebdev Oct 21, 2025
91e3e72
fix ts errors
priscilawebdev Oct 21, 2025
dfc98cd
feedback
priscilawebdev Oct 22, 2025
a158e10
fix test
priscilawebdev Oct 22, 2025
e87dca6
fix another test
priscilawebdev Oct 22, 2025
2dcf341
update error message
priscilawebdev Oct 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 110 additions & 5 deletions static/app/views/projectInstall/createProject.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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: [],
},
});
});

Expand Down Expand Up @@ -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(<CreateProject />, {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(<CreateProject />, {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(<CreateProject />, {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,
Expand Down
43 changes: 35 additions & 8 deletions static/app/views/projectInstall/createProject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {useTeams} from 'sentry/utils/useTeams';
import {
MultipleCheckboxOptions,
useCreateNotificationAction,
type IntegrationChannel,
} from 'sentry/views/projectInstall/issueAlertNotificationOptions';
import type {
AlertRuleOptions,
Expand All @@ -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 = {
Expand Down Expand Up @@ -82,7 +84,7 @@ function getMissingValues({
isOrgMemberWithNoAccess: boolean;
notificationProps: {
actions?: string[];
channel?: string;
channel?: IntegrationChannel;
};
projectName: string;
team: string | undefined;
Expand Down Expand Up @@ -130,6 +132,7 @@ function getSubmitTooltipText({
if (isMissingPlatform) {
return t('Please select a platform');
}

return t('Please select a team');
}

Expand All @@ -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);
Expand All @@ -163,6 +167,12 @@ export function CreateProject() {
createNotificationActionParam
);

const validateChannel = useValidateChannel({
channel: notificationProps.channel,
integrationId: notificationProps.integration?.id,
enabled: false,
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Channel Validation Hook Initialization Issue

The useValidateChannel hook in createProject.tsx is initialized with enabled: false, preventing channel validation from running. The form's validation logic and submit button state, which rely on validateChannel.error and isFetching, therefore don't reflect actual channel validation. This can allow invalid integration channels to be submitted or misrepresent validation status in the UI.

Fix in Cursor Fix in Web


const defaultTeam = accessTeams?.[0]?.slug;

const initialData: FormData = useMemo(() => {
Expand Down Expand Up @@ -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(
<K extends keyof FormData>(field: K, value: FormData[K]) => {
Expand Down Expand Up @@ -557,16 +575,25 @@ export function CreateProject() {
<Tooltip
title={
canUserCreateProject
? submitTooltipText
? isNotifyingViaIntegration && validateChannel.isFetching
? t('Validating integration channel\u2026')
: submitTooltipText
: t('You do not have permission to create projects')
}
disabled={formErrorCount === 0 && canUserCreateProject}
disabled={
formErrorCount === 0 &&
canUserCreateProject &&
!(isNotifyingViaIntegration && validateChannel.isFetching)
}
>
<Button
data-test-id="create-project"
priority="primary"
disabled={!(canUserCreateProject && formErrorCount === 0)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Button Enabled During Validation

The "Create Project" button remains enabled during integration channel validation, despite its tooltip showing "Validating integration channel…" and its busy state being active. This allows form submission before validation completes.

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the button is 'busy' by clicking on it doesn't submit the form - I tested.

busy={createProjectAndRules.isPending}
busy={
createProjectAndRules.isPending ||
(isNotifyingViaIntegration && validateChannel.isFetching)
}
onClick={() => debounceHandleProjectCreation(formData)}
>
{t('Create Project')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ describe('MessagingIntegrationAlertRule', () => {

const notificationProps: IssueAlertNotificationProps = {
actions: [],
channel: 'channel',
channel: {
label: 'channel',
value: 'channel',
},
integration: undefined,
provider: 'slack',
providersToIntegrations: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,24 @@ export const enum MultipleCheckboxOptions {
INTEGRATION = 'integration',
}

export type IntegrationChannel = {
label: string;
value: string;
new?: boolean;
};

export type IssueAlertNotificationProps = {
actions: MultipleCheckboxOptions[];
channel: string | undefined;
integration: OrganizationIntegration | undefined;
provider: string | undefined;
providersToIntegrations: Record<string, OrganizationIntegration[]>;
querySuccess: boolean;
setActions: (action: MultipleCheckboxOptions[]) => void;
setChannel: (channel: string | undefined) => void;
setChannel: (channel?: IntegrationChannel) => void;
setIntegration: (integration: OrganizationIntegration | undefined) => void;
setProvider: (provider: string | undefined) => void;
shouldRenderSetupButton: boolean;
channel?: IntegrationChannel;
};

export function useCreateNotificationAction({
Expand Down Expand Up @@ -109,7 +115,7 @@ export function useCreateNotificationAction({
const [integration, setIntegration] = useState<OrganizationIntegration | undefined>(
undefined
);
const [channel, setChannel] = useState<string | undefined>(undefined);
const [channel, setChannel] = useState<IntegrationChannel | undefined>(undefined);
const [shouldRenderSetupButton, setShouldRenderSetupButton] = useState<boolean>(false);

useEffect(() => {
Expand Down Expand Up @@ -143,7 +149,10 @@ export function useCreateNotificationAction({

if (firstAction.channel) {
// eslint-disable-next-line react-you-might-not-need-an-effect/no-derived-state
setChannel(firstAction.channel);
setChannel({
label: firstAction.channel,
value: firstAction.channel,
});
}
}, [defaultActions, providersToIntegrations]);

Expand Down Expand Up @@ -180,23 +189,23 @@ export function useCreateNotificationAction({
integrationAction = {
id: IssueAlertActionType.SLACK,
workspace: integration?.id,
channel,
channel: channel?.value,
};

break;
case 'discord':
integrationAction = {
id: IssueAlertActionType.DISCORD,
server: integration?.id,
channel_id: channel,
channel_id: channel?.value,
};

break;
case 'msteams':
integrationAction = {
id: IssueAlertActionType.MS_TEAMS,
team: integration?.id,
channel,
channel: channel?.value,
};
break;
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import IssueAlertOptions, {
describe('IssueAlertOptions', () => {
const notificationProps: IssueAlertNotificationProps = {
actions: [],
channel: 'channel',
channel: {
label: 'channel',
value: 'channel',
},
integration: OrganizationIntegrationsFixture(),
provider: 'slack',
providersToIntegrations: {},
Expand Down
Loading
Loading