Add support for custom icons in menu

- Update Browse Templates to use custom icon
This commit is contained in:
pythongosssss
2025-08-17 17:40:02 +01:00
parent 88579c2a40
commit 34e07fb481
5 changed files with 130 additions and 16 deletions

View File

@@ -178,6 +178,79 @@ test.describe('Menu', () => {
await comfyPage.menu.topbar.triggerTopbarCommand(['ext', 'foo-command'])
expect(await comfyPage.getVisibleToastCount()).toBe(1)
})
test('Browse Templates custom icon is visible and matches sidebar icon', async ({
comfyPage
}) => {
// Open the top menu
await comfyPage.menu.topbar.openTopbarMenu()
const menu = comfyPage.page.locator('.comfy-command-menu')
// Find the Browse Templates menu item
const browseTemplatesItem = menu.locator(
'.p-menubar-item-label:text-is("Browse Templates")'
)
await expect(browseTemplatesItem).toBeVisible()
// Check that the Browse Templates item has an icon
const menuIcon = browseTemplatesItem
.locator('..')
.locator('.p-menubar-item-icon')
.first()
await expect(menuIcon).toBeVisible()
// Get the icon's tag name and class to verify it's a component (not a string icon)
const menuIconType = await menuIcon.evaluate((el) => {
// If it's a Vue component, it will not have pi/mdi classes
// and should be an SVG or custom component
const tagName = el.tagName.toLowerCase()
const classes = el.className || ''
return {
tagName,
classes,
hasStringIcon:
typeof classes === 'string' &&
(classes.includes('pi ') || classes.includes('mdi '))
}
})
// Verify it's a component icon (not a string icon with pi/mdi classes)
expect(menuIconType.hasStringIcon).toBe(false)
// Close menu
await comfyPage.page.locator('body').click({ position: { x: 10, y: 10 } })
// Now check the sidebar templates button
const sidebarTemplatesButton = comfyPage.page.locator(
'.templates-tab-button'
)
await expect(sidebarTemplatesButton).toBeVisible()
// Get the sidebar icon info
const sidebarIcon = sidebarTemplatesButton.locator(
'.side-bar-button-icon'
)
await expect(sidebarIcon).toBeVisible()
const sidebarIconType = await sidebarIcon.evaluate((el) => {
const tagName = el.tagName.toLowerCase()
const classes = el.className || ''
return {
tagName,
classes,
hasStringIcon:
typeof classes === 'string' &&
(classes.includes('pi ') || classes.includes('mdi '))
}
})
// Verify sidebar also uses component icon (not string icon)
expect(sidebarIconType.hasStringIcon).toBe(false)
// Both should be using the same custom component (likely SVG elements)
expect(menuIconType.tagName).toBe('svg')
expect(sidebarIconType.tagName).toBe('svg')
})
})
// Only test 'Top' to reduce test time.

View File

@@ -7,7 +7,13 @@
}"
severity="secondary"
text
:icon="typeof command.icon === 'function' ? command.icon() : command.icon"
:icon="
typeof command.icon === 'function'
? command.icon()
: typeof command.icon === 'string'
? command.icon
: undefined
"
@click="() => commandStore.execute(command.id)"
/>
</template>

View File

@@ -66,18 +66,20 @@
class="p-menubar-item-icon pi pi-check text-sm"
:class="{ invisible: !item.comfyCommand?.active?.() }"
/>
<span
v-else-if="
item.icon && item.comfyCommand?.id !== 'Comfy.NewBlankWorkflow'
"
<component
:is="getIconComponent(item)"
v-else-if="getIconLocation(item) === 'left'"
class="p-menubar-item-icon"
:class="item.icon"
:class="typeof item.icon === 'string' ? item.icon : undefined"
/>
<span class="p-menubar-item-label text-nowrap">{{ item.label }}</span>
<i
v-if="item.comfyCommand?.id === 'Comfy.NewBlankWorkflow'"
<component
:is="getIconComponent(item)"
v-if="getIconLocation(item) === 'right'"
class="ml-auto"
:class="item.icon"
:class="typeof item.icon === 'string' ? item.icon : undefined"
/>
<span
v-if="item?.comfyCommand?.keybinding"
@@ -94,13 +96,14 @@
</template>
<script setup lang="ts">
import type { MenuItem } from 'primevue/menuitem'
import type { MenuItem as PrimeMenuItem } from 'primevue/menuitem'
import SelectButton from 'primevue/selectbutton'
import TieredMenu, {
type TieredMenuMethods,
type TieredMenuState
} from 'primevue/tieredmenu'
import { computed, nextTick, ref } from 'vue'
import { computed, defineAsyncComponent, markRaw, nextTick, ref } from 'vue'
import type { Component } from 'vue'
import { useI18n } from 'vue-i18n'
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
@@ -117,6 +120,15 @@ import { showNativeSystemMenu } from '@/utils/envUtil'
import { normalizeI18nKey } from '@/utils/formatUtil'
import { whileMouseDown } from '@/utils/mouseDownUtil'
// Extended MenuItem interface that supports component icons
interface MenuItem extends Omit<PrimeMenuItem, 'icon'> {
icon?: string | { component: Component } | undefined
}
const TemplateIcon = markRaw(
defineAsyncComponent(() => import('virtual:icons/comfy/template'))
)
const colorPaletteStore = useColorPaletteStore()
const menuItemsStore = useMenuItemStore()
const commandStore = useCommandStore()
@@ -187,7 +199,7 @@ const extraMenuItems: MenuItem[] = [
{
key: 'browse-templates',
label: t('menuLabels.Browse Templates'),
icon: 'pi pi-folder-open',
icon: { component: TemplateIcon },
command: () => commandStore.execute('Comfy.BrowseTemplates')
},
{
@@ -254,7 +266,8 @@ const translatedItems = computed(() => {
: [])
)
return items
// Cast to PrimeVue type - our template overrides icon handling
return items as PrimeMenuItem[]
})
const onMenuShow = () => {
@@ -303,6 +316,24 @@ const handleItemClick = (item: MenuItem, event: MouseEvent) => {
const hasActiveStateSiblings = (item: MenuItem): boolean => {
return menuItemsStore.menuItemHasActiveStateChildren[item.parentPath]
}
const getIconComponent = (
item: MenuItem | PrimeMenuItem
): string | Component | undefined => {
return typeof item.icon === 'string' ? 'i' : item.icon?.component
}
const getIconLocation = (
item: MenuItem | PrimeMenuItem
): 'left' | 'right' | null => {
if (!item.icon) return null
if (item.comfyCommand?.id === 'Comfy.NewBlankWorkflow') {
return 'right'
}
return 'left'
}
</script>
<style scoped>

View File

@@ -1,5 +1,6 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import type { Component } from 'vue'
import { useErrorHandling } from '@/composables/useErrorHandling'
import type { ComfyExtension } from '@/types/comfy'
@@ -11,7 +12,7 @@ export interface ComfyCommand {
function: () => void | Promise<void>
label?: string | (() => string)
icon?: string | (() => string)
icon?: string | { component: Component } | (() => string)
tooltip?: string | (() => string)
menubarLabel?: string | (() => string) // Menubar item label, if different from command label
versionAdded?: string
@@ -25,7 +26,7 @@ export class ComfyCommandImpl implements ComfyCommand {
id: string
function: () => void | Promise<void>
_label?: string | (() => string)
_icon?: string | (() => string)
_icon?: string | { component: Component } | (() => string)
_tooltip?: string | (() => string)
_menubarLabel?: string | (() => string)
versionAdded?: string

View File

@@ -57,7 +57,10 @@ export const useSidebarTabStore = defineStore('sidebarTab', () => {
useCommandStore().registerCommand({
id: `Workspace.ToggleSidebarTab.${tab.id}`,
icon: typeof tab.icon === 'string' ? tab.icon : undefined,
icon:
!tab.icon || typeof tab.icon === 'string'
? tab.icon
: { component: tab.icon },
label: labelFunction,
menubarLabel: menubarLabelFunction,
tooltip: tooltipFunction,