Skip to content

Commit 9c0c801

Browse files
committed
fix: sort attendees with the app's preferred locale
1 parent b3d80d1 commit 9c0c801

3 files changed

Lines changed: 47 additions & 21 deletions

File tree

src/libs/AttendeeUtils.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,14 +136,19 @@ function syncMissingAttendeesViolation<T extends {name: string}>(
136136
}
137137

138138
/**
139-
* Enrich each attendee with the live displayName/avatar from `personalDetails` and return them sorted alphabetically.
140-
* Centralised so every attendee renderer (pills, copy value, accessibility label, violation check) shares one canonical order.
139+
* Enrich each attendee with live `personalDetails` and return them sorted alphabetically by displayName.
141140
*/
141+
function enrichAndSortAttendees(attendees: Attendee[], personalDetailsList: OnyxEntry<PersonalDetailsList>, localeCompare: LocaleContextProps['localeCompare']): Attendee[];
142142
function enrichAndSortAttendees(
143-
attendees: Attendee[] | undefined,
143+
attendees: Attendee[] | string | undefined,
144+
personalDetailsList: OnyxEntry<PersonalDetailsList>,
145+
localeCompare: LocaleContextProps['localeCompare'],
146+
): Attendee[] | string | undefined;
147+
function enrichAndSortAttendees(
148+
attendees: Attendee[] | string | undefined,
144149
personalDetailsList: OnyxEntry<PersonalDetailsList>,
145150
localeCompare: LocaleContextProps['localeCompare'],
146-
): Attendee[] | undefined {
151+
): Attendee[] | string | undefined {
147152
if (!Array.isArray(attendees)) {
148153
return attendees;
149154
}

src/libs/TransactionUtils/index.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,15 +1201,27 @@ function getAttendees(transaction: OnyxInputOrEntry<Transaction>, currentUserPer
12011201
return attendees;
12021202
}
12031203

1204-
// Mirrors LocaleContextProvider so output here matches the user-facing pill order.
1205-
const ATTENDEES_DISPLAY_COLLATOR = new Intl.Collator(undefined, {usage: 'sort', sensitivity: 'variant', numeric: true, caseFirst: 'upper'});
1204+
// Mirrors LocaleContextProvider (same options + same IntlStore source) so non-React callers sort by the user's preferred locale.
1205+
const ATTENDEES_DISPLAY_COLLATOR_OPTIONS: Intl.CollatorOptions = {usage: 'sort', sensitivity: 'variant', numeric: true, caseFirst: 'upper'};
1206+
const attendeesDisplayCollatorCache = new Map<string, Intl.Collator>();
1207+
1208+
function getAttendeesDisplayCollator(): Intl.Collator {
1209+
const locale = IntlStore.getCurrentLocale() ?? '';
1210+
let collator = attendeesDisplayCollatorCache.get(locale);
1211+
if (!collator) {
1212+
collator = new Intl.Collator(locale || undefined, ATTENDEES_DISPLAY_COLLATOR_OPTIONS);
1213+
attendeesDisplayCollatorCache.set(locale, collator);
1214+
}
1215+
return collator;
1216+
}
12061217

12071218
/**
1208-
* Return the attendees list as an alphabetically sorted display string. Sorting here keeps every consumer in sync.
1219+
* Returns attendees joined as an alphabetically sorted display string. Sort is part of the contract — every consumer relies on it.
12091220
*/
12101221
function getAttendeesListDisplayString(attendees: Attendee[]): string {
1222+
const collator = getAttendeesDisplayCollator();
12111223
return [...attendees]
1212-
.sort((a, b) => ATTENDEES_DISPLAY_COLLATOR.compare(a.displayName ?? a.login ?? '', b.displayName ?? b.login ?? ''))
1224+
.sort((a, b) => collator.compare(a.displayName ?? a.login ?? '', b.displayName ?? b.login ?? ''))
12131225
.map((item) => item.displayName ?? item.login)
12141226
.join(', ');
12151227
}

tests/unit/AttendeeUtilsTest.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -86,38 +86,47 @@ describe('AttendeeUtils', () => {
8686
{email: 'a@x.com', displayName: 'apple', avatarUrl: '', login: 'a@x.com'},
8787
];
8888

89-
expect(enrichAndSortAttendees(attendees, undefined, localeCompare)?.map((a) => a.displayName)).toEqual(['apple', 'banana']);
89+
expect(enrichAndSortAttendees(attendees, undefined, localeCompare).map((a) => a.displayName)).toEqual(['apple', 'banana']);
9090
});
9191

9292
it('enriches displayName and avatar from personalDetails when accountID matches', () => {
93-
const attendees: Attendee[] = [{accountID: 1, displayName: 'Old', avatarUrl: 'old.png'} as Attendee];
94-
const personalDetailsList = {1: {accountID: 1, displayName: 'New', avatar: 'new.png'}} as unknown as PersonalDetailsList;
93+
const accountID = 1;
94+
const attendees: Attendee[] = [{accountID, displayName: 'Old', avatarUrl: 'old.png'}];
95+
const personalDetailsList: PersonalDetailsList = {[accountID]: {accountID, displayName: 'New', avatar: 'new.png'}};
9596

9697
const result = enrichAndSortAttendees(attendees, personalDetailsList, localeCompare);
9798

98-
expect(result?.[0].displayName).toBe('New');
99-
expect(result?.[0].avatarUrl).toBe('new.png');
99+
expect(result.at(0)?.displayName).toBe('New');
100+
expect(result.at(0)?.avatarUrl).toBe('new.png');
100101
});
101102

102103
it('falls back to stored value when personalDetails has empty strings', () => {
103-
const attendees: Attendee[] = [{accountID: 1, displayName: 'Stored', avatarUrl: 'stored.png'} as Attendee];
104-
const personalDetailsList = {1: {accountID: 1, displayName: '', avatar: ''}} as unknown as PersonalDetailsList;
104+
const accountID = 1;
105+
const attendees: Attendee[] = [{accountID, displayName: 'Stored', avatarUrl: 'stored.png'}];
106+
const personalDetailsList: PersonalDetailsList = {[accountID]: {accountID, displayName: '', avatar: ''}};
105107

106108
const result = enrichAndSortAttendees(attendees, personalDetailsList, localeCompare);
107109

108-
expect(result?.[0].displayName).toBe('Stored');
109-
expect(result?.[0].avatarUrl).toBe('stored.png');
110+
expect(result.at(0)?.displayName).toBe('Stored');
111+
expect(result.at(0)?.avatarUrl).toBe('stored.png');
110112
});
111113

112114
it('sorts using enriched displayName so a profile rename moves the pill', () => {
113-
const attendees: Attendee[] = [{accountID: 1, displayName: 'alice', avatarUrl: ''} as Attendee, {accountID: 2, displayName: 'bob', avatarUrl: ''} as Attendee];
114-
const personalDetailsList = {1: {accountID: 1, displayName: 'zoe'}} as unknown as PersonalDetailsList;
115+
const renamedAccountID = 1;
116+
const attendees: Attendee[] = [
117+
{accountID: renamedAccountID, displayName: 'alice', avatarUrl: ''},
118+
{accountID: 2, displayName: 'bob', avatarUrl: ''},
119+
];
120+
const personalDetailsList: PersonalDetailsList = {[renamedAccountID]: {accountID: renamedAccountID, displayName: 'zoe'}};
115121

116-
expect(enrichAndSortAttendees(attendees, personalDetailsList, localeCompare)?.map((a) => a.displayName)).toEqual(['bob', 'zoe']);
122+
expect(enrichAndSortAttendees(attendees, personalDetailsList, localeCompare).map((a) => a.displayName)).toEqual(['bob', 'zoe']);
117123
});
118124

119125
it('does not mutate the input array', () => {
120-
const attendees: Attendee[] = [{displayName: 'banana', avatarUrl: ''} as Attendee, {displayName: 'apple', avatarUrl: ''} as Attendee];
126+
const attendees: Attendee[] = [
127+
{displayName: 'banana', avatarUrl: ''},
128+
{displayName: 'apple', avatarUrl: ''},
129+
];
121130
const snapshot = [...attendees];
122131

123132
enrichAndSortAttendees(attendees, undefined, localeCompare);

0 commit comments

Comments
 (0)