This commit is contained in:
Mateusz Gruszczyński
2026-09-02 23:17:29 +02:00
parent 76bbd39378
commit 93d93eca91
10 changed files with 80 additions and 24 deletions
+1 -1
View File
@@ -693,7 +693,7 @@
<section id="flowEditor" class="flow-editor" hidden aria-label="Flow editor">
<header class="flow-editor-bar">
<div class="flow-editor-title"><button type="button" class="icon-button" data-action="close-flow-editor" aria-label="Back"></button><div><span class="eyebrow" data-i18n="flow.editorEyebrow">Flow editor</span><input id="flowName" maxlength="100" data-i18n-placeholder="flow.namePlaceholder" data-i18n-title="flow.nameAliasHint" placeholder="My automation"></div></div>
<div class="flow-editor-title"><button type="button" class="icon-button" data-action="close-flow-editor" aria-label="Back"></button><div><span class="eyebrow" data-i18n="flow.editorEyebrow">Flow editor</span><div class="flow-name-wrap"><div id="flowNameView" class="flow-name-view" hidden><button id="flowNameText" class="flow-name-text" type="button" data-action="edit-flow-name" data-i18n-title="flow.editName" title="Edit Flow name"></button><button class="flow-name-edit" type="button" data-action="edit-flow-name" data-i18n-title="flow.editName" data-i18n-aria="flow.editName" title="Edit Flow name" aria-label="Edit Flow name"></button></div><input id="flowName" maxlength="100" data-i18n-placeholder="flow.namePlaceholder" data-i18n-title="flow.nameAliasHint" placeholder="My automation"></div></div></div>
<div class="flow-editor-actions"><button class="secondary" type="button" data-action="flow-templates" data-i18n="flow.templates">Templates</button><button class="secondary" type="button" data-action="import-flow" data-i18n="flow.import">Import</button><button class="secondary" type="button" data-action="export-flow" data-i18n="flow.export">Export</button><button class="secondary" type="button" data-action="flow-dry-run" data-i18n="flow.dryRun">Dry-run</button><button class="secondary" type="button" data-action="flow-logs" data-i18n="flow.logs">Logs</button><label class="flow-enabled"><input type="checkbox" id="flowEnabled" checked><span data-i18n="common.enabled">Enabled</span></label><span id="flowCompileStatus" class="badge"></span><button class="primary" type="button" data-action="save-flow" data-i18n="actions.save">Save</button></div>
</header>
<div class="flow-editor-body">
+53 -7
View File
@@ -111,12 +111,48 @@ function flowDraftFrom(flow) {
};
}
function flowCurrentName() {
const input = $('#flowName');
if (input && !input.hidden) return input.value.trim();
return String(app.flowDraft?.name || '').trim();
}
function renderFlowNameMode() {
const input = $('#flowName'), view = $('#flowNameView'), text = $('#flowNameText');
if (!input || !view || !text || !app.flowDraft) return;
const name = String(app.flowDraft.name || '').trim();
input.value = name; text.textContent = name || tr('flow.newDefaultName');
const editing = app.flowNameEditing === true;
input.hidden = !editing; view.hidden = editing;
}
function editFlowName({ select = true } = {}) {
if (!app.flowDraft) return;
app.flowNameEditing = true; renderFlowNameMode();
const input = $('#flowName');
requestAnimationFrame(() => { input?.focus(); if (select) input?.select(); });
}
function commitFlowName({ keepEditingOnEmpty = true } = {}) {
if (!app.flowDraft) return false;
const input = $('#flowName'); if (!input) return false;
const name = input.value.trim();
if (!name) {
if (keepEditingOnEmpty) { app.flowNameEditing = true; input.focus(); }
return false;
}
if (app.flowDraft.name !== name) { app.flowDraft.name = name; app.flowDirty = true; }
app.flowNameEditing = false; renderFlowNameMode();
return true;
}
function openFlowEditor(id = '', { push = true } = {}) {
const flow = id ? app.flows.find(item => item.id === id) : null;
if (id && !flow) return toast(tr('flow.notFound'), true);
app.flowDraft = flowDraftFrom(flow);
app.flowSelectedNodeId = null; app.flowSelectedNodeIds = []; app.flowConnectFrom = null; app.flowDirty = false;
$('#flowName').value = app.flowDraft.name || '';
app.flowNameEditing = !flow;
renderFlowNameMode();
$('#flowEnabled').checked = app.flowDraft.enabled !== false;
$('#flowEditor').hidden = false;
document.body.classList.add('flow-editor-open');
@@ -129,7 +165,7 @@ function closeFlowEditor({ push = true, force = false } = {}) {
if (!force && app.flowDirty && !confirm(tr('confirm.discardChanges'))) return false;
stopFlowSharedInputValueRefresh();
$('#flowEditor').hidden = true; document.body.classList.remove('flow-editor-open');
app.flowDraft = null; app.flowSelectedNodeId = null; app.flowSelectedNodeIds = []; app.flowConnectFrom = null; app.flowDirty = false;
app.flowDraft = null; app.flowSelectedNodeId = null; app.flowSelectedNodeIds = []; app.flowConnectFrom = null; app.flowDirty = false; app.flowNameEditing = false;
if (push) updateBrowserUrl('/flows');
return true;
}
@@ -324,7 +360,7 @@ function renderFlowEditor() {
}).join('');
$('#flowEmptyHint').hidden = draft.nodes.length > 0;
const selectionCount = $('#flowSelectionCount'); if (selectionCount) selectionCount.textContent = (app.flowSelectedNodeIds || []).length ? tr('flow.selectedCount', { count:(app.flowSelectedNodeIds || []).length }) : '';
renderFlowEdges(); renderFlowInspector(); renderFlowInterpretation(); renderFlowRuntimeInfo(); refreshFlowSharedInputCurrentValues();
renderFlowNameMode(); renderFlowEdges(); renderFlowInspector(); renderFlowInterpretation(); renderFlowRuntimeInfo(); refreshFlowSharedInputCurrentValues();
const status = $('#flowCompileStatus');
status.textContent = draft.draft ? tr('flow.draftStatus') : tr('flow.compileCount', { schedules: draft.compiled_schedule_ids?.length || 0, automations: draft.compiled_automation_ids?.length || 0 });
status.classList.toggle('flow-draft-badge', draft.draft === true);
@@ -526,7 +562,7 @@ function removeFlowEdge(id) {
function flowSourcePayload() {
if (!app.flowDraft) return null;
return {
name: ($('#flowName')?.value || app.flowDraft.name || '').trim(),
name: flowCurrentName(),
enabled: $('#flowEnabled')?.checked !== false,
draft: app.flowDraft.draft === true,
description: app.flowDraft.description || '',
@@ -808,7 +844,7 @@ function applyFlowTemplate(key) {
const graph = materializeFlowPreset(preset);
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;
if (!app.flowDraft.id || flowCurrentName() === tr('flow.newDefaultName')) { app.flowDraft.name = flowPresetText(preset.name) || preset.id; if (app.flowNameEditing) $('#flowName').value = app.flowDraft.name; renderFlowNameMode(); }
markFlowPresetRecent(preset.id);
$('#flowTemplateDialog')?.close(); renderFlowEditor();
}
@@ -906,14 +942,15 @@ async function persistFlowDraft(body) {
async function applySavedFlow(saved, messageKey) {
const index = app.flows.findIndex(flow => flow.id === saved.id); if (index >= 0) app.flows[index] = saved; else app.flows.push(saved);
app.flowDraft = flowDraftFrom(saved); app.flowDirty = false;
app.flowDraft = flowDraftFrom(saved); app.flowDirty = false; app.flowNameEditing = false;
$('#flowEnabled').checked = saved.enabled === true;
renderFlows(); renderFlowEditor(); updateBrowserUrl(`/flows/${saved.id}`, true); await loadBootstrap(); toast(tr(messageKey));
}
async function saveFlow() {
if (!app.flowDraft) return;
const name = $('#flowName').value.trim(); if (!name) return toast(tr('flow.nameRequired'), true);
if (app.flowNameEditing && !commitFlowName()) return toast(tr('flow.nameRequired'), true);
const name = flowCurrentName(); if (!name) return toast(tr('flow.nameRequired'), true);
const body = { name, enabled: $('#flowEnabled').checked, draft: false, description: app.flowDraft.description || '', nodes: app.flowDraft.nodes, edges: app.flowDraft.edges };
if (app.flowDraft.id) body.expected_revision = Number(app.flowDraft.revision || 0);
try {
@@ -1079,6 +1116,7 @@ document.addEventListener('click', event => {
else if (action === 'flow-logs') openFlowLogs();
else if (action === 'flow-select-all') selectAllFlowNodes();
else if (action === 'flow-clear-selection') clearFlowSelection();
else if (action === 'edit-flow-name') editFlowName();
else if (action === 'flow-shared-input-settings') {
if (!showView('homeassistant')) return;
requestAnimationFrame(() => {
@@ -1097,5 +1135,13 @@ document.addEventListener('change', event => {
$('#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; });
$('#flowName')?.addEventListener('keydown', event => {
if (event.key === 'Enter') { event.preventDefault(); commitFlowName(); }
else if (event.key === 'Escape') { event.preventDefault(); $('#flowName').value = app.flowDraft?.name || ''; app.flowNameEditing = false; renderFlowNameMode(); }
});
$('#flowName')?.addEventListener('blur', () => {
if (!app.flowDraft || !app.flowNameEditing) return;
if ($('#flowName').value.trim()) commitFlowName();
});
$('#flowEnabled')?.addEventListener('change', () => { if (app.flowDraft) app.flowDirty = true; });
window.addEventListener('resize', () => { if (!$('#flowEditor')?.hidden) renderFlowEdges(); });
+9 -1
View File
@@ -6180,7 +6180,15 @@ body.flow-editor-open { overflow:hidden; }
.flow-editor-bar { min-height:76px; display:flex; align-items:center; justify-content:space-between; gap:20px; padding:10px 18px; border-bottom:1px solid var(--line); background:var(--surface); }
.flow-editor-title { display:flex; align-items:center; gap:12px; min-width:0; }
.flow-editor-title > div { display:grid; gap:2px; min-width:0; }
.flow-name-wrap { min-width:280px; min-height:36px; display:flex; align-items:center; }
.flow-name-view { display:flex; align-items:center; gap:6px; min-width:0; }
.flow-name-view[hidden] { display:none !important; }
.flow-name-text { appearance:none; border:0; background:transparent; color:var(--text); font:inherit; font-weight:750; font-size:1.12rem; line-height:1.25; padding:4px 2px; min-width:0; max-width:min(52vw,620px); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; cursor:text; text-align:left; }
.flow-name-text:hover { color:var(--accent); }
.flow-name-edit { appearance:none; border:0; background:transparent; color:var(--muted); width:28px; height:28px; border-radius:7px; padding:0; cursor:pointer; display:grid; place-items:center; opacity:.62; transition:opacity .15s ease,background .15s ease,color .15s ease; }
.flow-name-view:hover .flow-name-edit,.flow-name-edit:focus-visible { opacity:1; color:var(--accent); background:var(--accent-soft); outline:none; }
.flow-editor-title input { border:1px solid var(--line-strong); background:var(--input-bg); color:var(--text); font-weight:750; font-size:1.12rem; padding:7px 10px; min-width:280px; outline:none; border-radius:9px; box-shadow:inset 0 0 0 1px transparent; transition:border-color .15s ease,box-shadow .15s ease,background .15s ease; }
.flow-editor-title input[hidden] { display:none !important; }
.flow-editor-title input:hover { border-color:var(--accent-border); background:var(--surface-2); }
.flow-editor-title input:focus { border-color:var(--accent); box-shadow:0 0 0 3px var(--accent-soft); background:var(--input-bg); }
.flow-draft-badge { border-color:var(--orange); background:color-mix(in srgb,var(--orange) 14%,var(--surface)); color:var(--text); }
@@ -6236,7 +6244,7 @@ body.flow-editor-open { overflow:hidden; }
@media (max-width: 980px) {
.flow-editor-body { grid-template-columns:180px minmax(360px,1fr) 260px; }
.flow-editor-title input { min-width:180px; }
.flow-name-wrap,.flow-editor-title input { min-width:180px; }
}
@media (max-width: 760px) {
.flow-editor-bar { align-items:flex-start; flex-direction:column; }