From 77a961714c9b7b965e65206e4684a11b794f345e Mon Sep 17 00:00:00 2001 From: Grant Patterson Date: Thu, 3 Sep 2026 16:08:56 -0700 Subject: [PATCH 1/4] fix(issue-details): Don't show success notification on failure --- .../views/issueDetails/actions/index.spec.tsx | 26 +++++++++++++++++++ .../app/views/issueDetails/actions/index.tsx | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/static/app/views/issueDetails/actions/index.spec.tsx b/static/app/views/issueDetails/actions/index.spec.tsx index 0a3add6feac2..d6a64f740241 100644 --- a/static/app/views/issueDetails/actions/index.spec.tsx +++ b/static/app/views/issueDetails/actions/index.spec.tsx @@ -16,6 +16,7 @@ import { import {GlobalModal} from '@sentry/scraps/modal'; +import {clearIndicators} from 'sentry/actionCreators/indicator'; import { CMDKCollection, CommandPaletteProvider, @@ -23,6 +24,7 @@ import { } from 'sentry/components/commandPalette/ui/cmdk'; import type {CollectionTreeNode} from 'sentry/components/commandPalette/ui/collection'; import {CommandPaletteSlot} from 'sentry/components/commandPalette/ui/commandPaletteSlot'; +import Indicators from 'sentry/components/indicators'; import {mockTour} from 'sentry/components/tours/testUtils'; import {ConfigStore} from 'sentry/stores/configStore'; import {ModalStore} from 'sentry/stores/modalStore'; @@ -92,6 +94,7 @@ describe('GroupActions', () => { const analyticsSpy = jest.spyOn(analytics, 'trackAnalytics'); beforeEach(() => { + clearIndicators(); ConfigStore.init(); ProjectsStore.reset(); ProjectsStore.loadInitialData([project]); @@ -391,6 +394,29 @@ describe('GroupActions', () => { ); }); + it('does not report success when resolving fails', async () => { + MockApiClient.addMockResponse({ + url: `/projects/${organization.slug}/project/issues/`, + method: 'PUT', + statusCode: 500, + }); + + render( + + + + , + {organization} + ); + + await userEvent.click(screen.getByRole('button', {name: 'Resolve'})); + + expect( + await screen.findByText('Unable to update events. Please try again.') + ).toBeInTheDocument(); + expect(screen.queryByText('Issue resolved')).not.toBeInTheDocument(); + }); + it('can archive issue', async () => { const issuesApi = MockApiClient.addMockResponse({ url: `/projects/${organization.slug}/project/issues/`, diff --git a/static/app/views/issueDetails/actions/index.tsx b/static/app/views/issueDetails/actions/index.tsx index d952e30977f9..c852a2ec2be3 100644 --- a/static/app/views/issueDetails/actions/index.tsx +++ b/static/app/views/issueDetails/actions/index.tsx @@ -294,7 +294,7 @@ export function GroupActions({group, project, disabled, event}: GroupActionsProp data, }, { - complete: () => { + success: () => { clearIndicators(); if (successMessage) { addSuccessMessage(successMessage); From 7108c8b376ee9481230c77f039f831fd6ebb3b6d Mon Sep 17 00:00:00 2001 From: Grant Patterson Date: Thu, 3 Sep 2026 16:15:54 -0700 Subject: [PATCH 2/4] also for issue deletion --- .../views/issueDetails/actions/index.spec.tsx | 39 +++++++++++++++++++ .../app/views/issueDetails/actions/index.tsx | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/static/app/views/issueDetails/actions/index.spec.tsx b/static/app/views/issueDetails/actions/index.spec.tsx index d6a64f740241..1c6cb3306fc6 100644 --- a/static/app/views/issueDetails/actions/index.spec.tsx +++ b/static/app/views/issueDetails/actions/index.spec.tsx @@ -291,6 +291,45 @@ describe('GroupActions', () => { ); }); + it('does not report success or navigate when deletion fails', async () => { + const org = OrganizationFixture({ + ...organization, + access: [...organization.access, 'event:admin'], + }); + MockApiClient.addMockResponse({ + url: `/projects/${org.slug}/${project.slug}/issues/`, + method: 'DELETE', + statusCode: 500, + }); + const initialPath = `/organizations/${org.slug}/issues/${group.id}/`; + const {router} = render( + + + + + , + { + organization: org, + initialRouterConfig: { + location: {pathname: initialPath}, + route: '/organizations/:orgId/issues/:groupId/', + }, + } + ); + + await userEvent.click(screen.getByLabelText('More Actions')); + await userEvent.click(await screen.findByRole('menuitemradio', {name: 'Delete'})); + await userEvent.click( + within(screen.getByRole('dialog')).getByRole('button', {name: 'Delete'}) + ); + + expect( + await screen.findByText('Unable to delete events. Please try again.') + ).toBeInTheDocument(); + expect(screen.queryByText('Issue deleted')).not.toBeInTheDocument(); + expect(router.location.pathname).toBe(initialPath); + }); + it('delete for issue platform', async () => { const org = OrganizationFixture({ ...organization, diff --git a/static/app/views/issueDetails/actions/index.tsx b/static/app/views/issueDetails/actions/index.tsx index c852a2ec2be3..103c3fd2553d 100644 --- a/static/app/views/issueDetails/actions/index.tsx +++ b/static/app/views/issueDetails/actions/index.tsx @@ -266,7 +266,7 @@ export function GroupActions({group, project, disabled, event}: GroupActionsProp itemIds: [group.id], }, { - complete: () => { + success: () => { clearIndicators(); addSuccessMessage(t('Issue deleted')); From de712a3b9b14bfbe09faa72fedeb25600b7fc390 Mon Sep 17 00:00:00 2001 From: Grant Patterson Date: Thu, 3 Sep 2026 16:22:17 -0700 Subject: [PATCH 3/4] But still clear the cache and stuff on complete --- .../views/issueDetails/actions/index.spec.tsx | 40 +++++++++++++++++++ .../app/views/issueDetails/actions/index.tsx | 2 + 2 files changed, 42 insertions(+) diff --git a/static/app/views/issueDetails/actions/index.spec.tsx b/static/app/views/issueDetails/actions/index.spec.tsx index 1c6cb3306fc6..bb55beed4bce 100644 --- a/static/app/views/issueDetails/actions/index.spec.tsx +++ b/static/app/views/issueDetails/actions/index.spec.tsx @@ -456,6 +456,46 @@ describe('GroupActions', () => { expect(screen.queryByText('Issue resolved')).not.toBeInTheDocument(); }); + it('refetches group data when resolving fails', async () => { + MockApiClient.addMockResponse({ + url: `/projects/${organization.slug}/project/issues/`, + method: 'PUT', + statusCode: 500, + }); + const groupFetchApi = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/issues/${group.id}/`, + method: 'GET', + body: group, + }); + + function GroupActionsWrapper() { + const {data: groupData, isLoading} = useGroup({groupId: group.id}); + + if (isLoading || !groupData) { + return
Loading...
; + } + + return ( + + ); + } + + render( + + + + , + {organization} + ); + + await waitFor(() => expect(groupFetchApi).toHaveBeenCalledTimes(1)); + await userEvent.click(await screen.findByRole('button', {name: 'Resolve'})); + expect( + await screen.findByText('Unable to update events. Please try again.') + ).toBeInTheDocument(); + await waitFor(() => expect(groupFetchApi).toHaveBeenCalledTimes(2)); + }); + it('can archive issue', async () => { const issuesApi = MockApiClient.addMockResponse({ url: `/projects/${organization.slug}/project/issues/`, diff --git a/static/app/views/issueDetails/actions/index.tsx b/static/app/views/issueDetails/actions/index.tsx index 103c3fd2553d..6e46f46d27a4 100644 --- a/static/app/views/issueDetails/actions/index.tsx +++ b/static/app/views/issueDetails/actions/index.tsx @@ -300,6 +300,8 @@ export function GroupActions({group, project, disabled, event}: GroupActionsProp addSuccessMessage(successMessage); } onComplete?.(); + }, + complete: () => { queryClient.invalidateQueries({ queryKey: groupQueryKey({ organizationSlug: organization.slug, From ceb54effcf3aaf38133158ddee73c6386ab20704 Mon Sep 17 00:00:00 2001 From: Grant Patterson Date: Thu, 3 Sep 2026 17:34:40 -0700 Subject: [PATCH 4/4] same for delete feedback --- .../feedback/useDeleteFeedback.spec.tsx | 97 +++++++++++++++++++ .../components/feedback/useDeleteFeedback.tsx | 4 +- 2 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 static/app/components/feedback/useDeleteFeedback.spec.tsx diff --git a/static/app/components/feedback/useDeleteFeedback.spec.tsx b/static/app/components/feedback/useDeleteFeedback.spec.tsx new file mode 100644 index 000000000000..27951e15bb42 --- /dev/null +++ b/static/app/components/feedback/useDeleteFeedback.spec.tsx @@ -0,0 +1,97 @@ +import {Fragment} from 'react'; +import {OrganizationFixture} from 'sentry-fixture/organization'; + +import { + render, + screen, + userEvent, + waitFor, + within, +} from 'sentry-test/reactTestingLibrary'; + +import {GlobalModal} from '@sentry/scraps/modal'; + +import {clearIndicators} from 'sentry/actionCreators/indicator'; +import {useDeleteFeedback} from 'sentry/components/feedback/useDeleteFeedback'; +import Indicators from 'sentry/components/indicators'; + +const mockRefetchFeedbackList = jest.fn(); + +jest.mock('sentry/components/feedback/list/useRefetchFeedbackList', () => ({ + useRefetchFeedbackList: () => ({refetchFeedbackList: mockRefetchFeedbackList}), +})); + +const organization = OrganizationFixture(); +const initialPath = `/organizations/${organization.slug}/issues/feedback/123/`; + +function DeleteFeedbackButton() { + const deleteFeedback = useDeleteFeedback(['123'], 'project'); + + return ; +} + +function renderDeleteFeedback() { + return render( + + + + + , + { + organization, + initialRouterConfig: { + location: {pathname: initialPath}, + route: '/organizations/:orgId/issues/feedback/:feedbackId/', + }, + } + ); +} + +async function confirmDelete() { + await userEvent.click(screen.getByRole('button', {name: 'Delete feedback'})); + await userEvent.click( + within(await screen.findByRole('dialog')).getByRole('button', {name: 'Delete'}) + ); +} + +describe('useDeleteFeedback', () => { + beforeEach(() => { + clearIndicators(); + mockRefetchFeedbackList.mockClear(); + }); + + it('navigates after deleting feedback successfully', async () => { + MockApiClient.addMockResponse({ + url: `/projects/${organization.slug}/project/issues/`, + method: 'DELETE', + body: {}, + }); + const {router} = renderDeleteFeedback(); + + await confirmDelete(); + + await waitFor(() => { + expect(router.location.pathname).toBe( + `/organizations/${organization.slug}/issues/feedback/` + ); + }); + expect(mockRefetchFeedbackList).toHaveBeenCalledTimes(1); + }); + + it('refetches without navigating when deleting feedback fails', async () => { + MockApiClient.addMockResponse({ + url: `/projects/${organization.slug}/project/issues/`, + method: 'DELETE', + statusCode: 500, + }); + const {router} = renderDeleteFeedback(); + + await confirmDelete(); + + expect( + await screen.findByText('Unable to delete events. Please try again.') + ).toBeInTheDocument(); + expect(mockRefetchFeedbackList).toHaveBeenCalledTimes(1); + expect(router.location.pathname).toBe(initialPath); + }); +}); diff --git a/static/app/components/feedback/useDeleteFeedback.tsx b/static/app/components/feedback/useDeleteFeedback.tsx index 504266b853b9..a6a5918e2711 100644 --- a/static/app/components/feedback/useDeleteFeedback.tsx +++ b/static/app/components/feedback/useDeleteFeedback.tsx @@ -34,8 +34,7 @@ export const useDeleteFeedback = (feedbackIds: any, projectId: any) => { itemIds: feedbackIds, }, { - complete: () => { - refetchFeedbackList(); + success: () => { navigate( normalizeUrl({ pathname: makeFeedbackPathname({ @@ -51,6 +50,7 @@ export const useDeleteFeedback = (feedbackIds: any, projectId: any) => { }) ); }, + complete: refetchFeedbackList, } ); },