mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-01 01:59:08 +00:00
Compare commits
1 Commits
fix/codera
...
fix/codera
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69bc33eef5 |
@@ -121,6 +121,68 @@ describe('resolveSubgraphInputLink', () => {
|
||||
expect(result).toBe('seed_input')
|
||||
})
|
||||
|
||||
test('skips broken links where getLink returns undefined', () => {
|
||||
const { subgraph, subgraphNode } = createSubgraphSetup('prompt')
|
||||
addLinkedInteriorInput(subgraph, 'prompt', 'valid_input', 'valid')
|
||||
const broken = addLinkedInteriorInput(
|
||||
subgraph,
|
||||
'prompt',
|
||||
'broken_input',
|
||||
'broken'
|
||||
)
|
||||
|
||||
const originalGetLink = subgraph.getLink.bind(subgraph)
|
||||
vi.spyOn(subgraph, 'getLink').mockImplementation((linkId) => {
|
||||
if (typeof linkId !== 'number') return originalGetLink(linkId)
|
||||
if (linkId === broken.linkId) return undefined
|
||||
return originalGetLink(linkId)
|
||||
})
|
||||
|
||||
const result = resolveSubgraphInputLink(
|
||||
subgraphNode,
|
||||
'prompt',
|
||||
({ targetInput }) => targetInput.name
|
||||
)
|
||||
|
||||
expect(result).toBe('valid_input')
|
||||
})
|
||||
|
||||
test('returns result from latest connection when multiple links resolve', () => {
|
||||
const { subgraph, subgraphNode } = createSubgraphSetup('prompt')
|
||||
addLinkedInteriorInput(subgraph, 'prompt', 'older_input', 'older')
|
||||
addLinkedInteriorInput(subgraph, 'prompt', 'newer_input', 'newer')
|
||||
|
||||
const result = resolveSubgraphInputLink(
|
||||
subgraphNode,
|
||||
'prompt',
|
||||
({ targetInput }) => targetInput.name
|
||||
)
|
||||
|
||||
expect(result).toBe('newer_input')
|
||||
})
|
||||
|
||||
test('falls back to earlier link when latest resolve callback returns undefined', () => {
|
||||
const { subgraph, subgraphNode } = createSubgraphSetup('prompt')
|
||||
addLinkedInteriorInput(subgraph, 'prompt', 'fallback_input', 'fallback')
|
||||
const newer = addLinkedInteriorInput(
|
||||
subgraph,
|
||||
'prompt',
|
||||
'skipped_input',
|
||||
'skipped'
|
||||
)
|
||||
|
||||
const result = resolveSubgraphInputLink(
|
||||
subgraphNode,
|
||||
'prompt',
|
||||
({ targetInput }) => {
|
||||
if (targetInput.link === newer.linkId) return undefined
|
||||
return targetInput.name
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).toBe('fallback_input')
|
||||
})
|
||||
|
||||
test('caches getTargetWidget result within the same callback evaluation', () => {
|
||||
const { subgraph, subgraphNode } = createSubgraphSetup('model')
|
||||
const linked = addLinkedInteriorInput(
|
||||
|
||||
@@ -99,9 +99,7 @@ vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
vi.mock('@/renderer/core/thumbnail/useWorkflowThumbnail', () => ({
|
||||
useWorkflowThumbnail: () => ({
|
||||
storeThumbnail: vi.fn(),
|
||||
getThumbnail: vi.fn(),
|
||||
clearThumbnail: vi.fn(),
|
||||
moveWorkflowThumbnail: vi.fn()
|
||||
getThumbnail: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -855,13 +853,12 @@ describe('useWorkflowService', () => {
|
||||
|
||||
const existing = createSaveableWorkflow('workflows/test.app.json')
|
||||
vi.spyOn(workflowStore, 'getWorkflowByPath').mockReturnValue(existing)
|
||||
vi.spyOn(workflowStore, 'removeWorkflowEntry').mockResolvedValue()
|
||||
vi.spyOn(workflowStore, 'deleteWorkflow').mockResolvedValue()
|
||||
mockConfirm.mockResolvedValue(true)
|
||||
|
||||
await service.saveWorkflow(workflow)
|
||||
|
||||
expect(mockConfirm).toHaveBeenCalled()
|
||||
expect(workflowStore.removeWorkflowEntry).toHaveBeenCalledWith(existing)
|
||||
expect(workflowStore.renameWorkflow).toHaveBeenCalledWith(
|
||||
workflow,
|
||||
'workflows/test.app.json'
|
||||
|
||||
@@ -130,20 +130,11 @@ export const useWorkflowService = () => {
|
||||
|
||||
if (existingWorkflow && !existingWorkflow.isTemporary) {
|
||||
if ((await confirmOverwrite(newPath)) !== true) return false
|
||||
}
|
||||
|
||||
const needsOverwrite =
|
||||
!!existingWorkflow && !existingWorkflow.isTemporary && !isSelfOverwrite
|
||||
|
||||
// Close and remove the old workflow entry before saving the new content.
|
||||
// The file on disk is intentionally kept so that a save failure does not
|
||||
// cause data loss. The subsequent save with overwrite: true will
|
||||
// atomically replace it.
|
||||
if (needsOverwrite) {
|
||||
if (workflowStore.isOpen(existingWorkflow)) {
|
||||
await closeWorkflow(existingWorkflow, { warnIfUnsaved: false })
|
||||
if (!isSelfOverwrite) {
|
||||
const deleted = await deleteWorkflow(existingWorkflow, true)
|
||||
if (!deleted) return false
|
||||
}
|
||||
await workflowStore.removeWorkflowEntry(existingWorkflow)
|
||||
}
|
||||
|
||||
workflow.changeTracker?.checkState()
|
||||
@@ -152,19 +143,11 @@ export const useWorkflowService = () => {
|
||||
await saveWorkflow(workflow)
|
||||
} else if (workflow.isTemporary) {
|
||||
await renameWorkflow(workflow, newPath)
|
||||
if (needsOverwrite) {
|
||||
await workflowStore.saveWorkflow(workflow, { overwrite: true })
|
||||
} else {
|
||||
await workflowStore.saveWorkflow(workflow)
|
||||
}
|
||||
await workflowStore.saveWorkflow(workflow)
|
||||
} else {
|
||||
const tempWorkflow = workflowStore.saveAs(workflow, newPath)
|
||||
await openWorkflow(tempWorkflow)
|
||||
if (needsOverwrite) {
|
||||
await workflowStore.saveWorkflow(tempWorkflow, { overwrite: true })
|
||||
} else {
|
||||
await workflowStore.saveWorkflow(tempWorkflow)
|
||||
}
|
||||
await workflowStore.saveWorkflow(tempWorkflow)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -191,12 +174,7 @@ export const useWorkflowService = () => {
|
||||
await workflowStore.saveWorkflow(workflow)
|
||||
return
|
||||
}
|
||||
// Remove the old entry without deleting the file; the rename
|
||||
// will atomically replace it, preventing data loss on failure.
|
||||
if (workflowStore.isOpen(existing)) {
|
||||
await closeWorkflow(existing, { warnIfUnsaved: false })
|
||||
}
|
||||
await workflowStore.removeWorkflowEntry(existing)
|
||||
await deleteWorkflow(existing, true)
|
||||
}
|
||||
await renameWorkflow(workflow, expectedPath)
|
||||
toastStore.add({
|
||||
|
||||
@@ -151,16 +151,14 @@ export class ComfyWorkflow extends UserFile {
|
||||
super.unload()
|
||||
}
|
||||
|
||||
override async save({
|
||||
overwrite
|
||||
}: { force?: boolean; overwrite?: boolean } = {}) {
|
||||
override async save() {
|
||||
const { useWorkflowDraftStore } =
|
||||
await import('@/platform/workflow/persistence/stores/workflowDraftStore')
|
||||
const draftStore = useWorkflowDraftStore()
|
||||
this.content = JSON.stringify(this.activeState)
|
||||
// Force save to ensure the content is updated in remote storage incase
|
||||
// the isModified state is screwed by changeTracker.
|
||||
const ret = await super.save({ force: true, overwrite })
|
||||
const ret = await super.save({ force: true })
|
||||
this.changeTracker?.reset()
|
||||
this.isModified = false
|
||||
draftStore.removeDraft(this.path)
|
||||
|
||||
@@ -63,11 +63,7 @@ interface WorkflowStore {
|
||||
) => ComfyWorkflow
|
||||
renameWorkflow: (workflow: ComfyWorkflow, newPath: string) => Promise<void>
|
||||
deleteWorkflow: (workflow: ComfyWorkflow) => Promise<void>
|
||||
removeWorkflowEntry: (workflow: ComfyWorkflow) => Promise<void>
|
||||
saveWorkflow: (
|
||||
workflow: ComfyWorkflow,
|
||||
options?: { overwrite?: boolean }
|
||||
) => Promise<void>
|
||||
saveWorkflow: (workflow: ComfyWorkflow) => Promise<void>
|
||||
|
||||
workflows: ComfyWorkflow[]
|
||||
bookmarkedWorkflows: ComfyWorkflow[]
|
||||
@@ -543,32 +539,14 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a workflow entry from the store without deleting the file on disk.
|
||||
* Used during atomic overwrite to clear the old entry before saving the new
|
||||
* content, so that the save can use overwrite: true to replace the file.
|
||||
*/
|
||||
const removeWorkflowEntry = async (workflow: ComfyWorkflow) => {
|
||||
useWorkflowDraftStore().removeDraft(workflow.path)
|
||||
if (bookmarkStore.isBookmarked(workflow.path)) {
|
||||
await bookmarkStore.setBookmarked(workflow.path, false)
|
||||
}
|
||||
clearThumbnail(workflow.key)
|
||||
delete workflowLookup.value[workflow.path]
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a workflow.
|
||||
* @param workflow The workflow to save.
|
||||
* @param options.overwrite Force overwrite of existing file at the path.
|
||||
*/
|
||||
const saveWorkflow = async (
|
||||
workflow: ComfyWorkflow,
|
||||
options?: { overwrite?: boolean }
|
||||
) => {
|
||||
const saveWorkflow = async (workflow: ComfyWorkflow) => {
|
||||
isBusy.value = true
|
||||
try {
|
||||
await workflow.save({ overwrite: options?.overwrite })
|
||||
await workflow.save()
|
||||
// Synchronously detach and re-attach to force refresh the tree objects
|
||||
// without an async gap that would cause the tab to disappear.
|
||||
const openIndex = detachWorkflow(workflow)
|
||||
@@ -796,7 +774,6 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
createNewTemporary,
|
||||
renameWorkflow,
|
||||
deleteWorkflow,
|
||||
removeWorkflowEntry,
|
||||
saveAs,
|
||||
saveWorkflow,
|
||||
reorderWorkflows,
|
||||
|
||||
@@ -140,14 +140,11 @@ export class UserFile {
|
||||
* Saves the file to the remote storage.
|
||||
* @param force Whether to force the save even if the file is not modified.
|
||||
*/
|
||||
async save({
|
||||
force = false,
|
||||
overwrite
|
||||
}: { force?: boolean; overwrite?: boolean } = {}): Promise<UserFile> {
|
||||
async save({ force = false }: { force?: boolean } = {}): Promise<UserFile> {
|
||||
if (this.isPersisted && !this.isModified && !force) return this
|
||||
|
||||
const resp = await api.storeUserData(this.path, this.content, {
|
||||
overwrite: overwrite ?? this.isPersisted,
|
||||
overwrite: this.isPersisted,
|
||||
throwOnError: true,
|
||||
full_info: true
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user