diff --git a/static/app/components/avatar/actorAvatar.spec.tsx b/static/app/components/avatar/actorAvatar.spec.tsx index df6dab345a2d8f..b3326a672a53ba 100644 --- a/static/app/components/avatar/actorAvatar.spec.tsx +++ b/static/app/components/avatar/actorAvatar.spec.tsx @@ -96,4 +96,32 @@ describe('ActorAvatar', function () { expect(await screen.findByText('CT')).toBeInTheDocument(); expect(mockRequest).toHaveBeenCalled(); }); + + it('should fetch a user not in the store', async function () { + const organization = OrganizationFixture(); + + OrganizationStore.onUpdate(organization, {replace: true}); + + const user2 = UserFixture({id: '2', name: 'COOL USER'}); + + const mockRequest = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/members/`, + method: 'GET', + body: [{user: user2}], + }); + + render( + + ); + + expect(await screen.findByText('CU')).toBeInTheDocument(); + expect(mockRequest).toHaveBeenCalled(); + }); }); diff --git a/static/app/components/avatar/actorAvatar.tsx b/static/app/components/avatar/actorAvatar.tsx index 279272ca2dbec1..591408aff296a7 100644 --- a/static/app/components/avatar/actorAvatar.tsx +++ b/static/app/components/avatar/actorAvatar.tsx @@ -1,10 +1,11 @@ +import {useMemo} from 'react'; import * as Sentry from '@sentry/react'; import TeamAvatar from 'sentry/components/avatar/teamAvatar'; import UserAvatar from 'sentry/components/avatar/userAvatar'; -import LoadingIndicator from 'sentry/components/loadingIndicator'; -import MemberListStore from 'sentry/stores/memberListStore'; +import Placeholder from 'sentry/components/placeholder'; import type {Actor} from 'sentry/types'; +import {useMembers} from 'sentry/utils/useMembers'; import {useTeamsById} from 'sentry/utils/useTeamsById'; import type {BaseAvatarProps} from './baseAvatar'; @@ -24,12 +25,32 @@ function LoadTeamAvatar({ const team = teams.find(t => t.id === teamId); if (isLoading) { - return ; + const size = `${props.size}px`; + return ; } return ; } +/** + * Wrapper to assist loading the user from api or store + */ +function LoadMemberAvatar({ + userId, + ...props +}: {userId: string} & Omit, 'team'>) { + const ids = useMemo(() => [userId], [userId]); + const {members, fetching} = useMembers({ids}); + const user = members.find(u => u.id === userId); + + if (fetching) { + const size = `${props.size}px`; + return ; + } + + return ; +} + function ActorAvatar({size = 24, hasTooltip = true, actor, ...props}: Props) { const otherProps = { size, @@ -38,8 +59,7 @@ function ActorAvatar({size = 24, hasTooltip = true, actor, ...props}: Props) { }; if (actor.type === 'user') { - const user = actor.id ? MemberListStore.getById(actor.id) ?? actor : actor; - return ; + return ; } if (actor.type === 'team') { diff --git a/static/app/components/avatar/index.spec.tsx b/static/app/components/avatar/index.spec.tsx index 7971f0cd405df1..bd5be1eedc59b6 100644 --- a/static/app/components/avatar/index.spec.tsx +++ b/static/app/components/avatar/index.spec.tsx @@ -3,11 +3,13 @@ import {OrganizationFixture} from 'sentry-fixture/organization'; import {ProjectFixture} from 'sentry-fixture/project'; import {SentryAppFixture} from 'sentry-fixture/sentryApp'; import {TeamFixture} from 'sentry-fixture/team'; +import {UserFixture} from 'sentry-fixture/user'; import {render, screen} from 'sentry-test/reactTestingLibrary'; import AvatarComponent from 'sentry/components/avatar'; import ConfigStore from 'sentry/stores/configStore'; +import MemberListStore from 'sentry/stores/memberListStore'; import type {Avatar} from 'sentry/types'; describe('Avatar', function () { @@ -17,14 +19,14 @@ describe('Avatar', function () { avatarUrl: 'https://sentry.io/avatar/2d641b5d-8c74-44de-9cb6-fbd54701b35e/', }; - const user = { + const user = UserFixture({ id: '1', name: 'Jane Bloggs', username: 'janebloggs@example.com', email: 'janebloggs@example.com', ip_address: '127.0.0.1', avatar, - }; + }); window.__initialData = { links: {sentryUrl: 'https://sentry.io'}, @@ -121,7 +123,12 @@ describe('Avatar', function () { }); it('should show a gravatar when no avatar type is set and user has an email address', async function () { - render(); + render( + + ); await screen.findByRole('img'); const avatarElement = screen.getByTestId(`gravatar-avatar`); @@ -175,13 +182,14 @@ describe('Avatar', function () { ); }); - it('can display a actor Avatar', function () { + it('can display a actor Avatar', async function () { const actor = ActorFixture(); + MemberListStore.loadInitialData([user]); render(); - expect(screen.getByTestId(`letter_avatar-avatar`)).toBeInTheDocument(); - expect(screen.getByText('FB')).toBeInTheDocument(); + expect(await screen.findByTestId(`letter_avatar-avatar`)).toBeInTheDocument(); + expect(screen.getByText('JB')).toBeInTheDocument(); }); it('displays platform list icons for project Avatar', function () { diff --git a/static/app/views/dashboards/widgetCard/issueWidgetCard.spec.tsx b/static/app/views/dashboards/widgetCard/issueWidgetCard.spec.tsx index fcf4cd0e57ce28..bd211b893851e1 100644 --- a/static/app/views/dashboards/widgetCard/issueWidgetCard.spec.tsx +++ b/static/app/views/dashboards/widgetCard/issueWidgetCard.spec.tsx @@ -1,4 +1,5 @@ import {OrganizationFixture} from 'sentry-fixture/organization'; +import {UserFixture} from 'sentry-fixture/user'; import {initializeOrg} from 'sentry-test/initializeOrg'; import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; @@ -44,6 +45,7 @@ describe('Dashboards > IssueWidgetCard', function () { }, }; + const user = UserFixture(); const api = new MockApiClient(); beforeEach(function () { @@ -56,9 +58,9 @@ describe('Dashboards > IssueWidgetCard', function () { shortId: 'ISSUE', assignedTo: { type: 'user', - id: '2222222', - name: 'dashboard user', - email: 'dashboarduser@sentry.io', + id: user.id, + name: user.name, + email: user.email, }, lifetime: {count: 10, userCount: 5}, count: 6, @@ -70,7 +72,7 @@ describe('Dashboards > IssueWidgetCard', function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/users/', method: 'GET', - body: [], + body: [user], }); }); @@ -79,7 +81,7 @@ describe('Dashboards > IssueWidgetCard', function () { }); it('renders with title and issues chart', async function () { - MemberListStore.loadInitialData([]); + MemberListStore.loadInitialData([user]); render( IssueWidgetCard', function () { expect(await screen.findByText('assignee')).toBeInTheDocument(); expect(screen.getByText('title')).toBeInTheDocument(); expect(screen.getByText('issue')).toBeInTheDocument(); - expect(screen.getByText('DU')).toBeInTheDocument(); + expect(screen.getByText('FB')).toBeInTheDocument(); expect(screen.getByText('ISSUE')).toBeInTheDocument(); expect( screen.getByText('ChunkLoadError: Loading chunk app_bootstrap_index_tsx failed.') ).toBeInTheDocument(); - await userEvent.hover(screen.getByTitle('dashboard user')); - expect(await screen.findByText('Assigned to dashboard user')).toBeInTheDocument(); + await userEvent.hover(screen.getByTitle(user.name)); + expect(await screen.findByText(`Assigned to ${user.name}`)).toBeInTheDocument(); }); it('opens in issues page', async function () { @@ -186,7 +188,7 @@ describe('Dashboards > IssueWidgetCard', function () { }); it('maps lifetimeEvents and lifetimeUsers headers to more human readable', async function () { - MemberListStore.loadInitialData([]); + MemberListStore.loadInitialData([user]); render(