diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 2069f773075b..f151c1f1086d 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -294,6 +294,10 @@ export default { route: 'workspace/:policyID/settings', getRoute: (policyID: string) => `workspace/${policyID}/settings`, }, + WORKSPACE_SETTINGS_CURRENCY: { + route: 'workspace/:policyID/settings/currency', + getRoute: (policyID: string) => `workspace/${policyID}/settings/currency`, + }, WORKSPACE_CARD: { route: 'workspace/:policyID/card', getRoute: (policyID: string) => `workspace/${policyID}/card`, diff --git a/src/components/Form.js b/src/components/Form.js index 9836bd818536..b4e639dcf964 100644 --- a/src/components/Form.js +++ b/src/components/Form.js @@ -76,6 +76,10 @@ const propTypes = { /** Container styles */ style: stylePropTypes, + /** Submit button container styles */ + // eslint-disable-next-line react/forbid-prop-types + submitButtonStyles: PropTypes.arrayOf(PropTypes.object), + /** Custom content to display in the footer after submit button */ footerContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), @@ -98,6 +102,7 @@ const defaultProps = { shouldValidateOnBlur: true, footerContent: null, style: [], + submitButtonStyles: [], validate: () => ({}), }; @@ -447,7 +452,7 @@ function Form(props) { focusInput.focus(); } }} - containerStyles={[styles.mh0, styles.mt5, styles.flex1]} + containerStyles={[styles.mh0, styles.mt5, styles.flex1, ...props.submitButtonStyles]} enabledWhenOffline={props.enabledWhenOffline} isSubmitActionDangerous={props.isSubmitActionDangerous} disablePressOnEnter @@ -472,6 +477,7 @@ function Form(props) { props.isSubmitActionDangerous, props.isSubmitButtonVisible, props.submitButtonText, + props.submitButtonStyles, ], ); diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index c32120db2e45..f0df54d78af6 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -153,6 +153,7 @@ const SettingsModalStackNavigator = createModalStackNavigator({ Settings_Status_Set: () => require('../../../pages/settings/Profile/CustomStatus/StatusSetPage').default, Workspace_Initial: () => require('../../../pages/workspace/WorkspaceInitialPage').default, Workspace_Settings: () => require('../../../pages/workspace/WorkspaceSettingsPage').default, + Workspace_Settings_Currency: () => require('../../../pages/workspace/WorkspaceSettingsCurrencyPage').default, Workspace_Card: () => require('../../../pages/workspace/card/WorkspaceCardPage').default, Workspace_Reimburse: () => require('../../../pages/workspace/reimburse/WorkspaceReimbursePage').default, Workspace_RateAndUnit: () => require('../../../pages/workspace/reimburse/WorkspaceRateAndUnitPage').default, diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index 5616e8d63797..7153095683f0 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -185,6 +185,9 @@ export default { Workspace_Settings: { path: ROUTES.WORKSPACE_SETTINGS.route, }, + Workspace_Settings_Currency: { + path: ROUTES.WORKSPACE_SETTINGS_CURRENCY.route, + }, Workspace_Card: { path: ROUTES.WORKSPACE_CARD.route, }, diff --git a/src/pages/workspace/WorkspaceSettingsCurrencyPage.js b/src/pages/workspace/WorkspaceSettingsCurrencyPage.js new file mode 100644 index 000000000000..9c757b730cef --- /dev/null +++ b/src/pages/workspace/WorkspaceSettingsCurrencyPage.js @@ -0,0 +1,114 @@ +import React, {useState, useCallback} from 'react'; +import _ from 'underscore'; +import {withOnyx} from 'react-native-onyx'; +import PropTypes from 'prop-types'; +import useLocalize from '../../hooks/useLocalize'; +import ScreenWrapper from '../../components/ScreenWrapper'; +import HeaderWithBackButton from '../../components/HeaderWithBackButton'; +import SelectionList from '../../components/SelectionList'; +import Navigation from '../../libs/Navigation/Navigation'; +import ROUTES from '../../ROUTES'; +import compose from '../../libs/compose'; +import ONYXKEYS from '../../ONYXKEYS'; +import withPolicy, {policyDefaultProps, policyPropTypes} from './withPolicy'; +import * as Policy from '../../libs/actions/Policy'; +import * as PolicyUtils from '../../libs/PolicyUtils'; +import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView'; + +const propTypes = { + /** Constant, list of available currencies */ + currencyList: PropTypes.objectOf( + PropTypes.shape({ + /** Symbol of the currency */ + symbol: PropTypes.string.isRequired, + }), + ), + ...policyPropTypes, +}; + +const defaultProps = { + currencyList: {}, + ...policyDefaultProps, +}; + +const getDisplayText = (currencyCode, currencySymbol) => `${currencyCode} - ${currencySymbol}`; + +function WorkspaceSettingsCurrencyPage({currencyList, policy}) { + 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) => { + Policy.updateGeneralSettings(policy.id, policy.name, item.keyForList); + Navigation.goBack(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id)); + }; + + return ( + + Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} + shouldShow={_.isEmpty(policy) || !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy)} + subtitleKey={_.isEmpty(policy) ? undefined : 'workspace.common.notAuthorized'} + > + + + + + + ); +} + +WorkspaceSettingsCurrencyPage.displayName = 'WorkspaceSettingsCurrencyPage'; +WorkspaceSettingsCurrencyPage.propTypes = propTypes; +WorkspaceSettingsCurrencyPage.defaultProps = defaultProps; + +export default compose( + withPolicy, + withOnyx({ + currencyList: {key: ONYXKEYS.CURRENCY_LIST}, + }), +)(WorkspaceSettingsCurrencyPage); diff --git a/src/pages/workspace/WorkspaceSettingsPage.js b/src/pages/workspace/WorkspaceSettingsPage.js index 0f1f6c283a6b..fd975ebc9247 100644 --- a/src/pages/workspace/WorkspaceSettingsPage.js +++ b/src/pages/workspace/WorkspaceSettingsPage.js @@ -1,18 +1,16 @@ -import React, {useCallback, useMemo} from 'react'; +import React, {useCallback} from 'react'; import {Keyboard, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; import _ from 'underscore'; import lodashGet from 'lodash/get'; import ONYXKEYS from '../../ONYXKEYS'; -import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import styles from '../../styles/styles'; import compose from '../../libs/compose'; import * as Policy from '../../libs/actions/Policy'; import * as Expensicons from '../../components/Icon/Expensicons'; import AvatarWithImagePicker from '../../components/AvatarWithImagePicker'; import CONST from '../../CONST'; -import Picker from '../../components/Picker'; import TextInput from '../../components/TextInput'; import WorkspacePageWithSections from './WorkspacePageWithSections'; import withPolicy, {policyPropTypes, policyDefaultProps} from './withPolicy'; @@ -25,17 +23,29 @@ import Avatar from '../../components/Avatar'; import Navigation from '../../libs/Navigation/Navigation'; import ROUTES from '../../ROUTES'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../components/withWindowDimensions'; +import MenuItemWithTopDescription from '../../components/MenuItemWithTopDescription'; +import Text from '../../components/Text'; +import useLocalize from '../../hooks/useLocalize'; const propTypes = { - // The currency list constant object from Onyx + /** Constant, list of available currencies */ currencyList: PropTypes.objectOf( PropTypes.shape({ - // Symbol for the currency - symbol: PropTypes.string, + /** Symbol of the currency */ + symbol: PropTypes.string.isRequired, }), ), + + /** The route object passed to this page from the navigator */ + route: PropTypes.shape({ + /** Each parameter passed via the URL */ + params: PropTypes.shape({ + /** The policyID that is being configured */ + policyID: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, + ...policyPropTypes, - ...withLocalizePropTypes, ...windowDimensionsPropTypes, }; @@ -44,26 +54,22 @@ const defaultProps = { ...policyDefaultProps, }; -function WorkspaceSettingsPage(props) { - const currencyItems = useMemo(() => { - const currencyListKeys = _.keys(props.currencyList); - return _.map(currencyListKeys, (currencyCode) => ({ - value: currencyCode, - label: `${currencyCode} - ${props.currencyList[currencyCode].symbol}`, - })); - }, [props.currencyList]); +function WorkspaceSettingsPage({policy, currencyList, windowWidth, route}) { + const {translate} = useLocalize(); + + const formattedCurrency = !_.isEmpty(policy) && !_.isEmpty(currencyList) ? `${policy.outputCurrency} - ${currencyList[policy.outputCurrency].symbol}` : ''; const submit = useCallback( (values) => { - if (props.policy.isPolicyUpdating) { + if (policy.isPolicyUpdating) { return; } - const outputCurrency = values.currency; - Policy.updateGeneralSettings(props.policy.id, values.name.trim(), outputCurrency); + + Policy.updateGeneralSettings(policy.id, values.name.trim(), policy.outputCurrency); Keyboard.dismiss(); - Navigation.goBack(ROUTES.WORKSPACE_INITIAL.getRoute(props.policy.id)); + Navigation.goBack(ROUTES.WORKSPACE_INITIAL.getRoute(policy.id)); }, - [props.policy.id, props.policy.isPolicyUpdating], + [policy.id, policy.isPolicyUpdating, policy.outputCurrency], ); const validate = useCallback((values) => { @@ -81,33 +87,36 @@ function WorkspaceSettingsPage(props) { return errors; }, []); - const policyName = lodashGet(props.policy, 'name', ''); + const onPressCurrency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS_CURRENCY.getRoute(policy.id)), [policy.id]); + + const policyName = lodashGet(policy, 'name', ''); return ( {(hasVBA) => (
( Policy.updateWorkspaceAvatar(lodashGet(props.policy, 'id', ''), file)} - onImageRemoved={() => Policy.deleteWorkspaceAvatar(lodashGet(props.policy, 'id', ''))} + isUsingDefaultAvatar={!lodashGet(policy, 'avatar', null)} + onImageSelected={(file) => Policy.updateWorkspaceAvatar(lodashGet(policy, 'id', ''), file)} + onImageRemoved={() => Policy.deleteWorkspaceAvatar(lodashGet(policy, 'id', ''))} editorMaskImage={Expensicons.ImageCropSquareMask} - pendingAction={lodashGet(props.policy, 'pendingFields.avatar', null)} - errors={lodashGet(props.policy, 'errorFields.avatar', null)} - onErrorClose={() => Policy.clearAvatarErrors(props.policy.id)} - previewSource={UserUtils.getFullSizeAvatar(props.policy.avatar, '')} - headerTitle={props.translate('workspace.common.workspaceAvatar')} - originalFileName={props.policy.originalFileName} + pendingAction={lodashGet(policy, 'pendingFields.avatar', null)} + errors={lodashGet(policy, 'errorFields.avatar', null)} + onErrorClose={() => Policy.clearAvatarErrors(policy.id)} + previewSource={UserUtils.getFullSizeAvatar(policy.avatar, '')} + headerTitle={translate('workspace.common.workspaceAvatar')} + originalFileName={policy.originalFileName} /> - + - + + {hasVBA ? translate('workspace.editor.currencyInputDisabledText') : translate('workspace.editor.currencyInputHelpText')} + @@ -168,6 +179,5 @@ export default compose( withOnyx({ currencyList: {key: ONYXKEYS.CURRENCY_LIST}, }), - withLocalize, withNetwork(), )(WorkspaceSettingsPage);