From 590f019ca70498946b151a134fa7fa23603ebaff Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 10 Mar 2026 22:19:06 +0000 Subject: [PATCH] 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 --- core/base_repository.py | 2 +- routers/focus.py | 64 ++++++++++++++++++++++++++- routers/links.py | 12 +++++ routers/lists.py | 13 ++++++ routers/tasks.py | 35 ++++++++++++++- templates/focus.html | 15 +++---- templates/focus_detail.html | 87 ++++++++++++++++++++++++++++++++++++- templates/link_form.html | 3 +- templates/links.html | 2 +- templates/lists.html | 51 +++++++++++++++++++++- templates/task_detail.html | 22 +++++++++- templates/weblinks.html | 4 +- 12 files changed, 291 insertions(+), 19 deletions(-) diff --git a/core/base_repository.py b/core/base_repository.py index eaf77f9..3b9d614 100644 --- a/core/base_repository.py +++ b/core/base_repository.py @@ -162,7 +162,7 @@ class BaseRepository: "contact_id", "started_at", "weekly_hours", "effective_from", "task_id", "meeting_id", "list_item_id", - "domain_id", "title", + "domain_id", "title", "focus_priority", } clean_data = {} for k, v in data.items(): diff --git a/routers/focus.py b/routers/focus.py index 36730a8..aa392d0 100644 --- a/routers/focus.py +++ b/routers/focus.py @@ -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) diff --git a/routers/links.py b/routers/links.py index 32b334e..bb4701a 100644 --- a/routers/links.py +++ b/routers/links.py @@ -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) diff --git a/routers/lists.py b/routers/lists.py index 7db9aa1..fb88839 100644 --- a/routers/lists.py +++ b/routers/lists.py @@ -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, diff --git a/routers/tasks.py b/routers/tasks.py index c995d54..aa1d648 100644 --- a/routers/tasks.py +++ b/routers/tasks.py @@ -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, diff --git a/templates/focus.html b/templates/focus.html index 631a6cc..f60ff88 100644 --- a/templates/focus.html +++ b/templates/focus.html @@ -29,7 +29,7 @@ - # + P# Done ! @@ -46,7 +46,11 @@ {% for item in items %} - {{ loop.index }} + +
+ +
+ {% with reorder_url="/focus/reorder", item_id=item.id %}{% include 'partials/reorder_arrows.html' %}{% endwith %}
@@ -207,13 +211,8 @@ }); } - // Renumber rows function renumberFocusRows() { - var rows = document.querySelectorAll('.focus-drag-row'); - rows.forEach(function(row, i) { - var numCell = row.querySelector('.focus-row-num'); - if (numCell) numCell.textContent = i + 1; - }); + // No-op: priority is user-set, not auto-numbered } // Drag-and-drop reorder diff --git a/templates/focus_detail.html b/templates/focus_detail.html index 8332b11..2ed517a 100644 --- a/templates/focus_detail.html +++ b/templates/focus_detail.html @@ -94,7 +94,14 @@
- {{ li.content }} + {{ li.content }} + + +
@@ -110,6 +117,53 @@ + +
+
+

Links

+
+
+
+ + +
+
+ + + + +
+ + + New Link +
+ {% for l in linked_links %} +
+ {{ l.label }} + {{ l.url }} + {% if l.role %}{{ l.role }}{% endif %} +
+ Edit +
+ +
+
+
+ {% else %} +
No links
+ {% endfor %} +
+ {% endblock %} diff --git a/templates/link_form.html b/templates/link_form.html index dd42226..76320a5 100644 --- a/templates/link_form.html +++ b/templates/link_form.html @@ -16,8 +16,9 @@ {% if prefill_task_id is defined and prefill_task_id %}{% endif %} {% if prefill_meeting_id is defined and prefill_meeting_id %}{% endif %} {% if prefill_contact_id is defined and prefill_contact_id %}{% endif %} + {% if prefill_focus_id is defined and prefill_focus_id %}{% endif %} {% if from_project is defined and from_project %}{% endif %} -
Cancel
+
Cancel
{% endblock %} diff --git a/templates/links.html b/templates/links.html index 78c9e0e..1eed79c 100644 --- a/templates/links.html +++ b/templates/links.html @@ -14,7 +14,7 @@ {% if item.domain_color %}{{ item.domain_name }}{% endif %} {{ item.label }} {% if item.project_name %}{{ item.project_name }}{% endif %} - {{ item.url[:50] }}{% if item.url|length > 50 %}...{% endif %} + {{ item.url }}
Edit
diff --git a/templates/lists.html b/templates/lists.html index 12b52fc..f248e3e 100644 --- a/templates/lists.html +++ b/templates/lists.html @@ -24,7 +24,7 @@ {% if items %}
{% for item in items %} -
+
{% with reorder_url="/lists/reorder", item_id=item.id %} {% include 'partials/reorder_arrows.html' %} {% endwith %} @@ -53,6 +53,9 @@
{% endfor %}
+ {% else %}
@@ -63,6 +66,52 @@