mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-27 18:24:11 +00:00
Manager Conflict Nofitication (#4443)
Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: bymyself <cbyrne@comfy.org> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -27,7 +27,7 @@
|
||||
/>
|
||||
|
||||
<template v-if="item.footerComponent" #footer>
|
||||
<component :is="item.footerComponent" />
|
||||
<component :is="item.footerComponent" v-bind="item.footerProps" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -26,6 +26,27 @@
|
||||
}"
|
||||
>
|
||||
<div class="px-6 flex flex-col h-full">
|
||||
<!-- Conflict Warning Banner -->
|
||||
<div
|
||||
v-if="shouldShowManagerBanner"
|
||||
class="bg-yellow-600 bg-opacity-20 border border-yellow-400 rounded-lg p-4 mt-3 mb-4 flex items-center gap-6"
|
||||
>
|
||||
<i class="pi pi-exclamation-triangle text-yellow-600 text-lg"></i>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-sm font-bold m-0">
|
||||
{{ $t('manager.conflicts.warningBanner.title') }}
|
||||
</p>
|
||||
<p class="text-xs m-0">
|
||||
{{ $t('manager.conflicts.warningBanner.message') }}
|
||||
</p>
|
||||
<p
|
||||
class="text-sm font-bold m-0 cursor-pointer"
|
||||
@click="onClickWarningLink"
|
||||
>
|
||||
{{ $t('manager.conflicts.warningBanner.button') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<RegistrySearchBar
|
||||
v-model:searchQuery="searchQuery"
|
||||
v-model:searchMode="searchMode"
|
||||
@@ -70,7 +91,9 @@
|
||||
:is-selected="
|
||||
selectedNodePacks.some((pack) => pack.id === item.id)
|
||||
"
|
||||
@click.stop="(event) => selectNodePack(item, event)"
|
||||
@click.stop="
|
||||
(event: MouseEvent) => selectNodePack(item, event)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</VirtualGrid>
|
||||
@@ -102,7 +125,8 @@ import {
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
ref,
|
||||
watch
|
||||
watch,
|
||||
watchEffect
|
||||
} from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -120,7 +144,9 @@ import { useManagerStatePersistence } from '@/composables/manager/useManagerStat
|
||||
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
|
||||
import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus'
|
||||
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
|
||||
import { useConflictBannerState } from '@/composables/useConflictBannerState'
|
||||
import { useRegistrySearch } from '@/composables/useRegistrySearch'
|
||||
import { useComfyRegistryService } from '@/services/comfyRegistryService'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
||||
import type { TabItem } from '@/types/comfyManagerTypes'
|
||||
@@ -134,6 +160,8 @@ const { initialTab } = defineProps<{
|
||||
const { t } = useI18n()
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const { getPackById } = useComfyRegistryStore()
|
||||
const registryService = useComfyRegistryService()
|
||||
const conflictBannerState = useConflictBannerState()
|
||||
const persistedState = useManagerStatePersistence()
|
||||
const initialState = persistedState.loadStoredState()
|
||||
|
||||
@@ -150,6 +178,9 @@ const {
|
||||
toggle: toggleSideNav
|
||||
} = useResponsiveCollapse()
|
||||
|
||||
// Use conflict banner state from composable
|
||||
const { shouldShowManagerBanner, markConflictsAsSeen } = conflictBannerState
|
||||
|
||||
const tabs = ref<TabItem[]>([
|
||||
{ id: ManagerTab.All, label: t('g.all'), icon: 'pi-list' },
|
||||
{ id: ManagerTab.Installed, label: t('g.installed'), icon: 'pi-box' },
|
||||
@@ -313,6 +344,13 @@ watch([isAllTab, searchResults], () => {
|
||||
displayPacks.value = searchResults.value
|
||||
})
|
||||
|
||||
const onClickWarningLink = () => {
|
||||
window.open(
|
||||
'https://docs.comfy.org/troubleshooting/custom-node-issues',
|
||||
'_blank'
|
||||
)
|
||||
}
|
||||
|
||||
const onResultsChange = () => {
|
||||
switch (selectedTab.value?.id) {
|
||||
case ManagerTab.Installed:
|
||||
@@ -441,11 +479,57 @@ whenever(selectedNodePack, async () => {
|
||||
if (hasMultipleSelections.value) return
|
||||
// Only fetch if we haven't already for this pack
|
||||
if (lastFetchedPackId.value === pack.id) return
|
||||
const data = await getPackById.call(pack.id)
|
||||
|
||||
let data = null
|
||||
|
||||
// For installed nodes only, fetch version-specific information
|
||||
if (comfyManagerStore.isPackInstalled(pack.id)) {
|
||||
const installedPack = comfyManagerStore.getInstalledPackByCnrId(pack.id)
|
||||
if (installedPack?.ver) {
|
||||
// Fetch information for the installed version
|
||||
data = await registryService.getPackByVersion(pack.id, installedPack.ver)
|
||||
|
||||
// Race condition check: ensure selected pack hasn't changed during async operation
|
||||
if (selectedNodePack.value?.id !== pack.id) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For uninstalled nodes or if version-specific data fetch failed, use default API
|
||||
if (!data) {
|
||||
data = await getPackById.call(pack.id)
|
||||
}
|
||||
|
||||
// Race condition check: ensure selected pack hasn't changed during async operations
|
||||
if (selectedNodePack.value?.id !== pack.id) {
|
||||
return
|
||||
}
|
||||
|
||||
// If selected node hasn't changed since request, merge registry & Algolia data
|
||||
if (data?.id === pack.id) {
|
||||
const isNodeData = data && 'id' in data && data.id === pack.id
|
||||
const isVersionData = data && 'node_id' in data && data.node_id === pack.id
|
||||
|
||||
if (isNodeData || isVersionData) {
|
||||
lastFetchedPackId.value = pack.id
|
||||
const mergedPack = merge({}, pack, data)
|
||||
|
||||
// Merge API data first, then pack data (API data takes priority)
|
||||
const mergedPack = merge({}, data, pack)
|
||||
|
||||
// Ensure compatibility fields from API data take priority
|
||||
if (data?.supported_os !== undefined) {
|
||||
mergedPack.supported_os = data.supported_os
|
||||
}
|
||||
if (data?.supported_accelerators !== undefined) {
|
||||
mergedPack.supported_accelerators = data.supported_accelerators
|
||||
}
|
||||
if (data?.supported_comfyui_version !== undefined) {
|
||||
mergedPack.supported_comfyui_version = data.supported_comfyui_version
|
||||
}
|
||||
if (data?.supported_comfyui_frontend_version !== undefined) {
|
||||
mergedPack.supported_comfyui_frontend_version =
|
||||
data.supported_comfyui_frontend_version
|
||||
}
|
||||
// Update the pack in current selection without changing selection state
|
||||
const packIndex = selectedNodePacks.value.findIndex(
|
||||
(p) => p.id === mergedPack.id
|
||||
@@ -473,6 +557,14 @@ watch([searchQuery, selectedTab], () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Automatically mark conflicts as seen when banner is displayed
|
||||
// This ensures red dots disappear and banner is dismissed once user sees it
|
||||
watchEffect(() => {
|
||||
if (shouldShowManagerBanner.value) {
|
||||
markConflictsAsSeen()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
persistedState.persistState({
|
||||
selectedTabId: selectedTab.value?.id,
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<div class="w-[552px] max-h-[246px] flex flex-col">
|
||||
<ContentDivider :width="1" />
|
||||
<div class="px-4 py-6 w-full h-full flex flex-col gap-2">
|
||||
<!-- Description -->
|
||||
<!-- <div>
|
||||
<p class="text-sm leading-4 text-gray-100 m-0 mb-4">
|
||||
{{ $t('manager.conflicts.description') }}
|
||||
<br /><br />
|
||||
{{ $t('manager.conflicts.info') }}
|
||||
</p>
|
||||
</div> -->
|
||||
<!-- Conflict List Wrapper -->
|
||||
<div
|
||||
class="w-full flex flex-col bg-neutral-200 dark-theme:bg-black min-h-8 rounded-lg"
|
||||
>
|
||||
<div
|
||||
class="w-full h-8 flex items-center justify-between gap-2 pl-4"
|
||||
@click="toggleConflictsPanel"
|
||||
>
|
||||
<div class="flex-1 flex">
|
||||
<span
|
||||
class="text-xs font-bold text-yellow-600 dark-theme:text-yellow-400 mr-2"
|
||||
>{{ allConflictDetails.length }}</span
|
||||
>
|
||||
<span
|
||||
class="text-xs font-bold text-neutral-600 dark-theme:text-white"
|
||||
>{{ $t('manager.conflicts.conflicts') }}</span
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
:icon="
|
||||
conflictsExpanded
|
||||
? 'pi pi-chevron-down text-xs'
|
||||
: 'pi pi-chevron-right text-xs'
|
||||
"
|
||||
text
|
||||
class="text-neutral-600 dark-theme:text-neutral-300 !bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Conflicts list -->
|
||||
<div
|
||||
v-if="conflictsExpanded"
|
||||
class="py-2 px-4 flex flex-col gap-2.5 max-h-[142px] overflow-y-auto scrollbar-hide"
|
||||
>
|
||||
<div
|
||||
v-for="(conflict, i) in allConflictDetails"
|
||||
:key="i"
|
||||
class="flex items-center justify-between h-6 px-4 flex-shrink-0 conflict-list-item"
|
||||
>
|
||||
<span
|
||||
class="text-xs text-neutral-600 dark-theme:text-neutral-300"
|
||||
>{{ getConflictMessage(conflict, t) }}</span
|
||||
>
|
||||
<span class="pi pi-info-circle text-sm"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Extension List Wrapper -->
|
||||
<div
|
||||
class="w-full flex flex-col bg-neutral-200 dark-theme:bg-black min-h-8 rounded-lg"
|
||||
>
|
||||
<div
|
||||
class="w-full h-8 flex items-center justify-between gap-2 pl-4"
|
||||
@click="toggleExtensionsPanel"
|
||||
>
|
||||
<div class="flex-1 flex">
|
||||
<span
|
||||
class="text-xs font-bold text-yellow-600 dark-theme:text-yellow-400 mr-2"
|
||||
>{{ conflictData.length }}</span
|
||||
>
|
||||
<span
|
||||
class="text-xs font-bold text-neutral-600 dark-theme:text-white"
|
||||
>{{ $t('manager.conflicts.extensionAtRisk') }}</span
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
:icon="
|
||||
extensionsExpanded
|
||||
? 'pi pi-chevron-down text-xs'
|
||||
: 'pi pi-chevron-right text-xs'
|
||||
"
|
||||
text
|
||||
class="text-neutral-600 dark-theme:text-neutral-300 !bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Extension list -->
|
||||
<div
|
||||
v-if="extensionsExpanded"
|
||||
class="py-2 px-4 flex flex-col gap-2.5 max-h-[142px] overflow-y-auto scrollbar-hide"
|
||||
>
|
||||
<div
|
||||
v-for="conflictResult in conflictData"
|
||||
:key="conflictResult.package_id"
|
||||
class="flex items-center justify-between h-6 px-4 flex-shrink-0 conflict-list-item"
|
||||
>
|
||||
<span class="text-xs text-neutral-600 dark-theme:text-neutral-300">
|
||||
{{ conflictResult.package_name }}
|
||||
</span>
|
||||
<span class="pi pi-info-circle text-sm"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ContentDivider :width="1" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
|
||||
import { getConflictMessage } from '@/utils/conflictMessageUtil'
|
||||
|
||||
interface Props {
|
||||
conflicts?: ConflictDetectionResult[]
|
||||
conflictedPackages?: ConflictDetectionResult[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
conflicts: () => [],
|
||||
conflictedPackages: () => []
|
||||
})
|
||||
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const conflictsExpanded = ref<boolean>(false)
|
||||
const extensionsExpanded = ref<boolean>(false)
|
||||
|
||||
// Use conflictedPackages if provided, otherwise fallback to conflicts
|
||||
const conflictData = computed(() =>
|
||||
props.conflictedPackages.length > 0
|
||||
? props.conflictedPackages
|
||||
: props.conflicts
|
||||
)
|
||||
|
||||
const allConflictDetails = computed(() =>
|
||||
conflictData.value.flatMap((result) => result.conflicts)
|
||||
)
|
||||
|
||||
const toggleConflictsPanel = () => {
|
||||
conflictsExpanded.value = !conflictsExpanded.value
|
||||
extensionsExpanded.value = false
|
||||
}
|
||||
|
||||
const toggleExtensionsPanel = () => {
|
||||
extensionsExpanded.value = !extensionsExpanded.value
|
||||
conflictsExpanded.value = false
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.conflict-list-item:hover {
|
||||
background-color: rgba(0, 122, 255, 0.2);
|
||||
}
|
||||
</style>
|
||||
59
src/components/dialog/content/manager/NodeConflictFooter.vue
Normal file
59
src/components/dialog/content/manager/NodeConflictFooter.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-between w-full px-3 py-4">
|
||||
<div class="w-full flex items-center justify-between gap-2 pr-1">
|
||||
<Button
|
||||
:label="$t('manager.conflicts.conflictInfoTitle')"
|
||||
text
|
||||
severity="secondary"
|
||||
size="small"
|
||||
icon="pi pi-info-circle"
|
||||
:pt="{
|
||||
label: { class: 'text-sm' }
|
||||
}"
|
||||
@click="handleConflictInfoClick"
|
||||
/>
|
||||
<Button
|
||||
v-if="props.buttonText"
|
||||
:label="props.buttonText"
|
||||
severity="secondary"
|
||||
size="small"
|
||||
@click="handleButtonClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
interface Props {
|
||||
buttonText?: string
|
||||
onButtonClick?: () => void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
buttonText: undefined,
|
||||
onButtonClick: undefined
|
||||
})
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
const handleConflictInfoClick = () => {
|
||||
window.open(
|
||||
'https://docs.comfy.org/troubleshooting/custom-node-issues',
|
||||
'_blank'
|
||||
)
|
||||
}
|
||||
|
||||
const handleButtonClick = () => {
|
||||
// Close the conflict dialog
|
||||
dialogStore.closeDialog({ key: 'global-node-conflict' })
|
||||
|
||||
// Execute the custom button action if provided
|
||||
if (props.onButtonClick) {
|
||||
props.onButtonClick()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
12
src/components/dialog/content/manager/NodeConflictHeader.vue
Normal file
12
src/components/dialog/content/manager/NodeConflictHeader.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div class="h-12 flex items-center justify-between w-full pl-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Warning Icon -->
|
||||
<i class="pi pi-exclamation-triangle text-lg"></i>
|
||||
<!-- Title -->
|
||||
<p class="text-base font-bold">
|
||||
{{ $t('manager.conflicts.title') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -10,6 +10,14 @@ import enMessages from '@/locales/en/main.json'
|
||||
import PackVersionBadge from './PackVersionBadge.vue'
|
||||
import PackVersionSelectorPopover from './PackVersionSelectorPopover.vue'
|
||||
|
||||
// Mock config to prevent __COMFYUI_FRONTEND_VERSION__ error
|
||||
vi.mock('@/config', () => ({
|
||||
default: {
|
||||
app_title: 'ComfyUI',
|
||||
app_version: '1.0.0'
|
||||
}
|
||||
}))
|
||||
|
||||
const mockNodePack = {
|
||||
id: 'test-pack',
|
||||
name: 'Test Pack',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="inline-flex items-center gap-1 rounded-2xl text-xs cursor-pointer px-2 py-1"
|
||||
:class="{ 'bg-gray-100 dark-theme:bg-neutral-700': fill }"
|
||||
class="inline-flex items-center gap-1 rounded-2xl text-xs cursor-pointer py-1"
|
||||
:class="{ 'bg-gray-100 dark-theme:bg-neutral-700 px-1.5': fill }"
|
||||
aria-haspopup="true"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@@ -12,8 +12,7 @@
|
||||
>
|
||||
<i
|
||||
v-if="isUpdateAvailable"
|
||||
class="pi pi-arrow-circle-up text-blue-600"
|
||||
style="font-size: 8px"
|
||||
class="pi pi-arrow-circle-up text-blue-600 text-xs"
|
||||
/>
|
||||
<span>{{ installedVersion }}</span>
|
||||
<i class="pi pi-chevron-right" style="font-size: 8px" />
|
||||
|
||||
@@ -3,10 +3,13 @@ import { createPinia } from 'pinia'
|
||||
import Button from 'primevue/button'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Listbox from 'primevue/listbox'
|
||||
import Select from 'primevue/select'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import VerifiedIcon from '@/components/icons/VerifiedIcon.vue'
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
|
||||
// SelectedVersion is now using direct strings instead of enum
|
||||
@@ -15,7 +18,17 @@ import PackVersionSelectorPopover from './PackVersionSelectorPopover.vue'
|
||||
|
||||
// Default mock versions for reference
|
||||
const defaultMockVersions = [
|
||||
{ version: '1.0.0', createdAt: '2023-01-01' },
|
||||
{
|
||||
version: '1.0.0',
|
||||
createdAt: '2023-01-01',
|
||||
supported_os: ['windows', 'linux'],
|
||||
supported_accelerators: ['CPU'],
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0',
|
||||
supported_python_version: '>=3.8',
|
||||
is_banned: false,
|
||||
has_registry_data: true
|
||||
},
|
||||
{ version: '0.9.0', createdAt: '2022-12-01' },
|
||||
{ version: '0.8.0', createdAt: '2022-11-01' }
|
||||
]
|
||||
@@ -23,13 +36,24 @@ const defaultMockVersions = [
|
||||
const mockNodePack = {
|
||||
id: 'test-pack',
|
||||
name: 'Test Pack',
|
||||
latest_version: { version: '1.0.0' },
|
||||
repository: 'https://github.com/user/repo'
|
||||
latest_version: {
|
||||
version: '1.0.0',
|
||||
supported_os: ['windows', 'linux'],
|
||||
supported_accelerators: ['CPU'],
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0',
|
||||
supported_python_version: '>=3.8',
|
||||
is_banned: false,
|
||||
has_registry_data: true
|
||||
},
|
||||
repository: 'https://github.com/user/repo',
|
||||
has_registry_data: true
|
||||
}
|
||||
|
||||
// Create mock functions
|
||||
const mockGetPackVersions = vi.fn()
|
||||
const mockInstallPack = vi.fn().mockResolvedValue(undefined)
|
||||
const mockCheckVersionCompatibility = vi.fn()
|
||||
|
||||
// Mock the registry service
|
||||
vi.mock('@/services/comfyRegistryService', () => ({
|
||||
@@ -50,6 +74,13 @@ vi.mock('@/stores/comfyManagerStore', () => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock the conflict detection composable
|
||||
vi.mock('@/composables/useConflictDetection', () => ({
|
||||
useConflictDetection: vi.fn(() => ({
|
||||
checkVersionCompatibility: mockCheckVersionCompatibility
|
||||
}))
|
||||
}))
|
||||
|
||||
const waitForPromises = async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 16))
|
||||
await nextTick()
|
||||
@@ -60,6 +91,9 @@ describe('PackVersionSelectorPopover', () => {
|
||||
vi.clearAllMocks()
|
||||
mockGetPackVersions.mockReset()
|
||||
mockInstallPack.mockReset().mockResolvedValue(undefined)
|
||||
mockCheckVersionCompatibility
|
||||
.mockReset()
|
||||
.mockReturnValue({ hasConflict: false, conflicts: [] })
|
||||
})
|
||||
|
||||
const mountComponent = ({
|
||||
@@ -79,7 +113,12 @@ describe('PackVersionSelectorPopover', () => {
|
||||
global: {
|
||||
plugins: [PrimeVue, createPinia(), i18n],
|
||||
components: {
|
||||
Listbox
|
||||
Listbox,
|
||||
VerifiedIcon,
|
||||
Select
|
||||
},
|
||||
directives: {
|
||||
tooltip: Tooltip
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -121,14 +160,15 @@ describe('PackVersionSelectorPopover', () => {
|
||||
|
||||
const options = listbox.props('options')!
|
||||
// Check that we have both special options and version options
|
||||
expect(options.length).toBe(defaultMockVersions.length + 2) // 2 special options + version options
|
||||
// Latest version (1.0.0) should be excluded from the version list to avoid duplication
|
||||
expect(options.length).toBe(defaultMockVersions.length + 1) // 2 special options + version options minus 1 duplicate
|
||||
|
||||
// Check that special options exist
|
||||
expect(options.some((o) => o.value === 'nightly')).toBe(true)
|
||||
expect(options.some((o) => o.value === 'latest')).toBe(true)
|
||||
|
||||
// Check that version options exist
|
||||
expect(options.some((o) => o.value === '1.0.0')).toBe(true)
|
||||
// Check that version options exist (excluding latest version 1.0.0)
|
||||
expect(options.some((o) => o.value === '1.0.0')).toBe(false) // Should be excluded as it's the latest
|
||||
expect(options.some((o) => o.value === '0.9.0')).toBe(true)
|
||||
expect(options.some((o) => o.value === '0.8.0')).toBe(true)
|
||||
})
|
||||
@@ -329,4 +369,357 @@ describe('PackVersionSelectorPopover', () => {
|
||||
expect(listbox.props('modelValue')).toBe('nightly')
|
||||
})
|
||||
})
|
||||
|
||||
describe('version compatibility checking', () => {
|
||||
it('shows warning icon for incompatible versions', async () => {
|
||||
// Set up the mock for versions
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
// Mock compatibility check to return conflict for specific version
|
||||
mockCheckVersionCompatibility.mockImplementation((versionData) => {
|
||||
if (versionData.supported_os?.includes('linux')) {
|
||||
return {
|
||||
hasConflict: true,
|
||||
conflicts: [
|
||||
{
|
||||
type: 'os',
|
||||
current_value: 'windows',
|
||||
required_value: 'linux'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return { hasConflict: false, conflicts: [] }
|
||||
})
|
||||
|
||||
const nodePackWithCompatibility = {
|
||||
...mockNodePack,
|
||||
supported_os: ['linux'],
|
||||
supported_accelerators: ['CUDA']
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
props: { nodePack: nodePackWithCompatibility }
|
||||
})
|
||||
await waitForPromises()
|
||||
|
||||
// Check that compatibility checking function was called
|
||||
expect(mockCheckVersionCompatibility).toHaveBeenCalled()
|
||||
|
||||
// The warning icon should be shown for incompatible versions
|
||||
const warningIcons = wrapper.findAll('.pi-exclamation-triangle')
|
||||
expect(warningIcons.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('shows verified icon for compatible versions', async () => {
|
||||
// Set up the mock for versions
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
// Mock compatibility check to return no conflicts
|
||||
mockCheckVersionCompatibility.mockReturnValue({
|
||||
hasConflict: false,
|
||||
conflicts: []
|
||||
})
|
||||
|
||||
const wrapper = mountComponent()
|
||||
await waitForPromises()
|
||||
|
||||
// Check that compatibility checking function was called
|
||||
expect(mockCheckVersionCompatibility).toHaveBeenCalled()
|
||||
|
||||
// The verified icon should be shown for compatible versions
|
||||
// Look for the VerifiedIcon component or SVG elements
|
||||
const verifiedIcons = wrapper.findAll('svg')
|
||||
expect(verifiedIcons.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('calls checkVersionCompatibility with correct version data', async () => {
|
||||
// Set up the mock for versions with specific supported data
|
||||
const versionsWithCompatibility = [
|
||||
{
|
||||
version: '1.0.0',
|
||||
supported_os: ['windows', 'linux'],
|
||||
supported_accelerators: ['CUDA', 'CPU'],
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0'
|
||||
}
|
||||
]
|
||||
mockGetPackVersions.mockResolvedValueOnce(versionsWithCompatibility)
|
||||
|
||||
const nodePackWithCompatibility = {
|
||||
...mockNodePack,
|
||||
supported_os: ['windows'],
|
||||
supported_accelerators: ['CPU'],
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0',
|
||||
latest_version: {
|
||||
version: '1.0.0',
|
||||
supported_os: ['windows', 'linux'],
|
||||
supported_accelerators: ['CPU'], // latest_version data takes precedence
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0',
|
||||
supported_python_version: '>=3.8',
|
||||
is_banned: false,
|
||||
has_registry_data: true
|
||||
}
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
props: { nodePack: nodePackWithCompatibility }
|
||||
})
|
||||
await waitForPromises()
|
||||
|
||||
// Trigger compatibility check by accessing getVersionCompatibility
|
||||
const vm = wrapper.vm as any
|
||||
vm.getVersionCompatibility('1.0.0')
|
||||
|
||||
// Verify that checkVersionCompatibility was called with correct data
|
||||
// Since 1.0.0 is the latest version, it should use latest_version data
|
||||
expect(mockCheckVersionCompatibility).toHaveBeenCalledWith({
|
||||
supported_os: ['windows', 'linux'],
|
||||
supported_accelerators: ['CPU'], // latest_version data takes precedence
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0',
|
||||
supported_python_version: '>=3.8',
|
||||
is_banned: false,
|
||||
has_registry_data: true
|
||||
})
|
||||
})
|
||||
|
||||
it('shows version conflict warnings for ComfyUI and frontend versions', async () => {
|
||||
// Set up the mock for versions
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
// Mock compatibility check to return version conflicts
|
||||
mockCheckVersionCompatibility.mockImplementation((versionData) => {
|
||||
const conflicts = []
|
||||
if (versionData.supported_comfyui_version) {
|
||||
conflicts.push({
|
||||
type: 'comfyui_version',
|
||||
current_value: '0.5.0',
|
||||
required_value: versionData.supported_comfyui_version
|
||||
})
|
||||
}
|
||||
if (versionData.supported_comfyui_frontend_version) {
|
||||
conflicts.push({
|
||||
type: 'frontend_version',
|
||||
current_value: '1.0.0',
|
||||
required_value: versionData.supported_comfyui_frontend_version
|
||||
})
|
||||
}
|
||||
return {
|
||||
hasConflict: conflicts.length > 0,
|
||||
conflicts
|
||||
}
|
||||
})
|
||||
|
||||
const nodePackWithVersionRequirements = {
|
||||
...mockNodePack,
|
||||
supported_comfyui_version: '>=1.0.0',
|
||||
supported_comfyui_frontend_version: '>=2.0.0'
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
props: { nodePack: nodePackWithVersionRequirements }
|
||||
})
|
||||
await waitForPromises()
|
||||
|
||||
// Check that compatibility checking function was called
|
||||
expect(mockCheckVersionCompatibility).toHaveBeenCalled()
|
||||
|
||||
// The warning icon should be shown for version incompatible packages
|
||||
const warningIcons = wrapper.findAll('.pi-exclamation-triangle')
|
||||
expect(warningIcons.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('handles latest and nightly versions using nodePack data', async () => {
|
||||
// Set up the mock for versions
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
const nodePackWithCompatibility = {
|
||||
...mockNodePack,
|
||||
supported_os: ['windows'],
|
||||
supported_accelerators: ['CPU'],
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0',
|
||||
latest_version: {
|
||||
...mockNodePack.latest_version,
|
||||
supported_os: ['windows'], // Match nodePack data for test consistency
|
||||
supported_accelerators: ['CPU'], // Match nodePack data for test consistency
|
||||
supported_python_version: '>=3.8',
|
||||
is_banned: false,
|
||||
has_registry_data: true
|
||||
}
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
props: { nodePack: nodePackWithCompatibility }
|
||||
})
|
||||
await waitForPromises()
|
||||
|
||||
const vm = wrapper.vm as any
|
||||
|
||||
// Test latest version
|
||||
vm.getVersionCompatibility('latest')
|
||||
expect(mockCheckVersionCompatibility).toHaveBeenCalledWith({
|
||||
supported_os: ['windows'],
|
||||
supported_accelerators: ['CPU'],
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0',
|
||||
supported_python_version: '>=3.8',
|
||||
is_banned: false,
|
||||
has_registry_data: true
|
||||
})
|
||||
|
||||
// Test nightly version
|
||||
vm.getVersionCompatibility('nightly')
|
||||
expect(mockCheckVersionCompatibility).toHaveBeenCalledWith({
|
||||
supported_os: ['windows'],
|
||||
supported_accelerators: ['CPU'],
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0',
|
||||
supported_python_version: undefined,
|
||||
is_banned: false,
|
||||
has_registry_data: false
|
||||
})
|
||||
})
|
||||
|
||||
it('shows python version conflict warnings', async () => {
|
||||
// Set up the mock for versions
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
// Mock compatibility check to return python version conflicts
|
||||
mockCheckVersionCompatibility.mockImplementation((versionData) => {
|
||||
if (versionData.supported_python_version) {
|
||||
return {
|
||||
hasConflict: true,
|
||||
conflicts: [
|
||||
{
|
||||
type: 'python_version',
|
||||
current_value: '3.8.0',
|
||||
required_value: versionData.supported_python_version
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return { hasConflict: false, conflicts: [] }
|
||||
})
|
||||
|
||||
const nodePackWithPythonRequirement = {
|
||||
...mockNodePack,
|
||||
supported_python_version: '>=3.9.0'
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
props: { nodePack: nodePackWithPythonRequirement }
|
||||
})
|
||||
await waitForPromises()
|
||||
|
||||
// Check that compatibility checking function was called
|
||||
expect(mockCheckVersionCompatibility).toHaveBeenCalled()
|
||||
|
||||
// The warning icon should be shown for python version incompatible packages
|
||||
const warningIcons = wrapper.findAll('.pi-exclamation-triangle')
|
||||
expect(warningIcons.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('shows banned package warnings', async () => {
|
||||
// Set up the mock for versions
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
// Mock compatibility check to return banned conflicts
|
||||
mockCheckVersionCompatibility.mockImplementation((versionData) => {
|
||||
if (versionData.is_banned === true) {
|
||||
return {
|
||||
hasConflict: true,
|
||||
conflicts: [
|
||||
{
|
||||
type: 'banned',
|
||||
current_value: 'installed',
|
||||
required_value: 'not_banned'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return { hasConflict: false, conflicts: [] }
|
||||
})
|
||||
|
||||
const bannedNodePack = {
|
||||
...mockNodePack,
|
||||
is_banned: true,
|
||||
latest_version: {
|
||||
...mockNodePack.latest_version,
|
||||
is_banned: true
|
||||
}
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
props: { nodePack: bannedNodePack }
|
||||
})
|
||||
await waitForPromises()
|
||||
|
||||
// Check that compatibility checking function was called
|
||||
expect(mockCheckVersionCompatibility).toHaveBeenCalled()
|
||||
|
||||
// Open the dropdown to see the options
|
||||
const select = wrapper.find('.p-select')
|
||||
if (!select.exists()) {
|
||||
// Try alternative selector
|
||||
const selectButton = wrapper.find('[aria-haspopup="listbox"]')
|
||||
if (selectButton.exists()) {
|
||||
await selectButton.trigger('click')
|
||||
}
|
||||
} else {
|
||||
await select.trigger('click')
|
||||
}
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// The warning icon should be shown for banned packages in the dropdown options
|
||||
const warningIcons = wrapper.findAll('.pi-exclamation-triangle')
|
||||
expect(warningIcons.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('shows security pending warnings', async () => {
|
||||
// Set up the mock for versions
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
// Mock compatibility check to return security pending conflicts
|
||||
mockCheckVersionCompatibility.mockImplementation((versionData) => {
|
||||
if (versionData.has_registry_data === false) {
|
||||
return {
|
||||
hasConflict: true,
|
||||
conflicts: [
|
||||
{
|
||||
type: 'security_pending',
|
||||
current_value: 'no_registry_data',
|
||||
required_value: 'registry_data_available'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return { hasConflict: false, conflicts: [] }
|
||||
})
|
||||
|
||||
const securityPendingNodePack = {
|
||||
...mockNodePack,
|
||||
has_registry_data: false,
|
||||
latest_version: {
|
||||
...mockNodePack.latest_version,
|
||||
has_registry_data: false
|
||||
}
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
props: { nodePack: securityPendingNodePack }
|
||||
})
|
||||
await waitForPromises()
|
||||
|
||||
// Check that compatibility checking function was called
|
||||
expect(mockCheckVersionCompatibility).toHaveBeenCalled()
|
||||
|
||||
// The warning icon should be shown for security pending packages
|
||||
const warningIcons = wrapper.findAll('.pi-exclamation-triangle')
|
||||
expect(warningIcons.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="w-64 mt-2">
|
||||
<span class="pl-3 text-muted text-md font-semibold opacity-70">
|
||||
<span class="pl-3 text-muted text-md font-semibold text-neutral-500">
|
||||
{{ $t('manager.selectVersion') }}
|
||||
</span>
|
||||
<div
|
||||
@@ -25,11 +25,32 @@
|
||||
option-value="value"
|
||||
:options="versionOptions"
|
||||
:highlight-on-select="false"
|
||||
class="my-3 w-full max-h-[50vh] border-none shadow-none"
|
||||
class="my-3 w-full max-h-[50vh] border-none shadow-none rounded-md"
|
||||
>
|
||||
<template #option="slotProps">
|
||||
<div class="flex justify-between items-center w-full p-1">
|
||||
<span>{{ slotProps.option.label }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Show no icon for nightly versions since compatibility is uncertain -->
|
||||
<template v-if="slotProps.option.value === 'nightly'">
|
||||
<div class="w-4"></div>
|
||||
<!-- Empty space to maintain alignment -->
|
||||
</template>
|
||||
<template v-else>
|
||||
<i
|
||||
v-if="
|
||||
getVersionCompatibility(slotProps.option.value).hasConflict
|
||||
"
|
||||
v-tooltip="{
|
||||
value: getVersionCompatibility(slotProps.option.value)
|
||||
.conflictMessage,
|
||||
showDelay: 300
|
||||
}"
|
||||
class="pi pi-exclamation-triangle text-yellow-500"
|
||||
/>
|
||||
<VerifiedIcon v-else :size="16" />
|
||||
</template>
|
||||
<span>{{ slotProps.option.label }}</span>
|
||||
</div>
|
||||
<i
|
||||
v-if="selectedVersion === slotProps.option.value"
|
||||
class="pi pi-check text-highlight"
|
||||
@@ -67,10 +88,13 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import VerifiedIcon from '@/components/icons/VerifiedIcon.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { useComfyRegistryService } from '@/services/comfyRegistryService'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
import { getJoinedConflictMessages } from '@/utils/conflictMessageUtil'
|
||||
import { isSemVer } from '@/utils/formatUtil'
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
@@ -85,6 +109,7 @@ const emit = defineEmits<{
|
||||
const { t } = useI18n()
|
||||
const registryService = useComfyRegistryService()
|
||||
const managerStore = useComfyManagerStore()
|
||||
const { checkVersionCompatibility } = useConflictDetection()
|
||||
|
||||
const isQueueing = ref(false)
|
||||
|
||||
@@ -123,6 +148,9 @@ const versionOptions = ref<
|
||||
}[]
|
||||
>([])
|
||||
|
||||
// Store fetched versions with their full data
|
||||
const fetchedVersions = ref<components['schemas']['NodeVersion'][]>([])
|
||||
|
||||
const isLoadingVersions = ref(false)
|
||||
|
||||
const onNodePackChange = async () => {
|
||||
@@ -130,18 +158,27 @@ const onNodePackChange = async () => {
|
||||
|
||||
// Fetch versions from the registry
|
||||
const versions = await fetchVersions()
|
||||
fetchedVersions.value = versions
|
||||
|
||||
// Get latest version number to exclude from the list
|
||||
const latestVersionNumber = nodePack.latest_version?.version
|
||||
|
||||
const availableVersionOptions = versions
|
||||
.map((version) => ({
|
||||
value: version.version ?? '',
|
||||
label: version.version ?? ''
|
||||
}))
|
||||
.filter((option) => option.value)
|
||||
.filter((option) => option.value && option.value !== latestVersionNumber) // Exclude latest version from the list
|
||||
|
||||
// Add Latest option with actual version number
|
||||
const latestLabel = latestVersionNumber
|
||||
? `${t('manager.latestVersion')} (${latestVersionNumber})`
|
||||
: t('manager.latestVersion')
|
||||
|
||||
// Add Latest option
|
||||
const defaultVersions = [
|
||||
{
|
||||
value: 'latest' as ManagerComponents['schemas']['SelectedVersion'],
|
||||
label: t('manager.latestVersion')
|
||||
label: latestLabel
|
||||
}
|
||||
]
|
||||
|
||||
@@ -149,7 +186,7 @@ const onNodePackChange = async () => {
|
||||
if (nodePack.repository?.length) {
|
||||
defaultVersions.push({
|
||||
value: 'nightly' as ManagerComponents['schemas']['SelectedVersion'],
|
||||
label: t('manager.nightlyVersion')
|
||||
label: t('manager.nightlyVersion') // Keep as just "nightly" - no version number
|
||||
})
|
||||
}
|
||||
|
||||
@@ -173,16 +210,141 @@ const handleSubmit = async () => {
|
||||
throw new Error('Node ID is required for installation')
|
||||
}
|
||||
|
||||
// Convert 'latest' to actual version number for installation
|
||||
const actualVersion =
|
||||
selectedVersion.value === 'latest'
|
||||
? nodePack.latest_version?.version ?? 'latest'
|
||||
: selectedVersion.value
|
||||
|
||||
await managerStore.installPack.call({
|
||||
id: nodePack.id,
|
||||
version: selectedVersion.value,
|
||||
version: actualVersion,
|
||||
repository: nodePack.repository ?? '',
|
||||
channel: 'default' as ManagerComponents['schemas']['ManagerChannel'],
|
||||
mode: 'cache' as ManagerComponents['schemas']['ManagerDatabaseSource'],
|
||||
selected_version: selectedVersion.value
|
||||
selected_version: actualVersion
|
||||
})
|
||||
|
||||
isQueueing.value = false
|
||||
emit('submit')
|
||||
}
|
||||
|
||||
// Function to get version data (either from nodePack or fetchedVersions)
|
||||
const getVersionData = (version: string) => {
|
||||
// Use latest_version data for both "latest" and the actual latest version number
|
||||
const latestVersionNumber = nodePack.latest_version?.version
|
||||
const useLatestVersionData =
|
||||
version === 'latest' || version === latestVersionNumber
|
||||
|
||||
if (useLatestVersionData) {
|
||||
// For "latest" and the actual latest version number, use consistent data from latest_version
|
||||
const latestVersionData = nodePack.latest_version
|
||||
return {
|
||||
supported_os: latestVersionData?.supported_os ?? nodePack.supported_os,
|
||||
supported_accelerators:
|
||||
latestVersionData?.supported_accelerators ??
|
||||
nodePack.supported_accelerators,
|
||||
supported_comfyui_version:
|
||||
latestVersionData?.supported_comfyui_version ??
|
||||
nodePack.supported_comfyui_version,
|
||||
supported_comfyui_frontend_version:
|
||||
latestVersionData?.supported_comfyui_frontend_version ??
|
||||
nodePack.supported_comfyui_frontend_version,
|
||||
supported_python_version:
|
||||
(latestVersionData && 'supported_python_version' in latestVersionData
|
||||
? latestVersionData.supported_python_version as string | undefined
|
||||
: undefined) ??
|
||||
('supported_python_version' in nodePack
|
||||
? nodePack.supported_python_version as string | undefined
|
||||
: undefined),
|
||||
is_banned:
|
||||
(latestVersionData && 'is_banned' in latestVersionData
|
||||
? latestVersionData.is_banned as boolean | undefined
|
||||
: undefined) ?? ('is_banned' in nodePack ? nodePack.is_banned as boolean | undefined : false),
|
||||
has_registry_data:
|
||||
(latestVersionData && 'has_registry_data' in latestVersionData
|
||||
? latestVersionData.has_registry_data as boolean | undefined
|
||||
: undefined) ??
|
||||
('has_registry_data' in nodePack ? nodePack.has_registry_data as boolean | undefined : false)
|
||||
}
|
||||
}
|
||||
|
||||
if (version === 'nightly') {
|
||||
// For nightly, we can't determine exact compatibility since it's dynamic Git HEAD
|
||||
// But we can assume it's generally compatible (nightly = latest development)
|
||||
// Use nodePack data as fallback, but nightly is typically more permissive
|
||||
return {
|
||||
supported_os: nodePack.supported_os || [], // If no OS restrictions, assume all supported
|
||||
supported_accelerators: nodePack.supported_accelerators || [], // If no accelerator restrictions, assume all supported
|
||||
supported_comfyui_version: nodePack.supported_comfyui_version, // Use latest known requirement
|
||||
supported_comfyui_frontend_version:
|
||||
nodePack.supported_comfyui_frontend_version, // Use latest known requirement
|
||||
supported_python_version:
|
||||
'supported_python_version' in nodePack
|
||||
? nodePack.supported_python_version as string | undefined
|
||||
: undefined,
|
||||
is_banned: false, // Nightly versions from repositories are typically not banned
|
||||
has_registry_data: false // Nightly doesn't come from registry
|
||||
}
|
||||
}
|
||||
|
||||
// For specific versions, find in fetched versions
|
||||
const versionData = fetchedVersions.value.find((v) => v.version === version)
|
||||
if (versionData) {
|
||||
return {
|
||||
supported_os: versionData.supported_os,
|
||||
supported_accelerators: versionData.supported_accelerators,
|
||||
supported_comfyui_version: versionData.supported_comfyui_version,
|
||||
supported_comfyui_frontend_version:
|
||||
versionData.supported_comfyui_frontend_version,
|
||||
supported_python_version:
|
||||
'supported_python_version' in versionData
|
||||
? versionData.supported_python_version as string | undefined
|
||||
: undefined,
|
||||
is_banned: 'is_banned' in versionData ? versionData.is_banned as boolean | undefined : false,
|
||||
has_registry_data:
|
||||
'has_registry_data' in versionData
|
||||
? versionData.has_registry_data as boolean | undefined
|
||||
: false
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to nodePack data
|
||||
return {
|
||||
supported_os: nodePack.supported_os,
|
||||
supported_accelerators: nodePack.supported_accelerators,
|
||||
supported_comfyui_version: nodePack.supported_comfyui_version,
|
||||
supported_comfyui_frontend_version:
|
||||
nodePack.supported_comfyui_frontend_version,
|
||||
supported_python_version:
|
||||
'supported_python_version' in nodePack
|
||||
? nodePack.supported_python_version as string | undefined
|
||||
: undefined,
|
||||
is_banned: 'is_banned' in nodePack ? nodePack.is_banned as boolean | undefined : false,
|
||||
has_registry_data:
|
||||
'has_registry_data' in nodePack ? nodePack.has_registry_data as boolean | undefined : false
|
||||
}
|
||||
}
|
||||
|
||||
// Function to check version compatibility using centralized logic
|
||||
const checkVersionCompatibilityLocal = (
|
||||
versionData: ReturnType<typeof getVersionData>
|
||||
) => {
|
||||
return checkVersionCompatibility(versionData)
|
||||
}
|
||||
|
||||
// Main function to get version compatibility info
|
||||
const getVersionCompatibility = (version: string) => {
|
||||
const versionData = getVersionData(version)
|
||||
const compatibility = checkVersionCompatibilityLocal(versionData)
|
||||
|
||||
const conflictMessage = compatibility.hasConflict
|
||||
? getJoinedConflictMessages(compatibility.conflicts, t)
|
||||
: ''
|
||||
|
||||
return {
|
||||
hasConflict: compatibility.hasConflict,
|
||||
conflictMessage
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -12,7 +12,11 @@
|
||||
v-bind="$attrs"
|
||||
@click="onClick"
|
||||
>
|
||||
<span class="py-2 px-3 whitespace-nowrap">
|
||||
<span class="py-2 px-3 whitespace-nowrap text-xs flex items-center gap-2">
|
||||
<i
|
||||
v-if="hasWarning && !loading"
|
||||
class="pi pi-exclamation-triangle text-yellow-500"
|
||||
></i>
|
||||
<template v-if="loading">
|
||||
{{ loadingMessage ?? $t('g.loading') }}
|
||||
</template>
|
||||
@@ -31,13 +35,15 @@ const {
|
||||
loading = false,
|
||||
loadingMessage,
|
||||
fullWidth = false,
|
||||
variant = 'default'
|
||||
variant = 'default',
|
||||
hasWarning = false
|
||||
} = defineProps<{
|
||||
label: string
|
||||
loading?: boolean
|
||||
loadingMessage?: string
|
||||
fullWidth?: boolean
|
||||
variant?: 'default' | 'black'
|
||||
hasWarning?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -11,9 +11,22 @@ import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
|
||||
import PackEnableToggle from './PackEnableToggle.vue'
|
||||
|
||||
// Mock debounce to execute immediately
|
||||
vi.mock('lodash', () => ({
|
||||
debounce: <T extends (...args: any[]) => any>(fn: T) => fn
|
||||
// Mock lodash functions used throughout the app
|
||||
vi.mock('lodash', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('lodash')>()
|
||||
return {
|
||||
...actual,
|
||||
debounce: <T extends (...args: any[]) => any>(fn: T) => fn,
|
||||
memoize: <T extends (...args: any[]) => any>(fn: T) => fn
|
||||
}
|
||||
})
|
||||
|
||||
// Mock config to prevent __COMFYUI_FRONTEND_VERSION__ error
|
||||
vi.mock('@/config', () => ({
|
||||
default: {
|
||||
app_title: 'ComfyUI',
|
||||
app_version: '1.0.0'
|
||||
}
|
||||
}))
|
||||
|
||||
const mockNodePack = {
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
v-if="hasConflict"
|
||||
v-tooltip="{
|
||||
value: $t('manager.conflicts.warningTooltip'),
|
||||
showDelay: 300
|
||||
}"
|
||||
class="flex items-center justify-center w-6 h-6 cursor-pointer"
|
||||
@click="showConflictModal"
|
||||
>
|
||||
<i class="pi pi-exclamation-triangle text-yellow-500 text-xl"></i>
|
||||
</div>
|
||||
<ToggleSwitch
|
||||
:model-value="isEnabled"
|
||||
:disabled="isLoading"
|
||||
aria-label="Enable or disable pack"
|
||||
@update:model-value="onToggle"
|
||||
@update:model-value="handleToggleClick"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -13,18 +24,28 @@
|
||||
import { debounce } from 'lodash'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
|
||||
const TOGGLE_DEBOUNCE_MS = 256
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
const { nodePack, hasConflict } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
hasConflict?: boolean
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { isPackEnabled, enablePack, disablePack } = useComfyManagerStore()
|
||||
const conflictStore = useConflictDetectionStore()
|
||||
const { showNodeConflictDialog } = useDialogService()
|
||||
const { acknowledgeConflict, isConflictAcknowledged } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
const isLoading = ref(false)
|
||||
|
||||
@@ -61,9 +82,41 @@ const handleDisable = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleToggle = async (enable: boolean) => {
|
||||
const handleToggle = async (enable: boolean, skipConflictCheck = false) => {
|
||||
if (isLoading.value) return
|
||||
|
||||
// Check for conflicts when enabling
|
||||
if (enable && hasConflict && !skipConflictCheck) {
|
||||
const conflicts = conflictStore.getConflictsForPackage(nodePack.id || '')
|
||||
if (conflicts) {
|
||||
// Check if conflicts have been acknowledged
|
||||
const hasUnacknowledgedConflicts = conflicts.conflicts.some(
|
||||
(conflict) => !isConflictAcknowledged(nodePack.id || '', conflict.type)
|
||||
)
|
||||
|
||||
if (hasUnacknowledgedConflicts) {
|
||||
showNodeConflictDialog({
|
||||
conflictedPackages: [conflicts],
|
||||
buttonText: t('manager.conflicts.enableAnyway'),
|
||||
onButtonClick: async () => {
|
||||
// User chose "Enable Anyway" - acknowledge all conflicts and proceed
|
||||
for (const conflict of conflicts.conflicts) {
|
||||
acknowledgeConflict(nodePack.id || '', conflict.type, '0.1.0')
|
||||
}
|
||||
// Proceed with enabling using debounced function
|
||||
onToggle(enable)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No conflicts or conflicts acknowledged - proceed with toggle
|
||||
await performToggle(enable)
|
||||
}
|
||||
|
||||
const performToggle = async (enable: boolean) => {
|
||||
isLoading.value = true
|
||||
if (enable) {
|
||||
await handleEnable()
|
||||
@@ -73,11 +126,39 @@ const handleToggle = async (enable: boolean) => {
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
// Handle initial toggle click - check for conflicts first
|
||||
const handleToggleClick = (enable: boolean) => {
|
||||
void handleToggle(enable)
|
||||
}
|
||||
|
||||
const onToggle = debounce(
|
||||
(enable: boolean) => {
|
||||
void handleToggle(enable)
|
||||
void performToggle(enable) // Direct call to avoid circular reference
|
||||
},
|
||||
TOGGLE_DEBOUNCE_MS,
|
||||
{ trailing: true }
|
||||
)
|
||||
|
||||
// Show conflict modal when warning icon is clicked
|
||||
const showConflictModal = () => {
|
||||
const conflicts = conflictStore.getConflictsForPackage(nodePack.id || '')
|
||||
if (conflicts) {
|
||||
showNodeConflictDialog({
|
||||
conflictedPackages: [conflicts],
|
||||
buttonText: isEnabled.value
|
||||
? t('manager.conflicts.understood')
|
||||
: t('manager.conflicts.enableAnyway'),
|
||||
onButtonClick: async () => {
|
||||
// User chose button action - acknowledge all conflicts
|
||||
for (const conflict of conflicts.conflicts) {
|
||||
acknowledgeConflict(nodePack.id || '', conflict.type, '0.1.0')
|
||||
}
|
||||
// Only enable if currently disabled
|
||||
if (!isEnabled.value) {
|
||||
onToggle(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
:variant="variant"
|
||||
:loading="isInstalling"
|
||||
:loading-message="$t('g.installing')"
|
||||
:has-warning="hasConflict"
|
||||
@action="installAllPacks"
|
||||
@click="onClick"
|
||||
/>
|
||||
@@ -16,29 +17,41 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import PackActionButton from '@/components/dialog/content/manager/button/PackActionButton.vue'
|
||||
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { IsInstallingKey } from '@/types/comfyManagerTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
|
||||
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
const { nodePacks, variant, label } = defineProps<{
|
||||
nodePacks: NodePack[]
|
||||
variant?: 'default' | 'black'
|
||||
label?: string
|
||||
}>()
|
||||
const { nodePacks, variant, label, hasConflict, skipConflictCheck } =
|
||||
defineProps<{
|
||||
nodePacks: NodePack[]
|
||||
variant?: 'default' | 'black'
|
||||
label?: string
|
||||
hasConflict?: boolean
|
||||
skipConflictCheck?: boolean
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const isInstalling = inject(IsInstallingKey, ref(false))
|
||||
const managerStore = useComfyManagerStore()
|
||||
const { showNodeConflictDialog } = useDialogService()
|
||||
const { checkVersionCompatibility } = useConflictDetection()
|
||||
const { acknowledgeConflict, isConflictAcknowledged } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
const onClick = (): void => {
|
||||
isInstalling.value = true
|
||||
}
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const createPayload = (
|
||||
installItem: NodePack
|
||||
): ManagerComponents['schemas']['InstallPackParams'] => {
|
||||
@@ -65,17 +78,87 @@ const createPayload = (
|
||||
const installPack = (item: NodePack) =>
|
||||
managerStore.installPack.call(createPayload(item))
|
||||
|
||||
// Function to check compatibility for uninstalled packages using centralized logic
|
||||
function checkUninstalledPackageCompatibility(
|
||||
pack: NodePack
|
||||
): ConflictDetectionResult | null {
|
||||
const compatibility = checkVersionCompatibility({
|
||||
supported_os: pack.supported_os,
|
||||
supported_accelerators: pack.supported_accelerators,
|
||||
supported_comfyui_version: pack.supported_comfyui_version,
|
||||
supported_comfyui_frontend_version: pack.supported_comfyui_frontend_version
|
||||
})
|
||||
|
||||
if (compatibility.hasConflict) {
|
||||
return {
|
||||
package_id: pack.id || 'unknown',
|
||||
package_name: pack.name || 'unknown',
|
||||
has_conflict: true,
|
||||
conflicts: compatibility.conflicts,
|
||||
is_compatible: false
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const installAllPacks = async () => {
|
||||
if (!nodePacks?.length) return
|
||||
|
||||
// isInstalling.value = true
|
||||
|
||||
const uninstalledPacks = nodePacks.filter(
|
||||
(pack) => !managerStore.isPackInstalled(pack.id)
|
||||
)
|
||||
if (!uninstalledPacks.length) return
|
||||
|
||||
await Promise.all(uninstalledPacks.map(installPack))
|
||||
// Skip conflict check if explicitly requested (e.g., from "Install Anyway" button)
|
||||
if (!skipConflictCheck) {
|
||||
// Check for conflicts in uninstalled packages
|
||||
const packsWithConflicts: ConflictDetectionResult[] = []
|
||||
|
||||
for (const pack of uninstalledPacks) {
|
||||
const conflicts = checkUninstalledPackageCompatibility(pack)
|
||||
if (conflicts) {
|
||||
// Check if conflicts have been acknowledged
|
||||
const hasUnacknowledgedConflicts = conflicts.conflicts.some(
|
||||
(conflict) => !isConflictAcknowledged(pack.id || '', conflict.type)
|
||||
)
|
||||
|
||||
if (hasUnacknowledgedConflicts) {
|
||||
packsWithConflicts.push(conflicts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there are unacknowledged conflicts, show modal
|
||||
if (packsWithConflicts.length > 0) {
|
||||
showNodeConflictDialog({
|
||||
conflictedPackages: packsWithConflicts,
|
||||
buttonText: t('manager.conflicts.installAnyway'),
|
||||
onButtonClick: async () => {
|
||||
// User chose "Install Anyway" - acknowledge all conflicts and proceed
|
||||
for (const conflictedPack of packsWithConflicts) {
|
||||
for (const conflict of conflictedPack.conflicts) {
|
||||
acknowledgeConflict(
|
||||
conflictedPack.package_id,
|
||||
conflict.type,
|
||||
'0.1.0'
|
||||
)
|
||||
}
|
||||
}
|
||||
// Proceed with installation
|
||||
await performInstallation(uninstalledPacks)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// No conflicts or conflicts acknowledged - proceed with installation
|
||||
await performInstallation(uninstalledPacks)
|
||||
}
|
||||
|
||||
const performInstallation = async (packs: NodePack[]) => {
|
||||
await Promise.all(packs.map(installPack))
|
||||
managerStore.installPack.clear()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
<template v-if="nodePack">
|
||||
<div class="flex flex-col h-full z-40 overflow-hidden relative">
|
||||
<div class="top-0 z-10 px-6 pt-6 w-full">
|
||||
<InfoPanelHeader :node-packs="[nodePack]" />
|
||||
<InfoPanelHeader
|
||||
:node-packs="[nodePack]"
|
||||
:has-conflict="hasCompatibilityIssues"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref="scrollContainer"
|
||||
class="p-6 pt-2 overflow-y-auto flex-1 text-sm hidden-scrollbar"
|
||||
class="p-6 pt-2 overflow-y-auto flex-1 text-sm scrollbar-hide"
|
||||
>
|
||||
<div class="mb-6">
|
||||
<MetadataRow
|
||||
@@ -36,7 +39,11 @@
|
||||
</MetadataRow>
|
||||
</div>
|
||||
<div class="mb-6 overflow-hidden">
|
||||
<InfoTabs :node-pack="nodePack" />
|
||||
<InfoTabs
|
||||
:node-pack="nodePack"
|
||||
:has-compatibility-issues="hasCompatibilityIssues"
|
||||
:conflict-result="conflictResult"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,9 +66,11 @@ import PackEnableToggle from '@/components/dialog/content/manager/button/PackEna
|
||||
import InfoPanelHeader from '@/components/dialog/content/manager/infoPanel/InfoPanelHeader.vue'
|
||||
import InfoTabs from '@/components/dialog/content/manager/infoPanel/InfoTabs.vue'
|
||||
import MetadataRow from '@/components/dialog/content/manager/infoPanel/MetadataRow.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { IsInstallingKey } from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
|
||||
|
||||
interface InfoItem {
|
||||
key: string
|
||||
@@ -84,9 +93,41 @@ whenever(isInstalled, () => {
|
||||
})
|
||||
|
||||
const { isPackInstalled } = useComfyManagerStore()
|
||||
const { checkVersionCompatibility } = useConflictDetection()
|
||||
|
||||
const { t, d, n } = useI18n()
|
||||
|
||||
// Check compatibility once and pass to children
|
||||
const conflictResult = computed((): ConflictDetectionResult | null => {
|
||||
const compatibility = checkVersionCompatibility({
|
||||
supported_os: nodePack.supported_os,
|
||||
supported_accelerators: nodePack.supported_accelerators,
|
||||
supported_comfyui_version: nodePack.supported_comfyui_version,
|
||||
supported_comfyui_frontend_version:
|
||||
nodePack.supported_comfyui_frontend_version
|
||||
// TODO: Add when API provides these fields
|
||||
// supported_python_version: nodePack.supported_python_version,
|
||||
// is_banned: nodePack.is_banned,
|
||||
// has_registry_data: nodePack.has_registry_data
|
||||
})
|
||||
|
||||
if (compatibility.hasConflict && nodePack.id && nodePack.name) {
|
||||
return {
|
||||
package_id: nodePack.id,
|
||||
package_name: nodePack.name,
|
||||
has_conflict: true,
|
||||
conflicts: compatibility.conflicts,
|
||||
is_compatible: false
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const hasCompatibilityIssues = computed(
|
||||
() => conflictResult.value?.has_conflict ?? false
|
||||
)
|
||||
|
||||
const infoItems = computed<InfoItem[]>(() => [
|
||||
{
|
||||
key: 'publisher',
|
||||
@@ -128,17 +169,3 @@ whenever(
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
<style scoped>
|
||||
.hidden-scrollbar {
|
||||
/* Firefox */
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,7 +18,12 @@
|
||||
v-bind="$attrs"
|
||||
:node-packs="nodePacks"
|
||||
/>
|
||||
<PackInstallButton v-else v-bind="$attrs" :node-packs="nodePacks" />
|
||||
<PackInstallButton
|
||||
v-else
|
||||
v-bind="$attrs"
|
||||
:node-packs="nodePacks"
|
||||
:has-conflict="hasConflict"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,8 +45,9 @@ import PackIcon from '@/components/dialog/content/manager/packIcon/PackIcon.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const { nodePacks } = defineProps<{
|
||||
const { nodePacks, hasConflict } = defineProps<{
|
||||
nodePacks: components['schemas']['Node'][]
|
||||
hasConflict?: boolean
|
||||
}>()
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
<template>
|
||||
<div class="overflow-hidden">
|
||||
<Tabs :value="activeTab">
|
||||
<TabList>
|
||||
<Tab value="description">
|
||||
<TabList class="overflow-x-auto scrollbar-hide">
|
||||
<Tab v-if="hasCompatibilityIssues" value="warning" class="p-2 mr-6">
|
||||
<div class="flex items-center gap-1">
|
||||
<span>⚠️</span>
|
||||
{{ $t('g.warning') }}
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab value="description" class="p-2 mr-6">
|
||||
{{ $t('g.description') }}
|
||||
</Tab>
|
||||
<Tab value="nodes">
|
||||
<Tab value="nodes" class="p-2">
|
||||
{{ $t('g.nodes') }}
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanels class="overflow-auto">
|
||||
<TabPanels class="overflow-auto py-4 px-2">
|
||||
<TabPanel v-if="hasCompatibilityIssues" value="warning">
|
||||
<WarningTabPanel
|
||||
:node-pack="nodePack"
|
||||
:conflict-result="conflictResult"
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel value="description">
|
||||
<DescriptionTabPanel :node-pack="nodePack" />
|
||||
</TabPanel>
|
||||
@@ -27,14 +39,18 @@ import TabList from 'primevue/tablist'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import TabPanels from 'primevue/tabpanels'
|
||||
import Tabs from 'primevue/tabs'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, watchEffect } from 'vue'
|
||||
|
||||
import DescriptionTabPanel from '@/components/dialog/content/manager/infoPanel/tabs/DescriptionTabPanel.vue'
|
||||
import NodesTabPanel from '@/components/dialog/content/manager/infoPanel/tabs/NodesTabPanel.vue'
|
||||
import WarningTabPanel from '@/components/dialog/content/manager/infoPanel/tabs/WarningTabPanel.vue'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
const { nodePack, hasCompatibilityIssues, conflictResult } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
hasCompatibilityIssues?: boolean
|
||||
conflictResult?: ConflictDetectionResult | null
|
||||
}>()
|
||||
|
||||
const nodeNames = computed(() => {
|
||||
@@ -44,4 +60,14 @@ const nodeNames = computed(() => {
|
||||
})
|
||||
|
||||
const activeTab = ref('description')
|
||||
|
||||
// Watch for compatibility issues and automatically switch to warning tab
|
||||
watchEffect(() => {
|
||||
if (hasCompatibilityIssues) {
|
||||
activeTab.value = 'warning'
|
||||
} else if (activeTab.value === 'warning') {
|
||||
// If currently on warning tab but no issues, switch to description
|
||||
activeTab.value = 'description'
|
||||
}
|
||||
}, { flush: 'post' })
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div
|
||||
v-for="(conflict, index) in conflictResult?.conflicts || []"
|
||||
:key="index"
|
||||
class="p-3 bg-yellow-800/20 rounded-md"
|
||||
>
|
||||
<div class="text-sm">
|
||||
{{ getConflictMessage(conflict, $t) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
|
||||
import { getConflictMessage } from '@/utils/conflictMessageUtil'
|
||||
|
||||
const { conflictResult } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
conflictResult?: ConflictDetectionResult | null
|
||||
}>()
|
||||
</script>
|
||||
@@ -43,7 +43,7 @@
|
||||
</span>
|
||||
<p
|
||||
v-if="nodePack.description"
|
||||
class="flex-1 text-muted text-xs font-medium break-words overflow-hidden min-h-12 line-clamp-3 my-0 leading-4 mb-1 overflow-hidden"
|
||||
class="flex-1 text-muted text-xs font-medium break-words overflow-hidden max-h-12 line-clamp-3 my-0 leading-4 mb-2 overflow-hidden"
|
||||
>
|
||||
{{ nodePack.description }}
|
||||
</p>
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
<template>
|
||||
<div
|
||||
class="min-h-12 flex justify-between items-center px-4 py-2 text-xs text-muted font-medium leading-3"
|
||||
class="h-12 flex justify-between items-center px-4 text-xs text-muted font-medium leading-3"
|
||||
>
|
||||
<div v-if="nodePack.downloads" class="flex items-center gap-1.5">
|
||||
<i class="pi pi-download text-muted"></i>
|
||||
<span>{{ formattedDownloads }}</span>
|
||||
</div>
|
||||
<PackInstallButton v-if="!isInstalled" :node-packs="[nodePack]" />
|
||||
<PackEnableToggle v-else :node-pack="nodePack" />
|
||||
<div class="flex justify-end items-center gap-2">
|
||||
<template v-if="!isInstalled">
|
||||
<PackInstallButton
|
||||
:node-packs="[nodePack]"
|
||||
:has-conflict="hasConflict"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<PackEnableToggle :node-pack="nodePack" :has-conflict="hasConflict" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -17,7 +26,9 @@ 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 { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
@@ -32,4 +43,42 @@ const { n } = useI18n()
|
||||
const formattedDownloads = computed(() =>
|
||||
nodePack.downloads ? n(nodePack.downloads) : ''
|
||||
)
|
||||
|
||||
const conflictStore = useConflictDetectionStore()
|
||||
const { checkVersionCompatibility } = useConflictDetection()
|
||||
|
||||
// TODO: Package version mismatch issue - Package IDs include version suffixes (@1_0_3)
|
||||
// but UI searches without version. This causes conflict detection failures.
|
||||
// Once getConflictsForPackage is improved to handle version matching properly,
|
||||
// all the complex fallback logic below can be removed.
|
||||
const hasConflict = computed(() => {
|
||||
if (!nodePack.id) return false
|
||||
|
||||
// For installed packages, check conflicts from store
|
||||
if (isInstalled.value) {
|
||||
// Try exact match first
|
||||
let conflicts = conflictStore.getConflictsForPackage(nodePack.id)
|
||||
if (conflicts) return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// For uninstalled packages, check compatibility directly
|
||||
if (
|
||||
nodePack.supported_os ||
|
||||
nodePack.supported_accelerators ||
|
||||
nodePack.supported_comfyui_version
|
||||
) {
|
||||
const compatibility = checkVersionCompatibility({
|
||||
supported_os: nodePack.supported_os,
|
||||
supported_accelerators: nodePack.supported_accelerators,
|
||||
supported_comfyui_version: nodePack.supported_comfyui_version,
|
||||
supported_comfyui_frontend_version:
|
||||
nodePack.supported_comfyui_frontend_version
|
||||
})
|
||||
return compatibility.hasConflict
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -20,7 +20,10 @@
|
||||
{{ $n(nodePack.downloads) }}
|
||||
</div>
|
||||
<template v-if="isInstalled">
|
||||
<PackEnableToggle :node-pack="nodePack" />
|
||||
<PackEnableToggle
|
||||
:node-pack="nodePack"
|
||||
:has-conflict="!!packageConflicts"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<PackInstallButton :node-packs="[nodePack]" />
|
||||
@@ -35,6 +38,7 @@ import { computed } from 'vue'
|
||||
import PackEnableToggle from '@/components/dialog/content/manager/button/PackEnableToggle.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
@@ -42,5 +46,13 @@ const { nodePack } = defineProps<{
|
||||
}>()
|
||||
|
||||
const { isPackInstalled } = useComfyManagerStore()
|
||||
const conflictStore = useConflictDetectionStore()
|
||||
const isInstalled = computed(() => isPackInstalled(nodePack?.id))
|
||||
|
||||
const packageConflicts = computed(() => {
|
||||
if (!nodePack.id || !isInstalled.value) return null
|
||||
|
||||
// For installed packages, check conflicts from store
|
||||
return conflictStore.getConflictsForPackage(nodePack.id)
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user