fix: disable inspect for non-previewable assets (#8989)

## Summary
Prevent text/other assets from opening a blank fullscreen viewer by
restricting inspect/zoom to previewable media kinds.

## Changes
- Add `isPreviewableMediaType` helper in shared `formatUtil`.
- Gate inspect/zoom actions in `AssetsSidebarTab`, `MediaAssetCard`, and
`MediaAssetContextMenu` using an allowlist (`image`, `video`, `audio`,
`3D`).
- Build gallery items from previewable assets only.
- Add unit tests for `isPreviewableMediaType`.

## Why
`ResultGallery` only renders image/video/audio; text/other assets could
previously enter fullscreen with no renderable content.

## Review Focus
- Verify text/other assets no longer show Inspect and do not open
fullscreen.
- Verify image/video/audio behavior is unchanged.
- Verify 3D still opens the 3D viewer dialog.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8989-fix-disable-inspect-for-non-previewable-assets-30c6d73d36508103a9b9da4fe50236ea)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Benjamin Lu
2026-02-21 01:43:14 -08:00
committed by GitHub
parent 9184f9bce4
commit 17f34788dc
5 changed files with 53 additions and 11 deletions

View File

@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
import {
getMediaTypeFromFilename,
highlightQuery,
isPreviewableMediaType,
truncateFilename
} from './formatUtil'
@@ -196,4 +197,18 @@ describe('formatUtil', () => {
)
})
})
describe('isPreviewableMediaType', () => {
it('returns true for image/video/audio/3D', () => {
expect(isPreviewableMediaType('image')).toBe(true)
expect(isPreviewableMediaType('video')).toBe(true)
expect(isPreviewableMediaType('audio')).toBe(true)
expect(isPreviewableMediaType('3D')).toBe(true)
})
it('returns false for text/other', () => {
expect(isPreviewableMediaType('text')).toBe(false)
expect(isPreviewableMediaType('other')).toBe(false)
})
})
})

View File

@@ -583,3 +583,12 @@ export function getMediaTypeFromFilename(
return 'other'
}
export function isPreviewableMediaType(mediaType: MediaType): boolean {
return (
mediaType === 'image' ||
mediaType === 'video' ||
mediaType === 'audio' ||
mediaType === '3D'
)
}

View File

@@ -244,7 +244,11 @@ import { resolveOutputAssetItems } from '@/platform/assets/utils/outputAssetUtil
import { isCloud } from '@/platform/distribution/types'
import { useDialogStore } from '@/stores/dialogStore'
import { ResultItemImpl } from '@/stores/queueStore'
import { formatDuration, getMediaTypeFromFilename } from '@/utils/formatUtil'
import {
formatDuration,
getMediaTypeFromFilename,
isPreviewableMediaType
} from '@/utils/formatUtil'
import { cn } from '@/utils/tailwindUtil'
const { t } = useI18n()
@@ -405,6 +409,12 @@ const visibleAssets = computed(() => {
return listViewSelectableAssets.value
})
const previewableVisibleAssets = computed(() =>
visibleAssets.value.filter((asset) =>
isPreviewableMediaType(getMediaTypeFromFilename(asset.name))
)
)
const selectedAssets = computed(() => getSelectedAssets(visibleAssets.value))
const isBulkMode = computed(
@@ -430,12 +440,10 @@ watch(visibleAssets, (newAssets) => {
// so selection stays consistent with what this view can act on.
reconcileSelection(newAssets)
if (currentGalleryAssetId.value && galleryActiveIndex.value !== -1) {
const newIndex = newAssets.findIndex(
const newIndex = previewableVisibleAssets.value.findIndex(
(asset) => asset.id === currentGalleryAssetId.value
)
if (newIndex !== -1) {
galleryActiveIndex.value = newIndex
}
galleryActiveIndex.value = newIndex
}
})
@@ -446,7 +454,7 @@ watch(galleryActiveIndex, (index) => {
})
const galleryItems = computed(() => {
return visibleAssets.value.map((asset) => {
return previewableVisibleAssets.value.map((asset) => {
const mediaType = getMediaTypeFromFilename(asset.name)
const resultItem = new ResultItemImpl({
filename: asset.name,
@@ -552,6 +560,9 @@ const handleDeleteSelected = async () => {
const handleZoomClick = (asset: AssetItem) => {
const mediaType = getMediaTypeFromFilename(asset.name)
if (!isPreviewableMediaType(mediaType)) {
return
}
if (mediaType === '3D') {
const dialogStore = useDialogStore()
@@ -571,7 +582,9 @@ const handleZoomClick = (asset: AssetItem) => {
}
currentGalleryAssetId.value = asset.id
const index = visibleAssets.value.findIndex((a) => a.id === asset.id)
const index = previewableVisibleAssets.value.findIndex(
(a) => a.id === asset.id
)
if (index !== -1) {
galleryActiveIndex.value = index
}

View File

@@ -59,6 +59,7 @@
>
<IconGroup background-class="bg-white">
<Button
v-if="canInspect"
variant="overlay-white"
size="icon"
:aria-label="$t('mediaAsset.actions.zoom')"
@@ -141,7 +142,8 @@ import {
formatDuration,
formatSize,
getFilenameDetails,
getMediaTypeFromFilename
getMediaTypeFromFilename,
isPreviewableMediaType
} from '@/utils/formatUtil'
import { cn } from '@/utils/tailwindUtil'
@@ -217,6 +219,8 @@ const previewKind = computed((): PreviewKind => {
return getMediaTypeFromFilename(asset?.name || '')
})
const canInspect = computed(() => isPreviewableMediaType(fileKind.value))
// Get filename without extension
const fileName = computed(() => {
return getFilenameDetails(asset?.name || '').filename
@@ -278,7 +282,7 @@ const showActionsOverlay = computed(() => {
})
const handleZoomClick = () => {
if (asset) {
if (asset && canInspect.value) {
emit('zoom', asset)
}
}

View File

@@ -39,6 +39,7 @@ import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { isCloud } from '@/platform/distribution/types'
import { supportsWorkflowMetadata } from '@/platform/workflow/utils/workflowExtractionUtil'
import { isPreviewableMediaType } from '@/utils/formatUtil'
import { detectNodeTypeFromFilename } from '@/utils/loaderNodeUtil'
import { cn } from '@/utils/tailwindUtil'
@@ -206,8 +207,8 @@ const contextMenuItems = computed<MenuItem[]>(() => {
// Individual mode: Show all menu options
// Inspect (if not 3D)
if (fileKind !== '3D') {
// Inspect
if (isPreviewableMediaType(fileKind)) {
items.push({
label: t('mediaAsset.actions.inspect'),
icon: 'icon-[lucide--zoom-in]',