package controllers import ( "context" "strconv" "strings" "git.golem15.com/golem15/sm-seo-plugin/classes" "git.golem15.com/golem15/sm-seo-plugin/models" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/pact" "gorm.io/gorm" ) // rulesAdminController lists and edits URL rules (D-02, UI-SPEC C7). type rulesAdminController struct { app func() *backpack.App } func (rulesAdminController) ID() string { return RulesControllerID } func (rulesAdminController) ModelName() string { return `Golem15\SEO\Models\Rule` } func (rulesAdminController) ConfigDir() string { return "controllers/rules" } func (rulesAdminController) NewRecord() any { return &models.Rule{} } func (rulesAdminController) RequiredPermissions() []string { return []string{PermissionManage} } // FormBeforeCreate trims the address, normalises the match and refuses an // invalid or taken address. func (c rulesAdminController) FormBeforeCreate(ctx context.Context, model any) error { return c.check(ctx, model) } // FormBeforeUpdate checks an update like a create. func (c rulesAdminController) FormBeforeUpdate(ctx context.Context, model any) error { return c.check(ctx, model) } func (c rulesAdminController) check(ctx context.Context, model any) error { r, ok := model.(*models.Rule) if !ok || r == nil { return nil } r.URL = strings.TrimSpace(r.URL) r.Match = models.NormalizeMatch(r.Match) db, err := database(ctx, c.app) if err != nil { return err } return problemsError(ctx, translator(c.app), r.Problems(db)) } // FormAfterDelete records the deleted rule in history inside the delete's // transaction, so its values can be restored. func (c rulesAdminController) FormAfterDelete(ctx context.Context, model any) error { r, ok := model.(*models.Rule) if !ok || r == nil { return nil } svc, err := classes.NewService(appOf(c.app)) if err != nil { return err } db, err := database(ctx, c.app) if err != nil { return err } return svc.Record(ctx, db, classes.Change{ Kind: models.SubjectRule, SubjectID: strconv.FormatUint(uint64(r.ID), 10), What: svc.Phrase("golem15.seo::lang.history.what_rule", map[string]string{"url": r.URL}), Before: classes.RuleValues(*r), }) } // ListExtendRecords fills the overrides column: the translated labels of // the properties each rule sets, comma-joined. func (c rulesAdminController) ListExtendRecords(ctx context.Context, _ *gorm.DB, records []any) error { tr := translator(c.app) for _, record := range records { r, ok := record.(*models.Rule) if !ok || r == nil { continue } var labels []string for _, p := range models.RuleProperties { if strings.TrimSpace(r.Value(p.Column)) != "" { labels = append(labels, Phrase(ctx, tr, p.Label, nil)) } } r.Overrides = strings.Join(labels, ", ") } return nil } // service returns the SEO service of the booted app, nil before boot. func (c rulesAdminController) service() *classes.Service { svc, err := classes.NewService(appOf(c.app)) if err != nil { return nil } return svc } // AdminJS loads the SEO element bundle on the rules screens (the result // preview widget). func (rulesAdminController) AdminJS() []string { return []string{"assets/dist/seo.js"} } // AdminCSS loads the SEO element styles on the rules screens. func (rulesAdminController) AdminCSS() []string { return []string{"assets/dist/seo.css"} } // AdminActions registers the result preview's widget action (E3). func (c rulesAdminController) AdminActions() []pact.AdminAction { return []pact.AdminAction{classes.RuleSnippetAction(c.service, "golem15.seo::lang.snippet.action", PermissionManage)} } // PartialData serves P1 seo_status (canonical warnings) and P2 // seo_effective of a rule. func (c rulesAdminController) PartialData(ctx context.Context, name string, record any) (any, error) { svc := c.service() r, ok := record.(*models.Rule) if svc == nil || !ok || r == nil { if name == "seo_effective" { return classes.EffectiveView{}, nil } return classes.StatusView{}, nil } switch name { case "seo_status": return svc.RuleStatus(ctx, *r), nil case "seo_effective": return svc.RuleEffective(ctx, *r) } return nil, nil } // FilterOptions serves the match filter's two choices. func (rulesAdminController) FilterOptions(scope string) []pact.Option { if scope != models.FilterMatch { return nil } return []pact.Option{ {Value: models.MatchExact, Label: "golem15.seo::lang.rules.match_exact"}, {Value: models.MatchPattern, Label: "golem15.seo::lang.rules.match_pattern"}, } } var ( _ pact.AdminController = rulesAdminController{} _ pact.AdminPermissioned = rulesAdminController{} _ pact.AdminRecordSource = rulesAdminController{} _ pact.FormBeforeCreate = rulesAdminController{} _ pact.FormBeforeUpdate = rulesAdminController{} _ pact.ListExtendRecords = rulesAdminController{} _ pact.FilterOptions = rulesAdminController{} _ pact.FormAfterDelete = rulesAdminController{} _ pact.HasAdminActions = rulesAdminController{} _ pact.AdminPartialData = rulesAdminController{} _ pact.AdminClientAssets = rulesAdminController{} )