Add start/stop, test, and notification buttons to graph editor node overlays
- Output targets and sync clocks get start/stop (▶/■) with optimistic UI update - Test/preview button for templates, sources, and KC targets - Notification test button (🔔) for notification color strip sources - Fetch batch states to show correct running status for output targets - Sync clocks show running state from API (is_running) - Surgical DOM patching (patchNodeRunning) preserves hover state on toggle - Success button hover style (green) for start action Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
|
||||
import { GraphCanvas } from '../core/graph-canvas.js';
|
||||
import { computeLayout, ENTITY_COLORS, ENTITY_LABELS } from '../core/graph-layout.js';
|
||||
import { renderNodes, highlightNode, markOrphans, updateSelection } from '../core/graph-nodes.js';
|
||||
import { renderNodes, patchNodeRunning, highlightNode, markOrphans, updateSelection } from '../core/graph-nodes.js';
|
||||
import { renderEdges, highlightChain, clearEdgeHighlights, updateEdgesForNode, renderFlowDots, updateFlowDotsForNode } from '../core/graph-edges.js';
|
||||
import {
|
||||
devicesCache, captureTemplatesCache, ppTemplatesCache,
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
outputTargetsCache, patternTemplatesCache, scenePresetsCache,
|
||||
automationsCacheObj,
|
||||
} from '../core/state.js';
|
||||
import { fetchWithAuth } from '../core/api.js';
|
||||
import { showToast } from '../core/ui.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
|
||||
let _canvas = null;
|
||||
@@ -147,17 +149,31 @@ async function _fetchAllEntities() {
|
||||
devices, captureTemplates, ppTemplates, pictureSources,
|
||||
audioSources, audioTemplates, valueSources, colorStripSources,
|
||||
syncClocks, outputTargets, patternTemplates, scenePresets, automations,
|
||||
batchStatesResp,
|
||||
] = await Promise.all([
|
||||
devicesCache.fetch(), captureTemplatesCache.fetch(), ppTemplatesCache.fetch(),
|
||||
streamsCache.fetch(), audioSourcesCache.fetch(), audioTemplatesCache.fetch(),
|
||||
valueSourcesCache.fetch(), colorStripSourcesCache.fetch(), syncClocksCache.fetch(),
|
||||
outputTargetsCache.fetch(), patternTemplatesCache.fetch(), scenePresetsCache.fetch(),
|
||||
automationsCacheObj.fetch(),
|
||||
fetchWithAuth('/output-targets/batch/states').catch(() => null),
|
||||
]);
|
||||
|
||||
// Enrich output targets with running state from batch states
|
||||
let batchStates = {};
|
||||
if (batchStatesResp && batchStatesResp.ok) {
|
||||
const data = await batchStatesResp.json().catch(() => ({}));
|
||||
batchStates = data.states || {};
|
||||
}
|
||||
const enrichedTargets = (outputTargets || []).map(t => ({
|
||||
...t,
|
||||
running: batchStates[t.id]?.processing || false,
|
||||
}));
|
||||
|
||||
return {
|
||||
devices, captureTemplates, ppTemplates, pictureSources,
|
||||
audioSources, audioTemplates, valueSources, colorStripSources,
|
||||
syncClocks, outputTargets, patternTemplates, scenePresets, automations,
|
||||
syncClocks, outputTargets: enrichedTargets, patternTemplates, scenePresets, automations,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -181,6 +197,9 @@ function _renderGraph(container) {
|
||||
onNodeDblClick: _onNodeDblClick,
|
||||
onEditNode: _onEditNode,
|
||||
onDeleteNode: _onDeleteNode,
|
||||
onStartStopNode: _onStartStopNode,
|
||||
onTestNode: _onTestNode,
|
||||
onNotificationTest: _onNotificationTest,
|
||||
});
|
||||
markOrphans(nodeGroup, _nodeMap, _edges);
|
||||
|
||||
@@ -666,6 +685,75 @@ function _onDeleteNode(node) {
|
||||
fnMap[node.kind]?.();
|
||||
}
|
||||
|
||||
function _onStartStopNode(node) {
|
||||
const newRunning = !node.running;
|
||||
// Optimistic update — toggle UI immediately
|
||||
_updateNodeRunning(node.id, newRunning);
|
||||
|
||||
if (node.kind === 'output_target') {
|
||||
const action = newRunning ? 'start' : 'stop';
|
||||
fetchWithAuth(`/output-targets/${node.id}/${action}`, { method: 'POST' }).then(resp => {
|
||||
if (resp.ok) {
|
||||
showToast(t(action === 'start' ? 'device.started' : 'device.stopped'), 'success');
|
||||
} else {
|
||||
resp.json().catch(() => ({})).then(err => {
|
||||
showToast(err.detail || t(`target.error.${action}_failed`), 'error');
|
||||
});
|
||||
_updateNodeRunning(node.id, !newRunning); // revert
|
||||
}
|
||||
}).catch(() => { _updateNodeRunning(node.id, !newRunning); });
|
||||
} else if (node.kind === 'sync_clock') {
|
||||
const action = newRunning ? 'resume' : 'pause';
|
||||
fetchWithAuth(`/sync-clocks/${node.id}/${action}`, { method: 'POST' }).then(resp => {
|
||||
if (resp.ok) {
|
||||
showToast(t(action === 'pause' ? 'sync_clock.paused' : 'sync_clock.resumed'), 'success');
|
||||
} else {
|
||||
_updateNodeRunning(node.id, !newRunning); // revert
|
||||
}
|
||||
}).catch(() => { _updateNodeRunning(node.id, !newRunning); });
|
||||
}
|
||||
}
|
||||
|
||||
/** Update a node's running state in the model and patch it in-place (no re-render). */
|
||||
function _updateNodeRunning(nodeId, running) {
|
||||
const node = _nodeMap?.get(nodeId);
|
||||
if (!node) return;
|
||||
node.running = running;
|
||||
const nodeGroup = document.querySelector('.graph-nodes');
|
||||
const edgeGroup = document.querySelector('.graph-edges');
|
||||
if (nodeGroup) {
|
||||
patchNodeRunning(nodeGroup, node);
|
||||
}
|
||||
// Update flow dots since running set changed
|
||||
if (edgeGroup) {
|
||||
const runningIds = new Set();
|
||||
for (const n of _nodeMap.values()) {
|
||||
if (n.running) runningIds.add(n.id);
|
||||
}
|
||||
renderFlowDots(edgeGroup, _edges, runningIds);
|
||||
}
|
||||
}
|
||||
|
||||
function _onTestNode(node) {
|
||||
const fnMap = {
|
||||
capture_template: () => window.showTestTemplateModal?.(node.id),
|
||||
pp_template: () => window.showTestPPTemplateModal?.(node.id),
|
||||
audio_template: () => window.showTestAudioTemplateModal?.(node.id),
|
||||
picture_source: () => window.showTestStreamModal?.(node.id),
|
||||
audio_source: () => window.testAudioSource?.(node.id),
|
||||
value_source: () => window.testValueSource?.(node.id),
|
||||
color_strip_source: () => window.testColorStrip?.(node.id),
|
||||
output_target: () => window.testKCTarget?.(node.id),
|
||||
};
|
||||
fnMap[node.kind]?.();
|
||||
}
|
||||
|
||||
function _onNotificationTest(node) {
|
||||
if (node.kind === 'color_strip_source' && node.subtype === 'notification') {
|
||||
window.testNotification?.(node.id);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Keyboard ── */
|
||||
|
||||
function _onKeydown(e) {
|
||||
|
||||
Reference in New Issue
Block a user