manager: design improved related to infopanel & delete unused files

This commit is contained in:
Jin Yi
2026-01-23 15:09:08 +09:00
parent 94331f3281
commit 5d158e1487
8 changed files with 17 additions and 345 deletions

View File

@@ -1,92 +0,0 @@
<template>
<div v-if="nodePacks?.length" class="flex flex-col items-center">
<p class="text-center text-base font-bold">{{ nodePacks[0].name }}</p>
<div v-if="!importFailed" class="flex justify-center gap-2">
<template v-if="canTryNightlyUpdate">
<PackTryUpdateButton :node-pack="nodePacks[0]" size="md" />
<PackUninstallButton :node-packs="nodePacks" size="md" />
</template>
<template v-else-if="isAllInstalled">
<PackUninstallButton
v-bind="$attrs"
size="md"
:node-packs="nodePacks"
/>
</template>
<template v-else>
<PackInstallButton
v-bind="$attrs"
size="md"
:node-packs="nodePacks"
:has-conflict="hasConflict || computedHasConflict"
:conflict-info="conflictInfo"
/>
</template>
</div>
</div>
<div v-else class="flex flex-col items-center">
<NoResultsPlaceholder
:message="$t('manager.status.unknown')"
:title="$t('manager.tryAgainLater')"
/>
</div>
</template>
<script setup lang="ts">
import { computed, inject, ref, watch } from 'vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import type { components } from '@/types/comfyRegistryTypes'
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
import PackTryUpdateButton from '@/workbench/extensions/manager/components/manager/button/PackTryUpdateButton.vue'
import PackUninstallButton from '@/workbench/extensions/manager/components/manager/button/PackUninstallButton.vue'
import { usePackUpdateStatus } from '@/workbench/extensions/manager/composables/nodePack/usePackUpdateStatus'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
import { ImportFailedKey } from '@/workbench/extensions/manager/types/importFailedTypes'
const { nodePacks, hasConflict } = defineProps<{
nodePacks: components['schemas']['Node'][]
hasConflict?: boolean
}>()
const managerStore = useComfyManagerStore()
// Inject import failed context from parent
const importFailedContext = inject(ImportFailedKey)
const importFailed = importFailedContext?.importFailed
const isAllInstalled = ref(false)
watch(
[() => nodePacks, () => managerStore.installedPacks],
() => {
isAllInstalled.value = nodePacks.every((nodePack) =>
managerStore.isPackInstalled(nodePack.id)
)
},
{ immediate: true }
)
// Check if nightly update is available for the first pack
const { canTryNightlyUpdate } = usePackUpdateStatus(() => nodePacks[0])
// Add conflict detection for install button dialog
const { checkNodeCompatibility } = useConflictDetection()
// Compute conflict info for all node packs
const conflictInfo = computed<ConflictDetail[]>(() => {
if (!nodePacks?.length) return []
const allConflicts: ConflictDetail[] = []
for (const nodePack of nodePacks) {
const compatibilityCheck = checkNodeCompatibility(nodePack)
if (compatibilityCheck.conflicts) {
allConflicts.push(...compatibilityCheck.conflicts)
}
}
return allConflicts
})
const computedHasConflict = computed(() => conflictInfo.value.length > 0)
</script>

View File

@@ -9,7 +9,7 @@
{{ t('manager.actions') }}
</span>
</template>
<div class="flex justify-center gap-2 px-4 py-2">
<div class="flex flex-col gap-1 px-4">
<!-- Mixed: Don't show any button -->
<div v-if="isMixed" class="text-sm text-neutral-500">
{{ $t('manager.mixedSelectionMessage') }}

View File

@@ -1,81 +0,0 @@
<template>
<div class="overflow-hidden h-full flex flex-col">
<div class="flex-1 min-h-0">
<TabList v-model="activeTab" class="scrollbar-hide overflow-x-auto">
<Tab v-if="hasCompatibilityIssues" value="warning">
<div class="flex items-center gap-1">
<span></span>
{{ importFailed ? $t('g.error') : $t('g.warning') }}
</div>
</Tab>
<Tab value="description">
{{ $t('g.description') }}
</Tab>
<Tab value="nodes">
{{ $t('g.nodes') }}
</Tab>
</TabList>
</div>
<div class="p-2 scrollbar-custom">
<WarningTabPanel
v-if="activeTab === 'warning' && hasCompatibilityIssues"
:node-pack="nodePack"
:conflict-result="conflictResult"
/>
<DescriptionTabPanel
v-else-if="activeTab === 'description'"
:node-pack="nodePack"
/>
<NodesTabPanel
v-else-if="activeTab === 'nodes'"
:node-pack="nodePack"
:node-names="nodeNames"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, inject, ref, watchEffect } from 'vue'
import Tab from '@/components/tab/Tab.vue'
import TabList from '@/components/tab/TabList.vue'
import type { components } from '@/types/comfyRegistryTypes'
import DescriptionTabPanel from '@/workbench/extensions/manager/components/manager/infoPanel/tabs/DescriptionTabPanel.vue'
import NodesTabPanel from '@/workbench/extensions/manager/components/manager/infoPanel/tabs/NodesTabPanel.vue'
import WarningTabPanel from '@/workbench/extensions/manager/components/manager/infoPanel/tabs/WarningTabPanel.vue'
import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
import { ImportFailedKey } from '@/workbench/extensions/manager/types/importFailedTypes'
const { nodePack, hasCompatibilityIssues, conflictResult } = defineProps<{
nodePack: components['schemas']['Node']
hasCompatibilityIssues?: boolean
conflictResult?: ConflictDetectionResult | null
}>()
// Inject import failed context from parent
const importFailedContext = inject(ImportFailedKey)
const importFailed = importFailedContext?.importFailed
const nodeNames = computed(() => {
// @ts-expect-error comfy_nodes is an Algolia-specific field
const { comfy_nodes } = nodePack
return comfy_nodes ?? []
})
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>

View File

@@ -1,37 +0,0 @@
<template>
<div class="flex flex-col gap-4 text-sm">
<div v-for="(section, index) in sections" :key="index" class="mb-4">
<div class="mb-3">
{{ section.title }}
</div>
<div class="break-words text-muted">
<a
v-if="section.isUrl"
:href="section.text"
target="_blank"
rel="noopener noreferrer"
>
<i v-if="isGitHubLink(section.text)" class="pi pi-github text-base pr-1 pb-1" />
<span class="break-all">{{ section.text }}</span>
</a>
<MarkdownText v-else :text="section.text" class="text-muted" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import MarkdownText from '@/workbench/extensions/manager/components/manager/infoPanel/MarkdownText.vue'
export interface TextSection {
title: string
text: string
isUrl?: boolean
}
defineProps<{
sections: TextSection[]
}>()
const isGitHubLink = (url: string): boolean => url.includes('github.com')
</script>

View File

@@ -1,15 +0,0 @@
<template>
<div class="flex py-1.5 text-xs">
<div class="w-1/3 truncate pr-2 text-muted">{{ label }}</div>
<div class="w-2/3">
<slot>{{ value }}</slot>
</div>
</div>
</template>
<script setup lang="ts">
const { value = 'N/A', label } = defineProps<{
label: string
value?: string | number
}>()
</script>

View File

@@ -15,13 +15,6 @@ const i18n = createI18n({
}
})
const TRANSLATIONS = {
description: 'Description',
repository: 'Repository',
license: 'License',
noDescription: 'No description available'
}
describe('DescriptionTabPanel', () => {
const mountComponent = (props: {
nodePack: Partial<components['schemas']['Node']>
@@ -34,16 +27,6 @@ describe('DescriptionTabPanel', () => {
})
}
const getSectionByTitle = (
wrapper: ReturnType<typeof mountComponent>,
title: string
) => {
const sections = wrapper
.findComponent({ name: 'InfoTextSection' })
.props('sections')
return sections.find((s: any) => s.title === title)
}
const createNodePack = (
overrides: Partial<components['schemas']['Node']> = {}
) => ({
@@ -134,37 +117,36 @@ describe('DescriptionTabPanel', () => {
licenseTests.forEach((test) => {
it(test.name, () => {
const wrapper = mountComponent({ nodePack: test.nodePack })
const licenseSection = getSectionByTitle(wrapper, TRANSLATIONS.license)
expect(licenseSection).toBeDefined()
expect(licenseSection.text).toBe(test.expected.text)
expect(licenseSection.isUrl).toBe(test.expected.isUrl)
if (test.expected.isUrl) {
const link = wrapper.findAll('a').find((a) =>
a.text().includes(test.expected.text)
)
expect(link).toBeDefined()
expect(link!.attributes('href')).toBe(test.expected.text)
} else {
expect(wrapper.text()).toContain(test.expected.text)
}
})
})
})
describe('description sections', () => {
it('shows description section', () => {
it('shows description text', () => {
const wrapper = mountComponent({
nodePack: createNodePack()
})
const descriptionSection = getSectionByTitle(
wrapper,
TRANSLATIONS.description
)
expect(descriptionSection).toBeDefined()
expect(descriptionSection.text).toBe('Test description')
expect(wrapper.text()).toContain('Test description')
})
it('shows repository section when available', () => {
it('shows repository link when available', () => {
const wrapper = mountComponent({
nodePack: createNodePack({
repository: 'https://github.com/user/repo'
})
})
const repoSection = getSectionByTitle(wrapper, TRANSLATIONS.repository)
expect(repoSection).toBeDefined()
expect(repoSection.text).toBe('https://github.com/user/repo')
expect(repoSection.isUrl).toBe(true)
const repoLink = wrapper.find('a[href="https://github.com/user/repo"]')
expect(repoLink.exists()).toBe(true)
expect(repoLink.attributes('target')).toBe('_blank')
})
it('shows fallback text when description is missing', () => {
@@ -173,7 +155,7 @@ describe('DescriptionTabPanel', () => {
description: undefined
}
})
expect(wrapper.find('p').text()).toBe(TRANSLATIONS.noDescription)
expect(wrapper.text()).toContain('No description available')
})
})
})

View File

@@ -1,52 +0,0 @@
<template>
<div class="aspect-[2/1] w-full max-w-[204] overflow-hidden rounded-lg">
<!-- default banner show -->
<div v-if="showDefaultBanner" class="h-full w-full">
<img
:src="DEFAULT_BANNER"
:alt="$t('g.defaultBanner')"
class="h-full w-full object-cover"
/>
</div>
<!-- banner_url or icon show -->
<div v-else class="relative h-full w-full">
<!-- blur background -->
<div
v-if="imgSrc"
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{
backgroundImage: `url(${imgSrc})`,
filter: 'blur(10px)'
}"
></div>
<!-- image -->
<img
:src="isImageError ? DEFAULT_BANNER : imgSrc"
:alt="nodePack.name + ' banner'"
:class="
isImageError
? 'relative w-full h-full object-cover z-10'
: 'relative w-full h-full object-contain z-10'
"
@error="isImageError = true"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import type { components } from '@/types/comfyRegistryTypes'
const DEFAULT_BANNER = '/assets/images/fallback-gradient-avatar.svg'
const { nodePack } = defineProps<{
nodePack: components['schemas']['Node']
}>()
const isImageError = ref(false)
const showDefaultBanner = computed(() => !nodePack.banner_url && !nodePack.icon)
const imgSrc = computed(() => nodePack.banner_url || nodePack.icon)
</script>

View File

@@ -1,33 +0,0 @@
<template>
<div class="relative h-[104px] w-[224px] shadow-xl">
<div
v-for="(pack, index) in nodePacks.slice(0, maxVisible)"
:key="pack.id"
class="absolute h-[90px] w-[210px]"
:style="{
bottom: `${index * offset}px`,
right: `${index * offset}px`,
zIndex: maxVisible - index
}"
>
<div class="rounded-lg border p-0.5 shadow-lg">
<PackIcon :node-pack="pack" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { components } from '@/types/comfyRegistryTypes'
import PackIcon from '@/workbench/extensions/manager/components/manager/packIcon/PackIcon.vue'
const {
nodePacks,
maxVisible = 3,
offset = 8
} = defineProps<{
nodePacks: components['schemas']['Node'][]
maxVisible?: number
offset?: number
}>()
</script>