Backend: - Added color (hex) and icon (lucide name) columns to folders and tags - Default folders seeded with colors: Home=green, Work=indigo, Travel=blue, etc. - API returns color/icon in sidebar and CRUD responses - Create/update endpoints accept color and icon Frontend: - Mobile: horizontal scrollable pill tabs with colored dots - Desktop sidebar: colored dots next to folder names - Active pill gets tinted border matching folder color Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
117 lines
4.2 KiB
Python
117 lines
4.2 KiB
Python
"""Database models for folders and tags — editable taxonomy."""
|
|
|
|
import re
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import Column, String, Integer, Boolean, DateTime, ForeignKey, Index, UniqueConstraint
|
|
from sqlalchemy.dialects.postgresql import UUID
|
|
from sqlalchemy.orm import relationship
|
|
|
|
from app.database import Base
|
|
|
|
|
|
def new_id():
|
|
return str(uuid.uuid4())
|
|
|
|
|
|
def slugify(text: str) -> str:
|
|
s = text.lower().strip()
|
|
s = re.sub(r'[^\w\s-]', '', s)
|
|
s = re.sub(r'[\s_]+', '-', s)
|
|
return s.strip('-')
|
|
|
|
|
|
class Folder(Base):
|
|
__tablename__ = "folders"
|
|
|
|
id = Column(UUID(as_uuid=False), primary_key=True, default=new_id)
|
|
user_id = Column(String(64), nullable=False, index=True)
|
|
name = Column(String(128), nullable=False)
|
|
slug = Column(String(128), nullable=False)
|
|
color = Column(String(7), nullable=True) # hex like #4F46E5
|
|
icon = Column(String(32), nullable=True) # lucide icon name like "home"
|
|
is_active = Column(Boolean, default=True, nullable=False)
|
|
sort_order = Column(Integer, default=0, nullable=False)
|
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint('user_id', 'slug', name='uq_folder_user_slug'),
|
|
)
|
|
|
|
|
|
class Tag(Base):
|
|
__tablename__ = "tags"
|
|
|
|
id = Column(UUID(as_uuid=False), primary_key=True, default=new_id)
|
|
user_id = Column(String(64), nullable=False, index=True)
|
|
name = Column(String(128), nullable=False)
|
|
slug = Column(String(128), nullable=False)
|
|
color = Column(String(7), nullable=True)
|
|
icon = Column(String(32), nullable=True)
|
|
is_active = Column(Boolean, default=True, nullable=False)
|
|
sort_order = Column(Integer, default=0, nullable=False)
|
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint('user_id', 'slug', name='uq_tag_user_slug'),
|
|
)
|
|
|
|
|
|
class ItemTag(Base):
|
|
__tablename__ = "item_tags"
|
|
|
|
item_id = Column(UUID(as_uuid=False), ForeignKey("items.id", ondelete="CASCADE"), primary_key=True)
|
|
tag_id = Column(UUID(as_uuid=False), ForeignKey("tags.id", ondelete="CASCADE"), primary_key=True)
|
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
|
|
|
|
# Default folders with colors and icons
|
|
DEFAULT_FOLDERS = [
|
|
{"name": "Home", "color": "#059669", "icon": "home"},
|
|
{"name": "Family", "color": "#D97706", "icon": "heart"},
|
|
{"name": "Work", "color": "#4338CA", "icon": "briefcase"},
|
|
{"name": "Travel", "color": "#0EA5E9", "icon": "plane"},
|
|
{"name": "Knowledge", "color": "#8B5CF6", "icon": "book-open"},
|
|
{"name": "Faith", "color": "#10B981", "icon": "moon"},
|
|
{"name": "Projects", "color": "#F43F5E", "icon": "folder"},
|
|
]
|
|
|
|
# Default tags to seed for new users
|
|
DEFAULT_TAGS = [
|
|
"reference", "important", "legal", "financial", "insurance",
|
|
"research", "idea", "guide", "tutorial", "setup", "how-to",
|
|
"tools", "dev", "server", "selfhosted", "home-assistant",
|
|
"shopping", "compare", "buy", "product",
|
|
"family", "kids", "health", "travel", "faith",
|
|
"video", "read-later", "books",
|
|
]
|
|
|
|
|
|
async def ensure_user_taxonomy(db, user_id: str):
|
|
"""Seed default folders and tags for a new user if they have none."""
|
|
from sqlalchemy import select, func
|
|
|
|
folder_count = (await db.execute(
|
|
select(func.count()).where(Folder.user_id == user_id)
|
|
)).scalar() or 0
|
|
|
|
if folder_count == 0:
|
|
for i, f in enumerate(DEFAULT_FOLDERS):
|
|
db.add(Folder(id=new_id(), user_id=user_id, name=f["name"], slug=slugify(f["name"]),
|
|
color=f.get("color"), icon=f.get("icon"), sort_order=i))
|
|
await db.flush()
|
|
|
|
tag_count = (await db.execute(
|
|
select(func.count()).where(Tag.user_id == user_id)
|
|
)).scalar() or 0
|
|
|
|
if tag_count == 0:
|
|
for i, name in enumerate(DEFAULT_TAGS):
|
|
db.add(Tag(id=new_id(), user_id=user_id, name=name, slug=slugify(name), sort_order=i))
|
|
await db.flush()
|
|
|
|
await db.commit()
|