diff --git a/static/app/actionCreators/group.spec.tsx b/static/app/actionCreators/group.spec.tsx index 7d0f7fe2c982..798afbafbb92 100644 --- a/static/app/actionCreators/group.spec.tsx +++ b/static/app/actionCreators/group.spec.tsx @@ -78,23 +78,19 @@ describe('group', () => { jest.spyOn(GroupStore, 'onUpdate'); // stub GroupStore.onUpdate call from update }); - it('should use itemIds as query if provided', () => { + it('should use itemIds as query if provided', async () => { const request = MockApiClient.addMockResponse({ url: '/projects/1337/1337/issues/', method: 'PUT', }); - bulkUpdate( - new MockApiClient(), - { - orgId: '1337', - projectId: '1337', - itemIds: ['1', '2', '3'], - data: {status: 'unresolved'}, - query: 'is:resolved', - }, - {} - ); + await bulkUpdate(new MockApiClient(), { + orgId: '1337', + projectId: '1337', + itemIds: ['1', '2', '3'], + data: {status: 'unresolved'}, + query: 'is:resolved', + }); expect(request).toHaveBeenCalledTimes(1); expect(request).toHaveBeenCalledWith( @@ -103,23 +99,19 @@ describe('group', () => { ); }); - it('should use query as query if itemIds are absent', () => { + it('should use query as query if itemIds are absent', async () => { const request = MockApiClient.addMockResponse({ url: '/projects/1337/1337/issues/', method: 'PUT', }); - bulkUpdate( - new MockApiClient(), - { - orgId: '1337', - projectId: '1337', - itemIds: undefined, - data: {status: 'unresolved'}, - query: 'is:resolved', - }, - {} - ); + await bulkUpdate(new MockApiClient(), { + orgId: '1337', + projectId: '1337', + itemIds: undefined, + data: {status: 'unresolved'}, + query: 'is:resolved', + }); expect(request).toHaveBeenCalledTimes(1); expect(request).toHaveBeenCalledWith( @@ -128,22 +120,18 @@ describe('group', () => { ); }); - it('should apply project option', () => { + it('should apply project option', async () => { const request = MockApiClient.addMockResponse({ url: '/organizations/1337/issues/', method: 'PUT', }); - bulkUpdate( - new MockApiClient(), - { - orgId: '1337', - project: [99], - itemIds: ['1', '2', '3'], - data: {status: 'unresolved'}, - }, - {} - ); + await bulkUpdate(new MockApiClient(), { + orgId: '1337', + project: [99], + itemIds: ['1', '2', '3'], + data: {status: 'unresolved'}, + }); expect(request).toHaveBeenCalledTimes(1); expect(request).toHaveBeenCalledWith( @@ -152,88 +140,72 @@ describe('group', () => { ); }); - it('should normalize string assignedTo to Actor object for optimistic update', () => { + it('should normalize string assignedTo to Actor object for optimistic update', async () => { MockApiClient.addMockResponse({ url: '/projects/1337/1337/issues/', method: 'PUT', }); - bulkUpdate( - new MockApiClient(), - { - orgId: '1337', - projectId: '1337', - itemIds: ['1'], - data: {assignedTo: 'user:123'}, - }, - {} - ); + await bulkUpdate(new MockApiClient(), { + orgId: '1337', + projectId: '1337', + itemIds: ['1'], + data: {assignedTo: 'user:123'}, + }); expect(GroupStore.onUpdate).toHaveBeenCalledWith(expect.any(String), ['1'], { assignedTo: {type: 'user', id: '123', name: ''}, }); }); - it('should normalize empty assignedTo string to null for optimistic update', () => { + it('should normalize empty assignedTo string to null for optimistic update', async () => { MockApiClient.addMockResponse({ url: '/projects/1337/1337/issues/', method: 'PUT', }); - bulkUpdate( - new MockApiClient(), - { - orgId: '1337', - projectId: '1337', - itemIds: ['1'], - data: {assignedTo: ''}, - }, - {} - ); + await bulkUpdate(new MockApiClient(), { + orgId: '1337', + projectId: '1337', + itemIds: ['1'], + data: {assignedTo: ''}, + }); expect(GroupStore.onUpdate).toHaveBeenCalledWith(expect.any(String), ['1'], { assignedTo: null, }); }); - it('should normalize team assignedTo string for optimistic update', () => { + it('should normalize team assignedTo string for optimistic update', async () => { MockApiClient.addMockResponse({ url: '/projects/1337/1337/issues/', method: 'PUT', }); - bulkUpdate( - new MockApiClient(), - { - orgId: '1337', - projectId: '1337', - itemIds: ['1'], - data: {assignedTo: 'team:456'}, - }, - {} - ); + await bulkUpdate(new MockApiClient(), { + orgId: '1337', + projectId: '1337', + itemIds: ['1'], + data: {assignedTo: 'team:456'}, + }); expect(GroupStore.onUpdate).toHaveBeenCalledWith(expect.any(String), ['1'], { assignedTo: {type: 'team', id: '456', name: ''}, }); }); - it('should send raw string assignedTo to the API', () => { + it('should send raw string assignedTo to the API', async () => { const request = MockApiClient.addMockResponse({ url: '/projects/1337/1337/issues/', method: 'PUT', }); - bulkUpdate( - new MockApiClient(), - { - orgId: '1337', - projectId: '1337', - itemIds: ['1'], - data: {assignedTo: 'user:123'}, - }, - {} - ); + await bulkUpdate(new MockApiClient(), { + orgId: '1337', + projectId: '1337', + itemIds: ['1'], + data: {assignedTo: 'user:123'}, + }); expect(request).toHaveBeenCalledWith( '/projects/1337/1337/issues/', @@ -249,22 +221,18 @@ describe('group', () => { jest.spyOn(GroupStore, 'onMerge'); // stub GroupStore.onMerge call from mergeGroups }); - it('should use itemIds as query if provided', () => { + it('should use itemIds as query if provided', async () => { const request = MockApiClient.addMockResponse({ url: '/projects/1337/1337/issues/', method: 'PUT', }); - mergeGroups( - new MockApiClient(), - { - orgId: '1337', - projectId: '1337', - itemIds: ['1', '2', '3'], - query: 'is:resolved', - }, - {} - ); + await mergeGroups(new MockApiClient(), { + orgId: '1337', + projectId: '1337', + itemIds: ['1', '2', '3'], + query: 'is:resolved', + }); expect(request).toHaveBeenCalledTimes(1); expect(request).toHaveBeenCalledWith( @@ -273,22 +241,18 @@ describe('group', () => { ); }); - it('should use query as query if itemIds are absent', () => { + it('should use query as query if itemIds are absent', async () => { const request = MockApiClient.addMockResponse({ url: '/projects/1337/1337/issues/', method: 'PUT', }); - mergeGroups( - new MockApiClient(), - { - orgId: '1337', - projectId: '1337', - itemIds: undefined, - query: 'is:resolved', - }, - {} - ); + await mergeGroups(new MockApiClient(), { + orgId: '1337', + projectId: '1337', + itemIds: undefined, + query: 'is:resolved', + }); expect(request).toHaveBeenCalledTimes(1); expect(request).toHaveBeenCalledWith( diff --git a/static/app/actionCreators/group.tsx b/static/app/actionCreators/group.tsx index 43c3e3737f50..30279224b135 100644 --- a/static/app/actionCreators/group.tsx +++ b/static/app/actionCreators/group.tsx @@ -121,11 +121,7 @@ type BulkUpdateParams = UpdateParams & { failSilently?: boolean; }; -export async function bulkUpdate( - api: Client, - params: BulkUpdateParams, - options: RequestCallbacks = {} -) { +export async function bulkUpdate(api: Client, params: BulkUpdateParams) { const {itemIds, failSilently, data} = params; const path = getUpdateUrl(params); @@ -138,35 +134,23 @@ export async function bulkUpdate( : data; GroupStore.onUpdate(id, itemIds, optimisticData); - let responseMeta: any; - let statusText: string | undefined; - try { - const [response, status, meta] = await api.requestPromise(path, { + const [response] = await api.requestPromise(path, { query, method: 'PUT', data, includeAllArgs: true, }); - statusText = status; - responseMeta = meta; GroupStore.onUpdateSuccess(id, itemIds, response); - options?.success?.(response, statusText, responseMeta); } catch (error) { GroupStore.onUpdateError(id, itemIds, !!failSilently); - options?.error?.(error); - } finally { - options?.complete?.(responseMeta, statusText ?? ''); + throw error; } } type MergeGroupsParams = UpdateParams; -export async function mergeGroups( - api: Client, - params: MergeGroupsParams, - options: RequestCallbacks = {} -) { +export async function mergeGroups(api: Client, params: MergeGroupsParams) { const {itemIds} = params; const path = getUpdateUrl(params); @@ -175,25 +159,18 @@ export async function mergeGroups( GroupStore.onMerge(id, itemIds); - let responseMeta: any; - let statusText: string | undefined; - try { - const [response, status, meta] = await api.requestPromise(path, { + const [response] = await api.requestPromise(path, { query, method: 'PUT', data: {merge: 1}, includeAllArgs: true, }); - statusText = status; - responseMeta = meta; GroupStore.onMergeSuccess(id, itemIds, response); - options?.success?.(response, statusText, responseMeta); + return response; } catch (error) { GroupStore.onMergeError(id, itemIds, error); - options?.error?.(error); - } finally { - options?.complete?.(responseMeta, statusText ?? ''); + throw error; } } diff --git a/static/app/views/issueDetails/actions/index.tsx b/static/app/views/issueDetails/actions/index.tsx index 9024595ae706..904716c89059 100644 --- a/static/app/views/issueDetails/actions/index.tsx +++ b/static/app/views/issueDetails/actions/index.tsx @@ -285,36 +285,9 @@ export function GroupActions({group, project, disabled, event}: GroupActionsProp IssueListCacheStore.reset(); }; - const onUpdate = (data: UpdateData, onComplete?: () => void) => { + const onUpdate = async (data: UpdateData, onComplete?: () => void) => { const successMessage = getUpdateSuccessMessage(group, data); - bulkUpdate( - api, - { - orgId: organization.slug, - projectId: project.slug, - itemIds: [group.id], - data, - }, - { - success: () => { - clearIndicators(); - if (successMessage) { - addSuccessMessage(successMessage); - } - onComplete?.(); - }, - complete: () => { - queryClient.invalidateQueries({ - queryKey: groupQueryKey({ - organizationSlug: organization.slug, - groupId: group.id, - }), - }); - }, - } - ); - if (isResolutionStatus(data)) { trackIssueAction( data.status, @@ -326,6 +299,29 @@ export function GroupActions({group, project, disabled, event}: GroupActionsProp trackIssueAction('mark_reviewed'); } IssueListCacheStore.reset(); + + try { + await bulkUpdate(api, { + orgId: organization.slug, + projectId: project.slug, + itemIds: [group.id], + data, + }); + clearIndicators(); + if (successMessage) { + addSuccessMessage(successMessage); + } + onComplete?.(); + } catch { + // GroupStore already shows the error + } finally { + queryClient.invalidateQueries({ + queryKey: groupQueryKey({ + organizationSlug: organization.slug, + groupId: group.id, + }), + }); + } }; const onReprocessEvent = () => { diff --git a/static/app/views/issueDetails/actions/shareModal.tsx b/static/app/views/issueDetails/actions/shareModal.tsx index 48d9ed461f97..921e51a0dd20 100644 --- a/static/app/views/issueDetails/actions/shareModal.tsx +++ b/static/app/views/issueDetails/actions/shareModal.tsx @@ -94,40 +94,33 @@ export function ShareIssueModal({ ); }; - const handlePublicShare = ( + const handlePublicShare = async ( e: React.ChangeEvent | null, reshare?: boolean ) => { e?.preventDefault(); setLoading(true); onToggle(); - bulkUpdate( - api, - { + try { + await bulkUpdate(api, { orgId: organization.slug, projectId: projectSlug, itemIds: [groupId], data: { isPublic: reshare ?? !isPublished, }, - }, - { - success: () => { - queryClient.invalidateQueries({ - queryKey: groupQueryKey({ - organizationSlug: organization.slug, - groupId, - }), - }); - }, - error: () => { - addErrorMessage(t('Error sharing')); - }, - complete: () => { - setLoading(false); - }, - } - ); + }); + queryClient.invalidateQueries({ + queryKey: groupQueryKey({ + organizationSlug: organization.slug, + groupId, + }), + }); + } catch { + addErrorMessage(t('Error sharing')); + } finally { + setLoading(false); + } }; const shareUrl = group?.shareId ? getShareUrl(organization, group) : null; diff --git a/static/app/views/issueDetails/groupPriority.tsx b/static/app/views/issueDetails/groupPriority.tsx index 24e33ba124b8..850e47115a1b 100644 --- a/static/app/views/issueDetails/groupPriority.tsx +++ b/static/app/views/issueDetails/groupPriority.tsx @@ -38,7 +38,7 @@ function useChangePriority(group: Group, onChange?: (priority: PriorityLevel) => const organization = useOrganization(); const queryClient = useQueryClient(); - return (nextPriority: PriorityLevel) => { + return async (nextPriority: PriorityLevel) => { if (nextPriority === group.priority) { return; } @@ -53,33 +53,27 @@ function useChangePriority(group: Group, onChange?: (priority: PriorityLevel) => addLoadingMessage(t('Saving changes\u2026')); IssueListCacheStore.reset(); - bulkUpdate( - api, - { + try { + await bulkUpdate(api, { orgId: organization.slug, itemIds: [group.id], data: {priority: nextPriority}, failSilently: true, project: [group.project.id], - }, - { - success: () => { - queryClient.invalidateQueries({ - queryKey: groupQueryKey({ - organizationSlug: organization.slug, - groupId: group.id, - }), - }); - clearIndicators(); - addSuccessMessage(getPriorityUpdateSuccessMessage(nextPriority)); - onChange?.(nextPriority); - }, - error: () => { - clearIndicators(); - addErrorMessage(t('Unable to update issue priority')); - }, - } - ); + }); + queryClient.invalidateQueries({ + queryKey: groupQueryKey({ + organizationSlug: organization.slug, + groupId: group.id, + }), + }); + clearIndicators(); + addSuccessMessage(getPriorityUpdateSuccessMessage(nextPriority)); + onChange?.(nextPriority); + } catch { + clearIndicators(); + addErrorMessage(t('Unable to update issue priority')); + } }; } diff --git a/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/index.tsx b/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/index.tsx index 3d6849ccb40c..0c42bfecf512 100644 --- a/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/index.tsx +++ b/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/index.tsx @@ -94,21 +94,11 @@ export function SimilarStackTrace({project}: Props) { {ids: string[]; projectSlug: string; query?: string} >({ mutationFn: ({ids, projectSlug, query}) => - new Promise((resolve, reject) => { - mergeGroups( - api, - { - orgId: organization.slug, - projectId: projectSlug, - itemIds: [...ids, params.groupId], - query, - }, - { - success: (data: any) => resolve(data), - error: (err: any) => - reject(err instanceof Error ? err : new Error('Failed to merge issues')), - } - ); + mergeGroups(api, { + orgId: organization.slug, + projectId: projectSlug, + itemIds: [...ids, params.groupId], + query, }), onSuccess: data => { if (data?.merge?.parent && data.merge.parent !== params.groupId) { diff --git a/static/app/views/issueDetails/utils.tsx b/static/app/views/issueDetails/utils.tsx index c463db0de628..b54fe5e91496 100644 --- a/static/app/views/issueDetails/utils.tsx +++ b/static/app/views/issueDetails/utils.tsx @@ -22,25 +22,25 @@ import {useGroupData} from 'sentry/views/issueDetails/groupDataContext'; import {useGroupId} from 'sentry/views/issueDetails/groupIdContext'; import {useGroupTags} from 'sentry/views/issueDetails/groupTags/useGroupTags'; -export function markEventSeen( +export async function markEventSeen( api: Client, orgId: string, projectId: string, groupId: string ) { - bulkUpdate( - api, - { + IssueListCacheStore.markGroupAsSeen(groupId); + + try { + await bulkUpdate(api, { orgId, projectId, itemIds: [groupId], failSilently: true, data: {hasSeen: true}, - }, - {} - ); - - IssueListCacheStore.markGroupAsSeen(groupId); + }); + } catch { + // Marking an issue as seen is best-effort + } } export function useDefaultIssueEvent() { diff --git a/static/app/views/issueList/actions/index.tsx b/static/app/views/issueList/actions/index.tsx index aa7343436264..6092ea4c63d0 100644 --- a/static/app/views/issueList/actions/index.tsx +++ b/static/app/views/issueList/actions/index.tsx @@ -222,19 +222,7 @@ export function IssueListActions({ } function handleMerge() { - actionSelectedGroups(itemIds => { - mergeGroups( - api, - { - orgId: organization.slug, - itemIds, - query: queryExcludingPerformanceIssues, - project: selection.projects, - environment: selection.environments, - ...selection.datetime, - }, - {} - ); + actionSelectedGroups(async itemIds => { if (selection.projects[0]) { const trackProject = ProjectsStore.getById(`${selection.projects[0]}`); trackAnalytics('issues_stream.merged', { @@ -245,6 +233,19 @@ export function IssueListActions({ area, }); } + + try { + await mergeGroups(api, { + orgId: organization.slug, + itemIds, + query: queryExcludingPerformanceIssues, + project: selection.projects, + environment: selection.environments, + ...selection.datetime, + }); + } catch { + // GroupStore already shows the error + } }); } diff --git a/static/app/views/issueList/actions/utils.tsx b/static/app/views/issueList/actions/utils.tsx index 6b646db31357..c0022e9e3739 100644 --- a/static/app/views/issueList/actions/utils.tsx +++ b/static/app/views/issueList/actions/utils.tsx @@ -234,7 +234,7 @@ export function invalidateIssueQueries({ }); } -export function performBulkUpdate({ +export async function performBulkUpdate({ api, data, itemIds, @@ -257,9 +257,8 @@ export function performBulkUpdate({ addLoadingMessage(t('Saving changes…')); - bulkUpdate( - api, - { + try { + await bulkUpdate(api, { orgId: organizationSlug, itemIds, data, @@ -268,16 +267,11 @@ export function performBulkUpdate({ failSilently: true, ...projectConstraints, ...selection.datetime, - }, - { - success: () => { - clearIndicators(); - onSuccess?.(itemIds); - }, - error: () => { - clearIndicators(); - addErrorMessage(t('Unable to update issues')); - }, - } - ); + }); + clearIndicators(); + onSuccess?.(itemIds); + } catch { + clearIndicators(); + addErrorMessage(t('Unable to update issues')); + } } diff --git a/static/app/views/issueList/pages/inbox/issuePreview/issuePreviewActions.tsx b/static/app/views/issueList/pages/inbox/issuePreview/issuePreviewActions.tsx index bc8f1e118891..da3cfb631afb 100644 --- a/static/app/views/issueList/pages/inbox/issuePreview/issuePreviewActions.tsx +++ b/static/app/views/issueList/pages/inbox/issuePreview/issuePreviewActions.tsx @@ -148,65 +148,7 @@ function IssueResolutionActions({ const organization = useOrganization(); const location = useLocation(); const queryClient = useQueryClient(); - function handleUpdate(data: GroupStatusResolution) { - bulkUpdate( - api, - { - orgId: organization.slug, - projectId: project.slug, - itemIds: [group.id], - data, - }, - { - success: () => { - clearIndicators(); - addSuccessMessage( - data.status === GroupStatus.UNRESOLVED - ? t('Issue marked unresolved') - : t('Issue resolved') - ); - IssueListCacheStore.reset(); - const issueListUrl = getApiUrl('/organizations/$organizationIdOrSlug/issues/', { - path: {organizationIdOrSlug: organization.slug}, - }); - const issueCountUrl = getApiUrl( - '/organizations/$organizationIdOrSlug/issues-count/', - {path: {organizationIdOrSlug: organization.slug}} - ); - const issueUrl = getApiUrl( - '/organizations/$organizationIdOrSlug/issues/$issueId/', - { - path: { - organizationIdOrSlug: organization.slug, - issueId: group.id, - }, - } - ); - const issueActivitiesUrl = getApiUrl( - '/organizations/$organizationIdOrSlug/issues/$issueId/activities/', - { - path: { - organizationIdOrSlug: organization.slug, - issueId: group.id, - }, - } - ); - void queryClient.invalidateQueries({ - predicate: query => { - const url = safeParseQueryKey(query.queryKey)?.url; - - return ( - url === issueListUrl || - url === issueCountUrl || - url === issueUrl || - url === issueActivitiesUrl - ); - }, - }); - }, - } - ); - + async function handleUpdate(data: GroupStatusResolution) { const {alert_date, alert_rule_id, alert_type} = location.query; trackAnalytics('issue_inbox.resolve_clicked', { organization, @@ -221,6 +163,61 @@ function IssueResolutionActions({ ...getAnalyicsDataForProject(project), org_streamline_only: organization.streamlineOnly ?? undefined, }); + + try { + await bulkUpdate(api, { + orgId: organization.slug, + projectId: project.slug, + itemIds: [group.id], + data, + }); + clearIndicators(); + addSuccessMessage( + data.status === GroupStatus.UNRESOLVED + ? t('Issue marked unresolved') + : t('Issue resolved') + ); + IssueListCacheStore.reset(); + const issueListUrl = getApiUrl('/organizations/$organizationIdOrSlug/issues/', { + path: {organizationIdOrSlug: organization.slug}, + }); + const issueCountUrl = getApiUrl( + '/organizations/$organizationIdOrSlug/issues-count/', + {path: {organizationIdOrSlug: organization.slug}} + ); + const issueUrl = getApiUrl( + '/organizations/$organizationIdOrSlug/issues/$issueId/', + { + path: { + organizationIdOrSlug: organization.slug, + issueId: group.id, + }, + } + ); + const issueActivitiesUrl = getApiUrl( + '/organizations/$organizationIdOrSlug/issues/$issueId/activities/', + { + path: { + organizationIdOrSlug: organization.slug, + issueId: group.id, + }, + } + ); + void queryClient.invalidateQueries({ + predicate: query => { + const url = safeParseQueryKey(query.queryKey)?.url; + + return ( + url === issueListUrl || + url === issueCountUrl || + url === issueUrl || + url === issueActivitiesUrl + ); + }, + }); + } catch { + // GroupStore already shows the error + } } return (