mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +00:00
Integrated tab bar UI elements (#7853)
## Summary The current help / feedback is often overlooked by users, this adds a setting that makes it more visible moving it up into the tab bar and moves the user login/profile button out of the "action bar" into the tab bar. ## Changes - Add 'Comfy.UI.TabBarLayout' setting with Default/Integrated options - Move Help & User controls to tab bar when Integrated mode is enabled - Extract help center logic into shared useHelpCenter composable ## Screenshots (if applicable) <img width="515" height="540" alt="image" src="https://github.com/user-attachments/assets/c9e6057f-4fb1-4da6-b25d-9df4b19be31a" /> <img width="835" height="268" alt="image" src="https://github.com/user-attachments/assets/24afc0e8-97eb-45cf-af86-15a9b464e9a8" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7853-Integrated-tab-bar-UI-elements-2df6d73d365081b1beb8f7c641c2fa43) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action <action@github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -59,8 +59,11 @@
|
|||||||
{{ queuedCount }}
|
{{ queuedCount }}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
<CurrentUserButton v-if="isLoggedIn" class="shrink-0" />
|
<CurrentUserButton
|
||||||
<LoginButton v-else-if="isDesktop" />
|
v-if="isLoggedIn && !isIntegratedTabBar"
|
||||||
|
class="shrink-0"
|
||||||
|
/>
|
||||||
|
<LoginButton v-else-if="isDesktop && !isIntegratedTabBar" />
|
||||||
<Button
|
<Button
|
||||||
v-if="!isRightSidePanelOpen"
|
v-if="!isRightSidePanelOpen"
|
||||||
v-tooltip.bottom="rightSidePanelTooltipConfig"
|
v-tooltip.bottom="rightSidePanelTooltipConfig"
|
||||||
@@ -96,6 +99,7 @@ import Button from '@/components/ui/button/Button.vue'
|
|||||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||||
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
|
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import { useCommandStore } from '@/stores/commandStore'
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
@@ -107,6 +111,7 @@ import { useConflictAcknowledgment } from '@/workbench/extensions/manager/compos
|
|||||||
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
||||||
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||||
|
|
||||||
|
const settingStore = useSettingStore()
|
||||||
const workspaceStore = useWorkspaceStore()
|
const workspaceStore = useWorkspaceStore()
|
||||||
const rightSidePanelStore = useRightSidePanelStore()
|
const rightSidePanelStore = useRightSidePanelStore()
|
||||||
const managerState = useManagerState()
|
const managerState = useManagerState()
|
||||||
@@ -124,6 +129,9 @@ const { shouldShowRedDot: shouldShowConflictRedDot } =
|
|||||||
useConflictAcknowledgment()
|
useConflictAcknowledgment()
|
||||||
const isTopMenuHovered = ref(false)
|
const isTopMenuHovered = ref(false)
|
||||||
const queuedCount = computed(() => queueStore.pendingTasks.length)
|
const queuedCount = computed(() => queueStore.pendingTasks.length)
|
||||||
|
const isIntegratedTabBar = computed(
|
||||||
|
() => settingStore.get('Comfy.UI.TabBarLayout') === 'Integrated'
|
||||||
|
)
|
||||||
const queueHistoryTooltipConfig = computed(() =>
|
const queueHistoryTooltipConfig = computed(() =>
|
||||||
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory'))
|
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory'))
|
||||||
)
|
)
|
||||||
|
|||||||
134
src/components/helpcenter/HelpCenterPopups.vue
Normal file
134
src/components/helpcenter/HelpCenterPopups.vue
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Help Center Popup positioned within canvas area -->
|
||||||
|
<Teleport to="#graph-canvas-container">
|
||||||
|
<div
|
||||||
|
v-if="isHelpCenterVisible"
|
||||||
|
class="help-center-popup"
|
||||||
|
:class="{
|
||||||
|
'sidebar-left':
|
||||||
|
triggerLocation === 'sidebar' && sidebarLocation === 'left',
|
||||||
|
'sidebar-right':
|
||||||
|
triggerLocation === 'sidebar' && sidebarLocation === 'right',
|
||||||
|
'topbar-right': triggerLocation === 'topbar',
|
||||||
|
'small-sidebar': isSmall
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<HelpCenterMenuContent @close="closeHelpCenter" />
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
|
||||||
|
<!-- Release Notification Toast positioned within canvas area -->
|
||||||
|
<Teleport to="#graph-canvas-container">
|
||||||
|
<ReleaseNotificationToast
|
||||||
|
:class="{
|
||||||
|
'sidebar-left': sidebarLocation === 'left',
|
||||||
|
'sidebar-right': sidebarLocation === 'right',
|
||||||
|
'small-sidebar': isSmall
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
|
||||||
|
<!-- WhatsNew Popup positioned within canvas area -->
|
||||||
|
<Teleport to="#graph-canvas-container">
|
||||||
|
<WhatsNewPopup
|
||||||
|
:class="{
|
||||||
|
'sidebar-left': sidebarLocation === 'left',
|
||||||
|
'sidebar-right': sidebarLocation === 'right',
|
||||||
|
'small-sidebar': isSmall
|
||||||
|
}"
|
||||||
|
@whats-new-dismissed="handleWhatsNewDismissed"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
|
||||||
|
<!-- Backdrop to close popup when clicking outside -->
|
||||||
|
<Teleport to="body">
|
||||||
|
<div
|
||||||
|
v-if="isHelpCenterVisible"
|
||||||
|
class="help-center-backdrop"
|
||||||
|
@click="closeHelpCenter"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useHelpCenter } from '@/composables/useHelpCenter'
|
||||||
|
import ReleaseNotificationToast from '@/platform/updates/components/ReleaseNotificationToast.vue'
|
||||||
|
import WhatsNewPopup from '@/platform/updates/components/WhatsNewPopup.vue'
|
||||||
|
|
||||||
|
import HelpCenterMenuContent from './HelpCenterMenuContent.vue'
|
||||||
|
|
||||||
|
const { isSmall = false } = defineProps<{
|
||||||
|
isSmall?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
isHelpCenterVisible,
|
||||||
|
triggerLocation,
|
||||||
|
sidebarLocation,
|
||||||
|
closeHelpCenter,
|
||||||
|
handleWhatsNewDismissed
|
||||||
|
} = useHelpCenter()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.help-center-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-center-popup {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1rem;
|
||||||
|
z-index: 10000;
|
||||||
|
animation: slideInUp 0.2s ease-out;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-center-popup.sidebar-left {
|
||||||
|
left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-center-popup.sidebar-left.small-sidebar {
|
||||||
|
left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-center-popup.sidebar-right {
|
||||||
|
right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-center-popup.topbar-right {
|
||||||
|
top: 2rem;
|
||||||
|
right: 1rem;
|
||||||
|
bottom: auto;
|
||||||
|
animation: slideInDown 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -40,12 +40,13 @@
|
|||||||
v-if="userStore.isMultiUserServer"
|
v-if="userStore.isMultiUserServer"
|
||||||
:is-small="isSmall"
|
:is-small="isSmall"
|
||||||
/>
|
/>
|
||||||
<SidebarHelpCenterIcon :is-small="isSmall" />
|
<SidebarHelpCenterIcon v-if="!isIntegratedTabBar" :is-small="isSmall" />
|
||||||
<SidebarBottomPanelToggleButton :is-small="isSmall" />
|
<SidebarBottomPanelToggleButton :is-small="isSmall" />
|
||||||
<SidebarShortcutsToggleButton :is-small="isSmall" />
|
<SidebarShortcutsToggleButton :is-small="isSmall" />
|
||||||
<SidebarSettingsButton :is-small="isSmall" />
|
<SidebarSettingsButton :is-small="isSmall" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<HelpCenterPopups :is-small="isSmall" />
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -54,6 +55,7 @@ import { useResizeObserver } from '@vueuse/core'
|
|||||||
import { debounce } from 'es-toolkit/compat'
|
import { debounce } from 'es-toolkit/compat'
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import HelpCenterPopups from '@/components/helpcenter/HelpCenterPopups.vue'
|
||||||
import ComfyMenuButton from '@/components/sidebar/ComfyMenuButton.vue'
|
import ComfyMenuButton from '@/components/sidebar/ComfyMenuButton.vue'
|
||||||
import SidebarBottomPanelToggleButton from '@/components/sidebar/SidebarBottomPanelToggleButton.vue'
|
import SidebarBottomPanelToggleButton from '@/components/sidebar/SidebarBottomPanelToggleButton.vue'
|
||||||
import SidebarSettingsButton from '@/components/sidebar/SidebarSettingsButton.vue'
|
import SidebarSettingsButton from '@/components/sidebar/SidebarSettingsButton.vue'
|
||||||
@@ -89,6 +91,9 @@ const sidebarLocation = computed<'left' | 'right'>(() =>
|
|||||||
settingStore.get('Comfy.Sidebar.Location')
|
settingStore.get('Comfy.Sidebar.Location')
|
||||||
)
|
)
|
||||||
const sidebarStyle = computed(() => settingStore.get('Comfy.Sidebar.Style'))
|
const sidebarStyle = computed(() => settingStore.get('Comfy.Sidebar.Style'))
|
||||||
|
const isIntegratedTabBar = computed(
|
||||||
|
() => settingStore.get('Comfy.UI.TabBarLayout') === 'Integrated'
|
||||||
|
)
|
||||||
const isConnected = computed(
|
const isConnected = computed(
|
||||||
() =>
|
() =>
|
||||||
selectedTab.value ||
|
selectedTab.value ||
|
||||||
|
|||||||
@@ -1,204 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<SidebarIcon
|
||||||
<SidebarIcon
|
icon="pi pi-question-circle"
|
||||||
icon="pi pi-question-circle"
|
class="comfy-help-center-btn"
|
||||||
class="comfy-help-center-btn"
|
:label="$t('menu.help')"
|
||||||
:label="$t('menu.help')"
|
:tooltip="$t('sideToolbar.helpCenter')"
|
||||||
:tooltip="$t('sideToolbar.helpCenter')"
|
:icon-badge="shouldShowRedDot ? '•' : ''"
|
||||||
:icon-badge="shouldShowRedDot ? '•' : ''"
|
:is-small="isSmall"
|
||||||
:is-small="isSmall"
|
@click="toggleHelpCenter"
|
||||||
@click="toggleHelpCenter"
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Help Center Popup positioned within canvas area -->
|
|
||||||
<Teleport to="#graph-canvas-container">
|
|
||||||
<div
|
|
||||||
v-if="isHelpCenterVisible"
|
|
||||||
class="help-center-popup"
|
|
||||||
:class="{
|
|
||||||
'sidebar-left': sidebarLocation === 'left',
|
|
||||||
'sidebar-right': sidebarLocation === 'right',
|
|
||||||
'small-sidebar': isSmall
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<HelpCenterMenuContent @close="closeHelpCenter" />
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
|
|
||||||
<!-- Release Notification Toast positioned within canvas area -->
|
|
||||||
<Teleport to="#graph-canvas-container">
|
|
||||||
<ReleaseNotificationToast
|
|
||||||
:class="{
|
|
||||||
'sidebar-left': sidebarLocation === 'left',
|
|
||||||
'sidebar-right': sidebarLocation === 'right',
|
|
||||||
'small-sidebar': isSmall
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</Teleport>
|
|
||||||
|
|
||||||
<!-- WhatsNew Popup positioned within canvas area -->
|
|
||||||
<Teleport to="#graph-canvas-container">
|
|
||||||
<WhatsNewPopup
|
|
||||||
:class="{
|
|
||||||
'sidebar-left': sidebarLocation === 'left',
|
|
||||||
'sidebar-right': sidebarLocation === 'right',
|
|
||||||
'small-sidebar': isSmall
|
|
||||||
}"
|
|
||||||
@whats-new-dismissed="handleWhatsNewDismissed"
|
|
||||||
/>
|
|
||||||
</Teleport>
|
|
||||||
|
|
||||||
<!-- Backdrop to close popup when clicking outside -->
|
|
||||||
<Teleport to="body">
|
|
||||||
<div
|
|
||||||
v-if="isHelpCenterVisible"
|
|
||||||
class="help-center-backdrop"
|
|
||||||
@click="closeHelpCenter"
|
|
||||||
/>
|
|
||||||
</Teleport>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { storeToRefs } from 'pinia'
|
import { useHelpCenter } from '@/composables/useHelpCenter'
|
||||||
import { computed, onMounted, toRefs } from 'vue'
|
|
||||||
|
|
||||||
import HelpCenterMenuContent from '@/components/helpcenter/HelpCenterMenuContent.vue'
|
|
||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
|
||||||
import { useTelemetry } from '@/platform/telemetry'
|
|
||||||
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
|
|
||||||
import ReleaseNotificationToast from '@/platform/updates/components/ReleaseNotificationToast.vue'
|
|
||||||
import WhatsNewPopup from '@/platform/updates/components/WhatsNewPopup.vue'
|
|
||||||
import { useDialogService } from '@/services/dialogService'
|
|
||||||
import { useHelpCenterStore } from '@/stores/helpCenterStore'
|
|
||||||
import { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment'
|
|
||||||
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
|
|
||||||
|
|
||||||
import SidebarIcon from './SidebarIcon.vue'
|
import SidebarIcon from './SidebarIcon.vue'
|
||||||
|
|
||||||
const settingStore = useSettingStore()
|
defineProps<{
|
||||||
const releaseStore = useReleaseStore()
|
|
||||||
const helpCenterStore = useHelpCenterStore()
|
|
||||||
const { isVisible: isHelpCenterVisible } = storeToRefs(helpCenterStore)
|
|
||||||
const { shouldShowRedDot: showReleaseRedDot } = storeToRefs(releaseStore)
|
|
||||||
|
|
||||||
const conflictDetection = useConflictDetection()
|
|
||||||
|
|
||||||
const { showNodeConflictDialog } = useDialogService()
|
|
||||||
|
|
||||||
// Use conflict acknowledgment state from composable - call only once
|
|
||||||
const { shouldShowRedDot: shouldShowConflictRedDot, markConflictsAsSeen } =
|
|
||||||
useConflictAcknowledgment()
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
isSmall: boolean
|
isSmall: boolean
|
||||||
}>()
|
}>()
|
||||||
const { isSmall } = toRefs(props)
|
|
||||||
|
|
||||||
// Use either release red dot or conflict red dot
|
const { shouldShowRedDot, toggleHelpCenter } = useHelpCenter()
|
||||||
const shouldShowRedDot = computed((): boolean => {
|
|
||||||
const releaseRedDot = showReleaseRedDot.value
|
|
||||||
return releaseRedDot || shouldShowConflictRedDot.value
|
|
||||||
})
|
|
||||||
|
|
||||||
const sidebarLocation = computed(() =>
|
|
||||||
settingStore.get('Comfy.Sidebar.Location')
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle Help Center and track UI button click.
|
|
||||||
*/
|
|
||||||
const toggleHelpCenter = () => {
|
|
||||||
useTelemetry()?.trackUiButtonClicked({
|
|
||||||
button_id: 'sidebar_help_center_toggled'
|
|
||||||
})
|
|
||||||
helpCenterStore.toggle()
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeHelpCenter = () => {
|
|
||||||
helpCenterStore.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle What's New popup dismissal
|
|
||||||
* Check if conflict modal should be shown after ComfyUI update
|
|
||||||
*/
|
|
||||||
const handleWhatsNewDismissed = async () => {
|
|
||||||
try {
|
|
||||||
// Check if conflict modal should be shown after update
|
|
||||||
const shouldShow =
|
|
||||||
await conflictDetection.shouldShowConflictModalAfterUpdate()
|
|
||||||
if (shouldShow) {
|
|
||||||
showConflictModal()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[HelpCenter] Error checking conflict modal:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Show the node conflict dialog with current conflict data
|
|
||||||
*/
|
|
||||||
const showConflictModal = () => {
|
|
||||||
showNodeConflictDialog({
|
|
||||||
showAfterWhatsNew: true,
|
|
||||||
dialogComponentProps: {
|
|
||||||
onClose: () => {
|
|
||||||
markConflictsAsSeen()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize release store on mount
|
|
||||||
onMounted(async () => {
|
|
||||||
// Initialize release store to fetch releases for toast and popup
|
|
||||||
await releaseStore.initialize()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.help-center-backdrop {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 9999;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.help-center-popup {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 1rem;
|
|
||||||
z-index: 10000;
|
|
||||||
animation: slideInUp 0.2s ease-out;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.help-center-popup.sidebar-left {
|
|
||||||
left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.help-center-popup.sidebar-left.small-sidebar {
|
|
||||||
left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.help-center-popup.sidebar-right {
|
|
||||||
right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideInUp {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.p-badge) {
|
:deep(.p-badge) {
|
||||||
background: #ff3b30;
|
background: #ff3b30;
|
||||||
color: #ff3b30;
|
color: #ff3b30;
|
||||||
|
|||||||
@@ -9,11 +9,16 @@
|
|||||||
@click="popover?.toggle($event)"
|
@click="popover?.toggle($event)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-1 rounded-full hover:bg-interface-button-hover-surface"
|
:class="
|
||||||
|
cn(
|
||||||
|
'flex items-center gap-1 rounded-full hover:bg-interface-button-hover-surface justify-center',
|
||||||
|
compact && 'size-full '
|
||||||
|
)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<UserAvatar :photo-url="photoURL" />
|
<UserAvatar :photo-url="photoURL" :class="compact && 'size-full'" />
|
||||||
|
|
||||||
<i class="icon-[lucide--chevron-down] size-3 px-1" />
|
<i v-if="showArrow" class="icon-[lucide--chevron-down] size-3 px-1" />
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -38,9 +43,15 @@ import { computed, ref } from 'vue'
|
|||||||
import UserAvatar from '@/components/common/UserAvatar.vue'
|
import UserAvatar from '@/components/common/UserAvatar.vue'
|
||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||||
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
import CurrentUserPopover from './CurrentUserPopover.vue'
|
import CurrentUserPopover from './CurrentUserPopover.vue'
|
||||||
|
|
||||||
|
const { showArrow = true, compact = false } = defineProps<{
|
||||||
|
showArrow?: boolean
|
||||||
|
compact?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
const { isLoggedIn, userPhotoUrl } = useCurrentUser()
|
const { isLoggedIn, userPhotoUrl } = useCurrentUser()
|
||||||
|
|
||||||
const popover = ref<InstanceType<typeof Popover> | null>(null)
|
const popover = ref<InstanceType<typeof Popover> | null>(null)
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<Button
|
<Button
|
||||||
v-if="!isLoggedIn"
|
v-if="!isLoggedIn"
|
||||||
variant="secondary"
|
variant="textonly"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="rounded-full bg-secondary-background text-base-foreground hover:bg-secondary-background-hover"
|
:class="cn('group rounded-full text-base-foreground p-0', className)"
|
||||||
:aria-label="t('g.login')"
|
:aria-label="t('g.login')"
|
||||||
@click="handleSignIn()"
|
@click="handleSignIn()"
|
||||||
@mouseenter="showPopover"
|
@mouseenter="showPopover"
|
||||||
@mouseleave="hidePopover"
|
@mouseleave="hidePopover"
|
||||||
>
|
>
|
||||||
<i class="icon-[lucide--user] size-4" />
|
<span
|
||||||
|
class="flex size-full items-center justify-center rounded-full bg-secondary-background transition-colors group-hover:bg-transparent"
|
||||||
|
>
|
||||||
|
<i class="icon-[lucide--user] size-4" />
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Popover
|
<Popover
|
||||||
ref="popoverRef"
|
ref="popoverRef"
|
||||||
@@ -31,12 +35,18 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Popover from 'primevue/popover'
|
import Popover from 'primevue/popover'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||||
import { useExternalLink } from '@/composables/useExternalLink'
|
import { useExternalLink } from '@/composables/useExternalLink'
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
|
const { class: className } = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
|
||||||
const { isLoggedIn, handleSignIn } = useCurrentUser()
|
const { isLoggedIn, handleSignIn } = useCurrentUser()
|
||||||
const { buildDocsUrl } = useExternalLink()
|
const { buildDocsUrl } = useExternalLink()
|
||||||
|
|||||||
21
src/components/topbar/TopMenuHelpButton.vue
Normal file
21
src/components/topbar/TopMenuHelpButton.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<Button
|
||||||
|
class="comfy-help-center-btn relative text-base-foreground"
|
||||||
|
variant="textonly"
|
||||||
|
@click="toggleHelpCenter"
|
||||||
|
>
|
||||||
|
{{ $t('menu.helpAndFeedback') }}
|
||||||
|
<i class="icon-[lucide--circle-help] ml-0.5" />
|
||||||
|
<span
|
||||||
|
v-if="shouldShowRedDot"
|
||||||
|
class="absolute top-[7px] right-[7px] size-1.5 rounded-full bg-[#ff3b30]"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
|
import { useHelpCenter } from '@/composables/useHelpCenter'
|
||||||
|
|
||||||
|
const { shouldShowRedDot, toggleHelpCenter } = useHelpCenter('topbar')
|
||||||
|
</script>
|
||||||
@@ -67,6 +67,19 @@
|
|||||||
>
|
>
|
||||||
<i class="pi pi-plus" />
|
<i class="pi pi-plus" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<div
|
||||||
|
v-if="isIntegratedTabBar"
|
||||||
|
class="ml-auto flex shrink-0 items-center gap-2 px-2"
|
||||||
|
>
|
||||||
|
<TopMenuHelpButton />
|
||||||
|
<CurrentUserButton
|
||||||
|
v-if="isLoggedIn"
|
||||||
|
:show-arrow="false"
|
||||||
|
compact
|
||||||
|
class="shrink-0 p-1"
|
||||||
|
/>
|
||||||
|
<LoginButton v-else-if="isDesktop" class="p-1" />
|
||||||
|
</div>
|
||||||
<ContextMenu ref="menu" :model="contextMenuItems" />
|
<ContextMenu ref="menu" :model="contextMenuItems" />
|
||||||
<div v-if="isDesktop" class="window-actions-spacer app-drag shrink-0" />
|
<div v-if="isDesktop" class="window-actions-spacer app-drag shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
@@ -81,9 +94,14 @@ import { computed, nextTick, onUpdated, ref, watch } from 'vue'
|
|||||||
import type { WatchStopHandle } from 'vue'
|
import type { WatchStopHandle } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
|
||||||
|
import LoginButton from '@/components/topbar/LoginButton.vue'
|
||||||
|
import TopMenuHelpButton from '@/components/topbar/TopMenuHelpButton.vue'
|
||||||
import WorkflowTab from '@/components/topbar/WorkflowTab.vue'
|
import WorkflowTab from '@/components/topbar/WorkflowTab.vue'
|
||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
|
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||||
import { useOverflowObserver } from '@/composables/element/useOverflowObserver'
|
import { useOverflowObserver } from '@/composables/element/useOverflowObserver'
|
||||||
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||||
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
||||||
import {
|
import {
|
||||||
@@ -107,10 +125,16 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const settingStore = useSettingStore()
|
||||||
const workspaceStore = useWorkspaceStore()
|
const workspaceStore = useWorkspaceStore()
|
||||||
const workflowStore = useWorkflowStore()
|
const workflowStore = useWorkflowStore()
|
||||||
const workflowBookmarkStore = useWorkflowBookmarkStore()
|
const workflowBookmarkStore = useWorkflowBookmarkStore()
|
||||||
const workflowService = useWorkflowService()
|
const workflowService = useWorkflowService()
|
||||||
|
const { isLoggedIn } = useCurrentUser()
|
||||||
|
|
||||||
|
const isIntegratedTabBar = computed(
|
||||||
|
() => settingStore.get('Comfy.UI.TabBarLayout') === 'Integrated'
|
||||||
|
)
|
||||||
|
|
||||||
const rightClickedTab = ref<WorkflowOption | undefined>()
|
const rightClickedTab = ref<WorkflowOption | undefined>()
|
||||||
const menu = ref()
|
const menu = ref()
|
||||||
|
|||||||
100
src/composables/useHelpCenter.ts
Normal file
100
src/composables/useHelpCenter.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { computed, onMounted } from 'vue'
|
||||||
|
|
||||||
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
|
import { useTelemetry } from '@/platform/telemetry'
|
||||||
|
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
|
||||||
|
import { useDialogService } from '@/services/dialogService'
|
||||||
|
import { useHelpCenterStore } from '@/stores/helpCenterStore'
|
||||||
|
import type { HelpCenterTriggerLocation } from '@/stores/helpCenterStore'
|
||||||
|
import { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment'
|
||||||
|
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
|
||||||
|
|
||||||
|
export function useHelpCenter(
|
||||||
|
triggerFrom: HelpCenterTriggerLocation = 'sidebar'
|
||||||
|
) {
|
||||||
|
const settingStore = useSettingStore()
|
||||||
|
const releaseStore = useReleaseStore()
|
||||||
|
const helpCenterStore = useHelpCenterStore()
|
||||||
|
const { isVisible: isHelpCenterVisible, triggerLocation } =
|
||||||
|
storeToRefs(helpCenterStore)
|
||||||
|
const { shouldShowRedDot: showReleaseRedDot } = storeToRefs(releaseStore)
|
||||||
|
|
||||||
|
const conflictDetection = useConflictDetection()
|
||||||
|
const { showNodeConflictDialog } = useDialogService()
|
||||||
|
|
||||||
|
// Use conflict acknowledgment state from composable - call only once
|
||||||
|
const { shouldShowRedDot: shouldShowConflictRedDot, markConflictsAsSeen } =
|
||||||
|
useConflictAcknowledgment()
|
||||||
|
|
||||||
|
// Use either release red dot or conflict red dot
|
||||||
|
const shouldShowRedDot = computed((): boolean => {
|
||||||
|
const releaseRedDot = showReleaseRedDot.value
|
||||||
|
return releaseRedDot || shouldShowConflictRedDot.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const sidebarLocation = computed(() =>
|
||||||
|
settingStore.get('Comfy.Sidebar.Location')
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle Help Center and track UI button click.
|
||||||
|
*/
|
||||||
|
const toggleHelpCenter = () => {
|
||||||
|
useTelemetry()?.trackUiButtonClicked({
|
||||||
|
button_id: `${triggerFrom}_help_center_toggled`
|
||||||
|
})
|
||||||
|
helpCenterStore.toggle(triggerFrom)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeHelpCenter = () => {
|
||||||
|
helpCenterStore.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle What's New popup dismissal
|
||||||
|
* Check if conflict modal should be shown after ComfyUI update
|
||||||
|
*/
|
||||||
|
const handleWhatsNewDismissed = async () => {
|
||||||
|
try {
|
||||||
|
// Check if conflict modal should be shown after update
|
||||||
|
const shouldShow =
|
||||||
|
await conflictDetection.shouldShowConflictModalAfterUpdate()
|
||||||
|
if (shouldShow) {
|
||||||
|
showConflictModal()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[HelpCenter] Error checking conflict modal:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the node conflict dialog with current conflict data
|
||||||
|
*/
|
||||||
|
const showConflictModal = () => {
|
||||||
|
showNodeConflictDialog({
|
||||||
|
showAfterWhatsNew: true,
|
||||||
|
dialogComponentProps: {
|
||||||
|
onClose: () => {
|
||||||
|
markConflictsAsSeen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize release store on mount
|
||||||
|
onMounted(async () => {
|
||||||
|
// Initialize release store to fetch releases for toast and popup
|
||||||
|
await releaseStore.initialize()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
isHelpCenterVisible,
|
||||||
|
triggerLocation,
|
||||||
|
shouldShowRedDot,
|
||||||
|
sidebarLocation,
|
||||||
|
toggleHelpCenter,
|
||||||
|
closeHelpCenter,
|
||||||
|
handleWhatsNewDismissed
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -838,6 +838,7 @@
|
|||||||
"customNodesManager": "Custom Nodes Manager",
|
"customNodesManager": "Custom Nodes Manager",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
|
"helpAndFeedback": "Help & Feedback",
|
||||||
"queue": "Queue Panel"
|
"queue": "Queue Panel"
|
||||||
},
|
},
|
||||||
"tabMenu": {
|
"tabMenu": {
|
||||||
|
|||||||
@@ -369,6 +369,14 @@
|
|||||||
"Comfy_TreeExplorer_ItemPadding": {
|
"Comfy_TreeExplorer_ItemPadding": {
|
||||||
"name": "Tree explorer item padding"
|
"name": "Tree explorer item padding"
|
||||||
},
|
},
|
||||||
|
"Comfy_UI_TabBarLayout": {
|
||||||
|
"name": "Tab Bar Layout",
|
||||||
|
"tooltip": "Controls the layout of the tab bar. \"Integrated\" moves Help and User controls into the tab bar area.",
|
||||||
|
"options": {
|
||||||
|
"Default": "Default",
|
||||||
|
"Integrated": "Integrated"
|
||||||
|
}
|
||||||
|
},
|
||||||
"Comfy_UseNewMenu": {
|
"Comfy_UseNewMenu": {
|
||||||
"name": "Use new menu",
|
"name": "Use new menu",
|
||||||
"tooltip": "Enable the redesigned top menu bar.",
|
"tooltip": "Enable the redesigned top menu bar.",
|
||||||
|
|||||||
@@ -549,6 +549,16 @@ export const CORE_SETTINGS: SettingParams[] = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'Comfy.UI.TabBarLayout',
|
||||||
|
category: ['Appearance', 'General'],
|
||||||
|
name: 'Tab Bar Layout',
|
||||||
|
type: 'combo',
|
||||||
|
options: ['Default', 'Integrated'],
|
||||||
|
tooltip:
|
||||||
|
'Controls the layout of the tab bar. "Integrated" moves Help and User controls into the tab bar area.',
|
||||||
|
defaultValue: 'Default'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'Comfy.UseNewMenu',
|
id: 'Comfy.UseNewMenu',
|
||||||
category: ['Comfy', 'Menu', 'UseNewMenu'],
|
category: ['Comfy', 'Menu', 'UseNewMenu'],
|
||||||
|
|||||||
@@ -399,6 +399,7 @@ const zSettings = z.object({
|
|||||||
'Comfy.Canvas.BackgroundImage': z.string().optional(),
|
'Comfy.Canvas.BackgroundImage': z.string().optional(),
|
||||||
'Comfy.ConfirmClear': z.boolean(),
|
'Comfy.ConfirmClear': z.boolean(),
|
||||||
'Comfy.DevMode': z.boolean(),
|
'Comfy.DevMode': z.boolean(),
|
||||||
|
'Comfy.UI.TabBarLayout': z.enum(['Default', 'Integrated']),
|
||||||
'Comfy.Workflow.ShowMissingNodesWarning': z.boolean(),
|
'Comfy.Workflow.ShowMissingNodesWarning': z.boolean(),
|
||||||
'Comfy.Workflow.ShowMissingModelsWarning': z.boolean(),
|
'Comfy.Workflow.ShowMissingModelsWarning': z.boolean(),
|
||||||
'Comfy.Workflow.WarnBlueprintOverwrite': z.boolean(),
|
'Comfy.Workflow.WarnBlueprintOverwrite': z.boolean(),
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export type HelpCenterTriggerLocation = 'sidebar' | 'topbar'
|
||||||
|
|
||||||
export const useHelpCenterStore = defineStore('helpCenter', () => {
|
export const useHelpCenterStore = defineStore('helpCenter', () => {
|
||||||
const isVisible = ref(false)
|
const isVisible = ref(false)
|
||||||
|
const triggerLocation = ref<HelpCenterTriggerLocation>('sidebar')
|
||||||
|
|
||||||
const toggle = () => {
|
const toggle = (location: HelpCenterTriggerLocation = 'sidebar') => {
|
||||||
|
if (!isVisible.value) {
|
||||||
|
triggerLocation.value = location
|
||||||
|
}
|
||||||
isVisible.value = !isVisible.value
|
isVisible.value = !isVisible.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const show = () => {
|
const show = (location: HelpCenterTriggerLocation = 'sidebar') => {
|
||||||
|
triggerLocation.value = location
|
||||||
isVisible.value = true
|
isVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,6 +25,7 @@ export const useHelpCenterStore = defineStore('helpCenter', () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
isVisible,
|
isVisible,
|
||||||
|
triggerLocation,
|
||||||
toggle,
|
toggle,
|
||||||
show,
|
show,
|
||||||
hide
|
hide
|
||||||
|
|||||||
Reference in New Issue
Block a user