feat: batches with dry run, undo, and CSV import and export
- POST /_seo/api/v1/batch runs changes in one transaction: dry_run answers the diff and the conflicts (loops, chains, noindex on a published record, canonicals to redirected or noindex addresses) and rolls back; a refused change refuses the batch (D-12) - undo of a change or a whole batch from History and through MCP, with the undo_stale, undo_loop and undo_gone refusals (D-13) - CSV export and import of redirects, rules and records by URL: BOM, comma or semicolon, formula cells escaped, a dry-run preview, all or nothing with via import; the screen's apply token is HMAC-SHA256 over the file and a fingerprint of the data (transfer.stale) - Import and export singleton with the seo_transfer widget action, History record actions undo and undo_batch, navigation entry - MCP tools undo_change, undo_batch, apply_batch, export_csv, import_csv - TestBatch, TestUndo, TestCSVTransfer, TestLoopRefused mcp and import
This commit is contained in:
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.golem15.com/golem15/sm-seo-plugin/classes"
|
||||
"git.golem15.com/golem15/sm-seo-plugin/models"
|
||||
@@ -51,47 +50,6 @@ type UpdateRedirectInput struct {
|
||||
Note *string `json:"note,omitempty" jsonschema:"an internal note"`
|
||||
}
|
||||
|
||||
// RedirectBody is the decoded body of a redirect write; the batch and the
|
||||
// import use it too.
|
||||
type RedirectBody struct {
|
||||
Source *string `json:"source,omitempty"`
|
||||
Target *string `json:"target,omitempty"`
|
||||
Status *int `json:"status,omitempty"`
|
||||
Match *string `json:"match,omitempty"`
|
||||
Query *string `json:"query,omitempty"`
|
||||
IgnoreCase *bool `json:"ignore_case,omitempty"`
|
||||
TrailingSlash *bool `json:"trailing_slash,omitempty"`
|
||||
Note *string `json:"note,omitempty"`
|
||||
}
|
||||
|
||||
// Apply writes the fields sent onto r.
|
||||
func (b RedirectBody) Apply(r *models.Redirect) {
|
||||
if b.Source != nil {
|
||||
r.Source = strings.TrimSpace(*b.Source)
|
||||
}
|
||||
if b.Target != nil {
|
||||
r.Target = strings.TrimSpace(*b.Target)
|
||||
}
|
||||
if b.Status != nil {
|
||||
r.Status = models.RedirectStatus(*b.Status)
|
||||
}
|
||||
if b.Match != nil {
|
||||
r.Match = strings.TrimSpace(*b.Match)
|
||||
}
|
||||
if b.Query != nil {
|
||||
r.Query = strings.TrimSpace(*b.Query)
|
||||
}
|
||||
if b.IgnoreCase != nil {
|
||||
r.IgnoreCase = *b.IgnoreCase
|
||||
}
|
||||
if b.TrailingSlash != nil {
|
||||
r.TrailingSlash = *b.TrailingSlash
|
||||
}
|
||||
if b.Note != nil {
|
||||
r.Note = strings.TrimSpace(*b.Note)
|
||||
}
|
||||
}
|
||||
|
||||
// RedirectJSON is a redirect in an answer.
|
||||
func RedirectJSON(r models.Redirect) map[string]any {
|
||||
out := classes.RedirectValues(r)
|
||||
@@ -175,7 +133,7 @@ func ShowRedirect(service func() *classes.Service) http.HandlerFunc {
|
||||
// validation_failed for invalid fields. The origin is mcp through MCP.
|
||||
func CreateRedirect(service func() *classes.Service) http.HandlerFunc {
|
||||
return manage(service, func(ctx context.Context, svc *classes.Service, r *http.Request) (int, any, error) {
|
||||
var body RedirectBody
|
||||
var body classes.RedirectFields
|
||||
if err := decode(r, maxBody, &body); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
@@ -203,7 +161,7 @@ func UpdateRedirect(service func() *classes.Service) http.HandlerFunc {
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
var body RedirectBody
|
||||
var body classes.RedirectFields
|
||||
if err := decode(r, maxBody, &body); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user