Add support for page config
This commit is contained in:
@@ -10,14 +10,13 @@ package automation
|
||||
|
||||
import (
|
||||
"context"
|
||||
atypes "github.com/cortezaproject/corteza-server/automation/types"
|
||||
"github.com/cortezaproject/corteza-server/pkg/expr"
|
||||
"github.com/cortezaproject/corteza-server/pkg/wfexec"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
atypes "github.com/cortezaproject/corteza-server/automation/types"
|
||||
"github.com/cortezaproject/corteza-server/pkg/expr"
|
||||
"github.com/cortezaproject/corteza-server/pkg/wfexec"
|
||||
)
|
||||
|
||||
var _ wfexec.ExecResponse
|
||||
|
||||
@@ -338,6 +338,10 @@ endpoints:
|
||||
name: blocks
|
||||
required: false
|
||||
title: Blocks JSON
|
||||
- type: sqlxTypes.JSONText
|
||||
name: config
|
||||
required: false
|
||||
title: Config JSON
|
||||
- name: read
|
||||
path: "/{pageID}"
|
||||
method: GET
|
||||
@@ -400,6 +404,10 @@ endpoints:
|
||||
name: blocks
|
||||
required: false
|
||||
title: Blocks JSON
|
||||
- type: sqlxTypes.JSONText
|
||||
name: config
|
||||
required: false
|
||||
title: Config JSON
|
||||
- name: reorder
|
||||
method: POST
|
||||
title: Reorder pages
|
||||
|
||||
@@ -115,11 +115,18 @@ func (ctrl *Page) Create(ctx context.Context, r *request.PageCreate) (interface{
|
||||
}
|
||||
)
|
||||
|
||||
if len(r.Config) > 2 {
|
||||
if err = r.Config.Unmarshal(&mod.Config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(r.Blocks) > 2 {
|
||||
if err = r.Blocks.Unmarshal(&mod.Blocks); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
mod, err = ctrl.page.Create(ctx, mod)
|
||||
return ctrl.makePayload(ctx, mod, err)
|
||||
}
|
||||
@@ -158,7 +165,17 @@ func (ctrl *Page) Update(ctx context.Context, r *request.PageUpdate) (interface{
|
||||
}
|
||||
)
|
||||
|
||||
if len(r.Config) > 2 {
|
||||
// Process config if it was included in the request
|
||||
// if not, do not assume that config has been removed!
|
||||
if err = r.Config.Unmarshal(&mod.Config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(r.Blocks) > 2 {
|
||||
// Process blocks if they were included in the request
|
||||
// if not, do not assume that blocks were removed!
|
||||
if err = r.Blocks.Unmarshal(&mod.Blocks); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -11,15 +11,16 @@ package request
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/label"
|
||||
"github.com/cortezaproject/corteza-server/pkg/locale"
|
||||
"github.com/cortezaproject/corteza-server/pkg/payload"
|
||||
"github.com/go-chi/chi/v5"
|
||||
sqlxTypes "github.com/jmoiron/sqlx/types"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// dummy vars to prevent
|
||||
@@ -133,6 +134,11 @@ type (
|
||||
//
|
||||
// Blocks JSON
|
||||
Blocks sqlxTypes.JSONText
|
||||
|
||||
// Config POST parameter
|
||||
//
|
||||
// Config JSON
|
||||
Config sqlxTypes.JSONText
|
||||
}
|
||||
|
||||
PageRead struct {
|
||||
@@ -209,6 +215,11 @@ type (
|
||||
//
|
||||
// Blocks JSON
|
||||
Blocks sqlxTypes.JSONText
|
||||
|
||||
// Config POST parameter
|
||||
//
|
||||
// Config JSON
|
||||
Config sqlxTypes.JSONText
|
||||
}
|
||||
|
||||
PageReorder struct {
|
||||
@@ -474,6 +485,7 @@ func (r PageCreate) Auditable() map[string]interface{} {
|
||||
"labels": r.Labels,
|
||||
"visible": r.Visible,
|
||||
"blocks": r.Blocks,
|
||||
"config": r.Config,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,6 +539,11 @@ func (r PageCreate) GetBlocks() sqlxTypes.JSONText {
|
||||
return r.Blocks
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r PageCreate) GetConfig() sqlxTypes.JSONText {
|
||||
return r.Config
|
||||
}
|
||||
|
||||
// Fill processes request and fills internal variables
|
||||
func (r *PageCreate) Fill(req *http.Request) (err error) {
|
||||
|
||||
@@ -615,6 +632,13 @@ func (r *PageCreate) Fill(req *http.Request) (err error) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if val, ok := req.MultipartForm.Value["config"]; ok && len(val) > 0 {
|
||||
r.Config, err = payload.ParseJSONTextWithErr(val[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -692,6 +716,13 @@ func (r *PageCreate) Fill(req *http.Request) (err error) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if val, ok := req.Form["config"]; ok && len(val) > 0 {
|
||||
r.Config, err = payload.ParseJSONTextWithErr(val[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
@@ -810,6 +841,7 @@ func (r PageUpdate) Auditable() map[string]interface{} {
|
||||
"labels": r.Labels,
|
||||
"visible": r.Visible,
|
||||
"blocks": r.Blocks,
|
||||
"config": r.Config,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -868,6 +900,11 @@ func (r PageUpdate) GetBlocks() sqlxTypes.JSONText {
|
||||
return r.Blocks
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r PageUpdate) GetConfig() sqlxTypes.JSONText {
|
||||
return r.Config
|
||||
}
|
||||
|
||||
// Fill processes request and fills internal variables
|
||||
func (r *PageUpdate) Fill(req *http.Request) (err error) {
|
||||
|
||||
@@ -956,6 +993,13 @@ func (r *PageUpdate) Fill(req *http.Request) (err error) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if val, ok := req.MultipartForm.Value["config"]; ok && len(val) > 0 {
|
||||
r.Config, err = payload.ParseJSONTextWithErr(val[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1033,6 +1077,13 @@ func (r *PageUpdate) Fill(req *http.Request) (err error) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if val, ok := req.Form["config"]; ok && len(val) > 0 {
|
||||
r.Config, err = payload.ParseJSONTextWithErr(val[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -575,6 +575,11 @@ func (svc page) handleUpdate(ctx context.Context, upd *types.Page) pageUpdateHan
|
||||
changes |= pageChanged
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(res.Config, upd.Config) {
|
||||
res.Config = upd.Config
|
||||
changes |= pageChanged
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(res.Blocks, upd.Blocks) {
|
||||
res.Blocks = upd.Blocks
|
||||
changes |= pageChanged
|
||||
|
||||
@@ -25,6 +25,7 @@ type (
|
||||
|
||||
Handle string `json:"handle"`
|
||||
|
||||
Config PageConfig `json:"config"`
|
||||
Blocks PageBlocks `json:"blocks"`
|
||||
|
||||
Children PageSet `json:"children,omitempty"`
|
||||
@@ -74,6 +75,54 @@ type (
|
||||
Variants map[string]string `json:"variants,omitempty"`
|
||||
}
|
||||
|
||||
PageConfig struct {
|
||||
// How page is presented in the navigation
|
||||
NavItem struct {
|
||||
Icon *PageConfigIcon `json:"icon,omitempty"`
|
||||
} `json:"navItem"`
|
||||
|
||||
//// Example how page-config structure can evolve in the future
|
||||
//Views []struct {
|
||||
// // what kind of output is this view intended for (screen, mobile...?)
|
||||
// Output string
|
||||
//
|
||||
// // Migrated page blocks, might be replaced someday with a more complex structure
|
||||
// Blocks []PageBlock
|
||||
//}
|
||||
}
|
||||
|
||||
PageConfigIcon struct {
|
||||
// Icon types and sources
|
||||
//
|
||||
// Note that backed does not enforce or validate all src value (types due to a limited
|
||||
// awareness of capabilities and
|
||||
//
|
||||
// Type: empty or "link" (default):
|
||||
// Indicate that src will contain an absolute or relative link to an icon.
|
||||
// Can also be used for inline images (storing "base64:" prefixed string in source).
|
||||
// This type and reference is not validated by the backend.
|
||||
//
|
||||
// Type: "library"
|
||||
// Source references an icon from a library. Ref's value should be in the following
|
||||
// notation: "font-awesome://<icon-identifier>".
|
||||
// This type and source is not validated by the backend.
|
||||
//
|
||||
// Type: "svg"
|
||||
// SRC contains raw SVG document
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Other types that might be implemented in the future:
|
||||
// "attachment"
|
||||
// Reference (ID) to an existing attachment in local Corteza instance is expected
|
||||
// This type and reference must be validated by the backend.
|
||||
|
||||
Type string `json:"type,omitempty"`
|
||||
Src string `json:"src"`
|
||||
|
||||
// Any custom styling that should be applied to the icon
|
||||
Style map[string]string `json:"style,omitempty"`
|
||||
}
|
||||
|
||||
PageFilter struct {
|
||||
NamespaceID uint64 `json:"namespaceID,string"`
|
||||
ParentID uint64 `json:"parentID,string,omitempty"`
|
||||
@@ -320,3 +369,22 @@ func (set PageSet) RecursiveWalk(parent *Page, fn func(c *Page, parent *Page) er
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (bb *PageConfig) Scan(value interface{}) error {
|
||||
//lint:ignore S1034 This typecast is intentional, we need to get []byte out of a []uint8
|
||||
switch value.(type) {
|
||||
case nil:
|
||||
*bb = PageConfig{}
|
||||
case []uint8:
|
||||
b := value.([]byte)
|
||||
if err := json.Unmarshal(b, bb); err != nil {
|
||||
return errors.Wrapf(err, "cannot scan '%v' into PageConfig", string(b))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bb PageConfig) Value() (driver.Value, error) {
|
||||
return json.Marshal(bb)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ fields:
|
||||
- { field: Handle, lookupFilterPreprocessor: lower }
|
||||
- { field: Title }
|
||||
- { field: Description, type: "string" }
|
||||
- { field: Config, type: "types.PageConfig" }
|
||||
- { field: Blocks, type: "types.PageBlocks" }
|
||||
- { field: Visible, type: bool }
|
||||
- { field: Weight, type: int, sortable: true }
|
||||
|
||||
3
store/rdbms/compose_pages.gen.go
generated
3
store/rdbms/compose_pages.gen.go
generated
@@ -464,6 +464,7 @@ func (s Store) internalComposePageRowScanner(row rowScanner) (res *types.Page, e
|
||||
&res.Handle,
|
||||
&res.Title,
|
||||
&res.Description,
|
||||
&res.Config,
|
||||
&res.Blocks,
|
||||
&res.Visible,
|
||||
&res.Weight,
|
||||
@@ -516,6 +517,7 @@ func (Store) composePageColumns(aa ...string) []string {
|
||||
alias + "handle",
|
||||
alias + "title",
|
||||
alias + "description",
|
||||
alias + "config",
|
||||
alias + "blocks",
|
||||
alias + "visible",
|
||||
alias + "weight",
|
||||
@@ -554,6 +556,7 @@ func (s Store) internalComposePageEncoder(res *types.Page) store.Payload {
|
||||
"handle": res.Handle,
|
||||
"title": res.Title,
|
||||
"description": res.Description,
|
||||
"config": res.Config,
|
||||
"blocks": res.Blocks,
|
||||
"visible": res.Visible,
|
||||
"weight": res.Weight,
|
||||
|
||||
@@ -74,6 +74,10 @@ func (g genericUpgrades) Upgrade(ctx context.Context, t *ddl.Table) error {
|
||||
return g.all(ctx,
|
||||
g.AddRolesMetaField,
|
||||
)
|
||||
case "compose_page":
|
||||
return g.all(ctx,
|
||||
g.AddComposeBlockConfigField,
|
||||
)
|
||||
case "compose_module":
|
||||
return g.all(ctx,
|
||||
g.AlterComposeModuleRenameJsonToMeta,
|
||||
@@ -95,6 +99,7 @@ func (g genericUpgrades) Upgrade(ctx context.Context, t *ddl.Table) error {
|
||||
return g.all(ctx,
|
||||
g.AddScenariosField,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -274,6 +279,17 @@ func (g genericUpgrades) AddRolesMetaField(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (g genericUpgrades) AddComposeBlockConfigField(ctx context.Context) error {
|
||||
_, err := g.u.AddColumn(ctx, "compose_page", &ddl.Column{
|
||||
Name: "config",
|
||||
Type: ddl.ColumnType{Type: ddl.ColumnTypeJson},
|
||||
IsNull: false,
|
||||
DefaultValue: "'{}'",
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (g genericUpgrades) RenameReminders(ctx context.Context) error {
|
||||
return g.RenameTable(ctx, "sys_reminder", "reminders")
|
||||
}
|
||||
|
||||
@@ -517,6 +517,7 @@ func (Schema) ComposePage() *Table {
|
||||
ColumnDef("rel_namespace", ColumnTypeIdentifier),
|
||||
ColumnDef("rel_module", ColumnTypeIdentifier),
|
||||
ColumnDef("self_id", ColumnTypeIdentifier),
|
||||
ColumnDef("config", ColumnTypeJson),
|
||||
ColumnDef("blocks", ColumnTypeJson),
|
||||
ColumnDef("visible", ColumnTypeBoolean),
|
||||
ColumnDef("weight", ColumnTypeInteger),
|
||||
|
||||
@@ -30,6 +30,17 @@ func (h helper) repoMakePage(ns *types.Namespace, name string) *types.Page {
|
||||
NamespaceID: ns.ID,
|
||||
}
|
||||
|
||||
res.Blocks = types.PageBlocks{
|
||||
{BlockID: 1},
|
||||
{BlockID: 2},
|
||||
}
|
||||
|
||||
res.Config.NavItem.Icon = &types.PageConfigIcon{
|
||||
Type: "type",
|
||||
Src: "src",
|
||||
Style: map[string]string{"sty": "le"},
|
||||
}
|
||||
|
||||
h.noError(store.CreateComposePage(context.Background(), service.DefaultStore, res))
|
||||
return res
|
||||
}
|
||||
@@ -69,6 +80,8 @@ func TestPageRead(t *testing.T) {
|
||||
Assert(helpers.AssertNoErrors).
|
||||
Assert(jsonpath.Equal(`$.response.title`, m.Title)).
|
||||
Assert(jsonpath.Equal(`$.response.pageID`, fmt.Sprintf("%d", m.ID))).
|
||||
Assert(jsonpath.Equal(`$.response.config.navItem.icon.src`, `src`)).
|
||||
Assert(jsonpath.Len(`$.response.blocks`, 2)).
|
||||
End()
|
||||
}
|
||||
|
||||
@@ -153,13 +166,30 @@ func TestPageCreate(t *testing.T) {
|
||||
|
||||
ns := h.makeNamespace("some-namespace")
|
||||
|
||||
rsp := struct {
|
||||
Response *types.Page `json:"response"`
|
||||
}{}
|
||||
|
||||
h.apiInit().
|
||||
Post(fmt.Sprintf("/namespace/%d/page/", ns.ID)).
|
||||
FormData("title", "some-page").
|
||||
Header("Accept", "application/json").
|
||||
JSON(fmt.Sprintf(`{
|
||||
"title": "some-page",
|
||||
"config":{"navItem":{"icon":{"src":"my-icon"}}},
|
||||
"blocks":[{"blockID": "1"}]
|
||||
}`)).
|
||||
Expect(t).
|
||||
Status(http.StatusOK).
|
||||
Assert(helpers.AssertNoErrors).
|
||||
End()
|
||||
End().
|
||||
JSON(&rsp)
|
||||
|
||||
res := h.lookupPageByID(rsp.Response.ID)
|
||||
h.a.NotNil(res)
|
||||
h.a.Equal("some-page", res.Title)
|
||||
h.a.Len(res.Blocks, 1)
|
||||
h.a.NotNil(res.Config.NavItem.Icon)
|
||||
h.a.Equal("my-icon", res.Config.NavItem.Icon.Src)
|
||||
}
|
||||
|
||||
func TestPageUpdateForbidden(t *testing.T) {
|
||||
@@ -202,6 +232,36 @@ func TestPageUpdate(t *testing.T) {
|
||||
h.a.Equal("changed-name", res.Title)
|
||||
}
|
||||
|
||||
func TestPageUpdateWithBlocksAndConfig(t *testing.T) {
|
||||
h := newHelper(t)
|
||||
h.clearPages()
|
||||
|
||||
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "pages.search")
|
||||
ns := h.makeNamespace("some-namespace")
|
||||
res := h.repoMakePage(ns, "some-page")
|
||||
helpers.AllowMe(h, types.PageRbacResource(0, 0), "update")
|
||||
|
||||
h.apiInit().
|
||||
Post(fmt.Sprintf("/namespace/%d/page/%d", ns.ID, res.ID)).
|
||||
Header("Accept", "application/json").
|
||||
JSON(fmt.Sprintf(`{
|
||||
"title": "changed-name",
|
||||
"config":{"navItem":{"icon":{"src":"my-icon"}}},
|
||||
"blocks":[{"blockID": "1"}]
|
||||
}`)).
|
||||
Expect(t).
|
||||
Status(http.StatusOK).
|
||||
Assert(helpers.AssertNoErrors).
|
||||
End()
|
||||
|
||||
res = h.lookupPageByID(res.ID)
|
||||
h.a.NotNil(res)
|
||||
h.a.Equal("changed-name", res.Title)
|
||||
h.a.NotNil(res.Config.NavItem.Icon)
|
||||
h.a.Equal("my-icon", res.Config.NavItem.Icon.Src)
|
||||
h.a.Len(res.Blocks, 1)
|
||||
}
|
||||
|
||||
func TestPageReorder(t *testing.T) {
|
||||
h := newHelper(t)
|
||||
h.clearPages()
|
||||
|
||||
Reference in New Issue
Block a user