/* 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 = '') => ``;
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 `
${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})}
${button('↑ Nach oben', 'step-up')}${button('↓ Nach unten', 'step-down')}${button('Entfernen', 'remove-step')}
${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})}
`;
}
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 `
Parameter des Moduls
Felder, Datentypen und erlaubte Werte festlegen. Diese Angaben werden im Ablaufeditor als Eingabefelder angezeigt.
${schemaNode(schema)}
`;
}
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); }