mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
[refactor] Manager dialog simplification (#8041)
## Summary Simplifies the Manager dialog by consolidating components and using BaseModalLayout with v-model support for right panel state. ## Changes - **Consolidation**: Merged ManagerDialogContent, ManagerHeader, ManagerNavSidebar, RegistrySearchBar, and SearchFilterDropdown into single ManagerDialog component - **Right panel**: Added v-model:rightPanelOpen to BaseModalLayout for external panel state control; clicking a node card now auto-opens the info panel - **Cleanup**: Removed unused useResponsiveCollapse composable, TabItem and SearchOption types - **UI tweaks**: Moved action buttons (Install All/Update All) from header-right-area to contentFilter area [manager-capture.webm](https://github.com/user-attachments/assets/2dd6092a-965d-4885-8ba6-6a2cc51f024a) 🤖 Generated with [Claude Code](https://claude.com/claude-code) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8041-refactor-Manager-dialog-simplification-2e86d73d3650815ba699e49a2748b682) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action <action@github.com> Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
@@ -55,17 +55,4 @@ const dialogStore = useDialogStore()
|
||||
@apply p-2 2xl:p-[var(--p-dialog-content-padding)];
|
||||
@apply pt-0;
|
||||
}
|
||||
|
||||
.manager-dialog {
|
||||
height: 80vh;
|
||||
max-width: 1724px;
|
||||
max-height: 1026px;
|
||||
}
|
||||
|
||||
@media (min-width: 3000px) {
|
||||
.manager-dialog {
|
||||
max-width: 2200px;
|
||||
max-height: 1320px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="base-widget-layout rounded-2xl overflow-hidden relative">
|
||||
<Button
|
||||
v-show="!isRightPanelOpen && hasRightPanel"
|
||||
size="icon"
|
||||
size="lg"
|
||||
:class="
|
||||
cn('absolute top-4 right-18 z-10', 'transition-opacity duration-200', {
|
||||
'opacity-0 pointer-events-none': isRightPanelOpen || !hasRightPanel
|
||||
@@ -10,7 +10,7 @@
|
||||
"
|
||||
@click="toggleRightPanel"
|
||||
>
|
||||
<i class="icon-[lucide--panel-right] text-sm" />
|
||||
<i class="icon-[lucide--panel-right]" />
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
@@ -64,7 +64,7 @@
|
||||
>
|
||||
<Button
|
||||
v-if="isRightPanelOpen && hasRightPanel"
|
||||
size="icon"
|
||||
size="lg"
|
||||
@click="toggleRightPanel"
|
||||
>
|
||||
<i class="icon-[lucide--panel-right-close]" />
|
||||
@@ -90,7 +90,7 @@
|
||||
</div>
|
||||
<aside
|
||||
v-if="hasRightPanel && isRightPanelOpen"
|
||||
class="w-1/4 min-w-40 max-w-80"
|
||||
class="w-1/4 min-w-40 max-w-80 pt-16 pb-8"
|
||||
>
|
||||
<slot name="rightPanel"></slot>
|
||||
</aside>
|
||||
@@ -111,6 +111,10 @@ const { contentTitle } = defineProps<{
|
||||
contentTitle: string
|
||||
}>()
|
||||
|
||||
const isRightPanelOpen = defineModel<boolean>('rightPanelOpen', {
|
||||
default: false
|
||||
})
|
||||
|
||||
const BREAKPOINTS = { md: 880 }
|
||||
const PANEL_SIZES = {
|
||||
width: 'w-1/3',
|
||||
@@ -125,7 +129,6 @@ const breakpoints = useBreakpoints(BREAKPOINTS)
|
||||
const notMobile = breakpoints.greater('md')
|
||||
|
||||
const isLeftPanelOpen = ref<boolean>(true)
|
||||
const isRightPanelOpen = ref<boolean>(false)
|
||||
const mobileMenuOpen = ref<boolean>(false)
|
||||
|
||||
const hasRightPanel = computed(() => !!slots.rightPanel)
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
type BreakpointKey = keyof typeof breakpointsTailwind
|
||||
|
||||
/**
|
||||
* Composable for element with responsive collapsed state
|
||||
* @param breakpointThreshold - Breakpoint at which the element should become collapsible
|
||||
*/
|
||||
export const useResponsiveCollapse = (
|
||||
breakpointThreshold: BreakpointKey = 'lg'
|
||||
) => {
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||
|
||||
const isSmallScreen = breakpoints.smallerOrEqual(breakpointThreshold)
|
||||
const isOpen = ref(!isSmallScreen.value)
|
||||
|
||||
/**
|
||||
* Handles screen size changes to automatically open/close the element
|
||||
* when crossing the breakpoint threshold
|
||||
*/
|
||||
const onIsSmallScreenChange = () => {
|
||||
if (isSmallScreen.value && isOpen.value) {
|
||||
isOpen.value = false
|
||||
} else if (!isSmallScreen.value && !isOpen.value) {
|
||||
isOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
watch(isSmallScreen, onIsSmallScreenChange)
|
||||
|
||||
return {
|
||||
breakpoints,
|
||||
isOpen,
|
||||
isSmallScreen,
|
||||
|
||||
open: () => (isOpen.value = true),
|
||||
close: () => (isOpen.value = false),
|
||||
toggle: () => (isOpen.value = !isOpen.value)
|
||||
}
|
||||
}
|
||||
@@ -273,7 +273,7 @@
|
||||
"noItems": "No items"
|
||||
},
|
||||
"manager": {
|
||||
"title": "Custom Nodes Manager",
|
||||
"title": "Nodes Manager",
|
||||
"legacyMenuNotAvailable": "Legacy manager menu is not available, defaulting to the new manager menu.",
|
||||
"legacyManagerUI": "Use Legacy UI",
|
||||
"legacyManagerUIDescription": "To use the legacy Manager UI, start ComfyUI with --enable-manager-legacy-ui",
|
||||
|
||||
@@ -25,8 +25,6 @@ import type {
|
||||
ShowDialogOptions
|
||||
} from '@/stores/dialogStore'
|
||||
|
||||
import ManagerDialogContent from '@/workbench/extensions/manager/components/manager/ManagerDialogContent.vue'
|
||||
import ManagerHeader from '@/workbench/extensions/manager/components/manager/ManagerHeader.vue'
|
||||
import ImportFailedNodeContent from '@/workbench/extensions/manager/components/manager/ImportFailedNodeContent.vue'
|
||||
import ImportFailedNodeFooter from '@/workbench/extensions/manager/components/manager/ImportFailedNodeFooter.vue'
|
||||
import ImportFailedNodeHeader from '@/workbench/extensions/manager/components/manager/ImportFailedNodeHeader.vue'
|
||||
@@ -152,32 +150,6 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
function showManagerDialog(
|
||||
props: ComponentAttrs<typeof ManagerDialogContent> = {}
|
||||
) {
|
||||
dialogStore.showDialog({
|
||||
key: 'global-manager',
|
||||
component: ManagerDialogContent,
|
||||
headerComponent: ManagerHeader,
|
||||
dialogComponentProps: {
|
||||
closable: true,
|
||||
pt: {
|
||||
pcCloseButton: {
|
||||
root: {
|
||||
class: 'bg-dialog-surface w-9 h-9 p-1.5 rounded-full text-white'
|
||||
}
|
||||
},
|
||||
header: { class: 'py-0! px-6 m-0! h-[68px]' },
|
||||
content: {
|
||||
class: 'p-0! h-full w-[90vw] max-w-full flex-1 overflow-hidden'
|
||||
},
|
||||
root: { class: 'manager-dialog' }
|
||||
}
|
||||
},
|
||||
props
|
||||
})
|
||||
}
|
||||
|
||||
function parseError(error: Error) {
|
||||
const filename =
|
||||
'fileName' in error
|
||||
@@ -419,20 +391,10 @@ export const useDialogService = () => {
|
||||
}
|
||||
}
|
||||
|
||||
function toggleManagerDialog(
|
||||
props?: ComponentAttrs<typeof ManagerDialogContent>
|
||||
) {
|
||||
if (dialogStore.isDialogOpen('global-manager')) {
|
||||
dialogStore.closeDialog({ key: 'global-manager' })
|
||||
} else {
|
||||
showManagerDialog(props)
|
||||
}
|
||||
}
|
||||
|
||||
function showLayoutDialog(options: {
|
||||
key: string
|
||||
component: Component
|
||||
props: { onClose: () => void }
|
||||
props: { onClose: () => void } & Record<string, unknown>
|
||||
dialogComponentProps?: DialogComponentProps
|
||||
}) {
|
||||
const layoutDefaultProps: DialogComponentProps = {
|
||||
@@ -563,7 +525,6 @@ export const useDialogService = () => {
|
||||
showSettingsDialog,
|
||||
showAboutDialog,
|
||||
showExecutionErrorDialog,
|
||||
showManagerDialog,
|
||||
showApiNodesSignInDialog,
|
||||
showSignInDialog,
|
||||
showSubscriptionRequiredDialog,
|
||||
@@ -573,7 +534,6 @@ export const useDialogService = () => {
|
||||
prompt,
|
||||
showErrorDialog,
|
||||
confirm,
|
||||
toggleManagerDialog,
|
||||
showLayoutDialog,
|
||||
showImportFailedNodeDialog,
|
||||
showNodeConflictDialog
|
||||
|
||||
@@ -1,180 +1,218 @@
|
||||
<template>
|
||||
<div
|
||||
class="mx-auto flex h-full flex-col overflow-hidden"
|
||||
:aria-label="$t('manager.title')"
|
||||
<BaseModalLayout
|
||||
v-model:right-panel-open="isRightPanelOpen"
|
||||
:content-title="$t('manager.discoverCommunityContent')"
|
||||
class="manager-dialog"
|
||||
>
|
||||
<ContentDivider :width="0.3" />
|
||||
<Button
|
||||
v-if="isSmallScreen"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
:class="
|
||||
cn(
|
||||
'absolute top-1/2 z-10 -translate-y-1/2',
|
||||
isSideNavOpen ? 'left-[12rem]' : 'left-2'
|
||||
)
|
||||
"
|
||||
@click="toggleSideNav"
|
||||
>
|
||||
<i
|
||||
:class="isSideNavOpen ? 'pi pi-chevron-left' : 'pi pi-chevron-right'"
|
||||
/>
|
||||
</Button>
|
||||
<div class="relative flex flex-1 overflow-hidden">
|
||||
<ManagerNavSidebar
|
||||
v-if="isSideNavOpen"
|
||||
v-model:selected-tab="selectedTab"
|
||||
:tabs="tabs"
|
||||
/>
|
||||
<template #leftPanel>
|
||||
<LeftSidePanel v-model="selectedNavId" :nav-items="navItems">
|
||||
<template #header-icon>
|
||||
<i class="icon-[lucide--puzzle]" />
|
||||
</template>
|
||||
<template #header-title>
|
||||
<span class="text-neutral text-base">{{ $t('manager.title') }}</span>
|
||||
</template>
|
||||
</LeftSidePanel>
|
||||
</template>
|
||||
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<SingleSelect
|
||||
v-model="searchMode"
|
||||
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="{
|
||||
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>
|
||||
</template>
|
||||
|
||||
<template #contentFilter>
|
||||
<!-- Conflict Warning Banner -->
|
||||
<div
|
||||
class="flex-1 overflow-auto bg-base-background"
|
||||
:class="{
|
||||
'transition-all duration-300': isSmallScreen
|
||||
}"
|
||||
v-if="shouldShowManagerBanner"
|
||||
class="relative mx-6 mt-3 mb-4 flex items-center gap-6 rounded-lg bg-yellow-500/20 p-4"
|
||||
>
|
||||
<div class="flex h-full flex-col px-6">
|
||||
<!-- Conflict Warning Banner -->
|
||||
<div
|
||||
v-if="shouldShowManagerBanner"
|
||||
class="relative mt-3 mb-4 flex items-center gap-6 rounded-lg bg-yellow-500/20 p-4"
|
||||
<i
|
||||
class="icon-[lucide--triangle-alert] text-lg text-warning-background"
|
||||
/>
|
||||
<div class="flex flex-1 flex-col gap-2">
|
||||
<p class="m-0 text-sm font-bold">
|
||||
{{ $t('manager.conflicts.warningBanner.title') }}
|
||||
</p>
|
||||
<p class="m-0 text-xs">
|
||||
{{ $t('manager.conflicts.warningBanner.message') }}
|
||||
</p>
|
||||
<p
|
||||
class="m-0 cursor-pointer text-sm font-bold"
|
||||
@click="onClickWarningLink"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--triangle-alert] text-lg text-warning-background"
|
||||
/>
|
||||
<div class="flex flex-1 flex-col gap-2">
|
||||
<p class="m-0 text-sm font-bold">
|
||||
{{ $t('manager.conflicts.warningBanner.title') }}
|
||||
</p>
|
||||
<p class="m-0 text-xs">
|
||||
{{ $t('manager.conflicts.warningBanner.message') }}
|
||||
</p>
|
||||
<p
|
||||
class="m-0 cursor-pointer text-sm font-bold"
|
||||
@click="onClickWarningLink"
|
||||
>
|
||||
{{ $t('manager.conflicts.warningBanner.button') }}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
class="absolute top-0 right-0"
|
||||
variant="textonly"
|
||||
size="icon"
|
||||
@click="dismissWarningBanner"
|
||||
>
|
||||
<i class="pi pi-times text-xs text-base-foreground"></i>
|
||||
</Button>
|
||||
</div>
|
||||
<RegistrySearchBar
|
||||
v-model:search-query="searchQuery"
|
||||
v-model:search-mode="searchMode"
|
||||
v-model:sort-field="sortField"
|
||||
:search-results="searchResults"
|
||||
:suggestions="suggestions"
|
||||
:is-missing-tab="isMissingTab"
|
||||
:sort-options="sortOptions"
|
||||
:is-update-available-tab="isUpdateAvailableTab"
|
||||
{{ $t('manager.conflicts.warningBanner.button') }}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
class="absolute top-0 right-0"
|
||||
variant="textonly"
|
||||
size="icon"
|
||||
@click="dismissWarningBanner"
|
||||
>
|
||||
<i class="pi pi-times text-xs text-base-foreground"></i>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Filters Row -->
|
||||
<div class="relative flex flex-wrap justify-between gap-2 px-6 pb-4">
|
||||
<div>
|
||||
<PackInstallButton
|
||||
v-if="isMissingTab && missingNodePacks.length > 0"
|
||||
:disabled="isMissingLoading || !!missingError"
|
||||
:node-packs="missingNodePacks"
|
||||
size="lg"
|
||||
:label="$t('manager.installAllMissingNodes')"
|
||||
/>
|
||||
<div class="flex-1 overflow-auto">
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="h-full scrollbar-hide w-full overflow-auto"
|
||||
>
|
||||
<GridSkeleton :grid-style="GRID_STYLE" :skeleton-card-count />
|
||||
</div>
|
||||
<NoResultsPlaceholder
|
||||
v-else-if="searchResults.length === 0"
|
||||
:title="
|
||||
comfyManagerStore.error
|
||||
? $t('manager.errorConnecting')
|
||||
: $t('manager.noResultsFound')
|
||||
"
|
||||
:message="
|
||||
comfyManagerStore.error
|
||||
? $t('manager.tryAgainLater')
|
||||
: $t('manager.tryDifferentSearch')
|
||||
"
|
||||
/>
|
||||
<div v-else class="h-full" @click="handleGridContainerClick">
|
||||
<VirtualGrid
|
||||
id="results-grid"
|
||||
:items="resultsWithKeys"
|
||||
:buffer-rows="4"
|
||||
:grid-style="GRID_STYLE"
|
||||
@approach-end="onApproachEnd"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<PackCard
|
||||
:node-pack="item"
|
||||
:is-selected="
|
||||
selectedNodePacks.some((pack) => pack.id === item.id)
|
||||
"
|
||||
@click.stop="
|
||||
(event: MouseEvent) => selectNodePack(item, event)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</VirtualGrid>
|
||||
</div>
|
||||
</div>
|
||||
<PackUpdateButton
|
||||
v-if="isUpdateAvailableTab && hasUpdateAvailable"
|
||||
: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 class="z-20 flex w-[clamp(250px,33%,306px)] border-l-0">
|
||||
<ContentDivider orientation="vertical" :width="0.2" />
|
||||
<div class="isolate flex w-full flex-col">
|
||||
<InfoPanel
|
||||
v-if="!hasMultipleSelections && selectedNodePack"
|
||||
:node-pack="selectedNodePack"
|
||||
/>
|
||||
<InfoPanelMultiItem v-else :node-packs="selectedNodePacks" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<div v-if="isLoading" class="scrollbar-hide h-full w-full overflow-auto">
|
||||
<GridSkeleton :grid-style="GRID_STYLE" :skeleton-card-count />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<NoResultsPlaceholder
|
||||
v-else-if="searchResults.length === 0"
|
||||
:title="
|
||||
comfyManagerStore.error
|
||||
? $t('manager.errorConnecting')
|
||||
: $t('manager.noResultsFound')
|
||||
"
|
||||
:message="
|
||||
comfyManagerStore.error
|
||||
? $t('manager.tryAgainLater')
|
||||
: $t('manager.tryDifferentSearch')
|
||||
"
|
||||
/>
|
||||
<div v-else class="h-full" @click="handleGridContainerClick">
|
||||
<VirtualGrid
|
||||
id="results-grid"
|
||||
:items="resultsWithKeys"
|
||||
:buffer-rows="4"
|
||||
:grid-style="GRID_STYLE"
|
||||
@approach-end="onApproachEnd"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<PackCard
|
||||
:node-pack="item"
|
||||
:is-selected="
|
||||
selectedNodePacks.some((pack) => pack.id === item.id)
|
||||
"
|
||||
@click.stop="(event: MouseEvent) => selectNodePack(item, event)"
|
||||
/>
|
||||
</template>
|
||||
</VirtualGrid>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #rightPanel>
|
||||
<InfoPanel
|
||||
v-if="!hasMultipleSelections && selectedNodePack"
|
||||
:node-pack="selectedNodePack"
|
||||
/>
|
||||
<InfoPanelMultiItem v-else :node-packs="selectedNodePacks" />
|
||||
</template>
|
||||
</BaseModalLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { merge } from 'es-toolkit/compat'
|
||||
import { merge, stubTrue } from 'es-toolkit/compat'
|
||||
import type { AutoCompleteOptionSelectEvent } from 'primevue/autocomplete'
|
||||
import {
|
||||
computed,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
provide,
|
||||
ref,
|
||||
watch,
|
||||
watchEffect
|
||||
} from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import VirtualGrid from '@/components/common/VirtualGrid.vue'
|
||||
import SingleSelect from '@/components/input/SingleSelect.vue'
|
||||
import AutoCompletePlus from '@/components/primevueOverride/AutoCompletePlus.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useResponsiveCollapse } from '@/composables/element/useResponsiveCollapse'
|
||||
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
|
||||
import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
|
||||
import { useExternalLink } from '@/composables/useExternalLink'
|
||||
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import ManagerNavSidebar from '@/workbench/extensions/manager/components/manager/ManagerNavSidebar.vue'
|
||||
import type { NavItemData } from '@/types/navTypes'
|
||||
import { OnCloseKey } from '@/types/widgetTypes'
|
||||
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
|
||||
import PackUpdateButton from '@/workbench/extensions/manager/components/manager/button/PackUpdateButton.vue'
|
||||
import InfoPanel from '@/workbench/extensions/manager/components/manager/infoPanel/InfoPanel.vue'
|
||||
import InfoPanelMultiItem from '@/workbench/extensions/manager/components/manager/infoPanel/InfoPanelMultiItem.vue'
|
||||
import PackCard from '@/workbench/extensions/manager/components/manager/packCard/PackCard.vue'
|
||||
import RegistrySearchBar from '@/workbench/extensions/manager/components/manager/registrySearchBar/RegistrySearchBar.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 { usePackUpdateStatus } from '@/workbench/extensions/manager/composables/nodePack/usePackUpdateStatus'
|
||||
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 { useManagerStatePersistence } from '@/workbench/extensions/manager/composables/useManagerStatePersistence'
|
||||
import { useRegistrySearch } from '@/workbench/extensions/manager/composables/useRegistrySearch'
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
import type { TabItem } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
|
||||
const { initialTab } = defineProps<{
|
||||
const { initialTab, onClose } = defineProps<{
|
||||
initialTab?: ManagerTab
|
||||
onClose: () => void
|
||||
}>()
|
||||
|
||||
provide(OnCloseKey, onClose)
|
||||
|
||||
const { t } = useI18n()
|
||||
const { buildDocsUrl } = useExternalLink()
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
@@ -186,46 +224,56 @@ const initialState = persistedState.loadStoredState()
|
||||
const GRID_STYLE = {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(17rem, 1fr))',
|
||||
padding: '0.5rem',
|
||||
gap: '1.5rem'
|
||||
gap: '1.5rem',
|
||||
padding: '0'
|
||||
} as const
|
||||
|
||||
const {
|
||||
isSmallScreen,
|
||||
isOpen: isSideNavOpen,
|
||||
toggle: toggleSideNav
|
||||
} = useResponsiveCollapse()
|
||||
|
||||
// Use conflict acknowledgment state from composable
|
||||
const {
|
||||
shouldShowManagerBanner,
|
||||
dismissWarningBanner,
|
||||
dismissRedDotNotification
|
||||
} = conflictAcknowledgment
|
||||
|
||||
const tabs = ref<TabItem[]>([
|
||||
{ id: ManagerTab.All, label: t('g.all'), icon: 'pi-list' },
|
||||
{ id: ManagerTab.Installed, label: t('g.installed'), icon: 'pi-box' },
|
||||
// Missing nodes composable
|
||||
const {
|
||||
missingNodePacks,
|
||||
isLoading: isMissingLoading,
|
||||
error: missingError
|
||||
} = useMissingNodes()
|
||||
|
||||
// Update available nodes composable
|
||||
const {
|
||||
hasUpdateAvailable,
|
||||
enabledUpdateAvailableNodePacks,
|
||||
hasDisabledUpdatePacks
|
||||
} = useUpdateAvailableNodes()
|
||||
|
||||
// Navigation items for LeftSidePanel
|
||||
const navItems = computed<NavItemData[]>(() => [
|
||||
{ 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,
|
||||
label: t('manager.inWorkflow'),
|
||||
icon: 'pi-folder'
|
||||
icon: 'pi pi-folder'
|
||||
},
|
||||
{
|
||||
id: ManagerTab.Missing,
|
||||
label: t('g.missing'),
|
||||
icon: 'pi-exclamation-circle'
|
||||
icon: 'pi pi-exclamation-circle'
|
||||
},
|
||||
{
|
||||
id: ManagerTab.UpdateAvailable,
|
||||
label: t('g.updateAvailable'),
|
||||
icon: 'pi-sync'
|
||||
icon: 'pi pi-sync'
|
||||
}
|
||||
])
|
||||
|
||||
const initialTabId = initialTab ?? initialState.selectedTabId
|
||||
const selectedTab = ref<TabItem>(
|
||||
tabs.value.find((tab) => tab.id === initialTabId) || tabs.value[0]
|
||||
const initialTabId = initialTab ?? initialState.selectedTabId ?? ManagerTab.All
|
||||
const selectedNavId = ref<string | null>(initialTabId)
|
||||
|
||||
const selectedTab = computed(() =>
|
||||
navItems.value.find((item) => item.id === selectedNavId.value)
|
||||
)
|
||||
|
||||
const {
|
||||
@@ -243,6 +291,25 @@ const {
|
||||
initialSearchQuery: initialState.searchQuery
|
||||
})
|
||||
pageNumber.value = 0
|
||||
|
||||
// Filter and sort options for SingleSelect
|
||||
const filterOptions = computed(() => [
|
||||
{ name: t('manager.filter.nodePack'), value: 'packs' },
|
||||
{ name: t('g.nodes'), value: 'nodes' }
|
||||
])
|
||||
|
||||
const availableSortOptions = computed(() => {
|
||||
if (!sortOptions.value) return []
|
||||
return sortOptions.value.map((field) => ({
|
||||
name: field.label,
|
||||
value: field.id
|
||||
}))
|
||||
})
|
||||
|
||||
const onOptionSelect = (event: AutoCompleteOptionSelectEvent) => {
|
||||
searchQuery.value = event.value.query
|
||||
}
|
||||
|
||||
const onApproachEnd = () => {
|
||||
pageNumber.value++
|
||||
}
|
||||
@@ -433,6 +500,14 @@ const selectedNodePacks = ref<components['schemas']['Node'][]>([])
|
||||
const selectedNodePack = computed<components['schemas']['Node'] | null>(() =>
|
||||
selectedNodePacks.value.length === 1 ? selectedNodePacks.value[0] : null
|
||||
)
|
||||
const isRightPanelOpen = ref(false)
|
||||
|
||||
watch(
|
||||
() => selectedNodePacks.value.length,
|
||||
(length) => {
|
||||
isRightPanelOpen.value = length > 0
|
||||
}
|
||||
)
|
||||
|
||||
const getLoadingCount = () => {
|
||||
switch (selectedTab.value?.id) {
|
||||
@@ -452,28 +527,26 @@ const getLoadingCount = () => {
|
||||
const skeletonCardCount = computed(() => {
|
||||
const loadingCount = getLoadingCount()
|
||||
if (loadingCount) return loadingCount
|
||||
return isSmallScreen.value ? 12 : 16
|
||||
return 16
|
||||
})
|
||||
|
||||
const selectNodePack = (
|
||||
nodePack: components['schemas']['Node'],
|
||||
event: MouseEvent
|
||||
) => {
|
||||
// Handle multi-select with Shift or Ctrl/Cmd key
|
||||
if (event.shiftKey || event.ctrlKey || event.metaKey) {
|
||||
const index = selectedNodePacks.value.findIndex(
|
||||
(pack) => pack.id === nodePack.id
|
||||
)
|
||||
|
||||
if (index === -1) {
|
||||
// Add to selection if not already selected
|
||||
selectedNodePacks.value.push(nodePack)
|
||||
selectedNodePacks.value = [...selectedNodePacks.value, nodePack]
|
||||
} else {
|
||||
// Remove from selection if already selected
|
||||
selectedNodePacks.value.splice(index, 1)
|
||||
selectedNodePacks.value = selectedNodePacks.value.filter(
|
||||
(pack) => pack.id !== nodePack.id
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Single select behavior
|
||||
selectedNodePacks.value = [nodePack]
|
||||
}
|
||||
}
|
||||
@@ -490,32 +563,24 @@ const handleGridContainerClick = (event: MouseEvent) => {
|
||||
|
||||
const hasMultipleSelections = computed(() => selectedNodePacks.value.length > 1)
|
||||
|
||||
// Track the last pack ID for which we've fetched full registry data
|
||||
const lastFetchedPackId = ref<string | null>(null)
|
||||
|
||||
// Whenever a single pack is selected, fetch its full info once
|
||||
whenever(selectedNodePack, async () => {
|
||||
// Cancel any in-flight requests from previously selected node pack
|
||||
getPackById.cancel()
|
||||
// If only a single node pack is selected, fetch full node pack info from registry
|
||||
const pack = selectedNodePack.value
|
||||
if (!pack?.id) return
|
||||
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)
|
||||
// If selected node hasn't changed since request, merge registry & Algolia data
|
||||
if (data?.id === pack.id) {
|
||||
lastFetchedPackId.value = pack.id
|
||||
const mergedPack = merge({}, pack, data)
|
||||
// Update the pack in current selection without changing selection state
|
||||
const packIndex = selectedNodePacks.value.findIndex(
|
||||
(p) => p.id === mergedPack.id
|
||||
)
|
||||
if (packIndex !== -1) {
|
||||
selectedNodePacks.value.splice(packIndex, 1, mergedPack)
|
||||
}
|
||||
// Replace pack in displayPacks so that children receive a fresh prop reference
|
||||
const idx = displayPacks.value.findIndex((p) => p.id === mergedPack.id)
|
||||
if (idx !== -1) {
|
||||
displayPacks.value.splice(idx, 1, mergedPack)
|
||||
@@ -527,7 +592,7 @@ let gridContainer: HTMLElement | null = null
|
||||
onMounted(() => {
|
||||
gridContainer = document.getElementById('results-grid')
|
||||
})
|
||||
watch([searchQuery, selectedTab], () => {
|
||||
watch([searchQuery, selectedNavId], () => {
|
||||
gridContainer ??= document.getElementById('results-grid')
|
||||
if (gridContainer) {
|
||||
pageNumber.value = 0
|
||||
@@ -541,7 +606,7 @@ watchEffect(() => {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
persistedState.persistState({
|
||||
selectedTabId: selectedTab.value?.id,
|
||||
selectedTabId: (selectedTab.value?.id as ManagerTab) ?? ManagerTab.All,
|
||||
searchQuery: searchQuery.value,
|
||||
searchMode: searchMode.value,
|
||||
sortField: sortField.value
|
||||
@@ -1,45 +0,0 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json' with { type: 'json' }
|
||||
|
||||
import ManagerHeader from './ManagerHeader.vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: enMessages
|
||||
}
|
||||
})
|
||||
|
||||
describe('ManagerHeader', () => {
|
||||
const createWrapper = () => {
|
||||
return mount(ManagerHeader, {
|
||||
global: {
|
||||
plugins: [createPinia(), PrimeVue, i18n]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
it('renders the component title', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
expect(wrapper.find('h2').text()).toBe(
|
||||
enMessages.manager.discoverCommunityContent
|
||||
)
|
||||
})
|
||||
|
||||
it('has proper structure with flex container', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const flexContainer = wrapper.find('.flex.items-center')
|
||||
expect(flexContainer.exists()).toBe(true)
|
||||
|
||||
const title = flexContainer.find('h2')
|
||||
expect(title.exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,11 +0,0 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<h2 class="text-left text-lg font-normal">
|
||||
{{ $t('manager.discoverCommunityContent') }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
@@ -1,42 +0,0 @@
|
||||
<template>
|
||||
<aside
|
||||
class="z-5 flex w-3/12 max-w-[250px] translate-x-0 transition-transform duration-300 ease-in-out"
|
||||
>
|
||||
<ScrollPanel class="flex-1">
|
||||
<Listbox
|
||||
v-model="selectedTab"
|
||||
:options="tabs"
|
||||
option-label="label"
|
||||
list-style="max-height:unset"
|
||||
class="w-full border-0 bg-transparent shadow-none"
|
||||
:pt="{
|
||||
list: { class: 'p-3 gap-2' },
|
||||
option: { class: 'px-4 py-2 text-lg rounded-lg' },
|
||||
optionGroup: { class: 'p-0 text-left text-inherit' }
|
||||
}"
|
||||
>
|
||||
<template #option="slotProps">
|
||||
<div class="flex items-center text-left">
|
||||
<i :class="['pi', slotProps.option.icon, 'mr-2 text-sm']" />
|
||||
<span class="text-sm">{{ slotProps.option.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</Listbox>
|
||||
</ScrollPanel>
|
||||
<ContentDivider orientation="vertical" :width="0.3" />
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Listbox from 'primevue/listbox'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
import type { TabItem } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
|
||||
defineProps<{
|
||||
tabs: TabItem[]
|
||||
}>()
|
||||
|
||||
const selectedTab = defineModel<TabItem>('selectedTab')
|
||||
</script>
|
||||
@@ -3,9 +3,8 @@
|
||||
v-tooltip.top="
|
||||
hasDisabledUpdatePacks ? $t('manager.disabledNodesWontUpdate') : null
|
||||
"
|
||||
variant="textonly"
|
||||
class="border"
|
||||
size="sm"
|
||||
:size
|
||||
:disabled="isUpdating"
|
||||
@click="updateAllPacks"
|
||||
>
|
||||
@@ -19,14 +18,20 @@ import { ref } from 'vue'
|
||||
|
||||
import DotSpinner from '@/components/common/DotSpinner.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import type { ButtonVariants } from '@/components/ui/button/button.variants'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
const { nodePacks, hasDisabledUpdatePacks } = defineProps<{
|
||||
const {
|
||||
nodePacks,
|
||||
hasDisabledUpdatePacks,
|
||||
size = 'sm'
|
||||
} = defineProps<{
|
||||
nodePacks: NodePack[]
|
||||
hasDisabledUpdatePacks?: boolean
|
||||
size?: ButtonVariants['size']
|
||||
}>()
|
||||
|
||||
const isUpdating = ref<boolean>(false)
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
<template>
|
||||
<div class="relative w-full p-6">
|
||||
<div class="flex h-12 items-center justify-between gap-1">
|
||||
<div class="flex w-5/12 items-center">
|
||||
<AutoComplete
|
||||
v-model.lazy="searchQuery"
|
||||
:suggestions="suggestions || []"
|
||||
:placeholder="$t('manager.searchPlaceholder')"
|
||||
:complete-on-focus="false"
|
||||
:delay="8"
|
||||
option-label="query"
|
||||
class="w-full"
|
||||
:pt="{
|
||||
pcInputText: {
|
||||
root: {
|
||||
autofocus: true,
|
||||
class: 'w-full rounded-2xl'
|
||||
}
|
||||
},
|
||||
loader: {
|
||||
style: 'display: none'
|
||||
}
|
||||
}"
|
||||
:show-empty-message="false"
|
||||
@complete="stubTrue"
|
||||
@option-select="onOptionSelect"
|
||||
/>
|
||||
</div>
|
||||
<PackInstallButton
|
||||
v-if="isMissingTab && missingNodePacks.length > 0"
|
||||
:disabled="isLoading || !!error"
|
||||
:node-packs="missingNodePacks"
|
||||
:label="$t('manager.installAllMissingNodes')"
|
||||
/>
|
||||
<PackUpdateButton
|
||||
v-if="isUpdateAvailableTab && hasUpdateAvailable"
|
||||
:node-packs="enabledUpdateAvailableNodePacks"
|
||||
:has-disabled-update-packs="hasDisabledUpdatePacks"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-3 flex text-sm">
|
||||
<div class="ml-1 flex gap-6">
|
||||
<SearchFilterDropdown
|
||||
v-model:model-value="searchMode"
|
||||
:options="filterOptions"
|
||||
:label="$t('g.filter')"
|
||||
/>
|
||||
<SearchFilterDropdown
|
||||
v-model:model-value="sortField"
|
||||
:options="availableSortOptions"
|
||||
:label="$t('g.sort')"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-6 flex items-center gap-4">
|
||||
<small v-if="hasResults" class="text-color-secondary">
|
||||
{{ $t('g.resultsCount', { count: searchResults?.length || 0 }) }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { stubTrue } from 'es-toolkit/compat'
|
||||
import type { AutoCompleteOptionSelectEvent } from 'primevue/autocomplete'
|
||||
import AutoComplete from 'primevue/autocomplete'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import type {
|
||||
QuerySuggestion,
|
||||
SearchMode,
|
||||
SortableField
|
||||
} from '@/types/searchServiceTypes'
|
||||
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
|
||||
import PackUpdateButton from '@/workbench/extensions/manager/components/manager/button/PackUpdateButton.vue'
|
||||
import SearchFilterDropdown from '@/workbench/extensions/manager/components/manager/registrySearchBar/SearchFilterDropdown.vue'
|
||||
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
||||
import { useUpdateAvailableNodes } from '@/workbench/extensions/manager/composables/nodePack/useUpdateAvailableNodes'
|
||||
import { SortableAlgoliaField } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
import type { SearchOption } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
|
||||
const { searchResults, sortOptions } = defineProps<{
|
||||
searchResults?: components['schemas']['Node'][]
|
||||
suggestions?: QuerySuggestion[]
|
||||
sortOptions?: SortableField[]
|
||||
isMissingTab?: boolean
|
||||
isUpdateAvailableTab?: boolean
|
||||
}>()
|
||||
|
||||
const searchQuery = defineModel<string>('searchQuery')
|
||||
const searchMode = defineModel<SearchMode>('searchMode', { default: 'packs' })
|
||||
const sortField = defineModel<string>('sortField', {
|
||||
default: SortableAlgoliaField.Downloads
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// Get missing node packs from workflow with loading and error states
|
||||
const { missingNodePacks, isLoading, error } = useMissingNodes()
|
||||
|
||||
// Use the composable to get update available nodes
|
||||
const {
|
||||
hasUpdateAvailable,
|
||||
enabledUpdateAvailableNodePacks,
|
||||
hasDisabledUpdatePacks
|
||||
} = useUpdateAvailableNodes()
|
||||
|
||||
const hasResults = computed(
|
||||
() => searchQuery.value?.trim() && searchResults?.length
|
||||
)
|
||||
|
||||
const availableSortOptions = computed<SearchOption<string>[]>(() => {
|
||||
if (!sortOptions) return []
|
||||
return sortOptions.map((field) => ({
|
||||
id: field.id,
|
||||
label: field.label
|
||||
}))
|
||||
})
|
||||
const filterOptions: SearchOption<SearchMode>[] = [
|
||||
{ id: 'packs', label: t('manager.filter.nodePack') },
|
||||
{ id: 'nodes', label: t('g.nodes') }
|
||||
]
|
||||
|
||||
// When a dropdown query suggestion is selected, update the search query
|
||||
const onOptionSelect = (event: AutoCompleteOptionSelectEvent) => {
|
||||
searchQuery.value = event.value.query
|
||||
}
|
||||
</script>
|
||||
@@ -1,36 +0,0 @@
|
||||
<template>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-muted">{{ label }}:</span>
|
||||
<Dropdown
|
||||
:model-value="modelValue"
|
||||
:options="options"
|
||||
option-label="label"
|
||||
option-value="id"
|
||||
class="min-w-[6rem] border-none bg-transparent shadow-none"
|
||||
:pt="{
|
||||
input: { class: 'py-0 px-1 border-none' },
|
||||
trigger: { class: 'hidden' },
|
||||
panel: { class: 'shadow-md' },
|
||||
item: { class: 'py-2 px-3 text-sm' }
|
||||
}"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" generic="T">
|
||||
// oxlint-disable-next-line no-restricted-imports -- TODO: Migrate to Select component
|
||||
import Dropdown from 'primevue/dropdown'
|
||||
|
||||
import type { SearchOption } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
|
||||
defineProps<{
|
||||
options: SearchOption<T>[]
|
||||
label: string
|
||||
modelValue: T
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
'update:modelValue': [value: T]
|
||||
}>()
|
||||
</script>
|
||||
@@ -14,7 +14,6 @@ import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comf
|
||||
|
||||
/**
|
||||
* Composable to find missing NodePacks from workflow
|
||||
* Uses the same filtering approach as ManagerDialogContent.vue
|
||||
* Automatically fetches workflow pack data when initialized
|
||||
* This is a shared singleton composable - all components use the same instance
|
||||
*/
|
||||
@@ -25,7 +24,6 @@ export const useMissingNodes = createSharedComposable(() => {
|
||||
const { workflowPacks, isLoading, error, startFetchWorkflowPacks } =
|
||||
useWorkflowPacks()
|
||||
|
||||
// Same filtering logic as ManagerDialogContent.vue
|
||||
const filterMissingPacks = (packs: components['schemas']['Node'][]) =>
|
||||
packs.filter((pack) => !comfyManagerStore.isPackInstalled(pack.id))
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comf
|
||||
|
||||
/**
|
||||
* Composable to find NodePacks that have updates available
|
||||
* Uses the same filtering approach as ManagerDialogContent.vue
|
||||
* Automatically fetches installed pack data when initialized
|
||||
*/
|
||||
export const useUpdateAvailableNodes = () => {
|
||||
@@ -34,7 +33,6 @@ export const useUpdateAvailableNodes = () => {
|
||||
return compare(latestVersion, installedVersion) > 0
|
||||
}
|
||||
|
||||
// Same filtering logic as ManagerDialogContent.vue
|
||||
const filterOutdatedPacks = (packs: components['schemas']['Node'][]) =>
|
||||
packs.filter(isOutdatedPack)
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import type { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
import ManagerDialog from '@/workbench/extensions/manager/components/manager/ManagerDialog.vue'
|
||||
|
||||
const DIALOG_KEY = 'global-manager'
|
||||
|
||||
export function useManagerDialog() {
|
||||
const dialogService = useDialogService()
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
function hide() {
|
||||
dialogStore.closeDialog({ key: DIALOG_KEY })
|
||||
}
|
||||
|
||||
function show(initialTab?: ManagerTab) {
|
||||
dialogService.showLayoutDialog({
|
||||
key: DIALOG_KEY,
|
||||
component: ManagerDialog,
|
||||
props: {
|
||||
onClose: hide,
|
||||
initialTab
|
||||
},
|
||||
dialogComponentProps: {
|
||||
pt: {
|
||||
content: { class: '!px-0 overflow-hidden h-full !py-0' }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
show,
|
||||
hide
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,13 @@ vi.mock('@/stores/toastStore', () => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/workbench/extensions/manager/composables/useManagerDialog', () => ({
|
||||
useManagerDialog: vi.fn(() => ({
|
||||
show: vi.fn(),
|
||||
hide: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
describe('useManagerState', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
@@ -7,6 +7,7 @@ import { api } from '@/scripts/api'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
import { useManagerDialog } from '@/workbench/extensions/manager/composables/useManagerDialog'
|
||||
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
|
||||
export enum ManagerUIState {
|
||||
@@ -19,6 +20,7 @@ export function useManagerState() {
|
||||
const systemStatsStore = useSystemStatsStore()
|
||||
const { systemStats, isInitialized: systemInitialized } =
|
||||
storeToRefs(systemStatsStore)
|
||||
const managerDialog = useManagerDialog()
|
||||
|
||||
/**
|
||||
* The current manager UI state.
|
||||
@@ -186,11 +188,9 @@ export function useManagerState() {
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
dialogService.showManagerDialog({ initialTab: ManagerTab.All })
|
||||
await managerDialog.show(ManagerTab.All)
|
||||
} else {
|
||||
dialogService.showManagerDialog(
|
||||
options?.initialTab ? { initialTab: options.initialTab } : undefined
|
||||
)
|
||||
await managerDialog.show(options?.initialTab)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -20,12 +20,6 @@ export enum ManagerTab {
|
||||
UpdateAvailable = 'updateAvailable'
|
||||
}
|
||||
|
||||
export interface TabItem {
|
||||
id: ManagerTab
|
||||
label: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
export type TaskLog = {
|
||||
taskName: string
|
||||
taskId: string
|
||||
@@ -37,11 +31,6 @@ export interface UseNodePacksOptions {
|
||||
maxConcurrent?: number
|
||||
}
|
||||
|
||||
export interface SearchOption<T> {
|
||||
id: T
|
||||
label: string
|
||||
}
|
||||
|
||||
export enum SortableAlgoliaField {
|
||||
Downloads = 'total_install',
|
||||
Created = 'create_time',
|
||||
|
||||
Reference in New Issue
Block a user