Add tags to all entity types with chip-based input and autocomplete
- Add `tags: List[str]` field to all 13 entity types (devices, output targets, CSS sources, picture sources, audio sources, value sources, sync clocks, automations, scene presets, capture/audio/PP/pattern templates) - Update all stores, schemas, and route handlers for tag CRUD - Add GET /api/v1/tags endpoint aggregating unique tags across all stores - Create TagInput component with chip display, autocomplete dropdown, keyboard navigation, and API-backed suggestions - Display tag chips on all entity cards (searchable via existing text filter) - Add tag input to all 14 editor modals with dirty check support - Add CSS styles and i18n keys (en/ru/zh) for tag UI - Also includes code review fixes: thread safety, perf, store dedup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,14 +12,21 @@ import { updateTabBadge } from './tabs.js';
|
||||
import { ICON_SETTINGS, ICON_START, ICON_PAUSE, ICON_CLOCK, ICON_AUTOMATION, ICON_HELP, ICON_OK, ICON_TIMER, ICON_MONITOR, ICON_RADIO, ICON_SCENE, ICON_CLONE } from '../core/icons.js';
|
||||
import * as P from '../core/icon-paths.js';
|
||||
import { wrapCard } from '../core/card-colors.js';
|
||||
import { TagInput, renderTagChips } from '../core/tag-input.js';
|
||||
import { IconSelect } from '../core/icon-select.js';
|
||||
import { EntitySelect } from '../core/entity-palette.js';
|
||||
import { attachProcessPicker } from '../core/process-picker.js';
|
||||
import { csScenes, createSceneCard } from './scene-presets.js';
|
||||
|
||||
let _automationTagsInput = null;
|
||||
|
||||
class AutomationEditorModal extends Modal {
|
||||
constructor() { super('automation-editor-modal'); }
|
||||
|
||||
onForceClose() {
|
||||
if (_automationTagsInput) { _automationTagsInput.destroy(); _automationTagsInput = null; }
|
||||
}
|
||||
|
||||
snapshotValues() {
|
||||
return {
|
||||
name: document.getElementById('automation-editor-name').value,
|
||||
@@ -29,6 +36,7 @@ class AutomationEditorModal extends Modal {
|
||||
scenePresetId: document.getElementById('automation-scene-id').value,
|
||||
deactivationMode: document.getElementById('automation-deactivation-mode').value,
|
||||
deactivationScenePresetId: document.getElementById('automation-fallback-scene-id').value,
|
||||
tags: JSON.stringify(_automationTagsInput ? _automationTagsInput.getValue() : []),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -204,7 +212,8 @@ function createAutomationCard(automation, sceneMap = new Map()) {
|
||||
${deactivationLabel ? `<span class="card-meta">${deactivationLabel}</span>` : ''}
|
||||
${lastActivityMeta}
|
||||
</div>
|
||||
<div class="stream-card-props">${condPills}</div>`,
|
||||
<div class="stream-card-props">${condPills}</div>
|
||||
${renderTagChips(automation.tags)}`,
|
||||
actions: `
|
||||
<button class="btn btn-icon btn-secondary" onclick="cloneAutomation('${automation.id}')" title="${t('common.clone')}">${ICON_CLONE}</button>
|
||||
<button class="btn btn-icon btn-secondary" onclick="openAutomationEditor('${automation.id}')" title="${t('automations.edit')}">${ICON_SETTINGS}</button>
|
||||
@@ -240,6 +249,8 @@ export async function openAutomationEditor(automationId, cloneData) {
|
||||
if (_deactivationModeIconSelect) _deactivationModeIconSelect.setValue('none');
|
||||
document.getElementById('automation-fallback-scene-group').style.display = 'none';
|
||||
|
||||
let _editorTags = [];
|
||||
|
||||
if (automationId) {
|
||||
titleEl.innerHTML = `${ICON_AUTOMATION} ${t('automations.edit')}`;
|
||||
try {
|
||||
@@ -266,6 +277,7 @@ export async function openAutomationEditor(automationId, cloneData) {
|
||||
if (_deactivationModeIconSelect) _deactivationModeIconSelect.setValue(deactMode);
|
||||
_onDeactivationModeChange();
|
||||
_initSceneSelector('automation-fallback-scene-id', automation.deactivation_scene_preset_id);
|
||||
_editorTags = automation.tags || [];
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
return;
|
||||
@@ -293,6 +305,7 @@ export async function openAutomationEditor(automationId, cloneData) {
|
||||
if (_deactivationModeIconSelect) _deactivationModeIconSelect.setValue(cloneDeactMode);
|
||||
_onDeactivationModeChange();
|
||||
_initSceneSelector('automation-fallback-scene-id', cloneData.deactivation_scene_preset_id);
|
||||
_editorTags = cloneData.tags || [];
|
||||
} else {
|
||||
titleEl.innerHTML = `${ICON_AUTOMATION} ${t('automations.add')}`;
|
||||
idInput.value = '';
|
||||
@@ -314,6 +327,12 @@ export async function openAutomationEditor(automationId, cloneData) {
|
||||
modal.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
||||
el.placeholder = t(el.getAttribute('data-i18n-placeholder'));
|
||||
});
|
||||
|
||||
// Tags
|
||||
if (_automationTagsInput) { _automationTagsInput.destroy(); _automationTagsInput = null; }
|
||||
_automationTagsInput = new TagInput(document.getElementById('automation-tags-container'), { placeholder: t('tags.placeholder') });
|
||||
_automationTagsInput.setValue(_editorTags);
|
||||
|
||||
automationModal.snapshot();
|
||||
}
|
||||
|
||||
@@ -671,6 +690,7 @@ export async function saveAutomationEditor() {
|
||||
scene_preset_id: document.getElementById('automation-scene-id').value || null,
|
||||
deactivation_mode: document.getElementById('automation-deactivation-mode').value,
|
||||
deactivation_scene_preset_id: document.getElementById('automation-fallback-scene-id').value || null,
|
||||
tags: _automationTagsInput ? _automationTagsInput.getValue() : [],
|
||||
};
|
||||
|
||||
const automationId = idInput.value;
|
||||
|
||||
Reference in New Issue
Block a user