Skip to content

Commit

Permalink
Merge pull request #27861 from thiagobrez/thiagobrez/workspace-curren…
Browse files Browse the repository at this point in the history
…cy-selector-push-to-page

feat(workspace-settings): currency selector push to page
  • Loading branch information
grgia authored Oct 11, 2023
2 parents f557852 + c559c5c commit b820046
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 52 deletions.
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
8 changes: 7 additions & 1 deletion src/components/Form.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]),

Expand All @@ -98,6 +102,7 @@ const defaultProps = {
shouldValidateOnBlur: true,
footerContent: null,
style: [],
submitButtonStyles: [],
validate: () => ({}),
};

Expand Down Expand Up @@ -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
Expand All @@ -472,6 +477,7 @@ function Form(props) {
props.isSubmitActionDangerous,
props.isSubmitButtonVisible,
props.submitButtonText,
props.submitButtonStyles,
],
);

Expand Down
1 change: 1 addition & 0 deletions src/libs/Navigation/AppNavigator/ModalStackNavigators.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions src/libs/Navigation/linkingConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
114 changes: 114 additions & 0 deletions src/pages/workspace/WorkspaceSettingsCurrencyPage.js
Original file line number Diff line number Diff line change
@@ -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 (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
testID={WorkspaceSettingsCurrencyPage.displayName}
>
<FullPageNotFoundView
onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)}
shouldShow={_.isEmpty(policy) || !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy)}
subtitleKey={_.isEmpty(policy) ? undefined : 'workspace.common.notAuthorized'}
>
<HeaderWithBackButton
title={translate('workspace.editor.currencyInputLabel')}
onBackButtonPress={onBackButtonPress}
/>

<SelectionList
sections={sections}
textInputLabel={translate('workspace.editor.currencyInputLabel')}
textInputValue={searchText}
onChangeText={setSearchText}
onSelectRow={onSelectCurrency}
headerMessage={headerMessage}
initiallyFocusedOptionKey={initiallyFocusedOptionKey}
showScrollIndicator
/>
</FullPageNotFoundView>
</ScreenWrapper>
);
}

WorkspaceSettingsCurrencyPage.displayName = 'WorkspaceSettingsCurrencyPage';
WorkspaceSettingsCurrencyPage.propTypes = propTypes;
WorkspaceSettingsCurrencyPage.defaultProps = defaultProps;

export default compose(
withPolicy,
withOnyx({
currencyList: {key: ONYXKEYS.CURRENCY_LIST},
}),
)(WorkspaceSettingsCurrencyPage);
112 changes: 61 additions & 51 deletions src/pages/workspace/WorkspaceSettingsPage.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
};

Expand All @@ -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) => {
Expand All @@ -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 (
<WorkspacePageWithSections
headerText={props.translate('workspace.common.settings')}
route={props.route}
headerText={translate('workspace.common.settings')}
route={route}
guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_SETTINGS}
>
{(hasVBA) => (
<Form
formID={ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM}
submitButtonText={props.translate('workspace.editor.save')}
style={[styles.mh5, styles.flexGrow1]}
submitButtonText={translate('workspace.editor.save')}
style={styles.flexGrow1}
submitButtonStyles={[styles.mh5]}
scrollContextEnabled
validate={validate}
onSubmit={submit}
enabledWhenOffline
>
<AvatarWithImagePicker
isUploading={props.policy.isAvatarUploading}
source={lodashGet(props.policy, 'avatar')}
isUploading={policy.isAvatarUploading}
source={lodashGet(policy, 'avatar')}
size={CONST.AVATAR_SIZE.LARGE}
DefaultAvatar={() => (
<Avatar
containerStyles={styles.avatarLarge}
imageStyles={[styles.avatarLarge, styles.alignSelfCenter]}
source={props.policy.avatar ? props.policy.avatar : ReportUtils.getDefaultWorkspaceAvatar(policyName)}
source={policy.avatar ? policy.avatar : ReportUtils.getDefaultWorkspaceAvatar(policyName)}
fallbackIcon={Expensicons.FallbackWorkspaceAvatar}
size={CONST.AVATAR_SIZE.LARGE}
name={policyName}
Expand All @@ -117,39 +126,41 @@ function WorkspaceSettingsPage(props) {
type={CONST.ICON_TYPE_WORKSPACE}
fallbackIcon={Expensicons.FallbackWorkspaceAvatar}
style={[styles.mb3]}
anchorPosition={styles.createMenuPositionProfile(props.windowWidth)}
anchorPosition={styles.createMenuPositionProfile(windowWidth)}
anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP}}
isUsingDefaultAvatar={!lodashGet(props.policy, 'avatar', null)}
onImageSelected={(file) => 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}
/>
<OfflineWithFeedback pendingAction={lodashGet(props.policy, 'pendingFields.generalSettings')}>
<OfflineWithFeedback pendingAction={lodashGet(policy, 'pendingFields.generalSettings')}>
<TextInput
accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT}
inputID="name"
label={props.translate('workspace.editor.nameInputLabel')}
accessibilityLabel={props.translate('workspace.editor.nameInputLabel')}
containerStyles={[styles.mt4]}
defaultValue={props.policy.name}
label={translate('workspace.editor.nameInputLabel')}
accessibilityLabel={translate('workspace.editor.nameInputLabel')}
containerStyles={[styles.mt4, styles.mh5]}
defaultValue={policy.name}
maxLength={CONST.WORKSPACE_NAME_CHARACTER_LIMIT}
spellCheck={false}
/>
<View style={[styles.mt4]}>
<Picker
inputID="currency"
label={props.translate('workspace.editor.currencyInputLabel')}
items={currencyItems}
isDisabled={hasVBA}
defaultValue={props.policy.outputCurrency}
hintText={hasVBA ? props.translate('workspace.editor.currencyInputDisabledText') : props.translate('workspace.editor.currencyInputHelpText')}
<MenuItemWithTopDescription
title={formattedCurrency}
description={translate('workspace.editor.currencyInputLabel')}
shouldShowRightIcon
disabled={hasVBA}
onPress={onPressCurrency}
/>
<Text style={[styles.textLabel, styles.colorMuted, styles.mt2, styles.mh5]}>
{hasVBA ? translate('workspace.editor.currencyInputDisabledText') : translate('workspace.editor.currencyInputHelpText')}
</Text>
</View>
</OfflineWithFeedback>
</Form>
Expand All @@ -168,6 +179,5 @@ export default compose(
withOnyx({
currencyList: {key: ONYXKEYS.CURRENCY_LIST},
}),
withLocalize,
withNetwork(),
)(WorkspaceSettingsPage);

0 comments on commit b820046

Please sign in to comment.