FHIRFLARE-IG-Toolkit/templates/cp_view_processed_ig.html
Sudo-JHare f38b6aedba Incremental update
Update Push IG  log file with additional detail.
enahanced validation report structure
fixed the DANM expand collapse chevron, every UI change breaks that functionality
fixed the copy raw structure, pretty json, and raw json copy buttons so they work independant.
2025-04-14 21:38:36 +10:00

780 lines
44 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 ms-2" 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" 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" id="copy-raw-content-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 example content to clipboard</span>
</button>
</div>
<pre><code id="example-content-raw" class="p-2 d-block border bg-light" style="max-height: 400px; overflow-y: auto;"></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" 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" 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>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');
// --- Added: Buttons & Tooltips Constants (Using Corrected IDs) ---
const copyRawDefButton = document.getElementById('copy-raw-def-button');
const copyRawExButton = document.getElementById('copy-raw-content-button');
const copyPrettyJsonButton = document.getElementById('copy-pretty-json-button');
let copyRawDefTooltipInstance = null;
let copyRawExTooltipInstance = null;
let copyPrettyJsonTooltipInstance = null;
const structureBaseUrl = "{{ url_for('get_structure_definition') }}";
const exampleBaseUrl = "{{ url_for('get_example_content') }}";
// --- Added: 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;
}
// Dispose previous instance if exists, then create new one
bootstrap.Tooltip.getInstance(buttonElement)?.dispose();
tooltipInstance = new bootstrap.Tooltip(buttonElement);
// Store handler reference to potentially remove later if needed
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);
}
};
// Remove potentially old listener - best effort without storing reference
// buttonElement.removeEventListener('click', clickHandler); // May not work reliably without named fn
buttonElement.addEventListener('click', clickHandler); // Add the listener
return tooltipInstance;
}
// --- End Added ---
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 });
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);
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 || [], resourceType);
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 loading structure: ${error.message}</div>`;
rawStructureContent.textContent = `Error loading structure: ${error.message}`;
populateExampleSelector(resourceType);
})
.finally(() => {
structureLoading.style.display = 'none';
rawStructureLoading.style.display = 'none';
});
});
});
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';
}
}
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';
// --- ADDED: Reset Example Copy Button Tooltips ---
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'); }
// --- END ADDED ---
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(errText => { throw new Error(errText || `HTTP error ${response.status}`); });
}
return response.text();
})
.then(content => {
console.log("Example content received.");
exampleFilename.textContent = `Source: ${selectedFilePath.split('/').pop()}`;
exampleContentRaw.textContent = content;
try {
const jsonContent = JSON.parse(content);
exampleContentJson.textContent = JSON.stringify(jsonContent, null, 2);
} catch (e) {
console.warn("Example content is not valid JSON:", e.message);
exampleContentJson.textContent = '(Not valid JSON)';
}
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';
});
});
}
// --- ADDED: Setup Copy Buttons Calls---
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');
// --- END ADDED ---
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;
// Handle slices using id for accurate parent lookup
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]', '');
// Adjust parentPath for slices using id
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}`);
// Add modifierExtension for DomainResource, BackboneElement, or explicitly defined
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}`);
// Handle choice elements ([x])
if (isChoiceElement && el.type && el.type.length > 1) {
el.type.forEach(type => {
if (type.code === 'dateTime') return; // Skip dateTime as per FHIR convention
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 isOptional = min == 0 && isMustSupport && el.type && el.type.some(t => t.code === 'Extension') && path.includes('extension');
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="${isOptional ? 'Optional Must Support Extension' : 'Must Support'}"></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>`;
itemHtml += `<code class="fw-bold ms-1" title="${path}">${node.name}</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);
}
});
// Initialize collapse with manual click handling and debouncing
let lastClickTime = 0;
const debounceDelay = 200; // ms
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);
// Initialize Bootstrap Collapse
const bsCollapse = new bootstrap.Collapse(collapseEl, {
toggle: false
});
// Custom click handler with debounce
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();
}
});
// Update icon and log state
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');
}
});
collapseEl.addEventListener('shown.bs.collapse', event => {
console.log("Shown collapse completed for:", event.target.id, "Show class:", event.target.classList.contains('show'));
});
collapseEl.addEventListener('hidden.bs.collapse', event => {
console.log("Hidden collapse completed for:", event.target.id, "Show class:", event.target.classList.contains('show'));
});
});
}
} 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 %}