[feat] Rename license filter to 'Runs On' filter in template selector (#6543)

## Summary

Renamed the templates license filter to better reflect its actual
purpose - showing where a template executes (locally in ComfyUI vs
external/remote API).

The current "License" filter has been causing confusion with model
licensing terms (e.g., Apache vs flux-dev licensing). This PR clarifies
the filter's purpose by renaming it to "Runs On" and updating the
options to be more descriptive of inference location.

<img width="196" height="230" alt="image"
src="https://github.com/user-attachments/assets/8cbea263-f399-4945-82c1-357ec185f5a7"
/>

<img width="861" height="597" alt="image"
src="https://github.com/user-attachments/assets/af116876-d7a5-49c5-b791-1fda637ff3a3"
/>


## Changes

- **Filter name**: "License" → "Runs On"
- **Filter options**: 
  - "Open Source" → "ComfyUI"
  - "Closed Source (API Nodes)" → "External or Remote API"
- **Icon**: Changed from `file-text` to `server` for better visual
representation
- **Variable naming**: Updated all related variables, types, and tests
to use `runsOn` naming convention
- **Telemetry**: Updated metadata to track `selected_runs_on` instead of
`selected_licenses`

## Why "Runs On"?

- **Clear intent**: Users want to know if a template runs locally or
requires an API call
- **Avoids confusion**: Separates the concept from model licensing terms
- **Inclusive wording**: "Remote" is included alongside "API" to help
users who may not be familiar with API terminology
- **Cloud-agnostic**: "Runs On" works whether the app itself is running
locally or in the cloud

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6543-feat-Rename-license-filter-to-Runs-On-filter-in-template-selector-29f6d73d3650811f935bc1f3fce7d7ad)
by [Unito](https://www.unito.io)
This commit is contained in:
Christian Byrne
2025-11-02 16:35:42 -08:00
committed by GitHub
parent 8dfdac3fc4
commit 8df0a3885d
5 changed files with 49 additions and 52 deletions

View File

@@ -68,17 +68,17 @@
</template>
</MultiSelect>
<!-- License Filter -->
<!-- Runs On Filter -->
<MultiSelect
v-model="selectedLicenseObjects"
:label="licenseFilterLabel"
:options="licenseOptions"
v-model="selectedRunsOnObjects"
:label="runsOnFilterLabel"
:options="runsOnOptions"
:show-search-box="true"
:show-selected-count="true"
:show-clear-button="true"
>
<template #icon>
<i class="icon-[lucide--file-text]" />
<i class="icon-[lucide--server]" />
</template>
</MultiSelect>
</div>
@@ -528,12 +528,12 @@ const {
searchQuery,
selectedModels,
selectedUseCases,
selectedLicenses,
selectedRunsOn,
sortBy,
filteredTemplates,
availableModels,
availableUseCases,
availableLicenses,
availableRunsOn,
filteredCount,
totalCount,
resetFilters
@@ -561,15 +561,15 @@ const selectedUseCaseObjects = computed({
}
})
const selectedLicenseObjects = computed({
const selectedRunsOnObjects = computed({
get() {
return selectedLicenses.value.map((license) => ({
name: license,
value: license
return selectedRunsOn.value.map((runsOn) => ({
name: runsOn,
value: runsOn
}))
},
set(value: { name: string; value: string }[]) {
selectedLicenses.value = value.map((item) => item.value)
selectedRunsOn.value = value.map((item) => item.value)
}
})
@@ -602,10 +602,10 @@ const useCaseOptions = computed(() =>
}))
)
const licenseOptions = computed(() =>
availableLicenses.value.map((license) => ({
name: license,
value: license
const runsOnOptions = computed(() =>
availableRunsOn.value.map((runsOn) => ({
name: runsOn,
value: runsOn
}))
)
@@ -634,14 +634,14 @@ const useCaseFilterLabel = computed(() => {
}
})
const licenseFilterLabel = computed(() => {
if (selectedLicenseObjects.value.length === 0) {
return t('templateWorkflows.licenseFilter', 'License')
} else if (selectedLicenseObjects.value.length === 1) {
return selectedLicenseObjects.value[0].name
const runsOnFilterLabel = computed(() => {
if (selectedRunsOnObjects.value.length === 0) {
return t('templateWorkflows.runsOnFilter', 'Runs On')
} else if (selectedRunsOnObjects.value.length === 1) {
return selectedRunsOnObjects.value[0].name
} else {
return t('templateWorkflows.licensesSelected', {
count: selectedLicenseObjects.value.length
return t('templateWorkflows.runsOnSelected', {
count: selectedRunsOnObjects.value.length
})
}
})
@@ -708,7 +708,7 @@ watch(
sortBy,
selectedModels,
selectedUseCases,
selectedLicenses
selectedRunsOn
],
() => {
resetPagination()

View File

@@ -13,7 +13,7 @@ export function useTemplateFiltering(
const searchQuery = ref('')
const selectedModels = ref<string[]>([])
const selectedUseCases = ref<string[]>([])
const selectedLicenses = ref<string[]>([])
const selectedRunsOn = ref<string[]>([])
const sortBy = ref<
| 'default'
| 'alphabetical'
@@ -63,8 +63,8 @@ export function useTemplateFiltering(
return Array.from(tagSet).sort()
})
const availableLicenses = computed(() => {
return ['Open Source', 'Closed Source (API Nodes)']
const availableRunsOn = computed(() => {
return ['ComfyUI', 'External or Remote API']
})
const debouncedSearchQuery = refDebounced(searchQuery, 50)
@@ -108,21 +108,21 @@ export function useTemplateFiltering(
})
})
const filteredByLicenses = computed(() => {
if (selectedLicenses.value.length === 0) {
const filteredByRunsOn = computed(() => {
if (selectedRunsOn.value.length === 0) {
return filteredByUseCases.value
}
return filteredByUseCases.value.filter((template) => {
// Check if template has API in its tags or name (indicating it's a closed source API node)
// Check if template has API in its tags or name (indicating it runs on external/remote API)
const isApiTemplate =
template.tags?.includes('API') ||
template.name?.toLowerCase().includes('api_')
return selectedLicenses.value.some((selectedLicense) => {
if (selectedLicense === 'Closed Source (API Nodes)') {
return selectedRunsOn.value.some((selectedRunsOn) => {
if (selectedRunsOn === 'External or Remote API') {
return isApiTemplate
} else if (selectedLicense === 'Open Source') {
} else if (selectedRunsOn === 'ComfyUI') {
return !isApiTemplate
}
return false
@@ -142,7 +142,7 @@ export function useTemplateFiltering(
}
const sortedTemplates = computed(() => {
const templates = [...filteredByLicenses.value]
const templates = [...filteredByRunsOn.value]
switch (sortBy.value) {
case 'alphabetical':
@@ -195,7 +195,7 @@ export function useTemplateFiltering(
searchQuery.value = ''
selectedModels.value = []
selectedUseCases.value = []
selectedLicenses.value = []
selectedRunsOn.value = []
sortBy.value = 'default'
}
@@ -207,8 +207,8 @@ export function useTemplateFiltering(
selectedUseCases.value = selectedUseCases.value.filter((t) => t !== tag)
}
const removeLicenseFilter = (license: string) => {
selectedLicenses.value = selectedLicenses.value.filter((l) => l !== license)
const removeRunsOnFilter = (runsOn: string) => {
selectedRunsOn.value = selectedRunsOn.value.filter((r) => r !== runsOn)
}
const filteredCount = computed(() => filteredTemplates.value.length)
@@ -220,7 +220,7 @@ export function useTemplateFiltering(
search_query: searchQuery.value || undefined,
selected_models: selectedModels.value,
selected_use_cases: selectedUseCases.value,
selected_licenses: selectedLicenses.value,
selected_runs_on: selectedRunsOn.value,
sort_by: sortBy.value,
filtered_count: filteredCount.value,
total_count: totalCount.value
@@ -229,14 +229,14 @@ export function useTemplateFiltering(
// Watch for filter changes and track them
watch(
[searchQuery, selectedModels, selectedUseCases, selectedLicenses, sortBy],
[searchQuery, selectedModels, selectedUseCases, selectedRunsOn, sortBy],
() => {
// Only track if at least one filter is active (to avoid tracking initial state)
const hasActiveFilters =
searchQuery.value.trim() !== '' ||
selectedModels.value.length > 0 ||
selectedUseCases.value.length > 0 ||
selectedLicenses.value.length > 0 ||
selectedRunsOn.value.length > 0 ||
sortBy.value !== 'default'
if (hasActiveFilters) {
@@ -251,14 +251,14 @@ export function useTemplateFiltering(
searchQuery,
selectedModels,
selectedUseCases,
selectedLicenses,
selectedRunsOn,
sortBy,
// Computed
filteredTemplates,
availableModels,
availableUseCases,
availableLicenses,
availableRunsOn,
filteredCount,
totalCount,
@@ -266,6 +266,6 @@ export function useTemplateFiltering(
resetFilters,
removeModelFilter,
removeUseCaseFilter,
removeLicenseFilter
removeRunsOnFilter
}
}

View File

@@ -766,7 +766,7 @@
"modelFilter": "Model Filter",
"modelsSelected": "{count} Models",
"useCasesSelected": "{count} Use Cases",
"licensesSelected": "{count} Licenses",
"runsOnSelected": "{count} Runs On",
"resultsCount": "Showing {count} of {total} templates",
"sort": {
"recommended": "Recommended",

View File

@@ -190,7 +190,7 @@ export interface TemplateFilterMetadata {
search_query?: string
selected_models: string[]
selected_use_cases: string[]
selected_licenses: string[]
selected_runs_on: string[]
sort_by:
| 'default'
| 'alphabetical'

View File

@@ -101,11 +101,11 @@ describe('useTemplateFiltering', () => {
searchQuery,
selectedModels,
selectedUseCases,
selectedLicenses,
selectedRunsOn,
filteredTemplates,
availableModels,
availableUseCases,
availableLicenses,
availableRunsOn,
filteredCount,
totalCount,
removeUseCaseFilter,
@@ -120,10 +120,7 @@ describe('useTemplateFiltering', () => {
'Portrait',
'Video'
])
expect(availableLicenses.value).toEqual([
'Open Source',
'Closed Source (API Nodes)'
])
expect(availableRunsOn.value).toEqual(['ComfyUI', 'External or Remote API'])
searchQuery.value = 'enterprise'
await nextTick()
@@ -133,7 +130,7 @@ describe('useTemplateFiltering', () => {
'api-template'
])
selectedLicenses.value = ['Closed Source (API Nodes)']
selectedRunsOn.value = ['External or Remote API']
await nextTick()
expect(filteredTemplates.value.map((template) => template.name)).toEqual([
'api-template'