mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 18:52:19 +00:00
Merge branch 'main' into version-bump-1.38.12
This commit is contained in:
@@ -25,13 +25,18 @@ defineProps<{
|
|||||||
"
|
"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'text-sm text-muted-foreground truncate',
|
'text-sm text-muted-foreground truncate group',
|
||||||
tooltip ? 'cursor-help' : '',
|
tooltip ? 'cursor-help' : '',
|
||||||
singleline ? 'flex-1' : ''
|
singleline ? 'flex-1' : ''
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
|
|
||||||
|
<i
|
||||||
|
v-if="tooltip"
|
||||||
|
class="icon-[lucide--info] ml-0.5 size-3 relative top-[1px] group-hover:text-primary"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -107,15 +107,17 @@ function openFullSettings() {
|
|||||||
<FieldSwitch
|
<FieldSwitch
|
||||||
v-model="showAdvancedParameters"
|
v-model="showAdvancedParameters"
|
||||||
:label="t('rightSidePanel.globalSettings.showAdvanced')"
|
:label="t('rightSidePanel.globalSettings.showAdvanced')"
|
||||||
:tooltip="t('rightSidePanel.globalSettings.showAdvancedTooltip')"
|
:tooltip="t('settings.Comfy_Node_AlwaysShowAdvancedWidgets.tooltip')"
|
||||||
/>
|
/>
|
||||||
<FieldSwitch
|
<FieldSwitch
|
||||||
v-model="showToolbox"
|
v-model="showToolbox"
|
||||||
:label="t('rightSidePanel.globalSettings.showToolbox')"
|
:label="t('rightSidePanel.globalSettings.showToolbox')"
|
||||||
|
:tooltip="t('settings.Comfy_Canvas_SelectionToolbox.tooltip')"
|
||||||
/>
|
/>
|
||||||
<FieldSwitch
|
<FieldSwitch
|
||||||
v-model="nodes2Enabled"
|
v-model="nodes2Enabled"
|
||||||
:label="t('rightSidePanel.globalSettings.nodes2')"
|
:label="t('rightSidePanel.globalSettings.nodes2')"
|
||||||
|
:tooltip="t('settings.Comfy_VueNodes_Enabled.tooltip')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</PropertiesAccordionItem>
|
</PropertiesAccordionItem>
|
||||||
@@ -156,6 +158,7 @@ function openFullSettings() {
|
|||||||
<FieldSwitch
|
<FieldSwitch
|
||||||
v-model="snapToGrid"
|
v-model="snapToGrid"
|
||||||
:label="t('rightSidePanel.globalSettings.snapNodesToGrid')"
|
:label="t('rightSidePanel.globalSettings.snapNodesToGrid')"
|
||||||
|
:tooltip="t('settings.pysssss_SnapToGrid.tooltip')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</PropertiesAccordionItem>
|
</PropertiesAccordionItem>
|
||||||
@@ -187,6 +190,7 @@ function openFullSettings() {
|
|||||||
<FieldSwitch
|
<FieldSwitch
|
||||||
v-model="showConnectedLinks"
|
v-model="showConnectedLinks"
|
||||||
:label="t('rightSidePanel.globalSettings.showConnectedLinks')"
|
:label="t('rightSidePanel.globalSettings.showConnectedLinks')"
|
||||||
|
:tooltip="t('settings.Comfy_LinkRenderMode.tooltip')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</PropertiesAccordionItem>
|
</PropertiesAccordionItem>
|
||||||
|
|||||||
@@ -25,7 +25,24 @@ vi.mock('firebase/auth', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock pinia
|
// 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
|
// Mock the useCurrentUser composable
|
||||||
vi.mock('@/composables/auth/useCurrentUser', () => ({
|
vi.mock('@/composables/auth/useCurrentUser', () => ({
|
||||||
|
|||||||
@@ -12,12 +12,18 @@
|
|||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'flex items-center gap-1 rounded-full hover:bg-interface-button-hover-surface justify-center',
|
'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
|
<WorkspaceProfilePic
|
||||||
v-if="showWorkspaceIcon"
|
v-else-if="showWorkspaceIcon"
|
||||||
:workspace-name="workspaceName"
|
:workspace-name="workspaceName"
|
||||||
:class="compact && 'size-full'"
|
:class="compact && 'size-full'"
|
||||||
/>
|
/>
|
||||||
@@ -40,13 +46,16 @@
|
|||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<!-- Workspace mode: workspace-aware popover -->
|
<!-- Workspace mode: workspace-aware popover (only when ready) -->
|
||||||
<CurrentUserPopoverWorkspace
|
<CurrentUserPopoverWorkspace
|
||||||
v-if="teamWorkspacesEnabled"
|
v-if="teamWorkspacesEnabled && initState === 'ready'"
|
||||||
@close="closePopover"
|
@close="closePopover"
|
||||||
/>
|
/>
|
||||||
<!-- Legacy mode: original popover -->
|
<!-- Legacy mode: original popover -->
|
||||||
<CurrentUserPopover v-else @close="closePopover" />
|
<CurrentUserPopover
|
||||||
|
v-else-if="!teamWorkspacesEnabled"
|
||||||
|
@close="closePopover"
|
||||||
|
/>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -54,6 +63,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import Popover from 'primevue/popover'
|
import Popover from 'primevue/popover'
|
||||||
|
import Skeleton from 'primevue/skeleton'
|
||||||
import { computed, defineAsyncComponent, ref } from 'vue'
|
import { computed, defineAsyncComponent, ref } from 'vue'
|
||||||
|
|
||||||
import UserAvatar from '@/components/common/UserAvatar.vue'
|
import UserAvatar from '@/components/common/UserAvatar.vue'
|
||||||
@@ -85,12 +95,20 @@ const photoURL = computed<string | undefined>(
|
|||||||
() => userPhotoUrl.value ?? 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(() => {
|
const workspaceName = computed(() => {
|
||||||
if (!showWorkspaceIcon.value) return ''
|
if (!showWorkspaceIcon.value) return ''
|
||||||
const { workspaceName } = storeToRefs(useTeamWorkspaceStore())
|
return teamWorkspaceName.value
|
||||||
return workspaceName.value
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const popover = ref<InstanceType<typeof Popover> | null>(null)
|
const popover = ref<InstanceType<typeof Popover> | null>(null)
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ import { useDialogService } from '@/services/dialogService'
|
|||||||
|
|
||||||
const workspaceStore = useTeamWorkspaceStore()
|
const workspaceStore = useTeamWorkspaceStore()
|
||||||
const {
|
const {
|
||||||
|
initState,
|
||||||
workspaceName,
|
workspaceName,
|
||||||
isInPersonalWorkspace: isPersonalWorkspace,
|
isInPersonalWorkspace: isPersonalWorkspace,
|
||||||
isWorkspaceSubscribed
|
isWorkspaceSubscribed
|
||||||
@@ -234,15 +235,20 @@ const { userDisplayName, userEmail, userPhotoUrl, handleSignOut } =
|
|||||||
useCurrentUser()
|
useCurrentUser()
|
||||||
const authActions = useFirebaseAuthActions()
|
const authActions = useFirebaseAuthActions()
|
||||||
const dialogService = useDialogService()
|
const dialogService = useDialogService()
|
||||||
const { isActiveSubscription } = useSubscription()
|
const { isActiveSubscription, subscriptionStatus } = useSubscription()
|
||||||
const { totalCredits, isLoadingBalance } = useSubscriptionCredits()
|
const { totalCredits, isLoadingBalance } = useSubscriptionCredits()
|
||||||
const subscriptionDialog = useSubscriptionDialog()
|
const subscriptionDialog = useSubscriptionDialog()
|
||||||
|
|
||||||
const displayedCredits = computed(() => {
|
const displayedCredits = computed(() => {
|
||||||
const isSubscribed = isPersonalWorkspace.value
|
if (initState.value !== 'ready') return ''
|
||||||
? isActiveSubscription.value
|
// Only personal workspaces have subscription status from useSubscription()
|
||||||
: isWorkspaceSubscribed.value
|
// Team workspaces don't have backend subscription data yet
|
||||||
return isSubscribed ? totalCredits.value : '0'
|
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(() => {
|
const canUpgrade = computed(() => {
|
||||||
|
|||||||
@@ -4283,6 +4283,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
|||||||
item.selected = true
|
item.selected = true
|
||||||
this.selectedItems.add(item)
|
this.selectedItems.add(item)
|
||||||
this.state.selectionChanged = true
|
this.state.selectionChanged = true
|
||||||
|
|
||||||
|
if (item instanceof LGraphGroup) {
|
||||||
|
item.recomputeInsideNodes()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!(item instanceof LGraphNode)) return
|
if (!(item instanceof LGraphNode)) return
|
||||||
|
|
||||||
// Node-specific handling
|
// Node-specific handling
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect } from 'vitest'
|
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'
|
import { test } from './__fixtures__/testExtensions'
|
||||||
|
|
||||||
@@ -9,4 +9,72 @@ describe('LGraphGroup', () => {
|
|||||||
const link = new LGraphGroup('title', 929)
|
const link = new LGraphGroup('title', 929)
|
||||||
expect(link.serialize()).toMatchSnapshot('Basic')
|
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)
|
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 (!this.graph) throw new NullGraphError()
|
||||||
|
if (maxDepth <= 0 || visited.has(this.id)) return
|
||||||
|
|
||||||
|
visited.add(this.id)
|
||||||
|
|
||||||
const { nodes, reroutes, groups } = this.graph
|
const { nodes, reroutes, groups } = this.graph
|
||||||
const children = this._children
|
const children = this._children
|
||||||
this._nodes.length = 0
|
this._nodes.length = 0
|
||||||
@@ -261,10 +274,16 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
|
|||||||
if (isPointInRect(reroute.pos, this._bounding)) children.add(reroute)
|
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) {
|
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) => {
|
groups.sort((a, b) => {
|
||||||
if (a === this) {
|
if (a === this) {
|
||||||
|
|||||||
@@ -52,7 +52,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Comfy_Canvas_SelectionToolbox": {
|
"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": {
|
"Comfy_ConfirmClear": {
|
||||||
"name": "Require confirmation when clearing workflow"
|
"name": "Require confirmation when clearing workflow"
|
||||||
@@ -142,6 +143,7 @@
|
|||||||
},
|
},
|
||||||
"Comfy_LinkRenderMode": {
|
"Comfy_LinkRenderMode": {
|
||||||
"name": "Link Render Mode",
|
"name": "Link Render Mode",
|
||||||
|
"tooltip": "Controls the appearance and visibility of connection links between nodes on the canvas.",
|
||||||
"options": {
|
"options": {
|
||||||
"Straight": "Straight",
|
"Straight": "Straight",
|
||||||
"Linear": "Linear",
|
"Linear": "Linear",
|
||||||
@@ -253,6 +255,10 @@
|
|||||||
"name": "Snap highlights node",
|
"name": "Snap highlights node",
|
||||||
"tooltip": "When dragging a link over a node with viable input slot, highlight the 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": {
|
"Comfy_NodeBadge_NodeIdBadgeMode": {
|
||||||
"name": "Node ID badge mode",
|
"name": "Node ID badge mode",
|
||||||
"options": {
|
"options": {
|
||||||
@@ -474,6 +480,7 @@
|
|||||||
"tooltip": "The bezier control point offset from the reroute centre point"
|
"tooltip": "The bezier control point offset from the reroute centre point"
|
||||||
},
|
},
|
||||||
"pysssss_SnapToGrid": {
|
"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
|
<video
|
||||||
:controls="shouldShowControls"
|
:controls="shouldShowControls"
|
||||||
preload="metadata"
|
preload="metadata"
|
||||||
autoplay
|
|
||||||
muted
|
muted
|
||||||
loop
|
loop
|
||||||
playsinline
|
playsinline
|
||||||
|
|||||||
@@ -642,6 +642,8 @@ export const CORE_SETTINGS: SettingParams[] = [
|
|||||||
id: 'Comfy.LinkRenderMode',
|
id: 'Comfy.LinkRenderMode',
|
||||||
category: ['LiteGraph', 'Graph', 'LinkRenderMode'],
|
category: ['LiteGraph', 'Graph', 'LinkRenderMode'],
|
||||||
name: 'Link Render Mode',
|
name: 'Link Render Mode',
|
||||||
|
tooltip:
|
||||||
|
'Controls the appearance and visibility of connection links between nodes on the canvas.',
|
||||||
defaultValue: 2,
|
defaultValue: 2,
|
||||||
type: 'combo',
|
type: 'combo',
|
||||||
options: [
|
options: [
|
||||||
@@ -793,6 +795,8 @@ export const CORE_SETTINGS: SettingParams[] = [
|
|||||||
id: 'pysssss.SnapToGrid',
|
id: 'pysssss.SnapToGrid',
|
||||||
category: ['LiteGraph', 'Canvas', 'AlwaysSnapToGrid'],
|
category: ['LiteGraph', 'Canvas', 'AlwaysSnapToGrid'],
|
||||||
name: 'Always snap to grid',
|
name: 'Always snap to grid',
|
||||||
|
tooltip:
|
||||||
|
'When enabled, nodes will automatically align to the grid when moved or resized.',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
versionAdded: '1.3.13'
|
versionAdded: '1.3.13'
|
||||||
@@ -960,6 +964,8 @@ export const CORE_SETTINGS: SettingParams[] = [
|
|||||||
id: 'Comfy.Canvas.SelectionToolbox',
|
id: 'Comfy.Canvas.SelectionToolbox',
|
||||||
category: ['LiteGraph', 'Canvas', 'SelectionToolbox'],
|
category: ['LiteGraph', '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.',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
versionAdded: '1.10.5'
|
versionAdded: '1.10.5'
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ const maxSelectable = computed(() => {
|
|||||||
return 1
|
return 1
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const itemsKey = computed(() => props.items.map((item) => item.id).join('|'))
|
||||||
|
|
||||||
const filteredItems = ref<DropdownItem[]>([])
|
const filteredItems = ref<DropdownItem[]>([])
|
||||||
|
|
||||||
const defaultSorter = computed<SortOption['sorter']>(() => {
|
const defaultSorter = computed<SortOption['sorter']>(() => {
|
||||||
@@ -209,6 +211,7 @@ async function customSearcher(
|
|||||||
:items="sortedItems"
|
:items="sortedItems"
|
||||||
:is-selected="internalIsSelected"
|
:is-selected="internalIsSelected"
|
||||||
:max-selectable="maxSelectable"
|
:max-selectable="maxSelectable"
|
||||||
|
:update-key="itemsKey"
|
||||||
@close="closeDropdown"
|
@close="closeDropdown"
|
||||||
@item-click="handleSelection"
|
@item-click="handleSelection"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { MaybeRefOrGetter } from 'vue'
|
||||||
|
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
import FormDropdownMenuActions from './FormDropdownMenuActions.vue'
|
import FormDropdownMenuActions from './FormDropdownMenuActions.vue'
|
||||||
@@ -21,6 +23,7 @@ interface Props {
|
|||||||
query: string,
|
query: string,
|
||||||
onCleanup: (cleanupFn: () => void) => void
|
onCleanup: (cleanupFn: () => void) => void
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
|
updateKey?: MaybeRefOrGetter<unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProps<Props>()
|
defineProps<Props>()
|
||||||
@@ -54,6 +57,7 @@ const searchQuery = defineModel<string>('searchQuery')
|
|||||||
v-model:search-query="searchQuery"
|
v-model:search-query="searchQuery"
|
||||||
:sort-options="sortOptions"
|
:sort-options="sortOptions"
|
||||||
:searcher
|
:searcher
|
||||||
|
:update-key="updateKey"
|
||||||
/>
|
/>
|
||||||
<!-- List -->
|
<!-- List -->
|
||||||
<div class="relative flex h-full mt-2 overflow-y-scroll">
|
<div class="relative flex h-full mt-2 overflow-y-scroll">
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { MaybeRefOrGetter } from 'vue'
|
||||||
|
|
||||||
import Popover from 'primevue/popover'
|
import Popover from 'primevue/popover'
|
||||||
import { ref, useTemplateRef } from 'vue'
|
import { ref, useTemplateRef } from 'vue'
|
||||||
|
|
||||||
@@ -13,6 +15,7 @@ defineProps<{
|
|||||||
onCleanup: (cleanupFn: () => void) => void
|
onCleanup: (cleanupFn: () => void) => void
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
sortOptions: SortOption[]
|
sortOptions: SortOption[]
|
||||||
|
updateKey?: MaybeRefOrGetter<unknown>
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const layoutMode = defineModel<LayoutMode>('layoutMode')
|
const layoutMode = defineModel<LayoutMode>('layoutMode')
|
||||||
@@ -53,6 +56,7 @@ function handleSortSelected(item: SortOption) {
|
|||||||
<FormSearchInput
|
<FormSearchInput
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
:searcher
|
:searcher
|
||||||
|
:update-key="updateKey"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
actionButtonStyle,
|
actionButtonStyle,
|
||||||
|
|||||||
Reference in New Issue
Block a user