Repository navigation
feat(organization): sort, filter and page the org users table - #1832
dawsontoth wants to merge 1 commit into
Conversation
SimpleBrowseDataTable gains two opt-in props: `filter` (a search box driving TanStack's global filter) and `paginated` (client-side pages of 20, reusing the browse table's TablePagination, shown only once the rows outnumber one page). The org users table turns both on and makes every data column sortable, with Roles now an accessor column so it can be sorted and searched. A re-sort or a new filter returns to the first page, but new `data` keeps the current one (clamped to the last page if the list shrank): these lists poll every 10s, and TanStack's automatic reset would send the user back to page 1 whenever any record in the payload changed. The filtered and paginated row models live in a separate `studioClientTableFeatures` object rather than the shared feature set: the paginated row model defaults to a 10-row page, so registered globally it would truncate the browse, logs and every other table that did not remember `manualPagination: true`. Only the two features' APIs and state join the shared set, so every table keeps the one table type, and a SimpleBrowseDataTable that opts into neither prop bypasses both row models (`manualFiltering` / `manualPagination`). Refs #1264 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces client-side filtering and pagination to the SimpleBrowseDataTable component, along with comprehensive unit tests and updates to the organization users table to leverage these features. To support this, a new studioClientTableFeatures set is defined to handle client-side row models without impacting other tables. Feedback on these changes suggests moving TablePagination to a shared directory to maintain clean architecture dependencies, and replacing useLayoutEffect with a render-phase state adjustment to avoid SSR warnings.
|
|
||
| import { Input } from '@/components/ui/input'; | ||
| import { Table, TableBody, TableCell, TableHeader, TableHeadSortable, TableRow } from '@/components/ui/table'; | ||
| import { PAGE_SIZE_OPTIONS, TablePagination } from '@/features/instance/databases/components/TablePagination'; |
There was a problem hiding this comment.
Importing TablePagination from a feature-specific directory (@/features/instance/databases/components/) into a shared component (@/components/SimpleBrowseDataTable.tsx) violates the clean architecture principle of unidirectional dependencies. Shared components should be self-contained and not depend on feature-specific code.
Consider moving TablePagination.tsx to src/components/ and updating the import.
| import { PAGE_SIZE_OPTIONS, TablePagination } from '@/features/instance/databases/components/TablePagination'; | |
| import { PAGE_SIZE_OPTIONS, TablePagination } from '@/components/TablePagination'; |
| import { ColumnDef, Row, studioClientTableFeatures } from '@/lib/table'; | ||
| import { flexRender, PaginationState, RowData, SortingState, useTable } from '@tanstack/react-table'; | ||
| import { SearchIcon } from 'lucide-react'; | ||
| import React, { useLayoutEffect, useState } from 'react'; |
| useLayoutEffect(() => { | ||
| if (pagination.pageIndex > lastPageIndex) { | ||
| setPagination((current) => ({ ...current, pageIndex: lastPageIndex })); | ||
| } | ||
| }, [pagination.pageIndex, lastPageIndex]); |
There was a problem hiding this comment.
Using useLayoutEffect in a client component ('use client') will trigger SSR warnings during pre-rendering in frameworks like Next.js (e.g., "useLayoutEffect does nothing on the server...").
Instead of an effect, we can safely adjust the state during the render phase. When React detects a state update during render, it immediately aborts the current render and restarts it with the updated state, avoiding both SSR warnings and layout flashes.
if (pagination.pageIndex > lastPageIndex) {
setPagination((current) => ({ ...current, pageIndex: lastPageIndex }));
}
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
⊙ Problem
The organization Users page shows every member in one table with no way to sort it (every column sets
enableSorting: false), search it, or page through it (#1264). The table is the sharedSimpleBrowseDataTable, which backs ten small, fully-loaded lists; #1268 (instance users) and #1269 (org roles) ask for the same three things next.💡 Solution
SimpleBrowseDataTablegains two opt-in props, and the org users table turns both on:filter={{ label, placeholder }}adds a search box. It drives TanStack's global filter: case-insensitive substring match across every string or number column, except a column that setsenableGlobalFilter: false.paginatedpages the rows 20 at a time with the browse table's existingTablePagination. The pager only appears once the rows outnumber the smallest page (20), so a short list looks as it does today.datakeeps the current page, clamped to the last page if the list shrank. TanStack's default resets to page 1 on every newdataarray, and this list polls every 10s (getOrganizationRolesQueryOptions) while central manager rewrites a member'slastAccessedAton everyGET /User/current(central-managersrc/resources/user/User.js:149), so the default would keep bouncing an admin off page 2.On the org users table every data column is now sortable. Roles becomes an accessor column (the same alphabetical join as before) so it can be sorted and searched, and User Id is left out of the search since an opaque id matches many short queries. The placeholder says what is searched: "Filter by email, name, role or status".
A table that opts into neither prop renders exactly what it did: no search box, no pager, and
manualFiltering/manualPaginationkeep both new row models from running.⚖️ Alternatives
studioTableFeaturesand opt out per table (themanualSorting: truepattern AGENTS.md describes): rejected. Unlike sorting, the paginated row model is never a no-op (its default state is a 10-row page), so it would truncate the browse table (20–250 row server pages), the logs table,DataTableand the seven non-optingSimpleBrowseDataTablelists unless every one of them rememberedmanualPagination: true.better-alternative-exists: also bypass the filtered row model withmanualFiltering: !filterfor non-opting tables (adopted). Its claim thatfilterFns: { includesString }must be registered for the filter to work was overruled: the'auto'global filter resolvesfilterFn_includesStringdirectly (table-coreglobal-filtering/globalFilteringFeature.utils.js,table_getGlobalAutoFilterFn), and the tests filter through the real row model with no registry.ClustersListfilters on every keystroke the same way; the bundle cost of importingTablePagination(codex, then withdrawn by both legs in round 2), since it is already in the eager entry chunk throughsrc/features/instance/routes.ts→ databases routes →TableView.🔧 Changes
src/lib/table.ts:globalFilteringFeatureandrowPaginationFeaturejoin the shared feature set (APIs and state only, imported here), and the newstudioClientTableFeaturesadds their row models for tables that page and filter their own data.src/components/SimpleBrowseDataTable.tsx: thefilterandpaginatedprops; sorting, filter and page state held in React state so a sort or filter change can return to page 1 whileautoResetPageIndex: falsekeeps a refresh from doing so; a layout effect that clamps the page when the rows shrink; the search box; and the pager.src/features/instance/databases/components/TablePagination.tsx: exportsPAGE_SIZE_OPTIONS, so the shared table's page size and pager threshold come from the pager's own smallest option.src/features/organization/users/constants/tableDefinition.tsx: sortable columns, Roles as an accessor column, User Id excluded from the search.src/features/organization/users/index.tsx: opts the table intofilterandpaginated.✅ Verification
End-to-end route: component integration in jsdom with TanStack's real row models, the real
TablePaginationand the real org users column definitions. Not browser-verified, since the only authenticated dev origin (port 5173) was shared with other sessions. The authed Playwright spec (e2e/tests/org-users.authed.spec.ts) still asserts the Email/Roles/Status headers, which remain, but it was not run (no test account here) and it does not exercise the new controls.src/components/SimpleBrowseDataTable.test.tsx: a table that does not opt in renders all 45 rows with no search box or pager; case-insensitive filtering that skips anenableGlobalFilter: falsecolumn and shows "No results."; 20-row pages; no pager at exactly 20 rows; sort-then-page ordering; re-sort and re-filter return to page 1 and count only matching rows (no console errors); a refresh keeps the page; shrinking data moves to the new last page.src/features/organization/users/constants/tableDefinition.test.tsx: filtering by name, role and status but not id; sorting by Roles; the roles text is alphabetical without reordering the user's ownrolesarray.manualPagination: !paginated; forcingmanualFiltering: true; re-enabling the User Id column in the search;autoResetPageIndex: true; removing the page reset from sort, and from filter; disabling the clamp.tsc -bexit 0;oxlintexit 0;dprint checkexit 0; commitlint passed.?? []for sorting, whyautoResetPageIndexis off, why the tests await TanStack's microtask).Stacked follow-ups: #1835 (instance users, #1268) is based on this branch, and #1837 (org roles, #1269) on that one.
Closes #1264
🤖 Generated with Claude Code; posted via @dawsontoth.
Related PRs: #1793 independent (merged; it removed SimpleBrowseDataTable's unused, never-read pagination props, and this adds paging the component holds itself)
Complexity: medium
Review-Coverage: authored=claude; ran=codex,gemini; blocked=cursor-composer(no-receipt),domain(auth),cursor-muse(no-receipt); declined=cursor-grok,cursor-kimi; rounds=2; full=1 @ 28146a1
Review-Attention: read ~5m (raised: degraded review) @ 28146a1