v0.15.10
This commit is contained in:
@@ -10416,3 +10416,50 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr
|
||||
.flow-natural-preview .flow-preview-runtime span {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
|
||||
.flow-shortcut-dialog {
|
||||
width: min(680px, calc(100vw - 28px));
|
||||
}
|
||||
|
||||
.flow-shortcut-grid {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.flow-shortcut-grid > div {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, auto) 1fr;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 34px;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.flow-shortcut-keys {
|
||||
grid-column: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.flow-shortcut-grid > div > span:last-child {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.flow-shortcut-grid kbd {
|
||||
display: inline-block;
|
||||
min-width: 34px;
|
||||
padding: 4px 7px;
|
||||
border: 1px solid var(--line-strong, var(--line));
|
||||
border-radius: 6px;
|
||||
background: var(--surface-2);
|
||||
box-shadow: 0 1px 0 rgba(0, 0, 0, .08);
|
||||
font: inherit;
|
||||
font-size: .78rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
+31
-1
@@ -1001,7 +1001,9 @@
|
||||
class="secondary flow-selection-action" data-action="flow-duplicate-selection"
|
||||
data-i18n="flow.duplicateSelection" disabled>Duplikuj</button><button type="button"
|
||||
class="secondary flow-selection-action" data-action="flow-clear-selection"
|
||||
data-i18n="flow.clearSelection">Wyczyść zaznaczenie</button><span id="flowSelectionCount"
|
||||
data-i18n="flow.clearSelection">Wyczyść zaznaczenie</button><button type="button"
|
||||
class="secondary flow-selection-action" data-action="flow-shortcuts"
|
||||
data-i18n="flow.shortcuts">Skróty</button><span id="flowSelectionCount"
|
||||
class="badge"></span></div>
|
||||
<div class="flow-zoom-controls" role="group" data-i18n-aria="flow.zoomControls" aria-label="Canvas zoom">
|
||||
<button type="button" class="secondary icon-button" data-action="flow-zoom-out"
|
||||
@@ -1043,6 +1045,32 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<dialog id="flowShortcutDialog" class="sheet flow-shortcut-dialog">
|
||||
<div class="dialog-head">
|
||||
<div><span class="eyebrow" data-i18n="flow.editorEyebrow">Flow editor</span>
|
||||
<h2 data-i18n="flow.shortcuts">Skróty klawiaturowe</h2>
|
||||
</div><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<p class="field-note" data-i18n="flow.shortcutsHint">Skróty działają, gdy kursor nie znajduje się w polu formularza.</p>
|
||||
<div class="flow-shortcut-grid">
|
||||
<div><span class="flow-shortcut-keys"><kbd>A</kbd><kbd>Insert</kbd><kbd>Ctrl/Cmd+K</kbd></span><span data-i18n="flow.shortcutAdd">Dodaj / wyszukaj blok</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Enter</kbd></span><span data-i18n="flow.shortcutAddFirst">Dodaj pierwszy wynik wyszukiwania</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+S</kbd></span><span data-i18n="flow.shortcutSave">Zapisz Flow</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+A</kbd></span><span data-i18n="flow.shortcutSelectAll">Zaznacz wszystkie bloki</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+C</kbd></span><span data-i18n="flow.shortcutCopy">Kopiuj zaznaczone bloki</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+X</kbd></span><span data-i18n="flow.shortcutCut">Wytnij zaznaczone bloki</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+V</kbd></span><span data-i18n="flow.shortcutPaste">Wklej bloki</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+D</kbd></span><span data-i18n="flow.shortcutDuplicate">Duplikuj zaznaczenie</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Delete</kbd><kbd>Backspace</kbd></span><span data-i18n="flow.shortcutDelete">Usuń zaznaczone bloki</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>← ↑ ↓ →</kbd></span><span data-i18n="flow.shortcutMove">Przesuń zaznaczenie o 10 px</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Shift+← ↑ ↓ →</kbd></span><span data-i18n="flow.shortcutMoveFine">Przesuń precyzyjnie o 1 px</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Esc</kbd></span><span data-i18n="flow.shortcutClear">Wyczyść zaznaczenie / anuluj łączenie</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+0</kbd></span><span data-i18n="flow.shortcutFit">Dopasuj cały Flow do widoku</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd++ / -</kbd></span><span data-i18n="flow.shortcutZoom">Powiększ / pomniejsz</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>?</kbd></span><span data-i18n="flow.shortcutHelp">Pokaż tę listę skrótów</span></div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="flowBlockDialog" class="sheet flow-block-dialog">
|
||||
<div class="dialog-head">
|
||||
<div><span class="eyebrow" data-i18n="flow.blocks">Blocks</span>
|
||||
@@ -1069,6 +1097,8 @@
|
||||
data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button type="button" data-action="flow-logs"><span data-i18n="flow.logs">Logs</span><span
|
||||
data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button type="button" data-action="flow-shortcuts"><span data-i18n="flow.shortcuts">Shortcuts</span><span
|
||||
data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
|
||||
+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