mirror of
https://github.com/Theodor-Springmann-Stiftung/musenalm.git
synced 2026-02-04 10:35:30 +00:00
+Mor almanach edit, finished
This commit is contained in:
@@ -45,6 +45,7 @@ func (p *AlmanachEditPage) Setup(router *router.Router[*core.RequestEvent], app
|
|||||||
rg.BindFunc(middleware.IsAdminOrEditor())
|
rg.BindFunc(middleware.IsAdminOrEditor())
|
||||||
rg.GET(URL_ALMANACH_EDIT, p.GET(engine, app))
|
rg.GET(URL_ALMANACH_EDIT, p.GET(engine, app))
|
||||||
rg.POST(URL_ALMANACH_EDIT+"save", p.POSTSave(engine, app))
|
rg.POST(URL_ALMANACH_EDIT+"save", p.POSTSave(engine, app))
|
||||||
|
rg.POST(URL_ALMANACH_EDIT+"delete", p.POSTDelete(engine, app))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,6 +202,74 @@ func (p *AlmanachEditPage) POSTSave(engine *templating.Engine, app core.App) Han
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *AlmanachEditPage) POSTDelete(engine *templating.Engine, app core.App) HandleFunc {
|
||||||
|
return func(e *core.RequestEvent) error {
|
||||||
|
id := e.Request.PathValue("id")
|
||||||
|
req := templating.NewRequest(e)
|
||||||
|
|
||||||
|
payload := almanachDeletePayload{}
|
||||||
|
if err := e.BindBody(&payload); err != nil {
|
||||||
|
return e.JSON(http.StatusBadRequest, map[string]any{
|
||||||
|
"error": "Ungültige Formulardaten.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := req.CheckCSRF(payload.CSRFToken); err != nil {
|
||||||
|
return e.JSON(http.StatusBadRequest, map[string]any{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
entry, err := dbmodels.Entries_MusenalmID(app, id)
|
||||||
|
if err != nil {
|
||||||
|
return e.JSON(http.StatusNotFound, map[string]any{
|
||||||
|
"error": "Band wurde nicht gefunden.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.LastEdited != "" {
|
||||||
|
lastEdited, err := types.ParseDateTime(payload.LastEdited)
|
||||||
|
if err != nil {
|
||||||
|
return e.JSON(http.StatusBadRequest, map[string]any{
|
||||||
|
"error": "Ungültiger Bearbeitungszeitstempel.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if !entry.Updated().Time().Equal(lastEdited.Time()) {
|
||||||
|
return e.JSON(http.StatusConflict, map[string]any{
|
||||||
|
"error": "Der Eintrag wurde inzwischen geändert. Bitte Seite neu laden.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.RunInTransaction(func(tx core.App) error {
|
||||||
|
if err := deleteEntryRelations(tx, entry.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := deleteEntryItems(tx, entry.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := deleteEntryContents(tx, entry.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
record, err := tx.FindRecordById(dbmodels.ENTRIES_TABLE, entry.Id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Delete(record)
|
||||||
|
}); err != nil {
|
||||||
|
app.Logger().Error("Failed to delete almanach entry", "entry_id", entry.Id, "error", err)
|
||||||
|
return e.JSON(http.StatusInternalServerError, map[string]any{
|
||||||
|
"error": "Löschen fehlgeschlagen.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.JSON(http.StatusOK, map[string]any{
|
||||||
|
"success": true,
|
||||||
|
"redirect": "/suche/baende",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type almanachEditPayload struct {
|
type almanachEditPayload struct {
|
||||||
CSRFToken string `json:"csrf_token"`
|
CSRFToken string `json:"csrf_token"`
|
||||||
LastEdited string `json:"last_edited"`
|
LastEdited string `json:"last_edited"`
|
||||||
@@ -217,6 +286,11 @@ type almanachEditPayload struct {
|
|||||||
DeletedAgentRelationIDs []string `json:"deleted_agent_relation_ids"`
|
DeletedAgentRelationIDs []string `json:"deleted_agent_relation_ids"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type almanachDeletePayload struct {
|
||||||
|
CSRFToken string `json:"csrf_token"`
|
||||||
|
LastEdited string `json:"last_edited"`
|
||||||
|
}
|
||||||
|
|
||||||
type almanachEntryPayload struct {
|
type almanachEntryPayload struct {
|
||||||
PreferredTitle string `json:"preferred_title"`
|
PreferredTitle string `json:"preferred_title"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
@@ -292,24 +366,23 @@ func (payload *almanachEditPayload) Validate() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hasPreferred := false
|
preferredCount := 0
|
||||||
for _, relation := range payload.SeriesRelations {
|
for _, relation := range payload.SeriesRelations {
|
||||||
if strings.TrimSpace(relation.Type) == preferredSeriesRelationType {
|
if strings.TrimSpace(relation.Type) == preferredSeriesRelationType {
|
||||||
hasPreferred = true
|
preferredCount++
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !hasPreferred {
|
|
||||||
for _, relation := range payload.NewSeriesRelations {
|
for _, relation := range payload.NewSeriesRelations {
|
||||||
if strings.TrimSpace(relation.Type) == preferredSeriesRelationType {
|
if strings.TrimSpace(relation.Type) == preferredSeriesRelationType {
|
||||||
hasPreferred = true
|
preferredCount++
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if preferredCount == 0 {
|
||||||
if !hasPreferred {
|
|
||||||
return fmt.Errorf("Mindestens ein bevorzugter Reihentitel muss verknüpft sein.")
|
return fmt.Errorf("Mindestens ein bevorzugter Reihentitel muss verknüpft sein.")
|
||||||
}
|
}
|
||||||
|
if preferredCount > 1 {
|
||||||
|
return fmt.Errorf("Es darf nur ein bevorzugter Reihentitel gesetzt sein.")
|
||||||
|
}
|
||||||
|
|
||||||
// Check for duplicate series relations
|
// Check for duplicate series relations
|
||||||
seriesTargetIDs := make(map[string]bool)
|
seriesTargetIDs := make(map[string]bool)
|
||||||
@@ -604,3 +677,85 @@ func applyAgentRelations(tx core.App, entry *dbmodels.Entry, payload *almanachEd
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func deleteEntryRelations(tx core.App, entryID string) error {
|
||||||
|
seriesRelations, err := dbmodels.REntriesSeries_Entry(tx, entryID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
seriesTable := dbmodels.RelationTableName(dbmodels.ENTRIES_TABLE, dbmodels.SERIES_TABLE)
|
||||||
|
for _, relation := range seriesRelations {
|
||||||
|
record, err := tx.FindRecordById(seriesTable, relation.Id)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := tx.Delete(record); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
agentRelations, err := dbmodels.REntriesAgents_Entry(tx, entryID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
agentTable := dbmodels.RelationTableName(dbmodels.ENTRIES_TABLE, dbmodels.AGENTS_TABLE)
|
||||||
|
for _, relation := range agentRelations {
|
||||||
|
record, err := tx.FindRecordById(agentTable, relation.Id)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := tx.Delete(record); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteEntryItems(tx core.App, entryID string) error {
|
||||||
|
items, err := dbmodels.Items_Entry(tx, entryID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, item := range items {
|
||||||
|
record, err := tx.FindRecordById(dbmodels.ITEMS_TABLE, item.Id)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := tx.Delete(record); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteEntryContents(tx core.App, entryID string) error {
|
||||||
|
contents, err := dbmodels.Contents_Entry(tx, entryID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
relationsTable := dbmodels.RelationTableName(dbmodels.CONTENTS_TABLE, dbmodels.AGENTS_TABLE)
|
||||||
|
for _, content := range contents {
|
||||||
|
contentRelations, err := dbmodels.RContentsAgents_Content(tx, content.Id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, relation := range contentRelations {
|
||||||
|
record, err := tx.FindRecordById(relationsTable, relation.Id)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := tx.Delete(record); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
record, err := tx.FindRecordById(dbmodels.CONTENTS_TABLE, content.Id)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := tx.Delete(record); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -105,7 +105,8 @@ type AlmanachResult struct {
|
|||||||
x-target="changealmanachform user-message almanach-header-data"
|
x-target="changealmanachform user-message almanach-header-data"
|
||||||
hx-boost="false"
|
hx-boost="false"
|
||||||
method="POST"
|
method="POST"
|
||||||
data-save-endpoint="/almanach/{{ $model.result.Entry.MusenalmID }}/edit/save">
|
data-save-endpoint="/almanach/{{ $model.result.Entry.MusenalmID }}/edit/save"
|
||||||
|
data-delete-endpoint="/almanach/{{ $model.result.Entry.MusenalmID }}/edit/delete">
|
||||||
<input type="hidden" name="csrf_token" value="{{ $model.csrf_token }}" />
|
<input type="hidden" name="csrf_token" value="{{ $model.csrf_token }}" />
|
||||||
<input type="hidden" name="last_edited" value="{{ $model.result.Entry.Updated }}" />
|
<input type="hidden" name="last_edited" value="{{ $model.result.Entry.Updated }}" />
|
||||||
|
|
||||||
@@ -282,7 +283,7 @@ type AlmanachResult struct {
|
|||||||
</div>
|
</div>
|
||||||
<hr class="border-slate-400 mt-2 mb-3" />
|
<hr class="border-slate-400 mt-2 mb-3" />
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<relations-editor data-prefix="entries_series" data-link-base="/reihe/" data-new-label="(Neu)" data-add-toggle-id="series-add-toggle">
|
<relations-editor data-prefix="entries_series" data-link-base="/reihe/" data-new-label="(Neu)" data-add-toggle-id="series-add-toggle" data-preferred-label="Bevorzugter Reihentitel">
|
||||||
<div class="inputwrapper">
|
<div class="inputwrapper">
|
||||||
<label class="inputlabel" for="series-section">Reihen</label>
|
<label class="inputlabel" for="series-section">Reihen</label>
|
||||||
<div id="series-section" class="rel-section-container">
|
<div id="series-section" class="rel-section-container">
|
||||||
@@ -887,11 +888,46 @@ type AlmanachResult struct {
|
|||||||
</div>
|
</div>
|
||||||
<div class="w-full flex items-end justify-between gap-4 mt-6 flex-wrap">
|
<div class="w-full flex items-end justify-between gap-4 mt-6 flex-wrap">
|
||||||
<p id="almanach-save-feedback" class="text-sm text-gray-600" aria-live="polite"></p>
|
<p id="almanach-save-feedback" class="text-sm text-gray-600" aria-live="polite"></p>
|
||||||
<button type="button" class="submitbutton flex items-center gap-2 self-end" data-role="almanach-save">
|
<div class="flex items-center gap-3 self-end flex-wrap">
|
||||||
|
<a href="/almanach/{{ $model.result.Entry.MusenalmID }}" class="resetbutton w-40 flex items-center gap-2 justify-center">
|
||||||
|
<i class="ri-close-line"></i>
|
||||||
|
<span>Abbrechen</span>
|
||||||
|
</a>
|
||||||
|
<button type="button" class="resetbutton w-40 flex items-center gap-2 justify-center" data-role="almanach-reset">
|
||||||
|
<i class="ri-loop-left-line"></i>
|
||||||
|
<span>Reset</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="resetbutton w-40 flex items-center gap-2 justify-center bg-red-50 text-red-800 hover:bg-red-100 hover:text-red-900"
|
||||||
|
data-role="almanach-delete">
|
||||||
|
<i class="ri-delete-bin-line"></i>
|
||||||
|
<span>Eintrag löschen</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="submitbutton w-40 flex items-center gap-2 justify-center" data-role="almanach-save">
|
||||||
<i class="ri-save-line"></i>
|
<i class="ri-save-line"></i>
|
||||||
<span>Speichern</span>
|
<span>Speichern</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<dialog data-role="almanach-delete-dialog" class="fixed inset-0 m-auto rounded-md border border-slate-200 p-0 shadow-xl backdrop:bg-black/40">
|
||||||
|
<div class="p-5 w-[22rem]">
|
||||||
|
<div class="text-base font-bold text-gray-900">Eintrag löschen?</div>
|
||||||
|
<div class="text-sm font-bold text-gray-900 mt-1">{{ $model.result.Entry.PreferredTitle }}</div>
|
||||||
|
<p class="text-sm text-gray-700 mt-2">
|
||||||
|
Der Eintrag wird dauerhaft gelöscht. Verknüpfungen, Exemplare und Inhalte werden entfernt.
|
||||||
|
</p>
|
||||||
|
<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="almanach-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="almanach-delete-confirm">
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</almanach-edit-page>
|
</almanach-edit-page>
|
||||||
|
|||||||
@@ -6,10 +6,20 @@ export class AlmanachEditPage extends HTMLElement {
|
|||||||
this._pendingAgent = null;
|
this._pendingAgent = null;
|
||||||
this._form = null;
|
this._form = null;
|
||||||
this._saveButton = null;
|
this._saveButton = null;
|
||||||
|
this._resetButton = null;
|
||||||
|
this._deleteButton = null;
|
||||||
|
this._deleteDialog = null;
|
||||||
|
this._deleteConfirmButton = null;
|
||||||
|
this._deleteCancelButton = null;
|
||||||
this._statusEl = null;
|
this._statusEl = null;
|
||||||
this._saveEndpoint = "";
|
this._saveEndpoint = "";
|
||||||
|
this._deleteEndpoint = "";
|
||||||
this._isSaving = false;
|
this._isSaving = false;
|
||||||
this._handleSaveClick = this._handleSaveClick.bind(this);
|
this._handleSaveClick = this._handleSaveClick.bind(this);
|
||||||
|
this._handleResetClick = this._handleResetClick.bind(this);
|
||||||
|
this._handleDeleteClick = this._handleDeleteClick.bind(this);
|
||||||
|
this._handleDeleteConfirmClick = this._handleDeleteConfirmClick.bind(this);
|
||||||
|
this._handleDeleteCancelClick = this._handleDeleteCancelClick.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
@@ -86,19 +96,60 @@ export class AlmanachEditPage extends HTMLElement {
|
|||||||
this._teardownSaveHandling();
|
this._teardownSaveHandling();
|
||||||
this._form = this.querySelector("#changealmanachform");
|
this._form = this.querySelector("#changealmanachform");
|
||||||
this._saveButton = this.querySelector("[data-role='almanach-save']");
|
this._saveButton = this.querySelector("[data-role='almanach-save']");
|
||||||
|
this._resetButton = this.querySelector("[data-role='almanach-reset']");
|
||||||
|
this._deleteButton = this.querySelector("[data-role='almanach-delete']");
|
||||||
|
this._deleteDialog = this.querySelector("[data-role='almanach-delete-dialog']");
|
||||||
|
this._deleteConfirmButton = this.querySelector("[data-role='almanach-delete-confirm']");
|
||||||
|
this._deleteCancelButton = this.querySelector("[data-role='almanach-delete-cancel']");
|
||||||
this._statusEl = this.querySelector("#almanach-save-feedback");
|
this._statusEl = this.querySelector("#almanach-save-feedback");
|
||||||
if (!this._form || !this._saveButton) {
|
if (!this._form || !this._saveButton) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._saveEndpoint = this._form.getAttribute("data-save-endpoint") || this._deriveSaveEndpoint();
|
this._saveEndpoint = this._form.getAttribute("data-save-endpoint") || this._deriveSaveEndpoint();
|
||||||
|
this._deleteEndpoint = this._form.getAttribute("data-delete-endpoint") || "";
|
||||||
this._saveButton.addEventListener("click", this._handleSaveClick);
|
this._saveButton.addEventListener("click", this._handleSaveClick);
|
||||||
|
if (this._resetButton) {
|
||||||
|
this._resetButton.addEventListener("click", this._handleResetClick);
|
||||||
|
}
|
||||||
|
if (this._deleteButton) {
|
||||||
|
this._deleteButton.addEventListener("click", this._handleDeleteClick);
|
||||||
|
}
|
||||||
|
if (this._deleteConfirmButton) {
|
||||||
|
this._deleteConfirmButton.addEventListener("click", this._handleDeleteConfirmClick);
|
||||||
|
}
|
||||||
|
if (this._deleteCancelButton) {
|
||||||
|
this._deleteCancelButton.addEventListener("click", this._handleDeleteCancelClick);
|
||||||
|
}
|
||||||
|
if (this._deleteDialog) {
|
||||||
|
this._deleteDialog.addEventListener("cancel", this._handleDeleteCancelClick);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_teardownSaveHandling() {
|
_teardownSaveHandling() {
|
||||||
if (this._saveButton) {
|
if (this._saveButton) {
|
||||||
this._saveButton.removeEventListener("click", this._handleSaveClick);
|
this._saveButton.removeEventListener("click", this._handleSaveClick);
|
||||||
}
|
}
|
||||||
|
if (this._resetButton) {
|
||||||
|
this._resetButton.removeEventListener("click", this._handleResetClick);
|
||||||
|
}
|
||||||
|
if (this._deleteButton) {
|
||||||
|
this._deleteButton.removeEventListener("click", this._handleDeleteClick);
|
||||||
|
}
|
||||||
|
if (this._deleteConfirmButton) {
|
||||||
|
this._deleteConfirmButton.removeEventListener("click", this._handleDeleteConfirmClick);
|
||||||
|
}
|
||||||
|
if (this._deleteCancelButton) {
|
||||||
|
this._deleteCancelButton.removeEventListener("click", this._handleDeleteCancelClick);
|
||||||
|
}
|
||||||
|
if (this._deleteDialog) {
|
||||||
|
this._deleteDialog.removeEventListener("cancel", this._handleDeleteCancelClick);
|
||||||
|
}
|
||||||
this._saveButton = null;
|
this._saveButton = null;
|
||||||
|
this._resetButton = null;
|
||||||
|
this._deleteButton = null;
|
||||||
|
this._deleteDialog = null;
|
||||||
|
this._deleteConfirmButton = null;
|
||||||
|
this._deleteCancelButton = null;
|
||||||
this._statusEl = null;
|
this._statusEl = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,6 +208,81 @@ export class AlmanachEditPage extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _handleResetClick(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (this._isSaving) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._clearStatus();
|
||||||
|
try {
|
||||||
|
await this._reloadForm("");
|
||||||
|
} catch (error) {
|
||||||
|
this._showStatus(error instanceof Error ? error.message : "Formular konnte nicht aktualisiert werden.", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _handleDeleteClick(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (this._isSaving) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._deleteDialog && typeof this._deleteDialog.showModal === "function") {
|
||||||
|
this._deleteDialog.showModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleDeleteCancelClick(event) {
|
||||||
|
if (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
if (this._deleteDialog && this._deleteDialog.open) {
|
||||||
|
this._deleteDialog.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _handleDeleteConfirmClick(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!this._form || !this._deleteEndpoint || this._isSaving) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._deleteDialog && this._deleteDialog.open) {
|
||||||
|
this._deleteDialog.close();
|
||||||
|
}
|
||||||
|
this._clearStatus();
|
||||||
|
this._setSavingState(true);
|
||||||
|
try {
|
||||||
|
const formData = new FormData(this._form);
|
||||||
|
const payload = {
|
||||||
|
csrf_token: this._readValue(formData, "csrf_token"),
|
||||||
|
last_edited: this._readValue(formData, "last_edited"),
|
||||||
|
};
|
||||||
|
const response = await fetch(this._deleteEndpoint, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
let data = null;
|
||||||
|
try {
|
||||||
|
data = await response.clone().json();
|
||||||
|
} catch {
|
||||||
|
data = null;
|
||||||
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
const message = data?.error || `Löschen fehlgeschlagen (${response.status}).`;
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
const redirect = data?.redirect || "/suche/baende";
|
||||||
|
window.location.assign(redirect);
|
||||||
|
} catch (error) {
|
||||||
|
this._showStatus(error instanceof Error ? error.message : "Löschen fehlgeschlagen.", "error");
|
||||||
|
} finally {
|
||||||
|
this._setSavingState(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_buildPayload() {
|
_buildPayload() {
|
||||||
if (!this._form) {
|
if (!this._form) {
|
||||||
throw new Error("Formular konnte nicht gefunden werden.");
|
throw new Error("Formular konnte nicht gefunden werden.");
|
||||||
@@ -206,12 +332,15 @@ export class AlmanachEditPage extends HTMLElement {
|
|||||||
targetField: "series",
|
targetField: "series",
|
||||||
});
|
});
|
||||||
const newSeriesRelations = this._collectNewRelations("entries_series");
|
const newSeriesRelations = this._collectNewRelations("entries_series");
|
||||||
const hasPreferredSeries = [...seriesRelations, ...newSeriesRelations].some(
|
const preferredCount = [...seriesRelations, ...newSeriesRelations].filter(
|
||||||
(relation) => relation.type === PREFERRED_SERIES_RELATION,
|
(relation) => relation.type === PREFERRED_SERIES_RELATION,
|
||||||
);
|
).length;
|
||||||
if (!hasPreferredSeries) {
|
if (preferredCount === 0) {
|
||||||
throw new Error("Mindestens ein bevorzugter Reihentitel muss verknüpft sein.");
|
throw new Error("Mindestens ein bevorzugter Reihentitel muss verknüpft sein.");
|
||||||
}
|
}
|
||||||
|
if (preferredCount > 1) {
|
||||||
|
throw new Error("Es darf nur ein bevorzugter Reihentitel gesetzt sein.");
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
relations: agentRelations,
|
relations: agentRelations,
|
||||||
@@ -375,6 +504,12 @@ export class AlmanachEditPage extends HTMLElement {
|
|||||||
if (label) {
|
if (label) {
|
||||||
label.textContent = isSaving ? "Speichern..." : "Speichern";
|
label.textContent = isSaving ? "Speichern..." : "Speichern";
|
||||||
}
|
}
|
||||||
|
if (this._resetButton) {
|
||||||
|
this._resetButton.disabled = isSaving;
|
||||||
|
}
|
||||||
|
if (this._deleteButton) {
|
||||||
|
this._deleteButton.disabled = isSaving;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_clearStatus() {
|
_clearStatus() {
|
||||||
|
|||||||
@@ -253,6 +253,12 @@ export class DivManager extends HTMLElement {
|
|||||||
// Small delay to ensure element is visible before measuring
|
// Small delay to ensure element is visible before measuring
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
textareas.forEach((textarea) => {
|
textareas.forEach((textarea) => {
|
||||||
|
if (textarea.dataset.dmResizeBound !== "true") {
|
||||||
|
textarea.dataset.dmResizeBound = "true";
|
||||||
|
textarea.addEventListener("input", () => {
|
||||||
|
window.TextareaAutoResize(textarea);
|
||||||
|
});
|
||||||
|
}
|
||||||
window.TextareaAutoResize(textarea);
|
window.TextareaAutoResize(textarea);
|
||||||
});
|
});
|
||||||
}, 10);
|
}, 10);
|
||||||
|
|||||||
@@ -24,9 +24,11 @@ export class RelationsEditor extends HTMLElement {
|
|||||||
this._linkBase = this.getAttribute("data-link-base") || "";
|
this._linkBase = this.getAttribute("data-link-base") || "";
|
||||||
this._newLabel = this.getAttribute("data-new-label") || "(Neu)";
|
this._newLabel = this.getAttribute("data-new-label") || "(Neu)";
|
||||||
this._addToggleId = this.getAttribute("data-add-toggle-id") || "";
|
this._addToggleId = this.getAttribute("data-add-toggle-id") || "";
|
||||||
|
this._preferredLabel = (this.getAttribute("data-preferred-label") || "").trim();
|
||||||
this._emptyText = this.querySelector(".rel-empty-text");
|
this._emptyText = this.querySelector(".rel-empty-text");
|
||||||
this._setupAddPanel();
|
this._setupAddPanel();
|
||||||
this._setupDeleteToggles();
|
this._setupDeleteToggles();
|
||||||
|
this._setupPreferredOptionHandling();
|
||||||
}
|
}
|
||||||
|
|
||||||
_getExistingIds() {
|
_getExistingIds() {
|
||||||
@@ -230,6 +232,7 @@ export class RelationsEditor extends HTMLElement {
|
|||||||
typeSelect.innerHTML = this._typeSelect.innerHTML;
|
typeSelect.innerHTML = this._typeSelect.innerHTML;
|
||||||
typeSelect.value = this._typeSelect.value;
|
typeSelect.value = this._typeSelect.value;
|
||||||
typeSelect.name = `${this._prefix}_new_type`;
|
typeSelect.name = `${this._prefix}_new_type`;
|
||||||
|
typeSelect.addEventListener("change", () => this._updatePreferredOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
const uncertain = fragment.querySelector("[data-rel-input='uncertain']");
|
const uncertain = fragment.querySelector("[data-rel-input='uncertain']");
|
||||||
@@ -271,6 +274,7 @@ export class RelationsEditor extends HTMLElement {
|
|||||||
this._addPanel.classList.add("hidden");
|
this._addPanel.classList.add("hidden");
|
||||||
}
|
}
|
||||||
this._updateEmptyTextVisibility();
|
this._updateEmptyTextVisibility();
|
||||||
|
this._updatePreferredOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
_setupDeleteToggles() {
|
_setupDeleteToggles() {
|
||||||
@@ -328,6 +332,8 @@ export class RelationsEditor extends HTMLElement {
|
|||||||
icon.classList.remove("ri-arrow-go-back-line");
|
icon.classList.remove("ri-arrow-go-back-line");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._updatePreferredOptions();
|
||||||
});
|
});
|
||||||
|
|
||||||
button.addEventListener("mouseenter", () => {
|
button.addEventListener("mouseenter", () => {
|
||||||
@@ -374,4 +380,79 @@ export class RelationsEditor extends HTMLElement {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_setupPreferredOptionHandling() {
|
||||||
|
if (this._prefix !== "entries_series" || !this._preferredLabel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.querySelectorAll(`select[name^="${this._prefix}_type["]`).forEach((select) => {
|
||||||
|
select.addEventListener("change", () => this._updatePreferredOptions());
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this._typeSelect) {
|
||||||
|
this._typeSelect.addEventListener("change", () => this._updatePreferredOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
this._updatePreferredOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
_updatePreferredOptions() {
|
||||||
|
if (this._prefix !== "entries_series" || !this._preferredLabel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const preferredLabel = this._preferredLabel.trim();
|
||||||
|
|
||||||
|
const selects = [];
|
||||||
|
this.querySelectorAll(`select[name^="${this._prefix}_type["]`).forEach((select) => {
|
||||||
|
selects.push({ select, row: select.closest(ROLE_REL_ROW), isAddPanel: false });
|
||||||
|
});
|
||||||
|
if (this._addRow) {
|
||||||
|
this._addRow.querySelectorAll(`select[name='${this._prefix}_new_type']`).forEach((select) => {
|
||||||
|
selects.push({ select, row: select.closest(ROLE_REL_ROW), isAddPanel: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this._typeSelect) {
|
||||||
|
selects.push({ select: this._typeSelect, row: this._typeSelect.closest(ROLE_REL_ROW), isAddPanel: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasPreferred = selects.some(({ select, row, isAddPanel }) => {
|
||||||
|
if (isAddPanel) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const currentValue = (select?.value || "").trim();
|
||||||
|
if (!select || currentValue !== preferredLabel) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!row) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const deleteInput = row.querySelector(`input[name^="${this._prefix}_delete["]`);
|
||||||
|
return !(deleteInput && deleteInput.checked);
|
||||||
|
});
|
||||||
|
|
||||||
|
selects.forEach(({ select, row, isAddPanel }) => {
|
||||||
|
if (!select) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const option = Array.from(select.options).find((opt) => opt.value.trim() === preferredLabel);
|
||||||
|
if (!option) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const deleteInput = row ? row.querySelector(`input[name^="${this._prefix}_delete["]`) : null;
|
||||||
|
const rowDeleted = Boolean(deleteInput && deleteInput.checked);
|
||||||
|
const currentValue = (select.value || "").trim();
|
||||||
|
const keepVisible = !hasPreferred || (currentValue === preferredLabel && !rowDeleted);
|
||||||
|
if (isAddPanel && hasPreferred && currentValue === preferredLabel) {
|
||||||
|
const fallback = Array.from(select.options).find((opt) => opt.value.trim() !== preferredLabel);
|
||||||
|
if (fallback) {
|
||||||
|
select.value = fallback.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const shouldHide = !keepVisible || (isAddPanel && hasPreferred);
|
||||||
|
option.hidden = shouldHide;
|
||||||
|
option.disabled = shouldHide;
|
||||||
|
option.style.display = shouldHide ? "none" : "";
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user