Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions src/components/dialog/content/manager/ManagerDialogContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -408,19 +408,30 @@ const handleGridContainerClick = (event: MouseEvent) => {

const hasMultipleSelections = computed(() => selectedNodePacks.value.length > 1)

// Track the last pack ID for which we've fetched full registry data
const lastFetchedPackId = ref<string | null>(null)

// Whenever a single pack is selected, fetch its full info once
whenever(selectedNodePack, async () => {
// Cancel any in-flight requests from previously selected node pack
getPackById.cancel()

if (!selectedNodePack.value?.id) return

// If only a single node pack is selected, fetch full node pack info from registry
const pack = selectedNodePack.value
if (!pack?.id) return
if (hasMultipleSelections.value) return
const data = await getPackById.call(selectedNodePack.value.id)

if (data?.id === selectedNodePack.value?.id) {
// If selected node hasn't changed since request, merge registry & Algolia data
selectedNodePacks.value = [merge(selectedNodePack.value, data)]
// Only fetch if we haven't already for this pack
if (lastFetchedPackId.value === pack.id) return
const data = await getPackById.call(pack.id)
// If selected node hasn't changed since request, merge registry & Algolia data
if (data?.id === pack.id) {
lastFetchedPackId.value = pack.id
const mergedPack = merge({}, pack, data)
selectedNodePacks.value = [mergedPack]
// Replace pack in displayPacks so that children receive a fresh prop reference
const idx = displayPacks.value.findIndex((p) => p.id === mergedPack.id)
if (idx !== -1) {
displayPacks.value.splice(idx, 1, mergedPack)
}
Comment on lines +419 to +434
Copy link
Contributor

@christian-byrne christian-byrne Jun 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you verify using Network tab in dev tools that this caching is necessary? The getPackById should already be wrapped with useCachedRequest which handles request caching / deduplication. Although in general this doesn't really hurt to add, so not a big deal.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apart from caching, it also prevents an infinite loop issue I had, it should be OK to ship as is, not too important

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM

}
})

Expand Down
41 changes: 41 additions & 0 deletions src/components/dialog/content/manager/packBanner/PackBanner.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<template>
<img
:src="isImageError ? DEFAULT_BANNER : imgSrc"
:alt="nodePack.name + ' banner'"
class="object-cover"
:style="{ width: cssWidth, height: cssHeight }"
@error="isImageError = true"
/>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue'

import { components } from '@/types/comfyRegistryTypes'

const DEFAULT_BANNER = '/assets/images/fallback-gradient-avatar.svg'

const {
nodePack,
width = '100%',
height = '12rem'
} = defineProps<{
nodePack: components['schemas']['Node'] & { banner?: string } // Temporary measure until banner is in backend
width?: string
height?: string
}>()

const isImageError = ref(false)
const shouldShowFallback = computed(
() => !nodePack.banner || nodePack.banner.trim() === '' || isImageError.value
)
const imgSrc = computed(() =>
shouldShowFallback.value ? DEFAULT_BANNER : nodePack.banner
)

const convertToCssValue = (value: string | number) =>
typeof value === 'number' ? `${value}rem` : value

const cssWidth = computed(() => convertToCssValue(width))
const cssHeight = computed(() => convertToCssValue(height))
</script>
101 changes: 66 additions & 35 deletions src/components/dialog/content/manager/packCard/PackCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,15 @@
}"
:pt="{
body: { class: 'p-0 flex flex-col w-full h-full rounded-2xl gap-0' },
content: { class: 'flex-1 flex flex-col rounded-2xl' },
title: {
class:
'self-stretch w-full px-4 py-3 inline-flex justify-start items-center gap-6'
},
content: { class: 'flex-1 flex flex-col rounded-2xl min-h-0' },
title: { class: 'w-full h-full rounded-t-lg cursor-pointer' },
footer: { class: 'p-0 m-0' }
}"
>
<template #title>
<PackCardHeader :node-pack="nodePack" />
<PackBanner :node-pack="nodePack" />
</template>
<template #content>
<ContentDivider />
<template v-if="isInstalling">
<div
class="self-stretch inline-flex flex-col justify-center items-center gap-2 h-full"
Expand All @@ -34,46 +30,63 @@
</template>
<template v-else>
<div
class="self-stretch px-4 py-3 inline-flex justify-start items-start cursor-pointer"
class="self-stretch inline-flex flex-col justify-start items-start"
>
<PackIcon :node-pack="nodePack" />
<div
class="px-4 inline-flex flex-col justify-start items-start overflow-hidden"
class="px-4 py-3 inline-flex justify-start items-start cursor-pointer w-full"
>
<span
class="text-sm font-bold truncate overflow-hidden text-ellipsis"
>
{{ nodePack.name }}
</span>
<div
class="self-stretch inline-flex justify-center items-center gap-2.5"
class="inline-flex flex-col justify-start items-start overflow-hidden gap-y-3 w-full"
>
<span
class="text-base font-bold truncate overflow-hidden text-ellipsis"
>
{{ nodePack.name }}
</span>
<p
v-if="nodePack.description"
class="flex-1 justify-start text-muted text-sm font-medium leading-3 break-words overflow-hidden min-h-12 line-clamp-3"
class="flex-1 justify-start text-muted text-sm font-medium break-words overflow-hidden min-h-12 line-clamp-3 my-0 leading-5"
>
{{ nodePack.description }}
</p>
</div>
<div
class="self-stretch inline-flex justify-start items-center gap-2"
>
<div
v-if="nodesCount"
class="px-2 py-1 flex justify-center text-sm items-center gap-1"
>
<div class="text-center justify-center font-medium leading-3">
{{ nodesCount }} {{ $t('g.nodes') }}
</div>
</div>
<div class="px-2 py-1 flex justify-center items-center gap-1">
<div class="flex flex-col gap-y-2">
<div
v-if="isUpdateAvailable"
class="w-4 h-4 relative overflow-hidden"
class="self-stretch inline-flex justify-start items-center gap-1"
>
<i class="pi pi-arrow-circle-up text-blue-600" />
<div
v-if="nodesCount"
class="pr-2 py-1 flex justify-center text-sm items-center gap-1"
>
<div
class="text-center justify-center font-medium leading-3"
>
{{ nodesCount }} {{ $t('g.nodes') }}
</div>
</div>
<div class="px-2 py-1 flex justify-center items-center gap-1">
<div
v-if="isUpdateAvailable"
class="w-4 h-4 relative overflow-hidden"
>
<i class="pi pi-arrow-circle-up text-blue-600" />
</div>
<PackVersionBadge :node-pack="nodePack" />
</div>
<div
v-if="formattedLatestVersionDate"
class="px-2 py-1 flex justify-center items-center gap-1 text-xs text-muted font-medium"
>
{{ formattedLatestVersionDate }}
</div>
</div>
<div class="flex">
<span
v-if="publisherName"
class="text-xs text-muted font-medium leading-3 max-w-40 truncate"
>
{{ publisherName }}
</span>
</div>
<PackVersionBadge :node-pack="nodePack" />
</div>
</div>
</div>
Expand All @@ -92,11 +105,12 @@ import { whenever } from '@vueuse/core'
import Card from 'primevue/card'
import ProgressSpinner from 'primevue/progressspinner'
import { computed, provide, ref } from 'vue'
import { useI18n } from 'vue-i18n'

import ContentDivider from '@/components/common/ContentDivider.vue'
import PackVersionBadge from '@/components/dialog/content/manager/PackVersionBadge.vue'
import PackBanner from '@/components/dialog/content/manager/packBanner/PackBanner.vue'
import PackCardFooter from '@/components/dialog/content/manager/packCard/PackCardFooter.vue'
import PackIcon from '@/components/dialog/content/manager/packIcon/PackIcon.vue'
import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { IsInstallingKey } from '@/types/comfyManagerTypes'
Expand All @@ -107,6 +121,8 @@ const { nodePack, isSelected = false } = defineProps<{
isSelected?: boolean
}>()

const { d } = useI18n()

const isInstalling = ref(false)
provide(IsInstallingKey, isInstalling)

Expand All @@ -122,4 +138,19 @@ whenever(isInstalled, () => (isInstalling.value = false))

// TODO: remove type assertion once comfy_nodes is added to node (pack) info type in backend
const nodesCount = computed(() => (nodePack as any).comfy_nodes?.length)

const publisherName = computed(() => {
if (!nodePack) return null

const { publisher, author } = nodePack
return publisher?.name ?? publisher?.id ?? author
})

const formattedLatestVersionDate = computed(() => {
if (!nodePack.latest_version?.createdAt) return null

return d(new Date(nodePack.latest_version.createdAt), {
dateStyle: 'medium'
})
})
</script>
32 changes: 11 additions & 21 deletions src/components/dialog/content/manager/packCard/PackCardFooter.vue
Original file line number Diff line number Diff line change
@@ -1,39 +1,29 @@
<template>
<div
class="flex justify-between px-5 py-4 text-xs text-muted font-medium leading-3"
class="flex justify-between items-center px-4 py-2 text-xs text-muted font-medium leading-3"
>
<div class="flex items-center gap-2 cursor-pointer">
<span v-if="publisherName" class="max-w-40 truncate">
{{ publisherName }}
</span>
</div>
<div
v-if="nodePack.latest_version?.createdAt"
class="flex items-center gap-2 truncate"
>
{{ $t('g.updated') }}
{{
$d(new Date(nodePack.latest_version.createdAt), {
dateStyle: 'medium'
})
}}
<div v-if="nodePack.downloads" class="flex items-center gap-1.5">
<i class="pi pi-download text-muted"></i>
<span>{{ formattedDownloads }}</span>
</div>
<PackInstallButton :node-packs="[nodePack]" />
</div>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
import type { components } from '@/types/comfyRegistryTypes'

const { nodePack } = defineProps<{
nodePack: components['schemas']['Node']
}>()

const publisherName = computed(() => {
if (!nodePack) return null
const { n } = useI18n()

const { publisher, author } = nodePack
return publisher?.name ?? publisher?.id ?? author
})
const formattedDownloads = computed(() =>
nodePack.downloads ? n(nodePack.downloads) : ''
)
</script>