This commit is contained in:
Mateusz Gruszczyński
2026-09-02 09:02:56 +02:00
parent 9c6e51fc35
commit 43441d2fb7
16 changed files with 555 additions and 60 deletions
+170 -12
View File
@@ -57,7 +57,7 @@ function renderFlows() {
const host = $('#flowList'); if (!host) return;
const count = $('#flowListCount'); if (count) count.textContent = tr('flow.listCount', { count: app.flows.length });
host.innerHTML = app.flows.length ? app.flows.map(flow => `<article class="list-card flow-card ${flow.enabled ? '' : 'is-disabled'}">
<div class="list-card-head"><div><h3>${esc(flow.name)}</h3><p>${esc(flow.description || tr('flow.defaultDescription'))}</p></div><label class="flow-quick-toggle" title="${esc(tr('flow.quickToggleHint'))}"><input type="checkbox" data-flow-toggle="${esc(flow.id)}" ${flow.enabled ? 'checked' : ''}><span>${esc(flow.enabled ? tr('common.enabled') : tr('common.disabled'))}</span></label></div>
<div class="list-card-head"><div><h3>${esc(flow.name)}</h3><p>${esc(flow.description || tr('flow.defaultDescription'))}</p></div><button type="button" class="zone-enable-toggle ${flow.enabled ? 'active' : ''}" data-action="toggle-flow-enabled" data-id="${esc(flow.id)}" data-value="${flow.enabled ? 'false' : 'true'}" aria-label="${esc(tr(flow.enabled ? 'flow.disable' : 'flow.enable'))}" title="${esc(tr('flow.quickToggleHint'))}"><span>${flow.enabled ? '' : ''}</span>${esc(flow.enabled ? tr('common.enabled') : tr('common.disabled'))}</button></div>
<div class="card-stats"><div class="card-stat"><small>${esc(tr('flow.blocks'))}</small><strong>${flow.nodes?.length || 0}</strong></div><div class="card-stat"><small>${esc(tr('nav.schedules'))}</small><strong>${flow.compiled_schedule_ids?.length || 0}</strong></div><div class="card-stat"><small>${esc(tr('nav.automations'))}</small><strong>${flow.compiled_automation_ids?.length || 0}</strong></div></div>
<div class="card-footer"><small>${esc(tr('flow.compileCount', { schedules: flow.compiled_schedule_ids?.length || 0, automations: flow.compiled_automation_ids?.length || 0 }))}</small><div class="card-menu"><button data-action="edit-flow" data-id="${esc(flow.id)}">${esc(tr('flow.openEditor'))}</button><button class="danger" data-action="delete-flow" data-id="${esc(flow.id)}">${esc(tr('actions.delete'))}</button></div></div>
</article>`).join('') : `<div class="empty"><strong>${esc(tr('flow.emptyTitle'))}</strong>${esc(tr('flow.emptyText'))}</div>`;
@@ -303,9 +303,33 @@ async function importFlowFile(file) {
}
const FLOW_PRESET_CATEGORY_ORDER = ['comfort','energy','safety','night','reliability','home_assistant','advanced'];
const FLOW_PRESET_FAVORITES_KEY = 'gree_controller_flow_preset_favorites';
const FLOW_PRESET_RECENT_KEY = 'gree_controller_flow_preset_recent';
let flowPresetLoadPromise = null;
let flowPresetActiveCategory = '';
let flowPresetPreviewId = '';
let flowPresetSearch = '';
function flowPresetStoredIds(key) {
try {
const value = JSON.parse(localStorage.getItem(key) || '[]');
return Array.isArray(value) ? value.filter(item => typeof item === 'string') : [];
} catch (_) { return []; }
}
function flowPresetFavoriteIds() { return new Set(flowPresetStoredIds(FLOW_PRESET_FAVORITES_KEY)); }
function flowPresetRecentIds() { return flowPresetStoredIds(FLOW_PRESET_RECENT_KEY); }
function saveFlowPresetIds(key, ids) { try { localStorage.setItem(key, JSON.stringify(ids)); } catch (_) {} }
function toggleFlowPresetFavorite(id) {
const ids = flowPresetFavoriteIds();
if (ids.has(id)) ids.delete(id); else ids.add(id);
saveFlowPresetIds(FLOW_PRESET_FAVORITES_KEY, [...ids]);
renderFlowPresetBrowser(flowPresetActiveCategory);
}
function markFlowPresetRecent(id) {
const ids = flowPresetRecentIds().filter(value => value !== id);
ids.unshift(id);
saveFlowPresetIds(FLOW_PRESET_RECENT_KEY, ids.slice(0, 8));
}
function flowPresetText(value) {
if (typeof value === 'string') return value;
@@ -381,6 +405,84 @@ function materializeFlowPreset(preset) {
return { nodes, edges };
}
function flowPresetRequirements(preset) {
const nodes = preset?.flow?.nodes || [];
const placeholders = { zone:new Set(), device:new Set(), group:new Set(), shared:new Set() };
const visit = value => {
if (typeof value === 'string') {
const match = /^\$(zone|device|group|shared)(\d+)$/.exec(value);
if (match) placeholders[match[1]].add(value);
return;
}
if (Array.isArray(value)) value.forEach(visit);
else if (value && typeof value === 'object') Object.values(value).forEach(visit);
};
nodes.forEach(node => visit(node.config || {}));
const hasHa = nodes.some(node => ['ha_state','ha_numeric','ha_attribute','ha_available'].includes(node.kind));
const haEntities = [...new Set(nodes.map(node => node.config?.entity_id).filter(value => typeof value === 'string' && value && !value.startsWith('$')))];
const requirements = [];
const missing = [];
const addCount = (kind, count, available, key) => {
if (!count) return;
const label = tr(key, { count });
requirements.push({ label, ok:available >= count });
if (available < count) missing.push(label);
};
addCount('zone', placeholders.zone.size, (app.zones || []).length, 'flow.templateRequiresZone');
addCount('device', placeholders.device.size, (app.devices || []).length, 'flow.templateRequiresDevice');
addCount('group', placeholders.group.size, (app.groups || []).length, 'flow.templateRequiresGroup');
addCount('shared', placeholders.shared.size, (app.flowSharedInputs || []).length, 'flow.templateRequiresShared');
if (hasHa) {
const ready = Boolean(app.settings?.home_assistant?.url && app.settings?.home_assistant?.token_configured);
requirements.push({ label:tr('flow.templateRequiresHa'), ok:ready });
if (!ready) missing.push(tr('flow.templateRequiresHa'));
}
if (haEntities.length) requirements.push({ label:`${tr('flow.templateRequiresEntity')}: ${haEntities.join(', ')}`, ok:true, info:true });
return { requirements, missing };
}
function flowPresetPreviewGraph(preset) {
const nodes = preset?.flow?.nodes || [], edges = preset?.flow?.edges || [];
if (!nodes.length) return '';
const byId = new Map(nodes.map(node => [node.id, node]));
const xs = nodes.map(node => Number(node.x || 0)), ys = nodes.map(node => Number(node.y || 0));
const minX = Math.min(...xs), maxX = Math.max(...xs), minY = Math.min(...ys), maxY = Math.max(...ys);
const spanX = Math.max(1, maxX - minX), spanY = Math.max(1, maxY - minY);
const point = node => ({ x:8 + ((Number(node.x || 0) - minX) / spanX) * 84, y:12 + ((Number(node.y || 0) - minY) / spanY) * 76 });
const lines = edges.map(edge => {
const from = byId.get(edge.from), to = byId.get(edge.to); if (!from || !to) return '';
const a = point(from), b = point(to);
return `<line x1="${a.x.toFixed(2)}" y1="${a.y.toFixed(2)}" x2="${b.x.toFixed(2)}" y2="${b.y.toFixed(2)}"></line>`;
}).join('');
const blocks = nodes.map(node => {
const p = point(node), meta = FLOW_NODE_META[node.kind] || { title:node.kind, category:'logic' };
return `<div class="flow-template-preview-node flow-template-preview-node-${esc(meta.category)}" style="left:${p.x.toFixed(2)}%;top:${p.y.toFixed(2)}%" title="${esc(flowNodeTitle(meta))}">${esc(flowNodeTitle(meta))}</div>`;
}).join('');
return `<div class="flow-template-preview-canvas"><svg viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">${lines}</svg>${blocks}</div>`;
}
function renderFlowPresetPreview(preset) {
const host = $('#flowTemplatePreview'); if (!host) return;
if (!preset) {
host.innerHTML = `<div class="empty compact"><strong>${esc(tr('flow.templatePreview'))}</strong><span>${esc(tr('flow.templateSelectPreview'))}</span></div>`;
return;
}
const req = flowPresetRequirements(preset);
const favorite = flowPresetFavoriteIds().has(preset.id);
const reqMarkup = req.requirements.length
? req.requirements.map(item => `<span class="flow-template-requirement ${item.ok ? 'ok' : 'missing'} ${item.info ? 'info' : ''}">${item.ok ? '✓' : '!' } ${esc(item.label)}</span>`).join('')
: `<span class="flow-template-requirement ok">✓ ${esc(tr('flow.templateRequirementsReady'))}</span>`;
host.innerHTML = `<div class="flow-template-preview-head"><div><span class="eyebrow">${esc(tr('flow.templatePreview'))}</span><h3>${esc(flowPresetText(preset.name) || preset.id)}</h3></div><button type="button" class="flow-template-favorite ${favorite ? 'active' : ''}" data-flow-template-favorite="${esc(preset.id)}" title="${esc(favorite ? tr('flow.templateFavoriteRemove') : tr('flow.templateFavoriteAdd'))}" aria-label="${esc(favorite ? tr('flow.templateFavoriteRemove') : tr('flow.templateFavoriteAdd'))}">★</button></div>
<p>${esc(flowPresetText(preset.description))}</p>
<div class="flow-template-preview-stats"><span>${esc(tr('flow.templateNodesCount', { count:preset.flow.nodes.length }))}</span><span>${esc(tr('flow.templateEdgesCount', { count:preset.flow.edges.length }))}</span></div>
${flowPresetPreviewGraph(preset)}
<div class="flow-template-requirements"><strong>${esc(tr('flow.templateRequirements'))}</strong><div>${reqMarkup}</div>${req.missing.length ? `<p class="field-note warning-note">${esc(tr('flow.templateRequirementsMissing', { items:req.missing.join(', ') }))}</p>` : ''}</div>
<div class="dialog-actions"><button type="button" class="primary" data-flow-template-use="${esc(preset.id)}">${esc(tr('flow.templateUse'))}</button></div>`;
}
function flowPresetCategoryLabel(key) { return tr(`flow.templateCategory.${key}`) === `flow.templateCategory.${key}` ? key : tr(`flow.templateCategory.${key}`); }
function flowPresetCategoryHint(key) { const name = `flow.templateCategory.${key}.hint`, value = tr(name); return value === name ? '' : value; }
function renderFlowPresetBrowser(category = '') {
const tabs = $('#flowTemplateTabs'), host = $('#flowTemplateList');
if (!tabs || !host) return;
@@ -391,20 +493,36 @@ function renderFlowPresetBrowser(category = '') {
if (!byCategory.has(key)) byCategory.set(key, []);
byCategory.get(key).push(preset);
});
const categories = [...new Set([...FLOW_PRESET_CATEGORY_ORDER, ...byCategory.keys()])].filter(key => byCategory.has(key));
if (!categories.length) { tabs.innerHTML = ''; host.innerHTML = ''; return; }
flowPresetActiveCategory = categories.includes(category) ? category : (categories.includes(flowPresetActiveCategory) ? flowPresetActiveCategory : categories[0]);
tabs.innerHTML = categories.map(key => `<button type="button" class="flow-template-tab ${key === flowPresetActiveCategory ? 'active' : ''}" role="tab" aria-selected="${key === flowPresetActiveCategory}" data-flow-template-category="${esc(key)}">${esc(tr(`flow.templateCategory.${key}`) || key)} <span class="badge">${byCategory.get(key).length}</span></button>`).join('');
const active = byCategory.get(flowPresetActiveCategory) || [];
host.innerHTML = `<section class="flow-template-category-single"><div class="flow-template-category-head"><strong>${esc(tr(`flow.templateCategory.${flowPresetActiveCategory}`) || flowPresetActiveCategory)}</strong><span>${esc(tr(`flow.templateCategory.${flowPresetActiveCategory}.hint`) || '')}</span></div><div class="flow-template-category-grid">${active.map(preset => `<button type="button" class="flow-template-card" data-flow-template="${esc(preset.id)}"><strong>${esc(flowPresetText(preset.name) || preset.id)}</strong><span>${esc(flowPresetText(preset.description))}</span></button>`).join('')}</div></section>`;
const favorites = flowPresetFavoriteIds();
const recent = flowPresetRecentIds();
byCategory.set('favorites', presets.filter(preset => favorites.has(preset.id)));
byCategory.set('recent', recent.map(id => presets.find(preset => preset.id === id)).filter(Boolean));
const normalCategories = [...new Set([...FLOW_PRESET_CATEGORY_ORDER, ...presets.map(preset => preset.category || 'advanced')])].filter(key => (byCategory.get(key) || []).length);
const categories = ['favorites','recent', ...normalCategories];
if (!presets.length) { tabs.innerHTML = ''; host.innerHTML = ''; renderFlowPresetPreview(null); return; }
flowPresetActiveCategory = categories.includes(category) ? category : (categories.includes(flowPresetActiveCategory) ? flowPresetActiveCategory : normalCategories[0]);
tabs.innerHTML = categories.map(key => `<button type="button" class="flow-template-tab ${key === flowPresetActiveCategory ? 'active' : ''}" role="tab" aria-selected="${key === flowPresetActiveCategory}" data-flow-template-category="${esc(key)}">${esc(flowPresetCategoryLabel(key))} <span class="badge">${(byCategory.get(key) || []).length}</span></button>`).join('');
const query = flowPresetSearch.trim().toLocaleLowerCase(locale());
const activeAll = byCategory.get(flowPresetActiveCategory) || [];
const active = query ? activeAll.filter(preset => `${flowPresetText(preset.name)} ${flowPresetText(preset.description)} ${preset.id}`.toLocaleLowerCase(locale()).includes(query)) : activeAll;
const emptyKey = query ? 'flow.templateNoResults' : flowPresetActiveCategory === 'favorites' ? 'flow.templateNoFavorites' : flowPresetActiveCategory === 'recent' ? 'flow.templateNoRecent' : 'flow.templateNoResults';
const cards = active.map(preset => {
const isFavorite = favorites.has(preset.id), selected = preset.id === flowPresetPreviewId;
return `<article class="flow-template-card ${selected ? 'selected' : ''}"><button type="button" class="flow-template-card-main" data-flow-template-preview="${esc(preset.id)}"><strong>${esc(flowPresetText(preset.name) || preset.id)}</strong><span>${esc(flowPresetText(preset.description))}</span></button><button type="button" class="flow-template-favorite ${isFavorite ? 'active' : ''}" data-flow-template-favorite="${esc(preset.id)}" title="${esc(isFavorite ? tr('flow.templateFavoriteRemove') : tr('flow.templateFavoriteAdd'))}" aria-label="${esc(isFavorite ? tr('flow.templateFavoriteRemove') : tr('flow.templateFavoriteAdd'))}">★</button></article>`;
}).join('');
host.innerHTML = `<section class="flow-template-category-single"><div class="flow-template-category-head"><strong>${esc(flowPresetCategoryLabel(flowPresetActiveCategory))}</strong><span>${esc(flowPresetCategoryHint(flowPresetActiveCategory))}</span></div>${cards ? `<div class="flow-template-category-grid">${cards}</div>` : `<div class="empty compact"><span>${esc(tr(emptyKey))}</span></div>`}</section>`;
if (!active.some(preset => preset.id === flowPresetPreviewId)) flowPresetPreviewId = active[0]?.id || '';
renderFlowPresetPreview(presets.find(preset => preset.id === flowPresetPreviewId) || null);
}
async function openFlowTemplates() {
if (!app.flowDraft) return;
const host = $('#flowTemplateList'), tabs = $('#flowTemplateTabs');
const host = $('#flowTemplateList'), tabs = $('#flowTemplateTabs'), search = $('#flowTemplateSearch');
if (!host) return;
if (tabs) tabs.innerHTML = '';
if (search) { search.value = ''; flowPresetSearch = ''; }
host.innerHTML = `<p class="field-note">${esc(tr('common.loading'))}</p>`;
renderFlowPresetPreview(null);
$('#flowTemplateDialog')?.showModal();
try {
await loadFlowPresets();
@@ -415,6 +533,12 @@ async function openFlowTemplates() {
}
}
function previewFlowTemplate(key) {
const preset = (app.flowPresets || []).find(item => item.id === key); if (!preset) return;
flowPresetPreviewId = preset.id;
renderFlowPresetBrowser(flowPresetActiveCategory);
}
function applyFlowTemplate(key) {
if (!app.flowDraft) return;
const preset = (app.flowPresets || []).find(item => item.id === key);
@@ -424,9 +548,11 @@ function applyFlowTemplate(key) {
app.flowDraft.nodes = graph.nodes; app.flowDraft.edges = graph.edges;
app.flowDraft.description = flowPresetText(preset.description); app.flowSelectedNodeId = null; app.flowSelectedNodeIds = []; app.flowDirty = true;
if (!app.flowDraft.id || $('#flowName').value === tr('flow.newDefaultName')) $('#flowName').value = flowPresetText(preset.name) || preset.id;
markFlowPresetRecent(preset.id);
$('#flowTemplateDialog')?.close(); renderFlowEditor();
}
function localDateTimeInputValue(date = new Date()) {
const pad = value => String(value).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
@@ -474,13 +600,42 @@ async function runFlowDryRun() {
renderFlowDryRunResult(result);
} catch (error) { $('#flowTestResults').innerHTML = `<div class="empty compact"><strong>${esc(tr('flow.simulationFailed'))}</strong><span>${esc(error.message)}</span></div>`; }
}
function flowLogLevelLabel(level = '') {
const key = { info:'flow.logLevelInfo', warn:'flow.logLevelWarn', error:'flow.logLevelError' }[String(level).toLowerCase()];
return key ? tr(key) : level;
}
function flowLogKindLabel(kind = '') {
const key = {
'flow.created':'flow.logKindCreated', 'flow.updated':'flow.logKindUpdated', 'flow.deleted':'flow.logKindDeleted',
'flow.imported':'flow.logKindImported', 'flow.dry_run':'flow.logKindDryRun', 'flow.condition_error':'flow.logKindConditionError',
'flow.action_suppressed':'flow.logKindActionSuppressed', 'automation.fired':'flow.logKindActionFired',
'automation.error':'flow.logKindActionError', 'automation.blocked_by_zone':'flow.logKindBlockedZone',
'automation.blocked_by_manual_override':'flow.logKindBlockedManual', 'automation.blocked_by_local_thermostat':'flow.logKindBlockedThermostat',
'automation.blocked_by_temporary_thermostat':'flow.logKindBlockedTemporary', 'automation.blocked_by_thermostat_owner':'flow.logKindBlockedThermostatOwner',
'automation.blocked_by_fresh_ownership':'flow.logKindBlockedOwnership', 'automation.conflict':'flow.logKindConflict',
}[kind];
return key ? tr(key) : kind;
}
function flowLogMessage(event = {}) {
const name = app.flowDraft?.name || tr('nav.flows');
const key = {
'flow.created':'flow.logMessageCreated', 'flow.updated':'flow.logMessageUpdated', 'flow.deleted':'flow.logMessageDeleted',
'flow.imported':'flow.logMessageImported', 'flow.dry_run':'flow.logMessageDryRun', 'flow.condition_error':'flow.logMessageConditionError',
'flow.action_suppressed':'flow.logMessageActionSuppressed', 'automation.fired':'flow.logMessageActionFired',
'automation.error':'flow.logMessageActionError', 'automation.blocked_by_zone':'flow.logMessageBlockedZone',
'automation.blocked_by_manual_override':'flow.logMessageBlockedManual', 'automation.blocked_by_local_thermostat':'flow.logMessageBlockedThermostat',
'automation.blocked_by_temporary_thermostat':'flow.logMessageBlockedTemporary', 'automation.blocked_by_thermostat_owner':'flow.logMessageBlockedThermostatOwner',
'automation.blocked_by_fresh_ownership':'flow.logMessageBlockedOwnership', 'automation.conflict':'flow.logMessageConflict',
}[event.kind];
return key ? tr(key, { name }) : (event.message || '—');
}
async function openFlowLogs() {
if (!app.flowDraft?.id) return toast(tr('flow.saveBeforeLogs'), true);
$('#flowTestTitle').textContent = tr('flow.logs'); $('#flowSimulationControls').hidden = true; $('#flowTestResults').innerHTML = `<p class="field-note">${esc(tr('common.loading'))}</p>`; $('#flowTestDialog')?.showModal();
try {
const result = await api(`/api/flows/${encodeURIComponent(app.flowDraft.id)}/logs?limit=150`);
const events = result.events || [];
$('#flowTestResults').innerHTML = events.length ? events.map(event => `<article class="flow-log-row"><div><strong>${esc(event.kind)}</strong><span class="badge">${esc(event.level)}</span></div><p>${esc(event.message)}</p><small>${esc(new Date(event.timestamp).toLocaleString())}</small></article>`).join('') : `<div class="empty compact"><strong>${esc(tr('flow.noLogs'))}</strong></div>`;
$('#flowTestResults').innerHTML = events.length ? events.map(event => `<article class="flow-log-row"><div><strong>${esc(flowLogKindLabel(event.kind))}</strong><span class="badge">${esc(flowLogLevelLabel(event.level))}</span></div><p>${esc(flowLogMessage(event))}</p><small>${esc(new Date(event.timestamp).toLocaleString())}</small></article>`).join('') : `<div class="empty compact"><strong>${esc(tr('flow.noLogs'))}</strong></div>`;
} catch (error) { $('#flowTestResults').innerHTML = `<div class="empty compact"><strong>${esc(tr('flow.logsFailed'))}</strong><span>${esc(error.message)}</span></div>`; }
}
@@ -504,7 +659,7 @@ async function deleteFlow(id) {
async function toggleFlowEnabled(id, enabled) {
const flow = app.flows.find(item => item.id === id); if (!flow) return;
const toggle = $(`[data-flow-toggle="${CSS.escape(id)}"]`); if (toggle?.disabled) return;
const toggle = $(`[data-action="toggle-flow-enabled"][data-id="${CSS.escape(id)}"]`); if (toggle?.disabled) return;
if (toggle) toggle.disabled = true;
const body = { name:flow.name, enabled, description:flow.description || '', nodes:flow.nodes || [], edges:flow.edges || [], expected_revision:Number(flow.revision || 0) };
try {
@@ -597,7 +752,9 @@ document.addEventListener('click', event => {
const edge = event.target.closest?.('[data-flow-edge]'); if (edge) { removeFlowEdge(edge.dataset.flowEdge); return; }
const add = event.target.closest?.('[data-flow-add]'); if (add) { addFlowNode(add.dataset.flowAdd); return; }
const templateCategory = event.target.closest?.('[data-flow-template-category]'); if (templateCategory) { renderFlowPresetBrowser(templateCategory.dataset.flowTemplateCategory); return; }
const template = event.target.closest?.('[data-flow-template]'); if (template) { applyFlowTemplate(template.dataset.flowTemplate); return; }
const templateFavorite = event.target.closest?.('[data-flow-template-favorite]'); if (templateFavorite) { toggleFlowPresetFavorite(templateFavorite.dataset.flowTemplateFavorite); return; }
const templatePreview = event.target.closest?.('[data-flow-template-preview]'); if (templatePreview) { previewFlowTemplate(templatePreview.dataset.flowTemplatePreview); return; }
const templateUse = event.target.closest?.('[data-flow-template-use]'); if (templateUse) { applyFlowTemplate(templateUse.dataset.flowTemplateUse); return; }
const remove = event.target.closest?.('[data-flow-remove]'); if (remove) { removeFlowNode(remove.dataset.flowRemove); return; }
const output = event.target.closest?.('[data-flow-output]'); if (output) { app.flowConnectFrom = output.dataset.flowOutput; renderFlowEditor(); return; }
const input = event.target.closest?.('[data-flow-input]'); if (input) { if (app.flowConnectFrom) connectFlowNodes(app.flowConnectFrom, input.dataset.flowInput); return; }
@@ -606,6 +763,7 @@ document.addEventListener('click', event => {
if (action === 'new-flow') openFlowEditor();
else if (action === 'edit-flow') openFlowEditor(actionButton.dataset.id);
else if (action === 'delete-flow') deleteFlow(actionButton.dataset.id);
else if (action === 'toggle-flow-enabled') toggleFlowEnabled(actionButton.dataset.id, actionButton.dataset.value === 'true');
else if (action === 'save-flow') saveFlow();
else if (action === 'import-flow') $('#flowImportFile')?.click();
else if (action === 'export-flow') exportFlow();
@@ -629,9 +787,9 @@ document.addEventListener('click', event => {
});
document.addEventListener('change', event => {
if (event.target.matches?.('[data-flow-config]')) updateFlowConfig(event.target);
const toggle = event.target.closest?.('[data-flow-toggle]'); if (toggle) toggleFlowEnabled(toggle.dataset.flowToggle, toggle.checked);
});
$('#flowImportFile')?.addEventListener('change', event => importFlowFile(event.target.files?.[0]));
$('#flowTemplateSearch')?.addEventListener('input', event => { flowPresetSearch = event.target.value || ''; renderFlowPresetBrowser(flowPresetActiveCategory); });
$('#flowName')?.addEventListener('input', () => { if (app.flowDraft) app.flowDirty = true; });
$('#flowEnabled')?.addEventListener('change', () => { if (app.flowDraft) app.flowDirty = true; });
window.addEventListener('resize', () => { if (!$('#flowEditor')?.hidden) renderFlowEdges(); });