package controllers import ( "context" "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/lagoon" "git.golem15.com/golem15/summercms/modules/pact" "gorm.io/gorm" ) // AutolinksControllerID is the Auto-links screen (UI-SPEC C22). const AutolinksControllerID = "golem15.seo.autolinks" // ActionFromKeyphrases is the Auto-links toolbar action that adds the // records' focus keyphrases (UI-SPEC C22). const ActionFromKeyphrases = "from_keyphrases" // autolinksAdminController lists and edits the auto-link phrases (D-82). // History of a save comes from the plugin's own field-less form extension // on this controller, which runs inside the save transaction once the row // is written. type autolinksAdminController struct { app func() *backpack.App } func (autolinksAdminController) ID() string { return AutolinksControllerID } func (autolinksAdminController) ModelName() string { return `Golem15\SEO\Models\Autolink` } func (autolinksAdminController) ConfigDir() string { return "controllers/autolinks" } func (autolinksAdminController) RequiredPermissions() []string { return []string{PermissionManage} } // NewRecord is a phrase with the form's defaults: every locale, active, // entered by hand. func (autolinksAdminController) NewRecord() any { a := models.NewAutolink() return &a } // FormBeforeCreate refuses a short phrase, an invalid address and a // phrase the locale already has (UI-SPEC C22); a create is manual. func (c autolinksAdminController) FormBeforeCreate(ctx context.Context, model any) error { if a, ok := model.(*models.Autolink); ok && a != nil { a.Origin = models.AutolinkOriginManual } return c.check(ctx, model) } // FormBeforeUpdate checks an update like a create. func (c autolinksAdminController) FormBeforeUpdate(ctx context.Context, model any) error { return c.check(ctx, model) } func (c autolinksAdminController) check(ctx context.Context, model any) error { a, ok := model.(*models.Autolink) if !ok || a == 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 } problems, err := svc.AutolinkProblems(ctx, db, a) if err != nil { return err } return problemsError(ctx, translator(c.app), problems) } // FormAfterDelete records the deleted phrase in history inside the // delete's transaction. func (c autolinksAdminController) FormAfterDelete(ctx context.Context, model any) error { a, ok := model.(*models.Autolink) if !ok || a == 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.RecordAutolink(ctx, db, classes.AutolinkValues(*a), *a, true) } // ListRowStates marks an inactive phrase disabled (autolinks.state_off). func (autolinksAdminController) ListRowStates(_ context.Context, _ *gorm.DB, records []any) ([][]pact.RowState, error) { out := make([][]pact.RowState, len(records)) for i, record := range records { if a, ok := record.(*models.Autolink); ok && a != nil && !a.Enabled { out[i] = []pact.RowState{pact.RowStateDisabled} } } return out, nil } // autolinkOriginLabels are the phrase keys of the origins. var autolinkOriginLabels = map[string]string{ models.AutolinkOriginManual: "golem15.seo::lang.autolinks.origin_manual", models.AutolinkOriginKeyphrase: "golem15.seo::lang.autolinks.origin_keyphrase", models.AutolinkOriginMCP: "golem15.seo::lang.autolinks.origin_mcp", } // locales returns the site's content locales, the default first. func (c autolinksAdminController) locales(ctx context.Context) []string { svc, err := classes.NewService(appOf(c.app)) if err != nil { return nil } db, err := database(ctx, c.app) if err != nil { return nil } return svc.Locales(ctx, db) } // FilterOptions serves the choices of the locale and origin filters. func (c autolinksAdminController) FilterOptions(scope string) []pact.Option { switch scope { case models.FilterAutolinkLocale: out := []pact.Option{{Value: models.AutolinkLocaleAll, Label: "golem15.seo::lang.autolinks.locale_all"}} for _, code := range c.locales(context.Background()) { out = append(out, pact.Option{Value: code, Label: strings.ToUpper(code)}) } return out case models.FilterAutolinkOrigin: out := make([]pact.Option, 0, len(models.AutolinkOrigins)) for _, o := range models.AutolinkOrigins { out = append(out, pact.Option{Value: o, Label: autolinkOriginLabels[o]}) } return out } return nil } // DropdownOptions serves the locale dropdown: each content locale (the // empty option is every locale). func (c autolinksAdminController) DropdownOptions(field string) []pact.Option { if field != "locale" { return nil } codes := c.locales(context.Background()) out := make([]pact.Option, 0, len(codes)) for _, code := range codes { out = append(out, pact.Option{Value: code, Label: strings.ToUpper(code)}) } return out } // ListExtendRecords shows the locale (all languages when empty) and the // origin of the list's rows as their labels in the request locale. The // rows are list values only and never saved. func (c autolinksAdminController) ListExtendRecords(ctx context.Context, _ *gorm.DB, records []any) error { tr := translator(c.app) for _, record := range records { a, ok := record.(*models.Autolink) if !ok || a == nil { continue } if a.Locale == "" { a.Locale = Phrase(ctx, tr, "golem15.seo::lang.autolinks.locale_all", nil) } else { a.Locale = strings.ToUpper(a.Locale) } if key, ok := autolinkOriginLabels[a.Origin]; ok { a.Origin = Phrase(ctx, tr, key, nil) } } return nil } // AdminActions is the toolbar's Add focus keyphrases. func (c autolinksAdminController) AdminActions() []pact.AdminAction { return []pact.AdminAction{{ Name: ActionFromKeyphrases, Label: "golem15.seo::lang.autolinks.from_keyphrases", Permissions: []string{PermissionManage}, Run: func(ctx context.Context, _ pact.AdminActionInput) (pact.AdminActionResult, error) { svc, err := classes.NewService(appOf(c.app)) if err != nil { return pact.AdminActionResult{}, err } db, err := database(ctx, c.app) if err != nil { return pact.AdminActionResult{}, err } added := 0 err = lagoon.Transaction(ctx, db, func(ctx context.Context, tx *gorm.DB) error { var err error added, err = svc.FromKeyphrases(classes.WithTx(ctx, tx), tx) return err }) if err != nil { return pact.AdminActionResult{}, err } return pact.AdminActionResult{Message: KeyphrasesMessage(ctx, c.app, added)}, nil }, }} } // KeyphrasesMessage is the Add focus keyphrases toast: the count added, // or that there was nothing to add. func KeyphrasesMessage(ctx context.Context, app func() *backpack.App, added int) string { tr := translator(app) if added == 0 { return Phrase(ctx, tr, "golem15.seo::lang.autolinks.keyphrases_none", nil) } if tr == nil { return "golem15.seo::lang.autolinks.keyphrases_added" } return tr.Choice(ctx, "golem15.seo::lang.autolinks.keyphrases_added", added, nil) } var ( _ pact.AdminController = autolinksAdminController{} _ pact.AdminPermissioned = autolinksAdminController{} _ pact.AdminRecordSource = autolinksAdminController{} _ pact.FormBeforeCreate = autolinksAdminController{} _ pact.FormBeforeUpdate = autolinksAdminController{} _ pact.FormAfterDelete = autolinksAdminController{} _ pact.ListRowStates = autolinksAdminController{} _ pact.FilterOptions = autolinksAdminController{} _ pact.ListExtendRecords = autolinksAdminController{} _ pact.DropdownOptionsProvider = autolinksAdminController{} _ pact.HasAdminActions = autolinksAdminController{} )