Tables tab: row action pipeline, publish and unpublish from the row - #2575
Merged
Merged
Conversation
Every row of the tables tab gets its ⋯ menu, and every action on Tables one path (#2561, spec #2551): - api/services/table_actions.py, the table action service: preflight (what would run, what is left out and why, the consequences to state) and execute (re-check every named Table under a row lock and refuse the whole request if any is no longer allowed, else write all of them in one transaction). Publish takes a real Topic (the draft pseudo-topic is refused) and an embargo; unpublish names other people's Datasets that will hold a draft member. One logfmt line per Table per action on oeplatform.table_actions, after commit. The role rule mirrors myuser.get_table_permission_level, so the API can move onto the service (#2569) without changing who may do what. - TableActionView under profile/<user_id>/tables/actions/<action>, behind the owner rule: GET is the dialog, POST answers 204 + HX-Trigger tables-changed (message, focus), 409 + the dialog run again + HX-Trigger tables-refused, or 400 with the field error. A row action is a batch of one. Whether a changed Table is still shown is read off HX-Current-URL through the list's own filters. - The ⋯ menu: Edit metadata, Upload data, Publish… or Unpublish. An action above the user's role stays visible with its reason as text, aria-disabled plus a click guard rather than Bootstrap's .disabled. Entries whose ticket has not landed are absent. - The results region re-fetches itself on tables-changed; that request names the region as its trigger, so the view answers HX-Replace-Url and an action adds no history entry. - tables_tab.js: opens the dialog once filled, swaps 400/409 into it, toasts (success polite, ~5 s; refusals and failures assertive and persistent), one htmx:responseError handler (401 with a login link carrying next; anything else "may not have been made, reload"), and focus back to the row's ⋯ or the list heading. Tests: 34 in login/tests/test_table_actions.py through HTTP, 17 new vitest cases. Checked in headless Chrome against a seeded throwaway database. eslint skipped (SKIP=eslint): its --fix would reformat the 19 pre-existing findings in tables_tab.js and its test; the new lines follow the file's own style. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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 #2561. Slice 9 of #2551.
What it does
Every row of the tables tab gets its ⋯ menu, and every action on Tables goes through one service.
api/services/table_actions.py.preflight(user, action, names, params)says what would run, what is left out and why (grouped, with the text the user sees), and the consequences the dialog must state.execute(user, action, names, params, via=)re-checks every named Table under a row lock. If any Table is no longer allowed, it refuses the whole request (ActionRefused) and writes nothing. Otherwise it writes every Table in one transaction.oeplatform.table_actions, one per Table, after commit:table_action table=… action=… by=<pk> via=dashboard batch=<id>|-, plustopic=/embargo=on publish.profile/<user_id>/tables/actions/<action>(TableActionView), behind the owner rule. A row action is a batch of one: the endpoint takes repeatedtableparameters.HX-Trigger: tables-changedcarrying{message, focus}.HX-Trigger: tables-refused.aria-disabled="true", with the reason as visible text and a click guard. It does not use Bootstrap's.disabled, so the keyboard still reaches it (checked in Chrome).hx-trigger="tables-changed from:body"with its ownpage.url.HX-Replace-Urlinstead ofHX-Push-Url, and an action adds no history entry.HX-Current-URLthroughListing.matching.#tables-headingif the row is gone. A cancel returns focus to the ⋯.htmx:responseErrorhandler covers the rest: a 401 says "You have been logged out." with a login link carryingnext; any other error, and an unreachable server, say "the change may not have been made; reload".Decisions worth a look
api/services/, beside the dataset service, because API: single-table publish, unpublish and delete go through the table action service #2569 moves the API onto it. It importsPUBLISH_GATEandvisible_datasetsfromlogin/tables_tab.py. The reverse import would be a cycle, so the menu reads the role gates (ROLE_GATES) from the view's context instead. The menu costs no query: the page stays at 7.myuser.get_table_permission_level, the rule the API decorators read. It includes platform admins and members of an admin Organization. That keeps API: single-table publish, unpublish and delete go through the table action service #2569 from changing who may do what through the API. Consequence: the row'slevel(from Tables tab: Publishable, Review, Datasets and Topics columns #2554) ignores those two cases, so a platform admin who holds only Data editor directly sees Publish disabled in the menu, although the service would allow it. This is an edge case, and I left it alone rather than change Tables tab: Publishable, Review, Datasets and Topics columns #2554's Access cell._gate_reason) is the place to change.Preflight.ceilingisNone, because rows send one name and the ceilings for publish and unpublish are measured in Tables tab: selection, bulk bar and bulk publish/unpublish #2564.select_for_updateon the named Tables), so two concurrent publishes of one draft cannot both pass the check and give it a second Topic.api.actions.move_publishinside the transaction. Its own live license check raisingAPIErrorrolls back the whole batch and becomes a refusal.Tests
login/tests/test_table_actions.py, all through HTTP. They cover the menu gates (direct and through an Organization), the left-out groups, the real Topics only, other people's Datasets, nothing written by a preflight, the owner rule, publish with and without embargo, each unusable parameter, refused-whole (role lost, already published, gate failing), batch atomicity (the second write fails, the first is rolled back), the "not shown under the current filter" note, the log lines (batch id shared,-for one Table, none on refusal),HX-Replace-UrlvsHX-Push-Url, and the re-fetched state.tables_tab.test.js: the dialog opens on fill, 400/409 swap into it, the disabled click is swallowed, toasts (polite or assertive, timeout, text only), the 401 login link, 5xx and network errors, and focus to the ⋯, the heading, the opening row, or back after a cancel.login+base.tests.test_views: 273 green. Full suite: green exceptLightImportTest, the known artefact of the isolated-settings trick. vitest: 64 green. The OpenAPI drift guard is green.Checked in a browser
Headless Chrome, a seeded throwaway database, the sidebar shown, the branch's own static files.
?status=draft:history.lengthare unchanged;strangers_dataset (browser_stranger). Afterwards focus is on that row's ⋯ and the status cell reads Draft.?next=pointing at the view. Offline gives "The server could not be reached…".htmx:afterSwapcarries the target indetail.elt; the requesting element is inrequestConfig.elt. The test helper had mimicked the wrong shape.eslintwas skipped at commit (SKIP=eslint). Its--fixwould have reformatted the 19 findings already intables_tab.jsand its test; the new lines follow the file's existing style.🤖 Generated with Claude Code