a7e3890634
- 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
87 lines
2.8 KiB
Python
87 lines
2.8 KiB
Python
"""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"))
|