feat(admin): post content on the Blog Editor field type

- blogeditor and mlblogeditor field types (classes.BlogEditorTypes) on the
  plugin; post content is mlblogeditor with htmlColumn content_html
- Decode stores SanitizeHTMLReport output per locale with notices naming
  what was removed; clean HTML is stored byte for byte, nothing is refused
  because of markup
- Load serves HTML as stored and markdown posts as their content_html
- the admin hooks copy written content to content_html per locale without
  goldmark; untouched content leaves both columns as they are
- editor lang keys (en, pl plural notices), tracer smoke test, updated
  Postgres admin tests
This commit is contained in:
2026-10-10 12:02:55 +02:00
parent cbf4891703
commit fbd1c85168
9 changed files with 805 additions and 30 deletions

View File

@@ -6,7 +6,6 @@ import (
"fmt"
"strconv"
"git.golem15.com/golem15/sm-journal-plugin/classes"
"git.golem15.com/golem15/sm-journal-plugin/console"
"git.golem15.com/golem15/sm-journal-plugin/models"
"git.golem15.com/golem15/summercms/modules/backpack"
@@ -210,13 +209,75 @@ func preparePostWrite(ctx context.Context, post *models.Post) error {
"published_at": []string{"golem15.journal::lang.post.published_validation"},
}}
}
html, err := classes.FormatHTML(post.Content)
if err != nil {
return &cabana.ValidationError{Details: map[string]any{
"content": []string{err.Error()},
}}
// D-02: the Blog Editor's Decode already stored the policy's output in
// content; content_html gets the same HTML, without goldmark. Content the
// save does not write (untouched, D-03) leaves content_html as it is.
if writes, ok := cabana.FieldWritesFromContext(ctx); ok {
for _, w := range writes["content"] {
if !w.Default {
continue
}
if html, isString := w.Value.(string); isString {
post.ContentHTML = html
}
}
}
return nil
}
// FormAfterCreate writes content_html for every non-default locale whose
// content the save wrote (D-02).
func (c postsAdminController) FormAfterCreate(ctx context.Context, model any) error {
return c.writeContentHTMLTranslations(ctx, model)
}
// FormAfterUpdate writes content_html for every non-default locale whose
// content the save wrote (D-02). Untouched locales are not listed and keep
// their content_html.
func (c postsAdminController) FormAfterUpdate(ctx context.Context, model any) error {
return c.writeContentHTMLTranslations(ctx, model)
}
// writeContentHTMLTranslations stores each written non-default content
// value as that locale's content_html through the published
// cabana.TranslationWriter, inside the save transaction. Without a writer
// there is nothing to write.
func (c postsAdminController) writeContentHTMLTranslations(ctx context.Context, model any) error {
post, ok := model.(*models.Post)
if !ok || post == nil {
return errors.New("posts admin: form model is not a post")
}
writes, ok := cabana.FieldWritesFromContext(ctx)
if !ok {
return nil
}
var pending []cabana.FieldWrite
for _, w := range writes["content"] {
if !w.Default && w.Locale != "" {
pending = append(pending, w)
}
}
if len(pending) == 0 {
return nil
}
app := c.application()
if app == nil {
return nil
}
writer, ok := app.Lookup[cabana.TranslationWriter]()
if !ok || writer == nil {
return nil
}
tx := c.db(ctx)
for _, w := range pending {
html, isString := w.Value.(string)
if !isString {
return fmt.Errorf("posts admin: content for %s is %T, want a string", w.Locale, w.Value)
}
if err := writer.WriteTranslated(ctx, tx, post, "content_html", w.Locale, html); err != nil {
return err
}
}
post.ContentHTML = html
return nil
}