Compare commits
6 Commits
ff9be1249a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f88c6e5fd4 | |||
| c42fe9dc13 | |||
| 34b232de5f | |||
| 497436a0a3 | |||
| 75b055299a | |||
| 41b974c804 |
@@ -248,3 +248,72 @@ class BaseRepository:
|
||||
text(f"UPDATE {self.table} SET sort_order = :order WHERE id = :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)
|
||||
await repo.soft_delete(appointment_id)
|
||||
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)
|
||||
await repo.soft_delete(contact_id)
|
||||
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)
|
||||
await repo.soft_delete(decision_id)
|
||||
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)
|
||||
|
||||
152
routers/files.py
152
routers/files.py
@@ -91,9 +91,11 @@ async def sync_files(db: AsyncSession):
|
||||
))
|
||||
db_records = [dict(r._mapping) for r in result]
|
||||
|
||||
# Build lookup sets
|
||||
# Build lookup maps
|
||||
all_db_paths = {r["storage_path"] for r in db_records}
|
||||
active_db_paths = {r["storage_path"] for r in db_records if not r["is_deleted"]}
|
||||
deleted_on_disk = {r["storage_path"]: r for r in db_records
|
||||
if r["is_deleted"] and r["storage_path"] in disk_files}
|
||||
|
||||
# New on disk, not in DB at all → create record
|
||||
new_files = disk_files - all_db_paths
|
||||
@@ -116,6 +118,12 @@ async def sync_files(db: AsyncSession):
|
||||
})
|
||||
added += 1
|
||||
|
||||
# Soft-deleted in DB but still on disk → restore
|
||||
for rel_path, record in deleted_on_disk.items():
|
||||
repo = BaseRepository("files", db)
|
||||
await repo.restore(record["id"])
|
||||
added += 1
|
||||
|
||||
# Active in DB but missing from disk → soft-delete
|
||||
missing_files = active_db_paths - disk_files
|
||||
for record in db_records:
|
||||
@@ -127,10 +135,36 @@ async def sync_files(db: AsyncSession):
|
||||
return {"added": added, "removed": removed}
|
||||
|
||||
|
||||
SORT_OPTIONS = {
|
||||
"path": "storage_path ASC",
|
||||
"path_desc": "storage_path DESC",
|
||||
"name": "original_filename ASC",
|
||||
"name_desc": "original_filename DESC",
|
||||
"date": "created_at DESC",
|
||||
"date_asc": "created_at ASC",
|
||||
}
|
||||
|
||||
# Map type filter values to mime prefixes/patterns
|
||||
TYPE_FILTERS = {
|
||||
"image": "image/%",
|
||||
"document": "application/pdf",
|
||||
"text": "text/%",
|
||||
"spreadsheet": "application/vnd.%sheet%",
|
||||
"archive": "application/%zip%",
|
||||
}
|
||||
|
||||
PER_PAGE = 50
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_files(
|
||||
request: Request,
|
||||
folder: Optional[str] = None,
|
||||
sort: Optional[str] = None,
|
||||
q: Optional[str] = None,
|
||||
file_type: Optional[str] = None,
|
||||
tag: Optional[str] = None,
|
||||
page: int = 1,
|
||||
context_type: Optional[str] = None,
|
||||
context_id: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
@@ -142,38 +176,88 @@ async def list_files(
|
||||
|
||||
folders = get_folders()
|
||||
|
||||
order_by = SORT_OPTIONS.get(sort, "storage_path ASC")
|
||||
|
||||
# Normalize folder param from form: " " (space) = root, "" = all, None = all
|
||||
if folder is not None:
|
||||
if folder.strip() == "" and folder != "":
|
||||
# Space-only value means root folder
|
||||
folder = ""
|
||||
elif folder.strip() == "":
|
||||
# Empty string means "all folders" (no filter)
|
||||
folder = None
|
||||
|
||||
# Build dynamic WHERE clauses
|
||||
where_clauses = ["f.is_deleted = false"]
|
||||
params = {}
|
||||
|
||||
if context_type and context_id:
|
||||
# Files attached to a specific entity
|
||||
result = await db.execute(text("""
|
||||
where_clauses.append("fm.context_type = :ct AND fm.context_id = :cid")
|
||||
params["ct"] = context_type
|
||||
params["cid"] = context_id
|
||||
|
||||
if folder is not None:
|
||||
if folder == "":
|
||||
where_clauses.append("f.storage_path NOT LIKE '%/%'")
|
||||
else:
|
||||
where_clauses.append("f.storage_path LIKE :prefix")
|
||||
params["prefix"] = folder + "/%"
|
||||
|
||||
if q and q.strip():
|
||||
search_terms = q.strip().split()
|
||||
tsquery = " & ".join(f"{t}:*" for t in search_terms)
|
||||
where_clauses.append("f.search_vector @@ to_tsquery('english', :tsquery)")
|
||||
params["tsquery"] = tsquery
|
||||
|
||||
if file_type and file_type in TYPE_FILTERS:
|
||||
where_clauses.append("f.mime_type LIKE :mime_pattern")
|
||||
params["mime_pattern"] = TYPE_FILTERS[file_type]
|
||||
|
||||
if tag and tag.strip():
|
||||
where_clauses.append(":tag = ANY(f.tags)")
|
||||
params["tag"] = tag.strip()
|
||||
|
||||
where_sql = " AND ".join(where_clauses)
|
||||
|
||||
# Count total for pagination
|
||||
if context_type and context_id:
|
||||
count_sql = f"""
|
||||
SELECT COUNT(*) FROM files f
|
||||
JOIN file_mappings fm ON fm.file_id = f.id
|
||||
WHERE {where_sql}
|
||||
"""
|
||||
else:
|
||||
count_sql = f"SELECT COUNT(*) FROM files f WHERE {where_sql}"
|
||||
|
||||
total = (await db.execute(text(count_sql), params)).scalar()
|
||||
|
||||
# Paginate
|
||||
if page < 1:
|
||||
page = 1
|
||||
offset = (page - 1) * PER_PAGE
|
||||
total_pages = max(1, (total + PER_PAGE - 1) // PER_PAGE)
|
||||
|
||||
# Query
|
||||
if context_type and context_id:
|
||||
query_sql = f"""
|
||||
SELECT f.*, fm.context_type, fm.context_id
|
||||
FROM files f
|
||||
JOIN file_mappings fm ON fm.file_id = f.id
|
||||
WHERE f.is_deleted = false AND fm.context_type = :ct AND fm.context_id = :cid
|
||||
ORDER BY f.created_at DESC
|
||||
"""), {"ct": context_type, "cid": context_id})
|
||||
elif folder is not None:
|
||||
if folder == "":
|
||||
# Root folder: files with no directory separator in storage_path
|
||||
result = await db.execute(text("""
|
||||
SELECT * FROM files
|
||||
WHERE is_deleted = false AND storage_path NOT LIKE '%/%'
|
||||
ORDER BY created_at DESC
|
||||
"""))
|
||||
else:
|
||||
# Specific folder: storage_path starts with folder/
|
||||
result = await db.execute(text("""
|
||||
SELECT * FROM files
|
||||
WHERE is_deleted = false AND storage_path LIKE :prefix
|
||||
ORDER BY created_at DESC
|
||||
"""), {"prefix": folder + "/%"})
|
||||
WHERE {where_sql}
|
||||
ORDER BY {order_by}
|
||||
LIMIT :lim OFFSET :off
|
||||
"""
|
||||
else:
|
||||
# All files
|
||||
result = await db.execute(text("""
|
||||
SELECT * FROM files
|
||||
WHERE is_deleted = false
|
||||
ORDER BY created_at DESC
|
||||
"""))
|
||||
query_sql = f"""
|
||||
SELECT f.* FROM files f
|
||||
WHERE {where_sql}
|
||||
ORDER BY {order_by}
|
||||
LIMIT :lim OFFSET :off
|
||||
"""
|
||||
|
||||
params["lim"] = PER_PAGE
|
||||
params["off"] = offset
|
||||
result = await db.execute(text(query_sql), params)
|
||||
items = [dict(r._mapping) for r in result]
|
||||
|
||||
# Add derived folder field for display
|
||||
@@ -181,9 +265,23 @@ async def list_files(
|
||||
dirname = os.path.dirname(item["storage_path"])
|
||||
item["folder"] = dirname if dirname else "/"
|
||||
|
||||
# Get all unique tags for the tag filter dropdown
|
||||
tag_result = await db.execute(text(
|
||||
"SELECT DISTINCT unnest(tags) AS tag FROM files WHERE is_deleted = false AND tags IS NOT NULL ORDER BY tag"
|
||||
))
|
||||
all_tags = [r._mapping["tag"] for r in tag_result]
|
||||
|
||||
return templates.TemplateResponse("files.html", {
|
||||
"request": request, "sidebar": sidebar, "items": items,
|
||||
"folders": folders, "current_folder": folder,
|
||||
"current_sort": sort or "path",
|
||||
"current_q": q or "",
|
||||
"current_type": file_type or "",
|
||||
"current_tag": tag or "",
|
||||
"current_page": page,
|
||||
"total_pages": total_pages,
|
||||
"total_files": total,
|
||||
"all_tags": all_tags,
|
||||
"sync_result": sync_result,
|
||||
"context_type": context_type or "",
|
||||
"context_id": context_id or "",
|
||||
|
||||
@@ -121,6 +121,20 @@ async def add_to_focus(
|
||||
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")
|
||||
async def toggle_focus(focus_id: str, request: Request, db: AsyncSession = Depends(get_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)
|
||||
await repo.soft_delete(link_id)
|
||||
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"
|
||||
), {"cid": contact_id, "lid": list_id})
|
||||
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"
|
||||
), {"cid": contact_id, "mid": meeting_id})
|
||||
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)
|
||||
referer = request.headers.get("referer", "/notes")
|
||||
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"
|
||||
), {"cid": contact_id, "tid": task_id})
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.80rem;
|
||||
gap: 8px;
|
||||
padding: 7px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.92rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition);
|
||||
@@ -201,7 +201,7 @@ a:hover { color: var(--accent-hover); }
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
font-size: 0.85rem;
|
||||
font-size: 0.80rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
@@ -233,7 +233,7 @@ a:hover { color: var(--accent-hover); }
|
||||
.project-link {
|
||||
display: block;
|
||||
padding: 4px 10px 4px 18px;
|
||||
font-size: 0.85rem;
|
||||
font-size: 0.80rem;
|
||||
color: var(--text-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
white-space: nowrap;
|
||||
@@ -401,8 +401,8 @@ a:hover { color: var(--accent-hover); }
|
||||
.list-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
transition: background var(--transition);
|
||||
}
|
||||
@@ -466,6 +466,7 @@ a:hover { color: var(--accent-hover); }
|
||||
|
||||
.row-title {
|
||||
flex: 1;
|
||||
font-size: 0.80rem;
|
||||
font-weight: 500;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
@@ -735,6 +736,9 @@ a:hover { color: var(--accent-hover); }
|
||||
.detail-body {
|
||||
line-height: 1.75;
|
||||
color: var(--text);
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.detail-body p { margin-bottom: 1em; }
|
||||
@@ -792,17 +796,15 @@ a:hover { color: var(--accent-hover); }
|
||||
.capture-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: 8px;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
transition: background var(--transition);
|
||||
}
|
||||
|
||||
.capture-text {
|
||||
flex: 1;
|
||||
font-size: 0.92rem;
|
||||
font-size: 0.80rem;
|
||||
}
|
||||
|
||||
.capture-actions {
|
||||
@@ -815,20 +817,17 @@ a:hover { color: var(--accent-hover); }
|
||||
.focus-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: 6px;
|
||||
transition: all var(--transition);
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
transition: background var(--transition);
|
||||
}
|
||||
|
||||
.focus-item:hover { border-color: var(--accent); }
|
||||
.focus-item:hover { background: var(--surface2); }
|
||||
.focus-item.completed { opacity: 0.6; }
|
||||
.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); }
|
||||
|
||||
/* ---- Alerts ---- */
|
||||
@@ -1108,6 +1107,35 @@ a:hover { color: var(--accent-hover); }
|
||||
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 ---- */
|
||||
::-webkit-scrollbar { width: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{% set appt_date = appt.start_at.strftime('%A, %B %-d, %Y') if appt.start_at else 'No Date' %}
|
||||
{% if appt_date != current_date.value %}
|
||||
{% if not loop.first %}</div>{% endif %}
|
||||
<div class="date-group-label" style="padding: 12px 12px 4px; font-size: 0.78rem; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; {% if not loop.first %}border-top: 1px solid var(--border); margin-top: 4px;{% endif %}">
|
||||
<div class="date-group-label" style="padding: 6px 12px 4px; font-size: 0.78rem; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; {% if not loop.first %}border-top: 1px solid var(--border); margin-top: 4px;{% endif %}">
|
||||
{{ appt_date }}
|
||||
</div>
|
||||
<div>
|
||||
@@ -30,6 +30,9 @@
|
||||
{% endif %}
|
||||
|
||||
<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;">
|
||||
{% if appt.all_day %}
|
||||
<span class="status-badge status-active" style="font-size: 0.72rem;">All Day</span>
|
||||
|
||||
@@ -53,11 +53,12 @@
|
||||
</div>
|
||||
|
||||
{% if items %}
|
||||
<div class="card">
|
||||
{% for item in items %}
|
||||
|
||||
{# Batch header #}
|
||||
{% if item._batch_first and item.import_batch_id %}
|
||||
<div class="flex items-center justify-between mb-2 mt-3" style="padding:6px 12px;background:var(--surface2);border-radius:var(--radius-sm)">
|
||||
<div class="flex items-center justify-between" style="padding:6px 12px;background:var(--surface2);">
|
||||
<span class="text-xs text-muted">Batch · {{ batches[item.import_batch_id|string] }} items</span>
|
||||
<form action="/capture/batch/{{ item.import_batch_id }}/undo" method="post" style="display:inline" data-confirm="Delete all items in this batch?">
|
||||
<button class="btn btn-ghost btn-xs" style="color:var(--red)">Undo batch</button>
|
||||
@@ -99,6 +100,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">📥</div>
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
<div class="card">
|
||||
{% for item in items %}
|
||||
<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>
|
||||
{% if item.company %}<span class="row-tag">{{ item.company }}</span>{% endif %}
|
||||
{% if item.role %}<span class="row-meta">{{ item.role }}</span>{% endif %}
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
<div class="card mt-3">
|
||||
{% for item in items %}
|
||||
<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="status-badge status-{{ item.status }}">{{ item.status }}</span>
|
||||
<span class="row-tag impact-{{ item.impact }}">{{ item.impact }}</span>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Files<span class="page-count">{{ items|length }}</span></h1>
|
||||
<h1 class="page-title">Files<span class="page-count">{{ total_files }}</span></h1>
|
||||
<div class="flex gap-2">
|
||||
<form action="/files/sync" method="post" style="display:inline">
|
||||
<button type="submit" class="btn btn-secondary">Sync Files</button>
|
||||
@@ -16,47 +16,140 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if folders %}
|
||||
<div class="filter-bar" style="display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 16px;">
|
||||
<a href="/files" class="btn btn-xs {{ 'btn-primary' if current_folder is none else 'btn-ghost' }}">All</a>
|
||||
<a href="/files?folder=" class="btn btn-xs {{ 'btn-primary' if current_folder is not none and current_folder == '' else 'btn-ghost' }}">/</a>
|
||||
{% for f in folders %}
|
||||
<a href="/files?folder={{ f }}" class="btn btn-xs {{ 'btn-primary' if current_folder == f else 'btn-ghost' }}">{{ f }}</a>
|
||||
<form class="filters-bar" method="get" action="/files" style="display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 16px;">
|
||||
<input type="text" name="q" value="{{ current_q }}" class="form-input" placeholder="Search files..." style="max-width: 220px; padding: 6px 10px; font-size: 0.85rem;">
|
||||
|
||||
<select name="folder" class="form-input" style="max-width: 200px; padding: 6px 10px; font-size: 0.85rem;" onchange="this.form.submit()">
|
||||
<option value="" {{ 'selected' if current_folder is none }}>All folders</option>
|
||||
<option value=" " {{ 'selected' if current_folder is not none and current_folder == '' }}>/ (root)</option>
|
||||
{% for f in folders %}
|
||||
<option value="{{ f }}" {{ 'selected' if current_folder == f }}>{% if '/' in f %} {{ f.split('/')[-1] }}{% else %}{{ f }}{% endif %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<select name="file_type" class="form-input" style="max-width: 160px; padding: 6px 10px; font-size: 0.85rem;" onchange="this.form.submit()">
|
||||
<option value="">All types</option>
|
||||
<option value="image" {{ 'selected' if current_type == 'image' }}>Images</option>
|
||||
<option value="document" {{ 'selected' if current_type == 'document' }}>Documents</option>
|
||||
<option value="text" {{ 'selected' if current_type == 'text' }}>Text</option>
|
||||
<option value="spreadsheet" {{ 'selected' if current_type == 'spreadsheet' }}>Spreadsheets</option>
|
||||
<option value="archive" {{ 'selected' if current_type == 'archive' }}>Archives</option>
|
||||
</select>
|
||||
|
||||
{% if all_tags %}
|
||||
<select name="tag" class="form-input" style="max-width: 160px; padding: 6px 10px; font-size: 0.85rem;" onchange="this.form.submit()">
|
||||
<option value="">All tags</option>
|
||||
{% for t in all_tags %}
|
||||
<option value="{{ t }}" {{ 'selected' if current_tag == t }}>{{ t }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
|
||||
{% if current_q or current_type or current_tag or current_folder is not none %}
|
||||
<input type="hidden" name="sort" value="{{ current_sort }}">
|
||||
<button type="submit" class="btn btn-primary btn-xs">Search</button>
|
||||
<a href="/files" class="btn btn-ghost btn-xs">Clear</a>
|
||||
{% else %}
|
||||
<button type="submit" class="btn btn-primary btn-xs">Search</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
{% set qp = [] %}
|
||||
{% if current_folder is not none %}{% if current_folder == '' %}{{ qp.append('folder= ') or '' }}{% else %}{{ qp.append('folder=' ~ current_folder) or '' }}{% endif %}{% endif %}
|
||||
{% if current_q %}{{ qp.append('q=' ~ current_q) or '' }}{% endif %}
|
||||
{% if current_type %}{{ qp.append('file_type=' ~ current_type) or '' }}{% endif %}
|
||||
{% if current_tag %}{{ qp.append('tag=' ~ current_tag) or '' }}{% endif %}
|
||||
{% set filter_qs = qp | join('&') %}
|
||||
{% set sort_base = '/files?' ~ (filter_qs ~ '&' if filter_qs else '') %}
|
||||
|
||||
{% if items %}
|
||||
<div class="card" style="overflow-x: auto;">
|
||||
<table class="data-table" style="width: 100%; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr style="border-bottom: 1px solid var(--border); text-align: left;">
|
||||
<th style="padding: 6px 12px; font-weight: 600; font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em;">
|
||||
<a href="{{ sort_base }}sort={{ 'path_desc' if current_sort == 'path' else 'path' }}" style="color: var(--muted); text-decoration: none;">
|
||||
Path {{ '▲' if current_sort == 'path' else ('▼' if current_sort == 'path_desc' else '') }}
|
||||
</a>
|
||||
</th>
|
||||
<th style="padding: 6px 12px; font-weight: 600; font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em;">
|
||||
<a href="{{ sort_base }}sort={{ 'name_desc' if current_sort == 'name' else 'name' }}" style="color: var(--muted); text-decoration: none;">
|
||||
Name {{ '▲' if current_sort == 'name' else ('▼' if current_sort == 'name_desc' else '') }}
|
||||
</a>
|
||||
</th>
|
||||
<th style="padding: 6px 12px; font-weight: 600; font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em;">Type</th>
|
||||
<th style="padding: 6px 12px; font-weight: 600; font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em;">Size</th>
|
||||
<th style="padding: 6px 12px; font-weight: 600; font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em;">
|
||||
<a href="{{ sort_base }}sort={{ 'date_asc' if current_sort == 'date' else 'date' }}" style="color: var(--muted); text-decoration: none;">
|
||||
Date {{ '▼' if current_sort == 'date' else ('▲' if current_sort == 'date_asc' else '') }}
|
||||
</a>
|
||||
</th>
|
||||
<th style="padding: 6px 12px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr style="border-bottom: 1px solid var(--border);">
|
||||
<td style="padding: 6px 12px; color: var(--muted); font-size: 0.80rem;">{{ item.folder }}</td>
|
||||
<td style="padding: 6px 12px; font-size: 0.80rem;">
|
||||
<a href="/files/{{ item.id }}/preview" style="color: var(--accent);">{{ item.original_filename }}</a>
|
||||
</td>
|
||||
<td style="padding: 6px 12px;">
|
||||
{% if item.mime_type %}<span class="row-tag">{{ item.mime_type.split('/')|last }}</span>{% endif %}
|
||||
</td>
|
||||
<td style="padding: 6px 12px; color: var(--muted); font-size: 0.80rem; white-space: nowrap;">
|
||||
{% if item.size_bytes %}{{ "%.1f"|format(item.size_bytes / 1024) }} KB{% endif %}
|
||||
</td>
|
||||
<td style="padding: 6px 12px; color: var(--muted); font-size: 0.80rem; white-space: nowrap;">
|
||||
{{ item.created_at.strftime('%Y-%m-%d') if item.created_at else '' }}
|
||||
</td>
|
||||
<td style="padding: 6px 12px; text-align: right; white-space: nowrap;">
|
||||
<a href="/files/{{ item.id }}/download" class="btn btn-ghost btn-xs">Download</a>
|
||||
<form action="/files/{{ item.id }}/delete" method="post" data-confirm="Delete this file?" style="display:inline">
|
||||
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if total_pages > 1 %}
|
||||
{% set page_base = sort_base ~ 'sort=' ~ current_sort ~ '&' %}
|
||||
<div style="display: flex; justify-content: center; align-items: center; gap: 8px; margin-top: 16px;">
|
||||
{% if current_page > 1 %}
|
||||
<a href="{{ page_base }}page={{ current_page - 1 }}" class="btn btn-ghost btn-xs">Prev</a>
|
||||
{% endif %}
|
||||
|
||||
{% for p in range(1, total_pages + 1) %}
|
||||
{% if p == current_page %}
|
||||
<span class="btn btn-primary btn-xs" style="pointer-events: none;">{{ p }}</span>
|
||||
{% elif p <= 3 or p >= total_pages - 2 or (p >= current_page - 1 and p <= current_page + 1) %}
|
||||
<a href="{{ page_base }}page={{ p }}" class="btn btn-ghost btn-xs">{{ p }}</a>
|
||||
{% elif p == 4 and current_page > 5 %}
|
||||
<span style="color: var(--muted);">...</span>
|
||||
{% elif p == total_pages - 3 and current_page < total_pages - 4 %}
|
||||
<span style="color: var(--muted);">...</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if current_page < total_pages %}
|
||||
<a href="{{ page_base }}page={{ current_page + 1 }}" class="btn btn-ghost btn-xs">Next</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if items %}
|
||||
<div class="card">
|
||||
{% for item in items %}
|
||||
<div class="list-row">
|
||||
<span class="row-title">
|
||||
<a href="/files/{{ item.id }}/preview">{{ item.original_filename }}</a>
|
||||
</span>
|
||||
<span class="row-meta" style="color: var(--muted); font-size: 0.8rem;">{{ item.folder }}</span>
|
||||
{% if item.mime_type %}
|
||||
<span class="row-tag">{{ item.mime_type.split('/')|last }}</span>
|
||||
{% endif %}
|
||||
{% if item.size_bytes %}
|
||||
<span class="row-meta">{{ "%.1f"|format(item.size_bytes / 1024) }} KB</span>
|
||||
{% endif %}
|
||||
{% if item.description %}
|
||||
<span class="row-meta">{{ item.description[:50] }}</span>
|
||||
{% endif %}
|
||||
<div class="row-actions">
|
||||
<a href="/files/{{ item.id }}/download" class="btn btn-ghost btn-xs">Download</a>
|
||||
<form action="/files/{{ item.id }}/delete" method="post" data-confirm="Delete this file?" style="display:inline">
|
||||
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">📁</div>
|
||||
{% if current_q or current_type or current_tag %}
|
||||
<div class="empty-state-text">No files match your filters</div>
|
||||
<a href="/files" class="btn btn-secondary">Clear Filters</a>
|
||||
{% else %}
|
||||
<div class="empty-state-text">No files{{ ' in this folder' if current_folder is not none else ' uploaded yet' }}</div>
|
||||
<a href="/files/upload" class="btn btn-primary">Upload First File</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -14,8 +14,12 @@
|
||||
|
||||
<!-- Focus items -->
|
||||
{% if items %}
|
||||
<div class="card">
|
||||
{% for item in items %}
|
||||
<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">
|
||||
<div class="row-check">
|
||||
<input type="checkbox" id="f-{{ item.id }}" {{ 'checked' if item.completed }} onchange="this.form.submit()">
|
||||
@@ -32,6 +36,7 @@
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state mb-4"><div class="empty-state-text">No focus items for this day</div></div>
|
||||
{% endif %}
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
<div class="card">
|
||||
{% for item in items %}
|
||||
<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 %}
|
||||
<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 %}
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
<div class="card mt-2">
|
||||
{% for li in list_items %}
|
||||
<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' %}
|
||||
<div class="row-check">
|
||||
<form action="/lists/{{ item.id }}/items/{{ li.id }}/toggle" method="post" style="display:inline">
|
||||
@@ -68,6 +71,9 @@
|
||||
<!-- Child items -->
|
||||
{% for child in child_map.get(li.id|string, []) %}
|
||||
<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' %}
|
||||
<div class="row-check">
|
||||
<form action="/lists/{{ item.id }}/items/{{ child.id }}/toggle" method="post" style="display:inline">
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
<div class="card mt-3">
|
||||
{% for item in items %}
|
||||
<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-meta">
|
||||
{{ item.completed_count }}/{{ item.item_count }} items
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
<div class="card mt-3">
|
||||
{% for item in items %}
|
||||
<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-meta">{{ item.meeting_date }}</span>
|
||||
{% if item.location %}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% if item.body %}
|
||||
<div class="card"><div class="detail-body" style="white-space:pre-wrap;font-family:var(--font-body)">{{ item.body }}</div></div>
|
||||
<div class="card" style="overflow:hidden"><div class="detail-body" style="white-space:pre-wrap;font-family:var(--font-body);margin-right:20px">{{ item.body }}</div></div>
|
||||
{% else %}
|
||||
<div class="card"><div class="text-muted" style="padding:20px">No content yet. <a href="/notes/{{ item.id }}/edit">Start writing</a></div></div>
|
||||
{% endif %}
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
<div class="card">
|
||||
{% for item in items %}
|
||||
<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 %}
|
||||
<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 %}
|
||||
|
||||
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">
|
||||
{% 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 }}">
|
||||
{% with reorder_url="/tasks/reorder", item_id=item.id %}
|
||||
{% include 'partials/reorder_arrows.html' %}
|
||||
{% endwith %}
|
||||
<div class="row-check">
|
||||
<form action="/tasks/{{ item.id }}/toggle" method="post" style="display:inline">
|
||||
<input type="checkbox" id="check-{{ item.id }}" {{ 'checked' if item.status == 'done' }}
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
{% for entry in entries %}
|
||||
{% set entry_date = entry.start_at.strftime('%A, %B %-d') if entry.start_at else 'Unknown' %}
|
||||
{% if entry_date != current_date.value %}
|
||||
<div style="padding: 10px 12px 4px; font-size: 0.78rem; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; {% if not loop.first %}border-top: 1px solid var(--border); margin-top: 4px;{% endif %}">
|
||||
<div style="padding: 6px 12px 4px; font-size: 0.78rem; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; {% if not loop.first %}border-top: 1px solid var(--border); margin-top: 4px;{% endif %}">
|
||||
{{ entry_date }}
|
||||
</div>
|
||||
{% set current_date.value = entry_date %}
|
||||
|
||||
Reference in New Issue
Block a user