v0.15.10
This commit is contained in:
+78
-8
@@ -110,7 +110,7 @@ function renderFlows() {
|
||||
host.innerHTML = app.flows.length ? app.flows.map(flow => `<article class="list-card flow-card ${flow.enabled && !flow.draft ? '' : 'is-disabled'} ${flow.draft ? 'is-draft' : ''}" data-flow-card-id="${esc(flow.id)}" tabindex="0" aria-label="${esc(`${tr('flow.openEditor')}: ${flow.name}`)}">
|
||||
<div class="list-card-head"><div><h3>${esc(flow.name)}${flow.draft ? ` <span class="badge flow-draft-badge">${esc(tr('flow.draft'))}</span>` : ''}</h3><p>${esc(flow.description || tr('flow.defaultDescription'))}</p></div>${flow.draft ? `<button type="button" class="enable-toggle" disabled title="${esc(tr('flow.draftDisabledHint'))}"><span>${uiIcon('edit')}</span>${esc(tr('flow.disabledState'))}</button>` : `<button type="button" class="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>${uiIcon(flow.enabled ? 'check' : 'circle')}</span>${esc(flow.enabled ? tr('flow.enabledState') : tr('flow.disabledState'))}</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(flow.draft ? tr('flow.draftNoExecution') : tr('flow.compileCount', { schedules: flow.compiled_schedule_ids?.length || 0, automations: flow.compiled_automation_ids?.length || 0 }))}</small><div class="card-menu"><button class="card-primary-action" data-action="edit-flow" data-id="${esc(flow.id)}">${esc(tr('flow.openEditor'))}</button><details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">${uiIcon('more-horizontal')}</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-flow" data-id="${esc(flow.id)}">${esc(tr('actions.delete'))}</button></div></details></div></div>
|
||||
<div class="card-footer"><small>${esc(flow.draft ? tr('flow.draftNoExecution') : tr('flow.compileCount', { schedules: flow.compiled_schedule_ids?.length || 0, automations: flow.compiled_automation_ids?.length || 0 }))}</small><div class="card-menu"><button class="card-primary-action" data-action="edit-flow" data-id="${esc(flow.id)}">${esc(tr('flow.openEditor'))}</button><details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">${uiIcon('more-horizontal')}</summary><div class="card-overflow-menu"><button data-action="export-flow-by-id" data-id="${esc(flow.id)}">${esc(tr('flow.export'))}</button><button class="danger" data-action="delete-flow" data-id="${esc(flow.id)}">${esc(tr('actions.delete'))}</button></div></details></div></div>
|
||||
</article>`).join('') : `<div class="empty"><strong>${esc(tr('flow.emptyTitle'))}</strong>${esc(tr('flow.emptyText'))}</div>`;
|
||||
}
|
||||
|
||||
@@ -757,14 +757,28 @@ function downloadJsonFile(filename, data) {
|
||||
setTimeout(() => URL.revokeObjectURL(url), 0);
|
||||
}
|
||||
|
||||
function flowExportFilename(name) {
|
||||
const safe = String(name || 'flow').replace(/[^a-z0-9_-]+/gi, '-').replace(/^-+|-+$/g, '').toLowerCase() || 'flow';
|
||||
return `${safe}.flow.json`;
|
||||
}
|
||||
|
||||
async function exportFlowById(id) {
|
||||
if (!id) return;
|
||||
try {
|
||||
const flow = app.flows.find(item => item.id === id);
|
||||
const envelope = await api(`/api/flows/${encodeURIComponent(id)}/export`);
|
||||
downloadJsonFile(flowExportFilename(flow?.name || envelope?.flow?.name), envelope);
|
||||
toast(tr('flow.exported'));
|
||||
} catch (error) { toast(error.message, true); }
|
||||
}
|
||||
|
||||
async function exportFlow() {
|
||||
if (!app.flowDraft) return;
|
||||
try {
|
||||
const envelope = app.flowDraft.id && !app.flowDirty
|
||||
? await api(`/api/flows/${encodeURIComponent(app.flowDraft.id)}/export`)
|
||||
: { format: 'gree-controller-flow', version: 1, exported_at: new Date().toISOString(), shared_inputs: flowExportSharedInputs(), flow: flowSourcePayload() };
|
||||
const safe = (flowSourcePayload()?.name || 'flow').replace(/[^a-z0-9_-]+/gi, '-').replace(/^-+|-+$/g, '').toLowerCase() || 'flow';
|
||||
downloadJsonFile(`${safe}.flow.json`, envelope); toast(tr('flow.exported'));
|
||||
downloadJsonFile(flowExportFilename(flowSourcePayload()?.name), envelope); toast(tr('flow.exported'));
|
||||
} catch (error) { toast(error.message, true); }
|
||||
}
|
||||
|
||||
@@ -1250,6 +1264,14 @@ function duplicateSelectedFlowNodes() {
|
||||
if (pasteFlowNodes({ notify: false })) toast(tr('flow.blocksDuplicated', { count }));
|
||||
}
|
||||
|
||||
function cutSelectedFlowNodes() {
|
||||
const count = (app.flowSelectedNodeIds || []).length;
|
||||
if (!count || !copySelectedFlowNodes({ notify: false })) return false;
|
||||
removeSelectedFlowNodes();
|
||||
toast(tr('flow.blocksCut', { count }));
|
||||
return true;
|
||||
}
|
||||
|
||||
function removeSelectedFlowNodes() {
|
||||
if (!app.flowDraft || !(app.flowSelectedNodeIds || []).length) return;
|
||||
const selected = new Set(app.flowSelectedNodeIds);
|
||||
@@ -1259,6 +1281,24 @@ function removeSelectedFlowNodes() {
|
||||
app.flowSelectedNodeIds = []; app.flowSelectedNodeId = null; app.flowDirty = true; renderFlowEditor();
|
||||
}
|
||||
|
||||
function nudgeSelectedFlowNodes(dx, dy) {
|
||||
if (!app.flowDraft || !(app.flowSelectedNodeIds || []).length) return false;
|
||||
const selected = new Set(app.flowSelectedNodeIds);
|
||||
app.flowDraft.nodes.forEach(node => {
|
||||
if (!selected.has(node.id)) return;
|
||||
node.x = Math.max(12, Number(node.x || 0) + dx);
|
||||
node.y = Math.max(12, Number(node.y || 0) + dy);
|
||||
});
|
||||
app.flowDirty = true;
|
||||
renderFlowEditor();
|
||||
return true;
|
||||
}
|
||||
|
||||
function openFlowShortcuts() {
|
||||
const dialog = $('#flowShortcutDialog');
|
||||
if (dialog && !dialog.open) dialog.showModal();
|
||||
}
|
||||
|
||||
function updateFlowConfig(input) {
|
||||
const node = flowNodeById(app.flowSelectedNodeId); if (!node) return;
|
||||
const key = input.dataset.flowConfig; if (!key) return;
|
||||
@@ -1428,11 +1468,33 @@ document.addEventListener('keydown', event => {
|
||||
if (flowCard && event.target === flowCard && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); openFlowEditor(flowCard.dataset.flowCardId); return; }
|
||||
if ($('#flowEditor')?.hidden || !app.flowDraft) return;
|
||||
if (event.target.closest?.('input,select,textarea,[contenteditable="true"]')) return;
|
||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'a') { event.preventDefault(); selectAllFlowNodes(); return; }
|
||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'c') { event.preventDefault(); copySelectedFlowNodes(); return; }
|
||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'v') { event.preventDefault(); pasteFlowNodes(); return; }
|
||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'd') { event.preventDefault(); duplicateSelectedFlowNodes(); return; }
|
||||
if ((event.key === 'Delete' || event.key === 'Backspace') && (app.flowSelectedNodeIds || []).length) { event.preventDefault(); removeSelectedFlowNodes(); }
|
||||
if (event.target.closest?.('dialog[open]') || document.querySelector('dialog[open]')) return;
|
||||
|
||||
const modifier = event.ctrlKey || event.metaKey;
|
||||
const key = event.key.toLowerCase();
|
||||
if (modifier && key === 's') { event.preventDefault(); saveFlow(); return; }
|
||||
if (modifier && key === 'k') { event.preventDefault(); openFlowBlockLibrary(); return; }
|
||||
if (modifier && key === 'a') { event.preventDefault(); selectAllFlowNodes(); return; }
|
||||
if (modifier && key === 'c') { event.preventDefault(); copySelectedFlowNodes(); return; }
|
||||
if (modifier && key === 'x') { event.preventDefault(); cutSelectedFlowNodes(); return; }
|
||||
if (modifier && key === 'v') { event.preventDefault(); pasteFlowNodes(); return; }
|
||||
if (modifier && key === 'd') { event.preventDefault(); duplicateSelectedFlowNodes(); return; }
|
||||
if (modifier && event.key === '0') { event.preventDefault(); fitFlowToView(); return; }
|
||||
if (modifier && (event.key === '=' || event.key === '+')) { event.preventDefault(); setFlowZoom((app.flowZoom || 1) + .1); return; }
|
||||
if (modifier && event.key === '-') { event.preventDefault(); setFlowZoom((app.flowZoom || 1) - .1); return; }
|
||||
if (!modifier && !event.altKey && (key === 'a' || event.key === 'Insert')) { event.preventDefault(); openFlowBlockLibrary(); return; }
|
||||
if (!modifier && event.key === '?') { event.preventDefault(); openFlowShortcuts(); return; }
|
||||
if (event.key === 'Escape' && ((app.flowSelectedNodeIds || []).length || app.flowConnectFrom)) {
|
||||
event.preventDefault(); app.flowConnectFrom = null; clearFlowSelection(); return;
|
||||
}
|
||||
if ((event.key === 'Delete' || event.key === 'Backspace') && (app.flowSelectedNodeIds || []).length) { event.preventDefault(); removeSelectedFlowNodes(); return; }
|
||||
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key) && (app.flowSelectedNodeIds || []).length) {
|
||||
event.preventDefault();
|
||||
const step = event.shiftKey ? 1 : 10;
|
||||
const dx = event.key === 'ArrowLeft' ? -step : event.key === 'ArrowRight' ? step : 0;
|
||||
const dy = event.key === 'ArrowUp' ? -step : event.key === 'ArrowDown' ? step : 0;
|
||||
nudgeSelectedFlowNodes(dx, dy);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', event => {
|
||||
@@ -1454,6 +1516,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 === 'export-flow-by-id') exportFlowById(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 === 'flow-mobile-actions') $('#flowEditorActionsDialog')?.showModal();
|
||||
@@ -1476,6 +1539,7 @@ document.addEventListener('click', event => {
|
||||
else if (action === 'flow-select-all') selectAllFlowNodes();
|
||||
else if (action === 'flow-duplicate-selection') duplicateSelectedFlowNodes();
|
||||
else if (action === 'flow-clear-selection') clearFlowSelection();
|
||||
else if (action === 'flow-shortcuts') openFlowShortcuts();
|
||||
else if (action === 'edit-flow-name') editFlowName();
|
||||
else if (action === 'flow-shared-input-settings') {
|
||||
if (!showView('homeassistant')) return;
|
||||
@@ -1495,6 +1559,12 @@ document.addEventListener('change', event => {
|
||||
$('#flowImportFile')?.addEventListener('change', event => importFlowFile(event.target.files?.[0]));
|
||||
$('#flowTemplateSearch')?.addEventListener('input', event => { flowPresetSearch = event.target.value || ''; renderFlowPresetBrowser(flowPresetActiveCategory); });
|
||||
$('#flowBlockSearch')?.addEventListener('input', event => renderFlowBlockLibrary(event.target.value));
|
||||
$('#flowBlockSearch')?.addEventListener('keydown', event => {
|
||||
if (event.key !== 'Enter') return;
|
||||
const first = $('#flowBlockLibrary [data-flow-add]');
|
||||
if (!first) return;
|
||||
event.preventDefault(); first.click();
|
||||
});
|
||||
$('#flowName')?.addEventListener('input', () => { if (app.flowDraft) { app.flowDirty = true; renderFlowSaveStatus(); } });
|
||||
$('#flowName')?.addEventListener('keydown', event => {
|
||||
if (event.key === 'Enter') { event.preventDefault(); commitFlowName(); }
|
||||
|
||||
Reference in New Issue
Block a user