Compare commits

..

4 Commits

Author SHA1 Message Date
github-actions
892a9b6068 Update locales [skip ci] 2025-06-11 15:47:53 +00:00
Yiqun Xu
dfc38083fe Merge branch 'main' into implement-versioned 2025-06-11 08:42:43 -07:00
Yiqun Xu
1f055686c3 test: test 2025-06-11 04:59:07 -07:00
Yiqun Xu
f145a89c66 feat: Implement versioned default settings system 2025-06-11 04:07:33 -07:00
53 changed files with 359 additions and 625 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.22.2",
"version": "1.22.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@comfyorg/comfyui-frontend",
"version": "1.22.2",
"version": "1.22.1",
"license": "GPL-3.0-only",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.22.2",
"version": "1.22.1",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",

View File

@@ -93,14 +93,7 @@
import { whenever } from '@vueuse/core'
import { merge } from 'lodash'
import Button from 'primevue/button'
import {
computed,
onBeforeUnmount,
onMounted,
onUnmounted,
ref,
watch
} from 'vue'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import ContentDivider from '@/components/common/ContentDivider.vue'
@@ -113,7 +106,6 @@ import PackCard from '@/components/dialog/content/manager/packCard/PackCard.vue'
import RegistrySearchBar from '@/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue'
import GridSkeleton from '@/components/dialog/content/manager/skeleton/GridSkeleton.vue'
import { useResponsiveCollapse } from '@/composables/element/useResponsiveCollapse'
import { useManagerStatePersistence } from '@/composables/manager/useManagerStatePersistence'
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus'
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
@@ -124,15 +116,13 @@ import type { TabItem } from '@/types/comfyManagerTypes'
import { ManagerTab } from '@/types/comfyManagerTypes'
import { components } from '@/types/comfyRegistryTypes'
const { initialTab } = defineProps<{
initialTab?: ManagerTab
const { initialTab = ManagerTab.All } = defineProps<{
initialTab: ManagerTab
}>()
const { t } = useI18n()
const comfyManagerStore = useComfyManagerStore()
const { getPackById } = useComfyRegistryStore()
const persistedState = useManagerStatePersistence()
const initialState = persistedState.loadStoredState()
const GRID_STYLE = {
display: 'grid',
@@ -166,10 +156,8 @@ const tabs = ref<TabItem[]>([
icon: 'pi-sync'
}
])
const initialTabId = initialTab ?? initialState.selectedTabId
const selectedTab = ref<TabItem>(
tabs.value.find((tab) => tab.id === initialTabId) || tabs.value[0]
tabs.value.find((tab) => tab.id === initialTab) || tabs.value[0]
)
const {
@@ -180,11 +168,7 @@ const {
searchMode,
sortField,
suggestions
} = useRegistrySearch({
initialSortField: initialState.sortField,
initialSearchMode: initialState.searchMode,
initialSearchQuery: initialState.searchQuery
})
} = useRegistrySearch()
pageNumber.value = 0
const onApproachEnd = () => {
pageNumber.value++
@@ -248,11 +232,7 @@ watch(
if (!isEmptySearch.value) {
displayPacks.value = filterOutdatedPacks(installedPacks.value)
} else if (
!installedPacks.value.length &&
!installedPacksReady.value &&
!isLoadingInstalled.value
) {
} else if (!installedPacks.value.length) {
await startFetchInstalled()
} else {
displayPacks.value = filterOutdatedPacks(installedPacks.value)
@@ -446,13 +426,7 @@ whenever(selectedNodePack, async () => {
if (data?.id === pack.id) {
lastFetchedPackId.value = pack.id
const mergedPack = merge({}, pack, data)
// Update the pack in current selection without changing selection state
const packIndex = selectedNodePacks.value.findIndex(
(p) => p.id === mergedPack.id
)
if (packIndex !== -1) {
selectedNodePacks.value.splice(packIndex, 1, mergedPack)
}
selectedNodePacks.value = [mergedPack]
// Replace pack in displayPacks so that children receive a fresh prop reference
const idx = displayPacks.value.findIndex((p) => p.id === mergedPack.id)
if (idx !== -1) {
@@ -472,15 +446,6 @@ watch(searchQuery, () => {
}
})
onBeforeUnmount(() => {
persistedState.persistState({
selectedTabId: selectedTab.value?.id,
searchQuery: searchQuery.value,
searchMode: searchMode.value,
sortField: sortField.value
})
})
onUnmounted(() => {
getPackById.cancel()
})

View File

@@ -62,7 +62,6 @@ describe('PackVersionBadge', () => {
return mount(PackVersionBadge, {
props: {
nodePack: mockNodePack,
isSelected: false,
...props
},
global: {
@@ -163,58 +162,4 @@ describe('PackVersionBadge', () => {
// Verify that the hide method was called
expect(mockHide).toHaveBeenCalled()
})
describe('selection state changes', () => {
it('closes the popover when card is deselected', async () => {
const wrapper = mountComponent({
props: { isSelected: true }
})
// Change isSelected from true to false
await wrapper.setProps({ isSelected: false })
await nextTick()
// Verify that the hide method was called
expect(mockHide).toHaveBeenCalled()
})
it('does not close the popover when card is selected', async () => {
const wrapper = mountComponent({
props: { isSelected: false }
})
// Change isSelected from false to true
await wrapper.setProps({ isSelected: true })
await nextTick()
// Verify that the hide method was NOT called
expect(mockHide).not.toHaveBeenCalled()
})
it('does not close the popover when isSelected remains false', async () => {
const wrapper = mountComponent({
props: { isSelected: false }
})
// Change isSelected from false to false (no change)
await wrapper.setProps({ isSelected: false })
await nextTick()
// Verify that the hide method was NOT called
expect(mockHide).not.toHaveBeenCalled()
})
it('does not close the popover when isSelected remains true', async () => {
const wrapper = mountComponent({
props: { isSelected: true }
})
// Change isSelected from true to true (no change)
await wrapper.setProps({ isSelected: true })
await nextTick()
// Verify that the hide method was NOT called
expect(mockHide).not.toHaveBeenCalled()
})
})
})

View File

@@ -33,7 +33,7 @@
<script setup lang="ts">
import Button from 'primevue/button'
import Popover from 'primevue/popover'
import { computed, ref, watch } from 'vue'
import { computed, ref } from 'vue'
import PackVersionSelectorPopover from '@/components/dialog/content/manager/PackVersionSelectorPopover.vue'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
@@ -43,9 +43,8 @@ import { isSemVer } from '@/utils/formatUtil'
const TRUNCATED_HASH_LENGTH = 7
const { nodePack, isSelected } = defineProps<{
const { nodePack } = defineProps<{
nodePack: components['schemas']['Node']
isSelected: boolean
}>()
const popoverRef = ref()
@@ -70,14 +69,4 @@ const toggleVersionSelector = (event: Event) => {
const closeVersionSelector = () => {
popoverRef.value.hide()
}
// If the card is unselected, automatically close the version selector popover
watch(
() => isSelected,
(isSelected, wasSelected) => {
if (wasSelected && !isSelected) {
closeVersionSelector()
}
}
)
</script>

View File

@@ -191,100 +191,6 @@ describe('PackVersionSelectorPopover', () => {
expect(mockGetPackVersions).toHaveBeenCalledWith(newNodePack.id)
})
describe('nodePack.id changes', () => {
it('re-fetches versions when nodePack.id changes', async () => {
// Set up the mock for the initial fetch
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
const wrapper = mountComponent()
await waitForPromises()
// Verify initial fetch
expect(mockGetPackVersions).toHaveBeenCalledTimes(1)
expect(mockGetPackVersions).toHaveBeenCalledWith(mockNodePack.id)
// Set up the mock for the second fetch
const newVersions = [
{ version: '2.0.0', createdAt: '2023-06-01' },
{ version: '1.9.0', createdAt: '2023-05-01' }
]
mockGetPackVersions.mockResolvedValueOnce(newVersions)
// Update the nodePack with a new ID
const newNodePack = {
...mockNodePack,
id: 'different-pack',
name: 'Different Pack'
}
await wrapper.setProps({ nodePack: newNodePack })
await waitForPromises()
// Should fetch versions for the new nodePack
expect(mockGetPackVersions).toHaveBeenCalledTimes(2)
expect(mockGetPackVersions).toHaveBeenLastCalledWith(newNodePack.id)
// Check that new versions are displayed
const listbox = wrapper.findComponent(Listbox)
const options = listbox.props('options')!
expect(options.some((o) => o.value === '2.0.0')).toBe(true)
expect(options.some((o) => o.value === '1.9.0')).toBe(true)
})
it('does not re-fetch when nodePack changes but id remains the same', async () => {
// Set up the mock for the initial fetch
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
const wrapper = mountComponent()
await waitForPromises()
// Verify initial fetch
expect(mockGetPackVersions).toHaveBeenCalledTimes(1)
// Update the nodePack with same ID but different properties
const updatedNodePack = {
...mockNodePack,
name: 'Updated Test Pack',
description: 'New description'
}
await wrapper.setProps({ nodePack: updatedNodePack })
await waitForPromises()
// Should NOT fetch versions again
expect(mockGetPackVersions).toHaveBeenCalledTimes(1)
})
it('maintains selected version when switching to a new pack', async () => {
// Set up the mock for the initial fetch
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
const wrapper = mountComponent()
await waitForPromises()
// Select a specific version
const listbox = wrapper.findComponent(Listbox)
await listbox.setValue('0.9.0')
expect(listbox.props('modelValue')).toBe('0.9.0')
// Set up the mock for the second fetch
mockGetPackVersions.mockResolvedValueOnce([
{ version: '3.0.0', createdAt: '2023-07-01' },
{ version: '0.9.0', createdAt: '2023-04-01' }
])
// Update to a new pack that also has version 0.9.0
const newNodePack = {
id: 'another-pack',
name: 'Another Pack',
latest_version: { version: '3.0.0' }
}
await wrapper.setProps({ nodePack: newNodePack })
await waitForPromises()
// Selected version should remain the same if available
expect(listbox.props('modelValue')).toBe('0.9.0')
})
})
describe('Unclaimed GitHub packs handling', () => {
it('falls back to nightly when no versions exist', async () => {
// Set up the mock to return versions

View File

@@ -62,7 +62,7 @@ import { whenever } from '@vueuse/core'
import Button from 'primevue/button'
import Listbox from 'primevue/listbox'
import ProgressSpinner from 'primevue/progressspinner'
import { onMounted, ref } from 'vue'
import { onMounted, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import ContentDivider from '@/components/common/ContentDivider.vue'
@@ -161,11 +161,9 @@ const onNodePackChange = async () => {
}
whenever(
() => nodePack.id,
(nodePackId, oldNodePackId) => {
if (nodePackId !== oldNodePackId) {
void onNodePackChange()
}
() => nodePack,
() => {
void onNodePackChange()
},
{ deep: true, immediate: true }
)
@@ -184,4 +182,8 @@ const handleSubmit = async () => {
isQueueing.value = false
emit('submit')
}
onUnmounted(() => {
managerStore.installPack.clear()
})
</script>

View File

@@ -32,7 +32,7 @@
/>
</MetadataRow>
<MetadataRow :label="t('manager.version')">
<PackVersionBadge :node-pack="nodePack" :is-selected="true" />
<PackVersionBadge :node-pack="nodePack" />
</MetadataRow>
</div>
<div class="mb-6 overflow-hidden">
@@ -118,15 +118,7 @@ const onNodePackChange = () => {
y.value = 0
}
whenever(
() => nodePack.id,
(nodePackId, oldNodePackId) => {
if (nodePackId !== oldNodePackId) {
onNodePackChange()
}
},
{ immediate: true }
)
whenever(() => nodePack, onNodePackChange, { immediate: true, deep: true })
</script>
<style scoped>
.hidden-scrollbar {

View File

@@ -51,7 +51,6 @@ const isLoading = ref(false)
const registryNodeDefs = shallowRef<ListComfyNodesResponse | null>(null)
const fetchNodeDefs = async () => {
getNodeDefs.cancel()
isLoading.value = true
const { id: packId } = nodePack

View File

@@ -1,37 +1,11 @@
<template>
<div :style="{ width: cssWidth, height: cssHeight }" class="overflow-hidden">
<!-- default banner show -->
<div v-if="showDefaultBanner" class="w-full h-full">
<img
:src="DEFAULT_BANNER"
alt="default banner"
class="w-full h-full object-cover"
/>
</div>
<!-- banner_url or icon show -->
<div v-else class="relative w-full h-full">
<!-- blur background -->
<div
v-if="imgSrc"
class="absolute inset-0 bg-cover bg-center bg-no-repeat opacity-30"
:style="{
backgroundImage: `url(${imgSrc})`,
filter: 'blur(10px)'
}"
></div>
<!-- image -->
<img
:src="isImageError ? DEFAULT_BANNER : imgSrc"
:alt="nodePack.name + ' banner'"
:class="
isImageError
? 'relative w-full h-full object-cover z-10'
: 'relative w-full h-full object-contain z-10'
"
@error="isImageError = true"
/>
</div>
</div>
<img
:src="isImageError ? DEFAULT_BANNER : imgSrc"
:alt="nodePack.name + ' banner'"
class="object-cover"
:style="{ width: cssWidth, height: cssHeight }"
@error="isImageError = true"
/>
</template>
<script setup lang="ts">
@@ -46,15 +20,18 @@ const {
width = '100%',
height = '12rem'
} = defineProps<{
nodePack: components['schemas']['Node']
nodePack: components['schemas']['Node'] & { banner?: string } // Temporary measure until banner is in backend
width?: string
height?: string
}>()
const isImageError = ref(false)
const showDefaultBanner = computed(() => !nodePack.banner_url && !nodePack.icon)
const imgSrc = computed(() => nodePack.banner_url || nodePack.icon)
const shouldShowFallback = computed(
() => !nodePack.banner || nodePack.banner.trim() === '' || isImageError.value
)
const imgSrc = computed(() =>
shouldShowFallback.value ? DEFAULT_BANNER : nodePack.banner
)
const convertToCssValue = (value: string | number) =>
typeof value === 'number' ? `${value}rem` : value

View File

@@ -1,13 +1,13 @@
<template>
<Card
class="w-full h-full inline-flex flex-col justify-between items-start overflow-hidden rounded-lg shadow-elevation-3 dark-theme:bg-dark-elevation-2 transition-all duration-200"
class="w-full h-full inline-flex flex-col justify-between items-start overflow-hidden rounded-2xl shadow-elevation-3 dark-theme:bg-dark-elevation-2 transition-all duration-200"
:class="{
'selected-card': isSelected,
'outline outline-[6px] outline-[var(--p-primary-color)]': isSelected,
'opacity-60': isDisabled
}"
:pt="{
body: { class: 'p-0 flex flex-col w-full h-full rounded-lg gap-0' },
content: { class: 'flex-1 flex flex-col rounded-lg min-h-0' },
body: { class: 'p-0 flex flex-col w-full h-full rounded-2xl gap-0' },
content: { class: 'flex-1 flex flex-col rounded-2xl min-h-0' },
title: { class: 'w-full h-full rounded-t-lg cursor-pointer' },
footer: { class: 'p-0 m-0' }
}"
@@ -70,10 +70,7 @@
>
<i class="pi pi-arrow-circle-up text-blue-600" />
</div>
<PackVersionBadge
:node-pack="nodePack"
:is-selected="isSelected"
/>
<PackVersionBadge :node-pack="nodePack" />
</div>
<div
v-if="formattedLatestVersionDate"
@@ -116,15 +113,11 @@ import PackBanner from '@/components/dialog/content/manager/packBanner/PackBanne
import PackCardFooter from '@/components/dialog/content/manager/packCard/PackCardFooter.vue'
import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import {
IsInstallingKey,
type MergedNodePack,
type RegistryPack,
isMergedNodePack
} from '@/types/comfyManagerTypes'
import { IsInstallingKey } from '@/types/comfyManagerTypes'
import type { components } from '@/types/comfyRegistryTypes'
const { nodePack, isSelected = false } = defineProps<{
nodePack: MergedNodePack | RegistryPack
nodePack: components['schemas']['Node']
isSelected?: boolean
}>()
@@ -143,9 +136,9 @@ const isDisabled = computed(
whenever(isInstalled, () => (isInstalling.value = false))
const nodesCount = computed(() =>
isMergedNodePack(nodePack) ? nodePack.comfy_nodes?.length : undefined
)
// TODO: remove type assertion once comfy_nodes is added to node (pack) info type in backend
const nodesCount = computed(() => (nodePack as any).comfy_nodes?.length)
const publisherName = computed(() => {
if (!nodePack) return null
@@ -161,22 +154,3 @@ const formattedLatestVersionDate = computed(() => {
})
})
</script>
<style scoped>
.selected-card {
position: relative;
}
.selected-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 3px solid var(--p-primary-color);
border-radius: 0.5rem;
pointer-events: none;
z-index: 100;
}
</style>

View File

@@ -6,8 +6,7 @@
<i class="pi pi-download text-muted"></i>
<span>{{ formattedDownloads }}</span>
</div>
<PackInstallButton v-if="!isInstalled" :node-packs="[nodePack]" />
<PackEnableToggle v-else :node-pack="nodePack" />
<PackInstallButton :node-packs="[nodePack]" />
</div>
</template>
@@ -15,18 +14,13 @@
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import PackEnableToggle from '@/components/dialog/content/manager/button/PackEnableToggle.vue'
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import type { components } from '@/types/comfyRegistryTypes'
const { nodePack } = defineProps<{
nodePack: components['schemas']['Node']
}>()
const { isPackInstalled } = useComfyManagerStore()
const isInstalled = computed(() => isPackInstalled(nodePack?.id))
const { n } = useI18n()
const formattedDownloads = computed(() =>

View File

@@ -56,7 +56,7 @@ import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import SearchFilterDropdown from '@/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue'
import type { NodesIndexSuggestion } from '@/types/algoliaTypes'
import type { NodesIndexSuggestion } from '@/services/algoliaSearchService'
import {
type SearchOption,
SortableAlgoliaField

View File

@@ -86,6 +86,7 @@ import { useSettingStore } from '@/stores/settingStore'
import { useToastStore } from '@/stores/toastStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { getCurrentVersion } from '@/utils/versioning'
const emit = defineEmits<{
ready: []
@@ -300,6 +301,13 @@ onMounted(async () => {
CORE_SETTINGS.forEach((setting) => {
settingStore.addSetting(setting)
})
if (!settingStore.get('Comfy.InstalledVersion')) {
const currentVersion =
Object.keys(settingStore.settingValues).length > 0
? '0.0.1'
: getCurrentVersion()
await settingStore.set('Comfy.InstalledVersion', currentVersion)
}
// @ts-expect-error fixme ts strict error
await comfyApp.setup(canvasRef.value)
canvasStore.canvas = comfyApp.canvas

View File

@@ -1,54 +0,0 @@
import {
ManagerState,
ManagerTab,
SortableAlgoliaField
} from '@/types/comfyManagerTypes'
const STORAGE_KEY = 'Comfy.Manager.UI.State'
export const useManagerStatePersistence = () => {
/**
* Load the UI state from localStorage.
*/
const loadStoredState = (): ManagerState => {
try {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
return JSON.parse(stored)
}
} catch (e) {
console.error('Failed to load manager UI state:', e)
}
return {
selectedTabId: ManagerTab.All,
searchQuery: '',
searchMode: 'packs',
sortField: SortableAlgoliaField.Downloads
}
}
/**
* Persist the UI state to localStorage.
*/
const persistState = (state: ManagerState) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
}
/**
* Reset the UI state to the default values.
*/
const reset = () => {
persistState({
selectedTabId: ManagerTab.All,
searchQuery: '',
searchMode: 'packs',
sortField: SortableAlgoliaField.Downloads
})
}
return {
loadStoredState,
persistState,
reset
}
}

View File

@@ -658,19 +658,19 @@ export function useCoreCommands(): ComfyCommand[] {
{
id: 'Comfy.Manager.CustomNodesManager',
icon: 'pi pi-puzzle',
label: 'Toggle the Custom Nodes Manager',
label: 'Custom Nodes Manager',
versionAdded: '1.12.10',
function: () => {
dialogService.toggleManagerDialog()
dialogService.showManagerDialog()
}
},
{
id: 'Comfy.Manager.ToggleManagerProgressDialog',
icon: 'pi pi-spinner',
label: 'Toggle the Custom Nodes Manager Progress Bar',
label: 'Toggle Progress Dialog',
versionAdded: '1.13.9',
function: () => {
dialogService.toggleManagerProgressDialog()
dialogService.showManagerProgressDialog()
}
},
{

View File

@@ -15,10 +15,7 @@ export const useProgressFavicon = () => {
if (isIdle) {
favicon.value = defaultFavicon
} else {
const frame = Math.min(
Math.max(0, Math.floor(progress * totalFrames)),
totalFrames - 1
)
const frame = Math.floor(progress * totalFrames)
favicon.value = `/assets/images/favicon_progress_16x16/frame_${frame}.png`
}
}

View File

@@ -1,19 +1,20 @@
import { watchDebounced } from '@vueuse/core'
import type { Hit } from 'algoliasearch/dist/lite/browser'
import { memoize, orderBy } from 'lodash'
import { computed, ref, watch } from 'vue'
import { computed, onUnmounted, ref, watch } from 'vue'
import { useAlgoliaSearchService } from '@/services/algoliaSearchService'
import type {
import {
AlgoliaNodePack,
NodesIndexSuggestion,
SearchAttribute
} from '@/types/algoliaTypes'
SearchAttribute,
useAlgoliaSearchService
} from '@/services/algoliaSearchService'
import type { NodesIndexSuggestion } from '@/services/algoliaSearchService'
import { SortableAlgoliaField } from '@/types/comfyManagerTypes'
const SEARCH_DEBOUNCE_TIME = 320
const DEFAULT_PAGE_SIZE = 64
const DEFAULT_SORT_FIELD = SortableAlgoliaField.Downloads // Set in the index configuration
const DEFAULT_MAX_CACHE_SIZE = 64
const SORT_DIRECTIONS: Record<SortableAlgoliaField, 'asc' | 'desc'> = {
[SortableAlgoliaField.Downloads]: 'desc',
[SortableAlgoliaField.Created]: 'desc',
@@ -29,25 +30,18 @@ const isDateField = (field: SortableAlgoliaField): boolean =>
/**
* Composable for managing UI state of Comfy Node Registry search.
*/
export function useRegistrySearch(options: {
initialSortField?: SortableAlgoliaField
initialSearchMode?: 'nodes' | 'packs'
initialSearchQuery?: string
initialPageNumber?: number
}) {
const {
initialSortField = SortableAlgoliaField.Downloads,
initialSearchMode = 'packs',
initialSearchQuery = '',
initialPageNumber = 0
} = options
export function useRegistrySearch(
options: {
maxCacheSize?: number
} = {}
) {
const { maxCacheSize = DEFAULT_MAX_CACHE_SIZE } = options
const isLoading = ref(false)
const sortField = ref<SortableAlgoliaField>(initialSortField)
const searchMode = ref<'nodes' | 'packs'>(initialSearchMode)
const sortField = ref<SortableAlgoliaField>(SortableAlgoliaField.Downloads)
const searchMode = ref<'nodes' | 'packs'>('packs')
const pageSize = ref(DEFAULT_PAGE_SIZE)
const pageNumber = ref(initialPageNumber)
const searchQuery = ref(initialSearchQuery)
const pageNumber = ref(0)
const searchQuery = ref('')
const results = ref<AlgoliaNodePack[]>([])
const suggestions = ref<NodesIndexSuggestion[]>([])
@@ -68,7 +62,9 @@ export function useRegistrySearch(options: {
)
const { searchPacksCached, toRegistryPack, clearSearchPacksCache } =
useAlgoliaSearchService()
useAlgoliaSearchService({
maxCacheSize
})
const algoliaToRegistry = memoize(
toRegistryPack,
@@ -128,6 +124,8 @@ export function useRegistrySearch(options: {
immediate: true
})
onUnmounted(clearSearchPacksCache)
return {
isLoading,
pageNumber,
@@ -137,7 +135,6 @@ export function useRegistrySearch(options: {
searchQuery,
suggestions,
searchResults: resultsAsRegistryPacks,
nodeSearchResults: resultsAsNodes,
clearCache: clearSearchPacksCache
nodeSearchResults: resultsAsNodes
}
}

View File

@@ -35,7 +35,10 @@ export const CORE_SETTINGS: SettingParams[] = [
name: 'Action on link release (No modifier)',
type: 'combo',
options: Object.values(LinkReleaseTriggerAction),
defaultValue: LinkReleaseTriggerAction.CONTEXT_MENU
defaultValue: LinkReleaseTriggerAction.CONTEXT_MENU,
defaultsByInstallVersion: {
'1.21.3': LinkReleaseTriggerAction.SEARCH_BOX
}
},
{
id: 'Comfy.LinkRelease.ActionShift',
@@ -747,6 +750,13 @@ export const CORE_SETTINGS: SettingParams[] = [
defaultValue: false,
versionAdded: '1.8.7'
},
{
id: 'Comfy.InstalledVersion',
name: 'Installed version',
type: 'hidden',
defaultValue: null,
versionAdded: '1.22.1'
},
{
id: 'LiteGraph.ContextMenu.Scaling',
name: 'Scale node combo widget menus (lists) when zoomed in',

View File

@@ -43,15 +43,6 @@ import { checkMirrorReachable } from '@/utils/networkUtil'
defaultValue: true,
onChange: onChangeRestartApp
},
{
id: 'Comfy-Desktop.LaunchOptions',
category: ['Comfy-Desktop', 'General', 'Launch Options'],
name: 'Custom Launch Arguments',
tooltip:
'Custom command line arguments for launching the backend (e.g., --cpu). Requires restart.',
type: 'text',
defaultValue: ''
},
{
id: 'Comfy-Desktop.WindowStyle',
category: ['Comfy-Desktop', 'General', 'Window Style'],

View File

@@ -5,7 +5,7 @@ import { EventManagerInterface, PreviewManagerInterface } from './interfaces'
export class PreviewManager implements PreviewManagerInterface {
previewCamera: THREE.Camera
previewContainer: HTMLDivElement = null!
previewContainer: HTMLDivElement = {} as HTMLDivElement
showPreview: boolean = true
previewWidth: number = 120

View File

@@ -147,10 +147,10 @@
"label": "Load Default Workflow"
},
"Comfy_Manager_CustomNodesManager": {
"label": "Toggle the Custom Nodes Manager"
"label": "Custom Nodes Manager"
},
"Comfy_Manager_ToggleManagerProgressDialog": {
"label": "Toggle the Custom Nodes Manager Progress Bar"
"label": "Toggle Progress Dialog"
},
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "Open Mask Editor for Selected Node"

View File

@@ -828,8 +828,8 @@
"ComfyUI Issues": "ComfyUI Issues",
"Interrupt": "Interrupt",
"Load Default Workflow": "Load Default Workflow",
"Toggle the Custom Nodes Manager": "Toggle the Custom Nodes Manager",
"Toggle the Custom Nodes Manager Progress Bar": "Toggle the Custom Nodes Manager Progress Bar",
"Custom Nodes Manager": "Custom Nodes Manager",
"Toggle Progress Dialog": "Toggle Progress Dialog",
"Open Mask Editor for Selected Node": "Open Mask Editor for Selected Node",
"New": "New",
"Clipspace": "Clipspace",

View File

@@ -2,10 +2,6 @@
"Comfy-Desktop_AutoUpdate": {
"name": "Automatically check for updates"
},
"Comfy-Desktop_LaunchOptions": {
"name": "Custom Launch Arguments",
"tooltip": "Custom command line arguments for launching the backend (e.g., --cpu). Requires restart."
},
"Comfy-Desktop_SendStatistics": {
"name": "Send anonymous usage metrics"
},

View File

@@ -694,6 +694,7 @@
"ComfyUI Issues": "Problemas de ComfyUI",
"Contact Support": "Contactar soporte",
"Convert selected nodes to group node": "Convertir nodos seleccionados en nodo de grupo",
"Custom Nodes Manager": "Gestor de nodos personalizados",
"Delete Selected Items": "Eliminar elementos seleccionados",
"Desktop User Guide": "Guía de usuario de escritorio",
"Duplicate Current Workflow": "Duplicar flujo de trabajo actual",
@@ -747,13 +748,12 @@
"Toggle Logs Bottom Panel": "Alternar panel inferior de registros",
"Toggle Model Library Sidebar": "Alternar barra lateral de biblioteca de modelos",
"Toggle Node Library Sidebar": "Alternar barra lateral de biblioteca de nodos",
"Toggle Progress Dialog": "Alternar diálogo de progreso",
"Toggle Queue Sidebar": "Alternar barra lateral de cola",
"Toggle Search Box": "Alternar caja de búsqueda",
"Toggle Terminal Bottom Panel": "Alternar panel inferior de terminal",
"Toggle Theme (Dark/Light)": "Alternar tema (Oscuro/Claro)",
"Toggle Workflows Sidebar": "Alternar barra lateral de flujos de trabajo",
"Toggle the Custom Nodes Manager": "Alternar el Administrador de Nodos Personalizados",
"Toggle the Custom Nodes Manager Progress Bar": "Alternar la Barra de Progreso del Administrador de Nodos Personalizados",
"Undo": "Deshacer",
"Ungroup selected group nodes": "Desagrupar nodos de grupo seleccionados",
"Workflow": "Flujo de trabajo",

View File

@@ -2,10 +2,6 @@
"Comfy-Desktop_AutoUpdate": {
"name": "Verificar actualizaciones automáticamente"
},
"Comfy-Desktop_LaunchOptions": {
"name": "Argumentos de lanzamiento personalizados",
"tooltip": "Argumentos personalizados de línea de comandos para iniciar el backend (por ejemplo, --cpu). Requiere reinicio."
},
"Comfy-Desktop_SendStatistics": {
"name": "Enviar métricas de uso anónimas"
},

View File

@@ -694,6 +694,7 @@
"ComfyUI Issues": "Problèmes de ComfyUI",
"Contact Support": "Contacter le support",
"Convert selected nodes to group node": "Convertir les nœuds sélectionnés en nœud de groupe",
"Custom Nodes Manager": "Gestionnaire de Nœuds Personnalisés",
"Delete Selected Items": "Supprimer les éléments sélectionnés",
"Desktop User Guide": "Guide de l'utilisateur de bureau",
"Duplicate Current Workflow": "Dupliquer le flux de travail actuel",
@@ -747,13 +748,12 @@
"Toggle Logs Bottom Panel": "Basculer le panneau inférieur des journaux",
"Toggle Model Library Sidebar": "Basculer la barre latérale de la bibliothèque de modèles",
"Toggle Node Library Sidebar": "Basculer la barre latérale de la bibliothèque de nœuds",
"Toggle Progress Dialog": "Basculer la boîte de dialogue de progression",
"Toggle Queue Sidebar": "Basculer la barre latérale de la file d'attente",
"Toggle Search Box": "Basculer la boîte de recherche",
"Toggle Terminal Bottom Panel": "Basculer le panneau inférieur du terminal",
"Toggle Theme (Dark/Light)": "Basculer le thème (Sombre/Clair)",
"Toggle Workflows Sidebar": "Basculer la barre latérale des flux de travail",
"Toggle the Custom Nodes Manager": "Basculer le gestionnaire de nœuds personnalisés",
"Toggle the Custom Nodes Manager Progress Bar": "Basculer la barre de progression du gestionnaire de nœuds personnalisés",
"Undo": "Annuler",
"Ungroup selected group nodes": "Dégrouper les nœuds de groupe sélectionnés",
"Workflow": "Flux de travail",

View File

@@ -2,10 +2,6 @@
"Comfy-Desktop_AutoUpdate": {
"name": "Vérifier automatiquement les mises à jour"
},
"Comfy-Desktop_LaunchOptions": {
"name": "Arguments de lancement personnalisés",
"tooltip": "Arguments de ligne de commande personnalisés pour lancer le backend (par exemple, --cpu). Nécessite un redémarrage."
},
"Comfy-Desktop_SendStatistics": {
"name": "Envoyer des métriques d'utilisation anonymes"
},

View File

@@ -694,6 +694,7 @@
"ComfyUI Issues": "ComfyUIの問題",
"Contact Support": "サポートに連絡",
"Convert selected nodes to group node": "選択したノードをグループノードに変換",
"Custom Nodes Manager": "カスタムノードマネージャ",
"Delete Selected Items": "選択したアイテムを削除",
"Desktop User Guide": "デスクトップユーザーガイド",
"Duplicate Current Workflow": "現在のワークフローを複製",
@@ -708,10 +709,10 @@
"Interrupt": "中断",
"Load Default Workflow": "デフォルトワークフローを読み込む",
"Manage group nodes": "グループノードを管理",
"Move Selected Nodes Down": "選択したノードを下移動",
"Move Selected Nodes Left": "選択したノードを左移動",
"Move Selected Nodes Right": "選択したノードを右移動",
"Move Selected Nodes Up": "選択したノードを上移動",
"Move Selected Nodes Down": "選択したノードを下移動",
"Move Selected Nodes Left": "選択したノードを左移動",
"Move Selected Nodes Right": "選択したノードを右移動",
"Move Selected Nodes Up": "選択したノードを上移動",
"Mute/Unmute Selected Nodes": "選択したノードのミュート/ミュート解除",
"New": "新規",
"Next Opened Workflow": "次に開いたワークフロー",
@@ -747,13 +748,12 @@
"Toggle Logs Bottom Panel": "ログパネル下部を切り替え",
"Toggle Model Library Sidebar": "モデルライブラリサイドバーを切り替え",
"Toggle Node Library Sidebar": "ノードライブラリサイドバーを切り替え",
"Toggle Progress Dialog": "進行状況ダイアログの切り替え",
"Toggle Queue Sidebar": "キューサイドバーを切り替え",
"Toggle Search Box": "検索ボックスの切り替え",
"Toggle Terminal Bottom Panel": "ターミナルパネル下部を切り替え",
"Toggle Theme (Dark/Light)": "テーマを切り替え(ダーク/ライト)",
"Toggle Workflows Sidebar": "ワークフローサイドバーを切り替え",
"Toggle the Custom Nodes Manager": "カスタムノードマネージャーを切り替え",
"Toggle the Custom Nodes Manager Progress Bar": "カスタムノードマネージャーの進行状況バーを切り替え",
"Undo": "元に戻す",
"Ungroup selected group nodes": "選択したグループノードのグループ解除",
"Workflow": "ワークフロー",

View File

@@ -2,10 +2,6 @@
"Comfy-Desktop_AutoUpdate": {
"name": "自動的に更新を確認する"
},
"Comfy-Desktop_LaunchOptions": {
"name": "カスタム起動引数",
"tooltip": "バックエンドを起動するためのカスタムコマンドライン引数(例: --cpu。再起動が必要です。"
},
"Comfy-Desktop_SendStatistics": {
"name": "匿名の使用統計を送信する"
},

View File

@@ -694,6 +694,7 @@
"ComfyUI Issues": "ComfyUI 이슈 페이지",
"Contact Support": "고객 지원 문의",
"Convert selected nodes to group node": "선택한 노드를 그룹 노드로 변환",
"Custom Nodes Manager": "사용자 정의 노드 관리자",
"Delete Selected Items": "선택한 항목 삭제",
"Desktop User Guide": "데스크톱 사용자 가이드",
"Duplicate Current Workflow": "현재 워크플로 복제",
@@ -747,13 +748,12 @@
"Toggle Logs Bottom Panel": "로그 하단 패널 전환",
"Toggle Model Library Sidebar": "모델 라이브러리 사이드바 전환",
"Toggle Node Library Sidebar": "노드 라이브러리 사이드바 전환",
"Toggle Progress Dialog": "진행 상황 대화 상자 전환",
"Toggle Queue Sidebar": "실행 대기열 사이드바 전환",
"Toggle Search Box": "검색 상자 전환",
"Toggle Terminal Bottom Panel": "터미널 하단 패널 전환",
"Toggle Theme (Dark/Light)": "테마 전환 (어두운/밝은)",
"Toggle Workflows Sidebar": "워크플로 사이드바 전환",
"Toggle the Custom Nodes Manager": "커스텀 노드 매니저 전환",
"Toggle the Custom Nodes Manager Progress Bar": "커스텀 노드 매니저 진행률 표시줄 전환",
"Undo": "실행 취소",
"Ungroup selected group nodes": "선택한 그룹 노드 그룹 해제",
"Workflow": "워크플로",

View File

@@ -2,10 +2,6 @@
"Comfy-Desktop_AutoUpdate": {
"name": "자동 업데이트 확인"
},
"Comfy-Desktop_LaunchOptions": {
"name": "사용자 지정 실행 인수",
"tooltip": "백엔드 실행을 위한 사용자 지정 명령줄 인수입니다(예: --cpu). 재시작이 필요합니다."
},
"Comfy-Desktop_SendStatistics": {
"name": "익명 사용 통계 보내기"
},

View File

@@ -694,6 +694,7 @@
"ComfyUI Issues": "Проблемы ComfyUI",
"Contact Support": "Связаться с поддержкой",
"Convert selected nodes to group node": "Преобразовать выбранные ноды в групповую ноду",
"Custom Nodes Manager": "Менеджер Пользовательских Узлов",
"Delete Selected Items": "Удалить выбранные элементы",
"Desktop User Guide": "Руководство пользователя для настольных ПК",
"Duplicate Current Workflow": "Дублировать текущий рабочий процесс",
@@ -747,13 +748,12 @@
"Toggle Logs Bottom Panel": "Переключение нижней панели журналов",
"Toggle Model Library Sidebar": "Переключение боковой панели библиотеки моделей",
"Toggle Node Library Sidebar": "Переключение боковой панели библиотеки нод",
"Toggle Progress Dialog": "Переключить диалоговое окно прогресса",
"Toggle Queue Sidebar": "Переключение боковой панели очереди",
"Toggle Search Box": "Переключить поисковую панель",
"Toggle Terminal Bottom Panel": "Переключение нижней панели терминала",
"Toggle Theme (Dark/Light)": "Переключение темы (Тёмная/Светлая)",
"Toggle Workflows Sidebar": "Переключение боковой панели рабочих процессов",
"Toggle the Custom Nodes Manager": "Переключить менеджер пользовательских узлов",
"Toggle the Custom Nodes Manager Progress Bar": "Переключить индикатор выполнения менеджера пользовательских узлов",
"Undo": "Отменить",
"Ungroup selected group nodes": "Разгруппировать выбранные групповые ноды",
"Workflow": "Рабочий процесс",

View File

@@ -2,10 +2,6 @@
"Comfy-Desktop_AutoUpdate": {
"name": "Автоматически проверять обновления"
},
"Comfy-Desktop_LaunchOptions": {
"name": "Пользовательские параметры запуска",
"tooltip": "Пользовательские аргументы командной строки для запуска backend (например, --cpu). Требуется перезапуск."
},
"Comfy-Desktop_SendStatistics": {
"name": "Отправлять анонимную статистику использования"
},

View File

@@ -45,7 +45,7 @@
"label": "适应视图到选中节点"
},
"Comfy_Canvas_MoveSelectedNodes_Down": {
"label": "下移选中的节点"
"label": "下移选节点"
},
"Comfy_Canvas_MoveSelectedNodes_Left": {
"label": "向左移动选中节点"
@@ -54,7 +54,7 @@
"label": "向右移动选中节点"
},
"Comfy_Canvas_MoveSelectedNodes_Up": {
"label": "上移选中的节点"
"label": "上移选节点"
},
"Comfy_Canvas_ResetView": {
"label": "重置视图"

View File

@@ -694,6 +694,7 @@
"ComfyUI Issues": "ComfyUI 问题",
"Contact Support": "联系支持",
"Convert selected nodes to group node": "将选中节点转换为组节点",
"Custom Nodes Manager": "自定义节点管理器",
"Delete Selected Items": "删除选定的项目",
"Desktop User Guide": "桌面端用户指南",
"Duplicate Current Workflow": "复制当前工作流",
@@ -747,13 +748,12 @@
"Toggle Logs Bottom Panel": "切换日志底部面板",
"Toggle Model Library Sidebar": "切换模型库侧边栏",
"Toggle Node Library Sidebar": "切换节点库侧边栏",
"Toggle Progress Dialog": "切换进度对话框",
"Toggle Queue Sidebar": "切换队列侧边栏",
"Toggle Search Box": "切换搜索框",
"Toggle Terminal Bottom Panel": "切换终端底部面板",
"Toggle Theme (Dark/Light)": "切换主题(暗/亮)",
"Toggle Workflows Sidebar": "切换工作流侧边栏",
"Toggle the Custom Nodes Manager": "切换自定义节点管理器",
"Toggle the Custom Nodes Manager Progress Bar": "切换自定义节点管理器进度条",
"Undo": "撤销",
"Ungroup selected group nodes": "解散选中组节点",
"Workflow": "工作流",

View File

@@ -2,10 +2,6 @@
"Comfy-Desktop_AutoUpdate": {
"name": "自动检查更新"
},
"Comfy-Desktop_LaunchOptions": {
"name": "自定义启动参数",
"tooltip": "用于启动后端的自定义命令行参数(例如:--cpu。需要重启。"
},
"Comfy-Desktop_SendStatistics": {
"name": "发送匿名使用情况统计"
},

View File

@@ -448,9 +448,9 @@ const zSettings = z.object({
'Comfy.Toast.DisableReconnectingToast': z.boolean(),
'Comfy.Workflow.Persist': z.boolean(),
'Comfy.TutorialCompleted': z.boolean(),
'Comfy.InstalledVersion': z.string().nullable(),
'Comfy.Node.AllowImageSizeDraw': z.boolean(),
'Comfy-Desktop.AutoUpdate': z.boolean(),
'Comfy-Desktop.LaunchOptions': z.string(),
'Comfy-Desktop.SendStatistics': z.boolean(),
'Comfy-Desktop.WindowStyle': z.string(),
'Comfy-Desktop.UV.PythonInstallMirror': z.string(),
@@ -472,6 +472,7 @@ const zSettings = z.object({
'VHS.AdvancedPreviews': z.string(),
/** Settings used for testing */
'test.setting': z.any(),
'test.versionedSetting': z.any(),
'main.sub.setting.name': z.any(),
'single.setting': z.any(),
'LiteGraph.Node.DefaultPadding': z.boolean(),

View File

@@ -1,26 +1,68 @@
import QuickLRU from '@alloc/quick-lru'
import type {
BaseSearchParamsWithoutQuery,
Hit,
SearchQuery,
SearchResponse
} from 'algoliasearch/dist/lite/browser'
import { liteClient as algoliasearch } from 'algoliasearch/dist/lite/builds/browser'
import { omit } from 'lodash'
import type {
AlgoliaNodePack,
NodesIndexSuggestion,
SearchAttribute,
SearchNodePacksParams,
SearchPacksResult
} from '@/types/algoliaTypes'
import type { components } from '@/types/comfyRegistryTypes'
import { components } from '@/types/comfyRegistryTypes'
import { paramsToCacheKey } from '@/utils/formatUtil'
type RegistryNodePack = components['schemas']['Node']
const DEFAULT_MAX_CACHE_SIZE = 64
const DEFAULT_MIN_CHARS_FOR_SUGGESTIONS = 2
type SafeNestedProperty<
T,
K1 extends keyof T,
K2 extends keyof NonNullable<T[K1]>
> = T[K1] extends undefined | null ? undefined : NonNullable<T[K1]>[K2]
type RegistryNodePack = components['schemas']['Node']
type SearchPacksResult = {
nodePacks: Hit<AlgoliaNodePack>[]
querySuggestions: Hit<NodesIndexSuggestion>[]
}
export interface AlgoliaNodePack {
objectID: RegistryNodePack['id']
name: RegistryNodePack['name']
publisher_id: SafeNestedProperty<RegistryNodePack, 'publisher', 'id'>
description: RegistryNodePack['description']
comfy_nodes: string[]
total_install: RegistryNodePack['downloads']
id: RegistryNodePack['id']
create_time: string
update_time: SafeNestedProperty<
RegistryNodePack,
'latest_version',
'createdAt'
>
license: RegistryNodePack['license']
repository_url: RegistryNodePack['repository']
status: RegistryNodePack['status']
latest_version: SafeNestedProperty<
RegistryNodePack,
'latest_version',
'version'
>
latest_version_status: SafeNestedProperty<
RegistryNodePack,
'latest_version',
'status'
>
comfy_node_extract_status: SafeNestedProperty<
RegistryNodePack,
'latest_version',
'comfy_node_extract_status'
>
icon_url: RegistryNodePack['icon']
}
export type SearchAttribute = keyof AlgoliaNodePack
const RETRIEVE_ATTRIBUTES: SearchAttribute[] = [
'comfy_nodes',
'name',
@@ -39,7 +81,33 @@ const RETRIEVE_ATTRIBUTES: SearchAttribute[] = [
'icon_url'
]
export interface NodesIndexSuggestion {
nb_words: number
nodes_index: {
exact_nb_hits: number
facets: {
exact_matches: Record<string, number>
analytics: Record<string, any>
}
}
objectID: RegistryNodePack['id']
popularity: number
query: string
}
type SearchNodePacksParams = BaseSearchParamsWithoutQuery & {
pageSize: number
pageNumber: number
restrictSearchableAttributes: SearchAttribute[]
}
interface AlgoliaSearchServiceOptions {
/**
* Maximum number of search results to store in the cache.
* The cache is automatically cleared when the component is unmounted.
* @default 64
*/
maxCacheSize?: number
/**
* Minimum number of characters for suggestions. An additional query
* will be made to the suggestions/completions index for queries that
@@ -49,15 +117,17 @@ interface AlgoliaSearchServiceOptions {
minCharsForSuggestions?: number
}
const searchPacksCache = new QuickLRU<string, SearchPacksResult>({
maxSize: DEFAULT_MAX_CACHE_SIZE
})
export const useAlgoliaSearchService = (
options: AlgoliaSearchServiceOptions = {}
) => {
const { minCharsForSuggestions = DEFAULT_MIN_CHARS_FOR_SUGGESTIONS } = options
const {
maxCacheSize = DEFAULT_MAX_CACHE_SIZE,
minCharsForSuggestions = DEFAULT_MIN_CHARS_FOR_SUGGESTIONS
} = options
const searchClient = algoliasearch(__ALGOLIA_APP_ID__, __ALGOLIA_API_KEY__)
const searchPacksCache = new QuickLRU<string, SearchPacksResult>({
maxSize: maxCacheSize
})
const toRegistryLatestVersion = (
algoliaNode: AlgoliaNodePack

View File

@@ -21,6 +21,7 @@ import TemplateWorkflowsDialogHeader from '@/components/templates/TemplateWorkfl
import { t } from '@/i18n'
import type { ExecutionErrorWsMessage } from '@/schemas/apiSchema'
import { type ShowDialogOptions, useDialogStore } from '@/stores/dialogStore'
import { ManagerTab } from '@/types/comfyManagerTypes'
export type ConfirmationDialogType =
| 'default'
@@ -128,14 +129,16 @@ export const useDialogService = () => {
}
function showManagerDialog(
props: InstanceType<typeof ManagerDialogContent>['$props'] = {}
props: InstanceType<typeof ManagerDialogContent>['$props'] = {
initialTab: ManagerTab.All
}
) {
dialogStore.showDialog({
key: 'global-manager',
component: ManagerDialogContent,
headerComponent: ManagerHeader,
dialogComponentProps: {
closable: true,
closable: false,
pt: {
header: { class: '!p-0 !m-0' },
content: { class: '!px-0 h-[83vh] w-[90vw] overflow-y-hidden' }
@@ -394,26 +397,6 @@ export const useDialogService = () => {
}
}
function toggleManagerDialog(
props?: InstanceType<typeof ManagerDialogContent>['$props']
) {
if (dialogStore.isDialogOpen('global-manager')) {
dialogStore.closeDialog({ key: 'global-manager' })
} else {
showManagerDialog(props)
}
}
function toggleManagerProgressDialog(
props?: InstanceType<typeof ManagerProgressDialogContent>['$props']
) {
if (dialogStore.isDialogOpen('global-manager-progress-dialog')) {
dialogStore.closeDialog({ key: 'global-manager-progress-dialog' })
} else {
showManagerProgressDialog({ props })
}
}
return {
showLoadWorkflowWarning,
showMissingModelsWarning,
@@ -431,8 +414,6 @@ export const useDialogService = () => {
showUpdatePasswordDialog,
showExtensionDialog,
prompt,
confirm,
toggleManagerDialog,
toggleManagerProgressDialog
confirm
}
}

View File

@@ -169,16 +169,11 @@ export const useDialogStore = defineStore('dialog', () => {
return dialog
}
function isDialogOpen(key: string) {
return dialogStack.value.some((d) => d.key === key)
}
return {
dialogStack,
riseDialog,
showDialog,
closeDialog,
showExtensionDialog,
isDialogOpen
showExtensionDialog
}
})

View File

@@ -88,30 +88,54 @@ export const useExecutionStore = defineStore('execution', () => {
if (!activePrompt.value) return 0
const total = totalNodesToExecute.value
const done = nodesExecuted.value
return total > 0 ? done / total : 0
return done / total
})
function bindExecutionEvents() {
api.addEventListener('execution_start', handleExecutionStart)
api.addEventListener('execution_cached', handleExecutionCached)
api.addEventListener('executed', handleExecuted)
api.addEventListener('executing', handleExecuting)
api.addEventListener('progress', handleProgress)
api.addEventListener('status', handleStatus)
api.addEventListener('execution_error', handleExecutionError)
api.addEventListener(
'execution_start',
handleExecutionStart as EventListener
)
api.addEventListener(
'execution_cached',
handleExecutionCached as EventListener
)
api.addEventListener('executed', handleExecuted as EventListener)
api.addEventListener('executing', handleExecuting as EventListener)
api.addEventListener('progress', handleProgress as EventListener)
api.addEventListener('status', handleStatus as EventListener)
api.addEventListener(
'execution_error',
handleExecutionError as EventListener
)
}
api.addEventListener('progress_text', handleProgressText)
api.addEventListener('display_component', handleDisplayComponent)
api.addEventListener('progress_text', handleProgressText as EventListener)
api.addEventListener(
'display_component',
handleDisplayComponent as EventListener
)
function unbindExecutionEvents() {
api.removeEventListener('execution_start', handleExecutionStart)
api.removeEventListener('execution_cached', handleExecutionCached)
api.removeEventListener('executed', handleExecuted)
api.removeEventListener('executing', handleExecuting)
api.removeEventListener('progress', handleProgress)
api.removeEventListener('status', handleStatus)
api.removeEventListener('execution_error', handleExecutionError)
api.removeEventListener('progress_text', handleProgressText)
api.removeEventListener(
'execution_start',
handleExecutionStart as EventListener
)
api.removeEventListener(
'execution_cached',
handleExecutionCached as EventListener
)
api.removeEventListener('executed', handleExecuted as EventListener)
api.removeEventListener('executing', handleExecuting as EventListener)
api.removeEventListener('progress', handleProgress as EventListener)
api.removeEventListener('status', handleStatus as EventListener)
api.removeEventListener(
'execution_error',
handleExecutionError as EventListener
)
api.removeEventListener(
'progress_text',
handleProgressText as EventListener
)
}
function handleExecutionStart(e: CustomEvent<ExecutionStartWsMessage>) {
@@ -160,7 +184,7 @@ export const useExecutionStore = defineStore('execution', () => {
clientId.value = api.clientId
// Once we've received the clientId we no longer need to listen
api.removeEventListener('status', handleStatus)
api.removeEventListener('status', handleStatus as EventListener)
}
}

View File

@@ -7,6 +7,7 @@ import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import type { SettingParams } from '@/types/settingTypes'
import type { TreeNode } from '@/types/treeExplorerTypes'
import { compareVersions } from '@/utils/versioning'
export const getSettingInfo = (setting: SettingParams) => {
const parts = setting.category || setting.id.split('.')
@@ -83,6 +84,29 @@ export const useSettingStore = defineStore('setting', () => {
*/
function getDefaultValue<K extends keyof Settings>(key: K): Settings[K] {
const param = settingsById.value[key]
// Check for versioned defaults based on installation version
if (param?.defaultsByInstallVersion) {
const installedVersion = get('Comfy.InstalledVersion')
if (installedVersion) {
// Find the highest version that is <= installedVersion
const sortedVersions = Object.keys(param.defaultsByInstallVersion).sort(
(a, b) => compareVersions(a, b)
)
for (const version of sortedVersions.reverse()) {
if (compareVersions(installedVersion, version) >= 0) {
const versionedDefault = param.defaultsByInstallVersion[version]
return typeof versionedDefault === 'function'
? versionedDefault()
: versionedDefault
}
}
}
}
// Fall back to original defaultValue
return typeof param?.defaultValue === 'function'
? param.defaultValue()
: param?.defaultValue

View File

@@ -1,74 +0,0 @@
import type {
BaseSearchParamsWithoutQuery,
Hit
} from 'algoliasearch/dist/lite/browser'
import type { components } from '@/types/comfyRegistryTypes'
type SafeNestedProperty<
T,
K1 extends keyof T,
K2 extends keyof NonNullable<T[K1]>
> = T[K1] extends undefined | null ? undefined : NonNullable<T[K1]>[K2]
type RegistryNodePack = components['schemas']['Node']
export type SearchPacksResult = {
nodePacks: Hit<AlgoliaNodePack>[]
querySuggestions: Hit<NodesIndexSuggestion>[]
}
export interface AlgoliaNodePack {
objectID: RegistryNodePack['id']
name: RegistryNodePack['name']
publisher_id: SafeNestedProperty<RegistryNodePack, 'publisher', 'id'>
description: RegistryNodePack['description']
comfy_nodes: string[]
total_install: RegistryNodePack['downloads']
id: RegistryNodePack['id']
create_time: string
update_time: SafeNestedProperty<
RegistryNodePack,
'latest_version',
'createdAt'
>
license: RegistryNodePack['license']
repository_url: RegistryNodePack['repository']
status: RegistryNodePack['status']
latest_version: SafeNestedProperty<
RegistryNodePack,
'latest_version',
'version'
>
latest_version_status: SafeNestedProperty<
RegistryNodePack,
'latest_version',
'status'
>
comfy_node_extract_status: SafeNestedProperty<
RegistryNodePack,
'latest_version',
'comfy_node_extract_status'
>
icon_url: RegistryNodePack['icon']
}
export type SearchAttribute = keyof AlgoliaNodePack
export interface NodesIndexSuggestion {
nb_words: number
nodes_index: {
exact_nb_hits: number
facets: {
exact_matches: Record<string, number>
analytics: Record<string, any>
}
}
objectID: RegistryNodePack['id']
popularity: number
query: string
}
export type SearchNodePacksParams = BaseSearchParamsWithoutQuery & {
pageSize: number
pageNumber: number
restrictSearchableAttributes: SearchAttribute[]
}

View File

@@ -1,17 +1,10 @@
import type { InjectionKey, Ref } from 'vue'
import type { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
import type { AlgoliaNodePack } from '@/types/algoliaTypes'
import type { components } from '@/types/comfyRegistryTypes'
type RegistryPack = components['schemas']['Node']
type WorkflowNodeProperties = ComfyWorkflowJSON['nodes'][0]['properties']
export type RegistryPack = components['schemas']['Node']
export type MergedNodePack = RegistryPack & AlgoliaNodePack
export const isMergedNodePack = (
nodePack: RegistryPack | AlgoliaNodePack
): nodePack is MergedNodePack => 'comfy_nodes' in nodePack
export type PackField = keyof RegistryPack | null
export const IsInstallingKey: InjectionKey<Ref<boolean>> =
@@ -39,7 +32,7 @@ export enum SortableAlgoliaField {
}
export interface TabItem {
id: ManagerTab
id: string
label: string
icon: string
}
@@ -234,10 +227,3 @@ export interface InstallPackParams extends ManagerPackInfo {
export interface UpdateAllPacksParams {
mode?: ManagerDatabaseSource
}
export interface ManagerState {
selectedTabId: ManagerTab
searchQuery: string
searchMode: 'nodes' | 'packs'
sortField: SortableAlgoliaField
}

View File

@@ -35,6 +35,8 @@ export interface Setting {
export interface SettingParams extends FormItem {
id: keyof Settings
defaultValue: any | (() => any)
// Optional versioned defaults based on installation version
defaultsByInstallVersion?: Record<string, any | (() => any)>
onChange?: (newValue: any, oldValue?: any) => void
// By default category is id.split('.'). However, changing id to assign
// new category has poor backward compatibility. Use this field to overwrite

View File

@@ -4,12 +4,12 @@ import type { InjectionKey, ModelRef } from 'vue'
export interface TreeNode extends PrimeVueTreeNode {
label: string
children?: this[]
children?: TreeNode[]
}
export interface TreeExplorerNode<T = any> extends TreeNode {
data?: T
children?: this[]
children?: TreeExplorerNode<T>[]
icon?: string
/**
* Function to override what icon to use for the node.
@@ -62,7 +62,7 @@ export interface TreeExplorerNode<T = any> extends TreeNode {
}
export interface RenderedTreeExplorerNode<T = any> extends TreeExplorerNode<T> {
children?: this[]
children?: RenderedTreeExplorerNode<T>[]
icon: string
type: 'folder' | 'node'
/** Total number of leaves in the subtree */

View File

@@ -61,8 +61,8 @@ const mergeNumericInputSpec = <T extends IntInputSpec | FloatInputSpec>(
}
return mergeCommonInputSpec(
[type, { ...options1, ...mergedOptions }] as T,
[type, { ...options2, ...mergedOptions }] as T
[type, { ...options1, ...mergedOptions }] as unknown as T,
[type, { ...options2, ...mergedOptions }] as unknown as T
)
}
@@ -84,8 +84,8 @@ const mergeComboInputSpec = <T extends ComboInputSpec | ComboInputSpecV2>(
}
return mergeCommonInputSpec(
['COMBO', { ...options1, options: intersection }] as T,
['COMBO', { ...options2, options: intersection }] as T
['COMBO', { ...options1, options: intersection }] as unknown as T,
['COMBO', { ...options2, options: intersection }] as unknown as T
)
}
@@ -107,7 +107,9 @@ const mergeCommonInputSpec = <T extends InputSpec>(
return value1 === value2 || (_.isNil(value1) && _.isNil(value2))
})
return mergeIsValid ? ([type, { ...options1, ...options2 }] as T) : null
return mergeIsValid
? ([type, { ...options1, ...options2 }] as unknown as T)
: null
}
/**

View File

@@ -116,7 +116,7 @@ export const findNodeByKey = <T extends TreeNode>(
return null
}
for (const child of root.children) {
const result = findNodeByKey(child, key)
const result = findNodeByKey(child as T, key)
if (result) {
return result
}
@@ -130,11 +130,11 @@ export const findNodeByKey = <T extends TreeNode>(
* @returns A deep clone of the node.
*/
export function cloneTree<T extends TreeNode>(node: T): T {
const clone = { ...node }
const clone: T = { ...node } as T
// Clone children recursively
if (node.children && node.children.length > 0) {
clone.children = node.children.map((child) => cloneTree(child))
clone.children = node.children.map((child) => cloneTree(child as T))
}
return clone

37
src/utils/versioning.ts Normal file
View File

@@ -0,0 +1,37 @@
import { electronAPI, isElectron } from '@/utils/envUtil'
/**
* Compare two semantic version strings.
* @param a - First version string
* @param b - Second version string
* @returns 0 if equal, 1 if a > b, -1 if a < b
*/
export function compareVersions(a: string, b: string): number {
const parseVersion = (version: string) => {
return version.split('.').map((v) => parseInt(v, 10) || 0)
}
const versionA = parseVersion(a)
const versionB = parseVersion(b)
for (let i = 0; i < Math.max(versionA.length, versionB.length); i++) {
const numA = versionA[i] || 0
const numB = versionB[i] || 0
if (numA > numB) return 1
if (numA < numB) return -1
}
return 0
}
/**
* Get the current ComfyUI version for version tracking
*/
export function getCurrentVersion(): string {
if (isElectron()) {
return electronAPI().getComfyUIVersion()
}
// For web version, fallback to frontend version
return __COMFYUI_FRONTEND_VERSION__
}

View File

@@ -122,6 +122,26 @@ describe('useSettingStore', () => {
expect(store.get('test.setting')).toBe('default')
})
it('should use versioned default based on installation version', () => {
// Set up an installed version
store.settingValues['Comfy.InstalledVersion'] = '1.25.0'
const setting: SettingParams = {
id: 'test.versionedSetting',
name: 'test.versionedSetting',
type: 'text',
defaultValue: 'original',
defaultsByInstallVersion: {
'1.20.0': 'version_1_20',
'1.24.0': 'version_1_24'
}
}
store.addSetting(setting)
// Should use the highest version <= installed version
expect(store.get('test.versionedSetting')).toBe('version_1_24')
})
it('should set value and trigger onChange', async () => {
const onChangeMock = vi.fn()
const dispatchChangeMock = vi.mocked(app.ui.settings.dispatchChange)

View File

@@ -3,9 +3,10 @@ import dotenv from 'dotenv'
import IconsResolver from 'unplugin-icons/resolver'
import Icons from 'unplugin-icons/vite'
import Components from 'unplugin-vue-components/vite'
import { type UserConfig, defineConfig } from 'vite'
import { defineConfig } from 'vite'
import { createHtmlPlugin } from 'vite-plugin-html'
import vueDevTools from 'vite-plugin-vue-devtools'
import type { UserConfigExport } from 'vitest/config'
import {
addElementVnodeExportPlugin,
@@ -153,4 +154,4 @@ export default defineConfig({
optimizeDeps: {
exclude: ['@comfyorg/litegraph', '@comfyorg/comfyui-electron-types']
}
}) satisfies UserConfig as UserConfig
}) as UserConfigExport

View File

@@ -1,5 +1,6 @@
import { Plugin, defineConfig } from 'vite'
import { mergeConfig } from 'vite'
import type { UserConfig } from 'vitest/config'
import baseConfig from './vite.config.mts'
@@ -82,7 +83,7 @@ const mockElectronAPI: Plugin = {
}
export default mergeConfig(
baseConfig,
baseConfig as unknown as UserConfig,
defineConfig({
plugins: [mockElectronAPI]
})