Compare commits

..

14 Commits

Author SHA1 Message Date
Christian Byrne
4f5bbe0605 [refactor] Remove legacy manager UI support and tag from header (#5665)
## Summary

Removed the informational "Use Legacy UI" tag from the ManagerHeader
component while preserving all underlying legacy manager functionality.

## Changes

- **What**: Removed Tag component displaying legacy UI information from
ManagerHeader
- **Breaking**: None - all legacy manager functionality remains intact
- **Dependencies**: None

## Review Focus

Visual cleanup only - the `--enable-manager-legacy-ui` CLI flag and all
related functionality continues to work normally. Only the informational
UI tag has been removed from the header.
2025-09-19 02:07:51 -07:00
Christian Byrne
a975e50f1b [feat] Add tooltip support for Vue nodes (#5577)
## Summary

Added tooltip support for Vue node components using PrimeVue's v-tooltip
directive with proper data integration and container scoping.


https://github.com/user-attachments/assets/d1af31e6-ef6a-4df8-8de4-5098aa4490a1

## Changes

- **What**: Implemented tooltip functionality for Vue node headers,
input/output slots, and widgets using [PrimeVue
v-tooltip](https://primevue.org/tooltip/) directive
- **Dependencies**: Leverages existing PrimeVue tooltip system, no new
dependencies

## Review Focus

Container scoping implementation via provide/inject pattern for tooltip
positioning, proper TypeScript interfaces eliminating `as any` casts,
and integration with existing settings store for tooltip delays and
enable/disable functionality.

```mermaid
graph TD
    A[LGraphNode Container] --> B[provide tooltipContainer]
    B --> C[NodeHeader inject]
    B --> D[InputSlot inject]
    B --> E[OutputSlot inject]
    B --> F[NodeWidgets inject]

    G[useNodeTooltips composable] --> H[NodeDefStore lookup]
    G --> I[Settings integration]
    G --> J[i18n fallback]

    C --> G
    D --> G
    E --> G
    F --> G

    style A fill:#f9f9f9,stroke:#333,color:#000
    style G fill:#e8f4fd,stroke:#0066cc,color:#000
```

---------

Co-authored-by: DrJKL <DrJKL@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2025-09-19 01:07:50 -07:00
Christian Byrne
a17c74fa0c fix: add optional chaining to nodeDef access in NodeTooltip (#5663)
## Summary

Extension of https://github.com/Comfy-Org/ComfyUI_frontend/pull/5659:
Added optional chaining to NodeTooltip component to prevent TypeError
when `nodeDef` is undefined for unknown node types.

## Changes

- **What**: Added [optional chaining
operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining)
(`?.`) to safely access `nodeDef` properties in NodeTooltip component

## Review Focus

Error handling for node types not found in nodeDefStore and tooltip
display behavior for unrecognized nodes.

Fixes CLOUD-FRONTEND-STAGING-3N

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-19 01:04:12 -07:00
Christian Byrne
5e625a5002 [test] add Vue FormSelectButton widget component tests (#5576)
## Summary

Added comprehensive component tests for FormSelectButton widget with 497
test cases covering all interaction patterns and edge cases.

## Changes

- **What**: Created test suite for
[FormSelectButton.vue](https://vuejs.org/guide/scaling-up/testing.html)
component with full coverage of string/number/object options, PrimeVue
compatibility, disabled states, and visual styling
- **Dependencies**: No new dependencies (uses existing vitest,
@vue/test-utils)

## Review Focus

Test completeness covering edge cases like unicode characters, duplicate
values, and objects with missing properties. Verify test helper
functions correctly simulate user interactions.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5576-Add-Vue-FormSelectButton-widget-component-tests-26f6d73d36508171ae08ee74d0605db2)
by [Unito](https://www.unito.io)

---------

Co-authored-by: DrJKL <DrJKL@users.noreply.github.com>
2025-09-19 00:12:25 -07:00
Christian Byrne
002fac0232 [refactor] Migrate manager code to DDD structure (#5662)
## Summary

Reorganized custom nodes manager functionality from scattered technical
layers into a cohesive domain-focused module following [domain-driven
design](https://en.wikipedia.org/wiki/Domain-driven_design) principles.

## Changes

- **What**: Migrated all manager code from technical layers
(`src/components/`, `src/stores/`, etc.) to unified domain structure at
`src/workbench/extensions/manager/`
- **Breaking**: Import paths changed for all manager-related modules
(40+ files updated)

## Review Focus

Verify all import path updates are correct and no circular dependencies
introduced. Check that [Vue 3 composition
API](https://vuejs.org/guide/reusability/composables.html) patterns
remain consistent across relocated composables.


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5662-refactor-Migrate-manager-code-to-DDD-structure-2736d73d3650812c87faf6ed0fffb196)
by [Unito](https://www.unito.io)
2025-09-19 00:03:05 -07:00
Christian Byrne
7e115543fa fix: prevent TypeError when nodeDef is undefined in NodeTooltip (#5659)
## Summary

Fix TypeError in NodeTooltip component when `nodeDef` is undefined. This
occurs when hovering over nodes whose type is not found in the
nodeDefStore.

## Changes

- Add optional chaining (`?.`) to `nodeDef.description` access on line
71
- Follows the same defensive pattern used in previous fixes for similar
issues

## Context

This addresses Sentry issue
[CLOUD-FRONTEND-STAGING-1B](https://comfy-org.sentry.io/issues/6829258525/)
which shows 19 occurrences affecting 14 users.

The fix follows the same pattern as previous commits:
-
[290bf52fc](290bf52fc5)
- Fixed similar issue on line 112
-
[e8997a765](e8997a7653)
- Fixed multiple similar issues


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5659-fix-prevent-TypeError-when-nodeDef-is-undefined-in-NodeTooltip-2736d73d3650816e8be3f44889198b58)
by [Unito](https://www.unito.io)
2025-09-18 23:53:58 -07:00
Christian Byrne
80d75bb164 fix TypeError: nodes is not iterable when loading graph (#5660)
## Summary
- Fixes Sentry issue CLOUD-FRONTEND-STAGING-29 (TypeError: nodes is not
iterable)
- Adds defensive guard to check if nodes is valid array before iteration
- Gracefully handles malformed workflow data by skipping node processing

## Root Cause
The `collectMissingNodesAndModels` function in `src/scripts/app.ts:1135`
was attempting to iterate over `nodes` without checking if it was a
valid iterable, causing crashes when workflow data was malformed or
missing the nodes property.

## Fix
Added null/undefined/array validation before the for-loop:
```typescript
if (\!nodes || \!Array.isArray(nodes)) {
  console.warn('Workflow nodes data is missing or invalid, skipping node processing', { nodes, path })
  return
}
```

Fixes CLOUD-FRONTEND-STAGING-29

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5660-fix-TypeError-nodes-is-not-iterable-when-loading-graph-2736d73d365081cfb828d27e59a4811c)
by [Unito](https://www.unito.io)
2025-09-18 23:27:42 -07:00
snomiao
d59885839a fix: correct Claude PR review to use BASE_SHA for accurate diff comparison (#5654)
## Summary
- Fixes the Claude automated PR review comparing against wrong commits
- Updates the comprehensive-pr-review.md command to use `$BASE_SHA`
instead of `origin/$BASE_BRANCH`
- Resolves issue where Claude was reviewing unrelated changes from other
PRs

## Problem
As identified in #5651 (comment
https://github.com/Comfy-Org/ComfyUI_frontend/pull/5651#issuecomment-3310416767),
the Claude automated review was incorrectly analyzing changes that
weren't part of the PR being reviewed. The review was mentioning Turkish
language removal, linkRenderer changes, and other modifications that
weren't in the actual PR diff.

## Root Cause Analysis

### The Issue Explained (from Discord discussion)
When Christian Byrne noticed Claude was referencing things from previous
reviews on other PRs, we investigated and found:

1. **The backport branch was created from origin/main BEFORE Turkish
language support was merged**
   - Branch state: `main.A`
   - Backport changes committed: `main.A.Backport`

2. **Turkish language support was then merged into origin/main**
   - Main branch updated to: `main.A.Turkish`

3. **Claude review workflow checked out `main.A.Backport` and ran git
diff against `origin/main`**
   - This compared: `main.A.Backport <> main.A.Turkish`
   - The diff showed: `+++Backport` changes and `---Turkish` removal
   - Because the common parent of both branches was `main.A`

### Why This Happens
When using `origin/$BASE_BRANCH`, git resolves to the latest commit on
that branch. The diff includes:
1. The PR's actual changes (+++Backport)
2. The reverse of all commits merged to main since the PR was created
(---Turkish)

This causes Claude to review changes that appear as "removals" of code
from other merged PRs, leading to confusing comments about unrelated
code.

## Solution
Changed the git diff commands to use `$BASE_SHA` directly, which GitHub
Actions provides as the exact commit SHA that represents the merge base.
This ensures Claude only reviews the actual changes introduced by the
PR.

### Before (incorrect):
```bash
git diff --name-only "origin/$BASE_BRANCH"  # Compares against latest main
git diff "origin/$BASE_BRANCH"
git diff --name-status "origin/$BASE_BRANCH"
```

### After (correct):
```bash
git diff --name-only "$BASE_SHA"  # Compares against merge base
git diff "$BASE_SHA"
git diff --name-status "$BASE_SHA"
```

## Technical Details

### GitHub Actions Environment Variables
- `BASE_SHA`: The commit SHA of the merge base (where PR branched from
main)
- `BASE_BRANCH`: Not provided by GitHub Actions (this was the bug)
- Using `origin/$BASE_BRANCH` was falling back to comparing against the
latest main commit

### Alternative Approaches Considered
1. **Approach 1**: Rebase/update branch before running Claude review
   - Downside: Changes the PR's commits, not always desirable
2. **Approach 2**: Use BASE_SHA to diff against the merge base 
   - This is what GitHub's PR diff view does
   - Shows only the changes introduced by the PR

## Testing
The BASE_SHA environment variable is already correctly set in the
claude-pr-review.yml workflow (line 88), so this change will work
immediately once merged.

## Impact
- Claude reviews will now be accurate and only analyze the actual PR
changes
- No false positives about "removed" code from other PRs
- More reliable automated PR review process
- Developers won't be confused by comments about code they didn't change

## Verification
You can verify this fix by:
1. Creating a PR from an older branch
2. Merging another PR to main
3. Triggering Claude review with the label
4. Claude should only review the PR's changes, not show removals from
the newly merged commits

## Credits
Thanks to @Christian-Byrne for reporting the issue and @snomiao for the
root cause analysis.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-18 22:09:20 -07:00
snomiao
cbb0f765b8 feat: enable verbatimModuleSyntax in TypeScript config (#5533)
## Summary
- Enable `verbatimModuleSyntax` compiler option in TypeScript
configuration
- Update all type imports to use explicit `import type` syntax
- This change will Improve tree-shaking and bundler compatibility

## Motivation
The `verbatimModuleSyntax` option ensures that type-only imports are
explicitly marked with the `type` keyword. This:
- Makes import/export intentions clearer
- Improves tree-shaking by helping bundlers identify what can be safely
removed
- Ensures better compatibility with modern bundlers
- Follows TypeScript best practices for module syntax

## Changes
- Added `"verbatimModuleSyntax": true` to `tsconfig.json`
- Updated another 48+ files to use explicit `import type` syntax for
type-only imports
- No functional changes, only import/export syntax improvements

## Test Plan
- [x] TypeScript compilation passes
- [x] Build completes successfully  
- [x] Tests pass
- [ ] No runtime behavior changes

🤖 Generated with [Claude Code](https://claude.ai/code)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5533-feat-enable-verbatimModuleSyntax-in-TypeScript-config-26d6d73d36508190b424ef9b379b5130)
by [Unito](https://www.unito.io)
2025-09-18 21:05:56 -07:00
Christian Byrne
726a2fbbc9 feat: add manual dispatch to backport workflow (#5651)
Enables manual backport triggering for scenarios where labels are added
after PR merge.

Adds workflow_dispatch trigger to the backport workflow with support
for:
- Specifying PR number to backport post-merge
- Force rerun option to override duplicate detection  
- Proper handling of multi-version backport scenarios

Solves the issue where adding version labels (e.g., 1.27) after a PR is
already merged and backported (e.g., to 1.26) would not trigger
additional backports.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5651-feat-add-manual-dispatch-to-backport-workflow-2736d73d365081b6ba00c7a43c9ba06b)
by [Unito](https://www.unito.io)
2025-09-18 21:01:07 -07:00
snomiao
553b5aa02b feat: Add Turkish language support (#5438)
## Summary
- Added complete Turkish language translation for ComfyUI Frontend
- Integrated Turkish locale into the i18n system
- Added Turkish as a selectable language option in settings

## Implementation Details
- Added Turkish translation files provided by @naxci1:
  - `src/locales/tr/main.json` - Main UI translations
  - `src/locales/tr/commands.json` - Command translations
  - `src/locales/tr/nodeDefs.json` - Node definitions translations
  - `src/locales/tr/settings.json` - Settings translations
- Updated `src/i18n.ts` to import and register Turkish locale
- Added Turkish option to language selector in
`src/constants/coreSettings.ts`

## Test Plan
- [ ] Verify Turkish translations load correctly
- [ ] Test language switching to/from Turkish
- [ ] Check all UI elements display properly in Turkish
- [ ] Verify node descriptions and tooltips in Turkish
- [ ] Test command palette in Turkish

Fixes #5437

🤖 Generated with [Claude Code](https://claude.ai/code)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5438-feat-Add-Turkish-language-support-2686d73d36508184bbf2dc1e0cd15350)
by [Unito](https://www.unito.io)
2025-09-18 19:43:53 -07:00
Benjamin Lu
2ff0d951ed Slot functionality for vue nodes (#5628)
Allows for simple slot functionality in vue nodes mode.

Has:
- Drag new link from slot
- Connect new link from dropping on slot

Now:
- Tests

After:
- Drop on reroute
- Correct link color on connect
- Drop on node
- Hover effects

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5628-Slot-functionality-for-vue-nodes-2716d73d365081c59a3cef7c8a5e539e)
by [Unito](https://www.unito.io)

---------

Co-authored-by: bymyself <cbyrne@comfy.org>
Co-authored-by: AustinMroz <austin@comfy.org>
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-18 19:35:15 -07:00
Christian Byrne
1f88925144 fix: don't immediately close missing nodes dialog if manager is disabled (#5647)
If manager is disabled, it assumed all missing nodes are installed and
immediately closes the missing nodes warning when loading a workflow
with missing nodes.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5647-fix-don-t-immediately-close-missing-nodes-dialog-if-manager-is-disabled-2736d73d36508199a50bca2026528ab6)
by [Unito](https://www.unito.io)
2025-09-18 17:54:37 -07:00
AustinMroz
250433a91a Fix SaveAs (#5643)
Implementing subgraph blueprints (#5139) included changes to saving to
ensure that SaveAs generates a new workflow of the correct type. However
this code failed to utilize the pre-prepared state when performing the
actual save. This produced a couple of problems with both failing to
detach the workflow and failing to apply the correct state

This error is only encountered when using Save As from a non temporary
workflow (one loaded from the workflows sidebar tab).

As this state calculation code is only used in this code path, it has
been moved into the saveAs function of the workflowStore.

Resolves #5592

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5643-Fix-SaveAs-2726d73d3650818faa7af449d1f13c26)
by [Unito](https://www.unito.io)
2025-09-18 16:56:49 -07:00
154 changed files with 13137 additions and 611 deletions

View File

@@ -67,9 +67,9 @@ This is critical for better file inspection:
Use git locally for much faster analysis:
1. Get list of changed files: `git diff --name-only "origin/$BASE_BRANCH" > changed_files.txt`
2. Get the full diff: `git diff "origin/$BASE_BRANCH" > pr_diff.txt`
3. Get detailed file changes with status: `git diff --name-status "origin/$BASE_BRANCH" > file_changes.txt`
1. Get list of changed files: `git diff --name-only "$BASE_SHA" > changed_files.txt`
2. Get the full diff: `git diff "$BASE_SHA" > pr_diff.txt`
3. Get detailed file changes with status: `git diff --name-status "$BASE_SHA" > file_changes.txt`
### Step 1.5: Create Analysis Cache

2
.gitattributes vendored
View File

@@ -13,4 +13,4 @@
# Generated files
src/types/comfyRegistryTypes.ts linguist-generated=true
src/types/generatedManagerTypes.ts linguist-generated=true
src/workbench/extensions/manager/types/generatedManagerTypes.ts linguist-generated=true

View File

@@ -4,10 +4,25 @@ on:
pull_request_target:
types: [closed, labeled]
branches: [main]
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to backport'
required: true
type: string
force_rerun:
description: 'Force rerun even if backports exist'
required: false
type: boolean
default: false
jobs:
backport:
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'needs-backport')
if: >
(github.event_name == 'pull_request_target' &&
github.event.pull_request.merged == true &&
contains(github.event.pull_request.labels.*.name, 'needs-backport')) ||
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: write
@@ -15,6 +30,35 @@ jobs:
issues: write
steps:
- name: Validate inputs for manual triggers
if: github.event_name == 'workflow_dispatch'
run: |
# Validate PR number format
if ! [[ "${{ inputs.pr_number }}" =~ ^[0-9]+$ ]]; then
echo "::error::Invalid PR number format. Must be a positive integer."
exit 1
fi
# Validate PR exists and is merged
if ! gh pr view "${{ inputs.pr_number }}" --json merged >/dev/null 2>&1; then
echo "::error::PR #${{ inputs.pr_number }} not found or inaccessible."
exit 1
fi
MERGED=$(gh pr view "${{ inputs.pr_number }}" --json merged --jq '.merged')
if [ "$MERGED" != "true" ]; then
echo "::error::PR #${{ inputs.pr_number }} is not merged. Only merged PRs can be backported."
exit 1
fi
# Validate PR has needs-backport label
if ! gh pr view "${{ inputs.pr_number }}" --json labels --jq '.labels[].name' | grep -q "needs-backport"; then
echo "::error::PR #${{ inputs.pr_number }} does not have 'needs-backport' label."
exit 1
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout repository
uses: actions/checkout@v4
with:
@@ -29,7 +73,7 @@ jobs:
id: check-existing
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
run: |
# Check for existing backport PRs for this PR number
EXISTING_BACKPORTS=$(gh pr list --state all --search "backport-${PR_NUMBER}-to" --json title,headRefName,baseRefName | jq -r '.[].headRefName')
@@ -39,6 +83,13 @@ jobs:
exit 0
fi
# For manual triggers with force_rerun, proceed anyway
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.force_rerun }}" = "true" ]; then
echo "skip=false" >> $GITHUB_OUTPUT
echo "::warning::Force rerun requested - existing backports will be updated"
exit 0
fi
echo "Found existing backport PRs:"
echo "$EXISTING_BACKPORTS"
echo "skip=true" >> $GITHUB_OUTPUT
@@ -50,8 +101,17 @@ jobs:
run: |
# Extract version labels (e.g., "1.24", "1.22")
VERSIONS=""
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
for label in $(echo "$LABELS" | jq -r '.[].name'); do
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
# For manual triggers, get labels from the PR
LABELS=$(gh pr view ${{ inputs.pr_number }} --json labels | jq -r '.labels[].name')
else
# For automatic triggers, extract from PR event
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
LABELS=$(echo "$LABELS" | jq -r '.[].name')
fi
for label in $LABELS; do
# Match version labels like "1.24" (major.minor only)
if [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then
# Validate the branch exists before adding to list
@@ -75,12 +135,20 @@ jobs:
if: steps.check-existing.outputs.skip != 'true'
id: backport
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
MERGE_COMMIT: ${{ github.event.pull_request.merge_commit_sha }}
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
run: |
FAILED=""
SUCCESS=""
# Get PR data for manual triggers
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,mergeCommit)
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
else
PR_TITLE="${{ github.event.pull_request.title }}"
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
fi
for version in ${{ steps.versions.outputs.versions }}; do
echo "::group::Backporting to core/${version}"
@@ -133,10 +201,18 @@ jobs:
if: steps.check-existing.outputs.skip != 'true' && steps.backport.outputs.success
env:
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
run: |
# Get PR data for manual triggers
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,author)
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
else
PR_TITLE="${{ github.event.pull_request.title }}"
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
fi
for backport in ${{ steps.backport.outputs.success }}; do
IFS=':' read -r version branch <<< "${backport}"
@@ -165,9 +241,16 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
PR_NUMBER="${{ github.event.pull_request.number }}"
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json author,mergeCommit)
PR_NUMBER="${{ inputs.pr_number }}"
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
else
PR_NUMBER="${{ github.event.pull_request.number }}"
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
fi
for failure in ${{ steps.backport.outputs.failed }}; do
IFS=':' read -r version reason conflicts <<< "${failure}"

View File

@@ -9,7 +9,7 @@ module.exports = defineConfig({
entry: 'src/locales/en',
entryLocale: 'en',
output: 'src/locales',
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar'],
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr'],
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
'latent' is the short form of 'latent space'.
'mask' is in the context of image processing.

View File

@@ -1,5 +1,5 @@
import path from 'path'
import { Plugin } from 'vite'
import type { Plugin } from 'vite'
interface ShimResult {
code: string

View File

@@ -1,7 +1,7 @@
import glob from 'fast-glob'
import fs from 'fs-extra'
import { dirname, join } from 'node:path'
import { HtmlTagDescriptor, Plugin, normalizePath } from 'vite'
import { type HtmlTagDescriptor, type Plugin, normalizePath } from 'vite'
interface ImportMapSource {
name: string

View File

@@ -22,7 +22,7 @@ const config: KnipConfig = {
],
ignore: [
// Auto generated manager types
'src/types/generatedManagerTypes.ts',
'src/workbench/extensions/manager/types/generatedManagerTypes.ts',
'src/types/comfyRegistryTypes.ts',
// Used by a custom node (that should move off of this)
'src/scripts/ui/components/splitButton.ts',

View File

@@ -59,14 +59,13 @@ import { useI18n } from 'vue-i18n'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
import { useManagerState } from '@/composables/useManagerState'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useDialogStore } from '@/stores/dialogStore'
import type { MissingNodeType } from '@/types/comfy'
import { ManagerTab } from '@/types/comfyManagerTypes'
import PackInstallButton from './manager/button/PackInstallButton.vue'
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
const props = defineProps<{
missingNodeTypes: MissingNodeType[]
@@ -138,7 +137,7 @@ const allMissingNodesInstalled = computed(() => {
})
// Watch for completion and close dialog
watch(allMissingNodesInstalled, async (allInstalled) => {
if (allInstalled) {
if (allInstalled && showInstallAllButton.value) {
// Use nextTick to ensure state updates are complete
await nextTick()

View File

@@ -1,82 +0,0 @@
import { mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import PrimeVue from 'primevue/config'
import Tag from 'primevue/tag'
import Tooltip from 'primevue/tooltip'
import { describe, expect, it } from 'vitest'
import { createI18n } from 'vue-i18n'
import enMessages from '@/locales/en/main.json' with { type: 'json' }
import ManagerHeader from './ManagerHeader.vue'
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: enMessages
}
})
describe('ManagerHeader', () => {
const createWrapper = () => {
return mount(ManagerHeader, {
global: {
plugins: [createPinia(), PrimeVue, i18n],
directives: {
tooltip: Tooltip
},
components: {
Tag
}
}
})
}
it('renders the component title', () => {
const wrapper = createWrapper()
expect(wrapper.find('h2').text()).toBe(
enMessages.manager.discoverCommunityContent
)
})
it('displays the legacy manager UI tag', () => {
const wrapper = createWrapper()
const tag = wrapper.find('[data-pc-name="tag"]')
expect(tag.exists()).toBe(true)
expect(tag.text()).toContain(enMessages.manager.legacyManagerUI)
})
it('applies info severity to the tag', () => {
const wrapper = createWrapper()
const tag = wrapper.find('[data-pc-name="tag"]')
expect(tag.classes()).toContain('p-tag-info')
})
it('displays info icon in the tag', () => {
const wrapper = createWrapper()
const icon = wrapper.find('.pi-info-circle')
expect(icon.exists()).toBe(true)
})
it('has cursor-help class on the tag', () => {
const wrapper = createWrapper()
const tag = wrapper.find('[data-pc-name="tag"]')
expect(tag.classes()).toContain('cursor-help')
})
it('has proper structure with flex container', () => {
const wrapper = createWrapper()
const flexContainer = wrapper.find('.flex.justify-end.ml-auto.pr-4')
expect(flexContainer.exists()).toBe(true)
const tag = flexContainer.find('[data-pc-name="tag"]')
expect(tag.exists()).toBe(true)
})
})

View File

@@ -1,25 +0,0 @@
<template>
<div class="w-full">
<div class="flex items-center">
<h2 class="text-lg font-normal text-left">
{{ $t('manager.discoverCommunityContent') }}
</h2>
<div class="flex justify-end ml-auto pr-4 pl-2">
<Tag
v-tooltip.left="$t('manager.legacyManagerUIDescription')"
severity="info"
icon="pi pi-info-circle"
:value="$t('manager.legacyManagerUI')"
class="cursor-help ml-2"
:pt="{
root: { class: 'text-xs' }
}"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import Tag from 'primevue/tag'
</script>

View File

@@ -117,6 +117,7 @@ import { useWorkflowAutoSave } from '@/platform/workflow/persistence/composables
import { useWorkflowPersistence } from '@/platform/workflow/persistence/composables/useWorkflowPersistence'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys'
import { attachSlotLinkPreviewRenderer } from '@/renderer/core/canvas/links/slotLinkPreviewRenderer'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
import MiniMap from '@/renderer/extensions/minimap/MiniMap.vue'
@@ -404,6 +405,7 @@ onMounted(async () => {
// @ts-expect-error fixme ts strict error
await comfyApp.setup(canvasRef.value)
attachSlotLinkPreviewRenderer(comfyApp.canvas)
canvasStore.canvas = comfyApp.canvas
canvasStore.canvas.render_canvas_border = false
workspaceStore.spinner = false

View File

@@ -68,7 +68,7 @@ const onIdle = () => {
ctor.title_mode !== LiteGraph.NO_TITLE &&
canvas.graph_mouse[1] < node.pos[1] // If we are over a node, but not within the node then we are on its title
) {
return showTooltip(nodeDef.description)
return showTooltip(nodeDef?.description)
}
if (node.flags?.collapsed) return
@@ -83,7 +83,7 @@ const onIdle = () => {
const inputName = node.inputs[inputSlot].name
const translatedTooltip = st(
`nodeDefs.${normalizeI18nKey(node.type ?? '')}.inputs.${normalizeI18nKey(inputName)}.tooltip`,
nodeDef.inputs[inputName]?.tooltip ?? ''
nodeDef?.inputs[inputName]?.tooltip ?? ''
)
return showTooltip(translatedTooltip)
}
@@ -97,7 +97,7 @@ const onIdle = () => {
if (outputSlot !== -1) {
const translatedTooltip = st(
`nodeDefs.${normalizeI18nKey(node.type ?? '')}.outputs.${outputSlot}.tooltip`,
nodeDef.outputs[outputSlot]?.tooltip ?? ''
nodeDef?.outputs[outputSlot]?.tooltip ?? ''
)
return showTooltip(translatedTooltip)
}
@@ -107,7 +107,7 @@ const onIdle = () => {
if (widget && !isDOMWidget(widget)) {
const translatedTooltip = st(
`nodeDefs.${normalizeI18nKey(node.type ?? '')}.inputs.${normalizeI18nKey(widget.name)}.tooltip`,
nodeDef.inputs[widget.name]?.tooltip ?? ''
nodeDef?.inputs[widget.name]?.tooltip ?? ''
)
// Widget tooltip can be set dynamically, current translation collection does not support this.
return showTooltip(widget.tooltip ?? translatedTooltip)

View File

@@ -142,14 +142,14 @@ import { useI18n } from 'vue-i18n'
import PuzzleIcon from '@/components/icons/PuzzleIcon.vue'
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
import { useManagerState } from '@/composables/useManagerState'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { ReleaseNote } from '@/platform/updates/common/releaseService'
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
import { useCommandStore } from '@/stores/commandStore'
import { ManagerTab } from '@/types/comfyManagerTypes'
import { electronAPI, isElectron } from '@/utils/envUtil'
import { formatVersionAnchor } from '@/utils/formatUtil'
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
// Types
interface MenuItem {

View File

@@ -82,7 +82,6 @@ import { useI18n } from 'vue-i18n'
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
import { useManagerState } from '@/composables/useManagerState'
import SettingDialogContent from '@/platform/settings/components/SettingDialogContent.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useColorPaletteService } from '@/services/colorPaletteService'
@@ -90,10 +89,11 @@ import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
import { useMenuItemStore } from '@/stores/menuItemStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { ManagerTab } from '@/types/comfyManagerTypes'
import { showNativeSystemMenu } from '@/utils/envUtil'
import { normalizeI18nKey } from '@/utils/formatUtil'
import { whileMouseDown } from '@/utils/mouseDownUtil'
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
const colorPaletteStore = useColorPaletteStore()
const colorPaletteService = useColorPaletteService()

View File

@@ -2,9 +2,9 @@ import { whenever } from '@vueuse/core'
import { computed, onUnmounted, ref } from 'vue'
import { useNodePacks } from '@/composables/nodePack/useNodePacks'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import type { UseNodePacksOptions } from '@/types/comfyManagerTypes'
import type { components } from '@/types/comfyRegistryTypes'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import type { UseNodePacksOptions } from '@/workbench/extensions/manager/types/comfyManagerTypes'
export const useInstalledPacks = (options: UseNodePacksOptions = {}) => {
const comfyManagerStore = useComfyManagerStore()

View File

@@ -5,10 +5,10 @@ import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
import type { NodeProperty } from '@/lib/litegraph/src/LGraphNode'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { app } from '@/scripts/app'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import type { components } from '@/types/comfyRegistryTypes'
import { collectAllNodes } from '@/utils/graphTraversalUtil'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
/**
* Composable to find missing NodePacks from workflow

View File

@@ -2,7 +2,7 @@ import { get, useAsyncState } from '@vueuse/core'
import type { Ref } from 'vue'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import type { UseNodePacksOptions } from '@/types/comfyManagerTypes'
import type { UseNodePacksOptions } from '@/workbench/extensions/manager/types/comfyManagerTypes'
/**
* Handles fetching node packs from the registry given a list of node pack IDs

View File

@@ -1,8 +1,8 @@
import { computed } from 'vue'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import type { components } from '@/types/comfyRegistryTypes'
import { compareVersions, isSemVer } from '@/utils/formatUtil'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
export const usePackUpdateStatus = (
nodePack: components['schemas']['Node']

View File

@@ -1,7 +1,7 @@
import { type Ref, computed } from 'vue'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import type { components } from '@/types/comfyRegistryTypes'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
type NodePack = components['schemas']['Node']

View File

@@ -1,9 +1,9 @@
import { computed, onMounted } from 'vue'
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import type { components } from '@/types/comfyRegistryTypes'
import { compareVersions, isSemVer } from '@/utils/formatUtil'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
/**
* Composable to find NodePacks that have updates available

View File

@@ -7,9 +7,9 @@ import { app } from '@/scripts/app'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import type { UseNodePacksOptions } from '@/types/comfyManagerTypes'
import type { components } from '@/types/comfyRegistryTypes'
import { collectAllNodes } from '@/utils/graphTraversalUtil'
import type { UseNodePacksOptions } from '@/workbench/extensions/manager/types/comfyManagerTypes'
type WorkflowPack = {
id:

View File

@@ -5,9 +5,7 @@ import { computed, getCurrentInstance, onUnmounted, readonly, ref } from 'vue'
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
import config from '@/config'
import { useComfyManagerService } from '@/services/comfyManagerService'
import { useComfyRegistryService } from '@/services/comfyRegistryService'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import type { SystemStats } from '@/types'
@@ -28,6 +26,8 @@ import {
satisfiesVersion,
utilCheckVersionCompatibility
} from '@/utils/versionUtil'
import { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
/**
* Composable for conflict detection system.
@@ -641,7 +641,9 @@ export function useConflictDetection() {
async function initializeConflictDetection(): Promise<void> {
try {
// Check if manager is new Manager before proceeding
const { useManagerState } = await import('@/composables/useManagerState')
const { useManagerState } = await import(
'@/workbench/extensions/manager/composables/useManagerState'
)
const managerState = useManagerState()
if (!managerState.isNewManagerUI.value) {

View File

@@ -1,6 +1,5 @@
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
import { ManagerUIState, useManagerState } from '@/composables/useManagerState'
import { useModelSelectorDialog } from '@/composables/useModelSelectorDialog'
import {
DEFAULT_DARK_COLOR_PALETTE,
@@ -41,12 +40,16 @@ import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { ManagerTab } from '@/types/comfyManagerTypes'
import {
getAllNonIoNodesInSubgraph,
getExecutionIdsForSelectedNodes
} from '@/utils/graphTraversalUtil'
import { filterOutputNodes } from '@/utils/nodeFilterUtil'
import {
ManagerUIState,
useManagerState
} from '@/workbench/extensions/manager/composables/useManagerState'
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
const moveSelectedNodesVersionAdded = '1.22.2'

View File

@@ -2,9 +2,9 @@ import { type ComputedRef, computed, unref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useDialogService } from '@/services/dialogService'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
/**
* Extracting import failed conflicts from conflict list

View File

@@ -5,9 +5,9 @@ import { computed, ref, watch } from 'vue'
import { DEFAULT_PAGE_SIZE } from '@/constants/searchConstants'
import { useRegistrySearchGateway } from '@/services/gateway/registrySearchGateway'
import type { SearchAttribute } from '@/types/algoliaTypes'
import { SortableAlgoliaField } from '@/types/comfyManagerTypes'
import type { components } from '@/types/comfyRegistryTypes'
import type { QuerySuggestion, SearchMode } from '@/types/searchServiceTypes'
import { SortableAlgoliaField } from '@/workbench/extensions/manager/types/comfyManagerTypes'
type RegistryNodePack = components['schemas']['Node']

View File

@@ -3,7 +3,7 @@ import { onUnmounted, ref } from 'vue'
import type { LogsWsMessage } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import type { components } from '@/types/generatedManagerTypes'
import type { components } from '@/workbench/extensions/manager/types/generatedManagerTypes'
const LOGS_MESSAGE_TYPE = 'logs'
const MANAGER_WS_TASK_DONE_NAME = 'cm-task-completed'

View File

@@ -11,16 +11,16 @@ import {
} from '@/lib/litegraph/src/litegraph'
import { useToastStore } from '@/platform/updates/common/toastStore'
import {
ComfyLink,
ComfyNode,
ComfyWorkflowJSON
type ComfyLink,
type ComfyNode,
type ComfyWorkflowJSON
} from '@/platform/workflow/validation/schemas/workflowSchema'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import { useDialogService } from '@/services/dialogService'
import { useExecutionStore } from '@/stores/executionStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useWidgetStore } from '@/stores/widgetStore'
import { ComfyExtension } from '@/types/comfy'
import { type ComfyExtension } from '@/types/comfy'
import { ExecutableGroupNodeChildDTO } from '@/utils/executableGroupNodeChildDTO'
import { GROUP } from '@/utils/executableGroupNodeDto'
import { deserialiseAndCreate, serialise } from '@/utils/vintageClipboard'

View File

@@ -9,7 +9,7 @@ import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
import { t } from '@/i18n'
import type { IStringWidget } from '@/lib/litegraph/src/types/widgets'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { api } from '@/scripts/api'
import { ComfyApp, app } from '@/scripts/app'
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'

View File

@@ -1,9 +1,9 @@
import * as THREE from 'three'
import {
AnimationItem,
AnimationManagerInterface,
EventManagerInterface
type AnimationItem,
type AnimationManagerInterface,
type EventManagerInterface
} from '@/extensions/core/load3d/interfaces'
export class AnimationManager implements AnimationManagerInterface {

View File

@@ -2,11 +2,11 @@ import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import {
CameraManagerInterface,
CameraState,
CameraType,
EventManagerInterface,
NodeStorageInterface
type CameraManagerInterface,
type CameraState,
type CameraType,
type EventManagerInterface,
type NodeStorageInterface
} from './interfaces'
export class CameraManager implements CameraManagerInterface {

View File

@@ -2,9 +2,9 @@ import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import {
ControlsManagerInterface,
EventManagerInterface,
NodeStorageInterface
type ControlsManagerInterface,
type EventManagerInterface,
type NodeStorageInterface
} from './interfaces'
export class ControlsManager implements ControlsManagerInterface {

View File

@@ -1,4 +1,4 @@
import { EventCallback, EventManagerInterface } from './interfaces'
import { type EventCallback, type EventManagerInterface } from './interfaces'
export class EventManager implements EventManagerInterface {
private listeners: { [key: string]: EventCallback[] } = {}

View File

@@ -1,6 +1,9 @@
import * as THREE from 'three'
import { EventManagerInterface, LightingManagerInterface } from './interfaces'
import {
type EventManagerInterface,
type LightingManagerInterface
} from './interfaces'
export class LightingManager implements LightingManagerInterface {
lights: THREE.Light[] = []

View File

@@ -1,7 +1,7 @@
import * as THREE from 'three'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { CameraManager } from './CameraManager'
import { ControlsManager } from './ControlsManager'
@@ -16,11 +16,11 @@ import { SceneManager } from './SceneManager'
import { SceneModelManager } from './SceneModelManager'
import { ViewHelperManager } from './ViewHelperManager'
import {
CameraState,
CaptureResult,
Load3DOptions,
MaterialMode,
UpDirection
type CameraState,
type CaptureResult,
type Load3DOptions,
type MaterialMode,
type UpDirection
} from './interfaces'
class Load3d {

View File

@@ -4,7 +4,7 @@ import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { AnimationManager } from './AnimationManager'
import Load3d from './Load3d'
import { Load3DOptions } from './interfaces'
import { type Load3DOptions } from './interfaces'
class Load3dAnimation extends Load3d {
private animationManager: AnimationManager

View File

@@ -9,9 +9,9 @@ import { t } from '@/i18n'
import { useToastStore } from '@/platform/updates/common/toastStore'
import {
EventManagerInterface,
LoaderManagerInterface,
ModelManagerInterface
type EventManagerInterface,
type LoaderManagerInterface,
type ModelManagerInterface
} from './interfaces'
export class LoaderManager implements LoaderManagerInterface {

View File

@@ -1,6 +1,6 @@
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { NodeStorageInterface } from './interfaces'
import { type NodeStorageInterface } from './interfaces'
export class NodeStorage implements NodeStorageInterface {
private node: LGraphNode

View File

@@ -1,7 +1,10 @@
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { EventManagerInterface, PreviewManagerInterface } from './interfaces'
import {
type EventManagerInterface,
type PreviewManagerInterface
} from './interfaces'
export class PreviewManager implements PreviewManagerInterface {
previewCamera: THREE.Camera

View File

@@ -1,6 +1,6 @@
import * as THREE from 'three'
import { EventManagerInterface } from './interfaces'
import { type EventManagerInterface } from './interfaces'
export class RecordingManager {
private mediaRecorder: MediaRecorder | null = null

View File

@@ -2,7 +2,10 @@ import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import Load3dUtils from './Load3dUtils'
import { EventManagerInterface, SceneManagerInterface } from './interfaces'
import {
type EventManagerInterface,
type SceneManagerInterface
} from './interfaces'
export class SceneManager implements SceneManagerInterface {
scene: THREE.Scene

View File

@@ -2,7 +2,7 @@ import * as THREE from 'three'
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial'
import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2'
import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry'
import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader'
import { type GLTF } from 'three/examples/jsm/loaders/GLTFLoader'
import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils'
import { ColoredShadowMaterial } from './conditional-lines/ColoredShadowMaterial'
@@ -11,11 +11,11 @@ import { ConditionalEdgesShader } from './conditional-lines/ConditionalEdgesShad
import { ConditionalLineMaterial } from './conditional-lines/Lines2/ConditionalLineMaterial'
import { ConditionalLineSegmentsGeometry } from './conditional-lines/Lines2/ConditionalLineSegmentsGeometry'
import {
EventManagerInterface,
Load3DOptions,
MaterialMode,
ModelManagerInterface,
UpDirection
type EventManagerInterface,
type Load3DOptions,
type MaterialMode,
type ModelManagerInterface,
type UpDirection
} from './interfaces'
export class SceneModelManager implements ModelManagerInterface {

View File

@@ -2,7 +2,10 @@ import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper'
import { NodeStorageInterface, ViewHelperManagerInterface } from './interfaces'
import {
type NodeStorageInterface,
type ViewHelperManagerInterface
} from './interfaces'
export class ViewHelperManager implements ViewHelperManagerInterface {
viewHelper: ViewHelper = {} as ViewHelper

View File

@@ -2,13 +2,13 @@ import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper'
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { type GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
export type Load3DNodeType = 'Load3D' | 'Preview3D'

View File

@@ -4,7 +4,7 @@ https://github.com/rgthree/rgthree-comfy/blob/main/py/display_any.py
upstream requested in https://github.com/Kosinkadink/rfcs/blob/main/rfcs/0000-corenodes.md#preview-nodes
*/
import { app } from '@/scripts/app'
import { DOMWidget } from '@/scripts/domWidget'
import { type DOMWidget } from '@/scripts/domWidget'
import { ComfyWidgets } from '@/scripts/widgets'
import { useExtensionService } from '@/services/extensionService'

View File

@@ -2,7 +2,7 @@ import { nextTick } from 'vue'
import Load3D from '@/components/load3d/Load3D.vue'
import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
import { useExtensionService } from '@/services/extensionService'
import { useLoad3dService } from '@/services/load3dService'

View File

@@ -15,7 +15,7 @@ import type { ResultItemType } from '@/schemas/apiSchema'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import type { DOMWidget } from '@/scripts/domWidget'
import { useAudioService } from '@/services/audioService'
import { NodeLocatorId } from '@/types'
import { type NodeLocatorId } from '@/types'
import { getNodeByLocatorId } from '@/utils/graphTraversalUtil'
import { api } from '../../scripts/api'

View File

@@ -1,6 +1,6 @@
import {
ComfyNodeDef,
InputSpec,
type ComfyNodeDef,
type InputSpec,
isComboInputSpecV1
} from '@/schemas/nodeDefSchema'

View File

@@ -28,6 +28,10 @@ import ruCommands from './locales/ru/commands.json' with { type: 'json' }
import ru from './locales/ru/main.json' with { type: 'json' }
import ruNodes from './locales/ru/nodeDefs.json' with { type: 'json' }
import ruSettings from './locales/ru/settings.json' with { type: 'json' }
import trCommands from './locales/tr/commands.json' with { type: 'json' }
import tr from './locales/tr/main.json' with { type: 'json' }
import trNodes from './locales/tr/nodeDefs.json' with { type: 'json' }
import trSettings from './locales/tr/settings.json' with { type: 'json' }
import zhTWCommands from './locales/zh-TW/commands.json' with { type: 'json' }
import zhTW from './locales/zh-TW/main.json' with { type: 'json' }
import zhTWNodes from './locales/zh-TW/nodeDefs.json' with { type: 'json' }
@@ -55,7 +59,8 @@ const messages = {
ko: buildLocale(ko, koNodes, koCommands, koSettings),
fr: buildLocale(fr, frNodes, frCommands, frSettings),
es: buildLocale(es, esNodes, esCommands, esSettings),
ar: buildLocale(ar, arNodes, arCommands, arSettings)
ar: buildLocale(ar, arNodes, arCommands, arSettings),
tr: buildLocale(tr, trNodes, trCommands, trSettings)
}
export const i18n = createI18n({

View File

@@ -87,6 +87,7 @@ import type { PickNevers } from './types/utility'
import type { IBaseWidget } from './types/widgets'
import { alignNodes, distributeNodes, getBoundaryNodes } from './utils/arrange'
import { findFirstNode, getAllNestedItems } from './utils/collections'
import { resolveConnectingLinkColor } from './utils/linkColors'
import type { UUID } from './utils/uuid'
import { BaseWidget } from './widgets/BaseWidget'
import { toConcreteWidget } from './widgets/widgetMap'
@@ -4716,29 +4717,20 @@ export class LGraphCanvas
const connShape = fromSlot.shape
const connType = fromSlot.type
const colour =
connType === LiteGraph.EVENT
? LiteGraph.EVENT_LINK_COLOR
: LiteGraph.CONNECTING_LINK_COLOR
const colour = resolveConnectingLinkColor(connType)
// the connection being dragged by the mouse
if (this.linkRenderer) {
this.linkRenderer.renderLinkDirect(
this.linkRenderer.renderDraggingLink(
ctx,
pos,
highlightPos,
null,
false,
null,
colour,
fromDirection,
dragDirection,
{
...this.buildLinkRenderContext(),
linkMarkerShape: LinkMarkerShape.None
},
{
disabled: false
}
)
}

View File

@@ -0,0 +1,13 @@
import type { CanvasColour, ISlotType } from '../interfaces'
import { LiteGraph } from '../litegraph'
/**
* Resolve the colour used while rendering or previewing a connection of a given slot type.
*/
export function resolveConnectingLinkColor(
type: ISlotType | undefined
): CanvasColour {
return type === LiteGraph.EVENT
? LiteGraph.EVENT_LINK_COLOR
: LiteGraph.CONNECTING_LINK_COLOR
}

View File

@@ -24,7 +24,7 @@ Add your language code to the `outputLocales` array:
```javascript
module.exports = defineConfig({
// ... existing config
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es'], // Add your language here
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'tr'], // Add your language here
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
'latent' is the short form of 'latent space'.
'mask' is in the context of image processing.

View File

@@ -11,6 +11,7 @@ Our project supports multiple languages using `vue-i18n`. This allows users arou
- ko (한국어)
- fr (Français)
- es (Español)
- tr (Türkçe)
## How to Add a New Language

View File

@@ -0,0 +1,312 @@
{
"Comfy-Desktop_CheckForUpdates": {
"label": "Güncellemeleri Kontrol Et"
},
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
"label": "Özel Düğümler Klasörünü Aç"
},
"Comfy-Desktop_Folders_OpenInputsFolder": {
"label": "Girişler Klasörünü Aç"
},
"Comfy-Desktop_Folders_OpenLogsFolder": {
"label": "Kayıtlar Klasörünü Aç"
},
"Comfy-Desktop_Folders_OpenModelConfig": {
"label": "extra_model_paths.yaml dosyasını aç"
},
"Comfy-Desktop_Folders_OpenModelsFolder": {
"label": "Modeller Klasörünü Aç"
},
"Comfy-Desktop_Folders_OpenOutputsFolder": {
"label": ıktılar Klasörünü Aç"
},
"Comfy-Desktop_OpenDevTools": {
"label": "Geliştirici Araçlarını Aç"
},
"Comfy-Desktop_OpenUserGuide": {
"label": "Masaüstü Kullanıcı Kılavuzu"
},
"Comfy-Desktop_Quit": {
"label": ık"
},
"Comfy-Desktop_Reinstall": {
"label": "Yeniden Yükle"
},
"Comfy-Desktop_Restart": {
"label": "Yeniden Başlat"
},
"Comfy_3DViewer_Open3DViewer": {
"label": "Seçili Düğüm için 3D Görüntüleyiciyi (Beta) Aç"
},
"Comfy_BrowseTemplates": {
"label": "Şablonlara Gözat"
},
"Comfy_Canvas_DeleteSelectedItems": {
"label": "Seçili Öğeleri Sil"
},
"Comfy_Canvas_FitView": {
"label": "Görünümü seçili düğümlere sığdır"
},
"Comfy_Canvas_Lock": {
"label": "Tuvali Kilitle"
},
"Comfy_Canvas_MoveSelectedNodes_Down": {
"label": "Seçili Düğümleri Aşağı Taşı"
},
"Comfy_Canvas_MoveSelectedNodes_Left": {
"label": "Seçili Düğümleri Sola Taşı"
},
"Comfy_Canvas_MoveSelectedNodes_Right": {
"label": "Seçili Düğümleri Sağa Taşı"
},
"Comfy_Canvas_MoveSelectedNodes_Up": {
"label": "Seçili Düğümleri Yukarı Taşı"
},
"Comfy_Canvas_ResetView": {
"label": "Görünümü Sıfırla"
},
"Comfy_Canvas_Resize": {
"label": "Seçili Düğümleri Yeniden Boyutlandır"
},
"Comfy_Canvas_ToggleLinkVisibility": {
"label": "Tuval Bağlantı Görünürlüğünü Aç/Kapat"
},
"Comfy_Canvas_ToggleLock": {
"label": "Tuval Kilidini Aç/Kapat"
},
"Comfy_Canvas_ToggleMinimap": {
"label": "Mini Haritayı Aç/Kapat"
},
"Comfy_Canvas_ToggleSelected_Pin": {
"label": "Seçili Öğeleri Sabitle/Sabitlemeyi Kaldır"
},
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
"label": "Seçili Düğümleri Atla/Geri Al"
},
"Comfy_Canvas_ToggleSelectedNodes_Collapse": {
"label": "Seçili Düğümleri Daralt/Genişlet"
},
"Comfy_Canvas_ToggleSelectedNodes_Mute": {
"label": "Seçili Düğümleri Sessize Al/Sesi Aç"
},
"Comfy_Canvas_ToggleSelectedNodes_Pin": {
"label": "Seçili Düğümleri Sabitle/Sabitlemeyi Kaldır"
},
"Comfy_Canvas_Unlock": {
"label": "Tuvalin Kilidini Aç"
},
"Comfy_Canvas_ZoomIn": {
"label": "Yakınlaştır"
},
"Comfy_Canvas_ZoomOut": {
"label": "Uzaklaştır"
},
"Comfy_ClearPendingTasks": {
"label": "Bekleyen Görevleri Temizle"
},
"Comfy_ClearWorkflow": {
"label": "İş Akışını Temizle"
},
"Comfy_ContactSupport": {
"label": "Destekle İletişime Geç"
},
"Comfy_Dev_ShowModelSelector": {
"label": "Model Seçiciyi Göster (Geliştirici)"
},
"Comfy_DuplicateWorkflow": {
"label": "Mevcut İş Akışını Çoğalt"
},
"Comfy_ExportWorkflow": {
"label": "İş Akışını Dışa Aktar"
},
"Comfy_ExportWorkflowAPI": {
"label": "İş Akışını Dışa Aktar (API Formatı)"
},
"Comfy_Feedback": {
"label": "Geri Bildirim Ver"
},
"Comfy_Graph_ConvertToSubgraph": {
"label": "Seçimi Alt Grafiğe Dönüştür"
},
"Comfy_Graph_ExitSubgraph": {
"label": "Alt Grafikten Çık"
},
"Comfy_Graph_FitGroupToContents": {
"label": "Grubu İçeriğe Sığdır"
},
"Comfy_Graph_GroupSelectedNodes": {
"label": "Seçili Düğümleri Gruplandır"
},
"Comfy_Graph_UnpackSubgraph": {
"label": "Seçili Alt Grafiği Aç"
},
"Comfy_GroupNode_ConvertSelectedNodesToGroupNode": {
"label": "Seçili düğümleri grup düğümüne dönüştür"
},
"Comfy_GroupNode_ManageGroupNodes": {
"label": "Grup düğümlerini yönet"
},
"Comfy_GroupNode_UngroupSelectedGroupNodes": {
"label": "Seçili grup düğümlerinin grubunu çöz"
},
"Comfy_Help_AboutComfyUI": {
"label": "ComfyUI Hakkında'yı Aç"
},
"Comfy_Help_OpenComfyOrgDiscord": {
"label": "Comfy-Org Discord'unu Aç"
},
"Comfy_Help_OpenComfyUIDocs": {
"label": "ComfyUI Belgelerini Aç"
},
"Comfy_Help_OpenComfyUIForum": {
"label": "ComfyUI Forumunu Aç"
},
"Comfy_Help_OpenComfyUIIssues": {
"label": "ComfyUI Sorunlarını Aç"
},
"Comfy_Interrupt": {
"label": "Kes"
},
"Comfy_LoadDefaultWorkflow": {
"label": "Varsayılan İş Akışını Yükle"
},
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
"label": "Özel Düğüm Yöneticisi"
},
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
"label": "Özel Düğümler (Eski)"
},
"Comfy_Manager_ShowLegacyManagerMenu": {
"label": "Yönetici Menüsü (Eski)"
},
"Comfy_Manager_ShowMissingPacks": {
"label": "Eksik Özel Düğümleri Yükle"
},
"Comfy_Manager_ShowUpdateAvailablePacks": {
"label": "Özel Düğüm Güncellemelerini Kontrol Et"
},
"Comfy_Manager_ToggleManagerProgressDialog": {
"label": "Özel Düğüm Yöneticisi İlerleme Çubuğunu Aç/Kapat"
},
"Comfy_MaskEditor_BrushSize_Decrease": {
"label": "Maske Düzenleyicide Fırça Boyutunu Azalt"
},
"Comfy_MaskEditor_BrushSize_Increase": {
"label": "Maske Düzenleyicide Fırça Boyutunu Artır"
},
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "Seçili Düğüm için Maske Düzenleyiciyi Aç"
},
"Comfy_Memory_UnloadModels": {
"label": "Modelleri Boşalt"
},
"Comfy_Memory_UnloadModelsAndExecutionCache": {
"label": "Modelleri ve Yürütme Önbelleğini Boşalt"
},
"Comfy_NewBlankWorkflow": {
"label": "Yeni Boş İş Akışı"
},
"Comfy_OpenClipspace": {
"label": "Clipspace"
},
"Comfy_OpenManagerDialog": {
"label": "Yönetici"
},
"Comfy_OpenWorkflow": {
"label": "İş Akışını Aç"
},
"Comfy_PublishSubgraph": {
"label": "Alt Grafiği Yayınla"
},
"Comfy_QueuePrompt": {
"label": "İstemi Kuyruğa Al"
},
"Comfy_QueuePromptFront": {
"label": "İstemi Kuyruğa Al (Ön)"
},
"Comfy_QueueSelectedOutputNodes": {
"label": "Seçili Çıktı Düğümlerini Kuyruğa Al"
},
"Comfy_Redo": {
"label": "Yinele"
},
"Comfy_RefreshNodeDefinitions": {
"label": "Düğüm Tanımlarını Yenile"
},
"Comfy_SaveWorkflow": {
"label": "İş Akışını Kaydet"
},
"Comfy_SaveWorkflowAs": {
"label": "İş Akışını Farklı Kaydet"
},
"Comfy_ShowSettingsDialog": {
"label": "Ayarlar İletişim Kutusunu Göster"
},
"Comfy_ToggleCanvasInfo": {
"label": "Tuval Performansı"
},
"Comfy_ToggleHelpCenter": {
"label": "Yardım Merkezi"
},
"Comfy_ToggleTheme": {
"label": "Temayı Değiştir (Karanlık/Açık)"
},
"Comfy_Undo": {
"label": "Geri Al"
},
"Comfy_User_OpenSignInDialog": {
"label": "Giriş Yapma İletişim Kutusunu Aç"
},
"Comfy_User_SignOut": {
"label": ıkış Yap"
},
"Workspace_CloseWorkflow": {
"label": "Mevcut İş Akışını Kapat"
},
"Workspace_NextOpenedWorkflow": {
"label": "Sonraki Açılan İş Akışı"
},
"Workspace_PreviousOpenedWorkflow": {
"label": "Önceki Açılan İş Akışı"
},
"Workspace_SearchBox_Toggle": {
"label": "Arama Kutusunu Aç/Kapat"
},
"Workspace_ToggleBottomPanel": {
"label": "Alt Paneli Aç/Kapat"
},
"Workspace_ToggleBottomPanel_Shortcuts": {
"label": "Tuş Atamaları İletişim Kutusunu Göster"
},
"Workspace_ToggleBottomPanelTab_command-terminal": {
"label": "Terminal Alt Panelini Aç/Kapat"
},
"Workspace_ToggleBottomPanelTab_logs-terminal": {
"label": "Kayıtlar Alt Panelini Aç/Kapat"
},
"Workspace_ToggleBottomPanelTab_shortcuts-essentials": {
"label": "Temel Alt Paneli Aç/Kapat"
},
"Workspace_ToggleBottomPanelTab_shortcuts-view-controls": {
"label": "Görünüm Kontrolleri Alt Panelini Aç/Kapat"
},
"Workspace_ToggleFocusMode": {
"label": "Odak Modunu Aç/Kapat"
},
"Workspace_ToggleSidebarTab_model-library": {
"label": "Model Kütüphanesi Kenar Çubuğunu Aç/Kapat",
"tooltip": "Model Kütüphanesi"
},
"Workspace_ToggleSidebarTab_node-library": {
"label": "Düğüm Kütüphanesi Kenar Çubuğunu Aç/Kapat",
"tooltip": "Düğüm Kütüphanesi"
},
"Workspace_ToggleSidebarTab_queue": {
"label": "Kuyruk Kenar Çubuğunu Aç/Kapat",
"tooltip": "Kuyruk"
},
"Workspace_ToggleSidebarTab_workflows": {
"label": "İş Akışları Kenar Çubuğunu Aç/Kapat",
"tooltip": "İş Akışları"
}
}

1800
src/locales/tr/main.json Normal file

File diff suppressed because it is too large Load Diff

8653
src/locales/tr/nodeDefs.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,424 @@
{
"Comfy-Desktop_AutoUpdate": {
"name": "Güncellemeleri otomatik olarak kontrol et"
},
"Comfy-Desktop_SendStatistics": {
"name": "Anonim kullanım metrikleri gönder"
},
"Comfy-Desktop_UV_PypiInstallMirror": {
"name": "Pypi Yükleme Yansısı",
"tooltip": "Varsayılan pip yükleme yansısı"
},
"Comfy-Desktop_UV_PythonInstallMirror": {
"name": "Python Yükleme Yansısı",
"tooltip": "Yönetilen Python kurulumları Astral python-build-standalone projesinden indirilir. Bu değişken, Python kurulumları için farklı bir kaynak kullanmak üzere bir yansıma URL'sine ayarlanabilir. Sağlanan URL, örneğin https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz'deki https://github.com/astral-sh/python-build-standalone/releases/download'ın yerini alacaktır. Dağıtımlar, file:// URL şeması kullanılarak yerel bir dizinden okunabilir."
},
"Comfy-Desktop_UV_TorchInstallMirror": {
"name": "Torch Yükleme Yansısı",
"tooltip": "Pytorch için Pip yükleme yansısı"
},
"Comfy-Desktop_WindowStyle": {
"name": "Pencere Stili",
"tooltip": "Özel: Sistem başlık çubuğunu ComfyUI'nin Üst menüsüyle değiştirin",
"options": {
"default": "varsayılan",
"custom": "özel"
}
},
"Comfy_Canvas_BackgroundImage": {
"name": "Tuval arka plan resmi",
"tooltip": "Tuval arka planı için resim URL'si. Çıktılar panelindeki bir resme sağ tıklayıp \"Arka Plan Olarak Ayarla\"yı seçerek kullanabilir veya yükleme düğmesini kullanarak kendi resminizi yükleyebilirsiniz."
},
"Comfy_Canvas_NavigationMode": {
"name": "Tuval Gezinme Modu",
"options": {
"Standard (New)": "Standart (Yeni)",
"Drag Navigation": "Sürükleyerek Gezinme"
}
},
"Comfy_Canvas_SelectionToolbox": {
"name": "Seçim araç kutusunu göster"
},
"Comfy_ConfirmClear": {
"name": "İş akışını temizlerken onay iste"
},
"Comfy_DevMode": {
"name": "Geliştirici modu seçeneklerini etkinleştir (API kaydetme, vb.)"
},
"Comfy_DisableFloatRounding": {
"name": "Varsayılan ondalık sayı widget yuvarlamasını devre dışı bırak.",
"tooltip": "(sayfanın yeniden yüklenmesini gerektirir) Arka uçtaki düğüm tarafından yuvarlama ayarlandığında yuvarlama devre dışı bırakılamaz."
},
"Comfy_DisableSliders": {
"name": "Düğüm widget kaydırıcılarını devre dışı bırak"
},
"Comfy_DOMClippingEnabled": {
"name": "DOM öğesi kırpmayı etkinleştir (etkinleştirmek performansı düşürebilir)"
},
"Comfy_EditAttention_Delta": {
"name": "Ctrl+yukarı/aşağı hassasiyeti"
},
"Comfy_EnableTooltips": {
"name": "Araç İpuçlarını Etkinleştir"
},
"Comfy_EnableWorkflowViewRestore": {
"name": "İş akışlarında tuval konumunu ve yakınlaştırma seviyesini kaydet ve geri yükle"
},
"Comfy_FloatRoundingPrecision": {
"name": "Ondalık sayı widget yuvarlama ondalık basamakları [0 = otomatik].",
"tooltip": "(sayfanın yeniden yüklenmesini gerektirir)"
},
"Comfy_Graph_CanvasInfo": {
"name": "Sol alt köşede tuval bilgilerini göster (fps, vb.)"
},
"Comfy_Graph_CanvasMenu": {
"name": "Grafik tuval menüsünü göster"
},
"Comfy_Graph_CtrlShiftZoom": {
"name": "Hızlı yakınlaştırma kısayolunu etkinleştir (Ctrl + Shift + Sürükle)"
},
"Comfy_Graph_LinkMarkers": {
"name": "Bağlantı orta nokta işaretçileri",
"options": {
"None": "Yok",
"Circle": "Daire",
"Arrow": "Ok"
}
},
"Comfy_Graph_ZoomSpeed": {
"name": "Tuval yakınlaştırma hızı"
},
"Comfy_Group_DoubleClickTitleToEdit": {
"name": "Düzenlemek için grup başlığına çift tıkla"
},
"Comfy_GroupSelectedNodes_Padding": {
"name": "Seçili düğümleri gruplandırma dolgusu"
},
"Comfy_LinkRelease_Action": {
"name": "Bağlantı bırakıldığında eylem (Değiştirici yok)",
"options": {
"context menu": "bağlam menüsü",
"search box": "arama kutusu",
"no action": "eylem yok"
}
},
"Comfy_LinkRelease_ActionShift": {
"name": "Bağlantı bırakıldığında eylem (Shift)",
"options": {
"context menu": "bağlam menüsü",
"search box": "arama kutusu",
"no action": "eylem yok"
}
},
"Comfy_LinkRenderMode": {
"name": "Bağlantı Oluşturma Modu",
"options": {
"Straight": "Düz",
"Linear": "Doğrusal",
"Spline": "Eğri",
"Hidden": "Gizli"
}
},
"Comfy_Load3D_3DViewerEnable": {
"name": "3D Görüntüleyiciyi Etkinleştir (Beta)",
"tooltip": "Seçili düğümler için 3D Görüntüleyiciyi (Beta) etkinleştirir. Bu özellik, 3D modelleri doğrudan tam boyutlu 3D görüntüleyici içinde görselleştirmenize ve etkileşimde bulunmanıza olanak tanır."
},
"Comfy_Load3D_BackgroundColor": {
"name": "Başlangıç Arka Plan Rengi",
"tooltip": "3D sahnenin varsayılan arka plan rengini kontrol eder. Bu ayar, yeni bir 3D widget oluşturulduğunda arka plan görünümünü belirler, ancak oluşturulduktan sonra her widget için ayrı ayrı ayarlanabilir."
},
"Comfy_Load3D_CameraType": {
"name": "Başlangıç Kamera Tipi",
"tooltip": "Yeni bir 3D widget oluşturulduğunda kameranın varsayılan olarak perspektif mi yoksa ortografik mi olacağını kontrol eder. Bu varsayılan, oluşturulduktan sonra her widget için ayrı ayrı değiştirilebilir.",
"options": {
"perspective": "perspektif",
"orthographic": "ortografik"
}
},
"Comfy_Load3D_LightAdjustmentIncrement": {
"name": "Işık Ayarlama Artışı",
"tooltip": "3D sahnelerde ışık yoğunluğunu ayarlarken artış boyutunu kontrol eder. Daha küçük bir adım değeri, aydınlatma ayarlamaları üzerinde daha ince kontrol sağlarken, daha büyük bir değer ayarlama başına daha belirgin değişikliklere neden olur."
},
"Comfy_Load3D_LightIntensity": {
"name": "Başlangıç Işık Yoğunluğu",
"tooltip": "3D sahnedeki aydınlatmanın varsayılan parlaklık seviyesini ayarlar. Bu değer, yeni bir 3D widget oluşturulduğunda ışıkların nesneleri ne kadar yoğun aydınlatacağını belirler, ancak oluşturulduktan sonra her widget için ayrı ayrı ayarlanabilir."
},
"Comfy_Load3D_LightIntensityMaximum": {
"name": "Maksimum Işık Yoğunluğu",
"tooltip": "3D sahneler için izin verilen maksimum ışık yoğunluğu değerini ayarlar. Bu, herhangi bir 3D widget'ta aydınlatma ayarlanırken ayarlanabilecek üst parlaklık sınırını tanımlar."
},
"Comfy_Load3D_LightIntensityMinimum": {
"name": "Minimum Işık Yoğunluğu",
"tooltip": "3D sahneler için izin verilen minimum ışık yoğunluğu değerini ayarlar. Bu, herhangi bir 3D widget'ta aydınlatma ayarlanırken ayarlanabilecek alt parlaklık sınırını tanımlar."
},
"Comfy_Load3D_ShowGrid": {
"name": "Başlangıç Izgara Görünürlüğü",
"tooltip": "Yeni bir 3D widget oluşturulduğunda ızgaranın varsayılan olarak görünür olup olmadığını kontrol eder. Bu varsayılan, oluşturulduktan sonra her widget için ayrı ayrı değiştirilebilir."
},
"Comfy_Load3D_ShowPreview": {
"name": "Başlangıç Önizleme Görünürlüğü",
"tooltip": "Yeni bir 3D widget oluşturulduğunda önizleme ekranının varsayılan olarak görünür olup olmadığını kontrol eder. Bu varsayılan, oluşturulduktan sonra her widget için ayrı ayrı değiştirilebilir."
},
"Comfy_Locale": {
"name": "Dil"
},
"Comfy_MaskEditor_BrushAdjustmentSpeed": {
"name": "Fırça ayar hızı çarpanı",
"tooltip": "Ayarlama sırasında fırça boyutunun ve sertliğinin ne kadar hızlı değiştiğini kontrol eder. Daha yüksek değerler daha hızlı değişiklikler anlamına gelir."
},
"Comfy_MaskEditor_UseDominantAxis": {
"name": "Fırça ayarını baskın eksene kilitle",
"tooltip": "Etkinleştirildiğinde, fırça ayarları yalnızca daha fazla hareket ettiğiniz yöne bağlı olarak boyutu VEYA sertliği etkileyecektir"
},
"Comfy_MaskEditor_UseNewEditor": {
"name": "Yeni maske düzenleyiciyi kullan",
"tooltip": "Yeni maske düzenleyici arayüzüne geç"
},
"Comfy_ModelLibrary_AutoLoadAll": {
"name": "Tüm model klasörlerini otomatik olarak yükle",
"tooltip": "Doğruysa, model kütüphanesini açar açmaz tüm klasörler yüklenecektir (bu, yüklenirken gecikmelere neden olabilir). Yanlışsa, kök düzeyindeki model klasörleri yalnızca üzerlerine tıkladığınızda yüklenecektir."
},
"Comfy_ModelLibrary_NameFormat": {
"name": "Model kütüphanesi ağaç görünümünde hangi adın görüntüleneceği",
"tooltip": "Model listesinde ham dosya adının (dizin veya \".safetensors\" uzantısı olmadan) basitleştirilmiş bir görünümünü oluşturmak için \"dosyaadı\"nı seçin. Yapılandırılabilir model meta veri başlığını görüntülemek için \"başlık\"ı seçin.",
"options": {
"filename": "dosyaadı",
"title": "başlık"
}
},
"Comfy_Node_AllowImageSizeDraw": {
"name": "Görüntü önizlemesinin altında genişlik × yüksekliği göster"
},
"Comfy_Node_AutoSnapLinkToSlot": {
"name": "Bağlantıyı otomatik olarak düğüm yuvasına yapıştır",
"tooltip": "Bir bağlantıyı bir düğümün üzerine sürüklerken, bağlantı otomatik olarak düğüm üzerindeki uygun bir giriş yuvasına yapışır"
},
"Comfy_Node_BypassAllLinksOnDelete": {
"name": "Düğümleri silerken tüm bağlantıları koru",
"tooltip": "Bir düğümü silerken, tüm giriş ve çıkış bağlantılarını yeniden bağlamaya çalışın (silinen düğümü atlayarak)"
},
"Comfy_Node_DoubleClickTitleToEdit": {
"name": "Düzenlemek için düğüm başlığına çift tıkla"
},
"Comfy_Node_MiddleClickRerouteNode": {
"name": "Orta tıklama yeni bir Yeniden Yönlendirme düğümü oluşturur"
},
"Comfy_Node_Opacity": {
"name": "Düğüm opaklığı"
},
"Comfy_Node_ShowDeprecated": {
"name": "Aramada kullanımdan kaldırılmış düğümleri göster",
"tooltip": "Kullanımdan kaldırılmış düğümler arayüzde varsayılan olarak gizlidir, ancak bunları kullanan mevcut iş akışlarında işlevsel kalır."
},
"Comfy_Node_ShowExperimental": {
"name": "Aramada deneysel düğümleri göster",
"tooltip": "Deneysel düğümler arayüzde bu şekilde işaretlenmiştir ve gelecekteki sürümlerde önemli değişikliklere veya kaldırılmaya tabi olabilir. Üretim iş akışlarında dikkatli kullanın"
},
"Comfy_Node_SnapHighlightsNode": {
"name": "Yapıştırma düğümü vurgular",
"tooltip": "Uygun giriş yuvasına sahip bir düğümün üzerine bir bağlantı sürüklerken, düğümü vurgulayın"
},
"Comfy_NodeBadge_NodeIdBadgeMode": {
"name": "Düğüm ID rozeti modu",
"options": {
"None": "Yok",
"Show all": "Tümünü göster"
}
},
"Comfy_NodeBadge_NodeLifeCycleBadgeMode": {
"name": "Düğüm yaşam döngüsü rozeti modu",
"options": {
"None": "Yok",
"Show all": "Tümünü göster"
}
},
"Comfy_NodeBadge_NodeSourceBadgeMode": {
"name": "Düğüm kaynak rozeti modu",
"options": {
"None": "Yok",
"Show all": "Tümünü göster",
"Hide built-in": "Yerleşik olanı gizle"
}
},
"Comfy_NodeBadge_ShowApiPricing": {
"name": "API düğüm fiyatlandırma rozetini göster"
},
"Comfy_NodeSearchBoxImpl": {
"name": "Düğüm arama kutusu uygulaması",
"options": {
"default": "varsayılan",
"litegraph (legacy)": "litegraph (eski)"
}
},
"Comfy_NodeSearchBoxImpl_NodePreview": {
"name": "Düğüm önizlemesi",
"tooltip": "Yalnızca varsayılan uygulama için geçerlidir"
},
"Comfy_NodeSearchBoxImpl_ShowCategory": {
"name": "Arama sonuçlarında düğüm kategorisini göster",
"tooltip": "Yalnızca varsayılan uygulama için geçerlidir"
},
"Comfy_NodeSearchBoxImpl_ShowIdName": {
"name": "Arama sonuçlarında düğüm kimliği adını göster",
"tooltip": "Yalnızca varsayılan uygulama için geçerlidir"
},
"Comfy_NodeSearchBoxImpl_ShowNodeFrequency": {
"name": "Arama sonuçlarında düğüm sıklığını göster",
"tooltip": "Yalnızca varsayılan uygulama için geçerlidir"
},
"Comfy_NodeSuggestions_number": {
"name": "Düğüm öneri sayısı",
"tooltip": "Yalnızca litegraph arama kutusu/bağlam menüsü için"
},
"Comfy_Notification_ShowVersionUpdates": {
"name": "Sürüm güncellemelerini göster",
"tooltip": "Yeni modeller ve önemli yeni özellikler için güncellemeleri göster."
},
"Comfy_Pointer_ClickBufferTime": {
"name": "İşaretçi tıklama kayma gecikmesi",
"tooltip": "Bir işaretçi düğmesine bastıktan sonra, bu, işaretçi hareketinin göz ardı edilebileceği maksimum süredir (milisaniye cinsinden).\n\nTıklarken işaretçi hareket ettirilirse nesnelerin istemeden dürtülmesini önlemeye yardımcı olur."
},
"Comfy_Pointer_ClickDrift": {
"name": "İşaretçi tıklama kayması (maksimum mesafe)",
"tooltip": "İşaretçi bir düğmeyi basılı tutarken bu mesafeden daha fazla hareket ederse, bu sürükleme olarak kabul edilir (tıklama yerine).\n\nTıklarken işaretçi hareket ettirilirse nesnelerin istemeden dürtülmesini önlemeye yardımcı olur."
},
"Comfy_Pointer_DoubleClickTime": {
"name": "Çift tıklama aralığı (maksimum)",
"tooltip": "Çift tıklamanın iki tıklaması arasındaki milisaniye cinsinden maksimum süre. Bu değeri artırmak, çift tıklamaların bazen kaydedilmemesi durumunda yardımcı olabilir."
},
"Comfy_PreviewFormat": {
"name": "Önizleme görüntü formatı",
"tooltip": "Görüntü widget'ında bir önizleme görüntülerken, onu hafif bir görüntüye dönüştürün, örn. webp, jpeg, webp;50, vb."
},
"Comfy_PromptFilename": {
"name": "İş akışını kaydederken dosya adı iste"
},
"Comfy_Queue_MaxHistoryItems": {
"name": "Kuyruk geçmişi boyutu",
"tooltip": "Kuyruk geçmişinde gösterilen maksimum görev sayısı."
},
"Comfy_QueueButton_BatchCountLimit": {
"name": "Toplu iş sayısı sınırı",
"tooltip": "Tek bir düğme tıklamasıyla kuyruğa eklenen maksimum görev sayısı"
},
"Comfy_Sidebar_Location": {
"name": "Kenar çubuğu konumu",
"options": {
"left": "sol",
"right": "sağ"
}
},
"Comfy_Sidebar_Size": {
"name": "Kenar çubuğu boyutu",
"options": {
"normal": "normal",
"small": "küçük"
}
},
"Comfy_Sidebar_UnifiedWidth": {
"name": "Birleşik kenar çubuğu genişliği"
},
"Comfy_SnapToGrid_GridSize": {
"name": "Izgaraya yapıştırma boyutu",
"tooltip": "Shift tuşunu basılı tutarken düğümleri sürükleyip yeniden boyutlandırırken ızgaraya hizalanacaklar, bu o ızgaranın boyutunu kontrol eder."
},
"Comfy_TextareaWidget_FontSize": {
"name": "Metin alanı widget yazı tipi boyutu"
},
"Comfy_TextareaWidget_Spellcheck": {
"name": "Metin alanı widget yazım denetimi"
},
"Comfy_TreeExplorer_ItemPadding": {
"name": "Ağaç gezgini öğe dolgusu"
},
"Comfy_UseNewMenu": {
"name": "Yeni menüyü kullan",
"tooltip": "Menü çubuğu konumu. Mobil cihazlarda menü her zaman üstte gösterilir.",
"options": {
"Disabled": "Devre dışı",
"Top": "Üst",
"Bottom": "Alt"
}
},
"Comfy_Validation_Workflows": {
"name": "İş akışlarını doğrula"
},
"Comfy_VueNodes_Enabled": {
"name": "Vue düğüm oluşturmayı etkinleştir",
"tooltip": "Düğümleri tuval öğeleri yerine Vue bileşenleri olarak oluşturun. Deneysel özellik."
},
"Comfy_VueNodes_Widgets": {
"name": "Vue widget'larını etkinleştir",
"tooltip": "Widget'ları Vue düğümleri içinde Vue bileşenleri olarak oluşturun."
},
"Comfy_WidgetControlMode": {
"name": "Widget kontrol modu",
"tooltip": "Widget değerlerinin ne zaman güncelleneceğini (rastgele/artırma/azaltma), istem kuyruğa alınmadan önce veya sonra kontrol eder.",
"options": {
"before": "önce",
"after": "sonra"
}
},
"Comfy_Window_UnloadConfirmation": {
"name": "Pencereyi kapatırken onay göster"
},
"Comfy_Workflow_AutoSave": {
"name": "Otomatik Kaydet",
"options": {
"off": "kapalı",
"after delay": "gecikmeden sonra"
}
},
"Comfy_Workflow_AutoSaveDelay": {
"name": "Otomatik Kaydetme Gecikmesi (ms)",
"tooltip": "Yalnızca Otomatik Kaydetme \"gecikmeden sonra\" olarak ayarlandığında geçerlidir."
},
"Comfy_Workflow_ConfirmDelete": {
"name": "İş akışlarını silerken onay göster"
},
"Comfy_Workflow_Persist": {
"name": "İş akışı durumunu koru ve sayfayı (yeniden) yüklediğinde geri yükle"
},
"Comfy_Workflow_ShowMissingModelsWarning": {
"name": "Eksik model uyarısını göster"
},
"Comfy_Workflow_ShowMissingNodesWarning": {
"name": "Eksik düğüm uyarısını göster"
},
"Comfy_Workflow_SortNodeIdOnSave": {
"name": "İş akışını kaydederken düğüm kimliklerini sırala"
},
"Comfy_Workflow_WorkflowTabsPosition": {
"name": "Açılan iş akışları konumu",
"options": {
"Sidebar": "Kenar Çubuğu",
"Topbar": "Üst Çubuk",
"Topbar (2nd-row)": "Üst Çubuk (2. sıra)"
}
},
"LiteGraph_Canvas_MinFontSizeForLOD": {
"name": "Yakınlaştırma Düğümü Ayrıntı Seviyesi - yazı tipi boyutu eşiği",
"tooltip": "Düğümlerin ne zaman düşük kaliteli LOD oluşturmaya geçeceğini kontrol eder. Ne zaman geçiş yapılacağını belirlemek için piksel cinsinden yazı tipi boyutunu kullanır. Devre dışı bırakmak için 0'a ayarlayın. 1-24 arasındaki değerler LOD için minimum yazı tipi boyutu eşiğini ayarlar - daha yüksek değerler (24 piksel) = uzaklaştırırken düğümleri daha erken basitleştirilmiş oluşturmaya geçirin, daha düşük değerler (1 piksel) = tam düğüm kalitesini daha uzun süre koruyun."
},
"LiteGraph_Canvas_MaximumFps": {
"name": "Maksimum FPS",
"tooltip": "Tuvalin saniyede oluşturmasına izin verilen maksimum kare sayısı. Akıcılık pahasına GPU kullanımını sınırlar. 0 ise, ekran yenileme hızı kullanılır. Varsayılan: 0"
},
"LiteGraph_ContextMenu_Scaling": {
"name": "Yakınlaştırıldığında düğüm birleşik widget menülerini (listeleri) ölçeklendir"
},
"LiteGraph_Node_DefaultPadding": {
"name": "Yeni düğümleri her zaman küçült",
"tooltip": "Oluşturulduğunda düğümleri mümkün olan en küçük boyuta yeniden boyutlandırın. Devre dışı bırakıldığında, yeni eklenen bir düğüm widget değerlerini göstermek için biraz genişletilecektir."
},
"LiteGraph_Node_TooltipDelay": {
"name": "Araç İpucu Gecikmesi"
},
"LiteGraph_Reroute_SplineOffset": {
"name": "Yeniden yönlendirme eğri ofseti",
"tooltip": "Yeniden yönlendirme merkez noktasından bezier kontrol noktası ofseti"
},
"pysssss_SnapToGrid": {
"name": "Her zaman ızgaraya yapıştır"
}
}

View File

@@ -404,7 +404,8 @@ export const CORE_SETTINGS: SettingParams[] = [
{ value: 'ko', text: '한국어' },
{ value: 'fr', text: 'Français' },
{ value: 'es', text: 'Español' },
{ value: 'ar', text: 'عربي' }
{ value: 'ar', text: 'عربي' },
{ value: 'tr', text: 'Türkçe' }
],
defaultValue: () => navigator.language.split('-')[0] || 'en'
},

View File

@@ -17,7 +17,7 @@ import { downloadBlob } from '@/scripts/utils'
import { useDialogService } from '@/services/dialogService'
import { useDomWidgetStore } from '@/stores/domWidgetStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { appendJsonExt, generateUUID } from '@/utils/formatUtil'
import { appendJsonExt } from '@/utils/formatUtil'
export const useWorkflowService = () => {
const settingStore = useSettingStore()
@@ -112,13 +112,6 @@ export const useWorkflowService = () => {
await renameWorkflow(workflow, newPath)
await workflowStore.saveWorkflow(workflow)
} else {
// Generate new id when saving existing workflow as a new file
const id = generateUUID()
const state = JSON.parse(
JSON.stringify(workflow.activeState)
) as ComfyWorkflowJSON
state.id = id
const tempWorkflow = workflowStore.saveAs(workflow, newPath)
await openWorkflow(tempWorkflow)
await workflowStore.saveWorkflow(tempWorkflow)

View File

@@ -20,7 +20,7 @@ import {
parseNodeExecutionId,
parseNodeLocatorId
} from '@/types/nodeIdentification'
import { getPathDetails } from '@/utils/formatUtil'
import { generateUUID, getPathDetails } from '@/utils/formatUtil'
import { syncEntities } from '@/utils/syncUtil'
import { isSubgraph } from '@/utils/typeGuardUtil'
@@ -320,12 +320,19 @@ export const useWorkflowStore = defineStore('workflow', () => {
existingWorkflow: ComfyWorkflow,
path: string
): ComfyWorkflow => {
// Generate new id when saving existing workflow as a new file
const id = generateUUID()
const state = JSON.parse(
JSON.stringify(existingWorkflow.activeState)
) as ComfyWorkflowJSON
state.id = id
const workflow: ComfyWorkflow = new (existingWorkflow.constructor as any)({
path,
modified: Date.now(),
size: -1
})
workflow.originalContent = workflow.content = existingWorkflow.content
workflow.originalContent = workflow.content = JSON.stringify(state)
workflowLookup.value[workflow.path] = workflow
return workflow
}

View File

@@ -0,0 +1,73 @@
import { getActivePinia } from 'pinia'
import type {
INodeInputSlot,
INodeOutputSlot
} from '@/lib/litegraph/src/interfaces'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import type {
SlotDragSource,
SlotDropCandidate
} from '@/renderer/core/canvas/links/slotLinkDragState'
import { app } from '@/scripts/app'
interface CompatibilityResult {
allowable: boolean
targetNode?: LGraphNode
targetSlot?: INodeInputSlot | INodeOutputSlot
}
function resolveNode(nodeId: NodeId) {
const pinia = getActivePinia()
const canvasStore = pinia ? useCanvasStore() : null
const graph = canvasStore?.canvas?.graph ?? app.canvas?.graph
if (!graph) return null
const id = typeof nodeId === 'string' ? Number(nodeId) : nodeId
if (Number.isNaN(id)) return null
return graph.getNodeById(id)
}
export function evaluateCompatibility(
source: SlotDragSource,
candidate: SlotDropCandidate
): CompatibilityResult {
if (candidate.layout.nodeId === source.nodeId) {
return { allowable: false }
}
const isOutputToInput =
source.type === 'output' && candidate.layout.type === 'input'
const isInputToOutput =
source.type === 'input' && candidate.layout.type === 'output'
if (!isOutputToInput && !isInputToOutput) {
return { allowable: false }
}
const sourceNode = resolveNode(source.nodeId)
const targetNode = resolveNode(candidate.layout.nodeId)
if (!sourceNode || !targetNode) {
return { allowable: false }
}
if (isOutputToInput) {
const outputSlot = sourceNode.outputs?.[source.slotIndex]
const inputSlot = targetNode.inputs?.[candidate.layout.index]
if (!outputSlot || !inputSlot) {
return { allowable: false }
}
const allowable = sourceNode.canConnectTo(targetNode, inputSlot, outputSlot)
return { allowable, targetNode, targetSlot: inputSlot }
}
const inputSlot = sourceNode.inputs?.[source.slotIndex]
const outputSlot = targetNode.outputs?.[candidate.layout.index]
if (!inputSlot || !outputSlot) {
return { allowable: false }
}
const allowable = targetNode.canConnectTo(sourceNode, inputSlot, outputSlot)
return { allowable, targetNode, targetSlot: outputSlot }
}

View File

@@ -0,0 +1,95 @@
import { reactive, readonly } from 'vue'
import type { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import type { Point, SlotLayout } from '@/renderer/core/layout/types'
type SlotDragType = 'input' | 'output'
export interface SlotDragSource {
nodeId: string
slotIndex: number
type: SlotDragType
direction: LinkDirection
position: Readonly<Point>
}
export interface SlotDropCandidate {
layout: SlotLayout
compatible: boolean
}
interface PointerPosition {
client: Point
canvas: Point
}
interface SlotDragState {
active: boolean
pointerId: number | null
source: SlotDragSource | null
pointer: PointerPosition
candidate: SlotDropCandidate | null
}
const state = reactive<SlotDragState>({
active: false,
pointerId: null,
source: null,
pointer: {
client: { x: 0, y: 0 },
canvas: { x: 0, y: 0 }
},
candidate: null
})
function updatePointerPosition(
clientX: number,
clientY: number,
canvasX: number,
canvasY: number
) {
state.pointer.client.x = clientX
state.pointer.client.y = clientY
state.pointer.canvas.x = canvasX
state.pointer.canvas.y = canvasY
}
function setCandidate(candidate: SlotDropCandidate | null) {
state.candidate = candidate
}
function beginDrag(source: SlotDragSource, pointerId: number) {
state.active = true
state.source = source
state.pointerId = pointerId
state.candidate = null
}
function endDrag() {
state.active = false
state.pointerId = null
state.source = null
state.pointer.client.x = 0
state.pointer.client.y = 0
state.pointer.canvas.x = 0
state.pointer.canvas.y = 0
state.candidate = null
}
function getSlotLayout(nodeId: string, slotIndex: number, isInput: boolean) {
const slotKey = getSlotKey(nodeId, slotIndex, isInput)
return layoutStore.getSlotLayout(slotKey)
}
export function useSlotLinkDragState() {
return {
state: readonly(state),
beginDrag,
endDrag,
updatePointerPosition,
setCandidate,
getSlotLayout
}
}

View File

@@ -0,0 +1,95 @@
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
import type {
INodeInputSlot,
INodeOutputSlot,
ReadOnlyPoint
} from '@/lib/litegraph/src/interfaces'
import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
import { resolveConnectingLinkColor } from '@/lib/litegraph/src/utils/linkColors'
import {
type SlotDragSource,
useSlotLinkDragState
} from '@/renderer/core/canvas/links/slotLinkDragState'
import type { LinkRenderContext } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter'
function buildContext(canvas: LGraphCanvas): LinkRenderContext {
return {
renderMode: canvas.links_render_mode,
connectionWidth: canvas.connections_width,
renderBorder: canvas.render_connections_border,
lowQuality: canvas.low_quality,
highQualityRender: canvas.highquality_render,
scale: canvas.ds.scale,
linkMarkerShape: canvas.linkMarkerShape,
renderConnectionArrows: canvas.render_connection_arrows,
highlightedLinks: new Set(Object.keys(canvas.highlighted_links)),
defaultLinkColor: canvas.default_link_color,
linkTypeColors: (canvas.constructor as typeof LGraphCanvas)
.link_type_colors,
disabledPattern: canvas._pattern
}
}
export function attachSlotLinkPreviewRenderer(canvas: LGraphCanvas) {
const originalOnDrawForeground = canvas.onDrawForeground?.bind(canvas)
const patched = (
ctx: CanvasRenderingContext2D,
area: LGraphCanvas['visible_area']
) => {
originalOnDrawForeground?.(ctx, area)
const { state } = useSlotLinkDragState()
if (!state.active || !state.source) return
const { pointer, source } = state
const start = source.position
const sourceSlot = resolveSourceSlot(canvas, source)
const linkRenderer = canvas.linkRenderer
if (!linkRenderer) return
const context = buildContext(canvas)
const from: ReadOnlyPoint = [start.x, start.y]
const to: ReadOnlyPoint = [pointer.canvas.x, pointer.canvas.y]
const startDir = source.direction ?? LinkDirection.RIGHT
const endDir = LinkDirection.CENTER
const colour = resolveConnectingLinkColor(sourceSlot?.type)
ctx.save()
linkRenderer.renderDraggingLink(
ctx,
from,
to,
colour,
startDir,
endDir,
context
)
ctx.restore()
}
canvas.onDrawForeground = patched
}
function resolveSourceSlot(
canvas: LGraphCanvas,
source: SlotDragSource
): INodeInputSlot | INodeOutputSlot | undefined {
const graph = canvas.graph
if (!graph) return undefined
const nodeId = Number(source.nodeId)
if (!Number.isFinite(nodeId)) return undefined
const node = graph.getNodeById(nodeId)
if (!node) return undefined
return source.type === 'output'
? node.outputs?.[source.slotIndex]
: node.inputs?.[source.slotIndex]
}

View File

@@ -7,13 +7,10 @@
* Maintains backward compatibility with existing litegraph integration.
*/
import type { LGraph } from '@/lib/litegraph/src/LGraph'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { LLink } from '@/lib/litegraph/src/LLink'
import type { Reroute } from '@/lib/litegraph/src/Reroute'
import type {
CanvasColour,
INodeInputSlot,
INodeOutputSlot,
ReadOnlyPoint
} from '@/lib/litegraph/src/interfaces'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
@@ -27,7 +24,6 @@ import {
type ArrowShape,
CanvasPathRenderer,
type Direction,
type DragLinkData,
type LinkRenderData,
type RenderContext as PathRenderContext,
type Point,
@@ -209,7 +205,6 @@ export class LitegraphLinkAdapter {
case LinkDirection.DOWN:
return 'down'
case LinkDirection.CENTER:
case LinkDirection.NONE:
return 'none'
default:
return 'right'
@@ -502,57 +497,33 @@ export class LitegraphLinkAdapter {
}
}
/**
* Render a link being dragged from a slot to mouse position
* Used during link creation/reconnection
*/
renderDraggingLink(
ctx: CanvasRenderingContext2D,
fromNode: LGraphNode | null,
fromSlot: INodeOutputSlot | INodeInputSlot,
fromSlotIndex: number,
toPosition: ReadOnlyPoint,
context: LinkRenderContext,
options: {
fromInput?: boolean
color?: CanvasColour
disabled?: boolean
} = {}
from: ReadOnlyPoint,
to: ReadOnlyPoint,
colour: CanvasColour,
startDir: LinkDirection,
endDir: LinkDirection,
context: LinkRenderContext
): void {
if (!fromNode) return
// Get slot position using layout tree if available
const slotPos = getSlotPosition(
fromNode,
fromSlotIndex,
options.fromInput || false
this.renderLinkDirect(
ctx,
from,
to,
null,
false,
null,
colour,
startDir,
endDir,
{
...context,
linkMarkerShape: LinkMarkerShape.None
},
{
disabled: false
}
)
if (!slotPos) return
// Get slot direction
const slotDir =
fromSlot.dir ||
(options.fromInput ? LinkDirection.LEFT : LinkDirection.RIGHT)
// Create drag data
const dragData: DragLinkData = {
fixedPoint: { x: slotPos[0], y: slotPos[1] },
fixedDirection: this.convertDirection(slotDir),
dragPoint: { x: toPosition[0], y: toPosition[1] },
color: options.color ? String(options.color) : undefined,
type: fromSlot.type !== undefined ? String(fromSlot.type) : undefined,
disabled: options.disabled || false,
fromInput: options.fromInput || false
}
// Convert context
const pathContext = this.convertToPathRenderContext(context)
// Hide center marker when dragging links
pathContext.style.showCenterMarker = false
// Render using pure renderer
this.pathRenderer.drawDraggingLink(ctx, dragData, pathContext)
}
/**

View File

@@ -70,7 +70,7 @@ export interface RenderContext {
highlightedIds?: Set<string>
}
export interface DragLinkData {
interface DragLinkData {
/** Fixed end - the slot being dragged from */
fixedPoint: Point
fixedDirection: Direction

View File

@@ -1,89 +0,0 @@
import { type ComponentMountingOptions, mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import enMessages from '@/locales/en/main.json'
import { useSlotElementTracking } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking'
import InputSlot from './InputSlot.vue'
import OutputSlot from './OutputSlot.vue'
// Mock composable used by InputSlot/OutputSlot so we can assert call params
vi.mock(
'@/renderer/extensions/vueNodes/composables/useSlotElementTracking',
() => ({
useSlotElementTracking: vi.fn(() => ({ stop: vi.fn() }))
})
)
type InputSlotProps = ComponentMountingOptions<typeof InputSlot>['props']
type OutputSlotProps = ComponentMountingOptions<typeof OutputSlot>['props']
const mountInputSlot = (props: InputSlotProps) =>
mount(InputSlot, {
global: {
plugins: [
createI18n({
legacy: false,
locale: 'en',
messages: { en: enMessages }
}),
createPinia()
]
},
props
})
const mountOutputSlot = (props: OutputSlotProps) =>
mount(OutputSlot, {
global: {
plugins: [
createI18n({
legacy: false,
locale: 'en',
messages: { en: enMessages }
}),
createPinia()
]
},
props
})
describe('InputSlot/OutputSlot', () => {
beforeEach(() => {
vi.mocked(useSlotElementTracking).mockClear()
})
it('InputSlot registers with correct options', () => {
mountInputSlot({
nodeId: 'node-1',
index: 3,
slotData: { name: 'A', type: 'any', boundingRect: [0, 0, 0, 0] }
})
expect(useSlotElementTracking).toHaveBeenLastCalledWith(
expect.objectContaining({
nodeId: 'node-1',
index: 3,
type: 'input'
})
)
})
it('OutputSlot registers with correct options', () => {
mountOutputSlot({
nodeId: 'node-2',
index: 1,
slotData: { name: 'B', type: 'any', boundingRect: [0, 0, 0, 0] }
})
expect(useSlotElementTracking).toHaveBeenLastCalledWith(
expect.objectContaining({
nodeId: 'node-2',
index: 1,
type: 'output'
})
)
})
})

View File

@@ -1,21 +1,12 @@
<template>
<div v-if="renderError" class="node-error p-1 text-red-500 text-xs"></div>
<div
v-else
class="lg-slot lg-slot--input flex items-center cursor-crosshair group rounded-r-lg h-6"
:class="{
'opacity-70': readonly,
'lg-slot--connected': connected,
'lg-slot--compatible': compatible,
'lg-slot--dot-only': dotOnly,
'pr-6 hover:bg-black/5 hover:dark:bg-white/5': !dotOnly
}"
>
<div v-else v-tooltip.left="tooltipConfig" :class="slotWrapperClass">
<!-- Connection Dot -->
<SlotConnectionDot
ref="connectionDotRef"
:color="slotColor"
class="-translate-x-1/2"
v-on="readonly ? {} : { pointerdown: onPointerDown }"
/>
<!-- Slot Name -->
@@ -31,7 +22,9 @@
<script setup lang="ts">
import {
type ComponentPublicInstance,
type Ref,
computed,
inject,
onErrorCaptured,
ref,
watchEffect
@@ -39,13 +32,16 @@ import {
import { useErrorHandling } from '@/composables/useErrorHandling'
import { getSlotColor } from '@/constants/slotColors'
import type { INodeSlot, LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
import { useSlotElementTracking } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking'
import { useSlotLinkInteraction } from '@/renderer/extensions/vueNodes/composables/useSlotLinkInteraction'
import { cn } from '@/utils/tailwindUtil'
import SlotConnectionDot from './SlotConnectionDot.vue'
interface InputSlotProps {
node?: LGraphNode
nodeType?: string
nodeId?: string
slotData: INodeSlot
index: number
@@ -61,6 +57,20 @@ const props = defineProps<InputSlotProps>()
const renderError = ref<string | null>(null)
const { toastErrorHandler } = useErrorHandling()
const tooltipContainer =
inject<Ref<HTMLElement | undefined>>('tooltipContainer')
const { getInputSlotTooltip, createTooltipConfig } = useNodeTooltips(
props.nodeType || '',
tooltipContainer
)
const tooltipConfig = computed(() => {
const slotName = props.slotData.localized_name || props.slotData.name || ''
const tooltipText = getInputSlotTooltip(slotName)
const fallbackText = tooltipText || `Input: ${slotName}`
return createTooltipConfig(fallbackText)
})
onErrorCaptured((error) => {
renderError.value = error.message
toastErrorHandler(error)
@@ -70,6 +80,20 @@ onErrorCaptured((error) => {
// Get slot color based on type
const slotColor = computed(() => getSlotColor(props.slotData.type))
const slotWrapperClass = computed(() =>
cn(
'lg-slot lg-slot--input flex items-center group rounded-r-lg h-6',
props.readonly ? 'cursor-default opacity-70' : 'cursor-crosshair',
props.dotOnly
? 'lg-slot--dot-only'
: 'pr-6 hover:bg-black/5 hover:dark:bg-white/5',
{
'lg-slot--connected': props.connected,
'lg-slot--compatible': props.compatible
}
)
)
const connectionDotRef = ref<ComponentPublicInstance<{
slotElRef: HTMLElement | undefined
}> | null>(null)
@@ -88,4 +112,11 @@ useSlotElementTracking({
type: 'input',
element: slotElRef
})
const { onPointerDown } = useSlotLinkInteraction({
nodeId: props.nodeId ?? '',
index: props.index,
type: 'input',
readonly: props.readonly
})
</script>

View File

@@ -4,6 +4,7 @@
</div>
<div
v-else
ref="nodeContainerRef"
:data-node-id="nodeData.id"
:class="
cn(
@@ -493,6 +494,10 @@ watch(
{ deep: true }
)
// Provide nodeImageUrls to child components
// Template ref for tooltip positioning
const nodeContainerRef = ref<HTMLElement>()
// Provide nodeImageUrls and tooltip container to child components
provide('nodeImageUrls', nodeImageUrls)
provide('tooltipContainer', nodeContainerRef)
</script>

View File

@@ -1,12 +1,16 @@
import { mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import { createPinia, setActivePinia } from 'pinia'
import PrimeVue from 'primevue/config'
import InputText from 'primevue/inputtext'
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import enMessages from '@/locales/en/main.json'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { Settings } from '@/schemas/apiSchema'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
import NodeHeader from './NodeHeader.vue'
@@ -24,19 +28,94 @@ const makeNodeData = (overrides: Partial<VueNodeData> = {}): VueNodeData => ({
...overrides
})
const mountHeader = (
props?: Partial<InstanceType<typeof NodeHeader>['$props']>
) => {
const setupMockStores = () => {
const pinia = createPinia()
setActivePinia(pinia)
const settingStore = useSettingStore()
const nodeDefStore = useNodeDefStore()
// Mock tooltip delay setting
vi.spyOn(settingStore, 'get').mockImplementation(
<K extends keyof Settings>(key: K): Settings[K] => {
switch (key) {
case 'Comfy.EnableTooltips':
return true as Settings[K]
case 'LiteGraph.Node.TooltipDelay':
return 500 as Settings[K]
default:
return undefined as Settings[K]
}
}
)
// Mock node definition store
const baseMockNodeDef: ComfyNodeDef = {
name: 'KSampler',
display_name: 'KSampler',
category: 'sampling',
python_module: 'test_module',
description: 'Advanced sampling node for diffusion models',
input: {
required: {
model: ['MODEL', {}],
positive: ['CONDITIONING', {}],
negative: ['CONDITIONING', {}]
},
optional: {},
hidden: {}
},
output: ['LATENT'],
output_is_list: [false],
output_name: ['samples'],
output_node: false,
deprecated: false,
experimental: false
}
const mockNodeDef = new ComfyNodeDefImpl(baseMockNodeDef)
vi.spyOn(nodeDefStore, 'nodeDefsByName', 'get').mockReturnValue({
KSampler: mockNodeDef
})
return { settingStore, nodeDefStore, pinia }
}
const createMountConfig = () => {
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: { en: enMessages }
})
return mount(NodeHeader, {
const { pinia } = setupMockStores()
return {
global: {
plugins: [PrimeVue, i18n, createPinia()],
components: { InputText }
},
plugins: [PrimeVue, i18n, pinia],
components: { InputText },
directives: {
tooltip: {
mounted: vi.fn(),
updated: vi.fn(),
unmounted: vi.fn()
}
},
provide: {
tooltipContainer: { value: document.createElement('div') }
}
}
}
}
const mountHeader = (
props?: Partial<InstanceType<typeof NodeHeader>['$props']>
) => {
const config = createMountConfig()
return mount(NodeHeader, {
...config,
props: {
nodeData: makeNodeData(),
readonly: false,
@@ -126,4 +205,68 @@ describe('NodeHeader.vue', () => {
const collapsedIcon = wrapper.get('i')
expect(collapsedIcon.classes()).toContain('pi-chevron-right')
})
describe('Tooltips', () => {
it('applies tooltip directive to node title with correct configuration', () => {
const wrapper = mountHeader({
nodeData: makeNodeData({ type: 'KSampler' })
})
const titleElement = wrapper.find('[data-testid="node-title"]')
expect(titleElement.exists()).toBe(true)
// Check that v-tooltip directive was applied
const directive = wrapper.vm.$el.querySelector(
'[data-testid="node-title"]'
)
expect(directive).toBeTruthy()
})
it('disables tooltip when in readonly mode', () => {
const wrapper = mountHeader({
readonly: true,
nodeData: makeNodeData({ type: 'KSampler' })
})
const titleElement = wrapper.find('[data-testid="node-title"]')
expect(titleElement.exists()).toBe(true)
})
it('disables tooltip when editing is active', async () => {
const wrapper = mountHeader({
nodeData: makeNodeData({ type: 'KSampler' })
})
// Enter edit mode
await wrapper.get('[data-testid="node-header-1"]').trigger('dblclick')
// Tooltip should be disabled during editing
const titleElement = wrapper.find('[data-testid="node-title"]')
expect(titleElement.exists()).toBe(true)
})
it('creates tooltip configuration when component mounts', () => {
const wrapper = mountHeader({
nodeData: makeNodeData({ type: 'KSampler' })
})
// Verify tooltip directive is applied to the title element
const titleElement = wrapper.find('[data-testid="node-title"]')
expect(titleElement.exists()).toBe(true)
// The tooltip composable should be initialized
expect(wrapper.vm).toBeDefined()
})
it('uses tooltip container from provide/inject', () => {
const wrapper = mountHeader({
nodeData: makeNodeData({ type: 'KSampler' })
})
expect(wrapper.exists()).toBe(true)
// Container should be provided through inject
const titleElement = wrapper.find('[data-testid="node-title"]')
expect(titleElement.exists()).toBe(true)
})
})
})

View File

@@ -5,7 +5,7 @@
<div
v-else
class="lg-node-header flex items-center justify-between p-4 rounded-t-2xl cursor-move"
:data-testid="`node-header-${nodeInfo?.id || ''}`"
:data-testid="`node-header-${nodeData?.id || ''}`"
@dblclick="handleDoubleClick"
>
<!-- Collapse/Expand Button -->
@@ -23,7 +23,11 @@
</button>
<!-- Node Title -->
<div class="text-sm font-bold truncate flex-1" data-testid="node-title">
<div
v-tooltip.top="tooltipConfig"
class="text-sm font-bold truncate flex-1"
data-testid="node-title"
>
<EditableText
:model-value="displayTitle"
:is-editing="isEditing"
@@ -36,23 +40,22 @@
</template>
<script setup lang="ts">
import { computed, onErrorCaptured, ref, watch } from 'vue'
import { type Ref, computed, inject, onErrorCaptured, ref, watch } from 'vue'
import EditableText from '@/components/common/EditableText.vue'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useErrorHandling } from '@/composables/useErrorHandling'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
import type { LODLevel } from '@/renderer/extensions/vueNodes/lod/useLOD'
interface NodeHeaderProps {
node?: LGraphNode // For backwards compatibility
nodeData?: VueNodeData // New clean data structure
nodeData?: VueNodeData
readonly?: boolean
lodLevel?: LODLevel
collapsed?: boolean
}
const props = defineProps<NodeHeaderProps>()
const { nodeData, readonly, collapsed } = defineProps<NodeHeaderProps>()
const emit = defineEmits<{
collapse: []
@@ -72,9 +75,22 @@ onErrorCaptured((error) => {
// Editing state
const isEditing = ref(false)
const nodeInfo = computed(() => props.nodeData || props.node)
const tooltipContainer =
inject<Ref<HTMLElement | undefined>>('tooltipContainer')
const { getNodeDescription, createTooltipConfig } = useNodeTooltips(
nodeData?.type || '',
tooltipContainer
)
const resolveTitle = (info: LGraphNode | VueNodeData | undefined) => {
const tooltipConfig = computed(() => {
if (readonly || isEditing.value) {
return { value: '', disabled: true }
}
const description = getNodeDescription.value
return createTooltipConfig(description)
})
const resolveTitle = (info: VueNodeData | undefined) => {
const title = (info?.title ?? '').trim()
if (title.length > 0) return title
const type = (info?.type ?? '').trim()
@@ -82,13 +98,13 @@ const resolveTitle = (info: LGraphNode | VueNodeData | undefined) => {
}
// Local state for title to provide immediate feedback
const displayTitle = ref(resolveTitle(nodeInfo.value))
const displayTitle = ref(resolveTitle(nodeData))
// Watch for external changes to the node title or type
watch(
() => [nodeInfo.value?.title, nodeInfo.value?.type] as const,
() => [nodeData?.title, nodeData?.type] as const,
() => {
const next = resolveTitle(nodeInfo.value)
const next = resolveTitle(nodeData)
if (next !== displayTitle.value) {
displayTitle.value = next
}
@@ -101,7 +117,7 @@ const handleCollapse = () => {
}
const handleDoubleClick = () => {
if (!props.readonly) {
if (!readonly) {
isEditing.value = true
}
}

View File

@@ -8,7 +8,8 @@
v-for="(input, index) in filteredInputs"
:key="`input-${index}`"
:slot-data="input"
:node-id="nodeInfo?.id != null ? String(nodeInfo.id) : ''"
:node-type="nodeData?.type || ''"
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
:index="getActualInputIndex(input, index)"
:readonly="readonly"
/>
@@ -19,7 +20,8 @@
v-for="(output, index) in filteredOutputs"
:key="`output-${index}`"
:slot-data="output"
:node-id="nodeInfo?.id != null ? String(nodeInfo.id) : ''"
:node-type="nodeData?.type || ''"
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
:index="index"
:readonly="readonly"
/>
@@ -32,7 +34,7 @@ import { computed, onErrorCaptured, ref } from 'vue'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useErrorHandling } from '@/composables/useErrorHandling'
import type { INodeSlot, LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
import type { LODLevel } from '@/renderer/extensions/vueNodes/lod/useLOD'
import { isSlotObject } from '@/utils/typeGuardUtil'
@@ -40,21 +42,18 @@ import InputSlot from './InputSlot.vue'
import OutputSlot from './OutputSlot.vue'
interface NodeSlotsProps {
node?: LGraphNode // For backwards compatibility
nodeData?: VueNodeData // New clean data structure
nodeData?: VueNodeData
readonly?: boolean
lodLevel?: LODLevel
}
const props = defineProps<NodeSlotsProps>()
const nodeInfo = computed(() => props.nodeData || props.node || null)
const { nodeData = null, readonly } = defineProps<NodeSlotsProps>()
// Filter out input slots that have corresponding widgets
const filteredInputs = computed(() => {
if (!nodeInfo.value?.inputs) return []
if (!nodeData?.inputs) return []
return nodeInfo.value.inputs
return nodeData.inputs
.filter((input) => {
// Check if this slot has a widget property (indicating it has a corresponding widget)
if (isSlotObject(input) && 'widget' in input && input.widget) {
@@ -76,7 +75,7 @@ const filteredInputs = computed(() => {
// Outputs don't have widgets, so we don't need to filter them
const filteredOutputs = computed(() => {
const outputs = nodeInfo.value?.outputs || []
const outputs = nodeData?.outputs || []
return outputs.map((output) =>
isSlotObject(output)
? output
@@ -94,10 +93,10 @@ const getActualInputIndex = (
input: INodeSlot,
filteredIndex: number
): number => {
if (!nodeInfo.value?.inputs) return filteredIndex
if (!nodeData?.inputs) return filteredIndex
// Find the actual index in the unfiltered inputs array
const actualIndex = nodeInfo.value.inputs.findIndex((i) => i === input)
const actualIndex = nodeData.inputs.findIndex((i) => i === input)
return actualIndex !== -1 ? actualIndex : filteredIndex
}

View File

@@ -31,7 +31,7 @@
type: widget.type,
boundingRect: [0, 0, 0, 0]
}"
:node-id="nodeInfo?.id != null ? String(nodeInfo.id) : ''"
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
:index="getWidgetInputIndex(widget)"
:readonly="readonly"
:dot-only="true"
@@ -40,6 +40,7 @@
<!-- Widget Component -->
<component
:is="widget.vueComponent"
v-tooltip.left="widget.tooltipConfig"
:widget="widget.simplified"
:model-value="widget.value"
:readonly="readonly"
@@ -51,15 +52,15 @@
</template>
<script setup lang="ts">
import { computed, onErrorCaptured, ref } from 'vue'
import { type Ref, computed, inject, onErrorCaptured, ref } from 'vue'
import type {
SafeWidgetData,
VueNodeData
} from '@/composables/graph/useGraphNodeManager'
import { useErrorHandling } from '@/composables/useErrorHandling'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
import { LODLevel } from '@/renderer/extensions/vueNodes/lod/useLOD'
// Import widget components directly
import WidgetInputText from '@/renderer/extensions/vueNodes/widgets/components/WidgetInputText.vue'
@@ -74,13 +75,12 @@ import { cn } from '@/utils/tailwindUtil'
import InputSlot from './InputSlot.vue'
interface NodeWidgetsProps {
node?: LGraphNode
nodeData?: VueNodeData
readonly?: boolean
lodLevel?: LODLevel
}
const props = defineProps<NodeWidgetsProps>()
const { nodeData, readonly, lodLevel } = defineProps<NodeWidgetsProps>()
const { shouldHandleNodePointerEvents, forwardEventToCanvas } =
useCanvasInteractions()
@@ -101,7 +101,13 @@ onErrorCaptured((error) => {
return false
})
const nodeInfo = computed(() => props.nodeData || props.node)
const nodeType = computed(() => nodeData?.type || '')
const tooltipContainer =
inject<Ref<HTMLElement | undefined>>('tooltipContainer')
const { getWidgetTooltip, createTooltipConfig } = useNodeTooltips(
nodeType.value,
tooltipContainer
)
interface ProcessedWidget {
name: string
@@ -110,14 +116,13 @@ interface ProcessedWidget {
simplified: SimplifiedWidget
value: WidgetValue
updateHandler: (value: unknown) => void
tooltipConfig: any
}
const processedWidgets = computed((): ProcessedWidget[] => {
const info = nodeInfo.value
if (!info?.widgets) return []
if (!nodeData?.widgets) return []
const widgets = info.widgets as SafeWidgetData[]
const lodLevel = props.lodLevel
const widgets = nodeData.widgets as SafeWidgetData[]
const result: ProcessedWidget[] = []
if (lodLevel === LODLevel.MINIMAL) {
@@ -148,13 +153,17 @@ const processedWidgets = computed((): ProcessedWidget[] => {
}
}
const tooltipText = getWidgetTooltip(widget)
const tooltipConfig = createTooltipConfig(tooltipText)
result.push({
name: widget.name,
type: widget.type,
vueComponent,
simplified,
value: widget.value,
updateHandler
updateHandler,
tooltipConfig
})
}
@@ -165,7 +174,7 @@ const processedWidgets = computed((): ProcessedWidget[] => {
// or restructuring data model to unify widgets and inputs
// Map a widget to its corresponding input slot index
const getWidgetInputIndex = (widget: ProcessedWidget): number => {
const inputs = nodeInfo.value?.inputs
const inputs = nodeData?.inputs
if (!inputs) return 0
const idx = inputs.findIndex((input: any) => {

View File

@@ -1,17 +1,6 @@
<template>
<div v-if="renderError" class="node-error p-1 text-red-500 text-xs"></div>
<div
v-else
class="lg-slot lg-slot--output flex items-center cursor-crosshair justify-end group rounded-l-lg h-6"
:class="{
'opacity-70': readonly,
'lg-slot--connected': connected,
'lg-slot--compatible': compatible,
'lg-slot--dot-only': dotOnly,
'pl-6 hover:bg-black/5 hover:dark:bg-white/5': !dotOnly,
'justify-center': dotOnly
}"
>
<div v-else v-tooltip.right="tooltipConfig" :class="slotWrapperClass">
<!-- Slot Name -->
<span
v-if="!dotOnly"
@@ -25,6 +14,7 @@
ref="connectionDotRef"
:color="slotColor"
class="translate-x-1/2"
v-on="readonly ? {} : { pointerdown: onPointerDown }"
/>
</div>
</template>
@@ -32,7 +22,9 @@
<script setup lang="ts">
import {
type ComponentPublicInstance,
type Ref,
computed,
inject,
onErrorCaptured,
ref,
watchEffect
@@ -40,13 +32,16 @@ import {
import { useErrorHandling } from '@/composables/useErrorHandling'
import { getSlotColor } from '@/constants/slotColors'
import type { INodeSlot, LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
import { useSlotElementTracking } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking'
import { useSlotLinkInteraction } from '@/renderer/extensions/vueNodes/composables/useSlotLinkInteraction'
import { cn } from '@/utils/tailwindUtil'
import SlotConnectionDot from './SlotConnectionDot.vue'
interface OutputSlotProps {
node?: LGraphNode
nodeType?: string
nodeId?: string
slotData: INodeSlot
index: number
@@ -63,6 +58,20 @@ const renderError = ref<string | null>(null)
const { toastErrorHandler } = useErrorHandling()
const tooltipContainer =
inject<Ref<HTMLElement | undefined>>('tooltipContainer')
const { getOutputSlotTooltip, createTooltipConfig } = useNodeTooltips(
props.nodeType || '',
tooltipContainer
)
const tooltipConfig = computed(() => {
const slotName = props.slotData.name || ''
const tooltipText = getOutputSlotTooltip(props.index)
const fallbackText = tooltipText || `Output: ${slotName}`
return createTooltipConfig(fallbackText)
})
onErrorCaptured((error) => {
renderError.value = error.message
toastErrorHandler(error)
@@ -72,6 +81,20 @@ onErrorCaptured((error) => {
// Get slot color based on type
const slotColor = computed(() => getSlotColor(props.slotData.type))
const slotWrapperClass = computed(() =>
cn(
'lg-slot lg-slot--output flex items-center justify-end group rounded-l-lg h-6',
props.readonly ? 'cursor-default opacity-70' : 'cursor-crosshair',
props.dotOnly
? 'lg-slot--dot-only justify-center'
: 'pl-6 hover:bg-black/5 hover:dark:bg-white/5',
{
'lg-slot--connected': props.connected,
'lg-slot--compatible': props.compatible
}
)
)
const connectionDotRef = ref<ComponentPublicInstance<{
slotElRef: HTMLElement | undefined
}> | null>(null)
@@ -90,4 +113,11 @@ useSlotElementTracking({
type: 'output',
element: slotElRef
})
const { onPointerDown } = useSlotLinkInteraction({
nodeId: props.nodeId ?? '',
index: props.index,
type: 'output',
readonly: props.readonly
})
</script>

View File

@@ -0,0 +1,120 @@
import { type MaybeRef, type Ref, computed, unref } from 'vue'
import type { SafeWidgetData } from '@/composables/graph/useGraphNodeManager'
import { st } from '@/i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { normalizeI18nKey } from '@/utils/formatUtil'
/**
* Composable for managing Vue node tooltips
* Provides tooltip text for node headers, slots, and widgets
*/
export function useNodeTooltips(
nodeType: MaybeRef<string>,
containerRef?: Ref<HTMLElement | undefined>
) {
const nodeDefStore = useNodeDefStore()
const settingsStore = useSettingStore()
// Check if tooltips are globally enabled
const tooltipsEnabled = computed(() =>
settingsStore.get('Comfy.EnableTooltips')
)
// Get node definition for tooltip data
const nodeDef = computed(() => nodeDefStore.nodeDefsByName[unref(nodeType)])
/**
* Get tooltip text for node description (header hover)
*/
const getNodeDescription = computed(() => {
if (!tooltipsEnabled.value || !nodeDef.value) return ''
const key = `nodeDefs.${normalizeI18nKey(unref(nodeType))}.description`
return st(key, nodeDef.value.description || '')
})
/**
* Get tooltip text for input slots
*/
const getInputSlotTooltip = (slotName: string) => {
if (!tooltipsEnabled.value || !nodeDef.value) return ''
const key = `nodeDefs.${normalizeI18nKey(unref(nodeType))}.inputs.${normalizeI18nKey(slotName)}.tooltip`
const inputTooltip = nodeDef.value.inputs?.[slotName]?.tooltip ?? ''
return st(key, inputTooltip)
}
/**
* Get tooltip text for output slots
*/
const getOutputSlotTooltip = (slotIndex: number) => {
if (!tooltipsEnabled.value || !nodeDef.value) return ''
const key = `nodeDefs.${normalizeI18nKey(unref(nodeType))}.outputs.${slotIndex}.tooltip`
const outputTooltip = nodeDef.value.outputs?.[slotIndex]?.tooltip ?? ''
return st(key, outputTooltip)
}
/**
* Get tooltip text for widgets
*/
const getWidgetTooltip = (widget: SafeWidgetData) => {
if (!tooltipsEnabled.value || !nodeDef.value) return ''
// First try widget-specific tooltip
const widgetTooltip = (widget as { tooltip?: string }).tooltip
if (widgetTooltip) return widgetTooltip
// Then try input-based tooltip lookup
const key = `nodeDefs.${normalizeI18nKey(unref(nodeType))}.inputs.${normalizeI18nKey(widget.name)}.tooltip`
const inputTooltip = nodeDef.value.inputs?.[widget.name]?.tooltip ?? ''
return st(key, inputTooltip)
}
/**
* Create tooltip configuration object for v-tooltip directive
*/
const createTooltipConfig = (text: string) => {
const tooltipDelay = settingsStore.get('LiteGraph.Node.TooltipDelay')
const tooltipText = text || ''
const config: {
value: string
showDelay: number
disabled: boolean
appendTo?: HTMLElement
pt?: any
} = {
value: tooltipText,
showDelay: tooltipDelay as number,
disabled: !tooltipsEnabled.value || !tooltipText,
pt: {
text: {
class:
'bg-charcoal-100 border border-slate-300 rounded-md px-4 py-2 text-white text-sm font-normal leading-tight max-w-75 shadow-none'
},
arrow: {
class: 'before:border-charcoal-100'
}
}
}
// If we have a container reference, append tooltips to it
if (containerRef?.value) {
config.appendTo = containerRef.value
}
return config
}
return {
tooltipsEnabled,
getNodeDescription,
getInputSlotTooltip,
getOutputSlotTooltip,
getWidgetTooltip,
createTooltipConfig
}
}

View File

@@ -150,7 +150,6 @@ export function useSlotElementTracking(options: {
(el) => {
if (!el) return
// Ensure node entry
const node = nodeSlotRegistryStore.ensureNode(nodeId)
if (!node.stopWatch) {
@@ -184,6 +183,8 @@ export function useSlotElementTracking(options: {
// Register slot
const slotKey = getSlotKey(nodeId, index, type === 'input')
el.dataset.slotKey = slotKey
node.slots.set(slotKey, { el, index, type })
// Seed initial sync from DOM
@@ -203,12 +204,15 @@ export function useSlotElementTracking(options: {
// Remove this slot from registry and layout
const slotKey = getSlotKey(nodeId, index, type === 'input')
node.slots.delete(slotKey)
const entry = node.slots.get(slotKey)
if (entry) {
delete entry.el.dataset.slotKey
node.slots.delete(slotKey)
}
layoutStore.deleteSlotLayout(slotKey)
// If node has no more slots, clean up
if (node.slots.size === 0) {
// Stop the node-level watcher when the last slot is gone
if (node.stopWatch) node.stopWatch()
nodeSlotRegistryStore.deleteNode(nodeId)
}

View File

@@ -0,0 +1,247 @@
import { type Fn, useEventListener } from '@vueuse/core'
import { onBeforeUnmount } from 'vue'
import { useSharedCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
import { evaluateCompatibility } from '@/renderer/core/canvas/links/slotLinkCompatibility'
import {
type SlotDropCandidate,
useSlotLinkDragState
} from '@/renderer/core/canvas/links/slotLinkDragState'
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import type { SlotLayout } from '@/renderer/core/layout/types'
import { app } from '@/scripts/app'
interface SlotInteractionOptions {
nodeId: string
index: number
type: 'input' | 'output'
readonly?: boolean
}
interface SlotInteractionHandlers {
onPointerDown: (event: PointerEvent) => void
}
interface PointerSession {
begin: (pointerId: number) => void
register: (...stops: Array<Fn | null | undefined>) => void
matches: (event: PointerEvent) => boolean
isActive: () => boolean
clear: () => void
}
function createPointerSession(): PointerSession {
let pointerId: number | null = null
let stops: Fn[] = []
const begin = (id: number) => {
pointerId = id
}
const register = (...newStops: Array<Fn | null | undefined>) => {
for (const stop of newStops) {
if (typeof stop === 'function') {
stops.push(stop)
}
}
}
const matches = (event: PointerEvent) =>
pointerId !== null && event.pointerId === pointerId
const isActive = () => pointerId !== null
const clear = () => {
for (const stop of stops) {
stop()
}
stops = []
pointerId = null
}
return { begin, register, matches, isActive, clear }
}
export function useSlotLinkInteraction({
nodeId,
index,
type,
readonly
}: SlotInteractionOptions): SlotInteractionHandlers {
if (readonly) {
return {
onPointerDown: () => {}
}
}
const { state, beginDrag, endDrag, updatePointerPosition } =
useSlotLinkDragState()
function candidateFromTarget(
target: EventTarget | null
): SlotDropCandidate | null {
if (!(target instanceof HTMLElement)) return null
const key = target.dataset['slotKey']
if (!key) return null
const layout = layoutStore.getSlotLayout(key)
if (!layout) return null
const candidate: SlotDropCandidate = { layout, compatible: false }
if (state.source) {
candidate.compatible = evaluateCompatibility(
state.source,
candidate
).allowable
}
return candidate
}
const conversion = useSharedCanvasPositionConversion()
const pointerSession = createPointerSession()
const cleanupInteraction = () => {
pointerSession.clear()
endDrag()
}
const updatePointerState = (event: PointerEvent) => {
const clientX = event.clientX
const clientY = event.clientY
const [canvasX, canvasY] = conversion.clientPosToCanvasPos([
clientX,
clientY
])
updatePointerPosition(clientX, clientY, canvasX, canvasY)
}
const handlePointerMove = (event: PointerEvent) => {
if (!pointerSession.matches(event)) return
updatePointerState(event)
app.canvas?.setDirty(true)
}
const connectSlots = (slotLayout: SlotLayout) => {
const canvas = app.canvas
const graph = canvas?.graph
const source = state.source
if (!canvas || !graph || !source) return
const sourceNode = graph.getNodeById(Number(source.nodeId))
const targetNode = graph.getNodeById(Number(slotLayout.nodeId))
if (!sourceNode || !targetNode) return
if (source.type === 'output' && slotLayout.type === 'input') {
const outputSlot = sourceNode.outputs?.[source.slotIndex]
const inputSlot = targetNode.inputs?.[slotLayout.index]
if (!outputSlot || !inputSlot) return
graph.beforeChange()
sourceNode.connectSlots(outputSlot, targetNode, inputSlot, undefined)
return
}
if (source.type === 'input' && slotLayout.type === 'output') {
const inputSlot = sourceNode.inputs?.[source.slotIndex]
const outputSlot = targetNode.outputs?.[slotLayout.index]
if (!inputSlot || !outputSlot) return
graph.beforeChange()
sourceNode.disconnectInput(source.slotIndex, true)
targetNode.connectSlots(outputSlot, sourceNode, inputSlot, undefined)
}
}
const finishInteraction = (event: PointerEvent) => {
if (!pointerSession.matches(event)) return
event.preventDefault()
if (state.source) {
const candidate = candidateFromTarget(event.target)
if (candidate?.compatible) {
connectSlots(candidate.layout)
}
}
cleanupInteraction()
app.canvas?.setDirty(true)
}
const handlePointerUp = (event: PointerEvent) => {
finishInteraction(event)
}
const handlePointerCancel = (event: PointerEvent) => {
if (!pointerSession.matches(event)) return
cleanupInteraction()
app.canvas?.setDirty(true)
}
const onPointerDown = (event: PointerEvent) => {
if (event.button !== 0) return
if (!nodeId) return
if (pointerSession.isActive()) return
const canvas = app.canvas
const graph = canvas?.graph
if (!canvas || !graph) return
const layout = layoutStore.getSlotLayout(
getSlotKey(nodeId, index, type === 'input')
)
if (!layout) return
const resolvedNode = graph.getNodeById(Number(nodeId))
const slot =
type === 'input'
? resolvedNode?.inputs?.[index]
: resolvedNode?.outputs?.[index]
const direction =
slot?.dir ?? (type === 'input' ? LinkDirection.LEFT : LinkDirection.RIGHT)
beginDrag(
{
nodeId,
slotIndex: index,
type,
direction,
position: layout.position
},
event.pointerId
)
pointerSession.begin(event.pointerId)
updatePointerState(event)
pointerSession.register(
useEventListener(window, 'pointermove', handlePointerMove, {
capture: true
}),
useEventListener(window, 'pointerup', handlePointerUp, {
capture: true
}),
useEventListener(window, 'pointercancel', handlePointerCancel, {
capture: true
})
)
app.canvas?.setDirty(true)
event.preventDefault()
event.stopPropagation()
}
onBeforeUnmount(() => {
if (pointerSession.isActive()) {
cleanupInteraction()
}
})
return {
onPointerDown
}
}

View File

@@ -0,0 +1,507 @@
import { mount } from '@vue/test-utils'
import PrimeVue from 'primevue/config'
import { describe, expect, it, vi } from 'vitest'
import FormSelectButton from './FormSelectButton.vue'
describe('FormSelectButton Core Component', () => {
// Type-safe helper for mounting component
const mountComponent = (
modelValue: string | null | undefined = null,
options: (string | number | Record<string, any>)[] = [],
props: Record<string, unknown> = {}
) => {
return mount(FormSelectButton, {
global: {
plugins: [PrimeVue]
},
props: {
modelValue,
options: options as any,
...props
}
})
}
const clickButton = async (
wrapper: ReturnType<typeof mount>,
buttonText: string
) => {
const buttons = wrapper.findAll('button')
const targetButtonIndex = buttons.findIndex((button) =>
button.text().includes(buttonText)
)
if (targetButtonIndex === -1) {
throw new Error(`Button with text "${buttonText}" not found`)
}
// Use get() which throws if element doesn't exist, providing better error messages
const targetButton = buttons.at(targetButtonIndex)!
await targetButton.trigger('click')
return targetButton
}
describe('Basic Rendering', () => {
it('renders as a horizontal button group layout', () => {
const options = ['option1', 'option2']
const wrapper = mountComponent(null, options)
const container = wrapper.find('div')
const buttons = wrapper.findAll('button')
// Verify layout behavior: container exists and contains buttons
expect(container.exists()).toBe(true)
expect(buttons).toHaveLength(2)
// Verify buttons are arranged horizontally (not vertically stacked)
// This tests the layout logic rather than specific CSS classes
buttons.forEach((button) => {
expect(button.exists()).toBe(true)
expect(button.element.tagName).toBe('BUTTON')
})
})
it('renders buttons for each option', () => {
const options = ['first', 'second', 'third']
const wrapper = mountComponent(null, options)
const buttons = wrapper.findAll('button')
expect(buttons).toHaveLength(3)
expect(buttons[0].text()).toBe('first')
expect(buttons[1].text()).toBe('second')
expect(buttons[2].text()).toBe('third')
})
it('renders empty container when no options provided', () => {
const wrapper = mountComponent(null, [])
const buttons = wrapper.findAll('button')
expect(buttons).toHaveLength(0)
})
it('applies proper button styling', () => {
const options = ['test']
const wrapper = mountComponent(null, options)
const button = wrapper.find('button')
expect(button.classes()).toContain('flex-1')
expect(button.classes()).toContain('h-6')
expect(button.classes()).toContain('px-5')
expect(button.classes()).toContain('py-[5px]')
expect(button.classes()).toContain('rounded')
expect(button.classes()).toContain('text-center')
expect(button.classes()).toContain('text-xs')
expect(button.classes()).toContain('font-normal')
})
})
describe('String Options', () => {
it('handles string array options', () => {
const options = ['apple', 'banana', 'cherry']
const wrapper = mountComponent('banana', options)
const buttons = wrapper.findAll('button')
expect(buttons).toHaveLength(3)
expect(buttons[0].text()).toBe('apple')
expect(buttons[1].text()).toBe('banana')
expect(buttons[2].text()).toBe('cherry')
})
it('emits correct string value when clicked', async () => {
const options = ['first', 'second', 'third']
const wrapper = mountComponent('first', options)
await clickButton(wrapper, 'second')
const emitted = wrapper.emitted('update:modelValue')
expect(emitted).toBeDefined()
expect(emitted![0]).toEqual(['second'])
})
it('highlights selected string option', () => {
const options = ['option1', 'option2', 'option3']
const wrapper = mountComponent('option2', options)
const buttons = wrapper.findAll('button')
expect(buttons[1].classes()).toContain('bg-white')
expect(buttons[1].classes()).toContain('text-neutral-900')
expect(buttons[0].classes()).not.toContain('bg-white')
expect(buttons[2].classes()).not.toContain('bg-white')
})
})
describe('Number Options', () => {
it('handles number array options', () => {
const options = [1, 2, 3]
const wrapper = mountComponent('2', options)
const buttons = wrapper.findAll('button')
expect(buttons).toHaveLength(3)
expect(buttons[0].text()).toBe('1')
expect(buttons[1].text()).toBe('2')
expect(buttons[2].text()).toBe('3')
})
it('emits string representation of number when clicked', async () => {
const options = [10, 20, 30]
const wrapper = mountComponent('10', options)
await clickButton(wrapper, '20')
const emitted = wrapper.emitted('update:modelValue')
expect(emitted).toBeDefined()
expect(emitted![0]).toEqual(['20'])
})
it('highlights selected number option', () => {
const options = [100, 200, 300]
const wrapper = mountComponent('200', options)
const buttons = wrapper.findAll('button')
expect(buttons[1].classes()).toContain('bg-white')
expect(buttons[1].classes()).toContain('text-neutral-900')
})
})
describe('Object Options', () => {
it('handles object array with label and value', () => {
const options = [
{ label: 'First Option', value: 'first' },
{ label: 'Second Option', value: 'second' }
]
const wrapper = mountComponent('first', options)
const buttons = wrapper.findAll('button')
expect(buttons).toHaveLength(2)
expect(buttons[0].text()).toBe('First Option')
expect(buttons[1].text()).toBe('Second Option')
})
it('emits object value when object option clicked', async () => {
const options = [
{ label: 'Apple', value: 'apple_val' },
{ label: 'Banana', value: 'banana_val' }
]
const wrapper = mountComponent('apple_val', options)
await clickButton(wrapper, 'Banana')
const emitted = wrapper.emitted('update:modelValue')
expect(emitted).toBeDefined()
expect(emitted![0]).toEqual(['banana_val'])
})
it('highlights selected object option by value', () => {
const options = [
{ label: 'Small', value: 'sm' },
{ label: 'Medium', value: 'md' },
{ label: 'Large', value: 'lg' }
]
const wrapper = mountComponent('md', options)
const buttons = wrapper.findAll('button')
expect(buttons[1].classes()).toContain('bg-white') // Medium
expect(buttons[0].classes()).not.toContain('bg-white')
expect(buttons[2].classes()).not.toContain('bg-white')
})
it('handles objects without value field', () => {
const options = [
{ label: 'First', name: 'first_name' },
{ label: 'Second', name: 'second_name' }
]
const wrapper = mountComponent('first_name', options)
const buttons = wrapper.findAll('button')
expect(buttons[0].text()).toBe('First')
expect(buttons[1].text()).toBe('Second')
expect(buttons[0].classes()).toContain('bg-white')
})
it('handles objects without label field', () => {
const options = [
{ value: 'val1', name: 'Name 1' },
{ value: 'val2', name: 'Name 2' }
]
const wrapper = mountComponent('val1', options)
const buttons = wrapper.findAll('button')
expect(buttons[0].text()).toBe('Name 1')
expect(buttons[1].text()).toBe('Name 2')
})
})
describe('PrimeVue Compatibility', () => {
it('uses custom optionLabel prop', () => {
const options = [
{ title: 'First Item', value: 'first' },
{ title: 'Second Item', value: 'second' }
]
const wrapper = mountComponent('first', options, { optionLabel: 'title' })
const buttons = wrapper.findAll('button')
expect(buttons[0].text()).toBe('First Item')
expect(buttons[1].text()).toBe('Second Item')
})
it('uses custom optionValue prop', () => {
const options = [
{ label: 'First', id: 'first_id' },
{ label: 'Second', id: 'second_id' }
]
const wrapper = mountComponent('first_id', options, { optionValue: 'id' })
const buttons = wrapper.findAll('button')
expect(buttons[0].classes()).toContain('bg-white')
expect(buttons[1].classes()).not.toContain('bg-white')
})
it('emits custom optionValue when clicked', async () => {
const options = [
{ label: 'First', id: 'first_id' },
{ label: 'Second', id: 'second_id' }
]
const wrapper = mountComponent('first_id', options, { optionValue: 'id' })
await clickButton(wrapper, 'Second')
const emitted = wrapper.emitted('update:modelValue')
expect(emitted).toBeDefined()
expect(emitted![0]).toEqual(['second_id'])
})
})
describe('Disabled State', () => {
it('disables all buttons when disabled prop is true', () => {
const options = ['option1', 'option2']
const wrapper = mountComponent('option1', options, { disabled: true })
const buttons = wrapper.findAll('button')
buttons.forEach((button) => {
expect(button.element.disabled).toBe(true)
expect(button.classes()).toContain('opacity-50')
expect(button.classes()).toContain('cursor-not-allowed')
})
})
it('does not emit events when disabled', async () => {
const options = ['option1', 'option2']
const wrapper = mountComponent('option1', options, { disabled: true })
await clickButton(wrapper, 'option2')
const emitted = wrapper.emitted('update:modelValue')
expect(emitted).toBeUndefined()
})
it('does not apply hover styles when disabled', () => {
const options = ['option1', 'option2']
const wrapper = mountComponent('option1', options, { disabled: true })
const buttons = wrapper.findAll('button')
buttons.forEach((button) => {
expect(button.classes()).not.toContain('hover:bg-zinc-200/50')
expect(button.classes()).not.toContain('cursor-pointer')
})
})
it('applies disabled styling to selected option', () => {
const options = ['option1', 'option2']
const wrapper = mountComponent('option1', options, { disabled: true })
const buttons = wrapper.findAll('button')
expect(buttons[0].classes()).not.toContain('bg-white') // Selected styling disabled
expect(buttons[0].classes()).toContain('opacity-50')
expect(buttons[0].classes()).toContain('text-zinc-500')
})
})
describe('Selection Logic', () => {
it('handles null modelValue', () => {
const options = ['option1', 'option2']
const wrapper = mountComponent(null, options)
const buttons = wrapper.findAll('button')
buttons.forEach((button) => {
expect(button.classes()).not.toContain('bg-white')
})
})
it('handles undefined modelValue', () => {
const options = ['option1', 'option2']
const wrapper = mountComponent(undefined, options)
const buttons = wrapper.findAll('button')
buttons.forEach((button) => {
expect(button.classes()).not.toContain('bg-white')
})
})
it('handles empty string modelValue', () => {
const options = ['', 'option1', 'option2']
const wrapper = mountComponent('', options)
const buttons = wrapper.findAll('button')
expect(buttons[0].classes()).toContain('bg-white') // Empty string is selected
expect(buttons[1].classes()).not.toContain('bg-white')
})
it('compares values as strings', () => {
const options = [1, '2', 3]
const wrapper = mountComponent('1', options)
const buttons = wrapper.findAll('button')
expect(buttons[0].classes()).toContain('bg-white') // '1' matches number 1 as string
})
})
describe('Visual States', () => {
it('applies selected styling to active option', () => {
const options = ['option1', 'option2']
const wrapper = mountComponent('option1', options)
const selectedButton = wrapper.findAll('button')[0]
expect(selectedButton.classes()).toContain('bg-white')
expect(selectedButton.classes()).toContain('text-neutral-900')
})
it('applies unselected styling to inactive options', () => {
const options = ['option1', 'option2']
const wrapper = mountComponent('option1', options)
const unselectedButton = wrapper.findAll('button')[1]
expect(unselectedButton.classes()).toContain('bg-transparent')
expect(unselectedButton.classes()).toContain('text-zinc-500')
})
it('applies hover effects to enabled unselected buttons', () => {
const options = ['option1', 'option2']
const wrapper = mountComponent('option1', options, { disabled: false })
const unselectedButton = wrapper.findAll('button')[1]
expect(unselectedButton.classes()).toContain('hover:bg-zinc-200/50')
expect(unselectedButton.classes()).toContain('cursor-pointer')
})
})
describe('Edge Cases', () => {
it('handles very long option text', () => {
const longText =
'This is a very long option text that might cause layout issues'
const options = ['short', longText, 'normal']
const wrapper = mountComponent('short', options)
const buttons = wrapper.findAll('button')
expect(buttons[1].text()).toBe(longText)
expect(buttons).toHaveLength(3)
})
it('handles options with special characters', () => {
const specialOptions = ['@#$%^&*()', '{}[]|\\:";\'<>?,./']
const wrapper = mountComponent(specialOptions[0], specialOptions)
const buttons = wrapper.findAll('button')
expect(buttons[0].text()).toBe('@#$%^&*()')
expect(buttons[0].classes()).toContain('bg-white')
})
it('handles unicode characters in options', () => {
const unicodeOptions = ['🎨 Art', '中文', 'العربية']
const wrapper = mountComponent('🎨 Art', unicodeOptions)
const buttons = wrapper.findAll('button')
expect(buttons[0].text()).toBe('🎨 Art')
expect(buttons[0].classes()).toContain('bg-white')
})
it('handles duplicate option values', () => {
const duplicateOptions = ['duplicate', 'unique', 'duplicate']
const wrapper = mountComponent('duplicate', duplicateOptions)
const buttons = wrapper.findAll('button')
expect(buttons[0].classes()).toContain('bg-white')
expect(buttons[2].classes()).toContain('bg-white') // Both duplicates selected
expect(buttons[1].classes()).not.toContain('bg-white')
})
it('handles mixed type options safely', () => {
const mixedOptions: any[] = [
'string',
123,
{ label: 'Object', value: 'obj' },
null
]
const wrapper = mountComponent('123', mixedOptions)
const buttons = wrapper.findAll('button')
expect(buttons).toHaveLength(4)
expect(buttons[1].classes()).toContain('bg-white') // Number 123 as string
})
it('handles objects with missing properties gracefully', () => {
const incompleteOptions = [
{}, // Empty object
{ randomProp: 'value' }, // No standard props
{ value: 'has_value' }, // No label
{ label: 'has_label' } // No value
]
const wrapper = mountComponent('has_value', incompleteOptions)
const buttons = wrapper.findAll('button')
expect(buttons).toHaveLength(4)
expect(buttons[2].classes()).toContain('bg-white')
})
it('handles large number of options', () => {
const manyOptions = Array.from(
{ length: 50 },
(_, i) => `Option ${i + 1}`
)
const wrapper = mountComponent('Option 25', manyOptions)
const buttons = wrapper.findAll('button')
expect(buttons).toHaveLength(50)
expect(buttons[24].classes()).toContain('bg-white') // Option 25 at index 24
})
it('fallback to index when all object properties are missing', () => {
const problematicOptions = [
{ someRandomProp: 'random1' },
{ anotherRandomProp: 'random2' }
]
const wrapper = mountComponent('0', problematicOptions)
const buttons = wrapper.findAll('button')
expect(buttons).toHaveLength(2)
expect(buttons[0].classes()).toContain('bg-white') // Falls back to index 0
})
})
describe('Event Handling', () => {
it('prevents click events when disabled', async () => {
const options = ['option1', 'option2']
const wrapper = mountComponent('option1', options, { disabled: true })
const clickHandler = vi.fn()
wrapper.vm.$el.addEventListener('click', clickHandler)
await clickButton(wrapper, 'option2')
expect(clickHandler).not.toHaveBeenCalled()
})
it('allows repeated selection of same option', async () => {
const options = ['option1', 'option2']
const wrapper = mountComponent('option1', options)
await clickButton(wrapper, 'option1')
await clickButton(wrapper, 'option1')
const emitted = wrapper.emitted('update:modelValue')
expect(emitted).toHaveLength(2)
expect(emitted![0]).toEqual(['option1'])
expect(emitted![1]).toEqual(['option1'])
})
})
})

View File

@@ -46,7 +46,7 @@ import { WidgetInputBaseClass } from '../layout'
interface Props {
modelValue: string | null | undefined
options: T[] // Now using generic type instead of any[]
options: T[]
optionLabel?: string // PrimeVue compatible prop
optionValue?: string // PrimeVue compatible prop
disabled?: boolean

View File

@@ -7,7 +7,7 @@ import type {
ModelFolderInfo
} from '@/platform/assets/schemas/assetSchema'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { WorkflowTemplates } from '@/platform/workflow/templates/types/template'
import { type WorkflowTemplates } from '@/platform/workflow/templates/types/template'
import type {
ComfyApiWorkflow,
ComfyWorkflowJSON,

View File

@@ -19,7 +19,7 @@ import { useWorkflowService } from '@/platform/workflow/core/services/workflowSe
import { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
import { useWorkflowValidation } from '@/platform/workflow/validation/composables/useWorkflowValidation'
import {
ComfyApiWorkflow,
type ComfyApiWorkflow,
type ComfyWorkflowJSON,
type ModelFile,
type NodeId,
@@ -61,9 +61,9 @@ import { useWidgetStore } from '@/stores/widgetStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import type { ComfyExtension, MissingNodeType } from '@/types/comfy'
import { ExtensionManager } from '@/types/extensionTypes'
import { type ExtensionManager } from '@/types/extensionTypes'
import type { NodeExecutionId } from '@/types/nodeIdentification'
import { ColorAdjustOptions, adjustColor } from '@/utils/colorUtil'
import { type ColorAdjustOptions, adjustColor } from '@/utils/colorUtil'
import { graphToPrompt } from '@/utils/executionUtil'
import { forEachNode } from '@/utils/graphTraversalUtil'
import {
@@ -1121,6 +1121,13 @@ export class ComfyApp {
nodes: ComfyWorkflowJSON['nodes'],
path: string = ''
) => {
if (!Array.isArray(nodes)) {
console.warn(
'Workflow nodes data is missing or invalid, skipping node processing',
{ nodes, path }
)
return
}
for (let n of nodes) {
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
if (n.type == 'T2IAdapterLoader') n.type = 'ControlNetLoader'

View File

@@ -2,7 +2,7 @@ import {
type AvifIinfBox,
type AvifIlocBox,
type AvifInfeBox,
ComfyMetadata,
type ComfyMetadata,
ComfyMetadataTags,
type IsobmffBoxContentRange
} from '@/types/metadataTypes'

View File

@@ -3,12 +3,12 @@ import {
type ComfyWorkflowJSON
} from '@/platform/workflow/validation/schemas/workflowSchema'
import {
ComfyMetadata,
type ComfyMetadata,
ComfyMetadataTags,
EbmlElementRange,
EbmlTagPosition,
TextRange,
VInt
type EbmlElementRange,
type EbmlTagPosition,
type TextRange,
type VInt
} from '@/types/metadataTypes'
const WEBM_SIGNATURE = [0x1a, 0x45, 0xdf, 0xa3]

View File

@@ -1,14 +1,14 @@
import {
ComfyApiWorkflow,
ComfyWorkflowJSON
type ComfyApiWorkflow,
type ComfyWorkflowJSON
} from '@/platform/workflow/validation/schemas/workflowSchema'
import {
ASCII,
ComfyMetadata,
type ComfyMetadata,
ComfyMetadataTags,
GltfChunkHeader,
GltfHeader,
GltfJsonData,
type GltfChunkHeader,
type GltfHeader,
type GltfJsonData,
GltfSizeBytes
} from '@/types/metadataTypes'

View File

@@ -1,12 +1,12 @@
import {
ComfyApiWorkflow,
ComfyWorkflowJSON
type ComfyApiWorkflow,
type ComfyWorkflowJSON
} from '@/platform/workflow/validation/schemas/workflowSchema'
import {
ASCII,
ComfyMetadata,
type ComfyMetadata,
ComfyMetadataTags,
IsobmffBoxContentRange
type IsobmffBoxContentRange
} from '@/types/metadataTypes'
// Set max read high, as atoms are stored near end of file

View File

@@ -1,4 +1,4 @@
import { ComfyMetadata } from '@/types/metadataTypes'
import { type ComfyMetadata } from '@/types/metadataTypes'
export async function getSvgMetadata(file: File): Promise<ComfyMetadata> {
const text = await file.text()

View File

@@ -1,6 +1,6 @@
import { useSettingStore } from '@/platform/settings/settingStore'
import { WORKFLOW_ACCEPT_STRING } from '@/platform/workflow/core/types/formats'
import { type StatusWsMessageStatus, TaskItem } from '@/schemas/apiSchema'
import { type StatusWsMessageStatus, type TaskItem } from '@/schemas/apiSchema'
import { useDialogService } from '@/services/dialogService'
import { useLitegraphService } from '@/services/litegraphService'
import { useCommandStore } from '@/stores/commandStore'

View File

@@ -1,10 +1,10 @@
import { Settings } from '@/schemas/apiSchema'
import { type Settings } from '@/schemas/apiSchema'
import type { ComfyApp } from '@/scripts/app'
import type { ComfyComponent } from '.'
import { $el } from '../../ui'
import { prop } from '../../utils'
import { ClassList, applyClasses, toggleElement } from '../utils'
import { type ClassList, applyClasses, toggleElement } from '../utils'
import type { ComfyPopup } from './popup'
type ComfyButtonProps = {

View File

@@ -1,6 +1,6 @@
import { $el } from '../../ui'
import { prop } from '../../utils'
import { ClassList, applyClasses } from '../utils'
import { type ClassList, applyClasses } from '../utils'
export class ComfyPopup extends EventTarget {
element = $el('div.comfyui-popup')

View File

@@ -5,20 +5,12 @@ import ApiNodesSignInContent from '@/components/dialog/content/ApiNodesSignInCon
import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
import ErrorDialogContent from '@/components/dialog/content/ErrorDialogContent.vue'
import LoadWorkflowWarning from '@/components/dialog/content/LoadWorkflowWarning.vue'
import ManagerProgressDialogContent from '@/components/dialog/content/ManagerProgressDialogContent.vue'
import MissingModelsWarning from '@/components/dialog/content/MissingModelsWarning.vue'
import PromptDialogContent from '@/components/dialog/content/PromptDialogContent.vue'
import SignInContent from '@/components/dialog/content/SignInContent.vue'
import TopUpCreditsDialogContent from '@/components/dialog/content/TopUpCreditsDialogContent.vue'
import UpdatePasswordContent from '@/components/dialog/content/UpdatePasswordContent.vue'
import ManagerDialogContent from '@/components/dialog/content/manager/ManagerDialogContent.vue'
import ManagerHeader from '@/components/dialog/content/manager/ManagerHeader.vue'
import NodeConflictDialogContent from '@/components/dialog/content/manager/NodeConflictDialogContent.vue'
import NodeConflictFooter from '@/components/dialog/content/manager/NodeConflictFooter.vue'
import NodeConflictHeader from '@/components/dialog/content/manager/NodeConflictHeader.vue'
import ManagerProgressFooter from '@/components/dialog/footer/ManagerProgressFooter.vue'
import ComfyOrgHeader from '@/components/dialog/header/ComfyOrgHeader.vue'
import ManagerProgressHeader from '@/components/dialog/header/ManagerProgressHeader.vue'
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
import TemplateWorkflowsContent from '@/components/templates/TemplateWorkflowsContent.vue'
import TemplateWorkflowsDialogHeader from '@/components/templates/TemplateWorkflowsDialogHeader.vue'
@@ -31,6 +23,14 @@ import {
useDialogStore
} from '@/stores/dialogStore'
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
import ManagerProgressDialogContent from '@/workbench/extensions/manager/components/ManagerProgressDialogContent.vue'
import ManagerProgressFooter from '@/workbench/extensions/manager/components/ManagerProgressFooter.vue'
import ManagerProgressHeader from '@/workbench/extensions/manager/components/ManagerProgressHeader.vue'
import ManagerDialogContent from '@/workbench/extensions/manager/components/manager/ManagerDialogContent.vue'
import ManagerHeader from '@/workbench/extensions/manager/components/manager/ManagerHeader.vue'
import NodeConflictDialogContent from '@/workbench/extensions/manager/components/manager/NodeConflictDialogContent.vue'
import NodeConflictFooter from '@/workbench/extensions/manager/components/manager/NodeConflictFooter.vue'
import NodeConflictHeader from '@/workbench/extensions/manager/components/manager/NodeConflictHeader.vue'
export type ConfirmationDialogType =
| 'default'

View File

@@ -16,7 +16,6 @@ import type {
SearchAttribute,
SearchNodePacksParams
} from '@/types/algoliaTypes'
import { SortableAlgoliaField } from '@/types/comfyManagerTypes'
import type { components } from '@/types/comfyRegistryTypes'
import type {
NodePackSearchProvider,
@@ -24,6 +23,7 @@ import type {
SortableField
} from '@/types/searchServiceTypes'
import { paramsToCacheKey } from '@/utils/formatUtil'
import { SortableAlgoliaField } from '@/workbench/extensions/manager/types/comfyManagerTypes'
type RegistryNodePack = components['schemas']['Node']

View File

@@ -29,7 +29,7 @@ const defaultMockTaskLogs = [
{ taskName: 'Task 2', logs: ['Log 3', 'Log 4'] }
]
vi.mock('@/stores/comfyManagerStore', () => ({
vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({
useComfyManagerStore: vi.fn(() => ({
taskLogs: [...defaultMockTaskLogs],
succeededTasksLogs: [...defaultMockTaskLogs],

View File

@@ -88,7 +88,7 @@ import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import {
useComfyManagerStore,
useManagerProgressDialogStore
} from '@/stores/comfyManagerStore'
} from '@/workbench/extensions/manager/stores/comfyManagerStore'
const comfyManagerStore = useComfyManagerStore()
const progressDialogContent = useManagerProgressDialogStore()

View File

@@ -78,13 +78,13 @@ import { useConflictDetection } from '@/composables/useConflictDetection'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
import { api } from '@/scripts/api'
import { useComfyManagerService } from '@/services/comfyManagerService'
import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
import { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService'
import {
useComfyManagerStore,
useManagerProgressDialogStore
} from '@/stores/comfyManagerStore'
import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
} from '@/workbench/extensions/manager/stores/comfyManagerStore'
const { t } = useI18n()
const dialogStore = useDialogStore()

View File

@@ -24,7 +24,7 @@ import { useI18n } from 'vue-i18n'
import {
useComfyManagerStore,
useManagerProgressDialogStore
} from '@/stores/comfyManagerStore'
} from '@/workbench/extensions/manager/stores/comfyManagerStore'
const progressDialogContent = useManagerProgressDialogStore()
const comfyManagerStore = useComfyManagerStore()

View File

@@ -143,24 +143,24 @@ import IconButton from '@/components/button/IconButton.vue'
import ContentDivider from '@/components/common/ContentDivider.vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import VirtualGrid from '@/components/common/VirtualGrid.vue'
import ManagerNavSidebar from '@/components/dialog/content/manager/ManagerNavSidebar.vue'
import InfoPanel from '@/components/dialog/content/manager/infoPanel/InfoPanel.vue'
import InfoPanelMultiItem from '@/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue'
import PackCard from '@/components/dialog/content/manager/packCard/PackCard.vue'
import RegistrySearchBar from '@/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue'
import GridSkeleton from '@/components/dialog/content/manager/skeleton/GridSkeleton.vue'
import { useResponsiveCollapse } from '@/composables/element/useResponsiveCollapse'
import { useManagerStatePersistence } from '@/composables/manager/useManagerStatePersistence'
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus'
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
import { useRegistrySearch } from '@/composables/useRegistrySearch'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import type { TabItem } from '@/types/comfyManagerTypes'
import { ManagerTab } from '@/types/comfyManagerTypes'
import type { components } from '@/types/comfyRegistryTypes'
import ManagerNavSidebar from '@/workbench/extensions/manager/components/manager/ManagerNavSidebar.vue'
import InfoPanel from '@/workbench/extensions/manager/components/manager/infoPanel/InfoPanel.vue'
import InfoPanelMultiItem from '@/workbench/extensions/manager/components/manager/infoPanel/InfoPanelMultiItem.vue'
import PackCard from '@/workbench/extensions/manager/components/manager/packCard/PackCard.vue'
import RegistrySearchBar from '@/workbench/extensions/manager/components/manager/registrySearchBar/RegistrySearchBar.vue'
import GridSkeleton from '@/workbench/extensions/manager/components/manager/skeleton/GridSkeleton.vue'
import { useManagerStatePersistence } from '@/workbench/extensions/manager/composables/useManagerStatePersistence'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import type { TabItem } from '@/workbench/extensions/manager/types/comfyManagerTypes'
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
const { initialTab } = defineProps<{
initialTab?: ManagerTab

View File

@@ -0,0 +1,45 @@
import { mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import PrimeVue from 'primevue/config'
import { describe, expect, it } from 'vitest'
import { createI18n } from 'vue-i18n'
import enMessages from '@/locales/en/main.json' with { type: 'json' }
import ManagerHeader from './ManagerHeader.vue'
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: enMessages
}
})
describe('ManagerHeader', () => {
const createWrapper = () => {
return mount(ManagerHeader, {
global: {
plugins: [createPinia(), PrimeVue, i18n]
}
})
}
it('renders the component title', () => {
const wrapper = createWrapper()
expect(wrapper.find('h2').text()).toBe(
enMessages.manager.discoverCommunityContent
)
})
it('has proper structure with flex container', () => {
const wrapper = createWrapper()
const flexContainer = wrapper.find('.flex.items-center')
expect(flexContainer.exists()).toBe(true)
const title = flexContainer.find('h2')
expect(title.exists()).toBe(true)
})
})

View File

@@ -0,0 +1,11 @@
<template>
<div class="w-full">
<div class="flex items-center">
<h2 class="text-lg font-normal text-left">
{{ $t('manager.discoverCommunityContent') }}
</h2>
</div>
</div>
</template>
<script setup lang="ts"></script>

View File

@@ -32,7 +32,7 @@ import Listbox from 'primevue/listbox'
import ScrollPanel from 'primevue/scrollpanel'
import ContentDivider from '@/components/common/ContentDivider.vue'
import type { TabItem } from '@/types/comfyManagerTypes'
import type { TabItem } from '@/workbench/extensions/manager/types/comfyManagerTypes'
defineProps<{
tabs: TabItem[]

Some files were not shown because too many files have changed in this diff Show More