Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -547,13 +547,15 @@ const navigationFilteredTemplates = computed(() => {
return workflowTemplatesStore.filterTemplatesByCategory(selectedNavItem.value)
})

// Template filtering
// Template filtering with scope awareness
const {
searchQuery,
selectedModels,
selectedUseCases,
selectedRunsOn,
sortBy,
activeModels,
activeUseCases,
filteredTemplates,
availableModels,
availableUseCases,
Expand All @@ -562,7 +564,7 @@ const {
totalCount,
resetFilters,
loadFuseOptions
} = useTemplateFiltering(navigationFilteredTemplates)
} = useTemplateFiltering(navigationFilteredTemplates, selectedNavItem)

/**
* Coordinates state between the selected navigation item and the sort order to
Expand Down Expand Up @@ -595,9 +597,11 @@ watch(selectedNavItem, () => coordinateNavAndSort('nav'))
watch(sortBy, () => coordinateNavAndSort('sort'))

// Convert between string array and object array for MultiSelect component
// Only show selected items that exist in the current scope
const selectedModelObjects = computed({
get() {
return selectedModels.value.map((model) => ({ name: model, value: model }))
// Only include selected models that exist in availableModels
return activeModels.value.map((model) => ({ name: model, value: model }))
},
set(value: { name: string; value: string }[]) {
selectedModels.value = value.map((item) => item.value)
Expand All @@ -606,7 +610,7 @@ const selectedModelObjects = computed({

const selectedUseCaseObjects = computed({
get() {
return selectedUseCases.value.map((useCase) => ({
return activeUseCases.value.map((useCase) => ({
name: useCase,
value: useCase
}))
Expand Down
83 changes: 83 additions & 0 deletions src/composables/useTemplateFiltering.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,4 +395,87 @@ describe('useTemplateFiltering', () => {
expect(mockGetFuseOptions).toHaveBeenCalledTimes(1)
})
})

describe('Scope-aware filtering', () => {
it('filters out inactive models when scope changes', () => {
// Start with image templates only
const templates = ref<TemplateInfo[]>([
{
name: 'flux-template',
description: 'Flux model template',
models: ['Flux', 'Dall-E'],
mediaType: 'image',
mediaSubtype: 'png'
}
])

const currentScope = ref('image')

const {
selectedModels,
activeModels,
inactiveModels,
filteredTemplates
} = useTemplateFiltering(templates, currentScope)

// Select models from both image and video domains
selectedModels.value = ['Flux', 'Luma']

// In image scope, only Flux should be active because Luma doesn't exist in any image template
expect(activeModels.value).toEqual(['Flux'])
expect(inactiveModels.value).toEqual(['Luma'])
expect(filteredTemplates.value).toHaveLength(1)
expect(filteredTemplates.value[0].name).toBe('flux-template')

// Switch to video scope with only video templates
currentScope.value = 'video'
templates.value = [
{
name: 'luma-template',
description: 'Luma video template',
models: ['Luma', 'Runway'],
mediaType: 'video',
mediaSubtype: 'mp4'
}
]

// In video scope, only Luma should be active because Flux doesn't exist in any video template
expect(activeModels.value).toEqual(['Luma'])
expect(inactiveModels.value).toEqual(['Flux'])
expect(filteredTemplates.value).toHaveLength(1)
expect(filteredTemplates.value[0].name).toBe('luma-template')
})

it('maintains selected filters across scope changes', () => {
const templates = ref<TemplateInfo[]>([
{
name: 'template1',
description: 'Template 1',
models: ['Model1'],
mediaType: 'image',
mediaSubtype: 'png'
}
])

const currentScope = ref('image')
const { selectedModels, activeModels } = useTemplateFiltering(
templates,
currentScope
)

// Select a model
selectedModels.value = ['Model1', 'Model2']

// Model1 is active, Model2 is not available
expect(activeModels.value).toEqual(['Model1'])
expect(selectedModels.value).toEqual(['Model1', 'Model2'])

// Change scope - selected models should persist
currentScope.value = 'video'
templates.value = []

expect(selectedModels.value).toEqual(['Model1', 'Model2'])
expect(activeModels.value).toEqual([])
})
})
})
66 changes: 56 additions & 10 deletions src/composables/useTemplateFiltering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ const defaultFuseOptions: IFuseOptions<TemplateInfo> = {
}

export function useTemplateFiltering(
templates: Ref<TemplateInfo[]> | TemplateInfo[]
templates: Ref<TemplateInfo[]> | TemplateInfo[],
currentScope?: Ref<string | null>
) {
const settingStore = useSettingStore()
const rankingStore = useTemplateRankingStore()
Expand Down Expand Up @@ -84,6 +85,40 @@ export function useTemplateFiltering(
return ['ComfyUI', 'External or Remote API']
})

// Compute which selected filters are actually applicable to the current scope
const activeModels = computed(() => {
if (!currentScope) {
return selectedModels.value
}
return selectedModels.value.filter((model) =>
availableModels.value.includes(model)
)
})

const activeUseCases = computed(() => {
if (!currentScope) {
return selectedUseCases.value
}
return selectedUseCases.value.filter((useCase) =>
availableUseCases.value.includes(useCase)
)
})

// Track which filters are inactive (selected but not applicable)
const inactiveModels = computed(() => {
if (!currentScope) return []
return selectedModels.value.filter(
(model) => !availableModels.value.includes(model)
)
})

const inactiveUseCases = computed(() => {
if (!currentScope) return []
return selectedUseCases.value.filter(
(useCase) => !availableUseCases.value.includes(useCase)
)
})

const debouncedSearchQuery = refThrottled(searchQuery, 50)

const filteredBySearch = computed(() => {
Expand All @@ -96,36 +131,39 @@ export function useTemplateFiltering(
})

const filteredByModels = computed(() => {
if (selectedModels.value.length === 0) {
// Use active models instead of selected models for filtering
if (activeModels.value.length === 0) {
return filteredBySearch.value
}

return filteredBySearch.value.filter((template) => {
if (!template.models || !Array.isArray(template.models)) {
return false
}
return selectedModels.value.some((selectedModel) =>
template.models?.includes(selectedModel)
return activeModels.value.some((activeModel) =>
template.models?.includes(activeModel)
)
})
})

const filteredByUseCases = computed(() => {
if (selectedUseCases.value.length === 0) {
// Use active use cases instead of selected use cases for filtering
if (activeUseCases.value.length === 0) {
return filteredByModels.value
}

return filteredByModels.value.filter((template) => {
if (!template.tags || !Array.isArray(template.tags)) {
return false
}
return selectedUseCases.value.some((selectedTag) =>
template.tags?.includes(selectedTag)
return activeUseCases.value.some((activeUseCase) =>
template.tags?.includes(activeUseCase)
)
})
})

const filteredByRunsOn = computed(() => {
// RunsOn filters are scope-independent
if (selectedRunsOn.value.length === 0) {
return filteredByUseCases.value
}
Expand All @@ -137,10 +175,10 @@ export function useTemplateFiltering(
const isExternalAPI = template.openSource === false
const isComfyUI = template.openSource !== false

return selectedRunsOn.value.some((selectedRunsOn) => {
if (selectedRunsOn === 'External or Remote API') {
return selectedRunsOn.value.some((runsOn) => {
if (runsOn === 'External or Remote API') {
return isExternalAPI
} else if (selectedRunsOn === 'ComfyUI') {
} else if (runsOn === 'ComfyUI') {
return isComfyUI
}
return false
Expand Down Expand Up @@ -343,6 +381,14 @@ export function useTemplateFiltering(
selectedRunsOn,
sortBy,

// Computed - Active filters (actually applied)
activeModels,
activeUseCases,

// Computed - Inactive filters (selected but not applicable)
inactiveModels,
inactiveUseCases,

// Computed
filteredTemplates,
availableModels,
Expand Down