Skip to content

Block Grouping Implementation Plan

For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add a GROUP block type that wraps child blocks in a named, positionable <div>, enabling scroll-sections, custom CSS targeting, and future animation grouping.

Architecture: New groupProperties block type using the existing parent field and sub-block storage pattern (children stored as properties of the parent block object). Engine renders a CreativeGroupBlock component that wraps children in a styled div. Builder adds multi-select and group/ungroup actions.

Tech Stack: Vue 2 (Options API), Vuex, Creative-Engine style system (computed styles() objects processed by StyleTreeParser)

Spec: Cavai-Documentation/src/DocumentationTexts/todos/BlockGrouping/feasibility.md


Chunk 1: Data Model & Store

Task 1: Add GROUP constant and type

Files:

  • Modify: Application-Frontend/src/constants/blocks.ts

  • Modify: Application-Frontend/src/pages/Chatbots/components/BuilderVisuals/Blocks/data/types.ts

  • [ ] Step 1: Add GROUP constant to blocks.ts

In src/constants/blocks.ts, add GROUP to the BLOCKS object:

typescript
export const BLOCKS = {
  // ... existing entries ...
  GROUP: 'groupProperties',
}

Do NOT add GROUP to VISUAL_ELEMENTS — groups are containers, not visual elements.

  • [ ] Step 2: Add GroupProperties type to types.ts

In src/pages/Chatbots/components/BuilderVisuals/Blocks/data/types.ts, add after the existing block type definitions:

typescript
export type GroupProperties = BlockBase & {
  blockType: 'groupProperties'
  groupName: string
  width: string
  height: string
  top: string
  left: string
  position: string
  justifyContent: string
  alignItems: string
  allowedSubBlocks: string[]
  subBlocksReorderable: boolean
}

Add GroupProperties to the CreativeBlocks union type if one exists, or to any block type union used by the store.

  • [ ] Step 3: Commit
bash
git add src/constants/blocks.ts src/pages/Chatbots/components/BuilderVisuals/Blocks/data/types.ts
git commit -m "feat: add GROUP block type constant and GroupProperties type"

Task 2: Add group defaults

Files:

  • Modify: Application-Frontend/src/pages/Chatbots/components/BuilderVisuals/Blocks/data/types.ts (or the file where block defaults live — check if there's a defaults.ts or if defaults are inline in the store)

  • [ ] Step 1: Find where block defaults are defined

Search for how other blocks define their defaults (e.g., search for formProperties default object creation in blocks.ts store or a dedicated defaults file). The addBlock mutation in the store creates blocks with defaults — check how it determines default values for each block type.

  • [ ] Step 2: Add group block defaults

Add a default factory for group blocks, following the existing pattern. The defaults should be:

typescript
{
  blockType: BLOCKS.GROUP,
  groupName: '',           // Set dynamically: "Group 1", "Group 2", etc.
  displayName: '',         // Set dynamically: same as groupName
  order: 0,                // Set dynamically by addBlock
  hidden: false,
  locked: false,
  width: '100%',
  height: '100%',
  top: '0',
  left: '0',
  position: 'relative',
  justifyContent: 'flex-start',
  alignItems: 'flex-start',
  allowedSubBlocks: ['*'],
  subBlocksReorderable: true,
}
  • [ ] Step 3: Commit
bash
git commit -m "feat: add default values for group block type"

Task 3: Add group/ungroup store mutations

Files:

  • Modify: Application-Frontend/src/store/modules/blocks.ts

  • [ ] Step 1: Add selectedBlocks state

In the BlocksState type and initial state, add:

typescript
selectedBlocks: string[]  // array of blockNames for multi-select

Initialize as [].

  • [ ] Step 2: Add setSelectedBlocks and toggleBlockSelection mutations
typescript
setSelectedBlocks(state: BlocksState, blockNames: string[]) {
  state.selectedBlocks = blockNames
},

toggleBlockSelection(state: BlocksState, blockName: string) {
  const index = state.selectedBlocks.indexOf(blockName)
  if (index === -1) {
    state.selectedBlocks.push(blockName)
  } else {
    state.selectedBlocks.splice(index, 1)
  }
},

clearSelectedBlocks(state: BlocksState) {
  state.selectedBlocks = []
},
  • [ ] Step 3: Add groupBlocks mutation

This mutation creates a new group block and moves selected blocks into it as sub-block properties:

typescript
groupBlocks(state: BlocksState, { blockNames, groupName }: { blockNames: string[], groupName: string }) {
  const blocks = state.creativeBlocks

  // Generate unique group blockName (e.g., "groupProperties-1")
  const groupId = uniqueBlockId(blocks, BLOCKS.GROUP)
  const groupBlockName = `${BLOCKS.GROUP}-${groupId}`

  // Create the group block with defaults
  const groupBlock = {
    blockName: groupBlockName,
    blockType: BLOCKS.GROUP,
    displayName: groupName,
    groupName: groupName,
    order: 0, // Will be set below
    hidden: false,
    locked: false,
    width: '100%',
    height: '100%',
    top: '0',
    left: '0',
    position: 'relative',
    justifyContent: 'flex-start',
    alignItems: 'flex-start',
    allowedSubBlocks: ['*'],
    subBlocksReorderable: true,
  }

  // Move selected blocks into the group as properties
  let childOrder = 0
  blockNames.forEach((blockName) => {
    const block = blocks[blockName]
    if (block) {
      block.parent = groupBlockName
      block.order = childOrder++
      // Move block from top-level to group property
      Vue.set(groupBlock, blockName, block)
      Vue.delete(blocks, blockName)
    }
  })

  // Set group order (use the highest order among moved blocks' original positions, or next available)
  groupBlock.order = Object.keys(blocks).length

  // Add group to top-level blocks
  Vue.set(blocks, groupBlockName, groupBlock)
}

Important: Uses Vue.set and Vue.delete for reactivity. This follows the same pattern as how form inputs are nested inside formProperties.

  • [ ] Step 4: Add ungroupBlocks mutation
typescript
ungroupBlocks(state: BlocksState, groupBlockName: string) {
  const blocks = state.creativeBlocks
  const groupBlock = blocks[groupBlockName]
  if (!groupBlock) return

  // Move children back to top-level
  const children = getSubBlocks(groupBlock)
  children.forEach((child) => {
    delete child.parent
    Vue.set(blocks, child.blockName, child)
    Vue.delete(groupBlock, child.blockName)
  })

  // Remove the group block
  Vue.delete(blocks, groupBlockName)
}
  • [ ] Step 5: Add moveBlockToGroup and moveBlockOutOfGroup mutations

For drag-in/out support:

typescript
moveBlockToGroup(state: BlocksState, { blockName, groupBlockName }: { blockName: string, groupBlockName: string }) {
  const blocks = state.creativeBlocks
  const block = blocks[blockName]
  const groupBlock = blocks[groupBlockName]
  if (!block || !groupBlock) return

  block.parent = groupBlockName
  Vue.set(groupBlock, blockName, block)
  Vue.delete(blocks, blockName)
},

moveBlockOutOfGroup(state: BlocksState, { blockName, groupBlockName }: { blockName: string, groupBlockName: string }) {
  const blocks = state.creativeBlocks
  const groupBlock = blocks[groupBlockName]
  if (!groupBlock || !groupBlock[blockName]) return

  const block = groupBlock[blockName]
  delete block.parent
  Vue.set(blocks, blockName, block)
  Vue.delete(groupBlock, blockName)
},
  • [ ] Step 6: Update sortedBlocks getter to include group blocks

The existing sortedBlocks getter sorts top-level blocks. Verify that group blocks (which are top-level) are included. They should be, since they're properties of creativeBlocks. No change may be needed, but verify.

  • [ ] Step 7: Update isDeletable in utils.ts

In src/pages/Chatbots/components/BuilderVisuals/Blocks/utils.ts, add GROUP to the deletable check. GROUP blocks have indexed names like 'groupProperties-1', so use includes() (same pattern as FORM_INPUT):

typescript
if (blockName.includes(BLOCKS.GROUP)) return true
  • [ ] Step 8: Add isGroupBlock helper to utils.ts
typescript
export const isGroupBlock = (blockName: string): boolean => {
  if (!blockName) return false
  return blockName.includes(BLOCKS.GROUP)
}

export const isGroupChild = (block: any, creativeBlocks: any): boolean => {
  if (!block?.parent) return false
  return isGroupBlock(block.parent)
}
  • [ ] Step 9: Commit
bash
git add src/store/modules/blocks.ts src/pages/Chatbots/components/BuilderVisuals/Blocks/utils.ts
git commit -m "feat: add group/ungroup store mutations and multi-select state"

Chunk 2: Engine Rendering

Task 4: Add isGroupBlock and isGroupChild to engine utils

Files:

  • Modify: Creative-Engine/src/utils/blockUtils.ts

  • Modify: Creative-Engine/src/utils/constants.ts (verify exact path — search for VISUAL_ELEMENTS in the engine)

  • [ ] Step 1: Add GROUP to engine block constants

Find where BLOCKS constants are defined in Creative-Engine (search for VISUAL_ELEMENTS or the existing block constant definitions). Add:

typescript
GROUP: 'groupProperties',
  • [ ] Step 2: Add helper functions to blockUtils.ts
typescript
export const isGroupBlock = (blockName: string): boolean => {
  return blockName?.includes(BLOCKS.GROUP) ?? false
}

export const isGroupChild = (block: any): boolean => {
  return block?.parent ? isGroupBlock(block.parent) : false
}
  • [ ] Step 3: Commit
bash
git add src/utils/blockUtils.ts src/utils/constants.ts
git commit -m "feat: add group block helpers to engine utils"

Task 5: Create CreativeGroupBlock component

Files:

  • Create: Creative-Engine/src/components/creative/CreativeGroupBlock/CreativeGroupBlock.vue

  • [ ] Step 1: Create the component

Follow the pattern of existing block components (e.g., CreativeGraphicBlock.vue). The group block renders a wrapper div with:

  • CSS class from groupName (sanitized)
  • Generated block class names (from getBlockClassNamesObject)
  • Position, size, and flex styles
  • Child blocks rendered by type inside

The code below is a starting point. Critical: you MUST adapt it to match the exact engine patterns before it will work. Specifically:

  1. Import path: DataStore is at @/services/dataStore, not @/dataStore — verify the exact path in the codebase
  2. DataStore refs: Use DataStore.restartCount.value (DataStore uses Vue refs)
  3. Mixins: Every engine block component uses StyleAndClassNameGenerationMixin for style registration. The styles() computed must use prependNamespaceSelectorConditionally() and toClassSelector() — not raw CSS selectors. Copy the exact pattern from CreativeGraphicBlock.vue
  4. Constants path: Engine constants are at @/utils/constants, not @/constants — verify
vue
<template>
  <div :class="[blockClassNames.wrap, sanitizedGroupName]">
    <template v-for="block in childTextBlocks">
      <creative-text-block :key="`${block.blockName}-${restartCount}`" :block="block" />
    </template>
    <template v-for="block in childGraphicBlocks">
      <creative-graphic-block :key="`${block.blockName}-${restartCount}`" :block="block" />
    </template>
    <template v-for="block in childButtonBlocks">
      <creative-button-block :key="`${block.blockName}-${restartCount}`" :block="block" />
    </template>
    <template v-for="block in childHtmlBlocks">
      <creative-html-block :key="`${block.blockName}-${restartCount}`" :block="block" />
    </template>
  </div>
</template>

<script>
import { defineComponent } from 'vue'
import { DataStore } from '@/services/dataStore'              // VERIFY exact path
import { getBlockClassNamesObject, getSubBlocks } from '@/utils/blockUtils'
import { BLOCKS } from '@/utils/constants'                    // VERIFY exact path
import StyleAndClassNameGenerationMixin from '@/mixins/StyleAndClassNameGenerationMixin' // VERIFY
import CreativeTextBlock from '@/components/creative/VisualElements/CreativeTextBlock.vue'
import CreativeGraphicBlock from '@/components/creative/VisualElements/CreativeGraphicBlock.vue'
import CreativeButtonBlock from '@/components/creative/VisualElements/CreativeButtonBlock.vue'
import CreativeHtmlBlock from '@/components/creative/VisualElements/CreativeHtmlBlock.vue'

export default defineComponent({
  name: 'CreativeGroupBlock',
  mixins: [StyleAndClassNameGenerationMixin],  // REQUIRED for style registration
  components: { CreativeTextBlock, CreativeGraphicBlock, CreativeButtonBlock, CreativeHtmlBlock },
  props: {
    block: { type: Object, required: true },
  },
  computed: {
    restartCount() {
      return DataStore.restartCount.value      // .value for Vue ref
    },
    blockClassNames() {
      return getBlockClassNamesObject(this.block.blockName)
    },
    sanitizedGroupName() {
      return (this.block.groupName || '')
        .toLowerCase()
        .replace(/[^a-z0-9-_\s]/g, '')
        .replace(/\s+/g, '-')
        .trim()
    },
    childBlocks() {
      return getSubBlocks(this.block)
    },
    childTextBlocks() {
      return this.childBlocks.filter((b) => b.blockType === BLOCKS.TEXT)
    },
    childGraphicBlocks() {
      return this.childBlocks.filter((b) => b.blockType === BLOCKS.GRAPHIC)
    },
    childButtonBlocks() {
      return this.childBlocks.filter((b) => b.blockType === BLOCKS.BUTTON)
    },
    childHtmlBlocks() {
      return this.childBlocks.filter((b) => b.blockType === BLOCKS.HTML)
    },
    styles() {
      // IMPORTANT: Copy the exact selector pattern from CreativeGraphicBlock.vue
      // This placeholder uses raw selectors — replace with prependNamespaceSelectorConditionally()
      const b = this.block
      return {
        [`.${this.blockClassNames.wrap}`]: {
          position: b.position || 'relative',
          width: b.width || '100%',
          height: b.height || '100%',
          top: b.top || '0',
          left: b.left || '0',
          display: 'flex',
          justifyContent: b.justifyContent || 'flex-start',
          alignItems: b.alignItems || 'flex-start',
          boxSizing: 'border-box',
          overflow: 'hidden',
        },
      }
    },
  },
})
</script>
  • [ ] Step 2: Adapt to exact engine patterns

Before this component will render, you MUST:

  1. Verify all import paths against the actual codebase
  2. Check which mixins CreativeGraphicBlock.vue uses and replicate them
  3. Replace the styles() selector pattern with the engine's prependNamespaceSelectorConditionally() / toClassSelector() pattern
  4. Add BlockMixin if needed for blockWithOverrides and blockVisible
  • [ ] Step 3: Commit
bash
git add src/components/creative/CreativeGroupBlock/
git commit -m "feat: add CreativeGroupBlock engine component"

Task 6: Render group blocks in CreativeBody.vue

Files:

  • Modify: Creative-Engine/src/components/conversationflow/CreativeBody.vue

  • [ ] Step 1: Add conditional import with tree-shaking guard

Add alongside the other #include guarded imports:

typescript
// #include when blocks have "groupProperties"
import CreativeGroupBlock from '@/components/creative/CreativeGroupBlock/CreativeGroupBlock.vue'
// #end

Register the component in the components object.

  • [ ] Step 2: Add group blocks to the template

Add before or after <CreativeVisualElements /> in the template:

vue
<!-- #include when blocks have "groupProperties" -->
<template v-for="groupBlock in groupBlocks">
  <CreativeGroupBlock
    v-if="blockVisible(groupBlock.blockName)"
    :key="groupBlock.blockName"
    :block="groupBlock"
    :style="{ order: groupBlock.order }"
  />
</template>
<!-- #end -->
  • [ ] Step 3: Add groupBlocks computed
typescript
groupBlocks() {
  const { creativeBlocks } = DataStore?.creativeSettings
  if (!creativeBlocks) return []
  return Object.values(creativeBlocks).filter(
    (block: any) => block?.blockName?.includes('groupProperties') && !block.hidden
  )
},
  • [ ] Step 4: Commit
bash
git add src/components/conversationflow/CreativeBody.vue
git commit -m "feat: render group blocks in CreativeBody with tree-shaking guard"

Task 7: Filter group children from top-level visual elements

Files:

  • Modify: Creative-Engine/src/components/creative/VisualElements/CreativeVisualElements.vue

  • [ ] Step 1: Update the visualElements computed filter

Import isGroupChild and add it to the filter:

typescript
import { isVisualElement, isGroupChild } from '@/utils/blockUtils'

// In computed:
visualElements(): Partial<CreativeBlocks> {
  const { creativeBlocks } = DataStore.creativeSettings
  const callback = (block: any) => isVisualElement(block.blockName) && !block.hidden && !isGroupChild(block)
  return pickBy(creativeBlocks, callback)
},

This ensures visual elements with a group parent are NOT rendered at the top level — they're rendered inside their CreativeGroupBlock instead.

Important: isGroupChild specifically checks if block.parent contains 'groupProperties', so it won't accidentally filter out form inputs, slider sub-blocks, or expandable sub-blocks which also use the parent field.

  • [ ] Step 2: Verify existing sub-blocks are unaffected

Confirm that form inputs, slider slides, and expandable sub-blocks continue to render correctly. Their parent values are 'formProperties', 'sliderProperties', 'expandableInitialProperties' etc. — none of which match the 'groupProperties' check.

  • [ ] Step 3: Commit
bash
git add src/components/creative/VisualElements/CreativeVisualElements.vue
git commit -m "feat: filter group children from top-level visual elements rendering"

Chunk 3: Builder UI

Task 8: Multi-select in block list

Files:

  • Modify: Application-Frontend/src/pages/Chatbots/components/BuilderVisuals/Blocks/BlockItem.vue

  • Modify: Application-Frontend/src/pages/Chatbots/components/BuilderVisuals/Blocks/BlocksList.vue

  • [ ] Step 1: Update BlockItem click handler for multi-select

In BlockItem.vue, update the onBlockClick method to support Cmd/Ctrl+click:

typescript
onBlockClick(event) {
  if (this.renaming) return

  if (event.metaKey || event.ctrlKey) {
    // Multi-select: toggle this block in selectedBlocks
    this.$store.commit('blocks/toggleBlockSelection', this.block.blockName)
  } else {
    // Normal click: single select (existing behavior)
    this.$store.commit('blocks/clearSelectedBlocks')
    this.setSelectedBlockPath(this.path)
  }
},
  • [ ] Step 2: Add multi-select visual state to BlockItem

Add computed property:

typescript
isMultiSelected() {
  return this.$store.state.blocks.selectedBlocks.includes(this.block.blockName)
},

Add class binding in template:

vue
:class="[{ 'multi-selected': isMultiSelected, ... }]"

Add CSS for .multi-selected state (a subtle highlight, e.g., outline or background tint).

  • [ ] Step 3: Clear multi-select on single click and escape

In BlocksList.vue, clear selectedBlocks when clicking empty space or pressing Escape. Add a click handler on the list container and a keydown listener.

  • [ ] Step 4: Commit
bash
git add src/pages/Chatbots/components/BuilderVisuals/Blocks/BlockItem.vue
git add src/pages/Chatbots/components/BuilderVisuals/Blocks/BlocksList.vue
git commit -m "feat: add multi-select support to block list (Cmd/Ctrl+click)"

Task 9: Group/Ungroup context menu actions

Files:

  • Modify: Application-Frontend/src/pages/Chatbots/components/BuilderVisuals/Blocks/BlocksList.vue

  • [ ] Step 1: Add "Group" option to context menu

The existing context menu has a "Paste" option. Add a "Group" option that appears when 2+ blocks are multi-selected:

vue
<v-list-item
  v-if="selectedBlocks.length >= 2"
  @click="groupSelectedBlocks"
>
  {{ $t('visuals.blocks.group') }}
</v-list-item>
  • [ ] Step 2: Add "Ungroup" option for group blocks

When right-clicking a group block:

vue
<v-list-item
  v-if="isGroupBlock(contextMenuBlock)"
  @click="ungroupBlock(contextMenuBlock)"
>
  {{ $t('visuals.blocks.ungroup') }}
</v-list-item>
  • [ ] Step 3: Implement groupSelectedBlocks method
typescript
groupSelectedBlocks() {
  const blockNames = this.$store.state.blocks.selectedBlocks
  const groupNumber = uniqueBlockId(this.creativeBlocks, BLOCKS.GROUP)
  const groupName = `Group ${groupNumber}`

  this.$store.commit('blocks/groupBlocks', { blockNames, groupName })
  this.$store.commit('blocks/clearSelectedBlocks')
},
  • [ ] Step 4: Implement ungroupBlock method
typescript
ungroupBlock(block) {
  this.$store.commit('blocks/ungroupBlocks', block.blockName)
},
  • [ ] Step 5: Add keyboard shortcut Cmd+G

Add keydown listener for Cmd+G / Ctrl+G that triggers groupSelectedBlocks when 2+ blocks are selected.

  • [ ] Step 6: Add i18n keys

In src/assets/i18n/en.js, under the visuals.blocks section:

javascript
group: 'Group',
ungroup: 'Ungroup',
  • [ ] Step 7: Commit
bash
git add src/pages/Chatbots/components/BuilderVisuals/Blocks/BlocksList.vue src/assets/i18n/en.js
git commit -m "feat: add Group/Ungroup context menu actions and Cmd+G shortcut"

Task 10: Render groups as collapsible items in block list

Files:

  • Modify: Application-Frontend/src/pages/Chatbots/components/BuilderVisuals/Blocks/BlocksList.vue

  • Modify: Application-Frontend/src/pages/Chatbots/components/BuilderVisuals/Blocks/BlockItem.vue

  • [ ] Step 1: Update BlocksList to render group children nested

The existing template already renders sub-blocks in a nested <ul> using getSubBlocks(block). Since group blocks use the same sub-block storage pattern (children as properties of the parent object), getSubBlocks should already pick them up. Verify this works.

If getSubBlocks returns group children correctly, the existing template structure should render them indented automatically. The key lines in BlocksList.vue:

vue
<ul :class="[{ 'sub-blocks': !block.locked }]">
  <li v-for="(subBlock, subIndex) in getSubBlocks(block)">
    <BlockItem :block="subBlock" :path="`${block.blockName}.${subBlock.blockName}`" is-sub-block />
  </li>
</ul>

This should work for group blocks out of the box — verify.

  • [ ] Step 2: Add collapse/expand toggle to group BlockItem

In BlockItem.vue, add a collapse toggle arrow for group blocks (same pattern as form blocks if they have one, or add new). Add local state:

typescript
data() {
  return {
    collapsed: false,
  }
},

Add toggle button in template (before the block name):

vue
<span v-if="isGroupBlock" class="collapse-toggle" @click.stop="collapsed = !collapsed">
  {{ collapsed ? '▸' : '▾' }}
</span>

Add computed:

typescript
isGroupBlock() {
  return this.block.blockName?.includes(BLOCKS.GROUP)
},

Emit collapsed state to parent so the sub-blocks <ul> can be hidden with v-show="!collapsed".

  • [ ] Step 3: Style the group item distinctly

Add CSS to visually distinguish group items (e.g., slightly different icon, bold name, or subtle background).

  • [ ] Step 4: Commit
bash
git add src/pages/Chatbots/components/BuilderVisuals/Blocks/BlocksList.vue
git add src/pages/Chatbots/components/BuilderVisuals/Blocks/BlockItem.vue
git commit -m "feat: render groups as collapsible items in block list"

Task 11: Drag blocks in/out of groups

Files:

  • Modify: Application-Frontend/src/pages/Chatbots/components/BuilderVisuals/Blocks/BlocksList.vue

  • [ ] Step 1: Allow dropping blocks onto a group header

When a block is dragged over a group item (not a sort area between blocks), show a "drop into group" visual indicator. On drop, call moveBlockToGroup mutation.

The existing SortArea components handle drops between blocks. Add an additional drop zone on the group BlockItem itself:

typescript
// In BlockItem.vue, for group blocks:
@dragover.prevent="onGroupDragOver"
@drop="onGroupDrop"
typescript
onGroupDrop(event) {
  if (!this.isGroupBlock) return
  const draggedBlock = this.$parent.draggingBlock
  if (draggedBlock && draggedBlock.blockName !== this.block.blockName) {
    this.$store.commit('blocks/moveBlockToGroup', {
      blockName: draggedBlock.blockName,
      groupBlockName: this.block.blockName,
    })
  }
},
  • [ ] Step 2: Allow dragging a sub-block out of a group

When a group child is dragged to a sort area outside the group, call moveBlockOutOfGroup mutation. This should be handled in the existing drop logic — when a sub-block is dropped at a top-level sort area, detect the parent change and call the appropriate mutation.

  • [ ] Step 3: Commit
bash
git add src/pages/Chatbots/components/BuilderVisuals/Blocks/BlocksList.vue
git add src/pages/Chatbots/components/BuilderVisuals/Blocks/BlockItem.vue
git commit -m "feat: support dragging blocks in/out of groups"

Chunk 4: Config Panel

Task 12: Create GroupConfiguration component

Files:

  • Create: Application-Frontend/src/pages/Chatbots/components/BuilderVisuals/Configuration/configs/GroupConfiguration.vue (verify the exact configs/ subdirectory path — check where FormConfiguration.vue and TextConfiguration.vue live)

  • Modify: The parent component that switches between config panels based on selected block type

  • [ ] Step 1: Find how config panels are switched

Search for where config components are selected based on selectedBlockPath / block type. There should be a parent component that conditionally renders FormConfiguration, SliderConfiguration, TextConfiguration, etc. This is where GroupConfiguration needs to be registered.

  • [ ] Step 2: Create GroupConfiguration component

Follow the pattern of existing config components (e.g., FormConfiguration.vue or a simpler one). Use the configurationLogic mixin:

vue
<template>
  <div v-if="blockData" class="group-configuration">
    <div class="config-section">
      <OptionRow :label="$t('visuals.blocks.groupName')">
        <InputField
          :value="blockData.groupName"
          :disabled="inputLocked"
          @input="updateValue($event, 'groupName')"
        />
      </OptionRow>
    </div>

    <div class="config-section">
      <OptionRow :label="$t('visuals.blocks.width')">
        <InputField
          :value="blockData.width"
          :disabled="inputLocked"
          @input="updateValue($event, 'width')"
        />
      </OptionRow>
      <OptionRow :label="$t('visuals.blocks.height')">
        <InputField
          :value="blockData.height"
          :disabled="inputLocked"
          @input="updateValue($event, 'height')"
        />
      </OptionRow>
    </div>

    <div class="config-section">
      <OptionRow :label="$t('visuals.blocks.top')">
        <InputField
          :value="blockData.top"
          :disabled="inputLocked"
          @input="updateValue($event, 'top')"
        />
      </OptionRow>
      <OptionRow :label="$t('visuals.blocks.left')">
        <InputField
          :value="blockData.left"
          :disabled="inputLocked"
          @input="updateValue($event, 'left')"
        />
      </OptionRow>
    </div>

    <AlignmentSection
      :value="{ justifyContent: blockData.justifyContent, alignItems: blockData.alignItems }"
      @update:justifyContent="updateValue($event, 'justifyContent')"
      @update:alignItems="updateValue($event, 'alignItems')"
    />
  </div>
</template>

<script>
import { configurationLogic } from './mixins/configurationLogic'

export default {
  name: 'GroupConfiguration',
  mixins: [configurationLogic],
  // components: register InputField, OptionRow, AlignmentSection
}
</script>

Note: Check how AlignmentSection works — it may accept different props. Look at how it's used in other config components and replicate. If AlignmentSection doesn't exist as a reusable component, check how flex alignment is configured in other blocks (likely via justifyContent and alignItems dropdowns using InputSelect or OptionList).

  • [ ] Step 3: Register GroupConfiguration in the config panel switcher

Add the group config to whatever component switches between config panels. Add a condition like:

typescript
if (selectedBlockName.includes(BLOCKS.GROUP)) {
  return 'GroupConfiguration'
}
  • [ ] Step 4: Add i18n keys

In src/assets/i18n/en.js:

javascript
groupName: 'Name',
width: 'Width',
height: 'Height',
top: 'Top',
left: 'Left',

Check if width, height, top, left already have i18n keys (they're common properties) — reuse if they exist.

  • [ ] Step 5: Add sectionSettings entry for GroupConfiguration

In src/pages/Chatbots/components/BuilderVisuals/Blocks/utils.ts, add to sectionSettings:

typescript
GroupName: ['groupName'],
GroupSize: ['width', 'height'],
GroupPosition: ['top', 'left'],
GroupAlignment: ['justifyContent', 'alignItems'],

This is needed if using sectionLogic mixin in sub-sections. If the config is built without section sub-components, this step can be skipped.

  • [ ] Step 6: Commit
bash
git add src/pages/Chatbots/components/BuilderVisuals/Configuration/configs/GroupConfiguration.vue
git commit -m "feat: add group block configuration panel"

Chunk 5: Integration & Polish

Task 13: End-to-end verification

  • [ ] Step 1: Test group creation in builder
  1. Open a creative in the builder
  2. Add 2+ visual element blocks (e.g., text + graphic)
  3. Cmd/Ctrl+click to select both
  4. Right-click → "Group"
  5. Verify: group appears in block list, children are indented
  6. Verify: group is collapsible
  7. Verify: clicking the group opens the config panel
  • [ ] Step 2: Test engine rendering
  1. With grouped blocks, check the preview iframe
  2. Verify: a wrapper div exists with the CSS class from groupName
  3. Verify: child blocks render inside the wrapper
  4. Verify: ungrouped blocks render normally at top level
  • [ ] Step 3: Test config panel
  1. Select a group in the block list
  2. Change the name → verify CSS class updates in preview
  3. Change width/height → verify the group div resizes
  4. Change alignment → verify children reposition
  • [ ] Step 4: Test ungroup
  1. Right-click a group → "Ungroup"
  2. Verify: children return to top level in block list
  3. Verify: group block is removed
  4. Verify: engine renders children at top level again
  • [ ] Step 5: Test drag in/out
  1. Drag an ungrouped block onto a group → verify it joins the group
  2. Drag a grouped block out to top level → verify it leaves the group
  • [ ] Step 6: Test save and reload
  1. Create a group, save the creative
  2. Reload the page
  3. Verify: group structure is preserved (stored correctly in creativeBlob)
  • [ ] Step 7: Test double midscroll use case
  1. Create a DoubleMidscrollSingleCreative
  2. Create two groups: "section-1" and "section-2"
  3. Add blocks to each group
  4. Use Script operator to translateY based on progress, targeting .section-1 and .section-2 CSS classes
  5. Verify scroll between sections works

Task 14: Nesting stretch goal (optional)

  • [ ] Step 1: Test if nesting works out of the box

Try dragging a group into another group. Since allowedSubBlocks: ['*'] allows all block types, and the parent field + sub-block storage pattern is recursive, it might just work.

Check:

  • Does the block list render nested groups with correct indentation?

  • Does the engine render nested group divs?

  • Does ungroup work correctly for inner groups?

  • [ ] Step 2: If it works, commit. If not, document what breaks.

bash
git commit -m "feat: verify group nesting support"

If nesting requires significant additional work, skip and note in the spec as a future task.

Internal documentation