fix: register pattern-templates API route; add responsive toolbar overflow menu
Lint & Test / test (push) Successful in 2m19s
Lint & Test / test (push) Successful in 2m19s
Pattern templates route existed but was never wired into the router, dependencies, or database table allowlist — causing 404 on graph tab load. Graph toolbar now collapses secondary actions into a "more" overflow menu on viewports narrower than 700px. Primary controls (fit, zoom, add) stay visible; search, filter, panels, undo/redo, relayout, fullscreen, and help move into the dropdown.
This commit is contained in:
@@ -30,6 +30,7 @@ from .routes.mqtt import router as mqtt_router
|
|||||||
from .routes.game_integration import router as game_integration_router
|
from .routes.game_integration import router as game_integration_router
|
||||||
from .routes.audio_processing_templates import router as audio_processing_templates_router
|
from .routes.audio_processing_templates import router as audio_processing_templates_router
|
||||||
from .routes.audio_filters import router as audio_filters_router
|
from .routes.audio_filters import router as audio_filters_router
|
||||||
|
from .routes.pattern_templates import router as pattern_templates_router
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
router.include_router(system_router)
|
router.include_router(system_router)
|
||||||
@@ -60,5 +61,6 @@ router.include_router(mqtt_router)
|
|||||||
router.include_router(game_integration_router)
|
router.include_router(game_integration_router)
|
||||||
router.include_router(audio_processing_templates_router)
|
router.include_router(audio_processing_templates_router)
|
||||||
router.include_router(audio_filters_router)
|
router.include_router(audio_filters_router)
|
||||||
|
router.include_router(pattern_templates_router)
|
||||||
|
|
||||||
__all__ = ["router"]
|
__all__ = ["router"]
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ from wled_controller.core.game_integration.event_bus import GameEventBus
|
|||||||
from wled_controller.storage.mqtt_source_store import MQTTSourceStore
|
from wled_controller.storage.mqtt_source_store import MQTTSourceStore
|
||||||
from wled_controller.core.mqtt.mqtt_manager import MQTTManager
|
from wled_controller.core.mqtt.mqtt_manager import MQTTManager
|
||||||
from wled_controller.storage.audio_processing_template_store import AudioProcessingTemplateStore
|
from wled_controller.storage.audio_processing_template_store import AudioProcessingTemplateStore
|
||||||
|
from wled_controller.storage.pattern_template_store import PatternTemplateStore
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
@@ -168,6 +169,10 @@ def get_audio_processing_template_store() -> AudioProcessingTemplateStore:
|
|||||||
return _get("audio_processing_template_store", "Audio processing template store")
|
return _get("audio_processing_template_store", "Audio processing template store")
|
||||||
|
|
||||||
|
|
||||||
|
def get_pattern_template_store() -> PatternTemplateStore:
|
||||||
|
return _get("pattern_template_store", "Pattern template store")
|
||||||
|
|
||||||
|
|
||||||
def get_database() -> Database:
|
def get_database() -> Database:
|
||||||
return _get("database", "Database")
|
return _get("database", "Database")
|
||||||
|
|
||||||
@@ -233,6 +238,7 @@ def init_dependencies(
|
|||||||
mqtt_store: MQTTSourceStore | None = None,
|
mqtt_store: MQTTSourceStore | None = None,
|
||||||
mqtt_manager: MQTTManager | None = None,
|
mqtt_manager: MQTTManager | None = None,
|
||||||
audio_processing_template_store: AudioProcessingTemplateStore | None = None,
|
audio_processing_template_store: AudioProcessingTemplateStore | None = None,
|
||||||
|
pattern_template_store: PatternTemplateStore | None = None,
|
||||||
):
|
):
|
||||||
"""Initialize global dependencies."""
|
"""Initialize global dependencies."""
|
||||||
_deps.update(
|
_deps.update(
|
||||||
@@ -267,5 +273,6 @@ def init_dependencies(
|
|||||||
"mqtt_store": mqtt_store,
|
"mqtt_store": mqtt_store,
|
||||||
"mqtt_manager": mqtt_manager,
|
"mqtt_manager": mqtt_manager,
|
||||||
"audio_processing_template_store": audio_processing_template_store,
|
"audio_processing_template_store": audio_processing_template_store,
|
||||||
|
"pattern_template_store": pattern_template_store,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ from wled_controller.core.mqtt.mqtt_service import MQTTService
|
|||||||
from wled_controller.core.mqtt.mqtt_manager import MQTTManager
|
from wled_controller.core.mqtt.mqtt_manager import MQTTManager
|
||||||
from wled_controller.storage.mqtt_source_store import MQTTSourceStore
|
from wled_controller.storage.mqtt_source_store import MQTTSourceStore
|
||||||
from wled_controller.storage.audio_processing_template_store import AudioProcessingTemplateStore
|
from wled_controller.storage.audio_processing_template_store import AudioProcessingTemplateStore
|
||||||
|
from wled_controller.storage.pattern_template_store import PatternTemplateStore
|
||||||
import wled_controller.core.audio.filters # noqa: F401 — trigger audio filter auto-registration
|
import wled_controller.core.audio.filters # noqa: F401 — trigger audio filter auto-registration
|
||||||
from wled_controller.core.devices.mqtt_client import set_mqtt_service
|
from wled_controller.core.devices.mqtt_client import set_mqtt_service
|
||||||
from wled_controller.core.backup.auto_backup import AutoBackupEngine
|
from wled_controller.core.backup.auto_backup import AutoBackupEngine
|
||||||
@@ -108,6 +109,7 @@ ha_manager = HomeAssistantManager(ha_store)
|
|||||||
mqtt_source_store = MQTTSourceStore(db)
|
mqtt_source_store = MQTTSourceStore(db)
|
||||||
audio_processing_template_store = AudioProcessingTemplateStore(db)
|
audio_processing_template_store = AudioProcessingTemplateStore(db)
|
||||||
game_integration_store = GameIntegrationStore(db)
|
game_integration_store = GameIntegrationStore(db)
|
||||||
|
pattern_template_store = PatternTemplateStore(db)
|
||||||
game_event_bus = GameEventBus()
|
game_event_bus = GameEventBus()
|
||||||
register_community_adapters()
|
register_community_adapters()
|
||||||
|
|
||||||
@@ -236,6 +238,7 @@ async def lifespan(app: FastAPI):
|
|||||||
mqtt_store=mqtt_source_store,
|
mqtt_store=mqtt_source_store,
|
||||||
mqtt_manager=mqtt_manager,
|
mqtt_manager=mqtt_manager,
|
||||||
audio_processing_template_store=audio_processing_template_store,
|
audio_processing_template_store=audio_processing_template_store,
|
||||||
|
pattern_template_store=pattern_template_store,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Register devices in processor manager for health monitoring
|
# Register devices in processor manager for health monitoring
|
||||||
|
|||||||
@@ -147,6 +147,90 @@ html:has(#tab-graph.active) {
|
|||||||
margin: 4px 2px;
|
margin: 4px 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Toolbar overflow button (hidden on wide screens) ── */
|
||||||
|
|
||||||
|
.graph-tb-overflow-btn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Toolbar overflow menu ── */
|
||||||
|
|
||||||
|
.graph-overflow-menu {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 25;
|
||||||
|
min-width: 170px;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px;
|
||||||
|
box-shadow: 0 4px 16px var(--shadow-color);
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-overflow-menu.open {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-overflow-menu.flip-up {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-overflow-menu button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-overflow-menu button:hover {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-overflow-menu button.active {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: var(--primary-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-overflow-menu button:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-overflow-menu .icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-overflow-sep {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--border-color);
|
||||||
|
margin: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Responsive: collapse toolbar on narrow viewports ── */
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.graph-toolbar [data-collapse] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.graph-tb-overflow-btn {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Legend panel ── */
|
/* ── Legend panel ── */
|
||||||
|
|
||||||
.graph-legend {
|
.graph-legend {
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ import {
|
|||||||
loadGraphEditor,
|
loadGraphEditor,
|
||||||
toggleGraphLegend, toggleGraphMinimap, toggleGraphFilter, toggleGraphFilterTypes, toggleGraphHelp, graphUndo, graphRedo,
|
toggleGraphLegend, toggleGraphMinimap, toggleGraphFilter, toggleGraphFilterTypes, toggleGraphHelp, graphUndo, graphRedo,
|
||||||
graphFitAll, graphZoomIn, graphZoomOut, graphRelayout,
|
graphFitAll, graphZoomIn, graphZoomOut, graphRelayout,
|
||||||
graphToggleFullscreen, graphAddEntity,
|
graphToggleFullscreen, graphAddEntity, toggleToolbarOverflow, closeToolbarOverflow,
|
||||||
} from './features/graph-editor.ts';
|
} from './features/graph-editor.ts';
|
||||||
|
|
||||||
// Layer 6: tabs, navigation, command palette, settings
|
// Layer 6: tabs, navigation, command palette, settings
|
||||||
@@ -575,6 +575,8 @@ Object.assign(window, {
|
|||||||
graphRelayout,
|
graphRelayout,
|
||||||
graphToggleFullscreen,
|
graphToggleFullscreen,
|
||||||
graphAddEntity,
|
graphAddEntity,
|
||||||
|
toggleToolbarOverflow,
|
||||||
|
closeToolbarOverflow,
|
||||||
|
|
||||||
// tabs / navigation / command palette
|
// tabs / navigation / command palette
|
||||||
switchTab,
|
switchTab,
|
||||||
|
|||||||
@@ -535,6 +535,86 @@ export function graphFitAll(): void {
|
|||||||
export function graphZoomIn(): void { if (_canvas) _canvas.zoomIn(); }
|
export function graphZoomIn(): void { if (_canvas) _canvas.zoomIn(); }
|
||||||
export function graphZoomOut(): void { if (_canvas) _canvas.zoomOut(); }
|
export function graphZoomOut(): void { if (_canvas) _canvas.zoomOut(); }
|
||||||
|
|
||||||
|
/* ── Toolbar overflow menu ──────────────────────────────── */
|
||||||
|
|
||||||
|
export function toggleToolbarOverflow(): void {
|
||||||
|
const menu = document.getElementById('graph-overflow-menu');
|
||||||
|
if (!menu) return;
|
||||||
|
const open = menu.classList.toggle('open');
|
||||||
|
if (open) {
|
||||||
|
_syncOverflowState();
|
||||||
|
// Position menu near the overflow button
|
||||||
|
const btn = document.querySelector('.graph-tb-overflow-btn') as HTMLElement | null;
|
||||||
|
const container = menu.parentElement;
|
||||||
|
if (btn && container) {
|
||||||
|
const btnRect = btn.getBoundingClientRect();
|
||||||
|
const containerRect = container.getBoundingClientRect();
|
||||||
|
const menuWidth = 180;
|
||||||
|
// Default: below the button, right-aligned
|
||||||
|
let top = btnRect.bottom - containerRect.top + 4;
|
||||||
|
let left = btnRect.right - containerRect.left - menuWidth;
|
||||||
|
// Flip above if would overflow bottom
|
||||||
|
if (top + 240 > containerRect.height) {
|
||||||
|
top = btnRect.top - containerRect.top - 4;
|
||||||
|
menu.classList.add('flip-up');
|
||||||
|
} else {
|
||||||
|
menu.classList.remove('flip-up');
|
||||||
|
}
|
||||||
|
// Clamp left edge
|
||||||
|
if (left < 4) left = 4;
|
||||||
|
menu.style.top = `${top}px`;
|
||||||
|
menu.style.left = `${left}px`;
|
||||||
|
}
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
document.addEventListener('pointerdown', _closeOverflowOutside, { once: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeToolbarOverflow(): void {
|
||||||
|
const menu = document.getElementById('graph-overflow-menu');
|
||||||
|
if (menu) menu.classList.remove('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function _closeOverflowOutside(e: Event): void {
|
||||||
|
const menu = document.getElementById('graph-overflow-menu');
|
||||||
|
if (!menu || !menu.classList.contains('open')) return;
|
||||||
|
const target = e.target as Element;
|
||||||
|
if (menu.contains(target) || target.closest('.graph-tb-overflow-btn')) {
|
||||||
|
// Re-listen if click was inside
|
||||||
|
document.addEventListener('pointerdown', _closeOverflowOutside, { once: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closeToolbarOverflow();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _syncOverflowState(): void {
|
||||||
|
// Sync disabled state for undo/redo
|
||||||
|
const undoBtn = document.getElementById('graph-undo-btn') as HTMLButtonElement | null;
|
||||||
|
const redoBtn = document.getElementById('graph-redo-btn') as HTMLButtonElement | null;
|
||||||
|
const overflowUndo = document.getElementById('graph-overflow-undo') as HTMLButtonElement | null;
|
||||||
|
const overflowRedo = document.getElementById('graph-overflow-redo') as HTMLButtonElement | null;
|
||||||
|
if (undoBtn && overflowUndo) overflowUndo.disabled = undoBtn.disabled;
|
||||||
|
if (redoBtn && overflowRedo) overflowRedo.disabled = redoBtn.disabled;
|
||||||
|
// Sync active state for toggle buttons
|
||||||
|
const pairs: [string, string][] = [
|
||||||
|
['graph-legend-toggle', 'graph-overflow-legend'],
|
||||||
|
['graph-minimap-toggle', 'graph-overflow-minimap'],
|
||||||
|
['graph-help-toggle', 'graph-overflow-help'],
|
||||||
|
];
|
||||||
|
for (const [tbId, ovId] of pairs) {
|
||||||
|
const tb = document.getElementById(tbId);
|
||||||
|
const ov = document.getElementById(ovId);
|
||||||
|
if (tb && ov) ov.classList.toggle('active', tb.classList.contains('active'));
|
||||||
|
}
|
||||||
|
// Sync filter active state
|
||||||
|
const filterTb = document.querySelector('.graph-filter-btn');
|
||||||
|
const filterOv = document.querySelector('.graph-overflow-filter-btn');
|
||||||
|
if (filterTb && filterOv) {
|
||||||
|
filterOv.classList.toggle('active', filterTb.classList.contains('active'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function graphToggleFullscreen(): void {
|
export function graphToggleFullscreen(): void {
|
||||||
const container = document.querySelector('#graph-editor-content .graph-container');
|
const container = document.querySelector('#graph-editor-content .graph-container');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -929,40 +1009,85 @@ function _graphHTML(): string {
|
|||||||
<button class="btn-icon" onclick="graphZoomOut()" title="${t('graph.zoom_out')}">
|
<button class="btn-icon" onclick="graphZoomOut()" title="${t('graph.zoom_out')}">
|
||||||
<svg class="icon" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/><path d="M8 11h6"/></svg>
|
<svg class="icon" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/><path d="M8 11h6"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<span class="graph-toolbar-sep"></span>
|
<span class="graph-toolbar-sep" data-collapse></span>
|
||||||
<button class="btn-icon" onclick="openCommandPalette()" title="${t('graph.search')} (/)">
|
<button class="btn-icon" onclick="openCommandPalette()" title="${t('graph.search')} (/)" data-collapse>
|
||||||
<svg class="icon" viewBox="0 0 24 24"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg>
|
<svg class="icon" viewBox="0 0 24 24"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon graph-filter-btn" onclick="toggleGraphFilter()" title="${t('graph.filter')} (F)">
|
<button class="btn-icon graph-filter-btn" onclick="toggleGraphFilter()" title="${t('graph.filter')} (F)" data-collapse>
|
||||||
<svg class="icon" viewBox="0 0 24 24"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>
|
<svg class="icon" viewBox="0 0 24 24"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon${_legendVisible ? ' active' : ''}" id="graph-legend-toggle" onclick="toggleGraphLegend()" title="${t('graph.legend')}">
|
<button class="btn-icon${_legendVisible ? ' active' : ''}" id="graph-legend-toggle" onclick="toggleGraphLegend()" title="${t('graph.legend')}" data-collapse>
|
||||||
<svg class="icon" viewBox="0 0 24 24"><path d="M4 7h16"/><path d="M4 12h16"/><path d="M4 17h16"/></svg>
|
<svg class="icon" viewBox="0 0 24 24"><path d="M4 7h16"/><path d="M4 12h16"/><path d="M4 17h16"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon${_minimapVisible ? ' active' : ''}" id="graph-minimap-toggle" onclick="toggleGraphMinimap()" title="${t('graph.minimap')}">
|
<button class="btn-icon${_minimapVisible ? ' active' : ''}" id="graph-minimap-toggle" onclick="toggleGraphMinimap()" title="${t('graph.minimap')}" data-collapse>
|
||||||
<svg class="icon" viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2"/><rect width="7" height="5" x="14" y="14" rx="1" fill="currentColor" opacity="0.3"/></svg>
|
<svg class="icon" viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2"/><rect width="7" height="5" x="14" y="14" rx="1" fill="currentColor" opacity="0.3"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<span class="graph-toolbar-sep"></span>
|
<span class="graph-toolbar-sep" data-collapse></span>
|
||||||
<button class="btn-icon" id="graph-undo-btn" onclick="graphUndo()" title="${t('graph.help.undo') || 'Undo'} (Ctrl+Z)" disabled>
|
<button class="btn-icon" id="graph-undo-btn" onclick="graphUndo()" title="${t('graph.help.undo') || 'Undo'} (Ctrl+Z)" disabled data-collapse>
|
||||||
<svg class="icon" viewBox="0 0 24 24"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/></svg>
|
<svg class="icon" viewBox="0 0 24 24"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon" id="graph-redo-btn" onclick="graphRedo()" title="${t('graph.help.redo') || 'Redo'} (Ctrl+Shift+Z)" disabled>
|
<button class="btn-icon" id="graph-redo-btn" onclick="graphRedo()" title="${t('graph.help.redo') || 'Redo'} (Ctrl+Shift+Z)" disabled data-collapse>
|
||||||
<svg class="icon" viewBox="0 0 24 24"><path d="M21 7v6h-6"/><path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7"/></svg>
|
<svg class="icon" viewBox="0 0 24 24"><path d="M21 7v6h-6"/><path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<span class="graph-toolbar-sep"></span>
|
<span class="graph-toolbar-sep" data-collapse></span>
|
||||||
<button class="btn-icon" onclick="graphRelayout()" title="${t('graph.relayout')}">
|
<button class="btn-icon" onclick="graphRelayout()" title="${t('graph.relayout')}" data-collapse>
|
||||||
<svg class="icon" viewBox="0 0 24 24"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg>
|
<svg class="icon" viewBox="0 0 24 24"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon" onclick="graphToggleFullscreen()" title="${t('graph.fullscreen')} (F11)">
|
<button class="btn-icon" onclick="graphToggleFullscreen()" title="${t('graph.fullscreen')} (F11)" data-collapse>
|
||||||
<svg class="icon" viewBox="0 0 24 24"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/></svg>
|
<svg class="icon" viewBox="0 0 24 24"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<span class="graph-toolbar-sep"></span>
|
<span class="graph-toolbar-sep" data-collapse></span>
|
||||||
<button class="btn-icon" onclick="graphAddEntity()" title="${t('graph.add_entity')} (+)">
|
<button class="btn-icon" onclick="graphAddEntity()" title="${t('graph.add_entity')} (+)">
|
||||||
<svg class="icon" viewBox="0 0 24 24"><path d="M12 5v14"/><path d="M5 12h14"/></svg>
|
<svg class="icon" viewBox="0 0 24 24"><path d="M12 5v14"/><path d="M5 12h14"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon${_helpVisible ? ' active' : ''}" id="graph-help-toggle" onclick="toggleGraphHelp()" title="${t('graph.help_title') || 'Shortcuts'} (?)">
|
<button class="btn-icon${_helpVisible ? ' active' : ''}" id="graph-help-toggle" onclick="toggleGraphHelp()" title="${t('graph.help_title') || 'Shortcuts'} (?)" data-collapse>
|
||||||
<svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>
|
<svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn-icon graph-tb-overflow-btn" onclick="toggleToolbarOverflow()" title="${t('graph.more') || 'More'}">
|
||||||
|
<svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="5" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="19" r="2"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="graph-overflow-menu" id="graph-overflow-menu">
|
||||||
|
<button onclick="openCommandPalette(); closeToolbarOverflow()">
|
||||||
|
<svg class="icon" viewBox="0 0 24 24"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg>
|
||||||
|
<span>${t('graph.search')}</span>
|
||||||
|
</button>
|
||||||
|
<button class="graph-overflow-filter-btn" onclick="toggleGraphFilter(); closeToolbarOverflow()">
|
||||||
|
<svg class="icon" viewBox="0 0 24 24"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>
|
||||||
|
<span>${t('graph.filter')}</span>
|
||||||
|
</button>
|
||||||
|
<button id="graph-overflow-legend" onclick="toggleGraphLegend(); closeToolbarOverflow()">
|
||||||
|
<svg class="icon" viewBox="0 0 24 24"><path d="M4 7h16"/><path d="M4 12h16"/><path d="M4 17h16"/></svg>
|
||||||
|
<span>${t('graph.legend')}</span>
|
||||||
|
</button>
|
||||||
|
<button id="graph-overflow-minimap" onclick="toggleGraphMinimap(); closeToolbarOverflow()">
|
||||||
|
<svg class="icon" viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2"/><rect width="7" height="5" x="14" y="14" rx="1" fill="currentColor" opacity="0.3"/></svg>
|
||||||
|
<span>${t('graph.minimap')}</span>
|
||||||
|
</button>
|
||||||
|
<div class="graph-overflow-sep"></div>
|
||||||
|
<button id="graph-overflow-undo" onclick="graphUndo(); closeToolbarOverflow()">
|
||||||
|
<svg class="icon" viewBox="0 0 24 24"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/></svg>
|
||||||
|
<span>${t('graph.help.undo') || 'Undo'}</span>
|
||||||
|
</button>
|
||||||
|
<button id="graph-overflow-redo" onclick="graphRedo(); closeToolbarOverflow()">
|
||||||
|
<svg class="icon" viewBox="0 0 24 24"><path d="M21 7v6h-6"/><path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7"/></svg>
|
||||||
|
<span>${t('graph.help.redo') || 'Redo'}</span>
|
||||||
|
</button>
|
||||||
|
<div class="graph-overflow-sep"></div>
|
||||||
|
<button onclick="graphRelayout(); closeToolbarOverflow()">
|
||||||
|
<svg class="icon" viewBox="0 0 24 24"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg>
|
||||||
|
<span>${t('graph.relayout')}</span>
|
||||||
|
</button>
|
||||||
|
<button onclick="graphToggleFullscreen(); closeToolbarOverflow()">
|
||||||
|
<svg class="icon" viewBox="0 0 24 24"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/></svg>
|
||||||
|
<span>${t('graph.fullscreen')}</span>
|
||||||
|
</button>
|
||||||
|
<div class="graph-overflow-sep"></div>
|
||||||
|
<button id="graph-overflow-help" onclick="toggleGraphHelp(); closeToolbarOverflow()">
|
||||||
|
<svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>
|
||||||
|
<span>${t('graph.help_title') || 'Shortcuts'}</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="graph-legend${_legendVisible ? ' visible' : ''}">
|
<div class="graph-legend${_legendVisible ? ' visible' : ''}">
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ _ENTITY_TABLES = [
|
|||||||
"mqtt_sources",
|
"mqtt_sources",
|
||||||
"game_integrations",
|
"game_integrations",
|
||||||
"audio_processing_templates",
|
"audio_processing_templates",
|
||||||
|
"pattern_templates",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user