Patterns
List Screen
The management/list page recipe. Every list page stacks the same way: breadcrumb → action bar → search bar → table. Only the buttons, filters and columns differ.
Source in the MFEs
reference-fe pages/index.tsxPolicyManagement.vuerate-fe Template/index.tsx
List Management Screen
ScreenA full list page: breadcrumb → action bar → search bar → table, ready to adapt.
Install to
src/pages/<Entity>/Requiresantd@ant-design/iconsapp-breadcrumbapp-action-barapp-search-barAnatomy
A list screen is always these four bands, in this order:
- 1Breadcrumb — AppBreadcrumb — where the user is; hidden on mobile (the header shows the page name instead).
- 2Action bar — AppActionBar — Create / Refresh / Delete, wrapped in two house dividers.
- 3Search bar — AppSearchBar — keyword Input + active filter tags + Add-filter dropdown.
- 4Table — AntD Table — row selection, a status badge column, a right-fixed action dropdown, and pagination with showTotal.
Live example
A working reference-data list. Select rows to enable Delete; the status column uses the badge classes.
Preview
code: USD
| Code | Name | Status | Updated | ||
|---|---|---|---|---|---|
| CURRENCY.USD | US Dollar | ACTIVE | 2026-06-30 | ||
| CURRENCY.VND | Vietnamese Dong | ACTIVE | 2026-06-28 | ||
| UNIT.KG | Kilogram | DRAFT | 2026-06-21 | ||
| UNIT.LEGACY | Deprecated unit | REJECTED | 2026-05-11 |
- 1-4 of 4 items
- 1
// The fixed stack — breadcrumb → action bar → search bar → table.
// AppActionBar and AppSearchBar own the dividers/spacing; only the contents change.
<Row className="w-100">
<AppBreadcrumb items={TEMPLATE_BREADCRUMB_ITEMS.management} translate={t} />
<AppActionBar>
<Button size="small" onClick={handleCreate} disabled={!canCreate}>
<i className="fa-solid fa-plus" /> {t('actions.create')}
</Button>
<Button size="small" onClick={handleRefresh}>
<i className="fa-solid fa-sync" /> {t('actions.refresh')}
</Button>
<Button size="small" onClick={handleDeleteSelected} disabled={selectedRowKeys.length === 0}>
<i className="fa-regular fa-trash-can" /> {t('actions.delete')}
</Button>
</AppActionBar>
<AppSearchBar>
<Input placeholder={t('placeholder.code')} prefix={<SearchOutlined />}
style={{ minWidth: 280, maxWidth: 520 }} allowClear />
{/* active filter tags + Add-filter dropdown live here */}
</AppSearchBar>
<Table
rowKey="id"
columns={columns}
dataSource={rows}
loading={loading}
rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys }}
onRow={(record) => ({ onDoubleClick: () => canEdit && navigate(`/edit/${record.id}`) })}
pagination={{
current: page, pageSize, total,
showSizeChanger: true,
showTotal: (total, range) => `${range[0]}-${range[1]} of ${total} items`,
pageSizeOptions: ['10', '20', '50', '100'],
}}
/>
</Row>Composition
The screen is assembled from the shared pieces documented elsewhere in this kit — the Action bar and Search bar patterns, and the Badge component. Two columns carry most of the house conventions:
Preview
ACTIVEDRAFTREJECTED
// Status column → the badge component (Components → Badge)
{
title: t('label.status'),
dataIndex: 'status',
width: 120,
render: (status: string) => (
<span className={`${getStatusClass(status)} badge-rvn`}>{status}</span>
),
}
// Action column → ONLY a dropdown, fixed right, ~44px wide
{
key: 'action', title: '', width: 44, fixed: 'right',
render: (_t, row) => (
<Dropdown trigger={['click']} menu={{ items: rowMenu(row) }}>
<Button type="text" size="small"><MoreOutlined style={{ fontSize: 18 }} /></Button>
</Dropdown>
),
}Rules
Don't double the dividers
AppActionBar already renders a divider above and below its button row. Don’t add another
Divider right before or after it.Action column = dropdown only
The right-fixed action column holds a single
Dropdown + MoreOutlined, ~44px wide. Never scatter inline edit/delete icons in the cell.Double-click to open
Rows navigate to the edit view on double-click via
onRow (React) / customRow (Vue) — after checking the edit permission.