Compare commits

..

4 Commits

Author SHA1 Message Date
Deep Mehta
f5c26130f2 Remove cloud notification badge from topbar
- Remove persistent 'NEW' badge that appeared after modal dismissal
- Keep one-time modal notification for macOS desktop users
- Clean up unused badge translations
2025-11-25 02:12:08 +05:30
Deep Mehta
7bba8f09f6 Fix: Cloud notification badge reactivity and modal timing
- Fix badge not appearing by accessing settingStore.settingValues directly for proper reactivity
- Increase modal delay to 2s to ensure it appears after missing models dialog
- Move setting update inside setTimeout to only save when modal actually shows

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 04:17:25 +05:30
Deep Mehta
de39ece33f Add analytics tracking for cloud notification feature
Adds Mixpanel event tracking and UTM parameters to measure:
- Modal impression rate (cloud_notification_modal_shown)
- Click-through rate (cloud_notification_explore_cloud_clicked)
- Dismiss rate (cloud_notification_continue_locally_clicked)
- Badge engagement (cloud_notification_badge_clicked)

UTM parameters enable full funnel tracking:
- Desktop users clicking Explore Cloud
- Cloud website signups with utm_source=desktop
- Subscription conversions attributed to desktop

This enables measurement of the desktop→cloud conversion funnel to validate the 5.6% signup-to-paid conversion rate goal.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 02:00:00 +05:30
Deep Mehta
e5ba351dcc [feat] Add cloud notification modal for macOS desktop users
Adds a one-time informational modal that introduces Comfy Cloud to macOS desktop users. The modal emphasizes that ComfyUI remains free and open source, with Cloud as an optional service for GPU access.

Key features:
- Shows once on first launch for macOS + Electron users
- Persistent badge in topbar after dismissal for easy re-access
- Clean design with Comfy Cloud branding
- Non-intrusive messaging focused on infrastructure benefits

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 20:53:58 +05:30
37 changed files with 257 additions and 175 deletions

View File

@@ -1,5 +1,5 @@
# Description: When upstream electron API is updated, click dispatch to update the TypeScript type definitions in this repo
name: 'Api: Update Electron API Types'
description: 'When upstream electron API is updated, click dispatch to update the TypeScript type definitions in this repo'
on:
workflow_dispatch:

View File

@@ -1,5 +1,5 @@
# Description: When upstream ComfyUI-Manager API is updated, click dispatch to update the TypeScript type definitions in this repo
name: 'Api: Update Manager API Types'
description: 'When upstream ComfyUI-Manager API is updated, click dispatch to update the TypeScript type definitions in this repo'
on:
# Manual trigger

View File

@@ -1,5 +1,5 @@
# Description: When upstream comfy-api is updated, click dispatch to update the TypeScript type definitions in this repo
name: 'Api: Update Registry API Types'
description: 'When upstream comfy-api is updated, click dispatch to update the TypeScript type definitions in this repo'
on:
# Manual trigger

View File

@@ -1,5 +1,5 @@
# Description: Validates JSON syntax in all tracked .json files (excluding tsconfig*.json) using jq
name: "CI: JSON Validation"
description: "Validates JSON syntax in all tracked .json files (excluding tsconfig*.json) using jq"
on:
push:

View File

@@ -1,5 +1,5 @@
# Description: Linting and code formatting validation for pull requests
name: "CI: Lint Format"
description: "Linting and code formatting validation for pull requests"
on:
pull_request:

View File

@@ -1,5 +1,5 @@
# Description: Validates Python code in tools/devtools directory
name: "CI: Python Validation"
description: "Validates Python code in tools/devtools directory"
on:
pull_request:

View File

@@ -1,5 +1,5 @@
# Description: Deploys test results from forked PRs (forks can't access deployment secrets)
name: "CI: Tests E2E (Deploy for Forks)"
description: "Deploys test results from forked PRs (forks can't access deployment secrets)"
on:
workflow_run:

View File

@@ -1,5 +1,5 @@
# Description: End-to-end testing with Playwright across multiple browsers, deploys test reports to Cloudflare Pages
name: "CI: Tests E2E"
description: "End-to-end testing with Playwright across multiple browsers, deploys test reports to Cloudflare Pages"
on:
push:

View File

@@ -1,5 +1,5 @@
# Description: Deploys Storybook previews from forked PRs (forks can't access deployment secrets)
name: "CI: Tests Storybook (Deploy for Forks)"
description: "Deploys Storybook previews from forked PRs (forks can't access deployment secrets)"
on:
workflow_run:

View File

@@ -1,9 +1,10 @@
# Description: Builds Storybook and runs visual regression testing via Chromatic, deploys previews to Cloudflare Pages
name: "CI: Tests Storybook"
description: "Builds Storybook and runs visual regression testing via Chromatic, deploys previews to Cloudflare Pages"
on:
workflow_dispatch: # Allow manual triggering
pull_request:
branches: [main]
jobs:
# Post starting comment for non-forked PRs

View File

@@ -1,5 +1,5 @@
# Description: Unit and component testing with Vitest
name: "CI: Tests Unit"
description: "Unit and component testing with Vitest"
on:
push:

View File

@@ -1,5 +1,5 @@
# Description: Validates YAML syntax and style using yamllint with relaxed rules
name: "CI: YAML Validation"
description: "Validates YAML syntax and style using yamllint with relaxed rules"
on:
push:

View File

@@ -1,5 +1,5 @@
# Description: Generates and updates translations for core ComfyUI components using OpenAI
name: "i18n: Update Core"
description: "Generates and updates translations for core ComfyUI components using OpenAI"
on:
# Manual dispatch for urgent translation updates

View File

@@ -1,5 +1,5 @@
# Description: AI-powered code review triggered by adding the 'claude-review' label to a PR
name: "PR: Claude Review"
description: "AI-powered code review triggered by adding the 'claude-review' label to a PR"
permissions:
contents: read

View File

@@ -148,10 +148,10 @@ jobs:
done
{
echo "results<<EOF"
echo "results<<'EOF'"
cat "$RESULTS_FILE"
echo "EOF"
} >> "$GITHUB_OUTPUT"
} >> $GITHUB_OUTPUT
- name: Ensure release labels
if: steps.check_version.outputs.is_minor_bump == 'true'

View File

@@ -1,5 +1,5 @@
# Description: Manual workflow to increment package version with semantic versioning support
name: "Release: Version Bump"
description: "Manual workflow to increment package version with semantic versioning support"
on:
workflow_dispatch:

View File

@@ -1,5 +1,5 @@
# Description: Automated weekly documentation accuracy check and update via Claude
name: "Weekly Documentation Check"
description: "Automated weekly documentation accuracy check and update via Claude"
permissions:
contents: write

View File

@@ -22,11 +22,7 @@
<h1 v-if="title" class="font-inter font-bold text-3xl text-neutral-300">
{{ title }}
</h1>
<p
v-if="statusText"
class="text-lg text-neutral-400"
data-testid="startup-status-text"
>
<p v-if="statusText" class="text-lg text-neutral-400">
{{ statusText }}
</p>
</div>

View File

@@ -9,9 +9,9 @@ test.beforeEach(async ({ comfyPage }) => {
test.describe('Vue Nodes - LOD', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 8)
await comfyPage.setup()
await comfyPage.loadWorkflow('default')
await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 8)
})
test('should toggle LOD based on zoom threshold', async ({ comfyPage }) => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.33.7",
"version": "1.33.5",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",

View File

@@ -17,6 +17,7 @@ import { computed, onMounted } from 'vue'
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
import config from '@/config'
import { t } from '@/i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService'
@@ -47,7 +48,7 @@ const showContextMenu = (event: MouseEvent) => {
}
}
onMounted(() => {
onMounted(async () => {
window['__COMFYUI_FRONTEND_VERSION__'] = config.app_version
if (isElectron()) {
@@ -77,5 +78,25 @@ onMounted(() => {
// Initialize conflict detection in background
// This runs async and doesn't block UI setup
void conflictDetection.initializeConflictDetection()
// Show cloud notification for macOS desktop users (one-time)
// Delayed to ensure it appears after workflow loading (missing models dialog, etc.)
if (isElectron()) {
const isMacOS = navigator.platform.toLowerCase().includes('mac')
if (isMacOS) {
const settingStore = useSettingStore()
const hasShownNotification = settingStore.get(
'Comfy.Desktop.CloudNotificationShown'
)
if (!hasShownNotification) {
// Delay to show after initial workflow loading completes
setTimeout(async () => {
dialogService.showCloudNotification()
await settingStore.set('Comfy.Desktop.CloudNotificationShown', true)
}, 2000)
}
}
}
})
</script>

View File

@@ -0,0 +1,126 @@
<template>
<div class="w-[480px] p-6">
<!-- Header with Logo -->
<div class="mb-6">
<div class="mb-2 flex items-center gap-3">
<img
src="/assets/images/comfy-cloud-logo.svg"
alt="Comfy Cloud"
class="h-8 w-8 shrink-0"
/>
<h1 class="text-2xl font-semibold">
{{ t('cloudNotification.title') }}
</h1>
</div>
<p class="text-base text-muted">
{{ t('cloudNotification.message') }}
</p>
</div>
<!-- Features -->
<div class="mb-6 space-y-4">
<div class="flex gap-3">
<i class="pi pi-check-circle mt-0.5 shrink-0 text-xl text-blue-500"></i>
<div class="flex-1">
<div class="mb-1 font-medium">
{{ t('cloudNotification.feature1Title') }}
</div>
<div class="text-sm text-muted">
{{ t('cloudNotification.feature1') }}
</div>
</div>
</div>
<div class="flex gap-3">
<i class="pi pi-server mt-0.5 shrink-0 text-xl text-blue-500"></i>
<div class="flex-1">
<div class="mb-1 font-medium">
{{ t('cloudNotification.feature2Title') }}
</div>
<div class="text-sm text-muted">
{{ t('cloudNotification.feature2') }}
</div>
</div>
</div>
<div class="flex gap-3">
<i class="pi pi-tag mt-0.5 shrink-0 text-xl text-blue-500"></i>
<div class="flex-1">
<div class="mb-1 font-medium">
{{ t('cloudNotification.feature3Title') }}
</div>
<div class="text-sm text-muted">
{{ t('cloudNotification.feature3') }}
</div>
</div>
</div>
</div>
<!-- Footer Note -->
<div
class="mb-6 rounded border-l-2 border-blue-500 bg-blue-500/5 py-2.5 pl-3 pr-4"
>
<p class="whitespace-pre-line text-sm text-muted">
{{ t('cloudNotification.feature4') }}
</p>
</div>
<!-- Actions -->
<div class="flex gap-3">
<Button
:label="t('cloudNotification.continueLocally')"
severity="secondary"
outlined
class="flex-1"
@click="onDismiss"
/>
<Button
:label="t('cloudNotification.exploreCloud')"
icon="pi pi-arrow-right"
icon-pos="right"
class="flex-1"
@click="onExplore"
/>
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useTelemetry } from '@/platform/telemetry'
import { useDialogStore } from '@/stores/dialogStore'
const { t } = useI18n()
// Track when modal is shown
onMounted(() => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'cloud_notification_modal_shown'
})
})
const onDismiss = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'cloud_notification_continue_locally_clicked'
})
useDialogStore().closeDialog()
}
const onExplore = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'cloud_notification_explore_cloud_clicked'
})
// Add UTM parameters for attribution tracking
const url = new URL('https://www.comfy.org/cloud')
url.searchParams.set('utm_source', 'desktop')
url.searchParams.set('utm_medium', 'notification')
url.searchParams.set('utm_campaign', 'macos_first_launch')
window.open(url.toString(), '_blank')
useDialogStore().closeDialog()
}
</script>

View File

@@ -83,7 +83,7 @@
role="combobox"
:aria-expanded="false"
aria-haspopup="listbox"
:tabindex="0"
tabindex="0"
>
<template
v-if="showSearchBox || showSelectedCount || showClearButton"

View File

@@ -1,5 +1,3 @@
import { isCloud } from '@/platform/distribution/types'
export const CORE_MENU_COMMANDS = [
[[], ['Comfy.NewBlankWorkflow']],
[[], []], // Separator after New
@@ -16,16 +14,13 @@ export const CORE_MENU_COMMANDS = [
[['Edit'], ['Comfy.Undo', 'Comfy.Redo']],
[['Edit'], ['Comfy.ClearWorkflow']],
[['Edit'], ['Comfy.OpenClipspace']],
[['Edit'], ['Comfy.RefreshNodeDefinitions']],
[
['Edit'],
[
'Comfy.RefreshNodeDefinitions',
...(isCloud
? []
: [
'Comfy.Memory.UnloadModels',
'Comfy.Memory.UnloadModelsAndExecutionCache'
])
'Comfy.Memory.UnloadModels',
'Comfy.Memory.UnloadModelsAndExecutionCache'
]
],
[['View'], []],

View File

@@ -1186,8 +1186,7 @@
"Vue Nodes": "Vue Nodes",
"Canvas Navigation": "Canvas Navigation",
"PlanCredits": "Plan & Credits",
"VueNodes": "Vue Nodes",
"Nodes 2_0": "Nodes 2.0"
"VueNodes": "Vue Nodes"
},
"serverConfigItems": {
"listen": {
@@ -2199,10 +2198,6 @@
"vueNodesMigrationMainMenu": {
"message": "Switch back to Nodes 2.0 anytime from the main menu."
},
"linearMode": {
"share": "Share",
"openWorkflow": "Open Workflow"
},
"missingNodes": {
"cloud": {
"title": "These nodes aren't available on Comfy Cloud yet",
@@ -2217,5 +2212,18 @@
"description": "This workflow uses custom nodes you haven't installed yet.",
"replacementInstruction": "Install these nodes to run this workflow, or replace them with installed alternatives. Missing nodes are highlighted in red on the canvas."
}
},
"cloudNotification": {
"title": "Discover Comfy Cloud",
"message": "Get access to industry-grade GPUs and run workflows up to 10x faster",
"feature1Title": "No Setup Required",
"feature1": "Start creating instantly with popular models pre-installed",
"feature2Title": "Powerful GPUs",
"feature2": "A100 and RTX PRO 6000 GPUs for heavy video models",
"feature3Title": "$20/month",
"feature3": "Simple subscription with unlimited workflow runs",
"feature4": "ComfyUI stays free and open source.\nCloud is optional—for instant access to high-end GPUs.",
"continueLocally": "Continue Locally",
"exploreCloud": "Explore Cloud"
}
}

View File

@@ -2001,7 +2001,7 @@
}
},
"EmptyHunyuanLatentVideo": {
"display_name": "Empty HunyuanVideo 1.0 Latent",
"display_name": "EmptyHunyuanLatentVideo",
"inputs": {
"width": {
"name": "width"
@@ -2023,7 +2023,7 @@
}
},
"EmptyHunyuanVideo15Latent": {
"display_name": "Empty HunyuanVideo 1.5 Latent",
"display_name": "EmptyHunyuanVideo15Latent",
"inputs": {
"width": {
"name": "width"

View File

@@ -293,6 +293,12 @@ export const CORE_SETTINGS: SettingParams[] = [
type: 'boolean',
defaultValue: true
},
{
id: 'Comfy.Desktop.CloudNotificationShown',
name: 'Cloud notification shown',
type: 'boolean',
defaultValue: false
},
{
id: 'Comfy.Graph.ZoomSpeed',
category: ['LiteGraph', 'Canvas', 'ZoomSpeed'],

View File

@@ -38,9 +38,8 @@ function onChange(
}
// Backward compatibility with old settings dialog.
// Some extensions still listens event emitted by the old settings dialog.
if (setting) {
app.ui.settings.dispatchChange(setting.id, newValue, oldValue)
}
// @ts-expect-error 'setting' is possibly 'undefined'.ts(18048)
app.ui.settings.dispatchChange(setting.id, newValue, oldValue)
}
export const useSettingStore = defineStore('setting', () => {

View File

@@ -7,17 +7,14 @@ import {
mergePreservedQueryIntoQuery
} from '@/platform/navigation/preservedQueryManager'
import { PRESERVED_QUERY_NAMESPACES } from '@/platform/navigation/preservedQueryNamespaces'
import { clearWorkflowPersistenceStorage } from '@/platform/workflow/persistence/workflowStorage'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useTemplateUrlLoader } from '@/platform/workflow/templates/composables/useTemplateUrlLoader'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { api } from '@/scripts/api'
import { app as comfyApp } from '@/scripts/app'
import { getStorageValue, setStorageValue } from '@/scripts/utils'
import { useCommandStore } from '@/stores/commandStore'
import { isCloud } from '@/platform/distribution/types'
export function useWorkflowPersistence() {
const workflowStore = useWorkflowStore()
@@ -25,7 +22,6 @@ export function useWorkflowPersistence() {
const route = useRoute()
const router = useRouter()
const templateUrlLoader = useTemplateUrlLoader()
const { onUserLogout } = useCurrentUser()
const TEMPLATE_NAMESPACE = PRESERVED_QUERY_NAMESPACES.TEMPLATE
const ensureTemplateQueryFromIntent = async () => {
@@ -187,10 +183,6 @@ export function useWorkflowPersistence() {
}
})
onUserLogout(() => {
if (isCloud) clearWorkflowPersistenceStorage()
})
const restoreWorkflowTabsState = () => {
if (!workflowPersistenceEnabled.value) return
const isRestorable = storedWorkflows?.length > 0 && storedActiveIndex >= 0

View File

@@ -1,39 +0,0 @@
const LOCAL_STORAGE_KEYS = [
'workflow',
'Comfy.PreviousWorkflow',
'Comfy.OpenWorkflowsPaths',
'Comfy.ActiveWorkflowIndex'
]
const SESSION_KEY_PREFIXES = [
'workflow:',
'Comfy.PreviousWorkflow',
'Comfy.OpenWorkflowsPaths',
'Comfy.ActiveWorkflowIndex'
]
const shouldClearSessionKey = (key: string): boolean =>
SESSION_KEY_PREFIXES.some((prefix) =>
prefix.endsWith(':')
? key.startsWith(prefix)
: key === prefix || key.startsWith(`${prefix}:`)
)
/**
* Removes any workflow state persisted in storage so that the next login
* starts with a clean slate.
*/
export const clearWorkflowPersistenceStorage = () => {
LOCAL_STORAGE_KEYS.forEach((key) => {
localStorage.removeItem(key)
})
// Iterate backwards because removing items mutates sessionStorage.length.
for (let i = sessionStorage.length - 1; i >= 0; i--) {
const key = sessionStorage.key(i)
if (!key) continue
if (shouldClearSessionKey(key)) {
sessionStorage.removeItem(key)
}
}
}

View File

@@ -154,6 +154,7 @@ import { useTelemetry } from '@/platform/telemetry'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { useTransformState } from '@/renderer/core/layout/transform/useTransformState'
import SlotConnectionDot from '@/renderer/extensions/vueNodes/components/SlotConnectionDot.vue'
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
import { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/composables/useNodePointerInteractions'
@@ -200,6 +201,13 @@ const { bringNodeToFront } = useNodeZIndex()
useVueElementTracking(() => nodeData.id, 'node')
const transformState = useTransformState()
if (!transformState) {
throw new Error(
'TransformState must be provided for node resize functionality'
)
}
const { selectedNodeIds } = storeToRefs(useCanvasStore())
const isSelected = computed(() => {
return selectedNodeIds.value.has(nodeData.id)
@@ -356,24 +364,29 @@ const cornerResizeHandles: CornerResizeHandle[] = [
const MIN_NODE_WIDTH = 225
const { startResize } = useNodeResize((result, element) => {
if (isCollapsed.value) return
const { startResize } = useNodeResize(
(result, element) => {
if (isCollapsed.value) return
// Clamp width to minimum to avoid conflicts with CSS min-width
const clampedWidth = Math.max(result.size.width, MIN_NODE_WIDTH)
// Clamp width to minimum to avoid conflicts with CSS min-width
const clampedWidth = Math.max(result.size.width, MIN_NODE_WIDTH)
// Apply size directly to DOM element - ResizeObserver will pick this up
element.style.setProperty('--node-width', `${clampedWidth}px`)
element.style.setProperty('--node-height', `${result.size.height}px`)
// Apply size directly to DOM element - ResizeObserver will pick this up
element.style.setProperty('--node-width', `${clampedWidth}px`)
element.style.setProperty('--node-height', `${result.size.height}px`)
const currentPosition = position.value
const deltaX = Math.abs(result.position.x - currentPosition.x)
const deltaY = Math.abs(result.position.y - currentPosition.y)
const currentPosition = position.value
const deltaX = Math.abs(result.position.x - currentPosition.x)
const deltaY = Math.abs(result.position.y - currentPosition.y)
if (deltaX > POSITION_EPSILON || deltaY > POSITION_EPSILON) {
moveNodeTo(result.position)
if (deltaX > POSITION_EPSILON || deltaY > POSITION_EPSILON) {
moveNodeTo(result.position)
}
},
{
transformState
}
})
)
const handleResizePointerDown = (direction: ResizeHandleDirection) => {
return (event: PointerEvent) => {

View File

@@ -7,7 +7,12 @@ import { useShiftKeySync } from '@/renderer/extensions/vueNodes/composables/useS
import type { ResizeHandleDirection } from './resizeMath'
import { createResizeSession, toCanvasDelta } from './resizeMath'
import { useTransformState } from '@/renderer/core/layout/transform/useTransformState'
import type { useTransformState } from '@/renderer/core/layout/transform/useTransformState'
interface UseNodeResizeOptions {
/** Transform state for coordinate conversion */
transformState: ReturnType<typeof useTransformState>
}
interface ResizeCallbackPayload {
size: Size
@@ -21,9 +26,13 @@ interface ResizeCallbackPayload {
* Handles pointer capture, coordinate calculations, and size constraints.
*/
export function useNodeResize(
resizeCallback: (payload: ResizeCallbackPayload, element: HTMLElement) => void
resizeCallback: (
payload: ResizeCallbackPayload,
element: HTMLElement
) => void,
options: UseNodeResizeOptions
) {
const transformState = useTransformState()
const { transformState } = options
const isResizing = ref(false)
const resizeStartPointer = ref<Point | null>(null)

View File

@@ -374,6 +374,7 @@ const zSettings = z.object({
'Comfy.Workflow.ShowMissingNodesWarning': z.boolean(),
'Comfy.Workflow.ShowMissingModelsWarning': z.boolean(),
'Comfy.Workflow.WarnBlueprintOverwrite': z.boolean(),
'Comfy.Desktop.CloudNotificationShown': z.boolean(),
'Comfy.DisableFloatRounding': z.boolean(),
'Comfy.DisableSliders': z.boolean(),
'Comfy.DOMClippingEnabled': z.boolean(),

View File

@@ -2,6 +2,7 @@ import { merge } from 'es-toolkit/compat'
import type { Component } from 'vue'
import ApiNodesSignInContent from '@/components/dialog/content/ApiNodesSignInContent.vue'
import CloudNotificationContent from '@/components/dialog/content/CloudNotificationContent.vue'
import MissingNodesContent from '@/components/dialog/content/MissingNodesContent.vue'
import MissingNodesFooter from '@/components/dialog/content/MissingNodesFooter.vue'
import MissingNodesHeader from '@/components/dialog/content/MissingNodesHeader.vue'
@@ -541,6 +542,16 @@ export const useDialogService = () => {
show()
}
function showCloudNotification() {
dialogStore.showDialog({
key: 'global-cloud-notification',
component: CloudNotificationContent,
dialogComponentProps: {
closable: true
}
})
}
return {
showLoadWorkflowWarning,
showMissingModelsWarning,
@@ -555,6 +566,7 @@ export const useDialogService = () => {
showTopUpCreditsDialog,
showUpdatePasswordDialog,
showExtensionDialog,
showCloudNotification,
prompt,
showErrorDialog,
confirm,

View File

@@ -5,7 +5,6 @@ import Splitter from 'primevue/splitter'
import SplitterPanel from 'primevue/splitterpanel'
import { computed } from 'vue'
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
import LoginButton from '@/components/topbar/LoginButton.vue'
import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
@@ -15,20 +14,20 @@ import {
isValidWidgetValue,
safeWidgetMapper
} from '@/composables/graph/useGraphNodeManager'
import { useAssetsSidebarTab } from '@/composables/sidebarTabs/useAssetsSidebarTab'
import { t } from '@/i18n'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { useTelemetry } from '@/platform/telemetry'
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import NodeWidgets from '@/renderer/extensions/vueNodes/components/NodeWidgets.vue'
import WidgetInputNumberInput from '@/renderer/extensions/vueNodes/widgets/components/WidgetInputNumber.vue'
import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
//import { useQueueStore } from '@/stores/queueStore'
import { useQueueSettingsStore } from '@/stores/queueStore'
import { isElectron } from '@/utils/envUtil'
//const queueStore = useQueueStore()
const nodeOutputStore = useNodeOutputStore()
const commandStore = useCommandStore()
const nodeDatas = computed(() => {
@@ -115,16 +114,9 @@ function openFeedback() {
class="h-[calc(100%-38px)] w-full bg-comfy-menu-secondary-bg"
:pt="{ gutter: { class: 'bg-transparent w-4 -mx-3' } }"
>
<SplitterPanel :size="1" class="min-w-min bg-comfy-menu-bg">
<div
class="sidebar-content-container h-full w-full overflow-x-hidden overflow-y-auto border-r-1 border-node-component-border"
>
<ExtensionSlot :extension="useAssetsSidebarTab()" />
</div>
</SplitterPanel>
<SplitterPanel
:size="98"
class="flex flex-row overflow-y-auto flex-wrap min-w-min gap-4 m-4"
:size="99"
class="flex flex-row overflow-y-auto flex-wrap min-w-min gap-4"
>
<img
v-for="previewUrl in nodeOutputStore.latestOutput"
@@ -140,26 +132,18 @@ function openFeedback() {
</SplitterPanel>
<SplitterPanel :size="1" class="flex flex-col gap-1 p-1 min-w-min">
<div
class="actionbar-container flex h-12 items-center rounded-lg border border-[var(--interface-stroke)] p-2 gap-2 bg-comfy-menu-bg justify-end"
class="actionbar-container flex h-12 items-center rounded-lg border border-[var(--interface-stroke)] p-2 gap-2 bg-comfy-menu-bg justify-center"
>
<Button label="Feedback" severity="secondary" @click="openFeedback" />
<Button
:label="t('g.feedback')"
severity="secondary"
@click="openFeedback"
/>
<Button
:label="t('linearMode.openWorkflow')"
label="Open Workflow"
severity="secondary"
class="min-w-max"
icon="icon-[comfy--workflow]"
icon-pos="right"
@click="useCanvasStore().linearMode = false"
/>
<Button
:label="t('linearMode.share')"
severity="contrast"
@click="useWorkflowService().exportWorkflow('workflow', 'workflow')"
/>
<!--<Button label="Share" severity="contrast" /> Temporarily disabled-->
<CurrentUserButton v-if="isLoggedIn" />
<LoginButton v-else-if="isDesktop" />
</div>

View File

@@ -1,42 +0,0 @@
import { describe, expect, it, beforeEach } from 'vitest'
import { clearWorkflowPersistenceStorage } from '@/platform/workflow/persistence/workflowStorage'
describe('workflowStorage', () => {
beforeEach(() => {
localStorage.clear()
sessionStorage.clear()
})
it('clears all workflow persistence keys from storage', () => {
localStorage.setItem('workflow', 'data')
localStorage.setItem('Comfy.PreviousWorkflow', 'prev')
localStorage.setItem('Comfy.OpenWorkflowsPaths', '[]')
localStorage.setItem('Comfy.ActiveWorkflowIndex', '1')
localStorage.setItem('unrelated', 'keep')
sessionStorage.setItem('workflow:client-1', 'session-data')
sessionStorage.setItem('Comfy.PreviousWorkflow:client-1', 'prev')
sessionStorage.setItem('Comfy.ActiveWorkflowIndex:client-1', '0')
sessionStorage.setItem('Comfy.OpenWorkflowsPaths:client-1', '[]')
sessionStorage.setItem('custom', 'keep')
clearWorkflowPersistenceStorage()
expect(localStorage.getItem('workflow')).toBeNull()
expect(localStorage.getItem('Comfy.PreviousWorkflow')).toBeNull()
expect(localStorage.getItem('Comfy.OpenWorkflowsPaths')).toBeNull()
expect(localStorage.getItem('Comfy.ActiveWorkflowIndex')).toBeNull()
expect(localStorage.getItem('unrelated')).toBe('keep')
expect(sessionStorage.getItem('workflow:client-1')).toBeNull()
expect(sessionStorage.getItem('Comfy.PreviousWorkflow:client-1')).toBeNull()
expect(
sessionStorage.getItem('Comfy.ActiveWorkflowIndex:client-1')
).toBeNull()
expect(
sessionStorage.getItem('Comfy.OpenWorkflowsPaths:client-1')
).toBeNull()
expect(sessionStorage.getItem('custom')).toBe('keep')
})
})