mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
Merge branch 'main' into refactor/rebuild-select-reka-ui-9700
This commit is contained in:
25
.github/workflows/ci-tests-storybook.yaml
vendored
25
.github/workflows/ci-tests-storybook.yaml
vendored
@@ -4,6 +4,8 @@ name: 'CI: Tests Storybook'
|
||||
on:
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
# Post starting comment for non-forked PRs
|
||||
@@ -138,6 +140,29 @@ jobs:
|
||||
"${{ github.head_ref }}" \
|
||||
"completed"
|
||||
|
||||
# Deploy Storybook to production URL on main branch push
|
||||
deploy-production:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
|
||||
- name: Build Storybook
|
||||
run: pnpm build-storybook
|
||||
|
||||
- name: Deploy to Cloudflare Pages (production)
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
run: |
|
||||
npx wrangler@^4.0.0 pages deploy storybook-static \
|
||||
--project-name=comfy-storybook \
|
||||
--branch=main
|
||||
|
||||
# Update comment with Chromatic URLs for version-bump branches
|
||||
update-comment-with-chromatic:
|
||||
needs: [chromatic-deployment, deploy-and-comment]
|
||||
|
||||
1
global.d.ts
vendored
1
global.d.ts
vendored
@@ -35,6 +35,7 @@ interface Window {
|
||||
mixpanel_token?: string
|
||||
posthog_project_token?: string
|
||||
posthog_api_host?: string
|
||||
posthog_debug?: boolean
|
||||
require_whitelist?: boolean
|
||||
subscription_required?: boolean
|
||||
max_upload_size?: number
|
||||
|
||||
@@ -43,6 +43,9 @@ export function useAppSetDefaultView() {
|
||||
const extra = (app.rootGraph.extra ??= {})
|
||||
extra.linearMode = openAsApp
|
||||
workflow.changeTracker?.checkState()
|
||||
useTelemetry()?.trackDefaultViewSet({
|
||||
default_view: openAsApp ? 'app' : 'graph'
|
||||
})
|
||||
closeDialog()
|
||||
showAppliedDialog(openAsApp)
|
||||
}
|
||||
|
||||
@@ -324,7 +324,8 @@ function safeWidgetMapper(
|
||||
}
|
||||
: (extractWidgetDisplayOptions(effectiveWidget) ?? options),
|
||||
slotMetadata: slotInfo,
|
||||
slotName: name !== widget.name ? widget.name : undefined
|
||||
slotName: name !== widget.name ? widget.name : undefined,
|
||||
tooltip: widget.tooltip
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
|
||||
@@ -27,7 +27,7 @@ function addDynamicCombo(node: LGraphNode, inputs: DynamicInputs) {
|
||||
`${namePrefix}.${depth}.${inputIndex}`,
|
||||
Array.isArray(input)
|
||||
? ['COMFY_DYNAMICCOMBO_V3', { options: getSpec(input, depth + 1) }]
|
||||
: [input, {}]
|
||||
: [input, { tooltip: `${groupIndex}` }]
|
||||
])
|
||||
return {
|
||||
key: `${groupIndex}`,
|
||||
@@ -106,6 +106,13 @@ describe('Dynamic Combos', () => {
|
||||
expect(node.inputs[1].name).toBe('0.0.0.0')
|
||||
expect(node.inputs[3].name).toBe('2.2.0.0')
|
||||
})
|
||||
test('Dynamically added widgets have tooltips', () => {
|
||||
const node = testNode()
|
||||
addDynamicCombo(node, [['INT'], ['STRING']])
|
||||
expect.soft(node.widgets[1].tooltip).toBe('0')
|
||||
node.widgets[0].value = '1'
|
||||
expect.soft(node.widgets[1].tooltip).toBe('1')
|
||||
})
|
||||
})
|
||||
describe('Autogrow', () => {
|
||||
const inputsSpec = { required: { image: ['IMAGE', {}] } }
|
||||
|
||||
@@ -31,6 +31,7 @@ export type RemoteConfig = {
|
||||
mixpanel_token?: string
|
||||
posthog_project_token?: string
|
||||
posthog_api_host?: string
|
||||
posthog_debug?: boolean
|
||||
subscription_required?: boolean
|
||||
server_health_alert?: ServerHealthAlert
|
||||
max_upload_size?: number
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { AuditLog } from '@/services/customerEventsService'
|
||||
import type {
|
||||
AuthMetadata,
|
||||
BeginCheckoutMetadata,
|
||||
DefaultViewSetMetadata,
|
||||
EnterLinearMetadata,
|
||||
ShareFlowMetadata,
|
||||
ExecutionErrorMetadata,
|
||||
@@ -27,7 +28,8 @@ import type {
|
||||
TemplateMetadata,
|
||||
UiButtonClickMetadata,
|
||||
WorkflowCreatedMetadata,
|
||||
WorkflowImportMetadata
|
||||
WorkflowImportMetadata,
|
||||
WorkflowSavedMetadata
|
||||
} from './types'
|
||||
|
||||
/**
|
||||
@@ -157,6 +159,14 @@ export class TelemetryRegistry implements TelemetryDispatcher {
|
||||
this.dispatch((provider) => provider.trackWorkflowOpened?.(metadata))
|
||||
}
|
||||
|
||||
trackWorkflowSaved(metadata: WorkflowSavedMetadata): void {
|
||||
this.dispatch((provider) => provider.trackWorkflowSaved?.(metadata))
|
||||
}
|
||||
|
||||
trackDefaultViewSet(metadata: DefaultViewSetMetadata): void {
|
||||
this.dispatch((provider) => provider.trackDefaultViewSet?.(metadata))
|
||||
}
|
||||
|
||||
trackEnterLinear(metadata: EnterLinearMetadata): void {
|
||||
this.dispatch((provider) => provider.trackEnterLinear?.(metadata))
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { getExecutionContext } from '../../utils/getExecutionContext'
|
||||
import type {
|
||||
AuthMetadata,
|
||||
CreditTopupMetadata,
|
||||
DefaultViewSetMetadata,
|
||||
EnterLinearMetadata,
|
||||
ShareFlowMetadata,
|
||||
ExecutionContext,
|
||||
@@ -40,7 +41,8 @@ import type {
|
||||
TemplateMetadata,
|
||||
UiButtonClickMetadata,
|
||||
WorkflowCreatedMetadata,
|
||||
WorkflowImportMetadata
|
||||
WorkflowImportMetadata,
|
||||
WorkflowSavedMetadata
|
||||
} from '../../types'
|
||||
import { remoteConfig } from '@/platform/remoteConfig/remoteConfig'
|
||||
import type { RemoteConfig } from '@/platform/remoteConfig/types'
|
||||
@@ -359,6 +361,14 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
||||
this.trackEvent(TelemetryEvents.WORKFLOW_OPENED, metadata)
|
||||
}
|
||||
|
||||
trackWorkflowSaved(metadata: WorkflowSavedMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.WORKFLOW_SAVED, metadata)
|
||||
}
|
||||
|
||||
trackDefaultViewSet(metadata: DefaultViewSetMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.DEFAULT_VIEW_SET, metadata)
|
||||
}
|
||||
|
||||
trackEnterLinear(metadata: EnterLinearMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.ENTER_LINEAR_MODE, metadata)
|
||||
}
|
||||
|
||||
@@ -80,27 +80,30 @@ describe('PostHogTelemetryProvider', () => {
|
||||
createProvider()
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
expect(hoisted.mockInit).toHaveBeenCalledWith('phc_test_token', {
|
||||
api_host: 'https://t.comfy.org',
|
||||
autocapture: false,
|
||||
capture_pageview: false,
|
||||
capture_pageleave: false,
|
||||
persistence: 'localStorage+cookie',
|
||||
debug: false
|
||||
})
|
||||
expect(hoisted.mockInit).toHaveBeenCalledWith(
|
||||
'phc_test_token',
|
||||
expect.objectContaining({
|
||||
api_host: 'https://t.comfy.org',
|
||||
ui_host: 'https://us.posthog.com',
|
||||
autocapture: false,
|
||||
capture_pageview: false,
|
||||
capture_pageleave: false,
|
||||
persistence: 'localStorage+cookie'
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('uses custom api_host from config when provided', async () => {
|
||||
it('enables debug mode when posthog_debug is true in config', async () => {
|
||||
window.__CONFIG__ = {
|
||||
posthog_project_token: 'phc_test_token',
|
||||
posthog_api_host: 'https://custom.host.com'
|
||||
posthog_debug: true
|
||||
} as typeof window.__CONFIG__
|
||||
new PostHogTelemetryProvider()
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
expect(hoisted.mockInit).toHaveBeenCalledWith(
|
||||
'phc_test_token',
|
||||
expect.objectContaining({ api_host: 'https://custom.host.com' })
|
||||
expect.objectContaining({ debug: true })
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { RemoteConfig } from '@/platform/remoteConfig/types'
|
||||
|
||||
import type {
|
||||
AuthMetadata,
|
||||
DefaultViewSetMetadata,
|
||||
EnterLinearMetadata,
|
||||
ShareFlowMetadata,
|
||||
ExecutionContext,
|
||||
@@ -34,7 +35,8 @@ import type {
|
||||
TemplateMetadata,
|
||||
UiButtonClickMetadata,
|
||||
WorkflowCreatedMetadata,
|
||||
WorkflowImportMetadata
|
||||
WorkflowImportMetadata,
|
||||
WorkflowSavedMetadata
|
||||
} from '../../types'
|
||||
import { TelemetryEvents } from '../../types'
|
||||
import { getExecutionContext } from '../../utils/getExecutionContext'
|
||||
@@ -102,11 +104,14 @@ export class PostHogTelemetryProvider implements TelemetryProvider {
|
||||
this.posthog!.init(apiKey, {
|
||||
api_host:
|
||||
window.__CONFIG__?.posthog_api_host || 'https://t.comfy.org',
|
||||
ui_host: 'https://us.posthog.com',
|
||||
autocapture: false,
|
||||
capture_pageview: false,
|
||||
capture_pageleave: false,
|
||||
persistence: 'localStorage+cookie',
|
||||
debug: import.meta.env.VITE_POSTHOG_DEBUG === 'true'
|
||||
debug:
|
||||
window.__CONFIG__?.posthog_debug ??
|
||||
import.meta.env.VITE_POSTHOG_DEBUG === 'true'
|
||||
})
|
||||
this.isInitialized = true
|
||||
this.flushEventQueue()
|
||||
@@ -344,6 +349,14 @@ export class PostHogTelemetryProvider implements TelemetryProvider {
|
||||
this.trackEvent(TelemetryEvents.WORKFLOW_OPENED, metadata)
|
||||
}
|
||||
|
||||
trackWorkflowSaved(metadata: WorkflowSavedMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.WORKFLOW_SAVED, metadata)
|
||||
}
|
||||
|
||||
trackDefaultViewSet(metadata: DefaultViewSetMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.DEFAULT_VIEW_SET, metadata)
|
||||
}
|
||||
|
||||
trackEnterLinear(metadata: EnterLinearMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.ENTER_LINEAR_MODE, metadata)
|
||||
}
|
||||
|
||||
@@ -149,6 +149,15 @@ export interface EnterLinearMetadata {
|
||||
source?: string
|
||||
}
|
||||
|
||||
export interface WorkflowSavedMetadata {
|
||||
is_app: boolean
|
||||
is_new: boolean
|
||||
}
|
||||
|
||||
export interface DefaultViewSetMetadata {
|
||||
default_view: 'app' | 'graph'
|
||||
}
|
||||
|
||||
type ShareFlowStep =
|
||||
| 'dialog_opened'
|
||||
| 'save_prompted'
|
||||
@@ -377,6 +386,8 @@ export interface TelemetryProvider {
|
||||
// Workflow management events
|
||||
trackWorkflowImported?(metadata: WorkflowImportMetadata): void
|
||||
trackWorkflowOpened?(metadata: WorkflowImportMetadata): void
|
||||
trackWorkflowSaved?(metadata: WorkflowSavedMetadata): void
|
||||
trackDefaultViewSet?(metadata: DefaultViewSetMetadata): void
|
||||
trackEnterLinear?(metadata: EnterLinearMetadata): void
|
||||
trackShareFlow?(metadata: ShareFlowMetadata): void
|
||||
|
||||
@@ -490,6 +501,8 @@ export const TelemetryEvents = {
|
||||
|
||||
// Workflow Creation
|
||||
WORKFLOW_CREATED: 'app:workflow_created',
|
||||
WORKFLOW_SAVED: 'app:workflow_saved',
|
||||
DEFAULT_VIEW_SET: 'app:default_view_set',
|
||||
|
||||
// Execution Lifecycle
|
||||
EXECUTION_START: 'execution_start',
|
||||
@@ -540,4 +553,6 @@ export type TelemetryEventProperties =
|
||||
| WorkflowCreatedMetadata
|
||||
| EnterLinearMetadata
|
||||
| ShareFlowMetadata
|
||||
| WorkflowSavedMetadata
|
||||
| DefaultViewSetMetadata
|
||||
| SubscriptionMetadata
|
||||
|
||||
@@ -149,6 +149,8 @@ export const useWorkflowService = () => {
|
||||
await openWorkflow(tempWorkflow)
|
||||
await workflowStore.saveWorkflow(tempWorkflow)
|
||||
}
|
||||
|
||||
useTelemetry()?.trackWorkflowSaved({ is_app: isApp, is_new: true })
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -189,6 +191,7 @@ export const useWorkflowService = () => {
|
||||
}
|
||||
|
||||
await workflowStore.saveWorkflow(workflow)
|
||||
useTelemetry()?.trackWorkflowSaved({ is_app: isApp, is_new: false })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -187,6 +187,46 @@ describe('useMinimapViewport', () => {
|
||||
expect(transform.height).toBeCloseTo(viewportHeight * 0.5) // 300 * 0.5 = 150
|
||||
})
|
||||
|
||||
it('should maintain strict reference equality for viewportTransform when canvas state is unchanged', () => {
|
||||
vi.mocked(calculateNodeBounds).mockReturnValue({
|
||||
minX: 0,
|
||||
minY: 0,
|
||||
maxX: 500,
|
||||
maxY: 400,
|
||||
width: 500,
|
||||
height: 400
|
||||
})
|
||||
|
||||
vi.mocked(enforceMinimumBounds).mockImplementation((bounds) => bounds)
|
||||
vi.mocked(calculateMinimapScale).mockReturnValue(0.5)
|
||||
|
||||
const canvasRef = ref(mockCanvas) as Ref<MinimapCanvas | null>
|
||||
const graphRef = ref(mockGraph) as Ref<LGraph | null>
|
||||
|
||||
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
|
||||
|
||||
mockCanvas.ds.scale = 2
|
||||
mockCanvas.ds.offset = [-100, -50]
|
||||
|
||||
viewport.updateBounds()
|
||||
viewport.updateCanvasDimensions()
|
||||
viewport.updateViewport()
|
||||
|
||||
const initialTransform = viewport.viewportTransform.value
|
||||
|
||||
viewport.updateViewport()
|
||||
const transformAfterIdle = viewport.viewportTransform.value
|
||||
|
||||
expect(transformAfterIdle).toBe(initialTransform)
|
||||
|
||||
mockCanvas.ds.offset = [-150, -50]
|
||||
viewport.updateViewport()
|
||||
const transformAfterPan = viewport.viewportTransform.value
|
||||
|
||||
expect(transformAfterPan).not.toBe(initialTransform)
|
||||
expect(transformAfterPan.x).not.toBe(initialTransform.x)
|
||||
})
|
||||
|
||||
it('should center view on world coordinates', () => {
|
||||
const canvasRef = ref(mockCanvas) as Ref<MinimapCanvas | null>
|
||||
const graphRef = ref(mockGraph) as Ref<LGraph | null>
|
||||
|
||||
@@ -90,12 +90,16 @@ export function useMinimapViewport(
|
||||
const centerOffsetX = (width - bounds.value.width * scale.value) / 2
|
||||
const centerOffsetY = (height - bounds.value.height * scale.value) / 2
|
||||
|
||||
viewportTransform.value = {
|
||||
x: (worldX - bounds.value.minX) * scale.value + centerOffsetX,
|
||||
y: (worldY - bounds.value.minY) * scale.value + centerOffsetY,
|
||||
width: viewportWidth * scale.value,
|
||||
height: viewportHeight * scale.value
|
||||
}
|
||||
const x = (worldX - bounds.value.minX) * scale.value + centerOffsetX
|
||||
const y = (worldY - bounds.value.minY) * scale.value + centerOffsetY
|
||||
const w = viewportWidth * scale.value
|
||||
const h = viewportHeight * scale.value
|
||||
|
||||
const curr = viewportTransform.value
|
||||
if (curr.x === x && curr.y === y && curr.width === w && curr.height === h)
|
||||
return
|
||||
|
||||
viewportTransform.value = { x, y, width: w, height: h }
|
||||
}
|
||||
|
||||
const updateBounds = () => {
|
||||
|
||||
@@ -15,7 +15,34 @@ import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import WidgetSelectDropdown from '@/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue'
|
||||
import { createMockWidget } from './widgetTestUtils'
|
||||
|
||||
const mockCheckState = vi.hoisted(() => vi.fn())
|
||||
const mockAssetsData = vi.hoisted(() => ({ items: [] as AssetItem[] }))
|
||||
|
||||
vi.mock('@/platform/workflow/management/stores/workflowStore', async () => {
|
||||
const actual = await vi.importActual(
|
||||
'@/platform/workflow/management/stores/workflowStore'
|
||||
)
|
||||
return {
|
||||
...actual,
|
||||
useWorkflowStore: () => ({
|
||||
activeWorkflow: {
|
||||
changeTracker: {
|
||||
checkState: mockCheckState
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
fetchApi: vi.fn(),
|
||||
apiURL: vi.fn((url: string) => url),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock(
|
||||
'@/renderer/extensions/vueNodes/widgets/composables/useAssetWidgetData',
|
||||
() => ({
|
||||
@@ -456,3 +483,69 @@ describe('WidgetSelectDropdown cloud asset mode (COM-14333)', () => {
|
||||
expect(selectedSet.has('missing-missing_model.safetensors')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('WidgetSelectDropdown undo tracking', () => {
|
||||
interface UndoTrackingInstance extends ComponentPublicInstance {
|
||||
updateSelectedItems: (selectedSet: Set<string>) => void
|
||||
handleFilesUpdate: (files: File[]) => Promise<void>
|
||||
}
|
||||
|
||||
const mountForUndo = (
|
||||
widget: SimplifiedWidget<string | undefined>,
|
||||
modelValue: string | undefined
|
||||
): VueWrapper<UndoTrackingInstance> => {
|
||||
return mount(WidgetSelectDropdown, {
|
||||
props: {
|
||||
widget,
|
||||
modelValue,
|
||||
assetKind: 'image',
|
||||
allowUpload: true,
|
||||
uploadFolder: 'input'
|
||||
},
|
||||
global: {
|
||||
plugins: [PrimeVue, createTestingPinia(), i18n]
|
||||
}
|
||||
}) as unknown as VueWrapper<UndoTrackingInstance>
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockCheckState.mockClear()
|
||||
})
|
||||
|
||||
it('calls checkState after dropdown selection changes modelValue', () => {
|
||||
const widget = createMockWidget<string | undefined>({
|
||||
value: 'img_001.png',
|
||||
name: 'test_image',
|
||||
type: 'combo',
|
||||
options: { values: ['img_001.png', 'photo_abc.jpg'] }
|
||||
})
|
||||
const wrapper = mountForUndo(widget, 'img_001.png')
|
||||
|
||||
wrapper.vm.updateSelectedItems(new Set(['input-1']))
|
||||
|
||||
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['photo_abc.jpg'])
|
||||
expect(mockCheckState).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('calls checkState after file upload completes', async () => {
|
||||
const { api } = await import('@/scripts/api')
|
||||
vi.mocked(api.fetchApi).mockResolvedValue({
|
||||
status: 200,
|
||||
json: () => Promise.resolve({ name: 'uploaded.png', subfolder: '' })
|
||||
} as Response)
|
||||
|
||||
const widget = createMockWidget<string | undefined>({
|
||||
value: 'img_001.png',
|
||||
name: 'test_image',
|
||||
type: 'combo',
|
||||
options: { values: ['img_001.png'] }
|
||||
})
|
||||
const wrapper = mountForUndo(widget, 'img_001.png')
|
||||
|
||||
const file = new File(['test'], 'uploaded.png', { type: 'image/png' })
|
||||
await wrapper.vm.handleFilesUpdate([file])
|
||||
|
||||
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['uploaded.png'])
|
||||
expect(mockCheckState).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
getAssetFilename
|
||||
} from '@/platform/assets/utils/assetMetadataUtils'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import FormDropdown from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdown.vue'
|
||||
import type {
|
||||
FilterOption,
|
||||
@@ -376,6 +377,7 @@ function updateSelectedItems(selectedItems: Set<string>) {
|
||||
return
|
||||
}
|
||||
modelValue.value = name
|
||||
useWorkflowStore().activeWorkflow?.changeTracker?.checkState()
|
||||
}
|
||||
|
||||
const uploadFile = async (
|
||||
@@ -450,6 +452,9 @@ async function handleFilesUpdate(files: File[]) {
|
||||
if (props.widget.callback) {
|
||||
props.widget.callback(uploadedPaths[0])
|
||||
}
|
||||
|
||||
// 5. Snapshot undo state so the image change gets its own undo entry
|
||||
useWorkflowStore().activeWorkflow?.changeTracker?.checkState()
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error)
|
||||
toastStore.addAlert(`Upload failed: ${error}`)
|
||||
|
||||
@@ -40,6 +40,7 @@ function addMultilineWidget(
|
||||
})
|
||||
|
||||
widget.element = inputEl
|
||||
widget.inputEl = inputEl
|
||||
widget.options.minNodeSize = [400, 200]
|
||||
|
||||
inputEl.addEventListener('input', (event) => {
|
||||
|
||||
@@ -216,14 +216,18 @@ export const useLitegraphService = () => {
|
||||
*/
|
||||
function addNodeInput(node: LGraphNode, inputSpec: InputSpec) {
|
||||
addInputSocket(node, inputSpec)
|
||||
addInputWidget(node, inputSpec)
|
||||
addInputWidget(node, inputSpec, { dynamic: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Add a widget to the node. For both primitive types and custom widgets
|
||||
* (unless `socketless`), an input socket is also added.
|
||||
*/
|
||||
function addInputWidget(node: LGraphNode, inputSpec: InputSpec) {
|
||||
function addInputWidget(
|
||||
node: LGraphNode,
|
||||
inputSpec: InputSpec,
|
||||
{ dynamic }: { dynamic?: boolean } = {}
|
||||
) {
|
||||
const widgetInputSpec = { ...inputSpec }
|
||||
if (inputSpec.widgetType) {
|
||||
widgetInputSpec.type = inputSpec.widgetType
|
||||
@@ -254,6 +258,7 @@ export const useLitegraphService = () => {
|
||||
advanced: inputSpec.advanced,
|
||||
hidden: inputSpec.hidden
|
||||
})
|
||||
if (dynamic) widget.tooltip = inputSpec.tooltip
|
||||
}
|
||||
|
||||
if (!widget?.options?.socketless) {
|
||||
|
||||
@@ -74,6 +74,8 @@ export interface SimplifiedWidget<
|
||||
/** Optional input specification backing this widget */
|
||||
spec?: InputSpecV2
|
||||
|
||||
tooltip?: string
|
||||
|
||||
controlWidget?: SafeControlWidget
|
||||
}
|
||||
|
||||
|
||||
@@ -402,7 +402,7 @@ const isUnresolvedTab = computed(
|
||||
)
|
||||
|
||||
// Map of tab IDs to their empty state i18n key suffixes
|
||||
const tabEmptyStateKeys: Partial<Record<ManagerTab, string>> = {
|
||||
const tabEmptyStateKeys: Record<string, string> = {
|
||||
[ManagerTab.AllInstalled]: 'allInstalled',
|
||||
[ManagerTab.UpdateAvailable]: 'updateAvailable',
|
||||
[ManagerTab.Conflicting]: 'conflicting',
|
||||
@@ -410,12 +410,27 @@ const tabEmptyStateKeys: Partial<Record<ManagerTab, string>> = {
|
||||
[ManagerTab.Missing]: 'missing'
|
||||
}
|
||||
|
||||
const managerApiDependentTabs = new Set<string>([
|
||||
ManagerTab.AllInstalled,
|
||||
ManagerTab.UpdateAvailable,
|
||||
ManagerTab.Conflicting,
|
||||
ManagerTab.NotInstalled,
|
||||
ManagerTab.Missing
|
||||
])
|
||||
|
||||
const isManagerErrorRelevant = computed(() => {
|
||||
const tabId = selectedTab.value?.id
|
||||
return (
|
||||
!!comfyManagerStore.error && !!tabId && managerApiDependentTabs.has(tabId)
|
||||
)
|
||||
})
|
||||
|
||||
// Empty state messages based on current tab and search state
|
||||
const emptyStateTitle = computed(() => {
|
||||
if (comfyManagerStore.error) return t('manager.errorConnecting')
|
||||
if (isManagerErrorRelevant.value) return t('manager.errorConnecting')
|
||||
if (searchQuery.value) return t('manager.noResultsFound')
|
||||
|
||||
const tabId = selectedTab.value?.id as ManagerTab | undefined
|
||||
const tabId = selectedTab.value?.id
|
||||
const emptyStateKey = tabId ? tabEmptyStateKeys[tabId] : undefined
|
||||
|
||||
return emptyStateKey
|
||||
@@ -424,7 +439,7 @@ const emptyStateTitle = computed(() => {
|
||||
})
|
||||
|
||||
const emptyStateMessage = computed(() => {
|
||||
if (comfyManagerStore.error) return t('manager.tryAgainLater')
|
||||
if (isManagerErrorRelevant.value) return t('manager.tryAgainLater')
|
||||
if (searchQuery.value) {
|
||||
const baseMessage = t('manager.tryDifferentSearch')
|
||||
if (isLegacyManagerSearch.value) {
|
||||
@@ -433,7 +448,7 @@ const emptyStateMessage = computed(() => {
|
||||
return baseMessage
|
||||
}
|
||||
|
||||
const tabId = selectedTab.value?.id as ManagerTab | undefined
|
||||
const tabId = selectedTab.value?.id
|
||||
const emptyStateKey = tabId ? tabEmptyStateKeys[tabId] : undefined
|
||||
|
||||
return emptyStateKey
|
||||
|
||||
Reference in New Issue
Block a user