Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
9c8b9e2
create new folder
JS00001 May 13, 2026
4d06dbe
setup the component
JS00001 May 13, 2026
c080185
add the sortihng & comparing
JS00001 May 13, 2026
5d6d393
add row data
JS00001 May 13, 2026
d8f2b3a
build the category items
JS00001 May 13, 2026
05da384
add more properties to the table header
JS00001 May 13, 2026
dc9532a
fix grid columns and add ne wheaderprosp
JS00001 May 13, 2026
9aa5b23
add the row content
JS00001 May 13, 2026
47ea4f0
add selection mode
JS00001 May 13, 2026
1519879
Merge branch 'jsenyitko-table-exploration' of github.com:Expensify/Ap…
JS00001 May 13, 2026
065f072
add checkboxes
JS00001 May 14, 2026
f405093
Merge branch 'main' of github.com:Expensify/App into jsenyitko-catego…
JS00001 May 14, 2026
7cdf97d
update table data
JS00001 May 15, 2026
87cc7c0
Merge branch 'main' of github.com:Expensify/App into jsenyitko-catego…
JS00001 May 15, 2026
3c9e62f
add more selection logic
JS00001 May 15, 2026
33d738f
update selection state for table row
JS00001 May 15, 2026
ed97bf1
shift click logic
JS00001 May 15, 2026
a413425
add data type type
JS00001 May 15, 2026
2691534
add selection middleware
JS00001 May 15, 2026
9edf692
update middleware types
JS00001 May 15, 2026
1206ed9
update table component to use new props
JS00001 May 15, 2026
e803a39
Merge branch 'jsenyitko-workspace-tables' of github.com:Expensify/App…
JS00001 May 19, 2026
140e8d4
Merge branch 'jsenyitko-workspace-tables' of github.com:Expensify/App…
JS00001 May 19, 2026
801e4c7
add toggling
JS00001 May 19, 2026
c41ece5
Merge branch 'jsenyitko-workspace-tables' of github.com:Expensify/App…
JS00001 May 20, 2026
50c6229
Merge branch 'jsenyitko-workspace-tables' of github.com:Expensify/App…
JS00001 May 20, 2026
8527678
add some mobile slection logic
JS00001 May 20, 2026
3096412
use global onyx state for selection enabled
JS00001 May 20, 2026
0cc1be5
add toggling to categories
JS00001 May 20, 2026
541f733
cleanup code
JS00001 May 20, 2026
0b36466
stash
JS00001 May 20, 2026
16c6c73
Merge branch 'jsenyitko-workspace-tables' of github.com:Expensify/App…
JS00001 May 20, 2026
ab750dc
Merge branch 'jsenyitko-workspace-tables' of github.com:Expensify/App…
JS00001 May 21, 2026
fc0a2bd
add checkbox to header
JS00001 May 21, 2026
ae293ed
add indeterminate checks
JS00001 May 21, 2026
fc6038c
add row seleection logic
JS00001 May 21, 2026
1729f47
add row selection handling
JS00001 May 21, 2026
c4f6a2c
remove dead code
JS00001 May 21, 2026
99f7a6d
Merge branch 'jsenyitko-workspace-tables' of github.com:Expensify/App…
JS00001 May 21, 2026
c8b4192
add selection to the table ref
JS00001 May 21, 2026
2fa5612
cleanup code in workspace categories list
JS00001 May 21, 2026
d0b5c9a
fix lint & types
JS00001 May 21, 2026
9b0c9ed
fix sorting
JS00001 May 21, 2026
61bfb01
Merge branch 'jsenyitko-workspace-tables' of github.com:Expensify/App…
JS00001 May 22, 2026
20df429
add searchbar
JS00001 May 22, 2026
d966f50
Merge branch 'jsenyitko-workspace-tables' of github.com:Expensify/App…
JS00001 May 22, 2026
0086b17
Merge branch 'jsenyitko-workspace-tables' of github.com:Expensify/App…
JS00001 May 22, 2026
5a3da72
fix alignment
JS00001 May 22, 2026
7cc0244
fix all types
JS00001 May 22, 2026
44e2c8d
fix tests
JS00001 May 22, 2026
7d902b9
disable line for refs
JS00001 May 22, 2026
c3ffdb8
fix PR Comments
JS00001 May 22, 2026
516ac23
fix alignment and width
JS00001 May 22, 2026
301e6c3
Merge branch 'jsenyitko-workspace-tables' of github.com:Expensify/App…
JS00001 May 25, 2026
3ff7d1b
hide approver col when no approver
JS00001 May 25, 2026
c1834d8
disable row when the item is disabled
JS00001 May 25, 2026
d449070
add domain list as tab bar page
JS00001 May 25, 2026
9593adc
fix searchbar for company card one-off
JS00001 May 25, 2026
dbaf2a1
fix selection behavior
JS00001 May 25, 2026
21e4d66
Merge branch 'jsenyitko-workspace-tables' of github.com:Expensify/App…
JS00001 May 26, 2026
825cac8
cleanup the selection logic
JS00001 May 26, 2026
5da4316
cleanup code
JS00001 May 26, 2026
caed047
Merge branch 'jsenyitko-workspace-tables' of github.com:Expensify/App…
JS00001 May 27, 2026
458d265
add selection modal on mobile
JS00001 May 27, 2026
70d1052
Merge branch 'jsenyitko-workspace-tables' of github.com:Expensify/App…
JS00001 May 28, 2026
736902e
drill down selection props
JS00001 May 28, 2026
ffa3765
add bnback subtitles & locking for rows
JS00001 May 28, 2026
f104456
Merge branch 'jsenyitko-workspace-tables' of github.com:Expensify/App…
JS00001 May 28, 2026
fb1c240
fix styling differences
JS00001 May 28, 2026
86899f2
make long pressing select hte right row
JS00001 May 29, 2026
a392480
Merge branch 'jsenyitko-workspace-tables' of github.com:Expensify/App…
JS00001 May 29, 2026
b36f3fe
add proper selection mode enabling/disabling
JS00001 May 29, 2026
1e12e5f
remove gl code col if there are no gl codes
JS00001 May 29, 2026
27a6359
fix long selection
JS00001 May 29, 2026
9cd0c20
Merge branch 'jsenyitko-workspace-tables' of github.com:Expensify/App…
JS00001 Jun 1, 2026
b1bb5e5
Merge branch 'main' of github.com:Expensify/App into jsenyitko-catego…
JS00001 Jun 2, 2026
4d2efe4
fix react compiler errors
JS00001 Jun 2, 2026
3940d65
Merge branch 'main' of github.com:Expensify/App into jsenyitko-catego…
JS00001 Jun 3, 2026
aa7c938
use tokenized searhc, accessibility, selection negation
JS00001 Jun 3, 2026
d65b7ff
fix selection mode bug
JS00001 Jun 3, 2026
146012e
Merge branch 'main' of github.com:Expensify/App into jsenyitko-catego…
JS00001 Jun 4, 2026
7b10b01
address comments
JS00001 Jun 4, 2026
a3212be
fix tooltip
JS00001 Jun 4, 2026
0ec0fb8
handle the switch for non workspace admins
JS00001 Jun 4, 2026
d99e20c
reset mobile exfy
JS00001 Jun 4, 2026
a2fe057
fix lint
JS00001 Jun 4, 2026
846dd6c
fix eslint
JS00001 Jun 4, 2026
41f2e34
fix conflicts
JS00001 Jun 4, 2026
1c37889
Merge branch 'main' of github.com:Expensify/App into jsenyitko-catego…
JS00001 Jun 8, 2026
be6184b
fix selection when all disabled, fix cursor, fix sentry label
JS00001 Jun 8, 2026
2c2d6ef
remove margin, left align
JS00001 Jun 8, 2026
94493da
Merge branches 'jsenyitko-categories-table' and 'main' of github.com:…
JS00001 Jun 8, 2026
d4067eb
fix react compliance check
JS00001 Jun 8, 2026
dbaa45d
Merge branch 'main' of github.com:Expensify/App into jsenyitko-catego…
JS00001 Jun 9, 2026
90113c7
fix comments
JS00001 Jun 9, 2026
dbfef71
fix eslint
JS00001 Jun 9, 2026
c81051d
fix type errors
JS00001 Jun 9, 2026
336070c
Merge branch 'main' of github.com:Expensify/App into jsenyitko-catego…
JS00001 Jun 9, 2026
5e2ab08
update to use can write
JS00001 Jun 9, 2026
c87ca98
fix selection
JS00001 Jun 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9108,6 +9108,7 @@ const CONST = {
MORE_DROPDOWN: 'WorkspaceMembers-MoreDropdown',
},
CATEGORIES: {
ROW: 'WorkspaceCategories-Row',
ADD_BUTTON: 'WorkspaceCategories-AddButton',
MORE_DROPDOWN: 'WorkspaceCategories-MoreDropdown',
BULK_ACTIONS_DROPDOWN: 'WorkspaceCategories-BulkActionsDropdown',
Expand Down
78 changes: 64 additions & 14 deletions src/components/Table/Table.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import type {FlashListRef} from '@shopify/flash-list';
import React, {useImperativeHandle, useRef} from 'react';
import MenuItem from '@components/MenuItem';
import Modal from '@components/Modal';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import {turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
import CONST from '@src/CONST';
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';
import type {TableHandle, TableMethods, TableProps} from './types';
import type {TableData, TableHandle, TableMethods, TableProps} from './types';

/**
* A composable table component that provides filtering, search, and sorting functionality.
Expand All @@ -32,12 +40,13 @@ import type {TableHandle, TableMethods, TableProps} from './types';
* 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
*
Expand Down Expand Up @@ -129,39 +138,51 @@ import type {TableHandle, TableMethods, TableProps} from './types';
* </Table>
* ```
*/
function Table<T, ColumnKey extends string = string, FilterKey extends string = string>({
function Table<DataType extends TableData, ColumnKey extends string = string, FilterKey extends string = string>({
ref,
title,
data = [],
columns,
filters,
data = [],
selectedKeys = [],
compareItems,
isItemInFilter,
isItemInSearch,
initialSortColumn,
children,
selectionEnabled,
onRowSelectionChange,
...listProps
}: TableProps<T, ColumnKey, FilterKey>) {
}: TableProps<DataType, ColumnKey, FilterKey>) {
const {translate} = useLocalize();
const icons = useMemoizedLazyExpensifyIcons(['CheckSquare']);
const isMobileSelectionEnabled = useMobileSelectionMode();

if (!columns || columns.length === 0) {
throw new Error('Table columns must be provided');
}

const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout();

const {middleware: filterMiddleware, currentFilters, methods: filterMethods} = useFiltering<T, FilterKey>({filters, isItemInFilter});
const {middleware: filterMiddleware, currentFilters, methods: filterMethods} = useFiltering<DataType, FilterKey>({filters, isItemInFilter});
const filteredData = filterMiddleware(data);

const {middleware: searchMiddleware, activeSearchString, methods: searchMethods} = useSearching<T>({isItemInSearch});
const {middleware: searchMiddleware, activeSearchString, methods: searchMethods} = useSearching<DataType>({isItemInSearch});
const searchedData = searchMiddleware(filteredData);

const {middleware: sortMiddleware, activeSorting, methods: sortMethods} = useSorting<T, ColumnKey>({compareItems, initialSortColumn});
const {middleware: sortMiddleware, activeSorting, methods: sortMethods} = useSorting<DataType, ColumnKey>({compareItems, initialSortColumn});
const sortedData = sortMiddleware(searchedData);

const processedData = [filterMiddleware, searchMiddleware, sortMiddleware].reduce((acc, middleware) => middleware(acc), data);
const {middleware: selectionMiddleware, methods: selectionMethods, mobileSelectionModalRowKey} = useSelection<DataType>({data: sortedData, selectedKeys, onRowSelectionChange});
const processedData = selectionMiddleware(sortedData);

const listRef = useRef<FlashListRef<T>>(null);
const listRef = useRef<FlashListRef<DataType>>(null);

const tableMethods: TableMethods<ColumnKey, FilterKey> = {
...filterMethods,
...sortMethods,
...searchMethods,
...selectionMethods,
};

/**
Expand All @@ -179,9 +200,9 @@ function Table<T, ColumnKey extends string = string, FilterKey extends string =
return () => processedData;
}

return listRef.current?.[property as keyof FlashListRef<T>];
return listRef.current?.[property as keyof FlashListRef<DataType>];
},
}) as TableHandle<T, ColumnKey, FilterKey>;
}) as TableHandle<DataType, ColumnKey, FilterKey>;
});

const originalDataLength = data?.length ?? 0;
Expand All @@ -199,8 +220,17 @@ function Table<T, ColumnKey extends string = string, FilterKey extends string =
const hasSearchString = activeSearchString.trim().length > 0;
const isEmptyResult = processedData.length === 0 && originalDataLength > 0 && (hasSearchString || hasActiveFilters);

const handleMobileSelectionPress = () => {
turnOnMobileSelectionMode();

if (mobileSelectionModalRowKey) {
tableMethods.handleSingleRowSelection(mobileSelectionModalRowKey);
tableMethods.setMobileSelectionModalRowKey(null);
}
};

// eslint-disable-next-line react/jsx-no-constructed-context-values
const contextValue: TableContextValue<T, ColumnKey, FilterKey> = {
const contextValue: TableContextValue<DataType, ColumnKey, FilterKey> = {
title,
listRef,
listProps,
Expand All @@ -216,9 +246,29 @@ function Table<T, ColumnKey extends string = string, FilterKey extends string =
hasSearchString,
isEmptyResult,
shouldUseNarrowTableLayout,
selectionEnabled,
isMobileSelectionEnabled,
};

return <TableContext.Provider value={contextValue as unknown as TableContextValue<unknown, string>}>{children}</TableContext.Provider>;
return (
<TableContext.Provider value={contextValue as unknown as TableContextValue<TableData, string, string>}>
{children}

<Modal
shouldPreventScrollOnFocus
isVisible={!!mobileSelectionModalRowKey}
type={CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED}
onClose={() => tableMethods.setMobileSelectionModalRowKey(null)}
>
<MenuItem
icon={icons.CheckSquare}
title={translate('common.select')}
onPress={handleMobileSelectionPress}
pressableTestID={CONST.SELECTION_LIST_WITH_MODAL_TEST_ID}
/>
</Modal>
</TableContext.Provider>
);
}

export default Table;
16 changes: 13 additions & 3 deletions src/components/Table/TableBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import useBottomSafeSafeAreaPaddingStyle from '@hooks/useBottomSafeSafeAreaPaddi
import useDebouncedAccessibilityAnnouncement from '@hooks/useDebouncedAccessibilityAnnouncement';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import type {TableData} from '.';
import {useTableContext} from './TableContext';

/**
Expand Down Expand Up @@ -45,10 +46,19 @@ type TableBodyProps = ViewProps & {
* </Table>
* ```
*/
function TableBody<T>({contentContainerStyle, style, ...props}: TableBodyProps) {
function TableBody<DataType extends TableData>({contentContainerStyle, style, ...props}: TableBodyProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {processedData: filteredAndSortedData, activeSearchString, listProps, listRef, shouldUseNarrowTableLayout, hasActiveFilters, hasSearchString, isEmptyResult} = useTableContext<T>();
const {
processedData: filteredAndSortedData,
activeSearchString,
listProps,
listRef,
shouldUseNarrowTableLayout,
hasActiveFilters,
hasSearchString,
isEmptyResult,
} = useTableContext<DataType>();
const {ListEmptyComponent, contentContainerStyle: listContentContainerStyle, ...restListProps} = listProps ?? {};

const tableBodyContentContainerStyle = useBottomSafeSafeAreaPaddingStyle({
Expand Down Expand Up @@ -88,7 +98,7 @@ function TableBody<T>({contentContainerStyle, style, ...props}: TableBodyProps)
style={[styles.flex1, styles.mnh0, style]}
{...props}
>
<FlashList<T>
<FlashList<DataType>
ref={listRef}
data={filteredAndSortedData}
style={[styles.flex1, styles.mnh0]}
Expand Down
27 changes: 17 additions & 10 deletions src/components/Table/TableContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +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, TableData, TableMethods, TableRow} from './types';

/**
* The shape of the Table context value.
* This context is provided by the `<Table>` 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<T, ColumnKey extends string = string, FilterKey extends string = string> = {
type TableContextValue<DataType extends TableData, ColumnKey extends string = string, FilterKey extends string = string> = {
/** The title of the table when shown on smaller screens. */
title?: string;

/** Reference to the underlying FlashList for programmatic control. */
listRef: React.RefObject<FlashListRef<T> | null>;
listRef: React.RefObject<FlashListRef<DataType> | null>;

/** FlashList props passed through from the Table component. */
listProps: SharedListProps<T>;
listProps: SharedListProps<DataType>;

/** 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: Array<TableRow<DataType>>;

/** The original length of the data array before any processing. */
originalDataLength: number;
Expand Down Expand Up @@ -54,11 +57,14 @@ type TableContextValue<T, ColumnKey extends string = string, FilterKey extends s
/** Whether the table has an empty result caused by search or filters. */
isEmptyResult: boolean;

/** Whether or not table selection is enabled on mobile */
isMobileSelectionEnabled: boolean;

/** Whether to use a narrow layout (e.g. on mobile screens). */
shouldUseNarrowTableLayout: boolean;
};

const defaultTableContextValue: TableContextValue<unknown, string> = {
const defaultTableContextValue: TableContextValue<TableData, string> = {
listRef: React.createRef(),
processedData: [],
originalDataLength: 0,
Expand All @@ -71,11 +77,12 @@ const defaultTableContextValue: TableContextValue<unknown, string> = {
activeSearchString: '',
tableMethods: {} as TableMethods<string, string>,
filterConfig: undefined,
listProps: {} as SharedListProps<unknown>,
listProps: {} as SharedListProps<TableData>,
hasActiveFilters: false,
hasSearchString: false,
isEmptyResult: false,
shouldUseNarrowTableLayout: false,
isMobileSelectionEnabled: false,
};

const TableContext = createContext(defaultTableContextValue);
Expand All @@ -97,14 +104,14 @@ const TableContext = createContext(defaultTableContextValue);
* }
* ```
*/
function useTableContext<T, ColumnKey extends string = string>() {
function useTableContext<DataType extends TableData, ColumnKey extends string = string>() {
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<T, ColumnKey>;
return context as unknown as TableContextValue<DataType, ColumnKey>;
}

export default TableContext;
Expand Down
Loading
Loading