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>
425 lines
11 KiB
TypeScript
425 lines
11 KiB
TypeScript
// eslint-disable-next-line storybook/no-renderer-packages
|
|
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
import type { ElectronAPI } from '@comfyorg/comfyui-electron-types'
|
|
import { nextTick, provide } from 'vue'
|
|
import type { ElectronWindow } from '@/utils/envUtil'
|
|
import { createMemoryHistory, createRouter } from 'vue-router'
|
|
|
|
import InstallView from './InstallView.vue'
|
|
|
|
// Create a mock router for stories
|
|
const createMockRouter = () =>
|
|
createRouter({
|
|
history: createMemoryHistory(),
|
|
routes: [
|
|
{ path: '/', component: { template: '<div>Home</div>' } },
|
|
{
|
|
path: '/server-start',
|
|
component: { template: '<div>Server Start</div>' }
|
|
},
|
|
{
|
|
path: '/manual-configuration',
|
|
component: { template: '<div>Manual Configuration</div>' }
|
|
}
|
|
]
|
|
})
|
|
|
|
const meta: Meta<typeof InstallView> = {
|
|
title: 'Desktop/Views/InstallView',
|
|
component: InstallView,
|
|
parameters: {
|
|
layout: 'fullscreen',
|
|
backgrounds: {
|
|
default: 'dark',
|
|
values: [
|
|
{ name: 'dark', value: '#0a0a0a' },
|
|
{ name: 'neutral-900', value: '#171717' },
|
|
{ name: 'neutral-950', value: '#0a0a0a' }
|
|
]
|
|
}
|
|
},
|
|
decorators: [
|
|
(story) => {
|
|
// Create router for this story
|
|
const router = createMockRouter()
|
|
|
|
// Mock electron API
|
|
;(window as ElectronWindow).electronAPI = {
|
|
getPlatform: () => 'darwin',
|
|
Config: {
|
|
getDetectedGpu: () => Promise.resolve('mps')
|
|
},
|
|
Events: {
|
|
trackEvent: (
|
|
_eventName: string,
|
|
_data?: Record<string, unknown>
|
|
) => {}
|
|
},
|
|
installComfyUI: (
|
|
_options: Parameters<ElectronAPI['installComfyUI']>[0]
|
|
) => {},
|
|
changeTheme: (_theme: Parameters<ElectronAPI['changeTheme']>[0]) => {},
|
|
getSystemPaths: () =>
|
|
Promise.resolve({
|
|
defaultInstallPath: '/Users/username/ComfyUI'
|
|
}),
|
|
validateInstallPath: () =>
|
|
Promise.resolve({
|
|
isValid: true,
|
|
exists: false,
|
|
canWrite: true,
|
|
freeSpace: 100000000000,
|
|
requiredSpace: 10000000000,
|
|
isNonDefaultDrive: false
|
|
}),
|
|
validateComfyUISource: () =>
|
|
Promise.resolve({
|
|
isValid: true
|
|
}),
|
|
showDirectoryPicker: () => Promise.resolve('/Users/username/ComfyUI')
|
|
}
|
|
|
|
return {
|
|
setup() {
|
|
// Provide router for all child components
|
|
provide('router', router)
|
|
return {
|
|
story
|
|
}
|
|
},
|
|
template: '<div style="width: 100vw; height: 100vh;"><story /></div>'
|
|
}
|
|
}
|
|
]
|
|
}
|
|
|
|
export default meta
|
|
type Story = StoryObj<typeof meta>
|
|
|
|
// Default story - start at GPU selection
|
|
export const GpuSelection: Story = {
|
|
render: () => ({
|
|
components: { InstallView },
|
|
setup() {
|
|
// The component will automatically start at step 1
|
|
return {}
|
|
},
|
|
template: '<InstallView />'
|
|
})
|
|
}
|
|
|
|
// Story showing the install location step
|
|
export const InstallLocation: Story = {
|
|
render: () => ({
|
|
components: { InstallView },
|
|
setup() {
|
|
return {}
|
|
},
|
|
async mounted() {
|
|
// Wait for component to be fully mounted
|
|
await nextTick()
|
|
|
|
// Select Apple Metal option to enable navigation
|
|
const hardwareOptions = this.$el.querySelectorAll(
|
|
'.p-selectbutton-option'
|
|
)
|
|
if (hardwareOptions.length > 0) {
|
|
hardwareOptions[0].click() // Click Apple Metal (first option)
|
|
}
|
|
|
|
await nextTick()
|
|
|
|
// Click Next to go to step 2
|
|
const buttons = Array.from(
|
|
this.$el.querySelectorAll('button')
|
|
) as HTMLButtonElement[]
|
|
const nextBtn = buttons.find((btn) => btn.textContent?.includes('Next'))
|
|
if (nextBtn) {
|
|
nextBtn.click()
|
|
}
|
|
},
|
|
template: '<InstallView />'
|
|
})
|
|
}
|
|
|
|
// Story showing the migration step (currently empty)
|
|
export const MigrationStep: Story = {
|
|
render: () => ({
|
|
components: { InstallView },
|
|
setup() {
|
|
return {}
|
|
},
|
|
async mounted() {
|
|
// Wait for component to be fully mounted
|
|
await nextTick()
|
|
|
|
// Select Apple Metal option to enable navigation
|
|
const hardwareOptions = this.$el.querySelectorAll(
|
|
'.p-selectbutton-option'
|
|
)
|
|
if (hardwareOptions.length > 0) {
|
|
hardwareOptions[0].click() // Click Apple Metal (first option)
|
|
}
|
|
|
|
await nextTick()
|
|
|
|
// Click Next to go to step 2
|
|
const buttons1 = Array.from(
|
|
this.$el.querySelectorAll('button')
|
|
) as HTMLButtonElement[]
|
|
const nextBtn1 = buttons1.find((btn) => btn.textContent?.includes('Next'))
|
|
if (nextBtn1) {
|
|
nextBtn1.click()
|
|
}
|
|
|
|
await nextTick()
|
|
|
|
// Click Next again to go to step 3
|
|
const buttons2 = Array.from(
|
|
this.$el.querySelectorAll('button')
|
|
) as HTMLButtonElement[]
|
|
const nextBtn2 = buttons2.find((btn) => btn.textContent?.includes('Next'))
|
|
if (nextBtn2) {
|
|
nextBtn2.click()
|
|
}
|
|
},
|
|
template: '<InstallView />'
|
|
})
|
|
}
|
|
|
|
// Story showing the desktop settings configuration
|
|
export const DesktopSettings: Story = {
|
|
render: () => ({
|
|
components: { InstallView },
|
|
setup() {
|
|
return {}
|
|
},
|
|
async mounted() {
|
|
// Wait for component to be fully mounted
|
|
await nextTick()
|
|
|
|
// Select Apple Metal option to enable navigation
|
|
const hardwareOptions = this.$el.querySelectorAll(
|
|
'.p-selectbutton-option'
|
|
)
|
|
if (hardwareOptions.length > 0) {
|
|
hardwareOptions[0].click() // Click Apple Metal (first option)
|
|
}
|
|
|
|
await nextTick()
|
|
|
|
// Click Next to go to step 2
|
|
const buttons1 = Array.from(
|
|
this.$el.querySelectorAll('button')
|
|
) as HTMLButtonElement[]
|
|
const nextBtn1 = buttons1.find((btn) => btn.textContent?.includes('Next'))
|
|
if (nextBtn1) {
|
|
nextBtn1.click()
|
|
}
|
|
|
|
await nextTick()
|
|
|
|
// Click Next again to go to step 3
|
|
const buttons2 = Array.from(
|
|
this.$el.querySelectorAll('button')
|
|
) as HTMLButtonElement[]
|
|
const nextBtn2 = buttons2.find((btn) => btn.textContent?.includes('Next'))
|
|
if (nextBtn2) {
|
|
nextBtn2.click()
|
|
}
|
|
|
|
await nextTick()
|
|
|
|
// Click Next again to go to step 4
|
|
const buttons3 = Array.from(
|
|
this.$el.querySelectorAll('button')
|
|
) as HTMLButtonElement[]
|
|
const nextBtn3 = buttons3.find((btn) => btn.textContent?.includes('Next'))
|
|
if (nextBtn3) {
|
|
nextBtn3.click()
|
|
}
|
|
},
|
|
template: '<InstallView />'
|
|
})
|
|
}
|
|
|
|
// Story with Windows platform (no Apple Metal option)
|
|
export const WindowsPlatform: Story = {
|
|
render: () => {
|
|
// Override the platform to Windows
|
|
;(window as ElectronWindow).electronAPI.getPlatform = () => 'win32'
|
|
;(window as ElectronWindow).electronAPI.Config.getDetectedGpu = () =>
|
|
Promise.resolve('nvidia')
|
|
|
|
return {
|
|
components: { InstallView },
|
|
setup() {
|
|
return {}
|
|
},
|
|
template: '<InstallView />'
|
|
}
|
|
}
|
|
}
|
|
|
|
// Story with macOS platform (Apple Metal option)
|
|
export const MacOSPlatform: Story = {
|
|
name: 'macOS Platform',
|
|
render: () => {
|
|
// Override the platform to macOS
|
|
;(window as ElectronWindow).electronAPI.getPlatform = () => 'darwin'
|
|
;(window as ElectronWindow).electronAPI.Config.getDetectedGpu = () =>
|
|
Promise.resolve('mps')
|
|
|
|
return {
|
|
components: { InstallView },
|
|
setup() {
|
|
return {}
|
|
},
|
|
template: '<InstallView />'
|
|
}
|
|
}
|
|
}
|
|
|
|
// Story with CPU selected
|
|
export const CpuSelected: Story = {
|
|
render: () => ({
|
|
components: { InstallView },
|
|
setup() {
|
|
return {}
|
|
},
|
|
async mounted() {
|
|
// Wait for component to be fully mounted
|
|
await nextTick()
|
|
|
|
// Find and click the CPU hardware option
|
|
const hardwareButtons = this.$el.querySelectorAll('.hardware-option')
|
|
// CPU is the button with "CPU" text
|
|
for (const button of hardwareButtons) {
|
|
if (button.textContent?.includes('CPU')) {
|
|
button.click()
|
|
break
|
|
}
|
|
}
|
|
},
|
|
template: '<InstallView />'
|
|
})
|
|
}
|
|
|
|
// Story with manual install selected
|
|
export const ManualInstall: Story = {
|
|
render: () => ({
|
|
components: { InstallView },
|
|
setup() {
|
|
return {}
|
|
},
|
|
async mounted() {
|
|
// Wait for component to be fully mounted
|
|
await nextTick()
|
|
|
|
// Find and click the Manual Install hardware option
|
|
const hardwareButtons = this.$el.querySelectorAll('.hardware-option')
|
|
// Manual Install is the button with "Manual Install" text
|
|
for (const button of hardwareButtons) {
|
|
if (button.textContent?.includes('Manual Install')) {
|
|
button.click()
|
|
break
|
|
}
|
|
}
|
|
},
|
|
template: '<InstallView />'
|
|
})
|
|
}
|
|
|
|
// Story with error state (invalid install path)
|
|
export const ErrorState: Story = {
|
|
render: () => {
|
|
// Override validation to return an error
|
|
;(window as ElectronWindow).electronAPI.validateInstallPath = () =>
|
|
Promise.resolve({
|
|
isValid: false,
|
|
exists: false,
|
|
canWrite: false,
|
|
freeSpace: 100000000000,
|
|
requiredSpace: 10000000000,
|
|
isNonDefaultDrive: false,
|
|
error: 'Story mock: Example error state'
|
|
})
|
|
|
|
return {
|
|
components: { InstallView },
|
|
setup() {
|
|
return {}
|
|
},
|
|
async mounted() {
|
|
// Wait for component to be fully mounted
|
|
await nextTick()
|
|
|
|
// Select Apple Metal option to enable navigation
|
|
const hardwareOptions = this.$el.querySelectorAll(
|
|
'.p-selectbutton-option'
|
|
)
|
|
if (hardwareOptions.length > 0) {
|
|
hardwareOptions[0].click() // Click Apple Metal (first option)
|
|
}
|
|
|
|
await nextTick()
|
|
|
|
// Click Next to go to step 2 where error will be shown
|
|
const buttons = Array.from(
|
|
this.$el.querySelectorAll('button')
|
|
) as HTMLButtonElement[]
|
|
const nextBtn = buttons.find((btn) => btn.textContent?.includes('Next'))
|
|
if (nextBtn) {
|
|
nextBtn.click()
|
|
}
|
|
},
|
|
template: '<InstallView />'
|
|
}
|
|
}
|
|
}
|
|
|
|
// Story with warning state (non-default drive)
|
|
export const WarningState: Story = {
|
|
render: () => {
|
|
// Override validation to return a warning about non-default drive
|
|
;(window as ElectronWindow).electronAPI.validateInstallPath = () =>
|
|
Promise.resolve({
|
|
isValid: true,
|
|
exists: false,
|
|
canWrite: true,
|
|
freeSpace: 500_000_000_000,
|
|
requiredSpace: 10_000_000_000,
|
|
isNonDefaultDrive: true
|
|
})
|
|
|
|
return {
|
|
components: { InstallView },
|
|
setup() {
|
|
return {}
|
|
},
|
|
async mounted() {
|
|
// Wait for component to be fully mounted
|
|
await nextTick()
|
|
|
|
// Select Apple Metal option to enable navigation
|
|
const hardwareOptions = this.$el.querySelectorAll('.hardware-option')
|
|
if (hardwareOptions.length > 0) {
|
|
hardwareOptions[0].click() // Click Apple Metal (first option)
|
|
}
|
|
|
|
await nextTick()
|
|
|
|
// Click Next to go to step 2 where warning will be shown
|
|
const buttons = Array.from(
|
|
this.$el.querySelectorAll('button')
|
|
) as HTMLButtonElement[]
|
|
const nextBtn = buttons.find((btn) => btn.textContent?.includes('Next'))
|
|
if (nextBtn) {
|
|
nextBtn.click()
|
|
}
|
|
},
|
|
template: '<InstallView />'
|
|
}
|
|
}
|
|
}
|