Repository navigation
Tables tab: columns collapse by the list's own width, stacked rows (#2555) - #2577
Merged
Merged
Conversation
The list now fits its width without sideways scrolling. Container queries
on `.dash` measure the list, not the screen, so the profile sidebar's
share does not matter: Topics (and Created, once it exists) drop first,
then Review and Datasets, and on the narrowest lists each row stacks into
two lines (title and ⋯, then Status, Publishable and Modified). The filter
bar folds everything but Search behind "Filters (n)" where it does not fit
on one line; the toggle only flips aria-expanded and the CSS shows the
panel from it, so a fold left open by a narrow window hides nothing on a
wide one.
A stacked row has no column headers, so the region carries a "Sort by"
select: ListPage.sort_options, every sort in both directions with the
words each Sort declares for them ("Status: drafts first"). The module
rewrites its request like a filter's, keeping every filter and returning
to page 1. ListPage.folded_count counts the filters behind the toggle
(every one but free text) for the region's data-folded.
The thresholds are measured, not the spec's provisional 1,360 / 1,100 /
900: in headless Chrome on the real row partial, worst of the first six
pages of accounts with 130 and 2,068 Tables, sidebar shown, in DejaVu
Sans (the widest fallback of the system font stack; Segoe UI needs about
7 % less). This slice's row needs 980 / 863 / 651 px; with #2560's
sortable Publishable header and #2561's ⋯ menu, both built and open, it
needs 1,030 / 913 / 701, so the thresholds are 1,050 / 930 / 720 and
nothing scrolls whichever lands first. The bar needs 958 on one line and
folds below 960. So: every column at 1920, without Topics at 1440, also
without Review and Datasets at 1280, stacked at 1024 and below, bar
folded at 1280 and below. With stand-ins for the columns still to come
(☐, Modified, Created) the row needs 1,294 / 1,047 / 835; a slice adding
one re-measures with benchmarks/tables_tab/widths.mjs. Organization chips
are capped at 11rem with an ellipsis (full name in the title), so the
Access column is bounded whatever an Organization is called.
Server cost per row, measured with benchmarks/tables_tab (the factsheet
benchmark's style, throwaway database): 0.8-1.2 ms with 6 KB of metadata
per Table, 1.4-2.2 ms with 60 KB, 5.8-7.5 ms with 500 KB, against 6-10 ms
per old card. Rendering is 0.34-0.47 ms per row at any size; what grows is
the oemetadata the live Publish gate decodes. Still 7 queries per request.
Closes #2555
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
jh-RLI
added a commit
that referenced
this pull request
Oct 2, 2026
Brings in #2560 (stored Publish gate, PR #2576) and #2555 (collapse by the list's own width, PR #2577). Conflicts, all additive, both sides kept: - tables_tab.css: #2555's container queries after this branch's menu, dialog and toast rules, so the stacked layout still overrides the sticky ⋯ cell. - tables_tab.js: both sets of new constants; the header's bullet list joined into one. - tables_region.html: both halves of the docstring; #2555's data-folded beside this branch's hx-get/hx-trigger. - changelog: #2555 and #2560 first, then #2561. Follow-up the merge required, beyond the conflicts: - PUBLISH_GATE moved to dataedit.publish_gate, so the action service imports publish_checks from there instead of from login.tables_tab. - The preflight now reads the stored verdict, as the spec asks: a stored pass is not validated again (publishing still validates live, and a Table failing there refuses the whole request, now naming that Table only); a stored fail or no verdict yet runs the checks live, which names the failed check and agrees with the Publishable cell, where live wins. Two tests pin both cases. Full suite green bar the known LightImportTest artefact of the isolated settings (green on its own); vitest 69 green. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2555. Slice 3 of spec #2551.
The tables tab now fits its width from a wide screen to a phone without sideways scrolling. Columns collapse by the list's own width, using container queries on
.dash. As the list narrows, Topics drops first (and Created, once it exists), then Review and Datasets. On the narrowest lists each row stacks into two lines: title and ⋯, then Status, Publishable and Modified. Where the filter bar does not fit on one line, everything but Search folds behind "Filters (n)". Stacked rows have no column headers, so a "Sort by" select takes their place.What changed
login/listing.py(generic, knows nothing about Tables):Sortgainsascending/descendingwords.ListPage.sort_optionslists every sort in both directions, with the current one selected.ListPage.folded_countcounts the active filters behind the toggle (every one but free text).login/tables_tab.py: the four sorts declare their words: "Table: A to Z", "Status: drafts first", "Review: not reviewed first", "Datasets: most first", and so on. Ascending stays "least done first".list_sort_select.html(new, generic): the select, inside the swapped region, so it always shows the sort on screen.tables_tab.jsrewrites its request the way it rewrites a filter's: every filter kept, page dropped.user_tables.html:display: contents, so the controls sit in the row as before.aria-expanded, and the CSS reads it. A fold left open in a narrow window therefore hides nothing in a wide one.tables_tab.css: the container queries, with the measurements beside them. Organization chips are capped at 11rem with an ellipsis; the full name is intitle, and every holder is already in the link'saria-label. That bounds the Access column whatever an Organization is called (names may be up to 150 characters).benchmarks/tables_tab/(new):seed.pyports the WF-06 prototype's generator (accounts p90 = 130, max = 2,068 mixed, maxreal = 2,068 as measured).run.pymeasures the row cost;widths.mjsmeasures the column widths in headless Chrome.Measurement 1: thresholds, on the real row partial, sidebar shown
Taken in headless Chrome 152. Each number is the narrowest list a set of columns fits, on the worst of the first six pages of each account. The page uses the system font stack, which falls back to DejaVu Sans on Linux. That is one of the widest fonts, so the numbers are conservative; I re-measured this slice's row with Segoe UI injected as a web font.
The thresholds are set for the row as it will be once #2560 and #2561 are in too. Both are built and open. Together they make the row 50 px wider: #2561 adds the ⋯ menu, and #2560 makes the Publishable header sortable. I measured that on a trial merge of all three branches (see "Merge note"). With thresholds set for this slice's row alone, a 1280 screen would scroll the table by 16 px as soon as both land.
What that gives with the sidebar:
No sideways scrolling at 1920, 1440, 1280, 1024, 800 and 390 px. I checked each width on all three accounts, first six pages each, in both fonts, on this branch and on the trial merge with #2560 + #2561. The table never scrolls. The page scrolls at 1024 only, by 90–126 px in DejaVu and 0–3 px in Segoe UI, and that is the site navbar (see "Found, not fixed").
Departures from the issue, for you to rule on
benchmarks/tables_tab/widths.mjsand moves the numbers; the CSS comment says so. Once they are in, Review also drops at 1440. Keeping it there would mean trimming about 90 px from the row, for example cell padding, the header sort arrows and the Table column's 13rem minimum. That is a design call, so it is not in this PR.Measurement 2: server cost per row
Measured with
python -m benchmarks.tables_tab.run, in the model-factsheet benchmark's style (throwaway database). Values are medians of 7 over the first 6 pages. Every request is 7 queries at every account size.The old cards cost 6–10 ms each. A row stays below 10 ms, so nothing was trimmed. The rendering cost is constant. What grows is the
oemetadatathe live Publish gate decodes in the page query: real documents range from a few KB to 500 KB. If that ever matters, the lever is to fetch only the license part of the document rather than all of it.Checked in a real browser (headless Chrome, real htmx, sidebar shown)
On the p90 account at 390 px, starting on page 3:
page=3, and the toggle reads "Filters (1)".sortleaves the URL.Merge note: #2560 (PR #2576) and #2561 (PR #2575)
I trial-merged both onto this branch and resolved them, as a throwaway branch that is not pushed. Whichever of the three lands later will see these conflicts:
login/listing.pyandlogin/tables_tab.py(Store the Publish gate result: Publishable filter and sort #2560): both sides add fields toSort; keep both. Store the Publish gate result: Publishable filter and sort #2560's new Publishable sort needs its words:Sort("publishable", "Publishable", F("publishable"), "not publishable first", "publishable first", nulls_last=True).SortSelectTests.test_every_sort_in_both_directions_least_done_firstthen gains the two Publishable entries.user_tables.html(Store the Publish gate result: Publishable filter and sort #2560): take this branch's version without the placeholderf-publishable. Store the Publish gate result: Publishable filter and sort #2560's real Publishable control is aChoiceFilter, so the controls loop renders it inside the fold panel by itself.tables_tab.js,tables_tab.css,tables_region.html(Tables tab: row action pipeline, with publish and unpublish from the row #2561): union both sides.api/services/table_actions.pyimportsPUBLISH_GATEfromlogin.tables_tab, and Store the Publish gate result: Publishable filter and sort #2560 moves it todataedit.publish_gate. Change the import whichever lands second.On the merged tree: vitest is green (69 in
login), the ⋯ menu opens in a stacked row at 390 px without overflow, and the widths are the "with #2560 + #2561" column above.Found, not fixed (both predate this branch)
.ms-auto) stick out 90–126 px. That belongs to the navbar rework (Feature 2025 rework oep homepage #2352).SyntaxError: Identifier 'reverseApiBaseUrl' has already been declared. htmx restores the page from its history cache and re-runs the inlineconstinbase/templates/base/reverseUrl.html. The restored state is correct. Develop shows the same error on the same steps.Tests
login/tests/test_tables_narrow.py(11 tests): the Sort by options, labels and selection, including an unknown sort; the select riding in the region; the order of controls behind the toggle;folded_count(stale values and Search do not count);data-foldedon an htmx swap.tables_tab.test.js(5 tests added): the Sort by request rewrite, focus back on the select, the toggle, "Filters (n)" following the region, and an open fold surviving a swap.login: 248 tests green. vitest: 135 green.QueryCountTestsandFilterQueryCountTestsare unchanged: still 7 unfiltered, 12 with every option filter named.oekg.tests.test_iri_resolution.LightImportTest, which cannot find the isolated settings module from its subprocess.🤖 Generated with Claude Code