feat: add drag-and-drop reorder to list detail page items
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -406,3 +406,17 @@ async def reorder_list_item(
|
||||
filters["parent_item_id"] = None
|
||||
await repo.move_in_order(item_id, direction, filters=filters)
|
||||
return RedirectResponse(url=f"/lists/{list_id}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/{list_id}/items/reorder-all")
|
||||
async def reorder_all_list_items(
|
||||
list_id: str,
|
||||
request: Request,
|
||||
item_ids: str = Form(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
repo = BaseRepository("list_items", db)
|
||||
ids = [i.strip() for i in item_ids.split(",") if i.strip()]
|
||||
if ids:
|
||||
await repo.reorder(ids)
|
||||
return RedirectResponse(url=f"/lists/{list_id}", status_code=303)
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
{% if list_items %}
|
||||
<div class="card mt-2">
|
||||
{% for li in list_items %}
|
||||
<div class="list-row {{ 'completed' if li.completed }}">
|
||||
<div class="list-row li-drag-row {{ 'completed' if li.completed }}" draggable="true" data-id="{{ li.id }}">
|
||||
{% with reorder_url="/lists/" ~ item.id ~ "/items/reorder", item_id=li.id %}
|
||||
{% include 'partials/reorder_arrows.html' %}
|
||||
{% endwith %}
|
||||
@@ -133,6 +133,9 @@
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<form id="li-reorder-form" action="/lists/{{ item.id }}/items/reorder-all" method="post" style="display:none;">
|
||||
<input type="hidden" name="item_ids" id="li-reorder-ids">
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="empty-state mt-3">
|
||||
<div class="empty-state-icon">☐</div>
|
||||
@@ -175,6 +178,52 @@
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
// Drag-and-drop reorder for top-level list items
|
||||
var dragRow = null;
|
||||
var container = document.querySelector('.card.mt-2');
|
||||
if (container) {
|
||||
container.querySelectorAll('.li-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('.li-drag-row.drag-over').forEach(function(el) { el.classList.remove('drag-over'); });
|
||||
if (dragRow) {
|
||||
var allIds = [];
|
||||
container.querySelectorAll('.li-drag-row').forEach(function(r) { allIds.push(r.dataset.id); });
|
||||
document.getElementById('li-reorder-ids').value = allIds.join(',');
|
||||
document.getElementById('li-reorder-form').submit();
|
||||
}
|
||||
dragRow = null;
|
||||
});
|
||||
row.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
if (!dragRow || row === dragRow) return;
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
container.querySelectorAll('.li-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('.li-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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Link picker: insert URL at cursor position in target input
|
||||
document.querySelectorAll('.link-picker').forEach(function(sel) {
|
||||
sel.addEventListener('change', function() {
|
||||
|
||||
Reference in New Issue
Block a user