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:
2026-03-09 22:20:19 +03:00
parent 2712c6682e
commit 30fa107ef7
120 changed files with 2471 additions and 1949 deletions

View File

@@ -41,7 +41,8 @@ async def list_audio_templates(
responses = [
AudioTemplateResponse(
id=t.id, name=t.name, engine_type=t.engine_type,
engine_config=t.engine_config, created_at=t.created_at,
engine_config=t.engine_config, tags=getattr(t, 'tags', []),
created_at=t.created_at,
updated_at=t.updated_at, description=t.description,
)
for t in templates
@@ -63,10 +64,12 @@ async def create_audio_template(
template = store.create_template(
name=data.name, engine_type=data.engine_type,
engine_config=data.engine_config, description=data.description,
tags=data.tags,
)
return AudioTemplateResponse(
id=template.id, name=template.name, engine_type=template.engine_type,
engine_config=template.engine_config, created_at=template.created_at,
engine_config=template.engine_config, tags=getattr(template, 'tags', []),
created_at=template.created_at,
updated_at=template.updated_at, description=template.description,
)
except ValueError as e:
@@ -89,7 +92,8 @@ async def get_audio_template(
raise HTTPException(status_code=404, detail=f"Audio template {template_id} not found")
return AudioTemplateResponse(
id=t.id, name=t.name, engine_type=t.engine_type,
engine_config=t.engine_config, created_at=t.created_at,
engine_config=t.engine_config, tags=getattr(t, 'tags', []),
created_at=t.created_at,
updated_at=t.updated_at, description=t.description,
)
@@ -106,11 +110,12 @@ async def update_audio_template(
t = store.update_template(
template_id=template_id, name=data.name,
engine_type=data.engine_type, engine_config=data.engine_config,
description=data.description,
description=data.description, tags=data.tags,
)
return AudioTemplateResponse(
id=t.id, name=t.name, engine_type=t.engine_type,
engine_config=t.engine_config, created_at=t.created_at,
engine_config=t.engine_config, tags=getattr(t, 'tags', []),
created_at=t.created_at,
updated_at=t.updated_at, description=t.description,
)
except ValueError as e: