Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
fb9bc50
feat: add provider logo overlays to workflow template thumbnails
Jan 28, 2026
9c5f482
style: update logo overlay to pill badge with provider name
Jan 29, 2026
0c5b97e
test: update LogoOverlay tests for pill badge design
Jan 29, 2026
019262b
fix: use stable provider key for logo v-for loop
Jan 29, 2026
85a7d19
fix: validate logo index entries before building URLs
Jan 29, 2026
d5166a0
test: refactor LogoOverlay tests to focus on behavior
Jan 29, 2026
fc36229
Revert "fix: validate logo index entries before building URLs"
Jan 29, 2026
a51a93c
refactor: use function declaration for mockGetLogoUrl
Jan 30, 2026
31ea8e4
test: remove Tailwind class selector in favor of behavioral assertion
Jan 30, 2026
d77fa09
refactor: directly re-export getLogoUrl from store
Jan 30, 2026
467a571
refactor: use axios for fetchLogoIndex consistent with getCoreWorkflo…
Jan 30, 2026
4b8edbf
feat: support stacked logos with overlapping design
christian-byrne Jan 30, 2026
b998a5c
fix: address CodeRabbit review feedback
christian-byrne Jan 31, 2026
edd357c
fix: address additional CodeRabbit review feedback
christian-byrne Jan 31, 2026
0310759
fix: use Intl.ListFormat for localized provider labels and type test …
christian-byrne Jan 31, 2026
00468ca
refactor: access getLogoUrl directly from store instead of re-exporting
christian-byrne Feb 1, 2026
95d254b
refactor: use zod schema for LogoIndex validation
christian-byrne Feb 1, 2026
ee28040
fix: restore accidentally removed comment
christian-byrne Feb 1, 2026
90a203e
refactor: address CodeRabbit review comments
christian-byrne Feb 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,11 @@
"
/>
</template>
<LogoOverlay
v-if="template.logos?.length"
:logos="template.logos"
:get-logo-url="getLogoUrl"
/>
<ProgressSpinner
v-if="loadingTemplate === template.name"
class="absolute inset-0 z-10 m-auto h-12 w-12"
Expand Down Expand Up @@ -392,6 +397,7 @@ import AudioThumbnail from '@/components/templates/thumbnails/AudioThumbnail.vue
import CompareSliderThumbnail from '@/components/templates/thumbnails/CompareSliderThumbnail.vue'
import DefaultThumbnail from '@/components/templates/thumbnails/DefaultThumbnail.vue'
import HoverDissolveThumbnail from '@/components/templates/thumbnails/HoverDissolveThumbnail.vue'
import LogoOverlay from '@/components/templates/thumbnails/LogoOverlay.vue'
import Button from '@/components/ui/button/Button.vue'
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
Expand Down Expand Up @@ -472,7 +478,8 @@ const {
loadWorkflowTemplate,
getTemplateThumbnailUrl,
getTemplateTitle,
getTemplateDescription
getTemplateDescription,
getLogoUrl
} = useTemplateWorkflows()

const getEffectiveSourceModule = (template: TemplateInfo) =>
Expand Down
95 changes: 95 additions & 0 deletions src/components/templates/thumbnails/LogoOverlay.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { mount } from '@vue/test-utils'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Resolve the ESLint no-unresolved error for @vue/test-utils.

ESLint reports import-x/no-unresolved; ensure the dependency is installed and the resolver is configured for the workspace.

#!/bin/bash
set -euo pipefail

# Check package.json files for `@vue/test-utils` dependency
fd package.json -t f -E node_modules -E dist -E build | while read -r f; do
  echo "== $f =="
  rg -n '"@vue/test-utils"' "$f" || true
done

# Check eslint resolver configuration
rg -n "import/resolver|alias|tsconfig" .eslintrc* package.json
🧰 Tools
🪛 ESLint

[error] 1-1: Unable to resolve path to module '@vue/test-utils'.

(import-x/no-unresolved)

🤖 Prompt for AI Agents
In `@src/components/templates/thumbnails/LogoOverlay.test.ts` at line 1, ESLint
flags the import of `@vue/test-utils` as unresolved in LogoOverlay.test.ts (the
`import { mount } from '@vue/test-utils'` line); install the correct package
(e.g., add `@vue/test-utils` to devDependencies for the project's Vue version) and
ensure your import resolver is configured (update ESLint settings such as
import/resolver in .eslintrc or tsconfig/paths so the workspace resolver
recognizes node_modules and any path aliases); after installing, run the
provided verification script to confirm package.json entries include
"@vue/test-utils" and that import/resolver or tsconfig alias settings are
present.

import { describe, expect, it } from 'vitest'

import LogoOverlay from '@/components/templates/thumbnails/LogoOverlay.vue'
import type { LogoInfo } from '@/platform/workflow/templates/types/template'

describe('LogoOverlay', () => {
const mockGetLogoUrl = (provider: string) => `/logos/${provider}.png`
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

const mountOverlay = (logos: LogoInfo[], props = {}) => {
return mount(LogoOverlay, {
props: {
logos,
getLogoUrl: mockGetLogoUrl,
...props
}
})
}

it('renders nothing when logos array is empty', () => {
const wrapper = mountOverlay([])
expect(wrapper.findAll('img')).toHaveLength(0)
})

it('renders a logo with correct src and alt', () => {
const wrapper = mountOverlay([{ provider: 'Google' }])
const img = wrapper.find('img')
expect(img.attributes('src')).toBe('/logos/Google.png')
expect(img.attributes('alt')).toBe('Google')
})

it('renders multiple logos', () => {
const wrapper = mountOverlay([
{ provider: 'Google' },
{ provider: 'OpenAI' },
{ provider: 'Stability' }
])
expect(wrapper.findAll('img')).toHaveLength(3)
})

it('applies default position when not specified', () => {
const wrapper = mountOverlay([{ provider: 'Google' }])
const container = wrapper.find('div')
expect(container.classes()).toContain('bottom-2')

Check failure on line 44 in src/components/templates/thumbnails/LogoOverlay.test.ts

View workflow job for this annotation

GitHub Actions / test

src/components/templates/thumbnails/LogoOverlay.test.ts > LogoOverlay > applies default position when not specified

AssertionError: expected [ 'pointer-events-none', …(4) ] to include 'bottom-2' ❯ src/components/templates/thumbnails/LogoOverlay.test.ts:44:33

Check failure on line 44 in src/components/templates/thumbnails/LogoOverlay.test.ts

View workflow job for this annotation

GitHub Actions / test

src/components/templates/thumbnails/LogoOverlay.test.ts > LogoOverlay > applies default position when not specified

AssertionError: expected [ 'pointer-events-none', …(4) ] to include 'bottom-2' ❯ src/components/templates/thumbnails/LogoOverlay.test.ts:44:33

Check failure on line 44 in src/components/templates/thumbnails/LogoOverlay.test.ts

View workflow job for this annotation

GitHub Actions / test

src/components/templates/thumbnails/LogoOverlay.test.ts > LogoOverlay > applies default position when not specified

AssertionError: expected [ 'pointer-events-none', …(4) ] to include 'bottom-2' ❯ src/components/templates/thumbnails/LogoOverlay.test.ts:44:33
expect(container.classes()).toContain('right-2')
})

it('applies custom position from logo config', () => {
const wrapper = mountOverlay([
{ provider: 'Google', position: 'top-2 left-2' }
])
const container = wrapper.find('div')
expect(container.classes()).toContain('top-2')
expect(container.classes()).toContain('left-2')
})

it('applies default medium size class', () => {
const wrapper = mountOverlay([{ provider: 'Google' }])
const img = wrapper.find('img')
expect(img.classes()).toContain('h-8')

Check failure on line 60 in src/components/templates/thumbnails/LogoOverlay.test.ts

View workflow job for this annotation

GitHub Actions / test

src/components/templates/thumbnails/LogoOverlay.test.ts > LogoOverlay > applies default medium size class

AssertionError: expected [ 'h-5', 'w-5', 'rounded-[50%]' ] to include 'h-8' ❯ src/components/templates/thumbnails/LogoOverlay.test.ts:60:27

Check failure on line 60 in src/components/templates/thumbnails/LogoOverlay.test.ts

View workflow job for this annotation

GitHub Actions / test

src/components/templates/thumbnails/LogoOverlay.test.ts > LogoOverlay > applies default medium size class

AssertionError: expected [ 'h-5', 'w-5', 'rounded-[50%]' ] to include 'h-8' ❯ src/components/templates/thumbnails/LogoOverlay.test.ts:60:27

Check failure on line 60 in src/components/templates/thumbnails/LogoOverlay.test.ts

View workflow job for this annotation

GitHub Actions / test

src/components/templates/thumbnails/LogoOverlay.test.ts > LogoOverlay > applies default medium size class

AssertionError: expected [ 'h-5', 'w-5', 'rounded-[50%]' ] to include 'h-8' ❯ src/components/templates/thumbnails/LogoOverlay.test.ts:60:27
expect(img.classes()).toContain('w-8')
})

it('applies small size class', () => {
const wrapper = mountOverlay([{ provider: 'Google', size: 'sm' }])
const img = wrapper.find('img')
expect(img.classes()).toContain('h-6')

Check failure on line 67 in src/components/templates/thumbnails/LogoOverlay.test.ts

View workflow job for this annotation

GitHub Actions / test

src/components/templates/thumbnails/LogoOverlay.test.ts > LogoOverlay > applies small size class

AssertionError: expected [ 'h-5', 'w-5', 'rounded-[50%]' ] to include 'h-6' ❯ src/components/templates/thumbnails/LogoOverlay.test.ts:67:27

Check failure on line 67 in src/components/templates/thumbnails/LogoOverlay.test.ts

View workflow job for this annotation

GitHub Actions / test

src/components/templates/thumbnails/LogoOverlay.test.ts > LogoOverlay > applies small size class

AssertionError: expected [ 'h-5', 'w-5', 'rounded-[50%]' ] to include 'h-6' ❯ src/components/templates/thumbnails/LogoOverlay.test.ts:67:27

Check failure on line 67 in src/components/templates/thumbnails/LogoOverlay.test.ts

View workflow job for this annotation

GitHub Actions / test

src/components/templates/thumbnails/LogoOverlay.test.ts > LogoOverlay > applies small size class

AssertionError: expected [ 'h-5', 'w-5', 'rounded-[50%]' ] to include 'h-6' ❯ src/components/templates/thumbnails/LogoOverlay.test.ts:67:27
expect(img.classes()).toContain('w-6')
})

it('applies large size class', () => {
const wrapper = mountOverlay([{ provider: 'Google', size: 'lg' }])
const img = wrapper.find('img')
expect(img.classes()).toContain('h-12')

Check failure on line 74 in src/components/templates/thumbnails/LogoOverlay.test.ts

View workflow job for this annotation

GitHub Actions / test

src/components/templates/thumbnails/LogoOverlay.test.ts > LogoOverlay > applies large size class

AssertionError: expected [ 'h-5', 'w-5', 'rounded-[50%]' ] to include 'h-12' ❯ src/components/templates/thumbnails/LogoOverlay.test.ts:74:27
expect(img.classes()).toContain('w-12')
})

it('applies default opacity', () => {
const wrapper = mountOverlay([{ provider: 'Google' }])
const container = wrapper.find('div')
expect(container.attributes('style')).toContain('opacity: 0.9')
})

it('applies custom opacity', () => {
const wrapper = mountOverlay([{ provider: 'Google', opacity: 0.5 }])
const container = wrapper.find('div')
expect(container.attributes('style')).toContain('opacity: 0.5')
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

it('images are not draggable', () => {
const wrapper = mountOverlay([{ provider: 'Google' }])
const img = wrapper.find('img')
expect(img.attributes('draggable')).toBe('false')
})
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
58 changes: 58 additions & 0 deletions src/components/templates/thumbnails/LogoOverlay.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<template>
<div
v-for="(logo, index) in validLogos"
:key="index"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
:class="
cn('pointer-events-none absolute z-10', logo.position ?? defaultPosition)
"
>
<div
v-show="!failedLogos.has(logo.provider)"
class="flex items-center gap-1.5 rounded-full bg-black/20 px-2 py-1"
:style="{ opacity: logo.opacity ?? 1 }"
>
<img
:src="logo.url"
:alt="logo.provider"
class="h-5 w-5 rounded-[50%]"
draggable="false"
@error="onImageError(logo.provider)"
/>
<span class="text-sm font-medium text-white">
{{ logo.provider }}
</span>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Honor size/opacity defaults and align default position.

size is currently ignored, default opacity is 1, and the default position is top-left; this diverges from the LogoInfo contract/tests and will break expected rendering.

🛠️ Proposed fix
 const {
   logos,
   getLogoUrl,
-  defaultPosition = 'top-2 left-2'
+  defaultPosition = 'bottom-2 right-2'
 } = defineProps<{
   logos: LogoInfo[]
   getLogoUrl: (provider: string) => string
   defaultPosition?: string
 }>()
 
+function getLogoSizeClass(size?: 'sm' | 'md' | 'lg') {
+  switch (size) {
+    case 'sm':
+      return 'h-6 w-6'
+    case 'lg':
+      return 'h-12 w-12'
+    default:
+      return 'h-8 w-8'
+  }
+}
+
 const failedLogos = ref(new Set<string>())
-      :style="{ opacity: logo.opacity ?? 1 }"
+      :style="{ opacity: logo.opacity ?? 0.9 }"
-        class="h-5 w-5 rounded-[50%]"
+        :class="cn('rounded-full', getLogoSizeClass(logo.size))"

Also applies to: 37-57

🤖 Prompt for AI Agents
In `@src/components/templates/thumbnails/LogoOverlay.vue` around lines 11 - 23,
Update LogoOverlay.vue to respect the LogoInfo contract by using logo.size to
set the img dimensions, honoring logo.opacity with default 1, and defaulting the
overlay position to top-left when logo.position is missing: bind the image size
via :style or explicit width/height using logo.size (fallback to the
component/test default), keep :style="{ opacity: logo.opacity ?? 1 }" for
opacity, and ensure the container CSS classes reflect top-left alignment unless
logo.position specifies another value; adjust any uses of onImageError and the
container class string accordingly so size, opacity, and position are applied
consistently (also apply the same fixes in the other block referenced at lines
37-57).

</div>
</div>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue'

import type { LogoInfo } from '@/platform/workflow/templates/types/template'
import { cn } from '@/utils/tailwindUtil'

const {
logos,
getLogoUrl,
defaultPosition = 'top-2 left-2'
} = defineProps<{
logos: LogoInfo[]
getLogoUrl: (provider: string) => string
defaultPosition?: string
}>()

const failedLogos = ref(new Set<string>())

const onImageError = (provider: string) => {
failedLogos.value = new Set([...failedLogos.value, provider])
}

const validLogos = computed(() =>
logos
.map((logo) => ({
...logo,
url: getLogoUrl(logo.provider)
}))
.filter((logo) => logo.url)
)
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,6 @@ export function useTemplateWorkflows() {
*/
const fetchTemplateJson = async (id: string, sourceModule: string) => {
if (sourceModule === 'default') {
// Default templates provided by frontend are served on this separate endpoint
return fetch(api.fileURL(`/templates/${id}.json`)).then((r) => r.json())
} else {
return fetch(
Expand All @@ -167,6 +166,13 @@ export function useTemplateWorkflows() {
}
}

/**
* Gets logo URL for a provider name
*/
const getLogoUrl = (provider: string): string => {
return workflowTemplatesStore.getLogoUrl(provider)
}
Comment thread
christian-byrne marked this conversation as resolved.
Outdated

return {
// State
selectedTemplate,
Expand All @@ -183,6 +189,7 @@ export function useTemplateWorkflows() {
getTemplateThumbnailUrl,
getTemplateTitle,
getTemplateDescription,
loadWorkflowTemplate
loadWorkflowTemplate,
getLogoUrl
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { generateCategoryId, getCategoryIcon } from '@/utils/categoryUtil'
import { normalizeI18nKey } from '@/utils/formatUtil'

import type {
LogoIndex,
TemplateGroup,
TemplateInfo,
WorkflowTemplates
Expand All @@ -31,6 +32,7 @@ export const useWorkflowTemplatesStore = defineStore(
const customTemplates = shallowRef<{ [moduleName: string]: string[] }>({})
const coreTemplates = shallowRef<WorkflowTemplates[]>([])
const englishTemplates = shallowRef<WorkflowTemplates[]>([])
const logoIndex = shallowRef<LogoIndex>({})
const isLoaded = ref(false)
const knownTemplateNames = ref(new Set<string>())

Expand Down Expand Up @@ -475,15 +477,18 @@ export const useWorkflowTemplatesStore = defineStore(
customTemplates.value = await api.getWorkflowTemplates()
const locale = i18n.global.locale.value

const [coreResult, englishResult] = await Promise.all([
api.getCoreWorkflowTemplates(locale),
isCloud && locale !== 'en'
? api.getCoreWorkflowTemplates('en')
: Promise.resolve([])
])
const [coreResult, englishResult, logoIndexResult] =
await Promise.all([
api.getCoreWorkflowTemplates(locale),
isCloud && locale !== 'en'
? api.getCoreWorkflowTemplates('en')
: Promise.resolve([]),
fetchLogoIndex()
])

coreTemplates.value = coreResult
englishTemplates.value = englishResult
logoIndex.value = logoIndexResult

const coreNames = coreTemplates.value.flatMap((category) =>
category.templates.map((template) => template.name)
Expand All @@ -498,6 +503,22 @@ export const useWorkflowTemplatesStore = defineStore(
}
}

async function fetchLogoIndex(): Promise<LogoIndex> {
try {
const response = await fetch(api.fileURL('/templates/index_logo.json'))
if (!response.ok) return {}
return await response.json()
} catch {
return {}
}
}
Comment thread
christian-byrne marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
christian-byrne marked this conversation as resolved.

function getLogoUrl(provider: string): string {
const logoPath = logoIndex.value[provider]
if (!logoPath) return ''
return api.fileURL(`/templates/${logoPath}`)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function getEnglishMetadata(templateName: string): {
tags?: string[]
category?: string
Expand Down Expand Up @@ -534,7 +555,8 @@ export const useWorkflowTemplatesStore = defineStore(
loadWorkflowTemplates,
knownTemplateNames,
getTemplateByName,
getEnglishMetadata
getEnglishMetadata,
getLogoUrl
}
}
)
17 changes: 17 additions & 0 deletions src/platform/workflow/templates/types/template.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
export interface LogoInfo {
/** Provider name matching index_logo.json */
provider: string
/** Tailwind positioning classes */
position?: string
/** Size: 'sm' (24px), 'md' (32px), 'lg' (48px) */
size?: 'sm' | 'md' | 'lg'
/** Opacity 0-1, default 0.9 */
opacity?: number
}

export type LogoIndex = Record<string, string>

export interface TemplateInfo {
name: string
/**
Expand Down Expand Up @@ -47,6 +60,10 @@ export interface TemplateInfo {
* If not specified, the template will be included on all distributions.
*/
includeOnDistributions?: TemplateIncludeOnDistributionEnum[]
/**
* Logo overlays to display on the template thumbnail.
*/
logos?: LogoInfo[]
}

export enum TemplateIncludeOnDistributionEnum {
Expand Down