diff --git a/.github/ISSUE_TEMPLATE/Standard.md b/.github/ISSUE_TEMPLATE/Standard.md index 663c6004a534..fa50d48b341b 100644 --- a/.github/ISSUE_TEMPLATE/Standard.md +++ b/.github/ISSUE_TEMPLATE/Standard.md @@ -10,6 +10,7 @@ ___ **Version Number:** **Reproducible in staging?:** **Reproducible in production?:** +**If this was caught on HybridApp, is this reproducible on New Expensify Standalone?:** **If this was caught during regression testing, add the test name, ID and link from TestRail:** **Email or phone of affected tester (no customers):** **Logs:** https://stackoverflow.com/c/expensify/questions/4856 @@ -34,9 +35,11 @@ Can the user still use Expensify without this being fixed? Have you informed the Check off any platforms that are affected by this issue ---> Which of our officially supported platforms is this issue occurring on? -- [ ] Android: Native +- [ ] Android: Standalone +- [ ] Android: HybridApp - [ ] Android: mWeb Chrome -- [ ] iOS: Native +- [ ] iOS: Standalone +- [ ] iOS: HybridApp - [ ] iOS: mWeb Safari - [ ] MacOS: Chrome / Safari - [ ] MacOS: Desktop diff --git a/docs/articles/expensify-classic/expensify-billing/Consolidated-Domain-Billing.md b/docs/articles/expensify-classic/expensify-billing/Consolidated-Domain-Billing.md index 2e829c0785d3..671d0c41e772 100644 --- a/docs/articles/expensify-classic/expensify-billing/Consolidated-Domain-Billing.md +++ b/docs/articles/expensify-classic/expensify-billing/Consolidated-Domain-Billing.md @@ -16,9 +16,12 @@ When a Domain Admin enables Consolidated Domain Billing, all Group workspaces ow If you don’t have multiple billing owners across your organization, or if you want to keep billing separate for any reason, then this feature isn’t necessary. If you have an Annual Subscription and enable Consolidated Domain Billing, the Consolidated Domain Billing feature will gather the amounts due for each Group workspace Billing Owner (listed under **Settings > Workspaces > Group**). To make full use of the Annual Subscription for all workspaces in your domain, you should also be the billing owner for all Group workspaces. + {% include faq-begin.md %} + ## How do I take over the billing of a workspace with Consolidated Domain Billing enabled? You’ll have to toggle off Consolidated Domain Billing, take over ownership of the workspace, and then toggle it back on. + ## Can I use Consolidated Domain Billing to cover the bill for some workspaces, but not others? No, this feature means that you’ll be paying the bill for all domain members who choose a subscription. diff --git a/src/CONST.ts b/src/CONST.ts index caca91a59055..ed25777a7561 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1108,7 +1108,7 @@ const CONST = { }, TIMING: { CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION: 'calc_most_recent_last_modified_action', - CHAT_FINDER_RENDER: 'search_render', + SEARCH_ROUTER_RENDER: 'search_router_render', CHAT_RENDER: 'chat_render', OPEN_REPORT: 'open_report', HOMEPAGE_INITIAL_RENDER: 'homepage_initial_render', @@ -1492,7 +1492,9 @@ const CONST = { MARK_CHECKS_TO_BE_PRINTED: 'markChecksToBePrinted', REIMBURSABLE_ACCOUNT: 'reimbursableAccount', REIMBURSABLE: 'reimbursable', + AUTO_SYNC: 'autoSync', ENABLE_NEW_CATEGORIES: 'enableNewCategories', + SHOULD_AUTO_CREATE_VENDOR: 'shouldAutoCreateVendor', MAPPINGS: { CLASSES: 'classes', }, diff --git a/src/ROUTES.ts b/src/ROUTES.ts index c2c404d7fea5..2cfc796b336e 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) => { @@ -670,6 +669,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export/date-select', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/export/date-select` as const, }, + WORKSPACE_ACCOUNTING_QUICKBOOKS_DESKTOP_ADVANCED: { + route: 'settings/workspaces/:policyID/accounting/quickbooks-desktop/advanced', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-desktop/advanced` as const, + }, POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_EXPORT_DATE_SELECT: { route: 'settings/workspaces/:policyID/accounting/quickbooks-desktop/export/date-select', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-desktop/export/date-select` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 3da0f86d651e..68060d9ee294 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -143,7 +143,6 @@ const SCREENS = { ROOT: 'SaveTheWorld_Root', }, LEFT_MODAL: { - CHAT_FINDER: 'ChatFinder', WORKSPACE_SWITCHER: 'WorkspaceSwitcher', }, RIGHT_MODAL: { @@ -317,6 +316,7 @@ const SCREENS = { QUICKBOOKS_ONLINE_ADVANCED: 'Policy_Accounting_Quickbooks_Online_Advanced', QUICKBOOKS_ONLINE_ACCOUNT_SELECTOR: 'Policy_Accounting_Quickbooks_Online_Account_Selector', QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECTOR: 'Policy_Accounting_Quickbooks_Online_Invoice_Account_Selector', + QUICKBOOKS_DESKTOP_ADVANCED: 'Policy_Accounting_Quickbooks_Desktop_Advanced', QUICKBOOKS_DESKTOP_EXPORT_DATE_SELECT: 'Workspace_Accounting_Quickbooks_Desktop_Export_Date_Select', QUICKBOOKS_DESKTOP_EXPORT_PREFERRED_EXPORTER: 'Workspace_Accounting_Quickbooks_Desktop_Export_Preferred_Exporter', QUICKBOOKS_DESKTOP_EXPORT_OUT_OF_POCKET_EXPENSES: 'Workspace_Accounting_Quickbooks_Desktop_Export_Out_Of_Pocket_Expenses', diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx index 2578845914fc..443a553d4689 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx @@ -25,7 +25,9 @@ type BaseAnchorForAttachmentsOnlyProps = AnchorForAttachmentsOnlyProps & { function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', onPressIn, onPressOut}: BaseAnchorForAttachmentsOnlyProps) { const sourceURLWithAuth = addEncryptedAuthTokenToURL(source); const sourceID = (source.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; + const [download] = useOnyx(`${ONYXKEYS.COLLECTION.DOWNLOAD}${sourceID}`); + const {isOffline} = useNetwork(); const styles = useThemeStyles(); @@ -35,7 +37,7 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', onP {({anchor, report, reportNameValuePairs, action, checkIfContextMenuActive, isDisabled}) => ( { if (isDownloading || isOffline || !sourceID) { return; @@ -61,6 +63,7 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', onP shouldShowDownloadIcon={!!sourceID && !isOffline} shouldShowLoadingSpinnerIcon={isDownloading} isUsedAsChatAttachment + isUploading={!sourceID} /> )} diff --git a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx index ee594f66aabc..e6ac9f9f21c7 100644 --- a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx @@ -24,9 +24,12 @@ type DefaultAttachmentViewProps = { containerStyles?: StyleProp; icon?: IconAsset; + + /** Flag indicating if the attachment is being uploaded. */ + isUploading?: boolean; }; -function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = false, shouldShowDownloadIcon, containerStyles, icon}: DefaultAttachmentViewProps) { +function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = false, shouldShowDownloadIcon, containerStyles, icon, isUploading}: DefaultAttachmentViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -53,7 +56,7 @@ function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = fa )} {shouldShowLoadingSpinnerIcon && ( - + ; -}; +type AttachmentViewProps = Attachment & { + /** Whether this view is the active screen */ + isFocused?: boolean; -type AttachmentViewProps = AttachmentViewOnyxProps & - Attachment & { - /** Whether this view is the active screen */ - isFocused?: boolean; + /** Function for handle on press */ + onPress?: (e?: GestureResponderEvent | KeyboardEvent) => void; - /** Function for handle on press */ - onPress?: (e?: GestureResponderEvent | KeyboardEvent) => void; + /** Whether the attachment is used in attachment modal */ + isUsedInAttachmentModal?: boolean; - isUsedInAttachmentModal?: boolean; + /** Flag to show/hide download icon */ + shouldShowDownloadIcon?: boolean; - /** Flag to show/hide download icon */ - shouldShowDownloadIcon?: boolean; + /** Flag to show the loading indicator */ + shouldShowLoadingSpinnerIcon?: boolean; - /** Flag to show the loading indicator */ - shouldShowLoadingSpinnerIcon?: boolean; + /** Notify parent that the UI should be modified to accommodate keyboard */ + onToggleKeyboard?: (shouldFadeOut: boolean) => void; - /** Notify parent that the UI should be modified to accommodate keyboard */ - onToggleKeyboard?: (shouldFadeOut: boolean) => void; + /** A callback when the PDF fails to load */ + onPDFLoadError?: () => void; - /** A callback when the PDF fails to load */ - onPDFLoadError?: () => void; + /** Extra styles to pass to View wrapper */ + containerStyles?: StyleProp; - /** Extra styles to pass to View wrapper */ - containerStyles?: StyleProp; + /** Denotes whether it is a workspace avatar or not */ + isWorkspaceAvatar?: boolean; - /** Denotes whether it is a workspace avatar or not */ - isWorkspaceAvatar?: boolean; + /** Denotes whether it is an icon (ex: SVG) */ + maybeIcon?: boolean; - /** Denotes whether it is an icon (ex: SVG) */ - maybeIcon?: boolean; + /** Fallback source to use in case of error */ + fallbackSource?: AttachmentSource; - /** Fallback source to use in case of error */ - fallbackSource?: AttachmentSource; + /* Whether it is hovered or not */ + isHovered?: boolean; - /* Whether it is hovered or not */ - isHovered?: boolean; + /** Whether the attachment is used as a chat attachment */ + isUsedAsChatAttachment?: boolean; - /** Whether the attachment is used as a chat attachment */ - isUsedAsChatAttachment?: boolean; + /* Flag indicating whether the attachment has been uploaded. */ + isUploaded?: boolean; - /* Flag indicating whether the attachment has been uploaded. */ - isUploaded?: boolean; - }; + /** Flag indicating if the attachment is being uploaded. */ + isUploading?: boolean; +}; function AttachmentView({ source, @@ -95,16 +92,20 @@ function AttachmentView({ isWorkspaceAvatar, maybeIcon, fallbackSource, - transaction, + transactionID = '-1', reportActionID, isHovered, duration, isUsedAsChatAttachment, isUploaded = true, + isUploading = false, }: AttachmentViewProps) { const {translate} = useLocalize(); const {updateCurrentlyPlayingURL} = usePlaybackContext(); const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); + + const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); + const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -288,20 +289,16 @@ function AttachmentView({ ); } AttachmentView.displayName = 'AttachmentView'; -export default memo( - withOnyx({ - transaction: { - key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - }, - })(AttachmentView), -); +export default memo(AttachmentView); export type {AttachmentViewProps}; 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/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 1faef0c6b44c..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,7 +293,7 @@ function MoneyRequestConfirmationList({ const distance = TransactionUtils.getDistanceInMeters(transaction, unit); const prevDistance = usePrevious(distance); - const shouldCalculateDistanceAmount = isDistanceRequest && (iouAmount === 0 || prevRate !== rate || prevDistance !== distance); + const shouldCalculateDistanceAmount = isDistanceRequest && (iouAmount === 0 || prevRate !== rate || prevDistance !== distance || prevCurrency !== currency); const hasRoute = TransactionUtils.hasRoute(transaction, isDistanceRequest); const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate) && !isMovingTransactionFromTrackExpense; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 5dc56caa0ff6..cdff8a1988e1 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -203,7 +203,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; diff --git a/src/components/Search/SearchRouter/SearchButton.tsx b/src/components/Search/SearchRouter/SearchButton.tsx index 05693ad5ea22..7ed22ec8162f 100644 --- a/src/components/Search/SearchRouter/SearchButton.tsx +++ b/src/components/Search/SearchRouter/SearchButton.tsx @@ -1,30 +1,37 @@ 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'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import Permissions from '@libs/Permissions'; +import Performance from '@libs/Performance'; +import * as Session from '@userActions/Session'; +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(); const {openSearchRouter} = useSearchRouterContext(); - if (!Permissions.canUseNewSearchRouter()) { - return; - } - return ( { + style={[styles.flexRow, styles.touchableButtonImage, style]} + onPress={Session.checkIfActionIsAllowed(() => { + Timing.start(CONST.TIMING.SEARCH_ROUTER_RENDER); + Performance.markStart(CONST.TIMING.SEARCH_ROUTER_RENDER); + openSearchRouter(); - }} + })} > void; +}; + +function SearchRouter({onRouterClose}: SearchRouterProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [betas] = useOnyx(ONYXKEYS.BETAS); @@ -37,7 +41,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 taxRates = getAllTaxRates(); @@ -69,7 +72,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, @@ -91,15 +96,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 = () => { @@ -136,18 +132,18 @@ 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 standardizedQuery = SearchQueryUtils.standardizeQueryJSON(query, cardList, taxRates); const queryString = SearchQueryUtils.buildSearchQueryString(standardizedQuery); Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: queryString})); @@ -155,22 +151,24 @@ function SearchRouter() { }, // eslint-disable-next-line react-compiler/react-compiler // eslint-disable-next-line react-hooks/exhaustive-deps - [closeSearchRouter], + [onRouterClose], ); useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => { - closeSearchRouter(); - clearUserQuery(); + closeAndClearRouter(); }); - const modalWidth = isSmallScreenWidth ? styles.w100 : {width: variables.popoverWidth}; + const modalWidth = isSmallScreenWidth ? styles.w100 : {width: variables.searchRouterPopoverWidth}; return ( - + {isSmallScreenWidth && ( closeSearchRouter()} + onBackButtonPress={() => onRouterClose()} /> )} { + 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/SearchRouterContext.tsx b/src/components/Search/SearchRouter/SearchRouterContext.tsx index d935fff110a4..2e4cbec0d6bb 100644 --- a/src/components/Search/SearchRouter/SearchRouterContext.tsx +++ b/src/components/Search/SearchRouter/SearchRouterContext.tsx @@ -1,10 +1,12 @@ -import React, {useContext, useMemo, useState} from 'react'; +import React, {useContext, useMemo, useRef, useState} from 'react'; +import * as Modal from '@userActions/Modal'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; const defaultSearchContext = { isSearchRouterDisplayed: false, openSearchRouter: () => {}, closeSearchRouter: () => {}, + toggleSearchRouter: () => {}, }; type SearchRouterContext = typeof defaultSearchContext; @@ -13,15 +15,39 @@ 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 = () => { + Modal.close( + () => { + setIsSearchRouterDisplayed(true); + searchRouterDisplayedRef.current = true; + }, + false, + 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/components/Search/SearchRouter/SearchRouterInput.tsx b/src/components/Search/SearchRouter/SearchRouterInput.tsx index 70e2e64074c9..ef6963152c42 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,34 +85,45 @@ 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 89a59f23647b..832af8168ab4 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 SearchQueryUtils from '@libs/SearchQueryUtils'; 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_RENDER); + Performance.markEnd(CONST.TIMING.SEARCH_ROUTER_RENDER); +}; + function isSearchQueryItem(item: OptionData | SearchQueryItem): item is SearchQueryItem { if ('singleIcon' in item && item.singleIcon && 'query' in item && item.query) { return true; @@ -72,7 +80,6 @@ function SearchRouterItem(props: UserListItemProps | SearchQueryList return ( @@ -136,7 +143,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( @@ -158,7 +165,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] : []); } @@ -173,8 +180,12 @@ 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} + sectionTitleStyles={styles.mhn2} + shouldSingleExecuteRowSelect /> ); } diff --git a/src/components/Search/SearchRouter/SearchRouterModal.tsx b/src/components/Search/SearchRouter/SearchRouterModal.tsx index 1f438d254a5f..7e403461dd34 100644 --- a/src/components/Search/SearchRouter/SearchRouterModal.tsx +++ b/src/components/Search/SearchRouter/SearchRouterModal.tsx @@ -17,10 +17,10 @@ function SearchRouterModal() { type={modalType} fullscreen isVisible={isSearchRouterDisplayed} - popoverAnchorPosition={{right: 20, top: 20}} + popoverAnchorPosition={{right: 6, top: 6}} onClose={closeSearchRouter} > - {isSearchRouterDisplayed && } + {isSearchRouterDisplayed && } ); } diff --git a/src/languages/en.ts b/src/languages/en.ts index c2ff45408d3a..3da38a186f60 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -108,7 +108,6 @@ import type { PayerSettledParams, PaySomeoneParams, ReconciliationWorksParams, - ReimbursementRateParams, RemovedFromApprovalWorkflowParams, RemovedTheRequestParams, RemoveMemberPromptParams, @@ -277,6 +276,7 @@ const translations = { close: 'Close', download: 'Download', downloading: 'Downloading', + uploading: 'Uploading', pin: 'Pin', unPin: 'Unpin', back: 'Back', @@ -1015,7 +1015,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!', @@ -2458,6 +2458,12 @@ const translations = { classesDescription: 'Choose how to handle QuickBooks Desktop classes in Expensify.', tagsDisplayedAsDescription: 'Line item level', reportFieldsDisplayedAsDescription: 'Report level', + advancedConfig: { + autoSyncDescription: 'Expensify will automatically sync with QuickBooks Desktop every day.', + createEntities: 'Auto-create entities', + createEntitiesDescription: + "Expensify will automatically create vendors in QuickBooks Desktop if they don't exist already, and auto-create customers when exporting invoices.", + }, }, qbo: { importDescription: 'Choose which coding configurations to import from QuickBooks Online to Expensify.', @@ -5062,6 +5068,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 400e5e8880f3..6825561d74ac 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -106,7 +106,6 @@ import type { PayerSettledParams, PaySomeoneParams, ReconciliationWorksParams, - ReimbursementRateParams, RemovedFromApprovalWorkflowParams, RemovedTheRequestParams, RemoveMemberPromptParams, @@ -267,6 +266,7 @@ const translations = { close: 'Cerrar', download: 'Descargar', downloading: 'Descargando', + uploading: 'Subiendo', pin: 'Fijar', unPin: 'Desfijar', back: 'Volver', @@ -1010,7 +1010,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!', @@ -2482,6 +2482,11 @@ const translations = { classesDescription: 'Elige cómo gestionar las clases de QuickBooks Desktop en Expensify.', tagsDisplayedAsDescription: 'Nivel de partida', reportFieldsDisplayedAsDescription: 'Nivel de informe', + advancedConfig: { + autoSyncDescription: 'Expensify se sincronizará automáticamente con QuickBooks Desktop todos los días.', + createEntities: 'Crear entidades automáticamente', + createEntitiesDescription: 'Expensify creará automáticamente proveedores en QuickBooks Desktop si aún no existen, y creará automáticamente clientes al exportar facturas.', + }, }, qbo: { importDescription: 'Elige que configuraciónes de codificación son importadas desde QuickBooks Online a Expensify.', @@ -5577,6 +5582,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/languages/params.ts b/src/languages/params.ts index f787e630ab0d..9fd980d1e98f 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 = { @@ -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/libs/API/types.ts b/src/libs/API/types.ts index 2463fc1cdc0c..53d9ed329969 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -259,6 +259,8 @@ const WRITE_COMMANDS = { UPDATE_QUICKBOOKS_ONLINE_EXPORT: 'UpdateQuickbooksOnlineExport', UPDATE_QUICKBOOKS_DESKTOP_EXPORT_DATE: 'UpdateQuickbooksDesktopExportDate', UPDATE_MANY_POLICY_CONNECTION_CONFIGS: 'UpdateManyPolicyConnectionConfigurations', + UPDATE_QUICKBOOKS_DESKTOP_AUTO_CREATE_VENDOR: 'UpdateQuickbooksDesktopAutoCreateVendor', + UPDATE_QUICKBOOKS_DESKTOP_AUTO_SYNC: 'UpdateQuickbooksDesktopAutoSync', UPDATE_QUICKBOOKS_DESKTOP_EXPORT: 'UpdateQuickbooksDesktopExport', UPDATE_QUICKBOOKS_DESKTOP_REIMBURSABLE_EXPENSES_ACCOUNT: 'UpdateQuickbooksDesktopReimbursableExpensesAccount', UPDATE_QUICKBOOKS_DESKTOP_MARK_CHECKS_TO_BE_PRINTED: 'UpdateQuickbooksDesktopMarkChecksToBePrinted', @@ -698,6 +700,8 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_QUICKBOOKS_ONLINE_COLLECTION_ACCOUNT_ID]: Parameters.UpdateQuickbooksOnlineGenericTypeParams; [WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_EXPORT_DATE]: Parameters.UpdateQuickbooksDesktopGenericTypeParams; [WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_MARK_CHECKS_TO_BE_PRINTED]: Parameters.UpdateQuickbooksDesktopGenericTypeParams; + [WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_AUTO_CREATE_VENDOR]: Parameters.UpdateQuickbooksDesktopGenericTypeParams; + [WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_AUTO_SYNC]: Parameters.UpdateQuickbooksDesktopGenericTypeParams; [WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_REIMBURSABLE_EXPENSES_ACCOUNT]: Parameters.UpdateQuickbooksDesktopGenericTypeParams; [WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION]: Parameters.UpdateQuickbooksDesktopExpensesExportDestinationTypeParams; [WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_ENABLE_NEW_CATEGORIES]: Parameters.UpdateQuickbooksDesktopGenericTypeParams; diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 939bd3d1aa10..e7ad63467781 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -6,7 +6,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 { @@ -592,12 +591,12 @@ function validateReportActionJSON(json: string) { /** * Gets the reason for showing LHN row */ -function getReasonForShowingRowInLHN(report: OnyxEntry): TranslationPaths | null { +function getReasonForShowingRowInLHN(report: OnyxEntry, hasRBR = false): TranslationPaths | null { if (!report) { return null; } - const doesReportHaveViolations = OptionsListUtils.shouldShowViolations(report, transactionViolations); + const doesReportHaveViolations = ReportUtils.shouldShowViolations(report, transactionViolations); const reason = ReportUtils.reasonForReportToBeInOptionList({ report, @@ -611,7 +610,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 + 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`; + } + + // 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) { return 'debug.reasonVisibleInLHN.isFocused'; } @@ -645,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/E2E/reactNativeLaunchingTest.ts b/src/libs/E2E/reactNativeLaunchingTest.ts index f952998f0aad..fdd305baf88c 100644 --- a/src/libs/E2E/reactNativeLaunchingTest.ts +++ b/src/libs/E2E/reactNativeLaunchingTest.ts @@ -32,7 +32,7 @@ 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.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 72% rename from src/libs/E2E/tests/openChatFinderPageTest.e2e.ts rename to src/libs/E2E/tests/openSearchRouterTest.e2e.ts index 2c2f2eda4efe..840af5acc2c9 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,36 +18,29 @@ 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 [openSearchRouterPromise, openSearchRouterResolve] = getPromiseWithResolve(); const [loadSearchOptionsPromise, loadSearchOptionsResolve] = getPromiseWithResolve(); - Promise.all([openSearchPagePromise, loadSearchOptionsPromise]).then(() => { + Promise.all([openSearchRouterPromise, loadSearchOptionsPromise]).then(() => { console.debug(`[E2E] Submitting!`); E2EClient.submitTestDone(); }); 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_RENDER) { E2EClient.submitTestResults({ branch: Config.E2E_BRANCH, - name: 'Open Chat Finder Page TTI', + name: 'Open Search Router TTI', metric: entry.duration, unit: 'ms', }) .then(() => { - openSearchPageResolve(); + openSearchRouterResolve(); console.debug('[E2E] Done with search, exiting…'); }) .catch((err) => { diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 40910014faa9..045de686bb6b 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -7,6 +7,7 @@ import ActiveGuidesEventListener from '@components/ActiveGuidesEventListener'; import ComposeProviders from '@components/ComposeProviders'; import OptionsListContextProvider from '@components/OptionListContextProvider'; import {SearchContextProvider} from '@components/Search/SearchContext'; +import {useSearchRouterContext} from '@components/Search/SearchRouter/SearchRouterContext'; import SearchRouterModal from '@components/Search/SearchRouter/SearchRouterModal'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useOnboardingFlowRouter from '@hooks/useOnboardingFlow'; @@ -228,6 +229,8 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie const screenOptions = getRootNavigatorScreenOptions(shouldUseNarrowLayout, styles, StyleUtils); const {canUseDefaultRooms} = usePermissions(); const {activeWorkspaceID} = useActiveWorkspace(); + const {toggleSearchRouter} = useSearchRouterContext(); + const onboardingModalScreenOptions = useMemo(() => screenOptions.onboardingModalNavigator(onboardingIsMediumOrLargerScreenWidth), [screenOptions, onboardingIsMediumOrLargerScreenWidth]); const onboardingScreenOptions = useMemo( () => getOnboardingModalScreenOptions(shouldUseNarrowLayout, styles, StyleUtils, onboardingIsMediumOrLargerScreenWidth), @@ -236,6 +239,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) { @@ -346,16 +350,14 @@ 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)), - true, - true, - ); + Session.checkIfActionIsAllowed(() => { + toggleSearchRouter(); + })(); }, shortcutsOverviewShortcutConfig.descriptionKey, shortcutsOverviewShortcutConfig.modifiers, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index c32f63906443..f149d72bc34c 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -323,6 +323,8 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/accounting/qbd/export/QuickbooksDesktopOutOfPocketExpenseEntitySelectPage').default, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_EXPORT]: () => require('../../../../pages/workspace/accounting/qbd/export/QuickbooksDesktopExportPage').default, + [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_ADVANCED]: () => + require('../../../../pages/workspace/accounting/qbd/advanced/QuickbooksDesktopAdvancedPage').default, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_SETUP_MODAL]: () => require('../../../../pages/workspace/accounting/qbd/QuickBooksDesktopSetupPage').default, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_SETUP_REQUIRED_DEVICE_MODAL]: () => require('../../../../pages/workspace/accounting/qbd/RequireQuickBooksDesktopPage').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} > - 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/AppNavigator/createCustomPlatformStackBottomTabNavigator/TopBar.tsx b/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/TopBar.tsx index 3b4879839ae0..cef1266d2d26 100644 --- a/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/TopBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/TopBar.tsx @@ -2,32 +2,26 @@ 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 SearchQueryUtils from '@libs/SearchQueryUtils'; 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 +57,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/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index 552310dceae4..380977e4e06f 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -45,6 +45,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_ADVANCED, SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_ACCOUNT_SELECTOR, SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECTOR, + SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_ADVANCED, SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_EXPORT_DATE_SELECT, SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_EXPORT_PREFERRED_EXPORTER, SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_EXPORT_OUT_OF_POCKET_EXPENSES, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 8f152e2d35de..1bb9d61b2251 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, }, @@ -393,6 +392,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECTOR]: { path: ROUTES.WORKSPACE_ACCOUNTING_QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECTOR.route, }, + [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_ADVANCED]: { + path: ROUTES.WORKSPACE_ACCOUNTING_QUICKBOOKS_DESKTOP_ADVANCED.route, + }, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_EXPORT_DATE_SELECT]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_EXPORT_DATE_SELECT.route}, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_EXPORT_PREFERRED_EXPORTER]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_PREFERRED_EXPORTER.route}, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_EXPORT_OUT_OF_POCKET_EXPENSES_ACCOUNT_SELECT]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index cbd6a2af95bf..a90b45ed5903 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -436,6 +436,9 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_EXPORT_PREFERRED_EXPORTER]: { policyID: string; }; + [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_ADVANCED]: { + policyID: string; + }; [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_EXPORT_DATE_SELECT]: { policyID: string; }; @@ -1280,7 +1283,6 @@ type TransactionDuplicateNavigatorParamList = { }; type LeftModalNavigatorParamList = { - [SCREENS.LEFT_MODAL.CHAT_FINDER]: undefined; [SCREENS.LEFT_MODAL.WORKSPACE_SWITCHER]: undefined; }; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index fbf2f3b94c7c..142a299f3d74 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'; @@ -343,26 +342,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, @@ -481,78 +460,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. */ @@ -749,7 +656,7 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails } function hasReportErrors(report: Report, reportActions: OnyxEntry) { - return !isEmptyObject(getAllReportErrors(report, reportActions)); + return !isEmptyObject(ReportUtils.getAllReportErrors(report, reportActions)); } /** @@ -817,7 +724,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; @@ -1771,23 +1678,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 */ @@ -1898,7 +1788,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, @@ -2629,7 +2519,6 @@ export { getPersonalDetailsForAccountIDs, getIOUConfirmationOptionsFromPayeePersonalDetail, isSearchStringMatchUserDetails, - getAllReportErrors, getPolicyExpenseReportOption, getIOUReportIDOfLastAction, getParticipantsOption, @@ -2655,13 +2544,11 @@ export { getFirstKeyForList, canCreateOptimisticPersonalDetailOption, getUserToInviteOption, - shouldShowViolations, getPersonalDetailSearchTerms, getCurrentUserSearchTerms, getEmptyOptions, shouldUseBoldText, getAlternateText, - getAllReportActionsErrorsAndReportActionThatRequiresAttention, hasReportErrors, }; diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index de3afbabadc2..24de2e612208 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -2,7 +2,6 @@ import type {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import type {IOUType} from '@src/CONST'; 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); @@ -58,17 +57,6 @@ function canUseNewDotQBD(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.NEW_DOT_QBD) || canUseAllBetas(betas); } -/** - * 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. */ @@ -88,7 +76,6 @@ export default { canUseNewDotCopilot, canUseWorkspaceRules, canUseCombinedTrackSubmit, - canUseNewSearchRouter, canUseCategoryAndTagApprovers, canUseNewDotQBD, }; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d7ff4538748c..bf687c973abb 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 isReportMessageAttachment from './isReportMessageAttachment'; import localeCompare from './LocaleCompare'; @@ -1377,7 +1378,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; @@ -6260,6 +6261,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; @@ -8473,6 +8580,10 @@ export { reasonForReportToBeInOptionList, getReasonAndReportActionThatRequiresAttention, isPolicyRelatedReport, + hasReportErrorsOtherThanFailedReceipt, + shouldShowViolations, + getAllReportErrors, + getAllReportActionsErrorsAndReportActionThatRequiresAttention, }; export type { diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index eb5b3c58cdef..944f703e96cb 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..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'; @@ -60,7 +59,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; diff --git a/src/libs/actions/TaxRate.ts b/src/libs/actions/TaxRate.ts index 52a8b8e143b8..e1586f9cb24b 100644 --- a/src/libs/actions/TaxRate.ts +++ b/src/libs/actions/TaxRate.ts @@ -1,4 +1,4 @@ -import type {OnyxCollection} from 'react-native-onyx'; +import type {NullishDeep, OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {FormOnyxValues} from '@components/Form/types'; import * as API from '@libs/API'; @@ -288,6 +288,11 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { const firstTaxID = Object.keys(policyTaxRates ?? {}) .sort((a, b) => a.localeCompare(b)) .at(0); + const customUnits = policy?.customUnits ?? {}; + const customUnitID = Object.keys(customUnits).at(0) ?? '-1'; + const ratesToUpdate = Object.values(customUnits?.[customUnitID]?.rates ?? {}).filter( + (rate) => !!rate.attributes?.taxRateExternalID && taxesToDelete.includes(rate.attributes?.taxRateExternalID), + ); if (!policyTaxRates) { console.debug('Policy or tax rates not found'); @@ -296,6 +301,33 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { const isForeignTaxRemoved = foreignTaxDefault && taxesToDelete.includes(foreignTaxDefault); + const optimisticRates: Record> = {}; + const successRates: Record = {}; + const failureRates: Record = {}; + + ratesToUpdate.forEach((rate) => { + const rateID = rate.customUnitRateID ?? ''; + optimisticRates[rateID] = { + attributes: { + taxRateExternalID: null, + taxClaimablePercentage: null, + }, + pendingFields: { + taxRateExternalID: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + taxClaimablePercentage: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }; + successRates[rateID] = {pendingFields: {taxRateExternalID: null, taxClaimablePercentage: null}}; + failureRates[rateID] = { + attributes: {...rate?.attributes}, + pendingFields: {taxRateExternalID: null, taxClaimablePercentage: null}, + errorFields: { + taxRateExternalID: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + taxClaimablePercentage: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + }, + }; + }); + const onyxData: OnyxData = { optimisticData: [ { @@ -310,6 +342,11 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { return acc; }, {}), }, + customUnits: customUnits && { + [customUnitID]: { + rates: optimisticRates, + }, + }, }, }, ], @@ -325,6 +362,11 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { return acc; }, {}), }, + customUnits: customUnits && { + [customUnitID]: { + rates: successRates, + }, + }, }, }, ], @@ -344,6 +386,11 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { return acc; }, {}), }, + customUnits: customUnits && { + [customUnitID]: { + rates: failureRates, + }, + }, }, }, ], diff --git a/src/libs/actions/connections/QuickbooksDesktop.ts b/src/libs/actions/connections/QuickbooksDesktop.ts index 80f50dce7b8a..d20af8426395 100644 --- a/src/libs/actions/connections/QuickbooksDesktop.ts +++ b/src/libs/actions/connections/QuickbooksDesktop.ts @@ -341,6 +341,21 @@ function updateQuickbooksDesktopExpensesExportDestination( + policyID: string, + settingValue: TSettingValue, +) { + const onyxData = buildOnyxDataForQuickbooksConfiguration(policyID, CONST.QUICKBOOKS_DESKTOP_CONFIG.SHOULD_AUTO_CREATE_VENDOR, settingValue, !settingValue); + + const parameters: UpdateQuickbooksDesktopGenericTypeParams = { + policyID, + settingValue: JSON.stringify(settingValue), + idempotencyKey: String(CONST.QUICKBOOKS_DESKTOP_CONFIG.SHOULD_AUTO_CREATE_VENDOR), + }; + + API.write(WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_AUTO_CREATE_VENDOR, parameters, onyxData); +} + function updateQuickbooksDesktopMarkChecksToBePrinted( policyID: string, settingValue: TSettingValue, @@ -425,9 +440,22 @@ function updateQuickbooksDesktopExportDate(policyID: string, settingValue: TSettingValue) { + const onyxData = buildOnyxDataForQuickbooksConfiguration(policyID, CONST.QUICKBOOKS_DESKTOP_CONFIG.AUTO_SYNC, {enabled: settingValue}, {enabled: !settingValue}); + + const parameters: UpdateQuickbooksDesktopGenericTypeParams = { + policyID, + settingValue: JSON.stringify(settingValue), + idempotencyKey: String(CONST.QUICKBOOKS_DESKTOP_CONFIG.AUTO_SYNC), + }; + API.write(WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_AUTO_SYNC, parameters, onyxData); +} + export { + updateQuickbooksDesktopAutoSync, updateQuickbooksDesktopPreferredExporter, updateQuickbooksDesktopMarkChecksToBePrinted, + updateQuickbooksDesktopShouldAutoCreateVendor, updateQuickbooksDesktopExpensesExportDestination, updateQuickbooksDesktopReimbursableExpensesAccount, getQuickbooksDesktopCodatSetupLink, diff --git a/src/pages/ChatFinderPage/ChatFinderPageFooter.tsx b/src/pages/ChatFinderPage/ChatFinderPageFooter.tsx deleted file mode 100644 index 4c006abacfc7..000000000000 --- a/src/pages/ChatFinderPage/ChatFinderPageFooter.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import ReferralProgramCTA from '@components/ReferralProgramCTA'; -import CONST from '@src/CONST'; - -function ChatFinderPageFooter() { - return ; -} - -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/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 [ diff --git a/src/pages/Search/SearchPageBottomTab.tsx b/src/pages/Search/SearchPageBottomTab.tsx index dfb671da7af1..0b1089349ea1 100644 --- a/src/pages/Search/SearchPageBottomTab.tsx +++ b/src/pages/Search/SearchPageBottomTab.tsx @@ -86,6 +86,8 @@ function SearchPageBottomTab() { ); } + const shouldDisplayCancelSearch = shouldUseNarrowLayout && !SearchUtils.isCannedSearchQuery(queryJSON); + return ( {shouldUseNarrowLayout ? ( 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} - + { @@ -43,6 +45,8 @@ function BaseSidebarScreen() { updateLastAccessedWorkspace(undefined); }, [activeWorkspace, activeWorkspaceID]); + const shouldDisplaySearch = shouldUseNarrowLayout; + return ( { @@ -61,15 +63,16 @@ function IOURequestStepDistanceRate({ const sections = Object.values(rates) .sort((rateA, rateB) => (rateA?.rate ?? 0) - (rateB?.rate ?? 0)) .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, alternateText: rate.name ? rateForDisplay : '', keyForList: rate.customUnitRateID, value: rate.customUnitRateID, isDisabled: !rate.enabled, - isSelected: currentRateID ? currentRateID === rate.customUnitRateID : rate.name === CONST.CUSTOM_UNITS.DEFAULT_RATE, + isSelected, }; }); @@ -108,7 +111,7 @@ function IOURequestStepDistanceRate({ shouldShowWrapper testID={IOURequestStepDistanceRate.displayName} > - {translate('iou.chooseARate', {unit})} + {translate('iou.chooseARate')} QuickbooksDesktop.updateQuickbooksDesktopAutoSync(policyID, isOn), + subscribedSetting: CONST.QUICKBOOKS_DESKTOP_CONFIG.AUTO_SYNC, + errors: ErrorUtils.getLatestErrorField(qbdConfig, CONST.QUICKBOOKS_DESKTOP_CONFIG.AUTO_SYNC), + pendingAction: settingsPendingAction([CONST.QUICKBOOKS_DESKTOP_CONFIG.AUTO_SYNC], qbdConfig?.pendingFields), + }, + { + title: translate('workspace.qbd.advancedConfig.createEntities'), + subtitle: translate('workspace.qbd.advancedConfig.createEntitiesDescription'), + switchAccessibilityLabel: translate('workspace.qbd.advancedConfig.createEntitiesDescription'), + isActive: !!qbdConfig?.shouldAutoCreateVendor, + onToggle: (isOn: boolean) => { + QuickbooksDesktop.updateQuickbooksDesktopShouldAutoCreateVendor(policyID, isOn); + }, + subscribedSetting: CONST.QUICKBOOKS_DESKTOP_CONFIG.SHOULD_AUTO_CREATE_VENDOR, + errors: ErrorUtils.getLatestErrorField(qbdConfig, CONST.QUICKBOOKS_DESKTOP_CONFIG.SHOULD_AUTO_CREATE_VENDOR), + pendingAction: settingsPendingAction([CONST.QUICKBOOKS_DESKTOP_CONFIG.SHOULD_AUTO_CREATE_VENDOR], qbdConfig?.pendingFields), + }, + ]; + + return ( + Navigation.goBack(ROUTES.POLICY_ACCOUNTING.getRoute(policyID))} + > + {qbdToggleSettingItems.map((item) => ( + clearQBDErrorField(policyID, item.subscribedSetting)} + /> + ))} + + ); +} + +QuickbooksDesktopAdvancedPage.displayName = 'QuickbooksDesktopAdvancedPage'; + +export default withPolicyConnections(QuickbooksDesktopAdvancedPage); diff --git a/src/pages/workspace/accounting/utils.tsx b/src/pages/workspace/accounting/utils.tsx index 7e694babdd5c..cc1dfa1fcd9f 100644 --- a/src/pages/workspace/accounting/utils.tsx +++ b/src/pages/workspace/accounting/utils.tsx @@ -259,7 +259,7 @@ function getAccountingIntegrationData( onImportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_IMPORT.getRoute(policyID)), onExportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_EXPORT.getRoute(policyID)), onCardReconciliationPagePress: () => {}, - onAdvancedPagePress: () => {}, + onAdvancedPagePress: () => Navigation.navigate(ROUTES.WORKSPACE_ACCOUNTING_QUICKBOOKS_DESKTOP_ADVANCED.getRoute(policyID)), // TODO: [QBD] Make sure all values are passed to subscribedSettings subscribedImportSettings: [CONST.QUICKBOOKS_DESKTOP_CONFIG.ENABLE_NEW_CATEGORIES, CONST.QUICKBOOKS_DESKTOP_CONFIG.MAPPINGS.CLASSES], subscribedExportSettings: [ @@ -269,7 +269,7 @@ function getAccountingIntegrationData( CONST.QUICKBOOKS_DESKTOP_CONFIG.REIMBURSABLE_ACCOUNT, CONST.QUICKBOOKS_DESKTOP_CONFIG.MARK_CHECKS_TO_BE_PRINTED, ], - subscribedAdvancedSettings: [], + subscribedAdvancedSettings: [CONST.QUICKBOOKS_DESKTOP_CONFIG.SHOULD_AUTO_CREATE_VENDOR, CONST.QUICKBOOKS_DESKTOP_CONFIG.AUTO_SYNC], }; default: return undefined; diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx index c4d033351b37..0b7d925f2ee2 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx @@ -1,8 +1,7 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useState} 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 ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -26,22 +25,17 @@ 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 type {Rate, TaxRateAttributes} from '@src/types/onyx/Policy'; -type PolicyDistanceRateDetailsPageOnyxProps = { - /** Policy details */ - policy: OnyxEntry; -}; +type PolicyDistanceRateDetailsPageProps = StackScreenProps; -type PolicyDistanceRateDetailsPageProps = PolicyDistanceRateDetailsPageOnyxProps & StackScreenProps; - -function PolicyDistanceRateDetailsPage({policy, route}: PolicyDistanceRateDetailsPageProps) { +function PolicyDistanceRateDetailsPage({route}: PolicyDistanceRateDetailsPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [isWarningModalVisible, setIsWarningModalVisible] = useState(false); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const policyID = route.params.policyID; + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID}`); const rateID = route.params.rateID; const customUnits = policy?.customUnits ?? {}; const customUnit = customUnits[Object.keys(customUnits)[0]]; @@ -155,7 +149,7 @@ function PolicyDistanceRateDetailsPage({policy, route}: PolicyDistanceRateDetail )} - {isDistanceTrackTaxEnabled && isPolicyTrackTaxEnabled && ( + {isDistanceTrackTaxEnabled && !!taxRate && isPolicyTrackTaxEnabled && ( ({ - policy: { - key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID}`, - }, -})(PolicyDistanceRateDetailsPage); +export default PolicyDistanceRateDetailsPage; diff --git a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitTypePage.tsx b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitTypePage.tsx index e4f14f6c137a..94a1382b4454 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitTypePage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitTypePage.tsx @@ -23,7 +23,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; type WorkspaceEditCardLimitTypePageProps = StackScreenProps; @@ -38,7 +37,7 @@ function WorkspaceEditCardLimitTypePage({route}: WorkspaceEditCardLimitTypePageP const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`); const card = cardsList?.[cardID]; - const areApprovalsConfigured = !isEmptyObject(policy?.approver) && policy?.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL; + const areApprovalsConfigured = PolicyUtils.getApprovalWorkflow(policy) !== CONST.POLICY.APPROVAL_MODE.OPTIONAL; const defaultLimitType = areApprovalsConfigured ? CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART : CONST.EXPENSIFY_CARD.LIMIT_TYPES.MONTHLY; const initialLimitType = card?.nameValuePairs?.limitType ?? defaultLimitType; const promptTranslationKey = diff --git a/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx index de4bca070d51..e0ae8954720c 100644 --- a/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx @@ -11,11 +11,11 @@ import RadioListItem from '@components/SelectionList/RadioListItem'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as PolicyUtils from '@libs/PolicyUtils'; import * as Card from '@userActions/Card'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; type LimitTypeStepProps = { // The policy that the card will be issued under @@ -27,7 +27,7 @@ function LimitTypeStep({policy}: LimitTypeStepProps) { const styles = useThemeStyles(); const [issueNewCard] = useOnyx(ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD); - const areApprovalsConfigured = !isEmptyObject(policy?.approver) && policy?.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL; + const areApprovalsConfigured = PolicyUtils.getApprovalWorkflow(policy) !== CONST.POLICY.APPROVAL_MODE.OPTIONAL; const defaultType = areApprovalsConfigured ? CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART : CONST.EXPENSIFY_CARD.LIMIT_TYPES.MONTHLY; const [typeSelected, setTypeSelected] = useState(issueNewCard?.data?.limitType ?? defaultType); diff --git a/src/styles/index.ts b/src/styles/index.ts index 4738844f314f..015dea9b7ecc 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 22bc7cb9bbcb..fd0a3a3cabc7 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, }, diff --git a/src/styles/variables.ts b/src/styles/variables.ts index dccee6ed3e53..dc6655791489 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -167,6 +167,7 @@ export default { modalContentMaxWidth: 360, listItemHeightNormal: 64, popoverWidth: 375, + searchRouterPopoverWidth: 512, bankAccountActionPopoverRightSpacing: 32, bankAccountActionPopoverTopSpacing: 14, addPaymentPopoverRightSpacing: 23, diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 5ecf9ad8d84f..2bd94bce6da4 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1247,10 +1247,10 @@ type QBDConnectionConfig = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Configuration of automatic synchronization from QuickBooks Desktop to the app */ autoSync: { - /** TODO: Will be handled in another issue */ + /** Job ID of the synchronization */ jobID: string; - /** Whether changes made in QuickBooks Online should be reflected into the app automatically */ + /** Whether changes made in QuickBooks Desktop should be reflected into the app automatically */ enabled: boolean; }; diff --git a/tests/e2e/config.ts b/tests/e2e/config.ts index 4d4f1711a628..c8e89721c998 100644 --- a/tests/e2e/config.ts +++ b/tests/e2e/config.ts @@ -4,7 +4,7 @@ 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', + OpenSearchRouter: 'Open search router TTI', ReportTyping: 'Report typing', ChatOpening: 'Chat opening', Linking: 'Linking', @@ -73,8 +73,8 @@ export default { name: TEST_NAMES.AppStartTime, // ... any additional config you might need }, - [TEST_NAMES.OpenChatFinderPage]: { - name: TEST_NAMES.OpenChatFinderPage, + [TEST_NAMES.OpenSearchRouter]: { + name: TEST_NAMES.OpenSearchRouter, }, [TEST_NAMES.ReportTyping]: { name: TEST_NAMES.ReportTyping, diff --git a/tests/perf-test/ChatFinderPage.perf-test.tsx b/tests/perf-test/SearchRouter.perf-test.tsx similarity index 66% rename from tests/perf-test/ChatFinderPage.perf-test.tsx rename to tests/perf-test/SearchRouter.perf-test.tsx index 4346977a1cd0..e9154a36a9a1 100644 --- a/tests/perf-test/ChatFinderPage.perf-test.tsx +++ b/tests/perf-test/SearchRouter.perf-test.tsx @@ -1,28 +1,23 @@ 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 {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 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 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'; @@ -68,6 +63,9 @@ jest.mock('@react-navigation/native', () => { getCurrentRoute: () => jest.fn(), getState: () => jest.fn(), }), + useNavigationState: () => ({ + routes: [], + }), }; }); @@ -86,15 +84,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( @@ -134,49 +123,33 @@ afterEach(() => { Onyx.clear(); }); -type ChatFinderPageProps = StackScreenProps & { - betas?: OnyxEntry; - reports?: OnyxCollection; - isSearchingForReports?: OnyxEntry; -}; +const mockOnClose = jest.fn(); -function ChatFinderPageWrapper(args: ChatFinderPageProps) { +function SearchRouterWrapper() { return ( - + ); } -function ChatFinderPageWithCachedOptions(args: ChatFinderPageProps) { +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'); + await screen.findByTestId('SearchRouter'); }; - const navigation = {addListener} as unknown as StackNavigationProp; - return waitForBatchedUpdates() .then(() => Onyx.multiSet({ @@ -186,31 +159,19 @@ test('[ChatFinderPage] should render list with cached options', async () => { [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, }), ) - .then(() => - measurePerformance( - , - {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({ @@ -220,13 +181,5 @@ test('[ChatFinderPage] should interact when text input changes', async () => { [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, }), ) - .then(() => - measurePerformance( - , - {scenario}, - ), - ); + .then(() => measureRenders(, {scenario})); }); diff --git a/tests/unit/DebugUtilsTest.ts b/tests/unit/DebugUtilsTest.ts index 34c2ad2bde73..fa44b8972cf3 100644 --- a/tests/unit/DebugUtilsTest.ts +++ b/tests/unit/DebugUtilsTest.ts @@ -783,6 +783,134 @@ 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, true); + 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, 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', + }, + }, + true, + ); + expect(reason).toBe('debug.reasonVisibleInLHN.hasRBR'); + }); }); describe('getReasonAndReportActionForGBRInLHNRow', () => { beforeAll(() => { @@ -963,8 +1091,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 +1138,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', () => {