From 9c8b9e261a614fe107c6e4207f4cce3308bbfa35 Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Wed, 13 May 2026 11:09:39 -0400 Subject: [PATCH 01/18] create new folder --- .../WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx | 5 +++++ src/components/Tables/WorkspaceCategoriesTable/index.tsx | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx create mode 100644 src/components/Tables/WorkspaceCategoriesTable/index.tsx diff --git a/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx b/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx new file mode 100644 index 000000000000..97ff54b3622d --- /dev/null +++ b/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx @@ -0,0 +1,5 @@ +type WorkspaceCategoriesTableRowProps = {}; + +export default function WorkspaceCategoriesTableRow({}: WorkspaceCategoriesTableRowProps) { + return <>; +} diff --git a/src/components/Tables/WorkspaceCategoriesTable/index.tsx b/src/components/Tables/WorkspaceCategoriesTable/index.tsx new file mode 100644 index 000000000000..9e2008ca8b1f --- /dev/null +++ b/src/components/Tables/WorkspaceCategoriesTable/index.tsx @@ -0,0 +1,5 @@ +type WorkspaceCategoriesTableProps = {}; + +export default function WorkspaceCategoriesTable({}: WorkspaceCategoriesTableProps) { + return <>; +} From 4d06dbe9193b0fb13b555bc6b5ab23606619c596 Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Wed, 13 May 2026 11:24:30 -0400 Subject: [PATCH 02/18] setup the component --- .../WorkspaceCategoriesTableRow.tsx | 10 +++- .../Tables/WorkspaceCategoriesTable/index.tsx | 55 ++++++++++++++++++- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx b/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx index 97ff54b3622d..04390ffc0ff8 100644 --- a/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx +++ b/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx @@ -1,5 +1,11 @@ -type WorkspaceCategoriesTableRowProps = {}; +import {WorkspaceCategoryTableRowData} from '.'; -export default function WorkspaceCategoriesTableRow({}: WorkspaceCategoriesTableRowProps) { +type WorkspaceCategoriesTableRowProps = { + item: WorkspaceCategoryTableRowData; + + rowIndex: number; +}; + +export default function WorkspaceCategoriesTableRow({rowIndex, item}: WorkspaceCategoriesTableRowProps) { return <>; } diff --git a/src/components/Tables/WorkspaceCategoriesTable/index.tsx b/src/components/Tables/WorkspaceCategoriesTable/index.tsx index 9e2008ca8b1f..b45efe0b9a16 100644 --- a/src/components/Tables/WorkspaceCategoriesTable/index.tsx +++ b/src/components/Tables/WorkspaceCategoriesTable/index.tsx @@ -1,5 +1,54 @@ -type WorkspaceCategoriesTableProps = {}; +import type {ListRenderItemInfo} from '@shopify/flash-list'; +import Table, {TableColumn} from '@components/Table/'; +import useLocalize from '@hooks/useLocalize'; +import {AvatarSource} from '@libs/UserAvatarUtils'; +import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; +import WorkspaceCategoriesTableRow from './WorkspaceCategoriesTableRow'; -export default function WorkspaceCategoriesTable({}: WorkspaceCategoriesTableProps) { - return <>; +export type WorkspaceCategoryTableColumnKey = 'name' | 'glCode' | 'approver' | 'enabled' | 'actions'; + +export type WorkspaceCategoryTableRowData = { + name: string; + glCode: string; + approverAvatar?: AvatarSource; + approverDisplayName?: string; + isDisabled: boolean; + errors: OnyxCommon.Errors; + pendingAction: OnyxCommon.PendingAction; +}; + +type WorkspaceCategoriesTableProps = { + categories: WorkspaceCategoryTableRowData[]; + + shouldShowApproverColumn: boolean; +}; + +export default function WorkspaceCategoriesTable({categories, shouldShowApproverColumn}: WorkspaceCategoriesTableProps) { + const {translate} = useLocalize(); + + const categoryTableColumns: Array> = [ + {key: 'name', label: translate('common.name')}, + {key: 'glCode', label: translate('workspace.categories.glCode')}, + ...(shouldShowApproverColumn ? [{key: 'approver', label: translate('common.approver')} as const] : []), + {key: 'enabled', label: translate('common.enabled')}, + {key: 'actions', label: ''}, + ]; + + const renderCategoryItem = ({item, index}: ListRenderItemInfo) => ( + + ); + + return ( + + + +
+ ); } From c080185d5f7b9038aa0d76b615a8e8646a0ab1c6 Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Wed, 13 May 2026 11:38:56 -0400 Subject: [PATCH 03/18] add the sortihng & comparing --- .../Tables/WorkspaceCategoriesTable/index.tsx | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/components/Tables/WorkspaceCategoriesTable/index.tsx b/src/components/Tables/WorkspaceCategoriesTable/index.tsx index b45efe0b9a16..ecb9668c1750 100644 --- a/src/components/Tables/WorkspaceCategoriesTable/index.tsx +++ b/src/components/Tables/WorkspaceCategoriesTable/index.tsx @@ -1,5 +1,5 @@ import type {ListRenderItemInfo} from '@shopify/flash-list'; -import Table, {TableColumn} from '@components/Table/'; +import Table, {CompareItemsCallback, IsItemInSearchCallback, TableColumn} from '@components/Table/'; import useLocalize from '@hooks/useLocalize'; import {AvatarSource} from '@libs/UserAvatarUtils'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; @@ -24,7 +24,7 @@ type WorkspaceCategoriesTableProps = { }; export default function WorkspaceCategoriesTable({categories, shouldShowApproverColumn}: WorkspaceCategoriesTableProps) { - const {translate} = useLocalize(); + const {translate, localeCompare} = useLocalize(); const categoryTableColumns: Array> = [ {key: 'name', label: translate('common.name')}, @@ -34,6 +34,27 @@ export default function WorkspaceCategoriesTable({categories, shouldShowApprover {key: 'actions', label: ''}, ]; + const compareItems: CompareItemsCallback = (item1, item2, activeSorting) => { + const orderMultiplier = activeSorting.order === 'asc' ? 1 : -1; + + if (activeSorting.columnKey === 'approver') { + const approver1 = item1.approverDisplayName || ''; + const approver2 = item2.approverDisplayName || ''; + return localeCompare(approver1, approver2) * orderMultiplier; + } + + if (activeSorting.columnKey === 'enabled') { + return (item1.isDisabled === item2.isDisabled ? 0 : item1.isDisabled ? 1 : -1) * orderMultiplier; + } + + return localeCompare(item1.name, item2.name) * orderMultiplier; + }; + + const isItemInSearch: IsItemInSearchCallback = (item, searchValue) => { + const searchLower = searchValue.toLowerCase(); + return item.name.toLowerCase().includes(searchLower) || item.glCode.toLowerCase().includes(searchLower); + }; + const renderCategoryItem = ({item, index}: ListRenderItemInfo) => ( From 5d6d393c2030fd410f93542a3347fc42cfc07b7e Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Wed, 13 May 2026 11:54:46 -0400 Subject: [PATCH 04/18] add row data --- .../WorkspaceCategoriesTableRow.tsx | 33 +++++++++++++++++-- .../Tables/WorkspaceCategoriesTable/index.tsx | 1 + 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx b/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx index 04390ffc0ff8..1b8fca828a47 100644 --- a/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx +++ b/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx @@ -1,11 +1,40 @@ +import {View} from 'react-native'; +import Table from '@components/Table'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; import {WorkspaceCategoryTableRowData} from '.'; type WorkspaceCategoriesTableRowProps = { item: WorkspaceCategoryTableRowData; rowIndex: number; + + shouldShowApproverColumn: boolean; }; -export default function WorkspaceCategoriesTableRow({rowIndex, item}: WorkspaceCategoriesTableRowProps) { - return <>; +export default function WorkspaceCategoriesTableRow({rowIndex, shouldShowApproverColumn, item}: WorkspaceCategoriesTableRowProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + + return ( + + {({hovered}) => ( + <> + + + + + {shouldShowApproverColumn && } + + + + )} + + ); } diff --git a/src/components/Tables/WorkspaceCategoriesTable/index.tsx b/src/components/Tables/WorkspaceCategoriesTable/index.tsx index ecb9668c1750..8912d682f362 100644 --- a/src/components/Tables/WorkspaceCategoriesTable/index.tsx +++ b/src/components/Tables/WorkspaceCategoriesTable/index.tsx @@ -15,6 +15,7 @@ export type WorkspaceCategoryTableRowData = { isDisabled: boolean; errors: OnyxCommon.Errors; pendingAction: OnyxCommon.PendingAction; + action: () => void; }; type WorkspaceCategoriesTableProps = { From d8f2b3a45393001de29f4497a615f5c15c0bd897 Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Wed, 13 May 2026 12:29:30 -0400 Subject: [PATCH 05/18] build the category items --- .../WorkspaceCategoriesTableRow.tsx | 1 + .../Tables/WorkspaceCategoriesTable/index.tsx | 10 +- .../categories/WorkspaceCategoriesPage.tsx | 210 ++++++++++-------- 3 files changed, 127 insertions(+), 94 deletions(-) diff --git a/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx b/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx index 1b8fca828a47..a27d5f06c327 100644 --- a/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx +++ b/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import {View} from 'react-native'; import Table from '@components/Table'; import useTheme from '@hooks/useTheme'; diff --git a/src/components/Tables/WorkspaceCategoriesTable/index.tsx b/src/components/Tables/WorkspaceCategoriesTable/index.tsx index 8912d682f362..913f8be37e66 100644 --- a/src/components/Tables/WorkspaceCategoriesTable/index.tsx +++ b/src/components/Tables/WorkspaceCategoriesTable/index.tsx @@ -1,4 +1,5 @@ import type {ListRenderItemInfo} from '@shopify/flash-list'; +import React from 'react'; import Table, {CompareItemsCallback, IsItemInSearchCallback, TableColumn} from '@components/Table/'; import useLocalize from '@hooks/useLocalize'; import {AvatarSource} from '@libs/UserAvatarUtils'; @@ -8,13 +9,15 @@ import WorkspaceCategoriesTableRow from './WorkspaceCategoriesTableRow'; export type WorkspaceCategoryTableColumnKey = 'name' | 'glCode' | 'approver' | 'enabled' | 'actions'; export type WorkspaceCategoryTableRowData = { + keyForList: string; name: string; - glCode: string; + glCode?: string; approverAvatar?: AvatarSource; + approverAccountID?: number; approverDisplayName?: string; isDisabled: boolean; - errors: OnyxCommon.Errors; - pendingAction: OnyxCommon.PendingAction; + errors?: OnyxCommon.Errors; + pendingAction?: OnyxCommon.PendingAction; action: () => void; }; @@ -60,6 +63,7 @@ export default function WorkspaceCategoriesTable({categories, shouldShowApprover ); diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index e9b6c1280aed..81cd5e8c7348 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -20,6 +20,7 @@ import type {ListItem} from '@components/SelectionList/types'; import SelectionListWithModal from '@components/SelectionListWithModal'; import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; import Switch from '@components/Switch'; +import WorkspaceCategoriesTable, {WorkspaceCategoryTableRowData} from '@components/Tables/WorkspaceCategoriesTable'; import Text from '@components/Text'; import useAutoTurnSelectionModeOffWhenHasNoActiveOption from '@hooks/useAutoTurnSelectionModeOffWhenHasNoActiveOption'; import useCleanupSelectedOptions from '@hooks/useCleanupSelectedOptions'; @@ -219,9 +220,10 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { const glCodeTextStyle = useMemo(() => [styles.alignSelfStart], [styles.alignSelfStart]); const switchContainerStyle = useMemo(() => [StyleUtils.getMinimumWidth(variables.w72)], [StyleUtils]); - const categoryList = useMemo(() => { + const categoryRows = useMemo(() => { const categories = Object.values(policyCategories ?? {}); - return categories.reduce((acc, value) => { + + return categories.reduce((acc, value) => { const isDisabled = value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; if (!isOffline && isDisabled) { @@ -230,81 +232,102 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { const approverEmail = shouldShowApproverColumn ? (getCategoryApproverRule(policy?.rules?.approvalRules ?? [], value.name)?.approver ?? '') : ''; const approverPersonalDetail = getPersonalDetailByEmail(approverEmail); - const {avatar, displayName = approverEmail, accountID} = approverPersonalDetail ?? {}; + const {avatar: approverAvatar, displayName = approverEmail, accountID: approverAccountID} = approverPersonalDetail ?? {}; const approverDisplayName = displayName ? formatPhoneNumber(displayName) : ''; acc.push({ - text: getDecodedCategoryName(value.name), keyForList: value.name, + name: getDecodedCategoryName(value.name), + glCode: value['GL Code'], + approverAvatar, + approverAccountID, + approverDisplayName, isDisabled, - pendingAction: value.pendingAction, errors: value.errors ?? undefined, - rightElement: isControlPolicyWithWideLayout ? ( - <> - - - {value['GL Code']} - - - {shouldShowApproverColumn && ( - - {approverDisplayName ? ( - <> - - - {approverDisplayName} - - - ) : null} - - )} - - { - if (isDisablingOrDeletingLastEnabledCategory(policy, policyCategories, [value])) { - showCannotDeleteOrDisableLastCategoryModal(); - return; - } - updateWorkspaceCategoryEnabled(newValue, value.name); - }} - showLockIcon={isDisablingOrDeletingLastEnabledCategory(policy, policyCategories, [value])} - /> - - - ) : ( - { - if (isDisablingOrDeletingLastEnabledCategory(policy, policyCategories, [value])) { - showCannotDeleteOrDisableLastCategoryModal(); - return; - } - updateWorkspaceCategoryEnabled(newValue, value.name); - }} - showLockIcon={isDisablingOrDeletingLastEnabledCategory(policy, policyCategories, [value])} - /> - ), + pendingAction: value.pendingAction, + action: () => { + const path = isQuickSettingsFlow + ? ROUTES.SETTINGS_CATEGORY_SETTINGS.getRoute(policyId, value.name, backTo) + : ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyId, value.name); + + Navigation.navigate(path); + }, }); return acc; + + // acc.push({ + // text: getDecodedCategoryName(value.name), + // keyForList: value.name, + // isDisabled, + // pendingAction: value.pendingAction, + // errors: value.errors ?? undefined, + // rightElement: isControlPolicyWithWideLayout ? ( + // <> + // + // + // {value['GL Code']} + // + // + // {shouldShowApproverColumn && ( + // + // {approverDisplayName ? ( + // <> + // + // + // {approverDisplayName} + // + // + // ) : null} + // + // )} + // + // { + // if (isDisablingOrDeletingLastEnabledCategory(policy, policyCategories, [value])) { + // showCannotDeleteOrDisableLastCategoryModal(); + // return; + // } + // updateWorkspaceCategoryEnabled(newValue, value.name); + // }} + // showLockIcon={isDisablingOrDeletingLastEnabledCategory(policy, policyCategories, [value])} + // /> + // + // + // ) : ( + // { + // if (isDisablingOrDeletingLastEnabledCategory(policy, policyCategories, [value])) { + // showCannotDeleteOrDisableLastCategoryModal(); + // return; + // } + // updateWorkspaceCategoryEnabled(newValue, value.name); + // }} + // showLockIcon={isDisablingOrDeletingLastEnabledCategory(policy, policyCategories, [value])} + // /> + // ), + // }); + + // return acc; }, []); }, [ showCannotDeleteOrDisableLastCategoryModal, @@ -333,9 +356,9 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { }, [localeCompare], ); - const [inputValue, setInputValue, filteredCategoryList] = useSearchResults(categoryList, filterCategory, sortCategories); + const [inputValue, setInputValue, filteredCategoryList] = useSearchResults(categoryRows, filterCategory, sortCategories); - useAutoTurnSelectionModeOffWhenHasNoActiveOption(categoryList); + useAutoTurnSelectionModeOffWhenHasNoActiveOption(categoryRows); const toggleCategory = useCallback( (category: ListItem) => { @@ -434,7 +457,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { setSelectedCategories([]); }); }; - const hasVisibleCategories = categoryList.some((category) => category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline); + const hasVisibleCategories = categoryRows.some((category) => category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline); const policyHasAccountingConnections = hasAccountingConnections(policy); @@ -695,7 +718,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { {translate('workspace.categories.subtitle')} )} - {categoryList.length > CONST.SEARCH_ITEM_LIMIT && ( + {categoryRows.length > CONST.SEARCH_ITEM_LIMIT && ( )} {hasVisibleCategories && !isLoading && ( - item && toggleCategory(item)} - onSelectAll={filteredCategoryList.length > 0 ? toggleAllCategories : undefined} - shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} - turnOnSelectionModeOnLongPress={isSmallScreenWidth} - customListHeader={getCustomListHeader()} - customListHeaderContent={headerContent} - canSelectMultiple={canSelectMultiple} - selectAllAccessibilityLabel={translate('accessibilityHints.selectAllCategories')} - shouldShowListEmptyContent={false} - onDismissError={dismissError} - showScrollIndicator={false} - shouldHeaderBeInsideList - shouldShowRightCaret + + + // item && toggleCategory(item)} + // onSelectAll={filteredCategoryList.length > 0 ? toggleAllCategories : undefined} + // shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} + // turnOnSelectionModeOnLongPress={isSmallScreenWidth} + // customListHeader={getCustomListHeader()} + // customListHeaderContent={headerContent} + // canSelectMultiple={canSelectMultiple} + // selectAllAccessibilityLabel={translate('accessibilityHints.selectAllCategories')} + // shouldShowListEmptyContent={false} + // onDismissError={dismissError} + // showScrollIndicator={false} + // shouldHeaderBeInsideList + // shouldShowRightCaret + // /> )} {!hasVisibleCategories && !isLoading && inputValue.length === 0 && ( From 05da384e7d53ba85aa96802e088fffdec43d2fc4 Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Wed, 13 May 2026 12:43:54 -0400 Subject: [PATCH 06/18] add more properties to the table header --- src/components/Table/TableHeader.tsx | 6 ++- src/components/Table/types.ts | 6 +++ .../Tables/WorkspaceCategoriesTable/index.tsx | 43 ++++++++++++++++--- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/src/components/Table/TableHeader.tsx b/src/components/Table/TableHeader.tsx index b0d94614b6bc..67ee4fa2e4f3 100644 --- a/src/components/Table/TableHeader.tsx +++ b/src/components/Table/TableHeader.tsx @@ -62,6 +62,8 @@ function TableHeader({style, shouldHideHea return null; } + const gridTemplateColumns = columns.map((column) => column.width ?? '1fr').join(' '); + return ( ({style, shouldHideHea styles.gap3, // Use Grid on web when available (will override flex if supported) styles.dGrid, - !shouldUseNarrowTableLayout && {gridTemplateColumns: `repeat(${columns.length}, 1fr)`}, + !shouldUseNarrowTableLayout && {gridTemplateColumns}, style, ]} // eslint-disable-next-line react/jsx-props-no-spreading @@ -148,6 +150,7 @@ function TableHeaderColumn({column}: {colu styles.tableHeaderContentHeight, column.styling?.flex ? {flex: column.styling.flex} : styles.flex1, column.styling?.containerStyles, + !column.sortable && styles.cursorDefault, ]; return ( @@ -155,6 +158,7 @@ function TableHeaderColumn({column}: {colu accessible accessibilityLabel={column.label} accessibilityRole="button" + disabled={!column.sortable} sentryLabel={CONST.SENTRY_LABEL.TABLE_HEADER.SORTABLE_COLUMN} style={tableHeaderStyles} onPress={() => toggleSorting(column.key)} diff --git a/src/components/Table/types.ts b/src/components/Table/types.ts index 39f32d7e6359..40c332be9690 100644 --- a/src/components/Table/types.ts +++ b/src/components/Table/types.ts @@ -31,6 +31,12 @@ type TableColumn = { /** Display label shown in the table header. */ label: string; + /** Whether the column is sortable or not */ + sortable: boolean; + + /** Optional fixed width for the column */ + width?: number | string; + /** Optional styling configuration for the column. */ styling?: TableColumnStyling; }; diff --git a/src/components/Tables/WorkspaceCategoriesTable/index.tsx b/src/components/Tables/WorkspaceCategoriesTable/index.tsx index 913f8be37e66..cd19ae85615b 100644 --- a/src/components/Tables/WorkspaceCategoriesTable/index.tsx +++ b/src/components/Tables/WorkspaceCategoriesTable/index.tsx @@ -6,7 +6,7 @@ import {AvatarSource} from '@libs/UserAvatarUtils'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import WorkspaceCategoriesTableRow from './WorkspaceCategoriesTableRow'; -export type WorkspaceCategoryTableColumnKey = 'name' | 'glCode' | 'approver' | 'enabled' | 'actions'; +export type WorkspaceCategoryTableColumnKey = 'selection' | 'name' | 'glCode' | 'approver' | 'enabled' | 'actions'; export type WorkspaceCategoryTableRowData = { keyForList: string; @@ -31,11 +31,40 @@ export default function WorkspaceCategoriesTable({categories, shouldShowApprover const {translate, localeCompare} = useLocalize(); const categoryTableColumns: Array> = [ - {key: 'name', label: translate('common.name')}, - {key: 'glCode', label: translate('workspace.categories.glCode')}, - ...(shouldShowApproverColumn ? [{key: 'approver', label: translate('common.approver')} as const] : []), - {key: 'enabled', label: translate('common.enabled')}, - {key: 'actions', label: ''}, + { + key: 'selection', + label: '', + sortable: false, + }, + { + key: 'name', + label: translate('common.name'), + sortable: true, + }, + { + key: 'glCode', + label: translate('workspace.categories.glCode'), + sortable: true, + }, + ...(shouldShowApproverColumn + ? [ + { + key: 'approver' as const, + label: translate('common.approver'), + sortable: true, + }, + ] + : []), + { + key: 'enabled', + label: translate('common.enabled'), + sortable: true, + }, + { + key: 'actions', + label: '', + sortable: false, + }, ]; const compareItems: CompareItemsCallback = (item1, item2, activeSorting) => { @@ -56,7 +85,7 @@ export default function WorkspaceCategoriesTable({categories, shouldShowApprover const isItemInSearch: IsItemInSearchCallback = (item, searchValue) => { const searchLower = searchValue.toLowerCase(); - return item.name.toLowerCase().includes(searchLower) || item.glCode.toLowerCase().includes(searchLower); + return !!item.name.toLowerCase().includes(searchLower) || !!item.glCode?.toLowerCase().includes(searchLower); }; const renderCategoryItem = ({item, index}: ListRenderItemInfo) => ( From dc9532ab0b87f432857e92fcca352cf9be82a45c Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Wed, 13 May 2026 13:16:15 -0400 Subject: [PATCH 07/18] fix grid columns and add ne wheaderprosp --- src/components/Table/TableHeader.tsx | 2 +- src/components/Table/TableRow.tsx | 4 ++-- .../WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx | 5 +++++ src/components/Tables/WorkspaceCategoriesTable/index.tsx | 3 +++ 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/Table/TableHeader.tsx b/src/components/Table/TableHeader.tsx index 67ee4fa2e4f3..cb406c738f8c 100644 --- a/src/components/Table/TableHeader.tsx +++ b/src/components/Table/TableHeader.tsx @@ -62,7 +62,7 @@ function TableHeader({style, shouldHideHea return null; } - const gridTemplateColumns = columns.map((column) => column.width ?? '1fr').join(' '); + const gridTemplateColumns = columns.map((column) => (column.width ? `${column.width}px` : '1fr')).join(' '); return ( (column.width ? `${column.width}px` : '1fr')).join(' '); const tableRowPressableStyles = [ styles.mh5, @@ -81,7 +81,7 @@ export default function TableRow({ styles.gap3, styles.dFlex, // Use Grid on web when available (will override flex if supported) - !shouldUseNarrowTableLayout && [styles.dGrid, {gridTemplateColumns: `repeat(${columnCount}, 1fr)`}], + !shouldUseNarrowTableLayout && [styles.dGrid, {gridTemplateColumns}], ]; const renderChildren = (state: PressableStateCallbackType) => { diff --git a/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx b/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx index a27d5f06c327..ad039fb388e3 100644 --- a/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx +++ b/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx @@ -1,6 +1,7 @@ import React from 'react'; import {View} from 'react-native'; import Table from '@components/Table'; +import Text from '@components/Text'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {WorkspaceCategoryTableRowData} from '.'; @@ -29,6 +30,10 @@ export default function WorkspaceCategoriesTableRow({rowIndex, shouldShowApprove <> + + {item.name} + + {shouldShowApproverColumn && } diff --git a/src/components/Tables/WorkspaceCategoriesTable/index.tsx b/src/components/Tables/WorkspaceCategoriesTable/index.tsx index cd19ae85615b..ece4d62f1abb 100644 --- a/src/components/Tables/WorkspaceCategoriesTable/index.tsx +++ b/src/components/Tables/WorkspaceCategoriesTable/index.tsx @@ -35,6 +35,7 @@ export default function WorkspaceCategoriesTable({categories, shouldShowApprover key: 'selection', label: '', sortable: false, + width: 52, }, { key: 'name', @@ -59,11 +60,13 @@ export default function WorkspaceCategoriesTable({categories, shouldShowApprover key: 'enabled', label: translate('common.enabled'), sortable: true, + width: 64, }, { key: 'actions', label: '', sortable: false, + width: 52, }, ]; From 9aa5b23f7e9520978e4cf47e6f18faecd69b0ab6 Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Wed, 13 May 2026 13:23:41 -0400 Subject: [PATCH 08/18] add the row content --- src/components/Table/TableBody.tsx | 1 + .../WorkspaceCategoriesTableRow.tsx | 37 ++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/components/Table/TableBody.tsx b/src/components/Table/TableBody.tsx index 76587f776fc2..cf87afeacbc2 100644 --- a/src/components/Table/TableBody.tsx +++ b/src/components/Table/TableBody.tsx @@ -84,6 +84,7 @@ function TableBody({contentContainerStyle, ...props}: TableBodyProps) { > data={filteredAndSortedData} + showsVerticalScrollIndicator={false} ListEmptyComponent={isEmptyResult ? EmptyResultComponent : ListEmptyComponent} contentContainerStyle={[filteredAndSortedData.length === 0 && styles.flex1, listContentContainerStyle, contentContainerStyle]} keyboardShouldPersistTaps="handled" diff --git a/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx b/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx index ad039fb388e3..2285f7960f81 100644 --- a/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx +++ b/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx @@ -1,9 +1,15 @@ import React from 'react'; import {View} from 'react-native'; +import Avatar from '@components/Avatar'; +import Icon from '@components/Icon'; import Table from '@components/Table'; import Text from '@components/Text'; +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 {WorkspaceCategoryTableRowData} from '.'; type WorkspaceCategoriesTableRowProps = { @@ -17,6 +23,7 @@ type WorkspaceCategoriesTableRowProps = { export default function WorkspaceCategoriesTableRow({rowIndex, shouldShowApproverColumn, item}: WorkspaceCategoriesTableRowProps) { const theme = useTheme(); const styles = useThemeStyles(); + const icons = useMemoizedLazyExpensifyIcons(['ArrowRight']); return ( {item.name} - + + {item.glCode} + - {shouldShowApproverColumn && } + {shouldShowApproverColumn && ( + + {item.approverDisplayName && item.approverAccountID && ( + <> + + + + )} + + )} + + + + )} From 47ea4f0cbfe944f3fdc2db06a332d76681828256 Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Wed, 13 May 2026 13:38:06 -0400 Subject: [PATCH 09/18] add selection mode --- src/components/Table/Table.tsx | 2 ++ src/components/Table/TableContext.tsx | 3 +++ src/components/Table/TableHeader.tsx | 10 +++++--- src/components/Table/TableRow.tsx | 24 +++++++++++++++---- src/components/Table/types.ts | 3 +++ .../WorkspaceCategoriesTableRow.tsx | 2 -- .../Tables/WorkspaceCategoriesTable/index.tsx | 9 ++----- 7 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 8c280a8c6cfc..413ec53fc269 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -140,6 +140,7 @@ function Table) { if (!columns || columns.length === 0) { @@ -200,6 +201,7 @@ function Table; + /** Whether or not selection is enabled for the table */ + selectionEnabled?: boolean; + /** The data array after filtering, searching, and sorting have been applied. */ processedData: T[]; diff --git a/src/components/Table/TableHeader.tsx b/src/components/Table/TableHeader.tsx index cb406c738f8c..9c637c9a40bf 100644 --- a/src/components/Table/TableHeader.tsx +++ b/src/components/Table/TableHeader.tsx @@ -52,7 +52,7 @@ type TableHeaderProps = ViewProps & { function TableHeader({style, shouldHideHeaderWhenEmptySearch = true, ...props}: TableHeaderProps) { const theme = useTheme(); const styles = useThemeStyles(); - const {columns, isEmptyResult, title, shouldUseNarrowTableLayout} = useTableContext(); + const {columns, isEmptyResult, title, shouldUseNarrowTableLayout, selectionEnabled} = useTableContext(); if (shouldUseNarrowTableLayout && !title) { return null; @@ -62,7 +62,11 @@ function TableHeader({style, shouldHideHea return null; } - const gridTemplateColumns = columns.map((column) => (column.width ? `${column.width}px` : '1fr')).join(' '); + const gridTemplateColumns = columns.map((column) => (column.width ? `${column.width}px` : '1fr')); + + if (selectionEnabled) { + gridTemplateColumns.unshift('64px'); + } return ( ({style, shouldHideHea styles.gap3, // Use Grid on web when available (will override flex if supported) styles.dGrid, - !shouldUseNarrowTableLayout && {gridTemplateColumns}, + !shouldUseNarrowTableLayout && {gridTemplateColumns: gridTemplateColumns.join(' ')}, style, ]} // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/src/components/Table/TableRow.tsx b/src/components/Table/TableRow.tsx index d7dedf13e3ad..4afe2080e850 100644 --- a/src/components/Table/TableRow.tsx +++ b/src/components/Table/TableRow.tsx @@ -1,6 +1,7 @@ import React from 'react'; import type {PressableStateCallbackType} from 'react-native'; import {View} from 'react-native'; +import Checkbox from '@components/Checkbox'; import type {OfflineWithFeedbackProps} from '@components/OfflineWithFeedback'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import type {PressableWithFeedbackProps} from '@components/Pressable/PressableWithFeedback'; @@ -53,12 +54,16 @@ export default function TableRow({ const theme = useTheme(); const styles = useThemeStyles(); - const {processedData, columns, shouldUseNarrowTableLayout} = useTableContext(); + const {processedData, columns, shouldUseNarrowTableLayout, selectionEnabled} = useTableContext(); const rowCount = processedData.length; const isLastRow = rowIndex === rowCount - 1; const isInteractive = interactive && !isLoading; - const gridTemplateColumns = columns.map((column) => (column.width ? `${column.width}px` : '1fr')).join(' '); + const gridTemplateColumns = columns.map((column) => (column.width ? `${column.width}px` : '1fr')); + + if (selectionEnabled) { + gridTemplateColumns.unshift('64px'); + } const tableRowPressableStyles = [ styles.mh5, @@ -81,7 +86,7 @@ export default function TableRow({ styles.gap3, styles.dFlex, // Use Grid on web when available (will override flex if supported) - !shouldUseNarrowTableLayout && [styles.dGrid, {gridTemplateColumns}], + !shouldUseNarrowTableLayout && [styles.dGrid, {gridTemplateColumns: gridTemplateColumns.join(' ')}], ]; const renderChildren = (state: PressableStateCallbackType) => { @@ -124,7 +129,18 @@ export default function TableRow({ ) : ( - {renderChildren(state)} + + {selectionEnabled && ( + + {}} + /> + + )} + + {renderChildren(state)} + ) } diff --git a/src/components/Table/types.ts b/src/components/Table/types.ts index 40c332be9690..636ad549ef19 100644 --- a/src/components/Table/types.ts +++ b/src/components/Table/types.ts @@ -117,6 +117,9 @@ type TableProps>; diff --git a/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx b/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx index 2285f7960f81..e6979f5d4117 100644 --- a/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx +++ b/src/components/Tables/WorkspaceCategoriesTable/WorkspaceCategoriesTableRow.tsx @@ -35,8 +35,6 @@ export default function WorkspaceCategoriesTableRow({rowIndex, shouldShowApprove > {({hovered}) => ( <> - - {item.name} diff --git a/src/components/Tables/WorkspaceCategoriesTable/index.tsx b/src/components/Tables/WorkspaceCategoriesTable/index.tsx index ece4d62f1abb..8b3bd9798825 100644 --- a/src/components/Tables/WorkspaceCategoriesTable/index.tsx +++ b/src/components/Tables/WorkspaceCategoriesTable/index.tsx @@ -6,7 +6,7 @@ import {AvatarSource} from '@libs/UserAvatarUtils'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import WorkspaceCategoriesTableRow from './WorkspaceCategoriesTableRow'; -export type WorkspaceCategoryTableColumnKey = 'selection' | 'name' | 'glCode' | 'approver' | 'enabled' | 'actions'; +export type WorkspaceCategoryTableColumnKey = 'name' | 'glCode' | 'approver' | 'enabled' | 'actions'; export type WorkspaceCategoryTableRowData = { keyForList: string; @@ -31,12 +31,6 @@ export default function WorkspaceCategoriesTable({categories, shouldShowApprover const {translate, localeCompare} = useLocalize(); const categoryTableColumns: Array> = [ - { - key: 'selection', - label: '', - sortable: false, - width: 52, - }, { key: 'name', label: translate('common.name'), @@ -101,6 +95,7 @@ export default function WorkspaceCategoriesTable({categories, shouldShowApprover return ( Date: Thu, 14 May 2026 07:47:04 -0400 Subject: [PATCH 10/18] add checkboxes --- src/components/Table/TableHeader.tsx | 34 ++++++++++++++++++++-------- src/components/Table/TableRow.tsx | 2 +- src/styles/variables.ts | 1 + 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/components/Table/TableHeader.tsx b/src/components/Table/TableHeader.tsx index 9c637c9a40bf..d5425fe26ee4 100644 --- a/src/components/Table/TableHeader.tsx +++ b/src/components/Table/TableHeader.tsx @@ -1,6 +1,7 @@ import React, {useRef} from 'react'; import {View} from 'react-native'; import type {ViewProps} from 'react-native'; +import Checkbox from '@components/Checkbox'; import Icon from '@components/Icon'; import {PressableWithFeedback} from '@components/Pressable'; import Text from '@components/Text'; @@ -65,7 +66,7 @@ function TableHeader({style, shouldHideHea const gridTemplateColumns = columns.map((column) => (column.width ? `${column.width}px` : '1fr')); if (selectionEnabled) { - gridTemplateColumns.unshift('64px'); + gridTemplateColumns.unshift(`${variables.tableCheckboxColumnWidth}px`); } return ( @@ -100,15 +101,28 @@ function TableHeader({style, shouldHideHea )} - {!shouldUseNarrowTableLayout && - columns.map((column) => { - return ( - - ); - })} + {!shouldUseNarrowTableLayout && ( + <> + {selectionEnabled && ( + + {}} + /> + + )} + + {columns.map((column) => { + return ( + + ); + })} + + )} ); } diff --git a/src/components/Table/TableRow.tsx b/src/components/Table/TableRow.tsx index 22904b11d13f..2e3c7580811f 100644 --- a/src/components/Table/TableRow.tsx +++ b/src/components/Table/TableRow.tsx @@ -63,7 +63,7 @@ export default function TableRow({ const gridTemplateColumns = columns.map((column) => (column.width ? `${column.width}px` : '1fr')); if (selectionEnabled) { - gridTemplateColumns.unshift('64px'); + gridTemplateColumns.unshift(`${variables.tableCheckboxColumnWidth}px`); } const tableRowPressableStyles = [ diff --git a/src/styles/variables.ts b/src/styles/variables.ts index f1791dee9512..64be4a2d434d 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -131,6 +131,7 @@ export default { tableGroupRowPaddingVertical: 4, tableGroupRowHeight: 36, tableSkeletonHeight: 32, + tableCheckboxColumnWidth: 20, sectionMenuItemHeight: 52, sectionMenuItemHeightCompact: 44, optionsListSectionHeaderHeight: getValueUsingPixelRatio(32, 38), From 7cdf97d5c0536e551539804fdb2a81745062327e Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Fri, 15 May 2026 07:08:42 -0400 Subject: [PATCH 11/18] update table data --- src/components/Table/Table.tsx | 14 +++++++++----- src/components/Table/types.ts | 18 +++++++++++++++++- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 413ec53fc269..502ee1058d79 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -1,12 +1,12 @@ import type {FlashListRef} from '@shopify/flash-list'; -import React, {useImperativeHandle, useRef} from 'react'; +import React, {useImperativeHandle, useRef, useState} from 'react'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useFiltering from './middlewares/filtering'; import useSearching from './middlewares/searching'; import useSorting from './middlewares/sorting'; import TableContext from './TableContext'; import type {TableContextValue} from './TableContext'; -import type {TableHandle, TableMethods, TableProps} from './types'; +import type {TableHandle, TableMethods, TableProps, TableRowData} from './types'; /** * A composable table component that provides filtering, search, and sorting functionality. @@ -129,7 +129,7 @@ import type {TableHandle, TableMethods, TableProps} from './types'; *
* ``` */ -function Table({ +function Table({ ref, title, data = [], @@ -143,6 +143,10 @@ function Table) { + const [tableData, setTableData] = useState[]>(() => { + return data.map((item, index) => ({...item, checked: false})); + }); + if (!columns || columns.length === 0) { throw new Error('Table columns must be provided'); } @@ -155,7 +159,7 @@ function Table({compareItems, initialSortColumn}); - const processedData = [filterMiddleware, searchMiddleware, sortMiddleware].reduce((acc, middleware) => middleware(acc), data); + const processedData = [filterMiddleware, searchMiddleware, sortMiddleware].reduce((acc, middleware) => middleware(acc), tableData); const listRef = useRef>(null); @@ -181,7 +185,7 @@ function Table; }); - const originalDataLength = data?.length ?? 0; + const originalDataLength = tableData?.length ?? 0; const shouldUseNarrowTableLayout = shouldUseNarrowLayout || isMediumScreenWidth; // Check if filters are applied (not default values) diff --git a/src/components/Table/types.ts b/src/components/Table/types.ts index 6b7528d0577c..c1375daad50e 100644 --- a/src/components/Table/types.ts +++ b/src/components/Table/types.ts @@ -41,6 +41,10 @@ type TableColumn = { styling?: TableColumnStyling; }; +type TableRowData = T & { + selected?: boolean; +}; + /** * Methods exposed by the Table component for programmatic control. * Combines sorting, filtering, and searching capabilities. @@ -144,4 +148,16 @@ type TableProps>; }>; -export type {TableColumn, TableMethods, TableHandle, TableProps, SharedListProps, CompareItemsCallback, IsItemInFilterCallback, IsItemInSearchCallback, FilterConfig, ActiveSorting}; +export type { + TableColumn, + TableMethods, + TableRowData, + TableHandle, + TableProps, + SharedListProps, + CompareItemsCallback, + IsItemInFilterCallback, + IsItemInSearchCallback, + FilterConfig, + ActiveSorting, +}; From 3c9e62fb37cbd859232078b0b43d3d0670fd34b4 Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Fri, 15 May 2026 12:46:45 -0400 Subject: [PATCH 12/18] add more selection logic --- src/components/Table/Table.tsx | 44 ++++++++++++++----- src/components/Table/TableContext.tsx | 21 +++++---- src/components/Table/TableRow.tsx | 12 +++-- src/components/Table/middlewares/filtering.ts | 32 +++++++------- src/components/Table/middlewares/selection.ts | 10 +++++ src/components/Table/types.ts | 31 +++++++------ 6 files changed, 92 insertions(+), 58 deletions(-) create mode 100644 src/components/Table/middlewares/selection.ts diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 502ee1058d79..721c086713ec 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -129,7 +129,7 @@ import type {TableHandle, TableMethods, TableProps, TableRowData} from './types' * * ``` */ -function Table({ +function Table({ ref, title, data = [], @@ -142,9 +142,13 @@ function Table) { - const [tableData, setTableData] = useState[]>(() => { - return data.map((item, index) => ({...item, checked: false})); +}: TableProps) { + const [tableData, setTableData] = useState[]>(() => { + return data.map((item, index) => ({ + ...item, + selected: false, + rowKey: index.toString(), + })); }); if (!columns || columns.length === 0) { @@ -153,15 +157,15 @@ function Table({filters, isItemInFilter}); + const {middleware: filterMiddleware, currentFilters, methods: filterMethods} = useFiltering, FilterKey>({filters, isItemInFilter}); - const {middleware: searchMiddleware, activeSearchString, methods: searchMethods} = useSearching({isItemInSearch}); + const {middleware: searchMiddleware, activeSearchString, methods: searchMethods} = useSearching>({isItemInSearch}); - const {middleware: sortMiddleware, activeSorting, methods: sortMethods} = useSorting({compareItems, initialSortColumn}); + const {middleware: sortMiddleware, activeSorting, methods: sortMethods} = useSorting, ColumnKey>({compareItems, initialSortColumn}); const processedData = [filterMiddleware, searchMiddleware, sortMiddleware].reduce((acc, middleware) => middleware(acc), tableData); - const listRef = useRef>(null); + const listRef = useRef>(null); const tableMethods: TableMethods = { ...filterMethods, @@ -180,9 +184,9 @@ function Table]; + return listRef.current?.[property as keyof FlashListRef]; }, - }) as TableHandle; + }) as TableHandle; }); const originalDataLength = tableData?.length ?? 0; @@ -200,8 +204,23 @@ function Table 0; const isEmptyResult = processedData.length === 0 && originalDataLength > 0 && (hasSearchString || hasActiveFilters); + /** + * + */ + const handleRowSelection = (rowKey: string) => { + setTableData((prevData) => { + return prevData.map((item) => { + if (item.rowKey === rowKey) { + return {...item, selected: !item.selected}; + } + + return item; + }); + }); + }; + // eslint-disable-next-line react/jsx-no-constructed-context-values - const contextValue: TableContextValue = { + const contextValue: TableContextValue = { title, listRef, listProps, @@ -216,11 +235,12 @@ function Table}>{children}; + return }>{children}; } export default Table; diff --git a/src/components/Table/TableContext.tsx b/src/components/Table/TableContext.tsx index ac2961417dd6..f7d61ddc9460 100644 --- a/src/components/Table/TableContext.tsx +++ b/src/components/Table/TableContext.tsx @@ -2,30 +2,30 @@ import type {FlashListRef} from '@shopify/flash-list'; import React, {createContext, useContext} from 'react'; import type {FilterConfig} from './middlewares/filtering'; import type {ActiveSorting} from './middlewares/sorting'; -import type {SharedListProps, TableColumn, TableMethods} from './types'; +import type {SharedListProps, TableColumn, TableMethods, TableRowData} from './types'; /** * The shape of the Table context value. * This context is provided by the `` component and consumed by its sub-components. * - * @template T - The type of items in the table's data array. + * @template DataType - The type of items in the table's data array. * @template ColumnKey - A string literal type representing the valid column keys. */ -type TableContextValue = { +type TableContextValue = { /** The title of the table when shown on smaller screens. */ title?: string; /** Reference to the underlying FlashList for programmatic control. */ - listRef: React.RefObject | null>; + listRef: React.RefObject | null>; /** FlashList props passed through from the Table component. */ - listProps: SharedListProps; + listProps: SharedListProps; /** Whether or not selection is enabled for the table */ selectionEnabled?: boolean; /** The data array after filtering, searching, and sorting have been applied. */ - processedData: T[]; + processedData: TableRowData[]; /** The original length of the data array before any processing. */ originalDataLength: number; @@ -57,11 +57,13 @@ type TableContextValue void; + /** Whether to use a narrow layout (e.g. on mobile screens). */ shouldUseNarrowTableLayout: boolean; }; -const defaultTableContextValue: TableContextValue = { +const defaultTableContextValue: TableContextValue = { listRef: React.createRef(), processedData: [], originalDataLength: 0, @@ -75,6 +77,7 @@ const defaultTableContextValue: TableContextValue = { tableMethods: {} as TableMethods, filterConfig: undefined, listProps: {} as SharedListProps, + handleRowSelection: () => {}, hasActiveFilters: false, hasSearchString: false, isEmptyResult: false, @@ -100,14 +103,14 @@ const TableContext = createContext(defaultTableContextValue); * } * ``` */ -function useTableContext() { +function useTableContext() { const context = useContext(TableContext); if (context === defaultTableContextValue && context.activeFilters === undefined) { throw new Error('useTableContext must be used within a Table provider'); } - return context as unknown as TableContextValue; + return context as unknown as TableContextValue; } export default TableContext; diff --git a/src/components/Table/TableRow.tsx b/src/components/Table/TableRow.tsx index a3097ee151bd..ea39402a39fd 100644 --- a/src/components/Table/TableRow.tsx +++ b/src/components/Table/TableRow.tsx @@ -55,8 +55,9 @@ export default function TableRow({ const theme = useTheme(); const styles = useThemeStyles(); - const {processedData, columns, shouldUseNarrowTableLayout, selectionEnabled} = useTableContext(); + const {processedData, columns, shouldUseNarrowTableLayout, handleRowSelection, selectionEnabled} = useTableContext(); + const item = processedData[rowIndex]; const rowCount = processedData.length; const isLastRow = rowIndex === rowCount - 1; const isInteractive = interactive && !isLoading; @@ -98,11 +99,7 @@ export default function TableRow({ }; return ( - + {}} + onPress={() => handleRowSelection(item.rowKey)} /> )} diff --git a/src/components/Table/middlewares/filtering.ts b/src/components/Table/middlewares/filtering.ts index 84a01bbd77e4..690269c2de1a 100644 --- a/src/components/Table/middlewares/filtering.ts +++ b/src/components/Table/middlewares/filtering.ts @@ -23,12 +23,12 @@ type FilterConfig = Record = (item: T, filters: string[]) => boolean; +type IsItemInFilterCallback = (item: DataType, filters: string[]) => boolean; /** * Methods exposed by the table to control filtering. @@ -46,34 +46,34 @@ type FilteringMethods = { /** * Props for the filtering middleware. * - * @template T - The type of items in the data array. + * @template DataType - The type of items in the data array. * @template FilterKey - The type of filter keys. */ -type UseFilteringProps = { +type UseFilteringProps = { filters?: FilterConfig; - isItemInFilter?: IsItemInFilterCallback; + isItemInFilter?: IsItemInFilterCallback; }; /** * Result returned by the filtering middleware. * - * @template T - The type of items in the data array. + * @template DataType - The type of items in the data array. * @template FilterKey - The type of filter keys. */ -type UseFilteringResult = MiddlewareHookResult> & { +type UseFilteringResult = MiddlewareHookResult> & { currentFilters: Record; }; /** * Provides functionality to filter table data. * - * @template T - The type of items in the data array. + * @template DataType - The type of items in the data array. * @template FilterKey - The type of filter keys. * @param filters - The filters to use. * @param isItemInFilter - The callback to check if an item matches a filter. * @returns The result of the filtering middleware. */ -function useFiltering({filters, isItemInFilter}: UseFilteringProps): UseFilteringResult { +function useFiltering({filters, isItemInFilter}: UseFilteringProps): UseFilteringResult { const [currentFilters, setCurrentFilters] = useState>(() => { const initialFilters = {} as Record; @@ -97,7 +97,7 @@ function useFiltering({filters, isItemInFi return currentFilters; }; - const middleware: Middleware = (data) => filter({data, filters, currentFilters, isItemInFilter}); + const middleware: Middleware = (data) => filter({data, filters, currentFilters, isItemInFilter}); const methods: FilteringMethods = { updateFilter, @@ -110,20 +110,20 @@ function useFiltering({filters, isItemInFi /** * Parameters for the filtering middleware. * - * @template T - The type of items in the data array. + * @template DataType - The type of items in the data array. * @template FilterKey - The type of filter keys. */ -type FilteringMiddlewareParams = { - data: T[]; +type FilteringMiddlewareParams = { + data: DataType[]; filters?: FilterConfig; currentFilters: Record; - isItemInFilter?: IsItemInFilterCallback; + isItemInFilter?: IsItemInFilterCallback; }; /** * Filters table data based on the current filters. * - * @template T - The type of items in the data array. + * @template DataType - The type of items in the data array. * @template FilterKey - The type of filter keys. * @param data - The data to filter. * @param filters - The filters to use. @@ -131,7 +131,7 @@ type FilteringMiddlewareParams = { * @param isItemInFilter - The callback to check if an item matches a filter. * @returns The filtered data. */ -function filter({data, filters, currentFilters, isItemInFilter}: FilteringMiddlewareParams): T[] { +function filter({data, filters, currentFilters, isItemInFilter}: FilteringMiddlewareParams): DataType[] { if (!filters) { // No filters configured, return original data. return data; diff --git a/src/components/Table/middlewares/selection.ts b/src/components/Table/middlewares/selection.ts new file mode 100644 index 000000000000..8deb492c3b05 --- /dev/null +++ b/src/components/Table/middlewares/selection.ts @@ -0,0 +1,10 @@ +import {Middleware} from './types'; + +export default function useSelection() { + const middleware: Middleware = (data) => { + // Return filtered data; + return data; + }; + + return {middleware}; +} diff --git a/src/components/Table/types.ts b/src/components/Table/types.ts index c1375daad50e..07dbaff70f3f 100644 --- a/src/components/Table/types.ts +++ b/src/components/Table/types.ts @@ -41,15 +41,18 @@ type TableColumn = { styling?: TableColumnStyling; }; -type TableRowData = T & { - selected?: boolean; +type TableRowData = DataType & { + /** The key for the row */ + rowKey: string; + + /** Whether or not the row is selected or not */ + selected: boolean; }; /** * Methods exposed by the Table component for programmatic control. * Combines sorting, filtering, and searching capabilities. * - * @template T - The type of items in the table's data array (unused in methods, kept for consistency). * @template ColumnKey - A string literal type representing the valid column keys. * @template FilterKey - A string literal type representing the valid filter keys. */ @@ -59,18 +62,18 @@ type TableMethods = FlashListRef & TableMethods; +type TableHandle = FlashListRef & TableMethods; /** * FlashList props with the 'data' prop omitted, as the Table manages data internally. * - * @template T - The type of items in the table's data array. + * @template DataType - The type of items in the table's data array. */ -type SharedListProps = Omit, 'data'>; +type SharedListProps = Omit, 'data'>; /** * Props for the Table component. @@ -79,7 +82,7 @@ type SharedListProps = Omit, 'data'>; * state and provides context, while child components (``, ``, * ``, ``) consume that context to render UI. * - * @template T - The type of items in the table's data array. + * @template DataType - The type of items in the table's data array. * @template ColumnKey - A string literal type representing the valid column keys. * @template FilterKey - A string literal type representing the valid filter keys. * @@ -99,13 +102,13 @@ type SharedListProps = Omit, 'data'>; *
* ``` */ -type TableProps = SharedListProps & +type TableProps = SharedListProps & PropsWithChildren<{ /** The title for the table when shown on smaller screens */ title?: string; /** Array of data items to display in the table. */ - data: T[] | undefined; + data: TableRowData[] | undefined; /** Whether multi selection is enabled */ selectionEnabled?: boolean; @@ -130,22 +133,22 @@ type TableProps; + compareItems?: CompareItemsCallback; /** * Predicate function to determine if an item matches the active filters. * Receives an item and an array of active filter values. */ - isItemInFilter?: IsItemInFilterCallback; + isItemInFilter?: IsItemInFilterCallback; /** * Predicate function to determine if an item matches the search string. * Receives an item and the current search string. */ - isItemInSearch?: IsItemInSearchCallback; + isItemInSearch?: IsItemInSearchCallback; /** Ref to access table methods programmatically. */ - ref?: React.Ref>; + ref?: React.Ref>; }>; export type { From 33d738ff795934d78704790442dfeac63e2b79fc Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Fri, 15 May 2026 12:54:30 -0400 Subject: [PATCH 13/18] update selection state for table row --- src/components/Table/TableRow.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/Table/TableRow.tsx b/src/components/Table/TableRow.tsx index ea39402a39fd..fde198480fa8 100644 --- a/src/components/Table/TableRow.tsx +++ b/src/components/Table/TableRow.tsx @@ -77,6 +77,7 @@ export default function TableRow({ shouldUseNarrowTableLayout && !isLoading && styles.pv4, !shouldUseNarrowTableLayout && !isLoading && styles.pv2, isLastRow ? styles.tableBottomRadius : styles.borderBottom, + item.selected && [styles.activeComponentBG, {borderColor: theme.buttonHoveredBG}], shouldUseNarrowTableLayout ? styles.tableRowHeightCompact : styles.tableRowHeight, ]; @@ -90,6 +91,16 @@ export default function TableRow({ !shouldUseNarrowTableLayout && [styles.dGrid, {gridTemplateColumns: gridTemplateColumns.join(' ')}], ]; + const tableRowPressableHoverStyle = (() => { + if (!isInteractive) { + return undefined; + } else if (item.selected) { + return styles.activeComponentBG; + } else { + return styles.hoveredComponentBG; + } + })(); + const renderChildren = (state: PressableStateCallbackType) => { if (typeof children === 'function') { return children(state); @@ -106,8 +117,8 @@ export default function TableRow({ style={tableRowPressableStyles} sentryLabel={sentryLabel} interactive={isInteractive} + hoverStyle={tableRowPressableHoverStyle} pressDimmingValue={isInteractive ? undefined : 1} - hoverStyle={isInteractive && styles.hoveredComponentBG} role={isInteractive ? CONST.ROLE.BUTTON : CONST.ROLE.PRESENTATION} onPress={onPress} {...props} From ed97bf1d007761df499b467790c5a273488e93a2 Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Fri, 15 May 2026 13:35:54 -0400 Subject: [PATCH 14/18] shift click logic --- src/components/Table/Table.tsx | 47 ++++++++++++++++++- src/components/Table/TableContext.tsx | 5 +- src/components/Table/TableRow.tsx | 32 ++++++++----- src/components/Table/middlewares/selection.ts | 10 ---- .../categories/WorkspaceCategoriesPage.tsx | 2 +- 5 files changed, 72 insertions(+), 24 deletions(-) delete mode 100644 src/components/Table/middlewares/selection.ts diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 721c086713ec..e489ef79df35 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -143,6 +143,9 @@ function Table) { + const lastSelectedRowBooleanRef = useRef(false); + const lastSelectedRowKeyRef = useRef(null); + const [tableData, setTableData] = useState[]>(() => { return data.map((item, index) => ({ ...item, @@ -204,14 +207,55 @@ function Table 0; const isEmptyResult = processedData.length === 0 && originalDataLength > 0 && (hasSearchString || hasActiveFilters); + const handleShiftRowSelection = (rowKey: string) => { + const rowKeyExists = processedData.some((item) => item.rowKey === rowKey); + + if (!rowKeyExists) { + return; + } + + const lastSelectedKey = lastSelectedRowKeyRef.current; + + if (!lastSelectedKey) { + handleRowSelection(rowKey); + return; + } + + const rowKeys = processedData.map((item) => item.rowKey); + const lastIndex = rowKeys.indexOf(lastSelectedKey); + const currentIndex = rowKeys.indexOf(rowKey); + + if (lastIndex === -1 || currentIndex === -1) { + handleRowSelection(rowKey); + return; + } + + const [start, end] = currentIndex > lastIndex ? [lastIndex, currentIndex] : [currentIndex, lastIndex]; + const newSelectedState = lastSelectedRowBooleanRef.current; + + setTableData((prevData) => { + return prevData.map((item) => { + if (rowKeys.indexOf(item.rowKey) >= start && rowKeys.indexOf(item.rowKey) <= end) { + return {...item, selected: newSelectedState}; + } + + return item; + }); + }); + }; + /** * */ + // JACK_TODO: going to clean this up const handleRowSelection = (rowKey: string) => { setTableData((prevData) => { return prevData.map((item) => { if (item.rowKey === rowKey) { - return {...item, selected: !item.selected}; + const selected = !item.selected; + lastSelectedRowKeyRef.current = rowKey; + lastSelectedRowBooleanRef.current = selected; + return {...item, selected}; } return item; @@ -235,6 +279,7 @@ function Table void; + handleRowSelection: (rowKey: string) => void; /** Whether to use a narrow layout (e.g. on mobile screens). */ @@ -77,11 +79,12 @@ const defaultTableContextValue: TableContextValue = { tableMethods: {} as TableMethods, filterConfig: undefined, listProps: {} as SharedListProps, - handleRowSelection: () => {}, hasActiveFilters: false, hasSearchString: false, isEmptyResult: false, shouldUseNarrowTableLayout: false, + handleShiftRowSelection: () => {}, + handleRowSelection: () => {}, }; const TableContext = createContext(defaultTableContextValue); diff --git a/src/components/Table/TableRow.tsx b/src/components/Table/TableRow.tsx index fde198480fa8..2139af4b3b26 100644 --- a/src/components/Table/TableRow.tsx +++ b/src/components/Table/TableRow.tsx @@ -55,7 +55,7 @@ export default function TableRow({ const theme = useTheme(); const styles = useThemeStyles(); - const {processedData, columns, shouldUseNarrowTableLayout, handleRowSelection, selectionEnabled} = useTableContext(); + const {processedData, columns, shouldUseNarrowTableLayout, handleRowSelection, handleShiftRowSelection, selectionEnabled} = useTableContext(); const item = processedData[rowIndex]; const rowCount = processedData.length; @@ -109,6 +109,25 @@ export default function TableRow({ return children; }; + const CheckboxComponent = ( + + { + const webEvent = event as unknown as MouseEvent; + + if (webEvent && webEvent.shiftKey) { + handleShiftRowSelection(item.rowKey); + return; + } + + handleRowSelection(item.rowKey); + }} + /> + + ); + return ( ) : ( - {selectionEnabled && ( - - handleRowSelection(item.rowKey)} - /> - - )} - + {selectionEnabled && CheckboxComponent} {renderChildren(state)} ) diff --git a/src/components/Table/middlewares/selection.ts b/src/components/Table/middlewares/selection.ts deleted file mode 100644 index 8deb492c3b05..000000000000 --- a/src/components/Table/middlewares/selection.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {Middleware} from './types'; - -export default function useSelection() { - const middleware: Middleware = (data) => { - // Return filtered data; - return data; - }; - - return {middleware}; -} diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index cb1f7ba15c10..eb627ef5aaac 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -247,7 +247,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { action: () => { const path = isQuickSettingsFlow ? ROUTES.SETTINGS_CATEGORY_SETTINGS.getRoute(policyId, value.name, backTo) - : ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyId, value.name); + : createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(value.name)); Navigation.navigate(path); }, From a413425437e2f433a08000f35b5b616957c1ac2a Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Fri, 15 May 2026 14:12:49 -0400 Subject: [PATCH 15/18] add data type type --- src/components/Table/Table.tsx | 4 ++-- src/components/Table/TableContext.tsx | 10 +++++----- src/components/Table/middlewares/filtering.ts | 16 ++++++++++------ src/components/Table/middlewares/selection.ts | 12 ++++++++++++ src/components/Table/types.ts | 18 ++++++++++++------ 5 files changed, 41 insertions(+), 19 deletions(-) create mode 100644 src/components/Table/middlewares/selection.ts diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index e489ef79df35..a420bf145c10 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -6,7 +6,7 @@ import useSearching from './middlewares/searching'; import useSorting from './middlewares/sorting'; import TableContext from './TableContext'; import type {TableContextValue} from './TableContext'; -import type {TableHandle, TableMethods, TableProps, TableRowData} from './types'; +import type {TableData, TableHandle, TableMethods, TableProps, TableRow, TableRowData} from './types'; /** * A composable table component that provides filtering, search, and sorting functionality. @@ -129,7 +129,7 @@ import type {TableHandle, TableMethods, TableProps, TableRowData} from './types' * * ``` */ -function Table({ +function Table({ ref, title, data = [], diff --git a/src/components/Table/TableContext.tsx b/src/components/Table/TableContext.tsx index c592212fdb42..7dc1f14209a3 100644 --- a/src/components/Table/TableContext.tsx +++ b/src/components/Table/TableContext.tsx @@ -2,7 +2,7 @@ import type {FlashListRef} from '@shopify/flash-list'; import React, {createContext, useContext} from 'react'; import type {FilterConfig} from './middlewares/filtering'; import type {ActiveSorting} from './middlewares/sorting'; -import type {SharedListProps, TableColumn, TableMethods, TableRowData} from './types'; +import type {SharedListProps, TableColumn, TableData, TableMethods, TableRowData} from './types'; /** * The shape of the Table context value. @@ -11,7 +11,7 @@ import type {SharedListProps, TableColumn, TableMethods, TableRowData} from './t * @template DataType - The type of items in the table's data array. * @template ColumnKey - A string literal type representing the valid column keys. */ -type TableContextValue = { +type TableContextValue = { /** The title of the table when shown on smaller screens. */ title?: string; @@ -65,7 +65,7 @@ type TableContextValue = { +const defaultTableContextValue: TableContextValue = { listRef: React.createRef(), processedData: [], originalDataLength: 0, @@ -78,7 +78,7 @@ const defaultTableContextValue: TableContextValue = { activeSearchString: '', tableMethods: {} as TableMethods, filterConfig: undefined, - listProps: {} as SharedListProps, + listProps: {} as SharedListProps, hasActiveFilters: false, hasSearchString: false, isEmptyResult: false, @@ -106,7 +106,7 @@ const TableContext = createContext(defaultTableContextValue); * } * ``` */ -function useTableContext() { +function useTableContext() { const context = useContext(TableContext); if (context === defaultTableContextValue && context.activeFilters === undefined) { diff --git a/src/components/Table/middlewares/filtering.ts b/src/components/Table/middlewares/filtering.ts index 690269c2de1a..9f3f7bd7fb43 100644 --- a/src/components/Table/middlewares/filtering.ts +++ b/src/components/Table/middlewares/filtering.ts @@ -1,4 +1,5 @@ import {useState} from 'react'; +import {TableData} from '../types'; import type {Middleware, MiddlewareHookResult} from './types'; /** @@ -28,7 +29,7 @@ type FilterConfig = Record = (item: DataType, filters: string[]) => boolean; +type IsItemInFilterCallback = (item: DataType, filters: string[]) => boolean; /** * Methods exposed by the table to control filtering. @@ -49,7 +50,7 @@ type FilteringMethods = { * @template DataType - The type of items in the data array. * @template FilterKey - The type of filter keys. */ -type UseFilteringProps = { +type UseFilteringProps = { filters?: FilterConfig; isItemInFilter?: IsItemInFilterCallback; }; @@ -60,7 +61,7 @@ type UseFilteringProps = { * @template DataType - The type of items in the data array. * @template FilterKey - The type of filter keys. */ -type UseFilteringResult = MiddlewareHookResult> & { +type UseFilteringResult = MiddlewareHookResult> & { currentFilters: Record; }; @@ -73,7 +74,10 @@ type UseFilteringResult = Middlewar * @param isItemInFilter - The callback to check if an item matches a filter. * @returns The result of the filtering middleware. */ -function useFiltering({filters, isItemInFilter}: UseFilteringProps): UseFilteringResult { +function useFiltering({ + filters, + isItemInFilter, +}: UseFilteringProps): UseFilteringResult { const [currentFilters, setCurrentFilters] = useState>(() => { const initialFilters = {} as Record; @@ -113,7 +117,7 @@ function useFiltering({filters, isI * @template DataType - The type of items in the data array. * @template FilterKey - The type of filter keys. */ -type FilteringMiddlewareParams = { +type FilteringMiddlewareParams = { data: DataType[]; filters?: FilterConfig; currentFilters: Record; @@ -131,7 +135,7 @@ type FilteringMiddlewareParams = { * @param isItemInFilter - The callback to check if an item matches a filter. * @returns The filtered data. */ -function filter({data, filters, currentFilters, isItemInFilter}: FilteringMiddlewareParams): DataType[] { +function filter({data, filters, currentFilters, isItemInFilter}: FilteringMiddlewareParams): DataType[] { if (!filters) { // No filters configured, return original data. return data; diff --git a/src/components/Table/middlewares/selection.ts b/src/components/Table/middlewares/selection.ts new file mode 100644 index 000000000000..c208c454714e --- /dev/null +++ b/src/components/Table/middlewares/selection.ts @@ -0,0 +1,12 @@ +import {useState} from 'react'; +import {TableData} from '../types'; + +export default function useSelection() { + const [selectedKeys, setSelectedKeys] = useState([]); + + const middleware = (data: DataType[]) => { + return data.map((item) => ({...item, selected: selectedKeys.includes(item.rowKey)})); + }; + + return {}; +} diff --git a/src/components/Table/types.ts b/src/components/Table/types.ts index 07dbaff70f3f..9cadb90d37d8 100644 --- a/src/components/Table/types.ts +++ b/src/components/Table/types.ts @@ -5,6 +5,10 @@ import type {FilterConfig, FilteringMethods, IsItemInFilterCallback} from './mid import type {IsItemInSearchCallback, SearchingMethods} from './middlewares/searching'; import type {ActiveSorting, CompareItemsCallback, SortingMethods} from './middlewares/sorting'; +type TableData = { + rowKey: string; +}; + /** * Styling options for a table column. */ @@ -41,7 +45,7 @@ type TableColumn = { styling?: TableColumnStyling; }; -type TableRowData = DataType & { +type TableRow = DataType & { /** The key for the row */ rowKey: string; @@ -66,14 +70,14 @@ type TableMethods = FlashListRef & TableMethods; +type TableHandle = FlashListRef & TableMethods; /** * FlashList props with the 'data' prop omitted, as the Table manages data internally. * * @template DataType - The type of items in the table's data array. */ -type SharedListProps = Omit, 'data'>; +type SharedListProps = Omit, 'data'>; /** * Props for the Table component. @@ -102,13 +106,13 @@ type SharedListProps = Omit, 'data'>; * * ``` */ -type TableProps = SharedListProps & +type TableProps = SharedListProps & PropsWithChildren<{ /** The title for the table when shown on smaller screens */ title?: string; /** Array of data items to display in the table. */ - data: TableRowData[] | undefined; + data: TableRow[] | undefined; /** Whether multi selection is enabled */ selectionEnabled?: boolean; @@ -152,9 +156,11 @@ type TableProps; export type { + TableData, + TableRow, TableColumn, TableMethods, - TableRowData, + TableRow as TableRowData, TableHandle, TableProps, SharedListProps, From 2691534996b8078be6eecbb9eb35be08038cc07c Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Fri, 15 May 2026 14:39:26 -0400 Subject: [PATCH 16/18] add selection middleware --- src/components/Table/Table.tsx | 83 ++-------------- src/components/Table/TableContext.tsx | 4 - src/components/Table/middlewares/selection.ts | 99 ++++++++++++++++++- src/components/Table/types.ts | 6 +- 4 files changed, 107 insertions(+), 85 deletions(-) diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index a420bf145c10..9e80d1478ead 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -3,6 +3,7 @@ import React, {useImperativeHandle, useRef, useState} from 'react'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useFiltering from './middlewares/filtering'; import useSearching from './middlewares/searching'; +import useSelection from './middlewares/selection'; import useSorting from './middlewares/sorting'; import TableContext from './TableContext'; import type {TableContextValue} from './TableContext'; @@ -143,30 +144,21 @@ function Table) { - const lastSelectedRowBooleanRef = useRef(false); - const lastSelectedRowKeyRef = useRef(null); - - const [tableData, setTableData] = useState[]>(() => { - return data.map((item, index) => ({ - ...item, - selected: false, - rowKey: index.toString(), - })); - }); - if (!columns || columns.length === 0) { throw new Error('Table columns must be provided'); } const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout(); - const {middleware: filterMiddleware, currentFilters, methods: filterMethods} = useFiltering, FilterKey>({filters, isItemInFilter}); + const {middleware: filterMiddleware, currentFilters, methods: filterMethods} = useFiltering({filters, isItemInFilter}); - const {middleware: searchMiddleware, activeSearchString, methods: searchMethods} = useSearching>({isItemInSearch}); + const {middleware: searchMiddleware, activeSearchString, methods: searchMethods} = useSearching({isItemInSearch}); - const {middleware: sortMiddleware, activeSorting, methods: sortMethods} = useSorting, ColumnKey>({compareItems, initialSortColumn}); + const {middleware: sortMiddleware, activeSorting, methods: sortMethods} = useSorting({compareItems, initialSortColumn}); - const processedData = [filterMiddleware, searchMiddleware, sortMiddleware].reduce((acc, middleware) => middleware(acc), tableData); + const {middleware: selectionMiddleware, methods: selectionMethods} = useSelection({data}); + + const processedData = [filterMiddleware, searchMiddleware, sortMiddleware, selectionMiddleware].reduce((acc, middleware) => middleware(acc), data); const listRef = useRef>(null); @@ -174,6 +166,7 @@ function Table; }); - const originalDataLength = tableData?.length ?? 0; + const originalDataLength = data?.length ?? 0; const shouldUseNarrowTableLayout = shouldUseNarrowLayout || isMediumScreenWidth; // Check if filters are applied (not default values) @@ -207,62 +200,6 @@ function Table 0; const isEmptyResult = processedData.length === 0 && originalDataLength > 0 && (hasSearchString || hasActiveFilters); - const handleShiftRowSelection = (rowKey: string) => { - const rowKeyExists = processedData.some((item) => item.rowKey === rowKey); - - if (!rowKeyExists) { - return; - } - - const lastSelectedKey = lastSelectedRowKeyRef.current; - - if (!lastSelectedKey) { - handleRowSelection(rowKey); - return; - } - - const rowKeys = processedData.map((item) => item.rowKey); - const lastIndex = rowKeys.indexOf(lastSelectedKey); - const currentIndex = rowKeys.indexOf(rowKey); - - if (lastIndex === -1 || currentIndex === -1) { - handleRowSelection(rowKey); - return; - } - - const [start, end] = currentIndex > lastIndex ? [lastIndex, currentIndex] : [currentIndex, lastIndex]; - const newSelectedState = lastSelectedRowBooleanRef.current; - - setTableData((prevData) => { - return prevData.map((item) => { - if (rowKeys.indexOf(item.rowKey) >= start && rowKeys.indexOf(item.rowKey) <= end) { - return {...item, selected: newSelectedState}; - } - - return item; - }); - }); - }; - - /** - * - */ - // JACK_TODO: going to clean this up - const handleRowSelection = (rowKey: string) => { - setTableData((prevData) => { - return prevData.map((item) => { - if (item.rowKey === rowKey) { - const selected = !item.selected; - lastSelectedRowKeyRef.current = rowKey; - lastSelectedRowBooleanRef.current = selected; - return {...item, selected}; - } - - return item; - }); - }); - }; - // eslint-disable-next-line react/jsx-no-constructed-context-values const contextValue: TableContextValue = { title, @@ -279,8 +216,6 @@ function Table void; - - handleRowSelection: (rowKey: string) => void; - /** Whether to use a narrow layout (e.g. on mobile screens). */ shouldUseNarrowTableLayout: boolean; }; diff --git a/src/components/Table/middlewares/selection.ts b/src/components/Table/middlewares/selection.ts index c208c454714e..51dc7b563b3b 100644 --- a/src/components/Table/middlewares/selection.ts +++ b/src/components/Table/middlewares/selection.ts @@ -1,12 +1,105 @@ -import {useState} from 'react'; +import {useRef, useState} from 'react'; import {TableData} from '../types'; +import {MiddlewareHookResult} from './types'; -export default function useSelection() { +export type UseSelectionProps = { + data: DataType[]; +}; + +export type SelectionMethods = { + handleSelectAll: () => void; + + handleMultipleRowSelection: (rowKey: string) => void; + + handleSingleRowSelection: (rowKey: string) => void; +}; + +export type UseSelectionResult = MiddlewareHookResult; + +export default function useSelection({data}: UseSelectionProps): UseSelectionResult { + const lastSelectedRowKeyRef = useRef(null); + const lastSelectedRowIsSelectedRef = useRef(false); const [selectedKeys, setSelectedKeys] = useState([]); + /** + * + */ + const handleSelectAll = () => {}; + + /** + * + */ + const handleMultipleRowSelection = (rowKey: string) => { + const rowKeys = data.map((item) => item.rowKey); + const rowKeyExists = rowKeys.includes(rowKey); + + if (!rowKeyExists) { + return; + } + + const lastSelectedRowKey = lastSelectedRowKeyRef.current; + const lastSelectedRowIsSelected = lastSelectedRowIsSelectedRef.current; + + if (!lastSelectedRowKey) { + handleSingleRowSelection(rowKey); + return; + } + + const currentSelectedRowIndex = rowKeys.indexOf(rowKey); + const lastSelectedRowIndex = rowKeys.indexOf(lastSelectedRowKey); + + if (currentSelectedRowIndex === -1 || lastSelectedRowIndex === -1) { + handleSingleRowSelection(rowKey); + return; + } + + const startIndex = Math.min(currentSelectedRowIndex, lastSelectedRowIndex); + const endIndex = Math.max(currentSelectedRowIndex, lastSelectedRowIndex); + + setSelectedKeys((prevSelectedKeys) => { + const newSelectedKeys = [...prevSelectedKeys]; + + for (let i = startIndex; i <= endIndex; i++) { + const key = rowKeys[i]; + if (lastSelectedRowIsSelected) { + if (!newSelectedKeys.includes(key)) { + newSelectedKeys.push(key); + } + } else { + const index = newSelectedKeys.indexOf(key); + if (index !== -1) { + newSelectedKeys.splice(index, 1); + } + } + } + + return newSelectedKeys; + }); + }; + + /** + * + */ + const handleSingleRowSelection = (rowKey: string) => { + setSelectedKeys((prevSelectedKeys) => { + if (prevSelectedKeys.includes(rowKey)) { + return prevSelectedKeys.filter((key) => key !== rowKey); + } else { + return [...prevSelectedKeys, rowKey]; + } + }); + }; + const middleware = (data: DataType[]) => { return data.map((item) => ({...item, selected: selectedKeys.includes(item.rowKey)})); }; - return {}; + return { + middleware, + methods: { + handleSelectAll, + handleMultipleRowSelection, + handleSingleRowSelection, + }, + }; } diff --git a/src/components/Table/types.ts b/src/components/Table/types.ts index 9cadb90d37d8..6b94c6754d9d 100644 --- a/src/components/Table/types.ts +++ b/src/components/Table/types.ts @@ -3,6 +3,7 @@ import type {PropsWithChildren} from 'react'; import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; import type {FilterConfig, FilteringMethods, IsItemInFilterCallback} from './middlewares/filtering'; import type {IsItemInSearchCallback, SearchingMethods} from './middlewares/searching'; +import {SelectionMethods} from './middlewares/selection'; import type {ActiveSorting, CompareItemsCallback, SortingMethods} from './middlewares/sorting'; type TableData = { @@ -46,9 +47,6 @@ type TableColumn = { }; type TableRow = DataType & { - /** The key for the row */ - rowKey: string; - /** Whether or not the row is selected or not */ selected: boolean; }; @@ -60,7 +58,7 @@ type TableRow = DataType & { * @template ColumnKey - A string literal type representing the valid column keys. * @template FilterKey - A string literal type representing the valid filter keys. */ -type TableMethods = SortingMethods & FilteringMethods & SearchingMethods; +type TableMethods = SortingMethods & FilteringMethods & SearchingMethods & SelectionMethods; /** * The ref handle type for the Table component. From 9edf6925bc58a986d3c3abc2442247c95d67be89 Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Fri, 15 May 2026 14:56:33 -0400 Subject: [PATCH 17/18] update middleware types --- src/components/Table/Table.tsx | 9 +++++++-- src/components/Table/middlewares/selection.ts | 4 ++-- src/components/Table/middlewares/types.ts | 10 +++------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 9e80d1478ead..911e4c7701e4 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -33,12 +33,13 @@ import type {TableData, TableHandle, TableMethods, TableProps, TableRow, TableRo * 1. **Filtering** - Applies dropdown filter selections * 2. **Searching** - Applies search string filtering * 3. **Sorting** - Sorts data by the active column + * 4. **Selection** - Applies row selection state & provides helpers for selection * * Each middleware transforms the data array and passes it to the next. * * ## Generic Type Parameters * - * - `T` - The type of items in your data array + * - `DataType` - The type of items in your data array * - `ColumnKey` - String literal union of valid column keys (e.g., `'name' | 'date'`) * - `FilterKey` - String literal union of valid filter keys * @@ -158,7 +159,11 @@ function Table({data}); - const processedData = [filterMiddleware, searchMiddleware, sortMiddleware, selectionMiddleware].reduce((acc, middleware) => middleware(acc), data); + // Apply the middleware + const filteredData = filterMiddleware(data); + const searchedData = searchMiddleware(filteredData); + const sortedData = sortMiddleware(searchedData); + const processedData = selectionMiddleware(sortedData); const listRef = useRef>(null); diff --git a/src/components/Table/middlewares/selection.ts b/src/components/Table/middlewares/selection.ts index 51dc7b563b3b..a9b10ca0d860 100644 --- a/src/components/Table/middlewares/selection.ts +++ b/src/components/Table/middlewares/selection.ts @@ -1,5 +1,5 @@ import {useRef, useState} from 'react'; -import {TableData} from '../types'; +import {TableData, TableRow} from '../types'; import {MiddlewareHookResult} from './types'; export type UseSelectionProps = { @@ -14,7 +14,7 @@ export type SelectionMethods = { handleSingleRowSelection: (rowKey: string) => void; }; -export type UseSelectionResult = MiddlewareHookResult; +export type UseSelectionResult = MiddlewareHookResult>; export default function useSelection({data}: UseSelectionProps): UseSelectionResult { const lastSelectedRowKeyRef = useRef(null); diff --git a/src/components/Table/middlewares/types.ts b/src/components/Table/middlewares/types.ts index af4af5589929..80c0922936b9 100644 --- a/src/components/Table/middlewares/types.ts +++ b/src/components/Table/middlewares/types.ts @@ -3,19 +3,15 @@ * * Middlewares are pure functions that receive data and return transformed data. * They are chained together in a pipeline: filter → search → sort. - * - * @template T - The type of items in the data array. */ -type Middleware = (data: T[]) => T[]; +type Middleware = (data: InputDataType[]) => OutputDataType[]; /** * Result returned by a middleware hook. - * - * @template T - The type of items in the data array. */ -type MiddlewareHookResult = { +type MiddlewareHookResult = { /** The middleware function to apply to data. */ - middleware: Middleware; + middleware: Middleware; /** Optional methods exposed by the middleware for external control. */ methods: Methods; From 1206ed946016a94a78391c1894642955dc484643 Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Fri, 15 May 2026 15:16:53 -0400 Subject: [PATCH 18/18] update table component to use new props --- src/components/Table/Table.tsx | 4 +- src/components/Table/TableRow.tsx | 40 +++++++++---------- src/components/Table/middlewares/selection.ts | 32 +++++++-------- src/components/Table/types.ts | 4 +- .../Tables/WorkspaceCategoriesTable/index.tsx | 5 +-- .../WorkspaceCompanyCardsTableRow.tsx | 23 ++++++----- .../WorkspaceCompanyCardsTable/index.tsx | 5 +++ .../categories/WorkspaceCategoriesPage.tsx | 4 +- 8 files changed, 60 insertions(+), 57 deletions(-) diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 911e4c7701e4..cf7c3170ea6d 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -1,5 +1,5 @@ import type {FlashListRef} from '@shopify/flash-list'; -import React, {useImperativeHandle, useRef, useState} from 'react'; +import React, {useImperativeHandle, useRef} from 'react'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useFiltering from './middlewares/filtering'; import useSearching from './middlewares/searching'; @@ -7,7 +7,7 @@ import useSelection from './middlewares/selection'; import useSorting from './middlewares/sorting'; import TableContext from './TableContext'; import type {TableContextValue} from './TableContext'; -import type {TableData, TableHandle, TableMethods, TableProps, TableRow, TableRowData} from './types'; +import type {TableData, TableHandle, TableMethods, TableProps} from './types'; /** * A composable table component that provides filtering, search, and sorting functionality. diff --git a/src/components/Table/TableRow.tsx b/src/components/Table/TableRow.tsx index 2139af4b3b26..6ccb623e9569 100644 --- a/src/components/Table/TableRow.tsx +++ b/src/components/Table/TableRow.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import type {PressableStateCallbackType} from 'react-native'; +import type {GestureResponderEvent, KeyboardEvent, PressableStateCallbackType} from 'react-native'; import {View} from 'react-native'; import Checkbox from '@components/Checkbox'; import type {OfflineWithFeedbackProps} from '@components/OfflineWithFeedback'; @@ -55,7 +55,7 @@ export default function TableRow({ const theme = useTheme(); const styles = useThemeStyles(); - const {processedData, columns, shouldUseNarrowTableLayout, handleRowSelection, handleShiftRowSelection, selectionEnabled} = useTableContext(); + const {processedData, columns, shouldUseNarrowTableLayout, tableMethods, selectionEnabled} = useTableContext(); const item = processedData[rowIndex]; const rowCount = processedData.length; @@ -109,24 +109,14 @@ export default function TableRow({ return children; }; - const CheckboxComponent = ( - - { - const webEvent = event as unknown as MouseEvent; - - if (webEvent && webEvent.shiftKey) { - handleShiftRowSelection(item.rowKey); - return; - } - - handleRowSelection(item.rowKey); - }} - /> - - ); + const handleCheckboxPress = (event?: MouseEvent) => { + if (event && event.shiftKey) { + tableMethods.handleMultipleRowSelection(item.keyForList); + return; + } + + tableMethods.handleSingleRowSelection(item.keyForList); + }; return ( @@ -156,7 +146,15 @@ export default function TableRow({
) : ( - {selectionEnabled && CheckboxComponent} + {selectionEnabled && ( + + handleCheckboxPress(event as unknown as MouseEvent)} + /> + + )} {renderChildren(state)} ) diff --git a/src/components/Table/middlewares/selection.ts b/src/components/Table/middlewares/selection.ts index a9b10ca0d860..959e478573e9 100644 --- a/src/components/Table/middlewares/selection.ts +++ b/src/components/Table/middlewares/selection.ts @@ -9,9 +9,9 @@ export type UseSelectionProps = { export type SelectionMethods = { handleSelectAll: () => void; - handleMultipleRowSelection: (rowKey: string) => void; + handleMultipleRowSelection: (keyForList: string) => void; - handleSingleRowSelection: (rowKey: string) => void; + handleSingleRowSelection: (keyForList: string) => void; }; export type UseSelectionResult = MiddlewareHookResult>; @@ -29,11 +29,11 @@ export default function useSelection({data}: UseSele /** * */ - const handleMultipleRowSelection = (rowKey: string) => { - const rowKeys = data.map((item) => item.rowKey); - const rowKeyExists = rowKeys.includes(rowKey); + const handleMultipleRowSelection = (keyForList: string) => { + const keyForLists = data.map((item) => item.keyForList); + const keyForListExists = keyForLists.includes(keyForList); - if (!rowKeyExists) { + if (!keyForListExists) { return; } @@ -41,15 +41,15 @@ export default function useSelection({data}: UseSele const lastSelectedRowIsSelected = lastSelectedRowIsSelectedRef.current; if (!lastSelectedRowKey) { - handleSingleRowSelection(rowKey); + handleSingleRowSelection(keyForList); return; } - const currentSelectedRowIndex = rowKeys.indexOf(rowKey); - const lastSelectedRowIndex = rowKeys.indexOf(lastSelectedRowKey); + const currentSelectedRowIndex = keyForLists.indexOf(keyForList); + const lastSelectedRowIndex = keyForLists.indexOf(lastSelectedRowKey); if (currentSelectedRowIndex === -1 || lastSelectedRowIndex === -1) { - handleSingleRowSelection(rowKey); + handleSingleRowSelection(keyForList); return; } @@ -60,7 +60,7 @@ export default function useSelection({data}: UseSele const newSelectedKeys = [...prevSelectedKeys]; for (let i = startIndex; i <= endIndex; i++) { - const key = rowKeys[i]; + const key = keyForLists[i]; if (lastSelectedRowIsSelected) { if (!newSelectedKeys.includes(key)) { newSelectedKeys.push(key); @@ -80,18 +80,18 @@ export default function useSelection({data}: UseSele /** * */ - const handleSingleRowSelection = (rowKey: string) => { + const handleSingleRowSelection = (keyForList: string) => { setSelectedKeys((prevSelectedKeys) => { - if (prevSelectedKeys.includes(rowKey)) { - return prevSelectedKeys.filter((key) => key !== rowKey); + if (prevSelectedKeys.includes(keyForList)) { + return prevSelectedKeys.filter((key) => key !== keyForList); } else { - return [...prevSelectedKeys, rowKey]; + return [...prevSelectedKeys, keyForList]; } }); }; const middleware = (data: DataType[]) => { - return data.map((item) => ({...item, selected: selectedKeys.includes(item.rowKey)})); + return data.map((item) => ({...item, selected: selectedKeys.includes(item.keyForList)})); }; return { diff --git a/src/components/Table/types.ts b/src/components/Table/types.ts index 6b94c6754d9d..636cd59139f5 100644 --- a/src/components/Table/types.ts +++ b/src/components/Table/types.ts @@ -7,7 +7,7 @@ import {SelectionMethods} from './middlewares/selection'; import type {ActiveSorting, CompareItemsCallback, SortingMethods} from './middlewares/sorting'; type TableData = { - rowKey: string; + keyForList: string; }; /** @@ -110,7 +110,7 @@ type TableProps[] | undefined; + data: DataType[] | undefined; /** Whether multi selection is enabled */ selectionEnabled?: boolean; diff --git a/src/components/Tables/WorkspaceCategoriesTable/index.tsx b/src/components/Tables/WorkspaceCategoriesTable/index.tsx index 8b3bd9798825..eaa8f92b1b2a 100644 --- a/src/components/Tables/WorkspaceCategoriesTable/index.tsx +++ b/src/components/Tables/WorkspaceCategoriesTable/index.tsx @@ -1,6 +1,6 @@ import type {ListRenderItemInfo} from '@shopify/flash-list'; import React from 'react'; -import Table, {CompareItemsCallback, IsItemInSearchCallback, TableColumn} from '@components/Table/'; +import Table, {CompareItemsCallback, IsItemInSearchCallback, TableColumn, TableData} from '@components/Table/'; import useLocalize from '@hooks/useLocalize'; import {AvatarSource} from '@libs/UserAvatarUtils'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; @@ -8,8 +8,7 @@ import WorkspaceCategoriesTableRow from './WorkspaceCategoriesTableRow'; export type WorkspaceCategoryTableColumnKey = 'name' | 'glCode' | 'approver' | 'enabled' | 'actions'; -export type WorkspaceCategoryTableRowData = { - keyForList: string; +export type WorkspaceCategoryTableRowData = TableData & { name: string; glCode?: string; approverAvatar?: AvatarSource; diff --git a/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableRow.tsx b/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableRow.tsx index bdc2ac9a40e1..b9fc8c897415 100644 --- a/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableRow.tsx +++ b/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableRow.tsx @@ -4,7 +4,7 @@ import {View} from 'react-native'; import Button from '@components/Button'; import Icon from '@components/Icon'; import ReportActionAvatars from '@components/ReportActionAvatars'; -import Table from '@components/Table'; +import Table, {TableData} from '@components/Table'; import Text from '@components/Text'; import TextWithTooltip from '@components/TextWithTooltip'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; @@ -22,19 +22,20 @@ import type {Card, CompanyCardFeed, CompanyCardFeedWithDomainID} from '@src/type import type {CardAssignmentData} from '@src/types/onyx/Card'; import WorkspaceCompanyCardsTableSkeleton from './WorkspaceCompanyCardsTableSkeleton'; -type WorkspaceCompanyCardTableRowData = CardAssignmentData & { - /** Whether the card is deleted */ - isCardDeleted: boolean; +type WorkspaceCompanyCardTableRowData = TableData & + CardAssignmentData & { + /** Whether the card is deleted */ + isCardDeleted: boolean; - /** Whether the card is assigned */ - isAssigned: boolean; + /** Whether the card is assigned */ + isAssigned: boolean; - /** Assigned card */ - assignedCard?: Card; + /** Assigned card */ + assignedCard?: Card; - /** On dismiss error callback */ - onDismissError?: () => void; -}; + /** On dismiss error callback */ + onDismissError?: () => void; + }; type WorkspaceCompanyCardTableRowProps = { /** The workspace company card table item */ diff --git a/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx b/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx index ed57d777458c..b03621cb39cb 100644 --- a/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx +++ b/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx @@ -148,18 +148,22 @@ function WorkspaceCompanyCardsTable({ { key: 'member', label: translate('common.member'), + sortable: true, }, { key: 'card', label: translate('workspace.companyCards.card'), + sortable: true, }, { key: 'customCardName', label: translate('workspace.companyCards.cardName'), + sortable: true, }, { key: 'actions', label: '', + sortable: false, styling: { containerStyles: [styles.justifyContentEnd, styles.pr3], }, @@ -173,6 +177,7 @@ function WorkspaceCompanyCardsTable({ return { cardName, + keyForList: `${cardName}_${assignedCard?.cardID ?? 'unassigned'}_${encryptedCardNumber}`, encryptedCardNumber, customCardName: assignedCard?.cardID && customCardNames?.[assignedCard.cardID] ? customCardNames?.[assignedCard.cardID] : getDefaultCardName(cardholder?.displayName ?? ''), isCardDeleted: assignedCard?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index eb627ef5aaac..2bec634b49b9 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -222,7 +222,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { const categoryRows = useMemo(() => { const categories = Object.values(policyCategories ?? {}); - return categories.reduce((acc, value) => { + return categories.reduce((acc, value, index) => { const isDisabled = value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; if (!isOffline && isDisabled) { @@ -235,7 +235,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { const approverDisplayName = displayName ? formatPhoneNumber(displayName) : ''; acc.push({ - keyForList: value.name, + keyForList: `${value.name}-${index}`, name: getDecodedCategoryName(value.name), glCode: value['GL Code'], approverAvatar,