feat: daily focus search, show all tasks, and list item support
- Add list_item_id column to daily_focus (task_id now nullable, CHECK constraint ensures exactly one) - Remove LIMIT 50 + [:15] slice — show up to 200 items with "show more" at 25 - Add text search (ILIKE) for filtering available items - Add tab strip to switch between Tasks and List Items sources - Toggle syncs list_item completed status alongside task status - Graceful [Deleted] fallback for removed source items Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -161,7 +161,7 @@ class BaseRepository:
|
|||||||
"category", "instructions", "expected_output", "estimated_days",
|
"category", "instructions", "expected_output", "estimated_days",
|
||||||
"contact_id", "started_at",
|
"contact_id", "started_at",
|
||||||
"weekly_hours", "effective_from",
|
"weekly_hours", "effective_from",
|
||||||
"task_id", "meeting_id",
|
"task_id", "meeting_id", "list_item_id",
|
||||||
}
|
}
|
||||||
clean_data = {}
|
clean_data = {}
|
||||||
for k, v in data.items():
|
for k, v in data.items():
|
||||||
|
|||||||
107
routers/focus.py
107
routers/focus.py
@@ -1,4 +1,4 @@
|
|||||||
"""Daily Focus: date-scoped task commitment list."""
|
"""Daily Focus: date-scoped task/list-item commitment list."""
|
||||||
|
|
||||||
from fastapi import APIRouter, Request, Form, Depends
|
from fastapi import APIRouter, Request, Form, Depends
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
@@ -23,33 +23,49 @@ async def focus_view(
|
|||||||
domain_id: Optional[str] = None,
|
domain_id: Optional[str] = None,
|
||||||
area_id: Optional[str] = None,
|
area_id: Optional[str] = None,
|
||||||
project_id: Optional[str] = None,
|
project_id: Optional[str] = None,
|
||||||
|
search: Optional[str] = None,
|
||||||
|
source_type: Optional[str] = None,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
sidebar = await get_sidebar_data(db)
|
sidebar = await get_sidebar_data(db)
|
||||||
target_date = date.fromisoformat(focus_date) if focus_date else date.today()
|
target_date = date.fromisoformat(focus_date) if focus_date else date.today()
|
||||||
|
if not source_type:
|
||||||
|
source_type = "tasks"
|
||||||
|
|
||||||
|
# --- Focus items (both tasks and list items) ---
|
||||||
result = await db.execute(text("""
|
result = await db.execute(text("""
|
||||||
SELECT df.*, t.title, t.priority, t.status as task_status,
|
SELECT df.*,
|
||||||
|
t.title as title, t.priority, t.status as task_status,
|
||||||
t.project_id, t.due_date, t.estimated_minutes,
|
t.project_id, t.due_date, t.estimated_minutes,
|
||||||
p.name as project_name,
|
p.name as project_name,
|
||||||
d.name as domain_name, d.color as domain_color
|
d.name as domain_name, d.color as domain_color,
|
||||||
|
li.content as list_item_content, li.list_id as list_item_list_id,
|
||||||
|
li.completed as list_item_completed,
|
||||||
|
l.name as list_name
|
||||||
FROM daily_focus df
|
FROM daily_focus df
|
||||||
JOIN tasks t ON df.task_id = t.id
|
LEFT JOIN tasks t ON df.task_id = t.id
|
||||||
LEFT JOIN projects p ON t.project_id = p.id
|
LEFT JOIN projects p ON t.project_id = p.id
|
||||||
LEFT JOIN domains d ON t.domain_id = d.id
|
LEFT JOIN domains d ON t.domain_id = d.id
|
||||||
|
LEFT JOIN list_items li ON df.list_item_id = li.id
|
||||||
|
LEFT JOIN lists l ON li.list_id = l.id
|
||||||
WHERE df.focus_date = :target_date AND df.is_deleted = false
|
WHERE df.focus_date = :target_date AND df.is_deleted = false
|
||||||
ORDER BY df.sort_order, df.created_at
|
ORDER BY df.sort_order, df.created_at
|
||||||
"""), {"target_date": target_date})
|
"""), {"target_date": target_date})
|
||||||
items = [dict(r._mapping) for r in result]
|
items = [dict(r._mapping) for r in result]
|
||||||
|
|
||||||
# Available tasks to add (open, not already in today's focus)
|
# --- Available tasks ---
|
||||||
|
available_tasks = []
|
||||||
|
if source_type == "tasks":
|
||||||
avail_where = [
|
avail_where = [
|
||||||
"t.is_deleted = false",
|
"t.is_deleted = false",
|
||||||
"t.status NOT IN ('done', 'cancelled')",
|
"t.status NOT IN ('done', 'cancelled')",
|
||||||
"t.id NOT IN (SELECT task_id FROM daily_focus WHERE focus_date = :target_date AND is_deleted = false)",
|
"t.id NOT IN (SELECT task_id FROM daily_focus WHERE focus_date = :target_date AND is_deleted = false AND task_id IS NOT NULL)",
|
||||||
]
|
]
|
||||||
avail_params = {"target_date": target_date}
|
avail_params = {"target_date": target_date}
|
||||||
|
|
||||||
|
if search:
|
||||||
|
avail_where.append("t.title ILIKE :search")
|
||||||
|
avail_params["search"] = f"%{search}%"
|
||||||
if domain_id:
|
if domain_id:
|
||||||
avail_where.append("t.domain_id = :domain_id")
|
avail_where.append("t.domain_id = :domain_id")
|
||||||
avail_params["domain_id"] = domain_id
|
avail_params["domain_id"] = domain_id
|
||||||
@@ -61,7 +77,6 @@ async def focus_view(
|
|||||||
avail_params["project_id"] = project_id
|
avail_params["project_id"] = project_id
|
||||||
|
|
||||||
avail_sql = " AND ".join(avail_where)
|
avail_sql = " AND ".join(avail_where)
|
||||||
|
|
||||||
result = await db.execute(text(f"""
|
result = await db.execute(text(f"""
|
||||||
SELECT t.id, t.title, t.priority, t.due_date,
|
SELECT t.id, t.title, t.priority, t.due_date,
|
||||||
p.name as project_name, d.name as domain_name
|
p.name as project_name, d.name as domain_name
|
||||||
@@ -70,10 +85,47 @@ async def focus_view(
|
|||||||
LEFT JOIN domains d ON t.domain_id = d.id
|
LEFT JOIN domains d ON t.domain_id = d.id
|
||||||
WHERE {avail_sql}
|
WHERE {avail_sql}
|
||||||
ORDER BY t.priority ASC, t.due_date ASC NULLS LAST
|
ORDER BY t.priority ASC, t.due_date ASC NULLS LAST
|
||||||
LIMIT 50
|
LIMIT 200
|
||||||
"""), avail_params)
|
"""), avail_params)
|
||||||
available_tasks = [dict(r._mapping) for r in result]
|
available_tasks = [dict(r._mapping) for r in result]
|
||||||
|
|
||||||
|
# --- Available list items ---
|
||||||
|
available_list_items = []
|
||||||
|
if source_type == "list_items":
|
||||||
|
li_where = [
|
||||||
|
"li.is_deleted = false",
|
||||||
|
"li.completed = false",
|
||||||
|
"l.is_deleted = false",
|
||||||
|
"li.id NOT IN (SELECT list_item_id FROM daily_focus WHERE focus_date = :target_date AND is_deleted = false AND list_item_id IS NOT NULL)",
|
||||||
|
]
|
||||||
|
li_params = {"target_date": target_date}
|
||||||
|
|
||||||
|
if search:
|
||||||
|
li_where.append("li.content ILIKE :search")
|
||||||
|
li_params["search"] = f"%{search}%"
|
||||||
|
if domain_id:
|
||||||
|
li_where.append("l.domain_id = :domain_id")
|
||||||
|
li_params["domain_id"] = domain_id
|
||||||
|
if area_id:
|
||||||
|
li_where.append("l.area_id = :area_id")
|
||||||
|
li_params["area_id"] = area_id
|
||||||
|
if project_id:
|
||||||
|
li_where.append("l.project_id = :project_id")
|
||||||
|
li_params["project_id"] = project_id
|
||||||
|
|
||||||
|
li_sql = " AND ".join(li_where)
|
||||||
|
result = await db.execute(text(f"""
|
||||||
|
SELECT li.id, li.content, li.list_id, l.name as list_name,
|
||||||
|
d.name as domain_name
|
||||||
|
FROM list_items li
|
||||||
|
JOIN lists l ON li.list_id = l.id
|
||||||
|
LEFT JOIN domains d ON l.domain_id = d.id
|
||||||
|
WHERE {li_sql}
|
||||||
|
ORDER BY l.name ASC, li.sort_order ASC
|
||||||
|
LIMIT 200
|
||||||
|
"""), li_params)
|
||||||
|
available_list_items = [dict(r._mapping) for r in result]
|
||||||
|
|
||||||
# Estimated total minutes
|
# Estimated total minutes
|
||||||
total_est = sum(i.get("estimated_minutes") or 0 for i in items)
|
total_est = sum(i.get("estimated_minutes") or 0 for i in items)
|
||||||
|
|
||||||
@@ -87,13 +139,17 @@ async def focus_view(
|
|||||||
|
|
||||||
return templates.TemplateResponse("focus.html", {
|
return templates.TemplateResponse("focus.html", {
|
||||||
"request": request, "sidebar": sidebar,
|
"request": request, "sidebar": sidebar,
|
||||||
"items": items, "available_tasks": available_tasks,
|
"items": items,
|
||||||
|
"available_tasks": available_tasks,
|
||||||
|
"available_list_items": available_list_items,
|
||||||
"focus_date": target_date,
|
"focus_date": target_date,
|
||||||
"total_estimated": total_est,
|
"total_estimated": total_est,
|
||||||
"domains": domains, "areas": areas, "projects": projects,
|
"domains": domains, "areas": areas, "projects": projects,
|
||||||
"current_domain_id": domain_id or "",
|
"current_domain_id": domain_id or "",
|
||||||
"current_area_id": area_id or "",
|
"current_area_id": area_id or "",
|
||||||
"current_project_id": project_id or "",
|
"current_project_id": project_id or "",
|
||||||
|
"current_search": search or "",
|
||||||
|
"current_source_type": source_type,
|
||||||
"page_title": "Daily Focus", "active_nav": "focus",
|
"page_title": "Daily Focus", "active_nav": "focus",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -101,8 +157,9 @@ async def focus_view(
|
|||||||
@router.post("/add")
|
@router.post("/add")
|
||||||
async def add_to_focus(
|
async def add_to_focus(
|
||||||
request: Request,
|
request: Request,
|
||||||
task_id: str = Form(...),
|
|
||||||
focus_date: str = Form(...),
|
focus_date: str = Form(...),
|
||||||
|
task_id: Optional[str] = Form(None),
|
||||||
|
list_item_id: Optional[str] = Form(None),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
repo = BaseRepository("daily_focus", db)
|
repo = BaseRepository("daily_focus", db)
|
||||||
@@ -114,10 +171,17 @@ async def add_to_focus(
|
|||||||
"""), {"fd": parsed_date})
|
"""), {"fd": parsed_date})
|
||||||
next_order = result.scalar()
|
next_order = result.scalar()
|
||||||
|
|
||||||
await repo.create({
|
data = {
|
||||||
"task_id": task_id, "focus_date": parsed_date,
|
"focus_date": parsed_date,
|
||||||
"sort_order": next_order, "completed": False,
|
"sort_order": next_order,
|
||||||
})
|
"completed": False,
|
||||||
|
}
|
||||||
|
if task_id:
|
||||||
|
data["task_id"] = task_id
|
||||||
|
if list_item_id:
|
||||||
|
data["list_item_id"] = list_item_id
|
||||||
|
|
||||||
|
await repo.create(data)
|
||||||
return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303)
|
return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@@ -141,12 +205,25 @@ async def toggle_focus(focus_id: str, request: Request, db: AsyncSession = Depen
|
|||||||
item = await repo.get(focus_id)
|
item = await repo.get(focus_id)
|
||||||
if item:
|
if item:
|
||||||
await repo.update(focus_id, {"completed": not item["completed"]})
|
await repo.update(focus_id, {"completed": not item["completed"]})
|
||||||
# Also toggle the task status
|
if item["task_id"]:
|
||||||
|
# Sync task status
|
||||||
task_repo = BaseRepository("tasks", db)
|
task_repo = BaseRepository("tasks", db)
|
||||||
if not item["completed"]:
|
if not item["completed"]:
|
||||||
await task_repo.update(item["task_id"], {"status": "done", "completed_at": datetime.now(timezone.utc)})
|
await task_repo.update(item["task_id"], {"status": "done", "completed_at": datetime.now(timezone.utc)})
|
||||||
else:
|
else:
|
||||||
await task_repo.update(item["task_id"], {"status": "open", "completed_at": None})
|
await task_repo.update(item["task_id"], {"status": "open", "completed_at": None})
|
||||||
|
elif item["list_item_id"]:
|
||||||
|
# Sync list item completed status
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
if not item["completed"]:
|
||||||
|
await db.execute(text(
|
||||||
|
"UPDATE list_items SET completed = true, completed_at = :now, updated_at = :now WHERE id = :id"
|
||||||
|
), {"id": item["list_item_id"], "now": now})
|
||||||
|
else:
|
||||||
|
await db.execute(text(
|
||||||
|
"UPDATE list_items SET completed = false, completed_at = NULL, updated_at = :now WHERE id = :id"
|
||||||
|
), {"id": item["list_item_id"], "now": now})
|
||||||
|
await db.commit()
|
||||||
focus_date = item["focus_date"] if item else date.today()
|
focus_date = item["focus_date"] if item else date.today()
|
||||||
return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303)
|
return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303)
|
||||||
|
|
||||||
|
|||||||
@@ -26,11 +26,25 @@
|
|||||||
<label for="f-{{ item.id }}"></label>
|
<label for="f-{{ item.id }}"></label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
{% if item.task_id %}
|
||||||
<span class="priority-dot priority-{{ item.priority }}"></span>
|
<span class="priority-dot priority-{{ item.priority }}"></span>
|
||||||
|
{% if item.title %}
|
||||||
<a href="/tasks/{{ item.task_id }}" class="focus-title">{{ item.title }}</a>
|
<a href="/tasks/{{ item.task_id }}" class="focus-title">{{ item.title }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="focus-title" style="color:var(--muted)">[Deleted]</span>
|
||||||
|
{% endif %}
|
||||||
{% 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 %}
|
||||||
{% if item.estimated_minutes %}<span class="focus-meta">~{{ item.estimated_minutes }}min</span>{% endif %}
|
{% if item.estimated_minutes %}<span class="focus-meta">~{{ item.estimated_minutes }}min</span>{% endif %}
|
||||||
{% if item.due_date %}<span class="focus-meta">{{ item.due_date }}</span>{% endif %}
|
{% if item.due_date %}<span class="focus-meta">{{ item.due_date }}</span>{% endif %}
|
||||||
|
{% elif item.list_item_id %}
|
||||||
|
<span style="color:var(--muted);font-size:0.85rem;margin-right:4px">☰</span>
|
||||||
|
{% if item.list_item_content %}
|
||||||
|
<a href="/lists/{{ item.list_item_list_id }}" class="focus-title">{{ item.list_item_content }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="focus-title" style="color:var(--muted)">[Deleted]</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if item.list_name %}<span class="row-tag" style="background:var(--purple);color:#fff">{{ item.list_name }}</span>{% endif %}
|
||||||
|
{% endif %}
|
||||||
<form action="/focus/{{ item.id }}/remove" method="post" style="display:inline">
|
<form action="/focus/{{ item.id }}/remove" method="post" style="display:inline">
|
||||||
<button class="btn btn-ghost btn-xs" style="color:var(--red)" title="Remove from focus">×</button>
|
<button class="btn btn-ghost btn-xs" style="color:var(--red)" title="Remove from focus">×</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -41,34 +55,53 @@
|
|||||||
<div class="empty-state mb-4"><div class="empty-state-text">No focus items for this day</div></div>
|
<div class="empty-state mb-4"><div class="empty-state-text">No focus items for this day</div></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Add task to focus -->
|
<!-- Add to Focus -->
|
||||||
<div class="card mt-4">
|
<div class="card mt-4">
|
||||||
<div class="card-header"><h2 class="card-title">Add to Focus</h2></div>
|
<div class="card-header"><h2 class="card-title">Add to Focus</h2></div>
|
||||||
|
|
||||||
|
<!-- Tab strip -->
|
||||||
|
<div class="tab-strip" style="padding: 0 12px;">
|
||||||
|
<a href="/focus?focus_date={{ focus_date }}&source_type=tasks{% if current_search %}&search={{ current_search }}{% endif %}{% if current_domain_id %}&domain_id={{ current_domain_id }}{% endif %}{% if current_project_id %}&project_id={{ current_project_id }}{% endif %}"
|
||||||
|
class="tab-item {{ 'active' if current_source_type == 'tasks' }}">Tasks</a>
|
||||||
|
<a href="/focus?focus_date={{ focus_date }}&source_type=list_items{% if current_search %}&search={{ current_search }}{% endif %}{% if current_domain_id %}&domain_id={{ current_domain_id }}{% endif %}{% if current_project_id %}&project_id={{ current_project_id }}{% endif %}"
|
||||||
|
class="tab-item {{ 'active' if current_source_type == 'list_items' }}">List Items</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters -->
|
||||||
<form class="filters-bar" method="get" action="/focus" id="focus-filters" style="padding: 8px 12px; border-bottom: 1px solid var(--border);">
|
<form class="filters-bar" method="get" action="/focus" id="focus-filters" style="padding: 8px 12px; border-bottom: 1px solid var(--border);">
|
||||||
<input type="hidden" name="focus_date" value="{{ focus_date }}">
|
<input type="hidden" name="focus_date" value="{{ focus_date }}">
|
||||||
|
<input type="hidden" name="source_type" value="{{ current_source_type }}">
|
||||||
|
<input type="text" name="search" value="{{ current_search }}" placeholder="Search..." class="filter-select" style="min-width:150px">
|
||||||
<select name="domain_id" class="filter-select" id="focus-domain" onchange="this.form.submit()">
|
<select name="domain_id" class="filter-select" id="focus-domain" onchange="this.form.submit()">
|
||||||
<option value="">All Domains</option>
|
<option value="">All Domains</option>
|
||||||
{% for d in domains %}
|
{% for d in domains %}
|
||||||
<option value="{{ d.id }}" {{ 'selected' if current_domain_id == d.id|string }}>{{ d.name }}</option>
|
<option value="{{ d.id }}" {{ 'selected' if current_domain_id == d.id|string }}>{{ d.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
{% if current_source_type == 'tasks' %}
|
||||||
<select name="area_id" class="filter-select" onchange="this.form.submit()">
|
<select name="area_id" class="filter-select" onchange="this.form.submit()">
|
||||||
<option value="">All Areas</option>
|
<option value="">All Areas</option>
|
||||||
{% for a in areas %}
|
{% for a in areas %}
|
||||||
<option value="{{ a.id }}" {{ 'selected' if current_area_id == a.id|string }}>{{ a.name }}</option>
|
<option value="{{ a.id }}" {{ 'selected' if current_area_id == a.id|string }}>{{ a.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
{% endif %}
|
||||||
<select name="project_id" class="filter-select" id="focus-project" onchange="this.form.submit()">
|
<select name="project_id" class="filter-select" id="focus-project" onchange="this.form.submit()">
|
||||||
<option value="">All Projects</option>
|
<option value="">All Projects</option>
|
||||||
{% for p in projects %}
|
{% for p in projects %}
|
||||||
<option value="{{ p.id }}" {{ 'selected' if current_project_id == p.id|string }}>{{ p.name }}</option>
|
<option value="{{ p.id }}" {{ 'selected' if current_project_id == p.id|string }}>{{ p.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
<button type="submit" class="btn btn-ghost btn-xs">Search</button>
|
||||||
|
{% if current_search or current_domain_id or current_area_id or current_project_id %}
|
||||||
|
<a href="/focus?focus_date={{ focus_date }}&source_type={{ current_source_type }}" class="btn btn-ghost btn-xs" style="color:var(--red)">Clear</a>
|
||||||
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% for t in available_tasks[:15] %}
|
<!-- Available tasks -->
|
||||||
<div class="list-row">
|
{% if current_source_type == 'tasks' %}
|
||||||
|
{% for t in available_tasks %}
|
||||||
|
<div class="list-row{% if loop.index > 25 %} focus-hidden-item hidden{% endif %}">
|
||||||
<span class="priority-dot priority-{{ t.priority }}"></span>
|
<span class="priority-dot priority-{{ t.priority }}"></span>
|
||||||
<span class="row-title">{{ t.title }}</span>
|
<span class="row-title">{{ t.title }}</span>
|
||||||
{% if t.project_name %}<span class="row-tag">{{ t.project_name }}</span>{% endif %}
|
{% if t.project_name %}<span class="row-tag">{{ t.project_name }}</span>{% endif %}
|
||||||
@@ -82,6 +115,34 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div style="padding: 16px; color: var(--muted); font-size: 0.85rem;">No available tasks matching filters</div>
|
<div style="padding: 16px; color: var(--muted); font-size: 0.85rem;">No available tasks matching filters</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% if available_tasks|length > 25 %}
|
||||||
|
<div style="padding: 8px 12px; text-align:center;" id="focus-show-more-wrap">
|
||||||
|
<button class="btn btn-ghost btn-sm" id="focus-show-more">Show all {{ available_tasks|length }} tasks</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Available list items -->
|
||||||
|
{% elif current_source_type == 'list_items' %}
|
||||||
|
{% for li in available_list_items %}
|
||||||
|
<div class="list-row{% if loop.index > 25 %} focus-hidden-item hidden{% endif %}">
|
||||||
|
<span style="color:var(--muted);font-size:0.85rem;margin-right:4px">☰</span>
|
||||||
|
<span class="row-title">{{ li.content }}</span>
|
||||||
|
{% if li.list_name %}<span class="row-tag" style="background:var(--purple);color:#fff">{{ li.list_name }}</span>{% endif %}
|
||||||
|
<form action="/focus/add" method="post" style="display:inline">
|
||||||
|
<input type="hidden" name="list_item_id" value="{{ li.id }}">
|
||||||
|
<input type="hidden" name="focus_date" value="{{ focus_date }}">
|
||||||
|
<button class="btn btn-ghost btn-xs" style="color:var(--green)">+ Focus</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div style="padding: 16px; color: var(--muted); font-size: 0.85rem;">No available list items matching filters</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% if available_list_items|length > 25 %}
|
||||||
|
<div style="padding: 8px 12px; text-align:center;" id="focus-show-more-wrap">
|
||||||
|
<button class="btn btn-ghost btn-sm" id="focus-show-more">Show all {{ available_list_items|length }} items</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -91,9 +152,10 @@
|
|||||||
var currentProjectId = '{{ current_project_id }}';
|
var currentProjectId = '{{ current_project_id }}';
|
||||||
var form = document.getElementById('focus-filters');
|
var form = document.getElementById('focus-filters');
|
||||||
|
|
||||||
|
if (domainSel) {
|
||||||
domainSel.addEventListener('change', function() {
|
domainSel.addEventListener('change', function() {
|
||||||
var did = domainSel.value;
|
var did = domainSel.value;
|
||||||
if (!did) { form.submit(); return; }
|
if (!did || !projectSel) { form.submit(); return; }
|
||||||
fetch('/projects/api/by-domain?domain_id=' + encodeURIComponent(did))
|
fetch('/projects/api/by-domain?domain_id=' + encodeURIComponent(did))
|
||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
.then(function(projects) {
|
.then(function(projects) {
|
||||||
@@ -109,6 +171,17 @@
|
|||||||
})
|
})
|
||||||
.catch(function() { form.submit(); });
|
.catch(function() { form.submit(); });
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show more button
|
||||||
|
var showMoreBtn = document.getElementById('focus-show-more');
|
||||||
|
if (showMoreBtn) {
|
||||||
|
showMoreBtn.addEventListener('click', function() {
|
||||||
|
var hidden = document.querySelectorAll('.focus-hidden-item.hidden');
|
||||||
|
hidden.forEach(function(el) { el.classList.remove('hidden'); });
|
||||||
|
document.getElementById('focus-show-more-wrap').style.display = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user