feat: enhanced capture queue with full conversion, batching, and filtering
- Convert to 7 entity types: task, note, project, list item, contact, decision, weblink - Each conversion has a dedicated form page with pre-filled fields and context selectors - Multi-line paste creates batch with shared import_batch_id and undo button - 3-tab filtering: Inbox (unprocessed), Processed (with conversion links), All - Context pre-fill: optional area/project selectors on capture form - Processed items show type badge and link to converted entity - Sidebar badge count for unprocessed items already working Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
"""Capture: quick text capture queue with conversion."""
|
||||
"""Capture: quick text capture queue with conversion to any entity type."""
|
||||
|
||||
import re
|
||||
from uuid import uuid4
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Request, Form, Depends
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.responses import RedirectResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import text
|
||||
from typing import Optional
|
||||
|
||||
from core.database import get_db
|
||||
from core.base_repository import BaseRepository
|
||||
@@ -14,25 +17,58 @@ from core.sidebar import get_sidebar_data
|
||||
router = APIRouter(prefix="/capture", tags=["capture"])
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
CONVERT_TYPES = {
|
||||
"task": "Task",
|
||||
"note": "Note",
|
||||
"project": "Project",
|
||||
"list_item": "List Item",
|
||||
"contact": "Contact",
|
||||
"decision": "Decision",
|
||||
"weblink": "Weblink",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_capture(request: Request, show: str = "unprocessed", db: AsyncSession = Depends(get_db)):
|
||||
async def list_capture(request: Request, show: str = "inbox", db: AsyncSession = Depends(get_db)):
|
||||
sidebar = await get_sidebar_data(db)
|
||||
if show == "all":
|
||||
filters = {}
|
||||
else:
|
||||
filters = {"processed": False}
|
||||
|
||||
result = await db.execute(text("""
|
||||
SELECT * FROM capture WHERE is_deleted = false
|
||||
AND (:show_all OR processed = false)
|
||||
ORDER BY created_at DESC
|
||||
"""), {"show_all": show == "all"})
|
||||
if show == "processed":
|
||||
where = "is_deleted = false AND processed = true"
|
||||
elif show == "all":
|
||||
where = "is_deleted = false"
|
||||
else: # inbox
|
||||
where = "is_deleted = false AND processed = false"
|
||||
|
||||
result = await db.execute(text(f"""
|
||||
SELECT * FROM capture WHERE {where} ORDER BY created_at DESC
|
||||
"""))
|
||||
items = [dict(r._mapping) for r in result]
|
||||
|
||||
# Mark first item per batch for batch-undo display
|
||||
batches = {}
|
||||
for item in items:
|
||||
bid = item.get("import_batch_id")
|
||||
if bid:
|
||||
bid_str = str(bid)
|
||||
if bid_str not in batches:
|
||||
batches[bid_str] = 0
|
||||
item["_batch_first"] = True
|
||||
else:
|
||||
item["_batch_first"] = False
|
||||
batches[bid_str] += 1
|
||||
else:
|
||||
item["_batch_first"] = False
|
||||
|
||||
# Get lists for list_item conversion
|
||||
result = await db.execute(text(
|
||||
"SELECT id, name FROM lists WHERE is_deleted = false ORDER BY name"
|
||||
))
|
||||
all_lists = [dict(r._mapping) for r in result]
|
||||
|
||||
return templates.TemplateResponse("capture.html", {
|
||||
"request": request, "sidebar": sidebar, "items": items,
|
||||
"show": show,
|
||||
"show": show, "batches": batches, "all_lists": all_lists,
|
||||
"convert_types": CONVERT_TYPES,
|
||||
"page_title": "Capture", "active_nav": "capture",
|
||||
})
|
||||
|
||||
@@ -41,22 +77,91 @@ async def list_capture(request: Request, show: str = "unprocessed", db: AsyncSes
|
||||
async def add_capture(
|
||||
request: Request,
|
||||
raw_text: str = Form(...),
|
||||
area_id: Optional[str] = Form(None),
|
||||
project_id: Optional[str] = Form(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
repo = BaseRepository("capture", db)
|
||||
# Multi-line: split into individual items
|
||||
lines = [l.strip() for l in raw_text.strip().split("\n") if l.strip()]
|
||||
batch_id = str(uuid4()) if len(lines) > 1 else None
|
||||
|
||||
for line in lines:
|
||||
await repo.create({"raw_text": line, "processed": False})
|
||||
data = {"raw_text": line, "processed": False}
|
||||
if batch_id:
|
||||
data["import_batch_id"] = batch_id
|
||||
if area_id and area_id.strip():
|
||||
data["area_id"] = area_id
|
||||
if project_id and project_id.strip():
|
||||
data["project_id"] = project_id
|
||||
await repo.create(data)
|
||||
|
||||
return RedirectResponse(url="/capture", status_code=303)
|
||||
|
||||
|
||||
# ---- Batch undo (must be before /{capture_id} routes) ----
|
||||
|
||||
@router.post("/batch/{batch_id}/undo")
|
||||
async def batch_undo(batch_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""Delete all items from a batch."""
|
||||
await db.execute(text("""
|
||||
UPDATE capture SET is_deleted = true, deleted_at = now(), updated_at = now()
|
||||
WHERE import_batch_id = :bid AND is_deleted = false
|
||||
"""), {"bid": batch_id})
|
||||
await db.commit()
|
||||
return RedirectResponse(url="/capture", status_code=303)
|
||||
|
||||
|
||||
# ---- Conversion form page ----
|
||||
|
||||
@router.get("/{capture_id}/convert/{convert_type}")
|
||||
async def convert_form(
|
||||
capture_id: str, convert_type: str,
|
||||
request: Request, db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Show conversion form for a specific capture item."""
|
||||
repo = BaseRepository("capture", db)
|
||||
item = await repo.get(capture_id)
|
||||
if not item or convert_type not in CONVERT_TYPES:
|
||||
return RedirectResponse(url="/capture", status_code=303)
|
||||
|
||||
sidebar = await get_sidebar_data(db)
|
||||
|
||||
result = await db.execute(text(
|
||||
"SELECT id, name FROM lists WHERE is_deleted = false ORDER BY name"
|
||||
))
|
||||
all_lists = [dict(r._mapping) for r in result]
|
||||
|
||||
# Parse name for contact pre-fill
|
||||
parts = item["raw_text"].strip().split(None, 1)
|
||||
first_name = parts[0] if parts else item["raw_text"]
|
||||
last_name = parts[1] if len(parts) > 1 else ""
|
||||
|
||||
# Extract URL for weblink pre-fill
|
||||
url_match = re.search(r'https?://\S+', item["raw_text"])
|
||||
prefill_url = url_match.group(0) if url_match else ""
|
||||
prefill_label = item["raw_text"].replace(prefill_url, "").strip() if url_match else item["raw_text"]
|
||||
|
||||
return templates.TemplateResponse("capture_convert.html", {
|
||||
"request": request, "sidebar": sidebar,
|
||||
"item": item, "convert_type": convert_type,
|
||||
"type_label": CONVERT_TYPES[convert_type],
|
||||
"all_lists": all_lists,
|
||||
"first_name": first_name, "last_name": last_name,
|
||||
"prefill_url": prefill_url, "prefill_label": prefill_label or item["raw_text"],
|
||||
"page_title": f"Convert to {CONVERT_TYPES[convert_type]}",
|
||||
"active_nav": "capture",
|
||||
})
|
||||
|
||||
|
||||
# ---- Conversion handlers ----
|
||||
|
||||
@router.post("/{capture_id}/to-task")
|
||||
async def convert_to_task(
|
||||
capture_id: str,
|
||||
request: Request,
|
||||
capture_id: str, request: Request,
|
||||
domain_id: str = Form(...),
|
||||
project_id: Optional[str] = Form(None),
|
||||
priority: int = Form(3),
|
||||
title: Optional[str] = Form(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
capture_repo = BaseRepository("capture", db)
|
||||
@@ -65,23 +170,177 @@ async def convert_to_task(
|
||||
return RedirectResponse(url="/capture", status_code=303)
|
||||
|
||||
task_repo = BaseRepository("tasks", db)
|
||||
data = {"title": item["raw_text"], "domain_id": domain_id, "status": "open", "priority": 3}
|
||||
data = {
|
||||
"title": (title or item["raw_text"]).strip(),
|
||||
"domain_id": domain_id, "status": "open", "priority": priority,
|
||||
}
|
||||
if project_id and project_id.strip():
|
||||
data["project_id"] = project_id
|
||||
task = await task_repo.create(data)
|
||||
|
||||
await capture_repo.update(capture_id, {
|
||||
"processed": True,
|
||||
"converted_to_type": "task",
|
||||
"processed": True, "converted_to_type": "task",
|
||||
"converted_to_id": str(task["id"]),
|
||||
})
|
||||
return RedirectResponse(url="/capture", status_code=303)
|
||||
return RedirectResponse(url=f"/tasks/{task['id']}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/{capture_id}/to-note")
|
||||
async def convert_to_note(
|
||||
capture_id: str, request: Request,
|
||||
title: Optional[str] = Form(None),
|
||||
domain_id: Optional[str] = Form(None),
|
||||
project_id: Optional[str] = Form(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
capture_repo = BaseRepository("capture", db)
|
||||
item = await capture_repo.get(capture_id)
|
||||
if not item:
|
||||
return RedirectResponse(url="/capture", status_code=303)
|
||||
|
||||
note_repo = BaseRepository("notes", db)
|
||||
raw = item["raw_text"]
|
||||
data = {"title": (title or raw[:100]).strip(), "body": raw}
|
||||
if domain_id and domain_id.strip():
|
||||
data["domain_id"] = domain_id
|
||||
if project_id and project_id.strip():
|
||||
data["project_id"] = project_id
|
||||
note = await note_repo.create(data)
|
||||
|
||||
await capture_repo.update(capture_id, {
|
||||
"processed": True, "converted_to_type": "note",
|
||||
"converted_to_id": str(note["id"]),
|
||||
})
|
||||
return RedirectResponse(url=f"/notes/{note['id']}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/{capture_id}/to-project")
|
||||
async def convert_to_project(
|
||||
capture_id: str, request: Request,
|
||||
domain_id: str = Form(...),
|
||||
name: Optional[str] = Form(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
capture_repo = BaseRepository("capture", db)
|
||||
item = await capture_repo.get(capture_id)
|
||||
if not item:
|
||||
return RedirectResponse(url="/capture", status_code=303)
|
||||
|
||||
project_repo = BaseRepository("projects", db)
|
||||
data = {"name": (name or item["raw_text"]).strip(), "domain_id": domain_id, "status": "active"}
|
||||
project = await project_repo.create(data)
|
||||
|
||||
await capture_repo.update(capture_id, {
|
||||
"processed": True, "converted_to_type": "project",
|
||||
"converted_to_id": str(project["id"]),
|
||||
})
|
||||
return RedirectResponse(url=f"/projects/{project['id']}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/{capture_id}/to-list_item")
|
||||
async def convert_to_list_item(
|
||||
capture_id: str, request: Request,
|
||||
list_id: str = Form(...),
|
||||
content: Optional[str] = Form(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
capture_repo = BaseRepository("capture", db)
|
||||
item = await capture_repo.get(capture_id)
|
||||
if not item:
|
||||
return RedirectResponse(url="/capture", status_code=303)
|
||||
|
||||
li_repo = BaseRepository("list_items", db)
|
||||
data = {"list_id": list_id, "content": (content or item["raw_text"]).strip()}
|
||||
li = await li_repo.create(data)
|
||||
|
||||
await capture_repo.update(capture_id, {
|
||||
"processed": True, "converted_to_type": "list_item",
|
||||
"converted_to_id": str(li["id"]), "list_id": list_id,
|
||||
})
|
||||
return RedirectResponse(url=f"/lists/{list_id}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/{capture_id}/to-contact")
|
||||
async def convert_to_contact(
|
||||
capture_id: str, request: Request,
|
||||
first_name: str = Form(...),
|
||||
last_name: Optional[str] = Form(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
capture_repo = BaseRepository("capture", db)
|
||||
item = await capture_repo.get(capture_id)
|
||||
if not item:
|
||||
return RedirectResponse(url="/capture", status_code=303)
|
||||
|
||||
contact_repo = BaseRepository("contacts", db)
|
||||
data = {"first_name": first_name.strip()}
|
||||
if last_name and last_name.strip():
|
||||
data["last_name"] = last_name.strip()
|
||||
contact = await contact_repo.create(data)
|
||||
|
||||
await capture_repo.update(capture_id, {
|
||||
"processed": True, "converted_to_type": "contact",
|
||||
"converted_to_id": str(contact["id"]),
|
||||
})
|
||||
return RedirectResponse(url=f"/contacts/{contact['id']}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/{capture_id}/to-decision")
|
||||
async def convert_to_decision(
|
||||
capture_id: str, request: Request,
|
||||
title: Optional[str] = Form(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
capture_repo = BaseRepository("capture", db)
|
||||
item = await capture_repo.get(capture_id)
|
||||
if not item:
|
||||
return RedirectResponse(url="/capture", status_code=303)
|
||||
|
||||
decision_repo = BaseRepository("decisions", db)
|
||||
data = {"title": (title or item["raw_text"]).strip(), "status": "proposed", "impact": "medium"}
|
||||
decision = await decision_repo.create(data)
|
||||
|
||||
await capture_repo.update(capture_id, {
|
||||
"processed": True, "converted_to_type": "decision",
|
||||
"converted_to_id": str(decision["id"]),
|
||||
})
|
||||
return RedirectResponse(url=f"/decisions/{decision['id']}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/{capture_id}/to-weblink")
|
||||
async def convert_to_weblink(
|
||||
capture_id: str, request: Request,
|
||||
label: Optional[str] = Form(None),
|
||||
url: Optional[str] = Form(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
capture_repo = BaseRepository("capture", db)
|
||||
item = await capture_repo.get(capture_id)
|
||||
if not item:
|
||||
return RedirectResponse(url="/capture", status_code=303)
|
||||
|
||||
weblink_repo = BaseRepository("weblinks", db)
|
||||
raw = item["raw_text"]
|
||||
url_match = re.search(r'https?://\S+', raw)
|
||||
link_url = (url.strip() if url and url.strip() else None) or (url_match.group(0) if url_match else raw)
|
||||
link_label = (label.strip() if label and label.strip() else None) or (raw.replace(link_url, "").strip() if url_match else raw[:100])
|
||||
if not link_label:
|
||||
link_label = link_url
|
||||
|
||||
data = {"label": link_label, "url": link_url}
|
||||
weblink = await weblink_repo.create(data)
|
||||
|
||||
await capture_repo.update(capture_id, {
|
||||
"processed": True, "converted_to_type": "weblink",
|
||||
"converted_to_id": str(weblink["id"]),
|
||||
})
|
||||
return RedirectResponse(url="/weblinks", status_code=303)
|
||||
|
||||
|
||||
@router.post("/{capture_id}/dismiss")
|
||||
async def dismiss_capture(capture_id: str, db: AsyncSession = Depends(get_db)):
|
||||
repo = BaseRepository("capture", db)
|
||||
await repo.update(capture_id, {"processed": True})
|
||||
await repo.update(capture_id, {"processed": True, "converted_to_type": "dismissed"})
|
||||
return RedirectResponse(url="/capture", status_code=303)
|
||||
|
||||
|
||||
|
||||
@@ -8,28 +8,91 @@
|
||||
<div class="card mb-4">
|
||||
<form action="/capture/add" method="post">
|
||||
<label class="form-label mb-2">Quick Capture (one item per line)</label>
|
||||
<textarea name="raw_text" class="form-textarea" rows="3" placeholder="Type or paste items here... Each line becomes a separate capture item"></textarea>
|
||||
<textarea name="raw_text" class="form-textarea" rows="3" placeholder="Type or paste items here... Each line becomes a separate capture item" required></textarea>
|
||||
<details class="mt-2">
|
||||
<summary class="text-sm text-muted" style="cursor:pointer">Context (optional)</summary>
|
||||
<div class="form-grid mt-2" style="grid-template-columns:1fr 1fr">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Project</label>
|
||||
<select name="project_id" class="form-select">
|
||||
<option value="">None</option>
|
||||
{% for d in sidebar.domain_tree %}
|
||||
<optgroup label="{{ d.name }}">
|
||||
{% for a in d.areas %}{% for p in a.projects %}
|
||||
<option value="{{ p.id }}">{{ p.name }}</option>
|
||||
{% endfor %}{% endfor %}
|
||||
{% for p in d.standalone_projects %}
|
||||
<option value="{{ p.id }}">{{ p.name }}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Area</label>
|
||||
<select name="area_id" class="form-select">
|
||||
<option value="">None</option>
|
||||
{% for d in sidebar.domain_tree %}
|
||||
{% for a in d.areas %}
|
||||
<option value="{{ a.id }}">{{ d.name }} / {{ a.name }}</option>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<div class="mt-2"><button type="submit" class="btn btn-primary">Capture</button></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<a href="/capture?show=unprocessed" class="btn {{ 'btn-primary' if show != 'all' else 'btn-secondary' }} btn-sm">Unprocessed</a>
|
||||
<a href="/capture?show=all" class="btn {{ 'btn-primary' if show == 'all' else 'btn-secondary' }} btn-sm">All</a>
|
||||
<!-- Filter tabs -->
|
||||
<div class="tab-strip mb-3">
|
||||
<a href="/capture?show=inbox" class="tab-item {{ 'active' if show == 'inbox' or show not in ('processed', 'all') }}">Inbox</a>
|
||||
<a href="/capture?show=processed" class="tab-item {{ 'active' if show == 'processed' }}">Processed</a>
|
||||
<a href="/capture?show=all" class="tab-item {{ 'active' if show == 'all' }}">All</a>
|
||||
</div>
|
||||
|
||||
{% if items %}
|
||||
{% 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)">
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="capture-item {{ 'completed' if item.processed }}">
|
||||
<div class="capture-text {{ 'text-muted' if item.processed }}" style="{{ 'text-decoration:line-through' if item.processed }}">{{ item.raw_text }}</div>
|
||||
{% if not item.processed %}
|
||||
|
||||
{% if item.processed %}
|
||||
<div class="capture-actions">
|
||||
<form action="/capture/{{ item.id }}/to-task" method="post" class="flex gap-2">
|
||||
<select name="domain_id" class="filter-select" style="font-size:0.75rem;padding:3px 6px" required>
|
||||
{% for d in sidebar.domain_tree %}<option value="{{ d.id }}">{{ d.name }}</option>{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="btn btn-ghost btn-xs" style="color:var(--green)">To Task</button>
|
||||
</form>
|
||||
{% if item.converted_to_type and item.converted_to_type != 'dismissed' %}
|
||||
<span class="row-tag">{{ item.converted_to_type|replace('_', ' ') }}</span>
|
||||
{% if item.converted_to_id %}
|
||||
{% if item.converted_to_type == 'list_item' and item.list_id %}
|
||||
<a href="/lists/{{ item.list_id }}" class="btn btn-ghost btn-xs">View →</a>
|
||||
{% elif item.converted_to_type == 'weblink' %}
|
||||
<a href="/weblinks" class="btn btn-ghost btn-xs">View →</a>
|
||||
{% elif item.converted_to_type in ('task', 'note', 'project', 'contact', 'decision') %}
|
||||
<a href="/{{ item.converted_to_type }}s/{{ item.converted_to_id }}" class="btn btn-ghost btn-xs">View →</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% elif item.converted_to_type == 'dismissed' %}
|
||||
<span class="row-tag" style="color:var(--muted)">dismissed</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="capture-actions">
|
||||
<select onchange="if(this.value) window.location.href=this.value" class="filter-select" style="font-size:0.75rem;padding:3px 6px">
|
||||
<option value="">Convert...</option>
|
||||
{% for key, label in convert_types.items() %}
|
||||
<option value="/capture/{{ item.id }}/convert/{{ key }}">{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<form action="/capture/{{ item.id }}/dismiss" method="post" style="display:inline"><button class="btn btn-ghost btn-xs">Dismiss</button></form>
|
||||
<form action="/capture/{{ item.id }}/delete" method="post" style="display:inline"><button class="btn btn-ghost btn-xs" style="color:var(--red)">×</button></form>
|
||||
</div>
|
||||
@@ -37,6 +100,11 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-icon">📥</div><div class="empty-state-text">Capture queue is empty</div></div>
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">📥</div>
|
||||
<div class="empty-state-text">
|
||||
{% if show == 'processed' %}No processed items{% elif show == 'all' %}No capture items{% else %}Inbox is empty{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
153
templates/capture_convert.html
Normal file
153
templates/capture_convert.html
Normal file
@@ -0,0 +1,153 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="breadcrumb">
|
||||
<a href="/capture">Capture</a> <span class="sep">/</span> Convert to {{ type_label }}
|
||||
</div>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Convert to {{ type_label }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<!-- Original text -->
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Original Text</label>
|
||||
<div style="padding:10px 12px;background:var(--surface2);border-radius:var(--radius);font-size:0.92rem;color:var(--text-secondary)">{{ item.raw_text }}</div>
|
||||
</div>
|
||||
|
||||
<form action="/capture/{{ item.id }}/to-{{ convert_type }}" method="post">
|
||||
<div class="form-grid">
|
||||
|
||||
{% if convert_type == 'task' %}
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" name="title" class="form-input" value="{{ item.raw_text }}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Domain *</label>
|
||||
<select name="domain_id" class="form-select" required>
|
||||
{% for d in sidebar.domain_tree %}
|
||||
<option value="{{ d.id }}">{{ d.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Project</label>
|
||||
<select name="project_id" class="form-select">
|
||||
<option value="">None</option>
|
||||
{% for d in sidebar.domain_tree %}
|
||||
<optgroup label="{{ d.name }}">
|
||||
{% for a in d.areas %}{% for p in a.projects %}
|
||||
<option value="{{ p.id }}" {{ 'selected' if item.project_id and p.id|string == item.project_id|string }}>{{ p.name }}</option>
|
||||
{% endfor %}{% endfor %}
|
||||
{% for p in d.standalone_projects %}
|
||||
<option value="{{ p.id }}" {{ 'selected' if item.project_id and p.id|string == item.project_id|string }}>{{ p.name }}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Priority</label>
|
||||
<select name="priority" class="form-select">
|
||||
<option value="1">1 - Critical</option>
|
||||
<option value="2">2 - High</option>
|
||||
<option value="3" selected>3 - Normal</option>
|
||||
<option value="4">4 - Low</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{% elif convert_type == 'note' %}
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" name="title" class="form-input" value="{{ item.raw_text[:100] }}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Domain</label>
|
||||
<select name="domain_id" class="form-select">
|
||||
<option value="">None</option>
|
||||
{% for d in sidebar.domain_tree %}
|
||||
<option value="{{ d.id }}">{{ d.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Project</label>
|
||||
<select name="project_id" class="form-select">
|
||||
<option value="">None</option>
|
||||
{% for d in sidebar.domain_tree %}
|
||||
<optgroup label="{{ d.name }}">
|
||||
{% for a in d.areas %}{% for p in a.projects %}
|
||||
<option value="{{ p.id }}" {{ 'selected' if item.project_id and p.id|string == item.project_id|string }}>{{ p.name }}</option>
|
||||
{% endfor %}{% endfor %}
|
||||
{% for p in d.standalone_projects %}
|
||||
<option value="{{ p.id }}" {{ 'selected' if item.project_id and p.id|string == item.project_id|string }}>{{ p.name }}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{% elif convert_type == 'project' %}
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Project Name</label>
|
||||
<input type="text" name="name" class="form-input" value="{{ item.raw_text }}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Domain *</label>
|
||||
<select name="domain_id" class="form-select" required>
|
||||
{% for d in sidebar.domain_tree %}
|
||||
<option value="{{ d.id }}">{{ d.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{% elif convert_type == 'list_item' %}
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Content</label>
|
||||
<input type="text" name="content" class="form-input" value="{{ item.raw_text }}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Add to List *</label>
|
||||
<select name="list_id" class="form-select" required>
|
||||
{% for lst in all_lists %}
|
||||
<option value="{{ lst.id }}" {{ 'selected' if item.list_id and lst.id|string == item.list_id|string }}>{{ lst.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{% elif convert_type == 'contact' %}
|
||||
<div class="form-group">
|
||||
<label class="form-label">First Name *</label>
|
||||
<input type="text" name="first_name" class="form-input" value="{{ first_name }}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Last Name</label>
|
||||
<input type="text" name="last_name" class="form-input" value="{{ last_name }}">
|
||||
</div>
|
||||
|
||||
{% elif convert_type == 'decision' %}
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Decision Title</label>
|
||||
<input type="text" name="title" class="form-input" value="{{ item.raw_text }}" required>
|
||||
</div>
|
||||
|
||||
{% elif convert_type == 'weblink' %}
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Label</label>
|
||||
<input type="text" name="label" class="form-input" value="{{ prefill_label }}" required>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">URL</label>
|
||||
<input type="url" name="url" class="form-input" value="{{ prefill_url }}" placeholder="https://" required>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Convert to {{ type_label }}</button>
|
||||
<a href="/capture" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user