Merge branch 'main' into version-bump-1.38.12

This commit is contained in:
Alexander Brown
2026-01-26 19:07:48 -08:00
committed by GitHub
14 changed files with 190 additions and 24 deletions

View File

@@ -25,13 +25,18 @@ defineProps<{
"
:class="
cn(
'text-sm text-muted-foreground truncate',
'text-sm text-muted-foreground truncate group',
tooltip ? 'cursor-help' : '',
singleline ? 'flex-1' : ''
)
"
>
{{ label }}
<i
v-if="tooltip"
class="icon-[lucide--info] ml-0.5 size-3 relative top-[1px] group-hover:text-primary"
/>
</span>
<slot />
</div>

View File

@@ -107,15 +107,17 @@ function openFullSettings() {
<FieldSwitch
v-model="showAdvancedParameters"
:label="t('rightSidePanel.globalSettings.showAdvanced')"
:tooltip="t('rightSidePanel.globalSettings.showAdvancedTooltip')"
:tooltip="t('settings.Comfy_Node_AlwaysShowAdvancedWidgets.tooltip')"
/>
<FieldSwitch
v-model="showToolbox"
:label="t('rightSidePanel.globalSettings.showToolbox')"
:tooltip="t('settings.Comfy_Canvas_SelectionToolbox.tooltip')"
/>
<FieldSwitch
v-model="nodes2Enabled"
:label="t('rightSidePanel.globalSettings.nodes2')"
:tooltip="t('settings.Comfy_VueNodes_Enabled.tooltip')"
/>
</div>
</PropertiesAccordionItem>
@@ -156,6 +158,7 @@ function openFullSettings() {
<FieldSwitch
v-model="snapToGrid"
:label="t('rightSidePanel.globalSettings.snapNodesToGrid')"
:tooltip="t('settings.pysssss_SnapToGrid.tooltip')"
/>
</div>
</PropertiesAccordionItem>
@@ -187,6 +190,7 @@ function openFullSettings() {
<FieldSwitch
v-model="showConnectedLinks"
:label="t('rightSidePanel.globalSettings.showConnectedLinks')"
:tooltip="t('settings.Comfy_LinkRenderMode.tooltip')"
/>
</div>
</PropertiesAccordionItem>

View File

@@ -25,7 +25,24 @@ vi.mock('firebase/auth', () => ({
}))
// Mock pinia
vi.mock('pinia')
vi.mock('pinia', () => ({
storeToRefs: vi.fn((store) => store)
}))
// Mock the useFeatureFlags composable
vi.mock('@/composables/useFeatureFlags', () => ({
useFeatureFlags: vi.fn(() => ({
flags: { teamWorkspacesEnabled: false }
}))
}))
// Mock the useTeamWorkspaceStore
vi.mock('@/platform/workspace/stores/teamWorkspaceStore', () => ({
useTeamWorkspaceStore: vi.fn(() => ({
workspaceName: { value: '' },
initState: { value: 'idle' }
}))
}))
// Mock the useCurrentUser composable
vi.mock('@/composables/auth/useCurrentUser', () => ({

View File

@@ -12,12 +12,18 @@
:class="
cn(
'flex items-center gap-1 rounded-full hover:bg-interface-button-hover-surface justify-center',
compact && 'size-full aspect-square'
compact && 'size-full '
)
"
>
<Skeleton
v-if="showWorkspaceSkeleton"
shape="circle"
width="32px"
height="32px"
/>
<WorkspaceProfilePic
v-if="showWorkspaceIcon"
v-else-if="showWorkspaceIcon"
:workspace-name="workspaceName"
:class="compact && 'size-full'"
/>
@@ -40,13 +46,16 @@
}
}"
>
<!-- Workspace mode: workspace-aware popover -->
<!-- Workspace mode: workspace-aware popover (only when ready) -->
<CurrentUserPopoverWorkspace
v-if="teamWorkspacesEnabled"
v-if="teamWorkspacesEnabled && initState === 'ready'"
@close="closePopover"
/>
<!-- Legacy mode: original popover -->
<CurrentUserPopover v-else @close="closePopover" />
<CurrentUserPopover
v-else-if="!teamWorkspacesEnabled"
@close="closePopover"
/>
</Popover>
</div>
</template>
@@ -54,6 +63,7 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import Popover from 'primevue/popover'
import Skeleton from 'primevue/skeleton'
import { computed, defineAsyncComponent, ref } from 'vue'
import UserAvatar from '@/components/common/UserAvatar.vue'
@@ -85,12 +95,20 @@ const photoURL = computed<string | undefined>(
() => userPhotoUrl.value ?? undefined
)
const showWorkspaceIcon = computed(() => isCloud && teamWorkspacesEnabled.value)
const { workspaceName: teamWorkspaceName, initState } = storeToRefs(
useTeamWorkspaceStore()
)
const showWorkspaceSkeleton = computed(
() => isCloud && teamWorkspacesEnabled.value && initState.value === 'loading'
)
const showWorkspaceIcon = computed(
() => isCloud && teamWorkspacesEnabled.value && initState.value === 'ready'
)
const workspaceName = computed(() => {
if (!showWorkspaceIcon.value) return ''
const { workspaceName } = storeToRefs(useTeamWorkspaceStore())
return workspaceName.value
return teamWorkspaceName.value
})
const popover = ref<InstanceType<typeof Popover> | null>(null)

View File

@@ -217,6 +217,7 @@ import { useDialogService } from '@/services/dialogService'
const workspaceStore = useTeamWorkspaceStore()
const {
initState,
workspaceName,
isInPersonalWorkspace: isPersonalWorkspace,
isWorkspaceSubscribed
@@ -234,15 +235,20 @@ const { userDisplayName, userEmail, userPhotoUrl, handleSignOut } =
useCurrentUser()
const authActions = useFirebaseAuthActions()
const dialogService = useDialogService()
const { isActiveSubscription } = useSubscription()
const { isActiveSubscription, subscriptionStatus } = useSubscription()
const { totalCredits, isLoadingBalance } = useSubscriptionCredits()
const subscriptionDialog = useSubscriptionDialog()
const displayedCredits = computed(() => {
const isSubscribed = isPersonalWorkspace.value
? isActiveSubscription.value
: isWorkspaceSubscribed.value
return isSubscribed ? totalCredits.value : '0'
if (initState.value !== 'ready') return ''
// Only personal workspaces have subscription status from useSubscription()
// Team workspaces don't have backend subscription data yet
if (isPersonalWorkspace.value) {
// Wait for subscription status to load
if (subscriptionStatus.value === null) return ''
return isActiveSubscription.value ? totalCredits.value : '0'
}
return '0'
})
const canUpgrade = computed(() => {

View File

@@ -4283,6 +4283,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
item.selected = true
this.selectedItems.add(item)
this.state.selectionChanged = true
if (item instanceof LGraphGroup) {
item.recomputeInsideNodes()
return
}
if (!(item instanceof LGraphNode)) return
// Node-specific handling

View File

@@ -1,6 +1,6 @@
import { describe, expect } from 'vitest'
import { LGraphGroup } from '@/lib/litegraph/src/litegraph'
import { LGraph, LGraphGroup } from '@/lib/litegraph/src/litegraph'
import { test } from './__fixtures__/testExtensions'
@@ -9,4 +9,72 @@ describe('LGraphGroup', () => {
const link = new LGraphGroup('title', 929)
expect(link.serialize()).toMatchSnapshot('Basic')
})
describe('recomputeInsideNodes', () => {
test('uses visited set to avoid redundant computation', () => {
const graph = new LGraph()
// Create 4 nested groups: outer -> mid1 -> mid2 -> inner
const outer = new LGraphGroup('outer')
outer.pos = [0, 0]
outer.size = [400, 400]
graph.add(outer)
const mid1 = new LGraphGroup('mid1')
mid1.pos = [10, 10]
mid1.size = [300, 300]
graph.add(mid1)
const mid2 = new LGraphGroup('mid2')
mid2.pos = [20, 20]
mid2.size = [200, 200]
graph.add(mid2)
const inner = new LGraphGroup('inner')
inner.pos = [30, 30]
inner.size = [100, 100]
graph.add(inner)
// Track the visited set to verify each group is only fully processed once
const visited = new Set<number>()
outer.recomputeInsideNodes(100, visited)
// All nested groups should be in the visited set
expect(visited.has(outer.id)).toBe(true)
expect(visited.has(mid1.id)).toBe(true)
expect(visited.has(mid2.id)).toBe(true)
expect(visited.has(inner.id)).toBe(true)
expect(visited.size).toBe(4)
// Verify children relationships are correct
expect(outer.children.has(mid1)).toBe(true)
expect(outer.children.has(mid2)).toBe(true)
expect(outer.children.has(inner)).toBe(true)
expect(mid1.children.has(mid2)).toBe(true)
expect(mid1.children.has(inner)).toBe(true)
expect(mid2.children.has(inner)).toBe(true)
})
test('respects maxDepth limit', () => {
const graph = new LGraph()
const outer = new LGraphGroup('outer')
outer.pos = [0, 0]
outer.size = [300, 300]
graph.add(outer)
const inner = new LGraphGroup('inner')
inner.pos = [10, 10]
inner.size = [100, 100]
graph.add(inner)
// With maxDepth=1, inner group is added as child but not processed
outer.recomputeInsideNodes(1)
// outer should have inner as a child
expect(outer.children.has(inner)).toBe(true)
// inner should not have computed its own children (it was never processed)
expect(inner.children.size).toBe(0)
})
})
})

View File

@@ -241,8 +241,21 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
return this.pinned ? false : snapPoint(this.pos, snapTo)
}
recomputeInsideNodes(): void {
/**
* Recomputes which items (nodes, reroutes, nested groups) are inside this group.
* Recursively processes nested groups to ensure their children are also computed.
* @param maxDepth Maximum recursion depth for nested groups. Use 1 to skip nested group computation.
* @param visited Set of already visited group IDs to prevent redundant computation.
*/
recomputeInsideNodes(
maxDepth: number = 100,
visited: Set<number> = new Set()
): void {
if (!this.graph) throw new NullGraphError()
if (maxDepth <= 0 || visited.has(this.id)) return
visited.add(this.id)
const { nodes, reroutes, groups } = this.graph
const children = this._children
this._nodes.length = 0
@@ -261,10 +274,16 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
if (isPointInRect(reroute.pos, this._bounding)) children.add(reroute)
}
// Move groups we wholly contain
// Move groups we wholly contain and recursively compute their children
const containedGroups: LGraphGroup[] = []
for (const group of groups) {
if (containsRect(this._bounding, group._bounding)) children.add(group)
if (group !== this && containsRect(this._bounding, group._bounding)) {
children.add(group)
containedGroups.push(group)
}
}
for (const group of containedGroups)
group.recomputeInsideNodes(maxDepth - 1, visited)
groups.sort((a, b) => {
if (a === this) {

View File

@@ -52,7 +52,8 @@
}
},
"Comfy_Canvas_SelectionToolbox": {
"name": "Show selection toolbox"
"name": "Show selection toolbox",
"tooltip": "Display a floating toolbar when nodes are selected, providing quick access to common actions."
},
"Comfy_ConfirmClear": {
"name": "Require confirmation when clearing workflow"
@@ -142,6 +143,7 @@
},
"Comfy_LinkRenderMode": {
"name": "Link Render Mode",
"tooltip": "Controls the appearance and visibility of connection links between nodes on the canvas.",
"options": {
"Straight": "Straight",
"Linear": "Linear",
@@ -253,6 +255,10 @@
"name": "Snap highlights node",
"tooltip": "When dragging a link over a node with viable input slot, highlight the node"
},
"Comfy_Node_AlwaysShowAdvancedWidgets": {
"name": "Always show advanced widgets on all nodes",
"tooltip": "When enabled, advanced widgets are always visible on all nodes without needing to expand them individually."
},
"Comfy_NodeBadge_NodeIdBadgeMode": {
"name": "Node ID badge mode",
"options": {
@@ -474,6 +480,7 @@
"tooltip": "The bezier control point offset from the reroute centre point"
},
"pysssss_SnapToGrid": {
"name": "Always snap to grid"
"name": "Always snap to grid",
"tooltip": "When enabled, nodes will automatically align to the grid when moved or resized."
}
}
}

View File

@@ -7,7 +7,6 @@
<video
:controls="shouldShowControls"
preload="metadata"
autoplay
muted
loop
playsinline

View File

@@ -642,6 +642,8 @@ export const CORE_SETTINGS: SettingParams[] = [
id: 'Comfy.LinkRenderMode',
category: ['LiteGraph', 'Graph', 'LinkRenderMode'],
name: 'Link Render Mode',
tooltip:
'Controls the appearance and visibility of connection links between nodes on the canvas.',
defaultValue: 2,
type: 'combo',
options: [
@@ -793,6 +795,8 @@ export const CORE_SETTINGS: SettingParams[] = [
id: 'pysssss.SnapToGrid',
category: ['LiteGraph', 'Canvas', 'AlwaysSnapToGrid'],
name: 'Always snap to grid',
tooltip:
'When enabled, nodes will automatically align to the grid when moved or resized.',
type: 'boolean',
defaultValue: false,
versionAdded: '1.3.13'
@@ -960,6 +964,8 @@ export const CORE_SETTINGS: SettingParams[] = [
id: 'Comfy.Canvas.SelectionToolbox',
category: ['LiteGraph', 'Canvas', 'SelectionToolbox'],
name: 'Show selection toolbox',
tooltip:
'Display a floating toolbar when nodes are selected, providing quick access to common actions.',
type: 'boolean',
defaultValue: true,
versionAdded: '1.10.5'

View File

@@ -78,6 +78,8 @@ const maxSelectable = computed(() => {
return 1
})
const itemsKey = computed(() => props.items.map((item) => item.id).join('|'))
const filteredItems = ref<DropdownItem[]>([])
const defaultSorter = computed<SortOption['sorter']>(() => {
@@ -209,6 +211,7 @@ async function customSearcher(
:items="sortedItems"
:is-selected="internalIsSelected"
:max-selectable="maxSelectable"
:update-key="itemsKey"
@close="closeDropdown"
@item-click="handleSelection"
/>

View File

@@ -1,4 +1,6 @@
<script setup lang="ts">
import type { MaybeRefOrGetter } from 'vue'
import { cn } from '@/utils/tailwindUtil'
import FormDropdownMenuActions from './FormDropdownMenuActions.vue'
@@ -21,6 +23,7 @@ interface Props {
query: string,
onCleanup: (cleanupFn: () => void) => void
) => Promise<void>
updateKey?: MaybeRefOrGetter<unknown>
}
defineProps<Props>()
@@ -54,6 +57,7 @@ const searchQuery = defineModel<string>('searchQuery')
v-model:search-query="searchQuery"
:sort-options="sortOptions"
:searcher
:update-key="updateKey"
/>
<!-- List -->
<div class="relative flex h-full mt-2 overflow-y-scroll">

View File

@@ -1,4 +1,6 @@
<script setup lang="ts">
import type { MaybeRefOrGetter } from 'vue'
import Popover from 'primevue/popover'
import { ref, useTemplateRef } from 'vue'
@@ -13,6 +15,7 @@ defineProps<{
onCleanup: (cleanupFn: () => void) => void
) => Promise<void>
sortOptions: SortOption[]
updateKey?: MaybeRefOrGetter<unknown>
}>()
const layoutMode = defineModel<LayoutMode>('layoutMode')
@@ -53,6 +56,7 @@ function handleSortSelected(item: SortOption) {
<FormSearchInput
v-model="searchQuery"
:searcher
:update-key="updateKey"
:class="
cn(
actionButtonStyle,