wip: implement admin crud

This commit is contained in:
Björn Fromme committed 2026-07-16 09:03:42 +02:00
1 parent 306cd6bec4
commit e902a5e298
78 files changed
+2555 -101

No files matched your search

@@ -0,0 +1,84 @@
import { Controller } from '@hotwired/stimulus'
import { useFetch } from '../mixins/use_fetch'
import Autocomplete from '@trevoreyre/autocomplete-js'
export default class extends Controller {
static targets = [ 'input', 'field', 'resetButton', 'searchIcon' ]
static values = { url: String, choices: Array, selected: Boolean }
initialize() {
useFetch(this)
}
connect() {
const searchHandler = input => {
return new Promise(resolve => {
// Check input minimum length
if (input.length < 2) {
return resolve([])
}
// Search choices if provided
if (this.choicesValue.length > 0) {
return resolve(this.choicesValue.filter(choice => {
return -1 !== choice.text.toLowerCase().search(input.toLowerCase())
}))
}
// Use API otherwise
let data = { search: input }
fetch(this.urlValue, { method: 'POST', body: JSON.stringify(data), credentials: 'include' })
.then(this.checkStatus)
.then(this.parseJSON)
.then(data => {
resolve(data)
})
})
}
const submitHandler = result => {
this.fieldTarget.value = result.value
this.selectedValue = true
this.element.dispatchEvent(new CustomEvent('select', {
detail: { result }
}))
}
this.autocomplete = new Autocomplete(this.element, {
search: searchHandler,
getResultValue: result => result.text,
submitOnEnter: true,
onSubmit: submitHandler,
debounceTime: 250,
})
if (this.fieldTarget.value) {
this.selectedValue = true
}
// prevent form submission when selecting entry in autocomplete list with enter key
this.element.addEventListener('keypress', e => {
if (e.key === 'Enter') {
e.preventDefault()
}
})
}
reset() {
this.inputTarget.value = null
this.fieldTarget.value = null
this.selectedValue = false
this.element.dispatchEvent(new CustomEvent('reset'))
}
selectedValueChanged() {
this.resetButtonTarget.classList.toggle('hidden', false === this.selectedValue)
this.searchIconTarget.classList.toggle('hidden', this.selectedValue)
}
disconnect() {
this.autocomplete.destroy()
}
}
@@ -0,0 +1,56 @@
import { Controller } from '@hotwired/stimulus'
import flatpickr from 'flatpickr';
import { German } from 'flatpickr/dist/l10n/de';
/* stimulusFetch: 'lazy' */
export default class extends Controller {
static targets = [ 'field' ]
static values = {
minDate: String,
maxDate: String,
disableWeekends: Boolean,
}
initialize() {
flatpickr.localize(German)
}
connect() {
const minDate = this.minDateValue
const maxDate = this.maxDateValue
const options = {
dateFormat: 'Y-m-d',
altFormat: 'd.m.Y',
minDate,
maxDate,
altInput: true,
allowInput: true,
onClose(dates, currentdatestring, instance) {
instance.setDate(instance.altInput.value, true, instance.config.altFormat)
this.element.dispatchEvent(new CustomEvent('datepicker:closed'))
},
onChange(dates, currentdatestring, instance) {
this.element.dispatchEvent(new CustomEvent('datepicker:picked', {
detail: {
date: instance.altInput.value
}
}))
},
}
if (true === this.disableWeekendsValue) {
options['disable'] = [
function(date) {
return (date.getDay() === 0 || date.getDay() === 6);
}
]
}
this.picker = flatpickr(this.fieldTarget, options)
}
disconnect() {
this.picker.destroy()
}
}
@@ -0,0 +1,36 @@
import { Controller } from '@hotwired/stimulus'
import { useClickOutside } from 'stimulus-use'
export default class extends Controller {
static targets = [ 'label', 'dropdown', 'choice' ]
static values = { placeholder: String }
connect() {
this.update()
useClickOutside(this)
}
toggle() {
this.dropdownTarget.classList.toggle('hidden')
}
update() {
let labels = []
this.choiceTargets.forEach((choice) => {
if (choice.checked) {
labels.push(choice.dataset.label)
}
this.labelTarget.innerText = labels.join(', ')
})
if (0 === labels.length) {
this.labelTarget.innerText = this.placeholderValue
}
}
clickOutside(e) {
this.dropdownTarget.classList.toggle('hidden', true)
}
}
+42
View File
@@ -0,0 +1,42 @@
import { Controller } from '@hotwired/stimulus'
import Sortable from 'sortablejs'
import { useFetch } from '../mixins/use_fetch'
/* stimulusFetch: 'lazy' */
export default class extends Controller {
static targets = [ 'item', 'handle', 'positionField', 'positionLabel' ]
static values = { url: String }
connect() {
useFetch(this)
new Sortable(this.element, {
item: '[data-sortable-target="item"]',
handle: '[data-sortable-target="handle"]',
sort: true,
onEnd: () => {
this.updateOrdering()
if (this.urlValue) {
this.saveOrdering()
}
}
})
}
updateOrdering() {
this.positionFieldTargets.forEach((field, index) => {
field.value = index + 1
})
this.positionLabelTargets.forEach((label, index) => {
label.innerText = index + 1
})
}
saveOrdering() {
const body = new FormData()
this.positionFieldTargets.forEach(field => {
body.append(`sorting[${field.dataset.id}]`, field.value)
})
fetch(this.urlValue, this.getInitObject('post', body))
}
}