mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-06-14 12:10:03 +00:00
operation UI fixes, CRSF token handling for security internally Fhirpath added to IG processing view explicit removal of narrative views in the ig viewer changes to enforce hapi server config is read. added Prism for code highlighting in the Base and ui operations
827 lines
47 KiB
HTML
827 lines
47 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block content %}
|
|
<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">{{ title }}</h1>
|
|
<div class="col-lg-6 mx-auto">
|
|
<p class="lead mb-4">
|
|
View details of the processed FHIR Implementation Guide.
|
|
</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('view_igs') }}" class="btn btn-outline-secondary btn-lg px-4">Manage FHIR Packages</a>
|
|
<a href="{{ url_for('push_igs') }}" class="btn btn-outline-secondary btn-lg px-4">Upload IG's</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container mt-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h2><i class="bi bi-info-circle me-2"></i>{{ title }}</h2>
|
|
<a href="{{ url_for('view_igs') }}" class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Back to Package List</a>
|
|
</div>
|
|
|
|
{% if processed_ig %}
|
|
<div class="card">
|
|
<div class="card-header">Package Details</div>
|
|
<div class="card-body">
|
|
<dl class="row">
|
|
<dt class="col-sm-3">Package Name</dt>
|
|
<dd class="col-sm-9"><code>{{ processed_ig.package_name }}</code></dd>
|
|
<dt class="col-sm-3">Package Version</dt>
|
|
<dd class="col-sm-9">{{ processed_ig.version }}</dd>
|
|
<dt class="col-sm-3">Processed At</dt>
|
|
<dd class="col-sm-9">{{ processed_ig.processed_date.strftime('%Y-%m-%d %H:%M:%S UTC') }}</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
|
|
{% if config.DISPLAY_PROFILE_RELATIONSHIPS %}
|
|
<div class="card mt-4">
|
|
<div class="card-header">Profile Relationships</div>
|
|
<div class="card-body">
|
|
<h6>Complies With</h6>
|
|
{% if complies_with_profiles %}
|
|
<ul>
|
|
{% for profile in complies_with_profiles %}
|
|
<li>{{ profile }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
<p class="text-muted"><em>No profiles declared as compatible.</em></p>
|
|
{% endif %}
|
|
|
|
<h6>Required Dependent Profiles (Must Also Validate Against)</h6>
|
|
{% if imposed_profiles %}
|
|
<ul>
|
|
{% for profile in imposed_profiles %}
|
|
<li>{{ profile }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
<p class="text-muted"><em>No imposed profiles.</em></p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="card mt-4">
|
|
<div class="card-header">Resource Types Found / Defined</div>
|
|
<div class="card-body">
|
|
{% if profile_list or base_list %}
|
|
<p class="mb-2">
|
|
<small>
|
|
<span class="badge bg-warning text-dark border me-1" style="font-size: 0.8em;" title="Profile contains Must Support elements">MS</span> = Contains Must Support Elements<br>
|
|
<span class="badge bg-info text-dark border me-1" style="font-size: 0.8em;" title="An optional Extension which contains Must Support sub-elements (e.g., value[x], type)">Optional MS Ext</span> = Optional Extension with Must Support Sub-Elements
|
|
</small>
|
|
</p>
|
|
{% if profile_list %}
|
|
<p class="mb-2"><small><span class="badge bg-primary text-light border me-1">Examples</span> = Examples will be displayed when selecting profile Types if contained in the IG</small></p>
|
|
<h6>Profiles Defined ({{ profile_list|length }}):</h6>
|
|
<div class="d-flex flex-wrap gap-1 mb-3 resource-type-list">
|
|
{% for type_info in profile_list %}
|
|
<a href="#" class="resource-type-link text-decoration-none"
|
|
data-package-name="{{ processed_ig.package_name }}"
|
|
data-package-version="{{ processed_ig.version }}"
|
|
data-resource-type="{{ type_info.name }}"
|
|
aria-label="View structure for {{ type_info.name }}">
|
|
{% if type_info.must_support %}
|
|
{% if optional_usage_elements.get(type_info.name) %}
|
|
<span class="badge bg-info text-dark border" title="Optional Extension with Must Support sub-elements">{{ type_info.name }}</span>
|
|
{% else %}
|
|
<span class="badge bg-warning text-dark border" title="Contains Must Support elements">{{ type_info.name }}</span>
|
|
{% endif %}
|
|
{% else %}
|
|
<span class="badge bg-light text-dark border">{{ type_info.name }}</span>
|
|
{% endif %}
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<p class="text-muted"><em>No profiles defined.</em></p>
|
|
{% endif %}
|
|
{% if base_list %}
|
|
<h6>Base Resource Types Referenced ({{ base_list|length }}):</h6>
|
|
<div class="d-flex flex-wrap gap-1 resource-type-list">
|
|
{% for type_info in base_list %}
|
|
<a href="#" class="resource-type-link text-decoration-none"
|
|
data-package-name="{{ processed_ig.package_name }}"
|
|
data-package-version="{{ processed_ig.version }}"
|
|
data-resource-type="{{ type_info.name }}"
|
|
aria-label="View structure for {{ type_info.name }}">
|
|
{% if type_info.must_support %}
|
|
<span class="badge bg-warning text-dark border" title="Contains Must Support elements">{{ type_info.name }}</span>
|
|
{% else %}
|
|
<span class="badge bg-light text-dark border">{{ type_info.name }}</span>
|
|
{% endif %}
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<p class="text-muted"><em>No base resource types referenced.</em></p>
|
|
{% endif %}
|
|
{% else %}
|
|
<p class="text-muted"><em>No resource type information extracted or stored.</em></p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div id="structure-display-wrapper" class="mt-4" style="display: none;">
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<span>Structure Definition for: <code id="structure-title"></code></span>
|
|
<button type="button" class="btn-close" aria-label="Close" onclick="document.getElementById('structure-display-wrapper').style.display='none'; document.getElementById('example-display-wrapper').style.display='none'; document.getElementById('raw-structure-wrapper').style.display='none';"></button>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="structure-loading" class="text-center py-3" style="display: none;">
|
|
<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>
|
|
</div>
|
|
<div id="structure-fallback-message" class="alert alert-info" style="display: none;"></div>
|
|
<div id="structure-content"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="raw-structure-wrapper" class="mt-4" style="display: none;">
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<span>Raw Structure Definition for <code id="raw-structure-title"></code></span>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary btn-copy" id="copy-raw-def-button"
|
|
data-bs-toggle="tooltip" data-bs-placement="top" title="Copy Raw Definition JSON">
|
|
<i class="bi bi-clipboard" aria-hidden="true"></i>
|
|
<span class="visually-hidden">Copy Raw Definition JSON to clipboard</span>
|
|
</button>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="raw-structure-loading" class="text-center py-3" style="display: none;">
|
|
<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>
|
|
</div>
|
|
<pre><code id="raw-structure-content" class="p-2 d-block border bg-light language-json" style="max-height: 400px; overflow-y: auto;"></code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="example-display-wrapper" class="mt-4" style="display: none;">
|
|
<div class="card">
|
|
<div class="card-header">Examples for <code id="example-resource-type-title"></code></div>
|
|
<div class="card-body">
|
|
<div class="mb-3" id="example-selector-wrapper" style="display: none;">
|
|
<label for="example-select" class="form-label">Select Example:</label>
|
|
<select class="form-select form-select-sm" id="example-select"><option selected value="">-- Select --</option></select>
|
|
</div>
|
|
<div id="example-loading" class="text-center py-3" style="display: none;">
|
|
<div class="spinner-border spinner-border-sm text-secondary" role="status"><span class="visually-hidden">Loading...</span></div>
|
|
</div>
|
|
<div id="example-content-wrapper" style="display: none;">
|
|
<h6 id="example-filename" class="mt-2 small text-muted"></h6>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
<h6>Raw Content</h6>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary btn-copy" id="copy-raw-ex-button"
|
|
data-bs-toggle="tooltip" data-bs-placement="top" title="Copy Raw Content">
|
|
<i class="bi bi-clipboard" aria-hidden="true"></i>
|
|
<span class="visually-hidden">Copy Raw Content to clipboard</span>
|
|
</button>
|
|
</div>
|
|
<pre><code id="example-content-raw" class="p-2 d-block border bg-light language-json" style="max-height: 400px; overflow-y: auto; white-space: pre;"></code></pre>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
<h6>Pretty-Printed JSON</h6>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary btn-copy" id="copy-pretty-json-button"
|
|
data-bs-toggle="tooltip" data-bs-placement="top" title="Copy JSON">
|
|
<i class="bi bi-clipboard" aria-hidden="true"></i>
|
|
<span class="visually-hidden">Copy JSON example to clipboard</span>
|
|
</button>
|
|
</div>
|
|
<pre><code id="example-content-json" class="p-2 d-block border bg-light language-json" style="max-height: 400px; overflow-y: auto;"></code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="alert alert-warning" role="alert">Processed IG details not available.</div>
|
|
{% endif %}
|
|
</div>
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
|
<script>const examplesData = {{ examples_by_type | tojson | safe }};</script>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
try {
|
|
const structureDisplayWrapper = document.getElementById('structure-display-wrapper');
|
|
const structureDisplay = document.getElementById('structure-content');
|
|
const structureTitle = document.getElementById('structure-title');
|
|
const structureLoading = document.getElementById('structure-loading');
|
|
const structureFallbackMessage = document.getElementById('structure-fallback-message');
|
|
const exampleDisplayWrapper = document.getElementById('example-display-wrapper');
|
|
const exampleSelectorWrapper = document.getElementById('example-selector-wrapper');
|
|
const exampleSelect = document.getElementById('example-select');
|
|
const exampleResourceTypeTitle = document.getElementById('example-resource-type-title');
|
|
const exampleLoading = document.getElementById('example-loading');
|
|
const exampleContentWrapper = document.getElementById('example-content-wrapper');
|
|
const exampleFilename = document.getElementById('example-filename');
|
|
const exampleContentRaw = document.getElementById('example-content-raw');
|
|
const exampleContentJson = document.getElementById('example-content-json');
|
|
const rawStructureWrapper = document.getElementById('raw-structure-wrapper');
|
|
const rawStructureTitle = document.getElementById('raw-structure-title');
|
|
const rawStructureContent = document.getElementById('raw-structure-content');
|
|
const rawStructureLoading = document.getElementById('raw-structure-loading');
|
|
const copyRawDefButton = document.getElementById('copy-raw-def-button');
|
|
const copyRawExButton = document.getElementById('copy-raw-ex-button');
|
|
const copyPrettyJsonButton = document.getElementById('copy-pretty-json-button');
|
|
let copyRawDefTooltipInstance = null;
|
|
let copyRawExTooltipInstance = null;
|
|
let copyPrettyJsonTooltipInstance = null;
|
|
|
|
const structureBaseUrl = "{{ url_for('get_structure') }}";
|
|
const exampleBaseUrl = "{{ url_for('get_example') }}";
|
|
|
|
// Initialize Highlight.js
|
|
hljs.configure({ languages: ['json'] });
|
|
hljs.highlightAll();
|
|
|
|
// Generic Copy Logic Function
|
|
function setupCopyButton(buttonElement, sourceElement, tooltipInstance, successTitle, originalTitle, errorTitle, nothingTitle) {
|
|
if (!buttonElement || !sourceElement) {
|
|
console.warn("Copy button or source element not found for setup:", buttonElement?.id, sourceElement?.id);
|
|
return null;
|
|
}
|
|
bootstrap.Tooltip.getInstance(buttonElement)?.dispose();
|
|
tooltipInstance = new bootstrap.Tooltip(buttonElement);
|
|
|
|
const clickHandler = () => {
|
|
const textToCopy = sourceElement.textContent;
|
|
const copyIcon = buttonElement.querySelector('i');
|
|
console.log(`Copy button clicked: ${buttonElement.id}`);
|
|
if (textToCopy && textToCopy.trim() && textToCopy !== '(Not valid JSON)') {
|
|
navigator.clipboard.writeText(textToCopy).then(() => {
|
|
console.log(` > Content copied successfully!`);
|
|
buttonElement.setAttribute('data-bs-original-title', successTitle);
|
|
tooltipInstance.show();
|
|
if (copyIcon) copyIcon.classList.replace('bi-clipboard', 'bi-check-lg');
|
|
setTimeout(() => {
|
|
if (buttonElement.isConnected) {
|
|
tooltipInstance.hide();
|
|
buttonElement.setAttribute('data-bs-original-title', originalTitle);
|
|
if (copyIcon) copyIcon.classList.replace('bi-check-lg', 'bi-clipboard');
|
|
}
|
|
}, 2000);
|
|
}).catch(err => {
|
|
console.error(` > Failed to copy content for ${buttonElement.id}:`, err);
|
|
buttonElement.setAttribute('data-bs-original-title', errorTitle);
|
|
tooltipInstance.show();
|
|
setTimeout(() => {
|
|
if (buttonElement.isConnected) {
|
|
tooltipInstance.hide();
|
|
buttonElement.setAttribute('data-bs-original-title', originalTitle);
|
|
}
|
|
}, 2000);
|
|
});
|
|
} else {
|
|
console.log(' > No valid content to copy.');
|
|
buttonElement.setAttribute('data-bs-original-title', nothingTitle);
|
|
tooltipInstance.show();
|
|
setTimeout(() => {
|
|
if (buttonElement.isConnected) {
|
|
tooltipInstance.hide();
|
|
buttonElement.setAttribute('data-bs-original-title', originalTitle);
|
|
}
|
|
}, 1500);
|
|
}
|
|
};
|
|
buttonElement.addEventListener('click', clickHandler);
|
|
return tooltipInstance;
|
|
}
|
|
|
|
// Setup Copy Buttons
|
|
copyRawDefTooltipInstance = setupCopyButton(copyRawDefButton, rawStructureContent, copyRawDefTooltipInstance, 'Copied Definition!', 'Copy Raw Definition JSON', 'Copy Failed!', 'Nothing to copy');
|
|
copyRawExTooltipInstance = setupCopyButton(copyRawExButton, exampleContentRaw, copyRawExTooltipInstance, 'Copied Raw!', 'Copy Raw Content', 'Copy Failed!', 'Nothing to copy');
|
|
copyPrettyJsonTooltipInstance = setupCopyButton(copyPrettyJsonButton, exampleContentJson, copyPrettyJsonTooltipInstance, 'Copied JSON!', 'Copy JSON', 'Copy Failed!', 'Nothing to copy');
|
|
|
|
// Handle Resource Type Link Clicks
|
|
document.querySelectorAll('.resource-type-list').forEach(container => {
|
|
container.addEventListener('click', function(event) {
|
|
const link = event.target.closest('.resource-type-link');
|
|
if (!link) return;
|
|
event.preventDefault();
|
|
console.log("Resource type link clicked:", link.dataset.resourceType);
|
|
|
|
const pkgName = link.dataset.packageName;
|
|
const pkgVersion = link.dataset.packageVersion;
|
|
const resourceType = link.dataset.resourceType;
|
|
if (!pkgName || !pkgVersion || !resourceType) {
|
|
console.error("Missing data attributes:", { pkgName, pkgVersion, resourceType });
|
|
structureDisplay.innerHTML = `<div class="alert alert-danger">Error: Missing resource type information</div>`;
|
|
rawStructureContent.textContent = `Error: Missing resource type information`;
|
|
return;
|
|
}
|
|
|
|
const structureParams = new URLSearchParams({ package_name: pkgName, package_version: pkgVersion, resource_type: resourceType });
|
|
const structureFetchUrl = `${structureBaseUrl}?${structureParams.toString()}`;
|
|
console.log("Fetching structure from:", structureFetchUrl);
|
|
|
|
structureTitle.textContent = `${resourceType} (${pkgName}#${pkgVersion})`;
|
|
rawStructureTitle.textContent = `${resourceType} (${pkgName}#${pkgVersion})`;
|
|
structureDisplay.innerHTML = '';
|
|
rawStructureContent.textContent = '';
|
|
structureLoading.style.display = 'block';
|
|
rawStructureLoading.style.display = 'block';
|
|
structureFallbackMessage.style.display = 'none';
|
|
structureDisplayWrapper.style.display = 'block';
|
|
rawStructureWrapper.style.display = 'block';
|
|
exampleDisplayWrapper.style.display = 'none';
|
|
|
|
fetch(structureFetchUrl)
|
|
.then(response => {
|
|
console.log("Structure fetch response status:", response.status);
|
|
if (!response.ok) {
|
|
return response.text().then(text => {
|
|
try {
|
|
const errData = JSON.parse(text);
|
|
throw new Error(errData.error || `HTTP error ${response.status}: Server error`);
|
|
} catch (e) {
|
|
throw new Error(`HTTP error ${response.status}: Failed to load StructureDefinition for ${resourceType} in ${pkgName}#{pkgVersion}. Please check server logs or try re-importing the package.`);
|
|
}
|
|
});
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
console.log("Structure data received:", data);
|
|
if (data.fallback_used) {
|
|
structureFallbackMessage.style.display = 'block';
|
|
structureFallbackMessage.textContent = `Structure definition not found in this IG; using base FHIR definition from ${data.source_package || 'FHIR core'}.`;
|
|
} else {
|
|
structureFallbackMessage.style.display = 'none';
|
|
}
|
|
renderStructureTree(data.structure_definition.snapshot.element, data.must_support_paths || [], resourceType);
|
|
// Client-side fallback to remove narrative
|
|
if (data.structure_definition && ('text' in data.structure_definition || 'Text' in data.structure_definition)) {
|
|
console.warn(`Narrative text found in StructureDefinition for ${resourceType}, removing client-side`);
|
|
delete data.structure_definition.text;
|
|
delete data.structure_definition.Text; // Handle case sensitivity
|
|
}
|
|
rawStructureContent.textContent = JSON.stringify(data.structure_definition, null, 4);
|
|
hljs.highlightElement(rawStructureContent);
|
|
populateExampleSelector(resourceType);
|
|
})
|
|
.catch(error => {
|
|
console.error("Error fetching structure:", error);
|
|
structureFallbackMessage.style.display = 'none';
|
|
structureDisplay.innerHTML = `<div class="alert alert-danger">Error loading structure: ${error.message}</div>`;
|
|
rawStructureContent.textContent = `Error: ${error.message}`;
|
|
populateExampleSelector(resourceType);
|
|
})
|
|
.finally(() => {
|
|
structureLoading.style.display = 'none';
|
|
rawStructureLoading.style.display = 'none';
|
|
});
|
|
});
|
|
});
|
|
|
|
// Populate Example Selector
|
|
function populateExampleSelector(resourceOrProfileIdentifier) {
|
|
console.log("Populating examples for:", resourceOrProfileIdentifier);
|
|
exampleResourceTypeTitle.textContent = resourceOrProfileIdentifier;
|
|
const availableExamples = examplesData[resourceOrProfileIdentifier] || [];
|
|
console.log("Available examples:", availableExamples);
|
|
exampleSelect.innerHTML = '<option selected value="">-- Select --</option>';
|
|
exampleContentWrapper.style.display = 'none';
|
|
exampleFilename.textContent = '';
|
|
exampleContentRaw.textContent = '';
|
|
exampleContentJson.textContent = '';
|
|
|
|
if (availableExamples.length > 0) {
|
|
availableExamples.forEach(filePath => {
|
|
const filename = filePath.split('/').pop();
|
|
const option = document.createElement('option');
|
|
option.value = filePath;
|
|
option.textContent = filename;
|
|
exampleSelect.appendChild(option);
|
|
});
|
|
exampleSelectorWrapper.style.display = 'block';
|
|
exampleDisplayWrapper.style.display = 'block';
|
|
} else {
|
|
exampleSelectorWrapper.style.display = 'none';
|
|
exampleDisplayWrapper.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Handle Example Selection
|
|
if (exampleSelect) {
|
|
exampleSelect.addEventListener('change', function(event) {
|
|
const selectedFilePath = this.value;
|
|
console.log("Selected example file:", selectedFilePath);
|
|
exampleContentRaw.textContent = '';
|
|
exampleContentJson.textContent = '';
|
|
exampleFilename.textContent = '';
|
|
exampleContentWrapper.style.display = 'none';
|
|
|
|
if (copyRawExTooltipInstance && copyRawExButton?.isConnected) {
|
|
copyRawExTooltipInstance.hide();
|
|
copyRawExButton.setAttribute('data-bs-original-title', 'Copy Raw Content');
|
|
copyRawExButton.querySelector('i')?.classList.replace('bi-check-lg', 'bi-clipboard');
|
|
}
|
|
if (copyPrettyJsonTooltipInstance && copyPrettyJsonButton?.isConnected) {
|
|
copyPrettyJsonTooltipInstance.hide();
|
|
copyPrettyJsonButton.setAttribute('data-bs-original-title', 'Copy JSON');
|
|
copyPrettyJsonButton.querySelector('i')?.classList.replace('bi-check-lg', 'bi-clipboard');
|
|
}
|
|
|
|
if (!selectedFilePath) return;
|
|
|
|
const exampleParams = new URLSearchParams({
|
|
package_name: "{{ processed_ig.package_name }}",
|
|
package_version: "{{ processed_ig.version }}",
|
|
filename: selectedFilePath
|
|
});
|
|
const exampleFetchUrl = `${exampleBaseUrl}?${exampleParams.toString()}`;
|
|
console.log("Fetching example from:", exampleFetchUrl);
|
|
exampleLoading.style.display = 'block';
|
|
|
|
fetch(exampleFetchUrl)
|
|
.then(response => {
|
|
console.log("Example fetch response status:", response.status);
|
|
if (!response.ok) {
|
|
return response.text().then(text => {
|
|
try {
|
|
const errData = JSON.parse(text);
|
|
throw new Error(errData.error || `HTTP error ${response.status}: Server error`);
|
|
} catch (e) {
|
|
throw new Error(`HTTP error ${response.status}: Failed to load example ${selectedFilePath.split('/').pop()} in {{ processed_ig.package_name }}#{{ processed_ig.version }}. Please check server logs or try re-importing the package.`);
|
|
}
|
|
});
|
|
}
|
|
return response.text(); // Get raw JSON string
|
|
})
|
|
.then(data => {
|
|
console.log("Example content received.");
|
|
exampleFilename.textContent = `Source: ${selectedFilePath.split('/').pop()}`;
|
|
// Raw JSON: flat string
|
|
exampleContentRaw.textContent = data;
|
|
// Pretty JSON: 4-space indented, remove narrative
|
|
try {
|
|
const parsedData = JSON.parse(data);
|
|
if (parsedData && ('text' in parsedData || 'Text' in parsedData)) {
|
|
console.warn(`Narrative text found in example ${selectedFilePath}, removing client-side`);
|
|
delete parsedData.text;
|
|
delete parsedData.Text; // Handle case sensitivity
|
|
}
|
|
exampleContentJson.textContent = JSON.stringify(parsedData, null, 4);
|
|
} catch (e) {
|
|
console.error("Error parsing JSON for pretty display:", e);
|
|
exampleContentJson.textContent = `Error: Invalid JSON - ${e.message}`;
|
|
}
|
|
hljs.highlightElement(exampleContentRaw);
|
|
hljs.highlightElement(exampleContentJson);
|
|
exampleContentWrapper.style.display = 'block';
|
|
})
|
|
.catch(error => {
|
|
console.error("Error fetching example:", error);
|
|
exampleFilename.textContent = 'Error loading example';
|
|
exampleContentRaw.textContent = `Error: ${error.message}`;
|
|
exampleContentJson.textContent = `Error: ${error.message}`;
|
|
exampleContentWrapper.style.display = 'block';
|
|
})
|
|
.finally(() => {
|
|
exampleLoading.style.display = 'none';
|
|
});
|
|
});
|
|
}
|
|
|
|
function buildTreeData(elements) {
|
|
const treeRoot = { children: {}, element: null, name: 'Root' };
|
|
const nodeMap = { 'Root': treeRoot };
|
|
|
|
console.log("Building tree with elements:", elements.length);
|
|
elements.forEach((el, index) => {
|
|
const path = el.path;
|
|
const id = el.id || null;
|
|
const sliceName = el.sliceName || null;
|
|
|
|
console.log(`Element ${index}: path=${path}, id=${id}, sliceName=${sliceName}`);
|
|
|
|
if (!path) {
|
|
console.warn(`Skipping element ${index} with no path`);
|
|
return;
|
|
}
|
|
|
|
const parts = path.split('.');
|
|
let currentPath = '';
|
|
let parentNode = treeRoot;
|
|
|
|
for (let i = 0; i < parts.length; i++) {
|
|
let part = parts[i];
|
|
const isChoiceElement = part.endsWith('[x]');
|
|
const cleanPart = part.replace(/\[\d+\]/g, '').replace('[x]', '');
|
|
currentPath = i === 0 ? cleanPart : `${currentPath}.${cleanPart}`;
|
|
let nodeKey = isChoiceElement ? part : cleanPart;
|
|
|
|
if (sliceName && i === parts.length - 1) {
|
|
nodeKey = sliceName;
|
|
currentPath = id || currentPath;
|
|
}
|
|
|
|
if (!nodeMap[currentPath]) {
|
|
const newNode = {
|
|
children: {},
|
|
element: null,
|
|
name: nodeKey,
|
|
path: currentPath
|
|
};
|
|
let parentPath = i === 0 ? 'Root' : parts.slice(0, i).join('.').replace(/\[\d+\]/g, '').replace('[x]', '');
|
|
if (id && id.includes(':')) {
|
|
const idParts = id.split('.');
|
|
const sliceIndex = idParts.findIndex(p => p.includes(':'));
|
|
if (sliceIndex >= 0 && sliceIndex < i) {
|
|
const sliceIdPrefix = idParts.slice(0, sliceIndex + 1).join('.');
|
|
const sliceNode = elements.find(e => e.id === sliceIdPrefix);
|
|
if (sliceNode && sliceNode.sliceName) {
|
|
parentPath = sliceIdPrefix;
|
|
}
|
|
}
|
|
}
|
|
parentNode = nodeMap[parentPath] || treeRoot;
|
|
parentNode.children[nodeKey] = newNode;
|
|
nodeMap[currentPath] = newNode;
|
|
console.log(`Created node: path=${currentPath}, name=${nodeKey}, parentPath=${parentPath}`);
|
|
|
|
if (el.path === cleanPart + '.modifierExtension' ||
|
|
(el.type && el.type.some(t => t.code === 'DomainResource' || t.code === 'BackboneElement'))) {
|
|
const modExtPath = `${currentPath}.modifierExtension`;
|
|
const modExtNode = {
|
|
children: {},
|
|
element: {
|
|
path: modExtPath,
|
|
id: modExtPath,
|
|
short: 'Extensions that cannot be ignored',
|
|
definition: 'May be used to represent additional information that modifies the understanding of the element.',
|
|
min: 0,
|
|
max: '*',
|
|
type: [{ code: 'Extension' }],
|
|
isModifier: true
|
|
},
|
|
name: 'modifierExtension',
|
|
path: modExtPath
|
|
};
|
|
newNode.children['modifierExtension'] = modExtNode;
|
|
nodeMap[modExtPath] = modExtNode;
|
|
console.log(`Added modifierExtension: path=${modExtPath}`);
|
|
}
|
|
}
|
|
|
|
if (i === parts.length - 1) {
|
|
const targetNode = nodeMap[currentPath];
|
|
targetNode.element = el;
|
|
console.log(`Assigned element to node: path=${currentPath}, id=${id}, sliceName=${sliceName}`);
|
|
|
|
if (isChoiceElement && el.type && el.type.length > 1) {
|
|
el.type.forEach(type => {
|
|
if (type.code === 'dateTime') return;
|
|
const typeName = `onset${type.code.charAt(0).toUpperCase() + type.code.slice(1)}`;
|
|
const typePath = `${currentPath}.${typeName}`;
|
|
const typeNode = {
|
|
children: {},
|
|
element: {
|
|
path: typePath,
|
|
id: typePath,
|
|
short: el.short || typeName,
|
|
definition: el.definition || `Choice of type ${type.code}`,
|
|
min: el.min,
|
|
max: el.max,
|
|
type: [{ code: type.code }],
|
|
mustSupport: el.mustSupport || false
|
|
},
|
|
name: typeName,
|
|
path: typePath
|
|
};
|
|
targetNode.children[typeName] = typeNode;
|
|
nodeMap[typePath] = typeNode;
|
|
console.log(`Added choice type node: path=${typePath}, name=${typeName}`);
|
|
});
|
|
}
|
|
}
|
|
|
|
parentNode = nodeMap[currentPath];
|
|
}
|
|
});
|
|
|
|
const treeData = Object.values(treeRoot.children);
|
|
console.log("Tree data constructed:", treeData);
|
|
return treeData;
|
|
}
|
|
|
|
function renderNodeAsLi(node, mustSupportPathsSet, resourceType, level = 0) {
|
|
if (!node || !node.element) {
|
|
console.warn("Skipping render for invalid node:", node);
|
|
return '';
|
|
}
|
|
const el = node.element;
|
|
const path = el.path || 'N/A';
|
|
const id = el.id || null;
|
|
const sliceName = el.sliceName || null;
|
|
const min = el.min !== undefined ? el.min : '';
|
|
const max = el.max || '';
|
|
const short = el.short || '';
|
|
const definition = el.definition || '';
|
|
|
|
console.log(`Rendering node: path=${path}, id=${id}, sliceName=${sliceName}`);
|
|
|
|
let isMustSupport = mustSupportPathsSet.has(path) || (id && mustSupportPathsSet.has(id));
|
|
let mustSupportTitle = isMustSupport ? 'Must Support' : '';
|
|
let fhirPathExpression = '';
|
|
if (sliceName) {
|
|
const slicePath = `${path}:${sliceName}`;
|
|
if (mustSupportPathsSet.has(slicePath)) {
|
|
isMustSupport = true;
|
|
mustSupportTitle = `Must Support (Slice: ${sliceName})`;
|
|
fhirPathExpression = `${path}[sliceName='${sliceName}']`;
|
|
}
|
|
}
|
|
mustSupportPathsSet.forEach(msPath => {
|
|
if (msPath.includes('where') && msPath.startsWith(path)) {
|
|
isMustSupport = true;
|
|
mustSupportTitle = `Must Support (FHIRPath: ${msPath})`;
|
|
fhirPathExpression = msPath;
|
|
}
|
|
});
|
|
|
|
let isOptional = min == 0 && isMustSupport && el.type && el.type.some(t => t.code === 'Extension') && path.includes('extension');
|
|
if (isOptional) {
|
|
mustSupportTitle = `Optional Must Support Extension${sliceName ? ` (Slice: ${sliceName})` : ''}`;
|
|
fhirPathExpression = sliceName ? `${path}[sliceName='${sliceName}']` : path;
|
|
}
|
|
mustSupportTitle += isMustSupport ? ' (Validated by HAPI FHIR Server)' : '';
|
|
|
|
const liClass = isMustSupport ? 'list-group-item py-1 px-2 list-group-item-warning' : 'list-group-item py-1 px-2';
|
|
const mustSupportDisplay = isMustSupport ? `<i class="bi bi-check-circle-fill ${isOptional ? 'text-info' : 'text-warning'} ms-1" title="${mustSupportTitle}" data-bs-toggle="tooltip"></i>` : '';
|
|
const hasChildren = Object.keys(node.children).length > 0;
|
|
const collapseId = `collapse-${path.replace(/[\.\:\/\[\]\(\)]/g, '-')}`;
|
|
const padding = level * 20;
|
|
const pathStyle = `padding-left: ${padding}px; white-space: nowrap;`;
|
|
|
|
let typeString = 'N/A';
|
|
if (el.type && el.type.length > 0) {
|
|
typeString = el.type.map(t => {
|
|
let s = t.code || '';
|
|
let profiles = t.targetProfile || t.profile || [];
|
|
if (profiles.length > 0) {
|
|
const targetTypes = profiles.map(p => (p || '').split('/').pop()).filter(Boolean).join(', ');
|
|
if (targetTypes) {
|
|
s += `(<span class="text-muted fst-italic" title="${profiles.join(', ')}">${targetTypes}</span>)`;
|
|
}
|
|
}
|
|
return s;
|
|
}).join(' | ');
|
|
}
|
|
|
|
let childrenHtml = '';
|
|
if (hasChildren) {
|
|
childrenHtml += `<ul class="collapse list-group list-group-flush" id="${collapseId}">`;
|
|
Object.values(node.children).sort((a, b) => (a.element?.path ?? a.name).localeCompare(b.element?.path ?? b.name)).forEach(childNode => {
|
|
childrenHtml += renderNodeAsLi(childNode, mustSupportPathsSet, resourceType, level + 1);
|
|
});
|
|
childrenHtml += `</ul>`;
|
|
}
|
|
|
|
let itemHtml = `<li class="${liClass}">`;
|
|
itemHtml += `<div class="row gx-2 align-items-center ${hasChildren ? 'collapse-toggle' : ''}" ${hasChildren ? `data-collapse-id="${collapseId}" role="button" aria-expanded="false" aria-controls="${collapseId}"` : ''}>`;
|
|
itemHtml += `<div class="col-lg-4 col-md-3 text-truncate" style="${pathStyle}">`;
|
|
itemHtml += `<span style="display: inline-block; width: 1.2em; text-align: center;">`;
|
|
if (hasChildren) {
|
|
itemHtml += `<i class="bi bi-chevron-right small toggle-icon"></i>`;
|
|
}
|
|
itemHtml += `</span>`;
|
|
const displayName = sliceName ? `<i>${node.name}:${sliceName}</i>` : node.name;
|
|
const pathTitle = fhirPathExpression ? `${path} (FHIRPath: ${fhirPathExpression})` : path + (sliceName ? ` (Slice: ${sliceName})` : '');
|
|
itemHtml += `<code class="fw-bold ms-1" title="${pathTitle}" data-bs-toggle="tooltip">${displayName}</code>`;
|
|
itemHtml += `</div>`;
|
|
itemHtml += `<div class="col-lg-1 col-md-1 text-center text-muted small"><code>${min}..${max}</code></div>`;
|
|
itemHtml += `<div class="col-lg-3 col-md-3 text-truncate small">${typeString}</div>`;
|
|
itemHtml += `<div class="col-lg-1 col-md-1 text-center">${mustSupportDisplay}</div>`;
|
|
let descriptionTooltipAttrs = '';
|
|
if (definition) {
|
|
const escapedDefinition = definition
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''')
|
|
.replace(/\n/g, ' ');
|
|
descriptionTooltipAttrs = `data-bs-toggle="tooltip" data-bs-placement="top" title="${escapedDefinition}"`;
|
|
}
|
|
itemHtml += `<div class="col-lg-3 col-md-4 text-muted text-truncate small" ${descriptionTooltipAttrs}>${short || (definition ? '(definition only)' : '')}</div>`;
|
|
itemHtml += `</div>`;
|
|
itemHtml += childrenHtml;
|
|
itemHtml += `</li>`;
|
|
return itemHtml;
|
|
}
|
|
|
|
function renderStructureTree(elements, mustSupportPaths, resourceType) {
|
|
if (!elements || elements.length === 0) {
|
|
structureDisplay.innerHTML = '<p class="text-muted"><em>No elements found in Structure Definition.</em></p>';
|
|
return;
|
|
}
|
|
|
|
const mustSupportPathsSet = new Set(mustSupportPaths);
|
|
console.log("Must Support Paths Set for Tree:", mustSupportPathsSet);
|
|
|
|
let html = '<p><small>' +
|
|
'<i class="bi bi-check-circle-fill text-warning ms-1"></i> = Must Support (yellow row)<br>' +
|
|
'<i class="bi bi-check-circle-fill text-info ms-1"></i> = Optional Must Support Extension (blue icon)</small></p>';
|
|
html += `<div class="row gx-2 small fw-bold border-bottom mb-1 d-none d-md-flex"><div class="col-lg-4 col-md-3">Path</div><div class="col-lg-1 col-md-1 text-center">Card.</div><div class="col-lg-3 col-md-3">Type(s)</div><div class="col-lg-1 col-md-1 text-center">Status</div><div class="col-lg-3 col-md-4">Description</div></div>`;
|
|
html += '<ul class="list-group list-group-flush structure-tree-root">';
|
|
|
|
const treeData = buildTreeData(elements);
|
|
treeData.sort((a, b) => (a.element?.path ?? a.name).localeCompare(b.element?.path ?? b.name));
|
|
treeData.forEach(rootNode => {
|
|
html += renderNodeAsLi(rootNode, mustSupportPathsSet, resourceType, 0);
|
|
});
|
|
html += '</ul>';
|
|
|
|
structureDisplay.innerHTML = html;
|
|
|
|
const tooltipTriggerList = [].slice.call(structureDisplay.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
|
tooltipTriggerList.forEach(tooltipTriggerEl => {
|
|
if (tooltipTriggerEl.getAttribute('title')) {
|
|
new bootstrap.Tooltip(tooltipTriggerEl);
|
|
}
|
|
});
|
|
|
|
let lastClickTime = 0;
|
|
const debounceDelay = 200;
|
|
structureDisplay.querySelectorAll('.collapse-toggle').forEach(toggleEl => {
|
|
const collapseId = toggleEl.getAttribute('data-collapse-id');
|
|
if (!collapseId) {
|
|
console.warn("No collapse ID for toggle:", toggleEl.outerHTML);
|
|
return;
|
|
}
|
|
const collapseEl = document.querySelector(`#${collapseId}`);
|
|
if (!collapseEl) {
|
|
console.warn("No collapse element found for ID:", collapseId);
|
|
return;
|
|
}
|
|
const toggleIcon = toggleEl.querySelector('.toggle-icon');
|
|
if (!toggleIcon) {
|
|
console.warn("No toggle icon for:", collapseId);
|
|
return;
|
|
}
|
|
|
|
console.log("Initializing collapse for:", collapseId);
|
|
const bsCollapse = new bootstrap.Collapse(collapseEl, {
|
|
toggle: false
|
|
});
|
|
|
|
toggleEl.addEventListener('click', event => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
const now = Date.now();
|
|
if (now - lastClickTime < debounceDelay) {
|
|
console.log("Debounced click ignored for:", collapseId);
|
|
return;
|
|
}
|
|
lastClickTime = now;
|
|
console.log("Click toggle for:", collapseId, "Current show:", collapseEl.classList.contains('show'));
|
|
if (collapseEl.classList.contains('show')) {
|
|
bsCollapse.hide();
|
|
} else {
|
|
bsCollapse.show();
|
|
}
|
|
});
|
|
|
|
collapseEl.addEventListener('show.bs.collapse', event => {
|
|
console.log("Show collapse triggered for:", event.target.id, "Show class:", event.target.classList.contains('show'));
|
|
if (toggleIcon) {
|
|
toggleIcon.classList.remove('bi-chevron-right');
|
|
toggleIcon.classList.add('bi-chevron-down');
|
|
}
|
|
});
|
|
collapseEl.addEventListener('hide.bs.collapse', event => {
|
|
console.log("Hide collapse triggered for:", event.target.id, "Show class:", event.target.classList.contains('show'));
|
|
if (toggleIcon) {
|
|
toggleIcon.classList.remove('bi-chevron-down');
|
|
toggleIcon.classList.add('bi-chevron-right');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.error("JavaScript error in cp_view_processed_ig.html:", e);
|
|
const body = document.querySelector('body');
|
|
if (body) {
|
|
const errorDiv = document.createElement('div');
|
|
errorDiv.className = 'alert alert-danger m-3';
|
|
errorDiv.textContent = 'A critical error occurred while initializing the page view. Please check the console for details.';
|
|
body.insertBefore(errorDiv, body.firstChild);
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
|
|
{% endblock %} |