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:
2026-03-10 22:19:06 +00:00
parent 30cff10150
commit 590f019ca7
12 changed files with 291 additions and 19 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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,