diff --git a/.github/workflows/deployExpensifyHelp.yml b/.github/workflows/deployExpensifyHelp.yml
index 831ec0c0b95e..5572c3704967 100644
--- a/.github/workflows/deployExpensifyHelp.yml
+++ b/.github/workflows/deployExpensifyHelp.yml
@@ -51,6 +51,8 @@ jobs:
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: helpdot
directory: ./docs/_site
+ # Add the conditional on this step to prevent execution for pull requests from forks
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
- name: Setup Cloudflare CLI
run: pip3 install cloudflare==2.19.0
diff --git a/android/app/build.gradle b/android/app/build.gradle
index e6b32dcd4d4b..e4116d33ba74 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -98,8 +98,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001045200
- versionName "1.4.52-0"
+ versionCode 1001045301
+ versionName "1.4.53-1"
}
flavorDimensions "default"
diff --git a/docs/articles/expensify-classic/copilots-and-delegates/Act-as-a-copilot.md b/docs/articles/expensify-classic/copilots-and-delegates/Act-as-a-copilot.md
new file mode 100644
index 000000000000..04bc82a90774
--- /dev/null
+++ b/docs/articles/expensify-classic/copilots-and-delegates/Act-as-a-copilot.md
@@ -0,0 +1,42 @@
+---
+title: Act as a Copilot
+description: How to access another account after being granted Copilot permissions
+---
+
+
+After being assigned as a Copilot, you can access the account you’ve been given Copilot permissions to via the Expensify website or the mobile app.
+
+{% include selector.html values="desktop, mobile" %}
+
+{% include option.html value="desktop" %}
+To switch to Copilot mode,
+1. Click your profile icon in the upper left side of the page.
+2. In the “Copilot Access” section of the dropdown, choose the account you wish to access.
+
+The Expensify header will change to blue, and an airplane icon will appear to show that you are in copilot mode. You can return to your own account any time by clicking your profile icon and selecting **Return to your account**.
+
+{% include end-option.html %}
+
+{% include option.html value="mobile" %}
+To switch to Copilot mode,
+1. Tap the menu icon in the top left.
+2. Tap your profile icon.
+3. Tap **Switch to Copilot Mode** and choose the account.
+
+An airplane icon will appear to show that you are in copilot mode. To return to your own account, follow the same steps above and select **Return to your account**.
+
+{% include end-option.html %}
+
+{% include end-selector.html %}
+
+# FAQs
+
+**Can a Copilot’s secondary login be used to forward receipts?**
+
+Yes, a Copilot can use any of the email addresses tied to their account to forward receipts into the account they are copiloting. To ensure a receipt is routed to the Expensify account you are copiloting instead of your own account, email the receipt to receipts@expensify.com with the email address of the account you are copiloting as the subject line of the email.
+
+**Can I add another copilot to an account that I’m copiloting?**
+
+No, only the original account holder can add another Copilot to their account.
+
+
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 3991493df583..5ac5e5c13af6 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageTypeAPPLCFBundleShortVersionString
- 1.4.52
+ 1.4.53CFBundleSignature????CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.52.0
+ 1.4.53.1ITSAppUsesNonExemptEncryptionLSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index c04d2b7d3802..c6f84a5c53a5 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageTypeBNDLCFBundleShortVersionString
- 1.4.52
+ 1.4.53CFBundleSignature????CFBundleVersion
- 1.4.52.0
+ 1.4.53.1
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 7d5db6a4159b..3f622696676b 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName$(PRODUCT_NAME)CFBundleShortVersionString
- 1.4.52
+ 1.4.53CFBundleVersion
- 1.4.52.0
+ 1.4.53.1NSExtensionNSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index 5529a99e861d..705ba84bada9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.52-0",
+ "version": "1.4.53-1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.52-0",
+ "version": "1.4.53-1",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 1f6e6ce85450..78c13f2768f4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.52-0",
+ "version": "1.4.53-1",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 8c48cbad561f..820937b1eb89 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -330,8 +330,8 @@ const ONYXKEYS = {
ADD_DEBIT_CARD_FORM: 'addDebitCardForm',
ADD_DEBIT_CARD_FORM_DRAFT: 'addDebitCardFormDraft',
WORKSPACE_SETTINGS_FORM: 'workspaceSettingsForm',
- WORKSPACE_CATEGORY_CREATE_FORM: 'workspaceCategoryCreate',
- WORKSPACE_CATEGORY_CREATE_FORM_DRAFT: 'workspaceCategoryCreateDraft',
+ WORKSPACE_CATEGORY_FORM: 'workspaceCategoryForm',
+ WORKSPACE_CATEGORY_FORM_DRAFT: 'workspaceCategoryFormDraft',
WORKSPACE_TAG_CREATE_FORM: 'workspaceTagCreate',
WORKSPACE_TAG_CREATE_FORM_DRAFT: 'workspaceTagCreateDraft',
WORKSPACE_SETTINGS_FORM_DRAFT: 'workspaceSettingsFormDraft',
@@ -417,7 +417,7 @@ type AllOnyxKeys = DeepValueOf;
type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: FormTypes.AddDebitCardForm;
[ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: FormTypes.WorkspaceSettingsForm;
- [ONYXKEYS.FORMS.WORKSPACE_CATEGORY_CREATE_FORM]: FormTypes.WorkspaceCategoryCreateForm;
+ [ONYXKEYS.FORMS.WORKSPACE_CATEGORY_FORM]: FormTypes.WorkspaceCategoryForm;
[ONYXKEYS.FORMS.WORKSPACE_TAG_CREATE_FORM]: FormTypes.WorkspaceTagCreateForm;
[ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: FormTypes.WorkspaceRateAndUnitForm;
[ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: FormTypes.CloseAccountForm;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index ad2d9c10700b..680c5bced9a9 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -555,7 +555,7 @@ const ROUTES = {
},
WORKSPACE_CATEGORY_SETTINGS: {
route: 'settings/workspaces/:policyID/categories/:categoryName',
- getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURI(categoryName)}` as const,
+ getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}` as const,
},
WORKSPACE_CATEGORIES_SETTINGS: {
route: 'settings/workspaces/:policyID/categories/settings',
@@ -569,6 +569,10 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/categories/new',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/new` as const,
},
+ WORKSPACE_CATEGORY_EDIT: {
+ route: 'workspace/:policyID/categories/:categoryName/edit',
+ getRoute: (policyID: string, categoryName: string) => `workspace/${policyID}/categories/${encodeURI(categoryName)}/edit` as const,
+ },
WORKSPACE_TAGS: {
route: 'settings/workspaces/:policyID/tags',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags` as const,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 6c742f08bfb7..7ccb24aa19e5 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -231,6 +231,7 @@ const SCREENS = {
SHARE: 'Workspace_Profile_Share',
NAME: 'Workspace_Profile_Name',
CATEGORY_CREATE: 'Category_Create',
+ CATEGORY_EDIT: 'Category_Edit',
CATEGORY_SETTINGS: 'Category_Settings',
CATEGORIES_SETTINGS: 'Categories_Settings',
MORE_FEATURES: 'Workspace_More_Features',
diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx
index 2998b2258ef1..eac41b6d627a 100644
--- a/src/components/AvatarCropModal/AvatarCropModal.tsx
+++ b/src/components/AvatarCropModal/AvatarCropModal.tsx
@@ -404,9 +404,8 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose
>
diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx
index 17d58f5abaf2..3abc5b17ae80 100644
--- a/src/components/Button/index.tsx
+++ b/src/components/Button/index.tsx
@@ -241,6 +241,7 @@ function Button(
navigateToWaypointEditPage(Object.keys(transaction?.comment?.waypoints ?? {}).length)}
text={translate('distance.addStop')}
isDisabled={numberOfWaypoints === MAX_WAYPOINTS}
- innerStyles={[styles.ph10]}
+ innerStyles={[styles.pl10, styles.pr10]}
/>
)}
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js
index 25a40a709658..ecfcdd70336f 100755
--- a/src/components/EmojiPicker/EmojiPickerMenu/index.js
+++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js
@@ -112,6 +112,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected, activeEmoji}) {
disableHorizontalKeys: isFocused,
// We pass true without checking visibility of the component because if the popover is not visible this picker won't be mounted
isActive: true,
+ allowNegativeIndexes: true,
});
const filterEmojis = _.throttle((searchTerm) => {
diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx
index e4484980fff1..b4da5c0b0fa2 100644
--- a/src/components/Icon/index.tsx
+++ b/src/components/Icon/index.tsx
@@ -40,6 +40,9 @@ type IconProps = {
/** Is icon pressed */
pressed?: boolean;
+ /** Is icon will be used with text */
+ hasText?: boolean;
+
/** Additional styles to add to the Icon */
additionalStyles?: StyleProp;
@@ -56,6 +59,7 @@ function Icon({
height = variables.iconSizeNormal,
fill = undefined,
small = false,
+ hasText = false,
large = false,
medium = false,
inline = false,
@@ -67,7 +71,7 @@ function Icon({
}: IconProps) {
const StyleUtils = useStyleUtils();
const styles = useThemeStyles();
- const {width: iconWidth, height: iconHeight} = StyleUtils.getIconWidthAndHeightStyle(small, medium, large, width, height);
+ const {width: iconWidth, height: iconHeight} = StyleUtils.getIconWidthAndHeightStyle(small, medium, large, width, height, hasText);
const iconStyles = [StyleUtils.getWidthAndHeightStyle(width ?? 0, height), IconWrapperStyles, styles.pAbsolute, additionalStyles];
if (inline) {
diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index f9ce70e851bc..6835bcf3f5fc 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -224,7 +224,7 @@ type MenuItemBaseProps = {
onSecondaryInteraction?: (event: GestureResponderEvent | MouseEvent) => void;
/** Array of objects that map display names to their corresponding tooltip */
- titleWithTooltips?: DisplayNameWithTooltip[];
+ titleWithTooltips?: DisplayNameWithTooltip[] | undefined;
/** Icon should be displayed in its own color */
displayInDefaultIconColor?: boolean;
diff --git a/src/components/VideoPlayerContexts/PlaybackContext.tsx b/src/components/VideoPlayerContexts/PlaybackContext.tsx
index ae33a82fd0e7..caad4ce5519b 100644
--- a/src/components/VideoPlayerContexts/PlaybackContext.tsx
+++ b/src/components/VideoPlayerContexts/PlaybackContext.tsx
@@ -15,15 +15,15 @@ function PlaybackContextProvider({children}: ChildrenProps) {
const {currentReportID} = useCurrentReportID() ?? {};
const pauseVideo = useCallback(() => {
- currentVideoPlayerRef.current?.setStatusAsync({shouldPlay: false});
+ currentVideoPlayerRef.current?.setStatusAsync?.({shouldPlay: false});
}, [currentVideoPlayerRef]);
const stopVideo = useCallback(() => {
- currentVideoPlayerRef.current?.stopAsync();
+ currentVideoPlayerRef.current?.stopAsync?.();
}, [currentVideoPlayerRef]);
const playVideo = useCallback(() => {
- currentVideoPlayerRef.current?.getStatusAsync().then((status) => {
+ currentVideoPlayerRef.current?.getStatusAsync?.().then((status) => {
const newStatus: AVPlaybackStatusToSet = {shouldPlay: true};
if ('durationMillis' in status && status.durationMillis === status.positionMillis) {
newStatus.positionMillis = 0;
@@ -33,7 +33,7 @@ function PlaybackContextProvider({children}: ChildrenProps) {
}, [currentVideoPlayerRef]);
const unloadVideo = useCallback(() => {
- currentVideoPlayerRef.current?.unloadAsync();
+ currentVideoPlayerRef.current?.unloadAsync?.();
}, [currentVideoPlayerRef]);
const updateCurrentlyPlayingURL = useCallback(
@@ -61,7 +61,7 @@ function PlaybackContextProvider({children}: ChildrenProps) {
const checkVideoPlaying = useCallback(
(statusCallback: StatusCallback) => {
- currentVideoPlayerRef.current?.getStatusAsync().then((status) => {
+ currentVideoPlayerRef.current?.getStatusAsync?.().then((status) => {
statusCallback('isPlaying' in status && status.isPlaying);
});
},
diff --git a/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx
index 0a53801683c3..f953ed802623 100644
--- a/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx
+++ b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx
@@ -22,7 +22,7 @@ function VideoPopoverMenuContextProvider({children}: ChildrenProps) {
const updatePlaybackSpeed = useCallback(
(speed: PlaybackSpeed) => {
setCurrentPlaybackSpeed(speed);
- currentVideoPlayerRef.current?.setStatusAsync({rate: speed});
+ currentVideoPlayerRef.current?.setStatusAsync?.({rate: speed});
},
[currentVideoPlayerRef],
);
diff --git a/src/hooks/useArrowKeyFocusManager.ts b/src/hooks/useArrowKeyFocusManager.ts
index 78ffc7f87209..b11999d61cf3 100644
--- a/src/hooks/useArrowKeyFocusManager.ts
+++ b/src/hooks/useArrowKeyFocusManager.ts
@@ -12,6 +12,7 @@ type Config = {
itemsPerRow?: number;
disableCyclicTraversal?: boolean;
disableHorizontalKeys?: boolean;
+ allowNegativeIndexes?: boolean;
};
type UseArrowKeyFocusManager = [number, (index: number) => void];
@@ -44,6 +45,7 @@ export default function useArrowKeyFocusManager({
itemsPerRow,
disableCyclicTraversal = false,
disableHorizontalKeys = false,
+ allowNegativeIndexes = false,
}: Config): UseArrowKeyFocusManager {
const allowHorizontalArrowKeys = !!itemsPerRow;
const [focusedIndex, setFocusedIndex] = useState(initialFocusedIndex);
@@ -84,7 +86,13 @@ export default function useArrowKeyFocusManager({
while (disabledIndexes.includes(newFocusedIndex)) {
newFocusedIndex -= allowHorizontalArrowKeys ? itemsPerRow : 1;
if (newFocusedIndex < 0) {
- break;
+ if (disableCyclicTraversal) {
+ if (!allowNegativeIndexes) {
+ return actualIndex;
+ }
+ break;
+ }
+ newFocusedIndex = maxIndex;
}
if (newFocusedIndex === currentFocusedIndex) {
// all indexes are disabled
@@ -93,7 +101,7 @@ export default function useArrowKeyFocusManager({
}
return newFocusedIndex;
});
- }, [allowHorizontalArrowKeys, disableCyclicTraversal, disabledIndexes, itemsPerRow, maxIndex]);
+ }, [allowHorizontalArrowKeys, disableCyclicTraversal, disabledIndexes, itemsPerRow, maxIndex, allowNegativeIndexes]);
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_UP, arrowUpCallback, arrowConfig);
@@ -127,8 +135,11 @@ export default function useArrowKeyFocusManager({
newFocusedIndex += allowHorizontalArrowKeys ? itemsPerRow : 1;
}
- if (newFocusedIndex < 0) {
- break;
+ if (newFocusedIndex > maxIndex) {
+ if (disableCyclicTraversal) {
+ return actualIndex;
+ }
+ newFocusedIndex = 0;
}
if (newFocusedIndex === currentFocusedIndex) {
// all indexes are disabled
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 6ec5983583fc..eecd81c54123 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1789,6 +1789,7 @@ export default {
},
genericFailureMessage: 'An error occurred while updating the category, please try again.',
addCategory: 'Add category',
+ editCategory: 'Edit category',
categoryRequiredError: 'Category name is required.',
existingCategoryError: 'A category with this name already exists.',
invalidCategoryName: 'Invalid category name.',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index c2eb6374affa..cd36f9071de6 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1813,6 +1813,7 @@ export default {
},
genericFailureMessage: 'Se ha producido un error al intentar eliminar la categoría. Por favor, inténtalo más tarde.',
addCategory: 'Añadir categoría',
+ editCategory: 'Editar categoría',
categoryRequiredError: 'Lo nombre de la categoría es obligatorio.',
existingCategoryError: 'Ya existe una categoría con este nombre.',
invalidCategoryName: 'Lo nombre de la categoría es invalido.',
diff --git a/src/libs/API/parameters/RenameWorkspaceCategoriesParams.ts b/src/libs/API/parameters/RenameWorkspaceCategoriesParams.ts
new file mode 100644
index 000000000000..4ed07858564f
--- /dev/null
+++ b/src/libs/API/parameters/RenameWorkspaceCategoriesParams.ts
@@ -0,0 +1,10 @@
+type RenameWorkspaceCategoriesParams = {
+ policyID: string;
+ /**
+ * Stringified JSON object with type of following structure:
+ * {[oldName: string]: string;} where value is new category name
+ */
+ categories: string;
+};
+
+export default RenameWorkspaceCategoriesParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 25c336753203..302b980685e1 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -151,6 +151,7 @@ export type {default as UpdateWorkspaceDescriptionParams} from './UpdateWorkspac
export type {default as UpdateWorkspaceMembersRoleParams} from './UpdateWorkspaceMembersRoleParams';
export type {default as SetWorkspaceCategoriesEnabledParams} from './SetWorkspaceCategoriesEnabledParams';
export type {default as CreateWorkspaceCategoriesParams} from './CreateWorkspaceCategoriesParams';
+export type {default as RenameWorkspaceCategoriesParams} from './RenameWorkspaceCategoriesParams';
export type {default as SetWorkspaceRequiresCategoryParams} from './SetWorkspaceRequiresCategoryParams';
export type {default as DeleteWorkspaceCategoriesParams} from './DeleteWorkspaceCategoriesParams';
export type {default as SetWorkspaceAutoReportingParams} from './SetWorkspaceAutoReportingParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 07f1ca09d7c5..50b6206bc341 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -118,6 +118,7 @@ const WRITE_COMMANDS = {
CREATE_WORKSPACE_FROM_IOU_PAYMENT: 'CreateWorkspaceFromIOUPayment',
SET_WORKSPACE_CATEGORIES_ENABLED: 'SetWorkspaceCategoriesEnabled',
CREATE_WORKSPACE_CATEGORIES: 'CreateWorkspaceCategories',
+ RENAME_WORKSPACE_CATEGORY: 'RenameWorkspaceCategory',
CREATE_POLICY_TAG: 'CreatePolicyTag',
SET_WORKSPACE_REQUIRES_CATEGORY: 'SetWorkspaceRequiresCategory',
DELETE_WORKSPACE_CATEGORIES: 'DeleteWorkspaceCategories',
@@ -282,6 +283,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.CREATE_WORKSPACE_FROM_IOU_PAYMENT]: Parameters.CreateWorkspaceFromIOUPaymentParams;
[WRITE_COMMANDS.SET_WORKSPACE_CATEGORIES_ENABLED]: Parameters.SetWorkspaceCategoriesEnabledParams;
[WRITE_COMMANDS.CREATE_WORKSPACE_CATEGORIES]: Parameters.CreateWorkspaceCategoriesParams;
+ [WRITE_COMMANDS.RENAME_WORKSPACE_CATEGORY]: Parameters.RenameWorkspaceCategoriesParams;
[WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams;
[WRITE_COMMANDS.DELETE_WORKSPACE_CATEGORIES]: Parameters.DeleteWorkspaceCategoriesParams;
[WRITE_COMMANDS.SET_POLICY_REQUIRES_TAG]: Parameters.SetPolicyRequiresTag;
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
index a4d7593cf750..2e55593ddd01 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
@@ -263,6 +263,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/members/WorkspaceMemberDetailsPage').default as React.ComponentType,
[SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION]: () => require('../../../pages/workspace/members/WorkspaceMemberDetailsRoleSelectionPage').default as React.ComponentType,
[SCREENS.WORKSPACE.CATEGORY_CREATE]: () => require('../../../pages/workspace/categories/CreateCategoryPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.CATEGORY_EDIT]: () => require('../../../pages/workspace/categories/EditCategoryPage').default as React.ComponentType,
[SCREENS.WORKSPACE.TAGS_SETTINGS]: () => require('../../../pages/workspace/tags/WorkspaceTagsSettingsPage').default as React.ComponentType,
[SCREENS.WORKSPACE.TAGS_EDIT]: () => require('../../../pages/workspace/tags/WorkspaceEditTagsPage').default as React.ComponentType,
[SCREENS.WORKSPACE.TAG_CREATE]: () => require('../../../pages/workspace/tags/WorkspaceCreateTagPage').default as React.ComponentType,
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 fd108f2c95f3..7561fb44933c 100755
--- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
@@ -12,7 +12,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.WORKFLOWS_PAYER,
],
[SCREENS.WORKSPACE.TAGS]: [SCREENS.WORKSPACE.TAGS_SETTINGS, SCREENS.WORKSPACE.TAGS_EDIT, SCREENS.WORKSPACE.TAG_CREATE],
- [SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS],
+ [SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS, SCREENS.WORKSPACE.CATEGORY_EDIT],
};
export default FULL_SCREEN_TO_RHP_MAPPING;
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 04bc53e7b542..56734e3c7242 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -265,7 +265,7 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.CATEGORY_SETTINGS]: {
path: ROUTES.WORKSPACE_CATEGORY_SETTINGS.route,
parse: {
- categoryName: (categoryName: string) => decodeURI(categoryName),
+ categoryName: (categoryName: string) => decodeURIComponent(categoryName),
},
},
[SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: {
@@ -283,6 +283,12 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.CATEGORY_CREATE]: {
path: ROUTES.WORKSPACE_CATEGORY_CREATE.route,
},
+ [SCREENS.WORKSPACE.CATEGORY_EDIT]: {
+ path: ROUTES.WORKSPACE_CATEGORY_EDIT.route,
+ parse: {
+ categoryName: (categoryName: string) => decodeURI(categoryName),
+ },
+ },
[SCREENS.WORKSPACE.TAGS_SETTINGS]: {
path: ROUTES.WORKSPACE_TAGS_SETTINGS.route,
},
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index da418625ff55..939841279eed 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -168,6 +168,10 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.CATEGORY_CREATE]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.CATEGORY_EDIT]: {
+ policyID: string;
+ categoryName: string;
+ };
[SCREENS.WORKSPACE.CATEGORY_SETTINGS]: {
policyID: string;
categoryName: string;
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index d5575869444a..2bc5f20576e1 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -266,10 +266,7 @@ function getPolicyMembersByIdWithoutCurrentUser(policyMembers: OnyxCollection TransactionUtils.hasMissingSmartscanFields(transaction));
}
+/**
+ * Get the transactions related to a report preview with receipts
+ * Get the details linked to the IOU reportAction
+ *
+ * NOTE: This method is only meant to be used inside this action file. Do not export and use it elsewhere. Use withOnyx or Onyx.connect() instead.
+ */
+function getLinkedTransaction(reportAction: OnyxEntry): Transaction | EmptyObject {
+ let transactionID = '';
+
+ if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) {
+ transactionID = (reportAction?.originalMessage as IOUMessage)?.IOUTransactionID ?? '';
+ }
+
+ return allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? {};
+}
+
+/**
+ * Retrieve the particular transaction object given its ID.
+ *
+ * NOTE: This method is only meant to be used inside this action file. Do not export and use it elsewhere. Use withOnyx or Onyx.connect() instead.
+ */
+function getTransaction(transactionID: string): OnyxEntry | EmptyObject {
+ return allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? {};
+}
+
/**
* Given a parent IOU report action get report name for the LHN.
*/
@@ -2357,7 +2382,7 @@ function getTransactionReportName(reportAction: OnyxEntry)
return Localize.translateLocal(translationKey, {amount: formattedAmount, payer: ''});
}
- const transaction = TransactionUtils.getTransaction(originalMessage.IOUTransactionID ?? '');
+ const transaction = getTransaction(originalMessage.IOUTransactionID ?? '');
const transactionDetails = getTransactionDetails(!isEmptyObject(transaction) ? transaction : null);
const formattedAmount = CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency);
const isRequestSettled = isSettled(originalMessage.IOUReportID);
diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts
index f8f9ed0e0d47..bc94c8fee8fc 100644
--- a/src/libs/TransactionUtils.ts
+++ b/src/libs/TransactionUtils.ts
@@ -4,16 +4,13 @@ import Onyx from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {RecentWaypoint, Report, ReportAction, TaxRate, TaxRates, Transaction, TransactionViolation} from '@src/types/onyx';
-import type {IOUMessage} from '@src/types/onyx/OriginalMessage';
+import type {RecentWaypoint, Report, TaxRate, TaxRates, Transaction, TransactionViolation} from '@src/types/onyx';
import type {Comment, Receipt, TransactionChanges, TransactionPendingFieldsKey, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction';
-import type {EmptyObject} from '@src/types/utils/EmptyObject';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import {isCorporateCard, isExpensifyCard} from './CardUtils';
import DateUtils from './DateUtils';
import * as NumberUtils from './NumberUtils';
import {getCleanedTagName} from './PolicyUtils';
-import type {OptimisticIOUReportAction} from './ReportUtils';
let allTransactions: OnyxCollection = {};
@@ -248,15 +245,6 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra
return updatedTransaction;
}
-/**
- * Retrieve the particular transaction object given its ID.
- *
- * @deprecated Use withOnyx() or Onyx.connect() instead
- */
-function getTransaction(transactionID: string): OnyxEntry | EmptyObject {
- return allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? {};
-}
-
/**
* Return the comment field (referred to as description in the App) from the transaction.
* The comment does not have its modifiedComment counterpart.
@@ -493,22 +481,6 @@ function hasRoute(transaction: OnyxEntry): boolean {
return !!transaction?.routes?.route0?.geometry?.coordinates;
}
-/**
- * Get the transactions related to a report preview with receipts
- * Get the details linked to the IOU reportAction
- *
- * @deprecated Use Onyx.connect() or withOnyx() instead
- */
-function getLinkedTransaction(reportAction: OnyxEntry): Transaction | EmptyObject {
- let transactionID = '';
-
- if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) {
- transactionID = (reportAction?.originalMessage as IOUMessage)?.IOUTransactionID ?? '';
- }
-
- return allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? {};
-}
-
function getAllReportTransactions(reportID?: string): Transaction[] {
// `reportID` from the `/CreateDistanceRequest` endpoint return's number instead of string for created `transaction`.
// For reference, https://github.com/Expensify/App/pull/26536#issuecomment-1703573277.
@@ -617,7 +589,6 @@ export {
calculateTaxAmount,
getEnabledTaxRateCount,
getUpdatedTransaction,
- getTransaction,
getDescription,
getHeaderTitleTranslationKey,
getRequestType,
@@ -638,7 +609,6 @@ export {
getTagArrayFromName,
getTagForDisplay,
getTransactionViolations,
- getLinkedTransaction,
getAllReportTransactions,
hasReceipt,
hasEReceipt,
diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts
index 2adcfd29e00d..28ce7ea2a159 100644
--- a/src/libs/actions/Policy.ts
+++ b/src/libs/actions/Policy.ts
@@ -436,6 +436,7 @@ function buildAnnounceRoomMembersOnyxData(policyID: string, accountIDs: number[]
}
function setWorkspaceAutoReporting(policyID: string, enabled: boolean, frequency: ValueOf) {
+ const policy = ReportUtils.getPolicy(policyID);
const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
@@ -443,7 +444,7 @@ function setWorkspaceAutoReporting(policyID: string, enabled: boolean, frequency
value: {
autoReporting: enabled,
harvesting: {
- enabled: true,
+ enabled,
},
autoReportingFrequency: frequency,
pendingFields: {isAutoApprovalEnabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE},
@@ -456,7 +457,11 @@ function setWorkspaceAutoReporting(policyID: string, enabled: boolean, frequency
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
- autoReporting: !enabled,
+ autoReporting: policy.autoReporting ?? null,
+ harvesting: {
+ enabled: policy.harvesting?.enabled ?? null,
+ },
+ autoReportingFrequency: policy.autoReportingFrequency ?? null,
pendingFields: {isAutoApprovalEnabled: null, harvesting: null},
},
},
@@ -478,6 +483,8 @@ function setWorkspaceAutoReporting(policyID: string, enabled: boolean, frequency
}
function setWorkspaceAutoReportingFrequency(policyID: string, frequency: ValueOf) {
+ const policy = ReportUtils.getPolicy(policyID);
+
const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
@@ -494,6 +501,7 @@ function setWorkspaceAutoReportingFrequency(policyID: string, frequency: ValueOf
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
+ autoReportingFrequency: policy.autoReportingFrequency ?? null,
pendingFields: {autoReportingFrequency: null},
},
},
@@ -515,6 +523,7 @@ function setWorkspaceAutoReportingFrequency(policyID: string, frequency: ValueOf
function setWorkspaceAutoReportingMonthlyOffset(policyID: string, autoReportingOffset: number | ValueOf) {
const value = JSON.stringify({autoReportingOffset: autoReportingOffset.toString()});
+ const policy = ReportUtils.getPolicy(policyID);
const optimisticData: OnyxUpdate[] = [
{
@@ -532,6 +541,7 @@ function setWorkspaceAutoReportingMonthlyOffset(policyID: string, autoReportingO
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
+ autoReportingOffset: policy.autoReportingOffset ?? null,
pendingFields: {autoReportingOffset: null},
},
},
@@ -553,6 +563,7 @@ function setWorkspaceAutoReportingMonthlyOffset(policyID: string, autoReportingO
function setWorkspaceApprovalMode(policyID: string, approver: string, approvalMode: ValueOf) {
const isAutoApprovalEnabled = approvalMode === CONST.POLICY.APPROVAL_MODE.BASIC;
+ const policy = ReportUtils.getPolicy(policyID);
const value = {
approver,
@@ -576,6 +587,9 @@ function setWorkspaceApprovalMode(policyID: string, approver: string, approvalMo
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
+ approver: policy.approver ?? null,
+ approvalMode: policy.approvalMode ?? null,
+ isAutoApprovalEnabled: policy.isAutoApprovalEnabled ?? null,
pendingFields: {approvalMode: null},
},
},
@@ -627,8 +641,8 @@ function setWorkspacePayer(policyID: string, reimburserEmail: string, reimburser
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
- reimburserEmail: policy.reimburserEmail,
- reimburserAccountID: policy.reimburserAccountID,
+ reimburserEmail: policy.reimburserEmail ?? null,
+ reimburserAccountID: policy.reimburserAccountID ?? null,
errorFields: {reimburserEmail: ErrorUtils.getMicroSecondOnyxError('workflowsPayerPage.genericErrorMessage')},
pendingFields: {reimburserEmail: null},
},
@@ -677,9 +691,9 @@ function setWorkspaceReimbursement(policyID: string, reimbursementChoice: ValueO
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
- reimbursementChoice: policy.reimbursementChoice,
- reimburserAccountID: policy.reimburserAccountID,
- reimburserEmail: policy.reimburserEmail,
+ reimbursementChoice: policy.reimbursementChoice ?? null,
+ reimburserAccountID: policy.reimburserAccountID ?? null,
+ reimburserEmail: policy.reimburserEmail ?? null,
errorFields: {reimbursementChoice: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage')},
pendingFields: {reimbursementChoice: null},
},
@@ -2762,6 +2776,67 @@ function createPolicyCategory(policyID: string, categoryName: string) {
API.write(WRITE_COMMANDS.CREATE_WORKSPACE_CATEGORIES, parameters, onyxData);
}
+function renamePolicyCategory(policyID: string, policyCategory: {oldName: string; newName: string}) {
+ const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[policyCategory.oldName] ?? {};
+
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
+ value: {
+ [policyCategory.oldName]: null,
+ [policyCategory.newName]: {
+ ...policyCategoryToUpdate,
+ name: policyCategory.newName,
+ unencodedName: decodeURIComponent(policyCategory.newName),
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
+ value: {
+ [policyCategory.oldName]: null,
+ [policyCategory.newName]: {
+ ...policyCategoryToUpdate,
+ name: policyCategory.newName,
+ unencodedName: decodeURIComponent(policyCategory.newName),
+ errors: null,
+ pendingAction: null,
+ },
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
+ value: {
+ [policyCategory.newName]: null,
+ [policyCategory.oldName]: {
+ ...policyCategoryToUpdate,
+ name: policyCategory.oldName,
+ unencodedName: decodeURIComponent(policyCategory.oldName),
+ errors: ErrorUtils.getMicroSecondOnyxError('workspace.categories.genericFailureMessage'),
+ pendingAction: null,
+ },
+ },
+ },
+ ],
+ };
+
+ const parameters = {
+ policyID,
+ categories: JSON.stringify({[policyCategory.oldName]: policyCategory.newName}),
+ };
+
+ API.write(WRITE_COMMANDS.RENAME_WORKSPACE_CATEGORY, parameters, onyxData);
+}
+
function createPolicyTag(policyID: string, tagName: string) {
const tagListName = Object.keys(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})[0];
@@ -2809,7 +2884,6 @@ function createPolicyTag(policyID: string, tagName: string) {
tags: {
[tagName]: {
errors: ErrorUtils.getMicroSecondOnyxError('workspace.tags.genericFailureMessage'),
- pendingAction: null,
},
},
},
@@ -2826,6 +2900,36 @@ function createPolicyTag(policyID: string, tagName: string) {
API.write(WRITE_COMMANDS.CREATE_POLICY_TAG, parameters, onyxData);
}
+function clearPolicyTagErrors(policyID: string, tagName: string) {
+ const tagListName = Object.keys(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})[0];
+ const tag = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`]?.[tagListName].tags?.[tagName];
+ if (!tag) {
+ return;
+ }
+
+ if (tag.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) {
+ Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {
+ [tagListName]: {
+ tags: {
+ [tagName]: null,
+ },
+ },
+ });
+ return;
+ }
+
+ Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {
+ [tagListName]: {
+ tags: {
+ [tagName]: {
+ errors: null,
+ pendingAction: null,
+ },
+ },
+ },
+ });
+}
+
function setWorkspaceRequiresCategory(policyID: string, requiresCategory: boolean) {
const onyxData: OnyxData = {
optimisticData: [
@@ -3572,6 +3676,7 @@ export {
acceptJoinRequest,
declineJoinRequest,
createPolicyCategory,
+ renamePolicyCategory,
clearCategoryErrors,
setWorkspacePayer,
clearWorkspacePayerError,
@@ -3589,6 +3694,7 @@ export {
openPolicyDistanceRatesPage,
openPolicyMoreFeaturesPage,
createPolicyTag,
+ clearPolicyTagErrors,
clearWorkspaceReimbursementErrors,
deleteWorkspaceCategories,
};
diff --git a/src/pages/ReimbursementAccount/BankAccountStep.js b/src/pages/ReimbursementAccount/BankAccountStep.js
index ea652b6d7eb0..666e233196b6 100644
--- a/src/pages/ReimbursementAccount/BankAccountStep.js
+++ b/src/pages/ReimbursementAccount/BankAccountStep.js
@@ -135,6 +135,7 @@ function BankAccountStep(props) {
)}