Merge branch 'main' into drjkl/others-call-it-a-garage-sale
@@ -79,48 +79,15 @@ export class SubgraphSlotReference {
|
||||
|
||||
const node =
|
||||
type === 'input' ? currentGraph.inputNode : currentGraph.outputNode
|
||||
const slots =
|
||||
type === 'input' ? currentGraph.inputs : currentGraph.outputs
|
||||
|
||||
if (!node) {
|
||||
throw new Error(`No ${type} node found in subgraph`)
|
||||
}
|
||||
|
||||
// Calculate position for next available slot
|
||||
// const nextSlotIndex = slots?.length || 0
|
||||
// const slotHeight = 20
|
||||
// const slotY = node.pos[1] + 30 + nextSlotIndex * slotHeight
|
||||
|
||||
// Find last slot position
|
||||
const lastSlot = slots.at(-1)
|
||||
let slotX: number
|
||||
let slotY: number
|
||||
|
||||
if (lastSlot) {
|
||||
// If there are existing slots, position the new one below the last one
|
||||
const gapHeight = 20
|
||||
slotX = lastSlot.pos[0]
|
||||
slotY = lastSlot.pos[1] + gapHeight
|
||||
} else {
|
||||
// No existing slots - use slotAnchorX if available, otherwise calculate from node position
|
||||
if (currentGraph.slotAnchorX !== undefined) {
|
||||
// The actual slot X position seems to be slotAnchorX - 10
|
||||
slotX = currentGraph.slotAnchorX - 10
|
||||
} else {
|
||||
// Fallback: calculate from node edge
|
||||
slotX =
|
||||
type === 'input'
|
||||
? node.pos[0] + node.size[0] - 10 // Right edge for input node
|
||||
: node.pos[0] + 10 // Left edge for output node
|
||||
}
|
||||
// For Y position when no slots exist, use middle of node
|
||||
slotY = node.pos[1] + node.size[1] / 2
|
||||
}
|
||||
|
||||
// Convert from offset to canvas coordinates
|
||||
const canvasPos = window['app'].canvas.ds.convertOffsetToCanvas([
|
||||
slotX,
|
||||
slotY
|
||||
node.emptySlot.pos[0],
|
||||
node.emptySlot.pos[1]
|
||||
])
|
||||
return canvasPos
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 106 KiB |
@@ -398,6 +398,9 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
vueNodeData.set(id, extractVueNodeData(node))
|
||||
|
||||
const initializeVueNodeLayout = () => {
|
||||
// Check if the node was removed mid-sequence
|
||||
if (!nodeRefs.has(id)) return
|
||||
|
||||
// Extract actual positions after configure() has potentially updated them
|
||||
const nodePosition = { x: node.pos[0], y: node.pos[1] }
|
||||
const nodeSize = { width: node.size[0], height: node.size[1] }
|
||||
@@ -427,7 +430,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
} else {
|
||||
// Not during workflow loading - initialize layout immediately
|
||||
// This handles individual node additions during normal operation
|
||||
initializeVueNodeLayout()
|
||||
requestAnimationFrame(initializeVueNodeLayout)
|
||||
}
|
||||
|
||||
// Call original callback if provided
|
||||
|
||||
223
src/platform/assets/composables/useMediaAssetActions.test.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
|
||||
// Use vi.hoisted to create a mutable reference for isCloud
|
||||
const mockIsCloud = vi.hoisted(() => ({ value: false }))
|
||||
|
||||
// Track the filename passed to createAnnotatedPath
|
||||
const capturedFilenames = vi.hoisted(() => ({ values: [] as string[] }))
|
||||
|
||||
vi.mock('@/platform/distribution/types', () => ({
|
||||
get isCloud() {
|
||||
return mockIsCloud.value
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('primevue/usetoast', () => ({
|
||||
useToast: () => ({
|
||||
add: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key
|
||||
}),
|
||||
createI18n: () => ({
|
||||
global: {
|
||||
t: (key: string) => key
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/dialogStore', () => ({
|
||||
useDialogStore: () => ({
|
||||
showDialog: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useCopyToClipboard', () => ({
|
||||
useCopyToClipboard: () => ({
|
||||
copyToClipboard: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/workflow/core/services/workflowActionsService', () => ({
|
||||
useWorkflowActionsService: () => ({
|
||||
openWorkflowAction: vi.fn(),
|
||||
exportWorkflowAction: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/services/litegraphService', () => ({
|
||||
useLitegraphService: () => ({
|
||||
addNodeOnGraph: vi.fn().mockReturnValue({
|
||||
widgets: [{ name: 'image', value: '', callback: vi.fn() }],
|
||||
graph: { setDirtyCanvas: vi.fn() }
|
||||
} as unknown as LGraphNode),
|
||||
getCanvasCenter: vi.fn().mockReturnValue([100, 100])
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/nodeDefStore', () => ({
|
||||
useNodeDefStore: () => ({
|
||||
nodeDefsByName: {
|
||||
LoadImage: {
|
||||
name: 'LoadImage',
|
||||
display_name: 'Load Image'
|
||||
}
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/createAnnotatedPath', () => ({
|
||||
createAnnotatedPath: vi.fn((item: { filename: string }) => {
|
||||
capturedFilenames.values.push(item.filename)
|
||||
return item.filename
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/loaderNodeUtil', () => ({
|
||||
detectNodeTypeFromFilename: vi.fn().mockReturnValue({
|
||||
nodeType: 'LoadImage',
|
||||
widgetName: 'image'
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/typeGuardUtil', () => ({
|
||||
isResultItemType: vi.fn().mockReturnValue(true)
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/assets/utils/assetTypeUtil', () => ({
|
||||
getAssetType: vi.fn().mockReturnValue('input')
|
||||
}))
|
||||
|
||||
vi.mock('../schemas/assetMetadataSchema', () => ({
|
||||
getOutputAssetMetadata: vi.fn().mockReturnValue(null)
|
||||
}))
|
||||
|
||||
function createMockAsset(overrides: Partial<AssetItem> = {}): AssetItem {
|
||||
return {
|
||||
id: 'test-asset-id',
|
||||
name: 'original-name.jpeg',
|
||||
size: 1024,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
tags: ['input'],
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('useMediaAssetActions', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
capturedFilenames.values = []
|
||||
mockIsCloud.value = false
|
||||
})
|
||||
|
||||
describe('addWorkflow', () => {
|
||||
describe('OSS mode (isCloud = false)', () => {
|
||||
beforeEach(() => {
|
||||
mockIsCloud.value = false
|
||||
})
|
||||
|
||||
it('should use asset.name as filename', async () => {
|
||||
const { useMediaAssetActions } = await import('./useMediaAssetActions')
|
||||
const actions = useMediaAssetActions()
|
||||
|
||||
const asset = createMockAsset({
|
||||
name: 'my-image.jpeg',
|
||||
asset_hash: 'hash123.jpeg'
|
||||
})
|
||||
|
||||
await actions.addWorkflow(asset)
|
||||
|
||||
expect(capturedFilenames.values).toContain('my-image.jpeg')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Cloud mode (isCloud = true)', () => {
|
||||
beforeEach(() => {
|
||||
mockIsCloud.value = true
|
||||
})
|
||||
|
||||
it('should use asset_hash as filename when available', async () => {
|
||||
const { useMediaAssetActions } = await import('./useMediaAssetActions')
|
||||
const actions = useMediaAssetActions()
|
||||
|
||||
const asset = createMockAsset({
|
||||
name: 'original.jpeg',
|
||||
asset_hash: 'abc123hash.jpeg'
|
||||
})
|
||||
|
||||
await actions.addWorkflow(asset)
|
||||
|
||||
expect(capturedFilenames.values).toContain('abc123hash.jpeg')
|
||||
})
|
||||
|
||||
it('should fall back to asset.name when asset_hash is not available', async () => {
|
||||
const { useMediaAssetActions } = await import('./useMediaAssetActions')
|
||||
const actions = useMediaAssetActions()
|
||||
|
||||
const asset = createMockAsset({
|
||||
name: 'fallback-name.jpeg',
|
||||
asset_hash: undefined
|
||||
})
|
||||
|
||||
await actions.addWorkflow(asset)
|
||||
|
||||
expect(capturedFilenames.values).toContain('fallback-name.jpeg')
|
||||
})
|
||||
|
||||
it('should fall back to asset.name when asset_hash is null', async () => {
|
||||
const { useMediaAssetActions } = await import('./useMediaAssetActions')
|
||||
const actions = useMediaAssetActions()
|
||||
|
||||
const asset = createMockAsset({
|
||||
name: 'fallback-null.jpeg',
|
||||
asset_hash: null
|
||||
})
|
||||
|
||||
await actions.addWorkflow(asset)
|
||||
|
||||
expect(capturedFilenames.values).toContain('fallback-null.jpeg')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('addMultipleToWorkflow', () => {
|
||||
describe('Cloud mode (isCloud = true)', () => {
|
||||
beforeEach(() => {
|
||||
mockIsCloud.value = true
|
||||
})
|
||||
|
||||
it('should use asset_hash for each asset', async () => {
|
||||
const { useMediaAssetActions } = await import('./useMediaAssetActions')
|
||||
const actions = useMediaAssetActions()
|
||||
|
||||
const assets = [
|
||||
createMockAsset({
|
||||
id: '1',
|
||||
name: 'file1.jpeg',
|
||||
asset_hash: 'hash1.jpeg'
|
||||
}),
|
||||
createMockAsset({
|
||||
id: '2',
|
||||
name: 'file2.jpeg',
|
||||
asset_hash: 'hash2.jpeg'
|
||||
})
|
||||
]
|
||||
|
||||
await actions.addMultipleToWorkflow(assets)
|
||||
|
||||
expect(capturedFilenames.values).toContain('hash1.jpeg')
|
||||
expect(capturedFilenames.values).toContain('hash2.jpeg')
|
||||
expect(capturedFilenames.values).not.toContain('file1.jpeg')
|
||||
expect(capturedFilenames.values).not.toContain('file2.jpeg')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -272,10 +272,17 @@ export function useMediaAssetActions() {
|
||||
const metadata = getOutputAssetMetadata(targetAsset.user_metadata)
|
||||
const assetType = getAssetType(targetAsset, 'input')
|
||||
|
||||
// In Cloud mode, use asset_hash (the actual stored filename)
|
||||
// In OSS mode, use the original name
|
||||
const filename =
|
||||
isCloud && targetAsset.asset_hash
|
||||
? targetAsset.asset_hash
|
||||
: targetAsset.name
|
||||
|
||||
// Create annotated path for the asset
|
||||
const annotated = createAnnotatedPath(
|
||||
{
|
||||
filename: targetAsset.name,
|
||||
filename,
|
||||
subfolder: metadata?.subfolder || '',
|
||||
type: isResultItemType(assetType) ? assetType : undefined
|
||||
},
|
||||
@@ -407,9 +414,14 @@ export function useMediaAssetActions() {
|
||||
const metadata = getOutputAssetMetadata(asset.user_metadata)
|
||||
const assetType = getAssetType(asset, 'input')
|
||||
|
||||
// In Cloud mode, use asset_hash (the actual stored filename)
|
||||
// In OSS mode, use the original name
|
||||
const filename =
|
||||
isCloud && asset.asset_hash ? asset.asset_hash : asset.name
|
||||
|
||||
const annotated = createAnnotatedPath(
|
||||
{
|
||||
filename: asset.name,
|
||||
filename,
|
||||
subfolder: metadata?.subfolder || '',
|
||||
type: isResultItemType(assetType) ? assetType : undefined
|
||||
},
|
||||
|
||||