mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-06-14 12:10:03 +00:00
1788 lines
85 KiB
HTML
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 => ({ '<': '<', '>': '>', '&': '&', "'": ''', '"': '"' })[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¶m2=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 %} |