Skip to content

Commit 2fc9598

Browse files
committed
[fix] code review feedback
1 parent 434ecfe commit 2fc9598

File tree

15 files changed

+434
-86
lines changed

15 files changed

+434
-86
lines changed

src/components/dialog/GlobalDialog.vue

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,19 @@
33
<Dialog
44
v-for="item in dialogStore.dialogStack"
55
:key="item.key"
6+
:ref="(el) => setDialogRef(item.key, el)"
67
v-model:visible="item.visible"
78
class="global-dialog"
89
v-bind="item.dialogComponentProps"
9-
:pt="item.dialogComponentProps.pt"
10+
:pt="{
11+
...item.dialogComponentProps.pt,
12+
transition: {
13+
enterFromClass: 'opacity-0 scale-75',
14+
enterActiveClass: 'transition-all duration-200 ease-out',
15+
leaveActiveClass: 'transition-all duration-200 ease-in',
16+
leaveToClass: 'opacity-0 scale-75'
17+
}
18+
}"
1019
:aria-labelledby="item.key"
1120
>
1221
<template #header>
@@ -36,10 +45,40 @@
3645

3746
<script setup lang="ts">
3847
import Dialog from 'primevue/dialog'
48+
import { onMounted, ref } from 'vue'
3949
4050
import { useDialogStore } from '@/stores/dialogStore'
4151
4252
const dialogStore = useDialogStore()
53+
54+
// Store refs to Dialog components so we can call their close() method
55+
const dialogRefs = ref<Record<string, any>>({})
56+
57+
function setDialogRef(dialogKey: string, el: any) {
58+
if (el) {
59+
dialogRefs.value[dialogKey] = el
60+
} else {
61+
delete dialogRefs.value[dialogKey]
62+
}
63+
}
64+
65+
// Expose method for dialogStore to trigger proper close
66+
function triggerDialogClose(dialogKey: string) {
67+
const dialogRef = dialogRefs.value[dialogKey]
68+
if (!dialogRef || typeof dialogRef.close !== 'function') return
69+
70+
dialogRef.close()
71+
}
72+
73+
// Register with dialogStore so it can call Dialog.close() directly
74+
onMounted(() => {
75+
dialogStore.registerGlobalDialogCloseFn(triggerDialogClose)
76+
})
77+
78+
// Make the function available to the dialog store
79+
defineExpose({
80+
triggerDialogClose
81+
})
4382
</script>
4483

4584
<style>

src/lib/litegraph/src/litegraph.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export { BaseWidget } from './widgets/BaseWidget'
140140

141141
export { LegacyWidget } from './widgets/LegacyWidget'
142142

143-
export { isComboWidget } from './widgets/widgetMap'
143+
export { isComboWidget, isAssetWidget } from './widgets/widgetMap'
144144
// Additional test-specific exports
145145
export { LGraphButton } from './LGraphButton'
146146
export { MovingOutputLink } from './canvas/MovingOutputLink'

src/lib/litegraph/src/widgets/AssetWidget.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,22 @@ export class AssetWidget
1313
this.value = widget.value?.toString() ?? ''
1414
}
1515

16+
override set value(value: IAssetWidget['value']) {
17+
const oldValue = this.value
18+
super.value = value
19+
20+
// Force canvas redraw when value changes to show update immediately
21+
if (oldValue !== value && this.node.graph?.list_of_graphcanvas) {
22+
for (const canvas of this.node.graph.list_of_graphcanvas) {
23+
canvas.setDirty(true)
24+
}
25+
}
26+
}
27+
28+
override get value(): IAssetWidget['value'] {
29+
return super.value
30+
}
31+
1632
override get _displayValue(): string {
1733
return String(this.value) //FIXME: Resolve asset name
1834
}

src/lib/litegraph/src/widgets/widgetMap.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
22
import type {
3+
IAssetWidget,
34
IBaseWidget,
45
IComboWidget,
56
IWidget,
@@ -132,4 +133,9 @@ export function isComboWidget(widget: IBaseWidget): widget is IComboWidget {
132133
return widget.type === 'combo'
133134
}
134135

136+
/** Type guard: Narrow **from {@link IBaseWidget}** to {@link IAssetWidget}. */
137+
export function isAssetWidget(widget: IBaseWidget): widget is IAssetWidget {
138+
return widget.type === 'asset'
139+
}
140+
135141
// #endregion Type Guards

src/platform/assets/components/AssetBrowserModal.vue

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
:nav-items="availableCategories"
1313
>
1414
<template #header-icon>
15-
<div :class="cn('icon-[lucide--folder]', 'size-4')" />
15+
<div class="icon-[lucide--folder] size-4" />
1616
</template>
1717
<template #header-title>{{ $t('assetBrowser.browseAssets') }}</template>
1818
</LeftSidePanel>
@@ -47,7 +47,6 @@ import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBro
4747
import { useAssetBrowser } from '@/platform/assets/composables/useAssetBrowser'
4848
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
4949
import { OnCloseKey } from '@/types/widgetTypes'
50-
import { cn } from '@/utils/tailwindUtil'
5150
5251
const props = defineProps<{
5352
nodeType?: string
@@ -63,10 +62,8 @@ const emit = defineEmits<{
6362
close: []
6463
}>()
6564
66-
// Provide the close function for BaseModalLayout to inject
67-
provide(OnCloseKey, props.onClose || (() => {}))
65+
provide(OnCloseKey, props.onClose ?? (() => {}))
6866
69-
// Use AssetBrowser composable for all business logic
7067
const {
7168
searchQuery,
7269
selectedCategory,
@@ -76,22 +73,17 @@ const {
7673
selectAssetWithCallback
7774
} = useAssetBrowser(props.assets)
7875
79-
// Dialog controls panel visibility via prop
8076
const shouldShowLeftPanel = computed(() => {
8177
return props.showLeftPanel ?? true
8278
})
8379
84-
// Handle close button - call both the prop callback and emit the event
8580
const handleClose = () => {
8681
props.onClose?.()
8782
emit('close')
8883
}
8984
90-
// Handle asset selection and emit to parent
9185
const handleAssetSelectAndEmit = async (asset: AssetDisplayItem) => {
92-
emit('asset-select', asset) // Emit the full asset object
93-
94-
// Use composable for detail fetching and callback execution
86+
emit('asset-select', asset)
9587
await selectAssetWithCallback(asset.id, props.onSelect)
9688
}
9789
</script>

src/platform/assets/composables/useAssetBrowser.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -172,24 +172,17 @@ export function useAssetBrowser(assets: AssetItem[] = []) {
172172
assetId: string,
173173
onSelect?: (filename: string) => void
174174
): Promise<void> {
175-
// Always log selection for debugging
176175
if (import.meta.env.DEV) {
177-
console.log('Asset selected:', assetId)
176+
console.debug('Asset selected:', assetId)
178177
}
179178

180-
// If no callback provided, just return (no need to fetch details)
181179
if (!onSelect) {
182180
return
183181
}
184182

185183
try {
186-
// Fetch complete asset details to get user_metadata
187184
const detailAsset = await assetService.getAssetDetails(assetId)
188-
189-
// Extract filename from user_metadata
190185
const filename = detailAsset.user_metadata?.filename
191-
192-
// Validate filename using Zod schema
193186
const validatedFilename = assetFilenameSchema.safeParse(filename)
194187
if (!validatedFilename.success) {
195188
console.error(
@@ -201,7 +194,6 @@ export function useAssetBrowser(assets: AssetItem[] = []) {
201194
return
202195
}
203196

204-
// Execute callback with validated filename
205197
onSelect(validatedFilename.data)
206198
} catch (error) {
207199
console.error(`Failed to fetch asset details for ${assetId}:`, error)

src/platform/assets/composables/useAssetBrowserDialog.ts

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import { useRafFn } from '@vueuse/core'
2+
import { nextTick } from 'vue'
3+
14
import AssetBrowserModal from '@/platform/assets/components/AssetBrowserModal.vue'
25
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
36
import { assetService } from '@/platform/assets/services/assetService'
4-
import { useDialogStore } from '@/stores/dialogStore'
7+
import { type DialogComponentProps, useDialogStore } from '@/stores/dialogStore'
58

69
interface AssetBrowserDialogProps {
710
/** ComfyUI node type for context (e.g., 'CheckpointLoaderSimple') */
@@ -22,6 +25,42 @@ export const useAssetBrowserDialog = () => {
2225
const dialogKey = 'global-asset-browser'
2326
let onHideComplete: (() => void) | null = null
2427

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+
*/
2564
function hide(): Promise<void> {
2665
return new Promise((resolve) => {
2766
onHideComplete = resolve
@@ -30,15 +69,25 @@ export const useAssetBrowserDialog = () => {
3069
}
3170

3271
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)
3975

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 = {
4291
headless: true,
4392
modal: true,
4493
closable: true,
@@ -51,7 +100,7 @@ export const useAssetBrowserDialog = () => {
51100
},
52101
pt: {
53102
root: {
54-
class: 'rounded-2xl overflow-hidden'
103+
class: 'rounded-2xl overflow-hidden asset-browser-dialog'
55104
},
56105
header: {
57106
class: 'p-0 hidden'
@@ -62,17 +111,16 @@ export const useAssetBrowserDialog = () => {
62111
}
63112
}
64113

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+
})
76124

77125
dialogStore.showDialog({
78126
key: dialogKey,

src/platform/assets/services/assetService.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -140,15 +140,9 @@ function createAssetService() {
140140
return []
141141
}
142142

143-
// Find the category for this node type by reverse lookup in modelToNodeMap
143+
// Find the category for this node type using efficient O(1) lookup
144144
const modelToNodeStore = useModelToNodeStore()
145-
const modelToNodeMap = modelToNodeStore.modelToNodeMap
146-
147-
const category = Object.keys(modelToNodeMap).find((categoryKey) =>
148-
modelToNodeMap[categoryKey].some(
149-
(provider) => provider.nodeDef.name === nodeType
150-
)
151-
)
145+
const category = modelToNodeStore.getCategoryForNodeType(nodeType)
152146

153147
if (!category) {
154148
return []

src/renderer/extensions/vueNodes/widgets/composables/useComboWidget.ts

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@ import { ref } from 'vue'
33
import MultiSelectWidget from '@/components/graph/widgets/MultiSelectWidget.vue'
44
import { t } from '@/i18n'
55
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
6-
import type {
7-
IAssetWidget,
8-
IBaseWidget,
9-
IComboWidget
10-
} from '@/lib/litegraph/src/types/widgets'
6+
import { isAssetWidget, isComboWidget } from '@/lib/litegraph/src/litegraph'
7+
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
118
import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBrowserDialog'
129
import { assetService } from '@/platform/assets/services/assetService'
1310
import { useSettingStore } from '@/platform/settings/settingStore'
@@ -29,15 +26,6 @@ import {
2926

3027
import { useRemoteWidget } from './useRemoteWidget'
3128

32-
// Type guards for widget safety
33-
function isAssetWidget(widget: IBaseWidget): widget is IAssetWidget {
34-
return widget.type === 'asset'
35-
}
36-
37-
function isComboWidget(widget: IBaseWidget): widget is IComboWidget {
38-
return widget.type === 'combo'
39-
}
40-
4129
const getDefaultValue = (inputSpec: ComboInputSpec) => {
4230
if (inputSpec.default) return inputSpec.default
4331
if (inputSpec.options?.length) return inputSpec.options[0]
@@ -94,16 +82,15 @@ const addComboWidget = (
9482
if (!isAssetWidget(widget)) {
9583
throw new Error(`Expected asset widget but received ${widget.type}`)
9684
}
97-
const assetWidget = widget
9885
await assetBrowserDialog.show({
9986
nodeType: node.comfyClass || '',
10087
inputName: inputSpec.name,
101-
currentValue: assetWidget.value,
88+
currentValue: widget.value,
10289
onAssetSelected: (filename: string) => {
103-
assetWidget.value = filename
90+
widget.value = filename
10491
// Must call widget.callback to notify litegraph of value changes
10592
// This ensures proper serialization and triggers any downstream effects
106-
assetWidget.callback?.(assetWidget.value)
93+
widget.callback?.(widget.value)
10794
}
10895
})
10996
}

0 commit comments

Comments
 (0)