mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-15 01:48:06 +00:00
Compare commits
9 Commits
test/subgr
...
v1.35.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a68c169179 | ||
|
|
0fed12a62d | ||
|
|
8238afacf2 | ||
|
|
14c12350eb | ||
|
|
292fd5eb68 | ||
|
|
47aab2c8e1 | ||
|
|
c5312d6963 | ||
|
|
43e11c6873 | ||
|
|
4a041bead2 |
92
browser_tests/assets/groups/nested-groups-1-inner-node.json
Normal file
92
browser_tests/assets/groups/nested-groups-1-inner-node.json
Normal file
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"id": "2ba0b800-2f13-4f21-b8d6-c6cdb0152cae",
|
||||
"revision": 0,
|
||||
"last_node_id": 17,
|
||||
"last_link_id": 9,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 17,
|
||||
"type": "VAEDecode",
|
||||
"pos": [
|
||||
318.8446183157076,
|
||||
355.3961392345528
|
||||
],
|
||||
"size": [
|
||||
225,
|
||||
102
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [
|
||||
{
|
||||
"id": 4,
|
||||
"title": "Outer Group",
|
||||
"bounding": [
|
||||
-46.25245366331014,
|
||||
-150.82497138023245,
|
||||
1034.4034361963616,
|
||||
1007.338460439933
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "Inner Group",
|
||||
"bounding": [
|
||||
80.96059074101554,
|
||||
28.123757436778178,
|
||||
718.286373661183,
|
||||
691.2397164539732
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.7121393732101533,
|
||||
"offset": [
|
||||
289.18242848011835,
|
||||
367.0747755524199
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.35.5",
|
||||
"VHS_latentpreview": false,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true,
|
||||
"workflowRendererVersion": "Vue"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -1653,6 +1653,55 @@ export class ComfyPage {
|
||||
}, focusMode)
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the position of a group by title.
|
||||
* @param title The title of the group to find
|
||||
* @returns The group's canvas position
|
||||
* @throws Error if group not found
|
||||
*/
|
||||
async getGroupPosition(title: string): Promise<Position> {
|
||||
const pos = await this.page.evaluate((title) => {
|
||||
const groups = window['app'].graph.groups
|
||||
const group = groups.find((g: { title: string }) => g.title === title)
|
||||
if (!group) return null
|
||||
return { x: group.pos[0], y: group.pos[1] }
|
||||
}, title)
|
||||
if (!pos) throw new Error(`Group "${title}" not found`)
|
||||
return pos
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag a group by its title.
|
||||
* @param options.name The title of the group to drag
|
||||
* @param options.deltaX Horizontal drag distance in screen pixels
|
||||
* @param options.deltaY Vertical drag distance in screen pixels
|
||||
*/
|
||||
async dragGroup(options: {
|
||||
name: string
|
||||
deltaX: number
|
||||
deltaY: number
|
||||
}): Promise<void> {
|
||||
const { name, deltaX, deltaY } = options
|
||||
const screenPos = await this.page.evaluate((title) => {
|
||||
const app = window['app']
|
||||
const groups = app.graph.groups
|
||||
const group = groups.find((g: { title: string }) => g.title === title)
|
||||
if (!group) return null
|
||||
// Position in the title area of the group
|
||||
const clientPos = app.canvasPosToClientPos([
|
||||
group.pos[0] + 50,
|
||||
group.pos[1] + 15
|
||||
])
|
||||
return { x: clientPos[0], y: clientPos[1] }
|
||||
}, name)
|
||||
if (!screenPos) throw new Error(`Group "${name}" not found`)
|
||||
|
||||
await this.dragAndDrop(screenPos, {
|
||||
x: screenPos.x + deltaX,
|
||||
y: screenPos.y + deltaY
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const testComfySnapToGridGridSize = 50
|
||||
|
||||
@@ -32,4 +32,42 @@ test.describe('Vue Node Groups', () => {
|
||||
'vue-groups-fit-to-contents.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('should move nested groups together when dragging outer group', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('groups/nested-groups-1-inner-node')
|
||||
|
||||
// Get initial positions with null guards
|
||||
const outerInitial = await comfyPage.getGroupPosition('Outer Group')
|
||||
const innerInitial = await comfyPage.getGroupPosition('Inner Group')
|
||||
|
||||
const initialOffsetX = innerInitial.x - outerInitial.x
|
||||
const initialOffsetY = innerInitial.y - outerInitial.y
|
||||
|
||||
// Drag the outer group
|
||||
const dragDelta = { x: 100, y: 80 }
|
||||
await comfyPage.dragGroup({
|
||||
name: 'Outer Group',
|
||||
deltaX: dragDelta.x,
|
||||
deltaY: dragDelta.y
|
||||
})
|
||||
|
||||
// Use retrying assertion to wait for positions to update
|
||||
await expect(async () => {
|
||||
const outerFinal = await comfyPage.getGroupPosition('Outer Group')
|
||||
const innerFinal = await comfyPage.getGroupPosition('Inner Group')
|
||||
|
||||
const finalOffsetX = innerFinal.x - outerFinal.x
|
||||
const finalOffsetY = innerFinal.y - outerFinal.y
|
||||
|
||||
// Both groups should have moved
|
||||
expect(outerFinal.x).not.toBe(outerInitial.x)
|
||||
expect(innerFinal.x).not.toBe(innerInitial.x)
|
||||
|
||||
// The relative offset should be maintained (inner group moved with outer)
|
||||
expect(finalOffsetX).toBeCloseTo(initialOffsetX, 0)
|
||||
expect(finalOffsetY).toBeCloseTo(initialOffsetY, 0)
|
||||
}).toPass({ timeout: 5000 })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.35.7",
|
||||
"version": "1.35.8",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
@@ -10,48 +10,66 @@
|
||||
</div>
|
||||
|
||||
<div class="mx-1 flex flex-col items-end gap-1">
|
||||
<div
|
||||
class="actionbar-container pointer-events-auto flex h-12 items-center rounded-lg border border-interface-stroke px-2 shadow-interface"
|
||||
>
|
||||
<ActionBarButtons />
|
||||
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
ref="legacyCommandsContainerRef"
|
||||
class="[&:not(:has(*>*:not(:empty)))]:hidden"
|
||||
></div>
|
||||
<ComfyActionbar />
|
||||
<IconButton
|
||||
v-tooltip.bottom="queueHistoryTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="relative mr-2 text-base-foreground transition-colors duration-200 ease-in-out bg-secondary-background hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
|
||||
:aria-pressed="isQueueOverlayExpanded"
|
||||
:aria-label="
|
||||
t('sideToolbar.queueProgressOverlay.expandCollapsedQueue')
|
||||
"
|
||||
@click="toggleQueueOverlay"
|
||||
v-if="managerState.shouldShowManagerButtons.value && isDesktop"
|
||||
class="pointer-events-auto flex h-12 shrink-0 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
|
||||
>
|
||||
<i class="icon-[lucide--history] size-4" />
|
||||
<span
|
||||
v-if="queuedCount > 0"
|
||||
class="absolute -top-1 -right-1 min-w-[16px] rounded-full bg-primary-background py-0.25 text-[10px] font-medium leading-[14px] text-white"
|
||||
<IconButton
|
||||
v-tooltip.bottom="customNodesManagerTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="text-base-foreground transition-colors duration-200 ease-in-out bg-secondary-background hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
|
||||
:aria-label="t('menu.customNodesManager')"
|
||||
@click="openCustomNodeManager"
|
||||
>
|
||||
{{ queuedCount }}
|
||||
</span>
|
||||
</IconButton>
|
||||
<CurrentUserButton v-if="isLoggedIn" class="shrink-0" />
|
||||
<LoginButton v-else-if="isDesktop" />
|
||||
<IconButton
|
||||
v-if="!isRightSidePanelOpen"
|
||||
v-tooltip.bottom="rightSidePanelTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="mr-2 text-base-foreground transition-colors duration-200 ease-in-out bg-secondary-background hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
|
||||
:aria-label="t('rightSidePanel.togglePanel')"
|
||||
@click="rightSidePanelStore.togglePanel"
|
||||
<i class="icon-[lucide--puzzle] size-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="actionbar-container pointer-events-auto flex h-12 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
|
||||
>
|
||||
<i class="icon-[lucide--panel-right] size-4" />
|
||||
</IconButton>
|
||||
<ActionBarButtons />
|
||||
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
|
||||
<div
|
||||
ref="legacyCommandsContainerRef"
|
||||
class="[&:not(:has(*>*:not(:empty)))]:hidden"
|
||||
></div>
|
||||
<ComfyActionbar />
|
||||
<IconButton
|
||||
v-tooltip.bottom="queueHistoryTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="relative mr-2 text-base-foreground transition-colors duration-200 ease-in-out bg-secondary-background hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
|
||||
:aria-pressed="isQueueOverlayExpanded"
|
||||
:aria-label="
|
||||
t('sideToolbar.queueProgressOverlay.expandCollapsedQueue')
|
||||
"
|
||||
@click="toggleQueueOverlay"
|
||||
>
|
||||
<i class="icon-[lucide--history] size-4" />
|
||||
<span
|
||||
v-if="queuedCount > 0"
|
||||
class="absolute -top-1 -right-1 min-w-[16px] rounded-full bg-primary-background py-0.25 text-[10px] font-medium leading-[14px] text-white"
|
||||
>
|
||||
{{ queuedCount }}
|
||||
</span>
|
||||
</IconButton>
|
||||
<CurrentUserButton v-if="isLoggedIn" class="shrink-0" />
|
||||
<LoginButton v-else-if="isDesktop" />
|
||||
<IconButton
|
||||
v-if="!isRightSidePanelOpen"
|
||||
v-tooltip.bottom="rightSidePanelTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="mr-2 text-base-foreground transition-colors duration-200 ease-in-out bg-secondary-background hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
|
||||
:aria-label="t('rightSidePanel.togglePanel')"
|
||||
@click="rightSidePanelStore.togglePanel"
|
||||
>
|
||||
<i class="icon-[lucide--panel-right] size-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<QueueProgressOverlay
|
||||
v-model:expanded="isQueueOverlayExpanded"
|
||||
@@ -74,18 +92,23 @@ import ActionBarButtons from '@/components/topbar/ActionBarButtons.vue'
|
||||
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
|
||||
import LoginButton from '@/components/topbar/LoginButton.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useQueueStore } from '@/stores/queueStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
||||
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const managerState = useManagerState()
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
const isDesktop = isElectron()
|
||||
const { t } = useI18n()
|
||||
const { toastErrorHandler } = useErrorHandling()
|
||||
const isQueueOverlayExpanded = ref(false)
|
||||
const queueStore = useQueueStore()
|
||||
const isTopMenuHovered = ref(false)
|
||||
@@ -93,6 +116,9 @@ const queuedCount = computed(() => queueStore.pendingTasks.length)
|
||||
const queueHistoryTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory'))
|
||||
)
|
||||
const customNodesManagerTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('menu.customNodesManager'))
|
||||
)
|
||||
|
||||
// Right side panel toggle
|
||||
const { isOpen: isRightSidePanelOpen } = storeToRefs(rightSidePanelStore)
|
||||
@@ -112,10 +138,20 @@ onMounted(() => {
|
||||
const toggleQueueOverlay = () => {
|
||||
isQueueOverlayExpanded.value = !isQueueOverlayExpanded.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.actionbar-container {
|
||||
background-color: var(--comfy-menu-bg);
|
||||
const openCustomNodeManager = async () => {
|
||||
try {
|
||||
await managerState.openManager({
|
||||
initialTab: ManagerTab.All,
|
||||
showToastOnLegacyError: false
|
||||
})
|
||||
} catch (error) {
|
||||
try {
|
||||
toastErrorHandler(error)
|
||||
} catch (toastError) {
|
||||
console.error(error)
|
||||
console.error(toastError)
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</script>
|
||||
|
||||
@@ -173,7 +173,12 @@ export function useMoreOptionsMenu() {
|
||||
}
|
||||
|
||||
// Section 5: Subgraph operations
|
||||
options.push(...getSubgraphOptions(hasSubgraphsSelected))
|
||||
options.push(
|
||||
...getSubgraphOptions({
|
||||
hasSubgraphs: hasSubgraphsSelected,
|
||||
hasMultipleSelection: hasMultipleNodes.value
|
||||
})
|
||||
)
|
||||
|
||||
// Section 6: Multiple nodes operations
|
||||
if (hasMultipleNodes.value) {
|
||||
|
||||
106
src/composables/graph/useSelectionMenuOptions.test.ts
Normal file
106
src/composables/graph/useSelectionMenuOptions.test.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useSelectionMenuOptions } from '@/composables/graph/useSelectionMenuOptions'
|
||||
|
||||
const subgraphMocks = vi.hoisted(() => ({
|
||||
convertToSubgraph: vi.fn(),
|
||||
unpackSubgraph: vi.fn(),
|
||||
addSubgraphToLibrary: vi.fn(),
|
||||
createI18nMock: vi.fn(() => ({
|
||||
global: {
|
||||
t: vi.fn(),
|
||||
te: vi.fn(),
|
||||
d: vi.fn()
|
||||
}
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key
|
||||
}),
|
||||
createI18n: subgraphMocks.createI18nMock
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/graph/useSelectionOperations', () => ({
|
||||
useSelectionOperations: () => ({
|
||||
copySelection: vi.fn(),
|
||||
duplicateSelection: vi.fn(),
|
||||
deleteSelection: vi.fn(),
|
||||
renameSelection: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/graph/useNodeArrangement', () => ({
|
||||
useNodeArrangement: () => ({
|
||||
alignOptions: [{ localizedName: 'align-left', icon: 'align-left' }],
|
||||
distributeOptions: [{ localizedName: 'distribute', icon: 'distribute' }],
|
||||
applyAlign: vi.fn(),
|
||||
applyDistribute: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/graph/useSubgraphOperations', () => ({
|
||||
useSubgraphOperations: () => ({
|
||||
convertToSubgraph: subgraphMocks.convertToSubgraph,
|
||||
unpackSubgraph: subgraphMocks.unpackSubgraph,
|
||||
addSubgraphToLibrary: subgraphMocks.addSubgraphToLibrary
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/graph/useFrameNodes', () => ({
|
||||
useFrameNodes: () => ({
|
||||
frameNodes: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
describe('useSelectionMenuOptions - subgraph options', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('returns only convert option when no subgraphs are selected', () => {
|
||||
const { getSubgraphOptions } = useSelectionMenuOptions()
|
||||
const options = getSubgraphOptions({
|
||||
hasSubgraphs: false,
|
||||
hasMultipleSelection: true
|
||||
})
|
||||
|
||||
expect(options).toHaveLength(1)
|
||||
expect(options[0]?.label).toBe('contextMenu.Convert to Subgraph')
|
||||
expect(options[0]?.action).toBe(subgraphMocks.convertToSubgraph)
|
||||
})
|
||||
|
||||
it('includes convert, add to library, and unpack when subgraphs are selected', () => {
|
||||
const { getSubgraphOptions } = useSelectionMenuOptions()
|
||||
const options = getSubgraphOptions({
|
||||
hasSubgraphs: true,
|
||||
hasMultipleSelection: true
|
||||
})
|
||||
const labels = options.map((option) => option.label)
|
||||
|
||||
expect(labels).toContain('contextMenu.Convert to Subgraph')
|
||||
expect(labels).toContain('contextMenu.Add Subgraph to Library')
|
||||
expect(labels).toContain('contextMenu.Unpack Subgraph')
|
||||
|
||||
const convertOption = options.find(
|
||||
(option) => option.label === 'contextMenu.Convert to Subgraph'
|
||||
)
|
||||
expect(convertOption?.action).toBe(subgraphMocks.convertToSubgraph)
|
||||
})
|
||||
|
||||
it('hides convert option when only a single subgraph is selected', () => {
|
||||
const { getSubgraphOptions } = useSelectionMenuOptions()
|
||||
const options = getSubgraphOptions({
|
||||
hasSubgraphs: true,
|
||||
hasMultipleSelection: false
|
||||
})
|
||||
|
||||
const labels = options.map((option) => option.label)
|
||||
expect(labels).not.toContain('contextMenu.Convert to Subgraph')
|
||||
expect(labels).toEqual([
|
||||
'contextMenu.Add Subgraph to Library',
|
||||
'contextMenu.Unpack Subgraph'
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -63,9 +63,29 @@ export function useSelectionMenuOptions() {
|
||||
}
|
||||
]
|
||||
|
||||
const getSubgraphOptions = (hasSubgraphs: boolean): MenuOption[] => {
|
||||
const getSubgraphOptions = ({
|
||||
hasSubgraphs,
|
||||
hasMultipleSelection
|
||||
}: {
|
||||
hasSubgraphs: boolean
|
||||
hasMultipleSelection: boolean
|
||||
}): MenuOption[] => {
|
||||
const convertOption: MenuOption = {
|
||||
label: t('contextMenu.Convert to Subgraph'),
|
||||
icon: 'icon-[lucide--shrink]',
|
||||
action: convertToSubgraph,
|
||||
badge: BadgeVariant.NEW
|
||||
}
|
||||
|
||||
const options: MenuOption[] = []
|
||||
const showConvertOption = !hasSubgraphs || hasMultipleSelection
|
||||
|
||||
if (showConvertOption) {
|
||||
options.push(convertOption)
|
||||
}
|
||||
|
||||
if (hasSubgraphs) {
|
||||
return [
|
||||
options.push(
|
||||
{
|
||||
label: t('contextMenu.Add Subgraph to Library'),
|
||||
icon: 'icon-[lucide--folder-plus]',
|
||||
@@ -76,17 +96,10 @@ export function useSelectionMenuOptions() {
|
||||
icon: 'icon-[lucide--expand]',
|
||||
action: unpackSubgraph
|
||||
}
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
{
|
||||
label: t('contextMenu.Convert to Subgraph'),
|
||||
icon: 'icon-[lucide--shrink]',
|
||||
action: convertToSubgraph,
|
||||
badge: BadgeVariant.NEW
|
||||
}
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
const getMultipleNodesOptions = (): MenuOption[] => {
|
||||
|
||||
@@ -389,6 +389,13 @@ export const SERVER_CONFIG_ITEMS: ServerConfig<any>[] = [
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'enable-manager-legacy-ui',
|
||||
name: 'Use legacy Manager UI',
|
||||
tooltip: 'Uses the legacy ComfyUI-Manager UI instead of the new UI.',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'disable-all-custom-nodes',
|
||||
name: 'Disable loading all custom nodes.',
|
||||
|
||||
@@ -8564,9 +8564,11 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
node,
|
||||
newPos: this.calculateNewPosition(node, deltaX, deltaY)
|
||||
})
|
||||
} else {
|
||||
// Non-node children (nested groups, reroutes)
|
||||
child.move(deltaX, deltaY)
|
||||
} else if (!(child instanceof LGraphGroup)) {
|
||||
// Non-node, non-group children (reroutes, etc.)
|
||||
// Skip groups here - they're already in allItems and will be
|
||||
// processed in the main loop of moveChildNodesInGroupVueMode
|
||||
child.move(deltaX, deltaY, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -807,6 +807,7 @@
|
||||
"dark": "Dark",
|
||||
"light": "Light",
|
||||
"manageExtensions": "Manage Extensions",
|
||||
"customNodesManager": "Custom Nodes Manager",
|
||||
"settings": "Settings",
|
||||
"help": "Help",
|
||||
"queue": "Queue Panel"
|
||||
@@ -1333,6 +1334,10 @@
|
||||
"disable-metadata": {
|
||||
"name": "Disable saving prompt metadata in files."
|
||||
},
|
||||
"enable-manager-legacy-ui": {
|
||||
"name": "Use legacy Manager UI",
|
||||
"tooltip": "Uses the legacy ComfyUI-Manager UI instead of the new UI."
|
||||
},
|
||||
"disable-all-custom-nodes": {
|
||||
"name": "Disable loading all custom nodes."
|
||||
},
|
||||
@@ -2444,4 +2449,4 @@
|
||||
"recentReleases": "Recent releases",
|
||||
"helpCenterMenu": "Help Center Menu"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
variant="gray"
|
||||
:label="formattedDuration"
|
||||
/>
|
||||
<SquareChip v-if="fileFormat" variant="gray" :label="fileFormat" />
|
||||
</div>
|
||||
|
||||
<!-- Media actions - show on hover or when playing -->
|
||||
@@ -266,12 +265,6 @@ const formattedDuration = computed(() => {
|
||||
return formatDuration(Number(duration))
|
||||
})
|
||||
|
||||
const fileFormat = computed(() => {
|
||||
if (!asset?.name) return ''
|
||||
const parts = asset.name.split('.')
|
||||
return parts.length > 1 ? parts[parts.length - 1].toUpperCase() : ''
|
||||
})
|
||||
|
||||
const durationChipClasses = computed(() => {
|
||||
if (fileKind.value === 'audio') {
|
||||
return '-translate-y-11'
|
||||
@@ -289,7 +282,7 @@ const showStaticChips = computed(
|
||||
!!asset &&
|
||||
!isHovered.value &&
|
||||
!isVideoPlaying.value &&
|
||||
(formattedDuration.value || fileFormat.value)
|
||||
formattedDuration.value
|
||||
)
|
||||
|
||||
// Show action overlay when hovered OR playing
|
||||
|
||||
@@ -297,19 +297,26 @@ const handleContextMenu = (event: MouseEvent) => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Set initial DOM size from layout store, but respect intrinsic content minimum
|
||||
if (size.value && nodeContainerRef.value) {
|
||||
nodeContainerRef.value.style.setProperty(
|
||||
'--node-width',
|
||||
`${size.value.width}px`
|
||||
)
|
||||
nodeContainerRef.value.style.setProperty(
|
||||
'--node-height',
|
||||
`${size.value.height}px`
|
||||
)
|
||||
}
|
||||
initSizeStyles()
|
||||
})
|
||||
|
||||
/**
|
||||
* Set initial DOM size from layout store, but respect intrinsic content minimum.
|
||||
* Important: nodes can mount in a collapsed state, and the collapse watcher won't
|
||||
* run initially. Match the collapsed runtime behavior by writing to the correct
|
||||
* CSS variables on mount.
|
||||
*/
|
||||
function initSizeStyles() {
|
||||
const el = nodeContainerRef.value
|
||||
const { width, height } = size.value
|
||||
if (!el) return
|
||||
|
||||
const suffix = isCollapsed.value ? '-x' : ''
|
||||
|
||||
el.style.setProperty(`--node-width${suffix}`, `${width}px`)
|
||||
el.style.setProperty(`--node-height${suffix}`, `${height}px`)
|
||||
}
|
||||
|
||||
const baseResizeHandleClasses =
|
||||
'absolute h-3 w-3 opacity-0 pointer-events-auto focus-visible:outline focus-visible:outline-2 focus-visible:outline-white/40'
|
||||
|
||||
@@ -327,6 +334,7 @@ const { startResize } = useNodeResize((result, element) => {
|
||||
})
|
||||
|
||||
const handleResizePointerDown = (event: PointerEvent) => {
|
||||
if (event.button !== 0) return
|
||||
if (nodeData.flags?.pinned) return
|
||||
startResize(event)
|
||||
}
|
||||
|
||||
@@ -57,7 +57,10 @@ function useNodeDragIndividual() {
|
||||
const selectedNodes = toValue(selectedNodeIds)
|
||||
|
||||
// capture the starting positions of all other selected nodes
|
||||
if (selectedNodes?.has(nodeId) && selectedNodes.size > 1) {
|
||||
// Only move other selected items if the dragged node is part of the selection
|
||||
const isDraggedNodeInSelection = selectedNodes?.has(nodeId)
|
||||
|
||||
if (isDraggedNodeInSelection && selectedNodes.size > 1) {
|
||||
otherSelectedNodesStartPositions = new Map()
|
||||
|
||||
for (const id of selectedNodes) {
|
||||
@@ -73,9 +76,15 @@ function useNodeDragIndividual() {
|
||||
otherSelectedNodesStartPositions = null
|
||||
}
|
||||
|
||||
// Capture selected groups (filter from selectedItems which only contains selected items)
|
||||
selectedGroups = toValue(selectedItems).filter(isLGraphGroup)
|
||||
lastCanvasDelta = { x: 0, y: 0 }
|
||||
// Capture selected groups only if the dragged node is part of the selection
|
||||
// This prevents groups from moving when dragging an unrelated node
|
||||
if (isDraggedNodeInSelection) {
|
||||
selectedGroups = toValue(selectedItems).filter(isLGraphGroup)
|
||||
lastCanvasDelta = { x: 0, y: 0 }
|
||||
} else {
|
||||
selectedGroups = null
|
||||
lastCanvasDelta = null
|
||||
}
|
||||
|
||||
mutations.setSource(LayoutSource.Vue)
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ vi.mock('@/composables/useErrorHandling', () => ({
|
||||
vi.mock('@/renderer/extensions/vueNodes/layout/useNodeLayout', () => ({
|
||||
useNodeLayout: () => ({
|
||||
position: { x: 100, y: 50 },
|
||||
size: { width: 200, height: 100 },
|
||||
size: computed(() => ({ width: 200, height: 100 })),
|
||||
zIndex: 0,
|
||||
startDrag: vi.fn(),
|
||||
handleDrag: vi.fn(),
|
||||
@@ -201,4 +201,32 @@ describe('LGraphNode', () => {
|
||||
|
||||
expect(wrapper.classes()).toContain('outline-node-stroke-executing')
|
||||
})
|
||||
|
||||
it('should initialize height CSS vars for collapsed nodes', () => {
|
||||
const wrapper = mountLGraphNode({
|
||||
nodeData: {
|
||||
...mockNodeData,
|
||||
flags: { collapsed: true }
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.element.style.getPropertyValue('--node-height')).toBe('')
|
||||
expect(wrapper.element.style.getPropertyValue('--node-height-x')).toBe(
|
||||
'100px'
|
||||
)
|
||||
})
|
||||
|
||||
it('should initialize height CSS vars for expanded nodes', () => {
|
||||
const wrapper = mountLGraphNode({
|
||||
nodeData: {
|
||||
...mockNodeData,
|
||||
flags: { collapsed: false }
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.element.style.getPropertyValue('--node-height')).toBe(
|
||||
'100px'
|
||||
)
|
||||
expect(wrapper.element.style.getPropertyValue('--node-height-x')).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user