feat: manual sorting of additional services

This commit is contained in:
Björn Fromme
2026-08-04 17:06:41 +02:00
parent 045fdea923
commit 455456b6bc
9 changed files with 253 additions and 23 deletions
+90
View File
@@ -0,0 +1,90 @@
import { Controller } from '@hotwired/stimulus'
import Sortable from 'sortablejs'
export default class extends Controller {
static targets = [ 'list', 'item', 'handle', 'positionField', 'positionLabel', 'status' ]
static values = {
url: String,
}
connect() {
this.abortController = null
// The sortable container may be nested (e.g. a tbody) when the controller element also
// has to wrap elements that must not be dragged, like the status message.
new Sortable(this.hasListTarget ? this.listTarget : this.element, {
draggable: '[data-sortable-target="item"]',
handle: '[data-sortable-target="handle"]',
sort: true,
fallbackOnBody: true,
onEnd: e => {
this.updateOrdering()
this.persist()
}
})
}
disconnect() {
this.abortController?.abort()
}
updateOrdering() {
this.positionFieldTargets.forEach((field, index) => {
field.value = index + 1
})
this.positionLabelTargets.forEach((label, index) => {
label.innerText = index + 1
})
}
/**
* Sends the new order to the server. Only active when a url value is present, so the
* controller stays usable for plain form ordering.
*/
persist() {
if (!this.hasUrlValue || this.urlValue === '') {
return
}
// A drag while the previous request is still running makes that request obsolete.
this.abortController?.abort()
this.abortController = new AbortController()
const ids = this.itemTargets
.map(item => Number(item.dataset.sortableId))
.filter(id => Number.isInteger(id) && id > 0)
this.setStatus('speichert …')
fetch(this.urlValue, {
method: 'POST',
credentials: 'include',
signal: this.abortController.signal,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ ids }),
})
.then(response => {
if (!response.ok) {
throw new Error(`Unexpected response ${response.status}`)
}
this.setStatus('Reihenfolge gespeichert')
})
.catch(error => {
if (error.name === 'AbortError') {
return
}
console.error('Failed to persist ordering', error)
this.setStatus('Speichern fehlgeschlagen')
})
}
setStatus(message) {
this.statusTargets.forEach(status => {
status.innerText = message
})
}
}