diff --git a/.github/workflows/ci-tests-e2e.yaml b/.github/workflows/ci-tests-e2e.yaml index 90bf4112c..c1e0af411 100644 --- a/.github/workflows/ci-tests-e2e.yaml +++ b/.github/workflows/ci-tests-e2e.yaml @@ -144,9 +144,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v5 - # Setup pnpm/node to run playwright merge-reports (no browsers needed) - - name: Setup frontend - uses: ./.github/actions/setup-frontend + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 - name: Download blob reports uses: actions/download-artifact@v4 @@ -158,10 +159,10 @@ jobs: - name: Merge into HTML Report run: | # Generate HTML report - pnpm exec playwright merge-reports --reporter=html ./all-blob-reports + pnpm dlx @playwright/test merge-reports --reporter=html ./all-blob-reports # Generate JSON report separately with explicit output path PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \ - pnpm exec playwright merge-reports --reporter=json ./all-blob-reports + pnpm dlx @playwright/test merge-reports --reporter=json ./all-blob-reports - name: Upload HTML report uses: actions/upload-artifact@v4 diff --git a/.github/workflows/release-biweekly-comfyui.yaml b/.github/workflows/release-biweekly-comfyui.yaml index c0f57822b..6eb45a00e 100644 --- a/.github/workflows/release-biweekly-comfyui.yaml +++ b/.github/workflows/release-biweekly-comfyui.yaml @@ -69,7 +69,7 @@ jobs: - name: Checkout ComfyUI (sparse) uses: actions/checkout@v5 with: - repository: comfyanonymous/ComfyUI + repository: Comfy-Org/ComfyUI sparse-checkout: | requirements.txt path: comfyui @@ -184,7 +184,7 @@ jobs: # Note: This only affects the local checkout, NOT the fork's master branch # We only push the automation branch, leaving the fork's master untouched echo "Fetching upstream master..." - if ! git fetch https://github.com/comfyanonymous/ComfyUI.git master; then + if ! git fetch https://github.com/Comfy-Org/ComfyUI.git master; then echo "Failed to fetch upstream master" exit 1 fi @@ -257,7 +257,7 @@ jobs: # Extract fork owner from repository name FORK_OWNER=$(echo "$COMFYUI_FORK" | cut -d'/' -f1) - echo "Creating PR from ${COMFYUI_FORK} to comfyanonymous/ComfyUI" + echo "Creating PR from ${COMFYUI_FORK} to Comfy-Org/ComfyUI" # Configure git git config user.name "github-actions[bot]" @@ -288,7 +288,7 @@ jobs: # Try to create PR, ignore error if it already exists if ! gh pr create \ - --repo comfyanonymous/ComfyUI \ + --repo Comfy-Org/ComfyUI \ --head "${FORK_OWNER}:${BRANCH}" \ --base master \ --title "Bump comfyui-frontend-package to ${{ needs.resolve-version.outputs.target_version }}" \ @@ -297,7 +297,7 @@ jobs: # Check if PR already exists set +e - EXISTING_PR=$(gh pr list --repo comfyanonymous/ComfyUI --head "${FORK_OWNER}:${BRANCH}" --json number --jq '.[0].number' 2>&1) + EXISTING_PR=$(gh pr list --repo Comfy-Org/ComfyUI --head "${FORK_OWNER}:${BRANCH}" --json number --jq '.[0].number' 2>&1) PR_LIST_EXIT=$? set -e @@ -318,7 +318,7 @@ jobs: run: | echo "## ComfyUI PR Created" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "Draft PR created in comfyanonymous/ComfyUI" >> $GITHUB_STEP_SUMMARY + echo "Draft PR created in Comfy-Org/ComfyUI" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### PR Body:" >> $GITHUB_STEP_SUMMARY cat pr-body.txt >> $GITHUB_STEP_SUMMARY diff --git a/browser_tests/fixtures/ComfyPage.ts b/browser_tests/fixtures/ComfyPage.ts index 08cdd5cce..2f51533ce 100644 --- a/browser_tests/fixtures/ComfyPage.ts +++ b/browser_tests/fixtures/ComfyPage.ts @@ -3,7 +3,7 @@ import { test as base, expect } from '@playwright/test' import dotenv from 'dotenv' import * as fs from 'fs' -import type { LGraphNode } from '../../src/lib/litegraph/src/litegraph' +import type { LGraphNode, LGraph } from '../../src/lib/litegraph/src/litegraph' import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema' import type { KeyCombo } from '../../src/schemas/keyBindingSchema' import type { useWorkspaceStore } from '../../src/stores/workspaceStore' @@ -1591,14 +1591,29 @@ export class ComfyPage { return window['app'].graph.nodes }) } - async getNodeRefsByType(type: string): Promise { + async waitForGraphNodes(count: number) { + await this.page.waitForFunction((count) => { + return window['app']?.canvas.graph?.nodes?.length === count + }, count) + } + async getNodeRefsByType( + type: string, + includeSubgraph: boolean = false + ): Promise { return Promise.all( ( - await this.page.evaluate((type) => { - return window['app'].graph.nodes - .filter((n: LGraphNode) => n.type === type) - .map((n: LGraphNode) => n.id) - }, type) + await this.page.evaluate( + ({ type, includeSubgraph }) => { + const graph = ( + includeSubgraph ? window['app'].canvas.graph : window['app'].graph + ) as LGraph + const nodes = graph.nodes + return nodes + .filter((n: LGraphNode) => n.type === type) + .map((n: LGraphNode) => n.id) + }, + { type, includeSubgraph } + ) ).map((id: NodeId) => this.getNodeRefById(id)) ) } diff --git a/browser_tests/fixtures/VueNodeHelpers.ts b/browser_tests/fixtures/VueNodeHelpers.ts index e08b39bd7..3c11cfda2 100644 --- a/browser_tests/fixtures/VueNodeHelpers.ts +++ b/browser_tests/fixtures/VueNodeHelpers.ts @@ -159,8 +159,18 @@ export class VueNodeHelpers { getInputNumberControls(widget: Locator) { return { input: widget.locator('input'), - incrementButton: widget.locator('button').first(), - decrementButton: widget.locator('button').nth(1) + decrementButton: widget.getByTestId('decrement'), + incrementButton: widget.getByTestId('increment') } } + + /** + * Enter the subgraph of a node. + * @param nodeId - The ID of the node to enter the subgraph of. If not provided, the first matched subgraph will be entered. + */ + async enterSubgraph(nodeId?: string): Promise { + const locator = nodeId ? this.getNodeLocator(nodeId) : this.page + const editButton = locator.getByTestId('subgraph-enter-button') + await editButton.click() + } } diff --git a/browser_tests/tests/mobileBaseline.spec.ts b/browser_tests/tests/mobileBaseline.spec.ts index 21be3ca94..ff766a434 100644 --- a/browser_tests/tests/mobileBaseline.spec.ts +++ b/browser_tests/tests/mobileBaseline.spec.ts @@ -22,8 +22,14 @@ test.describe('Mobile Baseline Snapshots', () => { test('@mobile settings dialog', async ({ comfyPage }) => { await comfyPage.settingDialog.open() await comfyPage.nextFrame() + await expect(comfyPage.settingDialog.root).toHaveScreenshot( - 'mobile-settings-dialog.png' + 'mobile-settings-dialog.png', + { + mask: [ + comfyPage.settingDialog.root.getByTestId('current-user-indicator') + ] + } ) }) }) diff --git a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-settings-dialog-mobile-chrome-linux.png b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-settings-dialog-mobile-chrome-linux.png index 207d037bf..d6c21bb98 100644 Binary files a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-settings-dialog-mobile-chrome-linux.png and b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-settings-dialog-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/propertiesPanel/propertiesPanel.spec.ts b/browser_tests/tests/propertiesPanel/propertiesPanel.spec.ts index 9ff32c8a4..ff452b54a 100644 --- a/browser_tests/tests/propertiesPanel/propertiesPanel.spec.ts +++ b/browser_tests/tests/propertiesPanel/propertiesPanel.spec.ts @@ -8,13 +8,11 @@ test.describe('Properties panel', () => { const { propertiesPanel } = comfyPage.menu - await expect(propertiesPanel.panelTitle).toContainText( - 'No node(s) selected' - ) + await expect(propertiesPanel.panelTitle).toContainText('Workflow Overview') await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)']) - await expect(propertiesPanel.panelTitle).toContainText('3 nodes selected') + await expect(propertiesPanel.panelTitle).toContainText('3 items selected') await expect(propertiesPanel.root.getByText('KSampler')).toHaveCount(1) await expect( propertiesPanel.root.getByText('CLIP Text Encode (Prompt)') diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-chromium-linux.png index 07ff7a096..885e46356 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-pinned-node-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-pinned-node-chromium-linux.png index 7ecb4123e..39e3ade03 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-pinned-node-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-pinned-node-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-chromium-linux.png index 07ff7a096..885e46356 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png index 98db3de78..0dcab673a 100644 Binary files a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png and b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png index b83a4ed59..7ac84c627 100644 Binary files a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png and b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png index f30ba7467..a642edb35 100644 Binary files a/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts index 09a78fb6e..315758f5c 100644 --- a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts @@ -102,7 +102,7 @@ test.describe('Vue Node Link Interaction', () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) - await comfyPage.setup() + // await comfyPage.setup() await comfyPage.loadWorkflow('vueNodes/simple-triple') await comfyPage.vueNodes.waitForNodes() await fitToViewInstant(comfyPage) @@ -993,4 +993,51 @@ test.describe('Vue Node Link Interaction', () => { expect(linked).toBe(true) }) }) + + test('Dragging from subgraph input connects to correct slot', async ({ + comfyPage, + comfyMouse + }) => { + // Setup workflow with a KSampler node + await comfyPage.executeCommand('Comfy.NewBlankWorkflow') + await comfyPage.waitForGraphNodes(0) + await comfyPage.executeCommand('Workspace.SearchBox.Toggle') + await comfyPage.nextFrame() + await comfyPage.searchBox.fillAndSelectFirstNode('KSampler') + await comfyPage.waitForGraphNodes(1) + + // Convert the KSampler node to a subgraph + let ksamplerNode = (await comfyPage.getNodeRefsByType('KSampler'))?.[0] + await comfyPage.vueNodes.selectNode(String(ksamplerNode.id)) + await comfyPage.executeCommand('Comfy.Graph.ConvertToSubgraph') + + // Enter the subgraph + await comfyPage.vueNodes.enterSubgraph() + await fitToViewInstant(comfyPage) + + // Get the KSampler node inside the subgraph + ksamplerNode = (await comfyPage.getNodeRefsByType('KSampler', true))?.[0] + const positiveInput = await ksamplerNode.getInput(1) + const negativeInput = await ksamplerNode.getInput(2) + + const positiveInputPos = await getSlotCenter( + comfyPage.page, + ksamplerNode.id, + 1, + true + ) + + const sourceSlot = await comfyPage.getSubgraphInputSlot() + const calculatedSourcePos = await sourceSlot.getOpenSlotPosition() + + await comfyMouse.move(calculatedSourcePos) + await comfyMouse.drag(positiveInputPos) + await comfyMouse.drop() + + // Verify connection went to the correct slot + const positiveLinks = await positiveInput.getLinkCount() + const negativeLinks = await negativeInput.getLinkCount() + expect(positiveLinks).toBe(1) + expect(negativeLinks).toBe(0) + }) }) diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png index 8c0051b30..01d5994ad 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png index 3f07068c9..2e52a3827 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png index 2085c39b3..f637fce10 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png index a2332e2d4..16a48ace3 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png index bf2c01fc8..56f48867c 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png index a43c024dd..51ff6727b 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png index a9221a404..a8645017b 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png index 63a8c81d7..2a29eb590 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-linux.png index 783f0dc0c..8ccc65f98 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-linux.png index 8610dab8b..434c540e1 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-linux.png index c4d5979d0..6dc2d6637 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-linux.png index d43f10dde..1fec6b259 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png index 2cc234ffb..06d0e7c84 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png index 658ab5340..8fe5cd25b 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png index cfb61ef10..44c99675b 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png index 1e3fd5359..04790555d 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png index 880c63eca..759e50ca7 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png index b18d31a89..89194527f 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png differ diff --git a/browser_tests/tests/widget.spec.ts b/browser_tests/tests/widget.spec.ts index c8448f36c..075854860 100644 --- a/browser_tests/tests/widget.spec.ts +++ b/browser_tests/tests/widget.spec.ts @@ -194,7 +194,10 @@ test.describe('Image widget', () => { const comboEntry = comfyPage.page.getByRole('menuitem', { name: 'image32x32.webp' }) - await comboEntry.click({ noWaitAfter: true }) + await comboEntry.click() + + // Stabilization for the image swap + await comfyPage.nextFrame() // Expect the image preview to change automatically await expect(comfyPage.canvas).toHaveScreenshot( diff --git a/browser_tests/tests/widget.spec.ts-snapshots/image-preview-changed-by-combo-value-chromium-linux.png b/browser_tests/tests/widget.spec.ts-snapshots/image-preview-changed-by-combo-value-chromium-linux.png index c639941a7..224d671df 100644 Binary files a/browser_tests/tests/widget.spec.ts-snapshots/image-preview-changed-by-combo-value-chromium-linux.png and b/browser_tests/tests/widget.spec.ts-snapshots/image-preview-changed-by-combo-value-chromium-linux.png differ diff --git a/package.json b/package.json index 8444f4c8a..ed29f1fe3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@comfyorg/comfyui-frontend", "private": true, - "version": "1.37.10", + "version": "1.38.1", "type": "module", "repository": "https://github.com/Comfy-Org/ComfyUI_frontend", "homepage": "https://comfy.org", diff --git a/packages/design-system/src/css/style.css b/packages/design-system/src/css/style.css index 551e26c62..df8fe4a23 100644 --- a/packages/design-system/src/css/style.css +++ b/packages/design-system/src/css/style.css @@ -247,6 +247,7 @@ --inverted-background-hover: var(--color-charcoal-600); --warning-background: var(--color-gold-400); --warning-background-hover: var(--color-gold-500); + --success-background: var(--color-jade-600); --border-default: var(--color-smoke-600); --border-subtle: var(--color-smoke-400); --muted-background: var(--color-smoke-700); @@ -372,6 +373,7 @@ --inverted-background-hover: var(--color-smoke-200); --warning-background: var(--color-gold-600); --warning-background-hover: var(--color-gold-500); + --success-background: var(--color-jade-600); --border-default: var(--color-charcoal-200); --border-subtle: var(--color-charcoal-300); --muted-background: var(--color-charcoal-100); @@ -516,6 +518,7 @@ --color-inverted-background-hover: var(--inverted-background-hover); --color-warning-background: var(--warning-background); --color-warning-background-hover: var(--warning-background-hover); + --color-success-background: var(--success-background); --color-border-default: var(--border-default); --color-border-subtle: var(--border-subtle); --color-muted-background: var(--muted-background); diff --git a/src/assets/css/style.css b/src/assets/css/style.css index 627b129e4..301161e5d 100644 --- a/src/assets/css/style.css +++ b/src/assets/css/style.css @@ -1 +1,21 @@ -@import '@comfyorg/design-system/css/style.css'; \ No newline at end of file +@import '@comfyorg/design-system/css/style.css'; + +@media (prefers-reduced-motion: no-preference) { + /* List transition animations */ + .list-scale-move, + .list-scale-enter-active, + .list-scale-leave-active { + transition: opacity 150ms ease, transform 150ms ease; + } + + .list-scale-enter-from, + .list-scale-leave-to { + opacity: 0; + transform: scale(70%); + } + + .list-scale-leave-active { + position: absolute; + width: 100%; + } +} diff --git a/src/components/button/MoreButton.vue b/src/components/button/MoreButton.vue index d192efb90..0ffea76ce 100644 --- a/src/components/button/MoreButton.vue +++ b/src/components/button/MoreButton.vue @@ -1,6 +1,11 @@ + + diff --git a/src/components/ui/TypeformPopoverButton.vue b/src/components/ui/TypeformPopoverButton.vue new file mode 100644 index 000000000..e6808e8bc --- /dev/null +++ b/src/components/ui/TypeformPopoverButton.vue @@ -0,0 +1,29 @@ + + diff --git a/src/components/ui/ZoomPane.vue b/src/components/ui/ZoomPane.vue new file mode 100644 index 000000000..60ea6533b --- /dev/null +++ b/src/components/ui/ZoomPane.vue @@ -0,0 +1,59 @@ + + diff --git a/src/components/widget/nav/NavItem.vue b/src/components/widget/nav/NavItem.vue index c564dfcae..8c20ad929 100644 --- a/src/components/widget/nav/NavItem.vue +++ b/src/components/widget/nav/NavItem.vue @@ -1,5 +1,10 @@ diff --git a/src/composables/graph/useGraphHierarchy.test.ts b/src/composables/graph/useGraphHierarchy.test.ts new file mode 100644 index 000000000..510d50989 --- /dev/null +++ b/src/composables/graph/useGraphHierarchy.test.ts @@ -0,0 +1,133 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import type { LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph' +import * as measure from '@/lib/litegraph/src/measure' +import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' + +import { useGraphHierarchy } from './useGraphHierarchy' + +vi.mock('@/renderer/core/canvas/canvasStore') + +describe('useGraphHierarchy', () => { + let mockCanvasStore: ReturnType + let mockNode: LGraphNode + let mockGroups: LGraphGroup[] + + beforeEach(() => { + mockNode = { + boundingRect: [100, 100, 50, 50] + } as unknown as LGraphNode + + mockGroups = [] + + mockCanvasStore = { + canvas: { + graph: { + groups: mockGroups + } + } + } as any + + vi.mocked(useCanvasStore).mockReturnValue(mockCanvasStore) + }) + + describe('findParentGroup', () => { + it('returns null when no groups exist', () => { + const { findParentGroup } = useGraphHierarchy() + + const result = findParentGroup(mockNode) + + expect(result).toBeNull() + }) + + it('returns null when node is not in any group', () => { + const group = { + boundingRect: [0, 0, 50, 50] + } as unknown as LGraphGroup + mockGroups.push(group) + + vi.spyOn(measure, 'containsCentre').mockReturnValue(false) + + const { findParentGroup } = useGraphHierarchy() + const result = findParentGroup(mockNode) + + expect(result).toBeNull() + }) + + it('returns the only group when node is in exactly one group', () => { + const group = { + boundingRect: [0, 0, 200, 200] + } as unknown as LGraphGroup + mockGroups.push(group) + + vi.spyOn(measure, 'containsCentre').mockReturnValue(true) + + const { findParentGroup } = useGraphHierarchy() + const result = findParentGroup(mockNode) + + expect(result).toBe(group) + }) + + it('returns the smallest group when node is in multiple groups', () => { + const largeGroup = { + boundingRect: [0, 0, 300, 300] + } as unknown as LGraphGroup + const smallGroup = { + boundingRect: [50, 50, 100, 100] + } as unknown as LGraphGroup + mockGroups.push(largeGroup, smallGroup) + + vi.spyOn(measure, 'containsCentre').mockReturnValue(true) + vi.spyOn(measure, 'containsRect').mockReturnValue(false) + + const { findParentGroup } = useGraphHierarchy() + const result = findParentGroup(mockNode) + + expect(result).toBe(smallGroup) + }) + + it('returns the inner group when one group contains another', () => { + const outerGroup = { + boundingRect: [0, 0, 300, 300] + } as unknown as LGraphGroup + const innerGroup = { + boundingRect: [50, 50, 100, 100] + } as unknown as LGraphGroup + mockGroups.push(outerGroup, innerGroup) + + vi.spyOn(measure, 'containsCentre').mockReturnValue(true) + vi.spyOn(measure, 'containsRect').mockImplementation( + (container, contained) => { + // outerGroup contains innerGroup + if (container === outerGroup.boundingRect) { + return contained === innerGroup.boundingRect + } + return false + } + ) + + const { findParentGroup } = useGraphHierarchy() + const result = findParentGroup(mockNode) + + expect(result).toBe(innerGroup) + }) + + it('handles null canvas gracefully', () => { + mockCanvasStore.canvas = null as any + + const { findParentGroup } = useGraphHierarchy() + const result = findParentGroup(mockNode) + + expect(result).toBeNull() + }) + + it('handles null graph gracefully', () => { + mockCanvasStore.canvas!.graph = null as any + + const { findParentGroup } = useGraphHierarchy() + const result = findParentGroup(mockNode) + + expect(result).toBeNull() + }) + }) +}) diff --git a/src/composables/graph/useGraphHierarchy.ts b/src/composables/graph/useGraphHierarchy.ts new file mode 100644 index 000000000..14e2e310f --- /dev/null +++ b/src/composables/graph/useGraphHierarchy.ts @@ -0,0 +1,57 @@ +import type { LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph' +import { containsCentre, containsRect } from '@/lib/litegraph/src/measure' +import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' + +/** + * Composable for working with graph hierarchy, specifically group containment. + */ +export function useGraphHierarchy() { + const canvasStore = useCanvasStore() + + /** + * Finds the smallest group that contains the center of a node's bounding box. + * When multiple groups contain the node, returns the one with the smallest area. + * + * TODO: This traverses the entire graph and could be very slow; needs optimization. + * Consider spatial indexing or caching for large graphs. + * + * @param node - The node to find the parent group for + * @returns The parent group if found, otherwise null + */ + function findParentGroup(node: LGraphNode): LGraphGroup | null { + const graphGroups = (canvasStore.canvas?.graph?.groups ?? + []) as LGraphGroup[] + + let parent: LGraphGroup | null = null + + for (const group of graphGroups) { + const groupRect = group.boundingRect + if (!containsCentre(groupRect, node.boundingRect)) continue + + if (!parent) { + parent = group + continue + } + + const parentRect = parent.boundingRect + const candidateInsideParent = containsRect(parentRect, groupRect) + const parentInsideCandidate = containsRect(groupRect, parentRect) + + if (candidateInsideParent && !parentInsideCandidate) { + parent = group + continue + } + + const candidateArea = groupRect[2] * groupRect[3] + const parentArea = parentRect[2] * parentRect[3] + + if (candidateArea < parentArea) parent = group + } + + return parent + } + + return { + findParentGroup + } +} diff --git a/src/composables/graph/useGraphNodeManager.ts b/src/composables/graph/useGraphNodeManager.ts index 786e23f4e..e71e4cf36 100644 --- a/src/composables/graph/useGraphNodeManager.ts +++ b/src/composables/graph/useGraphNodeManager.ts @@ -104,7 +104,7 @@ function widgetWithVueTrack( return { get() {}, set() {} } }) } -export function useReactiveWidgetValue(widget: IBaseWidget) { +function useReactiveWidgetValue(widget: IBaseWidget) { widgetWithVueTrack(widget) widget.vueTrack() return widget.value @@ -120,12 +120,59 @@ function getControlWidget(widget: IBaseWidget): SafeControlWidget | undefined { update: (value) => (cagWidget.value = normalizeControlOption(value)) } } + function getNodeType(node: LGraphNode, widget: IBaseWidget) { if (!node.isSubgraphNode() || !isProxyWidget(widget)) return undefined const subNode = node.subgraph.getNodeById(widget._overlay.nodeId) return subNode?.type } +/** + * Shared widget enhancements used by both safeWidgetMapper and Right Side Panel + */ +interface SharedWidgetEnhancements { + /** Reactive widget value that updates when the widget changes */ + value: WidgetValue + /** Control widget for seed randomization/increment/decrement */ + controlWidget?: SafeControlWidget + /** Input specification from node definition */ + spec?: InputSpec + /** Node type (for subgraph promoted widgets) */ + nodeType?: string + /** Border style for promoted/advanced widgets */ + borderStyle?: string + /** Widget label */ + label?: string + /** Widget options */ + options?: Record +} + +/** + * Extracts common widget enhancements shared across different rendering contexts. + * This function centralizes the logic for extracting metadata and reactive values + * from widgets, ensuring consistency between Nodes 2.0 and Right Side Panel. + */ +export function getSharedWidgetEnhancements( + node: LGraphNode, + widget: IBaseWidget +): SharedWidgetEnhancements { + const nodeDefStore = useNodeDefStore() + + return { + value: useReactiveWidgetValue(widget), + controlWidget: getControlWidget(widget), + spec: nodeDefStore.getInputSpecForWidget(node, widget.name), + nodeType: getNodeType(node, widget), + borderStyle: widget.promoted + ? 'ring ring-component-node-widget-promoted' + : widget.advanced + ? 'ring ring-component-node-widget-advanced' + : undefined, + label: widget.label, + options: widget.options + } +} + /** * Validates that a value is a valid WidgetValue type */ @@ -157,20 +204,17 @@ const normalizeWidgetValue = (value: unknown): WidgetValue => { return undefined } -export function safeWidgetMapper( +function safeWidgetMapper( node: LGraphNode, slotMetadata: Map ): (widget: IBaseWidget) => SafeWidgetData { - const nodeDefStore = useNodeDefStore() return function (widget) { try { - const spec = nodeDefStore.getInputSpecForWidget(node, widget.name) + // Get shared enhancements used by both Nodes 2.0 and Right Side Panel + const sharedEnhancements = getSharedWidgetEnhancements(node, widget) const slotInfo = slotMetadata.get(widget.name) - const borderStyle = widget.promoted - ? 'ring ring-component-node-widget-promoted' - : widget.advanced - ? 'ring ring-component-node-widget-advanced' - : undefined + + // Wrapper callback specific to Nodes 2.0 rendering const callback = (v: unknown) => { const value = normalizeWidgetValue(v) widget.value = value ?? undefined @@ -185,16 +229,10 @@ export function safeWidgetMapper( return { name: widget.name, type: widget.type, - value: useReactiveWidgetValue(widget), - borderStyle, + ...sharedEnhancements, callback, - controlWidget: getControlWidget(widget), hasLayoutSize: typeof widget.computeLayoutSize === 'function', isDOMWidget: isDOMWidget(widget), - label: widget.label, - nodeType: getNodeType(node, widget), - options: widget.options, - spec, slotMetadata: slotInfo } } catch (error) { @@ -207,15 +245,77 @@ export function safeWidgetMapper( } } -export function isValidWidgetValue(value: unknown): value is WidgetValue { - return ( - value === null || - value === undefined || - typeof value === 'string' || - typeof value === 'number' || - typeof value === 'boolean' || - typeof value === 'object' - ) +// Extract safe data from LiteGraph node for Vue consumption +export function extractVueNodeData(node: LGraphNode): VueNodeData { + // Determine subgraph ID - null for root graph, string for subgraphs + const subgraphId = + node.graph && 'id' in node.graph && node.graph !== node.graph.rootGraph + ? String(node.graph.id) + : null + // Extract safe widget data + const slotMetadata = new Map() + + const reactiveWidgets = shallowReactive(node.widgets ?? []) + Object.defineProperty(node, 'widgets', { + get() { + return reactiveWidgets + }, + set(v) { + reactiveWidgets.splice(0, reactiveWidgets.length, ...v) + } + }) + const reactiveInputs = shallowReactive(node.inputs ?? []) + Object.defineProperty(node, 'inputs', { + get() { + return reactiveInputs + }, + set(v) { + reactiveInputs.splice(0, reactiveInputs.length, ...v) + } + }) + + const safeWidgets = reactiveComputed(() => { + node.inputs?.forEach((input, index) => { + if (!input?.widget?.name) return + slotMetadata.set(input.widget.name, { + index, + linked: input.link != null + }) + }) + return node.widgets?.map(safeWidgetMapper(node, slotMetadata)) ?? [] + }) + + const nodeType = + node.type || + node.constructor?.comfyClass || + node.constructor?.title || + node.constructor?.name || + 'Unknown' + + const apiNode = node.constructor?.nodeData?.api_node ?? false + const badges = node.badges + + return { + id: String(node.id), + title: typeof node.title === 'string' ? node.title : '', + type: nodeType, + mode: node.mode || 0, + titleMode: node.title_mode, + selected: node.selected || false, + executing: false, // Will be updated separately based on execution state + subgraphId, + apiNode, + badges, + hasErrors: !!node.has_errors, + widgets: safeWidgets, + inputs: reactiveInputs, + outputs: node.outputs ? [...node.outputs] : undefined, + flags: node.flags ? { ...node.flags } : undefined, + color: node.color || undefined, + bgcolor: node.bgcolor || undefined, + resizable: node.resizable, + shape: node.shape + } } export function useGraphNodeManager(graph: LGraph): GraphNodeManager { @@ -251,79 +351,6 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager { } } - // Extract safe data from LiteGraph node for Vue consumption - function extractVueNodeData(node: LGraphNode): VueNodeData { - // Determine subgraph ID - null for root graph, string for subgraphs - const subgraphId = - node.graph && 'id' in node.graph && node.graph !== node.graph.rootGraph - ? String(node.graph.id) - : null - // Extract safe widget data - const slotMetadata = new Map() - - const reactiveWidgets = shallowReactive(node.widgets ?? []) - Object.defineProperty(node, 'widgets', { - get() { - return reactiveWidgets - }, - set(v) { - reactiveWidgets.splice(0, reactiveWidgets.length, ...v) - } - }) - const reactiveInputs = shallowReactive(node.inputs ?? []) - Object.defineProperty(node, 'inputs', { - get() { - return reactiveInputs - }, - set(v) { - reactiveInputs.splice(0, reactiveInputs.length, ...v) - } - }) - - const safeWidgets = reactiveComputed(() => { - node.inputs?.forEach((input, index) => { - if (!input?.widget?.name) return - slotMetadata.set(input.widget.name, { - index, - linked: input.link != null - }) - }) - return node.widgets?.map(safeWidgetMapper(node, slotMetadata)) ?? [] - }) - - const nodeType = - node.type || - node.constructor?.comfyClass || - node.constructor?.title || - node.constructor?.name || - 'Unknown' - - const apiNode = node.constructor?.nodeData?.api_node ?? false - const badges = node.badges - - return { - id: String(node.id), - title: typeof node.title === 'string' ? node.title : '', - type: nodeType, - mode: node.mode || 0, - titleMode: node.title_mode, - selected: node.selected || false, - executing: false, // Will be updated separately based on execution state - subgraphId, - apiNode, - badges, - hasErrors: !!node.has_errors, - widgets: safeWidgets, - inputs: reactiveInputs, - outputs: node.outputs ? [...node.outputs] : undefined, - flags: node.flags ? { ...node.flags } : undefined, - color: node.color || undefined, - bgcolor: node.bgcolor || undefined, - resizable: node.resizable, - shape: node.shape - } - } - // Get access to original LiteGraph node (non-reactive) const getNode = (id: string): LGraphNode | undefined => { return nodeRefs.get(id) diff --git a/src/composables/useCoreCommands.ts b/src/composables/useCoreCommands.ts index 2025eb66c..5bd569085 100644 --- a/src/composables/useCoreCommands.ts +++ b/src/composables/useCoreCommands.ts @@ -1235,7 +1235,12 @@ export function useCoreCommands(): ComfyCommand[] { id: 'Comfy.ToggleLinear', icon: 'pi pi-database', label: 'toggle linear mode', - function: () => (canvasStore.linearMode = !canvasStore.linearMode) + function: () => { + const newMode = !canvasStore.linearMode + app.rootGraph.extra.linearMode = newMode + workflowStore.activeWorkflow?.changeTracker?.checkState() + canvasStore.linearMode = newMode + } } ] diff --git a/src/composables/useFeatureFlags.ts b/src/composables/useFeatureFlags.ts index 3388140ad..4e7368bc6 100644 --- a/src/composables/useFeatureFlags.ts +++ b/src/composables/useFeatureFlags.ts @@ -16,6 +16,7 @@ export enum ServerFeatureFlag { PRIVATE_MODELS_ENABLED = 'private_models_enabled', ONBOARDING_SURVEY_ENABLED = 'onboarding_survey_enabled', HUGGINGFACE_MODEL_IMPORT_ENABLED = 'huggingface_model_import_enabled', + LINEAR_TOGGLE_ENABLED = 'linear_toggle_enabled', ASYNC_MODEL_UPLOAD_ENABLED = 'async_model_upload_enabled' } @@ -77,6 +78,12 @@ export function useFeatureFlags() { ) ) }, + get linearToggleEnabled() { + return ( + remoteConfig.value.linear_toggle_enabled ?? + api.getServerFeature(ServerFeatureFlag.LINEAR_TOGGLE_ENABLED, false) + ) + }, get asyncModelUploadEnabled() { return ( remoteConfig.value.async_model_upload_enabled ?? diff --git a/src/core/graph/subgraph/proxyWidget.test.ts b/src/core/graph/subgraph/proxyWidget.test.ts index c0d41d23e..5b961ad80 100644 --- a/src/core/graph/subgraph/proxyWidget.test.ts +++ b/src/core/graph/subgraph/proxyWidget.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test, vi } from 'vitest' import { registerProxyWidgets } from '@/core/graph/subgraph/proxyWidget' +import { promoteWidget } from '@/core/graph/subgraph/proxyWidgetUtils' import { parseProxyWidgets } from '@/core/schemas/proxyWidget' import { LGraphNode } from '@/lib/litegraph/src/litegraph' import type { LGraphCanvas, SubgraphNode } from '@/lib/litegraph/src/litegraph' @@ -118,4 +119,23 @@ describe('Subgraph proxyWidgets', () => { subgraphNode.widgets[0].computedHeight = 10 expect(subgraphNode.widgets[0].value).toBe('value') }) + test('Prevents duplicate promotion', () => { + const [subgraphNode, innerNodes] = setupSubgraph(1) + innerNodes[0].addWidget('text', 'stringWidget', 'value', () => {}) + + const widget = innerNodes[0].widgets![0] + + // Promote once + promoteWidget(innerNodes[0], widget, [subgraphNode]) + expect(subgraphNode.widgets.length).toBe(1) + expect(subgraphNode.properties.proxyWidgets).toHaveLength(1) + + // Try to promote again - should not create duplicate + promoteWidget(innerNodes[0], widget, [subgraphNode]) + expect(subgraphNode.widgets.length).toBe(1) + expect(subgraphNode.properties.proxyWidgets).toHaveLength(1) + expect(subgraphNode.properties.proxyWidgets).toStrictEqual([ + ['1', 'stringWidget'] + ]) + }) }) diff --git a/src/core/graph/subgraph/proxyWidgetUtils.ts b/src/core/graph/subgraph/proxyWidgetUtils.ts index 27961e54d..eafb0f1dd 100644 --- a/src/core/graph/subgraph/proxyWidgetUtils.ts +++ b/src/core/graph/subgraph/proxyWidgetUtils.ts @@ -29,8 +29,13 @@ export function promoteWidget( parents: SubgraphNode[] ) { for (const parent of parents) { + const existingProxyWidgets = getProxyWidgets(parent) + // Prevent duplicate promotion + if (existingProxyWidgets.some(matchesPropertyItem([node, widget]))) { + continue + } const proxyWidgets = [ - ...getProxyWidgets(parent), + ...existingProxyWidgets, widgetItemToProperty([node, widget]) ] parent.properties.proxyWidgets = proxyWidgets diff --git a/src/lib/litegraph/src/LGraph.ts b/src/lib/litegraph/src/LGraph.ts index 561836d1e..04f2a4a4f 100644 --- a/src/lib/litegraph/src/LGraph.ts +++ b/src/lib/litegraph/src/LGraph.ts @@ -842,8 +842,13 @@ export class LGraph if (!list_of_graphcanvas) return for (const c of list_of_graphcanvas) { - // eslint-disable-next-line prefer-spread - c[action]?.apply(c, params) + const method = c[action] + + if (typeof method === 'function') { + const args = + params == null ? [] : Array.isArray(params) ? params : [params] + ;(method as (...args: unknown[]) => unknown).apply(c, args) + } } } diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index 3e09ba5fd..7dee812fc 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -52,6 +52,11 @@ import type { LinkSegment, NewNodePosition, NullableProperties, + Panel, + PanelButton, + PanelWidget, + PanelWidgetCallback, + PanelWidgetOptions, Point, Positionable, ReadOnlyRect, @@ -94,7 +99,7 @@ import type { SubgraphIO } from './types/serialisation' import type { NeverNever, PickNevers } from './types/utility' -import type { IBaseWidget } from './types/widgets' +import type { IBaseWidget, TWidgetValue } from './types/widgets' import { alignNodes, distributeNodes, getBoundaryNodes } from './utils/arrange' import { findFirstNode, getAllNestedItems } from './utils/collections' import { resolveConnectingLinkColor } from './utils/linkColors' @@ -232,6 +237,15 @@ interface ICreatePanelOptions { height?: number | string } +interface SlotTypeDefaultNodeOpts { + node?: string + title?: string + properties?: Record + inputs?: [string, string][] + outputs?: [string, string][] + json?: Parameters[0] +} + const cursors = { NE: 'nesw-resize', SE: 'nwse-resize', @@ -693,9 +707,9 @@ export class LGraphCanvas implements CustomEventDispatcher _highlight_input?: INodeInputSlot // TODO: Check if panels are used /** @deprecated Panels */ - node_panel?: any + node_panel?: Panel /** @deprecated Panels */ - options_panel?: any + options_panel?: Panel _bg_img?: HTMLImageElement _pattern?: CanvasPattern _pattern_img?: HTMLImageElement @@ -1249,11 +1263,13 @@ export class LGraphCanvas implements CustomEventDispatcher function inner_clicked( this: ContextMenuDivElement, - v: IContextMenuValue, - e: any, - prev: any + v?: string | IContextMenuValue, + _options?: unknown, + e?: MouseEvent, + prev?: ContextMenu ) { if (!node) return + if (!v || typeof v === 'string') return // TODO: This is a static method, so the below "that" appears broken. if (v.callback) void v.callback.call(this, node, v, e, prev) @@ -6354,8 +6370,7 @@ export class LGraphCanvas implements CustomEventDispatcher ? LiteGraph.slot_types_default_out : LiteGraph.slot_types_default_in if (slotTypesDefault?.[fromSlotType]) { - // TODO: Remove "any" kludge - let nodeNewType: any = false + let nodeNewType: string | Record | false = false if (typeof slotTypesDefault[fromSlotType] == 'object') { for (const typeX in slotTypesDefault[fromSlotType]) { if ( @@ -6373,11 +6388,13 @@ export class LGraphCanvas implements CustomEventDispatcher nodeNewType = slotTypesDefault[fromSlotType] } if (nodeNewType) { - // TODO: Remove "any" kludge - let nodeNewOpts: any = false - if (typeof nodeNewType == 'object' && nodeNewType.node) { - nodeNewOpts = nodeNewType - nodeNewType = nodeNewType.node + let nodeNewOpts: SlotTypeDefaultNodeOpts | undefined + let nodeTypeStr: string + if (typeof nodeNewType == 'object') { + nodeNewOpts = nodeNewType as SlotTypeDefaultNodeOpts + nodeTypeStr = nodeNewOpts.node ?? '' + } else { + nodeTypeStr = nodeNewType } // that.graph.beforeChange(); @@ -6386,7 +6403,7 @@ export class LGraphCanvas implements CustomEventDispatcher const nodeX = opts.position[0] + opts.posAdd[0] + xSizeFix const nodeY = opts.position[1] + opts.posAdd[1] + ySizeFix const pos = [nodeX, nodeY] - const newNode = LiteGraph.createNode(nodeNewType, nodeNewOpts.title, { + const newNode = LiteGraph.createNode(nodeTypeStr, nodeNewOpts?.title, { pos }) if (newNode) { @@ -6399,20 +6416,14 @@ export class LGraphCanvas implements CustomEventDispatcher } if (nodeNewOpts.inputs) { newNode.inputs = [] - for (const i in nodeNewOpts.inputs) { - newNode.addOutput( - nodeNewOpts.inputs[i][0], - nodeNewOpts.inputs[i][1] - ) + for (const input of nodeNewOpts.inputs) { + newNode.addInput(input[0], input[1]) } } if (nodeNewOpts.outputs) { newNode.outputs = [] - for (const i in nodeNewOpts.outputs) { - newNode.addOutput( - nodeNewOpts.outputs[i][0], - nodeNewOpts.outputs[i][1] - ) + for (const output of nodeNewOpts.outputs) { + newNode.addOutput(output[0], output[1]) } } if (nodeNewOpts.json) { @@ -6909,8 +6920,7 @@ export class LGraphCanvas implements CustomEventDispatcher // hide on mouse leave if (options.hide_on_mouse_leave) { - // FIXME: Remove "any" kludge - let prevent_timeout: any = false + let prevent_timeout = 0 let timeout_close: ReturnType | null = null LiteGraph.pointerListenerAdd(dialog, 'enter', function () { if (timeout_close) { @@ -7104,8 +7114,7 @@ export class LGraphCanvas implements CustomEventDispatcher // join node after inserting if (options.node_from) { - // FIXME: any - let iS: any = false + let iS: number | false = false switch (typeof options.slot_from) { case 'string': iS = options.node_from.findOutputSlot(options.slot_from) @@ -7131,8 +7140,8 @@ export class LGraphCanvas implements CustomEventDispatcher // try with first if no name set iS = 0 } - if (options.node_from.outputs[iS] !== undefined) { - if (iS !== false && iS > -1) { + if (iS !== false && options.node_from.outputs[iS] !== undefined) { + if (iS > -1) { if (node == null) throw new TypeError( 'options.slot_from was null when showing search box' @@ -7149,8 +7158,7 @@ export class LGraphCanvas implements CustomEventDispatcher } } if (options.node_to) { - // FIXME: any - let iS: any = false + let iS: number | false = false switch (typeof options.slot_from) { case 'string': iS = options.node_to.findInputSlot(options.slot_from) @@ -7176,8 +7184,8 @@ export class LGraphCanvas implements CustomEventDispatcher // try with first if no name set iS = 0 } - if (options.node_to.inputs[iS] !== undefined) { - if (iS !== false && iS > -1) { + if (iS !== false && options.node_to.inputs[iS] !== undefined) { + if (iS > -1) { if (node == null) throw new TypeError( 'options.slot_from was null when showing search box' @@ -7240,13 +7248,16 @@ export class LGraphCanvas implements CustomEventDispatcher const filter = graphcanvas.filter || graphcanvas.graph.filter - // FIXME: any // filter by type preprocess - let sIn: any = false - let sOut: any = false + let sIn: HTMLSelectElement | null = null + let sOut: HTMLSelectElement | null = null if (options.do_type_filter && that.search_box) { - sIn = that.search_box.querySelector('.slot_in_type_filter') - sOut = that.search_box.querySelector('.slot_out_type_filter') + sIn = that.search_box.querySelector( + '.slot_in_type_filter' + ) + sOut = that.search_box.querySelector( + '.slot_out_type_filter' + ) } const keys = Object.keys(LiteGraph.registered_node_types) @@ -7264,7 +7275,7 @@ export class LGraphCanvas implements CustomEventDispatcher // add general type if filtering if ( options.show_general_after_typefiltered && - (sIn.value || sOut.value) + (sIn?.value || sOut?.value) ) { const filtered_extra: string[] = [] for (const i in LiteGraph.registered_node_types) { @@ -7289,7 +7300,7 @@ export class LGraphCanvas implements CustomEventDispatcher // check il filtering gave no results if ( - (sIn.value || sOut.value) && + (sIn?.value || sOut?.value) && helper.childNodes.length == 0 && options.show_general_if_none_on_typefilter ) { @@ -7337,8 +7348,10 @@ export class LGraphCanvas implements CustomEventDispatcher if (options.do_type_filter && !opts.skipFilter) { const sType = type - let sV = - opts.inTypeOverride !== false ? opts.inTypeOverride : sIn.value + let sV: string | undefined = + typeof opts.inTypeOverride === 'string' + ? opts.inTypeOverride + : sIn?.value // type is stored if (sIn && sV && LiteGraph.registered_slot_in_types[sV]?.nodes) { const doesInc = @@ -7346,8 +7359,9 @@ export class LGraphCanvas implements CustomEventDispatcher if (doesInc === false) return false } - sV = sOut.value - if (opts.outTypeOverride !== false) sV = opts.outTypeOverride + sV = sOut?.value + if (typeof opts.outTypeOverride === 'string') + sV = opts.outTypeOverride // type is stored if (sOut && sV && LiteGraph.registered_slot_out_types[sV]?.nodes) { const doesInc = @@ -7628,15 +7642,14 @@ export class LGraphCanvas implements CustomEventDispatcher return dialog } - createPanel(title: string, options: ICreatePanelOptions) { + createPanel(title: string, options: ICreatePanelOptions): Panel { options = options || {} - // TODO: any kludge - const root: any = document.createElement('div') + const root = document.createElement('div') as Panel root.className = 'litegraph dialog' root.innerHTML = "
" - root.header = root.querySelector('.dialog-header') + root.header = root.querySelector('.dialog-header')! if (options.width) root.style.width = @@ -7653,11 +7666,11 @@ export class LGraphCanvas implements CustomEventDispatcher }) root.header.append(close) } - root.title_element = root.querySelector('.dialog-title') + root.title_element = root.querySelector('.dialog-title')! root.title_element.textContent = title - root.content = root.querySelector('.dialog-content') - root.alt_content = root.querySelector('.dialog-alt-content') - root.footer = root.querySelector('.dialog-footer') + root.content = root.querySelector('.dialog-content')! + root.alt_content = root.querySelector('.dialog-alt-content')! + root.footer = root.querySelector('.dialog-footer')! root.footer.style.marginTop = '-96px' root.close = function () { @@ -7667,7 +7680,7 @@ export class LGraphCanvas implements CustomEventDispatcher } // function to swap panel content - root.toggleAltContent = function (force: unknown) { + root.toggleAltContent = function (force?: boolean) { let vTo: string let vAlt: string if (force !== undefined) { @@ -7681,7 +7694,7 @@ export class LGraphCanvas implements CustomEventDispatcher root.content.style.display = vAlt } - root.toggleFooterVisibility = function (force: unknown) { + root.toggleFooterVisibility = function (force?: boolean) { let vTo: string if (force !== undefined) { vTo = force ? 'block' : 'none' @@ -7695,7 +7708,11 @@ export class LGraphCanvas implements CustomEventDispatcher this.content.innerHTML = '' } - root.addHTML = function (code: string, classname: string, on_footer: any) { + root.addHTML = function ( + code: string, + classname?: string, + on_footer?: boolean + ) { const elem = document.createElement('div') if (classname) elem.className = classname elem.innerHTML = code @@ -7704,9 +7721,12 @@ export class LGraphCanvas implements CustomEventDispatcher return elem } - root.addButton = function (name: any, callback: any, options: any) { - // TODO: any kludge - const elem: any = document.createElement('button') + root.addButton = function ( + name: string, + callback: () => void, + options?: unknown + ): PanelButton { + const elem = document.createElement('button') as PanelButton elem.textContent = name elem.options = options elem.classList.add('btn') @@ -7723,20 +7743,18 @@ export class LGraphCanvas implements CustomEventDispatcher root.addWidget = function ( type: string, - name: any, - value: unknown, - options: { label?: any; type?: any; values?: any; callback?: any }, - callback: (arg0: any, arg1: any, arg2: any) => void - ) { + name: string, + value: TWidgetValue, + options?: PanelWidgetOptions, + callback?: PanelWidgetCallback + ): PanelWidget { options = options || {} let str_value = String(value) type = type.toLowerCase() if (type == 'number' && typeof value === 'number') str_value = value.toFixed(3) - // FIXME: any kludge - const elem: HTMLDivElement & { options?: unknown; value?: unknown } = - document.createElement('div') + const elem: PanelWidget = document.createElement('div') as PanelWidget elem.className = 'property' elem.innerHTML = "" @@ -7744,7 +7762,6 @@ export class LGraphCanvas implements CustomEventDispatcher if (!nameSpan) throw new TypeError('Property name element was null.') nameSpan.textContent = options.label || name - // TODO: any kludge const value_element: HTMLSpanElement | null = elem.querySelector('.property_value') if (!value_element) throw new TypeError('Property name element was null.') @@ -7756,7 +7773,8 @@ export class LGraphCanvas implements CustomEventDispatcher if (type == 'code') { elem.addEventListener('click', function () { - root.inner_showCodePad(this.dataset['property']) + const property = this.dataset['property'] + if (property) root.inner_showCodePad?.(property) }) } else if (type == 'boolean') { elem.classList.add('boolean') @@ -7799,19 +7817,16 @@ export class LGraphCanvas implements CustomEventDispatcher value_element.textContent = str_value ?? '' value_element.addEventListener('click', function (event) { - const values = options.values || [] + const values = options?.values || [] const propname = this.parentElement?.dataset['property'] - const inner_clicked = (v: string | null) => { - // node.setProperty(propname,v); - // graphcanvas.dirty_canvas = true; - this.textContent = v + const inner_clicked = (v?: string) => { + this.textContent = v ?? null innerChange(propname, v) return false } new LiteGraph.ContextMenu(values, { event, className: 'dark', - // @ts-expect-error fixme ts strict error - callback signature mismatch callback: inner_clicked }) }) @@ -7819,9 +7834,10 @@ export class LGraphCanvas implements CustomEventDispatcher root.content.append(elem) - function innerChange(name: string | undefined, value: unknown) { - options.callback?.(name, value, options) - callback?.(name, value, options) + function innerChange(name: string | undefined, value: TWidgetValue) { + const opts = options || {} + opts.callback?.(name, value, opts) + callback?.(name, value, opts) } return elem @@ -7850,7 +7866,7 @@ export class LGraphCanvas implements CustomEventDispatcher }, onClose: () => { this.NODEPANEL_IS_OPEN = false - this.node_panel = null + this.node_panel = undefined } }) this.node_panel = panel @@ -7871,11 +7887,9 @@ export class LGraphCanvas implements CustomEventDispatcher panel.addHTML('

Properties

') - const fUpdate = ( - name: string, - value: string | number | boolean | object | undefined - ) => { + const fUpdate: PanelWidgetCallback = (name, value) => { if (!this.graph) throw new NullGraphError() + if (!name) return this.graph.beforeChange(node) switch (name) { case 'Title': @@ -7979,7 +7993,7 @@ export class LGraphCanvas implements CustomEventDispatcher panel.alt_content.innerHTML = "" const textarea: HTMLTextAreaElement = - panel.alt_content.querySelector('textarea') + panel.alt_content.querySelector('textarea')! const fDoneWith = function () { panel.toggleAltContent(false) panel.toggleFooterVisibility(true) diff --git a/src/lib/litegraph/src/LGraphNode.ts b/src/lib/litegraph/src/LGraphNode.ts index 0ca41a732..1ff503cdc 100644 --- a/src/lib/litegraph/src/LGraphNode.ts +++ b/src/lib/litegraph/src/LGraphNode.ts @@ -1,8 +1,6 @@ import { LGraphNodeProperties } from '@/lib/litegraph/src/LGraphNodeProperties' import { - calculateInputSlotPos, calculateInputSlotPosFromSlot, - calculateOutputSlotPos, getSlotPosition } from '@/renderer/core/canvas/litegraph/slotCalculations' import type { SlotPositionContext } from '@/renderer/core/canvas/litegraph/slotCalculations' @@ -41,6 +39,7 @@ import type { INodeSlotContextItem, IPinnable, ISlotType, + Panel, Point, Positionable, ReadOnlyRect, @@ -96,9 +95,12 @@ export type NodeId = number | string export type NodeProperty = string | number | boolean | object interface INodePropertyInfo { - name: string + name?: string type?: string - default_value: NodeProperty | undefined + default_value?: NodeProperty + widget?: string + label?: string + values?: TWidgetValue[] } interface IMouseOverData { @@ -606,8 +608,8 @@ export class LGraphNode target_slot: number, requested_slot?: number | string ): number | false | null - onShowCustomPanelInfo?(this: LGraphNode, panel: any): void - onAddPropertyToPanel?(this: LGraphNode, pName: string, panel: any): boolean + onShowCustomPanelInfo?(this: LGraphNode, panel: Panel): void + onAddPropertyToPanel?(this: LGraphNode, pName: string, panel: Panel): boolean onWidgetChanged?( this: LGraphNode, name: string, @@ -667,8 +669,7 @@ export class LGraphNode index: number, e: CanvasPointerEvent ): void - // TODO: Return type - onGetPropertyInfo?(this: LGraphNode, property: string): any + onGetPropertyInfo?(this: LGraphNode, property: string): INodePropertyInfo onNodeOutputAdd?(this: LGraphNode, value: unknown): void onNodeInputAdd?(this: LGraphNode, value: unknown): void onMenuNodeInputs?( @@ -3346,7 +3347,7 @@ export class LGraphNode * @returns Position of the input slot */ getInputPos(slot: number): Point { - return calculateInputSlotPos(this.#getSlotPositionContext(), slot) + return getSlotPosition(this, slot, true) } /** @@ -3366,10 +3367,7 @@ export class LGraphNode * @returns Position of the output slot */ getOutputPos(outputSlotIndex: number): Point { - return calculateOutputSlotPos( - this.#getSlotPositionContext(), - outputSlotIndex - ) + return getSlotPosition(this, outputSlotIndex, false) } /** diff --git a/src/lib/litegraph/src/contextMenuCompat.ts b/src/lib/litegraph/src/contextMenuCompat.ts index 38865bf20..c1f039cfa 100644 --- a/src/lib/litegraph/src/contextMenuCompat.ts +++ b/src/lib/litegraph/src/contextMenuCompat.ts @@ -81,6 +81,7 @@ class LegacyMenuCompat { return currentImpl }, set: (newImpl: LGraphCanvas[K]) => { + if (!newImpl) return const fnKey = `${methodName as string}:${newImpl.toString().slice(0, 100)}` if (!this.hasWarned.has(fnKey) && this.currentExtension) { this.hasWarned.add(fnKey) diff --git a/src/lib/litegraph/src/interfaces.ts b/src/lib/litegraph/src/interfaces.ts index 0983770a1..aad3dff3f 100644 --- a/src/lib/litegraph/src/interfaces.ts +++ b/src/lib/litegraph/src/interfaces.ts @@ -1,5 +1,6 @@ import type { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle' import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events' +import type { TWidgetValue } from '@/lib/litegraph/src/types/widgets' import type { ContextMenu } from './ContextMenu' import type { LGraphNode, NodeId } from './LGraphNode' @@ -493,3 +494,68 @@ export interface Hoverable extends HasBoundingRect { onPointerEnter?(e?: CanvasPointerEvent): void onPointerLeave?(e?: CanvasPointerEvent): void } + +/** + * Callback for panel widget value changes. + */ +export type PanelWidgetCallback = ( + name: string | undefined, + value: TWidgetValue, + options: PanelWidgetOptions +) => void + +/** + * Options for panel widgets. + */ +export interface PanelWidgetOptions { + label?: string + type?: string + widget?: string + values?: Array | null> + callback?: PanelWidgetCallback +} + +/** + * A button element with optional options property. + */ +export interface PanelButton extends HTMLButtonElement { + options?: unknown +} + +/** + * A widget element with options and value properties. + */ +export interface PanelWidget extends HTMLDivElement { + options?: PanelWidgetOptions + value?: TWidgetValue +} + +/** + * A dialog panel created by LGraphCanvas.createPanel(). + * Extends HTMLDivElement with additional properties and methods for panel management. + */ +export interface Panel extends HTMLDivElement { + header: HTMLElement + title_element: HTMLSpanElement + content: HTMLDivElement + alt_content: HTMLDivElement + footer: HTMLDivElement + node?: LGraphNode + onOpen?: () => void + onClose?: () => void + close(): void + toggleAltContent(force?: boolean): void + toggleFooterVisibility(force?: boolean): void + clear(): void + addHTML(code: string, classname?: string, on_footer?: boolean): HTMLDivElement + addButton(name: string, callback: () => void, options?: unknown): PanelButton + addSeparator(): void + addWidget( + type: string, + name: string, + value: TWidgetValue, + options?: PanelWidgetOptions, + callback?: PanelWidgetCallback + ): PanelWidget + inner_showCodePad?(property: string): void +} diff --git a/src/lib/litegraph/src/subgraph/SubgraphIONodeBase.ts b/src/lib/litegraph/src/subgraph/SubgraphIONodeBase.ts index 045cb2de1..a50633848 100644 --- a/src/lib/litegraph/src/subgraph/SubgraphIONodeBase.ts +++ b/src/lib/litegraph/src/subgraph/SubgraphIONodeBase.ts @@ -195,7 +195,7 @@ export abstract class SubgraphIONodeBase< if (!(options.length > 0)) return new LiteGraph.ContextMenu(options, { - event: event as any, + event, title: slot.name || 'Subgraph Output', callback: (item: IContextMenuValue) => { this.#onSlotMenuAction(item, slot, event) diff --git a/src/lib/litegraph/src/utils/widget.ts b/src/lib/litegraph/src/utils/widget.ts index e5d736901..8c61d7605 100644 --- a/src/lib/litegraph/src/utils/widget.ts +++ b/src/lib/litegraph/src/utils/widget.ts @@ -8,3 +8,18 @@ import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets' export function getWidgetStep(options: IWidgetOptions): number { return options.step2 || (options.step || 10) * 0.1 } + +export function evaluateInput(input: string): number | undefined { + // Check if v is a valid equation or a number + if (/^[\d\s.()*+/-]+$/.test(input)) { + // Solve the equation if possible + try { + input = eval(input) + } catch { + // Ignore eval errors + } + } + const newValue = Number(input) + if (isNaN(newValue)) return undefined + return newValue +} diff --git a/src/lib/litegraph/src/widgets/ChartWidget.ts b/src/lib/litegraph/src/widgets/ChartWidget.ts index 440ca4f08..1a32f1010 100644 --- a/src/lib/litegraph/src/widgets/ChartWidget.ts +++ b/src/lib/litegraph/src/widgets/ChartWidget.ts @@ -1,3 +1,5 @@ +import { t } from '@/i18n' + import type { IChartWidget } from '../types/widgets' import { BaseWidget } from './BaseWidget' import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget' @@ -29,7 +31,7 @@ export class ChartWidget ctx.textAlign = 'center' ctx.textBaseline = 'middle' - const text = 'Chart: Vue-only' + const text = `Chart: ${t('widgets.node2only')}` ctx.fillText(text, width / 2, y + height / 2) Object.assign(ctx, { diff --git a/src/lib/litegraph/src/widgets/ColorWidget.ts b/src/lib/litegraph/src/widgets/ColorWidget.ts index a0b0b3496..f2c50083a 100644 --- a/src/lib/litegraph/src/widgets/ColorWidget.ts +++ b/src/lib/litegraph/src/widgets/ColorWidget.ts @@ -1,3 +1,5 @@ +import { t } from '@/i18n' + import type { IColorWidget } from '../types/widgets' import { BaseWidget } from './BaseWidget' import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget' @@ -29,7 +31,7 @@ export class ColorWidget ctx.textAlign = 'center' ctx.textBaseline = 'middle' - const text = 'Color: Vue-only' + const text = `Color: ${t('widgets.node2only')}` ctx.fillText(text, width / 2, y + height / 2) Object.assign(ctx, { diff --git a/src/lib/litegraph/src/widgets/FileUploadWidget.ts b/src/lib/litegraph/src/widgets/FileUploadWidget.ts index f73de53f2..bffb9eb6c 100644 --- a/src/lib/litegraph/src/widgets/FileUploadWidget.ts +++ b/src/lib/litegraph/src/widgets/FileUploadWidget.ts @@ -1,3 +1,5 @@ +import { t } from '@/i18n' + import type { IFileUploadWidget } from '../types/widgets' import { BaseWidget } from './BaseWidget' import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget' @@ -29,7 +31,7 @@ export class FileUploadWidget ctx.textAlign = 'center' ctx.textBaseline = 'middle' - const text = 'Fileupload: Vue-only' + const text = `Fileupload: ${t('widgets.node2only')}` ctx.fillText(text, width / 2, y + height / 2) Object.assign(ctx, { diff --git a/src/lib/litegraph/src/widgets/GalleriaWidget.ts b/src/lib/litegraph/src/widgets/GalleriaWidget.ts index 75770b2bb..767e652de 100644 --- a/src/lib/litegraph/src/widgets/GalleriaWidget.ts +++ b/src/lib/litegraph/src/widgets/GalleriaWidget.ts @@ -1,3 +1,5 @@ +import { t } from '@/i18n' + import type { IGalleriaWidget } from '../types/widgets' import { BaseWidget } from './BaseWidget' import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget' @@ -29,7 +31,7 @@ export class GalleriaWidget ctx.textAlign = 'center' ctx.textBaseline = 'middle' - const text = 'Galleria: Vue-only' + const text = `Galleria: ${t('widgets.node2only')}` ctx.fillText(text, width / 2, y + height / 2) Object.assign(ctx, { diff --git a/src/lib/litegraph/src/widgets/ImageCompareWidget.ts b/src/lib/litegraph/src/widgets/ImageCompareWidget.ts index 3f593e0ee..d31e1b84c 100644 --- a/src/lib/litegraph/src/widgets/ImageCompareWidget.ts +++ b/src/lib/litegraph/src/widgets/ImageCompareWidget.ts @@ -1,3 +1,5 @@ +import { t } from '@/i18n' + import type { IImageCompareWidget } from '../types/widgets' import { BaseWidget } from './BaseWidget' import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget' @@ -29,7 +31,7 @@ export class ImageCompareWidget ctx.textAlign = 'center' ctx.textBaseline = 'middle' - const text = 'ImageCompare: Vue-only' + const text = `ImageCompare: ${t('widgets.node2only')}` ctx.fillText(text, width / 2, y + height / 2) Object.assign(ctx, { diff --git a/src/lib/litegraph/src/widgets/MarkdownWidget.ts b/src/lib/litegraph/src/widgets/MarkdownWidget.ts index 6ca6512fe..f995936e6 100644 --- a/src/lib/litegraph/src/widgets/MarkdownWidget.ts +++ b/src/lib/litegraph/src/widgets/MarkdownWidget.ts @@ -1,3 +1,5 @@ +import { t } from '@/i18n' + import type { IMarkdownWidget } from '../types/widgets' import { BaseWidget } from './BaseWidget' import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget' @@ -29,7 +31,7 @@ export class MarkdownWidget ctx.textAlign = 'center' ctx.textBaseline = 'middle' - const text = 'Markdown: Vue-only' + const text = `Markdown: ${t('widgets.node2only')}` ctx.fillText(text, width / 2, y + height / 2) Object.assign(ctx, { diff --git a/src/lib/litegraph/src/widgets/MultiSelectWidget.ts b/src/lib/litegraph/src/widgets/MultiSelectWidget.ts index 5535820f3..25b59cc1f 100644 --- a/src/lib/litegraph/src/widgets/MultiSelectWidget.ts +++ b/src/lib/litegraph/src/widgets/MultiSelectWidget.ts @@ -1,3 +1,5 @@ +import { t } from '@/i18n' + import type { IMultiSelectWidget } from '../types/widgets' import { BaseWidget } from './BaseWidget' import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget' @@ -29,7 +31,7 @@ export class MultiSelectWidget ctx.textAlign = 'center' ctx.textBaseline = 'middle' - const text = 'MultiSelect: Vue-only' + const text = `MultiSelect: ${t('widgets.node2only')}` ctx.fillText(text, width / 2, y + height / 2) Object.assign(ctx, { diff --git a/src/lib/litegraph/src/widgets/NumberWidget.ts b/src/lib/litegraph/src/widgets/NumberWidget.ts index 294bbb0b0..c142d8b4d 100644 --- a/src/lib/litegraph/src/widgets/NumberWidget.ts +++ b/src/lib/litegraph/src/widgets/NumberWidget.ts @@ -1,5 +1,5 @@ import type { INumericWidget } from '@/lib/litegraph/src/types/widgets' -import { getWidgetStep } from '@/lib/litegraph/src/utils/widget' +import { evaluateInput, getWidgetStep } from '@/lib/litegraph/src/utils/widget' import { BaseSteppedWidget } from './BaseSteppedWidget' import type { WidgetEventOptions } from './BaseWidget' @@ -68,19 +68,8 @@ export class NumberWidget 'Value', this.value, (v: string) => { - // Check if v is a valid equation or a number - if (/^[\d\s()*+/-]+|\d+\.\d+$/.test(v)) { - // Solve the equation if possible - try { - v = eval(v) - } catch { - // Ignore eval errors - } - } - const newValue = Number(v) - if (!isNaN(newValue)) { - this.setValue(newValue, { e, node, canvas }) - } + const parsed = evaluateInput(v) + if (parsed !== undefined) this.setValue(parsed, { e, node, canvas }) }, e ) diff --git a/src/lib/litegraph/src/widgets/SelectButtonWidget.ts b/src/lib/litegraph/src/widgets/SelectButtonWidget.ts index 463efe089..4739a9ff7 100644 --- a/src/lib/litegraph/src/widgets/SelectButtonWidget.ts +++ b/src/lib/litegraph/src/widgets/SelectButtonWidget.ts @@ -1,3 +1,5 @@ +import { t } from '@/i18n' + import type { ISelectButtonWidget } from '../types/widgets' import { BaseWidget } from './BaseWidget' import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget' @@ -29,7 +31,7 @@ export class SelectButtonWidget ctx.textAlign = 'center' ctx.textBaseline = 'middle' - const text = 'SelectButton: Vue-only' + const text = `SelectButton: ${t('widgets.node2only')}` ctx.fillText(text, width / 2, y + height / 2) Object.assign(ctx, { diff --git a/src/lib/litegraph/src/widgets/TextareaWidget.ts b/src/lib/litegraph/src/widgets/TextareaWidget.ts index e93ee1c7e..f29ccc5e5 100644 --- a/src/lib/litegraph/src/widgets/TextareaWidget.ts +++ b/src/lib/litegraph/src/widgets/TextareaWidget.ts @@ -1,3 +1,5 @@ +import { t } from '@/i18n' + import type { ITextareaWidget } from '../types/widgets' import { BaseWidget } from './BaseWidget' import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget' @@ -29,7 +31,7 @@ export class TextareaWidget ctx.textAlign = 'center' ctx.textBaseline = 'middle' - const text = 'Textarea: Vue-only' + const text = `Textarea: ${t('widgets.node2only')}` ctx.fillText(text, width / 2, y + height / 2) Object.assign(ctx, { diff --git a/src/lib/litegraph/src/widgets/TreeSelectWidget.ts b/src/lib/litegraph/src/widgets/TreeSelectWidget.ts index 23ad440a3..ee78d5919 100644 --- a/src/lib/litegraph/src/widgets/TreeSelectWidget.ts +++ b/src/lib/litegraph/src/widgets/TreeSelectWidget.ts @@ -1,3 +1,5 @@ +import { t } from '@/i18n' + import type { ITreeSelectWidget } from '../types/widgets' import { BaseWidget } from './BaseWidget' import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget' @@ -29,7 +31,7 @@ export class TreeSelectWidget ctx.textAlign = 'center' ctx.textBaseline = 'middle' - const text = 'TreeSelect: Vue-only' + const text = `TreeSelect: ${t('widgets.node2only')}` ctx.fillText(text, width / 2, y + height / 2) Object.assign(ctx, { diff --git a/src/locales/ar/commands.json b/src/locales/ar/commands.json index 82f7cbdea..1b342245e 100644 --- a/src/locales/ar/commands.json +++ b/src/locales/ar/commands.json @@ -254,6 +254,9 @@ "Comfy_RefreshNodeDefinitions": { "label": "تحديث تعريفات العقد" }, + "Comfy_RenameWorkflow": { + "label": "إعادة تسمية سير العمل" + }, "Comfy_SaveWorkflow": { "label": "حفظ سير العمل" }, diff --git a/src/locales/ar/main.json b/src/locales/ar/main.json index 46758312b..2073b0478 100644 --- a/src/locales/ar/main.json +++ b/src/locales/ar/main.json @@ -60,6 +60,7 @@ "findInLibrary": "ابحث عنه في قسم {type} من مكتبة النماذج.", "finish": "إنهاء", "genericLinkPlaceholder": "الصق الرابط هنا", + "importAnother": "استيراد آخر", "jobId": "معرّف المهمة", "loadingModels": "جارٍ تحميل {type}...", "maxFileSize": "الحد الأقصى لحجم الملف: {size}", @@ -1520,6 +1521,7 @@ "Redo": "إعادة", "Refresh Node Definitions": "تحديث تعريفات العقد", "Reinstall": "إعادة التثبيت", + "Rename": "إعادة التسمية", "Reset View": "إعادة تعيين العرض", "Resize Selected Nodes": "تغيير حجم العقد المحددة", "Restart": "إعادة التشغيل", @@ -2476,6 +2478,7 @@ "dropPrompt": "أسقط ملفك أو" }, "widgets": { + "node2only": "فقط Node 2.0", "selectModel": "اختر نموذج", "uploadSelect": { "placeholder": "اختر...", diff --git a/src/locales/en/commands.json b/src/locales/en/commands.json index 1032083c0..fb5f9957d 100644 --- a/src/locales/en/commands.json +++ b/src/locales/en/commands.json @@ -254,6 +254,9 @@ "Comfy_RefreshNodeDefinitions": { "label": "Refresh Node Definitions" }, + "Comfy_RenameWorkflow": { + "label": "Rename Workflow" + }, "Comfy_SaveWorkflow": { "label": "Save Workflow" }, diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 6ef5343ac..555ff07fb 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -190,6 +190,7 @@ "failed": "Failed", "cancelled": "Cancelled", "job": "Job", + "asset": "{count} assets | {count} asset | {count} assets", "untitled": "Untitled", "emDash": "—", "enabling": "Enabling {id}", @@ -435,6 +436,8 @@ "Save Image": "Save Image", "Rename": "Rename", "RenameWidget": "Rename Widget", + "FavoriteWidget": "Favorite Widget", + "UnfavoriteWidget": "Unfavorite Widget", "Copy": "Copy", "Duplicate": "Duplicate", "Paste": "Paste", @@ -677,7 +680,8 @@ "filterImage": "Image", "filterVideo": "Video", "filterAudio": "Audio", - "filter3D": "3D" + "filter3D": "3D", + "filterText": "Text" }, "backToAssets": "Back to all assets", "searchAssets": "Search Assets", @@ -1176,13 +1180,14 @@ "Queue Selected Output Nodes": "Queue Selected Output Nodes", "Redo": "Redo", "Refresh Node Definitions": "Refresh Node Definitions", + "Rename": "Rename", "Save": "Save", "Save As": "Save As", "Show Settings Dialog": "Show Settings Dialog", "Experimental: Enable AssetAPI": "Experimental: Enable AssetAPI", "Canvas Performance": "Canvas Performance", "Help Center": "Help Center", - "toggle linear mode": "toggle linear mode", + "toggle linear mode": "toggle simple mode", "Toggle Queue Panel V2": "Toggle Queue Panel V2", "Toggle Theme (Dark/Light)": "Toggle Theme (Dark/Light)", "Undo": "Undo", @@ -2082,6 +2087,7 @@ "Set Group Nodes to Always": "Set Group Nodes to Always" }, "widgets": { + "node2only": "Node 2.0 only", "selectModel": "Select model", "uploadSelect": { "placeholder": "Select...", @@ -2311,6 +2317,7 @@ "filterBy": "Filter by", "findInLibrary": "Find it in the {type} section of the models library.", "finish": "Finish", + "importAnother": "Import Another", "genericLinkPlaceholder": "Paste link here", "jobId": "Job ID", "loadingModels": "Loading {type}...", @@ -2407,7 +2414,7 @@ "zoom": "Zoom in", "moreOptions": "More options", "seeMoreOutputs": "See more outputs", - "addToWorkflow": "Add to current workflow", + "insertAsNodeInWorkflow": "Insert as node in workflow", "download": "Download", "openWorkflow": "Open as workflow in new tab", "exportWorkflow": "Export workflow", @@ -2428,11 +2435,23 @@ "downloadSelectedAll": "Download all", "deleteSelected": "Delete", "deleteSelectedAll": "Delete all", + "insertAllAssetsAsNodes": "Insert all assets as nodes", + "openWorkflowAll": "Open all workflows", + "exportWorkflowAll": "Export all workflows", "downloadStarted": "Downloading {count} files...", "downloadsStarted": "Started downloading {count} file(s)", "assetsDeletedSuccessfully": "{count} asset(s) deleted successfully", "failedToDeleteAssets": "Failed to delete selected assets", - "partialDeleteSuccess": "{succeeded} deleted successfully, {failed} failed" + "partialDeleteSuccess": "{succeeded} deleted successfully, {failed} failed", + "nodesAddedToWorkflow": "{count} node(s) added to workflow", + "failedToAddNodes": "Failed to add nodes to workflow", + "partialAddNodesSuccess": "{succeeded} added successfully, {failed} failed", + "workflowsOpened": "{count} workflow(s) opened in new tabs", + "noWorkflowsFound": "No workflow data found in selected assets", + "partialWorkflowsOpened": "{succeeded} workflow(s) opened, {failed} failed", + "workflowsExported": "{count} workflow(s) exported successfully", + "noWorkflowsToExport": "No workflow data found to export", + "partialWorkflowsExported": "{succeeded} exported successfully, {failed} failed" }, "noJobIdFound": "No job ID found for this asset", "unsupportedFileType": "Unsupported file type for loader node", @@ -2471,8 +2490,14 @@ "message": "Switch back to Nodes 2.0 anytime from the main menu." }, "linearMode": { - "share": "Share", - "openWorkflow": "Open Workflow" + "linearMode": "Simple Mode", + "beta": "Beta - Give Feedback", + "graphMode": "Graph Mode", + "dragAndDropImage": "Drag and drop an image", + "runCount": "Run count:", + "rerun": "Rerun", + "reuseParameters": "Reuse Parameters", + "downloadAll": "Download All" }, "missingNodes": { "cloud": { @@ -2492,8 +2517,10 @@ "rightSidePanel": { "togglePanel": "Toggle properties panel", "noSelection": "Select a node to see its properties and info.", - "title": "No node(s) selected | 1 node selected | {count} nodes selected", + "workflowOverview": "Workflow Overview", + "title": "No item(s) selected | 1 item selected | {count} items selected", "parameters": "Parameters", + "nodes": "Nodes", "info": "Info", "color": "Node color", "pinned": "Pinned", @@ -2503,9 +2530,43 @@ "inputs": "INPUTS", "inputsNone": "NO INPUTS", "inputsNoneTooltip": "Node has no inputs", + "advancedInputs": "ADVANCED INPUTS", + "showAdvancedInputsButton": "Show advanced inputs", "properties": "Properties", "nodeState": "Node state", - "settings": "Settings" + "settings": "Settings", + "addFavorite": "Favorite", + "removeFavorite": "Unfavorite", + "hideInput": "Hide input", + "showInput": "Show input", + "locateNode": "Locate node on canvas", + "favorites": "FAVORITED INPUTS", + "favoritesNone": "NO FAVORITED INPUTS", + "favoritesNoneTooltip": "Star widgets to quickly access them without selecting nodes", + "globalSettings": { + "title": "Global Settings", + "searchPlaceholder": "Search quick settings...", + "nodes": "NODES", + "canvas": "CANVAS", + "connectionLinks": "CONNECTION LINKS", + "showAdvanced": "Show advanced parameters", + "showAdvancedTooltip": "This is an important setting that when set to TRUE, reveals all advanced parameters for nodes", + "showInfoBadges": "Show info badges", + "showToolbox": "Show toolbox on selection", + "nodes2": "Nodes 2.0", + "gridSpacing": "Grid spacing", + "snapNodesToGrid": "Snap nodes to grid", + "linkShape": "Link shape", + "showConnectedLinks": "Show connected links", + "viewAllSettings": "View all settings" + }, + "groupSettings": "Group Settings", + "groups": "Groups", + "favoritesNoneDesc": "Inputs you favorite will show up here", + "noneSearchDesc": "No items match your search", + "nodesNoneDesc": "NO NODES", + "fallbackGroupTitle": "Group", + "fallbackNodeTitle": "Node" }, "help": { "recentReleases": "Recent releases", @@ -2527,4 +2588,4 @@ "failed": "Failed" } } -} \ No newline at end of file +} diff --git a/src/locales/es/commands.json b/src/locales/es/commands.json index 9a592660b..642a7a4dd 100644 --- a/src/locales/es/commands.json +++ b/src/locales/es/commands.json @@ -254,6 +254,9 @@ "Comfy_RefreshNodeDefinitions": { "label": "Actualizar Definiciones de Nodo" }, + "Comfy_RenameWorkflow": { + "label": "Renombrar flujo de trabajo" + }, "Comfy_SaveWorkflow": { "label": "Guardar Flujo de Trabajo" }, diff --git a/src/locales/es/main.json b/src/locales/es/main.json index 9e746e777..7f084101d 100644 --- a/src/locales/es/main.json +++ b/src/locales/es/main.json @@ -60,6 +60,7 @@ "findInLibrary": "Encuéntralo en la sección {type} de la biblioteca de modelos.", "finish": "Finalizar", "genericLinkPlaceholder": "Pega el enlace aquí", + "importAnother": "Importar otro", "jobId": "ID de tarea", "loadingModels": "Cargando {type}...", "maxFileSize": "Tamaño máximo de archivo: {size}", @@ -1520,6 +1521,7 @@ "Redo": "Rehacer", "Refresh Node Definitions": "Actualizar definiciones de nodo", "Reinstall": "Reinstalar", + "Rename": "Renombrar", "Reset View": "Restablecer vista", "Resize Selected Nodes": "Redimensionar Nodos Seleccionados", "Restart": "Reiniciar", @@ -2476,6 +2478,7 @@ "dropPrompt": "Suelta tu archivo o" }, "widgets": { + "node2only": "Solo Node 2.0", "selectModel": "Seleccionar modelo", "uploadSelect": { "placeholder": "Seleccionar...", diff --git a/src/locales/fa/commands.json b/src/locales/fa/commands.json index 400d7d11f..2993e0c22 100644 --- a/src/locales/fa/commands.json +++ b/src/locales/fa/commands.json @@ -254,6 +254,9 @@ "Comfy_RefreshNodeDefinitions": { "label": "به‌روزرسانی تعاریف نود" }, + "Comfy_RenameWorkflow": { + "label": "تغییر نام Workflow" + }, "Comfy_SaveWorkflow": { "label": "ذخیره ورک‌فلو" }, diff --git a/src/locales/fa/main.json b/src/locales/fa/main.json index dde4db3a4..162fc9b07 100644 --- a/src/locales/fa/main.json +++ b/src/locales/fa/main.json @@ -60,6 +60,7 @@ "findInLibrary": "در بخش {type} کتابخانه مدل‌ها پیدا کنید.", "finish": "پایان", "genericLinkPlaceholder": "لینک را اینجا وارد کنید", + "importAnother": "وارد کردن مورد دیگر", "jobId": "شناسه کار: {jobId}", "loadingModels": "در حال بارگذاری {type}...", "maxFileSize": "حداکثر اندازه فایل: {size}", @@ -237,22 +238,6 @@ "title": "ایجاد حساب کاربری" } }, - "authTimeout": { - "causes": [ - "فایروال یا پراکسی سازمانی که سرویس‌های احراز هویت را مسدود می‌کند", - "محدودیت‌های VPN یا شبکه", - "افزونه‌های مرورگر که در درخواست‌ها اختلال ایجاد می‌کنند", - "محدودیت‌های منطقه‌ای شبکه", - "امتحان مرورگر یا شبکه دیگر" - ], - "helpText": "نیاز به کمک دارید؟ تماس با", - "message": "در اتصال به ComfyUI Cloud با مشکل مواجه شده‌ایم. ممکن است به دلیل کندی اتصال یا مشکل موقت سرویس باشد.", - "restart": "خروج و تلاش مجدد", - "supportLink": "پشتیبانی", - "technicalDetails": "جزئیات فنی", - "title": "اتصال بیش از حد طول کشید", - "troubleshooting": "دلایل رایج:" - }, "breadcrumbsMenu": { "clearWorkflow": "پاک‌سازی workflow", "deleteBlueprint": "حذف blueprint", @@ -261,7 +246,6 @@ "enterNewName": "نام جدید را وارد کنید", "missingNodesWarning": "workflow شامل نودهای پشتیبانی‌نشده است (با رنگ قرمز مشخص شده‌اند)." }, - "checkingStatus": "در حال بررسی وضعیت حساب شما...", "clipboard": { "errorMessage": "کپی به کلیپ‌بورد ناموفق بود", "errorNotSupported": "Clipboard API در مرورگر شما پشتیبانی نمی‌شود", @@ -279,6 +263,49 @@ "cloudForgotPassword_sendResetLink": "ارسال لینک بازنشانی", "cloudForgotPassword_title": "فراموشی رمز عبور", "cloudOnboarding": { + "authTimeout": { + "causes": [ + "فایروال یا پراکسی سازمانی که سرویس‌های احراز هویت را مسدود می‌کند", + "محدودیت‌های VPN یا شبکه", + "افزونه‌های مرورگر که در درخواست‌ها اختلال ایجاد می‌کنند", + "محدودیت‌های منطقه‌ای شبکه", + "استفاده از مرورگر یا شبکه دیگر را امتحان کنید" + ], + "helpText": "نیاز به راهنمایی دارید؟ تماس با", + "message": "در اتصال به ComfyUI Cloud با مشکل مواجه شدیم. این مشکل ممکن است به دلیل کندی اتصال یا اختلال موقت سرویس باشد.", + "restart": "خروج و تلاش مجدد", + "supportLink": "پشتیبانی", + "technicalDetails": "جزئیات فنی", + "title": "اتصال بیش از حد طول کشید", + "troubleshooting": "دلایل رایج:" + }, + "checkingStatus": "در حال بررسی وضعیت حساب شما...", + "forgotPassword": { + "backToLogin": "بازگشت به ورود", + "didntReceiveEmail": "ایمیلی دریافت نکردید؟ با ما تماس بگیرید:", + "emailLabel": "ایمیل", + "emailPlaceholder": "ایمیل خود را وارد کنید", + "emailRequired": "وارد کردن ایمیل الزامی است", + "instructions": "آدرس ایمیل خود را وارد کنید تا لینک بازنشانی رمز عبور برای شما ارسال شود.", + "passwordResetError": "ارسال ایمیل بازنشانی رمز عبور ناموفق بود. لطفاً دوباره تلاش کنید.", + "passwordResetSent": "ایمیل بازنشانی رمز عبور ارسال شد", + "sendResetLink": "ارسال لینک بازنشانی", + "title": "فراموشی رمز عبور" + }, + "privateBeta": { + "desc": "برای پیوستن به لیست انتظار وارد شوید. زمانی که نوبت شما شد به شما اطلاع خواهیم داد. قبلاً اطلاع داده شده‌اید؟ وارد شوید و از Cloud استفاده کنید.", + "title": "کلود در حال حاضر در نسخه بتای خصوصی است" + }, + "retry": "تلاش دوباره", + "retrying": "در حال تلاش مجدد...", + "start": { + "desc": "بدون نیاز به تنظیمات اولیه. روی هر دستگاهی کار می‌کند.", + "download": "دانلود ComfyUI", + "explain": "چندین خروجی را به طور همزمان تولید کنید. گردش‌کارها را به راحتی به اشتراک بگذارید.", + "learnAboutButton": "درباره Cloud بیشتر بدانید", + "title": "در چند ثانیه شروع به خلق کنید", + "wantToRun": "می‌خواهید ComfyUI را به صورت محلی اجرا کنید؟" + }, "survey": { "options": { "familiarity": { @@ -616,18 +643,6 @@ "noStackTrace": "هیچ stacktraceی موجود نیست", "promptExecutionError": "اجرای prompt با شکست مواجه شد" }, - "forgotPassword": { - "backToLogin": "بازگشت به ورود", - "didntReceiveEmail": "ایمیلی دریافت نکردید؟ با ما تماس بگیرید:", - "emailLabel": "ایمیل", - "emailPlaceholder": "ایمیل خود را وارد کنید", - "emailRequired": "وارد کردن ایمیل الزامی است", - "instructions": "آدرس ایمیل خود را وارد کنید تا لینک بازنشانی رمز عبور برای شما ارسال شود.", - "passwordResetError": "ارسال ایمیل بازنشانی رمز عبور ناموفق بود. لطفاً دوباره تلاش کنید.", - "passwordResetSent": "ایمیل بازنشانی رمز عبور ارسال شد", - "sendResetLink": "ارسال لینک بازنشانی", - "title": "فراموشی رمز عبور" - }, "g": { "1x": "۱x", "2x": "۲x", @@ -1506,6 +1521,7 @@ "Redo": "انجام مجدد", "Refresh Node Definitions": "به‌روزرسانی تعاریف Node", "Reinstall": "نصب مجدد", + "Rename": "تغییر نام", "Reset View": "بازنشانی نما", "Resize Selected Nodes": "تغییر اندازه Nodeهای انتخاب‌شده", "Restart": "راه‌اندازی مجدد", @@ -1694,10 +1710,6 @@ }, "title": "دستگاه شما پشتیبانی نمی‌شود" }, - "privateBeta": { - "desc": "برای پیوستن به لیست انتظار وارد شوید. زمانی که نوبت شما شد به شما اطلاع خواهیم داد. قبلاً اطلاع‌رسانی شده‌اید؟ وارد شوید و از Cloud استفاده کنید.", - "title": "Cloud در حال حاضر در نسخه بتای خصوصی است" - }, "progressToast": { "allDownloadsCompleted": "همه دانلودها تکمیل شدند", "downloadingModel": "در حال دانلود مدل...", @@ -1773,8 +1785,6 @@ "update": "به‌روزرسانی", "whatsNew": "مشاهده تغییرات جدید" }, - "retry": "تلاش دوباره", - "retrying": "در حال تلاش مجدد...", "rightSidePanel": { "bypass": "عبور", "color": "رنگ نود", @@ -2165,14 +2175,6 @@ }, "workflows": "Workflowها" }, - "start": { - "desc": "بدون نیاز به تنظیمات. روی هر دستگاهی کار می‌کند.", - "download": "دانلود ComfyUI", - "explain": "چندین خروجی را همزمان تولید کنید. workflowها را به راحتی به اشتراک بگذارید.", - "learnAboutButton": "درباره Cloud بیشتر بدانید", - "title": "در چند ثانیه شروع به خلق کنید", - "wantToRun": "مایلید ComfyUI را به صورت محلی اجرا کنید؟" - }, "subgraphStore": { "blueprintName": "نام زیرگراف", "confirmDelete": "این عمل باعث حذف دائمی بلوپرینت از کتابخانه شما می‌شود", @@ -2486,6 +2488,7 @@ "dropPrompt": "فایل خود را رها کنید یا" }, "widgets": { + "node2only": "فقط Node 2.0", "selectModel": "انتخاب مدل", "uploadSelect": { "placeholder": "انتخاب...", diff --git a/src/locales/fr/commands.json b/src/locales/fr/commands.json index 3673bfd09..ea272f379 100644 --- a/src/locales/fr/commands.json +++ b/src/locales/fr/commands.json @@ -254,6 +254,9 @@ "Comfy_RefreshNodeDefinitions": { "label": "Actualiser les définitions de nœud" }, + "Comfy_RenameWorkflow": { + "label": "Renommer le workflow" + }, "Comfy_SaveWorkflow": { "label": "Enregistrer le flux de travail" }, diff --git a/src/locales/fr/main.json b/src/locales/fr/main.json index 7c112275a..ec766ed07 100644 --- a/src/locales/fr/main.json +++ b/src/locales/fr/main.json @@ -60,6 +60,7 @@ "findInLibrary": "Trouvez-le dans la section {type} de la bibliothèque de modèles.", "finish": "Terminer", "genericLinkPlaceholder": "Collez le lien ici", + "importAnother": "Importer un autre", "jobId": "ID de tâche", "loadingModels": "Chargement de {type}...", "maxFileSize": "Taille maximale du fichier : {size}", @@ -1520,6 +1521,7 @@ "Redo": "Refaire", "Refresh Node Definitions": "Actualiser les définitions de nœud", "Reinstall": "Réinstaller", + "Rename": "Renommer", "Reset View": "Réinitialiser la vue", "Resize Selected Nodes": "Redimensionner les nœuds sélectionnés", "Restart": "Redémarrer", @@ -2476,6 +2478,7 @@ "dropPrompt": "Déposez votre fichier ou" }, "widgets": { + "node2only": "Node 2.0 uniquement", "selectModel": "Sélectionner un modèle", "uploadSelect": { "placeholder": "Sélectionner...", diff --git a/src/locales/ja/commands.json b/src/locales/ja/commands.json index 07814cc7a..9e3756663 100644 --- a/src/locales/ja/commands.json +++ b/src/locales/ja/commands.json @@ -254,6 +254,9 @@ "Comfy_RefreshNodeDefinitions": { "label": "ノード定義を更新" }, + "Comfy_RenameWorkflow": { + "label": "ワークフローの名前を変更" + }, "Comfy_SaveWorkflow": { "label": "ワークフローを保存する" }, diff --git a/src/locales/ja/main.json b/src/locales/ja/main.json index 27588fa1b..45ac7a31d 100644 --- a/src/locales/ja/main.json +++ b/src/locales/ja/main.json @@ -60,6 +60,7 @@ "findInLibrary": "モデルライブラリの{type}セクションで見つけることができます。", "finish": "完了", "genericLinkPlaceholder": "ここにリンクを貼り付けてください", + "importAnother": "別のファイルをインポート", "jobId": "ジョブID", "loadingModels": "{type}を読み込み中...", "maxFileSize": "最大ファイルサイズ:{size}", @@ -1520,6 +1521,7 @@ "Redo": "やり直す", "Refresh Node Definitions": "ノード定義を更新", "Reinstall": "再インストール", + "Rename": "名前を変更", "Reset View": "ビューをリセット", "Resize Selected Nodes": "選択したノードのサイズ変更", "Restart": "再起動", @@ -2476,6 +2478,7 @@ "dropPrompt": "ファイルをドロップするか" }, "widgets": { + "node2only": "Node 2.0専用", "selectModel": "モデルを選択", "uploadSelect": { "placeholder": "選択...", diff --git a/src/locales/ko/commands.json b/src/locales/ko/commands.json index 0bd325d14..c3c801373 100644 --- a/src/locales/ko/commands.json +++ b/src/locales/ko/commands.json @@ -254,6 +254,9 @@ "Comfy_RefreshNodeDefinitions": { "label": "노드 정의 새로 고침" }, + "Comfy_RenameWorkflow": { + "label": "워크플로우 이름 변경" + }, "Comfy_SaveWorkflow": { "label": "워크플로 저장" }, diff --git a/src/locales/ko/main.json b/src/locales/ko/main.json index f09cd6273..267404a84 100644 --- a/src/locales/ko/main.json +++ b/src/locales/ko/main.json @@ -60,6 +60,7 @@ "findInLibrary": "모델 라이브러리의 {type} 섹션에서 찾을 수 있습니다.", "finish": "완료", "genericLinkPlaceholder": "여기에 링크를 붙여넣으세요", + "importAnother": "다른 항목 가져오기", "jobId": "작업 ID", "loadingModels": "{type} 불러오는 중...", "maxFileSize": "최대 파일 크기: {size}", @@ -1520,6 +1521,7 @@ "Redo": "다시 실행", "Refresh Node Definitions": "노드 정의 새로 고침", "Reinstall": "재설치", + "Rename": "이름 바꾸기", "Reset View": "보기 초기화", "Resize Selected Nodes": "선택된 노드 크기 조정", "Restart": "재시작", @@ -2476,6 +2478,7 @@ "dropPrompt": "파일을 끌어다 놓거나" }, "widgets": { + "node2only": "Node 2.0 전용", "selectModel": "모델 선택", "uploadSelect": { "placeholder": "선택...", diff --git a/src/locales/pt-BR/commands.json b/src/locales/pt-BR/commands.json index 87004ec52..87ab65656 100644 --- a/src/locales/pt-BR/commands.json +++ b/src/locales/pt-BR/commands.json @@ -254,6 +254,9 @@ "Comfy_RefreshNodeDefinitions": { "label": "Atualizar Definições de Nós" }, + "Comfy_RenameWorkflow": { + "label": "Renomear fluxo de trabalho" + }, "Comfy_SaveWorkflow": { "label": "Salvar Fluxo de Trabalho" }, diff --git a/src/locales/pt-BR/main.json b/src/locales/pt-BR/main.json index 2197b2d6c..a0372cbf3 100644 --- a/src/locales/pt-BR/main.json +++ b/src/locales/pt-BR/main.json @@ -60,6 +60,7 @@ "findInLibrary": "Encontre na seção {type} da biblioteca de modelos.", "finish": "Concluir", "genericLinkPlaceholder": "Cole o link aqui", + "importAnother": "Importar outro", "jobId": "ID do trabalho", "loadingModels": "Carregando {type}...", "maxFileSize": "Tamanho máximo do arquivo: {size}", @@ -1520,6 +1521,7 @@ "Redo": "Refazer", "Refresh Node Definitions": "Atualizar definições de nós", "Reinstall": "Reinstalar", + "Rename": "Renomear", "Reset View": "Redefinir visualização", "Resize Selected Nodes": "Redimensionar nós selecionados", "Restart": "Reiniciar", @@ -2486,6 +2488,7 @@ "dropPrompt": "Solte seu arquivo ou" }, "widgets": { + "node2only": "Apenas Node 2.0", "selectModel": "Selecionar modelo", "uploadSelect": { "placeholder": "Selecionar...", diff --git a/src/locales/ru/commands.json b/src/locales/ru/commands.json index bd77a89f9..33959377d 100644 --- a/src/locales/ru/commands.json +++ b/src/locales/ru/commands.json @@ -254,6 +254,9 @@ "Comfy_RefreshNodeDefinitions": { "label": "Обновить определения нод" }, + "Comfy_RenameWorkflow": { + "label": "Переименовать рабочий процесс" + }, "Comfy_SaveWorkflow": { "label": "Сохранить рабочий процесс" }, diff --git a/src/locales/ru/main.json b/src/locales/ru/main.json index fed0f7ac8..c7a44b232 100644 --- a/src/locales/ru/main.json +++ b/src/locales/ru/main.json @@ -60,6 +60,7 @@ "findInLibrary": "Найдите это в разделе {type} библиотеки моделей.", "finish": "Готово", "genericLinkPlaceholder": "Вставьте ссылку сюда", + "importAnother": "Импортировать другой", "jobId": "ID задачи", "loadingModels": "Загрузка {type}...", "maxFileSize": "Максимальный размер файла: {size}", @@ -1520,6 +1521,7 @@ "Redo": "Повторить", "Refresh Node Definitions": "Обновить определения нод", "Reinstall": "Переустановить", + "Rename": "Переименовать", "Reset View": "Сбросить вид", "Resize Selected Nodes": "Изменить размер выбранных узлов", "Restart": "Перезапустить", @@ -2476,6 +2478,7 @@ "dropPrompt": "Перетащите ваш файл или" }, "widgets": { + "node2only": "Только Node 2.0", "selectModel": "Выбрать модель", "uploadSelect": { "placeholder": "Выбрать...", diff --git a/src/locales/tr/commands.json b/src/locales/tr/commands.json index 8ca03bcfe..66fb0657a 100644 --- a/src/locales/tr/commands.json +++ b/src/locales/tr/commands.json @@ -254,6 +254,9 @@ "Comfy_RefreshNodeDefinitions": { "label": "Düğüm Tanımlarını Yenile" }, + "Comfy_RenameWorkflow": { + "label": "Çalışma Akışını Yeniden Adlandır" + }, "Comfy_SaveWorkflow": { "label": "İş Akışını Kaydet" }, diff --git a/src/locales/tr/main.json b/src/locales/tr/main.json index 9d585c5cb..f97dd93d3 100644 --- a/src/locales/tr/main.json +++ b/src/locales/tr/main.json @@ -60,6 +60,7 @@ "findInLibrary": "Bunu modeller kütüphanesinin {type} bölümünde bulabilirsiniz.", "finish": "Bitir", "genericLinkPlaceholder": "Bağlantıyı buraya yapıştırın", + "importAnother": "Başka Birini İçe Aktar", "jobId": "İş ID", "loadingModels": "{type} yükleniyor...", "maxFileSize": "Maksimum dosya boyutu: {size}", @@ -1520,6 +1521,7 @@ "Redo": "Yinele", "Refresh Node Definitions": "Düğüm Tanımlarını Yenile", "Reinstall": "Yeniden Yükle", + "Rename": "Yeniden Adlandır", "Reset View": "Görünümü Sıfırla", "Resize Selected Nodes": "Seçili Düğümleri Yeniden Boyutlandır", "Restart": "Yeniden Başlat", @@ -2476,6 +2478,7 @@ "dropPrompt": "Dosyanızı bırakın veya" }, "widgets": { + "node2only": "Yalnızca Node 2.0", "selectModel": "Model seç", "uploadSelect": { "placeholder": "Seç...", diff --git a/src/locales/zh-TW/commands.json b/src/locales/zh-TW/commands.json index 9b98bd230..677269ac3 100644 --- a/src/locales/zh-TW/commands.json +++ b/src/locales/zh-TW/commands.json @@ -254,6 +254,9 @@ "Comfy_RefreshNodeDefinitions": { "label": "重新整理節點定義" }, + "Comfy_RenameWorkflow": { + "label": "重新命名工作流程" + }, "Comfy_SaveWorkflow": { "label": "儲存工作流程" }, diff --git a/src/locales/zh-TW/main.json b/src/locales/zh-TW/main.json index e3cae98c8..30f2913b6 100644 --- a/src/locales/zh-TW/main.json +++ b/src/locales/zh-TW/main.json @@ -60,6 +60,7 @@ "findInLibrary": "可在模型庫的 {type} 區段找到。", "finish": "完成", "genericLinkPlaceholder": "請在此貼上連結", + "importAnother": "匯入其他", "jobId": "工作 ID", "loadingModels": "正在載入 {type}...", "maxFileSize": "最大檔案大小:{size}", @@ -1520,6 +1521,7 @@ "Redo": "重做", "Refresh Node Definitions": "重新整理節點定義", "Reinstall": "重新安裝", + "Rename": "重新命名", "Reset View": "重設視圖", "Resize Selected Nodes": "調整選取節點大小", "Restart": "重新啟動", @@ -2476,6 +2478,7 @@ "dropPrompt": "拖放您的檔案或" }, "widgets": { + "node2only": "僅限 Node 2.0", "selectModel": "選擇模型", "uploadSelect": { "placeholder": "選擇...", diff --git a/src/locales/zh/commands.json b/src/locales/zh/commands.json index 7e0132964..5b830c37a 100644 --- a/src/locales/zh/commands.json +++ b/src/locales/zh/commands.json @@ -254,6 +254,9 @@ "Comfy_RefreshNodeDefinitions": { "label": "刷新节点定义" }, + "Comfy_RenameWorkflow": { + "label": "重命名工作流" + }, "Comfy_SaveWorkflow": { "label": "保存工作流" }, diff --git a/src/locales/zh/main.json b/src/locales/zh/main.json index 05a7bdbff..edf052e5e 100644 --- a/src/locales/zh/main.json +++ b/src/locales/zh/main.json @@ -60,6 +60,7 @@ "findInLibrary": "模型在模型库的{type}区里", "finish": "完成", "genericLinkPlaceholder": "粘贴链接到这", + "importAnother": "导入其他", "jobId": "任务ID", "loadingModels": "正在加载{type}...", "maxFileSize": "最大文件大小:{size}", @@ -1520,6 +1521,7 @@ "Redo": "重做", "Refresh Node Definitions": "刷新节点定义", "Reinstall": "重装", + "Rename": "重命名", "Reset View": "重置视图", "Resize Selected Nodes": "调整选定节点的大小", "Restart": "重启", @@ -2486,6 +2488,7 @@ "dropPrompt": "将文件拖到此处或" }, "widgets": { + "node2only": "仅限 Node 2.0", "selectModel": "选择模型", "uploadSelect": { "placeholder": "请选择...", diff --git a/src/platform/assets/components/AssetBrowserModal.vue b/src/platform/assets/components/AssetBrowserModal.vue index 001387d05..b7db7494c 100644 --- a/src/platform/assets/components/AssetBrowserModal.vue +++ b/src/platform/assets/components/AssetBrowserModal.vue @@ -57,6 +57,7 @@ :assets="filteredAssets" :loading="isLoading" @asset-select="handleAssetSelectAndEmit" + @asset-deleted="refreshAssets" /> diff --git a/src/platform/assets/components/AssetCard.vue b/src/platform/assets/components/AssetCard.vue index a24610022..11e404d41 100644 --- a/src/platform/assets/components/AssetCard.vue +++ b/src/platform/assets/components/AssetCard.vue @@ -1,6 +1,5 @@ @@ -56,6 +57,7 @@ const { assets } = defineProps<{ defineEmits<{ assetSelect: [asset: AssetDisplayItem] + assetDeleted: [asset: AssetDisplayItem] }>() const assetsWithKey = computed(() => diff --git a/src/platform/assets/components/MediaAssetCard.vue b/src/platform/assets/components/MediaAssetCard.vue index 73273611b..8da39c67b 100644 --- a/src/platform/assets/components/MediaAssetCard.vue +++ b/src/platform/assets/components/MediaAssetCard.vue @@ -17,12 +17,14 @@ 'gap-2 select-none group', selected ? 'ring-3 ring-inset ring-modal-card-border-highlighted' - : 'hover:bg-modal-card-background-hovered' + : 'hover:bg-modal-card-background-hovered/20' ) " :data-selected="selected" @click.stop="$emit('click')" - @contextmenu.prevent="handleContextMenu" + @contextmenu.prevent.stop=" + asset ? emit('context-menu', $event, asset) : undefined + " >
@@ -64,7 +66,9 @@ variant="overlay-white" size="icon" :aria-label="$t('mediaAsset.actions.moreOptions')" - @click.stop="handleContextMenu" + @click.stop=" + asset ? emit('context-menu', $event, asset) : undefined + " > @@ -119,25 +123,10 @@
- - diff --git a/src/platform/assets/components/MediaAssetContextMenu.vue b/src/platform/assets/components/MediaAssetContextMenu.vue index 9325f7a17..0b54ce33b 100644 --- a/src/platform/assets/components/MediaAssetContextMenu.vue +++ b/src/platform/assets/components/MediaAssetContextMenu.vue @@ -11,6 +11,7 @@ ) } }" + @hide="emit('hide')" > diff --git a/src/platform/assets/components/UploadModelDialog.vue b/src/platform/assets/components/UploadModelDialog.vue index 5013e418d..f7ad5de71 100644 --- a/src/platform/assets/components/UploadModelDialog.vue +++ b/src/platform/assets/components/UploadModelDialog.vue @@ -48,6 +48,7 @@ @fetch-metadata="handleFetchMetadata" @upload="handleUploadModel" @close="handleClose" + @import-another="resetWizard" /> @@ -85,7 +86,8 @@ const { canUploadModel, fetchMetadata, uploadModel, - goToPreviousStep + goToPreviousStep, + resetWizard } = useUploadModelWizard(modelTypes) async function handleFetchMetadata() { diff --git a/src/platform/assets/components/UploadModelFooter.vue b/src/platform/assets/components/UploadModelFooter.vue index 04c27c394..95ca59e45 100644 --- a/src/platform/assets/components/UploadModelFooter.vue +++ b/src/platform/assets/components/UploadModelFooter.vue @@ -80,21 +80,33 @@ {{ $t('assetBrowser.upload') }} - + + + () diff --git a/src/platform/assets/components/UploadModelUrlInput.vue b/src/platform/assets/components/UploadModelUrlInput.vue index 8b814c0e5..d2c743d7a 100644 --- a/src/platform/assets/components/UploadModelUrlInput.vue +++ b/src/platform/assets/components/UploadModelUrlInput.vue @@ -20,7 +20,7 @@ :href="civitaiUrl" target="_blank" rel="noopener noreferrer" - class="text-muted underline" + class="text-muted-foreground underline" > {{ $t('assetBrowser.providerCivitai') }}, @@ -35,7 +35,7 @@ :href="huggingFaceUrl" target="_blank" rel="noopener noreferrer" - class="text-muted underline" + class="text-muted-foreground underline" > {{ $t('assetBrowser.providerHuggingFace') }} @@ -58,7 +58,7 @@ class="icon-[lucide--circle-check-big] absolute top-1/2 right-3 size-5 -translate-y-1/2 text-green-500" /> -

+

{{ error }}

diff --git a/src/platform/assets/components/UploadModelUrlInputCivitai.vue b/src/platform/assets/components/UploadModelUrlInputCivitai.vue index 39e244c86..82c1dcf31 100644 --- a/src/platform/assets/components/UploadModelUrlInputCivitai.vue +++ b/src/platform/assets/components/UploadModelUrlInputCivitai.vue @@ -11,7 +11,7 @@ {{ $t('assetBrowser.uploadModelDescription2Link') }} @@ -51,14 +51,14 @@ class="icon-[lucide--circle-check-big] absolute top-1/2 right-3 size-5 -translate-y-1/2 text-green-500" /> -

+

{{ error }}

@@ -148,6 +165,7 @@ import { LiteGraph, RenderShape } from '@/lib/litegraph/src/litegraph' +import { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode' import { TitleMode } from '@/lib/litegraph/src/types/globalEnums' import { useSettingStore } from '@/platform/settings/settingStore' import { useTelemetry } from '@/platform/telemetry' @@ -168,7 +186,7 @@ import { applyLightThemeColor } from '@/renderer/extensions/vueNodes/utils/nodeS import { app } from '@/scripts/app' import { useExecutionStore } from '@/stores/executionStore' import { useNodeOutputStore } from '@/stores/imagePreviewStore' -import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore' +import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore' import { isTransparent } from '@/utils/colorUtil' import { getLocatorIdFromNodeData, @@ -177,6 +195,7 @@ import { import { cn } from '@/utils/tailwindUtil' import { useNodeResize } from '../interactions/resize/useNodeResize' +import { WidgetInputBaseClass } from '../widgets/components/layout' import LivePreview from './LivePreview.vue' import NodeContent from './NodeContent.vue' import NodeHeader from './NodeHeader.vue' @@ -228,19 +247,6 @@ const bypassed = computed( ) const muted = computed((): boolean => nodeData.mode === LGraphEventMode.NEVER) -const nodeBodyBackgroundColor = computed(() => { - const colorPaletteStore = useColorPaletteStore() - - if (!nodeData.bgcolor) { - return '' - } - - return applyLightThemeColor( - nodeData.bgcolor, - Boolean(colorPaletteStore.completedActivePalette.light_theme) - ) -}) - const nodeOpacity = computed(() => { const globalOpacity = useSettingStore().get('Comfy.Node.Opacity') ?? 1 @@ -474,6 +480,22 @@ const lgraphNode = computed(() => { return getNodeByLocatorId(app.rootGraph, locatorId) }) +const showAdvancedInputsButton = computed(() => { + const node = lgraphNode.value + if (!node || !(node instanceof SubgraphNode)) return false + + // Check if there are hidden inputs (widgets not promoted) + const interiorNodes = node.subgraph.nodes + const allInteriorWidgets = interiorNodes.flatMap((n) => n.widgets ?? []) + + return allInteriorWidgets.some((w) => !w.computedDisabled && !w.promoted) +}) + +function handleShowAdvancedInputs() { + const rightSidePanelStore = useRightSidePanelStore() + rightSidePanelStore.focusSection('advanced-inputs') +} + const nodeMedia = computed(() => { const newOutputs = nodeOutputs.nodeOutputs[nodeOutputLocatorId.value] const node = lgraphNode.value diff --git a/src/renderer/extensions/vueNodes/components/NodeHeader.vue b/src/renderer/extensions/vueNodes/components/NodeHeader.vue index fe99c5d62..0f24cf873 100644 --- a/src/renderer/extensions/vueNodes/components/NodeHeader.vue +++ b/src/renderer/extensions/vueNodes/components/NodeHeader.vue @@ -11,7 +11,10 @@ headerShapeClass ) " - :style="headerStyle" + :style="{ + backgroundColor: applyLightThemeColor(nodeData?.color), + opacity: useSettingStore().get('Comfy.Node.Opacity') ?? 1 + }" :data-testid="`node-header-${nodeData?.id || ''}`" @dblclick="handleDoubleClick" > @@ -104,7 +107,6 @@ import NodeBadge from '@/renderer/extensions/vueNodes/components/NodeBadge.vue' import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips' import { applyLightThemeColor } from '@/renderer/extensions/vueNodes/utils/nodeStyleUtils' import { app } from '@/scripts/app' -import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore' import { normalizeI18nKey } from '@/utils/formatUtil' import { getLocatorIdFromNodeData, @@ -156,23 +158,6 @@ const enterSubgraphTooltipConfig = computed(() => { return createTooltipConfig(st('enterSubgraph', 'Enter Subgraph')) }) -const headerStyle = computed(() => { - const colorPaletteStore = useColorPaletteStore() - - const opacity = useSettingStore().get('Comfy.Node.Opacity') ?? 1 - - if (!nodeData?.color) { - return { backgroundColor: '', opacity } - } - - const headerColor = applyLightThemeColor( - nodeData.color, - Boolean(colorPaletteStore.completedActivePalette.light_theme) - ) - - return { backgroundColor: headerColor, opacity } -}) - const resolveTitle = (info: VueNodeData | undefined) => { const title = (info?.title ?? '').trim() if (title.length > 0) return title diff --git a/src/renderer/extensions/vueNodes/utils/nodeStyleUtils.ts b/src/renderer/extensions/vueNodes/utils/nodeStyleUtils.ts index 143684d13..57a0b9717 100644 --- a/src/renderer/extensions/vueNodes/utils/nodeStyleUtils.ts +++ b/src/renderer/extensions/vueNodes/utils/nodeStyleUtils.ts @@ -1,14 +1,13 @@ +import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore' import { adjustColor } from '@/utils/colorUtil' /** * Applies light theme color adjustments to a color */ -export function applyLightThemeColor( - color: string, - isLightTheme: boolean -): string { - if (!color || !isLightTheme) { - return color - } +export function applyLightThemeColor(color?: string): string { + if (!color) return '' + + if (!useColorPaletteStore().completedActivePalette.light_theme) return color + return adjustColor(color, { lightness: 0.5 }) } diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.test.ts index 6fbadb517..7eb3890bc 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.test.ts @@ -1,12 +1,16 @@ -import { mount } from '@vue/test-utils' -import PrimeVue from 'primevue/config' -import InputNumber from 'primevue/inputnumber' import { describe, expect, it } from 'vitest' +import { mount } from '@vue/test-utils' +import { createI18n } from 'vue-i18n' import type { SimplifiedWidget } from '@/types/simplifiedWidget' import WidgetInputNumberInput from './WidgetInputNumberInput.vue' +const i18n = createI18n({ + legacy: false, + locale: 'en' +}) + function createMockWidget( value: number = 0, type: 'int' | 'float' = 'int', @@ -24,10 +28,7 @@ function createMockWidget( function mountComponent(widget: SimplifiedWidget, modelValue: number) { return mount(WidgetInputNumberInput, { - global: { - plugins: [PrimeVue], - components: { InputNumber } - }, + global: { plugins: [i18n] }, props: { widget, modelValue @@ -36,7 +37,7 @@ function mountComponent(widget: SimplifiedWidget, modelValue: number) { } function getNumberInput(wrapper: ReturnType) { - const input = wrapper.get('input[inputmode="numeric"]') + const input = wrapper.get('input[inputmode="decimal"]') return input.element } @@ -53,7 +54,7 @@ describe('WidgetInputNumberInput Value Binding', () => { const widget = createMockWidget(10, 'int') const wrapper = mountComponent(widget, 10) - const inputNumber = wrapper.findComponent(InputNumber) + const inputNumber = wrapper await inputNumber.vm.$emit('update:modelValue', 20) const emitted = wrapper.emitted('update:modelValue') @@ -78,75 +79,6 @@ describe('WidgetInputNumberInput Value Binding', () => { }) }) -describe('WidgetInputNumberInput Component Rendering', () => { - it('renders InputNumber component with show-buttons', () => { - const widget = createMockWidget(5, 'int') - const wrapper = mountComponent(widget, 5) - - const inputNumber = wrapper.findComponent(InputNumber) - expect(inputNumber.exists()).toBe(true) - expect(inputNumber.props('showButtons')).toBe(true) - }) - - it('sets button layout to horizontal', () => { - const widget = createMockWidget(5, 'int') - const wrapper = mountComponent(widget, 5) - - const inputNumber = wrapper.findComponent(InputNumber) - expect(inputNumber.props('buttonLayout')).toBe('horizontal') - }) - - it('sets size to small', () => { - const widget = createMockWidget(5, 'int') - const wrapper = mountComponent(widget, 5) - - const inputNumber = wrapper.findComponent(InputNumber) - expect(inputNumber.props('size')).toBe('small') - }) -}) - -describe('WidgetInputNumberInput Step Value', () => { - it('defaults to 0 for unrestricted stepping', () => { - const widget = createMockWidget(5, 'int') - const wrapper = mountComponent(widget, 5) - - const inputNumber = wrapper.findComponent(InputNumber) - expect(inputNumber.props('step')).toBe(0) - }) - - it('uses step2 value when provided', () => { - const widget = createMockWidget(5, 'int', { step2: 0.5 }) - const wrapper = mountComponent(widget, 5) - - const inputNumber = wrapper.findComponent(InputNumber) - expect(inputNumber.props('step')).toBe(0.5) - }) - - it('calculates step from precision for precision 0', () => { - const widget = createMockWidget(5, 'int', { precision: 0 }) - const wrapper = mountComponent(widget, 5) - - const inputNumber = wrapper.findComponent(InputNumber) - expect(inputNumber.props('step')).toBe(1) - }) - - it('calculates step from precision for precision 1', () => { - const widget = createMockWidget(5, 'float', { precision: 1 }) - const wrapper = mountComponent(widget, 5) - - const inputNumber = wrapper.findComponent(InputNumber) - expect(inputNumber.props('step')).toBe(0.1) - }) - - it('calculates step from precision for precision 2', () => { - const widget = createMockWidget(5, 'float', { precision: 2 }) - const wrapper = mountComponent(widget, 5) - - const inputNumber = wrapper.findComponent(InputNumber) - expect(inputNumber.props('step')).toBe(0.01) - }) -}) - describe('WidgetInputNumberInput Grouping Behavior', () => { it('displays numbers without commas by default for int widgets', () => { const widget = createMockWidget(1000, 'int') @@ -202,24 +134,21 @@ describe('WidgetInputNumberInput Large Integer Precision Handling', () => { const widget = createMockWidget(1000, 'int') const wrapper = mountComponent(widget, 1000) - const inputNumber = wrapper.findComponent(InputNumber) - expect(inputNumber.props('showButtons')).toBe(true) + expect(wrapper.findAll('button').length).toBe(2) }) it('shows buttons for values at safe integer limit', () => { const widget = createMockWidget(SAFE_INTEGER_MAX, 'int') const wrapper = mountComponent(widget, SAFE_INTEGER_MAX) - const inputNumber = wrapper.findComponent(InputNumber) - expect(inputNumber.props('showButtons')).toBe(true) + expect(wrapper.findAll('button').length).toBe(2) }) it('hides buttons for unsafe large integer values', () => { const widget = createMockWidget(UNSAFE_LARGE_INTEGER, 'int') const wrapper = mountComponent(widget, UNSAFE_LARGE_INTEGER) - const inputNumber = wrapper.findComponent(InputNumber) - expect(inputNumber.props('showButtons')).toBe(false) + expect(wrapper.findAll('button').length).toBe(0) }) it('hides buttons for unsafe negative integer values', () => { @@ -227,8 +156,7 @@ describe('WidgetInputNumberInput Large Integer Precision Handling', () => { const widget = createMockWidget(unsafeNegative, 'int') const wrapper = mountComponent(widget, unsafeNegative) - const inputNumber = wrapper.findComponent(InputNumber) - expect(inputNumber.props('showButtons')).toBe(false) + expect(wrapper.findAll('button').length).toBe(0) }) it('shows tooltip for disabled buttons due to precision limits', (context) => { @@ -250,43 +178,19 @@ describe('WidgetInputNumberInput Large Integer Precision Handling', () => { expect(tooltipDiv.attributes('v-tooltip')).toBeUndefined() }) - it('handles edge case of zero value', () => { - const widget = createMockWidget(0, 'int') - const wrapper = mountComponent(widget, 0) + it('handles floating point values correctly', () => { + const widget = createMockWidget(1000.5, 'float') + const wrapper = mountComponent(widget, 1000.5) - const inputNumber = wrapper.findComponent(InputNumber) - expect(inputNumber.props('showButtons')).toBe(true) + expect(wrapper.findAll('button').length).toBe(2) }) - it('correctly identifies safe vs unsafe integers using Number.isSafeInteger', () => { - // Test the JavaScript behavior our component relies on - expect(Number.isSafeInteger(SAFE_INTEGER_MAX)).toBe(true) - expect(Number.isSafeInteger(SAFE_INTEGER_MAX + 1)).toBe(false) - expect(Number.isSafeInteger(UNSAFE_LARGE_INTEGER)).toBe(false) - expect(Number.isSafeInteger(-SAFE_INTEGER_MAX)).toBe(true) - expect(Number.isSafeInteger(-SAFE_INTEGER_MAX - 1)).toBe(false) - }) - - it('handles floating point values correctly', (context) => { - context.skip('needs diagnosis') - - const safeFloat = 1000.5 - const widget = createMockWidget(safeFloat, 'float') - const wrapper = mountComponent(widget, safeFloat) - - const inputNumber = wrapper.findComponent(InputNumber) - expect(inputNumber.props('showButtons')).toBe(true) - }) - - it('hides buttons for unsafe floating point values', (context) => { - context.skip('needs diagnosis') - + it('hides buttons for unsafe floating point values', () => { const unsafeFloat = UNSAFE_LARGE_INTEGER + 0.5 const widget = createMockWidget(unsafeFloat, 'float') const wrapper = mountComponent(widget, unsafeFloat) - const inputNumber = wrapper.findComponent(InputNumber) - expect(inputNumber.props('showButtons')).toBe(false) + expect(wrapper.findAll('button').length).toBe(0) }) }) @@ -295,18 +199,14 @@ describe('WidgetInputNumberInput Edge Cases for Precision Handling', () => { const widget = createMockWidget(0, 'int') // Mount with undefined as modelValue const wrapper = mount(WidgetInputNumberInput, { - global: { - plugins: [PrimeVue], - components: { InputNumber } - }, + global: { plugins: [i18n] }, props: { widget, modelValue: undefined as any } }) - const inputNumber = wrapper.findComponent(InputNumber) - expect(inputNumber.props('showButtons')).toBe(true) // Should default to safe behavior + expect(wrapper.findAll('button').length).toBe(2) }) it('handles NaN values gracefully', (context) => { @@ -314,24 +214,20 @@ describe('WidgetInputNumberInput Edge Cases for Precision Handling', () => { const widget = createMockWidget(NaN, 'int') const wrapper = mountComponent(widget, NaN) - const inputNumber = wrapper.findComponent(InputNumber) - // NaN is not a safe integer, so buttons should be hidden - expect(inputNumber.props('showButtons')).toBe(false) + expect(wrapper.findAll('button').length).toBe(0) }) it('handles Infinity values', () => { const widget = createMockWidget(Infinity, 'int') const wrapper = mountComponent(widget, Infinity) - const inputNumber = wrapper.findComponent(InputNumber) - expect(inputNumber.props('showButtons')).toBe(false) + expect(wrapper.findAll('button').length).toBe(0) }) it('handles negative Infinity values', () => { const widget = createMockWidget(-Infinity, 'int') const wrapper = mountComponent(widget, -Infinity) - const inputNumber = wrapper.findComponent(InputNumber) - expect(inputNumber.props('showButtons')).toBe(false) + expect(wrapper.findAll('button').length).toBe(0) }) }) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue index fb6be5cb8..1add11f4a 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue @@ -1,7 +1,9 @@ diff --git a/src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdown.vue b/src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdown.vue index 670c054a4..22d6db5ff 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdown.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdown.vue @@ -1,7 +1,6 @@