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:
2026-03-01 21:47:23 +00:00
parent 5092e7e015
commit a21e00d0e0
3 changed files with 514 additions and 34 deletions

View File

@@ -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...&#10;Each line becomes a separate capture item"></textarea>
<textarea name="raw_text" class="form-textarea" rows="3" placeholder="Type or paste items here...&#10;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 &middot; {{ 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 &rarr;</a>
{% elif item.converted_to_type == 'weblink' %}
<a href="/weblinks" class="btn btn-ghost btn-xs">View &rarr;</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 &rarr;</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)">&times;</button></form>
</div>
@@ -37,6 +100,11 @@
</div>
{% endfor %}
{% else %}
<div class="empty-state"><div class="empty-state-icon">&#128229;</div><div class="empty-state-text">Capture queue is empty</div></div>
<div class="empty-state">
<div class="empty-state-icon">&#128229;</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 %}

View 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 %}