1
+ import { useRafFn } from '@vueuse/core'
2
+ import { nextTick } from 'vue'
3
+
1
4
import AssetBrowserModal from '@/platform/assets/components/AssetBrowserModal.vue'
2
5
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
3
6
import { assetService } from '@/platform/assets/services/assetService'
4
- import { useDialogStore } from '@/stores/dialogStore'
7
+ import { type DialogComponentProps , useDialogStore } from '@/stores/dialogStore'
5
8
6
9
interface AssetBrowserDialogProps {
7
10
/** ComfyUI node type for context (e.g., 'CheckpointLoaderSimple') */
@@ -22,6 +25,42 @@ export const useAssetBrowserDialog = () => {
22
25
const dialogKey = 'global-asset-browser'
23
26
let onHideComplete : ( ( ) => void ) | null = null
24
27
28
+ /**
29
+ * Why we need Promise coordination with PrimeVue's onAfterHide:
30
+ *
31
+ * PrimeVue Dialog uses Vue's <transition> with specific animation lifecycle hooks.
32
+ * Source: https://github.com/primefaces/primevue/blob/72590856123052fa798b185dc2c88928a325258f/packages/primevue/src/dialog/Dialog.vue#L3-L4
33
+ *
34
+ * The animation flow is:
35
+ * 1. visible=false triggers @leave event
36
+ * https://github.com/primefaces/primevue/blob/72590856123052fa798b185dc2c88928a325258f/packages/primevue/src/dialog/Dialog.vue#L162-L170
37
+ * 2. CSS animation runs (~300ms)
38
+ * 3. @after-leave fires, emitting 'after-hide'
39
+ * https://github.com/primefaces/primevue/blob/72590856123052fa798b185dc2c88928a325258f/packages/primevue/src/dialog/Dialog.vue#L171-L180
40
+ *
41
+ * Why Vue/VueUse lifecycle hooks cannot manage this:
42
+ *
43
+ * - Vue's onBeforeUnmount/onUnmounted track COMPONENT lifecycle (mounting/unmounting)
44
+ * Source: https://github.com/vuejs/core/blob/928af5fe2f5f366b5c28b8549c3728735c8d8318/packages/runtime-core/src/apiLifecycle.ts#L91-94
45
+ * These only fire when a component is being destroyed, not during visibility changes
46
+ *
47
+ * - VueUse's tryOnUnmounted only fires when component unmounts
48
+ * Source: https://github.com/vueuse/vueuse/blob/cf905ccfb5dd7a6a3e65aa087a034b5157c9f9fb/packages/shared/tryOnUnmounted/index.ts
49
+ * It checks getLifeCycleTarget() and registers onUnmounted, but component remains mounted during animation
50
+ *
51
+ * - The Dialog component remains mounted during animation - only visibility changes
52
+ * - CSS transitions are managed by the browser, not Vue's reactivity system
53
+ * - Only PrimeVue's onAfterHide callback knows when the CSS animation completes
54
+ *
55
+ * Without Promise coordination, calling hide() synchronously after widget update
56
+ * causes both operations in the same event tick, preventing proper animation.
57
+ *
58
+ * Why nextTick() doesn't solve this:
59
+ * - nextTick() only defers to the next microtask, not the next animation frame
60
+ * - PrimeVue's animation timing depends on CSS transition duration (~300ms)
61
+ * - We need to wait for the actual animation completion, not just DOM updates
62
+ * - Only onAfterHide callback provides the correct timing signal
63
+ */
25
64
function hide ( ) : Promise < void > {
26
65
return new Promise ( ( resolve ) => {
27
66
onHideComplete = resolve
@@ -30,15 +69,25 @@ export const useAssetBrowserDialog = () => {
30
69
}
31
70
32
71
async function show ( props : AssetBrowserDialogProps ) {
33
- const handleAssetSelected = async ( assetPath : string ) => {
34
- // Update the widget value immediately - don't wait for animation
35
- props . onAssetSelected ?.( assetPath )
36
- // Then trigger the hide animation
37
- await hide ( )
38
- }
72
+ const handleAssetSelected = async ( filename : string ) => {
73
+ // This function is called by selectAssetWithCallback with the validated filename
74
+ props . onAssetSelected ?.( filename )
39
75
40
- // Default dialog configuration for AssetBrowserModal
41
- const dialogComponentProps = {
76
+ // Wait for Vue to flush all synchronous updates (widget value, DOM updates)
77
+ await nextTick ( )
78
+
79
+ // Wait for next animation frame when PrimeVue sets up animation state
80
+ return new Promise < void > ( ( resolve ) => {
81
+ const { pause } = useRafFn (
82
+ ( ) => {
83
+ pause ( )
84
+ void hide ( ) . then ( resolve )
85
+ } ,
86
+ { immediate : true }
87
+ )
88
+ } )
89
+ }
90
+ const dialogComponentProps : DialogComponentProps = {
42
91
headless : true ,
43
92
modal : true ,
44
93
closable : true ,
@@ -51,7 +100,7 @@ export const useAssetBrowserDialog = () => {
51
100
} ,
52
101
pt : {
53
102
root : {
54
- class : 'rounded-2xl overflow-hidden'
103
+ class : 'rounded-2xl overflow-hidden asset-browser-dialog '
55
104
} ,
56
105
header : {
57
106
class : 'p-0 hidden'
@@ -62,17 +111,16 @@ export const useAssetBrowserDialog = () => {
62
111
}
63
112
}
64
113
65
- // Fetch assets for the specific node type, fallback to empty array on error
66
- let assets : AssetItem [ ] = [ ]
67
- try {
68
- assets = await assetService . getAssetsForNodeType ( props . nodeType )
69
- } catch ( error ) {
70
- console . error (
71
- 'Failed to fetch assets for node type:' ,
72
- props . nodeType ,
73
- error
74
- )
75
- }
114
+ const assets : AssetItem [ ] = await assetService
115
+ . getAssetsForNodeType ( props . nodeType )
116
+ . catch ( ( error ) => {
117
+ console . error (
118
+ 'Failed to fetch assets for node type:' ,
119
+ props . nodeType ,
120
+ error
121
+ )
122
+ return [ ]
123
+ } )
76
124
77
125
dialogStore . showDialog ( {
78
126
key : dialogKey ,
0 commit comments