196 lines
7.9 KiB
JavaScript
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'));
|
|
}
|
|
|