Skip to content

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_SIZE as the canonical page-size constant.
  • Preserve active query params when building pagination links.
  • Normalize invalid page values and clamp out-of-range pages to valid bounds.
  • Keep stable data-testid hooks 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 search param for generic search wherever multi-field filtering is not explicitly required by product.
  • When search is present, apply a server-side OR condition 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 in 058-search-input-ux-policy.mdc.

Pagination Standard

  • Parse page from URL query params.
  • Compute limit from DEFAULT_PAGE_SIZE.
  • Derive offset = (normalizedPage - 1) * limit after normalization.
  • Compute total and totalPages from 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.tsx
  • apps/lms/src/app/\[locale\]/admin/tenants/page.tsx + apps/lms/src/app/\[locale\]/admin/tenants/_components/tenants-manager.tsx
  • apps/lms/src/app/\[locale\]/admin/courses/page.tsx
  • apps/lms/src/app/\[locale\]/admin/audit/page.tsx
  • apps/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:
    • PaginationControls may be used instead of AdminPaginationControls.
    • The page still must keep URL-driven query state, server-side filtering, canonical DEFAULT_PAGE_SIZE, and normalized/clamped page handling.

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-testid hooks for search, rows, empty state, and pagination controls.