mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 11:11:53 +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'])
|
await comfyPage.menu.topbar.triggerTopbarCommand(['ext', 'foo-command'])
|
||||||
expect(await comfyPage.getVisibleToastCount()).toBe(1)
|
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.
|
// Only test 'Top' to reduce test time.
|
||||||
|
|||||||
@@ -7,7 +7,13 @@
|
|||||||
}"
|
}"
|
||||||
severity="secondary"
|
severity="secondary"
|
||||||
text
|
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)"
|
@click="() => commandStore.execute(command.id)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -66,18 +66,20 @@
|
|||||||
class="p-menubar-item-icon pi pi-check text-sm"
|
class="p-menubar-item-icon pi pi-check text-sm"
|
||||||
:class="{ invisible: !item.comfyCommand?.active?.() }"
|
:class="{ invisible: !item.comfyCommand?.active?.() }"
|
||||||
/>
|
/>
|
||||||
<span
|
<component
|
||||||
v-else-if="
|
:is="getIconComponent(item)"
|
||||||
item.icon && item.comfyCommand?.id !== 'Comfy.NewBlankWorkflow'
|
v-else-if="getIconLocation(item) === 'left'"
|
||||||
"
|
|
||||||
class="p-menubar-item-icon"
|
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>
|
<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="ml-auto"
|
||||||
:class="item.icon"
|
:class="typeof item.icon === 'string' ? item.icon : undefined"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="item?.comfyCommand?.keybinding"
|
v-if="item?.comfyCommand?.keybinding"
|
||||||
@@ -94,13 +96,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { MenuItem } from 'primevue/menuitem'
|
import type { MenuItem as PrimeMenuItem } from 'primevue/menuitem'
|
||||||
import SelectButton from 'primevue/selectbutton'
|
import SelectButton from 'primevue/selectbutton'
|
||||||
import TieredMenu, {
|
import TieredMenu, {
|
||||||
type TieredMenuMethods,
|
type TieredMenuMethods,
|
||||||
type TieredMenuState
|
type TieredMenuState
|
||||||
} from 'primevue/tieredmenu'
|
} 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 { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
|
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
|
||||||
@@ -117,6 +120,15 @@ import { showNativeSystemMenu } from '@/utils/envUtil'
|
|||||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||||
import { whileMouseDown } from '@/utils/mouseDownUtil'
|
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 colorPaletteStore = useColorPaletteStore()
|
||||||
const menuItemsStore = useMenuItemStore()
|
const menuItemsStore = useMenuItemStore()
|
||||||
const commandStore = useCommandStore()
|
const commandStore = useCommandStore()
|
||||||
@@ -187,7 +199,7 @@ const extraMenuItems: MenuItem[] = [
|
|||||||
{
|
{
|
||||||
key: 'browse-templates',
|
key: 'browse-templates',
|
||||||
label: t('menuLabels.Browse Templates'),
|
label: t('menuLabels.Browse Templates'),
|
||||||
icon: 'pi pi-folder-open',
|
icon: { component: TemplateIcon },
|
||||||
command: () => commandStore.execute('Comfy.BrowseTemplates')
|
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 = () => {
|
const onMenuShow = () => {
|
||||||
@@ -303,6 +316,24 @@ const handleItemClick = (item: MenuItem, event: MouseEvent) => {
|
|||||||
const hasActiveStateSiblings = (item: MenuItem): boolean => {
|
const hasActiveStateSiblings = (item: MenuItem): boolean => {
|
||||||
return menuItemsStore.menuItemHasActiveStateChildren[item.parentPath]
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import type { Component } from 'vue'
|
||||||
|
|
||||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||||
import type { ComfyExtension } from '@/types/comfy'
|
import type { ComfyExtension } from '@/types/comfy'
|
||||||
@@ -11,7 +12,7 @@ export interface ComfyCommand {
|
|||||||
function: () => void | Promise<void>
|
function: () => void | Promise<void>
|
||||||
|
|
||||||
label?: string | (() => string)
|
label?: string | (() => string)
|
||||||
icon?: string | (() => string)
|
icon?: string | { component: Component } | (() => string)
|
||||||
tooltip?: string | (() => string)
|
tooltip?: string | (() => string)
|
||||||
menubarLabel?: string | (() => string) // Menubar item label, if different from command label
|
menubarLabel?: string | (() => string) // Menubar item label, if different from command label
|
||||||
versionAdded?: string
|
versionAdded?: string
|
||||||
@@ -25,7 +26,7 @@ export class ComfyCommandImpl implements ComfyCommand {
|
|||||||
id: string
|
id: string
|
||||||
function: () => void | Promise<void>
|
function: () => void | Promise<void>
|
||||||
_label?: string | (() => string)
|
_label?: string | (() => string)
|
||||||
_icon?: string | (() => string)
|
_icon?: string | { component: Component } | (() => string)
|
||||||
_tooltip?: string | (() => string)
|
_tooltip?: string | (() => string)
|
||||||
_menubarLabel?: string | (() => string)
|
_menubarLabel?: string | (() => string)
|
||||||
versionAdded?: string
|
versionAdded?: string
|
||||||
|
|||||||
@@ -57,7 +57,10 @@ export const useSidebarTabStore = defineStore('sidebarTab', () => {
|
|||||||
|
|
||||||
useCommandStore().registerCommand({
|
useCommandStore().registerCommand({
|
||||||
id: `Workspace.ToggleSidebarTab.${tab.id}`,
|
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,
|
label: labelFunction,
|
||||||
menubarLabel: menubarLabelFunction,
|
menubarLabel: menubarLabelFunction,
|
||||||
tooltip: tooltipFunction,
|
tooltip: tooltipFunction,
|
||||||
|
|||||||
Reference in New Issue
Block a user