Data Listing Patterns (Canonical)¶
This is the canonical rule for rendering, searching, and paging record collections in the Open Learning Hub monorepo.
Use this rule for admin and back-office list pages, and for any user-facing surface where data is displayed in structured rows.
Core Contract¶
- Use a semantic table for tabular records. Do not use an unordered list for row/column datasets.
- Prefer a single search input (
search) as the default filter UX. - Apply filtering server-side, not client-side, for authoritative result sets.
- Keep search state in URL query params so links are shareable and navigation state is preserved.
- Use the shared pagination pattern with
AdminPaginationControls. - Use
DEFAULT_PAGE_SIZEas the canonical page-size constant. - Preserve active query params when building pagination links.
- Normalize invalid
pagevalues and clamp out-of-range pages to valid bounds. - Keep stable
data-testidhooks for container, search input, table, rows, empty state, and pagination controls. Naming and exemptions: 059-data-testid-policy.mdc.
Search and Filtering Standard¶
- Use one
searchparam for generic search wherever multi-field filtering is not explicitly required by product. - When
searchis present, apply a server-sideORcondition across the intended searchable columns for the page. - Keep per-page field scope explicit in code comments or nearby docs (for example: actor/action/target/tenant for audit-style tables).
- Trim query input before use; ignore empty strings.
- Search input UI conventions (placeholder,
aria-label, clear-button behavior) are canonical in058-search-input-ux-policy.mdc.
Pagination Standard¶
- Parse
pagefrom URL query params. - Compute
limitfromDEFAULT_PAGE_SIZE. - Derive
offset = (normalizedPage - 1) * limitafter normalization. - Compute
totalandtotalPagesfrom a count query that applies the same filters as the data query. - Render shared pagination with preserved query state.
Rendering and Accessibility Standard¶
- Render column headers (
<thead>,<th>) with clear labels. - Keep row semantics in
<tbody>. - Show a user-safe empty state when no rows match the active query.
- Keep keyboard and screen-reader friendly controls (labeled search input, submit button, and pagination labels).
Adoption Guidance¶
- New list pages must follow this rule by default.
- Existing pages should be migrated opportunistically when touched for related work.
- Any exception (for example, intentionally multi-field filter UIs) must be documented in the owning app rule and justified by product requirements.
Reference Implementations¶
apps/lms/src/app/\[locale\]/admin/users/page.tsxapps/lms/src/app/\[locale\]/admin/tenants/page.tsx+apps/lms/src/app/\[locale\]/admin/tenants/_components/tenants-manager.tsxapps/lms/src/app/\[locale\]/admin/courses/page.tsxapps/lms/src/app/\[locale\]/admin/audit/page.tsxapps/lms/src/app/\[locale\]/admin/page.tsx(Recent activity section)
CMS Behavior-Equivalent Allowance¶
- In
apps/cms, behavior-equivalent primitives are allowed when they preserve the same contract:PaginationControlsmay be used instead ofAdminPaginationControls.- The page still must keep URL-driven query state, server-side filtering, canonical
DEFAULT_PAGE_SIZE, and normalized/clampedpagehandling.
Component-Scoped Exceptions¶
- Some embedded surfaces (dialogs, sidebars, and transient popovers) cannot reliably own URL query state.
- For those surfaces, document the exception in the owning feature and still enforce:
- server-side filtering and pagination for authoritative results,
- semantic table rendering when the dataset is tabular,
- stable
data-testidhooks for search, rows, empty state, and pagination controls.