mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-06-15 00:40:00 +00:00
Updated DOM
added some DOM fixes for collapse and expand events on view page and fallback display for resources.
This commit is contained in:
parent
1e43e0c4bb
commit
95f80fbe91
55
app.py
55
app.py
@ -289,7 +289,7 @@ def unload_ig():
|
||||
flash("Invalid package ID.", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
processed_ig = ProcessedIg.query.get(ig_id)
|
||||
processed_ig = db.session.get(ProcessedIg, ig_id)
|
||||
if processed_ig:
|
||||
try:
|
||||
db.session.delete(processed_ig)
|
||||
@ -318,16 +318,54 @@ def get_structure_definition():
|
||||
resource_identifier = request.args.get('resource_type')
|
||||
if not all([package_name, package_version, resource_identifier]):
|
||||
return jsonify({"error": "Missing query parameters"}), 400
|
||||
|
||||
# First, try to get the structure definition from the specified package
|
||||
tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], services._construct_tgz_filename(package_name, package_version))
|
||||
if not os.path.exists(tgz_path):
|
||||
return jsonify({"error": f"Package file not found: {tgz_path}"}), 404
|
||||
sd_data, _ = services.find_and_extract_sd(tgz_path, resource_identifier)
|
||||
sd_data = None
|
||||
fallback_used = False
|
||||
|
||||
if os.path.exists(tgz_path):
|
||||
sd_data, _ = services.find_and_extract_sd(tgz_path, resource_identifier)
|
||||
|
||||
# If not found, fall back to the core FHIR package (hl7.fhir.r4.core#4.0.1)
|
||||
if sd_data is None:
|
||||
return jsonify({"error": f"SD for '{resource_identifier}' not found."}), 404
|
||||
logger.debug(f"Structure definition for '{resource_identifier}' not found in {package_name}#{package_version}, attempting fallback to hl7.fhir.r4.core#4.0.1")
|
||||
core_package_name = "hl7.fhir.r4.core"
|
||||
core_package_version = "4.0.1"
|
||||
|
||||
# Ensure the core package is downloaded
|
||||
core_tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], services._construct_tgz_filename(core_package_name, core_package_version))
|
||||
if not os.path.exists(core_tgz_path):
|
||||
logger.debug(f"Core package {core_package_name}#{core_package_version} not found, attempting to download")
|
||||
try:
|
||||
result = services.import_package_and_dependencies(core_package_name, core_package_version)
|
||||
if result['errors'] and not result['downloaded']:
|
||||
logger.error(f"Failed to download core package: {result['errors'][0]}")
|
||||
return jsonify({"error": f"SD for '{resource_identifier}' not found in {package_name}#{package_version}, and failed to download core package: {result['errors'][0]}"}), 404
|
||||
except Exception as e:
|
||||
logger.error(f"Error downloading core package: {str(e)}")
|
||||
return jsonify({"error": f"SD for '{resource_identifier}' not found in {package_name}#{package_version}, and error downloading core package: {str(e)}"}), 500
|
||||
|
||||
# Try to extract the structure definition from the core package
|
||||
if os.path.exists(core_tgz_path):
|
||||
sd_data, _ = services.find_and_extract_sd(core_tgz_path, resource_identifier)
|
||||
if sd_data is None:
|
||||
return jsonify({"error": f"SD for '{resource_identifier}' not found in {package_name}#{package_version} or in core package {core_package_name}#{core_package_version}."}), 404
|
||||
fallback_used = True
|
||||
else:
|
||||
return jsonify({"error": f"SD for '{resource_identifier}' not found in {package_name}#{package_version}, and core package {core_package_name}#{core_package_version} could not be located."}), 404
|
||||
|
||||
elements = sd_data.get('snapshot', {}).get('element', []) or sd_data.get('differential', {}).get('element', [])
|
||||
processed_ig = ProcessedIg.query.filter_by(package_name=package_name, version=package_version).first()
|
||||
must_support_paths = processed_ig.must_support_elements.get(resource_identifier, []) if processed_ig else []
|
||||
return jsonify({"elements": elements, "must_support_paths": must_support_paths})
|
||||
|
||||
response = {
|
||||
"elements": elements,
|
||||
"must_support_paths": must_support_paths,
|
||||
"fallback_used": fallback_used,
|
||||
"source_package": f"{core_package_name}#{core_package_version}" if fallback_used else f"{package_name}#{package_version}"
|
||||
}
|
||||
return jsonify(response)
|
||||
|
||||
@app.route('/get-example')
|
||||
def get_example_content():
|
||||
@ -579,10 +617,5 @@ with app.app_context():
|
||||
db.create_all()
|
||||
logger.debug("Database initialization complete")
|
||||
|
||||
# Add route to serve favicon
|
||||
@app.route('/favicon.ico')
|
||||
def favicon():
|
||||
return send_from_directory(os.path.join(app.root_path, 'static'), 'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
@ -101,7 +101,7 @@ def find_and_extract_sd(tgz_path, resource_identifier): # Public version
|
||||
if fileobj: fileobj.close() # Ensure resource cleanup
|
||||
|
||||
if sd_data is None:
|
||||
logger.warning(f"SD matching '{resource_identifier}' not found within archive {os.path.basename(tgz_path)}")
|
||||
logger.info(f"SD matching '{resource_identifier}' not found within archive {os.path.basename(tgz_path)} - caller may attempt fallback")
|
||||
except tarfile.TarError as e:
|
||||
logger.error(f"TarError reading {tgz_path} in find_and_extract_sd: {e}")
|
||||
raise tarfile.TarError(f"Error reading package archive: {e}") from e
|
||||
@ -182,7 +182,6 @@ def extract_dependencies(tgz_path):
|
||||
error_message = f"Unexpected error extracting deps: {e}"; logger.error(error_message, exc_info=True); dependencies = None
|
||||
return dependencies, error_message
|
||||
|
||||
|
||||
# --- Recursive Import Orchestrator ---
|
||||
def import_package_and_dependencies(initial_name, initial_version):
|
||||
"""Orchestrates recursive download and dependency extraction."""
|
||||
@ -247,7 +246,6 @@ def import_package_and_dependencies(initial_name, initial_version):
|
||||
logger.info(f"Import finished. Processed: {proc_count}, Downloaded/Verified: {dl_count}, Errors: {err_count}")
|
||||
return results
|
||||
|
||||
|
||||
# --- Package File Content Processor (V6.2 - Fixed MS path handling) ---
|
||||
def process_package_file(tgz_path):
|
||||
""" Extracts types, profile status, MS elements, and examples from a downloaded .tgz package (Single Pass). """
|
||||
|
@ -1,9 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="px-4 py-5 my-5 text “
|
||||
|
||||
center">
|
||||
<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">
|
||||
@ -49,10 +47,10 @@ center">
|
||||
<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 }}">
|
||||
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 %}
|
||||
@ -65,15 +63,15 @@ center">
|
||||
<p class="text-muted"><em>No profiles defined.</em></p>
|
||||
{% endif %}
|
||||
{% if base_list %}
|
||||
<p class="mb-2"><small><span class="badge bg-primary text-light border me-1">Examples</span> = - Examples will be displayed when selecting Base Types if contained in the IG</small></p>
|
||||
<p class="mb-2"><small><span class="badge bg-primary text-light border me-1">Examples</span> = - Examples will be displayed when selecting Base Types if contained in the IG</small></p>
|
||||
<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 }}">
|
||||
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 %}
|
||||
@ -101,6 +99,7 @@ center">
|
||||
<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>
|
||||
@ -148,17 +147,8 @@ center">
|
||||
{% else %}
|
||||
<div class="alert alert-warning" role="alert">Processed IG details not available.</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Debugging: Display raw data for verification ------------------------------------------------------------------------------
|
||||
<div class="mt-4">
|
||||
<details>
|
||||
<summary class="text-muted small">Debug: Raw Data (Click to Expand)</summary>
|
||||
<pre class="p-2 border bg-light">Resource Types Info: {{ processed_ig.resource_types_info | tojson | safe }}</pre>
|
||||
<pre class="p-2 border bg-light">Examples: {{ processed_ig.examples | tojson | safe }}</pre>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
-------------------------------------------------------------------------------------------------------------DEBUG end -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>const examplesData = {{ examples_by_type | tojson | safe }};</script>
|
||||
<script>
|
||||
@ -168,6 +158,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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');
|
||||
@ -188,54 +179,73 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// Debugging: Log initial data
|
||||
console.log("Examples Data:", examplesData);
|
||||
|
||||
document.body.addEventListener('click', function(event) {
|
||||
const link = event.target.closest('.resource-type-link');
|
||||
if (!link) return;
|
||||
event.preventDefault();
|
||||
// Debug all clicks to identify interference
|
||||
document.addEventListener('click', function(event) {
|
||||
console.log("Click event on:", event.target, "Class:", event.target.className);
|
||||
}, true);
|
||||
|
||||
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 });
|
||||
return;
|
||||
}
|
||||
// Handle clicks on resource-type-link within resource-type-list containers
|
||||
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 structureParams = new URLSearchParams({ package_name: pkgName, package_version: pkgVersion, resource_type: resourceType });
|
||||
const structureFetchUrl = `${structureBaseUrl}?${structureParams.toString()}`;
|
||||
console.log("Fetching structure from:", structureFetchUrl);
|
||||
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 });
|
||||
return;
|
||||
}
|
||||
|
||||
structureTitle.textContent = `${resourceType} (${pkgName}#${pkgVersion})`;
|
||||
rawStructureTitle.textContent = `${resourceType} (${pkgName}#${pkgVersion})`;
|
||||
structureDisplay.innerHTML = '';
|
||||
rawStructureContent.textContent = '';
|
||||
structureLoading.style.display = 'block';
|
||||
rawStructureLoading.style.display = 'block';
|
||||
structureDisplayWrapper.style.display = 'block';
|
||||
rawStructureWrapper.style.display = 'block';
|
||||
exampleDisplayWrapper.style.display = 'none';
|
||||
const structureParams = new URLSearchParams({ package_name: pkgName, package_version: pkgVersion, resource_type: resourceType });
|
||||
const structureFetchUrl = `${structureBaseUrl}?${structureParams.toString()}`;
|
||||
console.log("Fetching structure from:", structureFetchUrl);
|
||||
|
||||
fetch(structureFetchUrl)
|
||||
.then(response => {
|
||||
console.log("Structure fetch response status:", response.status);
|
||||
return response.json().then(data => ({ ok: response.ok, status: response.status, data }));
|
||||
})
|
||||
.then(result => {
|
||||
if (!result.ok) throw new Error(result.data.error || `HTTP error ${result.status}`);
|
||||
console.log("Structure data received:", result.data);
|
||||
renderStructureTree(result.data.elements, result.data.must_support_paths || []);
|
||||
rawStructureContent.textContent = JSON.stringify(result.data, null, 2);
|
||||
populateExampleSelector(resourceType);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error fetching structure:", error);
|
||||
structureDisplay.innerHTML = `<div class="alert alert-danger">Error: ${error.message}</div>`;
|
||||
rawStructureContent.textContent = `Error: ${error.message}`;
|
||||
})
|
||||
.finally(() => {
|
||||
structureLoading.style.display = 'none';
|
||||
rawStructureLoading.style.display = 'none';
|
||||
});
|
||||
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);
|
||||
return response.json().then(data => ({ ok: response.ok, status: response.status, data }));
|
||||
})
|
||||
.then(result => {
|
||||
if (!result.ok) {
|
||||
throw new Error(result.data.error || `HTTP error ${result.status}`);
|
||||
}
|
||||
console.log("Structure data received:", result.data);
|
||||
if (result.data.fallback_used) {
|
||||
structureFallbackMessage.style.display = 'block';
|
||||
structureFallbackMessage.textContent = `Structure definition not found in this IG; using base FHIR definition from ${result.data.source_package}.`;
|
||||
} else {
|
||||
structureFallbackMessage.style.display = 'none';
|
||||
}
|
||||
renderStructureTree(result.data.elements, result.data.must_support_paths || []);
|
||||
rawStructureContent.textContent = JSON.stringify(result.data, null, 2);
|
||||
populateExampleSelector(resourceType);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error fetching structure:", error);
|
||||
structureFallbackMessage.style.display = 'none';
|
||||
structureDisplay.innerHTML = `<div class="alert alert-danger">Error: ${error.message}</div>`;
|
||||
rawStructureContent.textContent = `Error: ${error.message}`;
|
||||
})
|
||||
.finally(() => {
|
||||
structureLoading.style.display = 'none';
|
||||
rawStructureLoading.style.display = 'none';
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function populateExampleSelector(resourceOrProfileIdentifier) {
|
||||
@ -355,6 +365,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const mustSupportDisplay = isMustSupport ? '<i class="bi bi-check-circle-fill text-warning"></i>' : '';
|
||||
const hasChildren = Object.keys(node.children).length > 0;
|
||||
const collapseId = `collapse-${path.replace(/[\.\:]/g, '-')}`;
|
||||
console.log(`Rendering node: ${path}, Collapse ID: ${collapseId}`); // Debug collapse IDs
|
||||
const padding = level * 20;
|
||||
const pathStyle = `padding-left: ${padding}px; white-space: nowrap;`;
|
||||
let typeString = 'N/A';
|
||||
@ -378,7 +389,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
childrenHtml += `</ul>`;
|
||||
}
|
||||
let itemHtml = `<li class="${liClass}">`;
|
||||
itemHtml += `<div class="row gx-2 align-items-center ${hasChildren ? 'collapse-toggle' : ''}" ${hasChildren ? `data-bs-toggle="collapse" href="#${collapseId}" role="button" aria-expanded="false" aria-controls="${collapseId}"` : ''}>`;
|
||||
itemHtml += `<div class="row gx-2 align-items-center ${hasChildren ? 'collapse-toggle' : ''}" ${hasChildren ? `data-bs-toggle="collapse" data-bs-target="#${collapseId}" role="button" aria-expanded="false" aria-controls="${collapseId}"` : ''}>`;
|
||||
itemHtml += `<div class="col-lg-4 col-md-3 text-truncate" style="${pathStyle}"><span style="display: inline-block; width: 1.2em; text-align: center;">`;
|
||||
if (hasChildren) {
|
||||
itemHtml += `<i class="bi bi-chevron-right small toggle-icon"></i>`;
|
||||
@ -391,11 +402,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (definition) {
|
||||
const escapedDefinition = definition
|
||||
.replace(/\\/g, '\\\\') // Escape backslashes
|
||||
.replace(/"/g, '\\"') // Escape double quotes
|
||||
.replace(/'/g, "\\'") // Escape single quotes
|
||||
.replace(/\n/g, '\\n') // Escape newlines
|
||||
.replace(/\r/g, '\\r') // Escape carriage returns
|
||||
.replace(/\t/g, '\\t'); // Escape tabs
|
||||
.replace(/"/g, '\\"') // Escape double quotes
|
||||
.replace(/'/g, "\\'") // Escape single quotes
|
||||
.replace(/\n/g, '\\n') // Escape newlines
|
||||
.replace(/\r/g, '\\r') // Escape carriage returns
|
||||
.replace(/\t/g, '\\t'); // Escape tabs
|
||||
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}</div>`;
|
||||
@ -423,13 +434,34 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
tooltipTriggerList.forEach(function (tooltipTriggerEl) {
|
||||
new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
structureDisplay.querySelectorAll('.collapse').forEach(collapseEl => {
|
||||
collapseEl.addEventListener('show.bs.collapse', event => {
|
||||
event.target.previousElementSibling.querySelector('.toggle-icon')?.classList.replace('bi-chevron-right', 'bi-chevron-down');
|
||||
});
|
||||
collapseEl.addEventListener('hide.bs.collapse', event => {
|
||||
event.target.previousElementSibling.querySelector('.toggle-icon')?.classList.replace('bi-chevron-down', 'bi-chevron-right');
|
||||
});
|
||||
|
||||
// Initialize collapse toggles and chevron icons *after* rendering
|
||||
const collapseToggles = structureDisplay.querySelectorAll('.collapse-toggle');
|
||||
collapseToggles.forEach(toggleEl => {
|
||||
const collapseId = toggleEl.getAttribute('data-bs-target');
|
||||
const collapseEl = document.querySelector(collapseId);
|
||||
const toggleIcon = toggleEl.querySelector('.toggle-icon');
|
||||
|
||||
if (collapseEl && toggleIcon) {
|
||||
// Initially hide the collapse element
|
||||
collapseEl.style.display = 'none';
|
||||
|
||||
// Handle click on the toggle element
|
||||
toggleEl.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation(); // Stop propagation to prevent conflicts
|
||||
|
||||
// Toggle visibility with a slight delay
|
||||
const isExpanding = collapseEl.style.display === 'none';
|
||||
|
||||
toggleIcon.classList.replace(isExpanding ? 'bi-chevron-right' : 'bi-chevron-down', isExpanding ? 'bi-chevron-down' : 'bi-chevron-right');
|
||||
|
||||
// Use setTimeout for a slight delay before showing/hiding
|
||||
setTimeout(() => {
|
||||
collapseEl.style.display = isExpanding ? 'block' : 'none'; // Or 'flex', 'grid', etc.
|
||||
}, 5); // A very small delay (milliseconds)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user