Feature breakdown · built with CraftHQ
Data tables - sort, filter, pagination
Whole-team time at this build's pace
2 people incl. the product owner, requirements to merge
Estimate at this build's pace, not a measurement of this feature
Every list is searchable, sortable, filterable and paged, with one consistent look across the product. The measured window here is one shared-style change.
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)
Estimate at this build's pace, not a measurement of this feature
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
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
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
- 1searches, sorts, filters
- 2uses
- 3requests filtered rows
- 4scoped, filtered read
Sequence
- 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
Infrastructure
- 1list request (authenticate + tenant check)
- 2scoped, paged query
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 @uiFeature: Data tables with sort, filter and paginationAs a staff userI want to sort, filter, search and page any listSo that I can find a record fast in a long tableEvery list in the product is searchable, sortable, filterable and paged, with one consistent look.Background:Given a list uses the shared table componentRule: Any list can be searched, filtered and sortedScenario: Sort, filter and search a listGiven a list of recordsWhen the user searches, filters and sorts itThen only the matching records are shown, in the chosen orderScenario: A filter that matches nothingGiven a list of recordsWhen the user applies a filter that matches no rowsThen an empty-result state is shown rather than the full listScenario: An invalid filter is rejectedGiven a list of recordsWhen the user applies a filter the server does not acceptThen the request is refused and the list is unchangedScenario: Sorting cannot reach an unexposed column or rowGiven a list the user is authorised to seeWhen the user sorts itThen sorting reorders only the rows already fetched and authorised, so it cannot reach a column or a row the user may not seeScenario: A search on a sensitive identifier matches only its last four digitsGiven a list with a sensitive numeric identifier columnWhen the user searches by that identifierThen only the last four digits are matched, so a full value cannot be recoveredRule: The main screens fetch a bounded set and page in the browser; the API offers a cursorScenario: A main list screen fetches a bounded setGiven a main list screenWhen the list is loadedThen a bounded, scoped and filtered set is fetched, and the browser sorts and pages themScenario: A very long feed uses keyset pagination in the APIGiven a very long feed served through the API, such as the document-request trackerWhen the next page is requestedThen it loads by a cursor with a stable tiebreak key, so rows are not duplicated or skipped at the boundaryRule: One shared style keeps every table consistentScenario: One style fix reaches every sortable tableGiven every sortable table uses the shared header componentWhen the shared header style is changed onceThen every table in the product picks up the change@edge-casesScenario Outline: List behaviour at different sizesGiven a list with "<rows>" rows and a filter of "<filter>"When the user views itThen 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.
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 listLookup6
- Total6
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 modesA 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 filterA filter-parity integration test checks a list equals its export under a filter.
- The server-side filter is tenant-scopedCovered through the broader access and cross-tenant tests rather than a dedicated filter test.
- Sort and the shared header styleVerified 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.