From eaa19a1000a1a40acb10736a0ea7b1066585020d Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Mon, 2 Sep 2024 10:08:50 +0200 Subject: [PATCH 01/14] feat: add default categories handling --- src/libs/API/types.ts | 1 + src/libs/actions/Policy/Policy.ts | 56 +++++++++++++++++++ .../WorkspaceCategoriesSettingsPage.tsx | 53 ++++++++++++++++-- src/types/onyx/Policy.ts | 12 ++++ 4 files changed, 118 insertions(+), 4 deletions(-) diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index de63ed032afe..ec82775c6db8 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -14,6 +14,7 @@ const WRITE_COMMANDS = { SET_WORKSPACE_APPROVAL_MODE: 'SetWorkspaceApprovalMode', SET_WORKSPACE_PAYER: 'SetWorkspacePayer', SET_WORKSPACE_REIMBURSEMENT: 'SetWorkspaceReimbursement', + SET_WORKSPACE_DEFAULT_SPEND_CATEGORY: 'SetPolicyDefaultSpendCategory', DISMISS_REFERRAL_BANNER: 'DismissReferralBanner', UPDATE_PREFERRED_LOCALE: 'UpdatePreferredLocale', OPEN_APP: 'OpenApp', diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 19585a5e69c5..ea74cdf9962b 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -3380,6 +3380,61 @@ function upgradeToCorporate(policyID: string, featureName: string) { API.write(WRITE_COMMANDS.UPGRADE_TO_CORPORATE, parameters, {optimisticData, successData, failureData}); } +function setWorkspaceDefaultSpendCategory(policyID: string, groupID: string, category: string) { + const policy = getPolicy(policyID); + if (!policy) { + return; + } + + const {mccGroup} = policy; + + const optimisticData: OnyxUpdate[] = mccGroup + ? [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `policy_${policyID}`, + value: { + isPendingUpgrade: true, + mccGroup: { + ...mccGroup, + [groupID]: { + category, + groupID, + }, + }, + }, + }, + ] + : []; + + const successData: OnyxUpdate[] = mccGroup + ? [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `policy_${policyID}`, + value: { + isPendingUpgrade: false, + }, + }, + ] + : []; + + const failureData: OnyxUpdate[] = mccGroup + ? [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `policy_${policyID}`, + value: { + isPendingUpgrade: false, + mccGroup, + }, + }, + ] + : []; + + API.write(WRITE_COMMANDS.SET_WORKSPACE_DEFAULT_SPEND_CATEGORY, {policyID, groupID, category}, {optimisticData, successData, failureData}); +} + function getAdminPoliciesConnectedToSageIntacct(): Policy[] { return Object.values(allPolicies ?? {}).filter((policy): policy is Policy => !!policy && policy.role === CONST.POLICY.ROLE.ADMIN && !!policy?.connections?.intacct); } @@ -3403,6 +3458,7 @@ export { openWorkspaceReimburseView, setPolicyIDForReimburseView, clearOnyxDataForReimburseView, + setWorkspaceDefaultSpendCategory, setRateForReimburseView, setUnitForReimburseView, generateDefaultWorkspaceName, diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index 3c9edfd5fbf2..db2471dfc801 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -1,13 +1,17 @@ -import React from 'react'; +import React, {useMemo} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import FlatList from '@components/FlatList'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import CategorySelector from '@pages/workspace/distanceRates/CategorySelector'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -24,6 +28,7 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet const isConnectedToAccounting = Object.keys(policy?.connections ?? {}).length > 0; const policyID = route.params.policyID ?? '-1'; const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); + const [currentPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); const currentConnectionName = PolicyUtils.getCurrentConnectionName(policy); const toggleSubtitle = isConnectedToAccounting && currentConnectionName ? `${translate('workspace.categories.needCategoryForExportToIntegration')} ${currentConnectionName}.` : undefined; @@ -32,6 +37,21 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet setWorkspaceRequiresCategory(policyID, value); }; + const {data} = useMemo(() => { + if (!(currentPolicy && currentPolicy.mccGroup)) { + return {data: []}; + } + + return { + data: Object.keys(currentPolicy.mccGroup).map((mccKey) => ({ + mcc: mccKey[0].toUpperCase() + mccKey.slice(1), + category: currentPolicy.mccGroup?.[mccKey].category, + keyForList: mccKey, + groupID: mccKey, + })), + }; + }, [currentPolicy]); + const hasEnabledOptions = OptionsListUtils.hasEnabledOptions(policyCategories ?? {}); return ( - + Policy.clearPolicyErrorField(policy?.id ?? '-1', 'requiresCategory')} shouldPlaceSubtitleBelowSwitch /> - + {!!currentPolicy && data?.length > 0 && ( + <> + + Default spend categories + Customize how merchant spend is categorized for credit card transactions and scanned receipts. + + ( + { + if (!selectedCategory.text) { + return; + } + Policy.setWorkspaceDefaultSpendCategory(policyID, item.groupID, selectedCategory.text); + }} + /> + )} + /> + + )} + ); diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 9bac5f2e4de4..f5cdf5207ce3 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1226,6 +1226,15 @@ type Connections = { /** Names of integration connections */ type ConnectionName = keyof Connections; +/** Merchant Category Code. This is a way to identify the type of merchant (and type of spend) when a credit card is swiped. */ +type MccGroup = { + /** Default category for provided MCC Group */ + category: string; + + /** ID of the Merchant Category Code */ + groupID: string; +}; + /** Model of verified reimbursement bank account linked to policy */ type ACHAccount = { /** ID of the bank account */ @@ -1555,6 +1564,9 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< /** Whether GL codes are enabled */ glCodes?: boolean; + /** Policy MCC Group settings */ + mccGroup?: Record; + /** Workspace account ID configured for Expensify Card */ workspaceAccountID?: number; } & Partial, From 1874bff078b9e96c650cbbf1d4192703d821cfa7 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Mon, 2 Sep 2024 15:19:48 +0200 Subject: [PATCH 02/14] refactor: change flatlist to selection list --- .../CategorySelectorListItem.tsx | 57 +++++++++++ src/components/SelectionList/types.ts | 9 ++ .../WorkspaceCategoriesSettingsPage.tsx | 97 +++++++++---------- .../distanceRates/CategorySelector/index.tsx | 25 ++--- .../PolicyDistanceRatesSettingsPage.tsx | 6 +- 5 files changed, 132 insertions(+), 62 deletions(-) create mode 100644 src/components/SelectionList/CategorySelectorListItem.tsx diff --git a/src/components/SelectionList/CategorySelectorListItem.tsx b/src/components/SelectionList/CategorySelectorListItem.tsx new file mode 100644 index 000000000000..2cac48f3fc55 --- /dev/null +++ b/src/components/SelectionList/CategorySelectorListItem.tsx @@ -0,0 +1,57 @@ +import React, {useRef, useState} from 'react'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CategorySelector from '@pages/workspace/distanceRates/CategorySelector'; +import type {CategorySelectorRef} from '@pages/workspace/distanceRates/CategorySelector'; +import * as Policy from '@userActions/Policy/Policy'; +import BaseListItem from './BaseListItem'; +import type {InviteMemberListItemProps, ListItem} from './types'; + +function CategorySelectorListItem({item, onSelectRow, isFocused}: InviteMemberListItemProps) { + const styles = useThemeStyles(); + const [isCategoryPickerVisible, setIsCategoryPickerVisible] = useState(false); + const {policyID, groupID, categoryID} = item; + + if (!policyID || !groupID) { + return; + } + + const onSelect = (data: TItem) => { + setIsCategoryPickerVisible(true); + onSelectRow(data); + }; + + const setNewCategory = (selectedCategory: ListItem) => { + if (!selectedCategory.text) { + return; + } + Policy.setWorkspaceDefaultSpendCategory(policyID, groupID, selectedCategory.text); + }; + + return ( + + setIsCategoryPickerVisible(true)} + hidePickerModal={() => setIsCategoryPickerVisible(false)} + /> + + ); +} + +CategorySelectorListItem.displayName = 'CategorySelectorListItem'; + +export default CategorySelectorListItem; diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 824f5d82a3ce..7fa2073ba1a5 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -125,6 +125,15 @@ type ListItem = { /** ID of the report */ reportID?: string; + /** ID of the policy */ + policyID?: string; + + /** ID of the group */ + groupID?: string; + + /** ID of the category */ + categoryID?: string; + /** Whether this option should show subscript */ shouldShowSubscript?: boolean | null; diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index db2471dfc801..de0c0981fa55 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -1,17 +1,18 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; -import FlatList from '@components/FlatList'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; +import SelectionList from '@components/SelectionList'; +import CategorySelectorListItem from '@components/SelectionList/CategorySelectorListItem'; +import type {ListItem} from '@components/SelectionList/types'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import CategorySelector from '@pages/workspace/distanceRates/CategorySelector'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -37,20 +38,28 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet setWorkspaceRequiresCategory(policyID, value); }; - const {data} = useMemo(() => { + const {sections} = useMemo(() => { if (!(currentPolicy && currentPolicy.mccGroup)) { - return {data: []}; + return {sections: [{data: []}]}; } return { - data: Object.keys(currentPolicy.mccGroup).map((mccKey) => ({ - mcc: mccKey[0].toUpperCase() + mccKey.slice(1), - category: currentPolicy.mccGroup?.[mccKey].category, - keyForList: mccKey, - groupID: mccKey, - })), + sections: [ + { + data: Object.keys(currentPolicy.mccGroup).map( + (mccKey) => + ({ + categoryID: currentPolicy.mccGroup?.[mccKey].category, + keyForList: mccKey, + groupID: mccKey, + policyID, + tabIndex: -1, + } as ListItem), + ), + }, + ], }; - }, [currentPolicy]); + }, [currentPolicy, policyID]); const hasEnabledOptions = OptionsListUtils.hasEnabledOptions(policyCategories ?? {}); return ( @@ -65,46 +74,34 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet testID={WorkspaceCategoriesSettingsPage.displayName} > - - Policy.clearPolicyErrorField(policy?.id ?? '-1', 'requiresCategory')} - shouldPlaceSubtitleBelowSwitch - /> - {!!currentPolicy && data?.length > 0 && ( - <> - - Default spend categories - Customize how merchant spend is categorized for credit card transactions and scanned receipts. - - ( - { - if (!selectedCategory.text) { - return; - } - Policy.setWorkspaceDefaultSpendCategory(policyID, item.groupID, selectedCategory.text); - }} - /> - )} - /> - + Policy.clearPolicyErrorField(policy?.id ?? '-1', 'requiresCategory')} + shouldPlaceSubtitleBelowSwitch + /> + + {!!currentPolicy && sections?.length > 0 && ( + + Default spend categories + Customize how merchant spend is categorized for credit card transactions and scanned receipts. + + } + sections={sections} + ListItem={CategorySelectorListItem} + onSelectRow={() => {}} + /> )} - + ); diff --git a/src/pages/workspace/distanceRates/CategorySelector/index.tsx b/src/pages/workspace/distanceRates/CategorySelector/index.tsx index f7a1ba49d91e..965204b0429d 100644 --- a/src/pages/workspace/distanceRates/CategorySelector/index.tsx +++ b/src/pages/workspace/distanceRates/CategorySelector/index.tsx @@ -1,4 +1,4 @@ -import React, {useState} from 'react'; +import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; @@ -21,20 +21,22 @@ type CategorySelectorProps = { /** Any additional styles to apply */ wrapperStyle: StyleProp; -}; -function CategorySelector({defaultValue = '', wrapperStyle, label, setNewCategory, policyID}: CategorySelectorProps) { - const styles = useThemeStyles(); + /** Whether item is focused or active */ + focused?: boolean; - const [isPickerVisible, setIsPickerVisible] = useState(false); + /** Whether category item picker is visible */ + isPickerVisible: boolean; - const showPickerModal = () => { - setIsPickerVisible(true); - }; + /** Callback to show category picker */ + showPickerModal: () => void; - const hidePickerModal = () => { - setIsPickerVisible(false); - }; + /** Callback to hide category picker */ + hidePickerModal: () => void; +}; + +function CategorySelector({defaultValue = '', wrapperStyle, label, setNewCategory, policyID, focused, isPickerVisible, showPickerModal, hidePickerModal}: CategorySelectorProps) { + const styles = useThemeStyles(); const updateCategoryInput = (categoryItem: ListItem) => { setNewCategory(categoryItem); @@ -53,6 +55,7 @@ function CategorySelector({defaultValue = '', wrapperStyle, label, setNewCategor descriptionTextStyle={descStyle} onPress={showPickerModal} wrapperStyle={wrapperStyle} + focused={focused} /> setIsCategoryPickerVisible(true)} + hidePickerModal={() => setIsCategoryPickerVisible(false)} /> )} From 2646a9fe2ba735a7e3b72263a2ae219676abd44c Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Tue, 3 Sep 2024 00:36:00 +0200 Subject: [PATCH 03/14] fix: scrolling category selector bug --- src/components/CategoryPicker.tsx | 6 +++++- .../SelectionList/BaseSelectionList.tsx | 17 ++++++++++++++--- .../SelectionList/CategorySelectorListItem.tsx | 4 ++-- src/components/SelectionList/types.ts | 3 +++ .../WorkspaceCategoriesSettingsPage.tsx | 1 - .../CategorySelector/CategorySelectorModal.tsx | 6 +++++- .../distanceRates/CategorySelector/index.tsx | 17 ++++++++++++++++- 7 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index e5c85a8f5f6d..125af05eda06 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -24,9 +24,12 @@ type CategoryPickerProps = CategoryPickerOnyxProps & { policyID: string; selectedCategory?: string; onSubmit: (item: ListItem) => void; + + /** Whether SectionList should be wrapped with ScrollView */ + shouldWrapSectionList: boolean; }; -function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedCategories, policyCategoriesDraft, onSubmit}: CategoryPickerProps) { +function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedCategories, policyCategoriesDraft, onSubmit, shouldWrapSectionList}: CategoryPickerProps) { const {translate} = useLocalize(); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); @@ -84,6 +87,7 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC ListItem={RadioListItem} initiallyFocusedOptionKey={selectedOptionKey ?? undefined} isRowMultilineSupported + shouldWrapSectionList={shouldWrapSectionList} /> ); } diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 7d8f4c1738c8..b2aeb49e66ad 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -1,6 +1,6 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; import isEmpty from 'lodash/isEmpty'; -import type {ForwardedRef} from 'react'; +import type {ForwardedRef, ReactNode} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import type {LayoutChangeEvent, SectionList as RNSectionList, TextInput as RNTextInput, SectionListData, SectionListRenderItemInfo} from 'react-native'; import {View} from 'react-native'; @@ -10,6 +10,7 @@ import FixedFooter from '@components/FixedFooter'; import OptionsListSkeletonView from '@components/OptionsListSkeletonView'; import {PressableWithFeedback} from '@components/Pressable'; import SafeAreaConsumer from '@components/SafeAreaConsumer'; +import ScrollView from '@components/ScrollView'; import SectionList from '@components/SectionList'; import ShowMoreButton from '@components/ShowMoreButton'; import Text from '@components/Text'; @@ -33,6 +34,15 @@ import type {BaseSelectionListProps, ButtonOrCheckBoxRoles, FlattenedSectionsRet const getDefaultItemHeight = () => variables.optionRowHeight; +type SelectionListWrapperProps = {children: ReactNode; shouldWrapSectionList: boolean}; +function SelectionListWrapper({children, shouldWrapSectionList}: SelectionListWrapperProps) { + if (shouldWrapSectionList) { + return {children}; + } + + return children; +} + function BaseSelectionList( { sections, @@ -98,6 +108,7 @@ function BaseSelectionList( shouldUpdateFocusedIndex = false, onLongPressRow, shouldShowListEmptyContent = true, + shouldWrapSectionList = false, }: BaseSelectionListProps, ref: ForwardedRef, ) { @@ -697,7 +708,7 @@ function BaseSelectionList( {flattenedSections.allOptions.length === 0 ? ( renderListEmptyContent() ) : ( - <> + {!listHeaderContent && header()} ( onEndReachedThreshold={onEndReachedThreshold} /> {children} - + )} {showConfirmButton && ( diff --git a/src/components/SelectionList/CategorySelectorListItem.tsx b/src/components/SelectionList/CategorySelectorListItem.tsx index 2cac48f3fc55..07fecaa41be8 100644 --- a/src/components/SelectionList/CategorySelectorListItem.tsx +++ b/src/components/SelectionList/CategorySelectorListItem.tsx @@ -1,7 +1,6 @@ -import React, {useRef, useState} from 'react'; +import React, {useState} from 'react'; import useThemeStyles from '@hooks/useThemeStyles'; import CategorySelector from '@pages/workspace/distanceRates/CategorySelector'; -import type {CategorySelectorRef} from '@pages/workspace/distanceRates/CategorySelector'; import * as Policy from '@userActions/Policy/Policy'; import BaseListItem from './BaseListItem'; import type {InviteMemberListItemProps, ListItem} from './types'; @@ -47,6 +46,7 @@ function CategorySelectorListItem({item, onSelectRow, is isPickerVisible={isCategoryPickerVisible} showPickerModal={() => setIsCategoryPickerVisible(true)} hidePickerModal={() => setIsCategoryPickerVisible(false)} + shouldWrapSectionList /> ); diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 7fa2073ba1a5..5b7261033c44 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -501,6 +501,9 @@ type BaseSelectionListProps = Partial & { /** Whether to show the empty list content */ shouldShowListEmptyContent?: boolean; + + /** Whether SectionList should be wrapped with ScrollView */ + shouldWrapSectionList?: boolean; } & TRightHandSideComponent; type SelectionListHandle = { diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index de0c0981fa55..d03fefb021d0 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -3,7 +3,6 @@ import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; -import ScrollView from '@components/ScrollView'; import SelectionList from '@components/SelectionList'; import CategorySelectorListItem from '@components/SelectionList/CategorySelectorListItem'; import type {ListItem} from '@components/SelectionList/types'; diff --git a/src/pages/workspace/distanceRates/CategorySelector/CategorySelectorModal.tsx b/src/pages/workspace/distanceRates/CategorySelector/CategorySelectorModal.tsx index b48456ecce79..cc24bc53cd20 100644 --- a/src/pages/workspace/distanceRates/CategorySelector/CategorySelectorModal.tsx +++ b/src/pages/workspace/distanceRates/CategorySelector/CategorySelectorModal.tsx @@ -25,9 +25,12 @@ type CategorySelectorModalProps = { /** Label to display on field */ label: string; + + /** Whether SectionList should be wrapped with ScrollView */ + shouldWrapSectionList: boolean; }; -function CategorySelectorModal({policyID, isVisible, currentCategory, onCategorySelected, onClose, label}: CategorySelectorModalProps) { +function CategorySelectorModal({policyID, isVisible, currentCategory, onCategorySelected, onClose, label, shouldWrapSectionList}: CategorySelectorModalProps) { const styles = useThemeStyles(); return ( @@ -54,6 +57,7 @@ function CategorySelectorModal({policyID, isVisible, currentCategory, onCategory policyID={policyID} selectedCategory={currentCategory} onSubmit={onCategorySelected} + shouldWrapSectionList={shouldWrapSectionList} /> diff --git a/src/pages/workspace/distanceRates/CategorySelector/index.tsx b/src/pages/workspace/distanceRates/CategorySelector/index.tsx index 965204b0429d..2479d36b930d 100644 --- a/src/pages/workspace/distanceRates/CategorySelector/index.tsx +++ b/src/pages/workspace/distanceRates/CategorySelector/index.tsx @@ -33,9 +33,23 @@ type CategorySelectorProps = { /** Callback to hide category picker */ hidePickerModal: () => void; + + /** Whether SectionList should be wrapped with ScrollView */ + shouldWrapSectionList?: boolean; }; -function CategorySelector({defaultValue = '', wrapperStyle, label, setNewCategory, policyID, focused, isPickerVisible, showPickerModal, hidePickerModal}: CategorySelectorProps) { +function CategorySelector({ + defaultValue = '', + wrapperStyle, + label, + setNewCategory, + policyID, + focused, + isPickerVisible, + showPickerModal, + hidePickerModal, + shouldWrapSectionList = false, +}: CategorySelectorProps) { const styles = useThemeStyles(); const updateCategoryInput = (categoryItem: ListItem) => { @@ -64,6 +78,7 @@ function CategorySelector({defaultValue = '', wrapperStyle, label, setNewCategor onClose={hidePickerModal} onCategorySelected={updateCategoryInput} label={label} + shouldWrapSectionList={shouldWrapSectionList} /> ); From 62bc63e21ee4256b8d7670ee826b5d7e6d2a7123 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Tue, 3 Sep 2024 00:47:38 +0200 Subject: [PATCH 04/14] fix: SetWorkspaceDefaultSpendCategory API types --- .../parameters/SetWorkspaceDefaultSpendCategoryParams.ts | 7 +++++++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 1 + 3 files changed, 9 insertions(+) create mode 100644 src/libs/API/parameters/SetWorkspaceDefaultSpendCategoryParams.ts diff --git a/src/libs/API/parameters/SetWorkspaceDefaultSpendCategoryParams.ts b/src/libs/API/parameters/SetWorkspaceDefaultSpendCategoryParams.ts new file mode 100644 index 000000000000..84c2deacb546 --- /dev/null +++ b/src/libs/API/parameters/SetWorkspaceDefaultSpendCategoryParams.ts @@ -0,0 +1,7 @@ +type SetWorkspaceDefaultSpendCategoryParams = { + policyID: string; + groupID: string; + category: string; +}; + +export default SetWorkspaceDefaultSpendCategoryParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 997ee7dc1fc9..a94e4744fffb 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -178,6 +178,7 @@ export type {default as SetWorkspaceAutoReportingMonthlyOffsetParams} from './Se export type {default as SetWorkspaceApprovalModeParams} from './SetWorkspaceApprovalModeParams'; export type {default as SetWorkspacePayerParams} from './SetWorkspacePayerParams'; export type {default as SetWorkspaceReimbursementParams} from './SetWorkspaceReimbursementParams'; +export type {default as SetWorkspaceDefaultSpendCategoryParams} from './SetWorkspaceDefaultSpendCategoryParams'; export type {default as SetPolicyRequiresTag} from './SetPolicyRequiresTag'; export type {default as SetPolicyTagsRequired} from './SetPolicyTagsRequired'; export type {default as RenamePolicyTaglistParams} from './RenamePolicyTaglistParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 9fb71e67dc46..850b186975c5 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -541,6 +541,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE]: Parameters.SetWorkspaceApprovalModeParams; [WRITE_COMMANDS.SET_WORKSPACE_PAYER]: Parameters.SetWorkspacePayerParams; [WRITE_COMMANDS.SET_WORKSPACE_REIMBURSEMENT]: Parameters.SetWorkspaceReimbursementParams; + [WRITE_COMMANDS.SET_WORKSPACE_DEFAULT_SPEND_CATEGORY]: Parameters.SetWorkspaceDefaultSpendCategoryParams; [WRITE_COMMANDS.SWITCH_TO_OLD_DOT]: Parameters.SwitchToOldDotParams; [WRITE_COMMANDS.TRACK_EXPENSE]: Parameters.TrackExpenseParams; [WRITE_COMMANDS.ENABLE_POLICY_CATEGORIES]: Parameters.EnablePolicyCategoriesParams; From 2fd739b4c968583fec4569d17d091d616636ea99 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Tue, 3 Sep 2024 00:55:34 +0200 Subject: [PATCH 05/14] fix: CategoryPicker types --- src/components/CategoryPicker.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index 125af05eda06..a48366930d73 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -26,10 +26,10 @@ type CategoryPickerProps = CategoryPickerOnyxProps & { onSubmit: (item: ListItem) => void; /** Whether SectionList should be wrapped with ScrollView */ - shouldWrapSectionList: boolean; + shouldWrapSectionList?: boolean; }; -function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedCategories, policyCategoriesDraft, onSubmit, shouldWrapSectionList}: CategoryPickerProps) { +function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedCategories, policyCategoriesDraft, onSubmit, shouldWrapSectionList = false}: CategoryPickerProps) { const {translate} = useLocalize(); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); From 09cd340bb83a4bfa3d9753203ce2c6dcfb2705a2 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Tue, 3 Sep 2024 01:48:43 +0200 Subject: [PATCH 06/14] feat: add translations --- src/languages/en.ts | 2 ++ src/languages/es.ts | 2 ++ .../workspace/categories/WorkspaceCategoriesSettingsPage.tsx | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 30e2dad391d9..526394a54732 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2823,6 +2823,8 @@ export default { disableCategory: 'Disable category', enableCategories: 'Enable categories', enableCategory: 'Enable category', + defaultSpendCategories: 'Default spend categories', + spendCategoriesDescription: 'Customize how merchant spend is categorized for credit card transactions and scanned receipts.', deleteFailureMessage: 'An error occurred while deleting the category, please try again.', categoryName: 'Category name', requiresCategory: 'Members must categorize all expenses', diff --git a/src/languages/es.ts b/src/languages/es.ts index 16b8f6f42ed2..84777f56a3b9 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2871,6 +2871,8 @@ export default { disableCategory: 'Desactivar categoría', enableCategories: 'Activar categorías', enableCategory: 'Activar categoría', + defaultSpendCategories: 'Categorías de gasto predeterminadas', + spendCategoriesDescription: 'Personaliza cómo se categorizan los gastos de los comerciantes para las transacciones con tarjeta de crédito y los recibos escaneados.', deleteFailureMessage: 'Se ha producido un error al intentar eliminar la categoría. Por favor, inténtalo más tarde.', categoryName: 'Nombre de la categoría', requiresCategory: 'Los miembros deben clasificar todos los gastos', diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index d03fefb021d0..46a68f0148f2 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -91,8 +91,8 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet - Default spend categories - Customize how merchant spend is categorized for credit card transactions and scanned receipts. + {translate('workspace.categories.defaultSpendCategories')} + {translate('workspace.categories.spendCategoriesDescription')} } sections={sections} From 4b9d455359541cc0945a54ef6eccfc287cf0e066 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Tue, 3 Sep 2024 12:42:44 +0200 Subject: [PATCH 07/14] fix: VirtualizedList inside ScrollView error --- src/components/CategoryPicker.tsx | 12 ++++++++---- src/components/SelectionList/BaseSelectionList.tsx | 14 ++------------ .../SelectionList/CategorySelectorListItem.tsx | 2 +- src/components/SelectionList/types.ts | 3 --- .../categories/WorkspaceCategoriesSettingsPage.tsx | 4 ++-- .../CategorySelector/CategorySelectorModal.tsx | 8 ++++---- .../distanceRates/CategorySelector/index.tsx | 8 ++++---- 7 files changed, 21 insertions(+), 30 deletions(-) diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index a48366930d73..7932e6889259 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -3,6 +3,8 @@ import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import getPlatform from '@libs/getPlatform'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -25,11 +27,13 @@ type CategoryPickerProps = CategoryPickerOnyxProps & { selectedCategory?: string; onSubmit: (item: ListItem) => void; - /** Whether SectionList should be wrapped with ScrollView */ - shouldWrapSectionList?: boolean; + /** Whether SectionList should have overflow: "auto" enabled */ + shouldAddOverflow?: boolean; }; -function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedCategories, policyCategoriesDraft, onSubmit, shouldWrapSectionList = false}: CategoryPickerProps) { +function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedCategories, policyCategoriesDraft, onSubmit, shouldAddOverflow = false}: CategoryPickerProps) { + const styles = useThemeStyles(); + const isWeb = getPlatform() === CONST.PLATFORM.WEB; const {translate} = useLocalize(); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); @@ -87,7 +91,7 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC ListItem={RadioListItem} initiallyFocusedOptionKey={selectedOptionKey ?? undefined} isRowMultilineSupported - shouldWrapSectionList={shouldWrapSectionList} + sectionListStyle={isWeb && shouldAddOverflow && [styles.overflowAuto, styles.flex1]} /> ); } diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index b2aeb49e66ad..97510491cbbd 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -34,15 +34,6 @@ import type {BaseSelectionListProps, ButtonOrCheckBoxRoles, FlattenedSectionsRet const getDefaultItemHeight = () => variables.optionRowHeight; -type SelectionListWrapperProps = {children: ReactNode; shouldWrapSectionList: boolean}; -function SelectionListWrapper({children, shouldWrapSectionList}: SelectionListWrapperProps) { - if (shouldWrapSectionList) { - return {children}; - } - - return children; -} - function BaseSelectionList( { sections, @@ -108,7 +99,6 @@ function BaseSelectionList( shouldUpdateFocusedIndex = false, onLongPressRow, shouldShowListEmptyContent = true, - shouldWrapSectionList = false, }: BaseSelectionListProps, ref: ForwardedRef, ) { @@ -708,7 +698,7 @@ function BaseSelectionList( {flattenedSections.allOptions.length === 0 ? ( renderListEmptyContent() ) : ( - + <> {!listHeaderContent && header()} ( onEndReachedThreshold={onEndReachedThreshold} /> {children} - + )} {showConfirmButton && ( diff --git a/src/components/SelectionList/CategorySelectorListItem.tsx b/src/components/SelectionList/CategorySelectorListItem.tsx index 07fecaa41be8..f23b800fb322 100644 --- a/src/components/SelectionList/CategorySelectorListItem.tsx +++ b/src/components/SelectionList/CategorySelectorListItem.tsx @@ -46,7 +46,7 @@ function CategorySelectorListItem({item, onSelectRow, is isPickerVisible={isCategoryPickerVisible} showPickerModal={() => setIsCategoryPickerVisible(true)} hidePickerModal={() => setIsCategoryPickerVisible(false)} - shouldWrapSectionList + shouldAddOverflow /> ); diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 5b7261033c44..7fa2073ba1a5 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -501,9 +501,6 @@ type BaseSelectionListProps = Partial & { /** Whether to show the empty list content */ shouldShowListEmptyContent?: boolean; - - /** Whether SectionList should be wrapped with ScrollView */ - shouldWrapSectionList?: boolean; } & TRightHandSideComponent; type SelectionListHandle = { diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index 46a68f0148f2..5ba15727752c 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -81,7 +81,7 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet onToggle={updateWorkspaceRequiresCategory} pendingAction={policy?.pendingFields?.requiresCategory} disabled={!policy?.areCategoriesEnabled || !hasEnabledOptions || isConnectedToAccounting} - wrapperStyle={[styles.pv2, styles.mh4]} + wrapperStyle={[styles.pv2, styles.mh5]} errors={policy?.errorFields?.requiresCategory ?? undefined} onCloseError={() => Policy.clearPolicyErrorField(policy?.id ?? '-1', 'requiresCategory')} shouldPlaceSubtitleBelowSwitch @@ -90,7 +90,7 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet {!!currentPolicy && sections?.length > 0 && ( + {translate('workspace.categories.defaultSpendCategories')} {translate('workspace.categories.spendCategoriesDescription')} diff --git a/src/pages/workspace/distanceRates/CategorySelector/CategorySelectorModal.tsx b/src/pages/workspace/distanceRates/CategorySelector/CategorySelectorModal.tsx index cc24bc53cd20..b555441f1f73 100644 --- a/src/pages/workspace/distanceRates/CategorySelector/CategorySelectorModal.tsx +++ b/src/pages/workspace/distanceRates/CategorySelector/CategorySelectorModal.tsx @@ -26,11 +26,11 @@ type CategorySelectorModalProps = { /** Label to display on field */ label: string; - /** Whether SectionList should be wrapped with ScrollView */ - shouldWrapSectionList: boolean; + /** Whether SectionList should have overflow: "auto" enabled */ + shouldAddOverflow: boolean; }; -function CategorySelectorModal({policyID, isVisible, currentCategory, onCategorySelected, onClose, label, shouldWrapSectionList}: CategorySelectorModalProps) { +function CategorySelectorModal({policyID, isVisible, currentCategory, onCategorySelected, onClose, label, shouldAddOverflow}: CategorySelectorModalProps) { const styles = useThemeStyles(); return ( @@ -57,7 +57,7 @@ function CategorySelectorModal({policyID, isVisible, currentCategory, onCategory policyID={policyID} selectedCategory={currentCategory} onSubmit={onCategorySelected} - shouldWrapSectionList={shouldWrapSectionList} + shouldAddOverflow={shouldAddOverflow} /> diff --git a/src/pages/workspace/distanceRates/CategorySelector/index.tsx b/src/pages/workspace/distanceRates/CategorySelector/index.tsx index 2479d36b930d..ef6e188bfda2 100644 --- a/src/pages/workspace/distanceRates/CategorySelector/index.tsx +++ b/src/pages/workspace/distanceRates/CategorySelector/index.tsx @@ -34,8 +34,8 @@ type CategorySelectorProps = { /** Callback to hide category picker */ hidePickerModal: () => void; - /** Whether SectionList should be wrapped with ScrollView */ - shouldWrapSectionList?: boolean; + /** Whether SectionList should have overflow: "auto" enabled */ + shouldAddOverflow?: boolean; }; function CategorySelector({ @@ -48,7 +48,7 @@ function CategorySelector({ isPickerVisible, showPickerModal, hidePickerModal, - shouldWrapSectionList = false, + shouldAddOverflow = false, }: CategorySelectorProps) { const styles = useThemeStyles(); @@ -78,7 +78,7 @@ function CategorySelector({ onClose={hidePickerModal} onCategorySelected={updateCategoryInput} label={label} - shouldWrapSectionList={shouldWrapSectionList} + shouldAddOverflow={shouldAddOverflow} /> ); From 645bb70f85a469575b07f91310e71042ddcb3fb4 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Tue, 3 Sep 2024 12:52:37 +0200 Subject: [PATCH 08/14] fix: remove unused types --- src/components/SelectionList/BaseSelectionList.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 97510491cbbd..7d8f4c1738c8 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -1,6 +1,6 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; import isEmpty from 'lodash/isEmpty'; -import type {ForwardedRef, ReactNode} from 'react'; +import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import type {LayoutChangeEvent, SectionList as RNSectionList, TextInput as RNTextInput, SectionListData, SectionListRenderItemInfo} from 'react-native'; import {View} from 'react-native'; @@ -10,7 +10,6 @@ import FixedFooter from '@components/FixedFooter'; import OptionsListSkeletonView from '@components/OptionsListSkeletonView'; import {PressableWithFeedback} from '@components/Pressable'; import SafeAreaConsumer from '@components/SafeAreaConsumer'; -import ScrollView from '@components/ScrollView'; import SectionList from '@components/SectionList'; import ShowMoreButton from '@components/ShowMoreButton'; import Text from '@components/Text'; From 898ff98a1843910a74f814873def0516c306af4a Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Tue, 3 Sep 2024 13:23:49 +0200 Subject: [PATCH 09/14] fix: cleanup --- .../categories/WorkspaceCategoriesSettingsPage.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index 5ba15727752c..3383e8e33735 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -45,10 +45,10 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet return { sections: [ { - data: Object.keys(currentPolicy.mccGroup).map( - (mccKey) => + data: Object.entries(currentPolicy.mccGroup).map( + ([mccKey, mccGroup]) => ({ - categoryID: currentPolicy.mccGroup?.[mccKey].category, + categoryID: mccGroup.category, keyForList: mccKey, groupID: mccKey, policyID, @@ -61,6 +61,8 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet }, [currentPolicy, policyID]); const hasEnabledOptions = OptionsListUtils.hasEnabledOptions(policyCategories ?? {}); + const isToggleDisabled = !policy?.areCategoriesEnabled || !hasEnabledOptions || isConnectedToAccounting; + return ( Policy.clearPolicyErrorField(policy?.id ?? '-1', 'requiresCategory')} From 5981f6e6d780257b944161feb2b8affef061b362 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Tue, 3 Sep 2024 13:33:31 +0200 Subject: [PATCH 10/14] refactor: add a comment explaining sectionListStyles --- src/components/CategoryPicker.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index 7932e6889259..7156ce537b41 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -36,6 +36,8 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC const isWeb = getPlatform() === CONST.PLATFORM.WEB; const {translate} = useLocalize(); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); + // Ensure scrolling works for the SectionList in a nested lists structure involving a Modal on the web. + const sectionListStyles = isWeb && shouldAddOverflow && [styles.overflowAuto, styles.flex1]; const selectedOptions = useMemo(() => { if (!selectedCategory) { @@ -91,7 +93,7 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC ListItem={RadioListItem} initiallyFocusedOptionKey={selectedOptionKey ?? undefined} isRowMultilineSupported - sectionListStyle={isWeb && shouldAddOverflow && [styles.overflowAuto, styles.flex1]} + sectionListStyle={sectionListStyles} /> ); } From 73431914cd10b04cb137fb671d8df5f68a71f0fc Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Wed, 4 Sep 2024 12:19:01 +0200 Subject: [PATCH 11/14] refactor: address review comments --- src/libs/actions/Policy/Policy.ts | 16 +--------------- .../SpendCategorySelectorListItem.tsx} | 10 +++++----- .../WorkspaceCategoriesSettingsPage.tsx | 6 +++--- 3 files changed, 9 insertions(+), 23 deletions(-) rename src/{components/SelectionList/CategorySelectorListItem.tsx => pages/workspace/categories/SpendCategorySelectorListItem.tsx} (81%) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 9599bcc40b06..49bfb1c20b09 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -3468,7 +3468,6 @@ function setWorkspaceDefaultSpendCategory(policyID: string, groupID: string, cat onyxMethod: Onyx.METHOD.MERGE, key: `policy_${policyID}`, value: { - isPendingUpgrade: true, mccGroup: { ...mccGroup, [groupID]: { @@ -3481,32 +3480,19 @@ function setWorkspaceDefaultSpendCategory(policyID: string, groupID: string, cat ] : []; - const successData: OnyxUpdate[] = mccGroup - ? [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `policy_${policyID}`, - value: { - isPendingUpgrade: false, - }, - }, - ] - : []; - const failureData: OnyxUpdate[] = mccGroup ? [ { onyxMethod: Onyx.METHOD.MERGE, key: `policy_${policyID}`, value: { - isPendingUpgrade: false, mccGroup, }, }, ] : []; - API.write(WRITE_COMMANDS.SET_WORKSPACE_DEFAULT_SPEND_CATEGORY, {policyID, groupID, category}, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.SET_WORKSPACE_DEFAULT_SPEND_CATEGORY, {policyID, groupID, category}, {optimisticData, successData: [], failureData}); } /** * Call the API to set the receipt required amount for the given policy diff --git a/src/components/SelectionList/CategorySelectorListItem.tsx b/src/pages/workspace/categories/SpendCategorySelectorListItem.tsx similarity index 81% rename from src/components/SelectionList/CategorySelectorListItem.tsx rename to src/pages/workspace/categories/SpendCategorySelectorListItem.tsx index f23b800fb322..77dede3cebbe 100644 --- a/src/components/SelectionList/CategorySelectorListItem.tsx +++ b/src/pages/workspace/categories/SpendCategorySelectorListItem.tsx @@ -1,11 +1,11 @@ import React, {useState} from 'react'; +import BaseListItem from '@components/SelectionList/BaseListItem'; +import type {BaseListItemProps, ListItem} from '@components/SelectionList/types'; import useThemeStyles from '@hooks/useThemeStyles'; import CategorySelector from '@pages/workspace/distanceRates/CategorySelector'; import * as Policy from '@userActions/Policy/Policy'; -import BaseListItem from './BaseListItem'; -import type {InviteMemberListItemProps, ListItem} from './types'; -function CategorySelectorListItem({item, onSelectRow, isFocused}: InviteMemberListItemProps) { +function SpendCategorySelectorListItem({item, onSelectRow, isFocused}: BaseListItemProps) { const styles = useThemeStyles(); const [isCategoryPickerVisible, setIsCategoryPickerVisible] = useState(false); const {policyID, groupID, categoryID} = item; @@ -52,6 +52,6 @@ function CategorySelectorListItem({item, onSelectRow, is ); } -CategorySelectorListItem.displayName = 'CategorySelectorListItem'; +SpendCategorySelectorListItem.displayName = 'SpendCategorySelectorListItem'; -export default CategorySelectorListItem; +export default SpendCategorySelectorListItem; diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index 3383e8e33735..64c875b60cdb 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -4,7 +4,7 @@ import {useOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; -import CategorySelectorListItem from '@components/SelectionList/CategorySelectorListItem'; +import SpendCategorySelectorListItem from '@components/SelectionList/SpendCategorySelectorListItem'; import type {ListItem} from '@components/SelectionList/types'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; @@ -92,13 +92,13 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet {!!currentPolicy && sections?.length > 0 && ( + {translate('workspace.categories.defaultSpendCategories')} {translate('workspace.categories.spendCategoriesDescription')} } sections={sections} - ListItem={CategorySelectorListItem} + ListItem={SpendCategorySelectorListItem} onSelectRow={() => {}} /> )} From 41e696825344ae115c26ed03195a472383202607 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Wed, 4 Sep 2024 14:06:43 +0200 Subject: [PATCH 12/14] fix: scroll error --- src/components/CategoryPicker.tsx | 14 ++++---------- src/components/SelectionList/BaseSelectionList.tsx | 8 +++++++- src/components/SelectionList/types.ts | 3 +++ .../categories/SpendCategorySelectorListItem.tsx | 2 +- .../categories/WorkspaceCategoriesSettingsPage.tsx | 2 +- .../CategorySelector/CategorySelectorModal.tsx | 8 ++++---- .../distanceRates/CategorySelector/index.tsx | 8 ++++---- 7 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index 7156ce537b41..7955e5993ba9 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -3,8 +3,6 @@ import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import getPlatform from '@libs/getPlatform'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -27,17 +25,13 @@ type CategoryPickerProps = CategoryPickerOnyxProps & { selectedCategory?: string; onSubmit: (item: ListItem) => void; - /** Whether SectionList should have overflow: "auto" enabled */ - shouldAddOverflow?: boolean; + /** Whether SectionList should use custom ScrollView */ + shouldUseCustomScrollView?: boolean; }; -function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedCategories, policyCategoriesDraft, onSubmit, shouldAddOverflow = false}: CategoryPickerProps) { - const styles = useThemeStyles(); - const isWeb = getPlatform() === CONST.PLATFORM.WEB; +function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedCategories, policyCategoriesDraft, onSubmit, shouldUseCustomScrollView = false}: CategoryPickerProps) { const {translate} = useLocalize(); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); - // Ensure scrolling works for the SectionList in a nested lists structure involving a Modal on the web. - const sectionListStyles = isWeb && shouldAddOverflow && [styles.overflowAuto, styles.flex1]; const selectedOptions = useMemo(() => { if (!selectedCategory) { @@ -93,7 +87,7 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC ListItem={RadioListItem} initiallyFocusedOptionKey={selectedOptionKey ?? undefined} isRowMultilineSupported - sectionListStyle={sectionListStyles} + shouldUseCustomScrollView={shouldUseCustomScrollView} /> ); } diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 7d8f4c1738c8..ffeeb12150b1 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -2,7 +2,7 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; import isEmpty from 'lodash/isEmpty'; import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import type {LayoutChangeEvent, SectionList as RNSectionList, TextInput as RNTextInput, SectionListData, SectionListRenderItemInfo} from 'react-native'; +import type {LayoutChangeEvent, SectionList as RNSectionList, TextInput as RNTextInput, ScrollViewProps, SectionListData, SectionListRenderItemInfo} from 'react-native'; import {View} from 'react-native'; import Button from '@components/Button'; import Checkbox from '@components/Checkbox'; @@ -10,6 +10,7 @@ import FixedFooter from '@components/FixedFooter'; import OptionsListSkeletonView from '@components/OptionsListSkeletonView'; import {PressableWithFeedback} from '@components/Pressable'; import SafeAreaConsumer from '@components/SafeAreaConsumer'; +import ScrollView from '@components/ScrollView'; import SectionList from '@components/SectionList'; import ShowMoreButton from '@components/ShowMoreButton'; import Text from '@components/Text'; @@ -73,6 +74,7 @@ function BaseSelectionList( shouldStopPropagation = false, shouldShowTooltips = true, shouldUseDynamicMaxToRenderPerBatch = false, + shouldUseCustomScrollView = false, rightHandSideComponent, isLoadingNewOptions = false, onLayout, @@ -423,6 +425,9 @@ function BaseSelectionList( ); + // eslint-disable-next-line react/jsx-props-no-spreading + const scrollComponent = shouldUseCustomScrollView ? (props: ScrollViewProps) => : undefined; + const renderItem = ({item, index, section}: SectionListRenderItemInfo>) => { const normalizedIndex = index + (section?.indexOffset ?? 0); const isDisabled = !!section.isDisabled || item.isDisabled; @@ -701,6 +706,7 @@ function BaseSelectionList( {!listHeaderContent && header()} = Partial & { /** Whether to use dynamic maxToRenderPerBatch depending on the visible number of elements */ shouldUseDynamicMaxToRenderPerBatch?: boolean; + /** Whether SectionList should use custom ScrollView */ + shouldUseCustomScrollView?: boolean; + /** Whether keyboard shortcuts should be disabled */ disableKeyboardShortcuts?: boolean; diff --git a/src/pages/workspace/categories/SpendCategorySelectorListItem.tsx b/src/pages/workspace/categories/SpendCategorySelectorListItem.tsx index 77dede3cebbe..b52b7a4ec0be 100644 --- a/src/pages/workspace/categories/SpendCategorySelectorListItem.tsx +++ b/src/pages/workspace/categories/SpendCategorySelectorListItem.tsx @@ -46,7 +46,7 @@ function SpendCategorySelectorListItem({item, onSelectRo isPickerVisible={isCategoryPickerVisible} showPickerModal={() => setIsCategoryPickerVisible(true)} hidePickerModal={() => setIsCategoryPickerVisible(false)} - shouldAddOverflow + shouldUseCustomScrollView /> ); diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index 64c875b60cdb..927128a8953b 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -4,7 +4,6 @@ import {useOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; -import SpendCategorySelectorListItem from '@components/SelectionList/SpendCategorySelectorListItem'; import type {ListItem} from '@components/SelectionList/types'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; @@ -19,6 +18,7 @@ import {setWorkspaceRequiresCategory} from '@userActions/Policy/Category'; import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import SpendCategorySelectorListItem from './SpendCategorySelectorListItem'; type WorkspaceCategoriesSettingsPageProps = WithPolicyConnectionsProps; diff --git a/src/pages/workspace/distanceRates/CategorySelector/CategorySelectorModal.tsx b/src/pages/workspace/distanceRates/CategorySelector/CategorySelectorModal.tsx index b555441f1f73..ce40753a23fc 100644 --- a/src/pages/workspace/distanceRates/CategorySelector/CategorySelectorModal.tsx +++ b/src/pages/workspace/distanceRates/CategorySelector/CategorySelectorModal.tsx @@ -26,11 +26,11 @@ type CategorySelectorModalProps = { /** Label to display on field */ label: string; - /** Whether SectionList should have overflow: "auto" enabled */ - shouldAddOverflow: boolean; + /** Whether SectionList should use custom ScrollView */ + shouldUseCustomScrollView: boolean; }; -function CategorySelectorModal({policyID, isVisible, currentCategory, onCategorySelected, onClose, label, shouldAddOverflow}: CategorySelectorModalProps) { +function CategorySelectorModal({policyID, isVisible, currentCategory, onCategorySelected, onClose, label, shouldUseCustomScrollView}: CategorySelectorModalProps) { const styles = useThemeStyles(); return ( @@ -57,7 +57,7 @@ function CategorySelectorModal({policyID, isVisible, currentCategory, onCategory policyID={policyID} selectedCategory={currentCategory} onSubmit={onCategorySelected} - shouldAddOverflow={shouldAddOverflow} + shouldUseCustomScrollView={shouldUseCustomScrollView} /> diff --git a/src/pages/workspace/distanceRates/CategorySelector/index.tsx b/src/pages/workspace/distanceRates/CategorySelector/index.tsx index ef6e188bfda2..8628a4df0178 100644 --- a/src/pages/workspace/distanceRates/CategorySelector/index.tsx +++ b/src/pages/workspace/distanceRates/CategorySelector/index.tsx @@ -34,8 +34,8 @@ type CategorySelectorProps = { /** Callback to hide category picker */ hidePickerModal: () => void; - /** Whether SectionList should have overflow: "auto" enabled */ - shouldAddOverflow?: boolean; + /** Whether SectionList should use custom ScrollView */ + shouldUseCustomScrollView?: boolean; }; function CategorySelector({ @@ -48,7 +48,7 @@ function CategorySelector({ isPickerVisible, showPickerModal, hidePickerModal, - shouldAddOverflow = false, + shouldUseCustomScrollView = false, }: CategorySelectorProps) { const styles = useThemeStyles(); @@ -78,7 +78,7 @@ function CategorySelector({ onClose={hidePickerModal} onCategorySelected={updateCategoryInput} label={label} - shouldAddOverflow={shouldAddOverflow} + shouldUseCustomScrollView={shouldUseCustomScrollView} /> ); From b7742e5b9bd231bf8bfe65496ce34c352daceffa Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Wed, 4 Sep 2024 14:23:54 +0200 Subject: [PATCH 13/14] fix: focus button --- .../workspace/categories/SpendCategorySelectorListItem.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/categories/SpendCategorySelectorListItem.tsx b/src/pages/workspace/categories/SpendCategorySelectorListItem.tsx index b52b7a4ec0be..76b0f11b467a 100644 --- a/src/pages/workspace/categories/SpendCategorySelectorListItem.tsx +++ b/src/pages/workspace/categories/SpendCategorySelectorListItem.tsx @@ -2,6 +2,7 @@ import React, {useState} from 'react'; import BaseListItem from '@components/SelectionList/BaseListItem'; import type {BaseListItemProps, ListItem} from '@components/SelectionList/types'; import useThemeStyles from '@hooks/useThemeStyles'; +import blurActiveElement from '@libs/Accessibility/blurActiveElement'; import CategorySelector from '@pages/workspace/distanceRates/CategorySelector'; import * as Policy from '@userActions/Policy/Policy'; @@ -45,7 +46,10 @@ function SpendCategorySelectorListItem({item, onSelectRo setNewCategory={setNewCategory} isPickerVisible={isCategoryPickerVisible} showPickerModal={() => setIsCategoryPickerVisible(true)} - hidePickerModal={() => setIsCategoryPickerVisible(false)} + hidePickerModal={() => { + setIsCategoryPickerVisible(false); + blurActiveElement(); + }} shouldUseCustomScrollView /> From 147fc6932dd13757bf5d48294adcb20ad368c6a9 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Wed, 4 Sep 2024 17:34:50 +0200 Subject: [PATCH 14/14] fix: hide section header when empty --- .../workspace/categories/WorkspaceCategoriesSettingsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index 927128a8953b..03c01e5a7264 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -89,7 +89,7 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet shouldPlaceSubtitleBelowSwitch /> - {!!currentPolicy && sections?.length > 0 && ( + {!!currentPolicy && sections[0].data.length > 0 && (