Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 73 additions & 2 deletions metazarr/demo/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import { openStore, CorsError } from "../src/store.js";
import { buildTree, buildTreeFromV3, buildTreeFromCrawl, openNode, insertNode } from "../src/hierarchy.js";
import { detectConventions } from "../src/conventions.js";
import { validateStore } from "../src/report.js";
import { renderTree, highlightNode } from "./tree.js";
import { renderDetail } from "./detail-panel.js";
import { renderReport, renderProgressUI } from "./report-panel.js";

// DOM elements
const form = document.getElementById("open-form");
Expand All @@ -22,6 +24,8 @@ const addPathBtn = document.getElementById("add-path-btn");
// App state
let currentStore = null;
let currentTree = null;
let currentReport = null;
let currentAbortController = null;

// --- URL state ---

Expand Down Expand Up @@ -95,6 +99,7 @@ async function openStoreFromUrl(url, autoSelectNode) {
}

updateUrlParams(url, null);
injectValidateAllButton();

// Auto-select node from URL if provided
if (autoSelectNode && currentTree) {
Expand Down Expand Up @@ -174,12 +179,78 @@ backBtn.addEventListener("click", showTreeView);

function onNodeSelect(node) {
highlightNode(treeContainer, node.path);
const conventions = detectConventions(node.attrs);
renderDetail(node, conventions, detailPanel);

// If we have a cached report, show a "Back to Report" link
if (currentReport) {
const backLink = document.createElement("button");
backLink.className = "report-back-link";
backLink.textContent = "\u2190 Back to Report";
backLink.addEventListener("click", () => showReport(currentReport));
detailPanel.innerHTML = "";
detailPanel.appendChild(backLink);

// Render detail below the back link
const detailWrapper = document.createElement("div");
detailPanel.appendChild(detailWrapper);
const conventions = detectConventions(node.attrs);
renderDetail(node, conventions, detailWrapper);
} else {
const conventions = detectConventions(node.attrs);
renderDetail(node, conventions, detailPanel);
}

updateUrlParams(urlInput.value.trim(), node.path);
showDetailView();
}

// --- Validate All ---

function injectValidateAllButton() {
// Remove existing button if present
const existing = document.getElementById("validate-all-btn");
if (existing) existing.remove();

const btn = document.createElement("button");
btn.id = "validate-all-btn";
btn.className = "validate-all-btn";
btn.textContent = "Validate All";
btn.addEventListener("click", runValidateAll);
statusEl.appendChild(btn);
}

async function runValidateAll() {
if (!currentTree) return;

// Cancel any in-progress validation
if (currentAbortController) {
currentAbortController.abort();
}
currentAbortController = new AbortController();
currentReport = null;

const progressUI = renderProgressUI(detailPanel);
showDetailView();

try {
const report = await validateStore(currentTree, {
onProgress: ({ completed, total }) => progressUI.update(completed, total),
signal: currentAbortController.signal,
});
currentReport = report;
showReport(report);
} catch (err) {
if (err.name !== "AbortError") {
detailPanel.innerHTML = `<div class="error-banner">Validation failed: ${escapeHtml(err.message)}</div>`;
}
} finally {
currentAbortController = null;
}
}

function showReport(report) {
renderReport(report, onNodeSelect, detailPanel);
}

// --- Manual path addition ---

addPathBtn.addEventListener("click", addPath);
Expand Down
29 changes: 28 additions & 1 deletion metazarr/demo/detail-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -350,9 +350,36 @@ function createConventionsSection(node, conventions) {
const section = document.createElement("div");
section.className = "conventions-section";

const header = document.createElement("div");
header.className = "conventions-header";

const h3 = document.createElement("h3");
h3.textContent = `Conventions (${conventions.length})`;
section.appendChild(h3);
header.appendChild(h3);

// Validate All button for this node (only if there are validatable conventions)
const validatable = conventions.filter((c) => c.schemaUrl);
if (validatable.length > 1) {
const validateAllBtn = document.createElement("button");
validateAllBtn.className = "validate-btn";
validateAllBtn.textContent = "Validate All";
validateAllBtn.addEventListener("click", async () => {
validateAllBtn.disabled = true;
const cards = section.querySelectorAll(".convention-card");
for (const card of cards) {
const btn = card.querySelector(".validate-btn");
if (btn && btn !== validateAllBtn) {
btn.click();
}
}
// Re-enable after a tick so all async validations have started
await new Promise((r) => setTimeout(r, 0));
validateAllBtn.disabled = false;
});
header.appendChild(validateAllBtn);
}

section.appendChild(header);

if (conventions.length === 0) {
const msg = document.createElement("div");
Expand Down
Loading
Loading