Appearance
Block Ordering Refactor Plan
Problem Summary
Form input reordering causes blockName mismatches because:
- Object property keys (e.g.,
formInputProperties-2) and blockName values (e.g.,"formInputProperties-3") diverge - blockName never updates after creation, only
orderandtypeIndexchange - Gap-filling logic reuses deleted IDs for completely different input types
- 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:
- Remove blockName property entirely - use object key as source of truth
- Update all code that reads blockName to use object key instead
- 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:
- Add syncBlockNameWithKey() utility:
typescript
const syncBlockNameWithKey = (parentBlock: any): void => {
Object.entries(parentBlock).forEach(([key, value]) => {
if (value?.blockType && value.blockName !== key) {
value.blockName = key
}
})
}Call after every mutation:
- After
addBlock - After
removeBlock - After
updateBlockOrder - After
duplicateBlock
- After
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:
- 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
}- 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:
- 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 }
}- 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:
- Add to VISUAL_ELEMENTS array:
typescript
export const VISUAL_ELEMENTS = [
BLOCKS.TEXT,
BLOCKS.GRAPHIC,
BLOCKS.HTML,
BLOCKS.BUTTON,
BLOCKS.FORM_INPUT // NEW
]Remove special form input handling in addBlock mutation
Use standard visual element patterns for add/remove/reorder
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
Recommended Approach
Phase 1: Quick Fix (Option 2 + Option 3)
Immediate implementation to stop the bleeding:
- Implement Option 3 (no gap filling) - 1 hour
- Implement Option 2 (sync blockName) - 1-2 days
- 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:
- Revert uniqueBlockId change - restore gap filling
- Remove syncBlockNamesWithKeys calls - accept mismatches
- Document the issue for future reference
Keep the original code in git history for easy rollback.
Success Criteria
Phase 1 is successful when:
- ✅ blockName always matches object property key
- ✅ No ID reuse after deletion
- ✅ Reordering works smoothly
- ✅ typeIndex updates correctly
- ✅ Form submission works
- ✅ Creative-Engine renders correctly
- ✅ No console errors or warnings
Future Considerations
After Phase 1 is stable, consider:
Remove blockName property entirely (Option 1)
- Use object keys as single source of truth
- Simplify code significantly
Refactor to use arrays instead of objects for sub-blocks
- Easier to reorder
- No key/property mismatch possible
- But requires more extensive changes
Add visual reordering UI in configuration panel
- Drag-and-drop interface
- Live preview of order changes
- Better UX than current system
Questions for Discussion
- Should we keep gap-filling for visual elements (text, graphic) but disable for form inputs?
- Is blockName property needed at all, or can we use object keys everywhere?
- Should form inputs be in VISUAL_ELEMENTS array?
- Do we need typeIndex, or can we calculate it on-the-fly?
- Should we migrate existing creatives or handle mismatches gracefully?
Related Documentation
block-ordering-and-reordering-system.md- Complete architecture analysisadd-blocks.md- How to add new block typesform-system-complete-guide.md- Form system overview