mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-30 21:09:53 +00:00
Add node pack actions: install, uninstall, enable, disable, change version (#3016)
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
@@ -1,24 +0,0 @@
|
||||
<template>
|
||||
<Button
|
||||
outlined
|
||||
class="m-0 p-0 rounded-lg border-neutral-700"
|
||||
severity="secondary"
|
||||
:class="{
|
||||
'w-full': fullWidth,
|
||||
'w-min-content': !fullWidth
|
||||
}"
|
||||
>
|
||||
<span class="py-2.5 px-3">
|
||||
{{ multi ? $t('manager.installSelected') : $t('g.install') }}
|
||||
</span>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
defineProps<{
|
||||
fullWidth?: boolean
|
||||
multi?: boolean
|
||||
}>()
|
||||
</script>
|
||||
@@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<Button
|
||||
v-if="displayVersion"
|
||||
:label="displayVersion"
|
||||
:label="installedVersion"
|
||||
severity="secondary"
|
||||
icon="pi pi-chevron-right"
|
||||
icon-pos="right"
|
||||
@@ -22,44 +21,40 @@
|
||||
}"
|
||||
>
|
||||
<PackVersionSelectorPopover
|
||||
:selected-version="selectedVersion"
|
||||
:installed-version="installedVersion"
|
||||
:node-pack="nodePack"
|
||||
@select="onSelect"
|
||||
@cancel="closeVersionSelector"
|
||||
@apply="applyVersionSelection"
|
||||
@submit="closeVersionSelector"
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { whenever } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import Popover from 'primevue/popover'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import PackVersionSelectorPopover from '@/components/dialog/content/manager/PackVersionSelectorPopover.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { SelectedVersion } from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const { nodePack, version = SelectedVersion.NIGHTLY } = defineProps<{
|
||||
const { nodePack } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
version?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:version': [version: string]
|
||||
}>()
|
||||
|
||||
const popoverRef = ref()
|
||||
const selectedVersion = ref<string>(version)
|
||||
|
||||
const displayVersion = computed(() => {
|
||||
if (selectedVersion.value === SelectedVersion.LATEST) {
|
||||
// If there is no version, treat as unclaimed GitHub pack and use nightly
|
||||
return nodePack?.latest_version?.version || SelectedVersion.NIGHTLY
|
||||
}
|
||||
return selectedVersion.value
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const installedVersion = computed(() => {
|
||||
if (!nodePack.id) return SelectedVersion.NIGHTLY
|
||||
return (
|
||||
managerStore.installedPacks[nodePack.id]?.ver ??
|
||||
nodePack.latest_version?.version ??
|
||||
SelectedVersion.NIGHTLY
|
||||
)
|
||||
})
|
||||
|
||||
const toggleVersionSelector = (event: Event) => {
|
||||
@@ -69,17 +64,4 @@ const toggleVersionSelector = (event: Event) => {
|
||||
const closeVersionSelector = () => {
|
||||
popoverRef.value.hide()
|
||||
}
|
||||
|
||||
const onSelect = (newVersion: string) => {
|
||||
selectedVersion.value = newVersion
|
||||
}
|
||||
|
||||
const applyVersionSelection = (newVersion: string) => {
|
||||
selectedVersion.value = newVersion
|
||||
emit('update:version', newVersion)
|
||||
// TODO: after manager store added, install the pack here
|
||||
closeVersionSelector()
|
||||
}
|
||||
|
||||
whenever(() => version, onSelect)
|
||||
</script>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{{ $t('manager.selectVersion') }}
|
||||
</span>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
v-if="isLoadingVersions || isQueueing"
|
||||
class="text-center text-muted py-4 flex flex-col items-center"
|
||||
>
|
||||
<ProgressSpinner class="w-8 h-8 mb-2" />
|
||||
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
<Listbox
|
||||
v-else
|
||||
v-model="currentSelection"
|
||||
v-model="selectedVersion"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
:options="allVersionOptions"
|
||||
@@ -31,9 +31,9 @@
|
||||
<div class="flex justify-between items-center w-full p-1">
|
||||
<span>{{ slotProps.option.label }}</span>
|
||||
<i
|
||||
v-if="currentSelection === slotProps.option.value"
|
||||
v-if="selectedVersion === slotProps.option.value"
|
||||
class="pi pi-check text-highlight"
|
||||
></i>
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Listbox>
|
||||
@@ -43,46 +43,61 @@
|
||||
text
|
||||
severity="secondary"
|
||||
:label="$t('g.cancel')"
|
||||
:disabled="isQueueing"
|
||||
@click="emit('cancel')"
|
||||
/>
|
||||
<Button
|
||||
severity="secondary"
|
||||
:label="$t('g.install')"
|
||||
@click="emit('apply', currentSelection ?? SelectedVersion.LATEST)"
|
||||
class="py-3 px-4 dark-theme:bg-unset bg-black/80 dark-theme:text-unset text-neutral-100 rounded-lg"
|
||||
:disabled="isQueueing"
|
||||
@click="handleSubmit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAsyncState } from '@vueuse/core'
|
||||
import { useAsyncState, whenever } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import Listbox from 'primevue/listbox'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import { useComfyRegistryService } from '@/services/comfyRegistryService'
|
||||
import { SelectedVersion } from '@/types/comfyManagerTypes'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import {
|
||||
ManagerChannel,
|
||||
ManagerDatabaseSource,
|
||||
SelectedVersion
|
||||
} from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const { nodePack, selectedVersion = SelectedVersion.NIGHTLY } = defineProps<{
|
||||
const { nodePack } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
selectedVersion?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
cancel: []
|
||||
apply: [version: string]
|
||||
submit: []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const registryService = useComfyRegistryService()
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const currentSelection = ref<string>(selectedVersion)
|
||||
const isQueueing = ref(false)
|
||||
|
||||
const selectedVersion = ref<string>(SelectedVersion.LATEST)
|
||||
onMounted(() => {
|
||||
selectedVersion.value =
|
||||
nodePack.publisher?.name === 'Unclaimed'
|
||||
? SelectedVersion.NIGHTLY
|
||||
: nodePack.latest_version?.version ?? SelectedVersion.NIGHTLY
|
||||
})
|
||||
|
||||
const fetchVersions = async () => {
|
||||
if (!nodePack?.id) return []
|
||||
@@ -90,21 +105,29 @@ const fetchVersions = async () => {
|
||||
}
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
isLoading: isLoadingVersions,
|
||||
state: versions,
|
||||
execute: startFetchVersions
|
||||
} = useAsyncState(fetchVersions, [])
|
||||
|
||||
const specialOptions = computed(() => [
|
||||
{
|
||||
value: SelectedVersion.NIGHTLY,
|
||||
label: t('manager.nightlyVersion')
|
||||
},
|
||||
{
|
||||
value: SelectedVersion.LATEST,
|
||||
label: t('manager.latestVersion')
|
||||
const specialOptions = computed(() => {
|
||||
const options = [
|
||||
{
|
||||
value: SelectedVersion.LATEST,
|
||||
label: t('manager.latestVersion')
|
||||
}
|
||||
]
|
||||
|
||||
// Only include nightly option if there is a repo
|
||||
if (nodePack.repository?.length) {
|
||||
options.push({
|
||||
value: SelectedVersion.NIGHTLY,
|
||||
label: t('manager.nightlyVersion')
|
||||
})
|
||||
}
|
||||
])
|
||||
|
||||
return options
|
||||
})
|
||||
|
||||
const versionOptions = computed(() =>
|
||||
versions.value.map((version) => ({
|
||||
@@ -118,9 +141,28 @@ const allVersionOptions = computed(() => [
|
||||
...versionOptions.value
|
||||
])
|
||||
|
||||
watch(
|
||||
() => nodePack,
|
||||
whenever(
|
||||
() => nodePack.id,
|
||||
() => startFetchVersions(),
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const handleSubmit = async () => {
|
||||
isQueueing.value = true
|
||||
await managerStore.installPack.call({
|
||||
id: nodePack.id,
|
||||
repository: nodePack.repository ?? '',
|
||||
channel: ManagerChannel.DEFAULT,
|
||||
mode: ManagerDatabaseSource.CACHE,
|
||||
version: selectedVersion.value,
|
||||
selected_version: selectedVersion.value
|
||||
})
|
||||
|
||||
isQueueing.value = false
|
||||
emit('submit')
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
managerStore.installPack.clear()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -2,8 +2,7 @@ import { VueWrapper, mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import Button from 'primevue/button'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Popover from 'primevue/popover'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
@@ -21,7 +20,36 @@ const mockNodePack = {
|
||||
}
|
||||
}
|
||||
|
||||
const mockInstalledPacks = {
|
||||
'test-pack': { ver: '1.5.0' },
|
||||
'installed-pack': { ver: '2.0.0' }
|
||||
}
|
||||
|
||||
vi.mock('@/stores/comfyManagerStore', () => ({
|
||||
useComfyManagerStore: vi.fn(() => ({
|
||||
installedPacks: mockInstalledPacks,
|
||||
isPackInstalled: (id: string) =>
|
||||
!!mockInstalledPacks[id as keyof typeof mockInstalledPacks]
|
||||
}))
|
||||
}))
|
||||
|
||||
const mockToggle = vi.fn()
|
||||
const mockHide = vi.fn()
|
||||
const PopoverStub = {
|
||||
name: 'Popover',
|
||||
template: '<div><slot></slot></div>',
|
||||
methods: {
|
||||
toggle: mockToggle,
|
||||
hide: mockHide
|
||||
}
|
||||
}
|
||||
|
||||
describe('PackVersionBadge', () => {
|
||||
beforeEach(() => {
|
||||
mockToggle.mockReset()
|
||||
mockHide.mockReset()
|
||||
})
|
||||
|
||||
const mountComponent = ({
|
||||
props = {}
|
||||
}: Record<string, any> = {}): VueWrapper => {
|
||||
@@ -38,122 +66,100 @@ describe('PackVersionBadge', () => {
|
||||
},
|
||||
global: {
|
||||
plugins: [PrimeVue, createPinia(), i18n],
|
||||
components: {
|
||||
Popover,
|
||||
PackVersionSelectorPopover
|
||||
stubs: {
|
||||
Popover: PopoverStub,
|
||||
PackVersionSelectorPopover: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
it('renders with default version (NIGHTLY)', () => {
|
||||
it('renders with installed version from store', () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
const button = wrapper.findComponent(Button)
|
||||
expect(button.exists()).toBe(true)
|
||||
expect(button.props('label')).toBe('1.5.0') // From mockInstalledPacks
|
||||
})
|
||||
|
||||
it('falls back to latest_version when not installed', () => {
|
||||
// Use a nodePack that's not in the installedPacks
|
||||
const uninstalledPack = {
|
||||
id: 'uninstalled-pack',
|
||||
name: 'Uninstalled Pack',
|
||||
latest_version: {
|
||||
version: '3.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
props: { nodePack: uninstalledPack }
|
||||
})
|
||||
|
||||
const button = wrapper.findComponent(Button)
|
||||
expect(button.exists()).toBe(true)
|
||||
expect(button.props('label')).toBe('3.0.0') // From latest_version
|
||||
})
|
||||
|
||||
it('falls back to NIGHTLY when no latest_version and not installed', () => {
|
||||
// Use a nodePack with no latest_version and not in installedPacks
|
||||
const noVersionPack = {
|
||||
id: 'no-version-pack',
|
||||
name: 'No Version Pack'
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
props: { nodePack: noVersionPack }
|
||||
})
|
||||
|
||||
const button = wrapper.findComponent(Button)
|
||||
expect(button.exists()).toBe(true)
|
||||
expect(button.props('label')).toBe(SelectedVersion.NIGHTLY)
|
||||
})
|
||||
|
||||
it('renders with provided version', () => {
|
||||
const version = '2.0.0'
|
||||
const wrapper = mountComponent({ props: { version } })
|
||||
it('falls back to NIGHTLY when nodePack.id is missing', () => {
|
||||
const invalidPack = {
|
||||
name: 'Invalid Pack'
|
||||
}
|
||||
|
||||
const button = wrapper.findComponent(Button)
|
||||
expect(button.exists()).toBe(true)
|
||||
expect(button.props('label')).toBe(version)
|
||||
})
|
||||
|
||||
it('shows actual latest (semantic) version prop when version is set to latest', () => {
|
||||
const wrapper = mountComponent({
|
||||
props: { version: SelectedVersion.LATEST }
|
||||
props: { nodePack: invalidPack }
|
||||
})
|
||||
|
||||
const button = wrapper.findComponent(Button)
|
||||
expect(button.exists()).toBe(true)
|
||||
expect(button.props('label')).toBe(mockNodePack.latest_version.version)
|
||||
expect(button.props('label')).toBe(SelectedVersion.NIGHTLY)
|
||||
})
|
||||
|
||||
it('toggles the popover when button is clicked', async () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
// Spy on the toggle method
|
||||
const popoverToggleSpy = vi.fn()
|
||||
const popover = wrapper.findComponent(Popover)
|
||||
popover.vm.toggle = popoverToggleSpy
|
||||
|
||||
// Open the popover
|
||||
// Click the button
|
||||
await wrapper.findComponent(Button).trigger('click')
|
||||
|
||||
// Verify that the toggle method was called
|
||||
expect(popoverToggleSpy).toHaveBeenCalled()
|
||||
expect(mockToggle).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('emits update:version event when version is selected', async () => {
|
||||
it('closes the popover when cancel is emitted', async () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
// Open the popover
|
||||
await wrapper.findComponent(Button).trigger('click')
|
||||
|
||||
// Simulate the popover emitting an apply event
|
||||
wrapper.findComponent(PackVersionSelectorPopover).vm.$emit('apply', '3.0.0')
|
||||
await nextTick()
|
||||
|
||||
// Check if the update:version event was emitted with the correct value
|
||||
expect(wrapper.emitted('update:version')).toBeTruthy()
|
||||
expect(wrapper.emitted('update:version')![0]).toEqual(['3.0.0'])
|
||||
})
|
||||
|
||||
it('closes the popover when cancel is clicked', async () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
// Open the popover
|
||||
await wrapper.findComponent(Button).trigger('click')
|
||||
|
||||
// Simulate the popover emitting a cancel event
|
||||
wrapper.findComponent(PackVersionSelectorPopover).vm.$emit('cancel')
|
||||
await nextTick()
|
||||
|
||||
// Check if the popover is hidden
|
||||
expect(wrapper.findComponent(Popover).isVisible()).toBe(false)
|
||||
// Verify that the hide method was called
|
||||
expect(mockHide).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('updates displayed version when version prop changes', async () => {
|
||||
const wrapper = mountComponent({ props: { version: '1.0.0' } })
|
||||
it('closes the popover when submit is emitted', async () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
expect(wrapper.findComponent(Button).props('label')).toBe('1.0.0')
|
||||
// Simulate the popover emitting a submit event
|
||||
wrapper.findComponent(PackVersionSelectorPopover).vm.$emit('submit')
|
||||
await nextTick()
|
||||
|
||||
// Update the version prop
|
||||
await wrapper.setProps({ version: '2.0.0' })
|
||||
|
||||
// Check if the displayed version was updated
|
||||
expect(wrapper.findComponent(Button).props('label')).toBe('2.0.0')
|
||||
})
|
||||
|
||||
it('handles null or undefined nodePack', async () => {
|
||||
const wrapper = mountComponent({ props: { nodePack: null } })
|
||||
|
||||
const button = wrapper.findComponent(Button)
|
||||
expect(button.exists()).toBe(true)
|
||||
expect(button.props('label')).toBe(SelectedVersion.NIGHTLY)
|
||||
|
||||
// Should not crash when clicking the button
|
||||
await button.trigger('click')
|
||||
expect(wrapper.findComponent(Popover).isVisible()).toBe(false)
|
||||
})
|
||||
|
||||
it('handles missing latest_version (unclaimed pack) by falling back to NIGHTLY', async () => {
|
||||
const incompleteNodePack = { id: 'test-pack', name: 'Test Pack' }
|
||||
const wrapper = mountComponent({
|
||||
props: {
|
||||
nodePack: incompleteNodePack,
|
||||
version: SelectedVersion.LATEST
|
||||
}
|
||||
})
|
||||
|
||||
const button = wrapper.findComponent(Button)
|
||||
expect(button.exists()).toBe(true)
|
||||
|
||||
// Should fallback to nightly string when latest_version is missing
|
||||
expect(button.props('label')).toBe(SelectedVersion.NIGHTLY)
|
||||
// Verify that the hide method was called
|
||||
expect(mockHide).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,7 +12,8 @@ import { SelectedVersion } from '@/types/comfyManagerTypes'
|
||||
|
||||
import PackVersionSelectorPopover from '../PackVersionSelectorPopover.vue'
|
||||
|
||||
const mockVersions = [
|
||||
// Default mock versions for reference
|
||||
const defaultMockVersions = [
|
||||
{ version: '1.0.0', createdAt: '2023-01-01' },
|
||||
{ version: '0.9.0', createdAt: '2022-12-01' },
|
||||
{ version: '0.8.0', createdAt: '2022-11-01' }
|
||||
@@ -21,17 +22,31 @@ const mockVersions = [
|
||||
const mockNodePack = {
|
||||
id: 'test-pack',
|
||||
name: 'Test Pack',
|
||||
latest_version: { version: '1.0.0' }
|
||||
latest_version: { version: '1.0.0' },
|
||||
repository: 'https://github.com/user/repo'
|
||||
}
|
||||
|
||||
const mockGetPackVersions = vi.fn().mockResolvedValue(mockVersions)
|
||||
// Create mock functions
|
||||
const mockGetPackVersions = vi.fn()
|
||||
const mockInstallPack = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
// Mock the registry service
|
||||
vi.mock('@/services/comfyRegistryService', () => ({
|
||||
useComfyRegistryService: vi.fn(() => ({
|
||||
getPackVersions: mockGetPackVersions
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock the manager store
|
||||
vi.mock('@/stores/comfyManagerStore', () => ({
|
||||
useComfyManagerStore: vi.fn(() => ({
|
||||
installPack: {
|
||||
call: mockInstallPack,
|
||||
clear: vi.fn()
|
||||
}
|
||||
}))
|
||||
}))
|
||||
|
||||
const waitForPromises = async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 16))
|
||||
await nextTick()
|
||||
@@ -40,8 +55,8 @@ const waitForPromises = async () => {
|
||||
describe('PackVersionSelectorPopover', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockGetPackVersions.mockClear()
|
||||
mockGetPackVersions.mockResolvedValue(mockVersions)
|
||||
mockGetPackVersions.mockReset()
|
||||
mockInstallPack.mockReset().mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
const mountComponent = ({
|
||||
@@ -56,7 +71,6 @@ describe('PackVersionSelectorPopover', () => {
|
||||
return mount(PackVersionSelectorPopover, {
|
||||
props: {
|
||||
nodePack: mockNodePack,
|
||||
selectedVersion: SelectedVersion.NIGHTLY,
|
||||
...props
|
||||
},
|
||||
global: {
|
||||
@@ -69,6 +83,9 @@ describe('PackVersionSelectorPopover', () => {
|
||||
}
|
||||
|
||||
it('fetches versions on mount', async () => {
|
||||
// Set up the mock for this specific test
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
mountComponent()
|
||||
await waitForPromises()
|
||||
|
||||
@@ -79,7 +96,9 @@ describe('PackVersionSelectorPopover', () => {
|
||||
// Delay the promise resolution
|
||||
mockGetPackVersions.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => setTimeout(() => resolve(mockVersions), 1000))
|
||||
new Promise((resolve) =>
|
||||
setTimeout(() => resolve(defaultMockVersions), 1000)
|
||||
)
|
||||
)
|
||||
|
||||
const wrapper = mountComponent()
|
||||
@@ -88,6 +107,9 @@ describe('PackVersionSelectorPopover', () => {
|
||||
})
|
||||
|
||||
it('displays special options and version options in the listbox', async () => {
|
||||
// Set up the mock for this specific test
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
const wrapper = mountComponent()
|
||||
await waitForPromises()
|
||||
|
||||
@@ -95,29 +117,23 @@ describe('PackVersionSelectorPopover', () => {
|
||||
expect(listbox.exists()).toBe(true)
|
||||
|
||||
const options = listbox.props('options')!
|
||||
expect(options.length).toBe(5) // 2 special options + 3 version options
|
||||
// Check that we have both special options and version options
|
||||
expect(options.length).toBe(defaultMockVersions.length + 2) // 2 special options + version options
|
||||
|
||||
// Check special options
|
||||
expect(options[0].value).toBe(SelectedVersion.NIGHTLY)
|
||||
expect(options[1].value).toBe(SelectedVersion.LATEST)
|
||||
// Check that special options exist
|
||||
expect(options.some((o) => o.value === SelectedVersion.NIGHTLY)).toBe(true)
|
||||
expect(options.some((o) => o.value === SelectedVersion.LATEST)).toBe(true)
|
||||
|
||||
// Check version options
|
||||
expect(options[2].value).toBe('1.0.0')
|
||||
expect(options[3].value).toBe('0.9.0')
|
||||
expect(options[4].value).toBe('0.8.0')
|
||||
})
|
||||
|
||||
it('initializes with the provided selectedVersion prop', async () => {
|
||||
const selectedVersion = '0.9.0'
|
||||
const wrapper = mountComponent({ props: { selectedVersion } })
|
||||
await waitForPromises()
|
||||
|
||||
// Check that the listbox has the correct model value
|
||||
const listbox = wrapper.findComponent(Listbox)
|
||||
expect(listbox.props('modelValue')).toBe(selectedVersion)
|
||||
// Check that version options exist
|
||||
expect(options.some((o) => o.value === '1.0.0')).toBe(true)
|
||||
expect(options.some((o) => o.value === '0.9.0')).toBe(true)
|
||||
expect(options.some((o) => o.value === '0.8.0')).toBe(true)
|
||||
})
|
||||
|
||||
it('emits cancel event when cancel button is clicked', async () => {
|
||||
// Set up the mock for this specific test
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
const wrapper = mountComponent()
|
||||
await waitForPromises()
|
||||
|
||||
@@ -127,35 +143,42 @@ describe('PackVersionSelectorPopover', () => {
|
||||
expect(wrapper.emitted('cancel')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('emits apply event with current selection when apply button is clicked', async () => {
|
||||
const selectedVersion = '0.9.0'
|
||||
const wrapper = mountComponent({ props: { selectedVersion } })
|
||||
await waitForPromises()
|
||||
it('calls installPack and emits submit when install button is clicked', async () => {
|
||||
// Set up the mock for this specific test
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
const applyButton = wrapper.findAllComponents(Button)[1]
|
||||
await applyButton.trigger('click')
|
||||
|
||||
expect(wrapper.emitted('apply')).toBeTruthy()
|
||||
expect(wrapper.emitted('apply')![0]).toEqual([selectedVersion])
|
||||
})
|
||||
|
||||
it('emits apply event with LATEST when no selection and apply button is clicked', async () => {
|
||||
const wrapper = mountComponent({ props: { selectedVersion: null } })
|
||||
await waitForPromises()
|
||||
|
||||
const applyButton = wrapper.findAllComponents(Button)[1]
|
||||
await applyButton.trigger('click')
|
||||
|
||||
expect(wrapper.emitted('apply')).toBeTruthy()
|
||||
expect(wrapper.emitted('apply')![0]).toEqual([SelectedVersion.LATEST])
|
||||
})
|
||||
|
||||
it('is reactive to nodePack prop changes', async () => {
|
||||
const wrapper = mountComponent()
|
||||
await waitForPromises()
|
||||
|
||||
// Clear mock calls to check if getPackVersions is called again
|
||||
mockGetPackVersions.mockClear()
|
||||
// Set the selected version
|
||||
await wrapper.findComponent(Listbox).setValue('0.9.0')
|
||||
|
||||
const installButton = wrapper.findAllComponents(Button)[1]
|
||||
await installButton.trigger('click')
|
||||
|
||||
// Check that installPack was called with the correct parameters
|
||||
expect(mockInstallPack).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: mockNodePack.id,
|
||||
repository: mockNodePack.repository,
|
||||
version: '0.9.0',
|
||||
selected_version: '0.9.0'
|
||||
})
|
||||
)
|
||||
|
||||
// Check that submit was emitted
|
||||
expect(wrapper.emitted('submit')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('is reactive to nodePack prop changes', async () => {
|
||||
// Set up the mock for the initial fetch
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
const wrapper = mountComponent()
|
||||
await waitForPromises()
|
||||
|
||||
// Set up the mock for the second fetch after prop change
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
// Update the nodePack prop
|
||||
const newNodePack = { ...mockNodePack, id: 'new-test-pack' }
|
||||
@@ -167,23 +190,45 @@ describe('PackVersionSelectorPopover', () => {
|
||||
})
|
||||
|
||||
describe('Unclaimed GitHub packs handling', () => {
|
||||
it('falls back to nightly when comfy-api returns null when fetching versions', async () => {
|
||||
mockGetPackVersions.mockResolvedValueOnce(null)
|
||||
it('falls back to nightly when no versions exist', async () => {
|
||||
// Set up the mock to return versions
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
const packWithRepo = {
|
||||
...mockNodePack,
|
||||
latest_version: undefined
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
props: {
|
||||
nodePack: packWithRepo
|
||||
}
|
||||
})
|
||||
|
||||
const wrapper = mountComponent()
|
||||
await waitForPromises()
|
||||
|
||||
const listbox = wrapper.findComponent(Listbox)
|
||||
expect(listbox.exists()).toBe(true)
|
||||
expect(listbox.props('modelValue')).toBe(SelectedVersion.NIGHTLY)
|
||||
})
|
||||
|
||||
it('falls back to nightly when component mounts with no versions (unclaimed pack)', async () => {
|
||||
mockGetPackVersions.mockResolvedValueOnce([])
|
||||
it('defaults to nightly when publisher name is "Unclaimed"', async () => {
|
||||
// Set up the mock to return versions
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
const unclaimedNodePack = {
|
||||
...mockNodePack,
|
||||
publisher: { name: 'Unclaimed' }
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
props: {
|
||||
nodePack: unclaimedNodePack
|
||||
}
|
||||
})
|
||||
|
||||
const wrapper = mountComponent()
|
||||
await waitForPromises()
|
||||
|
||||
const listbox = wrapper.findComponent(Listbox)
|
||||
expect(listbox.exists()).toBe(true)
|
||||
expect(listbox.props('modelValue')).toBe(SelectedVersion.NIGHTLY)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<Button
|
||||
outlined
|
||||
class="m-0 p-0 rounded-lg border-neutral-700"
|
||||
:class="{
|
||||
'w-full': fullWidth,
|
||||
'w-min-content': !fullWidth
|
||||
}"
|
||||
:disabled="isExecuted"
|
||||
v-bind="$attrs"
|
||||
@click="onClick"
|
||||
>
|
||||
<span class="py-2.5 px-3">
|
||||
<template v-if="isExecuted">
|
||||
{{ loadingMessage ?? $t('g.loading') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ label }}
|
||||
</template>
|
||||
</span>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const {
|
||||
label,
|
||||
loadingMessage,
|
||||
fullWidth = false
|
||||
} = defineProps<{
|
||||
label: string
|
||||
loadingMessage?: string
|
||||
fullWidth?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
action: []
|
||||
}>()
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const isExecuted = ref(false)
|
||||
|
||||
const onClick = (): void => {
|
||||
isExecuted.value = true
|
||||
emit('action')
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<ToggleSwitch
|
||||
:model-value="isEnabled"
|
||||
:disabled="isLoading"
|
||||
aria-label="Enable or disable pack"
|
||||
@update:model-value="onToggle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { debounce } from 'lodash'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import {
|
||||
InstallPackParams,
|
||||
ManagerChannel,
|
||||
SelectedVersion
|
||||
} from '@/types/comfyManagerTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const TOGGLE_DEBOUNCE_MS = 300
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
}>()
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
const isLoading = ref(false)
|
||||
|
||||
const isEnabled = computed(() => managerStore.isPackEnabled(nodePack.id))
|
||||
const version = computed(() => {
|
||||
const id = nodePack.id
|
||||
if (!id) return SelectedVersion.NIGHTLY
|
||||
return (
|
||||
managerStore.installedPacks[id]?.ver ??
|
||||
nodePack.latest_version?.version ??
|
||||
SelectedVersion.NIGHTLY
|
||||
)
|
||||
})
|
||||
|
||||
const handleEnable = async () => {
|
||||
if (!nodePack.id) return
|
||||
|
||||
// Enable is done by using the install endpoint with a disabled pack
|
||||
managerStore.installPack.call({
|
||||
id: nodePack.id,
|
||||
version: version.value,
|
||||
selected_version: version.value,
|
||||
repository: nodePack.repository ?? '',
|
||||
channel: ManagerChannel.DEFAULT,
|
||||
mode: 'default' as InstallPackParams['mode']
|
||||
})
|
||||
}
|
||||
|
||||
const handleDisable = async () => {
|
||||
managerStore.disablePack({
|
||||
id: nodePack.id,
|
||||
version: version.value
|
||||
})
|
||||
}
|
||||
|
||||
const handleToggle = async (enable: boolean) => {
|
||||
if (isLoading.value) return
|
||||
|
||||
isLoading.value = true
|
||||
if (enable) {
|
||||
await handleEnable()
|
||||
} else {
|
||||
await handleDisable()
|
||||
}
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const onToggle = debounce(handleToggle, TOGGLE_DEBOUNCE_MS)
|
||||
</script>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<PackActionButton
|
||||
v-bind="$attrs"
|
||||
:label="
|
||||
nodePacks.length > 1 ? $t('manager.installSelected') : $t('g.install')
|
||||
"
|
||||
severity="secondary"
|
||||
:loading-message="$t('g.installing')"
|
||||
@action="installAllPacks"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PackActionButton from '@/components/dialog/content/manager/button/PackActionButton.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import {
|
||||
ManagerChannel,
|
||||
ManagerDatabaseSource,
|
||||
SelectedVersion
|
||||
} from '@/types/comfyManagerTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
const { nodePacks } = defineProps<{
|
||||
nodePacks: NodePack[]
|
||||
}>()
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const createPayload = (installItem: NodePack) => {
|
||||
const isUnclaimedPack = installItem.publisher?.name === 'Unclaimed'
|
||||
const versionToInstall = isUnclaimedPack
|
||||
? SelectedVersion.NIGHTLY
|
||||
: installItem.latest_version?.version ?? SelectedVersion.LATEST
|
||||
|
||||
return {
|
||||
id: installItem.id,
|
||||
repository: installItem.repository ?? '',
|
||||
channel: ManagerChannel.DEV,
|
||||
mode: ManagerDatabaseSource.CACHE,
|
||||
selected_version: versionToInstall,
|
||||
version: versionToInstall
|
||||
}
|
||||
}
|
||||
|
||||
const installPack = (item: NodePack) =>
|
||||
managerStore.installPack.call(createPayload(item))
|
||||
|
||||
const installAllPacks = async () => {
|
||||
if (!nodePacks?.length) return
|
||||
|
||||
const uninstalledPacks = nodePacks.filter(
|
||||
(pack) => !managerStore.isPackInstalled(pack.id)
|
||||
)
|
||||
if (!uninstalledPacks.length) return
|
||||
|
||||
await Promise.all(uninstalledPacks.map(installPack))
|
||||
managerStore.installPack.clear()
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<PackActionButton
|
||||
v-bind="$attrs"
|
||||
:label="
|
||||
nodePacks.length > 1
|
||||
? $t('manager.uninstallSelected')
|
||||
: $t('manager.uninstall')
|
||||
"
|
||||
severity="danger"
|
||||
:loading-message="$t('manager.uninstalling')"
|
||||
@action="uninstallItems"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PackActionButton from '@/components/dialog/content/manager/button/PackActionButton.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { ManagerPackInfo } from '@/types/comfyManagerTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
const { nodePacks } = defineProps<{
|
||||
nodePacks: NodePack[]
|
||||
}>()
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const createPayload = (uninstallItem: NodePack): ManagerPackInfo => {
|
||||
return {
|
||||
id: uninstallItem.id,
|
||||
version: uninstallItem.latest_version?.version
|
||||
}
|
||||
}
|
||||
|
||||
const uninstallPack = (item: NodePack) =>
|
||||
managerStore.uninstallPack(createPayload(item))
|
||||
|
||||
const uninstallItems = async () => {
|
||||
if (!nodePacks?.length) return
|
||||
await Promise.all(nodePacks.map(uninstallPack))
|
||||
}
|
||||
</script>
|
||||
@@ -1,10 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full z-40 hidden-scrollbar w-80">
|
||||
<div class="p-6 flex-1 overflow-hidden text-sm">
|
||||
<PackCardHeader
|
||||
:node-pack="nodePack"
|
||||
:install-button-full-width="false"
|
||||
/>
|
||||
<InfoPanelHeader :node-packs="[nodePack]" />
|
||||
<div class="mb-6">
|
||||
<MetadataRow
|
||||
v-for="item in infoItems"
|
||||
@@ -21,11 +18,7 @@
|
||||
/>
|
||||
</MetadataRow>
|
||||
<MetadataRow :label="t('manager.version')">
|
||||
<PackVersionBadge
|
||||
:node-pack="nodePack"
|
||||
:version="selectedVersion"
|
||||
@update:version="updateSelectedVersion"
|
||||
/>
|
||||
<PackVersionBadge :node-pack="nodePack" />
|
||||
</MetadataRow>
|
||||
</div>
|
||||
<div class="mb-6 overflow-hidden">
|
||||
@@ -36,17 +29,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import PackStatusMessage from '@/components/dialog/content/manager/PackStatusMessage.vue'
|
||||
import PackVersionBadge from '@/components/dialog/content/manager/PackVersionBadge.vue'
|
||||
import InfoPanelHeader from '@/components/dialog/content/manager/infoPanel/InfoPanelHeader.vue'
|
||||
import InfoTabs from '@/components/dialog/content/manager/infoPanel/InfoTabs.vue'
|
||||
import MetadataRow from '@/components/dialog/content/manager/infoPanel/MetadataRow.vue'
|
||||
import PackCardHeader from '@/components/dialog/content/manager/packCard/PackCardHeader.vue'
|
||||
import { SelectedVersion } from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import { formatNumber } from '@/utils/formatUtil'
|
||||
|
||||
interface InfoItem {
|
||||
key: string
|
||||
@@ -58,31 +49,18 @@ const { nodePack } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
}>()
|
||||
|
||||
const { t, d } = useI18n()
|
||||
|
||||
const packCardHeaderRef = ref(null)
|
||||
|
||||
const selectedVersion = ref<string>(
|
||||
nodePack.latest_version?.version || SelectedVersion.NIGHTLY
|
||||
)
|
||||
const updateSelectedVersion = (version: string) => {
|
||||
selectedVersion.value = version
|
||||
if (packCardHeaderRef.value) {
|
||||
packCardHeaderRef.value.updateVersion?.(version)
|
||||
}
|
||||
}
|
||||
const { t, d, n } = useI18n()
|
||||
|
||||
const infoItems = computed<InfoItem[]>(() => [
|
||||
{
|
||||
key: 'publisher',
|
||||
label: t('manager.createdBy'),
|
||||
// TODO: handle all Comfy Registry publisher types dynamically (e.g., organizations, multiple authors)
|
||||
value: nodePack.publisher?.name
|
||||
value: nodePack.publisher?.name ?? nodePack.publisher?.id
|
||||
},
|
||||
{
|
||||
key: 'downloads',
|
||||
label: t('manager.downloads'),
|
||||
value: nodePack.downloads ? formatNumber(nodePack.downloads) : undefined
|
||||
value: nodePack.downloads ? n(nodePack.downloads) : undefined
|
||||
},
|
||||
{
|
||||
key: 'lastUpdated',
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div v-if="nodePacks?.length" class="flex flex-col items-center mb-6">
|
||||
<slot name="thumbnail">
|
||||
<PackIcon :node-pack="nodePacks[0]" width="24" height="24" />
|
||||
</slot>
|
||||
<h2
|
||||
class="text-2xl font-bold text-center mt-4 mb-2"
|
||||
style="word-break: break-all"
|
||||
>
|
||||
<slot name="title">
|
||||
{{ nodePacks[0].name }}
|
||||
</slot>
|
||||
</h2>
|
||||
<div class="mt-2 mb-4 w-full max-w-xs flex justify-center">
|
||||
<slot name="install-button">
|
||||
<PackUninstallButton
|
||||
v-if="isAllInstalled"
|
||||
v-bind="$attrs"
|
||||
:node-packs="nodePacks"
|
||||
/>
|
||||
<PackInstallButton v-else v-bind="$attrs" :node-packs="nodePacks" />
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex flex-col items-center mb-6">
|
||||
<NoResultsPlaceholder
|
||||
:message="$t('manager.status.unknown')"
|
||||
:title="$t('manager.tryAgainLater')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import PackUninstallButton from '@/components/dialog/content/manager/button/PackUninstallButton.vue'
|
||||
import PackIcon from '@/components/dialog/content/manager/packIcon/PackIcon.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const { nodePacks } = defineProps<{
|
||||
nodePacks: components['schemas']['Node'][]
|
||||
}>()
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const isAllInstalled = computed(() =>
|
||||
nodePacks.every((nodePack) => managerStore.isPackInstalled(nodePack.id))
|
||||
)
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="p-6 flex-1 overflow-auto">
|
||||
<PackCardHeader>
|
||||
<InfoPanelHeader :node-packs="nodePacks">
|
||||
<template #thumbnail>
|
||||
<PackIconStacked :node-packs="nodePacks" />
|
||||
</template>
|
||||
@@ -10,9 +10,9 @@
|
||||
{{ $t('manager.packsSelected') }}
|
||||
</template>
|
||||
<template #install-button>
|
||||
<PackInstallButton :full-width="true" :multi="true" />
|
||||
<PackInstallButton :full-width="true" :node-packs="nodePacks" />
|
||||
</template>
|
||||
</PackCardHeader>
|
||||
</InfoPanelHeader>
|
||||
<div class="mb-6">
|
||||
<MetadataRow :label="$t('g.status')">
|
||||
<PackStatusMessage status-type="NodeVersionStatusActive" />
|
||||
@@ -30,10 +30,10 @@
|
||||
import { useAsyncState } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import PackInstallButton from '@/components/dialog/content/manager/PackInstallButton.vue'
|
||||
import PackStatusMessage from '@/components/dialog/content/manager/PackStatusMessage.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import InfoPanelHeader from '@/components/dialog/content/manager/infoPanel/InfoPanelHeader.vue'
|
||||
import MetadataRow from '@/components/dialog/content/manager/infoPanel/MetadataRow.vue'
|
||||
import PackCardHeader from '@/components/dialog/content/manager/packCard/PackCardHeader.vue'
|
||||
import PackIconStacked from '@/components/dialog/content/manager/packIcon/PackIconStacked.vue'
|
||||
import { useComfyRegistryService } from '@/services/comfyRegistryService'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
@@ -59,6 +59,7 @@ const { state: allNodeDefs } = useAsyncState(
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
const totalNodesCount = computed(() =>
|
||||
allNodeDefs.value.reduce(
|
||||
(total, nodeDefs) => total + (nodeDefs?.length || 0),
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<i
|
||||
class="pi pi-box text-muted text-2xl ml-1 mr-5"
|
||||
style="opacity: 0.5"
|
||||
></i>
|
||||
/>
|
||||
<span class="text-lg relative top-[.25rem]">{{
|
||||
$t('manager.nodePack')
|
||||
}}</span>
|
||||
@@ -27,10 +27,15 @@
|
||||
v-if="nodePack.downloads"
|
||||
class="flex items-center text-sm text-muted tracking-tighter"
|
||||
>
|
||||
<i class="pi pi-download mr-2"></i>
|
||||
{{ formatNumber(nodePack.downloads) }}
|
||||
<i class="pi pi-download mr-2" />
|
||||
{{ $n(nodePack.downloads) }}
|
||||
</div>
|
||||
<PackInstallButton />
|
||||
<template v-if="isPackInstalled">
|
||||
<PackEnableToggle :node-pack="nodePack" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<PackInstallButton :node-packs="[nodePack]" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -66,17 +71,13 @@
|
||||
<span v-if="nodePack.publisher?.name">
|
||||
{{ nodePack.publisher.name }}
|
||||
</span>
|
||||
<PackVersionBadge
|
||||
v-if="isPackInstalled"
|
||||
:node-pack="nodePack"
|
||||
v-model:version="selectedVersion"
|
||||
/>
|
||||
<PackVersionBadge v-if="isPackInstalled" :node-pack="nodePack" />
|
||||
<span v-else-if="nodePack.latest_version">
|
||||
{{ nodePack.latest_version.version }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="nodePack.latest_version"
|
||||
v-if="nodePack.latest_version?.createdAt"
|
||||
class="flex items-center gap-2 truncate"
|
||||
>
|
||||
{{ $t('g.updated') }}
|
||||
@@ -93,24 +94,24 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Card from 'primevue/card'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/PackInstallButton.vue'
|
||||
import PackVersionBadge from '@/components/dialog/content/manager/PackVersionBadge.vue'
|
||||
import PackEnableToggle from '@/components/dialog/content/manager/button/PackEnableToggle.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import PackIcon from '@/components/dialog/content/manager/packIcon/PackIcon.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { formatNumber } from '@/utils/formatUtil'
|
||||
|
||||
const { nodePack, isSelected = false } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
isSelected?: boolean
|
||||
}>()
|
||||
const selectedVersion = ref<string | undefined>(undefined)
|
||||
const isPackInstalled = computed(
|
||||
() =>
|
||||
// TODO: after manager store added
|
||||
// managerStore.isPackInstalled(nodePack?.id)
|
||||
true
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const isPackInstalled = computed(() =>
|
||||
managerStore.isPackInstalled(nodePack?.id)
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center mb-6">
|
||||
<slot name="thumbnail">
|
||||
<PackIcon :node-pack="nodePack" width="24" height="24" />
|
||||
</slot>
|
||||
<h2
|
||||
class="text-2xl font-bold text-center mt-4 mb-2"
|
||||
style="word-break: break-all"
|
||||
>
|
||||
<slot name="title">{{ nodePack?.name }}</slot>
|
||||
</h2>
|
||||
<div class="mt-2 mb-4 w-full max-w-xs flex justify-center">
|
||||
<slot name="install-button">
|
||||
<PackInstallButton
|
||||
:full-width="installButtonFullWidth"
|
||||
:multi="multi"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PackInstallButton from '@/components/dialog/content/manager/PackInstallButton.vue'
|
||||
import PackIcon from '@/components/dialog/content/manager/packIcon/PackIcon.vue'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
defineProps<{
|
||||
nodePack?: components['schemas']['Node']
|
||||
multi?: boolean
|
||||
installButtonFullWidth?: boolean
|
||||
}>()
|
||||
</script>
|
||||
@@ -46,6 +46,7 @@
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"install": "Install",
|
||||
"installing": "Installing",
|
||||
"overwrite": "Overwrite",
|
||||
"customize": "Customize",
|
||||
"experimental": "BETA",
|
||||
@@ -98,6 +99,10 @@
|
||||
"selectVersion": "Select Version",
|
||||
"downloads": "Downloads",
|
||||
"repository": "Repository",
|
||||
"uninstall": "Uninstall",
|
||||
"uninstalling": "Uninstalling",
|
||||
"update": "Update",
|
||||
"uninstallSelected": "Uninstall Selected",
|
||||
"license": "License",
|
||||
"nightlyVersion": "Nightly",
|
||||
"latestVersion": "Latest",
|
||||
|
||||
@@ -158,6 +158,7 @@
|
||||
"import": "Importer",
|
||||
"insert": "Insérer",
|
||||
"install": "Installer",
|
||||
"installing": "Installation",
|
||||
"keybinding": "Raccourci clavier",
|
||||
"loadAllFolders": "Charger tous les dossiers",
|
||||
"loadWorkflow": "Charger le flux de travail",
|
||||
@@ -411,6 +412,10 @@
|
||||
"totalNodes": "Total de Nœuds",
|
||||
"tryAgainLater": "Veuillez réessayer plus tard.",
|
||||
"tryDifferentSearch": "Veuillez essayer une autre requête de recherche.",
|
||||
"uninstall": "Désinstaller",
|
||||
"uninstallSelected": "Désinstaller sélectionné",
|
||||
"uninstalling": "Désinstallation",
|
||||
"update": "Mettre à jour",
|
||||
"version": "Version"
|
||||
},
|
||||
"menu": {
|
||||
|
||||
@@ -158,6 +158,7 @@
|
||||
"import": "インポート",
|
||||
"insert": "挿入",
|
||||
"install": "インストール",
|
||||
"installing": "インストール中",
|
||||
"keybinding": "キーバインディング",
|
||||
"loadAllFolders": "すべてのフォルダーを読み込む",
|
||||
"loadWorkflow": "ワークフローを読み込む",
|
||||
@@ -411,6 +412,10 @@
|
||||
"totalNodes": "合計ノード数",
|
||||
"tryAgainLater": "後ほど再試行してください。",
|
||||
"tryDifferentSearch": "別の検索クエリを試してみてください。",
|
||||
"uninstall": "アンインストール",
|
||||
"uninstallSelected": "選択したものをアンインストール",
|
||||
"uninstalling": "アンインストール中",
|
||||
"update": "更新",
|
||||
"version": "バージョン"
|
||||
},
|
||||
"menu": {
|
||||
|
||||
@@ -158,6 +158,7 @@
|
||||
"import": "가져오기",
|
||||
"insert": "삽입",
|
||||
"install": "설치",
|
||||
"installing": "설치 중",
|
||||
"keybinding": "키 바인딩",
|
||||
"loadAllFolders": "모든 폴더 로드",
|
||||
"loadWorkflow": "워크플로 로드",
|
||||
@@ -411,6 +412,10 @@
|
||||
"totalNodes": "총 노드",
|
||||
"tryAgainLater": "나중에 다시 시도해 주세요.",
|
||||
"tryDifferentSearch": "다른 검색어를 시도해 주세요.",
|
||||
"uninstall": "제거",
|
||||
"uninstallSelected": "선택 항목 제거",
|
||||
"uninstalling": "제거 중",
|
||||
"update": "업데이트",
|
||||
"version": "버전"
|
||||
},
|
||||
"menu": {
|
||||
|
||||
@@ -158,6 +158,7 @@
|
||||
"import": "Импорт",
|
||||
"insert": "Вставить",
|
||||
"install": "Установить",
|
||||
"installing": "Установка",
|
||||
"keybinding": "Привязка клавиш",
|
||||
"loadAllFolders": "Загрузить все папки",
|
||||
"loadWorkflow": "Загрузить рабочий процесс",
|
||||
@@ -411,6 +412,10 @@
|
||||
"totalNodes": "Всего Узлов",
|
||||
"tryAgainLater": "Пожалуйста, попробуйте позже.",
|
||||
"tryDifferentSearch": "Пожалуйста, попробуйте изменить запрос.",
|
||||
"uninstall": "Удалить",
|
||||
"uninstallSelected": "Удалить выбранное",
|
||||
"uninstalling": "Удаление",
|
||||
"update": "Обновить",
|
||||
"version": "Версия"
|
||||
},
|
||||
"menu": {
|
||||
|
||||
@@ -158,6 +158,7 @@
|
||||
"import": "导入",
|
||||
"insert": "插入",
|
||||
"install": "安装",
|
||||
"installing": "正在安装",
|
||||
"keybinding": "按键绑定",
|
||||
"loadAllFolders": "加载所有文件夹",
|
||||
"loadWorkflow": "加载工作流",
|
||||
@@ -411,6 +412,10 @@
|
||||
"totalNodes": "节点总数",
|
||||
"tryAgainLater": "请稍后再试。",
|
||||
"tryDifferentSearch": "请尝试不同的搜索查询。",
|
||||
"uninstall": "卸载",
|
||||
"uninstallSelected": "卸载所选",
|
||||
"uninstalling": "正在卸载",
|
||||
"update": "更新",
|
||||
"version": "版本"
|
||||
},
|
||||
"menu": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { partition } from 'lodash'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { ref, watchEffect } from 'vue'
|
||||
|
||||
import { useCachedRequest } from '@/composables/useCachedRequest'
|
||||
import { useManagerQueue } from '@/composables/useManagerQueue'
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
InstallPackParams,
|
||||
InstalledPacksResponse,
|
||||
ManagerPackInfo,
|
||||
ManagerPackInstalled,
|
||||
UpdateAllPacksParams
|
||||
} from '@/types/comfyManagerTypes'
|
||||
|
||||
@@ -18,20 +20,61 @@ import {
|
||||
export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
const managerService = useComfyManagerService()
|
||||
const installedPacks = ref<InstalledPacksResponse>({})
|
||||
const enabledPacks = ref<Set<string>>(new Set())
|
||||
const disabledPacks = ref<Set<string>>(new Set())
|
||||
const isStale = ref(true)
|
||||
|
||||
const { statusMessage, allTasksDone, enqueueTask } = useManagerQueue()
|
||||
|
||||
const refreshInstalledList = async () => {
|
||||
const packs = await managerService.listInstalledPacks()
|
||||
if (packs) installedPacks.value = packs
|
||||
isStale.value = false
|
||||
}
|
||||
|
||||
const setStale = () => {
|
||||
isStale.value = true
|
||||
}
|
||||
|
||||
const isPackInstalled = (packName: string | undefined): boolean => {
|
||||
if (!packName) return false
|
||||
return !!installedPacks.value[packName] || disabledPacks.value.has(packName)
|
||||
}
|
||||
|
||||
const isPackEnabled = (packName: string | undefined): boolean =>
|
||||
!!packName && enabledPacks.value.has(packName)
|
||||
|
||||
/**
|
||||
* A pack is disabled if there is a disabled entry and no corresponding enabled entry
|
||||
* @example
|
||||
* installedPacks = {
|
||||
* "packname@1_0_2": { enabled: false, cnr_id: "packname" },
|
||||
* "packname": { enabled: true, cnr_id: "packname" }
|
||||
* }
|
||||
* isDisabled("packname") // false
|
||||
*
|
||||
* installedPacks = {
|
||||
* "packname@1_0_2": { enabled: false, cnr_id: "packname" },
|
||||
* }
|
||||
* isDisabled("packname") // true
|
||||
*/
|
||||
const isPackDisabled = (pack: ManagerPackInstalled) =>
|
||||
pack.enabled === false && pack.cnr_id && !installedPacks.value[pack.cnr_id]
|
||||
|
||||
const packsToIdSet = (packs: ManagerPackInstalled[]) =>
|
||||
packs.reduce((acc, pack) => {
|
||||
const id = pack.cnr_id || pack.aux_id
|
||||
if (id) acc.add(id)
|
||||
return acc
|
||||
}, new Set<string>())
|
||||
|
||||
watchEffect(() => {
|
||||
const packs = Object.values(installedPacks.value)
|
||||
const [disabled, enabled] = partition(packs, isPackDisabled)
|
||||
enabledPacks.value = packsToIdSet(enabled)
|
||||
disabledPacks.value = packsToIdSet(disabled)
|
||||
})
|
||||
|
||||
const refreshInstalledList = async () => {
|
||||
isStale.value = false
|
||||
const packs = await managerService.listInstalledPacks()
|
||||
if (packs) installedPacks.value = packs
|
||||
}
|
||||
|
||||
whenever(isStale, refreshInstalledList, { immediate: true })
|
||||
|
||||
const installPack = useCachedRequest<InstallPackParams, void>(
|
||||
@@ -84,16 +127,6 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
})
|
||||
}
|
||||
|
||||
const isPackInstalled = (packName: string | undefined): boolean => {
|
||||
if (!packName) return false
|
||||
return !!installedPacks.value[packName]
|
||||
}
|
||||
|
||||
const isPackEnabled = (packName: string | undefined): boolean => {
|
||||
if (!packName) return false
|
||||
return !!installedPacks.value[packName]?.enabled
|
||||
}
|
||||
|
||||
return {
|
||||
// Manager state
|
||||
isLoading: managerService.isLoading,
|
||||
|
||||
@@ -5,11 +5,6 @@ type RegistryPack = components['schemas']['Node']
|
||||
type WorkflowNodeProperties = ComfyWorkflowJSON['nodes'][0]['properties']
|
||||
export type PackField = keyof RegistryPack | null
|
||||
|
||||
export type PackWithSelectedVersion = {
|
||||
nodePack: RegistryPack
|
||||
selectedVersion?: InstallPackParams['selected_version']
|
||||
}
|
||||
|
||||
export interface TabItem {
|
||||
id: string
|
||||
label: string
|
||||
@@ -175,10 +170,6 @@ export interface InstallPackParams extends ManagerPackInfo {
|
||||
* Semantic version, Git commit hash, `latest`, or `nightly`.
|
||||
*/
|
||||
selected_version: WorkflowNodeProperties['ver'] | SelectedVersion
|
||||
/**
|
||||
* If set to `imported`, returns only the packs that were imported at app startup.
|
||||
*/
|
||||
mode?: 'imported' | 'default'
|
||||
/**
|
||||
* The GitHub link to the repository of the pack to install.
|
||||
* Required if `selected_version` is `nightly`.
|
||||
@@ -189,6 +180,7 @@ export interface InstallPackParams extends ManagerPackInfo {
|
||||
* Used in coordination with pip package whitelist and version lock features.
|
||||
*/
|
||||
pip?: string[]
|
||||
mode: ManagerDatabaseSource
|
||||
channel: ManagerChannel
|
||||
skip_post_install?: boolean
|
||||
}
|
||||
|
||||
@@ -336,14 +336,6 @@ export const generateUUID = (): string => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a number to a locale-specific string
|
||||
* @param num The number to format
|
||||
* @returns The formatted number or 'N/A' if the number is undefined
|
||||
*/
|
||||
export const formatNumber = (num?: number): string =>
|
||||
num?.toLocaleString() ?? 'N/A'
|
||||
|
||||
/**
|
||||
* Checks if a URL is a Civitai model URL
|
||||
* @example
|
||||
|
||||
Reference in New Issue
Block a user