From bfe089ff35ca2251f320075666ab7d1665485d6a Mon Sep 17 00:00:00 2001 From: Jin Yi Date: Mon, 18 Aug 2025 10:43:44 +0900 Subject: [PATCH 1/4] feat: improve multi-package selection handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Check each package individually for conflicts in install dialog - Show only packages with actual conflicts in warning dialog - Hide action buttons for mixed installed/uninstalled selections - Display dynamic status based on selected packages priority - Deduplicate conflict information across multiple packages - Fix PackIcon blur background opacity 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../manager/button/PackInstallButton.vue | 22 ++- .../manager/infoPanel/InfoPanelMultiItem.vue | 142 +++++++++++++++--- .../content/manager/packIcon/PackIcon.vue | 2 +- src/locales/en/main.json | 1 + 4 files changed, 141 insertions(+), 26 deletions(-) diff --git a/src/components/dialog/content/manager/button/PackInstallButton.vue b/src/components/dialog/content/manager/button/PackInstallButton.vue index 414e55f7f1..50739e407b 100644 --- a/src/components/dialog/content/manager/button/PackInstallButton.vue +++ b/src/components/dialog/content/manager/button/PackInstallButton.vue @@ -27,6 +27,7 @@ import { computed } from 'vue' import IconTextButton from '@/components/button/IconTextButton.vue' import DotSpinner from '@/components/common/DotSpinner.vue' +import { useConflictDetection } from '@/composables/useConflictDetection' import { t } from '@/i18n' import { useDialogService } from '@/services/dialogService' import { useComfyManagerStore } from '@/stores/comfyManagerStore' @@ -96,15 +97,20 @@ const installAllPacks = async () => { if (!nodePacks?.length) return if (hasConflict && conflictInfo) { - const conflictedPackages: ConflictDetectionResult[] = nodePacks.map( - (pack) => ({ - package_id: pack.id || '', - package_name: pack.name || '', - has_conflict: true, - conflicts: conflictInfo || [], - is_compatible: false + // Check each package individually for conflicts + const { checkNodeCompatibility } = useConflictDetection() + const conflictedPackages: ConflictDetectionResult[] = nodePacks + .map((pack) => { + const compatibilityCheck = checkNodeCompatibility(pack) + return { + package_id: pack.id || '', + package_name: pack.name || '', + has_conflict: compatibilityCheck.hasConflict, + conflicts: compatibilityCheck.conflicts, + is_compatible: !compatibilityCheck.hasConflict + } }) - ) + .filter((result) => result.has_conflict) // Only show packages with conflicts showNodeConflictDialog({ conflictedPackages, diff --git a/src/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue b/src/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue index ece04696e7..0670132a2b 100644 --- a/src/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue +++ b/src/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue @@ -14,23 +14,32 @@
- + import { useAsyncState } from '@vueuse/core' -import { computed, onUnmounted, ref, watch } from 'vue' +import { computed, onUnmounted, provide } from 'vue' import PackStatusMessage from '@/components/dialog/content/manager/PackStatusMessage.vue' import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue' @@ -54,28 +63,127 @@ import PackUninstallButton from '@/components/dialog/content/manager/button/Pack import InfoPanelHeader from '@/components/dialog/content/manager/infoPanel/InfoPanelHeader.vue' import MetadataRow from '@/components/dialog/content/manager/infoPanel/MetadataRow.vue' import PackIconStacked from '@/components/dialog/content/manager/packIcon/PackIconStacked.vue' +import { useConflictDetection } from '@/composables/useConflictDetection' import { useComfyManagerStore } from '@/stores/comfyManagerStore' import { useComfyRegistryStore } from '@/stores/comfyRegistryStore' +import { useConflictDetectionStore } from '@/stores/conflictDetectionStore' import { components } from '@/types/comfyRegistryTypes' +import type { ConflictDetail } from '@/types/conflictDetectionTypes' +import { ImportFailedKey } from '@/types/importFailedTypes' const { nodePacks } = defineProps<{ nodePacks: components['schemas']['Node'][] }>() -const { getNodeDefs } = useComfyRegistryStore() const managerStore = useComfyManagerStore() +const conflictDetectionStore = useConflictDetectionStore() +const { checkNodeCompatibility } = useConflictDetection() + +const { getNodeDefs } = useComfyRegistryStore() -const isAllInstalled = ref(false) -watch( - [() => nodePacks, () => managerStore.installedPacks], - () => { - isAllInstalled.value = nodePacks.every((nodePack) => - managerStore.isPackInstalled(nodePack.id) +// Check if any package has import failed status +const hasImportFailed = computed(() => { + return nodePacks.some((pack) => { + if (!pack.id) return false + const conflicts = conflictDetectionStore.getConflictsForPackageByID(pack.id) + return ( + conflicts?.conflicts?.some((c) => c.type === 'import_failed') || false ) - }, - { immediate: true } + }) +}) + +// Provide import failed context for PackStatusMessage +provide(ImportFailedKey, { + importFailed: hasImportFailed, + showImportFailedDialog: () => {} // No-op for multi-selection +}) + +// Check installation status +const installedPacks = computed(() => + nodePacks.filter((pack) => managerStore.isPackInstalled(pack.id)) +) + +const notInstalledPacks = computed(() => + nodePacks.filter((pack) => !managerStore.isPackInstalled(pack.id)) +) + +const isAllInstalled = computed( + () => installedPacks.value.length === nodePacks.length +) + +const isNoneInstalled = computed( + () => notInstalledPacks.value.length === nodePacks.length +) + +const isMixed = computed( + () => installedPacks.value.length > 0 && notInstalledPacks.value.length > 0 ) +// Check for conflicts in not-installed packages - store per package +const packageConflicts = computed(() => { + const conflictsByPackage = new Map() + + for (const pack of notInstalledPacks.value) { + const compatibilityCheck = checkNodeCompatibility(pack) + if (compatibilityCheck.hasConflict && pack.id) { + conflictsByPackage.set(pack.id, compatibilityCheck.conflicts) + } + } + + return conflictsByPackage +}) + +// Aggregate all unique conflicts for display +const conflictInfo = computed(() => { + const conflictMap = new Map() + + packageConflicts.value.forEach((conflicts) => { + conflicts.forEach((conflict) => { + const key = `${conflict.type}-${conflict.current_value}-${conflict.required_value}` + if (!conflictMap.has(key)) { + conflictMap.set(key, conflict) + } + }) + }) + + return Array.from(conflictMap.values()) +}) + +const hasConflicts = computed(() => conflictInfo.value.length > 0) + +// Determine the most important status from all selected packages +const overallStatus = computed(() => { + // Check for import failed first (highest priority for installed packages) + if (hasImportFailed.value) { + // Import failed doesn't have a specific status enum, so we return active + // but the PackStatusMessage will handle it via hasImportFailed prop + return 'NodeVersionStatusActive' as components['schemas']['NodeVersionStatus'] + } + + // Priority order: banned > deleted > flagged > pending > active + const statusPriority = [ + 'NodeStatusBanned', + 'NodeVersionStatusBanned', + 'NodeStatusDeleted', + 'NodeVersionStatusDeleted', + 'NodeVersionStatusFlagged', + 'NodeVersionStatusPending', + 'NodeStatusActive', + 'NodeVersionStatusActive' + ] + + for (const priorityStatus of statusPriority) { + if (nodePacks.some((pack) => pack.status === priorityStatus)) { + return priorityStatus as + | components['schemas']['NodeStatus'] + | components['schemas']['NodeVersionStatus'] + } + } + + // Default to active if no specific status found + return 'NodeVersionStatusActive' as components['schemas']['NodeVersionStatus'] +}) + const getPackNodes = async (pack: components['schemas']['Node']) => { if (!pack.latest_version?.version) return [] const nodeDefs = await getNodeDefs.call({ diff --git a/src/components/dialog/content/manager/packIcon/PackIcon.vue b/src/components/dialog/content/manager/packIcon/PackIcon.vue index 71ec4a400e..ae6d188c20 100644 --- a/src/components/dialog/content/manager/packIcon/PackIcon.vue +++ b/src/components/dialog/content/manager/packIcon/PackIcon.vue @@ -13,7 +13,7 @@
Date: Mon, 18 Aug 2025 11:13:03 +0900 Subject: [PATCH 2/4] refactor: extract multi-package logic into reusable composables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create usePackageSelection composable for installation state management - Create usePackageStatus composable for status priority logic - Refactor InfoPanelMultiItem to use new composables - Reduce component complexity by separating business logic - Improve code reusability across components 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../manager/infoPanel/InfoPanelMultiItem.vue | 89 ++++--------------- src/composables/usePackageSelection.ts | 51 +++++++++++ src/composables/usePackageStatus.ts | 63 +++++++++++++ 3 files changed, 131 insertions(+), 72 deletions(-) create mode 100644 src/composables/usePackageSelection.ts create mode 100644 src/composables/usePackageStatus.ts diff --git a/src/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue b/src/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue index 0670132a2b..1edee19ee0 100644 --- a/src/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue +++ b/src/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue @@ -55,7 +55,7 @@