Viel neues
This commit is contained in:
195
qa-tool/htdocs/js/steps.js
Normal file
195
qa-tool/htdocs/js/steps.js
Normal file
@@ -0,0 +1,195 @@
|
||||
// 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'));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user