diff --git a/src/components/sidebar/tabs/AssetsSidebarGridView.vue b/src/components/sidebar/tabs/AssetsSidebarGridView.vue
new file mode 100644
index 000000000..ac0bfcf39
--- /dev/null
+++ b/src/components/sidebar/tabs/AssetsSidebarGridView.vue
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+ {{
+ t(
+ assetType === 'input'
+ ? 'sideToolbar.importedAssetsHeader'
+ : 'sideToolbar.generatedAssetsHeader'
+ )
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/sidebar/tabs/AssetsSidebarListView.vue b/src/components/sidebar/tabs/AssetsSidebarListView.vue
index c7dbc292b..b39d982a5 100644
--- a/src/components/sidebar/tabs/AssetsSidebarListView.vue
+++ b/src/components/sidebar/tabs/AssetsSidebarListView.vue
@@ -2,7 +2,7 @@
-
-
-
-
-
+ @zoom="handleZoomClick"
+ @output-count-click="enterFolderView"
+ />
@@ -220,15 +209,14 @@ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
-import VirtualGrid from '@/components/common/VirtualGrid.vue'
import Load3dViewerContent from '@/components/load3d/Load3dViewerContent.vue'
+import AssetsSidebarGridView from '@/components/sidebar/tabs/AssetsSidebarGridView.vue'
import AssetsSidebarListView from '@/components/sidebar/tabs/AssetsSidebarListView.vue'
import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue'
import ResultGallery from '@/components/sidebar/tabs/queue/ResultGallery.vue'
import Tab from '@/components/tab/Tab.vue'
import TabList from '@/components/tab/TabList.vue'
import Button from '@/components/ui/button/Button.vue'
-import MediaAssetCard from '@/platform/assets/components/MediaAssetCard.vue'
import MediaAssetContextMenu from '@/platform/assets/components/MediaAssetContextMenu.vue'
import MediaAssetFilterBar from '@/platform/assets/components/MediaAssetFilterBar.vue'
import { getAssetType } from '@/platform/assets/composables/media/assetMappers'
@@ -407,14 +395,14 @@ const showLoadingState = computed(
() =>
loading.value &&
displayAssets.value.length === 0 &&
- (!isListView.value || activeJobsCount.value === 0)
+ activeJobsCount.value === 0
)
const showEmptyState = computed(
() =>
!loading.value &&
displayAssets.value.length === 0 &&
- (!isListView.value || activeJobsCount.value === 0)
+ activeJobsCount.value === 0
)
watch(displayAssets, (newAssets) => {
@@ -456,14 +444,6 @@ const galleryItems = computed(() => {
})
})
-// Add key property for VirtualGrid
-const mediaAssetsWithKey = computed(() => {
- return displayAssets.value.map((asset) => ({
- ...asset,
- key: asset.id
- }))
-})
-
const refreshAssets = async () => {
await currentAssets.value.fetchMediaList()
if (error.value) {
diff --git a/src/components/sidebar/tabs/assets/ActiveJobCard.test.ts b/src/components/sidebar/tabs/assets/ActiveJobCard.test.ts
new file mode 100644
index 000000000..59846bb5e
--- /dev/null
+++ b/src/components/sidebar/tabs/assets/ActiveJobCard.test.ts
@@ -0,0 +1,111 @@
+import { mount } from '@vue/test-utils'
+import { describe, expect, it, vi } from 'vitest'
+import { createI18n } from 'vue-i18n'
+
+import ActiveJobCard from './ActiveJobCard.vue'
+
+import type { JobListItem } from '@/composables/queue/useJobList'
+
+vi.mock('@/composables/useProgressBarBackground', () => ({
+ useProgressBarBackground: () => ({
+ progressBarPrimaryClass: 'bg-blue-500',
+ hasProgressPercent: (val: number | undefined) => typeof val === 'number',
+ progressPercentStyle: (val: number) => ({ width: `${val}%` })
+ })
+}))
+
+const i18n = createI18n({
+ legacy: false,
+ locale: 'en',
+ messages: {
+ en: {
+ sideToolbar: {
+ activeJobStatus: 'Active job: {status}'
+ }
+ }
+ }
+})
+
+const createJob = (overrides: Partial = {}): JobListItem => ({
+ id: 'test-job-1',
+ title: 'Running...',
+ meta: 'Step 5/10',
+ state: 'running',
+ progressTotalPercent: 50,
+ progressCurrentPercent: 75,
+ ...overrides
+})
+
+const mountComponent = (job: JobListItem) =>
+ mount(ActiveJobCard, {
+ props: { job },
+ global: {
+ plugins: [i18n]
+ }
+ })
+
+describe('ActiveJobCard', () => {
+ it('displays percentage and progress bar when job is running', () => {
+ const wrapper = mountComponent(
+ createJob({ state: 'running', progressTotalPercent: 65 })
+ )
+
+ expect(wrapper.text()).toContain('65%')
+ const progressBar = wrapper.find('.bg-blue-500')
+ expect(progressBar.exists()).toBe(true)
+ expect(progressBar.attributes('style')).toContain('width: 65%')
+ })
+
+ it('displays status text when job is pending', () => {
+ const wrapper = mountComponent(
+ createJob({
+ state: 'pending',
+ title: 'In queue...',
+ progressTotalPercent: undefined
+ })
+ )
+
+ expect(wrapper.text()).toContain('In queue...')
+ const progressBar = wrapper.find('.bg-blue-500')
+ expect(progressBar.exists()).toBe(false)
+ })
+
+ it('shows spinner for pending state', () => {
+ const wrapper = mountComponent(createJob({ state: 'pending' }))
+
+ const spinner = wrapper.find('.icon-\\[lucide--loader-circle\\]')
+ expect(spinner.exists()).toBe(true)
+ expect(spinner.classes()).toContain('animate-spin')
+ })
+
+ it('shows error icon for failed state', () => {
+ const wrapper = mountComponent(
+ createJob({ state: 'failed', title: 'Failed' })
+ )
+
+ const errorIcon = wrapper.find('.icon-\\[lucide--circle-alert\\]')
+ expect(errorIcon.exists()).toBe(true)
+ expect(wrapper.text()).toContain('Failed')
+ })
+
+ it('shows preview image when running with iconImageUrl', () => {
+ const wrapper = mountComponent(
+ createJob({
+ state: 'running',
+ iconImageUrl: 'https://example.com/preview.jpg'
+ })
+ )
+
+ const img = wrapper.find('img')
+ expect(img.exists()).toBe(true)
+ expect(img.attributes('src')).toBe('https://example.com/preview.jpg')
+ })
+
+ it('has proper accessibility attributes', () => {
+ const wrapper = mountComponent(createJob({ title: 'Generating...' }))
+
+ const container = wrapper.find('[role="status"]')
+ expect(container.exists()).toBe(true)
+ expect(container.attributes('aria-label')).toBe('Active job: Generating...')
+ })
+})
diff --git a/src/components/sidebar/tabs/assets/ActiveJobCard.vue b/src/components/sidebar/tabs/assets/ActiveJobCard.vue
new file mode 100644
index 000000000..8121d9bc3
--- /dev/null
+++ b/src/components/sidebar/tabs/assets/ActiveJobCard.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ Math.round(progressPercent ?? 0) }}%
+
+
+
+
+
+
+ {{ statusText }}
+
+
+
+
+
+
+
diff --git a/src/locales/en/main.json b/src/locales/en/main.json
index 75afcfd07..16116ac1c 100644
--- a/src/locales/en/main.json
+++ b/src/locales/en/main.json
@@ -709,6 +709,7 @@
"noGeneratedFiles": "No generated files found",
"generatedAssetsHeader": "Generated assets",
"importedAssetsHeader": "Imported assets",
+ "activeJobStatus": "Active job: {status}",
"noFilesFoundMessage": "Upload files or generate content to see them here",
"browseTemplates": "Browse example templates",
"openWorkflow": "Open workflow in local file system",
diff --git a/src/utils/queueUtil.ts b/src/utils/queueUtil.ts
index cf9f47b6b..81931012d 100644
--- a/src/utils/queueUtil.ts
+++ b/src/utils/queueUtil.ts
@@ -1,6 +1,15 @@
import type { TaskItemImpl } from '@/stores/queueStore'
import type { JobState } from '@/types/queue'
+/**
+ * Checks if a job state represents an active (in-progress) job.
+ */
+export function isActiveJobState(state: JobState): boolean {
+ return (
+ state === 'pending' || state === 'initialization' || state === 'running'
+ )
+}
+
/**
* Map a task to a UI job state, including initialization override.
*