diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index c67a02daa5f2..9aeb689dfdb8 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -100,8 +100,19 @@ function WorkspacesListPage() { for (const policy of workspaceListPolicies ?? []) { if (policy.isJoinRequestPending && policy.nonMemberDetails) { - const {policyID, ownerAccountID} = policy.nonMemberDetails; - const ownerDetails = ownerAccountID ? ownerDisplayDetails?.[ownerAccountID] : undefined; + const {policyID, ownerAccountID, ownerEmail, ownerDefaultAvatar} = policy.nonMemberDetails; + let ownerDetails = ownerAccountID ? ownerDisplayDetails?.[ownerAccountID] : undefined; + + // The owner of a policy the user only requested to join is usually not in the personal details list, + // so fall back to the owner email and default avatar the join request already provides. + if (!ownerDetails && ownerAccountID && ownerEmail) { + ownerDetails = { + accountID: ownerAccountID, + login: ownerEmail, + displayName: ownerEmail, + avatar: ownerDefaultAvatar, + }; + } const pendingWorkspaceRow: WorkspaceRowData = { keyForList: policyID, @@ -121,7 +132,7 @@ function WorkspacesListPage() { ownerAvatar: ownerDetails ? ownerDetails.avatar : undefined, ownerName: ownerDetails ? temporaryGetDisplayNameOrDefault({passedPersonalDetails: ownerDetails, translate}) : undefined, iconType: policy.nonMemberDetails.avatar ? CONST.ICON_TYPE_AVATAR : CONST.ICON_TYPE_ICON, - icon: policy.nonMemberDetails.avatar ? policy.nonMemberDetails.avatar : getDefaultWorkspaceAvatar(policy.name), + icon: policy.nonMemberDetails.avatar ? policy.nonMemberDetails.avatar : getDefaultWorkspaceAvatar(policy.nonMemberDetails.name), action: () => null, dismissError: () => null, }; diff --git a/src/selectors/Policy.ts b/src/selectors/Policy.ts index ca880d3e84c2..3a15511e36b6 100644 --- a/src/selectors/Policy.ts +++ b/src/selectors/Policy.ts @@ -2,6 +2,7 @@ import {hasSynchronizationErrorMessage, isConnectionUnverified} from '@libs/acti import {getDisplayNameForWorkspace} from '@libs/actions/Policy/Policy'; // eslint-disable-next-line no-restricted-imports -- isPaidGroupPolicy is intentional: copy-settings targets are billing/paid-only (Collect/Control), so free group plans like Submit must be excluded (see createCopySettingsEligibleTargetsSelector). import {getActiveAdminWorkspaces, getOwnedPaidPolicies, isPaidGroupPolicy, isPendingDeletePolicy, isPolicyAdmin, shouldShowPolicy} from '@libs/PolicyUtils'; +import {getDefaultAvatarURL} from '@libs/UserAvatarUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -67,7 +68,12 @@ type WorkspaceListPolicy = Pick & {policyID: string}; + nonMemberDetails?: Pick & { + policyID: string; + + /** Default avatar URL for the owner, derived here so the page doesn't re-hash the email on every render */ + ownerDefaultAvatar?: string; + }; }; /** @@ -95,6 +101,9 @@ const createWorkspaceListPoliciesSelector = name: details.name, type: details.type, ownerAccountID: details.ownerAccountID, + ownerEmail: details.ownerEmail, + ownerDefaultAvatar: + details.ownerAccountID && details.ownerEmail ? getDefaultAvatarURL({accountID: details.ownerAccountID, accountEmail: details.ownerEmail}) : undefined, avatar: details.avatar, }; } diff --git a/tests/ui/WorkspaceListPageTest.tsx b/tests/ui/WorkspaceListPageTest.tsx index 33e16b0b2042..934bc4a0c324 100644 --- a/tests/ui/WorkspaceListPageTest.tsx +++ b/tests/ui/WorkspaceListPageTest.tsx @@ -9,6 +9,7 @@ import type {WorkspaceNavigatorParamList} from '@libs/Navigation/types'; import WorkspacesListPage from '@pages/workspace/WorkspacesListPage'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; @@ -135,6 +136,35 @@ describe('WorkspaceListPage', () => { expect(newWorkspaceButton).toBeOnTheScreen(); }); + it('should show the owner email of a pending join request workspace when the owner is not in the personal details list', async () => { + const TEST_POLICY_ID = 'pending-policy-id'; + const OWNER_EMAIL = 'owner@example.com'; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${TEST_POLICY_ID}`, { + id: TEST_POLICY_ID, + isJoinRequestPending: true, + policyDetailsForNonMembers: { + [TEST_POLICY_ID]: { + name: 'Pending Workspace', + type: CONST.POLICY.TYPE.TEAM, + ownerAccountID: 42, + ownerEmail: OWNER_EMAIL, + }, + }, + }); + + renderPage(); + + await waitForBatchedUpdatesWithAct(); + + expect(screen.getByText('Pending Workspace')).toBeOnTheScreen(); + expect(screen.getByText(new RegExp(OWNER_EMAIL))).toBeOnTheScreen(); + // The workspace default icon is derived from nonMemberDetails.name ("Pending Workspace"), so the + // rendered SVG test ID is keyed by its first alphanumeric character. The icon is decorative and + // hidden from accessibility, so the query must include hidden elements. + expect(screen.getByTestId('SvgDefaultAvatar_p Icon', {includeHiddenElements: true})).toBeOnTheScreen(); + }); + it('should show a "New workspace" button when there are workspaces but no domains', async () => { const TEST_POLICY_ID = 'test-policy-id'; diff --git a/tests/unit/PolicySelectorTest.ts b/tests/unit/PolicySelectorTest.ts index cb7c6a54dffb..09b173ae455d 100644 --- a/tests/unit/PolicySelectorTest.ts +++ b/tests/unit/PolicySelectorTest.ts @@ -1,3 +1,5 @@ +import {getDefaultAvatarURL} from '@libs/UserAvatarUtils'; + import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -337,6 +339,7 @@ describe('createWorkspaceListPoliciesSelector', () => { name: 'External WS', type: CONST.POLICY.TYPE.CORPORATE, ownerAccountID: 99, + ownerEmail: 'owner@example.com', avatar: 'https://img/ext.png', }, }, @@ -349,6 +352,8 @@ describe('createWorkspaceListPoliciesSelector', () => { name: 'External WS', type: CONST.POLICY.TYPE.CORPORATE, ownerAccountID: 99, + ownerEmail: 'owner@example.com', + ownerDefaultAvatar: getDefaultAvatarURL({accountID: 99, accountEmail: 'owner@example.com'}), avatar: 'https://img/ext.png', }); });