feat: focus priority, focus links, task list assignment, lists drag-and-drop, URL display fixes
- Focus page: turn sequence number into persistent editable priority (focus_priority column) - Focus detail: add links section (add existing, create new, unlink) via focus_links junction table - Focus detail: add copy and inline edit for checklist items - Task detail lists tab: add existing list assignment and unlink actions - Lists page: add drag-and-drop reorder support - Links/bookmarks pages: remove artificial URL truncation, use CSS ellipsis Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -63,7 +63,7 @@ async def focus_view(
|
||||
WHERE df.is_deleted = false
|
||||
AND (t.id IS NULL OR t.is_deleted = false)
|
||||
AND (li.id IS NULL OR li.is_deleted = false)
|
||||
ORDER BY df.sort_order, df.created_at
|
||||
ORDER BY df.focus_priority ASC NULLS LAST, df.sort_order, df.created_at
|
||||
"""))
|
||||
items = [dict(r._mapping) for r in result]
|
||||
|
||||
@@ -344,10 +344,23 @@ async def focus_detail(
|
||||
"""), {"lid": list_id})
|
||||
list_items = [dict(r._mapping) for r in result]
|
||||
|
||||
# Load linked links
|
||||
result = await db.execute(text("""
|
||||
SELECT l.*, fl.role
|
||||
FROM links l JOIN focus_links fl ON fl.link_id = l.id
|
||||
WHERE fl.focus_id = :fid AND l.is_deleted = false
|
||||
ORDER BY fl.created_at
|
||||
"""), {"fid": focus_id})
|
||||
linked_links = [dict(r._mapping) for r in result]
|
||||
|
||||
# All links for the "add existing" dropdown
|
||||
all_links = await BaseRepository("links", db).list()
|
||||
|
||||
return templates.TemplateResponse("focus_detail.html", {
|
||||
"request": request, "sidebar": sidebar, "item": item,
|
||||
"domain": domain, "project": project, "all_lists": all_lists,
|
||||
"note": note, "list_id": list_id, "list_items": list_items,
|
||||
"linked_links": linked_links, "all_links": all_links,
|
||||
"page_title": item.get("title", "Focus Item"), "active_nav": "focus",
|
||||
})
|
||||
|
||||
@@ -392,6 +405,17 @@ async def toggle_focus_list_item(
|
||||
return RedirectResponse(url=f"/focus/{focus_id}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/{focus_id}/list-item/{item_id}/edit")
|
||||
async def edit_focus_list_item(
|
||||
focus_id: str, item_id: str, request: Request,
|
||||
content: str = Form(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
li_repo = BaseRepository("list_items", db)
|
||||
await li_repo.update(item_id, {"content": content.strip()})
|
||||
return RedirectResponse(url=f"/focus/{focus_id}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/{focus_id}/list-item/{item_id}/delete")
|
||||
async def delete_focus_list_item(
|
||||
focus_id: str, item_id: str, request: Request,
|
||||
@@ -600,6 +624,44 @@ async def toggle_focus(focus_id: str, request: Request, db: AsyncSession = Depen
|
||||
return RedirectResponse(url=referer, status_code=303)
|
||||
|
||||
|
||||
@router.post("/{focus_id}/set-priority")
|
||||
async def set_focus_priority(focus_id: str, request: Request, focus_priority: Optional[str] = Form(None), db: AsyncSession = Depends(get_db)):
|
||||
repo = BaseRepository("daily_focus", db)
|
||||
val = None
|
||||
if focus_priority and focus_priority.strip():
|
||||
try:
|
||||
val = int(focus_priority.strip())
|
||||
except ValueError:
|
||||
pass
|
||||
await repo.update(focus_id, {"focus_priority": val})
|
||||
return RedirectResponse(url="/focus", status_code=303)
|
||||
|
||||
|
||||
@router.post("/{focus_id}/links/add")
|
||||
async def add_focus_link(
|
||||
focus_id: str,
|
||||
link_id: str = Form(...),
|
||||
role: Optional[str] = Form(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
await db.execute(text("""
|
||||
INSERT INTO focus_links (focus_id, link_id, role)
|
||||
VALUES (:fid, :lid, :role) ON CONFLICT DO NOTHING
|
||||
"""), {"fid": focus_id, "lid": link_id, "role": role if role and role.strip() else None})
|
||||
return RedirectResponse(url=f"/focus/{focus_id}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/{focus_id}/links/{link_id}/remove")
|
||||
async def remove_focus_link(
|
||||
focus_id: str, link_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
await db.execute(text(
|
||||
"DELETE FROM focus_links WHERE focus_id = :fid AND link_id = :lid"
|
||||
), {"fid": focus_id, "lid": link_id})
|
||||
return RedirectResponse(url=f"/focus/{focus_id}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/{focus_id}/toggle-critical")
|
||||
async def toggle_critical(focus_id: str, request: Request, db: AsyncSession = Depends(get_db)):
|
||||
repo = BaseRepository("daily_focus", db)
|
||||
|
||||
@@ -53,6 +53,7 @@ async def create_form(
|
||||
task_id: Optional[str] = None,
|
||||
meeting_id: Optional[str] = None,
|
||||
contact_id: Optional[str] = None,
|
||||
focus_id: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
sidebar = await get_sidebar_data(db)
|
||||
@@ -69,6 +70,7 @@ async def create_form(
|
||||
"prefill_task_id": task_id or "",
|
||||
"prefill_meeting_id": meeting_id or "",
|
||||
"prefill_contact_id": contact_id or "",
|
||||
"prefill_focus_id": focus_id or "",
|
||||
})
|
||||
|
||||
|
||||
@@ -78,6 +80,7 @@ async def create_link(
|
||||
domain_id: Optional[str] = Form(None), project_id: Optional[str] = Form(None),
|
||||
task_id: Optional[str] = Form(None), meeting_id: Optional[str] = Form(None),
|
||||
contact_id: Optional[str] = Form(None),
|
||||
focus_id: Optional[str] = Form(None),
|
||||
description: Optional[str] = Form(None), tags: Optional[str] = Form(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
@@ -111,6 +114,15 @@ async def create_link(
|
||||
await db.commit()
|
||||
return RedirectResponse(url=f"/contacts/{contact_id}", status_code=303)
|
||||
|
||||
# Attach to focus item if created from focus context
|
||||
if focus_id and focus_id.strip():
|
||||
await db.execute(text("""
|
||||
INSERT INTO focus_links (focus_id, link_id)
|
||||
VALUES (:fid, :lid) ON CONFLICT DO NOTHING
|
||||
"""), {"fid": focus_id, "lid": link["id"]})
|
||||
await db.commit()
|
||||
return RedirectResponse(url=f"/focus/{focus_id}", status_code=303)
|
||||
|
||||
# Redirect back to context if created from task/meeting/project
|
||||
if task_id and task_id.strip():
|
||||
return RedirectResponse(url=f"/tasks/{task_id}?tab=links", status_code=303)
|
||||
|
||||
@@ -375,6 +375,19 @@ async def reorder_list(
|
||||
return RedirectResponse(url=request.headers.get("referer", "/lists"), status_code=303)
|
||||
|
||||
|
||||
@router.post("/reorder-all")
|
||||
async def reorder_all_lists(
|
||||
request: Request,
|
||||
item_ids: str = Form(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
repo = BaseRepository("lists", db)
|
||||
ids = [i.strip() for i in item_ids.split(",") if i.strip()]
|
||||
if ids:
|
||||
await repo.reorder(ids)
|
||||
return RedirectResponse(url=request.headers.get("referer", "/lists"), status_code=303)
|
||||
|
||||
|
||||
@router.post("/{list_id}/items/reorder")
|
||||
async def reorder_list_item(
|
||||
list_id: str,
|
||||
|
||||
@@ -235,6 +235,7 @@ async def task_detail(
|
||||
# Tab-specific data
|
||||
tab_data = []
|
||||
all_contacts = []
|
||||
all_lists = []
|
||||
|
||||
if tab == "notes":
|
||||
result = await db.execute(text("""
|
||||
@@ -268,6 +269,13 @@ async def task_detail(
|
||||
ORDER BY l.sort_order, l.created_at DESC
|
||||
"""), {"tid": task_id})
|
||||
tab_data = [dict(r._mapping) for r in result]
|
||||
# Available lists for add dropdown (not already assigned to this task)
|
||||
result = await db.execute(text("""
|
||||
SELECT id, name FROM lists
|
||||
WHERE is_deleted = false AND (task_id IS NULL OR task_id != :tid)
|
||||
ORDER BY name
|
||||
"""), {"tid": task_id})
|
||||
all_lists = [dict(r._mapping) for r in result]
|
||||
|
||||
elif tab == "decisions":
|
||||
result = await db.execute(text("""
|
||||
@@ -315,7 +323,7 @@ async def task_detail(
|
||||
"request": request, "sidebar": sidebar, "item": item,
|
||||
"domain": domain, "project": project, "parent": parent,
|
||||
"subtasks": subtasks, "tab": tab, "tab_data": tab_data,
|
||||
"all_contacts": all_contacts, "counts": counts,
|
||||
"all_contacts": all_contacts, "all_lists": all_lists, "counts": counts,
|
||||
"running_task_id": running_task_id,
|
||||
"page_title": item["title"], "active_nav": "tasks",
|
||||
})
|
||||
@@ -471,6 +479,31 @@ async def quick_add(
|
||||
|
||||
# ---- Contact linking ----
|
||||
|
||||
@router.post("/{task_id}/lists/add")
|
||||
async def add_list_to_task(
|
||||
task_id: str,
|
||||
list_id: str = Form(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
await db.execute(text(
|
||||
"UPDATE lists SET task_id = :tid, updated_at = now() WHERE id = :lid"
|
||||
), {"tid": task_id, "lid": list_id})
|
||||
await db.commit()
|
||||
return RedirectResponse(url=f"/tasks/{task_id}?tab=lists", status_code=303)
|
||||
|
||||
|
||||
@router.post("/{task_id}/lists/{list_id}/remove")
|
||||
async def remove_list_from_task(
|
||||
task_id: str, list_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
await db.execute(text(
|
||||
"UPDATE lists SET task_id = NULL, updated_at = now() WHERE id = :lid AND task_id = :tid"
|
||||
), {"tid": task_id, "lid": list_id})
|
||||
await db.commit()
|
||||
return RedirectResponse(url=f"/tasks/{task_id}?tab=lists", status_code=303)
|
||||
|
||||
|
||||
@router.post("/{task_id}/contacts/add")
|
||||
async def add_contact(
|
||||
task_id: str,
|
||||
|
||||
Reference in New Issue
Block a user