Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
12 changes: 12 additions & 0 deletions static/app/views/issueDetails/actions/shareModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Fragment, useRef, useState} from 'react';
import styled from '@emotion/styled';
import {useQueryClient} from '@tanstack/react-query';

import {Button} from '@sentry/scraps/button';
import {Checkbox} from '@sentry/scraps/checkbox';
Expand All @@ -18,6 +19,7 @@ import {useLegacyStore} from 'sentry/stores/useLegacyStore';
import type {Event} from 'sentry/types/event';
import type {Group} from 'sentry/types/group';
import type {Organization} from 'sentry/types/organization';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
import {getAnalyticsDataForEvent, getAnalyticsDataForGroup} from 'sentry/utils/events';
import {normalizeUrl} from 'sentry/utils/url/normalizeUrl';
import {useApi} from 'sentry/utils/useApi';
Expand Down Expand Up @@ -61,6 +63,7 @@ export function ShareIssueModal({
const groups = useLegacyStore(GroupStore);
const group = (groups as Group[]).find(item => item.id === groupId);
const api = useApi({persistInFlight: true});
const queryClient = useQueryClient();
const [loading, setLoading] = useState(false);
const isPublished = group?.isPublic;

Expand Down Expand Up @@ -109,6 +112,15 @@ export function ShareIssueModal({
},
},
{
success: () => {
queryClient.invalidateQueries({
queryKey: [
getApiUrl('/organizations/$organizationIdOrSlug/issues/$issueId/', {
path: {organizationIdOrSlug: organization.slug, issueId: groupId},
}),
],
});
},
error: () => {
addErrorMessage(t('Error sharing'));
},
Expand Down
14 changes: 14 additions & 0 deletions static/app/views/issueDetails/groupPriority.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {useQueryClient} from '@tanstack/react-query';

import {bulkUpdate} from 'sentry/actionCreators/group';
import {
addErrorMessage,
Expand All @@ -12,6 +14,7 @@ import {t} from 'sentry/locale';
import {IssueListCacheStore} from 'sentry/stores/IssueListCacheStore';
import {PriorityLevel, type Group} from 'sentry/types/group';
import {trackAnalytics} from 'sentry/utils/analytics';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
import {getAnalyticsDataForGroup} from 'sentry/utils/events';
import {useApi} from 'sentry/utils/useApi';
import {useOrganization} from 'sentry/utils/useOrganization';
Expand All @@ -33,6 +36,7 @@ const getPriorityUpdateSuccessMessage = (priority: PriorityLevel) =>
function useChangePriority(group: Group, onChange?: (priority: PriorityLevel) => void) {
const api = useApi({persistInFlight: true});
const organization = useOrganization();
const queryClient = useQueryClient();

return (nextPriority: PriorityLevel) => {
if (nextPriority === group.priority) {
Expand Down Expand Up @@ -60,6 +64,16 @@ function useChangePriority(group: Group, onChange?: (priority: PriorityLevel) =>
},
{
success: () => {
queryClient.invalidateQueries({
queryKey: [
getApiUrl('/organizations/$organizationIdOrSlug/issues/$issueId/', {
path: {
organizationIdOrSlug: organization.slug,
issueId: group.id,
},
}),
],
});
clearIndicators();
addSuccessMessage(getPriorityUpdateSuccessMessage(nextPriority));
onChange?.(nextPriority);
Expand Down
94 changes: 94 additions & 0 deletions static/app/views/issueDetails/useAssignIssueMutation.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import {ActorFixture} from 'sentry-fixture/actor';
import {GroupFixture} from 'sentry-fixture/group';
import {OrganizationFixture} from 'sentry-fixture/organization';

import {act, renderHookWithProviders, waitFor} from 'sentry-test/reactTestingLibrary';

import {GroupStore} from 'sentry/stores/groupStore';
import {useAssignIssueMutation} from 'sentry/views/issueDetails/useAssignIssueMutation';
import {useGroup} from 'sentry/views/issueDetails/useGroup';

describe('useAssignIssueMutation', () => {
const organization = OrganizationFixture();

beforeEach(() => {
MockApiClient.clearMockResponses();
GroupStore.reset();
});

it('assigns an issue', async () => {
const group = GroupFixture({id: '1'});
const assignee = ActorFixture({id: '2', type: 'user'});
const assignRequest = MockApiClient.addMockResponse({
method: 'PUT',
url: `/organizations/${organization.slug}/issues/${group.id}/`,
body: {...group, assignedTo: assignee},
});
const {result} = renderHookWithProviders(useAssignIssueMutation, {organization});

await act(async () => {
await result.current.mutateAsync({
actor: assignee,
assignedBy: 'assignee_selector',
groupId: group.id,
orgSlug: organization.slug,
});
});

expect(assignRequest).toHaveBeenCalledWith(
`/organizations/${organization.slug}/issues/${group.id}/`,
expect.objectContaining({
method: 'PUT',
data: {assignedTo: `user:${assignee.id}`, assignedBy: 'assignee_selector'},
})
);
});

it.each([
{
name: 'assignment',
assignedTo: ActorFixture({id: '2', type: 'user'}),
},
{
name: 'unassignment',
assignedTo: null,
},
])('invalidates the group query after an $name', async ({assignedTo}) => {
const group = GroupFixture({id: '1', activity: []});
const response = {...group, assignedTo};

GroupStore.add([group]);
const assignRequest = MockApiClient.addMockResponse({
method: 'PUT',
url: `/organizations/${organization.slug}/issues/${group.id}/`,
body: response,
});
const groupRequest = MockApiClient.addMockResponse({
method: 'GET',
url: `/organizations/${organization.slug}/issues/${group.id}/`,
body: group,
});

const {result} = renderHookWithProviders(
() => {
useGroup({groupId: group.id});
return useAssignIssueMutation();
},
{organization}
);

await waitFor(() => expect(groupRequest).toHaveBeenCalledTimes(1));

await act(async () => {
await result.current.mutateAsync({
actor: assignedTo,
groupId: group.id,
orgSlug: organization.slug,
});
});

expect(assignRequest).toHaveBeenCalledTimes(1);
await waitFor(() => expect(groupRequest).toHaveBeenCalledTimes(2));
expect(GroupStore.get(group.id)?.assignedTo).toEqual(assignedTo);
});
});
34 changes: 21 additions & 13 deletions static/app/views/issueDetails/useAssignIssueMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import {GroupStore} from 'sentry/stores/groupStore';
import type {Actor} from 'sentry/types/core';
import type {Group} from 'sentry/types/group';
import {buildTeamId, buildUserId} from 'sentry/utils';
import type {ApiResponse} from 'sentry/utils/api/apiFetch';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
import {uniqueId} from 'sentry/utils/guid';
import {fetchMutation} from 'sentry/utils/queryClient';
import type {RequestError} from 'sentry/utils/requestError/requestError';
import {groupApiOptions} from 'sentry/views/issueDetails/useGroup';
import {useEnvironmentsFromUrl} from 'sentry/views/issueDetails/utils';

export type AssignedBy = 'suggested_assignee' | 'assignee_selector';

Expand Down Expand Up @@ -64,15 +64,19 @@ export function useAssignIssueMutation(
> = {}
) {
const queryClient = useQueryClient();
const environments = useEnvironmentsFromUrl();

return useMutation<Group, RequestError, AssignIssueVariables, AssignIssueContext>({
...options,
mutationFn: variables => {
const actorId = variables.actor ? makeActorId(variables.actor) : '';
return fetchMutation<Group>({
method: 'PUT',
url: `/organizations/${variables.orgSlug}/issues/${variables.groupId}/`,
url: getApiUrl('/organizations/$organizationIdOrSlug/issues/$issueId/', {
path: {
organizationIdOrSlug: variables.orgSlug,
issueId: variables.groupId,
},
}),
data: {
assignedTo: actorId,
assignedBy: variables.assignedBy,
Expand All @@ -87,19 +91,23 @@ export function useAssignIssueMutation(
return {changeId};
},
onSuccess: (response, variables, onMutateResult, context) => {
// Update react query cache so that useGroup() reflects the new assignee
queryClient.setQueryData(
groupApiOptions({
organizationSlug: variables.orgSlug,
groupId: variables.groupId,
environments,
}).queryKey,
prev =>
prev ? {...prev, json: {...prev.json, assignedTo: response.assignedTo}} : prev
const queryKey = [

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Might be nice to expose this as a util we can reuse to update the group data that isn't environment specific. I also had to do this for the activity section when adding comments, but I think I ended up passing environments.

getApiUrl('/organizations/$organizationIdOrSlug/issues/$issueId/', {
path: {
organizationIdOrSlug: variables.orgSlug,
issueId: variables.groupId,
},
}),
];

// Update react query cache so that useGroup() reflects the new assignee.
queryClient.setQueriesData<ApiResponse<Group>>({queryKey}, prev =>
prev ? {...prev, json: {...prev.json, assignedTo: response.assignedTo}} : prev
);
// Dual-write to GroupStore
// TODO: Remove this when we no longer rely on GroupStore for updates
GroupStore.onAssignToSuccess(onMutateResult.changeId, variables.groupId, response);
queryClient.invalidateQueries({queryKey});
addSuccessMessage(getAssignIssueSuccessMessage(response.assignedTo));
options.onSuccess?.(response, variables, onMutateResult, context);
},
Expand Down
Loading