Skip to content

Block Ordering Refactor Plan

Problem Summary

Form input reordering causes blockName mismatches because:

  1. Object property keys (e.g., formInputProperties-2) and blockName values (e.g., "formInputProperties-3") diverge
  2. blockName never updates after creation, only order and typeIndex change
  3. Gap-filling logic reuses deleted IDs for completely different input types
  4. Multiple numbering systems (blockName suffix, typeIndex, order) are independent

Proposed Solutions

Option 1: Make blockName Dynamic (Simplest)

Concept: Update blockName to always match the object property key.

Changes Required:

  1. Remove blockName property entirely - use object key as source of truth
  2. Update all code that reads blockName to use object key instead
  3. Simplify renumberFormInputDisplayNames() to only update typeIndex/displayName

Pros:

  • Eliminates the mismatch completely
  • Simplifies code (one less property to manage)
  • Object keys already work correctly

Cons:

  • Large refactor across entire codebase
  • May break Creative-Engine if it expects blockName property
  • Need to update all references to block.blockName

Estimated Effort: 3-5 days


Option 2: Sync blockName with Property Key (Medium Complexity)

Concept: Keep blockName property but ensure it always matches the object property key.

Changes Required:

  1. Add syncBlockNameWithKey() utility:
typescript
const syncBlockNameWithKey = (parentBlock: any): void => {
  Object.entries(parentBlock).forEach(([key, value]) => {
    if (value?.blockType && value.blockName !== key) {
      value.blockName = key
    }
  })
}
  1. Call after every mutation:

    • After addBlock
    • After removeBlock
    • After updateBlockOrder
    • After duplicateBlock
  2. Update renumberFormInputDisplayNames():

typescript
const renumberFormInputDisplayNames = (formBlock: any): void => {
  // ... existing logic ...
  
  // NEW: Sync blockName with property keys
  syncBlockNameWithKey(formBlock)
}

Pros:

  • Minimal code changes
  • Maintains backward compatibility
  • Easy to implement

Cons:

  • Still have two sources of truth (key and property)
  • Need to remember to call sync function everywhere
  • Doesn't fix the root architectural issue

Estimated Effort: 1-2 days


Option 3: Use Sequential Numbering (No Gaps)

Concept: Stop reusing deleted IDs, always increment to next number.

Changes Required:

  1. Modify uniqueBlockId() to never fill gaps:
typescript
export const uniqueBlockId = (blocks: object, newBlockType: string): number => {
  const blocksOfType = findBlocksOfTypeRecursively(blocks, newBlockType)
  
  const ids = blocksOfType
    .map(({ blockName }) => parseInt(last(blockName.split('-')), 10))
    .filter(id => !isNaN(id))
    .sort((a, b) => a - b)
  
  // CHANGED: Always use max + 1, never fill gaps
  return isEmpty(ids) ? 1 : (last(ids) as number) + 1
}
  1. Accept that IDs will have gaps (e.g., 1, 2, 4, 7, 8)

Pros:

  • Prevents ID reuse confusion
  • Simple change
  • Each block keeps its original ID forever

Cons:

  • IDs can grow large over time
  • Doesn't fix the blockName vs key mismatch
  • Still have the dual numbering system issue

Estimated Effort: 1 hour


Option 4: Regenerate All blockNames on Reorder (Most Robust)

Concept: Like slider blocks, regenerate ALL blockNames when order changes.

Changes Required:

  1. Create regenerateFormInputBlockNames():
typescript
const regenerateFormInputBlockNames = (
  creativeBlocks: object,
  formBlock: any
): any => {
  const formInputs = getSubBlocks(formBlock).filter(block =>
    block.blockType === 'formInputProperties'
  )
  
  // Sort by order
  formInputs.sort((a, b) => a.order - b.order)
  
  // Create new form block with regenerated names
  const newFormBlock = { ...formBlock }
  const nameMapping = {}
  
  formInputs.forEach(input => {
    // Delete old property
    delete newFormBlock[input.blockName]
    
    // Generate new unique blockName
    const { blockName } = generateVisualElementName(
      creativeBlocks,
      input.blockType
    )
    
    // Track the change
    nameMapping[input.blockName] = blockName
    
    // Update the input
    input.blockName = blockName
    
    // Add with new key
    newFormBlock[blockName] = input
  })
  
  // Renumber display names
  renumberFormInputDisplayNames(newFormBlock)
  
  return { newFormBlock, nameMapping }
}
  1. Call in updateBlockOrder():
typescript
if (isSubBlock && block.blockType === 'formInputProperties') {
  const parentBlock = creativeBlocks[block.parent]
  if (parentBlock) {
    const { newFormBlock } = regenerateFormInputBlockNames(
      creativeBlocks,
      parentBlock
    )
    creativeBlocks[block.parent] = newFormBlock
  }
}

Pros:

  • Completely fixes the mismatch issue
  • Similar to working slider block pattern
  • Clean state after every reorder

Cons:

  • More complex implementation
  • May need to update references in other places
  • Performance cost on every reorder

Estimated Effort: 2-3 days


Option 5: Add Form Inputs to VISUAL_ELEMENTS (Architectural Change)

Concept: Treat form inputs like other visual elements (text, graphic, etc.)

Changes Required:

  1. Add to VISUAL_ELEMENTS array:
typescript
export const VISUAL_ELEMENTS = [
  BLOCKS.TEXT,
  BLOCKS.GRAPHIC,
  BLOCKS.HTML,
  BLOCKS.BUTTON,
  BLOCKS.FORM_INPUT  // NEW
]
  1. Remove special form input handling in addBlock mutation

  2. Use standard visual element patterns for add/remove/reorder

  3. Update form rendering to find inputs via getSubBlocks()

Pros:

  • Uses proven, working patterns
  • Simplifies special-case code
  • Consistent with other blocks

Cons:

  • Major architectural change
  • May affect Creative-Engine rendering
  • Need to verify form submission still works
  • Could break existing creatives

Estimated Effort: 5-7 days


Phase 1: Quick Fix (Option 2 + Option 3)

Immediate implementation to stop the bleeding:

  1. Implement Option 3 (no gap filling) - 1 hour
  2. Implement Option 2 (sync blockName) - 1-2 days
  3. Add comprehensive tests for reordering

Timeline: 2-3 days Risk: Low Impact: Fixes immediate issue

Phase 2: Long-term Solution (Option 4 or Option 5)

After Phase 1 is stable, evaluate:

  • If form inputs are causing other issues → Option 5 (add to VISUAL_ELEMENTS)
  • If just reordering is the issue → Option 4 (regenerate on reorder)

Timeline: 1-2 weeks Risk: Medium Impact: Architectural improvement

Implementation Details for Phase 1

Step 1: Disable Gap Filling

File: /src/pages/Chatbots/components/BuilderVisuals/Blocks/utils.ts

typescript
export const uniqueBlockId = (blocks: object, newBlockType: string): number => {
  const blocksOfType = findBlocksOfTypeRecursively(blocks, newBlockType)
  
  const ids = blocksOfType
    .map(({ blockName }) => parseInt(last(blockName.split('-')), 10))
    .filter(id => !isNaN(id))
    .sort((a, b) => a - b)
  
  // CHANGED: Always increment, never fill gaps
  return isEmpty(ids) ? 1 : (last(ids) as number) + 1
}

Step 2: Add Sync Function

File: /src/store/modules/blocks.ts

typescript
/**
 * Ensure blockName property matches object property key
 * Fixes mismatches caused by reordering
 */
const syncBlockNamesWithKeys = (parentBlock: any): void => {
  Object.entries(parentBlock).forEach(([key, value]) => {
    // Skip non-block properties
    if (!value || typeof value !== 'object' || !value.blockType) {
      return
    }
    
    // Sync blockName with key
    if (value.blockName !== key) {
      console.warn(`Syncing blockName: ${value.blockName} → ${key}`)
      value.blockName = key
    }
  })
}

Step 3: Call Sync After Mutations

In addBlock mutation (after renumberFormInputDisplayNames):

typescript
// Renumber all form input display names to ensure correct numbering
renumberFormInputDisplayNames(formParent)

// NEW: Sync blockNames with property keys
syncBlockNamesWithKeys(formParent)

state.creativeBlocks = {
  ...state.creativeBlocks,
  formProperties: formParent,
}

In removeBlock mutation (after renumberFormInputDisplayNames):

typescript
// Renumber remaining form inputs if this was a form input
if (parent.blockType === 'formProperties') {
  renumberFormInputDisplayNames(parent)
  
  // NEW: Sync blockNames with property keys
  syncBlockNamesWithKeys(parent)
}

In updateBlockOrder mutation (after renumberFormInputDisplayNames):

typescript
// If reordering form inputs, renumber
if (isSubBlock && block.blockType === 'formInputProperties') {
  const parentBlock = creativeBlocks[block.parent]
  if (parentBlock) {
    renumberFormInputDisplayNames(parentBlock)
    
    // NEW: Sync blockNames with property keys
    syncBlockNamesWithKeys(parentBlock)
  }
}

In duplicateBlock mutation (after renumberFormInputDisplayNames):

typescript
// Renumber all form input display names to ensure correct numbering
renumberFormInputDisplayNames(formParent)

// NEW: Sync blockNames with property keys
syncBlockNamesWithKeys(formParent)

Step 4: Add Tests

Create test file: /tests/unit/block-ordering.spec.ts

typescript
describe('Block Ordering', () => {
  it('should maintain blockName consistency after reordering', () => {
    // Test that blockName matches property key after reorder
  })
  
  it('should not reuse deleted IDs', () => {
    // Test that uniqueBlockId always increments
  })
  
  it('should renumber typeIndex correctly', () => {
    // Test that typeIndex updates based on order
  })
  
  it('should preserve custom displayNames', () => {
    // Test that custom names aren't overwritten
  })
})

Testing Checklist

After implementing Phase 1:

  • [ ] Create form with 5 different input types
  • [ ] Reorder inputs multiple times
  • [ ] Verify blockName matches property key in JSON
  • [ ] Delete middle input
  • [ ] Add new input
  • [ ] Verify new input doesn't reuse deleted ID
  • [ ] Verify typeIndex updates correctly
  • [ ] Duplicate form block
  • [ ] Verify all blockNames are unique
  • [ ] Test form submission still works
  • [ ] Test Creative-Engine rendering

Migration Strategy

For Existing Creatives

Create migration script to fix existing JSON:

typescript
const fixBlockNameMismatches = (creativeBlocks: any): any => {
  const fixed = { ...creativeBlocks }
  
  Object.entries(fixed).forEach(([key, block]) => {
    if (block.blockType === 'formProperties') {
      // Fix form block's sub-blocks
      Object.entries(block).forEach(([subKey, subBlock]) => {
        if (subBlock?.blockType && subBlock.blockName !== subKey) {
          console.log(`Fixing: ${subBlock.blockName} → ${subKey}`)
          subBlock.blockName = subKey
        }
      })
    }
  })
  
  return fixed
}

Run this on creative load or as one-time migration.

Rollback Plan

If Phase 1 causes issues:

  1. Revert uniqueBlockId change - restore gap filling
  2. Remove syncBlockNamesWithKeys calls - accept mismatches
  3. Document the issue for future reference

Keep the original code in git history for easy rollback.

Success Criteria

Phase 1 is successful when:

  1. ✅ blockName always matches object property key
  2. ✅ No ID reuse after deletion
  3. ✅ Reordering works smoothly
  4. ✅ typeIndex updates correctly
  5. ✅ Form submission works
  6. ✅ Creative-Engine renders correctly
  7. ✅ No console errors or warnings

Future Considerations

After Phase 1 is stable, consider:

  1. Remove blockName property entirely (Option 1)

    • Use object keys as single source of truth
    • Simplify code significantly
  2. Refactor to use arrays instead of objects for sub-blocks

    • Easier to reorder
    • No key/property mismatch possible
    • But requires more extensive changes
  3. Add visual reordering UI in configuration panel

    • Drag-and-drop interface
    • Live preview of order changes
    • Better UX than current system

Questions for Discussion

  1. Should we keep gap-filling for visual elements (text, graphic) but disable for form inputs?
  2. Is blockName property needed at all, or can we use object keys everywhere?
  3. Should form inputs be in VISUAL_ELEMENTS array?
  4. Do we need typeIndex, or can we calculate it on-the-fly?
  5. Should we migrate existing creatives or handle mismatches gracefully?
  • block-ordering-and-reordering-system.md - Complete architecture analysis
  • add-blocks.md - How to add new block types
  • form-system-complete-guide.md - Form system overview

Internal documentation