Appearance
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:
- Lists all creatives in the group (including multi-format/mass-format children)
- Provides global delivery settings (DSP, impression pixels, wrappers)
- Allows per-creative delivery setting overrides
- Enables publishing unpublished creatives directly from the modal
- Exports tags via clipboard copy or ZIP download
Where It Lives
Files
| File | Purpose |
|---|---|
Application-Frontend/src/pages/CreativeGroups/components/BulkDeliveryExportDialog.vue | The main dialog component |
Application-Frontend/src/pages/CreativeGroups/index.vue | Creative Groups listing page (hosts the dialog) |
Application-Frontend/src/assets/i18n/en.js | i18n 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 fileMaster/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
childCreativesarray - N child creatives — one per format, each with its own
creative_blob,stub_file, andbuild_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.childCreativesand 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:
| Setting | Description | Default |
|---|---|---|
| DSP / Click Macro | The ad server macro for click tracking | None (id: 0) |
| Impression Pixel Tags | Custom HTML tags appended to the tag | Disabled |
| Impression Pixel URLs | URLs rendered as 1x1 <img> tags | Disabled |
| Use onLoad wrapper | Wraps tag in window.addEventListener('load', ...) | Off |
| Enable Advantage | Enables Advantage wrapper | Off |
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:
- "Custom settings" checkbox — enables per-creative overrides
- When enabled: shows the same delivery settings (DSP, pixels, wrappers) but only for this creative
- 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:
| Button | Action |
|---|---|
| Publish All | Sequentially publishes all unpublished creatives (only shown if there are unpublished ones) |
| Copy Tags | Copies all selected tags to clipboard with name/format headers |
| Download ZIP | Downloads 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:
| Column | Description |
|---|---|
| Creative Name | The creative's display name |
| Format | The format label (e.g., "300x250", "Fullscreen") |
| Type | Creative type (e.g., "banner", "expandable") |
| Creative ID | Full ID in format brandId-campaignId-creativeGroupId-creativeId |
| Tag | The 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
- Click "Publish" button next to a standalone creative (no parent)
- Calls
chatbotService.updateCreative(id, entity, build=true)where:entityincludesname,header_title(required by API validation),creative_blob, andcreative_group_idbuild=truetriggers an async build job
- Response contains a
jobId waitForBuild()pollschatbotService.getBuildJob(creativeId, jobId)every 1 second- Waits until
progress.status === 'Done'(max 60 attempts = 60 seconds timeout) - On success: re-fetches all creatives to update the list with new
stub_file - 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:
- One "Publish" button is shown on the group header (not per child format)
- Clicking it calls
chatbotService.updateCreative(parentId, entity, build=true)where:entityhas 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
- The dialog waits for all build jobs (parent + each child) in parallel using
Promise.all - 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
- Click "Publish All" button
- Groups unpublished creatives: unique parent IDs for multi-format, standalone creatives separately
- Publishes each master creative once (builds all children), then each standalone individually
- Re-fetches all creatives once at the end
- 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:
SetandMapcannot be used for reactive state — Vue 2 cannot track them- Instead, plain objects
{}are used withVue.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:
| Key | Value |
|---|---|
title | Bulk Tag Export |
exportTags | Export tags |
selectCreatives | Select creatives |
selectAll | Select all |
unpublished | Unpublished |
unpublishedWarning | {count} creative(s) skipped — not published |
deliverySettings | Delivery settings |
noCreatives | No creatives found in this creative group. |
exportingSummary | Exporting {selected} of {total} creatives |
publish | Publish |
publishAll | Publish all |
publishing | Publishing... |
publishSuccess | Creative published successfully |
publishError | Failed to publish creative |
customSettings | Custom settings |
useGlobalSettings | Using global settings |
downloadCsv | Download CSV |
csvDownloaded | CSV downloaded |
Design System Components Used
The dialog reuses existing design system components for consistency and DRY code:
| Component | Used For | Source |
|---|---|---|
MacroSelector | DSP/Click Macro dropdown (global + per-creative overrides) | BuilderDelivery/MacroSelector.vue |
ImpressionPixelOptions | Impression pixel tags + URLs (global + per-creative overrides) | BuilderDelivery/ImpressionPixelOptions.vue |
OptionRow | useOnLoad and enableAdvantage toggles (global + per-creative overrides) | OptionRow/OptionRow.vue |
Card | Section container for delivery settings (with no-padding no-shadow) | common/Card/Card.vue |
Icon | Close button icon | common/Icon.vue |
TooltipWrapper | Hover tooltips on export buttons | common/TooltipWrapper.vue |
TheButton | All 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:
| Package | Used For |
|---|---|
jszip | ZIP file generation |
file-saver | Triggering file download (ZIP + CSV) |
vue | Vue.set() / Vue.delete() for reactivity |
lodash | capitalize |
Key Services Used
| Service | Method | Purpose |
|---|---|---|
creativeGroupService | getCreativeGroupCreatives(id, sort, page, perPage) | Fetches creatives in a group |
chatbotService | updateCreative(id, entity, build) | Publishes a creative (build=true). For parent creatives, entity has parent_creative + child_creatives structure |
chatbotService | getBuildJob(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