Files
proxmox-selfservice/qa-tool/htdocs/js/steps.js
Sven Steinert fce31ebcd7 Viel neues
2026-04-30 12:06:00 +02:00

196 lines
7.9 KiB
JavaScript

// auto-split module
function parseTemplate(text) {
if (window.jsyaml && typeof jsyaml.load === 'function') {
try {
return jsyaml.load(text);
} catch (e) {}
}
try {
return JSON.parse(text);
} catch (e) {}
throw new Error('Vorlage konnte weder als YAML noch JSON gelesen werden.');
}
function captureEditsIntoTemplate() {
if (!template) return;
const rows = [...(els.stepsTableBody?.querySelectorAll('tr') || [])];
template.steps = rows.map((row, i) => {
const kind = row.getAttribute('data-kind') || 'step';
if (kind === 'group') {
return {
kind: 'group',
title: (row.querySelector('.tpl-group-title')?.value || '').trim(),
collapsed: row.getAttribute('data-collapsed') === '1'
};
}
return {
kind: 'step',
id: (row.querySelector('.tpl-id')?.value || '').trim() || `step-${String(i+1).padStart(3,'0')}`,
title: (row.querySelector('.tpl-title')?.value || row.querySelector('.tpl-title')?.textContent || '').trim(),
expected: (row.querySelector('.tpl-expected')?.value || '').trim(),
required: !!(row.querySelector('.tpl-required')?.checked),
status: row.querySelector('select.status')?.value || '',
comment: row.querySelector('.run-comment')?.value || '',
evidence: row.querySelector('.run-evidence')?.value || ''
};
});
}
function nextStepNumber() {
const stepsOnly = (template?.steps || []).filter(s => (s.kind || s.type || 'step') === 'step');
// Versuche numerischen Suffix aus "step-XYZ" zu lesen, sonst zähle Steps
const nums = stepsOnly
.map(s => String(s.id || ''))
.map(id => {
const m = id.match(/step-(\d+)/i);
return m ? parseInt(m[1], 10) : null;
})
.filter(n => Number.isFinite(n));
const base = nums.length ? Math.max(...nums) : stepsOnly.length;
return base + 1;
}
function makeStepId(n) {
const num = String(Math.max(1, n)).padStart(3, '0');
return `step-${num}`;
}
function renumberSteps() {
if (!template || !Array.isArray(template.steps)) return;
let n = 1;
template.steps.forEach(s => {
const k = s.kind || s.type || 'step';
if (k !== 'step') return;
const id = `step-${String(n).padStart(3,'0')}`;
s.id = id;
n++;
});
}
function ensureRenumberAndRender() {
renumberSteps();
renderSteps(template.steps);
recomputeGroupStyles();
}
function renderSteps(steps) {
if (!els.stepsTableBody) return;
els.stepsTableBody.innerHTML = '';
let groupCollapsed = false;
steps.forEach((s, idx) => {
if ((s.kind || 'step') === 'group') {
const trG = document.createElement('tr');
trG.setAttribute('data-kind', 'group');
trG.className = 'group-row';
if (s.collapsed) trG.setAttribute('data-collapsed', '1');
/* drag via handle only */
// trG.setAttribute('draggable','true');
trG.innerHTML = `
<td colspan="4">
<div class="step-head" style="grid-template-columns: 1fr auto auto;">
<input type="text" class="tpl-group-title group-title" placeholder="Gruppen-Titel (z. B. Einrichtung des Moduls)" value="${escAttr(s.title||'')}">
<div class="group-actions">
<button type="button" class="btn btn-group-status" data-status="pass" title="Alle in Gruppe: PASS">✅</button>
<button type="button" class="btn btn-group-status" data-status="fail" title="Alle in Gruppe: FAIL">❌</button>
<button type="button" class="btn btn-group-status" data-status="skip" title="Alle in Gruppe: SKIP">⏭️</button>
<button type="button" class="btn btn-group-status" data-status="blocked" title="Alle in Gruppe: BLOCK">⛔</button>
<button type="button" class="btn btn-toggle-group" title="Gruppe ein-/ausklappen"><span class="chev">${s.collapsed?'▸':'▾'}</span></button>
<button type="button" class="btn btn-delete-group" title="Diese Gruppe löschen">🗑️</button>
</div>
<div class="drag-handle" draggable="true" title="Ziehen zum Verschieben">⋮⋮</div>
</div>
</td>`;
els.stepsTableBody.appendChild(trG);
groupCollapsed = !!s.collapsed;
return;
}
const id = s.id || '';
const tr = document.createElement('tr');
tr.setAttribute('data-kind', 'step');
/* drag via handle only */
// tr.setAttribute('draggable','true');
if (groupCollapsed) tr.setAttribute('data-hidden', '1');
tr.innerHTML = `
<td class="cell-step">
<div class="step-head">
<input type="text" class="tpl-id" value="${escAttr(id)}" />
<textarea class="tpl-title" placeholder="Titel">${escHTML(s.title||'')}</textarea>
<span class="req-pin" title="Pflichtschritt">${s.required ? '📌' : ''}</span>
<button type="button" class="btn btn-delete-step" title="Diesen Step löschen">🗑️</button>
<div class="drag-handle" draggable="true" title="Ziehen zum Verschieben">⋮⋮</div>
</div>
<label class="req-row"><input type="checkbox" class="tpl-required"${s.required?' checked':''}> required</label>
</td>
<td><textarea class="tpl-expected" placeholder="Erwartetes Verhalten">${escHTML(s.expected||'')}</textarea></td>
<td>
<select class="status" data-step="${escAttr(id)}">
<option value="" ${!s.status ? 'selected':''}></option>
<option value="pass" ${s.status==='pass'?'selected':''}>pass</option>
<option value="fail" ${s.status==='fail'?'selected':''}>fail</option>
<option value="skip" ${s.status==='skip'?'selected':''}>skip</option>
<option value="blocked" ${s.status==='blocked'?'selected':''}>blocked</option>
</select>
</td>
<td>
<textarea class="run-comment" placeholder="Kommentar">${escHTML(s.comment||'')}</textarea>
<input class="run-evidence" placeholder="Evidenz-URL" value="${escAttr(s.evidence||'')}">
</td>`;
els.stepsTableBody.appendChild(tr);
updateStatusClass(tr.querySelector('select.status'));
// nach jedem Render Schritt neu bewerten
recomputeGroupStyles();
});
}
function recomputeGroupStyles() {
try {
const body = els.stepsTableBody;
if (!body) return;
const rows = [...body.querySelectorAll('tr')];
// Collect groups with following step rows until next group
let groups = [];
let current = null;
rows.forEach(r => {
const kind = r.getAttribute('data-kind') || 'step';
if (kind === 'group') {
current = {
row: r,
steps: []
};
groups.push(current);
} else if (current) {
current.steps.push(r);
}
});
groups.forEach(g => {
g.row.classList.remove('group-ok', 'group-fail');
const statuses = g.steps.map(tr => (tr.querySelector('select.status')?.value || '').toUpperCase());
if (!statuses.length) return;
const allPass = statuses.every(s => s === 'PASS');
const anyFail = statuses.some(s => s === 'FAIL' || s === 'BLOCKED');
if (allPass) g.row.classList.add('group-ok');
else if (anyFail) g.row.classList.add('group-fail');
});
} catch (e) {
console.warn('recomputeGroupStyles failed', e);
}
}
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'));
}