mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-09 01:20:09 +00:00
Add support for custom icons in menu
- Update Browse Templates to use custom icon
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user