mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-07 00:20:07 +00:00
Merge branch 'main' into version-bump-1.38.12
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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', () => ({
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<video
|
||||
:controls="shouldShowControls"
|
||||
preload="metadata"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user