Skip to content

Format Dropdown: Child Override Indicators

Status

Shelved. Attempted on branch block-grouping but reverted. Too many conflicts with existing reactive architecture.

Two features were explored:

  1. Purple override dots on master sections (simple, worked)
  2. Block swap to preview child appearance from master (complex, broke things)

What Worked (Simple Approach)

Purple override dots on OptionRow

Shows which child formats have overridden each section when viewing master.

Files:

  • sectionLogic.ts: Added childOverrides computed (checks overriddenSettings intersection per child)
  • OptionRow.vue: Added childOverrides prop + purple dot rendering
  • MobileOptionsBar.vue: Added "(master)" label on largest format in dropdown

Implementation was clean and non-invasive. Can be re-applied independently.

How it works

typescript
// sectionLogic.ts
childOverrides() {
  if (!this.isMasterCreative || !this.selectedBlockPath) return []
  const sectionPaths = getSectionSettings(this.$options.name, this.selectedBlockPath)
  return this.childCreativeData
    .filter(({ creativeSettings }) => {
      const overrides = creativeSettings?.creativeProperties?.overriddenSettings || []
      return !isEmpty(intersection(sectionPaths, overrides))
    })
    .map(({ creativeSettings }) => getFormatLabel(creativeSettings.creativeProperties.format))
}

What Failed (Block Swap Approach)

Goal

Preview child creative's actual appearance in the iframe while staying on master, with non-overridden sections editable (writing to master).

Approach

  1. Snapshot master blocks before swap
  2. Replace creativeBlocks with child's parsed blob
  3. Lock only overridden sections (child-specific values)
  4. Write edits to both visible blocks and master snapshot
  5. Restore snapshot when switching back

Issues Encountered

1. Reactive watchers overwrite master snapshot

When blocks are swapped, section component watchers fire and re-emit child values through updateBlockValue. These writes hit the master snapshot, overwriting master edits.

Attempted fix: snapshotWriteLocked flag + setTimeout(0) to unlock after watcher flush. Result: Still didn't fully prevent overwrites. Vue 2's watcher timing is unreliable for this pattern.

2. Block title shows "undefined"

visuals.blocks.undefined appeared as title when viewing child blocks. The child's creative_blob has different block structure/naming than what configurationLogic expects from the selectedBlockPath.

Root cause: selectedBlockPath references master block names. Child blocks may have different internal naming or missing blocks entirely.

3. Override values not reflected in child preview

Child showed master's 64px font-size even though it had overridden to 32px. The child's creative_blob stores the FULL block data (both inherited and overridden), but the blob might be stale or the override merging happens at build time, not in the stored blob.

4. Previous session issues (fixed but informative)

  • setCreativeBlocks is too heavy for swaps (merges defaults, fixes mismatches)
  • setCreativeProperties with child data overwrites master metadata (isTemplate, format)
  • FormatSelector navigate() needs MouseEvent parameter
  • Master tab loses white background styling during swap

Why Block Swap Fundamentally Conflicts

The architecture assumes creativeBlocks is a single source of truth for BOTH:

  • Preview rendering (iframe gets data from this state)
  • Config panel display (components read from this state via getBlockByName)
  • Save operations (compares against savedBlocks)
  • Template tracking (watches for changes)

Swapping blocks temporarily breaks all these assumptions:

  • Config panels try to display child data using master's block paths
  • Watchers detect "changes" that are actually just the swap
  • Template tracking fires false positives
  • Auto-save logic gets confused about what's dirty

Proper Solution (Future)

To make this work correctly would require one of:

A. Separate preview data channel

  • Keep master in creativeBlocks (config panels read this)
  • Send child blocks to preview iframe via separate postMessage
  • Preview renders child, config shows/edits master
  • Requires preview communication refactor

B. Read-only overlay state

  • Add previewingBlocks to state (separate from creativeBlocks)
  • configurationLogic.blockData reads from previewingBlocks for display
  • Edits still write to creativeBlocks (master)
  • Preview reads from previewingBlocks
  • Complex but doesn't break existing save/tracking logic

C. Build-and-preview approach

  • When selecting child in dropdown, trigger a quick build of master+child
  • Preview the BUILT output (which correctly merges master values + overrides)
  • Config panels untouched (stay on master)
  • Slowest but most correct

Key Files Reference

FileRole
store/modules/blocks.tsBlock state, mutations, getters
store/modules/builder.tscreativeFormats, childCreativeData, getCreativeData
Configuration/mixins/sectionLogic.tsOverride logic per section
Configuration/mixins/configurationLogic.tsBlock data access for config panels
BuilderVisuals/MobileOptionsBar.vueFormat dropdown
BuilderVisuals/FormatSelector.vueFormat tabs (full navigation)
OptionRow/OptionRow.vueConfig rows with override indicators
Blocks/utils.tsgetSectionSettings() mapping
mixins/responsivePreviewControls.tsPreview size management

Architecture Context

How overrides work at build time

  1. Master creativeBlocks = base for all formats
  2. Each child has overriddenSettings array (dot-notation paths)
  3. On build: start with master blocks, overlay child's values only for overridden paths
  4. Non-overridden paths always come from master

What the dropdown currently does (without our changes)

  • Changes preview iframe dimensions (width/height)
  • Stores masterDevicePreset for persistence
  • Does NOT swap block data or change config panels
  • Master stays fully editable

What format TABS do (full navigation)

  • Auto-save current creative
  • Route to child creative URL
  • Triggers full fetchChatBotData() API call
  • Loads child's blocks via setCreativeBlocks (with default merging)
  • Config panels show child data, override system enables unlock/lock

Lessons Learned

  1. Don't swap creativeBlocks temporarily -- too many side effects from watchers, template tracking, and save logic
  2. Vue 2 watcher timing is unreliable -- setTimeout(0) doesn't reliably run after all watchers
  3. Child creative_blob may be stale -- it reflects the state at last build, not live master values
  4. selectedBlockPath is master-specific -- child blocks may not have matching paths
  5. The simple approach (dots only) is safe and useful -- can be shipped independently
  6. Block swap needs architectural refactoring -- would need separate preview data channel or overlay state

Internal documentation