Skip to content

Commit 2a348d4

Browse files
committed
Merge remote-tracking branch 'upstream/main' into jakubstec/domains/security-group-details-page-group-permissions5
2 parents 2f4607b + 196660f commit 2a348d4

65 files changed

Lines changed: 1288 additions & 210 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/ONYXKEYS.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,9 @@ const ONYXKEYS = {
786786
SNAPSHOT: 'snapshot_',
787787

788788
// Shared NVPs
789+
/** Collection of agent prompts keyed by agent accountID, representing agents owned by the current user */
790+
SHARED_NVP_AGENT_PROMPT: 'sharedNVP_agentPrompt_',
791+
789792
/** Collection of objects where each object represents the owner of the workspace that is past due billing AND the user is a member of. */
790793
SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END: 'sharedNVP_private_billingGracePeriodEnd_',
791794

@@ -1308,6 +1311,7 @@ type OnyxCollectionValuesMapping = {
13081311
[ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER]: OnyxTypes.PolicyJoinMember;
13091312
[ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS]: OnyxTypes.PolicyConnectionSyncProgress;
13101313
[ONYXKEYS.COLLECTION.SNAPSHOT]: OnyxTypes.SearchResults;
1314+
[ONYXKEYS.COLLECTION.SHARED_NVP_AGENT_PROMPT]: OnyxTypes.AgentPrompt;
13111315
[ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END]: OnyxTypes.BillingGraceEndPeriod;
13121316
[ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER]: OnyxTypes.CardFeeds;
13131317
[ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS]: OnyxTypes.ExpensifyCardSettings;

src/ROUTES.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4116,6 +4116,10 @@ const ROUTES = {
41164116
route: 'domain/:domainAccountID/members/move',
41174117
getRoute: (domainAccountID: number) => `domain/${domainAccountID}/members/move` as const,
41184118
},
4119+
DOMAIN_MEMBER_MOVE_TO_GROUP: {
4120+
route: 'domain/:domainAccountID/members/:accountID/move',
4121+
getRoute: (domainAccountID: number, accountID: number) => `domain/${domainAccountID}/members/${accountID}/move` as const,
4122+
},
41194123

41204124
MULTIFACTOR_AUTHENTICATION_MAGIC_CODE: `multifactor-authentication/magic-code`,
41214125
MULTIFACTOR_AUTHENTICATION_BIOMETRICS_TEST: 'multifactor-authentication/scenario/biometrics-test',

src/SCREENS.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,7 @@ const SCREENS = {
10211021
MEMBER_FORCE_TWO_FACTOR_AUTH: 'Member_Force_Two_Factor_Auth',
10221022
MEMBER_LOCK_ACCOUNT: 'Member_Lock_Account',
10231023
MEMBERS_MOVE_TO_GROUP: 'Members_Move_To_Group',
1024+
MEMBER_MOVE_TO_GROUP: 'Member_Move_To_Group',
10241025
GROUP_DETAILS: 'Domain_Group_Details',
10251026
GROUP_EDIT_NAME: 'Domain_Group_Edit_Name',
10261027
},

src/components/MoneyRequestConfirmationList/sections/AttendeeField.tsx

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import UserPills from '@components/UserPills';
66
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
77
import useLocalize from '@hooks/useLocalize';
88
import useThemeStyles from '@hooks/useThemeStyles';
9+
import {enrichAndSortAttendees} from '@libs/AttendeeUtils';
910
import Navigation from '@libs/Navigation/Navigation';
10-
import {sortAlphabetically} from '@libs/OptionsListUtils';
11-
import {getAttendees} from '@libs/TransactionUtils';
11+
import {getAttendees, getAttendeesListDisplayString} from '@libs/TransactionUtils';
1212
import CONST from '@src/CONST';
1313
import type {IOUAction, IOUType} from '@src/CONST';
1414
import type {TranslationPaths} from '@src/languages/types';
@@ -33,35 +33,22 @@ function AttendeeField({formattedAmountPerAttendee, isReadOnly, transactionID, a
3333
const personalDetailsList = usePersonalDetails();
3434
const shouldDisplayAttendeesError = formError === 'violations.missingAttendees';
3535

36-
const iouAttendees = getAttendees(transaction, currentUserPersonalDetails);
36+
const rawIouAttendees = getAttendees(transaction, currentUserPersonalDetails);
37+
const iouAttendees = enrichAndSortAttendees(rawIouAttendees, personalDetailsList, localeCompare);
3738

3839
return (
3940
<MenuItemWithTopDescription
4041
key="attendees"
4142
shouldShowRightIcon={!isReadOnly}
42-
accessibilityLabel={`${translate('iou.attendees')}, ${iouAttendees?.map((a) => a?.displayName ?? a?.login).join(', ')}`}
43+
accessibilityLabel={`${translate('iou.attendees')}, ${Array.isArray(iouAttendees) ? getAttendeesListDisplayString(iouAttendees) : ''}`}
4344
description={`${translate('iou.attendees')} ${
4445
iouAttendees?.length && iouAttendees.length > 1 && formattedAmountPerAttendee ? `\u00B7 ${formattedAmountPerAttendee} ${translate('common.perPerson')}` : ''
4546
}`}
4647
descriptionTextStyle={styles.textLabelSupportingNormal}
4748
titleComponent={
4849
Array.isArray(iouAttendees) ? (
4950
<UserPills
50-
users={sortAlphabetically(
51-
iouAttendees.map((a) => {
52-
const pd = a?.accountID ? personalDetailsList?.[a.accountID] : undefined;
53-
const freshAvatar = typeof pd?.avatar === 'string' ? pd.avatar : undefined;
54-
return {
55-
...a,
56-
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
57-
displayName: pd?.displayName || a?.displayName,
58-
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
59-
avatarUrl: freshAvatar || a?.avatarUrl,
60-
};
61-
}),
62-
'displayName',
63-
localeCompare,
64-
).map((a) => ({
51+
users={iouAttendees.map((a) => ({
6552
avatar: a?.avatarUrl,
6653
displayName: a?.displayName ?? a?.login ?? a?.email ?? '',
6754
accountID: a?.accountID,

src/components/ReportActionItem/MoneyRequestView.tsx

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,13 @@ import type {ViolationField} from '@hooks/useViolations';
4141
import useViolations from '@hooks/useViolations';
4242
import {updateMoneyRequestBillable, updateMoneyRequestReimbursable, updateMoneyRequestTaxRate} from '@libs/actions/IOU/UpdateMoneyRequest';
4343
import initSplitExpense from '@libs/actions/SplitExpenses';
44-
import {getIsMissingAttendeesViolation} from '@libs/AttendeeUtils';
44+
import {enrichAndSortAttendees, getIsMissingAttendeesViolation} from '@libs/AttendeeUtils';
4545
import {getBrokenConnectionUrlToFixPersonalCard, getCompanyCardDescription} from '@libs/CardUtils';
4646
import {getDecodedCategoryName, isCategoryMissing} from '@libs/CategoryUtils';
4747
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
4848
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
4949
import {getRateFromMerchant} from '@libs/MergeTransactionUtils';
50-
import {hasEnabledOptions, sortAlphabetically} from '@libs/OptionsListUtils';
50+
import {hasEnabledOptions} from '@libs/OptionsListUtils';
5151
import Parser from '@libs/Parser';
5252
import {
5353
canSubmitPerDiemExpenseFromWorkspace,
@@ -82,6 +82,7 @@ import {
8282
} from '@libs/ReportUtils';
8383
import {hasEnabledTags, shouldShowDependentTagList} from '@libs/TagsOptionsListUtils';
8484
import {
85+
getAttendeesListDisplayString,
8586
getBillable,
8687
getCurrency,
8788
getDescription,
@@ -283,7 +284,8 @@ function MoneyRequestView({
283284
const isTransactionScanning = isScanning(updatedTransaction ?? transaction);
284285
const hasRoute = hasRouteTransactionUtils(transactionBackup ?? transaction, isDistanceRequest);
285286

286-
const actualAttendees = isFromMergeTransaction && updatedTransaction ? updatedTransaction.comment?.attendees : transactionAttendees;
287+
const rawActualAttendees = isFromMergeTransaction && updatedTransaction ? updatedTransaction.comment?.attendees : transactionAttendees;
288+
const actualAttendees = enrichAndSortAttendees(rawActualAttendees, personalDetailsList, localeCompare);
287289

288290
// Use the updated transaction amount in merge flow to have correct positive/negative sign
289291
const actualAmount = isFromMergeTransaction && updatedTransaction ? updatedTransaction.amount : transactionAmount;
@@ -802,7 +804,8 @@ function MoneyRequestView({
802804
const previousTagLength = getLengthOfTag(previousTag ?? '');
803805
const currentTagLength = getLengthOfTag(currentTransactionTag ?? '');
804806

805-
const getAttendeesTitle = Array.isArray(actualAttendees) ? actualAttendees.map((item) => item?.displayName ?? item?.login).join(', ') : '';
807+
// actualAttendees is already sorted by enrichAndSortAttendees above; pass without localeCompare to preserve that order while stripping the SMS domain.
808+
const getAttendeesTitle = Array.isArray(actualAttendees) ? getAttendeesListDisplayString(actualAttendees) : '';
806809
const attendeesCopyValue = !canEdit ? getAttendeesTitle : undefined;
807810

808811
const tagList = policyTagLists.map(({name, orderWeight, tags}, index) => {
@@ -1171,21 +1174,7 @@ function MoneyRequestView({
11711174
titleComponent={
11721175
Array.isArray(actualAttendees) ? (
11731176
<UserPills
1174-
users={sortAlphabetically(
1175-
actualAttendees.map((a) => {
1176-
const pd = a?.accountID ? personalDetailsList?.[a.accountID] : undefined;
1177-
const freshAvatar = typeof pd?.avatar === 'string' ? pd.avatar : undefined;
1178-
return {
1179-
...a,
1180-
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
1181-
displayName: pd?.displayName || a?.displayName,
1182-
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
1183-
avatarUrl: freshAvatar || a?.avatarUrl,
1184-
};
1185-
}),
1186-
'displayName',
1187-
localeCompare,
1188-
).map((a) => ({
1177+
users={actualAttendees.map((a) => ({
11891178
avatar: a?.avatarUrl,
11901179
displayName: a?.displayName ?? a?.login ?? a?.email ?? '',
11911180
accountID: a?.accountID,

src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,9 @@ function TransactionPreviewContent({
156156
currentUserEmail,
157157
currentUserAccountID,
158158
originalTransaction,
159+
convertToDisplayString,
159160
}),
160-
[transactionPreviewCommonArguments, shouldShowRBR, violationMessage, reportActions, currentUserEmail, currentUserAccountID, originalTransaction],
161+
[transactionPreviewCommonArguments, shouldShowRBR, violationMessage, reportActions, currentUserEmail, currentUserAccountID, originalTransaction, convertToDisplayString],
161162
);
162163
const getTranslatedText = (item: TranslationPathOrText) => (item.translationPath ? translate(item.translationPath) : (item.text ?? ''));
163164

src/components/Search/SearchList/ListItem/TransactionGroupListItem.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {SearchGroupBy} from '@components/Search/types';
1414
import type {ListItem} from '@components/SelectionList/types';
1515
import useActionLoadingReportIDs from '@hooks/useActionLoadingReportIDs';
1616
import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle';
17+
import {useCurrencyListActions} from '@hooks/useCurrencyList';
1718
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
1819
import useLocalize from '@hooks/useLocalize';
1920
import useOnyx from '@hooks/useOnyx';
@@ -94,6 +95,7 @@ function TransactionGroupListItem<TItem extends ListItem>({
9495
const {isLargeScreenWidth} = useResponsiveLayout();
9596
const currentUserDetails = useCurrentUserPersonalDetails();
9697
const isScreenFocused = useIsFocused();
98+
const {convertToDisplayString} = useCurrencyListActions();
9799

98100
const oneTransactionItem = groupItem.isOneTransactionReport ? groupItem.transactions.at(0) : undefined;
99101
const [parentReport] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(oneTransactionItem?.reportID)}`);
@@ -141,6 +143,7 @@ function TransactionGroupListItem<TItem extends ListItem>({
141143
allReportMetadata,
142144
cardFeeds,
143145
conciergeReportID,
146+
convertToDisplayString,
144147
}) as [TransactionListItemType[], number, boolean];
145148
transactions = sectionData.map((transactionItem) => ({
146149
...transactionItem,

src/components/Search/SearchMultipleSelectionPicker.tsx

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, {useState} from 'react';
1+
import React, {useEffect, useState} from 'react';
22
import MultiSelectListItem from '@components/SelectionList/ListItem/MultiSelectListItem';
33
import SelectionListWithSections from '@components/SelectionList/SelectionListWithSections';
44
import useDebouncedState from '@hooks/useDebouncedState';
@@ -33,36 +33,49 @@ function SearchMultipleSelectionPicker<T extends string | string[]>({
3333
const {translate, localeCompare} = useLocalize();
3434
const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState('');
3535

36-
const [selectedItemIDs, setSelectedItemIDs] = useState(() => new Set((initiallySelectedItems ?? []).map((item) => item.value.toString())));
36+
const [initialSelectedIDs] = useState(() => new Set((initiallySelectedItems ?? []).map((item) => item.value.toString())));
37+
const [selectedItemIDs, setSelectedItemIDs] = useState(() => initialSelectedIDs);
38+
// Clear after mount to prevent FlashList from auto-scrolling when data changes
39+
// cause the key to transition from "not found" to "found" (e.g., clearing a search).
40+
// Deferred by one frame so FlashList processes the initial scroll first.
41+
const [initiallyFocusedKey, setInitiallyFocusedKey] = useState(() => {
42+
let minItem: SearchMultipleSelectionPickerItem<T> | undefined;
43+
for (const item of items) {
44+
if (initialSelectedIDs.has(item.value.toString())) {
45+
if (!minItem || sortOptionsWithEmptyValue(item.value.toString(), minItem.value.toString(), localeCompare) < 0) {
46+
minItem = item;
47+
}
48+
}
49+
}
50+
return minItem?.name;
51+
});
52+
useEffect(() => {
53+
const id = requestAnimationFrame(() => {
54+
setInitiallyFocusedKey(undefined);
55+
});
56+
return () => cancelAnimationFrame(id);
57+
}, []);
3758

3859
const searchLower = debouncedSearchTerm.toLowerCase();
39-
const selectedSectionData: Array<{text: string; keyForList: string; isSelected: boolean; value: T; leftElement?: React.ReactNode}> = [];
40-
const remainingSectionData: typeof selectedSectionData = [];
60+
const sectionData: Array<{text: string; keyForList: string; isSelected: boolean; value: T; leftElement?: React.ReactNode}> = [];
4161
for (const item of items) {
4262
if (!item.name.toLowerCase().includes(searchLower)) {
4363
continue;
4464
}
4565
const isSelected = selectedItemIDs.has(item.value.toString());
46-
(isSelected ? selectedSectionData : remainingSectionData).push({text: item.name, keyForList: item.name, isSelected, value: item.value, leftElement: item.leftElement});
66+
sectionData.push({text: item.name, keyForList: item.name, isSelected, value: item.value, leftElement: item.leftElement});
4767
}
4868

49-
const sortByValue = (a: {value: string | string[]}, b: {value: string | string[]}) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString(), localeCompare);
50-
selectedSectionData.sort(sortByValue);
51-
remainingSectionData.sort(sortByValue);
69+
sectionData.sort((a, b) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString(), localeCompare));
5270

53-
const noResultsFound = !selectedSectionData.length && !remainingSectionData.length;
71+
const noResultsFound = !sectionData.length;
5472
const sections = noResultsFound
5573
? []
5674
: [
57-
{
58-
title: undefined,
59-
data: selectedSectionData,
60-
sectionIndex: 0,
61-
},
6275
{
6376
title: pickerTitle,
64-
data: remainingSectionData,
65-
sectionIndex: 1,
77+
data: sectionData,
78+
sectionIndex: 0,
6679
},
6780
];
6881

@@ -101,6 +114,8 @@ function SearchMultipleSelectionPicker<T extends string | string[]>({
101114
<SelectionListWithSections
102115
sections={sections}
103116
ListItem={MultiSelectListItem}
117+
initiallyFocusedItemKey={initiallyFocusedKey}
118+
shouldClearInputOnSelect={false}
104119
shouldShowTextInput={shouldShowTextInput}
105120
textInputOptions={textInputOptions}
106121
onSelectRow={onSelectItem}

src/components/Search/SearchStaticList.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeed
2222
import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton';
2323
import StatusBadge from '@components/StatusBadge';
2424
import TransactionItemRow from '@components/TransactionItemRow';
25+
import {useCurrencyListActions} from '@hooks/useCurrencyList';
2526
import useLocalize from '@hooks/useLocalize';
2627
import useStyleUtils from '@hooks/useStyleUtils';
2728
import useTheme from '@hooks/useTheme';
@@ -69,6 +70,7 @@ function SearchStaticList({
6970
const theme = useTheme();
7071
const StyleUtils = useStyleUtils();
7172
const {translate, localeCompare, formatPhoneNumber} = useLocalize();
73+
const {convertToDisplayString} = useCurrencyListActions();
7274
const session = useSession();
7375
const accountID = session?.accountID ?? CONST.DEFAULT_NUMBER_ID;
7476
const email = session?.email;
@@ -96,6 +98,7 @@ function SearchStaticList({
9698
bankAccountList: undefined,
9799
allReportMetadata: undefined,
98100
conciergeReportID: undefined,
101+
convertToDisplayString,
99102
});
100103

101104
return getSortedSections(type, status, filteredData, localeCompare, translate, sortBy, sortOrder, validGroupBy)

src/components/Search/index.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton';
1212
import {useWideRHPActions} from '@components/WideRHPContextProvider';
1313
import useActionLoadingReportIDs from '@hooks/useActionLoadingReportIDs';
1414
import useArchivedReportsIdSet from '@hooks/useArchivedReportsIdSet';
15+
import {useCurrencyListActions} from '@hooks/useCurrencyList';
1516
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
1617
import useLocalize from '@hooks/useLocalize';
1718
import useMultipleSnapshots from '@hooks/useMultipleSnapshots';
@@ -352,6 +353,8 @@ function Search({
352353
selector: savedSearchSelector,
353354
});
354355

356+
const {convertToDisplayString} = useCurrencyListActions();
357+
355358
const validGroupBy = getValidGroupBy(groupBy);
356359
const prevValidGroupBy = usePrevious(validGroupBy);
357360
const isSearchResultsEmpty = !searchResults?.data || isSearchResultsEmptyUtil(searchResults, validGroupBy);
@@ -544,6 +547,7 @@ function Search({
544547
conciergeReportID,
545548
onyxPersonalDetailsList,
546549
policyForMovingExpenses,
550+
convertToDisplayString,
547551
});
548552
return {
549553
baseFilteredData: filteredData1,
@@ -576,6 +580,7 @@ function Search({
576580
conciergeReportID,
577581
onyxPersonalDetailsList,
578582
policyForMovingExpenses,
583+
convertToDisplayString,
579584
]);
580585

581586
// For group-by views, each grouped item has a transactionsQueryJSON with a hash pointing to a separate snapshot
@@ -612,6 +617,7 @@ function Search({
612617
cardFeeds,
613618
allReportMetadata,
614619
conciergeReportID,
620+
convertToDisplayString,
615621
});
616622
return {
617623
...item,
@@ -635,6 +641,7 @@ function Search({
635641
bankAccountList,
636642
allReportMetadata,
637643
conciergeReportID,
644+
convertToDisplayString,
638645
]);
639646

640647
const hasLoadedAllTransactions = useMemo(() => {

0 commit comments

Comments
 (0)