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