Skip to content

Block Grouping System

How GROUP blocks work across Application-Frontend (AF) and Creative-Engine (CE).

Overview

GROUP blocks wrap child visual elements in a positionable flex container. Use cases: scroll-sections for double midscroll, custom CSS targeting via group name, layout control with flex alignment.

Storage Pattern

In AF (runtime -- flat model)

AF uses a flat data model: group children are top-level entries in creativeBlocks with a parent field pointing to their group. This avoids the dual-data-model bugs that plagued earlier implementations (syncGroupChildren).

creativeBlocks: {
  baseProperties: { ... },
  blockGroupProperties-1: {
    blockName: 'blockGroupProperties-1',
    blockType: 'blockGroupProperties',
    displayName: 'Block Group',
    order: 'a0',               // fractional string key
    groupName: 'Block Group',
    allowedSubBlocks: ['textProperties', 'buttonProperties', 'graphicProperties', 'videoProperties', 'htmlProperties'],
    // NO children stored here in AF runtime
  },
  textProperties-1: {
    blockName: 'textProperties-1',
    parent: 'blockGroupProperties-1',   // child of group
    order: 'a0',                        // fractional order within group
    ...
  },
  buttonProperties-1: {
    blockName: 'buttonProperties-1',
    parent: 'blockGroupProperties-1',
    order: 'a1',
    ...
  },
}

In CE/Backend (serialized -- nested model)

CE and backend expect children nested inside the group object with integer orders. serializeOrdersForSave() converts before save/preview:

creativeBlocks: {
  blockGroupProperties-1: {
    blockName: 'blockGroupProperties-1',
    blockType: 'blockGroupProperties',
    order: 0,                          // integer
    textProperties-1: { blockName: 'textProperties-1', parent: 'blockGroupProperties-1', order: 0, ... },
    buttonProperties-1: { blockName: 'buttonProperties-1', parent: 'blockGroupProperties-1', order: 1, ... },
  }
}

Data Flow (load/save boundary)

Backend JSON (nested, integers)
  --> setCreativeBlocks()
    --> migrateNumericOrders()      // integers -> fractional string keys
    --> promoteGroupChildren()      // nested children -> top-level with parent field
    --> AF runtime (flat, fractional)

AF runtime (flat, fractional)
  --> serializeOrdersForSave()     // flat -> nested, fractional -> integers
  --> Backend/CE (nested, integers)

Key points:

  • Fractional indexing is AF-internal only. CE and backend never see fractional keys.
  • promoteGroupChildren() clones children before promoting to avoid mutating input.
  • serializeOrdersForSave() deep-clones via JSON.parse(JSON.stringify(...)) so it is non-destructive.

Ordering System

Block ordering uses the fractional-indexing npm library. See src/utils/orderUtils.ts.

FunctionPurpose
insertBetween(above, below)Key between two neighbors
insertAfter(lastKey)Key after last item (append)
insertBefore(firstKey)Key before first item (prepend)
insertNBetween(above, below, n)N evenly-spaced keys
compareOrder(a, b)Sort comparator for fractional keys
migrateNumericOrders(blocks)Legacy integer -> fractional migration
promoteGroupChildren(blocks)Nested group children -> flat top-level
serializeOrdersForSave(blocks)Flat/fractional -> nested/integer for save

Engine Rendering

Wrap + container + inner approach:

html
<div class="gp1-wrap">             <!-- flexbox positioning (alignment, z-index) -->
  <div class="gp-block gp1">       <!-- styling (size, bg, border, shadow, animation) -->
    <div class="gp-inner">         <!-- padding inset container -->
      <CreativeTextBlock />
      <CreativeButtonBlock />
    </div>
  </div>
</div>

CSS override for children -- child visual elements normally render with position: absolute wraps. The group overrides this via CSS specificity:

javascript
// Group styles():
[groupSelector + ' > div']: {         // Override children's wraps
  position: 'relative',
  width: 'auto',
  height: 'auto',
  pointerEvents: 'auto',
},
[groupSelector + ' > div > div']: {   // Override children's inner blocks
  position: 'relative',
},

This works because .gp-block.gp1 > .gp-inner > div beats .t1-wrap in specificity. Children participate in flex flow without modifying their own components.

Filtering -- CreativeVisualElements.vue filters out group children with !isGroupChild(block) to prevent double-rendering. CreativeBody.vue renders group blocks separately with a tree-shaking #include guard.

Important: The #include annotation must match the actual block key: #include when blocks have "blockGroupProperties" (not "groupProperties").

Class Name Prefix

Groups use gp (not g) to avoid clashing with graphic blocks:

  • graphicProperties-1 -> g1
  • blockGroupProperties-1 -> gp1

Handled in: getBlockPrefix() (engine), classNameFromBlockName() (frontend).

Builder UI (Application-Frontend)

Block List

  • Multi-select: Cmd+click to toggle block selection
  • Group: Toolbar button appears when 1+ blocks selected, or Shift+G keyboard shortcut
  • Ungroup: Right-click a group -> "Ungroup"
  • Collapse: Click chevron on group to collapse/expand children
  • Drag into group: Drag a visual element onto a group's sort area
  • Drag out of group: Drag a child to a top-level sort area
  • Drag between groups: Atomic moveBlockBetweenGroups mutation (single undo snapshot)
  • Shortcodes on hover: Block type abbreviation displayed on hover (T1, GP1, etc.)

DnD Constraint System

  • CONTAINER_BLOCK_TYPES: blockGroupProperties, formProperties, sliderProperties, conversationProperties
  • canDropHere(): Prevents drops across incompatible containers
  • canDropInContainer(): Shows disabled overlay on containers that can't accept the dragged block
  • Container-locked blocks: Form inputs, slider slides, conversation blocks can't leave their parent
  • Extended drop zones: When container is disabled, absolute-positioned catchers allow dropping above/below

Configuration Panel

  • BlockGroupConfiguration.vue using configurationLogic mixin
  • Sections: Size, BackgroundStyle, Border (color + width + radius), BoxShadow, Padding, FlexAlignment
  • Alignment controls group position within the creative (on the wrap), not children layout

Store Mutations

MutationPurpose
createBlockGroup({ selectedBlocks, groupName })Create group from selected blocks
ungroupBlocks(groupId)Dissolve group, promote children to top-level
moveBlockToGroup({ blockName, groupId, targetOrder })Move standalone block into group
moveBlockOutOfGroup({ blockName, targetOrder })Move child out of group (auto-deletes empty group)
moveBlockBetweenGroups({ blockName, targetGroupId, targetOrder })Atomic cross-group move (single undo snapshot)

Undo/Redo

  • Snapshot-based: pushUndoSnapshot() stores cloneDeep(creativeBlocks) before each mutation
  • Covers: add, remove, duplicate, reorder, group, ungroup, move
  • Tab-scoped: stack cleared when leaving Design tab to avoid conflicts with CavaiFlow

Key Files

FilePurpose
AF/src/utils/orderUtils.tsFractional indexing utilities, serialization, migration
AF/src/store/modules/blocks.tsAll group/ordering mutations, sortedBlocks getter, undo/redo
AF/src/pages/.../Blocks/BlocksList.vueDnD with groups, multi-select, keyboard shortcuts
AF/src/pages/.../Blocks/BlockItem.vueShortcodes on hover, selection UI
AF/src/pages/.../Blocks/SortArea.vueDrop zones with parent/side props
AF/src/pages/.../Blocks/BlockActionsMenu.vueContext menu (group, ungroup, bring forward, etc.)
AF/src/pages/.../Configuration/configs/BlockGroupConfiguration.vueGroup config panel
AF/src/pages/.../Blocks/data/defaults.tsblockGroupDefaults
AF/src/pages/.../Blocks/data/types.tsBlockGroupProperties type
CE/src/components/creative/CreativeGroupBlock/CreativeGroupBlock.vueEngine group component
CE/src/components/conversationflow/CreativeBody.vueRenders groups with tree-shaking guard
CE/src/components/creative/VisualElements/CreativeVisualElements.vueFilters out group children
CE/src/utils/constants.tsBLOCKS.GROUP = 'blockGroupProperties'

Limitations (v1)

  • No nested groups (group inside group) -- prevented by moveBlockToGroup guard + canDropHere
  • Only visual elements can be grouped (text, graphic, button, video, html) -- not form, slider, conversation
  • Group children are not targetable by change operators
  • architecture/block-ordering-and-reordering-system.md -- pre-fractional ordering analysis (historical)
  • architecture/sub-block-patterns-comparison.md -- how group sub-blocks compare to form and slider patterns
  • architecture/blocks-store-structure-analysis.md -- blocks.ts structure
  • todos/BlockGrouping/feasibility.md -- original design spec
  • todos/BlockGrouping/ordering-bugs-and-fractional-indexing.md -- bug analysis that led to fractional indexing

Internal documentation