mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-12 00:20:15 +00:00
merge main into rh-test
This commit is contained in:
@@ -28,29 +28,7 @@
|
||||
@show="onMenuShow"
|
||||
>
|
||||
<template #item="{ item, props }">
|
||||
<div
|
||||
v-if="item.key === 'theme'"
|
||||
class="flex items-center gap-4 px-4 py-5"
|
||||
@click.stop.prevent
|
||||
>
|
||||
{{ item.label }}
|
||||
<SelectButton
|
||||
:options="[darkLabel, lightLabel]"
|
||||
:model-value="activeTheme"
|
||||
@click.stop.prevent
|
||||
@update:model-value="onThemeChange"
|
||||
>
|
||||
<template #option="{ option }">
|
||||
<div class="flex items-center gap-2">
|
||||
<i v-if="option === lightLabel" class="pi pi-sun" />
|
||||
<i v-if="option === darkLabel" class="pi pi-moon" />
|
||||
<span>{{ option }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</SelectButton>
|
||||
</div>
|
||||
<a
|
||||
v-else
|
||||
class="p-menubar-item-link px-4 py-2"
|
||||
v-bind="props.action"
|
||||
:href="item.url"
|
||||
@@ -95,7 +73,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import TieredMenu, {
|
||||
type TieredMenuMethods,
|
||||
type TieredMenuState
|
||||
@@ -104,27 +81,30 @@ import { computed, nextTick, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
|
||||
import SettingDialogContent from '@/components/dialog/content/SettingDialogContent.vue'
|
||||
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useAboutPanelStore } from '@/stores/aboutPanelStore'
|
||||
import SettingDialogContent from '@/platform/settings/components/SettingDialogContent.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useColorPaletteService } from '@/services/colorPaletteService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useMenuItemStore } from '@/stores/menuItemStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { showNativeSystemMenu } from '@/utils/envUtil'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { whileMouseDown } from '@/utils/mouseDownUtil'
|
||||
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
||||
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const colorPaletteService = useColorPaletteService()
|
||||
const menuItemsStore = useMenuItemStore()
|
||||
const commandStore = useCommandStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const aboutPanelStore = useAboutPanelStore()
|
||||
const settingStore = useSettingStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const managerState = useManagerState()
|
||||
|
||||
const menuRef = ref<
|
||||
({ dirty: boolean } & TieredMenuMethods & TieredMenuState) | null
|
||||
>(null)
|
||||
@@ -157,31 +137,33 @@ const showSettings = (defaultPanel?: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
// Temporary duplicated from LoadWorkflowWarning.vue
|
||||
// Determines if ComfyUI-Manager is installed by checking for its badge in the about panel
|
||||
// This allows us to conditionally show the Manager button only when the extension is available
|
||||
// TODO: Remove this check when Manager functionality is fully migrated into core
|
||||
const isManagerInstalled = computed(() => {
|
||||
return aboutPanelStore.badges.some(
|
||||
(badge) =>
|
||||
badge.label.includes('ComfyUI-Manager') ||
|
||||
badge.url.includes('ComfyUI-Manager')
|
||||
)
|
||||
})
|
||||
|
||||
const showManageExtensions = () => {
|
||||
if (isManagerInstalled.value) {
|
||||
useDialogService().showManagerDialog()
|
||||
} else {
|
||||
showSettings('extension')
|
||||
}
|
||||
const showManageExtensions = async () => {
|
||||
await managerState.openManager({
|
||||
initialTab: ManagerTab.All,
|
||||
showToastOnLegacyError: false
|
||||
})
|
||||
}
|
||||
|
||||
const extraMenuItems = computed<MenuItem[]>(() => [
|
||||
const themeMenuItems = computed(() => {
|
||||
return colorPaletteStore.palettes.map((palette) => ({
|
||||
key: `theme-${palette.id}`,
|
||||
label: palette.name,
|
||||
parentPath: 'theme',
|
||||
comfyCommand: {
|
||||
active: () => colorPaletteStore.activePaletteId === palette.id
|
||||
},
|
||||
command: async () => {
|
||||
await colorPaletteService.loadColorPalette(palette.id)
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
const extraMenuItems = computed(() => [
|
||||
{ separator: true },
|
||||
{
|
||||
key: 'theme',
|
||||
label: t('menu.theme')
|
||||
label: t('menu.theme'),
|
||||
items: themeMenuItems.value
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
@@ -204,19 +186,6 @@ const extraMenuItems = computed<MenuItem[]>(() => [
|
||||
}
|
||||
])
|
||||
|
||||
const lightLabel = computed(() => t('menu.light'))
|
||||
const darkLabel = computed(() => t('menu.dark'))
|
||||
|
||||
const activeTheme = computed(() => {
|
||||
return colorPaletteStore.completedActivePalette.light_theme
|
||||
? lightLabel.value
|
||||
: darkLabel.value
|
||||
})
|
||||
|
||||
const onThemeChange = async () => {
|
||||
await commandStore.execute('Comfy.ToggleTheme')
|
||||
}
|
||||
|
||||
const translatedItems = computed(() => {
|
||||
const items = menuItemsStore.menuItems.map(translateMenuItem)
|
||||
let helpIndex = items.findIndex((item) => item.key === 'Help')
|
||||
@@ -301,11 +270,18 @@ const handleItemClick = (item: MenuItem, event: MouseEvent) => {
|
||||
}
|
||||
|
||||
const hasActiveStateSiblings = (item: MenuItem): boolean => {
|
||||
return menuItemsStore.menuItemHasActiveStateChildren[item.parentPath]
|
||||
// Check if this item has siblings with active state (either from store or theme items)
|
||||
return (
|
||||
item.parentPath &&
|
||||
(item.parentPath === 'theme' ||
|
||||
menuItemsStore.menuItemHasActiveStateChildren[item.parentPath])
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
:deep(.p-menubar-submenu.dropdown-direction-up) {
|
||||
@apply top-auto bottom-full flex-col-reverse;
|
||||
}
|
||||
@@ -323,12 +299,34 @@ const hasActiveStateSiblings = (item: MenuItem): boolean => {
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.comfy-command-menu {
|
||||
--p-tieredmenu-item-focus-background: color-mix(
|
||||
in srgb,
|
||||
var(--fg-color) 15%,
|
||||
transparent
|
||||
);
|
||||
--p-tieredmenu-item-active-background: color-mix(
|
||||
in srgb,
|
||||
var(--fg-color) 10%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
.comfy-command-menu ul {
|
||||
background-color: var(--comfy-menu-secondary-bg) !important;
|
||||
}
|
||||
|
||||
.comfy-command-menu-top .p-tieredmenu-submenu {
|
||||
left: calc(100% + 15px) !important;
|
||||
top: -4px !important;
|
||||
}
|
||||
@media (max-height: 700px) {
|
||||
.comfy-command-menu .p-tieredmenu-submenu {
|
||||
@apply absolute max-h-[90vh] overflow-y-auto;
|
||||
}
|
||||
/* Help (last) submenu upward offset in compact mode */
|
||||
.p-tieredmenu-root-list
|
||||
> .p-tieredmenu-item:last-of-type
|
||||
.p-tieredmenu-submenu {
|
||||
top: -188px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { VueWrapper, mount } from '@vue/test-utils'
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Button from 'primevue/button'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { h } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
import enMessages from '@/locales/en/main.json' with { type: 'json' }
|
||||
|
||||
import CurrentUserButton from './CurrentUserButton.vue'
|
||||
|
||||
@@ -9,9 +9,7 @@
|
||||
aria-label="user profile"
|
||||
@click="popover?.toggle($event)"
|
||||
>
|
||||
<div
|
||||
class="flex items-center rounded-full bg-[var(--p-content-background)]"
|
||||
>
|
||||
<div class="flex items-center rounded-full bg-(--p-content-background)">
|
||||
<UserAvatar :photo-url="photoURL" />
|
||||
|
||||
<i class="pi pi-chevron-down px-1" :style="{ fontSize: '0.5rem' }" />
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { VueWrapper, mount } from '@vue/test-utils'
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Button from 'primevue/button'
|
||||
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { h } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
import enMessages from '@/locales/en/main.json' with { type: 'json' }
|
||||
|
||||
import CurrentUserPopover from './CurrentUserPopover.vue'
|
||||
|
||||
@@ -41,11 +42,13 @@ afterAll(() => {
|
||||
})
|
||||
|
||||
// Mock the useCurrentUser composable
|
||||
const mockHandleSignOut = vi.fn()
|
||||
vi.mock('@/composables/auth/useCurrentUser', () => ({
|
||||
useCurrentUser: vi.fn(() => ({
|
||||
userPhotoUrl: 'https://example.com/avatar.jpg',
|
||||
userDisplayName: 'Test User',
|
||||
userEmail: 'test@example.com'
|
||||
userEmail: 'test@example.com',
|
||||
handleSignOut: mockHandleSignOut
|
||||
}))
|
||||
}))
|
||||
|
||||
@@ -155,8 +158,8 @@ describe('CurrentUserPopover', () => {
|
||||
// Click the logout button
|
||||
await logoutButton.trigger('click')
|
||||
|
||||
// Verify logout was called
|
||||
expect(mockLogout).toHaveBeenCalled()
|
||||
// Verify handleSignOut was called
|
||||
expect(mockHandleSignOut).toHaveBeenCalled()
|
||||
|
||||
// Verify close event was emitted
|
||||
expect(wrapper.emitted('close')).toBeTruthy()
|
||||
@@ -8,7 +8,7 @@
|
||||
class="mb-3"
|
||||
:photo-url="userPhotoUrl"
|
||||
:pt:icon:class="{
|
||||
'!text-2xl': !userPhotoUrl
|
||||
'text-2xl!': !userPhotoUrl
|
||||
}"
|
||||
size="large"
|
||||
/>
|
||||
@@ -88,7 +88,8 @@ const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const { userDisplayName, userEmail, userPhotoUrl } = useCurrentUser()
|
||||
const { userDisplayName, userEmail, userPhotoUrl, handleSignOut } =
|
||||
useCurrentUser()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const dialogService = useDialogService()
|
||||
|
||||
@@ -103,7 +104,7 @@ const handleTopUp = () => {
|
||||
}
|
||||
|
||||
const handleLogout = async () => {
|
||||
await authActions.logout()
|
||||
await handleSignOut()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<div
|
||||
v-show="showTopMenu && workflowTabsPosition === 'Topbar'"
|
||||
class="w-full flex content-end z-[1001] h-[38px]"
|
||||
class="w-full flex content-end z-1001 h-[38px]"
|
||||
style="background: var(--border-color)"
|
||||
>
|
||||
<WorkflowTabs />
|
||||
@@ -14,19 +14,19 @@
|
||||
:class="{ dropzone: isDropZone, 'dropzone-active': isDroppable }"
|
||||
>
|
||||
<CommandMenubar />
|
||||
<div class="flex-grow min-w-0 app-drag h-full"></div>
|
||||
<div class="grow min-w-0 app-drag h-full"></div>
|
||||
<div
|
||||
ref="menuRight"
|
||||
class="comfyui-menu-right flex-shrink-1 overflow-auto"
|
||||
/>
|
||||
<Actionbar />
|
||||
<CurrentUserButton class="flex-shrink-0" />
|
||||
<CurrentUserButton class="shrink-0" />
|
||||
</div>
|
||||
|
||||
<!-- Virtual top menu for native window (drag handle) -->
|
||||
<div
|
||||
v-show="isNativeWindow() && !showTopMenu"
|
||||
class="fixed top-0 left-0 app-drag w-full h-[var(--comfy-topbar-height)]"
|
||||
class="fixed top-0 left-0 app-drag w-full h-(--comfy-topbar-height)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -39,8 +39,8 @@ import Actionbar from '@/components/actionbar/ComfyActionbar.vue'
|
||||
import CommandMenubar from '@/components/topbar/CommandMenubar.vue'
|
||||
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
|
||||
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { electronAPI, isElectron, isNativeWindow } from '@/utils/envUtil'
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ import Button from 'primevue/button'
|
||||
import Menu from 'primevue/menu'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import type { ComfyWorkflow } from '@/stores/workflowStore'
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
||||
|
||||
const props = defineProps<{
|
||||
workflows: ComfyWorkflow[]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
ref="workflowTabRef"
|
||||
class="flex p-2 gap-2 workflow-tab"
|
||||
class="flex p-2 gap-2 workflow-tab group"
|
||||
v-bind="$attrs"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
@@ -11,9 +11,13 @@
|
||||
{{ workflowOption.workflow.filename }}
|
||||
</span>
|
||||
<div class="relative">
|
||||
<span v-if="shouldShowStatusIndicator" class="status-indicator">•</span>
|
||||
<span
|
||||
v-if="shouldShowStatusIndicator"
|
||||
class="group-hover:hidden absolute font-bold text-2xl top-1/2 left-1/2 -translate-1/2 z-10 bg-(--comfy-menu-secondary-bg) w-4"
|
||||
>•</span
|
||||
>
|
||||
<Button
|
||||
class="close-button p-0 w-auto"
|
||||
class="close-button p-0 w-auto invisible"
|
||||
icon="pi pi-times"
|
||||
text
|
||||
severity="secondary"
|
||||
@@ -40,11 +44,11 @@ import {
|
||||
usePragmaticDraggable,
|
||||
usePragmaticDroppable
|
||||
} from '@/composables/usePragmaticDragAndDrop'
|
||||
import { useWorkflowThumbnail } from '@/renderer/thumbnail/composables/useWorkflowThumbnail'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { ComfyWorkflow } from '@/stores/workflowStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useWorkflowThumbnail } from '@/renderer/core/thumbnail/useWorkflowThumbnail'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
import WorkflowTabPopover from './WorkflowTabPopover.vue'
|
||||
@@ -174,16 +178,6 @@ onUnmounted(() => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.status-indicator {
|
||||
@apply absolute font-bold;
|
||||
font-size: 1.5rem;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.p-tooltip.workflow-tab-tooltip {
|
||||
z-index: 1200 !important;
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
import Popover from 'primevue/popover'
|
||||
import { computed, nextTick, ref, toRefs, useId } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
|
||||
const POPOVER_WIDTH = 250
|
||||
|
||||
@@ -169,6 +169,8 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
.workflow-preview-content {
|
||||
@apply flex flex-col rounded-xl overflow-hidden;
|
||||
max-width: var(--popover-width);
|
||||
@@ -204,6 +206,8 @@ defineExpose({
|
||||
</style>
|
||||
|
||||
<style>
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
.workflow-popover-fade {
|
||||
--p-popover-background: transparent;
|
||||
--p-popover-content-padding: 0;
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
/>
|
||||
<Button
|
||||
v-tooltip="{ value: $t('sideToolbar.newBlankWorkflow'), showDelay: 300 }"
|
||||
class="new-blank-workflow-button flex-shrink-0 no-drag rounded-none"
|
||||
class="new-blank-workflow-button shrink-0 no-drag rounded-none"
|
||||
icon="pi pi-plus"
|
||||
text
|
||||
severity="secondary"
|
||||
@@ -65,7 +65,7 @@
|
||||
<ContextMenu ref="menu" :model="contextMenuItems" />
|
||||
<div
|
||||
v-if="menuSetting !== 'Bottom' && isDesktop"
|
||||
class="window-actions-spacer flex-shrink-0 app-drag"
|
||||
class="window-actions-spacer shrink-0 app-drag"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -81,11 +81,12 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import WorkflowTab from '@/components/topbar/WorkflowTab.vue'
|
||||
import { useOverflowObserver } from '@/composables/element/useOverflowObserver'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useWorkflowBookmarkStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { ComfyWorkflow, useWorkflowBookmarkStore } from '@/stores/workflowStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
import { whileMouseDown } from '@/utils/mouseDownUtil'
|
||||
@@ -302,12 +303,14 @@ onUpdated(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
.workflow-tabs-container {
|
||||
background-color: var(--comfy-menu-secondary-bg);
|
||||
}
|
||||
|
||||
:deep(.p-togglebutton) {
|
||||
@apply p-0 bg-transparent rounded-none flex-shrink relative border-0 border-r border-solid;
|
||||
@apply p-0 bg-transparent rounded-none shrink relative border-0 border-r border-solid;
|
||||
border-right-color: var(--border-color);
|
||||
min-width: 90px;
|
||||
}
|
||||
@@ -355,14 +358,6 @@ onUpdated(() => {
|
||||
@apply visible;
|
||||
}
|
||||
|
||||
:deep(.p-togglebutton:hover) .status-indicator {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
:deep(.p-togglebutton) .close-button {
|
||||
@apply invisible;
|
||||
}
|
||||
|
||||
:deep(.p-scrollpanel-content) {
|
||||
@apply h-full;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user