Skip to content

Commit 31f240b

Browse files
authored
feat: add option to validate all conventions (#11)
1 parent c44eaca commit 31f240b

8 files changed

Lines changed: 935 additions & 5 deletions

File tree

metazarr/demo/app.js

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
import { openStore, CorsError } from "../src/store.js";
66
import { buildTree, buildTreeFromV3, buildTreeFromCrawl, openNode, insertNode } from "../src/hierarchy.js";
77
import { detectConventions } from "../src/conventions.js";
8+
import { validateStore } from "../src/report.js";
89
import { renderTree, highlightNode } from "./tree.js";
910
import { renderDetail } from "./detail-panel.js";
11+
import { renderReport, renderProgressUI } from "./report-panel.js";
1012

1113
// DOM elements
1214
const form = document.getElementById("open-form");
@@ -22,6 +24,8 @@ const addPathBtn = document.getElementById("add-path-btn");
2224
// App state
2325
let currentStore = null;
2426
let currentTree = null;
27+
let currentReport = null;
28+
let currentAbortController = null;
2529

2630
// --- URL state ---
2731

@@ -95,6 +99,7 @@ async function openStoreFromUrl(url, autoSelectNode) {
9599
}
96100

97101
updateUrlParams(url, null);
102+
injectValidateAllButton();
98103

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

175180
function onNodeSelect(node) {
176181
highlightNode(treeContainer, node.path);
177-
const conventions = detectConventions(node.attrs);
178-
renderDetail(node, conventions, detailPanel);
182+
183+
// If we have a cached report, show a "Back to Report" link
184+
if (currentReport) {
185+
const backLink = document.createElement("button");
186+
backLink.className = "report-back-link";
187+
backLink.textContent = "\u2190 Back to Report";
188+
backLink.addEventListener("click", () => showReport(currentReport));
189+
detailPanel.innerHTML = "";
190+
detailPanel.appendChild(backLink);
191+
192+
// Render detail below the back link
193+
const detailWrapper = document.createElement("div");
194+
detailPanel.appendChild(detailWrapper);
195+
const conventions = detectConventions(node.attrs);
196+
renderDetail(node, conventions, detailWrapper);
197+
} else {
198+
const conventions = detectConventions(node.attrs);
199+
renderDetail(node, conventions, detailPanel);
200+
}
201+
179202
updateUrlParams(urlInput.value.trim(), node.path);
180203
showDetailView();
181204
}
182205

206+
// --- Validate All ---
207+
208+
function injectValidateAllButton() {
209+
// Remove existing button if present
210+
const existing = document.getElementById("validate-all-btn");
211+
if (existing) existing.remove();
212+
213+
const btn = document.createElement("button");
214+
btn.id = "validate-all-btn";
215+
btn.className = "validate-all-btn";
216+
btn.textContent = "Validate All";
217+
btn.addEventListener("click", runValidateAll);
218+
statusEl.appendChild(btn);
219+
}
220+
221+
async function runValidateAll() {
222+
if (!currentTree) return;
223+
224+
// Cancel any in-progress validation
225+
if (currentAbortController) {
226+
currentAbortController.abort();
227+
}
228+
currentAbortController = new AbortController();
229+
currentReport = null;
230+
231+
const progressUI = renderProgressUI(detailPanel);
232+
showDetailView();
233+
234+
try {
235+
const report = await validateStore(currentTree, {
236+
onProgress: ({ completed, total }) => progressUI.update(completed, total),
237+
signal: currentAbortController.signal,
238+
});
239+
currentReport = report;
240+
showReport(report);
241+
} catch (err) {
242+
if (err.name !== "AbortError") {
243+
detailPanel.innerHTML = `<div class="error-banner">Validation failed: ${escapeHtml(err.message)}</div>`;
244+
}
245+
} finally {
246+
currentAbortController = null;
247+
}
248+
}
249+
250+
function showReport(report) {
251+
renderReport(report, onNodeSelect, detailPanel);
252+
}
253+
183254
// --- Manual path addition ---
184255

185256
addPathBtn.addEventListener("click", addPath);

metazarr/demo/detail-panel.js

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,9 +350,36 @@ function createConventionsSection(node, conventions) {
350350
const section = document.createElement("div");
351351
section.className = "conventions-section";
352352

353+
const header = document.createElement("div");
354+
header.className = "conventions-header";
355+
353356
const h3 = document.createElement("h3");
354357
h3.textContent = `Conventions (${conventions.length})`;
355-
section.appendChild(h3);
358+
header.appendChild(h3);
359+
360+
// Validate All button for this node (only if there are validatable conventions)
361+
const validatable = conventions.filter((c) => c.schemaUrl);
362+
if (validatable.length > 1) {
363+
const validateAllBtn = document.createElement("button");
364+
validateAllBtn.className = "validate-btn";
365+
validateAllBtn.textContent = "Validate All";
366+
validateAllBtn.addEventListener("click", async () => {
367+
validateAllBtn.disabled = true;
368+
const cards = section.querySelectorAll(".convention-card");
369+
for (const card of cards) {
370+
const btn = card.querySelector(".validate-btn");
371+
if (btn && btn !== validateAllBtn) {
372+
btn.click();
373+
}
374+
}
375+
// Re-enable after a tick so all async validations have started
376+
await new Promise((r) => setTimeout(r, 0));
377+
validateAllBtn.disabled = false;
378+
});
379+
header.appendChild(validateAllBtn);
380+
}
381+
382+
section.appendChild(header);
356383

357384
if (conventions.length === 0) {
358385
const msg = document.createElement("div");

0 commit comments

Comments
 (0)