fix: improve command palette actions and automation condition button
All checks were successful
Lint & Test / test (push) Successful in 1m16s

- Action items (start/stop, enable/disable) no longer close the palette
- Action items toggle state after success (Start→Stop, Enable→Disable)
- Toast z-index raised above command palette backdrop (3000→3500)
- Automation condition remove button uses ICON_TRASH SVG instead of ✕
This commit is contained in:
2026-03-26 02:21:52 +03:00
parent 3e0bf8538c
commit c0853ce184
3 changed files with 50 additions and 42 deletions

View File

@@ -49,7 +49,7 @@
--z-log-overlay: 2100; --z-log-overlay: 2100;
--z-confirm: 2500; --z-confirm: 2500;
--z-command-palette: 3000; --z-command-palette: 3000;
--z-toast: 3000; --z-toast: 3500;
--z-overlay-spinner: 9999; --z-overlay-spinner: 9999;
--z-lightbox: 10000; --z-lightbox: 10000;
--z-connection: 10000; --z-connection: 10000;

View File

@@ -57,26 +57,31 @@ function _buildItems(results: any[], states: any = {}) {
nav: ['targets', 'led-targets', 'led-targets', 'data-target-id', tgt.id], running, nav: ['targets', 'led-targets', 'led-targets', 'data-target-id', tgt.id], running,
}); });
} }
// Action items: start or stop // Action item: toggle start/stop
if (running) { const actionItem: any = {
items.push({ name: tgt.name, group: 'actions',
name: tgt.name, detail: t('search.action.stop'), group: 'actions', icon: '■', detail: running ? t('search.action.stop') : t('search.action.start'),
action: async () => { icon: running ? '■' : '▶',
const resp = await fetchWithAuth(`/output-targets/${tgt.id}/stop`, { method: 'POST' }); _running: running, _targetId: tgt.id,
if (resp.ok) { showToast(t('device.stopped'), 'success'); } action: async () => {
else { const err = await resp.json().catch(() => ({})); const d = err.detail || err.message || ''; const ds = Array.isArray(d) ? d.map(x => x.msg || x).join('; ') : String(d); showToast(ds || t('target.error.stop_failed'), 'error'); } const isRunning = actionItem._running;
}, const endpoint = isRunning ? 'stop' : 'start';
}); const resp = await fetchWithAuth(`/output-targets/${tgt.id}/${endpoint}`, { method: 'POST' });
} else { if (resp.ok) {
items.push({ showToast(t(isRunning ? 'device.stopped' : 'device.started'), 'success');
name: tgt.name, detail: t('search.action.start'), group: 'actions', icon: '▶', actionItem._running = !isRunning;
action: async () => { actionItem.detail = !isRunning ? t('search.action.stop') : t('search.action.start');
const resp = await fetchWithAuth(`/output-targets/${tgt.id}/start`, { method: 'POST' }); actionItem.icon = !isRunning ? '■' : '▶';
if (resp.ok) { showToast(t('device.started'), 'success'); } _render();
else { const err = await resp.json().catch(() => ({})); const d = err.detail || err.message || ''; const ds = Array.isArray(d) ? d.map(x => x.msg || x).join('; ') : String(d); showToast(ds || t('target.error.start_failed'), 'error'); } } else {
}, const err = await resp.json().catch(() => ({}));
}); const d = err.detail || err.message || '';
} const ds = Array.isArray(d) ? d.map(x => x.msg || x).join('; ') : String(d);
showToast(ds || t(`target.error.${endpoint}_failed`), 'error');
}
},
};
items.push(actionItem);
}); });
_mapEntities(css, c => items.push({ _mapEntities(css, c => items.push({
@@ -89,25 +94,28 @@ function _buildItems(results: any[], states: any = {}) {
name: a.name, detail: a.enabled ? 'enabled' : '', group: 'automations', icon: ICON_AUTOMATION, name: a.name, detail: a.enabled ? 'enabled' : '', group: 'automations', icon: ICON_AUTOMATION,
nav: ['automations', null, 'automations', 'data-automation-id', a.id], nav: ['automations', null, 'automations', 'data-automation-id', a.id],
}); });
if (a.enabled) { const autoItem: any = {
items.push({ name: a.name, group: 'actions', icon: ICON_AUTOMATION,
name: a.name, detail: t('search.action.disable'), group: 'actions', icon: ICON_AUTOMATION, detail: a.enabled ? t('search.action.disable') : t('search.action.enable'),
action: async () => { _enabled: a.enabled,
const resp = await fetchWithAuth(`/automations/${a.id}/disable`, { method: 'POST' }); action: async () => {
if (resp.ok) { showToast(t('search.action.disable') + ': ' + a.name, 'success'); } const isEnabled = autoItem._enabled;
else { const err = await resp.json().catch(() => ({})); const d = err.detail || err.message || ''; const ds = Array.isArray(d) ? d.map(x => x.msg || x).join('; ') : String(d); showToast(ds || (t('search.action.disable') + ' failed'), 'error'); } const endpoint = isEnabled ? 'disable' : 'enable';
}, const resp = await fetchWithAuth(`/automations/${a.id}/${endpoint}`, { method: 'POST' });
}); if (resp.ok) {
} else { showToast(t('search.action.' + endpoint) + ': ' + a.name, 'success');
items.push({ autoItem._enabled = !isEnabled;
name: a.name, detail: t('search.action.enable'), group: 'actions', icon: ICON_AUTOMATION, autoItem.detail = !isEnabled ? t('search.action.disable') : t('search.action.enable');
action: async () => { _render();
const resp = await fetchWithAuth(`/automations/${a.id}/enable`, { method: 'POST' }); } else {
if (resp.ok) { showToast(t('search.action.enable') + ': ' + a.name, 'success'); } const err = await resp.json().catch(() => ({}));
else { const err = await resp.json().catch(() => ({})); const d = err.detail || err.message || ''; const ds = Array.isArray(d) ? d.map(x => x.msg || x).join('; ') : String(d); showToast(ds || (t('search.action.enable') + ' failed'), 'error'); } const d = err.detail || err.message || '';
}, const ds = Array.isArray(d) ? d.map(x => x.msg || x).join('; ') : String(d);
}); showToast(ds || (t('search.action.' + endpoint) + ' failed'), 'error');
} }
},
};
items.push(autoItem);
}); });
_mapEntities(capTempl, ct => items.push({ _mapEntities(capTempl, ct => items.push({
@@ -378,13 +386,13 @@ function _onClick(e: Event) {
function _selectCurrent() { function _selectCurrent() {
if (_selectedIdx < 0 || _selectedIdx >= _filtered.length) return; if (_selectedIdx < 0 || _selectedIdx >= _filtered.length) return;
const item = _filtered[_selectedIdx]; const item = _filtered[_selectedIdx];
closeCommandPalette();
if (item.action) { if (item.action) {
item.action().catch(err => { item.action().catch(err => {
if (!err.isAuth) showToast(err.message || 'Action failed', 'error'); if (!err.isAuth) showToast(err.message || 'Action failed', 'error');
}); });
return; return;
} }
closeCommandPalette();
// If graph tab is active, navigate to graph node instead of card // If graph tab is active, navigate to graph node instead of card
const graphTabActive = document.querySelector('.tab-btn[data-tab="graph"].active'); const graphTabActive = document.querySelector('.tab-btn[data-tab="graph"].active');
if (graphTabActive) { if (graphTabActive) {

View File

@@ -610,7 +610,7 @@ function addAutomationConditionRow(condition: any) {
<select class="condition-type-select"> <select class="condition-type-select">
${CONDITION_TYPE_KEYS.map(k => `<option value="${k}" ${condType === k ? 'selected' : ''}>${t('automations.condition.' + k)}</option>`).join('')} ${CONDITION_TYPE_KEYS.map(k => `<option value="${k}" ${condType === k ? 'selected' : ''}>${t('automations.condition.' + k)}</option>`).join('')}
</select> </select>
<button type="button" class="btn-remove-condition" onclick="this.closest('.automation-condition-row').remove(); if(window._autoGenerateAutomationName) window._autoGenerateAutomationName();" title="Remove">&#x2715;</button> <button type="button" class="btn-remove-condition" onclick="this.closest('.automation-condition-row').remove(); if(window._autoGenerateAutomationName) window._autoGenerateAutomationName();" title="Remove">${ICON_TRASH}</button>
</div> </div>
<div class="condition-fields-container"></div> <div class="condition-fields-container"></div>
`; `;