diff --git a/automation/automation/http_request_handler.gen.go b/automation/automation/http_request_handler.gen.go index 0a0635a49..50b7797ef 100644 --- a/automation/automation/http_request_handler.gen.go +++ b/automation/automation/http_request_handler.gen.go @@ -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 diff --git a/compose/rest.yaml b/compose/rest.yaml index 592cc860e..f22afe7d3 100644 --- a/compose/rest.yaml +++ b/compose/rest.yaml @@ -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 diff --git a/compose/rest/page.go b/compose/rest/page.go index 7fee4efdd..891f7c3f7 100644 --- a/compose/rest/page.go +++ b/compose/rest/page.go @@ -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 } diff --git a/compose/rest/request/page.go b/compose/rest/request/page.go index 56c31f10b..b54234811 100644 --- a/compose/rest/request/page.go +++ b/compose/rest/request/page.go @@ -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 + } + } } { diff --git a/compose/service/page.go b/compose/service/page.go index 59f03a3a2..a1622fddd 100644 --- a/compose/service/page.go +++ b/compose/service/page.go @@ -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 diff --git a/compose/types/page.go b/compose/types/page.go index a4adaa889..acc7c5918 100644 --- a/compose/types/page.go +++ b/compose/types/page.go @@ -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://". + // 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) +} diff --git a/store/compose_pages.yaml b/store/compose_pages.yaml index 221fc50b4..7c863d6e0 100644 --- a/store/compose_pages.yaml +++ b/store/compose_pages.yaml @@ -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 } diff --git a/store/rdbms/compose_pages.gen.go b/store/rdbms/compose_pages.gen.go index fa484d575..8c96aa943 100644 --- a/store/rdbms/compose_pages.gen.go +++ b/store/rdbms/compose_pages.gen.go @@ -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, diff --git a/store/rdbms/generic_upgrades.go b/store/rdbms/generic_upgrades.go index e16cab3ef..bd12a131d 100644 --- a/store/rdbms/generic_upgrades.go +++ b/store/rdbms/generic_upgrades.go @@ -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") } diff --git a/store/rdbms/rdbms_schema.go b/store/rdbms/rdbms_schema.go index d8dda49d4..243558af4 100644 --- a/store/rdbms/rdbms_schema.go +++ b/store/rdbms/rdbms_schema.go @@ -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), diff --git a/tests/compose/page_test.go b/tests/compose/page_test.go index ee115faf2..168beb567 100644 --- a/tests/compose/page_test.go +++ b/tests/compose/page_test.go @@ -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()