Skip to content

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 templates computed and templatesResponse from 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 inheritFormat data property and the @change:inheritFormat event

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.vue

  • Modify: 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.js

  • Modify: src/store/modules/creativeWizard.ts (remove old templatesResponse state)

  • [ ] Step 1: Add i18n keys

In the creativeWizard section of en.js:

js
currentWorkspace: 'Current',
noTemplatesFound: 'No templates found',
  • [ ] Step 2: Remove old templatesResponse from Vuex state

In creativeWizard.ts:

  • Remove templatesResponse from CreativeWizardState type

  • Remove from initialState()

  • Remove setTemplatesResponse mutation

  • Remove fetchCreativeTemplates action (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.ts

  • src/components/CreativeWizard/CreativeWizard.vue

  • src/components/CreativeWizard/wizardSavingLogic.ts

  • src/components/CreativeWizard/TemplateSelector/TemplateSelector.vue

  • src/components/CreativeWizard/TemplateSelector/TemplateRow.vue

  • src/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
  1. Open creative wizard, click "Start from Template"
  2. Verify current workspace templates appear immediately, expanded
  3. Verify other workspace templates load and appear in collapsed sections
  4. Expand another workspace section, verify templates are listed
  5. Search for a template name -- verify results filter across all workspaces
  6. Select a template from another workspace, verify creative is created in current workspace
  7. Verify no "Inherit Format" toggle exists
  8. Select a template without having selected formats -- verify template formats are pre-selected
  9. 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.

Internal documentation