feat: focus priority, focus links, task list assignment, lists drag-and-drop, URL display fixes

- Focus page: turn sequence number into persistent editable priority (focus_priority column)
- Focus detail: add links section (add existing, create new, unlink) via focus_links junction table
- Focus detail: add copy and inline edit for checklist items
- Task detail lists tab: add existing list assignment and unlink actions
- Lists page: add drag-and-drop reorder support
- Links/bookmarks pages: remove artificial URL truncation, use CSS ellipsis

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 22:19:06 +00:00
parent 30cff10150
commit 590f019ca7
12 changed files with 291 additions and 19 deletions

View File

@@ -29,7 +29,7 @@
</colgroup>
<thead>
<tr style="border-bottom:2px solid var(--border);background:var(--surface2);">
<th class="focus-sort-head" data-col="0" style="padding:3px 2px;font-size:0.7rem;font-weight:600;color:var(--muted);text-align:center;cursor:pointer;" title="Sort by order">#</th>
<th class="focus-sort-head" data-col="0" style="padding:3px 2px;font-size:0.7rem;font-weight:600;color:var(--muted);text-align:center;cursor:pointer;" title="Sort by priority">P#</th>
<th style="padding:3px 1px;"></th>
<th class="focus-sort-head" data-col="2" style="padding:3px 1px;font-size:0.7rem;font-weight:600;color:var(--muted);cursor:pointer;" title="Sort by done">Done</th>
<th class="focus-sort-head" data-col="3" style="padding:3px 1px;font-size:0.7rem;font-weight:600;color:var(--muted);cursor:pointer;text-align:center;" title="Sort by critical">!</th>
@@ -46,7 +46,11 @@
<tbody class="focus-drag-group">
{% for item in items %}
<tr class="focus-drag-row" draggable="true" data-id="{{ item.id }}" style="border-bottom:1px solid var(--border);{{ 'opacity:0.6;' if item.completed }}">
<td class="focus-row-num" data-sort="{{ '%04d'|format(loop.index) }}" style="padding:1px 2px;vertical-align:middle;text-align:center;color:var(--muted);font-size:0.72rem;font-weight:600;">{{ loop.index }}</td>
<td class="focus-row-num" data-sort="{{ '%04d'|format(item.focus_priority or 9999) }}" style="padding:1px 0;vertical-align:middle;text-align:center;">
<form action="/focus/{{ item.id }}/set-priority" method="post" style="display:inline;margin:0;">
<input type="number" name="focus_priority" value="{{ item.focus_priority if item.focus_priority is not none else '' }}" min="1" max="999" style="width:30px;padding:0 1px;font-size:0.72rem;font-weight:600;text-align:center;border:1px solid transparent;background:transparent;color:var(--muted);border-radius:3px;" onfocus="this.style.borderColor='var(--accent)';this.style.background='var(--surface2)'" onblur="this.style.borderColor='transparent';this.style.background='transparent';if(this.defaultValue!==this.value)this.form.submit()">
</form>
</td>
<td style="padding:1px 1px;vertical-align:middle;">{% with reorder_url="/focus/reorder", item_id=item.id %}{% include 'partials/reorder_arrows.html' %}{% endwith %}</td>
<td data-sort="{{ '1' if item.completed else '0' }}" style="padding:1px 1px;vertical-align:middle;">
<form action="/focus/{{ item.id }}/toggle" method="post" style="display:inline">
@@ -207,13 +211,8 @@
});
}
// Renumber rows
function renumberFocusRows() {
var rows = document.querySelectorAll('.focus-drag-row');
rows.forEach(function(row, i) {
var numCell = row.querySelector('.focus-row-num');
if (numCell) numCell.textContent = i + 1;
});
// No-op: priority is user-set, not auto-numbered
}
// Drag-and-drop reorder

View File

@@ -94,7 +94,14 @@
<label for="li-{{ li.id }}"></label>
</div>
</form>
<span style="flex:1;font-size:0.82rem;{{ 'text-decoration:line-through;' if li.completed }}">{{ li.content }}</span>
<span class="cl-display-{{ li.id }}" style="flex:1;font-size:0.82rem;{{ 'text-decoration:line-through;' if li.completed }}">{{ li.content }}</span>
<form class="cl-edit-{{ li.id }}" action="/focus/{{ item.id }}/list-item/{{ li.id }}/edit" method="post" style="display:none;flex:1;gap:4px;align-items:center;">
<input type="text" name="content" value="{{ li.content }}" class="form-input" style="flex:1;padding:2px 6px;font-size:0.82rem;" required>
<button type="submit" class="btn btn-primary btn-xs">Save</button>
<button type="button" class="btn btn-ghost btn-xs cl-edit-cancel" data-id="{{ li.id }}">Cancel</button>
</form>
<button type="button" class="btn btn-ghost btn-xs cl-copy-btn" data-text="{{ li.content|e }}" title="Copy" style="color:var(--muted);padding:0 3px;">&#128203;</button>
<button type="button" class="btn btn-ghost btn-xs cl-edit-btn" data-id="{{ li.id }}" title="Edit" style="color:var(--muted);padding:0 3px;">&#9998;</button>
<form action="/focus/{{ item.id }}/list-item/{{ li.id }}/delete" method="post" style="display:inline">
<button class="btn btn-ghost btn-xs" style="color:var(--red);padding:0 4px;" title="Remove">&times;</button>
</form>
@@ -110,6 +117,53 @@
</div>
<!-- Links -->
<div class="card mt-4">
<div class="card-header">
<h2 class="card-title">Links</h2>
</div>
<form action="/focus/{{ item.id }}/links/add" method="post" class="flex gap-2 items-end" style="padding: 12px; border-bottom: 1px solid var(--border);">
<div class="form-group" style="flex:2; margin:0;">
<label class="form-label">Link</label>
<select name="link_id" class="form-select" required>
<option value="">Select link...</option>
{% for l in all_links %}
<option value="{{ l.id }}">{{ l.label }} — {{ l.url[:40] }}{% if l.url|length > 40 %}...{% endif %}</option>
{% endfor %}
</select>
</div>
<div class="form-group" style="flex:1; margin:0;">
<label class="form-label">Role</label>
<input type="text" name="role" class="form-input" list="link-roles" placeholder="e.g. reference, docs...">
<datalist id="link-roles">
<option value="website">
<option value="documentation">
<option value="reference">
<option value="repository">
<option value="social">
<option value="resource">
</datalist>
</div>
<button type="submit" class="btn btn-primary btn-sm">Add</button>
<a href="/links/create?focus_id={{ item.id }}" class="btn btn-ghost btn-sm">+ New Link</a>
</form>
{% for l in linked_links %}
<div class="list-row">
<span class="row-title"><a href="{{ l.url }}" target="_blank">{{ l.label }}</a></span>
<span class="row-meta" style="min-width:0;overflow:hidden;text-overflow:ellipsis;">{{ l.url }}</span>
{% if l.role %}<span class="row-tag">{{ l.role }}</span>{% endif %}
<div class="row-actions">
<a href="/links/{{ l.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
<form action="/focus/{{ item.id }}/links/{{ l.id }}/remove" method="post" style="display:inline">
<button class="btn btn-ghost btn-xs">Unlink</button>
</form>
</div>
</div>
{% else %}
<div class="text-sm text-muted" style="padding: 12px;">No links</div>
{% endfor %}
</div>
<script>
(function() {
var container = document.getElementById('focus-checklist');
@@ -169,5 +223,36 @@
});
});
})();
// Copy list item text
document.querySelectorAll('.cl-copy-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var text = btn.getAttribute('data-text');
navigator.clipboard.writeText(text).then(function() {
var orig = btn.innerHTML;
btn.innerHTML = '&#10003;';
btn.style.color = 'var(--green)';
setTimeout(function() { btn.innerHTML = orig; btn.style.color = 'var(--muted)'; }, 1200);
});
});
});
// Edit list item inline
document.querySelectorAll('.cl-edit-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var id = btn.dataset.id;
document.querySelector('.cl-display-' + id).style.display = 'none';
var form = document.querySelector('.cl-edit-' + id);
form.style.display = 'flex';
form.querySelector('input').focus();
});
});
document.querySelectorAll('.cl-edit-cancel').forEach(function(btn) {
btn.addEventListener('click', function() {
var id = btn.dataset.id;
document.querySelector('.cl-display-' + id).style.display = '';
document.querySelector('.cl-edit-' + id).style.display = 'none';
});
});
</script>
{% endblock %}

View File

@@ -16,8 +16,9 @@
{% if prefill_task_id is defined and prefill_task_id %}<input type="hidden" name="task_id" value="{{ prefill_task_id }}">{% endif %}
{% if prefill_meeting_id is defined and prefill_meeting_id %}<input type="hidden" name="meeting_id" value="{{ prefill_meeting_id }}">{% endif %}
{% if prefill_contact_id is defined and prefill_contact_id %}<input type="hidden" name="contact_id" value="{{ prefill_contact_id }}">{% endif %}
{% if prefill_focus_id is defined and prefill_focus_id %}<input type="hidden" name="focus_id" value="{{ prefill_focus_id }}">{% endif %}
{% if from_project is defined and from_project %}<input type="hidden" name="from_project" value="{{ from_project }}">{% endif %}
<div class="form-actions"><button type="submit" class="btn btn-primary">{{ 'Save' if item else 'Create' }}</button><a href="{{ '/projects/' ~ from_project ~ '?tab=links' if from_project is defined and from_project else ('/contacts/' ~ prefill_contact_id if prefill_contact_id is defined and prefill_contact_id else ('/projects/' ~ prefill_project_id ~ '?tab=links' if prefill_project_id is defined and prefill_project_id else ('/tasks/' ~ prefill_task_id ~ '?tab=links' if prefill_task_id is defined and prefill_task_id else '/links'))) }}" class="btn btn-secondary">Cancel</a></div>
<div class="form-actions"><button type="submit" class="btn btn-primary">{{ 'Save' if item else 'Create' }}</button><a href="{{ '/projects/' ~ from_project ~ '?tab=links' if from_project is defined and from_project else ('/focus/' ~ prefill_focus_id if prefill_focus_id is defined and prefill_focus_id else ('/contacts/' ~ prefill_contact_id if prefill_contact_id is defined and prefill_contact_id else ('/projects/' ~ prefill_project_id ~ '?tab=links' if prefill_project_id is defined and prefill_project_id else ('/tasks/' ~ prefill_task_id ~ '?tab=links' if prefill_task_id is defined and prefill_task_id else '/links')))) }}" class="btn btn-secondary">Cancel</a></div>
</div>
</form></div>
{% endblock %}

View File

@@ -14,7 +14,7 @@
{% 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 %}
<span class="row-meta">{{ item.url[:50] }}{% if item.url|length > 50 %}...{% endif %}</span>
<span class="row-meta" style="min-width:0;overflow:hidden;text-overflow:ellipsis;">{{ item.url }}</span>
<div class="row-actions">
<a href="/links/{{ item.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
<form action="/links/{{ item.id }}/delete" method="post" data-confirm="Delete?" style="display:inline"><button class="btn btn-ghost btn-xs" style="color:var(--red)">Del</button></form>

View File

@@ -24,7 +24,7 @@
{% if items %}
<div class="card mt-3">
{% for item in items %}
<div class="list-row">
<div class="list-row lists-drag-row" draggable="true" data-id="{{ item.id }}">
{% with reorder_url="/lists/reorder", item_id=item.id %}
{% include 'partials/reorder_arrows.html' %}
{% endwith %}
@@ -53,6 +53,9 @@
</div>
{% endfor %}
</div>
<form id="lists-reorder-form" action="/lists/reorder-all" method="post" style="display:none;">
<input type="hidden" name="item_ids" id="lists-reorder-ids">
</form>
{% else %}
<div class="empty-state mt-3">
<div class="empty-state-icon">&#9776;</div>
@@ -63,6 +66,52 @@
<script>
(function() {
// Drag-and-drop reorder
var dragRow = null;
var container = document.querySelector('.card.mt-3');
if (container) {
container.querySelectorAll('.lists-drag-row').forEach(function(row) {
row.addEventListener('dragstart', function(e) {
dragRow = row;
row.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', row.dataset.id);
});
row.addEventListener('dragend', function() {
row.classList.remove('dragging');
container.querySelectorAll('.lists-drag-row.drag-over').forEach(function(el) { el.classList.remove('drag-over'); });
if (dragRow) {
var allIds = [];
container.querySelectorAll('.lists-drag-row').forEach(function(r) { allIds.push(r.dataset.id); });
document.getElementById('lists-reorder-ids').value = allIds.join(',');
document.getElementById('lists-reorder-form').submit();
}
dragRow = null;
});
row.addEventListener('dragover', function(e) {
e.preventDefault();
if (!dragRow || row === dragRow) return;
e.dataTransfer.dropEffect = 'move';
container.querySelectorAll('.lists-drag-row.drag-over').forEach(function(el) { el.classList.remove('drag-over'); });
row.classList.add('drag-over');
});
row.addEventListener('dragleave', function() { row.classList.remove('drag-over'); });
row.addEventListener('drop', function(e) {
e.preventDefault();
if (!dragRow || row === dragRow) return;
row.classList.remove('drag-over');
var rows = Array.from(container.querySelectorAll('.lists-drag-row'));
var dragIdx = rows.indexOf(dragRow);
var targetIdx = rows.indexOf(row);
if (dragIdx < targetIdx) {
container.insertBefore(dragRow, row.nextSibling);
} else {
container.insertBefore(dragRow, row);
}
});
});
}
var domainSel = document.getElementById('domain-filter');
var projectSel = document.getElementById('project-filter');
var currentProjectId = '{{ current_project_id }}';

View File

@@ -133,11 +133,31 @@
{% endfor %}
{% elif tab == 'lists' %}
<a href="/lists/create?task_id={{ item.id }}&domain_id={{ item.domain_id }}" class="btn btn-ghost btn-sm mb-3">+ New List</a>
<div class="card mb-3">
<form action="/tasks/{{ item.id }}/lists/add" method="post" class="flex gap-2 items-end" style="padding: 12px;">
<div class="form-group" style="flex:1; margin:0;">
<label class="form-label">Add Existing List</label>
<select name="list_id" class="form-select" required>
<option value="">Select list...</option>
{% for l in all_lists %}
<option value="{{ l.id }}">{{ l.name }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-primary btn-sm">Add</button>
<a href="/lists/create?task_id={{ item.id }}&domain_id={{ item.domain_id }}" class="btn btn-ghost btn-sm">+ New List</a>
</form>
</div>
{% for l in tab_data %}
<div class="list-row">
<span class="row-title"><a href="/lists/{{ l.id }}">{{ l.name }}</a></span>
<span class="row-meta">{{ l.item_count }} items</span>
<div class="row-actions">
<a href="/lists/{{ l.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
<form action="/tasks/{{ item.id }}/lists/{{ l.id }}/remove" method="post" style="display:inline">
<button class="btn btn-ghost btn-xs" title="Unlink">Unlink</button>
</form>
</div>
</div>
{% else %}
<div class="empty-state"><div class="empty-state-text">No lists linked to this task</div></div>

View File

@@ -64,9 +64,7 @@
<span class="row-title">
<a href="{{ item.url }}" target="_blank" rel="noopener">{{ item.label }}</a>
</span>
<span class="row-meta text-xs" style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
{{ item.url }}
</span>
<span class="row-meta" style="min-width:0;overflow:hidden;text-overflow:ellipsis;">{{ item.url }}</span>
{% if item.tags %}
{% for tag in item.tags %}
<span class="row-tag">{{ tag }}</span>