3
0

Add support for page config

This commit is contained in:
Denis Arh
2022-02-10 13:55:54 +01:00
parent 699e94bf69
commit b478b37775
11 changed files with 239 additions and 10 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}
}
}
{

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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 }

View File

@@ -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,

View File

@@ -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")
}

View File

@@ -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),

View File

@@ -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()