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:
2026-03-10 22:23:52 +00:00
parent 590f019ca7
commit ba936752a1
2 changed files with 64 additions and 1 deletions

View File

@@ -406,3 +406,17 @@ async def reorder_list_item(
filters["parent_item_id"] = None filters["parent_item_id"] = None
await repo.move_in_order(item_id, direction, filters=filters) await repo.move_in_order(item_id, direction, filters=filters)
return RedirectResponse(url=f"/lists/{list_id}", status_code=303) 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)

View File

@@ -53,7 +53,7 @@
{% if list_items %} {% if list_items %}
<div class="card mt-2"> <div class="card mt-2">
{% for li in list_items %} {% 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 %} {% with reorder_url="/lists/" ~ item.id ~ "/items/reorder", item_id=li.id %}
{% include 'partials/reorder_arrows.html' %} {% include 'partials/reorder_arrows.html' %}
{% endwith %} {% endwith %}
@@ -133,6 +133,9 @@
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
</div> </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 %} {% else %}
<div class="empty-state mt-3"> <div class="empty-state mt-3">
<div class="empty-state-icon">&#9744;</div> <div class="empty-state-icon">&#9744;</div>
@@ -175,6 +178,52 @@
</div> </div>
<script> <script>
(function() { (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 // Link picker: insert URL at cursor position in target input
document.querySelectorAll('.link-picker').forEach(function(sel) { document.querySelectorAll('.link-picker').forEach(function(sel) {
sel.addEventListener('change', function() { sel.addEventListener('change', function() {