From b69454519c8c89ae718a71493d63f172f6d6e44e Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Mon, 11 Mar 2024 17:15:14 +0100 Subject: [PATCH 01/10] feat: policy distance rates settings --- src/CONST.ts | 4 + src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + src/languages/en.ts | 8 ++ src/languages/es.ts | 8 ++ ...olicyDistanceRatesDefaultCategoryParams.ts | 6 + .../SetPolicyDistanceRatesUnitParams.ts | 6 + src/libs/API/parameters/index.ts | 2 + src/libs/API/types.ts | 4 + .../AppNavigator/ModalStackNavigators.tsx | 1 + .../CENTRAL_PANE_TO_RHP_MAPPING.ts | 2 +- src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/Navigation/types.ts | 3 + src/libs/actions/Policy.ts | 122 ++++++++++++++++++ .../CategorySelectorModal.tsx | 82 ++++++++++++ .../distanceRates/CategorySelector/index.tsx | 73 +++++++++++ .../distanceRates/CategorySelector/types.ts | 8 ++ .../distanceRates/PolicyDistanceRatesPage.tsx | 2 +- .../PolicyDistanceRatesSettingsPage.tsx | 80 ++++++++++++ .../UnitSelector/UnitSelectorModal.tsx | 80 ++++++++++++ .../distanceRates/UnitSelector/index.tsx | 69 ++++++++++ .../distanceRates/UnitSelector/types.ts | 13 ++ 22 files changed, 579 insertions(+), 2 deletions(-) create mode 100644 src/libs/API/parameters/SetPolicyDistanceRatesDefaultCategoryParams.ts create mode 100644 src/libs/API/parameters/SetPolicyDistanceRatesUnitParams.ts create mode 100644 src/pages/workspace/distanceRates/CategorySelector/CategorySelectorModal.tsx create mode 100644 src/pages/workspace/distanceRates/CategorySelector/index.tsx create mode 100644 src/pages/workspace/distanceRates/CategorySelector/types.ts create mode 100644 src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx create mode 100644 src/pages/workspace/distanceRates/UnitSelector/UnitSelectorModal.tsx create mode 100644 src/pages/workspace/distanceRates/UnitSelector/index.tsx create mode 100644 src/pages/workspace/distanceRates/UnitSelector/types.ts diff --git a/src/CONST.ts b/src/CONST.ts index ce2029c78713..0b1dcd7ec250 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1416,6 +1416,10 @@ const CONST = { DISABLE: 'disable', ENABLE: 'enable', }, + UNITS: { + MI: 'Miles', + KM: 'Kilometers', + }, }, CUSTOM_UNITS: { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 87be6247c27c..5ffb2f13d27e 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -581,6 +581,10 @@ const ROUTES = { route: 'workspace/:policyID/distance-rates/new', getRoute: (policyID: string) => `workspace/${policyID}/distance-rates/new` as const, }, + WORKSPACE_DISTANCE_RATES_SETTINGS: { + route: 'workspace/:policyID/distance-rates/settings', + getRoute: (policyID: string) => `workspace/${policyID}/distance-rates/settings` as const, + }, // Referral program promotion REFERRAL_DETAILS_MODAL: { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 387d874efcf8..b3860b7a0e89 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -231,6 +231,7 @@ const SCREENS = { MEMBER_DETAILS_ROLE_SELECTION: 'Workspace_Member_Details_Role_Selection', DISTANCE_RATES: 'Distance_Rates', CREATE_DISTANCE_RATE: 'Create_Distance_Rate', + DISTANCE_RATES_SETTINGS: 'Distance_Rates_Settings', }, EDIT_REQUEST: { diff --git a/src/languages/en.ts b/src/languages/en.ts index be2d0dc569fd..722e3d708024 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1927,8 +1927,16 @@ export default { status: 'Status', enabled: 'Enabled', disabled: 'Disabled', + unit: 'Unit', + defaultCategory: 'Default category', errors: { createRateGenericFailureMessage: 'An error occurred while creating the distance rate, please try again.', + updateRateUnitGenericFailureMessage: 'An error occurred while updating the distance rate unit, please try again.', + updateRateDefaultCategoryGenericFailureMessage: 'An error occurred while updating the distance rate default category, please try again.', + }, + units: { + MI: 'Miles', + KM: 'Kilometers', }, }, editor: { diff --git a/src/languages/es.ts b/src/languages/es.ts index b85071197298..be71730f1cc4 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1952,8 +1952,16 @@ export default { status: 'Estado', enabled: 'Activada', disabled: 'Desactivada', + unit: 'Unit', + defaultCategory: 'Default category', errors: { createRateGenericFailureMessage: 'An error occurred while creating the distance rate, please try again.', + updateRateUnitGenericFailureMessage: 'An error occurred while updating the distance rate unit, please try again.', + updateRateDefaultCategoryGenericFailureMessage: 'An error occurred while updating the distance rate default category, please try again.', + }, + units: { + MI: 'Millas', + KM: 'Kilómetros', }, }, editor: { diff --git a/src/libs/API/parameters/SetPolicyDistanceRatesDefaultCategoryParams.ts b/src/libs/API/parameters/SetPolicyDistanceRatesDefaultCategoryParams.ts new file mode 100644 index 000000000000..d2d11993a172 --- /dev/null +++ b/src/libs/API/parameters/SetPolicyDistanceRatesDefaultCategoryParams.ts @@ -0,0 +1,6 @@ +type SetPolicyDistanceRatesDefaultCategoryParams = { + policyID: string; + customUnit: string; +}; + +export default SetPolicyDistanceRatesDefaultCategoryParams; diff --git a/src/libs/API/parameters/SetPolicyDistanceRatesUnitParams.ts b/src/libs/API/parameters/SetPolicyDistanceRatesUnitParams.ts new file mode 100644 index 000000000000..c841f480d1bf --- /dev/null +++ b/src/libs/API/parameters/SetPolicyDistanceRatesUnitParams.ts @@ -0,0 +1,6 @@ +type SetPolicyDistanceRatesUnitParams = { + policyID: string; + customUnit: string; +}; + +export default SetPolicyDistanceRatesUnitParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 85f3d9d87f57..717d33eee8ee 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -161,3 +161,5 @@ export type {default as DeclineJoinRequestParams} from './DeclineJoinRequest'; export type {default as JoinPolicyInviteLinkParams} from './JoinPolicyInviteLink'; export type {default as OpenPolicyDistanceRatesPageParams} from './OpenPolicyDistanceRatesPageParams'; export type {default as CreatePolicyDistanceRateParams} from './CreatePolicyDistanceRateParams'; +export type {default as SetPolicyDistanceRatesUnitParams} from './SetPolicyDistanceRatesUnitParams'; +export type {default as SetPolicyDistanceRatesDefaultCategoryParams} from './SetPolicyDistanceRatesDefaultCategoryParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 51cf2721878b..be2d8b450425 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -161,6 +161,8 @@ const WRITE_COMMANDS = { ACCEPT_JOIN_REQUEST: 'AcceptJoinRequest', DECLINE_JOIN_REQUEST: 'DeclineJoinRequest', CREATE_POLICY_DISTANCE_RATE: 'CreatePolicyDistanceRate', + SET_POLICY_DISTANCE_RATES_UNIT: 'SetPolicyDistanceRatesUnit', + SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY: 'SetPolicyDistanceRatesDefaultCategory', } as const; type WriteCommand = ValueOf; @@ -320,6 +322,8 @@ type WriteCommandParameters = { [WRITE_COMMANDS.ACCEPT_JOIN_REQUEST]: Parameters.AcceptJoinRequestParams; [WRITE_COMMANDS.DECLINE_JOIN_REQUEST]: Parameters.DeclineJoinRequestParams; [WRITE_COMMANDS.CREATE_POLICY_DISTANCE_RATE]: Parameters.CreatePolicyDistanceRateParams; + [WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_UNIT]: Parameters.SetPolicyDistanceRatesUnitParams; + [WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY]: Parameters.SetPolicyDistanceRatesDefaultCategoryParams; }; const READ_COMMANDS = { diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index 7786e16e00ef..da51fa4d3433 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -255,6 +255,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/members/WorkspaceMemberDetailsRoleSelectionPage').default as React.ComponentType, [SCREENS.WORKSPACE.CATEGORY_CREATE]: () => require('../../../pages/workspace/categories/CreateCategoryPage').default as React.ComponentType, [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE]: () => require('../../../pages/workspace/distanceRates/PolicyNewDistanceRatePage').default as React.ComponentType, + [SCREENS.WORKSPACE.DISTANCE_RATES_SETTINGS]: () => require('../../../pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage').default as React.ComponentType, [SCREENS.REIMBURSEMENT_ACCOUNT]: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default as React.ComponentType, [SCREENS.GET_ASSISTANCE]: () => require('../../../pages/GetAssistancePage').default as React.ComponentType, [SCREENS.SETTINGS.TWO_FACTOR_AUTH]: () => require('../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').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 8c5a32182e99..954b2b5b7cef 100755 --- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts @@ -7,7 +7,7 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = [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], [SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS], - [SCREENS.WORKSPACE.DISTANCE_RATES]: [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE], + [SCREENS.WORKSPACE.DISTANCE_RATES]: [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE, SCREENS.WORKSPACE.DISTANCE_RATES_SETTINGS], }; export default CENTRAL_PANE_TO_RHP_MAPPING; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index cc337c3b603f..7ada76628c82 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -297,6 +297,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE]: { path: ROUTES.WORKSPACE_CREATE_DISTANCE_RATE.route, }, + [SCREENS.WORKSPACE.DISTANCE_RATES_SETTINGS]: { + path: ROUTES.WORKSPACE_DISTANCE_RATES_SETTINGS.route, + }, [SCREENS.REIMBURSEMENT_ACCOUNT]: { path: ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.route, exact: true, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index fc7ebc8f1b72..5ad59483bf38 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -224,6 +224,9 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE]: { policyID: string; }; + [SCREENS.WORKSPACE.DISTANCE_RATES_SETTINGS]: { + policyID: string; + }; [SCREENS.GET_ASSISTANCE]: { backTo: Routes; }; diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index e6fcbe9e1ba9..1311e2867a21 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -23,6 +23,8 @@ import type { OpenWorkspaceMembersPageParams, OpenWorkspaceParams, OpenWorkspaceReimburseViewParams, + SetPolicyDistanceRatesDefaultCategoryParams, + SetPolicyDistanceRatesUnitParams, SetWorkspaceApprovalModeParams, SetWorkspaceAutoReportingFrequencyParams, SetWorkspaceAutoReportingMonthlyOffsetParams, @@ -2858,6 +2860,124 @@ function clearCreateDistanceRateError(policyID: string, currentRates: Record; + + /** Whether the modal is visible */ + isVisible: boolean; + + /** Selected category */ + currentCategory: string; + + /** Function to call when the user selects a category */ + onCategorySelected: (value: CategoryItemType) => void; + + /** Function to call when the user closes the category selector modal */ + onClose: () => void; + + /** Label to display on field */ + label: string; +}; + +function CategorySelectorModal({policyCategories, isVisible, currentCategory, onCategorySelected, onClose, label}: CategorySelectorModalProps) { + const styles = useThemeStyles(); + + const categories = useMemo( + () => + Object.values(policyCategories ?? {}).map((value) => ({ + value: value.name, + text: value.name, + keyForList: value.name, + isSelected: value.name === currentCategory, + })), + [currentCategory, policyCategories], + ); + + return ( + + + + + + + ); +} + +CategorySelectorModal.displayName = 'CategorySelectorModal'; + +export default CategorySelectorModal; diff --git a/src/pages/workspace/distanceRates/CategorySelector/index.tsx b/src/pages/workspace/distanceRates/CategorySelector/index.tsx new file mode 100644 index 000000000000..f739e163a47f --- /dev/null +++ b/src/pages/workspace/distanceRates/CategorySelector/index.tsx @@ -0,0 +1,73 @@ +import React, {useState} from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type * as OnyxTypes from '@src/types/onyx'; +import CategorySelectorModal from './CategorySelectorModal'; +import type CategoryItemType from './types'; + +type CategorySelectorProps = { + /** Collection of categories attached to a policy */ + policyCategories: OnyxEntry; + + /** Function to call when the user selects a category */ + setNewCategory: (value: CategoryItemType) => void; + + /** Currently selected category */ + defaultValue?: string; + + /** Label to display on field */ + label: string; + + /** Any additional styles to apply */ + wrapperStyle: StyleProp; +}; + +function CategorySelector({policyCategories, defaultValue = '', wrapperStyle, label, setNewCategory}: CategorySelectorProps) { + const styles = useThemeStyles(); + + const [isPickerVisible, setIsPickerVisible] = useState(false); + + const showPickerModal = () => { + setIsPickerVisible(true); + }; + + const hidePickerModal = () => { + setIsPickerVisible(false); + }; + + const updateCategoryInput = (categoryItem: CategoryItemType) => { + setNewCategory(categoryItem); + hidePickerModal(); + }; + + const title = defaultValue; + const descStyle = title.length === 0 ? styles.textNormal : null; + + return ( + + + + + ); +} + +CategorySelector.displayName = 'CategorySelector'; + +export default CategorySelector; diff --git a/src/pages/workspace/distanceRates/CategorySelector/types.ts b/src/pages/workspace/distanceRates/CategorySelector/types.ts new file mode 100644 index 000000000000..78e78afb4547 --- /dev/null +++ b/src/pages/workspace/distanceRates/CategorySelector/types.ts @@ -0,0 +1,8 @@ +type CategoryItemType = { + value: string; + text: string; + keyForList: string; + isSelected: boolean; +}; + +export default CategoryItemType; diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index b9e1ca47b8a8..8f70fa9c13db 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -113,7 +113,7 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps) }; const openSettings = () => { - // Navigation.navigate(ROUTES.WORKSPACE_DISTANCE_RATES_SETTINGS.getRoute(policyID)); + Navigation.navigate(ROUTES.WORKSPACE_DISTANCE_RATES_SETTINGS.getRoute(policyID)); }; const editRate = () => { diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx new file mode 100644 index 000000000000..47df30dfd271 --- /dev/null +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx @@ -0,0 +1,80 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import * as Policy from '@userActions/Policy'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type * as OnyxTypes from '@src/types/onyx'; +import CategorySelector from './CategorySelector'; +import type CategoryItemType from './CategorySelector/types'; +import UnitSelector from './UnitSelector'; +import type {UnitItemType} from './UnitSelector/types'; + +type PolicyDistanceRatesSettingsPageOnyxProps = { + /** Policy details */ + policy: OnyxEntry; + + /** Collection of categories attached to a policy */ + policyCategories: OnyxEntry; +}; + +type PolicyDistanceRatesSettingsPageProps = PolicyDistanceRatesSettingsPageOnyxProps & StackScreenProps; + +function PolicyDistanceRatesSettingsPage({policy, policyCategories, route}: PolicyDistanceRatesSettingsPageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const policyID = route.params.policyID; + const customUnits = policy?.customUnits ?? {}; + const customUnit = customUnits[Object.keys(customUnits)[0]]; + const customUnitID = customUnit.customUnitID; + + const defaultCategory = customUnits[customUnitID].defaultCategory; + const defaultUnit = customUnits[customUnitID].attributes.unit.toUpperCase(); + + const setNewUnit = (unit: UnitItemType) => { + Policy.setPolicyDistanceRatesUnit(policyID, customUnit, {...customUnit, attributes: {unit: unit.value}}); + }; + + const setNewCategory = (category: CategoryItemType) => { + Policy.setPolicyDistanceRatesDefaultCategory(policyID, customUnit, {...customUnit, defaultCategory: category.value}); + }; + + return ( + + + + {policy?.areCategoriesEnabled && ( + + )} + + ); +} + +PolicyDistanceRatesSettingsPage.displayName = 'PolicyDistanceRatesSettingsPage'; + +export default withOnyx({ + policy: { + key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID}`, + }, + policyCategories: { + key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route.params.policyID}`, + }, +})(PolicyDistanceRatesSettingsPage); diff --git a/src/pages/workspace/distanceRates/UnitSelector/UnitSelectorModal.tsx b/src/pages/workspace/distanceRates/UnitSelector/UnitSelectorModal.tsx new file mode 100644 index 000000000000..cd4a7b926c4c --- /dev/null +++ b/src/pages/workspace/distanceRates/UnitSelector/UnitSelectorModal.tsx @@ -0,0 +1,80 @@ +import React, {useMemo} from 'react'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Modal from '@components/Modal'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; +import type {Unit} from '@src/types/onyx/Policy'; +import type {UnitItemType, UnitType} from './types'; + +type UnitSelectorModalProps = { + /** Whether the modal is visible */ + isVisible: boolean; + + /** Selected unit */ + currentUnit: string; + + /** Function to call when the user selects a unit */ + onUnitSelected: (value: UnitItemType) => void; + + /** Function to call when the user closes the unit selector modal */ + onClose: () => void; + + /** Label to display on field */ + label: string; +}; + +function UnitSelectorModal({isVisible, currentUnit, onUnitSelected, onClose, label}: UnitSelectorModalProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const units = useMemo( + () => + Object.keys(CONST.POLICY.UNITS).map((key) => ({ + value: key.toLowerCase() as Unit, + text: translate(`workspace.distanceRates.units.${key as UnitType}`), + keyForList: key, + isSelected: key === currentUnit, + })), + [currentUnit, translate], + ); + + return ( + + + + + + + ); +} + +UnitSelectorModal.displayName = 'UnitSelectorModal'; + +export default UnitSelectorModal; diff --git a/src/pages/workspace/distanceRates/UnitSelector/index.tsx b/src/pages/workspace/distanceRates/UnitSelector/index.tsx new file mode 100644 index 000000000000..8615f101fdda --- /dev/null +++ b/src/pages/workspace/distanceRates/UnitSelector/index.tsx @@ -0,0 +1,69 @@ +import React, {useState} from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type {UnitItemType, UnitType} from './types'; +import UnitSelectorModal from './UnitSelectorModal'; + +type UnitSelectorProps = { + /** Function to call when the user selects a unit */ + setNewUnit: (value: UnitItemType) => void; + + /** Currently selected unit */ + defaultValue: string; + + /** Label to display on field */ + label: string; + + /** Any additional styles to apply */ + wrapperStyle: StyleProp; +}; + +function UnitSelector({defaultValue = '', wrapperStyle, label, setNewUnit}: UnitSelectorProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const [isPickerVisible, setIsPickerVisible] = useState(false); + + const showPickerModal = () => { + setIsPickerVisible(true); + }; + + const hidePickerModal = () => { + setIsPickerVisible(false); + }; + + const updateUnitInput = (UnitItem: UnitItemType) => { + setNewUnit(UnitItem); + hidePickerModal(); + }; + + const title = defaultValue ? translate(`workspace.distanceRates.units.${defaultValue as UnitType}`) : ''; + const descStyle = title.length === 0 ? styles.textNormal : null; + + return ( + + + + + ); +} + +UnitSelector.displayName = 'UnitSelector'; + +export default UnitSelector; diff --git a/src/pages/workspace/distanceRates/UnitSelector/types.ts b/src/pages/workspace/distanceRates/UnitSelector/types.ts new file mode 100644 index 000000000000..98f2af3f48cf --- /dev/null +++ b/src/pages/workspace/distanceRates/UnitSelector/types.ts @@ -0,0 +1,13 @@ +import type CONST from '@src/CONST'; +import type {Unit} from '@src/types/onyx/Policy'; + +type UnitType = keyof typeof CONST.POLICY.UNITS; + +type UnitItemType = { + value: Unit; + text: string; + keyForList: string; + isSelected: boolean; +}; + +export type {UnitType, UnitItemType}; From 8a489d3a70907d5c24f7021e8a47def7047dcc79 Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Wed, 13 Mar 2024 12:54:38 +0100 Subject: [PATCH 02/10] fix: after conflicts resolve --- src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx | 2 +- .../linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts | 2 +- src/libs/actions/Policy.ts | 8 -------- .../distanceRates/PolicyDistanceRatesSettingsPage.tsx | 2 +- 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index 293dd15440b9..42932a78bd0a 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -262,7 +262,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/members/WorkspaceMemberDetailsPage').default as React.ComponentType, [SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION]: () => require('../../../pages/workspace/members/WorkspaceMemberDetailsRoleSelectionPage').default as React.ComponentType, [SCREENS.WORKSPACE.CATEGORY_CREATE]: () => require('../../../pages/workspace/categories/CreateCategoryPage').default as React.ComponentType, - [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE]: () => require('../../../workspace/distanceRates/CreateDistanceRatePage').default as React.ComponentType, + [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE]: () => require('../../../pages/workspace/distanceRates/CreateDistanceRatePage').default as React.ComponentType, [SCREENS.WORKSPACE.DISTANCE_RATES_SETTINGS]: () => require('../../../pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage').default as React.ComponentType, [SCREENS.WORKSPACE.TAGS_SETTINGS]: () => require('../../../pages/workspace/tags/WorkspaceTagsSettingsPage').default as React.ComponentType, [SCREENS.WORKSPACE.TAGS_EDIT]: () => require('../../../pages/workspace/tags/WorkspaceEditTagsPage').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index da56b98519d4..905c62e5f812 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -13,7 +13,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { ], [SCREENS.WORKSPACE.TAGS]: [SCREENS.WORKSPACE.TAGS_SETTINGS, SCREENS.WORKSPACE.TAGS_EDIT, SCREENS.WORKSPACE.TAG_CREATE], [SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS], - [SCREENS.WORKSPACE.DISTANCE_RATES]: [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE], + [SCREENS.WORKSPACE.DISTANCE_RATES]: [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE, SCREENS.WORKSPACE.DISTANCE_RATES_SETTINGS], }; export default FULL_SCREEN_TO_RHP_MAPPING; diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index cb03857c0507..22370983e4bb 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -3536,10 +3536,6 @@ function clearCreateDistanceRateError(policyID: string, currentRates: Record Date: Wed, 13 Mar 2024 13:16:03 +0100 Subject: [PATCH 03/10] fix: update error message --- src/libs/actions/Policy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 22370983e4bb..641dd06a97e8 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -3574,7 +3574,7 @@ function setPolicyDistanceRatesUnit(policyID: string, currentCustomUnit: CustomU customUnits: { [currentCustomUnit.customUnitID]: { ...currentCustomUnit, - errors: ErrorUtils.getMicroSecondOnyxError('workspace.distanceRates.errors.updateRateUnitGenericFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage'), pendingAction: null, }, }, @@ -3629,7 +3629,7 @@ function setPolicyDistanceRatesDefaultCategory(policyID: string, currentCustomUn customUnits: { [currentCustomUnit.customUnitID]: { ...currentCustomUnit, - errors: ErrorUtils.getMicroSecondOnyxError('workspace.distanceRates.errors.updateRateDefaultCategoryGenericFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage'), pendingAction: null, }, }, From de14e6669e44b818dd7001c11556936c9106dcd7 Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Thu, 14 Mar 2024 12:07:36 +0100 Subject: [PATCH 04/10] fix: remove redundant error clearing --- src/libs/actions/Policy.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 9279874b1fc5..1d08ccf0c1d6 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -3620,7 +3620,6 @@ function setPolicyDistanceRatesUnit(policyID: string, currentCustomUnit: CustomU value: { customUnits: { [newCustomUnit.customUnitID]: { - errors: null, pendingAction: null, }, }, @@ -3675,7 +3674,6 @@ function setPolicyDistanceRatesDefaultCategory(policyID: string, currentCustomUn value: { customUnits: { [newCustomUnit.customUnitID]: { - errors: null, pendingAction: null, }, }, From b088355852b54216d05a8748d804239dd5eacf1c Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Mon, 18 Mar 2024 11:37:43 +0100 Subject: [PATCH 05/10] fix: remove redundant prop --- .../workspace/distanceRates/UnitSelector/UnitSelectorModal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/workspace/distanceRates/UnitSelector/UnitSelectorModal.tsx b/src/pages/workspace/distanceRates/UnitSelector/UnitSelectorModal.tsx index cd4a7b926c4c..ec5e99778d8a 100644 --- a/src/pages/workspace/distanceRates/UnitSelector/UnitSelectorModal.tsx +++ b/src/pages/workspace/distanceRates/UnitSelector/UnitSelectorModal.tsx @@ -68,7 +68,6 @@ function UnitSelectorModal({isVisible, currentUnit, onUnitSelected, onClose, lab initiallyFocusedOptionKey={currentUnit} onSelectRow={onUnitSelected} shouldStopPropagation - shouldUseDynamicMaxToRenderPerBatch /> From 6a666d3545ff6c89029cc33ba5c0f8db7ff67770 Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Mon, 18 Mar 2024 13:39:07 +0100 Subject: [PATCH 06/10] fix: access wrapper --- .../PolicyDistanceRatesSettingsPage.tsx | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx index a5546e7d389b..ecc982e32dec 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx @@ -7,6 +7,8 @@ import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import type {SettingsNavigatorParamList} from '@navigation/types'; +import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -47,24 +49,32 @@ function PolicyDistanceRatesSettingsPage({policy, policyCategories, route}: Poli }; return ( - - - - {policy?.areCategoriesEnabled && ( - - )} - + + + + + + {policy?.areCategoriesEnabled && ( + + )} + + + ); } From 7e175f65d4334f036ec36b083b6e181955b876a7 Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Mon, 18 Mar 2024 16:16:52 +0100 Subject: [PATCH 07/10] fix: cr fixes --- src/ROUTES.ts | 4 ++-- src/languages/es.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 3c32f5f88087..6410b00743f1 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -614,8 +614,8 @@ const ROUTES = { getRoute: (policyID: string) => `settings/workspaces/${policyID}/distance-rates/new` as const, }, WORKSPACE_DISTANCE_RATES_SETTINGS: { - route: 'workspace/:policyID/distance-rates/settings', - getRoute: (policyID: string) => `workspace/${policyID}/distance-rates/settings` as const, + route: 'settings/workspace/:policyID/distance-rates/settings', + getRoute: (policyID: string) => `settings/workspace/${policyID}/distance-rates/settings` as const, }, // Referral program promotion REFERRAL_DETAILS_MODAL: { diff --git a/src/languages/es.ts b/src/languages/es.ts index dc0e68504cbd..8a4c28839291 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2021,8 +2021,8 @@ export default { status: 'Estado', enabled: 'Activada', disabled: 'Desactivada', - unit: 'Unit', - defaultCategory: 'Default category', + unit: 'Unidad', + defaultCategory: 'Categoría predeterminada', units: { MI: 'Millas', KM: 'Kilómetros', From 55ca7493b68159df4f4a05871e2bfe1699915087 Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Tue, 19 Mar 2024 14:44:52 +0100 Subject: [PATCH 08/10] fix: more cr fixes --- src/CONST.ts | 4 -- src/components/UnitPicker.tsx | 56 +++++++++++++++++++ src/languages/en.ts | 4 -- src/languages/es.ts | 4 -- .../CategorySelectorModal.tsx | 39 ++++--------- .../distanceRates/CategorySelector/index.tsx | 16 +++--- .../distanceRates/CategorySelector/types.ts | 8 --- .../PolicyDistanceRatesSettingsPage.tsx | 20 +++---- .../UnitSelector/UnitSelectorModal.tsx | 31 +++------- .../distanceRates/UnitSelector/index.tsx | 29 ++++++---- .../distanceRates/UnitSelector/types.ts | 13 ----- .../WorkspaceRateAndUnitPage/UnitPage.tsx | 46 +++------------ 12 files changed, 112 insertions(+), 158 deletions(-) create mode 100644 src/components/UnitPicker.tsx delete mode 100644 src/pages/workspace/distanceRates/CategorySelector/types.ts delete mode 100644 src/pages/workspace/distanceRates/UnitSelector/types.ts diff --git a/src/CONST.ts b/src/CONST.ts index 28a72dd6e00e..aac314ca926f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1459,10 +1459,6 @@ const CONST = { DISABLE: 'disable', ENABLE: 'enable', }, - UNITS: { - MI: 'Miles', - KM: 'Kilometers', - }, }, CUSTOM_UNITS: { diff --git a/src/components/UnitPicker.tsx b/src/components/UnitPicker.tsx new file mode 100644 index 000000000000..04620a4b72f1 --- /dev/null +++ b/src/components/UnitPicker.tsx @@ -0,0 +1,56 @@ +import Str from 'expensify-common/lib/str'; +import React, {useMemo} from 'react'; +import useLocalize from '@hooks/useLocalize'; +import CONST from '@src/CONST'; +import type {Unit} from '@src/types/onyx/Policy'; +import SelectionList from './SelectionList'; +import RadioListItem from './SelectionList/RadioListItem'; + +type UnitItemType = { + value: Unit; + text: string; + keyForList: string; + isSelected: boolean; +}; + +type UnitPickerProps = { + defaultValue: Unit; + onOptionSelected: (unit: UnitItemType) => void; +}; + +function UnitPicker({defaultValue, onOptionSelected}: UnitPickerProps) { + const {translate} = useLocalize(); + const unitItems = useMemo( + () => ({ + [CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS]: translate('common.kilometers'), + [CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES]: translate('common.miles'), + }), + [translate], + ); + + const unitOptions = useMemo(() => { + const arr: UnitItemType[] = []; + Object.entries(unitItems).forEach(([unit, label]) => { + arr.push({ + value: unit as Unit, + text: Str.recapitalize(label), + keyForList: unit, + isSelected: defaultValue === unit, + }); + }); + return arr; + }, [defaultValue, unitItems]); + + return ( + unit.isSelected)?.keyForList} + /> + ); +} + +export default UnitPicker; + +export type {UnitItemType}; diff --git a/src/languages/en.ts b/src/languages/en.ts index bcfd95426e3c..a3280f4b64ef 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2005,10 +2005,6 @@ export default { disabled: 'Disabled', unit: 'Unit', defaultCategory: 'Default category', - units: { - MI: 'Miles', - KM: 'Kilometers', - }, }, editor: { descriptionInputLabel: 'Description', diff --git a/src/languages/es.ts b/src/languages/es.ts index 656e754f28a2..6fb7779b4bcc 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2030,10 +2030,6 @@ export default { disabled: 'Desactivada', unit: 'Unidad', defaultCategory: 'Categoría predeterminada', - units: { - MI: 'Millas', - KM: 'Kilómetros', - }, }, editor: { nameInputLabel: 'Nombre', diff --git a/src/pages/workspace/distanceRates/CategorySelector/CategorySelectorModal.tsx b/src/pages/workspace/distanceRates/CategorySelector/CategorySelectorModal.tsx index d6e0129d039b..b48456ecce79 100644 --- a/src/pages/workspace/distanceRates/CategorySelector/CategorySelectorModal.tsx +++ b/src/pages/workspace/distanceRates/CategorySelector/CategorySelectorModal.tsx @@ -1,18 +1,15 @@ -import React, {useMemo} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; +import React from 'react'; +import CategoryPicker from '@components/CategoryPicker'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Modal from '@components/Modal'; import ScreenWrapper from '@components/ScreenWrapper'; -import SelectionList from '@components/SelectionList'; -import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {ListItem} from '@components/SelectionList/types'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; -import type * as OnyxTypes from '@src/types/onyx'; -import type CategoryItemType from './types'; type CategorySelectorModalProps = { - /** Collection of categories attached to a policy */ - policyCategories: OnyxEntry; + /** The ID of the associated policy */ + policyID: string; /** Whether the modal is visible */ isVisible: boolean; @@ -21,7 +18,7 @@ type CategorySelectorModalProps = { currentCategory: string; /** Function to call when the user selects a category */ - onCategorySelected: (value: CategoryItemType) => void; + onCategorySelected: (value: ListItem) => void; /** Function to call when the user closes the category selector modal */ onClose: () => void; @@ -30,20 +27,9 @@ type CategorySelectorModalProps = { label: string; }; -function CategorySelectorModal({policyCategories, isVisible, currentCategory, onCategorySelected, onClose, label}: CategorySelectorModalProps) { +function CategorySelectorModal({policyID, isVisible, currentCategory, onCategorySelected, onClose, label}: CategorySelectorModalProps) { const styles = useThemeStyles(); - const categories = useMemo( - () => - Object.values(policyCategories ?? {}).map((value) => ({ - value: value.name, - text: value.name, - keyForList: value.name, - isSelected: value.name === currentCategory, - })), - [currentCategory, policyCategories], - ); - return ( - diff --git a/src/pages/workspace/distanceRates/CategorySelector/index.tsx b/src/pages/workspace/distanceRates/CategorySelector/index.tsx index f739e163a47f..f7a1ba49d91e 100644 --- a/src/pages/workspace/distanceRates/CategorySelector/index.tsx +++ b/src/pages/workspace/distanceRates/CategorySelector/index.tsx @@ -1,19 +1,17 @@ import React, {useState} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import type {ListItem} from '@components/SelectionList/types'; import useThemeStyles from '@hooks/useThemeStyles'; -import type * as OnyxTypes from '@src/types/onyx'; import CategorySelectorModal from './CategorySelectorModal'; -import type CategoryItemType from './types'; type CategorySelectorProps = { - /** Collection of categories attached to a policy */ - policyCategories: OnyxEntry; + /** The ID of the associated policy */ + policyID: string; /** Function to call when the user selects a category */ - setNewCategory: (value: CategoryItemType) => void; + setNewCategory: (value: ListItem) => void; /** Currently selected category */ defaultValue?: string; @@ -25,7 +23,7 @@ type CategorySelectorProps = { wrapperStyle: StyleProp; }; -function CategorySelector({policyCategories, defaultValue = '', wrapperStyle, label, setNewCategory}: CategorySelectorProps) { +function CategorySelector({defaultValue = '', wrapperStyle, label, setNewCategory, policyID}: CategorySelectorProps) { const styles = useThemeStyles(); const [isPickerVisible, setIsPickerVisible] = useState(false); @@ -38,7 +36,7 @@ function CategorySelector({policyCategories, defaultValue = '', wrapperStyle, la setIsPickerVisible(false); }; - const updateCategoryInput = (categoryItem: CategoryItemType) => { + const updateCategoryInput = (categoryItem: ListItem) => { setNewCategory(categoryItem); hidePickerModal(); }; @@ -57,7 +55,7 @@ function CategorySelector({policyCategories, defaultValue = '', wrapperStyle, la wrapperStyle={wrapperStyle} /> ; - - /** Collection of categories attached to a policy */ - policyCategories: OnyxEntry; }; type PolicyDistanceRatesSettingsPageProps = PolicyDistanceRatesSettingsPageOnyxProps & StackScreenProps; -function PolicyDistanceRatesSettingsPage({policy, policyCategories, route}: PolicyDistanceRatesSettingsPageProps) { +function PolicyDistanceRatesSettingsPage({policy, route}: PolicyDistanceRatesSettingsPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -38,14 +35,14 @@ function PolicyDistanceRatesSettingsPage({policy, policyCategories, route}: Poli const customUnitID = customUnit?.customUnitID ?? ''; const defaultCategory = customUnits[customUnitID].defaultCategory; - const defaultUnit = customUnits[customUnitID].attributes.unit.toUpperCase(); + const defaultUnit = customUnits[customUnitID].attributes.unit; const setNewUnit = (unit: UnitItemType) => { Policy.setPolicyDistanceRatesUnit(policyID, customUnit, {...customUnit, attributes: {unit: unit.value}}); }; - const setNewCategory = (category: CategoryItemType) => { - Policy.setPolicyDistanceRatesDefaultCategory(policyID, customUnit, {...customUnit, defaultCategory: category.value}); + const setNewCategory = (category: ListItem) => { + Policy.setPolicyDistanceRatesDefaultCategory(policyID, customUnit, {...customUnit, defaultCategory: category.text}); }; return ( @@ -65,7 +62,7 @@ function PolicyDistanceRatesSettingsPage({policy, policyCategories, route}: Poli /> {policy?.areCategoriesEnabled && ( `${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID}`, }, - policyCategories: { - key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route.params.policyID}`, - }, })(PolicyDistanceRatesSettingsPage); diff --git a/src/pages/workspace/distanceRates/UnitSelector/UnitSelectorModal.tsx b/src/pages/workspace/distanceRates/UnitSelector/UnitSelectorModal.tsx index ec5e99778d8a..5a9f5e7c6ea1 100644 --- a/src/pages/workspace/distanceRates/UnitSelector/UnitSelectorModal.tsx +++ b/src/pages/workspace/distanceRates/UnitSelector/UnitSelectorModal.tsx @@ -1,21 +1,19 @@ -import React, {useMemo} from 'react'; +import React from 'react'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Modal from '@components/Modal'; import ScreenWrapper from '@components/ScreenWrapper'; -import SelectionList from '@components/SelectionList'; -import RadioListItem from '@components/SelectionList/RadioListItem'; -import useLocalize from '@hooks/useLocalize'; +import type {UnitItemType} from '@components/UnitPicker'; +import UnitPicker from '@components/UnitPicker'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import type {Unit} from '@src/types/onyx/Policy'; -import type {UnitItemType, UnitType} from './types'; type UnitSelectorModalProps = { /** Whether the modal is visible */ isVisible: boolean; /** Selected unit */ - currentUnit: string; + currentUnit: Unit; /** Function to call when the user selects a unit */ onUnitSelected: (value: UnitItemType) => void; @@ -29,18 +27,6 @@ type UnitSelectorModalProps = { function UnitSelectorModal({isVisible, currentUnit, onUnitSelected, onClose, label}: UnitSelectorModalProps) { const styles = useThemeStyles(); - const {translate} = useLocalize(); - - const units = useMemo( - () => - Object.keys(CONST.POLICY.UNITS).map((key) => ({ - value: key.toLowerCase() as Unit, - text: translate(`workspace.distanceRates.units.${key as UnitType}`), - keyForList: key, - isSelected: key === currentUnit, - })), - [currentUnit, translate], - ); return ( - diff --git a/src/pages/workspace/distanceRates/UnitSelector/index.tsx b/src/pages/workspace/distanceRates/UnitSelector/index.tsx index 8615f101fdda..41ad28d2d644 100644 --- a/src/pages/workspace/distanceRates/UnitSelector/index.tsx +++ b/src/pages/workspace/distanceRates/UnitSelector/index.tsx @@ -1,10 +1,12 @@ -import React, {useState} from 'react'; +import Str from 'expensify-common/lib/str'; +import React, {useMemo, useState} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import type {UnitItemType} from '@components/UnitPicker'; import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import type {UnitItemType, UnitType} from './types'; +import CONST from '@src/CONST'; +import type {Unit} from '@src/types/onyx/Policy'; import UnitSelectorModal from './UnitSelectorModal'; type UnitSelectorProps = { @@ -12,7 +14,7 @@ type UnitSelectorProps = { setNewUnit: (value: UnitItemType) => void; /** Currently selected unit */ - defaultValue: string; + defaultValue: Unit; /** Label to display on field */ label: string; @@ -21,8 +23,7 @@ type UnitSelectorProps = { wrapperStyle: StyleProp; }; -function UnitSelector({defaultValue = '', wrapperStyle, label, setNewUnit}: UnitSelectorProps) { - const styles = useThemeStyles(); +function UnitSelector({defaultValue, wrapperStyle, label, setNewUnit}: UnitSelectorProps) { const {translate} = useLocalize(); const [isPickerVisible, setIsPickerVisible] = useState(false); @@ -35,21 +36,25 @@ function UnitSelector({defaultValue = '', wrapperStyle, label, setNewUnit}: Unit setIsPickerVisible(false); }; - const updateUnitInput = (UnitItem: UnitItemType) => { - setNewUnit(UnitItem); + const updateUnitInput = (unit: UnitItemType) => { + setNewUnit(unit); hidePickerModal(); }; - const title = defaultValue ? translate(`workspace.distanceRates.units.${defaultValue as UnitType}`) : ''; - const descStyle = title.length === 0 ? styles.textNormal : null; + const unitTranslations = useMemo( + () => ({ + [CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS]: translate('common.kilometers'), + [CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES]: translate('common.miles'), + }), + [translate], + ); return ( diff --git a/src/pages/workspace/distanceRates/UnitSelector/types.ts b/src/pages/workspace/distanceRates/UnitSelector/types.ts deleted file mode 100644 index 98f2af3f48cf..000000000000 --- a/src/pages/workspace/distanceRates/UnitSelector/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type CONST from '@src/CONST'; -import type {Unit} from '@src/types/onyx/Policy'; - -type UnitType = keyof typeof CONST.POLICY.UNITS; - -type UnitItemType = { - value: Unit; - text: string; - keyForList: string; - isSelected: boolean; -}; - -export type {UnitType, UnitItemType}; diff --git a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/UnitPage.tsx b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/UnitPage.tsx index 3bd5a9e01bab..36efc239fe69 100644 --- a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/UnitPage.tsx +++ b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/UnitPage.tsx @@ -1,9 +1,9 @@ import React, {useEffect, useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; -import SelectionList from '@components/SelectionList'; -import RadioListItem from '@components/SelectionList/RadioListItem'; import Text from '@components/Text'; +import type {UnitItemType} from '@components/UnitPicker'; +import UnitPicker from '@components/UnitPicker'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; @@ -16,14 +16,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {WorkspaceRateAndUnit} from '@src/types/onyx'; -import type {Unit} from '@src/types/onyx/Policy'; - -type OptionRow = { - value: Unit; - text: string; - keyForList: string; - isSelected: boolean; -}; type WorkspaceUnitPageBaseProps = WithPolicyProps; @@ -35,13 +27,6 @@ type WorkspaceUnitPageProps = WorkspaceUnitPageBaseProps & WorkspaceRateAndUnitO function WorkspaceUnitPage(props: WorkspaceUnitPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const unitItems = useMemo( - () => ({ - [CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS]: translate('workspace.reimburse.kilometers'), - [CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES]: translate('workspace.reimburse.miles'), - }), - [translate], - ); useEffect(() => { if (props.workspaceRateAndUnit?.policyID === props.policy?.id) { @@ -51,8 +36,8 @@ function WorkspaceUnitPage(props: WorkspaceUnitPageProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const updateUnit = (unit: Unit) => { - Policy.setUnitForReimburseView(unit); + const updateUnit = (unit: UnitItemType) => { + Policy.setUnitForReimburseView(unit.value); Navigation.navigate(ROUTES.WORKSPACE_RATE_AND_UNIT.getRoute(props.policy?.id ?? '')); }; @@ -61,20 +46,6 @@ function WorkspaceUnitPage(props: WorkspaceUnitPageProps) { return defaultDistanceCustomUnit?.attributes.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES; }, [props.policy?.customUnits]); - const unitOptions = useMemo(() => { - const arr: OptionRow[] = []; - Object.entries(unitItems).forEach(([unit, label]) => { - arr.push({ - value: unit as Unit, - text: label, - keyForList: unit, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - isSelected: (props.workspaceRateAndUnit?.unit || defaultValue) === unit, - }); - }); - return arr; - }, [defaultValue, props.workspaceRateAndUnit?.unit, unitItems]); - return ( ( <> {translate('workspace.reimburse.trackDistanceChooseUnit')} - - updateUnit(unit.value)} - initiallyFocusedOptionKey={unitOptions.find((unit) => unit.isSelected)?.keyForList} + )} From 56958516d2a5d088964cd185c7a77334619be07b Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Tue, 19 Mar 2024 15:20:44 +0100 Subject: [PATCH 09/10] fix: added util to get unit translation --- src/components/UnitPicker.tsx | 26 +++++++------------ src/languages/en.ts | 2 -- src/languages/es.ts | 2 -- src/libs/WorkspacesSettingsUtils.ts | 16 ++++++++++++ .../distanceRates/UnitSelector/index.tsx | 14 +++------- .../WorkspaceRateAndUnitPage/InitialPage.tsx | 15 ++++------- 6 files changed, 35 insertions(+), 40 deletions(-) diff --git a/src/components/UnitPicker.tsx b/src/components/UnitPicker.tsx index 04620a4b72f1..a9202a348e4d 100644 --- a/src/components/UnitPicker.tsx +++ b/src/components/UnitPicker.tsx @@ -1,6 +1,7 @@ import Str from 'expensify-common/lib/str'; import React, {useMemo} from 'react'; import useLocalize from '@hooks/useLocalize'; +import {getUnitTranslationKey} from '@libs/WorkspacesSettingsUtils'; import CONST from '@src/CONST'; import type {Unit} from '@src/types/onyx/Policy'; import SelectionList from './SelectionList'; @@ -18,28 +19,21 @@ type UnitPickerProps = { onOptionSelected: (unit: UnitItemType) => void; }; +const units = [CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS, CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES]; + function UnitPicker({defaultValue, onOptionSelected}: UnitPickerProps) { const {translate} = useLocalize(); - const unitItems = useMemo( - () => ({ - [CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS]: translate('common.kilometers'), - [CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES]: translate('common.miles'), - }), - [translate], - ); - const unitOptions = useMemo(() => { - const arr: UnitItemType[] = []; - Object.entries(unitItems).forEach(([unit, label]) => { - arr.push({ + const unitOptions = useMemo( + () => + units.map((unit) => ({ value: unit as Unit, - text: Str.recapitalize(label), + text: Str.recapitalize(translate(getUnitTranslationKey(unit))), keyForList: unit, isSelected: defaultValue === unit, - }); - }); - return arr; - }, [defaultValue, unitItems]); + })), + [defaultValue, translate], + ); return ( { return workspacesUnreadStatuses; } +/** + * @param unit Unit + * @returns translation key for the unit + */ +function getUnitTranslationKey(unit: Unit): TranslationPaths { + const unitTranslationKeysStrategy: Record = { + [CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS]: 'common.kilometers', + [CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES]: 'common.miles', + }; + + return unitTranslationKeysStrategy[unit]; +} + export { getBrickRoadForPolicy, getWorkspacesBrickRoads, @@ -204,5 +219,6 @@ export { checkIfWorkspaceSettingsTabHasRBR, hasWorkspaceSettingsRBR, getChatTabBrickRoad, + getUnitTranslationKey, }; export type {BrickRoad}; diff --git a/src/pages/workspace/distanceRates/UnitSelector/index.tsx b/src/pages/workspace/distanceRates/UnitSelector/index.tsx index 41ad28d2d644..e6511e7d6418 100644 --- a/src/pages/workspace/distanceRates/UnitSelector/index.tsx +++ b/src/pages/workspace/distanceRates/UnitSelector/index.tsx @@ -1,11 +1,11 @@ import Str from 'expensify-common/lib/str'; -import React, {useMemo, useState} from 'react'; +import React, {useState} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import type {UnitItemType} from '@components/UnitPicker'; import useLocalize from '@hooks/useLocalize'; -import CONST from '@src/CONST'; +import {getUnitTranslationKey} from '@libs/WorkspacesSettingsUtils'; import type {Unit} from '@src/types/onyx/Policy'; import UnitSelectorModal from './UnitSelectorModal'; @@ -41,19 +41,13 @@ function UnitSelector({defaultValue, wrapperStyle, label, setNewUnit}: UnitSelec hidePickerModal(); }; - const unitTranslations = useMemo( - () => ({ - [CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS]: translate('common.kilometers'), - [CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES]: translate('common.miles'), - }), - [translate], - ); + const title = Str.recapitalize(translate(getUnitTranslationKey(defaultValue))); return ( ({ - [CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS]: translate('workspace.reimburse.kilometers'), - [CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES]: translate('workspace.reimburse.miles'), - }), - [translate], - ); - const saveUnitAndRate = (newUnit: Unit, newRate: string) => { const distanceCustomUnit = Object.values(props.policy?.customUnits ?? {}).find((unit) => unit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE); if (!distanceCustomUnit) { @@ -93,6 +87,7 @@ function WorkspaceRateAndUnitPage(props: WorkspaceRateAndUnitPageProps) { const unitValue = props.workspaceRateAndUnit?.unit ?? distanceCustomUnit?.attributes.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES; const rateValue = props.workspaceRateAndUnit?.rate ?? distanceCustomRate?.rate?.toString() ?? ''; + const unitTitle = Str.recapitalize(translate(getUnitTranslationKey(unitValue))); const submit = () => { saveUnitAndRate(unitValue, rateValue); @@ -131,7 +126,7 @@ function WorkspaceRateAndUnitPage(props: WorkspaceRateAndUnitPageProps) { /> Navigation.navigate(ROUTES.WORKSPACE_RATE_AND_UNIT_UNIT.getRoute(props.policy?.id ?? ''))} shouldShowRightIcon /> From 68dc3a44219067bb8c73afbd5be93ce61472c447 Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Tue, 19 Mar 2024 16:39:55 +0100 Subject: [PATCH 10/10] fix: offline and error handling --- src/libs/actions/Policy.ts | 29 ++++++++---- .../PolicyDistanceRatesSettingsPage.tsx | 45 ++++++++++++++----- src/types/onyx/Policy.ts | 1 + 3 files changed, 54 insertions(+), 21 deletions(-) diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index c08af39fab9f..3e4c71896b03 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -82,7 +82,7 @@ import type { ReportAction, Transaction, } from '@src/types/onyx'; -import type {Errors, OnyxValueWithOfflineFeedback, PendingAction} from '@src/types/onyx/OnyxCommon'; +import type {ErrorFields, Errors, OnyxValueWithOfflineFeedback, PendingAction} from '@src/types/onyx/OnyxCommon'; import type {OriginalMessageJoinPolicyChangeLog} from '@src/types/onyx/OriginalMessage'; import type {Attributes, CustomUnit, Rate, Unit} from '@src/types/onyx/Policy'; import type {OnyxData} from '@src/types/onyx/Request'; @@ -3893,6 +3893,16 @@ function clearCreateDistanceRateItemAndError(policyID: string, customUnitID: str }); } +function clearPolicyDistanceRatesErrorFields(policyID: string, customUnitID: string, updatedErrorFields: ErrorFields) { + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + customUnits: { + [customUnitID]: { + errorFields: updatedErrorFields, + }, + }, + }); +} + function setPolicyDistanceRatesUnit(policyID: string, currentCustomUnit: CustomUnit, newCustomUnit: CustomUnit) { const optimisticData: OnyxUpdate[] = [ { @@ -3902,7 +3912,7 @@ function setPolicyDistanceRatesUnit(policyID: string, currentCustomUnit: CustomU customUnits: { [newCustomUnit.customUnitID]: { ...newCustomUnit, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + pendingFields: {attributes: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, }, }, }, @@ -3916,7 +3926,7 @@ function setPolicyDistanceRatesUnit(policyID: string, currentCustomUnit: CustomU value: { customUnits: { [newCustomUnit.customUnitID]: { - pendingAction: null, + pendingFields: null, }, }, }, @@ -3931,8 +3941,8 @@ function setPolicyDistanceRatesUnit(policyID: string, currentCustomUnit: CustomU customUnits: { [currentCustomUnit.customUnitID]: { ...currentCustomUnit, - errors: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage'), - pendingAction: null, + errorFields: {attributes: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage')}, + pendingFields: {attributes: null}, }, }, }, @@ -3956,7 +3966,7 @@ function setPolicyDistanceRatesDefaultCategory(policyID: string, currentCustomUn customUnits: { [newCustomUnit.customUnitID]: { ...newCustomUnit, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + pendingFields: {defaultCategory: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, }, }, }, @@ -3970,7 +3980,7 @@ function setPolicyDistanceRatesDefaultCategory(policyID: string, currentCustomUn value: { customUnits: { [newCustomUnit.customUnitID]: { - pendingAction: null, + pendingFields: {defaultCategory: null}, }, }, }, @@ -3985,8 +3995,8 @@ function setPolicyDistanceRatesDefaultCategory(policyID: string, currentCustomUn customUnits: { [currentCustomUnit.customUnitID]: { ...currentCustomUnit, - errors: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage'), - pendingAction: null, + errorFields: {defaultCategory: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage')}, + pendingFields: {defaultCategory: null}, }, }, }, @@ -4243,4 +4253,5 @@ export { setWorkspaceCurrencyDefault, setForeignCurrencyDefault, setPolicyCustomTaxName, + clearPolicyDistanceRatesErrorFields, }; diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx index 2e0f02063905..f650a618250e 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx @@ -3,6 +3,7 @@ import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import type {ListItem} from '@components/SelectionList/types'; import type {UnitItemType} from '@components/UnitPicker'; @@ -15,6 +16,7 @@ import * as Policy from '@userActions/Policy'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; +import type {CustomUnit} from '@src/types/onyx/Policy'; import CategorySelector from './CategorySelector'; import UnitSelector from './UnitSelector'; @@ -36,6 +38,7 @@ function PolicyDistanceRatesSettingsPage({policy, route}: PolicyDistanceRatesSet const defaultCategory = customUnits[customUnitID].defaultCategory; const defaultUnit = customUnits[customUnitID].attributes.unit; + const errorFields = customUnits[customUnitID].errorFields; const setNewUnit = (unit: UnitItemType) => { Policy.setPolicyDistanceRatesUnit(policyID, customUnit, {...customUnit, attributes: {unit: unit.value}}); @@ -45,6 +48,10 @@ function PolicyDistanceRatesSettingsPage({policy, route}: PolicyDistanceRatesSet Policy.setPolicyDistanceRatesDefaultCategory(policyID, customUnit, {...customUnit, defaultCategory: category.text}); }; + const clearErrorFields = (fieldName: keyof CustomUnit) => { + Policy.clearPolicyDistanceRatesErrorFields(policyID, customUnitID, {...errorFields, [fieldName]: null}); + }; + return ( @@ -54,20 +61,34 @@ function PolicyDistanceRatesSettingsPage({policy, route}: PolicyDistanceRatesSet testID={PolicyDistanceRatesSettingsPage.displayName} > - - {policy?.areCategoriesEnabled && ( - clearErrorFields('attributes')} + > + + + {policy?.areCategoriesEnabled && ( + clearErrorFields('defaultCategory')} + > + + )} diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 84ac101a7d7a..46d86e8abb24 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -25,6 +25,7 @@ type CustomUnit = OnyxCommon.OnyxValueWithOfflineFeedback<{ defaultCategory?: string; enabled?: boolean; errors?: OnyxCommon.Errors; + errorFields?: OnyxCommon.ErrorFields; }>; type DisabledFields = {