Appearance
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.tsModify:
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 adefaults.tsor 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
selectedBlocksstate
In the BlocksState type and initial state, add:
typescript
selectedBlocks: string[] // array of blockNames for multi-selectInitialize as [].
- [ ] Step 2: Add
setSelectedBlocksandtoggleBlockSelectionmutations
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
groupBlocksmutation
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
ungroupBlocksmutation
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
moveBlockToGroupandmoveBlockOutOfGroupmutations
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
sortedBlocksgetter 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
isDeletablein 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
isGroupBlockhelper 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.tsModify:
Creative-Engine/src/utils/constants.ts(verify exact path — search forVISUAL_ELEMENTSin 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:
- Import path: DataStore is at
@/services/dataStore, not@/dataStore— verify the exact path in the codebase - DataStore refs: Use
DataStore.restartCount.value(DataStore uses Vue refs) - Mixins: Every engine block component uses
StyleAndClassNameGenerationMixinfor style registration. Thestyles()computed must useprependNamespaceSelectorConditionally()andtoClassSelector()— not raw CSS selectors. Copy the exact pattern fromCreativeGraphicBlock.vue - 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:
- Verify all import paths against the actual codebase
- Check which mixins
CreativeGraphicBlock.vueuses and replicate them - Replace the
styles()selector pattern with the engine'sprependNamespaceSelectorConditionally()/toClassSelector()pattern - Add
BlockMixinif needed forblockWithOverridesandblockVisible
- [ ] 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'
// #endRegister 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
visualElementscomputed 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.vueModify:
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.vueModify:
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 exactconfigs/subdirectory path — check whereFormConfiguration.vueandTextConfiguration.vuelive)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
- Open a creative in the builder
- Add 2+ visual element blocks (e.g., text + graphic)
- Cmd/Ctrl+click to select both
- Right-click → "Group"
- Verify: group appears in block list, children are indented
- Verify: group is collapsible
- Verify: clicking the group opens the config panel
- [ ] Step 2: Test engine rendering
- With grouped blocks, check the preview iframe
- Verify: a wrapper div exists with the CSS class from groupName
- Verify: child blocks render inside the wrapper
- Verify: ungrouped blocks render normally at top level
- [ ] Step 3: Test config panel
- Select a group in the block list
- Change the name → verify CSS class updates in preview
- Change width/height → verify the group div resizes
- Change alignment → verify children reposition
- [ ] Step 4: Test ungroup
- Right-click a group → "Ungroup"
- Verify: children return to top level in block list
- Verify: group block is removed
- Verify: engine renders children at top level again
- [ ] Step 5: Test drag in/out
- Drag an ungrouped block onto a group → verify it joins the group
- Drag a grouped block out to top level → verify it leaves the group
- [ ] Step 6: Test save and reload
- Create a group, save the creative
- Reload the page
- Verify: group structure is preserved (stored correctly in creativeBlob)
- [ ] Step 7: Test double midscroll use case
- Create a DoubleMidscrollSingleCreative
- Create two groups: "section-1" and "section-2"
- Add blocks to each group
- Use Script operator to translateY based on progress, targeting
.section-1and.section-2CSS classes - 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.