Frontend annoyances

This commit is contained in:
Simon Martens
2026-01-09 08:28:16 +01:00
parent 492d398d27
commit a08a7e5710
9 changed files with 717 additions and 473 deletions

View File

@@ -311,6 +311,21 @@ func (payload *almanachEditPayload) Validate() error {
return fmt.Errorf("Mindestens ein bevorzugter Reihentitel muss verknüpft sein.") return fmt.Errorf("Mindestens ein bevorzugter Reihentitel muss verknüpft sein.")
} }
// Check for duplicate series relations
seriesTargetIDs := make(map[string]bool)
for _, relation := range payload.SeriesRelations {
if seriesTargetIDs[relation.TargetID] {
return fmt.Errorf("Doppelte Reihenverknüpfungen sind nicht erlaubt.")
}
seriesTargetIDs[relation.TargetID] = true
}
for _, relation := range payload.NewSeriesRelations {
if seriesTargetIDs[relation.TargetID] {
return fmt.Errorf("Doppelte Reihenverknüpfungen sind nicht erlaubt.")
}
seriesTargetIDs[relation.TargetID] = true
}
return nil return nil
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -288,7 +288,7 @@ type AlmanachResult struct {
<i class="ri-add-line"></i> Reihe hinzufügen <i class="ri-add-line"></i> Reihe hinzufügen
</button> </button>
<button type="button" id="agents-add-toggle" class="text-gray-700 hover:text-gray-900"> <button type="button" id="agents-add-toggle" class="text-gray-700 hover:text-gray-900">
<i class="ri-add-line"></i> Akteur hinzufügen <i class="ri-add-line"></i> Person hinzufügen
</button> </button>
</div> </div>
</div> </div>
@@ -297,19 +297,19 @@ type AlmanachResult struct {
<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">
<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="flex flex-col gap-2 mt-2"> <div id="series-section" class="rel-section-container">
{{- if $model.result.Series -}} {{- if $model.result.Series -}}
{{- range $i, $s := $model.result.Series -}} {{- range $i, $s := $model.result.Series -}}
{{- $rel := index $model.result.EntriesSeries $s.Id -}} {{- $rel := index $model.result.EntriesSeries $s.Id -}}
{{- if $rel -}} {{- if $rel -}}
<div data-rel-row class="entries-series-row border border-stone-200 rounded-xs bg-stone-50 px-3 py-2"> <div data-rel-row class="entries-series-row rel-row">
<div class="grid grid-cols-[1fr_14rem_5.5rem_7rem] gap-3 items-center"> <div class="rel-grid">
<div data-rel-strike class="relation-strike flex flex-col min-w-0"> <div data-rel-strike class="relation-strike rel-name-col">
<a data-rel-link href="/reihe/{{ $s.MusenalmID }}" class="text-base text-gray-800 no-underline hover:text-slate-900 truncate"> <a data-rel-link href="/reihe/{{ $s.MusenalmID }}" class="rel-link">
<span data-rel-name>{{- $s.Title -}}</span> <span data-rel-name>{{- $s.Title -}}</span>
</a> </a>
{{- if $s.Pseudonyms -}} {{- if $s.Pseudonyms -}}
<div data-rel-detail-container class="text-xs text-gray-600 truncate"><span data-rel-detail>{{- $s.Pseudonyms -}}</span></div> <div data-rel-detail-container class="rel-detail"><span data-rel-detail>{{- $s.Pseudonyms -}}</span></div>
{{- end -}} {{- end -}}
</div> </div>
<div data-rel-strike class="relation-strike"> <div data-rel-strike class="relation-strike">
@@ -319,15 +319,15 @@ type AlmanachResult struct {
{{- end -}} {{- end -}}
</select> </select>
</div> </div>
<div data-rel-strike class="relation-strike flex items-center gap-2"> <div data-rel-strike class="relation-strike rel-uncertain-container">
<input <input
type="checkbox" type="checkbox"
name="entries_series_uncertain[{{ $rel.Id }}]" name="entries_series_uncertain[{{ $rel.Id }}]"
id="entries_series_uncertain_{{ $rel.Id }}" id="entries_series_uncertain_{{ $rel.Id }}"
{{ if $rel.Uncertain }}checked{{ end }} /> {{ if $rel.Uncertain }}checked{{ end }} />
<label for="entries_series_uncertain_{{ $rel.Id }}" class="text-sm text-gray-700">Unsicher</label> <label for="entries_series_uncertain_{{ $rel.Id }}" class="rel-uncertain-label">Unsicher</label>
</div> </div>
<div class="flex justify-end"> <div class="rel-button-container">
<button <button
type="button" type="button"
class="text-sm" class="text-sm"
@@ -344,13 +344,13 @@ type AlmanachResult struct {
{{- end -}} {{- end -}}
{{- end -}} {{- end -}}
{{- else -}} {{- else -}}
<div class="text-sm text-gray-600">Keine Reihen verknüpft.</div> <div class="rel-empty-text">Keine Reihen verknüpft.</div>
{{- end -}} {{- end -}}
</div> </div>
<div data-role="relation-add-row" class="mt-2"></div> <div data-role="relation-add-row" class="mt-2 px-2"></div>
<div data-role="relation-add-panel" class="mt-2 hidden"> <div data-role="relation-add-panel" class="mt-2 px-2 hidden">
<div class="border border-stone-200 rounded-xs bg-stone-50 px-3 py-2"> <div class="rel-row">
<div class="grid grid-cols-[1fr_14rem_5.5rem_7rem] gap-3 items-center"> <div class="rel-grid">
<div class="min-w-0"> <div class="min-w-0">
<label for="series-add-select" class="sr-only">Reihe suchen</label> <label for="series-add-select" class="sr-only">Reihe suchen</label>
<single-select-remote <single-select-remote
@@ -372,11 +372,11 @@ type AlmanachResult struct {
{{- end -}} {{- end -}}
</select> </select>
</div> </div>
<div class="flex items-center gap-2"> <div class="rel-uncertain-container">
<input data-role="relation-uncertain" type="checkbox" name="entries_series_new_uncertain" id="entries_series_new_uncertain" /> <input data-role="relation-uncertain" type="checkbox" name="entries_series_new_uncertain" id="entries_series_new_uncertain" />
<label for="entries_series_new_uncertain" class="text-sm text-gray-700">Unsicher</label> <label for="entries_series_new_uncertain" class="rel-uncertain-label">Unsicher</label>
</div> </div>
<div class="flex justify-end"> <div class="rel-button-container">
<div class="flex items-center gap-3 text-lg"> <div class="flex items-center gap-3 text-lg">
<button type="button" data-role="relation-add-apply" class="text-gray-700 hover:text-gray-900" id="series-add-apply" aria-label="Reihe hinzufügen"> <button type="button" data-role="relation-add-apply" class="text-gray-700 hover:text-gray-900" id="series-add-apply" aria-label="Reihe hinzufügen">
<i class="ri-check-line"></i> <i class="ri-check-line"></i>
@@ -387,29 +387,29 @@ type AlmanachResult struct {
</div> </div>
</div> </div>
</div> </div>
<div data-role="relation-add-error" class="text-xs text-red-700 mt-2 hidden">Bitte Reihe auswählen.</div> <div data-role="relation-add-error" class="text-xs text-red-700 mt-2 hidden" data-error-empty="Bitte Reihe auswählen." data-error-duplicate="Diese Reihe ist bereits verknüpft.">Bitte Reihe auswählen.</div>
</div> </div>
</div> </div>
<template data-role="relation-new-template"> <template data-role="relation-new-template">
<div data-rel-row class="border border-stone-200 rounded-xs bg-stone-50 px-3 py-2"> <div data-rel-row class="rel-row">
<div class="grid grid-cols-[1fr_14rem_5.5rem_7rem] gap-3 items-center"> <div class="rel-grid">
<div data-rel-strike class="relation-strike flex flex-col min-w-0"> <div data-rel-strike class="relation-strike rel-name-col">
<div class="text-base text-gray-800 truncate"> <div class="text-base text-gray-800 truncate">
<a data-rel-link class="no-underline hover:text-slate-900"> <a data-rel-link class="no-underline hover:text-slate-900">
<span data-rel-name></span> <span data-rel-name></span>
</a> </a>
<em data-rel-new class="text-sm text-gray-600 ml-1"></em> <em data-rel-new class="rel-new-badge"></em>
</div> </div>
<div data-rel-detail-container class="text-xs text-gray-600 truncate"><span data-rel-detail></span></div> <div data-rel-detail-container class="rel-detail"><span data-rel-detail></span></div>
</div> </div>
<div data-rel-strike class="relation-strike"> <div data-rel-strike class="relation-strike">
<select data-rel-input="type" class="inputselect font-bold w-full"></select> <select data-rel-input="type" class="inputselect font-bold w-full"></select>
</div> </div>
<div data-rel-strike class="relation-strike flex items-center gap-2"> <div data-rel-strike class="relation-strike rel-uncertain-container">
<input data-rel-input="uncertain" type="checkbox" /> <input data-rel-input="uncertain" type="checkbox" />
<label data-rel-uncertain-label class="text-sm text-gray-700">Unsicher</label> <label data-rel-uncertain-label class="rel-uncertain-label">Unsicher</label>
</div> </div>
<div class="flex justify-end"> <div class="rel-button-container">
<button type="button" class="text-sm text-red-700 hover:text-red-900" data-role="relation-new-delete"> <button type="button" class="text-sm text-red-700 hover:text-red-900" data-role="relation-new-delete">
<i class="ri-delete-bin-line mr-1"></i> Entfernen <i class="ri-delete-bin-line mr-1"></i> Entfernen
</button> </button>
@@ -426,19 +426,19 @@ type AlmanachResult struct {
<relations-editor data-prefix="entries_agents" data-link-base="/person/" data-new-label="(Neu)" data-add-toggle-id="agents-add-toggle"> <relations-editor data-prefix="entries_agents" data-link-base="/person/" data-new-label="(Neu)" data-add-toggle-id="agents-add-toggle">
<div class="inputwrapper"> <div class="inputwrapper">
<label class="inputlabel" for="agents-section">Personen &amp; Körperschaften</label> <label class="inputlabel" for="agents-section">Personen &amp; Körperschaften</label>
<div id="agents-section" class="flex flex-col gap-2 mt-2"> <div id="agents-section" class="rel-section-container">
{{- if $model.result.EntriesAgents -}} {{- if $model.result.EntriesAgents -}}
{{- range $i, $r := $model.result.EntriesAgents -}} {{- range $i, $r := $model.result.EntriesAgents -}}
{{- $a := index $model.result.Agents $r.Agent -}} {{- $a := index $model.result.Agents $r.Agent -}}
<div data-rel-row class="entries-agent-row border border-stone-200 rounded-xs bg-stone-50 px-3 py-2"> <div data-rel-row class="entries-agent-row rel-row">
<div class="grid grid-cols-[1fr_14rem_5.5rem_7rem] gap-3 items-center"> <div class="rel-grid">
<div data-rel-strike class="relation-strike flex flex-col min-w-0"> <div data-rel-strike class="relation-strike rel-name-col">
{{- if $a -}} {{- if $a -}}
<a data-rel-link href="/person/{{ $a.Id }}" class="text-base text-gray-800 no-underline hover:text-slate-900 truncate"> <a data-rel-link href="/person/{{ $a.Id }}" class="rel-link">
<span data-rel-name>{{- $a.Name -}}</span> <span data-rel-name>{{- $a.Name -}}</span>
</a> </a>
{{- if $a.BiographicalData -}} {{- if $a.BiographicalData -}}
<div data-rel-detail-container class="text-xs text-gray-600 truncate"><span data-rel-detail>{{- $a.BiographicalData -}}</span></div> <div data-rel-detail-container class="rel-detail"><span data-rel-detail>{{- $a.BiographicalData -}}</span></div>
{{- end -}} {{- end -}}
{{- else -}} {{- else -}}
<div class="text-base text-gray-800">Unbekannte Person</div> <div class="text-base text-gray-800">Unbekannte Person</div>
@@ -451,15 +451,15 @@ type AlmanachResult struct {
{{- end -}} {{- end -}}
</select> </select>
</div> </div>
<div data-rel-strike class="relation-strike flex items-center gap-2"> <div data-rel-strike class="relation-strike rel-uncertain-container">
<input <input
type="checkbox" type="checkbox"
name="entries_agents_uncertain[{{ $r.Id }}]" name="entries_agents_uncertain[{{ $r.Id }}]"
id="entries_agents_uncertain_{{ $r.Id }}" id="entries_agents_uncertain_{{ $r.Id }}"
{{ if $r.Uncertain }}checked{{ end }} /> {{ if $r.Uncertain }}checked{{ end }} />
<label for="entries_agents_uncertain_{{ $r.Id }}" class="text-sm text-gray-700">Unsicher</label> <label for="entries_agents_uncertain_{{ $r.Id }}" class="rel-uncertain-label">Unsicher</label>
</div> </div>
<div class="flex justify-end"> <div class="rel-button-container">
<button <button
type="button" type="button"
class="text-sm" class="text-sm"
@@ -475,13 +475,13 @@ type AlmanachResult struct {
</div> </div>
{{- end -}} {{- end -}}
{{- else -}} {{- else -}}
<div class="text-sm text-gray-600">Keine Personen verknüpft.</div> <div class="rel-empty-text">Keine Personen verknüpft.</div>
{{- end -}} {{- end -}}
</div> </div>
<div data-role="relation-add-row" class="mt-2"></div> <div data-role="relation-add-row" class="mt-2 px-2"></div>
<div data-role="relation-add-panel" class="mt-2 hidden"> <div data-role="relation-add-panel" class="mt-2 px-2 hidden">
<div class="border border-stone-200 rounded-xs bg-stone-50 px-3 py-2"> <div class="rel-row">
<div class="grid grid-cols-[1fr_14rem_5.5rem_7rem] gap-3 items-center"> <div class="rel-grid">
<div class="min-w-0"> <div class="min-w-0">
<label for="agents-add-select" class="sr-only">Akteur suchen</label> <label for="agents-add-select" class="sr-only">Akteur suchen</label>
<single-select-remote <single-select-remote
@@ -503,13 +503,14 @@ type AlmanachResult struct {
{{- end -}} {{- end -}}
</select> </select>
</div> </div>
<div class="flex items-center gap-2"> <div class="rel-uncertain-container">
<input data-role="relation-uncertain" type="checkbox" name="entries_agents_new_uncertain" id="entries_agents_new_uncertain" /> <input data-role="relation-uncertain" type="checkbox" name="entries_agents_new_uncertain" id="entries_agents_new_uncertain" />
<label for="entries_agents_new_uncertain" class="text-sm text-gray-700">Unsicher</label> <label for="entries_agents_new_uncertain" class="rel-uncertain-label">Unsicher</label>
</div> </div>
<div class="flex justify-end"> <div class="rel-button-container">
<div class="flex items-center gap-3 text-lg"> <div class="flex items-center gap-3 text-lg">
<button type="button" data-role="relation-add-apply" class="text-gray-700 hover:text-gray-900" id="agents-add-apply" aria-label="Akteur hinzufügen"> <button type="button" data-role="relation-add-apply" class="text-gray-700
hover:text-gray-900" id="agents-add-apply" aria-label="Person hinzufügen">
<i class="ri-check-line"></i> <i class="ri-check-line"></i>
</button> </button>
<button type="button" data-role="relation-add-close" class="text-gray-700 hover:text-gray-900" id="agents-add-close" aria-label="Ausblenden"> <button type="button" data-role="relation-add-close" class="text-gray-700 hover:text-gray-900" id="agents-add-close" aria-label="Ausblenden">
@@ -518,29 +519,29 @@ type AlmanachResult struct {
</div> </div>
</div> </div>
</div> </div>
<div data-role="relation-add-error" class="text-xs text-red-700 mt-2 hidden">Bitte Akteur auswählen.</div> <div data-role="relation-add-error" class="text-xs text-red-700 mt-2 hidden" data-error-empty="Bitte Akteur auswählen.">Bitte Akteur auswählen.</div>
</div> </div>
</div> </div>
<template data-role="relation-new-template"> <template data-role="relation-new-template">
<div data-rel-row class="border border-stone-200 rounded-xs bg-stone-50 px-3 py-2"> <div data-rel-row class="rel-row">
<div class="grid grid-cols-[1fr_14rem_5.5rem_7rem] gap-3 items-center"> <div class="rel-grid">
<div data-rel-strike class="relation-strike flex flex-col min-w-0"> <div data-rel-strike class="relation-strike rel-name-col">
<div class="text-base text-gray-800 truncate"> <div class="text-base text-gray-800 truncate">
<a data-rel-link class="no-underline hover:text-slate-900"> <a data-rel-link class="no-underline hover:text-slate-900">
<span data-rel-name></span> <span data-rel-name></span>
</a> </a>
<em data-rel-new class="text-sm text-gray-600 ml-1"></em> <em data-rel-new class="rel-new-badge"></em>
</div> </div>
<div data-rel-detail-container class="text-xs text-gray-600 truncate"><span data-rel-detail></span></div> <div data-rel-detail-container class="rel-detail"><span data-rel-detail></span></div>
</div> </div>
<div data-rel-strike class="relation-strike"> <div data-rel-strike class="relation-strike">
<select data-rel-input="type" class="inputselect font-bold w-full"></select> <select data-rel-input="type" class="inputselect font-bold w-full"></select>
</div> </div>
<div data-rel-strike class="relation-strike flex items-center gap-2"> <div data-rel-strike class="relation-strike rel-uncertain-container">
<input data-rel-input="uncertain" type="checkbox" /> <input data-rel-input="uncertain" type="checkbox" />
<label data-rel-uncertain-label class="text-sm text-gray-700">Unsicher</label> <label data-rel-uncertain-label class="rel-uncertain-label">Unsicher</label>
</div> </div>
<div class="flex justify-end"> <div class="rel-button-container">
<button type="button" class="text-sm text-red-700 hover:text-red-900" data-role="relation-new-delete"> <button type="button" class="text-sm text-red-700 hover:text-red-900" data-role="relation-new-delete">
<i class="ri-delete-bin-line mr-1"></i> Entfernen <i class="ri-delete-bin-line mr-1"></i> Entfernen
</button> </button>

View File

@@ -215,6 +215,14 @@ export class AlmanachEditPage extends HTMLElement {
}); });
const newAgentRelations = this._collectNewRelations("entries_agents"); const newAgentRelations = this._collectNewRelations("entries_agents");
// Validate no duplicate series relations
const allSeriesRelations = [...seriesRelations, ...newSeriesRelations];
const seriesTargetIds = allSeriesRelations.map((r) => r.target_id);
const duplicateSeries = seriesTargetIds.filter((id, index) => seriesTargetIds.indexOf(id) !== index);
if (duplicateSeries.length > 0) {
throw new Error("Doppelte Reihenverknüpfungen sind nicht erlaubt.");
}
return { return {
csrf_token: this._readValue(formData, "csrf_token"), csrf_token: this._readValue(formData, "csrf_token"),
last_edited: this._readValue(formData, "last_edited"), last_edited: this._readValue(formData, "last_edited"),
@@ -281,30 +289,37 @@ export class AlmanachEditPage extends HTMLElement {
_collectRelations(formData, { prefix, targetField }) { _collectRelations(formData, { prefix, targetField }) {
const relations = []; const relations = [];
const deleted = []; const deleted = [];
// Iterate over ID fields instead of type fields (IDs are always submitted even when disabled)
for (const [key, value] of formData.entries()) { for (const [key, value] of formData.entries()) {
if (!key.startsWith(`${prefix}_type[`)) { if (!key.startsWith(`${prefix}_id[`)) {
continue; continue;
} }
const relationKey = key.slice(key.indexOf("[") + 1, -1); const relationKey = key.slice(key.indexOf("[") + 1, -1);
const targetKey = `${prefix}_${targetField}[${relationKey}]`; const targetKey = `${prefix}_${targetField}[${relationKey}]`;
const relationIdKey = `${prefix}_id[${relationKey}]`; const typeKey = `${prefix}_type[${relationKey}]`;
const deleteKey = `${prefix}_delete[${relationKey}]`; const deleteKey = `${prefix}_delete[${relationKey}]`;
const uncertainKey = `${prefix}_uncertain[${relationKey}]`; const uncertainKey = `${prefix}_uncertain[${relationKey}]`;
const relationId = (value || "").trim();
const targetId = (formData.get(targetKey) || "").trim(); const targetId = (formData.get(targetKey) || "").trim();
if (!targetId) {
if (!targetId || !relationId) {
continue; continue;
} }
const relationId = (formData.get(relationIdKey) || relationKey).trim();
// Check if marked for deletion
if (formData.has(deleteKey)) { if (formData.has(deleteKey)) {
if (relationId) {
deleted.push(relationId); deleted.push(relationId);
}
continue; continue;
} }
// Not deleted, add to relations
const type = (formData.get(typeKey) || "").trim();
relations.push({ relations.push({
id: relationId, id: relationId,
target_id: targetId, target_id: targetId,
type: (value || "").trim(), type: type,
uncertain: formData.has(uncertainKey), uncertain: formData.has(uncertainKey),
}); });
} }

View File

@@ -46,6 +46,57 @@
position: relative; position: relative;
} }
/* Relations editor abstracted classes */
.rel-section-container {
@apply flex flex-col gap-2 mt-1 px-2;
}
.rel-row {
@apply border border-stone-200 rounded-xs bg-stone-50 px-3 py-2;
}
.rel-grid {
@apply grid grid-cols-[1fr_14rem_5.5rem_7rem] gap-3 items-center;
}
.rel-name-col {
@apply flex flex-col min-w-0;
}
.rel-link {
@apply text-base text-gray-800 no-underline hover:text-slate-900 truncate;
}
.rel-detail {
@apply text-xs text-gray-600 truncate;
}
.rel-new-badge {
@apply text-sm text-gray-600 ml-1;
}
.rel-uncertain-container {
@apply flex items-center gap-2;
}
.rel-uncertain-label {
@apply text-sm text-gray-700;
}
.rel-button-container {
@apply flex justify-end;
}
.rel-empty-text {
@apply px-1.5 italic text-gray-600;
}
/* Disabled form controls in deleted relations */
[data-rel-row].bg-red-50 select:disabled,
[data-rel-row].bg-red-50 input[type="checkbox"]:disabled:not([data-delete-toggle]) {
@apply opacity-50 cursor-not-allowed;
}
/* Diagonal hatching pattern for deleted relations, items, and pills */ /* Diagonal hatching pattern for deleted relations, items, and pills */
[data-rel-row], [data-rel-row],
.items-row, .items-row,
@@ -62,13 +113,7 @@
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background: repeating-linear-gradient( background: repeating-linear-gradient(-45deg, transparent, transparent 6px, rgba(220, 38, 38, 0.2) 6px, rgba(220, 38, 38, 0.2) 10px);
-45deg,
transparent,
transparent 6px,
rgba(220, 38, 38, 0.2) 6px,
rgba(220, 38, 38, 0.2) 10px
);
border-radius: 0.25rem; border-radius: 0.25rem;
pointer-events: none; pointer-events: none;
z-index: 1; z-index: 1;
@@ -94,13 +139,7 @@
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background: repeating-linear-gradient( background: repeating-linear-gradient(-45deg, #f5f5f4, #f5f5f4 6px, rgba(220, 38, 38, 0.25) 6px, rgba(220, 38, 38, 0.25) 10px);
-45deg,
#f5f5f4,
#f5f5f4 6px,
rgba(220, 38, 38, 0.25) 6px,
rgba(220, 38, 38, 0.25) 10px
);
border-radius: 0.25rem; border-radius: 0.25rem;
pointer-events: none; pointer-events: none;
z-index: 0; z-index: 0;
@@ -345,7 +384,7 @@
} }
.mss-toggle-button { .mss-toggle-button {
@apply text-gray-700 hover:text-gray-900 font-semibold text-lg px-2 py-0.5 rounded-xs border border-transparent bg-transparent whitespace-nowrap leading-none; @apply text-gray-700 hover:text-gray-900 font-semibold text-lg py-0.5 rounded-xs border border-transparent bg-transparent whitespace-nowrap leading-none;
} }
.mss-options-list { .mss-options-list {

View File

@@ -196,16 +196,42 @@ export class ItemsEditor extends HTMLElement {
row.setAttribute(REMOVED_ROW_STATE, removed ? "true" : "false"); row.setAttribute(REMOVED_ROW_STATE, removed ? "true" : "false");
row.classList.toggle("bg-red-50", removed); row.classList.toggle("bg-red-50", removed);
const editButton = row.querySelector(".items-edit-button");
if (editButton) {
if (removed) {
editButton.classList.add("hidden");
} else {
editButton.classList.remove("hidden");
}
}
row.querySelectorAll("[data-delete-label]").forEach((label) => { row.querySelectorAll("[data-delete-label]").forEach((label) => {
const nextLabel = removed const button = label.closest(`.${REMOVE_BUTTON_CLASS}`);
? label.getAttribute("data-delete-active") || "Wird entfernt" const isHovered = button && button.matches(":hover");
: label.getAttribute("data-delete-default") || "Entfernen";
let nextLabel;
if (removed && isHovered) {
nextLabel = label.getAttribute("data-delete-hover") || "Rückgängig";
} else if (removed) {
nextLabel = label.getAttribute("data-delete-active") || "Wird entfernt";
} else {
nextLabel = label.getAttribute("data-delete-default") || "Entfernen";
}
label.textContent = nextLabel; label.textContent = nextLabel;
}); });
row.querySelectorAll(`.${REMOVE_BUTTON_CLASS} i`).forEach((icon) => { row.querySelectorAll(`.${REMOVE_BUTTON_CLASS} i`).forEach((icon) => {
const button = icon.closest(`.${REMOVE_BUTTON_CLASS}`);
const isHovered = button && button.matches(":hover");
if (removed) { if (removed) {
if (isHovered) {
icon.classList.remove("hidden");
icon.classList.add("ri-arrow-go-back-line");
icon.classList.remove("ri-delete-bin-line");
} else {
icon.classList.add("hidden"); icon.classList.add("hidden");
icon.classList.remove("ri-delete-bin-line", "ri-arrow-go-back-line"); icon.classList.remove("ri-delete-bin-line", "ri-arrow-go-back-line");
}
} else { } else {
icon.classList.remove("hidden"); icon.classList.remove("hidden");
icon.classList.add("ri-delete-bin-line"); icon.classList.add("ri-delete-bin-line");

View File

@@ -24,10 +24,61 @@ 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._emptyText = this.querySelector(".rel-empty-text");
this._setupAddPanel(); this._setupAddPanel();
this._setupDeleteToggles(); this._setupDeleteToggles();
} }
_getExistingIds() {
const ids = new Set();
// For series: entries_series_series[...]
// For agents: entries_agents_agent[...]
const targetField = this._prefix === "entries_series" ? "series" : "agent";
// Get existing relation target IDs (from server-rendered rows)
this.querySelectorAll(`input[name^="${this._prefix}_${targetField}["]`).forEach((input) => {
const value = input.value.trim();
if (value) {
ids.add(value);
}
});
// Get new relation target IDs (from dynamically added rows, but not from the add panel)
if (this._addRow) {
this._addRow.querySelectorAll(`input[name="${this._prefix}_new_id"]`).forEach((input) => {
const value = input.value.trim();
if (value) {
ids.add(value);
}
});
}
return ids;
}
_updateEmptyTextVisibility() {
if (!this._emptyText) {
return;
}
// Check if there are any existing relations (server-rendered rows)
const targetField = this._prefix === "entries_series" ? "series" : "agent";
const hasExisting = this.querySelectorAll(`input[name^="${this._prefix}_${targetField}["]`).length > 0;
// Check if there are any new relations in the add row
const hasNew = this._addRow && this._addRow.querySelectorAll(`input[name="${this._prefix}_new_id"]`).length > 0;
// Check if add panel is visible
const isPanelVisible = this._addPanel && !this._addPanel.classList.contains("hidden");
// Hide empty text if: panel is visible OR there are any relations (existing or new)
if (isPanelVisible || hasExisting || hasNew) {
this._emptyText.classList.add("hidden");
} else {
this._emptyText.classList.remove("hidden");
}
}
_setupAddPanel() { _setupAddPanel() {
this._addToggle = this.querySelector(ROLE_ADD_TOGGLE); this._addToggle = this.querySelector(ROLE_ADD_TOGGLE);
if (this._addToggleId) { if (this._addToggleId) {
@@ -51,15 +102,24 @@ export class RelationsEditor extends HTMLElement {
return; return;
} }
// Set up filtering for single-select-remote (only for series, not agents)
if (this._addSelect && this._prefix === "entries_series") {
this._addSelect.addEventListener("ssrbeforefetch", () => {
this._addSelect._excludeIds = Array.from(this._getExistingIds());
});
}
if (this._addToggle) { if (this._addToggle) {
this._addToggle.addEventListener("click", () => { this._addToggle.addEventListener("click", () => {
this._addPanel.classList.toggle("hidden"); this._addPanel.classList.toggle("hidden");
this._updateEmptyTextVisibility();
}); });
} }
if (this._addClose) { if (this._addClose) {
this._addClose.addEventListener("click", () => { this._addClose.addEventListener("click", () => {
this._addPanel.classList.add("hidden"); this._addPanel.classList.add("hidden");
this._updateEmptyTextVisibility();
}); });
} }
@@ -78,16 +138,28 @@ export class RelationsEditor extends HTMLElement {
const hasSelection = idInput && idInput.value.trim().length > 0; const hasSelection = idInput && idInput.value.trim().length > 0;
if (!hasSelection) { if (!hasSelection) {
if (this._addError) { if (this._addError) {
this._addError.textContent = this._addError.getAttribute("data-error-empty") || "Bitte Reihe auswählen.";
this._addError.classList.remove("hidden"); this._addError.classList.remove("hidden");
} }
return; return;
} }
if (this._addError) {
this._addError.classList.add("hidden");
}
if (!this._pendingItem) { if (!this._pendingItem) {
return; return;
} }
// Check for duplicates (only for series, not agents)
if (this._prefix === "entries_series") {
const existingIds = this._getExistingIds();
if (existingIds.has(this._pendingItem.id)) {
if (this._addError) {
this._addError.textContent = this._addError.getAttribute("data-error-duplicate") || "Diese Verknüpfung existiert bereits.";
this._addError.classList.remove("hidden");
}
return;
}
}
if (this._addError) {
this._addError.classList.add("hidden");
}
this._insertNewRow(); this._insertNewRow();
}); });
} }
@@ -187,6 +259,7 @@ export class RelationsEditor extends HTMLElement {
if (this._addPanel) { if (this._addPanel) {
this._addPanel.classList.add("hidden"); this._addPanel.classList.add("hidden");
} }
this._updateEmptyTextVisibility();
}); });
} }
@@ -197,6 +270,7 @@ export class RelationsEditor extends HTMLElement {
if (this._addPanel) { if (this._addPanel) {
this._addPanel.classList.add("hidden"); this._addPanel.classList.add("hidden");
} }
this._updateEmptyTextVisibility();
} }
_setupDeleteToggles() { _setupDeleteToggles() {
@@ -212,20 +286,42 @@ export class RelationsEditor extends HTMLElement {
const row = button.closest(ROLE_REL_ROW); const row = button.closest(ROLE_REL_ROW);
if (row) { if (row) {
row.classList.toggle("bg-red-50", checkbox.checked); row.classList.toggle("bg-red-50", checkbox.checked);
// Disable/enable form controls (but not the delete checkbox itself)
row.querySelectorAll("select, input[type='checkbox']").forEach((control) => {
// Skip the delete checkbox itself
if (control === checkbox) {
return;
}
control.disabled = checkbox.checked;
});
} }
const isHovered = button.matches(":hover");
const label = button.querySelector("[data-delete-label]"); const label = button.querySelector("[data-delete-label]");
if (label) { if (label) {
label.textContent = checkbox.checked let nextLabel;
? label.getAttribute("data-delete-active") || "Wird entfernt" if (checkbox.checked && isHovered) {
: label.getAttribute("data-delete-default") || "Entfernen"; nextLabel = label.getAttribute("data-delete-hover") || "Rückgängig";
} else if (checkbox.checked) {
nextLabel = label.getAttribute("data-delete-active") || "Wird entfernt";
} else {
nextLabel = label.getAttribute("data-delete-default") || "Entfernen";
}
label.textContent = nextLabel;
} }
const icon = button.querySelector("i"); const icon = button.querySelector("i");
if (icon) { if (icon) {
if (checkbox.checked) { if (checkbox.checked) {
if (isHovered) {
icon.classList.remove("hidden");
icon.classList.add("ri-arrow-go-back-line");
icon.classList.remove("ri-delete-bin-line");
} else {
icon.classList.add("hidden"); icon.classList.add("hidden");
icon.classList.remove("ri-delete-bin-line", "ri-arrow-go-back-line"); icon.classList.remove("ri-delete-bin-line", "ri-arrow-go-back-line");
}
} else { } else {
icon.classList.remove("hidden"); icon.classList.remove("hidden");
icon.classList.add("ri-delete-bin-line"); icon.classList.add("ri-delete-bin-line");

View File

@@ -181,6 +181,10 @@ export class SingleSelectRemote extends HTMLElement {
if (this._fetchController) { if (this._fetchController) {
this._fetchController.abort(); this._fetchController.abort();
} }
// Dispatch event before fetch to allow filtering
this.dispatchEvent(new CustomEvent("ssrbeforefetch", { bubbles: true }));
this._fetchController = new AbortController(); this._fetchController = new AbortController();
const url = new URL(this._endpoint, window.location.origin); const url = new URL(this._endpoint, window.location.origin);
url.searchParams.set("q", query); url.searchParams.set("q", query);
@@ -194,7 +198,15 @@ export class SingleSelectRemote extends HTMLElement {
} }
const data = await resp.json(); const data = await resp.json();
const items = Array.isArray(data?.[this._resultKey]) ? data[this._resultKey] : []; const items = Array.isArray(data?.[this._resultKey]) ? data[this._resultKey] : [];
this._options = items.filter((item) => item && item.id && item.name); let filteredItems = items.filter((item) => item && item.id && item.name);
// Filter out excluded IDs if provided
if (this._excludeIds && Array.isArray(this._excludeIds)) {
const excludeSet = new Set(this._excludeIds);
filteredItems = filteredItems.filter((item) => !excludeSet.has(item.id));
}
this._options = filteredItems;
this._highlightedIndex = this._options.length > 0 ? 0 : -1; this._highlightedIndex = this._options.length > 0 ? 0 : -1;
this._renderOptions(); this._renderOptions();
if (this._options.length > 0) { if (this._options.length > 0) {