Skip to content

Master/Child Creative Override System

Overview

Creatives in Cavai can exist in a master/child relationship. The master defines base settings for all formats (sizes). Child creatives inherit all values from master but can selectively override specific sections.

Data Model

Creative Data Structure

creativeGroup
  ├── masterCreative (e.g. responsive/largest format)
  ├── childCreative (300x600)
  ├── childCreative (320x480)
  └── childCreative (728x90)

Each creative stores its data as a creative_blob JSON containing:

json
{
  "creativeSettings": {
    "creativeBlocks": { ... },
    "creativeProperties": {
      "format": { "width": 300, "height": 600 },
      "overriddenSettings": ["textProperties-1.style.fontSize", "textProperties-1.style.color"]
    }
  }
}

overriddenSettings

Array of dot-notation paths stored per child creative. Only paths listed here have child-specific values. All other paths inherit from master on build.

Example paths:

  • textProperties-1.style.fontSize
  • buttonProperties-1.backgroundSettings.backgroundColor
  • graphicProperties-2.sizeStyle.width

Frontend Architecture

Key Files

FileRole
Configuration/mixins/sectionLogic.tsPer-section override logic
Blocks/utils.ts (getSectionSettings)Maps section component names to setting paths
store/modules/blocks.tsBlock state, isSectionOverridden getter
store/modules/builder.tscreativeFormats, childCreativeData, masterData
OptionRow/OverrideControls.vueLock/unlock UI per section
OptionRow/OptionRow.vueRenders locked state, override indicators

sectionLogic Mixin

Used by 41+ section components. Provides:

typescript
computed: {
  inputLocked()    // Whether this section's inputs should be disabled
  isOverridden()   // Whether this section has been overridden (child context)
  childOverrides() // Array of format labels that override this section (master context)
  overrideProps()  // Bundled props to pass to OptionRow
}
methods: {
  unlockSection()  // Add this section's paths to overriddenSettings
  lockSection()    // Remove paths and revert to master values
}

getSectionSettings(sectionName, blockPath)

Maps a Vue component name (e.g. 'FontSizeSection') to its setting paths relative to the block:

typescript
// Example return for FontSizeSection on textProperties-1:
['textProperties-1.style.fontSize']

// Example return for PaddingSection on buttonProperties-2:
['buttonProperties-2.padding.top', 'buttonProperties-2.padding.right', ...]

This mapping lives in Blocks/utils.ts and must be updated when adding new section components.

inputLocked Behavior

ContextConditionLocked?
On master, not viewing child--Never locked
On master, viewing child via dropdownSection IS overriddenLocked (child-specific values)
On master, viewing child via dropdownSection NOT overriddenUnlocked (editing master)
On actual child creativeSection NOT overriddenLocked (inherits from master)
On actual child creativeSection IS overriddenUnlocked (can edit override)

Override Indicators

  • OverrideControls: Lock/unlock buttons on each section when on a child creative
  • OverrideIndicator: 3px purple dot showing a section is overridden
  • Child override dots: Purple dots on master showing which children have overrides (new)

Viewing Child Format from Master (Dropdown Preview)

Mechanism

When the user selects a child format in the MobileOptionsBar dropdown while on master:

  1. Master blocks are snapshot'd (cloneDeep)
  2. Child's blocks are parsed from creativeFormats (already in memory, no API call)
  3. creativeBlocks is replaced with child's blocks
  4. Config panels show child data, preview renders child appearance
  5. Overridden sections are locked, non-overridden sections are editable
  6. Edits write to both visible creativeBlocks and masterSnapshot
  7. On restore (tab click, navigation), snapshot is restored with edits intact

Why Edits Target Master

Non-overridden values in child blocks ARE the master values (child inherits them). So editing them while viewing the child is equivalent to editing master. The dual-write to snapshot ensures the edit persists.

Overridden sections are locked because their values are child-specific -- editing them from master context would be ambiguous (which creative would the edit target?).

State Fields (blocks.ts)

typescript
viewingChildId: number | null     // Currently viewed child ID (null = master view)
masterSnapshot: {                 // Frozen master state for restore
  creativeBlocks: object
  savedBlocks: object
} | null

Build-Time Behavior

On build (backend), the override system merges:

  1. Start with master's creativeBlocks as base
  2. For each child: apply only the paths listed in overriddenSettings
  3. All other values come from master

This means editing master values propagates to all children that haven't overridden those specific paths.

Common Pitfalls

  1. Missing getSectionSettings entry: Every new section component using sectionLogic must register its paths in utils.ts, or unlocking/locking will silently fail.

  2. setCreativeBlocks is heavy: Merges defaults, fixes mismatches, clears templateModified. Don't use for temporary block swaps -- use direct assignment.

  3. setCreativeProperties side effects: Overwrites master metadata (isTemplate, format type). Never call when temporarily viewing child data.

  4. Auto-save while viewing child: The blocksUnsaved getter compares creativeBlocks to savedBlocks. Since both are set to child data during swap, it correctly shows "no unsaved changes" for the child view. Master edits accumulate in the snapshot and become visible as unsaved when restoring.

  5. Container parent field in groups: When a container block (form, conversation, slider, AR) is inside a group, its parent field points to the group's blockName. Any operation that replaces block data (e.g. switching form templates) must preserve this parent field, or the container will be orphaned from its group. The engine uses parent to determine whether to render the container inside the group or at top-level.

Internal documentation