From bf1c5dee7adc25d701b02ad920dde009192ad5b1 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Tue, 30 Jun 2026 03:43:28 +0530 Subject: [PATCH 1/3] Migrate workspace per diem page to the new Table component Signed-off-by: krishna2323 --- src/CONST/index.ts | 1 + .../WorkspacePerDiemTableRow.tsx | 111 ++++++ .../Tables/WorkspacePerDiemTable/index.tsx | 118 ++++++ src/libs/Navigation/types.ts | 2 + .../perDiem/WorkspacePerDiemPage.tsx | 337 ++++++------------ 5 files changed, 346 insertions(+), 223 deletions(-) create mode 100644 src/components/Tables/WorkspacePerDiemTable/WorkspacePerDiemTableRow.tsx create mode 100644 src/components/Tables/WorkspacePerDiemTable/index.tsx diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 6e9546ea95f6..e96f733aff87 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -8379,6 +8379,7 @@ const CONST = { CHOOSE_SPEND_RULE: 'WorkspaceExpensifyCard-ChooseSpendRule', }, PER_DIEM: { + ROW: 'WorkspacePerDiem-Row', MORE_DROPDOWN: 'WorkspacePerDiem-MoreDropdown', BULK_ACTIONS_DROPDOWN: 'WorkspacePerDiem-BulkActionsDropdown', }, diff --git a/src/components/Tables/WorkspacePerDiemTable/WorkspacePerDiemTableRow.tsx b/src/components/Tables/WorkspacePerDiemTable/WorkspacePerDiemTableRow.tsx new file mode 100644 index 000000000000..9ca68877f602 --- /dev/null +++ b/src/components/Tables/WorkspacePerDiemTable/WorkspacePerDiemTableRow.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import {View} from 'react-native'; +import Icon from '@components/Icon'; +import Table from '@components/Table'; +import type {TableData} from '@components/Table'; +import TextWithTooltip from '@components/TextWithTooltip'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; + +type PerDiemTableRowData = TableData & { + subRateID: string; + rateID: string; + destination: string; + subRateName: string; + rate: number; + formattedAmount: string; + disabled?: boolean; + pendingAction?: OnyxCommon.PendingAction; + action: () => void; +}; + +type WorkspacePerDiemTableRowProps = { + item: PerDiemTableRowData; + rowIndex: number; + shouldUseNarrowTableLayout: boolean; +}; + +function WorkspacePerDiemTableRow({item, rowIndex, shouldUseNarrowTableLayout}: WorkspacePerDiemTableRowProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const icons = useMemoizedLazyExpensifyIcons(['ArrowRight']); + + const accessibilityLabel = [item.destination, item.subRateName, item.formattedAmount].filter(Boolean).join(', '); + + return ( + + {({hovered}) => ( + <> + {shouldUseNarrowTableLayout && ( + + + + + )} + + {!shouldUseNarrowTableLayout && ( + + + + )} + + {!shouldUseNarrowTableLayout && ( + + + + )} + + {!shouldUseNarrowTableLayout && ( + + + + )} + + + + )} + + ); +} + +export default WorkspacePerDiemTableRow; +export type {PerDiemTableRowData}; diff --git a/src/components/Tables/WorkspacePerDiemTable/index.tsx b/src/components/Tables/WorkspacePerDiemTable/index.tsx new file mode 100644 index 000000000000..675040bd3c4d --- /dev/null +++ b/src/components/Tables/WorkspacePerDiemTable/index.tsx @@ -0,0 +1,118 @@ +import type {ListRenderItemInfo} from '@shopify/flash-list'; +import React from 'react'; +import Table from '@components/Table'; +import type {CompareItemsCallback, IsItemInSearchCallback, TableColumn} from '@components/Table'; +import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import tokenizedSearch from '@libs/tokenizedSearch'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import WorkspacePerDiemTableRow from './WorkspacePerDiemTableRow'; +import type {PerDiemTableRowData} from './WorkspacePerDiemTableRow'; + +type PerDiemTableColumnKey = 'destination' | 'subrate' | 'amount' | 'actions'; + +type WorkspacePerDiemTableProps = { + perDiemData: PerDiemTableRowData[]; + selectionEnabled: boolean; + selectedKeys: string[]; + onRowSelectionChange: (selectedRowKeys: string[]) => void; + EmptyStateComponent: React.ReactElement; +}; + +function WorkspacePerDiemTable({perDiemData, selectionEnabled, selectedKeys, onRowSelectionChange, EmptyStateComponent}: WorkspacePerDiemTableProps) { + const {translate, localeCompare} = useLocalize(); + const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout(); + const shouldUseNarrowTableLayout = shouldUseNarrowLayout || isMediumScreenWidth; + + const columns: Array> = [ + {key: 'destination', label: translate('common.destination'), sortable: true}, + {key: 'subrate', label: translate('common.subrate'), sortable: true}, + {key: 'amount', label: translate('workspace.perDiem.amount'), sortable: true}, + {key: 'actions', label: '', sortable: false, width: variables.tableCaretColumnWidth}, + ]; + + const compareItems: CompareItemsCallback = (item1, item2, activeSorting) => { + const orderMultiplier = activeSorting.order === 'asc' ? 1 : -1; + const destinationComparison = localeCompare(item1.destination, item2.destination) * orderMultiplier; + const subRateNameComparison = localeCompare(item1.subRateName, item2.subRateName) * orderMultiplier; + const amountComparison = (item1.rate - item2.rate) * orderMultiplier; + + if (activeSorting.columnKey === 'destination') { + if (destinationComparison !== 0) { + return destinationComparison; + } + if (subRateNameComparison !== 0) { + return subRateNameComparison; + } + return amountComparison; + } + + if (activeSorting.columnKey === 'subrate') { + if (subRateNameComparison !== 0) { + return subRateNameComparison; + } + if (destinationComparison !== 0) { + return destinationComparison; + } + return amountComparison; + } + + if (activeSorting.columnKey === 'amount') { + if (amountComparison !== 0) { + return amountComparison; + } + if (destinationComparison !== 0) { + return destinationComparison; + } + return subRateNameComparison; + } + + return 0; + }; + + const isItemInSearch: IsItemInSearchCallback = (item, searchString) => { + const matchingItems = tokenizedSearch([item], searchString, (option) => [option.destination, option.subRateName]); + return matchingItems.length > 0; + }; + + const renderItem = ({item, index}: ListRenderItemInfo) => ( + + ); + + const isEmpty = perDiemData.length === 0; + const shouldShowSearchBar = perDiemData.length >= CONST.STANDARD_LIST_ITEM_LIMIT; + + return ( + item.keyForList} + compareItems={compareItems} + isItemInSearch={isItemInSearch} + initialSortColumn="destination" + narrowLayoutSortColumn="destination" + title={translate('common.perDiem')} + > + {isEmpty && EmptyStateComponent} + {!isEmpty && ( + <> + {shouldShowSearchBar && } + + + + )} +
+ ); +} + +export default WorkspacePerDiemTable; diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 502bf54ed2ad..6e1be22c0d55 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2779,6 +2779,8 @@ type WorkspaceSplitNavigatorParamList = { }; [SCREENS.WORKSPACE.PER_DIEM]: { policyID: string; + // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md + backTo?: Routes; }; [SCREENS.WORKSPACE.WORKFLOWS]: { policyID: string; diff --git a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx index 22fac23fa8b2..8a4206402fd3 100644 --- a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx +++ b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx @@ -11,14 +11,12 @@ import {ModalActions} from '@components/Modal/Global/ModalContext'; import RenderHTML from '@components/RenderHTML'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; -import SearchBar from '@components/SearchBar'; -import TableListItem from '@components/SelectionList/ListItem/TableListItem'; -import type {ListItem} from '@components/SelectionList/ListItem/types'; -import SelectionListWithModal from '@components/SelectionListWithModal'; -import Text from '@components/Text'; +import WorkspacePerDiemTable from '@components/Tables/WorkspacePerDiemTable'; +import type {PerDiemTableRowData} from '@components/Tables/WorkspacePerDiemTable/WorkspacePerDiemTableRow'; import useCleanupSelectedOptions from '@hooks/useCleanupSelectedOptions'; import useConfirmModal from '@hooks/useConfirmModal'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useFilteredSelection from '@hooks/useFilteredSelection'; import useGenericEmptyStateIllustration from '@hooks/useGenericEmptyStateIllustration'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -28,21 +26,18 @@ import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchBackPress from '@hooks/useSearchBackPress'; -import useSearchResults from '@hooks/useSearchResults'; import useShouldDisplayButtonsInSeparateLine from '@hooks/useShouldDisplayButtonsInSeparateLine'; import useThemeStyles from '@hooks/useThemeStyles'; import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; +import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import {convertAmountToDisplayString} from '@libs/CurrencyUtils'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import {hasEnabledOptions} from '@libs/OptionsListUtils'; import {canMemberWrite, getPerDiemCustomUnit} from '@libs/PolicyUtils'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; -import tokenizedSearch from '@libs/tokenizedSearch'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import {turnOffMobileSelectionMode} from '@userActions/MobileSelectionMode'; import {close} from '@userActions/Modal'; import {deleteWorkspacePerDiemRates, downloadPerDiemCSV, openPolicyPerDiemPage} from '@userActions/Policy/PerDiem'; import CONST from '@src/CONST'; @@ -53,17 +48,6 @@ import type {PendingAction} from '@src/types/onyx/OnyxCommon'; import type {Rate} from '@src/types/onyx/Policy'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; -type PolicyOption = ListItem & { - /** subRateID is used as a key for identification of the entry */ - subRateID: string; - - /** rateID is used as a key for identification of the entry */ - rateID: string; - - /** subRateName is used for filters */ - subRateName: string; -}; - type SubRateData = { pendingAction?: PendingAction; destination: string; @@ -95,36 +79,15 @@ function getSubRatesData(customUnitRates: Rate[]) { return subRatesData; } -function generateSingleSubRateData(customUnitRates: Rate[], rateID: string, subRateID: string) { - const selectedRate = customUnitRates.find((rate) => rate.customUnitRateID === rateID); - if (!selectedRate) { - return null; - } - const selectedSubRate = selectedRate.subRates?.find((subRate) => subRate.id === subRateID); - if (!selectedSubRate) { - return null; - } - return { - pendingAction: selectedRate.pendingAction, - destination: selectedRate.name ?? '', - subRateName: selectedSubRate.name, - rate: selectedSubRate.rate, - currency: selectedRate.currency ?? CONST.CURRENCY.USD, - rateID: selectedRate.customUnitRateID, - subRateID: selectedSubRate.id, - }; -} - -type WorkspacePerDiemPageProps = PlatformStackScreenProps; +type WorkspacePerDiemPageProps = PlatformStackScreenProps; function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply the correct modal type for the decision modal // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth, isInLandscapeMode} = useResponsiveLayout(); const styles = useThemeStyles(); - const {translate, localeCompare} = useLocalize(); + const {translate} = useLocalize(); const {showConfirmModal} = useConfirmModal(); - const [selectedPerDiem, setSelectedPerDiem] = useState([]); const [isDownloadFailureModalVisible, setIsDownloadFailureModalVisible] = useState(false); const policyID = route.params.policyID; const backTo = route.params?.backTo; @@ -138,16 +101,14 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { const expensifyIcons = useMemoizedLazyExpensifyIcons(['Gear', 'Table', 'Download', 'Trashcan']); const genericIllustration = useGenericEmptyStateIllustration(); - const [customUnit, allRatesArray, allSubRates] = useMemo(() => { + const [customUnit, allSubRates] = useMemo(() => { const customUnits = getPerDiemCustomUnit(policy); const customUnitRates: Record = customUnits?.rates ?? {}; const allRates = Object.values(customUnitRates); const allSubRatesMemo = getSubRatesData(allRates); - return [customUnits, allRates, allSubRatesMemo]; + return [customUnits, allSubRatesMemo]; }, [policy]); - const canSelectMultiple = canWritePerDiem && (shouldUseNarrowLayout ? isMobileSelectionModeEnabled : true); - const fetchPerDiem = useCallback(() => { openPolicyPerDiemPage(policyID); }, [policyID]); @@ -160,111 +121,36 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { }, [fetchPerDiem]), ); - const cleanupSelectedOption = useCallback(() => setSelectedPerDiem([]), []); - useCleanupSelectedOptions(cleanupSelectedOption); - - const subRatesList = useMemo( - () => - allSubRates.map((value) => { - const isDisabled = value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; - return { - text: value.destination, - subRateID: value.subRateID, - rateID: value.rateID, - keyForList: value.subRateID, - isDisabled, - pendingAction: value.pendingAction, - subRateName: value.subRateName, - rightElement: ( - <> - - - {value.subRateName} - - - - - {convertAmountToDisplayString(value.rate, value.currency)} - - - - ), - }; - }), - [allSubRates, styles.flex2, styles.alignItemsStart, styles.textSupporting, styles.label, styles.pl2, styles.alignSelfEnd], - ); - - const filterRate = useCallback((rate: PolicyOption, searchInput: string) => { - const results = tokenizedSearch([rate], searchInput, (option) => [option.text ?? '', option.subRateName ?? '']); - return results.length > 0; - }, []); - const sortRates = useCallback((rates: PolicyOption[]) => rates.sort((a, b) => localeCompare(a.text ?? '', b.text ?? '')), [localeCompare]); - const [inputValue, setInputValue, filteredSubRatesList] = useSearchResults(subRatesList, filterRate, sortRates); - - const toggleSubRate = (subRate: PolicyOption) => { - if (selectedPerDiem.find((selectedSubRate) => selectedSubRate.subRateID === subRate.subRateID) !== undefined) { - setSelectedPerDiem((prev) => prev.filter((selectedSubRate) => selectedSubRate.subRateID !== subRate.subRateID)); - } else { - const subRateData = generateSingleSubRateData(allRatesArray, subRate.rateID, subRate.subRateID); - if (!subRateData) { - return; + const selectableSubRatesByID = useMemo(() => { + const map: Record = {}; + for (const subRate of allSubRates) { + if (subRate.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + map[subRate.subRateID] = subRate; } - setSelectedPerDiem((prev) => [...prev, subRateData]); } - }; + return map; + }, [allSubRates]); - const toggleAllSubRates = () => { - if (selectedPerDiem.length > 0) { - setSelectedPerDiem([]); - } else { - const availablePerDiemRates = allSubRates.filter( - (subRate) => - subRate.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && filteredSubRatesList.some((filteredSubRate) => filteredSubRate.subRateID === subRate.subRateID), - ); - setSelectedPerDiem(availablePerDiemRates); - } - }; + const filterSelectableSubRate = useCallback((subRate?: SubRateData) => !!subRate, []); - const getCustomListHeader = () => { - if (filteredSubRatesList.length === 0) { - return null; - } - const header = ( - - - {translate('common.destination')} - - - {translate('common.subrate')} - - - {translate('workspace.perDiem.amount')} - - - ); - if (canSelectMultiple) { - return header; - } - return {header}; - }; + const [selectedSubRateIDs, setSelectedSubRateIDs] = useFilteredSelection(selectableSubRatesByID, filterSelectableSubRate); + + const clearTableSelection = useCallback(() => { + setSelectedSubRateIDs((prev) => (prev.length > 0 ? [] : prev)); + }, [setSelectedSubRateIDs]); + + useCleanupSelectedOptions(clearTableSelection); const openSettings = useCallback(() => { Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_SETTINGS.getRoute(policyID)); }, [policyID]); - const openSubRateDetails = (rate: PolicyOption) => { - if (canWritePerDiem && isSmallScreenWidth && isMobileSelectionModeEnabled) { - toggleSubRate(rate); - return; - } - Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_DETAILS.getRoute(policyID, rate.rateID, rate.subRateID)); - }; + const openSubRateDetails = useCallback( + (rateID: string, subRateID: string) => { + Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_DETAILS.getRoute(policyID, rateID, subRateID)); + }, + [policyID], + ); const showOfflineModal = useCallback(() => { close(() => { @@ -277,12 +163,37 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { }); }, [showConfirmModal, translate]); - const handleDeletePerDiemRates = () => { - deleteWorkspacePerDiemRates(policyID, customUnit, selectedPerDiem); - setSelectedPerDiem([]); - }; + const handleDeletePerDiemRates = useCallback(() => { + const subRatesToDelete = selectedSubRateIDs.map((subRateID) => selectableSubRatesByID[subRateID]).filter((subRate): subRate is SubRateData => !!subRate); + deleteWorkspacePerDiemRates(policyID, customUnit, subRatesToDelete); + setSelectedSubRateIDs([]); + turnOffMobileSelectionMode(); + }, [selectedSubRateIDs, selectableSubRatesByID, policyID, customUnit, setSelectedSubRateIDs]); + + const hasVisibleSubRates = allSubRates.some((subRate) => subRate.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline); - const hasVisibleSubRates = subRatesList.some((subRate) => subRate.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline); + const perDiemRows: PerDiemTableRowData[] = useMemo( + () => + allSubRates + .filter((subRate) => isOffline || subRate.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) + .map((subRate) => { + const isDeleting = subRate.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + + return { + keyForList: subRate.subRateID, + subRateID: subRate.subRateID, + rateID: subRate.rateID, + destination: subRate.destination, + subRateName: subRate.subRateName, + rate: subRate.rate, + formattedAmount: convertAmountToDisplayString(subRate.rate, subRate.currency), + disabled: isDeleting, + pendingAction: subRate.pendingAction, + action: () => openSubRateDetails(subRate.rateID, subRate.subRateID), + }; + }), + [allSubRates, isOffline, openSubRateDetails], + ); const secondaryActions = useMemo(() => { const menuItems = []; @@ -352,15 +263,15 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { const getHeaderButtons = () => { const options: Array>> = []; - if (shouldUseNarrowLayout ? canSelectMultiple : selectedPerDiem.length > 0) { + if (canWritePerDiem && (shouldUseNarrowLayout ? isMobileSelectionModeEnabled : selectedSubRateIDs.length > 0)) { options.push({ icon: expensifyIcons.Trashcan, - text: translate('workspace.perDiem.deleteRates', {count: selectedPerDiem.length}), + text: translate('workspace.perDiem.deleteRates', {count: selectedSubRateIDs.length}), value: CONST.POLICY.BULK_ACTION_TYPES.DELETE, onSelected: async () => { const {action} = await showConfirmModal({ title: translate('workspace.perDiem.deletePerDiemRate'), - prompt: translate('workspace.perDiem.areYouSureDelete', {count: selectedPerDiem.length}), + prompt: translate('workspace.perDiem.areYouSureDelete', {count: selectedSubRateIDs.length}), confirmText: translate('common.delete'), cancelText: translate('common.cancel'), danger: true, @@ -376,11 +287,11 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { onPress={() => null} shouldAlwaysShowDropdownMenu buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} - customText={translate('workspace.common.selected', {count: selectedPerDiem.length})} + customText={translate('workspace.common.selected', {count: selectedSubRateIDs.length})} options={options} isSplitButton={false} style={[shouldDisplayButtonsInSeparateLine && styles.flexGrow1, shouldDisplayButtonsInSeparateLine && styles.mb3]} - isDisabled={!selectedPerDiem.length} + isDisabled={!selectedSubRateIDs.length} sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.PER_DIEM.BULK_ACTIONS_DROPDOWN} /> ); @@ -414,32 +325,49 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { return; } - setSelectedPerDiem([]); - }, [setSelectedPerDiem, isMobileSelectionModeEnabled]); + setSelectedSubRateIDs([]); + }, [setSelectedSubRateIDs, isMobileSelectionModeEnabled]); useSearchBackPress({ - onClearSelection: () => { - setSelectedPerDiem([]); - }, + onClearSelection: clearTableSelection, onNavigationCallBack: () => Navigation.goBack(backTo), }); const selectionModeHeader = isMobileSelectionModeEnabled && shouldUseNarrowLayout; + const headerButtons = getHeaderButtons(); - const headerContent = ( - <> - - - - {subRatesList.length >= CONST.STANDARD_LIST_ITEM_LIMIT && ( - - )} - + const subtitleContent = ( + + + + ); + + const emptyStateContent = ( + + { + if (isOffline) { + showOfflineModal(); + return; + } + Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_IMPORT.getRoute(policyID)); + }, + success: true, + }, + ] + : [] + } + /> + ); return ( @@ -465,7 +393,7 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { shouldDisplayHelpButton onBackButtonPress={() => { if (isMobileSelectionModeEnabled) { - setSelectedPerDiem([]); + clearTableSelection(); turnOffMobileSelectionMode(); return; } @@ -478,10 +406,10 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { Navigation.goBack(); }} > - {!shouldDisplayButtonsInSeparateLine && getHeaderButtons()} + {!shouldDisplayButtonsInSeparateLine && headerButtons} - {!!getHeaderButtons() && shouldDisplayButtonsInSeparateLine && {getHeaderButtons()}} - {(!hasVisibleSubRates || isLoading) && headerContent} + {!!headerButtons && shouldDisplayButtonsInSeparateLine && {headerButtons}} + {(!hasVisibleSubRates || isLoading) && subtitleContent} {isLoading && ( )} - {hasVisibleSubRates && !isLoading && ( - item.subRateID)} - onSelectAll={canWritePerDiem && filteredSubRatesList.length > 0 ? toggleAllSubRates : undefined} - style={{listItemTitleContainerStyles: styles.flex3}} - onTurnOnSelectionMode={(item) => canWritePerDiem && item && toggleSubRate(item)} - shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} - customListHeaderContent={headerContent} - shouldShowListEmptyContent={false} - showScrollIndicator={false} - turnOnSelectionModeOnLongPress={canWritePerDiem} - shouldHeaderBeInsideList - shouldShowRightCaret - /> - )} - {!hasVisibleSubRates && !isLoading && ( - - { - if (isOffline) { - showOfflineModal(); - return; - } - Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_IMPORT.getRoute(policyID)); - }, - success: true, - }, - ] - : [] - } + {!isLoading && ( + <> + {hasVisibleSubRates && subtitleContent} + - + )} Date: Tue, 30 Jun 2026 04:23:22 +0530 Subject: [PATCH 2/3] Align per diem table with migrated table conventions Signed-off-by: krishna2323 --- .../WorkspacePerDiemTableRow.tsx | 27 +++++++--------- .../Tables/WorkspacePerDiemTable/index.tsx | 32 +++++++++++++++---- .../perDiem/WorkspacePerDiemPage.tsx | 2 +- 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/src/components/Tables/WorkspacePerDiemTable/WorkspacePerDiemTableRow.tsx b/src/components/Tables/WorkspacePerDiemTable/WorkspacePerDiemTableRow.tsx index 9ca68877f602..eab7f122e004 100644 --- a/src/components/Tables/WorkspacePerDiemTable/WorkspacePerDiemTableRow.tsx +++ b/src/components/Tables/WorkspacePerDiemTable/WorkspacePerDiemTableRow.tsx @@ -2,30 +2,22 @@ import React from 'react'; import {View} from 'react-native'; import Icon from '@components/Icon'; import Table from '@components/Table'; -import type {TableData} from '@components/Table'; import TextWithTooltip from '@components/TextWithTooltip'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; - -type PerDiemTableRowData = TableData & { - subRateID: string; - rateID: string; - destination: string; - subRateName: string; - rate: number; - formattedAmount: string; - disabled?: boolean; - pendingAction?: OnyxCommon.PendingAction; - action: () => void; -}; +import type {PerDiemTableRowData} from '.'; type WorkspacePerDiemTableRowProps = { + /** Data about the per diem subrate */ item: PerDiemTableRowData; + + /** The index of the row relative to all other rows */ rowIndex: number; + + /** Whether to use narrow table row layout */ shouldUseNarrowTableLayout: boolean; }; @@ -45,6 +37,7 @@ function WorkspacePerDiemTableRow({item, rowIndex, shouldUseNarrowTableLayout}: sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.PER_DIEM.ROW} offlineWithFeedback={{ pendingAction: item.pendingAction, + shouldHideOnDelete: false, }} onPress={item.action} > @@ -53,10 +46,12 @@ function WorkspacePerDiemTableRow({item, rowIndex, shouldUseNarrowTableLayout}: {shouldUseNarrowTableLayout && ( void; +}; + type WorkspacePerDiemTableProps = { perDiemData: PerDiemTableRowData[]; selectionEnabled: boolean; @@ -20,7 +33,8 @@ type WorkspacePerDiemTableProps = { EmptyStateComponent: React.ReactElement; }; -function WorkspacePerDiemTable({perDiemData, selectionEnabled, selectedKeys, onRowSelectionChange, EmptyStateComponent}: WorkspacePerDiemTableProps) { +export default function WorkspacePerDiemTable({perDiemData, selectionEnabled, selectedKeys, onRowSelectionChange, EmptyStateComponent}: WorkspacePerDiemTableProps) { + const styles = useThemeStyles(); const {translate, localeCompare} = useLocalize(); const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout(); const shouldUseNarrowTableLayout = shouldUseNarrowLayout || isMediumScreenWidth; @@ -28,7 +42,14 @@ function WorkspacePerDiemTable({perDiemData, selectionEnabled, selectedKeys, onR const columns: Array> = [ {key: 'destination', label: translate('common.destination'), sortable: true}, {key: 'subrate', label: translate('common.subrate'), sortable: true}, - {key: 'amount', label: translate('workspace.perDiem.amount'), sortable: true}, + { + key: 'amount', + label: translate('workspace.perDiem.amount'), + sortable: true, + styling: { + containerStyles: [styles.justifyContentEnd], + }, + }, {key: 'actions', label: '', sortable: false, width: variables.tableCaretColumnWidth}, ]; @@ -78,7 +99,6 @@ function WorkspacePerDiemTable({perDiemData, selectionEnabled, selectedKeys, onR const renderItem = ({item, index}: ListRenderItemInfo) => ( Date: Tue, 30 Jun 2026 04:45:28 +0530 Subject: [PATCH 3/3] Use composite keys for per diem table row selection Signed-off-by: krishna2323 --- .../perDiem/WorkspacePerDiemPage.tsx | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx index 7319396a9053..7443d462fd6b 100644 --- a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx +++ b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx @@ -58,6 +58,10 @@ type SubRateData = { subRateID: string; }; +function getPerDiemRowKey(rateID: string, subRateID: string) { + return `${rateID}:${subRateID}`; +} + function getSubRatesData(customUnitRates: Rate[]) { const subRatesData: SubRateData[] = []; for (const rate of customUnitRates) { @@ -121,11 +125,11 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { }, [fetchPerDiem]), ); - const selectableSubRatesByID = useMemo(() => { + const selectableSubRatesByKey = useMemo(() => { const map: Record = {}; for (const subRate of allSubRates) { if (subRate.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { - map[subRate.subRateID] = subRate; + map[getPerDiemRowKey(subRate.rateID, subRate.subRateID)] = subRate; } } return map; @@ -133,11 +137,11 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { const filterSelectableSubRate = useCallback((subRate?: SubRateData) => !!subRate, []); - const [selectedSubRateIDs, setSelectedSubRateIDs] = useFilteredSelection(selectableSubRatesByID, filterSelectableSubRate); + const [selectedSubRateKeys, setSelectedSubRateKeys] = useFilteredSelection(selectableSubRatesByKey, filterSelectableSubRate); const clearTableSelection = useCallback(() => { - setSelectedSubRateIDs((prev) => (prev.length > 0 ? [] : prev)); - }, [setSelectedSubRateIDs]); + setSelectedSubRateKeys((prev) => (prev.length > 0 ? [] : prev)); + }, [setSelectedSubRateKeys]); useCleanupSelectedOptions(clearTableSelection); @@ -164,11 +168,11 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { }, [showConfirmModal, translate]); const handleDeletePerDiemRates = useCallback(() => { - const subRatesToDelete = selectedSubRateIDs.map((subRateID) => selectableSubRatesByID[subRateID]).filter((subRate): subRate is SubRateData => !!subRate); + const subRatesToDelete = selectedSubRateKeys.map((rowKey) => selectableSubRatesByKey[rowKey]).filter((subRate): subRate is SubRateData => !!subRate); deleteWorkspacePerDiemRates(policyID, customUnit, subRatesToDelete); - setSelectedSubRateIDs([]); + setSelectedSubRateKeys([]); turnOffMobileSelectionMode(); - }, [selectedSubRateIDs, selectableSubRatesByID, policyID, customUnit, setSelectedSubRateIDs]); + }, [selectedSubRateKeys, selectableSubRatesByKey, policyID, customUnit, setSelectedSubRateKeys]); const hasVisibleSubRates = allSubRates.some((subRate) => subRate.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline); @@ -180,7 +184,7 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { const isDeleting = subRate.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; return { - keyForList: subRate.subRateID, + keyForList: getPerDiemRowKey(subRate.rateID, subRate.subRateID), subRateID: subRate.subRateID, rateID: subRate.rateID, destination: subRate.destination, @@ -263,15 +267,15 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { const getHeaderButtons = () => { const options: Array>> = []; - if (canWritePerDiem && (shouldUseNarrowLayout ? isMobileSelectionModeEnabled : selectedSubRateIDs.length > 0)) { + if (canWritePerDiem && (shouldUseNarrowLayout ? isMobileSelectionModeEnabled : selectedSubRateKeys.length > 0)) { options.push({ icon: expensifyIcons.Trashcan, - text: translate('workspace.perDiem.deleteRates', {count: selectedSubRateIDs.length}), + text: translate('workspace.perDiem.deleteRates', {count: selectedSubRateKeys.length}), value: CONST.POLICY.BULK_ACTION_TYPES.DELETE, onSelected: async () => { const {action} = await showConfirmModal({ title: translate('workspace.perDiem.deletePerDiemRate'), - prompt: translate('workspace.perDiem.areYouSureDelete', {count: selectedSubRateIDs.length}), + prompt: translate('workspace.perDiem.areYouSureDelete', {count: selectedSubRateKeys.length}), confirmText: translate('common.delete'), cancelText: translate('common.cancel'), danger: true, @@ -287,11 +291,11 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { onPress={() => null} shouldAlwaysShowDropdownMenu buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} - customText={translate('workspace.common.selected', {count: selectedSubRateIDs.length})} + customText={translate('workspace.common.selected', {count: selectedSubRateKeys.length})} options={options} isSplitButton={false} style={[shouldDisplayButtonsInSeparateLine && styles.flexGrow1, shouldDisplayButtonsInSeparateLine && styles.mb3]} - isDisabled={!selectedSubRateIDs.length} + isDisabled={!selectedSubRateKeys.length} sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.PER_DIEM.BULK_ACTIONS_DROPDOWN} /> ); @@ -325,8 +329,8 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { return; } - setSelectedSubRateIDs([]); - }, [setSelectedSubRateIDs, isMobileSelectionModeEnabled]); + setSelectedSubRateKeys([]); + }, [setSelectedSubRateKeys, isMobileSelectionModeEnabled]); useSearchBackPress({ onClearSelection: clearTableSelection, @@ -423,8 +427,8 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) {