mirror of
https://github.com/Theodor-Springmann-Stiftung/musenalm.git
synced 2026-02-04 02:25:30 +00:00
FIX: delete button and layout in baende vendpoint
This commit is contained in:
@@ -46,7 +46,7 @@ func (p *AlmanachEditPage) Setup(router *router.Router[*core.RequestEvent], ia p
|
||||
rg.BindFunc(middleware.IsAdminOrEditor())
|
||||
rg.GET(URL_ALMANACH_EDIT, p.GET(engine, app))
|
||||
rg.POST(URL_ALMANACH_EDIT+"save", p.POSTSave(engine, app, ia))
|
||||
rg.POST(URL_ALMANACH_EDIT+"delete", p.POSTDelete(engine, app))
|
||||
rg.POST(URL_ALMANACH_EDIT+"delete", p.POSTDelete(engine, app, ia))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ func (p *AlmanachEditPage) POSTSave(engine *templating.Engine, app core.App, ma
|
||||
}
|
||||
}
|
||||
|
||||
func (p *AlmanachEditPage) POSTDelete(engine *templating.Engine, app core.App) HandleFunc {
|
||||
func (p *AlmanachEditPage) POSTDelete(engine *templating.Engine, app core.App, ma pagemodels.IApp) HandleFunc {
|
||||
return func(e *core.RequestEvent) error {
|
||||
id := e.Request.PathValue("id")
|
||||
req := templating.NewRequest(e)
|
||||
@@ -302,6 +302,9 @@ func (p *AlmanachEditPage) POSTDelete(engine *templating.Engine, app core.App) H
|
||||
// Invalidate sorted entries cache since entry was deleted
|
||||
InvalidateSortedEntriesCache()
|
||||
|
||||
// Invalidate Bände cache since entry was deleted
|
||||
ma.ResetBaendeCache()
|
||||
|
||||
// Delete from FTS5 index asynchronously
|
||||
go func(appInstance core.App, entryID string) {
|
||||
if err := dbmodels.DeleteFTS5Entry(appInstance, entryID); err != nil {
|
||||
@@ -333,8 +336,8 @@ type almanachEditPayload struct {
|
||||
}
|
||||
|
||||
type almanachDeletePayload struct {
|
||||
CSRFToken string `json:"csrf_token"`
|
||||
LastEdited string `json:"last_edited"`
|
||||
CSRFToken string `json:"csrf_token" form:"csrf_token"`
|
||||
LastEdited string `json:"last_edited" form:"last_edited"`
|
||||
}
|
||||
|
||||
type almanachEntryPayload struct {
|
||||
|
||||
@@ -24,6 +24,7 @@ const (
|
||||
URL_BAENDE = "/baende/"
|
||||
URL_BAENDE_RESULTS = "/baende/results/"
|
||||
URL_BAENDE_MORE = "/baende/more/"
|
||||
URL_BAENDE_DELETE = "/baende/delete-info/{id}"
|
||||
TEMPLATE_BAENDE = "/baende/"
|
||||
URL_BAENDE_DETAILS = "/baende/details/{id}"
|
||||
BAENDE_PAGE_SIZE = 150
|
||||
@@ -77,6 +78,7 @@ func (p *BaendePage) Setup(router *router.Router[*core.RequestEvent], ia pagemod
|
||||
rg.GET("more/", p.handleMore(engine, app, ia))
|
||||
rg.GET("details/{id}", p.handleDetails(engine, app))
|
||||
rg.GET("row/{id}", p.handleRow(engine, app))
|
||||
rg.GET("delete-info/{id}", p.handleDeleteInfo(engine, app))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -221,6 +223,45 @@ func (p *BaendePage) handleDetails(engine *templating.Engine, app core.App) Hand
|
||||
}
|
||||
}
|
||||
|
||||
func (p *BaendePage) handleDeleteInfo(engine *templating.Engine, app core.App) HandleFunc {
|
||||
return func(e *core.RequestEvent) error {
|
||||
req := templating.NewRequest(e)
|
||||
if req.User() == nil {
|
||||
return e.Redirect(303, "/login/")
|
||||
}
|
||||
|
||||
id := e.Request.PathValue("id")
|
||||
if id == "" {
|
||||
return engine.Response404(e, nil, nil)
|
||||
}
|
||||
|
||||
entry, err := dbmodels.Entries_MusenalmID(app, id)
|
||||
if err != nil {
|
||||
return engine.Response404(e, err, nil)
|
||||
}
|
||||
|
||||
items, err := dbmodels.Items_Entry(app, entry.Id)
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to get items for delete dialog", "error", err)
|
||||
}
|
||||
|
||||
contents, err := dbmodels.Contents_Entry(app, entry.Id)
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to get contents for delete dialog", "error", err)
|
||||
}
|
||||
|
||||
dbmodels.Sort_Contents_Numbering(contents)
|
||||
|
||||
data := map[string]any{
|
||||
"entry": entry,
|
||||
"items": items,
|
||||
"contents": contents,
|
||||
}
|
||||
|
||||
return engine.Response200(e, "/baende/delete_info/", data, "fragment")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *BaendePage) buildResultData(app core.App, ma pagemodels.IApp, e *core.RequestEvent, req *templating.Request, showAggregated bool) (map[string]any, error) {
|
||||
data := map[string]any{}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -150,51 +150,12 @@ class="container-normal font-sans mt-10">
|
||||
|
||||
<div class="mt-2">
|
||||
<div class="border-b px-3 border-zinc-300">
|
||||
<!-- Row 1: Search and Filters -->
|
||||
<div class="flex flex-wrap items-end justify-start min-h-14 gap-x-2 gap-y-3 pb-3">
|
||||
<!-- Search box -->
|
||||
<div class="min-w-[22.5rem] max-w-96 flex flex-row bg-stone-50 relative font-sans text-lg items-center">
|
||||
<div>
|
||||
<i class="ri-search-line"></i><i class="-ml-0.5 inline-block ri-arrow-right-s-line"></i>
|
||||
</div>
|
||||
<div class="border-b-4 border-zinc-300 grow">
|
||||
<form
|
||||
method="GET"
|
||||
action="/baende/"
|
||||
hx-get="/baende/results/"
|
||||
hx-indicator="#baende-search-spinner"
|
||||
hx-push-url="false"
|
||||
hx-swap="outerHTML"
|
||||
hx-target="#baenderesults"
|
||||
role="search"
|
||||
@submit="offset = 0; hasMore = true"
|
||||
aria-label="Bändesuche">
|
||||
<input type="hidden" name="sort" :value="sortField" />
|
||||
<input type="hidden" name="order" :value="sortOrder" />
|
||||
<div class="relative">
|
||||
<input
|
||||
class="px-2 py-0.5 pr-7 font-sans placeholder:italic w-full text-lg"
|
||||
type="search"
|
||||
name="search"
|
||||
value="{{ $model.search }}"
|
||||
placeholder="Signatur oder Suchbegriff"
|
||||
x-model="search"
|
||||
@input.debounce.500="selectedLetter = ''; clearFilters(); ((search.trim().length >= 3) || /^[0-9]+$/.test(search.trim()) || search === '') && $el.form.requestSubmit()"
|
||||
@search.debounce.500="selectedLetter = ''; clearFilters(); ((search.trim().length >= 3) || /^[0-9]+$/.test(search.trim()) || search === '') && $el.form.requestSubmit()"
|
||||
autocomplete="off" />
|
||||
<span id="baende-search-spinner" class="htmx-indicator absolute right-1 top-1/2 -translate-y-1/2 text-slate-900">
|
||||
<i class="ri-loader-4-line spinning" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
<button x-show="false">Suchen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Row 1: Filters -->
|
||||
<div class="flex flex-wrap items-end justify-center min-h-14 gap-x-2 gap-y-3 pb-3">
|
||||
<!-- Alphabet navigation toggle -->
|
||||
<div class="relative">
|
||||
<details class="font-sans text-base list-none" data-role="alphabet-toggle" @toggle="alphabetOpen = $el.open; if ($el.open) { closeOtherDropdowns($el); }">
|
||||
<summary class="cursor-pointer text-gray-700 hover:text-slate-900 bg-gray-100 px-3 py-1.5 rounded-md flex items-center gap-2"
|
||||
<summary class="cursor-pointer select-none text-gray-700 hover:text-slate-900 bg-gray-100 px-3 py-1.5 rounded-md flex items-center gap-2"
|
||||
:class="selectedLetter ? 'font-semibold text-slate-900 ring-1 ring-slate-300' : ''">
|
||||
<span x-text="selectedLetter ? `Alphabet: ${selectedLetter}` : 'Alphabet'"></span>
|
||||
<i class="ri-arrow-down-s-line transform origin-center transition-transform" :class="{ 'rotate-180': alphabetOpen }"></i>
|
||||
@@ -239,7 +200,7 @@ class="container-normal font-sans mt-10">
|
||||
<!-- Status filter -->
|
||||
<div class="relative" x-data="{ open: false }" data-role="baende-filter">
|
||||
<details class="font-sans text-base list-none" @toggle="open = $el.open; if ($el.open) { closeOtherDropdowns($el); }">
|
||||
<summary class="cursor-pointer text-gray-700 hover:text-slate-900 bg-gray-100 px-3 py-1.5 rounded-md flex items-center gap-2"
|
||||
<summary class="cursor-pointer select-none text-gray-700 hover:text-slate-900 bg-gray-100 px-3 py-1.5 rounded-md flex items-center gap-2"
|
||||
:class="activeFilterType === 'status' ? 'font-semibold text-slate-900 ring-1 ring-slate-300' : ''">
|
||||
<span x-text="activeFilterType === 'status' ? `Status: ${statusLabels[activeFilterValue] || activeFilterValue}` : 'Status'"></span>
|
||||
<i class="ri-arrow-down-s-line transform origin-center transition-transform" :class="{ 'rotate-180': open }"></i>
|
||||
@@ -284,7 +245,7 @@ class="container-normal font-sans mt-10">
|
||||
<!-- Person filter -->
|
||||
<div class="relative" x-data="{ open: false }" data-role="baende-filter">
|
||||
<details class="font-sans text-base list-none" @toggle="open = $el.open; if ($el.open) { closeOtherDropdowns($el); }">
|
||||
<summary class="cursor-pointer text-gray-700 hover:text-slate-900 bg-gray-100 px-3 py-1.5 rounded-md flex items-center gap-2"
|
||||
<summary class="cursor-pointer select-none text-gray-700 hover:text-slate-900 bg-gray-100 px-3 py-1.5 rounded-md flex items-center gap-2"
|
||||
:class="activeFilterType === 'person' ? 'font-semibold text-slate-900 ring-1 ring-slate-300' : ''">
|
||||
<span x-text="activeFilterType === 'person' ? `Person: ${personLabels[activeFilterValue] || activeFilterValue}` : 'Person'"></span>
|
||||
<i class="ri-arrow-down-s-line transform origin-center transition-transform" :class="{ 'rotate-180': open }"></i>
|
||||
@@ -337,7 +298,7 @@ class="container-normal font-sans mt-10">
|
||||
<!-- Benutzer filter -->
|
||||
<div class="relative" x-data="{ open: false }" data-role="baende-filter">
|
||||
<details class="font-sans text-base list-none" @toggle="open = $el.open; if ($el.open) { closeOtherDropdowns($el); }">
|
||||
<summary class="cursor-pointer text-gray-700 hover:text-slate-900 bg-gray-100 px-3 py-1.5 rounded-md flex items-center gap-2"
|
||||
<summary class="cursor-pointer select-none text-gray-700 hover:text-slate-900 bg-gray-100 px-3 py-1.5 rounded-md flex items-center gap-2"
|
||||
:class="activeFilterType === 'user' ? 'font-semibold text-slate-900 ring-1 ring-slate-300' : ''">
|
||||
<span x-text="activeFilterType === 'user' ? `Benutzer: ${userLabels[activeFilterValue] || activeFilterValue}` : 'Benutzer'"></span>
|
||||
<i class="ri-arrow-down-s-line transform origin-center transition-transform" :class="{ 'rotate-180': open }"></i>
|
||||
@@ -385,7 +346,7 @@ class="container-normal font-sans mt-10">
|
||||
<!-- Jahr filter -->
|
||||
<div class="relative" x-data="{ open: false }" data-role="baende-filter">
|
||||
<details class="font-sans text-base list-none" @toggle="open = $el.open; if ($el.open) { closeOtherDropdowns($el); }">
|
||||
<summary class="cursor-pointer text-gray-700 hover:text-slate-900 bg-gray-100 px-3 py-1.5 rounded-md flex items-center gap-2"
|
||||
<summary class="cursor-pointer select-none text-gray-700 hover:text-slate-900 bg-gray-100 px-3 py-1.5 rounded-md flex items-center gap-2"
|
||||
:class="activeFilterType === 'year' ? 'font-semibold text-slate-900 ring-1 ring-slate-300' : ''">
|
||||
<span x-text="activeFilterType === 'year' ? `Jahr: ${yearLabels[activeFilterValue] || activeFilterValue}` : 'Jahr'"></span>
|
||||
<i class="ri-arrow-down-s-line transform origin-center transition-transform" :class="{ 'rotate-180': open }"></i>
|
||||
@@ -435,7 +396,7 @@ class="container-normal font-sans mt-10">
|
||||
<!-- Ort filter -->
|
||||
<div class="relative" x-data="{ open: false }" data-role="baende-filter">
|
||||
<details class="font-sans text-base list-none" @toggle="open = $el.open; if ($el.open) { closeOtherDropdowns($el); }">
|
||||
<summary class="cursor-pointer text-gray-700 hover:text-slate-900 bg-gray-100 px-3 py-1.5 rounded-md flex items-center gap-2"
|
||||
<summary class="cursor-pointer select-none text-gray-700 hover:text-slate-900 bg-gray-100 px-3 py-1.5 rounded-md flex items-center gap-2"
|
||||
:class="activeFilterType === 'place' ? 'font-semibold text-slate-900 ring-1 ring-slate-300' : ''">
|
||||
<span x-text="activeFilterType === 'place' ? `Ort: ${placeLabels[activeFilterValue] || activeFilterValue}` : 'Ort'"></span>
|
||||
<i class="ri-arrow-down-s-line transform origin-center transition-transform" :class="{ 'rotate-180': open }"></i>
|
||||
@@ -482,11 +443,51 @@ class="container-normal font-sans mt-10">
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Row 2: Spalten + count -->
|
||||
<div class="flex flex-wrap items-center justify-end gap-3 pb-0 mt-3">
|
||||
<!-- Row 2: Search + Spalten + count -->
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 pb-0 mt-3">
|
||||
<!-- Search box -->
|
||||
<div class="min-w-[22.5rem] max-w-96 flex flex-row bg-stone-50 relative font-sans text-lg items-center">
|
||||
<div>
|
||||
<i class="ri-search-line"></i><i class="-ml-0.5 inline-block ri-arrow-right-s-line"></i>
|
||||
</div>
|
||||
<div class="border-b-4 border-zinc-300 grow">
|
||||
<form
|
||||
method="GET"
|
||||
action="/baende/"
|
||||
hx-get="/baende/results/"
|
||||
hx-indicator="#baende-search-spinner"
|
||||
hx-push-url="false"
|
||||
hx-swap="outerHTML"
|
||||
hx-target="#baenderesults"
|
||||
role="search"
|
||||
@submit="offset = 0; hasMore = true"
|
||||
aria-label="Bändesuche">
|
||||
<input type="hidden" name="sort" :value="sortField" />
|
||||
<input type="hidden" name="order" :value="sortOrder" />
|
||||
<div class="relative">
|
||||
<input
|
||||
class="px-2 py-0.5 pr-7 font-sans placeholder:italic w-full text-lg"
|
||||
type="search"
|
||||
name="search"
|
||||
value="{{ $model.search }}"
|
||||
placeholder="Signatur oder Suchbegriff"
|
||||
x-model="search"
|
||||
@input.debounce.500="selectedLetter = ''; clearFilters(); ((search.trim().length >= 3) || /^[0-9]+$/.test(search.trim()) || search === '') && $el.form.requestSubmit()"
|
||||
@search.debounce.500="selectedLetter = ''; clearFilters(); ((search.trim().length >= 3) || /^[0-9]+$/.test(search.trim()) || search === '') && $el.form.requestSubmit()"
|
||||
autocomplete="off" />
|
||||
<span id="baende-search-spinner" class="htmx-indicator absolute right-1 top-1/2 -translate-y-1/2 text-slate-900">
|
||||
<i class="ri-loader-4-line spinning" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
<button x-show="false">Suchen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-end gap-3">
|
||||
<div class="relative" x-data="{ open: false }">
|
||||
<details class="font-sans text-base list-none" data-role="baende-column-toggle" @toggle="open = $el.open; if ($el.open) { closeOtherDropdowns($el); }">
|
||||
<summary class="cursor-pointer text-gray-600 hover:text-slate-900 px-2 py-1 rounded-xs flex items-center gap-2 text-lg font-semibold font-sans">
|
||||
<summary class="cursor-pointer select-none text-gray-600 hover:text-slate-900 px-2 py-1 rounded-xs flex items-center gap-2 text-lg font-semibold font-sans">
|
||||
<i class="ri-eye-line"></i>
|
||||
<span>Spalten</span>
|
||||
<i class="ri-arrow-down-s-line transform origin-center transition-transform" :class="{ 'rotate-180': open }"></i>
|
||||
@@ -504,10 +505,9 @@ class="container-normal font-sans mt-10">
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<div id="baende-count" class="text-lg font-semibold font-sans text-gray-600 whitespace-nowrap">
|
||||
{{ if $model.current_count }}{{ $model.current_count }} / {{ end }}{{ if $model.total_count }}{{ $model.total_count }}{{ else }}{{ len $model.result.Entries }}{{ end }} Bände
|
||||
</div>
|
||||
<div id="baende-count" class="text-lg font-semibold font-sans text-gray-600 whitespace-nowrap">
|
||||
{{ if $model.current_count }}{{ $model.current_count }} / {{ end }}{{ if $model.total_count }}{{ $model.total_count }}{{ else }}{{ len $model.result.Entries }}{{ end }} Bände
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -544,6 +544,27 @@ class="container-normal font-sans mt-10">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dialog data-role="baende-delete-dialog" class="dbform fixed inset-0 m-auto rounded-md border border-slate-200 p-0 shadow-xl backdrop:bg-black/40">
|
||||
<div class="p-5 w-[26rem]">
|
||||
<div class="text-base font-bold text-gray-900">Band löschen?</div>
|
||||
<div data-role="baende-delete-title" class="text-sm font-bold text-gray-900 mt-1"></div>
|
||||
<div data-role="baende-delete-impacts" class="mt-2 text-sm text-gray-700">
|
||||
Lade Informationen …
|
||||
</div>
|
||||
<div data-role="baende-delete-error" class="mt-2 text-sm text-red-700 hidden"></div>
|
||||
<div class="flex items-center justify-end gap-3 mt-4">
|
||||
<button type="button" class="resetbutton w-auto px-3 py-1 text-sm" data-role="baende-delete-cancel">Abbrechen</button>
|
||||
<button
|
||||
type="button"
|
||||
class="submitbutton w-auto bg-red-700 hover:bg-red-800 px-3 py-1 text-sm"
|
||||
data-role="baende-delete-confirm">
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
<input type="hidden" data-role="baende-delete-csrf" value="{{ $model.csrf_token }}" />
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const filterRoots = document.querySelectorAll('[data-role="baende-filter"]');
|
||||
@@ -563,4 +584,146 @@ class="container-normal font-sans mt-10">
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
(() => {
|
||||
const dialog = document.querySelector("[data-role='baende-delete-dialog']");
|
||||
if (!dialog) return;
|
||||
const titleEl = dialog.querySelector("[data-role='baende-delete-title']");
|
||||
const impactsEl = dialog.querySelector("[data-role='baende-delete-impacts']");
|
||||
const errorEl = dialog.querySelector("[data-role='baende-delete-error']");
|
||||
const cancelBtn = dialog.querySelector("[data-role='baende-delete-cancel']");
|
||||
const confirmBtn = dialog.querySelector("[data-role='baende-delete-confirm']");
|
||||
const csrfInput = dialog.querySelector("[data-role='baende-delete-csrf']");
|
||||
|
||||
let currentEntryId = "";
|
||||
let currentDeleteEndpoint = "";
|
||||
|
||||
const closeDialog = (event) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (dialog.open) {
|
||||
dialog.close();
|
||||
}
|
||||
};
|
||||
|
||||
const openDialog = () => {
|
||||
if (typeof dialog.showModal === "function") {
|
||||
dialog.showModal();
|
||||
}
|
||||
};
|
||||
|
||||
const setError = (message) => {
|
||||
if (!errorEl) return;
|
||||
if (message) {
|
||||
errorEl.textContent = message;
|
||||
errorEl.classList.remove("hidden");
|
||||
} else {
|
||||
errorEl.textContent = "";
|
||||
errorEl.classList.add("hidden");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = async (event) => {
|
||||
let button = null;
|
||||
const path = typeof event.composedPath === "function" ? event.composedPath() : [];
|
||||
for (const node of path) {
|
||||
if (node && node.getAttribute && node.getAttribute("data-role") === "baende-delete") {
|
||||
button = node;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!button && event.target && event.target.closest) {
|
||||
button = event.target.closest("[data-role='baende-delete']");
|
||||
}
|
||||
if (!button) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
currentEntryId = button.getAttribute("data-entry-id") || "";
|
||||
currentDeleteEndpoint = button.getAttribute("data-delete-endpoint") || "";
|
||||
const entryTitle = button.getAttribute("data-entry-title") || "";
|
||||
|
||||
if (titleEl) {
|
||||
titleEl.textContent = entryTitle ? entryTitle : "Unbekannter Eintrag";
|
||||
}
|
||||
if (impactsEl) {
|
||||
impactsEl.textContent = "Lade Informationen …";
|
||||
}
|
||||
setError("");
|
||||
openDialog();
|
||||
|
||||
if (!currentEntryId || !impactsEl) return;
|
||||
try {
|
||||
const response = await fetch(`/baende/delete-info/${encodeURIComponent(currentEntryId)}`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Infos konnten nicht geladen werden.");
|
||||
}
|
||||
const html = await response.text();
|
||||
impactsEl.innerHTML = html;
|
||||
} catch (error) {
|
||||
impactsEl.textContent = "Infos konnten nicht geladen werden.";
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("click", handleDeleteClick, true);
|
||||
|
||||
if (cancelBtn) {
|
||||
cancelBtn.addEventListener("click", closeDialog);
|
||||
}
|
||||
dialog.addEventListener("cancel", closeDialog);
|
||||
|
||||
if (confirmBtn) {
|
||||
confirmBtn.addEventListener("click", async (event) => {
|
||||
event.preventDefault();
|
||||
if (!currentDeleteEndpoint || !csrfInput) return;
|
||||
setError("");
|
||||
try {
|
||||
const response = await fetch(currentDeleteEndpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
csrf_token: csrfInput.value || "",
|
||||
}),
|
||||
});
|
||||
let data = null;
|
||||
try {
|
||||
data = await response.clone().json();
|
||||
} catch {
|
||||
data = null;
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.error || "Löschen fehlgeschlagen.");
|
||||
}
|
||||
closeDialog();
|
||||
if (currentEntryId) {
|
||||
const rows = document.querySelectorAll(`tr[data-entry-id='${CSS.escape(currentEntryId)}']`);
|
||||
rows.forEach((row) => row.remove());
|
||||
const countEl = document.getElementById("baende-count");
|
||||
if (countEl) {
|
||||
const raw = countEl.textContent || "";
|
||||
const hasSlash = raw.includes("/");
|
||||
const nums = raw.match(/\d+/g) || [];
|
||||
if (nums.length > 0) {
|
||||
let current = parseInt(nums[0], 10);
|
||||
let total = nums.length > 1 ? parseInt(nums[1], 10) : null;
|
||||
if (!Number.isNaN(current) && current > 0) current -= 1;
|
||||
if (total !== null && !Number.isNaN(total) && total > 0) total -= 1;
|
||||
if (hasSlash && total !== null) {
|
||||
countEl.textContent = `${current} / ${total} Bände`;
|
||||
} else {
|
||||
countEl.textContent = `${current} Bände`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : "Löschen fehlgeschlagen.");
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -157,15 +157,19 @@
|
||||
</a>
|
||||
<div class="data-tip">Bearbeiten</div>
|
||||
</tool-tip>
|
||||
<form method="POST" action="/almanach/{{ $entry.MusenalmID }}/edit/delete" class="inline" onsubmit="event.stopPropagation(); return confirm('Band wirklich löschen?');">
|
||||
<input type="hidden" name="csrf_token" value="{{ $model.csrf_token }}" />
|
||||
<tool-tip position="top" class="inline">
|
||||
<button type="submit" onclick="event.stopPropagation();" class="inline-flex items-center gap-1 rounded-xs bg-red-50 px-2 py-1 text-xs font-semibold text-red-700 hover:bg-red-100 hover:text-red-900">
|
||||
<i class="ri-delete-bin-line"></i>
|
||||
</button>
|
||||
<div class="data-tip">Löschen</div>
|
||||
</tool-tip>
|
||||
</form>
|
||||
<tool-tip position="top" class="inline">
|
||||
<button
|
||||
type="button"
|
||||
onclick="event.stopPropagation();"
|
||||
data-role="baende-delete"
|
||||
data-entry-id="{{ $entry.MusenalmID }}"
|
||||
data-entry-title="{{- if $entry.PreferredTitle -}}{{ $entry.PreferredTitle }}{{- else if ne $entry.Year 0 -}}{{ $entry.Year }}{{- else -}}[o.J.]{{- end -}}"
|
||||
data-delete-endpoint="/almanach/{{ $entry.MusenalmID }}/edit/delete"
|
||||
class="inline-flex items-center gap-1 rounded-xs bg-red-50 px-2 py-1 text-xs font-semibold text-red-700 hover:bg-red-100 hover:text-red-900">
|
||||
<i class="ri-delete-bin-line"></i>
|
||||
</button>
|
||||
<div class="data-tip">Löschen</div>
|
||||
</tool-tip>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
61
views/routes/baende/delete_info/body.gohtml
Normal file
61
views/routes/baende/delete_info/body.gohtml
Normal file
@@ -0,0 +1,61 @@
|
||||
{{ $entry := .entry }}
|
||||
{{ $items := .items }}
|
||||
{{ $contents := .contents }}
|
||||
|
||||
<div>
|
||||
<div class="text-sm text-gray-700">
|
||||
Der Eintrag wird dauerhaft gelöscht. Verknüpfungen, Exemplare und Beiträge werden entfernt.
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="text-sm font-semibold text-gray-700">Betroffene Beiträge ({{ len $contents }})</div>
|
||||
<div class="mt-2 max-h-40 overflow-auto pr-1">
|
||||
{{- if $contents -}}
|
||||
<ul class="flex flex-col gap-2 pl-0 pr-0 m-0 list-none">
|
||||
{{- range $content := $contents -}}
|
||||
<li class="flex items-baseline justify-between gap-3 ml-0 pl-0 text-sm text-gray-700">
|
||||
<span>
|
||||
{{- if $content.PreferredTitle -}}
|
||||
{{ $content.PreferredTitle }}
|
||||
{{- else if $content.TitleStmt -}}
|
||||
{{ $content.TitleStmt }}
|
||||
{{- else -}}
|
||||
[Ohne Titel]
|
||||
{{- end -}}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">Alm {{ $content.MusenalmID }}</span>
|
||||
</li>
|
||||
{{- end -}}
|
||||
</ul>
|
||||
{{- else -}}
|
||||
<div class="italic text-gray-500">Keine Beiträge betroffen.</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="text-sm font-semibold text-gray-700">Betroffene Exemplare ({{ len $items }})</div>
|
||||
<div class="mt-2 max-h-40 overflow-auto pr-1">
|
||||
{{- if $items -}}
|
||||
<ul class="flex flex-col gap-2 pl-0 pr-0 m-0 list-none">
|
||||
{{- range $item := $items -}}
|
||||
<li class="flex items-baseline justify-between gap-3 ml-0 pl-0 text-sm text-gray-700">
|
||||
<span>
|
||||
{{- if $item.Identifier -}}
|
||||
{{ $item.Identifier }}
|
||||
{{- else -}}
|
||||
[Ohne Signatur]
|
||||
{{- end -}}
|
||||
</span>
|
||||
{{- if $item.Location -}}
|
||||
<span class="text-xs text-gray-500">{{ $item.Location }}</span>
|
||||
{{- end -}}
|
||||
</li>
|
||||
{{- end -}}
|
||||
</ul>
|
||||
{{- else -}}
|
||||
<div class="italic text-gray-500">Keine Exemplare betroffen.</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -21,16 +21,19 @@
|
||||
</a>
|
||||
<div class="data-tip">Bearbeiten</div>
|
||||
</tool-tip>
|
||||
<form method="POST" action="/almanach/{{ $entry.MusenalmID }}/edit/delete" class="inline" onsubmit="event.stopPropagation(); return confirm('Band wirklich löschen?');">
|
||||
<input type="hidden" name="csrf_token" value="{{ $result.CSRFToken }}" />
|
||||
<tool-tip position="top" class="inline">
|
||||
<button type="submit" onclick="event.stopPropagation();" class="inline-flex items-center gap-1 rounded-xs bg-red-50 px-2 py-1 text-xs font-semibold text-red-700 hover:bg-red-100 hover:text-red-900">
|
||||
<i class="ri-delete-bin-line"></i>
|
||||
|
||||
</button>
|
||||
<div class="data-tip">Löschen</div>
|
||||
</tool-tip>
|
||||
</form>
|
||||
<tool-tip position="top" class="inline">
|
||||
<button
|
||||
type="button"
|
||||
onclick="event.stopPropagation();"
|
||||
data-role="baende-delete"
|
||||
data-entry-id="{{ $entry.MusenalmID }}"
|
||||
data-entry-title="{{- if $entry.PreferredTitle -}}{{ $entry.PreferredTitle }}{{- else if ne $entry.Year 0 -}}{{ $entry.Year }}{{- else -}}[o.J.]{{- end -}}"
|
||||
data-delete-endpoint="/almanach/{{ $entry.MusenalmID }}/edit/delete"
|
||||
class="inline-flex items-center gap-1 rounded-xs bg-red-50 px-2 py-1 text-xs font-semibold text-red-700 hover:bg-red-100 hover:text-red-900">
|
||||
<i class="ri-delete-bin-line"></i>
|
||||
</button>
|
||||
<div class="data-tip">Löschen</div>
|
||||
</tool-tip>
|
||||
{{- end -}}
|
||||
<tool-tip position="top" class="inline">
|
||||
<button
|
||||
|
||||
@@ -27,15 +27,19 @@
|
||||
</a>
|
||||
<div class="data-tip">Bearbeiten</div>
|
||||
</tool-tip>
|
||||
<form method="POST" action="/almanach/{{ $entry.MusenalmID }}/edit/delete" class="inline" onsubmit="event.stopPropagation(); return confirm('Band wirklich löschen?');">
|
||||
<input type="hidden" name="csrf_token" value="{{ $model.csrf_token }}" />
|
||||
<tool-tip position="top" class="inline">
|
||||
<button type="submit" onclick="event.stopPropagation();" class="inline-flex items-center gap-1 rounded-xs bg-red-50 px-2 py-1 text-xs font-semibold text-red-700 hover:bg-red-100 hover:text-red-900">
|
||||
<i class="ri-delete-bin-line"></i>
|
||||
</button>
|
||||
<div class="data-tip">Löschen</div>
|
||||
</tool-tip>
|
||||
</form>
|
||||
<tool-tip position="top" class="inline">
|
||||
<button
|
||||
type="button"
|
||||
onclick="event.stopPropagation();"
|
||||
data-role="baende-delete"
|
||||
data-entry-id="{{ $entry.MusenalmID }}"
|
||||
data-entry-title="{{- if $entry.PreferredTitle -}}{{ $entry.PreferredTitle }}{{- else if ne $entry.Year 0 -}}{{ $entry.Year }}{{- else -}}[o.J.]{{- end -}}"
|
||||
data-delete-endpoint="/almanach/{{ $entry.MusenalmID }}/edit/delete"
|
||||
class="inline-flex items-center gap-1 rounded-xs bg-red-50 px-2 py-1 text-xs font-semibold text-red-700 hover:bg-red-100 hover:text-red-900">
|
||||
<i class="ri-delete-bin-line"></i>
|
||||
</button>
|
||||
<div class="data-tip">Löschen</div>
|
||||
</tool-tip>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,15 +27,19 @@
|
||||
</a>
|
||||
<div class="data-tip">Bearbeiten</div>
|
||||
</tool-tip>
|
||||
<form method="POST" action="/almanach/{{ $entry.MusenalmID }}/edit/delete" class="inline" onsubmit="event.stopPropagation(); return confirm('Band wirklich löschen?');">
|
||||
<input type="hidden" name="csrf_token" value="{{ $model.csrf_token }}" />
|
||||
<tool-tip position="top" class="inline">
|
||||
<button type="submit" onclick="event.stopPropagation();" class="inline-flex items-center gap-1 rounded-xs bg-red-50 px-2 py-1 text-xs font-semibold text-red-700 hover:bg-red-100 hover:text-red-900">
|
||||
<i class="ri-delete-bin-line"></i>
|
||||
</button>
|
||||
<div class="data-tip">Löschen</div>
|
||||
</tool-tip>
|
||||
</form>
|
||||
<tool-tip position="top" class="inline">
|
||||
<button
|
||||
type="button"
|
||||
onclick="event.stopPropagation();"
|
||||
data-role="baende-delete"
|
||||
data-entry-id="{{ $entry.MusenalmID }}"
|
||||
data-entry-title="{{- if $entry.PreferredTitle -}}{{ $entry.PreferredTitle }}{{- else if ne $entry.Year 0 -}}{{ $entry.Year }}{{- else -}}[o.J.]{{- end -}}"
|
||||
data-delete-endpoint="/almanach/{{ $entry.MusenalmID }}/edit/delete"
|
||||
class="inline-flex items-center gap-1 rounded-xs bg-red-50 px-2 py-1 text-xs font-semibold text-red-700 hover:bg-red-100 hover:text-red-900">
|
||||
<i class="ri-delete-bin-line"></i>
|
||||
</button>
|
||||
<div class="data-tip">Löschen</div>
|
||||
</tool-tip>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user