Skip to content

Tables tab: row action pipeline, publish and unpublish from the row - #2575

Merged
jh-RLI merged 2 commits into
developfrom
feature-2561-row-actions
Oct 2, 2026
Merged

jh-RLI merged 2 commits into
developfrom
feature-2561-row-actions

Conversation

@jh-RLI

@jh-RLI jh-RLI commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

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.

  • The table action service is in 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.
    • Log lines go to oeplatform.table_actions, one per Table, after commit: table_action table=… action=… by=<pk> via=dashboard batch=<id>|-, plus topic=/embargo= on publish.
  • The dashboard endpoint is profile/<user_id>/tables/actions/<action> (TableActionView), behind the owner rule. A row action is a batch of one: the endpoint takes repeated table parameters.
    • GET returns the dialog.
    • POST succeeds with 204 and HX-Trigger: tables-changed carrying {message, focus}.
    • POST refused answers 409 with the dialog run again ("Nothing was changed: …") and HX-Trigger: tables-refused.
    • An unusable parameter answers 400 with the error beside its field.
  • The ⋯ menu has Edit metadata and Upload data (links to the existing pages), then Publish… or Unpublish.
    • An action above the user's role stays in the menu, marked 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).
    • Manage access, the Dataset entries and Delete are absent until their tickets.
  • Publish offers the 13 real Topics and an embargo of none, 6 months or 1 year. The service refuses the draft pseudo-topic. It leaves out Tables below Table admin, Tables already published, and Tables that fail the gate.
  • Unpublish asks for a plain confirmation: the Table "will no longer be listed under its topics". The dialog names other people's Datasets that will then hold a draft member.
  • After an action:
    • The results region re-fetches itself, because it declares hx-trigger="tables-changed from:body" with its own page.url.
    • That request names the region as its trigger, so the view answers HX-Replace-Url instead of HX-Push-Url, and an action adds no history entry.
    • The message names a changed Table that the current filter no longer shows. The server reads this off HX-Current-URL through Listing.matching.
    • Focus goes to the row's ⋯, or to #tables-heading if the row is gone. A cancel returns focus to the ⋯.
  • Toasts: success is polite and leaves after about 5 s. Refusals and failures are assertive and stay until dismissed. One htmx:responseError handler covers the rest: a 401 says "You have been logged out." with a login link carrying next; any other error, and an unreachable server, say "the change may not have been made; reload".

Decisions worth a look

Tests

  • 34 tests in 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-Url vs HX-Push-Url, and the re-fetched state.
  • 17 new vitest cases in 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 except LightImportTest, 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.

  • A Data editor's Publish… shows "Only Table admins can publish". Clicking it keeps the menu open and opens no dialog. ArrowDown reaches it.
  • Publishing under ?status=draft:
    • a confirm without a Topic shows "Choose a topic." in the still-open dialog;
    • with a Topic and 6 months, the dialog closes and the toast reads Published "Ready wind data" under climate, embargoed for 6 months. It is not shown under the current filter.;
    • focus lands on the heading and the row is gone;
    • the address and history.length are unchanged;
    • the toast is gone after 5 s.
  • Unpublish names strangers_dataset (browser_stranger). Afterwards focus is on that row's ⋯ and the status cell reads Draft.
  • Cancel (Escape) returns focus to the ⋯.
  • A role lowered while the dialog was open gives a 409: "Nothing was changed: Only Table admins can publish ("b_ready_two")." appears in the still-open dialog and as an assertive toast.
  • Logged out, a menu entry gives the toast "You have been logged out. Log in again" with ?next= pointing at the view. Offline gives "The server could not be reached…".
  • The browser checks found two things, both fixed in this branch:
    • htmx:afterSwap carries the target in detail.elt; the requesting element is in requestConfig.elt. The test helper had mimicked the wrong shape.
    • A sticky cell is its own stacking context, so the later rows' ⋯ painted over an open menu.
  • No JS errors in the console. The only console lines are the browser's own resource messages for the deliberate 400 and 409.

eslint was skipped at commit (SKIP=eslint). Its --fix would have reformatted the 19 findings already in tables_tab.js and its test; the new lines follow the file's existing style.

🤖 Generated with Claude Code

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>
@jh-RLI
jh-RLI merged commit 06559da into develop Oct 2, 2026
4 of 5 checks passed
@jh-RLI
jh-RLI deleted the feature-2561-row-actions branch October 2, 2026 18:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tables tab: row action pipeline, with publish and unpublish from the row

1 participant