diff --git a/android/app/build.gradle b/android/app/build.gradle
index 0b2271d16716..62316cf4cf38 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -98,8 +98,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001045605
- versionName "1.4.56-5"
+ versionCode 1001045700
+ versionName "1.4.57-0"
}
flavorDimensions "default"
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 37741d484d63..071a7f4802da 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.4.56
+ 1.4.57
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.56.5
+ 1.4.57.0
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index e228808076d0..c12ea4bfdf91 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.4.56
+ 1.4.57
CFBundleSignature
????
CFBundleVersion
- 1.4.56.5
+ 1.4.57.0
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 11f83011d47a..afa6db80106f 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName
$(PRODUCT_NAME)
CFBundleShortVersionString
- 1.4.56
+ 1.4.57
CFBundleVersion
- 1.4.56.5
+ 1.4.57.0
NSExtension
NSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index 6e7f23674852..34c2b335abeb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.56-5",
+ "version": "1.4.57-0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.56-5",
+ "version": "1.4.57-0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 83c3095585be..453310e2859a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.56-5",
+ "version": "1.4.57-0",
"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.",
diff --git a/src/components/DistanceEReceipt.tsx b/src/components/DistanceEReceipt.tsx
index fda0c5441734..20b927913bfb 100644
--- a/src/components/DistanceEReceipt.tsx
+++ b/src/components/DistanceEReceipt.tsx
@@ -15,9 +15,9 @@ import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import ImageSVG from './ImageSVG';
import PendingMapView from './MapView/PendingMapView';
+import ReceiptImage from './ReceiptImage';
import ScrollView from './ScrollView';
import Text from './Text';
-import ThumbnailImage from './ThumbnailImage';
type DistanceEReceiptProps = {
/** The transaction for the distance request */
@@ -30,7 +30,7 @@ function DistanceEReceipt({transaction}: DistanceEReceiptProps) {
const thumbnail = TransactionUtils.hasReceipt(transaction) ? ReceiptUtils.getThumbnailAndImageURIs(transaction).thumbnail : null;
const {amount: transactionAmount, currency: transactionCurrency, merchant: transactionMerchant, created: transactionDate} = ReportUtils.getTransactionDetails(transaction) ?? {};
const formattedTransactionAmount = CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency);
- const thumbnailSource = tryResolveUrlFromApiRoot((thumbnail as string) || '');
+ const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail ?? '');
const waypoints = useMemo(() => transaction?.comment?.waypoints ?? {}, [transaction?.comment?.waypoints]);
const sortedWaypoints = useMemo(
() =>
@@ -58,11 +58,9 @@ function DistanceEReceipt({transaction}: DistanceEReceiptProps) {
{TransactionUtils.isFetchingWaypointsFromServer(transaction) || !thumbnailSource ? (
) : (
-
)}
diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx
index 023dcc16e696..63889f76e67c 100644
--- a/src/components/EReceiptThumbnail.tsx
+++ b/src/components/EReceiptThumbnail.tsx
@@ -5,6 +5,7 @@ import {withOnyx} from 'react-native-onyx';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ReportUtils from '@libs/ReportUtils';
+import colors from '@styles/theme/colors';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -14,6 +15,7 @@ import * as eReceiptBGs from './Icon/EReceiptBGs';
import * as Expensicons from './Icon/Expensicons';
import * as MCCIcons from './Icon/MCCIcons';
import Image from './Image';
+import Text from './Text';
type EReceiptThumbnailOnyxProps = {
transaction: OnyxEntry;
@@ -26,6 +28,15 @@ type EReceiptThumbnailProps = EReceiptThumbnailOnyxProps & {
// eslint-disable-next-line react/no-unused-prop-types
transactionID: string;
+ /** Border radius to be applied on the parent view. */
+ borderRadius?: number;
+
+ /** The file extension of the receipt that the preview thumbnail is being displayed for. */
+ fileExtension?: string;
+
+ /** Whether it is a receipt thumbnail we are displaying. */
+ isReceiptThumbnail?: boolean;
+
/** Center the eReceipt Icon vertically */
centerIconV?: boolean;
@@ -42,13 +53,14 @@ const backgroundImages = {
[CONST.ERECEIPT_COLORS.PINK]: eReceiptBGs.EReceiptBG_Pink,
};
-function EReceiptThumbnail({transaction, centerIconV = true, iconSize = 'large'}: EReceiptThumbnailProps) {
+function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptThumbnail = false, centerIconV = true, iconSize = 'large'}: EReceiptThumbnailProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
+ const colorCode = isReceiptThumbnail ? StyleUtils.getFileExtensionColorCode(fileExtension) : StyleUtils.getEReceiptColorCode(transaction);
- const backgroundImage = useMemo(() => backgroundImages[StyleUtils.getEReceiptColorCode(transaction)], [StyleUtils, transaction]);
+ const backgroundImage = useMemo(() => backgroundImages[colorCode], [colorCode]);
- const colorStyles = StyleUtils.getEReceiptColorStyles(StyleUtils.getEReceiptColorCode(transaction));
+ const colorStyles = StyleUtils.getEReceiptColorStyles(colorCode);
const primaryColor = colorStyles?.backgroundColor;
const secondaryColor = colorStyles?.color;
const transactionDetails = ReportUtils.getTransactionDetails(transaction);
@@ -58,15 +70,21 @@ function EReceiptThumbnail({transaction, centerIconV = true, iconSize = 'large'}
let receiptIconWidth: number = variables.eReceiptIconWidth;
let receiptIconHeight: number = variables.eReceiptIconHeight;
let receiptMCCSize: number = variables.eReceiptMCCHeightWidth;
+ let labelFontSize: number = variables.fontSizeNormal;
+ let labelLineHeight: number = variables.lineHeightLarge;
if (iconSize === 'small') {
receiptIconWidth = variables.eReceiptIconWidthSmall;
receiptIconHeight = variables.eReceiptIconHeightSmall;
receiptMCCSize = variables.eReceiptMCCHeightWidthSmall;
+ labelFontSize = variables.fontSizeExtraSmall;
+ labelLineHeight = variables.lineHeightXSmall;
} else if (iconSize === 'medium') {
receiptIconWidth = variables.eReceiptIconWidthMedium;
receiptIconHeight = variables.eReceiptIconHeightMedium;
receiptMCCSize = variables.eReceiptMCCHeightWidthMedium;
+ labelFontSize = variables.fontSizeLabel;
+ labelLineHeight = variables.lineHeightNormal;
}
return (
@@ -77,6 +95,7 @@ function EReceiptThumbnail({transaction, centerIconV = true, iconSize = 'large'}
styles.overflowHidden,
styles.alignItemsCenter,
centerIconV ? styles.justifyContentCenter : {},
+ borderRadius ? {borderRadius} : {},
]}
>
- {MCCIcon ? (
+ {isReceiptThumbnail && fileExtension && (
+
+ {fileExtension.toUpperCase()}
+
+ )}
+ {MCCIcon && !isReceiptThumbnail ? (
({
transaction: {
key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
},
})(EReceiptThumbnail);
-export type {EReceiptThumbnailProps, EReceiptThumbnailOnyxProps};
+export type {IconSize, EReceiptThumbnailProps, EReceiptThumbnailOnyxProps};
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.js b/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.js
index 7caab5e6fb55..c6f9f601f4df 100644
--- a/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.js
+++ b/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.js
@@ -24,7 +24,12 @@ const useEmojiPickerMenu = () => {
const [preferredSkinTone] = usePreferredEmojiSkinTone();
const {windowHeight} = useWindowDimensions();
const StyleUtils = useStyleUtils();
- const listStyle = StyleUtils.getEmojiPickerListHeight(isListFiltered, windowHeight);
+ /**
+ * At EmojiPicker has set innerContainerStyle with maxHeight: '95%' by styles.popoverInnerContainer
+ * to avoid the list style to be cut off due to the list height being larger than the container height
+ * so we need to calculate listStyle based on the height of the window and innerContainerStyle at the EmojiPicker
+ */
+ const listStyle = StyleUtils.getEmojiPickerListHeight(isListFiltered, windowHeight * 0.95);
useEffect(() => {
setFilteredEmojis(allEmojis);
diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx
index 4550a7aef5d2..79e2c5b12a12 100755
--- a/src/components/MoneyRequestConfirmationList.tsx
+++ b/src/components/MoneyRequestConfirmationList.tsx
@@ -35,10 +35,10 @@ import ButtonWithDropdownMenu from './ButtonWithDropdownMenu';
import type {DropdownOption} from './ButtonWithDropdownMenu/types';
import ConfirmedRoute from './ConfirmedRoute';
import FormHelpMessage from './FormHelpMessage';
-import Image from './Image';
import MenuItemWithTopDescription from './MenuItemWithTopDescription';
import OptionsSelector from './OptionsSelector';
import ReceiptEmptyState from './ReceiptEmptyState';
+import ReceiptImage from './ReceiptImage';
import SettlementButton from './SettlementButton';
import ShowMoreButton from './ShowMoreButton';
import Switch from './Switch';
@@ -577,8 +577,12 @@ function MoneyRequestConfirmationList({
);
}, [isReadOnly, iouType, bankAccountRoute, iouCurrencyCode, policyID, selectedParticipants.length, confirm, splitOrRequestOptions, formError, styles.ph1, styles.mb2]);
- const {image: receiptImage, thumbnail: receiptThumbnail} =
- receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : ({} as ReceiptUtils.ThumbnailAndImageURI);
+ const {
+ image: receiptImage,
+ thumbnail: receiptThumbnail,
+ isThumbnail,
+ fileExtension,
+ } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : ({} as ReceiptUtils.ThumbnailAndImageURI);
return (
// @ts-expect-error This component is deprecated and will not be migrated to TypeScript (context: https://expensify.slack.com/archives/C01GTK53T8Q/p1709232289899589?thread_ts=1709156803.359359&cid=C01GTK53T8Q)
)}
-
+ {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */}
{receiptImage || receiptThumbnail ? (
-
) : (
// The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate")
diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
index f27cd507d668..513fdbfb1fd5 100755
--- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
+++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
@@ -37,12 +37,12 @@ import ConfirmedRoute from './ConfirmedRoute';
import ConfirmModal from './ConfirmModal';
import FormHelpMessage from './FormHelpMessage';
import * as Expensicons from './Icon/Expensicons';
-import Image from './Image';
import MenuItemWithTopDescription from './MenuItemWithTopDescription';
import optionPropTypes from './optionPropTypes';
import OptionsSelector from './OptionsSelector';
import PDFThumbnail from './PDFThumbnail';
import ReceiptEmptyState from './ReceiptEmptyState';
+import ReceiptImage from './ReceiptImage';
import SettlementButton from './SettlementButton';
import Switch from './Switch';
import tagPropTypes from './tagPropTypes';
@@ -897,6 +897,8 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
const {
image: receiptImage,
thumbnail: receiptThumbnail,
+ isThumbnail,
+ fileExtension,
isLocalFile,
} = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : {};
@@ -911,16 +913,18 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
onPassword={() => setIsAttachmentInvalid(true)}
/>
) : (
-
),
- [receiptFilename, receiptImage, styles, receiptThumbnail, isLocalFile, isAttachmentInvalid],
+ [isLocalFile, receiptFilename, receiptImage, styles.moneyRequestImage, isAttachmentInvalid, isThumbnail, receiptThumbnail, fileExtension],
);
return (
diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx
new file mode 100644
index 000000000000..08892f11b021
--- /dev/null
+++ b/src/components/ReceiptImage.tsx
@@ -0,0 +1,136 @@
+import React from 'react';
+import {View} from 'react-native';
+import useThemeStyles from '@hooks/useThemeStyles';
+import type IconAsset from '@src/types/utils/IconAsset';
+import EReceiptThumbnail from './EReceiptThumbnail';
+import type {IconSize} from './EReceiptThumbnail';
+import Image from './Image';
+import PDFThumbnail from './PDFThumbnail';
+import ThumbnailImage from './ThumbnailImage';
+
+type Style = {height: number; borderRadius: number; margin: number};
+
+type ReceiptImageProps = (
+ | {
+ /** Transaction ID of the transaction the receipt belongs to */
+ transactionID: string;
+
+ /** Whether it is EReceipt */
+ isEReceipt: boolean;
+
+ /** Whether it is receipt preview thumbnail we are displaying */
+ isThumbnail?: boolean;
+
+ /** Url of the receipt image */
+ source?: string;
+
+ /** Whether it is a pdf thumbnail we are displaying */
+ isPDFThumbnail?: boolean;
+ }
+ | {
+ transactionID?: string;
+ isEReceipt?: boolean;
+ isThumbnail: boolean;
+ source?: string;
+ isPDFThumbnail?: boolean;
+ }
+ | {
+ transactionID?: string;
+ isEReceipt?: boolean;
+ isThumbnail?: boolean;
+ source: string;
+ isPDFThumbnail?: boolean;
+ }
+ | {
+ transactionID?: string;
+ isEReceipt?: boolean;
+ isThumbnail?: boolean;
+ source: string;
+ isPDFThumbnail: string;
+ }
+) & {
+ /** Whether we should display the receipt with ThumbnailImage component */
+ shouldUseThumbnailImage?: boolean;
+
+ /** Whether the receipt image requires an authToken */
+ isAuthTokenRequired?: boolean;
+
+ /** Any additional styles to apply */
+ style?: Style;
+
+ /** The file extension of the receipt file */
+ fileExtension?: string;
+
+ /** number of images displayed in the same parent container */
+ iconSize?: IconSize;
+
+ /** If the image fails to load – show the provided fallback icon */
+ fallbackIcon?: IconAsset;
+
+ /** The size of the fallback icon */
+ fallbackIconSize?: number;
+};
+
+function ReceiptImage({
+ transactionID,
+ isPDFThumbnail = false,
+ isThumbnail = false,
+ shouldUseThumbnailImage = false,
+ isEReceipt = false,
+ source,
+ isAuthTokenRequired,
+ style,
+ fileExtension,
+ iconSize,
+ fallbackIcon,
+ fallbackIconSize,
+}: ReceiptImageProps) {
+ const styles = useThemeStyles();
+
+ if (isPDFThumbnail) {
+ return (
+
+ );
+ }
+
+ if (isEReceipt || isThumbnail) {
+ const props = isThumbnail && {borderRadius: style?.borderRadius, fileExtension, isReceiptThumbnail: true};
+ return (
+
+
+
+ );
+ }
+
+ if (shouldUseThumbnailImage) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
+
+export type {ReceiptImageProps};
+export default ReceiptImage;
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index bb0308ee4509..5c382ca8ee33 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -259,6 +259,8 @@ function MoneyRequestView({
) : (
{
- if (thumbnail) {
- return typeof thumbnail === 'string' ? {uri: thumbnail} : thumbnail;
- }
-
- return typeof image === 'string' ? {uri: image} : image;
- }, [image, thumbnail]);
+ let propsObj: ReceiptImageProps;
if (isEReceipt) {
- receiptImageComponent = (
-
-
-
- );
+ propsObj = {isEReceipt: true, transactionID: transaction.transactionID, iconSize: isSingleImage ? 'medium' : ('small' as IconSize)};
} else if (thumbnail && !isLocalFile) {
- receiptImageComponent = (
-
- );
+ propsObj = {
+ shouldUseThumbnailImage: true,
+ source: thumbnailSource,
+ fallbackIcon: Expensicons.Receipt,
+ fallbackIconSize: isSingleImage ? variables.iconSizeSuperLarge : variables.iconSizeExtraLarge,
+ };
} else if (isLocalFile && filename && Str.isPDF(filename) && typeof attachmentModalSource === 'string') {
- receiptImageComponent = (
-
- );
+ propsObj = {isPDFThumbnail: true, source: attachmentModalSource};
} else {
- receiptImageComponent = (
-
- );
+ propsObj = {
+ isThumbnail,
+ ...(isThumbnail && {iconSize: (isSingleImage ? 'medium' : 'small') as IconSize, fileExtension}),
+ source: thumbnail ?? image ?? '',
+ };
}
if (enablePreviewModal) {
@@ -113,14 +102,14 @@ function ReportActionItemImage({thumbnail, image, enablePreviewModal = false, tr
accessibilityLabel={translate('accessibilityHints.viewAttachment')}
accessibilityRole={CONST.ROLE.BUTTON}
>
- {receiptImageComponent}
+
)}
);
}
- return receiptImageComponent;
+ return ;
}
ReportActionItemImage.displayName = 'ReportActionItemImage';
diff --git a/src/components/ReportActionItem/ReportActionItemImages.tsx b/src/components/ReportActionItem/ReportActionItemImages.tsx
index c66dc36d1ed5..ee8cb0849ca0 100644
--- a/src/components/ReportActionItem/ReportActionItemImages.tsx
+++ b/src/components/ReportActionItem/ReportActionItemImages.tsx
@@ -65,21 +65,23 @@ function ReportActionItemImages({images, size, total, isHovered = false}: Report
return (
- {shownImages.map(({thumbnail, image, transaction, isLocalFile, filename}, index) => {
+ {shownImages.map(({thumbnail, isThumbnail, image, transaction, isLocalFile, fileExtension, filename}, index) => {
// Show a border to separate multiple images. Shown to the right for each except the last.
const shouldShowBorder = shownImages.length > 1 && index < shownImages.length - 1;
const borderStyle = shouldShowBorder ? styles.reportActionItemImageBorder : {};
return (
diff --git a/src/components/TagPicker/index.js b/src/components/TagPicker/index.tsx
similarity index 53%
rename from src/components/TagPicker/index.js
rename to src/components/TagPicker/index.tsx
index 341ea9cddae9..af8acd19e8c4 100644
--- a/src/components/TagPicker/index.js
+++ b/src/components/TagPicker/index.tsx
@@ -1,7 +1,7 @@
-import lodashGet from 'lodash/get';
import React, {useMemo, useState} from 'react';
+import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
+import type {EdgeInsets} from 'react-native-safe-area-context';
import OptionsSelector from '@components/OptionsSelector';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
@@ -10,22 +10,64 @@ import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import {defaultProps, propTypes} from './tagPickerPropTypes';
+import type {PolicyTag, PolicyTagList, PolicyTags, RecentlyUsedTags} from '@src/types/onyx';
-function TagPicker({selectedTag, tag, tagIndex, policyTags, policyRecentlyUsedTags, shouldShowDisabledAndSelectedOption, insets, onSubmit}) {
+type SelectedTagOption = {
+ name: string;
+ enabled: boolean;
+ accountID: number | null;
+};
+
+type TagPickerOnyxProps = {
+ /** Collection of tag list on a policy */
+ policyTags: OnyxEntry;
+
+ /** List of recently used tags */
+ policyRecentlyUsedTags: OnyxEntry;
+};
+
+type TagPickerProps = TagPickerOnyxProps & {
+ /** The policyID we are getting tags for */
+ // It's used in withOnyx HOC.
+ // eslint-disable-next-line react/no-unused-prop-types
+ policyID: string;
+
+ /** The selected tag of the money request */
+ selectedTag: string;
+
+ /** The name of tag list we are getting tags for */
+ tagListName: string;
+
+ /** Callback to submit the selected tag */
+ onSubmit: () => void;
+
+ /**
+ * Safe area insets required for reflecting the portion of the view,
+ * that is not covered by navigation bars, tab bars, toolbars, and other ancestor views.
+ */
+ insets: EdgeInsets;
+
+ /** Should show the selected option that is disabled? */
+ shouldShowDisabledAndSelectedOption?: boolean;
+
+ /** Indicates which tag list index was selected */
+ tagListIndex: number;
+};
+
+function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRecentlyUsedTags, shouldShowDisabledAndSelectedOption = false, insets, onSubmit}: TagPickerProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const [searchValue, setSearchValue] = useState('');
- const policyRecentlyUsedTagsList = lodashGet(policyRecentlyUsedTags, tag, []);
- const policyTagList = PolicyUtils.getTagList(policyTags, tagIndex);
+ const policyRecentlyUsedTagsList = useMemo(() => policyRecentlyUsedTags?.[tagListName] ?? [], [policyRecentlyUsedTags, tagListName]);
+ const policyTagList = PolicyUtils.getTagList(policyTags, tagListIndex);
const policyTagsCount = PolicyUtils.getCountOfEnabledTagsOfList(policyTagList.tags);
const isTagsCountBelowThreshold = policyTagsCount < CONST.TAG_LIST_THRESHOLD;
const shouldShowTextInput = !isTagsCountBelowThreshold;
- const selectedOptions = useMemo(() => {
+ const selectedOptions: SelectedTagOption[] = useMemo(() => {
if (!selectedTag) {
return [];
}
@@ -39,13 +81,13 @@ function TagPicker({selectedTag, tag, tagIndex, policyTags, policyRecentlyUsedTa
];
}, [selectedTag]);
- const enabledTags = useMemo(() => {
+ const enabledTags: PolicyTags | Array = useMemo(() => {
if (!shouldShowDisabledAndSelectedOption) {
return policyTagList.tags;
}
- const selectedNames = _.map(selectedOptions, (s) => s.name);
- const tags = [...selectedOptions, ..._.filter(policyTagList.tags, (policyTag) => policyTag.enabled && !selectedNames.includes(policyTag.name))];
- return tags;
+ const selectedNames = selectedOptions.map((s) => s.name);
+
+ return [...selectedOptions, ...Object.values(policyTagList.tags).filter((policyTag) => policyTag.enabled && !selectedNames.includes(policyTag.name))];
}, [selectedOptions, policyTagList, shouldShowDisabledAndSelectedOption]);
const sections = useMemo(
@@ -53,12 +95,13 @@ function TagPicker({selectedTag, tag, tagIndex, policyTags, policyRecentlyUsedTa
[searchValue, enabledTags, selectedOptions, policyRecentlyUsedTagsList],
);
- const headerMessage = OptionsListUtils.getHeaderMessageForNonUserList(lodashGet(sections, '[0].data.length', 0) > 0, searchValue);
+ const headerMessage = OptionsListUtils.getHeaderMessageForNonUserList((sections?.[0]?.data?.length ?? 0) > 0, searchValue);
- const selectedOptionKey = lodashGet(_.filter(lodashGet(sections, '[0].data', []), (policyTag) => policyTag.searchText === selectedTag)[0], 'keyForList');
+ const selectedOptionKey = sections[0]?.data?.filter((policyTag) => policyTag.searchText === selectedTag)?.[0]?.keyForList;
return (
({
policyTags: {
key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
},
@@ -92,3 +133,5 @@ export default withOnyx({
key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${policyID}`,
},
})(TagPicker);
+
+export type {SelectedTagOption};
diff --git a/src/components/TagPicker/tagPickerPropTypes.js b/src/components/TagPicker/tagPickerPropTypes.js
deleted file mode 100644
index cbdc73f5d056..000000000000
--- a/src/components/TagPicker/tagPickerPropTypes.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import PropTypes from 'prop-types';
-import tagPropTypes from '@components/tagPropTypes';
-import safeAreaInsetPropTypes from '@pages/safeAreaInsetPropTypes';
-
-const propTypes = {
- /** The policyID we are getting tags for */
- policyID: PropTypes.string.isRequired,
-
- /** The selected tag of the money request */
- selectedTag: PropTypes.string.isRequired,
-
- /** The name of tag list we are getting tags for */
- tag: PropTypes.string.isRequired,
-
- /** The index of a tag list */
- tagIndex: PropTypes.number.isRequired,
-
- /** Callback to submit the selected tag */
- onSubmit: PropTypes.func.isRequired,
-
- /**
- * Safe area insets required for reflecting the portion of the view,
- * that is not covered by navigation bars, tab bars, toolbars, and other ancestor views.
- */
- insets: safeAreaInsetPropTypes.isRequired,
-
- /* Onyx Props */
- /** Collection of tags attached to a policy */
- policyTags: tagPropTypes,
-
- /** List of recently used tags */
- policyRecentlyUsedTags: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.string)),
-
- /** Should show the selected option that is disabled? */
- shouldShowDisabledAndSelectedOption: PropTypes.bool,
-};
-
-const defaultProps = {
- policyTags: {},
- policyRecentlyUsedTags: {},
- shouldShowDisabledAndSelectedOption: false,
-};
-
-export {propTypes, defaultProps};
diff --git a/src/components/VideoPlayer/BaseVideoPlayer.js b/src/components/VideoPlayer/BaseVideoPlayer.js
index 341b3828eafb..91737ad3938a 100644
--- a/src/components/VideoPlayer/BaseVideoPlayer.js
+++ b/src/components/VideoPlayer/BaseVideoPlayer.js
@@ -9,6 +9,7 @@ import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeed
import {useFullScreenContext} from '@components/VideoPlayerContexts/FullScreenContext';
import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext';
import VideoPopoverMenu from '@components/VideoPopoverMenu';
+import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
@@ -54,6 +55,7 @@ function BaseVideoPlayer({
videoResumeTryNumber,
} = usePlaybackContext();
const {isFullScreenRef} = useFullScreenContext();
+ const {isOffline} = useNetwork();
const [duration, setDuration] = useState(videoDuration * 1000);
const [position, setPosition] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
@@ -253,13 +255,20 @@ function BaseVideoPlayer({
style={[styles.w100, styles.h100, videoPlayerStyle]}
videoStyle={[styles.w100, styles.h100, videoStyle]}
source={{
- uri: sourceURL,
+ // if video is loading and is offline, we want to change uri to null to
+ // reset the video player after connection is back
+ uri: !isLoading || (isLoading && !isOffline) ? sourceURL : null,
}}
shouldPlay={false}
useNativeControls={false}
resizeMode={resizeMode}
isLooping={isLooping}
- onReadyForDisplay={onVideoLoaded}
+ onReadyForDisplay={(e) => {
+ if (isCurrentlyURLSet && !isUploading) {
+ playVideo();
+ }
+ onVideoLoaded(e);
+ }}
onPlaybackStatusUpdate={handlePlaybackStatusUpdate}
onFullscreenUpdate={handleFullscreenUpdate}
/>
diff --git a/src/hooks/useViewportOffsetTop/index.native.ts b/src/hooks/useViewportOffsetTop/index.native.ts
new file mode 100644
index 000000000000..b166c360dacc
--- /dev/null
+++ b/src/hooks/useViewportOffsetTop/index.native.ts
@@ -0,0 +1,7 @@
+/**
+ * Native doesn't support DOM so default value is 0
+ */
+
+export default function useViewportOffsetTop(): number {
+ return 0;
+}
diff --git a/src/hooks/useViewportOffsetTop/index.ts b/src/hooks/useViewportOffsetTop/index.ts
new file mode 100644
index 000000000000..56fb19187c4f
--- /dev/null
+++ b/src/hooks/useViewportOffsetTop/index.ts
@@ -0,0 +1,47 @@
+import {useEffect, useRef, useState} from 'react';
+import addViewportResizeListener from '@libs/VisualViewport';
+
+/**
+ * A hook that returns the offset of the top edge of the visual viewport
+ */
+export default function useViewportOffsetTop(shouldAdjustScrollView = false): number {
+ const [viewportOffsetTop, setViewportOffsetTop] = useState(0);
+ const initialHeight = useRef(window.visualViewport?.height ?? window.innerHeight).current;
+ const cachedDefaultOffsetTop = useRef(0);
+ useEffect(() => {
+ const updateOffsetTop = (event?: Event) => {
+ let targetOffsetTop = window.visualViewport?.offsetTop ?? 0;
+ if (event?.target instanceof VisualViewport) {
+ targetOffsetTop = event.target.offsetTop;
+ }
+
+ if (shouldAdjustScrollView && window.visualViewport) {
+ const adjustScrollY = Math.round(initialHeight - window.visualViewport.height);
+ if (cachedDefaultOffsetTop.current === 0) {
+ cachedDefaultOffsetTop.current = targetOffsetTop;
+ }
+
+ if (adjustScrollY > targetOffsetTop) {
+ setViewportOffsetTop(adjustScrollY);
+ } else if (targetOffsetTop !== 0 && adjustScrollY === targetOffsetTop) {
+ setViewportOffsetTop(cachedDefaultOffsetTop.current);
+ } else {
+ setViewportOffsetTop(targetOffsetTop);
+ }
+ } else {
+ setViewportOffsetTop(targetOffsetTop);
+ }
+ };
+ updateOffsetTop();
+ return addViewportResizeListener(updateOffsetTop);
+ }, [initialHeight, shouldAdjustScrollView]);
+
+ useEffect(() => {
+ if (!shouldAdjustScrollView) {
+ return;
+ }
+ window.scrollTo({top: viewportOffsetTop});
+ }, [shouldAdjustScrollView, viewportOffsetTop]);
+
+ return viewportOffsetTop;
+}
diff --git a/src/libs/E2E/reactNativeLaunchingTest.ts b/src/libs/E2E/reactNativeLaunchingTest.ts
index 931b41524696..31faa803ec61 100644
--- a/src/libs/E2E/reactNativeLaunchingTest.ts
+++ b/src/libs/E2E/reactNativeLaunchingTest.ts
@@ -68,6 +68,7 @@ E2EClient.getTestConfig()
return E2EClient.submitTestResults({
name: config.name,
error: `Test '${config.name}' not found`,
+ critical: false,
});
}
diff --git a/src/libs/E2E/types.ts b/src/libs/E2E/types.ts
index 5185a75625a3..3cb33e3bbce3 100644
--- a/src/libs/E2E/types.ts
+++ b/src/libs/E2E/types.ts
@@ -39,6 +39,12 @@ type TestResult = {
/** Optional, if set indicates that the test run failed and has no valid results. */
error?: string;
+ /**
+ * Whether error is critical. If `true`, then server will be stopped and `e2e` tests will fail. Otherwise will simply log a warning.
+ * Default value is `true`
+ */
+ critical?: boolean;
+
/** Render count */
renderCount?: number;
};
diff --git a/src/libs/Notification/PushNotification/index.native.ts b/src/libs/Notification/PushNotification/index.native.ts
index 382591e5b698..4e028ad82392 100644
--- a/src/libs/Notification/PushNotification/index.native.ts
+++ b/src/libs/Notification/PushNotification/index.native.ts
@@ -84,11 +84,6 @@ function refreshNotificationOptInStatus() {
const init: Init = () => {
// Setup event listeners
Airship.addListener(EventType.PushReceived, (notification) => {
- // By default, refresh notification opt-in status to true if we receive a notification
- if (!isUserOptedInToPushNotifications) {
- PushNotificationActions.setPushNotificationOptInStatus(true);
- }
-
pushNotificationEventCallback(EventType.PushReceived, notification.pushPayload);
});
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index 46e217ba20b1..372881b41a86 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -7,6 +7,7 @@ import lodashSet from 'lodash/set';
import lodashSortBy from 'lodash/sortBy';
import Onyx from 'react-native-onyx';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
+import type {SelectedTagOption} from '@components/TagPicker';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -19,6 +20,7 @@ import type {
PolicyCategories,
PolicyTag,
PolicyTagList,
+ PolicyTags,
Report,
ReportAction,
ReportActions,
@@ -54,12 +56,6 @@ import * as TaskUtils from './TaskUtils';
import * as TransactionUtils from './TransactionUtils';
import * as UserUtils from './UserUtils';
-type Tag = {
- enabled: boolean;
- name: string;
- accountID: number | null;
-};
-
type Option = Partial;
/**
@@ -131,7 +127,7 @@ type GetOptionsConfig = {
categories?: PolicyCategories;
recentlyUsedCategories?: string[];
includeTags?: boolean;
- tags?: Record;
+ tags?: PolicyTags | Array;
recentlyUsedTags?: string[];
canInviteUser?: boolean;
includeSelectedOptions?: boolean;
@@ -914,7 +910,7 @@ function sortCategories(categories: Record): Category[] {
/**
* Sorts tags alphabetically by name.
*/
-function sortTags(tags: Record | Tag[]) {
+function sortTags(tags: Record | Array) {
let sortedTags;
if (Array.isArray(tags)) {
@@ -1095,7 +1091,7 @@ function getCategoryListSections(
*
* @param tags - an initial tag array
*/
-function getTagsOptions(tags: Category[]): Option[] {
+function getTagsOptions(tags: Array>): Option[] {
return tags.map((tag) => {
// This is to remove unnecessary escaping backslash in tag name sent from backend.
const cleanedName = PolicyUtils.getCleanedTagName(tag.name);
@@ -1112,7 +1108,13 @@ function getTagsOptions(tags: Category[]): Option[] {
/**
* Build the section list for tags
*/
-function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOptions: Category[], searchInputValue: string, maxRecentReportsToShow: number) {
+function getTagListSections(
+ tags: Array,
+ recentlyUsedTags: string[],
+ selectedOptions: SelectedTagOption[],
+ searchInputValue: string,
+ maxRecentReportsToShow: number,
+) {
const tagSections = [];
const sortedTags = sortTags(tags);
const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name);
@@ -1424,7 +1426,7 @@ function getOptions(
}
if (includeTags) {
- const tagOptions = getTagListSections(Object.values(tags), recentlyUsedTags, selectedOptions as Category[], searchInputValue, maxRecentReportsToShow);
+ const tagOptions = getTagListSections(Object.values(tags), recentlyUsedTags, selectedOptions as SelectedTagOption[], searchInputValue, maxRecentReportsToShow);
return {
recentReports: [],
@@ -1851,7 +1853,7 @@ function getFilteredOptions(
categories: PolicyCategories = {},
recentlyUsedCategories: string[] = [],
includeTags = false,
- tags: Record = {},
+ tags: PolicyTags | Array = {},
recentlyUsedTags: string[] = [],
canInviteUser = true,
includeSelectedOptions = false,
diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts
index 75d14be1a907..cf8937874216 100644
--- a/src/libs/ReceiptUtils.ts
+++ b/src/libs/ReceiptUtils.ts
@@ -1,11 +1,6 @@
import Str from 'expensify-common/lib/str';
import _ from 'lodash';
-import type {ImageSourcePropType} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
-import ReceiptDoc from '@assets/images/receipt-doc.png';
-import ReceiptGeneric from '@assets/images/receipt-generic.png';
-import ReceiptHTML from '@assets/images/receipt-html.png';
-import ReceiptSVG from '@assets/images/receipt-svg.png';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type {Transaction} from '@src/types/onyx';
@@ -14,16 +9,13 @@ import * as FileUtils from './fileDownload/FileUtils';
import * as TransactionUtils from './TransactionUtils';
type ThumbnailAndImageURI = {
- image: ImageSourcePropType | string;
- thumbnail: ImageSourcePropType | string | null;
- transaction?: Transaction;
+ image?: string;
+ thumbnail?: string;
+ transaction?: OnyxEntry;
isLocalFile?: boolean;
+ isThumbnail?: boolean;
filename?: string;
-};
-
-type FileNameAndExtension = {
fileExtension?: string;
- fileName?: string;
};
/**
@@ -35,11 +27,11 @@ type FileNameAndExtension = {
*/
function getThumbnailAndImageURIs(transaction: OnyxEntry, receiptPath: string | null = null, receiptFileName: string | null = null): ThumbnailAndImageURI {
if (TransactionUtils.isFetchingWaypointsFromServer(transaction)) {
- return {thumbnail: null, image: ReceiptGeneric, isLocalFile: true};
+ return {isThumbnail: true, isLocalFile: true};
}
- // URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg
// If there're errors, we need to display them in preview. We can store many files in errors, but we just need to get the last one
const errors = _.findLast(transaction?.errors) as ReceiptError | undefined;
+ // URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg
const path = errors?.source ?? transaction?.receipt?.source ?? receiptPath ?? '';
// filename of uploaded image or last part of remote URI
const filename = errors?.filename ?? transaction?.filename ?? receiptFileName ?? '';
@@ -48,12 +40,12 @@ function getThumbnailAndImageURIs(transaction: OnyxEntry, receiptPa
const isReceiptPDF = Str.isPDF(filename);
if (hasEReceipt) {
- return {thumbnail: null, image: ROUTES.ERECEIPT.getRoute(transaction.transactionID), transaction, filename};
+ return {image: ROUTES.ERECEIPT.getRoute(transaction.transactionID), transaction, filename};
}
// For local files, we won't have a thumbnail yet
if ((isReceiptImage || isReceiptPDF) && typeof path === 'string' && (path.startsWith('blob:') || path.startsWith('file:'))) {
- return {thumbnail: null, image: path, isLocalFile: true, filename};
+ return {image: path, isLocalFile: true, filename};
}
if (isReceiptImage) {
@@ -64,22 +56,9 @@ function getThumbnailAndImageURIs(transaction: OnyxEntry, receiptPa
return {thumbnail: `${path.substring(0, path.length - 4)}.jpg.1024.jpg`, image: path, filename};
}
- const {fileExtension} = FileUtils.splitExtensionFromFileName(filename) as FileNameAndExtension;
- let image = ReceiptGeneric;
- if (fileExtension === CONST.IOU.FILE_TYPES.HTML) {
- image = ReceiptHTML;
- }
-
- if (fileExtension === CONST.IOU.FILE_TYPES.DOC || fileExtension === CONST.IOU.FILE_TYPES.DOCX) {
- image = ReceiptDoc;
- }
-
- if (fileExtension === CONST.IOU.FILE_TYPES.SVG) {
- image = ReceiptSVG;
- }
-
const isLocalFile = typeof path === 'number' || path.startsWith('blob:') || path.startsWith('file:') || path.startsWith('/');
- return {thumbnail: image, image: path, isLocalFile, filename};
+ const {fileExtension} = FileUtils.splitExtensionFromFileName(filename);
+ return {isThumbnail: true, fileExtension: Object.values(CONST.IOU.FILE_TYPES).find((type) => type === fileExtension), image: path, isLocalFile, filename};
}
// eslint-disable-next-line import/prefer-default-export
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index 64bac4f46b5a..11f870b891b6 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -263,9 +263,14 @@ function getContinuousReportActionChain(sortedReportActions: ReportAction[], id?
let index;
if (id) {
- index = sortedReportActions.findIndex((obj) => obj.reportActionID === id);
+ index = sortedReportActions.findIndex((reportAction) => reportAction.reportActionID === id);
} else {
- index = sortedReportActions.findIndex((obj) => obj.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD);
+ index = sortedReportActions.findIndex(
+ (reportAction) =>
+ reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD &&
+ reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE &&
+ !reportAction.isOptimisticAction,
+ );
}
if (index === -1) {
@@ -299,6 +304,9 @@ function getContinuousReportActionChain(sortedReportActions: ReportAction[], id?
while (
(startIndex > 0 && sortedReportActions[startIndex].reportActionID === sortedReportActions[startIndex - 1].previousReportActionID) ||
sortedReportActions[startIndex - 1]?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD ||
+ sortedReportActions[startIndex - 1]?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ||
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ sortedReportActions[startIndex - 1]?.isOptimisticAction ||
sortedReportActions[startIndex - 1]?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM
) {
startIndex--;
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index ae3bf0cdbd68..b67e44fcf5ad 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -4396,15 +4396,6 @@ function getPayMoneyRequestParams(chatReport: OnyxTypes.Report, iouReport: OnyxT
];
const successData: OnyxUpdate[] = [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`,
- value: {
- [optimisticIOUReportAction.reportActionID]: {
- pendingAction: null,
- },
- },
- },
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`,
@@ -4634,7 +4625,7 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject) {
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`,
value: {
- [expenseReport.reportActionID ?? '']: {
+ [optimisticApprovedReportAction.reportActionID]: {
errors: ErrorUtils.getMicroSecondOnyxError('iou.error.other'),
},
},
diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts
index 3efba3e54dcd..45fc139b6742 100644
--- a/src/libs/actions/Policy.ts
+++ b/src/libs/actions/Policy.ts
@@ -342,6 +342,7 @@ function deleteWorkspace(policyID: string, policyName: string) {
const reportsToArchive = Object.values(allReports ?? {}).filter(
(report) => report?.policyID === policyID && (ReportUtils.isChatRoom(report) || ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isTaskReport(report)),
);
+ const finallyData: OnyxUpdate[] = [];
reportsToArchive.forEach((report) => {
const {reportID, ownerAccountID} = report ?? {};
optimisticData.push({
@@ -369,12 +370,22 @@ function deleteWorkspace(policyID: string, policyName: string) {
emailClosingReport = allPersonalDetails?.[ownerAccountID]?.login ?? '';
}
const optimisticClosedReportAction = ReportUtils.buildOptimisticClosedReportAction(emailClosingReport, policyName, CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED);
- const optimisticReportActions: Record = {};
- optimisticReportActions[optimisticClosedReportAction.reportActionID] = optimisticClosedReportAction as ReportAction;
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
- value: optimisticReportActions,
+ value: {
+ [optimisticClosedReportAction.reportActionID]: optimisticClosedReportAction as ReportAction,
+ },
+ });
+
+ // We are temporarily adding this workaround because 'DeleteWorkspace' doesn't
+ // support receiving the optimistic reportActions' ids for the moment.
+ finallyData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {
+ [optimisticClosedReportAction.reportActionID]: null,
+ },
});
});
@@ -406,7 +417,7 @@ function deleteWorkspace(policyID: string, policyName: string) {
const params: DeleteWorkspaceParams = {policyID};
- API.write(WRITE_COMMANDS.DELETE_WORKSPACE, params, {optimisticData, failureData});
+ API.write(WRITE_COMMANDS.DELETE_WORKSPACE, params, {optimisticData, finallyData, failureData});
// Reset the lastAccessedWorkspacePolicyID
if (policyID === lastAccessedWorkspacePolicyID) {
diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js
index 7d10e0e55e79..eecffd81d88b 100644
--- a/src/pages/EditRequestPage.js
+++ b/src/pages/EditRequestPage.js
@@ -37,7 +37,7 @@ const propTypes = {
/** reportID for the "transaction thread" */
threadReportID: PropTypes.string,
- /** The index of a tag list */
+ /** Indicates which tag list index was selected */
tagIndex: PropTypes.string,
}),
}).isRequired,
@@ -78,10 +78,10 @@ function EditRequestPage({report, route, policy, policyCategories, policyTags, p
const defaultCurrency = lodashGet(route, 'params.currency', '') || transactionCurrency;
const fieldToEdit = lodashGet(route, ['params', 'field'], '');
- const tagIndex = Number(lodashGet(route, ['params', 'tagIndex'], undefined));
+ const tagListIndex = Number(lodashGet(route, ['params', 'tagIndex'], undefined));
- const tag = TransactionUtils.getTag(transaction, tagIndex);
- const policyTagListName = PolicyUtils.getTagListName(policyTags, tagIndex);
+ const tag = TransactionUtils.getTag(transaction, tagListIndex);
+ const policyTagListName = PolicyUtils.getTagListName(policyTags, tagListIndex);
const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]);
// A flag for verifying that the current report is a sub-report of a workspace chat
@@ -129,14 +129,14 @@ function EditRequestPage({report, route, policy, policyCategories, policyTags, p
IOU.updateMoneyRequestTag(
transaction.transactionID,
report.reportID,
- IOUUtils.insertTagIntoTransactionTagsString(transactionTag, updatedTag, tagIndex),
+ IOUUtils.insertTagIntoTransactionTagsString(transactionTag, updatedTag, tagListIndex),
policy,
policyTags,
policyCategories,
);
Navigation.dismissModal();
},
- [tag, transaction.transactionID, report.reportID, transactionTag, tagIndex, policy, policyTags, policyCategories],
+ [tag, transaction.transactionID, report.reportID, transactionTag, tagListIndex, policy, policyTags, policyCategories],
);
if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.AMOUNT) {
@@ -159,7 +159,7 @@ function EditRequestPage({report, route, policy, policyCategories, policyTags, p
diff --git a/src/pages/EditRequestTagPage.js b/src/pages/EditRequestTagPage.js
index b64cb925a213..1aead9ee1f6e 100644
--- a/src/pages/EditRequestTagPage.js
+++ b/src/pages/EditRequestTagPage.js
@@ -15,21 +15,21 @@ const propTypes = {
/** The policyID we are getting tags for */
policyID: PropTypes.string.isRequired,
- /** The tag name to which the default tag belongs to */
- tagName: PropTypes.string,
+ /** The tag list name to which the default tag belongs to */
+ tagListName: PropTypes.string,
- /** The index of a tag list */
- tagIndex: PropTypes.number.isRequired,
+ /** Indicates which tag list index was selected */
+ tagListIndex: PropTypes.number.isRequired,
/** Callback to fire when the Save button is pressed */
onSubmit: PropTypes.func.isRequired,
};
const defaultProps = {
- tagName: '',
+ tagListName: '',
};
-function EditRequestTagPage({defaultTag, policyID, tagName, tagIndex, onSubmit}) {
+function EditRequestTagPage({defaultTag, policyID, tagListName, tagListIndex, onSubmit}) {
const styles = useThemeStyles();
const {translate} = useLocalize();
@@ -46,14 +46,14 @@ function EditRequestTagPage({defaultTag, policyID, tagName, tagIndex, onSubmit})
{({insets}) => (
<>
{translate('iou.tagSelection')}
{
setDraftSplitTransaction({tag: transactionChanges.tag.trim()});
}}
diff --git a/src/pages/TransactionReceiptPage.tsx b/src/pages/TransactionReceiptPage.tsx
index 8db9e05a5139..f6f2c90e5d2c 100644
--- a/src/pages/TransactionReceiptPage.tsx
+++ b/src/pages/TransactionReceiptPage.tsx
@@ -28,7 +28,7 @@ type TransactionReceiptProps = TransactionReceiptOnyxProps & StackScreenProps;
+
/** Tells us if the sidebar has rendered */
isSidebarLoaded: OnyxEntry;
@@ -93,7 +96,7 @@ type OnyxHOCProps = {
type ReportScreenNavigationProps = StackScreenProps;
-type ReportScreenProps = OnyxHOCProps & ViewportOffsetTopProps & CurrentReportIDContextValue & ReportScreenOnyxProps & ReportScreenNavigationProps;
+type ReportScreenProps = OnyxHOCProps & CurrentReportIDContextValue & ReportScreenOnyxProps & ReportScreenNavigationProps;
/** Get the currently viewed report ID as number */
function getReportID(route: ReportScreenNavigationProps['route']): string {
@@ -131,7 +134,7 @@ function ReportScreen({
markReadyForHydration,
policies = {},
isSidebarLoaded = false,
- viewportOffsetTop,
+ modal,
isComposerFullSize = false,
userLeavingStatus = false,
currentReportID = '',
@@ -259,6 +262,8 @@ function ReportScreen({
Timing.start(CONST.TIMING.CHAT_RENDER);
Performance.markStart(CONST.TIMING.CHAT_RENDER);
}
+ const [isComposerFocus, setIsComposerFocus] = useState(false);
+ const viewportOffsetTop = useViewportOffsetTop(Browser.isMobileSafari() && isComposerFocus && !modal?.willAlertModalBecomeVisible);
const {reportPendingAction, reportErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report);
const screenWrapperStyle: ViewStyle[] = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}];
@@ -644,6 +649,8 @@ function ReportScreen({
{isCurrentReportLoadedFromOnyx ? (
setIsComposerFocus(true)}
+ onComposerBlur={() => setIsComposerFocus(false)}
report={report}
pendingAction={reportPendingAction}
isComposerFullSize={!!isComposerFullSize}
@@ -663,81 +670,82 @@ function ReportScreen({
ReportScreen.displayName = 'ReportScreen';
-export default withViewportOffsetTop(
- withCurrentReportID(
- withOnyx(
- {
- isSidebarLoaded: {
- key: ONYXKEYS.IS_SIDEBAR_LOADED,
- },
- sortedAllReportActions: {
- key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`,
- canEvict: false,
- selector: (allReportActions: OnyxEntry) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true),
- },
- report: {
- key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${getReportID(route)}`,
- allowStaleData: true,
- selector: reportWithoutHasDraftSelector,
- },
- reportMetadata: {
- key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_METADATA}${getReportID(route)}`,
- initialValue: {
- isLoadingInitialReportActions: true,
- isLoadingOlderReportActions: false,
- isLoadingNewerReportActions: false,
- },
- },
- isComposerFullSize: {
- key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${getReportID(route)}`,
- initialValue: false,
- },
- betas: {
- key: ONYXKEYS.BETAS,
- },
- policies: {
- key: ONYXKEYS.COLLECTION.POLICY,
- allowStaleData: true,
- },
- accountManagerReportID: {
- key: ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID,
- initialValue: null,
- },
- userLeavingStatus: {
- key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${getReportID(route)}`,
- initialValue: false,
+export default withCurrentReportID(
+ withOnyx(
+ {
+ modal: {
+ key: ONYXKEYS.MODAL,
+ },
+ isSidebarLoaded: {
+ key: ONYXKEYS.IS_SIDEBAR_LOADED,
+ },
+ sortedAllReportActions: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`,
+ canEvict: false,
+ selector: (allReportActions: OnyxEntry) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true),
+ },
+ report: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${getReportID(route)}`,
+ allowStaleData: true,
+ selector: reportWithoutHasDraftSelector,
+ },
+ reportMetadata: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_METADATA}${getReportID(route)}`,
+ initialValue: {
+ isLoadingInitialReportActions: true,
+ isLoadingOlderReportActions: false,
+ isLoadingNewerReportActions: false,
},
- parentReportAction: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : 0}`,
- selector: (parentReportActions: OnyxEntry, props: WithOnyxInstanceState): OnyxEntry => {
- const parentReportActionID = props?.report?.parentReportActionID;
- if (!parentReportActionID) {
- return null;
- }
- return parentReportActions?.[parentReportActionID] ?? null;
- },
- canEvict: false,
+ },
+ isComposerFullSize: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${getReportID(route)}`,
+ initialValue: false,
+ },
+ betas: {
+ key: ONYXKEYS.BETAS,
+ },
+ policies: {
+ key: ONYXKEYS.COLLECTION.POLICY,
+ allowStaleData: true,
+ },
+ accountManagerReportID: {
+ key: ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID,
+ initialValue: null,
+ },
+ userLeavingStatus: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${getReportID(route)}`,
+ initialValue: false,
+ },
+ parentReportAction: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : 0}`,
+ selector: (parentReportActions: OnyxEntry, props: WithOnyxInstanceState): OnyxEntry => {
+ const parentReportActionID = props?.report?.parentReportActionID;
+ if (!parentReportActionID) {
+ return null;
+ }
+ return parentReportActions?.[parentReportActionID] ?? null;
},
+ canEvict: false,
},
- true,
- )(
- memo(
- ReportScreen,
- (prevProps, nextProps) =>
- prevProps.isSidebarLoaded === nextProps.isSidebarLoaded &&
- lodashIsEqual(prevProps.sortedAllReportActions, nextProps.sortedAllReportActions) &&
- lodashIsEqual(prevProps.reportMetadata, nextProps.reportMetadata) &&
- prevProps.isComposerFullSize === nextProps.isComposerFullSize &&
- lodashIsEqual(prevProps.betas, nextProps.betas) &&
- lodashIsEqual(prevProps.policies, nextProps.policies) &&
- prevProps.accountManagerReportID === nextProps.accountManagerReportID &&
- prevProps.userLeavingStatus === nextProps.userLeavingStatus &&
- prevProps.currentReportID === nextProps.currentReportID &&
- prevProps.viewportOffsetTop === nextProps.viewportOffsetTop &&
- lodashIsEqual(prevProps.parentReportAction, nextProps.parentReportAction) &&
- lodashIsEqual(prevProps.route, nextProps.route) &&
- lodashIsEqual(prevProps.report, nextProps.report),
- ),
+ },
+ true,
+ )(
+ memo(
+ ReportScreen,
+ (prevProps, nextProps) =>
+ prevProps.isSidebarLoaded === nextProps.isSidebarLoaded &&
+ lodashIsEqual(prevProps.sortedAllReportActions, nextProps.sortedAllReportActions) &&
+ lodashIsEqual(prevProps.reportMetadata, nextProps.reportMetadata) &&
+ prevProps.isComposerFullSize === nextProps.isComposerFullSize &&
+ lodashIsEqual(prevProps.betas, nextProps.betas) &&
+ lodashIsEqual(prevProps.policies, nextProps.policies) &&
+ prevProps.accountManagerReportID === nextProps.accountManagerReportID &&
+ prevProps.userLeavingStatus === nextProps.userLeavingStatus &&
+ prevProps.currentReportID === nextProps.currentReportID &&
+ lodashIsEqual(prevProps.modal, nextProps.modal) &&
+ lodashIsEqual(prevProps.parentReportAction, nextProps.parentReportAction) &&
+ lodashIsEqual(prevProps.route, nextProps.route) &&
+ lodashIsEqual(prevProps.report, nextProps.report),
),
),
);
diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
index 1e0e322be258..70340e9e1fec 100644
--- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
@@ -85,6 +85,12 @@ type ReportActionComposeProps = ReportActionComposeOnyxProps &
/** Whether the report is ready for display */
isReportReadyForDisplay?: boolean;
+
+ /** A method to call when the input is focus */
+ onComposerFocus?: () => void;
+
+ /** A method to call when the input is blur */
+ onComposerBlur?: () => void;
};
// We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will
@@ -107,6 +113,8 @@ function ReportActionCompose({
isReportReadyForDisplay = true,
isEmptyChat,
lastReportAction,
+ onComposerFocus,
+ onComposerBlur,
}: ReportActionComposeProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
@@ -293,20 +301,25 @@ function ReportActionCompose({
isKeyboardVisibleWhenShowingModalRef.current = true;
}, []);
- const onBlur = useCallback((event: NativeSyntheticEvent) => {
- const webEvent = event as unknown as FocusEvent;
- setIsFocused(false);
- if (suggestionsRef.current) {
- suggestionsRef.current.resetSuggestions();
- }
- if (webEvent.relatedTarget && webEvent.relatedTarget === actionButtonRef.current) {
- isKeyboardVisibleWhenShowingModalRef.current = true;
- }
- }, []);
+ const onBlur = useCallback(
+ (event: NativeSyntheticEvent) => {
+ const webEvent = event as unknown as FocusEvent;
+ setIsFocused(false);
+ onComposerBlur?.();
+ if (suggestionsRef.current) {
+ suggestionsRef.current.resetSuggestions();
+ }
+ if (webEvent.relatedTarget && webEvent.relatedTarget === actionButtonRef.current) {
+ isKeyboardVisibleWhenShowingModalRef.current = true;
+ }
+ },
+ [onComposerBlur],
+ );
const onFocus = useCallback(() => {
setIsFocused(true);
- }, []);
+ onComposerFocus?.();
+ }, [onComposerFocus]);
// resets the composer to normal size when
// the send button is pressed.
diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx
index 3a787e1dbd0f..bd143f9ef196 100644
--- a/src/pages/home/report/ReportFooter.tsx
+++ b/src/pages/home/report/ReportFooter.tsx
@@ -51,6 +51,12 @@ type ReportFooterProps = ReportFooterOnyxProps & {
/** Whether the composer is in full size */
isComposerFullSize?: boolean;
+
+ /** A method to call when the input is focus */
+ onComposerFocus: () => void;
+
+ /** A method to call when the input is blur */
+ onComposerBlur: () => void;
};
function ReportFooter({
@@ -63,6 +69,8 @@ function ReportFooter({
isReportReadyForDisplay = true,
listHeight = 0,
isComposerFullSize = false,
+ onComposerBlur,
+ onComposerFocus,
}: ReportFooterProps) {
const styles = useThemeStyles();
const {isOffline} = useNetwork();
@@ -137,6 +145,8 @@ function ReportFooter({
SidebarUtils.getOrderedReportIDs(null, chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs),
+ [chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs],
+ );
+
const optionListItems = useMemo(() => {
- const reportIDs = SidebarUtils.getOrderedReportIDs(
- null,
- chatReports,
- betas,
- policies,
- priorityMode,
- allReportActions,
- transactionViolations,
- activeWorkspaceID,
- policyMemberAccountIDs,
- );
+ const reportIDs = optionItemsMemoized;
if (deepEqual(reportIDsRef.current, reportIDs)) {
return reportIDsRef.current;
@@ -160,7 +156,7 @@ function SidebarLinksData({
reportIDsRef.current = reportIDs;
}
return reportIDsRef.current || [];
- }, [chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs, isLoading, network.isOffline, prevPriorityMode]);
+ }, [optionItemsMemoized, priorityMode, isLoading, network.isOffline, prevPriorityMode]);
// We need to make sure the current report is in the list of reports, but we do not want
// to have to re-generate the list every time the currentReportID changes. To do that
@@ -334,4 +330,27 @@ export default compose(
initialValue: {},
},
}),
-)(SidebarLinksData);
+)(
+ /*
+ While working on audit on the App Start App metric we noticed that by memoizing SidebarLinksData we can avoid 2 additional run of getOrderedReportIDs.
+ With that we can reduce app start up time by ~2s on heavy account.
+ More details - https://github.com/Expensify/App/issues/35234#issuecomment-1926914534
+ */
+ memo(
+ SidebarLinksData,
+ (prevProps, nextProps) =>
+ _.isEqual(prevProps.chatReports, nextProps.chatReports) &&
+ _.isEqual(prevProps.allReportActions, nextProps.allReportActions) &&
+ prevProps.isLoadingApp === nextProps.isLoadingApp &&
+ prevProps.priorityMode === nextProps.priorityMode &&
+ _.isEqual(prevProps.betas, nextProps.betas) &&
+ _.isEqual(prevProps.policies, nextProps.policies) &&
+ prevProps.network.isOffline === nextProps.network.isOffline &&
+ _.isEqual(prevProps.insets, nextProps.insets) &&
+ prevProps.onLinkClick === nextProps.onLinkClick &&
+ _.isEqual(prevProps.policyMembers, nextProps.policyMembers) &&
+ _.isEqual(prevProps.transactionViolations, nextProps.transactionViolations) &&
+ _.isEqual(prevProps.currentUserPersonalDetails, nextProps.currentUserPersonalDetails) &&
+ prevProps.currentReportID === nextProps.currentReportID,
+ ),
+);
diff --git a/src/pages/iou/request/step/IOURequestStepRoutePropTypes.js b/src/pages/iou/request/step/IOURequestStepRoutePropTypes.js
index dbcf83bda62a..8b191fa0b58e 100644
--- a/src/pages/iou/request/step/IOURequestStepRoutePropTypes.js
+++ b/src/pages/iou/request/step/IOURequestStepRoutePropTypes.js
@@ -22,5 +22,8 @@ export default PropTypes.shape({
/** A path to go to when the user presses the back button */
backTo: PropTypes.string,
+
+ /** Indicates which tag list index was selected */
+ tagIndex: PropTypes.string,
}),
});
diff --git a/src/pages/iou/request/step/IOURequestStepTag.js b/src/pages/iou/request/step/IOURequestStepTag.js
index 79ed26b76b19..ed55628ecaa9 100644
--- a/src/pages/iou/request/step/IOURequestStepTag.js
+++ b/src/pages/iou/request/step/IOURequestStepTag.js
@@ -91,15 +91,15 @@ function IOURequestStepTag({
const styles = useThemeStyles();
const {translate} = useLocalize();
- const tagIndex = Number(rawTagIndex);
- const policyTagListName = PolicyUtils.getTagListName(policyTags, tagIndex);
+ const tagListIndex = Number(rawTagIndex);
+ const policyTagListName = PolicyUtils.getTagListName(policyTags, tagListIndex);
const isEditing = action === CONST.IOU.ACTION.EDIT;
const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT;
const isEditingSplitBill = isEditing && isSplitBill;
const currentTransaction = isEditingSplitBill && !lodashIsEmpty(splitDraftTransaction) ? splitDraftTransaction : transaction;
const transactionTag = TransactionUtils.getTag(currentTransaction);
- const tag = TransactionUtils.getTag(currentTransaction, tagIndex);
+ const tag = TransactionUtils.getTag(currentTransaction, tagListIndex);
const reportAction = reportActions[report.parentReportActionID || reportActionID];
const canEditSplitBill = isSplitBill && reportAction && session.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction);
const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]);
@@ -119,7 +119,7 @@ function IOURequestStepTag({
*/
const updateTag = (selectedTag) => {
const isSelectedTag = selectedTag.searchText === tag;
- const updatedTag = IOUUtils.insertTagIntoTransactionTagsString(transactionTag, isSelectedTag ? '' : selectedTag.searchText, tagIndex);
+ const updatedTag = IOUUtils.insertTagIntoTransactionTagsString(transactionTag, isSelectedTag ? '' : selectedTag.searchText, tagListIndex);
if (isEditingSplitBill) {
IOU.setDraftSplitTransaction(transactionID, {tag: updatedTag});
navigateBack();
@@ -147,8 +147,8 @@ function IOURequestStepTag({
{translate('iou.tagSelection')}
) => void;
+
+ /** Function to validate the edited values of the form */
+ validateEdit?: (values: FormOnyxValues) => FormInputErrors;
};
-function CategoryForm({onSubmit, policyCategories, categoryName}: CategoryFormProps) {
+function CategoryForm({onSubmit, policyCategories, categoryName, validateEdit}: CategoryFormProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {inputCallbackRef} = useAutoFocusInput();
@@ -67,7 +70,8 @@ function CategoryForm({onSubmit, policyCategories, categoryName}: CategoryFormPr
formID={ONYXKEYS.FORMS.WORKSPACE_CATEGORY_FORM}
onSubmit={submit}
submitButtonText={translate('common.save')}
- validate={validate}
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ validate={validateEdit || validate}
style={[styles.mh5, styles.flex1]}
enabledWhenOffline
>
diff --git a/src/pages/workspace/categories/EditCategoryPage.tsx b/src/pages/workspace/categories/EditCategoryPage.tsx
index 0e5ed0589934..dbf7c8913515 100644
--- a/src/pages/workspace/categories/EditCategoryPage.tsx
+++ b/src/pages/workspace/categories/EditCategoryPage.tsx
@@ -2,7 +2,7 @@ import type {StackScreenProps} from '@react-navigation/stack';
import React, {useCallback} from 'react';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
-import type {FormOnyxValues} from '@components/Form/types';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import useLocalize from '@hooks/useLocalize';
@@ -32,9 +32,29 @@ function EditCategoryPage({route, policyCategories}: EditCategoryPageProps) {
const {translate} = useLocalize();
const currentCategoryName = route.params.categoryName;
+ const validate = useCallback(
+ (values: FormOnyxValues) => {
+ const errors: FormInputErrors = {};
+ const newCategoryName = values.categoryName.trim();
+
+ if (!newCategoryName) {
+ errors.categoryName = 'workspace.categories.categoryRequiredError';
+ } else if (policyCategories?.[newCategoryName] && currentCategoryName !== newCategoryName) {
+ errors.categoryName = 'workspace.categories.existingCategoryError';
+ }
+
+ return errors;
+ },
+ [policyCategories, currentCategoryName],
+ );
+
const editCategory = useCallback(
(values: FormOnyxValues) => {
- Policy.renamePolicyCategory(route.params.policyID, {oldName: currentCategoryName, newName: values.categoryName});
+ const newCategoryName = values.categoryName.trim();
+ // Do not call the API if the edited category name is the same as the current category name
+ if (currentCategoryName !== newCategoryName) {
+ Policy.renamePolicyCategory(route.params.policyID, {oldName: currentCategoryName, newName: values.categoryName});
+ }
},
[currentCategoryName, route.params.policyID],
);
@@ -58,6 +78,7 @@ function EditCategoryPage({route, policyCategories}: EditCategoryPageProps) {
/>
diff --git a/src/pages/workspace/tags/EditTagPage.tsx b/src/pages/workspace/tags/EditTagPage.tsx
index 92d7c0a11ac9..fbde96eb2b91 100644
--- a/src/pages/workspace/tags/EditTagPage.tsx
+++ b/src/pages/workspace/tags/EditTagPage.tsx
@@ -57,7 +57,11 @@ function EditTagPage({route, policyTags}: EditTagPageProps) {
const editTag = useCallback(
(values: FormOnyxValues) => {
- Policy.renamePolicyTag(route.params.policyID, {oldName: currentTagName, newName: values.tagName.trim()});
+ const tagName = values.tagName.trim();
+ // Do not call the API if the edited tag name is the same as the current tag name
+ if (currentTagName !== tagName) {
+ Policy.renamePolicyTag(route.params.policyID, {oldName: currentTagName, newName: values.tagName.trim()});
+ }
Keyboard.dismiss();
Navigation.dismissModal();
},
diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts
index fa4c19539072..152e023b7d94 100644
--- a/src/styles/utils/index.ts
+++ b/src/styles/utils/index.ts
@@ -288,6 +288,20 @@ function getEReceiptColorCode(transaction: OnyxEntry): EReceiptColo
return eReceiptColors[colorHash];
}
+/**
+ * Helper method to return eReceipt color code for Receipt Thumbnails
+ */
+function getFileExtensionColorCode(fileExtension?: string): EReceiptColorName {
+ switch (fileExtension) {
+ case CONST.IOU.FILE_TYPES.DOC:
+ return CONST.ERECEIPT_COLORS.PINK;
+ case CONST.IOU.FILE_TYPES.HTML:
+ return CONST.ERECEIPT_COLORS.TANGERINE;
+ default:
+ return CONST.ERECEIPT_COLORS.GREEN;
+ }
+}
+
/**
* Helper method to return eReceipt color styles
*/
@@ -1139,6 +1153,7 @@ const staticStyleUtils = {
parseStyleFromFunction,
getEReceiptColorStyles,
getEReceiptColorCode,
+ getFileExtensionColorCode,
getNavigationModalCardStyle,
getCardStyles,
getOpacityStyle,
diff --git a/src/styles/variables.ts b/src/styles/variables.ts
index d911a2fc4b0e..50dc4cbd34fa 100644
--- a/src/styles/variables.ts
+++ b/src/styles/variables.ts
@@ -169,7 +169,7 @@ export default {
addBankAccountLeftSpacing: 3,
eReceiptThumbnailSmallBreakpoint: 110,
eReceiptThumbnailMediumBreakpoint: 335,
- eReceiptThumnailCenterReceiptBreakpoint: 200,
+ eReceiptThumbnailCenterReceiptBreakpoint: 200,
eReceiptIconHeight: 100,
eReceiptIconWidth: 72,
eReceiptEmptyIconWidth: 76,
diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts
index 8066b85d1e44..f469ac7fff70 100644
--- a/src/types/onyx/PolicyTag.ts
+++ b/src/types/onyx/PolicyTag.ts
@@ -26,7 +26,7 @@ type PolicyTagList = Record<
/** Flag that determines if tags are required */
required: boolean;
- /** Nested tags */
+ /** List of tags */
tags: PolicyTags;
/** Index by which the tag appears in the hierarchy of tags */
diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts
index 1a7541955720..7bb83506035b 100644
--- a/src/types/onyx/Transaction.ts
+++ b/src/types/onyx/Transaction.ts
@@ -57,7 +57,7 @@ type Geometry = {
type?: GeometryType;
};
-type ReceiptSource = string | number;
+type ReceiptSource = string;
type Receipt = {
receiptID?: number;
diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts
index 33cdefb749ec..c71a88809302 100644
--- a/tests/actions/IOUTest.ts
+++ b/tests/actions/IOUTest.ts
@@ -1584,7 +1584,6 @@ describe('actions/IOU', () => {
reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && reportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY,
) ?? null;
expect(payIOUAction).toBeTruthy();
- expect(payIOUAction?.pendingAction).toBeFalsy();
resolve();
},
diff --git a/tests/e2e/testRunner.ts b/tests/e2e/testRunner.ts
index 5edc8c068229..8d898321309e 100644
--- a/tests/e2e/testRunner.ts
+++ b/tests/e2e/testRunner.ts
@@ -93,9 +93,14 @@ const runTests = async (): Promise => {
// Collect results while tests are being executed
server.addTestResultListener((testResult) => {
- if (testResult?.error != null) {
+ const {critical = true} = testResult;
+
+ if (testResult?.error != null && critical) {
throw new Error(`Test '${testResult.name}' failed with error: ${testResult.error}`);
}
+ if (testResult?.error != null && !critical) {
+ Logger.warn(`Test '${testResult.name}' failed with error: ${testResult.error}`);
+ }
let result = 0;
if (testResult?.duration !== undefined) {