[backport core/1.35] feat(manager): add Try Update button for nightly packs (#7635)

Backport of #7610 to `core/1.35`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7635-backport-core-1-35-feat-manager-add-Try-Update-button-for-nightly-packs-2ce6d73d36508135a070e17689cee5e7)
by [Unito](https://www.unito.io)

Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com>
This commit is contained in:
Comfy Org PR Bot
2025-12-20 09:21:11 +09:00
committed by GitHub
parent a26fc1cd8f
commit 36027a858f
7 changed files with 178 additions and 15 deletions

View File

@@ -294,6 +294,8 @@
"uninstall": "Uninstall", "uninstall": "Uninstall",
"uninstalling": "Uninstalling {id}", "uninstalling": "Uninstalling {id}",
"update": "Update", "update": "Update",
"tryUpdate": "Try Update",
"tryUpdateTooltip": "Pull latest changes from repository. Nightly versions may have updates that cannot be detected automatically.",
"uninstallSelected": "Uninstall Selected", "uninstallSelected": "Uninstall Selected",
"updateSelected": "Update Selected", "updateSelected": "Update Selected",
"updateAll": "Update All", "updateAll": "Update All",

View File

@@ -63,7 +63,7 @@ const {
fill?: boolean fill?: boolean
}>() }>()
const { isUpdateAvailable } = usePackUpdateStatus(nodePack) const { isUpdateAvailable } = usePackUpdateStatus(() => nodePack)
const popoverRef = ref() const popoverRef = ref()
const managerStore = useComfyManagerStore() const managerStore = useComfyManagerStore()

View File

@@ -0,0 +1,63 @@
<template>
<IconTextButton
v-tooltip.top="$t('manager.tryUpdateTooltip')"
v-bind="$attrs"
type="transparent"
:label="computedLabel"
:border="true"
:size="size"
:disabled="isUpdating"
@click="tryUpdate"
>
<template v-if="isUpdating" #icon>
<DotSpinner duration="1s" :size="size === 'sm' ? 12 : 16" />
</template>
</IconTextButton>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import IconTextButton from '@/components/button/IconTextButton.vue'
import DotSpinner from '@/components/common/DotSpinner.vue'
import type { ButtonSize } from '@/types/buttonTypes'
import type { components } from '@/types/comfyRegistryTypes'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
type NodePack = components['schemas']['Node']
const { nodePack, size = 'sm' } = defineProps<{
nodePack: NodePack
size?: ButtonSize
}>()
const { t } = useI18n()
const managerStore = useComfyManagerStore()
const isUpdating = ref(false)
async function tryUpdate() {
if (!nodePack.id) {
console.warn('Pack missing required id:', nodePack)
return
}
isUpdating.value = true
try {
await managerStore.updatePack.call({
id: nodePack.id,
version: 'nightly'
})
managerStore.updatePack.clear()
} catch (error) {
console.error('Nightly update failed:', error)
} finally {
isUpdating.value = false
}
}
const computedLabel = computed(() =>
isUpdating.value ? t('g.updating') : t('manager.tryUpdate')
)
</script>

View File

@@ -5,7 +5,14 @@
<InfoPanelHeader <InfoPanelHeader
:node-packs="[nodePack]" :node-packs="[nodePack]"
:has-conflict="hasCompatibilityIssues" :has-conflict="hasCompatibilityIssues"
/> >
<template v-if="canTryNightlyUpdate" #install-button>
<div class="flex w-full justify-center gap-2">
<PackTryUpdateButton :node-pack="nodePack" size="md" />
<PackUninstallButton :node-packs="[nodePack]" size="md" />
</div>
</template>
</InfoPanelHeader>
</div> </div>
<div <div
ref="scrollContainer" ref="scrollContainer"
@@ -68,9 +75,12 @@ import type { components } from '@/types/comfyRegistryTypes'
import PackStatusMessage from '@/workbench/extensions/manager/components/manager/PackStatusMessage.vue' import PackStatusMessage from '@/workbench/extensions/manager/components/manager/PackStatusMessage.vue'
import PackVersionBadge from '@/workbench/extensions/manager/components/manager/PackVersionBadge.vue' import PackVersionBadge from '@/workbench/extensions/manager/components/manager/PackVersionBadge.vue'
import PackEnableToggle from '@/workbench/extensions/manager/components/manager/button/PackEnableToggle.vue' import PackEnableToggle from '@/workbench/extensions/manager/components/manager/button/PackEnableToggle.vue'
import PackTryUpdateButton from '@/workbench/extensions/manager/components/manager/button/PackTryUpdateButton.vue'
import PackUninstallButton from '@/workbench/extensions/manager/components/manager/button/PackUninstallButton.vue'
import InfoPanelHeader from '@/workbench/extensions/manager/components/manager/infoPanel/InfoPanelHeader.vue' import InfoPanelHeader from '@/workbench/extensions/manager/components/manager/infoPanel/InfoPanelHeader.vue'
import InfoTabs from '@/workbench/extensions/manager/components/manager/infoPanel/InfoTabs.vue' import InfoTabs from '@/workbench/extensions/manager/components/manager/infoPanel/InfoTabs.vue'
import MetadataRow from '@/workbench/extensions/manager/components/manager/infoPanel/MetadataRow.vue' import MetadataRow from '@/workbench/extensions/manager/components/manager/infoPanel/MetadataRow.vue'
import { usePackUpdateStatus } from '@/workbench/extensions/manager/composables/nodePack/usePackUpdateStatus'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection' import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
import { useImportFailedDetection } from '@/workbench/extensions/manager/composables/useImportFailedDetection' import { useImportFailedDetection } from '@/workbench/extensions/manager/composables/useImportFailedDetection'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore' import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
@@ -99,6 +109,8 @@ whenever(isInstalled, () => {
isInstalling.value = false isInstalling.value = false
}) })
const { canTryNightlyUpdate } = usePackUpdateStatus(() => nodePack)
const { checkNodeCompatibility } = useConflictDetection() const { checkNodeCompatibility } = useConflictDetection()
const { getConflictsForPackageByID } = useConflictDetectionStore() const { getConflictsForPackageByID } = useConflictDetectionStore()

View File

@@ -18,12 +18,27 @@
<div v-if="isMixed" class="text-sm text-neutral-500"> <div v-if="isMixed" class="text-sm text-neutral-500">
{{ $t('manager.mixedSelectionMessage') }} {{ $t('manager.mixedSelectionMessage') }}
</div> </div>
<!-- All installed: Show uninstall button --> <!-- All installed: Show update (if nightly) and uninstall buttons -->
<PackUninstallButton <div
v-else-if="isAllInstalled" v-else-if="isAllInstalled"
size="md" class="flex w-full justify-center gap-2"
:node-packs="installedPacks" >
/> <IconTextButton
v-if="hasNightlyPacks"
v-tooltip.top="$t('manager.tryUpdateTooltip')"
type="transparent"
:label="updateSelectedLabel"
:border="true"
size="md"
:disabled="isUpdatingSelected"
@click="updateSelectedNightlyPacks"
>
<template v-if="isUpdatingSelected" #icon>
<DotSpinner duration="1s" :size="16" />
</template>
</IconTextButton>
<PackUninstallButton size="md" :node-packs="installedPacks" />
</div>
<!-- None installed: Show install button --> <!-- None installed: Show install button -->
<PackInstallButton <PackInstallButton
v-else-if="isNoneInstalled" v-else-if="isNoneInstalled"
@@ -55,8 +70,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { useAsyncState } from '@vueuse/core' import { useAsyncState } from '@vueuse/core'
import { computed, onUnmounted, provide, toRef } from 'vue' import { computed, onUnmounted, provide, ref, toRef } from 'vue'
import { useI18n } from 'vue-i18n'
import IconTextButton from '@/components/button/IconTextButton.vue'
import DotSpinner from '@/components/common/DotSpinner.vue'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore' import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import type { components } from '@/types/comfyRegistryTypes' import type { components } from '@/types/comfyRegistryTypes'
import PackStatusMessage from '@/workbench/extensions/manager/components/manager/PackStatusMessage.vue' import PackStatusMessage from '@/workbench/extensions/manager/components/manager/PackStatusMessage.vue'
@@ -68,6 +86,7 @@ import PackIconStacked from '@/workbench/extensions/manager/components/manager/p
import { usePacksSelection } from '@/workbench/extensions/manager/composables/nodePack/usePacksSelection' import { usePacksSelection } from '@/workbench/extensions/manager/composables/nodePack/usePacksSelection'
import { usePacksStatus } from '@/workbench/extensions/manager/composables/nodePack/usePacksStatus' import { usePacksStatus } from '@/workbench/extensions/manager/composables/nodePack/usePacksStatus'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection' import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes' import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
import { ImportFailedKey } from '@/workbench/extensions/manager/types/importFailedTypes' import { ImportFailedKey } from '@/workbench/extensions/manager/types/importFailedTypes'
@@ -75,6 +94,8 @@ const { nodePacks } = defineProps<{
nodePacks: components['schemas']['Node'][] nodePacks: components['schemas']['Node'][]
}>() }>()
const { t } = useI18n()
const managerStore = useComfyManagerStore()
const nodePacksRef = toRef(() => nodePacks) const nodePacksRef = toRef(() => nodePacks)
// Use new composables for cleaner code // Use new composables for cleaner code
@@ -83,11 +104,40 @@ const {
notInstalledPacks, notInstalledPacks,
isAllInstalled, isAllInstalled,
isNoneInstalled, isNoneInstalled,
isMixed isMixed,
nightlyPacks,
hasNightlyPacks
} = usePacksSelection(nodePacksRef) } = usePacksSelection(nodePacksRef)
const { hasImportFailed, overallStatus } = usePacksStatus(nodePacksRef) const { hasImportFailed, overallStatus } = usePacksStatus(nodePacksRef)
// Batch update state for nightly packs
const isUpdatingSelected = ref(false)
async function updateSelectedNightlyPacks() {
if (nightlyPacks.value.length === 0) return
isUpdatingSelected.value = true
try {
for (const pack of nightlyPacks.value) {
if (!pack.id) continue
await managerStore.updatePack.call({
id: pack.id,
version: 'nightly'
})
}
managerStore.updatePack.clear()
} catch (error) {
console.error('Batch nightly update failed:', error)
} finally {
isUpdatingSelected.value = false
}
}
const updateSelectedLabel = computed(() =>
isUpdatingSelected.value ? t('g.updating') : t('manager.updateSelected')
)
const { checkNodeCompatibility } = useConflictDetection() const { checkNodeCompatibility } = useConflictDetection()
const { getNodeDefs } = useComfyRegistryStore() const { getNodeDefs } = useComfyRegistryStore()

View File

@@ -1,19 +1,26 @@
import { toValue } from '@vueuse/core'
import { compare, valid } from 'semver' import { compare, valid } from 'semver'
import type { MaybeRefOrGetter } from 'vue'
import { computed } from 'vue' import { computed } from 'vue'
import type { components } from '@/types/comfyRegistryTypes' import type { components } from '@/types/comfyRegistryTypes'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore' import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
export const usePackUpdateStatus = ( export const usePackUpdateStatus = (
nodePack: components['schemas']['Node'] nodePackSource: MaybeRefOrGetter<components['schemas']['Node']>
) => { ) => {
const { isPackInstalled, getInstalledPackVersion } = useComfyManagerStore() const { isPackInstalled, isPackEnabled, getInstalledPackVersion } =
useComfyManagerStore()
const isInstalled = computed(() => isPackInstalled(nodePack?.id)) // Use toValue to unwrap the source reactively inside computeds
const nodePack = computed(() => toValue(nodePackSource))
const isInstalled = computed(() => isPackInstalled(nodePack.value?.id))
const isEnabled = computed(() => isPackEnabled(nodePack.value?.id))
const installedVersion = computed(() => const installedVersion = computed(() =>
getInstalledPackVersion(nodePack.id ?? '') getInstalledPackVersion(nodePack.value?.id ?? '')
) )
const latestVersion = computed(() => nodePack.latest_version?.version) const latestVersion = computed(() => nodePack.value?.latest_version?.version)
const isNightlyPack = computed( const isNightlyPack = computed(
() => !!installedVersion.value && !valid(installedVersion.value) () => !!installedVersion.value && !valid(installedVersion.value)
@@ -31,9 +38,19 @@ export const usePackUpdateStatus = (
return compare(latestVersion.value, installedVersion.value) > 0 return compare(latestVersion.value, installedVersion.value) > 0
}) })
/**
* Nightly packs can always "try update" since we cannot compare git hashes
* to determine if an update is actually available. This allows users to
* pull the latest changes from the repository.
*/
const canTryNightlyUpdate = computed(
() => isInstalled.value && isEnabled.value && isNightlyPack.value
)
return { return {
isUpdateAvailable, isUpdateAvailable,
isNightlyPack, isNightlyPack,
canTryNightlyUpdate,
installedVersion, installedVersion,
latestVersion latestVersion
} }

View File

@@ -1,3 +1,4 @@
import { valid } from 'semver'
import { computed } from 'vue' import { computed } from 'vue'
import type { Ref } from 'vue' import type { Ref } from 'vue'
@@ -41,12 +42,30 @@ export function usePacksSelection(nodePacks: Ref<NodePack[]>) {
return 'mixed' return 'mixed'
}) })
/**
* Nightly packs are installed packs with a non-semver version (git hash)
* that are also enabled
*/
const nightlyPacks = computed(() =>
installedPacks.value.filter((pack) => {
if (!pack.id) return false
const version = managerStore.getInstalledPackVersion(pack.id)
const isNightly = !!version && !valid(version)
const isEnabled = managerStore.isPackEnabled(pack.id)
return isNightly && isEnabled
})
)
const hasNightlyPacks = computed(() => nightlyPacks.value.length > 0)
return { return {
installedPacks, installedPacks,
notInstalledPacks, notInstalledPacks,
isAllInstalled, isAllInstalled,
isNoneInstalled, isNoneInstalled,
isMixed, isMixed,
selectionState selectionState,
nightlyPacks,
hasNightlyPacks
} }
} }