Card bulk operations, remove expand/collapse, graph color picker fix
- Bulk selection mode: Ctrl+Click or toggle button to enter, Escape to exit - Shift+Click for range select, bottom toolbar with SVG icon action buttons - All CardSections wired with bulk actions: Delete everywhere, Start/Stop for targets, Enable/Disable for automations - Remove expand/collapse all buttons (no collapsible sections remain) - Fix graph node color picker overlay persisting after outside click - Add Icons section to frontend.md conventions - Add trash2, listChecks, circleOff icons to icon system - Backend: processing loop performance improvements (monotonic timestamps, deque-based FPS tracking) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,7 @@ import {
|
||||
ICON_CLONE, ICON_EDIT, ICON_START, ICON_STOP,
|
||||
ICON_LED, ICON_FPS, ICON_OVERLAY, ICON_LED_PREVIEW,
|
||||
ICON_GLOBE, ICON_RADIO, ICON_PLUG, ICON_FILM, ICON_SUN_DIM, ICON_TARGET_ICON, ICON_HELP,
|
||||
ICON_WARNING, ICON_PALETTE, ICON_WRENCH, ICON_TEMPLATE,
|
||||
ICON_WARNING, ICON_PALETTE, ICON_WRENCH, ICON_TEMPLATE, ICON_TRASH,
|
||||
} from '../core/icons.js';
|
||||
import { EntitySelect } from '../core/entity-palette.js';
|
||||
import { IconSelect } from '../core/icon-select.js';
|
||||
@@ -39,11 +39,73 @@ import { updateSubTabHash, updateTabBadge } from './tabs.js';
|
||||
// createPatternTemplateCard is imported via window.* to avoid circular deps
|
||||
// (pattern-templates.js calls window.loadTargetsTab)
|
||||
|
||||
// ── Bulk action handlers ──
|
||||
async function _bulkStartTargets(ids) {
|
||||
const results = await Promise.allSettled(ids.map(id =>
|
||||
fetchWithAuth(`/output-targets/${id}/start`, { method: 'POST' })
|
||||
));
|
||||
const failed = results.filter(r => r.status === 'rejected' || (r.value && !r.value.ok)).length;
|
||||
if (failed) showToast(`${ids.length - failed}/${ids.length} started`, 'warning');
|
||||
else showToast(t('device.started'), 'success');
|
||||
}
|
||||
|
||||
async function _bulkStopTargets(ids) {
|
||||
const results = await Promise.allSettled(ids.map(id =>
|
||||
fetchWithAuth(`/output-targets/${id}/stop`, { method: 'POST' })
|
||||
));
|
||||
const failed = results.filter(r => r.status === 'rejected' || (r.value && !r.value.ok)).length;
|
||||
if (failed) showToast(`${ids.length - failed}/${ids.length} stopped`, 'warning');
|
||||
else showToast(t('device.stopped'), 'success');
|
||||
}
|
||||
|
||||
async function _bulkDeleteTargets(ids) {
|
||||
const results = await Promise.allSettled(ids.map(id =>
|
||||
fetchWithAuth(`/output-targets/${id}`, { method: 'DELETE' })
|
||||
));
|
||||
const failed = results.filter(r => r.status === 'rejected' || (r.value && !r.value.ok)).length;
|
||||
if (failed) showToast(`${ids.length - failed}/${ids.length} deleted`, 'warning');
|
||||
else showToast(t('targets.deleted'), 'success');
|
||||
outputTargetsCache.invalidate();
|
||||
await loadTargetsTab();
|
||||
}
|
||||
|
||||
async function _bulkDeleteDevices(ids) {
|
||||
const results = await Promise.allSettled(ids.map(id =>
|
||||
fetchWithAuth(`/devices/${id}`, { method: 'DELETE' })
|
||||
));
|
||||
const failed = results.filter(r => r.status === 'rejected' || (r.value && !r.value.ok)).length;
|
||||
if (failed) showToast(`${ids.length - failed}/${ids.length} deleted`, 'warning');
|
||||
else showToast(t('device.removed'), 'success');
|
||||
devicesCache.invalidate();
|
||||
await loadTargetsTab();
|
||||
}
|
||||
|
||||
async function _bulkDeletePatternTemplates(ids) {
|
||||
const results = await Promise.allSettled(ids.map(id =>
|
||||
fetchWithAuth(`/pattern-templates/${id}`, { method: 'DELETE' })
|
||||
));
|
||||
const failed = results.filter(r => r.status === 'rejected' || (r.value && !r.value.ok)).length;
|
||||
if (failed) showToast(`${ids.length - failed}/${ids.length} deleted`, 'warning');
|
||||
else showToast(t('targets.deleted'), 'success');
|
||||
patternTemplatesCache.invalidate();
|
||||
await loadTargetsTab();
|
||||
}
|
||||
|
||||
const _targetBulkActions = [
|
||||
{ key: 'start', labelKey: 'bulk.start', icon: ICON_START, handler: _bulkStartTargets },
|
||||
{ key: 'stop', labelKey: 'bulk.stop', icon: ICON_STOP, handler: _bulkStopTargets },
|
||||
{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteTargets },
|
||||
];
|
||||
|
||||
// ── Card section instances ──
|
||||
const csDevices = new CardSection('led-devices', { titleKey: 'targets.section.devices', gridClass: 'devices-grid', addCardOnclick: "showAddDevice()", keyAttr: 'data-device-id', emptyKey: 'section.empty.devices' });
|
||||
const csLedTargets = new CardSection('led-targets', { titleKey: 'targets.section.targets', gridClass: 'devices-grid', addCardOnclick: "showTargetEditor()", keyAttr: 'data-target-id', emptyKey: 'section.empty.targets', headerExtra: `<button class="btn btn-sm btn-danger" onclick="event.stopPropagation(); stopAllLedTargets()" data-stop-all="led" data-i18n-title="targets.stop_all.button" data-i18n-aria-label="targets.stop_all.button">${ICON_STOP}</button>` });
|
||||
const csKCTargets = new CardSection('kc-targets', { titleKey: 'targets.section.key_colors', gridClass: 'devices-grid', addCardOnclick: "showKCEditor()", keyAttr: 'data-kc-target-id', emptyKey: 'section.empty.kc_targets', headerExtra: `<button class="btn btn-sm btn-danger" onclick="event.stopPropagation(); stopAllKCTargets()" data-stop-all="kc" data-i18n-title="targets.stop_all.button" data-i18n-aria-label="targets.stop_all.button">${ICON_STOP}</button>` });
|
||||
const csPatternTemplates = new CardSection('kc-patterns', { titleKey: 'targets.section.pattern_templates', gridClass: 'templates-grid', addCardOnclick: "showPatternTemplateEditor()", keyAttr: 'data-pattern-template-id', emptyKey: 'section.empty.pattern_templates' });
|
||||
const csDevices = new CardSection('led-devices', { titleKey: 'targets.section.devices', gridClass: 'devices-grid', addCardOnclick: "showAddDevice()", keyAttr: 'data-device-id', emptyKey: 'section.empty.devices', bulkActions: [
|
||||
{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteDevices },
|
||||
] });
|
||||
const csLedTargets = new CardSection('led-targets', { titleKey: 'targets.section.targets', gridClass: 'devices-grid', addCardOnclick: "showTargetEditor()", keyAttr: 'data-target-id', emptyKey: 'section.empty.targets', headerExtra: `<button class="btn btn-sm btn-danger" onclick="event.stopPropagation(); stopAllLedTargets()" data-stop-all="led" data-i18n-title="targets.stop_all.button" data-i18n-aria-label="targets.stop_all.button">${ICON_STOP}</button>`, bulkActions: _targetBulkActions });
|
||||
const csKCTargets = new CardSection('kc-targets', { titleKey: 'targets.section.key_colors', gridClass: 'devices-grid', addCardOnclick: "showKCEditor()", keyAttr: 'data-kc-target-id', emptyKey: 'section.empty.kc_targets', headerExtra: `<button class="btn btn-sm btn-danger" onclick="event.stopPropagation(); stopAllKCTargets()" data-stop-all="kc" data-i18n-title="targets.stop_all.button" data-i18n-aria-label="targets.stop_all.button">${ICON_STOP}</button>`, bulkActions: _targetBulkActions });
|
||||
const csPatternTemplates = new CardSection('kc-patterns', { titleKey: 'targets.section.pattern_templates', gridClass: 'templates-grid', addCardOnclick: "showPatternTemplateEditor()", keyAttr: 'data-pattern-template-id', emptyKey: 'section.empty.pattern_templates', bulkActions: [
|
||||
{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeletePatternTemplates },
|
||||
] });
|
||||
|
||||
// Re-render targets tab when language changes (only if tab is active)
|
||||
document.addEventListener('languageChanged', () => {
|
||||
@@ -521,16 +583,6 @@ const _targetSectionMap = {
|
||||
'kc-patterns': [csPatternTemplates],
|
||||
};
|
||||
|
||||
export function expandAllTargetSections() {
|
||||
const activeSubTab = localStorage.getItem('activeTargetSubTab') || 'led-devices';
|
||||
CardSection.expandAll(_targetSectionMap[activeSubTab] || []);
|
||||
}
|
||||
|
||||
export function collapseAllTargetSections() {
|
||||
const activeSubTab = localStorage.getItem('activeTargetSubTab') || 'led-devices';
|
||||
CardSection.collapseAll(_targetSectionMap[activeSubTab] || []);
|
||||
}
|
||||
|
||||
let _loadTargetsLock = false;
|
||||
let _actionInFlight = false;
|
||||
|
||||
@@ -682,7 +734,7 @@ export async function loadTargetsTab() {
|
||||
CardSection.bindAll([csDevices, csLedTargets, csKCTargets, csPatternTemplates]);
|
||||
|
||||
// Render tree sidebar with expand/collapse buttons
|
||||
_targetsTree.setExtraHtml(`<button class="btn-expand-collapse" onclick="expandAllTargetSections()" data-i18n-title="section.expand_all" title="${t('section.expand_all')}">⊞</button><button class="btn-expand-collapse" onclick="collapseAllTargetSections()" data-i18n-title="section.collapse_all" title="${t('section.collapse_all')}">⊟</button><button class="tutorial-trigger-btn" onclick="startTargetsTutorial()" data-i18n-title="tour.restart" title="${t('tour.restart')}">${ICON_HELP}</button>`);
|
||||
_targetsTree.setExtraHtml(`<button class="tutorial-trigger-btn" onclick="startTargetsTutorial()" data-i18n-title="tour.restart" title="${t('tour.restart')}">${ICON_HELP}</button>`);
|
||||
_targetsTree.update(treeGroups, activeLeaf);
|
||||
_targetsTree.observeSections('targets-panel-content');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user