Session 1: bug fix, global search, admin trash, lists CRUD
This commit is contained in:
@@ -44,6 +44,10 @@
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
||||
Links
|
||||
</a>
|
||||
<a href="/lists" class="nav-item {{ 'active' if active_nav == 'lists' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
|
||||
Lists
|
||||
</a>
|
||||
<a href="/capture" class="nav-item {{ 'active' if active_nav == 'capture' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg>
|
||||
Capture
|
||||
@@ -76,6 +80,10 @@
|
||||
</div>
|
||||
|
||||
<div class="nav-section" style="margin-top: auto; padding-bottom: 12px;">
|
||||
<a href="/admin/trash" class="nav-item {{ 'active' if active_nav == 'trash' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
||||
Trash
|
||||
</a>
|
||||
<a href="/domains" class="nav-item {{ 'active' if active_nav == 'domains' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v6m0 6v6m-7-7h6m6 0h6"/></svg>
|
||||
Manage Domains
|
||||
@@ -99,6 +107,11 @@
|
||||
<span class="topbar-env">DEV</span>
|
||||
{% endif %}
|
||||
<div class="topbar-spacer"></div>
|
||||
<button class="search-trigger" onclick="openSearch()" title="Search (Cmd/K)">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<span>Search...</span>
|
||||
<kbd>⌘K</kbd>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="page-content">
|
||||
@@ -106,6 +119,19 @@
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<!-- Search Modal -->
|
||||
<div id="search-modal" class="search-modal hidden">
|
||||
<div class="search-modal-backdrop" onclick="closeSearch()"></div>
|
||||
<div class="search-modal-content">
|
||||
<div class="search-modal-input-wrap">
|
||||
<svg class="search-modal-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" class="search-modal-input" placeholder="Search tasks, projects, notes..." autocomplete="off">
|
||||
<kbd class="search-modal-esc" onclick="closeSearch()">Esc</kbd>
|
||||
</div>
|
||||
<div id="search-results" class="search-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
98
templates/list_detail.html
Normal file
98
templates/list_detail.html
Normal file
@@ -0,0 +1,98 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<!-- Breadcrumb -->
|
||||
<div class="breadcrumb">
|
||||
<a href="/lists">Lists</a>
|
||||
<span class="sep">/</span>
|
||||
{% if domain %}<span style="color: {{ domain.color or 'var(--accent)' }}">{{ domain.name }}</span><span class="sep">/</span>{% endif %}
|
||||
{% if project %}<a href="/projects/{{ project.id }}">{{ project.name }}</a><span class="sep">/</span>{% endif %}
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-header">
|
||||
<h1 class="detail-title">{{ item.name }}</h1>
|
||||
<div class="flex gap-2">
|
||||
<a href="/lists/{{ item.id }}/edit" class="btn btn-secondary btn-sm">Edit</a>
|
||||
<form action="/lists/{{ item.id }}/delete" method="post" data-confirm="Delete this list?" style="display:inline">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-meta mt-2">
|
||||
<span class="detail-meta-item">
|
||||
<span class="row-tag">{{ item.list_type }}</span>
|
||||
</span>
|
||||
{% if item.description %}
|
||||
<p class="text-secondary mt-1">{{ item.description }}</p>
|
||||
{% endif %}
|
||||
{% if item.tags %}
|
||||
<div class="mt-1">
|
||||
{% for tag in item.tags %}
|
||||
<span class="row-tag">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Add item form -->
|
||||
<form class="quick-add mt-3" action="/lists/{{ item.id }}/items/add" method="post">
|
||||
<input type="text" name="content" placeholder="Add item..." required>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Add</button>
|
||||
</form>
|
||||
|
||||
<!-- List items -->
|
||||
{% if list_items %}
|
||||
<div class="card mt-2">
|
||||
{% for li in list_items %}
|
||||
<div class="list-row {{ 'completed' if li.completed }}">
|
||||
{% if item.list_type == 'checklist' %}
|
||||
<div class="row-check">
|
||||
<form action="/lists/{{ item.id }}/items/{{ li.id }}/toggle" method="post" style="display:inline">
|
||||
<input type="checkbox" id="li-{{ li.id }}" {{ 'checked' if li.completed }}
|
||||
onchange="this.form.submit()">
|
||||
<label for="li-{{ li.id }}"></label>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
<span class="row-title" style="{{ 'text-decoration: line-through; opacity: 0.6;' if li.completed }}">
|
||||
{{ li.content }}
|
||||
</span>
|
||||
<div class="row-actions">
|
||||
<form action="/lists/{{ item.id }}/items/{{ li.id }}/delete" method="post" style="display:inline">
|
||||
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Child items -->
|
||||
{% for child in child_map.get(li.id|string, []) %}
|
||||
<div class="list-row {{ 'completed' if child.completed }}" style="padding-left: 48px;">
|
||||
{% if item.list_type == 'checklist' %}
|
||||
<div class="row-check">
|
||||
<form action="/lists/{{ item.id }}/items/{{ child.id }}/toggle" method="post" style="display:inline">
|
||||
<input type="checkbox" id="li-{{ child.id }}" {{ 'checked' if child.completed }}
|
||||
onchange="this.form.submit()">
|
||||
<label for="li-{{ child.id }}"></label>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
<span class="row-title" style="{{ 'text-decoration: line-through; opacity: 0.6;' if child.completed }}">
|
||||
{{ child.content }}
|
||||
</span>
|
||||
<div class="row-actions">
|
||||
<form action="/lists/{{ item.id }}/items/{{ child.id }}/delete" method="post" style="display:inline">
|
||||
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state mt-3">
|
||||
<div class="empty-state-icon">☐</div>
|
||||
<div class="empty-state-text">No items yet. Add one above.</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
82
templates/list_form.html
Normal file
82
templates/list_form.html
Normal file
@@ -0,0 +1,82 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">{{ page_title }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form method="post" action="{{ '/lists/' ~ item.id ~ '/edit' if item else '/lists/create' }}">
|
||||
<div class="form-grid">
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Name *</label>
|
||||
<input type="text" name="name" class="form-input" required
|
||||
value="{{ item.name if item else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Domain *</label>
|
||||
<select name="domain_id" class="form-select" required>
|
||||
<option value="">Select domain...</option>
|
||||
{% for d in domains %}
|
||||
<option value="{{ d.id }}"
|
||||
{{ 'selected' if (item and item.domain_id|string == d.id|string) or (not item and prefill_domain_id == d.id|string) }}>
|
||||
{{ d.name }}
|
||||
</option>
|
||||
{% 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 a in areas %}
|
||||
<option value="{{ a.id }}"
|
||||
{{ 'selected' if item and item.area_id and item.area_id|string == a.id|string }}>
|
||||
{{ a.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 p in projects %}
|
||||
<option value="{{ p.id }}"
|
||||
{{ 'selected' if (item and item.project_id and item.project_id|string == p.id|string) or (not item and prefill_project_id == p.id|string) }}>
|
||||
{{ p.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Type</label>
|
||||
<select name="list_type" class="form-select">
|
||||
<option value="checklist" {{ 'selected' if item and item.list_type == 'checklist' }}>Checklist</option>
|
||||
<option value="ordered" {{ 'selected' if item and item.list_type == 'ordered' }}>Ordered</option>
|
||||
<option value="reference" {{ 'selected' if item and item.list_type == 'reference' }}>Reference</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" class="form-textarea" rows="3">{{ item.description if item and item.description else '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Tags</label>
|
||||
<input type="text" name="tags" class="form-input" placeholder="tag1, tag2, ..."
|
||||
value="{{ item.tags|join(', ') if item and item.tags else '' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">{{ 'Save Changes' if item else 'Create List' }}</button>
|
||||
<a href="{{ '/lists/' ~ item.id if item else '/lists' }}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
60
templates/lists.html
Normal file
60
templates/lists.html
Normal file
@@ -0,0 +1,60 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Lists<span class="page-count">{{ items|length }}</span></h1>
|
||||
<a href="/lists/create" class="btn btn-primary">+ New List</a>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<form class="filters-bar" method="get" action="/lists">
|
||||
<select name="domain_id" class="filter-select" onchange="this.form.submit()">
|
||||
<option value="">All Domains</option>
|
||||
{% for d in domains %}
|
||||
<option value="{{ d.id }}" {{ 'selected' if current_domain_id == d.id|string }}>{{ d.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="project_id" class="filter-select" onchange="this.form.submit()">
|
||||
<option value="">All Projects</option>
|
||||
{% for p in projects %}
|
||||
<option value="{{ p.id }}" {{ 'selected' if current_project_id == p.id|string }}>{{ p.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
|
||||
{% if items %}
|
||||
<div class="card mt-3">
|
||||
{% for item in items %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/lists/{{ item.id }}">{{ item.name }}</a></span>
|
||||
<span class="row-meta">
|
||||
{{ item.completed_count }}/{{ item.item_count }} items
|
||||
</span>
|
||||
{% if item.item_count > 0 %}
|
||||
<div class="progress-bar" style="width: 80px;">
|
||||
<div class="progress-fill" style="width: {{ (item.completed_count / item.item_count * 100) if item.item_count > 0 else 0 }}%"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<span class="row-tag">{{ item.list_type }}</span>
|
||||
{% if item.domain_name %}
|
||||
<span class="row-domain-tag" style="background: {{ item.domain_color or '#4F6EF7' }}22; color: {{ item.domain_color or '#4F6EF7' }}">{{ item.domain_name }}</span>
|
||||
{% endif %}
|
||||
{% if item.project_name %}
|
||||
<span class="row-tag">{{ item.project_name }}</span>
|
||||
{% endif %}
|
||||
<div class="row-actions">
|
||||
<a href="/lists/{{ item.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||
<form action="/lists/{{ item.id }}/delete" method="post" data-confirm="Delete this list?" 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 mt-3">
|
||||
<div class="empty-state-icon">☰</div>
|
||||
<div class="empty-state-text">No lists yet</div>
|
||||
<a href="/lists/create" class="btn btn-primary">Create First List</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
37
templates/search.html
Normal file
37
templates/search.html
Normal file
@@ -0,0 +1,37 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Search</h1>
|
||||
</div>
|
||||
|
||||
<form class="search-page-form" method="get" action="/search">
|
||||
<input type="text" name="q" value="{{ query }}" class="form-input" placeholder="Search tasks, projects, notes, contacts..." autofocus style="max-width: 600px;">
|
||||
<button type="submit" class="btn btn-primary">Search</button>
|
||||
</form>
|
||||
|
||||
{% if query %}
|
||||
<p class="text-muted mt-2">{{ results|length }} result{{ 's' if results|length != 1 }} for "{{ query }}"</p>
|
||||
{% endif %}
|
||||
|
||||
{% if results %}
|
||||
<div class="card mt-3">
|
||||
{% for item in results %}
|
||||
<div class="list-row">
|
||||
<span class="search-type-badge search-type-{{ item.type }}">{{ item.type_label }}</span>
|
||||
<span class="row-title"><a href="{{ item.url }}">{{ item.name }}</a></span>
|
||||
{% if item.context %}
|
||||
<span class="row-meta">{{ item.context }}</span>
|
||||
{% endif %}
|
||||
{% if item.status %}
|
||||
<span class="status-badge status-{{ item.status }}">{{ item.status|replace('_', ' ') }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elif query %}
|
||||
<div class="empty-state mt-3">
|
||||
<div class="empty-state-icon">🔍</div>
|
||||
<div class="empty-state-text">No results found for "{{ query }}"</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
54
templates/trash.html
Normal file
54
templates/trash.html
Normal file
@@ -0,0 +1,54 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Trash<span class="page-count">{{ total_deleted }}</span></h1>
|
||||
{% if total_deleted > 0 %}
|
||||
<form action="/admin/trash/empty" method="post" data-confirm="Permanently delete ALL {{ total_deleted }} items? This cannot be undone." style="display:inline">
|
||||
<button type="submit" class="btn btn-danger">Empty Trash</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Type filter -->
|
||||
<div class="filters-bar">
|
||||
<a href="/admin/trash" class="btn {{ 'btn-primary' if not current_type else 'btn-secondary' }} btn-sm">
|
||||
All ({{ total_deleted }})
|
||||
</a>
|
||||
{% for entity in trash_entities %}
|
||||
{% set count = entity_counts.get(entity.table, 0) %}
|
||||
{% if count > 0 %}
|
||||
<a href="/admin/trash?entity_type={{ entity.table }}" class="btn {{ 'btn-primary' if current_type == entity.table else 'btn-secondary' }} btn-sm">
|
||||
{{ entity.label }} ({{ count }})
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if deleted_items %}
|
||||
<div class="card mt-3">
|
||||
{% for item in deleted_items %}
|
||||
<div class="list-row">
|
||||
<span class="search-type-badge search-type-{{ item.table }}">{{ item.type_label }}</span>
|
||||
<span class="row-title">{{ item.name }}</span>
|
||||
{% if item.deleted_at %}
|
||||
<span class="row-meta">Deleted {{ item.deleted_at.strftime('%Y-%m-%d %H:%M') if item.deleted_at else '' }}</span>
|
||||
{% endif %}
|
||||
<div class="row-actions" style="opacity: 1;">
|
||||
<form action="/admin/trash/{{ item.table }}/{{ item.id }}/restore" method="post" style="display:inline">
|
||||
<button type="submit" class="btn btn-secondary btn-xs">Restore</button>
|
||||
</form>
|
||||
<form action="/admin/trash/{{ item.table }}/{{ item.id }}/permanent-delete" method="post"
|
||||
data-confirm="Permanently delete '{{ item.name }}'? This cannot be undone." style="display:inline">
|
||||
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Delete Forever</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state mt-3">
|
||||
<div class="empty-state-icon">🗑</div>
|
||||
<div class="empty-state-text">Trash is empty</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user