feat(ui): improve arrangement configurator, song preview, and downloads
Simplify ArrangementConfigurator: replace color pickers with compact pills, add click-to-add from pool, use watcher-based auto-select for new/cloned arrangements, remove group_colors from save payload. Enhance SongsBlock preview: color-coded group headers with tinted backgrounds, PDF download button inside preview modal, .pro download link per matched song, show DB ccli_id with fallback to CTS ccli_id. Fix Modal z-index for nested dialogs. Fix SlideUploader duplicate upload on watch by adding deep option and upload guard. Expand API log detail sections by default and increase JSON tree depth. Convert song download button from emit to direct .pro download link.
This commit is contained in:
parent
b40c371edc
commit
af0c72ebcc
|
|
@ -31,21 +31,27 @@ public function preview(Song $song, SongArrangement $arrangement): JsonResponse
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function download(Song $song, SongArrangement $arrangement): Response
|
public function download(Song $song, SongArrangement $arrangement): Response|JsonResponse
|
||||||
{
|
{
|
||||||
abort_unless($arrangement->song_id === $song->id, 404);
|
abort_unless($arrangement->song_id === $song->id, 404);
|
||||||
|
|
||||||
|
try {
|
||||||
$groupsInOrder = $this->buildGroupsInOrder($arrangement);
|
$groupsInOrder = $this->buildGroupsInOrder($arrangement);
|
||||||
|
|
||||||
$pdf = Pdf::loadView('pdf.song', [
|
$pdf = Pdf::loadView('pdf.song', [
|
||||||
'song' => $song,
|
'song' => $song,
|
||||||
'arrangement' => $arrangement,
|
'arrangement' => $arrangement,
|
||||||
'groupsInOrder' => $groupsInOrder,
|
'groupsInOrder' => $groupsInOrder,
|
||||||
]);
|
])->setPaper('a4', 'portrait');
|
||||||
|
|
||||||
$filename = str($song->title)->slug() . '-' . str($arrangement->name)->slug() . '.pdf';
|
$filename = str($song->title)->slug().'-'.str($arrangement->name)->slug().'.pdf';
|
||||||
|
|
||||||
return $pdf->download($filename);
|
return $pdf->download($filename);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'PDF-Erzeugung fehlgeschlagen: '.$e->getMessage(),
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildGroupsInOrder(SongArrangement $arrangement): array
|
private function buildGroupsInOrder(SongArrangement $arrangement): array
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { router } from '@inertiajs/vue3'
|
import { router } from '@inertiajs/vue3'
|
||||||
import { VueDraggable } from 'vue-draggable-plus'
|
import { VueDraggable } from 'vue-draggable-plus'
|
||||||
|
|
||||||
|
|
@ -58,76 +58,41 @@ watch(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const colorMap = computed(() => {
|
|
||||||
const map = {}
|
|
||||||
|
|
||||||
poolGroups.value.forEach((group) => {
|
|
||||||
map[group.id] = group.color
|
|
||||||
})
|
|
||||||
|
|
||||||
arrangementGroups.value.forEach((group) => {
|
|
||||||
map[group.id] = group.color
|
|
||||||
})
|
|
||||||
|
|
||||||
return map
|
|
||||||
})
|
|
||||||
|
|
||||||
const groupPillStyle = (group) => ({
|
const groupPillStyle = (group) => ({
|
||||||
backgroundColor: group.color,
|
backgroundColor: group.color,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const pendingAutoSelect = ref(false)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.arrangements.length,
|
||||||
|
(newLen, oldLen) => {
|
||||||
|
if (pendingAutoSelect.value && newLen > oldLen) {
|
||||||
|
const newest = props.arrangements.reduce((a, b) => a.id > b.id ? a : b)
|
||||||
|
selectedId.value = newest.id
|
||||||
|
pendingAutoSelect.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const addArrangement = () => {
|
const addArrangement = () => {
|
||||||
const name = window.prompt('Name des neuen Arrangements')
|
const name = window.prompt('Name des neuen Arrangements')
|
||||||
|
if (!name) return
|
||||||
if (!name) {
|
pendingAutoSelect.value = true
|
||||||
return
|
router.post(`/songs/${props.songId}/arrangements`, { name }, { preserveScroll: true })
|
||||||
}
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
`/songs/${props.songId}/arrangements`,
|
|
||||||
{ name },
|
|
||||||
{
|
|
||||||
preserveScroll: true,
|
|
||||||
onSuccess: () => {
|
|
||||||
nextTick(() => {
|
|
||||||
const updatedArrangements = props.arrangements
|
|
||||||
if (updatedArrangements.length > 0) {
|
|
||||||
const newest = updatedArrangements.reduce((a, b) => a.id > b.id ? a : b)
|
|
||||||
selectedId.value = newest.id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cloneArrangement = () => {
|
const cloneArrangement = () => {
|
||||||
if (!selectedArrangement.value) {
|
if (!selectedArrangement.value) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = window.prompt('Name des neuen Arrangements', `${selectedArrangement.value.name} Kopie`)
|
const name = window.prompt('Name des neuen Arrangements', `${selectedArrangement.value.name} Kopie`)
|
||||||
|
if (!name) return
|
||||||
|
pendingAutoSelect.value = true
|
||||||
|
router.post(`/arrangements/${selectedArrangement.value.id}/clone`, { name }, { preserveScroll: true })
|
||||||
|
}
|
||||||
|
|
||||||
if (!name) {
|
const addGroupFromPool = (group) => {
|
||||||
return
|
arrangementGroups.value.push({ ...group })
|
||||||
}
|
saveArrangement()
|
||||||
|
|
||||||
router.post(
|
|
||||||
`/arrangements/${selectedArrangement.value.id}/clone`,
|
|
||||||
{ name },
|
|
||||||
{
|
|
||||||
preserveScroll: true,
|
|
||||||
onSuccess: () => {
|
|
||||||
nextTick(() => {
|
|
||||||
const updatedArrangements = props.arrangements
|
|
||||||
if (updatedArrangements.length > 0) {
|
|
||||||
const newest = updatedArrangements.reduce((a, b) => a.id > b.id ? a : b)
|
|
||||||
selectedId.value = newest.id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeGroupAt = (index) => {
|
const removeGroupAt = (index) => {
|
||||||
|
|
@ -135,25 +100,6 @@ const removeGroupAt = (index) => {
|
||||||
saveArrangement()
|
saveArrangement()
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncPoolColor = (groupId, color) => {
|
|
||||||
poolGroups.value = poolGroups.value.map((group) => {
|
|
||||||
if (group.id !== groupId) {
|
|
||||||
return group
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...group,
|
|
||||||
color,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateGroupColor = (group, color) => {
|
|
||||||
group.color = color
|
|
||||||
syncPoolColor(group.id, color)
|
|
||||||
saveArrangement()
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveArrangement = () => {
|
const saveArrangement = () => {
|
||||||
if (!selectedArrangement.value) {
|
if (!selectedArrangement.value) {
|
||||||
return
|
return
|
||||||
|
|
@ -166,7 +112,6 @@ const saveArrangement = () => {
|
||||||
song_group_id: group.id,
|
song_group_id: group.id,
|
||||||
order: index + 1,
|
order: index + 1,
|
||||||
})),
|
})),
|
||||||
group_colors: colorMap.value,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
|
|
@ -241,8 +186,8 @@ const deleteArrangement = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-4 lg:grid-cols-2">
|
<div class="grid gap-4 lg:grid-cols-2">
|
||||||
<div class="space-y-2">
|
<div class="space-y-1">
|
||||||
<h4 class="text-sm font-semibold uppercase tracking-wide text-gray-600">
|
<h4 class="text-xs font-semibold uppercase tracking-wide text-gray-500">
|
||||||
Verfügbare Gruppen
|
Verfügbare Gruppen
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
|
|
@ -250,79 +195,76 @@ const deleteArrangement = () => {
|
||||||
v-model="poolGroups"
|
v-model="poolGroups"
|
||||||
:group="{ name: 'song-groups', pull: 'clone', put: false }"
|
:group="{ name: 'song-groups', pull: 'clone', put: false }"
|
||||||
:sort="false"
|
:sort="false"
|
||||||
class="min-h-24 rounded-lg border border-dashed border-gray-300 bg-gray-50 p-3"
|
ghost-class="drag-ghost"
|
||||||
>
|
chosen-class="drag-chosen"
|
||||||
<div
|
drag-class="drag-active"
|
||||||
v-for="group in poolGroups"
|
class="flex flex-wrap gap-1.5 rounded-lg border border-dashed border-gray-300 bg-gray-50 p-2"
|
||||||
:key="group.id"
|
|
||||||
class="mb-2 flex items-center gap-2 rounded-md border border-gray-200 bg-white p-2"
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="inline-flex rounded-full px-3 py-1 text-sm font-semibold text-white"
|
v-for="group in poolGroups"
|
||||||
|
:key="group.id"
|
||||||
|
class="inline-flex cursor-grab rounded-full px-2.5 py-0.5 text-xs font-semibold text-white transition-opacity hover:opacity-80"
|
||||||
:style="groupPillStyle(group)"
|
:style="groupPillStyle(group)"
|
||||||
|
@click="addGroupFromPool(group)"
|
||||||
>
|
>
|
||||||
{{ group.name }}
|
{{ group.name }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<label class="ml-auto flex items-center gap-2 text-xs text-gray-600">
|
|
||||||
Farbe ändern
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
class="h-8 w-8 cursor-pointer rounded border border-gray-300"
|
|
||||||
:value="group.color"
|
|
||||||
@input="updateGroupColor(group, $event.target.value)"
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</VueDraggable>
|
</VueDraggable>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-1">
|
||||||
<h4 class="text-sm font-semibold uppercase tracking-wide text-gray-600">
|
<h4 class="text-xs font-semibold uppercase tracking-wide text-gray-500">
|
||||||
Gruppenfolge
|
Gruppenfolge
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<VueDraggable
|
<VueDraggable
|
||||||
v-model="arrangementGroups"
|
v-model="arrangementGroups"
|
||||||
:group="{ name: 'song-groups', pull: true, put: true }"
|
:group="{ name: 'song-groups', pull: true, put: true }"
|
||||||
class="min-h-24 rounded-lg border border-gray-200 bg-gray-50 p-3"
|
ghost-class="drag-ghost"
|
||||||
|
chosen-class="drag-chosen"
|
||||||
|
drag-class="drag-active"
|
||||||
|
class="flex min-h-10 flex-wrap gap-1.5 rounded-lg border border-gray-200 bg-gray-50 p-2"
|
||||||
@end="saveArrangement"
|
@end="saveArrangement"
|
||||||
>
|
>
|
||||||
<div
|
<span
|
||||||
v-for="(group, index) in arrangementGroups"
|
v-for="(group, index) in arrangementGroups"
|
||||||
:key="`${group.id}-${index}`"
|
:key="`${group.id}-${index}`"
|
||||||
class="mb-2 flex items-center gap-2 rounded-md border border-gray-200 bg-white p-2"
|
class="inline-flex cursor-grab items-center gap-1 rounded-full py-0.5 pl-2.5 pr-1 text-xs font-semibold text-white"
|
||||||
>
|
|
||||||
<span data-testid="arrangement-drag-handle" class="cursor-move text-gray-500">⋮⋮</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
class="inline-flex rounded-full px-3 py-1 text-sm font-semibold text-white"
|
|
||||||
:style="groupPillStyle(group)"
|
:style="groupPillStyle(group)"
|
||||||
>
|
>
|
||||||
{{ group.name }}
|
{{ group.name }}
|
||||||
</span>
|
|
||||||
|
|
||||||
<label class="ml-auto flex items-center gap-2 text-xs text-gray-600">
|
|
||||||
Farbe ändern
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
class="h-8 w-8 cursor-pointer rounded border border-gray-300"
|
|
||||||
:value="group.color"
|
|
||||||
@input="updateGroupColor(group, $event.target.value)"
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
data-testid="arrangement-remove-button"
|
data-testid="arrangement-remove-button"
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded px-2 py-1 text-xs font-semibold text-red-600 hover:bg-red-50"
|
class="ml-0.5 flex h-4 w-4 items-center justify-center rounded-full text-white/70 transition-colors hover:bg-white/20 hover:text-white"
|
||||||
@click="removeGroupAt(index)"
|
@click.stop="removeGroupAt(index)"
|
||||||
>
|
>
|
||||||
Entfernen
|
×
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</span>
|
||||||
</VueDraggable>
|
</VueDraggable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.drag-ghost) {
|
||||||
|
opacity: 0.4;
|
||||||
|
ring: 2px solid white;
|
||||||
|
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.8), 0 0 0 4px rgba(99, 102, 241, 0.6);
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.drag-chosen) {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.drag-active) {
|
||||||
|
opacity: 1 !important;
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 2px rgba(255, 255, 255, 0.8);
|
||||||
|
border-radius: 9999px;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,12 @@ const closePreview = () => {
|
||||||
previewData.value = null
|
previewData.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const downloadPreviewPdf = () => {
|
||||||
|
if (previewData.value?.song?.id && previewData.value?.arrangement?.id) {
|
||||||
|
window.location.href = `/songs/${previewData.value.song.id}/arrangements/${previewData.value.arrangement.id}/pdf`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const downloadSongPdf = (serviceSong) => {
|
const downloadSongPdf = (serviceSong) => {
|
||||||
if (!serviceSong.song_id || !serviceSong.song_arrangement_id) {
|
if (!serviceSong.song_id || !serviceSong.song_arrangement_id) {
|
||||||
showToast('Bitte zuerst ein Arrangement auswählen.', 'warning')
|
showToast('Bitte zuerst ein Arrangement auswählen.', 'warning')
|
||||||
|
|
@ -251,7 +257,7 @@ const toastClasses = () => {
|
||||||
</h4>
|
</h4>
|
||||||
<div class="mt-2 flex flex-wrap items-center gap-2 text-xs text-gray-600">
|
<div class="mt-2 flex flex-wrap items-center gap-2 text-xs text-gray-600">
|
||||||
<span class="rounded-full bg-gray-100 px-2.5 py-1 font-medium">
|
<span class="rounded-full bg-gray-100 px-2.5 py-1 font-medium">
|
||||||
CCLI: {{ serviceSong.cts_ccli_id || '-' }}
|
CCLI: {{ serviceSong.song?.ccli_id || serviceSong.cts_ccli_id || '-' }}
|
||||||
</span>
|
</span>
|
||||||
<span class="rounded-full bg-gray-100 px-2.5 py-1 font-medium">
|
<span class="rounded-full bg-gray-100 px-2.5 py-1 font-medium">
|
||||||
Hat Uebersetzung:
|
Hat Uebersetzung:
|
||||||
|
|
@ -405,6 +411,13 @@ const toastClasses = () => {
|
||||||
>
|
>
|
||||||
PDF herunterladen
|
PDF herunterladen
|
||||||
</button>
|
</button>
|
||||||
|
<a
|
||||||
|
data-testid="songs-block-download-pro-button"
|
||||||
|
:href="`/api/songs/${serviceSong.song_id}/download-pro`"
|
||||||
|
class="inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-xs font-semibold text-gray-700 transition hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
.pro herunterladen
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -449,22 +462,43 @@ const toastClasses = () => {
|
||||||
<span class="inline-block h-6 w-6 animate-spin rounded-full border-2 border-amber-400 border-t-transparent"></span>
|
<span class="inline-block h-6 w-6 animate-spin rounded-full border-2 border-amber-400 border-t-transparent"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="previewData" class="space-y-4">
|
<div v-else-if="previewData" class="space-y-3">
|
||||||
<div v-for="(group, idx) in previewData.groups" :key="idx" class="rounded-lg border border-gray-200 p-4">
|
<div v-for="(group, idx) in previewData.groups" :key="idx"
|
||||||
<div class="mb-2 flex items-center gap-2">
|
class="rounded-lg border overflow-hidden"
|
||||||
<span
|
:style="{ borderColor: group.color || '#6b7280' }"
|
||||||
class="inline-flex rounded-full px-3 py-1 text-xs font-semibold text-white"
|
>
|
||||||
|
<!-- Group name as box title bar -->
|
||||||
|
<div class="px-3 py-1.5 text-xs font-bold text-white"
|
||||||
:style="{ backgroundColor: group.color || '#6b7280' }"
|
:style="{ backgroundColor: group.color || '#6b7280' }"
|
||||||
>
|
>
|
||||||
{{ group.name }}
|
{{ group.name }}
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-for="(slide, sIdx) in group.slides" :key="sIdx" class="mt-2 whitespace-pre-wrap text-sm text-gray-700 leading-relaxed">{{ slide.text_content }}</div>
|
<!-- Slides content with lightened background -->
|
||||||
|
<div class="px-3 py-2 space-y-1"
|
||||||
|
:style="{ backgroundColor: (group.color || '#6b7280') + '15' }"
|
||||||
|
>
|
||||||
|
<div v-for="(slide, sIdx) in group.slides" :key="sIdx"
|
||||||
|
class="whitespace-pre-wrap text-sm text-gray-700 leading-snug"
|
||||||
|
>{{ slide.text_content }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="previewData.song?.copyright_text" class="border-t pt-3 text-xs text-gray-400">
|
<div v-if="previewData.song?.copyright_text" class="border-t pt-3 text-xs text-gray-400">
|
||||||
{{ previewData.song.copyright_text }}
|
{{ previewData.song.copyright_text }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end pt-3 border-t border-gray-200">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center gap-1.5 rounded-lg bg-amber-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-amber-700"
|
||||||
|
@click="downloadPreviewPdf"
|
||||||
|
>
|
||||||
|
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||||
|
</svg>
|
||||||
|
PDF herunterladen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ const maxWidthClass = computed(() => {
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-show="show"
|
v-show="show"
|
||||||
class="mb-6 transform overflow-hidden rounded-lg bg-white shadow-xl transition-all sm:mx-auto sm:w-full"
|
class="relative z-10 mb-6 transform overflow-hidden rounded-lg bg-white shadow-xl transition-all sm:mx-auto sm:w-full"
|
||||||
:class="maxWidthClass"
|
:class="maxWidthClass"
|
||||||
>
|
>
|
||||||
<slot v-if="showSlot" />
|
<slot v-if="showSlot" />
|
||||||
|
|
|
||||||
|
|
@ -54,10 +54,10 @@ watch(files, (newFiles) => {
|
||||||
if (newFiles.length > 0 && !uploading.value) {
|
if (newFiles.length > 0 && !uploading.value) {
|
||||||
processFiles()
|
processFiles()
|
||||||
}
|
}
|
||||||
})
|
}, { deep: true })
|
||||||
|
|
||||||
function processFiles() {
|
function processFiles() {
|
||||||
if (files.value.length === 0) return
|
if (files.value.length === 0 || uploading.value) return
|
||||||
|
|
||||||
uploadError.value = ''
|
uploadError.value = ''
|
||||||
totalCount.value = files.value.length
|
totalCount.value = files.value.length
|
||||||
|
|
@ -242,7 +242,6 @@ function dismissError() {
|
||||||
:disabled="isUploading"
|
:disabled="isUploading"
|
||||||
:show-select-button="false"
|
:show-select-button="false"
|
||||||
:class="['slide-dropzone', { 'slide-dropzone--inline': inline }]"
|
:class="['slide-dropzone', { 'slide-dropzone--inline': inline }]"
|
||||||
@change="processFiles"
|
|
||||||
>
|
>
|
||||||
<template #placeholder-img>
|
<template #placeholder-img>
|
||||||
<div class="flex flex-col items-center gap-2 pointer-events-none">
|
<div class="flex flex-col items-center gap-2 pointer-events-none">
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,10 @@ async function toggleExpanded(id) {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
detailData.value[id] = await response.json()
|
const data = await response.json()
|
||||||
|
data._showContext = true
|
||||||
|
data._showResponse = true
|
||||||
|
detailData.value[id] = data
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Detail load error:', e)
|
console.error('Detail load error:', e)
|
||||||
|
|
@ -211,7 +214,7 @@ async function toggleExpanded(id) {
|
||||||
Antwort-Daten
|
Antwort-Daten
|
||||||
</button>
|
</button>
|
||||||
<div v-if="detailData[log.id]._showResponse && detailData[log.id].response_body" class="mt-1 max-h-96 overflow-y-auto rounded-lg bg-gray-50 p-3">
|
<div v-if="detailData[log.id]._showResponse && detailData[log.id].response_body" class="mt-1 max-h-96 overflow-y-auto rounded-lg bg-gray-50 p-3">
|
||||||
<JsonTreeViewer :data="parseJson(detailData[log.id].response_body)" :expand-depth="1" />
|
<JsonTreeViewer :data="parseJson(detailData[log.id].response_body)" :expand-depth="2" />
|
||||||
</div>
|
</div>
|
||||||
<p v-if="detailData[log.id]._showResponse && !detailData[log.id].response_body" class="mt-1 text-sm italic text-gray-400">Keine Antwort-Daten verfügbar</p>
|
<p v-if="detailData[log.id]._showResponse && !detailData[log.id].response_body" class="mt-1 text-sm italic text-gray-400">Keine Antwort-Daten verfügbar</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -541,18 +541,17 @@ function pageRange() {
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- Herunterladen -->
|
<!-- Herunterladen -->
|
||||||
<button
|
<a
|
||||||
data-testid="song-list-download-button"
|
data-testid="song-list-download-button"
|
||||||
type="button"
|
:href="`/api/songs/${song.id}/download-pro`"
|
||||||
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm transition-all hover:border-emerald-300 hover:bg-emerald-50 hover:text-emerald-700"
|
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm transition-all hover:border-emerald-300 hover:bg-emerald-50 hover:text-emerald-700"
|
||||||
title="Als .pro herunterladen"
|
title="Als .pro herunterladen"
|
||||||
@click="$emit('download', song)"
|
|
||||||
>
|
>
|
||||||
<svg class="mr-1 h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
<svg class="mr-1 h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||||
</svg>
|
</svg>
|
||||||
Herunterladen
|
Herunterladen
|
||||||
</button>
|
</a>
|
||||||
|
|
||||||
<!-- Löschen -->
|
<!-- Löschen -->
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue