From 230ae91a2cd1794ef31df93ee9a83e6b8616969e Mon Sep 17 00:00:00 2001 From: Toocky Date: Sat, 28 Feb 2026 13:31:54 -0600 Subject: [PATCH 1/5] Migrated global blocks change to Current 2024.9.x Branch --- .../Admin/Page/Builder/Selector.vue | 90 ++++++- .../components/PageBlocks/Configurator.vue | 25 ++ client/web/compose/src/store/namespace.js | 10 + .../compose/src/views/Admin/Modules/List.vue | 2 +- .../compose/src/views/Admin/Pages/Builder.vue | 142 +++++++++-- lib/js/src/api-clients/compose.ts | 2 + lib/js/src/compose/types/namespace.ts | 8 + lib/js/src/compose/types/page-block/base.ts | 3 + locale/en/corteza-webapp-compose/block.yaml | 4 + locale/en/corteza-webapp-compose/page.yaml | 1 + server/compose/envoy/store_decode.go | 16 +- server/compose/envoy/yaml_decode.gen.go | 19 ++ server/compose/envoy/yaml_decode.go | 89 ++++--- server/compose/envoy/yaml_encode.gen.go | 7 +- server/compose/envoy/yaml_encode.go | 235 ++++++++++-------- server/compose/model/models.gen.go | 10 +- server/compose/namespace.cue | 12 + server/compose/rest.yaml | 4 + server/compose/rest/namespace.go | 8 + server/compose/rest/request/namespace.go | 25 ++ server/compose/service/namespace.go | 31 +++ server/compose/types/namespace.go | 26 ++ server/store/adapters/rdbms/aux_types.gen.go | 4 + server/store/adapters/rdbms/queries.gen.go | 4 + server/store/adapters/rdbms/upgrade_fixes.go | 9 + 25 files changed, 598 insertions(+), 188 deletions(-) diff --git a/client/web/compose/src/components/Admin/Page/Builder/Selector.vue b/client/web/compose/src/components/Admin/Page/Builder/Selector.vue index 5ebf7b4232..fce73f5e7d 100644 --- a/client/web/compose/src/components/Admin/Page/Builder/Selector.vue +++ b/client/web/compose/src/components/Admin/Page/Builder/Selector.vue @@ -33,20 +33,21 @@
@@ -54,9 +55,9 @@ + + + + + + + + + + + + + + + + [], }, - existingBlocks: { + existingLayoutBlocks: { + type: Array, + default: () => [], + }, + + selectableGlobalBlocks: { type: Array, default: () => [], }, @@ -111,7 +159,8 @@ export default { return { current: undefined, - selectedExistingBlock: undefined, + selectedLayoutBlock: undefined, + selectedGlobalBlock: undefined, types: [ { @@ -220,9 +269,26 @@ export default { setDefaultValues () { this.current = undefined - this.selectedExistingBlock = undefined + this.selectedLayoutBlock = undefined + this.selectedGlobalBlock = undefined this.types = [] }, + + fetchBlockData (blockID) { + if (blockID.includes('-')) { + return this.selectableGlobalBlocks.find((b) => b.blockID === blockID) + } + + return this.existingLayoutBlocks.find((b) => b.blockID === blockID) + }, + + selectBlock (block, clone = false) { + if (clone) { + this.$emit('select', this.fetchBlockData(block).clone()) + } else { + this.$emit('select', this.fetchBlockData(block)) + } + }, }, } - + \ No newline at end of file diff --git a/client/web/compose/src/components/PageBlocks/Configurator.vue b/client/web/compose/src/components/PageBlocks/Configurator.vue index 36361d3575..daacd824b3 100644 --- a/client/web/compose/src/components/PageBlocks/Configurator.vue +++ b/client/web/compose/src/components/PageBlocks/Configurator.vue @@ -184,6 +184,17 @@ > {{ $t('general.border.show') }} + + + {{ $t('general.globalBlock.label') }} + @@ -405,6 +416,8 @@ export default { options: [], }, abortableRequests: [], + initialBlockState: undefined, + initialBlockID: undefined, } }, @@ -476,6 +489,8 @@ export default { }, created () { + this.initialBlockState = this.block.meta.namespaceID + this.initialBlockID = this.block.blockID this.fetchRoles() }, @@ -519,6 +534,16 @@ export default { } this.abortableRequests = [] }, + + updateGlobalState (value) { + this.block.blockID = this.initialBlockState === value ? this.initialBlockID : NoID + + if (value) { + this.block.meta.namespaceID = value + } else { + this.block.meta.namespaceID = undefined + } + }, }, } 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 f6b79411e1..6b7d030e96 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 @@ -" @@ -449,6 +464,8 @@ export default { pages: 'page/set', getModuleByID: 'module/getByID', previousPage: 'ui/previousPage', + namespaces: 'namespace/set', + getNamespaceBlocks: 'namespace/getNamespaceBlocksByID', }), trPage: { @@ -517,10 +534,18 @@ export default { return this.hasChildren || !this.page.canDeletePage || !!this.page.deletedAt }, - selectableExistingBlocks () { + selectableExistingLayoutBlocks () { return this.page.blocks.filter(({ blockID }) => !this.usedBlocks.some(b => b.blockID === blockID)) }, + selectableNamespaceGlobalBlocks () { + const { namespaceID } = this.namespace + + return this.getNamespaceBlocks(namespaceID).filter(({ blockID }) => { + return !this.usedBlocks.some(b => b.blockID === blockID) + }) + }, + // Blocks used on page or tabbed usedBlocks () { const tabbedIDs = new Set() @@ -657,6 +682,7 @@ export default { deletePageLayout: 'pageLayout/delete', setPageHandle: 'ui/setPageHandle', setLayoutHandle: 'ui/setLayoutHandle', + updateNamespace: 'namespace/update', }), fulfilEditRequest (blockID) { @@ -939,7 +965,7 @@ export default { return Promise.all([ this.findPageByID({ ...this.page, force: true }), this.findLayoutByID({ ...this.layout }), - ]).then(([page, layout]) => { + ]).then(async ([page, layout]) => { const blocks = [ ...page.blocks.filter(({ blockID }) => { // Check if block exists in any other layout, if not delete it permanently @@ -948,11 +974,23 @@ export default { ...this.blocks, ] + const namespaceBlocks = [] + const pageBlocks = [] + + blocks.forEach(b => { + b.meta.namespaceID ? namespaceBlocks.push(b) : pageBlocks.push(b) + }) + + const updatedGlobalBlocks = await this.processGlobalBlocks(namespaceBlocks) + return this.updatePage({ namespaceID, ...page, blocks }) .then(this.updateTabbedBlockIDs) .then(async page => { const blocks = this.blocks.map(({ blockID, meta, xywh }) => { - if (blockID === NoID) { + // If the global blocks is a newly created global block + if (meta.namespaceID && blockID === NoID) { + blockID = (updatedGlobalBlocks.find(block => block.meta.tempID === meta.tempID) || {}).blockID + } else if (blockID === NoID) { blockID = (page.blocks.find(block => block.meta.tempID === meta.tempID) || {}).blockID } @@ -988,6 +1026,45 @@ export default { }).catch(this.toastErrorHandler(this.$t('notification:page.page-layout.save.failed'))) }, + async processGlobalBlocks (globalBlocks = []) { + if (!globalBlocks.length) return [] + + let { namespaceID, name, slug, enabled, meta } = this.namespace + + const newGlobalBlocks = globalBlocks + .filter(({ blockID }) => blockID === NoID) + .map((block) => { + block.meta.namespaceID = namespaceID + + return block + }) + + const namespace = this.namespaces.find((n) => n.namespaceID === this.namespace.namespaceID) + + const existingGlobalBlocks = namespace.blocks + + globalBlocks + .filter(({ blockID }) => blockID !== NoID) + .forEach((block) => { + const blockID = String(block.blockID).replace(`${namespaceID}-`, '') + const matchingBlockIndex = existingGlobalBlocks.findIndex(b => b.blockID === blockID) + + if (matchingBlockIndex > -1) { + const normalBlockID = existingGlobalBlocks[matchingBlockIndex].blockID + existingGlobalBlocks[matchingBlockIndex] = block + existingGlobalBlocks[matchingBlockIndex].blockID = normalBlockID + } + }) + + const namespaceBlocks = existingGlobalBlocks.concat(newGlobalBlocks) + + return this.updateNamespace({ blocks: namespaceBlocks, namespaceID, name, meta, slug, enabled }).then((ns) => { + this.$store.dispatch('namespace/load', { force: true }) + + return ns.blocks + }) + }, + async handleCloneLayout ({ ref = false }) { this.processing = true this.processingLayout = true @@ -1161,26 +1238,35 @@ export default { const tempBlocks = [] const { blocks = [] } = this.layout || {} + const { namespaceID } = this.namespace blocks.forEach(({ blockID, xywh, meta = {} }) => { - let block = this.page.blocks.find(b => b.blockID === blockID) - - if (block) { - block.xywh = xywh - block.meta.hidden = !!meta.hidden - tempBlocks.push(block) - - if (block.kind === 'Tabs') { - const { tabs = [] } = block.options - tabs.forEach(tab => { - if (blocks.some(b => b.blockID === tab.blockID)) return - - block = this.page.blocks.find(b => b.blockID === tab.blockID) - - if (block) { - tempBlocks.push(block) - } - }) + if (blockID) { + let block = cloneDeep(this.fetchBlockData({ + blockID, + meta, + })) + + if (block) { + block.blockID = meta.namespaceID ? `${namespaceID}-${block.blockID}` : block.blockID + + block.xywh = xywh + block.meta.hidden = !!meta.hidden + tempBlocks.push(block) + + if (block.kind === 'Tabs') { + const { tabs = [] } = block.options + tabs.forEach(tab => { + if (blocks.some(b => b.blockID === tab.blockID)) return + + // global blocks are not added to tabs, it's unnecessary + block = this.page.blocks.find(b => b.blockID === tab.blockID) + + if (block) { + tempBlocks.push(block) + } + }) + } } } }) @@ -1228,6 +1314,18 @@ export default { this.$root.$off('tab-createRequest', this.fulfilCreateRequest) this.$root.$off('tabChange', this.untabBlock) }, + + 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) + }, }, } diff --git a/lib/js/src/api-clients/compose.ts b/lib/js/src/api-clients/compose.ts index fad9162be1..c12adf1be6 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 eb925b116f..28e0946e70 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 @@ -78,6 +82,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, 'canDeleteNamespace', diff --git a/lib/js/src/compose/types/page-block/base.ts b/lib/js/src/compose/types/page-block/base.ts index b38c2f837e..3ae9871c77 100644 --- a/lib/js/src/compose/types/page-block/base.ts +++ b/lib/js/src/compose/types/page-block/base.ts @@ -32,6 +32,8 @@ interface PageBlockMeta { customID?: string; customCSSClass?: 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 @@ -59,6 +61,7 @@ export class PageBlock { expression: '', roles: [], }, + namespaceID: undefined, } public style: PageBlockStyle = { diff --git a/locale/en/corteza-webapp-compose/block.yaml b/locale/en/corteza-webapp-compose/block.yaml index 8ba4c51565..50fb979351 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}}) @@ -219,6 +221,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 4443f5d341..cff2919b31 100644 --- a/locale/en/corteza-webapp-compose/page.yaml +++ b/locale/en/corteza-webapp-compose/page.yaml @@ -79,6 +79,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}}" diff --git a/server/compose/envoy/store_decode.go b/server/compose/envoy/store_decode.go index 13c5c81e2c..b479e295f0 100644 --- a/server/compose/envoy/store_decode.go +++ b/server/compose/envoy/store_decode.go @@ -164,28 +164,28 @@ func decodePageRefs(p *types.Page) (refs map[string]envoyx.Ref) { for index, b := range p.Blocks { switch b.Kind { case "RecordList": - refs = envoyx.MergeRefs(refs, getPageBlockRecordListRefs(b, index)) + refs = envoyx.MergeRefs(refs, getPageBlockRecordListRefs(b.Options, index)) case "Automation": - refs = envoyx.MergeRefs(refs, getPageBlockAutomationRefs(b, index)) + refs = envoyx.MergeRefs(refs, getPageBlockAutomationRefs(b.Options, index)) case "RecordOrganizer": - refs = envoyx.MergeRefs(refs, getPageBlockRecordOrganizerRefs(b, index)) + refs = envoyx.MergeRefs(refs, getPageBlockRecordOrganizerRefs(b.Options, index)) case "Chart": - refs = envoyx.MergeRefs(refs, getPageBlockChartRefs(b, index)) + refs = envoyx.MergeRefs(refs, getPageBlockChartRefs(b.Options, index)) case "Calendar": - refs = envoyx.MergeRefs(refs, getPageBlockCalendarRefs(b, index)) + refs = envoyx.MergeRefs(refs, getPageBlockCalendarRefs(b.Options, index)) case "Metric": - refs = envoyx.MergeRefs(refs, getPageBlockMetricRefs(b, index)) + refs = envoyx.MergeRefs(refs, getPageBlockMetricRefs(b.Options, index)) case "Comment": - refs = envoyx.MergeRefs(refs, getPageBlockCommentRefs(b, index)) + refs = envoyx.MergeRefs(refs, getPageBlockCommentRefs(b.Options, index)) case "Progress": - refs = envoyx.MergeRefs(refs, getPageBlockProgressRefs(b, index)) + refs = envoyx.MergeRefs(refs, getPageBlockProgressRefs(b.Options, index)) } } diff --git a/server/compose/envoy/yaml_decode.gen.go b/server/compose/envoy/yaml_decode.gen.go index c368e1d186..d90405777d 100644 --- a/server/compose/envoy/yaml_decode.gen.go +++ b/server/compose/envoy/yaml_decode.gen.go @@ -1306,6 +1306,25 @@ func (d *auxYamlDoc) unmarshalNamespaceNode(dctx documentContext, n *yaml.Node, switch strings.ToLower(k.Value) { + case "blocks": + + // Handle custom node decoder + // + // The decoder may update the passed resource with arbitrary values + // as well as provide additional references and identifiers for the node. + var ( + auxRefs map[string]envoyx.Ref + auxIdents envoyx.Identifiers + ) + auxRefs, auxIdents, err = d.unmarshalNamespaceBlocksNode(r, n) + if err != nil { + return err + } + refs = envoyx.MergeRefs(refs, auxRefs) + ii = ii.Merge(auxIdents) + + break + case "id": // Handle identifiers err = y7s.DecodeScalar(n, "id", &auxNodeValue) diff --git a/server/compose/envoy/yaml_decode.go b/server/compose/envoy/yaml_decode.go index 1135ad5160..e6115d0e62 100644 --- a/server/compose/envoy/yaml_decode.go +++ b/server/compose/envoy/yaml_decode.go @@ -73,40 +73,55 @@ func (d *auxYamlDoc) unmarshalPageBlocksNode(r *types.Page, n *yaml.Node) (refs refs = map[string]envoyx.Ref{} for index, b := range r.Blocks { - switch b.Kind { - case "RecordList": - refs = envoyx.MergeRefs(refs, getPageBlockRecordListRefs(b, index)) + idents, err = unmarshalResourceBlocksNode(b.Kind, b.Options, index, refs) + } + return +} - case "Automation": - refs = envoyx.MergeRefs(refs, getPageBlockAutomationRefs(b, index)) +func (d *auxYamlDoc) unmarshalNamespaceBlocksNode(r *types.Namespace, n *yaml.Node) (refs map[string]envoyx.Ref, idents envoyx.Identifiers, err error) { + refs = map[string]envoyx.Ref{} - case "RecordOrganizer": - refs = envoyx.MergeRefs(refs, getPageBlockRecordOrganizerRefs(b, index)) + for index, gBlock := range r.Blocks { + idents, err = unmarshalResourceBlocksNode(gBlock.Kind, gBlock.Options, index, refs) + } - case "Chart": - refs = envoyx.MergeRefs(refs, getPageBlockChartRefs(b, index)) + return +} - case "Calendar": - refs = envoyx.MergeRefs(refs, getPageBlockCalendarRefs(b, index)) +func unmarshalResourceBlocksNode(kind string, options map[string]interface{}, index int, refs map[string]envoyx.Ref) (idents envoyx.Identifiers, err error) { + switch kind { + case "RecordList": + refs = envoyx.MergeRefs(refs, getPageBlockRecordListRefs(options, index)) - case "Metric": - refs = envoyx.MergeRefs(refs, getPageBlockMetricRefs(b, index)) + case "Automation": + refs = envoyx.MergeRefs(refs, getPageBlockAutomationRefs(options, index)) - case "Comment": - refs = envoyx.MergeRefs(refs, getPageBlockCommentRefs(b, index)) + case "RecordOrganizer": + refs = envoyx.MergeRefs(refs, getPageBlockRecordOrganizerRefs(options, index)) - case "Progress": - refs = envoyx.MergeRefs(refs, getPageBlockProgressRefs(b, index)) - } + case "Chart": + refs = envoyx.MergeRefs(refs, getPageBlockChartRefs(options, index)) + + case "Calendar": + refs = envoyx.MergeRefs(refs, getPageBlockCalendarRefs(options, index)) + + case "Metric": + refs = envoyx.MergeRefs(refs, getPageBlockMetricRefs(options, index)) + + case "Comment": + refs = envoyx.MergeRefs(refs, getPageBlockCommentRefs(options, index)) + + case "Progress": + refs = envoyx.MergeRefs(refs, getPageBlockProgressRefs(options, index)) } return } -func getPageBlockRecordListRefs(b types.PageBlock, index int) (refs map[string]envoyx.Ref) { +func getPageBlockRecordListRefs(options map[string]interface{}, index int) (refs map[string]envoyx.Ref) { refs = make(map[string]envoyx.Ref) - id := optString(b.Options, "module", "moduleID") + id := optString(options, "module", "moduleID") if id == "" || id == "0" { return } @@ -119,10 +134,10 @@ func getPageBlockRecordListRefs(b types.PageBlock, index int) (refs map[string]e return } -func getPageBlockChartRefs(b types.PageBlock, index int) (refs map[string]envoyx.Ref) { +func getPageBlockChartRefs(options map[string]interface{}, index int) (refs map[string]envoyx.Ref) { refs = make(map[string]envoyx.Ref) - id := optString(b.Options, "chart", "chartID") + id := optString(options, "chart", "chartID") if id == "" || id == "0" { return } @@ -135,10 +150,10 @@ func getPageBlockChartRefs(b types.PageBlock, index int) (refs map[string]envoyx return } -func getPageBlockCalendarRefs(b types.PageBlock, index int) (refs map[string]envoyx.Ref) { +func getPageBlockCalendarRefs(options map[string]interface{}, index int) (refs map[string]envoyx.Ref) { refs = make(map[string]envoyx.Ref) - ff, _ := b.Options["feeds"].([]interface{}) + ff, _ := options["feeds"].([]interface{}) for j, f := range ff { feed, _ := f.(map[string]interface{}) opt, _ := (feed["options"]).(map[string]interface{}) @@ -157,10 +172,10 @@ func getPageBlockCalendarRefs(b types.PageBlock, index int) (refs map[string]env return } -func getPageBlockMetricRefs(b types.PageBlock, index int) (refs map[string]envoyx.Ref) { +func getPageBlockMetricRefs(options map[string]interface{}, index int) (refs map[string]envoyx.Ref) { refs = make(map[string]envoyx.Ref) - mm, _ := b.Options["metrics"].([]interface{}) + mm, _ := options["metrics"].([]interface{}) for j, m := range mm { mops, _ := m.(map[string]interface{}) @@ -178,26 +193,26 @@ func getPageBlockMetricRefs(b types.PageBlock, index int) (refs map[string]envoy return } -func getPageBlockCommentRefs(b types.PageBlock, index int) (refs map[string]envoyx.Ref) { +func getPageBlockCommentRefs(options map[string]interface{}, index int) (refs map[string]envoyx.Ref) { // Same difference - return getPageBlockRecordListRefs(b, index) + return getPageBlockRecordListRefs(options, index) } -func getPageBlockProgressRefs(b types.PageBlock, index int) (refs map[string]envoyx.Ref) { +func getPageBlockProgressRefs(options map[string]interface{}, index int) (refs map[string]envoyx.Ref) { refs = make(map[string]envoyx.Ref) var aux *envoyx.Ref - aux = getPageBlockProgressValueRefs(b.Options["minValue"]) + aux = getPageBlockProgressValueRefs(options["minValue"]) if aux != nil { refs[fmt.Sprintf("Blocks.%d.Options.minValue.ModuleID", index)] = *aux } - aux = getPageBlockProgressValueRefs(b.Options["maxValue"]) + aux = getPageBlockProgressValueRefs(options["maxValue"]) if aux != nil { refs[fmt.Sprintf("Blocks.%d.Options.maxValue.ModuleID", index)] = *aux } - aux = getPageBlockProgressValueRefs(b.Options["value"]) + aux = getPageBlockProgressValueRefs(options["value"]) if aux != nil { refs[fmt.Sprintf("Blocks.%d.Options.value.ModuleID", index)] = *aux } @@ -226,15 +241,15 @@ func getPageBlockProgressValueRefs(val any) (ref *envoyx.Ref) { } } -func getPageBlockRecordOrganizerRefs(b types.PageBlock, index int) (refs map[string]envoyx.Ref) { +func getPageBlockRecordOrganizerRefs(options map[string]interface{}, index int) (refs map[string]envoyx.Ref) { // Same difference - return getPageBlockRecordListRefs(b, index) + return getPageBlockRecordListRefs(options, index) } -func getPageBlockAutomationRefs(b types.PageBlock, index int) (refs map[string]envoyx.Ref) { +func getPageBlockAutomationRefs(options map[string]interface{}, index int) (refs map[string]envoyx.Ref) { refs = make(map[string]envoyx.Ref) - bb, _ := b.Options["buttons"].([]interface{}) + bb, _ := options["buttons"].([]interface{}) for buttonIx, b := range bb { button, _ := b.(map[string]interface{}) id := optString(button, "workflow", "workflowID") @@ -489,4 +504,4 @@ func (d *auxYamlDoc) procMappingRefs(in map[string]string) (out map[string]envoy } return -} +} \ No newline at end of file diff --git a/server/compose/envoy/yaml_encode.gen.go b/server/compose/envoy/yaml_encode.gen.go index 701997a9d3..da343579d6 100644 --- a/server/compose/envoy/yaml_encode.gen.go +++ b/server/compose/envoy/yaml_encode.gen.go @@ -398,6 +398,10 @@ func (e YamlEncoder) encodeNamespace(ctx context.Context, p envoyx.EncodeParams, res := node.Resource.(*types.Namespace) // Pre-compute some map values so we can omit error checking when encoding yaml nodes + auxBlocks, err := e.encodeNamespaceBlocksC(ctx, p, tt, node, res, res.Blocks) + if err != nil { + return + } auxCreatedAt, err := e.encodeTimestamp(p, res.CreatedAt) if err != nil { return @@ -413,6 +417,7 @@ func (e YamlEncoder) encodeNamespace(ctx context.Context, p envoyx.EncodeParams, } out, err = y7s.AddMap(out, + "blocks", auxBlocks, "createdAt", auxCreatedAt, "deletedAt", auxDeletedAt, "enabled", res.Enabled, @@ -718,4 +723,4 @@ func safeParentIdentifier(tt envoyx.Traverser, n *envoyx.Node, ref envoyx.Ref) ( } return aux.Identifiers.FriendlyIdentifier() -} +} \ No newline at end of file diff --git a/server/compose/envoy/yaml_encode.go b/server/compose/envoy/yaml_encode.go index 6a5dc8420d..624abc5f9a 100644 --- a/server/compose/envoy/yaml_encode.go +++ b/server/compose/envoy/yaml_encode.go @@ -1,14 +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) { @@ -89,128 +88,152 @@ func (e YamlEncoder) encodeModuleFieldOptionsC(ctx context.Context, p envoyx.Enc } func (e YamlEncoder) encodePageBlocksC(ctx context.Context, p envoyx.EncodeParams, tt envoyx.Traverser, n *envoyx.Node, pg *types.Page, bb types.PageBlocks) (_ any, err error) { + var aux any out, _ := y7s.MakeSeq() - var aux any for i, b := range pg.Blocks { - aux, err = e.encodePageBlockC(ctx, p, tt, n, pg, i, b) + 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 } } return out, nil } -func (e YamlEncoder) encodePageBlockC(ctx context.Context, p envoyx.EncodeParams, tt envoyx.Traverser, n *envoyx.Node, pg *types.Page, index int, b types.PageBlock) (_ any, err error) { - - switch b.Kind { - case "RecordList": - b = e.cleanupPageblockRecordList(b) - - modRef := n.References[fmt.Sprintf("Blocks.%d.Options.ModuleID", index)] - b.Options["module"] = safeParentIdentifier(tt, n, modRef) - delete(b.Options, "moduleID") - break - - case "RecordOrganizer": - modRef := n.References[fmt.Sprintf("Blocks.%d.Options.ModuleID", index)] - b.Options["module"] = safeParentIdentifier(tt, n, modRef) - delete(b.Options, "moduleID") - break - - case "Chart": - chrRef := n.References[fmt.Sprintf("Blocks.%d.Options.ChartID", index)] - b.Options["chart"] = safeParentIdentifier(tt, n, chrRef) - delete(b.Options, "chartID") - break - - case "Calendar": - ff, _ := b.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, _ := b.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, _ := b.Options["metrics"].([]interface{}) - for i, m := range mm { - modRef := n.References[fmt.Sprintf("Blocks.%d.Options.metrics.%d.ModuleID", index, i)] +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 + out, _ := y7s.MakeSeq() - mops, _ := m.(map[string]interface{}) - mops["module"] = safeParentIdentifier(tt, n, modRef) - delete(mops, "moduleID") - } - break + 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 + } - case "Comment": - modRef := n.References[fmt.Sprintf("Blocks.%d.Options.ModuleID", index)] - b.Options["module"] = safeParentIdentifier(tt, n, modRef) - delete(b.Options, "moduleID") - break + gB.Options = options + aux = gB - case "Progress": - err = e.encodeProgressPageblockVal("minValue", index, n, tt, &b) - if err != nil { - return - } + out, err = y7s.AddSeq(out, aux) + if err != nil { + return nil, err + } + } - err = e.encodeProgressPageblockVal("maxValue", index, n, tt, &b) - if err != nil { - return - } - - err = e.encodeProgressPageblockVal("value", index, n, tt, &b) - if err != nil { - return - } - break - } + return out, nil +} - return b, 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{}) (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, b *types.PageBlock) (err error) { - if reflect2.IsNil(b.Options[k]) { +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 := b.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(b types.PageBlock) (out types.PageBlock) { - out = b - rawFF, has := out.Options["fields"] +func (e YamlEncoder) cleanupPageblockRecordList(options map[string]interface{}) (out map[string]interface{}) { + out = options + rawFF, has := out["fields"] if !has { return } @@ -232,7 +255,7 @@ func (e YamlEncoder) cleanupPageblockRecordList(b types.PageBlock) (out types.Pa } } - out.Options["fields"] = retFF + out["fields"] = retFF - return b -} + return +} \ No newline at end of file diff --git a/server/compose/model/models.gen.go b/server/compose/model/models.gen.go index 13f1992f59..43044887de 100644 --- a/server/compose/model/models.gen.go +++ b/server/compose/model/models.gen.go @@ -522,6 +522,14 @@ var Namespace = &dal.Model{ Store: &dal.CodecAlias{Ident: "meta"}, }, + &dal.Attribute{ + Ident: "Blocks", + Type: &dal.TypeJSON{ + DefaultValue: "{}", + }, + Store: &dal.CodecAlias{Ident: "blocks"}, + }, + &dal.Attribute{ Ident: "Name", Sortable: true, Type: &dal.TypeText{}, @@ -1200,4 +1208,4 @@ func init() { Record, RecordRevision, ) -} +} \ No newline at end of file diff --git a/server/compose/namespace.cue b/server/compose/namespace.cue index dcfba7d91f..d767c64005 100644 --- a/server/compose/namespace.cue +++ b/server/compose/namespace.cue @@ -27,6 +27,18 @@ namespace: { omitSetter: true omitGetter: true } + blocks: { + goType: "types.GlobalBlocks" + dal: { type: "JSON", defaultEmptyObject: true } + omitSetter: true + omitGetter: true + envoy: { + yaml: { + customDecoder: true + customEncoder: true + } + } + } name: { sortable: true dal: {} diff --git a/server/compose/rest.yaml b/server/compose/rest.yaml index 271805e7bc..78e2006c86 100644 --- a/server/compose/rest.yaml +++ b/server/compose/rest.yaml @@ -73,6 +73,10 @@ endpoints: name: meta required: true title: Meta data + - type: sqlxTypes.JSONText + name: blocks + required: false + title: Blocks - name: read method: GET title: Read namespace diff --git a/server/compose/rest/namespace.go b/server/compose/rest/namespace.go index c399fc5f6a..aeae367068 100644 --- a/server/compose/rest/namespace.go +++ b/server/compose/rest/namespace.go @@ -174,6 +174,14 @@ func (ctrl Namespace) Update(ctx context.Context, r *request.NamespaceUpdate) (i 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(&ns.Blocks); err != nil { + return nil, err + } + } + ns, err = ctrl.namespace.Update(ctx, ns) return ctrl.makePayload(ctx, ns, err) } diff --git a/server/compose/rest/request/namespace.go b/server/compose/rest/request/namespace.go index ec7d030ca0..376e9f843d 100644 --- a/server/compose/rest/request/namespace.go +++ b/server/compose/rest/request/namespace.go @@ -135,6 +135,11 @@ type ( // Meta data Meta sqlxTypes.JSONText + // Blocks POST parameter + // + // Blocks + Blocks sqlxTypes.JSONText + // Labels POST parameter // // Labels @@ -570,6 +575,7 @@ func (r NamespaceUpdate) Auditable() map[string]interface{} { "slug": r.Slug, "enabled": r.Enabled, "meta": r.Meta, + "blocks": r.Blocks, "labels": r.Labels, "updatedAt": r.UpdatedAt, } @@ -600,6 +606,11 @@ func (r NamespaceUpdate) GetMeta() sqlxTypes.JSONText { return r.Meta } +// Auditable returns all auditable/loggable parameters +func (r NamespaceUpdate) GetBlocks() sqlxTypes.JSONText { + return r.Blocks +} + // Auditable returns all auditable/loggable parameters func (r NamespaceUpdate) GetLabels() map[string]labelTypes.LabelValue { return r.Labels @@ -659,6 +670,13 @@ func (r *NamespaceUpdate) Fill(req *http.Request) (err error) { } } + if val, ok := req.MultipartForm.Value["blocks"]; ok && len(val) > 0 { + r.Blocks, err = payload.ParseJSONTextWithErr(val[0]) + if err != nil { + return err + } + } + if val, ok := req.MultipartForm.Value["labels[]"]; ok { r.Labels, err = label.ParseStrings(val) if err != nil { @@ -715,6 +733,13 @@ func (r *NamespaceUpdate) Fill(req *http.Request) (err error) { } } + if val, ok := req.Form["blocks"]; ok && len(val) > 0 { + r.Blocks, err = payload.ParseJSONTextWithErr(val[0]) + if err != nil { + return err + } + } + if val, ok := req.Form["labels[]"]; ok { r.Labels, err = label.ParseStrings(val) if err != nil { diff --git a/server/compose/service/namespace.go b/server/compose/service/namespace.go index 38a8476af3..96a8bb2a92 100644 --- a/server/compose/service/namespace.go +++ b/server/compose/service/namespace.go @@ -653,6 +653,37 @@ func (svc namespace) handleUpdate(ctx context.Context, upd *types.Namespace) nam res.Meta = upd.Meta } + // Get max blockID for later use + blockID := uint64(0) + for _, b := range res.Blocks { + if b.BlockID == blockID { + // assign ids on new page blocks + for i := range upd.Blocks { + upd.Blocks[i].BlockID = uint64(i) + 1 + } + } + + if b.BlockID > blockID { + blockID = b.BlockID + } + } + + if !reflect.DeepEqual(res.Blocks, upd.Blocks) { + res.Blocks = upd.Blocks + changes |= namespaceChanged + } + + // Assure blockIDs + for i, b := range res.Blocks { + if b.BlockID == 0 { + blockID++ + b.BlockID = blockID + res.Blocks[i] = b + + changes |= namespaceChanged + } + } + if upd.Labels != nil { if label.Changed(res.Labels, upd.Labels) { changes |= namespaceLabelsChanged diff --git a/server/compose/types/namespace.go b/server/compose/types/namespace.go index ea181286eb..3b19e6aedc 100644 --- a/server/compose/types/namespace.go +++ b/server/compose/types/namespace.go @@ -16,6 +16,7 @@ type ( Slug string `json:"slug"` Enabled bool `json:"enabled"` Meta NamespaceMeta `json:"meta"` + Blocks GlobalBlocks `json:"blocks"` Labels map[string]labelTypes.LabelValue `json:"labels,omitempty"` @@ -29,6 +30,28 @@ type ( Name string `json:"name"` } + GlobalBlocks []GlobalBlock + + GlobalBlock struct { + BlockID uint64 `json:"blockID,string,omitempty"` + + Options map[string]interface{} `json:"options,omitempty"` + Style PageBlockStyle `json:"style,omitempty"` + Kind string `json:"kind"` + XYWH [4]int `json:"xywh"` // x,y,w,h + Meta map[string]any `json:"meta,omitempty"` + + // Warning: value of this field is now handled via resource-translation facility + // struct field is kept for the convenience for now since it allows us + // easy encoding/decoding of the outgoing/incoming values + Title string `json:"title,omitempty"` + + // Warning: value of this field is now handled via resource-translation facility + // struct field is kept for the convenience for now since it allows us + // easy encoding/decoding of the outgoing/incoming values + Description string `json:"description,omitempty"` + } + NamespaceFilter struct { NamespaceID []string `json:"namespaceID"` @@ -92,3 +115,6 @@ func (set NamespaceSet) FindByHandle(handle string) *Namespace { func (nm *NamespaceMeta) Scan(src any) error { return sql.ParseJSON(src, nm) } func (nm NamespaceMeta) Value() (driver.Value, error) { return json.Marshal(nm) } + +func (nm *GlobalBlocks) Scan(src any) error { return sql.ParseJSON(src, nm) } +func (nm GlobalBlocks) Value() (driver.Value, error) { return json.Marshal(nm) } diff --git a/server/store/adapters/rdbms/aux_types.gen.go b/server/store/adapters/rdbms/aux_types.gen.go index 96a72d66df..4da509c55c 100644 --- a/server/store/adapters/rdbms/aux_types.gen.go +++ b/server/store/adapters/rdbms/aux_types.gen.go @@ -279,6 +279,7 @@ type ( Slug string `db:"slug"` Enabled bool `db:"enabled"` Meta composeType.NamespaceMeta `db:"meta"` + Blocks composeType.GlobalBlocks `db:"blocks"` Name string `db:"name"` CreatedAt time.Time `db:"created_at"` UpdatedAt *time.Time `db:"updated_at"` @@ -1619,6 +1620,7 @@ func (aux *auxComposeNamespace) encode(res *composeType.Namespace) (_ error) { aux.Slug = res.Slug aux.Enabled = res.Enabled aux.Meta = res.Meta + aux.Blocks = res.Blocks aux.Name = res.Name aux.CreatedAt = res.CreatedAt aux.UpdatedAt = res.UpdatedAt @@ -1635,6 +1637,7 @@ func (aux auxComposeNamespace) decode() (res *composeType.Namespace, _ error) { res.Slug = aux.Slug res.Enabled = aux.Enabled res.Meta = aux.Meta + res.Blocks = aux.Blocks res.Name = aux.Name res.CreatedAt = aux.CreatedAt res.UpdatedAt = aux.UpdatedAt @@ -1651,6 +1654,7 @@ func (aux *auxComposeNamespace) scan(row scanner) error { &aux.Slug, &aux.Enabled, &aux.Meta, + &aux.Blocks, &aux.Name, &aux.CreatedAt, &aux.UpdatedAt, diff --git a/server/store/adapters/rdbms/queries.gen.go b/server/store/adapters/rdbms/queries.gen.go index 78e231e6d0..f4840a28cb 100644 --- a/server/store/adapters/rdbms/queries.gen.go +++ b/server/store/adapters/rdbms/queries.gen.go @@ -1869,6 +1869,7 @@ var ( "slug", "enabled", "meta", + "blocks", "name", "created_at", "updated_at", @@ -1886,6 +1887,7 @@ var ( "slug": res.Slug, "enabled": res.Enabled, "meta": res.Meta, + "blocks": res.Blocks, "name": res.Name, "created_at": res.CreatedAt, "updated_at": res.UpdatedAt, @@ -1906,6 +1908,7 @@ var ( "slug": res.Slug, "enabled": res.Enabled, "meta": res.Meta, + "blocks": res.Blocks, "name": res.Name, "created_at": res.CreatedAt, "updated_at": res.UpdatedAt, @@ -1924,6 +1927,7 @@ var ( "slug": res.Slug, "enabled": res.Enabled, "meta": res.Meta, + "blocks": res.Blocks, "name": res.Name, "created_at": res.CreatedAt, "updated_at": res.UpdatedAt, diff --git a/server/store/adapters/rdbms/upgrade_fixes.go b/server/store/adapters/rdbms/upgrade_fixes.go index 4ea62756b4..4c4ae8e159 100644 --- a/server/store/adapters/rdbms/upgrade_fixes.go +++ b/server/store/adapters/rdbms/upgrade_fixes.go @@ -55,6 +55,7 @@ var ( fix_2024_09_03_renameFederationNodeSyncComposeID, fix_2024_09_03_addFederationNodeSyncNodeIDIndex, fix_2024_9_7_migrateLabelsValueToJsonb, + fix_2024_09_07_migrateNamespacePageBlocks, } fixesPost = []func(context.Context, *Store) error{ @@ -1312,6 +1313,14 @@ func fix_2024_9_7_migrateLabelsValueToJsonb(ctx context.Context, s *Store) (err return nil } + +func fix_2024_03_00_migrateNamespacePageBlocks(ctx context.Context, s *Store) (err error) { + return addColumn(ctx, s, + "compose_namespace", + &dal.Attribute{Ident: "blocks", Type: &dal.TypeJSON{DefaultValue: "[]"}}, + ) +} + func count(ctx context.Context, s *Store, table string, ee ...goqu.Expression) (count int) { db := s.DB.(goqu.SQLDatabase) From f71987fb935b449fc31d3f2a66d3caf906dbf83a Mon Sep 17 00:00:00 2001 From: Toocky Date: Sat, 28 Feb 2026 19:50:41 -0600 Subject: [PATCH 2/5] Fixed Migration --- server/store/adapters/rdbms/upgrade_fixes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/store/adapters/rdbms/upgrade_fixes.go b/server/store/adapters/rdbms/upgrade_fixes.go index 4c4ae8e159..8320f70c0d 100644 --- a/server/store/adapters/rdbms/upgrade_fixes.go +++ b/server/store/adapters/rdbms/upgrade_fixes.go @@ -1314,7 +1314,7 @@ func fix_2024_9_7_migrateLabelsValueToJsonb(ctx context.Context, s *Store) (err } -func fix_2024_03_00_migrateNamespacePageBlocks(ctx context.Context, s *Store) (err error) { +func fix_2024_09_07_migrateNamespacePageBlocks(ctx context.Context, s *Store) (err error) { return addColumn(ctx, s, "compose_namespace", &dal.Attribute{Ident: "blocks", Type: &dal.TypeJSON{DefaultValue: "[]"}}, From b68a7d10cd431534053823c951c8cf74d09245b4 Mon Sep 17 00:00:00 2001 From: Toocky Date: Wed, 4 Mar 2026 17:08:44 -0600 Subject: [PATCH 3/5] Migrate outdated Global blocks branch to latest code, test and resolve issues. --- .../Admin/Page/Builder/Selector.vue | 90 ++++++- .../components/PageBlocks/Configurator.vue | 25 ++ client/web/compose/src/store/namespace.js | 10 + .../compose/src/views/Admin/Modules/List.vue | 2 +- .../compose/src/views/Admin/Pages/Builder.vue | 143 +++++++++-- lib/js/src/api-clients/compose.ts | 2 + lib/js/src/compose/types/namespace.ts | 8 + lib/js/src/compose/types/page-block/base.ts | 3 + locale/en/corteza-webapp-compose/block.yaml | 4 + locale/en/corteza-webapp-compose/page.yaml | 1 + server/compose/envoy/store_decode.go | 16 +- server/compose/envoy/yaml_decode.gen.go | 19 ++ server/compose/envoy/yaml_decode.go | 89 ++++--- server/compose/envoy/yaml_encode.gen.go | 7 +- server/compose/envoy/yaml_encode.go | 235 ++++++++++-------- server/compose/model/models.gen.go | 11 +- server/compose/namespace.cue | 12 + server/compose/rest.yaml | 4 + server/compose/rest/namespace.go | 16 ++ server/compose/rest/request/namespace.go | 50 ++++ server/compose/service/namespace.go | 31 +++ server/compose/types/namespace.go | 31 +++ server/store/adapters/rdbms/aux_types.gen.go | 4 + server/store/adapters/rdbms/queries.gen.go | 4 + server/store/adapters/rdbms/upgrade_fixes.go | 9 + 25 files changed, 638 insertions(+), 188 deletions(-) diff --git a/client/web/compose/src/components/Admin/Page/Builder/Selector.vue b/client/web/compose/src/components/Admin/Page/Builder/Selector.vue index 5ebf7b4232..fce73f5e7d 100644 --- a/client/web/compose/src/components/Admin/Page/Builder/Selector.vue +++ b/client/web/compose/src/components/Admin/Page/Builder/Selector.vue @@ -33,20 +33,21 @@
@@ -54,9 +55,9 @@ + + + + + + + + + + + + + + + + [], }, - existingBlocks: { + existingLayoutBlocks: { + type: Array, + default: () => [], + }, + + selectableGlobalBlocks: { type: Array, default: () => [], }, @@ -111,7 +159,8 @@ export default { return { current: undefined, - selectedExistingBlock: undefined, + selectedLayoutBlock: undefined, + selectedGlobalBlock: undefined, types: [ { @@ -220,9 +269,26 @@ export default { setDefaultValues () { this.current = undefined - this.selectedExistingBlock = undefined + this.selectedLayoutBlock = undefined + this.selectedGlobalBlock = undefined this.types = [] }, + + fetchBlockData (blockID) { + if (blockID.includes('-')) { + return this.selectableGlobalBlocks.find((b) => b.blockID === blockID) + } + + return this.existingLayoutBlocks.find((b) => b.blockID === blockID) + }, + + selectBlock (block, clone = false) { + if (clone) { + this.$emit('select', this.fetchBlockData(block).clone()) + } else { + this.$emit('select', this.fetchBlockData(block)) + } + }, }, } - + \ No newline at end of file diff --git a/client/web/compose/src/components/PageBlocks/Configurator.vue b/client/web/compose/src/components/PageBlocks/Configurator.vue index 36361d3575..daacd824b3 100644 --- a/client/web/compose/src/components/PageBlocks/Configurator.vue +++ b/client/web/compose/src/components/PageBlocks/Configurator.vue @@ -184,6 +184,17 @@ > {{ $t('general.border.show') }} + + + {{ $t('general.globalBlock.label') }} + @@ -405,6 +416,8 @@ export default { options: [], }, abortableRequests: [], + initialBlockState: undefined, + initialBlockID: undefined, } }, @@ -476,6 +489,8 @@ export default { }, created () { + this.initialBlockState = this.block.meta.namespaceID + this.initialBlockID = this.block.blockID this.fetchRoles() }, @@ -519,6 +534,16 @@ export default { } this.abortableRequests = [] }, + + updateGlobalState (value) { + this.block.blockID = this.initialBlockState === value ? this.initialBlockID : NoID + + if (value) { + this.block.meta.namespaceID = value + } else { + this.block.meta.namespaceID = undefined + } + }, }, } 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 f6b79411e1..6b7d030e96 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 @@ -" @@ -366,6 +381,7 @@ diff --git a/lib/js/src/api-clients/compose.ts b/lib/js/src/api-clients/compose.ts index fad9162be1..c12adf1be6 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 eb925b116f..28e0946e70 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 @@ -78,6 +82,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, 'canDeleteNamespace', diff --git a/lib/js/src/compose/types/page-block/base.ts b/lib/js/src/compose/types/page-block/base.ts index b38c2f837e..3ae9871c77 100644 --- a/lib/js/src/compose/types/page-block/base.ts +++ b/lib/js/src/compose/types/page-block/base.ts @@ -32,6 +32,8 @@ interface PageBlockMeta { customID?: string; customCSSClass?: 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 @@ -59,6 +61,7 @@ export class PageBlock { expression: '', roles: [], }, + namespaceID: undefined, } public style: PageBlockStyle = { diff --git a/locale/en/corteza-webapp-compose/block.yaml b/locale/en/corteza-webapp-compose/block.yaml index 8ba4c51565..50fb979351 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}}) @@ -219,6 +221,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 4443f5d341..cff2919b31 100644 --- a/locale/en/corteza-webapp-compose/page.yaml +++ b/locale/en/corteza-webapp-compose/page.yaml @@ -79,6 +79,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}}" diff --git a/server/compose/envoy/store_decode.go b/server/compose/envoy/store_decode.go index 13c5c81e2c..b479e295f0 100644 --- a/server/compose/envoy/store_decode.go +++ b/server/compose/envoy/store_decode.go @@ -164,28 +164,28 @@ func decodePageRefs(p *types.Page) (refs map[string]envoyx.Ref) { for index, b := range p.Blocks { switch b.Kind { case "RecordList": - refs = envoyx.MergeRefs(refs, getPageBlockRecordListRefs(b, index)) + refs = envoyx.MergeRefs(refs, getPageBlockRecordListRefs(b.Options, index)) case "Automation": - refs = envoyx.MergeRefs(refs, getPageBlockAutomationRefs(b, index)) + refs = envoyx.MergeRefs(refs, getPageBlockAutomationRefs(b.Options, index)) case "RecordOrganizer": - refs = envoyx.MergeRefs(refs, getPageBlockRecordOrganizerRefs(b, index)) + refs = envoyx.MergeRefs(refs, getPageBlockRecordOrganizerRefs(b.Options, index)) case "Chart": - refs = envoyx.MergeRefs(refs, getPageBlockChartRefs(b, index)) + refs = envoyx.MergeRefs(refs, getPageBlockChartRefs(b.Options, index)) case "Calendar": - refs = envoyx.MergeRefs(refs, getPageBlockCalendarRefs(b, index)) + refs = envoyx.MergeRefs(refs, getPageBlockCalendarRefs(b.Options, index)) case "Metric": - refs = envoyx.MergeRefs(refs, getPageBlockMetricRefs(b, index)) + refs = envoyx.MergeRefs(refs, getPageBlockMetricRefs(b.Options, index)) case "Comment": - refs = envoyx.MergeRefs(refs, getPageBlockCommentRefs(b, index)) + refs = envoyx.MergeRefs(refs, getPageBlockCommentRefs(b.Options, index)) case "Progress": - refs = envoyx.MergeRefs(refs, getPageBlockProgressRefs(b, index)) + refs = envoyx.MergeRefs(refs, getPageBlockProgressRefs(b.Options, index)) } } diff --git a/server/compose/envoy/yaml_decode.gen.go b/server/compose/envoy/yaml_decode.gen.go index c368e1d186..d90405777d 100644 --- a/server/compose/envoy/yaml_decode.gen.go +++ b/server/compose/envoy/yaml_decode.gen.go @@ -1306,6 +1306,25 @@ func (d *auxYamlDoc) unmarshalNamespaceNode(dctx documentContext, n *yaml.Node, switch strings.ToLower(k.Value) { + case "blocks": + + // Handle custom node decoder + // + // The decoder may update the passed resource with arbitrary values + // as well as provide additional references and identifiers for the node. + var ( + auxRefs map[string]envoyx.Ref + auxIdents envoyx.Identifiers + ) + auxRefs, auxIdents, err = d.unmarshalNamespaceBlocksNode(r, n) + if err != nil { + return err + } + refs = envoyx.MergeRefs(refs, auxRefs) + ii = ii.Merge(auxIdents) + + break + case "id": // Handle identifiers err = y7s.DecodeScalar(n, "id", &auxNodeValue) diff --git a/server/compose/envoy/yaml_decode.go b/server/compose/envoy/yaml_decode.go index 1135ad5160..e6115d0e62 100644 --- a/server/compose/envoy/yaml_decode.go +++ b/server/compose/envoy/yaml_decode.go @@ -73,40 +73,55 @@ func (d *auxYamlDoc) unmarshalPageBlocksNode(r *types.Page, n *yaml.Node) (refs refs = map[string]envoyx.Ref{} for index, b := range r.Blocks { - switch b.Kind { - case "RecordList": - refs = envoyx.MergeRefs(refs, getPageBlockRecordListRefs(b, index)) + idents, err = unmarshalResourceBlocksNode(b.Kind, b.Options, index, refs) + } + return +} - case "Automation": - refs = envoyx.MergeRefs(refs, getPageBlockAutomationRefs(b, index)) +func (d *auxYamlDoc) unmarshalNamespaceBlocksNode(r *types.Namespace, n *yaml.Node) (refs map[string]envoyx.Ref, idents envoyx.Identifiers, err error) { + refs = map[string]envoyx.Ref{} - case "RecordOrganizer": - refs = envoyx.MergeRefs(refs, getPageBlockRecordOrganizerRefs(b, index)) + for index, gBlock := range r.Blocks { + idents, err = unmarshalResourceBlocksNode(gBlock.Kind, gBlock.Options, index, refs) + } - case "Chart": - refs = envoyx.MergeRefs(refs, getPageBlockChartRefs(b, index)) + return +} - case "Calendar": - refs = envoyx.MergeRefs(refs, getPageBlockCalendarRefs(b, index)) +func unmarshalResourceBlocksNode(kind string, options map[string]interface{}, index int, refs map[string]envoyx.Ref) (idents envoyx.Identifiers, err error) { + switch kind { + case "RecordList": + refs = envoyx.MergeRefs(refs, getPageBlockRecordListRefs(options, index)) - case "Metric": - refs = envoyx.MergeRefs(refs, getPageBlockMetricRefs(b, index)) + case "Automation": + refs = envoyx.MergeRefs(refs, getPageBlockAutomationRefs(options, index)) - case "Comment": - refs = envoyx.MergeRefs(refs, getPageBlockCommentRefs(b, index)) + case "RecordOrganizer": + refs = envoyx.MergeRefs(refs, getPageBlockRecordOrganizerRefs(options, index)) - case "Progress": - refs = envoyx.MergeRefs(refs, getPageBlockProgressRefs(b, index)) - } + case "Chart": + refs = envoyx.MergeRefs(refs, getPageBlockChartRefs(options, index)) + + case "Calendar": + refs = envoyx.MergeRefs(refs, getPageBlockCalendarRefs(options, index)) + + case "Metric": + refs = envoyx.MergeRefs(refs, getPageBlockMetricRefs(options, index)) + + case "Comment": + refs = envoyx.MergeRefs(refs, getPageBlockCommentRefs(options, index)) + + case "Progress": + refs = envoyx.MergeRefs(refs, getPageBlockProgressRefs(options, index)) } return } -func getPageBlockRecordListRefs(b types.PageBlock, index int) (refs map[string]envoyx.Ref) { +func getPageBlockRecordListRefs(options map[string]interface{}, index int) (refs map[string]envoyx.Ref) { refs = make(map[string]envoyx.Ref) - id := optString(b.Options, "module", "moduleID") + id := optString(options, "module", "moduleID") if id == "" || id == "0" { return } @@ -119,10 +134,10 @@ func getPageBlockRecordListRefs(b types.PageBlock, index int) (refs map[string]e return } -func getPageBlockChartRefs(b types.PageBlock, index int) (refs map[string]envoyx.Ref) { +func getPageBlockChartRefs(options map[string]interface{}, index int) (refs map[string]envoyx.Ref) { refs = make(map[string]envoyx.Ref) - id := optString(b.Options, "chart", "chartID") + id := optString(options, "chart", "chartID") if id == "" || id == "0" { return } @@ -135,10 +150,10 @@ func getPageBlockChartRefs(b types.PageBlock, index int) (refs map[string]envoyx return } -func getPageBlockCalendarRefs(b types.PageBlock, index int) (refs map[string]envoyx.Ref) { +func getPageBlockCalendarRefs(options map[string]interface{}, index int) (refs map[string]envoyx.Ref) { refs = make(map[string]envoyx.Ref) - ff, _ := b.Options["feeds"].([]interface{}) + ff, _ := options["feeds"].([]interface{}) for j, f := range ff { feed, _ := f.(map[string]interface{}) opt, _ := (feed["options"]).(map[string]interface{}) @@ -157,10 +172,10 @@ func getPageBlockCalendarRefs(b types.PageBlock, index int) (refs map[string]env return } -func getPageBlockMetricRefs(b types.PageBlock, index int) (refs map[string]envoyx.Ref) { +func getPageBlockMetricRefs(options map[string]interface{}, index int) (refs map[string]envoyx.Ref) { refs = make(map[string]envoyx.Ref) - mm, _ := b.Options["metrics"].([]interface{}) + mm, _ := options["metrics"].([]interface{}) for j, m := range mm { mops, _ := m.(map[string]interface{}) @@ -178,26 +193,26 @@ func getPageBlockMetricRefs(b types.PageBlock, index int) (refs map[string]envoy return } -func getPageBlockCommentRefs(b types.PageBlock, index int) (refs map[string]envoyx.Ref) { +func getPageBlockCommentRefs(options map[string]interface{}, index int) (refs map[string]envoyx.Ref) { // Same difference - return getPageBlockRecordListRefs(b, index) + return getPageBlockRecordListRefs(options, index) } -func getPageBlockProgressRefs(b types.PageBlock, index int) (refs map[string]envoyx.Ref) { +func getPageBlockProgressRefs(options map[string]interface{}, index int) (refs map[string]envoyx.Ref) { refs = make(map[string]envoyx.Ref) var aux *envoyx.Ref - aux = getPageBlockProgressValueRefs(b.Options["minValue"]) + aux = getPageBlockProgressValueRefs(options["minValue"]) if aux != nil { refs[fmt.Sprintf("Blocks.%d.Options.minValue.ModuleID", index)] = *aux } - aux = getPageBlockProgressValueRefs(b.Options["maxValue"]) + aux = getPageBlockProgressValueRefs(options["maxValue"]) if aux != nil { refs[fmt.Sprintf("Blocks.%d.Options.maxValue.ModuleID", index)] = *aux } - aux = getPageBlockProgressValueRefs(b.Options["value"]) + aux = getPageBlockProgressValueRefs(options["value"]) if aux != nil { refs[fmt.Sprintf("Blocks.%d.Options.value.ModuleID", index)] = *aux } @@ -226,15 +241,15 @@ func getPageBlockProgressValueRefs(val any) (ref *envoyx.Ref) { } } -func getPageBlockRecordOrganizerRefs(b types.PageBlock, index int) (refs map[string]envoyx.Ref) { +func getPageBlockRecordOrganizerRefs(options map[string]interface{}, index int) (refs map[string]envoyx.Ref) { // Same difference - return getPageBlockRecordListRefs(b, index) + return getPageBlockRecordListRefs(options, index) } -func getPageBlockAutomationRefs(b types.PageBlock, index int) (refs map[string]envoyx.Ref) { +func getPageBlockAutomationRefs(options map[string]interface{}, index int) (refs map[string]envoyx.Ref) { refs = make(map[string]envoyx.Ref) - bb, _ := b.Options["buttons"].([]interface{}) + bb, _ := options["buttons"].([]interface{}) for buttonIx, b := range bb { button, _ := b.(map[string]interface{}) id := optString(button, "workflow", "workflowID") @@ -489,4 +504,4 @@ func (d *auxYamlDoc) procMappingRefs(in map[string]string) (out map[string]envoy } return -} +} \ No newline at end of file diff --git a/server/compose/envoy/yaml_encode.gen.go b/server/compose/envoy/yaml_encode.gen.go index 701997a9d3..da343579d6 100644 --- a/server/compose/envoy/yaml_encode.gen.go +++ b/server/compose/envoy/yaml_encode.gen.go @@ -398,6 +398,10 @@ func (e YamlEncoder) encodeNamespace(ctx context.Context, p envoyx.EncodeParams, res := node.Resource.(*types.Namespace) // Pre-compute some map values so we can omit error checking when encoding yaml nodes + auxBlocks, err := e.encodeNamespaceBlocksC(ctx, p, tt, node, res, res.Blocks) + if err != nil { + return + } auxCreatedAt, err := e.encodeTimestamp(p, res.CreatedAt) if err != nil { return @@ -413,6 +417,7 @@ func (e YamlEncoder) encodeNamespace(ctx context.Context, p envoyx.EncodeParams, } out, err = y7s.AddMap(out, + "blocks", auxBlocks, "createdAt", auxCreatedAt, "deletedAt", auxDeletedAt, "enabled", res.Enabled, @@ -718,4 +723,4 @@ func safeParentIdentifier(tt envoyx.Traverser, n *envoyx.Node, ref envoyx.Ref) ( } return aux.Identifiers.FriendlyIdentifier() -} +} \ No newline at end of file diff --git a/server/compose/envoy/yaml_encode.go b/server/compose/envoy/yaml_encode.go index 6a5dc8420d..624abc5f9a 100644 --- a/server/compose/envoy/yaml_encode.go +++ b/server/compose/envoy/yaml_encode.go @@ -1,14 +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) { @@ -89,128 +88,152 @@ func (e YamlEncoder) encodeModuleFieldOptionsC(ctx context.Context, p envoyx.Enc } func (e YamlEncoder) encodePageBlocksC(ctx context.Context, p envoyx.EncodeParams, tt envoyx.Traverser, n *envoyx.Node, pg *types.Page, bb types.PageBlocks) (_ any, err error) { + var aux any out, _ := y7s.MakeSeq() - var aux any for i, b := range pg.Blocks { - aux, err = e.encodePageBlockC(ctx, p, tt, n, pg, i, b) + 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 } } return out, nil } -func (e YamlEncoder) encodePageBlockC(ctx context.Context, p envoyx.EncodeParams, tt envoyx.Traverser, n *envoyx.Node, pg *types.Page, index int, b types.PageBlock) (_ any, err error) { - - switch b.Kind { - case "RecordList": - b = e.cleanupPageblockRecordList(b) - - modRef := n.References[fmt.Sprintf("Blocks.%d.Options.ModuleID", index)] - b.Options["module"] = safeParentIdentifier(tt, n, modRef) - delete(b.Options, "moduleID") - break - - case "RecordOrganizer": - modRef := n.References[fmt.Sprintf("Blocks.%d.Options.ModuleID", index)] - b.Options["module"] = safeParentIdentifier(tt, n, modRef) - delete(b.Options, "moduleID") - break - - case "Chart": - chrRef := n.References[fmt.Sprintf("Blocks.%d.Options.ChartID", index)] - b.Options["chart"] = safeParentIdentifier(tt, n, chrRef) - delete(b.Options, "chartID") - break - - case "Calendar": - ff, _ := b.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, _ := b.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, _ := b.Options["metrics"].([]interface{}) - for i, m := range mm { - modRef := n.References[fmt.Sprintf("Blocks.%d.Options.metrics.%d.ModuleID", index, i)] +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 + out, _ := y7s.MakeSeq() - mops, _ := m.(map[string]interface{}) - mops["module"] = safeParentIdentifier(tt, n, modRef) - delete(mops, "moduleID") - } - break + 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 + } - case "Comment": - modRef := n.References[fmt.Sprintf("Blocks.%d.Options.ModuleID", index)] - b.Options["module"] = safeParentIdentifier(tt, n, modRef) - delete(b.Options, "moduleID") - break + gB.Options = options + aux = gB - case "Progress": - err = e.encodeProgressPageblockVal("minValue", index, n, tt, &b) - if err != nil { - return - } + out, err = y7s.AddSeq(out, aux) + if err != nil { + return nil, err + } + } - err = e.encodeProgressPageblockVal("maxValue", index, n, tt, &b) - if err != nil { - return - } - - err = e.encodeProgressPageblockVal("value", index, n, tt, &b) - if err != nil { - return - } - break - } + return out, nil +} - return b, 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{}) (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, b *types.PageBlock) (err error) { - if reflect2.IsNil(b.Options[k]) { +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 := b.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(b types.PageBlock) (out types.PageBlock) { - out = b - rawFF, has := out.Options["fields"] +func (e YamlEncoder) cleanupPageblockRecordList(options map[string]interface{}) (out map[string]interface{}) { + out = options + rawFF, has := out["fields"] if !has { return } @@ -232,7 +255,7 @@ func (e YamlEncoder) cleanupPageblockRecordList(b types.PageBlock) (out types.Pa } } - out.Options["fields"] = retFF + out["fields"] = retFF - return b -} + return +} \ No newline at end of file diff --git a/server/compose/model/models.gen.go b/server/compose/model/models.gen.go index 13f1992f59..00f4082360 100644 --- a/server/compose/model/models.gen.go +++ b/server/compose/model/models.gen.go @@ -522,6 +522,15 @@ var Namespace = &dal.Model{ Store: &dal.CodecAlias{Ident: "meta"}, }, + &dal.Attribute{ + Ident: "Blocks", + Type: &dal.TypeJSON{ + HasDefault: true, + DefaultValue: "[]", + }, + Store: &dal.CodecAlias{Ident: "blocks"}, + }, + &dal.Attribute{ Ident: "Name", Sortable: true, Type: &dal.TypeText{}, @@ -1200,4 +1209,4 @@ func init() { Record, RecordRevision, ) -} +} \ No newline at end of file diff --git a/server/compose/namespace.cue b/server/compose/namespace.cue index dcfba7d91f..d767c64005 100644 --- a/server/compose/namespace.cue +++ b/server/compose/namespace.cue @@ -27,6 +27,18 @@ namespace: { omitSetter: true omitGetter: true } + blocks: { + goType: "types.GlobalBlocks" + dal: { type: "JSON", defaultEmptyObject: true } + omitSetter: true + omitGetter: true + envoy: { + yaml: { + customDecoder: true + customEncoder: true + } + } + } name: { sortable: true dal: {} diff --git a/server/compose/rest.yaml b/server/compose/rest.yaml index 271805e7bc..78e2006c86 100644 --- a/server/compose/rest.yaml +++ b/server/compose/rest.yaml @@ -73,6 +73,10 @@ endpoints: name: meta required: true title: Meta data + - type: sqlxTypes.JSONText + name: blocks + required: false + title: Blocks - name: read method: GET title: Read namespace diff --git a/server/compose/rest/namespace.go b/server/compose/rest/namespace.go index c399fc5f6a..1b01e43833 100644 --- a/server/compose/rest/namespace.go +++ b/server/compose/rest/namespace.go @@ -140,6 +140,14 @@ func (ctrl Namespace) Create(ctx context.Context, r *request.NamespaceCreate) (i 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(&ns.Blocks); err != nil { + return nil, err + } + } + ns, err = ctrl.namespace.Create(ctx, ns) return ctrl.makePayload(ctx, ns, err) } @@ -174,6 +182,14 @@ func (ctrl Namespace) Update(ctx context.Context, r *request.NamespaceUpdate) (i 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(&ns.Blocks); err != nil { + return nil, err + } + } + ns, err = ctrl.namespace.Update(ctx, ns) return ctrl.makePayload(ctx, ns, err) } diff --git a/server/compose/rest/request/namespace.go b/server/compose/rest/request/namespace.go index ec7d030ca0..43586e2ad0 100644 --- a/server/compose/rest/request/namespace.go +++ b/server/compose/rest/request/namespace.go @@ -100,6 +100,11 @@ type ( // // Meta data Meta sqlxTypes.JSONText + + // Blocks POST parameter + // + // Blocks + Blocks sqlxTypes.JSONText } NamespaceRead struct { @@ -135,6 +140,11 @@ type ( // Meta data Meta sqlxTypes.JSONText + // Blocks POST parameter + // + // Blocks + Blocks sqlxTypes.JSONText + // Labels POST parameter // // Labels @@ -380,6 +390,7 @@ func (r NamespaceCreate) Auditable() map[string]interface{} { "slug": r.Slug, "enabled": r.Enabled, "meta": r.Meta, + "blocks": r.Blocks, } } @@ -408,6 +419,11 @@ func (r NamespaceCreate) GetMeta() sqlxTypes.JSONText { return r.Meta } +// Auditable returns all auditable/loggable parameters +func (r NamespaceCreate) GetBlocks() sqlxTypes.JSONText { + return r.Blocks +} + // Fill processes request and fills internal variables func (r *NamespaceCreate) Fill(req *http.Request) (err error) { @@ -468,6 +484,13 @@ func (r *NamespaceCreate) Fill(req *http.Request) (err error) { return err } } + + if val, ok := req.MultipartForm.Value["blocks"]; ok && len(val) > 0 { + r.Blocks, err = payload.ParseJSONTextWithErr(val[0]) + if err != nil { + return err + } + } } } @@ -517,6 +540,13 @@ func (r *NamespaceCreate) Fill(req *http.Request) (err error) { return err } } + + if val, ok := req.Form["blocks"]; ok && len(val) > 0 { + r.Blocks, err = payload.ParseJSONTextWithErr(val[0]) + if err != nil { + return err + } + } } return err @@ -570,6 +600,7 @@ func (r NamespaceUpdate) Auditable() map[string]interface{} { "slug": r.Slug, "enabled": r.Enabled, "meta": r.Meta, + "blocks": r.Blocks, "labels": r.Labels, "updatedAt": r.UpdatedAt, } @@ -600,6 +631,11 @@ func (r NamespaceUpdate) GetMeta() sqlxTypes.JSONText { return r.Meta } +// Auditable returns all auditable/loggable parameters +func (r NamespaceUpdate) GetBlocks() sqlxTypes.JSONText { + return r.Blocks +} + // Auditable returns all auditable/loggable parameters func (r NamespaceUpdate) GetLabels() map[string]labelTypes.LabelValue { return r.Labels @@ -659,6 +695,13 @@ func (r *NamespaceUpdate) Fill(req *http.Request) (err error) { } } + if val, ok := req.MultipartForm.Value["blocks"]; ok && len(val) > 0 { + r.Blocks, err = payload.ParseJSONTextWithErr(val[0]) + if err != nil { + return err + } + } + if val, ok := req.MultipartForm.Value["labels[]"]; ok { r.Labels, err = label.ParseStrings(val) if err != nil { @@ -715,6 +758,13 @@ func (r *NamespaceUpdate) Fill(req *http.Request) (err error) { } } + if val, ok := req.Form["blocks"]; ok && len(val) > 0 { + r.Blocks, err = payload.ParseJSONTextWithErr(val[0]) + if err != nil { + return err + } + } + if val, ok := req.Form["labels[]"]; ok { r.Labels, err = label.ParseStrings(val) if err != nil { diff --git a/server/compose/service/namespace.go b/server/compose/service/namespace.go index 38a8476af3..96a8bb2a92 100644 --- a/server/compose/service/namespace.go +++ b/server/compose/service/namespace.go @@ -653,6 +653,37 @@ func (svc namespace) handleUpdate(ctx context.Context, upd *types.Namespace) nam res.Meta = upd.Meta } + // Get max blockID for later use + blockID := uint64(0) + for _, b := range res.Blocks { + if b.BlockID == blockID { + // assign ids on new page blocks + for i := range upd.Blocks { + upd.Blocks[i].BlockID = uint64(i) + 1 + } + } + + if b.BlockID > blockID { + blockID = b.BlockID + } + } + + if !reflect.DeepEqual(res.Blocks, upd.Blocks) { + res.Blocks = upd.Blocks + changes |= namespaceChanged + } + + // Assure blockIDs + for i, b := range res.Blocks { + if b.BlockID == 0 { + blockID++ + b.BlockID = blockID + res.Blocks[i] = b + + changes |= namespaceChanged + } + } + if upd.Labels != nil { if label.Changed(res.Labels, upd.Labels) { changes |= namespaceLabelsChanged diff --git a/server/compose/types/namespace.go b/server/compose/types/namespace.go index ea181286eb..509fd2e65a 100644 --- a/server/compose/types/namespace.go +++ b/server/compose/types/namespace.go @@ -16,6 +16,7 @@ type ( Slug string `json:"slug"` Enabled bool `json:"enabled"` Meta NamespaceMeta `json:"meta"` + Blocks GlobalBlocks `json:"blocks"` Labels map[string]labelTypes.LabelValue `json:"labels,omitempty"` @@ -29,6 +30,28 @@ type ( Name string `json:"name"` } + GlobalBlocks []GlobalBlock + + GlobalBlock struct { + BlockID uint64 `json:"blockID,string,omitempty"` + + Options map[string]interface{} `json:"options,omitempty"` + Style PageBlockStyle `json:"style,omitempty"` + Kind string `json:"kind"` + XYWH [4]int `json:"xywh"` // x,y,w,h + Meta map[string]any `json:"meta,omitempty"` + + // Warning: value of this field is now handled via resource-translation facility + // struct field is kept for the convenience for now since it allows us + // easy encoding/decoding of the outgoing/incoming values + Title string `json:"title,omitempty"` + + // Warning: value of this field is now handled via resource-translation facility + // struct field is kept for the convenience for now since it allows us + // easy encoding/decoding of the outgoing/incoming values + Description string `json:"description,omitempty"` + } + NamespaceFilter struct { NamespaceID []string `json:"namespaceID"` @@ -92,3 +115,11 @@ func (set NamespaceSet) FindByHandle(handle string) *Namespace { func (nm *NamespaceMeta) Scan(src any) error { return sql.ParseJSON(src, nm) } func (nm NamespaceMeta) Value() (driver.Value, error) { return json.Marshal(nm) } + +func (nm *GlobalBlocks) Scan(src any) error { return sql.ParseJSON(src, nm) } +func (nm GlobalBlocks) Value() (driver.Value, error) { + if nm == nil { + return "[]", nil + } + return json.Marshal(nm) +} diff --git a/server/store/adapters/rdbms/aux_types.gen.go b/server/store/adapters/rdbms/aux_types.gen.go index 96a72d66df..4da509c55c 100644 --- a/server/store/adapters/rdbms/aux_types.gen.go +++ b/server/store/adapters/rdbms/aux_types.gen.go @@ -279,6 +279,7 @@ type ( Slug string `db:"slug"` Enabled bool `db:"enabled"` Meta composeType.NamespaceMeta `db:"meta"` + Blocks composeType.GlobalBlocks `db:"blocks"` Name string `db:"name"` CreatedAt time.Time `db:"created_at"` UpdatedAt *time.Time `db:"updated_at"` @@ -1619,6 +1620,7 @@ func (aux *auxComposeNamespace) encode(res *composeType.Namespace) (_ error) { aux.Slug = res.Slug aux.Enabled = res.Enabled aux.Meta = res.Meta + aux.Blocks = res.Blocks aux.Name = res.Name aux.CreatedAt = res.CreatedAt aux.UpdatedAt = res.UpdatedAt @@ -1635,6 +1637,7 @@ func (aux auxComposeNamespace) decode() (res *composeType.Namespace, _ error) { res.Slug = aux.Slug res.Enabled = aux.Enabled res.Meta = aux.Meta + res.Blocks = aux.Blocks res.Name = aux.Name res.CreatedAt = aux.CreatedAt res.UpdatedAt = aux.UpdatedAt @@ -1651,6 +1654,7 @@ func (aux *auxComposeNamespace) scan(row scanner) error { &aux.Slug, &aux.Enabled, &aux.Meta, + &aux.Blocks, &aux.Name, &aux.CreatedAt, &aux.UpdatedAt, diff --git a/server/store/adapters/rdbms/queries.gen.go b/server/store/adapters/rdbms/queries.gen.go index 78e231e6d0..f4840a28cb 100644 --- a/server/store/adapters/rdbms/queries.gen.go +++ b/server/store/adapters/rdbms/queries.gen.go @@ -1869,6 +1869,7 @@ var ( "slug", "enabled", "meta", + "blocks", "name", "created_at", "updated_at", @@ -1886,6 +1887,7 @@ var ( "slug": res.Slug, "enabled": res.Enabled, "meta": res.Meta, + "blocks": res.Blocks, "name": res.Name, "created_at": res.CreatedAt, "updated_at": res.UpdatedAt, @@ -1906,6 +1908,7 @@ var ( "slug": res.Slug, "enabled": res.Enabled, "meta": res.Meta, + "blocks": res.Blocks, "name": res.Name, "created_at": res.CreatedAt, "updated_at": res.UpdatedAt, @@ -1924,6 +1927,7 @@ var ( "slug": res.Slug, "enabled": res.Enabled, "meta": res.Meta, + "blocks": res.Blocks, "name": res.Name, "created_at": res.CreatedAt, "updated_at": res.UpdatedAt, diff --git a/server/store/adapters/rdbms/upgrade_fixes.go b/server/store/adapters/rdbms/upgrade_fixes.go index 4ea62756b4..85d55ce662 100644 --- a/server/store/adapters/rdbms/upgrade_fixes.go +++ b/server/store/adapters/rdbms/upgrade_fixes.go @@ -55,6 +55,7 @@ var ( fix_2024_09_03_renameFederationNodeSyncComposeID, fix_2024_09_03_addFederationNodeSyncNodeIDIndex, fix_2024_9_7_migrateLabelsValueToJsonb, + fix_2024_09_07_migrateNamespacePageBlocks, } fixesPost = []func(context.Context, *Store) error{ @@ -1312,6 +1313,14 @@ func fix_2024_9_7_migrateLabelsValueToJsonb(ctx context.Context, s *Store) (err return nil } + +func fix_2024_09_07_migrateNamespacePageBlocks(ctx context.Context, s *Store) (err error) { + return addColumn(ctx, s, + "compose_namespace", + &dal.Attribute{Ident: "blocks", Type: &dal.TypeJSON{HasDefault: true, DefaultValue: "[]"}}, + ) +} + func count(ctx context.Context, s *Store, table string, ee ...goqu.Expression) (count int) { db := s.DB.(goqu.SQLDatabase) From a4d47cce94f5640cec4b2ca41dbe3b066d74e3ed Mon Sep 17 00:00:00 2001 From: Toocky Date: Wed, 4 Mar 2026 18:10:36 -0600 Subject: [PATCH 4/5] fixed bugs --- .../compose/src/views/Admin/Pages/Builder.vue | 3 ++- server/compose/model/models.gen.go | 3 ++- server/compose/rest/namespace.go | 8 ++++++ server/compose/rest/request/namespace.go | 25 +++++++++++++++++++ server/compose/types/namespace.go | 7 +++++- server/store/adapters/rdbms/upgrade_fixes.go | 2 +- 6 files changed, 44 insertions(+), 4 deletions(-) diff --git a/client/web/compose/src/views/Admin/Pages/Builder.vue b/client/web/compose/src/views/Admin/Pages/Builder.vue index a3c0be8af7..820cf7249f 100644 --- a/client/web/compose/src/views/Admin/Pages/Builder.vue +++ b/client/web/compose/src/views/Admin/Pages/Builder.vue @@ -381,6 +381,7 @@