Phase 2: Tags UI, Custom Fields UI, Notifications Bell
- Tags UI: TagsPage (CRUD, color picker), TagBadge, TagSelector (multi-select, inline creation) - Custom Fields Backend: model, schema, service, routes, migration 0041 - Custom Fields Frontend: CustomFieldsPage (definitions CRUD), CustomFieldRenderer (dynamic field rendering) - Custom Fields: _collect_custom_field_definitions() extended to merge DB definitions with plugin definitions - Notifications Bell: NotificationBell (30s polling, unread badge), NotificationDropdown, NotificationItem - NotificationBell integrated into TopBar - Routes: /tags, /settings/custom-fields registered - Settings nav: Custom Fields entry added - Menu items: Tags added to automation plugin manifest
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
"""Create custom_field_definitions table for user-defined custom fields.
|
||||
|
||||
Revision ID: 0041_custom_field_definitions
|
||||
Revises: 0040_outbox
|
||||
Create Date: 2026-07-26
|
||||
|
||||
Stores user-defined custom field definitions that are merged with
|
||||
plugin-provided custom fields at query time.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers
|
||||
revision: str = "0041_custom_field_definitions"
|
||||
down_revision: Union[str, None] = "0040_outbox"
|
||||
branch_labels: Union[str, None] = None
|
||||
depends_on: Union[str, None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS custom_field_definitions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
entity VARCHAR(50) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
label VARCHAR(200) NOT NULL,
|
||||
field_type VARCHAR(20) NOT NULL DEFAULT 'text',
|
||||
options JSONB,
|
||||
default_value JSONB,
|
||||
required BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_by UUID,
|
||||
updated_by UUID,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
# Indexes
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS ix_custom_field_def_tenant "
|
||||
"ON custom_field_definitions (tenant_id)"
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS ix_custom_field_def_entity "
|
||||
"ON custom_field_definitions (entity)"
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS ix_custom_field_def_tenant_active "
|
||||
"ON custom_field_definitions (tenant_id, is_active)"
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_custom_field_def_tenant_entity_name "
|
||||
"ON custom_field_definitions (tenant_id, entity, name)"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
conn.execute(sa.text("DROP INDEX IF EXISTS uq_custom_field_def_tenant_entity_name"))
|
||||
conn.execute(sa.text("DROP INDEX IF EXISTS ix_custom_field_def_tenant_active"))
|
||||
conn.execute(sa.text("DROP INDEX IF EXISTS ix_custom_field_def_entity"))
|
||||
conn.execute(sa.text("DROP INDEX IF EXISTS ix_custom_field_def_tenant"))
|
||||
conn.execute(sa.text("DROP TABLE IF EXISTS custom_field_definitions"))
|
||||
Reference in New Issue
Block a user