feat: Implement drag-and-drop reordering for routine items and add corresponding E2E tests
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m41s
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m41s
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -22,7 +22,18 @@
|
||||
</div>
|
||||
|
||||
<div v-else class="items-list">
|
||||
<div v-for="(item, idx) in items" :key="item.id || `new-${idx}`" class="item-row">
|
||||
<div
|
||||
v-for="(item, idx) in items"
|
||||
:key="item.id || `new-${idx}`"
|
||||
class="item-row"
|
||||
:class="{ 'drag-over': dragOverIdx === idx, dragging: draggingIdx === idx }"
|
||||
draggable="true"
|
||||
@dragstart="(e) => onDragStart(e, idx)"
|
||||
@dragover.prevent="(e) => onDragOver(e, idx)"
|
||||
@drop.prevent="onDrop(idx)"
|
||||
@dragend="onDragEnd"
|
||||
>
|
||||
<span class="drag-handle" title="Drag to reorder">⠿</span>
|
||||
<div class="item-left">
|
||||
<img
|
||||
v-if="item.image_url"
|
||||
@@ -150,6 +161,8 @@ const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const localImageFile = ref<File | null>(null)
|
||||
const removedItemIds = ref<string[]>([])
|
||||
const draggingIdx = ref<number | null>(null)
|
||||
const dragOverIdx = ref<number | null>(null)
|
||||
|
||||
onMounted(async () => {
|
||||
if (isEdit.value && props.id) {
|
||||
@@ -320,6 +333,36 @@ function cancelEditItem() {
|
||||
newItemLocalFile.value = null
|
||||
}
|
||||
|
||||
function onDragStart(e: DragEvent, idx: number) {
|
||||
if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move'
|
||||
cancelEditItem()
|
||||
draggingIdx.value = idx
|
||||
}
|
||||
|
||||
function onDragOver(e: DragEvent, idx: number) {
|
||||
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'
|
||||
if (draggingIdx.value === null || draggingIdx.value === idx) return
|
||||
dragOverIdx.value = idx
|
||||
}
|
||||
|
||||
function onDrop(idx: number) {
|
||||
if (draggingIdx.value === null || draggingIdx.value === idx) {
|
||||
draggingIdx.value = null
|
||||
dragOverIdx.value = null
|
||||
return
|
||||
}
|
||||
const moved = items.value.splice(draggingIdx.value, 1)[0]
|
||||
items.value.splice(idx, 0, moved)
|
||||
normalizeItemOrder()
|
||||
draggingIdx.value = null
|
||||
dragOverIdx.value = null
|
||||
}
|
||||
|
||||
function onDragEnd() {
|
||||
draggingIdx.value = null
|
||||
dragOverIdx.value = null
|
||||
}
|
||||
|
||||
async function uploadImage(file: File): Promise<string> {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
@@ -473,6 +516,29 @@ function handleCancel() {
|
||||
border: 1px solid var(--form-input-border, #e6e6e6);
|
||||
border-radius: 10px;
|
||||
padding: 0.55rem 0.7rem;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.item-row.drag-over {
|
||||
border-color: var(--btn-primary, #667eea);
|
||||
background: var(--form-input-bg-hover, #f0f4ff);
|
||||
}
|
||||
|
||||
.item-row.dragging {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
color: var(--form-label, #aaa);
|
||||
font-size: 1.15rem;
|
||||
padding-right: 0.45rem;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.item-left {
|
||||
@@ -480,6 +546,7 @@ function handleCancel() {
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
gap: 0.6rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-thumb {
|
||||
|
||||
Reference in New Issue
Block a user