Files
BartelLuis 7b979e6243
CI / javascript-check (push) Successful in 14s
CI / container-policy (push) Successful in 4s
CI / python-tests (push) Successful in 1m48s
CI / container-verify (push) Skipped
CI / container-publish (push) Successful in 49s
feat(ui): replace configuration editors with graphical forms
2026-09-14 21:52:39 +02:00

393 lines
28 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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); }