List Organizations Block
ListOrganizations is an organization management table built on BaseTable, with optional tabs, search chips, row actions, and pagination.
Installation
- npm
- yarn
- pnpm
- bun
npm install @nodeblocks/frontend-list-organization-block
yarn add @nodeblocks/frontend-list-organization-block
pnpm add @nodeblocks/frontend-list-organization-block
bun add @nodeblocks/frontend-list-organization-block
What You Need
| Item | Why it matters |
|---|---|
labels | Copy for header cells, status text, row actions, search field, and empty state |
data | Table rows (ListOrganizationsRowData[]) |
listOrganizationsTitle | Header title |
searchValue + search chips + handlers (optional) | Controlled search draft, chip list, and submit/delete flow |
tabs + currentTab + onTabChange (optional) | Optional workflow/status filtering UI |
statusMatch (optional) | Maps raw row status values to active/inactive text in the status column |
pagination (optional) | Controlled page state |
| row action + navigation handlers (optional) | labels.rowActions + resolveRowAction + action handlers, plus row click navigation |
ListOrganizations does not own table state. Keep tab selection, search draft/chips, and pagination in your app, then pass handlers (onTabChange, onSearchFieldChange, onSearchSubmit, pagination.onPageChange) to update state.
Rows use the ListOrganizationsRowData shape (id, createdAt, name, joinDate, status). By default, the generated status column treats raw values 'active' and 'inactive' as active/inactive labels; pass statusMatch when your API uses different status values. statusMatch does not filter tabs or choose row actions; keep those checks aligned with your raw row.status values.
Code Examples
- Quick Start
- Labels and URLs
- Compound Components
- Block Override
function Example() { const allOrganizationData = Array.from({length: 20}, (_, i) => ({ id: String(i + 1), createdAt: new Date(2024, 0, i + 1).toISOString(), name: `Organization ${i + 1}`, joinDate: new Date(2024, 0, i + 5).toISOString(), status: i % 2 === 0 ? 'active' : 'inactive', })); const tabs = [ {key: 'all', label: 'All Organizations'}, {key: 'active', label: 'Active'}, {key: 'inactive', label: 'Inactive'}, ]; const labels = { emptyStateMessage: 'No organizations found', searchFieldPlaceholder: 'Search organizations...', rowActions: { activate: 'Activate Organization', deactivate: 'Deactivate Organization', }, headerRow: { createdAt: 'Created At', name: 'Organization Name', joinDate: 'Join Date', status: 'Status', }, cellData: { statusActive: 'Active', statusInactive: 'Inactive', }, }; const [currentTab, setCurrentTab] = React.useState('all'); const [searchValue, setSearchValue] = React.useState(''); const [currentPage, setCurrentPage] = React.useState(1); const [lastAction, setLastAction] = React.useState('Search, activation, and navigation feedback will appear here.'); const itemsPerPage = 5; const filteredByTab = allOrganizationData.filter(row => { if (currentTab === 'active') return row.status === 'active'; if (currentTab === 'inactive') return row.status === 'inactive'; return true; }); const filtered = searchValue.trim() ? filteredByTab.filter(row => { const keyword = searchValue.toLowerCase(); return row.name.toLowerCase().includes(keyword) || row.status.toLowerCase().includes(keyword); }) : filteredByTab; const totalPages = Math.max(1, Math.ceil(filtered.length / itemsPerPage)); const start = (currentPage - 1) * itemsPerPage; const data = filtered.slice(start, start + itemsPerPage); return ( <> <ListOrganizations listOrganizationsTitle="Organizations Management" labels={labels} data={data} tabs={tabs} currentTab={currentTab} onTabChange={tab => { setCurrentTab(tab); setCurrentPage(1); }} searchValue={searchValue} onSearchFieldChange={setSearchValue} onSearchSubmit={() => setLastAction(`Search submitted: ${searchValue}`)} statusMatch={{active: 'active', inactive: 'inactive'}} resolveRowAction={row => (row.status === 'active' ? ['deactivate'] : ['activate'])} onItemActivate={rowId => setLastAction(`Activate organization: ${rowId}`)} onItemDeactivate={rowId => setLastAction(`Deactivate organization: ${rowId}`)} pagination={{ currentPage, totalPages, onPageChange: setCurrentPage, }} rowHref={row => `/organizations/${row.id}`} onNavigate={to => setLastAction(`Navigate: ${to}`)} /> <div style={{marginTop: 12, color: '#475569', fontSize: 13}}>{lastAction}</div> </> ); }
Customize labels, status matching, search chips, and row navigation behavior.
function Example() { const labels = { emptyStateMessage: 'No matching organizations', searchFieldPlaceholder: 'Search by name or status', rowActions: { activate: 'Enable', deactivate: 'Disable', }, headerRow: { createdAt: 'Created', name: 'Name', joinDate: 'Joined', status: 'Current status', }, cellData: { statusActive: 'Enabled', statusInactive: 'Disabled', }, }; const data = [ { id: 'org-101', createdAt: new Date(2024, 3, 10).toISOString(), name: 'Basal Docs', joinDate: new Date(2024, 3, 12).toISOString(), status: 'enabled', }, ]; const chips = [{key: 'chip-1', label: 'Basal'}]; return ( <ListOrganizations listOrganizationsTitle="Organizations" labels={labels} data={data} searchValue="" onSearchFieldChange={() => {}} onSearchSubmit={() => {}} searchChipsTitle="Search Keywords" searchChips={chips} onSearchChipDelete={() => {}} statusMatch={{active: 'enabled', inactive: 'disabled'}} resolveRowAction={row => (row.status === 'enabled' ? ['deactivate'] : ['activate'])} onItemActivate={() => {}} onItemDeactivate={() => {}} rowHref={row => `/admin/organizations/${row.id}`} onNavigate={() => {}} /> ); }
Pass searchChipsTitle, searchChips, and onSearchChipDelete to render ListOrganizations.SearchChips between header and tabs. Omit chips when there are no active keywords.
Compose header/tabs/search chips/content/pagination explicitly.
function Example() { const allOrganizationData = Array.from({length: 18}, (_, i) => ({ id: String(i + 1), createdAt: new Date(2024, 0, i + 1).toISOString(), name: `Organization ${i + 1}`, joinDate: new Date(2024, 0, i + 5).toISOString(), status: i % 2 === 0 ? 'active' : 'inactive', })); const labels = { emptyStateMessage: 'No organizations found', searchFieldPlaceholder: 'Search organizations...', rowActions: { activate: 'Activate Organization', deactivate: 'Deactivate Organization', }, headerRow: { createdAt: 'Created At', name: 'Organization Name', joinDate: 'Join Date', status: 'Status', }, cellData: { statusActive: 'Active', statusInactive: 'Inactive', }, }; const tabs = [ {key: 'all', label: 'All Organizations'}, {key: 'active', label: 'Active'}, {key: 'inactive', label: 'Inactive'}, ]; const [currentTab, setCurrentTab] = React.useState('all'); const [currentPage, setCurrentPage] = React.useState(1); const [searchValue, setSearchValue] = React.useState(''); const [isLoading, setIsLoading] = React.useState(false); const [searchChips, setSearchChips] = React.useState([]); const [lastAction, setLastAction] = React.useState('Inline status will update when you submit search or trigger row actions.'); const itemsPerPage = 5; const filteredByTab = allOrganizationData.filter(row => { if (currentTab === 'active') return row.status === 'active'; if (currentTab === 'inactive') return row.status === 'inactive'; return true; }); const keywords = searchChips.map(chip => String(chip.label).toLowerCase()); const filtered = keywords.length ? filteredByTab.filter(row => keywords.some( keyword => row.name.toLowerCase().includes(keyword) || row.status.toLowerCase().includes(keyword), ), ) : filteredByTab; const totalPages = Math.max(1, Math.ceil(filtered.length / itemsPerPage)); const pageStart = (currentPage - 1) * itemsPerPage; const data = filtered.slice(pageStart, pageStart + itemsPerPage); return ( <ListOrganizations listOrganizationsTitle="Organizations Management" labels={labels} data={data} tabs={tabs} currentTab={currentTab} onTabChange={tab => { setCurrentTab(tab); setCurrentPage(1); }} > <ListOrganizations.Header> <ListOrganizations.Title listOrganizationsTitle="Organizations Management" /> <ListOrganizations.Action labels={labels} searchValue={searchValue} onSearchFieldChange={setSearchValue} onSearchSubmit={() => { const trimmed = searchValue.trim(); if (trimmed && !searchChips.some(chip => chip.label === trimmed)) { setSearchChips(prev => [...prev, {key: `search-${Date.now()}`, label: trimmed}]); } setSearchValue(''); }} /> </ListOrganizations.Header> {searchChips.length > 0 ? ( <ListOrganizations.SearchChips searchChipsTitle="Search Keywords" searchChips={searchChips} onSearchChipDelete={chip => setSearchChips(prev => prev.filter(item => item.key !== chip.key))} /> ) : null} <ListOrganizations.Tabs /> <ListOrganizations.Content> {isLoading ? ( <ListOrganizations.Loader /> ) : ( <ListOrganizations.Table labels={labels} data={data} statusMatch={{active: 'active', inactive: 'inactive'}} resolveRowAction={row => (row.status === 'active' ? ['deactivate'] : ['activate'])} onItemActivate={rowId => setLastAction(`Activate: ${rowId}`)} onItemDeactivate={rowId => setLastAction(`Deactivate: ${rowId}`)} rowHref={row => `/organizations/${row.id}`} onNavigate={to => setLastAction(`Navigate: ${to}`)} /> )} </ListOrganizations.Content> <ListOrganizations.Pagination pagination={{ currentPage, totalPages, onPageChange: setCurrentPage, }} data={data} /> <div style={{marginTop: 12, color: '#475569', fontSize: 13}}>{lastAction}</div> </ListOrganizations> ); }
Use function children to prepend custom blocks and control order.
function Example() { const labels = { emptyStateMessage: 'No organizations found', searchFieldPlaceholder: 'Search organizations...', rowActions: { activate: 'Activate Organization', deactivate: 'Deactivate Organization', }, headerRow: { createdAt: 'Created At', name: 'Organization Name', joinDate: 'Join Date', status: 'Status', }, cellData: { statusActive: 'Active', statusInactive: 'Inactive', }, }; const data = [ { id: '1', createdAt: new Date(2024, 0, 1).toISOString(), name: 'Organization 1', joinDate: new Date(2024, 0, 5).toISOString(), status: 'active', }, ]; return ( <ListOrganizations listOrganizationsTitle="Organizations Management" labels={labels} data={data} tabs={[{key: 'all', label: 'All Organizations'}]} currentTab="all" onTabChange={() => {}} searchValue="" onSearchFieldChange={() => {}} onSearchSubmit={() => {}} resolveRowAction={row => (row.status === 'active' ? ['deactivate'] : ['activate'])} onItemActivate={() => {}} onItemDeactivate={() => {}} > {({defaultBlocks, defaultBlockOrder}) => ({ blocks: { ...defaultBlocks, customNotification: ( <div style={{ marginBottom: 12, padding: 12, background: '#eef4ff', border: '1px solid #cddcff', borderRadius: 8, fontSize: 14, }} > Custom notification: showing {data.length} organizations </div> ), }, blockOrder: ['customNotification', ...defaultBlockOrder], })} </ListOrganizations> ); }
When to use block overrides
Use overrides when you need custom banners or status panels before the table layout while keeping default list behavior. defaultBlockOrder is header, searchChips, tabs, content, pagination, and defaultBlocks also includes title, action, header, loader, table, searchChips, tabs, content, and pagination.
Important Props
Core Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
labels | { emptyStateMessage: string; searchFieldPlaceholder: string; rowActions?: { activate: string; deactivate: string }; headerRow: { createdAt: string; name: string; joinDate: string; status: string }; cellData: { statusActive: string; statusInactive: string } } | Yes | - | UI copy for search, table cells, row actions, and empty state |
data | ListOrganizationsRowData[] | Yes | - | Table rows |
listOrganizationsTitle | ReactNode | Yes | - | Header title |
isLoading | boolean | No | undefined | Loading state for content/pagination |
searchValue | string | No | undefined | Controlled search input value |
onSearchFieldChange | (value: string) => void | No | undefined | Search input change handler |
onSearchSubmit | () => void | No | undefined | Triggered by search icon click or Enter key |
searchChipsTitle | ReactNode | No | undefined | Label above active search chips |
searchChips | BaseTableSearchChip[] | No | undefined | Active search chips |
onSearchChipDelete | (chip: BaseTableSearchChip, index: number, event: SyntheticEvent) => void | No | undefined | Remove-chip handler |
tabs | { key: string; label: string; isDisabled?: boolean; subtitle?: string }[] | No | undefined | Optional tab definitions |
currentTab | string | No | undefined | Active tab key |
onTabChange | (tab: string) => void | No | undefined | Tab change handler |
statusMatch | { active: string; inactive: string } | No | { active: 'active', inactive: 'inactive' } | Maps raw row.status values to active/inactive text in the generated status column |
pagination | { currentPage: number; totalPages: number; onPageChange: (page: number) => void; className?: string } | No | undefined | Page controls (pages are counted from 1) |
rowHref | (row: ListOrganizationsRowData) => string | No | undefined | Build row link; requires onNavigate |
onNavigate | (to: string) => void | No | undefined | Called when row click resolves rowHref |
shouldShowDropdownMenu | (row: ListOrganizationsRowData) => boolean | No | undefined | Hide/show row menu per row |
resolveRowAction | (row: ListOrganizationsRowData) => ('activate' | 'deactivate')[] | undefined | No | undefined | Row action types to render |
onItemActivate | (rowId: string) => void | No | undefined | Handler for activate action |
onItemDeactivate | (rowId: string) => void | No | undefined | Handler for deactivate action |
ListOrganizationsRowData shape:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
id | string | Yes | - | Unique row id |
createdAt | string | Yes | - | ISO datetime string used in created-at column |
name | string | Yes | - | Organization name |
joinDate | string | Yes | - | ISO datetime string used in join-date column |
status | string | Yes | - | Raw status value; statusMatch only controls status-column display, while tabs and row actions use your own comparisons |
Content Props
labels keys:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
labels.emptyStateMessage | string | Yes | - | Empty table message |
labels.searchFieldPlaceholder | string | Yes | - | Search input placeholder |
labels.rowActions.activate | string | No | undefined | Activate action label |
labels.rowActions.deactivate | string | No | undefined | Deactivate action label |
labels.headerRow.createdAt | string | Yes | - | Created-at header |
labels.headerRow.name | string | Yes | - | Name header |
labels.headerRow.joinDate | string | Yes | - | Join-date header |
labels.headerRow.status | string | Yes | - | Status header |
labels.cellData.statusActive | string | Yes | - | Cell label for active rows |
labels.cellData.statusInactive | string | Yes | - | Cell label for inactive rows |
Subcomponents:
| Component | Prop | Type | Required | Default | Description |
|---|---|---|---|---|---|
Title | listOrganizationsTitle | ReactNode | No | Root listOrganizationsTitle via BaseTable headerTitle | Title text (children wins) |
Title | children | ReactNode | No | Root title | Custom title markup |
Action | labels | { emptyStateMessage: string; searchFieldPlaceholder: string; rowActions?: { activate: string; deactivate: string }; headerRow: { createdAt: string; name: string; joinDate: string; status: string }; cellData: { statusActive: string; statusInactive: string } } | No | Root labels | Search input copy |
Action | searchValue | string | No | Root searchValue | Controlled search input |
Action | onSearchFieldChange | (value: string) => void | No | Root handler | Search input change |
Action | onSearchSubmit | () => void | No | Root handler | Search submit |
Action | children | ReactNode | No | Built search controls | Custom action content |
SearchChips | searchChipsTitle | ReactNode | No | Root searchChipsTitle | Chips group title |
SearchChips | searchChips | BaseTableSearchChip[] | No | Root searchChips | Chips data |
SearchChips | onSearchChipDelete | (chip: BaseTableSearchChip, index: number, event: SyntheticEvent) => void | No | Root handler | Delete-chip callback |
Tabs | value | string | No | Root currentTab | Override active tab value |
Tabs | onChange | TabsProps['onChange'] | No | Root onTabChange | MUI tab change callback |
Tabs | tabProps | MUI Tab props excluding label, value, and disabled | No | undefined | Props forwarded to each MUI tab |
Table | labels | same shape as root labels | No | Root labels | Generates columns/no-rows overlay/row action labels |
Table | statusMatch | { active: string; inactive: string } | No | { active: 'active', inactive: 'inactive' } | Status-column display mapping for raw row.status values |
Table | columns | BaseTableColumn<ListOrganizationsRowData>[] | No | Generated from labels + statusMatch | Override grid columns |
Table | rowActions | (row: ListOrganizationsRowData) => BaseTableRowAction<ListOrganizationsRowData>[] | No | Generated from row-action props | Override row actions |
Table | actionColumn | BaseTableActionColumn | No | { pin: 'right' } when row actions exist | Action column config |
Table | rowMenu | ReactNode or function | No | Default menu | Custom row menu UI |
Table | onRowClick | (row: ListOrganizationsRowData) => void | No | Derived from rowHref + onNavigate | Row click callback |
Loader | children | ReactNode | No | Default loader | Loading content |
Pagination | pagination | { currentPage: number; totalPages: number; onPageChange: (page: number) => void; className?: string } | No | Root pagination | Pagination controls |
Pagination | data | ListOrganizationsRowData[] | No | Root data | Row count source |
Pagination | isLoading | boolean | No | Root isLoading | Hides pagination while loading or when there are no rows |
Title, Action, Header, Loader, SearchChips, Tabs, Content, Table, and Pagination are ListOrganizations.Title, etc. ListOrganizations.Tabs reads the tab definitions from the root tabs prop.
Layout and Composition Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
children | BlocksOverride | ReactNode | No | undefined | Compound JSX children or function override returning blocks and blockOrder |
className | string | No | undefined | Class on root container (nbb-list-organizations-container) |
sx | SxProps | No | undefined | MUI system styles for root |
ListOrganizations renders a BaseTable root with headerTitle, search actions, optional tabs, rows, row actions, no-rows overlay, and optional pagination. It inherits StackProps passthrough (except children). defaultBlockOrder is header, searchChips, tabs, content, pagination.
Default UI Blocks
| Block | Built on | Notes |
|---|---|---|
ListOrganizations (root) | BaseTable | Root table wrapper |
ListOrganizations.Title | BaseTable.Header.Title | Title from listOrganizationsTitle |
ListOrganizations.Action | BaseTable.Header.Actions + TextField | Search field with search icon submit |
ListOrganizations.Header | BaseTable.Header | Wraps title/actions |
ListOrganizations.SearchChips | BaseTable.SearchChips | Active keyword chips block |
ListOrganizations.Tabs | BaseTable.Tabs | Optional tabs from tabs |
ListOrganizations.Loader | BaseTable.Content.Loader | Loading state |
ListOrganizations.Content | BaseTable.Content | Content wrapper |
ListOrganizations.Table | BaseTable.Content.Grid | Data grid with generated columns/actions |
ListOrganizations.Pagination | BaseTable.Pagination | Page controls |
| no-rows overlay icon | PersonOutlined | Used when data is empty |
TypeScript
import {
ListOrganizations,
ListOrganizationsRowData,
BaseTableSearchChip,
} from '@nodeblocks/frontend-list-organization-block';
const rows: ListOrganizationsRowData[] = [
{
id: '1',
createdAt: new Date().toISOString(),
name: 'Organization 1',
joinDate: new Date().toISOString(),
status: 'active',
},
];
const chips: BaseTableSearchChip[] = [{key: 'keyword-1', label: 'Org 1'}];
<ListOrganizations
listOrganizationsTitle="Organizations Management"
labels={{
emptyStateMessage: 'No organizations found',
searchFieldPlaceholder: 'Search organizations...',
rowActions: {activate: 'Activate Organization', deactivate: 'Deactivate Organization'},
headerRow: {
createdAt: 'Created At',
name: 'Organization Name',
joinDate: 'Join Date',
status: 'Status',
},
cellData: {statusActive: 'Active', statusInactive: 'Inactive'},
}}
data={rows}
searchChipsTitle="Search Keywords"
searchChips={chips}
statusMatch={{active: 'active', inactive: 'inactive'}}
/>;