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>
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 115 KiB |
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
ref="menuButtonRef"
|
||||
v-tooltip="{
|
||||
value: t('sideToolbar.labels.menu'),
|
||||
showDelay: 300,
|
||||
@@ -29,6 +30,7 @@
|
||||
>
|
||||
<template #item="{ item, props }">
|
||||
<a
|
||||
v-if="item.key !== 'nodes-2.0-toggle'"
|
||||
class="p-menubar-item-link px-4 py-2"
|
||||
v-bind="props.action"
|
||||
:href="item.url"
|
||||
@@ -65,6 +67,34 @@
|
||||
</span>
|
||||
<i v-if="item.items" class="pi pi-angle-right ml-auto" />
|
||||
</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>
|
||||
</TieredMenu>
|
||||
</template>
|
||||
@@ -73,6 +103,7 @@
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import TieredMenu from 'primevue/tieredmenu'
|
||||
import type { TieredMenuMethods, TieredMenuState } from 'primevue/tieredmenu'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -80,6 +111,7 @@ import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.
|
||||
import ComfyLogo from '@/components/icons/ComfyLogo.vue'
|
||||
import { useWorkflowTemplateSelectorDialog } from '@/composables/useWorkflowTemplateSelectorDialog'
|
||||
import SettingDialogContent from '@/platform/settings/components/SettingDialogContent.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useColorPaletteService } from '@/services/colorPaletteService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
@@ -98,10 +130,19 @@ const colorPaletteStore = useColorPaletteStore()
|
||||
const colorPaletteService = useColorPaletteService()
|
||||
const dialogStore = useDialogStore()
|
||||
const managerState = useManagerState()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const menuRef = ref<
|
||||
({ dirty: boolean } & TieredMenuMethods & TieredMenuState) | 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()
|
||||
|
||||
@@ -164,6 +205,10 @@ const extraMenuItems = computed(() => [
|
||||
label: t('menu.theme'),
|
||||
items: themeMenuItems.value
|
||||
},
|
||||
{
|
||||
key: 'nodes-2.0-toggle',
|
||||
label: 'Nodes 2.0'
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
key: 'browse-templates',
|
||||
@@ -281,6 +326,17 @@ const hasActiveStateSiblings = (item: MenuItem): boolean => {
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -15,11 +15,24 @@
|
||||
size="small"
|
||||
:label="t('vueNodesMigration.button')"
|
||||
text
|
||||
@click="handleOpenSettings"
|
||||
@click="switchBack"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</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>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -29,20 +42,38 @@ import Toast from 'primevue/toast'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
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 toast = useToast()
|
||||
const dialogService = useDialogService()
|
||||
const isDismissed = useVueNodesMigrationDismissed()
|
||||
|
||||
const handleOpenSettings = () => {
|
||||
dialogService.showSettingsDialog()
|
||||
const switchBack = async () => {
|
||||
await disableVueNodes()
|
||||
toast.removeGroup('vue-nodes-migration')
|
||||
isDismissed.value = true
|
||||
showMainMenuToast()
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
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>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<i class="icon-[lucide--sparkles]"></i>
|
||||
<span class="pl-2">{{ $t('vueNodesBanner.message') }}</span>
|
||||
<i class="icon-[lucide--rocket]"></i>
|
||||
<span class="pl-2 text-sm">{{ $t('vueNodesBanner.message') }}</span>
|
||||
<Button
|
||||
class="cursor-pointer bg-transparent rounded h-7 px-3 border border-white text-white ml-4 text-xs"
|
||||
@click="handleTryItOut"
|
||||
|
||||
@@ -25,6 +25,8 @@ function useVueNodeLifecycleIndividual() {
|
||||
|
||||
const isVueNodeToastDismissed = useVueNodesMigrationDismissed()
|
||||
|
||||
let hasShownMigrationToast = false
|
||||
|
||||
const initializeNodeManager = () => {
|
||||
// Use canvas graph if available (handles subgraph contexts), fallback to app graph
|
||||
const activeGraph = comfyApp.canvas?.graph
|
||||
@@ -85,7 +87,12 @@ function useVueNodeLifecycleIndividual() {
|
||||
ensureCorrectLayoutScale(
|
||||
comfyApp.canvas?.graph?.extra.workflowRendererVersion
|
||||
)
|
||||
if (!wasEnabled && !isVueNodeToastDismissed.value) {
|
||||
if (
|
||||
wasEnabled === false &&
|
||||
!isVueNodeToastDismissed.value &&
|
||||
!hasShownMigrationToast
|
||||
) {
|
||||
hasShownMigrationToast = true
|
||||
useToastStore().add({
|
||||
group: 'vue-nodes-migration',
|
||||
severity: 'info',
|
||||
|
||||
@@ -2043,14 +2043,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"vueNodesMigration": {
|
||||
"message": "Prefer the classic node design?",
|
||||
"button": "Open Settings"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"vueNodesMigration": {
|
||||
"message": "Prefer the legacy design?",
|
||||
"button": "Switch back"
|
||||
},
|
||||
"vueNodesMigrationMainMenu": {
|
||||
"message": "Switch back to Nodes 2.0 anytime from the main menu."
|
||||
},
|
||||
"cloud": {
|
||||
"missingNodes": {
|
||||
"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. "
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||