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

View File

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

View File

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

View File

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

View File

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