393 lines
28 KiB
JavaScript
393 lines
28 KiB
JavaScript
/* Graphical module parameters and ordered postinstallation steps. */
|
||
'use strict';
|
||
|
||
const moduleFormTools = (() => {
|
||
let sequence = 0;
|
||
const uid = prefix => `${prefix}-${++sequence}`;
|
||
const list = value => Array.isArray(value) ? value : value?.items || [];
|
||
const own = (value, key) => Object.prototype.hasOwnProperty.call(value || {}, key);
|
||
const copy = value => value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
||
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
||
const metadata = value => esc(JSON.stringify(value));
|
||
const original = element => JSON.parse(element.dataset.original || '{}');
|
||
const fieldValue = (container, name) => [...container.querySelectorAll('[name]')].find(item => item.name === name);
|
||
const direct = (container, attribute) => [...container.children].find(item => item.hasAttribute(attribute));
|
||
const button = (label, action, attributes = '') => `<button type="button" class="button small" data-module-action="${action}" ${attributes}>${esc(label)}</button>`;
|
||
const rootIn = (container, selector) => container.matches?.(selector) ? container : container.querySelector(selector);
|
||
const moduleName = module => `${module.name || 'Modul'} · v${module.version ?? '–'}`;
|
||
|
||
function ensureDataEditors(root) {
|
||
for (let parent = root.parentElement; parent; parent = parent.parentElement) if (parent.dataset.dataEditorsBound) return;
|
||
wireDataEditors(root);
|
||
}
|
||
|
||
function syncHiddenControls(root) {
|
||
for (const input of root.querySelectorAll('input,select,textarea')) input.disabled = Boolean(input.closest('[hidden]'));
|
||
}
|
||
|
||
function showError(root, error) {
|
||
const element = direct(root, 'data-module-form-error');
|
||
if (element) { element.textContent = error.message || String(error); element.hidden = false; }
|
||
}
|
||
|
||
function clearError(root) {
|
||
const element = direct(root, 'data-module-form-error');
|
||
if (element) { element.textContent = ''; element.hidden = true; }
|
||
}
|
||
|
||
function secretRow(name, value, secrets) {
|
||
const key = uid('step-secret');
|
||
const options = [{value:'', label:'Geheimnis auswählen'}, ...list(secrets).map(secret => ({value:secret.id, label:secret.name || 'Gespeichertes Geheimnis'}))];
|
||
if (value && !options.some(item => item.value === value)) options.push({value, label:'Bisheriges Geheimnis (nicht in der Liste verfügbar)'});
|
||
return `<div class="form-grid" data-step-secret data-secret-key="${key}">${field(`${key}-name`, 'Bezeichnung im Modul', name, {placeholder:'z. B. API_KEY', hint:'Der vom Modul erwartete Parametername.'})}${field(`${key}-value`, 'Gespeichertes Geheimnis', value, {type:'select', options})}<div class="full">${button('Geheimniszuordnung entfernen', 'remove-secret')}</div></div>`;
|
||
}
|
||
|
||
function stepParameters(key, values, module) {
|
||
return dataEditor(`${key}-parameters`, values || {}, {label:'Parameter dieses Schritts', schema:module?.parameters_schema || {type:'object'}});
|
||
}
|
||
|
||
function stepCard(step, modules, secrets) {
|
||
const key = uid('postinstall-step');
|
||
const published = list(modules).filter(item => item.status === 'published');
|
||
const module = published.find(item => item.id === step.module_id);
|
||
const options = [{value:'', label:'Veröffentlichtes Modul auswählen'}, ...published.map(item => ({value:item.id, label:moduleName(item)}))];
|
||
if (step.module_id && !module) options.push({value:step.module_id, label:'Bisherige Modulversion (nicht verfügbar)'});
|
||
return `<section class="card" data-postinstall-step data-step-key="${key}" data-original="${metadata(step)}" data-current-module="${esc(step.module_id || '')}">
|
||
<div class="card-header"><h3 data-step-heading>Schritt</h3><div class="header-actions">${button('↑ Nach oben', 'step-up')}${button('↓ Nach unten', 'step-down')}${button('Entfernen', 'remove-step')}</div></div>
|
||
<div class="card-content stack"><div class="form-grid">${field(`${key}-module`, 'Modul und Version', step.module_id || '', {type:'select', full:true, options})}${field(`${key}-required`, 'Pflichtschritt: Der Lauf ist nur bei erfolgreicher Prüfung abgeschlossen.', step.required !== false, {type:'checkbox', full:true})}</div>
|
||
<div data-step-parameters>${stepParameters(key, step.parameters || {}, module)}</div>
|
||
<details${Object.keys(step.secret_refs || {}).length ? ' open' : ''}><summary>Geheimnisse für diesen Schritt</summary><p class="small-text muted">Gespeicherte Geheimnisse anhand ihres Namens zuordnen. Die Werte bleiben verborgen.</p><div class="stack" data-step-secrets>${Object.entries(step.secret_refs || {}).map(([name, value]) => secretRow(name, value, secrets)).join('')}</div>${button('Geheimnis zuordnen', 'add-secret')}</details></div>
|
||
</section>`;
|
||
}
|
||
|
||
function postinstall(profile = {}, modules = [], secrets = []) {
|
||
return `<div class="full stack" data-postinstall-editor data-original="${metadata(profile)}">
|
||
${dataEditor('postinstall-values', profile.values || {}, {label:'Gemeinsame Profilparameter', schema:{type:'object'}})}
|
||
<div><h3 class="section-label">Ablauf der Nachkonfiguration</h3><p class="small-text muted">Module auswählen und in der gewünschten Reihenfolge anordnen. Abhängigkeiten müssen vor dem jeweiligen Modul stehen.</p></div>
|
||
${list(modules).some(item => item.status === 'published') ? '' : '<div class="alert alert-info">Es sind noch keine veröffentlichten Module verfügbar. Übernehmen Sie zunächst eine Modulvorlage und veröffentlichen Sie den geprüften Stand.</div>'}
|
||
<div class="stack" data-postinstall-steps>${list(profile.steps).map(step => stepCard(step, modules, secrets)).join('')}</div>
|
||
<p class="small-text muted" data-no-steps${list(profile.steps).length ? ' hidden' : ''}>Fügen Sie den ersten Schritt hinzu. Mindestens ein Schritt muss als Pflichtschritt markiert sein.</p>
|
||
<div>${button('Schritt hinzufügen', 'add-step')}</div>
|
||
${field('postinstall-reboot-budget', 'Maximale geplante Neustarts', profile.reboot_budget ?? 1, {type:'number', min:0, max:5, full:true})}
|
||
<div class="alert alert-danger" data-module-form-error role="alert" hidden></div>
|
||
</div>`;
|
||
}
|
||
|
||
function readStep(card, index, modules = null) {
|
||
const key = card.dataset.stepKey;
|
||
const saved = original(card);
|
||
const moduleId = fieldValue(card, `${key}-module`).value;
|
||
if (!moduleId) throw new Error(`Wählen Sie für Schritt ${index + 1} ein veröffentlichtes Modul aus.`);
|
||
if (modules && !list(modules).some(item => item.id === moduleId && item.status === 'published')) throw new Error(`Das Modul in Schritt ${index + 1} ist nicht verfügbar. Wählen Sie eine veröffentlichte Modulversion.`);
|
||
const parameters = readDataEditor(card, `${key}-parameters`);
|
||
if (!object(parameters)) throw new Error(`Die Parameter in Schritt ${index + 1} müssen eine Gruppe von Feldern sein.`);
|
||
const secretRefs = {};
|
||
for (const row of card.querySelectorAll('[data-step-secret]')) {
|
||
const name = fieldValue(row, `${row.dataset.secretKey}-name`).value.trim();
|
||
const value = fieldValue(row, `${row.dataset.secretKey}-value`).value;
|
||
if (!/^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(name)) throw new Error(`Geben Sie für die Geheimniszuordnung in Schritt ${index + 1} einen gültigen Parameternamen ein (Buchstaben, Zahlen und Unterstrich).`);
|
||
if (!value) throw new Error(`Wählen Sie für „${name}“ in Schritt ${index + 1} ein gespeichertes Geheimnis aus.`);
|
||
if (own(secretRefs, name)) throw new Error(`Die Geheimnisbezeichnung „${name}“ ist in Schritt ${index + 1} doppelt vorhanden.`);
|
||
Object.defineProperty(secretRefs, name, {value, enumerable:true, configurable:true, writable:true});
|
||
}
|
||
return {id:saved.id || card.dataset.stepId || (card.dataset.stepId = `step-${Date.now().toString(36)}-${uid('new')}`), module_id:moduleId, parameters, secret_refs:secretRefs, required:fieldValue(card, `${key}-required`).checked};
|
||
}
|
||
|
||
function readPostinstall(container, previous = {}) {
|
||
const root = rootIn(container, '[data-postinstall-editor]');
|
||
if (!root) throw new Error('Der Editor für die Nachkonfiguration wurde nicht gefunden. Öffnen Sie das Formular erneut.');
|
||
const steps = [...root.querySelector('[data-postinstall-steps]').children].map((card, index) => readStep(card, index, root._moduleFormsModules));
|
||
if (!steps.length) throw new Error('Fügen Sie mindestens einen Schritt zur Nachkonfiguration hinzu.');
|
||
if (steps.length > 50) throw new Error('Ein Profil darf höchstens 50 Schritte enthalten.');
|
||
if (!steps.some(step => step.required)) throw new Error('Markieren Sie mindestens eine Abschlussprüfung als Pflichtschritt.');
|
||
if (new Set(steps.map(step => step.id)).size !== steps.length) throw new Error('Das Profil enthält doppelte Schrittkennungen. Entfernen Sie den doppelten Schritt und fügen Sie ihn neu hinzu.');
|
||
const values = readDataEditor(root, 'postinstall-values');
|
||
if (!object(values)) throw new Error('Die gemeinsamen Profilparameter müssen eine Gruppe von Feldern sein.');
|
||
const input = fieldValue(root, 'postinstall-reboot-budget');
|
||
const rebootBudget = input ? Number(input.value) : previous.reboot_budget ?? 1;
|
||
if (input?.value === '' || !Number.isInteger(rebootBudget) || rebootBudget < 0 || rebootBudget > 5) throw new Error('Wählen Sie für geplante Neustarts eine ganze Zahl zwischen 0 und 5.');
|
||
return {values, steps, reboot_budget:rebootBudget};
|
||
}
|
||
|
||
function renumberSteps(root) {
|
||
const steps = [...root.querySelector('[data-postinstall-steps]').children];
|
||
steps.forEach((card, index) => {
|
||
card.querySelector('[data-step-heading]').textContent = `Schritt ${index + 1}`;
|
||
card.querySelector('[data-module-action="step-up"]').disabled = index === 0;
|
||
card.querySelector('[data-module-action="step-down"]').disabled = index === steps.length - 1;
|
||
});
|
||
root.querySelector('[data-no-steps]').hidden = steps.length > 0;
|
||
root.querySelector('[data-module-action="add-step"]').disabled = steps.length >= 50;
|
||
}
|
||
|
||
function wirePostinstall(container, modules, secrets) {
|
||
const root = rootIn(container, '[data-postinstall-editor]');
|
||
if (!root) return;
|
||
root._moduleFormsModules = list(modules);
|
||
root._moduleFormsSecrets = list(secrets);
|
||
ensureDataEditors(root);
|
||
renumberSteps(root);
|
||
if (root.dataset.moduleFormsWired) return;
|
||
root.dataset.moduleFormsWired = 'true';
|
||
root.addEventListener('click', event => {
|
||
const control = event.target.closest('[data-module-action]');
|
||
if (!control || !root.contains(control)) return;
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
clearError(root);
|
||
const card = control.closest('[data-postinstall-step]');
|
||
const steps = root.querySelector('[data-postinstall-steps]');
|
||
try {
|
||
switch (control.dataset.moduleAction) {
|
||
case 'add-step':
|
||
if (steps.children.length < 50) steps.insertAdjacentHTML('beforeend', stepCard({id:`step-${Date.now().toString(36)}-${uid('new')}`, parameters:{}, secret_refs:{}, required:true}, root._moduleFormsModules, root._moduleFormsSecrets));
|
||
break;
|
||
case 'remove-step': card.remove(); break;
|
||
case 'step-up': if (card.previousElementSibling) steps.insertBefore(card, card.previousElementSibling); break;
|
||
case 'step-down': if (card.nextElementSibling) steps.insertBefore(card.nextElementSibling, card); break;
|
||
case 'add-secret': card.querySelector('[data-step-secrets]').insertAdjacentHTML('beforeend', secretRow('', '', root._moduleFormsSecrets)); break;
|
||
case 'remove-secret': control.closest('[data-step-secret]').remove(); break;
|
||
}
|
||
renumberSteps(root);
|
||
ensureDataEditors(root);
|
||
} catch (error) { showError(root, error); }
|
||
});
|
||
root.addEventListener('change', event => {
|
||
const card = event.target.closest('[data-postinstall-step]');
|
||
if (!card || event.target.name !== `${card.dataset.stepKey}-module`) return;
|
||
const holder = card.querySelector('[data-step-parameters]');
|
||
const cache = card._moduleParameterCache || (card._moduleParameterCache = new Map());
|
||
const previousId = card.dataset.currentModule;
|
||
const nextId = event.target.value;
|
||
const module = root._moduleFormsModules.find(item => item.id === nextId);
|
||
// Keep the actual fields, including incomplete edits, when switching away.
|
||
// Switching back restores their original DOM rather than recreating values.
|
||
const fragment = document.createDocumentFragment();
|
||
let currentValues = {};
|
||
try { currentValues = readDataEditor(card, `${card.dataset.stepKey}-parameters`); } catch (_) { /* Preserve invalid edits in the cached fields. */ }
|
||
while (holder.firstChild) fragment.append(holder.firstChild);
|
||
cache.set(previousId, fragment);
|
||
if (cache.has(nextId)) {
|
||
holder.append(cache.get(nextId));
|
||
cache.delete(nextId);
|
||
} else {
|
||
const properties = module?.parameters_schema?.properties || {};
|
||
const values = copy(module?.default_parameters || {});
|
||
for (const [name, rule] of Object.entries(properties)) {
|
||
if (own(currentValues, name)) Object.defineProperty(values, name, {value:copy(currentValues[name]), enumerable:true, configurable:true, writable:true});
|
||
else if (own(rule, 'default')) Object.defineProperty(values, name, {value:copy(rule.default), enumerable:true, configurable:true, writable:true});
|
||
}
|
||
holder.innerHTML = stepParameters(card.dataset.stepKey, values, module);
|
||
}
|
||
card.dataset.currentModule = nextId;
|
||
ensureDataEditors(root);
|
||
clearError(root);
|
||
});
|
||
}
|
||
|
||
const types = [{value:'string',label:'Text'}, {value:'integer',label:'Ganze Zahl'}, {value:'number',label:'Zahl'}, {value:'boolean',label:'Ja / Nein'}, {value:'array',label:'Liste'}, {value:'object',label:'Gruppe von Feldern'}];
|
||
const typeDefaults = {string:'', integer:0, number:0, boolean:false, array:[], object:{}};
|
||
const limits = {string:[['minLength','Mindestlänge'],['maxLength','Höchstlänge']], number:[['minimum','Kleinster Wert'],['maximum','Größter Wert']], integer:[['minimum','Kleinster Wert'],['maximum','Größter Wert']], array:[['minItems','Mindestens so viele Einträge'],['maxItems','Höchstens so viele Einträge']]};
|
||
|
||
function propertyCard(name, schema, required, depth) {
|
||
const key = uid('schema-property');
|
||
return `<section class="card" data-schema-property data-property-key="${key}"><div class="card-header"><strong>Parameter</strong>${button('Parameter entfernen', 'schema-remove-property')}</div><div class="card-content stack"><div class="form-grid">${field(`${key}-name`, 'Parametername', name, {required:true, placeholder:'z. B. servers'})}${field(`${key}-required`, 'Eingabe erforderlich', required, {type:'checkbox'})}</div>${schemaNode(schema, depth + 1)}</div></section>`;
|
||
}
|
||
|
||
function schemaBody(schema, type, key, depth) {
|
||
if (type === 'custom') return `<div class="stack" data-schema-body>${dataEditor(`${key}-custom`, schema, {label:'Weitere Parameterregeln grafisch bearbeiten'})}</div>`;
|
||
const managed = new Set(['type','title','description','default','enum', ...(limits[type] || []).map(item => item[0])]);
|
||
if (type === 'object') ['properties','required','additionalProperties'].forEach(name => managed.add(name));
|
||
if (type === 'array') ['items','uniqueItems'].forEach(name => managed.add(name));
|
||
const extras = Object.fromEntries(Object.entries(schema).filter(([name]) => !managed.has(name)));
|
||
let specific = '';
|
||
if (type === 'object') {
|
||
const props = schema.properties || {};
|
||
const required = new Set(schema.required || []);
|
||
const additional = !own(schema, 'additionalProperties') ? 'unset' : typeof schema.additionalProperties === 'boolean' ? String(schema.additionalProperties) : 'schema';
|
||
specific = `<div class="stack" data-schema-properties>${Object.entries(props).map(([name, rule]) => propertyCard(name, rule, required.has(name), depth)).join('')}</div><div>${button('Parameter hinzufügen', 'schema-add-property')}</div>${field(`${key}-additional`, 'Weitere Felder erlauben', additional, {type:'select', options:[{value:'unset',label:'Keine zusätzliche Einschränkung'}, {value:'false',label:'Nur die aufgeführten Felder'}, {value:'true',label:'Weitere Felder zulassen'}, {value:'schema',label:'Weitere Felder anhand eigener Regeln'}]})}<div data-schema-additional${additional === 'schema' ? '' : ' hidden'}>${dataEditor(`${key}-additional-schema`, object(schema.additionalProperties) ? schema.additionalProperties : {}, {label:'Regeln für weitere Felder'})}</div>`;
|
||
} else if (type === 'array') {
|
||
specific = `${field(`${key}-has-items`, 'Einträge der Liste definieren', own(schema, 'items'), {type:'checkbox'})}<div data-schema-items${own(schema, 'items') ? '' : ' hidden'}>${schemaNode(schema.items ?? {type:'string'}, depth + 1)}</div>${field(`${key}-unique`, 'Jeder Eintrag darf nur einmal vorkommen', schema.uniqueItems === true, {type:'checkbox'})}`;
|
||
}
|
||
const limitFields = (limits[type] || []).map(([name, label]) => field(`${key}-${name}`, label, schema[name] ?? '', {type:'number', ...(name.startsWith('min') || name.startsWith('max') ? (type === 'array' || type === 'string' ? {min:0} : {}) : {})}).replace('<input ', '<input step="any" ')).join('');
|
||
return `<div class="stack" data-schema-body data-original="${metadata(schema)}">
|
||
<div class="form-grid">${field(`${key}-title`, 'Anzeigename (optional)', schema.title || '')}${field(`${key}-description`, 'Beschreibung (optional)', schema.description || '')}${limitFields}</div>
|
||
${specific}
|
||
<div>${field(`${key}-has-default`, 'Vorgabewert anbieten', own(schema, 'default'), {type:'checkbox'})}<div data-schema-default${own(schema, 'default') ? '' : ' hidden'}>${dataEditor(`${key}-default`, own(schema, 'default') ? schema.default : copy(typeDefaults[type]), {label:'Vorgabewert'})}</div></div>
|
||
<div>${field(`${key}-has-enum`, 'Auswahl auf vorgegebene Werte beschränken', own(schema, 'enum'), {type:'checkbox'})}<div data-schema-enum${own(schema, 'enum') ? '' : ' hidden'}>${dataEditor(`${key}-enum`, schema.enum || [], {label:'Erlaubte Auswahlwerte', schema:{type:'array'}})}</div></div>
|
||
<details${Object.keys(extras).length ? ' open' : ''}><summary>Weitere Regeln</summary>${dataEditor(`${key}-extras`, extras, {label:'Weitere Parameterregeln', schema:{type:'object'}})}</details>
|
||
</div>`;
|
||
}
|
||
|
||
function schemaNode(schema = {type:'string'}, depth = 0) {
|
||
const key = uid('schema-node');
|
||
const supported = object(schema) && types.some(item => item.value === schema.type) && depth < 8;
|
||
const type = supported ? schema.type : 'custom';
|
||
const options = [...types, {value:'custom', label:'Flexible / bestehende Regeln'}];
|
||
return `<div class="stack" data-schema-node data-schema-key="${key}" data-schema-depth="${depth}" data-current-type="${type}">${field(`${key}-type`, 'Werttyp', type, {type:'select', options})}${schemaBody(schema, type, key, depth)}</div>`;
|
||
}
|
||
|
||
function readSchemaNode(node, selectedType = null) {
|
||
const key = node.dataset.schemaKey;
|
||
const type = selectedType || fieldValue(node, `${key}-type`).value;
|
||
const body = direct(node, 'data-schema-body');
|
||
if (type === 'custom') {
|
||
const result = readDataEditor(body, `${key}-custom`);
|
||
if (!object(result) && typeof result !== 'boolean') throw new Error('Parameterregeln müssen eine Gruppe von Feldern oder Ja / Nein sein.');
|
||
return result;
|
||
}
|
||
const previous = original(body);
|
||
const extras = readDataEditor(body, `${key}-extras`);
|
||
if (!object(extras)) throw new Error('Weitere Parameterregeln müssen eine Gruppe von Feldern sein.');
|
||
const result = {...extras, type};
|
||
for (const name of ['title','description']) {
|
||
const value = fieldValue(body, `${key}-${name}`).value;
|
||
if (value || own(previous, name)) result[name] = value;
|
||
}
|
||
for (const [name, label] of limits[type] || []) {
|
||
const raw = fieldValue(body, `${key}-${name}`).value;
|
||
if (raw !== '') {
|
||
const value = Number(raw);
|
||
if (!Number.isFinite(value) || ((type === 'string' || type === 'array') && (!Number.isInteger(value) || value < 0))) throw new Error(`„${label}“ benötigt ${type === 'string' || type === 'array' ? 'eine nicht negative ganze Zahl' : 'eine gültige Zahl'}.`);
|
||
result[name] = value;
|
||
}
|
||
}
|
||
const pair = limits[type];
|
||
if (pair && own(result, pair[0][0]) && own(result, pair[1][0]) && result[pair[0][0]] > result[pair[1][0]]) throw new Error('Der kleinste erlaubte Wert darf nicht größer als der größte erlaubte Wert sein.');
|
||
if (type === 'object') {
|
||
const props = {};
|
||
const required = [];
|
||
const previousProperties = previous.properties || {};
|
||
for (const card of direct(body, 'data-schema-properties').children) {
|
||
const cardKey = card.dataset.propertyKey;
|
||
const name = fieldValue(card, `${cardKey}-name`).value.trim();
|
||
if (!name) throw new Error('Geben Sie jedem Parameter einen Namen oder entfernen Sie die leere Parameterzeile.');
|
||
if (own(props, name)) throw new Error(`Der Parametername „${name}“ ist doppelt vorhanden.`);
|
||
Object.defineProperty(props, name, {value:readSchemaNode(card.querySelector('[data-schema-node]')), enumerable:true, configurable:true, writable:true});
|
||
if (fieldValue(card, `${cardKey}-required`).checked) required.push(name);
|
||
}
|
||
// Required keys without a property definition are valid schema and remain intact.
|
||
for (const name of previous.required || []) if (!own(previousProperties, name) && !required.includes(name)) required.push(name);
|
||
if (Object.keys(props).length || own(previous, 'properties')) result.properties = props;
|
||
if (required.length || own(previous, 'required')) result.required = required;
|
||
const additional = fieldValue(body, `${key}-additional`).value;
|
||
if (additional === 'true' || additional === 'false') result.additionalProperties = additional === 'true';
|
||
else if (additional === 'schema') {
|
||
const rule = readDataEditor(body, `${key}-additional-schema`);
|
||
if (!object(rule) && typeof rule !== 'boolean') throw new Error('Regeln für weitere Felder müssen eine Gruppe oder Ja / Nein sein.');
|
||
result.additionalProperties = rule;
|
||
}
|
||
} else if (type === 'array') {
|
||
if (fieldValue(body, `${key}-has-items`).checked) result.items = readSchemaNode(direct(body, 'data-schema-items').firstElementChild);
|
||
const unique = fieldValue(body, `${key}-unique`).checked;
|
||
if (unique || own(previous, 'uniqueItems')) result.uniqueItems = unique;
|
||
}
|
||
if (fieldValue(body, `${key}-has-default`).checked) result.default = readDataEditor(body, `${key}-default`);
|
||
if (fieldValue(body, `${key}-has-enum`).checked) {
|
||
const values = readDataEditor(body, `${key}-enum`);
|
||
if (!Array.isArray(values) || !values.length) throw new Error('Fügen Sie mindestens einen erlaubten Auswahlwert hinzu oder deaktivieren Sie die eingeschränkte Auswahl.');
|
||
if (new Set(values.map(value => JSON.stringify(value))).size !== values.length) throw new Error('Erlaubte Auswahlwerte dürfen nicht doppelt vorkommen.');
|
||
result.enum = values;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
function moduleSchema(schema = {type:'object', properties:{}, additionalProperties:false}) {
|
||
return `<div class="full stack" data-module-schema-editor><h3 class="section-label">Parameter des Moduls</h3><p class="small-text muted">Felder, Datentypen und erlaubte Werte festlegen. Diese Angaben werden im Ablaufeditor als Eingabefelder angezeigt.</p>${schemaNode(schema)}<div class="alert alert-danger" data-module-form-error role="alert" hidden></div></div>`;
|
||
}
|
||
|
||
function readModuleSchema(container, previousSchema) {
|
||
const root = rootIn(container, '[data-module-schema-editor]');
|
||
if (!root) {
|
||
if (previousSchema !== undefined) return copy(previousSchema);
|
||
throw new Error('Der Parametereditor wurde nicht gefunden. Öffnen Sie das Formular erneut.');
|
||
}
|
||
const result = readSchemaNode(direct(root, 'data-schema-node'));
|
||
if (!object(result)) throw new Error('Die Parameterdefinition des Moduls muss eine Gruppe von Regeln sein.');
|
||
return result;
|
||
}
|
||
|
||
function wireModuleSchema(container) {
|
||
const root = rootIn(container, '[data-module-schema-editor]');
|
||
if (!root) return;
|
||
ensureDataEditors(root);
|
||
syncHiddenControls(root);
|
||
if (root.dataset.moduleFormsWired) return;
|
||
root.dataset.moduleFormsWired = 'true';
|
||
root.addEventListener('click', event => {
|
||
const control = event.target.closest('[data-module-action]');
|
||
if (!control || !root.contains(control)) return;
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
clearError(root);
|
||
const node = control.closest('[data-schema-node]');
|
||
if (control.dataset.moduleAction === 'schema-remove-property') control.closest('[data-schema-property]').remove();
|
||
if (control.dataset.moduleAction === 'schema-add-property') {
|
||
const body = direct(node, 'data-schema-body');
|
||
direct(body, 'data-schema-properties').insertAdjacentHTML('beforeend', propertyCard('', {type:'string'}, false, Number(node.dataset.schemaDepth)));
|
||
ensureDataEditors(root);
|
||
}
|
||
syncHiddenControls(root);
|
||
});
|
||
root.addEventListener('change', event => {
|
||
const node = event.target.closest('[data-schema-node]');
|
||
if (!node) return;
|
||
const key = node.dataset.schemaKey;
|
||
const body = direct(node, 'data-schema-body');
|
||
const name = event.target.name;
|
||
clearError(root);
|
||
if (name === `${key}-type`) {
|
||
const nextType = event.target.value;
|
||
const cache = node._moduleSchemaCache || (node._moduleSchemaCache = new Map());
|
||
if (nextType === 'custom' || node.dataset.currentType === 'custom') cache.delete(nextType);
|
||
let next;
|
||
if (!cache.has(nextType)) {
|
||
try {
|
||
const previousType = node.dataset.currentType;
|
||
const shared = nextType === 'custom' ? readSchemaNode(node, previousType) : readDataEditor(body, `${key}-${previousType === 'custom' ? 'custom' : 'extras'}`);
|
||
next = nextType === 'custom' ? shared : {...(object(shared) ? shared : {}), type:nextType};
|
||
if (nextType === 'object') {
|
||
if (!own(next, 'properties')) next.properties = {};
|
||
if (!own(next, 'additionalProperties')) next.additionalProperties = false;
|
||
}
|
||
if (nextType === 'array' && !own(next, 'items')) next.items = {type:'string'};
|
||
if (object(next)) for (const attribute of ['title','description']) {
|
||
const input = fieldValue(body, `${key}-${attribute}`);
|
||
if (input?.value) next[attribute] = input.value;
|
||
}
|
||
} catch (error) {
|
||
event.target.value = node.dataset.currentType;
|
||
showError(root, error);
|
||
return;
|
||
}
|
||
}
|
||
cache.set(node.dataset.currentType, body);
|
||
body.remove();
|
||
if (cache.has(nextType)) { node.append(cache.get(nextType)); cache.delete(nextType); }
|
||
else {
|
||
node.insertAdjacentHTML('beforeend', schemaBody(next, nextType, key, Number(node.dataset.schemaDepth)));
|
||
}
|
||
node.dataset.currentType = nextType;
|
||
ensureDataEditors(root);
|
||
} else {
|
||
const toggles = {'has-items':'data-schema-items', 'has-default':'data-schema-default', 'has-enum':'data-schema-enum'};
|
||
for (const [suffix, attribute] of Object.entries(toggles)) if (name === `${key}-${suffix}`) {
|
||
const section = [...body.querySelectorAll(`[${attribute}]`)].find(item => item.closest('[data-schema-node]') === node);
|
||
if (section) section.hidden = !event.target.checked;
|
||
}
|
||
if (name === `${key}-additional`) direct(body, 'data-schema-additional').hidden = event.target.value !== 'schema';
|
||
}
|
||
syncHiddenControls(root);
|
||
});
|
||
}
|
||
|
||
return {postinstall, readPostinstall, wirePostinstall, moduleSchema, readModuleSchema, wireModuleSchema};
|
||
})();
|
||
|
||
function postinstallEditor(profile, modules, secrets) { return moduleFormTools.postinstall(profile, modules, secrets); }
|
||
function readPostinstallEditor(container, previousProfile = {}) { return moduleFormTools.readPostinstall(container, previousProfile); }
|
||
function wirePostinstallEditor(container, modules, secrets) { return moduleFormTools.wirePostinstall(container, modules, secrets); }
|
||
function moduleSchemaEditor(schema) { return moduleFormTools.moduleSchema(schema); }
|
||
function readModuleSchemaEditor(container, previousSchema) { return moduleFormTools.readModuleSchema(container, previousSchema); }
|
||
function wireModuleSchemaEditor(container) { return moduleFormTools.wireModuleSchema(container); }
|