Skip to content

Bulk Tag Export from Creative Groups ​

This document describes the Bulk Tag Export feature, which allows exporting delivery tags for all creatives in a creative group at once, with shared or per-creative delivery settings.

Overview ​

The Bulk Tag Export modal is accessed from the Creative Groups listing page. Each creative group row has an "Export tags" action in its actions dropdown menu. Clicking it opens a modal that:

  1. Lists all creatives in the group (including multi-format/mass-format children)
  2. Provides global delivery settings (DSP, impression pixels, wrappers)
  3. Allows per-creative delivery setting overrides
  4. Enables publishing unpublished creatives directly from the modal
  5. Exports tags via clipboard copy or ZIP download

Where It Lives ​

Files ​

FilePurpose
Application-Frontend/src/pages/CreativeGroups/components/BulkDeliveryExportDialog.vueThe main dialog component
Application-Frontend/src/pages/CreativeGroups/index.vueCreative Groups listing page (hosts the dialog)
Application-Frontend/src/assets/i18n/en.jsi18n keys under delivery.bulkExport

Entry Point ​

The dialog is triggered from CreativeGroups/index.vue via the additionalActions() method override. The tableActions mixin (from src/mixins/tableActions.ts) provides an additionalActions() hook that returns [] by default. The creative groups page overrides it to add an "Export tags" action per row:

javascript
additionalActions(item) {
  return [{
    title: this.$t('delivery.bulkExport.exportTags'),
    action: () => this.openBulkExport(item),
  }]
}

This adds the action to the existing row actions dropdown (alongside edit, clone, delete, etc.).

Data Flow ​

User clicks "Export tags" on a creative group row
  │
  ▼
BulkDeliveryExportDialog opens with creativeGroupItem = { id, name, brand_id, campaign_id }
  │
  ▼
Watcher on `opened` triggers fetchCreatives()
  │
  ▼
API call: GET /creative_groups/{id}/creatives (via creativeGroupService.getCreativeGroupCreatives)
  │
  ▼
Response contains:
  - Standalone creatives (no parent, no children)
  - Master creatives (with childCreatives[] array)
  - Child creatives are NOT returned as top-level entries (backend filters with .whereDoesntHave('parentCreative'))
  │
  ▼
Frontend parsing:
  - Standalone creatives → parsed directly into the list
  - Master creatives → each child from childCreatives[] is extracted and added as its own entry
  - For each creative: JSON.parse(creative_blob) → extract format, type, stubFile
  │
  ▼
All published creatives are pre-selected (checkboxes checked)
  │
  ▼
User configures delivery settings (global or per-creative overrides)
  │
  ▼
User clicks "Copy Tags" or "Download ZIP"
  │
  ▼
For each selected+published creative: makeCreativeTag() generates the delivery tag
  │
  ▼
Tags are copied to clipboard or bundled into a ZIP file

Master/Child Creative Handling (Mass Formats) ​

When a creative is created with multiple formats (e.g., 300x250, 980x400, 320x480), the system creates:

  • 1 master creative — holds the shared creative content, has a childCreatives array
  • N child creatives — one per format, each with its own creative_blob, stub_file, and build_id

The backend API GET /creative_groups/{id}/creatives:

  • Filters OUT child creatives at the query level: .whereDoesntHave('parentCreative')
  • Eagerly loads children INTO their parent: .preload('childCreatives')

So the response looks like:

json
{
  "data": [
    {
      "id": "101",
      "name": "My Creative",
      "creative_blob": "...",
      "childCreatives": [
        { "id": "102", "name": "My Creative", "creative_blob": "...", "stub_file": "..." },
        { "id": "103", "name": "My Creative", "creative_blob": "...", "stub_file": "..." }
      ]
    },
    {
      "id": "104",
      "name": "Standalone Creative",
      "creative_blob": "...",
      "stub_file": "...",
      "childCreatives": []
    }
  ]
}

The fetchCreatives() method handles this by checking isMaster(creative):

  • If master → iterates through creative.childCreatives and parses each child
  • If standalone → parses the creative directly
  • Masters themselves are never added to the list (they don't have their own format/tag)

The isMaster() utility (from src/utils/creativeUtils.ts) checks if childCreatives exists and is non-empty.

The Three Sections ​

Section 1: Creative Selection ​

A checkbox list of all creatives in the group.

  • Select All / Deselect All toggle at the top
  • Each creative row shows:
    • Checkbox (disabled if unpublished)
    • Creative name
    • Format chip (e.g., "300x250" or "Responsive")
    • "Unpublished" warning badge (if no stub_file)
    • "Publish" button (if unpublished)
    • Expand arrow (if published) — toggles per-creative override panel
  • Unpublished creatives are dimmed (opacity: 0.5) and cannot be selected for export

Publish status is determined by the stub_file field. A creative has a stub_file when it has been successfully built. The stub_file is a URL to the compiled creative JavaScript file.

Section 2: Global Delivery Settings ​

These settings apply to ALL selected creatives by default:

SettingDescriptionDefault
DSP / Click MacroThe ad server macro for click trackingNone (id: 0)
Impression Pixel TagsCustom HTML tags appended to the tagDisabled
Impression Pixel URLsURLs rendered as 1x1 <img> tagsDisabled
Use onLoad wrapperWraps tag in window.addEventListener('load', ...)Off
Enable AdvantageEnables Advantage wrapperOff

The DSP dropdown uses the ADSERVERS array from src/utils/adservers.ts, which contains all supported ad servers with their click macros, domain macros, and cache buster macros.

Section 3: Per-Creative Overrides ​

Clicking the expand arrow on a published creative reveals an override panel:

  1. "Custom settings" checkbox — enables per-creative overrides
  2. When enabled: shows the same delivery settings (DSP, pixels, wrappers) but only for this creative
  3. When disabled: shows "Using global settings" hint

Override values are initialized from the current global settings when first enabled. They are stored in a overrides reactive object keyed by creative ID.

Section 4: Export Actions ​

Three actions at the bottom:

ButtonAction
Publish AllSequentially publishes all unpublished creatives (only shown if there are unpublished ones)
Copy TagsCopies all selected tags to clipboard with name/format headers
Download ZIPDownloads a ZIP file with one .txt file per creative

A summary line shows: "Exporting X of Y creatives" and/or "N creative(s) skipped — not published".

Tag Generation ​

Tags are generated using makeCreativeTag() from src/utils/TagGenerator.ts. For each selected creative:

javascript
makeCreativeTag({
  settings: {
    type: creative.type,                    // From creative_blob
    clickMacroId: src.clickMacroId,         // From global or override
    impPixelTagsEnabled: src.impPixelTagsEnabled,
    impPixelTags: src.impPixelTags,
    impPixel: src.impPixel,
    impPixelUrl: src.impPixelUrl,
    useOnLoad: src.useOnLoad,
    enableAdvantage: src.enableAdvantage,
    customTrigger: false,                   // Not used in bulk export
    customTriggerId: null,
    backgroundClickthrough: false,          // Not used in bulk export
    backgroundClickthroughUrl: null,
  },
  fullID: `${brandId}-${campaignId}-${creativeGroupId}-${creativeId}`,
  stubFile: creative.stubFile,
})

Where src is either the per-creative override object (if enabled) or the global settings (this).

The fullID follows the format brandId-campaignId-creativeGroupId-creativeId and is embedded in the generated <script> tag as data-creative-id.

Copy Tags Format ​

When copying tags, the output uses two-level headers to organize by creative name and format:

html
<!--------- My Creative --------->
<!--- 300x250 --->
<script data-creative-id='5-10-20-102' ...>
...
</script>

<!--- 980x400 --->
<script data-creative-id='5-10-20-103' ...>
...
</script>

<!--------- Another Creative --------->
<!--- Responsive --->
<script data-creative-id='5-10-20-104' ...>
...
</script>

Creatives sharing the same name (e.g., mass-format children) are grouped under a single name header, with each format as a sub-header.

ZIP Download ​

The ZIP file is named after the creative group (e.g., My Campaign Group.zip) and contains one .txt file per selected creative, named {format}_{name}.txt (e.g., 300x250_My Creative.txt).

Uses JSZip for ZIP generation and file-saver for download.

CSV Download ​

The CSV file is named after the creative group (e.g., My Campaign Group.csv) and contains one row per selected creative with columns:

ColumnDescription
Creative NameThe creative's display name
FormatThe format label (e.g., "300x250", "Fullscreen")
TypeCreative type (e.g., "banner", "expandable")
Creative IDFull ID in format brandId-campaignId-creativeGroupId-creativeId
TagThe complete delivery tag

All fields are quoted for proper CSV handling. Useful for media teams trafficking tags into spreadsheets.

Publishing from the Dialog ​

Unpublished creatives can be published directly from the dialog.

Standalone Creative Publish ​

  1. Click "Publish" button next to a standalone creative (no parent)
  2. Calls chatbotService.updateCreative(id, entity, build=true) where:
    • entity includes name, header_title (required by API validation), creative_blob, and creative_group_id
    • build=true triggers an async build job
  3. Response contains a jobId
  4. waitForBuild() polls chatbotService.getBuildJob(creativeId, jobId) every 1 second
  5. Waits until progress.status === 'Done' (max 60 attempts = 60 seconds timeout)
  6. On success: re-fetches all creatives to update the list with new stub_file
  7. On failure: shows the actual build error message

Master/Multi-Format Creative Publish ​

Child creatives cannot be published individually — the backend returns "A child creative can only be updated via its parent". Instead:

  1. One "Publish" button is shown on the group header (not per child format)
  2. Clicking it calls chatbotService.updateCreative(parentId, entity, build=true) where:
    • entity has a different structure for parent creatives:
      json
      {
        "parent_creative": { "name": "...", "header_title": "...", "creative_blob": "..." },
        "child_creatives": [
          { "id": "102", "name": "...", "header_title": "...", "creative_blob": "..." },
          { "id": "103", "name": "...", "header_title": "...", "creative_blob": "..." }
        ],
        "creative_group_id": "20"
      }
    • All children are included in the payload so they all get built
  3. The dialog waits for all build jobs (parent + each child) in parallel using Promise.all
  4. On success: re-fetches all creatives

Master creative data is stored in this.masterCreatives (keyed by parent ID) during fetchCreatives(). Each child creative in this.creatives has a parentId property pointing back to its master.

Publish All ​

  1. Click "Publish All" button
  2. Groups unpublished creatives: unique parent IDs for multi-format, standalone creatives separately
  3. Publishes each master creative once (builds all children), then each standalone individually
  4. Re-fetches all creatives once at the end
  5. Shows a count of failures

Important: header_title is Required ​

The updateCreative API requires a header_title field in the entity. Omitting it causes a validation error: "Please enter header_title". The dialog stores creative.header_title || creative.name during fetch and passes it back during publish.

Vue 2 Reactivity Notes ​

Since the frontend uses Vue 2, certain JavaScript features are NOT reactive:

  • Set and Map cannot be used for reactive state — Vue 2 cannot track them
  • Instead, plain objects {} are used with Vue.set() / Vue.delete() for all dynamic key additions/removals
  • The dialog uses this pattern for: selected, expanded, publishing, overrides

Example:

javascript
// Adding a key reactively
Vue.set(this.selected, creativeId, true)

// Removing a key reactively
Vue.delete(this.selected, creativeId)

i18n Keys ​

All i18n keys are under delivery.bulkExport in src/assets/i18n/en.js:

KeyValue
titleBulk Tag Export
exportTagsExport tags
selectCreativesSelect creatives
selectAllSelect all
unpublishedUnpublished
unpublishedWarning{count} creative(s) skipped — not published
deliverySettingsDelivery settings
noCreativesNo creatives found in this creative group.
exportingSummaryExporting {selected} of {total} creatives
publishPublish
publishAllPublish all
publishingPublishing...
publishSuccessCreative published successfully
publishErrorFailed to publish creative
customSettingsCustom settings
useGlobalSettingsUsing global settings
downloadCsvDownload CSV
csvDownloadedCSV downloaded

Design System Components Used ​

The dialog reuses existing design system components for consistency and DRY code:

ComponentUsed ForSource
MacroSelectorDSP/Click Macro dropdown (global + per-creative overrides)BuilderDelivery/MacroSelector.vue
ImpressionPixelOptionsImpression pixel tags + URLs (global + per-creative overrides)BuilderDelivery/ImpressionPixelOptions.vue
OptionRowuseOnLoad and enableAdvantage toggles (global + per-creative overrides)OptionRow/OptionRow.vue
CardSection container for delivery settings (with no-padding no-shadow)common/Card/Card.vue
IconClose button iconcommon/Icon.vue
TooltipWrapperHover tooltips on export buttonscommon/TooltipWrapper.vue
TheButtonAll action buttons (Publish, Copy Tags, Download ZIP, Download CSV)common/Button/TheButton.vue

The creative selection list uses native <input type="checkbox"> with a fixed-width wrapper div (28px) for compact checkbox rows with indeterminate state support (not possible with ToggleSwitch).

SCSS uses design system mixins and variables: @include light-scrollbar, @include text(), $size-*, $alpha-*, $border-radius-milli.

Dependencies ​

No new npm packages are required. All dependencies are already in the project:

PackageUsed For
jszipZIP file generation
file-saverTriggering file download (ZIP + CSV)
vueVue.set() / Vue.delete() for reactivity
lodashcapitalize

Key Services Used ​

ServiceMethodPurpose
creativeGroupServicegetCreativeGroupCreatives(id, sort, page, perPage)Fetches creatives in a group
chatbotServiceupdateCreative(id, entity, build)Publishes a creative (build=true). For parent creatives, entity has parent_creative + child_creatives structure
chatbotServicegetBuildJob(creativeId, jobId)Polls build job status

Limitations / Future Improvements ​

  • VAST/VPAID tags are not supported in bulk export (only standard script tags)
  • Background clickthrough and custom trigger settings are not exposed (set to false/null)
  • Preset saving (saving favorite DSP configs for reuse) is not implemented
  • CSV export in regular delivery tab could be added for single-creative export too
  • Tag generation does not include per-creative delivery settings stored in the creative's own creative_blob — all settings come from the dialog's global/override values

Internal documentation