diff --git a/docs/articles/expensify-classic/expenses/Distance-Tracking.md b/docs/articles/expensify-classic/expenses/Distance-Tracking.md
deleted file mode 100644
index c0d8956f71ac..000000000000
--- a/docs/articles/expensify-classic/expenses/Distance-Tracking.md
+++ /dev/null
@@ -1,81 +0,0 @@
----
-title: Distance Tracking in Expensify
-description: Learn how distance tracking works in Expensify!
----
-
-# Overview
-
-Expensify provides a convenient feature for tracking your mileage-related expenses. You'll find all the essential information to begin logging your trips below.
-
-# How to Use Distance Tracking
-## Mobile App
-
-First, you’ll want to click the **+** in the top right corner.
-
-If you select **Manually Create**, you’ll be prompted to enter your mileage, select a rate, and code the expense before clicking **Save**.
-
- ![Click manually create or odometer to create a distance request.](https://help.expensify.com/assets/images/ExpensifyHelp_CreateExpense_Mobile.png){:width="100%"}
-
-If you select **Manually Create**:
- - Enter your mileage.
- - Select a rate.
- - Code the expense.
- - Click **Save**.
-
-![Enter your mileage, rate, code the expense, and click save.](https://help.expensify.com/assets/images/ExpensifyHelp_ManualDistance_Mobile.png){:width="100%"}
-
-If you select **Odometer**:
- - Enter your vehicle’s mileage reading before and after your trip.
- - Select your rate.
- - Code the expense.
- - Click **Save**.
-
-![Etner your mileage readings, your rate, code the expense, and click save.](https://help.expensify.com/assets/images/ExpensifyHelp_Odometer_Mobile.png){:width="100%"}
-
-The **Start GPS** option also exists on the mobile app. However, we’ve learned that most customers prefer to track their mileage after their trips (thus not needing to hit that start button!)
-
-We’ve temporarily paused the development of GPS mileage tracking in the mobile app, and we recommend you use one of the above options instead!
-
-
-## Web
-
-Navigate to the **Expenses** page, click **New Expense**, and review the two **Distance** options.
-
-![Select manually create or create from map to create a new distance request.](https://help.expensify.com/assets/images/ExpensifyHelp_CreateExpense.png){:width="100%"}
-
-If you select **Manually Create**:
- - Enter the number of miles for your trip.
- - Mileage rate is automatically selected based on your history, or manually select it if it's your first time.
- - Complete any other applicable coding fields.
- - Click **Save**.
-
-![Enter the number of miles, select your rate, code the expense, and click save.](https://help.expensify.com/assets/images/ExpensifyHelp_ManualDistance.png){:width="100%"}
-
-For **Create from Map** expenses:
- - Add your start and end location, and the distance will be calculated.
- - You can also click **Add Destination** for multiple stops.
- - Leave **Create Receipt** selected if you want a map receipt generated.
- - Click **Save**.
-
-![Enter your start and end locations, and click save.](https://help.expensify.com/assets/images/ExpensifyHelp_ManualDistanceMap.png){:width="100%"}
-
-Once you click **Save**, review the details from your map selection.
- - Select your rate.
- - Enter any other applicable coding.
- - Click **Save**.
-
-![Select your rate, code the expense, and click save.](https://help.expensify.com/assets/images/ExpensifyHelp_ManualDistanceConfirm.png){:width="100%"}
-
-# Mileage Tracking FAQs
-## **How can I change the rate of my mileage expenses?**
-You can change the rate by going to Settings > Workspaces > [Your Workspace] > Expenses > Distance > Add a Mileage Rate.
-If you submit mileage expenses on a group workspace, only workspace admins can do this.
-
-## **Do you plan to add the "Create from Map" option to the mobile app or "Odometer" option to web?**
-Not now, but if that changes, you'll be the first to know!
-
-## **Will you restart maintenance on the mobile app's GPS option anytime soon?**
-Not now, but if that changes, you'll be the first to know!
-
-## **Does Expensify automatically update IRS Mileage rates?**
- We never automatically update mileage rates in Expensify because different companies want the new rates to become effective on different dates.
diff --git a/docs/articles/expensify-classic/expenses/Track-mileage-expenses.md b/docs/articles/expensify-classic/expenses/Track-mileage-expenses.md
new file mode 100644
index 000000000000..e8b9ab0eac75
--- /dev/null
+++ b/docs/articles/expensify-classic/expenses/Track-mileage-expenses.md
@@ -0,0 +1,66 @@
+---
+title: Track mileage expenses
+description: Add mileage-related expenses
+---
+
+
+
+You can track your mileage-related expenses by logging your trips in Expensify. You have a couple of different options for logging distance:
+
+- Web app:
+ - **Manually create**: Manually enter the number of miles for the trip
+ - **Create from map**: Automatically determine the trip distance based on the start and end location.
+- Mobile app:
+ - **Manually create**: Manually enter the miles for the trip and your mileage rate
+ - **Odometer**: Enter your odometer reading before and after the trip
+ - **Start GPS**: Currently under development and unavailable for use.
+
+{% include info.html %}
+When adding a distance expense, the rates available are determined by the rates set in your workspace rate settings. To update these rates or add a new rate, you must be a Workspace Admin.
+{% include end-info.html %}
+
+{% include selector.html values="desktop, mobile" %}
+
+{% include option.html value="desktop" %}
+
+1. Click the **Expenses** tab.
+2. Click **New Expense**.
+3. Select the expense type.
+ - **Manually create**:
+ - Enter the number of miles for the trip.
+ - Select your rate.
+ - If desired, select the category, add a description, or select a report to add the expense to.
+ - Click **Save**.
+ - **Create from map**:
+ - Add your start location as point A.
+ - Add your end location as point B.
+ - If applicable, click **Add Destination** to add additional stops.
+ - To generate a map receipt, leave the Create Receipt checkbox selected.
+ - Click **Save**.
+ - Select your rate.
+ - If desired, select the category, add a description, or select a report to add the expense to.
+ - Click **Save**.
+
+{% include end-option.html %}
+
+{% include option.html value="mobile" %}
+
+1. Click the + icon in the top right corner.
+2. Under the Distance section, select the expense type.
+ - **Manually create**:
+ - Enter your mileage.
+ - Select your rate.
+ - If desired, click **More Options** to select the category, add a description, or select a report to add the expense to.
+ - Click **Save**.
+ - **Odometer**:
+ - Enter your vehicle’s odometer reading before the trip.
+ - Enter your vehicle’s odometer reading after the trip.
+ - Select your rate.
+ - If desired, click **More Options** to select the category, add a description, or select a report to add the expense to.
+ - Click **Save**.
+{% include end-option.html %}
+
+{% include end-selector.html %}
+
+
+
diff --git a/docs/articles/expensify-classic/settings/Copilot.md b/docs/articles/expensify-classic/settings/Copilot.md
deleted file mode 100644
index 31bc0eff60e6..000000000000
--- a/docs/articles/expensify-classic/settings/Copilot.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-title: Copilot
-description: Safely delegate tasks without sharing login information.
----
-
-# About
-The Copilot feature allows you to safely delegate tasks without sharing login information. Your chosen user can access your account through their own Expensify account, with customizable permissions to manage expenses, create reports, and more. This can even be extended to users outside your policy or domain.
-
-# How-to
-# How to add a Copilot
-1. Log into the Expensify desktop website.
-2. Navigate to *Settings > Account > Account Details > _Copilot: Delegated Access_*.
-3. Enter the email address or phone number of your Copilot and select whether you want to give them Full Access or the ability to Submit Only.
- - *Full Access Copilot*: Your Copilot will have full access to your account. Nearly every action you can do and everything you can see in your account will also be available to your Copilot. They *will not* have the ability to add or remove other Copilots from your account.
- - *Submit Only Copilot*: Your Copilot will have the same limitations as a Full Access Copilot, with the added restriction of not being able to approve reports on your behalf.
-4. Click Invite Copilot.
-
-If your Copilot already has an Expensify account, they will get an email notifying them that they can now access your account from within their account as well.
-If they do not already have an Expensify account, they will be provided with a link to create one. Once they have created their Expensify account, they will be able to access your account from within their own account.
-
-# How to use Copilot
-A designated copilot can access another account via the Expensify website or the mobile app.
-
-## How to switch to Copilot mode (on the Expensify website):
-1. Click your profile icon in the upper left side of the page.
-2. In the “Copilot Access” section of the dropdown, choose the account you wish to access.
-3. When you Copilot into someone else’s account, the Expensify header will change color and an airplane icon will appear.
-4. You can return to your own account at any time by accessing the user menu and choosing “Return to your account”.
-
-## How to switch to Copilot Mode (on the mobile app):
-1. Tap on the menu icon on the top left-hand side of the screen, then tap your profile icon.
-2. Tap “Switch to Copilot Mode”, then choose the account you wish to access.
-3. You can return to your own account at any time by accessing the user menu and choosing “Return to your account”.
-
-# How to remove a Copilot
-If you ever need to remove a Copilot, you can do so by following the below steps:
-1. Log into the Expensify desktop website
-2. Navigate to *Settings > Your Account > Account Details > _Copilot: Delegated Access_*
-3. Click the red X next to the Copilot you'd like to remove
-
-
-# Deep Dive
-## Copilot Permissions
-A Copilot can do the following actions in your account:
-- Prepare expenses on your behalf
-- Approve and reimburse others' expenses on your behalf (Note: this applies only to **Full Access** Copilots)
-- View and make changes to your account/domain/policy settings
-- View all expenses you can see within your own account
-
-## Copilot restrictions
-A Copilot cannot do the following actions in your account:
-- Change or reset your password
-- Add/remove other Copilots
-
-## Forwarding receipts to receipts@expensify.com as a Copilot
-To ensure a receipt is routed to the Expensify account in which you are a copilot rather than your own you’ll need to do the following:
-1. Forward the email to receipts@expensify.com
-2. Put the email of the account in which you are a copilot in the subject line
-3. Send
-
-
-{% include faq-begin.md %}
-## Can a Copilot's Secondary Login be used to forward receipts?
-Yes! A Copilot can use any of the email addresses tied to their account to forward receipts into the account of the person they're assisting.
-
-## I'm in Copilot mode for an account; Can I add another Copilot to that account on their behalf?
-No, only the original account holder can add another Copilot to the account.
-## Is there a restriction on the number of Copilots I can have or the number of users for whom I can act as a Copilot?
-There is no limit! You can have as many Copilots as you like, and you can be a Copilot for as many users as you need.
-
-{% include faq-end.md %}
diff --git a/docs/articles/expensify-classic/workspaces/Change-member-workspace-roles.md b/docs/articles/expensify-classic/workspaces/Change-member-workspace-roles.md
new file mode 100644
index 000000000000..29fbc8b46323
--- /dev/null
+++ b/docs/articles/expensify-classic/workspaces/Change-member-workspace-roles.md
@@ -0,0 +1,26 @@
+---
+title: Change member workspace roles
+description: Update a member's role for a workspace
+---
+
+
+To change the roles and permissions for members of your workspace,
+
+1. Hover over Settings, then click **Workspaces**.
+2. Click the **Group** tab on the left.
+3. Click the desired workspace name.
+4. Click the **Members** tab on the left.
+5. Click the Settings icon next to the desired member.
+6. Select a new role for the member.
+
+| | Employee | Auditor | Workspace Admin |
+|---------------------------|----------------------------------|---------|-----------------|
+| Submit reports | Yes | Yes | Yes |
+| Comment on reports | Yes | Yes | Yes |
+| Approve workspace reports | Only reports submitted to them | Yes | Yes |
+| Edit workspace settings | No | No | Yes |
+
+7. If your workspace uses Advanced Approvals, select an “Approves to.” This determines who the member’s reports must be approved by, if applicable. If “no one” is selected, then any one with the Auditor or Workspace Admin role can approve the member’s reports.
+8. Click **Save**.
+
+
diff --git a/src/CONST.ts b/src/CONST.ts
index d13c3b374f5a..12c254736cdb 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -335,6 +335,7 @@ const CONST = {
TRACK_EXPENSE: 'trackExpense',
P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests',
WORKFLOWS_DELAYED_SUBMISSION: 'workflowsDelayedSubmission',
+ ACCOUNTING: 'accounting',
},
BUTTON_STATES: {
DEFAULT: 'default',
diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx
index d389ac4b92f0..da8e3694a7d2 100644
--- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx
+++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx
@@ -59,7 +59,6 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', dow
role={CONST.ROLE.BUTTON}
>
;
};
-type Attachment = {
- source: AvatarSource;
- isAuthTokenRequired: boolean;
- file: FileObject;
- isReceipt: boolean;
- hasBeenFlagged?: boolean;
- reportActionID?: string;
-};
-
type ImagePickerResponse = {
height: number;
name: string;
@@ -79,7 +71,7 @@ type ImagePickerResponse = {
width: number;
};
-type FileObject = File | ImagePickerResponse;
+type FileObject = Partial;
type ChildrenProps = {
displayFileInModal: (data: FileObject) => void;
@@ -181,7 +173,7 @@ function AttachmentModal({
const [isAuthTokenRequiredState, setIsAuthTokenRequiredState] = useState(isAuthTokenRequired);
const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState(null);
const [attachmentInvalidReason, setAttachmentInvalidReason] = useState(null);
- const [sourceState, setSourceState] = useState(() => source);
+ const [sourceState, setSourceState] = useState(() => source);
const [modalType, setModalType] = useState(CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE);
const [isConfirmButtonDisabled, setIsConfirmButtonDisabled] = useState(false);
const [confirmButtonFadeAnimation] = useState(() => new Animated.Value(1));
@@ -190,7 +182,7 @@ function AttachmentModal({
const {windowWidth, isSmallScreenWidth} = useWindowDimensions();
const isOverlayModalVisible = (isReceiptAttachment && isDeleteReceiptConfirmModalVisible) || (!isReceiptAttachment && isAttachmentInvalid);
- const [file, setFile] = useState | undefined>(
+ const [file, setFile] = useState(
originalFileName
? {
name: originalFileName,
@@ -211,7 +203,7 @@ function AttachmentModal({
(attachment: Attachment) => {
setSourceState(attachment.source);
setFile(attachment.file);
- setIsAuthTokenRequiredState(attachment.isAuthTokenRequired);
+ setIsAuthTokenRequiredState(attachment.isAuthTokenRequired ?? false);
onCarouselAttachmentChange(attachment);
},
[onCarouselAttachmentChange],
@@ -222,7 +214,7 @@ function AttachmentModal({
*/
const getModalType = useCallback(
(sourceURL: string, fileObject: FileObject) =>
- sourceURL && (Str.isPDF(sourceURL) || (fileObject && Str.isPDF(fileObject.name || translate('attachmentView.unknownFilename'))))
+ sourceURL && (Str.isPDF(sourceURL) || (fileObject && Str.isPDF(fileObject.name ?? translate('attachmentView.unknownFilename'))))
? CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE
: CONST.MODAL.MODAL_TYPE.CENTERED,
[translate],
@@ -292,14 +284,14 @@ function AttachmentModal({
}, [transaction, report]);
const isValidFile = useCallback((fileObject: FileObject) => {
- if (fileObject.size > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
+ if (fileObject.size !== undefined && fileObject.size > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
setIsAttachmentInvalid(true);
setAttachmentInvalidReasonTitle('attachmentPicker.attachmentTooLarge');
setAttachmentInvalidReason('attachmentPicker.sizeExceeded');
return false;
}
- if (fileObject.size < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
+ if (fileObject.size !== undefined && fileObject.size < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
setIsAttachmentInvalid(true);
setAttachmentInvalidReasonTitle('attachmentPicker.attachmentTooSmall');
setAttachmentInvalidReason('attachmentPicker.sizeNotMet');
@@ -352,7 +344,7 @@ function AttachmentModal({
setSourceState(inputSource);
setFile(updatedFile);
setModalType(inputModalType);
- } else {
+ } else if (fileObject.uri) {
const inputModalType = getModalType(fileObject.uri, fileObject);
setIsModalOpen(true);
setSourceState(fileObject.uri);
@@ -536,7 +528,6 @@ function AttachmentModal({
onNavigate={onNavigate}
onClose={closeModal}
source={source}
- onToggleKeyboard={updateConfirmButtonVisibility}
setDownloadButtonVisibility={setDownloadButtonVisibility}
/>
) : (
@@ -546,7 +537,6 @@ function AttachmentModal({
!shouldShowNotFoundPage && (
({
},
})(memo(AttachmentModal));
-export type {Attachment, FileObject};
+export type {FileObject};
diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.js b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.tsx
similarity index 71%
rename from src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.js
rename to src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.tsx
index f4cbffc0e1e4..839e05c419df 100644
--- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.js
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.tsx
@@ -1,19 +1,15 @@
-import PropTypes from 'prop-types';
import React from 'react';
+import type {StyleProp, ViewStyle} from 'react-native';
import {PixelRatio, View} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
-const propTypes = {
+type AttachmentCarouselCellRendererProps = {
/** Cell Container styles */
- style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
+ style?: StyleProp;
};
-const defaultProps = {
- style: [],
-};
-
-function AttachmentCarouselCellRenderer(props) {
+function AttachmentCarouselCellRenderer(props: AttachmentCarouselCellRendererProps) {
const styles = useThemeStyles();
const {windowWidth, isSmallScreenWidth} = useWindowDimensions();
const modalStyles = styles.centeredModalStyles(isSmallScreenWidth, true);
@@ -28,8 +24,6 @@ function AttachmentCarouselCellRenderer(props) {
);
}
-AttachmentCarouselCellRenderer.propTypes = propTypes;
-AttachmentCarouselCellRenderer.defaultProps = defaultProps;
AttachmentCarouselCellRenderer.displayName = 'AttachmentCarouselCellRenderer';
export default React.memo(AttachmentCarouselCellRenderer);
diff --git a/src/components/Attachments/AttachmentCarousel/CarouselActions.js b/src/components/Attachments/AttachmentCarousel/CarouselActions.tsx
similarity index 73%
rename from src/components/Attachments/AttachmentCarousel/CarouselActions.js
rename to src/components/Attachments/AttachmentCarousel/CarouselActions.tsx
index cf5309222c4e..6138f07809c5 100644
--- a/src/components/Attachments/AttachmentCarousel/CarouselActions.js
+++ b/src/components/Attachments/AttachmentCarousel/CarouselActions.tsx
@@ -1,25 +1,22 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
import {useEffect} from 'react';
import KeyboardShortcut from '@libs/KeyboardShortcut';
import CONST from '@src/CONST';
-const propTypes = {
+type CarouselActionsProps = {
/** Callback to cycle through attachments */
- onCycleThroughAttachments: PropTypes.func.isRequired,
+ onCycleThroughAttachments: (deltaSlide: number) => void;
};
-function CarouselActions({onCycleThroughAttachments}) {
+function CarouselActions({onCycleThroughAttachments}: CarouselActionsProps) {
useEffect(() => {
const shortcutLeftConfig = CONST.KEYBOARD_SHORTCUTS.ARROW_LEFT;
const unsubscribeLeftKey = KeyboardShortcut.subscribe(
shortcutLeftConfig.shortcutKey,
- (e) => {
- if (lodashGet(e, 'target.blur')) {
+ (event) => {
+ if (event?.target instanceof HTMLElement) {
// prevents focus from highlighting around the modal
- e.target.blur();
+ event.target.blur();
}
-
onCycleThroughAttachments(-1);
},
shortcutLeftConfig.descriptionKey,
@@ -29,12 +26,11 @@ function CarouselActions({onCycleThroughAttachments}) {
const shortcutRightConfig = CONST.KEYBOARD_SHORTCUTS.ARROW_RIGHT;
const unsubscribeRightKey = KeyboardShortcut.subscribe(
shortcutRightConfig.shortcutKey,
- (e) => {
- if (lodashGet(e, 'target.blur')) {
+ (event) => {
+ if (event?.target instanceof HTMLElement) {
// prevents focus from highlighting around the modal
- e.target.blur();
+ event.target.blur();
}
-
onCycleThroughAttachments(1);
},
shortcutRightConfig.descriptionKey,
@@ -50,6 +46,4 @@ function CarouselActions({onCycleThroughAttachments}) {
return null;
}
-CarouselActions.propTypes = propTypes;
-
export default CarouselActions;
diff --git a/src/components/Attachments/AttachmentCarousel/CarouselButtons.js b/src/components/Attachments/AttachmentCarousel/CarouselButtons.tsx
similarity index 75%
rename from src/components/Attachments/AttachmentCarousel/CarouselButtons.js
rename to src/components/Attachments/AttachmentCarousel/CarouselButtons.tsx
index a2c5dadb101d..2037ebdab086 100644
--- a/src/components/Attachments/AttachmentCarousel/CarouselButtons.js
+++ b/src/components/Attachments/AttachmentCarousel/CarouselButtons.tsx
@@ -1,8 +1,6 @@
-import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
-import _ from 'underscore';
-import * as AttachmentCarouselViewPropTypes from '@components/Attachments/propTypes';
+import type {Attachment} from '@components/Attachments/types';
import Button from '@components/Button';
import * as Expensicons from '@components/Icon/Expensicons';
import Tooltip from '@components/Tooltip';
@@ -11,36 +9,34 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
-const propTypes = {
+type CarouselButtonsProps = {
/** Where the arrows should be visible */
- shouldShowArrows: PropTypes.bool.isRequired,
+ shouldShowArrows: boolean;
/** The current page index */
- page: PropTypes.number.isRequired,
+ page: number;
/** The attachments from the carousel */
- attachments: AttachmentCarouselViewPropTypes.attachmentsPropType.isRequired,
+ attachments: Attachment[];
/** Callback to go one page back */
- onBack: PropTypes.func.isRequired,
+ onBack: () => void;
+
/** Callback to go one page forward */
- onForward: PropTypes.func.isRequired,
+ onForward: () => void;
- autoHideArrow: PropTypes.func,
- cancelAutoHideArrow: PropTypes.func,
-};
+ /** Callback for autohiding carousel button arrows */
+ autoHideArrow?: () => void;
-const defaultProps = {
- autoHideArrow: () => {},
- cancelAutoHideArrow: () => {},
+ /** Callback for cancelling autohiding of carousel button arrows */
+ cancelAutoHideArrow?: () => void;
};
-function CarouselButtons({page, attachments, shouldShowArrows, onBack, onForward, cancelAutoHideArrow, autoHideArrow}) {
+function CarouselButtons({page, attachments, shouldShowArrows, onBack, onForward, cancelAutoHideArrow, autoHideArrow}: CarouselButtonsProps) {
const theme = useTheme();
const styles = useThemeStyles();
const isBackDisabled = page === 0;
- const isForwardDisabled = page === _.size(attachments) - 1;
-
+ const isForwardDisabled = page === attachments.length - 1;
const {translate} = useLocalize();
const {isSmallScreenWidth} = useWindowDimensions();
@@ -80,8 +76,6 @@ function CarouselButtons({page, attachments, shouldShowArrows, onBack, onForward
) : null;
}
-CarouselButtons.propTypes = propTypes;
-CarouselButtons.defaultProps = defaultProps;
CarouselButtons.displayName = 'CarouselButtons';
export default CarouselButtons;
diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.js b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx
similarity index 65%
rename from src/components/Attachments/AttachmentCarousel/CarouselItem.js
rename to src/components/Attachments/AttachmentCarousel/CarouselItem.tsx
index b2c9fed64467..4988110538fe 100644
--- a/src/components/Attachments/AttachmentCarousel/CarouselItem.js
+++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx
@@ -1,8 +1,8 @@
-import PropTypes from 'prop-types';
import React, {useContext, useState} from 'react';
+import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
import AttachmentView from '@components/Attachments/AttachmentView';
-import * as AttachmentsPropTypes from '@components/Attachments/propTypes';
+import type {Attachment} from '@components/Attachments/types';
import Button from '@components/Button';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import SafeAreaConsumer from '@components/SafeAreaConsumer';
@@ -12,55 +12,27 @@ import useThemeStyles from '@hooks/useThemeStyles';
import ReportAttachmentsContext from '@pages/home/report/ReportAttachmentsContext';
import CONST from '@src/CONST';
-const propTypes = {
+type CarouselItemProps = {
/** Attachment required information such as the source and file name */
- item: PropTypes.shape({
- /** Report action ID of the attachment */
- reportActionID: PropTypes.string,
-
- /** Whether source URL requires authentication */
- isAuthTokenRequired: PropTypes.bool,
-
- /** URL to full-sized attachment or SVG function */
- source: AttachmentsPropTypes.attachmentSourcePropType.isRequired,
-
- /** Additional information about the attachment file */
- file: PropTypes.shape({
- /** File name of the attachment */
- name: PropTypes.string.isRequired,
- }).isRequired,
-
- /** Whether the attachment has been flagged */
- hasBeenFlagged: PropTypes.bool,
-
- /** The id of the transaction related to the attachment */
- transactionID: PropTypes.string,
-
- duration: PropTypes.number,
- }).isRequired,
+ item: Attachment;
/** onPress callback */
- onPress: PropTypes.func,
+ onPress?: () => void;
- isModalHovered: PropTypes.bool,
+ /** Whether attachment carousel modal is hovered over */
+ isModalHovered?: boolean;
/** Whether the attachment is currently being viewed in the carousel */
- isFocused: PropTypes.bool.isRequired,
-};
-
-const defaultProps = {
- onPress: undefined,
- isModalHovered: false,
+ isFocused: boolean;
};
-function CarouselItem({item, onPress, isFocused, isModalHovered}) {
+function CarouselItem({item, onPress, isFocused, isModalHovered}: CarouselItemProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {isAttachmentHidden} = useContext(ReportAttachmentsContext);
- // eslint-disable-next-line es/no-nullish-coalescing-operators
- const [isHidden, setIsHidden] = useState(() => isAttachmentHidden(item.reportActionID) ?? item.hasBeenFlagged);
+ const [isHidden, setIsHidden] = useState(() => (item.reportActionID ? isAttachmentHidden(item.reportActionID) : item.hasBeenFlagged));
- const renderButton = (style) => (
+ const renderButton = (style: StyleProp) => (
{children}
@@ -108,7 +81,7 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}) {
reportActionID={item.reportActionID}
isHovered={isModalHovered}
isFocused={isFocused}
- optionalVideoDuration={item.duration}
+ duration={item.duration}
/>
@@ -121,8 +94,6 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}) {
);
}
-CarouselItem.propTypes = propTypes;
-CarouselItem.defaultProps = defaultProps;
CarouselItem.displayName = 'CarouselItem';
export default CarouselItem;
diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts
index 87cabecc5878..87a9108d5f2e 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts
+++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts
@@ -2,11 +2,12 @@ import type {ForwardedRef} from 'react';
import {createContext} from 'react';
import type PagerView from 'react-native-pager-view';
import type {SharedValue} from 'react-native-reanimated';
+import type {AttachmentSource} from '@components/Attachments/types';
/** The pager items array is used within the pager to render and navigate between the images */
type AttachmentCarouselPagerItems = {
/** The source of the image is used to identify each attachment/page in the pager */
- source: string;
+ source: AttachmentSource;
/** The index of the pager item determines the order of the images in the pager */
index: number;
diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx
index 33d9f20b5e57..b7ef9309eb10 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx
+++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx
@@ -1,5 +1,6 @@
import type {ForwardedRef} from 'react';
import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
+import type {NativeSyntheticEvent} from 'react-native';
import {View} from 'react-native';
import type {NativeViewGestureHandlerProps} from 'react-native-gesture-handler';
import {createNativeWrapper} from 'react-native-gesture-handler';
@@ -7,6 +8,7 @@ import type {PagerViewProps} from 'react-native-pager-view';
import PagerView from 'react-native-pager-view';
import Animated, {useAnimatedProps, useSharedValue} from 'react-native-reanimated';
import CarouselItem from '@components/Attachments/AttachmentCarousel/CarouselItem';
+import type {Attachment, AttachmentSource} from '@components/Attachments/types';
import useThemeStyles from '@hooks/useThemeStyles';
import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext';
import usePageScrollHandler from './usePageScrollHandler';
@@ -20,22 +22,24 @@ type AttachmentCarouselPagerHandle = {
setPage: (selectedPage: number) => void;
};
-type Attachment = {
- source: string;
-};
-
type AttachmentCarouselPagerProps = {
/** The attachments to be rendered in the pager. */
items: Attachment[];
/** The source (URL) of the currently active attachment. */
- activeSource: string;
+ activeSource: AttachmentSource;
/** The index of the initial page to be rendered. */
initialPage: number;
/** A callback to be called when the page is changed. */
- onPageSelected: () => void;
+ onPageSelected: (
+ event: NativeSyntheticEvent<
+ Readonly<{
+ position: number;
+ }>
+ >,
+ ) => void;
/**
* A callback that can be used to toggle the attachment carousel arrows, when the scale of the image changes.
@@ -112,6 +116,12 @@ function AttachmentCarouselPager(
onRequestToggleArrows();
}, [isScrollEnabled.value, onRequestToggleArrows]);
+ const extractItemKey = useCallback(
+ (item: Attachment, index: number) =>
+ typeof item.source === 'string' || typeof item.source === 'number' ? `source-${item.source}` : `reportActionID-${item.reportActionID}` ?? `index-${index}`,
+ [],
+ );
+
const contextValue = useMemo(
() => ({
pagerItems,
@@ -146,14 +156,11 @@ function AttachmentCarouselPager(
const carouselItems = items.map((item, index) => (
@@ -179,3 +186,4 @@ function AttachmentCarouselPager(
AttachmentCarouselPager.displayName = 'AttachmentCarouselPager';
export default React.forwardRef(AttachmentCarouselPager);
+export type {AttachmentCarouselPagerHandle};
diff --git a/src/components/Attachments/AttachmentCarousel/attachmentCarouselPropTypes.js b/src/components/Attachments/AttachmentCarousel/attachmentCarouselPropTypes.js
deleted file mode 100644
index 5aa665683162..000000000000
--- a/src/components/Attachments/AttachmentCarousel/attachmentCarouselPropTypes.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import PropTypes from 'prop-types';
-import transactionPropTypes from '@components/transactionPropTypes';
-import reportActionPropTypes from '@pages/home/report/reportActionPropTypes';
-import reportPropTypes from '@pages/reportPropTypes';
-
-const propTypes = {
- /** source is used to determine the starting index in the array of attachments */
- source: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
-
- /** Callback to update the parent modal's state with a source and name from the attachments array */
- onNavigate: PropTypes.func,
-
- /** Function to change the download button Visibility */
- setDownloadButtonVisibility: PropTypes.func,
-
- /** Object of report actions for this report */
- reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),
-
- /** The report currently being looked at */
- report: reportPropTypes.isRequired,
-
- /** The parent of `report` */
- parentReport: reportPropTypes,
-
- /** The report actions of the parent report */
- parentReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),
-
- /** The transaction attached to the parent report action */
- transaction: transactionPropTypes,
-};
-
-const defaultProps = {
- source: '',
- reportActions: {},
- parentReport: {},
- parentReportActions: {},
- transaction: {},
- onNavigate: () => {},
- setDownloadButtonVisibility: () => {},
-};
-
-export {propTypes, defaultProps};
diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.ts
similarity index 82%
rename from src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js
rename to src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.ts
index 9524c5203110..342afa1d5366 100644
--- a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js
+++ b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.ts
@@ -1,20 +1,19 @@
import {Parser as HtmlParser} from 'htmlparser2';
-import _ from 'underscore';
+import type {OnyxEntry} from 'react-native-onyx';
+import type {Attachment} from '@components/Attachments/types';
import * as FileUtils from '@libs/fileDownload/FileUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
import CONST from '@src/CONST';
+import type {ReportAction, ReportActions} from '@src/types/onyx';
/**
* Constructs the initial component state from report actions
- * @param {Object} parentReportAction
- * @param {Object} reportActions
- * @param {Object} transaction
- * @returns {Array}
*/
-function extractAttachmentsFromReport(parentReportAction, reportActions) {
- const actions = [parentReportAction, ...ReportActionsUtils.getSortedReportActions(_.values(reportActions))];
- const attachments = [];
+function extractAttachmentsFromReport(parentReportAction?: OnyxEntry, reportActions?: OnyxEntry) {
+ const actions = [...(parentReportAction ? [parentReportAction] : []), ...ReportActionsUtils.getSortedReportActions(Object.values(reportActions ?? {}))];
+ const attachments: Attachment[] = [];
+
// We handle duplicate image sources by considering the first instance as original. Selecting any duplicate
// and navigating back (<) shows the image preceding the first instance, not the selected duplicate's position.
const uniqueSources = new Set();
@@ -30,7 +29,6 @@ function extractAttachmentsFromReport(parentReportAction, reportActions) {
uniqueSources.add(source);
const splittedUrl = attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE].split('/');
attachments.unshift({
- reportActionID: null,
source: tryResolveUrlFromApiRoot(attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]),
isAuthTokenRequired: Boolean(attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]),
file: {name: splittedUrl[splittedUrl.length - 1]},
@@ -73,14 +71,14 @@ function extractAttachmentsFromReport(parentReportAction, reportActions) {
},
});
- _.forEach(actions, (action, key) => {
+ actions.forEach((action, key) => {
if (!ReportActionsUtils.shouldReportActionBeVisible(action, key) || ReportActionsUtils.isMoneyRequestAction(action)) {
return;
}
- const decision = _.get(action, ['message', 0, 'moderationDecision', 'decision'], '');
+ const decision = action?.message?.[0].moderationDecision?.decision;
const hasBeenFlagged = decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || decision === CONST.MODERATION.MODERATOR_DECISION_HIDDEN;
- const html = _.get(action, ['message', 0, 'html'], '').replace('/>', `data-flagged="${hasBeenFlagged}" data-id="${action.reportActionID}"/>`);
+ const html = (action?.message?.[0].html ?? '').replace('/>', `data-flagged="${hasBeenFlagged}" data-id="${action.reportActionID}"/>`);
htmlParser.write(html);
});
htmlParser.end();
diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.tsx
similarity index 67%
rename from src/components/Attachments/AttachmentCarousel/index.native.js
rename to src/components/Attachments/AttachmentCarousel/index.native.tsx
index f02b6690ae8e..f6d63fc9307d 100644
--- a/src/components/Attachments/AttachmentCarousel/index.native.js
+++ b/src/components/Attachments/AttachmentCarousel/index.native.tsx
@@ -1,63 +1,62 @@
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {Keyboard, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
+import type {Attachment, AttachmentSource} from '@components/Attachments/types';
import BlockingView from '@components/BlockingViews/BlockingView';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import * as Illustrations from '@components/Icon/Illustrations';
-import withLocalize from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import Navigation from '@libs/Navigation/Navigation';
import variables from '@styles/variables';
import ONYXKEYS from '@src/ONYXKEYS';
-import {defaultProps, propTypes} from './attachmentCarouselPropTypes';
import CarouselButtons from './CarouselButtons';
import extractAttachmentsFromReport from './extractAttachmentsFromReport';
+import type {AttachmentCarouselPagerHandle} from './Pager';
import AttachmentCarouselPager from './Pager';
+import type {AttachmentCaraouselOnyxProps, AttachmentCarouselProps} from './types';
import useCarouselArrows from './useCarouselArrows';
-function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, translate, onClose}) {
+function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, onClose}: AttachmentCarouselProps) {
const styles = useThemeStyles();
- const pagerRef = useRef(null);
- const [page, setPage] = useState();
- const [attachments, setAttachments] = useState([]);
- const [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows] = useCarouselArrows();
- const [activeSource, setActiveSource] = useState(source);
+ const {translate} = useLocalize();
+ const pagerRef = useRef(null);
+ const [page, setPage] = useState();
+ const [attachments, setAttachments] = useState([]);
+ const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows();
+ const [activeSource, setActiveSource] = useState(source);
- const compareImage = useCallback((attachment) => attachment.source === source, [source]);
+ const compareImage = useCallback((attachment: Attachment) => attachment.source === source, [source]);
useEffect(() => {
- const parentReportAction = parentReportActions[report.parentReportActionID];
+ const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : undefined;
const attachmentsFromReport = extractAttachmentsFromReport(parentReportAction, reportActions);
- const initialPage = _.findIndex(attachmentsFromReport, compareImage);
+ const initialPage = attachmentsFromReport.findIndex(compareImage);
// Dismiss the modal when deleting an attachment during its display in preview.
- if (initialPage === -1 && _.find(attachments, compareImage)) {
+ if (initialPage === -1 && attachments.find(compareImage)) {
Navigation.dismissModal();
} else {
setPage(initialPage);
setAttachments(attachmentsFromReport);
// Update the download button visibility in the parent modal
- setDownloadButtonVisibility(initialPage !== -1);
+ if (setDownloadButtonVisibility) {
+ setDownloadButtonVisibility(initialPage !== -1);
+ }
// Update the parent modal's state with the source and name from the mapped attachments
- if (!_.isUndefined(attachmentsFromReport[initialPage])) {
+ if (attachmentsFromReport[initialPage] !== undefined && onNavigate) {
onNavigate(attachmentsFromReport[initialPage]);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [reportActions, compareImage]);
- /**
- * Updates the page state when the user navigates between attachments
- * @param {Object} item
- * @param {number} index
- */
+ /** Updates the page state when the user navigates between attachments */
const updatePage = useCallback(
- (newPageIndex) => {
+ (newPageIndex: number) => {
Keyboard.dismiss();
setShouldShowArrows(true);
@@ -66,7 +65,9 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
setPage(newPageIndex);
setActiveSource(item.source);
- onNavigate(item);
+ if (onNavigate) {
+ onNavigate(item);
+ }
},
[setShouldShowArrows, attachments, onNavigate],
);
@@ -76,10 +77,13 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
* @param {Number} deltaSlide
*/
const cycleThroughAttachments = useCallback(
- (deltaSlide) => {
+ (deltaSlide: number) => {
+ if (page === undefined) {
+ return;
+ }
const nextPageIndex = page + deltaSlide;
updatePage(nextPageIndex);
- pagerRef.current.setPage(nextPageIndex);
+ pagerRef.current?.setPage(nextPageIndex);
autoHideArrows();
},
@@ -91,7 +95,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
* @param {Boolean} showArrows if showArrows is passed, it will set the visibility to the passed value
*/
const toggleArrows = useCallback(
- (showArrows) => {
+ (showArrows?: boolean) => {
if (showArrows === undefined) {
setShouldShowArrows((prevShouldShowArrows) => !prevShouldShowArrows);
return;
@@ -148,23 +152,15 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
);
}
-AttachmentCarousel.propTypes = propTypes;
-AttachmentCarousel.defaultProps = defaultProps;
AttachmentCarousel.displayName = 'AttachmentCarousel';
-export default compose(
- withOnyx({
- reportActions: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`,
- canEvict: false,
- },
- parentReport: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report ? report.parentReportID : '0'}`,
- },
- parentReportActions: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '0'}`,
- canEvict: false,
- },
- }),
- withLocalize,
-)(AttachmentCarousel);
+export default withOnyx({
+ parentReportActions: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`,
+ canEvict: false,
+ },
+ reportActions: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`,
+ canEvict: false,
+ },
+})(AttachmentCarousel);
diff --git a/src/components/Attachments/AttachmentCarousel/index.js b/src/components/Attachments/AttachmentCarousel/index.tsx
similarity index 65%
rename from src/components/Attachments/AttachmentCarousel/index.js
rename to src/components/Attachments/AttachmentCarousel/index.tsx
index ef6a11f6e67c..f05abfd6a0de 100644
--- a/src/components/Attachments/AttachmentCarousel/index.js
+++ b/src/components/Attachments/AttachmentCarousel/index.tsx
@@ -1,25 +1,25 @@
+import isEqual from 'lodash/isEqual';
import React, {useCallback, useEffect, useRef, useState} from 'react';
+import type {ListRenderItemInfo} from 'react-native';
import {FlatList, Keyboard, PixelRatio, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
+import type {Attachment, AttachmentSource} from '@components/Attachments/types';
import BlockingView from '@components/BlockingViews/BlockingView';
import * as Illustrations from '@components/Icon/Illustrations';
-import withLocalize from '@components/withLocalize';
-import withWindowDimensions from '@components/withWindowDimensions';
+import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import Navigation from '@libs/Navigation/Navigation';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import AttachmentCarouselCellRenderer from './AttachmentCarouselCellRenderer';
-import {defaultProps, propTypes} from './attachmentCarouselPropTypes';
import CarouselActions from './CarouselActions';
import CarouselButtons from './CarouselButtons';
import CarouselItem from './CarouselItem';
import extractAttachmentsFromReport from './extractAttachmentsFromReport';
+import type {AttachmentCaraouselOnyxProps, AttachmentCarouselProps, UpdatePageProps} from './types';
import useCarouselArrows from './useCarouselArrows';
const viewabilityConfig = {
@@ -28,79 +28,79 @@ const viewabilityConfig = {
itemVisiblePercentThreshold: 95,
};
-function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, translate}) {
+function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility}: AttachmentCarouselProps) {
const theme = useTheme();
+ const {translate} = useLocalize();
const styles = useThemeStyles();
- const scrollRef = useRef(null);
+ const scrollRef = useRef(null);
const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
const [containerWidth, setContainerWidth] = useState(0);
const [page, setPage] = useState(0);
- const [attachments, setAttachments] = useState([]);
- const [activeSource, setActiveSource] = useState(source);
- const [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows] = useCarouselArrows();
+ const [attachments, setAttachments] = useState([]);
+ const [activeSource, setActiveSource] = useState(source);
+ const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows();
- const compareImage = useCallback((attachment) => attachment.source === source, [source]);
+ const compareImage = useCallback((attachment: Attachment) => attachment.source === source, [source]);
useEffect(() => {
- const parentReportAction = parentReportActions[report.parentReportActionID];
- const attachmentsFromReport = extractAttachmentsFromReport(parentReportAction, reportActions);
+ const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : undefined;
+ const attachmentsFromReport = extractAttachmentsFromReport(parentReportAction, reportActions ?? undefined);
- const initialPage = _.findIndex(attachmentsFromReport, compareImage);
-
- if (_.isEqual(attachments, attachmentsFromReport)) {
+ if (isEqual(attachments, attachmentsFromReport)) {
return;
}
+ const initialPage = attachmentsFromReport.findIndex(compareImage);
+
// Dismiss the modal when deleting an attachment during its display in preview.
- if (initialPage === -1 && _.find(attachments, compareImage)) {
+ if (initialPage === -1 && attachments.find(compareImage)) {
Navigation.dismissModal();
} else {
setPage(initialPage);
setAttachments(attachmentsFromReport);
// Update the download button visibility in the parent modal
- setDownloadButtonVisibility(initialPage !== -1);
+ if (setDownloadButtonVisibility) {
+ setDownloadButtonVisibility(initialPage !== -1);
+ }
// Update the parent modal's state with the source and name from the mapped attachments
- if (!_.isUndefined(attachmentsFromReport[initialPage])) {
+ if (attachmentsFromReport[initialPage] !== undefined && onNavigate) {
onNavigate(attachmentsFromReport[initialPage]);
}
}
- }, [attachments, reportActions, parentReportActions, compareImage, report.parentReportActionID, setDownloadButtonVisibility, onNavigate]);
+ }, [reportActions, parentReportActions, compareImage, report.parentReportActionID, attachments, setDownloadButtonVisibility, onNavigate]);
- /**
- * Updates the page state when the user navigates between attachments
- * @param {Object} item
- * @param {number} index
- */
+ /** Updates the page state when the user navigates between attachments */
const updatePage = useCallback(
- ({viewableItems}) => {
+ ({viewableItems}: UpdatePageProps) => {
Keyboard.dismiss();
// Since we can have only one item in view at a time, we can use the first item in the array
// to get the index of the current page
- const entry = _.first(viewableItems);
+ const entry = viewableItems[0];
if (!entry) {
setActiveSource(null);
return;
}
- setPage(entry.index);
- setActiveSource(entry.item.source);
+ if (entry.index !== null) {
+ setPage(entry.index);
+ setActiveSource(entry.item.source);
+ }
- onNavigate(entry.item);
+ if (onNavigate) {
+ onNavigate(entry.item);
+ }
},
[onNavigate],
);
- /**
- * Increments or decrements the index to get another selected item
- * @param {Number} deltaSlide
- */
+ /** Increments or decrements the index to get another selected item */
const cycleThroughAttachments = useCallback(
- (deltaSlide) => {
+ (deltaSlide: number) => {
const nextIndex = page + deltaSlide;
const nextItem = attachments[nextIndex];
@@ -113,14 +113,15 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
[attachments, canUseTouchScreen, page],
);
- /**
- * Calculate items layout information to optimize scrolling performance
- * @param {*} data
- * @param {Number} index
- * @returns {{offset: Number, length: Number, index: Number}}
- */
+ const extractItemKey = useCallback(
+ (item: Attachment, index: number) =>
+ typeof item.source === 'string' || typeof item.source === 'number' ? `source-${item.source}` : `reportActionID-${item.reportActionID}` ?? `index-${index}`,
+ [],
+ );
+
+ /** Calculate items layout information to optimize scrolling performance */
const getItemLayout = useCallback(
- (_data, index) => ({
+ (data: ArrayLike | null | undefined, index: number) => ({
length: containerWidth,
offset: containerWidth * index,
index,
@@ -128,30 +129,17 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
[containerWidth],
);
- /**
- * Defines how a single attachment should be rendered
- * @param {Object} item
- * @param {String} item.reportActionID
- * @param {Boolean} item.isAuthTokenRequired
- * @param {String} item.source
- * @param {Object} item.file
- * @param {String} item.file.name
- * @param {Boolean} item.hasBeenFlagged
- * @returns {JSX.Element}
- */
+ /** Defines how a single attachment should be rendered */
const renderItem = useCallback(
- ({item, index}) => (
+ ({item}: ListRenderItemInfo) => (
setShouldShowArrows((oldState) => !oldState) : undefined}
+ onPress={canUseTouchScreen ? () => setShouldShowArrows((oldState: boolean) => !oldState) : undefined}
isModalHovered={shouldShowArrows}
- index={index}
- activeIndex={page}
/>
),
- [activeSource, attachments.length, canUseTouchScreen, page, setShouldShowArrows, shouldShowArrows],
+ [activeSource, canUseTouchScreen, setShouldShowArrows, shouldShowArrows],
);
return (
@@ -184,7 +172,6 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
{containerWidth > 0 && (
item.source}
+ keyExtractor={extractItemKey}
viewabilityConfig={viewabilityConfig}
onViewableItemsChanged={updatePage}
/>
@@ -220,24 +207,15 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
);
}
-AttachmentCarousel.propTypes = propTypes;
-AttachmentCarousel.defaultProps = defaultProps;
AttachmentCarousel.displayName = 'AttachmentCarousel';
-export default compose(
- withOnyx({
- reportActions: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`,
- canEvict: false,
- },
- parentReport: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report ? report.parentReportID : '0'}`,
- },
- parentReportActions: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '0'}`,
- canEvict: false,
- },
- }),
- withLocalize,
- withWindowDimensions,
-)(AttachmentCarousel);
+export default withOnyx({
+ parentReportActions: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`,
+ canEvict: false,
+ },
+ reportActions: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`,
+ canEvict: false,
+ },
+})(AttachmentCarousel);
diff --git a/src/components/Attachments/AttachmentCarousel/types.ts b/src/components/Attachments/AttachmentCarousel/types.ts
new file mode 100644
index 000000000000..8ba3489a5fcf
--- /dev/null
+++ b/src/components/Attachments/AttachmentCarousel/types.ts
@@ -0,0 +1,35 @@
+import type {ViewToken} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import type {Attachment, AttachmentSource} from '@components/Attachments/types';
+import type {Report, ReportActions} from '@src/types/onyx';
+
+type UpdatePageProps = {
+ viewableItems: ViewToken[];
+};
+
+type AttachmentCaraouselOnyxProps = {
+ /** Object of report actions for this report */
+ reportActions: OnyxEntry;
+
+ /** The report actions of the parent report */
+ parentReportActions: OnyxEntry;
+};
+
+type AttachmentCarouselProps = AttachmentCaraouselOnyxProps & {
+ /** Source is used to determine the starting index in the array of attachments */
+ source: AttachmentSource;
+
+ /** Callback to update the parent modal's state with a source and name from the attachments array */
+ onNavigate?: (attachment: Attachment) => void;
+
+ /** Function to change the download button Visibility */
+ setDownloadButtonVisibility?: (isButtonVisible: boolean) => void;
+
+ /** The report currently being looked at */
+ report: Report;
+
+ /** A callback that is called when swipe-down-to-close gesture happens */
+ onClose: () => void;
+};
+
+export type {AttachmentCarouselProps, UpdatePageProps, AttachmentCaraouselOnyxProps};
diff --git a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.js b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts
similarity index 72%
rename from src/components/Attachments/AttachmentCarousel/useCarouselArrows.js
rename to src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts
index 0c55c3ae519d..12ca3db4e2ff 100644
--- a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.js
+++ b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts
@@ -1,3 +1,4 @@
+import type {SetStateAction} from 'react';
import {useCallback, useEffect, useRef, useState} from 'react';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import CONST from '@src/CONST';
@@ -5,12 +6,17 @@ import CONST from '@src/CONST';
function useCarouselArrows() {
const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
const [shouldShowArrows, setShouldShowArrowsInternal] = useState(canUseTouchScreen);
- const autoHideArrowTimeout = useRef(null);
+ const autoHideArrowTimeout = useRef(null);
/**
* Cancels the automatic hiding of the arrows.
*/
- const cancelAutoHideArrows = useCallback(() => clearTimeout(autoHideArrowTimeout.current), []);
+ const cancelAutoHideArrows = useCallback(() => {
+ if (!autoHideArrowTimeout.current) {
+ return;
+ }
+ clearTimeout(autoHideArrowTimeout.current);
+ }, []);
/**
* Automatically hide the arrows if there is no interaction for 3 seconds.
@@ -27,7 +33,7 @@ function useCarouselArrows() {
}, [canUseTouchScreen, cancelAutoHideArrows]);
const setShouldShowArrows = useCallback(
- (show = true) => {
+ (show: SetStateAction = true) => {
setShouldShowArrowsInternal(show);
autoHideArrows();
},
@@ -39,7 +45,7 @@ function useCarouselArrows() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- return [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows];
+ return {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows};
}
export default useCarouselArrows;
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx
old mode 100755
new mode 100644
similarity index 54%
rename from src/components/Attachments/AttachmentView/AttachmentViewImage/index.js
rename to src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx
index 67f87b1733d3..c195c1e34554
--- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js
+++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx
@@ -1,26 +1,31 @@
import React, {memo} from 'react';
import ImageView from '@components/ImageView';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import CONST from '@src/CONST';
-import {attachmentViewImageDefaultProps, attachmentViewImagePropTypes} from './propTypes';
+import type {AttachmentViewProps} from '..';
-const propTypes = {
- ...attachmentViewImagePropTypes,
- ...withLocalizePropTypes,
+type AttachmentViewImageProps = Pick & {
+ url: string;
+
+ loadComplete: boolean;
+
+ isImage: boolean;
+
+ /** Function for handle on error */
+ onError?: () => void;
};
-function AttachmentViewImage({url, file, isAuthTokenRequired, isFocused, loadComplete, onPress, onError, isImage, translate}) {
+function AttachmentViewImage({url, file, isAuthTokenRequired, loadComplete, onPress, onError, isImage}: AttachmentViewImageProps) {
+ const {translate} = useLocalize();
const styles = useThemeStyles();
const children = (
);
@@ -30,7 +35,8 @@ function AttachmentViewImage({url, file, isAuthTokenRequired, isFocused, loadCom
disabled={loadComplete}
style={[styles.flex1, styles.flexRow, styles.alignSelfStretch]}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON}
- accessibilityLabel={file.name || translate('attachmentView.unknownFilename')}
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ accessibilityLabel={file?.name || translate('attachmentView.unknownFilename')}
>
{children}
@@ -39,8 +45,6 @@ function AttachmentViewImage({url, file, isAuthTokenRequired, isFocused, loadCom
);
}
-AttachmentViewImage.propTypes = propTypes;
-AttachmentViewImage.defaultProps = attachmentViewImageDefaultProps;
AttachmentViewImage.displayName = 'AttachmentViewImage';
-export default compose(memo, withLocalize)(AttachmentViewImage);
+export default memo(AttachmentViewImage);
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/propTypes.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/propTypes.js
deleted file mode 100644
index f2a275fc9a21..000000000000
--- a/src/components/Attachments/AttachmentView/AttachmentViewImage/propTypes.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import PropTypes from 'prop-types';
-import {attachmentViewDefaultProps, attachmentViewPropTypes} from '@components/Attachments/AttachmentView/propTypes';
-
-const attachmentViewImagePropTypes = {
- ...attachmentViewPropTypes,
-
- url: PropTypes.string.isRequired,
-
- loadComplete: PropTypes.bool.isRequired,
-
- isImage: PropTypes.bool.isRequired,
-};
-
-const attachmentViewImageDefaultProps = {
- ...attachmentViewDefaultProps,
-
- loadComplete: false,
- isImage: false,
-};
-
-export {attachmentViewImagePropTypes, attachmentViewImageDefaultProps};
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx
similarity index 79%
rename from src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js
rename to src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx
index 2f16b63aacc6..44aeb2a58b81 100644
--- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js
+++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx
@@ -1,21 +1,8 @@
-import PropTypes from 'prop-types';
import React, {memo, useCallback, useContext, useEffect} from 'react';
+import type {GestureResponderEvent} from 'react-native';
import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext';
import PDFView from '@components/PDFView';
-import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes';
-
-const baseAttachmentViewPdfPropTypes = {
- ...attachmentViewPdfPropTypes,
-
- /** Triggered when the PDF's onScaleChanged event is triggered */
- onScaleChanged: PropTypes.func,
-};
-
-const baseAttachmentViewPdfDefaultProps = {
- ...attachmentViewPdfDefaultProps,
-
- onScaleChanged: undefined,
-};
+import type AttachmentViewPdfProps from './types';
function BaseAttachmentViewPdf({
file,
@@ -28,7 +15,7 @@ function BaseAttachmentViewPdf({
onLoadComplete,
errorLabelStyles,
style,
-}) {
+}: AttachmentViewPdfProps) {
const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext);
const isScrollEnabled = attachmentCarouselPagerContext === null ? undefined : attachmentCarouselPagerContext.isScrollEnabled;
@@ -46,7 +33,7 @@ function BaseAttachmentViewPdf({
* as well as call the onScaleChanged prop of the AttachmentViewPdf component if defined.
*/
const onScaleChanged = useCallback(
- (newScale) => {
+ (newScale: number) => {
if (onScaleChangedProp !== undefined) {
onScaleChangedProp(newScale);
}
@@ -66,13 +53,13 @@ function BaseAttachmentViewPdf({
* Otherwise it means that the PDF is currently zoomed in, therefore the onTap callback should be ignored
*/
const onPress = useCallback(
- (e) => {
+ (event?: GestureResponderEvent | KeyboardEvent) => {
if (onPressProp !== undefined) {
- onPressProp(e);
+ onPressProp(event);
}
- if (attachmentCarouselPagerContext !== null && isScrollEnabled.value) {
- attachmentCarouselPagerContext.onTap(e);
+ if (attachmentCarouselPagerContext !== null && isScrollEnabled?.value) {
+ attachmentCarouselPagerContext.onTap();
}
},
[attachmentCarouselPagerContext, isScrollEnabled, onPressProp],
@@ -80,10 +67,11 @@ function BaseAttachmentViewPdf({
return (
{
isPanGestureActive.value = false;
+ if (!isScrollEnabled) {
+ return;
+ }
isScrollEnabled.value = true;
});
@@ -93,7 +96,4 @@ function AttachmentViewPdf(props) {
);
}
-AttachmentViewPdf.propTypes = attachmentViewPdfPropTypes;
-AttachmentViewPdf.defaultProps = attachmentViewPdfDefaultProps;
-
export default memo(AttachmentViewPdf);
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.tsx
similarity index 54%
rename from src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.js
rename to src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.tsx
index 103ff292760f..4ee60e9dfff5 100644
--- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.js
+++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.tsx
@@ -1,8 +1,8 @@
import React, {memo} from 'react';
import BaseAttachmentViewPdf from './BaseAttachmentViewPdf';
-import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes';
+import type AttachmentViewPdfProps from './types';
-function AttachmentViewPdf(props) {
+function AttachmentViewPdf(props: AttachmentViewPdfProps) {
return (
& {
+ encryptedSourceUrl: string;
+ onLoadComplete: (path: string) => void;
+
+ /** Additional style props */
+ style?: StyleProp;
+
+ /** Styles for the error label */
+ errorLabelStyles?: StyleProp;
+
+ /** Triggered when the PDF's onScaleChanged event is triggered */
+ onScaleChanged?: (scale: number) => void;
+};
+
+export default AttachmentViewPdfProps;
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewVideo/index.js b/src/components/Attachments/AttachmentView/AttachmentViewVideo/index.tsx
similarity index 52%
rename from src/components/Attachments/AttachmentView/AttachmentViewVideo/index.js
rename to src/components/Attachments/AttachmentView/AttachmentViewVideo/index.tsx
index 2b71e799beed..03e0c0252a66 100644
--- a/src/components/Attachments/AttachmentView/AttachmentViewVideo/index.js
+++ b/src/components/Attachments/AttachmentView/AttachmentViewVideo/index.tsx
@@ -1,28 +1,17 @@
-import PropTypes from 'prop-types';
import React from 'react';
import VideoPlayer from '@components/VideoPlayer';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
+import type {AttachmentViewProps} from '..';
-const propTypes = {
+type AttachmentViewVideoProps = Pick & {
/** Video file source URL */
- source: PropTypes.string.isRequired,
+ source: string;
- /** Whether the video is currently being hovered over */
- isHovered: PropTypes.bool,
-
- shouldUseSharedVideoElement: PropTypes.bool,
-
- videoDuration: PropTypes.number,
-};
-
-const defaultProps = {
- isHovered: false,
- shouldUseSharedVideoElement: false,
- videoDuration: 0,
+ shouldUseSharedVideoElement?: boolean;
};
-function AttachmentViewVideo({source, isHovered, shouldUseSharedVideoElement, videoDuration}) {
+function AttachmentViewVideo({source, isHovered = false, shouldUseSharedVideoElement = false, duration = 0}: AttachmentViewVideoProps) {
const {isSmallScreen} = useWindowDimensions();
const styles = useThemeStyles();
@@ -31,14 +20,12 @@ function AttachmentViewVideo({source, isHovered, shouldUseSharedVideoElement, vi
url={source}
shouldUseSharedVideoElement={shouldUseSharedVideoElement && !isSmallScreen}
isVideoHovered={isHovered}
- videoDuration={videoDuration}
+ videoDuration={duration}
style={[styles.w100, styles.h100]}
/>
);
}
-AttachmentViewVideo.propTypes = propTypes;
-AttachmentViewVideo.defaultProps = defaultProps;
AttachmentViewVideo.displayName = 'AttachmentViewVideo';
export default React.memo(AttachmentViewVideo);
diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.tsx
old mode 100755
new mode 100644
similarity index 66%
rename from src/components/Attachments/AttachmentView/index.js
rename to src/components/Attachments/AttachmentView/index.tsx
index 9fe37734e8ee..2685a5cef407
--- a/src/components/Attachments/AttachmentView/index.js
+++ b/src/components/Attachments/AttachmentView/index.tsx
@@ -1,10 +1,10 @@
import Str from 'expensify-common/lib/str';
-import PropTypes from 'prop-types';
import React, {memo, useEffect, useState} from 'react';
+import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native';
import {ActivityIndicator, View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
-import * as AttachmentsPropTypes from '@components/Attachments/propTypes';
+import type {Attachment, AttachmentSource} from '@components/Attachments/types';
import DistanceEReceipt from '@components/DistanceEReceipt';
import EReceipt from '@components/EReceipt';
import Icon from '@components/Icon';
@@ -13,74 +13,61 @@ import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import Tooltip from '@components/Tooltip';
import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as CachedPDFPaths from '@libs/actions/CachedPDFPaths';
import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL';
-import compose from '@libs/compose';
import * as TransactionUtils from '@libs/TransactionUtils';
+import type {ColorValue} from '@styles/utils/types';
import variables from '@styles/variables';
import ONYXKEYS from '@src/ONYXKEYS';
+import type {Transaction} from '@src/types/onyx';
import AttachmentViewImage from './AttachmentViewImage';
import AttachmentViewPdf from './AttachmentViewPdf';
import AttachmentViewVideo from './AttachmentViewVideo';
-import {attachmentViewDefaultProps, attachmentViewPropTypes} from './propTypes';
-const propTypes = {
- ...attachmentViewPropTypes,
- ...withLocalizePropTypes,
+type AttachmentViewOnyxProps = {
+ transaction: OnyxEntry;
+};
- /** URL to full-sized attachment, SVG function, or numeric static image on native platforms */
- source: AttachmentsPropTypes.attachmentSourcePropType.isRequired,
+type AttachmentViewProps = AttachmentViewOnyxProps &
+ Attachment & {
+ /** Whether this view is the active screen */
+ isFocused?: boolean;
- /** Flag to show/hide download icon */
- shouldShowDownloadIcon: PropTypes.bool,
+ /** Function for handle on press */
+ onPress?: (e?: GestureResponderEvent | KeyboardEvent) => void;
- /** Flag to show the loading indicator */
- shouldShowLoadingSpinnerIcon: PropTypes.bool,
+ /** Whether this AttachmentView is shown as part of a AttachmentCarousel */
+ isUsedInCarousel?: boolean;
- /** Notify parent that the UI should be modified to accommodate keyboard */
- onToggleKeyboard: PropTypes.func,
+ isUsedInAttachmentModal?: boolean;
- /** Extra styles to pass to View wrapper */
- // eslint-disable-next-line react/forbid-prop-types
- containerStyles: PropTypes.arrayOf(PropTypes.object),
+ /** Flag to show/hide download icon */
+ shouldShowDownloadIcon?: boolean;
- /** Denotes whether it is a workspace avatar or not */
- isWorkspaceAvatar: PropTypes.bool,
+ /** Flag to show the loading indicator */
+ shouldShowLoadingSpinnerIcon?: boolean;
- /** Denotes whether it is an icon (ex: SVG) */
- maybeIcon: PropTypes.bool,
+ /** Notify parent that the UI should be modified to accommodate keyboard */
+ onToggleKeyboard?: (shouldFadeOut: boolean) => void;
- /** The id of the transaction related to the attachment */
- // eslint-disable-next-line react/no-unused-prop-types
- transactionID: PropTypes.string,
+ /** Extra styles to pass to View wrapper */
+ containerStyles?: StyleProp;
- /** The id of the report action related to the attachment */
- reportActionID: PropTypes.string,
+ /** Denotes whether it is a workspace avatar or not */
+ isWorkspaceAvatar?: boolean;
- isHovered: PropTypes.bool,
+ /** Denotes whether it is an icon (ex: SVG) */
+ maybeIcon?: boolean;
- optionalVideoDuration: PropTypes.number,
-};
+ fallbackSource?: AttachmentSource;
-const defaultProps = {
- ...attachmentViewDefaultProps,
- shouldShowDownloadIcon: false,
- shouldShowLoadingSpinnerIcon: false,
- onToggleKeyboard: () => {},
- containerStyles: [],
- isWorkspaceAvatar: false,
- maybeIcon: false,
- transactionID: '',
- reportActionID: '',
- isHovered: false,
- optionalVideoDuration: 0,
- fallbackSource: Expensicons.Gallery,
-};
+ isHovered?: boolean;
+ };
function AttachmentView({
source,
@@ -91,7 +78,6 @@ function AttachmentView({
shouldShowDownloadIcon,
containerStyles,
onToggleKeyboard,
- translate,
isFocused,
isUsedInCarousel,
isUsedInAttachmentModal,
@@ -101,21 +87,22 @@ function AttachmentView({
transaction,
reportActionID,
isHovered,
- optionalVideoDuration,
-}) {
+ duration,
+}: AttachmentViewProps) {
+ const {translate} = useLocalize();
const {updateCurrentlyPlayingURL} = usePlaybackContext();
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const [loadComplete, setLoadComplete] = useState(false);
- const isVideo = (typeof source === 'string' && Str.isVideo(source)) || (file && Str.isVideo(file.name));
+ const isVideo = (typeof source === 'string' && Str.isVideo(source)) || (file?.name && Str.isVideo(file.name));
useEffect(() => {
if (!isFocused && !(file && isUsedInAttachmentModal)) {
return;
}
- updateCurrentlyPlayingURL(isVideo ? source : null);
- }, [isFocused, isVideo, source, updateCurrentlyPlayingURL, file, isUsedInAttachmentModal]);
+ updateCurrentlyPlayingURL(isVideo && typeof source === 'string' ? source : null);
+ }, [file, isFocused, isUsedInAttachmentModal, isVideo, source, updateCurrentlyPlayingURL]);
const [imageError, setImageError] = useState(false);
@@ -123,11 +110,11 @@ function AttachmentView({
// Handles case where source is a component (ex: SVG) or a number
// Number may represent a SVG or an image
- if ((maybeIcon && typeof source === 'number') || _.isFunction(source)) {
- let iconFillColor = '';
- let additionalStyles = [];
- if (isWorkspaceAvatar) {
- const defaultWorkspaceAvatarColor = StyleUtils.getDefaultWorkspaceAvatarColor(file.name);
+ if (typeof source === 'function' || (maybeIcon && typeof source === 'number')) {
+ let iconFillColor: ColorValue | undefined = '';
+ let additionalStyles: ViewStyle[] = [];
+ if (isWorkspaceAvatar && file) {
+ const defaultWorkspaceAvatarColor = StyleUtils.getDefaultWorkspaceAvatarColor(file.name ?? '');
iconFillColor = defaultWorkspaceAvatarColor.fill;
additionalStyles = [defaultWorkspaceAvatarColor];
}
@@ -143,7 +130,7 @@ function AttachmentView({
);
}
- if (TransactionUtils.hasEReceipt(transaction)) {
+ if (TransactionUtils.hasEReceipt(transaction) && transaction) {
return (
{
- const id = (transaction && transaction.transactionID) || reportActionID;
+ const onPDFLoadComplete = (path: string) => {
+ const id = (transaction && transaction.transactionID) ?? reportActionID;
if (path && id) {
CachedPDFPaths.add(id, path);
}
@@ -177,34 +164,31 @@ function AttachmentView({
return (
);
}
- if (TransactionUtils.isDistanceRequest(transaction)) {
+ if (TransactionUtils.isDistanceRequest(transaction) && transaction) {
return ;
}
// For this check we use both source and file.name since temporary file source is a blob
// both PDFs and images will appear as images when pasted into the text field.
// We also check for numeric source since this is how static images (used for preview) are represented in RN.
- const isImage = typeof source === 'number' || Str.isImage(source);
- if (isImage || (file && Str.isImage(file.name))) {
+ const isImage = typeof source === 'number' || (typeof source === 'string' && Str.isImage(source));
+ if (isImage || (file?.name && Str.isImage(file.name))) {
if (imageError) {
// AttachmentViewImage can't handle icon fallbacks, so we need to handle it here
- if (typeof fallbackSource === 'number' || _.isFunction(fallbackSource)) {
+ if (typeof fallbackSource === 'number' || typeof fallbackSource === 'function') {
return (
{
@@ -234,19 +216,19 @@ function AttachmentView({
);
}
- if (isVideo) {
+ if ((isVideo ?? (file?.name && Str.isVideo(file.name))) && typeof source === 'string') {
return (
);
}
return (
-
+
- {file && file.name}
+ {file?.name}
{!shouldShowLoadingSpinnerIcon && shouldShowDownloadIcon && (
@@ -279,16 +261,14 @@ function AttachmentView({
);
}
-AttachmentView.propTypes = propTypes;
-AttachmentView.defaultProps = defaultProps;
AttachmentView.displayName = 'AttachmentView';
-export default compose(
- memo,
- withLocalize,
- withOnyx({
+export default memo(
+ withOnyx({
transaction: {
key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
},
- }),
-)(AttachmentView);
+ })(AttachmentView),
+);
+
+export type {AttachmentViewProps};
diff --git a/src/components/Attachments/AttachmentView/propTypes.js b/src/components/Attachments/AttachmentView/propTypes.js
deleted file mode 100644
index 0a0d654912d3..000000000000
--- a/src/components/Attachments/AttachmentView/propTypes.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import PropTypes from 'prop-types';
-import * as AttachmentsPropTypes from '@components/Attachments/propTypes';
-
-const attachmentViewPropTypes = {
- /** Whether source url requires authentication */
- isAuthTokenRequired: PropTypes.bool,
-
- /** File object can be an instance of File or Object */
- file: AttachmentsPropTypes.attachmentFilePropType,
-
- /** Whether this view is the active screen */
- isFocused: PropTypes.bool,
-
- /** Whether this AttachmentView is shown as part of a AttachmentCarousel */
- isUsedInCarousel: PropTypes.bool,
-
- /** Function for handle on press */
- onPress: PropTypes.func,
-
- /** Handles scale changed event */
- onScaleChanged: PropTypes.func,
-};
-
-const attachmentViewDefaultProps = {
- isAuthTokenRequired: false,
- file: {
- name: '',
- },
- isFocused: false,
- isSingleElement: false,
- isUsedInCarousel: false,
- isUsedInAttachmentModal: false,
- onPress: undefined,
- onScaleChanged: () => {},
-};
-
-export {attachmentViewPropTypes, attachmentViewDefaultProps};
diff --git a/src/components/Attachments/propTypes.js b/src/components/Attachments/propTypes.js
deleted file mode 100644
index 13adc468ce64..000000000000
--- a/src/components/Attachments/propTypes.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import PropTypes from 'prop-types';
-
-const attachmentSourcePropType = PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.number]);
-const attachmentFilePropType = PropTypes.shape({
- name: PropTypes.string.isRequired,
-});
-
-const attachmentPropType = PropTypes.shape({
- /** Whether source url requires authentication */
- isAuthTokenRequired: PropTypes.bool,
-
- /** URL to full-sized attachment, SVG function, or numeric static image on native platforms */
- source: attachmentSourcePropType.isRequired,
-
- /** File object can be an instance of File or Object */
- file: attachmentFilePropType.isRequired,
-});
-
-const attachmentsPropType = PropTypes.arrayOf(attachmentPropType);
-
-export {attachmentSourcePropType, attachmentFilePropType, attachmentPropType, attachmentsPropType};
diff --git a/src/components/Attachments/types.ts b/src/components/Attachments/types.ts
new file mode 100644
index 000000000000..835482ca99d9
--- /dev/null
+++ b/src/components/Attachments/types.ts
@@ -0,0 +1,30 @@
+import type {FileObject} from '@components/AttachmentModal';
+import type IconAsset from '@src/types/utils/IconAsset';
+
+type AttachmentSource = string | IconAsset | number;
+
+type Attachment = {
+ /** Report action ID of the attachment */
+ reportActionID?: string;
+
+ /** Whether source url requires authentication */
+ isAuthTokenRequired?: boolean;
+
+ /** URL to full-sized attachment, SVG function, or numeric static image on native platforms */
+ source: AttachmentSource;
+
+ /** File object can be an instance of File or Object */
+ file?: FileObject;
+
+ /** Whether the attachment has been flagged */
+ hasBeenFlagged?: boolean;
+
+ /** The id of the transaction related to the attachment */
+ transactionID?: string;
+
+ isReceipt?: boolean;
+
+ duration?: number;
+};
+
+export type {AttachmentSource, Attachment};
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js
index ecfcdd70336f..926903be18d1 100755
--- a/src/components/EmojiPicker/EmojiPickerMenu/index.js
+++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js
@@ -109,7 +109,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected, activeEmoji}) {
initialFocusedIndex: -1,
disableCyclicTraversal: true,
onFocusedIndexChange,
- disableHorizontalKeys: isFocused,
+ allowHorizontalArrowKeys: !isFocused,
// We pass true without checking visibility of the component because if the popover is not visible this picker won't be mounted
isActive: true,
allowNegativeIndexes: true,
diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index b843443be4af..8b95474bf2fc 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -192,16 +192,18 @@ function ReportPreview({
if (isScanning) {
return translate('common.receipt');
}
- const payerOrApproverName = isPolicyExpenseChat ? ReportUtils.getPolicyName(chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true);
+ let payerOrApproverName = isPolicyExpenseChat ? ReportUtils.getPolicyName(chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true);
if (isApproved) {
return translate('iou.managerApproved', {manager: payerOrApproverName});
}
- const managerName = isPolicyExpenseChat && !hasNonReimbursableTransactions ? ReportUtils.getPolicyName(chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true);
- let paymentVerb: TranslationPaths = hasNonReimbursableTransactions ? 'iou.payerSpent' : 'iou.payerOwes';
+ let paymentVerb: TranslationPaths = 'iou.payerOwes';
if (iouSettled || iouReport?.isWaitingOnBankAccount) {
paymentVerb = 'iou.payerPaid';
+ } else if (hasNonReimbursableTransactions) {
+ paymentVerb = 'iou.payerSpent';
+ payerOrApproverName = ReportUtils.getDisplayNameForParticipant(chatReport?.ownerAccountID, true);
}
- return translate(paymentVerb, {payer: managerName});
+ return translate(paymentVerb, {payer: payerOrApproverName});
};
const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport);
diff --git a/src/components/VideoPlayer/BaseVideoPlayer.js b/src/components/VideoPlayer/BaseVideoPlayer.js
index 2043f5620912..1e9c4711f13a 100644
--- a/src/components/VideoPlayer/BaseVideoPlayer.js
+++ b/src/components/VideoPlayer/BaseVideoPlayer.js
@@ -64,16 +64,18 @@ function BaseVideoPlayer({
const isUploading = _.some(CONST.ATTACHMENT_LOCAL_URL_PREFIX, (prefix) => url.startsWith(prefix));
const shouldUseSharedVideoElementRef = useRef(shouldUseSharedVideoElement);
+ const [isFullscreen, setIsFullscreen] = useState(false);
+
const togglePlayCurrentVideo = useCallback(() => {
videoResumeTryNumber.current = 0;
if (!isCurrentlyURLSet) {
updateCurrentlyPlayingURL(url);
- } else if (isPlaying) {
+ } else if (isPlaying && !isFullscreen) {
pauseVideo();
- } else {
+ } else if (!isFullscreen) {
playVideo();
}
- }, [isCurrentlyURLSet, isPlaying, pauseVideo, playVideo, updateCurrentlyPlayingURL, url]);
+ }, [isCurrentlyURLSet, isPlaying, pauseVideo, playVideo, updateCurrentlyPlayingURL, url, isFullscreen]);
const showPopoverMenu = (e) => {
setPopoverAnchorPosition({horizontal: e.nativeEvent.pageX, vertical: e.nativeEvent.pageY});
@@ -124,6 +126,8 @@ function BaseVideoPlayer({
(e) => {
onFullscreenUpdate(e);
+ setIsFullscreen(e.fullscreenUpdate === VideoFullscreenUpdate.PLAYER_DID_PRESENT);
+
// fix for iOS native and mWeb: when switching to fullscreen and then exiting
// the fullscreen mode while playing, the video pauses
if (!isPlaying || e.fullscreenUpdate !== VideoFullscreenUpdate.PLAYER_DID_DISMISS) {
diff --git a/src/components/VideoPlayerContexts/PlaybackContext.tsx b/src/components/VideoPlayerContexts/PlaybackContext.tsx
index caad4ce5519b..fc07f3d20ea0 100644
--- a/src/components/VideoPlayerContexts/PlaybackContext.tsx
+++ b/src/components/VideoPlayerContexts/PlaybackContext.tsx
@@ -37,7 +37,7 @@ function PlaybackContextProvider({children}: ChildrenProps) {
}, [currentVideoPlayerRef]);
const updateCurrentlyPlayingURL = useCallback(
- (url: string) => {
+ (url: string | null) => {
if (currentlyPlayingURL && url !== currentlyPlayingURL) {
pauseVideo();
}
diff --git a/src/components/VideoPlayerContexts/types.ts b/src/components/VideoPlayerContexts/types.ts
index 4abaaf7debb9..ae46d795f4e7 100644
--- a/src/components/VideoPlayerContexts/types.ts
+++ b/src/components/VideoPlayerContexts/types.ts
@@ -6,7 +6,7 @@ import type {PopoverMenuItem} from '@components/PopoverMenu';
import type CONST from '@src/CONST';
type PlaybackContext = {
- updateCurrentlyPlayingURL: (url: string) => void;
+ updateCurrentlyPlayingURL: (url: string | null) => void;
currentlyPlayingURL: string | null;
originalParent: View | null;
sharedElement: View | null;
diff --git a/src/hooks/useArrowKeyFocusManager.ts b/src/hooks/useArrowKeyFocusManager.ts
index b11999d61cf3..a6882c600f2c 100644
--- a/src/hooks/useArrowKeyFocusManager.ts
+++ b/src/hooks/useArrowKeyFocusManager.ts
@@ -11,7 +11,7 @@ type Config = {
isActive?: boolean;
itemsPerRow?: number;
disableCyclicTraversal?: boolean;
- disableHorizontalKeys?: boolean;
+ allowHorizontalArrowKeys?: boolean;
allowNegativeIndexes?: boolean;
};
@@ -30,7 +30,7 @@ type UseArrowKeyFocusManager = [number, (index: number) => void];
* @param [config.isActive] – Whether the component is ready and should subscribe to KeyboardShortcut
* @param [config.itemsPerRow] – The number of items per row. If provided, the arrow keys will move focus horizontally as well as vertically
* @param [config.disableCyclicTraversal] – Whether to disable cyclic traversal of the list. If true, the arrow keys will have no effect when the first or last item is focused
- * @param [config.disableHorizontalKeys] – Whether to disable the right/left keys
+ * @param [config.allowHorizontalArrowKeys] – Whether to enable the right/left keys
*/
export default function useArrowKeyFocusManager({
maxIndex,
@@ -44,10 +44,9 @@ export default function useArrowKeyFocusManager({
isActive,
itemsPerRow,
disableCyclicTraversal = false,
- disableHorizontalKeys = false,
+ allowHorizontalArrowKeys = false,
allowNegativeIndexes = false,
}: Config): UseArrowKeyFocusManager {
- const allowHorizontalArrowKeys = !!itemsPerRow;
const [focusedIndex, setFocusedIndex] = useState(initialFocusedIndex);
const arrowConfig = useMemo(
() => ({
@@ -60,9 +59,9 @@ export default function useArrowKeyFocusManager({
const horizontalArrowConfig = useMemo(
() => ({
excludedNodes: shouldExcludeTextAreaNodes ? ['TEXTAREA'] : [],
- isActive: isActive && !disableHorizontalKeys,
+ isActive: isActive && allowHorizontalArrowKeys,
}),
- [isActive, shouldExcludeTextAreaNodes, disableHorizontalKeys],
+ [isActive, shouldExcludeTextAreaNodes, allowHorizontalArrowKeys],
);
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -75,16 +74,11 @@ export default function useArrowKeyFocusManager({
const nextIndex = disableCyclicTraversal ? -1 : maxIndex;
setFocusedIndex((actualIndex) => {
- let currentFocusedIndex = -1;
- if (allowHorizontalArrowKeys) {
- currentFocusedIndex = actualIndex > 0 ? actualIndex - itemsPerRow : nextIndex;
- } else {
- currentFocusedIndex = actualIndex > 0 ? actualIndex - 1 : nextIndex;
- }
+ const currentFocusedIndex = actualIndex > 0 ? actualIndex - (itemsPerRow ?? 1) : nextIndex;
let newFocusedIndex = currentFocusedIndex;
while (disabledIndexes.includes(newFocusedIndex)) {
- newFocusedIndex -= allowHorizontalArrowKeys ? itemsPerRow : 1;
+ newFocusedIndex -= itemsPerRow ?? 1;
if (newFocusedIndex < 0) {
if (disableCyclicTraversal) {
if (!allowNegativeIndexes) {
@@ -101,7 +95,7 @@ export default function useArrowKeyFocusManager({
}
return newFocusedIndex;
});
- }, [allowHorizontalArrowKeys, disableCyclicTraversal, disabledIndexes, itemsPerRow, maxIndex, allowNegativeIndexes]);
+ }, [disableCyclicTraversal, disabledIndexes, itemsPerRow, maxIndex, allowNegativeIndexes]);
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_UP, arrowUpCallback, arrowConfig);
@@ -117,10 +111,8 @@ export default function useArrowKeyFocusManager({
if (actualIndex === -1) {
currentFocusedIndex = 0;
- } else if (allowHorizontalArrowKeys) {
- currentFocusedIndex = actualIndex < maxIndex ? actualIndex + itemsPerRow : nextIndex;
} else {
- currentFocusedIndex = actualIndex < maxIndex ? actualIndex + 1 : nextIndex;
+ currentFocusedIndex = actualIndex < maxIndex ? actualIndex + (itemsPerRow ?? 1) : nextIndex;
}
if (disableCyclicTraversal && currentFocusedIndex > maxIndex) {
@@ -132,7 +124,7 @@ export default function useArrowKeyFocusManager({
if (actualIndex < 0) {
newFocusedIndex += 1;
} else {
- newFocusedIndex += allowHorizontalArrowKeys ? itemsPerRow : 1;
+ newFocusedIndex += itemsPerRow ?? 1;
}
if (newFocusedIndex > maxIndex) {
@@ -148,7 +140,7 @@ export default function useArrowKeyFocusManager({
}
return newFocusedIndex;
});
- }, [allowHorizontalArrowKeys, disableCyclicTraversal, disabledIndexes, itemsPerRow, maxIndex]);
+ }, [disableCyclicTraversal, disabledIndexes, itemsPerRow, maxIndex]);
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_DOWN, arrowDownCallback, arrowConfig);
@@ -160,8 +152,7 @@ export default function useArrowKeyFocusManager({
const nextIndex = disableCyclicTraversal ? -1 : maxIndex;
setFocusedIndex((actualIndex) => {
- let currentFocusedIndex = -1;
- currentFocusedIndex = actualIndex > 0 ? actualIndex - 1 : nextIndex;
+ const currentFocusedIndex = actualIndex > 0 ? actualIndex - 1 : nextIndex;
let newFocusedIndex = currentFocusedIndex;
@@ -187,8 +178,7 @@ export default function useArrowKeyFocusManager({
const nextIndex = disableCyclicTraversal ? maxIndex : 0;
setFocusedIndex((actualIndex) => {
- let currentFocusedIndex = -1;
- currentFocusedIndex = actualIndex < maxIndex ? actualIndex + 1 : nextIndex;
+ const currentFocusedIndex = actualIndex < maxIndex ? actualIndex + 1 : nextIndex;
let newFocusedIndex = currentFocusedIndex;
diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts
index d38700efd53d..a93efb8cbff5 100644
--- a/src/libs/ErrorUtils.ts
+++ b/src/libs/ErrorUtils.ts
@@ -40,16 +40,16 @@ function getAuthenticateErrorMessage(response: Response): keyof TranslationFlatO
* Method used to get an error object with microsecond as the key.
* @param error - error key or message to be saved
*/
-function getMicroSecondOnyxError(error: string | null, isTranslated = false): Errors {
- return {[DateUtils.getMicroseconds()]: error && [error, {isTranslated}]};
+function getMicroSecondOnyxError(error: string | null, isTranslated = false, errorKey?: number): Errors {
+ return {[errorKey ?? DateUtils.getMicroseconds()]: error && [error, {isTranslated}]};
}
/**
* Method used to get an error object with microsecond as the key and an object as the value.
* @param error - error key or message to be saved
*/
-function getMicroSecondOnyxErrorObject(error: Errors): ErrorFields {
- return {[DateUtils.getMicroseconds()]: error};
+function getMicroSecondOnyxErrorObject(error: Errors, errorKey?: number): ErrorFields {
+ return {[errorKey ?? DateUtils.getMicroseconds()]: error};
}
// We can assume that if error is a string, it has already been translated because it is server error
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index 0f83b260c8f2..529ab10fab8b 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -993,11 +993,11 @@ function getCategoryListSections(
const enabledCategories = Object.values(sortedCategories).filter((category) => category.enabled);
const categorySections: CategoryTreeSection[] = [];
- const numberOfCategories = enabledCategories.length;
+ const numberOfEnabledCategories = enabledCategories.length;
let indexOffset = 0;
- if (numberOfCategories === 0 && selectedOptions.length > 0) {
+ if (numberOfEnabledCategories === 0 && selectedOptions.length > 0) {
categorySections.push({
// "Selected" section
title: '',
@@ -1047,9 +1047,8 @@ function getCategoryListSections(
const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name);
const filteredCategories = enabledCategories.filter((category) => !selectedOptionNames.includes(category.name));
- const numberOfVisibleCategories = filteredCategories.length + selectedOptionNames.length;
- if (numberOfVisibleCategories < CONST.CATEGORY_LIST_THRESHOLD) {
+ if (numberOfEnabledCategories < CONST.CATEGORY_LIST_THRESHOLD) {
categorySections.push({
// "All" section when items amount less than the threshold
title: '',
@@ -1800,6 +1799,8 @@ function getShareLogOptions(reports: OnyxCollection, personalDetails: On
includePersonalDetails: true,
forcePolicyNamePreview: true,
includeOwnedWorkspaceChats: true,
+ includeSelfDM: true,
+ includeThreads: true,
});
}
diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts
index 071113a70fae..0f42737c270c 100644
--- a/src/libs/Permissions.ts
+++ b/src/libs/Permissions.ts
@@ -38,6 +38,10 @@ function canUseWorkflowsDelayedSubmission(betas: OnyxEntry): boolean {
return !!betas?.includes(CONST.BETAS.WORKFLOWS_DELAYED_SUBMISSION) || canUseAllBetas(betas);
}
+function canUseAccountingIntegrations(betas: OnyxEntry): boolean {
+ return !!betas?.includes(CONST.BETAS.ACCOUNTING) || canUseAllBetas(betas);
+}
+
/**
* Link previews are temporarily disabled.
*/
@@ -55,4 +59,5 @@ export default {
canUseReportFields,
canUseP2PDistanceRequests,
canUseWorkflowsDelayedSubmission,
+ canUseAccountingIntegrations,
};
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index aca25f4926a8..a3ec8cebc992 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -2175,8 +2175,7 @@ function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry<
const moneyRequestTotal = getMoneyRequestSpendBreakdown(report).totalDisplaySpend;
const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report?.currency);
- const payerOrApproverName =
- isExpenseReport(report) && !hasNonReimbursableTransactions(report?.reportID ?? '') ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report?.managerID) ?? '';
+ let payerOrApproverName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report?.managerID) ?? '';
const payerPaidAmountMessage = Localize.translateLocal('iou.payerPaidAmount', {
payer: payerOrApproverName,
amount: formattedAmount,
@@ -2193,7 +2192,8 @@ function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry<
return `${payerPaidAmountMessage} • ${Localize.translateLocal('iou.pending')}`;
}
- if (hasNonReimbursableTransactions(report?.reportID)) {
+ if (!isSettled(report?.reportID) && hasNonReimbursableTransactions(report?.reportID)) {
+ payerOrApproverName = getDisplayNameForParticipant(report?.ownerAccountID) ?? '';
return Localize.translateLocal('iou.payerSpentAmount', {payer: payerOrApproverName, amount: formattedAmount});
}
@@ -2548,7 +2548,7 @@ function getReportPreviewMessage(
const containsNonReimbursable = hasNonReimbursableTransactions(report.reportID);
const totalAmount = getMoneyRequestSpendBreakdown(report).totalDisplaySpend;
const policyName = getPolicyName(report, false, policy);
- const payerName = isExpenseReport(report) && !containsNonReimbursable ? policyName : getDisplayNameForParticipant(report.managerID, !isPreviewMessageForParentChatReport);
+ const payerName = isExpenseReport(report) ? policyName : getDisplayNameForParticipant(report.managerID, !isPreviewMessageForParentChatReport);
const formattedAmount = CurrencyUtils.convertToDisplayString(totalAmount, report.currency);
@@ -2628,10 +2628,10 @@ function getReportPreviewMessage(
}
if (containsNonReimbursable) {
- return Localize.translateLocal('iou.payerSpentAmount', {payer: payerName ?? '', amount: formattedAmount});
+ return Localize.translateLocal('iou.payerSpentAmount', {payer: getDisplayNameForParticipant(report.ownerAccountID) ?? '', amount: formattedAmount});
}
- return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount, comment});
+ return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount});
}
/**
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 5632268ef6ca..77954810f462 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -417,10 +417,10 @@ function resetMoneyRequestInfo(id = '') {
}
/** Helper function to get the receipt error for money requests, or the generic error if there's no receipt */
-function getReceiptError(receipt?: Receipt, filename?: string, isScanRequest = true): Errors | ErrorFields {
+function getReceiptError(receipt?: Receipt, filename?: string, isScanRequest = true, errorKey?: number): Errors | ErrorFields {
return isEmptyObject(receipt) || !isScanRequest
- ? ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage')
- : ErrorUtils.getMicroSecondOnyxErrorObject({error: CONST.IOU.RECEIPT_ERROR, source: receipt.source?.toString() ?? '', filename: filename ?? ''});
+ ? ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage', false, errorKey)
+ : ErrorUtils.getMicroSecondOnyxErrorObject({error: CONST.IOU.RECEIPT_ERROR, source: receipt.source?.toString() ?? '', filename: filename ?? ''}, errorKey);
}
function needsToBeManuallySubmitted(iouReport: OnyxTypes.Report) {
@@ -708,6 +708,8 @@ function buildOnyxDataForMoneyRequest(
},
);
+ const errorKey = DateUtils.getMicroseconds();
+
const failureData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
@@ -763,7 +765,7 @@ function buildOnyxDataForMoneyRequest(
[iouCreatedAction.reportActionID]: {
// Disabling this line since transaction.filename can be an empty string
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- errors: getReceiptError(transaction.receipt, transaction.filename || transaction.receipt?.filename, isScanRequest),
+ errors: getReceiptError(transaction.receipt, transaction.filename || transaction.receipt?.filename, isScanRequest, errorKey),
},
[iouAction.reportActionID]: {
errors: ErrorUtils.getMicroSecondOnyxError(null),
@@ -773,7 +775,7 @@ function buildOnyxDataForMoneyRequest(
[iouAction.reportActionID]: {
// Disabling this line since transaction.filename can be an empty string
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- errors: getReceiptError(transaction.receipt, transaction.filename || transaction.receipt?.filename, isScanRequest),
+ errors: getReceiptError(transaction.receipt, transaction.filename || transaction.receipt?.filename, isScanRequest, errorKey),
},
}),
},
@@ -783,7 +785,7 @@ function buildOnyxDataForMoneyRequest(
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`,
value: {
[transactionThreadCreatedReportAction.reportActionID]: {
- errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'),
+ errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage', false, errorKey),
},
},
},
@@ -3771,6 +3773,8 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor
});
}
+ const errorKey = DateUtils.getMicroseconds();
+
failureData.push(
{
onyxMethod: Onyx.METHOD.MERGE,
@@ -3779,7 +3783,9 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor
[reportAction.reportActionID]: {
...reportAction,
pendingAction: null,
- errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericDeleteFailureMessage'),
+ errors: {
+ [errorKey]: ['iou.error.genericDeleteFailureMessage', {isTranslated: false}],
+ },
},
},
},
@@ -3801,7 +3807,9 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor
[reportPreviewAction?.reportActionID ?? '']: {
...reportPreviewAction,
pendingAction: null,
- errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericDeleteFailureMessage'),
+ errors: {
+ [errorKey]: ['iou.error.genericDeleteFailureMessage', {isTranslated: false}],
+ },
},
},
},
diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts
index ef5a09483c67..f16b2e772a87 100644
--- a/src/libs/actions/Policy.ts
+++ b/src/libs/actions/Policy.ts
@@ -3114,7 +3114,7 @@ function clearPolicyTagErrors(policyID: string, tagName: string) {
function renamePolicyTag(policyID: string, policyTag: {oldName: string; newName: string}) {
const tagListName = Object.keys(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})[0];
- const oldTag = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`]?.[policyTag.oldName] ?? {};
+ const oldTag = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`]?.[tagListName]?.tags?.[policyTag.oldName] ?? {};
const onyxData: OnyxData = {
optimisticData: [
{
diff --git a/src/libs/actions/ReportActions.ts b/src/libs/actions/ReportActions.ts
index 72d1fe9fe63d..66deb8aca065 100644
--- a/src/libs/actions/ReportActions.ts
+++ b/src/libs/actions/ReportActions.ts
@@ -1,5 +1,4 @@
import Onyx from 'react-native-onyx';
-import type {OnyxEntry} from 'react-native-onyx';
import * as ReportActionUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import CONST from '@src/CONST';
@@ -7,7 +6,9 @@ import ONYXKEYS from '@src/ONYXKEYS';
import type ReportAction from '@src/types/onyx/ReportAction';
import * as Report from './Report';
-function clearReportActionErrors(reportID: string, reportAction: OnyxEntry) {
+type IgnoreDirection = 'parent' | 'child';
+
+function clearReportActionErrors(reportID: string, reportAction: ReportAction, keys?: string[]) {
const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction);
if (!reportAction?.reportActionID) {
@@ -34,6 +35,20 @@ function clearReportActionErrors(reportID: string, reportAction: OnyxEntry = {};
+
+ keys.forEach((key) => {
+ errors[key] = null;
+ });
+
+ Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, {
+ [reportAction.reportActionID]: {
+ errors,
+ },
+ });
+ return;
+ }
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, {
[reportAction.reportActionID]: {
errors: null,
@@ -41,7 +56,37 @@ function clearReportActionErrors(reportID: string, reportAction: OnyxEntry errorKeys.includes(err));
+
+ clearAllRelatedReportActionErrors(report.parentReportID, parentReportAction, 'child', parentErrorKeys);
+ }
+
+ if (reportAction.childReportID && ignore !== 'child') {
+ const childActions = ReportActionUtils.getAllReportActions(reportAction.childReportID);
+ Object.values(childActions).forEach((action) => {
+ const childErrorKeys = Object.keys(action.errors ?? {}).filter((err) => errorKeys.includes(err));
+ clearAllRelatedReportActionErrors(reportAction.childReportID ?? '', action, 'parent', childErrorKeys);
+ });
+ }
+}
+
export {
// eslint-disable-next-line import/prefer-default-export
- clearReportActionErrors,
+ clearAllRelatedReportActionErrors,
};
diff --git a/src/libs/focusComposerWithDelay.ts b/src/libs/focusComposerWithDelay/index.ts
similarity index 51%
rename from src/libs/focusComposerWithDelay.ts
rename to src/libs/focusComposerWithDelay/index.ts
index 6a2f85f7d311..75e8f6ca8a67 100644
--- a/src/libs/focusComposerWithDelay.ts
+++ b/src/libs/focusComposerWithDelay/index.ts
@@ -1,17 +1,18 @@
-import type {TextInput} from 'react-native';
-import * as EmojiPickerAction from './actions/EmojiPickerAction';
-import ComposerFocusManager from './ComposerFocusManager';
+import ComposerFocusManager from '@libs/ComposerFocusManager';
+import * as EmojiPickerAction from '@userActions/EmojiPickerAction';
+import setTextInputSelection from './setTextInputSelection';
+import type {FocusComposerWithDelay, InputType} from './types';
-type FocusComposerWithDelay = (shouldDelay?: boolean) => void;
/**
* Create a function that focuses the composer.
*/
-function focusComposerWithDelay(textInput: TextInput | null): FocusComposerWithDelay {
+function focusComposerWithDelay(textInput: InputType | null): FocusComposerWithDelay {
/**
* Focus the text input
* @param [shouldDelay] Impose delay before focusing the text input
+ * @param [forcedSelectionRange] Force selection range of text input
*/
- return (shouldDelay = false) => {
+ return (shouldDelay = false, forcedSelectionRange = undefined) => {
// There could be other animations running while we trigger manual focus.
// This prevents focus from making those animations janky.
if (!textInput || EmojiPickerAction.isEmojiPickerVisible()) {
@@ -20,6 +21,9 @@ function focusComposerWithDelay(textInput: TextInput | null): FocusComposerWithD
if (!shouldDelay) {
textInput.focus();
+ if (forcedSelectionRange) {
+ setTextInputSelection(textInput, forcedSelectionRange);
+ }
return;
}
ComposerFocusManager.isReadyToFocus().then(() => {
@@ -27,6 +31,9 @@ function focusComposerWithDelay(textInput: TextInput | null): FocusComposerWithD
return;
}
textInput.focus();
+ if (forcedSelectionRange) {
+ setTextInputSelection(textInput, forcedSelectionRange);
+ }
});
};
}
diff --git a/src/libs/focusComposerWithDelay/setTextInputSelection.ts b/src/libs/focusComposerWithDelay/setTextInputSelection.ts
new file mode 100644
index 000000000000..fbbc4738702f
--- /dev/null
+++ b/src/libs/focusComposerWithDelay/setTextInputSelection.ts
@@ -0,0 +1,15 @@
+import type {TextInput} from 'react-native';
+import shouldSetSelectionRange from '@libs/shouldSetSelectionRange';
+import type {InputType, Selection} from './types';
+
+const setSelectionRange = shouldSetSelectionRange();
+
+const setTextInputSelection = (textInput: InputType, forcedSelectionRange: Selection) => {
+ if (setSelectionRange) {
+ (textInput as HTMLTextAreaElement).setSelectionRange(forcedSelectionRange.start, forcedSelectionRange.end);
+ } else {
+ (textInput as TextInput).setSelection(forcedSelectionRange.start, forcedSelectionRange.end);
+ }
+};
+
+export default setTextInputSelection;
diff --git a/src/libs/focusComposerWithDelay/types.ts b/src/libs/focusComposerWithDelay/types.ts
new file mode 100644
index 000000000000..4cd2f785f2bc
--- /dev/null
+++ b/src/libs/focusComposerWithDelay/types.ts
@@ -0,0 +1,12 @@
+import type {TextInput} from 'react-native';
+
+type Selection = {
+ start: number;
+ end: number;
+};
+
+type FocusComposerWithDelay = (shouldDelay?: boolean, forcedSelectionRange?: Selection) => void;
+
+type InputType = TextInput | HTMLTextAreaElement;
+
+export type {Selection, FocusComposerWithDelay, InputType};
diff --git a/src/libs/shouldSetSelectionRange/index.ts b/src/libs/shouldSetSelectionRange/index.ts
new file mode 100644
index 000000000000..1b28bdef07cb
--- /dev/null
+++ b/src/libs/shouldSetSelectionRange/index.ts
@@ -0,0 +1,5 @@
+import type ShouldSetSelectionRange from './types';
+
+const shouldSetSelectionRange: ShouldSetSelectionRange = () => false;
+
+export default shouldSetSelectionRange;
diff --git a/src/libs/shouldSetSelectionRange/index.website.ts b/src/libs/shouldSetSelectionRange/index.website.ts
new file mode 100644
index 000000000000..655193ebfb67
--- /dev/null
+++ b/src/libs/shouldSetSelectionRange/index.website.ts
@@ -0,0 +1,5 @@
+import type ShouldSetSelectionRange from './types';
+
+const shouldSetSelectionRange: ShouldSetSelectionRange = () => true;
+
+export default shouldSetSelectionRange;
diff --git a/src/libs/shouldSetSelectionRange/types.ts b/src/libs/shouldSetSelectionRange/types.ts
new file mode 100644
index 000000000000..e0a80614a32e
--- /dev/null
+++ b/src/libs/shouldSetSelectionRange/types.ts
@@ -0,0 +1,3 @@
+type ShouldSetSelectionRange = () => boolean;
+
+export default ShouldSetSelectionRange;
diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx
index cdec6945d302..84d5cf5108e9 100644
--- a/src/pages/home/report/ReportActionItem.tsx
+++ b/src/pages/home/report/ReportActionItem.tsx
@@ -829,7 +829,7 @@ function ReportActionItem({
/>
ReportActions.clearReportActionErrors(report.reportID, action)}
+ onClose={() => ReportActions.clearAllRelatedReportActionErrors(report.reportID, action)}
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
pendingAction={
draftMessage !== undefined ? undefined : action.pendingAction ?? (action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : undefined)
diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx
index b5c18976aa86..0473450574d3 100644
--- a/src/pages/home/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx
@@ -27,6 +27,7 @@ import * as Browser from '@libs/Browser';
import * as ComposerUtils from '@libs/ComposerUtils';
import * as EmojiUtils from '@libs/EmojiUtils';
import focusComposerWithDelay from '@libs/focusComposerWithDelay';
+import type {Selection} from '@libs/focusComposerWithDelay/types';
import focusEditAfterCancelDelete from '@libs/focusEditAfterCancelDelete';
import onyxSubscribe from '@libs/onyxSubscribe';
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
@@ -42,6 +43,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type * as OnyxTypes from '@src/types/onyx';
import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu';
+import shouldUseEmojiPickerSelection from './shouldUseEmojiPickerSelection';
type ReportActionItemMessageEditProps = {
/** All the data of the action */
@@ -68,6 +70,7 @@ const emojiButtonID = 'emojiButton';
const messageEditInput = 'messageEditInput';
const isMobileSafari = Browser.isMobileSafari();
+const shouldUseForcedSelectionRange = shouldUseEmojiPickerSelection();
function ReportActionItemMessageEdit(
{action, draftMessage, reportID, index, shouldDisableEmojiPicker = false, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE}: ReportActionItemMessageEditProps,
@@ -108,10 +111,7 @@ function ReportActionItemMessageEdit(
}
return initialDraft;
});
- const [selection, setSelection] = useState<{
- start: number;
- end: number;
- }>(getInitialSelection);
+ const [selection, setSelection] = useState(getInitialSelection);
const [isFocused, setIsFocused] = useState(false);
const {hasExceededMaxCommentLength, validateCommentMaxLength} = useHandleExceedMaxCommentLength();
const [modal, setModal] = useState({
@@ -124,6 +124,7 @@ function ReportActionItemMessageEdit(
const isFocusedRef = useRef(false);
const insertedEmojis = useRef([]);
const draftRef = useRef(draft);
+ const emojiPickerSelectionRef = useRef(undefined);
useEffect(() => {
if (ReportActionsUtils.isDeletedAction(action) || Boolean(action.message && draftMessage === action.message[0].html) || Boolean(prevDraftMessage === draftMessage)) {
@@ -336,10 +337,17 @@ function ReportActionItemMessageEdit(
* @param emoji
*/
const addEmojiToTextBox = (emoji: string) => {
- setSelection((prevSelection) => ({
- start: prevSelection.start + emoji.length + CONST.SPACE_LENGTH,
- end: prevSelection.start + emoji.length + CONST.SPACE_LENGTH,
- }));
+ const newSelection = {
+ start: selection.start + emoji.length + CONST.SPACE_LENGTH,
+ end: selection.start + emoji.length + CONST.SPACE_LENGTH,
+ };
+ setSelection(newSelection);
+
+ if (shouldUseForcedSelectionRange) {
+ // On Android and Chrome mobile, focusing the input sets the cursor position back to the start.
+ // To fix this, immediately set the selection again after focusing the input.
+ emojiPickerSelectionRef.current = newSelection;
+ }
updateDraft(ComposerUtils.insertText(draft, selection, `${emoji} `));
};
@@ -453,7 +461,9 @@ function ReportActionItemMessageEdit(
focus(true)}
+ onModalHide={() => {
+ focus(true, emojiPickerSelectionRef.current ? {...emojiPickerSelectionRef.current} : undefined);
+ }}
onEmojiSelected={addEmojiToTextBox}
id={emojiButtonID}
emojiPickerID={action.reportActionID}
diff --git a/src/pages/home/report/ReportAttachments.tsx b/src/pages/home/report/ReportAttachments.tsx
index de145d5ef7e6..82b49d1e260c 100644
--- a/src/pages/home/report/ReportAttachments.tsx
+++ b/src/pages/home/report/ReportAttachments.tsx
@@ -1,7 +1,7 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useCallback} from 'react';
-import type {Attachment} from '@components/AttachmentModal';
import AttachmentModal from '@components/AttachmentModal';
+import type {Attachment} from '@components/Attachments/types';
import ComposerFocusManager from '@libs/ComposerFocusManager';
import Navigation from '@libs/Navigation/Navigation';
import type {AuthScreensParamList} from '@libs/Navigation/types';
diff --git a/src/pages/home/report/shouldUseEmojiPickerSelection/index.android.ts b/src/pages/home/report/shouldUseEmojiPickerSelection/index.android.ts
new file mode 100644
index 000000000000..b8ffbc0518b0
--- /dev/null
+++ b/src/pages/home/report/shouldUseEmojiPickerSelection/index.android.ts
@@ -0,0 +1,5 @@
+import type ShouldUseEmojiPickerSelection from './types';
+
+const shouldUseEmojiPickerSelection: ShouldUseEmojiPickerSelection = () => true;
+
+export default shouldUseEmojiPickerSelection;
diff --git a/src/pages/home/report/shouldUseEmojiPickerSelection/index.ts b/src/pages/home/report/shouldUseEmojiPickerSelection/index.ts
new file mode 100644
index 000000000000..2f94b3656e12
--- /dev/null
+++ b/src/pages/home/report/shouldUseEmojiPickerSelection/index.ts
@@ -0,0 +1,5 @@
+import type ShouldUseEmojiPickerSelection from './types';
+
+const shouldUseEmojiPickerSelection: ShouldUseEmojiPickerSelection = () => false;
+
+export default shouldUseEmojiPickerSelection;
diff --git a/src/pages/home/report/shouldUseEmojiPickerSelection/index.website.ts b/src/pages/home/report/shouldUseEmojiPickerSelection/index.website.ts
new file mode 100644
index 000000000000..ff1982325d1d
--- /dev/null
+++ b/src/pages/home/report/shouldUseEmojiPickerSelection/index.website.ts
@@ -0,0 +1,8 @@
+import * as Browser from '@libs/Browser';
+import type ShouldUseEmojiPickerSelection from './types';
+
+const isMobileChrome = Browser.isMobileChrome();
+
+const shouldUseEmojiPickerSelection: ShouldUseEmojiPickerSelection = () => isMobileChrome;
+
+export default shouldUseEmojiPickerSelection;
diff --git a/src/pages/home/report/shouldUseEmojiPickerSelection/types.ts b/src/pages/home/report/shouldUseEmojiPickerSelection/types.ts
new file mode 100644
index 000000000000..bbf96f867e7a
--- /dev/null
+++ b/src/pages/home/report/shouldUseEmojiPickerSelection/types.ts
@@ -0,0 +1,3 @@
+type ShouldUseEmojiPickerSelection = () => boolean;
+
+export default ShouldUseEmojiPickerSelection;
diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx
index 6533d79e03b9..a70f3959ceb2 100644
--- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx
+++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx
@@ -60,10 +60,12 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps)
[policy?.customUnits],
);
const customUnitRates: Record = useMemo(() => customUnit?.rates ?? {}, [customUnit]);
- const canDeleteSelectedRates = selectedDistanceRates.length !== Object.values(customUnitRates).length;
- const canDisableSelectedRates = Object.values(customUnitRates)
- .filter((rate: Rate) => !selectedDistanceRates.includes(rate))
- .some((rate) => rate.enabled);
+ // Filter out rates that will be deleted
+ const allSelectableRates = useMemo(() => Object.values(customUnitRates).filter((rate) => rate.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE), [customUnitRates]);
+ const canDisableOrDeleteSelectedRates = useMemo(
+ () => allSelectableRates.filter((rate: Rate) => !selectedDistanceRates.some((selectedRate) => selectedRate.customUnitRateID === rate.customUnitRateID)).some((rate) => rate.enabled),
+ [allSelectableRates, selectedDistanceRates],
+ );
function fetchDistanceRates() {
Policy.openPolicyDistanceRatesPage(policyID);
@@ -179,8 +181,6 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps)
};
const toggleAllRates = () => {
- const allSelectableRates = Object.values(customUnitRates).filter((rate) => rate.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE);
-
if (selectedDistanceRates.length === allSelectableRates.length) {
setSelectedDistanceRates([]);
} else {
@@ -201,7 +201,7 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps)
text: translate('workspace.distanceRates.deleteRates', {count: selectedDistanceRates.length}),
value: CONST.POLICY.DISTANCE_RATES_BULK_ACTION_TYPES.DELETE,
icon: Expensicons.Trashcan,
- onSelected: () => (canDeleteSelectedRates ? setIsDeleteModalVisible(true) : setIsWarningModalVisible(true)),
+ onSelected: () => (canDisableOrDeleteSelectedRates ? setIsDeleteModalVisible(true) : setIsWarningModalVisible(true)),
},
];
@@ -211,7 +211,7 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps)
text: translate('workspace.distanceRates.disableRates', {count: enabledRates.length}),
value: CONST.POLICY.DISTANCE_RATES_BULK_ACTION_TYPES.DISABLE,
icon: Expensicons.DocumentSlash,
- onSelected: () => (canDisableSelectedRates ? disableRates() : setIsWarningModalVisible(true)),
+ onSelected: () => (canDisableOrDeleteSelectedRates ? disableRates() : setIsWarningModalVisible(true)),
});
}
diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
index db59980a2cf4..fb9a463de01a 100644
--- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
+++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
@@ -264,7 +264,7 @@ function WorkspaceWorkflowsPage({policy, betas, route, reimbursementAccount, ses
const isPaidGroupPolicy = PolicyUtils.isPaidGroupPolicy(policy);
const isPolicyAdmin = PolicyUtils.isPolicyAdmin(policy);
- const isLoading = reimbursementAccount?.isLoading ?? true;
+ const isLoading = reimbursementAccount?.isLoading && policy?.reimbursementChoice === undefined;
return (
diff --git a/src/styles/utils/generators/TooltipStyleUtils.ts b/src/styles/utils/generators/TooltipStyleUtils.ts
index 21b271998370..bb77a851f567 100644
--- a/src/styles/utils/generators/TooltipStyleUtils.ts
+++ b/src/styles/utils/generators/TooltipStyleUtils.ts
@@ -6,6 +6,8 @@ import FontUtils from '@styles/utils/FontUtils';
import positioning from '@styles/utils/positioning';
// eslint-disable-next-line no-restricted-imports
import spacing from '@styles/utils/spacing';
+// eslint-disable-next-line no-restricted-imports
+import titleBarHeight from '@styles/utils/titleBarHeight';
import variables from '@styles/variables';
import type StyleUtilGenerator from './types';
@@ -181,11 +183,13 @@ const createTooltipStyleUtils: StyleUtilGenerator = (
if (isTooltipSizeReady) {
// Determine if the tooltip should display below the wrapped component.
- // If either a tooltip will try to render within GUTTER_WIDTH logical pixels of the top of the screen,
+ // If either a tooltip will try to render within GUTTER_WIDTH or desktop header logical pixels of the top of the screen,
// Or the wrapped component is overlapping at top-center with another element
// we'll display it beneath its wrapped component rather than above it as usual.
shouldShowBelow =
- shouldForceRenderingBelow || yOffset - tooltipHeight < GUTTER_WIDTH || !!(tooltip && isOverlappingAtTop(tooltip, xOffset, yOffset, tooltipTargetWidth, tooltipTargetHeight));
+ shouldForceRenderingBelow ||
+ yOffset - tooltipHeight - POINTER_HEIGHT < GUTTER_WIDTH + titleBarHeight ||
+ !!(tooltip && isOverlappingAtTop(tooltip, xOffset, yOffset, tooltipTargetWidth, tooltipTargetHeight));
// When the tooltip size is ready, we can start animating the scale.
scale = currentSize;
diff --git a/src/styles/utils/titleBarHeight/index.desktop.ts b/src/styles/utils/titleBarHeight/index.desktop.ts
new file mode 100644
index 000000000000..f53087b086b7
--- /dev/null
+++ b/src/styles/utils/titleBarHeight/index.desktop.ts
@@ -0,0 +1 @@
+export default 28;
diff --git a/src/styles/utils/titleBarHeight/index.ts b/src/styles/utils/titleBarHeight/index.ts
new file mode 100644
index 000000000000..7f810d3f328d
--- /dev/null
+++ b/src/styles/utils/titleBarHeight/index.ts
@@ -0,0 +1 @@
+export default 0;
diff --git a/src/types/modules/pusher.d.ts b/src/types/modules/pusher.d.ts
index e9aa50085e8d..ffcf7744773a 100644
--- a/src/types/modules/pusher.d.ts
+++ b/src/types/modules/pusher.d.ts
@@ -8,6 +8,7 @@ declare global {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface File {
+ name?: string;
source: string;
uri?: string;
}
diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts
index 3298cd51c9f1..33cdefb749ec 100644
--- a/tests/actions/IOUTest.ts
+++ b/tests/actions/IOUTest.ts
@@ -786,13 +786,12 @@ describe('actions/IOU', () => {
.then(
() =>
new Promise((resolve) => {
- ReportActions.clearReportActionErrors(iouReportID ?? '', iouAction);
- ReportActions.clearReportActionErrors(transactionThreadReport?.reportID ?? '', transactionThreadAction);
+ ReportActions.clearAllRelatedReportActionErrors(iouReportID ?? '', iouAction);
resolve();
}),
)
- // Then the reportAction should be removed from Onyx
+ // Then the reportAction from chat report should be removed from Onyx
.then(
() =>
new Promise((resolve) => {
@@ -809,6 +808,39 @@ describe('actions/IOU', () => {
}),
)
+ // Then the reportAction from iou report should be removed from Onyx
+ .then(
+ () =>
+ new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`,
+ waitForCollectionCallback: false,
+ callback: (reportActionsForReport) => {
+ Onyx.disconnect(connectionID);
+ iouAction = Object.values(reportActionsForReport ?? {}).find((reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) ?? null;
+ expect(iouAction).toBeFalsy();
+ resolve();
+ },
+ });
+ }),
+ )
+
+ // Then the reportAction from transaction report should be removed from Onyx
+ .then(
+ () =>
+ new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport?.reportID}`,
+ waitForCollectionCallback: false,
+ callback: (reportActionsForReport) => {
+ Onyx.disconnect(connectionID);
+ expect(reportActionsForReport).toMatchObject({});
+ resolve();
+ },
+ });
+ }),
+ )
+
// Along with the associated transaction
.then(
() =>
diff --git a/workflow_tests/mocks/authorChecklistMocks.ts b/workflow_tests/mocks/authorChecklistMocks.ts
index ff8abc00f798..3a7f49783111 100644
--- a/workflow_tests/mocks/authorChecklistMocks.ts
+++ b/workflow_tests/mocks/authorChecklistMocks.ts
@@ -6,7 +6,6 @@ const AUTHORCHECKLIST__CHECKLIST__CHECKOUT__STEP_MOCK = createMockStep('Checkout
const AUTHORCHECKLIST__CHECKLIST__AUTHORCHECKLIST_JS__STEP_MOCK = createMockStep('authorChecklist.js', 'Running authorChecklist.js', 'CHECKLIST', ['GITHUB_TOKEN'], []);
const AUTHORCHECKLIST__CHECKLIST__STEP_MOCKS = [AUTHORCHECKLIST__CHECKLIST__CHECKOUT__STEP_MOCK, AUTHORCHECKLIST__CHECKLIST__AUTHORCHECKLIST_JS__STEP_MOCK];
-export {
- // eslint-disable-next-line import/prefer-default-export
+export default {
AUTHORCHECKLIST__CHECKLIST__STEP_MOCKS,
};
diff --git a/workflow_tests/mocks/cherryPickMocks.ts b/workflow_tests/mocks/cherryPickMocks.ts
index 7dc77b1b5a97..e7762731c066 100644
--- a/workflow_tests/mocks/cherryPickMocks.ts
+++ b/workflow_tests/mocks/cherryPickMocks.ts
@@ -110,4 +110,4 @@ const getCherryPickMockSteps = (upToDate: boolean, hasConflicts: boolean): StepI
CHERRYPICK__CHERRYPICK__ANNOUNCES_A_CP_FAILURE_IN_THE_ANNOUNCE_SLACK_ROOM__STEP_MOCK,
];
-export {CHERRYPICK__VALIDATEACTOR__TRUE__STEP_MOCKS, CHERRYPICK__VALIDATEACTOR__FALSE__STEP_MOCKS, CHERRYPICK__CREATENEWVERSION__STEP_MOCKS, getCherryPickMockSteps};
+export {CHERRYPICK__CREATENEWVERSION__STEP_MOCKS, CHERRYPICK__VALIDATEACTOR__FALSE__STEP_MOCKS, CHERRYPICK__VALIDATEACTOR__TRUE__STEP_MOCKS, getCherryPickMockSteps};
diff --git a/workflow_tests/mocks/claMocks.ts b/workflow_tests/mocks/claMocks.ts
index ab2d5f15a756..ca065510cd4a 100644
--- a/workflow_tests/mocks/claMocks.ts
+++ b/workflow_tests/mocks/claMocks.ts
@@ -15,8 +15,8 @@ const CLA__CLA__CLA_ASSISTANT__STEP_MOCK = createMockStep(
['path-to-signatures', 'path-to-document', 'branch', 'remote-organization-name', 'remote-repository-name', 'lock-pullrequest-aftermerge', 'allowlist'],
['GITHUB_TOKEN', 'PERSONAL_ACCESS_TOKEN'],
);
-const CLA__CLA__NO_MATCHES__STEP_MOCKS = [CLA__CLA__CLA_COMMENT_CHECK__NO_MATCH__STEP_MOCK, CLA__CLA__CLA_COMMENT_RE_CHECK__NO_MATCH__STEP_MOCK, CLA__CLA__CLA_ASSISTANT__STEP_MOCK] as const;
-const CLA__CLA__CHECK_MATCH__STEP_MOCKS = [CLA__CLA__CLA_COMMENT_CHECK__MATCH__STEP_MOCK, CLA__CLA__CLA_COMMENT_RE_CHECK__NO_MATCH__STEP_MOCK, CLA__CLA__CLA_ASSISTANT__STEP_MOCK] as const;
-const CLA__CLA__RECHECK_MATCH__STEP_MOCKS = [CLA__CLA__CLA_COMMENT_CHECK__NO_MATCH__STEP_MOCK, CLA__CLA__CLA_COMMENT_RE_CHECK__MATCH__STEP_MOCK, CLA__CLA__CLA_ASSISTANT__STEP_MOCK] as const;
+const CLA__CLA__NO_MATCHES__STEP_MOCKS = [CLA__CLA__CLA_COMMENT_CHECK__NO_MATCH__STEP_MOCK, CLA__CLA__CLA_COMMENT_RE_CHECK__NO_MATCH__STEP_MOCK, CLA__CLA__CLA_ASSISTANT__STEP_MOCK];
+const CLA__CLA__CHECK_MATCH__STEP_MOCKS = [CLA__CLA__CLA_COMMENT_CHECK__MATCH__STEP_MOCK, CLA__CLA__CLA_COMMENT_RE_CHECK__NO_MATCH__STEP_MOCK, CLA__CLA__CLA_ASSISTANT__STEP_MOCK];
+const CLA__CLA__RECHECK_MATCH__STEP_MOCKS = [CLA__CLA__CLA_COMMENT_CHECK__NO_MATCH__STEP_MOCK, CLA__CLA__CLA_COMMENT_RE_CHECK__MATCH__STEP_MOCK, CLA__CLA__CLA_ASSISTANT__STEP_MOCK];
export {CLA__CLA__NO_MATCHES__STEP_MOCKS, CLA__CLA__CHECK_MATCH__STEP_MOCKS, CLA__CLA__RECHECK_MATCH__STEP_MOCKS};
diff --git a/workflow_tests/mocks/createNewVersionMocks.ts b/workflow_tests/mocks/createNewVersionMocks.ts
index 59aa6bc910a2..d3ebdfa57734 100644
--- a/workflow_tests/mocks/createNewVersionMocks.ts
+++ b/workflow_tests/mocks/createNewVersionMocks.ts
@@ -11,9 +11,9 @@ const CREATENEWVERSION__VALIDATEACTOR__GET_USER_PERMISSIONS__WRITE__STEP_MOCK =
const CREATENEWVERSION__VALIDATEACTOR__GET_USER_PERMISSIONS__NONE__STEP_MOCK = createMockStep('Get user permissions', 'Get user permissions', 'VALIDATEACTOR', [], ['GITHUB_TOKEN'], {
PERMISSION: 'read',
});
-const CREATENEWVERSION__VALIDATEACTOR__ADMIN__STEP_MOCKS = [CREATENEWVERSION__VALIDATEACTOR__GET_USER_PERMISSIONS__ADMIN__STEP_MOCK] as const;
-const CREATENEWVERSION__VALIDATEACTOR__WRITER__STEP_MOCKS = [CREATENEWVERSION__VALIDATEACTOR__GET_USER_PERMISSIONS__WRITE__STEP_MOCK] as const;
-const CREATENEWVERSION__VALIDATEACTOR__NO_PERMISSION__STEP_MOCKS = [CREATENEWVERSION__VALIDATEACTOR__GET_USER_PERMISSIONS__NONE__STEP_MOCK] as const;
+const CREATENEWVERSION__VALIDATEACTOR__ADMIN__STEP_MOCKS = [CREATENEWVERSION__VALIDATEACTOR__GET_USER_PERMISSIONS__ADMIN__STEP_MOCK];
+const CREATENEWVERSION__VALIDATEACTOR__WRITER__STEP_MOCKS = [CREATENEWVERSION__VALIDATEACTOR__GET_USER_PERMISSIONS__WRITE__STEP_MOCK];
+const CREATENEWVERSION__VALIDATEACTOR__NO_PERMISSION__STEP_MOCKS = [CREATENEWVERSION__VALIDATEACTOR__GET_USER_PERMISSIONS__NONE__STEP_MOCK];
// createnewversion
const CREATENEWVERSION__CREATENEWVERSION__RUN_TURNSTYLE__STEP_MOCK = createMockStep('Run turnstyle', 'Run turnstyle', 'CREATENEWVERSION', ['poll-interval-seconds'], ['GITHUB_TOKEN']);
@@ -37,7 +37,7 @@ const CREATENEWVERSION__CREATENEWVERSION__STEP_MOCKS = [
CREATENEWVERSION__CREATENEWVERSION__COMMIT_NEW_VERSION__STEP_MOCK,
CREATENEWVERSION__CREATENEWVERSION__UPDATE_MAIN_BRANCH__STEP_MOCK,
CREATENEWVERSION__CREATENEWVERSION__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK,
-] as const;
+];
export {
CREATENEWVERSION__VALIDATEACTOR__ADMIN__STEP_MOCKS,
diff --git a/workflow_tests/mocks/deployBlockerMocks.ts b/workflow_tests/mocks/deployBlockerMocks.ts
index 932d4626b67b..2280950890a0 100644
--- a/workflow_tests/mocks/deployBlockerMocks.ts
+++ b/workflow_tests/mocks/deployBlockerMocks.ts
@@ -3,7 +3,7 @@ import {createMockStep} from '../utils/utils';
// updateChecklist
const DEPLOYBLOCKER__UPDATECHECKLIST__STEP_MOCK = createMockStep('updateChecklist', 'Run updateChecklist', 'UPDATECHECKLIST');
-const DEPLOYBLOCKER__UPDATECHECKLIST__STEP_MOCKS = [DEPLOYBLOCKER__UPDATECHECKLIST__STEP_MOCK] as const;
+const DEPLOYBLOCKER__UPDATECHECKLIST__STEP_MOCKS = [DEPLOYBLOCKER__UPDATECHECKLIST__STEP_MOCK];
// deployblocker
const DEPLOYBLOCKER__DEPLOYBLOCKER__CHECKOUT__STEP_MOCK = createMockStep('Checkout', 'Checkout', 'DEPLOYBLOCKER');
@@ -35,6 +35,6 @@ const DEPLOYBLOCKER__DEPLOYBLOCKER__STEP_MOCKS = [
DEPLOYBLOCKER__DEPLOYBLOCKER__POST_THE_ISSUE_IN_THE_EXPENSIFY_OPEN_SOURCE_SLACK_ROOM__STEP_MOCK,
DEPLOYBLOCKER__DEPLOYBLOCKER__COMMENT_ON_DEPLOY_BLOCKER__STEP_MOCK,
DEPLOYBLOCKER__DEPLOYBLOCKER__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK,
-] as const;
+];
export {DEPLOYBLOCKER__UPDATECHECKLIST__STEP_MOCKS, DEPLOYBLOCKER__DEPLOYBLOCKER__STEP_MOCKS};
diff --git a/workflow_tests/mocks/deployMocks.ts b/workflow_tests/mocks/deployMocks.ts
index d0795477cfca..5888320a87b2 100644
--- a/workflow_tests/mocks/deployMocks.ts
+++ b/workflow_tests/mocks/deployMocks.ts
@@ -9,12 +9,7 @@ const DEPLOY_STAGING__SETUP_GIT__STEP_MOCK = createMockStep('Setup git for OSBot
]);
const DEPLOY_STAGING__TAG_VERSION__STEP_MOCK = createMockStep('Tag version', 'Tagging new version', 'DEPLOY_STAGING');
const DEPLOY_STAGING__PUSH_TAG__STEP_MOCK = createMockStep('🚀 Push tags to trigger staging deploy 🚀', 'Pushing tag to trigger staging deploy', 'DEPLOY_STAGING');
-const DEPLOY_STAGING_STEP_MOCKS = [
- DEPLOY_STAGING__CHECKOUT__STEP_MOCK,
- DEPLOY_STAGING__SETUP_GIT__STEP_MOCK,
- DEPLOY_STAGING__TAG_VERSION__STEP_MOCK,
- DEPLOY_STAGING__PUSH_TAG__STEP_MOCK,
-] as const;
+const DEPLOY_STAGING_STEP_MOCKS = [DEPLOY_STAGING__CHECKOUT__STEP_MOCK, DEPLOY_STAGING__SETUP_GIT__STEP_MOCK, DEPLOY_STAGING__TAG_VERSION__STEP_MOCK, DEPLOY_STAGING__PUSH_TAG__STEP_MOCK];
const DEPLOY_PRODUCTION__CHECKOUT__STEP_MOCK = createMockStep('Checkout', 'Checking out', 'DEPLOY_PRODUCTION', ['ref', 'token']);
const DEPLOY_PRODUCTION__SETUP_GIT__STEP_MOCK = createMockStep(
@@ -53,6 +48,6 @@ const DEPLOY_PRODUCTION_STEP_MOCKS = [
DEPLOY_PRODUCTION__RELEASE_PR_LIST__STEP_MOCK,
DEPLOY_PRODUCTION__GENERATE_RELEASE_BODY__STEP_MOCK,
DEPLOY_PRODUCTION__CREATE_RELEASE__STEP_MOCK,
-] as const;
+];
export {DEPLOY_STAGING_STEP_MOCKS, DEPLOY_PRODUCTION_STEP_MOCKS};
diff --git a/workflow_tests/mocks/failureNotifierMocks.ts b/workflow_tests/mocks/failureNotifierMocks.ts
index 0758d4bf7a70..cbea6fce95ae 100644
--- a/workflow_tests/mocks/failureNotifierMocks.ts
+++ b/workflow_tests/mocks/failureNotifierMocks.ts
@@ -8,7 +8,6 @@ const FAILURENOTIFIER__NOTIFYFAILURE__FETCH_WORKFLOW_RUN_JOBS__STEP_MOCK = creat
const FAILURENOTIFIER__NOTIFYFAILURE__PROCESS_EACH_FAILED_JOB__STEP_MOCK = createMockStep('Process Each Failed Job', 'Process Each Failed Job', 'NOTIFYFAILURE', [], []);
const FAILURENOTIFIER__NOTIFYFAILURE__STEP_MOCKS = [FAILURENOTIFIER__NOTIFYFAILURE__FETCH_WORKFLOW_RUN_JOBS__STEP_MOCK, FAILURENOTIFIER__NOTIFYFAILURE__PROCESS_EACH_FAILED_JOB__STEP_MOCK];
-export {
- // eslint-disable-next-line import/prefer-default-export
+export default {
FAILURENOTIFIER__NOTIFYFAILURE__STEP_MOCKS,
};
diff --git a/workflow_tests/mocks/finishReleaseCycleMocks.ts b/workflow_tests/mocks/finishReleaseCycleMocks.ts
index 360bb017da88..86493fb077c9 100644
--- a/workflow_tests/mocks/finishReleaseCycleMocks.ts
+++ b/workflow_tests/mocks/finishReleaseCycleMocks.ts
@@ -73,7 +73,7 @@ const FINISHRELEASECYCLE__VALIDATE__TEAM_MEMBER_NO_BLOCKERS__STEP_MOCKS = [
FINISHRELEASECYCLE__VALIDATE__CHECK_FOR_ANY_DEPLOY_BLOCKERS_FALSE__STEP_MOCK,
FINISHRELEASECYCLE__VALIDATE__REOPEN_AND_COMMENT_ON_ISSUE_HAS_BLOCKERS__STEP_MOCK,
FINISHRELEASECYCLE__VALIDATE__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK,
-] as const;
+];
const FINISHRELEASECYCLE__VALIDATE__TEAM_MEMBER_BLOCKERS__STEP_MOCKS = [
FINISHRELEASECYCLE__VALIDATE__CHECKOUT__STEP_MOCK,
FINISHRELEASECYCLE__VALIDATE__SETUP_GIT_FOR_OSBOTIFY__STEP_MOCK,
@@ -82,7 +82,7 @@ const FINISHRELEASECYCLE__VALIDATE__TEAM_MEMBER_BLOCKERS__STEP_MOCKS = [
FINISHRELEASECYCLE__VALIDATE__CHECK_FOR_ANY_DEPLOY_BLOCKERS_TRUE__STEP_MOCK,
FINISHRELEASECYCLE__VALIDATE__REOPEN_AND_COMMENT_ON_ISSUE_HAS_BLOCKERS__STEP_MOCK,
FINISHRELEASECYCLE__VALIDATE__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK,
-] as const;
+];
// eslint-disable-next-line rulesdir/no-negated-variables
const FINISHRELEASECYCLE__VALIDATE__NOT_TEAM_MEMBER_NO_BLOCKERS__STEP_MOCKS = [
FINISHRELEASECYCLE__VALIDATE__CHECKOUT__STEP_MOCK,
@@ -92,7 +92,7 @@ const FINISHRELEASECYCLE__VALIDATE__NOT_TEAM_MEMBER_NO_BLOCKERS__STEP_MOCKS = [
FINISHRELEASECYCLE__VALIDATE__CHECK_FOR_ANY_DEPLOY_BLOCKERS_FALSE__STEP_MOCK,
FINISHRELEASECYCLE__VALIDATE__REOPEN_AND_COMMENT_ON_ISSUE_HAS_BLOCKERS__STEP_MOCK,
FINISHRELEASECYCLE__VALIDATE__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK,
-] as const;
+];
// eslint-disable-next-line rulesdir/no-negated-variables
const FINISHRELEASECYCLE__VALIDATE__NOT_TEAM_MEMBER_BLOCKERS__STEP_MOCKS = [
FINISHRELEASECYCLE__VALIDATE__CHECKOUT__STEP_MOCK,
@@ -102,7 +102,7 @@ const FINISHRELEASECYCLE__VALIDATE__NOT_TEAM_MEMBER_BLOCKERS__STEP_MOCKS = [
FINISHRELEASECYCLE__VALIDATE__CHECK_FOR_ANY_DEPLOY_BLOCKERS_TRUE__STEP_MOCK,
FINISHRELEASECYCLE__VALIDATE__REOPEN_AND_COMMENT_ON_ISSUE_HAS_BLOCKERS__STEP_MOCK,
FINISHRELEASECYCLE__VALIDATE__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK,
-] as const;
+];
// updateproduction
const FINISHRELEASECYCLE__UPDATEPRODUCTION__CHECKOUT__STEP_MOCK = createMockStep('Checkout', 'Checkout', 'UPDATEPRODUCTION', ['ref', 'token'], []);
@@ -126,7 +126,7 @@ const FINISHRELEASECYCLE__UPDATEPRODUCTION__STEP_MOCKS = [
FINISHRELEASECYCLE__UPDATEPRODUCTION__SETUP_GIT_FOR_OSBOTIFY__STEP_MOCK,
FINISHRELEASECYCLE__UPDATEPRODUCTION__UPDATE_PRODUCTION_BRANCH__STEP_MOCK,
FINISHRELEASECYCLE__UPDATEPRODUCTION__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK,
-] as const;
+];
// createnewpatchversion
const FINISHRELEASECYCLE__CREATENEWPATCHVERSION__CREATE_NEW_VERSION__STEP_MOCK = createMockStep(
@@ -140,7 +140,7 @@ const FINISHRELEASECYCLE__CREATENEWPATCHVERSION__CREATE_NEW_VERSION__STEP_MOCK =
true,
'createNewVersion',
);
-const FINISHRELEASECYCLE__CREATENEWPATCHVERSION__STEP_MOCKS = [FINISHRELEASECYCLE__CREATENEWPATCHVERSION__CREATE_NEW_VERSION__STEP_MOCK] as const;
+const FINISHRELEASECYCLE__CREATENEWPATCHVERSION__STEP_MOCKS = [FINISHRELEASECYCLE__CREATENEWPATCHVERSION__CREATE_NEW_VERSION__STEP_MOCK];
// updatestaging
const FINISHRELEASECYCLE__UPDATESTAGING__CHECKOUT__STEP_MOCK = createMockStep('Checkout', 'Checkout', 'UPDATESTAGING', ['ref', 'token'], []);
@@ -164,7 +164,7 @@ const FINISHRELEASECYCLE__UPDATESTAGING__STEP_MOCKS = [
FINISHRELEASECYCLE__UPDATESTAGING__SETUP_GIT_FOR_OSBOTIFY__STEP_MOCK,
FINISHRELEASECYCLE__UPDATESTAGING__UPDATE_STAGING_BRANCH_TO_TRIGGER_STAGING_DEPLOY__STEP_MOCK,
FINISHRELEASECYCLE__UPDATESTAGING__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK,
-] as const;
+];
export {
FINISHRELEASECYCLE__VALIDATE__TEAM_MEMBER_NO_BLOCKERS__STEP_MOCKS,
diff --git a/workflow_tests/mocks/lintMocks.ts b/workflow_tests/mocks/lintMocks.ts
index 93a3fd190a41..851555313cff 100644
--- a/workflow_tests/mocks/lintMocks.ts
+++ b/workflow_tests/mocks/lintMocks.ts
@@ -13,9 +13,8 @@ const LINT__LINT__STEP_MOCKS = [
LINT__LINT__LINT_JAVASCRIPT_WITH_ESLINT__STEP_MOCK,
LINT__LINT__VERIFY_NO_PRETTIER__STEP_MOCK,
LINT__LINT__RUN_UNUSED_SEARCHER__STEP_MOCK,
-] as const;
+];
-export {
- // eslint-disable-next-line import/prefer-default-export
+export default {
LINT__LINT__STEP_MOCKS,
};
diff --git a/workflow_tests/mocks/lockDeploysMocks.ts b/workflow_tests/mocks/lockDeploysMocks.ts
index 2c9aa1fd6d11..138b30fbb306 100644
--- a/workflow_tests/mocks/lockDeploysMocks.ts
+++ b/workflow_tests/mocks/lockDeploysMocks.ts
@@ -29,9 +29,8 @@ const LOCKDEPLOYS__LOCKSTAGINGDEPLOYS__STEP_MOCKS = [
LOCKDEPLOYS__LOCKSTAGINGDEPLOYS__WAIT_FOR_STAGING_DEPLOYS_TO_FINISH__STEP_MOCK,
LOCKDEPLOYS__LOCKSTAGINGDEPLOYS__COMMENT_IN_STAGINGDEPLOYCASH_TO_GIVE_APPLAUSE_THE_GREEN_LIGHT_TO_BEGIN_QA__STEP_MOCK,
LOCKDEPLOYS__LOCKSTAGINGDEPLOYS__ANNOUNCE_FAILED_WORKFLOW__STEP_MOCK,
-] as const;
+];
-export {
- // eslint-disable-next-line import/prefer-default-export
+export default {
LOCKDEPLOYS__LOCKSTAGINGDEPLOYS__STEP_MOCKS,
};
diff --git a/workflow_tests/mocks/platformDeployMocks.ts b/workflow_tests/mocks/platformDeployMocks.ts
index 72176e34d1fa..1a14f488a81d 100644
--- a/workflow_tests/mocks/platformDeployMocks.ts
+++ b/workflow_tests/mocks/platformDeployMocks.ts
@@ -18,12 +18,12 @@ const PLATFORM_DEPLOY__VALIDATE_ACTOR__CHECK_USER_DEPLOYER__OUTSIDER__STEP_MOCK
['GITHUB_TOKEN'],
{IS_DEPLOYER: false},
);
-const PLATFORM_DEPLOY__VALIDATE_ACTOR__TEAM_MEMBER__STEP_MOCKS = [PLATFORM_DEPLOY__VALIDATE_ACTOR__CHECK_USER_DEPLOYER__TEAM_MEMBER__STEP_MOCK] as const;
-const PLATFORM_DEPLOY__VALIDATE_ACTOR__OUTSIDER__STEP_MOCKS = [PLATFORM_DEPLOY__VALIDATE_ACTOR__CHECK_USER_DEPLOYER__OUTSIDER__STEP_MOCK] as const;
+const PLATFORM_DEPLOY__VALIDATE_ACTOR__TEAM_MEMBER__STEP_MOCKS = [PLATFORM_DEPLOY__VALIDATE_ACTOR__CHECK_USER_DEPLOYER__TEAM_MEMBER__STEP_MOCK];
+const PLATFORM_DEPLOY__VALIDATE_ACTOR__OUTSIDER__STEP_MOCKS = [PLATFORM_DEPLOY__VALIDATE_ACTOR__CHECK_USER_DEPLOYER__OUTSIDER__STEP_MOCK];
// deployChecklist
const PLATFORM_DEPLOY__DEPLOY_CHECKLIST__STEP_MOCK = createMockStep('deployChecklist', 'Run deployChecklist', 'DEPLOY_CHECKLIST');
-const PLATFORM_DEPLOY__DEPLOY_CHECKLIST__STEP_MOCKS = [PLATFORM_DEPLOY__DEPLOY_CHECKLIST__STEP_MOCK] as const;
+const PLATFORM_DEPLOY__DEPLOY_CHECKLIST__STEP_MOCKS = [PLATFORM_DEPLOY__DEPLOY_CHECKLIST__STEP_MOCK];
// android
const PLATFORM_DEPLOY__ANDROID__CHECKOUT__STEP_MOCK = createMockStep('Checkout', 'Checking out', 'ANDROID');
@@ -75,7 +75,7 @@ const PLATFORM_DEPLOY__ANDROID__STEP_MOCKS = [
PLATFORM_DEPLOY__ANDROID__UPLOAD_ANDROID_VERSION_TO_GITHUB_ARTIFACTS__STEP_MOCK,
PLATFORM_DEPLOY__ANDROID__UPLOAD_TO_BROWSER_STACK__STEP_MOCK,
PLATFORM_DEPLOY__ANDROID__WARN_DEPLOYERS__STEP_MOCK,
-] as const;
+];
// desktop
const PLATFORM_DEPLOY__DESKTOP__CHECKOUT__STEP_MOCK = createMockStep('Checkout', 'Checking out', 'DESKTOP');
@@ -105,7 +105,7 @@ const PLATFORM_DEPLOY__DESKTOP__STEP_MOCKS = [
PLATFORM_DEPLOY__DESKTOP__DECRYPT_ID__STEP_MOCK,
PLATFORM_DEPLOY__DESKTOP__BUILD_PRODUCTION__STEP_MOCK,
PLATFORM_DEPLOY__DESKTOP__BUILD_STAGING__STEP_MOCK,
-] as const;
+];
// ios
const PLATFORM_DEPLOY__IOS__CHECKOUT__STEP_MOCK = createMockStep('Checkout', 'Checking out', 'IOS');
@@ -167,7 +167,7 @@ const PLATFORM_DEPLOY__IOS__STEP_MOCKS = [
PLATFORM_DEPLOY__IOS__SET_VERSION__STEP_MOCK,
PLATFORM_DEPLOY__IOS__RELEASE_FASTLANE__STEP_MOCK,
PLATFORM_DEPLOY__IOS__WARN_FAIL__STEP_MOCK,
-] as const;
+];
// web
const PLATFORM_DEPLOY__WEB__CHECKOUT__STEP_MOCK = createMockStep('Checkout', 'Checking out', 'WEB');
@@ -199,13 +199,13 @@ const PLATFORM_DEPLOY__WEB__STEP_MOCKS = [
PLATFORM_DEPLOY__WEB__DEPLOY_STAGING_S3__STEP_MOCK,
PLATFORM_DEPLOY__WEB__PURGE_PRODUCTION_CACHE__STEP_MOCK,
PLATFORM_DEPLOY__WEB__PURGE_STAGING_CACHE__STEP_MOCK,
-] as const;
+];
// post slack message on failure
const PLATFORM_DEPLOY__POST_SLACK_FAIL__POST_SLACK__STEP_MOCK = createMockStep('Post Slack message on failure', 'Posting Slack message on platform deploy failure', 'POST_SLACK_FAIL', [
'SLACK_WEBHOOK',
]);
-const PLATFORM_DEPLOY__POST_SLACK_FAIL__STEP_MOCKS = [PLATFORM_DEPLOY__POST_SLACK_FAIL__POST_SLACK__STEP_MOCK] as const;
+const PLATFORM_DEPLOY__POST_SLACK_FAIL__STEP_MOCKS = [PLATFORM_DEPLOY__POST_SLACK_FAIL__POST_SLACK__STEP_MOCK];
// post slack message on success
const PLATFORM_DEPLOY__POST_SLACK_SUCCESS__CHECKOUT__STEP_MOCK = createMockStep('Checkout', 'Checking out', 'POST_SLACK_SUCCESS');
@@ -237,7 +237,7 @@ const PLATFORM_DEPLOY__POST_SLACK_SUCCESS__STEP_MOCKS = [
PLATFORM_DEPLOY__POST_SLACK_SUCCESS__ANNOUNCE_CHANNEL__STEP_MOCK,
PLATFORM_DEPLOY__POST_SLACK_SUCCESS__DEPLOYER_CHANNEL__STEP_MOCK,
PLATFORM_DEPLOY__POST_SLACK_SUCCESS__EXPENSIFY_CHANNEL__STEP_MOCK,
-] as const;
+];
// post github comment
const PLATFORM_DEPLOY__POST_GIHUB_COMMENT__CHECKOUT__STEP_MOCK = createMockStep('Checkout', 'Checking out', 'POST_GITHUB_COMMENT');
@@ -267,7 +267,7 @@ const PLATFORM_DEPLOY__POST_GITHUB_COMMENT__STEP_MOCKS = [
PLATFORM_DEPLOY__POST_GIHUB_COMMENT__SET_VERSION__STEP_MOCK,
PLATFORM_DEPLOY__POST_GIHUB_COMMENT__GET_PR_LIST__STEP_MOCK,
PLATFORM_DEPLOY__POST_GIHUB_COMMENT__COMMENT__STEP_MOCK,
-] as const;
+];
export {
PLATFORM_DEPLOY__VALIDATE_ACTOR__TEAM_MEMBER__STEP_MOCKS,
diff --git a/workflow_tests/mocks/preDeployMocks.ts b/workflow_tests/mocks/preDeployMocks.ts
index df6bc63e7dd9..d9da9d51bd85 100644
--- a/workflow_tests/mocks/preDeployMocks.ts
+++ b/workflow_tests/mocks/preDeployMocks.ts
@@ -3,15 +3,15 @@ import {createMockStep} from '../utils/utils';
// typecheck
const TYPECHECK_WORKFLOW_MOCK_STEP = createMockStep('Run typecheck workflow', 'Running typecheck workflow', 'TYPECHECK');
-const TYPECHECK_JOB_MOCK_STEPS = [TYPECHECK_WORKFLOW_MOCK_STEP] as const;
+const TYPECHECK_JOB_MOCK_STEPS = [TYPECHECK_WORKFLOW_MOCK_STEP];
// lint
const LINT_WORKFLOW_MOCK_STEP = createMockStep('Run lint workflow', 'Running lint workflow', 'LINT');
-const LINT_JOB_MOCK_STEPS = [LINT_WORKFLOW_MOCK_STEP] as const;
+const LINT_JOB_MOCK_STEPS = [LINT_WORKFLOW_MOCK_STEP];
// test
const TEST_WORKFLOW_MOCK_STEP = createMockStep('Run test workflow', 'Running test workflow', 'TEST');
-const TEST_JOB_MOCK_STEPS = [TEST_WORKFLOW_MOCK_STEP] as const;
+const TEST_JOB_MOCK_STEPS = [TEST_WORKFLOW_MOCK_STEP];
// confirm_passing_build
const ANNOUNCE_IN_SLACK_MOCK_STEP = createMockStep('Announce failed workflow in Slack', 'Announcing failed workflow in slack', 'CONFIRM_PASSING_BUILD', ['SLACK_WEBHOOK']);
@@ -19,7 +19,7 @@ const CONFIRM_PASSING_BUILD_JOB_MOCK_STEPS = [
ANNOUNCE_IN_SLACK_MOCK_STEP,
// 2nd step runs normally
-] as const;
+];
// choose_deploy_actions
const GET_MERGED_PULL_REQUEST_MOCK_STEP__CHOOSE_DEPLOY = createMockStep('Get merged pull request', 'Getting merged pull request', 'CHOOSE_DEPLOY_ACTIONS', ['github_token'], null, {
@@ -47,21 +47,21 @@ const CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_LOCKED = [
CHECK_IF_STAGINGDEPLOYCASH_IS_LOCKED_MOCK_STEP__LOCKED,
// step 3 runs normally
-] as const;
+];
const CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED = [
GET_MERGED_PULL_REQUEST_MOCK_STEP__CHOOSE_DEPLOY,
CHECK_IF_STAGINGDEPLOYCASH_IS_LOCKED_MOCK_STEP__UNLOCKED,
// step 3 runs normally
-] as const;
+];
// skip_deploy
const COMMENT_ON_DEFERRED_PR_MOCK_STEP = createMockStep('Comment on deferred PR', 'Skipping deploy', 'SKIP_DEPLOY', ['github_token', 'number', 'body']);
-const SKIP_DEPLOY_JOB_MOCK_STEPS = [COMMENT_ON_DEFERRED_PR_MOCK_STEP] as const;
+const SKIP_DEPLOY_JOB_MOCK_STEPS = [COMMENT_ON_DEFERRED_PR_MOCK_STEP];
// create_new_version
const CREATE_NEW_VERSION_MOCK_STEP = createMockStep('Create new version', 'Creating new version', 'CREATE_NEW_VERSION', null, null, {NEW_VERSION: '1.2.3'}, null, true, 'createNewVersion');
-const CREATE_NEW_VERSION_JOB_MOCK_STEPS = [CREATE_NEW_VERSION_MOCK_STEP] as const;
+const CREATE_NEW_VERSION_JOB_MOCK_STEPS = [CREATE_NEW_VERSION_MOCK_STEP];
// update_staging
const RUN_TURNSTYLE_MOCK_STEP = createMockStep('Run turnstyle', 'Running turnstyle', 'UPDATE_STAGING', ['poll-interval-seconds'], ['GITHUB_TOKEN']);
@@ -75,10 +75,10 @@ const UPDATE_STAGING_JOB_MOCK_STEPS = [
SETUP_GIT_FOR_OSBOTIFY_MOCK_STEP,
UPDATE_STAGING_BRANCH_FROM_MAIN_MOCK_STEP,
ANNOUNCE_FAILED_WORKFLOW_IN_SLACK_MOCK_STEP,
-] as const;
+];
const PREDEPLOY__E2EPERFORMANCETESTS__PERFORM_E2E_TESTS__MOCK_STEP = createMockStep('Perform E2E tests', 'Perform E2E tests', 'E2EPERFORMANCETESTS');
-const PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS = [PREDEPLOY__E2EPERFORMANCETESTS__PERFORM_E2E_TESTS__MOCK_STEP] as const;
+const PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS = [PREDEPLOY__E2EPERFORMANCETESTS__PERFORM_E2E_TESTS__MOCK_STEP];
export {
TYPECHECK_JOB_MOCK_STEPS,
diff --git a/workflow_tests/mocks/reviewerChecklistMocks.ts b/workflow_tests/mocks/reviewerChecklistMocks.ts
index 20c87994f957..8a70c0c71597 100644
--- a/workflow_tests/mocks/reviewerChecklistMocks.ts
+++ b/workflow_tests/mocks/reviewerChecklistMocks.ts
@@ -3,9 +3,8 @@ import {createMockStep} from '../utils/utils';
// checklist
const REVIEWERCHECKLIST__CHECKLIST__REVIEWERCHECKLIST_JS__STEP_MOCK = createMockStep('reviewerChecklist.js', 'reviewerChecklist.js', 'CHECKLIST', ['GITHUB_TOKEN'], []);
-const REVIEWERCHECKLIST__CHECKLIST__STEP_MOCKS = [REVIEWERCHECKLIST__CHECKLIST__REVIEWERCHECKLIST_JS__STEP_MOCK] as const;
+const REVIEWERCHECKLIST__CHECKLIST__STEP_MOCKS = [REVIEWERCHECKLIST__CHECKLIST__REVIEWERCHECKLIST_JS__STEP_MOCK];
-export {
- // eslint-disable-next-line import/prefer-default-export
+export default {
REVIEWERCHECKLIST__CHECKLIST__STEP_MOCKS,
};
diff --git a/workflow_tests/mocks/testBuildMocks.ts b/workflow_tests/mocks/testBuildMocks.ts
index 12109d0a875b..f502bfb248ba 100644
--- a/workflow_tests/mocks/testBuildMocks.ts
+++ b/workflow_tests/mocks/testBuildMocks.ts
@@ -27,19 +27,19 @@ const TESTBUILD__VALIDATEACTOR__SET_HAS_READY_TO_BUILD_LABEL_FLAG__FALSE__STEP_M
const TESTBUILD__VALIDATEACTOR__TEAM_MEMBER_HAS_FLAG__STEP_MOCKS = [
TESTBUILD__VALIDATEACTOR__IS_TEAM_MEMBER__TRUE__STEP_MOCK,
TESTBUILD__VALIDATEACTOR__SET_HAS_READY_TO_BUILD_LABEL_FLAG__TRUE__STEP_MOCK,
-] as const;
+];
const TESTBUILD__VALIDATEACTOR__TEAM_MEMBER_NO_FLAG__STEP_MOCKS = [
TESTBUILD__VALIDATEACTOR__IS_TEAM_MEMBER__TRUE__STEP_MOCK,
TESTBUILD__VALIDATEACTOR__SET_HAS_READY_TO_BUILD_LABEL_FLAG__FALSE__STEP_MOCK,
-] as const;
+];
const TESTBUILD__VALIDATEACTOR__NO_TEAM_MEMBER_HAS_FLAG__STEP_MOCKS = [
TESTBUILD__VALIDATEACTOR__IS_TEAM_MEMBER__FALSE__STEP_MOCK,
TESTBUILD__VALIDATEACTOR__SET_HAS_READY_TO_BUILD_LABEL_FLAG__TRUE__STEP_MOCK,
-] as const;
+];
const TESTBUILD__VALIDATEACTOR__NO_TEAM_MEMBER_NO_FLAG__STEP_MOCKS = [
TESTBUILD__VALIDATEACTOR__IS_TEAM_MEMBER__FALSE__STEP_MOCK,
TESTBUILD__VALIDATEACTOR__SET_HAS_READY_TO_BUILD_LABEL_FLAG__FALSE__STEP_MOCK,
-] as const;
+];
// getbranchref
const TESTBUILD__GETBRANCHREF__CHECKOUT__STEP_MOCK = createMockStep('Checkout', 'Checkout', 'GETBRANCHREF', [], []);
@@ -51,7 +51,7 @@ const TESTBUILD__GETBRANCHREF__CHECK_IF_PULL_REQUEST_NUMBER_IS_CORRECT__STEP_MOC
['GITHUB_TOKEN'],
{REF: 'test-ref'},
);
-const TESTBUILD__GETBRANCHREF__STEP_MOCKS = [TESTBUILD__GETBRANCHREF__CHECKOUT__STEP_MOCK, TESTBUILD__GETBRANCHREF__CHECK_IF_PULL_REQUEST_NUMBER_IS_CORRECT__STEP_MOCK] as const;
+const TESTBUILD__GETBRANCHREF__STEP_MOCKS = [TESTBUILD__GETBRANCHREF__CHECKOUT__STEP_MOCK, TESTBUILD__GETBRANCHREF__CHECK_IF_PULL_REQUEST_NUMBER_IS_CORRECT__STEP_MOCK];
// android
const TESTBUILD__ANDROID__CHECKOUT__STEP_MOCK = createMockStep('Checkout', 'Checkout', 'ANDROID', ['ref'], []);
@@ -95,7 +95,7 @@ const TESTBUILD__ANDROID__STEP_MOCKS = [
TESTBUILD__ANDROID__CONFIGURE_MAPBOX_SDK__STEP_MOCK,
TESTBUILD__ANDROID__RUN_FASTLANE_BETA_TEST__STEP_MOCK,
TESTBUILD__ANDROID__UPLOAD_ARTIFACT__STEP_MOCK,
-] as const;
+];
// ios
const TESTBUILD__IOS__CHECKOUT__STEP_MOCK = createMockStep('Checkout', 'Checkout', 'IOS', ['ref'], []);
@@ -151,7 +151,7 @@ const TESTBUILD__IOS__STEP_MOCKS = [
TESTBUILD__IOS__CONFIGURE_AWS_CREDENTIALS__STEP_MOCK,
TESTBUILD__IOS__RUN_FASTLANE__STEP_MOCK,
TESTBUILD__IOS__UPLOAD_ARTIFACT__STEP_MOCK,
-] as const;
+];
// desktop
const TESTBUILD__DESKTOP__CHECKOUT__STEP_MOCK = createMockStep('Checkout', 'Checkout', 'DESKTOP', ['ref'], []);
@@ -191,7 +191,7 @@ const TESTBUILD__DESKTOP__STEP_MOCKS = [
TESTBUILD__DESKTOP__DECRYPT_DEVELOPER_ID_CERTIFICATE__STEP_MOCK,
TESTBUILD__DESKTOP__CONFIGURE_AWS_CREDENTIALS__STEP_MOCK,
TESTBUILD__DESKTOP__BUILD_DESKTOP_APP_FOR_TESTING__STEP_MOCK,
-] as const;
+];
// web
const TESTBUILD__WEB__CHECKOUT__STEP_MOCK = createMockStep('Checkout', 'Checkout', 'WEB', ['ref'], []);
@@ -221,7 +221,7 @@ const TESTBUILD__WEB__STEP_MOCKS = [
TESTBUILD__WEB__BUILD_WEB_FOR_TESTING__STEP_MOCK,
TESTBUILD__WEB__BUILD_DOCS__STEP_MOCK,
TESTBUILD__WEB__DEPLOY_TO_S3_FOR_INTERNAL_TESTING__STEP_MOCK,
-] as const;
+];
// postgithubcomment
const TESTBUILD__POSTGITHUBCOMMENT__CHECKOUT__STEP_MOCK = createMockStep('Checkout', 'Checkout', 'POSTGITHUBCOMMENT', ['ref'], []);
@@ -245,7 +245,7 @@ const TESTBUILD__POSTGITHUBCOMMENT__STEP_MOCKS = [
TESTBUILD__POSTGITHUBCOMMENT__READ_JSONS_WITH_ANDROID_PATHS__STEP_MOCK,
TESTBUILD__POSTGITHUBCOMMENT__READ_JSONS_WITH_IOS_PATHS__STEP_MOCK,
TESTBUILD__POSTGITHUBCOMMENT__PUBLISH_LINKS_TO_APPS_FOR_DOWNLOAD__STEP_MOCK,
-] as const;
+];
export {
TESTBUILD__VALIDATEACTOR__TEAM_MEMBER_HAS_FLAG__STEP_MOCKS,
diff --git a/workflow_tests/mocks/testMocks.ts b/workflow_tests/mocks/testMocks.ts
index 96f47d4204fb..cb8240169a8e 100644
--- a/workflow_tests/mocks/testMocks.ts
+++ b/workflow_tests/mocks/testMocks.ts
@@ -13,12 +13,12 @@ const TEST__JEST__STEP_MOCKS = [
TEST__JEST__GET_NUMBER_OF_CPU_CORES__STEP_MOCK,
TEST__JEST__CACHE_JEST_CACHE__STEP_MOCK,
TEST__JEST__JEST_TESTS__STEP_MOCK,
-] as const;
+];
// shelltests
const TEST__SHELLTESTS__CHECKOUT__STEP_MOCK = createMockStep('Checkout', 'Checkout', 'SHELLTESTS', [], []);
const TEST__SHELLTESTS__SETUP_NODE__STEP_MOCK = createMockStep('Setup Node', 'Setup Node', 'SHELLTESTS', [], []);
const TEST__SHELLTESTS__TEST_CI_GIT_LOGIC__STEP_MOCK = createMockStep('Test CI git logic', 'Test CI git logic', 'SHELLTESTS', [], []);
-const TEST__SHELLTESTS__STEP_MOCKS = [TEST__SHELLTESTS__CHECKOUT__STEP_MOCK, TEST__SHELLTESTS__SETUP_NODE__STEP_MOCK, TEST__SHELLTESTS__TEST_CI_GIT_LOGIC__STEP_MOCK] as const;
+const TEST__SHELLTESTS__STEP_MOCKS = [TEST__SHELLTESTS__CHECKOUT__STEP_MOCK, TEST__SHELLTESTS__SETUP_NODE__STEP_MOCK, TEST__SHELLTESTS__TEST_CI_GIT_LOGIC__STEP_MOCK];
export {TEST__JEST__STEP_MOCKS, TEST__SHELLTESTS__STEP_MOCKS};
diff --git a/workflow_tests/mocks/validateGithubActionsMocks.ts b/workflow_tests/mocks/validateGithubActionsMocks.ts
index 8249945dbfc7..2c1afba74387 100644
--- a/workflow_tests/mocks/validateGithubActionsMocks.ts
+++ b/workflow_tests/mocks/validateGithubActionsMocks.ts
@@ -11,9 +11,8 @@ const VALIDATEGITHUBACTIONS__VERIFY__STEP_MOCKS = [
VALIDATEGITHUBACTIONS__VERIFY__SETUP_NODE__STEP_MOCK,
VALIDATEGITHUBACTIONS__VERIFY__VERIFY_JAVASCRIPT_ACTION_BUILDS__STEP_MOCK,
VALIDATEGITHUBACTIONS__VERIFY__VALIDATE_ACTIONS_AND_WORKFLOWS__STEP_MOCK,
-] as const;
+];
-export {
- // eslint-disable-next-line import/prefer-default-export
+export default {
VALIDATEGITHUBACTIONS__VERIFY__STEP_MOCKS,
};
diff --git a/workflow_tests/mocks/verifyPodfileMocks.ts b/workflow_tests/mocks/verifyPodfileMocks.ts
index b1f0669b8b00..0cd7ea396dad 100644
--- a/workflow_tests/mocks/verifyPodfileMocks.ts
+++ b/workflow_tests/mocks/verifyPodfileMocks.ts
@@ -5,13 +5,8 @@ import {createMockStep} from '../utils/utils';
const VERIFYPODFILE__VERIFY__CHECKOUT__STEP_MOCK = createMockStep('Checkout', 'Checkout', 'VERIFY');
const VERIFYPODFILE__VERIFY__SETUP_NODE__STEP_MOCK = createMockStep('Setup Node', 'Setup Node', 'VERIFY', [], []);
const VERIFYPODFILE__VERIFY__VERIFY_PODFILE__STEP_MOCK = createMockStep('Verify podfile', 'Verify podfile', 'VERIFY', [], []);
-const VERIFYPODFILE__VERIFY__STEP_MOCKS = [
- VERIFYPODFILE__VERIFY__CHECKOUT__STEP_MOCK,
- VERIFYPODFILE__VERIFY__SETUP_NODE__STEP_MOCK,
- VERIFYPODFILE__VERIFY__VERIFY_PODFILE__STEP_MOCK,
-] as const;
+const VERIFYPODFILE__VERIFY__STEP_MOCKS = [VERIFYPODFILE__VERIFY__CHECKOUT__STEP_MOCK, VERIFYPODFILE__VERIFY__SETUP_NODE__STEP_MOCK, VERIFYPODFILE__VERIFY__VERIFY_PODFILE__STEP_MOCK];
-export {
- // eslint-disable-next-line import/prefer-default-export
+export default {
VERIFYPODFILE__VERIFY__STEP_MOCKS,
};
diff --git a/workflow_tests/mocks/verifySignedCommitsMocks.ts b/workflow_tests/mocks/verifySignedCommitsMocks.ts
index 953d09d1255f..6109af183fd7 100644
--- a/workflow_tests/mocks/verifySignedCommitsMocks.ts
+++ b/workflow_tests/mocks/verifySignedCommitsMocks.ts
@@ -9,9 +9,8 @@ const VERIFYSIGNEDCOMMITS__VERIFYSIGNEDCOMMITS__VERIFY_SIGNED_COMMITS__STEP_MOCK
['GITHUB_TOKEN'],
[],
);
-const VERIFYSIGNEDCOMMITS__VERIFYSIGNEDCOMMITS__STEP_MOCKS = [VERIFYSIGNEDCOMMITS__VERIFYSIGNEDCOMMITS__VERIFY_SIGNED_COMMITS__STEP_MOCK] as const;
+const VERIFYSIGNEDCOMMITS__VERIFYSIGNEDCOMMITS__STEP_MOCKS = [VERIFYSIGNEDCOMMITS__VERIFYSIGNEDCOMMITS__VERIFY_SIGNED_COMMITS__STEP_MOCK];
-export {
- // eslint-disable-next-line import/prefer-default-export
+export default {
VERIFYSIGNEDCOMMITS__VERIFYSIGNEDCOMMITS__STEP_MOCKS,
};