Appearance
Block Ordering and Reordering System - Complete Architecture Documentation
Overview
This document provides a comprehensive analysis of how block ordering, removal, and reordering works in the Cavai Application-Frontend, with special focus on the problematic areas in form blocks.
Core Concepts
Block Types
There are three categories of blocks:
Visual Elements (defined in
VISUAL_ELEMENTSarray)textProperties,graphicProperties,htmlProperties,buttonProperties- Can have multiple instances with dynamic keys (e.g.,
textProperties-1,textProperties-2) - Use
generateVisualElementName()to create unique blockNames
Specialized Blocks (NOT in
VISUAL_ELEMENTS)sliderProperties,formProperties,conversationProperties, etc.- Usually only one instance per creative
- Use static keys (e.g.,
formProperties, notformProperties-1)
Sub-Blocks (nested within parent blocks)
- Slider sub-blocks:
graphicProperties-X,textProperties-Xnested insliderProperties - Form sub-blocks:
formInputProperties-X,formSubmitButtonPropertiesnested informProperties - Stored as properties on parent object (NOT in arrays)
- Slider sub-blocks:
Block Identification
Each block has these key properties:
typescript
{
blockName: string, // Unique identifier (e.g., "formInputProperties-3")
blockType: string, // Type identifier (e.g., "formInputProperties")
displayName: string, // User-facing name (e.g., "Email #1")
order: number, // Position in rendering order
parent: string, // Parent block's blockName
typeIndex?: number // For form inputs: per-type numbering (e.g., 1st email, 2nd email)
}Storage Architecture
Top-Level Blocks
Stored directly in state.creativeBlocks:
json
{
"creativeBlocks": {
"baseProperties": { ... },
"formProperties": { ... },
"textProperties-1": { ... },
"textProperties-2": { ... }
}
}Sub-Blocks (Nested Storage)
Stored as properties within parent blocks:
json
{
"formProperties": {
"blockName": "formProperties",
"blockType": "formProperties",
"order": 1,
"formInputProperties-1": {
"blockName": "formInputProperties-1",
"blockType": "formInputProperties",
"type": "text",
"order": 0,
"parent": "formProperties"
},
"formInputProperties-2": {
"blockName": "formInputProperties-2",
"blockType": "formInputProperties",
"type": "email",
"order": 1,
"parent": "formProperties"
}
}
}CRITICAL: Sub-blocks are stored as object properties, NOT in arrays. The old inputs array pattern was removed.
Key Functions
1. getSubBlocks(block)
Location: /src/pages/Chatbots/components/BuilderVisuals/Blocks/utils.ts
Purpose: Extract all sub-blocks from a parent block
Implementation:
typescript
export const getSubBlocks = (block) => {
// Collect subblocks stored as direct object properties
const propSubBlocks = Object.values(block).filter((value: BlockBase) => value?.blockName)
// Legacy: Also check for array-based storage (being phased out)
const arraySubBlocks: BlockBase[] = []
if (Array.isArray((block as any).inputs)) {
arraySubBlocks.push(...((block as any).inputs as any[]).filter((v: any) => v && v.blockName))
}
// Merge and dedupe by blockName
const merged = [...propSubBlocks, ...arraySubBlocks]
const uniqueByName = Object.values(
merged.reduce((acc, cur) => {
acc[cur.blockName] = cur
return acc
}, {})
)
// Sort by order DESCENDING (highest order first)
return uniqueByName.sort((a, b) => (Number(b.order) || 0) - (Number(a.order) || 0))
}Important: Returns blocks sorted by order in DESCENDING order (highest first).
2. getBlockPath(candidateBlockName, creativeBlocks)
Location: /src/pages/Chatbots/components/BuilderVisuals/Blocks/utils.ts
Purpose: Find the path to a block (either top-level or nested)
Returns:
- Top-level block:
"formProperties" - Nested block:
"formProperties.formInputProperties-3"
Implementation:
typescript
export const getBlockPath = (candidateBlockName, creativeBlocks) => {
// Case 1: Top-level block
if (Object.values(creativeBlocks).map(block => block.blockName).includes(candidateBlockName)) {
return candidateBlockName
}
// Case 2: Nested sub-block
let allSubBlocks = []
getSubBlocks(creativeBlocks).forEach(block => {
allSubBlocks = allSubBlocks.concat(getSubBlocks(block))
})
for (const subBlock of allSubBlocks) {
if (subBlock.blockName === candidateBlockName) {
return [subBlock.parent, candidateBlockName].join('.')
}
}
return null
}3. generateVisualElementName(creativeBlocks, blockType)
Location: /src/store/modules/blocks.ts
Purpose: Generate unique blockName and displayName for new blocks
Implementation:
typescript
const generateVisualElementName = (creativeBlocks, blockType) => {
const id = uniqueBlockId(creativeBlocks, blockType)
const blockName = blockType === 'sliderProperties'
? blockType
: `${blockType}-${id}`
const getDisplayName = () => {
switch (blockType) {
case BLOCKS.TEXT: return 'Text'
case BLOCKS.GRAPHIC: return 'Graphic'
case BLOCKS.HTML: return 'HTML'
case BLOCKS.BUTTON: return 'Button'
default: return ''
}
}
return { blockName, displayName: `${getDisplayName()} #${id}` }
}4. uniqueBlockId(blocks, newBlockType)
Location: /src/pages/Chatbots/components/BuilderVisuals/Blocks/utils.ts
Purpose: Find next available ID for a block type
Algorithm:
- Find all existing blocks of this type recursively
- Extract numeric IDs from blockNames (e.g.,
formInputProperties-3→3) - Sort IDs
- Look for first gap in sequence (e.g., if have [1, 2, 4], return 3)
- If no gaps, return
max + 1
This is why blockNames can become mismatched after reordering!
Form Input Specific Functions
5. calculateFormInputTypeIndex(parentBlock, inputType)
Location: /src/store/modules/blocks.ts
Purpose: Calculate the next typeIndex for a specific input type
Implementation:
typescript
const calculateFormInputTypeIndex = (parentBlock, inputType) => {
const existingInputs = getSubBlocks(parentBlock).filter(block =>
block.blockType === 'formInputProperties' && (block.type || 'text') === inputType
)
// Sort by order to match renumberFormInputDisplayNames logic
existingInputs.sort((a, b) => {
const orderA = typeof a.order === 'number' ? a.order : 0
const orderB = typeof b.order === 'number' ? b.order : 0
return orderA - orderB
})
return existingInputs.length + 1
}6. renumberFormInputDisplayNames(formBlock)
Location: /src/store/modules/blocks.ts
Purpose: Renumber all form inputs by type after add/remove/reorder
Implementation:
typescript
const renumberFormInputDisplayNames = (formBlock) => {
const formInputs = getSubBlocks(formBlock).filter(block =>
block.blockType === 'formInputProperties'
)
// Sort by order ASCENDING
formInputs.sort((a, b) => {
const orderA = typeof a.order === 'number' ? a.order : 0
const orderB = typeof b.order === 'number' ? b.order : 0
return orderA - orderB
})
// Group by type and renumber each type
const typeCounters = {}
formInputs.forEach(input => {
const inputType = input.type || 'text'
if (!typeCounters[inputType]) {
typeCounters[inputType] = 0
}
typeCounters[inputType]++
input.typeIndex = typeCounters[inputType]
// Only update displayName/label if they match default pattern
if (isDefaultDisplayName(input.displayName, inputType)) {
const newDisplayName = generateFormInputDisplayName(inputType, typeCounters[inputType])
input.displayName = newDisplayName
if (isDefaultDisplayName(input.label, inputType)) {
input.label = newDisplayName
}
}
})
}Key Point: This function updates typeIndex, displayName, and label but does NOT update blockName.
Mutations (State Changes)
1. addBlock
Location: /src/store/modules/blocks.ts line 520
Flow for Form Inputs:
typescript
// 1. Check if adding form input
if (blockTypeToCreate === BLOCKS.FORM_INPUT) {
const parentBlock = state.creativeBlocks[targetBlockPath]
// 2. Create form input block with unique blockName
const formInputBlock = createFormInputBlock(
state,
parentBlock,
targetBlockPath,
inputType,
additionalProperties
)
// 3. Add to parent as property
const formParent = { ...state.creativeBlocks.formProperties }
formParent[formInputBlock.blockName] = formInputBlock
// 4. Set order based on ALL existing sub-blocks
const allSubBlocks = getSubBlocks(formParent)
formInputBlock.order = allSubBlocks.length
// 5. Renumber all form inputs
renumberFormInputDisplayNames(formParent)
// 6. Update state
state.creativeBlocks = {
...state.creativeBlocks,
formProperties: formParent
}
}2. removeBlock
Location: /src/store/modules/blocks.ts line 676
Flow:
typescript
removeBlock(state, name) {
const creativeBlocks = cloneDeep(state.creativeBlocks)
const path = getBlockPath(name, creativeBlocks)
// If nested path like "formProperties.formInputProperties-3"
if (path.includes('.')) {
const [parentKey, childKey] = path.split('.')
const parent = creativeBlocks[parentKey] || {}
// Remove the subblock object from parent
if (parent && parent[childKey]) {
delete parent[childKey]
}
// Renumber remaining form inputs
if (parent.blockType === 'formProperties') {
renumberFormInputDisplayNames(parent)
}
creativeBlocks[parentKey] = parent
}
// Remove the block
unset(creativeBlocks, path)
state.creativeBlocks = creativeBlocks
}PROBLEM: After deletion, renumberFormInputDisplayNames() updates typeIndex but NOT blockName. This creates a mismatch.
3. updateBlockOrder
Location: /src/store/modules/blocks.ts line 764
Flow:
typescript
updateBlockOrder(state, { block, newIndex }) {
const oldIndex = block.order
if (newIndex === oldIndex) return
const creativeBlocks = cloneDeep(state.creativeBlocks)
const path = getBlockPath(block.blockName, state.creativeBlocks)
const isSubBlock = path.includes('.')
// Get all blocks at this level
const blocks = getSubBlocks(isSubBlock ? creativeBlocks[block.parent] : creativeBlocks)
const orderByBlockNames = orderBy(blocks, ['order']).map(item => item.blockName)
// Reorder: remove from old position, insert at new position
orderByBlockNames.splice(oldIndex, 1)
orderByBlockNames.splice(newIndex, 0, block.blockName)
// Apply new indices
orderByBlockNames.forEach((blockName, index) => {
const pathComponents = [isSubBlock ? block.parent : undefined, blockName, 'order']
const subPath = compact(pathComponents).join('.')
set(creativeBlocks, subPath, index)
})
// If reordering form inputs, renumber
if (isSubBlock && block.blockType === 'formInputProperties') {
const parentBlock = creativeBlocks[block.parent]
if (parentBlock) {
renumberFormInputDisplayNames(parentBlock)
}
}
state.creativeBlocks = creativeBlocks
}PROBLEM: After reordering, renumberFormInputDisplayNames() updates typeIndex but NOT blockName. This creates mismatches.
The Root Problem
Issue: blockName vs typeIndex Mismatch
What happens:
You create form inputs in order:
formInputProperties-1(text, typeIndex: 1, order: 0)formInputProperties-2(email, typeIndex: 1, order: 1)formInputProperties-3(phone, typeIndex: 1, order: 2)
You reorder them (move phone to position 0):
formInputProperties-3(phone, typeIndex: 1, order: 0) ← order changedformInputProperties-1(text, typeIndex: 1, order: 1) ← order changedformInputProperties-2(email, typeIndex: 1, order: 2) ← order changed
renumberFormInputDisplayNames()runs and updates typeIndex based on NEW order:formInputProperties-3(phone, typeIndex: 1, order: 0) ← typeIndex stays 1formInputProperties-1(text, typeIndex: 1, order: 1) ← typeIndex stays 1formInputProperties-2(email, typeIndex: 1, order: 2) ← typeIndex stays 1
You delete the phone input (formInputProperties-3):
formInputProperties-1(text, order: 0)formInputProperties-2(email, order: 1)
renumberFormInputDisplayNames()runs again:formInputProperties-1(text, typeIndex: 1, order: 0)formInputProperties-2(email, typeIndex: 1, order: 1)
You add a new date input:
uniqueBlockId()sees gaps: [1, 2] and returns 3- New block gets
blockName: "formInputProperties-3" - But it's a DATE input, not the original PHONE input!
Result: formInputProperties-3 is now a date input, but the JSON shows it as formInputProperties-4 in your example because of complex reordering.
Why This Happens
The system has TWO separate numbering systems:
blockName suffix (e.g.,
-1,-2,-3)- Generated by
uniqueBlockId()which finds gaps in existing IDs - NEVER changes after creation
- Used as object property key
- Generated by
typeIndex (e.g., 1st email, 2nd email)
- Calculated by
renumberFormInputDisplayNames()based on current order - CHANGES on every add/remove/reorder
- Used for display names and CSS classes
- Calculated by
These two systems are completely independent and can diverge!
Example from Your JSON
Looking at your JSON:
json
"formInputProperties-2": {
"blockType": "formInputProperties",
"type": "phone",
"order": 3,
"blockName": "formInputProperties-3" // ← MISMATCH!
}The property key is formInputProperties-2 but blockName is formInputProperties-3. This is the core problem.
Why It's Overcomplicated
Multiple Sources of Truth
- Object property keys (e.g.,
formProperties["formInputProperties-2"]) - blockName property (e.g.,
blockName: "formInputProperties-3") - order property (e.g.,
order: 3) - typeIndex property (e.g.,
typeIndex: 1)
These should all be in sync but aren't.
Inconsistent Sorting
getSubBlocks()sorts DESCENDING by order (highest first)renumberFormInputDisplayNames()sorts ASCENDING by order (lowest first)updateBlockOrder()uses lodashorderBy()which sorts ASCENDING
Gap-Filling Logic
uniqueBlockId() tries to reuse deleted IDs, which causes:
- Deleted
formInputProperties-3(phone) - New input gets
formInputProperties-3(date) - But they're completely different inputs!
Dual Update Pattern
When reordering/removing:
- Update
orderproperty - Call
renumberFormInputDisplayNames()which updatestypeIndex,displayName,label - But NOT
blockName
This creates divergence.
Current Workarounds
1. uniquefyFormBlockNames()
Location: /src/store/modules/blocks.ts line 255
Used when duplicating form blocks to ensure unique names:
typescript
const uniquefyFormBlockNames = (creativeBlocks, block) => {
const uniqueCreativeBlocks = {}
const allBlocksClone = cloneDeep({ ...creativeBlocks, ...block })
getSubBlocks(allBlocksClone).forEach(subBlock => {
uniqueCreativeBlocks[subBlock.blockName] = subBlock
})
const subBlockNameDictionary = {}
// Iterate through sub-blocks and generate new unique names
getSubBlocks(block).forEach(subBlock => {
delete block[subBlock.blockName]
const { blockName } = generateVisualElementName(uniqueCreativeBlocks, subBlock.blockType)
subBlockNameDictionary[subBlock.blockName] = blockName
subBlock.blockName = blockName
uniqueCreativeBlocks[blockName] = subBlock
block[blockName] = subBlock
})
// Renumber form input display names
renumberFormInputDisplayNames(block)
return block
}Problem: This works for duplication but doesn't fix the reordering issue.
2. Renumbering After Every Operation
The code calls renumberFormInputDisplayNames() after:
- Adding form input
- Removing form input
- Reordering form input
- Duplicating form block
But this only updates typeIndex, not blockName.
Comparison with Slider Blocks
Slider blocks have similar architecture but work better because:
Slider sub-blocks ARE visual elements
- They're in the
VISUAL_ELEMENTSarray - They can have multiple instances
- Their blockNames are meant to be dynamic
- They're in the
No typeIndex complexity
- Slider blocks don't have per-type numbering
- Just simple order-based rendering
Simpler renaming
uniquefySlideBlockNames()regenerates ALL blockNames on duplication- No partial updates like form inputs
Summary of Issues
- blockName never updates after creation - causes property key vs blockName mismatch
- Gap-filling reuses IDs - deleted phone input's ID gets reused for date input
- Inconsistent sorting - different functions sort differently
- Multiple numbering systems - blockName suffix vs typeIndex
- Form inputs NOT in VISUAL_ELEMENTS - can't use standard visual element patterns
- Object property keys vs blockName - two ways to identify same block
Next Steps
See the accompanying block-ordering-refactor-plan.md for proposed solutions.