feat: Edit, Unlink, Delete actions on all project detail tabs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -419,25 +419,108 @@ async def remove_contact(
|
||||
return RedirectResponse(url=f"/projects/{project_id}?tab=contacts", status_code=303)
|
||||
|
||||
|
||||
# ---- Link unlinking ----
|
||||
# ---- Entity unlink / delete from project tabs ----
|
||||
|
||||
# Tasks (FK: tasks.project_id)
|
||||
@router.post("/{project_id}/tasks/{task_id}/unlink")
|
||||
async def unlink_task(project_id: str, task_id: str, db=Depends(get_db)):
|
||||
await db.execute(text(
|
||||
"UPDATE tasks SET project_id = NULL, updated_at = now() WHERE id = :tid AND project_id = :pid"
|
||||
), {"tid": task_id, "pid": project_id})
|
||||
await db.commit()
|
||||
return RedirectResponse(url=f"/projects/{project_id}?tab=tasks", status_code=303)
|
||||
|
||||
@router.post("/{project_id}/tasks/{task_id}/delete")
|
||||
async def delete_project_task(project_id: str, task_id: str, db=Depends(get_db)):
|
||||
await BaseRepository("tasks", db).soft_delete(task_id)
|
||||
return RedirectResponse(url=f"/projects/{project_id}?tab=tasks", status_code=303)
|
||||
|
||||
# Notes (FK: notes.project_id)
|
||||
@router.post("/{project_id}/notes/{note_id}/unlink")
|
||||
async def unlink_note(project_id: str, note_id: str, db=Depends(get_db)):
|
||||
await db.execute(text(
|
||||
"UPDATE notes SET project_id = NULL, updated_at = now() WHERE id = :nid AND project_id = :pid"
|
||||
), {"nid": note_id, "pid": project_id})
|
||||
await db.commit()
|
||||
return RedirectResponse(url=f"/projects/{project_id}?tab=notes", status_code=303)
|
||||
|
||||
@router.post("/{project_id}/notes/{note_id}/delete")
|
||||
async def delete_project_note(project_id: str, note_id: str, db=Depends(get_db)):
|
||||
await BaseRepository("notes", db).soft_delete(note_id)
|
||||
return RedirectResponse(url=f"/projects/{project_id}?tab=notes", status_code=303)
|
||||
|
||||
# Links (FK: links.project_id)
|
||||
@router.post("/{project_id}/links/{link_id}/unlink")
|
||||
async def unlink_link(
|
||||
project_id: str, link_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
async def unlink_link(project_id: str, link_id: str, db=Depends(get_db)):
|
||||
await db.execute(text(
|
||||
"UPDATE links SET project_id = NULL, updated_at = now() WHERE id = :lid AND project_id = :pid"
|
||||
), {"lid": link_id, "pid": project_id})
|
||||
await db.commit()
|
||||
return RedirectResponse(url=f"/projects/{project_id}?tab=links", status_code=303)
|
||||
|
||||
|
||||
@router.post("/{project_id}/links/{link_id}/delete")
|
||||
async def delete_project_link(
|
||||
project_id: str, link_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
repo = BaseRepository("links", db)
|
||||
await repo.soft_delete(link_id)
|
||||
async def delete_project_link(project_id: str, link_id: str, db=Depends(get_db)):
|
||||
await BaseRepository("links", db).soft_delete(link_id)
|
||||
return RedirectResponse(url=f"/projects/{project_id}?tab=links", status_code=303)
|
||||
|
||||
# Files (junction: file_mappings)
|
||||
@router.post("/{project_id}/files/{file_id}/unlink")
|
||||
async def unlink_file(project_id: str, file_id: str, db=Depends(get_db)):
|
||||
await db.execute(text(
|
||||
"DELETE FROM file_mappings WHERE file_id = :fid AND context_type = 'project' AND context_id = :pid::uuid"
|
||||
), {"fid": file_id, "pid": project_id})
|
||||
await db.commit()
|
||||
return RedirectResponse(url=f"/projects/{project_id}?tab=files", status_code=303)
|
||||
|
||||
@router.post("/{project_id}/files/{file_id}/delete")
|
||||
async def delete_project_file(project_id: str, file_id: str, db=Depends(get_db)):
|
||||
await BaseRepository("files", db).soft_delete(file_id)
|
||||
return RedirectResponse(url=f"/projects/{project_id}?tab=files", status_code=303)
|
||||
|
||||
# Lists (FK: lists.project_id)
|
||||
@router.post("/{project_id}/lists/{list_id}/unlink")
|
||||
async def unlink_list(project_id: str, list_id: str, db=Depends(get_db)):
|
||||
await db.execute(text(
|
||||
"UPDATE lists SET project_id = NULL, updated_at = now() WHERE id = :lid AND project_id = :pid"
|
||||
), {"lid": list_id, "pid": project_id})
|
||||
await db.commit()
|
||||
return RedirectResponse(url=f"/projects/{project_id}?tab=lists", status_code=303)
|
||||
|
||||
@router.post("/{project_id}/lists/{list_id}/delete")
|
||||
async def delete_project_list(project_id: str, list_id: str, db=Depends(get_db)):
|
||||
await BaseRepository("lists", db).soft_delete(list_id)
|
||||
return RedirectResponse(url=f"/projects/{project_id}?tab=lists", status_code=303)
|
||||
|
||||
# Decisions (junction: decision_projects)
|
||||
@router.post("/{project_id}/decisions/{decision_id}/unlink")
|
||||
async def unlink_decision(project_id: str, decision_id: str, db=Depends(get_db)):
|
||||
await db.execute(text(
|
||||
"DELETE FROM decision_projects WHERE decision_id = :did AND project_id = :pid"
|
||||
), {"did": decision_id, "pid": project_id})
|
||||
await db.commit()
|
||||
return RedirectResponse(url=f"/projects/{project_id}?tab=decisions", status_code=303)
|
||||
|
||||
@router.post("/{project_id}/decisions/{decision_id}/delete")
|
||||
async def delete_project_decision(project_id: str, decision_id: str, db=Depends(get_db)):
|
||||
await BaseRepository("decisions", db).soft_delete(decision_id)
|
||||
return RedirectResponse(url=f"/projects/{project_id}?tab=decisions", status_code=303)
|
||||
|
||||
# Meetings (junction: project_meetings) — remove already exists, add delete
|
||||
@router.post("/{project_id}/meetings/{meeting_id}/delete")
|
||||
async def delete_project_meeting(project_id: str, meeting_id: str, db=Depends(get_db)):
|
||||
await BaseRepository("meetings", db).soft_delete(meeting_id)
|
||||
return RedirectResponse(url=f"/projects/{project_id}?tab=meetings", status_code=303)
|
||||
|
||||
# Focus (FK: daily_focus.project_id)
|
||||
@router.post("/{project_id}/focus/{focus_id}/unlink")
|
||||
async def unlink_focus(project_id: str, focus_id: str, db=Depends(get_db)):
|
||||
await db.execute(text(
|
||||
"UPDATE daily_focus SET project_id = NULL, updated_at = now() WHERE id = :fid AND project_id = :pid"
|
||||
), {"fid": focus_id, "pid": project_id})
|
||||
await db.commit()
|
||||
return RedirectResponse(url=f"/projects/{project_id}?tab=focus", status_code=303)
|
||||
|
||||
@router.post("/{project_id}/focus/{focus_id}/delete")
|
||||
async def delete_project_focus(project_id: str, focus_id: str, db=Depends(get_db)):
|
||||
await BaseRepository("daily_focus", db).soft_delete(focus_id)
|
||||
return RedirectResponse(url=f"/projects/{project_id}?tab=focus", status_code=303)
|
||||
|
||||
@@ -68,6 +68,12 @@
|
||||
<span class="status-badge status-{{ t.status }}">{{ t.status|replace('_', ' ') }}</span>
|
||||
<div class="row-actions">
|
||||
<a href="/tasks/{{ t.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||
<form action="/projects/{{ item.id }}/tasks/{{ t.id }}/unlink" method="post" style="display:inline">
|
||||
<button class="btn btn-ghost btn-xs">Unlink</button>
|
||||
</form>
|
||||
<form action="/projects/{{ item.id }}/tasks/{{ t.id }}/delete" method="post" data-confirm="Delete this task?" style="display:inline">
|
||||
<button class="btn btn-ghost btn-xs" style="color:var(--red)">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
@@ -80,6 +86,15 @@
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/notes/{{ n.id }}">{{ n.title }}</a></span>
|
||||
<span class="row-meta">{{ n.updated_at.strftime('%Y-%m-%d') if n.updated_at else '' }}</span>
|
||||
<div class="row-actions">
|
||||
<a href="/notes/{{ n.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||
<form action="/projects/{{ item.id }}/notes/{{ n.id }}/unlink" method="post" style="display:inline">
|
||||
<button class="btn btn-ghost btn-xs">Unlink</button>
|
||||
</form>
|
||||
<form action="/projects/{{ item.id }}/notes/{{ n.id }}/delete" method="post" data-confirm="Delete this note?" style="display:inline">
|
||||
<button class="btn btn-ghost btn-xs" style="color:var(--red)">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No notes yet</div></div>
|
||||
@@ -111,6 +126,14 @@
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/files/{{ f.id }}">{{ f.original_filename }}</a></span>
|
||||
<span class="row-meta">{{ f.created_at.strftime('%Y-%m-%d') if f.created_at else '' }}</span>
|
||||
<div class="row-actions">
|
||||
<form action="/projects/{{ item.id }}/files/{{ f.id }}/unlink" method="post" style="display:inline">
|
||||
<button class="btn btn-ghost btn-xs">Unlink</button>
|
||||
</form>
|
||||
<form action="/projects/{{ item.id }}/files/{{ f.id }}/delete" method="post" data-confirm="Delete this file?" style="display:inline">
|
||||
<button class="btn btn-ghost btn-xs" style="color:var(--red)">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No files attached to this project</div></div>
|
||||
@@ -122,6 +145,15 @@
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/lists/{{ l.id }}">{{ l.name }}</a></span>
|
||||
<span class="row-meta">{{ l.item_count }} items</span>
|
||||
<div class="row-actions">
|
||||
<a href="/lists/{{ l.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||
<form action="/projects/{{ item.id }}/lists/{{ l.id }}/unlink" method="post" style="display:inline">
|
||||
<button class="btn btn-ghost btn-xs">Unlink</button>
|
||||
</form>
|
||||
<form action="/projects/{{ item.id }}/lists/{{ l.id }}/delete" method="post" data-confirm="Delete this list?" style="display:inline">
|
||||
<button class="btn btn-ghost btn-xs" style="color:var(--red)">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No lists linked to this project</div></div>
|
||||
@@ -133,6 +165,15 @@
|
||||
<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>
|
||||
<div class="row-actions">
|
||||
<a href="/decisions/{{ d.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||
<form action="/projects/{{ item.id }}/decisions/{{ d.id }}/unlink" method="post" style="display:inline">
|
||||
<button class="btn btn-ghost btn-xs">Unlink</button>
|
||||
</form>
|
||||
<form action="/projects/{{ item.id }}/decisions/{{ d.id }}/delete" method="post" data-confirm="Delete this decision?" style="display:inline">
|
||||
<button class="btn btn-ghost btn-xs" style="color:var(--red)">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No decisions linked to this project</div></div>
|
||||
@@ -160,8 +201,12 @@
|
||||
<span class="row-meta">{{ m.meeting_date }}</span>
|
||||
<span class="status-badge status-{{ m.status }}">{{ m.status }}</span>
|
||||
<div class="row-actions">
|
||||
<a href="/meetings/{{ m.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||
<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>
|
||||
<button class="btn btn-ghost btn-xs">Unlink</button>
|
||||
</form>
|
||||
<form action="/projects/{{ item.id }}/meetings/{{ m.id }}/delete" method="post" data-confirm="Delete this meeting?" style="display:inline">
|
||||
<button class="btn btn-ghost btn-xs" style="color:var(--red)">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -172,11 +217,17 @@
|
||||
{% elif tab == 'focus' %}
|
||||
{% for f in tab_data %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/focus/{{ f.id }}/edit">{{ f.title }}</a></span>
|
||||
<span class="row-title"><a href="/focus/{{ f.id }}">{{ f.title }}</a></span>
|
||||
{% if f.domain_name %}<span class="row-tag" style="{% if f.domain_color %}border-color:{{ f.domain_color }};color:{{ f.domain_color }}{% endif %}">{{ f.domain_name }}</span>{% endif %}
|
||||
<span class="row-meta">{{ f.created_at.strftime('%Y-%m-%d') if f.created_at else '' }}</span>
|
||||
<div class="row-actions">
|
||||
<a href="/focus/{{ f.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||
<form action="/projects/{{ item.id }}/focus/{{ f.id }}/unlink" method="post" style="display:inline">
|
||||
<button class="btn btn-ghost btn-xs">Unlink</button>
|
||||
</form>
|
||||
<form action="/projects/{{ item.id }}/focus/{{ f.id }}/delete" method="post" data-confirm="Delete this focus item?" style="display:inline">
|
||||
<button class="btn btn-ghost btn-xs" style="color:var(--red)">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
@@ -211,8 +262,12 @@
|
||||
{% if c.role %}<span class="row-tag">{{ c.role }}</span>{% endif %}
|
||||
<span class="row-meta">{{ c.linked_at.strftime('%Y-%m-%d') if c.linked_at else '' }}</span>
|
||||
<div class="row-actions">
|
||||
<a href="/contacts/{{ c.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||
<form action="/projects/{{ item.id }}/contacts/{{ c.id }}/remove" method="post" style="display:inline">
|
||||
<button class="btn btn-ghost btn-xs" title="Remove">Remove</button>
|
||||
<button class="btn btn-ghost btn-xs">Unlink</button>
|
||||
</form>
|
||||
<form action="/contacts/{{ c.id }}/delete" method="post" data-confirm="Delete this contact?" style="display:inline">
|
||||
<button class="btn btn-ghost btn-xs" style="color:var(--red)">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user