From 84eab1a1113600fa9e461722dab0f921a7c56b8a Mon Sep 17 00:00:00 2001 From: Tsaqif Date: Sun, 5 Nov 2023 10:42:34 +0700 Subject: [PATCH 0001/1506] Add CurrencySelectionList component and use it in IOU and WS currency settings Signed-off-by: Tsaqif --- src/components/CurrencySelectionList.js | 92 +++++++++++++++++++ src/pages/iou/IOUCurrencySelection.js | 74 ++------------- .../WorkspaceSettingsCurrencyPage.js | 67 ++------------ 3 files changed, 106 insertions(+), 127 deletions(-) create mode 100644 src/components/CurrencySelectionList.js diff --git a/src/components/CurrencySelectionList.js b/src/components/CurrencySelectionList.js new file mode 100644 index 000000000000..1c0733ae9927 --- /dev/null +++ b/src/components/CurrencySelectionList.js @@ -0,0 +1,92 @@ +import Str from 'expensify-common/lib/str'; +import PropTypes from 'prop-types'; +import React, {useMemo, useState} from 'react'; +import {withOnyx} from 'react-native-onyx'; +import _ from 'underscore'; +import compose from '@libs/compose'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import SelectionList from './SelectionList'; +import withLocalize, {withLocalizePropTypes} from './withLocalize'; + +const propTypes = { + /** Label for the search text input */ + textInputLabel: PropTypes.string.isRequired, + + /** Callback to fire when a currency is selected */ + onSelect: PropTypes.func.isRequired, + + /** Currency item to be selected initially */ + initiallySelectedCurrencyCode: PropTypes.string.isRequired, + + // The curency list constant object from Onyx + currencyList: PropTypes.objectOf( + PropTypes.shape({ + // Symbol for the currency + symbol: PropTypes.string, + + // Name of the currency + name: PropTypes.string, + + // ISO4217 Code for the currency + ISO4217: PropTypes.string, + }), + ), + + ...withLocalizePropTypes, +}; + +const defaultProps = { + currencyList: {}, +}; + +function CurrencySelectionList(props) { + const [searchValue, setSearchValue] = useState(''); + const {translate, currencyList} = props; + + const {sections, headerMessage} = useMemo(() => { + const currencyOptions = _.map(currencyList, (currencyInfo, currencyCode) => { + const isSelectedCurrency = currencyCode === props.initiallySelectedCurrencyCode; + return { + currencyName: currencyInfo.name, + text: `${currencyCode} - ${CurrencyUtils.getLocalizedCurrencySymbol(currencyCode)}`, + currencyCode, + keyForList: currencyCode, + isSelected: isSelectedCurrency, + }; + }); + + const searchRegex = new RegExp(Str.escapeForRegExp(searchValue.trim()), 'i'); + const filteredCurrencies = _.filter(currencyOptions, (currencyOption) => searchRegex.test(currencyOption.text) || searchRegex.test(currencyOption.currencyName)); + const isEmpty = searchValue.trim() && !filteredCurrencies.length; + + return { + sections: isEmpty ? [] : [{data: filteredCurrencies, indexOffset: 0}], + headerMessage: isEmpty ? translate('common.noResultsFound') : '', + }; + }, [currencyList, searchValue, translate, props.initiallySelectedCurrencyCode]); + + return ( + + ); +} + +CurrencySelectionList.displayName = 'CurrencySelectionList'; +CurrencySelectionList.propTypes = propTypes; +CurrencySelectionList.defaultProps = defaultProps; + +export default compose( + withLocalize, + withOnyx({ + currencyList: {key: ONYXKEYS.CURRENCY_LIST}, + }), +)(CurrencySelectionList); diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js index c7b5885865df..a18929061f1f 100644 --- a/src/pages/iou/IOUCurrencySelection.js +++ b/src/pages/iou/IOUCurrencySelection.js @@ -1,17 +1,15 @@ -import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect} from 'react'; import {Keyboard} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; +import CurrencySelectionList from '@components/CurrencySelectionList'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {withNetwork} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; -import SelectionList from '@components/SelectionList'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import compose from '@libs/compose'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -42,20 +40,6 @@ const propTypes = { }), }).isRequired, - // The currency list constant object from Onyx - currencyList: PropTypes.objectOf( - PropTypes.shape({ - // Symbol for the currency - symbol: PropTypes.string, - - // Name of the currency - name: PropTypes.string, - - // ISO4217 Code for the currency - ISO4217: PropTypes.string, - }), - ), - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ iou: iouPropTypes, @@ -63,13 +47,10 @@ const propTypes = { }; const defaultProps = { - currencyList: {}, iou: iouDefaultProps, }; function IOUCurrencySelection(props) { - const [searchValue, setSearchValue] = useState(''); - const optionsSelectorRef = useRef(); const selectedCurrencyCode = (lodashGet(props.route, 'params.currency', props.iou.currency) || CONST.CURRENCY.USD).toUpperCase(); const iouType = lodashGet(props.route, 'params.iouType', CONST.IOU.TYPE.REQUEST); const reportID = lodashGet(props.route, 'params.reportID', ''); @@ -113,59 +94,19 @@ function IOUCurrencySelection(props) { [props.route, props.navigation], ); - const {translate, currencyList} = props; - const {sections, headerMessage, initiallyFocusedOptionKey} = useMemo(() => { - const currencyOptions = _.map(currencyList, (currencyInfo, currencyCode) => { - const isSelectedCurrency = currencyCode === selectedCurrencyCode; - return { - currencyName: currencyInfo.name, - text: `${currencyCode} - ${CurrencyUtils.getLocalizedCurrencySymbol(currencyCode)}`, - currencyCode, - keyForList: currencyCode, - isSelected: isSelectedCurrency, - }; - }); - - const searchRegex = new RegExp(Str.escapeForRegExp(searchValue.trim()), 'i'); - const filteredCurrencies = _.filter(currencyOptions, (currencyOption) => searchRegex.test(currencyOption.text) || searchRegex.test(currencyOption.currencyName)); - const isEmpty = searchValue.trim() && !filteredCurrencies.length; - - return { - initiallyFocusedOptionKey: _.get( - _.find(filteredCurrencies, (currency) => currency.currencyCode === selectedCurrencyCode), - 'keyForList', - ), - sections: isEmpty - ? [] - : [ - { - data: filteredCurrencies, - indexOffset: 0, - }, - ], - headerMessage: isEmpty ? translate('common.noResultsFound') : '', - }; - }, [currencyList, searchValue, selectedCurrencyCode, translate]); - return ( optionsSelectorRef.current && optionsSelectorRef.current.focus()} testID={IOUCurrencySelection.displayName} > Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID))} /> - ); @@ -178,7 +119,6 @@ IOUCurrencySelection.defaultProps = defaultProps; export default compose( withLocalize, withOnyx({ - currencyList: {key: ONYXKEYS.CURRENCY_LIST}, iou: {key: ONYXKEYS.IOU}, }), withNetwork(), diff --git a/src/pages/workspace/WorkspaceSettingsCurrencyPage.js b/src/pages/workspace/WorkspaceSettingsCurrencyPage.js index ce1e1d7b8966..a20e8e272e4e 100644 --- a/src/pages/workspace/WorkspaceSettingsCurrencyPage.js +++ b/src/pages/workspace/WorkspaceSettingsCurrencyPage.js @@ -1,73 +1,30 @@ import PropTypes from 'prop-types'; -import React, {useCallback, useState} from 'react'; -import {withOnyx} from 'react-native-onyx'; +import React, {useCallback} from 'react'; import _ from 'underscore'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import CurrencySelectionList from '@components/CurrencySelectionList'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; -import SelectionList from '@components/SelectionList'; import useLocalize from '@hooks/useLocalize'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as Policy from '@userActions/Policy'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import {policyDefaultProps, policyPropTypes} from './withPolicy'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; const propTypes = { - /** Constant, list of available currencies */ - currencyList: PropTypes.objectOf( - PropTypes.shape({ - /** Symbol of the currency */ - symbol: PropTypes.string.isRequired, - }), - ), isLoadingReportData: PropTypes.bool, ...policyPropTypes, }; const defaultProps = { - currencyList: {}, isLoadingReportData: true, ...policyDefaultProps, }; -const getDisplayText = (currencyCode, currencySymbol) => `${currencyCode} - ${currencySymbol}`; - -function WorkspaceSettingsCurrencyPage({currencyList, policy, isLoadingReportData}) { +function WorkspaceSettingsCurrencyPage({policy, isLoadingReportData}) { const {translate} = useLocalize(); - const [searchText, setSearchText] = useState(''); - const trimmedText = searchText.trim().toLowerCase(); - const currencyListKeys = _.keys(currencyList); - - const filteredItems = _.filter(currencyListKeys, (currencyCode) => { - const currency = currencyList[currencyCode]; - return getDisplayText(currencyCode, currency.symbol).toLowerCase().includes(trimmedText); - }); - - let initiallyFocusedOptionKey; - - const currencyItems = _.map(filteredItems, (currencyCode) => { - const currency = currencyList[currencyCode]; - const isSelected = policy.outputCurrency === currencyCode; - - if (isSelected) { - initiallyFocusedOptionKey = currencyCode; - } - - return { - text: getDisplayText(currencyCode, currency.symbol), - keyForList: currencyCode, - isSelected, - }; - }); - - const sections = [{data: currencyItems, indexOffset: 0}]; - - const headerMessage = searchText.trim() && !currencyItems.length ? translate('common.noResultsFound') : ''; - const onBackButtonPress = useCallback(() => Navigation.goBack(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id)), [policy.id]); const onSelectCurrency = (item) => { @@ -90,15 +47,10 @@ function WorkspaceSettingsCurrencyPage({currencyList, policy, isLoadingReportDat onBackButtonPress={onBackButtonPress} /> - @@ -109,9 +61,4 @@ WorkspaceSettingsCurrencyPage.displayName = 'WorkspaceSettingsCurrencyPage'; WorkspaceSettingsCurrencyPage.propTypes = propTypes; WorkspaceSettingsCurrencyPage.defaultProps = defaultProps; -export default compose( - withPolicyAndFullscreenLoading, - withOnyx({ - currencyList: {key: ONYXKEYS.CURRENCY_LIST}, - }), -)(WorkspaceSettingsCurrencyPage); +export default withPolicyAndFullscreenLoading(WorkspaceSettingsCurrencyPage); From 87c644c9f65a5d9845a92aa0a6531ace1855ddb4 Mon Sep 17 00:00:00 2001 From: Tsaqif Date: Wed, 8 Nov 2023 20:19:09 +0700 Subject: [PATCH 0002/1506] Add currency url parameter for WorkspaceSettingsCurrencyPage Signed-off-by: Tsaqif --- src/ROUTES.ts | 2 +- src/components/CurrencySelectionList.js | 5 ++++- src/pages/iou/IOUCurrencySelection.js | 1 + .../workspace/WorkspaceSettingsCurrencyPage.js | 18 +++++++++++++++++- src/pages/workspace/WorkspaceSettingsPage.js | 2 +- 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index bcc4685368cb..0902309514fc 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -328,7 +328,7 @@ export default { }, WORKSPACE_SETTINGS_CURRENCY: { route: 'workspace/:policyID/settings/currency', - getRoute: (policyID: string) => `workspace/${policyID}/settings/currency`, + getRoute: (policyID: string, currency: string) => `workspace/${policyID}/settings/currency?currency=${currency}`, }, WORKSPACE_CARD: { route: 'workspace/:policyID/card', diff --git a/src/components/CurrencySelectionList.js b/src/components/CurrencySelectionList.js index 795e8477a523..ba3cc9f7d7ed 100644 --- a/src/components/CurrencySelectionList.js +++ b/src/components/CurrencySelectionList.js @@ -20,6 +20,9 @@ const propTypes = { /** Currency item to be selected initially */ initiallySelectedCurrencyCode: PropTypes.string.isRequired, + /** Currency item to be focused initially */ + initiallyFocusedCurrencyCode: PropTypes.string.isRequired, + // The curency list constant object from Onyx currencyList: PropTypes.objectOf( PropTypes.shape({ @@ -79,7 +82,7 @@ function CurrencySelectionList(props) { onChangeText={setSearchValue} onSelectRow={props.onSelect} headerMessage={headerMessage} - initiallyFocusedOptionKey={props.initiallySelectedCurrencyCode} + initiallyFocusedOptionKey={props.initiallyFocusedCurrencyCode} showScrollIndicator /> ); diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js index a18929061f1f..3fce092abda0 100644 --- a/src/pages/iou/IOUCurrencySelection.js +++ b/src/pages/iou/IOUCurrencySelection.js @@ -106,6 +106,7 @@ function IOUCurrencySelection(props) { diff --git a/src/pages/workspace/WorkspaceSettingsCurrencyPage.js b/src/pages/workspace/WorkspaceSettingsCurrencyPage.js index a20e8e272e4e..2bb2a283acbc 100644 --- a/src/pages/workspace/WorkspaceSettingsCurrencyPage.js +++ b/src/pages/workspace/WorkspaceSettingsCurrencyPage.js @@ -1,3 +1,4 @@ +import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback} from 'react'; import _ from 'underscore'; @@ -6,6 +7,7 @@ import CurrencySelectionList from '@components/CurrencySelectionList'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as Policy from '@userActions/Policy'; @@ -19,12 +21,25 @@ const propTypes = { }; const defaultProps = { + /** Route from navigation */ + route: PropTypes.shape({ + /** Params from the route */ + params: PropTypes.shape({ + /** Focused currency code */ + currency: PropTypes.string, + + /** ID of a policy */ + policyID: PropTypes.string, + }), + }).isRequired, isLoadingReportData: true, ...policyDefaultProps, }; -function WorkspaceSettingsCurrencyPage({policy, isLoadingReportData}) { +function WorkspaceSettingsCurrencyPage({route, policy, isLoadingReportData}) { const {translate} = useLocalize(); + const currencyParam = lodashGet(route, 'params.currency', '').toUpperCase(); + const focusedCurrencyCode = CurrencyUtils.isValidCurrencyCode(currencyParam) ? currencyParam : policy.outputCurrency; const onBackButtonPress = useCallback(() => Navigation.goBack(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id)), [policy.id]); const onSelectCurrency = (item) => { @@ -50,6 +65,7 @@ function WorkspaceSettingsCurrencyPage({policy, isLoadingReportData}) { diff --git a/src/pages/workspace/WorkspaceSettingsPage.js b/src/pages/workspace/WorkspaceSettingsPage.js index d913ae26c170..287f31305164 100644 --- a/src/pages/workspace/WorkspaceSettingsPage.js +++ b/src/pages/workspace/WorkspaceSettingsPage.js @@ -87,7 +87,7 @@ function WorkspaceSettingsPage({policy, currencyList, windowWidth, route}) { return errors; }, []); - const onPressCurrency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS_CURRENCY.getRoute(policy.id)), [policy.id]); + const onPressCurrency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS_CURRENCY.getRoute(policy.id, policy.outputCurrency)), [policy.id, policy.outputCurrency]); const policyName = lodashGet(policy, 'name', ''); From 1e508c14374e7d0809770031c7fd455eb593edf9 Mon Sep 17 00:00:00 2001 From: Tsaqif Date: Fri, 24 Nov 2023 07:45:11 +0700 Subject: [PATCH 0003/1506] Don't automatically save ws default currency when selecting currency in selection list Signed-off-by: Tsaqif --- src/ROUTES.ts | 2 +- src/components/CurrencySelectionList.js | 5 +---- .../workspace/WorkspaceSettingsCurrencyPage.js | 11 ++++------- src/pages/workspace/WorkspaceSettingsPage.js | 16 +++++++++++----- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 0902309514fc..9545a7ab5008 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -324,7 +324,7 @@ export default { }, WORKSPACE_SETTINGS: { route: 'workspace/:policyID/settings', - getRoute: (policyID: string) => `workspace/${policyID}/settings`, + getRoute: (policyID: string, currency: string) => `workspace/${policyID}/settings?currency=${currency}`, }, WORKSPACE_SETTINGS_CURRENCY: { route: 'workspace/:policyID/settings/currency', diff --git a/src/components/CurrencySelectionList.js b/src/components/CurrencySelectionList.js index ba3cc9f7d7ed..795e8477a523 100644 --- a/src/components/CurrencySelectionList.js +++ b/src/components/CurrencySelectionList.js @@ -20,9 +20,6 @@ const propTypes = { /** Currency item to be selected initially */ initiallySelectedCurrencyCode: PropTypes.string.isRequired, - /** Currency item to be focused initially */ - initiallyFocusedCurrencyCode: PropTypes.string.isRequired, - // The curency list constant object from Onyx currencyList: PropTypes.objectOf( PropTypes.shape({ @@ -82,7 +79,7 @@ function CurrencySelectionList(props) { onChangeText={setSearchValue} onSelectRow={props.onSelect} headerMessage={headerMessage} - initiallyFocusedOptionKey={props.initiallyFocusedCurrencyCode} + initiallyFocusedOptionKey={props.initiallySelectedCurrencyCode} showScrollIndicator /> ); diff --git a/src/pages/workspace/WorkspaceSettingsCurrencyPage.js b/src/pages/workspace/WorkspaceSettingsCurrencyPage.js index 2bb2a283acbc..b86ee9e34404 100644 --- a/src/pages/workspace/WorkspaceSettingsCurrencyPage.js +++ b/src/pages/workspace/WorkspaceSettingsCurrencyPage.js @@ -10,7 +10,6 @@ import useLocalize from '@hooks/useLocalize'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; -import * as Policy from '@userActions/Policy'; import ROUTES from '@src/ROUTES'; import {policyDefaultProps, policyPropTypes} from './withPolicy'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; @@ -39,12 +38,11 @@ const defaultProps = { function WorkspaceSettingsCurrencyPage({route, policy, isLoadingReportData}) { const {translate} = useLocalize(); const currencyParam = lodashGet(route, 'params.currency', '').toUpperCase(); - const focusedCurrencyCode = CurrencyUtils.isValidCurrencyCode(currencyParam) ? currencyParam : policy.outputCurrency; - const onBackButtonPress = useCallback(() => Navigation.goBack(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id)), [policy.id]); + const selectedCurrencyCode = CurrencyUtils.isValidCurrencyCode(currencyParam) ? currencyParam : policy.outputCurrency; + const onBackButtonPress = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id, selectedCurrencyCode)), [policy.id, selectedCurrencyCode]); const onSelectCurrency = (item) => { - Policy.updateGeneralSettings(policy.id, policy.name, item.keyForList); - Navigation.goBack(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id)); + Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id, item.currencyCode)); }; return ( @@ -65,8 +63,7 @@ function WorkspaceSettingsCurrencyPage({route, policy, isLoadingReportData}) { diff --git a/src/pages/workspace/WorkspaceSettingsPage.js b/src/pages/workspace/WorkspaceSettingsPage.js index 287f31305164..df43145d6ffd 100644 --- a/src/pages/workspace/WorkspaceSettingsPage.js +++ b/src/pages/workspace/WorkspaceSettingsPage.js @@ -16,6 +16,7 @@ import TextInput from '@components/TextInput'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import useLocalize from '@hooks/useLocalize'; import compose from '@libs/compose'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import * as UserUtils from '@libs/UserUtils'; @@ -42,6 +43,9 @@ const propTypes = { params: PropTypes.shape({ /** The policyID that is being configured */ policyID: PropTypes.string.isRequired, + + /** Selected currency code */ + currency: PropTypes.string, }).isRequired, }).isRequired, @@ -56,8 +60,10 @@ const defaultProps = { function WorkspaceSettingsPage({policy, currencyList, windowWidth, route}) { const {translate} = useLocalize(); + const currencyParam = lodashGet(route, 'params.currency', '').toUpperCase(); + const currencyCode = CurrencyUtils.isValidCurrencyCode(currencyParam) ? currencyParam : policy.outputCurrency; - const formattedCurrency = !_.isEmpty(policy) && !_.isEmpty(currencyList) ? `${policy.outputCurrency} - ${currencyList[policy.outputCurrency].symbol}` : ''; + const formattedCurrency = !_.isEmpty(policy) && !_.isEmpty(currencyList) ? `${currencyCode} - ${currencyList[currencyCode].symbol}` : ''; const submit = useCallback( (values) => { @@ -65,11 +71,11 @@ function WorkspaceSettingsPage({policy, currencyList, windowWidth, route}) { return; } - Policy.updateGeneralSettings(policy.id, values.name.trim(), policy.outputCurrency); + Policy.updateGeneralSettings(policy.id, values.name.trim(), currencyCode); Keyboard.dismiss(); - Navigation.goBack(ROUTES.WORKSPACE_INITIAL.getRoute(policy.id)); + Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policy.id)); }, - [policy.id, policy.isPolicyUpdating, policy.outputCurrency], + [policy.id, policy.isPolicyUpdating, currencyCode], ); const validate = useCallback((values) => { @@ -87,7 +93,7 @@ function WorkspaceSettingsPage({policy, currencyList, windowWidth, route}) { return errors; }, []); - const onPressCurrency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS_CURRENCY.getRoute(policy.id, policy.outputCurrency)), [policy.id, policy.outputCurrency]); + const onPressCurrency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS_CURRENCY.getRoute(policy.id, currencyCode)), [policy.id, currencyCode]); const policyName = lodashGet(policy, 'name', ''); From 81dc4ec8a8ef6feb66e8e685857ad738b1eab33f Mon Sep 17 00:00:00 2001 From: Tsaqif Date: Fri, 24 Nov 2023 09:43:02 +0700 Subject: [PATCH 0004/1506] Fix adding currency parameter to worksapace_settings get route calls Signed-off-by: Tsaqif --- src/pages/workspace/WorkspaceInitialPage.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index 77e831e62b63..1e27c89b405c 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -53,10 +53,10 @@ const defaultProps = { }; /** - * @param {string} policyID + * @param {Object} policy */ -function openEditor(policyID) { - Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policyID)); +function openEditor(policy) { + Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id, policy.outputCurrency)); } /** @@ -152,7 +152,7 @@ function WorkspaceInitialPage(props) { { translationKey: 'workspace.common.settings', icon: Expensicons.Gear, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id)))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id, policy.outputCurrency)))), brickRoadIndicator: hasGeneralSettingsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', }, { @@ -269,7 +269,7 @@ function WorkspaceInitialPage(props) { openEditor(policy.id)))} + onPress={singleExecution(waitForNavigate(() => openEditor(policy)))} accessibilityLabel={translate('workspace.common.settings')} role={CONST.ACCESSIBILITY_ROLE.BUTTON} > @@ -289,7 +289,7 @@ function WorkspaceInitialPage(props) { openEditor(policy.id)))} + onPress={singleExecution(waitForNavigate(() => openEditor(policy)))} accessibilityLabel={translate('workspace.common.settings')} role={CONST.ACCESSIBILITY_ROLE.BUTTON} > From 6c4c5b0cc041e57c020cf1c0efaa5976cd0f5e78 Mon Sep 17 00:00:00 2001 From: Tsaqif Date: Sun, 3 Dec 2023 15:54:49 +0700 Subject: [PATCH 0005/1506] Change display text format of currency selection list Signed-off-by: Tsaqif --- src/components/CurrencySelectionList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CurrencySelectionList.js b/src/components/CurrencySelectionList.js index 795e8477a523..7d43b09c8d66 100644 --- a/src/components/CurrencySelectionList.js +++ b/src/components/CurrencySelectionList.js @@ -50,7 +50,7 @@ function CurrencySelectionList(props) { const isSelectedCurrency = currencyCode === props.initiallySelectedCurrencyCode; return { currencyName: currencyInfo.name, - text: `${currencyCode} - ${CurrencyUtils.getLocalizedCurrencySymbol(currencyCode)}`, + text: `${currencyCode} - ${CurrencyUtils.getCurrencySymbol(currencyCode)}`, currencyCode, keyForList: currencyCode, isSelected: isSelectedCurrency, From 06e5faa05332dd6969f949dcbb12bdc77b3cbe3f Mon Sep 17 00:00:00 2001 From: Sheena Trepanier Date: Wed, 31 Jan 2024 16:33:41 -0800 Subject: [PATCH 0006/1506] Create unlimited-virtual-cards.md This is a new help doc for the imminent release of Unlimited Virtual Cards. --- .../expensify-card/unlimited-virtual-cards.md | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 docs/articles/expensify-classic/expensify-card/unlimited-virtual-cards.md diff --git a/docs/articles/expensify-classic/expensify-card/unlimited-virtual-cards.md b/docs/articles/expensify-classic/expensify-card/unlimited-virtual-cards.md new file mode 100644 index 000000000000..c5578249289a --- /dev/null +++ b/docs/articles/expensify-classic/expensify-card/unlimited-virtual-cards.md @@ -0,0 +1,86 @@ +--- +title: Unlimited Virtual Cards +description: Learn more about virtual cards and how they can help your business gain efficiency and insight into company spending. +--- + +# Overview + +For admins to issue virtual cards, your company **must upgrade to Expensify’s new Expensify Visa® Commercial Card.** + +Once upgraded to the new Expensify Card, admins can issue an unlimited number of virtual cards with a fixed or monthly limit for specific company purchases or recurring subscription payments _(e.g., Marketing purchases, Advertising, Travel, Amazon Web Services, etc.)._ + +This feature supports businesses that require tighter controls on company spending, allowing customers to set fixed or monthly spending limits for each virtual card. + +Use virtual cards if your company needs or wants: + +- To use one card per vendor or subscription, +- To issue cards for one-time purchases with a fixed amount, +- To issue cards for events or trips, +- To issue cards with a low limit that renews monthly, + +Admins can also name each virtual card, making it easy to categorize and assign them to specific accounts upon creation. Naming the card ensures a clear and organized overview of expenses within the Expensify platform. + +# How to set up virtual cards + +After adopting the new Expensify Card, domain admins can issue virtual cards to any employee using an email matching your domain. Once created and assigned, the card will be visible under the name given to the card. + +**To assign a virtual card:** + +1. Head to **Settings** > **Domains** > [**Company Cards**](https://www.expensify.com/domain_companycards). +2. Click the **Issue Virtual Cards** button. +3. Enter a card name (i.e., "Google Ads"). +4. Select a domain member to assign the card to. +5. Enter a card limit. +6. Select a **Limit Type** of _Fixed_ or _Monthly_. +7. Click **Issue Card**. + +![The Issue Virtual Cards modal is open in the middle of the screen. There are four options to set; Card Name, Assignee, Card Limit, and Limit type. A cancel (left) and save (right) button are at the bottom right of the modal.]({{site.url}}/assets/images/AdminissuedVirtualCards.png){:width="100%"} + +# How to edit virtual cards + +Domain admin can update the details of a virtual card on the [Company Cards](https://www.expensify.com/domain_companycards) page. + +**To edit a virtual card:** + +1. Click the **Edit** button to the right of the card. +2. Change the editable details. +3. Click **Edit Card** to save the changes. + +# How to terminate a virtual card + +Domain admin can also terminate a virtual card on the [Company Cards](https://www.expensify.com/domain_companycards) page by setting the limit to $0. + +**To terminate a virtual card:** + +1. Click the **Edit** button to the right of the card. +2. Set the limit to $0. +3. Click **Save**. +4. Refresh your web page, and the card will be removed from the list. + +{% include faq-begin.md %} + +**What is the difference between a fixed limit and a monthly limit?** + +There are two different limit types that are best suited for their intended purpose. + +- _Fixed limit_ spend cards are ideal for one-time expenses or providing employees access to a card for a designated purchase. +- _Monthly_ limit spend cards are perfect for managing recurring expenses such as subscriptions and memberships. + +**Where can employees see their virtual cards?** + +Employees can see their assigned virtual cards by navigating to **Settings** > **Account** > [**Credit Cards Import**](https://www.expensify.com/settings?param=%7B%22section%22:%22creditcards%22%7D) in their account. + +On this page, employees can see the remaining card limit, the type of card it is (i.e., fixed or monthly), and view the name given to the card. + +When the employee needs to use the card, they’ll click the **Show Details** button to expose the card details for making purchases. + +_Note: If the employee doesn’t have Two-Factor Authentication (2FA) enabled when they display the card details, they’ll be prompted to enable it. Enabling 2FA for their account provides the best protection from fraud and is **required** to dispute virtual card expenses._ + +**What do I do when there is fraud on one of our virtual cards?** + +If you or an employee loses their virtual card, experiences fraud, or suspects the card details are no longer secure, please [request a new card](https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-A-Transaction) immediately. A domain admin can also set the limit for the card to $0 to terminate the specific card immediately if the employee cannot take action. + +When the employee requests a new card, the compromised card will be terminated immediately. This is best practice for any Expensify Card and if fraud is suspected, action should be taken as soon as possible to reduce financial impact on the company. + +{% include faq-end.md %} + From 93072a93a9f792e0a63a5660b6bb8bbfb83429d6 Mon Sep 17 00:00:00 2001 From: Sheena Trepanier Date: Wed, 31 Jan 2024 16:37:45 -0800 Subject: [PATCH 0007/1506] Add files via upload --- docs/assets/images/AdminissuedVirtualCards.png | Bin 0 -> 157289 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/assets/images/AdminissuedVirtualCards.png diff --git a/docs/assets/images/AdminissuedVirtualCards.png b/docs/assets/images/AdminissuedVirtualCards.png new file mode 100644 index 0000000000000000000000000000000000000000..88df9b2f3fecef011db3e71c7eb12c02847cc97b GIT binary patch literal 157289 zcmeFZ_dnZT|36NRwp6uR6h$c=W~)Y9d$iQ3QL$%HGoc7#wv-mNi=tvwtt4h8Bu4Gp zdkZy#kk~KDtCR|@ zqq2@N6;*j0e>;lMbpD^VU;k56 z`kxzk{7>!w-R|#R`+uGJuYUM{rt-Hr{Qq{|EwRG~G`AV-)krggrPa~FS;XL8CVOvv$QIw_s37Y(m$t3>6~Q}#M;xllE9mz4;wihs_BeYYUgvpUx)d|*c(+mA$SBwDlxJ~^E+@}~UY31>M z`6hUbh<3xqh@+IoSxo1y6fO0U%X1H?!havpBjoD3m?o~6czz}%>{gJ}Z6)iN{lP!S z+4|;JzZFFOmovkMsy#!8ubRkT=r;}%gV1?)A~Ben-h}_ToR)}&;v%v^Kc=4Z;JBH5 ztW(x$GIE$EOAwW3l)Y&5SuFFZRxk+qe=kIZ4{iEp@gxwRna6kY3vd3*>+q+vE3p2D z8N^%X<0duFDKyx${+HYQg}(FX_AS}chVoD9i(MBRE*deF+9W>)PhlNOuW~W|b6xM@ zXJqa>zNd%Ujm66E>KUdtKYq6-H{PcG=a7`-Dz6Yyr#Y=KFZGe;n`GR{Q}ES)noX>@ zb_u$IM?y;P>RGsJ2T}~7|11wCd~lYN$>viL69}a{66+%362sJil*R2or7ZVbEx!J! zD$ZudwOV0h*N@J7)jDYJ3lak+21<^&JK(mx}_dmtPq0)$86cfw(k$4Di{GP!Yq{QzHU6+4^zMJpEIeb;6itaXmLvG6-ui_t+{&#ll@;y z`@aiZ$S|kWgmy*vf>+jm^#?bcfm@iKa>j$&0Akf^f>+!LZbUpQ_%KMcA#mn^D9`iH z1C;rc3GGrIUR)!~Iv&YQk4W>%!rYq1vzF2=90|_&J%ApcEkA*DpR&6iTQx+F8>_t@ zC=71>XXt*VJD2Kbbg^FxX3hJ^u*+&=Lb9s3Q~qZa+Teypk73C9gAz4$^U+1yQ5uu&3Wd zw#@!)m@Dg+&XgL{Krq%JKFd)U>;F5CN?0wc*D_D{g|V`~hRV<|syZ4Dij49KG5SS^ ztzcnn6_rD^mv?%@4E})tWpBpc)Jb`*tyW#U=#+lsYgPCCu>}mw*!$?wbRZ5GIdQzK zpWvh>z_R=Ekd;Zw3RQQ%PpXV`w6KA#3vT%5)+Q&T#79c;JcE6bhm6cPZ&=_NrpsSvYWzy#LQ_7ZS&Uo z|LfIX645TbERoLyZ$?)WubtVBMB(08vHzGjBTEVFtfo3TAm3TNKl%bcB}}-=p4*iK zi{AS((wLDWKB^P}qjzr%-Epc-pI)e&jN9$&AuK46AYFDL3zUh=}C0UEzj(3}0eP zbMy-&2zO=h9nQGj-*t!{t9&DKfQ0lX@KBy*iN3-futOg>-{o{$F7DF@RUTs?p{j&X zpJE)@R-1^GH?Ezqx|l*IXsACcag)?0+Z3?p$vA!6wr~0tz*3bQ5#RFq z=0CV^l4Ve-fZsy+$DB)1)F|Vqh|wXh^ZErnUd9@nQ;uPcUfu6hVZqbDjy(r-)|h1Y z6G-UpmF$8dt>E_`RQOD#X^l%}O1? z9f>At%v2*xVmICQ^br#BHh>NP>i+gml1}kZzt7jD#nF+HA5tVD|Ic855p_*yCI&UC z7<*m6E1Uf+b$|=MlMew5JiWiWq){%eyFFTVtFF%_BiJ9S{0>?I@Uz%C;+jIrSkuC; zF#B6zd_vx6r|fg?IbU#}tJyk`QX{4dR)KBnvdVsrft!Q|1#RM3z4*}Ll`&W3*`h!G zGddn{0J;0m>!Q!2*S?i5!COyrJ(BVK{jJuwu;;>EwLcL|W%FIEzhBKRaAn;HXY(UV zm?ME>V+Cp|K$MYgomKY&dz^0AzS0o6>~Q!|l!TxO=RoTIAoI9%78-z17ya4ysmkdZ zak8eJ6Q_2*EH;|=k<-(^-Nrkq#>7`E0vaYsEqU5uqiJ&n7foet;g8^~XWu;13|rRn zLbBB=J_5E<1-PQ7I8z z9mn5O7iO@wIx2|Ef87(@_rw&=W8_@_z(UYV#Q~%MglD+iW|F9~&Dmx0PPys+%OL>9 zsE*mCbfz<)ma3NXxjA`%SrR|`p!Q^Qy5+UiMeokU&&Qvot)*tPnS3ixSc)(E7BXN> z`a#LWfwG8$nS8{8=&gOaW0sP*5-(>n)d%;I-gL)TM0qj|G{@YAq}0c1=3wpnaK%HKZUM#^>L zI$*74?0{(ZEv{7v*R-RT+jMDV zzKfJTj}h+|Q6oWG3#=11(SUFe>oQv|1`MV<&>>N0fvz{q-nDWM><V)lyzLu|yVv*#8uU5bDn42GA*+N z9HFzMLZA^V`m5hdPeP|_9nf{$s%*)rHOf!!2*=;Ilm#aOT6uimLTV&GOlydd(((ER zi8m$xUQ5b4A)J`j{BNn6P|Y2Yi$m9NV}%;+#r@gJYjC}^53jo+rvb0nnL59D)VSgZ zmP}#buQc*Ov@5G-k>u>UIdiVvW%%S&MtR=kRH&#Y|QRheCKBsZA-@qM>b(vx8X z12%L~!yD@#cH!WWKzu4+X0)u-#r{gE$3zQ50gfZ`GTMhALL(`XCFd2p;H!!GOJGX1|SY!y-o!Gg?N%FdNrQhLRqV@^xx_{|&>wKpHwu0qdZlOtdPeO&P@XT^OYkBfi&g>Qr_ z!!hCWqLU@+^Srboj#ZkwPAv;}Zug0N?vvpuK?!X-C24vvwunAvPB|~Nvaj(RI#w0o zepW6I>LnD4spc6GZ{O5GM!WAF%^WPWc|+u8^2c^u{;*#}WVo|XhYY30_YtGxyxs6e zoS~Oj$E?HlwbLYApI7+%j@ClNZfy)9Fdk0$$5U^b&tLXTE>j5Lwl{l+0=YHBM|_Fg zoIbh9;A~@h{~2AlaNVFxRk7S*P_B76Ogm1rrZ8D^H(zunq4W9a2pyL1L+w9l)XN2{vkgcOuVmAF zrV=xPvtq20`aF$1W&04}|IUowAlirxQ^iQ)BhC?7d#8rh@3IFruNJK+VKNXl&(1+5$Pq?GfW}m#|P?S@tgkDGHSb43MDJz4&{< zx@h)puw%1-ZX`zI@a|}CtE}2ncD4>Ux<>4SSa+AN=y-G26$N$vtzVO;F=sbT^QQ`6 ztqbJ&*^hPqjxKFRUVND82+dzq^jDa?re$^av}*Aic>X3x`@%rEC#_Uo8gH>l5#3i4 zSrcIr1$^x8vUL@=Ko0*}pn!rKA2=VQ3MHzZw8?8NbC8ZUw8B2e)3YaYHjO@ON##f$ z&S+KPggQD`LovH$qK1e$2*l)d(GygLwS4|uZBSGH75UdA8;H_MmpX(d8 zixUsN`v@1OAv1ZBV>T9`4mUq7G1|YFojJj}wvGqc;}*M$S$;=p5UozU zXW?<;ak_C=(TYd4us8nyhEOR5ujWO~f&C4ckIHw(K~}qMuflQ&dzg1Y#UQRw5@xW zK&McHs?=4PGsv1N|6q0_1Zo}lM92ExKsKN#z(T4<=JsjhuO5gM*9h|kE8ma-54m-p z?>m7!GJWVim0hR#un#}Wr;n=BSN*Q-bV^nS(+A+SJf!Efg4_jhWtOH|KRVR><~sH_n-QZ7EqM zCR*x&LNJbE7^XwN$#F<~=q34#BjvkbFnfLHBEylwn%Zs?BnvJWMq=9=$PB^Rh)Y*B6m4cQ~pQRB`RmvY*l5HP;W>|hp8XLLh zg_%YsD2SWZ&?V;BHjFVHZEdNy2@QUCv^LE|`w&I{hB~e+vT<*o@k;l6W1W&*dD44y zG}-1=2=xP?Gl@Q|Yg<#*Az!a?3&(G`vnH6iM8)S7ef*;ClGoC(7M1Ph;+Xf5N**YT z>lvPsE35n?~!DkL2g5||$Q`24|N~wBX@mLkV?VXe;#wh#c+M13m z(d#9n@hAu@7!fg&65hX*Iy5dC ziw2&7x7CarzjZF#BR3(9^9CG>K^5feG$EGHzng4|3`Y&S^6_!r)DQ=7)#tRwGxAo& z+2M6;gqOu^aWn2DF-vnxcgFOBm7{L!+TY=$NO7|p(^apan7Z)!sy*?zXs_PAsr%l&0hEf+4BV+fC8ul&m-@aSIIQZdvLPtSdc z>#<4mTuSNQS~}<93t~gg!#`rZ;_=hd7Nk1BPh*PV*^O0a0mc)=3a7Z039Uelb4Xf~P3?wROpX5rFmS$FiS)tplxho1nuNO$~Tjixq-^ z>$^o>N$R(^6eLfljjTQqjCo|nYK#>%s{1X&4wa0Qwx8lw&oefB=@E`YRVVlfx>!C# zeAyK)-*(z^Jj}}x$(4QiGjl1D@TjR!bT4*&Gf~R9D6((YkT_K8ZmVBY-n?EO*z|6<#JCFh?D9}+98(_ z8}P+DS&qqAbXs%_Z{F{^UhmkQAso(m&(Tqe$yD^~sY4Sj(~0o(NBsaebsbrgl*nRv zw*j*Oui3Gm|6D{kVjTfUxDW8J4t-arKo!|Vw9g)o%%59ZLZ9zU#>i|-UY0H!Fe+IJ z68LtU8NNA$>-Dq^M)g`s{_69ZHC|gt1{ev@kSN&xhx!=E0IuvKgsB6bXY51J{CcMl zlxLg@iUEvoITQ`q1P^}ZFfcPE2vy@mtpqZoqEd^JG2tt8Xl~)x$)H8IH6<#TT4`ai zXIFh~^2%0C_jd^|f`!c2=E*IL^7Z>W-}(md4vsy^Odn(7q4H$STaPjn`Tw?=vbM@| z@t9}t1DyDm&~BA-PF11~%oDt;82zo%bi?O2NZ&{WbI zQx!<6_UTRBE>cMcWT52LkA&nRN?AoTdVX)pTm=$9I_ZxU5rlixXxtUclMzP!fezGqR$hcC2kwp2Rq5?~7Eg0+jT zWKv=(m;}#L8@-An`C#53+xW$c9mU?RTOh9s;i0w@Hg!1$U$U|+*vtu8?I+8Vd0em~ zO33Gq&1{A)DJn5pppgp`7i(-pgT}aU+60k|UWsh-G@m#{cu;WQz+AE$+Ab|c4Zg|?FL_)yN{SCUXd0&CmhCaPTT(4Z<~eDbMxK!M#TSGlsj96}@~H#-hOS|5 zsur^?Pu-O7A$+QK%$C2S zPfFyTmu0|#U_;DVy~45WrOKy#p1li@7t%-ZXJZQ zyEQ>O0x%f8*-t zNSJy)&*pYzUYXW=5okf9o!tY_#_Vmr(%`TCVc%yjf1;Kt@E6(J8=rI?>LQGO0|=XI zlOP%QT?MnFA>Fg;x$RH0YpfR8L;^iiXu2 z*7ec1HhQzcI%e%tU|DunsSHxB5!)BhQT)E|4yr&*D`m#f+OmF_`Rfu|S1L6ld82q9 zu^p_J6-4!{YA+81PXJ)Pk2RB!{dR2B`W0yHN-m^7S ziqS14vl417*pP8wT<$5lo=&&b^eiAs+~V8Xnq}f+B4Xl8GG)8|yjQg+BYcOiH0b@u z780!cVBobu)e5g|i+&Dp>+J!-R@a{Vcb^~f-cQljP))RPYrR`>SnsyFN?b%AmpFGe zS4n)gE$;^1OCHoqmQ-slk*IUbHbFaHuY$}3f-ky`OBV%0wTrUE0SQk{0qaf%*UZ6) zlgNcZbvplaqu%I-WHy4dVl--t$-JTOUT^;uEN|)K(#x&FL#hf}uGJfT+ikPcQ+^?& zRbpMrllV#zsn(uT4q^s(?GmZ zK8#I<&0pOu(||{@>f|$^$(-y3OC&)~V%%S9r3hv#pS`mRvH%Caxpk|nYc%J9qIL7O z=0%w)*St=Igzk+hilD`Pqy7E^ zGUj?q!E5~Gtq+YO3E+YODK&RD-|5<9*)kD+PUuGg5#jWW7)zQ%0}jMUKMIMh==F2( zkAQh2y=tpUzn&)0dSIv_FC?iCdBnVD0M36j##BM&d3rpGC7)3S0}VRsZrO> zoGi#Y#WOaE!MR)*k6kg&FRGD$o+@-u~A>ew{d3;Z2q>Nwo;bYh9x@;RvxEnm48 z?8f1``<6wt->dfh!Ny)N@GxB)pKcNobP3Pm`aU>J;d$X(n=pJC{LJ)OHTZYTirtiS zInYLHAUoAO9L;pj$!Y0yNS~LfqqzB@h@}9(b5zM^eJ5a#*UZ;5$cg^N6#CZGB4vn% z^-6RoXF9%1L7fH|a@;nTUg#HO6}d*-IEC|Ln?PFAh@}S_-WJpzO;>Chxxelx1quw4A9S8}YKuPNQy`RCD-1IH%3jyYS<07BVKkkXgpW zQeP5J1;0r=u+K}!4t8otA<60R+!y#^lbkQwx%VNLEHEURu^so;sC^oW6LvttSA60Fh_Oxn?mxQqYrlW-fB(T4X>dmPs|ApX}`YN)PNc) zSXpvQKNDzyruQe7h?l7S(`#XKJwp{69)PfM`1pH%CS0Ju%>_xu?%f;QE9o_P$(R%L z+XIV25rH`uCahQL^c~JvW4QSzLf2lkzHgfyJHA}hQS} zsqpz1GNT4LdXG()zQw0zZk&Q`o$Na@5Mxv2K?pC~a#+O6ZgY@Vk~v+KILEDkbo(JT zCzLTOkaY(7MfoZn1o&~*$`>0e>IeqLM4*;R{4vU@4+|ShFwv^;M;xI(1mo<{fEr7h+>Jmn$8MUX=@XE2HTSNSl&ux0V z%a7+M%bm?>o=4*1eN{)qd#+mU$@qsdV}XGRd$V_5dN))SAND*4zVy5NFX!_P9Gkf# zBtqbKh{j6Jw;18SC#8(4Y0!nt~AQrxH(n!g+%BmP9a>@QxFG`(OFBy(AxxZ$4Cq;;{0Ew0ripG)5Dpe(Shb;Tc}1fJs9>Zv!2 zaeOo@3BpZMlf0cg6`t|tFHyh}eQYxgl1+SGvYdF2F3(o)4!h1W>#7smdsBDI= z{h|@VBHsE>J*s$F!@fhKViFz8o&D$>2Pk&Sp3Ao`Yk#H^;Zl3jk69&3Ogb-ZI4>Rb zn3lP-Zc8f$@C%@#d`Nc?GKk1gmolsLn^a}%Pxo`XH>-Fri32@8({-z!zvQV*ZW=Tg z6I0xp`+Zu4>#OJN3^On?<=bHwPuV+PikcRk-cZ1Eaps}(_co`^P&j)>--CK>0k9Rx zUZj0%VW{XbFN^kJn9O%Rin37`FvH_(a=9CA+hv$h=IQKLg*iDcj$GOY+z$x zj(B@!G|D-6!#}ziK#;&>b$Vbu!|T~p)H$tRown6{kR>x(_<``RlV{mLj;=O!Rz|w) z7E{}%f{<;6-ThZw%NQiiXNua?5{vhEPru+5>L8}X3&nPY?~Y2(ob%2I3kDT-S6PWy z8j<{Yjp*FNF2DyB^pj_1Jo^~Le5gZWL4CIVXZ3Al>#NjHYbGYc|Kk7sCG|1ukAJkM zJGIF*e@r=!R(Rawjh>z5b1<%=k69`GZltsf{o zg(;9$!N-vLJHSyggwLRE;ANIo+<-$%0Jph_uAc7vJW4>Y{Os?qk zu&Q}OYQ8FnBFqf{j%CI!DrGa@WaOcP&ZXsX5h&6%J`4b(MilVr9 zNtmhNOl@uoC@qLN{Kz#CbjPgSoURT?(=gVu4vya0xQQ85e^-AeO84b-p3dj? zYeQjSU42rEu{BTnHyl9XevGK^bDs_*7S3mC#8kZ+Q#Fho3ty_&ww9X)1p=Rk`Dgq| z#Gvf7%7lW=Wn|Fq9SicxI^MzdUYXsb=t1Gx~ z4Ue~U@C>EH)}eiLk!GkX)0dAUFH9^Wk!uUbwY5IPF))|r#31j_0r4yD?(oK~Q*)Q(s=uAAvRJP-_V^P%r$Q&CU zL4y&6C5a4Vsu>d2bV{a2#2ZTztEFTS<3H)9*IWq1~qx)|PAJ9ucuxwKu3Y_L%nrhLRU3 zmCtCDv)~@xG-qa_E~OP#6}ukCoVZnuB+mRwyd}pLi2{)vAn6})7gOrmymgDRti>nl zDq0rYIdjPp>+cc1jELjsCDg=;-Wa~ zc9q^k&idu0Wh9weERPygRxX8NH1XQWkmoI5vRd{QExG5*y4+@#d0L zf7E0Pd&+P0;Kn+Vrn5iBL@@cLP9Txs3US7tvaT1nM{%~3-zJVM`rZOP)_sykq`G!Z zwvZbLV<^sfJQG2Ex{XjhR$!2NXPrk*&Y-b8(<^(|5HQAbobc~Z5ex<7INIB5K6avW zg|=OhKb7r`vo(e6DZ)NoF3qbcapea~D4K|!RG@#3D~56U-R-?)p6m;;+p7jZaR=v0 z$(Dt#Fi4$N>`o=HO5W7EhwIYjzM16rYs|JzF#$D4kMX-BB))f)U zTDpomzb(SUV_R^459v6QohU=Ux%bJZk4po08eR^HO!0owEEBnxmMPJ4+O?i?e@44B zqn+Q7BSmi_ai50=%NWXPD=foqonMWY#C2_JR^Jh-dbB2hDTr8MOAlD+x?aSD=^RmG zXLNl?1R~A_abN1c-J{YI`+2RrrUEBoa_f<#q0YTd@h=oIQVKFuNjN93iPms_X1ZOE z6SMeZ2F3lnmOkq82l~}m{;!<=Q?QF?Ll47nYVotNQ=bwp>ob!zI-Ov5TaWEvd8iRd z$_}qLrZ>A7EPM2Z+tu}=IozAf#h0?}2dXnD;fYkkZc91+R&<|JSv4{kTs!Z=I7HrC zxgXutm4`;=pDel7&>9$z-C_l>iUubY%305B$^p)xb-NFReo&+c%L=IX++pXLQ~6Jo z{vPUIPy24ahr1QQid>fRZN#(mdOHiekxOhGl|5$ z6ooWFmJ6)~aT`}x7FCuEn39E?zyvGJ=48(7)N0KgGo~4?*L=pj&bU87s(0ZVtaGuf zq2P{t-d?5V&jOTq?`|4A!t<9ykkry6D`m-10v zYB%k?(yM%(+T zu(&v6HK;P#?5_Fq4=Osd7k~sj@|*$V-LRbOZm6O-2h{wE3m>WMeVF7W;6wzbuuO^K zNR#GZ*x*TcnnZO-$i;(4W)`zTl*B8Qi)gca%ucinv+2SkTD4ax!jqsvSJ94FAzYD2 zZhQ3%`M#dgjHEhyQ+RIu&sqygIGFySkB{lBpbv^&^fA)6Hn%1spCy6I+drupjEV{Y z=@te|@>ct_IQp-~s%6L-toa|FSQ!U_nyK@9(w2~Km94B`d}yrD!}E0~A;F;bnKIR! zGQ*Ld$NlkNcLEQE`!Y&UqPj$oy!~1?2S_6}NTZ_n-$Kb>MCIqte`WIO{B(C0_0L7f zK5Y^z6TMOrHQLoFDPt%$F>|S7OY}*cE8ngN6P)a?Iel|ENCHz0YDH?9S1ZVvMjG8T z4K5zFn(UOpe`=+J{!HSEJfB@O9p@8>|Ko~XmS;YHTkLw@z0I1@jhra@Na+@fu6;v~2~^mo zF#j3HZ7MSpJM7g@YEOc*7+;FML*k7oX{-5)!8^$#nG8|(8fVm;B95pd4Ym^IwISW$ zvB)0er83iycaw_Z9`FlNzsay#(pfp$|gHKn5=6JlCJJt6?6|~QyLo? z!6?mSp!-_q`A|JLZmo+=uwqvM!<#-{KMN{0Yx9xF7Y~3mm)uT4uFizXaNKgV?ks%$ z;f88K&qol+&I>d};YczoZ7BKF4Z}jdx6c;P<9h|Y>GwI%m8{FS_S9Fr`O0HYs*15H zj^?S~;Olqr^UHC%&Z)#vRbH5vegcd|*P&}9yDQTvlL#us^D=LGaT3RFKRcuYO^uI@ z-@oQyL#!YC@e0Srn}-uOTM=5*@$rfMa@{vEwyg2Wz#Ez?EOQWZa+kyqxGHFP|5|`f z-&a}lG5NLpi+_6-s$gcf%ezMU39%0L0a_&yd5nTVdz`PQzTpx~S^#bZ6=jf!h$RZ! zXe`eI?6xYFs?IV|G=56nv2^uz?E+D>3wdT?iyqtiRsMAoIR?f+tE;AiPr$Je>l5|w z0@iIh_c9n=wG|W%=Y$B?MC9E2mwqmlKN-&`P-Yv{bv3ycKF8UydFP8ODAzUs zVBb-5AaPPB>UwW!OVl0(yz2GdPj0FVj5OLj{iIvmv^6Du$q4Or$OfGP!@y7AEcL&$ z@W>pXmU#(<)9@{kf2i?m*+0%5U&uIrjpUAZO5Phu*gdai?=P|I3V2>2`BpdeF8&W< zBFfosKQG!6uo<;nUh}0g-RQ>DLkX7cl_lI+Dl&={^KQiYrBs1Fm`O^LV ze&4!a8mUcES02ZZ!+tHKqwy{X%EuL+L*goD2(GJk@3S(9&$w%MS63VPT~`E0k7x4Y zJ1RwsN#AR<3MsBiFf@ zxaLM-x4#3z8silTmO31eQ;W-sYsN3lzFS_K5}HX-7=0`g5EviYqwn6=L%lwH zhlXxTUkdM7!CVcD?K8OQb+f%H%r`U(+ADbO@(P&*8&U`ahYdWZi)mT1*)!LsXxp=IU}v!jP(Fl5rlb21HUYd&vuUH zNtp?AV@taozpR3$_bB;Xyox!Og$>|{%?9Uy)10-c%cF2!Q?n75ZwS14)91lgU-fvk zcuM}XnulG$JlXG7NM_x7Lk(vNhcnhDd6#>@Mq;X6z@ z^c`Q?Ms;FH!ZBfNEQp=xJ;?e{T`KG=iimKj)1*1+Tq|C*8^@28xQDSE?X6d7CodrD zws#owkjOS*`%TN)A52J%j~l#59_b736#v>+NhzD;A%C)ujp_LG)U$fG`!U{bm$#Ay zN5|`fbgpYH-xXdx9V=r+eb;0UX)I>!+jw|C^CcJSinIT6UbI$7a z;&t7_aTMOaPV~H!2^kFghI&uM*5c^kfHcblb~%syF{0*iLI!t$BcD%mmB57DFRi_r z*s)=qn`;~0nz;;DFDws7V}cj_56sepTSsqi9Zw#aXp!_D&p(Gv{-VZTiRUm$_R9EK zieT_2%M3pDDDXYw`**x}Y}F#%Uq=YM!@8nj{4CEXpO*bjfkP|Nyk5pU!XBIo=!iAO z>gJr2=QhkPZOqNlh^E2{_0{pkFa;mU9m!tl2((H-bUCjk}czT5JM02b0ft# zSaw>}^GrN=7}XcNtnhRc*Pi_PY8<24hG$&q(pSa+-m;7Xvm%AKaQ+H0t&>0YeCTjE zH|R>)P52R}Fk2}lb&L+f&Fn3D^RoA?uW_>|R5M@@d5PQf@)Yz*{kk+ysU1G?MoXCp zeqaFj#a|xwz}j{vsr0o@DEM#N`Q!V+%g!W$VK>kRDl^nWjG2|mitxuuFgO4fG*T#9 zW~r~?BW!O1j&)$Z@|*@A!=$%G@9?;jNpo9jg4HI>gEL?4eL9E9gTKnPl!#-c)IpTY z7!UB10A}ceP}5P+W1ilr;~NPFa%=V48A;dz+gf^J`Da0#Bxw#DC_2DDtueyb2>-)<{H4v zyzhLB$SGcR7UrL~S&A-d!5^39VLwM~D+KK$sxTV0LNW%t$$F*_o;09gLd-R3-&(U4 zhNevLzNKo#xh#<{w#&cUz6Gq#813g5M&L@)|Ff~GthcDJ_{+nF6MjnMB0$+|raHzY z6cn)M3^r^3QNBv&*bU{oc2_qMjefd1gTP$keX70fwL@o;Vd6B|YE01yIE2pLZ8#kp zSyo5|@V=Qc?9pKUOGrwHb)TyY@9#0M2{?IpQKp3k1Um-BIV|%r^*`F}%EG&UL;YCV zYf9#eY|ZVEs@TM4u_hJvC|lZYOw_R;;xwN7NY+f2w=sZ2-nas>6lJ6Q-{VMSgMDTB zN^~~xET-H1fjGnz08SoWS!(`x$vXdvd@e4hUFYWAhGXgHDs%A|cm>)(NJ!W|b;H-^ zmHatZMwkkb^Im+hQ(f>t=Lx9|dd9;Mo`}bfe?bW!Z{4R*5f#kTs*i*r*VRQ7*!`mi z_|ZG$M++mf1N{bsW;<5=)uJqb@SfLNUN9)LMMVeJL=oLqKP}9Gbh*5B#gz=ysI7_o zwq}8v2aq)Qf4MKU-e3wZQusH_e+M3}4d0-}>scDzOneM%t zDHMvbsHBKBt53Le4iR&4tc{%K;*hr~5pz`+{hqKa-aeybe@(0nw5r zE`QQm@l=00oidNBK7qe|{~FOuGJSK2c_z7F#&4o7b-ns9#dBXjgrK(x{N97-TwChVRs15O zcxFhE;?E;(^CO0^Pg8$qE1WFGWA_t9)ngOk-6KP6_vfGmyh(k*2faZdiwfi}+W>;_ zza|JA78Na9L%o>O*0@+CqIMR%OB%f57Qgo~)~V`BwpCw%cKXv7!Q5x&8FMw*l4_^D z2z{le+kt6=v#c3vz^NaTWX;h%A~ut-gIVnmq>^k^6fNJXcw*g2v2#M%)BIV z<560D28*HR@9K>rMK2CyplaWyMlzy+R-U?&Gd@_>JJO73d4I^svDw9x)+r?UcmUbF zV{$CavR_SwU6v&w-&&I6uNB>uJO6Hf9leL#?lsPGj^|D;ZU3C&?DeMe*UU+}ML}`? zbA{vOuEpQ@I^>MiQ+8Gi1D=VVIhg`1I|=6QcOg%>&yy{*ZIk{^V(mo%6|5@>5(UoL8%K#nf-4=tLGU~IdKyPHdV zF=JyDE`8^`LT=qLvT@2mwdX%`zchNyG8pk1TSCbJRQ8D@P=P3;jnQ!V`L~(lqMv;M zIN)`LJIl^-SD`dY803tH-KvF&0Ar`7lw6i5MW*V0HM`HcR->Ma5)0)h23008EF<~5g=5G*C!%w& zG5h-Rl2$1Q`@_0)2EmigmYUkpvij;rZH1=y2L0nQ;RDdpJdzSh~v;H_vX%*?g zTgTW^h1AI-f5>5&?_#zk$;mS_uHmG!CkTWz3sI%W1Z@fAs5K#`NY%j~g(jN9oj=sG zVT=Rp&Nmtge-2v~xV~VQaXi|6Qb}Q6jYjb@N)+F6ix|9RnzR|Xz8Fbhp0={YXdz3; z!xN9Z13qB?h+sry?%su1VoK5IgKcl(WYse;O1}4?D9M2$B@TEQ1vaOM4%IcEf)l;9 z)1O$L?o1D{TneA=8-u>BKi>QB3hQ~-O(Iv1fH|V?=;%%)TODLgfk+BFeiS%-s>e&& zE=Ev~-aBm?tSQtxT9xqGn6ByAEB5BEpKj zq$iBac;a&P;RFUOl#oMn{%`)KLH)gw5^~yG>m&n9T$Xf4uwd!y&yJ`1Bz#YA`igx= zY);enGq1?#A*b!59mII3M%mJ4F3F>e1uS3F8E_OOQ+BTmKT~Ci3NO;MRyEatu6D#* zSSv-6qs>fqYIE4 zutB5w|55ekaY?P~`@iMRPTS1(Zl~o~S=r#wV45jn^I&R|^MK`)15nP2K;~8(B{Ma1 zsw^$%Awe7T^DgOc@X?1Zea`yfy~9;tUkYXB@>;TINu=+-5sy#Zd zw%vr98=JpzZlID8+1b&VsT@7VST{2*t0dl8b9YxfPHCl9f08>S)B1~F+t$-TLrECa zu&*GTUAYQY8j4F_^~y3nPvvLhyt5kfEfOK4b9Ou~V4cC3@Ug7DUq5{N} zfMOF|qq9CYy2K+Bor``LS~sx|zaq@>?|Qe;K^Be#NT(uA6>Q_h?n+`kAAX#3N<5h_ z-HR59G0aXV2DA_SL}p(SZ2Vd@6aK` z;-z=JSq3N7C>-CNA0Pj{%zSM&Ls^)MSj-w~DWp6Ffz64rSi>EbJ=X)x^VbvgoKrN7T;x2U{6R^8|jCb^ZTs^qMq`rNdEdoEU7 zD56u$CC3kLzO&>-qnd6Zk?CJmr}FJ8?>e>cgT5U!Z?$;*YUajH`xJw&~n7%lIiJCy=B1u4f4q z&q9QxI^-RzVwlHTsiF;ga(M@+Y-9ID()9iQuN{%G^Fx|`ow=hXXY@xJG1?J_5G~dp zLx&p}6~KrU-n^3-g0ZQ+o3|zF{j0tetLq*(KL%JaVP(1ae93QmHRdapI(auCGRQw^ zoN2wioN~2559CMc#yw{(2xj!tnf&^W>HgEo3)s*jx87Yw$=Wos6i0x%Vzyt^kG{MI zT~7wr~MIXKkXi!z+)E`F;ghbd|I61W=bB5ndN0* zksqFhAiV8z`)AR*l1J|7N$VEyX9&%bi26r#`8T~m^y|>-*=YdCa&IX0cK{Z!9lE*f zhiL$y89j)Rc`pRGdWg1K*zAf`X!TYuKhsls(VgU9m|=ev_2$X8ru@=&UM)pl;V*QY zM1`Y0x3XA`oXdGu2V*@#KEjsrbI1;$>-V2dM2Nmc@yU~qfyGgQVQM%N{%QOWUhvk= zi&cZ4US2Y5*Ecvi-JPa6j_`0eG4*q561UgX@O;#G=)u^fT8x)KZ{EeWnLXxABXl zT?InR&$9T|lPMhz9$!i)9Brol6}$A<5lEe9cja19p~@kUg3eapB3ILM3pu_^c$$@* zm@luqoVSHZjT(=zi(Qs@M2JE)RS0&-uIG^!r zmGBIWq(Z-u&nJVr^8~Z%Onl3kZzUNN!UDX5>6Wv6X5G?q zY_0$BYAjb}2CK84+mJ$E98o?J-FHeH+j?9qq(b~?Pd3I1($(paCp7=+c!S^>kb}Rx z-U-yOBz)ia!SuRr1#SBRzO|es`($}EGKa`@=N3DM({v`fJzAsx?%q(aP3PubO0Eg{ zfD5jLRh-KrKs>!owiMZ?xq#ywcS+)%Zn(_)emVcig*tTMqnr@!8<_Z;_MN-47XxQd zSU*dYjx=OZdVLdU*Y^AzFD(5WR=4hB)6W*Gf4=X}e6M6V6rQ&*ZTizF^1$FK-MV8V zfb-db>`DMYPym&o!|DytoOzAfli7DaLh9WXA*yT~k(+AhuiyN5{q(JtSR<61^}$;e zzbTb^fiGk0RS11n9_g@#YkpTUYZqB;6rod*w2 zl^}Q*j#h>fGUcRCL*lduu;q+wp$=y z4GvPtIAASfDCb^_SLFz!RoqOiS&(W@oq7 zvWby#y0jACr>1+U{G227FpN6^<;_pWMS5M!Yf_{(c2-OE2Z!I1tJWm?2Q}$c>U^Sk zFP5l$5w8D1YDhK5#2wNJL+|6&h@GEjSI>u|SbnC6(xbHDhtzOOxi7cK1#G5|Bck4fk9lgDUs)1hp^l=e+=~!$? z&8w&JxZb94--EHL0|!e=-04N1YW~f;S^?EA8G%#97x&s{4^C_*^?ZF&U^Xt}HcvcA z3yS>Xw|mUSZR=)&3;+;g*uG^^)4} z*L($veF$0QbV>&s9DguU0FZe=;$DNP`9g(}KuvA2Glk!IH7nork7f#q8FP_`|Di=a zRrc<~adFI+fXf-(7CJ0KfjOec=zwWR^<{%&?T#;N?-1r2zM4PJ(=9k^we9x~DFj*c zCakdZ}tTTj!ySvD1x^^Ipe1j$B|JZvG1cStD)%`+tnYGhuPq& zgPWaA3oOyF#WP%<)A2L@zPcc1-yqEb~@{3635 zObo(ZG}Fj1IA%>0mMy(MRB0O*&EIdZUOjS6!@4XX>BerIOjUUE)qt)X?sPXn&`S0r z>PAOyWze3C?MCJKTxp9bkS02ow=Q1WKG;|Qq;cyz=p)eC(efPChsL%z zy`938x7!c`jbhb@#p0hW3G$g;qTwo*KV92^04U_RDogoXu(+- zV=pgS-CFu-a1d8!Tlt~lMaipR5%ToD-TqryFb4@br1D^Op z?3IwwyG!L`tBE7-eFfpm1^!Q9{I78e=2KmR^N^^7dBWG>n)k^<^ZFl4`ot_YIL2!9 z8u~G?CIfTPRZ>F58}H8GA5K4Tr3fY0O%Zn-0n^jii0T@jv$f#VC}x0_m_2~$d@))()#n0vR*9BhOx~ok zFI>~;S;5}aaO%O~&xnCP4Bp!n_ta3$X=GwP;@WgpMrSsf=&lz(v3>u6eJ}1_-;2l2 z3)hc%(|F(A6Wrv=ppwq9-$bo6wvzuZ1^vD?)+d8ast{irCT@ zE-oGZBD#qJHN8PrXzH=IJ3o6P5i{^JYUEymuDfH%AE@sRjbt(GxHvlqfWH>r-4gXEbco%zhK!{oKREA>Gh2-TIw z+dsYCt{Z@#NS#s%%keG=HqJPaj#@1j+kO6(h|@@-5~BEWzNHr|=BKwBx706J{s#;a&B4MeT(T;{xZBxwmER78)4(bg6}9W=Dc&(f z8#&+$MP0AnNQW-FOLi^Rlwi$i@Z}Z!spd@gg&^C8 zpqJiGE}StGakUR&V%^=*+5bAu0>3~f)^o=6L(z1kzNM|{L6d!VvfPqtk~oI?5idiD z3ww<-Npe>!g`;7+TA=s$X(`sSE70U6pU0FRns=vP1Qw{rEk%x`ke{@Lg&+hBR~WH> z{>3UjItwOQ$?16H{FWRzM1Oc^v=v&)bBxa%izSz{22W!yW3jl%8ubm0Q2bLJJey$5 zgWpypge6pz*5%;1V)DBUi_`3a3&(ZRq?JwMPAL{R64t7HQ%{IR!t5%-0b6XyMvr@q>9BlXwbH1Zd(`Jc*(HE@ks%^TT9JG~ z>_4LHAggX}Qk^Kiv^VHVbA3}+(pwoqkO@5pJjy7RS_#FCt=xB6G zp4Syxu9E}WQgU9DGI=k*LCt)8&?!Y>4HWE$gf7u}0DXxJ%vsvpqJ5kY%uk)AuBN$ZmR0Y;U@ z{}ix~+?AMh^s%bg5maMbh9hcaxGtB{xv|}wHox!ex^+haTjFEcWC|S$z4JL=OM(li zt{?@*N%c|M22Ks?PkKdfo3W!OOE|%-(gJiCbu?`19BjwG{<)Pn2dtuxo}^&ni`m*^ zzfd)=%YHyGBEP0({-^{2ct&(+U)&C;tq=9hq^vaGJHqzJEYVa zLrosX;vp@CJ_s4VODPDhq0`N{9ETdOp)LuK{d zQy{nUFxcTgA5=m)j{V+TF6nCx4FklE{Ku8K2}5i#iMwL z{SVv(_;ZEhpty@SQ3Jzs&apP3%{OaUdp#}$?C5=j7$SRuJ^bkN^J9!1ooo=9j(r$>jeYS_>Syb>n}a<*v$yWD&ob zL}%9hKU^fhH$7=~ZUNo^9RA|)@B8MTlW>hN!eiwYcaAkQ?ST8B(Eo?hCm^QV$s)^WelYBJ$%^@lTs)hnERT7CS& zfA@KmR`W;1te5%POX7Im>Psr8&$RO3YrDVtuG+2lfle%30q2?gx}296ed=n}4|!KQ zDjDUGDZbTH#QYDsU(GH=E8wKs2miSMqM;s$I4_80#7<97h@UIv87X}pYnj|*>-fuu z`vDA59!c4@NyV1rx}gC%0yjE~QMX;F4~~$w-K)_0_c>OYtCwzR9eu|@O44H7XR-3{rUq{ zlGvGv{9hc(>Mj5I27sfk%3t=$?SX3?8`=|&>$IJF!zWIUw!;UJIp@7Nq%TIpm z)5%|F`}a?k%7f=#MZczlw`t+={~iT^y{O$;zd{C@iT`uZCl5Gj(Y6i$JCI8M4CH^E zF27E&(-yL3+t>fO6v5+fGx@)UBI5sl-MuX!Bx^Q-rHCS zuw9JBjoC}n?SrGW?-1-ix6^UxG1Al*cGG1fqNec zpt-yImz;zJ@i0dMC*s}ykjs`Qd9m?g8P4B6g7=hK&q8o&G3i-(cV%07H4ww6nq}4+ z(+tUs>`1(VF-1FExNfC6`*1W?CxZ9!$oy-NU*ja^S&m*4)yv!4X@3?o^6@egaej2+ zZv$36*)$^SJ!c3r9!>)2deX*!2Db|PdM(KQ*Zg))`DGnZRq;RR7L-x}zxmTnzsZ;> zU#!)sk&f4ls%Zb-zm%5@r!)y(%{L60*HELZRPP-tKt>gS(1aIGP91M+!dhOj+u(#3 zI2+joOgXJnkH*{KTAh!SM#gdgVNV9=yp(cW79b#vdD%pD`r$dVq<0?04mA|4RY#G* zxl7J_xBhd}SDC+Ew)jPByk+{EQkly7`I?lCT<{f%XpZrQlVb2ktfJJEHIb!oH1|U9 z{>6iiYdww^ETrr?`0V1nO=WxHZ3;XQ!|N8=%A3+A5{J{bmhas50_Z3_Yi(M)eWbY3 zXB25Q9f%h^OnLt^MLo_PMVfXE zlCR@3k~U${V}dQ@n;7kl>(ZqTfM@MkHh6rvunSZy0Dyc4f#ndl5@!?DY?9c~0TaKf zW<5S*P;sQjK$_c}32+bTgxo@kqoEgjc*F)udw;3G&f`3SFdzA%(;I@tj9C+~k>$;I z>v*a_6e7Z{mTnI1NnN+Kfj=6u2TVgyr4myghRIL*8WMlOJjx-FVCZebTJYoS~X)@K&6OfaW!&a`j*}x4wCMB zZizkY?Wh))yFZwXuR|8y5S1riNhF+S;U^c|EIf@Xk-|5g;hm#l->#DC@VfJnx6wUg zhdYMJ!mEf%j^~Y;;p%dBn=oO@#(i-47^mEp~ZM;$JBiYt@h~Ig;ehf7RFr|$H4+UOlZ@2f=FLd%XDyXq||~yHLstH zeor&C;+)tmeD69bt*~eLFljQpZz%|F7ppK6#^0Dj!0e&TRy|X7VL5~B(R?&5b9)2D zFVY9bTM2yrdvxP1nowSZ)<2^SzW%O){r1tdX2nKaJ_^l2JO0f;-IyM4Q;h_7lD&k! zXXYfv2Qbmj`>H1qo#B})UDvNeN;%)d$-XqJ@02^lTFTYE7Duh*jNsO z*ASJ#|GIP)MUZy&jW2ZQ)!xYJt8jTug>#O3yIom~D!$j}V9yQzubS=AO{OF_kg_o7T-j1K$pYTR! z4Ypq1Vl~k~YF*kOSb7c?slK@I&;p`Vb|UIe$tchYmqKFB1@z=_*Wz!1Vu@{gbI3!1 zxswN^Pu7`Jn#3j6KYtuG(GkRf;#%ung<}EhEfphMyf*vDrJbsaBV>i4OI}>+JhnTk zg9Ny&nsQ^?oPJeCo%U?m*CdUoe$;f6WtiwXrbEh%pFOpvv0y^BzC*zF+@XvRdCV@vw@tMd<1;jHjKl)j2U6`7I+5%;P$s@JoYeNb*F5_`ZL0G+C38))q3$~t4d#rd;oi1{mu~`|%K{`I{Y-0_pnns>X=blyvDV{V*I#Nvh>b6MQR5reN zoC@lQRC0A+u?*<{4L?a#@5llxX700f)XauCR7lV9)or6l4)=Kh*r-0<1xLzrwBB`; zO^JMS0pL5bK7sZ2tmoA`qHyLH+MKr7yNx!P{iN@!7 zwZz=msRFaGr5V%-2bhANxHwtQ^z<)WhdV!OZo|Ty$DX*bTdw26;VcGcgV6a+NT#p- zeRnH!9aNv&wc-ipfCZDKqWUQ?*+!kshG zi;_9eT6q#F>4eDROG0r$tH*AVl0Ebgw&N?y2KXQEjqz-K zr2HmC`pFaN0S@VCkN^^GR84(UPK5FwsJz#`$qCunFAyp0Y~=C~oI*3i7XS5u8tW2+ ziHMl(=W(Ig(9Qk!>hkmB(rHytzSBh@F)oSX@5#!Kc{Tq@?94@F<^fo5&oEN=t4PGu zRN|&AG)Ve4j~33795sEGhog@f)7CdYfY#k<7#N3`qcs=jzU9PxuRxe>_y*Xx=H!cI zO{)`)HMXbSRPRY6I402pN3BafIYJidj6!-4q(m7Qm43oKK(}}XL1TmW5yyrXz6+&qSVN7SaM%FaI zOqovM(hAT&O2YKf+&v&~`H%Z8CTH_JkT9ZD5D2^mQoIa*M=*ZrP8*T3Znq{|hkj(J z!b|u;UaFRPs~3N&)HE&jv1W%Z8vnKlty<20Zv!L~#j}kfC=n!iz65k8@@q)i&5`JdwaChbQ#Nv*wkX#cum~)^wQQ8&N{H(Wl#tT$|+^;#uUWZ_Y+GX?D7{W4oaTNh4H`Sbl zT{#C38S$PNWh)rge=)`;=-UlT(R9drG=`cXS_H~I`w>uBULoyC^$-H^9cE|A~o?9jD?h2^%yYE4Y+R%yI4h0 zE>Qd|r&sVvzkE1+k~r=O=rZ_dZsb%o%=X*?RX%)>qHl^?N!5PfqinosEYtdhe7vjNK3^a9TrY7G!Y*Jalt^vpOwg&IAcx zbD!isCW%*Q5?8M&2n z9sI}q`im+dy>(eC_o6OicdYQW&W&v7JLm`|IXT)>ZH4RAewKwD$;i7S_db0ew#Uo! z0=#UI`=!(*O`RaU6g3uMHwqgJ(7N1{MqQqo`we6%?`w0!ZC~B4N;f44&c!ZvyZF(r z=CotA;nSyzaKx1x4y&|1{l$pr(YV2)^6}wQjXbSa#J6kM>mlgAcXe~>#MHj@9)5>@ zG{f%n1K#4BnF-kJHM^x>Jz3GdfWrDfhiBqk8G3$s<5vKdYH@9Kp z%f*=uaX(OixL6@EcgjvK(`ewo{Ii7iIzM#?X4@wO&&FbAU>96(#|QvR8t93fu^Nn8$BqT-L9rJ?e4@HfH&>ZWT4WMoszV! zgmN4{qsu7ba;<*%dI9cePh?AONTIcV&5Mwj+Z>Ca=?KjSd*8Nljy-7bP`*wh8I4p` zO(?5bf~6CUn~W=+&P4TXkz>2(ZJ>yN#*(X;(h3mk6`2J>ZnEgVa+|DnzN7W)<74r$ z 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 Date: Tue, 13 Feb 2024 18:44:45 +0500 Subject: [PATCH 0008/1506] perf: use context for shared reports access --- src/App.tsx | 4 + .../LHNOptionsList/LHNOptionsList.tsx | 6 +- src/components/LHNOptionsList/types.ts | 3 - src/hooks/useOrderedReportIDs.tsx | 145 +++++++++++ src/hooks/useReports.tsx | 44 ++++ .../AppNavigator/ReportScreenIDSetter.ts | 13 +- src/libs/SidebarUtils.ts | 57 ++--- src/pages/home/sidebar/SidebarLinksData.js | 228 +----------------- src/pages/workspace/WorkspacesListPage.tsx | 12 +- src/types/onyx/PriorityMode.ts | 6 + 10 files changed, 228 insertions(+), 290 deletions(-) create mode 100644 src/hooks/useOrderedReportIDs.tsx create mode 100644 src/hooks/useReports.tsx create mode 100644 src/types/onyx/PriorityMode.ts diff --git a/src/App.tsx b/src/App.tsx index 7c1ead1d86d3..712b82c21291 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -26,6 +26,8 @@ import {KeyboardStateProvider} from './components/withKeyboardState'; import {WindowDimensionsProvider} from './components/withWindowDimensions'; import Expensify from './Expensify'; import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop'; +import {OrderedReportIDsContextProvider} from './hooks/useOrderedReportIDs'; +import {ReportsContextProvider} from './hooks/useReports'; import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; import * as Session from './libs/actions/Session'; import * as Environment from './libs/Environment/Environment'; @@ -79,6 +81,8 @@ function App({url}: AppProps) { EnvironmentProvider, CustomStatusBarAndBackgroundContextProvider, ActiveWorkspaceContextProvider, + ReportsContextProvider, + OrderedReportIDsContextProvider, ]} > diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index a55d329f39ee..5ab105eda8df 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -5,6 +5,7 @@ import {StyleSheet, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import withCurrentReportID from '@components/withCurrentReportID'; import usePermissions from '@hooks/usePermissions'; +import {useReports} from '@hooks/useReports'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import variables from '@styles/variables'; @@ -22,7 +23,6 @@ function LHNOptionsList({ onSelectRow, optionMode, shouldDisableFocusOptions = false, - reports = {}, reportActions = {}, policy = {}, preferredLocale = CONST.LOCALES.DEFAULT, @@ -33,6 +33,7 @@ function LHNOptionsList({ transactionViolations = {}, onFirstItemRendered = () => {}, }: LHNOptionsListProps) { + const reports = useReports(); const styles = useThemeStyles(); const {canUseViolations} = usePermissions(); @@ -134,9 +135,6 @@ LHNOptionsList.displayName = 'LHNOptionsList'; export default withCurrentReportID( withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, reportActions: { key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, }, diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index 58bea97f04c9..c58770c8383f 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -15,9 +15,6 @@ type LHNOptionsListOnyxProps = { /** The policy which the user has access to and which the report could be tied to */ policy: OnyxCollection; - /** All reports shared with the user */ - reports: OnyxCollection; - /** Array of report actions for this report */ reportActions: OnyxCollection; diff --git a/src/hooks/useOrderedReportIDs.tsx b/src/hooks/useOrderedReportIDs.tsx new file mode 100644 index 000000000000..3217830054de --- /dev/null +++ b/src/hooks/useOrderedReportIDs.tsx @@ -0,0 +1,145 @@ +import React, {createContext, useContext, useMemo} from 'react'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import {getCurrentUserAccountID} from '@libs/actions/Report'; +import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; +import SidebarUtils from '@libs/SidebarUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Beta, Policy, PolicyMembers, ReportAction, ReportActions, TransactionViolation} from '@src/types/onyx'; +import type PriorityMode from '@src/types/onyx/PriorityMode'; +import useActiveWorkspace from './useActiveWorkspace'; +import useCurrentReportID from './useCurrentReportID'; +import {useReports} from './useReports'; + +type OnyxProps = { + betas: OnyxEntry; + policies: OnyxCollection; + allReportActions: OnyxCollection; + transactionViolations: OnyxCollection; + policyMembers: OnyxCollection; + priorityMode: OnyxEntry; +}; + +type WithOrderedReportIDsContextProviderProps = OnyxProps & { + children: React.ReactNode; +}; + +const OrderedReportIDsContext = createContext({}); + +function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextProviderProps) { + const chatReports = useReports(); + const currentReportIDValue = useCurrentReportID(); + const {activeWorkspaceID} = useActiveWorkspace(); + + const policyMemberAccountIDs = useMemo( + () => getPolicyMembersByIdWithoutCurrentUser(props.policyMembers, activeWorkspaceID, getCurrentUserAccountID()), + [activeWorkspaceID, props.policyMembers], + ); + + const optionListItems = useMemo( + () => + SidebarUtils.getOrderedReportIDs( + null, + chatReports, + props.betas ?? [], + props.policies, + props.priorityMode, + props.allReportActions, + props.transactionViolations, + activeWorkspaceID, + policyMemberAccountIDs, + ), + [chatReports, props.betas, props.policies, props.priorityMode, props.allReportActions, props.transactionViolations, activeWorkspaceID, policyMemberAccountIDs], + ); + + // We need to make sure the current report is in the list of reports, but we do not want + // to have to re-generate the list every time the currentReportID changes. To do that + // we first generate the list as if there was no current report, then here we check if + // the current report is missing from the list, which should very rarely happen. In this + // case we re-generate the list a 2nd time with the current report included. + const optionListItemsWithCurrentReport = useMemo(() => { + if (currentReportIDValue?.currentReportID && !optionListItems.includes(currentReportIDValue.currentReportID)) { + return SidebarUtils.getOrderedReportIDs( + currentReportIDValue.currentReportID, + chatReports, + props.betas ?? [], + props.policies, + props.priorityMode, + props.allReportActions, + props.transactionViolations, + activeWorkspaceID, + policyMemberAccountIDs, + ); + } + return optionListItems; + }, [ + activeWorkspaceID, + chatReports, + currentReportIDValue?.currentReportID, + optionListItems, + policyMemberAccountIDs, + props.allReportActions, + props.betas, + props.policies, + props.priorityMode, + props.transactionViolations, + ]); + + return {props.children}; +} + +const reportActionsSelector = (reportActions: OnyxEntry) => { + if (!reportActions) { + return []; + } + + return Object.values(reportActions).map((reportAction) => { + const {reportActionID, actionName, originalMessage} = reportAction ?? {}; + const decision = reportAction?.message?.[0]?.moderationDecision?.decision; + return { + reportActionID, + actionName, + originalMessage, + message: [ + { + moderationDecision: {decision}, + }, + ], + }; + }); +}; + +const OrderedReportIDsContextProvider = withOnyx({ + priorityMode: { + key: ONYXKEYS.NVP_PRIORITY_MODE, + initialValue: CONST.PRIORITY_MODE.DEFAULT, + }, + betas: { + key: ONYXKEYS.BETAS, + initialValue: [], + }, + allReportActions: { + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + // @ts-expect-error Need some help in determining the correct type for this selector + selector: (actions) => reportActionsSelector(actions), + initialValue: {}, + }, + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + initialValue: {}, + }, + policyMembers: { + key: ONYXKEYS.COLLECTION.POLICY_MEMBERS, + }, + transactionViolations: { + key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, + initialValue: {}, + }, +})(WithOrderedReportIDsContextProvider); + +function useOrderedReportIDs() { + return useContext(OrderedReportIDsContext); +} + +export {OrderedReportIDsContextProvider, OrderedReportIDsContext, useOrderedReportIDs}; diff --git a/src/hooks/useReports.tsx b/src/hooks/useReports.tsx new file mode 100644 index 000000000000..c4082149cbf4 --- /dev/null +++ b/src/hooks/useReports.tsx @@ -0,0 +1,44 @@ +import React, {createContext, useContext, useEffect, useMemo, useState} from 'react'; +import Onyx from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report} from '@src/types/onyx'; + +type Reports = OnyxCollection; +type ReportsContextValue = Reports; + +type ReportsContextProviderProps = { + children: React.ReactNode; +}; + +const ReportsContext = createContext(null); + +function ReportsContextProvider(props: ReportsContextProviderProps) { + const [reports, setReports] = useState(null); + + useEffect(() => { + // eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs + const connID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (val) => { + setReports(val); + }, + }); + return () => { + Onyx.disconnect(connID); + }; + }, []); + + const contextValue = useMemo(() => reports ?? {}, [reports]); + + return {props.children}; +} + +function useReports() { + return useContext(ReportsContext); +} + +ReportsContextProvider.displayName = 'ReportsContextProvider'; + +export {ReportsContextProvider, ReportsContext, useReports}; diff --git a/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts b/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts index b4bb56262860..83df18d5492d 100644 --- a/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts +++ b/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts @@ -3,6 +3,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import usePermissions from '@hooks/usePermissions'; +import {useReports} from '@hooks/useReports'; import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as App from '@userActions/App'; @@ -11,9 +12,6 @@ import type {Policy, PolicyMembers, Report, ReportMetadata} from '@src/types/ony import type {ReportScreenWrapperProps} from './ReportScreenWrapper'; type ReportScreenIDSetterComponentProps = { - /** Available reports that would be displayed in this navigator */ - reports: OnyxCollection; - /** The policies which the user has access to */ policies: OnyxCollection; @@ -59,9 +57,10 @@ const getLastAccessedReportID = ( }; // This wrapper is reponsible for opening the last accessed report if there is no reportID specified in the route params -function ReportScreenIDSetter({route, reports, policies, policyMembers = {}, navigation, isFirstTimeNewExpensifyUser = false, reportMetadata, accountID}: ReportScreenIDSetterProps) { +function ReportScreenIDSetter({route, policies, policyMembers = {}, navigation, isFirstTimeNewExpensifyUser = false, reportMetadata, accountID}: ReportScreenIDSetterProps) { const {canUseDefaultRooms} = usePermissions(); const {activeWorkspaceID} = useActiveWorkspace(); + const reports = useReports(); useEffect(() => { // Don't update if there is a reportID in the params already @@ -83,7 +82,7 @@ function ReportScreenIDSetter({route, reports, policies, policyMembers = {}, nav !canUseDefaultRooms, policies, isFirstTimeNewExpensifyUser, - !!reports?.params?.openOnAdminRoom, + !!route?.params?.openOnAdminRoom, reportMetadata, activeWorkspaceID, policyMemberAccountIDs, @@ -106,10 +105,6 @@ function ReportScreenIDSetter({route, reports, policies, policyMembers = {}, nav ReportScreenIDSetter.displayName = 'ReportScreenIDSetter'; export default withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - allowStaleData: true, - }, policies: { key: ONYXKEYS.COLLECTION.POLICY, allowStaleData: true, diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 2347f5b9f5c5..e51276c5c28a 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -2,12 +2,12 @@ import Str from 'expensify-common/lib/str'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetails, PersonalDetailsList, TransactionViolation} from '@src/types/onyx'; import type Beta from '@src/types/onyx/Beta'; import type Policy from '@src/types/onyx/Policy'; +import type PriorityMode from '@src/types/onyx/PriorityMode'; import type Report from '@src/types/onyx/Report'; import type {ReportActions} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; @@ -78,59 +78,23 @@ function setIsSidebarLoadedReady() { resolveSidebarIsReadyPromise(); } -// Define a cache object to store the memoized results -const reportIDsCache = new Map(); - -// Function to set a key-value pair while maintaining the maximum key limit -function setWithLimit(map: Map, key: TKey, value: TValue) { - if (map.size >= 5) { - // If the map has reached its limit, remove the first (oldest) key-value pair - const firstKey = map.keys().next().value; - map.delete(firstKey); - } - map.set(key, value); -} - -// Variable to verify if ONYX actions are loaded -let hasInitialReportActions = false; - /** * @returns An array of reportIDs sorted in the proper order */ function getOrderedReportIDs( currentReportId: string | null, - allReports: Record, + allReports: OnyxCollection, betas: Beta[], - policies: Record, - priorityMode: ValueOf, + policies: OnyxCollection, + priorityMode: OnyxEntry, allReportActions: OnyxCollection, transactionViolations: OnyxCollection, currentPolicyID = '', policyMemberAccountIDs: number[] = [], ): string[] { - const currentReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentReportId}`]; - let reportActionCount = currentReportActions?.length ?? 0; - reportActionCount = Math.max(reportActionCount, 1); - - // Generate a unique cache key based on the function arguments - const cachedReportsKey = JSON.stringify( - [currentReportId, allReports, betas, policies, priorityMode, reportActionCount, currentPolicyID, policyMemberAccountIDs], - // Exclude some properties not to overwhelm a cached key value with huge data, which we don't need to store in a cacheKey - (key, value: unknown) => (['participantAccountIDs', 'participants', 'lastMessageText', 'visibleChatMemberAccountIDs'].includes(key) ? undefined : value), - ); - - // Check if the result is already in the cache - const cachedIDs = reportIDsCache.get(cachedReportsKey); - if (cachedIDs && hasInitialReportActions) { - return cachedIDs; - } - - // This is needed to prevent caching when Onyx is empty for a second render - hasInitialReportActions = Object.values(lastReportActions).length > 0; - const isInGSDMode = priorityMode === CONST.PRIORITY_MODE.GSD; const isInDefaultMode = !isInGSDMode; - const allReportsDictValues = Object.values(allReports); + const allReportsDictValues = Object.values(allReports ?? {}); // Filter out all the reports that shouldn't be displayed let reportsToDisplay = allReportsDictValues.filter((report) => { @@ -173,10 +137,18 @@ function getOrderedReportIDs( const archivedReports: Report[] = []; if (currentPolicyID || policyMemberAccountIDs.length > 0) { - reportsToDisplay = reportsToDisplay.filter((report) => ReportUtils.doesReportBelongToWorkspace(report, policyMemberAccountIDs, currentPolicyID)); + reportsToDisplay = reportsToDisplay.filter((report) => { + if (!report) { + return false; + } + return ReportUtils.doesReportBelongToWorkspace(report, policyMemberAccountIDs, currentPolicyID); + }); } // There are a few properties that need to be calculated for the report which are used when sorting reports. reportsToDisplay.forEach((report) => { + if (!report) { + return; + } // Normally, the spread operator would be used here to clone the report and prevent the need to reassign the params. // However, this code needs to be very performant to handle thousands of reports, so in the interest of speed, we're just going to disable this lint rule and add // the reportDisplayName property to the report object directly. @@ -219,7 +191,6 @@ function getOrderedReportIDs( // Now that we have all the reports grouped and sorted, they must be flattened into an array and only return the reportID. // The order the arrays are concatenated in matters and will determine the order that the groups are displayed in the sidebar. const LHNReports = [...pinnedAndGBRReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report.reportID); - setWithLimit(reportIDsCache, cachedReportsKey, LHNReports); return LHNReports; } diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index 3bd538e8beab..42681fea451b 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -1,11 +1,8 @@ import {deepEqual} from 'fast-equals'; -import lodashGet from 'lodash/get'; -import lodashMap from 'lodash/map'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import networkPropTypes from '@components/networkPropTypes'; import {withNetwork} from '@components/OnyxProvider'; import withCurrentReportID from '@components/withCurrentReportID'; @@ -13,13 +10,11 @@ import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalD import withNavigationFocus from '@components/withNavigationFocus'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useLocalize from '@hooks/useLocalize'; +import {useOrderedReportIDs} from '@hooks/useOrderedReportIDs'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import SidebarUtils from '@libs/SidebarUtils'; -import reportPropTypes from '@pages/reportPropTypes'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -28,107 +23,30 @@ import SidebarLinks, {basePropTypes} from './SidebarLinks'; const propTypes = { ...basePropTypes, - /* Onyx Props */ - /** List of reports */ - chatReports: PropTypes.objectOf(reportPropTypes), - - /** All report actions for all reports */ - - /** Object of report actions for this report */ - allReportActions: PropTypes.objectOf( - PropTypes.arrayOf( - PropTypes.shape({ - error: PropTypes.string, - message: PropTypes.arrayOf( - PropTypes.shape({ - moderationDecision: PropTypes.shape({ - decision: PropTypes.string, - }), - }), - ), - }), - ), - ), - /** Whether the reports are loading. When false it means they are ready to be used. */ isLoadingApp: PropTypes.bool, /** The chat priority mode */ priorityMode: PropTypes.string, - /** Beta features list */ - betas: PropTypes.arrayOf(PropTypes.string), - network: networkPropTypes.isRequired, - /** The policies which the user has access to */ - // eslint-disable-next-line react/forbid-prop-types - policies: PropTypes.object, - - // eslint-disable-next-line react/forbid-prop-types - policyMembers: PropTypes.object, - /** Session info for the currently logged in user. */ session: PropTypes.shape({ /** Currently logged in user accountID */ accountID: PropTypes.number, }), - /** All of the transaction violations */ - transactionViolations: PropTypes.shape({ - violations: PropTypes.arrayOf( - PropTypes.shape({ - /** The transaction ID */ - transactionID: PropTypes.number, - - /** The transaction violation type */ - type: PropTypes.string, - - /** The transaction violation message */ - message: PropTypes.string, - - /** The transaction violation data */ - data: PropTypes.shape({ - /** The transaction violation data field */ - field: PropTypes.string, - - /** The transaction violation data value */ - value: PropTypes.string, - }), - }), - ), - }), }; const defaultProps = { - chatReports: {}, - allReportActions: {}, isLoadingApp: true, priorityMode: CONST.PRIORITY_MODE.DEFAULT, - betas: [], - policies: {}, - policyMembers: {}, session: { accountID: '', }, - transactionViolations: {}, }; -function SidebarLinksData({ - isFocused, - allReportActions, - betas, - chatReports, - currentReportID, - insets, - isLoadingApp, - onLinkClick, - policies, - priorityMode, - network, - policyMembers, - session: {accountID}, - transactionViolations, -}) { +function SidebarLinksData({isFocused, currentReportID, insets, isLoadingApp, onLinkClick, priorityMode, network, policyMembers, session: {accountID}}) { const styles = useThemeStyles(); const {activeWorkspaceID} = useActiveWorkspace(); const {translate} = useLocalize(); @@ -141,19 +59,8 @@ function SidebarLinksData({ const reportIDsRef = useRef(null); const isLoading = isLoadingApp; + const reportIDs = useOrderedReportIDs(); const optionListItems = useMemo(() => { - const reportIDs = SidebarUtils.getOrderedReportIDs( - null, - chatReports, - betas, - policies, - priorityMode, - allReportActions, - transactionViolations, - activeWorkspaceID, - policyMemberAccountIDs, - ); - if (deepEqual(reportIDsRef.current, reportIDs)) { return reportIDsRef.current; } @@ -165,29 +72,7 @@ function SidebarLinksData({ reportIDsRef.current = reportIDs; } return reportIDsRef.current || []; - }, [chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs, isLoading, network.isOffline, prevPriorityMode]); - - // We need to make sure the current report is in the list of reports, but we do not want - // to have to re-generate the list every time the currentReportID changes. To do that - // we first generate the list as if there was no current report, then here we check if - // the current report is missing from the list, which should very rarely happen. In this - // case we re-generate the list a 2nd time with the current report included. - const optionListItemsWithCurrentReport = useMemo(() => { - if (currentReportID && !_.contains(optionListItems, currentReportID)) { - return SidebarUtils.getOrderedReportIDs( - currentReportID, - chatReports, - betas, - policies, - priorityMode, - allReportActions, - transactionViolations, - activeWorkspaceID, - policyMemberAccountIDs, - ); - } - return optionListItems; - }, [currentReportID, optionListItems, chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs]); + }, [reportIDs, isLoading, network.isOffline, prevPriorityMode, priorityMode]); const currentReportIDRef = useRef(currentReportID); currentReportIDRef.current = currentReportID; @@ -207,8 +92,8 @@ function SidebarLinksData({ // Data props: isActiveReport={isActiveReport} isLoading={isLoading} - optionListItems={optionListItemsWithCurrentReport} activeWorkspaceID={activeWorkspaceID} + optionListItems={optionListItems} /> ); @@ -218,97 +103,12 @@ SidebarLinksData.propTypes = propTypes; SidebarLinksData.defaultProps = defaultProps; SidebarLinksData.displayName = 'SidebarLinksData'; -/** - * This function (and the few below it), narrow down the data from Onyx to just the properties that we want to trigger a re-render of the component. This helps minimize re-rendering - * and makes the entire component more performant because it's not re-rendering when a bunch of properties change which aren't ever used in the UI. - * @param {Object} [report] - * @returns {Object|undefined} - */ -const chatReportSelector = (report) => - report && { - reportID: report.reportID, - participantAccountIDs: report.participantAccountIDs, - hasDraft: report.hasDraft, - isPinned: report.isPinned, - isHidden: report.isHidden, - notificationPreference: report.notificationPreference, - errorFields: { - addWorkspaceRoom: report.errorFields && report.errorFields.addWorkspaceRoom, - }, - lastMessageText: report.lastMessageText, - lastVisibleActionCreated: report.lastVisibleActionCreated, - iouReportID: report.iouReportID, - total: report.total, - nonReimbursableTotal: report.nonReimbursableTotal, - hasOutstandingChildRequest: report.hasOutstandingChildRequest, - isWaitingOnBankAccount: report.isWaitingOnBankAccount, - statusNum: report.statusNum, - stateNum: report.stateNum, - chatType: report.chatType, - type: report.type, - policyID: report.policyID, - visibility: report.visibility, - lastReadTime: report.lastReadTime, - // Needed for name sorting: - reportName: report.reportName, - policyName: report.policyName, - oldPolicyName: report.oldPolicyName, - // Other less obvious properites considered for sorting: - ownerAccountID: report.ownerAccountID, - currency: report.currency, - managerID: report.managerID, - // Other important less obivous properties for filtering: - parentReportActionID: report.parentReportActionID, - parentReportID: report.parentReportID, - isDeletedParentAction: report.isDeletedParentAction, - isUnreadWithMention: ReportUtils.isUnreadWithMention(report), - }; - -/** - * @param {Object} [reportActions] - * @returns {Object|undefined} - */ -const reportActionsSelector = (reportActions) => - reportActions && - lodashMap(reportActions, (reportAction) => { - const {reportActionID, parentReportActionID, actionName, errors = []} = reportAction; - const decision = lodashGet(reportAction, 'message[0].moderationDecision.decision'); - - return { - reportActionID, - parentReportActionID, - actionName, - errors, - message: [ - { - moderationDecision: {decision}, - }, - ], - }; - }); - -/** - * @param {Object} [policy] - * @returns {Object|undefined} - */ -const policySelector = (policy) => - policy && { - type: policy.type, - name: policy.name, - avatar: policy.avatar, - }; - export default compose( withCurrentReportID, withCurrentUserPersonalDetails, withNavigationFocus, withNetwork(), withOnyx({ - chatReports: { - key: ONYXKEYS.COLLECTION.REPORT, - selector: chatReportSelector, - initialValue: {}, - }, isLoadingApp: { key: ONYXKEYS.IS_LOADING_APP, }, @@ -316,26 +116,8 @@ export default compose( key: ONYXKEYS.NVP_PRIORITY_MODE, initialValue: CONST.PRIORITY_MODE.DEFAULT, }, - betas: { - key: ONYXKEYS.BETAS, - initialValue: [], - }, - allReportActions: { - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - selector: reportActionsSelector, - initialValue: {}, - }, - policies: { - key: ONYXKEYS.COLLECTION.POLICY, - selector: policySelector, - initialValue: {}, - }, policyMembers: { key: ONYXKEYS.COLLECTION.POLICY_MEMBERS, }, - transactionViolations: { - key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, - initialValue: {}, - }, }), )(SidebarLinksData); diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index aa4f1df1ba7a..5efc94ba5974 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -20,6 +20,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import {useReports} from '@hooks/useReports'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -33,7 +34,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; -import type {PolicyMembers, Policy as PolicyType, ReimbursementAccount, Report} from '@src/types/onyx'; +import type {PolicyMembers, Policy as PolicyType, ReimbursementAccount} from '@src/types/onyx'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; @@ -72,9 +73,6 @@ type WorkspaceListPageOnyxProps = { /** A collection of objects for all policies which key policy member objects by accountIDs */ allPolicyMembers: OnyxCollection; - - /** All reports shared with the user (coming from Onyx) */ - reports: OnyxCollection; }; type WorkspaceListPageProps = WithPolicyAndFullscreenLoadingProps & WorkspaceListPageOnyxProps; @@ -110,7 +108,8 @@ function dismissWorkspaceError(policyID: string, pendingAction: OnyxCommon.Pendi throw new Error('Not implemented'); } -function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, reports}: WorkspaceListPageProps) { +function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount}: WorkspaceListPageProps) { + const reports = useReports(); const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -407,8 +406,5 @@ export default withPolicyAndFullscreenLoading( reimbursementAccount: { key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, }, - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, })(WorkspacesListPage), ); diff --git a/src/types/onyx/PriorityMode.ts b/src/types/onyx/PriorityMode.ts new file mode 100644 index 000000000000..5fef300ed014 --- /dev/null +++ b/src/types/onyx/PriorityMode.ts @@ -0,0 +1,6 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type PriorityMode = ValueOf; + +export default PriorityMode; From 6921051449b209820347247511a943d8b4624ffb Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 15 Feb 2024 19:19:58 +0500 Subject: [PATCH 0009/1506] perf: improve renderItem and use FlatList --- .../LHNOptionsList/LHNOptionsList.tsx | 114 +++--------------- .../LHNOptionsList/OptionRowLHNData.tsx | 51 +------- src/components/LHNOptionsList/types.ts | 3 +- src/components/withCurrentReportID.tsx | 10 +- src/hooks/useOrderedReportIDs.tsx | 38 +++++- src/libs/SidebarUtils.ts | 40 +++++- src/pages/home/sidebar/SidebarLinks.js | 4 +- src/pages/home/sidebar/SidebarLinksData.js | 2 +- src/pages/workspace/WorkspacesListPage.tsx | 4 +- 9 files changed, 111 insertions(+), 155 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index e0a869f5222a..c70e355c46c5 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -1,41 +1,27 @@ -import {FlashList} from '@shopify/flash-list'; import type {ReactElement} from 'react'; -import React, {memo, useCallback} from 'react'; -import {StyleSheet, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import withCurrentReportID from '@components/withCurrentReportID'; -import usePermissions from '@hooks/usePermissions'; -import {useReports} from '@hooks/useReports'; +import React, {useCallback, memo} from 'react'; +import {FlatList, StyleSheet, View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; -import variables from '@styles/variables'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import OptionRowLHNData from './OptionRowLHNData'; -import type {LHNOptionsListOnyxProps, LHNOptionsListProps, RenderItemProps} from './types'; +import type {LHNOptionsListProps, RenderItemProps} from './types'; +import { OrderedReports } from '@libs/SidebarUtils'; +import CONST from '@src/CONST'; +import variables from '@styles/variables'; +import { FlashList } from '@shopify/flash-list'; -const keyExtractor = (item: string) => `report_${item}`; +const keyExtractor = (item: OrderedReports) => `report_${item?.reportID}`; function LHNOptionsList({ style, contentContainerStyles, data, onSelectRow, - optionMode, shouldDisableFocusOptions = false, - reportActions = {}, - policy = {}, - preferredLocale = CONST.LOCALES.DEFAULT, - personalDetails = {}, - transactions = {}, currentReportID = '', - draftComments = {}, - transactionViolations = {}, + optionMode, onFirstItemRendered = () => {}, }: LHNOptionsListProps) { - const reports = useReports(); const styles = useThemeStyles(); - const {canUseViolations} = usePermissions(); // When the first item renders we want to call the onFirstItemRendered callback. // At this point in time we know that the list is actually displaying items. @@ -53,69 +39,29 @@ function LHNOptionsList({ * Function which renders a row in the list */ const renderItem = useCallback( - ({item: reportID}: RenderItemProps): ReactElement => { - const itemFullReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; - const itemReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? null; - const itemParentReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport?.parentReportID}`] ?? null; - const itemParentReportAction = itemParentReportActions?.[itemFullReport?.parentReportActionID ?? ''] ?? null; - const itemPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport?.policyID}`] ?? null; - const transactionID = itemParentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? itemParentReportAction.originalMessage.IOUTransactionID ?? '' : ''; - const itemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? null; - const itemComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] ?? ''; - const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions); - const lastReportAction = sortedReportActions[0]; - - // Get the transaction for the last report action - let lastReportActionTransactionID = ''; - - if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { - lastReportActionTransactionID = lastReportAction.originalMessage?.IOUTransactionID ?? ''; - } - const lastReportActionTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${lastReportActionTransactionID}`] ?? {}; + ({item}: RenderItemProps): ReactElement => { return ( ); }, [ currentReportID, - draftComments, onSelectRow, - optionMode, - personalDetails, - policy, - preferredLocale, - reportActions, - reports, - shouldDisableFocusOptions, - transactions, - transactionViolations, - canUseViolations, onLayoutItem, ], ); return ( - @@ -133,30 +79,6 @@ function LHNOptionsList({ LHNOptionsList.displayName = 'LHNOptionsList'; -export default withCurrentReportID( - withOnyx({ - reportActions: { - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - }, - policy: { - key: ONYXKEYS.COLLECTION.POLICY, - }, - preferredLocale: { - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - transactions: { - key: ONYXKEYS.COLLECTION.TRANSACTION, - }, - draftComments: { - key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, - }, - transactionViolations: { - key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, - }, - })(memo(LHNOptionsList)), -); +export default memo(LHNOptionsList); export type {LHNOptionsListProps}; diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index a18d5a8ec1ec..622d7b9d9123 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -16,61 +16,12 @@ import type {OptionRowLHNDataProps} from './types'; */ function OptionRowLHNData({ isFocused = false, - fullReport, - reportActions, - personalDetails = {}, - preferredLocale = CONST.LOCALES.DEFAULT, comment, - policy, - receiptTransactions, - parentReportAction, - transaction, - lastReportActionTransaction = {}, - transactionViolations, - canUseViolations, + optionItem, ...propsToForward }: OptionRowLHNDataProps) { const reportID = propsToForward.reportID; - const optionItemRef = useRef(); - - const hasViolations = canUseViolations && ReportUtils.doesTransactionThreadHaveViolations(fullReport, transactionViolations, parentReportAction ?? null); - - const optionItem = useMemo(() => { - // Note: ideally we'd have this as a dependent selector in onyx! - const item = SidebarUtils.getOptionData({ - report: fullReport, - reportActions, - personalDetails, - preferredLocale: preferredLocale ?? CONST.LOCALES.DEFAULT, - policy, - parentReportAction, - hasViolations: !!hasViolations, - }); - if (deepEqual(item, optionItemRef.current)) { - return optionItemRef.current; - } - - optionItemRef.current = item; - - return item; - // Listen parentReportAction to update title of thread report when parentReportAction changed - // Listen to transaction to update title of transaction report when transaction changed - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - fullReport, - lastReportActionTransaction, - reportActions, - personalDetails, - preferredLocale, - policy, - parentReportAction, - transaction, - transactionViolations, - canUseViolations, - receiptTransactions, - ]); - useEffect(() => { if (!optionItem || !!optionItem.hasDraftComment || !comment || comment.length <= 0 || isFocused) { return; diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index c58770c8383f..4c79536571bf 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -8,6 +8,7 @@ import type CONST from '@src/CONST'; import type {OptionData} from '@src/libs/ReportUtils'; import type {Locale, PersonalDetailsList, Policy, Report, ReportAction, ReportActions, Transaction, TransactionViolation} from '@src/types/onyx'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; +import { OrderedReports } from '@libs/SidebarUtils'; type OptionMode = ValueOf; @@ -134,6 +135,6 @@ type OptionRowLHNProps = { onLayout?: (event: LayoutChangeEvent) => void; }; -type RenderItemProps = {item: string}; +type RenderItemProps = {item: OrderedReports}; export type {LHNOptionsListProps, OptionRowLHNDataProps, OptionRowLHNProps, LHNOptionsListOnyxProps, RenderItemProps}; diff --git a/src/components/withCurrentReportID.tsx b/src/components/withCurrentReportID.tsx index a452e7565b4e..cc49c44e0e77 100644 --- a/src/components/withCurrentReportID.tsx +++ b/src/components/withCurrentReportID.tsx @@ -39,7 +39,15 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro */ const updateCurrentReportID = useCallback( (state: NavigationState) => { - setCurrentReportID(Navigation.getTopmostReportId(state) ?? ''); + const reportID = Navigation.getTopmostReportId(state) ?? ''; + /** + * This is to make sure we don't set the undefined as reportID when + * switching between chat list and settings->workspaces tab. + * and doing so avoid unnecessary re-render of `useOrderedReportIDs`. + */ + if (reportID) { + setCurrentReportID(reportID); + } }, [setCurrentReportID], ); diff --git a/src/hooks/useOrderedReportIDs.tsx b/src/hooks/useOrderedReportIDs.tsx index 3217830054de..01dab5a3231a 100644 --- a/src/hooks/useOrderedReportIDs.tsx +++ b/src/hooks/useOrderedReportIDs.tsx @@ -1,6 +1,7 @@ import React, {createContext, useContext, useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import {usePersonalDetails} from '@components/OnyxProvider'; import {getCurrentUserAccountID} from '@libs/actions/Report'; import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; import SidebarUtils from '@libs/SidebarUtils'; @@ -10,6 +11,7 @@ import type {Beta, Policy, PolicyMembers, ReportAction, ReportActions, Transacti import type PriorityMode from '@src/types/onyx/PriorityMode'; import useActiveWorkspace from './useActiveWorkspace'; import useCurrentReportID from './useCurrentReportID'; +import usePermissions from './usePermissions'; import {useReports} from './useReports'; type OnyxProps = { @@ -31,6 +33,8 @@ function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextP const chatReports = useReports(); const currentReportIDValue = useCurrentReportID(); const {activeWorkspaceID} = useActiveWorkspace(); + const personalDetails = usePersonalDetails(); + const {canUseViolations} = usePermissions(); const policyMemberAccountIDs = useMemo( () => getPolicyMembersByIdWithoutCurrentUser(props.policyMembers, activeWorkspaceID, getCurrentUserAccountID()), @@ -49,8 +53,25 @@ function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextP props.transactionViolations, activeWorkspaceID, policyMemberAccountIDs, + personalDetails, + props.preferredLocale, + canUseViolations, + props.draftComments, ), - [chatReports, props.betas, props.policies, props.priorityMode, props.allReportActions, props.transactionViolations, activeWorkspaceID, policyMemberAccountIDs], + [ + chatReports, + props.betas, + props.policies, + props.priorityMode, + props.allReportActions, + props.transactionViolations, + activeWorkspaceID, + policyMemberAccountIDs, + personalDetails, + props.preferredLocale, + canUseViolations, + props.draftComments, + ], ); // We need to make sure the current report is in the list of reports, but we do not want @@ -70,6 +91,10 @@ function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextP props.transactionViolations, activeWorkspaceID, policyMemberAccountIDs, + personalDetails, + props.preferredLocale, + canUseViolations, + props.draftComments, ); } return optionListItems; @@ -84,6 +109,10 @@ function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextP props.policies, props.priorityMode, props.transactionViolations, + personalDetails, + props.preferredLocale, + canUseViolations, + props.draftComments, ]); return {props.children}; @@ -136,6 +165,13 @@ const OrderedReportIDsContextProvider = withOnyx, currentPolicyID = '', policyMemberAccountIDs: number[] = [], -): string[] { + personalDetails: OnyxEntry, + preferredLocale: DeepValueOf, + canUseViolations: boolean, + draftComments: OnyxCollection, +): OrderedReports[] { const isInGSDMode = priorityMode === CONST.PRIORITY_MODE.GSD; const isInDefaultMode = !isInGSDMode; const allReportsDictValues = Object.values(allReports ?? {}); @@ -190,7 +200,33 @@ function getOrderedReportIDs( // Now that we have all the reports grouped and sorted, they must be flattened into an array and only return the reportID. // The order the arrays are concatenated in matters and will determine the order that the groups are displayed in the sidebar. - const LHNReports = [...pinnedAndGBRReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report.reportID); + const LHNReports = [...pinnedAndGBRReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => { + const itemFullReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.reportID}`] ?? null; + const itemReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`] ?? null; + const itemParentReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport?.parentReportID}`] ?? null; + const itemParentReportAction = itemParentReportActions?.[itemFullReport?.parentReportActionID ?? ''] ?? null; + const itemPolicy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport?.policyID}`] ?? null; + const itemComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report?.reportID}`] ?? ''; + + const hasViolations = canUseViolations && ReportUtils.doesTransactionThreadHaveViolations(itemFullReport, transactionViolations, itemParentReportAction ?? null); + + const item = getOptionData({ + report: itemFullReport, + reportActions: itemReportActions, + personalDetails, + preferredLocale: preferredLocale ?? CONST.LOCALES.DEFAULT, + policy: itemPolicy, + parentReportAction: itemParentReportAction, + hasViolations: !!hasViolations, + }); + + return { + reportID: report.reportID, + optionItem: item, + comment: itemComment, + } + }); + return LHNReports; } diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index e78e656e0b7e..4c05c3d44ae8 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -1,7 +1,7 @@ /* eslint-disable rulesdir/onyx-props-must-have-default */ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import React, {memo, useCallback, useEffect, useMemo, useRef} from 'react'; import {InteractionManager, StyleSheet, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -179,5 +179,5 @@ export default withOnyx({ activePolicy: { key: ({activeWorkspaceID}) => `${ONYXKEYS.COLLECTION.POLICY}${activeWorkspaceID}`, }, -})(SidebarLinks); +})(memo(SidebarLinks)); export {basePropTypes}; diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index 42681fea451b..87006d3debe2 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -46,7 +46,7 @@ const defaultProps = { }, }; -function SidebarLinksData({isFocused, currentReportID, insets, isLoadingApp, onLinkClick, priorityMode, network, policyMembers, session: {accountID}}) { +function SidebarLinksData({isFocused, currentReportID, insets, isLoadingApp, onLinkClick, priorityMode, network, policyMembers, session: {accountID}}) { const styles = useThemeStyles(); const {activeWorkspaceID} = useActiveWorkspace(); const {translate} = useLocalize(); diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index d778648ea998..968148a8becb 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -107,6 +107,8 @@ function dismissWorkspaceError(policyID: string, pendingAction: OnyxCommon.Pendi throw new Error('Not implemented'); } +const stickyHeaderIndices = [0]; + function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount}: WorkspaceListPageProps) { const reports = useReports(); const theme = useTheme(); @@ -375,7 +377,7 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount}: data={workspaces} renderItem={getMenuItem} ListHeaderComponent={listHeaderComponent} - stickyHeaderIndices={[0]} + stickyHeaderIndices={stickyHeaderIndices} /> Date: Thu, 22 Feb 2024 00:58:36 +0700 Subject: [PATCH 0010/1506] fix: 32978 --- src/libs/SidebarUtils.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 49436576295c..9ac5227399f8 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -292,27 +292,35 @@ function getOptionData({ const isThreadMessage = ReportUtils.isThread(report) && reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && reportAction?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + // Currently the back-end is not returning the `lastActionName` so I use this to mock, + // ideally it should be returned from the back-end in the `report`, like the `lastMessageText` and `lastMessageHtml` + if (report.lastMessageHtml && report.lastMessageHtml.includes('invited ]*><\/mention-user>/g)?.length ?? []; const verb = - lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM + lastActionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || lastActionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM ? Localize.translate(preferredLocale, 'workspace.invite.invited') : Localize.translate(preferredLocale, 'workspace.invite.removed'); - const users = Localize.translate(preferredLocale, targetAccountIDs.length > 1 ? 'workspace.invite.users' : 'workspace.invite.user'); - result.alternateText = `${lastActorDisplayName} ${verb} ${targetAccountIDs.length} ${users}`.trim(); + const users = Localize.translate(preferredLocale, targetAccountIDsLength > 1 ? 'workspace.invite.users' : 'workspace.invite.user'); + result.alternateText = `${lastActorDisplayName} ${verb} ${targetAccountIDsLength} ${users}`.trim(); const roomName = lastAction?.originalMessage?.roomName ?? ''; if (roomName) { From dc88950ac4dbc552e4ca8b705ae4faff05dd70da Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Mon, 26 Feb 2024 17:58:22 +0100 Subject: [PATCH 0011/1506] refactor: revert changes to withCurrentReportID --- src/components/withCurrentReportID.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/components/withCurrentReportID.tsx b/src/components/withCurrentReportID.tsx index cc49c44e0e77..a452e7565b4e 100644 --- a/src/components/withCurrentReportID.tsx +++ b/src/components/withCurrentReportID.tsx @@ -39,15 +39,7 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro */ const updateCurrentReportID = useCallback( (state: NavigationState) => { - const reportID = Navigation.getTopmostReportId(state) ?? ''; - /** - * This is to make sure we don't set the undefined as reportID when - * switching between chat list and settings->workspaces tab. - * and doing so avoid unnecessary re-render of `useOrderedReportIDs`. - */ - if (reportID) { - setCurrentReportID(reportID); - } + setCurrentReportID(Navigation.getTopmostReportId(state) ?? ''); }, [setCurrentReportID], ); From 9ae8701c7ed4a5c98e69d689e63c4ac19694ebf2 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Mon, 26 Feb 2024 18:48:44 +0100 Subject: [PATCH 0012/1506] refactor: remove useReports context & hooks --- src/App.tsx | 2 - src/hooks/useReports.tsx | 44 ------------------- .../AppNavigator/ReportScreenIDSetter.ts | 13 ++++-- src/pages/workspace/WorkspacesListPage.tsx | 10 +++-- 4 files changed, 16 insertions(+), 53 deletions(-) delete mode 100644 src/hooks/useReports.tsx diff --git a/src/App.tsx b/src/App.tsx index 7cbf3478d16e..eb1750a7fe5f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -30,7 +30,6 @@ import {WindowDimensionsProvider} from './components/withWindowDimensions'; import Expensify from './Expensify'; import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop'; import {OrderedReportIDsContextProvider} from './hooks/useOrderedReportIDs'; -import {ReportsContextProvider} from './hooks/useReports'; import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; import InitialUrlContext from './libs/InitialUrlContext'; import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext'; @@ -77,7 +76,6 @@ function App({url}: AppProps) { CustomStatusBarAndBackgroundContextProvider, ActiveElementRoleProvider, ActiveWorkspaceContextProvider, - ReportsContextProvider, OrderedReportIDsContextProvider, PlaybackContextProvider, VolumeContextProvider, diff --git a/src/hooks/useReports.tsx b/src/hooks/useReports.tsx deleted file mode 100644 index c4082149cbf4..000000000000 --- a/src/hooks/useReports.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, {createContext, useContext, useEffect, useMemo, useState} from 'react'; -import Onyx from 'react-native-onyx'; -import type {OnyxCollection} from 'react-native-onyx'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {Report} from '@src/types/onyx'; - -type Reports = OnyxCollection; -type ReportsContextValue = Reports; - -type ReportsContextProviderProps = { - children: React.ReactNode; -}; - -const ReportsContext = createContext(null); - -function ReportsContextProvider(props: ReportsContextProviderProps) { - const [reports, setReports] = useState(null); - - useEffect(() => { - // eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs - const connID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (val) => { - setReports(val); - }, - }); - return () => { - Onyx.disconnect(connID); - }; - }, []); - - const contextValue = useMemo(() => reports ?? {}, [reports]); - - return {props.children}; -} - -function useReports() { - return useContext(ReportsContext); -} - -ReportsContextProvider.displayName = 'ReportsContextProvider'; - -export {ReportsContextProvider, ReportsContext, useReports}; diff --git a/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts b/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts index 297313e513f6..529f0f3d31a7 100644 --- a/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts +++ b/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts @@ -3,7 +3,6 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import usePermissions from '@hooks/usePermissions'; -import {useReports} from '@hooks/useReports'; import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -11,6 +10,9 @@ import type {Policy, PolicyMembers, Report, ReportMetadata} from '@src/types/ony import type {ReportScreenWrapperProps} from './ReportScreenWrapper'; type ReportScreenIDSetterComponentProps = { + /** Available reports that would be displayed in this navigator */ + reports: OnyxCollection; + /** The policies which the user has access to */ policies: OnyxCollection; @@ -56,10 +58,9 @@ const getLastAccessedReportID = ( }; // This wrapper is reponsible for opening the last accessed report if there is no reportID specified in the route params -function ReportScreenIDSetter({route, policies, policyMembers = {}, navigation, isFirstTimeNewExpensifyUser = false, reportMetadata, accountID}: ReportScreenIDSetterProps) { +function ReportScreenIDSetter({route, reports, policies, policyMembers = {}, navigation, isFirstTimeNewExpensifyUser = false, reportMetadata, accountID}: ReportScreenIDSetterProps) { const {canUseDefaultRooms} = usePermissions(); const {activeWorkspaceID} = useActiveWorkspace(); - const reports = useReports(); useEffect(() => { // Don't update if there is a reportID in the params already @@ -80,7 +81,7 @@ function ReportScreenIDSetter({route, policies, policyMembers = {}, navigation, !canUseDefaultRooms, policies, isFirstTimeNewExpensifyUser, - !!route?.params?.openOnAdminRoom, + !!reports?.params?.openOnAdminRoom, reportMetadata, activeWorkspaceID, policyMemberAccountIDs, @@ -101,6 +102,10 @@ function ReportScreenIDSetter({route, policies, policyMembers = {}, navigation, ReportScreenIDSetter.displayName = 'ReportScreenIDSetter'; export default withOnyx({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + allowStaleData: true, + }, policies: { key: ONYXKEYS.COLLECTION.POLICY, allowStaleData: true, diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index f646366360eb..a51efd608444 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -20,7 +20,6 @@ import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import {useReports} from '@hooks/useReports'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -73,6 +72,9 @@ type WorkspaceListPageOnyxProps = { /** A collection of objects for all policies which key policy member objects by accountIDs */ allPolicyMembers: OnyxCollection; + + /** All reports shared with the user (coming from Onyx) */ + reports: OnyxCollection; }; type WorkspaceListPageProps = WithPolicyAndFullscreenLoadingProps & WorkspaceListPageOnyxProps; @@ -110,8 +112,7 @@ function dismissWorkspaceError(policyID: string, pendingAction: OnyxCommon.Pendi const stickyHeaderIndices = [0]; -function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount}: WorkspaceListPageProps) { - const reports = useReports(); +function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, reports}: WorkspaceListPageProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -409,5 +410,8 @@ export default withPolicyAndFullscreenLoading( reimbursementAccount: { key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, }, + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, })(WorkspacesListPage), ); From b613e974a2f8e8b7c07e893e64bc6fdf377d08d1 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 28 Feb 2024 15:54:37 +0100 Subject: [PATCH 0013/1506] refactor: use reports from onyx in useOrderedReportIDs --- src/hooks/useOrderedReportIDs.tsx | 69 +++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/src/hooks/useOrderedReportIDs.tsx b/src/hooks/useOrderedReportIDs.tsx index 01dab5a3231a..6b3ebd8d9e28 100644 --- a/src/hooks/useOrderedReportIDs.tsx +++ b/src/hooks/useOrderedReportIDs.tsx @@ -4,23 +4,26 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {usePersonalDetails} from '@components/OnyxProvider'; import {getCurrentUserAccountID} from '@libs/actions/Report'; import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Beta, Policy, PolicyMembers, ReportAction, ReportActions, TransactionViolation} from '@src/types/onyx'; +import type {Beta, Locale, Policy, PolicyMembers, Report, ReportAction, ReportActions, TransactionViolation} from '@src/types/onyx'; import type PriorityMode from '@src/types/onyx/PriorityMode'; import useActiveWorkspace from './useActiveWorkspace'; import useCurrentReportID from './useCurrentReportID'; import usePermissions from './usePermissions'; -import {useReports} from './useReports'; type OnyxProps = { + chatReports: OnyxCollection; betas: OnyxEntry; policies: OnyxCollection; allReportActions: OnyxCollection; transactionViolations: OnyxCollection; policyMembers: OnyxCollection; priorityMode: OnyxEntry; + preferredLocale: OnyxEntry; + draftComments: OnyxCollection; }; type WithOrderedReportIDsContextProviderProps = OnyxProps & { @@ -30,7 +33,6 @@ type WithOrderedReportIDsContextProviderProps = OnyxProps & { const OrderedReportIDsContext = createContext({}); function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextProviderProps) { - const chatReports = useReports(); const currentReportIDValue = useCurrentReportID(); const {activeWorkspaceID} = useActiveWorkspace(); const personalDetails = usePersonalDetails(); @@ -45,7 +47,7 @@ function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextP () => SidebarUtils.getOrderedReportIDs( null, - chatReports, + props.chatReports, props.betas ?? [], props.policies, props.priorityMode, @@ -59,7 +61,7 @@ function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextP props.draftComments, ), [ - chatReports, + props.chatReports, props.betas, props.policies, props.priorityMode, @@ -83,7 +85,7 @@ function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextP if (currentReportIDValue?.currentReportID && !optionListItems.includes(currentReportIDValue.currentReportID)) { return SidebarUtils.getOrderedReportIDs( currentReportIDValue.currentReportID, - chatReports, + props.chatReports, props.betas ?? [], props.policies, props.priorityMode, @@ -100,7 +102,7 @@ function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextP return optionListItems; }, [ activeWorkspaceID, - chatReports, + props.chatReports, currentReportIDValue?.currentReportID, optionListItems, policyMemberAccountIDs, @@ -118,6 +120,52 @@ function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextP return {props.children}; } +/** + * This function (and the few below it), narrow down the data from Onyx to just the properties that we want to trigger a re-render of the component. This helps minimize re-rendering + * and makes the entire component more performant because it's not re-rendering when a bunch of properties change which aren't ever used in the UI. + * @param {Object} [report] + * @returns {Object|undefined} + */ +const chatReportSelector = (report) => + report && { + reportID: report.reportID, + participantAccountIDs: report.participantAccountIDs, + hasDraft: report.hasDraft, + isPinned: report.isPinned, + isHidden: report.isHidden, + notificationPreference: report.notificationPreference, + errorFields: { + addWorkspaceRoom: report.errorFields && report.errorFields.addWorkspaceRoom, + }, + lastMessageText: report.lastMessageText, + lastVisibleActionCreated: report.lastVisibleActionCreated, + iouReportID: report.iouReportID, + total: report.total, + nonReimbursableTotal: report.nonReimbursableTotal, + hasOutstandingChildRequest: report.hasOutstandingChildRequest, + isWaitingOnBankAccount: report.isWaitingOnBankAccount, + statusNum: report.statusNum, + stateNum: report.stateNum, + chatType: report.chatType, + type: report.type, + policyID: report.policyID, + visibility: report.visibility, + lastReadTime: report.lastReadTime, + // Needed for name sorting: + reportName: report.reportName, + policyName: report.policyName, + oldPolicyName: report.oldPolicyName, + // Other less obvious properites considered for sorting: + ownerAccountID: report.ownerAccountID, + currency: report.currency, + managerID: report.managerID, + // Other important less obivous properties for filtering: + parentReportActionID: report.parentReportActionID, + parentReportID: report.parentReportID, + isDeletedParentAction: report.isDeletedParentAction, + isUnreadWithMention: ReportUtils.isUnreadWithMention(report), + }; + const reportActionsSelector = (reportActions: OnyxEntry) => { if (!reportActions) { return []; @@ -140,6 +188,11 @@ const reportActionsSelector = (reportActions: OnyxEntry) => { }; const OrderedReportIDsContextProvider = withOnyx({ + chatReports: { + key: ONYXKEYS.COLLECTION.REPORT, + selector: chatReportSelector, + initialValue: {}, + }, priorityMode: { key: ONYXKEYS.NVP_PRIORITY_MODE, initialValue: CONST.PRIORITY_MODE.DEFAULT, @@ -151,7 +204,7 @@ const OrderedReportIDsContextProvider = withOnyx reportActionsSelector(actions), + selector: reportActionsSelector, initialValue: {}, }, policies: { From 7f15fbc34bdc512c6c72fe8b15232dc1ba53e756 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 28 Feb 2024 17:32:07 +0100 Subject: [PATCH 0014/1506] refactor: move creating orderedReport objects from getOrderedReportIds to the context itself --- .../LHNOptionsList/LHNOptionsList.tsx | 48 +++--- src/hooks/useOrderedReportIDs.tsx | 140 ++++++++---------- src/libs/SidebarUtils.ts | 46 +----- src/pages/home/sidebar/SidebarLinksData.js | 18 ++- 4 files changed, 100 insertions(+), 152 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index c70e355c46c5..8b18630a9646 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -1,13 +1,13 @@ +import {FlashList} from '@shopify/flash-list'; import type {ReactElement} from 'react'; -import React, {useCallback, memo} from 'react'; -import {FlatList, StyleSheet, View} from 'react-native'; +import React, {memo, useCallback} from 'react'; +import {StyleSheet, View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; +import {OrderedReports} from '@libs/SidebarUtils'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; import OptionRowLHNData from './OptionRowLHNData'; import type {LHNOptionsListProps, RenderItemProps} from './types'; -import { OrderedReports } from '@libs/SidebarUtils'; -import CONST from '@src/CONST'; -import variables from '@styles/variables'; -import { FlashList } from '@shopify/flash-list'; const keyExtractor = (item: OrderedReports) => `report_${item?.reportID}`; @@ -39,29 +39,23 @@ function LHNOptionsList({ * Function which renders a row in the list */ const renderItem = useCallback( - ({item}: RenderItemProps): ReactElement => { - - return ( - - ); - }, - [ - currentReportID, - onSelectRow, - onLayoutItem, - ], + ({item}: RenderItemProps): ReactElement => ( + + ), + [shouldDisableFocusOptions, currentReportID, onSelectRow, onLayoutItem, optionMode], ); return ( - diff --git a/src/hooks/useOrderedReportIDs.tsx b/src/hooks/useOrderedReportIDs.tsx index 6b3ebd8d9e28..8f071f036fc7 100644 --- a/src/hooks/useOrderedReportIDs.tsx +++ b/src/hooks/useOrderedReportIDs.tsx @@ -1,4 +1,4 @@ -import React, {createContext, useContext, useMemo} from 'react'; +import React, {createContext, useCallback, useContext, useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {usePersonalDetails} from '@components/OnyxProvider'; @@ -32,101 +32,90 @@ type WithOrderedReportIDsContextProviderProps = OnyxProps & { const OrderedReportIDsContext = createContext({}); -function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextProviderProps) { +function WithOrderedReportIDsContextProvider({ + children, + chatReports, + betas, + policies, + allReportActions, + transactionViolations, + policyMembers, + priorityMode, + preferredLocale, + draftComments, +}: WithOrderedReportIDsContextProviderProps) { const currentReportIDValue = useCurrentReportID(); - const {activeWorkspaceID} = useActiveWorkspace(); const personalDetails = usePersonalDetails(); const {canUseViolations} = usePermissions(); + const {activeWorkspaceID} = useActiveWorkspace(); - const policyMemberAccountIDs = useMemo( - () => getPolicyMembersByIdWithoutCurrentUser(props.policyMembers, activeWorkspaceID, getCurrentUserAccountID()), - [activeWorkspaceID, props.policyMembers], - ); + const policyMemberAccountIDs = useMemo(() => getPolicyMembersByIdWithoutCurrentUser(policyMembers, activeWorkspaceID, getCurrentUserAccountID()), [activeWorkspaceID, policyMembers]); - const optionListItems = useMemo( - () => + const getOrderedReportIDs = useCallback( + (currentReportID?: string) => SidebarUtils.getOrderedReportIDs( - null, - props.chatReports, - props.betas ?? [], - props.policies, - props.priorityMode, - props.allReportActions, - props.transactionViolations, + currentReportID ?? null, + chatReports, + betas ?? [], + policies, + priorityMode, + allReportActions, + transactionViolations, activeWorkspaceID, policyMemberAccountIDs, - personalDetails, - props.preferredLocale, - canUseViolations, - props.draftComments, ), - [ - props.chatReports, - props.betas, - props.policies, - props.priorityMode, - props.allReportActions, - props.transactionViolations, - activeWorkspaceID, - policyMemberAccountIDs, - personalDetails, - props.preferredLocale, - canUseViolations, - props.draftComments, - ], + [chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs], ); + const orderedReportIDs = useMemo(() => getOrderedReportIDs(), [getOrderedReportIDs]); + // We need to make sure the current report is in the list of reports, but we do not want // to have to re-generate the list every time the currentReportID changes. To do that // we first generate the list as if there was no current report, then here we check if // the current report is missing from the list, which should very rarely happen. In this // case we re-generate the list a 2nd time with the current report included. - const optionListItemsWithCurrentReport = useMemo(() => { - if (currentReportIDValue?.currentReportID && !optionListItems.includes(currentReportIDValue.currentReportID)) { - return SidebarUtils.getOrderedReportIDs( - currentReportIDValue.currentReportID, - props.chatReports, - props.betas ?? [], - props.policies, - props.priorityMode, - props.allReportActions, - props.transactionViolations, - activeWorkspaceID, - policyMemberAccountIDs, - personalDetails, - props.preferredLocale, - canUseViolations, - props.draftComments, - ); + const orderedReportIDsWithCurrentReport = useMemo(() => { + if (currentReportIDValue?.currentReportID && !orderedReportIDs.includes(currentReportIDValue.currentReportID)) { + return getOrderedReportIDs(currentReportIDValue.currentReportID); } - return optionListItems; - }, [ - activeWorkspaceID, - props.chatReports, - currentReportIDValue?.currentReportID, - optionListItems, - policyMemberAccountIDs, - props.allReportActions, - props.betas, - props.policies, - props.priorityMode, - props.transactionViolations, - personalDetails, - props.preferredLocale, - canUseViolations, - props.draftComments, - ]); - - return {props.children}; + return orderedReportIDs; + }, [getOrderedReportIDs, currentReportIDValue?.currentReportID, orderedReportIDs]); + + const orderedReportListItems = useMemo( + () => + orderedReportIDsWithCurrentReport.map((reportID) => { + const itemFullReport = chatReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; + const itemReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? null; + const itemParentReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport?.parentReportID}`] ?? null; + const itemParentReportAction = itemParentReportActions?.[itemFullReport?.parentReportActionID ?? ''] ?? null; + const itemPolicy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport?.policyID}`] ?? null; + const itemComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] ?? ''; + + const hasViolations = canUseViolations && ReportUtils.doesTransactionThreadHaveViolations(itemFullReport, transactionViolations, itemParentReportAction ?? null); + + const item = SidebarUtils.getOptionData({ + report: itemFullReport, + reportActions: itemReportActions, + personalDetails, + preferredLocale: preferredLocale ?? CONST.LOCALES.DEFAULT, + policy: itemPolicy, + parentReportAction: itemParentReportAction, + hasViolations: !!hasViolations, + }); + + return {reportID, optionItem: item, comment: itemComment}; + }), + [orderedReportIDsWithCurrentReport, canUseViolations, personalDetails, draftComments, preferredLocale, chatReports, allReportActions, policies, transactionViolations], + ); + + return {children}; } /** * This function (and the few below it), narrow down the data from Onyx to just the properties that we want to trigger a re-render of the component. This helps minimize re-rendering * and makes the entire component more performant because it's not re-rendering when a bunch of properties change which aren't ever used in the UI. - * @param {Object} [report] - * @returns {Object|undefined} */ -const chatReportSelector = (report) => +const chatReportSelector = (report: OnyxEntry) => report && { reportID: report.reportID, participantAccountIDs: report.participantAccountIDs, @@ -134,9 +123,7 @@ const chatReportSelector = (report) => isPinned: report.isPinned, isHidden: report.isHidden, notificationPreference: report.notificationPreference, - errorFields: { - addWorkspaceRoom: report.errorFields && report.errorFields.addWorkspaceRoom, - }, + errorFields: {addWorkspaceRoom: report.errorFields?.addWorkspaceRoom}, lastMessageText: report.lastMessageText, lastVisibleActionCreated: report.lastVisibleActionCreated, iouReportID: report.iouReportID, @@ -188,6 +175,7 @@ const reportActionsSelector = (reportActions: OnyxEntry) => { }; const OrderedReportIDsContextProvider = withOnyx({ + // @ts-expect-error Need some help in determining the correct type for this selector chatReports: { key: ONYXKEYS.COLLECTION.REPORT, selector: chatReportSelector, diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index f99384487650..f36becf40715 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -59,30 +59,20 @@ function compareStringDates(a: string, b: string): 0 | 1 | -1 { return 0; } -export type OrderedReports = { - reportID: string; - optionItem: ReportUtils.OptionData | undefined; - comment: string; -}; - /** * @returns An array of reportIDs sorted in the proper order */ function getOrderedReportIDs( currentReportId: string | null, allReports: OnyxCollection, - betas: Beta[], + betas: OnyxEntry, policies: OnyxCollection, priorityMode: OnyxEntry, allReportActions: OnyxCollection, transactionViolations: OnyxCollection, currentPolicyID = '', policyMemberAccountIDs: number[] = [], - personalDetails: OnyxEntry, - preferredLocale: DeepValueOf, - canUseViolations: boolean, - draftComments: OnyxCollection, -): OrderedReports[] { +): string[] { const isInGSDMode = priorityMode === CONST.PRIORITY_MODE.GSD; const isInDefaultMode = !isInGSDMode; const allReportsDictValues = Object.values(allReports ?? {}); @@ -93,12 +83,12 @@ function getOrderedReportIDs( const parentReportActions = allReportActions?.[parentReportActionsKey]; const parentReportAction = parentReportActions?.find((action) => action && report && action?.reportActionID === report?.parentReportActionID); const doesReportHaveViolations = - betas.includes(CONST.BETAS.VIOLATIONS) && !!parentReportAction && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction); + !!betas && betas.includes(CONST.BETAS.VIOLATIONS) && !!parentReportAction && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction); return ReportUtils.shouldReportBeInOptionList({ report, currentReportId: currentReportId ?? '', isInGSDMode, - betas, + betas: betas ?? [], policies, excludeEmptyChats: true, doesReportHaveViolations, @@ -178,33 +168,7 @@ function getOrderedReportIDs( // Now that we have all the reports grouped and sorted, they must be flattened into an array and only return the reportID. // The order the arrays are concatenated in matters and will determine the order that the groups are displayed in the sidebar. - const LHNReports = [...pinnedAndGBRReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => { - const itemFullReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.reportID}`] ?? null; - const itemReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`] ?? null; - const itemParentReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport?.parentReportID}`] ?? null; - const itemParentReportAction = itemParentReportActions?.[itemFullReport?.parentReportActionID ?? ''] ?? null; - const itemPolicy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport?.policyID}`] ?? null; - const itemComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report?.reportID}`] ?? ''; - - const hasViolations = canUseViolations && ReportUtils.doesTransactionThreadHaveViolations(itemFullReport, transactionViolations, itemParentReportAction ?? null); - - const item = getOptionData({ - report: itemFullReport, - reportActions: itemReportActions, - personalDetails, - preferredLocale: preferredLocale ?? CONST.LOCALES.DEFAULT, - policy: itemPolicy, - parentReportAction: itemParentReportAction, - hasViolations: !!hasViolations, - }); - - return { - reportID: report.reportID, - optionItem: item, - comment: itemComment, - }; - }); - + const LHNReports = [...pinnedAndGBRReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report.reportID); return LHNReports; } diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index 6d63a8a44fe1..4966a2414e97 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -51,22 +51,24 @@ function SidebarLinksData({isFocused, currentReportID, insets, isLoadingApp, onL // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => Policy.openWorkspace(activeWorkspaceID, policyMemberAccountIDs), [activeWorkspaceID]); - const reportIDsRef = useRef(null); + const orderedReportListItemsRef = useRef(null); const isLoading = isLoadingApp; - const reportIDs = useOrderedReportIDs(); + const orderedReportListItems = useOrderedReportIDs(); + const optionListItems = useMemo(() => { - if (deepEqual(reportIDsRef.current, reportIDs)) { - return reportIDsRef.current; + // this can be very heavy because we are no longer comapring the IDS but the whole objects for the list + if (deepEqual(orderedReportListItemsRef.current, orderedReportListItems)) { + return orderedReportListItemsRef.current; } // 1. We need to update existing reports only once while loading because they are updated several times during loading and causes this regression: https://github.com/Expensify/App/issues/24596#issuecomment-1681679531 // 2. If the user is offline, we need to update the reports unconditionally, since the loading of report data might be stuck in this case. // 3. Changing priority mode to Most Recent will call OpenApp. If there is an existing reports and the priority mode is updated, we want to immediately update the list instead of waiting the OpenApp request to complete - if (!isLoading || !reportIDsRef.current || network.isOffline || (reportIDsRef.current && prevPriorityMode !== priorityMode)) { - reportIDsRef.current = reportIDs; + if (!isLoading || !orderedReportListItemsRef.current || network.isOffline || (orderedReportListItemsRef.current && prevPriorityMode !== priorityMode)) { + orderedReportListItemsRef.current = orderedReportListItems; } - return reportIDsRef.current || []; - }, [reportIDs, isLoading, network.isOffline, prevPriorityMode, priorityMode]); + return orderedReportListItemsRef.current || []; + }, [orderedReportListItems, isLoading, network.isOffline, prevPriorityMode, priorityMode]); const currentReportIDRef = useRef(currentReportID); currentReportIDRef.current = currentReportID; From 7fa44fc71ce4eb20d70b4da92094b9a5b6d50394 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 28 Feb 2024 17:34:55 +0100 Subject: [PATCH 0015/1506] Revert "refactor: revert changes to withCurrentReportID" This reverts commit dc88950ac4dbc552e4ca8b705ae4faff05dd70da. --- src/components/withCurrentReportID.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/withCurrentReportID.tsx b/src/components/withCurrentReportID.tsx index a452e7565b4e..cc49c44e0e77 100644 --- a/src/components/withCurrentReportID.tsx +++ b/src/components/withCurrentReportID.tsx @@ -39,7 +39,15 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro */ const updateCurrentReportID = useCallback( (state: NavigationState) => { - setCurrentReportID(Navigation.getTopmostReportId(state) ?? ''); + const reportID = Navigation.getTopmostReportId(state) ?? ''; + /** + * This is to make sure we don't set the undefined as reportID when + * switching between chat list and settings->workspaces tab. + * and doing so avoid unnecessary re-render of `useOrderedReportIDs`. + */ + if (reportID) { + setCurrentReportID(reportID); + } }, [setCurrentReportID], ); From ae12b008886272f08668a089342bc9bacbcfb831 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 28 Feb 2024 17:47:45 +0100 Subject: [PATCH 0016/1506] refactor: remove comment in SidebarLinksData --- src/pages/home/sidebar/SidebarLinksData.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index 4966a2414e97..5cedc9bbb7fd 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -56,7 +56,6 @@ function SidebarLinksData({isFocused, currentReportID, insets, isLoadingApp, onL const orderedReportListItems = useOrderedReportIDs(); const optionListItems = useMemo(() => { - // this can be very heavy because we are no longer comapring the IDS but the whole objects for the list if (deepEqual(orderedReportListItemsRef.current, orderedReportListItems)) { return orderedReportListItemsRef.current; } From c87f579bda3c94452f6b093edfbc159996a7eb30 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 28 Feb 2024 18:00:43 +0100 Subject: [PATCH 0017/1506] refactor: rename useOrderedReportIDs to useOrderedReportListItems --- src/App.tsx | 4 ++-- src/components/withCurrentReportID.tsx | 2 +- ...tIDs.tsx => useOrderedReportListItems.tsx} | 20 +++++++++---------- src/pages/home/sidebar/SidebarLinksData.js | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) rename src/hooks/{useOrderedReportIDs.tsx => useOrderedReportListItems.tsx} (92%) diff --git a/src/App.tsx b/src/App.tsx index eb1750a7fe5f..5670859e8908 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,7 +29,7 @@ import {KeyboardStateProvider} from './components/withKeyboardState'; import {WindowDimensionsProvider} from './components/withWindowDimensions'; import Expensify from './Expensify'; import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop'; -import {OrderedReportIDsContextProvider} from './hooks/useOrderedReportIDs'; +import {OrderedReportListItemsContextProvider} from './hooks/useOrderedReportListItems'; import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; import InitialUrlContext from './libs/InitialUrlContext'; import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext'; @@ -76,7 +76,7 @@ function App({url}: AppProps) { CustomStatusBarAndBackgroundContextProvider, ActiveElementRoleProvider, ActiveWorkspaceContextProvider, - OrderedReportIDsContextProvider, + OrderedReportListItemsContextProvider, PlaybackContextProvider, VolumeContextProvider, VideoPopoverMenuContextProvider, diff --git a/src/components/withCurrentReportID.tsx b/src/components/withCurrentReportID.tsx index cc49c44e0e77..d5c5a6896a9c 100644 --- a/src/components/withCurrentReportID.tsx +++ b/src/components/withCurrentReportID.tsx @@ -43,7 +43,7 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro /** * This is to make sure we don't set the undefined as reportID when * switching between chat list and settings->workspaces tab. - * and doing so avoid unnecessary re-render of `useOrderedReportIDs`. + * and doing so avoid unnecessary re-render of `useOrderedReportListItems`. */ if (reportID) { setCurrentReportID(reportID); diff --git a/src/hooks/useOrderedReportIDs.tsx b/src/hooks/useOrderedReportListItems.tsx similarity index 92% rename from src/hooks/useOrderedReportIDs.tsx rename to src/hooks/useOrderedReportListItems.tsx index 8f071f036fc7..643fcf053012 100644 --- a/src/hooks/useOrderedReportIDs.tsx +++ b/src/hooks/useOrderedReportListItems.tsx @@ -26,13 +26,13 @@ type OnyxProps = { draftComments: OnyxCollection; }; -type WithOrderedReportIDsContextProviderProps = OnyxProps & { +type WithOrderedReportListItemsContextProviderProps = OnyxProps & { children: React.ReactNode; }; -const OrderedReportIDsContext = createContext({}); +const OrderedReportListItemsContext = createContext({}); -function WithOrderedReportIDsContextProvider({ +function WithOrderedReportListItemsContextProvider({ children, chatReports, betas, @@ -43,7 +43,7 @@ function WithOrderedReportIDsContextProvider({ priorityMode, preferredLocale, draftComments, -}: WithOrderedReportIDsContextProviderProps) { +}: WithOrderedReportListItemsContextProviderProps) { const currentReportIDValue = useCurrentReportID(); const personalDetails = usePersonalDetails(); const {canUseViolations} = usePermissions(); @@ -108,7 +108,7 @@ function WithOrderedReportIDsContextProvider({ [orderedReportIDsWithCurrentReport, canUseViolations, personalDetails, draftComments, preferredLocale, chatReports, allReportActions, policies, transactionViolations], ); - return {children}; + return {children}; } /** @@ -174,7 +174,7 @@ const reportActionsSelector = (reportActions: OnyxEntry) => { }); }; -const OrderedReportIDsContextProvider = withOnyx({ +const OrderedReportListItemsContextProvider = withOnyx({ // @ts-expect-error Need some help in determining the correct type for this selector chatReports: { key: ONYXKEYS.COLLECTION.REPORT, @@ -213,10 +213,10 @@ const OrderedReportIDsContextProvider = withOnyx { if (deepEqual(orderedReportListItemsRef.current, orderedReportListItems)) { From df1069c1a1dc695f8faa0dcfd748541bbe1ea8bb Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 28 Feb 2024 18:05:35 +0100 Subject: [PATCH 0018/1506] perf: use memo for extraData in LHNOptionList --- src/components/LHNOptionsList/LHNOptionsList.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 8b18630a9646..8301bc034a74 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -1,6 +1,6 @@ import {FlashList} from '@shopify/flash-list'; import type {ReactElement} from 'react'; -import React, {memo, useCallback} from 'react'; +import React, {memo, useCallback, useMemo} from 'react'; import {StyleSheet, View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import {OrderedReports} from '@libs/SidebarUtils'; @@ -53,6 +53,8 @@ function LHNOptionsList({ [shouldDisableFocusOptions, currentReportID, onSelectRow, onLayoutItem, optionMode], ); + const extraData = useMemo(() => [currentReportID], [currentReportID]); + return ( From 1d294bcd188dd768d7c917c48ba19f54bf5a0275 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 28 Feb 2024 19:14:59 +0100 Subject: [PATCH 0019/1506] fix: typescript fixes --- .../LHNOptionsList/LHNOptionsList.tsx | 5 +- .../LHNOptionsList/OptionRowLHNData.tsx | 14 +--- src/components/LHNOptionsList/types.ts | 74 ++++--------------- src/hooks/useOrderedReportListItems.tsx | 3 +- src/libs/SidebarUtils.ts | 3 +- src/pages/home/sidebar/SidebarLinks.js | 2 +- src/pages/home/sidebar/SidebarLinksData.js | 4 + src/types/onyx/index.ts | 2 + 8 files changed, 27 insertions(+), 80 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 8301bc034a74..1884997a58fe 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -3,13 +3,12 @@ import type {ReactElement} from 'react'; import React, {memo, useCallback, useMemo} from 'react'; import {StyleSheet, View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; -import {OrderedReports} from '@libs/SidebarUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import OptionRowLHNData from './OptionRowLHNData'; -import type {LHNOptionsListProps, RenderItemProps} from './types'; +import type {LHNOptionsListProps, OptionListItem, RenderItemProps} from './types'; -const keyExtractor = (item: OrderedReports) => `report_${item?.reportID}`; +const keyExtractor = (item: OptionListItem) => `report_${item.reportID}`; function LHNOptionsList({ style, diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index 622d7b9d9123..bc1a06ce5f67 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -1,10 +1,5 @@ -import {deepEqual} from 'fast-equals'; -import React, {useEffect, useMemo, useRef} from 'react'; -import * as ReportUtils from '@libs/ReportUtils'; -import SidebarUtils from '@libs/SidebarUtils'; +import React, {useEffect} from 'react'; import * as Report from '@userActions/Report'; -import CONST from '@src/CONST'; -import type {OptionData} from '@src/libs/ReportUtils'; import OptionRowLHN from './OptionRowLHN'; import type {OptionRowLHNDataProps} from './types'; @@ -14,12 +9,7 @@ import type {OptionRowLHNDataProps} from './types'; * The OptionRowLHN component is memoized, so it will only * re-render if the data really changed. */ -function OptionRowLHNData({ - isFocused = false, - comment, - optionItem, - ...propsToForward -}: OptionRowLHNDataProps) { +function OptionRowLHNData({isFocused = false, comment, optionItem, ...propsToForward}: OptionRowLHNDataProps) { const reportID = propsToForward.reportID; useEffect(() => { diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index 4c79536571bf..3f72748ab8a8 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -1,38 +1,22 @@ import type {ContentStyle} from '@shopify/flash-list'; import type {RefObject} from 'react'; import type {LayoutChangeEvent, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {CurrentReportIDContextValue} from '@components/withCurrentReportID'; import type CONST from '@src/CONST'; import type {OptionData} from '@src/libs/ReportUtils'; -import type {Locale, PersonalDetailsList, Policy, Report, ReportAction, ReportActions, Transaction, TransactionViolation} from '@src/types/onyx'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; -import { OrderedReports } from '@libs/SidebarUtils'; type OptionMode = ValueOf; -type LHNOptionsListOnyxProps = { - /** The policy which the user has access to and which the report could be tied to */ - policy: OnyxCollection; - - /** Array of report actions for this report */ - reportActions: OnyxCollection; - - /** Indicates which locale the user currently has selected */ - preferredLocale: OnyxEntry; - - /** List of users' personal details */ - personalDetails: OnyxEntry; - - /** The transaction from the parent report action */ - transactions: OnyxCollection; +type OptionListItem = { + /** The reportID of the report */ + reportID: string; - /** List of draft comments */ - draftComments: OnyxCollection; + /** The item that should be rendered */ + optionItem: OptionData | undefined; - /** The list of transaction violations */ - transactionViolations: OnyxCollection; + /** Comment added to report */ + comment: string; }; type CustomLHNOptionsListProps = { @@ -43,7 +27,7 @@ type CustomLHNOptionsListProps = { contentContainerStyles?: StyleProp; /** Sections for the section list */ - data: string[]; + data: OptionListItem[]; /** Callback to fire when a row is selected */ onSelectRow?: (optionItem: OptionData, popoverAnchor: RefObject) => void; @@ -58,51 +42,21 @@ type CustomLHNOptionsListProps = { onFirstItemRendered: () => void; }; -type LHNOptionsListProps = CustomLHNOptionsListProps & CurrentReportIDContextValue & LHNOptionsListOnyxProps; +type LHNOptionsListProps = CustomLHNOptionsListProps & CurrentReportIDContextValue; type OptionRowLHNDataProps = { /** Whether row should be focused */ isFocused?: boolean; - /** List of users' personal details */ - personalDetails?: PersonalDetailsList; - - /** The preferred language for the app */ - preferredLocale?: OnyxEntry; - - /** The full data of the report */ - fullReport: OnyxEntry; - - /** The policy which the user has access to and which the report could be tied to */ - policy?: OnyxEntry; - - /** The action from the parent report */ - parentReportAction?: OnyxEntry; - - /** The transaction from the parent report action */ - transaction: OnyxEntry; - - /** The transaction linked to the report's last action */ - lastReportActionTransaction?: OnyxEntry; - /** Comment added to report */ comment: string; - /** The receipt transaction from the parent report action */ - receiptTransactions: OnyxCollection; + /** The item that should be rendered */ + optionItem: OptionData | undefined; /** The reportID of the report */ reportID: string; - /** Array of report actions for this report */ - reportActions: OnyxEntry; - - /** List of transaction violation */ - transactionViolations: OnyxCollection; - - /** Whether the user can use violations */ - canUseViolations: boolean | undefined; - /** Toggle between compact and default view */ viewMode?: OptionMode; @@ -130,11 +84,11 @@ type OptionRowLHNProps = { style?: StyleProp; /** The item that should be rendered */ - optionItem?: OptionData; + optionItem: OptionData | undefined; onLayout?: (event: LayoutChangeEvent) => void; }; -type RenderItemProps = {item: OrderedReports}; +type RenderItemProps = {item: OptionListItem}; -export type {LHNOptionsListProps, OptionRowLHNDataProps, OptionRowLHNProps, LHNOptionsListOnyxProps, RenderItemProps}; +export type {LHNOptionsListProps, OptionRowLHNDataProps, OptionRowLHNProps, OptionListItem, RenderItemProps}; diff --git a/src/hooks/useOrderedReportListItems.tsx b/src/hooks/useOrderedReportListItems.tsx index 643fcf053012..970f306c3a80 100644 --- a/src/hooks/useOrderedReportListItems.tsx +++ b/src/hooks/useOrderedReportListItems.tsx @@ -8,8 +8,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Beta, Locale, Policy, PolicyMembers, Report, ReportAction, ReportActions, TransactionViolation} from '@src/types/onyx'; -import type PriorityMode from '@src/types/onyx/PriorityMode'; +import type {Beta, Locale, Policy, PolicyMembers, PriorityMode, Report, ReportAction, ReportActions, TransactionViolation} from '@src/types/onyx'; import useActiveWorkspace from './useActiveWorkspace'; import useCurrentReportID from './useCurrentReportID'; import usePermissions from './usePermissions'; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 777a61c70733..2e929639270e 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -4,12 +4,11 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetails, PersonalDetailsList, TransactionViolation} from '@src/types/onyx'; +import type {PersonalDetails, PersonalDetailsList, ReportActions, TransactionViolation} from '@src/types/onyx'; import type Beta from '@src/types/onyx/Beta'; import type Policy from '@src/types/onyx/Policy'; import type PriorityMode from '@src/types/onyx/PriorityMode'; import type Report from '@src/types/onyx/Report'; -import type {ReportActions} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import * as CollectionUtils from './CollectionUtils'; diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 8cda70d2486a..4c9803e257bc 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -33,7 +33,7 @@ const basePropTypes = { const propTypes = { ...basePropTypes, - optionListItems: PropTypes.arrayOf(PropTypes.string).isRequired, + optionListItems: PropTypes.arrayOf(PropTypes.object).isRequired, isLoading: PropTypes.bool.isRequired, diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index 91de05450aae..088899425a0b 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -31,12 +31,16 @@ const propTypes = { network: networkPropTypes.isRequired, + // eslint-disable-next-line react/forbid-prop-types + policyMembers: PropTypes.object, + ...withCurrentUserPersonalDetailsPropTypes, }; const defaultProps = { isLoadingApp: true, priorityMode: CONST.PRIORITY_MODE.DEFAULT, + policyMembers: {}, ...withCurrentUserPersonalDetailsDefaultProps, }; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 6846fc302639..f6147a27a49b 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -37,6 +37,7 @@ import type {PolicyMembers} from './PolicyMember'; import type PolicyMember from './PolicyMember'; import type {PolicyReportField, PolicyReportFields} from './PolicyReportField'; import type {PolicyTag, PolicyTagList, PolicyTags} from './PolicyTag'; +import type PriorityMode from './PriorityMode'; import type PrivatePersonalDetails from './PrivatePersonalDetails'; import type RecentlyUsedCategories from './RecentlyUsedCategories'; import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; @@ -111,6 +112,7 @@ export type { PolicyTag, PolicyTags, PolicyTagList, + PriorityMode, PrivatePersonalDetails, RecentWaypoint, RecentlyUsedCategories, From 7dfba4379e6bc48ef256c6363743ea13ad65b614 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 28 Feb 2024 20:08:53 +0100 Subject: [PATCH 0020/1506] fix: update reportActionsSelector to match the one from SidebarLinksData --- src/hooks/useOrderedReportListItems.tsx | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/hooks/useOrderedReportListItems.tsx b/src/hooks/useOrderedReportListItems.tsx index 970f306c3a80..fa2d18684464 100644 --- a/src/hooks/useOrderedReportListItems.tsx +++ b/src/hooks/useOrderedReportListItems.tsx @@ -1,3 +1,5 @@ +import lodashGet from 'lodash/get'; +import lodashMap from 'lodash/map'; import React, {createContext, useCallback, useContext, useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; @@ -152,29 +154,27 @@ const chatReportSelector = (report: OnyxEntry) => isUnreadWithMention: ReportUtils.isUnreadWithMention(report), }; -const reportActionsSelector = (reportActions: OnyxEntry) => { - if (!reportActions) { - return []; - } +const reportActionsSelector = (reportActions: OnyxEntry) => + reportActions && + lodashMap(reportActions, (reportAction) => { + const {reportActionID, parentReportActionID, actionName, errors = [], originalMessage} = reportAction; + const decision = lodashGet(reportAction, 'message[0].moderationDecision.decision'); - return Object.values(reportActions).map((reportAction) => { - const {reportActionID, actionName, originalMessage} = reportAction ?? {}; - const decision = reportAction?.message?.[0]?.moderationDecision?.decision; return { reportActionID, + parentReportActionID, actionName, - originalMessage, + errors, message: [ { moderationDecision: {decision}, }, ], + originalMessage, }; }); -}; const OrderedReportListItemsContextProvider = withOnyx({ - // @ts-expect-error Need some help in determining the correct type for this selector chatReports: { key: ONYXKEYS.COLLECTION.REPORT, selector: chatReportSelector, @@ -190,7 +190,6 @@ const OrderedReportListItemsContextProvider = withOnyx Date: Thu, 29 Feb 2024 18:01:07 +0500 Subject: [PATCH 0021/1506] fix: typescript issues --- src/hooks/useOrderedReportListItems.tsx | 70 +------------------------ src/libs/SidebarUtils.ts | 4 +- 2 files changed, 4 insertions(+), 70 deletions(-) diff --git a/src/hooks/useOrderedReportListItems.tsx b/src/hooks/useOrderedReportListItems.tsx index fa2d18684464..d368abd897af 100644 --- a/src/hooks/useOrderedReportListItems.tsx +++ b/src/hooks/useOrderedReportListItems.tsx @@ -1,9 +1,7 @@ -import lodashGet from 'lodash/get'; -import lodashMap from 'lodash/map'; import React, {createContext, useCallback, useContext, useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {usePersonalDetails} from '@components/OnyxProvider'; +import {usePersonalDetails, useReport} from '@components/OnyxProvider'; import {getCurrentUserAccountID} from '@libs/actions/Report'; import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -19,7 +17,7 @@ type OnyxProps = { chatReports: OnyxCollection; betas: OnyxEntry; policies: OnyxCollection; - allReportActions: OnyxCollection; + allReportActions: OnyxCollection; transactionViolations: OnyxCollection; policyMembers: OnyxCollection; priorityMode: OnyxEntry; @@ -112,72 +110,9 @@ function WithOrderedReportListItemsContextProvider({ return {children}; } -/** - * This function (and the few below it), narrow down the data from Onyx to just the properties that we want to trigger a re-render of the component. This helps minimize re-rendering - * and makes the entire component more performant because it's not re-rendering when a bunch of properties change which aren't ever used in the UI. - */ -const chatReportSelector = (report: OnyxEntry) => - report && { - reportID: report.reportID, - participantAccountIDs: report.participantAccountIDs, - hasDraft: report.hasDraft, - isPinned: report.isPinned, - isHidden: report.isHidden, - notificationPreference: report.notificationPreference, - errorFields: {addWorkspaceRoom: report.errorFields?.addWorkspaceRoom}, - lastMessageText: report.lastMessageText, - lastVisibleActionCreated: report.lastVisibleActionCreated, - iouReportID: report.iouReportID, - total: report.total, - nonReimbursableTotal: report.nonReimbursableTotal, - hasOutstandingChildRequest: report.hasOutstandingChildRequest, - isWaitingOnBankAccount: report.isWaitingOnBankAccount, - statusNum: report.statusNum, - stateNum: report.stateNum, - chatType: report.chatType, - type: report.type, - policyID: report.policyID, - visibility: report.visibility, - lastReadTime: report.lastReadTime, - // Needed for name sorting: - reportName: report.reportName, - policyName: report.policyName, - oldPolicyName: report.oldPolicyName, - // Other less obvious properites considered for sorting: - ownerAccountID: report.ownerAccountID, - currency: report.currency, - managerID: report.managerID, - // Other important less obivous properties for filtering: - parentReportActionID: report.parentReportActionID, - parentReportID: report.parentReportID, - isDeletedParentAction: report.isDeletedParentAction, - isUnreadWithMention: ReportUtils.isUnreadWithMention(report), - }; - -const reportActionsSelector = (reportActions: OnyxEntry) => - reportActions && - lodashMap(reportActions, (reportAction) => { - const {reportActionID, parentReportActionID, actionName, errors = [], originalMessage} = reportAction; - const decision = lodashGet(reportAction, 'message[0].moderationDecision.decision'); - - return { - reportActionID, - parentReportActionID, - actionName, - errors, - message: [ - { - moderationDecision: {decision}, - }, - ], - originalMessage, - }; - }); - const OrderedReportListItemsContextProvider = withOnyx({ chatReports: { key: ONYXKEYS.COLLECTION.REPORT, - selector: chatReportSelector, initialValue: {}, }, priorityMode: { @@ -190,7 +125,6 @@ const OrderedReportListItemsContextProvider = withOnyx, policies: OnyxCollection, priorityMode: OnyxEntry, - allReportActions: OnyxCollection, + allReportActions: OnyxCollection, transactionViolations: OnyxCollection, currentPolicyID = '', policyMemberAccountIDs: number[] = [], @@ -80,7 +80,7 @@ function getOrderedReportIDs( let reportsToDisplay = allReportsDictValues.filter((report) => { const parentReportActionsKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`; const parentReportActions = allReportActions?.[parentReportActionsKey]; - const parentReportAction = parentReportActions?.find((action) => action && report && action?.reportActionID === report?.parentReportActionID); + const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '']; const doesReportHaveViolations = !!betas && betas.includes(CONST.BETAS.VIOLATIONS) && !!parentReportAction && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction); return ReportUtils.shouldReportBeInOptionList({ From 6739b587e93144f2dfa7a2250ac15e3b11488289 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 29 Feb 2024 18:01:30 +0500 Subject: [PATCH 0022/1506] test: fix reassure failing test --- tests/perf-test/SidebarUtils.perf-test.ts | 26 ++++++--------------- tests/utils/LHNTestUtils.tsx | 3 ++- tests/utils/collections/createCollection.ts | 21 +++++++++++++++++ 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index 3aa65331b9c2..6fb63878a832 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -8,7 +8,7 @@ import type {PersonalDetails, TransactionViolation} from '@src/types/onyx'; import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; import type ReportAction from '@src/types/onyx/ReportAction'; -import createCollection from '../utils/collections/createCollection'; +import createCollection, {createNestedCollection} from '../utils/collections/createCollection'; import createPersonalDetails from '../utils/collections/personalDetails'; import createRandomPolicy from '../utils/collections/policies'; import createRandomReportAction from '../utils/collections/reportActions'; @@ -27,6 +27,12 @@ const reportActions = createCollection( (index) => createRandomReportAction(index), ); +const allReportActions = createNestedCollection( + (item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, + (item) => `${item.reportActionID}`, + (index) => createRandomReportAction(index), +); + const personalDetails = createCollection( (item) => item.accountID, (index) => createPersonalDetails(index), @@ -82,24 +88,6 @@ describe('SidebarUtils', () => { (index) => createRandomPolicy(index), ); - const allReportActions = Object.fromEntries( - Object.keys(reportActions).map((key) => [ - key, - [ - { - errors: reportActions[key].errors ?? [], - message: [ - { - moderationDecision: { - decision: reportActions[key].message?.[0]?.moderationDecision?.decision, - }, - }, - ], - }, - ], - ]), - ) as unknown as OnyxCollection; - await waitForBatchedUpdates(); await measureFunction(() => SidebarUtils.getOrderedReportIDs(currentReportId, allReports, betas, policies, CONST.PRIORITY_MODE.DEFAULT, allReportActions, transactionViolations)); }); diff --git a/tests/utils/LHNTestUtils.tsx b/tests/utils/LHNTestUtils.tsx index 80f28002f975..a10dbbdc17f8 100644 --- a/tests/utils/LHNTestUtils.tsx +++ b/tests/utils/LHNTestUtils.tsx @@ -10,6 +10,7 @@ import {LocaleContextProvider} from '@components/LocaleContextProvider'; import OnyxProvider from '@components/OnyxProvider'; import {CurrentReportIDContextProvider} from '@components/withCurrentReportID'; import {EnvironmentProvider} from '@components/withEnvironment'; +import {OrderedReportListItemsContextProvider} from '@hooks/useOrderedReportListItems'; import DateUtils from '@libs/DateUtils'; import ReportActionItemSingle from '@pages/home/report/ReportActionItemSingle'; import SidebarLinksData from '@pages/home/sidebar/SidebarLinksData'; @@ -280,7 +281,7 @@ function getFakeAdvancedReportAction(actionName: ActionName = 'IOU', actor = 'em function MockedSidebarLinks({currentReportID = ''}: MockedSidebarLinksProps) { return ( - + {}} diff --git a/tests/utils/collections/createCollection.ts b/tests/utils/collections/createCollection.ts index 848ef8f81f47..e62d9769bc53 100644 --- a/tests/utils/collections/createCollection.ts +++ b/tests/utils/collections/createCollection.ts @@ -9,3 +9,24 @@ export default function createCollection(createKey: (item: T, index: number) return map; } + +export function createNestedCollection( + createParentKey: (item: T, index: number) => string | number, + createKey: (item: T, index: number) => string | number, + createItem: (index: number) => T, + length = 500, +): Record> { + const map: Record> = {}; + + for (let i = 0; i < length; i++) { + const item = createItem(i); + const itemKey = createKey(item, i); + const itemParentKey = createParentKey(item, i); + map[itemParentKey] = { + ...map[itemParentKey], + [itemKey]: item, + }; + } + + return map; +} From 92fae0c08c3c3ca517fb57313f95e4fd2e77abf0 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 29 Feb 2024 18:02:17 +0500 Subject: [PATCH 0023/1506] revert: move option item data calculation to the renderItem component --- .../LHNOptionsList/LHNOptionsList.tsx | 117 +++++++++++++++--- .../LHNOptionsList/OptionRowLHNData.tsx | 63 +++++++++- src/components/LHNOptionsList/types.ts | 74 +++++++++-- src/hooks/useOrderedReportListItems.tsx | 47 +------ 4 files changed, 226 insertions(+), 75 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 1884997a58fe..5569e53942aa 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -2,13 +2,18 @@ import {FlashList} from '@shopify/flash-list'; import type {ReactElement} from 'react'; import React, {memo, useCallback, useMemo} from 'react'; import {StyleSheet, View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import withCurrentReportID from '@components/withCurrentReportID'; +import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import OptionRowLHNData from './OptionRowLHNData'; -import type {LHNOptionsListProps, OptionListItem, RenderItemProps} from './types'; +import type {LHNOptionsListOnyxProps, LHNOptionsListProps, RenderItemProps} from './types'; -const keyExtractor = (item: OptionListItem) => `report_${item.reportID}`; +const keyExtractor = (item: string) => `report_${item}`; function LHNOptionsList({ style, @@ -18,9 +23,18 @@ function LHNOptionsList({ shouldDisableFocusOptions = false, currentReportID = '', optionMode, + reports = {}, + reportActions = {}, + policy = {}, + preferredLocale = CONST.LOCALES.DEFAULT, + personalDetails = {}, + transactions = {}, + draftComments = {}, + transactionViolations = {}, onFirstItemRendered = () => {}, }: LHNOptionsListProps) { const styles = useThemeStyles(); + const {canUseViolations} = usePermissions(); // When the first item renders we want to call the onFirstItemRendered callback. // At this point in time we know that the list is actually displaying items. @@ -38,18 +52,64 @@ function LHNOptionsList({ * Function which renders a row in the list */ const renderItem = useCallback( - ({item}: RenderItemProps): ReactElement => ( - - ), - [shouldDisableFocusOptions, currentReportID, onSelectRow, onLayoutItem, optionMode], + ({item: reportID}: RenderItemProps): ReactElement => { + const itemFullReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; + const itemReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? null; + const itemParentReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport?.parentReportID}`] ?? null; + const itemParentReportAction = itemParentReportActions?.[itemFullReport?.parentReportActionID ?? ''] ?? null; + const itemPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport?.policyID}`] ?? null; + const transactionID = itemParentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? itemParentReportAction.originalMessage.IOUTransactionID ?? '' : ''; + const itemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? null; + const itemComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] ?? ''; + const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions); + const lastReportAction = sortedReportActions[0]; + + // Get the transaction for the last report action + let lastReportActionTransactionID = ''; + + if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { + lastReportActionTransactionID = lastReportAction.originalMessage?.IOUTransactionID ?? ''; + } + const lastReportActionTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${lastReportActionTransactionID}`] ?? {}; + + return ( + + ); + }, + [ + currentReportID, + draftComments, + onSelectRow, + optionMode, + personalDetails, + policy, + preferredLocale, + reportActions, + reports, + shouldDisableFocusOptions, + transactions, + transactionViolations, + canUseViolations, + onLayoutItem, + ], ); const extraData = useMemo(() => [currentReportID], [currentReportID]); @@ -74,6 +134,33 @@ function LHNOptionsList({ LHNOptionsList.displayName = 'LHNOptionsList'; -export default memo(LHNOptionsList); +export default withCurrentReportID( + withOnyx({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, + reportActions: { + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + }, + policy: { + key: ONYXKEYS.COLLECTION.POLICY, + }, + preferredLocale: { + key: ONYXKEYS.NVP_PREFERRED_LOCALE, + }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + transactions: { + key: ONYXKEYS.COLLECTION.TRANSACTION, + }, + draftComments: { + key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, + }, + transactionViolations: { + key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, + }, + })(memo(LHNOptionsList)), +); export type {LHNOptionsListProps}; diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index bc1a06ce5f67..a18d5a8ec1ec 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -1,5 +1,10 @@ -import React, {useEffect} from 'react'; +import {deepEqual} from 'fast-equals'; +import React, {useEffect, useMemo, useRef} from 'react'; +import * as ReportUtils from '@libs/ReportUtils'; +import SidebarUtils from '@libs/SidebarUtils'; import * as Report from '@userActions/Report'; +import CONST from '@src/CONST'; +import type {OptionData} from '@src/libs/ReportUtils'; import OptionRowLHN from './OptionRowLHN'; import type {OptionRowLHNDataProps} from './types'; @@ -9,9 +14,63 @@ import type {OptionRowLHNDataProps} from './types'; * The OptionRowLHN component is memoized, so it will only * re-render if the data really changed. */ -function OptionRowLHNData({isFocused = false, comment, optionItem, ...propsToForward}: OptionRowLHNDataProps) { +function OptionRowLHNData({ + isFocused = false, + fullReport, + reportActions, + personalDetails = {}, + preferredLocale = CONST.LOCALES.DEFAULT, + comment, + policy, + receiptTransactions, + parentReportAction, + transaction, + lastReportActionTransaction = {}, + transactionViolations, + canUseViolations, + ...propsToForward +}: OptionRowLHNDataProps) { const reportID = propsToForward.reportID; + const optionItemRef = useRef(); + + const hasViolations = canUseViolations && ReportUtils.doesTransactionThreadHaveViolations(fullReport, transactionViolations, parentReportAction ?? null); + + const optionItem = useMemo(() => { + // Note: ideally we'd have this as a dependent selector in onyx! + const item = SidebarUtils.getOptionData({ + report: fullReport, + reportActions, + personalDetails, + preferredLocale: preferredLocale ?? CONST.LOCALES.DEFAULT, + policy, + parentReportAction, + hasViolations: !!hasViolations, + }); + if (deepEqual(item, optionItemRef.current)) { + return optionItemRef.current; + } + + optionItemRef.current = item; + + return item; + // Listen parentReportAction to update title of thread report when parentReportAction changed + // Listen to transaction to update title of transaction report when transaction changed + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + fullReport, + lastReportActionTransaction, + reportActions, + personalDetails, + preferredLocale, + policy, + parentReportAction, + transaction, + transactionViolations, + canUseViolations, + receiptTransactions, + ]); + useEffect(() => { if (!optionItem || !!optionItem.hasDraftComment || !comment || comment.length <= 0 || isFocused) { return; diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index 3f72748ab8a8..e09e5cb6c8b5 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -1,22 +1,40 @@ import type {ContentStyle} from '@shopify/flash-list'; import type {RefObject} from 'react'; import type {LayoutChangeEvent, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {CurrentReportIDContextValue} from '@components/withCurrentReportID'; import type CONST from '@src/CONST'; import type {OptionData} from '@src/libs/ReportUtils'; +import type {Locale, PersonalDetailsList, Policy, Report, ReportAction, ReportActions, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {EmptyObject} from '@src/types/utils/EmptyObject'; type OptionMode = ValueOf; -type OptionListItem = { - /** The reportID of the report */ - reportID: string; +type LHNOptionsListOnyxProps = { + /** The policy which the user has access to and which the report could be tied to */ + policy: OnyxCollection; - /** The item that should be rendered */ - optionItem: OptionData | undefined; + /** All reports shared with the user */ + reports: OnyxCollection; - /** Comment added to report */ - comment: string; + /** Array of report actions for this report */ + reportActions: OnyxCollection; + + /** Indicates which locale the user currently has selected */ + preferredLocale: OnyxEntry; + + /** List of users' personal details */ + personalDetails: OnyxEntry; + + /** The transaction from the parent report action */ + transactions: OnyxCollection; + + /** List of draft comments */ + draftComments: OnyxCollection; + + /** The list of transaction violations */ + transactionViolations: OnyxCollection; }; type CustomLHNOptionsListProps = { @@ -27,7 +45,7 @@ type CustomLHNOptionsListProps = { contentContainerStyles?: StyleProp; /** Sections for the section list */ - data: OptionListItem[]; + data: string[]; /** Callback to fire when a row is selected */ onSelectRow?: (optionItem: OptionData, popoverAnchor: RefObject) => void; @@ -42,21 +60,51 @@ type CustomLHNOptionsListProps = { onFirstItemRendered: () => void; }; -type LHNOptionsListProps = CustomLHNOptionsListProps & CurrentReportIDContextValue; +type LHNOptionsListProps = CustomLHNOptionsListProps & CurrentReportIDContextValue & LHNOptionsListOnyxProps; type OptionRowLHNDataProps = { /** Whether row should be focused */ isFocused?: boolean; + /** List of users' personal details */ + personalDetails?: PersonalDetailsList; + + /** The preferred language for the app */ + preferredLocale?: OnyxEntry; + + /** The full data of the report */ + fullReport: OnyxEntry; + + /** The policy which the user has access to and which the report could be tied to */ + policy?: OnyxEntry; + + /** The action from the parent report */ + parentReportAction?: OnyxEntry; + + /** The transaction from the parent report action */ + transaction: OnyxEntry; + + /** The transaction linked to the report's last action */ + lastReportActionTransaction?: OnyxEntry; + /** Comment added to report */ comment: string; - /** The item that should be rendered */ - optionItem: OptionData | undefined; + /** The receipt transaction from the parent report action */ + receiptTransactions: OnyxCollection; /** The reportID of the report */ reportID: string; + /** Array of report actions for this report */ + reportActions: OnyxEntry; + + /** List of transaction violation */ + transactionViolations: OnyxCollection; + + /** Whether the user can use violations */ + canUseViolations: boolean | undefined; + /** Toggle between compact and default view */ viewMode?: OptionMode; @@ -89,6 +137,6 @@ type OptionRowLHNProps = { onLayout?: (event: LayoutChangeEvent) => void; }; -type RenderItemProps = {item: OptionListItem}; +type RenderItemProps = {item: string}; -export type {LHNOptionsListProps, OptionRowLHNDataProps, OptionRowLHNProps, OptionListItem, RenderItemProps}; +export type {LHNOptionsListProps, OptionRowLHNDataProps, OptionRowLHNProps, LHNOptionsListOnyxProps, RenderItemProps}; diff --git a/src/hooks/useOrderedReportListItems.tsx b/src/hooks/useOrderedReportListItems.tsx index d368abd897af..f9aaeaf400c1 100644 --- a/src/hooks/useOrderedReportListItems.tsx +++ b/src/hooks/useOrderedReportListItems.tsx @@ -1,17 +1,14 @@ import React, {createContext, useCallback, useContext, useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {usePersonalDetails, useReport} from '@components/OnyxProvider'; import {getCurrentUserAccountID} from '@libs/actions/Report'; import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; -import * as ReportUtils from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Beta, Locale, Policy, PolicyMembers, PriorityMode, Report, ReportAction, ReportActions, TransactionViolation} from '@src/types/onyx'; +import type {Beta, Policy, PolicyMembers, PriorityMode, Report, ReportAction, ReportActions, TransactionViolation} from '@src/types/onyx'; import useActiveWorkspace from './useActiveWorkspace'; import useCurrentReportID from './useCurrentReportID'; -import usePermissions from './usePermissions'; type OnyxProps = { chatReports: OnyxCollection; @@ -21,8 +18,6 @@ type OnyxProps = { transactionViolations: OnyxCollection; policyMembers: OnyxCollection; priorityMode: OnyxEntry; - preferredLocale: OnyxEntry; - draftComments: OnyxCollection; }; type WithOrderedReportListItemsContextProviderProps = OnyxProps & { @@ -40,12 +35,8 @@ function WithOrderedReportListItemsContextProvider({ transactionViolations, policyMembers, priorityMode, - preferredLocale, - draftComments, }: WithOrderedReportListItemsContextProviderProps) { const currentReportIDValue = useCurrentReportID(); - const personalDetails = usePersonalDetails(); - const {canUseViolations} = usePermissions(); const {activeWorkspaceID} = useActiveWorkspace(); const policyMemberAccountIDs = useMemo(() => getPolicyMembersByIdWithoutCurrentUser(policyMembers, activeWorkspaceID, getCurrentUserAccountID()), [activeWorkspaceID, policyMembers]); @@ -80,34 +71,7 @@ function WithOrderedReportListItemsContextProvider({ return orderedReportIDs; }, [getOrderedReportIDs, currentReportIDValue?.currentReportID, orderedReportIDs]); - const orderedReportListItems = useMemo( - () => - orderedReportIDsWithCurrentReport.map((reportID) => { - const itemFullReport = chatReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; - const itemReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? null; - const itemParentReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport?.parentReportID}`] ?? null; - const itemParentReportAction = itemParentReportActions?.[itemFullReport?.parentReportActionID ?? ''] ?? null; - const itemPolicy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport?.policyID}`] ?? null; - const itemComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] ?? ''; - - const hasViolations = canUseViolations && ReportUtils.doesTransactionThreadHaveViolations(itemFullReport, transactionViolations, itemParentReportAction ?? null); - - const item = SidebarUtils.getOptionData({ - report: itemFullReport, - reportActions: itemReportActions, - personalDetails, - preferredLocale: preferredLocale ?? CONST.LOCALES.DEFAULT, - policy: itemPolicy, - parentReportAction: itemParentReportAction, - hasViolations: !!hasViolations, - }); - - return {reportID, optionItem: item, comment: itemComment}; - }), - [orderedReportIDsWithCurrentReport, canUseViolations, personalDetails, draftComments, preferredLocale, chatReports, allReportActions, policies, transactionViolations], - ); - - return {children}; + return {children}; } const OrderedReportListItemsContextProvider = withOnyx({ @@ -138,13 +102,6 @@ const OrderedReportListItemsContextProvider = withOnyx Date: Thu, 29 Feb 2024 19:48:44 +0500 Subject: [PATCH 0024/1506] fix: linting --- src/hooks/useOrderedReportListItems.tsx | 2 +- src/pages/home/sidebar/SidebarLinks.js | 2 +- tests/utils/collections/createCollection.ts | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/hooks/useOrderedReportListItems.tsx b/src/hooks/useOrderedReportListItems.tsx index f9aaeaf400c1..bcd22320d1ca 100644 --- a/src/hooks/useOrderedReportListItems.tsx +++ b/src/hooks/useOrderedReportListItems.tsx @@ -6,7 +6,7 @@ import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Beta, Policy, PolicyMembers, PriorityMode, Report, ReportAction, ReportActions, TransactionViolation} from '@src/types/onyx'; +import type {Beta, Policy, PolicyMembers, PriorityMode, Report, ReportActions, TransactionViolation} from '@src/types/onyx'; import useActiveWorkspace from './useActiveWorkspace'; import useCurrentReportID from './useCurrentReportID'; diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 4c9803e257bc..8cda70d2486a 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -33,7 +33,7 @@ const basePropTypes = { const propTypes = { ...basePropTypes, - optionListItems: PropTypes.arrayOf(PropTypes.object).isRequired, + optionListItems: PropTypes.arrayOf(PropTypes.string).isRequired, isLoading: PropTypes.bool.isRequired, diff --git a/tests/utils/collections/createCollection.ts b/tests/utils/collections/createCollection.ts index e62d9769bc53..bcc37c301279 100644 --- a/tests/utils/collections/createCollection.ts +++ b/tests/utils/collections/createCollection.ts @@ -10,7 +10,7 @@ export default function createCollection(createKey: (item: T, index: number) return map; } -export function createNestedCollection( +function createNestedCollection( createParentKey: (item: T, index: number) => string | number, createKey: (item: T, index: number) => string | number, createItem: (index: number) => T, @@ -30,3 +30,7 @@ export function createNestedCollection( return map; } + +export { + createNestedCollection, +} \ No newline at end of file From e735658f12826766791c2bfa1d3bb7b82b4559d6 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 1 Mar 2024 13:30:58 +0500 Subject: [PATCH 0025/1506] test: fix failing test --- .../LHNOptionsList/LHNOptionsList.tsx | 2 +- src/components/withCurrentReportID.tsx | 2 +- src/hooks/useOrderedReportListItems.tsx | 18 +++++++-- tests/unit/SidebarOrderTest.ts | 6 +-- tests/utils/LHNTestUtils.tsx | 37 ++++++++++++------- 5 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 5569e53942aa..d5b422122144 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -112,7 +112,7 @@ function LHNOptionsList({ ], ); - const extraData = useMemo(() => [currentReportID], [currentReportID]); + const extraData = useMemo(() => [reportActions, reports, policy, personalDetails], [reportActions, reports, policy, personalDetails]); return ( diff --git a/src/components/withCurrentReportID.tsx b/src/components/withCurrentReportID.tsx index d5c5a6896a9c..0d052f759f81 100644 --- a/src/components/withCurrentReportID.tsx +++ b/src/components/withCurrentReportID.tsx @@ -45,7 +45,7 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro * switching between chat list and settings->workspaces tab. * and doing so avoid unnecessary re-render of `useOrderedReportListItems`. */ - if (reportID) { + if (reportID !== undefined) { setCurrentReportID(reportID); } }, diff --git a/src/hooks/useOrderedReportListItems.tsx b/src/hooks/useOrderedReportListItems.tsx index bcd22320d1ca..b160c372679a 100644 --- a/src/hooks/useOrderedReportListItems.tsx +++ b/src/hooks/useOrderedReportListItems.tsx @@ -22,6 +22,7 @@ type OnyxProps = { type WithOrderedReportListItemsContextProviderProps = OnyxProps & { children: React.ReactNode; + currentReportIDForTests?: string; }; const OrderedReportListItemsContext = createContext({}); @@ -35,8 +36,19 @@ function WithOrderedReportListItemsContextProvider({ transactionViolations, policyMembers, priorityMode, + /** + * Only required to make unit tests work, since we + * explicitly pass the currentReportID in LHNTestUtils + * to SidebarLinksData, so this context doesn't have an + * access to currentReportID in that case. + * + * So this is a work around to have currentReportID available + * only in testing environment. + */ + currentReportIDForTests, }: WithOrderedReportListItemsContextProviderProps) { const currentReportIDValue = useCurrentReportID(); + const derivedCurrentReportID = currentReportIDForTests ?? currentReportIDValue?.currentReportID; const {activeWorkspaceID} = useActiveWorkspace(); const policyMemberAccountIDs = useMemo(() => getPolicyMembersByIdWithoutCurrentUser(policyMembers, activeWorkspaceID, getCurrentUserAccountID()), [activeWorkspaceID, policyMembers]); @@ -65,11 +77,11 @@ function WithOrderedReportListItemsContextProvider({ // the current report is missing from the list, which should very rarely happen. In this // case we re-generate the list a 2nd time with the current report included. const orderedReportIDsWithCurrentReport = useMemo(() => { - if (currentReportIDValue?.currentReportID && !orderedReportIDs.includes(currentReportIDValue.currentReportID)) { - return getOrderedReportIDs(currentReportIDValue.currentReportID); + if (derivedCurrentReportID && !orderedReportIDs.includes(derivedCurrentReportID)) { + return getOrderedReportIDs(derivedCurrentReportID); } return orderedReportIDs; - }, [getOrderedReportIDs, currentReportIDValue?.currentReportID, orderedReportIDs]); + }, [getOrderedReportIDs, derivedCurrentReportID, orderedReportIDs]); return {children}; } diff --git a/tests/unit/SidebarOrderTest.ts b/tests/unit/SidebarOrderTest.ts index 27da8348f43d..10d30e4c6dc8 100644 --- a/tests/unit/SidebarOrderTest.ts +++ b/tests/unit/SidebarOrderTest.ts @@ -298,7 +298,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, ...reportCollectionDataSet, }), ) @@ -362,7 +362,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, ...reportCollectionDataSet, }), ) @@ -429,7 +429,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.POLICY}${fakeReport.policyID}`]: fakePolicy, ...reportCollectionDataSet, }), diff --git a/tests/utils/LHNTestUtils.tsx b/tests/utils/LHNTestUtils.tsx index a10dbbdc17f8..c8c3f145d951 100644 --- a/tests/utils/LHNTestUtils.tsx +++ b/tests/utils/LHNTestUtils.tsx @@ -281,19 +281,30 @@ function getFakeAdvancedReportAction(actionName: ActionName = 'IOU', actor = 'em function MockedSidebarLinks({currentReportID = ''}: MockedSidebarLinksProps) { return ( - - {}} - insets={{ - top: 0, - left: 0, - right: 0, - bottom: 0, - }} - isSmallScreenWidth={false} - currentReportID={currentReportID} - /> + + {/* + * Only required to make unit tests work, since we + * explicitly pass the currentReportID in LHNTestUtils + * to SidebarLinksData, so this context doesn't have an + * access to currentReportID in that case. + * + * So this is a work around to have currentReportID available + * only in testing environment. + * */} + + {}} + insets={{ + top: 0, + left: 0, + right: 0, + bottom: 0, + }} + isSmallScreenWidth={false} + currentReportID={currentReportID} + /> + ); } From 94fcc48f89f95ba50fc22a20ac04183dbd25cb8a Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 1 Mar 2024 13:31:12 +0500 Subject: [PATCH 0026/1506] fix: prettier --- tests/utils/collections/createCollection.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/utils/collections/createCollection.ts b/tests/utils/collections/createCollection.ts index bcc37c301279..4a2a1fa4eb78 100644 --- a/tests/utils/collections/createCollection.ts +++ b/tests/utils/collections/createCollection.ts @@ -31,6 +31,4 @@ function createNestedCollection( return map; } -export { - createNestedCollection, -} \ No newline at end of file +export {createNestedCollection}; From 57e63066ecf2cb1654002ffb909ca5f8ed9edfaa Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 4 Mar 2024 12:23:17 +0100 Subject: [PATCH 0027/1506] Add initial config for MyTripsPage --- src/ROUTES.ts | 2 ++ src/SCREENS.ts | 3 +++ .../BaseCentralPaneNavigator.tsx | 6 +++++- .../BottomTabBar.tsx | 9 +++++---- src/libs/Navigation/linkingConfig/config.ts | 2 ++ src/libs/Navigation/types.ts | 1 + src/pages/Travel/MyTripsPage.tsx | 20 +++++++++++++++++++ 7 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 src/pages/Travel/MyTripsPage.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index e9cdce4f6ed9..f7c455e923a6 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -553,6 +553,8 @@ const ROUTES = { getRoute: (contentType: string, backTo?: string) => getUrlWithBackToParam(`referral/${contentType}`, backTo), }, PROCESS_MONEY_REQUEST_HOLD: 'hold-request-educational', + + TRAVEL_MY_TRIPS: 'travel', } as const; /** diff --git a/src/SCREENS.ts b/src/SCREENS.ts index ff3dbfd7f901..234a24476d2a 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -22,6 +22,9 @@ const SCREENS = { VALIDATE_LOGIN: 'ValidateLogin', UNLINK_LOGIN: 'UnlinkLogin', SETTINGS_CENTRAL_PANE: 'SettingsCentralPane', + TRAVEL: { + MY_TRIPS: 'Travel_MyTrips', + }, SETTINGS: { ROOT: 'Settings_Root', SHARE_CODE: 'Settings_Share_Code', diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx index 1e5d3639a32f..ab20497a3c73 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx @@ -4,6 +4,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import ReportScreenWrapper from '@libs/Navigation/AppNavigator/ReportScreenWrapper'; import getCurrentUrl from '@libs/Navigation/currentUrl'; import type {CentralPaneNavigatorParamList} from '@navigation/types'; +import MyTripsPage from '@pages/Travel/MyTripsPage'; import SCREENS from '@src/SCREENS'; const Stack = createStackNavigator(); @@ -43,7 +44,10 @@ function BaseCentralPaneNavigator() { initialParams={{openOnAdminRoom: openOnAdminRoom === 'true' || undefined}} component={ReportScreenWrapper} /> - + {Object.entries(workspaceSettingsScreens).map(([screenName, componentGetter]) => ( - interceptAnonymousUser(() => - activeWorkspaceID ? Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(activeWorkspaceID)) : Navigation.navigate(ROUTES.ALL_SETTINGS), - ) + onPress={ + () => Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS) + // interceptAnonymousUser(() => + // activeWorkspaceID ? Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(activeWorkspaceID)) : Navigation.navigate(ROUTES.ALL_SETTINGS), + // ) } role={CONST.ROLE.BUTTON} accessibilityLabel={translate('common.settings')} diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 276829e8c691..3515f17f3867 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -67,6 +67,8 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.CATEGORIES]: { path: ROUTES.WORKSPACE_CATEGORIES.route, }, + + [SCREENS.TRAVEL.MY_TRIPS]: ROUTES.TRAVEL_MY_TRIPS, }, }, [SCREENS.NOT_FOUND]: '*', diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index a1e558869ebe..03d6bbeb5cca 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -90,6 +90,7 @@ type CentralPaneNavigatorParamList = { [SCREENS.WORKSPACE.CATEGORIES]: { policyID: string; }; + [SCREENS.TRAVEL.MY_TRIPS]: undefined; }; type WorkspaceSwitcherNavigatorParamList = { diff --git a/src/pages/Travel/MyTripsPage.tsx b/src/pages/Travel/MyTripsPage.tsx new file mode 100644 index 000000000000..1983332c4e10 --- /dev/null +++ b/src/pages/Travel/MyTripsPage.tsx @@ -0,0 +1,20 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import {View} from 'react-native'; +import Text from '@components/Text'; +import type {CentralPaneNavigatorParamList} from '@navigation/types'; +import type SCREENS from '@src/SCREENS'; + +type MyTripsPageProps = StackScreenProps; + +function MyTripsPage({route}: MyTripsPageProps) { + return ( + + MyTripsPage + + ); +} + +MyTripsPage.displayName = 'MyTripsPage'; + +export default MyTripsPage; From 3e95765db62e01513f27bfaf32161a4dd6598ec4 Mon Sep 17 00:00:00 2001 From: smelaa Date: Mon, 4 Mar 2024 15:50:52 +0100 Subject: [PATCH 0028/1506] ManageTrips init --- src/languages/en.ts | 10 +++++++ src/languages/es.ts | 10 +++++++ src/pages/Travel/ManageTrips.tsx | 47 ++++++++++++++++++++++++++++++++ src/pages/Travel/MyTripsPage.tsx | 27 ++++++++++++++---- 4 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 src/pages/Travel/ManageTrips.tsx diff --git a/src/languages/en.ts b/src/languages/en.ts index bd57843e9245..2b483de57c93 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1717,6 +1717,16 @@ export default { session: { offlineMessageRetry: "Looks like you're offline. Please check your connection and try again.", }, + travel: { + header: 'My trips', + title: 'Book or manage your trips', + subtitle: 'Use Expensify Travel to get the best travel offers and manage all your business expenses in a single place.', + bookOrManage: 'Book or manage', + features: { + saveMoney: 'Save money on your bookings', + alerts: 'Get real time alerts if your travel plans change', + }, + }, workspace: { common: { card: 'Cards', diff --git a/src/languages/es.ts b/src/languages/es.ts index 83ed2ca1c89c..d0ca18d0aead 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1741,6 +1741,16 @@ export default { session: { offlineMessageRetry: 'Parece que estás desconectado. Por favor, comprueba tu conexión e inténtalo de nuevo.', }, + travel: { + header: 'Mis viajes', + title: 'Reserva o gestiona tus viajes', + subtitle: 'Utiliza Expensify Travel para obtener las mejores ofertas de viaje y gestionar todos tus gastos de negocio en un solo lugar.', + bookOrManage: 'Reservar o gestionar', + features: { + saveMoney: 'Ahorra dinero en tus reservas', + alerts: 'Recibe alertas en tiempo real si cambian tus planes de viaje', + }, + }, workspace: { common: { card: 'Tarjetas', diff --git a/src/pages/Travel/ManageTrips.tsx b/src/pages/Travel/ManageTrips.tsx new file mode 100644 index 000000000000..3cd09c8e98d2 --- /dev/null +++ b/src/pages/Travel/ManageTrips.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import {ScrollView, View} from 'react-native'; +import type {FeatureListItem} from '@components/FeatureList'; +import FeatureList from '@components/FeatureList'; +import * as Illustrations from '@components/Icon/Illustrations'; +import LottieAnimations from '@components/LottieAnimations'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import App from '@src/App'; + +const tripsFeatures: FeatureListItem[] = [ + { + icon: Illustrations.MoneyReceipts, + translationKey: 'travel.features.saveMoney', + }, + { + icon: Illustrations.CreditCardsNew, + translationKey: 'travel.features.alerts', + }, +]; + +function ManageTrips() { + const styles = useThemeStyles(); + const {isSmallScreenWidth} = useWindowDimensions(); + const {translate} = useLocalize(); + + return ( + + + console.log('pressed')} + illustration={LottieAnimations.SaveTheWorld} + /> + + + ); +} + +ManageTrips.displayName = 'ManageTrips'; + +export default ManageTrips; diff --git a/src/pages/Travel/MyTripsPage.tsx b/src/pages/Travel/MyTripsPage.tsx index 1983332c4e10..ae9e604cc177 100644 --- a/src/pages/Travel/MyTripsPage.tsx +++ b/src/pages/Travel/MyTripsPage.tsx @@ -1,17 +1,34 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; -import {View} from 'react-native'; -import Text from '@components/Text'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Illustrations from '@components/Icon/Illustrations'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useLocalize from '@hooks/useLocalize'; +import Navigation from '@libs/Navigation/Navigation'; import type {CentralPaneNavigatorParamList} from '@navigation/types'; import type SCREENS from '@src/SCREENS'; +import ManageTrips from './ManageTrips'; type MyTripsPageProps = StackScreenProps; function MyTripsPage({route}: MyTripsPageProps) { + const {translate} = useLocalize(); return ( - - MyTripsPage - + + Navigation.goBack()} + /> + + ); } From 011d2273613ae5e72108af56212456267e2fef3a Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 4 Mar 2024 15:55:15 +0100 Subject: [PATCH 0029/1506] Add TravelMenu page --- src/ROUTES.ts | 2 + src/SCREENS.ts | 1 + src/languages/en.ts | 3 + src/languages/es.ts | 3 + .../Navigators/BottomTabNavigator.tsx | 6 ++ .../BottomTabBar.tsx | 29 +++++-- .../TAB_TO_CENTRAL_PANE_MAPPING.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 1 + src/libs/Navigation/types.ts | 1 + src/pages/Travel/TravelMenu.tsx | 86 +++++++++++++++++++ 10 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 src/pages/Travel/TravelMenu.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index f7c455e923a6..4496d102d037 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -555,6 +555,8 @@ const ROUTES = { PROCESS_MONEY_REQUEST_HOLD: 'hold-request-educational', TRAVEL_MY_TRIPS: 'travel', + + TRAVEL_HOME: 'travel-home', } as const; /** diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 234a24476d2a..b08f8efabf45 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -23,6 +23,7 @@ const SCREENS = { UNLINK_LOGIN: 'UnlinkLogin', SETTINGS_CENTRAL_PANE: 'SettingsCentralPane', TRAVEL: { + HOME: 'Travel_Home', MY_TRIPS: 'Travel_MyTrips', }, SETTINGS: { diff --git a/src/languages/en.ts b/src/languages/en.ts index bd57843e9245..1a7c08371f27 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2427,4 +2427,7 @@ export default { offline: "You appear to be offline. Unfortunately, Expensify Classic doesn't work offline, but New Expensify does. If you prefer to use Expensify Classic, try again when you have an internet connection.", }, + travel: { + myTrips: 'My trips', + }, } satisfies TranslationBase; diff --git a/src/languages/es.ts b/src/languages/es.ts index 83ed2ca1c89c..ff6cc2b03e9c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2919,4 +2919,7 @@ export default { offline: 'Parece que estás desconectado. Desafortunadamente, Expensify Classic no funciona sin conexión, pero New Expensify sí. Si prefieres utilizar Expensify Classic, inténtalo de nuevo cuando tengas conexión a internet.', }, + travel: { + myTrips: 'Mis viajes', + }, } satisfies EnglishTranslation; diff --git a/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx index ce03a8d5bcba..9ce1b8425f51 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx @@ -11,6 +11,8 @@ import ActiveRouteContext from './ActiveRouteContext'; const loadWorkspaceInitialPage = () => require('../../../../pages/workspace/WorkspaceInitialPage').default as React.ComponentType; +const loadTravelMenuPage = () => require('../../../../pages/Travel/TravelMenu').default as React.ComponentType; + const Tab = createCustomBottomTabNavigator(); const screenOptions: StackNavigationOptions = { @@ -35,6 +37,10 @@ function BottomTabNavigator() { name={SCREENS.WORKSPACE.INITIAL} getComponent={loadWorkspaceInitialPage} /> + ); diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx index 39435fb6287d..6e2595c515eb 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx @@ -91,14 +91,30 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps - + + Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS)} + role={CONST.ROLE.BUTTON} + accessibilityLabel={translate('workspace.common.travel')} + wrapperStyle={styles.flexGrow1} + style={styles.bottomTabBarItem} + > + + + + + Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS) - // interceptAnonymousUser(() => - // activeWorkspaceID ? Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(activeWorkspaceID)) : Navigation.navigate(ROUTES.ALL_SETTINGS), - // ) + onPress={() => + interceptAnonymousUser(() => + activeWorkspaceID ? Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(activeWorkspaceID)) : Navigation.navigate(ROUTES.ALL_SETTINGS), + ) } role={CONST.ROLE.BUTTON} accessibilityLabel={translate('common.settings')} @@ -116,6 +132,7 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps + ); } diff --git a/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts b/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts index f4316009b70b..94945fc2aa54 100755 --- a/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts @@ -15,6 +15,7 @@ const TAB_TO_CENTRAL_PANE_MAPPING: Record = { SCREENS.WORKSPACE.MEMBERS, SCREENS.WORKSPACE.CATEGORIES, ], + [SCREENS.TRAVEL.HOME]: [SCREENS.TRAVEL.MY_TRIPS], }; const generateCentralPaneToTabMapping = (): Record => { diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 3515f17f3867..babe4862bfeb 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -34,6 +34,7 @@ const config: LinkingOptions['config'] = { path: ROUTES.WORKSPACE_INITIAL.route, exact: true, }, + [SCREENS.TRAVEL.HOME]: ROUTES.TRAVEL_HOME, }, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 03d6bbeb5cca..daaf867f16c2 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -514,6 +514,7 @@ type BottomTabNavigatorParamList = { [SCREENS.HOME]: undefined; [SCREENS.ALL_SETTINGS]: undefined; [SCREENS.WORKSPACE.INITIAL]: undefined; + [SCREENS.TRAVEL.HOME]: undefined; }; type PublicScreensParamList = { diff --git a/src/pages/Travel/TravelMenu.tsx b/src/pages/Travel/TravelMenu.tsx new file mode 100644 index 000000000000..19a5a61991b4 --- /dev/null +++ b/src/pages/Travel/TravelMenu.tsx @@ -0,0 +1,86 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useMemo} from 'react'; +import {ScrollView} from 'react-native'; +import Breadcrumbs from '@components/Breadcrumbs'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MenuItemList from '@components/MenuItemList'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWaitForNavigation from '@hooks/useWaitForNavigation'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import Navigation from '@libs/Navigation/Navigation'; +import type {BottomTabNavigatorParamList} from '@navigation/types'; +import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type TravelMenuProps = StackScreenProps; + +function TravelMenu({route}: TravelMenuProps) { + const styles = useThemeStyles(); + const waitForNavigate = useWaitForNavigation(); + const {translate} = useLocalize(); + const {isSmallScreenWidth} = useWindowDimensions(); + + /** + * Retuns a list of menu items data for Travel menu + * @returns {Object} object with translationKey, style and items + */ + const menuItems = useMemo(() => { + const baseMenuItems = [ + { + translationKey: 'travel.myTrips', + icon: Expensicons.Luggage, + action: () => { + waitForNavigate(() => { + Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS); + })(); + }, + focused: !isSmallScreenWidth, + }, + ]; + return baseMenuItems.map((item) => ({ + key: item.translationKey, + title: translate(item.translationKey as TranslationPaths), + icon: item.icon, + onPress: item.action, + wrapperStyle: styles.sectionMenuItem, + isPaneMenu: true, + focused: item.focused, + hoverAndPressStyle: styles.hoveredComponentBG, + })); + }, [isSmallScreenWidth, styles.hoveredComponentBG, styles.sectionMenuItem, translate, waitForNavigate]); + + return ( + + + + + + + ); +} + +TravelMenu.displayName = 'TravelMenu'; + +export default TravelMenu; From e0673e0baf75e6610c39c234cd758e8958b6157c Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 5 Mar 2024 14:50:54 +0500 Subject: [PATCH 0030/1506] refactor: rename to useReportIDs --- src/App.tsx | 4 +- ...edReportListItems.tsx => useReportIDs.tsx} | 66 +++++++++++-------- src/pages/home/sidebar/SidebarLinksData.js | 4 +- tests/utils/LHNTestUtils.tsx | 6 +- 4 files changed, 46 insertions(+), 34 deletions(-) rename src/hooks/{useOrderedReportListItems.tsx => useReportIDs.tsx} (72%) diff --git a/src/App.tsx b/src/App.tsx index 3a294757e149..6cfc2587074b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,7 +29,7 @@ import {KeyboardStateProvider} from './components/withKeyboardState'; import {WindowDimensionsProvider} from './components/withWindowDimensions'; import Expensify from './Expensify'; import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop'; -import {OrderedReportListItemsContextProvider} from './hooks/useOrderedReportListItems'; +import {ReportIDsContextProvider} from './hooks/useReportIDs'; import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; import InitialUrlContext from './libs/InitialUrlContext'; import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext'; @@ -76,7 +76,7 @@ function App({url}: AppProps) { CustomStatusBarAndBackgroundContextProvider, ActiveElementRoleProvider, ActiveWorkspaceContextProvider, - OrderedReportListItemsContextProvider, + ReportIDsContextProvider, PlaybackContextProvider, VolumeContextProvider, VideoPopoverMenuContextProvider, diff --git a/src/hooks/useOrderedReportListItems.tsx b/src/hooks/useReportIDs.tsx similarity index 72% rename from src/hooks/useOrderedReportListItems.tsx rename to src/hooks/useReportIDs.tsx index 4580df82ed34..651e21f08b24 100644 --- a/src/hooks/useOrderedReportListItems.tsx +++ b/src/hooks/useReportIDs.tsx @@ -1,7 +1,7 @@ +import _ from 'lodash'; import React, {createContext, useCallback, useContext, useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import _ from 'underscore'; import {getCurrentUserAccountID} from '@libs/actions/Report'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; @@ -9,6 +9,7 @@ import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Beta, Policy, PolicyMembers, PriorityMode, Report, ReportActions, Transaction, TransactionViolation} from '@src/types/onyx'; +import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import useActiveWorkspace from './useActiveWorkspace'; import useCurrentReportID from './useCurrentReportID'; import usePermissions from './usePermissions'; @@ -24,14 +25,22 @@ type OnyxProps = { allTransactions: OnyxCollection; }; -type WithOrderedReportListItemsContextProviderProps = OnyxProps & { +type WithReportIDsContextProviderProps = OnyxProps & { children: React.ReactNode; currentReportIDForTests?: string; }; -const OrderedReportListItemsContext = createContext({}); +type ReportIDsContextValue = { + orderedReportIDs: string[]; + reportIDsWithErrors: Record; +}; + +const ReportIDsContext = createContext({ + orderedReportIDs: [], + reportIDsWithErrors: {}, +}); -function WithOrderedReportListItemsContextProvider({ +function WithReportIDsContextProvider({ children, chatReports, betas, @@ -51,7 +60,7 @@ function WithOrderedReportListItemsContextProvider({ * only in testing environment. */ currentReportIDForTests, -}: WithOrderedReportListItemsContextProviderProps) { +}: WithReportIDsContextProviderProps) { const currentReportIDValue = useCurrentReportID(); const derivedCurrentReportID = currentReportIDForTests ?? currentReportIDValue?.currentReportID; const {activeWorkspaceID} = useActiveWorkspace(); @@ -59,22 +68,25 @@ function WithOrderedReportListItemsContextProvider({ const policyMemberAccountIDs = useMemo(() => getPolicyMembersByIdWithoutCurrentUser(policyMembers, activeWorkspaceID, getCurrentUserAccountID()), [activeWorkspaceID, policyMembers]); - const chatReportsKeys = useMemo(() => _.keys(chatReports), [chatReports]); - const reportIDsWithErrors = useMemo(() => { - return _.reduce( - chatReportsKeys, - (errorsMap, reportKey) => { - const report = chatReports && chatReports[reportKey]; - const allReportsActions = allReportActions && allReportActions[reportKey.replace(ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.COLLECTION.REPORT_ACTIONS)]; - const errors = OptionsListUtils.getAllReportErrors(report, allReportsActions, allTransactions) || {}; - if (_.size(errors) === 0) { - return errorsMap; - } - return {...errorsMap, [reportKey.replace(ONYXKEYS.COLLECTION.REPORT, '')]: errors}; - }, - {}, - ); - }, [chatReportsKeys, allReportActions, allTransactions, chatReports]); + const chatReportsKeys = useMemo(() => Object.keys(chatReports ?? {}), [chatReports]); + // eslint-disable-next-line you-dont-need-lodash-underscore/reduce + const reportIDsWithErrors = useMemo( + () => + _.reduce( + chatReportsKeys, + (errorsMap, reportKey) => { + const report = chatReports?.[reportKey] ?? null; + const allReportsActions = allReportActions?.[reportKey.replace(ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.COLLECTION.REPORT_ACTIONS)] ?? null; + const errors = OptionsListUtils.getAllReportErrors(report, allReportsActions, allTransactions) || {}; + if (Object.values(errors).length === 0) { + return errorsMap; + } + return {...errorsMap, [reportKey.replace(ONYXKEYS.COLLECTION.REPORT, '')]: errors}; + }, + {}, + ), + [chatReportsKeys, allReportActions, allTransactions, chatReports], + ); const getOrderedReportIDs = useCallback( (currentReportID?: string) => @@ -116,10 +128,10 @@ function WithOrderedReportListItemsContextProvider({ [orderedReportIDsWithCurrentReport, reportIDsWithErrors], ); - return {children}; + return {children}; } -const OrderedReportListItemsContextProvider = withOnyx({ +const ReportIDsContextProvider = withOnyx({ chatReports: { key: ONYXKEYS.COLLECTION.REPORT, initialValue: {}, @@ -151,10 +163,10 @@ const OrderedReportListItemsContextProvider = withOnyx { if (deepEqual(orderedReportIDsRef.current, orderedReportIDs)) { diff --git a/tests/utils/LHNTestUtils.tsx b/tests/utils/LHNTestUtils.tsx index c8c3f145d951..04344ba71184 100644 --- a/tests/utils/LHNTestUtils.tsx +++ b/tests/utils/LHNTestUtils.tsx @@ -10,7 +10,7 @@ import {LocaleContextProvider} from '@components/LocaleContextProvider'; import OnyxProvider from '@components/OnyxProvider'; import {CurrentReportIDContextProvider} from '@components/withCurrentReportID'; import {EnvironmentProvider} from '@components/withEnvironment'; -import {OrderedReportListItemsContextProvider} from '@hooks/useOrderedReportListItems'; +import {ReportIDsContextProvider} from '@hooks/useReportIDs'; import DateUtils from '@libs/DateUtils'; import ReportActionItemSingle from '@pages/home/report/ReportActionItemSingle'; import SidebarLinksData from '@pages/home/sidebar/SidebarLinksData'; @@ -291,7 +291,7 @@ function MockedSidebarLinks({currentReportID = ''}: MockedSidebarLinksProps) { * So this is a work around to have currentReportID available * only in testing environment. * */} - + {}} @@ -304,7 +304,7 @@ function MockedSidebarLinks({currentReportID = ''}: MockedSidebarLinksProps) { isSmallScreenWidth={false} currentReportID={currentReportID} /> - + ); } From 30ca3030ee04390772e1a03bc69305fbf4160600 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 5 Mar 2024 14:51:43 +0500 Subject: [PATCH 0031/1506] refactor: don't set currentReportID if it's on workspaces screen --- src/components/withCurrentReportID.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/withCurrentReportID.tsx b/src/components/withCurrentReportID.tsx index 0d052f759f81..54cdc84f127a 100644 --- a/src/components/withCurrentReportID.tsx +++ b/src/components/withCurrentReportID.tsx @@ -4,6 +4,7 @@ import type {ComponentType, ForwardedRef, RefAttributes} from 'react'; import React, {createContext, forwardRef, useCallback, useMemo, useState} from 'react'; import getComponentDisplayName from '@libs/getComponentDisplayName'; import Navigation from '@libs/Navigation/Navigation'; +import SCREENS from '@src/SCREENS'; type CurrentReportIDContextValue = { updateCurrentReportID: (state: NavigationState) => void; @@ -40,14 +41,17 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro const updateCurrentReportID = useCallback( (state: NavigationState) => { const reportID = Navigation.getTopmostReportId(state) ?? ''; + /** * This is to make sure we don't set the undefined as reportID when * switching between chat list and settings->workspaces tab. - * and doing so avoid unnecessary re-render of `useOrderedReportListItems`. + * and doing so avoid unnecessary re-render of `useReportIDs`. */ - if (reportID !== undefined) { - setCurrentReportID(reportID); + const params = state.routes[state.index].params; + if (params && 'screen' in params && params.screen === SCREENS.SETTINGS.WORKSPACES) { + return; } + setCurrentReportID(reportID); }, [setCurrentReportID], ); From 121b5378606b353ecc014df082c140dfe5737d4f Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 5 Mar 2024 14:51:53 +0500 Subject: [PATCH 0032/1506] refactor: remove dead code --- tests/perf-test/SidebarUtils.perf-test.ts | 8 +------ tests/utils/collections/createCollection.ts | 23 --------------------- 2 files changed, 1 insertion(+), 30 deletions(-) diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index 8672d7c0cafe..2b2bdbc6b57a 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -10,7 +10,7 @@ import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; import type ReportAction from '@src/types/onyx/ReportAction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import createCollection, {createNestedCollection} from '../utils/collections/createCollection'; +import createCollection from '../utils/collections/createCollection'; import createPersonalDetails from '../utils/collections/personalDetails'; import createRandomPolicy from '../utils/collections/policies'; import createRandomReportAction from '../utils/collections/reportActions'; @@ -29,12 +29,6 @@ const reportActions = createCollection( (index) => createRandomReportAction(index), ); -// const allReportActions = createNestedCollection( -// (item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, -// (item) => `${item.reportActionID}`, -// (index) => createRandomReportAction(index), -// ); - const personalDetails = createCollection( (item) => item.accountID, (index) => createPersonalDetails(index), diff --git a/tests/utils/collections/createCollection.ts b/tests/utils/collections/createCollection.ts index 4a2a1fa4eb78..848ef8f81f47 100644 --- a/tests/utils/collections/createCollection.ts +++ b/tests/utils/collections/createCollection.ts @@ -9,26 +9,3 @@ export default function createCollection(createKey: (item: T, index: number) return map; } - -function createNestedCollection( - createParentKey: (item: T, index: number) => string | number, - createKey: (item: T, index: number) => string | number, - createItem: (index: number) => T, - length = 500, -): Record> { - const map: Record> = {}; - - for (let i = 0; i < length; i++) { - const item = createItem(i); - const itemKey = createKey(item, i); - const itemParentKey = createParentKey(item, i); - map[itemParentKey] = { - ...map[itemParentKey], - [itemKey]: item, - }; - } - - return map; -} - -export {createNestedCollection}; From 9e41603f9da5a3eb38873a8ab24476c2eadacdaf Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 5 Mar 2024 15:13:57 +0500 Subject: [PATCH 0033/1506] fix: linting --- src/hooks/useReportIDs.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/hooks/useReportIDs.tsx b/src/hooks/useReportIDs.tsx index 651e21f08b24..2e7c63182646 100644 --- a/src/hooks/useReportIDs.tsx +++ b/src/hooks/useReportIDs.tsx @@ -67,11 +67,10 @@ function WithReportIDsContextProvider({ const {canUseViolations} = usePermissions(); const policyMemberAccountIDs = useMemo(() => getPolicyMembersByIdWithoutCurrentUser(policyMembers, activeWorkspaceID, getCurrentUserAccountID()), [activeWorkspaceID, policyMembers]); - const chatReportsKeys = useMemo(() => Object.keys(chatReports ?? {}), [chatReports]); - // eslint-disable-next-line you-dont-need-lodash-underscore/reduce const reportIDsWithErrors = useMemo( () => + // eslint-disable-next-line you-dont-need-lodash-underscore/reduce _.reduce( chatReportsKeys, (errorsMap, reportKey) => { From 4a3284161b02ea616cc40a1bc741221672e6e777 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 5 Mar 2024 15:17:33 +0500 Subject: [PATCH 0034/1506] refactor: remove irrelevant changes --- src/components/LHNOptionsList/LHNOptionsList.tsx | 4 ++-- src/components/LHNOptionsList/types.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 31af022a12aa..be8ce677b641 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -20,15 +20,15 @@ function LHNOptionsList({ contentContainerStyles, data, onSelectRow, - shouldDisableFocusOptions = false, - currentReportID = '', optionMode, + shouldDisableFocusOptions = false, reports = {}, reportActions = {}, policy = {}, preferredLocale = CONST.LOCALES.DEFAULT, personalDetails = {}, transactions = {}, + currentReportID = '', draftComments = {}, transactionViolations = {}, onFirstItemRendered = () => {}, diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index 0d1bda775255..c122ab018392 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -139,7 +139,7 @@ type OptionRowLHNProps = { style?: StyleProp; /** The item that should be rendered */ - optionItem: OptionData | undefined; + optionItem?: OptionData; onLayout?: (event: LayoutChangeEvent) => void; }; From 0bf79160068973dad3dd71df6098e8eb364275da Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 5 Mar 2024 12:38:43 +0100 Subject: [PATCH 0035/1506] Add suitcase icon to Expensicons --- assets/images/suitcase.svg | 3 +++ src/components/Icon/Expensicons.ts | 2 ++ .../createCustomBottomTabNavigator/BottomTabBar.tsx | 2 +- src/pages/Travel/TravelMenu.tsx | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 assets/images/suitcase.svg diff --git a/assets/images/suitcase.svg b/assets/images/suitcase.svg new file mode 100644 index 000000000000..97036db6b5ac --- /dev/null +++ b/assets/images/suitcase.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 6c6c1b86eee1..4eaeff4b15ac 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -134,6 +134,7 @@ import Podcast from '@assets/images/social-podcast.svg'; import Twitter from '@assets/images/social-twitter.svg'; import Youtube from '@assets/images/social-youtube.svg'; import Stopwatch from '@assets/images/stopwatch.svg'; +import Suitcase from '@assets/images/suitcase.svg'; import Sync from '@assets/images/sync.svg'; import Task from '@assets/images/task.svg'; import ThreeDots from '@assets/images/three-dots.svg'; @@ -280,6 +281,7 @@ export { Send, Shield, Stopwatch, + Suitcase, Sync, Task, ThumbsUp, diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx index 6e2595c515eb..f3911f84ce05 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx @@ -101,7 +101,7 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps > { waitForNavigate(() => { Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS); From d2cd60cd1921bb5851a6158499e5054d13583dee Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 5 Mar 2024 13:44:07 +0100 Subject: [PATCH 0036/1506] Add simple illustrations for MyTripsPage --- .../simple-illustration__alert.svg | 15 ++++++ .../simple-illustration__piggybank.svg | 50 +++++++++++++++++++ src/components/Icon/Illustrations.ts | 4 ++ src/pages/Travel/ManageTrips.tsx | 7 +-- src/pages/Travel/MyTripsPage.tsx | 7 ++- 5 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 assets/images/simple-illustrations/simple-illustration__alert.svg create mode 100644 assets/images/simple-illustrations/simple-illustration__piggybank.svg diff --git a/assets/images/simple-illustrations/simple-illustration__alert.svg b/assets/images/simple-illustrations/simple-illustration__alert.svg new file mode 100644 index 000000000000..55429cf39b8f --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__alert.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__piggybank.svg b/assets/images/simple-illustrations/simple-illustration__piggybank.svg new file mode 100644 index 000000000000..c89eb3b342ef --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__piggybank.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index f8c048ebc4c0..728400f95bb3 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -29,6 +29,7 @@ import TadaYellow from '@assets/images/product-illustrations/tada--yellow.svg'; import TeleScope from '@assets/images/product-illustrations/telescope.svg'; import ThreeLeggedLaptopWoman from '@assets/images/product-illustrations/three_legged_laptop_woman.svg'; import ToddBehindCloud from '@assets/images/product-illustrations/todd-behind-cloud.svg'; +import Alert from '@assets/images/simple-illustrations/simple-illustration__alert.svg'; import Approval from '@assets/images/simple-illustrations/simple-illustration__approval.svg'; import BankArrow from '@assets/images/simple-illustrations/simple-illustration__bank-arrow.svg'; import BigRocket from '@assets/images/simple-illustrations/simple-illustration__bigrocket.svg'; @@ -58,6 +59,7 @@ import MoneyIntoWallet from '@assets/images/simple-illustrations/simple-illustra import MoneyWings from '@assets/images/simple-illustrations/simple-illustration__moneywings.svg'; import OpenSafe from '@assets/images/simple-illustrations/simple-illustration__opensafe.svg'; import PalmTree from '@assets/images/simple-illustrations/simple-illustration__palmtree.svg'; +import PiggyBank from '@assets/images/simple-illustrations/simple-illustration__piggybank.svg'; import Profile from '@assets/images/simple-illustrations/simple-illustration__profile.svg'; import QRCode from '@assets/images/simple-illustrations/simple-illustration__qr-code.svg'; import ReceiptEnvelope from '@assets/images/simple-illustrations/simple-illustration__receipt-envelope.svg'; @@ -146,4 +148,6 @@ export { Workflows, ThreeLeggedLaptopWoman, House, + PiggyBank, + Alert, }; diff --git a/src/pages/Travel/ManageTrips.tsx b/src/pages/Travel/ManageTrips.tsx index 3cd09c8e98d2..fd608374b4f4 100644 --- a/src/pages/Travel/ManageTrips.tsx +++ b/src/pages/Travel/ManageTrips.tsx @@ -7,15 +7,15 @@ import LottieAnimations from '@components/LottieAnimations'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import App from '@src/App'; +import colors from '@styles/theme/colors'; const tripsFeatures: FeatureListItem[] = [ { - icon: Illustrations.MoneyReceipts, + icon: Illustrations.PiggyBank, translationKey: 'travel.features.saveMoney', }, { - icon: Illustrations.CreditCardsNew, + icon: Illustrations.Alert, translationKey: 'travel.features.alerts', }, ]; @@ -36,6 +36,7 @@ function ManageTrips() { ctaAccessibilityLabel={translate('travel.bookOrManage')} onCtaPress={() => console.log('pressed')} illustration={LottieAnimations.SaveTheWorld} + illustrationBackgroundColor={colors.blue600} /> diff --git a/src/pages/Travel/MyTripsPage.tsx b/src/pages/Travel/MyTripsPage.tsx index ae9e604cc177..a89a0d2a60da 100644 --- a/src/pages/Travel/MyTripsPage.tsx +++ b/src/pages/Travel/MyTripsPage.tsx @@ -4,6 +4,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import Navigation from '@libs/Navigation/Navigation'; import type {CentralPaneNavigatorParamList} from '@navigation/types'; import type SCREENS from '@src/SCREENS'; @@ -13,6 +14,8 @@ type MyTripsPageProps = StackScreenProps Navigation.goBack()} /> From a467a7403fe662f23e9e31db16b6e50e88907a61 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 5 Mar 2024 14:53:47 +0100 Subject: [PATCH 0037/1506] Remove TravelMenuProps --- src/pages/Travel/TravelMenu.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/pages/Travel/TravelMenu.tsx b/src/pages/Travel/TravelMenu.tsx index 5c0830549f56..7edb2d37da24 100644 --- a/src/pages/Travel/TravelMenu.tsx +++ b/src/pages/Travel/TravelMenu.tsx @@ -1,4 +1,3 @@ -import type {StackScreenProps} from '@react-navigation/stack'; import React, {useMemo} from 'react'; import {ScrollView} from 'react-native'; import Breadcrumbs from '@components/Breadcrumbs'; @@ -10,15 +9,13 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; import useWindowDimensions from '@hooks/useWindowDimensions'; import Navigation from '@libs/Navigation/Navigation'; -import type {BottomTabNavigatorParamList} from '@navigation/types'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ROUTES from '@src/ROUTES'; -import type SCREENS from '@src/SCREENS'; -type TravelMenuProps = StackScreenProps; +// type TravelMenuProps = StackScreenProps; -function TravelMenu({route}: TravelMenuProps) { +function TravelMenu() { const styles = useThemeStyles(); const waitForNavigate = useWaitForNavigation(); const {translate} = useLocalize(); From 4cb11200f50d5fc35664b6b3824d170f31cfa86e Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 5 Mar 2024 15:45:08 +0100 Subject: [PATCH 0038/1506] Add fixes to travel page --- src/languages/en.ts | 3 --- .../createCustomBottomTabNavigator/BottomTabBar.tsx | 2 +- src/pages/Travel/TravelMenu.tsx | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 34314e1e65d8..2b483de57c93 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2437,7 +2437,4 @@ export default { offline: "You appear to be offline. Unfortunately, Expensify Classic doesn't work offline, but New Expensify does. If you prefer to use Expensify Classic, try again when you have an internet connection.", }, - travel: { - myTrips: 'My trips', - }, } satisfies TranslationBase; diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx index f3911f84ce05..5eaf56eb7edd 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx @@ -93,7 +93,7 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS)} + onPress={() => Navigation.navigate(ROUTES.TRAVEL_HOME)} role={CONST.ROLE.BUTTON} accessibilityLabel={translate('workspace.common.travel')} wrapperStyle={styles.flexGrow1} diff --git a/src/pages/Travel/TravelMenu.tsx b/src/pages/Travel/TravelMenu.tsx index 7edb2d37da24..12fca5d0c587 100644 --- a/src/pages/Travel/TravelMenu.tsx +++ b/src/pages/Travel/TravelMenu.tsx @@ -28,7 +28,7 @@ function TravelMenu() { const menuItems = useMemo(() => { const baseMenuItems = [ { - translationKey: 'travel.myTrips', + translationKey: 'travel.header', icon: Expensicons.Suitcase, action: () => { waitForNavigate(() => { From 9467b3a7f068b071f380ae36a3e39aeeb607b1e1 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Wed, 6 Mar 2024 13:35:59 +0530 Subject: [PATCH 0039/1506] feat: Receipt Audit Feature / Note type violations. Signed-off-by: Krishna Gupta --- src/components/ReceiptAudit.tsx | 33 +++++++++++++++++++ .../ReportActionItem/MoneyRequestView.tsx | 4 +++ src/styles/index.ts | 13 ++++++++ 3 files changed, 50 insertions(+) create mode 100644 src/components/ReceiptAudit.tsx diff --git a/src/components/ReceiptAudit.tsx b/src/components/ReceiptAudit.tsx new file mode 100644 index 000000000000..af8c063668e1 --- /dev/null +++ b/src/components/ReceiptAudit.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import {View} from 'react-native'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Icon from './Icon'; +import * as Expensicons from './Icon/Expensicons'; +import Text from './Text'; + +export default function ReceiptAudit({notes}: {notes: string[]}) { + const styles = useThemeStyles(); + const theme = useTheme(); + + return ( + + + + 0 ? Expensicons.Receipt : Expensicons.Checkmark} + fill={theme.white} + /> + + {notes.length > 0 ? `Receipt Audit : ${notes.length} Issue(s) Found` : 'Receipt Verified : No issues Found'} + + + + + {/* // If notes is a array of strings, map through it & show notes. */} + {notes.length > 0 && notes.map((message) => {message})} + + ); +} diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 0bd18d8ee7ea..234aa4465d40 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -6,6 +6,7 @@ import ConfirmedRoute from '@components/ConfirmedRoute'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import ReceiptAudit from '@components/ReceiptAudit'; import ReceiptEmptyState from '@components/ReceiptEmptyState'; import SpacerView from '@components/SpacerView'; import Switch from '@components/Switch'; @@ -285,6 +286,9 @@ function MoneyRequestView({ } /> )} + + + {canUseViolations && } borderWidth: 1, }, + receiptAuditTitleContainer: { + flexDirection: 'row', + gap: 4, + padding: 4, + paddingHorizontal: 8, + height: variables.inputHeightSmall, + borderRadius: variables.componentBorderRadiusSmall, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + backgroundColor: theme.border, + }, + mapViewContainer: { ...flex.flex1, minHeight: 300, From 8c400c3fae12d28f95ded76a52806e92ec54f740 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Wed, 6 Mar 2024 14:57:49 +0530 Subject: [PATCH 0040/1506] added translations. Signed-off-by: Krishna Gupta --- src/components/ReceiptAudit.tsx | 8 +++++--- src/components/ReportActionItem/MoneyRequestView.tsx | 5 ++++- src/languages/en.ts | 4 ++++ src/languages/es.ts | 4 ++++ 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/components/ReceiptAudit.tsx b/src/components/ReceiptAudit.tsx index af8c063668e1..7f11e9b9a1e4 100644 --- a/src/components/ReceiptAudit.tsx +++ b/src/components/ReceiptAudit.tsx @@ -1,5 +1,6 @@ import React from 'react'; import {View} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Icon from './Icon'; @@ -9,7 +10,9 @@ import Text from './Text'; export default function ReceiptAudit({notes}: {notes: string[]}) { const styles = useThemeStyles(); const theme = useTheme(); + const {translate} = useLocalize(); + const issuesFoundText = notes.length > 0 ? translate('iou.receiptIssuesFound', notes.length) : translate('iou.receiptNoIssuesFound'); return ( @@ -20,10 +23,9 @@ export default function ReceiptAudit({notes}: {notes: string[]}) { src={notes.length > 0 ? Expensicons.Receipt : Expensicons.Checkmark} fill={theme.white} /> - - {notes.length > 0 ? `Receipt Audit : ${notes.length} Issue(s) Found` : 'Receipt Verified : No issues Found'} - + {notes.length > 0 ? translate('iou.receiptAudit') : translate('iou.receiptVerified')} + {issuesFoundText} {/* // If notes is a array of strings, map through it & show notes. */} diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 234aa4465d40..639739780a73 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -287,7 +287,10 @@ function MoneyRequestView({ /> )} - + {canUseViolations && } diff --git a/src/languages/en.ts b/src/languages/en.ts index 0a52cca62ef5..f073e79335ea 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -601,6 +601,10 @@ export default { posted: 'Posted', deleteReceipt: 'Delete receipt', routePending: 'Route pending...', + receiptAudit: 'Receipt Audit', + receiptVerified: 'Receipt Verified', + receiptNoIssuesFound: 'No issues Found', + receiptIssuesFound: (count: number) => `${count} Issue(s) Found`, receiptScanning: 'Scan in progress…', receiptMissingDetails: 'Receipt missing details', receiptStatusTitle: 'Scanning…', diff --git a/src/languages/es.ts b/src/languages/es.ts index 013255c1e11e..b908300f4e46 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -594,6 +594,10 @@ export default { posted: 'Contabilizado', deleteReceipt: 'Eliminar recibo', routePending: 'Ruta pendiente...', + receiptAudit: 'Auditoría de recibos', + receiptVerified: 'Recibo verificado', + receiptNoIssuesFound: 'No se encontraron problemas', + receiptIssuesFound: (count: number) => `Se encontró ${count} problema(s)`, receiptScanning: 'Escaneo en curso…', receiptMissingDetails: 'Recibo con campos vacíos', receiptStatusTitle: 'Escaneando…', From ec06f1646d3de6329f2ff5fb43b0652c48a7d6f1 Mon Sep 17 00:00:00 2001 From: smelaa Date: Wed, 6 Mar 2024 10:32:34 +0100 Subject: [PATCH 0041/1506] Add main illustration --- assets/animations/Plane.lottie | Bin 0 -> 26027 bytes src/components/LottieAnimations/index.tsx | 5 +++++ src/pages/Travel/ManageTrips.tsx | 3 ++- src/styles/index.ts | 5 +++++ 4 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 assets/animations/Plane.lottie diff --git a/assets/animations/Plane.lottie b/assets/animations/Plane.lottie new file mode 100644 index 0000000000000000000000000000000000000000..77a7c0f5dfa8a734b8e0b6c7cb021088ecf3674c GIT binary patch literal 26027 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 literal 0 HcmV?d00001 diff --git a/src/components/LottieAnimations/index.tsx b/src/components/LottieAnimations/index.tsx index 18cb9188d60c..598819e19361 100644 --- a/src/components/LottieAnimations/index.tsx +++ b/src/components/LottieAnimations/index.tsx @@ -67,6 +67,11 @@ const DotLottieAnimations = { w: 200, h: 120, }, + Plane: { + file: require('@assets/animations/Plane.lottie'), + w: 180, + h: 200, + }, } satisfies Record; export default DotLottieAnimations; diff --git a/src/pages/Travel/ManageTrips.tsx b/src/pages/Travel/ManageTrips.tsx index fd608374b4f4..30c2f1151492 100644 --- a/src/pages/Travel/ManageTrips.tsx +++ b/src/pages/Travel/ManageTrips.tsx @@ -35,7 +35,8 @@ function ManageTrips() { ctaText={translate('travel.bookOrManage')} ctaAccessibilityLabel={translate('travel.bookOrManage')} onCtaPress={() => console.log('pressed')} - illustration={LottieAnimations.SaveTheWorld} + illustration={LottieAnimations.Plane} + illustrationStyle={styles.travelIllustrationStyle} illustrationBackgroundColor={colors.blue600} /> diff --git a/src/styles/index.ts b/src/styles/index.ts index 405a05cfce78..d16e769f95b8 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -1769,6 +1769,11 @@ const styles = (theme: ThemeColors) => marginBottom: -20, }, + travelIllustrationStyle: { + marginTop: 20, + marginBottom: -20, + }, + overlayStyles: (current: OverlayStylesParams, isModalOnTheLeft: boolean) => ({ ...positioning.pFixed, From 96744d5c4aa2622075e9a2febbe7d2c357eb740d Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Wed, 6 Mar 2024 15:56:41 +0530 Subject: [PATCH 0042/1506] extract noticeViolations from transactionViolations. Signed-off-by: Krishna Gupta --- src/components/ReceiptAudit.tsx | 6 +++--- src/components/ReportActionItem/MoneyRequestView.tsx | 7 ++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/components/ReceiptAudit.tsx b/src/components/ReceiptAudit.tsx index 7f11e9b9a1e4..f756ecd5dc7e 100644 --- a/src/components/ReceiptAudit.tsx +++ b/src/components/ReceiptAudit.tsx @@ -7,14 +7,14 @@ import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import Text from './Text'; -export default function ReceiptAudit({notes}: {notes: string[]}) { +export default function ReceiptAudit({notes = []}: {notes?: string[]}) { const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); const issuesFoundText = notes.length > 0 ? translate('iou.receiptIssuesFound', notes.length) : translate('iou.receiptNoIssuesFound'); return ( - + {/* // If notes is a array of strings, map through it & show notes. */} - {notes.length > 0 && notes.map((message) => {message})} + {notes.length > 0 && notes.map((message) => {message})} ); } diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 639739780a73..c4f067e35e89 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -147,6 +147,7 @@ function MoneyRequestView({ const {getViolationsForField} = useViolations(transactionViolations ?? []); const hasViolations = useCallback((field: ViolationField): boolean => !!canUseViolations && getViolationsForField(field).length > 0, [canUseViolations, getViolationsForField]); + const noticeViolations = transactionViolations?.filter((violation) => violation.type === 'notice').map((v) => ViolationsUtils.getViolationTranslation(v, translate)); let amountDescription = `${translate('iou.amount')}`; @@ -286,11 +287,7 @@ function MoneyRequestView({ } /> )} - - + {noticeViolations?.length && } {canUseViolations && } From 952a4dac2dff9293d91ce53a58467042262bb531 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Thu, 7 Mar 2024 11:50:47 +0530 Subject: [PATCH 0043/1506] Update the dot separator sub-state for the request preview. Signed-off-by: Krishna Gupta --- .../MoneyRequestPreviewContent.tsx | 3 +++ .../ReportActionItem/MoneyRequestView.tsx | 2 +- src/libs/TransactionUtils.ts | 13 +++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 8577c9fa5f97..fb524c6b5cb2 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -87,6 +87,7 @@ function MoneyRequestPreviewContent({ const hasReceipt = TransactionUtils.hasReceipt(transaction); const isScanning = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction); const hasViolations = TransactionUtils.hasViolation(transaction?.transactionID ?? '', transactionViolations); + const hasNoteTypeViolations = TransactionUtils.hasNoteTypeViolation(transaction?.transactionID ?? '', transactionViolations); const hasFieldErrors = TransactionUtils.hasMissingSmartscanFields(transaction); const shouldShowRBR = hasViolations || hasFieldErrors; const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); @@ -159,6 +160,8 @@ function MoneyRequestPreviewContent({ const isTooLong = violations.filter((v) => v.type === 'violation').length > 1 || violationMessage.length > 15; message += ` • ${isTooLong ? translate('violations.reviewRequired') : violationMessage}`; } + } else if (hasNoteTypeViolations && transaction && !ReportUtils.isReportApproved(iouReport) && !ReportUtils.isSettled(iouReport?.reportID)) { + message += ` • ${translate('violations.reviewRequired')}`; } else if (ReportUtils.isPaidGroupPolicyExpenseReport(iouReport) && ReportUtils.isReportApproved(iouReport) && !ReportUtils.isSettled(iouReport?.reportID)) { message += ` • ${translate('iou.approved')}`; } else if (iouReport?.isWaitingOnBankAccount) { diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index c4f067e35e89..440f8afb73ee 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -147,7 +147,7 @@ function MoneyRequestView({ const {getViolationsForField} = useViolations(transactionViolations ?? []); const hasViolations = useCallback((field: ViolationField): boolean => !!canUseViolations && getViolationsForField(field).length > 0, [canUseViolations, getViolationsForField]); - const noticeViolations = transactionViolations?.filter((violation) => violation.type === 'notice').map((v) => ViolationsUtils.getViolationTranslation(v, translate)); + const noticeViolations = transactionViolations?.filter((violation) => violation.type === 'note').map((v) => ViolationsUtils.getViolationTranslation(v, translate)); let amountDescription = `${translate('iou.amount')}`; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 8a98fe0f2cdc..5dea541b2af2 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -593,6 +593,17 @@ function hasViolation(transactionID: string, transactionViolations: OnyxCollecti return Boolean(transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some((violation: TransactionViolation) => violation.type === 'violation')); } +/** + * Checks if any violations for the provided transaction are of type 'note' + */ +function hasNoteTypeViolation(transactionID: string, transactionViolations: OnyxCollection): boolean { + return Boolean(transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some((violation: TransactionViolation) => violation.type === 'note')); +} + +function getTransactionNoteViolations(transactionID: string, transactionViolations: OnyxCollection): TransactionViolation[] | null { + return transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.filter((violation: TransactionViolation) => violation.type === 'note') ?? null; +} + function getTransactionViolations(transactionID: string, transactionViolations: OnyxCollection): TransactionViolation[] | null { return transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID] ?? null; } @@ -638,6 +649,7 @@ export { getTagArrayFromName, getTagForDisplay, getTransactionViolations, + getTransactionNoteViolations, getLinkedTransaction, getAllReportTransactions, hasReceipt, @@ -663,6 +675,7 @@ export { waypointHasValidAddress, getRecentTransactions, hasViolation, + hasNoteTypeViolation, }; export type {TransactionChanges}; From 8438383448a735e0d5dd871e10ae0e27f85fd9f4 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Thu, 7 Mar 2024 11:51:50 +0530 Subject: [PATCH 0044/1506] Remove redundant code. Signed-off-by: Krishna Gupta --- src/libs/TransactionUtils.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 5dea541b2af2..bc2fcaa39290 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -600,10 +600,6 @@ function hasNoteTypeViolation(transactionID: string, transactionViolations: Onyx return Boolean(transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some((violation: TransactionViolation) => violation.type === 'note')); } -function getTransactionNoteViolations(transactionID: string, transactionViolations: OnyxCollection): TransactionViolation[] | null { - return transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.filter((violation: TransactionViolation) => violation.type === 'note') ?? null; -} - function getTransactionViolations(transactionID: string, transactionViolations: OnyxCollection): TransactionViolation[] | null { return transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID] ?? null; } @@ -649,7 +645,6 @@ export { getTagArrayFromName, getTagForDisplay, getTransactionViolations, - getTransactionNoteViolations, getLinkedTransaction, getAllReportTransactions, hasReceipt, From 7f975195bf133d3ef94d5b1123a53b5fd171a457 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Thu, 7 Mar 2024 11:56:22 +0530 Subject: [PATCH 0045/1506] minor fix. Signed-off-by: Krishna Gupta --- src/components/ReportActionItem/MoneyRequestView.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 440f8afb73ee..481bcb177569 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -147,7 +147,12 @@ function MoneyRequestView({ const {getViolationsForField} = useViolations(transactionViolations ?? []); const hasViolations = useCallback((field: ViolationField): boolean => !!canUseViolations && getViolationsForField(field).length > 0, [canUseViolations, getViolationsForField]); - const noticeViolations = transactionViolations?.filter((violation) => violation.type === 'note').map((v) => ViolationsUtils.getViolationTranslation(v, translate)); + const noticeViolations = [ + {name: 'missingComment', type: 'violation'}, + {name: 'modifiedDate', type: 'violation'}, + ] + ?.filter((violation) => violation.type === 'note') + .map((v) => ViolationsUtils.getViolationTranslation(v, translate)); let amountDescription = `${translate('iou.amount')}`; @@ -287,7 +292,7 @@ function MoneyRequestView({ } /> )} - {noticeViolations?.length && } + {canUseViolations && } From cebc960dcf6e17bc04f2c0ed7158c91e2b112a23 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Thu, 7 Mar 2024 12:20:13 +0530 Subject: [PATCH 0046/1506] minor fix. Signed-off-by: Krishna Gupta --- src/components/ReportActionItem/MoneyRequestView.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 481bcb177569..79639a62babe 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -147,12 +147,7 @@ function MoneyRequestView({ const {getViolationsForField} = useViolations(transactionViolations ?? []); const hasViolations = useCallback((field: ViolationField): boolean => !!canUseViolations && getViolationsForField(field).length > 0, [canUseViolations, getViolationsForField]); - const noticeViolations = [ - {name: 'missingComment', type: 'violation'}, - {name: 'modifiedDate', type: 'violation'}, - ] - ?.filter((violation) => violation.type === 'note') - .map((v) => ViolationsUtils.getViolationTranslation(v, translate)); + const noteTypeViolations = transactionViolations?.filter((violation) => violation.type === 'note').map((v) => ViolationsUtils.getViolationTranslation(v, translate)); let amountDescription = `${translate('iou.amount')}`; @@ -292,8 +287,7 @@ function MoneyRequestView({ } /> )} - - + {canUseViolations && } Date: Thu, 7 Mar 2024 15:25:49 +0530 Subject: [PATCH 0047/1506] hide ReceiptAudit when scan is in progress. Signed-off-by: Krishna Gupta --- src/components/ReportActionItem/MoneyRequestView.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 79639a62babe..415a2666512a 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -129,6 +129,7 @@ function MoneyRequestView({ const canEditDate = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DATE); const canEditReceipt = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.RECEIPT); const canEditDistance = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DISTANCE); + const isReceiptBeingScanned = TransactionUtils.hasReceipt(transaction) && !TransactionUtils.isReceiptBeingScanned(transaction); // A flag for verifying that the current report is a sub-report of a workspace chat // if the policy of the report is either Collect or Control, then this report must be tied to workspace chat @@ -287,7 +288,7 @@ function MoneyRequestView({ } /> )} - + {!isReceiptBeingScanned && canUseViolations && } {canUseViolations && } Date: Thu, 7 Mar 2024 14:04:36 +0100 Subject: [PATCH 0048/1506] Add beta visibility --- src/CONST.ts | 1 + src/libs/Permissions.ts | 5 +++++ src/pages/Travel/MyTripsPage.tsx | 21 +++++++++++++++++---- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 9ed2903941b6..fa33b2578a54 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -309,6 +309,7 @@ const CONST = { VIOLATIONS: 'violations', REPORT_FIELDS: 'reportFields', WORKFLOWS_DELAYED_SUBMISSION: 'workflowsDelayedSubmission', + SPOTNANA_TRAVEL: 'spotnanaTravel', }, BUTTON_STATES: { DEFAULT: 'default', diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index c9f386f5bd7a..b3d7c302234d 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -30,6 +30,10 @@ function canUseWorkflowsDelayedSubmission(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.WORKFLOWS_DELAYED_SUBMISSION) || canUseAllBetas(betas); } +function canSeeTravelPage(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.SPOTNANA_TRAVEL) || canUseAllBetas(betas); +} + /** * Link previews are temporarily disabled. */ @@ -45,4 +49,5 @@ export default { canUseViolations, canUseReportFields, canUseWorkflowsDelayedSubmission, + canSeeTravelPage, }; diff --git a/src/pages/Travel/MyTripsPage.tsx b/src/pages/Travel/MyTripsPage.tsx index a89a0d2a60da..e82a73dd5247 100644 --- a/src/pages/Travel/MyTripsPage.tsx +++ b/src/pages/Travel/MyTripsPage.tsx @@ -1,20 +1,29 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import useWindowDimensions from '@hooks/useWindowDimensions'; import Navigation from '@libs/Navigation/Navigation'; +import Permissions from '@libs/Permissions'; import type {CentralPaneNavigatorParamList} from '@navigation/types'; +import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; +import type {Beta} from '@src/types/onyx'; import ManageTrips from './ManageTrips'; -type MyTripsPageProps = StackScreenProps; +type MyTripsPageOnyxProps = {betas: OnyxEntry}; -function MyTripsPage({route}: MyTripsPageProps) { +type MyTripsPageProps = StackScreenProps & MyTripsPageOnyxProps; + +function MyTripsPage({betas}: MyTripsPageProps) { const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); + const canSeeTravelPage = Permissions.canSeeTravelPage(betas); return ( Navigation.goBack()} /> - + {canSeeTravelPage ? : } ); } MyTripsPage.displayName = 'MyTripsPage'; -export default MyTripsPage; +export default withOnyx({ + betas: { + key: ONYXKEYS.BETAS, + }, +})(MyTripsPage); From ff03721e9dfba525fa5f4329a97579372cac3d10 Mon Sep 17 00:00:00 2001 From: smelaa Date: Thu, 7 Mar 2024 14:10:54 +0100 Subject: [PATCH 0049/1506] Wrap ManageTrips in FullPageNotFoundView --- src/pages/Travel/MyTripsPage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/Travel/MyTripsPage.tsx b/src/pages/Travel/MyTripsPage.tsx index e82a73dd5247..ddef2f303318 100644 --- a/src/pages/Travel/MyTripsPage.tsx +++ b/src/pages/Travel/MyTripsPage.tsx @@ -39,7 +39,9 @@ function MyTripsPage({betas}: MyTripsPageProps) { shouldShowBackButton={isSmallScreenWidth} onBackButtonPress={() => Navigation.goBack()} /> - {canSeeTravelPage ? : } + + + ); } From 3db41da8dcaf188939e53f342650ff6f24250a24 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Fri, 8 Mar 2024 16:53:21 +0530 Subject: [PATCH 0050/1506] minor updates. Signed-off-by: Krishna Gupta --- src/components/ReceiptAudit.tsx | 2 -- src/components/ReportActionItem/MoneyRequestView.tsx | 7 ++++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/ReceiptAudit.tsx b/src/components/ReceiptAudit.tsx index f756ecd5dc7e..5b91fe97ca0d 100644 --- a/src/components/ReceiptAudit.tsx +++ b/src/components/ReceiptAudit.tsx @@ -27,8 +27,6 @@ export default function ReceiptAudit({notes = []}: {notes?: string[]}) { {issuesFoundText} - - {/* // If notes is a array of strings, map through it & show notes. */} {notes.length > 0 && notes.map((message) => {message})} ); diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 415a2666512a..ffe373aade00 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -129,7 +129,8 @@ function MoneyRequestView({ const canEditDate = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DATE); const canEditReceipt = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.RECEIPT); const canEditDistance = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DISTANCE); - const isReceiptBeingScanned = TransactionUtils.hasReceipt(transaction) && !TransactionUtils.isReceiptBeingScanned(transaction); + const hasReceipt = TransactionUtils.hasReceipt(transaction); + const isReceiptBeingScanned = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction); // A flag for verifying that the current report is a sub-report of a workspace chat // if the policy of the report is either Collect or Control, then this report must be tied to workspace chat @@ -149,6 +150,7 @@ function MoneyRequestView({ const {getViolationsForField} = useViolations(transactionViolations ?? []); const hasViolations = useCallback((field: ViolationField): boolean => !!canUseViolations && getViolationsForField(field).length > 0, [canUseViolations, getViolationsForField]); const noteTypeViolations = transactionViolations?.filter((violation) => violation.type === 'note').map((v) => ViolationsUtils.getViolationTranslation(v, translate)); + const shouldShowNotesViolations = !isReceiptBeingScanned && canUseViolations && hasReceipt; let amountDescription = `${translate('iou.amount')}`; @@ -190,7 +192,6 @@ function MoneyRequestView({ } } - const hasReceipt = TransactionUtils.hasReceipt(transaction); let receiptURIs; const hasErrors = canEdit && TransactionUtils.hasMissingSmartscanFields(transaction); if (hasReceipt) { @@ -288,7 +289,7 @@ function MoneyRequestView({ } /> )} - {!isReceiptBeingScanned && canUseViolations && } + {shouldShowNotesViolations && } {canUseViolations && } Date: Fri, 8 Mar 2024 13:03:36 +0100 Subject: [PATCH 0051/1506] Refactor MyTripsPage --- src/ROUTES.ts | 2 - src/SCREENS.ts | 2 +- src/languages/en.ts | 1 + src/languages/es.ts | 4 +- .../AppNavigator/ModalStackNavigators.tsx | 6 ++ .../Navigators/BottomTabNavigator.tsx | 6 -- .../BaseCentralPaneNavigator.tsx | 5 -- .../Navigators/RightModalNavigator.tsx | 4 + .../BottomTabBar.tsx | 24 +----- .../TAB_TO_CENTRAL_PANE_MAPPING.ts | 1 - src/libs/Navigation/linkingConfig/config.ts | 9 +- src/libs/Navigation/types.ts | 8 +- src/libs/Permissions.ts | 4 +- src/pages/Travel/ManageTrips.tsx | 2 +- src/pages/Travel/MyTripsPage.tsx | 42 +++------- src/pages/Travel/TravelMenu.tsx | 83 ------------------- .../FloatingActionButtonAndPopover.js | 12 +++ 17 files changed, 56 insertions(+), 159 deletions(-) delete mode 100644 src/pages/Travel/TravelMenu.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 4496d102d037..f7c455e923a6 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -555,8 +555,6 @@ const ROUTES = { PROCESS_MONEY_REQUEST_HOLD: 'hold-request-educational', TRAVEL_MY_TRIPS: 'travel', - - TRAVEL_HOME: 'travel-home', } as const; /** diff --git a/src/SCREENS.ts b/src/SCREENS.ts index b08f8efabf45..b30ee4d45ec3 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -23,7 +23,6 @@ const SCREENS = { UNLINK_LOGIN: 'UnlinkLogin', SETTINGS_CENTRAL_PANE: 'SettingsCentralPane', TRAVEL: { - HOME: 'Travel_Home', MY_TRIPS: 'Travel_MyTrips', }, SETTINGS: { @@ -127,6 +126,7 @@ const SCREENS = { ROOM_INVITE: 'RoomInvite', REFERRAL: 'Referral', PROCESS_MONEY_REQUEST_HOLD: 'ProcessMoneyRequestHold', + TRAVEL: 'Travel', }, SIGN_IN_WITH_APPLE_DESKTOP: 'AppleSignInDesktop', SIGN_IN_WITH_GOOGLE_DESKTOP: 'GoogleSignInDesktop', diff --git a/src/languages/en.ts b/src/languages/en.ts index 2b483de57c93..65a3be6a2cfb 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1726,6 +1726,7 @@ export default { saveMoney: 'Save money on your bookings', alerts: 'Get real time alerts if your travel plans change', }, + bookTravel: 'Book Travel', }, workspace: { common: { diff --git a/src/languages/es.ts b/src/languages/es.ts index ca6cc81e8705..35707cf7424e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1750,6 +1750,7 @@ export default { saveMoney: 'Ahorra dinero en tus reservas', alerts: 'Recibe alertas en tiempo real si cambian tus planes de viaje', }, + bookTravel: 'Reservar viajes', }, workspace: { common: { @@ -2929,7 +2930,4 @@ export default { offline: 'Parece que estás desconectado. Desafortunadamente, Expensify Classic no funciona sin conexión, pero New Expensify sí. Si prefieres utilizar Expensify Classic, inténtalo de nuevo cuando tengas conexión a internet.', }, - travel: { - myTrips: 'Mis viajes', - }, } satisfies EnglishTranslation; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index 545641957c9a..ed3e34e76c1c 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -29,6 +29,7 @@ import type { SplitDetailsNavigatorParamList, TaskDetailsNavigatorParamList, TeachersUniteNavigatorParamList, + TravelNavigatorParamList, WalletStatementNavigatorParamList, WorkspaceSwitcherNavigatorParamList, } from '@navigation/types'; @@ -108,6 +109,10 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../pages/EditRequestReceiptPage').default as React.ComponentType, }); +const TravelModalStackNavigator = createModalStackNavigator({ + [SCREENS.TRAVEL.MY_TRIPS]: () => require('../../../pages/Travel/MyTripsPage').default as React.ComponentType, +}); + const SplitDetailsModalStackNavigator = createModalStackNavigator({ [SCREENS.SPLIT_DETAILS.ROOT]: () => require('../../../pages/iou/SplitBillDetailsPage').default as React.ComponentType, [SCREENS.SPLIT_DETAILS.EDIT_REQUEST]: () => require('../../../pages/EditSplitBillPage').default as React.ComponentType, @@ -336,4 +341,5 @@ export { TaskModalStackNavigator, WalletStatementStackNavigator, ProcessMoneyRequestHoldStackNavigator, + TravelModalStackNavigator, }; diff --git a/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx index 9ce1b8425f51..ce03a8d5bcba 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx @@ -11,8 +11,6 @@ import ActiveRouteContext from './ActiveRouteContext'; const loadWorkspaceInitialPage = () => require('../../../../pages/workspace/WorkspaceInitialPage').default as React.ComponentType; -const loadTravelMenuPage = () => require('../../../../pages/Travel/TravelMenu').default as React.ComponentType; - const Tab = createCustomBottomTabNavigator(); const screenOptions: StackNavigationOptions = { @@ -37,10 +35,6 @@ function BottomTabNavigator() { name={SCREENS.WORKSPACE.INITIAL} getComponent={loadWorkspaceInitialPage} /> - ); diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx index ab20497a3c73..5f3d522f6b41 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx @@ -4,7 +4,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import ReportScreenWrapper from '@libs/Navigation/AppNavigator/ReportScreenWrapper'; import getCurrentUrl from '@libs/Navigation/currentUrl'; import type {CentralPaneNavigatorParamList} from '@navigation/types'; -import MyTripsPage from '@pages/Travel/MyTripsPage'; import SCREENS from '@src/SCREENS'; const Stack = createStackNavigator(); @@ -44,10 +43,6 @@ function BaseCentralPaneNavigator() { initialParams={{openOnAdminRoom: openOnAdminRoom === 'true' || undefined}} component={ReportScreenWrapper} /> - {Object.entries(workspaceSettingsScreens).map(([screenName, componentGetter]) => ( + diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx index 5eaf56eb7edd..58d9efb43df5 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx @@ -11,6 +11,7 @@ import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as Session from '@libs/actions/Session'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute'; import Navigation from '@libs/Navigation/Navigation'; @@ -47,7 +48,8 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps // When we are redirected to the Settings tab from the OldDot, we don't want to call the Welcome.show() method. // To prevent this, the value of the bottomTabRoute?.name is checked here bottomTabRoute?.name === SCREENS.WORKSPACE.INITIAL || - (currentRoute && currentRoute.name !== NAVIGATORS.BOTTOM_TAB_NAVIGATOR && currentRoute.name !== NAVIGATORS.CENTRAL_PANE_NAVIGATOR) + Boolean(currentRoute && currentRoute.name !== NAVIGATORS.BOTTOM_TAB_NAVIGATOR && currentRoute.name !== NAVIGATORS.CENTRAL_PANE_NAVIGATOR) || + Session.isAnonymousUser() ) { return; } @@ -91,24 +93,7 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps - - Navigation.navigate(ROUTES.TRAVEL_HOME)} - role={CONST.ROLE.BUTTON} - accessibilityLabel={translate('workspace.common.travel')} - wrapperStyle={styles.flexGrow1} - style={styles.bottomTabBarItem} - > - - - - - + @@ -132,7 +117,6 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps - ); } diff --git a/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts b/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts index 94945fc2aa54..f4316009b70b 100755 --- a/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts @@ -15,7 +15,6 @@ const TAB_TO_CENTRAL_PANE_MAPPING: Record = { SCREENS.WORKSPACE.MEMBERS, SCREENS.WORKSPACE.CATEGORIES, ], - [SCREENS.TRAVEL.HOME]: [SCREENS.TRAVEL.MY_TRIPS], }; const generateCentralPaneToTabMapping = (): Record => { diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index babe4862bfeb..c7a259513885 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -34,14 +34,12 @@ const config: LinkingOptions['config'] = { path: ROUTES.WORKSPACE_INITIAL.route, exact: true, }, - [SCREENS.TRAVEL.HOME]: ROUTES.TRAVEL_HOME, }, }, [NAVIGATORS.CENTRAL_PANE_NAVIGATOR]: { screens: { [SCREENS.REPORT]: ROUTES.REPORT_WITH_ID.route, - [SCREENS.SETTINGS.WORKSPACES]: ROUTES.SETTINGS_WORKSPACES, [SCREENS.WORKSPACE.PROFILE]: ROUTES.WORKSPACE_PROFILE.route, [SCREENS.WORKSPACE.CARD]: { @@ -68,8 +66,6 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.CATEGORIES]: { path: ROUTES.WORKSPACE_CATEGORIES.route, }, - - [SCREENS.TRAVEL.MY_TRIPS]: ROUTES.TRAVEL_MY_TRIPS, }, }, [SCREENS.NOT_FOUND]: '*', @@ -529,6 +525,11 @@ const config: LinkingOptions['config'] = { [SCREENS.PROCESS_MONEY_REQUEST_HOLD_ROOT]: ROUTES.PROCESS_MONEY_REQUEST_HOLD, }, }, + [SCREENS.RIGHT_MODAL.TRAVEL]: { + screens: { + [SCREENS.TRAVEL.MY_TRIPS]: ROUTES.TRAVEL_MY_TRIPS, + }, + }, }, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index daaf867f16c2..8b89ee7b9ea2 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -90,7 +90,6 @@ type CentralPaneNavigatorParamList = { [SCREENS.WORKSPACE.CATEGORIES]: { policyID: string; }; - [SCREENS.TRAVEL.MY_TRIPS]: undefined; }; type WorkspaceSwitcherNavigatorParamList = { @@ -495,6 +494,11 @@ type RightModalNavigatorParamList = { [SCREENS.RIGHT_MODAL.PROCESS_MONEY_REQUEST_HOLD]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.REFERRAL]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.TRAVEL]: NavigatorScreenParams; +}; + +type TravelNavigatorParamList = { + [SCREENS.TRAVEL.MY_TRIPS]: undefined; }; type SettingsCentralPaneNavigatorParamList = { @@ -514,7 +518,6 @@ type BottomTabNavigatorParamList = { [SCREENS.HOME]: undefined; [SCREENS.ALL_SETTINGS]: undefined; [SCREENS.WORKSPACE.INITIAL]: undefined; - [SCREENS.TRAVEL.HOME]: undefined; }; type PublicScreensParamList = { @@ -635,4 +638,5 @@ export type { WorkspaceSwitcherNavigatorParamList, OnboardEngagementNavigatorParamList, SwitchPolicyIDParams, + TravelNavigatorParamList, }; diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index b3d7c302234d..beafd92024bd 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -30,7 +30,7 @@ function canUseWorkflowsDelayedSubmission(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.WORKFLOWS_DELAYED_SUBMISSION) || canUseAllBetas(betas); } -function canSeeTravelPage(betas: OnyxEntry): boolean { +function canUseSpotnanaTravel(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.SPOTNANA_TRAVEL) || canUseAllBetas(betas); } @@ -49,5 +49,5 @@ export default { canUseViolations, canUseReportFields, canUseWorkflowsDelayedSubmission, - canSeeTravelPage, + canUseSpotnanaTravel, }; diff --git a/src/pages/Travel/ManageTrips.tsx b/src/pages/Travel/ManageTrips.tsx index 30c2f1151492..817fb430c054 100644 --- a/src/pages/Travel/ManageTrips.tsx +++ b/src/pages/Travel/ManageTrips.tsx @@ -34,7 +34,7 @@ function ManageTrips() { subtitle={translate('travel.subtitle')} ctaText={translate('travel.bookOrManage')} ctaAccessibilityLabel={translate('travel.bookOrManage')} - onCtaPress={() => console.log('pressed')} + onCtaPress={() => {}} illustration={LottieAnimations.Plane} illustrationStyle={styles.travelIllustrationStyle} illustrationBackgroundColor={colors.blue600} diff --git a/src/pages/Travel/MyTripsPage.tsx b/src/pages/Travel/MyTripsPage.tsx index ddef2f303318..003775d1a10c 100644 --- a/src/pages/Travel/MyTripsPage.tsx +++ b/src/pages/Travel/MyTripsPage.tsx @@ -1,29 +1,15 @@ -import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; -import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import Navigation from '@libs/Navigation/Navigation'; -import Permissions from '@libs/Permissions'; -import type {CentralPaneNavigatorParamList} from '@navigation/types'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type SCREENS from '@src/SCREENS'; -import type {Beta} from '@src/types/onyx'; +import usePermissions from '@hooks/usePermissions'; import ManageTrips from './ManageTrips'; -type MyTripsPageOnyxProps = {betas: OnyxEntry}; - -type MyTripsPageProps = StackScreenProps & MyTripsPageOnyxProps; - -function MyTripsPage({betas}: MyTripsPageProps) { +function MyTripsPage() { const {translate} = useLocalize(); - const {isSmallScreenWidth} = useWindowDimensions(); - const canSeeTravelPage = Permissions.canSeeTravelPage(betas); + const {canUseSpotnanaTravel} = usePermissions(); return ( - Navigation.goBack()} - /> - + + @@ -48,8 +36,4 @@ function MyTripsPage({betas}: MyTripsPageProps) { MyTripsPage.displayName = 'MyTripsPage'; -export default withOnyx({ - betas: { - key: ONYXKEYS.BETAS, - }, -})(MyTripsPage); +export default MyTripsPage; diff --git a/src/pages/Travel/TravelMenu.tsx b/src/pages/Travel/TravelMenu.tsx deleted file mode 100644 index 12fca5d0c587..000000000000 --- a/src/pages/Travel/TravelMenu.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React, {useMemo} from 'react'; -import {ScrollView} from 'react-native'; -import Breadcrumbs from '@components/Breadcrumbs'; -import * as Expensicons from '@components/Icon/Expensicons'; -import MenuItemList from '@components/MenuItemList'; -import ScreenWrapper from '@components/ScreenWrapper'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWaitForNavigation from '@hooks/useWaitForNavigation'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import Navigation from '@libs/Navigation/Navigation'; -import CONST from '@src/CONST'; -import type {TranslationPaths} from '@src/languages/types'; -import ROUTES from '@src/ROUTES'; - -// type TravelMenuProps = StackScreenProps; - -function TravelMenu() { - const styles = useThemeStyles(); - const waitForNavigate = useWaitForNavigation(); - const {translate} = useLocalize(); - const {isSmallScreenWidth} = useWindowDimensions(); - - /** - * Retuns a list of menu items data for Travel menu - * @returns {Object} object with translationKey, style and items - */ - const menuItems = useMemo(() => { - const baseMenuItems = [ - { - translationKey: 'travel.header', - icon: Expensicons.Suitcase, - action: () => { - waitForNavigate(() => { - Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS); - })(); - }, - focused: !isSmallScreenWidth, - }, - ]; - return baseMenuItems.map((item) => ({ - key: item.translationKey, - title: translate(item.translationKey as TranslationPaths), - icon: item.icon, - onPress: item.action, - wrapperStyle: styles.sectionMenuItem, - isPaneMenu: true, - focused: item.focused, - hoverAndPressStyle: styles.hoveredComponentBG, - })); - }, [isSmallScreenWidth, styles.hoveredComponentBG, styles.sectionMenuItem, translate, waitForNavigate]); - - return ( - - - - - - - ); -} - -TravelMenu.displayName = 'TravelMenu'; - -export default TravelMenu; diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index 573cbe370aa7..090e8384c615 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -9,6 +9,7 @@ import withNavigation from '@components/withNavigation'; import withNavigationFocus from '@components/withNavigationFocus'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; @@ -78,6 +79,8 @@ function FloatingActionButtonAndPopover(props) { const prevIsFocused = usePrevious(props.isFocused); + const {canUseSpotnanaTravel} = usePermissions(); + /** * Check if LHN status changed from active to inactive. * Used to close already opened FAB menu when open any other pages (i.e. Press Command + K on web). @@ -191,6 +194,15 @@ function FloatingActionButtonAndPopover(props) { text: translate('sidebarScreen.saveTheWorld'), onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.TEACHERS_UNITE)), }, + ...(canUseSpotnanaTravel + ? [ + { + icon: Expensicons.Suitcase, + text: translate('travel.bookTravel'), + onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS)), + }, + ] + : []), ...(!props.isLoading && !Policy.hasActiveFreePolicy(props.allPolicies) ? [ { From b53833171f6cf42c67998fa757a464a9e65cb97e Mon Sep 17 00:00:00 2001 From: Krishna Date: Mon, 11 Mar 2024 11:50:00 +0530 Subject: [PATCH 0052/1506] Update MoneyRequestView.tsx --- src/components/ReportActionItem/MoneyRequestView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 3569791d64d4..d79ff88a4f1e 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -147,7 +147,7 @@ function MoneyRequestView({ const shouldShowBillable = isPolicyExpenseChat && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true)); const {getViolationsForField} = useViolations(transactionViolations ?? []); - const hasViolations = useCallback( + const hasViolations = useCallback( (field: ViolationField, data?: OnyxTypes.TransactionViolation['data']): boolean => !!canUseViolations && getViolationsForField(field, data).length > 0, [canUseViolations, getViolationsForField], ); From 581d09d7a3dec1f1c301c7dccfba4877e60cb7c4 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 11 Mar 2024 11:33:46 +0500 Subject: [PATCH 0053/1506] fix: comments --- src/components/withCurrentReportID.tsx | 2 +- src/hooks/useReportIDs.tsx | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/withCurrentReportID.tsx b/src/components/withCurrentReportID.tsx index 54cdc84f127a..bb3283a21d25 100644 --- a/src/components/withCurrentReportID.tsx +++ b/src/components/withCurrentReportID.tsx @@ -45,7 +45,7 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro /** * This is to make sure we don't set the undefined as reportID when * switching between chat list and settings->workspaces tab. - * and doing so avoid unnecessary re-render of `useReportIDs`. + * and doing so avoids an unnecessary re-render of `useReportIDs`. */ const params = state.routes[state.index].params; if (params && 'screen' in params && params.screen === SCREENS.SETTINGS.WORKSPACES) { diff --git a/src/hooks/useReportIDs.tsx b/src/hooks/useReportIDs.tsx index 2e7c63182646..547975ae1cd0 100644 --- a/src/hooks/useReportIDs.tsx +++ b/src/hooks/useReportIDs.tsx @@ -56,8 +56,7 @@ function WithReportIDsContextProvider({ * to SidebarLinksData, so this context doesn't have an * access to currentReportID in that case. * - * So this is a work around to have currentReportID available - * only in testing environment. + * This is a workaround to have currentReportID available in testing environment. */ currentReportIDForTests, }: WithReportIDsContextProviderProps) { @@ -109,7 +108,7 @@ function WithReportIDsContextProvider({ // We need to make sure the current report is in the list of reports, but we do not want // to have to re-generate the list every time the currentReportID changes. To do that - // we first generate the list as if there was no current report, then here we check if + // we first generate the list as if there was no current report, then we check if // the current report is missing from the list, which should very rarely happen. In this // case we re-generate the list a 2nd time with the current report included. const orderedReportIDsWithCurrentReport = useMemo(() => { From fffe41ac7ed346c542da74cbe7952e581be0eb1c Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 11 Mar 2024 11:58:40 +0500 Subject: [PATCH 0054/1506] fix: safely check the nested properties --- src/components/withCurrentReportID.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/withCurrentReportID.tsx b/src/components/withCurrentReportID.tsx index bb3283a21d25..22f68de9f57a 100644 --- a/src/components/withCurrentReportID.tsx +++ b/src/components/withCurrentReportID.tsx @@ -47,7 +47,7 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro * switching between chat list and settings->workspaces tab. * and doing so avoids an unnecessary re-render of `useReportIDs`. */ - const params = state.routes[state.index].params; + const params = state?.routes?.[state.index]?.params; if (params && 'screen' in params && params.screen === SCREENS.SETTINGS.WORKSPACES) { return; } From 6b41bf7819af297e0c3b7b32ced187b119c68193 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 11 Mar 2024 12:11:17 +0500 Subject: [PATCH 0055/1506] feat: add comments for extraData prop --- src/components/LHNOptionsList/LHNOptionsList.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index be8ce677b641..d141a5bbb3f4 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -129,6 +129,12 @@ function LHNOptionsList({ keyExtractor={keyExtractor} renderItem={renderItem} estimatedItemSize={optionMode === CONST.OPTION_MODE.COMPACT ? variables.optionRowHeightCompact : variables.optionRowHeight} + // Previously, we were passing `extraData={[currentReportID]}`, which upon every render, was causing the + // re-render because of the new array reference. FlashList's children actually don't depend on the + // `currentReportID` prop but they depend on the `reportActions`, `reports`, `policy`, `personalDetails`. + // Previously it was working for us because of the new array reference. Even if you only pass an empty + // array, it will still work because of the new reference. But it's better to pass the actual dependencies + // to avoid unnecessary re-renders. extraData={extraData} showsVerticalScrollIndicator={false} /> From 48d1543f0ff3a75aea75eb2bb9f176671475c5dd Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Tue, 12 Mar 2024 13:12:52 +0530 Subject: [PATCH 0056/1506] fix: translations. Signed-off-by: Krishna Gupta --- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 2c4f5d21f66f..7979981392fe 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -604,7 +604,7 @@ export default { receiptAudit: 'Receipt Audit', receiptVerified: 'Receipt Verified', receiptNoIssuesFound: 'No issues Found', - receiptIssuesFound: (count: number) => `${count} Issue(s) Found`, + receiptIssuesFound: (count: number) => `${count} ${count === 1 ? 'Issue' : 'Issues'} Found`, receiptScanning: 'Scan in progress…', receiptMissingDetails: 'Receipt missing details', receiptStatusTitle: 'Scanning…', diff --git a/src/languages/es.ts b/src/languages/es.ts index 2f6a064a3615..730d110a60a9 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -597,7 +597,7 @@ export default { receiptAudit: 'Auditoría de recibos', receiptVerified: 'Recibo verificado', receiptNoIssuesFound: 'No se encontraron problemas', - receiptIssuesFound: (count: number) => `Se encontró ${count} problema(s)`, + receiptIssuesFound: (count: number) => `Se encontró ${count} ${count === 1 ? 'problema' : 'problemas'}`, receiptScanning: 'Escaneo en curso…', receiptMissingDetails: 'Recibo con campos vacíos', receiptStatusTitle: 'Escaneando…', From 42b22e0383afdfa5221f116b197eb2877834c085 Mon Sep 17 00:00:00 2001 From: smelaa Date: Wed, 13 Mar 2024 10:52:07 +0100 Subject: [PATCH 0057/1506] Initial config to workspace address page --- src/ROUTES.ts | 4 +++ src/SCREENS.ts | 1 + src/languages/en.ts | 1 + src/languages/es.ts | 1 + .../AppNavigator/ModalStackNavigators.tsx | 1 + .../CENTRAL_PANE_TO_RHP_MAPPING.ts | 2 +- src/libs/Navigation/linkingConfig/config.ts | 3 ++ src/libs/Navigation/types.ts | 1 + .../workspace/WorkspaceProfileAddressPage.tsx | 32 +++++++++++++++++++ src/pages/workspace/WorkspaceProfilePage.tsx | 17 ++++++++++ 10 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 src/pages/workspace/WorkspaceProfileAddressPage.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index d9f0c6658a2b..2b27179db7a0 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -465,6 +465,10 @@ const ROUTES = { route: 'workspace/:policyID/profile', getRoute: (policyID: string) => `workspace/${policyID}/profile` as const, }, + WORKSPACE_PROFILE_ADDRESS: { + route: 'workspace/:policyID/profile/address', + getRoute: (policyID: string) => `workspace/${policyID}/profile/address` as const, + }, WORKSPACE_PROFILE_CURRENCY: { route: 'workspace/:policyID/profile/currency', getRoute: (policyID: string) => `workspace/${policyID}/profile/currency` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index a0e06b98da2b..b5c0040dd9f7 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -219,6 +219,7 @@ const SCREENS = { TAGS_SETTINGS: 'Tags_Settings', TAGS_EDIT: 'Tags_Edit', CURRENCY: 'Workspace_Profile_Currency', + ADDRESS: 'Workspace_Profile_Address', WORKFLOWS: 'Workspace_Workflows', WORKFLOWS_APPROVER: 'Workspace_Workflows_Approver', WORKFLOWS_AUTO_REPORTING_FREQUENCY: 'Workspace_Workflows_Auto_Reporting_Frequency', diff --git a/src/languages/en.ts b/src/languages/en.ts index ff91a4f6f205..9aeca12dd5d7 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1984,6 +1984,7 @@ export default { currencyInputLabel: 'Default currency', currencyInputHelpText: 'All expenses on this workspace will be converted to this currency.', currencyInputDisabledText: "The default currency can't be changed because this workspace is linked to a USD bank account.", + addressInputLabel: 'Company address', save: 'Save', genericFailureMessage: 'An error occurred updating the workspace, please try again.', avatarUploadFailureMessage: 'An error occurred uploading the avatar, please try again.', diff --git a/src/languages/es.ts b/src/languages/es.ts index c21f46ed8853..9580ec8df535 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2009,6 +2009,7 @@ export default { currencyInputLabel: 'Moneda por defecto', currencyInputHelpText: 'Todas los gastos en este espacio de trabajo serán convertidos a esta moneda.', currencyInputDisabledText: 'La moneda predeterminada no se puede cambiar porque este espacio de trabajo está vinculado a una cuenta bancaria en USD.', + addressInputLabel: 'Dirección de la empresa', save: 'Guardar', genericFailureMessage: 'Se produjo un error al guardar el espacio de trabajo. Por favor, inténtalo de nuevo.', avatarUploadFailureMessage: 'No se pudo subir el avatar. Por favor, inténtalo de nuevo.', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index d56e38564149..df67de82d635 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -249,6 +249,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/WorkspaceProfileDescriptionPage').default as React.ComponentType, [SCREENS.WORKSPACE.SHARE]: () => require('../../../pages/workspace/WorkspaceProfileSharePage').default as React.ComponentType, [SCREENS.WORKSPACE.CURRENCY]: () => require('../../../pages/workspace/WorkspaceProfileCurrencyPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ADDRESS]: () => require('../../../pages/workspace/WorkspaceProfileAddressPage').default as React.ComponentType, [SCREENS.WORKSPACE.CATEGORY_SETTINGS]: () => require('../../../pages/workspace/categories/CategorySettingsPage').default as React.ComponentType, [SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: () => require('../../../pages/workspace/categories/WorkspaceCategoriesSettingsPage').default as React.ComponentType, [SCREENS.WORKSPACE.MEMBER_DETAILS]: () => require('../../../pages/workspace/members/WorkspaceMemberDetailsPage').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts index 743bf2e0cff1..f6fe75901848 100755 --- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts @@ -2,7 +2,7 @@ import type {CentralPaneName} from '@libs/Navigation/types'; import SCREENS from '@src/SCREENS'; const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = { - [SCREENS.WORKSPACE.PROFILE]: [SCREENS.WORKSPACE.NAME, SCREENS.WORKSPACE.CURRENCY, SCREENS.WORKSPACE.DESCRIPTION, SCREENS.WORKSPACE.SHARE], + [SCREENS.WORKSPACE.PROFILE]: [SCREENS.WORKSPACE.NAME, SCREENS.WORKSPACE.CURRENCY, SCREENS.WORKSPACE.ADDRESS, SCREENS.WORKSPACE.DESCRIPTION, SCREENS.WORKSPACE.SHARE], [SCREENS.WORKSPACE.REIMBURSE]: [SCREENS.WORKSPACE.RATE_AND_UNIT, SCREENS.WORKSPACE.RATE_AND_UNIT_RATE, SCREENS.WORKSPACE.RATE_AND_UNIT_UNIT], [SCREENS.WORKSPACE.MEMBERS]: [SCREENS.WORKSPACE.INVITE, SCREENS.WORKSPACE.INVITE_MESSAGE, SCREENS.WORKSPACE.MEMBER_DETAILS, SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION], [SCREENS.WORKSPACE.WORKFLOWS]: [SCREENS.WORKSPACE.WORKFLOWS_APPROVER, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET], diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 97d7650a9043..296ccc30ba69 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -249,6 +249,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.CURRENCY]: { path: ROUTES.WORKSPACE_PROFILE_CURRENCY.route, }, + [SCREENS.WORKSPACE.ADDRESS]: { + path: ROUTES.WORKSPACE_PROFILE_ADDRESS.route, + }, [SCREENS.WORKSPACE.DESCRIPTION]: { path: ROUTES.WORKSPACE_PROFILE_DESCRIPTION.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 33e79b637cc4..1443a60f4b16 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -186,6 +186,7 @@ type SettingsNavigatorParamList = { [SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_DATE]: undefined; [SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_TIME]: undefined; [SCREENS.WORKSPACE.CURRENCY]: undefined; + [SCREENS.WORKSPACE.ADDRESS]: undefined; [SCREENS.WORKSPACE.NAME]: undefined; [SCREENS.WORKSPACE.DESCRIPTION]: undefined; [SCREENS.WORKSPACE.SHARE]: undefined; diff --git a/src/pages/workspace/WorkspaceProfileAddressPage.tsx b/src/pages/workspace/WorkspaceProfileAddressPage.tsx new file mode 100644 index 000000000000..80f9066040be --- /dev/null +++ b/src/pages/workspace/WorkspaceProfileAddressPage.tsx @@ -0,0 +1,32 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import {Text, View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import ScreenWrapper from '@components/ScreenWrapper'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type {PrivatePersonalDetails} from '@src/types/onyx'; + +type WorkspaceProfileAddressPageOnyxProps = { + /** User's private personal details */ + privatePersonalDetails: OnyxEntry; +}; + +type WorkspaceProfileAddressPageProps = StackScreenProps & WorkspaceProfileAddressPageOnyxProps; + +function WorkspaceProfileAddressPage({privatePersonalDetails, route}: WorkspaceProfileAddressPageProps) { + return ( + + abc + + ); +} + +WorkspaceProfileAddressPage.displayName = 'WorkspaceProfileAddressPage'; + +export default WorkspaceProfileAddressPage; diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index 9d90557b1d37..154708688fb0 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -50,7 +50,10 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi const currencySymbol = currencyList?.[outputCurrency]?.symbol ?? ''; const formattedCurrency = !isEmptyObject(policy) && !isEmptyObject(currencyList) ? `${outputCurrency} - ${currencySymbol}` : ''; + const formattedAddress = 'abc'; + const onPressCurrency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_CURRENCY.getRoute(policy?.id ?? '')), [policy?.id]); + const onPressAddress = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_ADDRESS.getRoute(policy?.id ?? '')), [policy?.id]); const onPressName = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_NAME.getRoute(policy?.id ?? '')), [policy?.id]); const onPressDescription = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_DESCRIPTION.getRoute(policy?.id ?? '')), [policy?.id]); const onPressShare = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_SHARE.getRoute(policy?.id ?? '')), [policy?.id]); @@ -186,6 +189,20 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi + + + + + {!readOnly && ( + + )} ) : ( - + ); } diff --git a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.android.tsx b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.android.tsx deleted file mode 100644 index 64391909b197..000000000000 --- a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.android.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import {Animated, View} from 'react-native'; -import useThemeStyles from '@hooks/useThemeStyles'; -import type FloatingMessageCounterContainerProps from './types'; - -function FloatingMessageCounterContainer({containerStyles, children}: FloatingMessageCounterContainerProps) { - const styles = useThemeStyles(); - - return ( - - {children} - - ); -} - -FloatingMessageCounterContainer.displayName = 'FloatingMessageCounterContainer'; - -export default FloatingMessageCounterContainer; diff --git a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.tsx b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.tsx deleted file mode 100644 index 8757d66160c4..000000000000 --- a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import {Animated} from 'react-native'; -import useThemeStyles from '@hooks/useThemeStyles'; -import type FloatingMessageCounterContainerProps from './types'; - -function FloatingMessageCounterContainer({accessibilityHint, containerStyles, children}: FloatingMessageCounterContainerProps) { - const styles = useThemeStyles(); - - return ( - - {children} - - ); -} - -FloatingMessageCounterContainer.displayName = 'FloatingMessageCounterContainer'; - -export default FloatingMessageCounterContainer; diff --git a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/types.ts b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/types.ts deleted file mode 100644 index cfe791eed79c..000000000000 --- a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type {StyleProp, ViewStyle} from 'react-native'; -import type ChildrenProps from '@src/types/utils/ChildrenProps'; - -type FloatingMessageCounterContainerProps = ChildrenProps & { - /** Styles to be assigned to Container */ - containerStyles?: StyleProp; - - /** Specifies the accessibility hint for the component */ - accessibilityHint?: string; -}; - -export default FloatingMessageCounterContainerProps; From 35ea3650ee17950c264c2af6275fec819511e24b Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 16 Apr 2024 17:28:44 -0700 Subject: [PATCH 0299/1506] Remove unused styles --- src/styles/index.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/styles/index.ts b/src/styles/index.ts index 537038d9f2e1..9bba82ac6b95 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3341,21 +3341,6 @@ const styles = (theme: ThemeColors) => ...visibility.hidden, }, - floatingMessageCounterWrapperAndroid: { - left: 0, - width: '100%', - alignItems: 'center', - position: 'absolute', - top: 0, - zIndex: 100, - ...visibility.hidden, - }, - - floatingMessageCounterSubWrapperAndroid: { - left: '50%', - width: 'auto', - }, - floatingMessageCounter: { left: '-50%', ...visibility.visible, From fda608b3b896d4d2ea2ede7101b5cd42ad85c93f Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 16 Apr 2024 17:31:41 -0700 Subject: [PATCH 0300/1506] Combine styles together --- src/pages/home/report/FloatingMessageCounter.tsx | 2 +- src/styles/index.ts | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/pages/home/report/FloatingMessageCounter.tsx b/src/pages/home/report/FloatingMessageCounter.tsx index 997198192bb6..34eb01b62817 100644 --- a/src/pages/home/report/FloatingMessageCounter.tsx +++ b/src/pages/home/report/FloatingMessageCounter.tsx @@ -52,7 +52,7 @@ function FloatingMessageCounter({isActive = false, onClick = () => {}}: Floating return ( diff --git a/src/styles/index.ts b/src/styles/index.ts index 9bba82ac6b95..df94258b5c77 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3333,24 +3333,20 @@ const styles = (theme: ThemeColors) => height: variables.communicationsLinkHeight, }, - floatingMessageCounterWrapper: { + floatingMessageCounterWrapper: (translateY: AnimatableNumericValue) => ({ position: 'absolute', left: '50%', top: 0, zIndex: 100, + transform: [{translateY}], ...visibility.hidden, - }, + }), floatingMessageCounter: { left: '-50%', ...visibility.visible, }, - floatingMessageCounterTransformation: (translateY: AnimatableNumericValue) => - ({ - transform: [{translateY}], - } satisfies ViewStyle), - confirmationAnimation: { height: 180, width: 180, From cf6d23e8bd6eefe3af55e1b6c529e336d69e1ada Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 16 Apr 2024 17:48:03 -0700 Subject: [PATCH 0301/1506] Migrate animation to reanimated --- .../home/report/FloatingMessageCounter.tsx | 27 ++++++++++--------- src/styles/index.ts | 5 ++-- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/pages/home/report/FloatingMessageCounter.tsx b/src/pages/home/report/FloatingMessageCounter.tsx index 34eb01b62817..8e73d36a878a 100644 --- a/src/pages/home/report/FloatingMessageCounter.tsx +++ b/src/pages/home/report/FloatingMessageCounter.tsx @@ -1,5 +1,6 @@ import React, {useCallback, useEffect, useMemo} from 'react'; -import {Animated, View} from 'react-native'; +import {View} from 'react-native'; +import Animated, {useAnimatedStyle, useSharedValue, withSpring} from 'react-native-reanimated'; import Button from '@components/Button'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -7,7 +8,6 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import useNativeDriver from '@libs/useNativeDriver'; import CONST from '@src/CONST'; type FloatingMessageCounterProps = { @@ -25,20 +25,18 @@ function FloatingMessageCounter({isActive = false, onClick = () => {}}: Floating const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); - const translateY = useMemo(() => new Animated.Value(MARKER_INACTIVE_TRANSLATE_Y), []); + const translateY = useSharedValue(MARKER_INACTIVE_TRANSLATE_Y); const show = useCallback(() => { - Animated.spring(translateY, { - toValue: MARKER_ACTIVE_TRANSLATE_Y, - useNativeDriver, - }).start(); + 'worklet'; + + translateY.value = withSpring(MARKER_ACTIVE_TRANSLATE_Y); }, [translateY]); const hide = useCallback(() => { - Animated.spring(translateY, { - toValue: MARKER_INACTIVE_TRANSLATE_Y, - useNativeDriver, - }).start(); + 'worklet'; + + translateY.value = withSpring(MARKER_INACTIVE_TRANSLATE_Y); }, [translateY]); useEffect(() => { @@ -49,10 +47,15 @@ function FloatingMessageCounter({isActive = false, onClick = () => {}}: Floating } }, [isActive, show, hide]); + const wrapperStyle = useAnimatedStyle(() => ({ + ...styles.floatingMessageCounterWrapper, + transform: [{translateY: translateY.value}], + })); + return ( diff --git a/src/styles/index.ts b/src/styles/index.ts index df94258b5c77..0badb7412b1a 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3333,14 +3333,13 @@ const styles = (theme: ThemeColors) => height: variables.communicationsLinkHeight, }, - floatingMessageCounterWrapper: (translateY: AnimatableNumericValue) => ({ + floatingMessageCounterWrapper: { position: 'absolute', left: '50%', top: 0, zIndex: 100, - transform: [{translateY}], ...visibility.hidden, - }), + }, floatingMessageCounter: { left: '-50%', From 316552a18579fbd5706b8306a1db30c986545eae Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Wed, 17 Apr 2024 08:03:05 +0700 Subject: [PATCH 0302/1506] temporary change version to test --- desktop/main.ts | 2 +- package-lock.json | 4 ++-- package.json | 2 +- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/desktop/main.ts b/desktop/main.ts index 640b6a417a73..662278d540fa 100644 --- a/desktop/main.ts +++ b/desktop/main.ts @@ -150,7 +150,7 @@ const manuallyCheckForUpdates = (menuItem?: MenuItem, browserWindow?: BrowserWin dialog.showMessageBox(browserWindow, { type: 'info', message: Localize.translate(preferredLocale, 'checkForUpdatesModal.available.title'), - detail: Localize.translate(preferredLocale, 'checkForUpdatesModal.available.message'), + detail: Localize.translate(preferredLocale, 'checkForUpdatesModal.available.message', {isSilentUpdate}), buttons: [Localize.translate(preferredLocale, 'checkForUpdatesModal.available.soundsGood')], }); } else if (result && 'error' in result && result.error) { diff --git a/package-lock.json b/package-lock.json index ae50c5d543b2..32adc60abe02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.62-10", + "version": "1.4.61-8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.62-10", + "version": "1.4.61-8", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 9a7d7ba0a333..0efbab6c503e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.62-10", + "version": "1.4.57-5", "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/languages/en.ts b/src/languages/en.ts index 9451407c822f..4f226ae96b59 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2456,7 +2456,7 @@ export default { checkForUpdatesModal: { available: { title: 'Update Available', - message: "The new version will be available shortly. We'll notify you when we're ready to update.", + message: ({isSilentUpdate}: {isSilentUpdate: boolean}) => `The new version will be available shortly.${isSilentUpdate ? " We'll notify you when we're ready to update." : ''}`, soundsGood: 'Sounds good', }, notAvailable: { diff --git a/src/languages/es.ts b/src/languages/es.ts index a56c8ac2739d..2ebfa6085286 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2488,7 +2488,7 @@ export default { checkForUpdatesModal: { available: { title: 'Actualización disponible', - message: 'La nueva versión estará disponible dentro de poco. Te notificaremos cuando esté lista.', + message: ({isSilentUpdate}: {isSilentUpdate: boolean}) => `La nueva versión estará disponible dentro de poco.${isSilentUpdate ? ' Te notificaremos cuando esté lista.' : ''}`, soundsGood: 'Suena bien', }, notAvailable: { From 1044bad394543fde51d2e89b6f935444b5fd3779 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 17 Apr 2024 11:49:42 +0700 Subject: [PATCH 0303/1506] rename props Avatar --- src/components/Avatar.tsx | 6 +++--- src/components/HeaderWithBackButton/index.tsx | 2 +- src/components/MentionSuggestions.tsx | 2 +- src/components/MultipleAvatars.tsx | 8 ++++---- src/components/RoomHeaderAvatars.tsx | 4 ++-- src/components/SubscriptAvatar.tsx | 4 ++-- .../UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx | 2 +- src/pages/home/report/ReportActionItemSingle.tsx | 2 +- src/pages/workspace/WorkspaceProfilePage.tsx | 2 +- src/pages/workspace/WorkspacesListRow.tsx | 2 +- 10 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index dfcf2cc675b9..09d22132b44e 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -51,7 +51,7 @@ type AvatarProps = { name?: string; /** ID of the Icon */ - iconID?: number | string; + accountID?: number | string; }; function Avatar({ @@ -65,7 +65,7 @@ function Avatar({ fallbackIconTestID = '', type = CONST.ICON_TYPE_AVATAR, name = '', - iconID, + accountID, }: AvatarProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -92,7 +92,7 @@ function Avatar({ let iconColors; if (isWorkspace) { - iconColors = StyleUtils.getDefaultWorkspaceAvatarColor(iconID?.toString() ?? ''); + iconColors = StyleUtils.getDefaultWorkspaceAvatarColor(accountID?.toString() ?? ''); } else if (useFallBackAvatar) { iconColors = StyleUtils.getBackgroundColorAndFill(theme.border, theme.icon); } else { diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index 6e649955880a..29d010d9c803 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -182,7 +182,7 @@ function HeaderWithBackButton({ containerStyles={[StyleUtils.getWidthAndHeightStyle(StyleUtils.getAvatarSize(CONST.AVATAR_SIZE.DEFAULT)), styles.mr3]} source={policyAvatar?.source} name={policyAvatar?.name} - iconID={policyAvatar?.id} + accountID={policyAvatar?.id} type={policyAvatar?.type} /> )} diff --git a/src/components/MentionSuggestions.tsx b/src/components/MentionSuggestions.tsx index e69484eacadd..b11ae4f5ecd8 100644 --- a/src/components/MentionSuggestions.tsx +++ b/src/components/MentionSuggestions.tsx @@ -83,7 +83,7 @@ function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSe source={item.icons[0].source} size={isIcon ? CONST.AVATAR_SIZE.MENTION_ICON : CONST.AVATAR_SIZE.SMALLER} name={item.icons[0].name} - iconID={item.icons[0].id} + accountID={item.icons[0].id} type={item.icons[0].type} fill={isIcon ? theme.success : undefined} fallbackIcon={item.icons[0].fallbackIcon} diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx index cbceace6df16..d5fcf6607179 100644 --- a/src/components/MultipleAvatars.tsx +++ b/src/components/MultipleAvatars.tsx @@ -156,7 +156,7 @@ function MultipleAvatars({ size={size} fill={icons[0].fill} name={icons[0].name} - iconID={icons[0].id} + accountID={icons[0].id} type={icons[0].type} fallbackIcon={icons[0].fallbackIcon} /> @@ -206,7 +206,7 @@ function MultipleAvatars({ source={icon.source ?? fallbackIcon} size={size} name={icon.name} - iconID={icon.id} + accountID={icon.id} type={icon.type} fallbackIcon={icon.fallbackIcon} /> @@ -265,7 +265,7 @@ function MultipleAvatars({ imageStyles={[singleAvatarStyle]} name={icons[0].name} type={icons[0].type} - iconID={icons[0].id} + accountID={icons[0].id} fallbackIcon={icons[0].fallbackIcon} /> @@ -285,7 +285,7 @@ function MultipleAvatars({ size={avatarSize} imageStyles={[singleAvatarStyle]} name={icons[1].name} - iconID={icons[1].id} + accountID={icons[1].id} type={icons[1].type} fallbackIcon={icons[1].fallbackIcon} /> diff --git a/src/components/RoomHeaderAvatars.tsx b/src/components/RoomHeaderAvatars.tsx index 341398e44b06..bdb4a0ac78ab 100644 --- a/src/components/RoomHeaderAvatars.tsx +++ b/src/components/RoomHeaderAvatars.tsx @@ -47,7 +47,7 @@ function RoomHeaderAvatars({icons, reportID}: RoomHeaderAvatarsProps) { imageStyles={styles.avatarLarge} size={CONST.AVATAR_SIZE.LARGE} name={icons[0].name} - iconID={icons[0].id} + accountID={icons[0].id} type={icons[0].type} fallbackIcon={icons[0].fallbackIcon} /> @@ -83,7 +83,7 @@ function RoomHeaderAvatars({icons, reportID}: RoomHeaderAvatarsProps) { size={CONST.AVATAR_SIZE.LARGE} containerStyles={[...iconStyle, StyleUtils.getAvatarBorderRadius(CONST.AVATAR_SIZE.LARGE_BORDERED, icon.type)]} name={icon.name} - iconID={icon.id} + accountID={icon.id} type={icon.type} fallbackIcon={icon.fallbackIcon} /> diff --git a/src/components/SubscriptAvatar.tsx b/src/components/SubscriptAvatar.tsx index d7ddb9ddf9b0..cc36657826f6 100644 --- a/src/components/SubscriptAvatar.tsx +++ b/src/components/SubscriptAvatar.tsx @@ -82,7 +82,7 @@ function SubscriptAvatar({ source={mainAvatar?.source} size={size} name={mainAvatar?.name} - iconID={mainAvatar?.id} + accountID={mainAvatar?.id} type={mainAvatar?.type} fallbackIcon={mainAvatar?.fallbackIcon} /> @@ -109,7 +109,7 @@ function SubscriptAvatar({ size={isSmall ? CONST.AVATAR_SIZE.SMALL_SUBSCRIPT : CONST.AVATAR_SIZE.SUBSCRIPT} fill={secondaryAvatar.fill} name={secondaryAvatar.name} - iconID={secondaryAvatar.id} + accountID={secondaryAvatar.id} type={secondaryAvatar.type} fallbackIcon={secondaryAvatar.fallbackIcon} /> diff --git a/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx b/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx index fa301167a230..a530bcfddc46 100644 --- a/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx +++ b/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx @@ -59,7 +59,7 @@ function BaseUserDetailsTooltip({accountID, fallbackUserDetails, icon, delegateA type={icon?.type ?? CONST.ICON_TYPE_AVATAR} name={icon?.name ?? userLogin} fallbackIcon={icon?.fallbackIcon} - iconID={icon?.id} + accountID={icon?.id} /> {title} diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 59c5b0e240ad..ce8d116a0b2d 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -200,7 +200,7 @@ function ReportActionItemSingle({ source={icon.source} type={icon.type} name={icon.name} - iconID={icon.id} + accountID={icon.id} fallbackIcon={fallbackIcon} /> diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index c328bea9bb96..d872cb0275f1 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -83,7 +83,7 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi fallbackIcon={Expensicons.FallbackWorkspaceAvatar} size={CONST.AVATAR_SIZE.XLARGE} name={policyName} - iconID={policy?.id ?? ''} + accountID={policy?.id ?? ''} type={CONST.ICON_TYPE_WORKSPACE} /> ), diff --git a/src/pages/workspace/WorkspacesListRow.tsx b/src/pages/workspace/WorkspacesListRow.tsx index 21c14ff8cda4..c9473914eaae 100644 --- a/src/pages/workspace/WorkspacesListRow.tsx +++ b/src/pages/workspace/WorkspacesListRow.tsx @@ -151,7 +151,7 @@ function WorkspacesListRow({ source={workspaceIcon} fallbackIcon={fallbackWorkspaceIcon} name={title} - iconID={policyID} + accountID={policyID} type={CONST.ICON_TYPE_WORKSPACE} /> Date: Wed, 17 Apr 2024 13:00:27 +0700 Subject: [PATCH 0304/1506] safely get personal detail --- src/components/OptionListContextProvider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index 94fe51711e02..5d4e704262f5 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -121,8 +121,8 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp }> = []; Object.keys(personalDetails).forEach((accoutID) => { - const prevPersonalDetail = prevPersonalDetails.current[accoutID]; - const personalDetail = personalDetails[accoutID]; + const prevPersonalDetail = prevPersonalDetails.current?.[accoutID]; + const personalDetail = personalDetails?.[accoutID]; if (isEqualPersonalDetail(prevPersonalDetail, personalDetail)) { return; From 1fecd977f9b534f119955a62bd4c7ecf31135bd2 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 17 Apr 2024 13:07:31 +0700 Subject: [PATCH 0305/1506] update to use usePrevious hook --- src/components/OptionListContextProvider.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index 5d4e704262f5..473c0f5bb083 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -51,7 +51,7 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp }); const personalDetails = usePersonalDetails(); - const prevPersonalDetails = useRef(personalDetails); + const prevPersonalDetails = usePrevious(personalDetails); const prevReports = usePrevious(reports); /** @@ -121,7 +121,7 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp }> = []; Object.keys(personalDetails).forEach((accoutID) => { - const prevPersonalDetail = prevPersonalDetails.current?.[accoutID]; + const prevPersonalDetail = prevPersonalDetails?.[accoutID]; const personalDetail = personalDetails?.[accoutID]; if (isEqualPersonalDetail(prevPersonalDetail, personalDetail)) { @@ -153,7 +153,6 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp return newOptions; }); - prevPersonalDetails.current = personalDetails; // eslint-disable-next-line react-hooks/exhaustive-deps }, [personalDetails]); From 4d872eb62691bc0f30b5ec0f9605e9a2926691b1 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 17 Apr 2024 10:10:13 +0200 Subject: [PATCH 0306/1506] Minor improvements --- src/libs/PolicyUtils.ts | 2 +- src/libs/actions/IOU.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 35abc7258d45..48e0032da8cd 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -11,8 +11,8 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import getPolicyIDFromState from './Navigation/getPolicyIDFromState'; import Navigation, {navigationRef} from './Navigation/Navigation'; import type {RootStackParamList, State} from './Navigation/types'; -import {getPersonalDetailByEmail} from './PersonalDetailsUtils'; import * as NetworkStore from './Network/NetworkStore'; +import {getPersonalDetailByEmail} from './PersonalDetailsUtils'; type MemberEmailsToAccountIDs = Record; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index ab39072a56b8..9916afbf3a96 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -856,7 +856,7 @@ function buildOnyxDataForInvoice( optimisticPolicyRecentlyUsedTags: OnyxTypes.RecentlyUsedTags, isNewChatReport: boolean, transactionThreadReport: OptimisticChatReport, - transactionThreadCreatedReportAction: OptimisticCreatedReportAction, + transactionThreadCreatedReportAction: OptimisticCreatedReportAction | EmptyObject, inviteReportAction?: OptimisticInviteReportAction, policy?: OnyxEntry, policyTagList?: OnyxEntry, From a9d6b4bd5ad26334c9645abe424f70ed7b94e33e Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 17 Apr 2024 16:35:14 +0700 Subject: [PATCH 0307/1506] fix add custom icon right button --- src/components/Button/index.tsx | 20 ++++++++++++------- .../ButtonWithDropdownMenu/index.tsx | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 68f1aac41a5b..25a9fcbc215a 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -115,6 +115,9 @@ type ButtonProps = Partial & { /** Boolean whether to display the right icon */ shouldShowRightIcon?: boolean; + + /** The custom icon to display to the right of the text */ + customRightIcon?: React.ReactNode; }; type KeyboardShortcutComponentProps = Pick; @@ -198,6 +201,7 @@ function Button( id = '', accessibilityLabel = '', + customRightIcon, ...rest }: ButtonProps, ref: ForwardedRef, @@ -253,13 +257,15 @@ function Button( {shouldShowRightIcon && ( - + {customRightIcon ?? ( + + )} )} diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index 9c2ab4b36734..16c9804f79d0 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -98,7 +98,7 @@ function ButtonWithDropdownMenu({ medium={!isButtonSizeLarge} innerStyles={[innerStyleDropButton, !isSplit && styles.dropDownButtonCartIconView]} enterKeyEventListenerPriority={enterKeyEventListenerPriority} - iconRight={getIconRightButton} + customRightIcon={getIconRightButton()} shouldShowRightIcon={!isSplit} /> From 23dd180b89f7b656b5c2138b49bac1c0a1a4c3ac Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 17 Apr 2024 11:48:47 +0200 Subject: [PATCH 0308/1506] improve getInvoicesChatName --- src/libs/ReportUtils.ts | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 28edbd79a9ba..4f50562de698 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3011,26 +3011,20 @@ function getInvoicesChatName(report: OnyxEntry): string { const invoiceReceiver = report?.invoiceReceiver; const isIndividual = invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.INDIVIDUAL; const invoiceReceiverAccountID = isIndividual ? invoiceReceiver.accountID : -1; - const policyID = isIndividual ? '' : invoiceReceiver?.policyID ?? ''; - let isReceiver = false; - - if (isIndividual && invoiceReceiverAccountID === currentUserAccountID) { - isReceiver = true; - } + const invoiceReceiverPolicyID = isIndividual ? '' : invoiceReceiver?.policyID ?? ''; + const isCurrentUserReceiver = + (isIndividual && invoiceReceiverAccountID === currentUserAccountID) || (!isIndividual && PolicyUtils.isPolicyEmployee(invoiceReceiverPolicyID, allPolicies)); - if (!isIndividual && PolicyUtils.isPolicyEmployee(policyID, allPolicies)) { - isReceiver = true; - } - - if (isReceiver) { - return getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]); + if (isCurrentUserReceiver) { + return getPolicyName(report); } if (isIndividual) { return PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[invoiceReceiverAccountID]); } - return getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]); + // TODO: Check this flow in a scope of the Invoice V0.3 + return getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiverPolicyID}`]); } /** From 12bd9c484f46510441726eb5626cb974153b6f5b Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 17 Apr 2024 11:52:24 +0200 Subject: [PATCH 0309/1506] add comment --- src/libs/ReportUtils.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 4f50562de698..8a0b1d43adf6 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2977,6 +2977,11 @@ function getAdminRoomInvitedParticipants(parentReportAction: ReportAction | Reco return roomName ? `${verb} ${users} ${preposition} ${roomName}` : `${verb} ${users}`; } +/** + * Get the invoice payer name based on its type: + * - Individual - a receiver display name. + * - Policy - a receiver policy name. + */ function getInvoicePayerName(report: OnyxEntry): string { const invoiceReceiver = report?.invoiceReceiver; const isIndividual = invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.INDIVIDUAL; From 7b711429ea0a4ff3a965b531a7097aacd3e945bc Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 17 Apr 2024 11:53:03 +0200 Subject: [PATCH 0310/1506] Revert "integrate deleted invoice message" This reverts commit 4e81a6c86a7a00ffbaa7692f17cf82d29f0cfb24. --- src/components/ReportActionItem/MoneyRequestAction.tsx | 2 -- src/languages/en.ts | 1 - src/languages/es.ts | 1 - src/pages/home/report/ReportActionItem.tsx | 2 -- 4 files changed, 6 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.tsx b/src/components/ReportActionItem/MoneyRequestAction.tsx index 9bef637bf292..7d9ba2697c7a 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.tsx +++ b/src/components/ReportActionItem/MoneyRequestAction.tsx @@ -116,8 +116,6 @@ function MoneyRequestAction({ message = 'parentReportAction.reversedTransaction'; } else if (isTrackExpenseAction) { message = 'parentReportAction.deletedExpense'; - } else if (action.childType === CONST.REPORT.TYPE.INVOICE) { - message = 'parentReportAction.deletedInvoice'; } else { message = 'parentReportAction.deletedRequest'; } diff --git a/src/languages/en.ts b/src/languages/en.ts index b6de47a3aaf0..05313113c521 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2541,7 +2541,6 @@ export default { reversedTransaction: '[Reversed transaction]', deletedTask: '[Deleted task]', hiddenMessage: '[Hidden message]', - deletedInvoice: '[Deleted invoice]', }, threads: { thread: 'Thread', diff --git a/src/languages/es.ts b/src/languages/es.ts index 6b963a874599..f99eaa4eef10 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3033,7 +3033,6 @@ export default { reversedTransaction: '[Transacción anulada]', deletedTask: '[Tarea eliminada]', hiddenMessage: '[Mensaje oculto]', - deletedInvoice: '[Factura eliminada]', }, threads: { thread: 'Hilo', diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index 06e222d86928..6a6eca9fb734 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -738,8 +738,6 @@ function ReportActionItem({ message = 'parentReportAction.reversedTransaction'; } else if (ReportActionsUtils.isTrackExpenseAction(parentReportAction)) { message = 'parentReportAction.deletedExpense'; - } else if (parentReportAction?.childType === CONST.REPORT.TYPE.INVOICE) { - message = 'parentReportAction.deletedInvoice'; } else { message = 'parentReportAction.deletedRequest'; } From 78f5f642773d697474736dbe2cbcf4eea6384d8b Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 17 Apr 2024 11:54:01 +0200 Subject: [PATCH 0311/1506] Revert "integrate deleted invoice message in getTransactionReportName" This reverts commit 19b54e34b25b9d9ddcb7c27ae4b237cdcac744ff. --- src/libs/ReportUtils.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8a0b1d43adf6..4f8a16246d61 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -191,9 +191,6 @@ type OptimisticIOUReportAction = Pick< | 'receipt' | 'whisperedToAccountIDs' | 'childReportID' - | 'childType' - | 'childVisibleActionCount' - | 'childCommenterCount' >; type ReportRouteParams = { @@ -2691,10 +2688,6 @@ function getTransactionReportName(reportAction: OnyxEntry Date: Wed, 17 Apr 2024 11:54:30 +0200 Subject: [PATCH 0312/1506] clear redundant participant props --- src/types/onyx/Report.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 2bad31bc372c..36f124a4b826 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -26,9 +26,6 @@ type PendingChatMember = { type Participant = { hidden?: boolean; role?: 'admin' | 'member'; - // TODO: Confirm - type?: 'policy' | 'individual'; - policyID?: string; }; type Participants = Record; From 75f8b2386c9056ec06bd781bb00d61b3afa4ffab Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 17 Apr 2024 11:58:26 +0200 Subject: [PATCH 0313/1506] Update optimistic invoice room creation to include current user as a member --- src/libs/actions/IOU.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 9916afbf3a96..130a43a79319 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1619,6 +1619,7 @@ function getDeleteTrackExpenseInformation( /** Gathers all the data needed to create an invoice. */ function getSendInvoiceInformation( transaction: OnyxEntry, + currentUserAccountID: number, invoiceChatReport?: OnyxEntry, receipt?: Receipt, policy?: OnyxEntry, @@ -1641,7 +1642,7 @@ function getSendInvoiceInformation( if (!chatReport) { isNewChatReport = true; - chatReport = ReportUtils.buildOptimisticChatReport([receiverAccountID], CONST.REPORT.DEFAULT_REPORT_NAME, CONST.REPORT.CHAT_TYPE.INVOICE, senderWorkspaceID); + chatReport = ReportUtils.buildOptimisticChatReport([receiverAccountID, currentUserAccountID], CONST.REPORT.DEFAULT_REPORT_NAME, CONST.REPORT.CHAT_TYPE.INVOICE, senderWorkspaceID); } // STEP 3: Create a new optimistic invoice report. @@ -3341,7 +3342,7 @@ function sendInvoice( optimisticTransactionID, optimisticTransactionThreadReportID, onyxData, - } = getSendInvoiceInformation(transaction, invoiceChatReport, receiptFile, policy, policyTagList, policyCategories); + } = getSendInvoiceInformation(transaction, currentUserAccountID, invoiceChatReport, receiptFile, policy, policyTagList, policyCategories); let parameters: SendInvoiceParams = { senderWorkspaceID, From 5625582f11ab9b8a4c136651fce4319b09ad6689 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 17 Apr 2024 12:16:41 +0200 Subject: [PATCH 0314/1506] sync changes --- src/CONST.ts | 3 ++- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- src/libs/ReportUtils.ts | 32 +++++++++++++++++--------------- src/types/onyx/Report.ts | 23 +++++++++++++---------- 5 files changed, 34 insertions(+), 28 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 0dbf26851ead..95056970457b 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -52,7 +52,7 @@ const chatTypes = { POLICY_ROOM: 'policyRoom', POLICY_EXPENSE_CHAT: 'policyExpenseChat', SELF_DM: 'selfDM', - INVOICE: 'invoiceRoom', + INVOICE: 'invoice', } as const; // Explicit type annotation is required @@ -1423,6 +1423,7 @@ const CONST = { SPLIT: 'split', REQUEST: 'request', TRACK_EXPENSE: 'track-expense', + INVOICE: 'invoice', }, REQUEST_TYPE: { DISTANCE: 'distance', diff --git a/src/languages/en.ts b/src/languages/en.ts index 05313113c521..f9c845b42a5a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -505,6 +505,7 @@ export default { beginningOfChatHistoryAnnounceRoomPartTwo: ({workspaceName}: BeginningOfChatHistoryAnnounceRoomPartTwo) => ` to chat about anything ${workspaceName} related.`, beginningOfChatHistoryUserRoomPartOne: 'Collaboration starts here! 🎉\nUse this space to chat about anything ', beginningOfChatHistoryUserRoomPartTwo: ' related.', + beginningOfChatHistoryInvoiceRoom: 'Collaboration starts here! 🎉 Use this room to view, discuss, and pay invoices.', beginningOfChatHistory: 'This is the beginning of your chat with ', beginningOfChatHistoryPolicyExpenseChatPartOne: 'Collaboration between ', beginningOfChatHistoryPolicyExpenseChatPartTwo: ' and ', @@ -522,7 +523,6 @@ export default { // eslint-disable-next-line @typescript-eslint/naming-convention 'track-expense': 'track an expense', }, - beginningOfChatHistoryInvoiceRoom: 'Collaboration starts here! 🎉 \nUse this room to view, discuss, and pay invoices.', }, reportAction: { asCopilot: 'as copilot for', diff --git a/src/languages/es.ts b/src/languages/es.ts index f99eaa4eef10..568e14e6b241 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -501,6 +501,7 @@ export default { beginningOfChatHistoryAnnounceRoomPartTwo: ({workspaceName}: BeginningOfChatHistoryAnnounceRoomPartTwo) => ` para chatear sobre cualquier cosa relacionada con ${workspaceName}.`, beginningOfChatHistoryUserRoomPartOne: '¡Este es el lugar para colaborar! 🎉\nUsa este espacio para chatear sobre cualquier cosa relacionada con ', beginningOfChatHistoryUserRoomPartTwo: '.', + beginningOfChatHistoryInvoiceRoom: '¡Este es el lugar para colaborar! 🎉 Utilice esta sala para ver, discutir y pagar facturas.', beginningOfChatHistory: 'Aquí comienzan tus conversaciones con ', beginningOfChatHistoryPolicyExpenseChatPartOne: '¡La colaboración entre ', beginningOfChatHistoryPolicyExpenseChatPartTwo: ' y ', @@ -518,7 +519,6 @@ export default { // eslint-disable-next-line @typescript-eslint/naming-convention 'track-expense': 'rastrear un gasto', }, - beginningOfChatHistoryInvoiceRoom: '¡Este es el lugar para colaborar! 🎉\nUsa esta sala para ver, discutir y pagar facturas.', }, reportAction: { asCopilot: 'como copiloto de', diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 4f8a16246d61..76693877b462 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -141,6 +141,7 @@ type OptimisticAddCommentReportAction = Pick< | 'childStatusNum' | 'childStateNum' | 'errors' + | 'whisperedToAccountIDs' > & {isOptimisticAction: boolean}; type OptimisticReportAction = { @@ -283,6 +284,7 @@ type OptimisticChatReport = Pick< | 'description' | 'writeCapability' | 'avatarUrl' + | 'invoiceReceiver' > & { isOptimisticReport: true; }; @@ -675,6 +677,13 @@ function isChatReport(report: OnyxEntry | EmptyObject): boolean { return report?.type === CONST.REPORT.TYPE.CHAT; } +/** + * Checks if a report is an invoice report. + */ +function isInvoiceReport(report: OnyxEntry | EmptyObject): boolean { + return report?.type === CONST.REPORT.TYPE.INVOICE; +} + /** * Checks if a report is an Expense report. */ @@ -862,6 +871,13 @@ function isPolicyExpenseChat(report: OnyxEntry | Participant | EmptyObje return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT || (report?.isPolicyExpenseChat ?? false); } +/** + * Whether the provided report is an invoice room chat. + */ +function isInvoiceRoom(report: OnyxEntry): boolean { + return getChatType(report) === CONST.REPORT.CHAT_TYPE.INVOICE; +} + /** * Whether the provided report belongs to a Control policy and is an expense chat */ @@ -906,20 +922,6 @@ function isPaidGroupPolicyExpenseReport(report: OnyxEntry): boolean { return isExpenseReport(report) && isPaidGroupPolicy(report); } -/** - * Check if Report is an invoice room - */ -function isInvoiceRoom(report: OnyxEntry): boolean { - return getChatType(report) === CONST.REPORT.CHAT_TYPE.INVOICE; -} - -/** - * Check if Report is an invoice report - */ -function isInvoiceReport(report: OnyxEntry | EmptyObject): boolean { - return report?.type === CONST.REPORT.TYPE.INVOICE; -} - /** * Checks if the supplied report is an invoice report in Open state and status. */ @@ -2218,7 +2220,7 @@ function hasNonReimbursableTransactions(iouReportID: string | undefined): boolea function getMoneyRequestSpendBreakdown(report: OnyxEntry, allReportsDict: OnyxCollection = null): SpendBreakdown { const allAvailableReports = allReportsDict ?? allReports; let moneyRequestReport; - if (isMoneyRequestReport(report)) { + if (isMoneyRequestReport(report) || isInvoiceReport(report)) { moneyRequestReport = report; } if (allAvailableReports && report?.iouReportID) { diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 36f124a4b826..344b7df5b2eb 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -28,6 +28,16 @@ type Participant = { role?: 'admin' | 'member'; }; +type InvoiceReceiver = + | { + type: 'individual'; + accountID: number; + } + | { + type: 'policy'; + policyID: string; + }; + type Participants = Record; type Report = OnyxCommon.OnyxValueWithOfflineFeedback< @@ -128,6 +138,9 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< /** Report cached total */ cachedTotal?: string; + /** Invoice room receiver data */ + invoiceReceiver?: InvoiceReceiver; + lastMessageTranslationKey?: string; parentReportID?: string; parentReportActionID?: string; @@ -186,16 +199,6 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< transactionThreadReportID?: string; fieldList?: Record; - - invoiceReceiver?: - | { - type: typeof CONST.INVOICE_RECEIVER_TYPE.INDIVIDUAL; - accountID: number; - } - | { - type: typeof CONST.INVOICE_RECEIVER_TYPE.POLICY; - policyID: string; - }; }, PolicyReportField['fieldID'] >; From 1b92e52f10d874af91ab346590e315aa640be244 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 17 Apr 2024 12:18:30 +0200 Subject: [PATCH 0315/1506] update invoice receiver const --- src/CONST.ts | 9 ++++----- src/libs/ReportUtils.ts | 10 +++++----- src/libs/actions/IOU.ts | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 95056970457b..6467769263dd 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -839,6 +839,10 @@ const CONST = { OWNER_EMAIL_FAKE: '__FAKE__', OWNER_ACCOUNT_ID_FAKE: 0, DEFAULT_REPORT_NAME: 'Chat Report', + INVOICE_RECEIVER_TYPE: { + INDIVIDUAL: 'individual', + BUSINESS: 'policy', + }, }, NEXT_STEP: { FINISHED: 'Finished!', @@ -4353,11 +4357,6 @@ const CONST = { MAX_TAX_RATE_INTEGER_PLACES: 4, MAX_TAX_RATE_DECIMAL_PLACES: 4, - - INVOICE_RECEIVER_TYPE: { - INDIVIDUAL: 'individual', - POLICY: 'policy', - }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 76693877b462..b2a1fd53afeb 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1971,7 +1971,7 @@ function getIcons( const invoiceRoomReport = getReport(report.chatReportID); const icons = [getWorkspaceIcon(invoiceRoomReport, policy)]; - if (invoiceRoomReport?.invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.INDIVIDUAL) { + if (invoiceRoomReport?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL) { icons.push(...getIconsForParticipants([invoiceRoomReport?.invoiceReceiver.accountID], personalDetails)); return icons; @@ -2979,7 +2979,7 @@ function getAdminRoomInvitedParticipants(parentReportAction: ReportAction | Reco */ function getInvoicePayerName(report: OnyxEntry): string { const invoiceReceiver = report?.invoiceReceiver; - const isIndividual = invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.INDIVIDUAL; + const isIndividual = invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL; if (isIndividual) { return PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[invoiceReceiver.accountID]); @@ -3009,7 +3009,7 @@ function getReportActionMessage(reportAction: ReportAction | EmptyObject, parent */ function getInvoicesChatName(report: OnyxEntry): string { const invoiceReceiver = report?.invoiceReceiver; - const isIndividual = invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.INDIVIDUAL; + const isIndividual = invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL; const invoiceReceiverAccountID = isIndividual ? invoiceReceiver.accountID : -1; const invoiceReceiverPolicyID = isIndividual ? '' : invoiceReceiver?.policyID ?? ''; const isCurrentUserReceiver = @@ -3032,7 +3032,7 @@ function getInvoicesChatName(report: OnyxEntry): string { */ function getInvoicesChatSubtitle(report: OnyxEntry): string { const invoiceReceiver = report?.invoiceReceiver; - const isIndividual = invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.INDIVIDUAL; + const isIndividual = invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL; const invoiceReceiverAccountID = isIndividual ? invoiceReceiver.accountID : -1; const policyID = isIndividual ? '' : invoiceReceiver?.policyID ?? ''; let isReceiver = false; @@ -5236,7 +5236,7 @@ function canLeaveRoom(report: OnyxEntry, isPolicyEmployee: boolean): boo } const isReceiverPolicyAdmin = - report?.invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.POLICY ? getPolicy(report?.invoiceReceiver?.policyID)?.role === CONST.POLICY.ROLE.ADMIN : false; + report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS ? getPolicy(report?.invoiceReceiver?.policyID)?.role === CONST.POLICY.ROLE.ADMIN : false; if (isReceiverPolicyAdmin) { return false; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 947a800f5ee5..ae93ec7413ac 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -5328,7 +5328,7 @@ function canIOUBePaid(iouReport: OnyxEntry | EmptyObject, chat } if (ReportUtils.isInvoiceReport(iouReport)) { - if (chatReport?.invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.INDIVIDUAL) { + if (chatReport?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL) { return chatReport?.invoiceReceiver?.accountID === userAccountID; } From fb69172a697a9892cc07f7a6a80581f3f3ee99e6 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 17 Apr 2024 12:22:27 +0200 Subject: [PATCH 0316/1506] improve getInvoicesChatSubtitle --- src/libs/ReportUtils.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b2a1fd53afeb..efd51ef4af99 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3034,18 +3034,11 @@ function getInvoicesChatSubtitle(report: OnyxEntry): string { const invoiceReceiver = report?.invoiceReceiver; const isIndividual = invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL; const invoiceReceiverAccountID = isIndividual ? invoiceReceiver.accountID : -1; - const policyID = isIndividual ? '' : invoiceReceiver?.policyID ?? ''; - let isReceiver = false; - - if (isIndividual && invoiceReceiverAccountID === currentUserAccountID) { - isReceiver = true; - } - - if (!isIndividual && PolicyUtils.isPolicyEmployee(policyID, allPolicies)) { - isReceiver = true; - } + const invoiceReceiverPolicyID = isIndividual ? '' : invoiceReceiver?.policyID ?? ''; + const isCurrentUserReceiver = + (isIndividual && invoiceReceiverAccountID === currentUserAccountID) || (!isIndividual && PolicyUtils.isPolicyEmployee(invoiceReceiverPolicyID, allPolicies)); - if (isReceiver) { + if (isCurrentUserReceiver) { let receiver = ''; if (isIndividual) { @@ -3057,7 +3050,8 @@ function getInvoicesChatSubtitle(report: OnyxEntry): string { return Localize.translateLocal('workspace.invoices.invoicesTo', {receiver}); } - return Localize.translateLocal('workspace.invoices.invoicesFrom', {sender: getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`])}); + // TODO: Check this flow in a scope of the Invoice V0.3 + return Localize.translateLocal('workspace.invoices.invoicesFrom', {sender: getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? ''}`])}); } /** From 2610f14920e41ec6a9b067bab9c9c4dab4c7fa77 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 17 Apr 2024 12:25:59 +0200 Subject: [PATCH 0317/1506] revert extra changes --- src/libs/ReportUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index efd51ef4af99..f41b310c2f2e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -141,7 +141,6 @@ type OptimisticAddCommentReportAction = Pick< | 'childStatusNum' | 'childStateNum' | 'errors' - | 'whisperedToAccountIDs' > & {isOptimisticAction: boolean}; type OptimisticReportAction = { @@ -192,6 +191,8 @@ type OptimisticIOUReportAction = Pick< | 'receipt' | 'whisperedToAccountIDs' | 'childReportID' + | 'childVisibleActionCount' + | 'childCommenterCount' >; type ReportRouteParams = { From b8e9d4368a6d2e90e3114cd8ea291d469a11330f Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 17 Apr 2024 12:31:31 +0200 Subject: [PATCH 0318/1506] feat: add proper copies --- src/languages/en.ts | 6 ++++++ src/languages/es.ts | 6 ++++++ .../EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx | 2 +- src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx | 2 +- .../PersonalInfo/substeps/SocialSecurityNumber.tsx | 4 ++-- .../utils/getInitialSubstepForPersonalInfo.ts | 4 ++-- 6 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 495ec8426ea8..bc4192735cea 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1702,6 +1702,12 @@ export default { address: 'Address', letsDoubleCheck: "Let's double check that everything looks right.", byAddingThisBankAccount: 'By adding this bank account, you confirm that you have read, understand and accept', + whatsYourLegalName: 'What’s your legal name?', + whatsYourDOB: 'What’s your date of birth?', + whatsYourAddress: 'What’s your address?', + noPOBoxesPlease: 'No PO boxes or mail-drop addresses, please.', + whatsYourSSN: 'What are the last four digits of your Social Security Number?', + noPersonalChecks: 'Don’t worry, no personal credit checks here!', whatsYourPhoneNumber: 'What’s your phone number?', weNeedThisToVerify: 'We need this to verify your wallet.', }, diff --git a/src/languages/es.ts b/src/languages/es.ts index 74076c22393f..37c51d2148aa 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1727,6 +1727,12 @@ export default { address: 'Dirección', letsDoubleCheck: 'Revisemos que todo esté bien', byAddingThisBankAccount: 'Añadiendo esta cuenta bancaria, confirmas que has leído, entendido y aceptado', + whatsYourLegalName: '¿Cuál es tu nombre legal?', + whatsYourDOB: '¿Cuál es tu fecha de nacimiento?', + whatsYourAddress: '¿Cuál es tu dirección?', + noPOBoxesPlease: 'Nada de apartados de correos ni direcciones de envío, por favor.', + whatsYourSSN: '¿Cuáles son los últimos 4 dígitos de tu número de la seguridad social?', + noPersonalChecks: 'No te preocupes, no hacemos verificaciones de crédito personales.', whatsYourPhoneNumber: '¿Cuál es tu número de teléfono?', weNeedThisToVerify: 'Necesitamos esto para verificar tu billetera.', }, diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx index a42b9c76d1a9..f83edb706686 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx @@ -67,7 +67,7 @@ function DateOfBirth({walletAdditionalDetails, onNext, isEditing}: DateOfBirthPr style={[styles.mh5, styles.flexGrow2, styles.justifyContentBetween]} submitButtonStyles={[styles.pb5, styles.mb0]} > - {translate('personalInfoStep.enterYourDateOfBirth')} + {translate('personalInfoStep.whatsYourDOB')} - {translate('personalInfoStep.enterYourLegalFirstAndLast')} + {translate('personalInfoStep.whatsYourLegalName')} - {translate('personalInfoStep.enterTheLast4')} - {translate('personalInfoStep.dontWorry')} + {translate('personalInfoStep.whatsYourSSN')} + {translate('personalInfoStep.noPersonalChecks')} Date: Wed, 17 Apr 2024 12:32:51 +0200 Subject: [PATCH 0319/1506] simplify invoice room subtitle --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f41b310c2f2e..417cfdc09bf9 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3149,7 +3149,7 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu */ function getChatRoomSubtitle(report: OnyxEntry): string | undefined { if (isInvoiceRoom(report)) { - return getInvoicesChatSubtitle(report); + return Localize.translateLocal('workspace.common.invoices'); } if (isChatThread(report)) { return ''; From 04ee7b1fb55945358135fcb907271a11a34be45e Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 17 Apr 2024 12:33:55 +0200 Subject: [PATCH 0320/1506] Revert "integrate report subtitle for invoice room" This reverts commit a524346f9402a98b9b6900a167c9ca15a1569078. --- src/languages/en.ts | 4 ---- src/languages/es.ts | 4 ---- src/languages/types.ts | 6 ------ src/libs/ReportUtils.ts | 31 +++---------------------------- 4 files changed, 3 insertions(+), 42 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index f9c845b42a5a..d2f76ee4ed44 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -30,8 +30,6 @@ import type { GoBackMessageParams, GoToRoomParams, InstantSummaryParams, - InvoicesFromParams, - InvoicesToParams, LocalTimeParams, LoggedInAsParams, LogSizeParams, @@ -2148,8 +2146,6 @@ export default { unlockVBACopy: "You're all set to accept payments by ACH or credit card!", viewUnpaidInvoices: 'View unpaid invoices', sendInvoice: 'Send invoice', - invoicesFrom: ({sender}: InvoicesFromParams) => `Invoices from ${sender}`, - invoicesTo: ({receiver}: InvoicesToParams) => `Invoices to ${receiver}`, }, travel: { unlockConciergeBookingTravel: 'Unlock Concierge travel booking', diff --git a/src/languages/es.ts b/src/languages/es.ts index 568e14e6b241..141e3ad3db91 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -29,8 +29,6 @@ import type { GoBackMessageParams, GoToRoomParams, InstantSummaryParams, - InvoicesFromParams, - InvoicesToParams, LocalTimeParams, LoggedInAsParams, LogSizeParams, @@ -2176,8 +2174,6 @@ export default { unlockVBACopy: '¡Todo listo para recibir pagos por transferencia o con tarjeta!', viewUnpaidInvoices: 'Ver facturas emitidas pendientes', sendInvoice: 'Enviar factura', - invoicesFrom: ({sender}: InvoicesFromParams) => `Facturas de ${sender}`, - invoicesTo: ({receiver}: InvoicesToParams) => `Facturas a ${receiver}`, }, travel: { unlockConciergeBookingTravel: 'Desbloquea la reserva de viajes con Concierge', diff --git a/src/languages/types.ts b/src/languages/types.ts index 8c4d287b7dfc..59e1bfe40af2 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -251,10 +251,6 @@ type ViolationsTaxOutOfPolicyParams = {taxName?: string}; type TaskCreatedActionParams = {title: string}; -type InvoicesFromParams = {sender: string}; - -type InvoicesToParams = {receiver: string}; - /* Translation Object types */ // eslint-disable-next-line @typescript-eslint/no-explicit-any type TranslationBaseValue = string | string[] | ((...args: any[]) => string); @@ -407,6 +403,4 @@ export type { ZipCodeExampleFormatParams, LogSizeParams, HeldRequestParams, - InvoicesFromParams, - InvoicesToParams, }; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 417cfdc09bf9..0ba38d8f91de 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3028,33 +3028,6 @@ function getInvoicesChatName(report: OnyxEntry): string { return getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiverPolicyID}`]); } -/** - * Get the subtitle for an invoice room. - */ -function getInvoicesChatSubtitle(report: OnyxEntry): string { - const invoiceReceiver = report?.invoiceReceiver; - const isIndividual = invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL; - const invoiceReceiverAccountID = isIndividual ? invoiceReceiver.accountID : -1; - const invoiceReceiverPolicyID = isIndividual ? '' : invoiceReceiver?.policyID ?? ''; - const isCurrentUserReceiver = - (isIndividual && invoiceReceiverAccountID === currentUserAccountID) || (!isIndividual && PolicyUtils.isPolicyEmployee(invoiceReceiverPolicyID, allPolicies)); - - if (isCurrentUserReceiver) { - let receiver = ''; - - if (isIndividual) { - receiver = PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[invoiceReceiverAccountID]); - } else { - receiver = getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiver?.policyID}`]); - } - - return Localize.translateLocal('workspace.invoices.invoicesTo', {receiver}); - } - - // TODO: Check this flow in a scope of the Invoice V0.3 - return Localize.translateLocal('workspace.invoices.invoicesFrom', {sender: getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? ''}`])}); -} - /** * Get the title for a report. */ @@ -3167,6 +3140,9 @@ function getChatRoomSubtitle(report: OnyxEntry): string | undefined { if (isArchivedRoom(report)) { return report?.oldPolicyName ?? ''; } + if (isInvoiceRoom(report)) { + return Localize.translateLocal('workspace.common.invoices'); + } return getPolicyName(report); } @@ -6208,7 +6184,6 @@ export { getDisplayNamesWithTooltips, getInvoicePayerName, getInvoicesChatName, - getInvoicesChatSubtitle, getReportName, getReport, getReportNotificationPreference, From c135183a053761716647812c8686e1fe6c81295d Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 17 Apr 2024 13:18:35 +0200 Subject: [PATCH 0321/1506] refactor: make useStepSubmit generic --- .../useReimbursementAccountStepFormSubmit.ts | 39 +++++------------- src/hooks/useStepFormSubmit.ts | 40 +++++++++++++++++++ ...seWalletAdditionalDetailsStepFormSubmit.ts | 27 +++++++++++++ .../PersonalInfo/substeps/Address.tsx | 5 +-- .../PersonalInfo/substeps/DateOfBirth.tsx | 5 +-- .../PersonalInfo/substeps/FullName.tsx | 5 +-- .../PersonalInfo/substeps/PhoneNumber.tsx | 5 +-- .../substeps/SocialSecurityNumber.tsx | 5 +-- 8 files changed, 87 insertions(+), 44 deletions(-) create mode 100644 src/hooks/useStepFormSubmit.ts create mode 100644 src/hooks/useWalletAdditionalDetailsStepFormSubmit.ts diff --git a/src/hooks/useReimbursementAccountStepFormSubmit.ts b/src/hooks/useReimbursementAccountStepFormSubmit.ts index 85a9b21b4d8d..bd17bfaa1234 100644 --- a/src/hooks/useReimbursementAccountStepFormSubmit.ts +++ b/src/hooks/useReimbursementAccountStepFormSubmit.ts @@ -1,46 +1,27 @@ -import {useCallback} from 'react'; -import type {FormOnyxKeys, FormOnyxValues} from '@components/Form/types'; -import * as FormActions from '@userActions/FormActions'; +import type {FormOnyxKeys} from '@components/Form/types'; +import useStepFormSubmit from '@hooks/useStepFormSubmit'; import type {OnyxFormKey} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; import type {SubStepProps} from './useSubStep/types'; type UseReimbursementAccountStepFormSubmitParams = Pick & { formId?: OnyxFormKey; - fieldIds: Array>; + fieldIds: Array>; shouldSaveDraft: boolean; }; /** * Hook for handling submit method in ReimbursementAccount substeps. * When user is in editing mode we should save values only when user confirm that - * @param formId - ID for particular form * @param onNext - callback * @param fieldIds - field IDs for particular step * @param shouldSaveDraft - if we should save draft values */ -export default function useReimbursementAccountStepFormSubmit({ - formId = ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, - onNext, - fieldIds, - shouldSaveDraft, -}: UseReimbursementAccountStepFormSubmitParams) { - return useCallback( - (values: FormOnyxValues) => { - if (shouldSaveDraft) { - const stepValues = fieldIds.reduce( - (acc, key) => ({ - ...acc, - [key]: values[key], - }), - {}, - ); - - FormActions.setDraftValues(formId, stepValues); - } - - onNext(); - }, - [onNext, formId, fieldIds, shouldSaveDraft], - ); +export default function useReimbursementAccountStepFormSubmit({onNext, fieldIds, shouldSaveDraft}: UseReimbursementAccountStepFormSubmitParams) { + return useStepFormSubmit({ + formId: ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, + onNext, + fieldIds, + shouldSaveDraft, + }); } diff --git a/src/hooks/useStepFormSubmit.ts b/src/hooks/useStepFormSubmit.ts new file mode 100644 index 000000000000..86d754bf2fc9 --- /dev/null +++ b/src/hooks/useStepFormSubmit.ts @@ -0,0 +1,40 @@ +import {useCallback} from 'react'; +import type {FormOnyxKeys, FormOnyxValues} from '@components/Form/types'; +import * as FormActions from '@userActions/FormActions'; +import type {OnyxFormKey, OnyxFormValuesMapping} from '@src/ONYXKEYS'; +import type {SubStepProps} from './useSubStep/types'; + +type UseStepFormSubmitParams = Pick & { + formId: OnyxFormKey; + fieldIds: Array>; + shouldSaveDraft: boolean; +}; + +/** + * Hook for handling submit method in substeps. + * When user is in editing mode we should save values only when user confirm that + * @param formId - ID for particular form + * @param onNext - callback + * @param fieldIds - field IDs for particular step + * @param shouldSaveDraft - if we should save draft values + */ +export default function useStepFormSubmit({formId, onNext, fieldIds, shouldSaveDraft}: UseStepFormSubmitParams) { + return useCallback( + (values: FormOnyxValues) => { + if (shouldSaveDraft) { + const stepValues = fieldIds.reduce( + (acc, key) => ({ + ...acc, + [key]: values[key], + }), + {}, + ); + + FormActions.setDraftValues(formId, stepValues); + } + + onNext(); + }, + [onNext, formId, fieldIds, shouldSaveDraft], + ); +} diff --git a/src/hooks/useWalletAdditionalDetailsStepFormSubmit.ts b/src/hooks/useWalletAdditionalDetailsStepFormSubmit.ts new file mode 100644 index 000000000000..2ca079a506cc --- /dev/null +++ b/src/hooks/useWalletAdditionalDetailsStepFormSubmit.ts @@ -0,0 +1,27 @@ +import type {FormOnyxKeys} from '@components/Form/types'; +import useStepFormSubmit from '@hooks/useStepFormSubmit'; +import type {OnyxFormKey} from '@src/ONYXKEYS'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {SubStepProps} from './useSubStep/types'; + +type UseWalletAdditionalDetailsStepFormSubmitParams = Pick & { + formId?: OnyxFormKey; + fieldIds: Array>; + shouldSaveDraft: boolean; +}; + +/** + * Hook for handling submit method in WalletAdditionalDetails substeps. + * When user is in editing mode we should save values only when user confirm that + * @param onNext - callback + * @param fieldIds - field IDs for particular step + * @param shouldSaveDraft - if we should save draft values + */ +export default function useWalletAdditionalDetailsStepFormSubmit({onNext, fieldIds, shouldSaveDraft}: UseWalletAdditionalDetailsStepFormSubmitParams) { + return useStepFormSubmit({ + formId: ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, + onNext, + fieldIds, + shouldSaveDraft, + }); +} diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/Address.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/Address.tsx index 445cf11282b1..d63dd7c08f01 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/Address.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/Address.tsx @@ -6,9 +6,9 @@ import FormProvider from '@components/Form/FormProvider'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; -import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWalletAdditionalDetailsStepFormSubmit from '@hooks/useWalletAdditionalDetailsStepFormSubmit'; import * as ValidationUtils from '@libs/ValidationUtils'; import AddressFormFields from '@pages/ReimbursementAccount/AddressFormFields'; import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; @@ -59,8 +59,7 @@ function Address({walletAdditionalDetails, onNext, isEditing}: AddressProps) { zipCode: walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.ZIP_CODE] ?? '', }; - const handleSubmit = useReimbursementAccountStepFormSubmit({ - formId: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, + const handleSubmit = useWalletAdditionalDetailsStepFormSubmit({ fieldIds: STEP_FIELDS, onNext, shouldSaveDraft: isEditing, diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx index f83edb706686..42cfe4ca11de 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx @@ -8,9 +8,9 @@ import InputWrapper from '@components/Form/InputWrapper'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; -import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWalletAdditionalDetailsStepFormSubmit from '@hooks/useWalletAdditionalDetailsStepFormSubmit'; import * as ValidationUtils from '@libs/ValidationUtils'; import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; import CONST from '@src/CONST'; @@ -51,8 +51,7 @@ function DateOfBirth({walletAdditionalDetails, onNext, isEditing}: DateOfBirthPr const dobDefaultValue = walletAdditionalDetails?.[PERSONAL_INFO_DOB_KEY] ?? walletAdditionalDetails?.[PERSONAL_INFO_DOB_KEY] ?? ''; - const handleSubmit = useReimbursementAccountStepFormSubmit({ - formId: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, + const handleSubmit = useWalletAdditionalDetailsStepFormSubmit({ fieldIds: STEP_FIELDS, onNext, shouldSaveDraft: isEditing, diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx index 32bd1537ce3b..6dbe855b2721 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx @@ -8,9 +8,9 @@ import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; -import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWalletAdditionalDetailsStepFormSubmit from '@hooks/useWalletAdditionalDetailsStepFormSubmit'; import * as ValidationUtils from '@libs/ValidationUtils'; import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; import CONST from '@src/CONST'; @@ -49,8 +49,7 @@ function FullName({walletAdditionalDetails, onNext, isEditing}: FullNameProps) { lastName: walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.LAST_NAME] ?? '', }; - const handleSubmit = useReimbursementAccountStepFormSubmit({ - formId: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, + const handleSubmit = useWalletAdditionalDetailsStepFormSubmit({ fieldIds: STEP_FIELDS, onNext, shouldSaveDraft: isEditing, diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumber.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumber.tsx index b465efe6fe79..9b5f8cf0a611 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumber.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumber.tsx @@ -8,9 +8,9 @@ import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; -import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWalletAdditionalDetailsStepFormSubmit from '@hooks/useWalletAdditionalDetailsStepFormSubmit'; import * as ValidationUtils from '@libs/ValidationUtils'; import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; import CONST from '@src/CONST'; @@ -42,8 +42,7 @@ function PhoneNumber({walletAdditionalDetails, onNext, isEditing}: PhoneNumberPr const defaultPhoneNumber = walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.PHONE_NUMBER] ?? ''; - const handleSubmit = useReimbursementAccountStepFormSubmit({ - formId: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, + const handleSubmit = useWalletAdditionalDetailsStepFormSubmit({ fieldIds: STEP_FIELDS, onNext, shouldSaveDraft: isEditing, diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx index fe23f4b9cbea..de9c9ce25938 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx @@ -8,9 +8,9 @@ import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; -import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWalletAdditionalDetailsStepFormSubmit from '@hooks/useWalletAdditionalDetailsStepFormSubmit'; import * as ValidationUtils from '@libs/ValidationUtils'; import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; import CONST from '@src/CONST'; @@ -43,8 +43,7 @@ function SocialSecurityNumber({walletAdditionalDetails, onNext, isEditing}: Soci const defaultSsnLast4 = walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.SSN_LAST_4] ?? ''; - const handleSubmit = useReimbursementAccountStepFormSubmit({ - formId: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, + const handleSubmit = useWalletAdditionalDetailsStepFormSubmit({ fieldIds: STEP_FIELDS, onNext, shouldSaveDraft: isEditing, From 74260e0750b612f6786a5ce585a5630b92fd76ea Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Wed, 17 Apr 2024 13:31:19 +0200 Subject: [PATCH 0322/1506] make connect to integrations work --- src/CONFIG.ts | 3 ++- src/libs/ApiUtils.ts | 6 ++++-- src/libs/actions/connections/ConnectToXero.ts | 2 +- .../actions/connections/getQuickBooksOnlineSetupLink.ts | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/CONFIG.ts b/src/CONFIG.ts index 76ea18d37d5f..9ed4242d7604 100644 --- a/src/CONFIG.ts +++ b/src/CONFIG.ts @@ -48,6 +48,7 @@ export default { EXPENSIFY: { // Note: This will be EXACTLY what is set for EXPENSIFY_URL whether the proxy is enabled or not. EXPENSIFY_URL: expensifyURL, + SECURE_EXPENSIFY_URL: secureExpensifyUrl, NEW_EXPENSIFY_URL: newExpensifyURL, // The DEFAULT API is the API used by most environments, except staging, where we use STAGING (defined below) @@ -72,7 +73,7 @@ export default { IS_USING_LOCAL_WEB: useNgrok || expensifyURLRoot.includes('dev'), PUSHER: { APP_KEY: get(Config, 'PUSHER_APP_KEY', '268df511a204fbb60884'), - SUFFIX: get(Config, 'PUSHER_DEV_SUFFIX', ''), + SUFFIX: ENVIRONMENT === CONST.ENVIRONMENT.DEV ? get(Config, 'PUSHER_DEV_SUFFIX', '') : '', CLUSTER: 'mt1', }, SITE_TITLE: 'New Expensify', diff --git a/src/libs/ApiUtils.ts b/src/libs/ApiUtils.ts index 0c8fa3f53915..1da9795f333b 100644 --- a/src/libs/ApiUtils.ts +++ b/src/libs/ApiUtils.ts @@ -38,12 +38,14 @@ function getApiRoot(request?: Request): string { const shouldUseSecure = request?.shouldUseSecure ?? false; if (shouldUseStagingServer) { - if (CONFIG.IS_USING_WEB_PROXY) { + if (CONFIG.IS_USING_WEB_PROXY && !request?.shouldSkipWebProxy) { return shouldUseSecure ? proxyConfig.STAGING_SECURE : proxyConfig.STAGING; } return shouldUseSecure ? CONFIG.EXPENSIFY.STAGING_SECURE_API_ROOT : CONFIG.EXPENSIFY.STAGING_API_ROOT; } - + if (request?.shouldSkipWebProxy) { + return shouldUseSecure ? CONFIG.EXPENSIFY.SECURE_EXPENSIFY_URL : CONFIG.EXPENSIFY.EXPENSIFY_URL; + } return shouldUseSecure ? CONFIG.EXPENSIFY.DEFAULT_SECURE_API_ROOT : CONFIG.EXPENSIFY.DEFAULT_API_ROOT; } diff --git a/src/libs/actions/connections/ConnectToXero.ts b/src/libs/actions/connections/ConnectToXero.ts index 0bd3d61a6c6e..abe96603fd85 100644 --- a/src/libs/actions/connections/ConnectToXero.ts +++ b/src/libs/actions/connections/ConnectToXero.ts @@ -2,7 +2,7 @@ import {getCommandURL} from '@libs/ApiUtils'; const getXeroSetupLink = (policyID: string) => { const params = new URLSearchParams({policyID}); - const commandURL = getCommandURL({command: 'ConnectPolicyToXero'}); + const commandURL = getCommandURL({command: 'ConnectPolicyToXero', shouldSkipWebProxy: true}); return commandURL + params.toString(); }; diff --git a/src/libs/actions/connections/getQuickBooksOnlineSetupLink.ts b/src/libs/actions/connections/getQuickBooksOnlineSetupLink.ts index b4317025b057..56b8309f7134 100644 --- a/src/libs/actions/connections/getQuickBooksOnlineSetupLink.ts +++ b/src/libs/actions/connections/getQuickBooksOnlineSetupLink.ts @@ -2,7 +2,7 @@ import {getCommandURL} from '@libs/ApiUtils'; function getQuickBooksOnlineSetupLink(policyID: string) { const params = new URLSearchParams({policyID}); - const commandURL = getCommandURL({command: 'ConnectPolicyToQuickbooksOnline'}); + const commandURL = getCommandURL({command: 'ConnectPolicyToQuickbooksOnline', shouldSkipWebProxy: true}); return commandURL + params.toString(); } From ed736323a070ed66e4e9f8208e56f1c779b60271 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Wed, 17 Apr 2024 13:47:52 +0200 Subject: [PATCH 0323/1506] fix connections empty object bug --- src/pages/workspace/accounting/WorkspaceAccountingPage.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/accounting/WorkspaceAccountingPage.tsx b/src/pages/workspace/accounting/WorkspaceAccountingPage.tsx index ad28cac7032c..c932b38f7642 100644 --- a/src/pages/workspace/accounting/WorkspaceAccountingPage.tsx +++ b/src/pages/workspace/accounting/WorkspaceAccountingPage.tsx @@ -30,6 +30,7 @@ import type {AnchorPosition} from '@styles/index'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, PolicyConnectionSyncProgress} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import ConnectToQuickbooksOnlineButton from './qboConnectionButton'; type WorkspaceAccountingPageOnyxProps = { @@ -44,6 +45,8 @@ type WorkspaceAccountingPageProps = WithPolicyAndFullscreenLoadingProps & policy: OnyxEntry; }; +// const AccountingIntegrations = Object.values(CONST.POLICY.CONNECTIONS.NAME); + function WorkspaceAccountingPage({policy, connectionSyncProgress}: WorkspaceAccountingPageProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -80,7 +83,7 @@ function WorkspaceAccountingPage({policy, connectionSyncProgress}: WorkspaceAcco ); const connectionsMenuItems: MenuItemProps[] = useMemo(() => { - if (!policyConnectedToQbo && !policyConnectedToXero && !isSyncInProgress) { + if (isEmptyObject(policy?.connections) && !isSyncInProgress) { return [ { icon: Expensicons.QBOSquare, From 6db22e200e3c0ca660000d34303c52fa6c04acf0 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 17 Apr 2024 14:26:15 +0200 Subject: [PATCH 0324/1506] Add INVOICE_RECEIVER_TYPE to consts --- src/CONST.ts | 9 +++++++++ src/types/onyx/Report.ts | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index fa66b916ab7c..08ba93053685 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -839,6 +839,10 @@ const CONST = { OWNER_EMAIL_FAKE: '__FAKE__', OWNER_ACCOUNT_ID_FAKE: 0, DEFAULT_REPORT_NAME: 'Chat Report', + INVOICE_RECEIVER_TYPE: { + INDIVIDUAL: 'individual', + BUSINESS: 'policy', + }, }, NEXT_STEP: { FINISHED: 'Finished!', @@ -4353,6 +4357,11 @@ const CONST = { MAX_TAX_RATE_INTEGER_PLACES: 4, MAX_TAX_RATE_DECIMAL_PLACES: 4, + + INVOICE_RECEIVER_TYPE: { + INDIVIDUAL: 'individual', + POLICY: 'policy', + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 344b7df5b2eb..51e3ce5fb37d 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -30,11 +30,11 @@ type Participant = { type InvoiceReceiver = | { - type: 'individual'; + type: typeof CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL; accountID: number; } | { - type: 'policy'; + type: typeof CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS; policyID: string; }; From 173432f67cb35058def61c27c6ab78d90b6087d2 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 17 Apr 2024 14:37:20 +0200 Subject: [PATCH 0325/1506] Code improvements --- src/CONST.ts | 5 -- ...raryForRefactorRequestConfirmationList.tsx | 5 +- src/libs/actions/IOU.ts | 67 +++++++++---------- 3 files changed, 32 insertions(+), 45 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 08ba93053685..6467769263dd 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4357,11 +4357,6 @@ const CONST = { MAX_TAX_RATE_INTEGER_PLACES: 4, MAX_TAX_RATE_DECIMAL_PLACES: 4, - - INVOICE_RECEIVER_TYPE: { - INDIVIDUAL: 'individual', - POLICY: 'policy', - }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index e41c6cc96085..7b444be18da9 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -256,10 +256,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ return allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${senderWorkspaceParticipant?.policyID}`]; }, [allPolicies, pickedParticipants]); - const canUpdateSenderWorkspace = useMemo( - () => PolicyUtils.getActiveAdminWorkspaces(allPolicies).length > 0 && !!transaction?.isFromGlobalCreate, - [allPolicies, transaction?.isFromGlobalCreate], - ); + const canUpdateSenderWorkspace = useMemo(() => PolicyUtils.canSendInvoice(allPolicies) && !!transaction?.isFromGlobalCreate, [allPolicies, transaction?.isFromGlobalCreate]); // A flag for showing the tags field const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]); diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 130a43a79319..d6be557b8107 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -863,24 +863,7 @@ function buildOnyxDataForInvoice( policyCategories?: OnyxEntry, ): [OnyxUpdate[], OnyxUpdate[], OnyxUpdate[]] { const clearedPendingFields = Object.fromEntries(Object.keys(transaction.pendingFields ?? {}).map((key) => [key, null])); - const optimisticData: OnyxUpdate[] = []; - - if (chatReport) { - optimisticData.push({ - // Use SET for new reports because it doesn't exist yet, is faster and we need the data to be available when we navigate to the chat page - onyxMethod: isNewChatReport ? Onyx.METHOD.SET : Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, - value: { - ...chatReport, - lastReadTime: DateUtils.getDBTime(), - lastMessageTranslationKey: '', - iouReportID: iouReport.reportID, - ...(isNewChatReport ? {pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}} : {}), - }, - }); - } - - optimisticData.push( + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, @@ -950,7 +933,22 @@ function buildOnyxDataForInvoice( key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`, value: null, }, - ); + ]; + + if (chatReport) { + optimisticData.push({ + // Use SET for new reports because it doesn't exist yet, is faster and we need the data to be available when we navigate to the chat page + onyxMethod: isNewChatReport ? Onyx.METHOD.SET : Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, + value: { + ...chatReport, + lastReadTime: DateUtils.getDBTime(), + lastMessageTranslationKey: '', + iouReportID: iouReport.reportID, + ...(isNewChatReport ? {pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}} : {}), + }, + }); + } if (optimisticPolicyRecentlyUsedCategories.length) { optimisticData.push({ @@ -968,21 +966,7 @@ function buildOnyxDataForInvoice( }); } - const successData: OnyxUpdate[] = []; - - if (isNewChatReport) { - successData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, - value: { - pendingFields: null, - errorFields: null, - isOptimisticReport: false, - }, - }); - } - - successData.push( + const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, @@ -1007,7 +991,6 @@ function buildOnyxDataForInvoice( pendingFields: clearedPendingFields, }, }, - { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, @@ -1049,7 +1032,19 @@ function buildOnyxDataForInvoice( }, }, }, - ); + ]; + + if (isNewChatReport) { + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, + value: { + pendingFields: null, + errorFields: null, + isOptimisticReport: false, + }, + }); + } const errorKey = DateUtils.getMicroseconds(); From b8a7641fb717f9210082575e24475fa8e09d9db7 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Wed, 17 Apr 2024 15:17:05 +0200 Subject: [PATCH 0326/1506] make accounting integrations more generative --- .../workspace/WorkspaceMoreFeaturesPage.tsx | 2 +- .../accounting/WorkspaceAccountingPage.tsx | 158 +++++++++--------- src/types/onyx/Request.ts | 1 + 3 files changed, 83 insertions(+), 78 deletions(-) diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index f37921f07c36..d496d3872683 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -48,7 +48,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro const {translate} = useLocalize(); const {canUseAccountingIntegrations} = usePermissions(); const hasAccountingConnection = !!policy?.areConnectionsEnabled && !!policy?.connections; - const isSyncTaxEnabled = !!policy?.connections?.quickbooksOnline.config.syncTax; + const isSyncTaxEnabled = !!policy?.connections?.quickbooksOnline?.config.syncTax; const spendItems: Item[] = [ { diff --git a/src/pages/workspace/accounting/WorkspaceAccountingPage.tsx b/src/pages/workspace/accounting/WorkspaceAccountingPage.tsx index c932b38f7642..c269db9f667b 100644 --- a/src/pages/workspace/accounting/WorkspaceAccountingPage.tsx +++ b/src/pages/workspace/accounting/WorkspaceAccountingPage.tsx @@ -30,6 +30,7 @@ import type {AnchorPosition} from '@styles/index'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, PolicyConnectionSyncProgress} from '@src/types/onyx'; +import type {ConnectionName} from '@src/types/onyx/Policy'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import ConnectToQuickbooksOnlineButton from './qboConnectionButton'; @@ -45,7 +46,47 @@ type WorkspaceAccountingPageProps = WithPolicyAndFullscreenLoadingProps & policy: OnyxEntry; }; -// const AccountingIntegrations = Object.values(CONST.POLICY.CONNECTIONS.NAME); +const accountingIntegrations = Object.values(CONST.POLICY.CONNECTIONS.NAME); + +function connectToAccountingIntegrationButton(connectionName: ConnectionName, policyID: string, environmentURL: string) { + // eslint-disable-next-line default-case + switch (connectionName) { + case 'quickbooksOnline': + return ( + + ); + case 'xero': + return ( + + ); + } +} + +function accountingIntegrationIcon(connectionName: ConnectionName) { + // eslint-disable-next-line default-case + switch (connectionName) { + case 'quickbooksOnline': + return Expensicons.QBOSquare; + case 'xero': + return Expensicons.XeroSquare; + } +} + +function accountingIntegrationTitleKey(connectionName: ConnectionName) { + // eslint-disable-next-line default-case + switch (connectionName) { + case 'quickbooksOnline': + return 'workspace.accounting.qbo'; + case 'xero': + return 'workspace.accounting.xero'; + } +} function WorkspaceAccountingPage({policy, connectionSyncProgress}: WorkspaceAccountingPageProps) { const theme = useTheme(); @@ -60,10 +101,7 @@ function WorkspaceAccountingPage({policy, connectionSyncProgress}: WorkspaceAcco const threeDotsMenuContainerRef = useRef(null); const isSyncInProgress = !!connectionSyncProgress?.stageInProgress && connectionSyncProgress.stageInProgress !== CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.JOB_DONE; - const qboSyncInProgress = isSyncInProgress && connectionSyncProgress.connectionName === 'quickbooksOnline'; - const xeroSyncInProgress = isSyncInProgress && connectionSyncProgress.connectionName === 'xero'; - const policyConnectedToQbo = !!policy?.connections?.quickbooksOnline; - const policyConnectedToXero = !!policy?.connections?.xero; + const connectedIntegration = accountingIntegrations.find((integration) => !!policy?.connections?.[integration]); const policyID = policy?.id ?? ''; const overflowMenu: ThreeDotsMenuProps['menuItems'] = useMemo( @@ -84,47 +122,26 @@ function WorkspaceAccountingPage({policy, connectionSyncProgress}: WorkspaceAcco const connectionsMenuItems: MenuItemProps[] = useMemo(() => { if (isEmptyObject(policy?.connections) && !isSyncInProgress) { - return [ - { - icon: Expensicons.QBOSquare, - iconType: 'avatar', - interactive: false, - wrapperStyle: [styles.sectionMenuItemTopDescription], - shouldShowRightComponent: true, - title: translate('workspace.accounting.qbo'), - rightComponent: ( - - ), - }, - { - icon: Expensicons.XeroSquare, - iconType: 'avatar', - interactive: false, - wrapperStyle: [styles.sectionMenuItemTopDescription], - shouldShowRightComponent: true, - title: translate('workspace.accounting.xero'), - rightComponent: ( - - ), - }, - ]; + return accountingIntegrations.map((integration) => ({ + icon: accountingIntegrationIcon(integration), + iconType: 'avatar', + interactive: false, + wrapperStyle: [styles.sectionMenuItemTopDescription], + shouldShowRightComponent: true, + title: translate(accountingIntegrationTitleKey(integration)), + rightComponent: connectToAccountingIntegrationButton(integration, policyID, environmentURL), + })); } return [ { - icon: qboSyncInProgress || policyConnectedToQbo ? Expensicons.QBOSquare : Expensicons.XeroSquare, + icon: accountingIntegrationIcon(connectedIntegration ?? connectionSyncProgress?.connectionName), iconType: 'avatar', interactive: false, wrapperStyle: [styles.sectionMenuItemTopDescription], shouldShowRightComponent: true, - title: qboSyncInProgress || policyConnectedToQbo ? translate('workspace.accounting.qbo') : translate('workspace.accounting.xero'), - description: qboSyncInProgress + title: translate(accountingIntegrationTitleKey(connectedIntegration ?? connectionSyncProgress?.connectionName)), + description: isSyncInProgress ? translate('workspace.accounting.connections.syncStageName', connectionSyncProgress.stageInProgress) : translate('workspace.accounting.lastSync'), rightComponent: isSyncInProgress ? ( @@ -150,7 +167,7 @@ function WorkspaceAccountingPage({policy, connectionSyncProgress}: WorkspaceAcco ), }, - ...(policyConnectedToQbo || policyConnectedToXero + ...(connectedIntegration ? [ { icon: Expensicons.Pencil, @@ -180,14 +197,14 @@ function WorkspaceAccountingPage({policy, connectionSyncProgress}: WorkspaceAcco : []), ]; }, [ + connectedIntegration, + connectionSyncProgress?.connectionName, connectionSyncProgress?.stageInProgress, environmentURL, isSyncInProgress, overflowMenu, - policyConnectedToQbo, - policyConnectedToXero, + policy?.connections, policyID, - qboSyncInProgress, styles.popoverMenuIcon, styles.sectionMenuItemTopDescription, theme.spinner, @@ -195,32 +212,17 @@ function WorkspaceAccountingPage({policy, connectionSyncProgress}: WorkspaceAcco translate, ]); - const otherIntegrationsItem = useMemo(() => { - if (qboSyncInProgress || policyConnectedToQbo) { - return { - icon: Expensicons.XeroSquare, - title: translate('workspace.accounting.xero'), - rightComponent: ( - - ), - }; - } - if (xeroSyncInProgress || policyConnectedToXero) { - return { - icon: Expensicons.QBOSquare, - title: translate('workspace.accounting.qbo'), - rightComponent: ( - - ), - }; + const otherIntegrationsItems = useMemo(() => { + if (isEmptyObject(policy?.connections) && !isSyncInProgress) { + return; } - }, [environmentURL, policyConnectedToQbo, policyConnectedToXero, policyID, qboSyncInProgress, translate, xeroSyncInProgress]); + const otherIntegrations = accountingIntegrations.filter((integration) => integration !== connectionSyncProgress?.connectionName && integration !== connectedIntegration); + return otherIntegrations.map((integration) => ({ + icon: Expensicons.XeroSquare, + title: translate(accountingIntegrationTitleKey(integration)), + rightComponent: connectToAccountingIntegrationButton(integration, policyID, environmentURL), + })); + }, [connectedIntegration, connectionSyncProgress?.connectionName, environmentURL, isSyncInProgress, policy?.connections, policyID, translate]); const headerThreeDotsMenuItems: ThreeDotsMenuProps['menuItems'] = [ { @@ -271,21 +273,23 @@ function WorkspaceAccountingPage({policy, connectionSyncProgress}: WorkspaceAcco menuItems={connectionsMenuItems} shouldUseSingleExecution /> - {otherIntegrationsItem && ( + {otherIntegrationsItems && ( - + {otherIntegrationsItems.map((integration) => ( + + ))} )} diff --git a/src/types/onyx/Request.ts b/src/types/onyx/Request.ts index f6e82d75db70..233519f010fc 100644 --- a/src/types/onyx/Request.ts +++ b/src/types/onyx/Request.ts @@ -21,6 +21,7 @@ type RequestData = { finallyData?: OnyxUpdate[]; resolve?: (value: Response) => void; reject?: (value?: unknown) => void; + shouldSkipWebProxy?: boolean; }; type Request = RequestData & OnyxData; From c711542ba80bfc705bad89ade38209adc5a2b135 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 17 Apr 2024 21:58:33 +0800 Subject: [PATCH 0327/1506] correctly access the report data --- src/libs/TaskUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TaskUtils.ts b/src/libs/TaskUtils.ts index 19e1025a09c8..188b8f5b31ae 100644 --- a/src/libs/TaskUtils.ts +++ b/src/libs/TaskUtils.ts @@ -38,7 +38,7 @@ function getTaskReportActionMessage(action: OnyxEntry): Pick Date: Wed, 17 Apr 2024 16:00:02 +0200 Subject: [PATCH 0328/1506] add permissions to report --- src/CONST.ts | 6 ++++++ src/types/onyx/Report.ts | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/CONST.ts b/src/CONST.ts index 2b85bcb2c326..425391f6ebea 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -837,6 +837,12 @@ const CONST = { OWNER_EMAIL_FAKE: '__FAKE__', OWNER_ACCOUNT_ID_FAKE: 0, DEFAULT_REPORT_NAME: 'Chat Report', + PERMISSIONS: { + READ: 'read', + WRITE: 'write', + SHARE: 'share', + OWN: 'own', + } }, NEXT_STEP: { FINISHED: 'Finished!', diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index fe3eec6dc11e..36d345b1084c 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -186,6 +186,8 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< transactionThreadReportID?: string; fieldList?: Record; + + permissions?: Array>; }, PolicyReportField['fieldID'] >; From de29862ae77c1cd6b5fd2fa03fc8e0443ed133ed Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 17 Apr 2024 16:01:28 +0200 Subject: [PATCH 0329/1506] create isReadOnly --- src/libs/ReportUtils.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 39964bfcc4d7..6b9ef9031cf0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5989,6 +5989,13 @@ function canReportBeMentionedWithinPolicy(report: OnyxEntry, policyID: s return isChatRoom(report) && !isThread(report); } +/** + * + * Checks if report is in read-only mode. + */ +function isReadOnly(report: OnyxEntry): boolean { + return !report?.permissions?.includes(CONST.REPORT.PERMISSIONS.WRITE) ?? false; +} export { getReportParticipantsTitle, @@ -6228,6 +6235,7 @@ export { buildParticipantsFromAccountIDs, canReportBeMentionedWithinPolicy, getAllHeldTransactions, + isReadOnly, }; export type { From 7a13ae3e44fc50e05432badaf9b3db0025275ce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Wed, 17 Apr 2024 16:01:32 +0200 Subject: [PATCH 0330/1506] Change caret position styles --- src/components/Composer/index.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index a8737fdac47a..59f7333433bb 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -289,13 +289,7 @@ function Composer( opacity: 0, }} > - + {`${valueBeforeCaret} `} Date: Wed, 17 Apr 2024 16:08:32 +0200 Subject: [PATCH 0331/1506] integrate OnboardingReportFooterMessage into ReportFooter --- src/libs/ReportUtils.ts | 17 +++++++++-------- .../report/OnboardingReportFooterMessage.tsx | 11 +++++++---- src/pages/home/report/ReportFooter.tsx | 6 ++++++ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 6b9ef9031cf0..e20686e1326a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5198,6 +5198,14 @@ function isMoneyRequestReportPendingDeletion(report: OnyxEntry | EmptyOb return parentReportAction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; } +/** + * + * Checks if report is in read-only mode. + */ +function isReadOnly(report: OnyxEntry): boolean { + return !report?.permissions?.includes(CONST.REPORT.PERMISSIONS.WRITE) ?? false; +} + function canUserPerformWriteAction(report: OnyxEntry) { const reportErrors = getAddWorkspaceRoomOrChatReportErrors(report); @@ -5206,7 +5214,7 @@ function canUserPerformWriteAction(report: OnyxEntry) { return false; } - return !isArchivedRoom(report) && isEmptyObject(reportErrors) && report && isAllowedToComment(report) && !isAnonymousUser; + return !isArchivedRoom(report) && isEmptyObject(reportErrors) && report && isAllowedToComment(report) && !isAnonymousUser && !isReadOnly(report); } /** @@ -5989,13 +5997,6 @@ function canReportBeMentionedWithinPolicy(report: OnyxEntry, policyID: s return isChatRoom(report) && !isThread(report); } -/** - * - * Checks if report is in read-only mode. - */ -function isReadOnly(report: OnyxEntry): boolean { - return !report?.permissions?.includes(CONST.REPORT.PERMISSIONS.WRITE) ?? false; -} export { getReportParticipantsTitle, diff --git a/src/pages/home/report/OnboardingReportFooterMessage.tsx b/src/pages/home/report/OnboardingReportFooterMessage.tsx index a48a609b51a7..f6c91d3b9f12 100644 --- a/src/pages/home/report/OnboardingReportFooterMessage.tsx +++ b/src/pages/home/report/OnboardingReportFooterMessage.tsx @@ -1,8 +1,7 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxCollection} from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; @@ -16,8 +15,9 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Policy as PolicyType, Report} from '@src/types/onyx'; -type OnboardingReportFooterMessageOnyxProps = {reports: OnyxCollection; policies: OnyxCollection}; -type OnboardingReportFooterMessageProps = OnboardingReportFooterMessageOnyxProps & {choice: ValueOf}; +// TODO: Use a proper choice type +type OnboardingReportFooterMessageOnyxProps = {choice: OnyxEntry; reports: OnyxCollection; policies: OnyxCollection}; +type OnboardingReportFooterMessageProps = OnboardingReportFooterMessageOnyxProps; function OnboardingReportFooterMessage({choice, reports, policies}: OnboardingReportFooterMessageProps) { const {translate} = useLocalize(); @@ -83,6 +83,9 @@ function OnboardingReportFooterMessage({choice, reports, policies}: OnboardingRe } OnboardingReportFooterMessage.displayName = 'OnboardingReportFooterMessage'; export default withOnyx({ + choice: { + key: ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, + }, reports: { key: ONYXKEYS.COLLECTION.REPORT, }, diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index bd143f9ef196..bccec0597fb0 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -20,6 +20,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; +import OnboardingReportFooterMessage from './OnboardingReportFooterMessage'; import ReportActionCompose from './ReportActionCompose/ReportActionCompose'; type ReportFooterOnyxProps = { @@ -81,6 +82,7 @@ function ReportFooter({ const isSmallSizeLayout = windowWidth - (isSmallScreenWidth ? 0 : variables.sideBarWidth) < variables.anonymousReportFooterBreakpoint; const hideComposer = !ReportUtils.canUserPerformWriteAction(report); + const isReadOnlyReport = ReportUtils.isReadOnly(report); const allPersonalDetails = usePersonalDetails(); @@ -125,6 +127,10 @@ function ReportFooter({ [report.reportID, handleCreateTask], ); + if (isReadOnlyReport) { + ; + } + return ( <> {hideComposer && ( From 471a02adff994ef218ec3128614a92534fa7f6c4 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 17 Apr 2024 16:09:13 +0200 Subject: [PATCH 0332/1506] prettify --- src/pages/home/report/OnboardingReportFooterMessage.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/home/report/OnboardingReportFooterMessage.tsx b/src/pages/home/report/OnboardingReportFooterMessage.tsx index f6c91d3b9f12..54659c14cf6e 100644 --- a/src/pages/home/report/OnboardingReportFooterMessage.tsx +++ b/src/pages/home/report/OnboardingReportFooterMessage.tsx @@ -81,7 +81,9 @@ function OnboardingReportFooterMessage({choice, reports, policies}: OnboardingRe ); } + OnboardingReportFooterMessage.displayName = 'OnboardingReportFooterMessage'; + export default withOnyx({ choice: { key: ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, From b4aed0fa5eb550664687f06008359fc21bb881c3 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 17 Apr 2024 16:10:17 +0200 Subject: [PATCH 0333/1506] restrict task action in header --- src/components/TaskHeaderActionButton.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/TaskHeaderActionButton.tsx b/src/components/TaskHeaderActionButton.tsx index 2d964f58c253..a7e3abcd3012 100644 --- a/src/components/TaskHeaderActionButton.tsx +++ b/src/components/TaskHeaderActionButton.tsx @@ -25,6 +25,10 @@ function TaskHeaderActionButton({report, session}: TaskHeaderActionButtonProps) const {translate} = useLocalize(); const styles = useThemeStyles(); + if (ReportUtils.isReadOnly(report)) { + return null; + } + return (