diff --git a/src/components/dialog/content/manager/PackVersionSelectorPopover.vue b/src/components/dialog/content/manager/PackVersionSelectorPopover.vue index 8a4b4a3cf..3bc6094eb 100644 --- a/src/components/dialog/content/manager/PackVersionSelectorPopover.vue +++ b/src/components/dialog/content/manager/PackVersionSelectorPopover.vue @@ -93,15 +93,33 @@ import VerifiedIcon from '@/components/icons/VerifiedIcon.vue' import { useConflictDetection } from '@/composables/useConflictDetection' import { useComfyRegistryService } from '@/services/comfyRegistryService' import { useComfyManagerStore } from '@/stores/comfyManagerStore' -import { - ManagerChannel, - ManagerDatabaseSource, - SelectedVersion -} from '@/types/comfyManagerTypes' import { components } from '@/types/comfyRegistryTypes' +import { components as ManagerComponents } from '@/types/generatedManagerTypes' import { getJoinedConflictMessages } from '@/utils/conflictMessageUtil' import { isSemVer } from '@/utils/formatUtil' +type ManagerChannel = ManagerComponents['schemas']['ManagerChannel'] +type ManagerDatabaseSource = + ManagerComponents['schemas']['ManagerDatabaseSource'] +type SelectedVersion = ManagerComponents['schemas']['SelectedVersion'] + +// Enum values for runtime use +const SelectedVersionValues = { + LATEST: 'latest' as SelectedVersion, + NIGHTLY: 'nightly' as SelectedVersion +} + +const ManagerChannelValues = { + STABLE: 'stable' as ManagerChannel, + DEV: 'dev' as ManagerChannel +} + +const ManagerDatabaseSourceValues = { + CACHE: 'cache' as ManagerDatabaseSource, + REMOTE: 'remote' as ManagerDatabaseSource, + LOCAL: 'local' as ManagerDatabaseSource +} + const { nodePack } = defineProps<{ nodePack: components['schemas']['Node'] }>() @@ -118,19 +136,21 @@ const { checkNodeCompatibility } = useConflictDetection() const isQueueing = ref(false) -const selectedVersion = ref(SelectedVersion.LATEST) +const selectedVersion = ref(SelectedVersionValues.LATEST) onMounted(() => { - const initialVersion = getInitialSelectedVersion() ?? SelectedVersion.LATEST + const initialVersion = + getInitialSelectedVersion() ?? SelectedVersionValues.LATEST selectedVersion.value = // Use NIGHTLY when version is a Git hash - isSemVer(initialVersion) ? initialVersion : SelectedVersion.NIGHTLY + isSemVer(initialVersion) ? initialVersion : SelectedVersionValues.NIGHTLY }) const getInitialSelectedVersion = () => { if (!nodePack.id) return // If unclaimed, set selected version to nightly - if (nodePack.publisher?.name === 'Unclaimed') return SelectedVersion.NIGHTLY + if (nodePack.publisher?.name === 'Unclaimed') + return SelectedVersionValues.NIGHTLY // If node pack is installed, set selected version to the installed version if (managerStore.isPackInstalled(nodePack.id)) @@ -180,7 +200,7 @@ const onNodePackChange = async () => { // Add Latest option const defaultVersions = [ { - value: SelectedVersion.LATEST, + value: SelectedVersionValues.LATEST, label: latestLabel } ] @@ -188,7 +208,7 @@ const onNodePackChange = async () => { // Add Nightly option if there is a non-empty `repository` field if (nodePack.repository?.length) { defaultVersions.push({ - value: SelectedVersion.NIGHTLY, + value: SelectedVersionValues.NIGHTLY, label: t('manager.nightlyVersion') }) } @@ -222,8 +242,8 @@ const handleSubmit = async () => { await managerStore.installPack.call({ id: nodePack.id, repository: nodePack.repository ?? '', - channel: ManagerChannel.DEFAULT, - mode: ManagerDatabaseSource.CACHE, + channel: ManagerChannelValues.STABLE, + mode: ManagerDatabaseSourceValues.CACHE, version: actualVersion, selected_version: selectedVersion.value }) diff --git a/src/components/dialog/content/manager/button/PackEnableToggle.vue b/src/components/dialog/content/manager/button/PackEnableToggle.vue index db2a58975..be449afc6 100644 --- a/src/components/dialog/content/manager/button/PackEnableToggle.vue +++ b/src/components/dialog/content/manager/button/PackEnableToggle.vue @@ -38,8 +38,8 @@ import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgme import { useDialogService } from '@/services/dialogService' import { useComfyManagerStore } from '@/stores/comfyManagerStore' import { useConflictDetectionStore } from '@/stores/conflictDetectionStore' -import { components as ManagerComponents } from '@/types/generatedManagerTypes' import type { components } from '@/types/comfyRegistryTypes' +import { components as ManagerComponents } from '@/types/generatedManagerTypes' const TOGGLE_DEBOUNCE_MS = 256 @@ -108,8 +108,12 @@ const handleEnable = () => { } return enablePack.call({ id: nodePack.id, - version: version.value ?? ('latest' as ManagerComponents['schemas']['SelectedVersion']), - selected_version: version.value ?? ('latest' as ManagerComponents['schemas']['SelectedVersion']), + version: + version.value ?? + ('latest' as ManagerComponents['schemas']['SelectedVersion']), + selected_version: + version.value ?? + ('latest' as ManagerComponents['schemas']['SelectedVersion']), repository: nodePack.repository ?? '', channel: 'default' as ManagerComponents['schemas']['ManagerChannel'], mode: 'cache' as ManagerComponents['schemas']['ManagerDatabaseSource'], @@ -123,7 +127,9 @@ const handleDisable = () => { } return disablePack({ id: nodePack.id, - version: version.value ?? ('latest' as ManagerComponents['schemas']['SelectedVersion']) + version: + version.value ?? + ('latest' as ManagerComponents['schemas']['SelectedVersion']) }) } diff --git a/src/components/dialog/content/manager/button/PackInstallButton.vue b/src/components/dialog/content/manager/button/PackInstallButton.vue index c19988f7b..2395e23b1 100644 --- a/src/components/dialog/content/manager/button/PackInstallButton.vue +++ b/src/components/dialog/content/manager/button/PackInstallButton.vue @@ -33,11 +33,11 @@ import { useDialogService } from '@/services/dialogService' import { useComfyManagerStore } from '@/stores/comfyManagerStore' import { ButtonSize } from '@/types/buttonTypes' import type { components } from '@/types/comfyRegistryTypes' -import { components as ManagerComponents } from '@/types/generatedManagerTypes' import { type ConflictDetail, ConflictDetectionResult } from '@/types/conflictDetectionTypes' +import { components as ManagerComponents } from '@/types/generatedManagerTypes' type NodePack = components['schemas']['Node'] @@ -123,7 +123,7 @@ const installAllPacks = async () => { }) return } - + // No conflicts or conflicts acknowledged - proceed with installation const uninstalledPacks = nodePacks.filter( (pack) => !managerStore.isPackInstalled(pack.id) diff --git a/src/components/dialog/content/manager/button/PackUpdateButton.vue b/src/components/dialog/content/manager/button/PackUpdateButton.vue index cccc6f779..447f370f4 100644 --- a/src/components/dialog/content/manager/button/PackUpdateButton.vue +++ b/src/components/dialog/content/manager/button/PackUpdateButton.vue @@ -1,21 +1,26 @@ diff --git a/src/components/dialog/content/manager/packCard/PackCardFooter.vue b/src/components/dialog/content/manager/packCard/PackCardFooter.vue index f8c7f07eb..86f41a44e 100644 --- a/src/components/dialog/content/manager/packCard/PackCardFooter.vue +++ b/src/components/dialog/content/manager/packCard/PackCardFooter.vue @@ -10,6 +10,8 @@ v-if="!isInstalled" :node-packs="[nodePack]" :is-installing="isInstalling" + :has-conflict="hasConflicts" + :conflict-info="conflictInfo" /> @@ -21,9 +23,11 @@ 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 { useConflictDetection } from '@/composables/useConflictDetection' import { useComfyManagerStore } from '@/stores/comfyManagerStore' import { IsInstallingKey } from '@/types/comfyManagerTypes' import type { components } from '@/types/comfyRegistryTypes' +import type { ConflictDetail } from '@/types/conflictDetectionTypes' const { nodePack } = defineProps<{ nodePack: components['schemas']['Node'] @@ -38,4 +42,16 @@ const { n } = useI18n() const formattedDownloads = computed(() => nodePack.downloads ? n(nodePack.downloads) : '' ) + +// Add conflict detection for the card button +const { checkNodeCompatibility } = useConflictDetection() + +// Check for conflicts with this specific node pack +const conflictInfo = computed(() => { + if (!nodePack) return [] + const compatibilityCheck = checkNodeCompatibility(nodePack) + return compatibilityCheck.conflicts || [] +}) + +const hasConflicts = computed(() => conflictInfo.value.length > 0) diff --git a/src/composables/useConflictDetection.ts b/src/composables/useConflictDetection.ts index 708dd6968..6ec8f2cdb 100644 --- a/src/composables/useConflictDetection.ts +++ b/src/composables/useConflictDetection.ts @@ -712,16 +712,27 @@ export function useConflictDetection() { const conflicts: ConflictDetail[] = [] // Check OS compatibility using centralized function - if (node.supported_os && node.supported_os.length > 0) { + // First try latest_version (most accurate), then fallback to top level + const supportedOS = + ('latest_version' in node ? node.latest_version?.supported_os : null) || + node.supported_os + + if (supportedOS && supportedOS.length > 0) { const currentOS = systemStats.system?.os || 'unknown' - const osConflict = checkOSConflict(node.supported_os, currentOS) + const osConflict = checkOSConflict(supportedOS, currentOS) if (osConflict) { conflicts.push(osConflict) } } // Check accelerator compatibility using centralized function - if (node.supported_accelerators && node.supported_accelerators.length > 0) { + // First try latest_version (most accurate), then fallback to top level + const supportedAccelerators = + ('latest_version' in node + ? node.latest_version?.supported_accelerators + : null) || node.supported_accelerators + + if (supportedAccelerators && supportedAccelerators.length > 0) { // Extract available accelerators from system stats const acceleratorInfo = extractAcceleratorInfo(systemStats) const availableAccelerators: Node['supported_accelerators'] = [] @@ -733,7 +744,7 @@ export function useConflictDetection() { }) const acceleratorConflict = checkAcceleratorConflict( - node.supported_accelerators, + supportedAccelerators, availableAccelerators ) if (acceleratorConflict) { @@ -742,13 +753,19 @@ export function useConflictDetection() { } // Check ComfyUI version compatibility - if (node.supported_comfyui_version) { + // First try latest_version (most accurate), then fallback to top level + const comfyuiVersionRequirement = + ('latest_version' in node + ? node.latest_version?.supported_comfyui_version + : null) || node.supported_comfyui_version + + if (comfyuiVersionRequirement) { const currentComfyUIVersion = systemStats.system?.comfyui_version if (currentComfyUIVersion && currentComfyUIVersion !== 'unknown') { const versionConflict = utilCheckVersionCompatibility( 'comfyui_version', currentComfyUIVersion, - node.supported_comfyui_version + comfyuiVersionRequirement ) if (versionConflict) { conflicts.push(versionConflict) @@ -757,13 +774,19 @@ export function useConflictDetection() { } // Check ComfyUI Frontend version compatibility - if (node.supported_comfyui_frontend_version) { + // First try latest_version (most accurate), then fallback to top level + const frontendVersionRequirement = + ('latest_version' in node + ? node.latest_version?.supported_comfyui_frontend_version + : null) || node.supported_comfyui_frontend_version + + if (frontendVersionRequirement) { const currentFrontendVersion = config.app_version if (currentFrontendVersion && currentFrontendVersion !== 'unknown') { const versionConflict = utilCheckVersionCompatibility( 'frontend_version', currentFrontendVersion, - node.supported_comfyui_frontend_version + frontendVersionRequirement ) if (versionConflict) { conflicts.push(versionConflict) diff --git a/src/locales/en/main.json b/src/locales/en/main.json index fbeef244b..055cbbc4b 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -24,6 +24,7 @@ "confirmed": "Confirmed", "reset": "Reset", "resetAll": "Reset All", + "clearFilters": "Clear Filters", "resetAllKeybindingsTooltip": "Reset all keybindings to default", "customizeFolder": "Customize Folder", "icon": "Icon", @@ -206,11 +207,6 @@ "installAllMissingNodes": "Install All Missing Nodes", "packsSelected": "packs selected", "mixedSelectionMessage": "Cannot perform bulk action on mixed selection", - "gettingInfo": "Getting info...", - "legacyMenuNotAvailable": "Legacy menu not available", - "legacyManagerUI": "Legacy Manager UI", - "legacyManagerUIDescription": "Switch to the legacy ComfyUI Manager interface", - "updateAll": "Update All", "notAvailable": "Not Available", "status": { "active": "Active", @@ -235,7 +231,7 @@ }, "conflicts": { "title": "Node Pack Issues Detected!", - "description": "We’ve detected conflicts between some of your extensions and the new version of ComfyUI. By updating you risk breaking workflows that rely on those extensions.", + "description": "We've detected conflicts between some of your extensions and the new version of ComfyUI. By updating you risk breaking workflows that rely on those extensions.", "info": "If you continue with the update, the conflicting extensions will be disabled automatically. You can review and manage them anytime in the ComfyUI Manager.", "extensionAtRisk": "Extension at Risk", "conflicts": "Conflicts", @@ -482,20 +478,6 @@ "error": "Unable to start ComfyUI Desktop" } }, - "shortcuts": { - "essentials": "Essential", - "viewControls": "View Controls", - "manageShortcuts": "Manage Shortcuts", - "noKeybinding": "No keybinding", - "keyboardShortcuts": "Keyboard Shortcuts", - "subcategories": { - "workflow": "Workflow", - "node": "Node", - "queue": "Queue", - "view": "View", - "panelControls": "Panel Controls" - } - }, "serverConfig": { "modifiedConfigs": "You have modified the following server configurations. Restart to apply changes.", "revertChanges": "Revert Changes", @@ -508,6 +490,14 @@ "queue": "Queue", "nodeLibrary": "Node Library", "workflows": "Workflows", + "templates": "Templates", + "labels": { + "queue": "Queue", + "nodes": "Nodes", + "models": "Models", + "workflows": "Workflows", + "templates": "Templates" + }, "browseTemplates": "Browse example templates", "openWorkflow": "Open workflow in local file system", "newBlankWorkflow": "Create a new blank workflow", @@ -604,7 +594,14 @@ "clipspace": "Open Clipspace", "resetView": "Reset canvas view", "clear": "Clear workflow", - "toggleBottomPanel": "Toggle Bottom Panel" + "toggleBottomPanel": "Toggle Bottom Panel", + "theme": "Theme", + "dark": "Dark", + "light": "Light", + "manageExtensions": "Manage Extensions", + "settings": "Settings", + "help": "Help", + "queue": "Queue Panel" }, "tabMenu": { "duplicateTab": "Duplicate Tab", @@ -1009,6 +1006,25 @@ "Mask Opacity": "Mask Opacity", "Image Layer": "Image Layer" }, + "commands": { + "runWorkflow": "Run workflow", + "runWorkflowFront": "Run workflow (Queue at front)", + "run": "Run", + "execute": "Execute", + "interrupt": "Cancel current run", + "refresh": "Refresh node definitions", + "clipspace": "Open Clipspace", + "resetView": "Reset canvas view", + "clear": "Clear workflow", + "toggleBottomPanel": "Toggle Bottom Panel", + "theme": "Theme", + "dark": "Dark", + "light": "Light", + "manageExtensions": "Manage Extensions", + "settings": "Settings", + "help": "Help", + "queue": "Queue Panel" + }, "menuLabels": { "Workflow": "Workflow", "Edit": "Edit", @@ -1026,6 +1042,7 @@ "Quit": "Quit", "Reinstall": "Reinstall", "Restart": "Restart", + "Open 3D Viewer (Beta) for Selected Node": "Open 3D Viewer (Beta) for Selected Node", "Browse Templates": "Browse Templates", "Delete Selected Items": "Delete Selected Items", "Fit view to selected nodes": "Fit view to selected nodes", @@ -1056,8 +1073,10 @@ "Export (API)": "Export (API)", "Give Feedback": "Give Feedback", "Convert Selection to Subgraph": "Convert Selection to Subgraph", + "Exit Subgraph": "Exit Subgraph", "Fit Group To Contents": "Fit Group To Contents", "Group Selected Nodes": "Group Selected Nodes", + "Unpack the selected Subgraph": "Unpack the selected Subgraph", "Convert selected nodes to group node": "Convert selected nodes to group node", "Manage group nodes": "Manage group nodes", "Ungroup selected group nodes": "Ungroup selected group nodes", @@ -1080,6 +1099,7 @@ "Unload Models and Execution Cache": "Unload Models and Execution Cache", "New": "New", "Clipspace": "Clipspace", + "Manager": "Manager", "Open": "Open", "Queue Prompt": "Queue Prompt", "Queue Prompt (Front)": "Queue Prompt (Front)", @@ -1089,6 +1109,8 @@ "Save": "Save", "Save As": "Save As", "Show Settings Dialog": "Show Settings Dialog", + "Canvas Performance": "Canvas Performance", + "Help Center": "Help Center", "Toggle Theme (Dark/Light)": "Toggle Theme (Dark/Light)", "Undo": "Undo", "Open Sign In Dialog": "Open Sign In Dialog", @@ -1163,7 +1185,8 @@ "User": "User", "Credits": "Credits", "API Nodes": "API Nodes", - "Notification Preferences": "Notification Preferences" + "Notification Preferences": "Notification Preferences", + "3DViewer": "3DViewer" }, "serverConfigItems": { "listen": { @@ -1515,12 +1538,31 @@ "depth": "Depth", "lineart": "Lineart" }, + "upDirections": { + "original": "Original" + }, "startRecording": "Start Recording", "stopRecording": "Stop Recording", "exportRecording": "Export Recording", "clearRecording": "Clear Recording", "resizeNodeMatchOutput": "Resize Node to match output", - "loadingBackgroundImage": "Loading Background Image" + "loadingBackgroundImage": "Loading Background Image", + "cameraType": { + "perspective": "Perspective", + "orthographic": "Orthographic" + }, + "viewer": { + "title": "3D Viewer (Beta)", + "apply": "Apply", + "cancel": "Cancel", + "cameraType": "Camera Type", + "sceneSettings": "Scene Settings", + "cameraSettings": "Camera Settings", + "lightSettings": "Light Settings", + "exportSettings": "Export Settings", + "modelSettings": "Model Settings" + }, + "openIn3DViewer": "Open in 3D Viewer" }, "toastMessages": { "nothingToQueue": "Nothing to queue", @@ -1558,7 +1600,8 @@ "useApiKeyTip": "Tip: Can't access normal login? Use the Comfy API Key option.", "nothingSelected": "Nothing selected", "cannotCreateSubgraph": "Cannot create subgraph", - "failedToConvertToSubgraph": "Failed to convert items to subgraph" + "failedToConvertToSubgraph": "Failed to convert items to subgraph", + "failedToInitializeLoad3dViewer": "Failed to initialize 3D Viewer" }, "auth": { "apiKey": { @@ -1632,15 +1675,6 @@ "passwordUpdate": { "success": "Password Updated", "successDetail": "Your password has been updated successfully" - }, - "deleteAccount": { - "deleteAccount": "Delete Account", - "confirmTitle": "Delete Account", - "confirmMessage": "Are you sure you want to delete your account? This action cannot be undone and will permanently remove all your data.", - "confirm": "Delete Account", - "cancel": "Cancel", - "success": "Account Deleted", - "successDetail": "Your account has been successfully deleted." } }, "validation": { @@ -1649,6 +1683,7 @@ "minLength": "Must be at least {length} characters", "maxLength": "Must be no more than {length} characters", "prefix": "Must start with {prefix}", + "descriptionRequired": "Description is required", "length": "Must be {length} characters", "password": { "requirements": "Password requirements", @@ -1720,5 +1755,32 @@ "whatsNewPopup": { "learnMore": "Learn more", "noReleaseNotes": "No release notes available." + }, + "breadcrumbsMenu": { + "duplicate": "Duplicate", + "clearWorkflow": "Clear Workflow", + "deleteWorkflow": "Delete Workflow", + "enterNewName": "Enter new name" + }, + "shortcuts": { + "essentials": "Essential", + "viewControls": "View Controls", + "manageShortcuts": "Manage Shortcuts", + "noKeybinding": "No keybinding", + "keyboardShortcuts": "Keyboard Shortcuts", + "subcategories": { + "workflow": "Workflow", + "node": "Node", + "queue": "Queue", + "view": "View", + "panelControls": "Panel Controls" + } + }, + "minimap": { + "nodeColors": "Node Colors", + "showLinks": "Show Links", + "showGroups": "Show Frames/Groups", + "renderBypassState": "Render Bypass State", + "renderErrorState": "Render Error State" } } \ No newline at end of file diff --git a/src/services/comfyManagerService.ts b/src/services/comfyManagerService.ts index 1364ff7f9..6bc09b4ae 100644 --- a/src/services/comfyManagerService.ts +++ b/src/services/comfyManagerService.ts @@ -20,18 +20,18 @@ const GENERIC_SECURITY_ERR_MSG = * API routes for ComfyUI Manager */ enum ManagerRoute { - START_QUEUE = 'v2/manager/queue/start', - RESET_QUEUE = 'v2/manager/queue/reset', - QUEUE_STATUS = 'v2/manager/queue/status', - UPDATE_ALL = 'v2/manager/queue/update_all', - LIST_INSTALLED = 'v2/customnode/installed', - GET_NODES = 'v2/customnode/getmappings', - IMPORT_FAIL_INFO = 'v2/customnode/import_fail_info', - IMPORT_FAIL_INFO_BULK = 'v2/customnode/import_fail_info_bulk', - REBOOT = 'v2/manager/reboot', - IS_LEGACY_MANAGER_UI = 'v2/manager/is_legacy_manager_ui', - TASK_HISTORY = 'v2/manager/queue/history', - QUEUE_TASK = 'v2/manager/queue/task' + START_QUEUE = 'manager/queue/start', + RESET_QUEUE = 'manager/queue/reset', + QUEUE_STATUS = 'manager/queue/status', + UPDATE_ALL = 'manager/queue/update_all', + LIST_INSTALLED = 'customnode/installed', + GET_NODES = 'customnode/getmappings', + IMPORT_FAIL_INFO = 'customnode/import_fail_info', + IMPORT_FAIL_INFO_BULK = 'customnode/import_fail_info_bulk', + REBOOT = 'manager/reboot', + IS_LEGACY_MANAGER_UI = 'manager/is_legacy_manager_ui', + TASK_HISTORY = 'manager/queue/history', + QUEUE_TASK = 'manager/queue/task' } const managerApiClient = axios.create({ diff --git a/src/types/comfyManagerTypes.ts b/src/types/comfyManagerTypes.ts index 20219bbce..e59a81480 100644 --- a/src/types/comfyManagerTypes.ts +++ b/src/types/comfyManagerTypes.ts @@ -3,9 +3,6 @@ import type { InjectionKey, Ref } from 'vue' import type { AlgoliaNodePack } from '@/types/algoliaTypes' import type { components } from '@/types/comfyRegistryTypes' import type { components as managerComponents } from '@/types/generatedManagerTypes' -import type { SearchMode } from '@/types/searchServiceTypes' - -type WorkflowNodeProperties = ComfyWorkflowJSON['nodes'][0]['properties'] export type RegistryPack = components['schemas']['Node'] export type MergedNodePack = RegistryPack & AlgoliaNodePack @@ -80,7 +77,6 @@ export enum SortableAlgoliaField { Name = 'name' } - export interface ManagerState { selectedTabId: ManagerTab searchQuery: string diff --git a/tests-ui/tests/components/dialog/footer/ManagerProgressFooter.test.ts b/tests-ui/tests/components/dialog/footer/ManagerProgressFooter.test.ts index f3c53b093..0b10ef329 100644 --- a/tests-ui/tests/components/dialog/footer/ManagerProgressFooter.test.ts +++ b/tests-ui/tests/components/dialog/footer/ManagerProgressFooter.test.ts @@ -197,9 +197,9 @@ describe('ManagerProgressFooter', () => { // Setup queue running state mockComfyManagerStore.uncompletedCount = 3 mockTaskLogs.push( - { taskName: 'Installing pack1', logs: [] }, - { taskName: 'Installing pack2', logs: [] }, - { taskName: 'Installing pack3', logs: [] } + { taskName: 'Installing pack1', taskId: '1', logs: [] }, + { taskName: 'Installing pack2', taskId: '2', logs: [] }, + { taskName: 'Installing pack3', taskId: '3', logs: [] } ) const wrapper = mountComponent() @@ -223,7 +223,7 @@ describe('ManagerProgressFooter', () => { it('should toggle expansion when expand button is clicked', async () => { mockComfyManagerStore.uncompletedCount = 1 - mockTaskLogs.push({ taskName: 'Installing', logs: [] }) + mockTaskLogs.push({ taskName: 'Installing', taskId: '1', logs: [] }) const wrapper = mountComponent() @@ -239,8 +239,8 @@ describe('ManagerProgressFooter', () => { // Setup tasks completed state mockComfyManagerStore.uncompletedCount = 0 mockTaskLogs.push( - { taskName: 'Installed pack1', logs: [] }, - { taskName: 'Installed pack2', logs: [] } + { taskName: 'Installed pack1', taskId: '1', logs: [] }, + { taskName: 'Installed pack2', taskId: '2', logs: [] } ) mockComfyManagerStore.allTasksDone = true diff --git a/tests-ui/tests/composables/useConflictDetection.test.ts b/tests-ui/tests/composables/useConflictDetection.test.ts index 78d98ea8f..9dfcb844a 100644 --- a/tests-ui/tests/composables/useConflictDetection.test.ts +++ b/tests-ui/tests/composables/useConflictDetection.test.ts @@ -3,8 +3,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { nextTick } from 'vue' import { useConflictDetection } from '@/composables/useConflictDetection' -import type { InstalledPacksResponse } from '@/types/comfyManagerTypes' import type { components } from '@/types/comfyRegistryTypes' +import type { components as ManagerComponents } from '@/types/generatedManagerTypes' + +type InstalledPacksResponse = + ManagerComponents['schemas']['InstalledPacksResponse'] // Mock dependencies vi.mock('@/scripts/api', () => ({ diff --git a/tests-ui/tests/composables/widgets/useManagerQueue.test.ts b/tests-ui/tests/composables/widgets/useManagerQueue.test.ts index 7fce2b5e1..964c0686b 100644 --- a/tests-ui/tests/composables/widgets/useManagerQueue.test.ts +++ b/tests-ui/tests/composables/widgets/useManagerQueue.test.ts @@ -1,39 +1,49 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { nextTick } from 'vue' +import { ref } from 'vue' import { useManagerQueue } from '@/composables/useManagerQueue' -import { api } from '@/scripts/api' +import { components } from '@/types/generatedManagerTypes' -vi.mock('@/scripts/api', () => ({ - api: { - addEventListener: vi.fn(), - removeEventListener: vi.fn(), - dispatchEvent: vi.fn() +// Mock dialog service +vi.mock('@/services/dialogService', () => ({ + useDialogService: () => ({ + showManagerProgressDialog: vi.fn() + }) +})) + +// Mock the app API +vi.mock('@/scripts/app', () => ({ + app: { + api: { + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + clientId: 'test-client-id' + } } })) +type ManagerTaskHistory = Record< + string, + components['schemas']['TaskHistoryItem'] +> +type ManagerTaskQueue = components['schemas']['TaskStateMessage'] + describe('useManagerQueue', () => { - const createMockTask = (result: any = 'result') => ({ - task: vi.fn().mockResolvedValue(result), - onComplete: vi.fn() - }) + let taskHistory: any + let taskQueue: any + let installedPacks: any - const createQueueWithMockTask = () => { - const queue = useManagerQueue() - const mockTask = createMockTask() - queue.enqueueTask(mockTask) - return { queue, mockTask } - } - - const getEventListenerCallback = () => - vi.mocked(api.addEventListener).mock.calls[0][1] - - const simulateServerStatus = async (status: 'all-done' | 'in_progress') => { - const event = new CustomEvent('cm-queue-status', { - detail: { status } + const createManagerQueue = () => { + taskHistory = ref({}) + taskQueue = ref({ + history: {}, + running_queue: [], + pending_queue: [], + installed_packs: {} }) - getEventListenerCallback()!(event as any) - await nextTick() + installedPacks = ref({}) + + return useManagerQueue(taskHistory, taskQueue, installedPacks) } beforeEach(() => { @@ -45,285 +55,111 @@ describe('useManagerQueue', () => { }) describe('initialization', () => { - it('should initialize with empty queue and DONE status', () => { - const queue = useManagerQueue() + it('should initialize with empty state', () => { + const queue = createManagerQueue() - expect(queue.queueLength.value).toBe(0) - expect(queue.statusMessage.value).toBe('all-done') + expect(queue.currentQueueLength.value).toBe(0) expect(queue.allTasksDone.value).toBe(true) + expect(queue.isProcessing.value).toBe(false) + expect(queue.historyCount.value).toBe(0) }) }) - describe('queue management', () => { - it('should add tasks to the queue', () => { - const queue = useManagerQueue() - const mockTask = createMockTask() + describe('task state management', () => { + it('should track task queue length', () => { + const queue = createManagerQueue() - queue.enqueueTask(mockTask) + // Add tasks to queue + taskQueue.value.running_queue = [ + { + ui_id: 'task1', + client_id: 'test-client-id', + task_name: 'Installing pack1' + } + ] + taskQueue.value.pending_queue = [ + { + ui_id: 'task2', + client_id: 'test-client-id', + task_name: 'Installing pack2' + } + ] - expect(queue.queueLength.value).toBe(1) + expect(queue.currentQueueLength.value).toBe(2) expect(queue.allTasksDone.value).toBe(false) }) - it('should clear the queue when clearQueue is called', () => { - const queue = useManagerQueue() + it('should handle empty queues', () => { + const queue = createManagerQueue() - // Add some tasks - queue.enqueueTask(createMockTask()) - queue.enqueueTask(createMockTask()) + taskQueue.value.running_queue = [] + taskQueue.value.pending_queue = [] - expect(queue.queueLength.value).toBe(2) - - // Clear the queue - queue.clearQueue() - - expect(queue.queueLength.value).toBe(0) + expect(queue.currentQueueLength.value).toBe(0) expect(queue.allTasksDone.value).toBe(true) }) }) - describe('server status handling', () => { - it('should update server status when receiving websocket events', async () => { - const queue = useManagerQueue() + describe('task history management', () => { + it('should track task history count', () => { + const queue = createManagerQueue() - await simulateServerStatus('in_progress') - - expect(queue.statusMessage.value).toBe('in_progress') - expect(queue.allTasksDone.value).toBe(false) - }) - - it('should handle invalid status values gracefully', async () => { - const queue = useManagerQueue() - - // Simulate an invalid status - const event = new CustomEvent('cm-queue-status', { - detail: null as any - }) - - getEventListenerCallback()!(event) - await nextTick() - - // Should maintain the default status - expect(queue.statusMessage.value).toBe('all-done') - }) - - it('should handle missing status property gracefully', async () => { - const queue = useManagerQueue() - - // Simulate a detail object without status property - const event = new CustomEvent('cm-queue-status', { - detail: { someOtherProperty: 'value' } as any - }) - - getEventListenerCallback()!(event) - await nextTick() - - // Should maintain the default status - expect(queue.statusMessage.value).toBe('all-done') - }) - }) - - describe('task execution', () => { - it('should start the next task when server is idle and queue has items', async () => { - const { queue, mockTask } = createQueueWithMockTask() - - await simulateServerStatus('all-done') - - // Task should have been started - expect(mockTask.task).toHaveBeenCalled() - expect(queue.queueLength.value).toBe(0) - }) - - it('should execute onComplete callback when task completes and server becomes idle', async () => { - const { mockTask } = createQueueWithMockTask() - - // Start the task - await simulateServerStatus('all-done') - expect(mockTask.task).toHaveBeenCalled() - - // Simulate task completion - await mockTask.task.mock.results[0].value - - // Simulate server cycle (in_progress -> done) - await simulateServerStatus('in_progress') - expect(mockTask.onComplete).not.toHaveBeenCalled() - - await simulateServerStatus('all-done') - expect(mockTask.onComplete).toHaveBeenCalled() - }) - - it('should handle tasks without onComplete callback', async () => { - const queue = useManagerQueue() - const mockTask = { task: vi.fn().mockResolvedValue('result') } - - queue.enqueueTask(mockTask) - - // Start the task - await simulateServerStatus('all-done') - expect(mockTask.task).toHaveBeenCalled() - - // Simulate task completion - await mockTask.task.mock.results[0].value - - // Simulate server cycle - await simulateServerStatus('in_progress') - await simulateServerStatus('all-done') - - // Should not throw errors even without onComplete - expect(queue.allTasksDone.value).toBe(true) - }) - - it('should process multiple tasks in sequence', async () => { - const queue = useManagerQueue() - const mockTask1 = createMockTask('result1') - const mockTask2 = createMockTask('result2') - - // Add tasks to the queue - queue.enqueueTask(mockTask1) - queue.enqueueTask(mockTask2) - expect(queue.queueLength.value).toBe(2) - - // Process first task - await simulateServerStatus('all-done') - expect(mockTask1.task).toHaveBeenCalled() - expect(queue.queueLength.value).toBe(1) - - // Complete first task - await mockTask1.task.mock.results[0].value - await simulateServerStatus('in_progress') - await simulateServerStatus('all-done') - expect(mockTask1.onComplete).toHaveBeenCalled() - - // Process second task - expect(mockTask2.task).toHaveBeenCalled() - expect(queue.queueLength.value).toBe(0) - - // Complete second task - await mockTask2.task.mock.results[0].value - await simulateServerStatus('in_progress') - await simulateServerStatus('all-done') - expect(mockTask2.onComplete).toHaveBeenCalled() - - // Queue should be empty and all tasks done - expect(queue.queueLength.value).toBe(0) - expect(queue.allTasksDone.value).toBe(true) - }) - - it('should handle task that returns rejected promise', async () => { - const queue = useManagerQueue() - const mockTask = { - task: vi.fn().mockRejectedValue(new Error('Task failed')), - onComplete: vi.fn() + taskHistory.value = { + task1: { + ui_id: 'task1', + client_id: 'test-client-id', + status: { status_str: 'success', completed: true } + }, + task2: { + ui_id: 'task2', + client_id: 'test-client-id', + status: { status_str: 'success', completed: true } + } } - queue.enqueueTask(mockTask) + expect(queue.historyCount.value).toBe(2) + }) - // Start the task - await simulateServerStatus('all-done') - expect(mockTask.task).toHaveBeenCalled() + it('should filter tasks by client ID', () => { + const queue = createManagerQueue() - // Let the promise rejection happen - try { - await mockTask.task() - } catch (e) { - // Ignore the error + const mockState = { + history: { + task1: { + ui_id: 'task1', + client_id: 'test-client-id', // This client + kind: 'install', + timestamp: '2024-01-01T00:00:00Z', + result: 'success', + status: { + status_str: 'success' as const, + completed: true, + messages: [] + } + }, + task2: { + ui_id: 'task2', + client_id: 'other-client-id', // Different client + kind: 'install', + timestamp: '2024-01-01T00:00:00Z', + result: 'success', + status: { + status_str: 'success' as const, + completed: true, + messages: [] + } + } + }, + running_queue: [], + pending_queue: [], + installed_packs: {} } - // Simulate server cycle - await simulateServerStatus('in_progress') - await simulateServerStatus('all-done') + queue.updateTaskState(mockState) - // onComplete should still be called for failed tasks - expect(mockTask.onComplete).toHaveBeenCalled() - }) - - it('should handle multiple multiple tasks enqueued at once while server busy', async () => { - const queue = useManagerQueue() - const mockTask1 = createMockTask() - const mockTask2 = createMockTask() - const mockTask3 = createMockTask() - - // Three tasks enqueued at once - await simulateServerStatus('in_progress') - await Promise.all([ - queue.enqueueTask(mockTask1), - queue.enqueueTask(mockTask2), - queue.enqueueTask(mockTask3) - ]) - - // Task 1 - await simulateServerStatus('all-done') - expect(mockTask1.task).toHaveBeenCalled() - - // Verify state of onComplete callbacks - expect(mockTask1.onComplete).toHaveBeenCalled() - expect(mockTask2.onComplete).not.toHaveBeenCalled() - expect(mockTask3.onComplete).not.toHaveBeenCalled() - - // Verify state of queue - expect(queue.queueLength.value).toBe(2) - expect(queue.allTasksDone.value).toBe(false) - - // Task 2 - await simulateServerStatus('in_progress') - await simulateServerStatus('all-done') - expect(mockTask2.task).toHaveBeenCalled() - - // Verify state of onComplete callbacks - expect(mockTask2.onComplete).toHaveBeenCalled() - expect(mockTask3.onComplete).not.toHaveBeenCalled() - - // Verify state of queue - expect(queue.queueLength.value).toBe(1) - expect(queue.allTasksDone.value).toBe(false) - - // Task 3 - await simulateServerStatus('in_progress') - await simulateServerStatus('all-done') - - // Verify state of onComplete callbacks - expect(mockTask3.task).toHaveBeenCalled() - expect(mockTask3.onComplete).toHaveBeenCalled() - - // Verify state of queue - expect(queue.queueLength.value).toBe(0) - expect(queue.allTasksDone.value).toBe(true) - }) - - it('should handle adding tasks while processing is in progress', async () => { - const queue = useManagerQueue() - const mockTask1 = createMockTask() - const mockTask2 = createMockTask() - - // Add first task and start processing - queue.enqueueTask(mockTask1) - await simulateServerStatus('all-done') - expect(mockTask1.task).toHaveBeenCalled() - - // Add second task while first is processing - queue.enqueueTask(mockTask2) - expect(queue.queueLength.value).toBe(1) - - // Complete first task - await mockTask1.task.mock.results[0].value - await simulateServerStatus('in_progress') - await simulateServerStatus('all-done') - - // Second task should now be processed - expect(mockTask2.task).toHaveBeenCalled() - }) - - it('should handle server status changes without tasks in queue', async () => { - const queue = useManagerQueue() - - // Cycle server status without any tasks - await simulateServerStatus('in_progress') - await simulateServerStatus('all-done') - await simulateServerStatus('in_progress') - await simulateServerStatus('all-done') - - // Should not cause any errors - expect(queue.allTasksDone.value).toBe(true) + // Should only include task from this client + expect(taskHistory.value).toHaveProperty('task1') + expect(taskHistory.value).not.toHaveProperty('task2') }) }) }) diff --git a/tests-ui/tests/store/comfyManagerStore.test.ts b/tests-ui/tests/store/comfyManagerStore.test.ts index 1ef3aa075..1f536c9b2 100644 --- a/tests-ui/tests/store/comfyManagerStore.test.ts +++ b/tests-ui/tests/store/comfyManagerStore.test.ts @@ -4,12 +4,14 @@ import { nextTick, ref } from 'vue' import { useComfyManagerService } from '@/services/comfyManagerService' import { useComfyManagerStore } from '@/stores/comfyManagerStore' -import { - InstalledPacksResponse, - ManagerChannel, - ManagerDatabaseSource, - ManagerPackInstalled -} from '@/types/comfyManagerTypes' +import { components as ManagerComponents } from '@/types/generatedManagerTypes' + +type InstalledPacksResponse = + ManagerComponents['schemas']['InstalledPacksResponse'] +type ManagerChannel = ManagerComponents['schemas']['ManagerChannel'] +type ManagerDatabaseSource = + ManagerComponents['schemas']['ManagerDatabaseSource'] +type ManagerPackInstalled = ManagerComponents['schemas']['ManagerPackInstalled'] vi.mock('@/services/comfyManagerService', () => ({ useComfyManagerService: vi.fn() @@ -82,8 +84,8 @@ describe('useComfyManagerStore', () => { isLoading: ref(false), error: ref(null), startQueue: vi.fn().mockResolvedValue(null), - resetQueue: vi.fn().mockResolvedValue(null), getQueueStatus: vi.fn().mockResolvedValue(null), + getTaskHistory: vi.fn().mockResolvedValue(null), listInstalledPacks: vi.fn().mockResolvedValue({}), getImportFailInfo: vi.fn().mockResolvedValue(null), getImportFailInfoBulk: vi.fn().mockResolvedValue({}), @@ -367,8 +369,8 @@ describe('useComfyManagerStore', () => { await store.installPack.call({ id: 'test-pack', repository: 'https://github.com/test/test-pack', - channel: ManagerChannel.DEV, - mode: ManagerDatabaseSource.CACHE, + channel: 'dev' as ManagerChannel, + mode: 'cache' as ManagerDatabaseSource, selected_version: 'latest', version: 'latest' }) @@ -384,8 +386,8 @@ describe('useComfyManagerStore', () => { await store.installPack.call({ id: 'test-pack', repository: 'https://github.com/test/test-pack', - channel: ManagerChannel.DEV, - mode: ManagerDatabaseSource.CACHE, + channel: 'dev' as ManagerChannel, + mode: 'cache' as ManagerDatabaseSource, selected_version: 'latest', version: 'latest' }) @@ -397,8 +399,8 @@ describe('useComfyManagerStore', () => { await store.installPack.call({ id: 'another-pack', repository: 'https://github.com/test/another-pack', - channel: ManagerChannel.DEV, - mode: ManagerDatabaseSource.CACHE, + channel: 'dev' as ManagerChannel, + mode: 'cache' as ManagerDatabaseSource, selected_version: 'latest', version: 'latest' }) @@ -415,8 +417,8 @@ describe('useComfyManagerStore', () => { await store.installPack.call({ id: 'pack-1', repository: 'https://github.com/test/pack-1', - channel: ManagerChannel.DEV, - mode: ManagerDatabaseSource.CACHE, + channel: 'dev' as ManagerChannel, + mode: 'cache' as ManagerDatabaseSource, selected_version: 'latest', version: 'latest' }) @@ -425,8 +427,8 @@ describe('useComfyManagerStore', () => { await store.installPack.call({ id: 'pack-2', repository: 'https://github.com/test/pack-2', - channel: ManagerChannel.DEV, - mode: ManagerDatabaseSource.CACHE, + channel: 'dev' as ManagerChannel, + mode: 'cache' as ManagerDatabaseSource, selected_version: 'latest', version: 'latest' })