diff --git a/android/app/build.gradle b/android/app/build.gradle
index c53ad2cd3cc7..d4cc7471a72b 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -107,8 +107,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1009000512
- versionName "9.0.5-12"
+ versionCode 1009000600
+ versionName "9.0.6-0"
// Supported language variants must be declared here to avoid from being removed during the compilation.
// This also helps us to not include unnecessary language variants in the APK.
resConfigs "en", "es"
diff --git a/assets/images/simple-illustrations/simple-illustration__empty-state.svg b/assets/images/simple-illustrations/simple-illustration__empty-state.svg
new file mode 100644
index 000000000000..154b2269c285
--- /dev/null
+++ b/assets/images/simple-illustrations/simple-illustration__empty-state.svg
@@ -0,0 +1,102 @@
+
+
+
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 2ccce98e6a21..2826f7e51db9 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 9.0.5
+ 9.0.6
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 9.0.5.12
+ 9.0.6.0
FullStory
OrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 8248e7db0454..cf3e420de4e7 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 9.0.5
+ 9.0.6
CFBundleSignature
????
CFBundleVersion
- 9.0.5.12
+ 9.0.6.0
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 87cdb420af38..58cdb65c40e9 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName
$(PRODUCT_NAME)
CFBundleShortVersionString
- 9.0.5
+ 9.0.6
CFBundleVersion
- 9.0.5.12
+ 9.0.6.0
NSExtension
NSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index c401dfe77198..64f5f7a6a94d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.0.5-12",
+ "version": "9.0.6-0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.5-12",
+ "version": "9.0.6-0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 5420a3e886ef..9f1c3ffca7a7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.5-12",
+ "version": "9.0.6-0",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
diff --git a/scripts/applyPatches.sh b/scripts/applyPatches.sh
index a4be88984561..9145629015ee 100755
--- a/scripts/applyPatches.sh
+++ b/scripts/applyPatches.sh
@@ -8,24 +8,17 @@ SCRIPTS_DIR=$(dirname "${BASH_SOURCE[0]}")
source "$SCRIPTS_DIR/shellUtils.sh"
# Wrapper to run patch-package.
-# We use `script` to preserve colorization when the output of patch-package is piped to tee
-# and we provide /dev/null to discard the output rather than sending it to a file
-# `script` has different syntax on macOS vs linux, so that's why we need a wrapper function
function patchPackage {
OS="$(uname)"
- if [[ "$OS" == "Darwin" ]]; then
- # macOS
- script -q /dev/null npx patch-package --error-on-fail
- elif [[ "$OS" == "Linux" ]]; then
- # Ubuntu/Linux
- script -q -c "npx patch-package --error-on-fail" /dev/null
+ if [[ "$OS" == "Darwin" || "$OS" == "Linux" ]]; then
+ npx patch-package --error-on-fail
else
error "Unsupported OS: $OS"
+ exit 1
fi
}
# Run patch-package and capture its output and exit code, while still displaying the original output to the terminal
-# (we use `script -q /dev/null` to preserve colorization in the output)
TEMP_OUTPUT="$(mktemp)"
patchPackage 2>&1 | tee "$TEMP_OUTPUT"
EXIT_CODE=${PIPESTATUS[0]}
@@ -36,7 +29,7 @@ rm -f "$TEMP_OUTPUT"
echo "$OUTPUT" | grep -q "Warning:"
WARNING_FOUND=$?
-printf "\n";
+printf "\n"
# Determine the final exit code
if [ "$EXIT_CODE" -eq 0 ]; then
diff --git a/src/CONST.ts b/src/CONST.ts
index b809bdaacaf6..50df9118a74e 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -840,6 +840,8 @@ const CONST = {
IOU: 'iou',
TASK: 'task',
INVOICE: 'invoice',
+ PAYCHECK: 'paycheck',
+ BILL: 'bill',
},
CHAT_TYPE: chatTypes,
WORKSPACE_CHAT_ROOMS: {
@@ -1123,8 +1125,6 @@ const CONST = {
// around each header.
EMOJI_NUM_PER_ROW: 8,
- EMOJI_FREQUENT_ROW_COUNT: 3,
-
EMOJI_DEFAULT_SKIN_TONE: -1,
// Amount of emojis to render ahead at the end of the update cycle
@@ -5250,6 +5250,13 @@ const CONST = {
},
EXCLUDE_FROM_LAST_VISITED_PATH: [SCREENS.NOT_FOUND, SCREENS.SAML_SIGN_IN, SCREENS.VALIDATE_LOGIN] as string[],
+
+ EMPTY_STATE_MEDIA: {
+ ANIMATION: 'animation',
+ ILLUSTRATION: 'illustration',
+ VIDEO: 'video',
+ },
+
UPGRADE_FEATURE_INTRO_MAPPING: [
{
id: 'reportFields',
@@ -5260,6 +5267,7 @@ const CONST = {
icon: 'Pencil',
},
],
+
REPORT_FIELD_TYPES: {
TEXT: 'text',
DATE: 'date',
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index bd4b294a6d68..8abb7738289c 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -445,6 +445,9 @@ const ONYXKEYS = {
* So for example: card_12345_Expensify Card
*/
WORKSPACE_CARDS_LIST: 'card_',
+
+ /** The bank account that Expensify Card payments will be reconciled against */
+ SHARED_NVP_EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION: 'sharedNVP_expensifyCard_continuousReconciliationConnection_',
},
/** List of Form ids */
@@ -535,8 +538,8 @@ const ONYXKEYS = {
REPORT_VIRTUAL_CARD_FRAUD_DRAFT: 'reportVirtualCardFraudFormDraft',
GET_PHYSICAL_CARD_FORM: 'getPhysicalCardForm',
GET_PHYSICAL_CARD_FORM_DRAFT: 'getPhysicalCardFormDraft',
- REPORT_FIELD_EDIT_FORM: 'reportFieldEditForm',
- REPORT_FIELD_EDIT_FORM_DRAFT: 'reportFieldEditFormDraft',
+ REPORT_FIELDS_EDIT_FORM: 'reportFieldsEditForm',
+ REPORT_FIELDS_EDIT_FORM_DRAFT: 'reportFieldsEditFormDraft',
REIMBURSEMENT_ACCOUNT_FORM: 'reimbursementAccount',
REIMBURSEMENT_ACCOUNT_FORM_DRAFT: 'reimbursementAccountDraft',
PERSONAL_BANK_ACCOUNT_FORM: 'personalBankAccount',
@@ -622,7 +625,7 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD]: FormTypes.ReportVirtualCardFraudForm;
[ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM]: FormTypes.ReportPhysicalCardForm;
[ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: FormTypes.GetPhysicalCardForm;
- [ONYXKEYS.FORMS.REPORT_FIELD_EDIT_FORM]: FormTypes.ReportFieldEditForm;
+ [ONYXKEYS.FORMS.REPORT_FIELDS_EDIT_FORM]: FormTypes.ReportFieldsEditForm;
[ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM]: FormTypes.ReimbursementAccountForm;
[ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM]: FormTypes.PersonalBankAccountForm;
[ONYXKEYS.FORMS.WORKSPACE_DESCRIPTION_FORM]: FormTypes.WorkspaceDescriptionForm;
@@ -692,6 +695,7 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END]: OnyxTypes.BillingGraceEndPeriod;
[ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_EXPENSIFY_CARD_SETTINGS]: OnyxTypes.ExpensifyCardSettings;
[ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST]: OnyxTypes.WorkspaceCardsList;
+ [ONYXKEYS.COLLECTION.SHARED_NVP_EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION]: OnyxTypes.BankAccount;
};
type OnyxValuesMapping = {
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index c47083db48eb..fbebd459fff0 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -676,6 +676,10 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/accounting/quickbooks-online/invoice-account-selector',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/invoice-account-selector` as const,
},
+ WORKSPACE_ACCOUNTING_RECONCILIATION_ACCOUNT_SETTINGS: {
+ route: 'settings/workspaces/:policyID/accounting/:connection/card-reconciliation',
+ getRoute: (policyID: string, connection: ValueOf) => `settings/workspaces/${policyID}/accounting/${connection}/card-reconciliation` as const,
+ },
WORKSPACE_CATEGORIES: {
route: 'settings/workspaces/:policyID/categories',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories` as const,
@@ -797,30 +801,30 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/reportFields/new',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/reportFields/new` as const,
},
- WORKSPACE_REPORT_FIELD_SETTINGS: {
- route: 'settings/workspaces/:policyID/reportField/:reportFieldID/edit',
- getRoute: (policyID: string, reportFieldID: string) => `settings/workspaces/${policyID}/reportField/${encodeURIComponent(reportFieldID)}/edit` as const,
+ WORKSPACE_REPORT_FIELDS_SETTINGS: {
+ route: 'settings/workspaces/:policyID/reportFields/:reportFieldID/edit',
+ getRoute: (policyID: string, reportFieldID: string) => `settings/workspaces/${policyID}/reportFields/${encodeURIComponent(reportFieldID)}/edit` as const,
},
- WORKSPACE_REPORT_FIELD_LIST_VALUES: {
- route: 'settings/workspaces/:policyID/reportField/listValues/:reportFieldID?',
- getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportField/listValues/${encodeURIComponent(reportFieldID ?? '')}` as const,
+ WORKSPACE_REPORT_FIELDS_LIST_VALUES: {
+ route: 'settings/workspaces/:policyID/reportFields/listValues/:reportFieldID?',
+ getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportFields/listValues/${encodeURIComponent(reportFieldID ?? '')}` as const,
},
- WORKSPACE_REPORT_FIELD_ADD_VALUE: {
- route: 'settings/workspaces/:policyID/reportField/addValue/:reportFieldID?',
- getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportField/addValue/${encodeURIComponent(reportFieldID ?? '')}` as const,
+ WORKSPACE_REPORT_FIELDS_ADD_VALUE: {
+ route: 'settings/workspaces/:policyID/reportFields/addValue/:reportFieldID?',
+ getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportFields/addValue/${encodeURIComponent(reportFieldID ?? '')}` as const,
},
- WORKSPACE_REPORT_FIELD_VALUE_SETTINGS: {
- route: 'settings/workspaces/:policyID/reportField/:valueIndex/:reportFieldID?',
+ WORKSPACE_REPORT_FIELDS_VALUE_SETTINGS: {
+ route: 'settings/workspaces/:policyID/reportFields/:valueIndex/:reportFieldID?',
getRoute: (policyID: string, valueIndex: number, reportFieldID?: string) =>
- `settings/workspaces/${policyID}/reportField/${valueIndex}/${encodeURIComponent(reportFieldID ?? '')}` as const,
+ `settings/workspaces/${policyID}/reportFields/${valueIndex}/${encodeURIComponent(reportFieldID ?? '')}` as const,
},
- WORKSPACE_REPORT_FIELD_EDIT_VALUE: {
- route: 'settings/workspaces/:policyID/reportField/new/:valueIndex/edit',
- getRoute: (policyID: string, valueIndex: number) => `settings/workspaces/${policyID}/reportField/new/${valueIndex}/edit` as const,
+ WORKSPACE_REPORT_FIELDS_EDIT_VALUE: {
+ route: 'settings/workspaces/:policyID/reportFields/new/:valueIndex/edit',
+ getRoute: (policyID: string, valueIndex: number) => `settings/workspaces/${policyID}/reportFields/new/${valueIndex}/edit` as const,
},
- WORKSPACE_EDIT_REPORT_FIELD_INITIAL_VALUE: {
- route: 'settings/workspaces/:policyID/reportField/:reportFieldID/edit/initialValue',
- getRoute: (policyID: string, reportFieldID: string) => `settings/workspaces/${policyID}/reportField/${encodeURIComponent(reportFieldID)}/edit/initialValue` as const,
+ WORKSPACE_EDIT_REPORT_FIELDS_INITIAL_VALUE: {
+ route: 'settings/workspaces/:policyID/reportFields/:reportFieldID/edit/initialValue',
+ getRoute: (policyID: string, reportFieldID: string) => `settings/workspaces/${policyID}/reportFields/${encodeURIComponent(reportFieldID)}/edit/initialValue` as const,
},
WORKSPACE_EXPENSIFY_CARD: {
route: 'settings/workspaces/:policyID/expensify-card',
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index d2a6b7c19ddd..0768ca8bb291 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -330,6 +330,7 @@ const SCREENS = {
SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT: 'Policy_Accounting_Sage_Intacct_Non_Reimbursable_Credit_Card_Account',
SAGE_INTACCT_ADVANCED: 'Policy_Accounting_Sage_Intacct_Advanced',
SAGE_INTACCT_PAYMENT_ACCOUNT: 'Policy_Accounting_Sage_Intacct_Payment_Account',
+ RECONCILIATION_ACCOUNT_SETTINGS: 'Policy_Accounting_Reconciliation_Account_Settings',
},
INITIAL: 'Workspace_Initial',
PROFILE: 'Workspace_Profile',
@@ -353,7 +354,7 @@ const SCREENS = {
TAG_EDIT: 'Tag_Edit',
TAXES: 'Workspace_Taxes',
REPORT_FIELDS: 'Workspace_ReportFields',
- REPORT_FIELD_SETTINGS: 'Workspace_ReportField_Settings',
+ REPORT_FIELDS_SETTINGS: 'Workspace_ReportFields_Settings',
REPORT_FIELDS_CREATE: 'Workspace_ReportFields_Create',
REPORT_FIELDS_LIST_VALUES: 'Workspace_ReportFields_ListValues',
REPORT_FIELDS_ADD_VALUE: 'Workspace_ReportFields_AddValue',
diff --git a/src/components/AccountingListSkeletonView.tsx b/src/components/AccountingListSkeletonView.tsx
index b977903d3adc..dbe8ada6c4b7 100644
--- a/src/components/AccountingListSkeletonView.tsx
+++ b/src/components/AccountingListSkeletonView.tsx
@@ -4,12 +4,14 @@ import ItemListSkeletonView from './Skeletons/ItemListSkeletonView';
type AccountingListSkeletonViewProps = {
shouldAnimate?: boolean;
+ gradientOpacityEnabled?: boolean;
};
-function AccountingListSkeletonView({shouldAnimate = true}: AccountingListSkeletonViewProps) {
+function AccountingListSkeletonView({shouldAnimate = true, gradientOpacityEnabled = false}: AccountingListSkeletonViewProps) {
return (
(
<>
{
+ if (!event) {
+ return;
+ }
+
+ if ('naturalSize' in event) {
+ setVideoAspectRatio(event.naturalSize.width / event.naturalSize.height);
+ } else {
+ setVideoAspectRatio(event.srcElement.videoWidth / event.srcElement.videoHeight);
+ }
+ };
+
+ const HeaderComponent = useMemo(() => {
+ switch (headerMediaType) {
+ case CONST.EMPTY_STATE_MEDIA.VIDEO:
+ return (
+
+ );
+ case CONST.EMPTY_STATE_MEDIA.ANIMATION:
+ return (
+
+ );
+ case CONST.EMPTY_STATE_MEDIA.ILLUSTRATION:
+ return (
+
+ );
+ default:
+ return null;
+ }
+ }, [headerMedia, headerMediaType, headerContentStyles, videoAspectRatio, styles.emptyStateVideo]);
+
+ return (
+
+
+
+
+
+
+ {HeaderComponent}
+
+ {title}
+ {subtitle}
+ {!!buttonText && !!buttonAction && (
+
+ )}
+
+
+
+
+ );
+}
+
+EmptyStateComponent.displayName = 'EmptyStateComponent';
+export default EmptyStateComponent;
diff --git a/src/components/EmptyStateComponent/types.ts b/src/components/EmptyStateComponent/types.ts
new file mode 100644
index 000000000000..326b25542f42
--- /dev/null
+++ b/src/components/EmptyStateComponent/types.ts
@@ -0,0 +1,41 @@
+import type {ImageStyle} from 'expo-image';
+import type {StyleProp, ViewStyle} from 'react-native';
+import type {ValueOf} from 'type-fest';
+import type DotLottieAnimation from '@components/LottieAnimations/types';
+import type SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton';
+import type TableRowSkeleton from '@components/Skeletons/TableRowSkeleton';
+import type CONST from '@src/CONST';
+import type IconAsset from '@src/types/utils/IconAsset';
+
+type ValidSkeletons = typeof SearchRowSkeleton | typeof TableRowSkeleton;
+type MediaTypes = ValueOf;
+
+type SharedProps = {
+ SkeletonComponent: ValidSkeletons;
+ title: string;
+ subtitle: string;
+ buttonText?: string;
+ buttonAction?: () => void;
+ headerStyles?: StyleProp;
+ headerMediaType: T;
+ headerContentStyles?: StyleProp;
+};
+
+type MediaType = SharedProps & {
+ headerMedia: HeaderMedia;
+};
+
+type VideoProps = MediaType;
+type IllustrationProps = MediaType;
+type AnimationProps = MediaType;
+
+type EmptyStateComponentProps = VideoProps | IllustrationProps | AnimationProps;
+
+type VideoLoadedEventType = {
+ srcElement: {
+ videoWidth: number;
+ videoHeight: number;
+ };
+};
+
+export type {EmptyStateComponentProps, VideoLoadedEventType};
diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts
index 7a8186d2f38e..da72b3025340 100644
--- a/src/components/Icon/Illustrations.ts
+++ b/src/components/Icon/Illustrations.ts
@@ -53,6 +53,7 @@ import ConciergeNew from '@assets/images/simple-illustrations/simple-illustratio
import CreditCardsNew from '@assets/images/simple-illustrations/simple-illustration__credit-cards.svg';
import CreditCardEyes from '@assets/images/simple-illustrations/simple-illustration__creditcardeyes.svg';
import EmailAddress from '@assets/images/simple-illustrations/simple-illustration__email-address.svg';
+import EmptyState from '@assets/images/simple-illustrations/simple-illustration__empty-state.svg';
import FolderOpen from '@assets/images/simple-illustrations/simple-illustration__folder-open.svg';
import Gears from '@assets/images/simple-illustrations/simple-illustration__gears.svg';
import HandCard from '@assets/images/simple-illustrations/simple-illustration__handcard.svg';
@@ -198,6 +199,7 @@ export {
CheckmarkCircle,
CreditCardEyes,
LockClosedOrange,
+ EmptyState,
FolderWithPapers,
VirtualCard,
};
diff --git a/src/components/OptionsListSkeletonView.tsx b/src/components/OptionsListSkeletonView.tsx
index a11077f95bb5..6dede512f405 100644
--- a/src/components/OptionsListSkeletonView.tsx
+++ b/src/components/OptionsListSkeletonView.tsx
@@ -18,16 +18,18 @@ function getLinedWidth(index: number): string {
type OptionsListSkeletonViewProps = {
shouldAnimate?: boolean;
+ gradientOpacityEnabled?: boolean;
shouldStyleAsTable?: boolean;
};
-function OptionsListSkeletonView({shouldAnimate = true, shouldStyleAsTable = false}: OptionsListSkeletonViewProps) {
+function OptionsListSkeletonView({shouldAnimate = true, shouldStyleAsTable = false, gradientOpacityEnabled = false}: OptionsListSkeletonViewProps) {
const styles = useThemeStyles();
return (
{
const lineWidth = getLinedWidth(itemIndex);
diff --git a/src/components/PopoverWithoutOverlay/index.tsx b/src/components/PopoverWithoutOverlay/index.tsx
index bcead42a64f2..7d58ad6d22be 100644
--- a/src/components/PopoverWithoutOverlay/index.tsx
+++ b/src/components/PopoverWithoutOverlay/index.tsx
@@ -124,6 +124,7 @@ function PopoverWithoutOverlay(
ref={viewRef(withoutOverlayRef)}
// Prevent the parent element to capture a click. This is useful when the modal component is put inside a pressable.
onClick={(e) => e.stopPropagation()}
+ dataSet={{dragArea: false}}
>
{
+ // If we're already navigating to these task editing pages, early return not to mark as completed, otherwise we would have not found page.
+ if (TaskUtils.isActiveTaskEditRoute(report.reportID)) {
+ return;
+ }
if (isCompleted) {
Task.reopenTask(report);
} else {
diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx
index fc5c23d5c9ec..7d54d65b310e 100644
--- a/src/components/Search/index.tsx
+++ b/src/components/Search/index.tsx
@@ -1,11 +1,12 @@
import {useNavigation} from '@react-navigation/native';
import type {StackNavigationProp} from '@react-navigation/stack';
+import lodashMemoize from 'lodash/memoize';
import React, {useCallback, useEffect, useRef} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import SearchTableHeader from '@components/SelectionList/SearchTableHeader';
import type {ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
-import TableListItemSkeleton from '@components/Skeletons/TableListItemSkeleton';
+import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
@@ -50,6 +51,9 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) {
const lastSearchResultsRef = useRef>();
const {setCurrentSearchHash} = useSearchContext();
+ const hash = SearchUtils.getQueryHash(query, policyIDs, sortBy, sortOrder);
+ const [currentSearchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`);
+
const getItemHeight = useCallback(
(item: TransactionListItemType | ReportListItemType) => {
if (SearchUtils.isTransactionListItemType(item)) {
@@ -70,8 +74,15 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) {
[isLargeScreenWidth],
);
- const hash = SearchUtils.getQueryHash(query, policyIDs, sortBy, sortOrder);
- const [currentSearchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`);
+ const getItemHeightMemoized = lodashMemoize(
+ (item: TransactionListItemType | ReportListItemType) => getItemHeight(item),
+ (item) => {
+ // List items are displayed differently on "L"arge and "N"arrow screens so the height will differ
+ // in addition the same items might be displayed as part of different Search screens ("Expenses", "All", "Finished")
+ const screenSizeHash = isLargeScreenWidth ? 'L' : 'N';
+ return `${hash}-${item.keyForList}-${screenSizeHash}`;
+ },
+ );
// save last non-empty search results to avoid ugly flash of loading screen when hash changes and onyx returns empty data
if (currentSearchResults?.data && currentSearchResults !== lastSearchResultsRef.current) {
@@ -101,7 +112,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) {
query={query}
hash={hash}
/>
-
+
>
);
}
@@ -197,7 +208,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) {
updateCellsBatchingPeriod={200}
ListItem={ListItem}
onSelectRow={openReport}
- getItemHeight={getItemHeight}
+ getItemHeight={getItemHeightMemoized}
shouldDebounceRowSelect
shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()}
listHeaderWrapperStyle={[styles.ph8, styles.pv3, styles.pb5]}
@@ -207,7 +218,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) {
onEndReached={fetchMoreResults}
listFooterContent={
isLoadingMoreItems ? (
-
diff --git a/src/components/Skeletons/ItemListSkeletonView.tsx b/src/components/Skeletons/ItemListSkeletonView.tsx
index 1ee2da8a8019..046cdfffbee5 100644
--- a/src/components/Skeletons/ItemListSkeletonView.tsx
+++ b/src/components/Skeletons/ItemListSkeletonView.tsx
@@ -1,6 +1,6 @@
-import React, {useMemo, useState} from 'react';
-import {View} from 'react-native';
-import type {StyleProp, ViewStyle} from 'react-native';
+import React, {useCallback, useMemo, useState} from 'react';
+import type {LayoutChangeEvent, StyleProp, ViewStyle} from 'react-native';
+import {StyleSheet, View} from 'react-native';
import SkeletonViewContentLoader from '@components/SkeletonViewContentLoader';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -10,22 +10,62 @@ type ListItemSkeletonProps = {
shouldAnimate?: boolean;
renderSkeletonItem: (args: {itemIndex: number}) => React.ReactNode;
fixedNumItems?: number;
+ gradientOpacityEnabled?: boolean;
itemViewStyle?: StyleProp;
itemViewHeight?: number;
};
-function ItemListSkeletonView({shouldAnimate = true, renderSkeletonItem, fixedNumItems, itemViewStyle = {}, itemViewHeight = CONST.LHN_SKELETON_VIEW_ITEM_HEIGHT}: ListItemSkeletonProps) {
+const getVerticalMargin = (style: StyleProp): number => {
+ if (!style) {
+ return 0;
+ }
+
+ const flattenStyle = StyleSheet.flatten(style);
+ const marginVertical = Number(flattenStyle?.marginVertical ?? 0);
+ const marginTop = Number(flattenStyle?.marginTop ?? 0);
+ const marginBottom = Number(flattenStyle?.marginBottom ?? 0);
+
+ return marginVertical + marginTop + marginBottom;
+};
+
+function ItemListSkeletonView({
+ shouldAnimate = true,
+ renderSkeletonItem,
+ fixedNumItems,
+ gradientOpacityEnabled = false,
+ itemViewStyle = {},
+ itemViewHeight = CONST.LHN_SKELETON_VIEW_ITEM_HEIGHT,
+}: ListItemSkeletonProps) {
const theme = useTheme();
const themeStyles = useThemeStyles();
const [numItems, setNumItems] = useState(fixedNumItems ?? 0);
+
+ const totalItemHeight = itemViewHeight + getVerticalMargin(itemViewStyle);
+
+ const handleLayout = useCallback(
+ (event: LayoutChangeEvent) => {
+ if (fixedNumItems) {
+ return;
+ }
+
+ const totalHeight = event.nativeEvent.layout.height;
+ const newNumItems = Math.ceil(totalHeight / totalItemHeight);
+ if (newNumItems !== numItems) {
+ setNumItems(newNumItems);
+ }
+ },
+ [fixedNumItems, numItems, totalItemHeight],
+ );
+
const skeletonViewItems = useMemo(() => {
const items = [];
for (let i = 0; i < numItems; i++) {
+ const opacity = gradientOpacityEnabled ? 1 - i / (numItems - 1) : 1;
items.push(
{
- if (fixedNumItems) {
- return;
- }
-
- const newNumItems = Math.ceil(event.nativeEvent.layout.height / itemViewHeight);
- if (newNumItems === numItems) {
- return;
- }
- setNumItems(newNumItems);
- }}
+ onLayout={handleLayout}
>
- {skeletonViewItems}
+ {skeletonViewItems}
);
}
diff --git a/src/components/Skeletons/TableListItemSkeleton.tsx b/src/components/Skeletons/SearchRowSkeleton.tsx
similarity index 54%
rename from src/components/Skeletons/TableListItemSkeleton.tsx
rename to src/components/Skeletons/SearchRowSkeleton.tsx
index 6ff3a3aedbb9..2359e47b7520 100644
--- a/src/components/Skeletons/TableListItemSkeleton.tsx
+++ b/src/components/Skeletons/SearchRowSkeleton.tsx
@@ -2,26 +2,41 @@ import React from 'react';
import {Circle, Rect} from 'react-native-svg';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
+import variables from '@styles/variables';
import CONST from '@src/CONST';
import ItemListSkeletonView from './ItemListSkeletonView';
-type TableListItemSkeletonProps = {
+type SearchRowSkeletonProps = {
shouldAnimate?: boolean;
fixedNumItems?: number;
+ gradientOpacityEnabled?: boolean;
};
-const barHeight = '10';
-const shortBarWidth = '40';
-const longBarWidth = '120';
+const barHeight = 8;
+const longBarWidth = 120;
+const leftPaneWidth = variables.sideBarWidth;
-function TableListItemSkeleton({shouldAnimate = true, fixedNumItems}: TableListItemSkeletonProps) {
+// 12 is the gap between the element and the right button
+const gapWidth = 12;
+
+// 80 is the width of the element itself
+const rightSideElementWidth = 80;
+
+// 24 is the padding of the central pane summing two sides
+const centralPanePadding = 40;
+
+// 80 is the width of the button on the right side
+const rightButtonWidth = 80;
+
+function SearchRowSkeleton({shouldAnimate = true, fixedNumItems, gradientOpacityEnabled = false}: SearchRowSkeletonProps) {
const styles = useThemeStyles();
- const {windowWidth, isSmallScreenWidth} = useWindowDimensions();
+ const {windowWidth, isSmallScreenWidth, isLargeScreenWidth} = useWindowDimensions();
if (isSmallScreenWidth) {
return (
(
@@ -51,7 +66,7 @@ function TableListItemSkeleton({shouldAnimate = true, fixedNumItems}: TableListI
height={4}
/>
);
}
+
return (
(
<>
-
+ {isLargeScreenWidth && (
+ <>
+
+
+
+ >
+ )}
+
+
>
)}
@@ -146,6 +181,6 @@ function TableListItemSkeleton({shouldAnimate = true, fixedNumItems}: TableListI
);
}
-TableListItemSkeleton.displayName = 'TableListItemSkeleton';
+SearchRowSkeleton.displayName = 'SearchRowSkeleton';
-export default TableListItemSkeleton;
+export default SearchRowSkeleton;
diff --git a/src/components/Skeletons/TableRowSkeleton.tsx b/src/components/Skeletons/TableRowSkeleton.tsx
new file mode 100644
index 000000000000..865bffc5842f
--- /dev/null
+++ b/src/components/Skeletons/TableRowSkeleton.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import {Circle, Rect} from 'react-native-svg';
+import useThemeStyles from '@hooks/useThemeStyles';
+import ItemListSkeletonView from './ItemListSkeletonView';
+
+type TableListItemSkeletonProps = {
+ shouldAnimate?: boolean;
+ fixedNumItems?: number;
+ gradientOpacityEnabled?: boolean;
+};
+
+const barHeight = '8';
+const shortBarWidth = '60';
+const longBarWidth = '124';
+
+function TableListItemSkeleton({shouldAnimate = true, fixedNumItems, gradientOpacityEnabled = false}: TableListItemSkeletonProps) {
+ const styles = useThemeStyles();
+
+ return (
+ (
+ <>
+
+
+
+ >
+ )}
+ />
+ );
+}
+
+TableListItemSkeleton.displayName = 'TableListItemSkeleton';
+
+export default TableListItemSkeleton;
diff --git a/src/components/TaskHeaderActionButton.tsx b/src/components/TaskHeaderActionButton.tsx
index 0c7e603a4aa2..5e563ea99763 100644
--- a/src/components/TaskHeaderActionButton.tsx
+++ b/src/components/TaskHeaderActionButton.tsx
@@ -5,6 +5,7 @@ import {withOnyx} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ReportUtils from '@libs/ReportUtils';
+import * as TaskUtils from '@libs/TaskUtils';
import * as Session from '@userActions/Session';
import * as Task from '@userActions/Task';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -36,7 +37,17 @@ function TaskHeaderActionButton({report, session}: TaskHeaderActionButtonProps)
isDisabled={!Task.canModifyTask(report, session?.accountID ?? -1)}
medium
text={translate(ReportUtils.isCompletedTaskReport(report) ? 'task.markAsIncomplete' : 'task.markAsComplete')}
- onPress={Session.checkIfActionIsAllowed(() => (ReportUtils.isCompletedTaskReport(report) ? Task.reopenTask(report) : Task.completeTask(report)))}
+ onPress={Session.checkIfActionIsAllowed(() => {
+ // If we're already navigating to these task editing pages, early return not to mark as completed, otherwise we would have not found page.
+ if (TaskUtils.isActiveTaskEditRoute(report.reportID)) {
+ return;
+ }
+ if (ReportUtils.isCompletedTaskReport(report)) {
+ Task.reopenTask(report);
+ } else {
+ Task.completeTask(report);
+ }
+ })}
style={styles.flex1}
/>
diff --git a/src/hooks/useTackInputFocus/index.ts b/src/hooks/useTackInputFocus/index.ts
index 124f8460127c..e6caa15f9dde 100644
--- a/src/hooks/useTackInputFocus/index.ts
+++ b/src/hooks/useTackInputFocus/index.ts
@@ -1,5 +1,6 @@
import {useCallback, useEffect} from 'react';
import useDebouncedState from '@hooks/useDebouncedState';
+import * as Browser from '@libs/Browser';
/**
* Detects input or text area focus on browsers, to avoid scrolling on virtual viewports
@@ -28,7 +29,13 @@ export default function useTackInputFocus(enable = false): boolean {
);
const resetScrollPositionOnVisualViewport = useCallback(() => {
- window.scrollTo({top: 0});
+ if (Browser.isChromeIOS() && window.visualViewport?.offsetTop) {
+ // On Chrome iOS, the visual viewport triggers a scroll event when the keyboard is opened, but some time the scroll position is not correct.
+ // So this change is specific to Chrome iOS, helping to reset the viewport position correctly.
+ window.scrollTo({top: -window.visualViewport.offsetTop});
+ } else {
+ window.scrollTo({top: 0});
+ }
}, []);
useEffect(() => {
diff --git a/src/hooks/useWindowDimensions/index.ts b/src/hooks/useWindowDimensions/index.ts
index 25757fda17e5..b391e45a61aa 100644
--- a/src/hooks/useWindowDimensions/index.ts
+++ b/src/hooks/useWindowDimensions/index.ts
@@ -23,7 +23,7 @@ export default function (useCachedViewportHeight = false): WindowDimensions {
unlockWindowDimensions: () => {},
};
- const isCachedViewportHeight = useCachedViewportHeight && Browser.isMobileSafari();
+ const isCachedViewportHeight = useCachedViewportHeight && Browser.isMobileWebKit();
const cachedViewportHeightWithKeyboardRef = useRef(initalViewportHeight);
const {width: windowWidth, height: windowHeight} = useWindowDimensions();
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 1ac9684ac22e..c7b5125d02fa 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -521,6 +521,7 @@ export default {
replyInThread: 'Reply in thread',
joinThread: 'Join thread',
leaveThread: 'Leave thread',
+ copyOnyxData: 'Copy Onyx data',
flagAsOffensive: 'Flag as offensive',
menu: 'Menu',
},
@@ -3125,6 +3126,13 @@ export default {
defaultVendor: 'Default vendor',
autoSync: 'Auto-sync',
reimbursedReports: 'Sync reimbursed reports',
+ reconciliationAccount: 'Reconciliation account',
+ chooseReconciliationAccount: {
+ chooseBankAccount: 'Choose the bank account that your Expensify Card payments will be reconciled against.',
+ accountMatches: 'Make sure this account matches your ',
+ settlementAccount: 'Expensify Card settlement account ',
+ reconciliationWorks: (lastFourPAN: string) => `(ending in ${lastFourPAN}) so Continuous Reconciliation works properly.`,
+ },
},
bills: {
manageYourBills: 'Manage your bills',
@@ -4050,7 +4058,7 @@ export default {
},
paymentCard: {
addPaymentCard: 'Add payment card',
- enterPaymentCardDetails: 'Enter your payment card details.',
+ enterPaymentCardDetails: 'Enter your payment card details',
security: 'Expensify is PCI-DSS compliant, uses bank-level encryption, and utilizes redundant infrastructure to protect your data.',
learnMoreAboutSecurity: 'Learn more about our security.',
},
diff --git a/src/languages/es.ts b/src/languages/es.ts
index ea9186daee78..075903d0f324 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -513,6 +513,7 @@ export default {
replyInThread: 'Responder en el hilo',
joinThread: 'Unirse al hilo',
leaveThread: 'Dejar hilo',
+ copyOnyxData: 'Copiar datos de Onyx',
flagAsOffensive: 'Marcar como ofensivo',
menu: 'Menú',
},
@@ -3111,6 +3112,13 @@ export default {
defaultVendor: 'Proveedor predeterminado',
autoSync: 'Autosincronización',
reimbursedReports: 'Sincronizar informes reembolsados',
+ reconciliationAccount: 'Cuenta de conciliación',
+ chooseReconciliationAccount: {
+ chooseBankAccount: 'Elige la cuenta bancaria con la que se conciliarán los pagos de tu Tarjeta Expensify.',
+ accountMatches: 'Asegúrate de que esta cuenta coincide con ',
+ settlementAccount: 'la cuenta de liquidación de tu Tarjeta Expensify ',
+ reconciliationWorks: (lastFourPAN: string) => `(que termina en ${lastFourPAN}) para que la conciliación continua funcione correctamente.`,
+ },
},
card: {
header: 'Desbloquea Tarjetas Expensify gratis',
@@ -4565,7 +4573,7 @@ export default {
},
paymentCard: {
addPaymentCard: 'Añade tarjeta de pago',
- enterPaymentCardDetails: 'Introduce los datos de tu tarjeta de pago.',
+ enterPaymentCardDetails: 'Introduce los datos de tu tarjeta de pago',
security: 'Expensify es PCI-DSS obediente, utiliza cifrado a nivel bancario, y emplea infraestructura redundante para proteger tus datos.',
learnMoreAboutSecurity: 'Conozca más sobre nuestra seguridad.',
},
diff --git a/src/libs/API/parameters/PolicyReportFieldsReplace.ts b/src/libs/API/parameters/DeletePolicyReportField.ts
similarity index 66%
rename from src/libs/API/parameters/PolicyReportFieldsReplace.ts
rename to src/libs/API/parameters/DeletePolicyReportField.ts
index c6d1834f0789..d79e9b07249e 100644
--- a/src/libs/API/parameters/PolicyReportFieldsReplace.ts
+++ b/src/libs/API/parameters/DeletePolicyReportField.ts
@@ -1,4 +1,4 @@
-type PolicyReportFieldsReplace = {
+type DeletePolicyReportField = {
policyID: string;
/**
* Stringified JSON object with type of following structure:
@@ -7,4 +7,4 @@ type PolicyReportFieldsReplace = {
reportFields: string;
};
-export default PolicyReportFieldsReplace;
+export default DeletePolicyReportField;
diff --git a/src/libs/API/parameters/UpdateFrequentlyUsedEmojisParams.ts b/src/libs/API/parameters/UpdateFrequentlyUsedEmojisParams.ts
deleted file mode 100644
index f790ada3aad9..000000000000
--- a/src/libs/API/parameters/UpdateFrequentlyUsedEmojisParams.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-type UpdateFrequentlyUsedEmojisParams = {value: string};
-
-export default UpdateFrequentlyUsedEmojisParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index ff8465cfeec7..ff62d9b69ea6 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -64,7 +64,6 @@ export type {default as UpdateAutomaticTimezoneParams} from './UpdateAutomaticTi
export type {default as UpdateChatPriorityModeParams} from './UpdateChatPriorityModeParams';
export type {default as UpdateDateOfBirthParams} from './UpdateDateOfBirthParams';
export type {default as UpdateDisplayNameParams} from './UpdateDisplayNameParams';
-export type {default as UpdateFrequentlyUsedEmojisParams} from './UpdateFrequentlyUsedEmojisParams';
export type {default as UpdateGroupChatNameParams} from './UpdateGroupChatNameParams';
export type {default as UpdateGroupChatMemberRolesParams} from './UpdateGroupChatMemberRolesParams';
export type {default as UpdateHomeAddressParams} from './UpdateHomeAddressParams';
@@ -242,7 +241,7 @@ export type {default as DeleteMoneyRequestOnSearchParams} from './DeleteMoneyReq
export type {default as HoldMoneyRequestOnSearchParams} from './HoldMoneyRequestOnSearchParams';
export type {default as UnholdMoneyRequestOnSearchParams} from './UnholdMoneyRequestOnSearchParams';
export type {default as UpdateNetSuiteSubsidiaryParams} from './UpdateNetSuiteSubsidiaryParams';
-export type {default as PolicyReportFieldsReplace} from './PolicyReportFieldsReplace';
+export type {default as DeletePolicyReportField} from './DeletePolicyReportField';
export type {default as ConnectPolicyToNetSuiteParams} from './ConnectPolicyToNetSuiteParams';
export type {default as CreateWorkspaceReportFieldParams} from './CreateWorkspaceReportFieldParams';
export type {default as UpdateWorkspaceReportFieldInitialValueParams} from './UpdateWorkspaceReportFieldInitialValueParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index ca284321e3bb..948ed7f76373 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -57,7 +57,6 @@ const WRITE_COMMANDS = {
VALIDATE_LOGIN: 'ValidateLogin',
VALIDATE_SECONDARY_LOGIN: 'ValidateSecondaryLogin',
UPDATE_PREFERRED_EMOJI_SKIN_TONE: 'UpdatePreferredEmojiSkinTone',
- UPDATE_FREQUENTLY_USED_EMOJIS: 'UpdateFrequentlyUsedEmojis',
UPDATE_CHAT_PRIORITY_MODE: 'UpdateChatPriorityMode',
SET_CONTACT_METHOD_AS_DEFAULT: 'SetContactMethodAsDefault',
UPDATE_THEME: 'UpdateTheme',
@@ -351,7 +350,6 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.VALIDATE_LOGIN]: Parameters.ValidateLoginParams;
[WRITE_COMMANDS.VALIDATE_SECONDARY_LOGIN]: Parameters.ValidateSecondaryLoginParams;
[WRITE_COMMANDS.UPDATE_PREFERRED_EMOJI_SKIN_TONE]: Parameters.UpdatePreferredEmojiSkinToneParams;
- [WRITE_COMMANDS.UPDATE_FREQUENTLY_USED_EMOJIS]: Parameters.UpdateFrequentlyUsedEmojisParams;
[WRITE_COMMANDS.UPDATE_CHAT_PRIORITY_MODE]: Parameters.UpdateChatPriorityModeParams;
[WRITE_COMMANDS.SET_CONTACT_METHOD_AS_DEFAULT]: Parameters.SetContactMethodAsDefaultParams;
[WRITE_COMMANDS.UPDATE_THEME]: Parameters.UpdateThemeParams;
@@ -426,7 +424,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.RENAME_WORKSPACE_CATEGORY]: Parameters.RenameWorkspaceCategoriesParams;
[WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams;
[WRITE_COMMANDS.DELETE_WORKSPACE_CATEGORIES]: Parameters.DeleteWorkspaceCategoriesParams;
- [WRITE_COMMANDS.DELETE_POLICY_REPORT_FIELD]: Parameters.PolicyReportFieldsReplace;
+ [WRITE_COMMANDS.DELETE_POLICY_REPORT_FIELD]: Parameters.DeletePolicyReportField;
[WRITE_COMMANDS.SET_POLICY_REQUIRES_TAG]: Parameters.SetPolicyRequiresTag;
[WRITE_COMMANDS.SET_POLICY_TAGS_REQUIRED]: Parameters.SetPolicyTagsRequired;
[WRITE_COMMANDS.RENAME_POLICY_TAG_LIST]: Parameters.RenamePolicyTaglistParams;
diff --git a/src/libs/Browser/index.ts b/src/libs/Browser/index.ts
index 98ad449c3dd0..aeec4f4def4a 100644
--- a/src/libs/Browser/index.ts
+++ b/src/libs/Browser/index.ts
@@ -1,4 +1,4 @@
-import type {GetBrowser, IsMobile, IsMobileChrome, IsMobileSafari, IsMobileWebKit, IsSafari, OpenRouteInDesktopApp} from './types';
+import type {GetBrowser, IsChromeIOS, IsMobile, IsMobileChrome, IsMobileSafari, IsMobileWebKit, IsSafari, OpenRouteInDesktopApp} from './types';
const getBrowser: GetBrowser = () => '';
@@ -10,8 +10,10 @@ const isMobileChrome: IsMobileChrome = () => false;
const isMobileWebKit: IsMobileWebKit = () => false;
+const isChromeIOS: IsChromeIOS = () => false;
+
const isSafari: IsSafari = () => false;
const openRouteInDesktopApp: OpenRouteInDesktopApp = () => {};
-export {getBrowser, isMobile, isMobileSafari, isMobileWebKit, isSafari, isMobileChrome, openRouteInDesktopApp};
+export {getBrowser, isMobile, isMobileSafari, isMobileWebKit, isSafari, isMobileChrome, isChromeIOS, openRouteInDesktopApp};
diff --git a/src/libs/Browser/index.website.ts b/src/libs/Browser/index.website.ts
index a83fa1cac70e..b89190dc7f78 100644
--- a/src/libs/Browser/index.website.ts
+++ b/src/libs/Browser/index.website.ts
@@ -1,7 +1,7 @@
import CONFIG from '@src/CONFIG';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
-import type {GetBrowser, IsMobile, IsMobileChrome, IsMobileSafari, IsMobileWebKit, IsSafari, OpenRouteInDesktopApp} from './types';
+import type {GetBrowser, IsChromeIOS, IsMobile, IsMobileChrome, IsMobileSafari, IsMobileWebKit, IsSafari, OpenRouteInDesktopApp} from './types';
/**
* Fetch browser name from UA string
@@ -66,6 +66,14 @@ const isMobileWebKit: IsMobileWebKit = () => {
return /iP(ad|od|hone)/i.test(userAgent) && /WebKit/i.test(userAgent);
};
+/**
+ * Checks if the requesting user agent is a Chrome browser on an iOS mobile device.
+ */
+const isChromeIOS: IsChromeIOS = () => {
+ const userAgent = navigator.userAgent;
+ return /iP(ad|od|hone)/i.test(userAgent) && /CriOS/i.test(userAgent);
+};
+
const isSafari: IsSafari = () => getBrowser() === 'safari' || isMobileSafari();
/**
@@ -109,4 +117,4 @@ const openRouteInDesktopApp: OpenRouteInDesktopApp = (shortLivedAuthToken = '',
}
};
-export {getBrowser, isMobile, isMobileSafari, isMobileWebKit, isSafari, isMobileChrome, openRouteInDesktopApp};
+export {getBrowser, isMobile, isMobileSafari, isMobileWebKit, isSafari, isMobileChrome, isChromeIOS, openRouteInDesktopApp};
diff --git a/src/libs/Browser/types.ts b/src/libs/Browser/types.ts
index 25f305953c87..cb242d3729aa 100644
--- a/src/libs/Browser/types.ts
+++ b/src/libs/Browser/types.ts
@@ -8,8 +8,10 @@ type IsMobileChrome = () => boolean;
type IsMobileWebKit = () => boolean;
+type IsChromeIOS = () => boolean;
+
type IsSafari = () => boolean;
type OpenRouteInDesktopApp = (shortLivedAuthToken?: string, email?: string) => void;
-export type {GetBrowser, IsMobile, IsMobileSafari, IsMobileChrome, IsMobileWebKit, IsSafari, OpenRouteInDesktopApp};
+export type {GetBrowser, IsMobile, IsMobileSafari, IsMobileChrome, IsMobileWebKit, IsSafari, IsChromeIOS, OpenRouteInDesktopApp};
diff --git a/src/libs/CurrencyUtils.ts b/src/libs/CurrencyUtils.ts
index 862b0ae5e928..c3b80797d750 100644
--- a/src/libs/CurrencyUtils.ts
+++ b/src/libs/CurrencyUtils.ts
@@ -125,9 +125,11 @@ function convertToDisplayString(amountInCents = 0, currency: string = CONST.CURR
style: 'currency',
currency: currencyWithFallback,
- // We are forcing the number of decimals because we override the default number of decimals in the backend for RSD
+ // We are forcing the number of decimals because we override the default number of decimals in the backend for some currencies
// See: https://github.com/Expensify/PHP-Libs/pull/834
- minimumFractionDigits: currency === 'RSD' ? getCurrencyDecimals(currency) : undefined,
+ minimumFractionDigits: getCurrencyDecimals(currency),
+ // For currencies that have decimal places > 2, floor to 2 instead as we don't support more than 2 decimal places.
+ maximumFractionDigits: 2,
});
}
@@ -175,9 +177,11 @@ function convertToDisplayStringWithoutCurrency(amountInCents: number, currency:
style: 'currency',
currency,
- // We are forcing the number of decimals because we override the default number of decimals in the backend for RSD
+ // We are forcing the number of decimals because we override the default number of decimals in the backend for some currencies
// See: https://github.com/Expensify/PHP-Libs/pull/834
- minimumFractionDigits: currency === 'RSD' ? getCurrencyDecimals(currency) : undefined,
+ minimumFractionDigits: getCurrencyDecimals(currency),
+ // For currencies that have decimal places > 2, floor to 2 instead as we don't support more than 2 decimal places.
+ maximumFractionDigits: 2,
})
.filter((x) => x.type !== 'currency')
.filter((x) => x.type !== 'literal' || x.value.trim().length !== 0)
diff --git a/src/libs/E2E/reactNativeLaunchingTest.ts b/src/libs/E2E/reactNativeLaunchingTest.ts
index f952998f0aad..f61bee8dae6a 100644
--- a/src/libs/E2E/reactNativeLaunchingTest.ts
+++ b/src/libs/E2E/reactNativeLaunchingTest.ts
@@ -36,6 +36,7 @@ const tests: Tests = {
[E2EConfig.TEST_NAMES.ChatOpening]: require('./tests/chatOpeningTest.e2e').default,
[E2EConfig.TEST_NAMES.ReportTyping]: require('./tests/reportTypingTest.e2e').default,
[E2EConfig.TEST_NAMES.Linking]: require('./tests/linkingTest.e2e').default,
+ [E2EConfig.TEST_NAMES.PreloadedLinking]: require('./tests/preloadedLinkingTest.e2e').default,
};
// Once we receive the TII measurement we know that the app is initialized and ready to be used:
diff --git a/src/libs/E2E/tests/preloadedLinkingTest.e2e.ts b/src/libs/E2E/tests/preloadedLinkingTest.e2e.ts
new file mode 100644
index 000000000000..a36200b1a702
--- /dev/null
+++ b/src/libs/E2E/tests/preloadedLinkingTest.e2e.ts
@@ -0,0 +1,82 @@
+import {DeviceEventEmitter} from 'react-native';
+import type {NativeConfig} from 'react-native-config';
+import Config from 'react-native-config';
+import Timing from '@libs/actions/Timing';
+import E2ELogin from '@libs/E2E/actions/e2eLogin';
+import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded';
+import E2EClient from '@libs/E2E/client';
+import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow';
+import getPromiseWithResolve from '@libs/E2E/utils/getPromiseWithResolve';
+import Navigation from '@libs/Navigation/Navigation';
+import Performance from '@libs/Performance';
+import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
+
+type ViewableItem = {
+ reportActionID?: string;
+};
+type ViewableItemResponse = Array<{item?: ViewableItem}>;
+
+const test = (config: NativeConfig) => {
+ console.debug('[E2E] Logging in for comment linking');
+
+ const reportID = getConfigValueOrThrow('reportID', config);
+ const linkedReportActionID = getConfigValueOrThrow('linkedReportActionID', config);
+
+ E2ELogin().then((neededLogin) => {
+ if (neededLogin) {
+ return waitForAppLoaded().then(() => E2EClient.submitTestDone());
+ }
+
+ const [appearMessagePromise, appearMessageResolve] = getPromiseWithResolve();
+ const [switchReportPromise, switchReportResolve] = getPromiseWithResolve();
+
+ Promise.all([appearMessagePromise, switchReportPromise])
+ .then(() => {
+ console.debug('[E2E] Test completed successfully, exiting…');
+ E2EClient.submitTestDone();
+ })
+ .catch((err) => {
+ console.debug('[E2E] Error while submitting test results:', err);
+ });
+
+ const subscription = DeviceEventEmitter.addListener('onViewableItemsChanged', (res: ViewableItemResponse) => {
+ console.debug('[E2E] Viewable items retrieved, verifying correct message…', res);
+ if (!!res && res?.[0]?.item?.reportActionID === linkedReportActionID) {
+ appearMessageResolve();
+ subscription.remove();
+ } else {
+ console.debug(`[E2E] Provided message id '${res?.[0]?.item?.reportActionID}' doesn't match to an expected '${linkedReportActionID}'. Waiting for a next one…`);
+ }
+ });
+
+ Performance.subscribeToMeasurements((entry) => {
+ if (entry.name === CONST.TIMING.SIDEBAR_LOADED) {
+ console.debug('[E2E] Sidebar loaded, navigating to a report…');
+ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID));
+ return;
+ }
+
+ if (entry.name === CONST.TIMING.REPORT_INITIAL_RENDER) {
+ console.debug('[E2E] Navigating to linked report action…');
+ Timing.start(CONST.TIMING.SWITCH_REPORT);
+ Performance.markStart(CONST.TIMING.SWITCH_REPORT);
+
+ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID, linkedReportActionID));
+ return;
+ }
+
+ if (entry.name === CONST.TIMING.CHAT_RENDER) {
+ E2EClient.submitTestResults({
+ branch: Config.E2E_BRANCH,
+ name: 'Comment linking',
+ metric: entry.duration,
+ });
+
+ switchReportResolve();
+ }
+ });
+ });
+};
+
+export default test;
diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts
index 6fb5725addfc..007a892c048e 100644
--- a/src/libs/EmojiUtils.ts
+++ b/src/libs/EmojiUtils.ts
@@ -1,4 +1,3 @@
-import {getUnixTime} from 'date-fns';
import {Str} from 'expensify-common';
import memoize from 'lodash/memoize';
import Onyx from 'react-native-onyx';
@@ -235,37 +234,6 @@ function mergeEmojisWithFrequentlyUsedEmojis(emojis: PickerEmojis): EmojiPickerL
return addSpacesToEmojiCategories(mergedEmojis);
}
-/**
- * Get the updated frequently used emojis list by usage
- */
-function getFrequentlyUsedEmojis(newEmoji: Emoji | Emoji[]): FrequentlyUsedEmoji[] {
- let frequentEmojiList = [...frequentlyUsedEmojis];
-
- const maxFrequentEmojiCount = CONST.EMOJI_FREQUENT_ROW_COUNT * CONST.EMOJI_NUM_PER_ROW - 1;
-
- const currentTimestamp = getUnixTime(new Date());
- (Array.isArray(newEmoji) ? [...newEmoji] : [newEmoji]).forEach((emoji) => {
- let currentEmojiCount = 1;
- const emojiIndex = frequentEmojiList.findIndex((e) => e.code === emoji.code);
- if (emojiIndex >= 0) {
- currentEmojiCount = frequentEmojiList[emojiIndex].count + 1;
- frequentEmojiList.splice(emojiIndex, 1);
- }
-
- const updatedEmoji = {...Emojis.emojiCodeTableWithSkinTones[emoji.code], count: currentEmojiCount, lastUpdatedAt: currentTimestamp};
-
- // We want to make sure the current emoji is added to the list
- // Hence, we take one less than the current frequent used emojis
- frequentEmojiList = frequentEmojiList.slice(0, maxFrequentEmojiCount);
- frequentEmojiList.push(updatedEmoji);
-
- // Sort the list by count and lastUpdatedAt in descending order
- frequentEmojiList.sort((a, b) => b.count - a.count || b.lastUpdatedAt - a.lastUpdatedAt);
- });
-
- return frequentEmojiList;
-}
-
/**
* Given an emoji item object, return an emoji code based on its type.
*/
@@ -601,7 +569,6 @@ export {
getLocalizedEmojiName,
getHeaderEmojis,
mergeEmojisWithFrequentlyUsedEmojis,
- getFrequentlyUsedEmojis,
containsOnlyEmojis,
replaceEmojis,
suggestEmojis,
diff --git a/src/libs/Environment/Environment.ts b/src/libs/Environment/Environment.ts
index 1f5a391d3b13..c343788bed05 100644
--- a/src/libs/Environment/Environment.ts
+++ b/src/libs/Environment/Environment.ts
@@ -38,6 +38,13 @@ function isDevelopment(): boolean {
return (Config?.ENVIRONMENT ?? CONST.ENVIRONMENT.DEV) === CONST.ENVIRONMENT.DEV;
}
+/**
+ * Are we running the app in staging?
+ */
+function isStaging(): boolean {
+ return (Config?.ENVIRONMENT ?? CONST.ENVIRONMENT.DEV) === CONST.ENVIRONMENT.STAGING;
+}
+
/**
* Are we running the app in production?
*/
@@ -76,4 +83,4 @@ function getSpotnanaEnvironmentTMCID(): Promise {
return getEnvironment().then((environment) => SPOTNANA_ENVIRONMENT_TMC_ID[environment]);
}
-export {getEnvironment, isInternalTestBuild, isDevelopment, isProduction, getEnvironmentURL, getOldDotEnvironmentURL, getTravelDotEnvironmentURL, getSpotnanaEnvironmentTMCID};
+export {getEnvironment, isInternalTestBuild, isDevelopment, isStaging, isProduction, getEnvironmentURL, getOldDotEnvironmentURL, getTravelDotEnvironmentURL, getSpotnanaEnvironmentTMCID};
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index 0b94972b2aa9..ce43c78a6fee 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -396,6 +396,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PAYMENT_ACCOUNT]: () =>
require('../../../../pages/workspace/accounting/intacct/advanced/SageIntacctPaymentAccountPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.RECONCILIATION_ACCOUNT_SETTINGS]: () => require('../../../../pages/workspace/accounting/ReconciliationAccountSettingsPage').default,
[SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage').default,
[SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage').default,
[SCREENS.WORKSPACE.TAX_EDIT]: () => require('../../../../pages/workspace/taxes/WorkspaceEditTaxPage').default,
@@ -408,13 +409,13 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Subscription/PaymentCard/ChangeBillingCurrency').default,
[SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD]: () => require('../../../../pages/settings/Subscription/PaymentCard').default,
[SCREENS.SETTINGS.ADD_PAYMENT_CARD_CHANGE_CURRENCY]: () => require('../../../../pages/settings/PaymentCard/ChangeCurrency').default,
- [SCREENS.WORKSPACE.REPORT_FIELDS_CREATE]: () => require('../../../../pages/workspace/reportFields/CreateReportFieldPage').default,
- [SCREENS.WORKSPACE.REPORT_FIELD_SETTINGS]: () => require('../../../../pages/workspace/reportFields/ReportFieldSettingsPage').default,
- [SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: () => require('../../../../pages/workspace/reportFields/ReportFieldListValuesPage').default,
- [SCREENS.WORKSPACE.REPORT_FIELDS_ADD_VALUE]: () => require('../../../../pages/workspace/reportFields/ReportFieldAddListValuePage').default,
- [SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS]: () => require('../../../../pages/workspace/reportFields/ReportFieldValueSettingsPage').default,
- [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE]: () => require('../../../../pages/workspace/reportFields/ReportFieldInitialValuePage').default,
- [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE]: () => require('../../../../pages/workspace/reportFields/ReportFieldEditValuePage').default,
+ [SCREENS.WORKSPACE.REPORT_FIELDS_CREATE]: () => require('../../../../pages/workspace/reportFields/CreateReportFieldsPage').default,
+ [SCREENS.WORKSPACE.REPORT_FIELDS_SETTINGS]: () => require('../../../../pages/workspace/reportFields/ReportFieldsSettingsPage').default,
+ [SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: () => require('../../../../pages/workspace/reportFields/ReportFieldsListValuesPage').default,
+ [SCREENS.WORKSPACE.REPORT_FIELDS_ADD_VALUE]: () => require('../../../../pages/workspace/reportFields/ReportFieldsAddListValuePage').default,
+ [SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS]: () => require('../../../../pages/workspace/reportFields/ReportFieldsValueSettingsPage').default,
+ [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE]: () => require('../../../../pages/workspace/reportFields/ReportFieldsInitialValuePage').default,
+ [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE]: () => require('../../../../pages/workspace/reportFields/ReportFieldsEditValuePage').default,
[SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_IMPORT]: () => require('../../../../pages/workspace/accounting/intacct/import/SageIntacctImportPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_TOGGLE_MAPPING]: () =>
require('../../../../pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage').default,
diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
index 54804a495754..31b44a2681fd 100755
--- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
@@ -112,6 +112,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT,
SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_ADVANCED,
SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PAYMENT_ACCOUNT,
+ SCREENS.WORKSPACE.ACCOUNTING.RECONCILIATION_ACCOUNT_SETTINGS,
],
[SCREENS.WORKSPACE.TAXES]: [
SCREENS.WORKSPACE.TAXES_SETTINGS,
@@ -143,7 +144,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
],
[SCREENS.WORKSPACE.REPORT_FIELDS]: [
SCREENS.WORKSPACE.REPORT_FIELDS_CREATE,
- SCREENS.WORKSPACE.REPORT_FIELD_SETTINGS,
+ SCREENS.WORKSPACE.REPORT_FIELDS_SETTINGS,
SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES,
SCREENS.WORKSPACE.REPORT_FIELDS_ADD_VALUE,
SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS,
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 6dc3434ba3a2..1b224f1c0065 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -449,6 +449,7 @@ const config: LinkingOptions['config'] = {
},
[SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_ADVANCED]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_ADVANCED.route},
[SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PAYMENT_ACCOUNT]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_PAYMENT_ACCOUNT.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.RECONCILIATION_ACCOUNT_SETTINGS]: {path: ROUTES.WORKSPACE_ACCOUNTING_RECONCILIATION_ACCOUNT_SETTINGS.route},
[SCREENS.WORKSPACE.DESCRIPTION]: {
path: ROUTES.WORKSPACE_PROFILE_DESCRIPTION.route,
},
@@ -587,34 +588,34 @@ const config: LinkingOptions['config'] = {
path: ROUTES.WORKSPACE_CREATE_REPORT_FIELD.route,
},
[SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: {
- path: ROUTES.WORKSPACE_REPORT_FIELD_LIST_VALUES.route,
+ path: ROUTES.WORKSPACE_REPORT_FIELDS_LIST_VALUES.route,
parse: {
reportFieldID: (reportFieldID: string) => decodeURIComponent(reportFieldID),
},
},
[SCREENS.WORKSPACE.REPORT_FIELDS_ADD_VALUE]: {
- path: ROUTES.WORKSPACE_REPORT_FIELD_ADD_VALUE.route,
+ path: ROUTES.WORKSPACE_REPORT_FIELDS_ADD_VALUE.route,
parse: {
reportFieldID: (reportFieldID: string) => decodeURIComponent(reportFieldID),
},
},
[SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS]: {
- path: ROUTES.WORKSPACE_REPORT_FIELD_VALUE_SETTINGS.route,
+ path: ROUTES.WORKSPACE_REPORT_FIELDS_VALUE_SETTINGS.route,
parse: {
reportFieldID: (reportFieldID: string) => decodeURIComponent(reportFieldID),
},
},
[SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE]: {
- path: ROUTES.WORKSPACE_REPORT_FIELD_EDIT_VALUE.route,
+ path: ROUTES.WORKSPACE_REPORT_FIELDS_EDIT_VALUE.route,
},
- [SCREENS.WORKSPACE.REPORT_FIELD_SETTINGS]: {
- path: ROUTES.WORKSPACE_REPORT_FIELD_SETTINGS.route,
+ [SCREENS.WORKSPACE.REPORT_FIELDS_SETTINGS]: {
+ path: ROUTES.WORKSPACE_REPORT_FIELDS_SETTINGS.route,
parse: {
reportFieldID: (reportFieldID: string) => decodeURIComponent(reportFieldID),
},
},
[SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE]: {
- path: ROUTES.WORKSPACE_EDIT_REPORT_FIELD_INITIAL_VALUE.route,
+ path: ROUTES.WORKSPACE_EDIT_REPORT_FIELDS_INITIAL_VALUE.route,
parse: {
reportFieldID: (reportFieldID: string) => decodeURIComponent(reportFieldID),
},
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index fc67fe6b8cc0..00fd98dc51aa 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -294,7 +294,7 @@ type SettingsNavigatorParamList = {
policyID: string;
valueIndex: number;
};
- [SCREENS.WORKSPACE.REPORT_FIELD_SETTINGS]: {
+ [SCREENS.WORKSPACE.REPORT_FIELDS_SETTINGS]: {
policyID: string;
reportFieldID: string;
};
@@ -588,6 +588,10 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PAYMENT_ACCOUNT]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.ACCOUNTING.RECONCILIATION_ACCOUNT_SETTINGS]: {
+ policyID: string;
+ connection: ValueOf;
+ };
[SCREENS.GET_ASSISTANCE]: {
backTo: Routes;
};
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index 330d9d6ef61d..73b04742878a 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -657,10 +657,11 @@ function isSearchStringMatchUserDetails(personalDetail: PersonalDetails, searchV
* Get the last message text from the report directly or from other sources for special cases.
*/
function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails: Partial | null, policy?: OnyxEntry): string {
- const lastReportAction = visibleReportActionItems[report?.reportID ?? '-1'] ?? null;
+ const reportID = report?.reportID ?? '-1';
+ const lastReportAction = visibleReportActionItems[reportID] ?? null;
// some types of actions are filtered out for lastReportAction, in some cases we need to check the actual last action
- const lastOriginalReportAction = lastReportActions[report?.reportID ?? '-1'] ?? null;
+ const lastOriginalReportAction = lastReportActions[reportID] ?? null;
let lastMessageTextFromReport = '';
if (ReportUtils.isArchivedRoom(report)) {
@@ -720,8 +721,10 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails
lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(TaskUtils.getTaskReportActionMessage(lastReportAction).text);
} else if (ReportActionUtils.isCreatedTaskReportAction(lastReportAction)) {
lastMessageTextFromReport = TaskUtils.getTaskCreatedMessage(lastReportAction);
- } else if (ReportActionUtils.isApprovedOrSubmittedReportAction(lastReportAction)) {
- lastMessageTextFromReport = ReportActionUtils.getReportActionMessageText(lastReportAction);
+ } else if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) {
+ lastMessageTextFromReport = ReportUtils.getIOUSubmittedMessage(reportID);
+ } else if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.APPROVED) {
+ lastMessageTextFromReport = ReportUtils.getIOUApprovedMessage(reportID);
}
return lastMessageTextFromReport || (report?.lastMessageText ?? '');
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index 3f8acd0e06fe..85850c15e534 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -260,6 +260,18 @@ function isRoomChangeLogAction(reportAction: OnyxEntry): reportAct
return isActionOfType(reportAction, ...Object.values(CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG));
}
+function isInviteOrRemovedAction(
+ reportAction: OnyxInputOrEntry,
+): reportAction is ReportAction> {
+ return isActionOfType(
+ reportAction,
+ CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.INVITE_TO_ROOM,
+ CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.REMOVE_FROM_ROOM,
+ CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.INVITE_TO_ROOM,
+ CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_FROM_ROOM,
+ );
+}
+
/**
* Returns whether the comment is a thread parent message/the first message in a thread
*/
@@ -1474,6 +1486,7 @@ export {
isClosedAction,
isRenamedAction,
isRoomChangeLogAction,
+ isInviteOrRemovedAction,
isChronosOOOListAction,
isAddCommentAction,
isPolicyChangeLogAction,
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index fb3bebd75274..97d8c605b0ab 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -52,6 +52,7 @@ import AccountUtils from './AccountUtils';
import * as IOU from './actions/IOU';
import * as PolicyActions from './actions/Policy/Policy';
import * as store from './actions/ReimbursementAccount/store';
+import * as SessionUtils from './actions/Session';
import * as CurrencyUtils from './CurrencyUtils';
import DateUtils from './DateUtils';
import {hasValidDraftComment} from './DraftCommentUtils';
@@ -1065,7 +1066,7 @@ function isSystemChat(report: OnyxEntry): boolean {
* Only returns true if this is our main 1:1 DM report with Concierge.
*/
function isConciergeChatReport(report: OnyxInputOrEntry): boolean {
- const participantAccountIDs = Object.keys(report?.participants ?? {});
+ const participantAccountIDs = Object.keys(report?.participants ?? {}).filter((accountID) => Number(accountID) !== currentUserAccountID);
return participantAccountIDs.length === 1 && Number(participantAccountIDs[0]) === CONST.ACCOUNT_ID.CONCIERGE && !isChatThread(report);
}
@@ -4002,11 +4003,19 @@ function buildOptimisticExpenseReport(chatReportID: string, policyID: string, pa
return expenseReport;
}
-function getIOUSubmittedMessage(reportID: string) {
+function getFormattedAmount(reportID: string) {
const report = getReportOrDraftReport(reportID);
const linkedReport = isChatThread(report) ? getParentReport(report) : report;
const formattedAmount = CurrencyUtils.convertToDisplayString(Math.abs(linkedReport?.total ?? 0), linkedReport?.currency);
- return Localize.translateLocal('iou.submittedAmount', {formattedAmount});
+ return formattedAmount;
+}
+
+function getIOUSubmittedMessage(reportID: string) {
+ return Localize.translateLocal('iou.submittedAmount', {formattedAmount: getFormattedAmount(reportID)});
+}
+
+function getIOUApprovedMessage(reportID: string) {
+ return Localize.translateLocal('iou.approvedAmount', {amount: getFormattedAmount(reportID)});
}
/**
@@ -5451,6 +5460,10 @@ function shouldReportBeInOptionList({
return false;
}
+ if (report?.type === CONST.REPORT.TYPE.PAYCHECK || report?.type === CONST.REPORT.TYPE.BILL) {
+ return false;
+ }
+
// Include the currently viewed report. If we excluded the currently viewed report, then there
// would be no way to highlight it in the options list and it would be confusing to users because they lose
// a sense of context.
@@ -6922,6 +6935,10 @@ function canJoinChat(report: OnyxInputOrEntry, parentReportAction: OnyxI
* Whether the user can leave a report
*/
function canLeaveChat(report: OnyxEntry, policy: OnyxEntry): boolean {
+ if (isPublicRoom(report) && SessionUtils.isAnonymousUser()) {
+ return false;
+ }
+
if (report?.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) {
return false;
}
@@ -7167,6 +7184,7 @@ export {
getGroupChatName,
getIOUReportActionDisplayMessage,
getIOUReportActionMessage,
+ getIOUApprovedMessage,
getIOUSubmittedMessage,
getIcons,
getIconsForParticipants,
diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts
index 80081c8f89c7..9efd1e584052 100644
--- a/src/libs/SidebarUtils.ts
+++ b/src/libs/SidebarUtils.ts
@@ -6,6 +6,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {PersonalDetails, PersonalDetailsList, ReportActions, TransactionViolation} from '@src/types/onyx';
import type Beta from '@src/types/onyx/Beta';
+import type {OriginalMessageChangeLog} from '@src/types/onyx/OriginalMessage';
import type Policy from '@src/types/onyx/Policy';
import type PriorityMode from '@src/types/onyx/PriorityMode';
import type Report from '@src/types/onyx/Report';
@@ -353,7 +354,7 @@ function getOptionData({
result.alternateText = Localize.translate(preferredLocale, 'newRoomPage.roomRenamedTo', {newName});
} else if (ReportActionsUtils.isTaskAction(lastAction)) {
result.alternateText = ReportUtils.formatReportLastMessageText(TaskUtils.getTaskReportActionMessage(lastAction).text);
- } else if (ReportActionsUtils.isRoomChangeLogAction(lastAction)) {
+ } else if (ReportActionsUtils.isInviteOrRemovedAction(lastAction)) {
const lastActionOriginalMessage = lastAction?.actionName ? ReportActionsUtils.getOriginalMessage(lastAction) : null;
const targetAccountIDs = lastActionOriginalMessage?.targetAccountIDs ?? [];
const targetAccountIDsLength = targetAccountIDs.length !== 0 ? targetAccountIDs.length : report.lastMessageHtml?.match(/]*><\/mention-user>/g)?.length ?? 0;
@@ -372,11 +373,9 @@ function getOptionData({
: ` ${Localize.translate(preferredLocale, 'workspace.invite.from')}`;
result.alternateText += `${preposition} ${roomName}`;
}
- if (lastActionName === CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.UPDATE_ROOM_DESCRIPTION) {
- result.alternateText = `${lastActorDisplayName} ${Localize.translate(preferredLocale, 'roomChangeLog.updateRoomDescription')} ${
- lastActionOriginalMessage?.description
- }`.trim();
- }
+ } else if (lastActionName === CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.UPDATE_ROOM_DESCRIPTION) {
+ const lastActionOriginalMessage = lastAction?.actionName ? (ReportActionsUtils.getOriginalMessage(lastAction) as OriginalMessageChangeLog | undefined) : null;
+ result.alternateText = `${lastActorDisplayName} ${Localize.translate(preferredLocale, 'roomChangeLog.updateRoomDescription')} ${lastActionOriginalMessage?.description}`.trim();
} else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.LEAVE_POLICY) {
result.alternateText = Localize.translateLocal('workspace.invite.leftWorkspace');
} else if (lastAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && lastActorDisplayName && lastMessageTextFromReport) {
diff --git a/src/libs/TaskUtils.ts b/src/libs/TaskUtils.ts
index bd0bd10cd83e..06745a49217b 100644
--- a/src/libs/TaskUtils.ts
+++ b/src/libs/TaskUtils.ts
@@ -1,12 +1,21 @@
import type {OnyxEntry} from 'react-native-onyx';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
import type {Message} from '@src/types/onyx/ReportAction';
import type ReportAction from '@src/types/onyx/ReportAction';
import * as Localize from './Localize';
+import Navigation from './Navigation/Navigation';
import {getReportActionHtml, getReportActionText} from './ReportActionsUtils';
import * as ReportConnection from './ReportConnection';
+/**
+ * Check if the active route belongs to task edit flow.
+ */
+function isActiveTaskEditRoute(reportID: string): boolean {
+ return [ROUTES.TASK_TITLE, ROUTES.TASK_ASSIGNEE, ROUTES.REPORT_DESCRIPTION].map((route) => route.getRoute(reportID)).some(Navigation.isActiveRoute);
+}
+
/**
* Given the Task reportAction name, return the appropriate message to be displayed and copied to clipboard.
*/
@@ -42,4 +51,4 @@ function getTaskCreatedMessage(reportAction: OnyxEntry) {
return taskTitle ? Localize.translateLocal('task.messages.created', {title: taskTitle}) : '';
}
-export {getTaskReportActionMessage, getTaskTitle, getTaskCreatedMessage};
+export {isActiveTaskEditRoute, getTaskReportActionMessage, getTaskTitle, getTaskCreatedMessage};
diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts
index 5870d642d8cd..8e2fff3868ae 100644
--- a/src/libs/actions/PersonalDetails.ts
+++ b/src/libs/actions/PersonalDetails.ts
@@ -25,7 +25,6 @@ import ROUTES from '@src/ROUTES';
import type {DateOfBirthForm} from '@src/types/form';
import type {PersonalDetails, PersonalDetailsList} from '@src/types/onyx';
import type {SelectedTimezone, Timezone} from '@src/types/onyx/PersonalDetails';
-import * as Session from './Session';
let currentUserEmail = '';
let currentUserAccountID = -1;
@@ -191,10 +190,6 @@ function updateAddress(street: string, street2: string, city: string, state: str
* selected timezone if set to automatically update.
*/
function updateAutomaticTimezone(timezone: Timezone) {
- if (Session.isAnonymousUser()) {
- return;
- }
-
if (!currentUserAccountID) {
return;
}
diff --git a/src/libs/actions/Policy/ReportField.ts b/src/libs/actions/Policy/ReportField.ts
index bccb08c47c18..27b67c9fe686 100644
--- a/src/libs/actions/Policy/ReportField.ts
+++ b/src/libs/actions/Policy/ReportField.ts
@@ -5,8 +5,8 @@ import * as API from '@libs/API';
import type {
CreateWorkspaceReportFieldListValueParams,
CreateWorkspaceReportFieldParams,
+ DeletePolicyReportField,
EnableWorkspaceReportFieldListValueParams,
- PolicyReportFieldsReplace,
RemoveWorkspaceReportFieldListValueParams,
UpdateWorkspaceReportFieldInitialValueParams,
} from '@libs/API/parameters';
@@ -260,7 +260,7 @@ function deleteReportFields(policyID: string, reportFieldsToUpdate: string[]) {
],
};
- const parameters: PolicyReportFieldsReplace = {
+ const parameters: DeletePolicyReportField = {
policyID,
reportFields: JSON.stringify(Object.values(updatedReportFields)),
};
diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts
index 2558969be2f3..9dc0f96c5886 100644
--- a/src/libs/actions/Policy/Tag.ts
+++ b/src/libs/actions/Policy/Tag.ts
@@ -1,4 +1,4 @@
-import type {NullishDeep, OnyxCollection, OnyxEntry} from 'react-native-onyx';
+import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import * as API from '@libs/API';
import type {EnablePolicyTagsParams, OpenPolicyTagsPageParams, RenamePolicyTaglistParams, RenamePolicyTagsParams, SetPolicyTagsEnabled, SetPolicyTagsRequired} from '@libs/API/parameters';
@@ -624,6 +624,9 @@ function renamePolicyTaglist(policyID: string, policyTagListName: {oldName: stri
}
function setPolicyRequiresTag(policyID: string, requiresTag: boolean) {
+ const policyTags = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {};
+ const isMultiLevelTags = PolicyUtils.isMultiLevelTags(policyTags);
+
const onyxData: OnyxData = {
optimisticData: [
{
@@ -667,6 +670,26 @@ function setPolicyRequiresTag(policyID: string, requiresTag: boolean) {
],
};
+ if (isMultiLevelTags) {
+ const getUpdatedTagsData = (required: boolean): OnyxUpdate => ({
+ key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
+ onyxMethod: Onyx.METHOD.MERGE,
+ value: {
+ ...Object.keys(policyTags).reduce((acc, key) => {
+ acc[key] = {
+ ...acc[key],
+ required,
+ };
+ return acc;
+ }, {}),
+ },
+ });
+
+ onyxData.optimisticData?.push(getUpdatedTagsData(requiresTag));
+ onyxData.failureData?.push(getUpdatedTagsData(!requiresTag));
+ onyxData.successData?.push(getUpdatedTagsData(requiresTag));
+ }
+
const parameters = {
policyID,
requiresTag,
diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index 7acc79485f0c..7b3b1abd04ef 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -13,7 +13,6 @@ import type {
SetContactMethodAsDefaultParams,
SetNameValuePairParams,
UpdateChatPriorityModeParams,
- UpdateFrequentlyUsedEmojisParams,
UpdateNewsletterSubscriptionParams,
UpdatePreferredEmojiSkinToneParams,
UpdateStatusParams,
@@ -37,7 +36,7 @@ import Visibility from '@libs/Visibility';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {BlockedFromConcierge, CustomStatusDraft, FrequentlyUsedEmoji, Policy} from '@src/types/onyx';
+import type {BlockedFromConcierge, CustomStatusDraft, Policy} from '@src/types/onyx';
import type Login from '@src/types/onyx/Login';
import type {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer';
import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails';
@@ -655,23 +654,6 @@ function updatePreferredSkinTone(skinTone: number) {
API.write(WRITE_COMMANDS.UPDATE_PREFERRED_EMOJI_SKIN_TONE, parameters, {optimisticData});
}
-/**
- * Sync frequentlyUsedEmojis with Onyx and Server
- */
-function updateFrequentlyUsedEmojis(frequentlyUsedEmojis: FrequentlyUsedEmoji[]) {
- const optimisticData: OnyxUpdate[] = [
- {
- onyxMethod: Onyx.METHOD.SET,
- key: ONYXKEYS.FREQUENTLY_USED_EMOJIS,
- value: frequentlyUsedEmojis,
- },
- ];
-
- const parameters: UpdateFrequentlyUsedEmojisParams = {value: JSON.stringify(frequentlyUsedEmojis)};
-
- API.write(WRITE_COMMANDS.UPDATE_FREQUENTLY_USED_EMOJIS, parameters, {optimisticData});
-}
-
/**
* Sync user chat priority mode with Onyx and Server
* @param mode
@@ -1045,7 +1027,6 @@ export {
setShouldUseStagingServer,
setMuteAllSounds,
clearUserErrorMessage,
- updateFrequentlyUsedEmojis,
joinScreenShare,
clearScreenShareRequest,
generateStatementPDF,
diff --git a/src/pages/EditReportFieldDate.tsx b/src/pages/EditReportFieldDate.tsx
index 38209ba1083b..06ba24f780ec 100644
--- a/src/pages/EditReportFieldDate.tsx
+++ b/src/pages/EditReportFieldDate.tsx
@@ -24,7 +24,7 @@ type EditReportFieldDatePageProps = {
isRequired: boolean;
/** Callback to fire when the Save button is pressed */
- onSubmit: (form: FormOnyxValues) => void;
+ onSubmit: (form: FormOnyxValues) => void;
};
function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, fieldKey}: EditReportFieldDatePageProps) {
@@ -33,8 +33,8 @@ function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, f
const inputRef = useRef(null);
const validate = useCallback(
- (value: FormOnyxValues) => {
- const errors: FormInputErrors = {};
+ (value: FormOnyxValues) => {
+ const errors: FormInputErrors = {};
if (isRequired && value[fieldKey].trim() === '') {
errors[fieldKey] = translate('common.error.fieldRequired');
}
@@ -46,7 +46,7 @@ function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, f
return (
) => {
+ const handleReportFieldChange = (form: FormOnyxValues) => {
const value = form[fieldKey];
if (isReportFieldTitle) {
ReportActions.updateReportName(report.reportID, value, report.reportName ?? '');
diff --git a/src/pages/EditReportFieldText.tsx b/src/pages/EditReportFieldText.tsx
index d619eb52b695..b855acf3e1c0 100644
--- a/src/pages/EditReportFieldText.tsx
+++ b/src/pages/EditReportFieldText.tsx
@@ -24,7 +24,7 @@ type EditReportFieldTextPageProps = {
isRequired: boolean;
/** Callback to fire when the Save button is pressed */
- onSubmit: (form: FormOnyxValues) => void;
+ onSubmit: (form: FormOnyxValues) => void;
};
function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, fieldKey}: EditReportFieldTextPageProps) {
@@ -33,8 +33,8 @@ function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, f
const {inputCallbackRef} = useAutoFocusInput();
const validate = useCallback(
- (values: FormOnyxValues) => {
- const errors: FormInputErrors = {};
+ (values: FormOnyxValues) => {
+ const errors: FormInputErrors = {};
if (isRequired && values[fieldKey].trim() === '') {
errors[fieldKey] = translate('common.error.fieldRequired');
}
@@ -46,7 +46,7 @@ function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, f
return (
diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx
index 662e92658e9d..6491245469a1 100644
--- a/src/pages/home/ReportScreen.tsx
+++ b/src/pages/home/ReportScreen.tsx
@@ -171,6 +171,7 @@ function ReportScreen({
const isLoadingReportOnyx = isLoadingOnyxValue(reportResult);
const permissions = useDeepCompareRef(reportOnyx?.permissions);
+ const lastAccessedReportID = ReportUtils.findLastAccessedReport(!canUseDefaultRooms, !!route.params.openOnAdminRoom, activeWorkspaceID)?.reportID;
useEffect(() => {
// Don't update if there is a reportID in the params already
@@ -183,8 +184,6 @@ function ReportScreen({
return;
}
- const lastAccessedReportID = ReportUtils.findLastAccessedReport(!canUseDefaultRooms, !!route.params.openOnAdminRoom, activeWorkspaceID)?.reportID;
-
// It's possible that reports aren't fully loaded yet
// in that case the reportID is undefined
if (!lastAccessedReportID) {
@@ -193,7 +192,7 @@ function ReportScreen({
Log.info(`[ReportScreen] no reportID found in params, setting it to lastAccessedReportID: ${lastAccessedReportID}`);
navigation.setParams({reportID: lastAccessedReportID});
- }, [activeWorkspaceID, canUseDefaultRooms, navigation, route]);
+ }, [lastAccessedReportID, activeWorkspaceID, canUseDefaultRooms, navigation, route]);
/**
* Create a lightweight Report so as to keep the re-rendering as light as possible by
diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
index 218c382fd776..f5143fc9ba21 100644
--- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
@@ -22,12 +22,14 @@ import Navigation from '@libs/Navigation/Navigation';
import Parser from '@libs/Parser';
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
+import * as ReportConnection from '@libs/ReportConnection';
import * as ReportUtils from '@libs/ReportUtils';
import * as TaskUtils from '@libs/TaskUtils';
import * as Download from '@userActions/Download';
import * as Report from '@userActions/Report';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
+import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Beta, OnyxInputOrEntry, ReportAction, ReportActionReactions, Transaction} from '@src/types/onyx';
import type IconAsset from '@src/types/utils/IconAsset';
@@ -400,6 +402,9 @@ const ContextMenuActions: ContextMenuAction[] = [
} else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) {
const displayMessage = ReportUtils.getIOUSubmittedMessage(reportID);
Clipboard.setString(displayMessage);
+ } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.APPROVED) {
+ const displayMessage = ReportUtils.getIOUApprovedMessage(reportID);
+ Clipboard.setString(displayMessage);
} else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD) {
Clipboard.setString(Localize.translateLocal('iou.heldExpense'));
} else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) {
@@ -524,6 +529,20 @@ const ContextMenuActions: ContextMenuAction[] = [
},
getDescription: () => {},
},
+ {
+ isAnonymousAction: true,
+ textTranslateKey: 'reportActionContextMenu.copyOnyxData',
+ icon: Expensicons.Copy,
+ successTextTranslateKey: 'reportActionContextMenu.copied',
+ successIcon: Expensicons.Checkmark,
+ shouldShow: (type) => type === CONST.CONTEXT_MENU_TYPES.REPORT && (Environment.isDevelopment() || Environment.isStaging() || Environment.isInternalTestBuild()),
+ onPress: (closePopover, {reportID}) => {
+ const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+ Clipboard.setString(JSON.stringify(report, null, 4));
+ hideContextMenu(true, ReportActionComposeFocusManager.focus);
+ },
+ getDescription: () => {},
+ },
{
isAnonymousAction: false,
textTranslateKey: 'reportActionContextMenu.deleteAction',
diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index 6cc55c825983..1df45303694a 100644
--- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -53,7 +53,6 @@ import * as EmojiPickerActions from '@userActions/EmojiPickerAction';
import * as InputFocus from '@userActions/InputFocus';
import * as Modal from '@userActions/Modal';
import * as Report from '@userActions/Report';
-import * as User from '@userActions/User';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type * as OnyxTypes from '@src/types/onyx';
@@ -304,22 +303,12 @@ function ComposerWithSuggestions(
const [composerHeight, setComposerHeight] = useState(0);
const textInputRef = useRef(null);
- const insertedEmojisRef = useRef([]);
const syncSelectionWithOnChangeTextRef = useRef(null);
// The ref to check whether the comment saving is in progress
const isCommentPendingSaved = useRef(false);
- /**
- * Update frequently used emojis list. We debounce this method in the constructor so that UpdateFrequentlyUsedEmojis
- * API is not called too often.
- */
- const debouncedUpdateFrequentlyUsedEmojis = useCallback(() => {
- User.updateFrequentlyUsedEmojis(EmojiUtils.getFrequentlyUsedEmojis(insertedEmojisRef.current));
- insertedEmojisRef.current = [];
- }, []);
-
/**
* Set the TextInput Ref
*/
@@ -421,8 +410,6 @@ function ComposerWithSuggestions(
if (suggestionsRef.current) {
suggestionsRef.current.resetSuggestions();
}
- insertedEmojisRef.current = [...insertedEmojisRef.current, ...newEmojis];
- debouncedUpdateFrequentlyUsedEmojis();
}
}
const newCommentConverted = convertToLTRForComposer(newComment);
@@ -461,18 +448,7 @@ function ComposerWithSuggestions(
debouncedBroadcastUserIsTyping(reportID);
}
},
- [
- debouncedUpdateFrequentlyUsedEmojis,
- findNewlyAddedChars,
- preferredLocale,
- preferredSkinTone,
- reportID,
- setIsCommentEmpty,
- suggestionsRef,
- raiseIsScrollLikelyLayoutTriggered,
- debouncedSaveReportComment,
- selection.end,
- ],
+ [findNewlyAddedChars, preferredLocale, preferredSkinTone, reportID, setIsCommentEmpty, suggestionsRef, raiseIsScrollLikelyLayoutTriggered, debouncedSaveReportComment, selection.end],
);
const prepareCommentAndResetComposer = useCallback((): string => {
diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx
index e732c5793f96..9135e494794e 100644
--- a/src/pages/home/report/ReportActionItem.tsx
+++ b/src/pages/home/report/ReportActionItem.tsx
@@ -621,6 +621,8 @@ function ReportActionItem({
children = ;
} else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) {
children = ;
+ } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.APPROVED) {
+ children = ;
} else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD) {
children = ;
} else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD_COMMENT) {
diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx
index 6618b20a5a6a..cb68410131ce 100644
--- a/src/pages/home/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx
@@ -37,7 +37,6 @@ import * as ComposerActions from '@userActions/Composer';
import * as EmojiPickerAction from '@userActions/EmojiPickerAction';
import * as InputFocus from '@userActions/InputFocus';
import * as Report from '@userActions/Report';
-import * as User from '@userActions/User';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type * as OnyxTypes from '@src/types/onyx';
@@ -105,7 +104,6 @@ function ReportActionItemMessageEdit(
const textInputRef = useRef<(HTMLTextAreaElement & TextInput) | null>(null);
const isFocusedRef = useRef(false);
- const insertedEmojis = useRef([]);
const draftRef = useRef(draft);
const emojiPickerSelectionRef = useRef(undefined);
// The ref to check whether the comment saving is in progress
@@ -214,19 +212,6 @@ function ReportActionItemMessageEdit(
[debouncedSaveDraft],
);
- /**
- * Update frequently used emojis list. We debounce this method in the constructor so that UpdateFrequentlyUsedEmojis
- * API is not called too often.
- */
- const debouncedUpdateFrequentlyUsedEmojis = useMemo(
- () =>
- lodashDebounce(() => {
- User.updateFrequentlyUsedEmojis(EmojiUtils.getFrequentlyUsedEmojis(insertedEmojis.current));
- insertedEmojis.current = [];
- }, 1000),
- [],
- );
-
/**
* Update the value of the draft in Onyx
*
@@ -236,13 +221,6 @@ function ReportActionItemMessageEdit(
(newDraftInput: string) => {
const {text: newDraft, emojis, cursorPosition} = EmojiUtils.replaceAndExtractEmojis(newDraftInput, preferredSkinTone, preferredLocale);
- if (emojis?.length > 0) {
- const newEmojis = EmojiUtils.getAddedEmojis(emojis, emojisPresentBefore.current);
- if (newEmojis?.length > 0) {
- insertedEmojis.current = [...insertedEmojis.current, ...newEmojis];
- debouncedUpdateFrequentlyUsedEmojis();
- }
- }
emojisPresentBefore.current = emojis;
setDraft(newDraft);
@@ -261,7 +239,7 @@ function ReportActionItemMessageEdit(
debouncedSaveDraft(newDraft);
isCommentPendingSaved.current = true;
},
- [debouncedSaveDraft, debouncedUpdateFrequentlyUsedEmojis, preferredSkinTone, preferredLocale, selection.end],
+ [debouncedSaveDraft, preferredSkinTone, preferredLocale, selection.end],
);
useEffect(() => {
diff --git a/src/pages/iou/request/step/IOURequestStepDistance.tsx b/src/pages/iou/request/step/IOURequestStepDistance.tsx
index 05b228c73c76..9235c3293a6d 100644
--- a/src/pages/iou/request/step/IOURequestStepDistance.tsx
+++ b/src/pages/iou/request/step/IOURequestStepDistance.tsx
@@ -90,7 +90,8 @@ function IOURequestStepDistance({
const previousValidatedWaypoints = usePrevious(validatedWaypoints);
const haveValidatedWaypointsChanged = !isEqual(previousValidatedWaypoints, validatedWaypoints);
const isRouteAbsentWithoutErrors = !hasRoute && !hasRouteError;
- const shouldFetchRoute = (isRouteAbsentWithoutErrors || haveValidatedWaypointsChanged) && !isLoadingRoute && Object.keys(validatedWaypoints).length > 1;
+ const isEmptyCoordinates = !transaction?.routes?.route0?.geometry?.coordinates?.length;
+ const shouldFetchRoute = (isEmptyCoordinates || isRouteAbsentWithoutErrors || haveValidatedWaypointsChanged) && !isLoadingRoute && Object.keys(validatedWaypoints).length > 1;
const [shouldShowAtLeastTwoDifferentWaypointsError, setShouldShowAtLeastTwoDifferentWaypointsError] = useState(false);
const nonEmptyWaypointsCount = useMemo(() => Object.keys(waypoints).filter((key) => !isEmpty(waypoints[key])).length, [waypoints]);
const duplicateWaypointsError = useMemo(
diff --git a/src/pages/signin/SignInPage.tsx b/src/pages/signin/SignInPage.tsx
index 0b4310cce337..024d5a7c5610 100644
--- a/src/pages/signin/SignInPage.tsx
+++ b/src/pages/signin/SignInPage.tsx
@@ -266,6 +266,7 @@ function SignInPage({credentials, account, activeClients = [], preferredLocale,
diff --git a/src/pages/workspace/accounting/ReconciliationAccountSettingsPage.tsx b/src/pages/workspace/accounting/ReconciliationAccountSettingsPage.tsx
new file mode 100644
index 000000000000..7f0e45678471
--- /dev/null
+++ b/src/pages/workspace/accounting/ReconciliationAccountSettingsPage.tsx
@@ -0,0 +1,81 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useMemo} from 'react';
+import {useOnyx} from 'react-native-onyx';
+import ConnectionLayout from '@components/ConnectionLayout';
+import SelectionList from '@components/SelectionList';
+import RadioListItem from '@components/SelectionList/RadioListItem';
+import Text from '@components/Text';
+import TextLink from '@components/TextLink';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import type {SettingsNavigatorParamList} from '@navigation/types';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type SCREENS from '@src/SCREENS';
+
+type ReconciliationAccountSettingsPageProps = StackScreenProps;
+
+function ReconciliationAccountSettingsPage({route}: ReconciliationAccountSettingsPageProps) {
+ const {policyID, connection} = route.params;
+ const settlementAccountEnding = '1234'; // TODO: use correct settlement account ending value https://github.com/Expensify/App/issues/44313
+
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
+ const [reconciliationConnection] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION}${policyID}`);
+
+ const selectedBankAccount = useMemo(() => reconciliationConnection ?? Object.values(bankAccountList ?? {})[0], [reconciliationConnection, bankAccountList]);
+
+ const sections = useMemo(() => {
+ const data = Object.values(bankAccountList ?? {}).map((bankAccount) => ({
+ text: bankAccount.title,
+ value: bankAccount.accountData?.bankAccountID,
+ keyForList: bankAccount.accountData?.bankAccountID?.toString(),
+ isSelected: bankAccount.accountData?.bankAccountID === selectedBankAccount?.accountData?.bankAccountID,
+ }));
+ return [{data}];
+ }, [bankAccountList, selectedBankAccount]);
+
+ const selectBankAccount = () => {
+ // TODO: add API call when it's implemented https://github.com/Expensify/Expensify/issues/407836
+ // Navigation.goBack();
+ };
+
+ return (
+
+ {translate('workspace.accounting.chooseReconciliationAccount.chooseBankAccount')}
+
+ {translate('workspace.accounting.chooseReconciliationAccount.accountMatches')}
+ {
+ // TODO: navigate to Settlement Account https://github.com/Expensify/App/issues/44313
+ }}
+ >
+ {translate('workspace.accounting.chooseReconciliationAccount.settlementAccount')}
+
+ {translate('workspace.accounting.chooseReconciliationAccount.reconciliationWorks', settlementAccountEnding)}
+
+
+
+
+ );
+}
+
+ReconciliationAccountSettingsPage.displayName = 'ReconciliationAccountSettingsPage';
+
+export default ReconciliationAccountSettingsPage;
diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
index 633d1833e43f..174251a80d5f 100644
--- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
+++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
@@ -7,6 +7,7 @@ import Button from '@components/Button';
import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
import ConfirmModal from '@components/ConfirmModal';
+import EmptyStateComponent from '@components/EmptyStateComponent';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
@@ -15,9 +16,9 @@ import SelectionList from '@components/SelectionList';
import ListItemRightCaretWithLabel from '@components/SelectionList/ListItemRightCaretWithLabel';
import TableListItem from '@components/SelectionList/TableListItem';
import type {ListItem} from '@components/SelectionList/types';
+import TableListItemSkeleton from '@components/Skeletons/TableRowSkeleton';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
-import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection';
import useEnvironment from '@hooks/useEnvironment';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
@@ -317,10 +318,15 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
color={theme.spinner}
/>
)}
+
{!hasVisibleCategories && !isLoading && (
-
)}
diff --git a/src/pages/workspace/reportFields/CreateReportFieldPage.tsx b/src/pages/workspace/reportFields/CreateReportFieldsPage.tsx
similarity index 95%
rename from src/pages/workspace/reportFields/CreateReportFieldPage.tsx
rename to src/pages/workspace/reportFields/CreateReportFieldsPage.tsx
index aaf3877f9490..4a02e701b058 100644
--- a/src/pages/workspace/reportFields/CreateReportFieldPage.tsx
+++ b/src/pages/workspace/reportFields/CreateReportFieldsPage.tsx
@@ -28,16 +28,16 @@ import INPUT_IDS from '@src/types/form/WorkspaceReportFieldForm';
import InitialListValueSelector from './InitialListValueSelector';
import TypeSelector from './TypeSelector';
-type CreateReportFieldPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps;
+type CreateReportFieldsPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps;
const defaultDate = DateUtils.extractDate(new Date().toString());
-function CreateReportFieldPage({
+function CreateReportFieldsPage({
policy,
route: {
params: {policyID},
},
-}: CreateReportFieldPageProps) {
+}: CreateReportFieldsPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const formRef = useRef(null);
@@ -101,7 +101,7 @@ function CreateReportFieldPage({
Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELD_LIST_VALUES.getRoute(policyID))}
+ onPress={() => Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELDS_LIST_VALUES.getRoute(policyID))}
/>
)}
@@ -190,6 +190,6 @@ function CreateReportFieldPage({
);
}
-CreateReportFieldPage.displayName = 'CreateReportFieldPage';
+CreateReportFieldsPage.displayName = 'CreateReportFieldsPage';
-export default withPolicyAndFullscreenLoading(CreateReportFieldPage);
+export default withPolicyAndFullscreenLoading(CreateReportFieldsPage);
diff --git a/src/pages/workspace/reportFields/ReportFieldAddListValuePage.tsx b/src/pages/workspace/reportFields/ReportFieldsAddListValuePage.tsx
similarity index 90%
rename from src/pages/workspace/reportFields/ReportFieldAddListValuePage.tsx
rename to src/pages/workspace/reportFields/ReportFieldsAddListValuePage.tsx
index 576dd1a6f6b0..a0969160365e 100644
--- a/src/pages/workspace/reportFields/ReportFieldAddListValuePage.tsx
+++ b/src/pages/workspace/reportFields/ReportFieldsAddListValuePage.tsx
@@ -21,13 +21,13 @@ import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
import INPUT_IDS from '@src/types/form/WorkspaceReportFieldForm';
-type ReportFieldAddListValuePageProps = StackScreenProps;
+type ReportFieldsAddListValuePageProps = StackScreenProps;
-function ReportFieldAddListValuePage({
+function ReportFieldsAddListValuePage({
route: {
params: {policyID, reportFieldID},
},
-}: ReportFieldAddListValuePageProps) {
+}: ReportFieldsAddListValuePageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {inputCallbackRef} = useAutoFocusInput();
@@ -61,7 +61,7 @@ function ReportFieldAddListValuePage({
;
+type ReportFieldsEditValuePageProps = StackScreenProps;
-function ReportFieldEditValuePage({
+function ReportFieldsEditValuePage({
route: {
params: {policyID, valueIndex},
},
-}: ReportFieldEditValuePageProps) {
+}: ReportFieldsEditValuePageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {inputCallbackRef} = useAutoFocusInput();
@@ -67,7 +67,7 @@ function ReportFieldEditValuePage({
;
-function ReportFieldInitialValuePage({
+type ReportFieldsInitialValuePageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps;
+function ReportFieldsInitialValuePage({
policy,
route: {
params: {policyID, reportFieldID},
},
-}: ReportFieldInitialValuePagePageProps) {
+}: ReportFieldsInitialValuePageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {inputCallbackRef} = useAutoFocusInput();
@@ -98,7 +98,7 @@ function ReportFieldInitialValuePage({
;
+type ReportFieldsListValuesPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps;
-function ReportFieldListValuesPage({
+function ReportFieldsListValuesPage({
policy,
route: {
params: {policyID, reportFieldID},
},
-}: ReportFieldListValuesPageProps) {
+}: ReportFieldsListValuesPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {isSmallScreenWidth} = useWindowDimensions();
@@ -140,7 +140,7 @@ function ReportFieldListValuesPage({
return;
}
- Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELD_VALUE_SETTINGS.getRoute(policyID, valueItem.index, reportFieldID));
+ Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELDS_VALUE_SETTINGS.getRoute(policyID, valueItem.index, reportFieldID));
setSelectedValues({});
};
@@ -248,7 +248,7 @@ function ReportFieldListValuesPage({
success
icon={Expensicons.Plus}
text={translate('workspace.reportFields.addValue')}
- onPress={() => Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELD_ADD_VALUE.getRoute(policyID, reportFieldID))}
+ onPress={() => Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELDS_ADD_VALUE.getRoute(policyID, reportFieldID))}
/>
);
};
@@ -262,7 +262,7 @@ function ReportFieldListValuesPage({
;
+type ReportFieldsSettingsPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps;
-function ReportFieldSettingsPage({
+function ReportFieldsSettingsPage({
policy,
route: {
params: {policyID, reportFieldID},
},
-}: ReportFieldSettingsPageProps) {
+}: ReportFieldsSettingsPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
@@ -60,7 +60,7 @@ function ReportFieldSettingsPage({
Navigation.navigate(ROUTES.WORKSPACE_EDIT_REPORT_FIELD_INITIAL_VALUE.getRoute(policyID, reportFieldID))}
+ onPress={() => Navigation.navigate(ROUTES.WORKSPACE_EDIT_REPORT_FIELDS_INITIAL_VALUE.getRoute(policyID, reportFieldID))}
/>
{isListFieldType && (
Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELD_LIST_VALUES.getRoute(policyID, reportFieldID))}
+ onPress={() => Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELDS_LIST_VALUES.getRoute(policyID, reportFieldID))}
/>
)}
@@ -121,6 +121,6 @@ function ReportFieldSettingsPage({
);
}
-ReportFieldSettingsPage.displayName = 'ReportFieldSettingsPage';
+ReportFieldsSettingsPage.displayName = 'ReportFieldsSettingsPage';
-export default withPolicyAndFullscreenLoading(ReportFieldSettingsPage);
+export default withPolicyAndFullscreenLoading(ReportFieldsSettingsPage);
diff --git a/src/pages/workspace/reportFields/ReportFieldValueSettingsPage.tsx b/src/pages/workspace/reportFields/ReportFieldsValueSettingsPage.tsx
similarity index 91%
rename from src/pages/workspace/reportFields/ReportFieldValueSettingsPage.tsx
rename to src/pages/workspace/reportFields/ReportFieldsValueSettingsPage.tsx
index f99842fdf5a7..209be2be26b1 100644
--- a/src/pages/workspace/reportFields/ReportFieldValueSettingsPage.tsx
+++ b/src/pages/workspace/reportFields/ReportFieldsValueSettingsPage.tsx
@@ -25,14 +25,14 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
-type ReportFieldValueSettingsPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps;
+type ReportFieldsValueSettingsPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps;
-function ReportFieldValueSettingsPage({
+function ReportFieldsValueSettingsPage({
policy,
route: {
params: {policyID, valueIndex, reportFieldID},
},
-}: ReportFieldValueSettingsPageProps) {
+}: ReportFieldsValueSettingsPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const [formDraft] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT);
@@ -81,7 +81,7 @@ function ReportFieldValueSettingsPage({
};
const navigateToEditValue = () => {
- Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELD_EDIT_VALUE.getRoute(policyID, valueIndex));
+ Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELDS_EDIT_VALUE.getRoute(policyID, valueIndex));
};
return (
@@ -93,7 +93,7 @@ function ReportFieldValueSettingsPage({
{
- Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELD_SETTINGS.getRoute(policyID, reportField.fieldID));
+ const navigateToReportFieldsSettings = (reportField: ReportFieldForList) => {
+ Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELDS_SETTINGS.getRoute(policyID, reportField.fieldID));
};
const handleDeleteReportFields = () => {
@@ -232,7 +232,7 @@ function WorkspaceReportFieldsPage({
canSelectMultiple
sections={reportFieldsSections}
onCheckboxPress={updateSelectedReportFields}
- onSelectRow={navigateToReportFieldSettings}
+ onSelectRow={navigateToReportFieldsSettings}
onSelectAll={toggleAllReportFields}
ListItem={TableListItem}
customListHeader={getCustomListHeader()}
diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx
index 92b016766742..cf9952720fc9 100644
--- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx
+++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx
@@ -7,6 +7,7 @@ import Button from '@components/Button';
import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
import ConfirmModal from '@components/ConfirmModal';
+import EmptyStateComponent from '@components/EmptyStateComponent';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
@@ -14,9 +15,9 @@ import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import ListItemRightCaretWithLabel from '@components/SelectionList/ListItemRightCaretWithLabel';
import TableListItem from '@components/SelectionList/TableListItem';
+import TableListItemSkeleton from '@components/Skeletons/TableRowSkeleton';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
-import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection';
import useEnvironment from '@hooks/useEnvironment';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
@@ -343,9 +344,13 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
/>
)}
{!hasVisibleTag && !isLoading && (
-
)}
diff --git a/src/styles/index.ts b/src/styles/index.ts
index a0795e5d378a..e422b8bd3d1e 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -5083,6 +5083,59 @@ const styles = (theme: ThemeColors) =>
fontSize: variables.fontSizeNormal,
fontWeight: FontUtils.fontWeight.bold,
},
+
+ skeletonBackground: {
+ flex: 1,
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ width: '100%',
+ height: '100%',
+ paddingRight: 8,
+ paddingLeft: 8,
+ },
+
+ emptyStateScrollView: {
+ minHeight: 400,
+ height: '100%',
+ flex: 1,
+ },
+
+ emptyStateForeground: (isSmallScreenWidth: boolean) => ({
+ justifyContent: 'center',
+ alignItems: 'center',
+ height: '100%',
+ padding: isSmallScreenWidth ? 32 : 0,
+ width: '100%',
+ }),
+
+ emptyStateContent: {
+ backgroundColor: theme.cardBG,
+ borderRadius: variables.componentBorderRadiusLarge,
+ maxWidth: 400,
+ },
+
+ emptyStateHeader: (isIllustration: boolean) => ({
+ borderTopLeftRadius: variables.componentBorderRadiusLarge,
+ borderTopRightRadius: variables.componentBorderRadiusLarge,
+ minHeight: 200,
+ alignItems: isIllustration ? 'center' : undefined,
+ justifyContent: isIllustration ? 'center' : undefined,
+ }),
+
+ emptyFolderBG: {
+ backgroundColor: theme.emptyFolderBG,
+ },
+
+ emptyStateVideo: {
+ borderTopLeftRadius: variables.componentBorderRadiusLarge,
+ borderTopRightRadius: variables.componentBorderRadiusLarge,
+ },
+
+ emptyStateFolderIconSize: {
+ width: 184,
+ height: 112,
+ },
} satisfies Styles);
type ThemeStyles = ReturnType;
diff --git a/src/styles/theme/themes/dark.ts b/src/styles/theme/themes/dark.ts
index b316f116c805..7ed23c7c0991 100644
--- a/src/styles/theme/themes/dark.ts
+++ b/src/styles/theme/themes/dark.ts
@@ -93,6 +93,7 @@ const darkTheme = {
white: colors.white,
videoPlayerBG: `${colors.productDark100}cc`,
transparentWhite: `${colors.white}51`,
+ emptyFolderBG: colors.yellow600,
// Adding a color here will animate the status bar to the right color when the screen is opened.
// Note that it needs to be a screen name, not a route url.
diff --git a/src/styles/theme/themes/light.ts b/src/styles/theme/themes/light.ts
index 05364515e264..2ebb558ee20b 100644
--- a/src/styles/theme/themes/light.ts
+++ b/src/styles/theme/themes/light.ts
@@ -93,6 +93,7 @@ const lightTheme = {
white: colors.white,
videoPlayerBG: `${colors.productDark100}cc`,
transparentWhite: `${colors.white}51`,
+ emptyFolderBG: colors.yellow600,
// Adding a color here will animate the status bar to the right color when the screen is opened.
// Note that it needs to be a screen name, not a route url.
diff --git a/src/styles/theme/types.ts b/src/styles/theme/types.ts
index 2d8618c24ebb..ffa42e99777d 100644
--- a/src/styles/theme/types.ts
+++ b/src/styles/theme/types.ts
@@ -97,6 +97,7 @@ type ThemeColors = {
white: Color;
videoPlayerBG: Color;
transparentWhite: Color;
+ emptyFolderBG: Color;
PAGE_THEMES: Record;
diff --git a/src/types/form/ReportFieldEditForm.ts b/src/types/form/ReportFieldEditForm.ts
deleted file mode 100644
index 7befa2cb6502..000000000000
--- a/src/types/form/ReportFieldEditForm.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import type Form from './Form';
-
-type ReportFieldEditForm = Form>;
-
-// eslint-disable-next-line import/prefer-default-export
-export type {ReportFieldEditForm};
diff --git a/src/types/form/ReportFieldsEditForm.ts b/src/types/form/ReportFieldsEditForm.ts
new file mode 100644
index 000000000000..8315490d31c7
--- /dev/null
+++ b/src/types/form/ReportFieldsEditForm.ts
@@ -0,0 +1,6 @@
+import type Form from './Form';
+
+type ReportFieldsEditForm = Form>;
+
+// eslint-disable-next-line import/prefer-default-export
+export type {ReportFieldsEditForm};
diff --git a/src/types/form/index.ts b/src/types/form/index.ts
index dbfc6e5095f6..450de82a60cf 100644
--- a/src/types/form/index.ts
+++ b/src/types/form/index.ts
@@ -25,7 +25,7 @@ export type {PrivateNotesForm} from './PrivateNotesForm';
export type {ProfileSettingsForm} from './ProfileSettingsForm';
export type {ReimbursementAccountForm} from './ReimbursementAccountForm';
export type {ReportDescriptionForm} from './ReportDescriptionForm';
-export type {ReportFieldEditForm} from './ReportFieldEditForm';
+export type {ReportFieldsEditForm} from './ReportFieldsEditForm';
export type {ReportVirtualCardFraudForm} from './ReportVirtualCardFraudForm';
export type {RequestPhysicalCardForm} from './RequestPhysicalCardForm';
export type {RoomNameForm} from './RoomNameForm';
diff --git a/tests/e2e/config.ts b/tests/e2e/config.ts
index 8963e07c31c8..9241463f6a0c 100644
--- a/tests/e2e/config.ts
+++ b/tests/e2e/config.ts
@@ -8,6 +8,7 @@ const TEST_NAMES = {
ReportTyping: 'Report typing',
ChatOpening: 'Chat opening',
Linking: 'Linking',
+ PreloadedLinking: 'Preloaded linking',
};
/**
@@ -89,6 +90,7 @@ export default {
// #announce Chat with many messages
reportID: '5421294415618529',
},
+ // linking from chat A to a specific message in chat B
[TEST_NAMES.Linking]: {
name: TEST_NAMES.Linking,
reportScreen: {
@@ -99,6 +101,16 @@ export default {
linkedReportID: '5421294415618529',
linkedReportActionID: '2845024374735019929',
},
+ // linking from chat A to a specific message in the same chat A
+ [TEST_NAMES.PreloadedLinking]: {
+ name: TEST_NAMES.PreloadedLinking,
+ reportScreen: {
+ autoFocus: true,
+ },
+ // Crowded Policy (Do Not Delete) Report, has a input bar available:
+ reportID: '5421294415618529',
+ linkedReportActionID: '8984197495983183608', // Message 4897
+ },
},
};
diff --git a/tests/unit/CurrencyUtilsTest.ts b/tests/unit/CurrencyUtilsTest.ts
index 5322faff763f..9a86c9188d42 100644
--- a/tests/unit/CurrencyUtilsTest.ts
+++ b/tests/unit/CurrencyUtilsTest.ts
@@ -155,8 +155,8 @@ describe('CurrencyUtils', () => {
['JPY', 2500.5, '¥25'],
['RSD', 100, 'RSD\xa01.00'],
['RSD', 145, 'RSD\xa01.45'],
- ['BHD', 12345, 'BHD\xa0123.450'],
- ['BHD', 1, 'BHD\xa00.010'],
+ ['BHD', 12345, 'BHD\xa0123.45'],
+ ['BHD', 1, 'BHD\xa00.01'],
])('Correctly displays %s', (currency, amount, expectedResult) => {
expect(CurrencyUtils.convertToDisplayString(amount, currency)).toBe(expectedResult);
});
diff --git a/tests/unit/EmojiTest.ts b/tests/unit/EmojiTest.ts
index 968e543363f2..84442db92553 100644
--- a/tests/unit/EmojiTest.ts
+++ b/tests/unit/EmojiTest.ts
@@ -1,15 +1,7 @@
-import {getUnixTime} from 'date-fns';
-import Onyx from 'react-native-onyx';
import Emojis, {importEmojiLocale} from '@assets/emojis';
import type {Emoji} from '@assets/emojis/types';
-import * as User from '@libs/actions/User';
import {buildEmojisTrie} from '@libs/EmojiTrie';
import * as EmojiUtils from '@libs/EmojiUtils';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import type {FrequentlyUsedEmoji} from '@src/types/onyx';
-import * as TestHelper from '../utils/TestHelper';
-import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
describe('EmojiTest', () => {
beforeAll(async () => {
@@ -196,327 +188,4 @@ describe('EmojiTest', () => {
},
]);
});
-
- describe('update frequently used emojis', () => {
- let spy: jest.SpyInstance;
-
- beforeAll(() => {
- Onyx.init({keys: ONYXKEYS});
- global.fetch = TestHelper.getGlobalFetchMock();
- spy = jest.spyOn(User, 'updateFrequentlyUsedEmojis');
- });
-
- beforeEach(() => {
- spy.mockClear();
- return Onyx.clear();
- });
-
- it('should put a less frequent and recent used emoji behind', () => {
- // Given an existing frequently used emojis list with count > 1
- const frequentlyEmojisList: FrequentlyUsedEmoji[] = [
- {
- code: '👋',
- name: 'wave',
- count: 2,
- lastUpdatedAt: 4,
- types: ['👋🏿', '👋🏾', '👋🏽', '👋🏼', '👋🏻'],
- },
- {
- code: '💤',
- name: 'zzz',
- count: 2,
- lastUpdatedAt: 3,
- },
- {
- code: '💯',
- name: '100',
- count: 2,
- lastUpdatedAt: 2,
- },
- {
- code: '👿',
- name: 'imp',
- count: 2,
- lastUpdatedAt: 1,
- },
- ];
- Onyx.merge(ONYXKEYS.FREQUENTLY_USED_EMOJIS, frequentlyEmojisList);
-
- return waitForBatchedUpdates().then(() => {
- // When add a new emoji
- const currentTime = getUnixTime(new Date());
- const smileEmoji: Emoji = {code: '😄', name: 'smile'};
- const newEmoji = [smileEmoji];
- User.updateFrequentlyUsedEmojis(EmojiUtils.getFrequentlyUsedEmojis(newEmoji));
-
- // Then the new emoji should be at the last item of the list
- const expectedSmileEmoji: FrequentlyUsedEmoji = {...smileEmoji, count: 1, lastUpdatedAt: currentTime};
-
- const expectedFrequentlyEmojisList = [...frequentlyEmojisList, expectedSmileEmoji];
- expect(spy).toBeCalledWith(expectedFrequentlyEmojisList);
- });
- });
-
- it('should put more frequent and recent used emoji to the front', () => {
- // Given an existing frequently used emojis list
- const smileEmoji: Emoji = {code: '😄', name: 'smile'};
- const frequentlyEmojisList: FrequentlyUsedEmoji[] = [
- {
- code: '😠',
- name: 'angry',
- count: 3,
- lastUpdatedAt: 5,
- },
- {
- code: '👋',
- name: 'wave',
- count: 2,
- lastUpdatedAt: 4,
- types: ['👋🏿', '👋🏾', '👋🏽', '👋🏼', '👋🏻'],
- },
- {
- code: '💤',
- name: 'zzz',
- count: 2,
- lastUpdatedAt: 3,
- },
- {
- code: '💯',
- name: '100',
- count: 1,
- lastUpdatedAt: 2,
- },
- {...smileEmoji, count: 1, lastUpdatedAt: 1},
- ];
- Onyx.merge(ONYXKEYS.FREQUENTLY_USED_EMOJIS, frequentlyEmojisList);
-
- return waitForBatchedUpdates().then(() => {
- // When add an emoji that exists in the list
- const currentTime = getUnixTime(new Date());
- const newEmoji = [smileEmoji];
- User.updateFrequentlyUsedEmojis(EmojiUtils.getFrequentlyUsedEmojis(newEmoji));
-
- // Then the count should be increased and put into the very front of the other emoji within the same count
- const expectedFrequentlyEmojisList = [frequentlyEmojisList[0], {...smileEmoji, count: 2, lastUpdatedAt: currentTime}, ...frequentlyEmojisList.slice(1, -1)];
- expect(spy).toBeCalledWith(expectedFrequentlyEmojisList);
- });
- });
-
- it('should sorted descending by count and lastUpdatedAt for multiple emoji added', () => {
- // Given an existing frequently used emojis list
- const smileEmoji: Emoji = {code: '😄', name: 'smile'};
- const zzzEmoji: Emoji = {code: '💤', name: 'zzz'};
- const impEmoji: Emoji = {code: '👿', name: 'imp'};
- const frequentlyEmojisList: FrequentlyUsedEmoji[] = [
- {
- code: '😠',
- name: 'angry',
- count: 3,
- lastUpdatedAt: 5,
- },
- {
- code: '👋',
- name: 'wave',
- count: 2,
- lastUpdatedAt: 4,
- types: ['👋🏿', '👋🏾', '👋🏽', '👋🏼', '👋🏻'],
- },
- {...zzzEmoji, count: 2, lastUpdatedAt: 3},
- {
- code: '💯',
- name: '100',
- count: 1,
- lastUpdatedAt: 2,
- },
- {...smileEmoji, count: 1, lastUpdatedAt: 1},
- ];
- Onyx.merge(ONYXKEYS.FREQUENTLY_USED_EMOJIS, frequentlyEmojisList);
-
- return waitForBatchedUpdates().then(() => {
- // When add multiple emojis that either exist or not exist in the list
- const currentTime = getUnixTime(new Date());
- const newEmoji = [smileEmoji, zzzEmoji, impEmoji];
- User.updateFrequentlyUsedEmojis(EmojiUtils.getFrequentlyUsedEmojis(newEmoji));
-
- // Then the count should be increased for existing emoji and sorted descending by count and lastUpdatedAt
- const expectedFrequentlyEmojisList = [
- {...zzzEmoji, count: 3, lastUpdatedAt: currentTime},
- frequentlyEmojisList[0],
- {...smileEmoji, count: 2, lastUpdatedAt: currentTime},
- frequentlyEmojisList[1],
- {...impEmoji, count: 1, lastUpdatedAt: currentTime},
- frequentlyEmojisList[3],
- ];
- expect(spy).toBeCalledWith(expectedFrequentlyEmojisList);
- });
- });
-
- it('make sure the most recent new emoji is added to the list even it is full with count > 1', () => {
- // Given an existing full (24 items) frequently used emojis list
- const smileEmoji: Emoji = {code: '😄', name: 'smile'};
- const zzzEmoji: Emoji = {code: '💤', name: 'zzz'};
- const impEmoji: Emoji = {code: '👿', name: 'imp'};
- const bookEmoji: Emoji = {code: '📚', name: 'books'};
- const frequentlyEmojisList: FrequentlyUsedEmoji[] = [
- {
- code: '😠',
- name: 'angry',
- count: 3,
- lastUpdatedAt: 24,
- },
- {
- code: '👋',
- name: 'wave',
- count: 3,
- lastUpdatedAt: 23,
- types: ['👋🏿', '👋🏾', '👋🏽', '👋🏼', '👋🏻'],
- },
- {
- code: '😡',
- name: 'rage',
- count: 3,
- lastUpdatedAt: 22,
- },
- {
- code: '😤',
- name: 'triumph',
- count: 3,
- lastUpdatedAt: 21,
- },
- {
- code: '🥱',
- name: 'yawning_face',
- count: 3,
- lastUpdatedAt: 20,
- },
- {
- code: '😫',
- name: 'tired_face',
- count: 3,
- lastUpdatedAt: 19,
- },
- {
- code: '😩',
- name: 'weary',
- count: 3,
- lastUpdatedAt: 18,
- },
- {
- code: '😓',
- name: 'sweat',
- count: 3,
- lastUpdatedAt: 17,
- },
- {
- code: '😞',
- name: 'disappointed',
- count: 3,
- lastUpdatedAt: 16,
- },
- {
- code: '😣',
- name: 'persevere',
- count: 3,
- lastUpdatedAt: 15,
- },
- {
- code: '😖',
- name: 'confounded',
- count: 3,
- lastUpdatedAt: 14,
- },
- {
- code: '👶',
- name: 'baby',
- count: 3,
- lastUpdatedAt: 13,
- types: ['👶🏿', '👶🏾', '👶🏽', '👶🏼', '👶🏻'],
- },
- {
- code: '👄',
- name: 'lips',
- count: 3,
- lastUpdatedAt: 12,
- },
- {
- code: '🐶',
- name: 'dog',
- count: 3,
- lastUpdatedAt: 11,
- },
- {
- code: '🦮',
- name: 'guide_dog',
- count: 3,
- lastUpdatedAt: 10,
- },
- {
- code: '🐱',
- name: 'cat',
- count: 3,
- lastUpdatedAt: 9,
- },
- {
- code: '🐈⬛',
- name: 'black_cat',
- count: 3,
- lastUpdatedAt: 8,
- },
- {
- code: '🕞',
- name: 'clock330',
- count: 3,
- lastUpdatedAt: 7,
- },
- {
- code: '🥎',
- name: 'softball',
- count: 3,
- lastUpdatedAt: 6,
- },
- {
- code: '🏀',
- name: 'basketball',
- count: 3,
- lastUpdatedAt: 5,
- },
- {
- code: '📟',
- name: 'pager',
- count: 3,
- lastUpdatedAt: 4,
- },
- {
- code: '🎬',
- name: 'clapper',
- count: 3,
- lastUpdatedAt: 3,
- },
- {
- code: '📺',
- name: 'tv',
- count: 3,
- lastUpdatedAt: 2,
- },
- {...bookEmoji, count: 3, lastUpdatedAt: 1},
- ];
- expect(frequentlyEmojisList.length).toBe(CONST.EMOJI_FREQUENT_ROW_COUNT * CONST.EMOJI_NUM_PER_ROW);
- Onyx.merge(ONYXKEYS.FREQUENTLY_USED_EMOJIS, frequentlyEmojisList);
-
- return waitForBatchedUpdates().then(() => {
- // When add new emojis
- const currentTime = getUnixTime(new Date());
- const newEmoji = [bookEmoji, smileEmoji, zzzEmoji, impEmoji, smileEmoji];
- User.updateFrequentlyUsedEmojis(EmojiUtils.getFrequentlyUsedEmojis(newEmoji));
-
- // Then the last emojis from the list should be replaced with the most recent new emoji (smile)
- const expectedFrequentlyEmojisList = [
- {...bookEmoji, count: 4, lastUpdatedAt: currentTime},
- ...frequentlyEmojisList.slice(0, -2),
- {...smileEmoji, count: 1, lastUpdatedAt: currentTime},
- ];
- expect(spy).toBeCalledWith(expectedFrequentlyEmojisList);
- });
- });
- });
});
diff --git a/tests/unit/SidebarFilterTest.ts b/tests/unit/SidebarFilterTest.ts
index c54c2cae54f8..1e0745916717 100644
--- a/tests/unit/SidebarFilterTest.ts
+++ b/tests/unit/SidebarFilterTest.ts
@@ -280,6 +280,35 @@ xdescribe('Sidebar', () => {
);
});
+ it('filter paycheck and bill report', () => {
+ const report1: Report = {
+ ...LHNTestUtils.getFakeReport(),
+ type: CONST.REPORT.TYPE.PAYCHECK,
+ };
+ const report2: Report = {
+ ...LHNTestUtils.getFakeReport(),
+ type: CONST.REPORT.TYPE.BILL,
+ };
+ const report3: Report = LHNTestUtils.getFakeReport();
+ LHNTestUtils.getDefaultRenderedSidebarLinks(report1.reportID);
+ const reportCollectionDataSet: ReportCollectionDataSet = {
+ [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1,
+ [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2,
+ [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3,
+ };
+ return (
+ waitForBatchedUpdates()
+ .then(() => Onyx.multiSet(reportCollectionDataSet))
+
+ // Then the reports 1 and 2 are hidden and 3 is not
+ .then(() => {
+ const hintText = Localize.translateLocal('accessibilityHints.navigatesToChat');
+ const optionRows = screen.queryAllByAccessibilityHint(hintText);
+ expect(optionRows).toHaveLength(1);
+ })
+ );
+ });
+
// NOTE: This is also for #focus mode, should we move this test block?
describe('all combinations of isArchived, isUserCreatedPolicyRoom, hasAddWorkspaceError, isUnread, isPinned, hasDraft', () => {
// Given a report that is the active report and doesn't change