Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/hts-ui/templates/pages/capability-statement.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
-#}
{% extends "layouts/base.html" %}

{% block content_class %} content--capability{% endblock %}

{% block title %}{{ chrome.i18n.t("cap-title") }} · {{ chrome.i18n.t("hts-app-title") }}{% endblock %}

{% block content %}
Expand Down
11 changes: 9 additions & 2 deletions crates/ui-chrome/templates/partials/capability-raw-card.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,16 @@
<details class="card json-fold" id="capability-json-fold"{% if raw_requested %} open{% else %} data-capability-json-node{% endif %}>
<summary class="card-head">
<h3>{{ i18n.t("cap-raw-toggle") }}</h3>
<span class="icon">{% include "icons/chevron-down.svg" %}</span>
<div class="card-head__actions">
<div class="card-head__tools">
<button type="button" class="editor-json__act" data-capability-json-fold="all" title="{{ i18n.t("editor-collapse-all") }}">{{ i18n.t("editor-collapse-all") }}</button>
<button type="button" class="editor-json__act" data-capability-json-fold="none" title="{{ i18n.t("editor-expand-all") }}">{{ i18n.t("editor-expand-all") }}</button>
</div>
<span class="icon">{% include "icons/chevron-down.svg" %}</span>
</div>
</summary>
<div class="card__body" id="capability-json-body"{% if !raw_requested %}
<div class="card__body" id="capability-json-body"{% if !raw_requested %}
role="region" tabindex="0" aria-label="{{ i18n.t("cap-raw-toggle") }}"
data-capability-json-body
data-loading-text="{{ i18n.t("cap-raw-loading") }}"
data-error-text="{{ i18n.t("cap-json-load-error") }}"
Expand Down
43 changes: 41 additions & 2 deletions crates/ui/assets/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@
@layer pages {
/* CapabilityStatement-only presentation: keep the backend note subordinate to
its interaction tags, and let the resource filter fit a phone-width card. */
.cap-transaction-note {
margin: 8px 0 0 16px;
.content--capability .cap-transaction-note {
padding-left: 10px;
border-left: 2px solid var(--surface-border);
color: var(--muted);
Expand Down Expand Up @@ -325,6 +324,9 @@ svg {

@layer components {
/* ---------- Sidebar ---------- */
.content--capability .card__body p {
margin: 0;
}

.sidebar {
position: fixed;
Expand Down Expand Up @@ -873,6 +875,12 @@ button.menu__option {
padding: 14px;
}

.content--capability .card__body {
display: flex;
flex-direction: column;
gap: 8px;
}

/* An empty-state pane: breathing room between the lede and its call to
action (the lede's own margin is 0 for the page head, where the section
gap already separates it). */
Expand Down Expand Up @@ -1348,6 +1356,10 @@ button.pill {
padding: 20px;
}

.content--capability .table-card {
padding: 0;
}

.toolbar {
display: flex;
align-items: center;
Expand Down Expand Up @@ -2957,6 +2969,10 @@ details.json-fold[open] > summary .icon {
overflow-x: auto;
}

.content--capability .table-wrap {
padding-inline: 20px;
}

.data-table {
width: 100%;
border-collapse: collapse;
Expand Down Expand Up @@ -3070,6 +3086,23 @@ details.json-fold[open] > summary .icon {
font-weight: 600;
}

.content--capability .tag {
margin-inline: 0 8px;
}

.content--capability {
display: flex;
flex-direction: column;
gap: 20px;
}

.content--capability > .page-head {
margin-bottom: 0px;
}

.content--capability > .card ~ .card {
margin-top: 0px;
}
.tag--type {
background: var(--accent-soft);
color: var(--text);
Expand Down Expand Up @@ -4374,6 +4407,12 @@ details.json-fold[open] > summary .icon {
gap: 6px;
}

.card-head__actions {
display: flex;
align-items: center;
gap: 10px;
}

.editor-json__act {
padding: 3px 8px;
font: inherit;
Expand Down
57 changes: 49 additions & 8 deletions crates/ui/assets/capability-json.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,8 @@
if (body) reset(body, false);
}

function closeOtherBranches(details) {
var root = details.closest("#capability-json-fold");
if (!root || details === root) return;
root.querySelectorAll("details[data-capability-json-node][open]").forEach(function (open) {
if (open === details || open === root || open.contains(details)) return;
collapse(open);
});
function rootFor(details) {
return details.closest("#capability-json-fold");
}

function triggerLoad(body) {
Expand All @@ -92,6 +87,39 @@
}
}

function expandNext(root) {
if (!root || root.dataset.capabilityJsonExpandAll !== "true") return;
var nodes = root.querySelectorAll("details[data-capability-json-node]");
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (node.open) continue;
node.open = true;
var body = bodyFor(node);
if (body) {
triggerLoad(body);
return;
}
}
}

function setAll(details, expand) {
var root = rootFor(details);
if (!root) return;
root.dataset.capabilityJsonExpandAll = expand ? "true" : "false";
if (expand) {
root.open = true;
var rootBody = bodyFor(root);
if (rootBody) {
triggerLoad(rootBody);
expandNext(root);
}
return;
}
root.querySelectorAll("details[data-capability-json-node]").forEach(function (node) {
collapse(node);
});
}

document.addEventListener(
"toggle",
function (event) {
Expand All @@ -101,17 +129,27 @@
if (!body) return;

if (details.open) {
closeOtherBranches(details);
triggerLoad(body);
} else {
// Removing the swapped subtree is the DOM budget: reopening issues a
// fresh, bounded request rather than retaining descendants invisibly.
if (details === rootFor(details)) details.dataset.capabilityJsonExpandAll = "false";
reset(body, false);
}
},
true,
);

document.addEventListener("click", function (event) {
var control = event.target.closest && event.target.closest("[data-capability-json-fold]");
if (!control) return;
event.preventDefault();
event.stopPropagation();
var details = control.closest("#capability-json-fold");
if (!details) return;
setAll(details, control.dataset.capabilityJsonFold === "none");
});

document.addEventListener("htmx:beforeRequest", function (event) {
var target = event.detail && event.detail.target;
if (!target || !target.hasAttribute("data-capability-json-body")) return;
Expand Down Expand Up @@ -152,6 +190,7 @@
var control = preferred || fallback;
if (control) control.focus({ preventScroll: true });
}
expandNext(rootFor(target));
});

document.addEventListener("htmx:afterRequest", function (event) {
Expand All @@ -162,6 +201,8 @@
if (!(status >= 200 && status < 400)) {
var details = target.closest("details[data-capability-json-node]");
reset(target, !!(details && details.open));
var root = rootFor(target);
if (root) root.dataset.capabilityJsonExpandAll = "false";
}
});
})();
8 changes: 8 additions & 0 deletions crates/ui/e2e/pages/capability-statement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ export class CapabilityStatementPage {
return this.rawBody.locator("#capability-json-loading");
}

get collapseAllJson(): Locator {
return this.rawDisclosure.locator('[data-capability-json-fold="all"]');
}

get expandAllJson(): Locator {
return this.rawDisclosure.locator('[data-capability-json-fold="none"]');
}

get jsonView(): Locator {
return this.rawBody.locator("#capability-json");
}
Expand Down
27 changes: 23 additions & 4 deletions crates/ui/e2e/tests/capability-statement.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,19 @@ test("Capability Statement JSON expands in bounded reloadable fragments", async

await expect(capabilityStatement.jsonOutline).toBeVisible();
await expect(capabilityStatement.jsonOutline).toHaveAttribute("data-item-count", /\d+/);
await expect(capabilityStatement.collapseAllJson).toBeVisible();
await expect(capabilityStatement.expandAllJson).toBeVisible();
await expect(capabilityStatement.rawDisclosure).toHaveAttribute("open", "");
await expect(capabilityStatement.rawBody).toHaveAttribute("tabindex", "0");

const format = capabilityStatement.jsonNode(capabilityStatement.jsonOutline, "format");
const formatBody = capabilityStatement.nodeBody(format);
await capabilityStatement.expandAllJson.click();
await expect(format).toHaveAttribute("open", "");
await expect(formatBody.locator("[data-capability-json-page], .json-view").first()).toBeVisible();
await capabilityStatement.collapseAllJson.click();
await expect(format).not.toHaveAttribute("open", "");
await expect(capabilityStatement.rawDisclosure).toHaveAttribute("open", "");
await format.locator(":scope > summary").click();
await expect(formatBody.locator("[data-capability-json-page], .json-view").first()).toBeVisible();
await expect(capabilityStatement.rawBody).toHaveCSS("overflow-y", "auto");
Expand All @@ -109,10 +119,9 @@ test("Capability Statement JSON expands in bounded reloadable fragments", async
const rest = capabilityStatement.jsonNode(capabilityStatement.jsonOutline, "rest");
const restBody = capabilityStatement.nodeBody(rest);
// An in-flight request is aborted and its busy state cleared when the node
// closes. Opening this sibling also closes and unloads the format branch.
// closes. Opening this sibling leaves the format branch available.
await rest.locator(":scope > summary").click();
await expect(format).not.toHaveAttribute("open", "");
await expect(formatBody.locator("[data-capability-json-page], .json-view")).toHaveCount(0);
await expect(format).toHaveAttribute("open", "");
await expect(restBody).toHaveAttribute("aria-busy", "true");
await rest.locator(":scope > summary").click();
await expect(restBody).not.toHaveAttribute("aria-busy", "true");
Expand Down Expand Up @@ -171,6 +180,12 @@ test("Capability Statement JSON expands in bounded reloadable fragments", async
"data-item-count",
"100",
);
await expect
.poll(() => capabilityStatement.rawBody.evaluate((body) => body.scrollHeight > body.clientHeight))
.toBe(true);
await capabilityStatement.rawBody.focus();
await capabilityStatement.rawBody.press("PageDown");
await expect.poll(() => capabilityStatement.rawBody.evaluate((body) => body.scrollTop)).toBeGreaterThan(0);
const next = capabilityStatement.pageControl(resourceBody, "next");
await expect(next).toBeEnabled();
await next.click();
Expand All @@ -191,6 +206,11 @@ test("Capability Statement JSON expands in bounded reloadable fragments", async
expect(secondPageCount).toBeLessThanOrEqual(100);
expect(fragmentRequests.get("/rest/0/resource")).toBe(2);

await capabilityStatement.collapseAllJson.click();
await expect(format).not.toHaveAttribute("open", "");
await expect(rest).not.toHaveAttribute("open", "");
await expect(capabilityStatement.rawDisclosure).toHaveAttribute("open", "");

// Closing the top-level disclosure unloads the entire incremental tree.
await capabilityStatement.rawSummary.click();
await expect(capabilityStatement.rawDisclosure).not.toHaveAttribute("open", "");
Expand All @@ -200,7 +220,6 @@ test("Capability Statement JSON expands in bounded reloadable fragments", async
await expect(capabilityStatement.jsonOutline).toBeVisible();
expect(fragmentRequests.get("")).toBe(2);
});

test("typing filters resource capabilities live", async ({ capabilityStatement }) => {
await capabilityStatement.goto();
await expect(capabilityStatement.resourceRow("Patient")).toBeVisible();
Expand Down
2 changes: 2 additions & 0 deletions crates/ui/templates/pages/capability-statement.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
-#}
{% extends "layouts/base.html" %}

{% block content_class %} content--capability{% endblock %}

{% block title %}{{ i18n.t("cap-title") }} — {{ i18n.t("app-title") }}{% endblock %}

{% block content %}
Expand Down