feat: vue nodes onboarding toggle in menu (#6671)

## Summary

Added Nodes 2.0 menu items with a toggle. 
Updated copy in banner and toast to be more descriptive.

## Screenshots (if applicable)


https://github.com/user-attachments/assets/85bf3ae4-0e0b-4e04-82c7-a26a73cbdd5b

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6671-feat-vue-nodes-onboarding-toggle-in-menu-2aa6d73d3650817d8e5bef0ad0f8bebb)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Simula_r
2025-11-13 20:19:18 -08:00
committed by GitHub
parent 693d408c4a
commit ecd87ae0f4
15 changed files with 111 additions and 14 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 115 KiB

View File

@@ -1,5 +1,6 @@
<template> <template>
<div <div
ref="menuButtonRef"
v-tooltip="{ v-tooltip="{
value: t('sideToolbar.labels.menu'), value: t('sideToolbar.labels.menu'),
showDelay: 300, showDelay: 300,
@@ -29,6 +30,7 @@
> >
<template #item="{ item, props }"> <template #item="{ item, props }">
<a <a
v-if="item.key !== 'nodes-2.0-toggle'"
class="p-menubar-item-link px-4 py-2" class="p-menubar-item-link px-4 py-2"
v-bind="props.action" v-bind="props.action"
:href="item.url" :href="item.url"
@@ -65,6 +67,34 @@
</span> </span>
<i v-if="item.items" class="pi pi-angle-right ml-auto" /> <i v-if="item.items" class="pi pi-angle-right ml-auto" />
</a> </a>
<div
v-else
class="flex items-center justify-between px-4 py-2"
@click.stop="handleNodes2ToggleClick"
>
<span class="p-menubar-item-label text-nowrap">{{ item.label }}</span>
<ToggleSwitch
v-model="nodes2Enabled"
class="ml-4"
:aria-label="item.label"
:pt="{
root: {
style: {
width: '38px',
height: '20px'
}
},
handle: {
style: {
width: '16px',
height: '16px'
}
}
}"
@click.stop
@update:model-value="onNodes2ToggleChange"
/>
</div>
</template> </template>
</TieredMenu> </TieredMenu>
</template> </template>
@@ -73,6 +103,7 @@
import type { MenuItem } from 'primevue/menuitem' import type { MenuItem } from 'primevue/menuitem'
import TieredMenu from 'primevue/tieredmenu' import TieredMenu from 'primevue/tieredmenu'
import type { TieredMenuMethods, TieredMenuState } from 'primevue/tieredmenu' import type { TieredMenuMethods, TieredMenuState } from 'primevue/tieredmenu'
import ToggleSwitch from 'primevue/toggleswitch'
import { computed, nextTick, ref } from 'vue' import { computed, nextTick, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
@@ -80,6 +111,7 @@ import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.
import ComfyLogo from '@/components/icons/ComfyLogo.vue' import ComfyLogo from '@/components/icons/ComfyLogo.vue'
import { useWorkflowTemplateSelectorDialog } from '@/composables/useWorkflowTemplateSelectorDialog' import { useWorkflowTemplateSelectorDialog } from '@/composables/useWorkflowTemplateSelectorDialog'
import SettingDialogContent from '@/platform/settings/components/SettingDialogContent.vue' import SettingDialogContent from '@/platform/settings/components/SettingDialogContent.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry' import { useTelemetry } from '@/platform/telemetry'
import { useColorPaletteService } from '@/services/colorPaletteService' import { useColorPaletteService } from '@/services/colorPaletteService'
import { useCommandStore } from '@/stores/commandStore' import { useCommandStore } from '@/stores/commandStore'
@@ -98,10 +130,19 @@ const colorPaletteStore = useColorPaletteStore()
const colorPaletteService = useColorPaletteService() const colorPaletteService = useColorPaletteService()
const dialogStore = useDialogStore() const dialogStore = useDialogStore()
const managerState = useManagerState() const managerState = useManagerState()
const settingStore = useSettingStore()
const menuRef = ref< const menuRef = ref<
({ dirty: boolean } & TieredMenuMethods & TieredMenuState) | null ({ dirty: boolean } & TieredMenuMethods & TieredMenuState) | null
>(null) >(null)
const menuButtonRef = ref<HTMLElement | null>(null)
const nodes2Enabled = computed({
get: () => settingStore.get('Comfy.VueNodes.Enabled') ?? false,
set: async (value: boolean) => {
await settingStore.set('Comfy.VueNodes.Enabled', value)
}
})
const telemetry = useTelemetry() const telemetry = useTelemetry()
@@ -164,6 +205,10 @@ const extraMenuItems = computed(() => [
label: t('menu.theme'), label: t('menu.theme'),
items: themeMenuItems.value items: themeMenuItems.value
}, },
{
key: 'nodes-2.0-toggle',
label: 'Nodes 2.0'
},
{ separator: true }, { separator: true },
{ {
key: 'browse-templates', key: 'browse-templates',
@@ -281,6 +326,17 @@ const hasActiveStateSiblings = (item: MenuItem): boolean => {
menuItemStore.menuItemHasActiveStateChildren[item.parentPath]) menuItemStore.menuItemHasActiveStateChildren[item.parentPath])
) )
} }
const handleNodes2ToggleClick = () => {
return false
}
const onNodes2ToggleChange = async (value: boolean) => {
await settingStore.set('Comfy.VueNodes.Enabled', value)
telemetry?.trackUiButtonClicked({
button_id: `menu_nodes_2.0_toggle_${value ? 'enabled' : 'disabled'}`
})
}
</script> </script>
<style scoped> <style scoped>

View File

@@ -15,11 +15,24 @@
size="small" size="small"
:label="t('vueNodesMigration.button')" :label="t('vueNodesMigration.button')"
text text
@click="handleOpenSettings" @click="switchBack"
/> />
</div> </div>
</template> </template>
</Toast> </Toast>
<Toast
group="vue-nodes-check-main-menu"
position="bottom-center"
class="w-auto"
>
<template #message>
<div class="flex flex-auto items-center justify-between gap-4">
<span class="whitespace-nowrap">{{
t('vueNodesMigrationMainMenu.message')
}}</span>
</div>
</template>
</Toast>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -29,20 +42,38 @@ import Toast from 'primevue/toast'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useVueNodesMigrationDismissed } from '@/composables/useVueNodesMigrationDismissed' import { useVueNodesMigrationDismissed } from '@/composables/useVueNodesMigrationDismissed'
import { useDialogService } from '@/services/dialogService' import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { useToastStore } from '@/platform/updates/common/toastStore'
const { t } = useI18n() const { t } = useI18n()
const toast = useToast() const toast = useToast()
const dialogService = useDialogService()
const isDismissed = useVueNodesMigrationDismissed() const isDismissed = useVueNodesMigrationDismissed()
const handleOpenSettings = () => { const switchBack = async () => {
dialogService.showSettingsDialog() await disableVueNodes()
toast.removeGroup('vue-nodes-migration') toast.removeGroup('vue-nodes-migration')
isDismissed.value = true isDismissed.value = true
showMainMenuToast()
} }
const handleClose = () => { const handleClose = () => {
isDismissed.value = true isDismissed.value = true
showMainMenuToast()
}
const disableVueNodes = async () => {
await useSettingStore().set('Comfy.VueNodes.Enabled', false)
useTelemetry()?.trackUiButtonClicked({
button_id: `vue_nodes_migration_toast_switch_back_clicked`
})
}
const showMainMenuToast = () => {
useToastStore().add({
group: 'vue-nodes-check-main-menu',
severity: 'info',
life: 5000
})
} }
</script> </script>

View File

@@ -4,8 +4,8 @@
class="pointer-events-auto relative w-full h-10 bg-gradient-to-r from-blue-600 to-blue-700 flex items-center justify-center px-4" class="pointer-events-auto relative w-full h-10 bg-gradient-to-r from-blue-600 to-blue-700 flex items-center justify-center px-4"
> >
<div class="flex items-center"> <div class="flex items-center">
<i class="icon-[lucide--sparkles]"></i> <i class="icon-[lucide--rocket]"></i>
<span class="pl-2">{{ $t('vueNodesBanner.message') }}</span> <span class="pl-2 text-sm">{{ $t('vueNodesBanner.message') }}</span>
<Button <Button
class="cursor-pointer bg-transparent rounded h-7 px-3 border border-white text-white ml-4 text-xs" class="cursor-pointer bg-transparent rounded h-7 px-3 border border-white text-white ml-4 text-xs"
@click="handleTryItOut" @click="handleTryItOut"

View File

@@ -25,6 +25,8 @@ function useVueNodeLifecycleIndividual() {
const isVueNodeToastDismissed = useVueNodesMigrationDismissed() const isVueNodeToastDismissed = useVueNodesMigrationDismissed()
let hasShownMigrationToast = false
const initializeNodeManager = () => { const initializeNodeManager = () => {
// Use canvas graph if available (handles subgraph contexts), fallback to app graph // Use canvas graph if available (handles subgraph contexts), fallback to app graph
const activeGraph = comfyApp.canvas?.graph const activeGraph = comfyApp.canvas?.graph
@@ -85,7 +87,12 @@ function useVueNodeLifecycleIndividual() {
ensureCorrectLayoutScale( ensureCorrectLayoutScale(
comfyApp.canvas?.graph?.extra.workflowRendererVersion comfyApp.canvas?.graph?.extra.workflowRendererVersion
) )
if (!wasEnabled && !isVueNodeToastDismissed.value) { if (
wasEnabled === false &&
!isVueNodeToastDismissed.value &&
!hasShownMigrationToast
) {
hasShownMigrationToast = true
useToastStore().add({ useToastStore().add({
group: 'vue-nodes-migration', group: 'vue-nodes-migration',
severity: 'info', severity: 'info',

View File

@@ -2043,14 +2043,17 @@
} }
} }
}, },
"vueNodesMigration": {
"message": "Prefer the classic node design?",
"button": "Open Settings"
},
"vueNodesBanner": { "vueNodesBanner": {
"message": "Nodes just got a new look and feel", "message": "Introducing Nodes 2.0 More flexible workflows, powerful new widgets, built for extensibility",
"tryItOut": "Try it out" "tryItOut": "Try it out"
}, },
"vueNodesMigration": {
"message": "Prefer the legacy design?",
"button": "Switch back"
},
"vueNodesMigrationMainMenu": {
"message": "Switch back to Nodes 2.0 anytime from the main menu."
},
"cloud": { "cloud": {
"missingNodes": { "missingNodes": {
"title": "These nodes aren't available on Comfy Cloud yet", "title": "These nodes aren't available on Comfy Cloud yet",
@@ -2063,4 +2066,4 @@
"cannotRun": "Workflow contains unsupported nodes (highlighted red). Remove these to run the workflow. " "cannotRun": "Workflow contains unsupported nodes (highlighted red). Remove these to run the workflow. "
} }
} }
} }