Appearance
Template Picker Improvements Implementation Plan
For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Make templates discoverable across workspaces, simplify format handling, and improve the picker UI.
Architecture: Modify the Vuex store to support multi-workspace template data, refactor TemplateSelector to group templates by workspace with collapsible sections, and remove the inherit-format toggle in favor of a simpler "template = content, format = your choice" model.
Tech Stack: Vue 2 Options API, Vuex, SCSS, existing component library (Card, InputField, ToggleSwitch, TheButton, Icon)
Branch: improve-templates (PR #1897)
Spec: Cavai-Documentation/src/DocumentationTexts/todos/Templates/template-improvements-spec.md
Task 1: Refactor Vuex store for multi-workspace templates
The store currently holds a single templatesResponse object (one workspace). We need to support templates from multiple workspaces, each with their own data and loading state.
Files:
Modify:
src/store/modules/creativeWizard.ts[ ] Step 1: Update the state type
Replace the single templatesResponse with a workspace-keyed structure:
ts
// In CreativeWizardState, replace templatesResponse with:
workspaceTemplates: Record<string, {
data: any[]
loading: boolean
workspaceName: string
}>Keep templatesResponse temporarily for backward compat during migration (remove in Task 4).
- [ ] Step 2: Add mutations for multi-workspace data
ts
setWorkspaceTemplates(
state: CreativeWizardState,
{ workspaceId, workspaceName, data }: { workspaceId: string, workspaceName: string, data: any[] },
) {
state.workspaceTemplates = {
...state.workspaceTemplates,
[workspaceId]: { data, loading: false, workspaceName },
}
}
setWorkspaceTemplatesLoading(
state: CreativeWizardState,
{ workspaceId, workspaceName }: { workspaceId: string, workspaceName: string },
) {
state.workspaceTemplates = {
...state.workspaceTemplates,
[workspaceId]: {
...(state.workspaceTemplates[workspaceId] || { data: [] }),
loading: true,
workspaceName,
},
}
}
clearWorkspaceTemplates(state: CreativeWizardState) {
state.workspaceTemplates = {}
}- [ ] Step 3: Add action to fetch templates for all enterprise workspaces
ts
async fetchAllEnterpriseTemplates(ctx) {
const enterprise = ctx.rootGetters.activeEnterprise
const activeWorkspace = ctx.rootGetters.activeWorkspace
if (!activeWorkspace) {
return
}
// If no enterprise, just fetch current workspace
if (!enterprise) {
ctx.commit('setWorkspaceTemplatesLoading', {
workspaceId: activeWorkspace.id,
workspaceName: activeWorkspace.name,
})
const res = await Axios.get(`workspaces/${activeWorkspace.id}/creative_templates`, {
params: { perPage: 50 },
})
ctx.commit('setWorkspaceTemplates', {
workspaceId: activeWorkspace.id,
workspaceName: activeWorkspace.name,
data: res.data.data,
})
return
}
// Fetch current workspace first (immediate display)
ctx.commit('setWorkspaceTemplatesLoading', {
workspaceId: activeWorkspace.id,
workspaceName: activeWorkspace.name,
})
const currentRes = await Axios.get(`workspaces/${activeWorkspace.id}/creative_templates`, {
params: { perPage: 50 },
})
ctx.commit('setWorkspaceTemplates', {
workspaceId: activeWorkspace.id,
workspaceName: activeWorkspace.name,
data: currentRes.data.data,
})
// Fetch enterprise workspaces, then fetch templates for each in parallel
const workspacesRes = await Axios.get(`enterprises/${enterprise.id}/workspaces`, {
params: { perPage: 100 },
})
const otherWorkspaces = workspacesRes.data.data.filter(
(ws) => String(ws.id) !== String(activeWorkspace.id),
)
// Set loading state for all other workspaces
for (const ws of otherWorkspaces) {
ctx.commit('setWorkspaceTemplatesLoading', {
workspaceId: ws.id,
workspaceName: ws.name,
})
}
// Fetch in parallel
await Promise.allSettled(
otherWorkspaces.map(async (ws) => {
const res = await Axios.get(`workspaces/${ws.id}/creative_templates`, {
params: { perPage: 50 },
})
ctx.commit('setWorkspaceTemplates', {
workspaceId: ws.id,
workspaceName: ws.name,
data: res.data.data,
})
}),
)
}- [ ] Step 4: Add initial state for workspaceTemplates
In initialState():
ts
workspaceTemplates: {},- [ ] Step 5: Commit
git add src/store/modules/creativeWizard.ts
git commit -m "Add multi-workspace template fetching to Vuex store"Task 2: Update CreativeWizard to fetch enterprise templates
Replace the single-workspace fetch with the new multi-workspace action.
Files:
Modify:
src/components/CreativeWizard/CreativeWizard.vue[ ] Step 1: Update mounted hook
Replace:
ts
mounted() {
if (this.activeWorkspace && !this.creativeProperties.isTemplate) {
this.fetchCreativeTemplates({
workspaces: [this.activeWorkspace.id],
} as ApiRequestPayload)
}
}With:
ts
mounted() {
if (this.activeWorkspace && !this.creativeProperties.isTemplate) {
this.fetchAllEnterpriseTemplates()
}
}- [ ] Step 2: Update mapActions
Add fetchAllEnterpriseTemplates to the mapActions call. Keep fetchCreativeTemplates for now (TemplateSelector still uses it for search).
- [ ] Step 3: Remove the
templatescomputed andtemplatesResponsefrom mapState
The templateSelectorDisabled computed currently checks !this.templates. Update it to check the new workspaceTemplates state instead:
ts
...mapState({
isEditMode: ({ creativeWizard }: State) => creativeWizard.editCreativeWizardOpen,
creativeBlocks: ({ blocks }: State) => blocks.creativeBlocks,
workspaceTemplates: ({ creativeWizard }: State) => creativeWizard.workspaceTemplates,
}),Update templateSelectorDisabled:
ts
templateSelectorDisabled(): boolean {
const hasTemplates = Object.values(this.workspaceTemplates).some(
(ws) => ws.data.length > 0 || ws.loading,
)
return (
!this.wizardCompleted('template') ||
!this.activeWorkspace ||
!hasTemplates ||
this.creatingCreatives ||
isEmpty(this.filteredFormats)
)
}- [ ] Step 4: Remove
inheritFormatdata property and the@change:inheritFormatevent
Remove from data:
ts
// Remove: inheritFormat: false,Remove from TemplateSelector usage:
html
<!-- Remove @change:inheritFormat="inheritFormat = $event" -->
<TemplateSelector
:selected-template="selectedTemplate"
:filtered-formats="filteredFormats"
@templateSelected="toggleSelectedTemplate($event)"
/>- [ ] Step 5: Update
disableCreateCreativeBtn
Remove the inheritFormat check:
ts
disableCreateCreativeBtn() {
if (this.templateSelectorVisible && isEmpty(this.filteredFormats)) {
return true
}
return !this.selectedTemplate || this.creatingCreatives
}- [ ] Step 6: Remove unused imports
Remove ApiRequestPayload import if no longer used in this file.
- [ ] Step 7: Commit
git add src/components/CreativeWizard/CreativeWizard.vue
git commit -m "Fetch templates from all enterprise workspaces in wizard"Task 3: Simplify format logic in wizardSavingLogic
Remove the inheritFormat toggle logic from completeWizard(). The user's selected formats are always used.
Files:
Modify:
src/components/CreativeWizard/wizardSavingLogic.ts[ ] Step 1: Simplify the template format handling in
completeWizard()
Replace the format inheritance block (lines 111-148) with simpler logic:
ts
if (this.selectedTemplate) {
template = this.selectedTemplate.creative_blob
? JSON.parse(this.selectedTemplate.creative_blob)
: null
// If user hasn't selected formats, use the template's child formats as defaults
if (isEmpty(this.filteredFormats)) {
const childCreativeFormats = compact(
this.selectedTemplate.childCreatives.map((creative) => {
const parsed = creative.creative_blob ? JSON.parse(creative.creative_blob) : null
return parsed?.creativeSettings?.creativeProperties?.format
}),
)
if (childCreativeFormats.length) {
this.selectedFormats = childCreativeFormats
}
}
}This removes the inheritFormat conditional entirely. If the user picked formats, those are used. If not, template formats are suggested as defaults.
- [ ] Step 2: Clean up unused imports
Check if uniq, uniqWith, isObject, isString are still needed after the simplification. Remove any that aren't used elsewhere in the file.
- [ ] Step 3: Commit
git add src/components/CreativeWizard/wizardSavingLogic.ts
git commit -m "Simplify template format handling, remove inherit format logic"Task 4: Redesign TemplateSelector with workspace grouping
This is the main UI change. Replace the flat list with grouped, collapsible workspace sections.
Files:
Modify:
src/components/CreativeWizard/TemplateSelector/TemplateSelector.vueModify:
src/components/CreativeWizard/TemplateSelector/TemplateRow.vue[ ] Step 1: Rewrite TemplateSelector template
Replace the current flat list with workspace-grouped sections:
html
<template>
<div class="template-selector">
<Card
:card-body-class="{ 'list-container': true }"
:title="$t('creatives.templates')"
no-padding
>
<CardSection class="search">
<InputField
v-model="search"
clearable
:filled="false"
:placeholder="$t('general.search')"
@clear="search = ''"
/>
</CardSection>
<CardSection class="templates">
<div
v-if="noTemplatesFound"
class="no-templates"
>
{{ $t('creativeWizard.noTemplatesFound') }}
</div>
<div
v-for="group in workspaceGroups"
v-else
:key="group.workspaceId"
class="workspace-group"
>
<div
class="workspace-header"
@click="toggleWorkspace(group.workspaceId)"
>
<Icon
:icon="isExpanded(group.workspaceId) ? 'chevron-down' : 'chevron-right'"
size="14px"
/>
<span class="workspace-name">{{ group.workspaceName }}</span>
<span
v-if="group.isCurrentWorkspace"
class="current-badge"
>
{{ $t('creativeWizard.currentWorkspace') }}
</span>
<span class="template-count">{{ group.templates.length }}</span>
</div>
<div v-if="isExpanded(group.workspaceId)">
<div
v-if="group.loading"
class="workspace-loading"
>
{{ $t('general.loading') }}...
</div>
<TemplateRow
v-for="template in group.templates"
v-else
:key="template.id"
:template="template"
:selected="selectedTemplate && template.id === selectedTemplate.id"
@selected="$emit('templateSelected', $event)"
/>
</div>
</div>
</CardSection>
</Card>
</div>
</template>- [ ] Step 2: Rewrite TemplateSelector script
ts
export default {
name: 'TemplateSelector',
components: {
CardSection,
TemplateRow,
Icon,
Card,
InputField,
},
props: {
selectedTemplate: Object,
},
data() {
return {
search: '',
collapsedWorkspaces: new Set(),
}
},
computed: {
...mapState({
workspaceTemplates: ({ creativeWizard }: State) => creativeWizard.workspaceTemplates,
}),
...mapGetters(['activeWorkspace']),
workspaceGroups() {
const activeId = String(this.activeWorkspace?.id)
const searchLower = this.search.toLowerCase().trim()
const groups = Object.entries(this.workspaceTemplates)
.map(([workspaceId, ws]) => {
const filtered = searchLower
? ws.data.filter((t) => t.name?.toLowerCase().includes(searchLower))
: ws.data
return {
workspaceId,
workspaceName: ws.workspaceName,
isCurrentWorkspace: String(workspaceId) === activeId,
loading: ws.loading,
templates: filtered,
}
})
.filter((group) => group.templates.length > 0 || group.loading)
.sort((a, b) => {
// Current workspace always first
if (a.isCurrentWorkspace) return -1
if (b.isCurrentWorkspace) return 1
return a.workspaceName.localeCompare(b.workspaceName)
})
return groups
},
noTemplatesFound() {
return this.workspaceGroups.every((g) => !g.loading && g.templates.length === 0)
},
},
methods: {
toggleWorkspace(workspaceId) {
if (this.collapsedWorkspaces.has(workspaceId)) {
this.collapsedWorkspaces.delete(workspaceId)
} else {
this.collapsedWorkspaces.add(workspaceId)
}
// Trigger reactivity (Set is not reactive in Vue 2)
this.collapsedWorkspaces = new Set(this.collapsedWorkspaces)
},
isExpanded(workspaceId) {
return !this.collapsedWorkspaces.has(workspaceId)
},
},
mounted() {
// Collapse all non-current workspaces by default
const activeId = String(this.activeWorkspace?.id)
Object.keys(this.workspaceTemplates).forEach((id) => {
if (String(id) !== activeId) {
this.collapsedWorkspaces.add(id)
}
})
this.collapsedWorkspaces = new Set(this.collapsedWorkspaces)
},
}- [ ] Step 3: Update TemplateSelector styles
Replace the existing styles with workspace-grouped styles. Remove the inherit-format-toggle-container and pagination styles. Add workspace group styles:
scss
.workspace-group {
&:not(:last-child) {
border-bottom: 1px solid $grey-90;
}
}
.workspace-header {
display: flex;
align-items: center;
gap: $size-8;
padding: $size-10 $size-5;
cursor: pointer;
user-select: none;
transition: background-color $transition-quick;
&:hover {
background-color: $grey-95;
}
}
.workspace-name {
font-weight: 500;
font-size: 13px;
}
.current-badge {
font-size: 11px;
color: var(--accent-primary);
font-weight: 500;
}
.template-count {
margin-left: auto;
font-size: 12px;
color: $grey-50;
}
.workspace-loading {
padding: $size-10 $size-20;
font-size: 13px;
color: $grey-50;
}
.no-templates {
display: flex;
align-items: center;
justify-content: center;
padding: $size-40;
color: $grey-50;
font-size: 14px;
}Keep the existing .search, .list-container, and .template-row base styles.
- [ ] Step 4: Update TemplateRow to show format chips
In TemplateRow.vue, replace the plain text format display with chips:
html
<div class="template-title-wrapper">
<div>{{ template ? template.name : '' }}</div>
<div
v-if="formatList.length"
class="template-formats"
>
<span
v-for="(format, i) in formatList"
:key="i"
class="format-chip"
>
{{ format }}
</span>
</div>
</div>Update the formats computed to return an array instead of a joined string, rename to formatList:
ts
formatList() {
if (!this.template.creative_blob) {
return []
}
const creativeData = JSON.parse(this.template.creative_blob)
const childFormats = compact(
this.template.childCreatives.map((creative) => {
if (!creative.creative_blob) {
return null
}
return JSON.parse(creative.creative_blob)
}),
).map((creative) => creative.creativeSettings.creativeProperties.format)
// Exclude 'fullscreen' (master format, not user-relevant)
return [
creativeData.creativeSettings.creativeProperties.format,
...childFormats,
]
.filter((format) => format !== 'fullscreen')
.map((format) => {
if (typeof format === 'string') {
return capitalCase(format)
}
return `${format.width}x${format.height}`
})
}Add chip styles:
scss
.template-formats {
display: flex;
flex-wrap: wrap;
gap: $size-4;
margin-top: $size-2;
}
.format-chip {
font-size: 11px;
padding: 1px $size-6;
border-radius: $size-4;
background-color: $grey-90;
color: $grey-40;
}- [ ] Step 5: Commit
git add src/components/CreativeWizard/TemplateSelector/TemplateSelector.vue
git add src/components/CreativeWizard/TemplateSelector/TemplateRow.vue
git commit -m "Redesign template picker with workspace groups and format chips"Task 5: Add i18n keys and clean up
Files:
Modify:
src/assets/i18n/en.jsModify:
src/store/modules/creativeWizard.ts(remove oldtemplatesResponsestate)[ ] Step 1: Add i18n keys
In the creativeWizard section of en.js:
js
currentWorkspace: 'Current',
noTemplatesFound: 'No templates found',- [ ] Step 2: Remove old
templatesResponsefrom Vuex state
In creativeWizard.ts:
Remove
templatesResponsefromCreativeWizardStatetypeRemove from
initialState()Remove
setTemplatesResponsemutationRemove
fetchCreativeTemplatesaction (no longer used)[ ] Step 3: Remove unused imports in TemplateSelector
Remove ToggleSwitch, TheButton, debounce, mapActions, and any other imports that are no longer used after removing pagination and inherit-format toggle.
- [ ] Step 4: Verify no other files reference the removed state/actions
Search for templatesResponse, fetchCreativeTemplates, setTemplatesResponse across the codebase. If any file still uses them, update those references.
- [ ] Step 5: Open and save all changed files to trigger lint
Changed files:
src/store/modules/creativeWizard.tssrc/components/CreativeWizard/CreativeWizard.vuesrc/components/CreativeWizard/wizardSavingLogic.tssrc/components/CreativeWizard/TemplateSelector/TemplateSelector.vuesrc/components/CreativeWizard/TemplateSelector/TemplateRow.vuesrc/assets/i18n/en.js[ ] Step 6: Commit
git add src/assets/i18n/en.js src/store/modules/creativeWizard.ts
git commit -m "Add i18n keys and remove old single-workspace template state"Task 6: Seed test templates and manual testing
Files: None (API calls only)
- [ ] Step 1: Seed templates via MCP
Use the Cavai MCP tools to create test templates across multiple workspaces. Create 3-5 templates per workspace with different names and formats.
- [ ] Step 2: Manual testing checklist
- Open creative wizard, click "Start from Template"
- Verify current workspace templates appear immediately, expanded
- Verify other workspace templates load and appear in collapsed sections
- Expand another workspace section, verify templates are listed
- Search for a template name -- verify results filter across all workspaces
- Select a template from another workspace, verify creative is created in current workspace
- Verify no "Inherit Format" toggle exists
- Select a template without having selected formats -- verify template formats are pre-selected
- Select a template with formats already selected -- verify selected formats are kept
- [ ] Step 3: Verify build compiles
Run: npm run build Expected: Successful build with no errors.