253 lines
9.4 KiB
JavaScript
253 lines
9.4 KiB
JavaScript
// ===================== QA App – komplette app.js =====================
|
||
|
||
// === DocBee: Token HIER eintragen ===
|
||
const DOCBEE_TOKEN = window.DOCBEE_TOKEN || ""; // DocBee Token eintragen"
|
||
const ENABLE_FALLBACK_NOTE = false; // auf true setzen, falls bei Fehler automatisch Notiz angelegt werden soll
|
||
|
||
window.addEventListener('error', e => {
|
||
try {
|
||
console.error(e.error || e.message || e);
|
||
} catch (_) {}
|
||
alert('JS-Fehler: ' + (e.message || e));
|
||
});
|
||
|
||
let template = null; // {name,module,module_version,pbx_version,steps:[...]}
|
||
|
||
|
||
|
||
|
||
// === DOM-Refs ===
|
||
const els = {
|
||
yamlFile: document.getElementById('yamlFile'),
|
||
stepsTableBody: document.querySelector('#stepsTable tbody'),
|
||
tplName: document.getElementById('tplName'),
|
||
statusTag: document.getElementById('statusTag'),
|
||
module: document.getElementById('module'),
|
||
moduleVersion: document.getElementById('moduleVersion'),
|
||
pbxVersion: document.getElementById('pbxVersion'),
|
||
tester: document.getElementById('tester'),
|
||
olmNummer: document.getElementById('olmNummer'),
|
||
docbeeUrl: document.getElementById('docbeeUrl'),
|
||
btnAddStep: document.getElementById('btnAddStep'),
|
||
btnSaveJSON: document.getElementById('btnSaveJSON'),
|
||
loadJSON: document.getElementById('loadJSON'),
|
||
btnAddGroup: document.getElementById('btnAddGroup'),
|
||
btnExportTemplateYAML: document.getElementById('btnExportTemplateYAML'),
|
||
btnExportAll: document.getElementById('btnExportAll'),
|
||
gitlabTplSelect: document.getElementById('gitlabTplSelect'),
|
||
gitlabTplStatus: document.getElementById('gitlabTplStatus'),
|
||
docbeeTokenStatus: document.getElementById('docbeeTokenStatus'),
|
||
btnGitlabReload: document.getElementById('btnGitlabReload'),
|
||
};
|
||
// === Drag Guard + Observer: Nur Drag über expliziten Griff zulassen ===
|
||
function enforceDragHandleOnly() {
|
||
if (!els.stepsTableBody) return;
|
||
els.stepsTableBody.querySelectorAll('tr[draggable="true"]').forEach(tr => tr.setAttribute('draggable', 'false'));
|
||
els.stepsTableBody.querySelectorAll('.drag-handle').forEach(h => h.setAttribute('draggable', 'true'));
|
||
}
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
if (els.stepsTableBody) {
|
||
const mo = new MutationObserver(() => enforceDragHandleOnly());
|
||
mo.observe(els.stepsTableBody, {
|
||
childList: true,
|
||
subtree: true
|
||
});
|
||
enforceDragHandleOnly();
|
||
}
|
||
});
|
||
document.addEventListener('dragstart', (e) => {
|
||
if (!e.target.closest('.drag-handle')) {
|
||
e.preventDefault();
|
||
}
|
||
}, {
|
||
capture: true
|
||
});
|
||
|
||
|
||
// === GitLab Events ===
|
||
if (els.gitlabTplSelect) els.gitlabTplSelect.addEventListener('change', async (e) => {
|
||
const p = e.target.value;
|
||
if (!p) return;
|
||
try {
|
||
els.gitlabTplStatus && (els.gitlabTplStatus.textContent = "GitLab: lade Datei…");
|
||
const y = await fetchGitlabFileRaw(p);
|
||
loadYAMLText(y);
|
||
els.gitlabTplStatus && (els.gitlabTplStatus.textContent = `GitLab: geladen – ${p.split('/').pop()}`);
|
||
} catch (err) {
|
||
els.gitlabTplStatus && (els.gitlabTplStatus.textContent = "GitLab: Fehler");
|
||
console.error("[GitLab] raw failed:", err);
|
||
alert("Vorlage aus GitLab konnte nicht geladen werden: " + err.message);
|
||
}
|
||
});
|
||
|
||
if (els.btnGitlabReload) els.btnGitlabReload.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
populateGitlabDropdown();
|
||
});
|
||
|
||
// === Drag & Drop (Rows verschieben, inkl. Gruppen) ===
|
||
let dragIndex = -1;
|
||
// ===== Funktion: rowIndexOf =====
|
||
// Zweck: siehe Inline-Kommentare im Funktionskörper.
|
||
function rowIndexOf(target) {
|
||
const tr = target.closest('tr');
|
||
if (!tr) return -1;
|
||
return [...els.stepsTableBody.children].indexOf(tr);
|
||
}
|
||
els.stepsTableBody && els.stepsTableBody.addEventListener('dragstart', (e) => {
|
||
if (!e.target.closest('.drag-handle')) {
|
||
e.preventDefault();
|
||
return;
|
||
}
|
||
const i = rowIndexOf(e.target);
|
||
if (i < 0) return;
|
||
dragIndex = i;
|
||
e.dataTransfer?.setData('text/plain', String(i));
|
||
if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move';
|
||
const row = e.target.closest('tr') || e.target;
|
||
e.dataTransfer?.setDragImage?.(row, 16, 16);
|
||
row.classList.add('dragging');
|
||
});
|
||
els.stepsTableBody && els.stepsTableBody.addEventListener('dragend', (e) => {
|
||
const tr = e.target.closest('tr');
|
||
if (tr) tr.classList.remove('dragging');
|
||
[...els.stepsTableBody.children].forEach(r => {
|
||
r.classList.remove('drag-over');
|
||
r.removeAttribute('data-pos');
|
||
});
|
||
dragIndex = -1;
|
||
});
|
||
els.stepsTableBody && els.stepsTableBody.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
const tr = e.target.closest('tr');
|
||
if (!tr) return;
|
||
[...els.stepsTableBody.children].forEach(r => {
|
||
r.classList.remove('drag-over');
|
||
r.removeAttribute('data-pos');
|
||
});
|
||
tr.classList.add('drag-over');
|
||
const rect = tr.getBoundingClientRect();
|
||
const pos = (e.clientY - rect.top) < rect.height / 2 ? 'before' : 'after';
|
||
tr.setAttribute('data-pos', pos);
|
||
});
|
||
els.stepsTableBody && els.stepsTableBody.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
captureEditsIntoTemplate();
|
||
const from = dragIndex >= 0 ? dragIndex : parseInt(e.dataTransfer?.getData('text/plain') || '-1', 10);
|
||
const tr = e.target.closest('tr');
|
||
if (from < 0 || !tr) return;
|
||
const toBase = rowIndexOf(tr);
|
||
const pos = tr.getAttribute('data-pos') || 'after';
|
||
let to = toBase + (pos === 'after' ? 1 : 0);
|
||
if (to === from || to - 1 === from) {
|
||
renderSteps(template.steps);
|
||
recomputeGroupStyles();
|
||
return;
|
||
}
|
||
// Element verschieben
|
||
const item = template.steps.splice(from, 1)[0];
|
||
if (to > from) to--; // nach dem Entfernen verschiebt sich Index
|
||
template.steps.splice(to, 0, item);
|
||
ensureRenumberAndRender();
|
||
});
|
||
|
||
document.getElementById('stepsTable')?.addEventListener('change', (e) => {
|
||
if (e.target && e.target.matches('select.status')) {
|
||
updateStatusClass(e.target);
|
||
recomputeGroupStyles();
|
||
}
|
||
});
|
||
|
||
els.btnExportMD && els.btnExportMD.addEventListener('click', exportMD);
|
||
els.btnExportCSV && els.btnExportCSV.addEventListener('click', exportCSV);
|
||
els.btnPrintPDF && els.btnPrintPDF.addEventListener('click', printPDF);
|
||
els.btnExportTemplateYAML && els.btnExportTemplateYAML.addEventListener('click', exportTemplateYAML);
|
||
|
||
els.btnSaveJSON && els.btnSaveJSON.addEventListener('click', () => {
|
||
const run = collectRun();
|
||
if (!run) return;
|
||
if (!checkRequired(run)) return;
|
||
const base = `qa-run-${safeName(run.module)}-${safeName(run.module_version)}-${safeName(run.pbx_version)}`;
|
||
download(JSON.stringify(run, null, 2), `${base}.json`, 'application/json');
|
||
});
|
||
els.loadJSON && els.loadJSON.addEventListener('change', async (e) => {
|
||
const f = e.target.files?.[0];
|
||
if (!f) return;
|
||
const txt = await f.text();
|
||
try {
|
||
const run = JSON.parse(txt);
|
||
template = {
|
||
name: run.name || '',
|
||
steps: (run.steps || []).map(s => ({
|
||
id: s.id,
|
||
title: s.title,
|
||
expected: s.expected,
|
||
required: !!s.required,
|
||
status: s.status || '',
|
||
comment: s.comment || '',
|
||
evidence: s.evidence || ''
|
||
}))
|
||
};
|
||
if (els.tplName) els.tplName.textContent = run.name || '—';
|
||
els.module.value = run.module || '';
|
||
els.moduleVersion.value = run.module_version || '';
|
||
els.pbxVersion.value = run.pbx_version || '';
|
||
els.tester.value = run.tester || '';
|
||
els.docbeeUrl.value = run.docbee_url || '';
|
||
renderSteps(template.steps);
|
||
recomputeGroupStyles();
|
||
if (els.statusTag) els.statusTag.textContent = 'Lauf geladen';
|
||
updateAutosave();
|
||
} catch (err) {
|
||
alert('Ungültiges JSON: ' + err.message);
|
||
}
|
||
});
|
||
|
||
// === Bindings absichern + Busy-Fix + Mini-CSS ===
|
||
(function ensureBindings() {
|
||
// ===== Funktion: bind =====
|
||
// Zweck: siehe Inline-Kommentare im Funktionskörper.
|
||
function bind() {
|
||
const b = document.getElementById('btnPushDocBee');
|
||
if (b) {
|
||
b.removeAttribute('disabled');
|
||
b.classList.remove('is-busy');
|
||
b.style.pointerEvents = 'auto';
|
||
b.addEventListener('click', postToDocBee, {
|
||
once: false
|
||
});
|
||
}
|
||
const p = document.getElementById('btnPrintPDF');
|
||
const btn = document.getElementById('btnExportAll');
|
||
if (btn) {
|
||
btn.addEventListener('click', e => {
|
||
e.preventDefault();
|
||
exportAll().catch(err => alert('Export error: ' + err.message));
|
||
});
|
||
console.log('Export-Button gebunden');
|
||
} else {
|
||
alert('Export-Button nicht gefunden (id=btnExportAll).');
|
||
}
|
||
}
|
||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bind);
|
||
else bind();
|
||
window.addEventListener('pageshow', () => {
|
||
const b = document.getElementById('btnPushDocBee');
|
||
if (b) {
|
||
b.removeAttribute('disabled');
|
||
b.classList.remove('is-busy');
|
||
b.style.pointerEvents = 'auto';
|
||
}
|
||
});
|
||
})();
|
||
(function injectAuxCss() {
|
||
const s = document.createElement('style');
|
||
s.textContent = `
|
||
.is-busy{opacity:.6;pointer-events:none}
|
||
.docbee-hint{margin-top:8px;font-size:14px}
|
||
.docbee-hint a{text-decoration:underline}
|
||
`;
|
||
document.head.appendChild(s);
|
||
})();
|
||
|
||
// ===================== Ende app.js =====================
|