From e79b51360840ccced600099ddcfdafb9dfb96a87 Mon Sep 17 00:00:00 2001 From: Emmy Leke Date: Wed, 17 Jan 2024 14:04:47 +0100 Subject: [PATCH 01/14] Add ability to configure columns in metric and chart blocks --- .../Admin/Module/Records/ColumnPicker.vue | 32 ++++++-- .../src/components/PageBlocks/ChartBase.vue | 2 + .../PageBlocks/ChartConfigurator.vue | 78 +++++++++++++++---- .../src/components/PageBlocks/MetricBase.vue | 2 + .../PageBlocks/MetricConfigurator/index.vue | 62 +++++++++++---- lib/js/src/compose/types/page-block/chart.ts | 18 ++++- lib/js/src/compose/types/page-block/metric.ts | 9 ++- .../compose/types/page-block/record-list.ts | 2 +- locale/en/corteza-webapp-compose/block.yaml | 6 +- 9 files changed, 164 insertions(+), 47 deletions(-) diff --git a/client/web/compose/src/components/Admin/Module/Records/ColumnPicker.vue b/client/web/compose/src/components/Admin/Module/Records/ColumnPicker.vue index 95b967e56f..111d4932ba 100644 --- a/client/web/compose/src/components/Admin/Module/Records/ColumnPicker.vue +++ b/client/web/compose/src/components/Admin/Module/Records/ColumnPicker.vue @@ -1,12 +1,15 @@ + diff --git a/client/web/workflow/src/components/ExpressionTable.vue b/client/web/workflow/src/components/ExpressionTable.vue index c09893154f..87db633526 100644 --- a/client/web/workflow/src/components/ExpressionTable.vue +++ b/client/web/workflow/src/components/ExpressionTable.vue @@ -97,8 +97,8 @@ - diff --git a/client/web/compose/src/components/PageBlocks/Configurator.vue b/client/web/compose/src/components/PageBlocks/Configurator.vue index 0dd9183226..f0d7919833 100644 --- a/client/web/compose/src/components/PageBlocks/Configurator.vue +++ b/client/web/compose/src/components/PageBlocks/Configurator.vue @@ -116,6 +116,17 @@ {{ $t('general.border.show') }} + + + {{ $t('general.globalBlock.label') }} + diff --git a/client/web/compose/src/store/namespace.js b/client/web/compose/src/store/namespace.js index d95f44d9f0..18a30ba5c2 100644 --- a/client/web/compose/src/store/namespace.js +++ b/client/web/compose/src/store/namespace.js @@ -37,6 +37,16 @@ export default function (ComposeAPI) { set (state) { return state.set }, + + getNamespaceBlocksByID (state, { getByID }) { + return (ID) => ((getByID(ID) || {}).blocks || []).map((b) => { + const ns = getByID(ID) + const block = compose.PageBlockMaker(b) + block.blockID = `${ns.namespaceID}-${b.blockID}` + + return block + }) + }, }, actions: { diff --git a/client/web/compose/src/views/Admin/Modules/List.vue b/client/web/compose/src/views/Admin/Modules/List.vue index 09659ca09b..cab535673a 100644 --- a/client/web/compose/src/views/Admin/Modules/List.vue +++ b/client/web/compose/src/views/Admin/Modules/List.vue @@ -1,4 +1,4 @@ -" @@ -346,6 +361,7 @@ diff --git a/client/web/compose/src/views/Public/Pages/View.vue b/client/web/compose/src/views/Public/Pages/View.vue index b265a34117..eec70f4b26 100644 --- a/client/web/compose/src/views/Public/Pages/View.vue +++ b/client/web/compose/src/views/Public/Pages/View.vue @@ -88,8 +88,9 @@ import Grid from 'corteza-webapp-compose/src/components/Public/Page/Grid' import RecordModal from 'corteza-webapp-compose/src/components/Public/Record/Modal' import MagnificationModal from 'corteza-webapp-compose/src/components/Public/Page/Block/Modal' import PageTranslator from 'corteza-webapp-compose/src/components/Admin/Page/PageTranslator' -import { NoID } from '@cortezaproject/corteza-js' import page from 'corteza-webapp-compose/src/mixins/page' +import { NoID } from '@cortezaproject/corteza-js' +import { fetchID } from 'corteza-webapp-compose/src/lib/block' export default { i18nOptions: { @@ -137,6 +138,8 @@ export default { computed: { ...mapGetters({ recordPaginationUsable: 'ui/recordPaginationUsable', + getPageLayouts: 'pageLayout/getByPageID', + namespaces: 'namespace/set', }), module () { @@ -211,11 +214,101 @@ export default { this.$root.$on('refetch-records', this.refetchRecords) }, + evaluateLayoutExpressions () { + const expressions = {} + const variables = { + screen: { + width: window.innerWidth, + height: window.innerHeight, + userAgent: navigator.userAgent, + breakpoint: this.getBreakpoint(), // This is from a global mixin uiHelpers + }, + user: this.$auth.user, + oldLayout: this.layout, + layout: undefined, + } + + this.layouts.forEach(layout => { + const { config = {} } = layout + if (!config.visibility.expression) return + + variables.layout = layout + + expressions[layout.pageLayoutID] = config.visibility.expression + }) + + return this.$SystemAPI.expressionEvaluate({ variables, expressions }).catch(e => { + this.toastErrorHandler(this.$t('notification:evaluate.failed'))(e) + Object.keys(expressions).forEach(key => (expressions[key] = false)) + + return expressions + }) + }, + + async determineLayout () { + // Clear stored records so they can be refetched with latest values + this.clearRecordSet() + this.layouts = this.getPageLayouts(this.page.pageID) + + let expressions = {} + + // Only evaluate if one of the layouts has an expressions variable + if (this.layouts.some(({ config = {} }) => config.visibility.expression)) { + this.pageTitle = this.page.title + expressions = await this.evaluateLayoutExpressions() + } + + // Check layouts for expressions/roles and find the first one that fits + this.layout = this.layouts.find(({ pageLayoutID, config = {} }) => { + const { expression, roles = [] } = config.visibility + + if (expression && !expressions[pageLayoutID]) return false + + if (!roles.length) return true + + return this.$auth.user.roles.some(roleID => roles.includes(roleID)) + }) + + if (!this.layout) { + this.toastWarning(this.$t('notification:page.page-layout.notFound.view')) + return this.$router.go(-1) + } + + const { handle, meta = {} } = this.layout || {} + const title = meta.title || this.page.title + this.pageTitle = title || handle || this.$t('navigation:noPageTitle') + document.title = [title, this.namespace.name, this.$t('general:label.app-name.public')].filter(v => v).join(' | ') + + this.blocks = (this.layout || {}).blocks.map(({ blockID, meta, xywh }) => { + const block = this.fetchBlockData({ + blockID, + meta, + }) + + if (block) { + block.xywh = xywh + return block + } + }).filter(b => b) + }, + refetchRecords () { // If on a record page, let it take care of events else just refetch non record-blocks (that use records) this.$root.$emit(this.page.moduleID !== NoID ? 'refetch-record-blocks' : `refetch-non-record-blocks:${this.page.pageID}`) }, + fetchBlockData ({ blockID, meta = {} }) { + blockID = fetchID({ blockID, meta }) + + if (meta.namespaceID) { + const { blocks = [] } = this.namespaces.find((n) => n.namespaceID === this.namespace.namespaceID) || {} + + return blocks.find((b) => fetchID(b) === blockID) + } + + return this.page.blocks.find((b) => fetchID(b) === blockID) + }, + setDefaultValues () { this.layouts = [] this.layout = undefined diff --git a/lib/js/src/api-clients/compose.ts b/lib/js/src/api-clients/compose.ts index 30377764cf..4363948116 100644 --- a/lib/js/src/api-clients/compose.ts +++ b/lib/js/src/api-clients/compose.ts @@ -228,6 +228,7 @@ export default class Compose { slug, enabled, meta, + blocks, labels, updatedAt, } = (a as KV) || {} @@ -252,6 +253,7 @@ export default class Compose { slug, enabled, meta, + blocks, labels, updatedAt, } diff --git a/lib/js/src/compose/types/namespace.ts b/lib/js/src/compose/types/namespace.ts index 0f5d4dc053..d30b8c2512 100644 --- a/lib/js/src/compose/types/namespace.ts +++ b/lib/js/src/compose/types/namespace.ts @@ -1,5 +1,6 @@ import { Apply, CortezaID, ISO8601Date, NoID } from '../../cast' import { IsOf } from '../../guards' +import { PageBlock, PageBlockMaker } from './page-block' interface MetaAdminRecordList { columns: string[]; @@ -22,6 +23,7 @@ interface Meta { interface PartialNamespace extends Partial> { meta?: Partial; + blocks?: Array, createdAt?: string|number|Date; updatedAt?: string|number|Date; deletedAt?: string|number|Date; @@ -38,6 +40,8 @@ export class Namespace { public meta: object = {} + public blocks: Array = [] + public createdAt?: Date = undefined public updatedAt?: Date = undefined public deletedAt?: Date = undefined @@ -75,6 +79,10 @@ export class Namespace { this.labels = { ...n.labels } } + if (n.blocks) { + this.blocks = n.blocks ? n.blocks.filter(b => b.kind).map(block => PageBlockMaker(block)) : [] + } + Apply(this, n, ISO8601Date, 'createdAt', 'updatedAt', 'deletedAt') Apply(this, n, Boolean, 'canCreateChart', diff --git a/lib/js/src/compose/types/page-block/base.ts b/lib/js/src/compose/types/page-block/base.ts index 38627ec892..d23dbcfee2 100644 --- a/lib/js/src/compose/types/page-block/base.ts +++ b/lib/js/src/compose/types/page-block/base.ts @@ -30,23 +30,25 @@ interface PageBlockMeta { hidden?: boolean; tempID?: string; visibility: Visibility; + // `namespaceID` is used to identify what namespace the block belongs too and also if the block is a global block on the namespace + namespaceID?: string; } -export type PageBlockInput = PageBlock | Partial +export type PageBlockInput = PageBlock | Partial; const defaultXYWH = [0, 0, 20, 15] export class PageBlock { // blockID is auto generated by the server in order to support resource translations - public blockID = NoID; + public blockID = NoID public kind = '' - public title = ''; - public description = ''; + public title = '' + public description = '' - public xywh: number[] = defaultXYWH + public xywh: number[] = defaultXYWH; - public options = {} + public options = {}; public meta: PageBlockMeta = { hidden: false, @@ -55,7 +57,8 @@ export class PageBlock { expression: '', roles: [], }, - } + namespaceID: undefined, + }; public style: PageBlockStyle = { variants: { @@ -67,7 +70,7 @@ export class PageBlock { border: { enabled: false, }, - } + }; constructor (i?: PageBlockInput) { this.apply(i) diff --git a/lib/js/src/compose/types/page-block/chart.ts b/lib/js/src/compose/types/page-block/chart.ts index c0d6716b9d..764c858e12 100644 --- a/lib/js/src/compose/types/page-block/chart.ts +++ b/lib/js/src/compose/types/page-block/chart.ts @@ -1,5 +1,5 @@ import { PageBlock, PageBlockInput, Registry } from './base' -import { Apply,NoID } from '../../../cast' +import { Apply, NoID } from '../../../cast' import { Options as PageBlockRecordListOptions } from './record-list' import { cloneDeep, merge } from 'lodash' @@ -30,7 +30,7 @@ const defaults: Readonly = Object.freeze({ recordListOptions: { fields: [], }, - } + }, }) export class PageBlockChart extends PageBlock { @@ -57,7 +57,7 @@ export class PageBlockChart extends PageBlock { } } - resetDrillDown(): void { + resetDrillDown (): void { this.options.drillDown = cloneDeep(defaults.drillDown) } } diff --git a/locale/en/corteza-webapp-compose/block.yaml b/locale/en/corteza-webapp-compose/block.yaml index 367db1c653..2947df7a5a 100644 --- a/locale/en/corteza-webapp-compose/block.yaml +++ b/locale/en/corteza-webapp-compose/block.yaml @@ -5,6 +5,8 @@ selector: clone: ref: Clone with references noRef: Clone without references + selectableGlobalBlocks: + placeholder: Select global block from other pages automation: addPlaceholderLabel: Add placeholder (dummy button) availableScriptsAndWorkflow: Available scripts and workflows ({{count}}) @@ -194,6 +196,8 @@ general: tooltip: dragAndDrop: Drag and drop to change order translations: Field translations + globalBlock: + label: Global block iframe: label: IFrame pickURLField: Pick an URL field diff --git a/locale/en/corteza-webapp-compose/page.yaml b/locale/en/corteza-webapp-compose/page.yaml index cfdb1cf3e1..0404c9543c 100644 --- a/locale/en/corteza-webapp-compose/page.yaml +++ b/locale/en/corteza-webapp-compose/page.yaml @@ -70,6 +70,7 @@ block: sample: Sample title: Add new block invalid-handle-characters: Should be at least 2 characters long. Can contain only letters, numbers, underscores and dots. Must end with letter or number + referencedGlobalBlock: This block may be used in other pages changeBlock: Change existing block referencedBlock: This block is used in other layouts copyOf: "Copy of {{title}}" From 57e6a7f78c03ba515ba2278009535e226b4ade6e Mon Sep 17 00:00:00 2001 From: Kelani Tolulope Date: Mon, 15 Apr 2024 13:22:36 +0100 Subject: [PATCH 13/14] apply reivew changes --- .../components/PageBlocks/Configurator.vue | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/client/web/compose/src/components/PageBlocks/Configurator.vue b/client/web/compose/src/components/PageBlocks/Configurator.vue index f0d7919833..bc4b59506a 100644 --- a/client/web/compose/src/components/PageBlocks/Configurator.vue +++ b/client/web/compose/src/components/PageBlocks/Configurator.vue @@ -115,18 +115,18 @@ > {{ $t('general.border.show') }} - - - {{ $t('general.globalBlock.label') }} - + + {{ $t('general.globalBlock.label') }} + + Date: Thu, 6 Jun 2024 17:49:08 +0300 Subject: [PATCH 14/14] Cleanup page Block encoder --- server/compose/envoy/yaml_encode.go | 230 ++++++++++++++-------------- 1 file changed, 118 insertions(+), 112 deletions(-) diff --git a/server/compose/envoy/yaml_encode.go b/server/compose/envoy/yaml_encode.go index d114d4de48..19d6d2c88c 100644 --- a/server/compose/envoy/yaml_encode.go +++ b/server/compose/envoy/yaml_encode.go @@ -1,13 +1,13 @@ package envoy import ( - "context" - "fmt" - "github.com/cortezaproject/corteza/server/compose/types" - "github.com/cortezaproject/corteza/server/pkg/envoyx" - "github.com/cortezaproject/corteza/server/pkg/y7s" - "github.com/modern-go/reflect2" - "gopkg.in/yaml.v3" + "context" + "fmt" + "github.com/cortezaproject/corteza/server/compose/types" + "github.com/cortezaproject/corteza/server/pkg/envoyx" + "github.com/cortezaproject/corteza/server/pkg/y7s" + "github.com/modern-go/reflect2" + "gopkg.in/yaml.v3" ) func (e YamlEncoder) encode(ctx context.Context, base *yaml.Node, p envoyx.EncodeParams, rt string, nodes envoyx.NodeSet, tt envoyx.Traverser) (out *yaml.Node, err error) { @@ -92,14 +92,17 @@ func (e YamlEncoder) encodePageBlocksC(ctx context.Context, p envoyx.EncodeParam out, _ := y7s.MakeSeq() for i, b := range pg.Blocks { - aux, err = e.encodePageBlockC(ctx, p, tt, n, i, b.Kind, b.Options) + options, err := e.encodePageBlockC(ctx, p, tt, n, i, b.Kind, b.Options) if err != nil { - return + return nil, err } + b.Options = options + aux = b + out, err = y7s.AddSeq(out, aux) if err != nil { - return + return nil, err } } @@ -107,122 +110,125 @@ func (e YamlEncoder) encodePageBlocksC(ctx context.Context, p envoyx.EncodeParam } func (e YamlEncoder) encodeNamespaceBlocksC(ctx context.Context, p envoyx.EncodeParams, tt envoyx.Traverser, n *envoyx.Node, ns *types.Namespace, bb types.GlobalBlocks) (_ any, err error) { - var aux any + var aux any out, _ := y7s.MakeSeq() - for i, gB := range ns.Blocks { - aux, err = e.encodePageBlockC(ctx, p, tt, n, i, gB.Kind, gB.Options) - if err != nil { - return - } + for i, gB := range ns.Blocks { + options, err := e.encodePageBlockC(ctx, p, tt, n, i, gB.Kind, gB.Options) + if err != nil { + return nil, err + } - out, err = y7s.AddSeq(out, aux) - if err != nil { - return - } - } + gB.Options = options + aux = gB + + out, err = y7s.AddSeq(out, aux) + if err != nil { + return nil, err + } + } return out, nil } -func (e YamlEncoder) encodePageBlockC(ctx context.Context, p envoyx.EncodeParams, tt envoyx.Traverser, n *envoyx.Node, index int, kind string, options map[string]interface{}) (_ any, err error) { - switch kind { - case "RecordList": - options = e.cleanupPageblockRecordList(options) - - modRef := n.References[fmt.Sprintf("Blocks.%d.Options.ModuleID", index)] - options["module"] = safeParentIdentifier(tt, n, modRef) - delete(options, "moduleID") - break - - case "RecordOrganizer": - modRef := n.References[fmt.Sprintf("Blocks.%d.Options.ModuleID", index)] - options["module"] = safeParentIdentifier(tt, n, modRef) - delete(options, "moduleID") - break - - case "Chart": - chrRef := n.References[fmt.Sprintf("Blocks.%d.Options.ChartID", index)] - options["chart"] = safeParentIdentifier(tt, n, chrRef) - delete(options, "chartID") - break - - case "Calendar": - ff, _ := options["feeds"].([]interface{}) - for i, f := range ff { - feed, _ := f.(map[string]interface{}) - fOpts, _ := (feed["options"]).(map[string]interface{}) - - modRef := n.References[fmt.Sprintf("Blocks.%d.Options.feeds.%d.ModuleID", index, i)] - fOpts["module"] = safeParentIdentifier(tt, n, modRef) - delete(fOpts, "moduleID") - } - break - - case "Automation": - bb, _ := options["buttons"].([]interface{}) - for i, b := range bb { - button, _ := b.(map[string]interface{}) - if _, has := button["workflowID"]; !has { - continue - } - - wfRef := n.References[fmt.Sprintf("Blocks.%d.Options.buttons.%d.WorkflowID", index, i)] - button["workflow"] = safeParentIdentifier(tt, n, wfRef) - delete(button, "workflowID") - i++ - } - break - - case "Metric": - mm, _ := options["metrics"].([]interface{}) - for i, m := range mm { - modRef := n.References[fmt.Sprintf("Blocks.%d.Options.metrics.%d.ModuleID", index, i)] - - mops, _ := m.(map[string]interface{}) - mops["module"] = safeParentIdentifier(tt, n, modRef) - delete(mops, "moduleID") - } - break - - case "Comment": - modRef := n.References[fmt.Sprintf("Blocks.%d.Options.ModuleID", index)] - options["module"] = safeParentIdentifier(tt, n, modRef) - delete(options, "moduleID") - break - - case "Progress": - err = e.encodeProgressPageblockVal("minValue", index, n, tt, options) - if err != nil { - return - } - - err = e.encodeProgressPageblockVal("maxValue", index, n, tt, options) - if err != nil { - return - } - - err = e.encodeProgressPageblockVal("value", index, n, tt, options) - if err != nil { - return - } - break - } - - return +func (e YamlEncoder) encodePageBlockC(ctx context.Context, p envoyx.EncodeParams, tt envoyx.Traverser, n *envoyx.Node, index int, kind string, options map[string]interface{}) (opts map[string]interface{}, err error) { + switch kind { + case "RecordList": + options = e.cleanupPageblockRecordList(options) + + modRef := n.References[fmt.Sprintf("Blocks.%d.Options.ModuleID", index)] + options["module"] = safeParentIdentifier(tt, n, modRef) + delete(options, "moduleID") + break + + case "RecordOrganizer": + modRef := n.References[fmt.Sprintf("Blocks.%d.Options.ModuleID", index)] + options["module"] = safeParentIdentifier(tt, n, modRef) + delete(options, "moduleID") + break + + case "Chart": + chrRef := n.References[fmt.Sprintf("Blocks.%d.Options.ChartID", index)] + options["chart"] = safeParentIdentifier(tt, n, chrRef) + delete(options, "chartID") + break + + case "Calendar": + ff, _ := options["feeds"].([]interface{}) + for i, f := range ff { + feed, _ := f.(map[string]interface{}) + fOpts, _ := (feed["options"]).(map[string]interface{}) + + modRef := n.References[fmt.Sprintf("Blocks.%d.Options.feeds.%d.ModuleID", index, i)] + fOpts["module"] = safeParentIdentifier(tt, n, modRef) + delete(fOpts, "moduleID") + } + break + + case "Automation": + bb, _ := options["buttons"].([]interface{}) + for i, b := range bb { + button, _ := b.(map[string]interface{}) + if _, has := button["workflowID"]; !has { + continue + } + + wfRef := n.References[fmt.Sprintf("Blocks.%d.Options.buttons.%d.WorkflowID", index, i)] + button["workflow"] = safeParentIdentifier(tt, n, wfRef) + delete(button, "workflowID") + i++ + } + break + + case "Metric": + mm, _ := options["metrics"].([]interface{}) + for i, m := range mm { + modRef := n.References[fmt.Sprintf("Blocks.%d.Options.metrics.%d.ModuleID", index, i)] + + mops, _ := m.(map[string]interface{}) + mops["module"] = safeParentIdentifier(tt, n, modRef) + delete(mops, "moduleID") + } + break + + case "Comment": + modRef := n.References[fmt.Sprintf("Blocks.%d.Options.ModuleID", index)] + options["module"] = safeParentIdentifier(tt, n, modRef) + delete(options, "moduleID") + break + + case "Progress": + options, err = e.encodeProgressPageblockVal("minValue", index, n, tt, options) + if err != nil { + return + } + + options, err = e.encodeProgressPageblockVal("maxValue", index, n, tt, options) + if err != nil { + return + } + + options, err = e.encodeProgressPageblockVal("value", index, n, tt, options) + if err != nil { + return + } + break + } + + return options, nil } -func (e YamlEncoder) encodeProgressPageblockVal(k string, index int, n *envoyx.Node, tt envoyx.Traverser, options map[string]interface{}) (err error) { +func (e YamlEncoder) encodeProgressPageblockVal(k string, index int, n *envoyx.Node, tt envoyx.Traverser, options map[string]interface{}) (opts map[string]interface{}, err error) { if reflect2.IsNil(options[k]) { return } - modRef := n.References[fmt.Sprintf("Blocks.%d.Options.%s.ModuleID", index, k)] - opt := options[k].(map[string]any) - opt["moduleID"] = safeParentIdentifier(tt, n, modRef) - delete(opt, "moduleID") + modRef := n.References[fmt.Sprintf("Blocks.%d.Options.%s.ModuleID", index, k)] + opt := options[k].(map[string]any) + opt["moduleID"] = safeParentIdentifier(tt, n, modRef) + delete(opt, "moduleID") - return + return } func (e YamlEncoder) cleanupPageblockRecordList(options map[string]interface{}) (out map[string]interface{}) {