FHIRFLARE-IG-Toolkit/templates/fhir_ui_operations.html.bak
Sudo-JHare 833bc12d4d Updates
UI, Core logic, FHIR UI
GOFSH integration
THEME FIXES
2025-04-19 01:05:14 +10:00

1788 lines
85 KiB
HTML

<style>
/* General Button Styles */
.btn-outline-dark-blue {
border-color: #007bff;
color: #007bff;
}
.btn-outline-dark-blue:hover {
background-color: #007bff;
color: #ffffff;
}
.btn-dark-blue {
background-color: #007bff;
border-color: #007bff;
color: #ffffff;
}
.btn-dark-blue.active {
background-color: #0056b3;
border-color: #0056b3;
}
.btn-outline-success {
border-color: #28a745;
color: #28a745;
}
.btn-outline-success:hover {
background-color: #28a745;
color: #ffffff;
}
.btn-success {
background-color: #28a745;
border-color: #28a745;
color: #ffffff;
}
.btn-success.active {
background-color: #218838;
border-color: #218838;
}
.try-out__btn {
background-color: #007bff;
color: #ffffff;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
}
.try-out__btn.cancel {
background-color: #dc3545;
}
.execute__btn {
background-color: #28a745;
color: #ffffff;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
margin-top: 15px;
display: none;
font-weight: bold;
}
.execute__btn:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
.copy-curl-btn, .copy-response-btn, .download-response-btn {
background-color: #6c757d;
color: #ffffff;
padding: 0.25rem 0.5rem;
font-size: 0.8em;
line-height: 1.5;
border-radius: 0.2rem;
border: none;
}
.copy-curl-btn:hover, .copy-response-btn:hover, .download-response-btn:hover {
background-color: #5a6268;
}
/* Dark Theme Button Overrides */
html[data-theme="dark"] .btn-outline-dark-blue {
border-color: #4dabf7;
color: #4dabf7;
}
html[data-theme="dark"] .btn-outline-dark-blue:hover {
background-color: #4dabf7;
color: #212529;
}
html[data-theme="dark"] .btn-dark-blue {
background-color: #4dabf7;
border-color: #4dabf7;
color: #212529;
}
html[data-theme="dark"] .btn-dark-blue.active {
background-color: #1a8cff;
border-color: #1a8cff;
}
html[data-theme="dark"] .btn-outline-success {
border-color: #2ecc71;
color: #2ecc71;
}
html[data-theme="dark"] .btn-outline-success:hover {
background-color: #2ecc71;
color: #212529;
}
html[data-theme="dark"] .btn-success {
background-color: #2ecc71;
border-color: #2ecc71;
color: #212529;
}
html[data-theme="dark"] .btn-success.active {
background-color: #27ae60;
border-color: #27ae60;
}
html[data-theme="dark"] .try-out__btn {
background-color: #4dabf7;
color: #212529;
}
html[data-theme="dark"] .try-out__btn.cancel {
background-color: #e74c3c;
color: #ffffff;
}
html[data-theme="dark"] .execute__btn {
background-color: #2ecc71;
color: #212529;
}
html[data-theme="dark"] .execute__btn:disabled {
background-color: #adb5bd;
}
html[data-theme="dark"] .copy-curl-btn,
html[data-theme="dark"] .copy-response-btn,
html[data-theme="dark"] .download-response-btn {
background-color: #adb5bd;
color: #212529;
}
html[data-theme="dark"] .copy-curl-btn:hover,
html[data-theme="dark"] .copy-response-btn:hover,
html[data-theme="dark"] .download-response-btn:hover {
background-color: #bdc3c7;
}
/* Operation Block */
.opblock {
margin-bottom: 10px;
border: 1px solid #dee2e6;
border-radius: 4px;
background: #fff;
}
.opblock-summary {
display: flex;
align-items: center;
padding: 10px;
cursor: pointer;
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
.opblock-summary:hover {
background: #e9ecef;
}
.opblock-summary-method {
width: 80px;
text-align: center;
color: white;
padding: 5px;
border-radius: 12px;
font-size: 0.9em;
margin-right: 10px;
}
.opblock-summary-method.get {
background: #61affe;
}
.opblock-summary-method.post {
background: #49cc90;
}
.opblock-summary-method.put {
background: #fca130;
}
.opblock-summary-method.delete {
background: #f93e3e;
}
.opblock-summary-method.patch {
background: #50e3c2;
}
.opblock-summary-path {
flex-grow: 1;
font-family: monospace;
font-size: 1em;
margin-right: 10px;
word-break: break-all;
}
.opblock-summary-description {
flex-grow: 2;
font-size: 0.9em;
color: #333;
margin-right: 10px;
}
.opblock-body {
padding: 15px;
display: none;
border-top: 1px solid #dee2e6;
}
.opblock.is-open .opblock-body {
display: block;
}
.opblock-section {
margin-bottom: 20px;
}
.opblock-section:last-child {
margin-bottom: 0;
}
.opblock-section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
border-bottom: 1px solid #eee;
padding-bottom: 5px;
}
.opblock-title {
font-size: 1.1em;
font-weight: bold;
margin: 0;
}
.arrow {
width: 20px;
height: 20px;
transition: transform 0.2s;
margin-left: auto;
}
.opblock.is-open .arrow {
transform: rotate(180deg);
}
/* Parameters */
.parameters-container {
margin-bottom: 15px;
}
.parameters-table {
width: 100%;
border-collapse: collapse;
}
.parameters-table th, .parameters-table td {
border: 1px solid #dee2e6;
padding: 8px;
text-align: left;
vertical-align: top;
}
.parameters-col_name {
width: 20%;
font-weight: bold;
}
.parameters-col_description {
width: 80%;
}
.parameters-col_description input[type="text"],
.parameters-col_description input[type="number"] {
width: 100%;
padding: 5px;
border: 1px solid #ced4da;
border-radius: 4px;
box-sizing: border-box;
}
.parameters-col_description input.is-invalid {
border-color: #dc3545;
}
.parameter__name.required span {
color: red;
margin-left: 2px;
font-weight: bold;
}
.parameter__type {
font-size: 0.9em;
color: #6c757d;
}
.parameter__in {
font-size: 0.8em;
color: #6c757d;
}
.renderedMarkdown p {
margin-bottom: 0.5rem;
}
.renderedMarkdown p:last-child {
margin-bottom: 0;
}
/* Request Body */
.request-body-textarea {
width: 100%;
min-height: 100px;
padding: 10px;
border: 1px solid #ced4da;
border-radius: 4px;
font-family: monospace;
box-sizing: border-box;
margin-bottom: 10px;
}
.content-type-wrapper label.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.content-type-wrapper select {
padding: 0.3rem 0.6rem;
font-size: 0.9em;
border-radius: 4px;
border: 1px solid #ced4da;
}
/* Responses */
.responses-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
.responses-table th, .responses-table td {
border: 1px solid #dee2e6;
padding: 8px;
text-align: left;
}
.response-col_status {
width: 100px;
font-weight: bold;
}
.response-control-media-type {
margin: 10px 0;
padding: 5px;
background-color: #f8f9fa;
border-radius: 4px;
}
.response-control-media-type__title {
font-weight: bold;
margin-right: 5px;
}
.response-control-media-type__accept-message {
font-size: 0.85em;
color: #6c757d;
margin-left: 10px;
}
.model-example {
margin-top: 10px;
}
.tab {
display: flex;
gap: 5px;
margin-bottom: 10px;
}
.tablinks.badge {
background: #dee2e6;
color: #333;
padding: 5px 10px;
border-radius: 12px;
cursor: pointer;
font-size: 0.9em;
border: none;
}
.tablinks.badge.active {
background: #007bff;
color: #ffffff;
}
.example-panel, .schema-panel {
border: 1px solid #eee;
border-radius: 4px;
margin-top: -1px;
}
/* Dark Theme Non-Button Overrides */
html[data-theme="dark"] .opblock {
background: #343a40;
border-color: #495057;
}
html[data-theme="dark"] .opblock-summary {
background: #495057;
border-bottom-color: #6c757d;
}
html[data-theme="dark"] .opblock-summary:hover {
background: #6c757d;
}
html[data-theme="dark"] .opblock-summary-description {
color: #f8f9fa;
}
html[data-theme="dark"] .opblock-body {
border-top-color: #6c757d;
}
html[data-theme="dark"] .parameters-table th,
html[data-theme="dark"] .parameters-table td {
border-color: #6c757d;
}
html[data-theme="dark"] .parameters-col_description input[type="text"],
html[data-theme="dark"] .parameters-col_description input[type="number"] {
background-color: #495057;
color: #f8f9fa;
border-color: #6c757d;
}
html[data-theme="dark"] .parameters-col_description input.is-invalid {
border-color: #e74c3c;
}
html[data-theme="dark"] .parameter__type,
html[data-theme="dark"] .parameter__in {
color: #adb5bd;
}
html[data-theme="dark"] .request-body-textarea {
background-color: #495057;
color: #f8f9fa;
border-color: #6c757d;
}
html[data-theme="dark"] .content-type-wrapper select {
background-color: #495057;
color: #f8f9fa;
border-color: #6c757d;
}
html[data-theme="dark"] .responses-table th,
html[data-theme="dark"] .responses-table td {
border-color: #6c757d;
}
html[data-theme="dark"] .response-control-media-type {
background-color: #495057;
}
html[data-theme="dark"] .response-control-media-type__accept-message {
color: #adb5bd;
}
html[data-theme="dark"] .tablinks.badge {
background: #6c757d;
color: #f8f9fa;
}
html[data-theme="dark"] .tablinks.badge.active {
background: #4dabf7;
color: #212529;
}
html[data-theme="dark"] .example-panel,
html[data-theme="dark"] .schema-panel {
border-color: #6c757d;
}
html[data-theme="dark"] .highlight-code {
background: #2d3436;
color: #f8f9fa;
}
html[data-theme="dark"] .execute-wrapper {
border-top-color: #6c757d;
}
html[data-theme="dark"] .execute-wrapper pre.response-output-content {
background: #495057;
color: #f8f9fa;
border-color: #6c757d;
}
html[data-theme="dark"] .execute-wrapper div.response-output-narrative {
background: #343a40;
color: #f8f9fa;
border-color: #6c757d;
}
html[data-theme="dark"] .request-url-output,
html[data-theme="dark"] .curl-output {
background: #495057;
color: #f8f9fa;
border-color: #6c757d;
}
/* Code Highlighting */
.highlight-code {
background: #333;
color: #f8f8f2;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
font-family: monospace;
margin: 0;
}
.highlight-code pre {
margin: 0;
padding: 0;
background: transparent;
border: none;
font-family: inherit;
font-size: inherit;
color: inherit;
}
/* Execution Response */
.execute-wrapper {
margin-top: 20px;
padding-top: 15px;
border-top: 1px dashed #ccc;
}
.execute-wrapper .opblock-section-header {
border-bottom: none;
margin-bottom: 5px;
padding-bottom: 0;
}
.execute-wrapper .response-format-select {
padding: 0.3rem 0.6rem;
font-size: 0.9em;
border-radius: 4px;
border: 1px solid #ced4da;
}
.execute-wrapper pre.response-output-content {
background: #f8f9fa;
color: #212529;
border: 1px solid #dee2e6;
padding: 10px;
border-radius: 4px;
max-height: 400px;
overflow: auto;
font-family: monospace;
white-space: pre-wrap;
word-break: break-all;
margin-top: 5px;
}
.execute-wrapper div.response-output-narrative {
display: none;
border: 1px solid #dee2e6;
padding: 15px;
border-radius: 4px;
margin-top: 5px;
max-height: 400px;
overflow: auto;
background: #ffffff;
color: #212529;
line-height: 1.5;
}
.execute-wrapper .response-status {
margin-top: 8px;
font-weight: bold;
}
/* Request URL, Curl, Controls */
.request-url-section, .curl-section {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.request-url-section:last-of-type, .curl-section:last-of-type {
border-bottom: none;
}
.request-url-output, .curl-output {
background: #e9ecef;
color: #212529;
padding: 10px 15px;
border: 1px solid #dee2e6;
border-radius: 4px;
font-family: monospace;
font-size: 0.9em;
white-space: pre-wrap;
word-break: break-all;
margin-top: 5px;
}
.request-url-output code, .curl-output code {
font-family: inherit;
}
.curl-section .opblock-section-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: none;
padding-bottom: 0;
margin-bottom: 5px;
}
.copy-curl-btn i, .copy-response-btn i, .download-response-btn i {
margin-right: 4px;
vertical-align: text-bottom;
}
.execute-wrapper .response-controls {
display: flex;
align-items: center;
gap: 5px;
margin-left: auto;
}
.execute-wrapper .opblock-section-header h4 {
margin-right: 10px;
}
.opblock-section h5.opblock-title {
font-weight: bold;
margin: 0;
font-size: 1rem;
}
</style>
<div class="px-4 py-5 my-5 text-center">
<img class="d-block mx-auto mb-4" src="{{ url_for('static', filename='FHIRFLARE.png') }}" alt="FHIRFLARE IG Toolkit" width="192" height="192">
<h1 class="display-5 fw-bold text-body-emphasis">FHIR UI Operations</h1>
<div class="col-lg-6 mx-auto">
<p class="lead mb-4">
Explore FHIR server operations by selecting resource types or system operations. Toggle between local HAPI or a custom server to interact with FHIR metadata, resources, and server-wide operations.
</p>
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
<a href="{{ url_for('index') }}" class="btn btn-primary btn-lg px-4 gap-3">Back to Home</a>
<a href="{{ url_for('fhir_ui') }}" class="btn btn-outline-secondary btn-lg px-4">FHIR API Explorer</a>
<a href="{{ url_for('validate_sample') }}" class="btn btn-outline-secondary btn-lg px-4">Validate FHIR Sample</a>
</div>
</div>
</div>
<div class="container mt-4">
<div class="card">
<div class="card-header"><i class="fas fa-cogs me-2"></i>FHIR Operations Configuration</div>
<div class="card-body">
<form id="fhirOperationsForm">
{{ form.hidden_tag() }}
<div class="mb-3">
<label class="form-label fw-bold">FHIR Server</label>
<div class="input-group">
<button type="button" class="btn btn-outline-primary" id="toggleServer">
<span id="toggleLabel">Use Local HAPI</span>
</button>
<input type="text" class="form-control" id="fhirServerUrl" name="fhir_server_url" placeholder="Enter FHIR Base URL e.g., https://hapi.fhir.org/baseR4" style="display: none;" aria-describedby="fhirServerHelp">
</div>
<small id="fhirServerHelp" class="form-text text-muted">Toggle to use local HAPI (/fhir proxy) or enter a custom FHIR server URL.</small>
</div>
<button type="button" class="btn btn-primary mb-3" id="fetchMetadata">Fetch Metadata</button>
</form>
<div class="banner3 mt-3" id="resourceTypes" style="display: none;">
<h5>Resource Types and Operations</h5>
<div class="d-flex flex-wrap gap-2" id="resourceButtons"></div>
</div>
<div id="swagger-ui" class="mt-4" style="display: none;">
<h5>Queries for <span id="selectedResource" class="fw-bold">Selected Resource</span></h5>
<div id="queryList"></div>
</div>
</div>
</div>
</div>
<script>
// --- Utility Functions ---
function escapeXml(unsafe) {
if (typeof unsafe !== 'string') return String(unsafe);
return unsafe.replace(/[<>&'"]/g, c => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', "'": '&apos;', '"': '&quot;' })[c] || c);
}
function jsonToFhirXml(json, indentLevel = 0) {
const PADDING = ' ';
const currentIndent = PADDING.repeat(indentLevel);
const nextIndent = PADDING.repeat(indentLevel + 1);
try {
const obj = typeof json === 'string' ? JSON.parse(json) : json;
if (!obj || typeof obj !== 'object') return `${currentIndent}<error>Invalid input: Not an object</error>`;
if (!Object.keys(obj).length) return `${currentIndent}<OperationOutcome xmlns="http://hl7.org/fhir">\n${nextIndent}<issue>\n${PADDING}${nextIndent}<severity value="information"/>\n${PADDING}${nextIndent}<code value="informational"/>\n${PADDING}${nextIndent}<diagnostics value="Successful operation with no content"/>\n${nextIndent}</issue>\n${currentIndent}</OperationOutcome>`;
if (!obj.resourceType) {
obj.resourceType = (obj.type && obj.entry) ? 'Bundle' : null;
if (!obj.resourceType) return `${currentIndent}<error>Invalid FHIR resource: Missing resourceType</error>`;
}
let xml = `${currentIndent}<${obj.resourceType} xmlns="http://hl7.org/fhir">\n`;
function buildXmlRecursive(currentObj, level) {
let innerXml = '';
const itemIndent = PADDING.repeat(level);
const childLevel = level + 1;
for (const key in currentObj) {
if (key === 'xmlns' || key === 'resourceType') continue;
const value = currentObj[key];
if (value === null || value === undefined) continue;
if (Array.isArray(value)) {
value.forEach(item => {
if (typeof item === 'object' && item !== null) {
innerXml += `${itemIndent}<${key}>\n${buildXmlRecursive(item, childLevel)}${itemIndent}</${key}>\n`;
} else {
innerXml += `${itemIndent}<${key} value="${escapeXml(item)}"/>\n`;
}
});
} else if (typeof value === 'object') {
innerXml += `${itemIndent}<${key}>\n${buildXmlRecursive(value, childLevel)}${itemIndent}</${key}>\n`;
} else {
if (key === 'div' && typeof value === 'string' && value.trim().startsWith('<div')) {
innerXml += `${itemIndent}<${key} xmlns="http://www.w3.org/1999/xhtml">${value}</${key}>\n`;
} else {
innerXml += `${itemIndent}<${key} value="${escapeXml(value)}"/>\n`;
}
}
}
return innerXml;
}
xml += buildXmlRecursive(obj, indentLevel + 1);
xml += `${currentIndent}</${obj.resourceType}>`;
return xml;
} catch (e) {
console.error("JSON to XML conversion error:", e);
return `${currentIndent}<error xmlns="http://local/error">\n${nextIndent}<message>Failed to convert to XML</message>\n${nextIndent}<detail>${escapeXml(e.message)}</detail>\n${currentIndent}</error>`;
}
}
function xmlToFhirJson(xmlString) {
try {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlString, "application/xml");
const parseError = xmlDoc.querySelector("parsererror");
if (parseError) {
console.error("XML Parsing Error:", parseError.textContent);
return JSON.stringify({ error: "Failed to parse XML", details: parseError.textContent }, null, 2);
}
const root = xmlDoc.documentElement;
if (!root) return JSON.stringify({ error: "No root element found in XML" }, null, 2);
const jsonResult = { resourceType: root.tagName };
function buildJsonFromNode(node) {
const nodeObj = {};
let textContent = "";
for (const child of node.childNodes) {
if (child.nodeType === Node.ELEMENT_NODE) {
const tag = child.tagName;
const childValue = buildJsonFromNode(child);
if (nodeObj.hasOwnProperty(tag)) {
if (!Array.isArray(nodeObj[tag])) nodeObj[tag] = [nodeObj[tag]];
nodeObj[tag].push(childValue);
} else {
nodeObj[tag] = childValue;
}
} else if (child.nodeType === Node.TEXT_NODE) {
textContent += child.nodeValue.trim();
}
}
if (Object.keys(nodeObj).length) return nodeObj;
if (node.hasAttribute('value')) return node.getAttribute('value');
if (textContent) return textContent;
if (node.nodeType === Node.ELEMENT_NODE && !node.childNodes.length && !node.attributes.length) return null;
return {};
}
const rootContent = buildJsonFromNode(root);
if (typeof rootContent === 'object' && rootContent !== null) Object.assign(jsonResult, rootContent);
else jsonResult._value = rootContent;
const idNode = root.querySelector(':scope > id[value]');
if (idNode) jsonResult.id = idNode.getAttribute('value');
const textNode = root.querySelector(':scope > text');
if (textNode) {
jsonResult.text = {};
const statusNode = textNode.querySelector(':scope > status[value]');
if (statusNode) jsonResult.text.status = statusNode.getAttribute('value');
const divNode = textNode.querySelector(':scope > div[xmlns="http://www.w3.org/1999/xhtml"]');
if (divNode) jsonResult.text.div = divNode.innerHTML;
}
return JSON.stringify(jsonResult, null, 2);
} catch (e) {
console.error("XML to JSON conversion error:", e);
return JSON.stringify({ error: `Failed to convert XML to JSON: ${e.message}` }, null, 2);
}
}
async function copyToClipboard(text, buttonElement) {
if (!navigator.clipboard) {
alert('Clipboard API not available.');
return;
}
try {
await navigator.clipboard.writeText(text);
const originalHtml = buttonElement.innerHTML;
buttonElement.innerHTML = `<i class="fas fa-check"></i> Copied!`;
buttonElement.disabled = true;
setTimeout(() => {
buttonElement.innerHTML = originalHtml;
buttonElement.disabled = false;
}, 1500);
} catch (err) {
alert(`Failed to copy text: ${err}`);
}
}
function downloadFile(content, filename, mimeType = 'application/octet-stream') {
try {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = filename;
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
URL.revokeObjectURL(url);
} catch (error) {
alert(`Failed to initiate download: ${error}`);
}
}
function generateCurlCommand(method, url, headers, body) {
let curl = `curl -X ${method.toUpperCase()} '${url}'`;
for (const key in headers) {
const lowerKey = key.toLowerCase();
if (['user-agent', 'connection', 'host', 'origin', 'referer', 'x-csrftoken'].includes(lowerKey)) continue;
const value = (headers[key] || '').replace(/'/g, "'\\''");
curl += ` \\\n -H '${key}: ${value}'`;
}
if (body !== undefined && body !== null && body !== '') {
const escapedBody = body.replace(/'/g, "'\\''");
const contentType = headers['Content-Type'] || '';
const dataFlag = contentType.includes('application/x-www-form-urlencoded') ? '-d' : '--data-binary';
curl += ` \\\n ${dataFlag} '${escapedBody}'`;
}
return curl;
}
// --- Main Application Logic ---
document.addEventListener('DOMContentLoaded', () => {
// --- DOM Element References ---
const fhirOperationsForm = document.getElementById('fhirOperationsForm');
const fetchMetadataButton = document.getElementById('fetchMetadata');
const toggleServerButton = document.getElementById('toggleServer');
const toggleLabel = document.getElementById('toggleLabel');
const fhirServerUrlInput = document.getElementById('fhirServerUrl');
const resourceTypesDisplayDiv = document.getElementById('resourceTypes');
const resourceButtonsContainer = document.getElementById('resourceButtons');
const swaggerUiContainer = document.getElementById('swagger-ui');
const selectedResourceSpan = document.getElementById('selectedResource');
const queryListContainer = document.getElementById('queryList');
// --- State Variables ---
let isUsingLocalHapi = true; // Default state
let currentSelectedResource = '';
let availableSystemOperations = [];
let fetchedMetadataCache = null;
// --- Helper Function to Update Toggle Button/Input UI ---
function updateServerToggleUI() {
if (!toggleLabel || !fhirServerUrlInput) {
console.error("Toggle UI elements not found!");
return;
}
toggleLabel.textContent = isUsingLocalHapi ? 'Use Local HAPI' : 'Use Custom URL';
fhirServerUrlInput.style.display = isUsingLocalHapi ? 'none' : 'block';
// Ensure input is cleared *only* when switching TO local
// No clearing needed here, handled in toggleServerSelection if needed
fhirServerUrlInput.classList.remove('is-invalid');
}
// --- Server Toggle Functionality ---
function toggleServerSelection() {
isUsingLocalHapi = !isUsingLocalHapi; // Flip the state first
updateServerToggleUI(); // Update the UI based on the new state
// Clear custom URL input value if we just switched TO local HAPI
if (isUsingLocalHapi) {
fhirServerUrlInput.value = '';
}
// Reset application state dependent on the server
if (resourceTypesDisplayDiv) resourceTypesDisplayDiv.style.display = 'none';
if (swaggerUiContainer) swaggerUiContainer.style.display = 'none';
fetchedMetadataCache = null; // Invalidate metadata cache
availableSystemOperations = []; // Clear parsed operations
console.log(`Server toggled: ${isUsingLocalHapi ? 'Local HAPI' : 'Custom URL'}`);
}
// Add event listener to the toggle button
if (toggleServerButton) {
toggleServerButton.addEventListener('click', toggleServerSelection);
} else {
console.error("Toggle server button (#toggleServer) not found!");
}
// --- Generate Query Examples ---
function getQueryExamplesForResourceType(resourceType) {
if (!fetchedMetadataCache?.rest?.[0]) return [];
const restSection = fetchedMetadataCache.rest[0];
const resourceMetadata = restSection.resource?.find(r => r.type === resourceType);
const supportedInteractions = resourceMetadata?.interaction?.map(i => i.code) || [];
const resourceSpecificOperations = resourceMetadata?.operation || [];
const allSearchParameters = resourceMetadata?.searchParam || [];
const queries = [];
const formatParam = { name: '_format', in: 'query', type: 'string', required: false, description: 'Response format (e.g., json, xml)', example: 'json' };
const idPathParam = { name: 'id', in: 'path', type: 'string', required: true, description: 'Resource logical ID', example: 'example' };
const versionIdPathParam = { name: 'version_id', in: 'path', type: 'string', required: true, description: 'Version ID', example: '1' };
const countQueryParam = { name: '_count', in: 'query', type: 'integer', required: false, description: 'Results per page', example: '10' };
const idQueryParam = { name: '_id', in: 'query', type: 'string', required: false, description: 'Search by ID', example: 'example' };
const lastUpdatedQueryParam = { name: '_lastUpdated', in: 'query', type: 'date', required: false, description: 'Last updated date', example: 'ge2024-01-01' };
const commonSearchParams = [idQueryParam, lastUpdatedQueryParam];
const specificSearchParams = allSearchParameters.filter(sp => !['_id', '_lastUpdated'].includes(sp.name)).slice(0, 3).map(sp => ({ name: sp.name, in: 'query', type: sp.type, required: false, description: sp.documentation || `Search by ${sp.name} (${sp.type})`, example: `${sp.type}-example` }));
const searchGetParams = [countQueryParam, formatParam, ...commonSearchParams, ...specificSearchParams];
const searchPostParams = [countQueryParam, formatParam];
const baseExample = { resourceType, id: "example", text: { status: "generated", div: `<div xmlns="http://www.w3.org/1999/xhtml">Example ${resourceType}</div>` } };
const baseExampleString = JSON.stringify(baseExample, null, 2);
const createExampleString = JSON.stringify({ ...baseExample, id: undefined }, null, 2);
const baseSchema = { resourceType, id: "string", meta: { versionId: "string", lastUpdated: "instant" }, text: { status: "code", div: "xhtml" } };
const bundleSchema = { resourceType: "Bundle", type: "code", total: "unsignedInt", link: [{ relation: "string", url: "uri" }], entry: [{ resource: baseSchema }] };
const paramsSchema = { resourceType: "Parameters", parameter: [{ name: "string", valueString: "string" }] };
const outcomeSchema = { resourceType: "OperationOutcome", issue: [{ severity: "code", code: "code", diagnostics: "string" }] };
const bundleSearchExample = JSON.stringify({ resourceType: "Bundle", type: "searchset", total: 1, entry: [{ resource: baseExample }] }, null, 2);
const bundleHistoryExample = JSON.stringify({ resourceType: "Bundle", type: "history", total: 1, entry: [{ resource: baseExample }] }, null, 2);
const patchExample = JSON.stringify({ resourceType: "Parameters", parameter: [{ name: "operation", part: [{ name: 'type', valueCode: 'replace' }, { name: 'path', valueString: `${resourceType}.active` }, { name: 'value', valueBoolean: false }] }] }, null, 2);
const deleteExample = JSON.stringify({ resourceType: "OperationOutcome", issue: [{ severity: "information", code: "informational", diagnostics: "Successful deletion" }] }, null, 2);
const postSearchExample = searchGetParams.filter(p => p.in === 'query' && p.example).map(p => `${p.name}=${encodeURIComponent(p.example)}`).join('&');
const interactionMap = {
'read': { label: `Read ${resourceType}`, method: 'GET', path: `${resourceType}/:id`, description: `Read a ${resourceType} by ID`, parameters: [idPathParam, formatParam], schema: baseSchema, example: baseExampleString },
'vread': { label: `VRead ${resourceType}`, method: 'GET', path: `${resourceType}/:id/_history/:version_id`, description: `Read specific version`, parameters: [idPathParam, versionIdPathParam, formatParam], schema: baseSchema, example: JSON.stringify({ ...baseExample, meta: { versionId: "1" } }, null, 2) },
'update': { label: `Update ${resourceType}`, method: 'PUT', path: `${resourceType}/:id`, description: `Update ${resourceType} with ID`, parameters: [idPathParam, formatParam], requestBody: true, schema: baseSchema, example: baseExampleString },
'patch': { label: `Patch ${resourceType}`, method: 'PATCH', path: `${resourceType}/:id`, description: `Patch ${resourceType} by ID`, parameters: [idPathParam, formatParam], requestBody: true, schema: paramsSchema, example: patchExample },
'delete': { label: `Delete ${resourceType}`, method: 'DELETE', path: `${resourceType}/:id`, description: `Delete ${resourceType}`, parameters: [idPathParam, formatParam], schema: outcomeSchema, example: deleteExample },
'create': { label: `Create ${resourceType}`, method: 'POST', path: `${resourceType}`, description: `Create new ${resourceType}`, parameters: [formatParam], requestBody: true, schema: baseSchema, example: createExampleString },
'search-type': { label: `Search ${resourceType} (GET)`, method: 'GET', path: `${resourceType}`, description: `Search ${resourceType} using GET`, parameters: searchGetParams, schema: bundleSchema, example: bundleSearchExample },
'history-type': { label: `Type History ${resourceType}`, method: 'GET', path: `${resourceType}/_history`, description: `History for all ${resourceType}`, parameters: [countQueryParam, formatParam, lastUpdatedQueryParam], schema: bundleSchema, example: bundleHistoryExample },
'history-instance': { label: `Instance History ${resourceType}`, method: 'GET', path: `${resourceType}/:id/_history`, description: `History for specific ${resourceType}`, parameters: [idPathParam, countQueryParam, formatParam, lastUpdatedQueryParam], schema: bundleSchema, example: bundleHistoryExample },
};
supportedInteractions.forEach(code => { if (interactionMap[code]) queries.push(interactionMap[code]); });
if (supportedInteractions.includes('search-type')) { queries.push({ label: `Search ${resourceType} (POST)`, method: 'POST', path: `${resourceType}/_search`, description: `Search ${resourceType} using POST`, parameters: searchPostParams, requestBody: true, schema: bundleSchema, example: postSearchExample }); }
resourceSpecificOperations.forEach(opDef => { const opName = opDef.name.startsWith('$') ? opDef.name : `$${opDef.name}`; const opDoc = opDef.documentation || `Execute ${opName} on ${resourceType}`; const opExample = JSON.stringify({ resourceType: "Parameters", parameter: [{ name: "example-param", valueString: "example-value" }] }, null, 2); const opMethod = opDef.definition?.toLowerCase().includes('get') ? 'GET' : 'POST'; const isInstanceLevel = opDef.definition?.toLowerCase().includes('/{id}/$'); queries.push({ label: `${opName} (${isInstanceLevel ? 'Instance' : 'Type'})`, method: opMethod, path: isInstanceLevel ? `${resourceType}/:id/${opName}` : `${resourceType}/${opName}`, description: opDoc, parameters: isInstanceLevel ? [idPathParam, formatParam] : [formatParam], requestBody: opMethod !== 'GET', schema: paramsSchema, example: opExample }); });
availableSystemOperations.forEach(op => { const opName = op.name.startsWith('$') ? op.name : `$${op.name}`; const opParams = [formatParam]; const opExample = JSON.stringify({ resourceType: "Parameters", parameter: [{ name: "info", valueString: `Input for ${opName}` }] }, null, 2); if (op.levels.includes('type')) { op.methods.forEach(method => { queries.push({ label: `${opName} (Type)`, method, path: `${resourceType}/${opName}`, description: op.documentation || `Type-level ${opName} on ${resourceType}`, parameters: opParams, requestBody: method !== 'GET', schema: paramsSchema, example: opExample }); }); } if (op.levels.includes('instance')) { op.methods.forEach(method => { queries.push({ label: `${opName} (Instance)`, method, path: `${resourceType}/:id/${opName}`, description: op.documentation || `Instance-level ${opName} on ${resourceType}`, parameters: [idPathParam, ...opParams], requestBody: method !== 'GET', schema: paramsSchema, example: opExample }); }); } });
return queries.filter((q, i, arr) => i === arr.findIndex(x => x.path === q.path && x.method === q.method));
}
// --- Generate System-Level Queries ---
function getSystemLevelQueries() {
if (!fetchedMetadataCache?.rest?.[0]) return [];
const restSection = fetchedMetadataCache.rest[0];
const supportedInteractions = restSection.interaction?.map(i => i.code) || [];
const queries = [];
const addedPaths = new Set(); // Track METHOD + PATH to avoid duplicates
const formatParam = { name: '_format', in: 'query', type: 'string', required: false, description: 'Response format', example: 'json' };
const countParam = { name: '_count', in: 'query', type: 'integer', required: false, description: 'Results per page', example: '10' };
const sinceParam = { name: '_since', in: 'query', type: 'instant', required: false, description: 'Changes after this time', example: new Date().toISOString() };
const contentParam = { name: '_content', in: 'query', type: 'string', required: false, description: 'Search content', example: 'example' };
const paramsSchema = { resourceType: "Parameters" };
const paramsExample = JSON.stringify({ resourceType: "Parameters", parameter: [] }, null, 2);
// --- Add Metadata (Capabilities) Operation ---
const metadataPath = 'metadata';
const metadataKey = `GET/${metadataPath}`;
if (!addedPaths.has(metadataKey)) {
queries.push({
name: 'metadata',
label: 'GET /metadata',
method: 'GET',
path: metadataPath,
description: 'server-capabilities: Fetch the server FHIR CapabilityStatement',
parameters: [formatParam],
schema: { resourceType: "CapabilityStatement" },
example: JSON.stringify(fetchedMetadataCache || { resourceType: "CapabilityStatement" }, null, 2)
});
addedPaths.add(metadataKey);
}
// --- Add Other Standard System Interactions ---
if (supportedInteractions.includes('transaction') || supportedInteractions.includes('batch')) {
const path = '';
const key = `POST/${path}`;
if (!addedPaths.has(key)) {
queries.push({
name: 'server-transaction',
label: 'POST /',
method: 'POST',
path: path,
description: 'server-transaction: Execute a FHIR Transaction (or FHIR Batch) Bundle',
parameters: [formatParam],
requestBody: true,
schema: { resourceType: "Bundle" },
example: JSON.stringify({ resourceType: "Bundle", type: "transaction", entry: [] }, null, 2)
});
addedPaths.add(key);
}
}
if (supportedInteractions.includes('history-system')) {
const path = '_history';
const key = `GET/${path}`;
if (!addedPaths.has(key)) {
queries.push({
name: 'server-history',
label: 'GET /_history',
method: 'GET',
path: path,
description: 'server-history: Fetch the resource change history across all resource types on the server',
parameters: [formatParam, countParam, sinceParam],
schema: { resourceType: "Bundle" },
example: JSON.stringify({ resourceType: "Bundle", type: "history" }, null, 2)
});
addedPaths.add(key);
}
}
if (supportedInteractions.includes('search-system')) {
const path = '';
const key = `GET/${path}`;
if (!addedPaths.has(key)) {
queries.push({
name: 'search-system',
label: 'GET /',
method: 'GET',
path: path,
description: 'Search across all resource types (limited support usually)',
parameters: [formatParam, countParam, contentParam],
schema: { resourceType: "Bundle" },
example: JSON.stringify({ resourceType: "Bundle", type: "searchset" }, null, 2)
});
addedPaths.add(key);
}
}
(restSection.operation || []).forEach(op => {
const opName = op.name;
const opPath = opName.startsWith('$') ? opName : `$${opName}`;
const defUrl = op.definition || '';
const doc = op.documentation || `Execute ${opName} at system level`;
if (opName === 'metadata' || opName === 'capabilities') return;
let methods = [];
if (['diff', 'meta', 'get-resource-counts'].includes(opName)) {
methods = ['GET', 'POST'];
} else if (['reindex', 'perform-reindexing-pass', 'mark-all-resources-for-reindexing', 'expunge', 'reindex-terminology', 'hapi.fhir.replace-references'].includes(opName)) {
methods = ['POST'];
} else {
methods = ['POST'];
if (defUrl.toLowerCase().includes('get')) methods.push('GET');
}
methods.forEach(method => {
const key = `${method}/${opPath}`;
if (!addedPaths.has(key)) {
let label = `${method} /${opPath}`;
let description = doc;
if (opName === 'diff') {
description = 'Compare two resources or two versions of a single resource';
} else if (opName === 'get-resource-counts') {
description = 'Provides the number of resources currently stored on the server, broken down by resource type';
} else if (opName === 'hapi.fhir.replace-references') {
description = 'Repoints referencing resources to another resource instance';
} else {
description = `${method}: /${opPath}`;
}
queries.push({
name: opName,
label: label,
method: method,
path: opPath,
description: description,
parameters: [formatParam],
requestBody: method !== 'GET',
schema: paramsSchema,
example: paramsExample
});
addedPaths.add(key);
}
});
});
return queries;
}
// --- Update Query List UI ---
function updateQueryListUI(resourceType) {
currentSelectedResource = resourceType;
selectedResourceSpan.textContent = resourceType === 'System' ? 'System Operations' : resourceType;
queryListContainer.innerHTML = '';
const queries = resourceType === 'System' ? getSystemLevelQueries() : getQueryExamplesForResourceType(resourceType);
if (!queries || !queries.length) {
queryListContainer.innerHTML = '<p>No operations defined or supported for this type.</p>';
swaggerUiContainer.style.display = 'block';
return;
}
queries.sort((a, b) => {
const pathA = a.path || '';
const pathB = b.path || '';
if (pathA < pathB) return -1;
if (pathA > pathB) return 1;
return a.method < b.method ? -1 : a.method > b.method ? 1 : 0;
}).forEach((query, i) => {
const blockId = `opblock-${i}`;
const block = document.createElement('div');
block.id = blockId;
block.className = `opblock opblock-${query.method.toLowerCase()}`;
block.dataset.queryIndex = i;
block.dataset.queryData = JSON.stringify(query);
block.innerHTML = `
<div class="opblock-summary">
<span class="opblock-summary-method ${query.method.toLowerCase()}">${query.method}</span>
<span class="opblock-summary-path">${query.path}</span>
<span class="opblock-summary-description">${query.description}</span>
<svg class="arrow" viewBox="0 0 20 20"><path fill="currentColor" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"/></svg>
</div>
<div class="opblock-body">
<div class="opblock-section parameters-section">
<div class="opblock-section-header">
<h4 class="opblock-title">Parameters</h4>
<div class="try-out">
<button class="btn btn-sm try-out__btn">Try it out</button>
</div>
</div>
<div class="parameters-container">
<table class="parameters-table">
<thead>
<tr>
<th class="parameters-col_name">Name</th>
<th class="parameters-col_description">Description</th>
</tr>
</thead>
<tbody>
${query.parameters && query.parameters.length ? query.parameters.map(p => `
<tr data-param-name="${p.name}" data-param-in="${p.in}">
<td class="parameters-col_name">
<div class="parameter__name ${p.required ? 'required' : ''}">${p.name}${p.required ? '<span>*</span>' : ''}</div>
<div class="parameter__type">${p.type}</div>
<div class="parameter__in">(${p.in})</div>
</td>
<td class="parameters-col_description">
<div class="renderedMarkdown"><p>${p.description || 'No description'}</p></div>
${p.example ? `<div class="renderedMarkdown"><p><i>Example:</i> ${p.example}</p></div>` : ''}
<input type="${p.type === 'integer' ? 'number' : 'text'}" placeholder="${p.example || p.name}" data-example="${p.example || ''}" value="" aria-label="${p.name} parameter value" disabled>
</td>
</tr>
`).join('') : `
<tr>
<td colspan="2"><div class="renderedMarkdown"><p>No parameters defined.</p></div></td>
</tr>
`}
</tbody>
</table>
</div>
</div>
${query.requestBody ? `
<div class="opblock-section request-body-section">
<div class="opblock-section-header">
<h4 class="opblock-title">Request Body</h4>
<div class="content-type-wrapper">
<label for="${blockId}-request-content-type" class="visually-hidden">Request Content Type</label>
<select id="${blockId}-request-content-type" class="content-type request-content-type" aria-label="Request body content type" disabled>
<option value="application/fhir+json" selected>application/fhir+json</option>
<option value="application/fhir+xml">application/fhir+xml</option>
${query.method === 'POST' && query.path.endsWith('_search') ? '<option value="application/x-www-form-urlencoded">application/x-www-form-urlencoded</option>' : ''}
</select>
</div>
</div>
<textarea class="request-body-textarea" placeholder="Enter request body..." aria-label="Request body input" disabled>${(query.method === 'POST' && query.path.endsWith('_search')) ? query.example : ''}</textarea>
<div class="model-example request-body-example" style="margin-top: 10px; ${(query.method === 'POST' && query.path.endsWith('_search')) ? 'display: none;' : ''}">
<ul class="tab" role="tablist">
<li class="tabitem active"><button class="tablinks badge active" data-name="example">Example Body</button></li>
</ul>
<div class="example-panel" style="display: block;">
<div class="highlight-code">
<pre class="request-example-code"><code class="language-json">${query.example && typeof query.example === 'string' && query.example.startsWith('{') ? query.example : '{}'}</code></pre>
</div>
</div>
</div>
</div>
` : ''}
<div class="opblock-section execute-section">
<button class="btn execute__btn" disabled>Execute</button>
</div>
<div class="execute-wrapper" style="display: none;">
<div class="opblock-section request-url-section">
<h5 class="opblock-title">Request URL</h5>
<pre class="request-url-output"><code></code></pre>
</div>
<div class="opblock-section curl-section">
<div class="opblock-section-header">
<h5 class="opblock-title">Curl</h5>
<button type="button" class="btn btn-sm copy-curl-btn" title="Copy Curl Command"><i class="fas fa-clipboard"></i> Copy</button>
</div>
<pre class="curl-output"><code></code></pre>
</div>
<div class="opblock-section-header">
<h4 class="opblock-title">Server Response</h4>
<div class="response-controls">
<select class="response-format-select" aria-label="Response display format" style="display: none;">
<option value="json">JSON</option>
<option value="xml">XML</option>
<option value="narrative">Narrative</option>
</select>
<button type="button" class="btn btn-sm copy-response-btn" title="Copy Response Body" style="display: none;"><i class="fas fa-clipboard"></i> Copy</button>
<button type="button" class="btn btn-sm download-response-btn" title="Download Response Body" style="display: none;"><i class="fas fa-download"></i> Download</button>
</div>
</div>
<pre class="response-output-content"><code></code></pre>
<div class="response-output-narrative" style="display:none;"></div>
<div class="response-status" style="margin-top: 5px; font-weight: bold;"></div>
</div>
<div class="opblock-section responses-section">
<div class="opblock-section-header">
<h4>Example Response / Schema</h4>
</div>
<table class="responses-table">
<thead>
<tr class="responses-header">
<td class="response-col_status">Code</td>
<td class="response-col_description">Description</td>
</tr>
</thead>
<tbody>
<tr class="response" data-code="200">
<td class="response-col_status">200</td>
<td class="response-col_description">
<div class="response-col_description__inner">
<div class="renderedMarkdown"><p>Success</p></div>
</div>
<section class="response-controls">
<div class="response-control-media-type">
<label for="${blockId}-example-media-type" class="response-control-media-type__title">Example Format:</label>
<div class="content-type-wrapper d-inline-block">
<select id="${blockId}-example-media-type" aria-label="Example Media Type" class="content-type example-media-type-select">
<option value="application/fhir+json">JSON</option>
<option value="application/fhir+xml">XML</option>
</select>
</div>
<small class="response-control-media-type__accept-message">Controls example/schema format.</small>
</div>
</section>
<div class="model-example">
<ul class="tab" role="tablist">
<li class="tabitem active"><button class="tablinks badge active" data-name="example">Example Value</button></li>
<li class="tabitem"><button class="tablinks badge" data-name="schema">Schema</button></li>
</ul>
<div class="example-panel" style="display: block;">
<div class="highlight-code">
<pre class="example-code-display"><code class="language-json">${query.example && typeof query.example === 'string' ? query.example : '{}'}</code></pre>
</div>
</div>
<div class="schema-panel" style="display: none;">
<div class="highlight-code">
<pre class="schema-code-display"><code class="language-json">${JSON.stringify(query.schema || {}, null, 2)}</code></pre>
</div>
</div>
</div>
</td>
</tr>
<tr class="response" data-code="4xx/5xx">
<td class="response-col_status">4xx/5xx</td>
<td class="response-col_description"><p>Error (e.g., Not Found, Server Error)</p></td>
</tr>
</tbody>
</table>
</div>
</div>
`;
queryListContainer.appendChild(block);
// --- Get Element References ONCE per block ---
const opblockSummary = block.querySelector('.opblock-summary');
const opblockBody = block.querySelector('.opblock-body');
const tryButton = block.querySelector('.try-out__btn');
const executeButton = block.querySelector('.execute__btn');
const paramInputs = block.querySelectorAll('.parameters-section input');
const reqBodyTextarea = block.querySelector('.request-body-textarea');
const reqContentTypeSelect = block.querySelector('.request-content-type');
const executeWrapper = block.querySelector('.execute-wrapper');
const respOutputPre = executeWrapper?.querySelector('.response-output-content');
const respOutputCode = respOutputPre?.querySelector('code');
const respNarrativeDiv = executeWrapper?.querySelector('.response-output-narrative');
const respStatusDiv = executeWrapper?.querySelector('.response-status');
const respFormatSelect = executeWrapper?.querySelector('.response-format-select');
const reqUrlOutput = executeWrapper?.querySelector('.request-url-output code');
const curlOutput = executeWrapper?.querySelector('.curl-output code');
const copyCurlButton = executeWrapper?.querySelector('.copy-curl-btn');
const copyRespButton = executeWrapper?.querySelector('.copy-response-btn');
const downloadRespButton = executeWrapper?.querySelector('.download-response-btn');
const exampleMediaTypeSelect = block.querySelector('.example-media-type-select');
const exampleTabs = block.querySelectorAll('.responses-section .tablinks.badge');
const examplePanel = block.querySelector('.responses-section .example-panel');
const schemaPanel = block.querySelector('.responses-section .schema-panel');
// --- Attach Event Listeners ---
if (opblockSummary) {
opblockSummary.addEventListener('click', (e) => {
if (e.target.closest('button, a, input, select')) return;
const wasOpen = block.classList.contains('is-open');
document.querySelectorAll('.opblock.is-open').forEach(openBlock => {
if (openBlock !== block) {
openBlock.classList.remove('is-open');
if (openBlock.querySelector('.opblock-body')) openBlock.querySelector('.opblock-body').style.display = 'none';
}
});
block.classList.toggle('is-open', !wasOpen);
if (opblockBody) opblockBody.style.display = wasOpen ? 'none' : 'block';
});
}
if (tryButton && executeButton) {
tryButton.addEventListener('click', () => {
const isEditing = tryButton.classList.contains('cancel');
const enable = !isEditing;
paramInputs.forEach(input => {
input.disabled = !enable;
if (!enable) {
input.value = input.dataset.example || '';
input.classList.remove('is-invalid');
}
});
if (reqBodyTextarea) reqBodyTextarea.disabled = !enable;
if (reqContentTypeSelect) reqContentTypeSelect.disabled = !enable;
if (!enable && reqBodyTextarea) {
reqBodyTextarea.value = (query.method === 'POST' && query.path.endsWith('_search')) ? query.example : '';
}
tryButton.textContent = enable ? 'Cancel' : 'Try it out';
tryButton.classList.toggle('cancel', enable);
tryButton.classList.toggle('btn-danger', enable);
tryButton.classList.toggle('btn-primary', !enable);
executeButton.style.display = enable ? 'inline-block' : 'none';
executeButton.disabled = !enable;
if (!enable && executeWrapper) {
executeWrapper.style.display = 'none';
if (reqUrlOutput) reqUrlOutput.textContent = '';
if (curlOutput) curlOutput.textContent = '';
if (respOutputCode) respOutputCode.textContent = '';
if (respNarrativeDiv) respNarrativeDiv.innerHTML = '';
if (respStatusDiv) respStatusDiv.textContent = '';
if (respFormatSelect) respFormatSelect.style.display = 'none';
if (copyRespButton) copyRespButton.style.display = 'none';
if (downloadRespButton) downloadRespButton.style.display = 'none';
}
});
}
if (reqContentTypeSelect && reqBodyTextarea && tryButton) {
const reqExampleCode = block.querySelector('.request-example-code code');
const reqExampleContainer = block.querySelector('.request-body-example');
reqContentTypeSelect.addEventListener('change', () => {
const type = reqContentTypeSelect.value;
const isFormUrlEncoded = type === 'application/x-www-form-urlencoded';
const isEditing = tryButton.classList.contains('cancel');
if (reqExampleContainer) reqExampleContainer.style.display = isFormUrlEncoded ? 'none' : 'block';
if (reqBodyTextarea && !isEditing) {
reqBodyTextarea.value = isFormUrlEncoded ? query.example : '';
reqBodyTextarea.placeholder = isFormUrlEncoded ? 'e.g., param1=value1&param2=value2' : 'Enter FHIR resource JSON or XML';
}
if (reqExampleCode && !isFormUrlEncoded) {
const jsonExample = query.example && typeof query.example === 'string' && query.example.startsWith('{') ? query.example : '{}';
if (type === 'application/fhir+json') {
reqExampleCode.className = 'language-json';
reqExampleCode.textContent = jsonExample;
} else if (type === 'application/fhir+xml') {
reqExampleCode.className = 'language-xml';
reqExampleCode.textContent = jsonToFhirXml(jsonExample);
}
}
});
reqContentTypeSelect.dispatchEvent(new Event('change'));
}
if (executeButton && executeWrapper && respStatusDiv && reqUrlOutput && curlOutput && respFormatSelect && copyRespButton && downloadRespButton && respOutputCode && respNarrativeDiv && respOutputPre) {
executeButton.addEventListener('click', async () => {
executeButton.disabled = true;
executeButton.textContent = 'Executing...';
executeWrapper.style.display = 'block';
reqUrlOutput.textContent = 'Building request...';
curlOutput.textContent = 'Building request...';
respOutputCode.textContent = '';
respNarrativeDiv.innerHTML = '';
respNarrativeDiv.style.display = 'none';
respOutputPre.style.display = 'block';
respStatusDiv.textContent = 'Executing request...';
respStatusDiv.style.color = '#6c757d';
respFormatSelect.style.display = 'none';
copyRespButton.style.display = 'none';
downloadRespButton.style.display = 'none';
respFormatSelect.value = 'json';
const queryDef = JSON.parse(block.dataset.queryData);
const method = queryDef.method;
const headers = { 'Accept': 'application/fhir+json, application/fhir+xml;q=0.9, */*;q=0.8' };
let body;
let path = queryDef.path;
const baseUrl = isUsingLocalHapi ? '/fhir' : (fhirServerUrlInput.value.trim().replace(/\/+$/, '') || '/fhir');
let url = `${baseUrl}`;
let validParams = true;
const missingParams = [];
block.querySelectorAll('.parameters-section tr[data-param-in="path"]').forEach(row => {
const paramName = row.dataset.paramName;
const input = row.querySelector('input');
const required = queryDef.parameters.find(p => p.name === paramName)?.required;
const value = input.value.trim();
input.classList.remove('is-invalid');
if (!value && required) {
validParams = false;
missingParams.push(`${paramName} (path)`);
input.classList.add('is-invalid');
} else if (value) {
path = path.replace(`:${paramName}`, encodeURIComponent(value));
} else {
path = path.replace(`:${paramName}`, '');
}
});
if (path.includes(':')) {
const remaining = path.match(/:(\w+)/g) || [];
if (remaining.length) {
validParams = false;
missingParams.push(...remaining);
}
}
url += path.startsWith('/') ? path : `/${path}`;
const searchParams = new URLSearchParams();
block.querySelectorAll('.parameters-section tr[data-param-in="query"]').forEach(row => {
const paramName = row.dataset.paramName;
const input = row.querySelector('input');
const required = queryDef.parameters.find(p => p.name === paramName)?.required;
const value = input.value.trim();
input.classList.remove('is-invalid');
if (value) searchParams.set(paramName, value);
else if (required) {
validParams = false;
missingParams.push(`${paramName} (query)`);
input.classList.add('is-invalid');
}
});
if (!validParams) {
const errorMsg = `Error: Missing required parameter(s): ${[...new Set(missingParams)].join(', ')}`;
respStatusDiv.textContent = errorMsg;
respStatusDiv.style.color = 'red';
reqUrlOutput.textContent = 'Error: Invalid parameters';
curlOutput.textContent = 'Error: Invalid parameters';
executeButton.disabled = false;
executeButton.textContent = 'Execute';
return;
}
const queryString = searchParams.toString();
if (queryString) url += (url.includes('?') ? '&' : '?') + queryString;
if (queryDef.requestBody) {
const contentType = reqContentTypeSelect ? reqContentTypeSelect.value : 'application/fhir+json';
headers['Content-Type'] = contentType;
body = reqBodyTextarea ? reqBodyTextarea.value : '';
if (contentType === 'application/fhir+xml' && body.trim().startsWith('{')) {
try { body = jsonToFhirXml(body); } catch (e) { console.warn("JSON->XML conversion failed", e); }
} else if (contentType === 'application/fhir+json' && body.trim().startsWith('<')) {
try { body = xmlToFhirJson(body); } catch (e) { console.warn("XML->JSON conversion failed", e); }
}
if (isUsingLocalHapi && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method) && fhirOperationsForm.querySelector('input[name="csrf_token"]')) {
headers['X-CSRFToken'] = fhirOperationsForm.querySelector('input[name="csrf_token"]').value;
}
}
reqUrlOutput.textContent = url;
curlOutput.textContent = generateCurlCommand(method, url, headers, body);
console.log(`Executing: ${method} ${url}`);
console.log("Headers:", headers);
if (body !== undefined) console.log("Body:", (body || '').substring(0, 300) + (body.length > 300 ? "..." : ""));
let respData = { json: null, xml: null, narrative: null, text: null, status: 0, statusText: '', contentType: '' };
try {
const resp = await fetch(url, { method, headers, body });
respData.status = resp.status;
respData.statusText = resp.statusText;
respData.contentType = resp.headers.get('Content-Type') || '';
respData.text = await resp.text();
respStatusDiv.textContent = `Status: ${resp.status} ${resp.statusText}`;
respStatusDiv.style.color = resp.ok ? 'green' : 'red';
if (respData.text) {
if (respData.contentType.includes('json')) {
try {
respData.json = JSON.parse(respData.text);
respData.xml = jsonToFhirXml(respData.json);
if (respData.json.text?.div) respData.narrative = respData.json.text.div;
} catch (e) {}
} else if (respData.contentType.includes('xml')) {
respData.xml = respData.text;
respData.json = JSON.parse(xmlToFhirJson(respData.xml));
try {
const p = new DOMParser();
const xd = p.parseFromString(respData.xml, "application/xml");
const nn = xd.querySelector("div[xmlns='http://www.w3.org/1999/xhtml']");
if (nn) respData.narrative = nn.outerHTML;
} catch(e) {}
} else {
respData.json = { contentType: respData.contentType, content: respData.text };
respData.xml = `<data contentType="${escapeXml(respData.contentType)}">${escapeXml(respData.text)}</data>`;
}
} else if (resp.ok) {
respData.json = { message: "Operation successful, no content returned." };
respData.xml = jsonToFhirXml({});
} else {
respData.json = { error: `Request failed with status ${resp.status}`, detail: resp.statusText };
respData.xml = `<error>Request failed with status ${resp.status} ${escapeXml(resp.statusText)}</error>`;
}
block.dataset.responseData = JSON.stringify(respData);
respFormatSelect.style.display = 'inline-block';
copyRespButton.style.display = 'inline-block';
downloadRespButton.style.display = 'inline-block';
respFormatSelect.disabled = false;
const narrativeOption = respFormatSelect.querySelector('option[value="narrative"]');
if (narrativeOption) narrativeOption.disabled = !respData.narrative;
respFormatSelect.dispatchEvent(new Event('change'));
} catch (e) {
console.error('Fetch Error:', e);
respStatusDiv.textContent = `Network Error: ${e.message}`;
respStatusDiv.style.color = 'red';
respOutputCode.textContent = `Request failed: ${e.message}\nURL: ${url}`;
respOutputCode.className = 'language-text';
respFormatSelect.style.display = 'none';
copyRespButton.style.display = 'none';
downloadRespButton.style.display = 'none';
} finally {
executeButton.disabled = false;
executeButton.textContent = 'Execute';
}
});
}
if (respFormatSelect && respNarrativeDiv && respOutputPre && respOutputCode) {
respFormatSelect.addEventListener('change', () => {
const format = respFormatSelect.value;
try {
const data = JSON.parse(block.dataset.responseData || '{}');
respNarrativeDiv.style.display = 'none';
respOutputPre.style.display = 'block';
switch (format) {
case 'xml':
respOutputCode.textContent = data.xml || data.text || '<No XML>';
respOutputCode.className = 'language-xml';
respOutputPre.style.whiteSpace = 'pre';
break;
case 'narrative':
if (data.narrative) {
respNarrativeDiv.innerHTML = data.narrative;
respNarrativeDiv.style.display = 'block';
respOutputPre.style.display = 'none';
} else {
respOutputCode.textContent = '<Narrative unavailable>';
respOutputCode.className = 'language-text';
respOutputPre.style.whiteSpace = 'pre-wrap';
}
break;
case 'json':
default:
respOutputCode.textContent = data.json ? JSON.stringify(data.json, null, 2) : (data.text || '<No JSON>');
respOutputCode.className = 'language-json';
respOutputPre.style.whiteSpace = 'pre-wrap';
break;
}
} catch (e) {}
});
}
if (exampleTabs.length && examplePanel && schemaPanel) {
exampleTabs.forEach(tab => {
tab.addEventListener('click', () => {
exampleTabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
const target = tab.dataset.name;
examplePanel.style.display = target === 'example' ? 'block' : 'none';
schemaPanel.style.display = target === 'schema' ? 'block' : 'none';
updateStaticExampleFormat(block);
});
});
}
if (exampleMediaTypeSelect) {
exampleMediaTypeSelect.addEventListener('change', () => updateStaticExampleFormat(block));
}
if (copyCurlButton && curlOutput) {
copyCurlButton.addEventListener('click', (e) => {
if (curlOutput.textContent) copyToClipboard(curlOutput.textContent, e.currentTarget);
});
}
if (copyRespButton && respFormatSelect) {
copyRespButton.addEventListener('click', (e) => {
const format = respFormatSelect.value;
let content = '';
if (format === 'narrative') {
content = respNarrativeDiv ? respNarrativeDiv.innerHTML : '';
} else {
content = respOutputCode ? respOutputCode.textContent : '';
}
if (content) copyToClipboard(content, e.currentTarget);
});
}
if (downloadRespButton && respFormatSelect) {
downloadRespButton.addEventListener('click', () => {
const format = respFormatSelect.value;
let content = '';
let ext = 'txt';
let mime = 'text/plain';
const data = JSON.parse(block.dataset.responseData || '{}');
const dateStamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `response-${currentSelectedResource || 'system'}-${query.method}-${dateStamp}`;
switch (format) {
case 'json':
content = data.json ? JSON.stringify(data.json, null, 2) : (data.text || '');
ext = 'json';
mime = 'application/json';
break;
case 'xml':
content = data.xml || data.text || '';
ext = 'xml';
mime = 'application/xml';
break;
case 'narrative':
content = respNarrativeDiv ? respNarrativeDiv.innerHTML : '';
ext = 'html';
mime = 'text/html';
break;
}
if (content) downloadFile(content, `${filename}.${ext}`, mime);
});
}
updateStaticExampleFormat(block); // Initialize example format
}); // End forEach query
swaggerUiContainer.style.display = 'block';
} // End updateQueryListUI
// --- Update Static Example/Schema Format Display ---
function updateStaticExampleFormat(block) {
const data = JSON.parse(block.dataset.queryData || '{}');
const mediaTypeSelect = block.querySelector('.example-media-type-select');
const examplePre = block.querySelector('.responses-section .example-code-display');
const exampleCode = examplePre?.querySelector('code');
const schemaPre = block.querySelector('.responses-section .schema-code-display');
const schemaCode = schemaPre?.querySelector('code');
if (!mediaTypeSelect || !exampleCode || !schemaCode || !examplePre || !schemaPre) return;
const mediaType = mediaTypeSelect.value;
const example = data.example || '';
const isJsonLike = typeof example === 'string' && example.trim().startsWith('{');
const exampleJson = isJsonLike ? example : '{}';
const schemaJson = JSON.stringify(data.schema || {}, null, 2);
if (mediaType === 'application/fhir+json') {
exampleCode.textContent = exampleJson;
exampleCode.className = 'language-json';
schemaCode.textContent = schemaJson;
schemaCode.className = 'language-json';
examplePre.style.whiteSpace = 'pre-wrap';
schemaPre.style.whiteSpace = 'pre-wrap';
} else if (mediaType === 'application/fhir+xml') {
exampleCode.textContent = isJsonLike ? jsonToFhirXml(exampleJson) : `<value>${escapeXml(example)}</value>`;
exampleCode.className = 'language-xml';
schemaCode.textContent = jsonToFhirXml(data.schema || {});
schemaCode.className = 'language-xml';
examplePre.style.whiteSpace = 'pre';
schemaPre.style.whiteSpace = 'pre';
}
}
// --- Fetch Server Metadata ---
if (fetchMetadataButton) {
fetchMetadataButton.addEventListener('click', async () => {
// Clear previous results immediately
if (resourceButtonsContainer) resourceButtonsContainer.innerHTML = '<span class="text-muted">Fetching...</span>';
if (resourceTypesDisplayDiv) resourceTypesDisplayDiv.style.display = 'block';
if (swaggerUiContainer) swaggerUiContainer.style.display = 'none'; // Hide old query list
fetchedMetadataCache = null; // Clear cache before fetch attempt
availableSystemOperations = [];
// Determine Base URL
const customUrl = fhirServerUrlInput.value.trim().replace(/\/+$/, '');
const baseUrl = isUsingLocalHapi ? '/fhir' : customUrl; // Use '/fhir' proxy path if local
// Validate custom URL only if not using local HAPI
if (!isUsingLocalHapi && !baseUrl) { // Should only happen if customUrl is empty
fhirServerUrlInput.classList.add('is-invalid');
alert('Please enter a valid FHIR server URL.');
if (resourceButtonsContainer) resourceButtonsContainer.innerHTML = `<span class="text-danger">Error: Custom URL required.</span>`;
return;
}
// Basic format check for custom URL
if (!isUsingLocalHapi) {
try {
new URL(baseUrl); // Check if it's a parseable URL format
} catch (_) {
fhirServerUrlInput.classList.add('is-invalid');
alert('Invalid custom URL format. Please enter a valid URL (e.g., https://example.com/fhir).');
if (resourceButtonsContainer) resourceButtonsContainer.innerHTML = `<span class="text-danger">Error: Invalid custom URL format.</span>`;
return;
}
}
fhirServerUrlInput.classList.remove('is-invalid');
// Construct metadata URL (always add /metadata)
const url = `${baseUrl}/metadata`;
console.log(`Fetching metadata from: ${url}`);
fetchMetadataButton.disabled = true;
fetchMetadataButton.textContent = 'Fetching...';
try {
const resp = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/fhir+json' } });
if (!resp.ok) { const errText = await resp.text(); throw new Error(`HTTP ${resp.status} ${resp.statusText}: ${errText.substring(0, 500)}`); }
const data = await resp.json();
console.log('Metadata received:', data);
fetchedMetadataCache = data; // Cache successful fetch
displayMetadataAndResourceButtons(data); // Parse and display
} catch (e) {
console.error('Metadata fetch error:', e);
if (resourceButtonsContainer) resourceButtonsContainer.innerHTML = `<span class="text-danger">Error fetching metadata: ${e.message}</span>`;
if (resourceTypesDisplayDiv) resourceTypesDisplayDiv.style.display = 'block'; // Keep container visible to show error
if (swaggerUiContainer) swaggerUiContainer.style.display = 'none';
alert(`Error fetching metadata: ${e.message}`);
fetchedMetadataCache = null; // Clear cache on error
availableSystemOperations = [];
} finally {
fetchMetadataButton.disabled = false;
fetchMetadataButton.textContent = 'Fetch Metadata';
}
});
} else {
console.error("Fetch Metadata button (#fetchMetadata) not found!");
}
// --- Display Metadata & Parse Operations ---
function displayMetadataAndResourceButtons(data) {
const rest = data?.rest?.[0];
if (!rest) { resourceButtonsContainer.innerHTML = `<span class="text-danger">Error: Invalid CapabilityStatement (missing 'rest').</span>`; resourceTypesDisplayDiv.style.display = 'block'; swaggerUiContainer.style.display = 'none'; availableSystemOperations = []; return; }
const resourceTypes = rest.resource?.map(r => r.type).filter(Boolean) || [];
const interactions = rest.interaction?.map(i => i.code) || [];
// --- PARSE OPERATIONS ---
availableSystemOperations = (rest.operation || []).map(op => {
const name = op.name || '';
const defUrl = op.definition || '';
let levels = new Set();
let methods = new Set(); // Start with empty set
// Determine Levels
if (defUrl.includes('System') || name.includes('system') || ['transaction', 'batch', 'metadata', 'history-system'].includes(name)) levels.add('system');
if (defUrl.includes('Type') || name.includes('type') || name.startsWith('$')) levels.add('type');
if (defUrl.includes('Instance') || name.includes('instance') || (name.startsWith('$') && !name.endsWith('-system'))) levels.add('instance');
if (!levels.size) { if (name.startsWith('$') || ['transaction', 'batch', 'metadata', 'history-system', 'capabilities'].includes(name)) levels.add('system'); else { levels.add('type'); levels.add('instance'); } }
// Determine Methods
if (['metadata', 'history-system', 'history-type', 'history-instance', 'capabilities'].includes(name)) {
methods.add('GET');
} else if (name === 'transaction' || name === 'batch' || name === 'server-transaction') {
methods.add('POST');
levels = new Set(['system']); // Force system level for transaction/batch
} else if (['diff', 'meta', 'get-resource-counts'].includes(name)) {
// These support both GET and POST according to the example metadata
methods.add('GET');
methods.add('POST');
} else if (['reindex', 'perform-reindexing-pass', 'mark-all-resources-for-reindexing', 'expunge', 'reindex-terminology', 'hapi.fhir.replace-references'].includes(name)) {
// These are typically POST only
methods.add('POST');
} else {
// Default guess: If documentation or def URL mentions GET, add it, otherwise default to POST
methods.add('POST'); // Assume POST by default for unknown operations
if (op.documentation?.toLowerCase().includes(' http get') || defUrl.toLowerCase().includes('get')) {
methods.add('GET');
}
}
return { name, methods: Array.from(methods), documentation: op.documentation || '', definition: defUrl, levels: Array.from(levels) };
}).filter(op => op.name); // Filter out operations without a name
console.log("Final Parsed Operations List:", availableSystemOperations);
resourceButtonsContainer.innerHTML = ''; // Clear previous buttons
const supportsSystemLevel = interactions.some(code => ['transaction', 'batch', 'history-system', 'search-system', 'capabilities'].includes(code)) || availableSystemOperations.some(op => op.levels.includes('system'));
if (supportsSystemLevel) {
const systemButton = document.createElement('button');
systemButton.type = 'button'; systemButton.className = 'btn btn-outline-success btn-sm me-2 mb-2'; systemButton.textContent = 'System';
systemButton.addEventListener('click', (event) => handleResourceOrSystemButtonClick(event.target, 'System'));
resourceButtonsContainer.appendChild(systemButton);
}
resourceTypes.sort().forEach(resType => {
const resourceButton = document.createElement('button');
resourceButton.type = 'button'; resourceButton.className = 'btn btn-outline-dark-blue btn-sm me-2 mb-2'; resourceButton.textContent = resType;
resourceButton.addEventListener('click', (event) => handleResourceOrSystemButtonClick(event.target, resType));
resourceButtonsContainer.appendChild(resourceButton);
});
resourceTypesDisplayDiv.style.display = 'block'; // Show the buttons container
const firstBtn = resourceButtonsContainer.querySelector('button');
if (firstBtn) { firstBtn.click(); }
else { resourceButtonsContainer.innerHTML = '<span class="text-warning">No resources or system operations found in metadata.</span>'; swaggerUiContainer.style.display = 'none'; }
}
// --- Handle Resource/System Button Clicks ---
function handleResourceOrSystemButtonClick(clickedButton, resourceOrSystemName) {
resourceButtonsContainer.querySelectorAll('.btn').forEach(button => {
button.classList.remove('active', 'btn-success', 'btn-dark-blue', 'btn-outline-success', 'btn-outline-dark-blue'); // Remove all relevant classes
// Re-apply the correct OUTLINE class
if (button.textContent === 'System') {
button.classList.add('btn-outline-success');
} else {
button.classList.add('btn-outline-dark-blue');
}
});
// Activate the clicked button
clickedButton.classList.add('active');
if (resourceOrSystemName === 'System') {
clickedButton.classList.remove('btn-outline-success'); // Remove outline
clickedButton.classList.add('btn-success'); // Add solid
} else {
clickedButton.classList.remove('btn-outline-dark-blue'); // Remove outline
clickedButton.classList.add('btn-dark-blue'); // Add solid
}
updateQueryListUI(resourceOrSystemName); // Update the operation list
}
// --- Initial Page State Setup ---
resourceTypesDisplayDiv.style.display = 'none';
swaggerUiContainer.style.display = 'none';
updateServerToggleUI(); // Set the initial UI state correctly based on default isUsingLocalHapi = true
}); // End DOMContentLoaded Listener
</script>
{% endblock %}