From c95e5e9225b48e110357406a601a8881153693ed Mon Sep 17 00:00:00 2001 From: I Nyoman Jyotisa Date: Thu, 3 Oct 2024 18:37:07 +0800 Subject: [PATCH 01/95] [P2P Distance] Rate currency doesn't match expense currency --- src/components/ReportActionItem/MoneyRequestView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 66c773ab9534..332b9d214de8 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -213,7 +213,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals const hasRoute = TransactionUtils.hasRoute(transactionBackup ?? transaction, isDistanceRequest); const rateID = TransactionUtils.getRateID(transaction) ?? '-1'; - const currency = policy ? policy.outputCurrency : PolicyUtils.getPersonalPolicy()?.outputCurrency ?? CONST.CURRENCY.USD; + const currency = transactionCurrency ?? CONST.CURRENCY.USD; const mileageRate = TransactionUtils.isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(currency) : distanceRates[rateID] ?? {}; const {unit} = mileageRate; From c0c7c83b90a50174712ed5334cd8eb2c0a2a8bca Mon Sep 17 00:00:00 2001 From: Tsaqif Date: Thu, 3 Oct 2024 21:25:21 +0700 Subject: [PATCH 02/95] Add some screen to screens opened from LHN Signed-off-by: Tsaqif --- src/components/FocusTrap/TOP_TAB_SCREENS.ts | 7 ++++++- .../Navigation/linkingConfig/getAdaptedStateFromPath.ts | 6 +++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/FocusTrap/TOP_TAB_SCREENS.ts b/src/components/FocusTrap/TOP_TAB_SCREENS.ts index 6bee36b86883..34610c4b0f11 100644 --- a/src/components/FocusTrap/TOP_TAB_SCREENS.ts +++ b/src/components/FocusTrap/TOP_TAB_SCREENS.ts @@ -1,5 +1,10 @@ +import type {TupleToUnion} from 'type-fest'; import CONST from '@src/CONST'; -const TOP_TAB_SCREENS: string[] = [CONST.TAB.NEW_CHAT, CONST.TAB.NEW_ROOM, CONST.TAB_REQUEST.DISTANCE, CONST.TAB_REQUEST.MANUAL, CONST.TAB_REQUEST.SCAN]; +const TOP_TAB_SCREENS = [CONST.TAB.NEW_CHAT, CONST.TAB.NEW_ROOM, CONST.TAB_REQUEST.DISTANCE, CONST.TAB_REQUEST.MANUAL, CONST.TAB_REQUEST.SCAN] as const; + +type TopTabScreen = TupleToUnion; + +export type {TopTabScreen}; export default TOP_TAB_SCREENS; diff --git a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts index c84213918f70..61a3e1de9866 100644 --- a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts +++ b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts @@ -2,6 +2,7 @@ import type {NavigationState, PartialState, Route} from '@react-navigation/nativ import {findFocusedRoute, getStateFromPath} from '@react-navigation/native'; import pick from 'lodash/pick'; import type {TupleToUnion} from 'type-fest'; +import type {TopTabScreen} from '@components/FocusTrap/TOP_TAB_SCREENS'; import {isAnonymousUser} from '@libs/actions/Session'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import type {BottomTabName, CentralPaneName, FullScreenName, NavigationPartialRoute, RootStackParamList} from '@libs/Navigation/types'; @@ -29,7 +30,10 @@ const RHP_SCREENS_OPENED_FROM_LHN = [ SCREENS.SETTINGS.EXIT_SURVEY.REASON, SCREENS.SETTINGS.EXIT_SURVEY.RESPONSE, SCREENS.SETTINGS.EXIT_SURVEY.CONFIRM, -] satisfies Screen[]; + CONST.TAB_REQUEST.DISTANCE, + CONST.TAB_REQUEST.MANUAL, + CONST.TAB_REQUEST.SCAN, +] satisfies Array; type RHPScreenOpenedFromLHN = TupleToUnion; From 830ac47fc05e52a975b7a4a1316f12cec622adf7 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Mon, 30 Sep 2024 16:00:08 +0200 Subject: [PATCH 03/95] Remove old chat finder functionality --- src/SCREENS.ts | 1 - src/libs/E2E/reactNativeLaunchingTest.ts | 1 - .../Navigators/LeftModalNavigator.tsx | 5 - .../ChatFinderPage/ChatFinderPageFooter.tsx | 11 - src/pages/ChatFinderPage/index.tsx | 209 ---------------- tests/e2e/config.ts | 4 - tests/perf-test/ChatFinderPage.perf-test.tsx | 232 ------------------ 7 files changed, 463 deletions(-) delete mode 100644 src/pages/ChatFinderPage/ChatFinderPageFooter.tsx delete mode 100644 src/pages/ChatFinderPage/index.tsx delete mode 100644 tests/perf-test/ChatFinderPage.perf-test.tsx diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 9a94d612dc80..8d363e6317c1 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -141,7 +141,6 @@ const SCREENS = { ROOT: 'SaveTheWorld_Root', }, LEFT_MODAL: { - CHAT_FINDER: 'ChatFinder', WORKSPACE_SWITCHER: 'WorkspaceSwitcher', }, RIGHT_MODAL: { diff --git a/src/libs/E2E/reactNativeLaunchingTest.ts b/src/libs/E2E/reactNativeLaunchingTest.ts index f952998f0aad..21cb0336c5fb 100644 --- a/src/libs/E2E/reactNativeLaunchingTest.ts +++ b/src/libs/E2E/reactNativeLaunchingTest.ts @@ -32,7 +32,6 @@ if (!appInstanceId) { // import your test here, define its name and config first in e2e/config.js const tests: Tests = { [E2EConfig.TEST_NAMES.AppStartTime]: require('./tests/appStartTimeTest.e2e').default, - [E2EConfig.TEST_NAMES.OpenChatFinderPage]: require('./tests/openChatFinderPageTest.e2e').default, [E2EConfig.TEST_NAMES.ChatOpening]: require('./tests/chatOpeningTest.e2e').default, [E2EConfig.TEST_NAMES.ReportTyping]: require('./tests/reportTypingTest.e2e').default, [E2EConfig.TEST_NAMES.Linking]: require('./tests/linkingTest.e2e').default, diff --git a/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx index 077bdce94545..50439c19845e 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx @@ -14,7 +14,6 @@ import Overlay from './Overlay'; type LeftModalNavigatorProps = StackScreenProps; -const loadChatFinder = () => require('../../../../pages/ChatFinderPage').default; const loadWorkspaceSwitcherPage = () => require('../../../../pages/WorkspaceSwitcherPage').default; const Stack = createStackNavigator(); @@ -37,10 +36,6 @@ function LeftModalNavigator({navigation}: LeftModalNavigatorProps) { screenOptions={screenOptions} id={NAVIGATORS.LEFT_MODAL_NAVIGATOR} > - ; -} - -ChatFinderPageFooter.displayName = 'ChatFinderPageFooter'; - -export default ChatFinderPageFooter; diff --git a/src/pages/ChatFinderPage/index.tsx b/src/pages/ChatFinderPage/index.tsx deleted file mode 100644 index aabf881a8bed..000000000000 --- a/src/pages/ChatFinderPage/index.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import type {StackScreenProps} from '@react-navigation/stack'; -import isEmpty from 'lodash/isEmpty'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {useOptionsList} from '@components/OptionListContextProvider'; -import ScreenWrapper from '@components/ScreenWrapper'; -import SelectionList from '@components/SelectionList'; -import UserListItem from '@components/SelectionList/UserListItem'; -import useCancelSearchOnModalClose from '@hooks/useCancelSearchOnModalClose'; -import useDebouncedState from '@hooks/useDebouncedState'; -import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners'; -import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; -import Navigation from '@libs/Navigation/Navigation'; -import type {RootStackParamList} from '@libs/Navigation/types'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import Performance from '@libs/Performance'; -import type {OptionData} from '@libs/ReportUtils'; -import * as Report from '@userActions/Report'; -import Timing from '@userActions/Timing'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import type SCREENS from '@src/SCREENS'; -import type * as OnyxTypes from '@src/types/onyx'; -import ChatFinderPageFooter from './ChatFinderPageFooter'; - -type ChatFinderPageOnyxProps = { - /** Beta features list */ - betas: OnyxEntry; - - /** Whether or not we are searching for reports on the server */ - isSearchingForReports: OnyxEntry; -}; - -type ChatFinderPageProps = ChatFinderPageOnyxProps & StackScreenProps; - -type ChatFinderPageSectionItem = { - data: OptionData[]; - shouldShow: boolean; -}; - -type ChatFinderPageSectionList = ChatFinderPageSectionItem[]; - -const setPerformanceTimersEnd = () => { - Timing.end(CONST.TIMING.CHAT_FINDER_RENDER); - Performance.markEnd(CONST.TIMING.CHAT_FINDER_RENDER); -}; - -const ChatFinderPageFooterInstance = ; - -function ChatFinderPage({betas, isSearchingForReports, navigation}: ChatFinderPageProps) { - const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); - const {translate} = useLocalize(); - const {isOffline} = useNetwork(); - const {options, areOptionsInitialized} = useOptionsList({ - shouldInitialize: isScreenTransitionEnd, - }); - - const offlineMessage: string = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; - - const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); - const [, debouncedSearchValueInServer, setSearchValueInServer] = useDebouncedState('', 500); - const updateSearchValue = useCallback( - (value: string) => { - setSearchValue(value); - setSearchValueInServer(value); - }, - [setSearchValue, setSearchValueInServer], - ); - useCancelSearchOnModalClose(); - - useEffect(() => { - Report.searchInServer(debouncedSearchValueInServer.trim()); - }, [debouncedSearchValueInServer]); - - const searchOptions = useMemo(() => { - if (!areOptionsInitialized || !isScreenTransitionEnd) { - return { - recentReports: [], - personalDetails: [], - userToInvite: null, - currentUserOption: null, - categoryOptions: [], - tagOptions: [], - taxRatesOptions: [], - headerMessage: '', - }; - } - const optionList = OptionsListUtils.getSearchOptions(options, '', betas ?? []); - const header = OptionsListUtils.getHeaderMessage(optionList.recentReports.length + optionList.personalDetails.length !== 0, !!optionList.userToInvite, ''); - return {...optionList, headerMessage: header}; - }, [areOptionsInitialized, betas, isScreenTransitionEnd, options]); - - const filteredOptions = useMemo(() => { - if (debouncedSearchValue.trim() === '') { - return { - recentReports: [], - personalDetails: [], - userToInvite: null, - headerMessage: '', - }; - } - - Timing.start(CONST.TIMING.SEARCH_FILTER_OPTIONS); - const newOptions = OptionsListUtils.filterOptions(searchOptions, debouncedSearchValue, {sortByReportTypeInSearch: true, preferChatroomsOverThreads: true}); - Timing.end(CONST.TIMING.SEARCH_FILTER_OPTIONS); - - const header = OptionsListUtils.getHeaderMessage(newOptions.recentReports.length + Number(!!newOptions.userToInvite) > 0, false, debouncedSearchValue); - return { - recentReports: newOptions.recentReports, - personalDetails: newOptions.personalDetails, - userToInvite: newOptions.userToInvite, - headerMessage: header, - }; - }, [debouncedSearchValue, searchOptions]); - - const {recentReports, personalDetails: localPersonalDetails, userToInvite, headerMessage} = debouncedSearchValue.trim() !== '' ? filteredOptions : searchOptions; - - const sections = useMemo((): ChatFinderPageSectionList => { - const newSections: ChatFinderPageSectionList = []; - - if (recentReports?.length > 0) { - newSections.push({ - data: recentReports, - shouldShow: true, - }); - } - - if (localPersonalDetails.length > 0) { - newSections.push({ - data: localPersonalDetails, - shouldShow: true, - }); - } - - if (!isEmpty(userToInvite)) { - newSections.push({ - data: [userToInvite], - shouldShow: true, - }); - } - - return newSections; - }, [localPersonalDetails, recentReports, userToInvite]); - - const selectReport = (option: OptionData) => { - if (!option) { - return; - } - - if (option.reportID) { - Navigation.closeAndNavigate(ROUTES.REPORT_WITH_ID.getRoute(option.reportID)); - } else { - Report.navigateToAndOpenReport(option.login ? [option.login] : []); - } - }; - - const handleScreenTransitionEnd = () => { - setIsScreenTransitionEnd(true); - }; - - const {isDismissed} = useDismissedReferralBanners({referralContentType: CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND}); - - return ( - - - - sections={areOptionsInitialized ? sections : CONST.EMPTY_ARRAY} - ListItem={UserListItem} - textInputValue={searchValue} - textInputLabel={translate('selectionList.nameEmailOrPhoneNumber')} - textInputHint={offlineMessage} - onChangeText={updateSearchValue} - headerMessage={headerMessage} - onLayout={setPerformanceTimersEnd} - onSelectRow={selectReport} - shouldSingleExecuteRowSelect - showLoadingPlaceholder={!areOptionsInitialized || !isScreenTransitionEnd} - footerContent={!isDismissed && ChatFinderPageFooterInstance} - isLoadingNewOptions={!!isSearchingForReports} - shouldDelayFocus={false} - /> - - ); -} - -ChatFinderPage.displayName = 'ChatFinderPage'; - -export default withOnyx({ - betas: { - key: ONYXKEYS.BETAS, - }, - isSearchingForReports: { - key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, - initWithStoredValues: false, - }, -})(ChatFinderPage); diff --git a/tests/e2e/config.ts b/tests/e2e/config.ts index bdb24bee0bc5..8b14fb8de7a0 100644 --- a/tests/e2e/config.ts +++ b/tests/e2e/config.ts @@ -4,7 +4,6 @@ const OUTPUT_DIR = process.env.WORKING_DIRECTORY || './tests/e2e/results'; // add your test name here … const TEST_NAMES = { AppStartTime: 'App start time', - OpenChatFinderPage: 'Open chat finder page TTI', ReportTyping: 'Report typing', ChatOpening: 'Chat opening', Linking: 'Linking', @@ -73,9 +72,6 @@ export default { name: TEST_NAMES.AppStartTime, // ... any additional config you might need }, - [TEST_NAMES.OpenChatFinderPage]: { - name: TEST_NAMES.OpenChatFinderPage, - }, [TEST_NAMES.ReportTyping]: { name: TEST_NAMES.ReportTyping, reportScreen: { diff --git a/tests/perf-test/ChatFinderPage.perf-test.tsx b/tests/perf-test/ChatFinderPage.perf-test.tsx deleted file mode 100644 index 4346977a1cd0..000000000000 --- a/tests/perf-test/ChatFinderPage.perf-test.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import type * as NativeNavigation from '@react-navigation/native'; -import type {StackNavigationProp, StackScreenProps} from '@react-navigation/stack'; -import {fireEvent, screen} from '@testing-library/react-native'; -import React, {useMemo} from 'react'; -import type {ComponentType} from 'react'; -import Onyx from 'react-native-onyx'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {measurePerformance} from 'reassure'; -import {LocaleContextProvider} from '@components/LocaleContextProvider'; -import OptionListContextProvider, {OptionsListContext} from '@components/OptionListContextProvider'; -import {KeyboardStateProvider} from '@components/withKeyboardState'; -import type {WithNavigationFocusProps} from '@components/withNavigationFocus'; -import type {RootStackParamList} from '@libs/Navigation/types'; -import {createOptionList} from '@libs/OptionsListUtils'; -import ChatFinderPage from '@pages/ChatFinderPage'; -import ComposeProviders from '@src/components/ComposeProviders'; -import OnyxProvider from '@src/components/OnyxProvider'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type SCREENS from '@src/SCREENS'; -import type {Beta, PersonalDetails, Report} from '@src/types/onyx'; -import createCollection from '../utils/collections/createCollection'; -import createPersonalDetails from '../utils/collections/personalDetails'; -import createRandomReport from '../utils/collections/reports'; -import createAddListenerMock from '../utils/createAddListenerMock'; -import * as TestHelper from '../utils/TestHelper'; -import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; -import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; - -jest.mock('lodash/debounce', () => - jest.fn((fn: Record) => { - // eslint-disable-next-line no-param-reassign - fn.cancel = jest.fn(); - return fn; - }), -); - -jest.mock('@src/libs/Log'); - -jest.mock('@src/libs/API', () => ({ - write: jest.fn(), - makeRequestWithSideEffects: jest.fn(), - read: jest.fn(), -})); - -jest.mock('@src/libs/Navigation/Navigation', () => ({ - dismissModalWithReport: jest.fn(), - getTopmostReportId: jest.fn(), - isNavigationReady: jest.fn(() => Promise.resolve()), - isDisplayedInModal: jest.fn(() => false), -})); - -jest.mock('@react-navigation/native', () => { - const actualNav = jest.requireActual('@react-navigation/native'); - return { - ...actualNav, - useFocusEffect: jest.fn(), - useIsFocused: () => true, - useRoute: () => jest.fn(), - useNavigation: () => ({ - navigate: jest.fn(), - addListener: () => jest.fn(), - }), - createNavigationContainerRef: () => ({ - addListener: () => jest.fn(), - removeListener: () => jest.fn(), - isReady: () => jest.fn(), - getCurrentRoute: () => jest.fn(), - getState: () => jest.fn(), - }), - }; -}); - -jest.mock('@src/components/withNavigationFocus', () => (Component: ComponentType) => { - function WithNavigationFocus(props: WithNavigationFocusProps) { - return ( - - ); - } - - WithNavigationFocus.displayName = 'WithNavigationFocus'; - - return WithNavigationFocus; -}); -// mock of useDismissedReferralBanners -jest.mock('../../src/hooks/useDismissedReferralBanners', () => ({ - // eslint-disable-next-line @typescript-eslint/naming-convention - __esModule: true, - default: jest.fn(() => ({ - isDismissed: false, - setAsDismissed: () => {}, - })), -})); - -const getMockedReports = (length = 100) => - createCollection( - (item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, - (index) => createRandomReport(index), - length, - ); - -const getMockedPersonalDetails = (length = 100) => - createCollection( - (item) => item.accountID, - (index) => createPersonalDetails(index), - length, - ); - -const mockedReports = getMockedReports(600); -const mockedBetas = Object.values(CONST.BETAS); -const mockedPersonalDetails = getMockedPersonalDetails(100); -const mockedOptions = createOptionList(mockedPersonalDetails, mockedReports); - -beforeAll(() => - Onyx.init({ - keys: ONYXKEYS, - safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT], - }), -); - -// Initialize the network key for OfflineWithFeedback -beforeEach(() => { - global.fetch = TestHelper.getGlobalFetchMock(); - wrapOnyxWithWaitForBatchedUpdates(Onyx); - Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); -}); - -// Clear out Onyx after each test so that each test starts with a clean state -afterEach(() => { - Onyx.clear(); -}); - -type ChatFinderPageProps = StackScreenProps & { - betas?: OnyxEntry; - reports?: OnyxCollection; - isSearchingForReports?: OnyxEntry; -}; - -function ChatFinderPageWrapper(args: ChatFinderPageProps) { - return ( - - - - - - ); -} - -function ChatFinderPageWithCachedOptions(args: ChatFinderPageProps) { - return ( - - ({options: mockedOptions, initializeOptions: () => {}, areOptionsInitialized: true}), [])}> - - - - ); -} - -test('[ChatFinderPage] should render list with cached options', async () => { - const {addListener} = createAddListenerMock(); - - const scenario = async () => { - await screen.findByTestId('ChatFinderPage'); - }; - - const navigation = {addListener} as unknown as StackNavigationProp; - - return waitForBatchedUpdates() - .then(() => - Onyx.multiSet({ - ...mockedReports, - [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, - [ONYXKEYS.BETAS]: mockedBetas, - [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, - }), - ) - .then(() => - measurePerformance( - , - {scenario}, - ), - ); -}); - -test('[ChatFinderPage] should interact when text input changes', async () => { - const {addListener} = createAddListenerMock(); - - const scenario = async () => { - await screen.findByTestId('ChatFinderPage'); - - const input = screen.getByTestId('selection-list-text-input'); - fireEvent.changeText(input, 'Email Four'); - fireEvent.changeText(input, 'Report'); - fireEvent.changeText(input, 'Email Five'); - }; - - const navigation = {addListener} as unknown as StackNavigationProp; - - return waitForBatchedUpdates() - .then(() => - Onyx.multiSet({ - ...mockedReports, - [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, - [ONYXKEYS.BETAS]: mockedBetas, - [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, - }), - ) - .then(() => - measurePerformance( - , - {scenario}, - ), - ); -}); From 4052f9a4db738cce4afbe8573a33f838db2afe59 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Wed, 2 Oct 2024 13:06:17 +0200 Subject: [PATCH 04/95] Use SearchRouter instead of Chat finder and migrate e2e tests --- src/CONST.ts | 2 +- src/ROUTES.ts | 1 - .../Search/SearchRouter/SearchButton.tsx | 12 +- src/libs/E2E/reactNativeLaunchingTest.ts | 2 + ...est.e2e.ts => openSearchRouterTest.e2e.ts} | 17 +- .../Navigation/AppNavigator/AuthScreens.tsx | 10 +- .../createCustomBottomTabNavigator/TopBar.tsx | 33 +-- src/libs/Navigation/linkingConfig/config.ts | 1 - src/libs/Navigation/types.ts | 1 - src/pages/Search/SearchPageBottomTab.tsx | 7 +- .../SidebarScreen/BaseSidebarScreen.tsx | 5 + tests/e2e/config.ts | 4 + tests/perf-test/ChatFinderPage.perf-test.tsx | 217 ++++++++++++++++++ 13 files changed, 259 insertions(+), 53 deletions(-) rename src/libs/E2E/tests/{openChatFinderPageTest.e2e.ts => openSearchRouterTest.e2e.ts} (79%) create mode 100644 tests/perf-test/ChatFinderPage.perf-test.tsx diff --git a/src/CONST.ts b/src/CONST.ts index 9a6bf21db303..0b130ae7564b 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1084,7 +1084,7 @@ const CONST = { }, TIMING: { CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION: 'calc_most_recent_last_modified_action', - CHAT_FINDER_RENDER: 'search_render', + SEARCH_ROUTER_OPEN: 'search_router_render', CHAT_RENDER: 'chat_render', OPEN_REPORT: 'open_report', HOMEPAGE_INITIAL_RENDER: 'homepage_initial_render', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 9c429dd3e909..d7e6b37a60fd 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -74,7 +74,6 @@ const ROUTES = { route: 'flag/:reportID/:reportActionID', getRoute: (reportID: string, reportActionID: string, backTo?: string) => getUrlWithBackToParam(`flag/${reportID}/${reportActionID}` as const, backTo), }, - CHAT_FINDER: 'chat-finder', PROFILE: { route: 'a/:accountID', getRoute: (accountID?: string | number, backTo?: string, login?: string) => { diff --git a/src/components/Search/SearchRouter/SearchButton.tsx b/src/components/Search/SearchRouter/SearchButton.tsx index 05693ad5ea22..53660ee1f6c2 100644 --- a/src/components/Search/SearchRouter/SearchButton.tsx +++ b/src/components/Search/SearchRouter/SearchButton.tsx @@ -5,7 +5,11 @@ import {PressableWithoutFeedback} from '@components/Pressable'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import Performance from '@libs/Performance'; import Permissions from '@libs/Permissions'; +import * as Session from '@userActions/Session'; +import Timing from '@userActions/Timing'; +import CONST from '@src/CONST'; import {useSearchRouterContext} from './SearchRouterContext'; function SearchButton() { @@ -22,9 +26,13 @@ function SearchButton() { { + onPress={Session.checkIfActionIsAllowed(() => { + // Todo [Search] add finishing for this timing in + Timing.start(CONST.TIMING.SEARCH_ROUTER_OPEN); + Performance.markStart(CONST.TIMING.SEARCH_ROUTER_OPEN); + openSearchRouter(); - }} + })} > ('./tests/appStartTimeTest.e2e').default, + // Todo [Search] rename + [E2EConfig.TEST_NAMES.OpenSearchRouter]: require('./tests/openSearchRouterTest.e2e').default, [E2EConfig.TEST_NAMES.ChatOpening]: require('./tests/chatOpeningTest.e2e').default, [E2EConfig.TEST_NAMES.ReportTyping]: require('./tests/reportTypingTest.e2e').default, [E2EConfig.TEST_NAMES.Linking]: require('./tests/linkingTest.e2e').default, diff --git a/src/libs/E2E/tests/openChatFinderPageTest.e2e.ts b/src/libs/E2E/tests/openSearchRouterTest.e2e.ts similarity index 79% rename from src/libs/E2E/tests/openChatFinderPageTest.e2e.ts rename to src/libs/E2E/tests/openSearchRouterTest.e2e.ts index 2c2f2eda4efe..6ce1891a63d4 100644 --- a/src/libs/E2E/tests/openChatFinderPageTest.e2e.ts +++ b/src/libs/E2E/tests/openSearchRouterTest.e2e.ts @@ -3,14 +3,12 @@ import E2ELogin from '@libs/E2E/actions/e2eLogin'; import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; import E2EClient from '@libs/E2E/client'; import getPromiseWithResolve from '@libs/E2E/utils/getPromiseWithResolve'; -import Navigation from '@libs/Navigation/Navigation'; import Performance from '@libs/Performance'; import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; const test = () => { // check for login (if already logged in the action will simply resolve) - console.debug('[E2E] Logging in for chat finder'); + console.debug('[E2E] Logging in for new search router'); E2ELogin().then((neededLogin: boolean): Promise | undefined => { if (neededLogin) { @@ -20,7 +18,7 @@ const test = () => { ); } - console.debug('[E2E] Logged in, getting chat finder metrics and submitting them…'); + console.debug('[E2E] Logged in, getting search router metrics and submitting them…'); const [openSearchPagePromise, openSearchPageResolve] = getPromiseWithResolve(); const [loadSearchOptionsPromise, loadSearchOptionsResolve] = getPromiseWithResolve(); @@ -32,19 +30,12 @@ const test = () => { }); Performance.subscribeToMeasurements((entry) => { - if (entry.name === CONST.TIMING.SIDEBAR_LOADED) { - console.debug(`[E2E] Sidebar loaded, navigating to chat finder route…`); - Performance.markStart(CONST.TIMING.CHAT_FINDER_RENDER); - Navigation.navigate(ROUTES.CHAT_FINDER); - return; - } - console.debug(`[E2E] Entry: ${JSON.stringify(entry)}`); - if (entry.name === CONST.TIMING.CHAT_FINDER_RENDER) { + if (entry.name === CONST.TIMING.SEARCH_ROUTER_OPEN) { E2EClient.submitTestResults({ branch: Config.E2E_BRANCH, - name: 'Open Chat Finder Page TTI', + name: 'Open Search Router TTI', metric: entry.duration, unit: 'ms', }) diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index f5f35fd21025..62be199ee183 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -8,6 +8,7 @@ import ComposeProviders from '@components/ComposeProviders'; import OptionsListContextProvider from '@components/OptionListContextProvider'; import {SearchContextProvider} from '@components/Search/SearchContext'; import SearchRouterModal from '@components/Search/SearchRouter/SearchRouterModal'; +import {useSearchRouterContext} from '@components/Search/SearchRouter/SearchRouterContext'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useOnboardingFlowRouter from '@hooks/useOnboardingFlow'; import usePermissions from '@hooks/usePermissions'; @@ -233,6 +234,8 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie const screenOptions = getRootNavigatorScreenOptions(shouldUseNarrowLayout, styles, StyleUtils); const {canUseDefaultRooms} = usePermissions(); const {activeWorkspaceID} = useActiveWorkspace(); + const {openSearchRouter} = useSearchRouterContext(); + const onboardingModalScreenOptions = useMemo(() => screenOptions.onboardingModalNavigator(onboardingIsMediumOrLargerScreenWidth), [screenOptions, onboardingIsMediumOrLargerScreenWidth]); const onboardingScreenOptions = useMemo( () => getOnboardingModalScreenOptions(shouldUseNarrowLayout, styles, StyleUtils, onboardingIsMediumOrLargerScreenWidth), @@ -241,6 +244,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie const modal = useRef({}); const [didPusherInit, setDidPusherInit] = useState(false); const {isOnboardingCompleted} = useOnboardingFlowRouter(); + let initialReportID: string | undefined; const isInitialRender = useRef(true); if (isInitialRender.current) { @@ -363,13 +367,15 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie ); // Listen for the key K being pressed so that focus can be given to - // the chat switcher, or new group chat + // Search Router, or new group chat // based on the key modifiers pressed and the operating system const unsubscribeSearchShortcut = KeyboardShortcut.subscribe( searchShortcutConfig.shortcutKey, () => { Modal.close( - Session.checkIfActionIsAllowed(() => Navigation.navigate(ROUTES.CHAT_FINDER)), + Session.checkIfActionIsAllowed(() => { + openSearchRouter(); + }), true, true, ); diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx index 4684eb9637be..8967486165f8 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx @@ -2,32 +2,25 @@ import React from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Breadcrumbs from '@components/Breadcrumbs'; -import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; import {PressableWithoutFeedback} from '@components/Pressable'; import SearchButton from '@components/Search/SearchRouter/SearchButton'; import Text from '@components/Text'; -import Tooltip from '@components/Tooltip'; import WorkspaceSwitcherButton from '@components/WorkspaceSwitcherButton'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import Performance from '@libs/Performance'; import * as SearchUtils from '@libs/SearchUtils'; import SignInButton from '@pages/home/sidebar/SignInButton'; import * as Session from '@userActions/Session'; -import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -type TopBarProps = {breadcrumbLabel: string; activeWorkspaceID?: string; shouldDisplaySearch?: boolean; isCustomSearchQuery?: boolean; shouldDisplaySearchRouter?: boolean}; +type TopBarProps = {breadcrumbLabel: string; activeWorkspaceID?: string; shouldDisplaySearch?: boolean; shouldDisplayCancelSearch?: boolean}; -function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, isCustomSearchQuery = false, shouldDisplaySearchRouter = false}: TopBarProps) { +function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, shouldDisplayCancelSearch = false}: TopBarProps) { const styles = useThemeStyles(); - const theme = useTheme(); const {translate} = useLocalize(); const policy = usePolicy(activeWorkspaceID); const [session] = useOnyx(ONYXKEYS.SESSION, {selector: (sessionValue) => sessionValue && {authTokenType: sessionValue.authTokenType}}); @@ -63,7 +56,7 @@ function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, {displaySignIn && } - {isCustomSearchQuery && ( + {shouldDisplayCancelSearch && ( {translate('common.cancel')} )} - {shouldDisplaySearchRouter && } - {displaySearch && ( - - { - Timing.start(CONST.TIMING.CHAT_FINDER_RENDER); - Performance.markStart(CONST.TIMING.CHAT_FINDER_RENDER); - Navigation.navigate(ROUTES.CHAT_FINDER); - })} - > - - - - )} + {displaySearch && } ); diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 114e89ff2bf5..980b28c3d635 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -76,7 +76,6 @@ const config: LinkingOptions['config'] = { [SCREENS.NOT_FOUND]: '*', [NAVIGATORS.LEFT_MODAL_NAVIGATOR]: { screens: { - [SCREENS.LEFT_MODAL.CHAT_FINDER]: ROUTES.CHAT_FINDER, [SCREENS.LEFT_MODAL.WORKSPACE_SWITCHER]: { path: ROUTES.WORKSPACE_SWITCHER, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index b698681966e2..ecd2895053f1 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1236,7 +1236,6 @@ type TransactionDuplicateNavigatorParamList = { }; type LeftModalNavigatorParamList = { - [SCREENS.LEFT_MODAL.CHAT_FINDER]: undefined; [SCREENS.LEFT_MODAL.WORKSPACE_SWITCHER]: undefined; }; diff --git a/src/pages/Search/SearchPageBottomTab.tsx b/src/pages/Search/SearchPageBottomTab.tsx index 38e4c5166884..b4fb2aac61ff 100644 --- a/src/pages/Search/SearchPageBottomTab.tsx +++ b/src/pages/Search/SearchPageBottomTab.tsx @@ -85,6 +85,8 @@ function SearchPageBottomTab() { ); } + const shouldDisplayCancelSearch = shouldUseNarrowLayout && !SearchUtils.isCannedSearchQuery(queryJSON); + return ( diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx index edc8dfb3cb3a..e77f2000b85f 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx @@ -4,6 +4,7 @@ import {useOnyx} from 'react-native-onyx'; import ScreenWrapper from '@components/ScreenWrapper'; import useActiveWorkspaceFromNavigationState from '@hooks/useActiveWorkspaceFromNavigationState'; import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import {updateLastAccessedWorkspace} from '@libs/actions/Policy/Policy'; import * as Browser from '@libs/Browser'; @@ -27,6 +28,7 @@ function BaseSidebarScreen() { const styles = useThemeStyles(); const activeWorkspaceID = useActiveWorkspaceFromNavigationState(); const {translate} = useLocalize(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const [activeWorkspace] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activeWorkspaceID ?? -1}`); useEffect(() => { @@ -43,6 +45,8 @@ function BaseSidebarScreen() { updateLastAccessedWorkspace(undefined); }, [activeWorkspace, activeWorkspaceID]); + const shouldDisplaySearch = shouldUseNarrowLayout; + return ( + jest.fn((fn: Record) => { + // eslint-disable-next-line no-param-reassign + fn.cancel = jest.fn(); + return fn; + }), +); + +jest.mock('@src/libs/Log'); + +jest.mock('@src/libs/API', () => ({ + write: jest.fn(), + makeRequestWithSideEffects: jest.fn(), + read: jest.fn(), +})); + +jest.mock('@src/libs/Navigation/Navigation', () => ({ + dismissModalWithReport: jest.fn(), + getTopmostReportId: jest.fn(), + isNavigationReady: jest.fn(() => Promise.resolve()), + isDisplayedInModal: jest.fn(() => false), +})); + +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native'); + return { + ...actualNav, + useFocusEffect: jest.fn(), + useIsFocused: () => true, + useRoute: () => jest.fn(), + useNavigation: () => ({ + navigate: jest.fn(), + addListener: () => jest.fn(), + }), + createNavigationContainerRef: () => ({ + addListener: () => jest.fn(), + removeListener: () => jest.fn(), + isReady: () => jest.fn(), + getCurrentRoute: () => jest.fn(), + getState: () => jest.fn(), + }), + }; +}); + +jest.mock('@src/components/withNavigationFocus', () => (Component: ComponentType) => { + function WithNavigationFocus(props: WithNavigationFocusProps) { + return ( + + ); + } + + WithNavigationFocus.displayName = 'WithNavigationFocus'; + + return WithNavigationFocus; +}); +// mock of useDismissedReferralBanners +jest.mock('../../src/hooks/useDismissedReferralBanners', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + __esModule: true, + default: jest.fn(() => ({ + isDismissed: false, + setAsDismissed: () => {}, + })), +})); + +const getMockedReports = (length = 100) => + createCollection( + (item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, + (index) => createRandomReport(index), + length, + ); + +const getMockedPersonalDetails = (length = 100) => + createCollection( + (item) => item.accountID, + (index) => createPersonalDetails(index), + length, + ); + +const mockedReports = getMockedReports(600); +const mockedBetas = Object.values(CONST.BETAS); +const mockedPersonalDetails = getMockedPersonalDetails(100); +const mockedOptions = createOptionList(mockedPersonalDetails, mockedReports); + +beforeAll(() => + Onyx.init({ + keys: ONYXKEYS, + safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT], + }), +); + +// Initialize the network key for OfflineWithFeedback +beforeEach(() => { + global.fetch = TestHelper.getGlobalFetchMock(); + wrapOnyxWithWaitForBatchedUpdates(Onyx); + Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); +}); + +// Clear out Onyx after each test so that each test starts with a clean state +afterEach(() => { + Onyx.clear(); +}); + +function ChatFinderPageWrapper(args: any) { + return ( + + + + {/* */} + + + ); +} + +function ChatFinderPageWithCachedOptions(args: any) { + return ( + + ({options: mockedOptions, initializeOptions: () => {}, areOptionsInitialized: true}), [])}> + {/* */} + + + ); +} + +test('[ChatFinderPage] should render list with cached options', async () => { + const {addListener} = createAddListenerMock(); + + const scenario = async () => { + await screen.findByTestId('ChatFinderPage'); // Todo [Search] fix testID no longer existing + }; + + // const navigation = {addListener} as unknown as StackNavigationProp; + + return waitForBatchedUpdates() + .then(() => + Onyx.multiSet({ + ...mockedReports, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, + [ONYXKEYS.BETAS]: mockedBetas, + [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, + }), + ) + .then(() => measureRenders(, {scenario})); +}); + +test('[ChatFinderPage] should interact when text input changes', async () => { + const {addListener} = createAddListenerMock(); + + const scenario = async () => { + await screen.findByTestId('ChatFinderPage'); + + const input = screen.getByTestId('selection-list-text-input'); + fireEvent.changeText(input, 'Email Four'); + fireEvent.changeText(input, 'Report'); + fireEvent.changeText(input, 'Email Five'); + }; + + // const navigation = {addListener} as unknown as StackNavigationProp; + + return waitForBatchedUpdates() + .then(() => + Onyx.multiSet({ + ...mockedReports, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, + [ONYXKEYS.BETAS]: mockedBetas, + [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, + }), + ) + .then(() => + measureRenders( + , + {scenario}, + ), + ); +}); From cb51d04588f2c442d9b18b1223104d10f73474b7 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Thu, 3 Oct 2024 16:55:14 +0200 Subject: [PATCH 05/95] Add performance timings handling for SearchRouterList --- src/components/Search/SearchRouter/SearchButton.tsx | 1 - src/components/Search/SearchRouter/SearchRouter.tsx | 3 +++ src/components/Search/SearchRouter/SearchRouterList.tsx | 9 +++++++++ src/libs/E2E/tests/openSearchRouterTest.e2e.ts | 6 +++--- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchButton.tsx b/src/components/Search/SearchRouter/SearchButton.tsx index 53660ee1f6c2..b1e59a323926 100644 --- a/src/components/Search/SearchRouter/SearchButton.tsx +++ b/src/components/Search/SearchRouter/SearchButton.tsx @@ -27,7 +27,6 @@ function SearchButton() { accessibilityLabel={translate('common.search')} style={[styles.flexRow, styles.touchableButtonImage]} onPress={Session.checkIfActionIsAllowed(() => { - // Todo [Search] add finishing for this timing in Timing.start(CONST.TIMING.SEARCH_ROUTER_OPEN); Performance.markStart(CONST.TIMING.SEARCH_ROUTER_OPEN); diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 8f5ad55bc0c9..868144492ce7 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -19,6 +19,7 @@ import * as SearchUtils from '@libs/SearchUtils'; import Navigation from '@navigation/Navigation'; import variables from '@styles/variables'; import * as Report from '@userActions/Report'; +import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -65,7 +66,9 @@ function SearchRouter() { }; } + Timing.start(CONST.TIMING.SEARCH_FILTER_OPTIONS); const newOptions = OptionsListUtils.filterOptions(searchOptions, debouncedInputValue, {sortByReportTypeInSearch: true, preferChatroomsOverThreads: true}); + Timing.end(CONST.TIMING.SEARCH_FILTER_OPTIONS); return { recentReports: newOptions.recentReports, diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index 7d86ce1150d5..a58ea969d28f 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -13,10 +13,13 @@ import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; +import Performance from '@libs/Performance'; import {getAllTaxRates} from '@libs/PolicyUtils'; import type {OptionData} from '@libs/ReportUtils'; import * as SearchUtils from '@libs/SearchUtils'; import * as Report from '@userActions/Report'; +import Timing from '@userActions/Timing'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -47,6 +50,11 @@ type SearchRouterListProps = { closeAndClearRouter: () => void; }; +const setPerformanceTimersEnd = () => { + Timing.end(CONST.TIMING.SEARCH_ROUTER_OPEN); + Performance.markEnd(CONST.TIMING.SEARCH_ROUTER_OPEN); +}; + function isSearchQueryItem(item: OptionData | SearchQueryItem): item is SearchQueryItem { if ('singleIcon' in item && item.singleIcon && 'query' in item && item.query) { return true; @@ -174,6 +182,7 @@ function SearchRouterList( ListItem={SearchRouterItem} containerStyle={[styles.mh100]} sectionListStyle={[isSmallScreenWidth ? styles.ph5 : styles.ph2, styles.pb2]} + onLayout={setPerformanceTimersEnd} ref={ref} showScrollIndicator={!isSmallScreenWidth} /> diff --git a/src/libs/E2E/tests/openSearchRouterTest.e2e.ts b/src/libs/E2E/tests/openSearchRouterTest.e2e.ts index 6ce1891a63d4..6c4646b09fbf 100644 --- a/src/libs/E2E/tests/openSearchRouterTest.e2e.ts +++ b/src/libs/E2E/tests/openSearchRouterTest.e2e.ts @@ -20,10 +20,10 @@ const test = () => { console.debug('[E2E] Logged in, getting search router metrics and submitting them…'); - const [openSearchPagePromise, openSearchPageResolve] = getPromiseWithResolve(); + const [openSearchRouterPromise, openSearchRouterResolve] = getPromiseWithResolve(); const [loadSearchOptionsPromise, loadSearchOptionsResolve] = getPromiseWithResolve(); - Promise.all([openSearchPagePromise, loadSearchOptionsPromise]).then(() => { + Promise.all([openSearchRouterPromise, loadSearchOptionsPromise]).then(() => { console.debug(`[E2E] Submitting!`); E2EClient.submitTestDone(); @@ -40,7 +40,7 @@ const test = () => { unit: 'ms', }) .then(() => { - openSearchPageResolve(); + openSearchRouterResolve(); console.debug('[E2E] Done with search, exiting…'); }) .catch((err) => { From 635e0da1032b297e9834791d4c7200b5852bc625 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Fri, 4 Oct 2024 11:52:05 +0200 Subject: [PATCH 06/95] Migrate reassure tests for ChatFinder to SearchRouter --- .../Search/SearchRouter/SearchRouter.tsx | 34 +++++----- .../Search/SearchRouter/SearchRouterInput.tsx | 1 + .../Search/SearchRouter/SearchRouterModal.tsx | 2 +- src/libs/E2E/reactNativeLaunchingTest.ts | 1 - .../Navigation/AppNavigator/AuthScreens.tsx | 2 +- ...rf-test.tsx => SearchRouter.perf-test.tsx} | 65 +++++-------------- 6 files changed, 35 insertions(+), 70 deletions(-) rename tests/perf-test/{ChatFinderPage.perf-test.tsx => SearchRouter.perf-test.tsx} (72%) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 868144492ce7..f49112e86c6c 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -23,13 +23,16 @@ import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {useSearchRouterContext} from './SearchRouterContext'; import SearchRouterInput from './SearchRouterInput'; import SearchRouterList from './SearchRouterList'; const SEARCH_DEBOUNCE_DELAY = 150; -function SearchRouter() { +type SearchRouterProps = { + onRouterClose: () => void; +}; + +function SearchRouter({onRouterClose}: SearchRouterProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [betas] = useOnyx(ONYXKEYS.BETAS); @@ -37,7 +40,6 @@ function SearchRouter() { const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false}); const {isSmallScreenWidth} = useResponsiveLayout(); - const {isSearchRouterDisplayed, closeSearchRouter} = useSearchRouterContext(); const listRef = useRef(null); const [textInputValue, debouncedInputValue, setTextInputValue] = useDebouncedState('', 500); @@ -90,15 +92,6 @@ function SearchRouter() { Report.searchInServer(debouncedInputValue.trim()); }, [debouncedInputValue]); - useEffect(() => { - if (!textInputValue && isSearchRouterDisplayed) { - return; - } - listRef.current?.updateAndScrollToFocusedIndex(0); - // eslint-disable-next-line react-compiler/react-compiler - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSearchRouterDisplayed]); - const contextualReportData = contextualReportID ? searchOptions.recentReports?.find((option) => option.reportID === contextualReportID) : undefined; const clearUserQuery = () => { @@ -135,40 +128,43 @@ function SearchRouter() { }; const closeAndClearRouter = useCallback(() => { - closeSearchRouter(); + onRouterClose(); clearUserQuery(); // eslint-disable-next-line react-compiler/react-compiler // eslint-disable-next-line react-hooks/exhaustive-deps - }, [closeSearchRouter]); + }, [onRouterClose]); const onSearchSubmit = useCallback( (query: SearchQueryJSON | undefined) => { if (!query) { return; } - closeSearchRouter(); + onRouterClose(); const queryString = SearchUtils.buildSearchQueryString(query); Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: queryString})); clearUserQuery(); }, // eslint-disable-next-line react-compiler/react-compiler // eslint-disable-next-line react-hooks/exhaustive-deps - [closeSearchRouter], + [onRouterClose], ); useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => { - closeSearchRouter(); + onRouterClose(); clearUserQuery(); }); const modalWidth = isSmallScreenWidth ? styles.w100 : {width: variables.popoverWidth}; return ( - + {isSmallScreenWidth && ( closeSearchRouter()} + onBackButtonPress={() => onRouterClose()} /> )} - {isSearchRouterDisplayed && } + {isSearchRouterDisplayed && } ); } diff --git a/src/libs/E2E/reactNativeLaunchingTest.ts b/src/libs/E2E/reactNativeLaunchingTest.ts index 672d66637ef0..fdd305baf88c 100644 --- a/src/libs/E2E/reactNativeLaunchingTest.ts +++ b/src/libs/E2E/reactNativeLaunchingTest.ts @@ -32,7 +32,6 @@ if (!appInstanceId) { // import your test here, define its name and config first in e2e/config.js const tests: Tests = { [E2EConfig.TEST_NAMES.AppStartTime]: require('./tests/appStartTimeTest.e2e').default, - // Todo [Search] rename [E2EConfig.TEST_NAMES.OpenSearchRouter]: require('./tests/openSearchRouterTest.e2e').default, [E2EConfig.TEST_NAMES.ChatOpening]: require('./tests/chatOpeningTest.e2e').default, [E2EConfig.TEST_NAMES.ReportTyping]: require('./tests/reportTypingTest.e2e').default, diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 62be199ee183..2ec31d8a8688 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -7,8 +7,8 @@ import ActiveGuidesEventListener from '@components/ActiveGuidesEventListener'; import ComposeProviders from '@components/ComposeProviders'; import OptionsListContextProvider from '@components/OptionListContextProvider'; import {SearchContextProvider} from '@components/Search/SearchContext'; -import SearchRouterModal from '@components/Search/SearchRouter/SearchRouterModal'; import {useSearchRouterContext} from '@components/Search/SearchRouter/SearchRouterContext'; +import SearchRouterModal from '@components/Search/SearchRouter/SearchRouterModal'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useOnboardingFlowRouter from '@hooks/useOnboardingFlow'; import usePermissions from '@hooks/usePermissions'; diff --git a/tests/perf-test/ChatFinderPage.perf-test.tsx b/tests/perf-test/SearchRouter.perf-test.tsx similarity index 72% rename from tests/perf-test/ChatFinderPage.perf-test.tsx rename to tests/perf-test/SearchRouter.perf-test.tsx index ed43b699adc4..5be0c93105c3 100644 --- a/tests/perf-test/ChatFinderPage.perf-test.tsx +++ b/tests/perf-test/SearchRouter.perf-test.tsx @@ -2,11 +2,11 @@ import type * as NativeNavigation from '@react-navigation/native'; import {fireEvent, screen} from '@testing-library/react-native'; import React, {useMemo} from 'react'; import type {ComponentType} from 'react'; -import {View} from 'react-native'; import Onyx from 'react-native-onyx'; import {measureRenders} from 'reassure'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; import OptionListContextProvider, {OptionsListContext} from '@components/OptionListContextProvider'; +import SearchRouter from '@components/Search/SearchRouter/SearchRouter'; import {KeyboardStateProvider} from '@components/withKeyboardState'; import type {WithNavigationFocusProps} from '@components/withNavigationFocus'; import {createOptionList} from '@libs/OptionsListUtils'; @@ -23,8 +23,6 @@ import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; -// Todo [Search] Either migrate this test to tests new SearchRouter or remove it completely. - jest.mock('lodash/debounce', () => jest.fn((fn: Record) => { // eslint-disable-next-line no-param-reassign @@ -66,6 +64,9 @@ jest.mock('@react-navigation/native', () => { getCurrentRoute: () => jest.fn(), getState: () => jest.fn(), }), + useNavigationState: () => ({ + routes: [], + }), }; }); @@ -84,15 +85,6 @@ jest.mock('@src/components/withNavigationFocus', () => (Component: ComponentType return WithNavigationFocus; }); -// mock of useDismissedReferralBanners -jest.mock('../../src/hooks/useDismissedReferralBanners', () => ({ - // eslint-disable-next-line @typescript-eslint/naming-convention - __esModule: true, - default: jest.fn(() => ({ - isDismissed: false, - setAsDismissed: () => {}, - })), -})); const getMockedReports = (length = 100) => createCollection( @@ -132,44 +124,33 @@ afterEach(() => { Onyx.clear(); }); -function ChatFinderPageWrapper(args: any) { +const mockOnClose = jest.fn(); + +function SearchRouterWrapper() { return ( - - {/* */} + ); } -function ChatFinderPageWithCachedOptions(args: any) { +function SearchRouterWrapperWithCachedOptions() { return ( ({options: mockedOptions, initializeOptions: () => {}, areOptionsInitialized: true}), [])}> - {/* */} + ); } -test('[ChatFinderPage] should render list with cached options', async () => { - const {addListener} = createAddListenerMock(); - +test('[SearchRouter] should render chat list with cached options', async () => { const scenario = async () => { - await screen.findByTestId('ChatFinderPage'); // Todo [Search] fix testID no longer existing + await screen.findByTestId('SearchRouter'); }; - // const navigation = {addListener} as unknown as StackNavigationProp; - return waitForBatchedUpdates() .then(() => Onyx.multiSet({ @@ -179,23 +160,19 @@ test('[ChatFinderPage] should render list with cached options', async () => { [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, }), ) - .then(() => measureRenders(, {scenario})); + .then(() => measureRenders(, {scenario})); }); -test('[ChatFinderPage] should interact when text input changes', async () => { - const {addListener} = createAddListenerMock(); - +test('[SearchRouter] should react to text input changes', async () => { const scenario = async () => { - await screen.findByTestId('ChatFinderPage'); + await screen.findByTestId('SearchRouter'); - const input = screen.getByTestId('selection-list-text-input'); + const input = screen.getByTestId('search-router-text-input'); fireEvent.changeText(input, 'Email Four'); fireEvent.changeText(input, 'Report'); fireEvent.changeText(input, 'Email Five'); }; - // const navigation = {addListener} as unknown as StackNavigationProp; - return waitForBatchedUpdates() .then(() => Onyx.multiSet({ @@ -205,13 +182,5 @@ test('[ChatFinderPage] should interact when text input changes', async () => { [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, }), ) - .then(() => - measureRenders( - , - {scenario}, - ), - ); + .then(() => measureRenders(, {scenario})); }); From 078420e53f8500f1e75fef3cfde10d64f2e7e8d5 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Fri, 4 Oct 2024 12:15:39 +0200 Subject: [PATCH 07/95] Remove SearchRouter dev check --- src/components/Search/SearchRouter/SearchButton.tsx | 5 ----- src/libs/Permissions.ts | 13 ------------- tests/perf-test/SearchRouter.perf-test.tsx | 1 - 3 files changed, 19 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchButton.tsx b/src/components/Search/SearchRouter/SearchButton.tsx index b1e59a323926..2ddc9a8262c2 100644 --- a/src/components/Search/SearchRouter/SearchButton.tsx +++ b/src/components/Search/SearchRouter/SearchButton.tsx @@ -6,7 +6,6 @@ import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Performance from '@libs/Performance'; -import Permissions from '@libs/Permissions'; import * as Session from '@userActions/Session'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; @@ -18,10 +17,6 @@ function SearchButton() { const {translate} = useLocalize(); const {openSearchRouter} = useSearchRouterContext(); - if (!Permissions.canUseNewSearchRouter()) { - return; - } - return ( ): boolean { return !!betas?.includes(CONST.BETAS.ALL); @@ -50,17 +49,6 @@ function canUseCombinedTrackSubmit(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.COMBINED_TRACK_SUBMIT); } -/** - * New Search Router is under construction and for now should be displayed only in dev to allow developers to work on it. - * We are not using BETA for this feature, as betas are heavier to cleanup, - * and the development of new router is expected to take 2-3 weeks at most - * - * After everything is implemented this function can be removed, as we will always use SearchRouter in the App. - */ -function canUseNewSearchRouter() { - return Environment.isDevelopment(); -} - /** * Link previews are temporarily disabled. */ @@ -80,5 +68,4 @@ export default { canUseNewDotCopilot, canUseWorkspaceRules, canUseCombinedTrackSubmit, - canUseNewSearchRouter, }; diff --git a/tests/perf-test/SearchRouter.perf-test.tsx b/tests/perf-test/SearchRouter.perf-test.tsx index 5be0c93105c3..e9154a36a9a1 100644 --- a/tests/perf-test/SearchRouter.perf-test.tsx +++ b/tests/perf-test/SearchRouter.perf-test.tsx @@ -18,7 +18,6 @@ import type {PersonalDetails, Report} from '@src/types/onyx'; import createCollection from '../utils/collections/createCollection'; import createPersonalDetails from '../utils/collections/personalDetails'; import createRandomReport from '../utils/collections/reports'; -import createAddListenerMock from '../utils/createAddListenerMock'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; From e21ed95a38c101515a3dbc8a10f7307375b6e22b Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Fri, 4 Oct 2024 17:37:12 +0200 Subject: [PATCH 08/95] Fix SearchRouter opening from keyboard shortcut --- .../SearchRouter/SearchRouterContext.tsx | 25 ++++++++++++++++--- .../Navigation/AppNavigator/AuthScreens.tsx | 12 +++------ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouterContext.tsx b/src/components/Search/SearchRouter/SearchRouterContext.tsx index d935fff110a4..4e01071a0916 100644 --- a/src/components/Search/SearchRouter/SearchRouterContext.tsx +++ b/src/components/Search/SearchRouter/SearchRouterContext.tsx @@ -1,10 +1,11 @@ -import React, {useContext, useMemo, useState} from 'react'; +import React, {useContext, useMemo, useRef, useState} from 'react'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; const defaultSearchContext = { isSearchRouterDisplayed: false, openSearchRouter: () => {}, closeSearchRouter: () => {}, + toggleSearchRouter: () => {}, }; type SearchRouterContext = typeof defaultSearchContext; @@ -13,15 +14,33 @@ const Context = React.createContext(defaultSearchContext); function SearchRouterContextProvider({children}: ChildrenProps) { const [isSearchRouterDisplayed, setIsSearchRouterDisplayed] = useState(false); + const searchRouterDisplayedRef = useRef(false); const routerContext = useMemo(() => { - const openSearchRouter = () => setIsSearchRouterDisplayed(true); - const closeSearchRouter = () => setIsSearchRouterDisplayed(false); + const openSearchRouter = () => { + setIsSearchRouterDisplayed(true); + searchRouterDisplayedRef.current = true; + }; + const closeSearchRouter = () => { + setIsSearchRouterDisplayed(false); + searchRouterDisplayedRef.current = false; + }; + + // There are callbacks that live outside of React render-loop and interact with SearchRouter + // So we need a function that is based on ref to correctly open/close it + const toggleSearchRouter = () => { + if (searchRouterDisplayedRef.current) { + closeSearchRouter(); + } else { + openSearchRouter(); + } + }; return { isSearchRouterDisplayed, openSearchRouter, closeSearchRouter, + toggleSearchRouter, }; }, [isSearchRouterDisplayed, setIsSearchRouterDisplayed]); diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 2ec31d8a8688..b9ea16a344f2 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -234,7 +234,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie const screenOptions = getRootNavigatorScreenOptions(shouldUseNarrowLayout, styles, StyleUtils); const {canUseDefaultRooms} = usePermissions(); const {activeWorkspaceID} = useActiveWorkspace(); - const {openSearchRouter} = useSearchRouterContext(); + const {toggleSearchRouter} = useSearchRouterContext(); const onboardingModalScreenOptions = useMemo(() => screenOptions.onboardingModalNavigator(onboardingIsMediumOrLargerScreenWidth), [screenOptions, onboardingIsMediumOrLargerScreenWidth]); const onboardingScreenOptions = useMemo( @@ -372,13 +372,9 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie const unsubscribeSearchShortcut = KeyboardShortcut.subscribe( searchShortcutConfig.shortcutKey, () => { - Modal.close( - Session.checkIfActionIsAllowed(() => { - openSearchRouter(); - }), - true, - true, - ); + Session.checkIfActionIsAllowed(() => { + toggleSearchRouter(); + })(); }, shortcutsOverviewShortcutConfig.descriptionKey, shortcutsOverviewShortcutConfig.modifiers, From 3fdadb27e4d92897e7eb4506683d6876ca02e129 Mon Sep 17 00:00:00 2001 From: truph01 Date: Sat, 5 Oct 2024 21:41:17 +0700 Subject: [PATCH 09/95] fix: Description with mentions shown in preview but after saved disappeared --- .../MentionReportRenderer/index.tsx | 4 +-- src/pages/ReportDetailsPage.tsx | 28 +++++++++++-------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/index.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/index.tsx index 7aa0f5eca22a..3e0445796a60 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/index.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/index.tsx @@ -57,7 +57,7 @@ function MentionReportRenderer({style, tnode, TDefaultRenderer, reports, ...defa const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const htmlAttributeReportID = tnode.attributes.reportid; - const {currentReportID: currentReportIDContext} = useContext(MentionReportContext); + const {currentReportID: currentReportIDContext, exactlyMatch} = useContext(MentionReportContext); const currentReportID = useCurrentReportID(); const currentReportIDValue = currentReportIDContext || currentReportID?.currentReportID; @@ -86,7 +86,7 @@ function MentionReportRenderer({style, tnode, TDefaultRenderer, reports, ...defa // eslint-disable-next-line react/jsx-props-no-spreading {...defaultRendererProps} style={ - isGroupPolicyReport + isGroupPolicyReport && (!exactlyMatch || navigationRoute) ? [styles.link, styleWithoutColor, StyleUtils.getMentionStyle(isCurrentRoomMention), {color: StyleUtils.getMentionTextColor(isCurrentRoomMention)}] : [] } diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index e51b8e36704a..4c84f7daacb0 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -10,6 +10,7 @@ import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView import DelegateNoAccessModal from '@components/DelegateNoAccessModal'; import DisplayNames from '@components/DisplayNames'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import MentionReportContext from '@components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; @@ -736,6 +737,9 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) { isTransactionDeleted.current = true; }, [caseID, iouTransactionID, moneyRequestReport?.reportID, report, requestParentReportAction, isSingleTransactionView]); + + const mentionReportContextValue = useMemo(() => ({currentReportID: report.reportID, exactlyMatch: true}), [report.reportID]); + return ( @@ -755,17 +759,19 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) { {shouldShowReportDescription && ( - Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report.reportID, Navigation.getActiveRoute()))} - /> + + Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report.reportID, Navigation.getActiveRoute()))} + /> + )} From 4b81669e070a276265442998aaba3890603e66b1 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Mon, 7 Oct 2024 11:47:54 +0200 Subject: [PATCH 10/95] close modals on openSearchRouter --- .../Search/SearchRouter/SearchRouterContext.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouterContext.tsx b/src/components/Search/SearchRouter/SearchRouterContext.tsx index 4e01071a0916..2e4cbec0d6bb 100644 --- a/src/components/Search/SearchRouter/SearchRouterContext.tsx +++ b/src/components/Search/SearchRouter/SearchRouterContext.tsx @@ -1,4 +1,5 @@ import React, {useContext, useMemo, useRef, useState} from 'react'; +import * as Modal from '@userActions/Modal'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; const defaultSearchContext = { @@ -18,8 +19,14 @@ function SearchRouterContextProvider({children}: ChildrenProps) { const routerContext = useMemo(() => { const openSearchRouter = () => { - setIsSearchRouterDisplayed(true); - searchRouterDisplayedRef.current = true; + Modal.close( + () => { + setIsSearchRouterDisplayed(true); + searchRouterDisplayedRef.current = true; + }, + false, + true, + ); }; const closeSearchRouter = () => { setIsSearchRouterDisplayed(false); From 47ba23f6e5dd5b744135ed46dbe76fac100df4a7 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Tue, 8 Oct 2024 11:05:11 +0200 Subject: [PATCH 11/95] fix design comments --- src/components/Search/SearchRouter/SearchRouterList.tsx | 1 + src/components/Search/SearchRouter/SearchRouterModal.tsx | 2 +- src/styles/index.ts | 4 ++-- src/styles/utils/spacing.ts | 4 ++++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index a58ea969d28f..70abc5c89550 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -185,6 +185,7 @@ function SearchRouterList( onLayout={setPerformanceTimersEnd} ref={ref} showScrollIndicator={!isSmallScreenWidth} + sectionTitleStyles={styles.mhn2} /> ); } diff --git a/src/components/Search/SearchRouter/SearchRouterModal.tsx b/src/components/Search/SearchRouter/SearchRouterModal.tsx index c44b66044ab3..7e403461dd34 100644 --- a/src/components/Search/SearchRouter/SearchRouterModal.tsx +++ b/src/components/Search/SearchRouter/SearchRouterModal.tsx @@ -17,7 +17,7 @@ function SearchRouterModal() { type={modalType} fullscreen isVisible={isSearchRouterDisplayed} - popoverAnchorPosition={{right: 20, top: 20}} + popoverAnchorPosition={{right: 6, top: 6}} onClose={closeSearchRouter} > {isSearchRouterDisplayed && } diff --git a/src/styles/index.ts b/src/styles/index.ts index 8220389867d9..b81d76c8d7d9 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3615,8 +3615,8 @@ const styles = (theme: ThemeColors) => searchInputStyle: { color: theme.textSupporting, - fontSize: 13, - lineHeight: 16, + fontSize: variables.fontSizeNormal, + lineHeight: variables.fontSizeNormalHeight, }, searchRouterTextInputContainer: { diff --git a/src/styles/utils/spacing.ts b/src/styles/utils/spacing.ts index c2008d8a68f0..9f741c9d925b 100644 --- a/src/styles/utils/spacing.ts +++ b/src/styles/utils/spacing.ts @@ -55,6 +55,10 @@ export default { marginHorizontal: 32, }, + mhn2: { + marginHorizontal: -8, + }, + mhn5: { marginHorizontal: -20, }, From 9136e8a0b1f765feaae603135be1a417534e16e5 Mon Sep 17 00:00:00 2001 From: I Nyoman Jyotisa Date: Tue, 8 Oct 2024 20:17:03 +0800 Subject: [PATCH 12/95] set the selected rates to the transaction currency and make prompt text agnostic --- src/languages/en.ts | 3 +-- src/languages/es.ts | 3 +-- src/languages/params.ts | 3 --- .../iou/request/step/IOURequestStepDistanceRate.tsx | 10 +++++++--- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 18dcbe35c9f6..b0fbf03298f0 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -109,7 +109,6 @@ import type { PayerSettledParams, PaySomeoneParams, ReconciliationWorksParams, - ReimbursementRateParams, RemovedFromApprovalWorkflowParams, RemovedTheRequestParams, RemoveMemberPromptParams, @@ -1009,7 +1008,7 @@ const translations = { changed: 'changed', removed: 'removed', transactionPending: 'Transaction pending.', - chooseARate: ({unit}: ReimbursementRateParams) => `Select a workspace reimbursement rate per ${unit}`, + chooseARate: `Select a workspace reimbursement rate per mile or kilometer`, unapprove: 'Unapprove', unapproveReport: 'Unapprove report', headsUp: 'Heads up!', diff --git a/src/languages/es.ts b/src/languages/es.ts index 8e16c03a91d3..aad4b3967866 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -107,7 +107,6 @@ import type { PayerSettledParams, PaySomeoneParams, ReconciliationWorksParams, - ReimbursementRateParams, RemovedFromApprovalWorkflowParams, RemovedTheRequestParams, RemoveMemberPromptParams, @@ -1003,7 +1002,7 @@ const translations = { changed: 'cambió', removed: 'eliminó', transactionPending: 'Transacción pendiente.', - chooseARate: ({unit}: ReimbursementRateParams) => `Selecciona una tasa de reembolso por ${unit} del espacio de trabajo`, + chooseARate: `Selecciona una tasa de reembolso por milla o kilómetro para el espacio de trabajo`, unapprove: 'Desaprobar', unapproveReport: 'Anular la aprobación del informe', headsUp: 'Atención!', diff --git a/src/languages/params.ts b/src/languages/params.ts index 5560316d2ddd..893b02e97591 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -279,8 +279,6 @@ type LogSizeAndDateParams = {size: number; date: string}; type HeldRequestParams = {comment: string}; -type ReimbursementRateParams = {unit: Unit}; - type ChangeFieldParams = {oldValue?: string; newValue: string; fieldName: string}; type ChangePolicyParams = {fromPolicy: string; toPolicy: string}; @@ -648,7 +646,6 @@ export type { PayerPaidAmountParams, PayerPaidParams, PayerSettledParams, - ReimbursementRateParams, RemovedTheRequestParams, RenamedRoomActionParams, ReportArchiveReasonsClosedParams, diff --git a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx index 59e1591a23ff..096651089cab 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx @@ -64,6 +64,8 @@ function IOURequestStepDistanceRate({ const currentRateID = TransactionUtils.getRateID(transaction) ?? '-1'; + const transactionCurrency = TransactionUtils.getCurrency(transaction); + const rates = DistanceRequestUtils.getMileageRates(policy, false, currentRateID); const navigateBack = () => { @@ -71,7 +73,9 @@ function IOURequestStepDistanceRate({ }; const sections = Object.values(rates).map((rate) => { - const rateForDisplay = DistanceRequestUtils.getRateForDisplay(rate.unit, rate.rate, rate.currency, translate, toLocaleDigit); + const isSelected = currentRateID ? currentRateID === rate.customUnitRateID : rate.name === CONST.CUSTOM_UNITS.DEFAULT_RATE; + + const rateForDisplay = DistanceRequestUtils.getRateForDisplay(rate.unit, rate.rate, isSelected ? transactionCurrency : rate.currency, translate, toLocaleDigit); return { text: rate.name ?? rateForDisplay, @@ -79,7 +83,7 @@ function IOURequestStepDistanceRate({ keyForList: rate.customUnitRateID, value: rate.customUnitRateID, isDisabled: !rate.enabled, - isSelected: currentRateID ? currentRateID === rate.customUnitRateID : rate.name === CONST.CUSTOM_UNITS.DEFAULT_RATE, + isSelected, }; }); @@ -118,7 +122,7 @@ function IOURequestStepDistanceRate({ shouldShowWrapper testID={IOURequestStepDistanceRate.displayName} > - {translate('iou.chooseARate', {unit})} + {translate('iou.chooseARate')} Date: Tue, 8 Oct 2024 20:19:49 +0800 Subject: [PATCH 13/95] lint fix --- src/languages/params.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/params.ts b/src/languages/params.ts index 893b02e97591..e4a52f5150fd 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -1,6 +1,6 @@ import type {OnyxInputOrEntry, ReportAction} from '@src/types/onyx'; import type {DelegateRole} from '@src/types/onyx/Account'; -import type {AllConnectionName, ConnectionName, PolicyConnectionSyncStage, SageIntacctMappingName, Unit} from '@src/types/onyx/Policy'; +import type {AllConnectionName, ConnectionName, PolicyConnectionSyncStage, SageIntacctMappingName} from '@src/types/onyx/Policy'; import type {ViolationDataType} from '@src/types/onyx/TransactionViolation'; type AddressLineParams = { From 5a7ada0c7df68a7c9370e03ce1a30398e1075e1b Mon Sep 17 00:00:00 2001 From: I Nyoman Jyotisa Date: Tue, 8 Oct 2024 21:14:38 +0800 Subject: [PATCH 14/95] migrate to useOnyx --- .../step/IOURequestStepDistanceRate.tsx | 40 ++++--------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx index 096651089cab..b2d633a9b880 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx @@ -1,6 +1,6 @@ import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; -import {useOnyx, withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import Text from '@components/Text'; @@ -23,35 +23,23 @@ import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; -type IOURequestStepDistanceRateOnyxProps = { - /** Policy details */ - policy: OnyxEntry; - - /** Collection of categories attached to the policy */ - policyCategories: OnyxEntry; - - /** Collection of tags attached to the policy */ - policyTags: OnyxEntry; +type IOURequestStepDistanceRateProps = WithWritableReportOrNotFoundProps & { + /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ + transaction: OnyxEntry; }; -type IOURequestStepDistanceRateProps = IOURequestStepDistanceRateOnyxProps & - WithWritableReportOrNotFoundProps & { - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - transaction: OnyxEntry; - }; - function IOURequestStepDistanceRate({ - policy: policyReal, report, reportDraft, route: { params: {action, reportID, backTo, transactionID}, }, transaction, - policyTags, - policyCategories, }: IOURequestStepDistanceRateProps) { const [policyDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${IOU.getIOURequestPolicyID(transaction, reportDraft) ?? '-1'}`); + const [policyReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '-1'}`); + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`); + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`); const policy = policyReal ?? policyDraft; @@ -137,20 +125,8 @@ function IOURequestStepDistanceRate({ IOURequestStepDistanceRate.displayName = 'IOURequestStepDistanceRate'; -const IOURequestStepDistanceRateWithOnyx = withOnyx({ - policy: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '-1'}`, - }, - policyCategories: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`, - }, - policyTags: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, - }, -})(IOURequestStepDistanceRate); - // eslint-disable-next-line rulesdir/no-negated-variables -const IOURequestStepDistanceRateWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepDistanceRateWithOnyx); +const IOURequestStepDistanceRateWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepDistanceRate); // eslint-disable-next-line rulesdir/no-negated-variables const IOURequestStepDistanceRateWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepDistanceRateWithWritableReportOrNotFound); From 36a4e27d7637d710b24a53aa1992c8ec606d954d Mon Sep 17 00:00:00 2001 From: I Nyoman Jyotisa Date: Tue, 8 Oct 2024 21:27:01 +0800 Subject: [PATCH 15/95] minor fix --- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index b0fbf03298f0..f4faade51600 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1008,7 +1008,7 @@ const translations = { changed: 'changed', removed: 'removed', transactionPending: 'Transaction pending.', - chooseARate: `Select a workspace reimbursement rate per mile or kilometer`, + chooseARate: 'Select a workspace reimbursement rate per mile or kilometer', unapprove: 'Unapprove', unapproveReport: 'Unapprove report', headsUp: 'Heads up!', diff --git a/src/languages/es.ts b/src/languages/es.ts index aad4b3967866..99d240c2372c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1002,7 +1002,7 @@ const translations = { changed: 'cambió', removed: 'eliminó', transactionPending: 'Transacción pendiente.', - chooseARate: `Selecciona una tasa de reembolso por milla o kilómetro para el espacio de trabajo`, + chooseARate: 'Selecciona una tasa de reembolso por milla o kilómetro para el espacio de trabajo', unapprove: 'Desaprobar', unapproveReport: 'Anular la aprobación del informe', headsUp: 'Atención!', From ac87753d2557f6c8621d5212a73e8c42b51bc5ec Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Tue, 8 Oct 2024 15:26:06 +0100 Subject: [PATCH 16/95] Fix expense report avatar --- .../home/report/ReportActionItemSingle.tsx | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index ca11a1b02d26..06dd57acfaac 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -95,13 +95,18 @@ function ReportActionItemSingle({ let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); const isTripRoom = ReportUtils.isTripRoom(report); const isReportPreviewAction = action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW; - const displayAllActors = isReportPreviewAction && !isTripRoom; + const displayAllActors = isReportPreviewAction && !isTripRoom && !ReportUtils.isPolicyExpenseChat(report); const isInvoiceReport = ReportUtils.isInvoiceReport(iouReport ?? null); const isWorkspaceActor = isInvoiceReport || (ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors)); const ownerAccountID = iouReport?.ownerAccountID ?? action?.childOwnerAccountID; let avatarSource = avatar; let avatarId: number | string | undefined = actorAccountID; + if (isReportPreviewAction && ReportUtils.isPolicyExpenseChat(report)) { + avatarId = ownerAccountID; + avatarSource = personalDetails[ownerAccountID ?? -1]?.avatar; + displayName = ReportUtils.getDisplayNameForParticipant(ownerAccountID); + } if (isWorkspaceActor) { displayName = ReportUtils.getPolicyName(report, undefined, policy); actorHint = displayName; @@ -193,6 +198,15 @@ function ReportActionItemSingle({ ); const getAvatar = () => { + if (shouldShowSubscriptAvatar) { + return ( + + ); + } if (displayAllActors) { return ( ); } - if (shouldShowSubscriptAvatar) { - return ( - - ); - } return ( Date: Tue, 8 Oct 2024 15:48:23 +0100 Subject: [PATCH 17/95] fix tooltip display name --- src/pages/home/report/ReportActionItemSingle.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 06dd57acfaac..5e1f0a591532 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -106,6 +106,7 @@ function ReportActionItemSingle({ avatarId = ownerAccountID; avatarSource = personalDetails[ownerAccountID ?? -1]?.avatar; displayName = ReportUtils.getDisplayNameForParticipant(ownerAccountID); + actorHint = displayName; } if (isWorkspaceActor) { displayName = ReportUtils.getPolicyName(report, undefined, policy); From c0542d589a974e741d89b1484b0598d21eb30ab5 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Tue, 8 Oct 2024 20:03:34 +0100 Subject: [PATCH 18/95] fix(debug mode): wrong reason for why RBR reports are visible in LHN --- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/DebugUtils.ts | 9 ++- src/libs/OptionsListUtils.ts | 119 +--------------------------- src/libs/ReportUtils.ts | 115 ++++++++++++++++++++++++++- src/libs/SidebarUtils.ts | 20 +---- src/libs/WorkspacesSettingsUtils.ts | 2 +- 7 files changed, 129 insertions(+), 138 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index c44ac6f2cedf..eb68cf9b23c2 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4931,6 +4931,7 @@ const translations = { reasonVisibleInLHN: { hasDraftComment: 'Has draft comment', hasGBR: 'Has GBR', + hasRBR: 'Has RBR', pinnedByUser: 'Pinned by user', hasIOUViolations: 'Has IOU violations', hasAddWorkspaceRoomErrors: 'Has add workspace room errors', diff --git a/src/languages/es.ts b/src/languages/es.ts index 76cb22772f88..7aec0d6eef50 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5443,6 +5443,7 @@ const translations = { reasonVisibleInLHN: { hasDraftComment: 'Tiene comentario en borrador', hasGBR: 'Tiene GBR', + hasRBR: 'Tiene RBR', pinnedByUser: 'Fijado por el usuario', hasIOUViolations: 'Tiene violaciones de IOU', hasAddWorkspaceRoomErrors: 'Tiene errores al agregar sala de espacio de trabajo', diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index ed8e486517ff..af682e9cdc6a 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -7,7 +7,6 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Beta, Policy, Report, ReportAction, ReportActions, TransactionViolation} from '@src/types/onyx'; -import * as OptionsListUtils from './OptionsListUtils'; import * as ReportUtils from './ReportUtils'; class NumberError extends SyntaxError { @@ -598,7 +597,11 @@ function getReasonForShowingRowInLHN(report: OnyxEntry): TranslationPath return null; } - const doesReportHaveViolations = OptionsListUtils.shouldShowViolations(report, transactionViolations); + const doesReportHaveViolations = ReportUtils.shouldShowViolations(report, transactionViolations); + + if (ReportUtils.hasReportErrorsOtherThanFailedReceipt(report, doesReportHaveViolations, transactionViolations)) { + return `debug.reasonVisibleInLHN.hasRBR`; + } const reason = ReportUtils.reasonForReportToBeInOptionList({ report, @@ -646,7 +649,7 @@ function getReasonAndReportActionForGBRInLHNRow(report: OnyxEntry): GBRR * Gets the report action that is causing the RBR to show up in LHN */ function getRBRReportAction(report: OnyxEntry, reportActions: OnyxEntry): OnyxEntry { - const {reportAction} = OptionsListUtils.getAllReportActionsErrorsAndReportActionThatRequiresAttention(report, reportActions); + const {reportAction} = ReportUtils.getAllReportActionsErrorsAndReportActionThatRequiresAttention(report, reportActions); return reportAction; } diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index a004974b88e4..2bb52b461f55 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -41,7 +41,6 @@ import type DeepValueOf from '@src/types/utils/DeepValueOf'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import times from '@src/utils/times'; import Timing from './actions/Timing'; -import * as ErrorUtils from './ErrorUtils'; import filterArrayByMatch from './filterArrayByMatch'; import localeCompare from './LocaleCompare'; import * as LocalePhoneNumber from './LocalePhoneNumber'; @@ -342,26 +341,6 @@ Onyx.connect({ }, }); -let allTransactions: OnyxCollection = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.TRANSACTION, - waitForCollectionCallback: true, - callback: (value) => { - if (!value) { - return; - } - - allTransactions = Object.keys(value) - .filter((key) => !!value[key]) - .reduce((result: OnyxCollection, key) => { - if (result) { - // eslint-disable-next-line no-param-reassign - result[key] = value[key]; - } - return result; - }, {}); - }, -}); let activePolicyID: OnyxEntry; Onyx.connect({ key: ONYXKEYS.NVP_ACTIVE_POLICY_ID, @@ -480,78 +459,6 @@ function uniqFast(items: string[]): string[] { return result; } -type ReportErrorsAndReportActionThatRequiresAttention = { - errors: OnyxCommon.ErrorFields; - reportAction?: OnyxEntry; -}; - -function getAllReportActionsErrorsAndReportActionThatRequiresAttention(report: OnyxEntry, reportActions: OnyxEntry): ReportErrorsAndReportActionThatRequiresAttention { - const reportActionsArray = Object.values(reportActions ?? {}); - const reportActionErrors: OnyxCommon.ErrorFields = {}; - let reportAction: OnyxEntry; - - for (const action of reportActionsArray) { - if (action && !isEmptyObject(action.errors)) { - Object.assign(reportActionErrors, action.errors); - - if (!reportAction) { - reportAction = action; - } - } - } - const parentReportAction: OnyxEntry = - !report?.parentReportID || !report?.parentReportActionID ? undefined : allReportActions?.[report.parentReportID ?? '-1']?.[report.parentReportActionID ?? '-1']; - - if (ReportActionUtils.wasActionTakenByCurrentUser(parentReportAction) && ReportActionUtils.isTransactionThread(parentReportAction)) { - const transactionID = ReportActionUtils.isMoneyRequestAction(parentReportAction) ? ReportActionUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID : null; - const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; - if (TransactionUtils.hasMissingSmartscanFields(transaction ?? null) && !ReportUtils.isSettled(transaction?.reportID)) { - reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericSmartscanFailureMessage'); - reportAction = undefined; - } - } else if ((ReportUtils.isIOUReport(report) || ReportUtils.isExpenseReport(report)) && report?.ownerAccountID === currentUserAccountID) { - if (ReportUtils.shouldShowRBRForMissingSmartscanFields(report?.reportID ?? '-1') && !ReportUtils.isSettled(report?.reportID)) { - reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericSmartscanFailureMessage'); - reportAction = ReportUtils.getReportActionWithMissingSmartscanFields(report?.reportID ?? '-1'); - } - } else if (ReportUtils.hasSmartscanError(reportActionsArray)) { - reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericSmartscanFailureMessage'); - reportAction = ReportUtils.getReportActionWithSmartscanError(reportActionsArray); - } - - return { - errors: reportActionErrors, - reportAction, - }; -} - -/** - * Get an object of error messages keyed by microtime by combining all error objects related to the report. - */ -function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry): OnyxCommon.Errors { - const reportErrors = report?.errors ?? {}; - const reportErrorFields = report?.errorFields ?? {}; - const {errors: reportActionErrors} = getAllReportActionsErrorsAndReportActionThatRequiresAttention(report, reportActions); - - // All error objects related to the report. Each object in the sources contains error messages keyed by microtime - const errorSources = { - reportErrors, - ...reportErrorFields, - ...reportActionErrors, - }; - - // Combine all error messages keyed by microtime into one object - const errorSourcesArray = Object.values(errorSources ?? {}); - const allReportErrors = {}; - - for (const errors of errorSourcesArray) { - if (!isEmptyObject(errors)) { - Object.assign(allReportErrors, errors); - } - } - return allReportErrors; -} - /** * Get the last actor display name from last actor details. */ @@ -745,7 +652,7 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails } function hasReportErrors(report: Report, reportActions: OnyxEntry) { - return !isEmptyObject(getAllReportErrors(report, reportActions)); + return !isEmptyObject(ReportUtils.getAllReportErrors(report, reportActions)); } /** @@ -813,7 +720,7 @@ function createOption( result.shouldShowSubscript = ReportUtils.shouldReportShowSubscript(report); result.isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); result.isOwnPolicyExpenseChat = report.isOwnPolicyExpenseChat ?? false; - result.allReportErrors = getAllReportErrors(report, reportActions); + result.allReportErrors = ReportUtils.getAllReportErrors(report, reportActions); result.brickRoadIndicator = hasReportErrors(report, reportActions) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; result.pendingAction = report.pendingFields ? report.pendingFields.addWorkspaceRoom ?? report.pendingFields.createChat : undefined; result.ownerAccountID = report.ownerAccountID; @@ -1767,23 +1674,6 @@ function getUserToInviteOption({ return userToInvite; } -/** - * Check whether report has violations - */ -function shouldShowViolations(report: Report, transactionViolations: OnyxCollection) { - const {parentReportID, parentReportActionID} = report ?? {}; - const canGetParentReport = parentReportID && parentReportActionID && allReportActions; - if (!canGetParentReport) { - return false; - } - const parentReportActions = allReportActions ? allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`] ?? {} : {}; - const parentReportAction = parentReportActions[parentReportActionID] ?? null; - if (!parentReportAction) { - return false; - } - return ReportUtils.shouldDisplayTransactionThreadViolations(report, transactionViolations, parentReportAction); -} - /** * filter options based on specific conditions */ @@ -1894,7 +1784,7 @@ function getOptions( // Filter out all the reports that shouldn't be displayed const filteredReportOptions = options.reports.filter((option) => { const report = option.item; - const doesReportHaveViolations = shouldShowViolations(report, transactionViolations); + const doesReportHaveViolations = ReportUtils.shouldShowViolations(report, transactionViolations); return ReportUtils.shouldReportBeInOptionList({ report, @@ -2625,7 +2515,6 @@ export { getPersonalDetailsForAccountIDs, getIOUConfirmationOptionsFromPayeePersonalDetail, isSearchStringMatchUserDetails, - getAllReportErrors, getPolicyExpenseReportOption, getIOUReportIDOfLastAction, getParticipantsOption, @@ -2651,13 +2540,11 @@ export { getFirstKeyForList, canCreateOptimisticPersonalDetailOption, getUserToInviteOption, - shouldShowViolations, getPersonalDetailSearchTerms, getCurrentUserSearchTerms, getEmptyOptions, shouldUseBoldText, getAlternateText, - getAllReportActionsErrorsAndReportActionThatRequiresAttention, hasReportErrors, }; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 4a8d48d4d81a..8bf7984e3e34 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -48,7 +48,7 @@ import type {Participant} from '@src/types/onyx/IOU'; import type {SelectedParticipant} from '@src/types/onyx/NewGroupChatDraft'; import type {OriginalMessageExportedToIntegration} from '@src/types/onyx/OldDotAction'; import type Onboarding from '@src/types/onyx/Onboarding'; -import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; +import type {ErrorFields, Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; import type {OriginalMessageChangeLog, PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type {Status} from '@src/types/onyx/PersonalDetails'; import type {ConnectionName} from '@src/types/onyx/Policy'; @@ -64,6 +64,7 @@ import * as SessionUtils from './actions/Session'; import * as CurrencyUtils from './CurrencyUtils'; import DateUtils from './DateUtils'; import {hasValidDraftComment} from './DraftCommentUtils'; +import * as ErrorUtils from './ErrorUtils'; import getAttachmentDetails from './fileDownload/getAttachmentDetails'; import getIsSmallScreenWidth from './getIsSmallScreenWidth'; import isReportMessageAttachment from './isReportMessageAttachment'; @@ -1375,7 +1376,7 @@ function findLastAccessedReport(ignoreDomainRooms: boolean, openOnAdminRoom = fa } // We allow public announce rooms, admins, and announce rooms through since we bypass the default rooms beta for them. - // Check where ReportUtils.findLastAccessedReport is called in MainDrawerNavigator.js for more context. + // Check where findLastAccessedReport is called in MainDrawerNavigator.js for more context. // Domain rooms are now the only type of default room that are on the defaultRooms beta. if (ignoreDomainRooms && isDomainRoom(report) && !hasExpensifyGuidesEmails(Object.keys(report?.participants ?? {}).map(Number))) { return false; @@ -6217,6 +6218,112 @@ function shouldAdminsRoomBeVisible(report: OnyxEntry): boolean { return true; } +/** + * Check whether report has violations + */ +function shouldShowViolations(report: Report, transactionViolations: OnyxCollection) { + const {parentReportID, parentReportActionID} = report ?? {}; + const canGetParentReport = parentReportID && parentReportActionID && allReportActions; + if (!canGetParentReport) { + return false; + } + const parentReportActions = allReportActions ? allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`] ?? {} : {}; + const parentReportAction = parentReportActions[parentReportActionID] ?? null; + if (!parentReportAction) { + return false; + } + return shouldDisplayTransactionThreadViolations(report, transactionViolations, parentReportAction); +} + +type ReportErrorsAndReportActionThatRequiresAttention = { + errors: ErrorFields; + reportAction?: OnyxEntry; +}; + +function getAllReportActionsErrorsAndReportActionThatRequiresAttention(report: OnyxEntry, reportActions: OnyxEntry): ReportErrorsAndReportActionThatRequiresAttention { + const reportActionsArray = Object.values(reportActions ?? {}); + const reportActionErrors: ErrorFields = {}; + let reportAction: OnyxEntry; + + for (const action of reportActionsArray) { + if (action && !isEmptyObject(action.errors)) { + Object.assign(reportActionErrors, action.errors); + + if (!reportAction) { + reportAction = action; + } + } + } + const parentReportAction: OnyxEntry = + !report?.parentReportID || !report?.parentReportActionID ? undefined : allReportActions?.[report.parentReportID ?? '-1']?.[report.parentReportActionID ?? '-1']; + + if (ReportActionsUtils.wasActionTakenByCurrentUser(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction)) { + const transactionID = ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID : null; + const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + if (TransactionUtils.hasMissingSmartscanFields(transaction ?? null) && !isSettled(transaction?.reportID)) { + reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericSmartscanFailureMessage'); + reportAction = undefined; + } + } else if ((isIOUReport(report) || isExpenseReport(report)) && report?.ownerAccountID === currentUserAccountID) { + if (shouldShowRBRForMissingSmartscanFields(report?.reportID ?? '-1') && !isSettled(report?.reportID)) { + reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericSmartscanFailureMessage'); + reportAction = getReportActionWithMissingSmartscanFields(report?.reportID ?? '-1'); + } + } else if (hasSmartscanError(reportActionsArray)) { + reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericSmartscanFailureMessage'); + reportAction = getReportActionWithSmartscanError(reportActionsArray); + } + + return { + errors: reportActionErrors, + reportAction, + }; +} + +/** + * Get an object of error messages keyed by microtime by combining all error objects related to the report. + */ +function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry): Errors { + const reportErrors = report?.errors ?? {}; + const reportErrorFields = report?.errorFields ?? {}; + const {errors: reportActionErrors} = getAllReportActionsErrorsAndReportActionThatRequiresAttention(report, reportActions); + + // All error objects related to the report. Each object in the sources contains error messages keyed by microtime + const errorSources = { + reportErrors, + ...reportErrorFields, + ...reportActionErrors, + }; + + // Combine all error messages keyed by microtime into one object + const errorSourcesArray = Object.values(errorSources ?? {}); + const allReportErrors = {}; + + for (const errors of errorSourcesArray) { + if (!isEmptyObject(errors)) { + Object.assign(allReportErrors, errors); + } + } + return allReportErrors; +} + +function hasReportErrorsOtherThanFailedReceipt(report: Report, doesReportHaveViolations: boolean, transactionViolations: OnyxCollection) { + const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`] ?? {}; + const allReportErrors = getAllReportErrors(report, reportActions) ?? {}; + const transactionReportActions = ReportActionsUtils.getAllReportActions(report.reportID); + const oneTransactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(report.reportID, transactionReportActions, undefined); + let doesTransactionThreadReportHasViolations = false; + if (oneTransactionThreadReportID) { + const transactionReport = getReport(oneTransactionThreadReportID); + doesTransactionThreadReportHasViolations = !!transactionReport && shouldShowViolations(transactionReport, transactionViolations); + } + return ( + doesTransactionThreadReportHasViolations || + doesReportHaveViolations || + Object.values(allReportErrors).some((error) => error?.[0] !== Localize.translateLocal('iou.error.genericSmartscanFailureMessage')) + ); +} + type ShouldReportBeInOptionListParams = { report: OnyxEntry; currentReportId: string; @@ -8428,6 +8535,10 @@ export { hasMissingInvoiceBankAccount, reasonForReportToBeInOptionList, getReasonAndReportActionThatRequiresAttention, + hasReportErrorsOtherThanFailedReceipt, + shouldShowViolations, + getAllReportErrors, + getAllReportActionsErrorsAndReportActionThatRequiresAttention, }; export type { diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 99811645a6ea..dd62b40a2493 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -105,23 +105,11 @@ function getOrderedReportIDs( if ((Object.values(CONST.REPORT.UNSUPPORTED_TYPE) as string[]).includes(report?.type ?? '')) { return; } - const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`] ?? {}; const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '-1', report?.parentReportActionID ?? '-1'); - const doesReportHaveViolations = OptionsListUtils.shouldShowViolations(report, transactionViolations); + const doesReportHaveViolations = ReportUtils.shouldShowViolations(report, transactionViolations); const isHidden = ReportUtils.getReportNotificationPreference(report) === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; const isFocused = report.reportID === currentReportId; - const allReportErrors = OptionsListUtils.getAllReportErrors(report, reportActions) ?? {}; - const transactionReportActions = ReportActionsUtils.getAllReportActions(report.reportID); - const oneTransactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(report.reportID, transactionReportActions, undefined); - let doesTransactionThreadReportHasViolations = false; - if (oneTransactionThreadReportID) { - const transactionReport = ReportUtils.getReport(oneTransactionThreadReportID); - doesTransactionThreadReportHasViolations = !!transactionReport && OptionsListUtils.shouldShowViolations(transactionReport, transactionViolations); - } - const hasErrorsOtherThanFailedReceipt = - doesTransactionThreadReportHasViolations || - doesReportHaveViolations || - Object.values(allReportErrors).some((error) => error?.[0] !== Localize.translateLocal('iou.error.genericSmartscanFailureMessage')); + const hasErrorsOtherThanFailedReceipt = ReportUtils.hasReportErrorsOtherThanFailedReceipt(report, doesReportHaveViolations, transactionViolations); const isReportInAccessible = report?.errorFields?.notFound; if (ReportUtils.isOneTransactionThread(report.reportID, report.parentReportID ?? '0', parentReportAction)) { return; @@ -235,7 +223,7 @@ function getOrderedReportIDs( } function shouldShowRedBrickRoad(report: Report, reportActions: OnyxEntry, hasViolations: boolean, transactionViolations?: OnyxCollection) { - const hasErrors = Object.keys(OptionsListUtils.getAllReportErrors(report, reportActions)).length !== 0; + const hasErrors = Object.keys(ReportUtils.getAllReportErrors(report, reportActions)).length !== 0; const oneTransactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(report.reportID, ReportActionsUtils.getAllReportActions(report.reportID)); if (oneTransactionThreadReportID) { @@ -291,7 +279,7 @@ function getOptionData({ const result: ReportUtils.OptionData = { text: '', alternateText: undefined, - allReportErrors: OptionsListUtils.getAllReportErrors(report, reportActions), + allReportErrors: ReportUtils.getAllReportErrors(report, reportActions), brickRoadIndicator: null, tooltipText: null, subtitle: undefined, diff --git a/src/libs/WorkspacesSettingsUtils.ts b/src/libs/WorkspacesSettingsUtils.ts index d8cd2ff00828..b8365d9e89d6 100644 --- a/src/libs/WorkspacesSettingsUtils.ts +++ b/src/libs/WorkspacesSettingsUtils.ts @@ -60,7 +60,7 @@ Onyx.connect({ */ const getBrickRoadForPolicy = (report: Report, altReportActions?: OnyxCollection): BrickRoad => { const reportActions = (altReportActions ?? allReportActions)?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`] ?? {}; - const reportErrors = OptionsListUtils.getAllReportErrors(report, reportActions); + const reportErrors = ReportUtils.getAllReportErrors(report, reportActions); const oneTransactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(report.reportID, reportActions); let doesReportContainErrors = Object.keys(reportErrors ?? {}).length !== 0 ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined; From 39bdf3309c65663506eedcab50cc50b58c7b81e2 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Tue, 8 Oct 2024 20:53:29 +0100 Subject: [PATCH 19/95] chore: fix test cases failing for DebugUtils --- src/libs/DebugUtils.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index af682e9cdc6a..6e95dca7114a 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -599,10 +599,6 @@ function getReasonForShowingRowInLHN(report: OnyxEntry): TranslationPath const doesReportHaveViolations = ReportUtils.shouldShowViolations(report, transactionViolations); - if (ReportUtils.hasReportErrorsOtherThanFailedReceipt(report, doesReportHaveViolations, transactionViolations)) { - return `debug.reasonVisibleInLHN.hasRBR`; - } - const reason = ReportUtils.reasonForReportToBeInOptionList({ report, // We can't pass report.reportID because it will cause reason to always be isFocused @@ -615,8 +611,12 @@ function getReasonForShowingRowInLHN(report: OnyxEntry): TranslationPath includeSelfDM: true, }); - // When there's no specific reason, we default to isFocused since the report is only showing because we're viewing it + // When there's no specific reason, we default to isFocused if the report is only showing because we're viewing it + // Otherwise we return hasRBR if the report has errors other that failed receipt if (reason === null || reason === CONST.REPORT_IN_LHN_REASONS.DEFAULT) { + if (ReportUtils.hasReportErrorsOtherThanFailedReceipt(report, doesReportHaveViolations, transactionViolations)) { + return `debug.reasonVisibleInLHN.hasRBR`; + } return 'debug.reasonVisibleInLHN.isFocused'; } From f0dc3c16dfbee852a5eb5d026ece715568b3ba02 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Tue, 8 Oct 2024 20:53:43 +0100 Subject: [PATCH 20/95] chore: remove unused dependency --- src/libs/WorkspacesSettingsUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/WorkspacesSettingsUtils.ts b/src/libs/WorkspacesSettingsUtils.ts index b8365d9e89d6..2be641035be7 100644 --- a/src/libs/WorkspacesSettingsUtils.ts +++ b/src/libs/WorkspacesSettingsUtils.ts @@ -9,7 +9,6 @@ import type {Policy, ReimbursementAccount, Report, ReportAction, ReportActions, import type {PolicyConnectionSyncProgress, Unit} from '@src/types/onyx/Policy'; import {isConnectionInProgress} from './actions/connections'; import * as CurrencyUtils from './CurrencyUtils'; -import * as OptionsListUtils from './OptionsListUtils'; import {hasCustomUnitsError, hasEmployeeListError, hasPolicyError, hasSyncError, hasTaxRateError} from './PolicyUtils'; import * as ReportActionsUtils from './ReportActionsUtils'; import * as ReportConnection from './ReportConnection'; From c7227f824fe82184304f7fffeaa8cda7df510986 Mon Sep 17 00:00:00 2001 From: I Nyoman Jyotisa Date: Wed, 9 Oct 2024 10:51:42 +0800 Subject: [PATCH 21/95] Fix: User is able to send message with max character limit --- src/components/AttachmentModal.tsx | 5 ++++- .../ReportActionCompose/AttachmentPickerWithMenuItems.tsx | 4 ++++ .../home/report/ReportActionCompose/ReportActionCompose.tsx | 2 ++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 8de7f4575e75..0bc233812ca7 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -132,6 +132,8 @@ type AttachmentModalProps = { fallbackSource?: AvatarSource; canEditReceipt?: boolean; + + shouldDisableSendButton?: boolean; }; function AttachmentModal({ @@ -158,6 +160,7 @@ function AttachmentModal({ shouldShowNotFoundPage = false, type = undefined, accountID = undefined, + shouldDisableSendButton = false, }: AttachmentModalProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -589,7 +592,7 @@ function AttachmentModal({ textStyles={[styles.buttonConfirmText]} text={translate('common.send')} onPress={submitAndClose} - isDisabled={isConfirmButtonDisabled} + isDisabled={isConfirmButtonDisabled || shouldDisableSendButton} pressOnEnter /> diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 005ca4df153a..31c5c6fc891f 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -86,6 +86,8 @@ type AttachmentPickerWithMenuItemsProps = { /** The personal details of everyone in the report */ reportParticipantIDs?: number[]; + + shouldDisableAttachmentItem?: boolean; }; /** @@ -110,6 +112,7 @@ function AttachmentPickerWithMenuItems({ onItemSelected, actionButtonRef, raiseIsScrollLikelyLayoutTriggered, + shouldDisableAttachmentItem, }: AttachmentPickerWithMenuItemsProps) { const isFocused = useIsFocused(); const theme = useTheme(); @@ -236,6 +239,7 @@ function AttachmentPickerWithMenuItems({ triggerAttachmentPicker(); }, shouldCallAfterModalHide: true, + disabled: shouldDisableAttachmentItem, }, ]; return ( diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 4a2dc6dc0781..39786e15c621 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -432,6 +432,7 @@ function ReportActionCompose({ onConfirm={addAttachment} onModalShow={() => setIsAttachmentPreviewActive(true)} onModalHide={onAttachmentPreviewClose} + shouldDisableSendButton={hasExceededMaxCommentLength} > {({displayFileInModal}) => ( <> @@ -451,6 +452,7 @@ function ReportActionCompose({ onAddActionPressed={onAddActionPressed} onItemSelected={onItemSelected} actionButtonRef={actionButtonRef} + shouldDisableAttachmentItem={hasExceededMaxCommentLength} /> { From e8fe3c3b7946322702cbb3d6176e42825b859397 Mon Sep 17 00:00:00 2001 From: truph01 Date: Wed, 9 Oct 2024 14:39:37 +0700 Subject: [PATCH 22/95] fix: type --- .../MentionReportRenderer/MentionReportContext.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext.tsx index 9fe1088c9809..2c72033ff682 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext.tsx @@ -2,10 +2,12 @@ import {createContext} from 'react'; type MentionReportContextProps = { currentReportID: string; + exactlyMatch: boolean; }; const MentionReportContext = createContext({ currentReportID: '', + exactlyMatch: false, }); export default MentionReportContext; From 4265a0ce03c89732b5aa9bdd68b766c49b6664ff Mon Sep 17 00:00:00 2001 From: truph01 Date: Wed, 9 Oct 2024 14:44:19 +0700 Subject: [PATCH 23/95] fix: type --- .../MentionReportRenderer/MentionReportContext.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext.tsx index 2c72033ff682..e44d3ef97df6 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext.tsx @@ -2,12 +2,11 @@ import {createContext} from 'react'; type MentionReportContextProps = { currentReportID: string; - exactlyMatch: boolean; + exactlyMatch?: boolean; }; const MentionReportContext = createContext({ currentReportID: '', - exactlyMatch: false, }); export default MentionReportContext; From 33cfb76fd1dfc027fc04ca3729ac6115085694b0 Mon Sep 17 00:00:00 2001 From: Daniel Silva Date: Wed, 9 Oct 2024 14:30:33 -0300 Subject: [PATCH 24/95] fixed web to only record if identified --- src/libs/Fullstory/index.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/libs/Fullstory/index.ts b/src/libs/Fullstory/index.ts index 8419bccabb90..c1cd0e0d37b2 100644 --- a/src/libs/Fullstory/index.ts +++ b/src/libs/Fullstory/index.ts @@ -56,6 +56,11 @@ const FS = { * Initializes the FullStory metadata with the provided metadata information. */ consentAndIdentify: (value: OnyxEntry) => { + // On the first subscribe for UserMetadata, this function will be called. We need + // to confirm that we actually have any value here before proceeding. + if (!value?.accountID) { + return; + } try { Environment.getEnvironment().then((envName: string) => { if (CONST.ENVIRONMENT.PRODUCTION !== envName) { @@ -63,11 +68,9 @@ const FS = { } FS.onReady().then(() => { FS.consent(true); - if (value) { - const localMetadata = value; - localMetadata.environment = envName; - FS.fsIdentify(localMetadata); - } + const localMetadata = value; + localMetadata.environment = envName; + FS.fsIdentify(localMetadata); }); }); } catch (e) { From 4259ad89bee7832ce0f7c6a31179cd2f83be4b56 Mon Sep 17 00:00:00 2001 From: Daniel Silva Date: Wed, 9 Oct 2024 14:39:49 -0300 Subject: [PATCH 25/95] simplifying web --- src/libs/Fullstory/index.ts | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/src/libs/Fullstory/index.ts b/src/libs/Fullstory/index.ts index c1cd0e0d37b2..f5e85ba67cf9 100644 --- a/src/libs/Fullstory/index.ts +++ b/src/libs/Fullstory/index.ts @@ -29,17 +29,11 @@ const FS = { */ onReady: () => new Promise((resolve) => { - Environment.getEnvironment().then((envName: string) => { - if (CONST.ENVIRONMENT.PRODUCTION !== envName) { - return; - } - // Initialised via HEAD snippet - if (!isInitialized()) { - init({orgId: ''}, resolve); - } else { - FullStory('observe', {type: 'start', callback: resolve}); - } - }); + if (!isInitialized()) { + init({orgId: ''}, resolve); + } else { + FullStory('observe', {type: 'start', callback: resolve}); + } }), /** @@ -83,17 +77,11 @@ const FS = { * If the metadata does not contain an email, the user identity is anonymized. * If the metadata contains an accountID, the user identity is defined with it. */ - fsIdentify: (metadata: OnyxEntry) => { - if (!metadata?.accountID) { - // anonymize FullStory user identity metadata - FS.anonymize(); - } else { - // define FullStory user identity - FullStory('setIdentity', { - uid: String(metadata.accountID), - properties: metadata, - }); - } + fsIdentify: (metadata: UserMetadata) => { + FullStory('setIdentity', { + uid: String(metadata.accountID), + properties: metadata, + }); }, /** From 63787c4e5d45efb66fbe677b1d11c00b63f23eb3 Mon Sep 17 00:00:00 2001 From: Daniel Silva Date: Wed, 9 Oct 2024 14:40:13 -0300 Subject: [PATCH 26/95] adjusting mobile to only record if we have a way of identifying our users --- src/libs/Fullstory/index.native.ts | 48 ++++++++++++++---------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/src/libs/Fullstory/index.native.ts b/src/libs/Fullstory/index.native.ts index 7974c35562ca..65233ccf016f 100644 --- a/src/libs/Fullstory/index.native.ts +++ b/src/libs/Fullstory/index.native.ts @@ -15,15 +15,8 @@ const FS = { * Initializes FullStory */ init: () => { - Environment.getEnvironment().then((envName: string) => { - // We only want to start fullstory if the app is running in production - if (envName !== CONST.ENVIRONMENT.PRODUCTION) { - return; - } - FullStory.restart(); - const [session] = useOnyx(ONYXKEYS.USER_METADATA); - FS.fsIdentify(session); - }); + const [session] = useOnyx(ONYXKEYS.USER_METADATA); + FS.consentAndIdentify(session); }, /** @@ -40,10 +33,23 @@ const FS = { * Initializes the FullStory metadata with the provided metadata information. */ consentAndIdentify: (value: OnyxEntry) => { + // On the first subscribe for UserMetadta, this function will be called. We need + // to confirm that we actually have any value here before proceeding. + if (!value?.accountID) { + return; + } try { - // We only use FullStory in production environment - FullStory.consent(true); - FS.fsIdentify(value); + // We only use FullStory in production environment. We need to check this here + // after the init function since this function is also called on updates for + // UserMetadata onyx key. + Environment.getEnvironment().then((envName: string) => { + if (envName !== CONST.ENVIRONMENT.PRODUCTION) { + return; + } + FullStory.restart(); + FullStory.consent(true); + FS.fsIdentify(value, envName); + }); } catch (e) { // error handler } @@ -51,21 +57,11 @@ const FS = { /** * Sets the FullStory user identity based on the provided metadata information. - * If the metadata is null or the email is 'undefined', the user identity is anonymized. - * If the metadata contains an accountID, the user identity is defined with it. */ - fsIdentify: (metadata: OnyxEntry) => { - if (!metadata?.accountID) { - // anonymize FullStory user identity metadata - FullStory.anonymize(); - } else { - Environment.getEnvironment().then((envName: string) => { - // define FullStory user identity - const localMetadata = metadata; - localMetadata.environment = envName; - FullStory.identify(String(localMetadata.accountID), localMetadata); - }); - } + fsIdentify: (metadata: UserMetadata, envName: string) => { + const localMetadata = metadata; + localMetadata.environment = envName; + FullStory.identify(String(localMetadata.accountID), localMetadata); }, }; From 374d964340f7869210cee2cdfd9a25e2fcfb7f34 Mon Sep 17 00:00:00 2001 From: Daniel Silva Date: Wed, 9 Oct 2024 14:55:19 -0300 Subject: [PATCH 27/95] adjusting session to be passed as a parameter to the FS lib --- src/Expensify.tsx | 3 ++- src/libs/Fullstory/index.native.ts | 5 ++--- src/libs/Fullstory/index.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Expensify.tsx b/src/Expensify.tsx index f5d4655c4861..e1fe19b3e6c8 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -149,7 +149,8 @@ function Expensify({ ActiveClientManager.init(); // Initialize Fullstory lib - FS.init(); + const [session] = useOnyx(ONYXKEYS.USER_METADATA); + FS.init(session); // Used for the offline indicator appearing when someone is offline const unsubscribeNetInfo = NetworkConnection.subscribeToNetInfo(); diff --git a/src/libs/Fullstory/index.native.ts b/src/libs/Fullstory/index.native.ts index 65233ccf016f..cd3290b6e529 100644 --- a/src/libs/Fullstory/index.native.ts +++ b/src/libs/Fullstory/index.native.ts @@ -14,9 +14,8 @@ const FS = { /** * Initializes FullStory */ - init: () => { - const [session] = useOnyx(ONYXKEYS.USER_METADATA); - FS.consentAndIdentify(session); + init: (value: OnyxEntry) => { + FS.consentAndIdentify(value); }, /** diff --git a/src/libs/Fullstory/index.ts b/src/libs/Fullstory/index.ts index f5e85ba67cf9..4c2f923b0e79 100644 --- a/src/libs/Fullstory/index.ts +++ b/src/libs/Fullstory/index.ts @@ -87,7 +87,7 @@ const FS = { /** * Init function, created so we're consistent with the native file */ - init: () => {}, + init: (value: OnyxEntry) => {}, }; export default FS; From 0c4dc63e0d0df365104b91bd0e1c2636c8b758fe Mon Sep 17 00:00:00 2001 From: Daniel Silva Date: Wed, 9 Oct 2024 14:59:08 -0300 Subject: [PATCH 28/95] fixing issues with eslint --- src/Expensify.tsx | 4 ++-- src/libs/Fullstory/index.native.ts | 2 -- src/libs/Fullstory/index.ts | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Expensify.tsx b/src/Expensify.tsx index e1fe19b3e6c8..8d3bf6d67071 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -98,6 +98,7 @@ function Expensify({ const [account] = useOnyx(ONYXKEYS.ACCOUNT); const [session] = useOnyx(ONYXKEYS.SESSION); const [lastRoute] = useOnyx(ONYXKEYS.LAST_ROUTE); + const [userMetadata] = useOnyx(ONYXKEYS.USER_METADATA); const [shouldShowRequire2FAModal, setShouldShowRequire2FAModal] = useState(false); useEffect(() => { @@ -149,8 +150,7 @@ function Expensify({ ActiveClientManager.init(); // Initialize Fullstory lib - const [session] = useOnyx(ONYXKEYS.USER_METADATA); - FS.init(session); + FS.init(userMetadata); // Used for the offline indicator appearing when someone is offline const unsubscribeNetInfo = NetworkConnection.subscribeToNetInfo(); diff --git a/src/libs/Fullstory/index.native.ts b/src/libs/Fullstory/index.native.ts index cd3290b6e529..8d97b8d4307e 100644 --- a/src/libs/Fullstory/index.native.ts +++ b/src/libs/Fullstory/index.native.ts @@ -1,9 +1,7 @@ import FullStory, {FSPage} from '@fullstory/react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {useOnyx} from 'react-native-onyx'; import CONST from '@src/CONST'; import * as Environment from '@src/libs/Environment/Environment'; -import ONYXKEYS from '@src/ONYXKEYS'; import type {UserMetadata} from '@src/types/onyx'; /** diff --git a/src/libs/Fullstory/index.ts b/src/libs/Fullstory/index.ts index 4c2f923b0e79..e9a10c4eabfa 100644 --- a/src/libs/Fullstory/index.ts +++ b/src/libs/Fullstory/index.ts @@ -87,7 +87,7 @@ const FS = { /** * Init function, created so we're consistent with the native file */ - init: (value: OnyxEntry) => {}, + init: (_value: OnyxEntry) => {}, }; export default FS; From e8c4fdcbd6b5ba5a57f1a580444371f186a2adbf Mon Sep 17 00:00:00 2001 From: Daniel Silva Date: Wed, 9 Oct 2024 15:01:41 -0300 Subject: [PATCH 29/95] fixing last style issue --- src/libs/Fullstory/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/Fullstory/index.ts b/src/libs/Fullstory/index.ts index e9a10c4eabfa..df65af358a55 100644 --- a/src/libs/Fullstory/index.ts +++ b/src/libs/Fullstory/index.ts @@ -87,6 +87,7 @@ const FS = { /** * Init function, created so we're consistent with the native file */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars init: (_value: OnyxEntry) => {}, }; From 343171766bb5cd87a6533df04a51be28cca97a71 Mon Sep 17 00:00:00 2001 From: Daniel Silva Date: Wed, 9 Oct 2024 15:18:17 -0300 Subject: [PATCH 30/95] removing withOnyx --- src/Expensify.tsx | 50 ++++++++++------------------------------------- 1 file changed, 10 insertions(+), 40 deletions(-) diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 8d3bf6d67071..c4403c559ae9 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -55,7 +55,7 @@ Onyx.registerLogger(({level, message}) => { } }); -type ExpensifyOnyxProps = { +type ExpensifyProps = { /** Whether the app is waiting for the server's response to determine if a room is public */ isCheckingPublicRoom: OnyxEntry; @@ -77,18 +77,7 @@ type ExpensifyOnyxProps = { /** Last visited path in the app */ lastVisitedPath: OnyxEntry; }; - -type ExpensifyProps = ExpensifyOnyxProps; - -function Expensify({ - isCheckingPublicRoom = true, - updateAvailable, - isSidebarLoaded = false, - screenShareRequest, - updateRequired = false, - focusModeNotification = false, - lastVisitedPath, -}: ExpensifyProps) { +function Expensify() { const appStateChangeListener = useRef(null); const [isNavigationReady, setIsNavigationReady] = useState(false); const [isOnyxMigrated, setIsOnyxMigrated] = useState(false); @@ -100,6 +89,13 @@ function Expensify({ const [lastRoute] = useOnyx(ONYXKEYS.LAST_ROUTE); const [userMetadata] = useOnyx(ONYXKEYS.USER_METADATA); const [shouldShowRequire2FAModal, setShouldShowRequire2FAModal] = useState(false); + const [isCheckingPublicRoom] = useOnyx(ONYXKEYS.IS_CHECKING_PUBLIC_ROOM); + const [updateAvailable] = useOnyx(ONYXKEYS.UPDATE_AVAILABLE); + const [updateRequired] = useOnyx(ONYXKEYS.UPDATE_REQUIRED); + const [isSidebarLoaded] = useOnyx(ONYXKEYS.IS_SIDEBAR_LOADED); + const [screenShareRequest] = useOnyx(ONYXKEYS.SCREEN_SHARE_REQUEST); + const [focusModeNotification] = useOnyx(ONYXKEYS.FOCUS_MODE_NOTIFICATION); + const [lastVisitedPath] = useOnyx(ONYXKEYS.LAST_VISITED_PATH); useEffect(() => { if (!account?.needsTwoFactorAuthSetup || account.requiresTwoFactorAuth) { @@ -305,30 +301,4 @@ function Expensify({ Expensify.displayName = 'Expensify'; -export default withOnyx({ - isCheckingPublicRoom: { - key: ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, - initWithStoredValues: false, - }, - updateAvailable: { - key: ONYXKEYS.UPDATE_AVAILABLE, - initWithStoredValues: false, - }, - updateRequired: { - key: ONYXKEYS.UPDATE_REQUIRED, - initWithStoredValues: false, - }, - isSidebarLoaded: { - key: ONYXKEYS.IS_SIDEBAR_LOADED, - }, - screenShareRequest: { - key: ONYXKEYS.SCREEN_SHARE_REQUEST, - }, - focusModeNotification: { - key: ONYXKEYS.FOCUS_MODE_NOTIFICATION, - initWithStoredValues: false, - }, - lastVisitedPath: { - key: ONYXKEYS.LAST_VISITED_PATH, - }, -})(Expensify); +Expensify(); \ No newline at end of file From 117552734179079c3153cf876ff185322e603480 Mon Sep 17 00:00:00 2001 From: Daniel Silva Date: Wed, 9 Oct 2024 15:23:04 -0300 Subject: [PATCH 31/95] exporting component correctly --- src/Expensify.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Expensify.tsx b/src/Expensify.tsx index c4403c559ae9..557ee1a64311 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -301,4 +301,4 @@ function Expensify() { Expensify.displayName = 'Expensify'; -Expensify(); \ No newline at end of file +export default Expensify; \ No newline at end of file From 741c5c189104ca8e3c59fa0753e0058d61f7bea7 Mon Sep 17 00:00:00 2001 From: Daniel Silva Date: Wed, 9 Oct 2024 15:26:55 -0300 Subject: [PATCH 32/95] style --- src/Expensify.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 557ee1a64311..5600c3b54d28 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -3,7 +3,7 @@ import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, use import type {NativeEventSubscription} from 'react-native'; import {AppState, Linking, NativeModules, Platform} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import Onyx, {useOnyx, withOnyx} from 'react-native-onyx'; +import Onyx, {useOnyx} from 'react-native-onyx'; import ConfirmModal from './components/ConfirmModal'; import DeeplinkWrapper from './components/DeeplinkWrapper'; import EmojiPicker from './components/EmojiPicker/EmojiPicker'; @@ -301,4 +301,4 @@ function Expensify() { Expensify.displayName = 'Expensify'; -export default Expensify; \ No newline at end of file +export default Expensify; From 9e481ebe616d3e80fdc0c83a3c8103371f55f397 Mon Sep 17 00:00:00 2001 From: Daniel Silva Date: Wed, 9 Oct 2024 17:26:53 -0300 Subject: [PATCH 33/95] disable linter --- src/Expensify.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 5600c3b54d28..b0c74e82edf9 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -152,6 +152,10 @@ function Expensify() { const unsubscribeNetInfo = NetworkConnection.subscribeToNetInfo(); return unsubscribeNetInfo; + + // This would alerting because of userMetadata. We'll remove the linter rule since + // we don't really need to run this effect again if that value changes. + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Log the platform and config to debug .env issues From 084aaac39aef942c3e6e4e9af47447bf5ca9a52d Mon Sep 17 00:00:00 2001 From: Daniel Silva Date: Wed, 9 Oct 2024 18:18:31 -0300 Subject: [PATCH 34/95] moving value to useEffect --- src/Expensify.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Expensify.tsx b/src/Expensify.tsx index b0c74e82edf9..7822ec16b879 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -145,19 +145,17 @@ function Expensify() { // Initialize this client as being an active client ActiveClientManager.init(); - // Initialize Fullstory lib - FS.init(userMetadata); - // Used for the offline indicator appearing when someone is offline const unsubscribeNetInfo = NetworkConnection.subscribeToNetInfo(); return unsubscribeNetInfo; - - // This would alerting because of userMetadata. We'll remove the linter rule since - // we don't really need to run this effect again if that value changes. - // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useEffect(() => { + // Initialize Fullstory lib + FS.init(userMetadata); + }, [userMetadata]); + // Log the platform and config to debug .env issues useEffect(() => { Log.info('App launched', false, {Platform, CONFIG}); From 2881a4954c584cd701503b3ef146921c4a665d81 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Thu, 10 Oct 2024 07:45:31 +0700 Subject: [PATCH 35/95] migrate IOURequestStepDistanceRate to useOnyx --- .../step/IOURequestStepDistanceRate.tsx | 40 ++++--------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx index 59e1591a23ff..144add6d5795 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx @@ -1,6 +1,6 @@ import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; -import {useOnyx, withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import Text from '@components/Text'; @@ -23,35 +23,23 @@ import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; -type IOURequestStepDistanceRateOnyxProps = { - /** Policy details */ - policy: OnyxEntry; - - /** Collection of categories attached to the policy */ - policyCategories: OnyxEntry; - - /** Collection of tags attached to the policy */ - policyTags: OnyxEntry; +type IOURequestStepDistanceRateProps = WithWritableReportOrNotFoundProps & { + /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ + transaction: OnyxEntry; }; -type IOURequestStepDistanceRateProps = IOURequestStepDistanceRateOnyxProps & - WithWritableReportOrNotFoundProps & { - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - transaction: OnyxEntry; - }; - function IOURequestStepDistanceRate({ - policy: policyReal, report, reportDraft, route: { params: {action, reportID, backTo, transactionID}, }, transaction, - policyTags, - policyCategories, }: IOURequestStepDistanceRateProps) { const [policyDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${IOU.getIOURequestPolicyID(transaction, reportDraft) ?? '-1'}`); + const [policyReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID || '-1'}`); + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID || '-1'}`); + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID || '-1'}`); const policy = policyReal ?? policyDraft; @@ -133,20 +121,8 @@ function IOURequestStepDistanceRate({ IOURequestStepDistanceRate.displayName = 'IOURequestStepDistanceRate'; -const IOURequestStepDistanceRateWithOnyx = withOnyx({ - policy: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '-1'}`, - }, - policyCategories: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`, - }, - policyTags: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, - }, -})(IOURequestStepDistanceRate); - // eslint-disable-next-line rulesdir/no-negated-variables -const IOURequestStepDistanceRateWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepDistanceRateWithOnyx); +const IOURequestStepDistanceRateWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepDistanceRate); // eslint-disable-next-line rulesdir/no-negated-variables const IOURequestStepDistanceRateWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepDistanceRateWithWritableReportOrNotFound); From 62acf3bfa11135b1a13ec45d0cf5d07071053ff6 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Thu, 10 Oct 2024 07:54:25 +0700 Subject: [PATCH 36/95] fix typescript and lint errors --- src/pages/iou/request/step/IOURequestStepDistanceRate.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx index 144add6d5795..0e26306778e9 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx @@ -37,9 +37,11 @@ function IOURequestStepDistanceRate({ transaction, }: IOURequestStepDistanceRateProps) { const [policyDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${IOU.getIOURequestPolicyID(transaction, reportDraft) ?? '-1'}`); + /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ const [policyReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID || '-1'}`); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID || '-1'}`); - const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID || '-1'}`); + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${report?.policyID || '-1'}`); + /* eslint-enable @typescript-eslint/prefer-nullish-coalescing */ const policy = policyReal ?? policyDraft; From 6aed040f608bfe097d3a15b7c9f9bc300fac14d1 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Thu, 10 Oct 2024 08:10:30 +0700 Subject: [PATCH 37/95] update onyx key --- src/libs/Permissions.ts | 2 +- .../iou/request/MoneyRequestParticipantsSelector.tsx | 8 ++++---- src/pages/iou/request/step/IOURequestStepDistanceRate.tsx | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 9fd94dcb86b8..1315e1ee3425 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -5,7 +5,7 @@ import type Beta from '@src/types/onyx/Beta'; import * as Environment from './Environment/Environment'; function canUseAllBetas(betas: OnyxEntry): boolean { - return !!betas?.includes(CONST.BETAS.ALL); + return true; } function canUseDefaultRooms(betas: OnyxEntry): boolean { diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index a2652b8693ee..12d7f1741b7a 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -455,10 +455,10 @@ function MoneyRequestParticipantsSelector({ const onSelectRow = useCallback( (option: Participant) => { - if (option.isPolicyExpenseChat && option.policyID && SubscriptionUtils.shouldRestrictUserBillableActions(option.policyID)) { - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(option.policyID)); - return; - } + // if (option.isPolicyExpenseChat && option.policyID && SubscriptionUtils.shouldRestrictUserBillableActions(option.policyID)) { + // Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(option.policyID)); + // return; + // } if (isIOUSplit) { addParticipantToSelection(option); diff --git a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx index 0e26306778e9..de13ff6cf620 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx @@ -39,7 +39,7 @@ function IOURequestStepDistanceRate({ const [policyDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${IOU.getIOURequestPolicyID(transaction, reportDraft) ?? '-1'}`); /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ const [policyReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID || '-1'}`); - const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID || '-1'}`); + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report?.policyID || '-1'}`); const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${report?.policyID || '-1'}`); /* eslint-enable @typescript-eslint/prefer-nullish-coalescing */ From 46e5519e1ad7d8ee29f4cc976effb23518b1460a Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Thu, 10 Oct 2024 08:18:47 +0700 Subject: [PATCH 38/95] revert unnesscary changes --- src/libs/Permissions.ts | 2 +- .../iou/request/MoneyRequestParticipantsSelector.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 1315e1ee3425..9fd94dcb86b8 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -5,7 +5,7 @@ import type Beta from '@src/types/onyx/Beta'; import * as Environment from './Environment/Environment'; function canUseAllBetas(betas: OnyxEntry): boolean { - return true; + return !!betas?.includes(CONST.BETAS.ALL); } function canUseDefaultRooms(betas: OnyxEntry): boolean { diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index 12d7f1741b7a..a2652b8693ee 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -455,10 +455,10 @@ function MoneyRequestParticipantsSelector({ const onSelectRow = useCallback( (option: Participant) => { - // if (option.isPolicyExpenseChat && option.policyID && SubscriptionUtils.shouldRestrictUserBillableActions(option.policyID)) { - // Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(option.policyID)); - // return; - // } + if (option.isPolicyExpenseChat && option.policyID && SubscriptionUtils.shouldRestrictUserBillableActions(option.policyID)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(option.policyID)); + return; + } if (isIOUSplit) { addParticipantToSelection(option); From 89c0a42397c1f4bbb1d984c8a83e74562c1392c8 Mon Sep 17 00:00:00 2001 From: truph01 Date: Thu, 10 Oct 2024 11:17:58 +0700 Subject: [PATCH 39/95] fx: Display error message in SignUpWelcomeForm --- src/pages/signin/SignUpWelcomeForm.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/pages/signin/SignUpWelcomeForm.tsx b/src/pages/signin/SignUpWelcomeForm.tsx index 68bb7d10796a..6f99ee0dfb0a 100644 --- a/src/pages/signin/SignUpWelcomeForm.tsx +++ b/src/pages/signin/SignUpWelcomeForm.tsx @@ -1,11 +1,14 @@ -import React from 'react'; +import React, {useMemo} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; +import FormHelpMessage from '@components/FormHelpMessage'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; import * as Session from '@userActions/Session'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Account} from '@src/types/onyx'; @@ -23,6 +26,7 @@ function SignUpWelcomeForm({account}: SignUpWelcomeFormProps) { const network = useNetwork(); const styles = useThemeStyles(); const {translate} = useLocalize(); + const serverErrorText = useMemo(() => (account ? ErrorUtils.getLatestErrorMessage(account) : ''), [account]); return ( <> @@ -37,6 +41,12 @@ function SignUpWelcomeForm({account}: SignUpWelcomeFormProps) { pressOnEnter style={[styles.mb2]} /> + {serverErrorText && ( + + )} Session.clearSignInData()} /> From 13807c5f0132562e6569284c0b41261ba3ea8558 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Thu, 10 Oct 2024 09:51:59 +0100 Subject: [PATCH 40/95] fix(debug mode): hasGBR showing instead of hasRBR in debug details page --- src/libs/DebugUtils.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 3b06c8b98540..db84c2b473a7 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -610,12 +610,13 @@ function getReasonForShowingRowInLHN(report: OnyxEntry): TranslationPath includeSelfDM: true, }); + if (ReportUtils.hasReportErrorsOtherThanFailedReceipt(report, doesReportHaveViolations, transactionViolations)) { + return `debug.reasonVisibleInLHN.hasRBR`; + } + // When there's no specific reason, we default to isFocused if the report is only showing because we're viewing it // Otherwise we return hasRBR if the report has errors other that failed receipt if (reason === null || reason === CONST.REPORT_IN_LHN_REASONS.DEFAULT) { - if (ReportUtils.hasReportErrorsOtherThanFailedReceipt(report, doesReportHaveViolations, transactionViolations)) { - return `debug.reasonVisibleInLHN.hasRBR`; - } return 'debug.reasonVisibleInLHN.isFocused'; } From 9358babd497b9c7f7b857bb32c6adc87660122fd Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Thu, 10 Oct 2024 10:18:17 +0100 Subject: [PATCH 41/95] fix(debug mode): not showing specific RBR reasons for report to be in LHN --- src/libs/DebugUtils.ts | 5 ++++- tests/unit/DebugUtilsTest.ts | 5 ++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index db84c2b473a7..3b6c1c77713f 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -610,7 +610,10 @@ function getReasonForShowingRowInLHN(report: OnyxEntry): TranslationPath includeSelfDM: true, }); - if (ReportUtils.hasReportErrorsOtherThanFailedReceipt(report, doesReportHaveViolations, transactionViolations)) { + if ( + !([CONST.REPORT_IN_LHN_REASONS.HAS_ADD_WORKSPACE_ROOM_ERRORS, CONST.REPORT_IN_LHN_REASONS.HAS_IOU_VIOLATIONS] as Array).includes(reason) && + ReportUtils.hasReportErrorsOtherThanFailedReceipt(report, doesReportHaveViolations, transactionViolations) + ) { return `debug.reasonVisibleInLHN.hasRBR`; } diff --git a/tests/unit/DebugUtilsTest.ts b/tests/unit/DebugUtilsTest.ts index 34c2ad2bde73..7ac77fa1b448 100644 --- a/tests/unit/DebugUtilsTest.ts +++ b/tests/unit/DebugUtilsTest.ts @@ -963,8 +963,7 @@ describe('DebugUtils', () => { ); expect(reportAction).toBeUndefined(); }); - // TODO: remove '.failing' once the implementation is fixed - it.failing('returns parentReportAction if it is a transaction thread, the transaction is missing smart scan fields and the report is not settled', async () => { + it('returns undefined if it is a transaction thread, the transaction is missing smart scan fields and the report is not settled', async () => { const MOCK_REPORTS: ReportCollectionDataSet = { [`${ONYXKEYS.COLLECTION.REPORT}1` as const]: { reportID: '1', @@ -1011,7 +1010,7 @@ describe('DebugUtils', () => { MOCK_REPORTS[`${ONYXKEYS.COLLECTION.REPORT}1`] as Report, undefined, ); - expect(reportAction).toBe(1); + expect(reportAction).toBe(undefined); }); describe("Report has missing fields, isn't settled and it's owner is the current user", () => { describe('Report is IOU', () => { From c4f70882f836269bf2fedb6f448e20e87db1315b Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Thu, 10 Oct 2024 12:16:03 +0200 Subject: [PATCH 42/95] fix PR comments --- src/components/HeaderWithBackButton/index.tsx | 2 +- src/components/Search/SearchRouter/SearchRouter.tsx | 3 +-- src/components/Search/SearchRouter/SearchRouterList.tsx | 2 +- src/styles/variables.ts | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index eb04ad5540eb..e1843ee506d5 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -191,7 +191,7 @@ function HeaderWithBackButton({ /> )} {middleContent} - + {children} {shouldShowDownloadButton && ( diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index f49112e86c6c..564e5f0feda0 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -150,8 +150,7 @@ function SearchRouter({onRouterClose}: SearchRouterProps) { ); useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => { - onRouterClose(); - clearUserQuery(); + closeAndClearRouter(); }); const modalWidth = isSmallScreenWidth ? styles.w100 : {width: variables.popoverWidth}; diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index 70abc5c89550..d05588afbe9a 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -167,7 +167,7 @@ function SearchRouterList( // Handle selection of "Recent chat" closeAndClearRouter(); if ('reportID' in item && item?.reportID) { - Navigation.closeAndNavigate(ROUTES.REPORT_WITH_ID.getRoute(item?.reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(item?.reportID)); } else if ('login' in item) { Report.navigateToAndOpenReport(item?.login ? [item.login] : []); } diff --git a/src/styles/variables.ts b/src/styles/variables.ts index 82d443928c7a..cd81e5fcb9c8 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -166,7 +166,7 @@ export default { signInLogoWidthLargeScreenPill: 162, modalContentMaxWidth: 360, listItemHeightNormal: 64, - popoverWidth: 375, + popoverWidth: 512, bankAccountActionPopoverRightSpacing: 32, bankAccountActionPopoverTopSpacing: 14, addPaymentPopoverRightSpacing: 23, From 7e44690b9ffe42ad43d43e3b4d074efe3518ed81 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Thu, 10 Oct 2024 12:32:26 +0200 Subject: [PATCH 43/95] fix paddings --- src/components/Search/SearchRouter/SearchButton.tsx | 9 +++++++-- src/pages/home/HeaderView.tsx | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchButton.tsx b/src/components/Search/SearchRouter/SearchButton.tsx index 2ddc9a8262c2..ae755fbf2e27 100644 --- a/src/components/Search/SearchRouter/SearchButton.tsx +++ b/src/components/Search/SearchRouter/SearchButton.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import {PressableWithoutFeedback} from '@components/Pressable'; @@ -11,7 +12,11 @@ import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import {useSearchRouterContext} from './SearchRouterContext'; -function SearchButton() { +type SearchButtonProps = { + style?: StyleProp; +}; + +function SearchButton({style}: SearchButtonProps) { const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); @@ -20,7 +25,7 @@ function SearchButton() { return ( { Timing.start(CONST.TIMING.SEARCH_ROUTER_OPEN); Performance.markStart(CONST.TIMING.SEARCH_ROUTER_OPEN); diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx index 42483cc3d223..31aaf7cb1f1c 100644 --- a/src/pages/home/HeaderView.tsx +++ b/src/pages/home/HeaderView.tsx @@ -280,7 +280,7 @@ function HeaderView({report, parentReportAction, reportID, onNavigationMenuButto {isTaskReport && !shouldUseNarrowLayout && ReportUtils.isOpenTaskReport(report, parentReportAction) && } {canJoin && !shouldUseNarrowLayout && joinButton} - + Date: Thu, 10 Oct 2024 15:21:25 +0200 Subject: [PATCH 44/95] fix linter --- .../TopBar.tsx | 33 +++---------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/TopBar.tsx b/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/TopBar.tsx index 4684eb9637be..8967486165f8 100644 --- a/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/TopBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/TopBar.tsx @@ -2,32 +2,25 @@ import React from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Breadcrumbs from '@components/Breadcrumbs'; -import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; import {PressableWithoutFeedback} from '@components/Pressable'; import SearchButton from '@components/Search/SearchRouter/SearchButton'; import Text from '@components/Text'; -import Tooltip from '@components/Tooltip'; import WorkspaceSwitcherButton from '@components/WorkspaceSwitcherButton'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import Performance from '@libs/Performance'; import * as SearchUtils from '@libs/SearchUtils'; import SignInButton from '@pages/home/sidebar/SignInButton'; import * as Session from '@userActions/Session'; -import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -type TopBarProps = {breadcrumbLabel: string; activeWorkspaceID?: string; shouldDisplaySearch?: boolean; isCustomSearchQuery?: boolean; shouldDisplaySearchRouter?: boolean}; +type TopBarProps = {breadcrumbLabel: string; activeWorkspaceID?: string; shouldDisplaySearch?: boolean; shouldDisplayCancelSearch?: boolean}; -function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, isCustomSearchQuery = false, shouldDisplaySearchRouter = false}: TopBarProps) { +function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, shouldDisplayCancelSearch = false}: TopBarProps) { const styles = useThemeStyles(); - const theme = useTheme(); const {translate} = useLocalize(); const policy = usePolicy(activeWorkspaceID); const [session] = useOnyx(ONYXKEYS.SESSION, {selector: (sessionValue) => sessionValue && {authTokenType: sessionValue.authTokenType}}); @@ -63,7 +56,7 @@ function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, {displaySignIn && } - {isCustomSearchQuery && ( + {shouldDisplayCancelSearch && ( {translate('common.cancel')} )} - {shouldDisplaySearchRouter && } - {displaySearch && ( - - { - Timing.start(CONST.TIMING.CHAT_FINDER_RENDER); - Performance.markStart(CONST.TIMING.CHAT_FINDER_RENDER); - Navigation.navigate(ROUTES.CHAT_FINDER); - })} - > - - - - )} + {displaySearch && } ); From 2fd6a047a5cc45353e016c42d5fbf72ce252347f Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Thu, 10 Oct 2024 16:05:19 +0100 Subject: [PATCH 45/95] chore: add test cases for hasRBR --- tests/unit/DebugUtilsTest.ts | 125 +++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/tests/unit/DebugUtilsTest.ts b/tests/unit/DebugUtilsTest.ts index 7ac77fa1b448..a87648d08eec 100644 --- a/tests/unit/DebugUtilsTest.ts +++ b/tests/unit/DebugUtilsTest.ts @@ -783,6 +783,131 @@ describe('DebugUtils', () => { const reason = DebugUtils.getReasonForShowingRowInLHN(baseReport); expect(reason).toBe('debug.reasonVisibleInLHN.isFocused'); }); + it('returns correct reason when report has one transaction thread with violations', async () => { + const MOCK_TRANSACTION_REPORT: Report = { + reportID: '1', + ownerAccountID: 12345, + type: CONST.REPORT.TYPE.EXPENSE, + }; + const MOCK_REPORTS: ReportCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.REPORT}1` as const]: MOCK_TRANSACTION_REPORT, + [`${ONYXKEYS.COLLECTION.REPORT}2` as const]: { + reportID: '2', + type: CONST.REPORT.TYPE.CHAT, + parentReportID: '1', + parentReportActionID: '1', + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + }, + }; + const MOCK_REPORT_ACTIONS: ReportActionsCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1` as const]: { + // eslint-disable-next-line @typescript-eslint/naming-convention + '1': { + reportActionID: '1', + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + actorAccountID: 12345, + created: '2024-08-08 18:20:44.171', + childReportID: '2', + message: { + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + amount: 10, + currency: CONST.CURRENCY.USD, + IOUReportID: '1', + text: 'Vacation expense', + IOUTransactionID: '1', + }, + }, + }, + }; + await Onyx.multiSet({ + ...MOCK_REPORTS, + ...MOCK_REPORT_ACTIONS, + [ONYXKEYS.SESSION]: { + accountID: 12345, + }, + [`${ONYXKEYS.COLLECTION.TRANSACTION}1` as const]: { + transactionID: '1', + amount: 10, + modifiedAmount: 10, + reportID: '1', + }, + [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}1` as const]: [ + { + type: CONST.VIOLATION_TYPES.VIOLATION, + name: CONST.VIOLATIONS.MISSING_CATEGORY, + }, + ], + }); + const reason = DebugUtils.getReasonForShowingRowInLHN(MOCK_TRANSACTION_REPORT); + expect(reason).toBe('debug.reasonVisibleInLHN.hasRBR'); + }); + it('returns correct reason when report has violations', async () => { + const MOCK_EXPENSE_REPORT: Report = { + reportID: '1', + chatReportID: '2', + parentReportID: '2', + parentReportActionID: '1', + ownerAccountID: 12345, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + type: CONST.REPORT.TYPE.EXPENSE, + }; + const MOCK_REPORTS: ReportCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.REPORT}1` as const]: MOCK_EXPENSE_REPORT, + [`${ONYXKEYS.COLLECTION.REPORT}2` as const]: { + reportID: '2', + chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + }, + }; + const MOCK_REPORT_ACTIONS: ReportActionsCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2` as const]: { + // eslint-disable-next-line @typescript-eslint/naming-convention + '1': { + reportActionID: '1', + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + actorAccountID: 12345, + created: '2024-08-08 18:20:44.171', + message: { + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + amount: 10, + currency: CONST.CURRENCY.USD, + IOUReportID: '1', + text: 'Vacation expense', + IOUTransactionID: '1', + }, + }, + }, + }; + await Onyx.multiSet({ + ...MOCK_REPORTS, + ...MOCK_REPORT_ACTIONS, + [ONYXKEYS.SESSION]: { + accountID: 12345, + }, + [`${ONYXKEYS.COLLECTION.TRANSACTION}1` as const]: { + transactionID: '1', + amount: 10, + modifiedAmount: 10, + reportID: '1', + }, + [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}1` as const]: [ + { + type: CONST.VIOLATION_TYPES.VIOLATION, + name: CONST.VIOLATIONS.MISSING_CATEGORY, + }, + ], + }); + const reason = DebugUtils.getReasonForShowingRowInLHN(MOCK_EXPENSE_REPORT); + expect(reason).toBe('debug.reasonVisibleInLHN.hasRBR'); + }); + it('returns correct reason when report has errors', () => { + const reason = DebugUtils.getReasonForShowingRowInLHN({ + ...baseReport, + errors: { + error: 'Something went wrong', + }, + }); + expect(reason).toBe('debug.reasonVisibleInLHN.hasRBR'); + }); }); describe('getReasonAndReportActionForGBRInLHNRow', () => { beforeAll(() => { From 7f557e0a0b225fda6979efaa3cae6ac2189bfa85 Mon Sep 17 00:00:00 2001 From: I Nyoman Jyotisa Date: Fri, 11 Oct 2024 08:31:41 +0800 Subject: [PATCH 46/95] update the selected currency on rate modal when workspace currency changes --- src/components/MoneyRequestConfirmationList.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 1faef0c6b44c..635a31e06623 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -292,7 +292,9 @@ function MoneyRequestConfirmationList({ const distance = TransactionUtils.getDistanceInMeters(transaction, unit); const prevDistance = usePrevious(distance); - const shouldCalculateDistanceAmount = isDistanceRequest && (iouAmount === 0 || prevRate !== rate || prevDistance !== distance); + const prevCurrency = usePrevious(currency); + + const shouldCalculateDistanceAmount = isDistanceRequest && (iouAmount === 0 || prevRate !== rate || prevDistance !== distance || prevCurrency !== currency); const hasRoute = TransactionUtils.hasRoute(transaction, isDistanceRequest); const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate) && !isMovingTransactionFromTrackExpense; From 90e18c518ab36f348eae3c735bb0188d936452cd Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Fri, 11 Oct 2024 13:36:56 +0200 Subject: [PATCH 47/95] fix PR comments --- src/components/Search/SearchRouter/SearchRouter.tsx | 2 +- src/styles/variables.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 564e5f0feda0..5c1f4e27c93d 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -153,7 +153,7 @@ function SearchRouter({onRouterClose}: SearchRouterProps) { closeAndClearRouter(); }); - const modalWidth = isSmallScreenWidth ? styles.w100 : {width: variables.popoverWidth}; + const modalWidth = isSmallScreenWidth ? styles.w100 : {width: variables.searchRouterPopoverWidth}; return ( Date: Fri, 11 Oct 2024 21:29:02 +0800 Subject: [PATCH 48/95] minor fix --- src/components/MoneyRequestConfirmationList.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 635a31e06623..f14ee940e329 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -256,6 +256,7 @@ function MoneyRequestConfirmationList({ const prevRate = usePrevious(rate); const currency = (mileageRate as MileageRate)?.currency ?? policyCurrency; + const prevCurrency = usePrevious(currency); // A flag for showing the categories field const shouldShowCategories = (isPolicyExpenseChat || isTypeInvoice) && (!!iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); @@ -292,8 +293,6 @@ function MoneyRequestConfirmationList({ const distance = TransactionUtils.getDistanceInMeters(transaction, unit); const prevDistance = usePrevious(distance); - const prevCurrency = usePrevious(currency); - const shouldCalculateDistanceAmount = isDistanceRequest && (iouAmount === 0 || prevRate !== rate || prevDistance !== distance || prevCurrency !== currency); const hasRoute = TransactionUtils.hasRoute(transaction, isDistanceRequest); From de2359ffba4eba811c3f4c19556c5828d1eab4b5 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Fri, 11 Oct 2024 18:33:46 +0100 Subject: [PATCH 49/95] fix(debug mode): is temporary focused reason shown instead of has RBR message --- src/libs/DebugUtils.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 3b6c1c77713f..97469f5f6c74 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -7,6 +7,7 @@ import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Beta, Policy, Report, ReportAction, ReportActions, TransactionViolation} from '@src/types/onyx'; import * as ReportUtils from './ReportUtils'; +import SidebarUtils from './SidebarUtils'; class NumberError extends SyntaxError { constructor() { @@ -127,6 +128,15 @@ Onyx.connect({ }, }); +let reportActions: OnyxCollection; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (value) => { + reportActions = value; + }, +}); + let transactionViolations: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, @@ -612,7 +622,7 @@ function getReasonForShowingRowInLHN(report: OnyxEntry): TranslationPath if ( !([CONST.REPORT_IN_LHN_REASONS.HAS_ADD_WORKSPACE_ROOM_ERRORS, CONST.REPORT_IN_LHN_REASONS.HAS_IOU_VIOLATIONS] as Array).includes(reason) && - ReportUtils.hasReportErrorsOtherThanFailedReceipt(report, doesReportHaveViolations, transactionViolations) + SidebarUtils.shouldShowRedBrickRoad(report, reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`], doesReportHaveViolations, transactionViolations) ) { return `debug.reasonVisibleInLHN.hasRBR`; } From 6a8b6fd22ecf49d14013c7dff83c3dcba8c2c2a2 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Fri, 11 Oct 2024 18:38:15 +0100 Subject: [PATCH 50/95] chore: fix eslint errors --- src/libs/DebugUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 97469f5f6c74..261d2db208d9 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -128,12 +128,12 @@ Onyx.connect({ }, }); -let reportActions: OnyxCollection; +let reportActionsCollection: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, waitForCollectionCallback: true, callback: (value) => { - reportActions = value; + reportActionsCollection = value; }, }); @@ -622,7 +622,7 @@ function getReasonForShowingRowInLHN(report: OnyxEntry): TranslationPath if ( !([CONST.REPORT_IN_LHN_REASONS.HAS_ADD_WORKSPACE_ROOM_ERRORS, CONST.REPORT_IN_LHN_REASONS.HAS_IOU_VIOLATIONS] as Array).includes(reason) && - SidebarUtils.shouldShowRedBrickRoad(report, reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`], doesReportHaveViolations, transactionViolations) + SidebarUtils.shouldShowRedBrickRoad(report, reportActionsCollection?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`], doesReportHaveViolations, transactionViolations) ) { return `debug.reasonVisibleInLHN.hasRBR`; } From 96f59359c35f3d5f2e4cf7f237ee64328e76db20 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Mon, 14 Oct 2024 10:34:46 +0100 Subject: [PATCH 51/95] refactor: apply suggestions --- src/libs/DebugUtils.ts | 17 ++--------------- src/pages/Debug/Report/DebugReportPage.tsx | 6 +++--- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 261d2db208d9..fee35c50e0dc 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -7,7 +7,6 @@ import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Beta, Policy, Report, ReportAction, ReportActions, TransactionViolation} from '@src/types/onyx'; import * as ReportUtils from './ReportUtils'; -import SidebarUtils from './SidebarUtils'; class NumberError extends SyntaxError { constructor() { @@ -128,15 +127,6 @@ Onyx.connect({ }, }); -let reportActionsCollection: OnyxCollection; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - waitForCollectionCallback: true, - callback: (value) => { - reportActionsCollection = value; - }, -}); - let transactionViolations: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, @@ -601,7 +591,7 @@ function validateReportActionJSON(json: string) { /** * Gets the reason for showing LHN row */ -function getReasonForShowingRowInLHN(report: OnyxEntry): TranslationPaths | null { +function getReasonForShowingRowInLHN(report: OnyxEntry, hasRBR: boolean): TranslationPaths | null { if (!report) { return null; } @@ -620,10 +610,7 @@ function getReasonForShowingRowInLHN(report: OnyxEntry): TranslationPath includeSelfDM: true, }); - if ( - !([CONST.REPORT_IN_LHN_REASONS.HAS_ADD_WORKSPACE_ROOM_ERRORS, CONST.REPORT_IN_LHN_REASONS.HAS_IOU_VIOLATIONS] as Array).includes(reason) && - SidebarUtils.shouldShowRedBrickRoad(report, reportActionsCollection?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`], doesReportHaveViolations, transactionViolations) - ) { + if (!([CONST.REPORT_IN_LHN_REASONS.HAS_ADD_WORKSPACE_ROOM_ERRORS, CONST.REPORT_IN_LHN_REASONS.HAS_IOU_VIOLATIONS] as Array).includes(reason) && hasRBR) { return `debug.reasonVisibleInLHN.hasRBR`; } diff --git a/src/pages/Debug/Report/DebugReportPage.tsx b/src/pages/Debug/Report/DebugReportPage.tsx index 530b4b5f4aec..28f4ddf3dc34 100644 --- a/src/pages/Debug/Report/DebugReportPage.tsx +++ b/src/pages/Debug/Report/DebugReportPage.tsx @@ -59,12 +59,12 @@ function DebugReportPage({ return []; } - const reasonLHN = DebugUtils.getReasonForShowingRowInLHN(report); - const {reason: reasonGBR, reportAction: reportActionGBR} = DebugUtils.getReasonAndReportActionForGBRInLHNRow(report) ?? {}; - const reportActionRBR = DebugUtils.getRBRReportAction(report, reportActions); const shouldDisplayViolations = ReportUtils.shouldDisplayTransactionThreadViolations(report, transactionViolations, parentReportAction); const shouldDisplayReportViolations = ReportUtils.isReportOwner(report) && ReportUtils.hasReportViolations(reportID); const hasRBR = SidebarUtils.shouldShowRedBrickRoad(report, reportActions, !!shouldDisplayViolations || shouldDisplayReportViolations, transactionViolations); + const reasonLHN = DebugUtils.getReasonForShowingRowInLHN(report, hasRBR); + const {reason: reasonGBR, reportAction: reportActionGBR} = DebugUtils.getReasonAndReportActionForGBRInLHNRow(report) ?? {}; + const reportActionRBR = DebugUtils.getRBRReportAction(report, reportActions); const hasGBR = !hasRBR && !!reasonGBR; return [ From 3a125b2ddee845df9dc58133dd6bc05b91edda56 Mon Sep 17 00:00:00 2001 From: truph01 Date: Mon, 14 Oct 2024 16:44:10 +0700 Subject: [PATCH 52/95] fix: lint --- src/pages/signin/SignUpWelcomeForm.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/signin/SignUpWelcomeForm.tsx b/src/pages/signin/SignUpWelcomeForm.tsx index 6f99ee0dfb0a..25b48a32fe53 100644 --- a/src/pages/signin/SignUpWelcomeForm.tsx +++ b/src/pages/signin/SignUpWelcomeForm.tsx @@ -2,7 +2,6 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; -import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import FormHelpMessage from '@components/FormHelpMessage'; import useLocalize from '@hooks/useLocalize'; From 212f461eeba8e82d7256a7c15b133f7b1b199f37 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Mon, 14 Oct 2024 10:52:04 +0100 Subject: [PATCH 53/95] fix: debug utils hasRBR tests --- src/libs/DebugUtils.ts | 2 +- tests/unit/DebugUtilsTest.ts | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index fee35c50e0dc..e7ad63467781 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -591,7 +591,7 @@ function validateReportActionJSON(json: string) { /** * Gets the reason for showing LHN row */ -function getReasonForShowingRowInLHN(report: OnyxEntry, hasRBR: boolean): TranslationPaths | null { +function getReasonForShowingRowInLHN(report: OnyxEntry, hasRBR = false): TranslationPaths | null { if (!report) { return null; } diff --git a/tests/unit/DebugUtilsTest.ts b/tests/unit/DebugUtilsTest.ts index a87648d08eec..fa44b8972cf3 100644 --- a/tests/unit/DebugUtilsTest.ts +++ b/tests/unit/DebugUtilsTest.ts @@ -838,7 +838,7 @@ describe('DebugUtils', () => { }, ], }); - const reason = DebugUtils.getReasonForShowingRowInLHN(MOCK_TRANSACTION_REPORT); + const reason = DebugUtils.getReasonForShowingRowInLHN(MOCK_TRANSACTION_REPORT, true); expect(reason).toBe('debug.reasonVisibleInLHN.hasRBR'); }); it('returns correct reason when report has violations', async () => { @@ -896,16 +896,19 @@ describe('DebugUtils', () => { }, ], }); - const reason = DebugUtils.getReasonForShowingRowInLHN(MOCK_EXPENSE_REPORT); + const reason = DebugUtils.getReasonForShowingRowInLHN(MOCK_EXPENSE_REPORT, true); expect(reason).toBe('debug.reasonVisibleInLHN.hasRBR'); }); it('returns correct reason when report has errors', () => { - const reason = DebugUtils.getReasonForShowingRowInLHN({ - ...baseReport, - errors: { - error: 'Something went wrong', + const reason = DebugUtils.getReasonForShowingRowInLHN( + { + ...baseReport, + errors: { + error: 'Something went wrong', + }, }, - }); + true, + ); expect(reason).toBe('debug.reasonVisibleInLHN.hasRBR'); }); }); From 2f90dd174d9154df9da609e5d102d4d005672659 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 14 Oct 2024 14:15:33 +0300 Subject: [PATCH 54/95] Add hasBrokenConnectionViolation and getBrokenConnectionDescription functions --- src/languages/en.ts | 4 +++ src/languages/es.ts | 4 +++ src/libs/TransactionUtils/index.ts | 52 ++++++++++++++++++++++++++++-- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 7f3a5a365142..2fcb51922583 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4688,6 +4688,10 @@ const translations = { return ''; }, + brokenConnection530Error: 'Receipt pending due to broken bank connection.', + adminBrokenConnectionError: 'Receipt pending due to broken bank connection. Please resolve in ', + memberBrokenConnectionError: 'Receipt pending due to broken bank connection. Please ask a workspace admin to resolve.', + markAsCashToIgnore: 'Mark as cash to ignore and request payment.', smartscanFailed: 'Receipt scanning failed. Enter details manually.', someTagLevelsRequired: ({tagName}: ViolationsTagOutOfPolicyParams = {}) => `Missing ${tagName ?? 'Tag'}`, tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams = {}) => `${tagName ?? 'Tag'} no longer valid`, diff --git a/src/languages/es.ts b/src/languages/es.ts index d4615b26a255..c483f26b8261 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5199,6 +5199,10 @@ const translations = { } return ''; }, + brokenConnection530Error: 'Recibo pendiente debido a una conexión bancaria rota.', + adminBrokenConnectionError: 'Recibo pendiente debido a una conexión bancaria rota. Por favor, resuélvelo en ', + memberBrokenConnectionError: 'Recibo pendiente debido a una conexión bancaria rota. Por favor, pide a un administrador del espacio de trabajo que lo resuelva.', + markAsCashToIgnore: 'Márcalo como efectivo para ignorar y solicitar el pago.', smartscanFailed: 'No se pudo escanear el recibo. Introduce los datos manualmente', someTagLevelsRequired: ({tagName}: ViolationsTagOutOfPolicyParams = {}) => `Falta ${tagName ?? 'Tag'}`, tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams = {}) => `La etiqueta ${tagName ? `${tagName} ` : ''}ya no es válida`, diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 11516af54b28..96ca91158eda 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -20,7 +20,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import type {IOURequestType} from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Beta, OnyxInputOrEntry, Policy, RecentWaypoint, ReviewDuplicates, TaxRate, TaxRates, Transaction, TransactionViolation, TransactionViolations} from '@src/types/onyx'; +import type {Beta, OnyxInputOrEntry, Policy, RecentWaypoint, Report, ReviewDuplicates, TaxRate, TaxRates, Transaction, TransactionViolation, TransactionViolations} from '@src/types/onyx'; import type {Comment, Receipt, TransactionChanges, TransactionPendingFieldsKey, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -583,7 +583,53 @@ function getTransactionViolations(transactionID: string, transactionViolations: * Check if there is pending rter violation in transactionViolations. */ function hasPendingRTERViolation(transactionViolations?: TransactionViolations | null): boolean { - return !!transactionViolations?.some((transactionViolation: TransactionViolation) => transactionViolation.name === CONST.VIOLATIONS.RTER && transactionViolation.data?.pendingPattern); + return !!transactionViolations?.some( + (transactionViolation: TransactionViolation) => + transactionViolation.name === CONST.VIOLATIONS.RTER && + transactionViolation.data?.pendingPattern && + transactionViolation.data?.rterType !== CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION && + transactionViolation.data?.rterType !== CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530, + ); +} + +/** + * Check if there is broken connection violation. + */ +function hasBrokenConnectionViolation(transactionID: string): boolean { + const violations = getTransactionViolations(transactionID, allTransactionViolations); + return !!violations?.find( + (violation) => + violation.name === CONST.VIOLATIONS.RTER && + (violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION || violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530), + ); +} + +/** + * Returns broken connection description. + */ +function getBrokenConnectionDescription(transactionID: string, report: OnyxEntry, policy: OnyxEntry) { + const violations = getTransactionViolations(transactionID, allTransactionViolations); + const brokenConnection530Error = violations?.find((violation) => violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530); + const brokenConnectionError = violations?.find((violation) => violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION); + const isPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); + + if (!brokenConnection530Error && !brokenConnectionError) { + return ''; + } + + if (brokenConnection530Error) { + return Localize.translateLocal('violations.brokenConnection530Error'); + } + + if (isPolicyAdmin) { + return `${Localize.translateLocal('violations.adminBrokenConnectionError')}`; + } + + if (ReportUtils.isReportApproved(report) || ReportUtils.isReportManuallyReimbursed(report) || (ReportUtils.isProcessingReport(report) && !PolicyUtils.isInstantSubmitEnabled(policy))) { + return Localize.translateLocal('violations.memberBrokenConnectionError'); + } + + return `${Localize.translateLocal('violations.memberBrokenConnectionError')}${Localize.translateLocal('violations.markAsCashToIgnore')}`; } /** @@ -1141,6 +1187,8 @@ export { getRecentTransactions, hasReservationList, hasViolation, + hasBrokenConnectionViolation, + getBrokenConnectionDescription, hasNoticeTypeViolation, hasWarningTypeViolation, hasModifiedAmountOrDateViolation, From 1d0811f96a28e507bc9b48ed468251e538ede386 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Mon, 14 Oct 2024 15:29:08 +0200 Subject: [PATCH 55/95] fix style bugs --- src/components/Search/SearchRouter/SearchRouterInput.tsx | 2 +- src/components/Search/SearchRouter/SearchRouterList.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouterInput.tsx b/src/components/Search/SearchRouter/SearchRouterInput.tsx index e28775c3686e..ee236494fcc6 100644 --- a/src/components/Search/SearchRouter/SearchRouterInput.tsx +++ b/src/components/Search/SearchRouter/SearchRouterInput.tsx @@ -88,7 +88,7 @@ function SearchRouterInput({ disabled={disabled} onSubmitEditing={onSubmit} shouldUseDisabledStyles={false} - textInputContainerStyles={styles.borderNone} + textInputContainerStyles={[styles.borderNone, styles.pb0]} inputStyle={[styles.searchInputStyle, inputWidth, styles.pl3, styles.pr3]} onFocus={() => { setIsFocused(true); diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index d05588afbe9a..8485d0c2eb50 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -80,7 +80,6 @@ function SearchRouterItem(props: UserListItemProps | SearchQueryList return ( @@ -145,7 +144,7 @@ function SearchRouterList( sections.push({title: translate('search.recentSearches'), data: recentSearchesData}); } - const styledRecentReports = recentReports.map((item) => ({...item, pressableStyle: styles.br2})); + const styledRecentReports = recentReports.map((item) => ({...item, pressableStyle: styles.br2, wrapperStyle: [styles.pr3, styles.pl3]})); sections.push({title: translate('search.recentChats'), data: styledRecentReports}); const onSelectRow = useCallback( @@ -182,6 +181,7 @@ function SearchRouterList( ListItem={SearchRouterItem} containerStyle={[styles.mh100]} sectionListStyle={[isSmallScreenWidth ? styles.ph5 : styles.ph2, styles.pb2]} + listItemWrapperStyle={[styles.pr3, styles.pl3]} onLayout={setPerformanceTimersEnd} ref={ref} showScrollIndicator={!isSmallScreenWidth} From 935067ed56dd45bd24edd3ce834edc2dd112bc7d Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Mon, 14 Oct 2024 16:03:53 +0200 Subject: [PATCH 56/95] add offline message --- src/CONST.ts | 2 +- .../Search/SearchRouter/SearchButton.tsx | 4 +- .../Search/SearchRouter/SearchRouter.tsx | 9 ++- .../Search/SearchRouter/SearchRouterInput.tsx | 77 ++++++++++++------- .../Search/SearchRouter/SearchRouterList.tsx | 5 +- .../E2E/tests/openSearchRouterTest.e2e.ts | 2 +- 6 files changed, 64 insertions(+), 35 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index ab6d057393e2..51da86fbcb7f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1102,7 +1102,7 @@ const CONST = { }, TIMING: { CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION: 'calc_most_recent_last_modified_action', - SEARCH_ROUTER_OPEN: 'search_router_render', + SEARCH_ROUTER_RENDER: 'search_router_render', CHAT_RENDER: 'chat_render', OPEN_REPORT: 'open_report', HOMEPAGE_INITIAL_RENDER: 'homepage_initial_render', diff --git a/src/components/Search/SearchRouter/SearchButton.tsx b/src/components/Search/SearchRouter/SearchButton.tsx index ae755fbf2e27..7ed22ec8162f 100644 --- a/src/components/Search/SearchRouter/SearchButton.tsx +++ b/src/components/Search/SearchRouter/SearchButton.tsx @@ -27,8 +27,8 @@ function SearchButton({style}: SearchButtonProps) { accessibilityLabel={translate('common.search')} style={[styles.flexRow, styles.touchableButtonImage, style]} onPress={Session.checkIfActionIsAllowed(() => { - Timing.start(CONST.TIMING.SEARCH_ROUTER_OPEN); - Performance.markStart(CONST.TIMING.SEARCH_ROUTER_OPEN); + Timing.start(CONST.TIMING.SEARCH_ROUTER_RENDER); + Performance.markStart(CONST.TIMING.SEARCH_ROUTER_RENDER); openSearchRouter(); })} diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 1e6d8ff16ba8..e8d613e0b6f1 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -3,6 +3,7 @@ import debounce from 'lodash/debounce'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import FormHelpMessage from '@components/FormHelpMessage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {useOptionsList} from '@components/OptionListContextProvider'; import type {SearchQueryJSON} from '@components/Search/types'; @@ -10,6 +11,7 @@ import type {SelectionListHandle} from '@components/SelectionList/types'; import useDebouncedState from '@hooks/useDebouncedState'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import Log from '@libs/Log'; @@ -176,8 +178,13 @@ function SearchRouter({onRouterClose}: SearchRouterProps) { setValue={setTextInputValue} isFullWidth={isSmallScreenWidth} updateSearch={onSearchChange} + onSubmit={() => { + onSearchSubmit(SearchUtils.buildSearchQueryJSON(textInputValue)); + }} routerListRef={listRef} - wrapperStyle={[isSmallScreenWidth ? styles.mv3 : styles.mv2, isSmallScreenWidth ? styles.mh5 : styles.mh2, styles.border]} + shouldShowOfflineMessage + wrapperStyle={[styles.border, styles.alignItemsCenter]} + outerWrapperStyle={[isSmallScreenWidth ? styles.mv3 : styles.mv2, isSmallScreenWidth ? styles.mh5 : styles.mh2]} wrapperFocusedStyle={[styles.borderColorFocus]} isSearchingForReports={isSearchingForReports} /> diff --git a/src/components/Search/SearchRouter/SearchRouterInput.tsx b/src/components/Search/SearchRouter/SearchRouterInput.tsx index ee236494fcc6..84ebf2edc0b7 100644 --- a/src/components/Search/SearchRouter/SearchRouterInput.tsx +++ b/src/components/Search/SearchRouter/SearchRouterInput.tsx @@ -2,9 +2,11 @@ import React, {useState} from 'react'; import type {ReactNode, RefObject} from 'react'; import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; +import FormHelpMessage from '@components/FormHelpMessage'; import type {SelectionListHandle} from '@components/SelectionList/types'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -31,6 +33,9 @@ type SearchRouterInputProps = { /** Whether the input is disabled */ disabled?: boolean; + /** Whether the offline message should be shown */ + shouldShowOfflineMessage?: boolean; + /** Whether the input should be focused */ autoFocus?: boolean; @@ -40,6 +45,9 @@ type SearchRouterInputProps = { /** Any additional styles to apply when input is focused */ wrapperFocusedStyle?: StyleProp; + /** Any additional styles to apply to text input along with FormHelperMessage */ + outerWrapperStyle?: StyleProp; + /** Component to be displayed on the right */ rightComponent?: ReactNode; @@ -55,15 +63,19 @@ function SearchRouterInput({ routerListRef, isFullWidth, disabled = false, + shouldShowOfflineMessage = false, autoFocus = true, wrapperStyle, wrapperFocusedStyle, + outerWrapperStyle, rightComponent, isSearchingForReports, }: SearchRouterInputProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [isFocused, setIsFocused] = useState(false); + const {isOffline} = useNetwork(); + const offlineMessage: string = isOffline && shouldShowOfflineMessage ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; const onChangeText = (text: string) => { setValue(text); @@ -73,35 +85,44 @@ function SearchRouterInput({ const inputWidth = isFullWidth ? styles.w100 : {width: variables.popoverWidth}; return ( - - - { - setIsFocused(true); - routerListRef?.current?.updateExternalTextInputFocus(true); - }} - onBlur={() => { - setIsFocused(false); - routerListRef?.current?.updateExternalTextInputFocus(false); - }} - isLoading={!!isSearchingForReports} - /> + + + + { + setIsFocused(true); + routerListRef?.current?.updateExternalTextInputFocus(true); + }} + onBlur={() => { + setIsFocused(false); + routerListRef?.current?.updateExternalTextInputFocus(false); + }} + isLoading={!!isSearchingForReports} + /> + + {rightComponent && {rightComponent}} - {rightComponent && {rightComponent}} + ); } diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index 8485d0c2eb50..84c300bfc895 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -51,8 +51,8 @@ type SearchRouterListProps = { }; const setPerformanceTimersEnd = () => { - Timing.end(CONST.TIMING.SEARCH_ROUTER_OPEN); - Performance.markEnd(CONST.TIMING.SEARCH_ROUTER_OPEN); + Timing.end(CONST.TIMING.SEARCH_ROUTER_RENDER); + Performance.markEnd(CONST.TIMING.SEARCH_ROUTER_RENDER); }; function isSearchQueryItem(item: OptionData | SearchQueryItem): item is SearchQueryItem { @@ -186,6 +186,7 @@ function SearchRouterList( ref={ref} showScrollIndicator={!isSmallScreenWidth} sectionTitleStyles={styles.mhn2} + shouldSingleExecuteRowSelect /> ); } diff --git a/src/libs/E2E/tests/openSearchRouterTest.e2e.ts b/src/libs/E2E/tests/openSearchRouterTest.e2e.ts index 6c4646b09fbf..840af5acc2c9 100644 --- a/src/libs/E2E/tests/openSearchRouterTest.e2e.ts +++ b/src/libs/E2E/tests/openSearchRouterTest.e2e.ts @@ -32,7 +32,7 @@ const test = () => { Performance.subscribeToMeasurements((entry) => { console.debug(`[E2E] Entry: ${JSON.stringify(entry)}`); - if (entry.name === CONST.TIMING.SEARCH_ROUTER_OPEN) { + if (entry.name === CONST.TIMING.SEARCH_ROUTER_RENDER) { E2EClient.submitTestResults({ branch: Config.E2E_BRANCH, name: 'Open Search Router TTI', From a6a74c5562f34291ebd5361297dd3048950966c4 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Mon, 14 Oct 2024 16:33:04 +0200 Subject: [PATCH 57/95] fix linter --- src/components/Search/SearchRouter/SearchRouter.tsx | 2 -- src/components/Search/SearchRouter/SearchRouterInput.tsx | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index e8d613e0b6f1..a957806ee4f7 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -3,7 +3,6 @@ import debounce from 'lodash/debounce'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; -import FormHelpMessage from '@components/FormHelpMessage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {useOptionsList} from '@components/OptionListContextProvider'; import type {SearchQueryJSON} from '@components/Search/types'; @@ -11,7 +10,6 @@ import type {SelectionListHandle} from '@components/SelectionList/types'; import useDebouncedState from '@hooks/useDebouncedState'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import Log from '@libs/Log'; diff --git a/src/components/Search/SearchRouter/SearchRouterInput.tsx b/src/components/Search/SearchRouter/SearchRouterInput.tsx index 84ebf2edc0b7..ef6963152c42 100644 --- a/src/components/Search/SearchRouter/SearchRouterInput.tsx +++ b/src/components/Search/SearchRouter/SearchRouterInput.tsx @@ -120,6 +120,7 @@ function SearchRouterInput({ {rightComponent && {rightComponent}} From 72b9cd3afd495b3e8c4d02c8cc29b87354184094 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Mon, 14 Oct 2024 19:40:57 +0200 Subject: [PATCH 58/95] add new strpipe link --- src/CONST.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/CONST.ts b/src/CONST.ts index 2556d0fc4331..7de5c968414d 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -741,6 +741,7 @@ const CONST = { HOW_TO_CONNECT_TO_SAGE_INTACCT: 'https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct#how-to-connect-to-sage-intacct', PRICING: `https://www.expensify.com/pricing`, COMPANY_CARDS_HELP: 'https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Commercial-Card-Feeds', + COMPANY_CARDS_STRIPE_HELP: 'https://dashboard.stripe.com/login?redirect=%2Fexpenses%2Fsettings', COMPANY_CARDS_CONNECT_CREDIT_CARDS_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Commercial-Card-Feeds#what-is-the-difference-between-commercial-card-feeds-and-your-direct-bank-connections', CUSTOM_REPORT_NAME_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/spending-insights/Custom-Templates', @@ -2563,6 +2564,7 @@ const CONST = { AMEX: 'amex', VISA: 'visa', MASTERCARD: 'mastercard', + STRIPE: 'stripe', }, FEED_TYPE: { CUSTOM: 'customFeed', From 8e895ce7a7b3faf5a83449efb6f4f2722770aab2 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Mon, 14 Oct 2024 19:41:12 +0200 Subject: [PATCH 59/95] add new translations --- src/languages/en.ts | 1 + src/languages/es.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index 5aff9beb85e7..6bb8b112d2e0 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3052,6 +3052,7 @@ const translations = { visa: `1. Visit [this help article](${CONST.COMPANY_CARDS_HELP}) for detailed instructionson how to set up your Visa Commercial Cards.\n\n2. [Contact your bank](${CONST.COMPANY_CARDS_HELP}) to verify they support a custom feed for your program, and ask them toenable it.\n\n3. *Once the feed is enabled and you have its details, continue to the next screen.*`, amex: `1. Visit [this help article](${CONST.COMPANY_CARDS_HELP}) to find out if American Express can enable a custom feed for your program.\n\n2. Once the feed is enabled, Amex will send you a production letter.\n\n3. *Once you have the feed information, continue to the next screen.*`, mastercard: `1. Visit [this help article](${CONST.COMPANY_CARDS_HELP}) for detailed instructions on how to set up your Mastercard Commercial Cards.\n\n 2. [Contact your bank](${CONST.COMPANY_CARDS_HELP}) to verify they support a custom feed for your program, and ask them to enable it.\n\n3. *Once the feed is enabled and you have its details, continue to the next screen.*`, + stripe: `1. Visit Stripe’s Dashboard, and go to [Settings](${CONST.COMPANY_CARDS_STRIPE_HELP}).\n\n2. Under Product Integrations, click Enable next to Expensify.\n\n3. Once the feed is enabled, click Submit below and we’ll work on adding it.`, }, whatBankIssuesCard: 'What bank issues these cards?', enterNameOfBank: 'Enter name of bank', diff --git a/src/languages/es.ts b/src/languages/es.ts index c63eab6eccd4..4d90073504a8 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3091,6 +3091,7 @@ const translations = { visa: `1. Visite [este artículo de ayuda](${CONST.COMPANY_CARDS_HELP}) para obtener instrucciones detalladas sobre cómo configurar sus tarjetas comerciales Visa.\n\n2. [Póngase en contacto con su banco](${CONST.COMPANY_CARDS_HELP}) para comprobar que admiten un feed personalizado para su programa, y pídales que lo activen.\n\n3. *Una vez que el feed esté habilitado y tengas sus datos, pasa a la siguiente pantalla.*`, amex: `1. Visite [este artículo de ayuda](${CONST.COMPANY_CARDS_HELP}) para saber si American Express puede habilitar un feed personalizado para su programa.\n\n2. Una vez activada la alimentación, Amex le enviará una carta de producción.\n\n3. *Una vez que tenga la información de alimentación, continúe con la siguiente pantalla.*`, mastercard: `1. Visite [este artículo de ayuda](${CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_SEGMENTS}) para obtener instrucciones detalladas sobre cómo configurar sus tarjetas comerciales Mastercard.\n\n 2. [Póngase en contacto con su banco](${CONST.COMPANY_CARDS_HELP}) para verificar que admiten un feed personalizado para su programa, y pídales que lo habiliten.\n\n3. *Una vez que el feed esté habilitado y tengas sus datos, pasa a la siguiente pantalla.*`, + stripe: `1. Visita el Panel de Stripe y ve a [Configuraciones](${CONST.COMPANY_CARDS_STRIPE_HELP}).\n\n2. En Integraciones de Productos, haz clic en Habilitar junto a Expensify.\n\n3. Una vez que la fuente esté habilitada, haz clic en Enviar abajo y comenzaremos a añadirla.`, }, whatBankIssuesCard: '¿Qué banco emite estas tarjetas?', enterNameOfBank: 'Introduzca el nombre del banco', From 2b38035a8652032e332b317a4b9d4b9c8bdba1fe Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Mon, 14 Oct 2024 19:41:26 +0200 Subject: [PATCH 60/95] update stripe navigation --- src/libs/CardUtils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index bd55808b95ff..bc881081be03 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -253,8 +253,7 @@ const getCorrectStepForSelectedBank = (selectedBank: ValueOf Date: Mon, 14 Oct 2024 19:41:42 +0200 Subject: [PATCH 61/95] adjust instructions page --- .../companyCards/addNew/CardInstructionsStep.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/companyCards/addNew/CardInstructionsStep.tsx b/src/pages/workspace/companyCards/addNew/CardInstructionsStep.tsx index 7338f0df5046..68301470ac1d 100644 --- a/src/pages/workspace/companyCards/addNew/CardInstructionsStep.tsx +++ b/src/pages/workspace/companyCards/addNew/CardInstructionsStep.tsx @@ -11,6 +11,7 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as CardUtils from '@libs/CardUtils'; import Parser from '@libs/Parser'; import * as CompanyCards from '@userActions/CompanyCards'; import CONST from '@src/CONST'; @@ -26,14 +27,21 @@ function CardInstructionsStep() { const data = addNewCard?.data; const feedProvider = data?.cardType; + const isStripeFeedProvider = feedProvider === CONST.COMPANY_CARDS.CARD_TYPE.STRIPE; + const buttonTranslation = isStripeFeedProvider ? translate('common.submit') : translate('common.next'); + const submit = () => { + if (isStripeFeedProvider) { + // TODO: add submit logic + return; + } CompanyCards.setAddNewCompanyCardStepAndData({ step: feedProvider === CONST.COMPANY_CARDS.CARD_TYPE.AMEX ? CONST.COMPANY_CARDS.STEP.CARD_DETAILS : CONST.COMPANY_CARDS.STEP.CARD_NAME, }); }; const handleBackButtonPress = () => { - CompanyCards.setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.CARD_TYPE}); + CompanyCards.setAddNewCompanyCardStepAndData({step: isStripeFeedProvider ? CONST.COMPANY_CARDS.STEP.SELECT_BANK : CONST.COMPANY_CARDS.STEP.CARD_TYPE}); }; return ( @@ -65,7 +73,7 @@ function CardInstructionsStep() { large style={[styles.w100]} onPress={submit} - text={translate('common.next')} + text={buttonTranslation} /> From 1880713c9022b4a27da60a9a596d0495cfdb602f Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Mon, 14 Oct 2024 19:41:58 +0200 Subject: [PATCH 62/95] update navigation to instructions --- src/pages/workspace/companyCards/addNew/SelectBankStep.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/workspace/companyCards/addNew/SelectBankStep.tsx b/src/pages/workspace/companyCards/addNew/SelectBankStep.tsx index 40c55e185cd8..a3fc94d2328f 100644 --- a/src/pages/workspace/companyCards/addNew/SelectBankStep.tsx +++ b/src/pages/workspace/companyCards/addNew/SelectBankStep.tsx @@ -31,9 +31,7 @@ function SelectBankStep() { } else { CompanyCards.setAddNewCompanyCardStepAndData({ step: CardUtils.getCorrectStepForSelectedBank(bankSelected), - data: { - selectedBank: bankSelected, - }, + data: {...(bankSelected === CONST.COMPANY_CARDS.BANKS.STRIPE ? {cardType: CONST.COMPANY_CARDS.CARD_TYPE.STRIPE} : {selectedBank: bankSelected})}, isEditing: false, }); } From 42d240e2603434f4a153674510f385b4f3899a3a Mon Sep 17 00:00:00 2001 From: layacat Date: Tue, 15 Oct 2024 08:44:24 +0700 Subject: [PATCH 63/95] fix: [QBO] There is no option to enter credentials --- src/pages/workspace/accounting/PolicyAccountingPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index 0f02e350d91a..b794043de36e 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -107,7 +107,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { const overflowMenu: ThreeDotsMenuProps['menuItems'] = useMemo( () => [ - ...(shouldShowEnterCredentials && (connectedIntegration === CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT || connectedIntegration === CONST.POLICY.CONNECTIONS.NAME.NETSUITE) + ...(connectedIntegration === CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT || connectedIntegration === CONST.POLICY.CONNECTIONS.NAME.NETSUITE ? [ { icon: Expensicons.Key, @@ -133,7 +133,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { shouldCallAfterModalHide: true, }, ], - [shouldShowEnterCredentials, translate, isOffline, policyID, connectedIntegration, startIntegrationFlow], + [translate, isOffline, policyID, connectedIntegration, startIntegrationFlow], ); useFocusEffect( From 8173b9d741f2aa399dc4e18d12505aa2aeda8ec0 Mon Sep 17 00:00:00 2001 From: layacat Date: Tue, 15 Oct 2024 09:12:34 +0700 Subject: [PATCH 64/95] fix: [QBO] There is no option to enter credentials --- src/pages/workspace/accounting/PolicyAccountingPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index b794043de36e..99979e359c20 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -107,7 +107,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { const overflowMenu: ThreeDotsMenuProps['menuItems'] = useMemo( () => [ - ...(connectedIntegration === CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT || connectedIntegration === CONST.POLICY.CONNECTIONS.NAME.NETSUITE + ...(shouldShowEnterCredentials ? [ { icon: Expensicons.Key, @@ -133,7 +133,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { shouldCallAfterModalHide: true, }, ], - [translate, isOffline, policyID, connectedIntegration, startIntegrationFlow], + [shouldShowEnterCredentials, translate, isOffline, policyID, connectedIntegration, startIntegrationFlow], ); useFocusEffect( From 147dcbc576d8830778de8ad507b2e36f0480a20b Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 15 Oct 2024 13:09:09 +0800 Subject: [PATCH 65/95] fix unlink success isn't translated --- src/pages/signin/LoginForm/BaseLoginForm.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/signin/LoginForm/BaseLoginForm.tsx b/src/pages/signin/LoginForm/BaseLoginForm.tsx index f3df5ebfb0b5..23c4d09e4a64 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.tsx +++ b/src/pages/signin/LoginForm/BaseLoginForm.tsx @@ -62,6 +62,7 @@ function BaseLoginForm({account, login, onLoginChanged, closeAccount, blurOnSubm const isFocused = useIsFocused(); const isLoading = useRef(false); const {shouldUseNarrowLayout, isInNarrowPaneModal} = useResponsiveLayout(); + const accountMessage = account?.message === 'unlinkLoginForm.succesfullyUnlinkedLogin' ? translate(account.message) : account?.message ?? ''; /** * Validate the input value and set the error for formError @@ -276,7 +277,7 @@ function BaseLoginForm({account, login, onLoginChanged, closeAccount, blurOnSubm style={[styles.mv2]} type="success" // eslint-disable-next-line @typescript-eslint/naming-convention,@typescript-eslint/prefer-nullish-coalescing - messages={{0: closeAccount?.success ? closeAccount.success : account?.message || ''}} + messages={{0: closeAccount?.success ? closeAccount.success : accountMessage}} /> )} { From 36157b36df34f7ff5b59b98bf16b8981f3f483b7 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 15 Oct 2024 13:14:27 +0800 Subject: [PATCH 66/95] migrate to useonyx --- src/pages/signin/LoginForm/BaseLoginForm.tsx | 26 +++++--------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/src/pages/signin/LoginForm/BaseLoginForm.tsx b/src/pages/signin/LoginForm/BaseLoginForm.tsx index 23c4d09e4a64..2e762224f904 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.tsx +++ b/src/pages/signin/LoginForm/BaseLoginForm.tsx @@ -3,8 +3,7 @@ import {Str} from 'expensify-common'; import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import DotIndicatorMessage from '@components/DotIndicatorMessage'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import AppleSignIn from '@components/SignInButtons/AppleSignIn'; @@ -34,24 +33,16 @@ import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {CloseAccountForm} from '@src/types/form'; -import type {Account} from '@src/types/onyx'; import htmlDivElementRef from '@src/types/utils/htmlDivElementRef'; import viewRef from '@src/types/utils/viewRef'; import type LoginFormProps from './types'; import type {InputHandle} from './types'; -type BaseLoginFormOnyxProps = { - /** The details about the account that the user is signing in with */ - account: OnyxEntry; +type BaseLoginFormProps = WithToggleVisibilityViewProps & LoginFormProps; - /** Message to display when user successfully closed their account */ - closeAccount: OnyxEntry; -}; - -type BaseLoginFormProps = WithToggleVisibilityViewProps & BaseLoginFormOnyxProps & LoginFormProps; - -function BaseLoginForm({account, login, onLoginChanged, closeAccount, blurOnSubmit = false, isVisible}: BaseLoginFormProps, ref: ForwardedRef) { +function BaseLoginForm({login, onLoginChanged, blurOnSubmit = false, isVisible}: BaseLoginFormProps, ref: ForwardedRef) { + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const [closeAccount] = useOnyx(ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM); const styles = useThemeStyles(); const {isOffline} = useNetwork(); const {translate} = useLocalize(); @@ -332,9 +323,4 @@ function BaseLoginForm({account, login, onLoginChanged, closeAccount, blurOnSubm BaseLoginForm.displayName = 'BaseLoginForm'; -export default withToggleVisibilityView( - withOnyx({ - account: {key: ONYXKEYS.ACCOUNT}, - closeAccount: {key: ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM}, - })(forwardRef(BaseLoginForm)), -); +export default withToggleVisibilityView(forwardRef(BaseLoginForm)); From 5ea6a71238ced609647682eb2433b71972edabed Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 15 Oct 2024 14:13:55 +0800 Subject: [PATCH 67/95] fix type --- src/pages/signin/LoginForm/index.native.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/signin/LoginForm/index.native.tsx b/src/pages/signin/LoginForm/index.native.tsx index 6d8f771810e7..9a9730639b21 100644 --- a/src/pages/signin/LoginForm/index.native.tsx +++ b/src/pages/signin/LoginForm/index.native.tsx @@ -6,7 +6,7 @@ import type {InputHandle} from './types'; import type LoginFormProps from './types'; function LoginForm({scrollPageToTop, ...rest}: LoginFormProps, ref: ForwardedRef) { - const loginFormRef = useRef(); + const loginFormRef = useRef(null); useImperativeHandle(ref, () => ({ isInputFocused: loginFormRef.current ? loginFormRef.current.isInputFocused : () => false, From 46efbfc339b21387d4008d3bbeac78a3a5a039e9 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Tue, 15 Oct 2024 11:18:56 +0200 Subject: [PATCH 68/95] remove unused import --- src/pages/workspace/companyCards/addNew/CardInstructionsStep.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/workspace/companyCards/addNew/CardInstructionsStep.tsx b/src/pages/workspace/companyCards/addNew/CardInstructionsStep.tsx index 68301470ac1d..0407986fe80d 100644 --- a/src/pages/workspace/companyCards/addNew/CardInstructionsStep.tsx +++ b/src/pages/workspace/companyCards/addNew/CardInstructionsStep.tsx @@ -11,7 +11,6 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as CardUtils from '@libs/CardUtils'; import Parser from '@libs/Parser'; import * as CompanyCards from '@userActions/CompanyCards'; import CONST from '@src/CONST'; From de0876a96e5597df91282f29d1f682d31bd2e491 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Tue, 15 Oct 2024 12:22:04 +0200 Subject: [PATCH 69/95] prevent stripe --- src/pages/workspace/companyCards/addNew/DetailsStep.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/companyCards/addNew/DetailsStep.tsx b/src/pages/workspace/companyCards/addNew/DetailsStep.tsx index 93eccbb85a32..e2ef2cddaefb 100644 --- a/src/pages/workspace/companyCards/addNew/DetailsStep.tsx +++ b/src/pages/workspace/companyCards/addNew/DetailsStep.tsx @@ -140,7 +140,7 @@ function DetailsStep() { contentContainerStyle={styles.flexGrow1} > - {feedProvider ? translate(`workspace.companyCards.addNewCard.feedDetails.${feedProvider}.title`) : ''} + {feedProvider && feedProvider !== CONST.COMPANY_CARDS.CARD_TYPE.STRIPE ? translate(`workspace.companyCards.addNewCard.feedDetails.${feedProvider}.title`) : ''} Date: Tue, 15 Oct 2024 14:49:38 +0300 Subject: [PATCH 70/95] Add broken connection violation warning display --- .../BrokenConnectionDescription.tsx | 62 +++++++++++++++++++ src/components/MoneyReportHeader.tsx | 30 +++++++-- src/components/MoneyRequestHeader.tsx | 22 ++++++- .../MoneyRequestHeaderStatusBar.tsx | 4 +- .../ReportActionItem/ReportPreview.tsx | 10 ++- src/libs/TransactionUtils/index.ts | 32 +++------- 6 files changed, 122 insertions(+), 38 deletions(-) create mode 100644 src/components/BrokenConnectionDescription.tsx diff --git a/src/components/BrokenConnectionDescription.tsx b/src/components/BrokenConnectionDescription.tsx new file mode 100644 index 000000000000..866e547e50a0 --- /dev/null +++ b/src/components/BrokenConnectionDescription.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; +import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy, Report} from '@src/types/onyx'; + +type BrokenConnectionDescriptionProps = { + /** Transaction id of the corresponding report */ + transactionID: string; + + /** Current report */ + report: OnyxEntry; + + /** Policy which the report is tied to */ + policy: OnyxEntry; +}; + +function BrokenConnectionDescription({transactionID, policy, report}: BrokenConnectionDescriptionProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const [transactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + + const brokenConnection530Error = transactionViolations?.find((violation) => violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530); + const brokenConnectionError = transactionViolations?.find((violation) => violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION); + const isPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); + + if (!brokenConnection530Error && !brokenConnectionError) { + return ''; + } + + if (brokenConnection530Error) { + return translate('violations.brokenConnection530Error'); + } + + if (isPolicyAdmin) { + return ( + <> + {`${translate('violations.adminBrokenConnectionError')}`} + {}} + >{`${translate('workspace.common.companyCards')}`} + + ); + } + + if (ReportUtils.isReportApproved(report) || ReportUtils.isReportManuallyReimbursed(report) || (ReportUtils.isProcessingReport(report) && !PolicyUtils.isInstantSubmitEnabled(policy))) { + return translate('violations.memberBrokenConnectionError'); + } + + return `${translate('violations.memberBrokenConnectionError')}${translate('violations.markAsCashToIgnore')}`; +} + +BrokenConnectionDescription.displayName = 'BrokenConnectionDescription'; + +export default BrokenConnectionDescription; diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index fdf6f8edd825..a0383aeb80bd 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -2,6 +2,7 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; +import BrokenConnectionDescription from '@components/BrokenConnectionDescription'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -108,6 +109,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const hasOnlyPendingTransactions = allTransactions.length > 0 && allTransactions.every((t) => TransactionUtils.isExpensifyCardTransaction(t) && TransactionUtils.isPending(t)); const transactionIDs = allTransactions.map((t) => t.transactionID); const hasAllPendingRTERViolations = TransactionUtils.allHavePendingRTERViolation([transaction?.transactionID ?? '-1']); + const shouldShowBrokenConnectionViolation = TransactionUtils.shouldShowBrokenConnectionViolation(transaction?.transactionID ?? '-1', moneyRequestReport, policy); const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(moneyRequestReport?.reportID ?? ''); const isPayAtEndExpense = TransactionUtils.isPayAtEndExpense(transaction); const isArchivedReport = ReportUtils.isArchivedRoomWithID(moneyRequestReport?.reportID); @@ -121,25 +123,29 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const onlyShowPayElsewhere = useMemo(() => !canIOUBePaid && getCanIOUBePaid(true), [canIOUBePaid, getCanIOUBePaid]); + const shouldShowMarkAsCashButton = hasAllPendingRTERViolations || (shouldShowBrokenConnectionViolation && !PolicyUtils.isPolicyAdmin(policy)); + const shouldShowPayButton = canIOUBePaid || onlyShowPayElsewhere; const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(moneyRequestReport, policy), [moneyRequestReport, policy]); const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(moneyRequestReport); - const shouldShowSubmitButton = !!moneyRequestReport && isDraft && reimbursableSpend !== 0 && !hasAllPendingRTERViolations; + const shouldShowSubmitButton = !!moneyRequestReport && isDraft && reimbursableSpend !== 0 && !hasAllPendingRTERViolations && !shouldShowBrokenConnectionViolation; const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin && ReportUtils.canBeExported(moneyRequestReport); - const shouldShowSettlementButton = (shouldShowPayButton || shouldShowApproveButton) && !hasAllPendingRTERViolations && !shouldShowExportIntegrationButton; + const shouldShowSettlementButton = + (shouldShowPayButton || shouldShowApproveButton) && !hasAllPendingRTERViolations && !shouldShowExportIntegrationButton && !shouldShowBrokenConnectionViolation; const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport); const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; - const shouldShowStatusBar = hasAllPendingRTERViolations || hasOnlyHeldExpenses || hasScanningReceipt || isPayAtEndExpense || hasOnlyPendingTransactions; + const shouldShowStatusBar = + hasAllPendingRTERViolations || shouldShowBrokenConnectionViolation || hasOnlyHeldExpenses || hasScanningReceipt || isPayAtEndExpense || hasOnlyPendingTransactions; const shouldShowNextStep = !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length && !shouldShowStatusBar; const shouldShowAnyButton = - shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep || hasAllPendingRTERViolations || shouldShowExportIntegrationButton; + shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep || shouldShowMarkAsCashButton || shouldShowExportIntegrationButton; const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport); const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport?.currency); const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(moneyRequestReport, policy); @@ -228,6 +234,18 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea if (hasOnlyHeldExpenses) { return {icon: getStatusIcon(Expensicons.Stopwatch), description: translate('iou.expensesOnHold')}; } + if (shouldShowBrokenConnectionViolation) { + return { + icon: getStatusIcon(Expensicons.Hourglass), + description: ( + + ), + }; + } if (hasAllPendingRTERViolations) { return {icon: getStatusIcon(Expensicons.Hourglass), description: translate('iou.pendingMatchWithCreditCardDescription')}; } @@ -336,7 +354,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea /> )} - {hasAllPendingRTERViolations && !shouldUseNarrowLayout && ( + {shouldShowMarkAsCashButton && !shouldUseNarrowLayout && (