From 086e464eda1d76fe94858aadd75feb6ac4af0b07 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 19 Jul 2024 17:47:42 +0500 Subject: [PATCH 01/47] feat: log network response and request. Allow filtering option in Debug Console --- src/libs/actions/OnyxUpdates.ts | 3 +- src/pages/settings/AboutPage/ConsolePage.tsx | 48 +++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/OnyxUpdates.ts b/src/libs/actions/OnyxUpdates.ts index 73c8b3d592bf..6769fa7e64a3 100644 --- a/src/libs/actions/OnyxUpdates.ts +++ b/src/libs/actions/OnyxUpdates.ts @@ -35,7 +35,8 @@ function applyHTTPSOnyxUpdates(request: Request, response: Response) { // apply successData or failureData. This ensures that we do not update any pending, loading, or other UI states contained // in successData/failureData until after the component has received and API data. const onyxDataUpdatePromise = response.onyxData ? updateHandler(response.onyxData) : Promise.resolve(); - + Log.info('[OnyxUpdateManager]-[Network]-[Request]', false, request); + Log.info('[OnyxUpdateManager]-[Network]-[Response]', false, response); return onyxDataUpdatePromise .then(() => { // Handle the request's success/failure data (client-side data) diff --git a/src/pages/settings/AboutPage/ConsolePage.tsx b/src/pages/settings/AboutPage/ConsolePage.tsx index aee11c89f22c..6f5942293f61 100644 --- a/src/pages/settings/AboutPage/ConsolePage.tsx +++ b/src/pages/settings/AboutPage/ConsolePage.tsx @@ -11,11 +11,13 @@ import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import InvertedFlatList from '@components/InvertedFlatList'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {addLog} from '@libs/actions/Console'; import {createLog, parseStringifiedMessages, sanitizeConsoleInput} from '@libs/Console'; @@ -40,16 +42,56 @@ type ConsolePageOnyxProps = { type ConsolePageProps = ConsolePageOnyxProps; +const filterBy = { + all: '', + network: '-[Network]-', +} as const; +type FilterBy = (typeof filterBy)[keyof typeof filterBy]; + function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) { const [input, setInput] = useState(''); const [logs, setLogs] = useState(capturedLogs); const [isGeneratingLogsFile, setIsGeneratingLogsFile] = useState(false); const [isLimitModalVisible, setIsLimitModalVisible] = useState(false); + const [activeFilterIndex, setActiveFilterIndex] = useState(filterBy.all); const {translate} = useLocalize(); const styles = useThemeStyles(); + const theme = useTheme(); const route = useRoute>(); + const menuItems: PopoverMenuItem[] = useMemo( + () => [ + { + text: translate('common.filterLogs'), + disabled: true, + }, + { + icon: Expensicons.All, + text: translate('common.all'), + iconFill: activeFilterIndex === filterBy.all ? theme.iconSuccessFill : theme.icon, + iconRight: Expensicons.Checkmark, + shouldShowRightIcon: activeFilterIndex === filterBy.all, + success: activeFilterIndex === filterBy.all, + onSelected: () => { + setActiveFilterIndex(filterBy.all); + }, + }, + { + icon: Expensicons.CardsAndDomains, + text: translate('common.network'), + iconFill: activeFilterIndex === filterBy.network ? theme.iconSuccessFill : theme.icon, + iconRight: Expensicons.CheckCircle, + shouldShowRightIcon: activeFilterIndex === filterBy.network, + success: activeFilterIndex === filterBy.network, + onSelected: () => { + setActiveFilterIndex(filterBy.network); + }, + }, + ], + [activeFilterIndex, theme.icon, theme.iconSuccessFill, translate], + ); + const logsList = useMemo( () => Object.entries(logs ?? {}) @@ -58,6 +100,8 @@ function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) { [logs], ); + const filteredLogsList = useMemo(() => logsList.filter((log) => log.message.includes(activeFilterIndex)), [activeFilterIndex, logsList]); + useEffect(() => { if (!shouldStoreLogs) { return; @@ -121,10 +165,12 @@ function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) { Navigation.goBack(route.params?.backTo)} + shouldShowThreeDotsButton + threeDotsMenuItems={menuItems} /> {translate('initialSettingsPage.debugConsole.noLogsAvailable')}} From 766f922bc0c50d5dc83dd01908c9232016ae8bcb Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 19 Jul 2024 17:48:06 +0500 Subject: [PATCH 02/47] feat: add locale values for filter options --- src/languages/en.ts | 2 ++ src/languages/es.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index a8281e609305..5e2654ebc847 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -368,6 +368,8 @@ export default { value: 'Value', downloadFailedTitle: 'Download failed', downloadFailedDescription: "Your download couldn't be completed. Please try again later.", + filterLogs: 'Filter Logs', + network: 'Network', }, location: { useCurrent: 'Use current location', diff --git a/src/languages/es.ts b/src/languages/es.ts index bbbbbfc79bac..2e383d04e451 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -358,6 +358,8 @@ export default { value: 'Valor', downloadFailedTitle: 'Error en la descarga', downloadFailedDescription: 'No se pudo completar la descarga. Por favor, inténtalo más tarde.', + filterLogs: 'Registros de filtrado', + network: 'La red', }, connectionComplete: { title: 'Conexión completa', From 347f9b26fc2f18ae3bd15f730299a7b04c175e71 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 22 Jul 2024 13:18:15 +0500 Subject: [PATCH 03/47] feat: use correct icons --- src/pages/settings/AboutPage/ConsolePage.tsx | 2 +- src/pages/settings/Troubleshoot/TroubleshootPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/settings/AboutPage/ConsolePage.tsx b/src/pages/settings/AboutPage/ConsolePage.tsx index 6f5942293f61..d6880a3ff052 100644 --- a/src/pages/settings/AboutPage/ConsolePage.tsx +++ b/src/pages/settings/AboutPage/ConsolePage.tsx @@ -78,7 +78,7 @@ function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) { }, }, { - icon: Expensicons.CardsAndDomains, + icon: Expensicons.Globe, text: translate('common.network'), iconFill: activeFilterIndex === filterBy.network ? theme.iconSuccessFill : theme.icon, iconRight: Expensicons.CheckCircle, diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index a3b914192284..10cce86ac08e 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -69,7 +69,7 @@ function TroubleshootPage({shouldStoreLogs, shouldMaskOnyxState}: TroubleshootPa const menuItems = useMemo(() => { const debugConsoleItem: BaseMenuItem = { translationKey: 'initialSettingsPage.troubleshoot.viewConsole', - icon: Expensicons.Gear, + icon: Expensicons.Bug, action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_CONSOLE.getRoute(ROUTES.SETTINGS_TROUBLESHOOT))), }; From c158a4a643be3232dfe5f905c20d8f65182dc21d Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 24 Jul 2024 17:21:48 +0500 Subject: [PATCH 04/47] feat: add filter option to debug console --- src/pages/settings/AboutPage/ConsolePage.tsx | 33 +++++++++----------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/pages/settings/AboutPage/ConsolePage.tsx b/src/pages/settings/AboutPage/ConsolePage.tsx index d6880a3ff052..97961138b280 100644 --- a/src/pages/settings/AboutPage/ConsolePage.tsx +++ b/src/pages/settings/AboutPage/ConsolePage.tsx @@ -1,7 +1,7 @@ import type {RouteProp} from '@react-navigation/native'; import {useRoute} from '@react-navigation/native'; import {format} from 'date-fns'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import type {ListRenderItem, ListRenderItemInfo} from 'react-native'; import {withOnyx} from 'react-native-onyx'; @@ -44,13 +44,12 @@ type ConsolePageProps = ConsolePageOnyxProps; const filterBy = { all: '', - network: '-[Network]-', + network: '[Network]', } as const; type FilterBy = (typeof filterBy)[keyof typeof filterBy]; function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) { const [input, setInput] = useState(''); - const [logs, setLogs] = useState(capturedLogs); const [isGeneratingLogsFile, setIsGeneratingLogsFile] = useState(false); const [isLimitModalVisible, setIsLimitModalVisible] = useState(false); const [activeFilterIndex, setActiveFilterIndex] = useState(filterBy.all); @@ -92,24 +91,22 @@ function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) { [activeFilterIndex, theme.icon, theme.iconSuccessFill, translate], ); - const logsList = useMemo( - () => - Object.entries(logs ?? {}) - .map(([key, value]) => ({key, ...value})) - .reverse(), - [logs], - ); - - const filteredLogsList = useMemo(() => logsList.filter((log) => log.message.includes(activeFilterIndex)), [activeFilterIndex, logsList]); - - useEffect(() => { + const prevLogs = useRef>({}); + const getLogs = useCallback(() => { if (!shouldStoreLogs) { - return; + return []; } - setLogs((prevLogs) => ({...prevLogs, ...capturedLogs})); + prevLogs.current = {...prevLogs.current, ...capturedLogs}; + return Object.entries(prevLogs.current ?? {}) + .map(([key, value]) => ({key, ...value})) + .reverse(); }, [capturedLogs, shouldStoreLogs]); + const logsList = useMemo(() => getLogs(), [getLogs]); + + const filteredLogsList = useMemo(() => logsList.filter((log) => log.message.includes(activeFilterIndex)), [activeFilterIndex, logsList]); + const executeArbitraryCode = () => { const sanitizedInput = sanitizeConsoleInput(input); @@ -121,14 +118,14 @@ function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) { useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, executeArbitraryCode); const saveLogs = () => { - const logsWithParsedMessages = parseStringifiedMessages(logsList); + const logsWithParsedMessages = parseStringifiedMessages(filteredLogsList); localFileDownload('logs', JSON.stringify(logsWithParsedMessages, null, 2)); }; const shareLogs = () => { setIsGeneratingLogsFile(true); - const logsWithParsedMessages = parseStringifiedMessages(logsList); + const logsWithParsedMessages = parseStringifiedMessages(filteredLogsList); // Generate a file with the logs and pass its path to the list of reports to share it with localFileCreate('logs', JSON.stringify(logsWithParsedMessages, null, 2)).then(({path, size}) => { From fa2d0b34ffa6ddccc03e7fadb824b75b5456d28f Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 24 Jul 2024 17:22:34 +0500 Subject: [PATCH 05/47] feat: add extraData parameter for Logs --- src/libs/Console/index.ts | 12 ++++++------ src/libs/Log.ts | 6 +++--- src/types/onyx/Console.ts | 3 +++ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/libs/Console/index.ts b/src/libs/Console/index.ts index 9bbdb173e61b..153008e2b785 100644 --- a/src/libs/Console/index.ts +++ b/src/libs/Console/index.ts @@ -53,7 +53,7 @@ function logMessage(args: unknown[]) { return String(arg); }) .join(' '); - const newLog = {time: new Date(), level: CONST.DEBUG_CONSOLE.LEVELS.INFO, message}; + const newLog = {time: new Date(), level: CONST.DEBUG_CONSOLE.LEVELS.INFO, message, extraData: ''}; addLog(newLog); } @@ -105,15 +105,15 @@ function createLog(text: string) { if (result !== undefined) { return [ - {time, level: CONST.DEBUG_CONSOLE.LEVELS.INFO, message: `> ${text}`}, - {time, level: CONST.DEBUG_CONSOLE.LEVELS.RESULT, message: String(result)}, + {time, level: CONST.DEBUG_CONSOLE.LEVELS.INFO, message: `> ${text}`, extraData: ''}, + {time, level: CONST.DEBUG_CONSOLE.LEVELS.RESULT, message: String(result), extraData: ''}, ]; } - return [{time, level: CONST.DEBUG_CONSOLE.LEVELS.INFO, message: `> ${text}`}]; + return [{time, level: CONST.DEBUG_CONSOLE.LEVELS.INFO, message: `> ${text}`, extraData: ''}]; } catch (error) { return [ - {time, level: CONST.DEBUG_CONSOLE.LEVELS.ERROR, message: `> ${text}`}, - {time, level: CONST.DEBUG_CONSOLE.LEVELS.ERROR, message: `Error: ${(error as Error).message}`}, + {time, level: CONST.DEBUG_CONSOLE.LEVELS.ERROR, message: `> ${text}`, extraData: ''}, + {time, level: CONST.DEBUG_CONSOLE.LEVELS.ERROR, message: `Error: ${(error as Error).message}`, extraData: ''}, ]; } } diff --git a/src/libs/Log.ts b/src/libs/Log.ts index 83965807263a..eefe383034e1 100644 --- a/src/libs/Log.ts +++ b/src/libs/Log.ts @@ -66,15 +66,15 @@ function serverLoggingCallback(logger: Logger, params: ServerLoggingCallbackOpti // callback methods are passed in here so we can decouple the logging library from the logging methods. const Log = new Logger({ serverLoggingCallback, - clientLoggingCallback: (message) => { + clientLoggingCallback: (message, extraData) => { if (!shouldAttachLog(message)) { return; } - console.debug(message); + console.debug(message, extraData); if (shouldCollectLogs) { - addLog({time: new Date(), level: CONST.DEBUG_CONSOLE.LEVELS.DEBUG, message}); + addLog({time: new Date(), level: CONST.DEBUG_CONSOLE.LEVELS.DEBUG, message, extraData}); } }, isDebug: true, diff --git a/src/types/onyx/Console.ts b/src/types/onyx/Console.ts index c8d2b714ae2b..03c76b6912b5 100644 --- a/src/types/onyx/Console.ts +++ b/src/types/onyx/Console.ts @@ -10,6 +10,9 @@ type Log = { /** Log message */ message: string; + + /** Additional data */ + extraData: string | Record | Array> | Error; }; /** Record of captured logs */ From 7249b23a947d74e070c4f31103e4a1e5d2b6e4f7 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 24 Jul 2024 18:02:08 +0500 Subject: [PATCH 06/47] feat: clear the Logs onyx state each time the app is launched --- src/libs/actions/Console.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/Console.ts b/src/libs/actions/Console.ts index 79276d3307ac..e2c4047c43b5 100644 --- a/src/libs/actions/Console.ts +++ b/src/libs/actions/Console.ts @@ -2,14 +2,27 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Log} from '@src/types/onyx'; +let isNewAppLaunch = true; /** * Merge the new log into the existing logs in Onyx * @param log the log to add */ function addLog(log: Log) { - Onyx.merge(ONYXKEYS.LOGS, { - [log.time.getTime()]: log, - }); + /** + * If this is the new app launch, we want to reset the log state in Onyx. + * This is because we don't want to keep logs from previous sessions and + * blow up the Onyx state. + */ + if (isNewAppLaunch) { + isNewAppLaunch = false; + Onyx.set(ONYXKEYS.LOGS, { + [log.time.getTime()]: log, + }); + } else { + Onyx.merge(ONYXKEYS.LOGS, { + [log.time.getTime()]: log, + }); + } } /** From eeb7f33d9b4ce2e5f8c6f8895266bc662848e6e6 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 24 Jul 2024 18:04:23 +0500 Subject: [PATCH 07/47] feat: Do not include request and response for Pusher authentication --- src/libs/Middleware/Logging.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/libs/Middleware/Logging.ts b/src/libs/Middleware/Logging.ts index f10e8d2f5120..d327dd06becc 100644 --- a/src/libs/Middleware/Logging.ts +++ b/src/libs/Middleware/Logging.ts @@ -32,15 +32,25 @@ function logRequestDetails(message: string, request: Request, response?: Respons logParams.requestID = response.requestID; } - Log.info(message, false, logParams); + const extraData: Record = {}; + /** + * We don't want to log the request and response data for AuthenticatePusher + * requests because they contain sensitive information. + */ + if (request.command !== 'AuthenticatePusher') { + extraData.request = request; + extraData.response = response; + } + + Log.info(message, false, logParams, false, extraData); } const Logging: Middleware = (response, request) => { const startTime = Date.now(); - logRequestDetails('Making API request', request); + logRequestDetails('[Network] Making API request', request); return response .then((data) => { - logRequestDetails(`Finished API request in ${Date.now() - startTime}ms`, request, data); + logRequestDetails(`[Network] Finished API request in ${Date.now() - startTime}ms`, request, data); return data; }) .catch((error: HttpsError) => { From fe66ee143bcdeede810359f5c4f4e224140d490e Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 24 Jul 2024 18:04:47 +0500 Subject: [PATCH 08/47] refactor: remove unused code --- src/libs/actions/OnyxUpdates.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/actions/OnyxUpdates.ts b/src/libs/actions/OnyxUpdates.ts index 6769fa7e64a3..672f325be58a 100644 --- a/src/libs/actions/OnyxUpdates.ts +++ b/src/libs/actions/OnyxUpdates.ts @@ -35,8 +35,6 @@ function applyHTTPSOnyxUpdates(request: Request, response: Response) { // apply successData or failureData. This ensures that we do not update any pending, loading, or other UI states contained // in successData/failureData until after the component has received and API data. const onyxDataUpdatePromise = response.onyxData ? updateHandler(response.onyxData) : Promise.resolve(); - Log.info('[OnyxUpdateManager]-[Network]-[Request]', false, request); - Log.info('[OnyxUpdateManager]-[Network]-[Response]', false, response); return onyxDataUpdatePromise .then(() => { // Handle the request's success/failure data (client-side data) From 46c3f7832638cfe40dadb42eeaab3bd7eb272aa8 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 25 Jul 2024 15:58:53 +0500 Subject: [PATCH 09/47] feat: flush all logs on each app launch --- src/libs/Log.ts | 13 +++++++------ src/libs/actions/Console.ts | 33 +++++++++++++++++---------------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/libs/Log.ts b/src/libs/Log.ts index eefe383034e1..72673b8d3f79 100644 --- a/src/libs/Log.ts +++ b/src/libs/Log.ts @@ -8,7 +8,7 @@ import type {Merge} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import pkg from '../../package.json'; -import {addLog} from './actions/Console'; +import {addLog, flushAllLogsOnAppLaunch} from './actions/Console'; import {shouldAttachLog} from './Console'; import getPlatform from './getPlatform'; import * as Network from './Network'; @@ -71,11 +71,12 @@ const Log = new Logger({ return; } - console.debug(message, extraData); - - if (shouldCollectLogs) { - addLog({time: new Date(), level: CONST.DEBUG_CONSOLE.LEVELS.DEBUG, message, extraData}); - } + flushAllLogsOnAppLaunch().then(() => { + console.debug(message, extraData); + if (shouldCollectLogs) { + addLog({time: new Date(), level: CONST.DEBUG_CONSOLE.LEVELS.DEBUG, message, extraData}); + } + }); }, isDebug: true, }); diff --git a/src/libs/actions/Console.ts b/src/libs/actions/Console.ts index e2c4047c43b5..6e585c6ad12d 100644 --- a/src/libs/actions/Console.ts +++ b/src/libs/actions/Console.ts @@ -8,21 +8,9 @@ let isNewAppLaunch = true; * @param log the log to add */ function addLog(log: Log) { - /** - * If this is the new app launch, we want to reset the log state in Onyx. - * This is because we don't want to keep logs from previous sessions and - * blow up the Onyx state. - */ - if (isNewAppLaunch) { - isNewAppLaunch = false; - Onyx.set(ONYXKEYS.LOGS, { - [log.time.getTime()]: log, - }); - } else { - Onyx.merge(ONYXKEYS.LOGS, { - [log.time.getTime()]: log, - }); - } + Onyx.merge(ONYXKEYS.LOGS, { + [log.time.getTime()]: log, + }); } /** @@ -41,4 +29,17 @@ function disableLoggingAndFlushLogs() { Onyx.set(ONYXKEYS.LOGS, null); } -export {addLog, setShouldStoreLogs, disableLoggingAndFlushLogs}; +/** + * Clears the persisted logs on app launch, + * so that we have fresh logs for the new app session. + */ +function flushAllLogsOnAppLaunch() { + if (!isNewAppLaunch) { + return Promise.resolve(); + } + + isNewAppLaunch = false; + return Onyx.set(ONYXKEYS.LOGS, {}); +} + +export {addLog, setShouldStoreLogs, disableLoggingAndFlushLogs, flushAllLogsOnAppLaunch}; From 50fb3dc6e55d4ed2f0335276a5671106605a3496 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 26 Jul 2024 15:05:07 +0500 Subject: [PATCH 10/47] feat: use correct filter icon and adjust the popup position for web and desktop --- assets/images/filter.svg | 11 +++++++++++ src/components/HeaderWithBackButton/index.tsx | 4 ++++ src/components/HeaderWithBackButton/types.ts | 6 ++++++ src/components/Icon/Expensicons.ts | 2 ++ src/pages/settings/AboutPage/ConsolePage.tsx | 6 +++++- 5 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 assets/images/filter.svg diff --git a/assets/images/filter.svg b/assets/images/filter.svg new file mode 100644 index 000000000000..9323573df12c --- /dev/null +++ b/assets/images/filter.svg @@ -0,0 +1,11 @@ + + + + + diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index 2d73e3c2dd24..f1e715bface8 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -53,6 +53,8 @@ function HeaderWithBackButton({ horizontal: 0, }, threeDotsMenuItems = [], + threeDotsMenuIcon, + threeDotsMenuIconFill, shouldEnableDetailPageNavigation = false, children = null, shouldOverlayDots = false, @@ -234,6 +236,8 @@ function HeaderWithBackButton({ {shouldShowPinButton && !!report && } {shouldShowThreeDotsButton && ( & { /** The anchor position of the menu */ threeDotsAnchorPosition?: AnchorPosition; + /** Icon displayed on the right of the title */ + threeDotsMenuIcon?: IconAsset; + + /** The fill color to pass into the icon. */ + threeDotsMenuIconFill?: string; + /** Whether we should show a close button */ shouldShowCloseButton?: boolean; diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 487df5594212..bd5fb88112fb 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -81,6 +81,7 @@ import ExpensifyLogoNew from '@assets/images/expensify-logo-new.svg'; import ExpensifyWordmark from '@assets/images/expensify-wordmark.svg'; import EyeDisabled from '@assets/images/eye-disabled.svg'; import Eye from '@assets/images/eye.svg'; +import Filter from '@assets/images/filter.svg'; import Flag from '@assets/images/flag.svg'; import FlagLevelOne from '@assets/images/flag_level_01.svg'; import FlagLevelTwo from '@assets/images/flag_level_02.svg'; @@ -372,4 +373,5 @@ export { CheckCircle, CheckmarkCircle, NetSuiteSquare, + Filter, }; diff --git a/src/pages/settings/AboutPage/ConsolePage.tsx b/src/pages/settings/AboutPage/ConsolePage.tsx index 97961138b280..eb9b13608039 100644 --- a/src/pages/settings/AboutPage/ConsolePage.tsx +++ b/src/pages/settings/AboutPage/ConsolePage.tsx @@ -19,6 +19,7 @@ import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import {addLog} from '@libs/actions/Console'; import {createLog, parseStringifiedMessages, sanitizeConsoleInput} from '@libs/Console'; import type {Log} from '@libs/Console'; @@ -56,7 +57,7 @@ function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); - + const {windowWidth} = useWindowDimensions(); const route = useRoute>(); const menuItems: PopoverMenuItem[] = useMemo( @@ -164,6 +165,9 @@ function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) { onBackButtonPress={() => Navigation.goBack(route.params?.backTo)} shouldShowThreeDotsButton threeDotsMenuItems={menuItems} + threeDotsAnchorPosition={styles.threeDotsPopoverOffset(windowWidth)} + threeDotsMenuIcon={Expensicons.Filter} + threeDotsMenuIconFill={theme.icon} /> Date: Sat, 27 Jul 2024 01:19:14 +0900 Subject: [PATCH 11/47] fix: null check instead of setting an empty value --- src/components/ButtonWithDropdownMenu/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index 094c26a2b387..8096ff8fe4d1 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -139,7 +139,12 @@ function ButtonWithDropdownMenu({ onClose={() => setIsMenuVisible(false)} onItemSelected={() => setIsMenuVisible(false)} anchorPosition={popoverAnchorPosition} - anchorRef={caretButton} + anchorRef={() => { + if (caretButton === null) { + return; + } + return caretButton; + }} withoutOverlay anchorAlignment={anchorAlignment} headerText={menuHeaderText} From 64c133933c2bdf26a77146be3d3c0205b3d1b64a Mon Sep 17 00:00:00 2001 From: jacobkim9881 Date: Sat, 27 Jul 2024 02:35:19 +0900 Subject: [PATCH 12/47] fix: type check failed --- src/components/ButtonWithDropdownMenu/index.tsx | 2 +- src/components/PopoverMenu.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index 8096ff8fe4d1..63be95626420 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -141,7 +141,7 @@ function ButtonWithDropdownMenu({ anchorPosition={popoverAnchorPosition} anchorRef={() => { if (caretButton === null) { - return; + return null; } return caretButton; }} diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index bcec153491c9..34d988cc5b58 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -61,7 +61,7 @@ type PopoverMenuProps = Partial & { anchorPosition: AnchorPosition; /** Ref of the anchor */ - anchorRef: RefObject; + anchorRef: RefObject; /** Where the popover should be positioned relative to the anchor points. */ anchorAlignment?: AnchorAlignment; From ceb7b4152cec0125957cc63b6894d50eb611a37b Mon Sep 17 00:00:00 2001 From: jacobkim9881 Date: Sat, 27 Jul 2024 02:55:38 +0900 Subject: [PATCH 13/47] canceled null check --- src/components/ButtonWithDropdownMenu/index.tsx | 2 +- src/components/PopoverMenu.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index 8096ff8fe4d1..63be95626420 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -141,7 +141,7 @@ function ButtonWithDropdownMenu({ anchorPosition={popoverAnchorPosition} anchorRef={() => { if (caretButton === null) { - return; + return null; } return caretButton; }} diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index bcec153491c9..34d988cc5b58 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -61,7 +61,7 @@ type PopoverMenuProps = Partial & { anchorPosition: AnchorPosition; /** Ref of the anchor */ - anchorRef: RefObject; + anchorRef: RefObject; /** Where the popover should be positioned relative to the anchor points. */ anchorAlignment?: AnchorAlignment; From a52a9975698ca43d3ad8c06bead7b4700f8280c8 Mon Sep 17 00:00:00 2001 From: jacobkim9881 Date: Sat, 27 Jul 2024 03:03:59 +0900 Subject: [PATCH 14/47] canceled null check --- src/components/ButtonWithDropdownMenu/index.tsx | 2 +- src/components/PopoverMenu.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index 63be95626420..8096ff8fe4d1 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -141,7 +141,7 @@ function ButtonWithDropdownMenu({ anchorPosition={popoverAnchorPosition} anchorRef={() => { if (caretButton === null) { - return null; + return; } return caretButton; }} diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index 34d988cc5b58..bcec153491c9 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -61,7 +61,7 @@ type PopoverMenuProps = Partial & { anchorPosition: AnchorPosition; /** Ref of the anchor */ - anchorRef: RefObject; + anchorRef: RefObject; /** Where the popover should be positioned relative to the anchor points. */ anchorAlignment?: AnchorAlignment; From 89d1ae18e01d3e32c24f3d606e5a0c2ca508ec11 Mon Sep 17 00:00:00 2001 From: jacobkim9881 Date: Sun, 28 Jul 2024 10:00:42 +0900 Subject: [PATCH 15/47] fix typecheck, lint not passed --- src/components/ButtonWithDropdownMenu/index.tsx | 9 +++------ src/types/utils/nullCheckRef.ts | 11 +++++++++++ 2 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 src/types/utils/nullCheckRef.ts diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index 8096ff8fe4d1..379458db0729 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -10,6 +10,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import CONST from '@src/CONST'; import type {AnchorPosition} from '@src/styles'; +import nullCheckRef from '@src/types/utils/nullCheckRef'; import type {ButtonWithDropdownMenuProps} from './types'; function ButtonWithDropdownMenu({ @@ -42,6 +43,7 @@ function ButtonWithDropdownMenu({ const [popoverAnchorPosition, setPopoverAnchorPosition] = useState(null); const {windowWidth, windowHeight} = useWindowDimensions(); const caretButton = useRef(null); + const nullRef = useRef(null); const selectedItem = options[selectedItemIndex] || options[0]; const innerStyleDropButton = StyleUtils.getDropDownButtonHeight(buttonSize); const isButtonSizeLarge = buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE; @@ -139,12 +141,7 @@ function ButtonWithDropdownMenu({ onClose={() => setIsMenuVisible(false)} onItemSelected={() => setIsMenuVisible(false)} anchorPosition={popoverAnchorPosition} - anchorRef={() => { - if (caretButton === null) { - return; - } - return caretButton; - }} + anchorRef={nullCheckRef(caretButton, nullRef)} withoutOverlay anchorAlignment={anchorAlignment} headerText={menuHeaderText} diff --git a/src/types/utils/nullCheckRef.ts b/src/types/utils/nullCheckRef.ts new file mode 100644 index 000000000000..f43db5d8e922 --- /dev/null +++ b/src/types/utils/nullCheckRef.ts @@ -0,0 +1,11 @@ +import type {MutableRefObject} from 'react'; +import type {View} from 'react-native'; + +function nullCheckRef(ref: MutableRefObject, nullRef: MutableRefObject): MutableRefObject { + if (ref === null) { + return nullRef; + } + return ref; +} + +export default nullCheckRef; From e1404d0afc7a60eed85605d9a4378844ebd91585 Mon Sep 17 00:00:00 2001 From: jacobkim9881 Date: Sun, 28 Jul 2024 14:38:27 +0900 Subject: [PATCH 16/47] fix added null check in the script --- src/components/ButtonWithDropdownMenu/index.tsx | 6 +++--- src/types/utils/nullCheckRef.ts | 11 ----------- 2 files changed, 3 insertions(+), 14 deletions(-) delete mode 100644 src/types/utils/nullCheckRef.ts diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index 379458db0729..5e65875a7e40 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -1,3 +1,4 @@ +import type {MutableRefObject} from 'react'; import React, {useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import Button from '@components/Button'; @@ -10,7 +11,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import CONST from '@src/CONST'; import type {AnchorPosition} from '@src/styles'; -import nullCheckRef from '@src/types/utils/nullCheckRef'; import type {ButtonWithDropdownMenuProps} from './types'; function ButtonWithDropdownMenu({ @@ -43,10 +43,10 @@ function ButtonWithDropdownMenu({ const [popoverAnchorPosition, setPopoverAnchorPosition] = useState(null); const {windowWidth, windowHeight} = useWindowDimensions(); const caretButton = useRef(null); - const nullRef = useRef(null); const selectedItem = options[selectedItemIndex] || options[0]; const innerStyleDropButton = StyleUtils.getDropDownButtonHeight(buttonSize); const isButtonSizeLarge = buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE; + const nullCheckRef = (ref: MutableRefObject) => ref ?? null; useEffect(() => { if (!caretButton.current) { @@ -141,7 +141,7 @@ function ButtonWithDropdownMenu({ onClose={() => setIsMenuVisible(false)} onItemSelected={() => setIsMenuVisible(false)} anchorPosition={popoverAnchorPosition} - anchorRef={nullCheckRef(caretButton, nullRef)} + anchorRef={nullCheckRef(caretButton)} withoutOverlay anchorAlignment={anchorAlignment} headerText={menuHeaderText} diff --git a/src/types/utils/nullCheckRef.ts b/src/types/utils/nullCheckRef.ts deleted file mode 100644 index f43db5d8e922..000000000000 --- a/src/types/utils/nullCheckRef.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type {MutableRefObject} from 'react'; -import type {View} from 'react-native'; - -function nullCheckRef(ref: MutableRefObject, nullRef: MutableRefObject): MutableRefObject { - if (ref === null) { - return nullRef; - } - return ref; -} - -export default nullCheckRef; From 5609006d769bebccf4d632d251b88f1060941e93 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 30 Jul 2024 15:37:07 +0500 Subject: [PATCH 17/47] chore: bump expensify-common --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index beea26ff027a..95a2888f1706 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,7 +55,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.49", + "expensify-common": "2.0.60", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -25759,9 +25759,9 @@ } }, "node_modules/expensify-common": { - "version": "2.0.49", - "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.49.tgz", - "integrity": "sha512-67QbRuR2XEl2RoNLSbyqGWATIbOXPV42azAfs2sqNT6iyWKcOgHUqRkWPhxA0GmSW35lwq66bvgPVsQUfMGCow==", + "version": "2.0.60", + "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.60.tgz", + "integrity": "sha512-s09uSewbOpPFwqvkw/sQA/T30joE6nAhQSWu1I6elUxIp0evw/Gj7IKTU+1F3vWr3u9N+KlZgCn5YJex9us2yw==", "dependencies": { "awesome-phonenumber": "^5.4.0", "classnames": "2.5.0", diff --git a/package.json b/package.json index 896d6695cca0..49b31fe7d401 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.49", + "expensify-common": "2.0.60", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", From 7d342ce703ff35ce7ea4652dad85219a5753702c Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Tue, 30 Jul 2024 12:38:04 +0200 Subject: [PATCH 18/47] map empty entity to top level --- src/languages/en.ts | 1 + .../accounting/PolicyAccountingPage.tsx | 2 +- .../intacct/SageIntacctEntityPage.tsx | 21 ++++++++++++------- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 32b9a9eff2b6..a8ef5452af18 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2077,6 +2077,7 @@ export default { letsDoubleCheck: "Let's double check that everything looks right.", lineItemLevel: 'Line-item level', reportLevel: 'Report level', + topLevel: 'Top level', appliedOnExport: 'Not imported into Expensify, applied on export', shareNote: { header: 'Easily share your workspace with other members.', diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index 89bbfd75d538..46a8b53e3fc4 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -276,7 +276,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { : { description: translate('workspace.intacct.entity'), iconRight: Expensicons.ArrowRight, - title: getCurrentSageIntacctEntityName(policy), + title: getCurrentSageIntacctEntityName(policy) ?? translate('workspace.common.topLevel'), wrapperStyle: [styles.sectionMenuItemTopDescription], titleStyle: styles.fontWeightNormal, shouldShowRightIcon: true, diff --git a/src/pages/workspace/accounting/intacct/SageIntacctEntityPage.tsx b/src/pages/workspace/accounting/intacct/SageIntacctEntityPage.tsx index abdea52d0df9..837a9d900cd2 100644 --- a/src/pages/workspace/accounting/intacct/SageIntacctEntityPage.tsx +++ b/src/pages/workspace/accounting/intacct/SageIntacctEntityPage.tsx @@ -2,6 +2,7 @@ import React from 'react'; import RadioListItem from '@components/SelectionList/RadioListItem'; import type {ListItem} from '@components/SelectionList/types'; import SelectionScreen from '@components/SelectionScreen'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import {clearSageIntacctErrorField, updateSageIntacctEntity} from '@libs/actions/connections/SageIntacct'; import * as ErrorUtils from '@libs/ErrorUtils'; @@ -14,22 +15,28 @@ function SageIntacctEntityPage({policy}: WithPolicyProps) { const styles = useThemeStyles(); const config = policy?.connections?.intacct?.config; const entityID = config?.entity ?? ''; + const {translate} = useLocalize(); const policyID = policy?.id ?? '-1'; - const sections = - policy?.connections?.intacct?.data?.entities.map((entity) => ({ + const sections = [ + { + text: translate('workspace.common.topLevel'), + value: translate('workspace.common.topLevel'), + keyForList: '', + isSelected: entityID === '', + }, + ]; + policy?.connections?.intacct?.data?.entities.forEach((entity) => { + sections.push({ text: entity.name, value: entity.name, keyForList: entity.id, isSelected: entity.id === entityID, - })) ?? []; + }); + }); const saveSelection = ({keyForList}: ListItem) => { - if (!keyForList) { - return; - } - updateSageIntacctEntity(policyID, keyForList ?? ''); Navigation.goBack(); }; From 90b3b9df920026aeaf1fdd9934ba591246b8f405 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Tue, 30 Jul 2024 12:46:10 +0200 Subject: [PATCH 19/47] add intacctImportDimensions translation --- src/languages/en.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index a8ef5452af18..9272633a2fb0 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3205,6 +3205,8 @@ export default { return 'Marking NetSuite bills and invoices as paid'; case 'intacctCheckConnection': return 'Checking Sage Intacct connection'; + case 'intacctImportDimensions': + return 'Importing Sage Intacct dimensions'; case 'intacctImportTitle': return 'Importing Sage Intacct data'; default: { From 70d4ae2aebf89fce2c86252759e7ca718d30eb0e Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Tue, 30 Jul 2024 13:06:24 +0200 Subject: [PATCH 20/47] add spanish translation --- src/languages/es.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/languages/es.ts b/src/languages/es.ts index 1636512a6fa4..4b49251b228d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2125,6 +2125,7 @@ export default { reuseExistingConnection: 'Reutilizar la conexión existente', existingConnections: 'Conexiones existentes', lastSyncDate: (connectionName: string, formattedDate: string) => `${connectionName} - Última sincronización ${formattedDate}`, + topLevel: 'Nivel superior', }, qbo: { importDescription: 'Elige que configuraciónes de codificación son importadas desde QuickBooks Online a Expensify.', From 5913244652b5637e5bc951880cd7b245b7591f79 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Tue, 30 Jul 2024 15:24:11 +0200 Subject: [PATCH 21/47] make employee default option only viable for departments, classes and locations mappings --- .../import/SageIntacctMappingsTypePage.tsx | 53 +++++++++++-------- .../import/SageIntacctToggleMappingsPage.tsx | 2 +- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/pages/workspace/accounting/intacct/import/SageIntacctMappingsTypePage.tsx b/src/pages/workspace/accounting/intacct/import/SageIntacctMappingsTypePage.tsx index 57ebac617393..7e0decc5243e 100644 --- a/src/pages/workspace/accounting/intacct/import/SageIntacctMappingsTypePage.tsx +++ b/src/pages/workspace/accounting/intacct/import/SageIntacctMappingsTypePage.tsx @@ -23,33 +23,44 @@ function SageIntacctMappingsTypePage({route}: SageIntacctMappingsTypePageProps) const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID ?? '-1'}`); const policyID = policy?.id ?? '-1'; const mappings = policy?.connections?.intacct?.config?.mappings; + const exportConfig = policy?.connections?.intacct?.config?.export; - const selectionOptions = useMemo( - () => [ - { + const selectionOptions = useMemo(() => { + const mappingOptions: SelectorType[] = []; + if ( + mappingName !== CONST.SAGE_INTACCT_CONFIG.MAPPINGS.CUSTOMERS && + mappingName !== CONST.SAGE_INTACCT_CONFIG.MAPPINGS.PROJECTS && + exportConfig?.reimbursable !== CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL + ) { + mappingOptions.push({ value: CONST.SAGE_INTACCT_MAPPING_VALUE.DEFAULT, text: translate('workspace.intacct.employeeDefault'), alternateText: translate('workspace.common.appliedOnExport'), keyForList: CONST.SAGE_INTACCT_MAPPING_VALUE.DEFAULT, isSelected: mappings?.[mappingName] === CONST.SAGE_INTACCT_MAPPING_VALUE.DEFAULT, - }, - { - value: CONST.SAGE_INTACCT_MAPPING_VALUE.TAG, - text: translate('workspace.common.tags'), - alternateText: translate('workspace.common.lineItemLevel'), - keyForList: CONST.SAGE_INTACCT_MAPPING_VALUE.TAG, - isSelected: mappings?.[mappingName] === CONST.SAGE_INTACCT_MAPPING_VALUE.TAG, - }, - { - value: CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD, - text: translate('workspace.common.reportFields'), - alternateText: translate('workspace.common.reportLevel'), - keyForList: CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD, - isSelected: mappings?.[mappingName] === CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD, - }, - ], - [mappingName, mappings, translate], - ); + }); + } + mappingOptions.push( + ...[ + { + value: CONST.SAGE_INTACCT_MAPPING_VALUE.TAG, + text: translate('workspace.common.tags'), + alternateText: translate('workspace.common.lineItemLevel'), + keyForList: CONST.SAGE_INTACCT_MAPPING_VALUE.TAG, + isSelected: mappings?.[mappingName] === CONST.SAGE_INTACCT_MAPPING_VALUE.TAG, + }, + { + value: CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD, + text: translate('workspace.common.reportFields'), + alternateText: translate('workspace.common.reportLevel'), + keyForList: CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD, + isSelected: mappings?.[mappingName] === CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD, + }, + ], + ); + + return mappingOptions; + }, [exportConfig?.reimbursable, mappingName, mappings, translate]); const updateMapping = useCallback( ({value}: SelectorType) => { diff --git a/src/pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage.tsx b/src/pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage.tsx index c76a9f0e26bc..1d43f3431db4 100644 --- a/src/pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage.tsx +++ b/src/pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage.tsx @@ -92,7 +92,7 @@ function SageIntacctToggleMappingsPage({route}: SageIntacctToggleMappingsPagePro updateSageIntacctMappingValue(policyID, mappingName, CONST.SAGE_INTACCT_MAPPING_VALUE.NONE); } else { setImportMapping(true); - updateSageIntacctMappingValue(policyID, mappingName, CONST.SAGE_INTACCT_MAPPING_VALUE.DEFAULT); + updateSageIntacctMappingValue(policyID, mappingName, CONST.SAGE_INTACCT_MAPPING_VALUE.TAG); } }} /> From 8d197bf783332e5cdfaa0c80a99aec4bf804ebd4 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Tue, 30 Jul 2024 16:32:26 +0200 Subject: [PATCH 22/47] change mapping value to tag when setting reimbrusaible reports to VENDOR_BILL --- src/libs/actions/connections/SageIntacct.ts | 13 +++++++++++++ .../export/SageIntacctReimbursableExpensesPage.tsx | 6 ++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/connections/SageIntacct.ts b/src/libs/actions/connections/SageIntacct.ts index 5ba12173c4e2..9e6dff071699 100644 --- a/src/libs/actions/connections/SageIntacct.ts +++ b/src/libs/actions/connections/SageIntacct.ts @@ -142,6 +142,18 @@ function updateSageIntacctMappingValue(policyID: string, mappingName: SageIntacc ); } +function changeMappingsValueFromDefaultToTag(policyID: string, mappings?: SageIntacctMappingType) { + if (mappings?.departments === CONST.SAGE_INTACCT_MAPPING_VALUE.DEFAULT) { + updateSageIntacctMappingValue(policyID, CONST.SAGE_INTACCT_CONFIG.MAPPINGS.DEPARTMENTS, CONST.SAGE_INTACCT_MAPPING_VALUE.TAG); + } + if (mappings?.classes === CONST.SAGE_INTACCT_MAPPING_VALUE.DEFAULT) { + updateSageIntacctMappingValue(policyID, CONST.SAGE_INTACCT_CONFIG.MAPPINGS.CLASSES, CONST.SAGE_INTACCT_MAPPING_VALUE.TAG); + } + if (mappings?.locations === CONST.SAGE_INTACCT_MAPPING_VALUE.DEFAULT) { + updateSageIntacctMappingValue(policyID, CONST.SAGE_INTACCT_CONFIG.MAPPINGS.LOCATIONS, CONST.SAGE_INTACCT_MAPPING_VALUE.TAG); + } +} + function updateSageIntacctSyncTaxConfiguration(policyID: string, enabled: boolean) { const optimisticData: OnyxUpdate[] = [ { @@ -769,4 +781,5 @@ export { updateSageIntacctSyncReimbursedReports, updateSageIntacctSyncReimbursementAccountID, updateSageIntacctEntity, + changeMappingsValueFromDefaultToTag, }; diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx index 7ade41b47a9a..c42a8888070d 100644 --- a/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx @@ -15,7 +15,7 @@ import Navigation from '@navigation/Navigation'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; -import {updateSageIntacctDefaultVendor, updateSageIntacctReimbursableExpensesExportDestination} from '@userActions/connections/SageIntacct'; +import {changeMappingsValueFromDefaultToTag, updateSageIntacctDefaultVendor, updateSageIntacctReimbursableExpensesExportDestination} from '@userActions/connections/SageIntacct'; import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -49,10 +49,12 @@ function SageIntacctReimbursableExpensesPage({policy}: WithPolicyProps) { updateSageIntacctReimbursableExpensesExportDestination(policyID, row.value); } if (row.value === CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL) { + // Employee default mapping value is not allowed when expense type is VENDOR_BILL, so we have to change mapping value to Tag + changeMappingsValueFromDefaultToTag(policyID, config?.mappings); Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.getRoute(policyID)); } }, - [reimbursable, policyID], + [reimbursable, policyID, config?.mappings], ); const defaultVendor = useMemo(() => { From 14266a2c8c656514ecd73c3d3025f60a832b1232 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 30 Jul 2024 17:13:36 +0200 Subject: [PATCH 23/47] Implement selection list for selecting approver --- src/languages/en.ts | 3 +- src/languages/es.ts | 3 +- src/libs/Navigation/types.ts | 1 + src/libs/actions/Policy/Policy.ts | 2 +- ...orkspaceWorkflowsApprovalsApproverPage.tsx | 210 +++++++++++++++++- src/types/onyx/ApprovalWorkflow.ts | 4 +- 6 files changed, 212 insertions(+), 11 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 6fdd5bbf3ab1..0dfe546c89e8 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1288,8 +1288,9 @@ export default { title: 'Expenses from', header: 'When the following members submit expenses:', }, - workflowsApprovalPage: { + workflowsApproverPage: { genericErrorMessage: "The approver couldn't be changed. Please try again or contact support.", + header: 'Send to this member for approval:', }, workflowsPayerPage: { title: 'Authorized payer', diff --git a/src/languages/es.ts b/src/languages/es.ts index f918da0b140f..148d9cef4c8d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1297,8 +1297,9 @@ export default { title: 'Gastos de', header: 'Cuando los siguientes miembros presenten gastos:', }, - workflowsApprovalPage: { + workflowsApproverPage: { genericErrorMessage: 'El aprobador no pudo ser cambiado. Por favor, inténtelo de nuevo o contacte al soporte.', + header: 'Enviar a este miembro para su aprobación:', }, workflowsPayerPage: { title: 'Pagador autorizado', diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 9a7aafb1963c..f55968b80971 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1119,6 +1119,7 @@ type FullScreenNavigatorParamList = { }; [SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_APPROVER]: { policyID: string; + approverIndex?: number; }; [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: { policyID: string; diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 1bce525e22bf..be6dd445e89a 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -482,7 +482,7 @@ function setWorkspaceApprovalMode(policyID: string, approver: string, approvalMo approver: policy?.approver, approvalMode: policy?.approvalMode, pendingFields: {approvalMode: null}, - errorFields: {approvalMode: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workflowsApprovalPage.genericErrorMessage')}, + errorFields: {approvalMode: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workflowsApproverPage.genericErrorMessage')}, }, }, ]; diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx index 8e9005c46587..11c7bb6510a6 100644 --- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx +++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx @@ -1,17 +1,24 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import type {SectionListData} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import Badge from '@components/Badge'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {FallbackAvatar} from '@components/Icon/Expensicons'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; +import InviteMemberListItem from '@components/SelectionList/InviteMemberListItem'; import type {ListItem, Section} from '@components/SelectionList/types'; import UserListItem from '@components/SelectionList/UserListItem'; +import Text from '@components/Text'; +import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; @@ -23,23 +30,208 @@ import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import * as Policy from '@userActions/Policy/Policy'; +import * as Workflow from '@userActions/Workflow'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {PersonalDetailsList, PolicyEmployee} from '@src/types/onyx'; +import type {Beta, PolicyEmployee} from '@src/types/onyx'; +import type {Icon} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; type WorkspaceWorkflowsApprovalsApproverPageOnyxProps = { - /** All of the personal details for everyone */ - personalDetails: OnyxEntry; + /** Beta features list */ + // eslint-disable-next-line react/no-unused-prop-types -- This prop is used in the component + betas: OnyxEntry; }; type WorkspaceWorkflowsApprovalsApproverPageProps = WorkspaceWorkflowsApprovalsApproverPageOnyxProps & WithPolicyAndFullscreenLoadingProps & StackScreenProps; + +type SelectionListApprover = { + text: string; + alternateText: string; + keyForList: string; + isSelected: boolean; + login: string; + rightElement?: React.ReactNode; + icons: Icon[]; +}; +type ApproverSection = SectionListData>; + +function WorkspaceWorkflowsApprovalsApproverPage(props: WorkspaceWorkflowsApprovalsApproverPageProps) { + if (props.betas?.includes(CONST.BETAS.WORKFLOWS_ADVANCED_APPROVAL)) { + // eslint-disable-next-line react/jsx-props-no-spreading + return ; + } + + // eslint-disable-next-line react/jsx-props-no-spreading + return ; +} + +function WorkspaceWorkflowsApprovalsApproverPageNew({policy, personalDetails, isLoadingReportData = true, route}: WorkspaceWorkflowsApprovalsApproverPageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); + const [approvalWorkflow, approvalWorkflowMetadata] = useOnyx(ONYXKEYS.APPROVAL_WORKFLOW); + const [selectedApproverEmail, setSelectedApproverEmail] = useState(undefined); + + // eslint-disable-next-line rulesdir/no-negated-variables + const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy); + const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList); + const approverIndex = route.params.approverIndex ?? 0; + + useEffect(() => { + const currentApprover = approvalWorkflow?.approvers[approverIndex]; + if (!currentApprover) { + return; + } + + setSelectedApproverEmail(currentApprover.email); + }, [approvalWorkflow?.approvers, approverIndex]); + + const sections: ApproverSection[] = useMemo(() => { + const approvers: SelectionListApprover[] = []; + + if (policy?.employeeList) { + const availableApprovers = Object.values(policy.employeeList) + .map((employee): SelectionListApprover | null => { + const isAdmin = employee?.role === CONST.REPORT.ROLE.ADMIN; + const email = employee.email; + + if (!email) { + return null; + } + + const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? ''); + const {avatar, displayName = email} = personalDetails?.[accountID] ?? {}; + + return { + text: displayName, + alternateText: email, + keyForList: email, + isSelected: selectedApproverEmail === email, + login: email, + icons: [{source: avatar ?? FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, name: displayName, id: accountID}], + rightElement: isAdmin ? : undefined, + }; + }) + .filter((approver): approver is SelectionListApprover => !!approver); + + approvers.push(...availableApprovers); + } + + const filteredApprovers = + debouncedSearchTerm !== '' + ? approvers.filter((option) => { + const searchValue = OptionsListUtils.getSearchValueForPhoneOrEmail(debouncedSearchTerm); + const isPartOfSearchTerm = !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue); + return isPartOfSearchTerm; + }) + : approvers; + + return [ + { + title: undefined, + data: filteredApprovers, + shouldShow: true, + }, + ]; + }, [debouncedSearchTerm, personalDetails, policy?.employeeList, policyMemberEmailsToAccountIDs, selectedApproverEmail, translate]); + + const nextStep = useCallback(() => { + if (!selectedApproverEmail) { + return; + } + + const accountID = Number(policyMemberEmailsToAccountIDs[selectedApproverEmail] ?? ''); + const {avatar, displayName = selectedApproverEmail} = personalDetails?.[accountID] ?? {}; + Workflow.setApprovalWorkflowApprover( + { + email: selectedApproverEmail, + avatar, + displayName, + isInMultipleWorkflows: false, + isCircularReference: false, + }, + approverIndex, + ); + + const firstApprover = approvalWorkflow?.approvers?.[0]?.email ?? ''; + Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(route.params.policyID, firstApprover)); + }, [approvalWorkflow?.approvers, approverIndex, personalDetails, policyMemberEmailsToAccountIDs, route.params.policyID, selectedApproverEmail]); + + const nextButton = useMemo( + () => ( + + ), + [nextStep, selectedApproverEmail, styles.flexBasisAuto, styles.flexGrow0, styles.flexReset, styles.flexShrink0, translate], + ); + + const toggleApprover = (approver: SelectionListApprover) => { + if (selectedApproverEmail === approver.login) { + setSelectedApproverEmail(undefined); + return; + } + setSelectedApproverEmail(approver.login); + }; + + const headerMessage = useMemo(() => (searchTerm && !sections[0].data.length ? translate('common.noResultsFound') : ''), [searchTerm, sections, translate]); + + return ( + + setDidScreenTransitionEnd(true)} + > + + + {translate('workflowsApproverPage.header')} + + + + + ); +} + type MemberOption = Omit & {accountID: number}; type MembersSection = SectionListData>; -function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoadingReportData = true, route}: WorkspaceWorkflowsApprovalsApproverPageProps) { +// TODO: Remove this component once the new workflow is enabled for all users +function WorkspaceWorkflowsApprovalsApproverPageOld({policy, personalDetails, isLoadingReportData = true, route}: WorkspaceWorkflowsApprovalsApproverPageProps) { const {translate} = useLocalize(); const policyName = policy?.name ?? ''; const [searchTerm, setSearchTerm] = useState(''); @@ -196,4 +388,10 @@ function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoa WorkspaceWorkflowsApprovalsApproverPage.displayName = 'WorkspaceWorkflowsApprovalsApproverPage'; -export default withPolicyAndFullscreenLoading(WorkspaceWorkflowsApprovalsApproverPage); +export default withPolicyAndFullscreenLoading( + withOnyx({ + betas: { + key: ONYXKEYS.BETAS, + }, + })(WorkspaceWorkflowsApprovalsApproverPage), +); diff --git a/src/types/onyx/ApprovalWorkflow.ts b/src/types/onyx/ApprovalWorkflow.ts index 70919ada9ddb..b5bce8037fdc 100644 --- a/src/types/onyx/ApprovalWorkflow.ts +++ b/src/types/onyx/ApprovalWorkflow.ts @@ -22,12 +22,12 @@ type Approver = { /** * Display name of the current user from their personal details */ - displayName?: string; + displayName: string; /** * Is this user used as an approver in more than one workflow (used to show a warning) */ - isInMultipleWorkflows: boolean; + isInMultipleWorkflows?: boolean; /** * Is this approver in a circular reference (approver forwards to themselves, or a cycle of forwards) From 7d3b2fe40893604f758d001b925598f9d8dde4e3 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 30 Jul 2024 17:45:33 +0200 Subject: [PATCH 24/47] Fix couple of bugs --- src/ROUTES.ts | 3 ++- .../WorkspaceWorkflowsApprovalsApproverPage.tsx | 16 +++++++++++----- ...rkspaceWorkflowsApprovalsExpensesFromPage.tsx | 2 +- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 0811ea02e9d6..3fa16f24be84 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -625,7 +625,8 @@ const ROUTES = { }, WORKSPACE_WORKFLOWS_APPROVALS_APPROVER: { route: 'settings/workspaces/:policyID/workflows/approvals/approver', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/workflows/approvals/approver` as const, + getRoute: (policyID: string, approverIndex?: number) => + `settings/workspaces/${policyID}/workflows/approvals/approver${approverIndex !== undefined ? `?approverIndex=${approverIndex}` : ''}` as const, }, WORKSPACE_WORKFLOWS_PAYER: { route: 'settings/workspaces/:policyID/workflows/payer', diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx index 11c7bb6510a6..e164ebf5de13 100644 --- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx +++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx @@ -24,6 +24,7 @@ import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import Permissions from '@libs/Permissions'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; @@ -61,7 +62,7 @@ type SelectionListApprover = { type ApproverSection = SectionListData>; function WorkspaceWorkflowsApprovalsApproverPage(props: WorkspaceWorkflowsApprovalsApproverPageProps) { - if (props.betas?.includes(CONST.BETAS.WORKFLOWS_ADVANCED_APPROVAL)) { + if (Permissions.canUseWorkflowsAdvancedApproval(props.betas) && props.route.params.approverIndex !== undefined) { // eslint-disable-next-line react/jsx-props-no-spreading return ; } @@ -80,7 +81,6 @@ function WorkspaceWorkflowsApprovalsApproverPageNew({policy, personalDetails, is // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy); - const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList); const approverIndex = route.params.approverIndex ?? 0; useEffect(() => { @@ -105,6 +105,7 @@ function WorkspaceWorkflowsApprovalsApproverPageNew({policy, personalDetails, is return null; } + const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList); const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? ''); const {avatar, displayName = email} = personalDetails?.[accountID] ?? {}; @@ -139,13 +140,14 @@ function WorkspaceWorkflowsApprovalsApproverPageNew({policy, personalDetails, is shouldShow: true, }, ]; - }, [debouncedSearchTerm, personalDetails, policy?.employeeList, policyMemberEmailsToAccountIDs, selectedApproverEmail, translate]); + }, [debouncedSearchTerm, personalDetails, policy?.employeeList, selectedApproverEmail, translate]); const nextStep = useCallback(() => { if (!selectedApproverEmail) { return; } + const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList); const accountID = Number(policyMemberEmailsToAccountIDs[selectedApproverEmail] ?? ''); const {avatar, displayName = selectedApproverEmail} = personalDetails?.[accountID] ?? {}; Workflow.setApprovalWorkflowApprover( @@ -160,8 +162,12 @@ function WorkspaceWorkflowsApprovalsApproverPageNew({policy, personalDetails, is ); const firstApprover = approvalWorkflow?.approvers?.[0]?.email ?? ''; - Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(route.params.policyID, firstApprover)); - }, [approvalWorkflow?.approvers, approverIndex, personalDetails, policyMemberEmailsToAccountIDs, route.params.policyID, selectedApproverEmail]); + if (!approvalWorkflow?.isBeingEdited && firstApprover) { + Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(route.params.policyID, firstApprover)); + } else { + Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(route.params.policyID)); + } + }, [approvalWorkflow?.approvers, approvalWorkflow?.isBeingEdited, approverIndex, personalDetails, policy?.employeeList, route.params.policyID, selectedApproverEmail]); const nextButton = useMemo( () => ( diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage.tsx index 13ccace08132..443b51387a93 100644 --- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage.tsx +++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage.tsx @@ -131,7 +131,7 @@ function WorkspaceWorkflowsApprovalsExpensesFromPage({policy, isLoadingReportDat if (approvalWorkflow?.isBeingEdited && firstApprover) { Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(route.params.policyID, firstApprover)); } else { - Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_APPROVER.getRoute(route.params.policyID)); + Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_APPROVER.getRoute(route.params.policyID, 0)); } }, [approvalWorkflow?.approvers, approvalWorkflow?.isBeingEdited, route.params.policyID, selectedMembers]); From 6900273fea8a8d0b0faac30c58193b5d9d15c0fd Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Wed, 31 Jul 2024 10:06:49 +0200 Subject: [PATCH 25/47] fix lint --- src/languages/en.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index 9272633a2fb0..c24d33c5b878 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3210,6 +3210,7 @@ export default { case 'intacctImportTitle': return 'Importing Sage Intacct data'; default: { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions return `Translation missing for stage: ${stage}`; } } From 5ce5a41fe5085aaac778456a8ed959013151ea94 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Wed, 31 Jul 2024 10:52:16 +0200 Subject: [PATCH 26/47] fix PR comments --- .../accounting/intacct/import/SageIntacctMappingsTypePage.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/workspace/accounting/intacct/import/SageIntacctMappingsTypePage.tsx b/src/pages/workspace/accounting/intacct/import/SageIntacctMappingsTypePage.tsx index 7e0decc5243e..4f3de5390cea 100644 --- a/src/pages/workspace/accounting/intacct/import/SageIntacctMappingsTypePage.tsx +++ b/src/pages/workspace/accounting/intacct/import/SageIntacctMappingsTypePage.tsx @@ -28,8 +28,7 @@ function SageIntacctMappingsTypePage({route}: SageIntacctMappingsTypePageProps) const selectionOptions = useMemo(() => { const mappingOptions: SelectorType[] = []; if ( - mappingName !== CONST.SAGE_INTACCT_CONFIG.MAPPINGS.CUSTOMERS && - mappingName !== CONST.SAGE_INTACCT_CONFIG.MAPPINGS.PROJECTS && + !([CONST.SAGE_INTACCT_CONFIG.MAPPINGS.CUSTOMERS, CONST.SAGE_INTACCT_CONFIG.MAPPINGS.PROJECTS] as string[]).includes(mappingName) && exportConfig?.reimbursable !== CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL ) { mappingOptions.push({ From 1504f23700853b4d6980c717fa877dad21eff46d Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 31 Jul 2024 11:10:30 +0200 Subject: [PATCH 27/47] Remove can select multiple flag --- .../approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx index e164ebf5de13..9ad76b2ffd5a 100644 --- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx +++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx @@ -214,7 +214,6 @@ function WorkspaceWorkflowsApprovalsApproverPageNew({policy, personalDetails, is /> {translate('workflowsApproverPage.header')} Date: Wed, 31 Jul 2024 11:56:36 +0200 Subject: [PATCH 28/47] fix PR comments --- src/libs/PolicyUtils.ts | 5 ++++- src/pages/workspace/accounting/PolicyAccountingPage.tsx | 2 +- .../accounting/intacct/advanced/SageIntacctAdvancedPage.tsx | 2 +- .../accounting/intacct/export/SageIntacctExportPage.tsx | 2 +- .../accounting/intacct/import/SageIntacctImportPage.tsx | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 2f27f0185759..97fd260b07d2 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -756,8 +756,11 @@ function getIntegrationLastSuccessfulDate(connection?: Connections[keyof Connect return (connection as ConnectionWithLastSyncData)?.lastSync?.successfulDate; } -function getCurrentSageIntacctEntityName(policy?: Policy): string | undefined { +function getCurrentSageIntacctEntityName(policy: Policy | undefined, translate: LocaleContextProps['translate']): string | undefined { const currentEntityID = policy?.connections?.intacct?.config?.entity; + if (!currentEntityID) { + return translate('workspace.common.topLevel'); + } const entities = policy?.connections?.intacct?.data?.entities; return entities?.find((entity) => entity.id === currentEntityID)?.name; } diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index 46a8b53e3fc4..79b0a5758ffb 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -276,7 +276,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { : { description: translate('workspace.intacct.entity'), iconRight: Expensicons.ArrowRight, - title: getCurrentSageIntacctEntityName(policy) ?? translate('workspace.common.topLevel'), + title: getCurrentSageIntacctEntityName(policy, translate), wrapperStyle: [styles.sectionMenuItemTopDescription], titleStyle: styles.fontWeightNormal, shouldShowRightIcon: true, diff --git a/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx b/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx index 2458b8579539..b91791c2ddec 100644 --- a/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx +++ b/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx @@ -100,7 +100,7 @@ function SageIntacctAdvancedPage({policy}: WithPolicyProps) { Date: Thu, 1 Aug 2024 02:27:51 +0900 Subject: [PATCH 29/47] fix: parameter name edited --- src/components/ButtonWithDropdownMenu/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index d0f79a75e451..26b9f7aa1736 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -150,7 +150,7 @@ function ButtonWithDropdownMenu({ onModalShow={onOptionsMenuShow} onItemSelected={() => setIsMenuVisible(false)} anchorPosition={popoverAnchorPosition} - anchorRef={nullCheckRef(caretButton)} + anchorRef={nullCheckRef(dropdownAnchor)} withoutOverlay anchorAlignment={anchorAlignment} headerText={menuHeaderText} From 33f65cfaf004b95e7a34f343b183d130faaf8161 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 31 Jul 2024 20:37:32 +0200 Subject: [PATCH 30/47] Fix TS --- src/libs/WorkflowUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/WorkflowUtils.ts b/src/libs/WorkflowUtils.ts index c01fbbea7b57..1365de4f881b 100644 --- a/src/libs/WorkflowUtils.ts +++ b/src/libs/WorkflowUtils.ts @@ -46,7 +46,7 @@ function getApprovalWorkflowApprovers({employees, firstEmail, personalDetailsByE email: nextEmail, forwardsTo: employees[nextEmail].forwardsTo, avatar: personalDetailsByEmail[nextEmail]?.avatar, - displayName: personalDetailsByEmail[nextEmail]?.displayName, + displayName: personalDetailsByEmail[nextEmail]?.displayName ?? nextEmail, isInMultipleWorkflows: false, isCircularReference, }); From 183a1ece4726d7c1c79513b18e672f7b6fabb058 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 1 Aug 2024 19:02:20 +0800 Subject: [PATCH 31/47] don't close expense report when paying --- src/libs/actions/IOU.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index acaf37fc8bf4..b75a81f76775 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7201,7 +7201,6 @@ function payMoneyRequest(paymentType: PaymentMethodType, chatReport: OnyxTypes.R const apiCommand = paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY ? WRITE_COMMANDS.PAY_MONEY_REQUEST_WITH_WALLET : WRITE_COMMANDS.PAY_MONEY_REQUEST; API.write(apiCommand, params, {optimisticData, successData, failureData}); - Navigation.dismissModalWithReport(chatReport); } function payInvoice(paymentMethodType: PaymentMethodType, chatReport: OnyxTypes.Report, invoiceReport: OnyxTypes.Report) { From 19a76a981e268d9a348b88dcd6a21096488e24f6 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Thu, 1 Aug 2024 13:37:19 +0200 Subject: [PATCH 32/47] fix: announce e2e build failures --- .../composite/buildAndroidE2EAPK/action.yml | 20 +++++++++++++++++++ .github/workflows/e2ePerformanceTests.yml | 4 +++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/actions/composite/buildAndroidE2EAPK/action.yml b/.github/actions/composite/buildAndroidE2EAPK/action.yml index cfce0d3ec7d8..424f6150f01c 100644 --- a/.github/actions/composite/buildAndroidE2EAPK/action.yml +++ b/.github/actions/composite/buildAndroidE2EAPK/action.yml @@ -37,6 +37,9 @@ inputs: EXPENSIFY_PARTNER_PASSWORD_EMAIL: description: The email address of the Expensify partner to use for the build required: true + SLACK_WEBHOOK_URL: + description: 'URL of the slack webhook' + required: true runs: using: composite @@ -75,6 +78,23 @@ runs: env: RUBYOPT: '-rostruct' + - name: Announce failed workflow in Slack + if: failure() + uses: 8398a7/action-slack@v3 + with: + status: custom + custom_payload: | + { + channel: '#e2e-announce', + attachments: [{ + color: 'danger', + text: `💥 ${process.env.AS_REPO} E2E APK build run failed on workflow 💥`, + }] + } + env: + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ inputs.SLACK_WEBHOOK_URL }} + - name: Upload APK uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml index 617a7a0abe05..e57556143978 100644 --- a/.github/workflows/e2ePerformanceTests.yml +++ b/.github/workflows/e2ePerformanceTests.yml @@ -68,6 +68,7 @@ jobs: EXPENSIFY_PARTNER_USER_ID: ${{ secrets.EXPENSIFY_PARTNER_USER_ID }} EXPENSIFY_PARTNER_USER_SECRET: ${{ secrets.EXPENSIFY_PARTNER_USER_SECRET }} EXPENSIFY_PARTNER_PASSWORD_EMAIL: ${{ secrets.EXPENSIFY_PARTNER_PASSWORD_EMAIL }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} PATH_ENV_FILE: tests/e2e/.env.e2e buildDelta: @@ -137,6 +138,7 @@ jobs: EXPENSIFY_PARTNER_USER_ID: ${{ secrets.EXPENSIFY_PARTNER_USER_ID }} EXPENSIFY_PARTNER_USER_SECRET: ${{ secrets.EXPENSIFY_PARTNER_USER_SECRET }} EXPENSIFY_PARTNER_PASSWORD_EMAIL: ${{ secrets.EXPENSIFY_PARTNER_PASSWORD_EMAIL }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} PATH_ENV_FILE: tests/e2e/.env.e2edelta runTestsInAWS: @@ -239,7 +241,7 @@ jobs: channel: '#e2e-announce', attachments: [{ color: 'danger', - text: `💥 ${process.env.AS_REPO} E2E Test run failed failed on workflow 💥`, + text: `💥 ${process.env.AS_REPO} E2E Test run failed on workflow 💥`, }] } env: From ed21ec4ed7a3e1286796d0eea0dd301d6d37f580 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Thu, 1 Aug 2024 14:06:55 +0200 Subject: [PATCH 33/47] fix: change emoji to distinguish different error messages --- .github/actions/composite/buildAndroidE2EAPK/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/composite/buildAndroidE2EAPK/action.yml b/.github/actions/composite/buildAndroidE2EAPK/action.yml index 424f6150f01c..25fafcb7be7f 100644 --- a/.github/actions/composite/buildAndroidE2EAPK/action.yml +++ b/.github/actions/composite/buildAndroidE2EAPK/action.yml @@ -88,7 +88,7 @@ runs: channel: '#e2e-announce', attachments: [{ color: 'danger', - text: `💥 ${process.env.AS_REPO} E2E APK build run failed on workflow 💥`, + text: `🚧 ${process.env.AS_REPO} E2E APK build run failed on workflow 🚧`, }] } env: From 9eb969c2d09cb106587bd14f21e5fce87c4a2fad Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Fri, 2 Aug 2024 10:20:32 +0700 Subject: [PATCH 34/47] fix: Icons Overflow from Dropdown Button --- src/pages/Search/SearchStatusMenuNarrow.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/pages/Search/SearchStatusMenuNarrow.tsx b/src/pages/Search/SearchStatusMenuNarrow.tsx index 1d10dc1dd431..6e77e6311fb2 100644 --- a/src/pages/Search/SearchStatusMenuNarrow.tsx +++ b/src/pages/Search/SearchStatusMenuNarrow.tsx @@ -79,12 +79,14 @@ function SearchStatusMenuNarrow({statusMenuItems, activeItemIndex, title}: Searc src={menuIcon} fill={theme.icon} /> - - {menuTitle} - + + + {menuTitle} + + Date: Fri, 2 Aug 2024 12:55:31 +0800 Subject: [PATCH 35/47] revert avatarURL when delete fails --- src/libs/actions/Policy/Policy.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 1bce525e22bf..9513ac99d82f 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -319,6 +319,7 @@ function deleteWorkspace(policyID: string, policyName: string) { }); }); + const policy = getPolicy(policyID); // Restore the old report stateNum and statusNum const failureData: OnyxUpdate[] = [ { @@ -328,6 +329,13 @@ function deleteWorkspace(policyID: string, policyName: string) { errors: reimbursementAccount?.errors ?? null, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + avatarURL: policy?.avatarURL, + }, + }, ]; reportsToArchive.forEach((report) => { From 639c0d48c8a49f093c8ee46cc47339f49bb86135 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Fri, 2 Aug 2024 13:07:28 +0800 Subject: [PATCH 36/47] only show delete workspace button to the owner --- src/pages/workspace/WorkspaceProfilePage.tsx | 23 ++++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index 1bf3ef068a1b..025cc9587bf6 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -4,7 +4,7 @@ import React, {useCallback, useState} from 'react'; import type {ImageStyle, StyleProp} from 'react-native'; import {Image, StyleSheet, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import Avatar from '@components/Avatar'; import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; import Button from '@components/Button'; @@ -55,6 +55,8 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, currencyList = { const {activeWorkspaceID, setActiveWorkspaceID} = useActiveWorkspace(); const {canUseSpotnanaTravel} = usePermissions(); + const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.accountID}); + // When we create a new workspace, the policy prop will be empty on the first render. Therefore, we have to use policyDraft until policy has been set in Onyx. const policy = policyDraft?.id ? policyDraft : policyProp; const outputCurrency = policy?.outputCurrency ?? ''; @@ -84,6 +86,7 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, currencyList = { }), ); const readOnly = !PolicyUtils.isPolicyAdmin(policy); + const isOwner = PolicyUtils.isPolicyOwner(policy, currentUserAccountID); const imageStyle: StyleProp = shouldUseNarrowLayout ? [styles.mhv12, styles.mhn5, styles.mbn5] : [styles.mhv8, styles.mhn8, styles.mbn5]; const shouldShowAddress = !readOnly || formattedAddress; @@ -270,14 +273,16 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, currencyList = { medium icon={Expensicons.QrCode} /> -