Add tree organization system with categories, tags, and folders

Features:
- Categories: Global and team-specific tree categorization (admin-managed)
- Tags: Flexible tree tagging with autocomplete (author + admin)
- User folders: Personal tree collections with subfolder support
  - Hierarchical structure (max 3 levels deep)
  - Right-click context menu for folder management
  - Cascade delete for subfolders
- Filter trees by category, tags, and folder in library view

Backend:
- New models: Category, Tag, UserFolder with relationships
- New API endpoints for categories, tags, and folders
- Tree organization migrations (005, 006)

Frontend:
- FolderSidebar with hierarchical folder tree
- FolderEditModal for create/edit with color picker
- AddToFolderMenu for quick tree organization
- TagInput with autocomplete and TagBadges display
- Updated TreeMetadataForm and TreeLibraryPage

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-02 01:31:13 -05:00
parent 2d99c52025
commit fafdaa50a5
41 changed files with 5006 additions and 221 deletions

View File

@@ -1,11 +1,17 @@
import uuid
from datetime import datetime, timezone
from typing import Optional
from sqlalchemy import String, DateTime, ForeignKey
from typing import Optional, TYPE_CHECKING
from sqlalchemy import String, DateTime, ForeignKey, Boolean
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID
from app.core.database import Base
if TYPE_CHECKING:
from app.models.team import Team
from app.models.tree import Tree
from app.models.session import Session
from app.models.folder import UserFolder
class User(Base):
__tablename__ = "users"
@@ -19,6 +25,7 @@ class User(Base):
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
role: Mapped[str] = mapped_column(String(50), nullable=False, default="engineer")
is_team_admin: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
team_id: Mapped[Optional[uuid.UUID]] = mapped_column(
UUID(as_uuid=True),
ForeignKey("teams.id"),
@@ -38,4 +45,15 @@ class User(Base):
# Relationships
team: Mapped[Optional["Team"]] = relationship("Team", back_populates="users")
trees: Mapped[list["Tree"]] = relationship("Tree", back_populates="author")
sessions: Mapped[list["Session"]] = relationship("Session", back_populates="user")
sessions: Mapped[list["Session"]] = relationship("Session", back_populates="user")
folders: Mapped[list["UserFolder"]] = relationship("UserFolder", back_populates="user")
@property
def is_admin(self) -> bool:
"""Returns True if user is a global (Patherly) admin."""
return self.role == "admin"
@property
def can_manage_team(self) -> bool:
"""Returns True if user can manage their team (team admin or global admin)."""
return self.is_admin or (self.is_team_admin and self.team_id is not None)