Skip to content

Fractional Order Serialization Fix (2026-05-21)

Background

PR #1859 (Block Grouping) rewrote block ordering from integer-based to fractional indexing. The fractional-indexing npm package generates string keys (like "a0", "a1", "aG") that sort lexicographically. This avoids renumbering every sibling on each reorder -- only the moved block gets a new key.

The backend validates order as schema.number() (see AB/start/validators/creative_validation.ts). A utility function serializeOrdersForSave() was created to convert string keys back to sequential integers (0, 1, 2, ...) before sending to the API.

The Bug

serializeOrdersForSave() was only called in updateChatbot() (normal save). It was missing from all other code paths that send creativeBlocks to the backend:

  • First-time creative creation
  • Mass format creation (creating multiple sizes at once)
  • Adding formats to an existing creative
  • Wizard-based creative editing and format conversion

This caused backend validation errors (Validation error: creative validation failed on creativeSettings.creativeBlocks.*.order) when creating new formats.

The Data Flow

Creative loaded from backend (integers: 0, 1, 2)
    |
    v
migrateNumericOrders() converts to strings ("a0", "a1", "a2")
    |
    v
In-memory state uses fractional strings for ordering
    |
    v
serializeOrdersForSave() converts back to integers before API call
    |
    v
Backend receives integers, validation passes

Files Changed

src/mixins/createCreative.ts

Central mixin used by the Creative Wizard and Templatizer for creating creatives. Two methods send creativeBlocks to the backend:

  • createNewCreative() -- creates a standalone creative. Constructs a creativeBlob payload with blocks, properties, and delivery, then calls chatbotService.createCreative(). Used when creating a single-format creative.

  • createMassFormatCreative() -- creates a parent creative plus child creatives for different sizes. Uses getCreativeBlob() to build the payload for each format. Called when the user selects multiple formats in the wizard. This was the primary trigger for the reported bug.

Both methods now wrap creativeBlocks with serializeOrdersForSave() before including in the payload.

src/pages/Chatbots/ChatbotBuilder.vue

The main builder page component. Has two save methods:

  • updateChatbot() -- already had serializeOrdersForSave() (working correctly)
  • createChatbot() -- first-time save when a new creative is created from the builder (not the wizard). Was missing the serialization. Now wraps this.creativeBlocks with serializeOrdersForSave().

src/components/CreativeWizard/wizardSavingLogic.ts

Mixin that handles all wizard save flows. Multiple methods construct payloads with creativeBlocks:

  • createAddedCreatives() -- adds new formats to an existing master creative. Sends masterData.creativeSettings.creativeBlocks in each child payload.

  • completeEditWizard() -- saves edits to a child creative's format. Sends this.creativeBlocks as part of the updated child creative blob.

  • completeStandaloneEditWizard() -- converts a standalone creative to multi-format. Sends this.creativeBlocks for both the new child creatives and the new master creative.

All three now serialize blocks before including in payloads.

src/utils/orderUtils.ts (not changed, for reference)

Contains the serialization utility:

  • serializeOrdersForSave(blocks) -- sorts blocks by their fractional string keys using compareOrder(), then assigns sequential integers (0, 1, 2, ...). Handles sub-blocks recursively (form inputs, slider templates, conversation children). Also nests group children back inside their parent group blocks for the backend/engine format.

  • migrateNumericOrders(blocks) -- the inverse: converts integer orders to fractional strings on creative load. Runs in setCreativeBlocks mutation.

A separate issue was reported: TypeError: e is not iterable in emitAnimationTriggers. This is unrelated to the serialization fix.

  • Cause: emitAnimationTriggers(components) was called with undefined when a flow step had no components. Added Array.isArray(components) guard in CE 6.9.0 (commit d06fb4e39).
  • Why it appears: Creatives built before CE 6.9.0 have the old engine code baked in. A rebuild bakes in the new code.
  • Impact: Harmless -- creatives render correctly, but AdValidation tools flag the TypeError.
  • Additional fix on theming branch: 655123f34 guards nextCompSet in addComponentsToFlow to prevent undefined from reaching emitAnimationTriggers in the first place.

Internal documentation