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:
@@ -22,6 +22,7 @@ import {
|
||||
ICON_MUSIC, ICON_TRENDING_UP, ICON_MAP_PIN, ICON_MONITOR, ICON_REFRESH,
|
||||
} from '../core/icons.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 { loadPictureSources } from './streams.js';
|
||||
@@ -31,10 +32,15 @@ export { getValueSourceIcon };
|
||||
// ── EntitySelect instances for value source editor ──
|
||||
let _vsAudioSourceEntitySelect = null;
|
||||
let _vsPictureSourceEntitySelect = null;
|
||||
let _vsTagsInput = null;
|
||||
|
||||
class ValueSourceModal extends Modal {
|
||||
constructor() { super('value-source-modal'); }
|
||||
|
||||
onForceClose() {
|
||||
if (_vsTagsInput) { _vsTagsInput.destroy(); _vsTagsInput = null; }
|
||||
}
|
||||
|
||||
snapshotValues() {
|
||||
const type = document.getElementById('value-source-type').value;
|
||||
return {
|
||||
@@ -58,6 +64,7 @@ class ValueSourceModal extends Modal {
|
||||
sceneSensitivity: document.getElementById('value-source-scene-sensitivity').value,
|
||||
sceneSmoothing: document.getElementById('value-source-scene-smoothing').value,
|
||||
schedule: JSON.stringify(_getScheduleFromUI()),
|
||||
tags: JSON.stringify(_vsTagsInput ? _vsTagsInput.getValue() : []),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -241,6 +248,11 @@ export async function showValueSourceModal(editData) {
|
||||
document.getElementById('value-source-mode').onchange = () => _autoGenerateVSName();
|
||||
document.getElementById('value-source-picture-source').onchange = () => _autoGenerateVSName();
|
||||
|
||||
// Tags
|
||||
if (_vsTagsInput) { _vsTagsInput.destroy(); _vsTagsInput = null; }
|
||||
_vsTagsInput = new TagInput(document.getElementById('value-source-tags-container'), { placeholder: t('tags.placeholder') });
|
||||
_vsTagsInput.setValue(editData ? (editData.tags || []) : []);
|
||||
|
||||
valueSourceModal.open();
|
||||
valueSourceModal.snapshot();
|
||||
}
|
||||
@@ -293,7 +305,7 @@ export async function saveValueSource() {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = { name, source_type: sourceType, description };
|
||||
const payload = { name, source_type: sourceType, description, tags: _vsTagsInput ? _vsTagsInput.getValue() : [] };
|
||||
|
||||
if (sourceType === 'static') {
|
||||
payload.value = parseFloat(document.getElementById('value-source-value').value);
|
||||
@@ -648,6 +660,7 @@ export function createValueSourceCard(src) {
|
||||
<div class="template-name">${icon} ${escapeHtml(src.name)}</div>
|
||||
</div>
|
||||
<div class="stream-card-props">${propsHtml}</div>
|
||||
${renderTagChips(src.tags)}
|
||||
${src.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(src.description)}</div>` : ''}`,
|
||||
actions: `
|
||||
<button class="btn btn-icon btn-secondary" onclick="testValueSource('${src.id}')" title="${t('value_source.test')}">${ICON_TEST}</button>
|
||||
|
||||
Reference in New Issue
Block a user