mirror of
https://github.com/Theodor-Springmann-Stiftung/musenalm.git
synced 2026-02-04 02:25:30 +00:00
Almanach list
This commit is contained in:
@@ -49,6 +49,13 @@ func (p *PersonEditPage) Setup(router *router.Router[*core.RequestEvent], app co
|
||||
type PersonEditResult struct {
|
||||
Agent *dbmodels.Agent
|
||||
User *dbmodels.User
|
||||
Prev *dbmodels.Agent
|
||||
Next *dbmodels.Agent
|
||||
Entries []*dbmodels.Entry
|
||||
EntryTypes map[string][]string
|
||||
Contents []*dbmodels.Content
|
||||
ContentEntries map[string]*dbmodels.Entry
|
||||
ContentTypes map[string][]string
|
||||
}
|
||||
|
||||
func NewPersonEditResult(app core.App, id string) (*PersonEditResult, error) {
|
||||
@@ -67,9 +74,37 @@ func NewPersonEditResult(app core.App, id string) (*PersonEditResult, error) {
|
||||
}
|
||||
}
|
||||
|
||||
prev, next, err := agentNeighbors(app, agent.Id)
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to load agent neighbors", "agent", agent.Id, "error", err)
|
||||
}
|
||||
|
||||
entries, entryTypes, err := agentEntries(app, agent.Id)
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to load agent entries", "agent", agent.Id, "error", err)
|
||||
}
|
||||
if len(entries) > 0 {
|
||||
dbmodels.Sort_Entries_Year_Title(entries)
|
||||
}
|
||||
|
||||
contents, contentEntries, contentTypes, err := agentContentsDetails(app, agent.Id)
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to load agent contents", "agent", agent.Id, "error", err)
|
||||
}
|
||||
if len(contents) > 0 {
|
||||
dbmodels.Sort_Contents_Numbering(contents)
|
||||
}
|
||||
|
||||
return &PersonEditResult{
|
||||
Agent: agent,
|
||||
User: user,
|
||||
Agent: agent,
|
||||
User: user,
|
||||
Prev: prev,
|
||||
Next: next,
|
||||
Entries: entries,
|
||||
EntryTypes: entryTypes,
|
||||
Contents: contents,
|
||||
ContentEntries: contentEntries,
|
||||
ContentTypes: contentTypes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -110,6 +145,92 @@ func (p *PersonEditPage) renderError(engine *templating.Engine, app core.App, e
|
||||
return engine.Response200(e, p.Template, data, p.Layout)
|
||||
}
|
||||
|
||||
func agentNeighbors(app core.App, currentID string) (*dbmodels.Agent, *dbmodels.Agent, error) {
|
||||
agents := []*dbmodels.Agent{}
|
||||
if err := app.RecordQuery(dbmodels.AGENTS_TABLE).All(&agents); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(agents) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
dbmodels.Sort_Agents_Name(agents)
|
||||
for index, item := range agents {
|
||||
if item.Id != currentID {
|
||||
continue
|
||||
}
|
||||
var prev *dbmodels.Agent
|
||||
var next *dbmodels.Agent
|
||||
if index > 0 {
|
||||
prev = agents[index-1]
|
||||
}
|
||||
if index+1 < len(agents) {
|
||||
next = agents[index+1]
|
||||
}
|
||||
return prev, next, nil
|
||||
}
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func agentEntries(app core.App, agentID string) ([]*dbmodels.Entry, map[string][]string, error) {
|
||||
relations, err := dbmodels.REntriesAgents_Agent(app, agentID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(relations) == 0 {
|
||||
return []*dbmodels.Entry{}, map[string][]string{}, nil
|
||||
}
|
||||
entryIds := make([]any, 0, len(relations))
|
||||
typeMap := make(map[string][]string)
|
||||
for _, relation := range relations {
|
||||
entryIds = append(entryIds, relation.Entry())
|
||||
typeMap[relation.Entry()] = append(typeMap[relation.Entry()], relation.Type())
|
||||
}
|
||||
entries, err := dbmodels.Entries_IDs(app, entryIds)
|
||||
if err != nil {
|
||||
return nil, typeMap, err
|
||||
}
|
||||
return entries, typeMap, nil
|
||||
}
|
||||
|
||||
func agentContentsDetails(app core.App, agentID string) ([]*dbmodels.Content, map[string]*dbmodels.Entry, map[string][]string, error) {
|
||||
relations, err := dbmodels.RContentsAgents_Agent(app, agentID)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
if len(relations) == 0 {
|
||||
return []*dbmodels.Content{}, map[string]*dbmodels.Entry{}, map[string][]string{}, nil
|
||||
}
|
||||
|
||||
contentIDs := make([]any, 0, len(relations))
|
||||
typeMap := make(map[string][]string)
|
||||
for _, relation := range relations {
|
||||
contentIDs = append(contentIDs, relation.Content())
|
||||
typeMap[relation.Content()] = append(typeMap[relation.Content()], relation.Type())
|
||||
}
|
||||
|
||||
contents, err := dbmodels.Contents_IDs(app, contentIDs)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
entryIDs := []any{}
|
||||
for _, content := range contents {
|
||||
entryIDs = append(entryIDs, content.Entry())
|
||||
}
|
||||
|
||||
entries, err := dbmodels.Entries_IDs(app, entryIDs)
|
||||
if err != nil {
|
||||
return contents, map[string]*dbmodels.Entry{}, typeMap, nil
|
||||
}
|
||||
|
||||
entryMap := make(map[string]*dbmodels.Entry, len(entries))
|
||||
for _, entry := range entries {
|
||||
entryMap[entry.Id] = entry
|
||||
}
|
||||
|
||||
return contents, entryMap, typeMap, nil
|
||||
}
|
||||
|
||||
type personEditForm struct {
|
||||
CSRFToken string `form:"csrf_token"`
|
||||
LastEdited string `form:"last_edited"`
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
|
||||
const (
|
||||
URL_REIHE_EDIT = "edit"
|
||||
URL_REIHE_DELETE = "edit/delete"
|
||||
TEMPLATE_REIHE_EDIT = "/reihe/edit/"
|
||||
)
|
||||
|
||||
@@ -43,12 +44,20 @@ func (p *ReiheEditPage) Setup(router *router.Router[*core.RequestEvent], app cor
|
||||
rg.BindFunc(middleware.IsAdminOrEditor())
|
||||
rg.GET(URL_REIHE_EDIT, p.GET(engine, app))
|
||||
rg.POST(URL_REIHE_EDIT, p.POST(engine, app))
|
||||
rg.POST(URL_REIHE_DELETE, p.POSTDelete(engine, app))
|
||||
return nil
|
||||
}
|
||||
|
||||
type ReiheEditResult struct {
|
||||
Series *dbmodels.Series
|
||||
User *dbmodels.User
|
||||
Prev *dbmodels.Series
|
||||
Next *dbmodels.Series
|
||||
Entries []*dbmodels.Entry
|
||||
Contents []*dbmodels.Content
|
||||
ContentEntries map[string]*dbmodels.Entry
|
||||
ContentTypes map[string][]string
|
||||
PreferredEntries []*dbmodels.Entry
|
||||
}
|
||||
|
||||
func NewReiheEditResult(app core.App, id string) (*ReiheEditResult, error) {
|
||||
@@ -67,9 +76,45 @@ func NewReiheEditResult(app core.App, id string) (*ReiheEditResult, error) {
|
||||
}
|
||||
}
|
||||
|
||||
prev, next, err := seriesNeighbors(app, series.Id)
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to load series neighbors", "series", series.Id, "error", err)
|
||||
}
|
||||
|
||||
entries, _, err := Entries_Series_IDs(app, []any{series.Id})
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to load series entries", "series", series.Id, "error", err)
|
||||
}
|
||||
if len(entries) > 0 {
|
||||
dbmodels.Sort_Entries_Year_Title(entries)
|
||||
}
|
||||
|
||||
contents, contentEntries, contentTypes, err := seriesContentsDetails(app, entries)
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to load series contents", "series", series.Id, "error", err)
|
||||
}
|
||||
if len(contents) > 0 {
|
||||
dbmodels.Sort_Contents_Numbering(contents)
|
||||
}
|
||||
|
||||
preferredEntries, err := preferredSeriesEntries(app, series.Id)
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to load preferred series entries", "series", series.Id, "error", err)
|
||||
}
|
||||
if len(preferredEntries) > 0 {
|
||||
dbmodels.Sort_Entries_Year_Title(preferredEntries)
|
||||
}
|
||||
|
||||
return &ReiheEditResult{
|
||||
Series: series,
|
||||
User: user,
|
||||
Series: series,
|
||||
User: user,
|
||||
Prev: prev,
|
||||
Next: next,
|
||||
Entries: entries,
|
||||
Contents: contents,
|
||||
ContentEntries: contentEntries,
|
||||
ContentTypes: contentTypes,
|
||||
PreferredEntries: preferredEntries,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -110,6 +155,183 @@ func (p *ReiheEditPage) renderError(engine *templating.Engine, app core.App, e *
|
||||
return engine.Response200(e, p.Template, data, p.Layout)
|
||||
}
|
||||
|
||||
type reiheDeletePayload struct {
|
||||
CSRFToken string `json:"csrf_token"`
|
||||
LastEdited string `json:"last_edited"`
|
||||
}
|
||||
|
||||
func (p *ReiheEditPage) POSTDelete(engine *templating.Engine, app core.App) HandleFunc {
|
||||
return func(e *core.RequestEvent) error {
|
||||
id := e.Request.PathValue("id")
|
||||
req := templating.NewRequest(e)
|
||||
|
||||
payload := reiheDeletePayload{}
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
series, err := dbmodels.Series_MusenalmID(app, id)
|
||||
if err != nil {
|
||||
return e.JSON(http.StatusNotFound, map[string]any{
|
||||
"error": "Reihe 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 !series.Updated().Time().Equal(lastEdited.Time()) {
|
||||
return e.JSON(http.StatusConflict, map[string]any{
|
||||
"error": "Die Reihe wurde inzwischen geändert. Bitte Seite neu laden.",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
preferredEntries, err := preferredSeriesEntries(app, series.Id)
|
||||
if err != nil {
|
||||
return e.JSON(http.StatusInternalServerError, map[string]any{
|
||||
"error": "Löschen fehlgeschlagen.",
|
||||
})
|
||||
}
|
||||
|
||||
if err := app.RunInTransaction(func(tx core.App) error {
|
||||
for _, entry := range preferredEntries {
|
||||
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 {
|
||||
continue
|
||||
}
|
||||
if err := tx.Delete(record); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
relations, err := dbmodels.REntriesSeries_Seriess(tx, []any{series.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relationsTable := dbmodels.RelationTableName(dbmodels.ENTRIES_TABLE, dbmodels.SERIES_TABLE)
|
||||
for _, relation := range relations {
|
||||
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.SERIES_TABLE, series.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Delete(record)
|
||||
}); err != nil {
|
||||
app.Logger().Error("Failed to delete series", "series_id", series.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": "/reihen",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func seriesNeighbors(app core.App, currentID string) (*dbmodels.Series, *dbmodels.Series, error) {
|
||||
series := []*dbmodels.Series{}
|
||||
if err := app.RecordQuery(dbmodels.SERIES_TABLE).All(&series); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(series) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
dbmodels.Sort_Series_Title(series)
|
||||
for index, item := range series {
|
||||
if item.Id != currentID {
|
||||
continue
|
||||
}
|
||||
var prev *dbmodels.Series
|
||||
var next *dbmodels.Series
|
||||
if index > 0 {
|
||||
prev = series[index-1]
|
||||
}
|
||||
if index+1 < len(series) {
|
||||
next = series[index+1]
|
||||
}
|
||||
return prev, next, nil
|
||||
}
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func seriesContentsDetails(app core.App, entries []*dbmodels.Entry) ([]*dbmodels.Content, map[string]*dbmodels.Entry, map[string][]string, error) {
|
||||
if len(entries) == 0 {
|
||||
return []*dbmodels.Content{}, map[string]*dbmodels.Entry{}, map[string][]string{}, nil
|
||||
}
|
||||
entryMap := make(map[string]*dbmodels.Entry, len(entries))
|
||||
for _, entry := range entries {
|
||||
entryMap[entry.Id] = entry
|
||||
}
|
||||
|
||||
contents := []*dbmodels.Content{}
|
||||
typeMap := make(map[string][]string)
|
||||
for _, entry := range entries {
|
||||
entryContents, err := dbmodels.Contents_Entry(app, entry.Id)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
for _, content := range entryContents {
|
||||
contents = append(contents, content)
|
||||
typeMap[content.Id] = append(typeMap[content.Id], content.MusenalmType()...)
|
||||
}
|
||||
}
|
||||
|
||||
return contents, entryMap, typeMap, nil
|
||||
}
|
||||
|
||||
func preferredSeriesEntries(app core.App, seriesID string) ([]*dbmodels.Entry, error) {
|
||||
relations, err := dbmodels.REntriesSeries_Seriess(app, []any{seriesID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(relations) == 0 {
|
||||
return []*dbmodels.Entry{}, nil
|
||||
}
|
||||
entryIDs := []any{}
|
||||
for _, relation := range relations {
|
||||
if strings.TrimSpace(relation.Type()) != preferredSeriesRelationType {
|
||||
continue
|
||||
}
|
||||
entryIDs = append(entryIDs, relation.Entry())
|
||||
}
|
||||
if len(entryIDs) == 0 {
|
||||
return []*dbmodels.Entry{}, nil
|
||||
}
|
||||
return dbmodels.Entries_IDs(app, entryIDs)
|
||||
}
|
||||
|
||||
type reiheEditForm struct {
|
||||
CSRFToken string `form:"csrf_token"`
|
||||
LastEdited string `form:"last_edited"`
|
||||
|
||||
@@ -491,13 +491,17 @@ class Ge extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
}
|
||||
constructor() {
|
||||
super(), this._showall = !1, this.shown = -1, this._headings = [], this._contents = [], this._checkbox = null;
|
||||
super(), this._showall = !1, this.shown = -1, this._headings = [], this._contents = [], this._checkbox = null, this._disabled = /* @__PURE__ */ new Set(), this._defaultIndex = null;
|
||||
}
|
||||
connectedCallback() {
|
||||
this._headings = Array.from(this.querySelectorAll(".tab-list-head")), this._contents = Array.from(this.querySelectorAll(".tab-list-panel")), this.hookupEvtHandlers(), this.hideDependent(), this._headings.length === 1 && this.expand(0);
|
||||
if (this._headings = Array.from(this.querySelectorAll(".tab-list-head")), this._contents = Array.from(this.querySelectorAll(".tab-list-panel")), this._readConfig(), this.hookupEvtHandlers(), this._applyDisabled(), this.hideDependent(), this._headings.length === 1) {
|
||||
this.expand(0);
|
||||
return;
|
||||
}
|
||||
this._defaultIndex !== null && this._expandFirstAvailable(this._defaultIndex);
|
||||
}
|
||||
expand(t) {
|
||||
t < 0 || t >= this._headings.length || (this.shown = t, this._contents.forEach((e, i) => {
|
||||
t < 0 || t >= this._headings.length || this._disabled.has(t) || (this.shown = t, this._contents.forEach((e, i) => {
|
||||
i === t ? (e.classList.remove("hidden"), this._headings[i].setAttribute("aria-pressed", "true")) : (e.classList.add("hidden"), this._headings[i].setAttribute("aria-pressed", "false"));
|
||||
}));
|
||||
}
|
||||
@@ -512,6 +516,32 @@ class Ge extends HTMLElement {
|
||||
for (let t of this._contents)
|
||||
t.classList.add("hidden");
|
||||
}
|
||||
_readConfig() {
|
||||
const t = (this.getAttribute("data-disabled-indices") || "").trim(), e = (this.getAttribute("data-default-index") || "").trim();
|
||||
if (this._disabled.clear(), t && t.split(",").map((i) => parseInt(i.trim(), 10)).filter((i) => Number.isFinite(i)).forEach((i) => this._disabled.add(i)), e !== "") {
|
||||
const i = parseInt(e, 10);
|
||||
this._defaultIndex = Number.isFinite(i) ? i : null;
|
||||
} else
|
||||
this._defaultIndex = null;
|
||||
}
|
||||
_applyDisabled() {
|
||||
this._headings.forEach((t, e) => {
|
||||
this._disabled.has(e) ? t.classList.add("pointer-events-none", "opacity-60") : t.classList.remove("pointer-events-none", "opacity-60");
|
||||
});
|
||||
}
|
||||
_expandFirstAvailable(t) {
|
||||
if (this._headings.length !== 0) {
|
||||
if (!this._disabled.has(t)) {
|
||||
this.expand(t);
|
||||
return;
|
||||
}
|
||||
for (let e = 0; e < this._headings.length; e += 1)
|
||||
if (!this._disabled.has(e)) {
|
||||
this.expand(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
restore() {
|
||||
for (let t of this._headings)
|
||||
t.classList.add("cursor-pointer"), t.classList.add("select-none"), t.setAttribute("role", "button"), t.setAttribute("aria-pressed", "false"), t.setAttribute("tabindex", "0"), t.classList.remove("pointer-events-none"), t.classList.remove("!text-slate-900");
|
||||
@@ -2966,9 +2996,44 @@ class li extends HTMLElement {
|
||||
connectedCallback() {
|
||||
setTimeout(() => {
|
||||
const t = this.querySelector("form");
|
||||
t && typeof window.FormLoad == "function" && window.FormLoad(t);
|
||||
t && typeof window.FormLoad == "function" && window.FormLoad(t), this._setupDelete();
|
||||
}, 0);
|
||||
}
|
||||
_setupDelete() {
|
||||
const t = this.querySelector("form");
|
||||
if (!t)
|
||||
return;
|
||||
const e = t.getAttribute("data-delete-endpoint");
|
||||
if (!e)
|
||||
return;
|
||||
const i = this.querySelector("[data-role='edit-delete-dialog']"), s = this.querySelector("[data-role='edit-delete']"), n = this.querySelector("[data-role='edit-delete-confirm']"), a = this.querySelector("[data-role='edit-delete-cancel']");
|
||||
if (!i || !s || !n || !a)
|
||||
return;
|
||||
s.addEventListener("click", (o) => {
|
||||
o.preventDefault(), typeof i.showModal == "function" && i.showModal();
|
||||
});
|
||||
const r = (o) => {
|
||||
o && o.preventDefault(), i.open && i.close();
|
||||
};
|
||||
a.addEventListener("click", r), i.addEventListener("cancel", r), n.addEventListener("click", async (o) => {
|
||||
o.preventDefault(), r();
|
||||
const d = new FormData(t), c = {
|
||||
csrf_token: d.get("csrf_token") || "",
|
||||
last_edited: d.get("last_edited") || ""
|
||||
}, h = await fetch(e, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json"
|
||||
},
|
||||
body: JSON.stringify(c)
|
||||
});
|
||||
if (!h.ok)
|
||||
return;
|
||||
const u = await h.json().catch(() => null), m = (u == null ? void 0 : u.redirect) || "/";
|
||||
window.location.assign(m);
|
||||
});
|
||||
}
|
||||
}
|
||||
const ri = "filter-list", oi = "scroll-button", di = "tool-tip", hi = "abbrev-tooltips", ci = "int-link", ui = "popup-image", mi = "tab-list", _i = "filter-pill", pi = "image-reel", fi = "multi-select-places", gi = "multi-select-simple", bi = "single-select-remote", Me = "reset-button", Ei = "div-manager", Si = "items-editor", vi = "almanach-edit-page", Li = "relations-editor", yi = "edit-page";
|
||||
customElements.define(ci, je);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -31,6 +31,35 @@
|
||||
<div class="">{{ $agent.Id }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-end gap-y-6 pr-6">
|
||||
<div class="">
|
||||
<div class="font-bold text-sm">
|
||||
<i class="ri-navigation-line"></i> Navigation
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
{{- if $model.result.Prev -}}
|
||||
<tool-tip position="top" class="!inline">
|
||||
<div class="data-tip">{{ $model.result.Prev.Name }}</div>
|
||||
<a
|
||||
href="/person/{{ $model.result.Prev.Id }}/edit"
|
||||
class="text-gray-700 hover:text-slate-950 no-underline">
|
||||
<i class="ri-arrow-left-s-line"></i>
|
||||
</a>
|
||||
</tool-tip>
|
||||
{{- end -}}
|
||||
{{- if $model.result.Next -}}
|
||||
<tool-tip position="top" class="!inline">
|
||||
<div class="data-tip">{{ $model.result.Next.Name }}</div>
|
||||
<a
|
||||
href="/person/{{ $model.result.Next.Id }}/edit"
|
||||
class="text-gray-700 hover:text-slate-950 no-underline">
|
||||
<i class="ri-arrow-right-s-line"></i>
|
||||
</a>
|
||||
</tool-tip>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-end gap-y-6 pr-4">
|
||||
<div class="">
|
||||
<div class="font-bold text-sm mb-1"><i class="ri-calendar-line"></i> Zuletzt bearbeitet</div>
|
||||
@@ -122,6 +151,85 @@
|
||||
<label for="edit_comment" class="inputlabel">Bearbeitungsvermerk</label>
|
||||
<textarea name="edit_comment" id="edit_comment" class="inputinput" autocomplete="off" rows="1">{{- $agent.Comment -}}</textarea>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<tab-list
|
||||
data-default-index="{{ if gt (len $model.result.Entries) 0 }}0{{ else if gt (len $model.result.Contents) 0 }}1{{ end }}"
|
||||
data-disabled-indices="{{ if and (eq (len $model.result.Entries) 0) (eq (len $model.result.Contents) 0) }}0,1{{ else if eq (len $model.result.Entries) 0 }}0{{ else if eq (len $model.result.Contents) 0 }}1{{ end }}">
|
||||
<div class="flex items-center gap-3 text-sm font-bold text-gray-700">
|
||||
<div class="tab-list-head flex items-center gap-2">
|
||||
<i class="ri-book-2-line"></i>
|
||||
<span>Verknüpfte Bände</span>
|
||||
<span class="text-xs bg-stone-200 text-gray-700 px-2 py-0.5 rounded-sm">{{ len $model.result.Entries }}</span>
|
||||
</div>
|
||||
<div class="tab-list-head flex items-center gap-2">
|
||||
<i class="ri-article-line"></i>
|
||||
<span>Verknüpfte Inhalte</span>
|
||||
<span class="text-xs bg-stone-200 text-gray-700 px-2 py-0.5 rounded-sm">{{ len $model.result.Contents }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="border-slate-400 mt-2 mb-3" />
|
||||
<div class="tab-list-panel text-sm text-gray-700 max-h-96 overflow-auto pr-1 pl-0 ml-0">
|
||||
{{- if $model.result.Entries -}}
|
||||
<ul class="flex flex-col gap-3 pl-0 pr-0 m-0 ml-0 list-none">
|
||||
{{- range $entry := $model.result.Entries -}}
|
||||
{{- $entryTypes := index $model.result.EntryTypes $entry.Id -}}
|
||||
<li class="flex items-baseline justify-between gap-3 ml-0 pl-0">
|
||||
<div class="flex flex-col gap-1">
|
||||
<a href="/almanach/{{ $entry.MusenalmID }}" class="no-underline hover:text-slate-900">
|
||||
{{- $entry.PreferredTitle -}}
|
||||
</a>
|
||||
{{- if $entryTypes -}}
|
||||
<div class="text-xs text-gray-600">
|
||||
Rolle:
|
||||
{{- range $i, $t := $entryTypes -}}
|
||||
{{- if $i }}, {{ end -}}{{ $t -}}
|
||||
{{- end -}}
|
||||
</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">{{ $entry.Year }}</span>
|
||||
</li>
|
||||
{{- end -}}
|
||||
</ul>
|
||||
{{- else -}}
|
||||
<div class="italic text-gray-500">Keine Bände verknüpft.</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
<div class="tab-list-panel text-sm text-gray-700 max-h-96 overflow-auto pr-1 pl-0 ml-0">
|
||||
{{- if $model.result.Contents -}}
|
||||
<ul class="flex flex-col gap-3 pl-0 pr-0 m-0 ml-0 list-none">
|
||||
{{- range $content := $model.result.Contents -}}
|
||||
{{- $entry := index $model.result.ContentEntries $content.Entry -}}
|
||||
{{- $types := index $model.result.ContentTypes $content.Id -}}
|
||||
<li class="flex flex-col gap-1 ml-0 pl-0">
|
||||
<a href="/beitrag/{{ $content.MusenalmID }}" class="no-underline hover:text-slate-900 font-semibold">
|
||||
{{- if $content.PreferredTitle -}}{{ $content.PreferredTitle }}{{- else -}}Inhalt #{{ $content.MusenalmID }}{{- end -}}
|
||||
</a>
|
||||
<div class="text-xs text-gray-600 flex flex-wrap gap-3">
|
||||
{{- if $entry -}}
|
||||
<span>Band: <a href="/almanach/{{ $entry.MusenalmID }}" class="no-underline hover:text-slate-900">{{ $entry.PreferredTitle }}</a></span>
|
||||
{{- end -}}
|
||||
{{- if $types -}}
|
||||
<span>
|
||||
Rolle:
|
||||
{{- range $i, $t := $types -}}
|
||||
{{- if $i }}, {{ end -}}{{ $t -}}
|
||||
{{- end -}}
|
||||
</span>
|
||||
{{- end -}}
|
||||
{{- if $content.MusenalmPagination -}}
|
||||
<span>Seite: {{ $content.MusenalmPagination }}</span>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</li>
|
||||
{{- end -}}
|
||||
</ul>
|
||||
{{- else -}}
|
||||
<div class="italic text-gray-500">Keine Inhalte verknüpft.</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</tab-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -31,6 +31,35 @@
|
||||
<div class="">{{ $series.Id }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-end gap-y-6 pr-6">
|
||||
<div class="">
|
||||
<div class="font-bold text-sm">
|
||||
<i class="ri-navigation-line"></i> Navigation
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
{{- if $model.result.Prev -}}
|
||||
<tool-tip position="top" class="!inline">
|
||||
<div class="data-tip">{{ $model.result.Prev.Title }}</div>
|
||||
<a
|
||||
href="/reihe/{{ $model.result.Prev.MusenalmID }}/edit"
|
||||
class="text-gray-700 hover:text-slate-950 no-underline">
|
||||
<i class="ri-arrow-left-s-line"></i>
|
||||
</a>
|
||||
</tool-tip>
|
||||
{{- end -}}
|
||||
{{- if $model.result.Next -}}
|
||||
<tool-tip position="top" class="!inline">
|
||||
<div class="data-tip">{{ $model.result.Next.Title }}</div>
|
||||
<a
|
||||
href="/reihe/{{ $model.result.Next.MusenalmID }}/edit"
|
||||
class="text-gray-700 hover:text-slate-950 no-underline">
|
||||
<i class="ri-arrow-right-s-line"></i>
|
||||
</a>
|
||||
</tool-tip>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-end gap-y-6 pr-4">
|
||||
<div class="">
|
||||
<div class="font-bold text-sm mb-1"><i class="ri-calendar-line"></i> Zuletzt bearbeitet</div>
|
||||
@@ -58,7 +87,8 @@
|
||||
class="w-full dbform"
|
||||
id="changeseriesform"
|
||||
method="POST"
|
||||
action="/reihe/{{ $series.MusenalmID }}/edit">
|
||||
action="/reihe/{{ $series.MusenalmID }}/edit"
|
||||
data-delete-endpoint="/reihe/{{ $series.MusenalmID }}/edit/delete">
|
||||
<input type="hidden" name="csrf_token" value="{{ $model.csrf_token }}" />
|
||||
<input type="hidden" name="last_edited" value="{{ $series.Updated }}" />
|
||||
|
||||
@@ -101,6 +131,74 @@
|
||||
<label for="edit_comment" class="inputlabel">Bearbeitungsvermerk</label>
|
||||
<textarea name="edit_comment" id="edit_comment" class="inputinput" autocomplete="off" rows="1">{{- $series.Comment -}}</textarea>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<tab-list
|
||||
data-default-index="{{ if gt (len $model.result.Entries) 0 }}0{{ else if gt (len $model.result.Contents) 0 }}1{{ end }}"
|
||||
data-disabled-indices="{{ if and (eq (len $model.result.Entries) 0) (eq (len $model.result.Contents) 0) }}0,1{{ else if eq (len $model.result.Entries) 0 }}0{{ else if eq (len $model.result.Contents) 0 }}1{{ end }}">
|
||||
<div class="flex items-center gap-3 text-sm font-bold text-gray-700">
|
||||
<div class="tab-list-head flex items-center gap-2">
|
||||
<i class="ri-book-2-line"></i>
|
||||
<span>Verknüpfte Bände</span>
|
||||
<span class="text-xs bg-stone-200 text-gray-700 px-2 py-0.5 rounded-sm">{{ len $model.result.Entries }}</span>
|
||||
</div>
|
||||
<div class="tab-list-head flex items-center gap-2">
|
||||
<i class="ri-article-line"></i>
|
||||
<span>Verknüpfte Inhalte</span>
|
||||
<span class="text-xs bg-stone-200 text-gray-700 px-2 py-0.5 rounded-sm">{{ len $model.result.Contents }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="border-slate-400 mt-2 mb-3" />
|
||||
<div class="tab-list-panel text-sm text-gray-700 max-h-96 overflow-auto pr-1 pl-0 ml-0">
|
||||
{{- if $model.result.Entries -}}
|
||||
<ul class="flex flex-col gap-2 pl-0 pr-0 m-0 ml-0 list-none">
|
||||
{{- range $entry := $model.result.Entries -}}
|
||||
<li class="flex items-baseline justify-between gap-3 ml-0 pl-0">
|
||||
<a href="/almanach/{{ $entry.MusenalmID }}" class="no-underline hover:text-slate-900">
|
||||
{{- $entry.PreferredTitle -}}
|
||||
</a>
|
||||
<span class="text-xs text-gray-500">{{ $entry.Year }}</span>
|
||||
</li>
|
||||
{{- end -}}
|
||||
</ul>
|
||||
{{- else -}}
|
||||
<div class="italic text-gray-500">Keine Bände verknüpft.</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
<div class="tab-list-panel text-sm text-gray-700 max-h-96 overflow-auto pr-1 pl-0 ml-0">
|
||||
{{- if $model.result.Contents -}}
|
||||
<ul class="flex flex-col gap-3 pl-0 pr-0 m-0 ml-0 list-none">
|
||||
{{- range $content := $model.result.Contents -}}
|
||||
{{- $entry := index $model.result.ContentEntries $content.Entry -}}
|
||||
{{- $types := index $model.result.ContentTypes $content.Id -}}
|
||||
<li class="flex flex-col gap-1 ml-0 pl-0">
|
||||
<a href="/beitrag/{{ $content.MusenalmID }}" class="no-underline hover:text-slate-900 font-semibold">
|
||||
{{- if $content.PreferredTitle -}}{{ $content.PreferredTitle }}{{- else -}}Inhalt #{{ $content.MusenalmID }}{{- end -}}
|
||||
</a>
|
||||
<div class="text-xs text-gray-600 flex flex-wrap gap-3">
|
||||
{{- if $entry -}}
|
||||
<span>Band: <a href="/almanach/{{ $entry.MusenalmID }}" class="no-underline hover:text-slate-900">{{ $entry.PreferredTitle }}</a></span>
|
||||
{{- end -}}
|
||||
{{- if $types -}}
|
||||
<span>
|
||||
Typ:
|
||||
{{- range $i, $t := $types -}}
|
||||
{{- if $i }}, {{ end -}}{{ $t -}}
|
||||
{{- end -}}
|
||||
</span>
|
||||
{{- end -}}
|
||||
{{- if $content.MusenalmPagination -}}
|
||||
<span>Seite: {{ $content.MusenalmPagination }}</span>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</li>
|
||||
{{- end -}}
|
||||
</ul>
|
||||
{{- else -}}
|
||||
<div class="italic text-gray-500">Keine Inhalte verknüpft.</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</tab-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -115,6 +213,13 @@
|
||||
<i class="ri-loop-left-line"></i>
|
||||
<span>Reset</span>
|
||||
</a>
|
||||
<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="edit-delete">
|
||||
<i class="ri-delete-bin-line"></i>
|
||||
<span>Reihe löschen</span>
|
||||
</button>
|
||||
<button type="submit" class="submitbutton w-40 flex items-center gap-2 justify-center">
|
||||
<i class="ri-save-line"></i>
|
||||
<span>Speichern</span>
|
||||
@@ -123,4 +228,36 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<dialog data-role="edit-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-[26rem]">
|
||||
<div class="text-base font-bold text-gray-900">Reihe löschen?</div>
|
||||
<div class="text-sm font-bold text-gray-900 mt-1">{{ $series.Title }}</div>
|
||||
<p class="text-sm text-gray-700 mt-2">
|
||||
Alle Bände, Inhalte und Verknüpfungen der bevorzugten Reihentitel werden gelöscht.
|
||||
</p>
|
||||
<div class="mt-3">
|
||||
<div class="text-sm font-semibold text-gray-700">Betroffene Bände</div>
|
||||
<div class="mt-2 max-h-40 overflow-auto pr-1">
|
||||
{{- if $model.result.PreferredEntries -}}
|
||||
<ul class="flex flex-col gap-2 pl-0 pr-0 m-0 list-none">
|
||||
{{- range $entry := $model.result.PreferredEntries -}}
|
||||
<li class="flex items-baseline justify-between gap-3 ml-0 pl-0 text-sm text-gray-700">
|
||||
<span>{{ $entry.PreferredTitle }}</span>
|
||||
<span class="text-xs text-gray-500">{{ $entry.Year }}</span>
|
||||
</li>
|
||||
{{- end -}}
|
||||
</ul>
|
||||
{{- else -}}
|
||||
<div class="italic text-gray-500">Keine Bände betroffen.</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</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="edit-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="edit-delete-confirm">
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</edit-page>
|
||||
|
||||
@@ -5,6 +5,69 @@ export class EditPage extends HTMLElement {
|
||||
if (form && typeof window.FormLoad === "function") {
|
||||
window.FormLoad(form);
|
||||
}
|
||||
this._setupDelete();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
_setupDelete() {
|
||||
const form = this.querySelector("form");
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
const deleteEndpoint = form.getAttribute("data-delete-endpoint");
|
||||
if (!deleteEndpoint) {
|
||||
return;
|
||||
}
|
||||
const dialog = this.querySelector("[data-role='edit-delete-dialog']");
|
||||
const deleteButton = this.querySelector("[data-role='edit-delete']");
|
||||
const confirmButton = this.querySelector("[data-role='edit-delete-confirm']");
|
||||
const cancelButton = this.querySelector("[data-role='edit-delete-cancel']");
|
||||
|
||||
if (!dialog || !deleteButton || !confirmButton || !cancelButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
deleteButton.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
if (typeof dialog.showModal === "function") {
|
||||
dialog.showModal();
|
||||
}
|
||||
});
|
||||
|
||||
const closeDialog = (event) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (dialog.open) {
|
||||
dialog.close();
|
||||
}
|
||||
};
|
||||
|
||||
cancelButton.addEventListener("click", closeDialog);
|
||||
dialog.addEventListener("cancel", closeDialog);
|
||||
|
||||
confirmButton.addEventListener("click", async (event) => {
|
||||
event.preventDefault();
|
||||
closeDialog();
|
||||
const formData = new FormData(form);
|
||||
const payload = {
|
||||
csrf_token: formData.get("csrf_token") || "",
|
||||
last_edited: formData.get("last_edited") || "",
|
||||
};
|
||||
const response = await fetch(deleteEndpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
const data = await response.json().catch(() => null);
|
||||
const redirect = data?.redirect || "/";
|
||||
window.location.assign(redirect);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,18 @@
|
||||
@apply px-1.5 italic text-gray-600;
|
||||
}
|
||||
|
||||
/* Reset global list indentation inside tab panels */
|
||||
.tab-list-panel ul {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.tab-list-panel li {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
/* 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]) {
|
||||
|
||||
@@ -8,16 +8,25 @@ export class TabList extends HTMLElement {
|
||||
this._headings = [];
|
||||
this._contents = [];
|
||||
this._checkbox = null;
|
||||
this._disabled = new Set();
|
||||
this._defaultIndex = null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this._headings = Array.from(this.querySelectorAll(".tab-list-head"));
|
||||
this._contents = Array.from(this.querySelectorAll(".tab-list-panel"));
|
||||
this._readConfig();
|
||||
this.hookupEvtHandlers();
|
||||
this._applyDisabled();
|
||||
this.hideDependent();
|
||||
|
||||
if (this._headings.length === 1) {
|
||||
this.expand(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._defaultIndex !== null) {
|
||||
this._expandFirstAvailable(this._defaultIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +34,9 @@ export class TabList extends HTMLElement {
|
||||
if (index < 0 || index >= this._headings.length) {
|
||||
return;
|
||||
}
|
||||
if (this._disabled.has(index)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.shown = index;
|
||||
|
||||
@@ -67,6 +79,53 @@ export class TabList extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
_readConfig() {
|
||||
const disabledRaw = (this.getAttribute("data-disabled-indices") || "").trim();
|
||||
const defaultRaw = (this.getAttribute("data-default-index") || "").trim();
|
||||
|
||||
this._disabled.clear();
|
||||
if (disabledRaw) {
|
||||
disabledRaw
|
||||
.split(",")
|
||||
.map((value) => parseInt(value.trim(), 10))
|
||||
.filter((value) => Number.isFinite(value))
|
||||
.forEach((value) => this._disabled.add(value));
|
||||
}
|
||||
|
||||
if (defaultRaw !== "") {
|
||||
const parsed = parseInt(defaultRaw, 10);
|
||||
this._defaultIndex = Number.isFinite(parsed) ? parsed : null;
|
||||
} else {
|
||||
this._defaultIndex = null;
|
||||
}
|
||||
}
|
||||
|
||||
_applyDisabled() {
|
||||
this._headings.forEach((heading, index) => {
|
||||
if (this._disabled.has(index)) {
|
||||
heading.classList.add("pointer-events-none", "opacity-60");
|
||||
} else {
|
||||
heading.classList.remove("pointer-events-none", "opacity-60");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_expandFirstAvailable(preferredIndex) {
|
||||
if (this._headings.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (!this._disabled.has(preferredIndex)) {
|
||||
this.expand(preferredIndex);
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < this._headings.length; i += 1) {
|
||||
if (!this._disabled.has(i)) {
|
||||
this.expand(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
restore() {
|
||||
for (let heading of this._headings) {
|
||||
heading.classList.add("cursor-pointer");
|
||||
|
||||
Reference in New Issue
Block a user