Skip to content

Commit b48636a

Browse files
authored
Extract AssetsSidebarTab template and improve UI structure (#6164)
## Summary - Extract sidebar template into reusable AssetSidebarTemplate component - Replace PrimeVue Tabs with TextButton for better visual consistency - Improve job detail view header layout with better spacing ## Changes - Created `AssetSidebarTemplate.vue` as a reusable template component - Replaced PrimeVue Tabs with TextButton components for tab navigation - Added i18n translation key for "Back to all assets" button - Improved spacing and layout in job detail view header - Maintained all existing functionality while cleaning up template structure ## Test Plan - [ ] Verify tab switching between Imported and Generated tabs works correctly - [ ] Test job detail view displays properly with Job ID and execution time - [ ] Confirm "Back to all assets" button returns to main view - [ ] Check that all existing media asset features remain functional - [ ] Verify UI consistency with other sidebar tabs [screen-capture.webm](https://github.com/user-attachments/assets/4ed192e1-a9f7-4fc1-a41e-f732741dd55d)
1 parent 13c5b44 commit b48636a

File tree

17 files changed

+814
-76
lines changed

17 files changed

+814
-76
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<template>
2+
<div
3+
class="flex h-full flex-col bg-interface-panel-surface"
4+
:class="props.class"
5+
>
6+
<div>
7+
<div
8+
v-if="slots.top"
9+
class="flex min-h-12 items-center border-b border-interface-stroke px-4 py-2"
10+
>
11+
<slot name="top" />
12+
</div>
13+
<div v-if="slots.header" class="px-4">
14+
<slot name="header" />
15+
</div>
16+
</div>
17+
<!-- h-0 to force scrollpanel to grow -->
18+
<ScrollPanel class="h-0 grow">
19+
<slot name="body" />
20+
</ScrollPanel>
21+
</div>
22+
</template>
23+
24+
<script setup lang="ts">
25+
import ScrollPanel from 'primevue/scrollpanel'
26+
import { useSlots } from 'vue'
27+
28+
const props = defineProps<{
29+
class?: string
30+
}>()
31+
32+
const slots = useSlots()
33+
</script>

src/components/sidebar/tabs/AssetsSidebarTab.vue

Lines changed: 156 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,48 @@
11
<template>
2-
<SidebarTabTemplate :title="$t('sideToolbar.mediaAssets')">
2+
<AssetsSidebarTemplate>
3+
<template #top>
4+
<span v-if="!isInFolderView" class="font-bold">
5+
{{ $t('sideToolbar.mediaAssets') }}
6+
</span>
7+
<div v-else class="flex w-full items-center justify-between gap-2">
8+
<div class="flex items-center gap-2">
9+
<span class="font-bold">{{ $t('Job ID') }}:</span>
10+
<span class="text-sm">{{ folderPromptId?.substring(0, 8) }}</span>
11+
<button
12+
class="m-0 cursor-pointer border-0 bg-transparent p-0 outline-0"
13+
role="button"
14+
@click="copyJobId"
15+
>
16+
<i class="mb-1 icon-[lucide--copy] text-sm"></i>
17+
</button>
18+
</div>
19+
<div>
20+
<span>{{ formattedExecutionTime }}</span>
21+
</div>
22+
</div>
23+
</template>
324
<template #header>
4-
<Tabs v-model:value="activeTab" class="w-full">
5-
<TabList class="border-b border-neutral-300">
6-
<Tab value="input">{{ $t('sideToolbar.labels.imported') }}</Tab>
7-
<Tab value="output">{{ $t('sideToolbar.labels.generated') }}</Tab>
8-
</TabList>
9-
</Tabs>
25+
<!-- Job Detail View Header -->
26+
<div v-if="isInFolderView" class="pt-4 pb-2">
27+
<IconTextButton
28+
:label="$t('sideToolbar.backToAssets')"
29+
type="secondary"
30+
@click="exitFolderView"
31+
>
32+
<template #icon>
33+
<i class="icon-[lucide--arrow-left] size-4" />
34+
</template>
35+
</IconTextButton>
36+
</div>
37+
<!-- Normal Tab View -->
38+
<TabList v-else v-model="activeTab" class="pt-4 pb-1">
39+
<Tab value="input">{{ $t('sideToolbar.labels.imported') }}</Tab>
40+
<Tab value="output">{{ $t('sideToolbar.labels.generated') }}</Tab>
41+
</TabList>
1042
</template>
1143
<template #body>
1244
<VirtualGrid
13-
v-if="mediaAssets.length"
45+
v-if="displayAssets.length"
1446
:items="mediaAssetsWithKey"
1547
:grid-style="{
1648
display: 'grid',
@@ -23,8 +55,11 @@
2355
<MediaAssetCard
2456
:asset="item"
2557
:selected="selectedAsset?.id === item.id"
58+
:show-output-count="shouldShowOutputCount(item)"
59+
:output-count="getOutputCount(item)"
2660
@click="handleAssetSelect(item)"
2761
@zoom="handleZoomClick(item)"
62+
@output-count-click="enterFolderView(item)"
2863
/>
2964
</template>
3065
</VirtualGrid>
@@ -45,7 +80,7 @@
4580
/>
4681
</div>
4782
</template>
48-
</SidebarTabTemplate>
83+
</AssetsSidebarTemplate>
4984
<ResultGallery
5085
v-model:active-index="galleryActiveIndex"
5186
:all-gallery-items="galleryItems"
@@ -54,23 +89,49 @@
5489

5590
<script setup lang="ts">
5691
import ProgressSpinner from 'primevue/progressspinner'
57-
import Tab from 'primevue/tab'
58-
import TabList from 'primevue/tablist'
59-
import Tabs from 'primevue/tabs'
92+
import { useToast } from 'primevue/usetoast'
6093
import { computed, ref, watch } from 'vue'
6194
95+
import IconTextButton from '@/components/button/IconTextButton.vue'
6296
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
6397
import VirtualGrid from '@/components/common/VirtualGrid.vue'
64-
import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue'
6598
import ResultGallery from '@/components/sidebar/tabs/queue/ResultGallery.vue'
99+
import Tab from '@/components/tab/Tab.vue'
100+
import TabList from '@/components/tab/TabList.vue'
101+
import { t } from '@/i18n'
66102
import MediaAssetCard from '@/platform/assets/components/MediaAssetCard.vue'
67103
import { useMediaAssets } from '@/platform/assets/composables/useMediaAssets'
104+
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
68105
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
69106
import { ResultItemImpl } from '@/stores/queueStore'
70-
import { getMediaTypeFromFilename } from '@/utils/formatUtil'
107+
import { formatDuration, getMediaTypeFromFilename } from '@/utils/formatUtil'
108+
109+
import AssetsSidebarTemplate from './AssetSidebarTemplate.vue'
71110
72111
const activeTab = ref<'input' | 'output'>('input')
73112
const selectedAsset = ref<AssetItem | null>(null)
113+
const folderPromptId = ref<string | null>(null)
114+
const folderExecutionTime = ref<number | undefined>(undefined)
115+
const isInFolderView = computed(() => folderPromptId.value !== null)
116+
117+
const getOutputCount = (item: AssetItem): number => {
118+
const count = item.user_metadata?.outputCount
119+
return typeof count === 'number' && count > 0 ? count : 0
120+
}
121+
122+
const shouldShowOutputCount = (item: AssetItem): boolean => {
123+
if (activeTab.value !== 'output' || isInFolderView.value) {
124+
return false
125+
}
126+
return getOutputCount(item) > 1
127+
}
128+
129+
const formattedExecutionTime = computed(() => {
130+
if (!folderExecutionTime.value) return ''
131+
return formatDuration(folderExecutionTime.value * 1000)
132+
})
133+
134+
const toast = useToast()
74135
75136
const inputAssets = useMediaAssets('input')
76137
const outputAssets = useMediaAssets('output')
@@ -84,7 +145,9 @@ const mediaAssets = computed(() => currentAssets.value.media.value)
84145
85146
const galleryActiveIndex = ref(-1)
86147
const galleryItems = computed(() => {
87-
return mediaAssets.value.map((asset) => {
148+
// Convert AssetItems to ResultItemImpl format for gallery
149+
// Use displayAssets instead of mediaAssets to show correct items based on view mode
150+
return displayAssets.value.map((asset) => {
88151
const mediaType = getMediaTypeFromFilename(asset.name)
89152
const resultItem = new ResultItemImpl({
90153
filename: asset.name,
@@ -105,9 +168,23 @@ const galleryItems = computed(() => {
105168
})
106169
})
107170
171+
// Store folder view assets separately
172+
const folderAssets = ref<AssetItem[]>([])
173+
174+
// Get display assets based on view mode
175+
const displayAssets = computed(() => {
176+
if (isInFolderView.value) {
177+
// Show all assets from the folder view
178+
return folderAssets.value
179+
}
180+
181+
// Normal view: show grouped assets (already have outputCount from API)
182+
return mediaAssets.value
183+
})
184+
108185
// Add key property for VirtualGrid
109186
const mediaAssetsWithKey = computed(() => {
110-
return mediaAssets.value.map((asset) => ({
187+
return displayAssets.value.map((asset) => ({
111188
...asset,
112189
key: asset.id
113190
}))
@@ -138,9 +215,71 @@ const handleAssetSelect = (asset: AssetItem) => {
138215
}
139216
140217
const handleZoomClick = (asset: AssetItem) => {
141-
const index = mediaAssets.value.findIndex((a) => a.id === asset.id)
218+
// Find the index of the clicked asset
219+
const index = displayAssets.value.findIndex((a) => a.id === asset.id)
142220
if (index !== -1) {
143221
galleryActiveIndex.value = index
144222
}
145223
}
224+
225+
const enterFolderView = (asset: AssetItem) => {
226+
const metadata = getOutputAssetMetadata(asset.user_metadata)
227+
if (!metadata) {
228+
console.warn('Invalid output asset metadata')
229+
return
230+
}
231+
232+
const { promptId, allOutputs, executionTimeInSeconds } = metadata
233+
234+
if (!promptId || !Array.isArray(allOutputs) || allOutputs.length === 0) {
235+
console.warn('Missing required folder view data')
236+
return
237+
}
238+
239+
folderPromptId.value = promptId
240+
folderExecutionTime.value = executionTimeInSeconds
241+
242+
folderAssets.value = allOutputs.map((output) => ({
243+
id: `${promptId}-${output.nodeId}-${output.filename}`,
244+
name: output.filename,
245+
size: 0,
246+
created_at: asset.created_at,
247+
tags: ['output'],
248+
preview_url: output.url,
249+
user_metadata: {
250+
promptId,
251+
nodeId: output.nodeId,
252+
subfolder: output.subfolder,
253+
executionTimeInSeconds,
254+
workflow: metadata.workflow
255+
}
256+
}))
257+
}
258+
259+
const exitFolderView = () => {
260+
folderPromptId.value = null
261+
folderExecutionTime.value = undefined
262+
folderAssets.value = []
263+
}
264+
265+
const copyJobId = async () => {
266+
if (folderPromptId.value) {
267+
try {
268+
await navigator.clipboard.writeText(folderPromptId.value)
269+
toast.add({
270+
severity: 'success',
271+
summary: t('mediaAsset.jobIdToast.copied'),
272+
detail: t('mediaAsset.jobIdToast.jobIdCopied'),
273+
life: 2000
274+
})
275+
} catch (error) {
276+
toast.add({
277+
severity: 'error',
278+
summary: t('mediaAsset.jobIdToast.error'),
279+
detail: t('mediaAsset.jobIdToast.jobIdCopyFailed'),
280+
life: 3000
281+
})
282+
}
283+
}
284+
}
146285
</script>

src/components/tab/Tab.vue

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<template>
2+
<button
3+
:id="tabId"
4+
:class="tabClasses"
5+
role="tab"
6+
:aria-selected="isActive"
7+
:aria-controls="panelId"
8+
:tabindex="0"
9+
@click="handleClick"
10+
>
11+
<slot />
12+
</button>
13+
</template>
14+
15+
<script setup lang="ts">
16+
import type { Ref } from 'vue'
17+
import { computed, inject } from 'vue'
18+
19+
import { cn } from '@/utils/tailwindUtil'
20+
21+
const { value, panelId } = defineProps<{
22+
value: string
23+
panelId?: string
24+
}>()
25+
26+
const currentValue = inject<Ref<string>>('tabs-value')
27+
const updateValue = inject<(value: string) => void>('tabs-update')
28+
29+
const tabId = computed(() => `tab-${value}`)
30+
const isActive = computed(() => currentValue?.value === value)
31+
32+
const tabClasses = computed(() => {
33+
return cn(
34+
// Base styles from TextButton
35+
'flex items-center justify-center shrink-0',
36+
'px-2.5 py-2 text-sm rounded-lg cursor-pointer transition-all duration-200',
37+
'outline-hidden border-none',
38+
// State styles with semantic tokens
39+
isActive.value
40+
? 'bg-interface-menu-component-surface-hovered text-text-primary text-bold'
41+
: 'bg-transparent text-text-secondary hover:bg-button-hover-surface focus:bg-button-hover-surface'
42+
)
43+
})
44+
45+
const handleClick = () => {
46+
updateValue?.(value)
47+
}
48+
</script>

0 commit comments

Comments
 (0)