diff --git a/src/platform/assets/components/AssetBrowserModal.vue b/src/platform/assets/components/AssetBrowserModal.vue
index cd89591aa4..e0c47361d1 100644
--- a/src/platform/assets/components/AssetBrowserModal.vue
+++ b/src/platform/assets/components/AssetBrowserModal.vue
@@ -63,12 +63,8 @@
diff --git a/src/platform/assets/composables/useModelUpload.ts b/src/platform/assets/composables/useModelUpload.ts
index 86372473d4..a8a1459160 100644
--- a/src/platform/assets/composables/useModelUpload.ts
+++ b/src/platform/assets/composables/useModelUpload.ts
@@ -3,13 +3,11 @@ import UploadModelDialog from '@/platform/assets/components/UploadModelDialog.vu
import UploadModelDialogHeader from '@/platform/assets/components/UploadModelDialogHeader.vue'
import UploadModelUpgradeModal from '@/platform/assets/components/UploadModelUpgradeModal.vue'
import UploadModelUpgradeModalHeader from '@/platform/assets/components/UploadModelUpgradeModalHeader.vue'
-import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { useDialogStore } from '@/stores/dialogStore'
-import type { UseAsyncStateReturn } from '@vueuse/core'
import { computed } from 'vue'
export function useModelUpload(
- execute?: UseAsyncStateReturn['execute']
+ onUploadSuccess?: () => Promise | void
) {
const dialogStore = useDialogStore()
const { flags } = useFeatureFlags()
@@ -37,7 +35,7 @@ export function useModelUpload(
component: UploadModelDialog,
props: {
onUploadSuccess: async () => {
- await execute?.()
+ await onUploadSuccess?.()
}
},
dialogComponentProps: {
diff --git a/src/stores/assetsStore.ts b/src/stores/assetsStore.ts
index b75b454009..5fe64e9110 100644
--- a/src/stores/assetsStore.ts
+++ b/src/stores/assetsStore.ts
@@ -1,4 +1,5 @@
import { useAsyncState } from '@vueuse/core'
+import { isEqual } from 'es-toolkit'
import { defineStore } from 'pinia'
import { computed, shallowReactive, ref, watch } from 'vue'
import {
@@ -279,59 +280,81 @@ export const useAssetsStore = defineStore('assets', () => {
new Map>>()
)
+ /**
+ * Internal helper to fetch and cache assets with a given key and fetcher
+ */
+ async function updateModelsForKey(
+ key: string,
+ fetcher: () => Promise
+ ): Promise {
+ if (!stateByNodeType.has(key)) {
+ stateByNodeType.set(
+ key,
+ useAsyncState(fetcher, [], {
+ immediate: false,
+ resetOnExecute: false,
+ onError: (err) => {
+ console.error(`Error fetching model assets for ${key}:`, err)
+ }
+ })
+ )
+ }
+
+ const state = stateByNodeType.get(key)!
+
+ modelLoadingByNodeType.set(key, true)
+ modelErrorByNodeType.set(key, null)
+
+ try {
+ await state.execute()
+ } finally {
+ modelLoadingByNodeType.set(key, state.isLoading.value)
+ }
+
+ const assets = state.state.value
+ const existingAssets = modelAssetsByNodeType.get(key)
+
+ if (!isEqual(existingAssets, assets)) {
+ modelAssetsByNodeType.set(key, assets)
+ }
+
+ modelErrorByNodeType.set(
+ key,
+ state.error.value instanceof Error ? state.error.value : null
+ )
+
+ return assets
+ }
+
/**
* Fetch and cache model assets for a specific node type
- * Uses VueUse's useAsyncState for automatic loading/error tracking
* @param nodeType The node type to fetch assets for (e.g., 'CheckpointLoaderSimple')
* @returns Promise resolving to the fetched assets
*/
async function updateModelsForNodeType(
nodeType: string
): Promise {
- if (!stateByNodeType.has(nodeType)) {
- stateByNodeType.set(
- nodeType,
- useAsyncState(
- () => assetService.getAssetsForNodeType(nodeType),
- [],
- {
- immediate: false,
- resetOnExecute: false,
- onError: (err) => {
- console.error(
- `Error fetching model assets for ${nodeType}:`,
- err
- )
- }
- }
- )
- )
- }
+ return updateModelsForKey(nodeType, () =>
+ assetService.getAssetsForNodeType(nodeType)
+ )
+ }
- const state = stateByNodeType.get(nodeType)!
-
- modelLoadingByNodeType.set(nodeType, true)
- modelErrorByNodeType.set(nodeType, null)
-
- try {
- await state.execute()
- const assets = state.state.value
- modelAssetsByNodeType.set(nodeType, assets)
- modelErrorByNodeType.set(
- nodeType,
- state.error.value instanceof Error ? state.error.value : null
- )
- return assets
- } finally {
- modelLoadingByNodeType.set(nodeType, state.isLoading.value)
- }
+ /**
+ * Fetch and cache model assets for a specific tag
+ * @param tag The tag to fetch assets for (e.g., 'models')
+ * @returns Promise resolving to the fetched assets
+ */
+ async function updateModelsForTag(tag: string): Promise {
+ const key = `tag:${tag}`
+ return updateModelsForKey(key, () => assetService.getAssetsByTag(tag))
}
return {
modelAssetsByNodeType,
modelLoadingByNodeType,
modelErrorByNodeType,
- updateModelsForNodeType
+ updateModelsForNodeType,
+ updateModelsForTag
}
}
@@ -339,7 +362,8 @@ export const useAssetsStore = defineStore('assets', () => {
modelAssetsByNodeType: shallowReactive(new Map()),
modelLoadingByNodeType: shallowReactive(new Map()),
modelErrorByNodeType: shallowReactive(new Map()),
- updateModelsForNodeType: async () => []
+ updateModelsForNodeType: async () => [],
+ updateModelsForTag: async () => []
}
}
@@ -347,7 +371,8 @@ export const useAssetsStore = defineStore('assets', () => {
modelAssetsByNodeType,
modelLoadingByNodeType,
modelErrorByNodeType,
- updateModelsForNodeType
+ updateModelsForNodeType,
+ updateModelsForTag
} = getModelState()
// Watch for completed downloads and refresh model caches
@@ -403,6 +428,7 @@ export const useAssetsStore = defineStore('assets', () => {
modelAssetsByNodeType,
modelLoadingByNodeType,
modelErrorByNodeType,
- updateModelsForNodeType
+ updateModelsForNodeType,
+ updateModelsForTag
}
})
diff --git a/tests-ui/platform/assets/components/AssetBrowserModal.test.ts b/tests-ui/platform/assets/components/AssetBrowserModal.test.ts
index 756dee2628..87135b4689 100644
--- a/tests-ui/platform/assets/components/AssetBrowserModal.test.ts
+++ b/tests-ui/platform/assets/components/AssetBrowserModal.test.ts
@@ -4,20 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import AssetBrowserModal from '@/platform/assets/components/AssetBrowserModal.vue'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
-
-const mockAssetService = vi.hoisted(() => ({
- getAssetsForNodeType: vi.fn(),
- getAssetsByTag: vi.fn(),
- getAssetDetails: vi.fn((id: string) =>
- Promise.resolve({
- id,
- name: 'Test Model',
- user_metadata: {
- filename: 'Test Model'
- }
- })
- )
-}))
+import { useAssetsStore } from '@/stores/assetsStore'
vi.mock('@/i18n', () => ({
t: (key: string, params?: Record) =>
@@ -25,9 +12,15 @@ vi.mock('@/i18n', () => ({
d: (date: Date) => date.toLocaleDateString()
}))
-vi.mock('@/platform/assets/services/assetService', () => ({
- assetService: mockAssetService
-}))
+vi.mock('@/stores/assetsStore', () => {
+ const store = {
+ modelAssetsByNodeType: new Map(),
+ modelLoadingByNodeType: new Map(),
+ updateModelsForNodeType: vi.fn(),
+ updateModelsForTag: vi.fn()
+ }
+ return { useAssetsStore: () => store }
+})
vi.mock('@/stores/modelToNodeStore', () => ({
useModelToNodeStore: () => ({
@@ -190,9 +183,12 @@ describe('AssetBrowserModal', () => {
})
}
+ const mockStore = useAssetsStore()
+
beforeEach(() => {
- mockAssetService.getAssetsForNodeType.mockReset()
- mockAssetService.getAssetsByTag.mockReset()
+ vi.resetAllMocks()
+ mockStore.modelAssetsByNodeType.clear()
+ mockStore.modelLoadingByNodeType.clear()
})
describe('Integration with useAssetBrowser', () => {
@@ -201,7 +197,7 @@ describe('AssetBrowserModal', () => {
createTestAsset('asset1', 'Model A', 'checkpoints'),
createTestAsset('asset2', 'Model B', 'loras')
]
- mockAssetService.getAssetsForNodeType.mockResolvedValueOnce(assets)
+ mockStore.modelAssetsByNodeType.set('CheckpointLoaderSimple', assets)
const wrapper = createWrapper({ nodeType: 'CheckpointLoaderSimple' })
await flushPromises()
@@ -218,7 +214,7 @@ describe('AssetBrowserModal', () => {
createTestAsset('c1', 'model.safetensors', 'checkpoints'),
createTestAsset('l1', 'lora.pt', 'loras')
]
- mockAssetService.getAssetsForNodeType.mockResolvedValueOnce(assets)
+ mockStore.modelAssetsByNodeType.set('CheckpointLoaderSimple', assets)
const wrapper = createWrapper({
nodeType: 'CheckpointLoaderSimple',
@@ -234,31 +230,54 @@ describe('AssetBrowserModal', () => {
})
describe('Data fetching', () => {
- it('fetches assets for node type', async () => {
- mockAssetService.getAssetsForNodeType.mockResolvedValueOnce([])
-
+ it('triggers store refresh for node type on mount', async () => {
createWrapper({ nodeType: 'CheckpointLoaderSimple' })
await flushPromises()
- expect(mockAssetService.getAssetsForNodeType).toHaveBeenCalledWith(
+ expect(mockStore.updateModelsForNodeType).toHaveBeenCalledWith(
'CheckpointLoaderSimple'
)
})
- it('fetches assets for tag when node type not provided', async () => {
- mockAssetService.getAssetsByTag.mockResolvedValueOnce([])
+ it('displays cached assets immediately from store', async () => {
+ const assets = [createTestAsset('asset1', 'Cached Model', 'checkpoints')]
+ mockStore.modelAssetsByNodeType.set('CheckpointLoaderSimple', assets)
- createWrapper({ assetType: 'loras' })
+ const wrapper = createWrapper({ nodeType: 'CheckpointLoaderSimple' })
+
+ const assetGrid = wrapper.findComponent({ name: 'AssetGrid' })
+ const gridAssets = assetGrid.props('assets') as AssetItem[]
+
+ expect(gridAssets).toHaveLength(1)
+ expect(gridAssets[0].name).toBe('Cached Model')
+ })
+
+ it('triggers store refresh for asset type (tag) on mount', async () => {
+ createWrapper({ assetType: 'models' })
await flushPromises()
- expect(mockAssetService.getAssetsByTag).toHaveBeenCalledWith('loras')
+ expect(mockStore.updateModelsForTag).toHaveBeenCalledWith('models')
+ })
+
+ it('uses tag: prefix for cache key when assetType is provided', async () => {
+ const assets = [createTestAsset('asset1', 'Tagged Model', 'models')]
+ mockStore.modelAssetsByNodeType.set('tag:models', assets)
+
+ const wrapper = createWrapper({ assetType: 'models' })
+ await flushPromises()
+
+ const assetGrid = wrapper.findComponent({ name: 'AssetGrid' })
+ const gridAssets = assetGrid.props('assets') as AssetItem[]
+
+ expect(gridAssets).toHaveLength(1)
+ expect(gridAssets[0].name).toBe('Tagged Model')
})
})
describe('Asset Selection', () => {
it('emits asset-select event when asset is selected', async () => {
const assets = [createTestAsset('asset1', 'Model A', 'checkpoints')]
- mockAssetService.getAssetsForNodeType.mockResolvedValueOnce(assets)
+ mockStore.modelAssetsByNodeType.set('CheckpointLoaderSimple', assets)
const wrapper = createWrapper({ nodeType: 'CheckpointLoaderSimple' })
await flushPromises()
@@ -271,7 +290,7 @@ describe('AssetBrowserModal', () => {
it('executes onSelect callback when provided', async () => {
const assets = [createTestAsset('asset1', 'Model A', 'checkpoints')]
- mockAssetService.getAssetsForNodeType.mockResolvedValueOnce(assets)
+ mockStore.modelAssetsByNodeType.set('CheckpointLoaderSimple', assets)
const onSelect = vi.fn()
const wrapper = createWrapper({
@@ -289,8 +308,6 @@ describe('AssetBrowserModal', () => {
describe('Left Panel Conditional Logic', () => {
it('hides left panel by default when showLeftPanel is undefined', async () => {
- mockAssetService.getAssetsForNodeType.mockResolvedValueOnce([])
-
const wrapper = createWrapper({ nodeType: 'CheckpointLoaderSimple' })
await flushPromises()
@@ -299,8 +316,6 @@ describe('AssetBrowserModal', () => {
})
it('shows left panel when showLeftPanel prop is explicitly true', async () => {
- mockAssetService.getAssetsForNodeType.mockResolvedValueOnce([])
-
const wrapper = createWrapper({
nodeType: 'CheckpointLoaderSimple',
showLeftPanel: true
@@ -318,7 +333,7 @@ describe('AssetBrowserModal', () => {
createTestAsset('asset1', 'Model A', 'checkpoints'),
createTestAsset('asset2', 'Model B', 'loras')
]
- mockAssetService.getAssetsForNodeType.mockResolvedValueOnce(assets)
+ mockStore.modelAssetsByNodeType.set('CheckpointLoaderSimple', assets)
const wrapper = createWrapper({
nodeType: 'CheckpointLoaderSimple',
@@ -339,8 +354,6 @@ describe('AssetBrowserModal', () => {
describe('Title Management', () => {
it('passes custom title to BaseModalLayout when title prop provided', async () => {
- mockAssetService.getAssetsForNodeType.mockResolvedValueOnce([])
-
const wrapper = createWrapper({
nodeType: 'CheckpointLoaderSimple',
title: 'Custom Title'
@@ -353,7 +366,7 @@ describe('AssetBrowserModal', () => {
it('passes computed contentTitle to BaseModalLayout when no title prop', async () => {
const assets = [createTestAsset('asset1', 'Model A', 'checkpoints')]
- mockAssetService.getAssetsForNodeType.mockResolvedValueOnce(assets)
+ mockStore.modelAssetsByNodeType.set('CheckpointLoaderSimple', assets)
const wrapper = createWrapper({ nodeType: 'CheckpointLoaderSimple' })
await flushPromises()