Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: improve multi-package selection handling
- 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 <[email protected]>
  • Loading branch information
viva-jinyi and claude committed Aug 27, 2025
commit bfe089ff35ca2251f320075666ab7d1665485d6a
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,32 @@
</div>
</template>
<template #install-button>
<!-- Mixed: Don't show any button -->
<div v-if="isMixed" class="text-sm text-neutral-500">
{{ $t('manager.mixedSelectionMessage') }}
</div>
<!-- All installed: Show uninstall button -->
<PackUninstallButton
v-if="isAllInstalled"
v-bind="$attrs"
v-else-if="isAllInstalled"
size="md"
:node-packs="nodePacks"
:node-packs="installedPacks"
/>
<!-- None installed: Show install button -->
<PackInstallButton
v-else
v-bind="$attrs"
v-else-if="isNoneInstalled"
size="md"
:node-packs="nodePacks"
:node-packs="notInstalledPacks"
:has-conflict="hasConflicts"
:conflict-info="conflictInfo"
/>
</template>
</InfoPanelHeader>
<div class="mb-6">
<MetadataRow :label="$t('g.status')">
<PackStatusMessage status-type="NodeVersionStatusActive" />
<PackStatusMessage
:status-type="overallStatus"
:has-compatibility-issues="hasConflicts"
/>
</MetadataRow>
<MetadataRow
:label="$t('manager.totalNodes')"
Expand All @@ -46,36 +55,135 @@

<script setup lang="ts">
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'
import PackUninstallButton from '@/components/dialog/content/manager/button/PackUninstallButton.vue'
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<string, ConflictDetail[]>()

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<ConflictDetail[]>(() => {
const conflictMap = new Map<string, ConflictDetail>()

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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<!-- blur background -->
<div
v-if="imgSrc"
class="absolute inset-0 bg-cover bg-center bg-no-repeat opacity-30"
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{
backgroundImage: `url(${imgSrc})`,
filter: 'blur(10px)'
Expand Down
1 change: 1 addition & 0 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@
"installSelected": "Install Selected",
"installAllMissingNodes": "Install All Missing Nodes",
"packsSelected": "packs selected",
"mixedSelectionMessage": "Cannot perform bulk action on mixed selection",
"status": {
"active": "Active",
"pending": "Pending",
Expand Down