Skip to content

Bulk Export: add an _until control to the UI #656

Description

@smunini

Summary

The Bulk Export page offers Since but no Until, so a user cannot bound an export to a time window from the UI.

Blocked on #657. _until is currently applied by only the S3 backend; SQLite, PostgreSQL, and MongoDB accept it and silently ignore it. Shipping this control before that is fixed would produce silently wrong exports on the default backend.

Current behavior

/ui/bulk-export's "Narrow it down" panel offers _elements, _typeFilter, and a Since preset with a custom-instant escape hatch (crates/ui/templates/pages/bulk-export.html:65-93). There is no Until control anywhere on the form.

Nothing in the UI carries the parameter: StartForm (crates/ui/src/bulk_export.rs:284-293) and parse_start_form (bulk_export.rs:295-312) have no until field, ExportJob (bulk_export.rs:55) stores only since, and kickoff() builds the query from _type, _elements, _typeFilter, and _since only (bulk_export.rs:369-380).

Expected behavior

An Until control beside Since, producing an _until query parameter, giving a bounded [_since, _until] export window. Leaving it empty keeps today's open-ended behavior.

Evidence

The REST layer already parses _until and threads it into ExportRequest.untilcrates/rest/src/handlers/bulk_export.rs:112-120 and :251, declared at crates/persistence/src/core/bulk_export.rs:354-356. The kick-off path needs no server-side change; only the backend query paths do, which is #657.

Suggested approach

Follow the Since precedent exactly:

  • Template — an Until block after the Since fields (bulk-export.html:76-93). Since is a preset <select> plus a custom-instant text input; Until has no natural preset list, so a single instant input with the same 2026-08-01T00:00:00Z placeholder is probably right.
  • StartForm + parse_start_form (bulk_export.rs:284-312) — add until. The form is parsed by hand because repeated types fields defeat axum::Form, so a new key is a one-line match arm.
  • ExportJob (bulk_export.rs:55) — add until beside since. Check that stored jobs still deserialize with the new field absent; existing jobs must not break.
  • kickoff() (bulk_export.rs:369-380) — push ("_until", &job.until) when non-empty.
  • Active Exports card (templates/partials/bulk_export_card.html) — surface it wherever Since is shown, so a running job states its window.
  • Strings — new label and hint keys in locales/{en,es,de}/main.ftl, next to the bulk-export-since-* block. UI copy: no capitalization convention — headings and buttons mix Title Case and sentence case #652 is settling the capitalization convention; new labels should land conforming.

Acceptance criteria

  • Bulk Export: _until is accepted but silently ignored by the SQLite, PostgreSQL, and MongoDB backends #657 has landed, so _until is honored by the active backend.
  • The Bulk Export form offers an Until input; leaving it empty is unchanged behavior.
  • Submitting an Until sends _until on the kick-off and the export respects it end to end.
  • Since and Until together produce a bounded window.
  • An invalid instant is reported to the user rather than producing a failed job (parse_instant returns a 400; kickoff() records non-202s onto the job at bulk_export.rs:405-412).
  • The window is visible on the Active Exports card.
  • Existing stored export jobs still deserialize after the ExportJob change.
  • Labels exist in en, es, and de.
  • crates/ui/e2e/tests/a11y.spec.ts passes with the new field.

Scope / out of scope

Out of scope: the backend gap, split out into #657.

Related: #655 (the Patients scope needs a patient-id field), #537 (built this workspace).

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingui

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions