feat(15-02): register journal export/import commands and Posts toolbar

JSON CLI matching PHP ExportPosts/ImportPosts ships as HasCommands, and Posts toolbar actions reuse that logic behind access_import_export.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jakub Zych
2026-10-06 18:47:53 +02:00
parent ab06569641
commit 4ff98f7de8
9 changed files with 770 additions and 5 deletions

View File

@@ -28,6 +28,7 @@ func init() {
var _ pact.FormBeforeCreate = postsAdminController{}
var _ pact.FormBeforeUpdate = postsAdminController{}
var _ pact.FormBeforeDelete = postsAdminController{}
var _ pact.HasAdminActions = postsAdminController{}
var _ cabana.FieldRelationProvider = postsAdminController{}
var _ pact.AdminController = categoriesAdminController{}

View File

@@ -3,9 +3,11 @@ package controllers
import (
"context"
"errors"
"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"
"git.golem15.com/golem15/summercms/modules/bouncer"
@@ -24,6 +26,9 @@ const (
PermissionAccessOtherPosts = "golem15.journal.access_other_posts"
// PermissionAccessPublish is required to persist published=true or published_at.
PermissionAccessPublish = "golem15.journal.access_publish"
// PermissionAccessImportExport gates journal:export-posts / import-posts
// toolbar actions (D-13).
PermissionAccessImportExport = "golem15.journal.access_import_export"
)
type postsAdminController struct {
@@ -215,6 +220,69 @@ func preparePostWrite(ctx context.Context, post *models.Post) error {
return nil
}
func (c postsAdminController) AdminActions() []pact.AdminAction {
return []pact.AdminAction{
{
Name: "exportPosts",
Label: "golem15.journal::lang.posts.export_post",
Permissions: []string{PermissionAccessImportExport},
Run: c.runExportPosts,
},
{
Name: "importPosts",
Label: "golem15.journal::lang.posts.import_post",
Permissions: []string{PermissionAccessImportExport},
Run: c.runImportPosts,
},
}
}
func (c postsAdminController) runExportPosts(ctx context.Context, in pact.AdminActionInput) (pact.AdminActionResult, error) {
path, dry, _ := console.IOPayloadPath(in.Payload, "journal-export")
n, attachments, err := console.ExportToDir(ctx, c.db(ctx), path, dry, nil)
if err != nil {
return pact.AdminActionResult{}, err
}
msg := fmt.Sprintf("Exported %d posts to: %s", n, path)
if dry {
msg = fmt.Sprintf("Would export %d posts to: %s", n, path)
}
if attachments {
msg += " File attachments are not included."
}
return pact.AdminActionResult{
Message: msg,
Data: map[string]any{"count": n, "path": path, "dry_run": dry},
}, nil
}
func (c postsAdminController) runImportPosts(ctx context.Context, in pact.AdminActionInput) (pact.AdminActionResult, error) {
path, dry, force := console.IOPayloadPath(in.Payload, "journal-export")
stats, err := console.ImportFromDir(ctx, c.db(ctx), path, force, dry, nil, func(post *models.Post) error {
return refuseUnpublishedWrite(ctx, post)
})
if err != nil {
return pact.AdminActionResult{}, err
}
msg := fmt.Sprintf("Results: %d created, %d updated, %d skipped, %d failed",
stats.Created, stats.Updated, stats.Skipped, stats.Failed)
if dry {
msg = "Dry run complete (no changes made). " + msg
}
return pact.AdminActionResult{
Message: msg,
Data: map[string]any{
"created": stats.Created,
"updated": stats.Updated,
"skipped": stats.Skipped,
"failed": stats.Failed,
"path": path,
"dry_run": dry,
"force": force,
},
}, nil
}
func refuseUnpublishedWrite(ctx context.Context, post *models.Post) error {
if !post.Published && post.PublishedAt == nil {
return nil

View File

@@ -14,7 +14,7 @@ defaultSort:
direction: desc
toolbar:
buttons: [create, delete]
buttons: [create, delete, exportPosts, importPosts]
search:
prompt: backend::lang.list.search_prompt