mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
manager: design improved related to infopanel & delete unused files
This commit is contained in:
@@ -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>
|
||||
@@ -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') }}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user