mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 18:52:19 +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') }}
|
{{ t('manager.actions') }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</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 -->
|
<!-- Mixed: Don't show any button -->
|
||||||
<div v-if="isMixed" class="text-sm text-neutral-500">
|
<div v-if="isMixed" class="text-sm text-neutral-500">
|
||||||
{{ $t('manager.mixedSelectionMessage') }}
|
{{ $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', () => {
|
describe('DescriptionTabPanel', () => {
|
||||||
const mountComponent = (props: {
|
const mountComponent = (props: {
|
||||||
nodePack: Partial<components['schemas']['Node']>
|
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 = (
|
const createNodePack = (
|
||||||
overrides: Partial<components['schemas']['Node']> = {}
|
overrides: Partial<components['schemas']['Node']> = {}
|
||||||
) => ({
|
) => ({
|
||||||
@@ -134,37 +117,36 @@ describe('DescriptionTabPanel', () => {
|
|||||||
licenseTests.forEach((test) => {
|
licenseTests.forEach((test) => {
|
||||||
it(test.name, () => {
|
it(test.name, () => {
|
||||||
const wrapper = mountComponent({ nodePack: test.nodePack })
|
const wrapper = mountComponent({ nodePack: test.nodePack })
|
||||||
const licenseSection = getSectionByTitle(wrapper, TRANSLATIONS.license)
|
if (test.expected.isUrl) {
|
||||||
expect(licenseSection).toBeDefined()
|
const link = wrapper.findAll('a').find((a) =>
|
||||||
expect(licenseSection.text).toBe(test.expected.text)
|
a.text().includes(test.expected.text)
|
||||||
expect(licenseSection.isUrl).toBe(test.expected.isUrl)
|
)
|
||||||
|
expect(link).toBeDefined()
|
||||||
|
expect(link!.attributes('href')).toBe(test.expected.text)
|
||||||
|
} else {
|
||||||
|
expect(wrapper.text()).toContain(test.expected.text)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('description sections', () => {
|
describe('description sections', () => {
|
||||||
it('shows description section', () => {
|
it('shows description text', () => {
|
||||||
const wrapper = mountComponent({
|
const wrapper = mountComponent({
|
||||||
nodePack: createNodePack()
|
nodePack: createNodePack()
|
||||||
})
|
})
|
||||||
const descriptionSection = getSectionByTitle(
|
expect(wrapper.text()).toContain('Test description')
|
||||||
wrapper,
|
|
||||||
TRANSLATIONS.description
|
|
||||||
)
|
|
||||||
expect(descriptionSection).toBeDefined()
|
|
||||||
expect(descriptionSection.text).toBe('Test description')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows repository section when available', () => {
|
it('shows repository link when available', () => {
|
||||||
const wrapper = mountComponent({
|
const wrapper = mountComponent({
|
||||||
nodePack: createNodePack({
|
nodePack: createNodePack({
|
||||||
repository: 'https://github.com/user/repo'
|
repository: 'https://github.com/user/repo'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
const repoSection = getSectionByTitle(wrapper, TRANSLATIONS.repository)
|
const repoLink = wrapper.find('a[href="https://github.com/user/repo"]')
|
||||||
expect(repoSection).toBeDefined()
|
expect(repoLink.exists()).toBe(true)
|
||||||
expect(repoSection.text).toBe('https://github.com/user/repo')
|
expect(repoLink.attributes('target')).toBe('_blank')
|
||||||
expect(repoSection.isUrl).toBe(true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows fallback text when description is missing', () => {
|
it('shows fallback text when description is missing', () => {
|
||||||
@@ -173,7 +155,7 @@ describe('DescriptionTabPanel', () => {
|
|||||||
description: undefined
|
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