List Products Table Block
ListProductsTable is a comprehensive tabular listing block built on MUI Table primitives. It supports pinned columns, tabs, dismissible search chip filters, row actions, and deep layout composition through compound child blocks or function-based block overrides.
Installation
- npm
- yarn
- pnpm
- bun
npm install @nodeblocks/frontend-list-products-table-block
yarn add @nodeblocks/frontend-list-products-table-block
pnpm add @nodeblocks/frontend-list-products-table-block
bun add @nodeblocks/frontend-list-products-table-block
What You Need
| Item | Why it matters |
|---|---|
listProductsTableTitle | Title or React Node for the table header |
createHref | The destination link/hash for the "Create Product" action button |
data | Array of row data matching ListProductsTableRowData |
labels | Full labels object mapping table headers, placeholders, action buttons, and empty/fallback states |
ListProductsTable is presentational and does not manage internal state for filters, search keywords, tab selection, or pagination. Tab changes only update the active tab state; if you want the rows to change, filter the data you pass into the table yourself or by query new list and update that data in response to callbacks such as onTabChange and onPageChange.
Code Examples
- Quick Start
- Labels and URLs
- Compound Components
- Block Override
function Example() { const [currentTab, setCurrentTab] = React.useState('published'); const [page, setPage] = React.useState(1); const [lastAction, setLastAction] = React.useState('Navigation and row actions will appear here.'); const products = [ { id: '1', tab: 'draft', category: 'Furniture', title: 'Premium Office Chair', publication: { status: 'PUBLISHED', since: '2026-01-12', until: '2026-02-12' }, createdBy: 'Marty', createdAt: '2026-01-10T09:21:00Z', updatedBy: 'Alice', updatedAt: '2026-01-11T10:30:00Z', favoritesCount: 14, ordersCount: 22, }, { id: '2', tab: 'published', category: 'Electronics', title: 'Wireless Headphones', publication: { status: 'PUBLISHED', since: '2026-01-03', until: '2026-03-03' }, createdBy: 'Ken', createdAt: '2026-01-02T09:21:00Z', updatedBy: 'Marty', updatedAt: '2026-01-05T10:30:00Z', favoritesCount: 34, ordersCount: 13, } ]; const visibleProducts = products.filter((product) => product.tab === currentTab); const tabs = [ { key: 'draft', label: 'Draft' }, { key: 'published', label: 'Published' }, { key: 'archived', label: 'Archived' }, ]; const labels = { emptyStateMessage: 'No products found', searchFieldPlaceholder: 'Search products', actions: { createProduct: 'Create Product', createProductShort: 'Create', filter: 'Filter', }, headerRow: { productTitle: 'Product Title', publicationPeriod: 'Publication Period', created: 'Created', updated: 'Updated', favorites: 'Favorites', orders: 'Orders', }, rowActions: { edit: 'Edit Product', archive: 'Archive Product', restore: 'Restore Product', }, cellData: { sinceUnset: 'Not set', untilUnset: 'Not set', dateUnset: 'No publication period', }, }; return ( <div style={{ minHeight: 450 }}> <ListProductsTable listProductsTableTitle="Product Management" createHref="#create" labels={labels} data={visibleProducts} tabs={tabs} currentTab={currentTab} onTabChange={setCurrentTab} onNavigate={(to) => setLastAction(`Navigating to: ${to}`)} pagination={{ currentPage: page, totalPages: 1, onPageChange: setPage, }} /> <div style={{marginTop: 12, color: '#475569', fontSize: 13}}>{lastAction}</div> </div> ); }
Define search states, keyword chips, active callbacks, row dropdown actions, and navigation targets for rows and order histories.
function Example() { const [currentTab, setCurrentTab] = React.useState('published'); const [searchValue, setSearchValue] = React.useState(''); const [chips, setChips] = React.useState([ { key: 'filter-premium', label: 'Premium' } ]); const [lastAction, setLastAction] = React.useState('Search, filters, and row actions will appear here.'); const products = [ { id: '1', tab: 'published', category: 'Furniture', title: 'Premium Ergonomic Desk', publication: { status: 'PUBLISHED', since: '2026-01-12' }, createdBy: 'Sarah', createdAt: '2026-01-10T09:21:00Z', updatedBy: 'Sarah', updatedAt: '2026-01-11T10:30:00Z', favoritesCount: 42, ordersCount: 8, } ]; const visibleProducts = products.filter((product) => product.tab === currentTab); const labels = { emptyStateMessage: 'Empty Catalog', searchFieldPlaceholder: 'Quick lookup...', actions: { createProduct: 'Add New Product', createProductShort: 'Add', filter: 'Apply Filter', }, headerRow: { productTitle: 'Catalog Item', publicationPeriod: 'Available Dates', created: 'First Saved', updated: 'Last Modified', favorites: 'Bookmarks', orders: 'Units Sold', }, rowActions: { edit: 'Modify Details', archive: 'Move to Archive', restore: 'Unarchive Item', }, cellData: { sinceUnset: 'TBD', untilUnset: 'No End Date', dateUnset: 'Availability not set', }, }; const handleChipDelete = (chipToDelete) => { setChips(prev => prev.filter(c => c.key !== chipToDelete.key)); }; const handleSearchSubmit = () => { if (searchValue.trim() && !chips.some(c => c.label === searchValue.trim())) { setChips(prev => [...prev, { key: `search-${Date.now()}`, label: searchValue.trim() }]); } setSearchValue(''); }; return ( <div style={{ minHeight: 450 }}> <ListProductsTable listProductsTableTitle="Global Catalog" createHref="#new-product" labels={labels} data={visibleProducts} tabs={[ { key: 'published', label: 'Active Catalog', subtitle: ' (1)' }, { key: 'archived', label: 'Archived' } ]} currentTab={currentTab} onTabChange={setCurrentTab} // Search & Filters searchValue={searchValue} onSearchFieldChange={setSearchValue} onSearchSubmit={handleSearchSubmit} onFilterButtonClick={() => setLastAction('Filter drawer opened')} searchChipsTitle="Active Keywords" searchChips={chips} onSearchChipDelete={handleChipDelete} // Navigation & URLs onNavigate={(to) => setLastAction(`Routing to: ${to}`)} rowHref={(row) => `#catalog/${row.id}`} ordersHref={(row) => `#catalog/${row.id}/orders`} // Action Handlers & Resolver onProductEdit={(id, title) => setLastAction(`Editing item ${id}: ${title}`)} onProductArchive={(id, title) => setLastAction(`Archived item: ${title}`)} onProductUnarchive={(id) => setLastAction(`Restored item ID: ${id}`)} resolveRowAction={(row) => ['edit', 'archive']} shouldShowDropdownMenu={() => true} /> <div style={{marginTop: 12, color: '#475569', fontSize: 13}}>{lastAction}</div> </div> ); }
- Use
rowHrefto supply the link target for each row click. Clicking a row automatically triggersonNavigatewith the generated URL. - Use
ordersHrefto define the anchor target inside theordersCountcolumn, allowing users to drill directly into custom order details. - Provide a
resolveRowActionfunction returning an array of'edit' \| 'archive' \| 'restore'values, orundefined, to selectively control which options appear inside the row actions menu dropdown.
Assemble the layout sections explicitly. Pass configuration properties directly to <ListProductsTable.Title> and other layout segments.
function Example() { const [currentTab, setCurrentTab] = React.useState('published'); const [lastAction, setLastAction] = React.useState('Navigation feedback will appear here.'); const products = [ { id: '1', category: 'Electronics', title: 'Studio Monitor Speakers', publication: { status: 'PUBLISHED', since: '2026-03-01' }, createdBy: 'Alex', createdAt: '2026-02-28T09:00:00Z', updatedBy: 'Alex', updatedAt: '2026-03-01T12:00:00Z', favoritesCount: 8, ordersCount: 4, } ]; const labels = { emptyStateMessage: 'Empty listing', searchFieldPlaceholder: 'Search...', actions: { createProduct: 'New', filter: 'Filters' }, headerRow: { productTitle: 'Item', publicationPeriod: 'Period', created: 'Created', updated: 'Updated', favorites: 'Favorites', orders: 'Orders', }, cellData: { sinceUnset: 'N/A', untilUnset: 'N/A', dateUnset: 'None' }, }; return ( <div style={{ minHeight: 450 }}> <ListProductsTable listProductsTableTitle="Custom Listing" createHref="#new" labels={labels} data={products} tabs={[{ key: 'published', label: 'Published' }]} currentTab={currentTab} onTabChange={setCurrentTab} onNavigate={(to) => setLastAction(`Route: ${to}`)} > <ListProductsTable.Header sx={{ padding: '16px 24px', backgroundColor: '#fbfcfe', borderRadius: '8px 8px 0 0', borderBottom: '1px solid #e0e0e0', }} > <ListProductsTable.Title listProductsTableTitle="Production Catalog" /> <ListProductsTable.Action /> </ListProductsTable.Header> <ListProductsTable.Tabs /> <ListProductsTable.Content> <ListProductsTable.Table /> </ListProductsTable.Content> <ListProductsTable.Pagination /> </ListProductsTable> <div style={{marginTop: 12, color: '#475569', fontSize: 13}}>{lastAction}</div> </div> ); }
Reorder blocks or inject warning and information panels between the header, tabs, or table using the function children override pattern.
function Example() { const [currentTab, setCurrentTab] = React.useState('published'); const products = [ { id: '1', category: 'Furniture', title: 'Handcrafted Oak Console', publication: { status: 'PUBLISHED', since: '2026-01-12' }, createdBy: 'Marty', createdAt: '2026-01-10T09:21:00Z', updatedBy: 'Alice', updatedAt: '2026-01-11T10:30:00Z', favoritesCount: 14, ordersCount: 22, } ]; const labels = { emptyStateMessage: 'No products', searchFieldPlaceholder: 'Search...', actions: { createProduct: 'Create', filter: 'Filter' }, headerRow: { productTitle: 'Title', publicationPeriod: 'Period', created: 'Created', updated: 'Updated', favorites: 'Likes', orders: 'Orders', }, cellData: { sinceUnset: '-', untilUnset: '-', dateUnset: 'No schedule' }, }; return ( <div style={{ minHeight: 500 }}> <ListProductsTable listProductsTableTitle="Custom Blocks Table" createHref="#new" labels={labels} data={products} tabs={[{ key: 'published', label: 'Published' }]} currentTab={currentTab} onTabChange={setCurrentTab} onNavigate={() => {}} > {({ defaultBlocks, defaultBlockOrder }) => ({ blocks: { ...defaultBlocks, systemAlert: ( <div style={{ background: '#fff2e8', border: '1px solid #ffbb96', borderRadius: '6px', padding: '10px 16px', fontSize: '13px', color: '#d4380d', }} > Scheduled system maintenance tonight at 02:00 UTC. Publication scheduling might be delayed. </div> ), }, blockOrder: ['systemAlert', 'header', 'tabs', 'content', 'pagination'], })} </ListProductsTable> </div> ); }
When to use block overrides
Use overrides to insert global status boards, filter panels, summary counters, or banners. The standard block order is: ['header', 'searchChips', 'tabs', 'content', 'pagination'] (defaultBlockOrder).
Important Props
Core Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
data | ListProductsTableRowData[] | Yes | - | Array of product data objects to display |
isLoading | boolean | No | undefined | Switches table layout to loader mode when true |
onNavigate | (to: string) => void | No | undefined | Navigation callback triggered on click actions (such as row clicks or create buttons) |
onProductEdit | (productId: string, title: string) => void | No | undefined | Callback triggered when the "Edit" row menu action is clicked |
onProductArchive | (productId: string, title: string) => void | No | undefined | Callback triggered when the "Archive" row menu action is clicked |
onProductUnarchive | (productId: string) => void | No | undefined | Callback triggered when the "Restore" row menu action is clicked |
resolveRowAction | (row: ListProductsTableRowData) => ('edit' | 'archive' | 'restore')[] | undefined | No | undefined | Evaluation callback mapping a row to active menu actions |
shouldShowDropdownMenu | (row: ListProductsTableRowData) => boolean | No | undefined | Conditional check to display or hide the dropdown row menu button entirely |
pagination | PaginationProps | No | undefined | Stateful pagination controls: { currentPage, totalPages, onPageChange, className? } |
Content Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
listProductsTableTitle | ReactNode | Yes | - | Title text or node rendered inside the Title sub-block |
createHref | string | Yes | - | Href passed to onNavigate callback on "Create Product" action click |
labels | { emptyStateMessage: string; searchFieldPlaceholder: string; actions: { createProduct: string; createProductShort?: string; filter: string }; headerRow: { productTitle: string; publicationPeriod: string; created: string; updated: string; favorites: string; orders: string }; rowActions?: { edit: string; archive: string; restore: string }; cellData: { sinceUnset: string; untilUnset: string; dateUnset: string } } | Yes | - | Direct map containing UI text configurations |
rowHref | (row: ListProductsTableRowData) => string | No | undefined | Custom row link generator callback |
ordersHref | (row: ListProductsTableRowData) => string | No | undefined | Direct link generator callback applied to the orders count |
tabs | { key: string; label: string; isDisabled?: boolean; subtitle?: string }[] | No | undefined | Product filtering tab definitions |
currentTab | string | No | undefined | Key of the currently active/selected tab |
onTabChange | (tab: string) => void | No | undefined | Switch callback triggered when another tab is clicked |
searchValue | string | No | undefined | Active search input value |
onSearchFieldChange | (value: string) => void | No | undefined | Callback triggered when typing inside the search input field |
onSearchSubmit | () => void | No | undefined | Triggered when pressing Enter inside search input or clicking search button |
onFilterButtonClick | () => void | No | undefined | Callback triggered when clicking the filter button next to search |
searchChipsTitle | ReactNode | No | undefined | Label displayed next to search chip container |
searchChips | BaseTableSearchChip[] | No | undefined | Active filters rendered as chips: { key, label } |
onSearchChipDelete | (chip: BaseTableSearchChip, index: number, event: SyntheticEvent) => void | No | undefined | Callback triggered when dismissing an active filter chip |
labels Structure
Provide exact translations and overrides inside the labels property matching this structure:
{
emptyStateMessage: string;
searchFieldPlaceholder: string;
actions: {
createProduct: string;
createProductShort?: string;
filter: string;
};
headerRow: {
productTitle: string;
publicationPeriod: string;
created: string;
updated: string;
favorites: string;
orders: string;
};
rowActions?: {
edit: string;
archive: string;
restore: string;
};
cellData: {
sinceUnset: string;
untilUnset: string;
dateUnset: string;
};
}
Layout and Composition Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
children | BlocksOverride | ReactNode | No | undefined | Compound child elements or override function child |
className | string | No | undefined | Custom styling class applied to the root element |
sx | SxProps | No | undefined | MUI SX styling overrides passed to the outer Stack |
ListProductsTable inherits all StackProps (except children). Default block order is ['header', 'searchChips', 'tabs', 'content', 'pagination'] (defaultBlockOrder).
Default UI Blocks
| Block | Built on | Notes |
|---|---|---|
ListProductsTable (root) | BaseTable | Main tabular wrapper layout wrapping full table context |
ListProductsTable.Header | BaseTable.Header | Flex container holding Title and Actions |
ListProductsTable.Title | BaseTable.Header.Title | Renders the section header |
ListProductsTable.Action | BaseTable.Header.Actions | Flex container holding search input, filters, and create actions |
ListProductsTable.SearchChips | BaseTable.SearchChips | Renders active search filters as dismissible chips |
ListProductsTable.Tabs | BaseTable.Tabs | Responsive tabs used for table filtering |
ListProductsTable.Content | BaseTable.Content | Outer block wrapper for tables and loading spinners |
ListProductsTable.Loader | BaseTable.Content.Loader | Spinner displayed during state transitions |
ListProductsTable.Table | BaseTable.Content.Grid | Primary table grid presenting product metadata and row actions |
ListProductsTable.Pagination | BaseTable.Pagination | Controls footer pagination controls |
TypeScript
import { ListProductsTable } from '@nodeblocks/frontend-list-products-table-block';
import type { ListProductsTableRowData } from '@nodeblocks/frontend-list-products-table-block';
const products: ListProductsTableRowData[] = [
{
id: 'prod-001',
category: 'Appliances',
title: 'Smart Smart Oven Pro',
publication: { status: 'PUBLISHED', since: '2026-05-15T00:00:00Z' },
createdBy: 'System admin',
createdAt: '2026-05-10T09:21:00Z',
updatedBy: 'System admin',
updatedAt: '2026-05-11T10:30:00Z',
favoritesCount: 15,
ordersCount: 3,
}
];
const tabs = [
{ key: 'published', label: 'Published Items' }
];
const labels = {
emptyStateMessage: 'No items',
searchFieldPlaceholder: 'Search catalog',
actions: { createProduct: 'New', filter: 'Filter' },
headerRow: {
productTitle: 'Product',
publicationPeriod: 'Period',
created: 'Created',
updated: 'Updated',
favorites: 'Favorites',
orders: 'Orders',
},
cellData: { sinceUnset: '-', untilUnset: '-', dateUnset: 'No schedule' },
};
<ListProductsTable
listProductsTableTitle="App Dashboard"
createHref="#new"
labels={labels}
data={products}
tabs={tabs}
currentTab="published"
/>;