All features

Feature breakdown · built with CraftHQ

PlatformUI

Data tables - sort, filter, pagination

Whole-team time at this build's pace

Estimated ~5 h

2 people incl. the product owner, requirements to merge

Estimate at this build's pace, not a measurement of this feature

PR review & merge~48 mintime from pull request opened to merged (includes review)

Every list is searchable, sortable, filterable and paged, with one consistent look across the product. The measured window here is one shared-style change.

Size: ~6 FP · Proxy estimate; an in-house count, not independently checked. Range 4 to 10. · How we counted 6 FP

How long a feature this size usually takes

Whole-team time — this build vs a typical team

~6 FP · FP × 6.8 to 9.4 h (ISBSG 2023)

Whole-team time at this build's pace2 people incl. the product owner, requirements to merge
~5 h3 to 8 h

Estimate at this build's pace, not a measurement of this feature

A typical team, whole lifecycleRequirements to release · middle half of 512 comparable projects (Java, Agile), ISBSG 2023
~41 to 56 habout 1–1.5 person-weeksmedian ~47 h
grey band = a typical team, middle halfhatched = our whole-team estimate

Both figures are whole-team time over the whole lifecycle, so they compare like for like. Ours is this build's pace — about 0.8 hours per function point across the whole build (two people, including the product owner, with requirements and review time). The industry range is an ISBSG project-level figure applied to one feature, so it is indicative.

The pull request time covers one change to this feature; the estimate covers building the whole feature.

Workflow

How it moved through the workflow

Select a stage. The build shipped, so every stage is complete; a person approved the merge.

Request

Give every list one consistent table style, starting with a one-height filter bar on the shared sortable header.

Questions

One decision was recorded before building: style each table on its own, or use one shared style and header component.

Approved

The product owner signs off the breakdown by moving the story into the approved state, and the build runs against that reference.

Definition of ready

The questions are answered.
The acceptance criteria are testable.
The size is estimated.
The scope is checked for splitting.
The product owner has signed off.

Building

This change shipped as one pull request. CraftHQ's build agents made the change with its tests; a failing check halts the work until it passes.

Checks

Three checks run on every pull request: one on the product owner's machine as CraftHQ builds, one on the engineer's machine before review, and a code-quality scan in CI. Merging is gated by the CI check passing and a person's approval, enforced by branch protection.

Review

An engineer reviews the pull request and merges it.

Merged

The pull request is merged to main after a person approves it. Nothing merges without that approval.

Definition of done

Every acceptance criterion is checked; where a scenario has a dedicated test it is cited, and where it was verified by review that is stated.
The three checks are green.
A person approved the merge.

Breakdown

The feature breakdown

Business first, then the engineering view. Tab through it.

Overview

The gap it closes

Long lists are hard to work with unless they can be searched, sorted, filtered and paged, and inconsistent tables across the product make it worse.

Who uses it

Staff who work through long lists of records.

User story

As a staff user, I want to sort, filter, search and page any list so that I can find a record fast in a long table.

Examples

Example 1

A user searches a long list, filters it to the rows they want, and sorts by a column. Only the matching rows are shown, in order.

Example 2

A very long feed is served through the API's keyset (cursor) pagination, loading the next page by a cursor; the document-request tracker uses this. The main list screens instead fetch a bounded, scoped, filtered set and sort and page it in the browser.

Dependencies & risks

Depends on

One shared table component: a server-side filter under the user's tenant and role scope, sorting and paging done in the browser over a bounded fetch, and keyset pagination available in the API; plus one shared sortable-header style.

Risk & mitigation

Per-table styling drifts over time. Reduced by one shared component, so a single style fix corrects every table at once.

How it works

Simplified diagrams of the parts involved, the steps of one request, and where it runs.

Architecture

People
Staff userworks through a list
Client (untrusted)
List screenfilter bar, sortable headers, pager
Shared table componentsorts and pages in the browser
Your app
List queryserver-side filter, tenant + role scope, bounded fetch
Databaseper-tenant isolation
  • 1searches, sorts, filters
  • 2uses
  • 3requests filtered rows
  • 4scoped, filtered read
Simplified.

Sequence

Browserthe staff user
App serverlist query
Databaseper-tenant isolation
  • 1send the search or sort
  • 2send the final search or sort
  • 3scoped, filtered query returning a bounded set
  • 4the bounded set of filtered rows
  • 5the rows; the browser sorts and pages them
Simplified.

Infrastructure

Client (untrusted)
Web appthe list screen
Your app
App serverfilter and paging, tenant + role scope
Databaseper-tenant isolation
  • 1list request (authenticate + tenant check)
  • 2scoped, paged query
Simplified.

Acceptance criteria

Each scenario is written as a Gherkin spec. The Engineering tab shows the test that covers each one, or notes where it was verified by review.

@platform @ui
Feature: Data tables with sort, filter and pagination
As a staff user
I want to sort, filter, search and page any list
So that I can find a record fast in a long table
Every list in the product is searchable, sortable, filterable and paged, with one consistent look.
Background:
Given a list uses the shared table component
Rule: Any list can be searched, filtered and sorted
Scenario: Sort, filter and search a list
Given a list of records
When the user searches, filters and sorts it
Then only the matching records are shown, in the chosen order
Scenario: A filter that matches nothing
Given a list of records
When the user applies a filter that matches no rows
Then an empty-result state is shown rather than the full list
Scenario: An invalid filter is rejected
Given a list of records
When the user applies a filter the server does not accept
Then the request is refused and the list is unchanged
Scenario: Sorting cannot reach an unexposed column or row
Given a list the user is authorised to see
When the user sorts it
Then sorting reorders only the rows already fetched and authorised, so it cannot reach a column or a row the user may not see
Scenario: A search on a sensitive identifier matches only its last four digits
Given a list with a sensitive numeric identifier column
When the user searches by that identifier
Then only the last four digits are matched, so a full value cannot be recovered
Rule: The main screens fetch a bounded set and page in the browser; the API offers a cursor
Scenario: A main list screen fetches a bounded set
Given a main list screen
When the list is loaded
Then a bounded, scoped and filtered set is fetched, and the browser sorts and pages them
Scenario: A very long feed uses keyset pagination in the API
Given a very long feed served through the API, such as the document-request tracker
When the next page is requested
Then it loads by a cursor with a stable tiebreak key, so rows are not duplicated or skipped at the boundary
Rule: One shared style keeps every table consistent
Scenario: One style fix reaches every sortable table
Given every sortable table uses the shared header component
When the shared header style is changed once
Then every table in the product picks up the change
@edge-cases
Scenario Outline: List behaviour at different sizes
Given a list with "<rows>" rows and a filter of "<filter>"
When the user views it
Then the result is "<result>"
Examples:
| rows | filter | result |
| none | a term | an empty-result state shown |
| few | none | all rows shown, sorted and paged in the browser |
| many | a term | matching rows, sorted and paged in the browser over a bounded, scoped fetch |
| very many | none | the API serves pages by a cursor |

Questions

Questions raised in planning, each with the decision taken.

Should each table carry its own styling, or should they share one style?

Per-table styling drifts and has to be fixed table by table. One shared style means a single fix reaches every table.

AOne shared style and a shared sortable-header component, so a single fix reaches every sortable table. · recommended
BStyle each table on its own.
Decided: One shared style; a single fix reaches every sortable table (option A).

How we sized it

A function point measures what the software does — its inputs, outputs, lookups and the data it stores — regardless of language or how many lines it took. Each counted item is weighted by complexity: a user input counts 3–6 points, an output or report 4–7, a lookup 3–6, data the feature owns 7–10, and data read from elsewhere 5–7.

  • Sort, filter, search and page any list
    Lookup
    6
  • Total
    6
6 function points (range 4–10). IFPUG-style count: each user action, lookup, output and stored data set, weighted by complexity. Technical plumbing is excluded, and a data store is counted once in the feature that owns it.

Components touched

Shared table component

One list component reused across every list: the filter runs on the server under the user's tenant and role scope, and sorting and paging run in the browser over a bounded fetch.

Bounded fetch and API keyset pagination

The main register and users screens fetch a bounded, scoped and filtered set and sort and page it in the browser. Keyset (cursor) pagination with a stable tiebreak key exists in the API and is used by the document-request tracker for very long feeds.

Shared sortable-header

One header style and component (a pager at page size 25, 50 or 100, and a sortable header with aria-sort and keyboard-operable buttons), so a single style fix corrects every table at once.

Data touched

List recordsread

Read with a server-side filter under the signed-in user's tenant and role scope, returning a bounded set the browser sorts and pages; the API also offers keyset pagination. Tenant isolation is enforced in the database (row-level security on every table, pinned per transaction); see Multi-tenant data isolation.

Security decisions

  • List reads run under the signed-in user's tenant and role scope, so a filter never reaches another organisation's rows or a row the user may not see.
  • The client sends the filter, but the server runs it under that scope, so a crafted filter cannot widen the query beyond what the user may see.
  • Sorting and paging reorder rows already fetched and authorised, so they cannot reach a column or a row the user may not see.
  • A search on a sensitive numeric identifier matches only its last four digits, so the list cannot recover a full value.

Subtasks

  • A shared table component: a server-side filter under tenant and role scope, with sorting and paging in the browser over a bounded fetch.
  • A search and filter in the browser, with client-side sort and paging.
  • Keyset (cursor) pagination in the API with a stable tiebreak key, used by the document-request tracker.
  • One shared sortable-header style and pager.

How each scenario is verified

  • The pager ranges correctly in both modes
    A pager test checks the classic mode range, empty and last-page disabling, and the keyset mode range with Next available only while the server has more.
  • The export matches the on-screen filter
    A filter-parity integration test checks a list equals its export under a filter.
  • The server-side filter is tenant-scoped
    Covered through the broader access and cross-tenant tests rather than a dedicated filter test.
  • Sort and the shared header style
    Verified by review of the client-side sort helper and the shared sortable header.

Risk tier

UI A person approves every merge. This is a UI-tier change; it is checked by review and the pager tests.

Activity

Real pull-request window for one shared-style change.

  • Pull request opened: the one-height filter bar
  • Merged to main after a person approved

Start here

Want this on your backlog?

Get a free baseline report on one repository, or scope a 3-month pilot measured against what your team’s work costs today.

Measured from your own git history
Every pull request tested before review
A person approves every merge