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 <noreply@anthropic.com>
This commit is contained in:
Jin Yi
2025-08-18 10:43:44 +09:00
parent bc3ba768fd
commit accd039de5
4 changed files with 152 additions and 12 deletions

View File

@@ -18,6 +18,7 @@
import { inject, ref } from 'vue'
import PackActionButton from '@/components/dialog/content/manager/button/PackActionButton.vue'
import { useConflictDetection } from '@/composables/useConflictDetection'
import { t } from '@/i18n'
import { useDialogService } from '@/services/dialogService'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
@@ -75,15 +76,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,

View File

@@ -14,12 +14,32 @@
</div>
</template>
<template #install-button>
<PackInstallButton :full-width="true" :node-packs="nodePacks" />
<!-- 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-else-if="isAllInstalled"
:full-width="true"
:node-packs="installedPacks"
/>
<!-- None installed: Show install button -->
<PackInstallButton
v-else-if="isNoneInstalled"
:full-width="true"
: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')"
@@ -35,22 +55,135 @@
<script setup lang="ts">
import { useAsyncState } from '@vueuse/core'
import { computed, onUnmounted } 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 managerStore = useComfyManagerStore()
const conflictDetectionStore = useConflictDetectionStore()
const { checkNodeCompatibility } = useConflictDetection()
const { getNodeDefs } = useComfyRegistryStore()
// 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
)
})
})
// 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({

View File

@@ -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)'

View File

@@ -200,6 +200,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",