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:
@@ -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
|
||||
|
||||
@@ -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;">📋</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;">✎</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">×</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 = '✓';
|
||||
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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">☰</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 }}';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user