|
|
@@ -5,8 +5,11 @@ import ( |
|
|
"fmt" |
|
|
"fmt" |
|
|
"net/http" |
|
|
"net/http" |
|
|
"net/url" |
|
|
"net/url" |
|
|
|
|
|
"regexp" |
|
|
|
|
|
"sort" |
|
|
"strconv" |
|
|
"strconv" |
|
|
"strings" |
|
|
"strings" |
|
|
|
|
|
"unicode" |
|
|
|
|
|
|
|
|
"github.com/go-chi/chi/v5" |
|
|
"github.com/go-chi/chi/v5" |
|
|
|
|
|
|
|
|
@@ -65,23 +68,69 @@ type templateFieldView struct { |
|
|
IsRequiredByUs bool |
|
|
IsRequiredByUs bool |
|
|
DisplayLabel string |
|
|
DisplayLabel string |
|
|
DisplayOrder int |
|
|
DisplayOrder int |
|
|
|
|
|
WebsiteSection string |
|
|
Notes string |
|
|
Notes string |
|
|
SampleValue string |
|
|
SampleValue string |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
type websiteSectionOptionView struct { |
|
|
|
|
|
Value string |
|
|
|
|
|
Label string |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
type templateDetailPageData struct { |
|
|
type templateDetailPageData struct { |
|
|
pageData |
|
|
pageData |
|
|
Detail *templatesvc.TemplateDetail |
|
|
|
|
|
Fields []templateFieldView |
|
|
|
|
|
|
|
|
Detail *templatesvc.TemplateDetail |
|
|
|
|
|
Fields []templateFieldView |
|
|
|
|
|
WebsiteSectionOptions []websiteSectionOptionView |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
type buildFieldView struct { |
|
|
type buildFieldView struct { |
|
|
|
|
|
Index int |
|
|
Path string |
|
|
Path string |
|
|
DisplayLabel string |
|
|
DisplayLabel string |
|
|
SampleValue string |
|
|
SampleValue string |
|
|
Value string |
|
|
Value string |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
type buildFieldGroupView struct { |
|
|
|
|
|
Title string |
|
|
|
|
|
Fields []buildFieldView |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
type buildFieldSectionView struct { |
|
|
|
|
|
Key string |
|
|
|
|
|
Title string |
|
|
|
|
|
Description string |
|
|
|
|
|
EditableGroups []buildFieldGroupView |
|
|
|
|
|
EditableFields []buildFieldView |
|
|
|
|
|
DisabledFields []buildFieldView |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
type pendingField struct { |
|
|
|
|
|
Field domain.TemplateField |
|
|
|
|
|
View buildFieldView |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
type fieldRole struct { |
|
|
|
|
|
Label string |
|
|
|
|
|
Order int |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
var blockIDPattern = regexp.MustCompile(`(?i)(?:^|[_.])([mcr]\d{3,})(?:[_.]|$)`) |
|
|
|
|
|
var looseBlockIDPattern = regexp.MustCompile(`(?i)([mcr]\d{3,})`) |
|
|
|
|
|
|
|
|
|
|
|
var knownBlockAreas = map[string]string{ |
|
|
|
|
|
"m1710": "Hero / Haupttitel", |
|
|
|
|
|
"c7886": "Intro / Einleitung", |
|
|
|
|
|
"r4830": "Services", |
|
|
|
|
|
"m4178": "Gallery / Medien", |
|
|
|
|
|
"c2929": "Ueber uns / About", |
|
|
|
|
|
"r4748": "Team", |
|
|
|
|
|
"r1508": "Testimonials", |
|
|
|
|
|
"c1165": "CTA / Highlight / Banner", |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
type buildNewPageData struct { |
|
|
type buildNewPageData struct { |
|
|
pageData |
|
|
pageData |
|
|
Templates []domain.Template |
|
|
Templates []domain.Template |
|
|
@@ -89,6 +138,8 @@ type buildNewPageData struct { |
|
|
SelectedDraftID string |
|
|
SelectedDraftID string |
|
|
SelectedTemplateID int64 |
|
|
SelectedTemplateID int64 |
|
|
SelectedManifestID string |
|
|
SelectedManifestID string |
|
|
|
|
|
FieldSections []buildFieldSectionView |
|
|
|
|
|
EditableFields []buildFieldView |
|
|
EnabledFields []buildFieldView |
|
|
EnabledFields []buildFieldView |
|
|
Form buildFormInput |
|
|
Form buildFormInput |
|
|
} |
|
|
} |
|
|
@@ -188,11 +239,17 @@ func (u *UI) TemplateDetail(w http.ResponseWriter, r *http.Request) { |
|
|
IsRequiredByUs: f.IsRequiredByUs, |
|
|
IsRequiredByUs: f.IsRequiredByUs, |
|
|
DisplayLabel: f.DisplayLabel, |
|
|
DisplayLabel: f.DisplayLabel, |
|
|
DisplayOrder: f.DisplayOrder, |
|
|
DisplayOrder: f.DisplayOrder, |
|
|
|
|
|
WebsiteSection: domain.NormalizeWebsiteSection(f.WebsiteSection), |
|
|
Notes: f.Notes, |
|
|
Notes: f.Notes, |
|
|
SampleValue: f.SampleValue, |
|
|
SampleValue: f.SampleValue, |
|
|
}) |
|
|
}) |
|
|
} |
|
|
} |
|
|
u.render.Render(w, "template_detail", templateDetailPageData{pageData: basePageData(r, "Template Detail", "/templates"), Detail: detail, Fields: fields}) |
|
|
|
|
|
|
|
|
u.render.Render(w, "template_detail", templateDetailPageData{ |
|
|
|
|
|
pageData: basePageData(r, "Template Detail", "/templates"), |
|
|
|
|
|
Detail: detail, |
|
|
|
|
|
Fields: fields, |
|
|
|
|
|
WebsiteSectionOptions: websiteSectionOptions(), |
|
|
|
|
|
}) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func (u *UI) OnboardTemplate(w http.ResponseWriter, r *http.Request) { |
|
|
func (u *UI) OnboardTemplate(w http.ResponseWriter, r *http.Request) { |
|
|
@@ -228,6 +285,7 @@ func (u *UI) UpdateTemplateFields(w http.ResponseWriter, r *http.Request) { |
|
|
required := r.FormValue(fmt.Sprintf("field_required_%d", i)) == "on" |
|
|
required := r.FormValue(fmt.Sprintf("field_required_%d", i)) == "on" |
|
|
label := r.FormValue(fmt.Sprintf("field_label_%d", i)) |
|
|
label := r.FormValue(fmt.Sprintf("field_label_%d", i)) |
|
|
notes := r.FormValue(fmt.Sprintf("field_notes_%d", i)) |
|
|
notes := r.FormValue(fmt.Sprintf("field_notes_%d", i)) |
|
|
|
|
|
websiteSection := r.FormValue(fmt.Sprintf("field_website_section_%d", i)) |
|
|
order, err := strconv.Atoi(strings.TrimSpace(r.FormValue(fmt.Sprintf("field_order_%d", i)))) |
|
|
order, err := strconv.Atoi(strings.TrimSpace(r.FormValue(fmt.Sprintf("field_order_%d", i)))) |
|
|
if err != nil { |
|
|
if err != nil { |
|
|
http.Redirect(w, r, fmt.Sprintf("/templates/%d?err=%s", templateID, urlQuery("invalid display order")), http.StatusSeeOther) |
|
|
http.Redirect(w, r, fmt.Sprintf("/templates/%d?err=%s", templateID, urlQuery("invalid display order")), http.StatusSeeOther) |
|
|
@@ -239,6 +297,7 @@ func (u *UI) UpdateTemplateFields(w http.ResponseWriter, r *http.Request) { |
|
|
IsRequiredByUs: boolPtr(required), |
|
|
IsRequiredByUs: boolPtr(required), |
|
|
DisplayLabel: strPtr(label), |
|
|
DisplayLabel: strPtr(label), |
|
|
DisplayOrder: intPtr(order), |
|
|
DisplayOrder: intPtr(order), |
|
|
|
|
|
WebsiteSection: strPtr(websiteSection), |
|
|
Notes: strPtr(notes), |
|
|
Notes: strPtr(notes), |
|
|
}) |
|
|
}) |
|
|
} |
|
|
} |
|
|
@@ -506,18 +565,564 @@ func (u *UI) loadBuildNewPageData(r *http.Request, page pageData, selectedDraftI |
|
|
return data, nil |
|
|
return data, nil |
|
|
} |
|
|
} |
|
|
data.SelectedManifestID = detail.Manifest.ID |
|
|
data.SelectedManifestID = detail.Manifest.ID |
|
|
for _, f := range detail.Fields { |
|
|
|
|
|
if !f.IsEnabled || f.FieldKind != "text" { |
|
|
|
|
|
|
|
|
data.EditableFields, data.FieldSections = buildFieldSections(detail.Fields, fieldValues) |
|
|
|
|
|
data.EnabledFields = data.EditableFields |
|
|
|
|
|
return data, nil |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func buildFieldSections(fields []domain.TemplateField, fieldValues map[string]string) ([]buildFieldView, []buildFieldSectionView) { |
|
|
|
|
|
sectionOrder := []string{ |
|
|
|
|
|
domain.WebsiteSectionHero, |
|
|
|
|
|
domain.WebsiteSectionIntro, |
|
|
|
|
|
domain.WebsiteSectionServices, |
|
|
|
|
|
domain.WebsiteSectionAbout, |
|
|
|
|
|
domain.WebsiteSectionTeam, |
|
|
|
|
|
domain.WebsiteSectionTestimonials, |
|
|
|
|
|
domain.WebsiteSectionCTA, |
|
|
|
|
|
domain.WebsiteSectionContact, |
|
|
|
|
|
domain.WebsiteSectionGallery, |
|
|
|
|
|
domain.WebsiteSectionFooter, |
|
|
|
|
|
domain.WebsiteSectionOther, |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sectionDescriptions := map[string]string{ |
|
|
|
|
|
domain.WebsiteSectionHero: "Headline-nahe Felder, bevorzugt nach Block-ID gruppiert.", |
|
|
|
|
|
domain.WebsiteSectionIntro: "Intro-/Einleitungs-Felder, bevorzugt nach Block-ID gruppiert.", |
|
|
|
|
|
domain.WebsiteSectionServices: "Services-Felder, bevorzugt nach Block-ID gruppiert.", |
|
|
|
|
|
domain.WebsiteSectionAbout: "About-Felder, bevorzugt nach Block-ID gruppiert.", |
|
|
|
|
|
domain.WebsiteSectionTeam: "Team-Felder, bevorzugt nach Block-ID gruppiert.", |
|
|
|
|
|
domain.WebsiteSectionTestimonials: "Testimonial-Felder, bevorzugt nach Block-ID gruppiert.", |
|
|
|
|
|
domain.WebsiteSectionCTA: "CTA-/Highlight-Felder, bevorzugt nach Block-ID gruppiert.", |
|
|
|
|
|
domain.WebsiteSectionContact: "Kontakt-Felder, bevorzugt nach Block-ID gruppiert.", |
|
|
|
|
|
domain.WebsiteSectionGallery: "Media/Gallery-Felder, Bildfelder bleiben im MVP nicht editierbar.", |
|
|
|
|
|
domain.WebsiteSectionFooter: "Footer-Felder, bevorzugt nach Block-ID gruppiert.", |
|
|
|
|
|
domain.WebsiteSectionOther: "Aktive Textfelder ausserhalb der Kern-Sections, bevorzugt nach Block-ID gruppiert.", |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sectionsByKey := make(map[string]buildFieldSectionView, len(sectionOrder)) |
|
|
|
|
|
pendingByKey := make(map[string][]pendingField, len(sectionOrder)) |
|
|
|
|
|
for _, key := range sectionOrder { |
|
|
|
|
|
sectionsByKey[key] = buildFieldSectionView{ |
|
|
|
|
|
Key: key, |
|
|
|
|
|
Title: domain.WebsiteSectionLabel(key), |
|
|
|
|
|
Description: sectionDescriptions[key], |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
for _, f := range fields { |
|
|
|
|
|
targetSection := preferredBuildSection(f) |
|
|
|
|
|
if isMediaOrGalleryField(f) || targetSection == domain.WebsiteSectionGallery { |
|
|
|
|
|
labelFallback := domain.WebsiteSectionLabel(domain.WebsiteSectionGallery) + " - " + humanizeKey(f.KeyName) |
|
|
|
|
|
if blockID := extractBlockID(f); blockID != "" { |
|
|
|
|
|
labelFallback = "Media - " + blockGroupTitle(blockID) |
|
|
|
|
|
} |
|
|
|
|
|
media := sectionsByKey[domain.WebsiteSectionGallery] |
|
|
|
|
|
media.DisabledFields = append(media.DisabledFields, buildFieldView{ |
|
|
|
|
|
Path: f.Path, |
|
|
|
|
|
DisplayLabel: effectiveLabel(f, labelFallback), |
|
|
|
|
|
SampleValue: f.SampleValue, |
|
|
|
|
|
Value: "", |
|
|
|
|
|
}) |
|
|
|
|
|
sectionsByKey[domain.WebsiteSectionGallery] = media |
|
|
|
|
|
continue |
|
|
|
|
|
} |
|
|
|
|
|
if !f.IsEnabled || !strings.EqualFold(strings.TrimSpace(f.FieldKind), "text") { |
|
|
|
|
|
continue |
|
|
|
|
|
} |
|
|
|
|
|
pf := pendingField{ |
|
|
|
|
|
Field: f, |
|
|
|
|
|
View: buildFieldView{ |
|
|
|
|
|
Path: f.Path, |
|
|
|
|
|
DisplayLabel: effectiveLabel(f, humanizeKey(f.KeyName)), |
|
|
|
|
|
SampleValue: f.SampleValue, |
|
|
|
|
|
Value: strings.TrimSpace(fieldValues[f.Path]), |
|
|
|
|
|
}, |
|
|
|
|
|
} |
|
|
|
|
|
pendingByKey[targetSection] = append(pendingByKey[targetSection], pf) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
for _, key := range sectionOrder { |
|
|
|
|
|
section := sectionsByKey[key] |
|
|
|
|
|
items := pendingByKey[key] |
|
|
|
|
|
switch key { |
|
|
|
|
|
case domain.WebsiteSectionServices: |
|
|
|
|
|
section = applyServicesGrouping(section, items) |
|
|
|
|
|
case domain.WebsiteSectionTestimonials: |
|
|
|
|
|
section = applyTestimonialsGrouping(section, items) |
|
|
|
|
|
case domain.WebsiteSectionHero, domain.WebsiteSectionIntro, domain.WebsiteSectionAbout, domain.WebsiteSectionTeam, domain.WebsiteSectionCTA, domain.WebsiteSectionContact, domain.WebsiteSectionFooter: |
|
|
|
|
|
section = applyTextGrouping(section, items) |
|
|
|
|
|
case domain.WebsiteSectionOther: |
|
|
|
|
|
section = applyOtherGrouping(section, items) |
|
|
|
|
|
case domain.WebsiteSectionGallery: |
|
|
|
|
|
// Gallery fields are handled as disabled entries only in this MVP. |
|
|
|
|
|
default: |
|
|
|
|
|
section = applyOtherGrouping(section, items) |
|
|
|
|
|
} |
|
|
|
|
|
sectionsByKey[key] = section |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sections := make([]buildFieldSectionView, 0, len(sectionOrder)) |
|
|
|
|
|
for _, key := range sectionOrder { |
|
|
|
|
|
sections = append(sections, sectionsByKey[key]) |
|
|
|
|
|
} |
|
|
|
|
|
return assignEditableIndexes(sections) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func applyServicesGrouping(section buildFieldSectionView, fields []pendingField) buildFieldSectionView { |
|
|
|
|
|
return applyBlockFirstGrouping(section, fields, "Services", applyServicesGroupingFallback) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func applyServicesGroupingFallback(section buildFieldSectionView, fields []pendingField) buildFieldSectionView { |
|
|
|
|
|
titles := make([]pendingField, 0) |
|
|
|
|
|
descriptions := make([]pendingField, 0) |
|
|
|
|
|
other := make([]buildFieldView, 0) |
|
|
|
|
|
|
|
|
|
|
|
for _, pf := range fields { |
|
|
|
|
|
key := strings.ToLower(strings.TrimSpace(pf.Field.KeyName)) |
|
|
|
|
|
switch { |
|
|
|
|
|
case strings.HasPrefix(key, "servicestitle_") || strings.HasPrefix(key, "servicestitle"): |
|
|
|
|
|
titles = append(titles, pf) |
|
|
|
|
|
case strings.HasPrefix(key, "servicesdescription_") || strings.HasPrefix(key, "servicesdescription"): |
|
|
|
|
|
descriptions = append(descriptions, pf) |
|
|
|
|
|
default: |
|
|
|
|
|
pf.View.DisplayLabel = effectiveLabel(pf.Field, "Services - "+humanizeKey(pf.Field.KeyName)) |
|
|
|
|
|
other = append(other, pf.View) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
maxCount := len(titles) |
|
|
|
|
|
if len(descriptions) > maxCount { |
|
|
|
|
|
maxCount = len(descriptions) |
|
|
|
|
|
} |
|
|
|
|
|
for i := 0; i < maxCount; i++ { |
|
|
|
|
|
block := buildFieldGroupView{Title: fmt.Sprintf("Service %d", i+1)} |
|
|
|
|
|
if i < len(titles) { |
|
|
|
|
|
item := titles[i] |
|
|
|
|
|
item.View.DisplayLabel = effectiveLabel(item.Field, fmt.Sprintf("Service %d - Titel", i+1)) |
|
|
|
|
|
block.Fields = append(block.Fields, item.View) |
|
|
|
|
|
} |
|
|
|
|
|
if i < len(descriptions) { |
|
|
|
|
|
item := descriptions[i] |
|
|
|
|
|
item.View.DisplayLabel = effectiveLabel(item.Field, fmt.Sprintf("Service %d - Beschreibung", i+1)) |
|
|
|
|
|
block.Fields = append(block.Fields, item.View) |
|
|
|
|
|
} |
|
|
|
|
|
if len(block.Fields) > 0 { |
|
|
|
|
|
section.EditableGroups = append(section.EditableGroups, block) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
section.EditableFields = append(section.EditableFields, other...) |
|
|
|
|
|
return section |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func applyTestimonialsGrouping(section buildFieldSectionView, fields []pendingField) buildFieldSectionView { |
|
|
|
|
|
return applyBlockFirstGrouping(section, fields, "Testimonials", applyTestimonialsGroupingFallback) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func applyTestimonialsGroupingFallback(section buildFieldSectionView, fields []pendingField) buildFieldSectionView { |
|
|
|
|
|
names := make([]pendingField, 0) |
|
|
|
|
|
titles := make([]pendingField, 0) |
|
|
|
|
|
descriptions := make([]pendingField, 0) |
|
|
|
|
|
other := make([]buildFieldView, 0) |
|
|
|
|
|
|
|
|
|
|
|
for _, pf := range fields { |
|
|
|
|
|
key := strings.ToLower(strings.TrimSpace(pf.Field.KeyName)) |
|
|
|
|
|
switch { |
|
|
|
|
|
case strings.HasPrefix(key, "testimonialsname_") || strings.HasPrefix(key, "testimonialsname"): |
|
|
|
|
|
names = append(names, pf) |
|
|
|
|
|
case strings.HasPrefix(key, "testimonialstitle_") || strings.HasPrefix(key, "testimonialstitle"): |
|
|
|
|
|
titles = append(titles, pf) |
|
|
|
|
|
case strings.HasPrefix(key, "testimonialsdescription_") || strings.HasPrefix(key, "testimonialsdescription"): |
|
|
|
|
|
descriptions = append(descriptions, pf) |
|
|
|
|
|
default: |
|
|
|
|
|
pf.View.DisplayLabel = effectiveLabel(pf.Field, "Testimonials - "+humanizeKey(pf.Field.KeyName)) |
|
|
|
|
|
other = append(other, pf.View) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
maxCount := len(names) |
|
|
|
|
|
if len(titles) > maxCount { |
|
|
|
|
|
maxCount = len(titles) |
|
|
|
|
|
} |
|
|
|
|
|
if len(descriptions) > maxCount { |
|
|
|
|
|
maxCount = len(descriptions) |
|
|
|
|
|
} |
|
|
|
|
|
for i := 0; i < maxCount; i++ { |
|
|
|
|
|
block := buildFieldGroupView{Title: fmt.Sprintf("Testimonial %d", i+1)} |
|
|
|
|
|
if i < len(names) { |
|
|
|
|
|
item := names[i] |
|
|
|
|
|
item.View.DisplayLabel = effectiveLabel(item.Field, fmt.Sprintf("Testimonial %d - Name", i+1)) |
|
|
|
|
|
block.Fields = append(block.Fields, item.View) |
|
|
|
|
|
} |
|
|
|
|
|
if i < len(titles) { |
|
|
|
|
|
item := titles[i] |
|
|
|
|
|
item.View.DisplayLabel = effectiveLabel(item.Field, fmt.Sprintf("Testimonial %d - Titel", i+1)) |
|
|
|
|
|
block.Fields = append(block.Fields, item.View) |
|
|
|
|
|
} |
|
|
|
|
|
if i < len(descriptions) { |
|
|
|
|
|
item := descriptions[i] |
|
|
|
|
|
item.View.DisplayLabel = effectiveLabel(item.Field, fmt.Sprintf("Testimonial %d - Beschreibung", i+1)) |
|
|
|
|
|
block.Fields = append(block.Fields, item.View) |
|
|
|
|
|
} |
|
|
|
|
|
if len(block.Fields) > 0 { |
|
|
|
|
|
section.EditableGroups = append(section.EditableGroups, block) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
section.EditableFields = append(section.EditableFields, other...) |
|
|
|
|
|
return section |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func applyTextGrouping(section buildFieldSectionView, fields []pendingField) buildFieldSectionView { |
|
|
|
|
|
return applyBlockFirstGrouping(section, fields, "Text", applyTextGroupingFallback) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func applyTextGroupingFallback(section buildFieldSectionView, fields []pendingField) buildFieldSectionView { |
|
|
|
|
|
titles := make([]pendingField, 0) |
|
|
|
|
|
descriptions := make([]pendingField, 0) |
|
|
|
|
|
names := make([]pendingField, 0) |
|
|
|
|
|
other := make([]buildFieldView, 0) |
|
|
|
|
|
|
|
|
|
|
|
for _, pf := range fields { |
|
|
|
|
|
key := strings.ToLower(strings.TrimSpace(pf.Field.KeyName)) |
|
|
|
|
|
switch { |
|
|
|
|
|
case strings.HasPrefix(key, "texttitle_") || strings.HasPrefix(key, "texttitle") || strings.HasPrefix(key, "exttitle_") || strings.HasPrefix(key, "exttitle"): |
|
|
|
|
|
titles = append(titles, pf) |
|
|
|
|
|
case strings.HasPrefix(key, "textdescription_") || strings.HasPrefix(key, "textdescription") || strings.HasPrefix(key, "extdescription_") || strings.HasPrefix(key, "extdescription"): |
|
|
|
|
|
descriptions = append(descriptions, pf) |
|
|
|
|
|
case strings.HasPrefix(key, "textname_") || strings.HasPrefix(key, "textname") || strings.HasPrefix(key, "extname_") || strings.HasPrefix(key, "extname"): |
|
|
|
|
|
names = append(names, pf) |
|
|
|
|
|
default: |
|
|
|
|
|
pf.View.DisplayLabel = effectiveLabel(pf.Field, "Text - "+humanizeKey(pf.Field.KeyName)) |
|
|
|
|
|
other = append(other, pf.View) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
maxCount := len(titles) |
|
|
|
|
|
if len(descriptions) > maxCount { |
|
|
|
|
|
maxCount = len(descriptions) |
|
|
|
|
|
} |
|
|
|
|
|
if len(names) > maxCount { |
|
|
|
|
|
maxCount = len(names) |
|
|
|
|
|
} |
|
|
|
|
|
for i := 0; i < maxCount; i++ { |
|
|
|
|
|
block := buildFieldGroupView{Title: fmt.Sprintf("Textblock %d", i+1)} |
|
|
|
|
|
if i < len(titles) { |
|
|
|
|
|
item := titles[i] |
|
|
|
|
|
item.View.DisplayLabel = effectiveLabel(item.Field, fmt.Sprintf("Textblock %d - Titel", i+1)) |
|
|
|
|
|
block.Fields = append(block.Fields, item.View) |
|
|
|
|
|
} |
|
|
|
|
|
if i < len(descriptions) { |
|
|
|
|
|
item := descriptions[i] |
|
|
|
|
|
item.View.DisplayLabel = effectiveLabel(item.Field, fmt.Sprintf("Textblock %d - Beschreibung", i+1)) |
|
|
|
|
|
block.Fields = append(block.Fields, item.View) |
|
|
|
|
|
} |
|
|
|
|
|
if i < len(names) { |
|
|
|
|
|
item := names[i] |
|
|
|
|
|
item.View.DisplayLabel = effectiveLabel(item.Field, fmt.Sprintf("Textblock %d - Name", i+1)) |
|
|
|
|
|
block.Fields = append(block.Fields, item.View) |
|
|
|
|
|
} |
|
|
|
|
|
if len(block.Fields) > 0 { |
|
|
|
|
|
section.EditableGroups = append(section.EditableGroups, block) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
section.EditableFields = append(section.EditableFields, other...) |
|
|
|
|
|
return section |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func applyOtherGrouping(section buildFieldSectionView, fields []pendingField) buildFieldSectionView { |
|
|
|
|
|
return applyBlockFirstGrouping(section, fields, "", applyOtherGroupingFallback) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func applyOtherGroupingFallback(section buildFieldSectionView, fields []pendingField) buildFieldSectionView { |
|
|
|
|
|
for _, pf := range fields { |
|
|
|
|
|
pf.View.DisplayLabel = effectiveLabel(pf.Field, normalizedSectionTitle(pf.Field.Section)+" - "+humanizeKey(pf.Field.KeyName)) |
|
|
|
|
|
section.EditableFields = append(section.EditableFields, pf.View) |
|
|
|
|
|
} |
|
|
|
|
|
sort.SliceStable(section.EditableFields, func(i, j int) bool { |
|
|
|
|
|
return section.EditableFields[i].Path < section.EditableFields[j].Path |
|
|
|
|
|
}) |
|
|
|
|
|
return section |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func applyBlockFirstGrouping(section buildFieldSectionView, fields []pendingField, fallbackPrefix string, fallback func(buildFieldSectionView, []pendingField) buildFieldSectionView) buildFieldSectionView { |
|
|
|
|
|
grouped := map[string][]pendingField{} |
|
|
|
|
|
withoutBlockID := make([]pendingField, 0) |
|
|
|
|
|
for _, pf := range fields { |
|
|
|
|
|
blockID := extractBlockID(pf.Field) |
|
|
|
|
|
if blockID == "" { |
|
|
|
|
|
withoutBlockID = append(withoutBlockID, pf) |
|
|
continue |
|
|
continue |
|
|
} |
|
|
} |
|
|
data.EnabledFields = append(data.EnabledFields, buildFieldView{ |
|
|
|
|
|
Path: f.Path, |
|
|
|
|
|
DisplayLabel: f.DisplayLabel, |
|
|
|
|
|
SampleValue: f.SampleValue, |
|
|
|
|
|
Value: strings.TrimSpace(fieldValues[f.Path]), |
|
|
|
|
|
|
|
|
grouped[blockID] = append(grouped[blockID], pf) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if len(grouped) > 0 { |
|
|
|
|
|
blockIDs := make([]string, 0, len(grouped)) |
|
|
|
|
|
for blockID := range grouped { |
|
|
|
|
|
blockIDs = append(blockIDs, blockID) |
|
|
|
|
|
} |
|
|
|
|
|
sort.SliceStable(blockIDs, func(i, j int) bool { |
|
|
|
|
|
li, lj := blockSortRank(blockIDs[i]), blockSortRank(blockIDs[j]) |
|
|
|
|
|
if li != lj { |
|
|
|
|
|
return li < lj |
|
|
|
|
|
} |
|
|
|
|
|
return blockIDs[i] < blockIDs[j] |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
for _, blockID := range blockIDs { |
|
|
|
|
|
items := grouped[blockID] |
|
|
|
|
|
sort.SliceStable(items, func(i, j int) bool { |
|
|
|
|
|
ri := deriveFieldRole(items[i].Field.KeyName) |
|
|
|
|
|
rj := deriveFieldRole(items[j].Field.KeyName) |
|
|
|
|
|
if ri.Order != rj.Order { |
|
|
|
|
|
return ri.Order < rj.Order |
|
|
|
|
|
} |
|
|
|
|
|
return items[i].Field.Path < items[j].Field.Path |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
group := buildFieldGroupView{Title: blockGroupTitle(blockID)} |
|
|
|
|
|
for _, item := range items { |
|
|
|
|
|
role := deriveFieldRole(item.Field.KeyName) |
|
|
|
|
|
fallbackLabel := role.Label |
|
|
|
|
|
if fallbackLabel == "" { |
|
|
|
|
|
fallbackLabel = humanizeKey(item.Field.KeyName) |
|
|
|
|
|
} |
|
|
|
|
|
item.View.DisplayLabel = effectiveLabel(item.Field, fallbackLabel) |
|
|
|
|
|
group.Fields = append(group.Fields, item.View) |
|
|
|
|
|
} |
|
|
|
|
|
if len(group.Fields) > 0 { |
|
|
|
|
|
section.EditableGroups = append(section.EditableGroups, group) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
return data, nil |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if len(withoutBlockID) > 0 { |
|
|
|
|
|
if fallback != nil { |
|
|
|
|
|
return fallback(section, withoutBlockID) |
|
|
|
|
|
} |
|
|
|
|
|
for _, pf := range withoutBlockID { |
|
|
|
|
|
labelPrefix := fallbackPrefix |
|
|
|
|
|
if labelPrefix == "" { |
|
|
|
|
|
labelPrefix = normalizedSectionTitle(pf.Field.Section) |
|
|
|
|
|
} |
|
|
|
|
|
pf.View.DisplayLabel = effectiveLabel(pf.Field, labelPrefix+" - "+humanizeKey(pf.Field.KeyName)) |
|
|
|
|
|
section.EditableFields = append(section.EditableFields, pf.View) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
return section |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func extractBlockID(f domain.TemplateField) string { |
|
|
|
|
|
candidates := []string{f.KeyName, f.Path} |
|
|
|
|
|
for _, candidate := range candidates { |
|
|
|
|
|
normalized := strings.ToLower(strings.TrimSpace(candidate)) |
|
|
|
|
|
if normalized == "" { |
|
|
|
|
|
continue |
|
|
|
|
|
} |
|
|
|
|
|
if match := blockIDPattern.FindStringSubmatch(normalized); len(match) > 1 { |
|
|
|
|
|
return match[1] |
|
|
|
|
|
} |
|
|
|
|
|
if match := looseBlockIDPattern.FindStringSubmatch(normalized); len(match) > 1 { |
|
|
|
|
|
return match[1] |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
return "" |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func blockGroupTitle(blockID string) string { |
|
|
|
|
|
blockID = strings.ToLower(strings.TrimSpace(blockID)) |
|
|
|
|
|
if blockID == "" { |
|
|
|
|
|
return "Unbekannter Block" |
|
|
|
|
|
} |
|
|
|
|
|
if known, ok := knownBlockAreas[blockID]; ok { |
|
|
|
|
|
return fmt.Sprintf("%s (%s)", known, blockID) |
|
|
|
|
|
} |
|
|
|
|
|
return "Block " + blockID |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func blockSortRank(blockID string) int { |
|
|
|
|
|
switch strings.ToLower(strings.TrimSpace(blockID)) { |
|
|
|
|
|
case "m1710": |
|
|
|
|
|
return 10 |
|
|
|
|
|
case "c7886": |
|
|
|
|
|
return 20 |
|
|
|
|
|
case "r4830": |
|
|
|
|
|
return 30 |
|
|
|
|
|
case "c2929": |
|
|
|
|
|
return 40 |
|
|
|
|
|
case "r4748": |
|
|
|
|
|
return 50 |
|
|
|
|
|
case "r1508": |
|
|
|
|
|
return 60 |
|
|
|
|
|
case "c1165": |
|
|
|
|
|
return 70 |
|
|
|
|
|
case "m4178": |
|
|
|
|
|
return 80 |
|
|
|
|
|
default: |
|
|
|
|
|
return 1000 |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func deriveFieldRole(key string) fieldRole { |
|
|
|
|
|
normalized := strings.ToLower(strings.TrimSpace(key)) |
|
|
|
|
|
switch { |
|
|
|
|
|
case strings.Contains(normalized, "subtitle"): |
|
|
|
|
|
return fieldRole{Label: "Untertitel", Order: 15} |
|
|
|
|
|
case strings.Contains(normalized, "title"): |
|
|
|
|
|
return fieldRole{Label: "Titel", Order: 20} |
|
|
|
|
|
case strings.Contains(normalized, "description"): |
|
|
|
|
|
return fieldRole{Label: "Beschreibung", Order: 30} |
|
|
|
|
|
case strings.Contains(normalized, "name"): |
|
|
|
|
|
return fieldRole{Label: "Name", Order: 40} |
|
|
|
|
|
case strings.Contains(normalized, "button") || strings.Contains(normalized, "cta"): |
|
|
|
|
|
return fieldRole{Label: "CTA Text", Order: 50} |
|
|
|
|
|
default: |
|
|
|
|
|
return fieldRole{Label: humanizeKey(key), Order: 100} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func assignEditableIndexes(sections []buildFieldSectionView) ([]buildFieldView, []buildFieldSectionView) { |
|
|
|
|
|
editable := make([]buildFieldView, 0) |
|
|
|
|
|
nextIndex := 0 |
|
|
|
|
|
for si := range sections { |
|
|
|
|
|
for gi := range sections[si].EditableGroups { |
|
|
|
|
|
for fi := range sections[si].EditableGroups[gi].Fields { |
|
|
|
|
|
sections[si].EditableGroups[gi].Fields[fi].Index = nextIndex |
|
|
|
|
|
editable = append(editable, sections[si].EditableGroups[gi].Fields[fi]) |
|
|
|
|
|
nextIndex++ |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
for fi := range sections[si].EditableFields { |
|
|
|
|
|
sections[si].EditableFields[fi].Index = nextIndex |
|
|
|
|
|
editable = append(editable, sections[si].EditableFields[fi]) |
|
|
|
|
|
nextIndex++ |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
return editable, sections |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func preferredBuildSection(f domain.TemplateField) string { |
|
|
|
|
|
websiteSection := strings.TrimSpace(f.WebsiteSection) |
|
|
|
|
|
if websiteSection != "" { |
|
|
|
|
|
normalized := domain.NormalizeWebsiteSection(websiteSection) |
|
|
|
|
|
if normalized == domain.WebsiteSectionServiceItem { |
|
|
|
|
|
return domain.WebsiteSectionServices |
|
|
|
|
|
} |
|
|
|
|
|
return normalized |
|
|
|
|
|
} |
|
|
|
|
|
return fallbackBuildSection(f) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func fallbackBuildSection(f domain.TemplateField) string { |
|
|
|
|
|
switch normalizedSection(f.Section) { |
|
|
|
|
|
case "services": |
|
|
|
|
|
return domain.WebsiteSectionServices |
|
|
|
|
|
case "testimonials": |
|
|
|
|
|
return domain.WebsiteSectionTestimonials |
|
|
|
|
|
case "text": |
|
|
|
|
|
if isMediaOrGalleryField(f) { |
|
|
|
|
|
return domain.WebsiteSectionGallery |
|
|
|
|
|
} |
|
|
|
|
|
return domain.WebsiteSectionOther |
|
|
|
|
|
default: |
|
|
|
|
|
if isMediaOrGalleryField(f) { |
|
|
|
|
|
return domain.WebsiteSectionGallery |
|
|
|
|
|
} |
|
|
|
|
|
return domain.WebsiteSectionOther |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func normalizedSection(raw string) string { |
|
|
|
|
|
section := strings.ToLower(strings.TrimSpace(raw)) |
|
|
|
|
|
switch section { |
|
|
|
|
|
case "ext": |
|
|
|
|
|
return "text" |
|
|
|
|
|
default: |
|
|
|
|
|
return section |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func normalizedSectionTitle(raw string) string { |
|
|
|
|
|
switch normalizedSection(raw) { |
|
|
|
|
|
case "text": |
|
|
|
|
|
return "Text" |
|
|
|
|
|
case "services": |
|
|
|
|
|
return "Services" |
|
|
|
|
|
case "testimonials": |
|
|
|
|
|
return "Testimonials" |
|
|
|
|
|
case "gallery", "media": |
|
|
|
|
|
return "Media" |
|
|
|
|
|
default: |
|
|
|
|
|
return "Feld" |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func isMediaOrGalleryField(f domain.TemplateField) bool { |
|
|
|
|
|
if strings.EqualFold(strings.TrimSpace(f.FieldKind), "image") { |
|
|
|
|
|
return true |
|
|
|
|
|
} |
|
|
|
|
|
section := strings.ToLower(strings.TrimSpace(f.Section)) |
|
|
|
|
|
key := strings.ToLower(strings.TrimSpace(f.KeyName)) |
|
|
|
|
|
path := strings.ToLower(strings.TrimSpace(f.Path)) |
|
|
|
|
|
if section == "gallery" || section == "media" { |
|
|
|
|
|
return true |
|
|
|
|
|
} |
|
|
|
|
|
hints := []string{"gallery", "image", "img", "photo", "picture"} |
|
|
|
|
|
for _, hint := range hints { |
|
|
|
|
|
if strings.Contains(section, hint) || strings.Contains(key, hint) || strings.Contains(path, hint) { |
|
|
|
|
|
return true |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
return false |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func effectiveLabel(f domain.TemplateField, fallback string) string { |
|
|
|
|
|
if !isRawPathLikeLabel(f.DisplayLabel, f.Path) { |
|
|
|
|
|
return strings.TrimSpace(f.DisplayLabel) |
|
|
|
|
|
} |
|
|
|
|
|
return strings.TrimSpace(fallback) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func isRawPathLikeLabel(label string, path string) bool { |
|
|
|
|
|
l := strings.TrimSpace(label) |
|
|
|
|
|
if l == "" { |
|
|
|
|
|
return true |
|
|
|
|
|
} |
|
|
|
|
|
if strings.EqualFold(l, strings.TrimSpace(path)) { |
|
|
|
|
|
return true |
|
|
|
|
|
} |
|
|
|
|
|
if strings.Contains(l, ".") || strings.Contains(l, "_") { |
|
|
|
|
|
return true |
|
|
|
|
|
} |
|
|
|
|
|
return false |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func humanizeKey(key string) string { |
|
|
|
|
|
raw := strings.TrimSpace(key) |
|
|
|
|
|
if raw == "" { |
|
|
|
|
|
return "Inhalt" |
|
|
|
|
|
} |
|
|
|
|
|
base := raw |
|
|
|
|
|
if idx := strings.Index(base, "_"); idx > 0 { |
|
|
|
|
|
base = base[:idx] |
|
|
|
|
|
} |
|
|
|
|
|
runes := make([]rune, 0, len(base)+4) |
|
|
|
|
|
for i, r := range base { |
|
|
|
|
|
if i > 0 && unicode.IsUpper(r) { |
|
|
|
|
|
runes = append(runes, ' ') |
|
|
|
|
|
} |
|
|
|
|
|
runes = append(runes, r) |
|
|
|
|
|
} |
|
|
|
|
|
human := strings.TrimSpace(string(runes)) |
|
|
|
|
|
if human == "" { |
|
|
|
|
|
return "Inhalt" |
|
|
|
|
|
} |
|
|
|
|
|
words := strings.Fields(strings.ToLower(human)) |
|
|
|
|
|
for i := range words { |
|
|
|
|
|
if len(words[i]) > 0 { |
|
|
|
|
|
words[i] = strings.ToUpper(words[i][:1]) + words[i][1:] |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
return strings.Join(words, " ") |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func buildFormInputFromRequest(r *http.Request) buildFormInput { |
|
|
func buildFormInputFromRequest(r *http.Request) buildFormInput { |
|
|
@@ -642,3 +1247,15 @@ func defaultDraftStatus(status string) string { |
|
|
return "draft" |
|
|
return "draft" |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func websiteSectionOptions() []websiteSectionOptionView { |
|
|
|
|
|
values := domain.WebsiteSectionOptions() |
|
|
|
|
|
out := make([]websiteSectionOptionView, 0, len(values)) |
|
|
|
|
|
for _, value := range values { |
|
|
|
|
|
out = append(out, websiteSectionOptionView{ |
|
|
|
|
|
Value: value, |
|
|
|
|
|
Label: domain.WebsiteSectionLabel(value), |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
|
|
|
return out |
|
|
|
|
|
} |