diff --git a/android/app/build.gradle b/android/app/build.gradle index 9343423c9f14..ba41291038f3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -107,8 +107,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001048016 - versionName "1.4.80-16" + versionCode 1001048107 + versionName "1.4.81-7" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/docs/_data/_routes.yml b/docs/_data/_routes.yml index ad738e44ab44..5fd65532c021 100644 --- a/docs/_data/_routes.yml +++ b/docs/_data/_routes.yml @@ -71,7 +71,7 @@ platforms: - href: integrations title: Integrations - icon: /assets/images/workflow.svg + icon: /assets/images/simple-illustration__monitor-remotesync.svg description: Integrate with accounting or HR software to streamline expense approvals. - href: spending-insights @@ -131,7 +131,7 @@ platforms: - href: connections title: Connections - icon: /assets/images/workflow.svg + icon: /assets/images/simple-illustration__monitor-remotesync.svg description: Connect to accounting software to streamline expense approvals. - href: settings diff --git a/docs/assets/images/simple-illustration__monitor-remotesync.svg b/docs/assets/images/simple-illustration__monitor-remotesync.svg new file mode 100644 index 000000000000..e4ed84a35851 --- /dev/null +++ b/docs/assets/images/simple-illustration__monitor-remotesync.svg @@ -0,0 +1,30 @@ + + + + + + + + + \ No newline at end of file diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index ad1d743d778d..e782155beee7 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.80 + 1.4.81 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.80.16 + 1.4.81.7 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 23bdf74a3648..c18015ff0858 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.80 + 1.4.81 CFBundleSignature ???? CFBundleVersion - 1.4.80.16 + 1.4.81.7 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 9f8cc7b1612e..2feda22f8a36 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.80 + 1.4.81 CFBundleVersion - 1.4.80.16 + 1.4.81.7 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c138d1b27f61..aca46d6b18ed 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1852,7 +1852,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.82): + - RNLiveMarkdown (0.1.83): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1870,9 +1870,9 @@ PODS: - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/common (= 0.1.82) + - RNLiveMarkdown/common (= 0.1.83) - Yoga - - RNLiveMarkdown/common (0.1.82): + - RNLiveMarkdown/common (0.1.83): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -2589,7 +2589,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 74b7b3d06d667ba0bbf41da7718f2607ae0dfe8f RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: d160a948e52282067439585c89a3962582c082ce + RNLiveMarkdown: 88030b7d9a31f5f6e67743df48ad952d64513b4a RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: df8fe93dbd251f25022f4023d31bc04160d4d65c RNPermissions: 0b61d30d21acbeafe25baaa47d9bae40a0c65216 diff --git a/package-lock.json b/package-lock.json index aaef29c6fad9..6fb017197b28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "new.expensify", - "version": "1.4.80-16", + "version": "1.4.81-7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.80-16", + "version": "1.4.81-7", "hasInstallScript": true, "license": "MIT", "dependencies": { "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.82", + "@expensify/react-native-live-markdown": "0.1.83", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -3558,9 +3558,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.82", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.82.tgz", - "integrity": "sha512-w/K2+0d1sAYvyLVpPv1ufDOTaj4y96Z362N3JDN+SDUmPQN2MvVGwsTL0ltzdw78yd62azFcQl6th7P6l62THQ==", + "version": "0.1.83", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.83.tgz", + "integrity": "sha512-xGn1P9FbFVueEF8BNKJJ4dQb0wPtsAvrrxND9pwVQT35ZL5cu1KZ4o6nzCqtesISPRB8Dw9Zx0ftIZy2uCQyzA==", "workspaces": [ "parser", "example", diff --git a/package.json b/package.json index ee1ad32a0067..dcd8fd60863c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.80-16", + "version": "1.4.81-7", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -65,7 +65,7 @@ "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.82", + "@expensify/react-native-live-markdown": "0.1.83", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", diff --git a/src/App.tsx b/src/App.tsx index 6316fa80fba1..9eda57816e9d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,7 +6,7 @@ import {PickerStateProvider} from 'react-native-picker-select'; import {SafeAreaProvider} from 'react-native-safe-area-context'; import '../wdyr'; import ActiveElementRoleProvider from './components/ActiveElementRoleProvider'; -import ActiveWorkspaceContextProvider from './components/ActiveWorkspace/ActiveWorkspaceProvider'; +import ActiveWorkspaceContextProvider from './components/ActiveWorkspaceProvider'; import ColorSchemeWrapper from './components/ColorSchemeWrapper'; import ComposeProviders from './components/ComposeProviders'; import CustomStatusBarAndBackground from './components/CustomStatusBarAndBackground'; diff --git a/src/CONST.ts b/src/CONST.ts index b0e3ab8c3af4..d3132b17a2ea 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1829,7 +1829,7 @@ const CONST = { NAME_DISTANCE: 'Distance', DISTANCE_UNIT_MILES: 'mi', DISTANCE_UNIT_KILOMETERS: 'km', - MILEAGE_IRS_RATE: 0.655, + MILEAGE_IRS_RATE: 0.67, DEFAULT_RATE: 'Default Rate', RATE_DECIMALS: 3, FAKE_P2P_ID: '_FAKE_P2P_ID_', @@ -3394,6 +3394,11 @@ const CONST = { * @deprecated Please stop using the accessibilityRole prop and use the role prop instead. */ IMAGE: 'image', + + /** + * @deprecated Please stop using the accessibilityRole prop and use the role prop instead. + */ + TEXTBOX: 'textbox', }, /** * Acceptable values for the `role` attribute on react native components. @@ -4753,6 +4758,7 @@ const CONST = { SESSION_STORAGE_KEYS: { INITIAL_URL: 'INITIAL_URL', + ACTIVE_WORKSPACE_ID: 'ACTIVE_WORKSPACE_ID', }, RESERVATION_TYPE: { diff --git a/src/components/ActiveWorkspace/ActiveWorkspaceProvider.tsx b/src/components/ActiveWorkspaceProvider/index.tsx similarity index 80% rename from src/components/ActiveWorkspace/ActiveWorkspaceProvider.tsx rename to src/components/ActiveWorkspaceProvider/index.tsx index 884b9a2a2d95..bc7260cdf10b 100644 --- a/src/components/ActiveWorkspace/ActiveWorkspaceProvider.tsx +++ b/src/components/ActiveWorkspaceProvider/index.tsx @@ -1,6 +1,6 @@ import React, {useMemo, useState} from 'react'; +import ActiveWorkspaceContext from '@components/ActiveWorkspace/ActiveWorkspaceContext'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; -import ActiveWorkspaceContext from './ActiveWorkspaceContext'; function ActiveWorkspaceContextProvider({children}: ChildrenProps) { const [activeWorkspaceID, setActiveWorkspaceID] = useState(undefined); @@ -10,7 +10,7 @@ function ActiveWorkspaceContextProvider({children}: ChildrenProps) { activeWorkspaceID, setActiveWorkspaceID, }), - [activeWorkspaceID], + [activeWorkspaceID, setActiveWorkspaceID], ); return {children}; diff --git a/src/components/ActiveWorkspaceProvider/index.website.tsx b/src/components/ActiveWorkspaceProvider/index.website.tsx new file mode 100644 index 000000000000..82e46d70f896 --- /dev/null +++ b/src/components/ActiveWorkspaceProvider/index.website.tsx @@ -0,0 +1,29 @@ +import React, {useCallback, useMemo, useState} from 'react'; +import ActiveWorkspaceContext from '@components/ActiveWorkspace/ActiveWorkspaceContext'; +import CONST from '@src/CONST'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; + +function ActiveWorkspaceContextProvider({children}: ChildrenProps) { + const [activeWorkspaceID, updateActiveWorkspaceID] = useState(undefined); + + const setActiveWorkspaceID = useCallback((workspaceID: string | undefined) => { + updateActiveWorkspaceID(workspaceID); + if (workspaceID && sessionStorage) { + sessionStorage?.setItem(CONST.SESSION_STORAGE_KEYS.ACTIVE_WORKSPACE_ID, workspaceID); + } else { + sessionStorage?.removeItem(CONST.SESSION_STORAGE_KEYS.ACTIVE_WORKSPACE_ID); + } + }, []); + + const value = useMemo( + () => ({ + activeWorkspaceID, + setActiveWorkspaceID, + }), + [activeWorkspaceID, setActiveWorkspaceID], + ); + + return {children}; +} + +export default ActiveWorkspaceContextProvider; diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx index e5eb09691eba..e641a0c2218a 100644 --- a/src/components/Breadcrumbs.tsx +++ b/src/components/Breadcrumbs.tsx @@ -68,7 +68,7 @@ function Breadcrumbs({breadcrumbs, style}: BreadcrumbsProps) { / {secondaryBreadcrumb.text} diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index 2fbd635ed42e..5e5f126da773 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -28,6 +28,7 @@ function Composer( // On Android the selection prop is required on the TextInput but this prop has issues on IOS selection, value, + isGroupPolicyReport = false, ...props }: ComposerProps, ref: ForwardedRef, @@ -36,7 +37,7 @@ function Composer( const {isFocused, shouldResetFocus} = useResetComposerFocus(textInput); const textContainsOnlyEmojis = useMemo(() => EmojiUtils.containsOnlyEmojis(value ?? ''), [value]); const theme = useTheme(); - const markdownStyle = useMarkdownStyle(value); + const markdownStyle = useMarkdownStyle(value, !isGroupPolicyReport ? ['mentionReport'] : []); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 14762b2d4bc1..eae27440d175 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -71,6 +71,7 @@ function Composer( isReportActionCompose = false, isComposerFullSize = false, shouldContainScroll = false, + isGroupPolicyReport = false, ...props }: ComposerProps, ref: ForwardedRef, @@ -78,7 +79,7 @@ function Composer( const textContainsOnlyEmojis = useMemo(() => EmojiUtils.containsOnlyEmojis(value ?? ''), [value]); const theme = useTheme(); const styles = useThemeStyles(); - const markdownStyle = useMarkdownStyle(value); + const markdownStyle = useMarkdownStyle(value, !isGroupPolicyReport ? ['mentionReport'] : []); const StyleUtils = useStyleUtils(); const textRef = useRef(null); const textInput = useRef(null); diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index 531bcd03f8bf..0ff91111bd07 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -70,6 +70,9 @@ type ComposerProps = TextInputProps & { /** Should make the input only scroll inside the element avoid scroll out to parent */ shouldContainScroll?: boolean; + + /** Indicates whether the composer is in a group policy report. Used for disabling report mentioning style in markdown input */ + isGroupPolicyReport?: boolean; }; export type {TextSelection, ComposerProps}; diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 6405e3026b1a..7a2650ab5bd6 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -165,8 +165,8 @@ function LHNOptionsList({ ); const extraData = useMemo( - () => [reportActions, reports, policy, personalDetails, data.length, draftComments], - [reportActions, reports, policy, personalDetails, data.length, draftComments], + () => [reportActions, reports, policy, personalDetails, data.length, draftComments, optionMode], + [reportActions, reports, policy, personalDetails, data.length, draftComments, optionMode], ); const previousOptionMode = usePrevious(optionMode); diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index ec52a6158ad7..5ea35c2d3c3f 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -27,6 +27,7 @@ import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import MoneyReportHeaderStatusBar from './MoneyReportHeaderStatusBar'; import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar'; +import type {ActionHandledType} from './ProcessMoneyReportHoldMenu'; import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu'; import SettlementButton from './SettlementButton'; @@ -79,7 +80,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea isActionOwner && (ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) || ReportUtils.isTrackExpenseReport(transactionThreadReport)) && !isDeletedParentAction; const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false); const [paymentType, setPaymentType] = useState(); - const [requestType, setRequestType] = useState<'pay' | 'approve'>(); + const [requestType, setRequestType] = useState(); const canAllowSettlement = ReportUtils.hasUpdatedTotal(moneyRequestReport, policy); const policyType = policy?.type; const isPayer = ReportUtils.isPayer(session, moneyRequestReport); @@ -124,7 +125,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea return; } setPaymentType(type); - setRequestType('pay'); + setRequestType(CONST.IOU.REPORT_ACTION_TYPE.PAY); if (ReportUtils.hasHeldExpenses(moneyRequestReport.reportID)) { setIsHoldMenuVisible(true); } else if (ReportUtils.isInvoiceReport(moneyRequestReport)) { @@ -135,7 +136,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea }; const confirmApproval = () => { - setRequestType('approve'); + setRequestType(CONST.IOU.REPORT_ACTION_TYPE.APPROVE); if (ReportUtils.hasHeldExpenses(moneyRequestReport.reportID)) { setIsHoldMenuVisible(true); } else { diff --git a/src/components/MoneyRequestAmountInput.tsx b/src/components/MoneyRequestAmountInput.tsx index 1cccfdc720b3..fb17297dc642 100644 --- a/src/components/MoneyRequestAmountInput.tsx +++ b/src/components/MoneyRequestAmountInput.tsx @@ -7,6 +7,7 @@ import * as Browser from '@libs/Browser'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import getOperatingSystem from '@libs/getOperatingSystem'; import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; +import shouldIgnoreSelectionWhenUpdatedManually from '@libs/shouldIgnoreSelectionWhenUpdatedManually'; import CONST from '@src/CONST'; import type {BaseTextInputRef} from './TextInput/BaseTextInput/types'; import TextInputWithCurrencySymbol from './TextInputWithCurrencySymbol'; @@ -140,6 +141,8 @@ function MoneyRequestAmountInput( }); const forwardDeletePressedRef = useRef(false); + // The ref is used to ignore any onSelectionChange event that happens while we are updating the selection manually in setNewAmount + const willSelectionBeUpdatedManually = useRef(false); /** * Sets the selection and the amount accordingly to the value passed to the input @@ -162,6 +165,7 @@ function MoneyRequestAmountInput( // setCurrentAmount contains another setState(setSelection) making it error-prone since it is leading to setSelection being called twice for a single setCurrentAmount call. This solution introducing the hasSelectionBeenSet flag was chosen for its simplicity and lower risk of future errors https://github.com/Expensify/App/issues/23300#issuecomment-1766314724. + willSelectionBeUpdatedManually.current = true; let hasSelectionBeenSet = false; setCurrentAmount((prevAmount) => { const strippedAmount = MoneyRequestUtils.stripCommaFromAmount(finalAmount); @@ -169,6 +173,7 @@ function MoneyRequestAmountInput( if (!hasSelectionBeenSet) { hasSelectionBeenSet = true; setSelection((prevSelection) => getNewSelection(prevSelection, isForwardDelete ? strippedAmount.length : prevAmount.length, strippedAmount.length)); + willSelectionBeUpdatedManually.current = false; } onAmountChange?.(strippedAmount); return strippedAmount; @@ -294,6 +299,10 @@ function MoneyRequestAmountInput( selectedCurrencyCode={currency} selection={selection} onSelectionChange={(e: NativeSyntheticEvent) => { + if (shouldIgnoreSelectionWhenUpdatedManually && willSelectionBeUpdatedManually.current) { + willSelectionBeUpdatedManually.current = false; + return; + } if (!shouldUpdateSelection) { return; } diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx index 6e81c9d57bc8..01896fb0a3cb 100644 --- a/src/components/ProcessMoneyReportHoldMenu.tsx +++ b/src/components/ProcessMoneyReportHoldMenu.tsx @@ -4,11 +4,15 @@ import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import {isLinkedTransactionHeld} from '@libs/ReportActionsUtils'; import * as IOU from '@userActions/IOU'; +import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; +import type DeepValueOf from '@src/types/utils/DeepValueOf'; import DecisionModal from './DecisionModal'; +type ActionHandledType = DeepValueOf; + type ProcessMoneyReportHoldMenuProps = { /** The chat report this report is linked to */ chatReport: OnyxEntry; @@ -35,7 +39,7 @@ type ProcessMoneyReportHoldMenuProps = { paymentType?: PaymentMethodType; /** Type of action handled */ - requestType?: 'pay' | 'approve'; + requestType?: ActionHandledType; }; function ProcessMoneyReportHoldMenu({ @@ -50,7 +54,7 @@ function ProcessMoneyReportHoldMenu({ moneyRequestReport, }: ProcessMoneyReportHoldMenuProps) { const {translate} = useLocalize(); - const isApprove = requestType === 'approve'; + const isApprove = requestType === CONST.IOU.REPORT_ACTION_TYPE.APPROVE; const onSubmit = (full: boolean) => { if (isApprove) { @@ -82,3 +86,4 @@ function ProcessMoneyReportHoldMenu({ ProcessMoneyReportHoldMenu.displayName = 'ProcessMoneyReportHoldMenu'; export default ProcessMoneyReportHoldMenu; +export type {ActionHandledType}; diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx index 65eb9b82ecdc..946856ecec37 100644 --- a/src/components/ReceiptImage.tsx +++ b/src/components/ReceiptImage.tsx @@ -76,9 +76,6 @@ type ReceiptImageProps = ( /** The colod of the fallback icon */ fallbackIconColor?: string; - - /** Whether the component is hovered */ - isHovered?: boolean; }; function ReceiptImage({ @@ -96,7 +93,6 @@ function ReceiptImage({ fallbackIconSize, shouldUseInitialObjectPosition = false, fallbackIconColor, - isHovered = false, }: ReceiptImageProps) { const styles = useThemeStyles(); @@ -134,7 +130,6 @@ function ReceiptImage({ fallbackIconSize={fallbackIconSize} fallbackIconColor={fallbackIconColor} objectPosition={shouldUseInitialObjectPosition ? CONST.IMAGE_OBJECT_POSITION.INITIAL : CONST.IMAGE_OBJECT_POSITION.TOP} - isHovered={isHovered} /> ); } diff --git a/src/components/ReportActionItem/MoneyRequestAction.tsx b/src/components/ReportActionItem/MoneyRequestAction.tsx index 4f91b2084b45..fd82e723c6b9 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.tsx +++ b/src/components/ReportActionItem/MoneyRequestAction.tsx @@ -10,7 +10,6 @@ import * as IOUUtils from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; -import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -92,7 +91,6 @@ function MoneyRequestAction({ } const childReportID = action?.childReportID ?? '0'; - Report.openReport(childReportID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); }; diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index b6d6915279cb..e03ed59ffb79 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -163,11 +163,7 @@ function MoneyRequestPreviewContent({ const isTooLong = violationsCount > 1 || violationMessage.length > 15; const hasViolationsAndFieldErrors = violationsCount > 0 && hasFieldErrors; - message += ` ${CONST.DOT_SEPARATOR} ${isTooLong || hasViolationsAndFieldErrors ? translate('violations.reviewRequired') : violationMessage}`; - if (shouldShowHoldMessage) { - message += ` ${CONST.DOT_SEPARATOR} ${translate('iou.hold')}`; - } - return message; + return `${message} ${CONST.DOT_SEPARATOR} ${isTooLong || hasViolationsAndFieldErrors ? translate('violations.reviewRequired') : violationMessage}`; } const isMerchantMissing = TransactionUtils.isMerchantMissing(transaction); diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 12a325f6aa19..cc99a3f6e108 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -171,7 +171,8 @@ function MoneyRequestView({ const {getViolationsForField} = useViolations(transactionViolations ?? []); const hasViolations = useCallback( - (field: ViolationField, data?: OnyxTypes.TransactionViolation['data']): boolean => !!canUseViolations && getViolationsForField(field, data).length > 0, + (field: ViolationField, data?: OnyxTypes.TransactionViolation['data'], policyHasDependentTags = false, tagValue?: string): boolean => + !!canUseViolations && getViolationsForField(field, data, policyHasDependentTags, tagValue).length > 0, [canUseViolations, getViolationsForField], ); @@ -238,7 +239,7 @@ function MoneyRequestView({ const getPendingFieldAction = (fieldPath: TransactionPendingFieldsKey) => transaction?.pendingFields?.[fieldPath] ?? pendingAction; const getErrorForField = useCallback( - (field: ViolationField, data?: OnyxTypes.TransactionViolation['data']) => { + (field: ViolationField, data?: OnyxTypes.TransactionViolation['data'], policyHasDependentTags = false, tagValue?: string) => { // Checks applied when creating a new expense // NOTE: receipt field can return multiple violations, so we need to handle it separately const fieldChecks: Partial> = { @@ -264,14 +265,14 @@ function MoneyRequestView({ } // Return violations if there are any - if (canUseViolations && hasViolations(field, data)) { - const violations = getViolationsForField(field, data); + if (hasViolations(field, data, policyHasDependentTags, tagValue)) { + const violations = getViolationsForField(field, data, policyHasDependentTags, tagValue); return ViolationsUtils.getViolationTranslation(violations[0], translate); } return ''; }, - [transactionAmount, isSettled, isCancelled, isPolicyExpenseChat, isEmptyMerchant, transactionDate, hasErrors, canUseViolations, hasViolations, translate, getViolationsForField], + [transactionAmount, isSettled, isCancelled, isPolicyExpenseChat, isEmptyMerchant, transactionDate, hasErrors, hasViolations, translate, getViolationsForField], ); const distanceRequestFields = canUseP2PDistanceRequests ? ( @@ -333,6 +334,37 @@ function MoneyRequestView({ ...parentReportAction?.errors, }; + const tagList = policyTagLists.map(({name, orderWeight}, index) => { + const tagError = getErrorForField( + 'tag', + { + tagListIndex: index, + tagListName: name, + }, + PolicyUtils.hasDependentTags(policy, policyTagList), + TransactionUtils.getTagForDisplay(transaction, index), + ); + return ( + + + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(CONST.IOU.ACTION.EDIT, iouType, orderWeight, transaction?.transactionID ?? '', report.reportID)) + } + brickRoadIndicator={tagError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + errorText={tagError} + /> + + ); + }); + return ( {shouldShowAnimatedBackground && } @@ -468,35 +500,7 @@ function MoneyRequestView({ /> )} - {shouldShowTag && - policyTagLists.map(({name, orderWeight}, index) => ( - - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(CONST.IOU.ACTION.EDIT, iouType, orderWeight, transaction?.transactionID ?? '', report.reportID), - ) - } - brickRoadIndicator={ - getErrorForField('tag', { - tagListIndex: index, - tagListName: name, - }) - ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR - : undefined - } - errorText={getErrorForField('tag', {tagListIndex: index, tagListName: name})} - /> - - ))} + {shouldShowTag && tagList} {isCardTransaction && ( (); + const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(iouReport, policy); + const {isSmallScreenWidth} = useWindowDimensions(); + const [paymentType, setPaymentType] = useState(); + const managerID = iouReport?.managerID ?? 0; const {totalDisplaySpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(iouReport); @@ -164,6 +173,32 @@ function ReportPreview({ [chatReport?.isOwnPolicyExpenseChat, policy?.harvesting?.enabled], ); + const confirmPayment = (type: PaymentMethodType | undefined) => { + if (!type) { + return; + } + setPaymentType(type); + setRequestType(CONST.IOU.REPORT_ACTION_TYPE.PAY); + if (ReportUtils.hasHeldExpenses(iouReport?.reportID)) { + setIsHoldMenuVisible(true); + } else if (chatReport && iouReport) { + if (ReportUtils.isInvoiceReport(iouReport)) { + IOU.payInvoice(type, chatReport, iouReport); + } else { + IOU.payMoneyRequest(type, chatReport, iouReport); + } + } + }; + + const confirmApproval = () => { + setRequestType(CONST.IOU.REPORT_ACTION_TYPE.APPROVE); + if (ReportUtils.hasHeldExpenses(iouReport?.reportID)) { + setIsHoldMenuVisible(true); + } else { + IOU.approveMoneyRequest(iouReport ?? {}, true); + } + }; + const getDisplayAmount = (): string => { if (totalDisplaySpend) { return CurrencyUtils.convertToDisplayString(totalDisplaySpend, iouReport?.currency); @@ -282,17 +317,6 @@ function ReportPreview({ }; }, [formattedMerchant, formattedDescription, moneyRequestComment, translate, numberOfRequests, numberOfScanningReceipts, numberOfPendingRequests]); - const confirmPayment = (paymentMethodType?: PaymentMethodType) => { - if (!paymentMethodType || !chatReport || !iouReport) { - return; - } - if (ReportUtils.isInvoiceReport(iouReport)) { - IOU.payInvoice(paymentMethodType, chatReport, iouReport); - } else { - IOU.payMoneyRequest(paymentMethodType, chatReport, iouReport); - } - }; - return ( + {isHoldMenuVisible && iouReport && requestType !== undefined && ( + setIsHoldMenuVisible(false)} + isVisible={isHoldMenuVisible} + paymentType={paymentType} + chatReport={chatReport} + moneyRequestReport={iouReport} + /> + )} ); } diff --git a/src/components/SelectionList/Search/ReportListItem.tsx b/src/components/SelectionList/Search/ReportListItem.tsx index bc5a84c33b90..9adff46395e6 100644 --- a/src/components/SelectionList/Search/ReportListItem.tsx +++ b/src/components/SelectionList/Search/ReportListItem.tsx @@ -146,64 +146,61 @@ function ReportListItem({ shouldSyncFocus={shouldSyncFocus} hoverStyle={item.isSelected && styles.activeComponentBG} > - {(hovered?: boolean) => ( - - {!isLargeScreenWidth && ( - - )} - - - - - {reportItem?.reportName} - {`${reportItem.transactions.length} ${translate('search.groupedExpenses')}`} - + + {!isLargeScreenWidth && ( + + )} + + + + + {reportItem?.reportName} + {`${reportItem.transactions.length} ${translate('search.groupedExpenses')}`} - - + + + + + {isLargeScreenWidth && ( + <> + {/** We add an empty view with type style to align the total with the table header */} + + + - - {isLargeScreenWidth && ( - <> - {/** We add an empty view with type style to align the total with the table header */} - - - - - - )} - - - {reportItem.transactions.map((transaction) => ( - { - openReportInRHP(transaction); - }} - showItemHeaderOnNarrowLayout={false} - containerStyle={styles.mt3} - isHovered={hovered} - isChildListItem - /> - ))} + + )} - )} + + {reportItem.transactions.map((transaction) => ( + { + openReportInRHP(transaction); + }} + showItemHeaderOnNarrowLayout={false} + containerStyle={styles.mt3} + isChildListItem + /> + ))} + ); } diff --git a/src/components/SelectionList/Search/TransactionListItem.tsx b/src/components/SelectionList/Search/TransactionListItem.tsx index b222631b7273..ecf9264301c2 100644 --- a/src/components/SelectionList/Search/TransactionListItem.tsx +++ b/src/components/SelectionList/Search/TransactionListItem.tsx @@ -50,16 +50,13 @@ function TransactionListItem({ shouldSyncFocus={shouldSyncFocus} hoverStyle={item.isSelected && styles.activeComponentBG} > - {(hovered?: boolean) => ( - { - onSelectRow(item); - }} - isHovered={hovered} - /> - )} + { + onSelectRow(item); + }} + /> ); } diff --git a/src/components/SelectionList/Search/TransactionListItemRow.tsx b/src/components/SelectionList/Search/TransactionListItemRow.tsx index 50a34be86f61..d17d923a54e1 100644 --- a/src/components/SelectionList/Search/TransactionListItemRow.tsx +++ b/src/components/SelectionList/Search/TransactionListItemRow.tsx @@ -33,10 +33,6 @@ type TransactionCellProps = { transactionItem: TransactionListItemType; } & CellProps; -type ReceiptCellProps = { - isHovered?: boolean; -} & TransactionCellProps; - type ActionCellProps = { onButtonPress: () => void; } & CellProps; @@ -51,7 +47,6 @@ type TransactionListItemRowProps = { onButtonPress: () => void; showItemHeaderOnNarrowLayout?: boolean; containerStyle?: StyleProp; - isHovered?: boolean; isChildListItem?: boolean; }; @@ -68,7 +63,7 @@ const getTypeIcon = (type?: SearchTransactionType) => { } }; -function ReceiptCell({transactionItem, isHovered = false}: ReceiptCellProps) { +function ReceiptCell({transactionItem}: TransactionCellProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -85,7 +80,6 @@ function ReceiptCell({transactionItem, isHovered = false}: ReceiptCellProps) { fallbackIconSize={20} fallbackIconColor={theme.icon} iconSize="x-small" - isHovered={isHovered} /> ); @@ -181,14 +175,14 @@ function TagCell({isLargeScreenWidth, showTooltip, transactionItem}: Transaction return isLargeScreenWidth ? ( ) : ( ); } @@ -209,15 +203,7 @@ function TaxCell({transactionItem, showTooltip}: TransactionCellProps) { ); } -function TransactionListItemRow({ - item, - showTooltip, - onButtonPress, - showItemHeaderOnNarrowLayout = true, - containerStyle, - isHovered = false, - isChildListItem = false, -}: TransactionListItemRowProps) { +function TransactionListItemRow({item, showTooltip, onButtonPress, showItemHeaderOnNarrowLayout = true, containerStyle, isChildListItem = false}: TransactionListItemRowProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isLargeScreenWidth} = useWindowDimensions(); @@ -242,7 +228,6 @@ function TransactionListItemRow({ transactionItem={item} isLargeScreenWidth={false} showTooltip={false} - isHovered={isHovered} /> diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton.tsx index b6e2a753c829..85954e68c5a9 100644 --- a/src/components/SettlementButton.tsx +++ b/src/components/SettlementButton.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useMemo} from 'react'; +import React, {useMemo} from 'react'; import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -8,7 +8,6 @@ import * as ReportUtils from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import * as BankAccounts from '@userActions/BankAccounts'; import * as IOU from '@userActions/IOU'; -import * as PaymentMethods from '@userActions/PaymentMethods'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; @@ -144,10 +143,6 @@ function SettlementButton({ const {translate} = useLocalize(); const {isOffline} = useNetwork(); - useEffect(() => { - PaymentMethods.openWalletPage(); - }, []); - const session = useSession(); const chatReport = ReportUtils.getReport(chatReportID); const isInvoiceReport = (!isEmptyObject(iouReport) && ReportUtils.isInvoiceReport(iouReport)) || false; diff --git a/src/components/ThumbnailImage.tsx b/src/components/ThumbnailImage.tsx index d4a45f3d93b3..3b89b7c3a7ad 100644 --- a/src/components/ThumbnailImage.tsx +++ b/src/components/ThumbnailImage.tsx @@ -2,6 +2,7 @@ import React, {useCallback, useEffect, useState} from 'react'; import type {ImageSourcePropType, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useNetwork from '@hooks/useNetwork'; +import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useThumbnailDimensions from '@hooks/useThumbnailDimensions'; @@ -48,9 +49,6 @@ type ThumbnailImageProps = { /** The object position of image */ objectPosition?: ImageObjectPosition; - - /** Whether the component is hovered */ - isHovered?: boolean; }; type UpdateImageSizeParams = { @@ -69,7 +67,6 @@ function ThumbnailImage({ fallbackIconSize = variables.iconSizeSuperLarge, fallbackIconColor, objectPosition = CONST.IMAGE_OBJECT_POSITION.INITIAL, - isHovered = false, }: ThumbnailImageProps) { const styles = useThemeStyles(); const theme = useTheme(); @@ -78,6 +75,7 @@ function ThumbnailImage({ const cachedDimensions = shouldDynamicallyResize && typeof previewSourceURL === 'string' ? thumbnailDimensionsCache.get(previewSourceURL) : null; const [imageDimensions, setImageDimensions] = useState({width: cachedDimensions?.width ?? imageWidth, height: cachedDimensions?.height ?? imageHeight}); const {thumbnailDimensionsStyles} = useThumbnailDimensions(imageDimensions.width, imageDimensions.height); + const StyleUtils = useStyleUtils(); useEffect(() => { setFailedToLoad(false); @@ -110,7 +108,7 @@ function ThumbnailImage({ if (failedToLoad || previewSourceURL === '') { return ( - + (null); - const {source, name, type, id} = useMemo(() => { + const mainAvatar = useMemo(() => { if (!policy) { return {source: Expensicons.ExpensifyAppIcon, name: CONST.WORKSPACE_SWITCHER.NAME, type: CONST.ICON_TYPE_AVATAR}; } @@ -56,7 +56,7 @@ function WorkspaceSwitcherButton({policy}: WorkspaceSwitcherButtonProps) { > {({hovered}) => ( { inputRef.current = ref; + if (isInputInitialized) { + return; + } setIsInputInitialized(true); }; diff --git a/src/hooks/useMarkdownStyle.ts b/src/hooks/useMarkdownStyle.ts index d1af33aa9da5..77ad5ce816f5 100644 --- a/src/hooks/useMarkdownStyle.ts +++ b/src/hooks/useMarkdownStyle.ts @@ -5,12 +5,25 @@ import FontUtils from '@styles/utils/FontUtils'; import variables from '@styles/variables'; import useTheme from './useTheme'; -function useMarkdownStyle(message: string | null = null): MarkdownStyle { +function useMarkdownStyle(message: string | null = null, excludeStyles: Array = []): MarkdownStyle { const theme = useTheme(); const emojiFontSize = containsOnlyEmojis(message ?? '') ? variables.fontSizeOnlyEmojis : variables.fontSizeNormal; - const markdownStyle = useMemo( + // this map is used to reset the styles that are not needed - passing undefined value can break the native side + const nonStylingDefaultValues: Record = useMemo( () => ({ + color: theme.text, + backgroundColor: 'transparent', + marginLeft: 0, + paddingLeft: 0, + borderColor: 'transparent', + borderWidth: 0, + }), + [theme], + ); + + const markdownStyle = useMemo(() => { + const styling = { syntax: { color: theme.syntax, }, @@ -59,9 +72,21 @@ function useMarkdownStyle(message: string | null = null): MarkdownStyle { color: theme.mentionText, backgroundColor: theme.mentionBG, }, - }), - [theme, emojiFontSize], - ); + }; + + if (excludeStyles.length) { + excludeStyles.forEach((key) => { + const style: Record = styling[key]; + if (style) { + Object.keys(style).forEach((styleKey) => { + style[styleKey] = nonStylingDefaultValues[styleKey] ?? style[styleKey]; + }); + } + }); + } + + return styling; + }, [theme, emojiFontSize, excludeStyles, nonStylingDefaultValues]); return markdownStyle; } diff --git a/src/hooks/useViolations.ts b/src/hooks/useViolations.ts index d1cc7ec70181..83c725a48db0 100644 --- a/src/hooks/useViolations.ts +++ b/src/hooks/useViolations.ts @@ -62,12 +62,12 @@ function useViolations(violations: TransactionViolation[]) { }, [violations]); const getViolationsForField = useCallback( - (field: ViolationField, data?: TransactionViolation['data']) => { + (field: ViolationField, data?: TransactionViolation['data'], policyHasDependentTags = false, tagValue?: string) => { const currentViolations = violationsByField.get(field) ?? []; // someTagLevelsRequired has special logic becase data.errorIndexes is a bit unique in how it denotes the tag list that has the violation // tagListIndex can be 0 so we compare with undefined - if (currentViolations[0]?.name === 'someTagLevelsRequired' && data?.tagListIndex !== undefined && Array.isArray(currentViolations[0]?.data?.errorIndexes)) { + if (currentViolations[0]?.name === CONST.VIOLATIONS.SOME_TAG_LEVELS_REQUIRED && data?.tagListIndex !== undefined && Array.isArray(currentViolations[0]?.data?.errorIndexes)) { return currentViolations .filter((violation) => violation.data?.errorIndexes?.includes(data?.tagListIndex ?? -1)) .map((violation) => ({ @@ -79,8 +79,28 @@ function useViolations(violations: TransactionViolation[]) { })); } + // missingTag has special logic for policies with dependent tags, because only one violation is returned for all tags + // when no tags are present, so the tag name isn't set in the violation data. That's why we add it here + if (policyHasDependentTags && currentViolations[0]?.name === CONST.VIOLATIONS.MISSING_TAG && data?.tagListName) { + return [ + { + ...currentViolations[0], + data: { + ...currentViolations[0].data, + tagName: data?.tagListName, + }, + }, + ]; + } + // tagOutOfPolicy has special logic because we have to account for multi-level tags and use tagName to find the right tag to put the violation on - if (currentViolations[0]?.name === 'tagOutOfPolicy' && data?.tagListName !== undefined && currentViolations[0]?.data?.tagName) { + if (currentViolations[0]?.name === CONST.VIOLATIONS.TAG_OUT_OF_POLICY && data?.tagListName !== undefined && currentViolations[0]?.data?.tagName) { + return currentViolations.filter((violation) => violation.data?.tagName === data?.tagListName); + } + + // allTagLevelsRequired has special logic because it is returned when one but not all the tags are set, + // so we need to return the violation for the tag fields without a tag set + if (currentViolations[0]?.name === CONST.VIOLATIONS.ALL_TAG_LEVELS_REQUIRED && tagValue) { return currentViolations.filter((violation) => violation.data?.tagName === data?.tagListName); } diff --git a/src/languages/en.ts b/src/languages/en.ts index 76e4d5d5a143..40c21a7fbfd0 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -689,7 +689,6 @@ export default { finished: 'Finished', sendInvoice: ({amount}: RequestAmountParams) => `Send ${amount} invoice`, submitAmount: ({amount}: RequestAmountParams) => `submit ${amount}`, - trackAmount: ({amount}: RequestAmountParams) => `track ${amount}`, submittedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `submitted ${formattedAmount}${comment ? ` for ${comment}` : ''}`, trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `tracking ${formattedAmount}${comment ? ` for ${comment}` : ''}`, splitAmount: ({amount}: SplitAmountParams) => `split ${amount}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index d03e13d1a9ff..a6d317c623e0 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -683,7 +683,6 @@ export default { finished: 'Finalizado', sendInvoice: ({amount}: RequestAmountParams) => `Enviar factura de ${amount}`, submitAmount: ({amount}: RequestAmountParams) => `solicitar ${amount}`, - trackAmount: ({amount}: RequestAmountParams) => `seguimiento de ${amount}`, submittedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `solicitó ${formattedAmount}${comment ? ` para ${comment}` : ''}`, trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `realizó un seguimiento de ${formattedAmount}${comment ? ` para ${comment}` : ''}`, splitAmount: ({amount}: SplitAmountParams) => `dividir ${amount}`, diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 50cb9a20dff6..032a8261bec9 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -380,7 +380,8 @@ function getDBTime(timestamp: string | number = ''): string { */ function getDBTimeWithSkew(timestamp: string | number = ''): string { if (networkTimeSkew > 0) { - return getDBTime(new Date(timestamp).valueOf() + networkTimeSkew); + const datetime = timestamp ? new Date(timestamp) : new Date(); + return getDBTime(datetime.valueOf() + networkTimeSkew); } return getDBTime(timestamp); } diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.tsx similarity index 91% rename from src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx rename to src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.tsx index 25bec235ed45..8be79e538ced 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.tsx @@ -1,5 +1,5 @@ import {useNavigation, useNavigationState} from '@react-navigation/native'; -import React, {useEffect} from 'react'; +import React, {useCallback, useEffect} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -12,6 +12,7 @@ import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Session from '@libs/actions/Session'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute'; import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute'; import Navigation from '@libs/Navigation/Navigation'; @@ -24,6 +25,7 @@ import * as Welcome from '@userActions/Welcome'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; @@ -36,9 +38,8 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); - const {activeWorkspaceID} = useActiveWorkspace(); - const navigation = useNavigation(); + const {activeWorkspaceID} = useActiveWorkspace(); useEffect(() => { const navigationState = navigation.getState() as State | undefined; @@ -68,13 +69,16 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps const chatTabBrickRoad = getChatTabBrickRoad(activeWorkspaceID); + const navigateToChats = useCallback(() => { + const route = activeWorkspaceID ? (`/w/${activeWorkspaceID}/home` as Route) : ROUTES.HOME; + Navigation.navigate(route); + }, [activeWorkspaceID]); + return ( { - Navigation.navigate(ROUTES.HOME); - }} + onPress={navigateToChats} role={CONST.ROLE.BUTTON} accessibilityLabel={translate('common.inbox')} wrapperStyle={styles.flex1} @@ -96,7 +100,7 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps { - Navigation.navigate(ROUTES.SEARCH.getRoute(CONST.TAB_SEARCH.ALL)); + interceptAnonymousUser(() => Navigation.navigate(ROUTES.SEARCH.getRoute(CONST.TAB_SEARCH.ALL))); }} role={CONST.ROLE.BUTTON} accessibilityLabel={translate('common.search')} diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx new file mode 100644 index 000000000000..4fecfdcb0e3e --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx @@ -0,0 +1,135 @@ +import {useNavigation, useNavigationState} from '@react-navigation/native'; +import React, {useCallback, useEffect} from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import {PressableWithFeedback} from '@components/Pressable'; +import Tooltip from '@components/Tooltip'; +import useActiveWorkspace from '@hooks/useActiveWorkspace'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Session from '@libs/actions/Session'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute'; +import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute'; +import Navigation from '@libs/Navigation/Navigation'; +import type {RootStackParamList, State} from '@libs/Navigation/types'; +import {getChatTabBrickRoad} from '@libs/WorkspacesSettingsUtils'; +import BottomTabAvatar from '@pages/home/sidebar/BottomTabAvatar'; +import BottomTabBarFloatingActionButton from '@pages/home/sidebar/BottomTabBarFloatingActionButton'; +import variables from '@styles/variables'; +import * as Welcome from '@userActions/Welcome'; +import CONST from '@src/CONST'; +import NAVIGATORS from '@src/NAVIGATORS'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Route} from '@src/ROUTES'; +import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; + +type PurposeForUsingExpensifyModalOnyxProps = { + isLoadingApp: OnyxEntry; +}; +type PurposeForUsingExpensifyModalProps = PurposeForUsingExpensifyModalOnyxProps; + +function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const navigation = useNavigation(); + const {activeWorkspaceID: contextActiveWorkspaceID} = useActiveWorkspace(); + const activeWorkspaceID = sessionStorage.getItem(CONST.SESSION_STORAGE_KEYS.ACTIVE_WORKSPACE_ID) ?? contextActiveWorkspaceID; + + useEffect(() => { + const navigationState = navigation.getState() as State | undefined; + const routes = navigationState?.routes; + const currentRoute = routes?.[navigationState?.index ?? 0]; + // When we are redirected to the Settings tab from the OldDot, we don't want to call the Welcome.show() method. + // To prevent this, the value of the bottomTabRoute?.name is checked here + if (!!(currentRoute && currentRoute.name !== NAVIGATORS.BOTTOM_TAB_NAVIGATOR && currentRoute.name !== NAVIGATORS.CENTRAL_PANE_NAVIGATOR) || Session.isAnonymousUser()) { + return; + } + + Welcome.isOnboardingFlowCompleted({onNotCompleted: () => Navigation.navigate(ROUTES.ONBOARDING_ROOT)}); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isLoadingApp]); + + // Parent navigator of the bottom tab bar is the root navigator. + const currentTabName = useNavigationState((state) => { + const topmostCentralPaneRoute = getTopmostCentralPaneRoute(state); + + if (topmostCentralPaneRoute && topmostCentralPaneRoute.name === SCREENS.SEARCH.CENTRAL_PANE) { + return SCREENS.SEARCH.CENTRAL_PANE; + } + + const topmostBottomTabRoute = getTopmostBottomTabRoute(state); + return topmostBottomTabRoute?.name ?? SCREENS.HOME; + }); + + const chatTabBrickRoad = getChatTabBrickRoad(activeWorkspaceID); + + const navigateToChats = useCallback(() => { + const route = activeWorkspaceID ? (`/w/${activeWorkspaceID}/home` as Route) : ROUTES.HOME; + Navigation.navigate(route); + }, [activeWorkspaceID]); + + return ( + + + + + + {chatTabBrickRoad && ( + + )} + + + + + { + interceptAnonymousUser(() => Navigation.navigate(ROUTES.SEARCH.getRoute(CONST.TAB_SEARCH.ALL))); + }} + role={CONST.ROLE.BUTTON} + accessibilityLabel={translate('common.search')} + wrapperStyle={styles.flex1} + style={styles.bottomTabBarItem} + > + + + + + + + + + + + ); +} + +BottomTabBar.displayName = 'BottomTabBar'; + +export default withOnyx({ + isLoadingApp: { + key: ONYXKEYS.IS_LOADING_APP, + }, +})(BottomTabBar); diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 9e1eb348451b..06a3dce8d59a 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -135,11 +135,6 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N // We want to clean saved scroll offsets for screens that aren't anymore in the state. cleanStaleScrollOffsets(state); - - // clear all window selection on navigation - // this is to prevent the selection from persisting when navigating to a new page in web - // using "?" to avoid crash in native - window?.getSelection?.()?.removeAllRanges?.(); }; return ( diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index bf68a694523a..10df3c703c92 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -1,5 +1,6 @@ import Onyx from 'react-native-onyx'; import * as ActiveClientManager from '@libs/ActiveClientManager'; +import Log from '@libs/Log'; import * as Request from '@libs/Request'; import * as RequestThrottle from '@libs/RequestThrottle'; import * as PersistedRequests from '@userActions/PersistedRequests'; @@ -26,10 +27,11 @@ let isQueuePaused = false; */ function pause() { if (isQueuePaused) { + Log.info('[SequentialQueue] Queue already paused'); return; } - console.debug('[SequentialQueue] Pausing the queue'); + Log.info('[SequentialQueue] Pausing the queue'); isQueuePaused = true; } @@ -40,6 +42,7 @@ function flushOnyxUpdatesQueue() { // The only situation where the queue is paused is if we found a gap between the app current data state and our server's. If that happens, // we'll trigger async calls to make the client updated again. While we do that, we don't want to insert anything in Onyx. if (isQueuePaused) { + Log.info('[SequentialQueue] Queue already paused'); return; } QueuedOnyxUpdates.flushQueue(); @@ -56,11 +59,18 @@ function flushOnyxUpdatesQueue() { function process(): Promise { // When the queue is paused, return early. This prevents any new requests from happening. The queue will be flushed again when the queue is unpaused. if (isQueuePaused) { + Log.info('[SequentialQueue] Unable to process. Queue is paused.'); + return Promise.resolve(); + } + + if (NetworkStore.isOffline()) { + Log.info('[SequentialQueue] Unable to process. We are offline.'); return Promise.resolve(); } const persistedRequests = PersistedRequests.getAll(); - if (persistedRequests.length === 0 || NetworkStore.isOffline()) { + if (persistedRequests.length === 0) { + Log.info('[SequentialQueue] Unable to process. No requests to process.'); return Promise.resolve(); } @@ -72,6 +82,7 @@ function process(): Promise { // A response might indicate that the queue should be paused. This happens when a gap in onyx updates is detected between the client and the server and // that gap needs resolved before the queue can continue. if (response?.shouldPauseQueue) { + Log.info("[SequentialQueue] Handled 'shouldPauseQueue' in response. Pausing the queue."); pause(); } PersistedRequests.remove(requestToProcess); @@ -102,16 +113,24 @@ function process(): Promise { function flush() { // When the queue is paused, return early. This will keep an requests in the queue and they will get flushed again when the queue is unpaused if (isQueuePaused) { + Log.info('[SequentialQueue] Unable to flush. Queue is paused.'); + return; + } + + if (isSequentialQueueRunning) { + Log.info('[SequentialQueue] Unable to flush. Queue is already running.'); return; } - if (isSequentialQueueRunning || PersistedRequests.getAll().length === 0) { + if (PersistedRequests.getAll().length === 0) { + Log.info('[SequentialQueue] Unable to flush. No requests to process.'); return; } // ONYXKEYS.PERSISTED_REQUESTS is shared across clients, thus every client/tab will have a copy // It is very important to only process the queue from leader client otherwise requests will be duplicated. if (!ActiveClientManager.isClientTheLeader()) { + Log.info('[SequentialQueue] Unable to flush. Client is not the leader.'); return; } @@ -128,6 +147,7 @@ function flush() { callback: () => { Onyx.disconnect(connectionID); process().finally(() => { + Log.info('[SequentialQueue] Finished processing queue.'); isSequentialQueueRunning = false; if (NetworkStore.isOffline() || PersistedRequests.getAll().length === 0) { resolveIsReadyPromise?.(); @@ -144,6 +164,7 @@ function flush() { */ function unpause() { if (!isQueuePaused) { + Log.info('[SequentialQueue] Unable to unpause queue. We are already processing.'); return; } diff --git a/src/libs/NetworkConnection.ts b/src/libs/NetworkConnection.ts index 2d020ec778ae..de064441047a 100644 --- a/src/libs/NetworkConnection.ts +++ b/src/libs/NetworkConnection.ts @@ -37,13 +37,13 @@ const triggerReconnectionCallbacks = throttle( * Called when the offline status of the app changes and if the network is "reconnecting" (going from offline to online) * then all of the reconnection callbacks are triggered */ -function setOfflineStatus(isCurrentlyOffline: boolean): void { - NetworkActions.setIsOffline(isCurrentlyOffline); +function setOfflineStatus(isCurrentlyOffline: boolean, reason = ''): void { + NetworkActions.setIsOffline(isCurrentlyOffline, reason); // When reconnecting, ie, going from offline to online, all the reconnection callbacks // are triggered (this is usually Actions that need to re-download data from the server) if (isOffline && !isCurrentlyOffline) { - NetworkActions.setIsBackendReachable(true); + NetworkActions.setIsBackendReachable(true, 'moved from offline to online'); triggerReconnectionCallbacks('offline status changed'); } @@ -74,13 +74,13 @@ Onyx.connect({ } shouldForceOffline = currentShouldForceOffline; if (shouldForceOffline) { - setOfflineStatus(true); + setOfflineStatus(true, 'shouldForceOffline was detected in the Onyx data'); Log.info(`[NetworkStatus] Setting "offlineStatus" to "true" because user is under force offline`); } else { // If we are no longer forcing offline fetch the NetInfo to set isOffline appropriately NetInfo.fetch().then((state) => { const isInternetReachable = !!state.isInternetReachable; - setOfflineStatus(isInternetReachable); + setOfflineStatus(isInternetReachable, 'NetInfo checked if the internet is reachable'); Log.info( `[NetworkStatus] The force-offline mode was turned off. Getting the device network status from NetInfo. Network state: ${JSON.stringify( state, @@ -122,20 +122,20 @@ function subscribeToBackendAndInternetReachability(): () => void { }) .then((isBackendReachable: boolean) => { if (isBackendReachable) { - NetworkActions.setIsBackendReachable(true); + NetworkActions.setIsBackendReachable(true, 'successfully completed API request'); return; } + NetworkActions.setIsBackendReachable(false, 'request succeeded, but internet reachability test failed'); checkInternetReachability().then((isInternetReachable: boolean) => { - setOfflineStatus(!isInternetReachable); + setOfflineStatus(!isInternetReachable, 'checkInternetReachability was called after api/ping returned a non-200 jsonCode'); setNetWorkStatus(isInternetReachable); - NetworkActions.setIsBackendReachable(false); }); }) .catch(() => { + NetworkActions.setIsBackendReachable(false, 'request failed and internet reachability test failed'); checkInternetReachability().then((isInternetReachable: boolean) => { - setOfflineStatus(!isInternetReachable); + setOfflineStatus(!isInternetReachable, 'checkInternetReachability was called after api/ping request failed'); setNetWorkStatus(isInternetReachable); - NetworkActions.setIsBackendReachable(false); }); }); }, CONST.NETWORK.BACKEND_CHECK_INTERVAL_MS); @@ -163,7 +163,7 @@ function subscribeToNetworkStatus(): () => void { Log.info('[NetworkConnection] Not setting offline status because shouldForceOffline = true'); return; } - setOfflineStatus(state.isInternetReachable === false); + setOfflineStatus(state.isInternetReachable === false, 'NetInfo received a state change event'); Log.info(`[NetworkStatus] NetInfo.addEventListener event coming, setting "offlineStatus" to ${!!state.isInternetReachable} with network state: ${JSON.stringify(state)}`); setNetWorkStatus(state.isInternetReachable); }); diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 7678de592a6f..b39c8465bd78 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -424,6 +424,13 @@ function canSendInvoice(policies: OnyxCollection): boolean { return getActiveAdminWorkspaces(policies).length > 0; } +function hasDependentTags(policy: OnyxEntry, policyTagList: OnyxEntry) { + if (!policy?.hasMultipleTagLists) { + return false; + } + return Object.values(policyTagList ?? {}).some((tagList) => Object.values(tagList.tags).some((tag) => !!tag.rules?.parentTagsFilter || !!tag.parentTagsFilter)); +} + /** Get the Xero organizations connected to the policy */ function getXeroTenants(policy: Policy | undefined): Tenant[] { // Due to the way optional chain is being handled in this useMemo we are forced to use this approach to properly handle undefined values @@ -516,6 +523,7 @@ export { shouldShowPolicy, getActiveAdminWorkspaces, canSendInvoice, + hasDependentTags, getXeroTenants, findCurrentXeroOrganization, getCurrentXeroOrganizationName, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 269f4c7bc01b..4de976aaf160 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6111,7 +6111,7 @@ function shouldDisableRename(report: OnyxEntry, policy: OnyxEntry, policy: OnyxEntry): boolean { - return PolicyUtils.isPolicyAdmin(policy) && !isAdminRoom(report) && !isArchivedRoom(report) && !isThread(report); + return PolicyUtils.isPolicyAdmin(policy) && !isAdminRoom(report) && !isArchivedRoom(report) && !isThread(report) && !isInvoiceRoom(report); } /** @@ -6164,18 +6164,27 @@ function getTaskAssigneeChatOnyxData( }, ); - successData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${assigneeChatReportID}`, - value: { - pendingFields: { - createChat: null, + // BE will send different report's participants and assigneeAccountID. We clear the optimistic ones to avoid duplicated entries + successData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${assigneeChatReportID}`, + value: { + pendingFields: { + createChat: null, + }, + isOptimisticReport: false, + participants: {[assigneeAccountID]: null}, }, - isOptimisticReport: false, - // BE will send a different participant. We clear the optimistic one to avoid duplicated entries - participants: {[assigneeAccountID]: null}, }, - }); + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: { + [assigneeAccountID]: null, + }, + }, + ); failureData.push( { diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index 2ceecb42dba5..686db5e6a6c5 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -7,7 +7,7 @@ import * as TransactionUtils from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PolicyCategories, PolicyTagList, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {PolicyCategories, PolicyTagList, Transaction, TransactionViolation, ViolationName} from '@src/types/onyx'; /** * Calculates tag out of policy and missing tag violations for the given transaction @@ -49,17 +49,41 @@ function getTagViolationsForSingleLevelTags( } /** - * Calculates some tag levels required and missing tag violations for the given transaction + * Calculates missing tag violations for policies with dependent tags */ -function getTagViolationsForMultiLevelTags( - updatedTransaction: Transaction, - transactionViolations: TransactionViolation[], - policyRequiresTags: boolean, - policyTagList: PolicyTagList, -): TransactionViolation[] { +function getTagViolationsForDependentTags(policyTagList: PolicyTagList, transactionViolations: TransactionViolation[], tagName: string) { + const tagViolations = [...transactionViolations]; + + if (!tagName) { + Object.values(policyTagList).forEach((tagList) => + tagViolations.push({ + name: CONST.VIOLATIONS.MISSING_TAG, + type: CONST.VIOLATION_TYPES.VIOLATION, + data: {tagName: tagList.name}, + }), + ); + } else { + const tags = TransactionUtils.getTagArrayFromName(tagName); + if (Object.keys(policyTagList).length !== tags.length || tags.includes('')) { + tagViolations.push({ + name: CONST.VIOLATIONS.ALL_TAG_LEVELS_REQUIRED, + type: CONST.VIOLATION_TYPES.VIOLATION, + data: {}, + }); + } + } + + return tagViolations; +} + +/** + * Calculates missing tag violations for policies with independent tags + */ +function getTagViolationForIndependentTags(policyTagList: PolicyTagList, transactionViolations: TransactionViolation[], transaction: Transaction) { const policyTagKeys = getSortedTagKeys(policyTagList); - const selectedTags = updatedTransaction.tag?.split(CONST.COLON) ?? []; + const selectedTags = transaction.tag?.split(CONST.COLON) ?? []; let newTransactionViolations = [...transactionViolations]; + newTransactionViolations = newTransactionViolations.filter( (violation) => violation.name !== CONST.VIOLATIONS.SOME_TAG_LEVELS_REQUIRED && violation.name !== CONST.VIOLATIONS.TAG_OUT_OF_POLICY, ); @@ -109,6 +133,30 @@ function getTagViolationsForMultiLevelTags( return newTransactionViolations; } +/** + * Calculates tag violations for a transaction on a policy with multi level tags + */ +function getTagViolationsForMultiLevelTags( + updatedTransaction: Transaction, + transactionViolations: TransactionViolation[], + policyTagList: PolicyTagList, + hasDependentTags: boolean, +): TransactionViolation[] { + const tagViolations = [ + CONST.VIOLATIONS.SOME_TAG_LEVELS_REQUIRED, + CONST.VIOLATIONS.TAG_OUT_OF_POLICY, + CONST.VIOLATIONS.MISSING_TAG, + CONST.VIOLATIONS.ALL_TAG_LEVELS_REQUIRED, + ] as ViolationName[]; + const filteredTransactionViolations = transactionViolations.filter((violation) => !tagViolations.includes(violation.name)); + + if (hasDependentTags) { + return getTagViolationsForDependentTags(policyTagList, filteredTransactionViolations, updatedTransaction.tag ?? ''); + } + + return getTagViolationForIndependentTags(policyTagList, filteredTransactionViolations, updatedTransaction); +} + const ViolationsUtils = { /** * Checks a transaction for policy violations and returns an object with Onyx method, key and updated transaction @@ -121,6 +169,7 @@ const ViolationsUtils = { policyTagList: PolicyTagList, policyRequiresCategories: boolean, policyCategories: PolicyCategories, + hasDependentTags: boolean, ): OnyxUpdate { const isPartialTransaction = TransactionUtils.isPartialMerchant(TransactionUtils.getMerchant(updatedTransaction)) && TransactionUtils.isAmountMissing(updatedTransaction); if (isPartialTransaction) { @@ -166,7 +215,7 @@ const ViolationsUtils = { newTransactionViolations = Object.keys(policyTagList).length === 1 ? getTagViolationsForSingleLevelTags(updatedTransaction, newTransactionViolations, policyRequiresTags, policyTagList) - : getTagViolationsForMultiLevelTags(updatedTransaction, newTransactionViolations, policyRequiresTags, policyTagList); + : getTagViolationsForMultiLevelTags(updatedTransaction, newTransactionViolations, policyTagList, hasDependentTags); } return { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 1bd4de43acfb..e21d86644c3e 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -506,6 +506,7 @@ function buildOnyxDataForMoneyRequest( ...iouReport, lastMessageText: iouAction.message?.[0]?.text, lastMessageHtml: iouAction.message?.[0]?.html, + lastVisibleActionCreated: iouAction.created, pendingFields: { ...(shouldCreateNewMoneyRequestReport ? {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : {preview: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), }, @@ -822,7 +823,15 @@ function buildOnyxDataForMoneyRequest( return [optimisticData, successData, failureData]; } - const violationsOnyxData = ViolationsUtils.getViolationsOnyxData(transaction, [], !!policy.requiresTag, policyTagList ?? {}, !!policy.requiresCategory, policyCategories ?? {}); + const violationsOnyxData = ViolationsUtils.getViolationsOnyxData( + transaction, + [], + !!policy.requiresTag, + policyTagList ?? {}, + !!policy.requiresCategory, + policyCategories ?? {}, + PolicyUtils.hasDependentTags(policy, policyTagList ?? {}), + ); if (violationsOnyxData) { optimisticData.push(violationsOnyxData); @@ -1136,7 +1145,15 @@ function buildOnyxDataForInvoice( return [optimisticData, successData, failureData]; } - const violationsOnyxData = ViolationsUtils.getViolationsOnyxData(transaction, [], !!policy.requiresTag, policyTagList ?? {}, !!policy.requiresCategory, policyCategories ?? {}); + const violationsOnyxData = ViolationsUtils.getViolationsOnyxData( + transaction, + [], + !!policy.requiresTag, + policyTagList ?? {}, + !!policy.requiresCategory, + policyCategories ?? {}, + PolicyUtils.hasDependentTags(policy, policyTagList ?? {}), + ); if (violationsOnyxData) { optimisticData.push(violationsOnyxData); @@ -1505,7 +1522,15 @@ function buildOnyxDataForTrackExpense( return [optimisticData, successData, failureData]; } - const violationsOnyxData = ViolationsUtils.getViolationsOnyxData(transaction, [], !!policy.requiresTag, policyTagList ?? {}, !!policy.requiresCategory, policyCategories ?? {}); + const violationsOnyxData = ViolationsUtils.getViolationsOnyxData( + transaction, + [], + !!policy.requiresTag, + policyTagList ?? {}, + !!policy.requiresCategory, + policyCategories ?? {}, + PolicyUtils.hasDependentTags(policy, policyTagList ?? {}), + ); if (violationsOnyxData) { optimisticData.push(violationsOnyxData); @@ -1983,6 +2008,7 @@ function getMoneyRequestInformation( reportPreviewAction = ReportUtils.updateReportPreview(iouReport, reportPreviewAction as ReportPreviewAction, false, comment, optimisticTransaction); } else { reportPreviewAction = ReportUtils.buildOptimisticReportPreview(chatReport, iouReport, comment, optimisticTransaction); + chatReport.lastVisibleActionCreated = reportPreviewAction.created; // Generated ReportPreview action is a parent report action of the iou report. // We are setting the iou report's parentReportActionID to display subtitle correctly in IOU page when offline. @@ -2675,6 +2701,7 @@ function getUpdateMoneyRequestParams( policyTagList ?? {}, !!policy.requiresCategory, policyCategories ?? {}, + PolicyUtils.hasDependentTags(policy, policyTagList ?? {}), ), ); failureData.push({ @@ -3835,6 +3862,7 @@ function createSplitsAndOnyxData( splitChatReport.lastMessageText = splitIOUReportAction.message?.[0]?.text; splitChatReport.lastMessageHtml = splitIOUReportAction.message?.[0]?.html; splitChatReport.lastActorAccountID = currentUserAccountID; + splitChatReport.lastVisibleActionCreated = splitIOUReportAction.created; let splitChatReportNotificationPreference = splitChatReport.notificationPreference; if (splitChatReportNotificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) { @@ -4639,7 +4667,7 @@ function startSplitBill({ API.write(WRITE_COMMANDS.START_SPLIT_BILL, parameters, {optimisticData, successData, failureData}); Navigation.dismissModalWithReport(splitChatReport); - Report.notifyNewAction(splitChatReport.chatReportID ?? '', currentUserAccountID); + Report.notifyNewAction(splitChatReport.reportID ?? '', currentUserAccountID); } /** Used for editing a split expense while it's still scanning or when SmartScan fails, it completes a split expense started by startSplitBill above. @@ -5130,6 +5158,7 @@ function editRegularMoneyRequest( policyTags, !!policy.requiresCategory, policyCategories, + PolicyUtils.hasDependentTags(policy, policyTags), ); optimisticData.push(updatedViolationsOnyxData); failureData.push({ diff --git a/src/libs/actions/Network.ts b/src/libs/actions/Network.ts index 9c88403b0e98..883e336d6c90 100644 --- a/src/libs/actions/Network.ts +++ b/src/libs/actions/Network.ts @@ -1,12 +1,24 @@ import Onyx from 'react-native-onyx'; +import Log from '@libs/Log'; import type {NetworkStatus} from '@libs/NetworkConnection'; import ONYXKEYS from '@src/ONYXKEYS'; -function setIsBackendReachable(isBackendReachable: boolean) { +function setIsBackendReachable(isBackendReachable: boolean, reason: string) { + if (isBackendReachable) { + Log.info(`[Network] Backend is reachable because: ${reason}`); + } else { + Log.info(`[Network] Backend is not reachable because: ${reason}`); + } Onyx.merge(ONYXKEYS.NETWORK, {isBackendReachable}); } -function setIsOffline(isOffline: boolean) { +function setIsOffline(isOffline: boolean, reason = '') { + if (reason) { + let textToLog = '[Network] Client is'; + textToLog += isOffline ? ' entering offline mode' : ' back online'; + textToLog += ` because: ${reason}`; + Log.info(textToLog); + } Onyx.merge(ONYXKEYS.NETWORK, {isOffline}); } diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 7326b70edb61..ab8ca2a1a4b6 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -469,7 +469,7 @@ function addActions(reportID: string, text = '', file?: FileObject) { const lastCommentText = ReportUtils.formatReportLastMessageText(lastComment?.text ?? ''); const optimisticReport: Partial = { - lastVisibleActionCreated: currentTime, + lastVisibleActionCreated: lastAction?.created, lastMessageTranslationKey: lastComment?.translationKey ?? '', lastMessageText: lastCommentText, lastMessageHtml: lastCommentText, @@ -1019,7 +1019,6 @@ function navigateToAndOpenReportWithAccountIDs(participantAccountIDs: number[]) */ function navigateToAndOpenChildReport(childReportID = '0', parentReportAction: Partial = {}, parentReportID = '0') { if (childReportID !== '0') { - openReport(childReportID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); } else { const participantAccountIDs = [...new Set([currentUserAccountID, Number(parentReportAction.actorAccountID)])]; diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index 55d898d3d4f3..57b551510d58 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -131,10 +131,10 @@ function createTaskAndNavigate( const optimisticAddCommentReport = ReportUtils.buildOptimisticTaskCommentReportAction(taskReportID, title, assigneeAccountID, `task for ${title}`, parentReportID); optimisticTaskReport.parentReportActionID = optimisticAddCommentReport.reportAction.reportActionID; - const currentTime = DateUtils.getDBTime(); + const currentTime = DateUtils.getDBTimeWithSkew(); const lastCommentText = ReportUtils.formatReportLastMessageText(optimisticAddCommentReport?.reportAction?.message?.[0]?.text ?? ''); const optimisticParentReport = { - lastVisibleActionCreated: currentTime, + lastVisibleActionCreated: optimisticAddCommentReport.reportAction.created, lastMessageText: lastCommentText, lastActorAccountID: currentUserAccountID, lastReadTime: currentTime, diff --git a/src/libs/shouldIgnoreSelectionWhenUpdatedManually/index.android.tsx b/src/libs/shouldIgnoreSelectionWhenUpdatedManually/index.android.tsx new file mode 100644 index 000000000000..289c56ad69be --- /dev/null +++ b/src/libs/shouldIgnoreSelectionWhenUpdatedManually/index.android.tsx @@ -0,0 +1,5 @@ +import type ShouldIgnoreSelectionWhenUpdatedManually from './types'; + +const shouldIgnoreSelectionWhenUpdatedManually: ShouldIgnoreSelectionWhenUpdatedManually = true; + +export default shouldIgnoreSelectionWhenUpdatedManually; diff --git a/src/libs/shouldIgnoreSelectionWhenUpdatedManually/index.tsx b/src/libs/shouldIgnoreSelectionWhenUpdatedManually/index.tsx new file mode 100644 index 000000000000..744a94aa1f32 --- /dev/null +++ b/src/libs/shouldIgnoreSelectionWhenUpdatedManually/index.tsx @@ -0,0 +1,5 @@ +import type ShouldIgnoreSelectionWhenUpdatedManually from './types'; + +const shouldIgnoreSelectionWhenUpdatedManually: ShouldIgnoreSelectionWhenUpdatedManually = false; + +export default shouldIgnoreSelectionWhenUpdatedManually; diff --git a/src/libs/shouldIgnoreSelectionWhenUpdatedManually/types.ts b/src/libs/shouldIgnoreSelectionWhenUpdatedManually/types.ts new file mode 100644 index 000000000000..56394183ef7d --- /dev/null +++ b/src/libs/shouldIgnoreSelectionWhenUpdatedManually/types.ts @@ -0,0 +1,3 @@ +type ShouldIgnoreSelectionWhenUpdatedManually = boolean; + +export default ShouldIgnoreSelectionWhenUpdatedManually; diff --git a/src/pages/RoomDescriptionPage.tsx b/src/pages/RoomDescriptionPage.tsx index 46a10483fd08..e95fa84c09c9 100644 --- a/src/pages/RoomDescriptionPage.tsx +++ b/src/pages/RoomDescriptionPage.tsx @@ -97,6 +97,7 @@ function RoomDescriptionPage({report, policies}: RoomDescriptionPageProps) { value={description} onChangeText={handleReportDescriptionChange} autoCapitalize="none" + isMarkdownEnabled /> diff --git a/src/pages/Search/SearchPageBottomTab.tsx b/src/pages/Search/SearchPageBottomTab.tsx index c201047033e6..663a01ba02b8 100644 --- a/src/pages/Search/SearchPageBottomTab.tsx +++ b/src/pages/Search/SearchPageBottomTab.tsx @@ -1,4 +1,5 @@ -import React from 'react'; +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useMemo} from 'react'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import ScreenWrapper from '@components/ScreenWrapper'; import Search from '@components/Search'; @@ -7,20 +8,42 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import Navigation from '@libs/Navigation/Navigation'; +import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; import TopBar from '@navigation/AppNavigator/createCustomBottomTabNavigator/TopBar'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; import type {SearchQuery} from '@src/types/onyx/SearchResults'; import SearchFilters from './SearchFilters'; +type SearchPageProps = StackScreenProps; + +const defaultSearchProps = { + query: '' as SearchQuery, + policyIDs: undefined, + sortBy: CONST.SEARCH_TABLE_COLUMNS.DATE, + sortOrder: CONST.SORT_ORDER.DESC, +}; function SearchPageBottomTab() { const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); const activeRoute = useActiveRoute(); const styles = useThemeStyles(); - const currentQuery = activeRoute?.params && 'query' in activeRoute.params ? activeRoute?.params?.query : ''; - const policyIDs = activeRoute?.params && 'policyIDs' in activeRoute.params ? (activeRoute?.params?.policyIDs as string) : undefined; - const query = currentQuery as SearchQuery; + + const { + query: rawQuery, + policyIDs, + sortBy, + sortOrder, + } = useMemo(() => { + if (activeRoute?.name !== SCREENS.SEARCH.CENTRAL_PANE || !activeRoute.params) { + return defaultSearchProps; + } + return {...defaultSearchProps, ...activeRoute.params} as SearchPageProps['route']['params']; + }, [activeRoute]); + + const query = rawQuery as SearchQuery; + const isValidQuery = Object.values(CONST.TAB_SEARCH).includes(query); const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH.getRoute(CONST.TAB_SEARCH.ALL)); @@ -45,6 +68,8 @@ function SearchPageBottomTab() { )} diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 0515ca011517..d7f4ee7f6c5c 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -775,6 +775,7 @@ function ComposerWithSuggestions( onLayout={onLayout} onScroll={hideSuggestionMenu} shouldContainScroll={Browser.isMobileSafari()} + isGroupPolicyReport={isGroupPolicyReport} /> diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index c700fea4fb85..940feb99bc27 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -210,12 +210,9 @@ function ReportActionsList({ [sortedReportActions, isOffline], ); - // whisper action doesn't affect lastVisibleActionCreated, so we should not take it into account while checking if there is the newest report action - const newestVisibleReportAction = useMemo(() => sortedVisibleReportActions.find((item) => !ReportActionsUtils.isWhisperAction(item)) ?? null, [sortedVisibleReportActions]); - const lastActionIndex = sortedVisibleReportActions[0]?.reportActionID; const reportActionSize = useRef(sortedVisibleReportActions.length); - const hasNewestReportAction = newestVisibleReportAction?.created === report.lastVisibleActionCreated; + const hasNewestReportAction = sortedVisibleReportActions[0]?.created === report.lastVisibleActionCreated; const hasNewestReportActionRef = useRef(hasNewestReportAction); hasNewestReportActionRef.current = hasNewestReportAction; const previousLastIndex = useRef(lastActionIndex); diff --git a/src/pages/home/sidebar/AvatarWithOptionalStatus.tsx b/src/pages/home/sidebar/AvatarWithOptionalStatus.tsx index 609e4044002e..5d4af7ea4961 100644 --- a/src/pages/home/sidebar/AvatarWithOptionalStatus.tsx +++ b/src/pages/home/sidebar/AvatarWithOptionalStatus.tsx @@ -19,12 +19,14 @@ function AvatarWithOptionalStatus({emojiStatus = '', isSelected = false}: Avatar - - {emojiStatus} - + + + {emojiStatus} + + ); diff --git a/src/pages/iou/MoneyRequestAmountForm.tsx b/src/pages/iou/MoneyRequestAmountForm.tsx index 5bbc9d22a97f..ce1cddc91958 100644 --- a/src/pages/iou/MoneyRequestAmountForm.tsx +++ b/src/pages/iou/MoneyRequestAmountForm.tsx @@ -228,19 +228,7 @@ function MoneyRequestAmountForm( ); const buttonText: string = useMemo(() => { - const currentAmount = moneyRequestAmountInput.current?.getAmount() ?? ''; if (skipConfirmation) { - if (currentAmount !== '') { - const currencyAmount = CurrencyUtils.convertToDisplayString(CurrencyUtils.convertToBackendAmount(Number.parseFloat(currentAmount)), currency) ?? ''; - let text = translate('iou.submitAmount', {amount: currencyAmount}); - if (iouType === CONST.IOU.TYPE.SPLIT) { - text = translate('iou.splitAmount', {amount: currencyAmount}); - } else if (iouType === CONST.IOU.TYPE.TRACK) { - text = translate('iou.trackAmount', {amount: currencyAmount}); - } - return text[0].toUpperCase() + text.slice(1); - } - if (iouType === CONST.IOU.TYPE.SPLIT) { return translate('iou.splitExpense'); } @@ -250,7 +238,7 @@ function MoneyRequestAmountForm( return translate('iou.submitExpense'); } return isEditing ? translate('common.save') : translate('common.next'); - }, [skipConfirmation, iouType, currency, isEditing, translate]); + }, [skipConfirmation, iouType, isEditing, translate]); const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); diff --git a/src/pages/workspace/WorkspaceNewRoomPage.tsx b/src/pages/workspace/WorkspaceNewRoomPage.tsx index 393afcd25eb8..5b265ff400e8 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.tsx +++ b/src/pages/workspace/WorkspaceNewRoomPage.tsx @@ -295,6 +295,7 @@ function WorkspaceNewRoomPage({policies, reports, formState, session, activePoli maxLength={CONST.REPORT_DESCRIPTION.MAX_LENGTH} autoCapitalize="none" shouldInterceptSwipe + isMarkdownEnabled /> diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index 58288f213818..738a0663b94b 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -106,7 +106,7 @@ function WorkspacePageWithSections({ reimbursementAccount = CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA, route, shouldUseScrollView = false, - shouldSkipVBBACall = false, + shouldSkipVBBACall = true, shouldShowBackButton = false, user, shouldShowLoading = true, diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index 36d5bfbbb75b..aabe99019c4f 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -125,6 +125,7 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi shouldShowOfflineIndicatorInWideScreen shouldShowNonAdmin icon={Illustrations.House} + shouldSkipVBBACall={false} > {(hasVBA?: boolean) => ( diff --git a/src/pages/workspace/bills/WorkspaceBillsPage.tsx b/src/pages/workspace/bills/WorkspaceBillsPage.tsx index 9e1810d74793..cdd733d59141 100644 --- a/src/pages/workspace/bills/WorkspaceBillsPage.tsx +++ b/src/pages/workspace/bills/WorkspaceBillsPage.tsx @@ -23,6 +23,7 @@ function WorkspaceBillsPage({route}: WorkspaceBillsPageProps) { shouldUseScrollView headerText={translate('workspace.common.bills')} route={route} + shouldSkipVBBACall={false} guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_BILLS} shouldShowOfflineIndicatorInWideScreen > diff --git a/src/pages/workspace/card/WorkspaceCardPage.tsx b/src/pages/workspace/card/WorkspaceCardPage.tsx index eeec0edf106b..c79a335376d6 100644 --- a/src/pages/workspace/card/WorkspaceCardPage.tsx +++ b/src/pages/workspace/card/WorkspaceCardPage.tsx @@ -24,6 +24,7 @@ function WorkspaceCardPage({route}: WorkspaceCardPageProps) { shouldUseScrollView headerText={translate('workspace.common.card')} route={route} + shouldSkipVBBACall={false} guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_CARD} shouldShowOfflineIndicatorInWideScreen > diff --git a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx index ef0a73788dfa..f1a973d5c47d 100644 --- a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx +++ b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx @@ -23,6 +23,7 @@ function WorkspaceInvoicesPage({route}: WorkspaceInvoicesPageProps) { headerText={translate('workspace.common.invoices')} guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_INVOICES} shouldShowOfflineIndicatorInWideScreen + shouldSkipVBBACall={false} route={route} > {(hasVBA?: boolean, policyID?: string) => ( diff --git a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/InitialPage.tsx b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/InitialPage.tsx index 0a384728f00d..08da904dec20 100644 --- a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/InitialPage.tsx +++ b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/InitialPage.tsx @@ -101,7 +101,6 @@ function WorkspaceRateAndUnitPage(props: WorkspaceRateAndUnitPageProps) { headerText={translate('workspace.reimburse.trackDistance')} route={props.route} guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_REIMBURSE} - shouldSkipVBBACall backButtonRoute="" shouldShowLoading={false} shouldShowBackButton diff --git a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage.tsx b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage.tsx index 1acd3f501314..ec37d1d110b1 100644 --- a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage.tsx +++ b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage.tsx @@ -64,7 +64,6 @@ function WorkspaceRatePage(props: WorkspaceRatePageProps) { headerText={translate('workspace.reimburse.trackDistanceRate')} route={props.route} guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_REIMBURSE} - shouldSkipVBBACall backButtonRoute={ROUTES.WORKSPACE_RATE_AND_UNIT.getRoute(props.policy?.id ?? '')} shouldShowLoading={false} shouldShowBackButton diff --git a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/UnitPage.tsx b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/UnitPage.tsx index 257c29572d71..e8430cab60bb 100644 --- a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/UnitPage.tsx +++ b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/UnitPage.tsx @@ -52,7 +52,6 @@ function WorkspaceUnitPage(props: WorkspaceUnitPageProps) { headerText={translate('workspace.reimburse.trackDistanceUnit')} route={props.route} guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_REIMBURSE} - shouldSkipVBBACall backButtonRoute={ROUTES.WORKSPACE_RATE_AND_UNIT.getRoute(props.policy?.id ?? '')} shouldShowLoading={false} shouldShowBackButton diff --git a/src/pages/workspace/reimburse/WorkspaceReimbursePage.tsx b/src/pages/workspace/reimburse/WorkspaceReimbursePage.tsx index 6bb15a8a17ce..d929ec10748a 100644 --- a/src/pages/workspace/reimburse/WorkspaceReimbursePage.tsx +++ b/src/pages/workspace/reimburse/WorkspaceReimbursePage.tsx @@ -20,7 +20,6 @@ function WorkspaceReimbursePage({route, policy}: WorkspaceReimbursePageProps) { headerText={translate('workspace.common.reimburse')} route={route} guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_REIMBURSE} - shouldSkipVBBACall shouldShowLoading={false} shouldShowOfflineIndicatorInWideScreen > diff --git a/src/pages/workspace/travel/WorkspaceTravelPage.tsx b/src/pages/workspace/travel/WorkspaceTravelPage.tsx index 1acae9e6d359..eb61397d10b2 100644 --- a/src/pages/workspace/travel/WorkspaceTravelPage.tsx +++ b/src/pages/workspace/travel/WorkspaceTravelPage.tsx @@ -25,6 +25,7 @@ function WorkspaceTravelPage({route}: WorkspaceTravelPageProps) { route={route} guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_TRAVEL} shouldShowOfflineIndicatorInWideScreen + shouldSkipVBBACall={false} > {(hasVBA, policyID) => ( diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index 25df6538bb01..ae55b00653ae 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -289,7 +289,6 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_WORKFLOWS} shouldShowOfflineIndicatorInWideScreen shouldShowNotFoundPage={!isPaidGroupPolicy || !isPolicyAdmin} - shouldSkipVBBACall isLoading={isLoading} shouldShowLoading={isLoading} shouldUseScrollView diff --git a/src/styles/index.ts b/src/styles/index.ts index 7b49b90505ba..d0cc09a9ccf0 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -4396,6 +4396,15 @@ const styles = (theme: ThemeColors) => emojiStatusLHN: { fontSize: 9, + ...(Browser.getBrowser() && !Browser.isMobile() && {transform: 'scale(.5)', fontSize: 22, overflow: 'visible'}), + ...(Browser.getBrowser() && + Browser.isSafari() && + !Browser.isMobile() && { + transform: 'scale(0.7)', + fontSize: 13, + lineHeight: 15, + overflow: 'visible', + }), }, onboardingVideoPlayer: { @@ -4423,6 +4432,7 @@ const styles = (theme: ThemeColors) => bottom: -4, borderColor: theme.highlightBG, borderWidth: 2, + overflow: 'hidden', }, moneyRequestViewImage: { ...spacing.mh5, diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts index 6b2a2bf5ffd6..467ba3271981 100644 --- a/src/types/onyx/PolicyTag.ts +++ b/src/types/onyx/PolicyTag.ts @@ -17,6 +17,21 @@ type PolicyTag = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** A list of errors keyed by microtime */ errors?: OnyxCommon.Errors | null; + + /** The rules applied to the policy tag */ + rules?: { + /** + * String representation of regex to match against parent tag. Eg, if San Francisco is a child tag of California + * its parentTagsFilter will be ^California$ + */ + parentTagsFilter?: string; + }; + + /** + * String representation of regex to match against parent tag. Eg, if San Francisco is a child tag of California + * its parentTagsFilter will be ^California$ + */ + parentTagsFilter?: string; }>; /** Record of policy tags, indexed by their name */ diff --git a/src/types/onyx/UserMetadata.ts b/src/types/onyx/UserMetadata.ts index 14181b33c4b4..a8b34cb29401 100644 --- a/src/types/onyx/UserMetadata.ts +++ b/src/types/onyx/UserMetadata.ts @@ -11,6 +11,8 @@ type UserMetadata = { /** User's account ID */ accountID?: number; + + /** Type of environment the user is using (staging or production) */ environment?: string; }; diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts index b967617918c1..5e8f93a56dbc 100644 --- a/tests/unit/ViolationUtilsTest.ts +++ b/tests/unit/ViolationUtilsTest.ts @@ -43,7 +43,7 @@ describe('getViolationsOnyxData', () => { }); it('should return an object with correct shape and with empty transactionViolations array', () => { - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false); expect(result).toEqual({ onyxMethod: Onyx.METHOD.SET, @@ -57,7 +57,7 @@ describe('getViolationsOnyxData', () => { {name: 'duplicatedTransaction', type: CONST.VIOLATION_TYPES.VIOLATION}, {name: 'receiptRequired', type: CONST.VIOLATION_TYPES.VIOLATION}, ]; - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false); expect(result.value).toEqual(expect.arrayContaining(transactionViolations)); }); @@ -70,24 +70,32 @@ describe('getViolationsOnyxData', () => { it('should add missingCategory violation if no category is included', () => { transaction.category = undefined; - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false); expect(result.value).toEqual(expect.arrayContaining([missingCategoryViolation, ...transactionViolations])); }); it('should add categoryOutOfPolicy violation when category is not in policy', () => { transaction.category = 'Bananas'; - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false); expect(result.value).toEqual(expect.arrayContaining([categoryOutOfPolicyViolation, ...transactionViolations])); }); it('should not include a categoryOutOfPolicy violation when category is in policy', () => { - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false); expect(result.value).not.toContainEqual(categoryOutOfPolicyViolation); }); it('should not add a category violation when the transaction is partial', () => { const partialTransaction = {...transaction, amount: 0, merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, category: undefined}; - const result = ViolationsUtils.getViolationsOnyxData(partialTransaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + const result = ViolationsUtils.getViolationsOnyxData( + partialTransaction, + transactionViolations, + policyRequiresTags, + policyTags, + policyRequiresCategories, + policyCategories, + false, + ); expect(result.value).not.toContainEqual(missingCategoryViolation); }); @@ -98,7 +106,7 @@ describe('getViolationsOnyxData', () => { {name: 'receiptRequired', type: CONST.VIOLATION_TYPES.VIOLATION}, ]; - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false); expect(result.value).toEqual(expect.arrayContaining([categoryOutOfPolicyViolation, ...transactionViolations])); }); @@ -110,7 +118,7 @@ describe('getViolationsOnyxData', () => { {name: 'receiptRequired', type: CONST.VIOLATION_TYPES.VIOLATION}, ]; - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false); expect(result.value).toEqual(expect.arrayContaining([missingCategoryViolation, ...transactionViolations])); }); @@ -122,7 +130,7 @@ describe('getViolationsOnyxData', () => { }); it('should not add any violations when categories are not required', () => { - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false); expect(result.value).not.toContainEqual([categoryOutOfPolicyViolation]); expect(result.value).not.toContainEqual([missingCategoryViolation]); @@ -147,7 +155,7 @@ describe('getViolationsOnyxData', () => { }); it("shouldn't update the transactionViolations if the policy requires tags and the transaction has a tag from the policy", () => { - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false); expect(result.value).toEqual(transactionViolations); }); @@ -155,7 +163,7 @@ describe('getViolationsOnyxData', () => { it('should add a missingTag violation if none is provided and policy requires tags', () => { transaction.tag = undefined; - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false); expect(result.value).toEqual(expect.arrayContaining([{...missingTagViolation}])); }); @@ -163,14 +171,22 @@ describe('getViolationsOnyxData', () => { it('should add a tagOutOfPolicy violation when policy requires tags and tag is not in the policy', () => { policyTags = {}; - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false); expect(result.value).toEqual([]); }); it('should not add a tag violation when the transaction is partial', () => { const partialTransaction = {...transaction, amount: 0, merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, tag: undefined}; - const result = ViolationsUtils.getViolationsOnyxData(partialTransaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + const result = ViolationsUtils.getViolationsOnyxData( + partialTransaction, + transactionViolations, + policyRequiresTags, + policyTags, + policyRequiresCategories, + policyCategories, + false, + ); expect(result.value).not.toContainEqual(missingTagViolation); }); @@ -181,7 +197,7 @@ describe('getViolationsOnyxData', () => { {name: 'receiptRequired', type: CONST.VIOLATION_TYPES.VIOLATION}, ]; - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false); expect(result.value).toEqual(expect.arrayContaining([{...tagOutOfPolicyViolation}, ...transactionViolations])); }); @@ -193,7 +209,7 @@ describe('getViolationsOnyxData', () => { {name: 'receiptRequired', type: CONST.VIOLATION_TYPES.VIOLATION}, ]; - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false); expect(result.value).toEqual(expect.arrayContaining([{...missingTagViolation}, ...transactionViolations])); }); @@ -205,7 +221,7 @@ describe('getViolationsOnyxData', () => { }); it('should not add any violations when tags are not required', () => { - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false); expect(result.value).not.toContainEqual([tagOutOfPolicyViolation]); expect(result.value).not.toContainEqual([missingTagViolation]); @@ -260,32 +276,40 @@ describe('getViolationsOnyxData', () => { }; // Test case where transaction has no tags - let result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + let result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false); expect(result.value).toEqual([someTagLevelsRequiredViolation]); // Test case where transaction has 1 tag transaction.tag = 'Africa'; someTagLevelsRequiredViolation.data = {errorIndexes: [1, 2]}; - result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false); expect(result.value).toEqual([someTagLevelsRequiredViolation]); // Test case where transaction has 2 tags transaction.tag = 'Africa::Project1'; someTagLevelsRequiredViolation.data = {errorIndexes: [1]}; - result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false); expect(result.value).toEqual([someTagLevelsRequiredViolation]); // Test case where transaction has all tags transaction.tag = 'Africa:Accounting:Project1'; - result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false); expect(result.value).toEqual([]); }); it('should return tagOutOfPolicy when a tag is not enabled in the policy but is set in the transaction', () => { policyTags.Department.tags.Accounting.enabled = false; transaction.tag = 'Africa:Accounting:Project1'; - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false); const violation = {...tagOutOfPolicyViolation, data: {tagName: 'Department'}}; expect(result.value).toEqual([violation]); }); + it('should return missingTag when all dependent tags are enabled in the policy but are not set in the transaction', () => { + const missingDepartmentTag = {...missingTagViolation, data: {tagName: 'Department'}}; + const missingRegionTag = {...missingTagViolation, data: {tagName: 'Region'}}; + const missingProjectTag = {...missingTagViolation, data: {tagName: 'Project'}}; + transaction.tag = undefined; + const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, true); + expect(result.value).toEqual(expect.arrayContaining([missingDepartmentTag, missingRegionTag, missingProjectTag])); + }); }); });