Links and Other Enhancements
This commit is contained in:
36
main.py
36
main.py
@@ -155,12 +155,48 @@ async def dashboard(request: Request):
|
|||||||
"""))
|
"""))
|
||||||
stats = dict(result.first()._mapping)
|
stats = dict(result.first()._mapping)
|
||||||
|
|
||||||
|
# Overdue projects (target_date in the past)
|
||||||
|
result = await db.execute(text("""
|
||||||
|
SELECT p.id, p.name, p.priority, p.target_date, p.status,
|
||||||
|
d.name as domain_name, d.color as domain_color,
|
||||||
|
count(t.id) FILTER (WHERE t.is_deleted = false) as task_count,
|
||||||
|
count(t.id) FILTER (WHERE t.is_deleted = false AND t.status = 'done') as done_count
|
||||||
|
FROM projects p
|
||||||
|
LEFT JOIN domains d ON p.domain_id = d.id
|
||||||
|
LEFT JOIN tasks t ON t.project_id = p.id
|
||||||
|
WHERE p.is_deleted = false AND p.status IN ('active', 'on_hold')
|
||||||
|
AND p.target_date < CURRENT_DATE
|
||||||
|
GROUP BY p.id, d.name, d.color
|
||||||
|
ORDER BY p.target_date ASC
|
||||||
|
LIMIT 10
|
||||||
|
"""))
|
||||||
|
overdue_projects = [dict(r._mapping) for r in result]
|
||||||
|
|
||||||
|
# Upcoming project deadlines (next 30 days)
|
||||||
|
result = await db.execute(text("""
|
||||||
|
SELECT p.id, p.name, p.priority, p.target_date, p.status,
|
||||||
|
d.name as domain_name, d.color as domain_color,
|
||||||
|
count(t.id) FILTER (WHERE t.is_deleted = false) as task_count,
|
||||||
|
count(t.id) FILTER (WHERE t.is_deleted = false AND t.status = 'done') as done_count
|
||||||
|
FROM projects p
|
||||||
|
LEFT JOIN domains d ON p.domain_id = d.id
|
||||||
|
LEFT JOIN tasks t ON t.project_id = p.id
|
||||||
|
WHERE p.is_deleted = false AND p.status IN ('active', 'on_hold')
|
||||||
|
AND p.target_date >= CURRENT_DATE AND p.target_date <= CURRENT_DATE + INTERVAL '30 days'
|
||||||
|
GROUP BY p.id, d.name, d.color
|
||||||
|
ORDER BY p.target_date ASC
|
||||||
|
LIMIT 10
|
||||||
|
"""))
|
||||||
|
upcoming_projects = [dict(r._mapping) for r in result]
|
||||||
|
|
||||||
return templates.TemplateResponse("dashboard.html", {
|
return templates.TemplateResponse("dashboard.html", {
|
||||||
"request": request,
|
"request": request,
|
||||||
"sidebar": sidebar,
|
"sidebar": sidebar,
|
||||||
"focus_items": focus_items,
|
"focus_items": focus_items,
|
||||||
"overdue_tasks": overdue_tasks,
|
"overdue_tasks": overdue_tasks,
|
||||||
"upcoming_tasks": upcoming_tasks,
|
"upcoming_tasks": upcoming_tasks,
|
||||||
|
"overdue_projects": overdue_projects,
|
||||||
|
"upcoming_projects": upcoming_projects,
|
||||||
"stats": stats,
|
"stats": stats,
|
||||||
"page_title": "Dashboard",
|
"page_title": "Dashboard",
|
||||||
"active_nav": "dashboard",
|
"active_nav": "dashboard",
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ TRASH_ENTITIES = [
|
|||||||
{"table": "meetings", "label": "Meetings", "name_col": "title", "url": "/meetings/{id}"},
|
{"table": "meetings", "label": "Meetings", "name_col": "title", "url": "/meetings/{id}"},
|
||||||
{"table": "decisions", "label": "Decisions", "name_col": "title", "url": "/decisions/{id}"},
|
{"table": "decisions", "label": "Decisions", "name_col": "title", "url": "/decisions/{id}"},
|
||||||
{"table": "files", "label": "Files", "name_col": "original_filename", "url": "/files"},
|
{"table": "files", "label": "Files", "name_col": "original_filename", "url": "/files"},
|
||||||
{"table": "weblinks", "label": "Weblinks", "name_col": "label", "url": "/weblinks"},
|
{"table": "link_folders", "label": "Link Folders", "name_col": "name", "url": "/weblinks"},
|
||||||
{"table": "weblink_folders", "label": "Weblink Folders", "name_col": "name", "url": "/weblinks"},
|
|
||||||
{"table": "appointments", "label": "Appointments", "name_col": "title", "url": "/appointments/{id}"},
|
{"table": "appointments", "label": "Appointments", "name_col": "title", "url": "/appointments/{id}"},
|
||||||
{"table": "capture", "label": "Capture", "name_col": "raw_text", "url": "/capture"},
|
{"table": "capture", "label": "Capture", "name_col": "raw_text", "url": "/capture"},
|
||||||
{"table": "daily_focus", "label": "Focus Items", "name_col": "id", "url": "/focus"},
|
{"table": "daily_focus", "label": "Focus Items", "name_col": "id", "url": "/focus"},
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from sqlalchemy import text
|
|||||||
from core.database import get_db
|
from core.database import get_db
|
||||||
from core.base_repository import BaseRepository
|
from core.base_repository import BaseRepository
|
||||||
from core.sidebar import get_sidebar_data
|
from core.sidebar import get_sidebar_data
|
||||||
|
from routers.weblinks import get_default_folder_id
|
||||||
|
|
||||||
router = APIRouter(prefix="/capture", tags=["capture"])
|
router = APIRouter(prefix="/capture", tags=["capture"])
|
||||||
templates = Jinja2Templates(directory="templates")
|
templates = Jinja2Templates(directory="templates")
|
||||||
@@ -24,7 +25,7 @@ CONVERT_TYPES = {
|
|||||||
"list_item": "List Item",
|
"list_item": "List Item",
|
||||||
"contact": "Contact",
|
"contact": "Contact",
|
||||||
"decision": "Decision",
|
"decision": "Decision",
|
||||||
"weblink": "Weblink",
|
"link": "Link",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -309,8 +310,8 @@ async def convert_to_decision(
|
|||||||
return RedirectResponse(url=f"/decisions/{decision['id']}", status_code=303)
|
return RedirectResponse(url=f"/decisions/{decision['id']}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{capture_id}/to-weblink")
|
@router.post("/{capture_id}/to-link")
|
||||||
async def convert_to_weblink(
|
async def convert_to_link(
|
||||||
capture_id: str, request: Request,
|
capture_id: str, request: Request,
|
||||||
label: Optional[str] = Form(None),
|
label: Optional[str] = Form(None),
|
||||||
url: Optional[str] = Form(None),
|
url: Optional[str] = Form(None),
|
||||||
@@ -321,7 +322,7 @@ async def convert_to_weblink(
|
|||||||
if not item:
|
if not item:
|
||||||
return RedirectResponse(url="/capture", status_code=303)
|
return RedirectResponse(url="/capture", status_code=303)
|
||||||
|
|
||||||
weblink_repo = BaseRepository("weblinks", db)
|
link_repo = BaseRepository("links", db)
|
||||||
raw = item["raw_text"]
|
raw = item["raw_text"]
|
||||||
url_match = re.search(r'https?://\S+', raw)
|
url_match = re.search(r'https?://\S+', raw)
|
||||||
link_url = (url.strip() if url and url.strip() else None) or (url_match.group(0) if url_match else raw)
|
link_url = (url.strip() if url and url.strip() else None) or (url_match.group(0) if url_match else raw)
|
||||||
@@ -330,13 +331,20 @@ async def convert_to_weblink(
|
|||||||
link_label = link_url
|
link_label = link_url
|
||||||
|
|
||||||
data = {"label": link_label, "url": link_url}
|
data = {"label": link_label, "url": link_url}
|
||||||
weblink = await weblink_repo.create(data)
|
link = await link_repo.create(data)
|
||||||
|
|
||||||
|
# Assign to Default folder
|
||||||
|
default_fid = await get_default_folder_id(db)
|
||||||
|
await db.execute(text("""
|
||||||
|
INSERT INTO folder_links (folder_id, link_id)
|
||||||
|
VALUES (:fid, :lid) ON CONFLICT DO NOTHING
|
||||||
|
"""), {"fid": default_fid, "lid": link["id"]})
|
||||||
|
|
||||||
await capture_repo.update(capture_id, {
|
await capture_repo.update(capture_id, {
|
||||||
"processed": True, "converted_to_type": "weblink",
|
"processed": True, "converted_to_type": "link",
|
||||||
"converted_to_id": str(weblink["id"]),
|
"converted_to_id": str(link["id"]),
|
||||||
})
|
})
|
||||||
return RedirectResponse(url="/weblinks", status_code=303)
|
return RedirectResponse(url="/links", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{capture_id}/dismiss")
|
@router.post("/{capture_id}/dismiss")
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ HISTORY_ENTITIES = [
|
|||||||
("meetings", "title", "Meeting", "/meetings"),
|
("meetings", "title", "Meeting", "/meetings"),
|
||||||
("decisions", "title", "Decision", "/decisions"),
|
("decisions", "title", "Decision", "/decisions"),
|
||||||
("lists", "name", "List", "/lists"),
|
("lists", "name", "List", "/lists"),
|
||||||
("weblinks", "label", "Weblink", "/weblinks"),
|
|
||||||
("appointments", "title", "Appointment", "/appointments"),
|
("appointments", "title", "Appointment", "/appointments"),
|
||||||
("links", "label", "Link", "/links"),
|
("links", "label", "Link", "/links"),
|
||||||
("files", "original_filename", "File", "/files"),
|
("files", "original_filename", "File", "/files"),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Links: URL references attached to domains/projects."""
|
"""Links: URL references attached to domains/projects/tasks/meetings."""
|
||||||
|
|
||||||
from fastapi import APIRouter, Request, Form, Depends
|
from fastapi import APIRouter, Request, Form, Depends
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
@@ -10,6 +10,7 @@ from typing import Optional
|
|||||||
from core.database import get_db
|
from core.database import get_db
|
||||||
from core.base_repository import BaseRepository
|
from core.base_repository import BaseRepository
|
||||||
from core.sidebar import get_sidebar_data
|
from core.sidebar import get_sidebar_data
|
||||||
|
from routers.weblinks import get_default_folder_id
|
||||||
|
|
||||||
router = APIRouter(prefix="/links", tags=["links"])
|
router = APIRouter(prefix="/links", tags=["links"])
|
||||||
templates = Jinja2Templates(directory="templates")
|
templates = Jinja2Templates(directory="templates")
|
||||||
@@ -45,7 +46,14 @@ async def list_links(request: Request, domain_id: Optional[str] = None, db: Asyn
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/create")
|
@router.get("/create")
|
||||||
async def create_form(request: Request, domain_id: Optional[str] = None, project_id: Optional[str] = None, db: AsyncSession = Depends(get_db)):
|
async def create_form(
|
||||||
|
request: Request,
|
||||||
|
domain_id: Optional[str] = None,
|
||||||
|
project_id: Optional[str] = None,
|
||||||
|
task_id: Optional[str] = None,
|
||||||
|
meeting_id: Optional[str] = None,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
sidebar = await get_sidebar_data(db)
|
sidebar = await get_sidebar_data(db)
|
||||||
domains_repo = BaseRepository("domains", db)
|
domains_repo = BaseRepository("domains", db)
|
||||||
domains = await domains_repo.list()
|
domains = await domains_repo.list()
|
||||||
@@ -54,22 +62,48 @@ async def create_form(request: Request, domain_id: Optional[str] = None, project
|
|||||||
return templates.TemplateResponse("link_form.html", {
|
return templates.TemplateResponse("link_form.html", {
|
||||||
"request": request, "sidebar": sidebar, "domains": domains, "projects": projects,
|
"request": request, "sidebar": sidebar, "domains": domains, "projects": projects,
|
||||||
"page_title": "New Link", "active_nav": "links",
|
"page_title": "New Link", "active_nav": "links",
|
||||||
"item": None, "prefill_domain_id": domain_id or "", "prefill_project_id": project_id or "",
|
"item": None,
|
||||||
|
"prefill_domain_id": domain_id or "",
|
||||||
|
"prefill_project_id": project_id or "",
|
||||||
|
"prefill_task_id": task_id or "",
|
||||||
|
"prefill_meeting_id": meeting_id or "",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@router.post("/create")
|
@router.post("/create")
|
||||||
async def create_link(
|
async def create_link(
|
||||||
request: Request, label: str = Form(...), url: str = Form(...),
|
request: Request, label: str = Form(...), url: str = Form(...),
|
||||||
domain_id: str = Form(...), project_id: Optional[str] = Form(None),
|
domain_id: Optional[str] = Form(None), project_id: Optional[str] = Form(None),
|
||||||
description: Optional[str] = Form(None), db: AsyncSession = Depends(get_db),
|
task_id: Optional[str] = Form(None), meeting_id: Optional[str] = Form(None),
|
||||||
|
description: Optional[str] = Form(None), tags: Optional[str] = Form(None),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
repo = BaseRepository("links", db)
|
repo = BaseRepository("links", db)
|
||||||
data = {"label": label, "url": url, "domain_id": domain_id, "description": description}
|
data = {"label": label, "url": url, "description": description}
|
||||||
|
if domain_id and domain_id.strip():
|
||||||
|
data["domain_id"] = domain_id
|
||||||
if project_id and project_id.strip():
|
if project_id and project_id.strip():
|
||||||
data["project_id"] = project_id
|
data["project_id"] = project_id
|
||||||
await repo.create(data)
|
if task_id and task_id.strip():
|
||||||
referer = request.headers.get("referer", "/links")
|
data["task_id"] = task_id
|
||||||
|
if meeting_id and meeting_id.strip():
|
||||||
|
data["meeting_id"] = meeting_id
|
||||||
|
if tags and tags.strip():
|
||||||
|
data["tags"] = [t.strip() for t in tags.split(",") if t.strip()]
|
||||||
|
link = await repo.create(data)
|
||||||
|
|
||||||
|
# Assign to Default folder
|
||||||
|
default_fid = await get_default_folder_id(db)
|
||||||
|
await db.execute(text("""
|
||||||
|
INSERT INTO folder_links (folder_id, link_id)
|
||||||
|
VALUES (:fid, :lid) ON CONFLICT DO NOTHING
|
||||||
|
"""), {"fid": default_fid, "lid": link["id"]})
|
||||||
|
|
||||||
|
# Redirect back to context if created from task/meeting
|
||||||
|
if task_id and task_id.strip():
|
||||||
|
return RedirectResponse(url=f"/tasks/{task_id}?tab=links", status_code=303)
|
||||||
|
if meeting_id and meeting_id.strip():
|
||||||
|
return RedirectResponse(url=f"/meetings/{meeting_id}?tab=links", status_code=303)
|
||||||
return RedirectResponse(url="/links", status_code=303)
|
return RedirectResponse(url="/links", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@@ -87,22 +121,31 @@ async def edit_form(link_id: str, request: Request, db: AsyncSession = Depends(g
|
|||||||
return templates.TemplateResponse("link_form.html", {
|
return templates.TemplateResponse("link_form.html", {
|
||||||
"request": request, "sidebar": sidebar, "domains": domains, "projects": projects,
|
"request": request, "sidebar": sidebar, "domains": domains, "projects": projects,
|
||||||
"page_title": "Edit Link", "active_nav": "links",
|
"page_title": "Edit Link", "active_nav": "links",
|
||||||
"item": item, "prefill_domain_id": "", "prefill_project_id": "",
|
"item": item,
|
||||||
|
"prefill_domain_id": "", "prefill_project_id": "",
|
||||||
|
"prefill_task_id": "", "prefill_meeting_id": "",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{link_id}/edit")
|
@router.post("/{link_id}/edit")
|
||||||
async def update_link(
|
async def update_link(
|
||||||
link_id: str, label: str = Form(...), url: str = Form(...),
|
link_id: str, label: str = Form(...), url: str = Form(...),
|
||||||
domain_id: str = Form(...), project_id: Optional[str] = Form(None),
|
domain_id: Optional[str] = Form(None), project_id: Optional[str] = Form(None),
|
||||||
description: Optional[str] = Form(None), db: AsyncSession = Depends(get_db),
|
description: Optional[str] = Form(None), tags: Optional[str] = Form(None),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
repo = BaseRepository("links", db)
|
repo = BaseRepository("links", db)
|
||||||
await repo.update(link_id, {
|
data = {
|
||||||
"label": label, "url": url, "domain_id": domain_id,
|
"label": label, "url": url,
|
||||||
|
"domain_id": domain_id if domain_id and domain_id.strip() else None,
|
||||||
"project_id": project_id if project_id and project_id.strip() else None,
|
"project_id": project_id if project_id and project_id.strip() else None,
|
||||||
"description": description,
|
"description": description,
|
||||||
})
|
}
|
||||||
|
if tags and tags.strip():
|
||||||
|
data["tags"] = [t.strip() for t in tags.split(",") if t.strip()]
|
||||||
|
else:
|
||||||
|
data["tags"] = None
|
||||||
|
await repo.update(link_id, data)
|
||||||
return RedirectResponse(url="/links", status_code=303)
|
return RedirectResponse(url="/links", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -118,12 +118,24 @@ async def meeting_detail(
|
|||||||
if not item:
|
if not item:
|
||||||
return RedirectResponse(url="/meetings", status_code=303)
|
return RedirectResponse(url="/meetings", status_code=303)
|
||||||
|
|
||||||
|
# Linked projects (always shown in header)
|
||||||
|
result = await db.execute(text("""
|
||||||
|
SELECT p.id, p.name, d.color as domain_color
|
||||||
|
FROM projects p
|
||||||
|
JOIN project_meetings pm ON pm.project_id = p.id
|
||||||
|
LEFT JOIN domains d ON p.domain_id = d.id
|
||||||
|
WHERE pm.meeting_id = :mid AND p.is_deleted = false
|
||||||
|
ORDER BY p.name
|
||||||
|
"""), {"mid": meeting_id})
|
||||||
|
projects = [dict(r._mapping) for r in result]
|
||||||
|
|
||||||
# Overview data (always needed for overview tab)
|
# Overview data (always needed for overview tab)
|
||||||
action_items = []
|
action_items = []
|
||||||
decisions = []
|
decisions = []
|
||||||
domains = []
|
domains = []
|
||||||
tab_data = []
|
tab_data = []
|
||||||
all_contacts = []
|
all_contacts = []
|
||||||
|
all_decisions = []
|
||||||
|
|
||||||
if tab == "overview":
|
if tab == "overview":
|
||||||
# Action items
|
# Action items
|
||||||
@@ -160,9 +172,9 @@ async def meeting_detail(
|
|||||||
"""), {"mid": meeting_id})
|
"""), {"mid": meeting_id})
|
||||||
tab_data = [dict(r._mapping) for r in result]
|
tab_data = [dict(r._mapping) for r in result]
|
||||||
|
|
||||||
elif tab == "weblinks":
|
elif tab == "links":
|
||||||
result = await db.execute(text("""
|
result = await db.execute(text("""
|
||||||
SELECT * FROM weblinks
|
SELECT * FROM links
|
||||||
WHERE meeting_id = :mid AND is_deleted = false
|
WHERE meeting_id = :mid AND is_deleted = false
|
||||||
ORDER BY sort_order, label
|
ORDER BY sort_order, label
|
||||||
"""), {"mid": meeting_id})
|
"""), {"mid": meeting_id})
|
||||||
@@ -187,6 +199,20 @@ async def meeting_detail(
|
|||||||
"""), {"mid": meeting_id})
|
"""), {"mid": meeting_id})
|
||||||
tab_data = [dict(r._mapping) for r in result]
|
tab_data = [dict(r._mapping) for r in result]
|
||||||
|
|
||||||
|
elif tab == "decisions":
|
||||||
|
result = await db.execute(text("""
|
||||||
|
SELECT * FROM decisions
|
||||||
|
WHERE meeting_id = :mid AND is_deleted = false
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
"""), {"mid": meeting_id})
|
||||||
|
tab_data = [dict(r._mapping) for r in result]
|
||||||
|
result = await db.execute(text("""
|
||||||
|
SELECT id, title FROM decisions
|
||||||
|
WHERE (meeting_id IS NULL) AND is_deleted = false
|
||||||
|
ORDER BY created_at DESC LIMIT 50
|
||||||
|
"""))
|
||||||
|
all_decisions = [dict(r._mapping) for r in result]
|
||||||
|
|
||||||
elif tab == "contacts":
|
elif tab == "contacts":
|
||||||
result = await db.execute(text("""
|
result = await db.execute(text("""
|
||||||
SELECT c.*, cm.role, cm.created_at as linked_at
|
SELECT c.*, cm.role, cm.created_at as linked_at
|
||||||
@@ -206,9 +232,10 @@ async def meeting_detail(
|
|||||||
counts = {}
|
counts = {}
|
||||||
for count_tab, count_sql in [
|
for count_tab, count_sql in [
|
||||||
("notes", "SELECT count(*) FROM notes WHERE meeting_id = :mid AND is_deleted = false"),
|
("notes", "SELECT count(*) FROM notes WHERE meeting_id = :mid AND is_deleted = false"),
|
||||||
("weblinks", "SELECT count(*) FROM weblinks WHERE meeting_id = :mid AND is_deleted = false"),
|
("links", "SELECT count(*) FROM links WHERE meeting_id = :mid AND is_deleted = false"),
|
||||||
("files", "SELECT count(*) FROM files f JOIN file_mappings fm ON fm.file_id = f.id WHERE fm.context_type = 'meeting' AND fm.context_id = :mid AND f.is_deleted = false"),
|
("files", "SELECT count(*) FROM files f JOIN file_mappings fm ON fm.file_id = f.id WHERE fm.context_type = 'meeting' AND fm.context_id = :mid AND f.is_deleted = false"),
|
||||||
("lists", "SELECT count(*) FROM lists WHERE meeting_id = :mid AND is_deleted = false"),
|
("lists", "SELECT count(*) FROM lists WHERE meeting_id = :mid AND is_deleted = false"),
|
||||||
|
("decisions", "SELECT count(*) FROM decisions WHERE meeting_id = :mid AND is_deleted = false"),
|
||||||
("contacts", "SELECT count(*) FROM contacts c JOIN contact_meetings cm ON cm.contact_id = c.id WHERE cm.meeting_id = :mid AND c.is_deleted = false"),
|
("contacts", "SELECT count(*) FROM contacts c JOIN contact_meetings cm ON cm.contact_id = c.id WHERE cm.meeting_id = :mid AND c.is_deleted = false"),
|
||||||
]:
|
]:
|
||||||
result = await db.execute(text(count_sql), {"mid": meeting_id})
|
result = await db.execute(text(count_sql), {"mid": meeting_id})
|
||||||
@@ -217,8 +244,10 @@ async def meeting_detail(
|
|||||||
return templates.TemplateResponse("meeting_detail.html", {
|
return templates.TemplateResponse("meeting_detail.html", {
|
||||||
"request": request, "sidebar": sidebar, "item": item,
|
"request": request, "sidebar": sidebar, "item": item,
|
||||||
"action_items": action_items, "decisions": decisions,
|
"action_items": action_items, "decisions": decisions,
|
||||||
"domains": domains, "tab": tab, "tab_data": tab_data,
|
"domains": domains, "projects": projects,
|
||||||
"all_contacts": all_contacts, "counts": counts,
|
"tab": tab, "tab_data": tab_data,
|
||||||
|
"all_contacts": all_contacts, "all_decisions": all_decisions,
|
||||||
|
"counts": counts,
|
||||||
"page_title": item["title"], "active_nav": "meetings",
|
"page_title": item["title"], "active_nav": "meetings",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -325,6 +354,31 @@ async def create_action_item(
|
|||||||
return RedirectResponse(url=f"/meetings/{meeting_id}", status_code=303)
|
return RedirectResponse(url=f"/meetings/{meeting_id}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
# ---- Decision linking ----
|
||||||
|
|
||||||
|
@router.post("/{meeting_id}/decisions/add")
|
||||||
|
async def add_decision(
|
||||||
|
meeting_id: str,
|
||||||
|
decision_id: str = Form(...),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
await db.execute(text("""
|
||||||
|
UPDATE decisions SET meeting_id = :mid WHERE id = :did
|
||||||
|
"""), {"mid": meeting_id, "did": decision_id})
|
||||||
|
return RedirectResponse(url=f"/meetings/{meeting_id}?tab=decisions", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{meeting_id}/decisions/{decision_id}/remove")
|
||||||
|
async def remove_decision(
|
||||||
|
meeting_id: str, decision_id: str,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
await db.execute(text("""
|
||||||
|
UPDATE decisions SET meeting_id = NULL WHERE id = :did AND meeting_id = :mid
|
||||||
|
"""), {"did": decision_id, "mid": meeting_id})
|
||||||
|
return RedirectResponse(url=f"/meetings/{meeting_id}?tab=decisions", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
# ---- Contact linking ----
|
# ---- Contact linking ----
|
||||||
|
|
||||||
@router.post("/{meeting_id}/contacts/add")
|
@router.post("/{meeting_id}/contacts/add")
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ async def project_detail(
|
|||||||
links = []
|
links = []
|
||||||
tab_data = []
|
tab_data = []
|
||||||
all_contacts = []
|
all_contacts = []
|
||||||
|
all_meetings = []
|
||||||
|
|
||||||
if tab == "notes":
|
if tab == "notes":
|
||||||
result = await db.execute(text("""
|
result = await db.execute(text("""
|
||||||
@@ -235,6 +236,21 @@ async def project_detail(
|
|||||||
"""), {"pid": project_id})
|
"""), {"pid": project_id})
|
||||||
tab_data = [dict(r._mapping) for r in result]
|
tab_data = [dict(r._mapping) for r in result]
|
||||||
|
|
||||||
|
elif tab == "meetings":
|
||||||
|
result = await db.execute(text("""
|
||||||
|
SELECT m.*, pm.created_at as linked_at
|
||||||
|
FROM meetings m
|
||||||
|
JOIN project_meetings pm ON pm.meeting_id = m.id
|
||||||
|
WHERE pm.project_id = :pid AND m.is_deleted = false
|
||||||
|
ORDER BY m.meeting_date DESC, m.start_at DESC NULLS LAST
|
||||||
|
"""), {"pid": project_id})
|
||||||
|
tab_data = [dict(r._mapping) for r in result]
|
||||||
|
result = await db.execute(text("""
|
||||||
|
SELECT id, title, meeting_date FROM meetings
|
||||||
|
WHERE is_deleted = false ORDER BY meeting_date DESC LIMIT 50
|
||||||
|
"""))
|
||||||
|
all_meetings = [dict(r._mapping) for r in result]
|
||||||
|
|
||||||
elif tab == "contacts":
|
elif tab == "contacts":
|
||||||
result = await db.execute(text("""
|
result = await db.execute(text("""
|
||||||
SELECT c.*, cp.role, cp.created_at as linked_at
|
SELECT c.*, cp.role, cp.created_at as linked_at
|
||||||
@@ -258,6 +274,7 @@ async def project_detail(
|
|||||||
("files", "SELECT count(*) FROM files f JOIN file_mappings fm ON fm.file_id = f.id WHERE fm.context_type = 'project' AND fm.context_id = :pid AND f.is_deleted = false"),
|
("files", "SELECT count(*) FROM files f JOIN file_mappings fm ON fm.file_id = f.id WHERE fm.context_type = 'project' AND fm.context_id = :pid AND f.is_deleted = false"),
|
||||||
("lists", "SELECT count(*) FROM lists WHERE project_id = :pid AND is_deleted = false"),
|
("lists", "SELECT count(*) FROM lists WHERE project_id = :pid AND is_deleted = false"),
|
||||||
("decisions", "SELECT count(*) FROM decisions d JOIN decision_projects dp ON dp.decision_id = d.id WHERE dp.project_id = :pid AND d.is_deleted = false"),
|
("decisions", "SELECT count(*) FROM decisions d JOIN decision_projects dp ON dp.decision_id = d.id WHERE dp.project_id = :pid AND d.is_deleted = false"),
|
||||||
|
("meetings", "SELECT count(*) FROM meetings m JOIN project_meetings pm ON pm.meeting_id = m.id WHERE pm.project_id = :pid AND m.is_deleted = false"),
|
||||||
("contacts", "SELECT count(*) FROM contacts c JOIN contact_projects cp ON cp.contact_id = c.id WHERE cp.project_id = :pid AND c.is_deleted = false"),
|
("contacts", "SELECT count(*) FROM contacts c JOIN contact_projects cp ON cp.contact_id = c.id WHERE cp.project_id = :pid AND c.is_deleted = false"),
|
||||||
]:
|
]:
|
||||||
result = await db.execute(text(count_sql), {"pid": project_id})
|
result = await db.execute(text(count_sql), {"pid": project_id})
|
||||||
@@ -267,7 +284,8 @@ async def project_detail(
|
|||||||
"request": request, "sidebar": sidebar, "item": item,
|
"request": request, "sidebar": sidebar, "item": item,
|
||||||
"domain": domain, "area": area,
|
"domain": domain, "area": area,
|
||||||
"tasks": tasks, "notes": notes, "links": links,
|
"tasks": tasks, "notes": notes, "links": links,
|
||||||
"tab_data": tab_data, "all_contacts": all_contacts, "counts": counts,
|
"tab_data": tab_data, "all_contacts": all_contacts,
|
||||||
|
"all_meetings": all_meetings, "counts": counts,
|
||||||
"progress": progress, "task_count": total, "done_count": done,
|
"progress": progress, "task_count": total, "done_count": done,
|
||||||
"tab": tab,
|
"tab": tab,
|
||||||
"page_title": item["name"], "active_nav": "projects",
|
"page_title": item["name"], "active_nav": "projects",
|
||||||
@@ -332,6 +350,32 @@ async def delete_project(project_id: str, db: AsyncSession = Depends(get_db)):
|
|||||||
return RedirectResponse(url="/projects", status_code=303)
|
return RedirectResponse(url="/projects", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
# ---- Meeting linking ----
|
||||||
|
|
||||||
|
@router.post("/{project_id}/meetings/add")
|
||||||
|
async def add_meeting(
|
||||||
|
project_id: str,
|
||||||
|
meeting_id: str = Form(...),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
await db.execute(text("""
|
||||||
|
INSERT INTO project_meetings (project_id, meeting_id)
|
||||||
|
VALUES (:pid, :mid) ON CONFLICT DO NOTHING
|
||||||
|
"""), {"pid": project_id, "mid": meeting_id})
|
||||||
|
return RedirectResponse(url=f"/projects/{project_id}?tab=meetings", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{project_id}/meetings/{meeting_id}/remove")
|
||||||
|
async def remove_meeting(
|
||||||
|
project_id: str, meeting_id: str,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
await db.execute(text(
|
||||||
|
"DELETE FROM project_meetings WHERE project_id = :pid AND meeting_id = :mid"
|
||||||
|
), {"pid": project_id, "mid": meeting_id})
|
||||||
|
return RedirectResponse(url=f"/projects/{project_id}?tab=meetings", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
# ---- Contact linking ----
|
# ---- Contact linking ----
|
||||||
|
|
||||||
@router.post("/{project_id}/contacts/add")
|
@router.post("/{project_id}/contacts/add")
|
||||||
|
|||||||
@@ -88,13 +88,6 @@ SEARCH_ENTITIES = [
|
|||||||
"domain_col": "NULL", "project_col": "NULL",
|
"domain_col": "NULL", "project_col": "NULL",
|
||||||
"url": "/decisions/{id}", "icon": "decision",
|
"url": "/decisions/{id}", "icon": "decision",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"type": "weblinks", "label": "Weblinks", "table": "weblinks", "alias": "w",
|
|
||||||
"name_col": "w.label", "status_col": "NULL",
|
|
||||||
"joins": "",
|
|
||||||
"domain_col": "NULL", "project_col": "NULL",
|
|
||||||
"url": "/weblinks", "icon": "weblink",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "processes", "label": "Processes", "table": "processes", "alias": "p",
|
"type": "processes", "label": "Processes", "table": "processes", "alias": "p",
|
||||||
"name_col": "p.name", "status_col": "p.status",
|
"name_col": "p.name", "status_col": "p.status",
|
||||||
|
|||||||
@@ -238,9 +238,9 @@ async def task_detail(
|
|||||||
"""), {"tid": task_id})
|
"""), {"tid": task_id})
|
||||||
tab_data = [dict(r._mapping) for r in result]
|
tab_data = [dict(r._mapping) for r in result]
|
||||||
|
|
||||||
elif tab == "weblinks":
|
elif tab == "links":
|
||||||
result = await db.execute(text("""
|
result = await db.execute(text("""
|
||||||
SELECT * FROM weblinks WHERE task_id = :tid AND is_deleted = false
|
SELECT * FROM links WHERE task_id = :tid AND is_deleted = false
|
||||||
ORDER BY sort_order, label
|
ORDER BY sort_order, label
|
||||||
"""), {"tid": task_id})
|
"""), {"tid": task_id})
|
||||||
tab_data = [dict(r._mapping) for r in result]
|
tab_data = [dict(r._mapping) for r in result]
|
||||||
@@ -291,7 +291,7 @@ async def task_detail(
|
|||||||
counts = {}
|
counts = {}
|
||||||
for count_tab, count_sql in [
|
for count_tab, count_sql in [
|
||||||
("notes", "SELECT count(*) FROM notes WHERE task_id = :tid AND is_deleted = false"),
|
("notes", "SELECT count(*) FROM notes WHERE task_id = :tid AND is_deleted = false"),
|
||||||
("weblinks", "SELECT count(*) FROM weblinks WHERE task_id = :tid AND is_deleted = false"),
|
("links", "SELECT count(*) FROM links WHERE task_id = :tid AND is_deleted = false"),
|
||||||
("files", "SELECT count(*) FROM files f JOIN file_mappings fm ON fm.file_id = f.id WHERE fm.context_type = 'task' AND fm.context_id = :tid AND f.is_deleted = false"),
|
("files", "SELECT count(*) FROM files f JOIN file_mappings fm ON fm.file_id = f.id WHERE fm.context_type = 'task' AND fm.context_id = :tid AND f.is_deleted = false"),
|
||||||
("lists", "SELECT count(*) FROM lists WHERE task_id = :tid AND is_deleted = false"),
|
("lists", "SELECT count(*) FROM lists WHERE task_id = :tid AND is_deleted = false"),
|
||||||
("decisions", "SELECT count(*) FROM decisions WHERE task_id = :tid AND is_deleted = false"),
|
("decisions", "SELECT count(*) FROM decisions WHERE task_id = :tid AND is_deleted = false"),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Weblinks: organized bookmark directory with recursive folders."""
|
"""Bookmarks: organized folder directory for links."""
|
||||||
|
|
||||||
from fastapi import APIRouter, Request, Form, Depends
|
from fastapi import APIRouter, Request, Form, Depends
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
@@ -11,12 +11,25 @@ from core.database import get_db
|
|||||||
from core.base_repository import BaseRepository
|
from core.base_repository import BaseRepository
|
||||||
from core.sidebar import get_sidebar_data
|
from core.sidebar import get_sidebar_data
|
||||||
|
|
||||||
router = APIRouter(prefix="/weblinks", tags=["weblinks"])
|
router = APIRouter(prefix="/weblinks", tags=["bookmarks"])
|
||||||
templates = Jinja2Templates(directory="templates")
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_default_folder_id(db: AsyncSession) -> str:
|
||||||
|
"""Return the Default folder id, creating it if it doesn't exist."""
|
||||||
|
result = await db.execute(text(
|
||||||
|
"SELECT id FROM link_folders WHERE name = 'Default' AND is_deleted = false ORDER BY created_at LIMIT 1"
|
||||||
|
))
|
||||||
|
row = result.first()
|
||||||
|
if row:
|
||||||
|
return str(row[0])
|
||||||
|
repo = BaseRepository("link_folders", db)
|
||||||
|
folder = await repo.create({"name": "Default", "sort_order": 0})
|
||||||
|
return str(folder["id"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
@router.get("/")
|
||||||
async def list_weblinks(
|
async def list_bookmarks(
|
||||||
request: Request,
|
request: Request,
|
||||||
folder_id: Optional[str] = None,
|
folder_id: Optional[str] = None,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
@@ -25,10 +38,10 @@ async def list_weblinks(
|
|||||||
|
|
||||||
# Get all folders for tree nav
|
# Get all folders for tree nav
|
||||||
result = await db.execute(text("""
|
result = await db.execute(text("""
|
||||||
SELECT wf.*, (SELECT count(*) FROM folder_weblinks fw WHERE fw.folder_id = wf.id) as link_count
|
SELECT lf.*, (SELECT count(*) FROM folder_links fl WHERE fl.folder_id = lf.id) as link_count
|
||||||
FROM weblink_folders wf
|
FROM link_folders lf
|
||||||
WHERE wf.is_deleted = false
|
WHERE lf.is_deleted = false
|
||||||
ORDER BY wf.sort_order, wf.name
|
ORDER BY lf.sort_order, lf.name
|
||||||
"""))
|
"""))
|
||||||
all_folders = [dict(r._mapping) for r in result]
|
all_folders = [dict(r._mapping) for r in result]
|
||||||
|
|
||||||
@@ -48,20 +61,20 @@ async def list_weblinks(
|
|||||||
current_folder = f
|
current_folder = f
|
||||||
break
|
break
|
||||||
|
|
||||||
# Get weblinks (filtered by folder or all unfiled)
|
# Get links (filtered by folder or all unfiled)
|
||||||
if folder_id:
|
if folder_id:
|
||||||
result = await db.execute(text("""
|
result = await db.execute(text("""
|
||||||
SELECT w.* FROM weblinks w
|
SELECT l.* FROM links l
|
||||||
JOIN folder_weblinks fw ON fw.weblink_id = w.id
|
JOIN folder_links fl ON fl.link_id = l.id
|
||||||
WHERE fw.folder_id = :fid AND w.is_deleted = false
|
WHERE fl.folder_id = :fid AND l.is_deleted = false
|
||||||
ORDER BY fw.sort_order, w.label
|
ORDER BY fl.sort_order, l.label
|
||||||
"""), {"fid": folder_id})
|
"""), {"fid": folder_id})
|
||||||
else:
|
else:
|
||||||
# Show all weblinks
|
# Show all links
|
||||||
result = await db.execute(text("""
|
result = await db.execute(text("""
|
||||||
SELECT w.* FROM weblinks w
|
SELECT l.* FROM links l
|
||||||
WHERE w.is_deleted = false
|
WHERE l.is_deleted = false
|
||||||
ORDER BY w.sort_order, w.label
|
ORDER BY l.sort_order, l.label
|
||||||
"""))
|
"""))
|
||||||
items = [dict(r._mapping) for r in result]
|
items = [dict(r._mapping) for r in result]
|
||||||
|
|
||||||
@@ -70,7 +83,7 @@ async def list_weblinks(
|
|||||||
"top_folders": top_folders, "child_folder_map": child_folder_map,
|
"top_folders": top_folders, "child_folder_map": child_folder_map,
|
||||||
"current_folder": current_folder,
|
"current_folder": current_folder,
|
||||||
"current_folder_id": folder_id or "",
|
"current_folder_id": folder_id or "",
|
||||||
"page_title": "Weblinks", "active_nav": "weblinks",
|
"page_title": "Bookmarks", "active_nav": "bookmarks",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -84,14 +97,14 @@ async def create_form(
|
|||||||
):
|
):
|
||||||
sidebar = await get_sidebar_data(db)
|
sidebar = await get_sidebar_data(db)
|
||||||
result = await db.execute(text(
|
result = await db.execute(text(
|
||||||
"SELECT id, name FROM weblink_folders WHERE is_deleted = false ORDER BY name"
|
"SELECT id, name FROM link_folders WHERE is_deleted = false ORDER BY name"
|
||||||
))
|
))
|
||||||
folders = [dict(r._mapping) for r in result]
|
folders = [dict(r._mapping) for r in result]
|
||||||
|
|
||||||
return templates.TemplateResponse("weblink_form.html", {
|
return templates.TemplateResponse("weblink_form.html", {
|
||||||
"request": request, "sidebar": sidebar,
|
"request": request, "sidebar": sidebar,
|
||||||
"folders": folders,
|
"folders": folders,
|
||||||
"page_title": "New Weblink", "active_nav": "weblinks",
|
"page_title": "New Link", "active_nav": "bookmarks",
|
||||||
"item": None,
|
"item": None,
|
||||||
"prefill_folder_id": folder_id or "",
|
"prefill_folder_id": folder_id or "",
|
||||||
"prefill_task_id": task_id or "",
|
"prefill_task_id": task_id or "",
|
||||||
@@ -100,7 +113,7 @@ async def create_form(
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/create")
|
@router.post("/create")
|
||||||
async def create_weblink(
|
async def create_link(
|
||||||
request: Request,
|
request: Request,
|
||||||
label: str = Form(...),
|
label: str = Form(...),
|
||||||
url: str = Form(...),
|
url: str = Form(...),
|
||||||
@@ -111,7 +124,7 @@ async def create_weblink(
|
|||||||
tags: Optional[str] = Form(None),
|
tags: Optional[str] = Form(None),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
repo = BaseRepository("weblinks", db)
|
repo = BaseRepository("links", db)
|
||||||
data = {"label": label, "url": url, "description": description}
|
data = {"label": label, "url": url, "description": description}
|
||||||
if task_id and task_id.strip():
|
if task_id and task_id.strip():
|
||||||
data["task_id"] = task_id
|
data["task_id"] = task_id
|
||||||
@@ -120,55 +133,55 @@ async def create_weblink(
|
|||||||
if tags and tags.strip():
|
if tags and tags.strip():
|
||||||
data["tags"] = [t.strip() for t in tags.split(",") if t.strip()]
|
data["tags"] = [t.strip() for t in tags.split(",") if t.strip()]
|
||||||
|
|
||||||
weblink = await repo.create(data)
|
link = await repo.create(data)
|
||||||
|
|
||||||
# Add to folder if specified
|
# Add to folder (default if none specified)
|
||||||
if folder_id and folder_id.strip():
|
effective_folder = folder_id if folder_id and folder_id.strip() else await get_default_folder_id(db)
|
||||||
await db.execute(text("""
|
await db.execute(text("""
|
||||||
INSERT INTO folder_weblinks (folder_id, weblink_id)
|
INSERT INTO folder_links (folder_id, link_id)
|
||||||
VALUES (:fid, :wid) ON CONFLICT DO NOTHING
|
VALUES (:fid, :lid) ON CONFLICT DO NOTHING
|
||||||
"""), {"fid": folder_id, "wid": weblink["id"]})
|
"""), {"fid": effective_folder, "lid": link["id"]})
|
||||||
|
|
||||||
if task_id and task_id.strip():
|
if task_id and task_id.strip():
|
||||||
return RedirectResponse(url=f"/tasks/{task_id}?tab=weblinks", status_code=303)
|
return RedirectResponse(url=f"/tasks/{task_id}?tab=links", status_code=303)
|
||||||
if meeting_id and meeting_id.strip():
|
if meeting_id and meeting_id.strip():
|
||||||
return RedirectResponse(url=f"/meetings/{meeting_id}?tab=weblinks", status_code=303)
|
return RedirectResponse(url=f"/meetings/{meeting_id}?tab=links", status_code=303)
|
||||||
redirect_url = f"/weblinks?folder_id={folder_id}" if folder_id and folder_id.strip() else "/weblinks"
|
redirect_url = f"/weblinks?folder_id={folder_id}" if folder_id and folder_id.strip() else "/weblinks"
|
||||||
return RedirectResponse(url=redirect_url, status_code=303)
|
return RedirectResponse(url=redirect_url, status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{weblink_id}/edit")
|
@router.get("/{link_id}/edit")
|
||||||
async def edit_form(weblink_id: str, request: Request, db: AsyncSession = Depends(get_db)):
|
async def edit_form(link_id: str, request: Request, db: AsyncSession = Depends(get_db)):
|
||||||
repo = BaseRepository("weblinks", db)
|
repo = BaseRepository("links", db)
|
||||||
sidebar = await get_sidebar_data(db)
|
sidebar = await get_sidebar_data(db)
|
||||||
item = await repo.get(weblink_id)
|
item = await repo.get(link_id)
|
||||||
if not item:
|
if not item:
|
||||||
return RedirectResponse(url="/weblinks", status_code=303)
|
return RedirectResponse(url="/weblinks", status_code=303)
|
||||||
|
|
||||||
result = await db.execute(text(
|
result = await db.execute(text(
|
||||||
"SELECT id, name FROM weblink_folders WHERE is_deleted = false ORDER BY name"
|
"SELECT id, name FROM link_folders WHERE is_deleted = false ORDER BY name"
|
||||||
))
|
))
|
||||||
folders = [dict(r._mapping) for r in result]
|
folders = [dict(r._mapping) for r in result]
|
||||||
|
|
||||||
# Current folder assignment
|
# Current folder assignment
|
||||||
result = await db.execute(text(
|
result = await db.execute(text(
|
||||||
"SELECT folder_id FROM folder_weblinks WHERE weblink_id = :wid LIMIT 1"
|
"SELECT folder_id FROM folder_links WHERE link_id = :lid LIMIT 1"
|
||||||
), {"wid": weblink_id})
|
), {"lid": link_id})
|
||||||
row = result.first()
|
row = result.first()
|
||||||
current_folder_id = str(row[0]) if row else ""
|
current_folder_id = str(row[0]) if row else ""
|
||||||
|
|
||||||
return templates.TemplateResponse("weblink_form.html", {
|
return templates.TemplateResponse("weblink_form.html", {
|
||||||
"request": request, "sidebar": sidebar,
|
"request": request, "sidebar": sidebar,
|
||||||
"folders": folders,
|
"folders": folders,
|
||||||
"page_title": "Edit Weblink", "active_nav": "weblinks",
|
"page_title": "Edit Link", "active_nav": "bookmarks",
|
||||||
"item": item,
|
"item": item,
|
||||||
"prefill_folder_id": current_folder_id,
|
"prefill_folder_id": current_folder_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{weblink_id}/edit")
|
@router.post("/{link_id}/edit")
|
||||||
async def update_weblink(
|
async def update_link(
|
||||||
weblink_id: str,
|
link_id: str,
|
||||||
label: str = Form(...),
|
label: str = Form(...),
|
||||||
url: str = Form(...),
|
url: str = Form(...),
|
||||||
description: Optional[str] = Form(None),
|
description: Optional[str] = Form(None),
|
||||||
@@ -176,7 +189,7 @@ async def update_weblink(
|
|||||||
tags: Optional[str] = Form(None),
|
tags: Optional[str] = Form(None),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
repo = BaseRepository("weblinks", db)
|
repo = BaseRepository("links", db)
|
||||||
data = {
|
data = {
|
||||||
"label": label, "url": url,
|
"label": label, "url": url,
|
||||||
"description": description if description and description.strip() else None,
|
"description": description if description and description.strip() else None,
|
||||||
@@ -186,23 +199,23 @@ async def update_weblink(
|
|||||||
else:
|
else:
|
||||||
data["tags"] = None
|
data["tags"] = None
|
||||||
|
|
||||||
await repo.update(weblink_id, data)
|
await repo.update(link_id, data)
|
||||||
|
|
||||||
# Update folder assignment
|
# Update folder assignment
|
||||||
await db.execute(text("DELETE FROM folder_weblinks WHERE weblink_id = :wid"), {"wid": weblink_id})
|
await db.execute(text("DELETE FROM folder_links WHERE link_id = :lid"), {"lid": link_id})
|
||||||
if folder_id and folder_id.strip():
|
if folder_id and folder_id.strip():
|
||||||
await db.execute(text("""
|
await db.execute(text("""
|
||||||
INSERT INTO folder_weblinks (folder_id, weblink_id)
|
INSERT INTO folder_links (folder_id, link_id)
|
||||||
VALUES (:fid, :wid) ON CONFLICT DO NOTHING
|
VALUES (:fid, :lid) ON CONFLICT DO NOTHING
|
||||||
"""), {"fid": folder_id, "wid": weblink_id})
|
"""), {"fid": folder_id, "lid": link_id})
|
||||||
|
|
||||||
return RedirectResponse(url="/weblinks", status_code=303)
|
return RedirectResponse(url="/weblinks", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{weblink_id}/delete")
|
@router.post("/{link_id}/delete")
|
||||||
async def delete_weblink(weblink_id: str, request: Request, db: AsyncSession = Depends(get_db)):
|
async def delete_link(link_id: str, request: Request, db: AsyncSession = Depends(get_db)):
|
||||||
repo = BaseRepository("weblinks", db)
|
repo = BaseRepository("links", db)
|
||||||
await repo.soft_delete(weblink_id)
|
await repo.soft_delete(link_id)
|
||||||
referer = request.headers.get("referer", "/weblinks")
|
referer = request.headers.get("referer", "/weblinks")
|
||||||
return RedirectResponse(url=referer, status_code=303)
|
return RedirectResponse(url=referer, status_code=303)
|
||||||
|
|
||||||
@@ -213,14 +226,14 @@ async def delete_weblink(weblink_id: str, request: Request, db: AsyncSession = D
|
|||||||
async def create_folder_form(request: Request, db: AsyncSession = Depends(get_db)):
|
async def create_folder_form(request: Request, db: AsyncSession = Depends(get_db)):
|
||||||
sidebar = await get_sidebar_data(db)
|
sidebar = await get_sidebar_data(db)
|
||||||
result = await db.execute(text(
|
result = await db.execute(text(
|
||||||
"SELECT id, name FROM weblink_folders WHERE is_deleted = false ORDER BY name"
|
"SELECT id, name FROM link_folders WHERE is_deleted = false ORDER BY name"
|
||||||
))
|
))
|
||||||
parent_folders = [dict(r._mapping) for r in result]
|
parent_folders = [dict(r._mapping) for r in result]
|
||||||
|
|
||||||
return templates.TemplateResponse("weblink_folder_form.html", {
|
return templates.TemplateResponse("weblink_folder_form.html", {
|
||||||
"request": request, "sidebar": sidebar,
|
"request": request, "sidebar": sidebar,
|
||||||
"parent_folders": parent_folders,
|
"parent_folders": parent_folders,
|
||||||
"page_title": "New Folder", "active_nav": "weblinks",
|
"page_title": "New Folder", "active_nav": "bookmarks",
|
||||||
"item": None,
|
"item": None,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -232,7 +245,7 @@ async def create_folder(
|
|||||||
parent_id: Optional[str] = Form(None),
|
parent_id: Optional[str] = Form(None),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
repo = BaseRepository("weblink_folders", db)
|
repo = BaseRepository("link_folders", db)
|
||||||
data = {"name": name}
|
data = {"name": name}
|
||||||
if parent_id and parent_id.strip():
|
if parent_id and parent_id.strip():
|
||||||
data["parent_id"] = parent_id
|
data["parent_id"] = parent_id
|
||||||
@@ -242,6 +255,13 @@ async def create_folder(
|
|||||||
|
|
||||||
@router.post("/folders/{folder_id}/delete")
|
@router.post("/folders/{folder_id}/delete")
|
||||||
async def delete_folder(folder_id: str, request: Request, db: AsyncSession = Depends(get_db)):
|
async def delete_folder(folder_id: str, request: Request, db: AsyncSession = Depends(get_db)):
|
||||||
repo = BaseRepository("weblink_folders", db)
|
# Prevent deleting the Default folder
|
||||||
|
result = await db.execute(text(
|
||||||
|
"SELECT name FROM link_folders WHERE id = :id"
|
||||||
|
), {"id": folder_id})
|
||||||
|
row = result.first()
|
||||||
|
if row and row[0] == "Default":
|
||||||
|
return RedirectResponse(url="/weblinks", status_code=303)
|
||||||
|
repo = BaseRepository("link_folders", db)
|
||||||
await repo.soft_delete(folder_id)
|
await repo.soft_delete(folder_id)
|
||||||
return RedirectResponse(url="/weblinks", status_code=303)
|
return RedirectResponse(url="/weblinks", status_code=303)
|
||||||
|
|||||||
@@ -1092,6 +1092,22 @@ a:hover { color: var(--accent-hover); }
|
|||||||
.weblink-folder-item.active { background: var(--accent-soft); color: var(--accent); font-weight: 500; }
|
.weblink-folder-item.active { background: var(--accent-soft); color: var(--accent); font-weight: 500; }
|
||||||
.weblinks-content { flex: 1; min-width: 0; }
|
.weblinks-content { flex: 1; min-width: 0; }
|
||||||
|
|
||||||
|
/* ---- Project Progress Mini Bar (dashboard) ---- */
|
||||||
|
.project-progress-mini {
|
||||||
|
width: 60px;
|
||||||
|
height: 6px;
|
||||||
|
background: var(--surface2);
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.project-progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--green);
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- Scrollbar ---- */
|
/* ---- Scrollbar ---- */
|
||||||
::-webkit-scrollbar { width: 6px; }
|
::-webkit-scrollbar { width: 6px; }
|
||||||
::-webkit-scrollbar-track { background: transparent; }
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
|||||||
@@ -87,9 +87,9 @@
|
|||||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>
|
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>
|
||||||
Files
|
Files
|
||||||
</a>
|
</a>
|
||||||
<a href="/weblinks" class="nav-item {{ 'active' if active_nav == 'weblinks' }}">
|
<a href="/weblinks" class="nav-item {{ 'active' if active_nav == 'bookmarks' }}">
|
||||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
||||||
Weblinks
|
Bookmarks
|
||||||
</a>
|
</a>
|
||||||
<a href="/time" class="nav-item {{ 'active' if active_nav == 'time' }}">
|
<a href="/time" class="nav-item {{ 'active' if active_nav == 'time' }}">
|
||||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||||
@@ -234,7 +234,7 @@
|
|||||||
<a href="/decisions" class="mob-more__item"><span>Decisions</span></a>
|
<a href="/decisions" class="mob-more__item"><span>Decisions</span></a>
|
||||||
<a href="/contacts" class="mob-more__item"><span>Contacts</span></a>
|
<a href="/contacts" class="mob-more__item"><span>Contacts</span></a>
|
||||||
<a href="/processes" class="mob-more__item"><span>Processes</span></a>
|
<a href="/processes" class="mob-more__item"><span>Processes</span></a>
|
||||||
<a href="/weblinks" class="mob-more__item"><span>Weblinks</span></a>
|
<a href="/weblinks" class="mob-more__item"><span>Bookmarks</span></a>
|
||||||
<a href="/admin" class="mob-more__item"><span>Admin</span></a>
|
<a href="/admin" class="mob-more__item"><span>Admin</span></a>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -75,8 +75,8 @@
|
|||||||
{% if item.converted_to_id %}
|
{% if item.converted_to_id %}
|
||||||
{% if item.converted_to_type == 'list_item' and item.list_id %}
|
{% if item.converted_to_type == 'list_item' and item.list_id %}
|
||||||
<a href="/lists/{{ item.list_id }}" class="btn btn-ghost btn-xs">View →</a>
|
<a href="/lists/{{ item.list_id }}" class="btn btn-ghost btn-xs">View →</a>
|
||||||
{% elif item.converted_to_type == 'weblink' %}
|
{% elif item.converted_to_type == 'link' %}
|
||||||
<a href="/weblinks" class="btn btn-ghost btn-xs">View →</a>
|
<a href="/links" class="btn btn-ghost btn-xs">View →</a>
|
||||||
{% elif item.converted_to_type in ('task', 'note', 'project', 'contact', 'decision') %}
|
{% elif item.converted_to_type in ('task', 'note', 'project', 'contact', 'decision') %}
|
||||||
<a href="/{{ item.converted_to_type }}s/{{ item.converted_to_id }}" class="btn btn-ghost btn-xs">View →</a>
|
<a href="/{{ item.converted_to_type }}s/{{ item.converted_to_id }}" class="btn btn-ghost btn-xs">View →</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
<input type="text" name="title" class="form-input" value="{{ item.raw_text }}" required>
|
<input type="text" name="title" class="form-input" value="{{ item.raw_text }}" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% elif convert_type == 'weblink' %}
|
{% elif convert_type == 'link' %}
|
||||||
<div class="form-group full-width">
|
<div class="form-group full-width">
|
||||||
<label class="form-label">Label</label>
|
<label class="form-label">Label</label>
|
||||||
<input type="text" name="label" class="form-input" value="{{ prefill_label }}" required>
|
<input type="text" name="label" class="form-input" value="{{ prefill_label }}" required>
|
||||||
|
|||||||
@@ -91,4 +91,50 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Project Deadlines -->
|
||||||
|
{% if overdue_projects or upcoming_projects %}
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="card-title">Project Deadlines</h2>
|
||||||
|
<a href="/projects/" class="btn btn-ghost btn-sm">All Projects</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if overdue_projects %}
|
||||||
|
<div class="text-xs text-muted mb-2" style="font-weight:600; color: var(--red);">OVERDUE</div>
|
||||||
|
{% for p in overdue_projects %}
|
||||||
|
<div class="list-row">
|
||||||
|
<span class="priority-dot priority-{{ p.priority }}"></span>
|
||||||
|
<span class="row-title"><a href="/projects/{{ p.id }}">{{ p.name }}</a></span>
|
||||||
|
{% if p.domain_name %}
|
||||||
|
<span class="row-meta">{{ p.domain_name }}</span>
|
||||||
|
{% endif %}
|
||||||
|
<div class="project-progress-mini">
|
||||||
|
<div class="project-progress-bar" style="width: {{ ((p.done_count / p.task_count * 100) if p.task_count else 0)|int }}%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="row-meta" style="min-width: 32px; text-align: right; font-size: 0.72rem;">{{ p.done_count }}/{{ p.task_count }}</span>
|
||||||
|
<span class="row-meta overdue">{{ p.target_date }}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if upcoming_projects %}
|
||||||
|
<div class="text-xs text-muted mb-2 {{ 'mt-3' if overdue_projects }}" style="font-weight:600;">NEXT 30 DAYS</div>
|
||||||
|
{% for p in upcoming_projects %}
|
||||||
|
<div class="list-row">
|
||||||
|
<span class="priority-dot priority-{{ p.priority }}"></span>
|
||||||
|
<span class="row-title"><a href="/projects/{{ p.id }}">{{ p.name }}</a></span>
|
||||||
|
{% if p.domain_name %}
|
||||||
|
<span class="row-meta">{{ p.domain_name }}</span>
|
||||||
|
{% endif %}
|
||||||
|
<div class="project-progress-mini">
|
||||||
|
<div class="project-progress-bar" style="width: {{ ((p.done_count / p.task_count * 100) if p.task_count else 0)|int }}%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="row-meta" style="min-width: 32px; text-align: right; font-size: 0.72rem;">{{ p.done_count }}/{{ p.task_count }}</span>
|
||||||
|
<span class="row-meta">{{ p.target_date }}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -7,11 +7,14 @@
|
|||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-group"><label class="form-label">Label *</label><input type="text" name="label" class="form-input" required value="{{ item.label if item else '' }}"></div>
|
<div class="form-group"><label class="form-label">Label *</label><input type="text" name="label" class="form-input" required value="{{ item.label if item else '' }}"></div>
|
||||||
<div class="form-group"><label class="form-label">URL *</label><input type="url" name="url" class="form-input" required value="{{ item.url if item else '' }}"></div>
|
<div class="form-group"><label class="form-label">URL *</label><input type="url" name="url" class="form-input" required value="{{ item.url if item else '' }}"></div>
|
||||||
<div class="form-group"><label class="form-label">Domain *</label>
|
<div class="form-group"><label class="form-label">Domain</label>
|
||||||
<select name="domain_id" class="form-select" required>{% for d in domains %}<option value="{{ d.id }}" {{ 'selected' if (item and item.domain_id|string == d.id|string) or (not item and prefill_domain_id == d.id|string) }}>{{ d.name }}</option>{% endfor %}</select></div>
|
<select name="domain_id" class="form-select"><option value="">-- None --</option>{% for d in domains %}<option value="{{ d.id }}" {{ 'selected' if (item and item.domain_id and item.domain_id|string == d.id|string) or (not item and prefill_domain_id == d.id|string) }}>{{ d.name }}</option>{% endfor %}</select></div>
|
||||||
<div class="form-group"><label class="form-label">Project</label>
|
<div class="form-group"><label class="form-label">Project</label>
|
||||||
<select name="project_id" class="form-select"><option value="">-- None --</option>{% for p in projects %}<option value="{{ p.id }}" {{ 'selected' if (item and item.project_id and item.project_id|string == p.id|string) or (not item and prefill_project_id == p.id|string) }}>{{ p.name }}</option>{% endfor %}</select></div>
|
<select name="project_id" class="form-select"><option value="">-- None --</option>{% for p in projects %}<option value="{{ p.id }}" {{ 'selected' if (item and item.project_id and item.project_id|string == p.id|string) or (not item and prefill_project_id == p.id|string) }}>{{ p.name }}</option>{% endfor %}</select></div>
|
||||||
|
<div class="form-group full-width"><label class="form-label">Tags</label><input type="text" name="tags" class="form-input" placeholder="tag1, tag2, ..." value="{{ item.tags|join(', ') if item and item.tags else '' }}"></div>
|
||||||
<div class="form-group full-width"><label class="form-label">Description</label><textarea name="description" class="form-textarea" rows="3">{{ item.description if item and item.description else '' }}</textarea></div>
|
<div class="form-group full-width"><label class="form-label">Description</label><textarea name="description" class="form-textarea" rows="3">{{ item.description if item and item.description else '' }}</textarea></div>
|
||||||
|
{% if prefill_task_id is defined and prefill_task_id %}<input type="hidden" name="task_id" value="{{ prefill_task_id }}">{% endif %}
|
||||||
|
{% if prefill_meeting_id is defined and prefill_meeting_id %}<input type="hidden" name="meeting_id" value="{{ prefill_meeting_id }}">{% endif %}
|
||||||
<div class="form-actions"><button type="submit" class="btn btn-primary">{{ 'Save' if item else 'Create' }}</button><a href="/links" class="btn btn-secondary">Cancel</a></div>
|
<div class="form-actions"><button type="submit" class="btn btn-primary">{{ 'Save' if item else 'Create' }}</button><a href="/links" class="btn btn-secondary">Cancel</a></div>
|
||||||
</div>
|
</div>
|
||||||
</form></div>
|
</form></div>
|
||||||
|
|||||||
@@ -23,6 +23,11 @@
|
|||||||
{% if item.start_at and item.end_at %}
|
{% if item.start_at and item.end_at %}
|
||||||
<span class="detail-meta-item">{{ item.start_at.strftime('%H:%M') }} - {{ item.end_at.strftime('%H:%M') }}</span>
|
<span class="detail-meta-item">{{ item.start_at.strftime('%H:%M') }} - {{ item.end_at.strftime('%H:%M') }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if projects %}
|
||||||
|
{% for p in projects %}
|
||||||
|
<span class="detail-meta-item"><a href="/projects/{{ p.id }}" style="color: {{ p.domain_color or 'var(--accent)' }};">{{ p.name }}</a></span>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
{% if item.tags %}
|
{% if item.tags %}
|
||||||
<div class="mt-1">{% for tag in item.tags %}<span class="row-tag">{{ tag }}</span>{% endfor %}</div>
|
<div class="mt-1">{% for tag in item.tags %}<span class="row-tag">{{ tag }}</span>{% endfor %}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -32,9 +37,10 @@
|
|||||||
<div class="tab-strip">
|
<div class="tab-strip">
|
||||||
<a href="/meetings/{{ item.id }}?tab=overview" class="tab-item {{ 'active' if tab == 'overview' }}">Overview</a>
|
<a href="/meetings/{{ item.id }}?tab=overview" class="tab-item {{ 'active' if tab == 'overview' }}">Overview</a>
|
||||||
<a href="/meetings/{{ item.id }}?tab=notes" class="tab-item {{ 'active' if tab == 'notes' }}">Notes{% if counts.notes %} ({{ counts.notes }}){% endif %}</a>
|
<a href="/meetings/{{ item.id }}?tab=notes" class="tab-item {{ 'active' if tab == 'notes' }}">Notes{% if counts.notes %} ({{ counts.notes }}){% endif %}</a>
|
||||||
<a href="/meetings/{{ item.id }}?tab=weblinks" class="tab-item {{ 'active' if tab == 'weblinks' }}">Weblinks{% if counts.weblinks %} ({{ counts.weblinks }}){% endif %}</a>
|
<a href="/meetings/{{ item.id }}?tab=links" class="tab-item {{ 'active' if tab == 'links' }}">Links{% if counts.links %} ({{ counts.links }}){% endif %}</a>
|
||||||
<a href="/meetings/{{ item.id }}?tab=files" class="tab-item {{ 'active' if tab == 'files' }}">Files{% if counts.files %} ({{ counts.files }}){% endif %}</a>
|
<a href="/meetings/{{ item.id }}?tab=files" class="tab-item {{ 'active' if tab == 'files' }}">Files{% if counts.files %} ({{ counts.files }}){% endif %}</a>
|
||||||
<a href="/meetings/{{ item.id }}?tab=lists" class="tab-item {{ 'active' if tab == 'lists' }}">Lists{% if counts.lists %} ({{ counts.lists }}){% endif %}</a>
|
<a href="/meetings/{{ item.id }}?tab=lists" class="tab-item {{ 'active' if tab == 'lists' }}">Lists{% if counts.lists %} ({{ counts.lists }}){% endif %}</a>
|
||||||
|
<a href="/meetings/{{ item.id }}?tab=decisions" class="tab-item {{ 'active' if tab == 'decisions' }}">Decisions{% if counts.decisions %} ({{ counts.decisions }}){% endif %}</a>
|
||||||
<a href="/meetings/{{ item.id }}?tab=processes" class="tab-item {{ 'active' if tab == 'processes' }}">Processes</a>
|
<a href="/meetings/{{ item.id }}?tab=processes" class="tab-item {{ 'active' if tab == 'processes' }}">Processes</a>
|
||||||
<a href="/meetings/{{ item.id }}?tab=contacts" class="tab-item {{ 'active' if tab == 'contacts' }}">Contacts{% if counts.contacts %} ({{ counts.contacts }}){% endif %}</a>
|
<a href="/meetings/{{ item.id }}?tab=contacts" class="tab-item {{ 'active' if tab == 'contacts' }}">Contacts{% if counts.contacts %} ({{ counts.contacts }}){% endif %}</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,15 +128,15 @@
|
|||||||
<div class="empty-state"><div class="empty-state-text">No notes linked to this meeting</div></div>
|
<div class="empty-state"><div class="empty-state-text">No notes linked to this meeting</div></div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% elif tab == 'weblinks' %}
|
{% elif tab == 'links' %}
|
||||||
<a href="/weblinks/create?meeting_id={{ item.id }}" class="btn btn-ghost btn-sm mb-3">+ New Weblink</a>
|
<a href="/links/create?meeting_id={{ item.id }}" class="btn btn-ghost btn-sm mb-3">+ New Link</a>
|
||||||
{% for w in tab_data %}
|
{% for w in tab_data %}
|
||||||
<div class="list-row">
|
<div class="list-row">
|
||||||
<span class="row-title"><a href="{{ w.url }}" target="_blank">{{ w.label }}</a></span>
|
<span class="row-title"><a href="{{ w.url }}" target="_blank">{{ w.label }}</a></span>
|
||||||
<span class="row-meta">{{ w.url[:50] }}{% if w.url|length > 50 %}...{% endif %}</span>
|
<span class="row-meta">{{ w.url[:50] }}{% if w.url|length > 50 %}...{% endif %}</span>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="empty-state"><div class="empty-state-text">No weblinks linked to this meeting</div></div>
|
<div class="empty-state"><div class="empty-state-text">No links linked to this meeting</div></div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% elif tab == 'files' %}
|
{% elif tab == 'files' %}
|
||||||
@@ -155,6 +161,38 @@
|
|||||||
<div class="empty-state"><div class="empty-state-text">No lists linked to this meeting</div></div>
|
<div class="empty-state"><div class="empty-state-text">No lists linked to this meeting</div></div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
{% elif tab == 'decisions' %}
|
||||||
|
<div class="card mb-4">
|
||||||
|
<form action="/meetings/{{ item.id }}/decisions/add" method="post" class="flex gap-2 items-end" style="padding: 12px;">
|
||||||
|
<div class="form-group" style="flex:1; margin:0;">
|
||||||
|
<label class="form-label">Decision</label>
|
||||||
|
<select name="decision_id" class="form-select" required>
|
||||||
|
<option value="">Select decision...</option>
|
||||||
|
{% for d in all_decisions %}
|
||||||
|
<option value="{{ d.id }}">{{ d.title }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm">Link</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<a href="/decisions/create?meeting_id={{ item.id }}" class="btn btn-ghost btn-sm mb-3">+ New Decision</a>
|
||||||
|
{% for d in tab_data %}
|
||||||
|
<div class="list-row">
|
||||||
|
<span class="row-title"><a href="/decisions/{{ d.id }}">{{ d.title }}</a></span>
|
||||||
|
<span class="status-badge status-{{ d.status }}">{{ d.status }}</span>
|
||||||
|
{% if d.impact %}<span class="row-tag">{{ d.impact }}</span>{% endif %}
|
||||||
|
{% if d.decided_at %}<span class="row-meta">{{ d.decided_at.strftime('%Y-%m-%d') if d.decided_at else '' }}</span>{% endif %}
|
||||||
|
<div class="row-actions">
|
||||||
|
<form action="/meetings/{{ item.id }}/decisions/{{ d.id }}/remove" method="post" style="display:inline">
|
||||||
|
<button class="btn btn-ghost btn-xs" title="Unlink">Unlink</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="empty-state"><div class="empty-state-text">No decisions linked to this meeting</div></div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
{% elif tab == 'processes' %}
|
{% elif tab == 'processes' %}
|
||||||
<div class="empty-state"><div class="empty-state-text">Process management coming soon</div></div>
|
<div class="empty-state"><div class="empty-state-text">Process management coming soon</div></div>
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
<a href="/projects/{{ item.id }}?tab=files" class="tab-item {{ 'active' if tab == 'files' }}">Files{% if counts.files %} ({{ counts.files }}){% endif %}</a>
|
<a href="/projects/{{ item.id }}?tab=files" class="tab-item {{ 'active' if tab == 'files' }}">Files{% if counts.files %} ({{ counts.files }}){% endif %}</a>
|
||||||
<a href="/projects/{{ item.id }}?tab=lists" class="tab-item {{ 'active' if tab == 'lists' }}">Lists{% if counts.lists %} ({{ counts.lists }}){% endif %}</a>
|
<a href="/projects/{{ item.id }}?tab=lists" class="tab-item {{ 'active' if tab == 'lists' }}">Lists{% if counts.lists %} ({{ counts.lists }}){% endif %}</a>
|
||||||
<a href="/projects/{{ item.id }}?tab=decisions" class="tab-item {{ 'active' if tab == 'decisions' }}">Decisions{% if counts.decisions %} ({{ counts.decisions }}){% endif %}</a>
|
<a href="/projects/{{ item.id }}?tab=decisions" class="tab-item {{ 'active' if tab == 'decisions' }}">Decisions{% if counts.decisions %} ({{ counts.decisions }}){% endif %}</a>
|
||||||
|
<a href="/projects/{{ item.id }}?tab=meetings" class="tab-item {{ 'active' if tab == 'meetings' }}">Meetings{% if counts.meetings %} ({{ counts.meetings }}){% endif %}</a>
|
||||||
<a href="/projects/{{ item.id }}?tab=processes" class="tab-item {{ 'active' if tab == 'processes' }}">Processes</a>
|
<a href="/projects/{{ item.id }}?tab=processes" class="tab-item {{ 'active' if tab == 'processes' }}">Processes</a>
|
||||||
<a href="/projects/{{ item.id }}?tab=contacts" class="tab-item {{ 'active' if tab == 'contacts' }}">Contacts{% if counts.contacts %} ({{ counts.contacts }}){% endif %}</a>
|
<a href="/projects/{{ item.id }}?tab=contacts" class="tab-item {{ 'active' if tab == 'contacts' }}">Contacts{% if counts.contacts %} ({{ counts.contacts }}){% endif %}</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -130,6 +131,37 @@
|
|||||||
<div class="empty-state"><div class="empty-state-text">No decisions linked to this project</div></div>
|
<div class="empty-state"><div class="empty-state-text">No decisions linked to this project</div></div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
{% elif tab == 'meetings' %}
|
||||||
|
<a href="/meetings/create" class="btn btn-ghost btn-sm mb-3">+ New Meeting</a>
|
||||||
|
<div class="card mb-4">
|
||||||
|
<form action="/projects/{{ item.id }}/meetings/add" method="post" class="flex gap-2 items-end" style="padding: 12px;">
|
||||||
|
<div class="form-group" style="flex:1; margin:0;">
|
||||||
|
<label class="form-label">Meeting</label>
|
||||||
|
<select name="meeting_id" class="form-select" required>
|
||||||
|
<option value="">Select meeting...</option>
|
||||||
|
{% for m in all_meetings %}
|
||||||
|
<option value="{{ m.id }}">{{ m.title }} ({{ m.meeting_date }})</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm">Add</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% for m in tab_data %}
|
||||||
|
<div class="list-row">
|
||||||
|
<span class="row-title"><a href="/meetings/{{ m.id }}">{{ m.title }}</a></span>
|
||||||
|
<span class="row-meta">{{ m.meeting_date }}</span>
|
||||||
|
<span class="status-badge status-{{ m.status }}">{{ m.status }}</span>
|
||||||
|
<div class="row-actions">
|
||||||
|
<form action="/projects/{{ item.id }}/meetings/{{ m.id }}/remove" method="post" style="display:inline">
|
||||||
|
<button class="btn btn-ghost btn-xs" title="Remove">Remove</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="empty-state"><div class="empty-state-text">No meetings linked to this project</div></div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
{% elif tab == 'processes' %}
|
{% elif tab == 'processes' %}
|
||||||
<div class="empty-state"><div class="empty-state-text">Process management coming soon</div></div>
|
<div class="empty-state"><div class="empty-state-text">Process management coming soon</div></div>
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
<div class="tab-strip">
|
<div class="tab-strip">
|
||||||
<a href="/tasks/{{ item.id }}?tab=overview" class="tab-item {{ 'active' if tab == 'overview' }}">Overview{% if counts.overview %} ({{ counts.overview }}){% endif %}</a>
|
<a href="/tasks/{{ item.id }}?tab=overview" class="tab-item {{ 'active' if tab == 'overview' }}">Overview{% if counts.overview %} ({{ counts.overview }}){% endif %}</a>
|
||||||
<a href="/tasks/{{ item.id }}?tab=notes" class="tab-item {{ 'active' if tab == 'notes' }}">Notes{% if counts.notes %} ({{ counts.notes }}){% endif %}</a>
|
<a href="/tasks/{{ item.id }}?tab=notes" class="tab-item {{ 'active' if tab == 'notes' }}">Notes{% if counts.notes %} ({{ counts.notes }}){% endif %}</a>
|
||||||
<a href="/tasks/{{ item.id }}?tab=weblinks" class="tab-item {{ 'active' if tab == 'weblinks' }}">Weblinks{% if counts.weblinks %} ({{ counts.weblinks }}){% endif %}</a>
|
<a href="/tasks/{{ item.id }}?tab=links" class="tab-item {{ 'active' if tab == 'links' }}">Links{% if counts.links %} ({{ counts.links }}){% endif %}</a>
|
||||||
<a href="/tasks/{{ item.id }}?tab=files" class="tab-item {{ 'active' if tab == 'files' }}">Files{% if counts.files %} ({{ counts.files }}){% endif %}</a>
|
<a href="/tasks/{{ item.id }}?tab=files" class="tab-item {{ 'active' if tab == 'files' }}">Files{% if counts.files %} ({{ counts.files }}){% endif %}</a>
|
||||||
<a href="/tasks/{{ item.id }}?tab=lists" class="tab-item {{ 'active' if tab == 'lists' }}">Lists{% if counts.lists %} ({{ counts.lists }}){% endif %}</a>
|
<a href="/tasks/{{ item.id }}?tab=lists" class="tab-item {{ 'active' if tab == 'lists' }}">Lists{% if counts.lists %} ({{ counts.lists }}){% endif %}</a>
|
||||||
<a href="/tasks/{{ item.id }}?tab=decisions" class="tab-item {{ 'active' if tab == 'decisions' }}">Decisions{% if counts.decisions %} ({{ counts.decisions }}){% endif %}</a>
|
<a href="/tasks/{{ item.id }}?tab=decisions" class="tab-item {{ 'active' if tab == 'decisions' }}">Decisions{% if counts.decisions %} ({{ counts.decisions }}){% endif %}</a>
|
||||||
@@ -107,15 +107,15 @@
|
|||||||
<div class="empty-state"><div class="empty-state-text">No notes linked to this task</div></div>
|
<div class="empty-state"><div class="empty-state-text">No notes linked to this task</div></div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% elif tab == 'weblinks' %}
|
{% elif tab == 'links' %}
|
||||||
<a href="/weblinks/create?task_id={{ item.id }}" class="btn btn-ghost btn-sm mb-3">+ New Weblink</a>
|
<a href="/links/create?task_id={{ item.id }}" class="btn btn-ghost btn-sm mb-3">+ New Link</a>
|
||||||
{% for w in tab_data %}
|
{% for w in tab_data %}
|
||||||
<div class="list-row">
|
<div class="list-row">
|
||||||
<span class="row-title"><a href="{{ w.url }}" target="_blank">{{ w.label }}</a></span>
|
<span class="row-title"><a href="{{ w.url }}" target="_blank">{{ w.label }}</a></span>
|
||||||
<span class="row-meta">{{ w.url[:50] }}{% if w.url|length > 50 %}...{% endif %}</span>
|
<span class="row-meta">{{ w.url[:50] }}{% if w.url|length > 50 %}...{% endif %}</span>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="empty-state"><div class="empty-state-text">No weblinks linked to this task</div></div>
|
<div class="empty-state"><div class="empty-state-text">No links linked to this task</div></div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% elif tab == 'files' %}
|
{% elif tab == 'files' %}
|
||||||
|
|||||||
@@ -44,8 +44,8 @@
|
|||||||
{% if prefill_task_id is defined and prefill_task_id %}<input type="hidden" name="task_id" value="{{ prefill_task_id }}">{% endif %}
|
{% if prefill_task_id is defined and prefill_task_id %}<input type="hidden" name="task_id" value="{{ prefill_task_id }}">{% endif %}
|
||||||
{% if prefill_meeting_id is defined and prefill_meeting_id %}<input type="hidden" name="meeting_id" value="{{ prefill_meeting_id }}">{% endif %}
|
{% if prefill_meeting_id is defined and prefill_meeting_id %}<input type="hidden" name="meeting_id" value="{{ prefill_meeting_id }}">{% endif %}
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">{{ 'Save Changes' if item else 'Add Weblink' }}</button>
|
<button type="submit" class="btn btn-primary">{{ 'Save Changes' if item else 'Add Link' }}</button>
|
||||||
<a href="{{ '/tasks/' ~ prefill_task_id ~ '?tab=weblinks' if prefill_task_id is defined and prefill_task_id else '/weblinks' }}" class="btn btn-secondary">Cancel</a>
|
<a href="{{ '/tasks/' ~ prefill_task_id ~ '?tab=links' if prefill_task_id is defined and prefill_task_id else '/weblinks' }}" class="btn btn-secondary">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1 class="page-title">Weblinks<span class="page-count">{{ items|length }}</span></h1>
|
<h1 class="page-title">Bookmarks<span class="page-count">{{ items|length }}</span></h1>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<a href="/weblinks/folders/create" class="btn btn-secondary">+ New Folder</a>
|
<a href="/weblinks/folders/create" class="btn btn-secondary">+ New Folder</a>
|
||||||
<a href="/weblinks/create{{ '?folder_id=' ~ current_folder_id if current_folder_id }}" class="btn btn-primary">+ New Weblink</a>
|
<a href="/weblinks/create{{ '?folder_id=' ~ current_folder_id if current_folder_id }}" class="btn btn-primary">+ New Link</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<!-- Folder sidebar -->
|
<!-- Folder sidebar -->
|
||||||
<div class="weblinks-folders">
|
<div class="weblinks-folders">
|
||||||
<a href="/weblinks" class="weblink-folder-item {{ 'active' if not current_folder_id }}">
|
<a href="/weblinks" class="weblink-folder-item {{ 'active' if not current_folder_id }}">
|
||||||
All Weblinks
|
All Links
|
||||||
</a>
|
</a>
|
||||||
{% for folder in top_folders %}
|
{% for folder in top_folders %}
|
||||||
<a href="/weblinks?folder_id={{ folder.id }}" class="weblink-folder-item {{ 'active' if current_folder_id == folder.id|string }}">
|
<a href="/weblinks?folder_id={{ folder.id }}" class="weblink-folder-item {{ 'active' if current_folder_id == folder.id|string }}">
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Weblinks list -->
|
<!-- Links list -->
|
||||||
<div class="weblinks-content">
|
<div class="weblinks-content">
|
||||||
{% if current_folder %}
|
{% if current_folder %}
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="row-actions">
|
<div class="row-actions">
|
||||||
<a href="/weblinks/{{ item.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
<a href="/weblinks/{{ item.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||||
<form action="/weblinks/{{ item.id }}/delete" method="post" data-confirm="Delete this weblink?" style="display:inline">
|
<form action="/weblinks/{{ item.id }}/delete" method="post" data-confirm="Delete this link?" style="display:inline">
|
||||||
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
|
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,8 +67,8 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<div class="empty-state-icon">🔗</div>
|
<div class="empty-state-icon">🔗</div>
|
||||||
<div class="empty-state-text">No weblinks{{ ' in this folder' if current_folder }} yet</div>
|
<div class="empty-state-text">No links{{ ' in this folder' if current_folder }} yet</div>
|
||||||
<a href="/weblinks/create{{ '?folder_id=' ~ current_folder_id if current_folder_id }}" class="btn btn-primary">Add Weblink</a>
|
<a href="/weblinks/create{{ '?folder_id=' ~ current_folder_id if current_folder_id }}" class="btn btn-primary">Add Link</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,10 +23,9 @@ SEED_IDS = {
|
|||||||
"meeting": "a0000000-0000-0000-0000-000000000007",
|
"meeting": "a0000000-0000-0000-0000-000000000007",
|
||||||
"decision": "a0000000-0000-0000-0000-000000000008",
|
"decision": "a0000000-0000-0000-0000-000000000008",
|
||||||
"appointment": "a0000000-0000-0000-0000-000000000009",
|
"appointment": "a0000000-0000-0000-0000-000000000009",
|
||||||
"weblink_folder": "a0000000-0000-0000-0000-00000000000a",
|
"link_folder": "a0000000-0000-0000-0000-00000000000a",
|
||||||
"list": "a0000000-0000-0000-0000-00000000000b",
|
"list": "a0000000-0000-0000-0000-00000000000b",
|
||||||
"link": "a0000000-0000-0000-0000-00000000000c",
|
"link": "a0000000-0000-0000-0000-00000000000c",
|
||||||
"weblink": "a0000000-0000-0000-0000-00000000000d",
|
|
||||||
"capture": "a0000000-0000-0000-0000-00000000000e",
|
"capture": "a0000000-0000-0000-0000-00000000000e",
|
||||||
"focus": "a0000000-0000-0000-0000-00000000000f",
|
"focus": "a0000000-0000-0000-0000-00000000000f",
|
||||||
"process": "a0000000-0000-0000-0000-000000000010",
|
"process": "a0000000-0000-0000-0000-000000000010",
|
||||||
@@ -153,12 +152,12 @@ def all_seeds(sync_conn):
|
|||||||
ON CONFLICT (id) DO NOTHING
|
ON CONFLICT (id) DO NOTHING
|
||||||
""", (d["appointment"],))
|
""", (d["appointment"],))
|
||||||
|
|
||||||
# Weblink folder
|
# Link folder
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO weblink_folders (id, name, is_deleted, created_at, updated_at)
|
INSERT INTO link_folders (id, name, is_deleted, created_at, updated_at)
|
||||||
VALUES (%s, 'Test Folder', false, now(), now())
|
VALUES (%s, 'Test Folder', false, now(), now())
|
||||||
ON CONFLICT (id) DO NOTHING
|
ON CONFLICT (id) DO NOTHING
|
||||||
""", (d["weblink_folder"],))
|
""", (d["link_folder"],))
|
||||||
|
|
||||||
# List
|
# List
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
@@ -174,18 +173,11 @@ def all_seeds(sync_conn):
|
|||||||
ON CONFLICT (id) DO NOTHING
|
ON CONFLICT (id) DO NOTHING
|
||||||
""", (d["link"], d["domain"]))
|
""", (d["link"], d["domain"]))
|
||||||
|
|
||||||
# Weblink
|
# Link folder junction
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO weblinks (id, label, url, is_deleted, created_at, updated_at)
|
INSERT INTO folder_links (folder_id, link_id)
|
||||||
VALUES (%s, 'Test Weblink', 'https://example.com/wl', false, now(), now())
|
|
||||||
ON CONFLICT (id) DO NOTHING
|
|
||||||
""", (d["weblink"],))
|
|
||||||
|
|
||||||
# Link weblink to folder via junction table
|
|
||||||
cur.execute("""
|
|
||||||
INSERT INTO folder_weblinks (folder_id, weblink_id)
|
|
||||||
VALUES (%s, %s) ON CONFLICT DO NOTHING
|
VALUES (%s, %s) ON CONFLICT DO NOTHING
|
||||||
""", (d["weblink_folder"], d["weblink"]))
|
""", (d["link_folder"], d["link"]))
|
||||||
|
|
||||||
# Capture
|
# Capture
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
@@ -285,11 +277,10 @@ def all_seeds(sync_conn):
|
|||||||
cur.execute("DELETE FROM processes WHERE id = %s", (d["process"],))
|
cur.execute("DELETE FROM processes WHERE id = %s", (d["process"],))
|
||||||
cur.execute("DELETE FROM daily_focus WHERE id = %s", (d["focus"],))
|
cur.execute("DELETE FROM daily_focus WHERE id = %s", (d["focus"],))
|
||||||
cur.execute("DELETE FROM capture WHERE id = %s", (d["capture"],))
|
cur.execute("DELETE FROM capture WHERE id = %s", (d["capture"],))
|
||||||
cur.execute("DELETE FROM folder_weblinks WHERE weblink_id = %s", (d["weblink"],))
|
cur.execute("DELETE FROM folder_links WHERE link_id = %s", (d["link"],))
|
||||||
cur.execute("DELETE FROM weblinks WHERE id = %s", (d["weblink"],))
|
|
||||||
cur.execute("DELETE FROM links WHERE id = %s", (d["link"],))
|
cur.execute("DELETE FROM links WHERE id = %s", (d["link"],))
|
||||||
cur.execute("DELETE FROM lists WHERE id = %s", (d["list"],))
|
cur.execute("DELETE FROM lists WHERE id = %s", (d["list"],))
|
||||||
cur.execute("DELETE FROM weblink_folders WHERE id = %s", (d["weblink_folder"],))
|
cur.execute("DELETE FROM link_folders WHERE id = %s", (d["link_folder"],))
|
||||||
cur.execute("DELETE FROM appointments WHERE id = %s", (d["appointment"],))
|
cur.execute("DELETE FROM appointments WHERE id = %s", (d["appointment"],))
|
||||||
cur.execute("DELETE FROM decisions WHERE id = %s", (d["decision"],))
|
cur.execute("DELETE FROM decisions WHERE id = %s", (d["decision"],))
|
||||||
cur.execute("DELETE FROM meetings WHERE id = %s", (d["meeting"],))
|
cur.execute("DELETE FROM meetings WHERE id = %s", (d["meeting"],))
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ PREFIX_TO_SEED = {
|
|||||||
"/lists": "list",
|
"/lists": "list",
|
||||||
"/meetings": "meeting",
|
"/meetings": "meeting",
|
||||||
"/decisions": "decision",
|
"/decisions": "decision",
|
||||||
"/weblinks": "weblink",
|
"/weblinks": "link",
|
||||||
"/weblinks/folders": "weblink_folder",
|
"/weblinks/folders": "link_folder",
|
||||||
"/appointments": "appointment",
|
"/appointments": "appointment",
|
||||||
"/focus": "focus",
|
"/focus": "focus",
|
||||||
"/capture": "capture",
|
"/capture": "capture",
|
||||||
|
|||||||
@@ -520,11 +520,11 @@ class TestCaptureConversions:
|
|||||||
assert dec.impact == "medium"
|
assert dec.impact == "medium"
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_convert_to_weblink(
|
async def test_convert_to_link(
|
||||||
self, client: AsyncClient, db_session: AsyncSession,
|
self, client: AsyncClient, db_session: AsyncSession,
|
||||||
):
|
):
|
||||||
cap_id = await _create_capture(db_session, "Check https://example.com/test for details")
|
cap_id = await _create_capture(db_session, "Check https://example.com/test for details")
|
||||||
r = await client.post(f"/capture/{cap_id}/to-weblink", data={}, follow_redirects=False)
|
r = await client.post(f"/capture/{cap_id}/to-link", data={}, follow_redirects=False)
|
||||||
assert r.status_code == 303
|
assert r.status_code == 303
|
||||||
|
|
||||||
result = await db_session.execute(
|
result = await db_session.execute(
|
||||||
@@ -532,10 +532,10 @@ class TestCaptureConversions:
|
|||||||
{"id": cap_id},
|
{"id": cap_id},
|
||||||
)
|
)
|
||||||
row = result.first()
|
row = result.first()
|
||||||
assert row.converted_to_type == "weblink"
|
assert row.converted_to_type == "link"
|
||||||
# URL should be extracted
|
# URL should be extracted
|
||||||
result = await db_session.execute(
|
result = await db_session.execute(
|
||||||
text("SELECT url FROM weblinks WHERE id = :id"), {"id": row.converted_to_id},
|
text("SELECT url FROM links WHERE id = :id"), {"id": row.converted_to_id},
|
||||||
)
|
)
|
||||||
assert "https://example.com/test" in result.first().url
|
assert "https://example.com/test" in result.first().url
|
||||||
|
|
||||||
@@ -1912,7 +1912,7 @@ class TestTaskDetailTabs:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_all_tabs_return_200(self, client: AsyncClient, seed_task: dict):
|
async def test_all_tabs_return_200(self, client: AsyncClient, seed_task: dict):
|
||||||
"""Every tab on task detail returns 200."""
|
"""Every tab on task detail returns 200."""
|
||||||
for tab in ("overview", "notes", "weblinks", "files", "lists", "decisions", "processes", "contacts"):
|
for tab in ("overview", "notes", "links", "files", "lists", "decisions", "processes", "contacts"):
|
||||||
r = await client.get(f"/tasks/{seed_task['id']}?tab={tab}")
|
r = await client.get(f"/tasks/{seed_task['id']}?tab={tab}")
|
||||||
assert r.status_code == 200, f"Tab '{tab}' returned {r.status_code}"
|
assert r.status_code == 200, f"Tab '{tab}' returned {r.status_code}"
|
||||||
|
|
||||||
@@ -2141,7 +2141,7 @@ class TestMeetingDetailTabs:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_all_meeting_tabs_return_200(self, client: AsyncClient, seed_meeting: dict):
|
async def test_all_meeting_tabs_return_200(self, client: AsyncClient, seed_meeting: dict):
|
||||||
"""Every tab on meeting detail returns 200."""
|
"""Every tab on meeting detail returns 200."""
|
||||||
for tab in ("overview", "notes", "weblinks", "files", "lists", "processes", "contacts"):
|
for tab in ("overview", "notes", "links", "files", "lists", "processes", "contacts"):
|
||||||
r = await client.get(f"/meetings/{seed_meeting['id']}?tab={tab}")
|
r = await client.get(f"/meetings/{seed_meeting['id']}?tab={tab}")
|
||||||
assert r.status_code == 200, f"Meeting tab '{tab}' returned {r.status_code}"
|
assert r.status_code == 200, f"Meeting tab '{tab}' returned {r.status_code}"
|
||||||
|
|
||||||
@@ -2554,28 +2554,28 @@ class TestEntityCreateWithParentContext:
|
|||||||
assert seed_task["id"] in r.text
|
assert seed_task["id"] in r.text
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_create_weblink_with_task_id(
|
async def test_create_link_with_task_id(
|
||||||
self, client: AsyncClient, db_session: AsyncSession, seed_task: dict,
|
self, client: AsyncClient, db_session: AsyncSession, seed_task: dict,
|
||||||
):
|
):
|
||||||
"""Creating a weblink with task_id sets the FK."""
|
"""Creating a link with task_id sets the FK."""
|
||||||
tag = _uid()
|
tag = _uid()
|
||||||
r = await client.post("/weblinks/create", data={
|
r = await client.post("/weblinks/create", data={
|
||||||
"label": f"TaskWeblink-{tag}",
|
"label": f"TaskLink-{tag}",
|
||||||
"url": "https://example.com/test",
|
"url": "https://example.com/test",
|
||||||
"task_id": seed_task["id"],
|
"task_id": seed_task["id"],
|
||||||
}, follow_redirects=False)
|
}, follow_redirects=False)
|
||||||
assert r.status_code == 303
|
assert r.status_code == 303
|
||||||
|
|
||||||
result = await db_session.execute(
|
result = await db_session.execute(
|
||||||
text("SELECT task_id FROM weblinks WHERE label = :l AND is_deleted = false"),
|
text("SELECT task_id FROM links WHERE label = :l AND is_deleted = false"),
|
||||||
{"l": f"TaskWeblink-{tag}"},
|
{"l": f"TaskLink-{tag}"},
|
||||||
)
|
)
|
||||||
row = result.first()
|
row = result.first()
|
||||||
assert row is not None
|
assert row is not None
|
||||||
assert str(row.task_id) == seed_task["id"]
|
assert str(row.task_id) == seed_task["id"]
|
||||||
|
|
||||||
await db_session.execute(
|
await db_session.execute(
|
||||||
text("DELETE FROM weblinks WHERE label = :l"), {"l": f"TaskWeblink-{tag}"}
|
text("DELETE FROM links WHERE label = :l"), {"l": f"TaskLink-{tag}"}
|
||||||
)
|
)
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user