You've already forked Organizations_register
Import UAPF package
245 lines
8.8 KiB
HTML
245 lines
8.8 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>VDVC Register Viewer</title>
|
|
<style>
|
|
:root { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
|
|
html, body { height: 100%; }
|
|
body { margin: 0; display: flex; flex-direction: column; overflow: hidden; }
|
|
header { padding: 12px 16px; border-bottom: 1px solid #ddd; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
|
header .meta { margin-left: auto; opacity: 0.85; font-size: 12px; }
|
|
|
|
main { display: grid; grid-template-columns: 1fr 420px; gap: 0; flex: 1; min-height: 0; overflow: hidden; }
|
|
|
|
.panel { border-right: 1px solid #eee; display: flex; flex-direction: column; min-height: 0; }
|
|
.panel:last-child { border-right: 0; }
|
|
.toolbar { padding: 10px 12px; border-bottom: 1px solid #eee; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
|
button { padding: 8px 10px; border: 1px solid #ccc; border-radius: 8px; background: #fff; cursor: pointer; }
|
|
button:disabled { opacity: 0.55; cursor: not-allowed; }
|
|
.status { font-size: 12px; opacity: 0.85; }
|
|
|
|
textarea {
|
|
width: 100%;
|
|
flex: 1;
|
|
min-height: 0;
|
|
border: 0;
|
|
outline: none;
|
|
padding: 12px;
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
font-size: 13px;
|
|
line-height: 1.35;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
overflow: auto;
|
|
}
|
|
|
|
pre { margin: 0; padding: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.35; white-space: pre-wrap; word-break: break-word; }
|
|
.side { flex: 1; min-height: 0; overflow: auto; }
|
|
.pill { display: inline-block; padding: 2px 8px; border-radius: 999px; border: 1px solid #ddd; font-size: 12px; }
|
|
.warn { color: #b00; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<strong>VDVC Register Viewer</strong>
|
|
<span class="pill" id="dirtyPill">clean</span>
|
|
<span class="pill" id="connPill">not connected</span>
|
|
<div class="meta" id="meta"></div>
|
|
</header>
|
|
|
|
<main>
|
|
<section class="panel">
|
|
<div class="toolbar">
|
|
<button id="btnLoad">Load</button>
|
|
<button id="btnSave" disabled>Save (commit)</button>
|
|
<button id="btnFormat">Format</button>
|
|
<button id="btnValidate">Check XML</button>
|
|
<span class="status" id="status"></span>
|
|
</div>
|
|
<textarea id="editor" spellcheck="false"></textarea>
|
|
</section>
|
|
|
|
<section class="panel">
|
|
<div class="toolbar">
|
|
<strong>Info</strong>
|
|
<span class="status" id="infoStatus"></span>
|
|
</div>
|
|
<div class="side">
|
|
<pre id="info"></pre>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<script>
|
|
const els = {
|
|
dirtyPill: document.getElementById('dirtyPill'),
|
|
connPill: document.getElementById('connPill'),
|
|
meta: document.getElementById('meta'),
|
|
status: document.getElementById('status'),
|
|
infoStatus: document.getElementById('infoStatus'),
|
|
info: document.getElementById('info'),
|
|
editor: document.getElementById('editor'),
|
|
btnLoad: document.getElementById('btnLoad'),
|
|
btnSave: document.getElementById('btnSave'),
|
|
btnFormat: document.getElementById('btnFormat'),
|
|
btnValidate: document.getElementById('btnValidate'),
|
|
};
|
|
|
|
let payload = null;
|
|
let dirty = false;
|
|
let loadedText = '';
|
|
|
|
function setDirty(v) {
|
|
dirty = v;
|
|
els.dirtyPill.textContent = dirty ? 'dirty' : 'clean';
|
|
els.btnSave.disabled = !dirty;
|
|
parent.postMessage({ type: 'PGV_DIRTY', dirty }, '*');
|
|
}
|
|
function setConnected(v) { els.connPill.textContent = v ? 'connected' : 'not connected'; }
|
|
function setStatus(msg) { els.status.textContent = msg || ''; }
|
|
function setInfoStatus(msg) { els.infoStatus.textContent = msg || ''; }
|
|
|
|
function safeJson(obj) { try { return JSON.stringify(obj, null, 2); } catch { return String(obj); } }
|
|
|
|
function formatXml(xml) {
|
|
const PADDING = ' ';
|
|
let formatted = '';
|
|
let indent = 0;
|
|
xml = xml.replace(/>\s+</g, '><').trim();
|
|
xml.split(/(?=<)/g).forEach(node => {
|
|
if (!node) return;
|
|
if (node.match(/^<\/\w/)) indent = Math.max(indent - 1, 0);
|
|
formatted += PADDING.repeat(indent) + node.trim() + '\n';
|
|
if (node.match(/^<\w([^>]*[^/])?>$/) && !node.startsWith('<?') && !node.startsWith('<!')) indent += 1;
|
|
});
|
|
return formatted.trim() + '\n';
|
|
}
|
|
|
|
function validateXml(xmlText) {
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(xmlText, 'application/xml');
|
|
const err = doc.querySelector('parsererror');
|
|
return err ? (err.textContent || 'XML parse error') : null;
|
|
}
|
|
|
|
// -------- Parent fetch proxy (NO direct fetch in iframe) --------
|
|
function pgvFetch(url) {
|
|
return new Promise((resolve, reject) => {
|
|
const reqId = Math.random().toString(36).slice(2);
|
|
const onMsg = (ev) => {
|
|
const m = ev.data;
|
|
if (!m || typeof m !== 'object') return;
|
|
if (m.type !== 'PGV_FETCH_RESULT') return;
|
|
if (m.reqId !== reqId) return;
|
|
window.removeEventListener('message', onMsg);
|
|
if (m.ok) resolve(m.text || '');
|
|
else reject(new Error(m.error || 'fetch failed'));
|
|
};
|
|
window.addEventListener('message', onMsg);
|
|
parent.postMessage({ type: 'PGV_FETCH', reqId, url }, '*');
|
|
});
|
|
}
|
|
|
|
function toAbs(url) {
|
|
// payload urls are usually relative to same origin; parent will enforce allowlist
|
|
return url;
|
|
}
|
|
|
|
async function tryLoadViaApiUrl() {
|
|
if (!payload?.apiUrl) return null;
|
|
const text = await pgvFetch(toAbs(payload.apiUrl));
|
|
// api might return JSON {content:"..."}
|
|
try {
|
|
const j = JSON.parse(text);
|
|
if (j && typeof j.content === 'string') return j.content;
|
|
} catch {}
|
|
return null;
|
|
}
|
|
|
|
async function tryLoadViaRawTarget() {
|
|
const t = payload?.targets || {};
|
|
const xmlPath = t.xml || payload?.path || null;
|
|
if (!xmlPath) return null;
|
|
|
|
// If it's already a /raw/... URL in payload.targets, use it; otherwise parent can resolve it if you pass a raw URL.
|
|
// The safest is to ask for /raw/ path directly from ProcessGit; we assume parent will accept same-origin URLs.
|
|
const maybeUrl = (typeof xmlPath === 'string') ? xmlPath : null;
|
|
if (!maybeUrl) return null;
|
|
|
|
// If viewer config gave only "vdvc-register.xml", parent won't know how to map.
|
|
// So we prefer when targets.xml is a full raw URL. If it isn't, just return null.
|
|
if (!maybeUrl.includes('/raw/')) return null;
|
|
|
|
const text = await pgvFetch(toAbs(maybeUrl));
|
|
return text;
|
|
}
|
|
|
|
async function doLoad() {
|
|
setStatus('Loading...');
|
|
try {
|
|
let content = await tryLoadViaApiUrl();
|
|
if (content == null) content = await tryLoadViaRawTarget();
|
|
|
|
if (content == null) throw new Error('No usable apiUrl or raw xml target.');
|
|
|
|
els.editor.value = content;
|
|
loadedText = content;
|
|
setDirty(false);
|
|
setStatus('Loaded.');
|
|
} catch (e) {
|
|
setStatus('Load failed.');
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
function doFormat() {
|
|
const v = els.editor.value || '';
|
|
const err = validateXml(v);
|
|
if (err) { setStatus('Cannot format: XML invalid'); return; }
|
|
els.editor.value = formatXml(v);
|
|
setDirty(els.editor.value !== loadedText);
|
|
setStatus('Formatted.');
|
|
}
|
|
|
|
function doValidate() {
|
|
const v = els.editor.value || '';
|
|
const err = validateXml(v);
|
|
if (err) { setStatus('Invalid XML'); setInfoStatus('Invalid XML'); return; }
|
|
setStatus('XML OK'); setInfoStatus('XML OK');
|
|
}
|
|
|
|
function doSave() {
|
|
setStatus('Use parent Save to commit.');
|
|
}
|
|
|
|
els.editor.addEventListener('input', () => setDirty((els.editor.value || '') !== loadedText));
|
|
els.btnLoad.addEventListener('click', doLoad);
|
|
els.btnFormat.addEventListener('click', doFormat);
|
|
els.btnValidate.addEventListener('click', doValidate);
|
|
els.btnSave.addEventListener('click', doSave);
|
|
|
|
window.addEventListener('message', (ev) => {
|
|
const msg = ev.data;
|
|
if (!msg || typeof msg !== 'object') return;
|
|
|
|
if (msg.type === 'PGV_INIT') {
|
|
payload = msg.payload || null;
|
|
setConnected(true);
|
|
els.meta.textContent = payload ? `${payload.branch || ''} ${payload.path || ''}`.trim() : '';
|
|
els.info.textContent = payload ? safeJson(payload) : 'No payload';
|
|
doLoad();
|
|
}
|
|
|
|
if (msg.type === 'PGV_COMMIT_OK') {
|
|
loadedText = els.editor.value || '';
|
|
setDirty(false);
|
|
setStatus('Committed.');
|
|
}
|
|
});
|
|
|
|
parent.postMessage({ type: 'PGV_READY' }, '*');
|
|
</script>
|
|
</body>
|
|
</html> |