feat: AI flow builder, visibility model, dashboard tabs, fork UI (#88)
- AI flow builder: scaffold → branch detail → assemble → review flow - Generate All one-click branch generation with stop/cancel - Regenerate scaffold suggestions button - 3-action review screen: Start Flow, Open in Editor, Build Another - Fix Publish button gated behind !isDirty - Fix visibility column enforcement in tree access filter - Add ?visibility filter and author_name to GET /trees - Dashboard tabbed flows: My Flows / My Team / Public / All - Create button in My Flows tab, window focus reload (stale data fix) - Fork UI with optional reason modal - Fix account_id nullability in User type and schema - Keep is_public and visibility in sync on updates Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit was merged in pull request #88.
This commit is contained in:
@@ -32,7 +32,7 @@ from app.core.tree_validation import can_publish_tree
|
||||
router = APIRouter(prefix="/trees", tags=["trees"])
|
||||
|
||||
|
||||
def build_tree_response(tree: Tree) -> TreeListResponse:
|
||||
def build_tree_response(tree: Tree, author_map: dict | None = None) -> TreeListResponse:
|
||||
"""Build TreeListResponse with category_info and tags."""
|
||||
category_info = None
|
||||
if tree.category_rel:
|
||||
@@ -42,6 +42,8 @@ def build_tree_response(tree: Tree) -> TreeListResponse:
|
||||
slug=tree.category_rel.slug
|
||||
)
|
||||
|
||||
author_name = (author_map or {}).get(tree.author_id)
|
||||
|
||||
return TreeListResponse(
|
||||
id=tree.id,
|
||||
name=tree.name,
|
||||
@@ -52,10 +54,12 @@ def build_tree_response(tree: Tree) -> TreeListResponse:
|
||||
category_info=category_info,
|
||||
tags=tree.tag_names,
|
||||
author_id=tree.author_id,
|
||||
author_name=author_name,
|
||||
account_id=tree.account_id,
|
||||
is_active=tree.is_active,
|
||||
is_public=tree.is_public,
|
||||
is_default=tree.is_default,
|
||||
visibility=tree.visibility,
|
||||
status=tree.status,
|
||||
version=tree.version,
|
||||
usage_count=tree.usage_count,
|
||||
@@ -125,6 +129,7 @@ async def list_trees(
|
||||
is_active: Optional[bool] = Query(None, description="Filter by active status"),
|
||||
author_id: Optional[UUID] = Query(None, description="Filter by author ID"),
|
||||
is_public: Optional[bool] = Query(None, description="Filter by public status"),
|
||||
visibility: Optional[str] = Query(None, description="Filter by visibility: private, team, link, public"),
|
||||
sort_by: Optional[str] = Query("usage_count", description="Sort order: usage_count, updated_at, created_at, name, name_desc, version"),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=100)
|
||||
@@ -158,6 +163,8 @@ async def list_trees(
|
||||
query = query.where(Tree.author_id == author_id)
|
||||
if is_public is not None:
|
||||
query = query.where(Tree.is_public == is_public)
|
||||
if visibility:
|
||||
query = query.where(Tree.visibility == visibility)
|
||||
|
||||
# Filter by tags (all specified tags must be present)
|
||||
if tags:
|
||||
@@ -206,7 +213,17 @@ async def list_trees(
|
||||
result = await db.execute(query)
|
||||
trees = result.scalars().unique().all()
|
||||
|
||||
return [build_tree_response(tree) for tree in trees]
|
||||
# Fetch author names in one query (avoids N+1)
|
||||
author_ids = {t.author_id for t in trees if t.author_id}
|
||||
author_map: dict = {}
|
||||
if author_ids:
|
||||
authors_result = await db.execute(
|
||||
select(User.id, User.name, User.email).where(User.id.in_(author_ids))
|
||||
)
|
||||
for row in authors_result:
|
||||
author_map[row.id] = row.name or row.email
|
||||
|
||||
return [build_tree_response(tree, author_map) for tree in trees]
|
||||
|
||||
|
||||
@router.get("/categories", response_model=list[str])
|
||||
@@ -612,6 +629,13 @@ async def update_tree(
|
||||
for field, value in update_data.items():
|
||||
setattr(tree, field, value)
|
||||
|
||||
# Keep visibility and is_public in sync
|
||||
if tree_data.is_public is not None:
|
||||
if tree_data.is_public and tree.visibility not in ('public',):
|
||||
tree.visibility = 'public'
|
||||
elif not tree_data.is_public and tree.visibility == 'public':
|
||||
tree.visibility = 'team' # downgrade from public to team
|
||||
|
||||
# Increment version if tree structure changed
|
||||
if "tree_structure" in update_data:
|
||||
tree.version += 1
|
||||
@@ -1057,6 +1081,7 @@ async def update_tree_visibility(
|
||||
# Update visibility
|
||||
old_visibility = tree.visibility
|
||||
tree.visibility = visibility_data.visibility
|
||||
tree.is_public = (visibility_data.visibility == 'public')
|
||||
|
||||
await log_audit(db, current_user.id, "tree.visibility.update", "tree", tree.id,
|
||||
{"tree_name": tree.name, "old_visibility": old_visibility,
|
||||
|
||||
Reference in New Issue
Block a user