feat: universal reorder grip handles and compact UI density
- Add generic move_in_order() to BaseRepository for reorder support - Add reusable reorder_arrows.html partial with grip dot handles - Add reorder routes to all 9 list routers (tasks, notes, links, contacts, meetings, decisions, appointments, lists, focus) - Compact row padding (6px 12px, gap 8px) on .list-row and .focus-item - Reduce font size to 0.80rem on row titles, sidebar nav, domain tree Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -248,3 +248,72 @@ class BaseRepository:
|
|||||||
text(f"UPDATE {self.table} SET sort_order = :order WHERE id = :id"),
|
text(f"UPDATE {self.table} SET sort_order = :order WHERE id = :id"),
|
||||||
{"order": (i + 1) * 10, "id": str(id)}
|
{"order": (i + 1) * 10, "id": str(id)}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def swap_sort_order(self, id_a: str, id_b: str) -> None:
|
||||||
|
"""Swap sort_order between two rows."""
|
||||||
|
result = await self.db.execute(
|
||||||
|
text(f"SELECT id, sort_order FROM {self.table} WHERE id IN (:a, :b)"),
|
||||||
|
{"a": str(id_a), "b": str(id_b)},
|
||||||
|
)
|
||||||
|
rows = {str(r._mapping["id"]): r._mapping["sort_order"] for r in result}
|
||||||
|
if len(rows) != 2:
|
||||||
|
return
|
||||||
|
await self.db.execute(
|
||||||
|
text(f"UPDATE {self.table} SET sort_order = :order WHERE id = :id"),
|
||||||
|
{"order": rows[str(id_b)], "id": str(id_a)},
|
||||||
|
)
|
||||||
|
await self.db.execute(
|
||||||
|
text(f"UPDATE {self.table} SET sort_order = :order WHERE id = :id"),
|
||||||
|
{"order": rows[str(id_a)], "id": str(id_b)},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def move_in_order(self, item_id: str, direction: str, filters: dict | None = None) -> None:
|
||||||
|
"""Move an item up or down within its sort group.
|
||||||
|
|
||||||
|
Handles lazy initialization (all sort_order=0) and swaps with neighbor.
|
||||||
|
filters: optional dict to scope the group (e.g. {"list_id": some_id}).
|
||||||
|
"""
|
||||||
|
where_clauses = ["is_deleted = false"]
|
||||||
|
params: dict[str, Any] = {}
|
||||||
|
if filters:
|
||||||
|
for i, (key, value) in enumerate(filters.items()):
|
||||||
|
if value is None:
|
||||||
|
where_clauses.append(f"{key} IS NULL")
|
||||||
|
else:
|
||||||
|
param_name = f"mf_{i}"
|
||||||
|
where_clauses.append(f"{key} = :{param_name}")
|
||||||
|
params[param_name] = value
|
||||||
|
|
||||||
|
where_sql = " AND ".join(where_clauses)
|
||||||
|
|
||||||
|
result = await self.db.execute(
|
||||||
|
text(f"SELECT id, sort_order FROM {self.table} WHERE {where_sql} ORDER BY sort_order, created_at"),
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
items = [dict(r._mapping) for r in result]
|
||||||
|
if len(items) < 2:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Lazy init: if all sort_order are 0, assign incremental values
|
||||||
|
if all(r["sort_order"] == 0 for r in items):
|
||||||
|
for i, r in enumerate(items):
|
||||||
|
await self.db.execute(
|
||||||
|
text(f"UPDATE {self.table} SET sort_order = :order WHERE id = :id"),
|
||||||
|
{"order": (i + 1) * 10, "id": str(r["id"])},
|
||||||
|
)
|
||||||
|
# Re-fetch
|
||||||
|
result = await self.db.execute(
|
||||||
|
text(f"SELECT id, sort_order FROM {self.table} WHERE {where_sql} ORDER BY sort_order, created_at"),
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
items = [dict(r._mapping) for r in result]
|
||||||
|
|
||||||
|
ids = [str(r["id"]) for r in items]
|
||||||
|
if item_id not in ids:
|
||||||
|
return
|
||||||
|
|
||||||
|
idx = ids.index(item_id)
|
||||||
|
if direction == "up" and idx > 0:
|
||||||
|
await self.swap_sort_order(ids[idx], ids[idx - 1])
|
||||||
|
elif direction == "down" and idx < len(ids) - 1:
|
||||||
|
await self.swap_sort_order(ids[idx], ids[idx + 1])
|
||||||
|
|||||||
@@ -300,3 +300,15 @@ async def delete_appointment(
|
|||||||
repo = BaseRepository("appointments", db)
|
repo = BaseRepository("appointments", db)
|
||||||
await repo.soft_delete(appointment_id)
|
await repo.soft_delete(appointment_id)
|
||||||
return RedirectResponse(url="/appointments", status_code=303)
|
return RedirectResponse(url="/appointments", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/reorder")
|
||||||
|
async def reorder_appointment(
|
||||||
|
request: Request,
|
||||||
|
item_id: str = Form(...),
|
||||||
|
direction: str = Form(...),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
repo = BaseRepository("appointments", db)
|
||||||
|
await repo.move_in_order(item_id, direction)
|
||||||
|
return RedirectResponse(url=request.headers.get("referer", "/appointments"), status_code=303)
|
||||||
|
|||||||
@@ -124,3 +124,15 @@ async def delete_contact(contact_id: str, db: AsyncSession = Depends(get_db)):
|
|||||||
repo = BaseRepository("contacts", db)
|
repo = BaseRepository("contacts", db)
|
||||||
await repo.soft_delete(contact_id)
|
await repo.soft_delete(contact_id)
|
||||||
return RedirectResponse(url="/contacts", status_code=303)
|
return RedirectResponse(url="/contacts", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/reorder")
|
||||||
|
async def reorder_contact(
|
||||||
|
request: Request,
|
||||||
|
item_id: str = Form(...),
|
||||||
|
direction: str = Form(...),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
repo = BaseRepository("contacts", db)
|
||||||
|
await repo.move_in_order(item_id, direction)
|
||||||
|
return RedirectResponse(url=request.headers.get("referer", "/contacts"), status_code=303)
|
||||||
|
|||||||
@@ -216,3 +216,15 @@ async def delete_decision(decision_id: str, request: Request, db: AsyncSession =
|
|||||||
repo = BaseRepository("decisions", db)
|
repo = BaseRepository("decisions", db)
|
||||||
await repo.soft_delete(decision_id)
|
await repo.soft_delete(decision_id)
|
||||||
return RedirectResponse(url="/decisions", status_code=303)
|
return RedirectResponse(url="/decisions", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/reorder")
|
||||||
|
async def reorder_decision(
|
||||||
|
request: Request,
|
||||||
|
item_id: str = Form(...),
|
||||||
|
direction: str = Form(...),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
repo = BaseRepository("decisions", db)
|
||||||
|
await repo.move_in_order(item_id, direction)
|
||||||
|
return RedirectResponse(url=request.headers.get("referer", "/decisions"), status_code=303)
|
||||||
|
|||||||
@@ -121,6 +121,20 @@ async def add_to_focus(
|
|||||||
return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303)
|
return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/reorder")
|
||||||
|
async def reorder_focus(
|
||||||
|
request: Request,
|
||||||
|
item_id: str = Form(...),
|
||||||
|
direction: str = Form(...),
|
||||||
|
focus_date: str = Form(...),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
repo = BaseRepository("daily_focus", db)
|
||||||
|
parsed_date = date.fromisoformat(focus_date)
|
||||||
|
await repo.move_in_order(item_id, direction, filters={"focus_date": parsed_date})
|
||||||
|
return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{focus_id}/toggle")
|
@router.post("/{focus_id}/toggle")
|
||||||
async def toggle_focus(focus_id: str, request: Request, db: AsyncSession = Depends(get_db)):
|
async def toggle_focus(focus_id: str, request: Request, db: AsyncSession = Depends(get_db)):
|
||||||
repo = BaseRepository("daily_focus", db)
|
repo = BaseRepository("daily_focus", db)
|
||||||
|
|||||||
@@ -154,3 +154,15 @@ async def delete_link(link_id: str, request: Request, db: AsyncSession = Depends
|
|||||||
repo = BaseRepository("links", db)
|
repo = BaseRepository("links", db)
|
||||||
await repo.soft_delete(link_id)
|
await repo.soft_delete(link_id)
|
||||||
return RedirectResponse(url=request.headers.get("referer", "/links"), status_code=303)
|
return RedirectResponse(url=request.headers.get("referer", "/links"), status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/reorder")
|
||||||
|
async def reorder_link(
|
||||||
|
request: Request,
|
||||||
|
item_id: str = Form(...),
|
||||||
|
direction: str = Form(...),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
repo = BaseRepository("links", db)
|
||||||
|
await repo.move_in_order(item_id, direction)
|
||||||
|
return RedirectResponse(url=request.headers.get("referer", "/links"), status_code=303)
|
||||||
|
|||||||
@@ -332,3 +332,35 @@ async def remove_contact(
|
|||||||
"DELETE FROM contact_lists WHERE contact_id = :cid AND list_id = :lid"
|
"DELETE FROM contact_lists WHERE contact_id = :cid AND list_id = :lid"
|
||||||
), {"cid": contact_id, "lid": list_id})
|
), {"cid": contact_id, "lid": list_id})
|
||||||
return RedirectResponse(url=f"/lists/{list_id}", status_code=303)
|
return RedirectResponse(url=f"/lists/{list_id}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/reorder")
|
||||||
|
async def reorder_list(
|
||||||
|
request: Request,
|
||||||
|
item_id: str = Form(...),
|
||||||
|
direction: str = Form(...),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
repo = BaseRepository("lists", db)
|
||||||
|
await repo.move_in_order(item_id, direction)
|
||||||
|
return RedirectResponse(url=request.headers.get("referer", "/lists"), status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{list_id}/items/reorder")
|
||||||
|
async def reorder_list_item(
|
||||||
|
list_id: str,
|
||||||
|
request: Request,
|
||||||
|
item_id: str = Form(...),
|
||||||
|
direction: str = Form(...),
|
||||||
|
parent_id: Optional[str] = Form(None),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
repo = BaseRepository("list_items", db)
|
||||||
|
filters = {"list_id": list_id}
|
||||||
|
if parent_id:
|
||||||
|
filters["parent_item_id"] = parent_id
|
||||||
|
else:
|
||||||
|
# Top-level items only (no parent)
|
||||||
|
filters["parent_item_id"] = None
|
||||||
|
await repo.move_in_order(item_id, direction, filters=filters)
|
||||||
|
return RedirectResponse(url=f"/lists/{list_id}", status_code=303)
|
||||||
|
|||||||
@@ -404,3 +404,15 @@ async def remove_contact(
|
|||||||
"DELETE FROM contact_meetings WHERE contact_id = :cid AND meeting_id = :mid"
|
"DELETE FROM contact_meetings WHERE contact_id = :cid AND meeting_id = :mid"
|
||||||
), {"cid": contact_id, "mid": meeting_id})
|
), {"cid": contact_id, "mid": meeting_id})
|
||||||
return RedirectResponse(url=f"/meetings/{meeting_id}?tab=contacts", status_code=303)
|
return RedirectResponse(url=f"/meetings/{meeting_id}?tab=contacts", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/reorder")
|
||||||
|
async def reorder_meeting(
|
||||||
|
request: Request,
|
||||||
|
item_id: str = Form(...),
|
||||||
|
direction: str = Form(...),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
repo = BaseRepository("meetings", db)
|
||||||
|
await repo.move_in_order(item_id, direction)
|
||||||
|
return RedirectResponse(url=request.headers.get("referer", "/meetings"), status_code=303)
|
||||||
|
|||||||
@@ -193,3 +193,15 @@ async def delete_note(note_id: str, request: Request, db: AsyncSession = Depends
|
|||||||
await repo.soft_delete(note_id)
|
await repo.soft_delete(note_id)
|
||||||
referer = request.headers.get("referer", "/notes")
|
referer = request.headers.get("referer", "/notes")
|
||||||
return RedirectResponse(url=referer, status_code=303)
|
return RedirectResponse(url=referer, status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/reorder")
|
||||||
|
async def reorder_note(
|
||||||
|
request: Request,
|
||||||
|
item_id: str = Form(...),
|
||||||
|
direction: str = Form(...),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
repo = BaseRepository("notes", db)
|
||||||
|
await repo.move_in_order(item_id, direction)
|
||||||
|
return RedirectResponse(url=request.headers.get("referer", "/notes"), status_code=303)
|
||||||
|
|||||||
@@ -481,3 +481,15 @@ async def remove_contact(
|
|||||||
"DELETE FROM contact_tasks WHERE contact_id = :cid AND task_id = :tid"
|
"DELETE FROM contact_tasks WHERE contact_id = :cid AND task_id = :tid"
|
||||||
), {"cid": contact_id, "tid": task_id})
|
), {"cid": contact_id, "tid": task_id})
|
||||||
return RedirectResponse(url=f"/tasks/{task_id}?tab=contacts", status_code=303)
|
return RedirectResponse(url=f"/tasks/{task_id}?tab=contacts", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/reorder")
|
||||||
|
async def reorder_task(
|
||||||
|
request: Request,
|
||||||
|
item_id: str = Form(...),
|
||||||
|
direction: str = Form(...),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
repo = BaseRepository("tasks", db)
|
||||||
|
await repo.move_in_order(item_id, direction)
|
||||||
|
return RedirectResponse(url=request.headers.get("referer", "/tasks"), status_code=303)
|
||||||
|
|||||||
@@ -152,11 +152,11 @@ a:hover { color: var(--accent-hover); }
|
|||||||
.nav-item {
|
.nav-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
font-size: 0.80rem;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 0.92rem;
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all var(--transition);
|
transition: all var(--transition);
|
||||||
@@ -201,7 +201,7 @@ a:hover { color: var(--accent-hover); }
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.80rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -233,7 +233,7 @@ a:hover { color: var(--accent-hover); }
|
|||||||
.project-link {
|
.project-link {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 4px 10px 4px 18px;
|
padding: 4px 10px 4px 18px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.80rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -401,8 +401,8 @@ a:hover { color: var(--accent-hover); }
|
|||||||
.list-row {
|
.list-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
padding: 10px 12px;
|
padding: 6px 12px;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
transition: background var(--transition);
|
transition: background var(--transition);
|
||||||
}
|
}
|
||||||
@@ -466,6 +466,7 @@ a:hover { color: var(--accent-hover); }
|
|||||||
|
|
||||||
.row-title {
|
.row-title {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
font-size: 0.80rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -815,8 +816,8 @@ a:hover { color: var(--accent-hover); }
|
|||||||
.focus-item {
|
.focus-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
padding: 14px 16px;
|
padding: 6px 12px;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
@@ -828,7 +829,7 @@ a:hover { color: var(--accent-hover); }
|
|||||||
.focus-item.completed { opacity: 0.6; }
|
.focus-item.completed { opacity: 0.6; }
|
||||||
.focus-item.completed .focus-title { text-decoration: line-through; }
|
.focus-item.completed .focus-title { text-decoration: line-through; }
|
||||||
|
|
||||||
.focus-title { flex: 1; font-weight: 500; }
|
.focus-title { flex: 1; font-size: 0.80rem; font-weight: 500; }
|
||||||
.focus-meta { font-size: 0.78rem; color: var(--muted); }
|
.focus-meta { font-size: 0.78rem; color: var(--muted); }
|
||||||
|
|
||||||
/* ---- Alerts ---- */
|
/* ---- Alerts ---- */
|
||||||
@@ -1108,6 +1109,35 @@ a:hover { color: var(--accent-hover); }
|
|||||||
transition: width 0.3s;
|
transition: width 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Reorder Grip Handle ---- */
|
||||||
|
.reorder-grip {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
margin-right: 4px;
|
||||||
|
opacity: 0.3;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.reorder-grip:hover { opacity: 0.9; }
|
||||||
|
.reorder-grip form { display: block; margin: 0; padding: 0; line-height: 0; }
|
||||||
|
.grip-btn {
|
||||||
|
display: block;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 10px;
|
||||||
|
width: 12px;
|
||||||
|
height: 10px;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 10px;
|
||||||
|
transition: color 0.15s;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.grip-btn:hover { color: var(--accent); }
|
||||||
|
|
||||||
/* ---- Scrollbar ---- */
|
/* ---- Scrollbar ---- */
|
||||||
::-webkit-scrollbar { width: 6px; }
|
::-webkit-scrollbar { width: 6px; }
|
||||||
::-webkit-scrollbar-track { background: transparent; }
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
|||||||
@@ -30,6 +30,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="list-row">
|
<div class="list-row">
|
||||||
|
{% with reorder_url="/appointments/reorder", item_id=appt.id %}
|
||||||
|
{% include 'partials/reorder_arrows.html' %}
|
||||||
|
{% endwith %}
|
||||||
<div style="flex-shrink: 0; min-width: 60px;">
|
<div style="flex-shrink: 0; min-width: 60px;">
|
||||||
{% if appt.all_day %}
|
{% if appt.all_day %}
|
||||||
<span class="status-badge status-active" style="font-size: 0.72rem;">All Day</span>
|
<span class="status-badge status-active" style="font-size: 0.72rem;">All Day</span>
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<div class="list-row">
|
<div class="list-row">
|
||||||
|
{% with reorder_url="/contacts/reorder", item_id=item.id %}
|
||||||
|
{% include 'partials/reorder_arrows.html' %}
|
||||||
|
{% endwith %}
|
||||||
<span class="row-title"><a href="/contacts/{{ item.id }}">{{ item.first_name }} {{ item.last_name or '' }}</a></span>
|
<span class="row-title"><a href="/contacts/{{ item.id }}">{{ item.first_name }} {{ item.last_name or '' }}</a></span>
|
||||||
{% if item.company %}<span class="row-tag">{{ item.company }}</span>{% endif %}
|
{% if item.company %}<span class="row-tag">{{ item.company }}</span>{% endif %}
|
||||||
{% if item.role %}<span class="row-meta">{{ item.role }}</span>{% endif %}
|
{% if item.role %}<span class="row-meta">{{ item.role }}</span>{% endif %}
|
||||||
|
|||||||
@@ -25,6 +25,9 @@
|
|||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<div class="list-row">
|
<div class="list-row">
|
||||||
|
{% with reorder_url="/decisions/reorder", item_id=item.id %}
|
||||||
|
{% include 'partials/reorder_arrows.html' %}
|
||||||
|
{% endwith %}
|
||||||
<span class="row-title"><a href="/decisions/{{ item.id }}">{{ item.title }}</a></span>
|
<span class="row-title"><a href="/decisions/{{ item.id }}">{{ item.title }}</a></span>
|
||||||
<span class="status-badge status-{{ item.status }}">{{ item.status }}</span>
|
<span class="status-badge status-{{ item.status }}">{{ item.status }}</span>
|
||||||
<span class="row-tag impact-{{ item.impact }}">{{ item.impact }}</span>
|
<span class="row-tag impact-{{ item.impact }}">{{ item.impact }}</span>
|
||||||
|
|||||||
@@ -16,6 +16,9 @@
|
|||||||
{% if items %}
|
{% if items %}
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<div class="focus-item {{ 'completed' if item.completed }}">
|
<div class="focus-item {{ 'completed' if item.completed }}">
|
||||||
|
{% with reorder_url="/focus/reorder", item_id=item.id, extra_fields={"focus_date": focus_date|string} %}
|
||||||
|
{% include 'partials/reorder_arrows.html' %}
|
||||||
|
{% endwith %}
|
||||||
<form action="/focus/{{ item.id }}/toggle" method="post" style="display:inline">
|
<form action="/focus/{{ item.id }}/toggle" method="post" style="display:inline">
|
||||||
<div class="row-check">
|
<div class="row-check">
|
||||||
<input type="checkbox" id="f-{{ item.id }}" {{ 'checked' if item.completed }} onchange="this.form.submit()">
|
<input type="checkbox" id="f-{{ item.id }}" {{ 'checked' if item.completed }} onchange="this.form.submit()">
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<div class="list-row">
|
<div class="list-row">
|
||||||
|
{% with reorder_url="/links/reorder", item_id=item.id %}
|
||||||
|
{% include 'partials/reorder_arrows.html' %}
|
||||||
|
{% endwith %}
|
||||||
{% if item.domain_color %}<span class="row-domain-tag" style="background:{{ item.domain_color }}22;color:{{ item.domain_color }}">{{ item.domain_name }}</span>{% endif %}
|
{% if item.domain_color %}<span class="row-domain-tag" style="background:{{ item.domain_color }}22;color:{{ item.domain_color }}">{{ item.domain_name }}</span>{% endif %}
|
||||||
<span class="row-title"><a href="{{ item.url }}" target="_blank">{{ item.label }}</a></span>
|
<span class="row-title"><a href="{{ item.url }}" target="_blank">{{ item.label }}</a></span>
|
||||||
{% if item.project_name %}<span class="row-tag">{{ item.project_name }}</span>{% endif %}
|
{% if item.project_name %}<span class="row-tag">{{ item.project_name }}</span>{% endif %}
|
||||||
|
|||||||
@@ -46,6 +46,9 @@
|
|||||||
<div class="card mt-2">
|
<div class="card mt-2">
|
||||||
{% for li in list_items %}
|
{% for li in list_items %}
|
||||||
<div class="list-row {{ 'completed' if li.completed }}">
|
<div class="list-row {{ 'completed' if li.completed }}">
|
||||||
|
{% with reorder_url="/lists/" ~ item.id ~ "/items/reorder", item_id=li.id %}
|
||||||
|
{% include 'partials/reorder_arrows.html' %}
|
||||||
|
{% endwith %}
|
||||||
{% if item.list_type == 'checklist' %}
|
{% if item.list_type == 'checklist' %}
|
||||||
<div class="row-check">
|
<div class="row-check">
|
||||||
<form action="/lists/{{ item.id }}/items/{{ li.id }}/toggle" method="post" style="display:inline">
|
<form action="/lists/{{ item.id }}/items/{{ li.id }}/toggle" method="post" style="display:inline">
|
||||||
@@ -68,6 +71,9 @@
|
|||||||
<!-- Child items -->
|
<!-- Child items -->
|
||||||
{% for child in child_map.get(li.id|string, []) %}
|
{% for child in child_map.get(li.id|string, []) %}
|
||||||
<div class="list-row {{ 'completed' if child.completed }}" style="padding-left: 48px;">
|
<div class="list-row {{ 'completed' if child.completed }}" style="padding-left: 48px;">
|
||||||
|
{% with reorder_url="/lists/" ~ item.id ~ "/items/reorder", item_id=child.id, extra_fields={"parent_id": li.id|string} %}
|
||||||
|
{% include 'partials/reorder_arrows.html' %}
|
||||||
|
{% endwith %}
|
||||||
{% if item.list_type == 'checklist' %}
|
{% if item.list_type == 'checklist' %}
|
||||||
<div class="row-check">
|
<div class="row-check">
|
||||||
<form action="/lists/{{ item.id }}/items/{{ child.id }}/toggle" method="post" style="display:inline">
|
<form action="/lists/{{ item.id }}/items/{{ child.id }}/toggle" method="post" style="display:inline">
|
||||||
|
|||||||
@@ -25,6 +25,9 @@
|
|||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<div class="list-row">
|
<div class="list-row">
|
||||||
|
{% with reorder_url="/lists/reorder", item_id=item.id %}
|
||||||
|
{% include 'partials/reorder_arrows.html' %}
|
||||||
|
{% endwith %}
|
||||||
<span class="row-title"><a href="/lists/{{ item.id }}">{{ item.name }}</a></span>
|
<span class="row-title"><a href="/lists/{{ item.id }}">{{ item.name }}</a></span>
|
||||||
<span class="row-meta">
|
<span class="row-meta">
|
||||||
{{ item.completed_count }}/{{ item.item_count }} items
|
{{ item.completed_count }}/{{ item.item_count }} items
|
||||||
|
|||||||
@@ -18,6 +18,9 @@
|
|||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<div class="list-row">
|
<div class="list-row">
|
||||||
|
{% with reorder_url="/meetings/reorder", item_id=item.id %}
|
||||||
|
{% include 'partials/reorder_arrows.html' %}
|
||||||
|
{% endwith %}
|
||||||
<span class="row-title"><a href="/meetings/{{ item.id }}">{{ item.title }}</a></span>
|
<span class="row-title"><a href="/meetings/{{ item.id }}">{{ item.title }}</a></span>
|
||||||
<span class="row-meta">{{ item.meeting_date }}</span>
|
<span class="row-meta">{{ item.meeting_date }}</span>
|
||||||
{% if item.location %}
|
{% if item.location %}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<div class="list-row">
|
<div class="list-row">
|
||||||
|
{% with reorder_url="/notes/reorder", item_id=item.id %}
|
||||||
|
{% include 'partials/reorder_arrows.html' %}
|
||||||
|
{% endwith %}
|
||||||
{% if item.domain_color %}<span class="row-domain-tag" style="background:{{ item.domain_color }}22;color:{{ item.domain_color }}">{{ item.domain_name }}</span>{% endif %}
|
{% if item.domain_color %}<span class="row-domain-tag" style="background:{{ item.domain_color }}22;color:{{ item.domain_color }}">{{ item.domain_name }}</span>{% endif %}
|
||||||
<span class="row-title"><a href="/notes/{{ item.id }}">{{ item.title }}</a></span>
|
<span class="row-title"><a href="/notes/{{ item.id }}">{{ item.title }}</a></span>
|
||||||
{% if item.project_name %}<span class="row-tag">{{ item.project_name }}</span>{% endif %}
|
{% if item.project_name %}<span class="row-tag">{{ item.project_name }}</span>{% endif %}
|
||||||
|
|||||||
30
templates/partials/reorder_arrows.html
Normal file
30
templates/partials/reorder_arrows.html
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{#
|
||||||
|
Reorder grip handle. Include with:
|
||||||
|
{% with reorder_url="/focus/reorder", item_id=item.id %}
|
||||||
|
{% include 'partials/reorder_arrows.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
Required context vars:
|
||||||
|
reorder_url - POST endpoint for the reorder action
|
||||||
|
item_id - ID of the current item
|
||||||
|
Optional:
|
||||||
|
extra_fields - dict of extra hidden fields (e.g. {"focus_date": "2026-03-03"})
|
||||||
|
#}
|
||||||
|
<span class="reorder-grip">
|
||||||
|
<form action="{{ reorder_url }}" method="post">
|
||||||
|
<input type="hidden" name="item_id" value="{{ item_id }}">
|
||||||
|
<input type="hidden" name="direction" value="up">
|
||||||
|
{% if extra_fields %}{% for k, v in extra_fields.items() %}
|
||||||
|
<input type="hidden" name="{{ k }}" value="{{ v }}">
|
||||||
|
{% endfor %}{% endif %}
|
||||||
|
<button type="submit" class="grip-btn" title="Move up">⠛</button>
|
||||||
|
</form>
|
||||||
|
<form action="{{ reorder_url }}" method="post">
|
||||||
|
<input type="hidden" name="item_id" value="{{ item_id }}">
|
||||||
|
<input type="hidden" name="direction" value="down">
|
||||||
|
{% if extra_fields %}{% for k, v in extra_fields.items() %}
|
||||||
|
<input type="hidden" name="{{ k }}" value="{{ v }}">
|
||||||
|
{% endfor %}{% endif %}
|
||||||
|
<button type="submit" class="grip-btn" title="Move down">⠓</button>
|
||||||
|
</form>
|
||||||
|
</span>
|
||||||
@@ -53,6 +53,9 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<div class="list-row {{ 'completed' if item.status in ['done', 'cancelled'] }} {{ 'timer-active' if running_task_id and item.id|string == running_task_id }}">
|
<div class="list-row {{ 'completed' if item.status in ['done', 'cancelled'] }} {{ 'timer-active' if running_task_id and item.id|string == running_task_id }}">
|
||||||
|
{% with reorder_url="/tasks/reorder", item_id=item.id %}
|
||||||
|
{% include 'partials/reorder_arrows.html' %}
|
||||||
|
{% endwith %}
|
||||||
<div class="row-check">
|
<div class="row-check">
|
||||||
<form action="/tasks/{{ item.id }}/toggle" method="post" style="display:inline">
|
<form action="/tasks/{{ item.id }}/toggle" method="post" style="display:inline">
|
||||||
<input type="checkbox" id="check-{{ item.id }}" {{ 'checked' if item.status == 'done' }}
|
<input type="checkbox" id="check-{{ item.id }}" {{ 'checked' if item.status == 'done' }}
|
||||||
|
|||||||
Reference in New Issue
Block a user