From 1aef14b9f33961d2f267b5de0320672b2416c7e8 Mon Sep 17 00:00:00 2001 From: angela-helios Date: Fri, 31 Jul 2026 13:56:42 -0400 Subject: [PATCH] feat(ui): Batch/Transaction workspace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Batch & Data nav entry becomes a real page (Brett's frames): drop or browse a Bundle JSON, review the execution plan — request line, one row per entry with its method chip and collapsible body, a Bundle JSON tab, and a one-line semantics explainer chosen by the bundle's own type (transaction: all or nothing; batch: independent entries) — then execute against the FHIR root and read the outcome: per-action status badges straight from the response bundle plus the aggregate created / updated / read / failed summary. A rolled-back transaction surfaces its OperationOutcome instead of pretending there are outcomes. The shell is server-rendered; batch.js does the parsing, rendering, and the POST, so the UI crate still never touches storage. Three locales. Closes #476 --- crates/ui/assets/app.css | 182 +++++++++++++++++ crates/ui/assets/batch.js | 276 ++++++++++++++++++++++++++ crates/ui/e2e/tests/a11y.spec.ts | 1 + crates/ui/e2e/tests/batch.spec.ts | 83 ++++++++ crates/ui/e2e/tests/no-cdn.spec.ts | 1 + crates/ui/src/lib.rs | 27 +++ crates/ui/templates/layouts/base.html | 4 +- crates/ui/templates/pages/batch.html | 94 +++++++++ crates/ui/tests/router_http.rs | 22 ++ locales/de/main.ftl | 27 +++ locales/en/main.ftl | 27 +++ locales/es/main.ftl | 27 +++ 12 files changed, 769 insertions(+), 2 deletions(-) create mode 100644 crates/ui/assets/batch.js create mode 100644 crates/ui/e2e/tests/batch.spec.ts create mode 100644 crates/ui/templates/pages/batch.html diff --git a/crates/ui/assets/app.css b/crates/ui/assets/app.css index 654f3a40a..2d1b8728f 100644 --- a/crates/ui/assets/app.css +++ b/crates/ui/assets/app.css @@ -3951,3 +3951,185 @@ html[data-nav="collapsed"] .menu__panel { left: 0; } color: var(--muted); font-size: 11px; } + +/* Batch / Transaction workspace (#476). */ +.batch-drop { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + width: 100%; + padding: 44px 16px; + border: 1.5px dashed var(--divider); + border-radius: 12px; + background: transparent; + color: var(--muted); + cursor: pointer; + font: inherit; +} + +.batch-drop strong { + color: var(--text); +} + +.batch-drop--over, +.batch-drop:hover { + border-color: var(--accent-strong); + color: var(--text); +} + +.request-strip { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; + padding: 10px 14px; + border: 1px solid var(--divider); + border-radius: 10px; + background: var(--surface); +} + +.request-strip__label { + font-size: 11px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--muted); +} + +.batch-tabs { + display: flex; + gap: 6px; + margin-bottom: 8px; +} + +.batch-tab { + padding: 8px 14px; + border: 1px solid var(--divider); + border-radius: 999px; + background: transparent; + color: var(--muted); + font: inherit; + cursor: pointer; +} + +.batch-tab[aria-selected="true"] { + background: var(--accent-soft); + color: var(--accent-text); + border-color: transparent; +} + +.batch-rows { + list-style: none; + margin: 0; + padding: 0; +} + +.batch-row { + border-top: 1px solid var(--divider); +} + +.batch-row__head { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + padding: 12px 4px; + border: 0; + background: transparent; + color: var(--text); + font: inherit; + cursor: pointer; + text-align: left; +} + +.batch-row__head--static { + cursor: default; +} + +.batch-row__num { + display: grid; + place-items: center; + width: 22px; + height: 22px; + border-radius: 50%; + background: var(--accent-soft); + color: var(--accent-text); + font-size: 12px; + flex: none; +} + +.batch-chip { + padding: 2px 8px; + border: 1px solid var(--divider); + border-radius: 6px; + font-family: ui-monospace, monospace; + font-size: 12px; + flex: none; +} + +.batch-chip--post { color: var(--accent-text); border-color: var(--accent-strong); } +.batch-chip--delete { color: var(--danger); border-color: var(--danger); } + +.batch-row__url { + font-family: ui-monospace, monospace; + font-size: 13px; + overflow-wrap: anywhere; +} + +.batch-row__arrow { + margin-left: auto; + color: var(--muted); +} + +.batch-row__body { + margin: 0 0 12px 34px; + padding: 12px; + border: 1px solid var(--divider); + border-radius: 8px; + background: var(--surface); + font-size: 12.5px; + overflow-x: auto; +} + +.batch-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 14px; +} + +.batch-footer__right { + display: flex; + gap: 8px; +} + +.batch-badge { + margin-left: auto; + padding: 2px 10px; + border-radius: 6px; + font-family: ui-monospace, monospace; + font-size: 12px; +} + +.batch-badge--ok { background: var(--accent-soft); color: var(--accent-text); } +.batch-badge--error { background: var(--danger-soft); color: var(--danger); } + +.batch-summary { + margin: 4px 0 10px; + color: var(--muted); +} + +.batch-error { + margin-top: 10px; + color: var(--danger); +} + +.batch-json { + margin: 0; + padding: 12px; + border: 1px solid var(--divider); + border-radius: 8px; + background: var(--surface); + font-size: 12.5px; + overflow-x: auto; +} diff --git a/crates/ui/assets/batch.js b/crates/ui/assets/batch.js new file mode 100644 index 000000000..43526c894 --- /dev/null +++ b/crates/ui/assets/batch.js @@ -0,0 +1,276 @@ +/* + * Batch / Transaction workspace (#476, Brett's frames): pick a Bundle JSON, + * review the execution plan (one row per entry, method chip, collapsible + * body), execute against the FHIR root, and read the per-action outcomes plus + * the aggregate result. The bundle's own `type` decides the semantics copy. + */ +(function () { + "use strict"; + + var root = document.getElementById("batch"); + if (!root || !window.fetch) return; + var messages = root.dataset; + + /* The effective tenant, stamped by the server (#344); FHIR calls carry it. */ + var TENANT = (document.querySelector('meta[name="hfs-tenant"]') || {}).content || ""; + function fhirHeaders(extra) { + var h = { Accept: "application/fhir+json" }; + if (TENANT) h["X-Tenant-ID"] = TENANT; + if (extra) for (var k in extra) h[k] = extra[k]; + return h; + } + + var stages = { + upload: document.getElementById("batch-upload"), + preflight: document.getElementById("batch-preflight"), + response: document.getElementById("batch-response"), + }; + function show(stage) { + for (var k in stages) stages[k].hidden = k !== stage; + } + + var drop = document.getElementById("batch-drop"); + var fileInput = document.getElementById("batch-file"); + var uploadError = document.getElementById("batch-upload-error"); + var requestLine = document.getElementById("batch-request-line"); + var semantics = document.getElementById("batch-semantics"); + var rows = document.getElementById("batch-rows"); + var rawJson = document.getElementById("batch-json"); + var tabActions = document.getElementById("batch-tab-actions"); + var tabJson = document.getElementById("batch-tab-json"); + var executeError = document.getElementById("batch-execute-error"); + var outcomes = document.getElementById("batch-outcomes"); + var overall = document.getElementById("batch-overall"); + var summary = document.getElementById("batch-summary"); + + var bundle = null; + + /* ---- stage 1: pick the file ---------------------------------------- */ + + drop.addEventListener("click", function () { + fileInput.click(); + }); + drop.addEventListener("dragover", function (e) { + e.preventDefault(); + drop.classList.add("batch-drop--over"); + }); + drop.addEventListener("dragleave", function () { + drop.classList.remove("batch-drop--over"); + }); + drop.addEventListener("drop", function (e) { + e.preventDefault(); + drop.classList.remove("batch-drop--over"); + if (e.dataTransfer.files && e.dataTransfer.files[0]) readFile(e.dataTransfer.files[0]); + }); + fileInput.addEventListener("change", function () { + if (fileInput.files && fileInput.files[0]) readFile(fileInput.files[0]); + }); + + function fail(message) { + uploadError.textContent = message; + uploadError.hidden = false; + } + + function readFile(file) { + uploadError.hidden = true; + var reader = new FileReader(); + reader.onload = function () { + var parsed; + try { + parsed = JSON.parse(reader.result); + } catch (e) { + return fail(messages.msgInvalidJson + " (" + e.message + ")"); + } + if (!parsed || parsed.resourceType !== "Bundle") return fail(messages.msgNotABundle); + if (parsed.type !== "batch" && parsed.type !== "transaction") return fail(messages.msgBadType); + bundle = parsed; + renderPreflight(); + show("preflight"); + }; + reader.readAsText(file); + } + + /* ---- stage 2: the execution plan ------------------------------------ */ + + function entries() { + return Array.isArray(bundle.entry) ? bundle.entry : []; + } + + function methodOf(entry) { + return ((entry.request && entry.request.method) || "?").toUpperCase(); + } + + function renderPreflight() { + var n = entries().length; + requestLine.textContent = + "POST [base] · Bundle · " + bundle.type + " · " + n + " " + messages.msgEntries; + semantics.textContent = + bundle.type === "transaction" ? messages.msgSemanticsTransaction : messages.msgSemanticsBatch; + + rows.textContent = ""; + entries().forEach(function (entry, i) { + var li = document.createElement("li"); + li.className = "batch-row"; + + var head = document.createElement("button"); + head.type = "button"; + head.className = "batch-row__head"; + head.setAttribute("aria-expanded", "false"); + + var num = document.createElement("span"); + num.className = "batch-row__num"; + num.textContent = String(i + 1); + + var method = methodOf(entry); + var chip = document.createElement("span"); + chip.className = "batch-chip batch-chip--" + method.toLowerCase(); + chip.textContent = method; + + var url = document.createElement("code"); + url.className = "batch-row__url"; + url.textContent = (entry.request && entry.request.url) || ""; + + var arrow = document.createElement("span"); + arrow.className = "batch-row__arrow"; + arrow.textContent = "▾"; + + head.appendChild(num); + head.appendChild(chip); + head.appendChild(url); + head.appendChild(arrow); + + var body = document.createElement("pre"); + body.className = "batch-row__body"; + body.hidden = true; + body.textContent = entry.resource + ? JSON.stringify(entry.resource, null, 2) + : messages.msgNoBody; + + head.addEventListener("click", function () { + body.hidden = !body.hidden; + head.setAttribute("aria-expanded", body.hidden ? "false" : "true"); + }); + + li.appendChild(head); + li.appendChild(body); + rows.appendChild(li); + }); + + rawJson.textContent = JSON.stringify(bundle, null, 2); + selectTab("actions"); + } + + function selectTab(which) { + var actions = which === "actions"; + tabActions.setAttribute("aria-selected", actions ? "true" : "false"); + tabJson.setAttribute("aria-selected", actions ? "false" : "true"); + rows.hidden = !actions; + rawJson.hidden = actions; + } + tabActions.addEventListener("click", function () { selectTab("actions"); }); + tabJson.addEventListener("click", function () { selectTab("json"); }); + + function reset() { + bundle = null; + fileInput.value = ""; + show("upload"); + } + document.getElementById("batch-cancel").addEventListener("click", reset); + document.getElementById("batch-upload-another").addEventListener("click", function () { + fileInput.click(); + }); + + /* ---- stage 3: execute and report ------------------------------------ */ + + function execute() { + executeError.hidden = true; + fetch("/", { + method: "POST", + headers: fhirHeaders({ "Content-Type": "application/fhir+json" }), + credentials: "same-origin", + body: JSON.stringify(bundle), + }) + .then(function (response) { + return response + .json() + .catch(function () { return null; }) + .then(function (body) { renderResponse(response, body); }); + }) + .catch(function (e) { + executeError.textContent = messages.msgRequestFailed + " (" + e.message + ")"; + executeError.hidden = false; + }); + } + document.getElementById("batch-execute").addEventListener("click", execute); + document.getElementById("batch-execute-again").addEventListener("click", execute); + document.getElementById("batch-back").addEventListener("click", function () { + show("preflight"); + }); + + function renderResponse(response, body) { + overall.textContent = String(response.status); + overall.className = "batch-badge " + (response.ok ? "batch-badge--ok" : "batch-badge--error"); + + outcomes.textContent = ""; + var created = 0, updated = 0, other = 0, failed = 0; + var responded = (body && Array.isArray(body.entry)) ? body.entry : []; + + // The whole-bundle failure case (e.g. a rolled-back transaction): show + // the OperationOutcome text instead of pretending there are outcomes. + if (!response.ok && (!responded.length || (body && body.resourceType === "OperationOutcome"))) { + var diag = ""; + if (body && body.issue && body.issue[0]) { + diag = body.issue[0].diagnostics || (body.issue[0].details && body.issue[0].details.text) || ""; + } + executeError.textContent = messages.msgRequestFailed + (diag ? " — " + diag : ""); + executeError.hidden = false; + return; + } + + responded.forEach(function (entry, i) { + var status = (entry.response && entry.response.status) || ""; + var request = entries()[i] || {}; + var li = document.createElement("li"); + li.className = "batch-row"; + var head = document.createElement("div"); + head.className = "batch-row__head batch-row__head--static"; + + var num = document.createElement("span"); + num.className = "batch-row__num"; + num.textContent = String(i + 1); + var method = methodOf(request); + var chip = document.createElement("span"); + chip.className = "batch-chip batch-chip--" + method.toLowerCase(); + chip.textContent = method; + var url = document.createElement("code"); + url.className = "batch-row__url"; + url.textContent = (request.request && request.request.url) || ""; + var badge = document.createElement("span"); + var code = parseInt(status, 10) || 0; + var ok = code >= 200 && code < 300; + badge.className = "batch-badge " + (ok ? "batch-badge--ok" : "batch-badge--error"); + badge.textContent = status; + + if (!ok) failed++; + else if (code === 201) created++; + else if (method === "PUT" || method === "PATCH") updated++; + else other++; + + head.appendChild(num); + head.appendChild(chip); + head.appendChild(url); + head.appendChild(badge); + li.appendChild(head); + outcomes.appendChild(li); + }); + + var parts = []; + if (created) parts.push(created + " " + messages.msgCreated); + if (updated) parts.push(updated + " " + messages.msgUpdated); + if (other) parts.push(other + " " + messages.msgOther); + if (failed) parts.push(failed + " " + messages.msgFailed); + summary.textContent = parts.join(" · "); + + show("response"); + } +})(); diff --git a/crates/ui/e2e/tests/a11y.spec.ts b/crates/ui/e2e/tests/a11y.spec.ts index f342bed70..2a0470af5 100644 --- a/crates/ui/e2e/tests/a11y.spec.ts +++ b/crates/ui/e2e/tests/a11y.spec.ts @@ -9,6 +9,7 @@ const WCAG = ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"]; const ROUTES = [ "/ui", "/ui/resources", + "/ui/batch", "/ui/compartments", "/ui/search-parameters", "/ui/queries", diff --git a/crates/ui/e2e/tests/batch.spec.ts b/crates/ui/e2e/tests/batch.spec.ts new file mode 100644 index 000000000..4d5c8614d --- /dev/null +++ b/crates/ui/e2e/tests/batch.spec.ts @@ -0,0 +1,83 @@ +import { test, expect } from "../pages/fixtures"; +import { writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +// The Batch/Transaction workspace (#476): upload → preflight → execute → +// response, entirely against the ordinary FHIR API. + +function bundleFile(type: "batch" | "transaction"): string { + const stamp = Date.now(); + const bundle = { + resourceType: "Bundle", + type, + entry: [ + { + fullUrl: "urn:uuid:00000000-0000-4000-8000-000000000001", + resource: { resourceType: "Patient", name: [{ family: `BatchUi${stamp}` }] }, + request: { method: "POST", url: "Patient" }, + }, + { + fullUrl: "urn:uuid:00000000-0000-4000-8000-000000000002", + resource: { resourceType: "Patient", name: [{ family: `BatchUiB${stamp}` }] }, + request: { method: "POST", url: "Patient" }, + }, + ], + }; + const file = join(tmpdir(), `hfs-e2e-bundle-${type}-${stamp}.json`); + writeFileSync(file, JSON.stringify(bundle)); + return file; +} + +test("a transaction bundle uploads, previews, executes, and reports", async ({ page }) => { + await page.goto("/ui/batch", { waitUntil: "networkidle" }); + + await page.locator("#batch-file").setInputFiles(bundleFile("transaction")); + + // Preflight: request line, all-or-nothing copy, one row per entry. + await expect(page.locator("#batch-preflight")).toBeVisible(); + await expect(page.locator("#batch-request-line")).toContainText("transaction"); + await expect(page.locator("#batch-request-line")).toContainText("2"); + await expect(page.locator("#batch-semantics")).toContainText(/all or nothing/i); + await expect(page.locator("#batch-rows .batch-row")).toHaveCount(2); + await expect(page.locator("#batch-rows .batch-chip").first()).toHaveText("POST"); + + // The accordion shows the entry body. + await page.locator("#batch-rows .batch-row__head").first().click(); + await expect(page.locator("#batch-rows .batch-row__body").first()).toBeVisible(); + await expect(page.locator("#batch-rows .batch-row__body").first()).toContainText("BatchUi"); + + // The Bundle JSON tab shows the raw payload. + await page.locator("#batch-tab-json").click(); + await expect(page.locator("#batch-json")).toBeVisible(); + await page.locator("#batch-tab-actions").click(); + + // Execute: outcomes per entry plus the aggregate summary. + await page.locator("#batch-execute").click(); + await expect(page.locator("#batch-response")).toBeVisible(); + await expect(page.locator("#batch-outcomes .batch-row")).toHaveCount(2); + await expect(page.locator("#batch-outcomes .batch-badge").first()).toContainText("201"); + await expect(page.locator("#batch-summary")).toContainText("2"); + await expect(page.locator("#batch-summary")).toContainText(/created/i); + await expect(page.locator("#batch-overall")).toHaveClass(/--ok/); + + // Back to the preflight keeps the parsed bundle. + await page.locator("#batch-back").click(); + await expect(page.locator("#batch-preflight")).toBeVisible(); + await expect(page.locator("#batch-rows .batch-row")).toHaveCount(2); +}); + +test("a batch bundle gets the independent-entries copy", async ({ page }) => { + await page.goto("/ui/batch", { waitUntil: "networkidle" }); + await page.locator("#batch-file").setInputFiles(bundleFile("batch")); + await expect(page.locator("#batch-semantics")).toContainText(/independently/i); +}); + +test("a non-bundle file is rejected with a message, not a crash", async ({ page }) => { + await page.goto("/ui/batch", { waitUntil: "networkidle" }); + const file = join(tmpdir(), `hfs-e2e-notabundle-${Date.now()}.json`); + writeFileSync(file, JSON.stringify({ resourceType: "Patient" })); + await page.locator("#batch-file").setInputFiles(file); + await expect(page.locator("#batch-upload-error")).toBeVisible(); + await expect(page.locator("#batch-preflight")).toBeHidden(); +}); diff --git a/crates/ui/e2e/tests/no-cdn.spec.ts b/crates/ui/e2e/tests/no-cdn.spec.ts index 407e94ffb..4fb7670d1 100644 --- a/crates/ui/e2e/tests/no-cdn.spec.ts +++ b/crates/ui/e2e/tests/no-cdn.spec.ts @@ -5,6 +5,7 @@ import { test, expect } from "../pages/fixtures"; const ROUTES = [ "/ui", "/ui/resources", + "/ui/batch", "/ui/compartments", "/ui/search-parameters", "/ui/queries", diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 1cf9edfe6..a6efc71f9 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -456,6 +456,15 @@ struct SearchParametersPage { view: search_params::SpView, } +/// Batch/Transaction workspace (#476): a static shell; batch.js does the rest. +#[derive(Template)] +#[template(path = "pages/batch.html")] +struct BatchPage { + status: Status, + i18n: I18n, + active_page: &'static str, +} + /// Compartment viewer & route tester (#237). Read-only: the base definitions /// are codegen'd into the binary; a tenant-scoped override layer is open /// question 1 on the issue. @@ -605,6 +614,8 @@ pub fn mount_with_conformance_source( .route("/ui/queries/params", get(query_params_catalog)) .route("/ui/search-parameters", get(search_parameters)) .route("/ui/compartments", get(compartments_page)) + // Batch/Transaction workspace (#476): upload → preflight → response. + .route("/ui/batch", get(batch_page)) // Schema-driven resource editor (#264). One POST endpoint applies every // structural mutation and re-renders: the document rides with it. .route("/ui/editor", get(editor::page)) @@ -1033,6 +1044,22 @@ struct CompartmentsQuery { target: String, } +/// Batch/Transaction workspace page (#476). The shell is server-rendered; +/// batch.js drives upload → preflight → execute → response entirely against +/// the ordinary FHIR root, so this crate never touches storage. +async fn batch_page( + State(state): State, + locale: RequestLocale, + rv: RequestVersion, + rt: RequestTenant, +) -> Response { + render(BatchPage { + status: current_status(state.version, rv.0, &rt), + i18n: I18n::new(locale), + active_page: "batch", + }) +} + /// Compartment viewer & tester page. async fn compartments_page( State(state): State, diff --git a/crates/ui/templates/layouts/base.html b/crates/ui/templates/layouts/base.html index 85b88b25f..4ff0f265a 100644 --- a/crates/ui/templates/layouts/base.html +++ b/crates/ui/templates/layouts/base.html @@ -92,10 +92,10 @@ - + {% include "icons/layers.svg" %} {{ i18n.t("nav-batch-transaction") }} - + {% include "icons/export.svg" %} {{ i18n.t("nav-bulk-export") }} diff --git a/crates/ui/templates/pages/batch.html b/crates/ui/templates/pages/batch.html new file mode 100644 index 000000000..1ea36d805 --- /dev/null +++ b/crates/ui/templates/pages/batch.html @@ -0,0 +1,94 @@ +{% extends "layouts/base.html" %} + +{% block title %}{{ i18n.t("batch-heading") }} — {{ i18n.t("app-title") }}{% endblock %} + +{% block content %} +
+

{{ i18n.t("batch-heading") }}

+

{{ i18n.t("batch-lede") }}

+
+ + +
+ + +
+
+

{% include "icons/export.svg" %} {{ i18n.t("batch-upload") }}

+
+ + + +
+ + + + + + +
+ + +{% endblock %} diff --git a/crates/ui/tests/router_http.rs b/crates/ui/tests/router_http.rs index f95d214b1..b67b7c19b 100644 --- a/crates/ui/tests/router_http.rs +++ b/crates/ui/tests/router_http.rs @@ -660,3 +660,25 @@ async fn version_selector_lists_the_enabled_versions() { // The default label is server-derived, not hardcoded markup. assert!(html.contains("FHIR R4")); } + +/// #476: the Batch/Transaction workspace mounts and the nav links it. +#[tokio::test] +async fn batch_page_serves_the_workspace_shell() { + let response = app() + .oneshot(Request::get("/ui/batch").body(Body::empty()).unwrap()) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + let html = body_text(response).await; + assert!(html.contains("")); + // The three client-driven stages are all in the shell. + assert!(html.contains(r#"id="batch-upload""#)); + assert!(html.contains(r#"id="batch-preflight""#)); + assert!(html.contains(r#"id="batch-response""#)); + // The nav entry is a real link now, current on this page. + assert!(html.contains(r#"href="/ui/batch" aria-current="page""#)); + // The semantics copy rides in as data for batch.js. + assert!(html.contains("data-msg-semantics-transaction")); + assert!(html.contains(r#"src="/ui/assets/batch.js""#)); +} diff --git a/locales/de/main.ftl b/locales/de/main.ftl index ac3a980ec..316c678bb 100644 --- a/locales/de/main.ftl +++ b/locales/de/main.ftl @@ -472,3 +472,30 @@ resources-types-heading = Ressourcentypen queries-saved-group = Gespeichert nav-collapse = Menü einklappen + +batch-heading = Batch / Transaction +batch-lede = Lade ein FHIR-Bundle hoch, prüfe die auszuführenden Aktionen, führe es gegen diesen Server aus und lies das Ergebnis jedes Eintrags. +batch-upload = Hochladen +batch-drop-hint = Bundle-JSON-Datei hier ablegen +batch-drop-browse = oder klicken zum Durchsuchen +batch-invalid-json = Diese Datei ist kein gültiges JSON +batch-not-a-bundle = Dieses JSON ist kein FHIR-Bundle +batch-bad-type = Hier lassen sich nur Bundles vom Typ batch oder transaction ausführen +batch-request = Anfrage +batch-entries = Einträge +batch-semantics-batch = Batch: Einträge laufen unabhängig — ein fehlgeschlagener Eintrag stoppt die anderen nicht und macht sie nicht rückgängig. +batch-semantics-transaction = Transaction: alles oder nichts — schlägt ein Eintrag fehl, rollt der Server das gesamte Bundle zurück. +batch-tab-actions = Aktionen +batch-tab-json = Bundle-JSON +batch-no-body = (kein Body — dieser Eintrag adressiert nur eine Ressource) +batch-cancel = Abbrechen +batch-upload-another = Weitere hochladen +batch-execute = Ausführen +batch-response-heading = Ergebnisse pro Aktion +batch-sum-created = erstellt +batch-sum-updated = aktualisiert +batch-sum-other = gelesen/sonstige +batch-sum-failed = fehlgeschlagen +batch-request-failed = Die Anfrage ist fehlgeschlagen +batch-back = Zurück zum Bundle +batch-execute-again = Erneut ausführen diff --git a/locales/en/main.ftl b/locales/en/main.ftl index 57a2648f0..70ca6e0f2 100644 --- a/locales/en/main.ftl +++ b/locales/en/main.ftl @@ -475,3 +475,30 @@ resources-types-heading = Resource types queries-saved-group = Saved nav-collapse = Collapse menu + +batch-heading = Batch / Transaction +batch-lede = Upload a FHIR Bundle, review the actions it will run, execute it against this server, and read the outcome of every entry. +batch-upload = Upload +batch-drop-hint = Drop a bundle JSON file here +batch-drop-browse = or click to browse +batch-invalid-json = That file is not valid JSON +batch-not-a-bundle = That JSON is not a FHIR Bundle +batch-bad-type = Only Bundles of type batch or transaction can be executed here +batch-request = Request +batch-entries = entries +batch-semantics-batch = Batch: entries run independently — a failed entry does not stop or undo the others. +batch-semantics-transaction = Transaction: all or nothing — if any entry fails, the server rolls the whole bundle back. +batch-tab-actions = Actions +batch-tab-json = Bundle JSON +batch-no-body = (no body — this entry only addresses a resource) +batch-cancel = Cancel +batch-upload-another = Upload another +batch-execute = Execute +batch-response-heading = Per-action outcomes +batch-sum-created = created +batch-sum-updated = updated +batch-sum-other = read/other +batch-sum-failed = failed +batch-request-failed = The request failed +batch-back = Back to bundle +batch-execute-again = Execute again diff --git a/locales/es/main.ftl b/locales/es/main.ftl index 263ab8a1d..bbc87167a 100644 --- a/locales/es/main.ftl +++ b/locales/es/main.ftl @@ -472,3 +472,30 @@ resources-types-heading = Tipos de recurso queries-saved-group = Guardadas nav-collapse = Colapsar menú + +batch-heading = Batch / Transaction +batch-lede = Sube un Bundle FHIR, revisa las acciones que va a ejecutar, ejecútalo contra este servidor y lee el resultado de cada entrada. +batch-upload = Subir +batch-drop-hint = Suelta aquí un fichero JSON de bundle +batch-drop-browse = o haz clic para explorar +batch-invalid-json = Ese fichero no es JSON válido +batch-not-a-bundle = Ese JSON no es un Bundle FHIR +batch-bad-type = Aquí solo se ejecutan Bundles de tipo batch o transaction +batch-request = Petición +batch-entries = entradas +batch-semantics-batch = Batch: las entradas se ejecutan de forma independiente — una entrada fallida no detiene ni deshace las demás. +batch-semantics-transaction = Transaction: todo o nada — si alguna entrada falla, el servidor revierte el bundle completo. +batch-tab-actions = Acciones +batch-tab-json = JSON del bundle +batch-no-body = (sin cuerpo — esta entrada solo direcciona un recurso) +batch-cancel = Cancelar +batch-upload-another = Subir otro +batch-execute = Ejecutar +batch-response-heading = Resultados por acción +batch-sum-created = creados +batch-sum-updated = actualizados +batch-sum-other = lecturas/otros +batch-sum-failed = fallidos +batch-request-failed = La petición falló +batch-back = Volver al bundle +batch-execute-again = Ejecutar de nuevo