mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
## Summary
This PR removes `any` types from widgets, services, stores, and test
files, replacing them with proper TypeScript types.
### Key Changes
#### Type Safety Improvements
- Replaced `any` with `unknown`, explicit types, or proper interfaces
across widgets and services
- Added proper type imports (TgpuRoot, Point, StyleValue, etc.)
- Created typed interfaces (NumericWidgetOptions, TestWindow,
ImportFailureDetail, etc.)
- Fixed function return types to be non-nullable where appropriate
- Added type guards and null checks instead of non-null assertions
- Used `ComponentProps` from vue-component-type-helpers for component
testing
#### Widget System
- Added index signature to IWidgetOptions for Record compatibility
- Centralized disabled logic in WidgetInputNumberInput
- Moved template type assertions to computed properties
- Fixed ComboWidget getOptionLabel type assertions
- Improved remote widget type handling with runtime checks
#### Services & Stores
- Fixed getOrCreateViewer to return non-nullable values
- Updated addNodeOnGraph to use specific options type `{ pos?: Point }`
- Added proper type assertions for settings store retrieval
- Fixed executionIdToCurrentId return type (string | undefined)
#### Test Infrastructure
- Exported GraphOrSubgraph from litegraph barrel to avoid circular
dependencies
- Updated test fixtures with proper TypeScript types (TestInfo,
LGraphNode)
- Replaced loose Record types with ComponentProps in tests
- Added proper error handling in WebSocket fixture
#### Code Organization
- Created shared i18n-types module for locale data types
- Made ImportFailureDetail non-exported (internal use only)
- Added @public JSDoc tag to ElectronWindow type
- Fixed console.log usage in scripts to use allowed methods
### Files Changed
**Widgets & Components:**
-
src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue
-
src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue
-
src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue
- src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue
-
src/renderer/extensions/vueNodes/widgets/composables/useRemoteWidget.ts
- src/lib/litegraph/src/widgets/ComboWidget.ts
- src/lib/litegraph/src/types/widgets.ts
- src/components/common/LazyImage.vue
- src/components/load3d/Load3dViewerContent.vue
**Services & Stores:**
- src/services/litegraphService.ts
- src/services/load3dService.ts
- src/services/colorPaletteService.ts
- src/stores/maskEditorStore.ts
- src/stores/nodeDefStore.ts
- src/platform/settings/settingStore.ts
- src/platform/workflow/management/stores/workflowStore.ts
**Composables & Utils:**
- src/composables/node/useWatchWidget.ts
- src/composables/useCanvasDrop.ts
- src/utils/widgetPropFilter.ts
- src/utils/queueDisplay.ts
- src/utils/envUtil.ts
**Test Files:**
- browser_tests/fixtures/ComfyPage.ts
- browser_tests/fixtures/ws.ts
- browser_tests/tests/actionbar.spec.ts
-
src/workbench/extensions/manager/components/manager/skeleton/PackCardGridSkeleton.test.ts
- src/lib/litegraph/src/subgraph/subgraphUtils.test.ts
- src/components/rightSidePanel/shared.test.ts
- src/platform/cloud/subscription/composables/useSubscription.test.ts
-
src/platform/workflow/persistence/composables/useWorkflowPersistence.test.ts
**Scripts & Types:**
- scripts/i18n-types.ts (new shared module)
- scripts/diff-i18n.ts
- scripts/check-unused-i18n-keys.ts
- src/workbench/extensions/manager/types/conflictDetectionTypes.ts
- src/types/algoliaTypes.ts
- src/types/simplifiedWidget.ts
**Infrastructure:**
- src/lib/litegraph/src/litegraph.ts (added GraphOrSubgraph export)
- src/lib/litegraph/src/infrastructure/CustomEventTarget.ts
- src/platform/assets/services/assetService.ts
**Stories:**
- apps/desktop-ui/src/views/InstallView.stories.ts
- src/components/queue/job/JobDetailsPopover.stories.ts
**Extension Manager:**
- src/workbench/extensions/manager/composables/useConflictDetection.ts
- src/workbench/extensions/manager/composables/useManagerQueue.ts
- src/workbench/extensions/manager/services/comfyManagerService.ts
- src/workbench/extensions/manager/utils/conflictMessageUtil.ts
### Testing
- [x] All TypeScript type checking passes (`pnpm typecheck`)
- [x] ESLint passes without errors (`pnpm lint`)
- [x] Format checks pass (`pnpm format:check`)
- [x] Knip (unused exports) passes (`pnpm knip`)
- [x] Pre-commit and pre-push hooks pass
Part of the "Road to No Explicit Any" initiative.
### Previous PRs in this series:
- Part 2: #7401
- Part 3: #7935
- Part 4: #7970
- Part 5: #8064
- Part 6: #8083
- Part 7: #8092
- Part 8 Group 1: #8253
- Part 8 Group 2: #8258
- Part 8 Group 3: #8304
- Part 8 Group 4: #8314
- Part 8 Group 5: #8329
- Part 8 Group 6: #8344
- Part 8 Group 7: #8459
- Part 8 Group 8: #8496
- Part 9: #8498
- Part 10: #8499
---------
Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com>
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
134 lines
3.1 KiB
Vue
134 lines
3.1 KiB
Vue
<template>
|
|
<div
|
|
ref="containerRef"
|
|
class="relative flex h-full w-full items-center justify-center overflow-hidden"
|
|
:class="containerClass"
|
|
>
|
|
<Skeleton
|
|
v-if="!isImageLoaded"
|
|
width="100%"
|
|
height="100%"
|
|
class="absolute inset-0"
|
|
/>
|
|
<img
|
|
v-if="cachedSrc"
|
|
:src="cachedSrc"
|
|
:alt="alt"
|
|
draggable="false"
|
|
:class="imageClass"
|
|
:style="imageStyle"
|
|
@load="onImageLoad"
|
|
@error="onImageError"
|
|
/>
|
|
<div
|
|
v-if="hasError"
|
|
class="absolute inset-0 flex items-center justify-center"
|
|
>
|
|
<img
|
|
src="/assets/images/default-template.png"
|
|
:alt="alt"
|
|
draggable="false"
|
|
:class="imageClass"
|
|
:style="imageStyle"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import Skeleton from 'primevue/skeleton'
|
|
import { computed, onUnmounted, ref, watch } from 'vue'
|
|
import type { StyleValue } from 'vue'
|
|
|
|
import { useIntersectionObserver } from '@/composables/useIntersectionObserver'
|
|
import { useMediaCache } from '@/services/mediaCacheService'
|
|
import type { ClassValue } from '@/utils/tailwindUtil'
|
|
|
|
const {
|
|
src,
|
|
alt = '',
|
|
containerClass = '',
|
|
imageClass = '',
|
|
imageStyle,
|
|
rootMargin = '300px'
|
|
} = defineProps<{
|
|
src: string
|
|
alt?: string
|
|
containerClass?: ClassValue
|
|
imageClass?: ClassValue
|
|
imageStyle?: StyleValue
|
|
rootMargin?: string
|
|
}>()
|
|
|
|
const containerRef = ref<HTMLElement | null>(null)
|
|
const isIntersecting = ref(false)
|
|
const isImageLoaded = ref(false)
|
|
const hasError = ref(false)
|
|
const cachedSrc = ref<string | undefined>(undefined)
|
|
|
|
const { getCachedMedia, acquireUrl, releaseUrl } = useMediaCache()
|
|
|
|
// Use intersection observer to detect when the image container comes into view
|
|
useIntersectionObserver(
|
|
containerRef,
|
|
(entries) => {
|
|
const entry = entries[0]
|
|
isIntersecting.value = entry?.isIntersecting ?? false
|
|
},
|
|
{
|
|
rootMargin,
|
|
threshold: 0.1
|
|
}
|
|
)
|
|
|
|
// Only start loading the image when it's in view
|
|
const shouldLoad = computed(() => isIntersecting.value)
|
|
|
|
watch(
|
|
shouldLoad,
|
|
async (shouldLoadVal) => {
|
|
if (shouldLoadVal && src && !cachedSrc.value && !hasError.value) {
|
|
try {
|
|
const cachedMedia = await getCachedMedia(src)
|
|
if (cachedMedia.error) {
|
|
hasError.value = true
|
|
} else if (cachedMedia.objectUrl) {
|
|
const acquiredUrl = acquireUrl(src)
|
|
cachedSrc.value = acquiredUrl || cachedMedia.objectUrl
|
|
} else {
|
|
cachedSrc.value = src
|
|
}
|
|
} catch (error) {
|
|
console.warn('Failed to load cached media:', error)
|
|
cachedSrc.value = src
|
|
}
|
|
} else if (!shouldLoadVal) {
|
|
if (cachedSrc.value?.startsWith('blob:')) {
|
|
releaseUrl(src)
|
|
}
|
|
// Hide image when out of view
|
|
isImageLoaded.value = false
|
|
cachedSrc.value = undefined
|
|
hasError.value = false
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
|
|
const onImageLoad = () => {
|
|
isImageLoaded.value = true
|
|
hasError.value = false
|
|
}
|
|
|
|
const onImageError = () => {
|
|
hasError.value = true
|
|
isImageLoaded.value = false
|
|
}
|
|
|
|
onUnmounted(() => {
|
|
if (cachedSrc.value?.startsWith('blob:')) {
|
|
releaseUrl(src)
|
|
}
|
|
})
|
|
</script>
|