-
Notifications
You must be signed in to change notification settings - Fork 3.9k
[No QA] [BYOC] [Bulk Card Assignments] Release 1: Generic Table component #77788
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
tgolen
merged 56 commits into
Expensify:byoc-bulk-card-assign-r1
from
margelo:byoc-bulk-card-assign-r1-generic-table-component
Dec 18, 2025
Merged
Changes from all commits
Commits
Show all changes
56 commits
Select commit
Hold shift + click to select a range
818736f
feat: create initial generic table component
chrispader ca9fa29
fix: remove merge conflicts for initial table
chrispader 73ed5ec
fix: types for compound components
chrispader f3165ff
refactor: fix eslint/ts errors and restructure exports
chrispader 5b40adf
refactor: improve generic table component
chrispader 8b2e1d4
feat: implement sorting and `TableHeader` component
chrispader 7f4d04d
feat: migrate `WorkspaceCompanyCardList` to generic table
chrispader 1eb24c2
feat: Add search bar, filter buttons, and responsive header layout to…
ikevin127 b3a27ee
refactor: fixed single-select label
ikevin127 f682cb9
refactor: use FlashList
chrispader c9fa96a
fix: update keyExtractor callback
chrispader efb34f9
fix: row gap of 8px
chrispader 44bcefd
fix: remove extra padding in search bar
chrispader afb9c7a
fix: remove unnecessary variable alias
chrispader d4cfbfa
refactor: remove extra callbacks
chrispader 4a0dd3d
fix: typo
chrispader a054cab
fix: TS error `no-lonely-if`, prefer early return
chrispader 6619b18
fix: clean up and simplify
chrispader 1242e33
feat: add imperative functions for getting current sorting/filtering/…
chrispader 94adeb5
feat: preserve widescreen sorting
chrispader 822fe5b
fix: add a ref to prevent recurrent state updates
chrispader a47ce9a
fix: toggle sort order only once
chrispader 5f2f483
fix: update sorting when we switch layout
chrispader 784f86f
feat: hide header in narrow layout
chrispader 6d134e0
fix: imperative functions
chrispader 8502a20
refactor: re-structure FilterButtons implementation
chrispader 1172c35
fix: remove `displayName` and add `ViewProps`
chrispader 40dc504
fix: responsive layout in company cards
chrispader 081efe9
refactor: update `useTableContext` imports
chrispader 96ace13
fix: remove unused TableHeaderContainer
chrispader 50a8dc8
fix: remove TableHeaderContainer import
chrispader 7ac42f5
refactor: remove unused Table.SortButtons
chrispader da32c86
fix: Table test errors and refactor
chrispader fab329f
fix: Expensicons error
chrispader 71909c7
fix: remove manual memo
chrispader 4ce427c
fix: dependency cycle
chrispader fe1a2a3
fix: Expensicons
chrispader f52716a
Merge branch 'byoc-bulk-card-assign-r1' into byoc-bulk-card-assign-r1…
chrispader 2477218
fix: rename list components
chrispader 171ddad
fix: `WorkspaceCompanyCardsTableHeaderButtons` import
chrispader 7f6185c
Update WorkspaceMemberDetailsPage.tsx
chrispader 7d03cc8
Update WorkspaceMemberDetailsPage.tsx
chrispader 5bbe140
refactor: inline methods
chrispader 438b5ba
refactor: Make Table.FilterButtons generic and refactor header buttons
chrispader 9bee8ac
fix: filter buttons in header bar
chrispader 3bc2932
feat: allow searching for "unassigned"/"assigned"
chrispader 76a8a90
refactor: add `contentContainerStyle` prop to `TableBody` and remove …
chrispader 0c06ccc
refactor: improve sorting
chrispader a747738
refactor: extract middlewares
chrispader 98b2b88
feature: added documentation and fixed comments
ikevin127 b28a3ba
fix: types
chrispader 37ab5ef
temp: rename files for git
chrispader 1fa4e18
temp: rename files back
chrispader dc98628
docs: add more comments
chrispader 62f7946
Merge branch 'byoc-bulk-card-assign-r1' into byoc-bulk-card-assign-r1…
chrispader 8ceeb1d
Merge branch 'byoc-bulk-card-assign-r1' into byoc-bulk-card-assign-r1…
chrispader File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,249 @@ | ||
| # Table Component | ||
|
|
||
| A composable, generic table component with built-in filtering, search, and sorting capabilities. | ||
|
|
||
| ## Quick Start | ||
|
|
||
| ```tsx | ||
| import Table from '@components/Table'; | ||
| import type { TableColumn, CompareItemsCallback } from '@components/Table'; | ||
|
|
||
| type Item = { id: string; name: string; status: string }; | ||
| type ColumnKey = 'name' | 'status'; | ||
|
|
||
| const columns: Array<TableColumn<ColumnKey>> = [ | ||
| { key: 'name', label: 'Name' }, | ||
| { key: 'status', label: 'Status' }, | ||
| ]; | ||
|
|
||
| function MyTable() { | ||
| return ( | ||
| <Table<Item, ColumnKey> | ||
| data={items} | ||
| columns={columns} | ||
| renderItem={({ item }) => <ItemRow item={item} />} | ||
| keyExtractor={(item) => item.id} | ||
| > | ||
| <Table.Header /> | ||
| <Table.Body /> | ||
| </Table> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| ## Compositional Pattern | ||
|
|
||
| The Table uses a **compound component pattern** where the parent `<Table>` manages all state and child components render specific UI parts: | ||
|
|
||
| | Component | Purpose | | ||
| |-----------|---------| | ||
| | `<Table>` | Parent container that manages state and provides context | | ||
| | `<Table.Header>` | Renders sortable column headers | | ||
| | `<Table.Body>` | Renders data rows using FlashList | | ||
| | `<Table.SearchBar>` | Search input that filters data | | ||
| | `<Table.FilterButtons>` | Dropdown filter buttons | | ||
|
|
||
| ### Flexible Composition | ||
|
|
||
| You only include the components you need: | ||
|
|
||
| ```tsx | ||
| // Minimal: just data rows | ||
| <Table data={items} columns={columns} renderItem={renderItem}> | ||
| <Table.Body /> | ||
| </Table> | ||
|
|
||
| // With search | ||
| <Table data={items} columns={columns} renderItem={renderItem} isItemInSearch={searchFn}> | ||
| <Table.SearchBar /> | ||
| <Table.Body /> | ||
| </Table> | ||
|
|
||
| // Full featured | ||
| <Table | ||
| data={items} | ||
| columns={columns} | ||
| renderItem={renderItem} | ||
| isItemInSearch={searchFn} | ||
| isItemInFilter={filterFn} | ||
| compareItems={compareFn} | ||
| filters={filterConfig} | ||
| > | ||
| <Table.SearchBar /> | ||
| <Table.FilterButtons /> | ||
| <Table.Header /> | ||
| <Table.Body /> | ||
| </Table> | ||
| ``` | ||
|
|
||
| ## Features | ||
|
|
||
| ### Sorting | ||
|
|
||
| Enable by providing `compareItems`: | ||
|
|
||
| ```tsx | ||
| const compareItems: CompareItemsCallback<Item, ColumnKey> = (a, b, { columnKey, order }) => { | ||
| const multiplier = order === 'asc' ? 1 : -1; | ||
| return a[columnKey].localeCompare(b[columnKey]) * multiplier; | ||
| }; | ||
|
|
||
| <Table | ||
| data={items} | ||
| columns={columns} | ||
| renderItem={renderItem} | ||
| compareItems={compareItems} | ||
| > | ||
| <Table.Header /> {/* Clicking headers toggles sort */} | ||
| <Table.Body /> | ||
| </Table> | ||
| ``` | ||
|
|
||
| Header click behavior: `ascending → descending → reset` | ||
|
|
||
| ### Searching | ||
|
|
||
| Enable by providing `isItemInSearch`: | ||
|
|
||
| ```tsx | ||
| const isItemInSearch = (item: Item, searchString: string) => | ||
| item.name.toLowerCase().includes(searchString.toLowerCase()); | ||
|
|
||
| <Table | ||
| data={items} | ||
| columns={columns} | ||
| renderItem={renderItem} | ||
| isItemInSearch={isItemInSearch} | ||
| > | ||
| <Table.SearchBar /> | ||
| <Table.Body /> | ||
| </Table> | ||
| ``` | ||
|
|
||
| ### Filtering | ||
|
|
||
| Enable by providing `filters` config and `isItemInFilter`: | ||
|
|
||
| ```tsx | ||
| import type { FilterConfig, IsItemInFilterCallback } from '@components/Table'; | ||
|
|
||
| const filterConfig: FilterConfig = { | ||
| status: { | ||
| filterType: 'single-select', // or 'multi-select' | ||
| options: [ | ||
| { label: 'All', value: 'all' }, | ||
| { label: 'Active', value: 'active' }, | ||
| { label: 'Inactive', value: 'inactive' }, | ||
| ], | ||
| default: 'all', | ||
| }, | ||
| }; | ||
|
|
||
| const isItemInFilter: IsItemInFilterCallback<Item> = (item, filterValues) => { | ||
| if (filterValues.includes('all')) return true; | ||
| return filterValues.includes(item.status); | ||
| }; | ||
|
|
||
| <Table | ||
| data={items} | ||
| columns={columns} | ||
| renderItem={renderItem} | ||
| filters={filterConfig} | ||
| isItemInFilter={isItemInFilter} | ||
| > | ||
| <Table.FilterButtons /> | ||
| <Table.Body /> | ||
| </Table> | ||
| ``` | ||
|
|
||
| ## Programmatic Control | ||
|
|
||
| Access table methods via ref: | ||
|
|
||
| ```tsx | ||
| import type { TableHandle } from '@components/Table'; | ||
|
|
||
| const tableRef = useRef<TableHandle<Item, ColumnKey>>(null); | ||
|
|
||
| // Update sorting programmatically | ||
| tableRef.current?.updateSorting({ columnKey: 'name', order: 'desc' }); | ||
|
|
||
| // Update search | ||
| tableRef.current?.updateSearchString('query'); | ||
|
|
||
| // Get current state | ||
| const sorting = tableRef.current?.getActiveSorting(); | ||
| const searchString = tableRef.current?.getActiveSearchString(); | ||
|
|
||
| // FlashList methods also available | ||
| tableRef.current?.scrollToIndex({ index: 0 }); | ||
|
|
||
| <Table ref={tableRef} {...props}> | ||
| <Table.Body /> | ||
| </Table> | ||
| ``` | ||
|
|
||
| ## Type Parameters | ||
|
|
||
| | Parameter | Description | | ||
| |-----------|-------------| | ||
| | `T` | Type of items in the data array | | ||
| | `ColumnKey` | String literal union of column keys (e.g., `'name' \| 'status'`) | | ||
| | `FilterKey` | String literal union of filter keys | | ||
|
|
||
| ## Architecture | ||
|
|
||
| ### Middleware Pipeline | ||
|
|
||
| Data processing flows through three middlewares: | ||
|
|
||
| ``` | ||
| data → [Filtering] → [Searching] → [Sorting] → processedData | ||
| ``` | ||
|
|
||
| Each middleware transforms the data array. The order is fixed: filters first, then search, then sort. | ||
|
|
||
| ### Context | ||
|
|
||
| All sub-components access shared state via `TableContext`. You can create custom sub-components using `useTableContext`: | ||
|
|
||
| ```tsx | ||
| import { useTableContext } from '@components/Table/TableContext'; | ||
|
|
||
| function CustomComponent<T>() { | ||
| const { processedData, activeSorting, updateSorting } = useTableContext<T>(); | ||
| // Build custom UI using context data... | ||
| } | ||
| ``` | ||
|
|
||
| ## Column Configuration | ||
|
|
||
| ```tsx | ||
| type TableColumn<ColumnKey> = { | ||
| key: ColumnKey; // Unique identifier | ||
| label: string; // Display text | ||
| styling?: { | ||
| flex?: number; // Column width ratio | ||
| containerStyles?: StyleProp<ViewStyle>; | ||
| labelStyles?: StyleProp<TextStyle>; | ||
| }; | ||
| }; | ||
| ``` | ||
|
|
||
| ## Props Reference | ||
|
|
||
| ### Table Props | ||
|
|
||
| | Prop | Type | Required | Description | | ||
| |------|------|----------|-------------| | ||
| | `data` | `T[]` | Yes | Array of items to display | | ||
| | `columns` | `TableColumn<ColumnKey>[]` | Yes | Column configuration | | ||
| | `renderItem` | FlashList's `renderItem` | Yes | Row renderer | | ||
| | `keyExtractor` | FlashList's `keyExtractor` | Yes | Unique key generator | | ||
| | `compareItems` | `CompareItemsCallback<T, ColumnKey>` | No | Sorting comparator | | ||
| | `isItemInSearch` | `IsItemInSearchCallback<T>` | No | Search predicate | | ||
| | `isItemInFilter` | `IsItemInFilterCallback<T>` | No | Filter predicate | | ||
| | `filters` | `FilterConfig<FilterKey>` | No | Filter dropdown config | | ||
| | `ref` | `Ref<TableHandle<T, ColumnKey, FilterKey>>` | No | Ref for programmatic control | | ||
|
|
||
| Plus all FlashList props except `data`. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very lovely, thank you!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @ikevin127 ! 🙌🏼❤️