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:
@@ -9,18 +9,26 @@ import { Modal } from '../core/modal.js';
|
||||
import { showToast, showConfirm } from '../core/ui.js';
|
||||
import { ICON_CLOCK, ICON_CLONE, ICON_EDIT, ICON_START, ICON_PAUSE } from '../core/icons.js';
|
||||
import { wrapCard } from '../core/card-colors.js';
|
||||
import { TagInput, renderTagChips } from '../core/tag-input.js';
|
||||
import { loadPictureSources } from './streams.js';
|
||||
|
||||
// ── Modal ──
|
||||
|
||||
let _syncClockTagsInput = null;
|
||||
|
||||
class SyncClockModal extends Modal {
|
||||
constructor() { super('sync-clock-modal'); }
|
||||
|
||||
onForceClose() {
|
||||
if (_syncClockTagsInput) { _syncClockTagsInput.destroy(); _syncClockTagsInput = null; }
|
||||
}
|
||||
|
||||
snapshotValues() {
|
||||
return {
|
||||
name: document.getElementById('sync-clock-name').value,
|
||||
speed: document.getElementById('sync-clock-speed').value,
|
||||
description: document.getElementById('sync-clock-description').value,
|
||||
tags: JSON.stringify(_syncClockTagsInput ? _syncClockTagsInput.getValue() : []),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -48,6 +56,11 @@ export async function showSyncClockModal(editData) {
|
||||
document.getElementById('sync-clock-description').value = '';
|
||||
}
|
||||
|
||||
// Tags
|
||||
if (_syncClockTagsInput) { _syncClockTagsInput.destroy(); _syncClockTagsInput = null; }
|
||||
_syncClockTagsInput = new TagInput(document.getElementById('sync-clock-tags-container'), { placeholder: t('tags.placeholder') });
|
||||
_syncClockTagsInput.setValue(isEdit ? (editData.tags || []) : []);
|
||||
|
||||
syncClockModal.open();
|
||||
syncClockModal.snapshot();
|
||||
}
|
||||
@@ -69,7 +82,7 @@ export async function saveSyncClock() {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = { name, speed, description };
|
||||
const payload = { name, speed, description, tags: _syncClockTagsInput ? _syncClockTagsInput.getValue() : [] };
|
||||
|
||||
try {
|
||||
const method = id ? 'PUT' : 'POST';
|
||||
@@ -199,6 +212,7 @@ export function createSyncClockCard(clock) {
|
||||
<span class="stream-card-prop">${statusIcon} ${statusLabel}</span>
|
||||
<span class="stream-card-prop">${ICON_CLOCK} ${clock.speed}x</span>
|
||||
</div>
|
||||
${renderTagChips(clock.tags)}
|
||||
${clock.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(clock.description)}</div>` : ''}`,
|
||||
actions: `
|
||||
<button class="btn btn-icon btn-secondary" onclick="event.stopPropagation(); ${toggleAction}" title="${toggleTitle}">${clock.is_running ? ICON_PAUSE : ICON_START}</button>
|
||||
|
||||
Reference in New Issue
Block a user