fix: vue nodes image preview widget, better multi image gallery support (#7178)

## Summary

Fix image preview to better handle multiple images, switching between
them, and showing the skeleton correctly.

## Changes

- **What**: ImagePreview.vue

## Screenshots (if applicable)

Old (broken)


https://github.com/user-attachments/assets/e4997569-bdf5-4015-a83c-bbaabeac96d6

New (fixed)


https://github.com/user-attachments/assets/19dda841-c909-4fcb-b4d4-99aa1372843b

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7178-fix-vue-nodes-image-preview-widget-better-multi-image-gallery-support-2c06d73d365081a2afa9e398200e8379)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Simula_r
2025-12-05 13:52:18 -08:00
committed by GitHub
parent 5e606f274f
commit ec1a7f1da4
3 changed files with 38 additions and 38 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

View File

@@ -1,7 +1,7 @@
<template>
<div
v-if="imageUrls.length > 0"
class="image-preview group relative flex size-full min-h-16 min-w-16 flex-col px-2 justify-center"
class="image-preview outline-none group relative flex size-full min-h-16 min-w-16 flex-col px-2 justify-center"
tabindex="0"
role="region"
:aria-label="$t('g.imagePreview')"
@@ -11,29 +11,30 @@
>
<!-- Image Wrapper -->
<div
class="h-full w-full overflow-hidden rounded-[5px] bg-node-component-surface"
class="h-full w-full overflow-hidden rounded-[5px] bg-node-component-surface relative"
>
<!-- Error State -->
<div
v-if="imageError"
class="flex size-full flex-col items-center justify-center bg-smoke-800/50 text-center text-white py-8"
class="flex size-full flex-col items-center justify-center bg-muted-background text-center text-base-foreground py-8"
>
<i class="mb-2 icon-[lucide--image-off] h-12 w-12 text-smoke-400" />
<p class="text-sm text-smoke-300">{{ $t('g.imageFailedToLoad') }}</p>
<p class="mt-1 text-xs text-smoke-400">
<i
class="mb-2 icon-[lucide--image-off] h-12 w-12 text-base-foreground"
/>
<p class="text-sm text-base-foreground">
{{ $t('g.imageFailedToLoad') }}
</p>
<p class="mt-1 text-xs text-base-foreground">
{{ getImageFilename(currentImageUrl) }}
</p>
</div>
<!-- Loading State -->
<Skeleton
v-if="isLoading && !imageError"
class="absolute inset-0 size-full"
border-radius="5px"
width="16rem"
height="16rem"
width="100%"
height="100%"
/>
<!-- Main Image -->
<img
v-if="!imageError"
@@ -83,39 +84,35 @@
<i class="icon-[lucide--x] h-4 w-4" />
</button>
</div>
<!-- Multiple Images Navigation -->
<div
v-if="hasMultipleImages"
class="absolute right-2 bottom-2 left-2 flex justify-center gap-1"
>
<button
v-for="(_, index) in imageUrls"
:key="index"
:class="getNavigationDotClass(index)"
:aria-label="
$t('g.viewImageOfTotal', {
index: index + 1,
total: imageUrls.length
})
"
@click="setCurrentIndex(index)"
/>
</div>
</div>
<!-- Image Dimensions -->
<div class="mt-2 text-center text-xs text-white">
<div class="pt-2 text-center text-xs text-base-foreground">
<span v-if="imageError" class="text-red-400">
{{ $t('g.errorLoadingImage') }}
</span>
<span v-else-if="isLoading" class="text-smoke-400">
<span v-else-if="isLoading" class="text-base-foreground">
{{ $t('g.loading') }}...
</span>
<span v-else>
{{ actualDimensions || $t('g.calculatingDimensions') }}
</span>
</div>
<!-- Multiple Images Navigation -->
<div v-if="hasMultipleImages" class="flex justify-center gap-1 pt-4">
<button
v-for="(_, index) in imageUrls"
:key="index"
:class="getNavigationDotClass(index)"
:aria-label="
$t('g.viewImageOfTotal', {
index: index + 1,
total: imageUrls.length
})
"
@click="setCurrentIndex(index)"
/>
</div>
</div>
</template>
@@ -230,6 +227,7 @@ const handleRemove = () => {
}
const setCurrentIndex = (index: number) => {
if (currentIndex.value === index) return
if (index >= 0 && index < props.imageUrls.length) {
currentIndex.value = index
actualDimensions.value = null
@@ -248,8 +246,10 @@ const handleMouseLeave = () => {
const getNavigationDotClass = (index: number) => {
return [
'w-2 h-2 rounded-full transition-all duration-200 border-0 cursor-pointer',
index === currentIndex.value ? 'bg-white' : 'bg-white/50 hover:bg-white/80'
'w-2 h-2 rounded-full transition-all duration-200 border-0 cursor-pointer p-0',
index === currentIndex.value
? 'bg-base-foreground'
: 'bg-base-foreground/50 hover:bg-base-foreground/80'
]
}

View File

@@ -225,16 +225,16 @@ describe('ImagePreview', () => {
const navigationDots = wrapper.findAll('.w-2.h-2.rounded-full')
// First dot should be active (has bg-white class)
expect(navigationDots[0].classes()).toContain('bg-white')
expect(navigationDots[1].classes()).toContain('bg-white/50')
expect(navigationDots[0].classes()).toContain('bg-base-foreground')
expect(navigationDots[1].classes()).toContain('bg-base-foreground/50')
// Switch to second image
await navigationDots[1].trigger('click')
await nextTick()
// Second dot should now be active
expect(navigationDots[0].classes()).toContain('bg-white/50')
expect(navigationDots[1].classes()).toContain('bg-white')
expect(navigationDots[0].classes()).toContain('bg-base-foreground/50')
expect(navigationDots[1].classes()).toContain('bg-base-foreground')
})
it('loads image without errors', async () => {