From 8df5cdcca899ded68d277227fb0f6faf30fcdca5 Mon Sep 17 00:00:00 2001 From: jaeone94 <89377375+jaeone94@users.noreply.github.com> Date: Wed, 20 May 2026 20:20:32 +0900 Subject: [PATCH] [backport cloud/1.43] fix: avoid false missing media after shared asset import (#12363) --- .../useSharedWorkflowUrlLoader.test.ts | 46 +++++++++++++- .../composables/useSharedWorkflowUrlLoader.ts | 61 ++++++++++--------- .../services/workflowShareService.test.ts | 10 +++ .../sharing/services/workflowShareService.ts | 3 + 4 files changed, 88 insertions(+), 32 deletions(-) diff --git a/src/platform/workflow/sharing/composables/useSharedWorkflowUrlLoader.test.ts b/src/platform/workflow/sharing/composables/useSharedWorkflowUrlLoader.test.ts index ebaff54b6c..7b0bc6ccd7 100644 --- a/src/platform/workflow/sharing/composables/useSharedWorkflowUrlLoader.test.ts +++ b/src/platform/workflow/sharing/composables/useSharedWorkflowUrlLoader.test.ts @@ -222,7 +222,7 @@ describe('useSharedWorkflowUrlLoader', () => { expect(mockHideTemplateSelector).not.toHaveBeenCalled() }) - it('calls import when non-owned assets exist and user confirms', async () => { + it('imports non-owned assets before loading graph when user confirms', async () => { mockQueryParams = { share: 'share-id-1' } const payload = makePayload({ assets: [ @@ -242,9 +242,13 @@ describe('useSharedWorkflowUrlLoader', () => { }) const { loadSharedWorkflowFromUrl } = useSharedWorkflowUrlLoader() - await loadSharedWorkflowFromUrl() + const loaded = await loadSharedWorkflowFromUrl() + expect(loaded).toBe('loaded') expect(mockImportPublishedAssets).toHaveBeenCalledWith(['a1']) + expect(mockImportPublishedAssets.mock.invocationCallOrder[0]).toBeLessThan( + mockLoadGraphData.mock.invocationCallOrder[0] + ) }) it('does not call import when user chooses open-only', async () => { @@ -309,6 +313,13 @@ describe('useSharedWorkflowUrlLoader', () => { const loaded = await loadSharedWorkflowFromUrl() expect(loaded).toBe('loaded-without-assets') + expect(mockLoadGraphData).toHaveBeenCalledWith( + { nodes: [] }, + true, + true, + 'Test Workflow', + { openSource: 'shared_url' } + ) expect(mockToastAdd).toHaveBeenCalledWith( expect.objectContaining({ severity: 'error', @@ -317,6 +328,37 @@ describe('useSharedWorkflowUrlLoader', () => { ) }) + it('clears share intent when graph load fails after importing assets', async () => { + mockQueryParams = { share: 'share-id-1', tab: 'assets' } + const payload = makePayload({ + assets: [ + { + id: 'a1', + name: 'img.png', + preview_url: '', + storage_url: '', + model: false, + public: false, + in_library: false + } + ] + }) + mockShowLayoutDialog.mockImplementation(() => { + resolveDialogWithConfirm(payload) + }) + mockLoadGraphData.mockRejectedValue(new Error('Graph load failed')) + + const { loadSharedWorkflowFromUrl } = useSharedWorkflowUrlLoader() + const loaded = await loadSharedWorkflowFromUrl() + + expect(loaded).toBe('failed') + expect(mockImportPublishedAssets).toHaveBeenCalledWith(['a1']) + expect(mockRouterReplace).toHaveBeenCalledWith({ query: { tab: 'assets' } }) + expect(preservedQueryMocks.clearPreservedQuery).toHaveBeenCalledWith( + 'share' + ) + }) + it('filters out in_library assets before importing', async () => { mockQueryParams = { share: 'share-id-1' } const payload = makePayload({ diff --git a/src/platform/workflow/sharing/composables/useSharedWorkflowUrlLoader.ts b/src/platform/workflow/sharing/composables/useSharedWorkflowUrlLoader.ts index f840cf4a9b..782ee24262 100644 --- a/src/platform/workflow/sharing/composables/useSharedWorkflowUrlLoader.ts +++ b/src/platform/workflow/sharing/composables/useSharedWorkflowUrlLoader.ts @@ -63,6 +63,11 @@ export function useSharedWorkflowUrlLoader() { void router.replace({ query: newQuery }) } + function clearShareIntent() { + cleanupUrlParams() + clearPreservedQuery(SHARE_NAMESPACE) + } + function showOpenSharedWorkflowDialog( shareId: string ): Promise { @@ -108,8 +113,7 @@ export function useSharedWorkflowUrlLoader() { } if (typeof shareParam !== 'string') { - cleanupUrlParams() - clearPreservedQuery(SHARE_NAMESPACE) + clearShareIntent() return 'not-present' } @@ -122,16 +126,14 @@ export function useSharedWorkflowUrlLoader() { summary: t('g.error'), detail: t('shareWorkflow.loadFailed') }) - cleanupUrlParams() - clearPreservedQuery(SHARE_NAMESPACE) + clearShareIntent() return 'failed' } const result = await showOpenSharedWorkflowDialog(shareParam) if (result.action === 'cancel') { - cleanupUrlParams() - clearPreservedQuery(SHARE_NAMESPACE) + clearShareIntent() return 'cancelled' } @@ -140,6 +142,26 @@ export function useSharedWorkflowUrlLoader() { const { payload } = result const workflowName = payload.name || t('openSharedWorkflow.dialogTitle') const nonOwnedAssets = payload.assets.filter((a) => !a.in_library) + let importFailed = false + + if (result.action === 'copy-and-open' && nonOwnedAssets.length > 0) { + try { + await workflowShareService.importPublishedAssets( + nonOwnedAssets.map((a) => a.id) + ) + } catch (importError) { + importFailed = true + console.error( + '[useSharedWorkflowUrlLoader] Failed to import assets:', + importError + ) + toast.add({ + severity: 'error', + summary: t('g.error'), + detail: t('openSharedWorkflow.importFailed') + }) + } + } try { await app.loadGraphData(payload.workflowJson, true, true, workflowName, { @@ -155,33 +177,12 @@ export function useSharedWorkflowUrlLoader() { summary: t('g.error'), detail: t('shareWorkflow.loadFailed') }) + clearShareIntent() return 'failed' } - if (result.action === 'copy-and-open' && nonOwnedAssets.length > 0) { - try { - await workflowShareService.importPublishedAssets( - nonOwnedAssets.map((a) => a.id) - ) - } catch (importError) { - console.error( - '[useSharedWorkflowUrlLoader] Failed to import assets:', - importError - ) - toast.add({ - severity: 'error', - summary: t('g.error'), - detail: t('openSharedWorkflow.importFailed') - }) - cleanupUrlParams() - clearPreservedQuery(SHARE_NAMESPACE) - return 'loaded-without-assets' - } - } - - cleanupUrlParams() - clearPreservedQuery(SHARE_NAMESPACE) - return 'loaded' + clearShareIntent() + return importFailed ? 'loaded-without-assets' : 'loaded' } return { diff --git a/src/platform/workflow/sharing/services/workflowShareService.test.ts b/src/platform/workflow/sharing/services/workflowShareService.test.ts index 67371d78fb..e48b570004 100644 --- a/src/platform/workflow/sharing/services/workflowShareService.test.ts +++ b/src/platform/workflow/sharing/services/workflowShareService.test.ts @@ -14,6 +14,7 @@ vi.mock('@/scripts/app', () => ({ const mockGetShareableAssets = vi.fn() const mockFetchApi = vi.fn() +const mockInvalidateInputAssetsIncludingPublic = vi.hoisted(() => vi.fn()) vi.mock( '@/platform/workflow/validation/schemas/workflowSchema', @@ -32,6 +33,13 @@ vi.mock('@/scripts/api', () => ({ } })) +vi.mock('@/platform/assets/services/assetService', () => ({ + assetService: { + invalidateInputAssetsIncludingPublic: + mockInvalidateInputAssetsIncludingPublic + } +})) + describe(useWorkflowShareService, () => { const mockShareableAssets: AssetInfo[] = [ { @@ -345,6 +353,7 @@ describe(useWorkflowShareService, () => { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ published_asset_ids: ['pa-1', 'pa-2'] }) }) + expect(mockInvalidateInputAssetsIncludingPublic).toHaveBeenCalledTimes(1) }) it('throws when import request fails', async () => { @@ -355,6 +364,7 @@ describe(useWorkflowShareService, () => { await expect(service.importPublishedAssets(['bad-id'])).rejects.toThrow( 'Failed to import assets: 400' ) + expect(mockInvalidateInputAssetsIncludingPublic).not.toHaveBeenCalled() }) it('throws when shared workflow payload is invalid', async () => { diff --git a/src/platform/workflow/sharing/services/workflowShareService.ts b/src/platform/workflow/sharing/services/workflowShareService.ts index 284a860e19..ad789975b8 100644 --- a/src/platform/workflow/sharing/services/workflowShareService.ts +++ b/src/platform/workflow/sharing/services/workflowShareService.ts @@ -4,6 +4,7 @@ import type { WorkflowPublishResult, WorkflowPublishStatus } from '@/platform/workflow/sharing/types/shareTypes' +import { assetService } from '@/platform/assets/services/assetService' import type { ThumbnailType } from '@/platform/workflow/sharing/types/comfyHubTypes' import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema' import { validateComfyWorkflow } from '@/platform/workflow/validation/schemas/workflowSchema' @@ -265,6 +266,8 @@ export function useWorkflowShareService() { if (!response.ok) { throw new Error(`Failed to import assets: ${response.status}`) } + + assetService.invalidateInputAssetsIncludingPublic() } return {