diff --git a/.github/workflows/i18n-update-core.yaml b/.github/workflows/i18n-update-core.yaml index 0ceaf7397..bdf58b52f 100644 --- a/.github/workflows/i18n-update-core.yaml +++ b/.github/workflows/i18n-update-core.yaml @@ -41,7 +41,7 @@ jobs: env: PLAYWRIGHT_TEST_URL: http://localhost:5173 - name: Update translations - run: pnpm locale + run: pnpm locale && pnpm format env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - name: Commit updated locales diff --git a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png index eaa359731..c7334eb16 100644 Binary files a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png and b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-empty-canvas-mobile-chrome-linux.png b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-empty-canvas-mobile-chrome-linux.png index ff608be64..e3acabfcb 100644 Binary files a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-empty-canvas-mobile-chrome-linux.png and b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-empty-canvas-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png b/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png index f1d41cf13..915fb36d9 100644 Binary files a/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png and b/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png index 390def0a2..f4bac78b7 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png and b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png differ diff --git a/docs/testing/unit-testing.md b/docs/testing/unit-testing.md index a17592643..9be403659 100644 --- a/docs/testing/unit-testing.md +++ b/docs/testing/unit-testing.md @@ -11,6 +11,7 @@ This guide covers patterns and examples for unit testing utilities, composables, 5. [Mocking Utility Functions](#mocking-utility-functions) 6. [Testing with Debounce and Throttle](#testing-with-debounce-and-throttle) 7. [Mocking Node Definitions](#mocking-node-definitions) +8. [Mocking Composables with Reactive State](#mocking-composables-with-reactive-state) ## Testing Vue Composables with Reactivity @@ -253,3 +254,79 @@ it('should validate node definition', () => { expect(validateComfyNodeDef(EXAMPLE_NODE_DEF)).not.toBeNull() }) ``` + +## Mocking Composables with Reactive State + +When mocking composables that return reactive refs, define the mock implementation inline in `vi.mock()`'s factory function. This ensures stable singleton instances across all test invocations. + +### Rules + +1. **Define mocks in the factory function** — Create `vi.fn()` and `ref()` instances directly inside `vi.mock()`, not in `beforeEach` +2. **Use singleton pattern** — The factory runs once; all calls to the composable return the same mock object +3. **Access mocks per-test** — Call the composable directly in each test to get the singleton instance rather than storing in a shared variable +4. **Wrap in `vi.mocked()` for type safety** — Use `vi.mocked(service.method).mockResolvedValue(...)` when configuring +5. **Rely on `vi.resetAllMocks()`** — Resets call counts without recreating instances; ref values may need manual reset if mutated + +### Pattern + +```typescript +// Example from: src/platform/updates/common/releaseStore.test.ts +import { ref } from 'vue' + +vi.mock('@/path/to/composable', () => { + const doSomething = vi.fn() + const isLoading = ref(false) + const error = ref(null) + return { + useMyComposable: () => ({ + doSomething, + isLoading, + error + }) + } +}) + +describe('MyStore', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should call the composable method', async () => { + const service = useMyComposable() + vi.mocked(service.doSomething).mockResolvedValue({ data: 'test' }) + + await store.initialize() + + expect(service.doSomething).toHaveBeenCalledWith(expectedArgs) + }) + + it('should handle errors from the composable', async () => { + const service = useMyComposable() + vi.mocked(service.doSomething).mockResolvedValue(null) + service.error.value = 'Something went wrong' + + await store.initialize() + + expect(store.error).toBe('Something went wrong') + }) +}) +``` + +### Anti-patterns + +```typescript +// ❌ Don't configure mock return values in beforeEach with shared variable +let mockService: { doSomething: Mock } +beforeEach(() => { + mockService = { doSomething: vi.fn() } + vi.mocked(useMyComposable).mockReturnValue(mockService) +}) + +// ❌ Don't auto-mock then override — reactive refs won't work correctly +vi.mock('@/path/to/composable') +vi.mocked(useMyComposable).mockReturnValue({ isLoading: ref(false) }) +``` + +``` + +``` diff --git a/package.json b/package.json index 36b9fac32..c8f9d33ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@comfyorg/comfyui-frontend", - "version": "1.38.12", + "version": "1.39.0", "private": true, "description": "Official front-end implementation of ComfyUI", "homepage": "https://comfy.org", diff --git a/src/App.vue b/src/App.vue index 7c11c4c7b..b5e17500f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,11 +1,13 @@ diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue index 1122fe85f..27c038076 100644 --- a/src/components/graph/GraphCanvas.vue +++ b/src/components/graph/GraphCanvas.vue @@ -149,7 +149,7 @@ import { app as comfyApp } from '@/scripts/app' import { ChangeTracker } from '@/scripts/changeTracker' import { IS_CONTROL_WIDGET, updateControlWidgetLabel } from '@/scripts/widgets' import { useColorPaletteService } from '@/services/colorPaletteService' -import { newUserService } from '@/services/newUserService' +import { useNewUserService } from '@/services/useNewUserService' import { storeToRefs } from 'pinia' import { useBootstrapStore } from '@/stores/bootstrapStore' @@ -457,11 +457,9 @@ onMounted(async () => { // Register core settings immediately after settings are ready CORE_SETTINGS.forEach(settingStore.addSetting) - // Wait for both i18n and newUserService in parallel - // (newUserService only needs settings, not i18n) await Promise.all([ until(() => isI18nReady.value || !!i18nError.value).toBe(true), - newUserService().initializeIfNewUser(settingStore) + useNewUserService().initializeIfNewUser() ]) if (i18nError.value) { console.warn( @@ -502,19 +500,9 @@ onMounted(async () => { await workflowPersistence.loadTemplateFromUrlIfPresent() // Accept workspace invite from URL if present (e.g., ?invite=TOKEN) - // Uses watch because feature flags load asynchronously - flag may be false initially - // then become true once remoteConfig or websocket features are loaded - if (inviteUrlLoader) { - const stopWatching = watch( - () => flags.teamWorkspacesEnabled, - async (enabled) => { - if (enabled) { - stopWatching() - await inviteUrlLoader.loadInviteFromUrl() - } - }, - { immediate: true } - ) + // WorkspaceAuthGate ensures flag state is resolved before GraphCanvas mounts + if (inviteUrlLoader && flags.teamWorkspacesEnabled) { + await inviteUrlLoader.loadInviteFromUrl() } // Initialize release store to fetch releases from comfy-api (fire-and-forget) diff --git a/src/components/graph/SelectionToolbox.test.ts b/src/components/graph/SelectionToolbox.test.ts index 441ca027f..bbbe9d786 100644 --- a/src/components/graph/SelectionToolbox.test.ts +++ b/src/components/graph/SelectionToolbox.test.ts @@ -13,6 +13,8 @@ import { createMockCanvas, createMockPositionable } from '@/utils/__tests__/litegraphTestUtils' +import * as litegraphUtil from '@/utils/litegraphUtil' +import * as nodeFilterUtil from '@/utils/nodeFilterUtil' function createMockExtensionService(): ReturnType { return { @@ -289,9 +291,8 @@ describe('SelectionToolbox', () => { ) }) - it('should show mask editor only for single image nodes', async () => { - const mockUtils = await import('@/utils/litegraphUtil') - const isImageNodeSpy = vi.spyOn(mockUtils, 'isImageNode') + it('should show mask editor only for single image nodes', () => { + const isImageNodeSpy = vi.spyOn(litegraphUtil, 'isImageNode') // Single image node isImageNodeSpy.mockReturnValue(true) @@ -307,9 +308,8 @@ describe('SelectionToolbox', () => { expect(wrapper2.find('.mask-editor-button').exists()).toBe(false) }) - it('should show Color picker button only for single Load3D nodes', async () => { - const mockUtils = await import('@/utils/litegraphUtil') - const isLoad3dNodeSpy = vi.spyOn(mockUtils, 'isLoad3dNode') + it('should show Color picker button only for single Load3D nodes', () => { + const isLoad3dNodeSpy = vi.spyOn(litegraphUtil, 'isLoad3dNode') // Single Load3D node isLoad3dNodeSpy.mockReturnValue(true) @@ -325,13 +325,9 @@ describe('SelectionToolbox', () => { expect(wrapper2.find('.load-3d-viewer-button').exists()).toBe(false) }) - it('should show ExecuteButton only when output nodes are selected', async () => { - const mockNodeFilterUtil = await import('@/utils/nodeFilterUtil') - const isOutputNodeSpy = vi.spyOn(mockNodeFilterUtil, 'isOutputNode') - const filterOutputNodesSpy = vi.spyOn( - mockNodeFilterUtil, - 'filterOutputNodes' - ) + it('should show ExecuteButton only when output nodes are selected', () => { + const isOutputNodeSpy = vi.spyOn(nodeFilterUtil, 'isOutputNode') + const filterOutputNodesSpy = vi.spyOn(nodeFilterUtil, 'filterOutputNodes') // With output node selected isOutputNodeSpy.mockReturnValue(true) diff --git a/src/components/graph/selectionToolbox/ExecuteButton.test.ts b/src/components/graph/selectionToolbox/ExecuteButton.test.ts index 6fe236d5b..370b96b1d 100644 --- a/src/components/graph/selectionToolbox/ExecuteButton.test.ts +++ b/src/components/graph/selectionToolbox/ExecuteButton.test.ts @@ -7,6 +7,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createI18n } from 'vue-i18n' import ExecuteButton from '@/components/graph/selectionToolbox/ExecuteButton.vue' +import { useSelectionState } from '@/composables/graph/useSelectionState' import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useCommandStore } from '@/stores/commandStore' @@ -47,7 +48,7 @@ describe('ExecuteButton', () => { } }) - beforeEach(async () => { + beforeEach(() => { // Set up Pinia with testing utilities setActivePinia( createTestingPinia({ @@ -71,10 +72,7 @@ describe('ExecuteButton', () => { vi.spyOn(commandStore, 'execute').mockResolvedValue() // Update the useSelectionState mock - const { useSelectionState } = vi.mocked( - await import('@/composables/graph/useSelectionState') - ) - useSelectionState.mockReturnValue({ + vi.mocked(useSelectionState).mockReturnValue({ selectedNodes: { value: mockSelectedNodes } diff --git a/src/components/queue/QueueInlineProgress.test.ts b/src/components/queue/QueueInlineProgress.test.ts new file mode 100644 index 000000000..d63dc430e --- /dev/null +++ b/src/components/queue/QueueInlineProgress.test.ts @@ -0,0 +1,75 @@ +import { mount } from '@vue/test-utils' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { nextTick, ref } from 'vue' +import type { Ref } from 'vue' + +import QueueInlineProgress from '@/components/queue/QueueInlineProgress.vue' + +const mockProgress = vi.hoisted(() => ({ + totalPercent: null as unknown as Ref, + currentNodePercent: null as unknown as Ref +})) + +vi.mock('@/composables/queue/useQueueProgress', () => ({ + useQueueProgress: () => ({ + totalPercent: mockProgress.totalPercent, + currentNodePercent: mockProgress.currentNodePercent + }) +})) + +const createWrapper = (props: { hidden?: boolean } = {}) => + mount(QueueInlineProgress, { props }) + +describe('QueueInlineProgress', () => { + beforeEach(() => { + mockProgress.totalPercent = ref(0) + mockProgress.currentNodePercent = ref(0) + }) + + it('renders when total progress is non-zero', () => { + mockProgress.totalPercent.value = 12 + + const wrapper = createWrapper() + + expect(wrapper.find('[aria-hidden="true"]').exists()).toBe(true) + }) + + it('renders when current node progress is non-zero', () => { + mockProgress.currentNodePercent.value = 33 + + const wrapper = createWrapper() + + expect(wrapper.find('[aria-hidden="true"]').exists()).toBe(true) + }) + + it('does not render when hidden', () => { + mockProgress.totalPercent.value = 45 + + const wrapper = createWrapper({ hidden: true }) + + expect(wrapper.find('[aria-hidden="true"]').exists()).toBe(false) + }) + + it('shows when progress becomes non-zero', async () => { + const wrapper = createWrapper() + + expect(wrapper.find('[aria-hidden="true"]').exists()).toBe(false) + + mockProgress.totalPercent.value = 10 + await nextTick() + expect(wrapper.find('[aria-hidden="true"]').exists()).toBe(true) + }) + + it('hides when progress returns to zero', async () => { + mockProgress.totalPercent.value = 10 + + const wrapper = createWrapper() + + expect(wrapper.find('[aria-hidden="true"]').exists()).toBe(true) + + mockProgress.totalPercent.value = 0 + mockProgress.currentNodePercent.value = 0 + await nextTick() + expect(wrapper.find('[aria-hidden="true"]').exists()).toBe(false) + }) +}) diff --git a/src/components/queue/QueueInlineProgress.vue b/src/components/queue/QueueInlineProgress.vue new file mode 100644 index 000000000..e591e50d2 --- /dev/null +++ b/src/components/queue/QueueInlineProgress.vue @@ -0,0 +1,36 @@ + + + diff --git a/src/components/queue/QueueInlineProgressSummary.vue b/src/components/queue/QueueInlineProgressSummary.vue new file mode 100644 index 000000000..758c5d316 --- /dev/null +++ b/src/components/queue/QueueInlineProgressSummary.vue @@ -0,0 +1,70 @@ + + + diff --git a/src/components/rightSidePanel/RightSidePanel.vue b/src/components/rightSidePanel/RightSidePanel.vue index b23346bf4..1ba514f66 100644 --- a/src/components/rightSidePanel/RightSidePanel.vue +++ b/src/components/rightSidePanel/RightSidePanel.vue @@ -8,12 +8,14 @@ import Tab from '@/components/tab/Tab.vue' import TabList from '@/components/tab/TabList.vue' import Button from '@/components/ui/button/Button.vue' import { useGraphHierarchy } from '@/composables/graph/useGraphHierarchy' +import { st } from '@/i18n' import { SubgraphNode } from '@/lib/litegraph/src/litegraph' import type { LGraphNode } from '@/lib/litegraph/src/litegraph' import { useSettingStore } from '@/platform/settings/settingStore' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore' import type { RightSidePanelTab } from '@/stores/workspace/rightSidePanelStore' +import { resolveNodeDisplayName } from '@/utils/nodeTitleUtil' import { cn } from '@/utils/tailwindUtil' import TabInfo from './info/TabInfo.vue' @@ -146,9 +148,12 @@ function resolveTitle() { return groups[0].title || t('rightSidePanel.fallbackGroupTitle') } if (nodes.length === 1) { - return ( - nodes[0].title || nodes[0].type || t('rightSidePanel.fallbackNodeTitle') - ) + const fallbackNodeTitle = t('rightSidePanel.fallbackNodeTitle') + return resolveNodeDisplayName(nodes[0], { + emptyLabel: fallbackNodeTitle, + untitledLabel: fallbackNodeTitle, + st + }) } } return t('rightSidePanel.title', { count: items.length }) diff --git a/src/components/rightSidePanel/parameters/SectionWidgets.vue b/src/components/rightSidePanel/parameters/SectionWidgets.vue index b1905a5f0..9ffbfa6dc 100644 --- a/src/components/rightSidePanel/parameters/SectionWidgets.vue +++ b/src/components/rightSidePanel/parameters/SectionWidgets.vue @@ -14,6 +14,8 @@ import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import PropertiesAccordionItem from '../layout/PropertiesAccordionItem.vue' +import { HideLayoutFieldKey } from '@/types/widgetTypes' + import { GetNodeParentGroupKey } from '../shared' import WidgetItem from './WidgetItem.vue' @@ -52,7 +54,7 @@ const rootElement = ref() const widgets = shallowRef(widgetsProp) watchEffect(() => (widgets.value = widgetsProp)) -provide('hideLayoutField', true) +provide(HideLayoutFieldKey, true) const canvasStore = useCanvasStore() const { t } = useI18n() diff --git a/src/components/rightSidePanel/parameters/WidgetItem.vue b/src/components/rightSidePanel/parameters/WidgetItem.vue index 6a2eca748..4cd05636a 100644 --- a/src/components/rightSidePanel/parameters/WidgetItem.vue +++ b/src/components/rightSidePanel/parameters/WidgetItem.vue @@ -1,9 +1,11 @@ @@ -115,7 +129,7 @@ -
+
workflowStore.activeWorkflow?.filename ?? t('manager.inWorkflow') +) + // Navigation items for LeftSidePanel -const navItems = computed(() => [ - { id: ManagerTab.All, label: t('g.all'), icon: 'pi pi-list' }, - { id: ManagerTab.Installed, label: t('g.installed'), icon: 'pi pi-box' }, +const navItems = computed<(NavItemData | NavGroupData)[]>(() => [ { - id: ManagerTab.Workflow, - label: t('manager.inWorkflow'), - icon: 'pi pi-folder' + id: ManagerTab.All, + label: t('manager.nav.allExtensions'), + icon: 'icon-[lucide--list]' }, { - id: ManagerTab.Missing, - label: t('g.missing'), - icon: 'pi pi-exclamation-circle' + id: ManagerTab.NotInstalled, + label: t('manager.nav.notInstalled'), + icon: 'icon-[lucide--globe]' }, { - id: ManagerTab.UpdateAvailable, - label: t('g.updateAvailable'), - icon: 'pi pi-sync' + title: t('manager.nav.installedSection'), + items: [ + { + id: ManagerTab.AllInstalled, + label: t('manager.nav.allInstalled'), + icon: 'icon-[lucide--download]' + }, + { + id: ManagerTab.UpdateAvailable, + label: t('manager.nav.updatesAvailable'), + icon: 'icon-[lucide--refresh-cw]' + }, + { + id: ManagerTab.Conflicting, + label: t('manager.nav.conflicting'), + icon: 'icon-[lucide--triangle-alert]', + badge: conflictDetectionStore.conflictedPackages.length || undefined + } + ] + }, + { + title: t('manager.nav.inWorkflowSection'), + items: [ + { + id: ManagerTab.Workflow, + label: t('manager.nav.allInWorkflow', { + workflowName: workflowName.value + }), + icon: 'icon-[lucide--share-2]' + }, + { + id: ManagerTab.Missing, + label: t('manager.nav.missingNodes'), + icon: 'icon-[lucide--triangle-alert]' + } + ] } ]) const initialTabId = initialTab ?? initialState.selectedTabId ?? ManagerTab.All const selectedNavId = ref(initialTabId) +// Helper to find a nav item by id in the nested structure +const findNavItemById = ( + items: (NavItemData | NavGroupData)[], + id: string | null +): NavItemData | undefined => { + for (const item of items) { + if ('items' in item) { + const found = item.items.find((subItem) => subItem.id === id) + if (found) return found + } else if (item.id === id) { + return item + } + } + return undefined +} + const selectedTab = computed(() => - navItems.value.find((item) => item.id === selectedNavId.value) + findNavItemById(navItems.value, selectedNavId.value) ) const { @@ -315,120 +383,20 @@ const isInitialLoad = computed( () => searchResults.value.length === 0 && searchQuery.value === '' ) -const isEmptySearch = computed(() => searchQuery.value === '') -const displayPacks = ref([]) - +// Use the new composable for tab-based display packs const { - startFetchInstalled, - filterInstalledPack, - installedPacks, - isLoading: isLoadingInstalled, - isReady: installedPacksReady -} = useInstalledPacks() - -const { - startFetchWorkflowPacks, - filterWorkflowPack, - workflowPacks, - isLoading: isLoadingWorkflow, - isReady: workflowPacksReady -} = useWorkflowPacks() - -const filterMissingPacks = (packs: components['schemas']['Node'][]) => - packs.filter((pack) => !comfyManagerStore.isPackInstalled(pack.id)) + displayPacks, + isLoading: isTabLoading, + workflowPacks +} = useManagerDisplayPacks(selectedNavId, searchResults, searchQuery, sortField) +// Tab helpers for template const isUpdateAvailableTab = computed( () => selectedTab.value?.id === ManagerTab.UpdateAvailable ) -const isInstalledTab = computed( - () => selectedTab.value?.id === ManagerTab.Installed -) const isMissingTab = computed( () => selectedTab.value?.id === ManagerTab.Missing ) -const isWorkflowTab = computed( - () => selectedTab.value?.id === ManagerTab.Workflow -) -const isAllTab = computed(() => selectedTab.value?.id === ManagerTab.All) - -const isOutdatedPack = (pack: components['schemas']['Node']) => { - const { isUpdateAvailable } = usePackUpdateStatus(pack) - return isUpdateAvailable.value === true -} -const filterOutdatedPacks = (packs: components['schemas']['Node'][]) => - packs.filter(isOutdatedPack) - -watch( - [isUpdateAvailableTab, installedPacks], - async () => { - if (!isUpdateAvailableTab.value) return - - if (!isEmptySearch.value) { - displayPacks.value = filterOutdatedPacks(installedPacks.value) - } else if ( - !installedPacks.value.length && - !installedPacksReady.value && - !isLoadingInstalled.value - ) { - await startFetchInstalled() - } else { - displayPacks.value = filterOutdatedPacks(installedPacks.value) - } - }, - { immediate: true } -) - -watch( - [isInstalledTab, installedPacks], - async () => { - if (!isInstalledTab.value) return - - if (!isEmptySearch.value) { - displayPacks.value = filterInstalledPack(searchResults.value) - } else if ( - !installedPacks.value.length && - !installedPacksReady.value && - !isLoadingInstalled.value - ) { - await startFetchInstalled() - } else { - displayPacks.value = installedPacks.value - } - }, - { immediate: true } -) - -watch( - [isMissingTab, isWorkflowTab, workflowPacks, installedPacks], - async () => { - if (!isWorkflowTab.value && !isMissingTab.value) return - - if (!isEmptySearch.value) { - displayPacks.value = isMissingTab.value - ? filterMissingPacks(filterWorkflowPack(searchResults.value)) - : filterWorkflowPack(searchResults.value) - } else if ( - !workflowPacks.value.length && - !isLoadingWorkflow.value && - !workflowPacksReady.value - ) { - await startFetchWorkflowPacks() - if (isMissingTab.value) { - await startFetchInstalled() - } - } else { - displayPacks.value = isMissingTab.value - ? filterMissingPacks(workflowPacks.value) - : workflowPacks.value - } - }, - { immediate: true } -) - -watch([isAllTab, searchResults], () => { - if (!isAllTab.value) return - displayPacks.value = searchResults.value -}) const onClickWarningLink = () => { window.open( @@ -439,49 +407,9 @@ const onClickWarningLink = () => { ) } -const onResultsChange = () => { - switch (selectedTab.value?.id) { - case ManagerTab.Installed: - displayPacks.value = isEmptySearch.value - ? installedPacks.value - : filterInstalledPack(searchResults.value) - break - case ManagerTab.Workflow: - displayPacks.value = isEmptySearch.value - ? workflowPacks.value - : filterWorkflowPack(searchResults.value) - break - case ManagerTab.Missing: - if (!isEmptySearch.value) { - displayPacks.value = filterMissingPacks( - filterWorkflowPack(searchResults.value) - ) - } - break - case ManagerTab.UpdateAvailable: - displayPacks.value = isEmptySearch.value - ? filterOutdatedPacks(installedPacks.value) - : filterOutdatedPacks(searchResults.value) - break - default: - displayPacks.value = searchResults.value - } -} - -watch(searchResults, onResultsChange, { flush: 'post' }) -watch(() => comfyManagerStore.installedPacksIds, onResultsChange) - const isLoading = computed(() => { if (isSearchLoading.value) return searchResults.value.length === 0 - if (selectedTab.value?.id === ManagerTab.Installed) { - return isLoadingInstalled.value - } - if ( - selectedTab.value?.id === ManagerTab.Workflow || - selectedTab.value?.id === ManagerTab.Missing - ) { - return isLoadingWorkflow.value - } + if (isTabLoading.value) return true return isInitialLoad.value }) @@ -501,14 +429,16 @@ const isRightPanelOpen = ref(false) watch( () => selectedNodePacks.value.length, - (length) => { - isRightPanelOpen.value = length > 0 + (length, oldLength) => { + if (length > 0 && oldLength === 0) { + isRightPanelOpen.value = true + } } ) const getLoadingCount = () => { switch (selectedTab.value?.id) { - case ManagerTab.Installed: + case ManagerTab.AllInstalled: return comfyManagerStore.installedPacksIds?.size case ManagerTab.Workflow: return workflowPacks.value?.length @@ -578,10 +508,6 @@ whenever(selectedNodePack, async () => { if (packIndex !== -1) { selectedNodePacks.value.splice(packIndex, 1, mergedPack) } - const idx = displayPacks.value.findIndex((p) => p.id === mergedPack.id) - if (idx !== -1) { - displayPacks.value.splice(idx, 1, mergedPack) - } } }) @@ -595,6 +521,7 @@ watch([searchQuery, selectedNavId], () => { pageNumber.value = 0 gridContainer.scrollTop = 0 } + unSelectItems() }) watchEffect(() => { diff --git a/src/workbench/extensions/manager/components/manager/button/PackInstallButton.vue b/src/workbench/extensions/manager/components/manager/button/PackInstallButton.vue index 3000a4a33..0aeab79fe 100644 --- a/src/workbench/extensions/manager/components/manager/button/PackInstallButton.vue +++ b/src/workbench/extensions/manager/components/manager/button/PackInstallButton.vue @@ -1,6 +1,6 @@ diff --git a/src/workbench/extensions/manager/components/manager/button/PackTryUpdateButton.vue b/src/workbench/extensions/manager/components/manager/button/PackTryUpdateButton.vue index 218aaf35f..201259560 100644 --- a/src/workbench/extensions/manager/components/manager/button/PackTryUpdateButton.vue +++ b/src/workbench/extensions/manager/components/manager/button/PackTryUpdateButton.vue @@ -1,7 +1,7 @@ diff --git a/src/workbench/extensions/manager/components/manager/button/PackUninstallButton.vue b/src/workbench/extensions/manager/components/manager/button/PackUninstallButton.vue index 7ff3e42aa..cac261f91 100644 --- a/src/workbench/extensions/manager/components/manager/button/PackUninstallButton.vue +++ b/src/workbench/extensions/manager/components/manager/button/PackUninstallButton.vue @@ -1,10 +1,6 @@ diff --git a/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanel.vue b/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanel.vue index 9f25902cd..9537b540b 100644 --- a/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanel.vue +++ b/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanel.vue @@ -1,62 +1,110 @@