mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 10:12:11 +00:00
Compare commits
6 Commits
feature/sm
...
claude/sla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d930514bea | ||
|
|
99cb7a2da1 | ||
|
|
b3d87673ec | ||
|
|
6a733918a7 | ||
|
|
a87d2cf1bd | ||
|
|
a1d689d3b3 |
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
@@ -22,7 +22,7 @@
|
||||
|
||||
<!-- OSS mode: Open Manager + Install All buttons -->
|
||||
<div v-else-if="showManagerButtons" class="flex justify-end gap-1 py-2 px-4">
|
||||
<Button variant="textonly" size="sm" @click="openManager">{{
|
||||
<Button variant="textonly" @click="openManager">{{
|
||||
$t('g.openManager')
|
||||
}}</Button>
|
||||
<PackInstallButton
|
||||
|
||||
@@ -64,12 +64,12 @@ import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, toRefs } from 'vue'
|
||||
|
||||
import HelpCenterMenuContent from '@/components/helpcenter/HelpCenterMenuContent.vue'
|
||||
import { useNodeConflictDialog } from '@/composables/useNodeConflictDialog'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
|
||||
import ReleaseNotificationToast from '@/platform/updates/components/ReleaseNotificationToast.vue'
|
||||
import WhatsNewPopup from '@/platform/updates/components/WhatsNewPopup.vue'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useHelpCenterStore } from '@/stores/helpCenterStore'
|
||||
import { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment'
|
||||
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
|
||||
@@ -84,7 +84,7 @@ const { shouldShowRedDot: showReleaseRedDot } = storeToRefs(releaseStore)
|
||||
|
||||
const conflictDetection = useConflictDetection()
|
||||
|
||||
const { show: showNodeConflictDialog } = useNodeConflictDialog()
|
||||
const { showNodeConflictDialog } = useDialogService()
|
||||
|
||||
// Use conflict acknowledgment state from composable - call only once
|
||||
const { shouldShowRedDot: shouldShowConflictRedDot, markConflictsAsSeen } =
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import MissingNodesContent from '@/components/dialog/content/MissingNodesContent.vue'
|
||||
import MissingNodesFooter from '@/components/dialog/content/MissingNodesFooter.vue'
|
||||
import MissingNodesHeader from '@/components/dialog/content/MissingNodesHeader.vue'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import type { ComponentProps } from 'vue-component-type-helpers'
|
||||
|
||||
const DIALOG_KEY = 'global-missing-nodes'
|
||||
|
||||
export const useMissingNodesDialog = () => {
|
||||
const dialogService = useDialogService()
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
function hide() {
|
||||
dialogStore.closeDialog({ key: DIALOG_KEY })
|
||||
}
|
||||
|
||||
function show(props: ComponentProps<typeof MissingNodesContent>) {
|
||||
dialogService.showSmallDialog({
|
||||
key: DIALOG_KEY,
|
||||
headerComponent: MissingNodesHeader,
|
||||
footerComponent: MissingNodesFooter,
|
||||
component: MissingNodesContent,
|
||||
props
|
||||
})
|
||||
}
|
||||
|
||||
return { show, hide }
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import NodeConflictDialogContent from '@/workbench/extensions/manager/components/manager/NodeConflictDialogContent.vue'
|
||||
import NodeConflictFooter from '@/workbench/extensions/manager/components/manager/NodeConflictFooter.vue'
|
||||
import NodeConflictHeader from '@/workbench/extensions/manager/components/manager/NodeConflictHeader.vue'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import type { DialogComponentProps } from '@/stores/dialogStore'
|
||||
import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
|
||||
|
||||
const DIALOG_KEY = 'global-node-conflict'
|
||||
|
||||
export const useNodeConflictDialog = () => {
|
||||
const dialogService = useDialogService()
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
function hide() {
|
||||
dialogStore.closeDialog({ key: DIALOG_KEY })
|
||||
}
|
||||
|
||||
function show(
|
||||
options: {
|
||||
showAfterWhatsNew?: boolean
|
||||
conflictedPackages?: ConflictDetectionResult[]
|
||||
dialogComponentProps?: DialogComponentProps
|
||||
buttonText?: string
|
||||
onButtonClick?: () => void
|
||||
} = {}
|
||||
) {
|
||||
const { buttonText, onButtonClick, showAfterWhatsNew, conflictedPackages } =
|
||||
options
|
||||
|
||||
return dialogService.showSmallDialog({
|
||||
key: DIALOG_KEY,
|
||||
headerComponent: NodeConflictHeader,
|
||||
footerComponent: NodeConflictFooter,
|
||||
component: NodeConflictDialogContent,
|
||||
props: {
|
||||
showAfterWhatsNew,
|
||||
conflictedPackages
|
||||
},
|
||||
footerProps: {
|
||||
buttonText,
|
||||
onButtonClick
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return { show, hide }
|
||||
}
|
||||
@@ -272,4 +272,108 @@ describe('useTemplateFiltering', () => {
|
||||
'beta-pro'
|
||||
])
|
||||
})
|
||||
|
||||
it('incorporates search relevance into recommended sorting', async () => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
const templates = ref<TemplateInfo[]>([
|
||||
{
|
||||
name: 'wan-video-exact',
|
||||
title: 'Wan Video Template',
|
||||
description: 'A template with Wan in title',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'png',
|
||||
date: '2024-01-01',
|
||||
usage: 10
|
||||
},
|
||||
{
|
||||
name: 'qwen-image-partial',
|
||||
title: 'Qwen Image Editor',
|
||||
description: 'A template that contains w, a, n scattered',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'png',
|
||||
date: '2024-01-01',
|
||||
usage: 1000 // Higher usage but worse search match
|
||||
},
|
||||
{
|
||||
name: 'wan-text-exact',
|
||||
title: 'Wan2.5: Text to Image',
|
||||
description: 'Another exact match for Wan',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'png',
|
||||
date: '2024-01-01',
|
||||
usage: 50
|
||||
}
|
||||
])
|
||||
|
||||
const { searchQuery, sortBy, filteredTemplates } =
|
||||
useTemplateFiltering(templates)
|
||||
|
||||
// Search for "Wan"
|
||||
searchQuery.value = 'Wan'
|
||||
sortBy.value = 'recommended'
|
||||
await nextTick()
|
||||
await vi.runOnlyPendingTimersAsync()
|
||||
await nextTick()
|
||||
|
||||
// Templates with "Wan" in title should rank higher than Qwen despite lower usage
|
||||
// because search relevance is now factored into the recommended sort
|
||||
const results = filteredTemplates.value.map((t) => t.name)
|
||||
|
||||
// Verify exact matches appear (Qwen might be filtered out by threshold)
|
||||
expect(results).toContain('wan-video-exact')
|
||||
expect(results).toContain('wan-text-exact')
|
||||
|
||||
// If Qwen appears, it should be ranked lower than exact matches
|
||||
if (results.includes('qwen-image-partial')) {
|
||||
const wanIndex = results.indexOf('wan-video-exact')
|
||||
const qwenIndex = results.indexOf('qwen-image-partial')
|
||||
expect(wanIndex).toBeLessThan(qwenIndex)
|
||||
}
|
||||
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('preserves Fuse search order when using default sort', async () => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
const templates = ref<TemplateInfo[]>([
|
||||
{
|
||||
name: 'portrait-basic',
|
||||
title: 'Basic Portrait',
|
||||
description: 'A basic template',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'png'
|
||||
},
|
||||
{
|
||||
name: 'portrait-pro',
|
||||
title: 'Portrait Pro Edition',
|
||||
description: 'Advanced portrait features',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'png'
|
||||
},
|
||||
{
|
||||
name: 'landscape-view',
|
||||
title: 'Landscape Generator',
|
||||
description: 'Generate landscapes',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'png'
|
||||
}
|
||||
])
|
||||
|
||||
const { searchQuery, sortBy, filteredTemplates } =
|
||||
useTemplateFiltering(templates)
|
||||
|
||||
searchQuery.value = 'Portrait Pro'
|
||||
sortBy.value = 'default'
|
||||
await nextTick()
|
||||
await vi.runOnlyPendingTimersAsync()
|
||||
await nextTick()
|
||||
|
||||
const results = filteredTemplates.value.map((t) => t.name)
|
||||
|
||||
// With default sort, Fuse's relevance ordering is preserved
|
||||
// "Portrait Pro Edition" should be first as it's the best match
|
||||
expect(results[0]).toBe('portrait-pro')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -82,13 +82,31 @@ export function useTemplateFiltering(
|
||||
|
||||
const debouncedSearchQuery = refDebounced(searchQuery, 50)
|
||||
|
||||
const filteredBySearch = computed(() => {
|
||||
// Store Fuse search results with scores for use in sorting
|
||||
const fuseSearchResults = computed(() => {
|
||||
if (!debouncedSearchQuery.value.trim()) {
|
||||
return null
|
||||
}
|
||||
return fuse.value.search(debouncedSearchQuery.value)
|
||||
})
|
||||
|
||||
// Map of template name to search score (lower is better in Fuse, 0 = perfect match)
|
||||
const searchScoreMap = computed(() => {
|
||||
const map = new Map<string, number>()
|
||||
if (fuseSearchResults.value) {
|
||||
fuseSearchResults.value.forEach((result) => {
|
||||
// Store the score (0 = perfect match, 1 = worst match)
|
||||
map.set(result.item.name, result.score ?? 1)
|
||||
})
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const filteredBySearch = computed(() => {
|
||||
if (!fuseSearchResults.value) {
|
||||
return templatesArray.value
|
||||
}
|
||||
|
||||
const results = fuse.value.search(debouncedSearchQuery.value)
|
||||
return results.map((result) => result.item)
|
||||
return fuseSearchResults.value.map((result) => result.item)
|
||||
})
|
||||
|
||||
const filteredByModels = computed(() => {
|
||||
@@ -165,31 +183,66 @@ export function useTemplateFiltering(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Helper to get search relevance score (higher is better, 0-1 range)
|
||||
// Fuse returns scores where 0 = perfect match, 1 = worst match
|
||||
// We invert it so higher = better for combining with other scores
|
||||
const getSearchRelevance = (template: TemplateInfo): number => {
|
||||
const fuseScore = searchScoreMap.value.get(template.name)
|
||||
if (fuseScore === undefined) return 0 // Not in search results or no search
|
||||
return 1 - fuseScore // Invert: 0 (worst) -> 1 (best)
|
||||
}
|
||||
|
||||
const hasActiveSearch = computed(
|
||||
() => debouncedSearchQuery.value.trim() !== ''
|
||||
)
|
||||
|
||||
const sortedTemplates = computed(() => {
|
||||
const templates = [...filteredByRunsOn.value]
|
||||
|
||||
switch (sortBy.value) {
|
||||
case 'recommended':
|
||||
// Curated: usage × 0.5 + internal × 0.3 + freshness × 0.2
|
||||
// When searching, heavily weight search relevance
|
||||
// Formula with search: searchRelevance × 0.6 + (usage × 0.5 + internal × 0.3 + freshness × 0.2) × 0.4
|
||||
// Formula without search: usage × 0.5 + internal × 0.3 + freshness × 0.2
|
||||
return templates.sort((a, b) => {
|
||||
const scoreA = rankingStore.computeDefaultScore(
|
||||
const baseScoreA = rankingStore.computeDefaultScore(
|
||||
a.date,
|
||||
a.searchRank,
|
||||
a.usage
|
||||
)
|
||||
const scoreB = rankingStore.computeDefaultScore(
|
||||
const baseScoreB = rankingStore.computeDefaultScore(
|
||||
b.date,
|
||||
b.searchRank,
|
||||
b.usage
|
||||
)
|
||||
return scoreB - scoreA
|
||||
|
||||
if (hasActiveSearch.value) {
|
||||
const searchA = getSearchRelevance(a)
|
||||
const searchB = getSearchRelevance(b)
|
||||
const finalA = searchA * 0.6 + baseScoreA * 0.4
|
||||
const finalB = searchB * 0.6 + baseScoreB * 0.4
|
||||
return finalB - finalA
|
||||
}
|
||||
|
||||
return baseScoreB - baseScoreA
|
||||
})
|
||||
case 'popular':
|
||||
// User-driven: usage × 0.9 + freshness × 0.1
|
||||
// When searching, include search relevance
|
||||
// Formula with search: searchRelevance × 0.5 + (usage × 0.9 + freshness × 0.1) × 0.5
|
||||
// Formula without search: usage × 0.9 + freshness × 0.1
|
||||
return templates.sort((a, b) => {
|
||||
const scoreA = rankingStore.computePopularScore(a.date, a.usage)
|
||||
const scoreB = rankingStore.computePopularScore(b.date, b.usage)
|
||||
return scoreB - scoreA
|
||||
const baseScoreA = rankingStore.computePopularScore(a.date, a.usage)
|
||||
const baseScoreB = rankingStore.computePopularScore(b.date, b.usage)
|
||||
|
||||
if (hasActiveSearch.value) {
|
||||
const searchA = getSearchRelevance(a)
|
||||
const searchB = getSearchRelevance(b)
|
||||
const finalA = searchA * 0.5 + baseScoreA * 0.5
|
||||
const finalB = searchB * 0.5 + baseScoreB * 0.5
|
||||
return finalB - finalA
|
||||
}
|
||||
|
||||
return baseScoreB - baseScoreA
|
||||
})
|
||||
case 'alphabetical':
|
||||
return templates.sort((a, b) => {
|
||||
@@ -209,6 +262,12 @@ export function useTemplateFiltering(
|
||||
const vramB = getVramMetric(b)
|
||||
|
||||
if (vramA === vramB) {
|
||||
// Use search relevance as tiebreaker when searching
|
||||
if (hasActiveSearch.value) {
|
||||
const searchA = getSearchRelevance(a)
|
||||
const searchB = getSearchRelevance(b)
|
||||
if (searchA !== searchB) return searchB - searchA
|
||||
}
|
||||
const nameA = a.title || a.name || ''
|
||||
const nameB = b.title || b.name || ''
|
||||
return nameA.localeCompare(nameB)
|
||||
@@ -225,11 +284,20 @@ export function useTemplateFiltering(
|
||||
typeof a.size === 'number' ? a.size : Number.POSITIVE_INFINITY
|
||||
const sizeB =
|
||||
typeof b.size === 'number' ? b.size : Number.POSITIVE_INFINITY
|
||||
if (sizeA === sizeB) return 0
|
||||
if (sizeA === sizeB) {
|
||||
// Use search relevance as tiebreaker when searching
|
||||
if (hasActiveSearch.value) {
|
||||
const searchA = getSearchRelevance(a)
|
||||
const searchB = getSearchRelevance(b)
|
||||
if (searchA !== searchB) return searchB - searchA
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return sizeA - sizeB
|
||||
})
|
||||
case 'default':
|
||||
default:
|
||||
// 'default' preserves Fuse's search order (already sorted by relevance)
|
||||
return templates
|
||||
}
|
||||
})
|
||||
|
||||
@@ -6,7 +6,9 @@ import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
|
||||
import { PLYLoader } from 'three/examples/jsm/loaders/PLYLoader'
|
||||
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
|
||||
import { MtlObjBridge, OBJLoader2Parallel } from 'wwobjloader2'
|
||||
import OBJLoader2WorkerUrl from 'wwobjloader2/worker?url'
|
||||
// Use pre-bundled worker module (has all dependencies included)
|
||||
// The unbundled 'wwobjloader2/worker' has ES imports that fail in production builds
|
||||
import OBJLoader2WorkerUrl from 'wwobjloader2/bundle/worker/module?url'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
|
||||
@@ -28,6 +28,7 @@ import type {
|
||||
} from '@/lib/litegraph/src/types/serialisation'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { UUID } from '@/lib/litegraph/src/utils/uuid'
|
||||
import { AssetWidget } from '@/lib/litegraph/src/widgets/AssetWidget'
|
||||
import { toConcreteWidget } from '@/lib/litegraph/src/widgets/widgetMap'
|
||||
|
||||
import { ExecutableNodeDTO } from './ExecutableNodeDTO'
|
||||
@@ -333,6 +334,8 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
const promotedWidget = toConcreteWidget(widget, this).createCopyForNode(
|
||||
this
|
||||
)
|
||||
if (widget instanceof AssetWidget)
|
||||
promotedWidget.options.nodeType ??= widget.node.type
|
||||
|
||||
Object.assign(promotedWidget, {
|
||||
get name() {
|
||||
|
||||
@@ -27,6 +27,8 @@ export interface IWidgetOptions<TValues = unknown[]> {
|
||||
socketless?: boolean
|
||||
/** If `true`, the widget will not be rendered by the Vue renderer. */
|
||||
canvasOnly?: boolean
|
||||
/** Used as a temporary override for determining the asset type in vue mode*/
|
||||
nodeType?: string
|
||||
|
||||
values?: TValues
|
||||
/** Optional function to format values for display (e.g., hash → human-readable name) */
|
||||
|
||||
@@ -378,6 +378,10 @@
|
||||
"warningTooltip": "This package may have compatibility issues with your current environment"
|
||||
}
|
||||
},
|
||||
"importFailed": {
|
||||
"title": "Import Failed",
|
||||
"copyError": "Copy Error"
|
||||
},
|
||||
"issueReport": {
|
||||
"helpFix": "Help Fix This"
|
||||
},
|
||||
|
||||
@@ -99,7 +99,10 @@
|
||||
</span>
|
||||
</div>
|
||||
<!-- Multiple Images Navigation -->
|
||||
<div v-if="hasMultipleImages" class="flex justify-center gap-1 pt-4">
|
||||
<div
|
||||
v-if="hasMultipleImages"
|
||||
class="flex flex-wrap justify-center gap-1 pt-4"
|
||||
>
|
||||
<button
|
||||
v-for="(_, index) in imageUrls"
|
||||
:key="index"
|
||||
|
||||
@@ -60,8 +60,9 @@ const combinedProps = computed(() => ({
|
||||
}))
|
||||
|
||||
const getAssetData = () => {
|
||||
if (props.isAssetMode && props.nodeType) {
|
||||
return useAssetWidgetData(toRef(() => props.nodeType))
|
||||
const nodeType = props.widget.options?.nodeType ?? props.nodeType
|
||||
if (props.isAssetMode && nodeType) {
|
||||
return useAssetWidgetData(toRef(nodeType))
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import type { ToggleSwitchProps } from 'primevue/toggleswitch'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
import WidgetToggleSwitch from './WidgetToggleSwitch.vue'
|
||||
@@ -11,9 +11,9 @@ import WidgetToggleSwitch from './WidgetToggleSwitch.vue'
|
||||
describe('WidgetToggleSwitch Value Binding', () => {
|
||||
const createMockWidget = (
|
||||
value: boolean = false,
|
||||
options: Partial<ToggleSwitchProps> = {},
|
||||
options: IWidgetOptions = {},
|
||||
callback?: (value: boolean) => void
|
||||
): SimplifiedWidget<boolean> => ({
|
||||
): SimplifiedWidget<boolean, IWidgetOptions> => ({
|
||||
name: 'test_toggle',
|
||||
type: 'boolean',
|
||||
value,
|
||||
@@ -149,4 +149,47 @@ describe('WidgetToggleSwitch Value Binding', () => {
|
||||
expect(emitted![3]).toContain(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Label Display (label_on/label_off)', () => {
|
||||
it('displays label_on when value is true', () => {
|
||||
const widget = createMockWidget(true, { on: 'inside', off: 'outside' })
|
||||
const wrapper = mountComponent(widget, true)
|
||||
|
||||
expect(wrapper.text()).toContain('inside')
|
||||
})
|
||||
|
||||
it('displays label_off when value is false', () => {
|
||||
const widget = createMockWidget(false, { on: 'inside', off: 'outside' })
|
||||
const wrapper = mountComponent(widget, false)
|
||||
|
||||
expect(wrapper.text()).toContain('outside')
|
||||
})
|
||||
|
||||
it('does not display label when no on/off options provided', () => {
|
||||
const widget = createMockWidget(false, {})
|
||||
const wrapper = mountComponent(widget, false)
|
||||
|
||||
expect(wrapper.find('span').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('updates label when value changes', async () => {
|
||||
const widget = createMockWidget(false, { on: 'enabled', off: 'disabled' })
|
||||
const wrapper = mountComponent(widget, false)
|
||||
|
||||
expect(wrapper.text()).toContain('disabled')
|
||||
|
||||
await wrapper.setProps({ modelValue: true })
|
||||
expect(wrapper.text()).toContain('enabled')
|
||||
})
|
||||
|
||||
it('falls back to true/false when only partial options provided', () => {
|
||||
const widgetOnOnly = createMockWidget(true, { on: 'active' })
|
||||
const wrapperOn = mountComponent(widgetOnOnly, true)
|
||||
expect(wrapperOn.text()).toContain('active')
|
||||
|
||||
const widgetOffOnly = createMockWidget(false, { off: 'inactive' })
|
||||
const wrapperOff = mountComponent(widgetOffOnly, false)
|
||||
expect(wrapperOff.text()).toContain('inactive')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
<template>
|
||||
<WidgetLayoutField :widget>
|
||||
<ToggleSwitch
|
||||
v-model="modelValue"
|
||||
v-bind="filteredProps"
|
||||
class="ml-auto block"
|
||||
:aria-label="widget.name"
|
||||
/>
|
||||
<div class="ml-auto flex w-fit items-center gap-2">
|
||||
<span
|
||||
v-if="stateLabel"
|
||||
:class="
|
||||
cn(
|
||||
'text-sm transition-colors',
|
||||
modelValue
|
||||
? 'text-node-component-slot-text'
|
||||
: 'text-node-component-slot-text/50'
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ stateLabel }}
|
||||
</span>
|
||||
<ToggleSwitch
|
||||
v-model="modelValue"
|
||||
v-bind="filteredProps"
|
||||
:aria-label="widget.name"
|
||||
/>
|
||||
</div>
|
||||
</WidgetLayoutField>
|
||||
</template>
|
||||
|
||||
@@ -13,7 +27,9 @@
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import {
|
||||
STANDARD_EXCLUDED_PROPS,
|
||||
filterWidgetProps
|
||||
@@ -22,7 +38,7 @@ import {
|
||||
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
||||
|
||||
const { widget } = defineProps<{
|
||||
widget: SimplifiedWidget<boolean>
|
||||
widget: SimplifiedWidget<boolean, IWidgetOptions>
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<boolean>()
|
||||
@@ -30,4 +46,10 @@ const modelValue = defineModel<boolean>()
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(widget.options, STANDARD_EXCLUDED_PROPS)
|
||||
)
|
||||
|
||||
const stateLabel = computed(() => {
|
||||
const options = widget.options
|
||||
if (!options?.on && !options?.off) return null
|
||||
return modelValue.value ? (options.on ?? 'true') : (options.off ?? 'false')
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -46,7 +46,6 @@ import {
|
||||
ComponentWidgetImpl,
|
||||
DOMWidgetImpl
|
||||
} from '@/scripts/domWidget'
|
||||
import { useMissingNodesDialog } from '@/composables/useMissingNodesDialog'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
@@ -1030,7 +1029,7 @@ export class ComfyApp {
|
||||
|
||||
private showMissingNodesError(missingNodeTypes: MissingNodeType[]) {
|
||||
if (useSettingStore().get('Comfy.Workflow.ShowMissingNodesWarning')) {
|
||||
useMissingNodesDialog().show({ missingNodeTypes })
|
||||
useDialogService().showLoadWorkflowWarning({ missingNodeTypes })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ import { merge } from 'es-toolkit/compat'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
import ApiNodesSignInContent from '@/components/dialog/content/ApiNodesSignInContent.vue'
|
||||
import MissingNodesContent from '@/components/dialog/content/MissingNodesContent.vue'
|
||||
import MissingNodesFooter from '@/components/dialog/content/MissingNodesFooter.vue'
|
||||
import MissingNodesHeader from '@/components/dialog/content/MissingNodesHeader.vue'
|
||||
import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
|
||||
import ErrorDialogContent from '@/components/dialog/content/ErrorDialogContent.vue'
|
||||
import MissingModelsWarning from '@/components/dialog/content/MissingModelsWarning.vue'
|
||||
@@ -27,7 +30,14 @@ import ManagerProgressFooter from '@/workbench/extensions/manager/components/Man
|
||||
import ManagerProgressHeader from '@/workbench/extensions/manager/components/ManagerProgressHeader.vue'
|
||||
import ManagerDialogContent from '@/workbench/extensions/manager/components/manager/ManagerDialogContent.vue'
|
||||
import ManagerHeader from '@/workbench/extensions/manager/components/manager/ManagerHeader.vue'
|
||||
import type { ComponentAttrs, ComponentProps } from 'vue-component-type-helpers'
|
||||
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'
|
||||
import NodeConflictDialogContent from '@/workbench/extensions/manager/components/manager/NodeConflictDialogContent.vue'
|
||||
import NodeConflictFooter from '@/workbench/extensions/manager/components/manager/NodeConflictFooter.vue'
|
||||
import NodeConflictHeader from '@/workbench/extensions/manager/components/manager/NodeConflictHeader.vue'
|
||||
import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
|
||||
import type { ComponentAttrs } from 'vue-component-type-helpers'
|
||||
|
||||
export type ConfirmationDialogType =
|
||||
| 'default'
|
||||
@@ -40,6 +50,32 @@ export type ConfirmationDialogType =
|
||||
export const useDialogService = () => {
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
function showLoadWorkflowWarning(
|
||||
props: ComponentAttrs<typeof MissingNodesContent>
|
||||
) {
|
||||
dialogStore.showDialog({
|
||||
key: 'global-missing-nodes',
|
||||
headerComponent: MissingNodesHeader,
|
||||
footerComponent: MissingNodesFooter,
|
||||
component: MissingNodesContent,
|
||||
dialogComponentProps: {
|
||||
closable: true,
|
||||
pt: {
|
||||
root: { class: 'bg-base-background border-border-default' },
|
||||
header: { class: '!p-0 !m-0' },
|
||||
content: { class: '!p-0 overflow-y-hidden' },
|
||||
footer: { class: '!p-0' },
|
||||
pcCloseButton: {
|
||||
root: {
|
||||
class: '!w-7 !h-7 !border-none !outline-none !p-2 !m-1.5'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
props
|
||||
})
|
||||
}
|
||||
|
||||
function showMissingModelsWarning(
|
||||
props: ComponentAttrs<typeof MissingModelsWarning>
|
||||
) {
|
||||
@@ -417,44 +453,6 @@ export const useDialogService = () => {
|
||||
}
|
||||
}
|
||||
|
||||
function showSmallDialog<T extends Component>(options: {
|
||||
key: string
|
||||
component: T
|
||||
headerComponent?: Component
|
||||
footerComponent?: Component
|
||||
props?: ComponentProps<T>
|
||||
footerProps?: Record<string, unknown>
|
||||
dialogComponentProps?: DialogComponentProps
|
||||
}) {
|
||||
const smallDialogDefaultProps: DialogComponentProps = {
|
||||
closable: true,
|
||||
pt: {
|
||||
root: { class: 'bg-base-background border-border-default' },
|
||||
header: { class: '!p-0 !m-0' },
|
||||
content: { class: '!p-0 overflow-y-hidden' },
|
||||
footer: { class: '!p-0' },
|
||||
pcCloseButton: {
|
||||
root: {
|
||||
class: '!w-7 !h-7 !border-none !outline-none !p-2 !m-1.5'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dialogStore.showDialog({
|
||||
key: options.key,
|
||||
component: options.component,
|
||||
headerComponent: options.headerComponent,
|
||||
footerComponent: options.footerComponent,
|
||||
props: options.props,
|
||||
footerProps: options.footerProps,
|
||||
dialogComponentProps: merge(
|
||||
smallDialogDefaultProps,
|
||||
options.dialogComponentProps || {}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function showLayoutDialog(options: {
|
||||
key: string
|
||||
component: Component
|
||||
@@ -487,6 +485,91 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
function showImportFailedNodeDialog(
|
||||
options: {
|
||||
conflictedPackages?: ConflictDetectionResult[]
|
||||
dialogComponentProps?: DialogComponentProps
|
||||
} = {}
|
||||
) {
|
||||
const { dialogComponentProps, conflictedPackages } = options
|
||||
|
||||
return dialogStore.showDialog({
|
||||
key: 'global-import-failed',
|
||||
headerComponent: ImportFailedNodeHeader,
|
||||
footerComponent: ImportFailedNodeFooter,
|
||||
component: ImportFailedNodeContent,
|
||||
dialogComponentProps: {
|
||||
closable: true,
|
||||
pt: {
|
||||
root: { class: 'bg-base-background border-border-default' },
|
||||
header: { class: '!p-0 !m-0' },
|
||||
content: { class: '!p-0 overflow-y-hidden' },
|
||||
footer: { class: '!p-0' },
|
||||
pcCloseButton: {
|
||||
root: {
|
||||
class: '!w-7 !h-7 !border-none !outline-none !p-2 !m-1.5'
|
||||
}
|
||||
}
|
||||
},
|
||||
...dialogComponentProps
|
||||
},
|
||||
props: {
|
||||
conflictedPackages: conflictedPackages ?? []
|
||||
},
|
||||
footerProps: {
|
||||
conflictedPackages: conflictedPackages ?? []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function showNodeConflictDialog(
|
||||
options: {
|
||||
showAfterWhatsNew?: boolean
|
||||
conflictedPackages?: ConflictDetectionResult[]
|
||||
dialogComponentProps?: DialogComponentProps
|
||||
buttonText?: string
|
||||
onButtonClick?: () => void
|
||||
} = {}
|
||||
) {
|
||||
const {
|
||||
dialogComponentProps,
|
||||
buttonText,
|
||||
onButtonClick,
|
||||
showAfterWhatsNew,
|
||||
conflictedPackages
|
||||
} = options
|
||||
|
||||
return dialogStore.showDialog({
|
||||
key: 'global-node-conflict',
|
||||
headerComponent: NodeConflictHeader,
|
||||
footerComponent: NodeConflictFooter,
|
||||
component: NodeConflictDialogContent,
|
||||
dialogComponentProps: {
|
||||
closable: true,
|
||||
pt: {
|
||||
header: { class: '!p-0 !m-0' },
|
||||
content: { class: '!p-0 overflow-y-hidden' },
|
||||
footer: { class: '!p-0' },
|
||||
pcCloseButton: {
|
||||
root: {
|
||||
class:
|
||||
'!w-7 !h-7 !border-none !outline-none !p-2 !m-1.5 bg-dialog-surface text-white'
|
||||
}
|
||||
}
|
||||
},
|
||||
...dialogComponentProps
|
||||
},
|
||||
props: {
|
||||
showAfterWhatsNew,
|
||||
conflictedPackages
|
||||
},
|
||||
footerProps: {
|
||||
buttonText,
|
||||
onButtonClick
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function showSubscriptionRequiredDialog() {
|
||||
if (!isCloud || !window.__CONFIG__?.subscription_required) {
|
||||
return
|
||||
@@ -499,6 +582,7 @@ export const useDialogService = () => {
|
||||
}
|
||||
|
||||
return {
|
||||
showLoadWorkflowWarning,
|
||||
showMissingModelsWarning,
|
||||
showSettingsDialog,
|
||||
showAboutDialog,
|
||||
@@ -516,7 +600,8 @@ export const useDialogService = () => {
|
||||
confirm,
|
||||
toggleManagerDialog,
|
||||
toggleManagerProgressDialog,
|
||||
showSmallDialog,
|
||||
showLayoutDialog
|
||||
showLayoutDialog,
|
||||
showImportFailedNodeDialog,
|
||||
showNodeConflictDialog
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,19 +215,12 @@ export const useDialogStore = defineStore('dialog', () => {
|
||||
F extends Component = Component
|
||||
>(options: ShowDialogOptions<H, B, F>) {
|
||||
const dialogKey = options.key || genDialogKey()
|
||||
const existingIndex = dialogStack.value.findIndex(
|
||||
(d) => d.key === dialogKey
|
||||
)
|
||||
let dialog =
|
||||
existingIndex !== -1 ? dialogStack.value[existingIndex] : undefined
|
||||
|
||||
let dialog = dialogStack.value.find((d) => d.key === dialogKey)
|
||||
|
||||
if (dialog) {
|
||||
if (!dialog.visible) {
|
||||
dialogStack.value.splice(existingIndex, 1)
|
||||
dialog = createDialog({ ...options, key: dialogKey })
|
||||
} else {
|
||||
dialog.visible = true
|
||||
riseDialog(dialog)
|
||||
}
|
||||
dialog.visible = true
|
||||
riseDialog(dialog)
|
||||
} else {
|
||||
dialog = createDialog({ ...options, key: dialogKey })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="flex w-[490px] flex-col border-t-1 border-border-default">
|
||||
<div class="flex h-full w-full flex-col gap-4 p-4">
|
||||
<!-- Error Details -->
|
||||
<div v-if="importFailedPackages.length > 0" class="flex flex-col gap-3">
|
||||
<div
|
||||
v-for="pkg in importFailedPackages"
|
||||
:key="pkg.packageId"
|
||||
class="flex flex-col gap-2 max-h-60 overflow-x-hidden overflow-y-auto scrollbar-custom"
|
||||
role="region"
|
||||
:aria-label="`Error traceback for ${pkg.packageId}`"
|
||||
tabindex="0"
|
||||
>
|
||||
<!-- Error Message -->
|
||||
<div
|
||||
v-if="pkg.traceback || pkg.errorMessage"
|
||||
class="text-xs p-4 rounded-md bg-secondary-background font-mono"
|
||||
>
|
||||
{{ pkg.traceback || pkg.errorMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
|
||||
|
||||
const { conflictedPackages } = defineProps<{
|
||||
conflictedPackages: ConflictDetectionResult[]
|
||||
}>()
|
||||
|
||||
interface ImportFailedPackage {
|
||||
packageId: string
|
||||
packageName: string
|
||||
errorMessage: string
|
||||
traceback: string
|
||||
}
|
||||
|
||||
const importFailedPackages = computed((): ImportFailedPackage[] => {
|
||||
return conflictedPackages
|
||||
.filter((pkg) =>
|
||||
pkg.conflicts.some((conflict) => conflict.type === 'import_failed')
|
||||
)
|
||||
.map((pkg) => {
|
||||
const importFailedConflict = pkg.conflicts.find(
|
||||
(conflict) => conflict.type === 'import_failed'
|
||||
)
|
||||
if (!importFailedConflict) {
|
||||
return {
|
||||
packageId: pkg.package_id,
|
||||
packageName: pkg.package_name,
|
||||
errorMessage: 'Unknown import error',
|
||||
traceback: ''
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
packageId: pkg.package_id,
|
||||
packageName: pkg.package_name,
|
||||
errorMessage:
|
||||
importFailedConflict.current_value || 'Unknown import error',
|
||||
traceback: importFailedConflict.required_value || ''
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div class="flex w-full items-center justify-between px-3 pb-4">
|
||||
<div class="flex w-full items-start justify-end gap-2 pr-1">
|
||||
<Button variant="secondary" @click="handleCopyError">
|
||||
{{ $t('importFailed.copyError') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
|
||||
|
||||
const { conflictedPackages = [] } = defineProps<{
|
||||
conflictedPackages?: ConflictDetectionResult[]
|
||||
}>()
|
||||
|
||||
const { copyToClipboard } = useCopyToClipboard()
|
||||
|
||||
const formatErrorText = computed(() => {
|
||||
const errorParts: string[] = []
|
||||
|
||||
conflictedPackages.forEach((pkg) => {
|
||||
const importFailedConflict = pkg.conflicts.find(
|
||||
(conflict) => conflict.type === 'import_failed'
|
||||
)
|
||||
|
||||
if (importFailedConflict?.required_value) {
|
||||
errorParts.push(importFailedConflict.required_value)
|
||||
}
|
||||
})
|
||||
|
||||
return errorParts.join('\n\n')
|
||||
})
|
||||
|
||||
const handleCopyError = () => {
|
||||
copyToClipboard(formatErrorText.value)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div class="flex w-full items-center justify-between p-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="icon-[lucide--triangle-alert] text-gold-600"></i>
|
||||
<p class="m-0 text-sm">
|
||||
{{ $t('importFailed.title') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
@@ -185,25 +185,18 @@ describe('NodeConflictDialogContent', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Import Failed Extensions section should show 1 package
|
||||
const sections = wrapper.findAll('.bg-secondary-background')
|
||||
expect(sections.length).toBeGreaterThan(0)
|
||||
|
||||
const importFailedSection = sections.find((section) =>
|
||||
section.text().includes('Import Failed Extensions')
|
||||
)
|
||||
expect(importFailedSection).toBeDefined()
|
||||
expect(importFailedSection!.text()).toContain('1')
|
||||
expect(importFailedSection!.text()).toContain('Import Failed Extensions')
|
||||
const importFailedSection = wrapper.findAll(
|
||||
'.w-full.flex.flex-col.bg-base-background'
|
||||
)[0]
|
||||
expect(importFailedSection.text()).toContain('1')
|
||||
expect(importFailedSection.text()).toContain('Import Failed Extensions')
|
||||
|
||||
// Conflicts section should show 3 conflicts (excluding import_failed)
|
||||
const conflictsSection = sections.find(
|
||||
(section) =>
|
||||
section.text().includes('Conflicts') &&
|
||||
!section.text().includes('Import Failed')
|
||||
)
|
||||
expect(conflictsSection).toBeDefined()
|
||||
expect(conflictsSection!.text()).toContain('3')
|
||||
expect(conflictsSection!.text()).toContain('Conflicts')
|
||||
const conflictsSection = wrapper.findAll(
|
||||
'.w-full.flex.flex-col.bg-base-background'
|
||||
)[1]
|
||||
expect(conflictsSection.text()).toContain('3')
|
||||
expect(conflictsSection.text()).toContain('Conflicts')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -215,14 +208,10 @@ describe('NodeConflictDialogContent', () => {
|
||||
it('should toggle import failed panel', async () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Find import failed panel header - look for the one containing "Import Failed Extensions"
|
||||
const headers = wrapper.findAll(
|
||||
// Find import failed panel header (first one)
|
||||
const importFailedHeader = wrapper.find(
|
||||
'[data-testid="conflict-dialog-panel-toggle"]'
|
||||
)
|
||||
const importFailedHeader = headers.find((header) =>
|
||||
header.text().includes('Import Failed Extensions')
|
||||
)
|
||||
expect(importFailedHeader).toBeDefined()
|
||||
|
||||
// Initially collapsed
|
||||
expect(
|
||||
@@ -230,7 +219,7 @@ describe('NodeConflictDialogContent', () => {
|
||||
).toBe(false)
|
||||
|
||||
// Click to expand import failed panel
|
||||
await importFailedHeader!.trigger('click')
|
||||
await importFailedHeader.trigger('click')
|
||||
|
||||
// Should be expanded now and show package name
|
||||
const expandedContent = wrapper.find(
|
||||
@@ -247,46 +236,34 @@ describe('NodeConflictDialogContent', () => {
|
||||
it('should toggle conflicts panel', async () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Find conflicts panel header - look for the one containing "Conflicts"
|
||||
const headers = wrapper.findAll(
|
||||
// Find conflicts panel header (second one)
|
||||
const conflictsHeader = wrapper.findAll(
|
||||
'[data-testid="conflict-dialog-panel-toggle"]'
|
||||
)
|
||||
const conflictsHeader = headers.find(
|
||||
(header) =>
|
||||
header.text().includes('Conflicts') &&
|
||||
!header.text().includes('Import Failed')
|
||||
)
|
||||
expect(conflictsHeader).toBeDefined()
|
||||
)[1]
|
||||
|
||||
// Click to expand conflicts panel
|
||||
await conflictsHeader!.trigger('click')
|
||||
await conflictsHeader.trigger('click')
|
||||
|
||||
// Should be expanded now - look for the expanded content
|
||||
const expandedPanels = wrapper.findAll(
|
||||
'[data-testid="conflict-dialog-panel-expanded"]'
|
||||
)
|
||||
expect(expandedPanels.length).toBeGreaterThan(0)
|
||||
// Should be expanded now
|
||||
const conflictItems = wrapper.findAll('[aria-label*="Conflict:"]')
|
||||
expect(conflictItems.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should toggle extensions panel', async () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Find extensions panel header - look for the one containing "Extensions at Risk"
|
||||
const headers = wrapper.findAll(
|
||||
// Find extensions panel header (third one)
|
||||
const extensionsHeader = wrapper.findAll(
|
||||
'[data-testid="conflict-dialog-panel-toggle"]'
|
||||
)
|
||||
const extensionsHeader = headers.find((header) =>
|
||||
header.text().includes('Extensions at Risk')
|
||||
)
|
||||
expect(extensionsHeader).toBeDefined()
|
||||
)[2]
|
||||
|
||||
// Click to expand extensions panel
|
||||
await extensionsHeader!.trigger('click')
|
||||
await extensionsHeader.trigger('click')
|
||||
|
||||
// Should be expanded now and show all package names
|
||||
const expandedContent = wrapper.find(
|
||||
const expandedContent = wrapper.findAll(
|
||||
'[data-testid="conflict-dialog-panel-expanded"]'
|
||||
)
|
||||
)[0]
|
||||
expect(expandedContent.exists()).toBe(true)
|
||||
expect(expandedContent.text()).toContain('Test Package 1')
|
||||
expect(expandedContent.text()).toContain('Test Package 2')
|
||||
@@ -296,27 +273,18 @@ describe('NodeConflictDialogContent', () => {
|
||||
it('should collapse other panels when opening one', async () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const headers = wrapper.findAll(
|
||||
const importFailedHeader = wrapper.findAll(
|
||||
'[data-testid="conflict-dialog-panel-toggle"]'
|
||||
)
|
||||
const importFailedHeader = headers.find((header) =>
|
||||
header.text().includes('Import Failed Extensions')
|
||||
)
|
||||
const conflictsHeader = headers.find(
|
||||
(header) =>
|
||||
header.text().includes('Conflicts') &&
|
||||
!header.text().includes('Import Failed')
|
||||
)
|
||||
const extensionsHeader = headers.find((header) =>
|
||||
header.text().includes('Extensions at Risk')
|
||||
)
|
||||
|
||||
expect(importFailedHeader).toBeDefined()
|
||||
expect(conflictsHeader).toBeDefined()
|
||||
expect(extensionsHeader).toBeDefined()
|
||||
)[0]
|
||||
const conflictsHeader = wrapper.findAll(
|
||||
'[data-testid="conflict-dialog-panel-toggle"]'
|
||||
)[1]
|
||||
const extensionsHeader = wrapper.findAll(
|
||||
'[data-testid="conflict-dialog-panel-toggle"]'
|
||||
)[2]
|
||||
|
||||
// Open import failed panel first
|
||||
await importFailedHeader!.trigger('click')
|
||||
await importFailedHeader.trigger('click')
|
||||
|
||||
// Verify import failed panel is open
|
||||
expect((wrapper.vm as any).importFailedExpanded).toBe(true)
|
||||
@@ -324,7 +292,7 @@ describe('NodeConflictDialogContent', () => {
|
||||
expect((wrapper.vm as any).extensionsExpanded).toBe(false)
|
||||
|
||||
// Open conflicts panel
|
||||
await conflictsHeader!.trigger('click')
|
||||
await conflictsHeader.trigger('click')
|
||||
|
||||
// Verify conflicts panel is open and others are closed
|
||||
expect((wrapper.vm as any).importFailedExpanded).toBe(false)
|
||||
@@ -332,7 +300,7 @@ describe('NodeConflictDialogContent', () => {
|
||||
expect((wrapper.vm as any).extensionsExpanded).toBe(false)
|
||||
|
||||
// Open extensions panel
|
||||
await extensionsHeader!.trigger('click')
|
||||
await extensionsHeader.trigger('click')
|
||||
|
||||
// Verify extensions panel is open and others are closed
|
||||
expect((wrapper.vm as any).importFailedExpanded).toBe(false)
|
||||
@@ -349,50 +317,30 @@ describe('NodeConflictDialogContent', () => {
|
||||
it('should display individual conflict details excluding import_failed', async () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Expand conflicts panel - find the one containing "Conflicts"
|
||||
const headers = wrapper.findAll(
|
||||
// Expand conflicts panel (second header)
|
||||
const conflictsHeader = wrapper.findAll(
|
||||
'[data-testid="conflict-dialog-panel-toggle"]'
|
||||
)
|
||||
const conflictsHeader = headers.find(
|
||||
(header) =>
|
||||
header.text().includes('Conflicts') &&
|
||||
!header.text().includes('Import Failed')
|
||||
)
|
||||
expect(conflictsHeader).toBeDefined()
|
||||
await conflictsHeader!.trigger('click')
|
||||
)[1]
|
||||
await conflictsHeader.trigger('click')
|
||||
|
||||
// Should display conflict messages (excluding import_failed)
|
||||
// Look for the expanded panel content that contains conflict items
|
||||
const expandedPanel = wrapper.find(
|
||||
'[data-testid="conflict-dialog-panel-expanded"]'
|
||||
)
|
||||
expect(expandedPanel.exists()).toBe(true)
|
||||
|
||||
// Check that it contains the expected conflict details
|
||||
const conflictElements = expandedPanel.findAll('.flex.h-6')
|
||||
expect(conflictElements).toHaveLength(3) // 2 from Package1 + 1 from Package2
|
||||
const conflictItems = wrapper.findAll('[aria-label*="Conflict:"]')
|
||||
expect(conflictItems).toHaveLength(3) // 2 from Package1 + 1 from Package2
|
||||
})
|
||||
|
||||
it('should display import failed packages separately', async () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Expand import failed panel - find the one containing "Import Failed Extensions"
|
||||
const headers = wrapper.findAll(
|
||||
// Expand import failed panel (first header)
|
||||
const importFailedHeader = wrapper.findAll(
|
||||
'[data-testid="conflict-dialog-panel-toggle"]'
|
||||
)
|
||||
const importFailedHeader = headers.find((header) =>
|
||||
header.text().includes('Import Failed Extensions')
|
||||
)
|
||||
expect(importFailedHeader).toBeDefined()
|
||||
await importFailedHeader!.trigger('click')
|
||||
)[0]
|
||||
await importFailedHeader.trigger('click')
|
||||
|
||||
// Should display only import failed package
|
||||
const expandedPanel = wrapper.find(
|
||||
'[data-testid="conflict-dialog-panel-expanded"]'
|
||||
const importFailedItems = wrapper.findAll(
|
||||
'[aria-label*="Import failed package:"]'
|
||||
)
|
||||
expect(expandedPanel.exists()).toBe(true)
|
||||
|
||||
const importFailedItems = expandedPanel.findAll('.flex.h-6')
|
||||
expect(importFailedItems).toHaveLength(1)
|
||||
expect(importFailedItems[0].text()).toContain('Test Package 3')
|
||||
})
|
||||
@@ -400,24 +348,16 @@ describe('NodeConflictDialogContent', () => {
|
||||
it('should display all package names in extensions list', async () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Expand extensions panel - find the one containing "Extensions at Risk"
|
||||
const headers = wrapper.findAll(
|
||||
// Expand extensions panel (third header)
|
||||
const extensionsHeader = wrapper.findAll(
|
||||
'[data-testid="conflict-dialog-panel-toggle"]'
|
||||
)
|
||||
const extensionsHeader = headers.find((header) =>
|
||||
header.text().includes('Extensions at Risk')
|
||||
)
|
||||
expect(extensionsHeader).toBeDefined()
|
||||
await extensionsHeader!.trigger('click')
|
||||
)[2]
|
||||
await extensionsHeader.trigger('click')
|
||||
|
||||
// Should display all package names
|
||||
const expandedPanel = wrapper.find(
|
||||
'[data-testid="conflict-dialog-panel-expanded"]'
|
||||
)
|
||||
expect(expandedPanel.exists()).toBe(true)
|
||||
expect(expandedPanel.text()).toContain('Test Package 1')
|
||||
expect(expandedPanel.text()).toContain('Test Package 2')
|
||||
expect(expandedPanel.text()).toContain('Test Package 3')
|
||||
expect(wrapper.text()).toContain('Test Package 1')
|
||||
expect(wrapper.text()).toContain('Test Package 2')
|
||||
expect(wrapper.text()).toContain('Test Package 3')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<!-- Import Failed List Wrapper -->
|
||||
<div
|
||||
v-if="importFailedConflicts.length > 0"
|
||||
class="flex min-h-8 w-full flex-col rounded-lg bg-secondary-background"
|
||||
class="flex min-h-8 w-full flex-col rounded-lg bg-base-background"
|
||||
>
|
||||
<div
|
||||
data-testid="conflict-dialog-panel-toggle"
|
||||
@@ -50,7 +50,8 @@
|
||||
<div
|
||||
v-for="(packageName, i) in importFailedConflicts"
|
||||
:key="i"
|
||||
class="hover:bg-alpha-azure-600-30 flex h-6 flex-shrink-0 items-center justify-between px-4"
|
||||
:aria-label="`Import failed package: ${packageName}`"
|
||||
class="flex min-h-6 shrink-0 hover:bg-node-component-surface-hovered items-center justify-between px-4 py-1"
|
||||
>
|
||||
<span class="text-xs text-muted">
|
||||
{{ packageName }}
|
||||
@@ -62,7 +63,7 @@
|
||||
<!-- Conflict List Wrapper -->
|
||||
<div
|
||||
v-if="allConflictDetails.length > 0"
|
||||
class="flex min-h-8 w-full flex-col rounded-lg bg-secondary-background"
|
||||
class="flex min-h-8 w-full flex-col rounded-lg bg-base-background"
|
||||
>
|
||||
<div
|
||||
data-testid="conflict-dialog-panel-toggle"
|
||||
@@ -98,7 +99,8 @@
|
||||
<div
|
||||
v-for="(conflict, i) in allConflictDetails"
|
||||
:key="i"
|
||||
class="hover:bg-alpha-azure-600-30 flex h-6 flex-shrink-0 items-center justify-between px-4"
|
||||
:aria-label="`Conflict: ${getConflictMessage(conflict, t)}`"
|
||||
class="flex min-h-6 shrink-0 hover:bg-node-component-surface-hovered items-center justify-between px-4 py-1"
|
||||
>
|
||||
<span class="text-xs text-muted">{{
|
||||
getConflictMessage(conflict, t)
|
||||
@@ -110,7 +112,7 @@
|
||||
<!-- Extension List Wrapper -->
|
||||
<div
|
||||
v-if="conflictData.length > 0"
|
||||
class="flex min-h-8 w-full flex-col rounded-lg bg-secondary-background"
|
||||
class="flex min-h-8 w-full flex-col rounded-lg bg-base-background"
|
||||
>
|
||||
<div
|
||||
data-testid="conflict-dialog-panel-toggle"
|
||||
@@ -146,7 +148,7 @@
|
||||
<div
|
||||
v-for="conflictResult in conflictData"
|
||||
:key="conflictResult.package_id"
|
||||
class="hover:bg-alpha-azure-600-30 flex h-6 flex-shrink-0 items-center justify-between px-4"
|
||||
class="flex min-h-6 shrink-0 hover:bg-node-component-surface-hovered items-center justify-between px-4 py-1"
|
||||
>
|
||||
<span class="text-xs text-muted">
|
||||
{{ conflictResult.package_name }}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<template>
|
||||
<div class="flex w-full items-center justify-between p-4">
|
||||
<div class="flex h-12 w-full items-center justify-between pl-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Warning Icon -->
|
||||
<i class="icon-[lucide--triangle-alert] text-gold-600"></i>
|
||||
<p class="m-0 text-sm">
|
||||
<!-- Title -->
|
||||
<p class="text-base font-bold">
|
||||
{{ $t('manager.conflicts.title') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -34,13 +34,15 @@ import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useNodeConflictDialog } from '@/composables/useNodeConflictDialog'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment'
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
import { useConflictDetectionStore } from '@/workbench/extensions/manager/stores/conflictDetectionStore'
|
||||
import type { components as ManagerComponents } from '@/workbench/extensions/manager/types/generatedManagerTypes'
|
||||
|
||||
import { useImportFailedDetection } from '../../../composables/useImportFailedDetection'
|
||||
|
||||
const TOGGLE_DEBOUNCE_MS = 256
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
@@ -51,8 +53,9 @@ const { t } = useI18n()
|
||||
const { isPackEnabled, enablePack, disablePack, installedPacks } =
|
||||
useComfyManagerStore()
|
||||
const { getConflictsForPackageByID } = useConflictDetectionStore()
|
||||
const { show: showNodeConflictDialog } = useNodeConflictDialog()
|
||||
const { showNodeConflictDialog } = useDialogService()
|
||||
const { acknowledgmentState, markConflictsAsSeen } = useConflictAcknowledgment()
|
||||
const { showImportFailedDialog } = useImportFailedDetection(nodePack.id || '')
|
||||
|
||||
const isLoading = ref(false)
|
||||
|
||||
@@ -81,23 +84,36 @@ const canToggleDirectly = computed(() => {
|
||||
const showConflictModal = (skipModalDismissed: boolean) => {
|
||||
let modal_dismissed = acknowledgmentState.value.modal_dismissed
|
||||
if (skipModalDismissed) modal_dismissed = false
|
||||
|
||||
if (packageConflict.value && !modal_dismissed) {
|
||||
showNodeConflictDialog({
|
||||
conflictedPackages: [packageConflict.value],
|
||||
buttonText: !isEnabled.value
|
||||
? t('manager.conflicts.enableAnyway')
|
||||
: t('manager.conflicts.understood'),
|
||||
onButtonClick: async () => {
|
||||
if (!isEnabled.value) {
|
||||
await handleEnable()
|
||||
// Check if there's an import failed conflict first
|
||||
const hasImportFailed = packageConflict.value.conflicts.some(
|
||||
(conflict) => conflict.type === 'import_failed'
|
||||
)
|
||||
if (hasImportFailed) {
|
||||
// Show import failed dialog instead of general conflict dialog
|
||||
showImportFailedDialog(() => {
|
||||
markConflictsAsSeen()
|
||||
})
|
||||
} else {
|
||||
// Show general conflict dialog for other types of conflicts
|
||||
showNodeConflictDialog({
|
||||
conflictedPackages: [packageConflict.value],
|
||||
buttonText: !isEnabled.value
|
||||
? t('manager.conflicts.enableAnyway')
|
||||
: t('manager.conflicts.understood'),
|
||||
onButtonClick: async () => {
|
||||
if (!isEnabled.value) {
|
||||
await handleEnable()
|
||||
}
|
||||
},
|
||||
dialogComponentProps: {
|
||||
onClose: () => {
|
||||
markConflictsAsSeen()
|
||||
}
|
||||
}
|
||||
},
|
||||
dialogComponentProps: {
|
||||
onClose: () => {
|
||||
markConflictsAsSeen()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
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 { useNodeConflictDialog } from '@/composables/useNodeConflictDialog'
|
||||
import { t } from '@/i18n'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
@@ -54,7 +54,8 @@ const {
|
||||
}>()
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
const { show: showNodeConflictDialog } = useNodeConflictDialog()
|
||||
const { showNodeConflictDialog } = useDialogService()
|
||||
const { t } = useI18n()
|
||||
|
||||
// Check if any of the packs are currently being installed
|
||||
const isInstalling = computed(() => {
|
||||
|
||||
@@ -1,43 +1,37 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<button
|
||||
v-if="importFailedInfo"
|
||||
class="inline-flex cursor-pointer items-center justify-end gap-1 border-none bg-transparent outline-none"
|
||||
@click="showImportFailedDialog"
|
||||
>
|
||||
<i class="pi pi-code text-base"></i>
|
||||
<span class="text-sm text-base-foreground">{{
|
||||
t('serverStart.openLogs')
|
||||
}}</span>
|
||||
</button>
|
||||
<div
|
||||
v-for="(conflict, index) in conflictResult?.conflicts || []"
|
||||
:key="index"
|
||||
class="rounded-md bg-yellow-800/20 p-3"
|
||||
class="rounded-md bg-secondary-background/60 px-2 py-1"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 text-sm break-words">
|
||||
<!-- Import failed conflicts show detailed error message -->
|
||||
<template v-if="conflict.type === 'import_failed'">
|
||||
<div
|
||||
v-if="conflict.required_value"
|
||||
class="max-h-64 overflow-x-hidden scrollbar-custom overflow-y-auto rounded px-2"
|
||||
>
|
||||
<p class="text-xs text-muted-foreground break-all font-mono">
|
||||
{{ conflict.required_value }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Other conflict types use standard message -->
|
||||
<template v-else>
|
||||
<div class="text-sm break-words">
|
||||
{{ getConflictMessage(conflict, $t) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { useImportFailedDetection } from '@/workbench/extensions/manager/composables/useImportFailedDetection'
|
||||
import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
|
||||
import { getConflictMessage } from '@/workbench/extensions/manager/utils/conflictMessageUtil'
|
||||
|
||||
const { nodePack, conflictResult } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
const { conflictResult } = defineProps<{
|
||||
conflictResult: ConflictDetectionResult | null | undefined
|
||||
}>()
|
||||
const packageId = computed(() => nodePack?.id || '')
|
||||
const { importFailedInfo, showImportFailedDialog } =
|
||||
useImportFailedDetection(packageId)
|
||||
</script>
|
||||
|
||||
@@ -410,7 +410,7 @@ describe('useConflictDetection', () => {
|
||||
mockComfyManagerService.getImportFailInfoBulk
|
||||
).mockResolvedValue({
|
||||
'fail-pack': {
|
||||
msg: 'Import error',
|
||||
error: 'Import error',
|
||||
name: 'fail-pack',
|
||||
path: '/path/to/pack'
|
||||
} as any // The actual API returns different structure than types
|
||||
@@ -428,7 +428,7 @@ describe('useConflictDetection', () => {
|
||||
// Import failure should match the actual implementation
|
||||
expect(result.results[0].conflicts).toContainEqual({
|
||||
type: 'import_failed',
|
||||
current_value: 'installed',
|
||||
current_value: 'Import error',
|
||||
required_value: 'Import error'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -389,7 +389,10 @@ export function useConflictDetection() {
|
||||
* @returns Array of conflict detection results for failed imports
|
||||
*/
|
||||
function detectImportFailConflicts(
|
||||
importFailInfo: Record<string, { msg: string; name: string; path: string }>
|
||||
importFailInfo: Record<
|
||||
string,
|
||||
{ error?: string; traceback?: string } | null
|
||||
>
|
||||
): ConflictDetectionResult[] {
|
||||
const results: ConflictDetectionResult[] = []
|
||||
if (!importFailInfo || typeof importFailInfo !== 'object') {
|
||||
@@ -400,8 +403,11 @@ export function useConflictDetection() {
|
||||
for (const [packageId, failureInfo] of Object.entries(importFailInfo)) {
|
||||
if (failureInfo && typeof failureInfo === 'object') {
|
||||
// Extract error information from Manager API response
|
||||
const errorMsg = failureInfo.msg || 'Unknown import error'
|
||||
const modulePath = failureInfo.path || ''
|
||||
const errorMsg = failureInfo.error || 'Unknown import error'
|
||||
const traceback = failureInfo.traceback || ''
|
||||
|
||||
// Combine error and traceback for display
|
||||
const fullErrorInfo = traceback || errorMsg
|
||||
|
||||
results.push({
|
||||
package_id: packageId,
|
||||
@@ -410,8 +416,8 @@ export function useConflictDetection() {
|
||||
conflicts: [
|
||||
{
|
||||
type: 'import_failed',
|
||||
current_value: 'installed',
|
||||
required_value: failureInfo.msg
|
||||
current_value: errorMsg,
|
||||
required_value: fullErrorInfo
|
||||
}
|
||||
],
|
||||
is_compatible: false
|
||||
@@ -420,8 +426,8 @@ export function useConflictDetection() {
|
||||
console.warn(
|
||||
`[ConflictDetection] Python import failure detected for ${packageId}:`,
|
||||
{
|
||||
path: modulePath,
|
||||
error: errorMsg
|
||||
error: errorMsg,
|
||||
hasTraceback: !!traceback
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,8 @@ describe('useImportFailedDetection', () => {
|
||||
>
|
||||
|
||||
mockDialogService = {
|
||||
showErrorDialog: vi.fn()
|
||||
showErrorDialog: vi.fn(),
|
||||
showImportFailedNodeDialog: vi.fn()
|
||||
} as unknown as ReturnType<typeof dialogService.useDialogService>
|
||||
|
||||
vi.mocked(comfyManagerStore.useComfyManagerStore).mockReturnValue(
|
||||
@@ -226,13 +227,22 @@ describe('useImportFailedDetection', () => {
|
||||
|
||||
showImportFailedDialog()
|
||||
|
||||
expect(mockDialogService.showErrorDialog).toHaveBeenCalledWith(
|
||||
expect.any(Error),
|
||||
{
|
||||
title: 'manager.failedToInstall',
|
||||
reportType: 'importFailedError'
|
||||
expect(mockDialogService.showImportFailedNodeDialog).toHaveBeenCalledWith({
|
||||
conflictedPackages: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
package_id: 'test-package',
|
||||
package_name: 'Test Package',
|
||||
conflicts: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: 'import_failed'
|
||||
})
|
||||
])
|
||||
})
|
||||
]),
|
||||
dialogComponentProps: {
|
||||
onClose: undefined
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle null packageId', () => {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { computed, unref } from 'vue'
|
||||
import type { ComputedRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
import { useConflictDetectionStore } from '@/workbench/extensions/manager/stores/conflictDetectionStore'
|
||||
import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
|
||||
import type {
|
||||
ConflictDetail,
|
||||
ConflictDetectionResult
|
||||
} from '@/workbench/extensions/manager/types/conflictDetectionTypes'
|
||||
|
||||
/**
|
||||
* Extracting import failed conflicts from conflict list
|
||||
@@ -24,22 +26,18 @@ function extractImportFailedConflicts(conflicts?: ConflictDetail[] | null) {
|
||||
* Creating import failed dialog
|
||||
*/
|
||||
function createImportFailedDialog() {
|
||||
const { t } = useI18n()
|
||||
const { showErrorDialog } = useDialogService()
|
||||
const { showImportFailedNodeDialog } = useDialogService()
|
||||
|
||||
return (importFailedInfo: ConflictDetail[] | null) => {
|
||||
if (importFailedInfo) {
|
||||
const errorMessage =
|
||||
importFailedInfo
|
||||
.map((conflict) => conflict.required_value)
|
||||
.filter(Boolean)
|
||||
.join('\n') || t('manager.importFailedGenericError')
|
||||
|
||||
const error = new Error(errorMessage)
|
||||
|
||||
showErrorDialog(error, {
|
||||
title: t('manager.failedToInstall'),
|
||||
reportType: 'importFailedError'
|
||||
return (
|
||||
conflictedPackages: ConflictDetectionResult[] | null,
|
||||
onClose?: () => void
|
||||
) => {
|
||||
if (conflictedPackages && conflictedPackages.length > 0) {
|
||||
showImportFailedNodeDialog({
|
||||
conflictedPackages,
|
||||
dialogComponentProps: {
|
||||
onClose
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -74,13 +72,16 @@ export function useImportFailedDetection(
|
||||
return importFailedInfo.value !== null
|
||||
})
|
||||
|
||||
const showImportFailedDialog = createImportFailedDialog()
|
||||
const openDialog = createImportFailedDialog()
|
||||
|
||||
return {
|
||||
importFailedInfo,
|
||||
importFailed,
|
||||
showImportFailedDialog: () =>
|
||||
showImportFailedDialog(importFailedInfo.value),
|
||||
showImportFailedDialog: (onClose?: () => void) => {
|
||||
if (conflicts.value) {
|
||||
openDialog([conflicts.value], onClose)
|
||||
}
|
||||
},
|
||||
isInstalled
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user