chore: manager dialog modified

This commit is contained in:
Jin Yi
2026-01-22 21:24:46 +09:00
parent 4a5e7c8bcb
commit 4340a0ab55
16 changed files with 461 additions and 371 deletions

View File

@@ -3,7 +3,7 @@
v-tooltip.right="{ v-tooltip.right="{
value: tooltipText, value: tooltipText,
disabled: !isOverflowing, disabled: !isOverflowing,
pt: { text: { class: 'whitespace-nowrap' } } pt: { text: { class: 'w-max whitespace-nowrap' } }
}" }"
class="flex cursor-pointer items-center-safe gap-2 rounded-md px-4 py-3 text-sm transition-colors text-base-foreground" class="flex cursor-pointer items-center-safe gap-2 rounded-md px-4 py-3 text-sm transition-colors text-base-foreground"
:class=" :class="

View File

@@ -295,6 +295,17 @@
"changingVersion": "Changing version from {from} to {to}", "changingVersion": "Changing version from {from} to {to}",
"dependencies": "Dependencies", "dependencies": "Dependencies",
"inWorkflow": "In Workflow", "inWorkflow": "In Workflow",
"nav": {
"allExtensions": "All Extensions",
"notInstalled": "Not Installed",
"installedSection": "INSTALLED",
"allInstalled": "All installed",
"updatesAvailable": "Updates Available",
"conflicting": "Conflicting",
"inWorkflowSection": "IN WORKFLOW",
"allInWorkflow": "All in: {workflowName}",
"missingNodes": "Missing Nodes"
},
"infoPanelEmpty": "Click an item to see the info", "infoPanelEmpty": "Click an item to see the info",
"applyChanges": "Apply Changes", "applyChanges": "Apply Changes",
"restartToApplyChanges": "To apply changes, please restart ComfyUI", "restartToApplyChanges": "To apply changes, please restart ComfyUI",

View File

@@ -16,32 +16,63 @@
</template> </template>
<template #header> <template #header>
<div class="flex items-center gap-2"> <div class="flex w-full items-center justify-between gap-2">
<SingleSelect <div class="flex items-center gap-2">
v-model="searchMode" <SingleSelect
class="min-w-34" v-model="searchMode"
:options="filterOptions" class="min-w-34"
:options="filterOptions"
/>
<AutoCompletePlus
v-model.lazy="searchQuery"
:suggestions="suggestions"
:placeholder="$t('manager.searchPlaceholder')"
:complete-on-focus="false"
:delay="8"
option-label="query"
class="w-full min-w-md max-w-lg"
:pt="{
root: { class: 'relative' },
pcInputText: {
root: {
autofocus: true,
class:
'w-full h-10 rounded-lg bg-comfy-input text-comfy-input-foreground border-none outline-none text-sm'
}
},
overlay: {
class: 'bg-comfy-input rounded-lg mt-1 shadow-lg border border-border-default'
},
list: { class: 'p-1' },
option: {
class:
'px-3 py-2 rounded hover:bg-button-hover-surface cursor-pointer text-sm'
},
loader: { style: 'display: none' }
}"
:show-empty-message="false"
@complete="stubTrue"
@option-select="onOptionSelect"
>
<template #dropdownicon>
<i
class="pi pi-search absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
/>
</template>
</AutoCompletePlus>
</div>
<PackInstallButton
v-if="isMissingTab && missingNodePacks.length > 0"
:disabled="isMissingLoading || !!missingError"
:node-packs="missingNodePacks"
size="lg"
:label="$t('manager.installAllMissingNodes')"
/> />
<AutoCompletePlus <PackUpdateButton
v-model.lazy="searchQuery" v-if="isUpdateAvailableTab && hasUpdateAvailable"
:suggestions="suggestions" :node-packs="enabledUpdateAvailableNodePacks"
:placeholder="$t('manager.searchPlaceholder')" :has-disabled-update-packs="hasDisabledUpdatePacks"
:complete-on-focus="false" size="lg"
:delay="8"
option-label="query"
class="w-full min-w-md max-w-lg"
:pt="{
pcInputText: {
root: {
autofocus: true,
class: 'w-full rounded-lg h-10'
}
},
loader: { style: 'display: none' }
}"
:show-empty-message="false"
@complete="stubTrue"
@option-select="onOptionSelect"
/> />
</div> </div>
</template> </template>
@@ -79,37 +110,18 @@
</Button> </Button>
</div> </div>
<!-- Filters Row --> <!-- Sort Options -->
<div class="relative flex flex-wrap justify-between gap-2 px-6 pb-4"> <div class="flex justify-end px-6 pb-4">
<div> <SingleSelect
<PackInstallButton v-model="sortField"
v-if="isMissingTab && missingNodePacks.length > 0" :label="$t('g.sort')"
:disabled="isMissingLoading || !!missingError" :options="availableSortOptions"
:node-packs="missingNodePacks" class="w-48"
size="lg" >
:label="$t('manager.installAllMissingNodes')" <template #icon>
/> <i class="icon-[lucide--arrow-up-down] text-muted-foreground" />
<PackUpdateButton </template>
v-if="isUpdateAvailableTab && hasUpdateAvailable" </SingleSelect>
:node-packs="enabledUpdateAvailableNodePacks"
:has-disabled-update-packs="hasDisabledUpdatePacks"
size="lg"
/>
</div>
<!-- Sort Options on right -->
<div>
<SingleSelect
v-model="sortField"
:label="$t('g.sort')"
:options="availableSortOptions"
class="w-48"
>
<template #icon>
<i class="icon-[lucide--arrow-up-down] text-muted-foreground" />
</template>
</SingleSelect>
</div>
</div> </div>
</template> </template>
@@ -118,7 +130,7 @@
<GridSkeleton :grid-style="GRID_STYLE" :skeleton-card-count /> <GridSkeleton :grid-style="GRID_STYLE" :skeleton-card-count />
</div> </div>
<NoResultsPlaceholder <NoResultsPlaceholder
v-else-if="searchResults.length === 0" v-else-if="displayPacks.length === 0"
:title=" :title="
comfyManagerStore.error comfyManagerStore.error
? $t('manager.errorConnecting') ? $t('manager.errorConnecting')
@@ -130,7 +142,7 @@
: $t('manager.tryDifferentSearch') : $t('manager.tryDifferentSearch')
" "
/> />
<div v-else class="h-full" @click="handleGridContainerClick"> <div v-else class="h-full w-full" @click="handleGridContainerClick">
<VirtualGrid <VirtualGrid
id="results-grid" id="results-grid"
:items="resultsWithKeys" :items="resultsWithKeys"
@@ -185,9 +197,10 @@ import Button from '@/components/ui/button/Button.vue'
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue' import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue' import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
import { useExternalLink } from '@/composables/useExternalLink' import { useExternalLink } from '@/composables/useExternalLink'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore' import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import type { components } from '@/types/comfyRegistryTypes' import type { components } from '@/types/comfyRegistryTypes'
import type { NavItemData } from '@/types/navTypes' import type { NavGroupData, NavItemData } from '@/types/navTypes'
import { OnCloseKey } from '@/types/widgetTypes' import { OnCloseKey } from '@/types/widgetTypes'
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue' import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
import PackUpdateButton from '@/workbench/extensions/manager/components/manager/button/PackUpdateButton.vue' import PackUpdateButton from '@/workbench/extensions/manager/components/manager/button/PackUpdateButton.vue'
@@ -195,14 +208,13 @@ import InfoPanel from '@/workbench/extensions/manager/components/manager/infoPan
import InfoPanelMultiItem from '@/workbench/extensions/manager/components/manager/infoPanel/InfoPanelMultiItem.vue' import InfoPanelMultiItem from '@/workbench/extensions/manager/components/manager/infoPanel/InfoPanelMultiItem.vue'
import PackCard from '@/workbench/extensions/manager/components/manager/packCard/PackCard.vue' import PackCard from '@/workbench/extensions/manager/components/manager/packCard/PackCard.vue'
import GridSkeleton from '@/workbench/extensions/manager/components/manager/skeleton/GridSkeleton.vue' import GridSkeleton from '@/workbench/extensions/manager/components/manager/skeleton/GridSkeleton.vue'
import { useInstalledPacks } from '@/workbench/extensions/manager/composables/nodePack/useInstalledPacks'
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes' import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
import { usePackUpdateStatus } from '@/workbench/extensions/manager/composables/nodePack/usePackUpdateStatus'
import { useUpdateAvailableNodes } from '@/workbench/extensions/manager/composables/nodePack/useUpdateAvailableNodes' import { useUpdateAvailableNodes } from '@/workbench/extensions/manager/composables/nodePack/useUpdateAvailableNodes'
import { useWorkflowPacks } from '@/workbench/extensions/manager/composables/nodePack/useWorkflowPacks'
import { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment' import { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment'
import { useManagerDisplayPacks } from '@/workbench/extensions/manager/composables/useManagerDisplayPacks'
import { useManagerStatePersistence } from '@/workbench/extensions/manager/composables/useManagerStatePersistence' import { useManagerStatePersistence } from '@/workbench/extensions/manager/composables/useManagerStatePersistence'
import { useRegistrySearch } from '@/workbench/extensions/manager/composables/useRegistrySearch' import { useRegistrySearch } from '@/workbench/extensions/manager/composables/useRegistrySearch'
import { useConflictDetectionStore } from '@/workbench/extensions/manager/stores/conflictDetectionStore'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore' import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes' import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
@@ -218,14 +230,16 @@ const { buildDocsUrl } = useExternalLink()
const comfyManagerStore = useComfyManagerStore() const comfyManagerStore = useComfyManagerStore()
const { getPackById } = useComfyRegistryStore() const { getPackById } = useComfyRegistryStore()
const conflictAcknowledgment = useConflictAcknowledgment() const conflictAcknowledgment = useConflictAcknowledgment()
const conflictDetectionStore = useConflictDetectionStore()
const workflowStore = useWorkflowStore()
const persistedState = useManagerStatePersistence() const persistedState = useManagerStatePersistence()
const initialState = persistedState.loadStoredState() const initialState = persistedState.loadStoredState()
const GRID_STYLE = { const GRID_STYLE = {
display: 'grid', display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(17rem, 1fr))', gridTemplateColumns: 'repeat(auto-fill, minmax(14rem, 1fr))',
gap: '1.5rem', gap: '1rem',
padding: '0' padding: '0.5rem'
} as const } as const
const { const {
@@ -248,32 +262,84 @@ const {
hasDisabledUpdatePacks hasDisabledUpdatePacks
} = useUpdateAvailableNodes() } = useUpdateAvailableNodes()
// Get the current workflow name for the nav item
const workflowName = computed(
() => workflowStore.activeWorkflow?.filename ?? t('manager.inWorkflow')
)
// Navigation items for LeftSidePanel // Navigation items for LeftSidePanel
const navItems = computed<NavItemData[]>(() => [ const navItems = computed<(NavItemData | NavGroupData)[]>(() => [
{ id: ManagerTab.All, label: t('g.all'), icon: 'pi pi-list' },
{ id: ManagerTab.Installed, label: t('g.installed'), icon: 'pi pi-box' },
{ {
id: ManagerTab.Workflow, id: ManagerTab.All,
label: t('manager.inWorkflow'), label: t('manager.nav.allExtensions'),
icon: 'pi pi-folder' icon: 'icon-[lucide--list]'
}, },
{ {
id: ManagerTab.Missing, id: ManagerTab.NotInstalled,
label: t('g.missing'), label: t('manager.nav.notInstalled'),
icon: 'pi pi-exclamation-circle' icon: 'icon-[lucide--globe]'
}, },
{ {
id: ManagerTab.UpdateAvailable, title: t('manager.nav.installedSection'),
label: t('g.updateAvailable'), items: [
icon: 'pi pi-sync' {
id: ManagerTab.AllInstalled,
label: t('manager.nav.allInstalled'),
icon: 'icon-[lucide--download]'
},
{
id: ManagerTab.UpdateAvailable,
label: t('manager.nav.updatesAvailable'),
icon: 'icon-[lucide--refresh-cw]'
},
{
id: ManagerTab.Conflicting,
label: t('manager.nav.conflicting'),
icon: 'icon-[lucide--triangle-alert]',
badge: conflictDetectionStore.conflictedPackages.length || undefined
}
]
},
{
title: t('manager.nav.inWorkflowSection'),
items: [
{
id: ManagerTab.Workflow,
label: t('manager.nav.allInWorkflow', {
workflowName: workflowName.value
}),
icon: 'icon-[lucide--share-2]'
},
{
id: ManagerTab.Missing,
label: t('manager.nav.missingNodes'),
icon: 'icon-[lucide--triangle-alert]'
}
]
} }
]) ])
const initialTabId = initialTab ?? initialState.selectedTabId ?? ManagerTab.All const initialTabId = initialTab ?? initialState.selectedTabId ?? ManagerTab.All
const selectedNavId = ref<string | null>(initialTabId) const selectedNavId = ref<string | null>(initialTabId)
// Helper to find a nav item by id in the nested structure
const findNavItemById = (
items: (NavItemData | NavGroupData)[],
id: string | null
): NavItemData | undefined => {
for (const item of items) {
if ('items' in item) {
const found = item.items.find((subItem) => subItem.id === id)
if (found) return found
} else if (item.id === id) {
return item
}
}
return undefined
}
const selectedTab = computed(() => const selectedTab = computed(() =>
navItems.value.find((item) => item.id === selectedNavId.value) findNavItemById(navItems.value, selectedNavId.value)
) )
const { const {
@@ -318,120 +384,20 @@ const isInitialLoad = computed(
() => searchResults.value.length === 0 && searchQuery.value === '' () => searchResults.value.length === 0 && searchQuery.value === ''
) )
const isEmptySearch = computed(() => searchQuery.value === '') // Use the new composable for tab-based display packs
const displayPacks = ref<components['schemas']['Node'][]>([])
const { const {
startFetchInstalled, displayPacks,
filterInstalledPack, isLoading: isTabLoading,
installedPacks, workflowPacks
isLoading: isLoadingInstalled, } = useManagerDisplayPacks(selectedNavId, searchResults, searchQuery, sortField)
isReady: installedPacksReady
} = useInstalledPacks()
const {
startFetchWorkflowPacks,
filterWorkflowPack,
workflowPacks,
isLoading: isLoadingWorkflow,
isReady: workflowPacksReady
} = useWorkflowPacks()
const filterMissingPacks = (packs: components['schemas']['Node'][]) =>
packs.filter((pack) => !comfyManagerStore.isPackInstalled(pack.id))
// Tab helpers for template
const isUpdateAvailableTab = computed( const isUpdateAvailableTab = computed(
() => selectedTab.value?.id === ManagerTab.UpdateAvailable () => selectedTab.value?.id === ManagerTab.UpdateAvailable
) )
const isInstalledTab = computed(
() => selectedTab.value?.id === ManagerTab.Installed
)
const isMissingTab = computed( const isMissingTab = computed(
() => selectedTab.value?.id === ManagerTab.Missing () => selectedTab.value?.id === ManagerTab.Missing
) )
const isWorkflowTab = computed(
() => selectedTab.value?.id === ManagerTab.Workflow
)
const isAllTab = computed(() => selectedTab.value?.id === ManagerTab.All)
const isOutdatedPack = (pack: components['schemas']['Node']) => {
const { isUpdateAvailable } = usePackUpdateStatus(pack)
return isUpdateAvailable.value === true
}
const filterOutdatedPacks = (packs: components['schemas']['Node'][]) =>
packs.filter(isOutdatedPack)
watch(
[isUpdateAvailableTab, installedPacks],
async () => {
if (!isUpdateAvailableTab.value) return
if (!isEmptySearch.value) {
displayPacks.value = filterOutdatedPacks(installedPacks.value)
} else if (
!installedPacks.value.length &&
!installedPacksReady.value &&
!isLoadingInstalled.value
) {
await startFetchInstalled()
} else {
displayPacks.value = filterOutdatedPacks(installedPacks.value)
}
},
{ immediate: true }
)
watch(
[isInstalledTab, installedPacks],
async () => {
if (!isInstalledTab.value) return
if (!isEmptySearch.value) {
displayPacks.value = filterInstalledPack(searchResults.value)
} else if (
!installedPacks.value.length &&
!installedPacksReady.value &&
!isLoadingInstalled.value
) {
await startFetchInstalled()
} else {
displayPacks.value = installedPacks.value
}
},
{ immediate: true }
)
watch(
[isMissingTab, isWorkflowTab, workflowPacks, installedPacks],
async () => {
if (!isWorkflowTab.value && !isMissingTab.value) return
if (!isEmptySearch.value) {
displayPacks.value = isMissingTab.value
? filterMissingPacks(filterWorkflowPack(searchResults.value))
: filterWorkflowPack(searchResults.value)
} else if (
!workflowPacks.value.length &&
!isLoadingWorkflow.value &&
!workflowPacksReady.value
) {
await startFetchWorkflowPacks()
if (isMissingTab.value) {
await startFetchInstalled()
}
} else {
displayPacks.value = isMissingTab.value
? filterMissingPacks(workflowPacks.value)
: workflowPacks.value
}
},
{ immediate: true }
)
watch([isAllTab, searchResults], () => {
if (!isAllTab.value) return
displayPacks.value = searchResults.value
})
const onClickWarningLink = () => { const onClickWarningLink = () => {
window.open( window.open(
@@ -442,49 +408,9 @@ const onClickWarningLink = () => {
) )
} }
const onResultsChange = () => {
switch (selectedTab.value?.id) {
case ManagerTab.Installed:
displayPacks.value = isEmptySearch.value
? installedPacks.value
: filterInstalledPack(searchResults.value)
break
case ManagerTab.Workflow:
displayPacks.value = isEmptySearch.value
? workflowPacks.value
: filterWorkflowPack(searchResults.value)
break
case ManagerTab.Missing:
if (!isEmptySearch.value) {
displayPacks.value = filterMissingPacks(
filterWorkflowPack(searchResults.value)
)
}
break
case ManagerTab.UpdateAvailable:
displayPacks.value = isEmptySearch.value
? filterOutdatedPacks(installedPacks.value)
: filterOutdatedPacks(searchResults.value)
break
default:
displayPacks.value = searchResults.value
}
}
watch(searchResults, onResultsChange, { flush: 'post' })
watch(() => comfyManagerStore.installedPacksIds, onResultsChange)
const isLoading = computed(() => { const isLoading = computed(() => {
if (isSearchLoading.value) return searchResults.value.length === 0 if (isSearchLoading.value) return searchResults.value.length === 0
if (selectedTab.value?.id === ManagerTab.Installed) { if (isTabLoading.value) return true
return isLoadingInstalled.value
}
if (
selectedTab.value?.id === ManagerTab.Workflow ||
selectedTab.value?.id === ManagerTab.Missing
) {
return isLoadingWorkflow.value
}
return isInitialLoad.value return isInitialLoad.value
}) })
@@ -511,7 +437,7 @@ watch(
const getLoadingCount = () => { const getLoadingCount = () => {
switch (selectedTab.value?.id) { switch (selectedTab.value?.id) {
case ManagerTab.Installed: case ManagerTab.AllInstalled:
return comfyManagerStore.installedPacksIds?.size return comfyManagerStore.installedPacksIds?.size
case ManagerTab.Workflow: case ManagerTab.Workflow:
return workflowPacks.value?.length return workflowPacks.value?.length
@@ -581,10 +507,6 @@ whenever(selectedNodePack, async () => {
if (packIndex !== -1) { if (packIndex !== -1) {
selectedNodePacks.value.splice(packIndex, 1, mergedPack) selectedNodePacks.value.splice(packIndex, 1, mergedPack)
} }
const idx = displayPacks.value.findIndex((p) => p.id === mergedPack.id)
if (idx !== -1) {
displayPacks.value.splice(idx, 1, mergedPack)
}
} }
}) })

View File

@@ -1,6 +1,6 @@
<template> <template>
<Button <Button
variant="secondary" variant="primary"
:size :size
:disabled="isLoading || isInstalling" :disabled="isLoading || isInstalling"
@click="installAllPacks" @click="installAllPacks"
@@ -14,6 +14,7 @@
duration="1s" duration="1s"
:size="size === 'sm' ? 12 : 16" :size="size === 'sm' ? 12 : 16"
/> />
<i v-else class="icon-[lucide--download]" />
<span>{{ computedLabel }}</span> <span>{{ computedLabel }}</span>
</Button> </Button>
</template> </template>

View File

@@ -1,7 +1,7 @@
<template> <template>
<Button <Button
v-tooltip.top="$t('manager.tryUpdateTooltip')" v-tooltip.top="$t('manager.tryUpdateTooltip')"
variant="textonly" variant="inverted"
:size :size
:disabled="isUpdating" :disabled="isUpdating"
@click="tryUpdate" @click="tryUpdate"

View File

@@ -1,8 +1,7 @@
<template> <template>
<Button <Button
variant="textonly" variant="destructive"
:size :size
class="border border-red-500"
@click="uninstallItems" @click="uninstallItems"
> >
{{ {{

View File

@@ -3,12 +3,13 @@
v-tooltip.top=" v-tooltip.top="
hasDisabledUpdatePacks ? $t('manager.disabledNodesWontUpdate') : null hasDisabledUpdatePacks ? $t('manager.disabledNodesWontUpdate') : null
" "
class="border" variant="primary"
:size :size
:disabled="isUpdating" :disabled="isUpdating"
@click="updateAllPacks" @click="updateAllPacks"
> >
<DotSpinner v-if="isUpdating" duration="1s" :size="12" /> <DotSpinner v-if="isUpdating" duration="1s" :size="12" />
<i v-else class="icon-[lucide--refresh-cw]" />
<span>{{ $t('manager.updateAll') }}</span> <span>{{ $t('manager.updateAll') }}</span>
</Button> </Button>
</template> </template>

View File

@@ -1,22 +1,15 @@
<template> <template>
<template v-if="nodePack"> <template v-if="nodePack">
<div class="relative z-40 flex h-full flex-col overflow-hidden"> <div class="flex h-full flex-col overflow-hidden">
<div class="top-0 z-10 w-full px-6 pt-6"> <div class="w-full px-6 pt-6">
<InfoPanelHeader <InfoPanelHeader
:node-packs="[nodePack]" :node-packs="[nodePack]"
:has-conflict="hasCompatibilityIssues" :has-conflict="hasCompatibilityIssues"
> />
<template v-if="canTryNightlyUpdate" #install-button>
<div class="flex w-full justify-center gap-2">
<PackTryUpdateButton :node-pack="nodePack" size="md" />
<PackUninstallButton :node-packs="[nodePack]" size="md" />
</div>
</template>
</InfoPanelHeader>
</div> </div>
<div <div
ref="scrollContainer" ref="scrollContainer"
class="scrollbar-hide flex-1 overflow-y-auto p-6 pt-2 text-sm" class="scrollbar-hide flex-1 flex flex-col p-6 pt-2 text-sm"
> >
<div class="mb-6"> <div class="mb-6">
<MetadataRow <MetadataRow
@@ -49,7 +42,7 @@
<PackVersionBadge :node-pack="nodePack" :is-selected="true" /> <PackVersionBadge :node-pack="nodePack" :is-selected="true" />
</MetadataRow> </MetadataRow>
</div> </div>
<div class="mb-6 overflow-hidden"> <div class="mb-6 flex-1 overflow-hidden">
<InfoTabs <InfoTabs
:node-pack="nodePack" :node-pack="nodePack"
:has-compatibility-issues="hasCompatibilityIssues" :has-compatibility-issues="hasCompatibilityIssues"
@@ -75,12 +68,9 @@ import type { components } from '@/types/comfyRegistryTypes'
import PackStatusMessage from '@/workbench/extensions/manager/components/manager/PackStatusMessage.vue' import PackStatusMessage from '@/workbench/extensions/manager/components/manager/PackStatusMessage.vue'
import PackVersionBadge from '@/workbench/extensions/manager/components/manager/PackVersionBadge.vue' import PackVersionBadge from '@/workbench/extensions/manager/components/manager/PackVersionBadge.vue'
import PackEnableToggle from '@/workbench/extensions/manager/components/manager/button/PackEnableToggle.vue' import PackEnableToggle from '@/workbench/extensions/manager/components/manager/button/PackEnableToggle.vue'
import PackTryUpdateButton from '@/workbench/extensions/manager/components/manager/button/PackTryUpdateButton.vue'
import PackUninstallButton from '@/workbench/extensions/manager/components/manager/button/PackUninstallButton.vue'
import InfoPanelHeader from '@/workbench/extensions/manager/components/manager/infoPanel/InfoPanelHeader.vue' import InfoPanelHeader from '@/workbench/extensions/manager/components/manager/infoPanel/InfoPanelHeader.vue'
import InfoTabs from '@/workbench/extensions/manager/components/manager/infoPanel/InfoTabs.vue' import InfoTabs from '@/workbench/extensions/manager/components/manager/infoPanel/InfoTabs.vue'
import MetadataRow from '@/workbench/extensions/manager/components/manager/infoPanel/MetadataRow.vue' import MetadataRow from '@/workbench/extensions/manager/components/manager/infoPanel/MetadataRow.vue'
import { usePackUpdateStatus } from '@/workbench/extensions/manager/composables/nodePack/usePackUpdateStatus'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection' import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
import { useImportFailedDetection } from '@/workbench/extensions/manager/composables/useImportFailedDetection' import { useImportFailedDetection } from '@/workbench/extensions/manager/composables/useImportFailedDetection'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore' import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
@@ -109,8 +99,6 @@ whenever(isInstalled, () => {
isInstalling.value = false isInstalling.value = false
}) })
const { canTryNightlyUpdate } = usePackUpdateStatus(() => nodePack)
const { checkNodeCompatibility } = useConflictDetection() const { checkNodeCompatibility } = useConflictDetection()
const { getConflictsForPackageByID } = useConflictDetectionStore() const { getConflictsForPackageByID } = useConflictDetectionStore()

View File

@@ -1,36 +1,24 @@
<template> <template>
<div v-if="nodePacks?.length" class="flex flex-col items-center"> <div v-if="nodePacks?.length" class="flex flex-col items-center">
<slot name="thumbnail"> <PackIcon :node-pack="nodePacks[0]" width="204" height="106" />
<PackIcon :node-pack="nodePacks[0]" width="204" height="106" /> <p class="text-center text-base font-bold">{{ nodePacks[0].name }}</p>
</slot> <div v-if="!importFailed" class="flex justify-center gap-2">
<h2 <template v-if="canTryNightlyUpdate">
class="mt-4 mb-2 text-center text-2xl font-bold" <PackTryUpdateButton :node-pack="nodePacks[0]" size="md" />
style="word-break: break-all" <PackUninstallButton :node-packs="nodePacks" size="md" />
> </template>
<slot name="title"> <template v-else-if="isAllInstalled">
<span class="inline-block text-base">{{ nodePacks[0].name }}</span> <PackUninstallButton v-bind="$attrs" size="md" :node-packs="nodePacks" />
</slot> </template>
</h2> <template v-else>
<div
v-if="!importFailed"
class="mt-2 mb-4 flex w-full max-w-xs justify-center"
>
<slot name="install-button">
<PackUninstallButton
v-if="isAllInstalled"
v-bind="$attrs"
size="md"
:node-packs="nodePacks"
/>
<PackInstallButton <PackInstallButton
v-else
v-bind="$attrs" v-bind="$attrs"
size="md" size="md"
:node-packs="nodePacks" :node-packs="nodePacks"
:has-conflict="hasConflict || computedHasConflict" :has-conflict="hasConflict || computedHasConflict"
:conflict-info="conflictInfo" :conflict-info="conflictInfo"
/> />
</slot> </template>
</div> </div>
</div> </div>
<div v-else class="flex flex-col items-center"> <div v-else class="flex flex-col items-center">
@@ -47,8 +35,10 @@ import { computed, inject, ref, watch } from 'vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue' import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import type { components } from '@/types/comfyRegistryTypes' import type { components } from '@/types/comfyRegistryTypes'
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue' 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 PackUninstallButton from '@/workbench/extensions/manager/components/manager/button/PackUninstallButton.vue'
import PackIcon from '@/workbench/extensions/manager/components/manager/packIcon/PackIcon.vue' import PackIcon from '@/workbench/extensions/manager/components/manager/packIcon/PackIcon.vue'
import { usePackUpdateStatus } from '@/workbench/extensions/manager/composables/nodePack/usePackUpdateStatus'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection' import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore' import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes' import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
@@ -76,6 +66,9 @@ watch(
{ immediate: true } { immediate: true }
) )
// Check if nightly update is available for the first pack
const { canTryNightlyUpdate } = usePackUpdateStatus(() => nodePacks[0])
// Add conflict detection for install button dialog // Add conflict detection for install button dialog
const { checkNodeCompatibility } = useConflictDetection() const { checkNodeCompatibility } = useConflictDetection()

View File

@@ -1,54 +1,46 @@
<template> <template>
<div class="overflow-hidden"> <div class="overflow-hidden h-full flex flex-col">
<Tabs :value="activeTab"> <div class="flex-1 min-h-0">
<TabList class="scrollbar-hide overflow-x-auto"> <TabList v-model="activeTab" class="scrollbar-hide overflow-x-auto">
<Tab <Tab v-if="hasCompatibilityIssues" value="warning">
v-if="hasCompatibilityIssues"
value="warning"
class="mr-6 p-2 font-inter"
>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<span></span> <span></span>
{{ importFailed ? $t('g.error') : $t('g.warning') }} {{ importFailed ? $t('g.error') : $t('g.warning') }}
</div> </div>
</Tab> </Tab>
<Tab value="description" class="mr-6 p-2 font-inter"> <Tab value="description">
{{ $t('g.description') }} {{ $t('g.description') }}
</Tab> </Tab>
<Tab value="nodes" class="p-2 font-inter"> <Tab value="nodes">
{{ $t('g.nodes') }} {{ $t('g.nodes') }}
</Tab> </Tab>
</TabList> </TabList>
<TabPanels class="overflow-auto px-2 py-4"> </div>
<TabPanel
v-if="hasCompatibilityIssues" <div class="p-2 scrollbar-custom">
value="warning" <WarningTabPanel
class="bg-transparent" v-if="activeTab === 'warning' && hasCompatibilityIssues"
> :node-pack="nodePack"
<WarningTabPanel :conflict-result="conflictResult"
:node-pack="nodePack" />
:conflict-result="conflictResult" <DescriptionTabPanel
/> v-else-if="activeTab === 'description'"
</TabPanel> :node-pack="nodePack"
<TabPanel value="description"> />
<DescriptionTabPanel :node-pack="nodePack" /> <NodesTabPanel
</TabPanel> v-else-if="activeTab === 'nodes'"
<TabPanel value="nodes"> :node-pack="nodePack"
<NodesTabPanel :node-pack="nodePack" :node-names="nodeNames" /> :node-names="nodeNames"
</TabPanel> />
</TabPanels> </div>
</Tabs>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Tab from 'primevue/tab'
import TabList from 'primevue/tablist'
import TabPanel from 'primevue/tabpanel'
import TabPanels from 'primevue/tabpanels'
import Tabs from 'primevue/tabs'
import { computed, inject, ref, watchEffect } from 'vue' 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 type { components } from '@/types/comfyRegistryTypes'
import DescriptionTabPanel from '@/workbench/extensions/manager/components/manager/infoPanel/tabs/DescriptionTabPanel.vue' 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 NodesTabPanel from '@/workbench/extensions/manager/components/manager/infoPanel/tabs/NodesTabPanel.vue'

View File

@@ -9,7 +9,7 @@
<template v-if="conflict.type === 'import_failed'"> <template v-if="conflict.type === 'import_failed'">
<div <div
v-if="conflict.required_value" v-if="conflict.required_value"
class="max-h-64 overflow-x-hidden scrollbar-custom overflow-y-auto rounded px-2" class="overflow-x-hidden rounded px-2"
> >
<p class="text-xs text-muted-foreground break-all font-mono"> <p class="text-xs text-muted-foreground break-all font-mono">
{{ conflict.required_value }} {{ conflict.required_value }}

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="aspect-7/3 w-full overflow-hidden"> <div class="aspect-7/3 w-full overflow-hidden z-0">
<!-- default banner show --> <!-- default banner show -->
<div v-if="showDefaultBanner" class="h-full w-full"> <div v-if="showDefaultBanner" class="h-full w-full">
<img <img

View File

@@ -1,31 +1,25 @@
<template> <template>
<Card <div
class="shadow-elevation-3 inline-flex size-full flex-col items-start justify-between overflow-hidden rounded-lg transition-all duration-200" :class="
:class="{ cn(
'selected-card': isSelected, 'flex size-full flex-col overflow-hidden rounded-lg bg-modal-card-background transition-colors duration-200 cursor-pointer select-none',
'opacity-60': isDisabled isSelected
}" ? 'ring-3 ring-modal-card-border-highlighted'
:pt="{ : 'hover:bg-modal-card-background-hovered',
body: { class: 'p-0 flex flex-col w-full h-full rounded-lg gap-0' }, isDisabled && 'opacity-60'
content: { class: 'flex-1 flex flex-col rounded-lg min-h-0' }, )
title: { class: 'w-full h-full rounded-t-lg cursor-pointer' }, "
footer: {
class: 'p-0 m-0 flex flex-col gap-0',
style: {
borderTop: isLightTheme ? '1px solid #f4f4f4' : '1px solid #2C2C2C'
}
}
}"
> >
<template #title> <!-- Banner -->
<div class="w-full cursor-pointer rounded-t-lg">
<PackBanner :node-pack="nodePack" /> <PackBanner :node-pack="nodePack" />
</template> </div>
<template #content>
<div class="h-full w-full px-4 pt-4 pb-3"> <!-- Content -->
<div class="flex flex-1 flex-col rounded-lg min-h-0">
<div class="h-full w-full py-2 px-3">
<div class="flex h-full w-full flex-col gap-y-1"> <div class="flex h-full w-full flex-col gap-y-1">
<span <span class="truncate overflow-hidden text-xs font-bold text-ellipsis">
class="truncate overflow-hidden text-sm font-bold text-ellipsis"
>
{{ nodePack.name }} {{ nodePack.name }}
</span> </span>
<p <p
@@ -63,19 +57,20 @@
</div> </div>
</div> </div>
</div> </div>
</template> </div>
<template #footer>
<!-- Footer -->
<div class="border-t border-border-default">
<PackCardFooter :node-pack="nodePack" :is-installing="isInstalling" /> <PackCardFooter :node-pack="nodePack" :is-installing="isInstalling" />
</template> </div>
</Card> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Card from 'primevue/card'
import { computed, provide } from 'vue' import { computed, provide } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore' import { cn } from '@/utils/tailwindUtil'
import PackVersionBadge from '@/workbench/extensions/manager/components/manager/PackVersionBadge.vue' import PackVersionBadge from '@/workbench/extensions/manager/components/manager/PackVersionBadge.vue'
import PackBanner from '@/workbench/extensions/manager/components/manager/packBanner/PackBanner.vue' import PackBanner from '@/workbench/extensions/manager/components/manager/packBanner/PackBanner.vue'
import PackCardFooter from '@/workbench/extensions/manager/components/manager/packCard/PackCardFooter.vue' import PackCardFooter from '@/workbench/extensions/manager/components/manager/packCard/PackCardFooter.vue'
@@ -96,11 +91,6 @@ const { nodePack, isSelected = false } = defineProps<{
const { d, t } = useI18n() const { d, t } = useI18n()
const colorPaletteStore = useColorPaletteStore()
const isLightTheme = computed(
() => colorPaletteStore.completedActivePalette.light_theme
)
const { isPackInstalled, isPackEnabled, isPackInstalling } = const { isPackInstalled, isPackEnabled, isPackInstalling } =
useComfyManagerStore() useComfyManagerStore()
@@ -133,22 +123,3 @@ const formattedLatestVersionDate = computed(() => {
}) })
}) })
</script> </script>
<style scoped>
.selected-card {
position: relative;
}
.selected-card::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 4px solid var(--p-primary-color);
border-radius: 0.5rem;
pointer-events: none;
z-index: 100;
}
</style>

View File

@@ -0,0 +1,207 @@
import { whenever } from '@vueuse/core'
import { orderBy } from 'es-toolkit/compat'
import type { Ref } from 'vue'
import { computed } from 'vue'
import { useRegistrySearchGateway } from '@/services/gateway/registrySearchGateway'
import type { components } from '@/types/comfyRegistryTypes'
import { useInstalledPacks } from '@/workbench/extensions/manager/composables/nodePack/useInstalledPacks'
import { usePackUpdateStatus } from '@/workbench/extensions/manager/composables/nodePack/usePackUpdateStatus'
import { useWorkflowPacks } from '@/workbench/extensions/manager/composables/nodePack/useWorkflowPacks'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import { useConflictDetectionStore } from '@/workbench/extensions/manager/stores/conflictDetectionStore'
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
type NodePack = components['schemas']['Node']
export function useManagerDisplayPacks(
selectedTabId: Ref<string | null>,
searchResults: Ref<NodePack[]>,
searchQuery: Ref<string>,
sortField: Ref<string>
) {
const comfyManagerStore = useComfyManagerStore()
const conflictDetectionStore = useConflictDetectionStore()
const { getSortValue, getSortableFields } = useRegistrySearchGateway()
const {
startFetchInstalled,
filterInstalledPack,
installedPacks,
isLoading: isLoadingInstalled,
isReady: installedPacksReady
} = useInstalledPacks()
const {
startFetchWorkflowPacks,
filterWorkflowPack,
workflowPacks,
isLoading: isLoadingWorkflow,
isReady: workflowPacksReady
} = useWorkflowPacks()
const tabType = computed(() => selectedTabId.value as ManagerTab | null)
const isEmptySearch = computed(() => searchQuery.value === '')
// Sorting function for packs not from searchResults
const sortPacks = (packs: NodePack[]) => {
if (!sortField.value || packs.length === 0) return packs
const sortableFields = getSortableFields()
const fieldConfig = sortableFields.find((f) => f.id === sortField.value)
const direction = fieldConfig?.direction || 'desc'
return orderBy(
packs,
[(pack) => getSortValue(pack, sortField.value)],
[direction]
)
}
// Filter functions
const filterNotInstalled = (packs: NodePack[]) =>
packs.filter((p) => !comfyManagerStore.isPackInstalled(p.id))
const filterConflicting = (packs: NodePack[]) =>
packs.filter(
(p) =>
!!p.id &&
conflictDetectionStore.conflictedPackages.some(
(c) => c.package_id === p.id
)
)
const filterOutdated = (packs: NodePack[]) =>
packs.filter((p) => {
const { isUpdateAvailable } = usePackUpdateStatus(p)
return isUpdateAvailable.value
})
const filterMissing = (packs: NodePack[]) =>
packs.filter((p) => !comfyManagerStore.isPackInstalled(p.id))
// Data fetching triggers using whenever
const needsInstalledPacks = computed(() =>
[
ManagerTab.AllInstalled,
ManagerTab.UpdateAvailable,
ManagerTab.Conflicting
].includes(tabType.value as ManagerTab)
)
const needsWorkflowPacks = computed(() =>
[ManagerTab.Workflow, ManagerTab.Missing].includes(
tabType.value as ManagerTab
)
)
whenever(
() =>
needsInstalledPacks.value &&
!installedPacksReady.value &&
!isLoadingInstalled.value,
() => startFetchInstalled()
)
whenever(
() =>
needsWorkflowPacks.value &&
!workflowPacksReady.value &&
!isLoadingWorkflow.value,
() => startFetchWorkflowPacks()
)
// For Missing tab, also need installed packs to determine what's missing
whenever(
() =>
tabType.value === ManagerTab.Missing &&
!installedPacksReady.value &&
!isLoadingInstalled.value,
() => startFetchInstalled()
)
// Single computed for display packs - replaces 7 watches
const displayPacks = computed(() => {
const tab = tabType.value
const hasSearch = !isEmptySearch.value
switch (tab) {
case ManagerTab.All:
return searchResults.value
case ManagerTab.NotInstalled:
return filterNotInstalled(searchResults.value)
case ManagerTab.AllInstalled:
return hasSearch
? filterInstalledPack(searchResults.value)
: sortPacks(installedPacks.value)
case ManagerTab.UpdateAvailable:
return sortPacks(
filterOutdated(
hasSearch
? filterInstalledPack(searchResults.value)
: installedPacks.value
)
)
case ManagerTab.Conflicting:
return sortPacks(
filterConflicting(
hasSearch
? filterInstalledPack(searchResults.value)
: installedPacks.value
)
)
case ManagerTab.Workflow: {
return hasSearch
? filterWorkflowPack(searchResults.value)
: sortPacks(workflowPacks.value)
}
case ManagerTab.Missing: {
const base = hasSearch
? filterWorkflowPack(searchResults.value)
: workflowPacks.value
return sortPacks(filterMissing(base))
}
default:
return searchResults.value
}
})
// Loading state - single computed
const isLoading = computed(() => {
const tab = tabType.value
if (
[
ManagerTab.AllInstalled,
ManagerTab.UpdateAvailable,
ManagerTab.Conflicting
].includes(tab as ManagerTab)
) {
return isLoadingInstalled.value
}
if ([ManagerTab.Workflow, ManagerTab.Missing].includes(tab as ManagerTab)) {
return isLoadingWorkflow.value
}
return false
})
const missingNodePacks = computed(() => filterMissing(workflowPacks.value))
return {
displayPacks,
isLoading,
isLoadingInstalled,
isLoadingWorkflow,
installedPacks,
workflowPacks,
filterInstalledPack,
filterWorkflowPack,
missingNodePacks
}
}

View File

@@ -90,7 +90,10 @@ export function useRegistrySearch(
} }
const onQueryChange = () => updateSearchResults({ append: false }) const onQueryChange = () => updateSearchResults({ append: false })
const onPageChange = () => updateSearchResults({ append: true }) const onPageChange = () => {
if (pageNumber.value === 0) return
updateSearchResults({ append: true })
}
watch([sortField, searchMode], onQueryChange) watch([sortField, searchMode], onQueryChange)
watch(pageNumber, onPageChange) watch(pageNumber, onPageChange)

View File

@@ -14,10 +14,12 @@ export const IsInstallingKey: InjectionKey<Ref<boolean>> =
export enum ManagerTab { export enum ManagerTab {
All = 'all', All = 'all',
Installed = 'installed', NotInstalled = 'notInstalled',
AllInstalled = 'allInstalled',
UpdateAvailable = 'updateAvailable',
Conflicting = 'conflicting',
Workflow = 'workflow', Workflow = 'workflow',
Missing = 'missing', Missing = 'missing'
UpdateAvailable = 'updateAvailable'
} }
export type TaskLog = { export type TaskLog = {