mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-24 06:35:10 +00:00
Compare commits
10 Commits
dom-widget
...
refactor/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82ca3031dd | ||
|
|
1b3522b250 | ||
|
|
256ad03210 | ||
|
|
3f27e7532d | ||
|
|
939323b563 | ||
|
|
08daa6f3ef | ||
|
|
5faf9e0105 | ||
|
|
282f9ce27a | ||
|
|
11eff4981f | ||
|
|
927773f553 |
27
.github/workflows/danger.yaml
vendored
27
.github/workflows/danger.yaml
vendored
@@ -1,27 +0,0 @@
|
||||
name: Danger PR Review
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, synchronize]
|
||||
|
||||
jobs:
|
||||
danger:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run Danger
|
||||
run: npx danger ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
154
.github/workflows/pr-checks.yml
vendored
Normal file
154
.github/workflows/pr-checks.yml
vendored
Normal file
@@ -0,0 +1,154 @@
|
||||
name: PR Checks
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_run: ${{ steps.check-changes.outputs.should_run }}
|
||||
has_browser_tests: ${{ steps.check-coverage.outputs.has_browser_tests }}
|
||||
has_screen_recording: ${{ steps.check-recording.outputs.has_recording }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Ensure base branch is available
|
||||
run: |
|
||||
# Fetch the specific base commit to ensure it's available for git diff
|
||||
git fetch origin ${{ github.event.pull_request.base.sha }}
|
||||
|
||||
- name: Check if significant changes exist
|
||||
id: check-changes
|
||||
run: |
|
||||
# Get list of changed files
|
||||
CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }})
|
||||
|
||||
# Filter for src/ files
|
||||
SRC_FILES=$(echo "$CHANGED_FILES" | grep '^src/' || true)
|
||||
|
||||
if [ -z "$SRC_FILES" ]; then
|
||||
echo "No src/ files changed"
|
||||
echo "should_run=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Count lines changed in src files
|
||||
TOTAL_LINES=0
|
||||
for file in $SRC_FILES; do
|
||||
if [ -f "$file" ]; then
|
||||
# Count added lines (non-empty)
|
||||
ADDED=$(git diff ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} -- "$file" | grep '^+' | grep -v '^+++' | grep -v '^+$' | wc -l)
|
||||
# Count removed lines (non-empty)
|
||||
REMOVED=$(git diff ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} -- "$file" | grep '^-' | grep -v '^---' | grep -v '^-$' | wc -l)
|
||||
TOTAL_LINES=$((TOTAL_LINES + ADDED + REMOVED))
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Total lines changed in src/: $TOTAL_LINES"
|
||||
|
||||
if [ $TOTAL_LINES -gt 3 ]; then
|
||||
echo "should_run=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "should_run=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Check browser test coverage
|
||||
id: check-coverage
|
||||
if: steps.check-changes.outputs.should_run == 'true'
|
||||
run: |
|
||||
# Check if browser tests were updated
|
||||
BROWSER_TEST_CHANGES=$(git diff --name-only ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} | grep '^browser_tests/.*\.ts$' || true)
|
||||
|
||||
if [ -n "$BROWSER_TEST_CHANGES" ]; then
|
||||
echo "has_browser_tests=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "has_browser_tests=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Check for screen recording
|
||||
id: check-recording
|
||||
if: steps.check-changes.outputs.should_run == 'true'
|
||||
run: |
|
||||
# Check PR body for screen recording
|
||||
PR_BODY="${{ github.event.pull_request.body }}"
|
||||
|
||||
# Check for GitHub user attachments or YouTube links
|
||||
if echo "$PR_BODY" | grep -qiE 'github\.com/user-attachments/assets/[a-f0-9-]+|youtube\.com/watch|youtu\.be/'; then
|
||||
echo "has_recording=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "has_recording=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Final check and create results
|
||||
id: final-check
|
||||
if: always()
|
||||
run: |
|
||||
# Initialize results
|
||||
WARNINGS_JSON=""
|
||||
|
||||
# Only run checks if should_run is true
|
||||
if [ "${{ steps.check-changes.outputs.should_run }}" == "true" ]; then
|
||||
# Check browser test coverage
|
||||
if [ "${{ steps.check-coverage.outputs.has_browser_tests }}" != "true" ]; then
|
||||
if [ -n "$WARNINGS_JSON" ]; then
|
||||
WARNINGS_JSON="${WARNINGS_JSON},"
|
||||
fi
|
||||
WARNINGS_JSON="${WARNINGS_JSON}{\"message\":\"⚠️ **Warning: E2E Test Coverage Missing**\\n\\nIf this PR modifies behavior that can be covered by browser-based E2E tests, those tests are required. PRs lacking applicable test coverage may not be reviewed until added. Please add or update browser tests to ensure code quality and prevent regressions.\"}"
|
||||
fi
|
||||
|
||||
# Check screen recording
|
||||
if [ "${{ steps.check-recording.outputs.has_recording }}" != "true" ]; then
|
||||
if [ -n "$WARNINGS_JSON" ]; then
|
||||
WARNINGS_JSON="${WARNINGS_JSON},"
|
||||
fi
|
||||
WARNINGS_JSON="${WARNINGS_JSON}{\"message\":\"⚠️ **Warning: Visual Documentation Missing**\\n\\nIf this PR changes user-facing behavior, visual proof (screen recording or screenshot) is required. PRs without applicable visual documentation may not be reviewed until provided.\\nYou can add it by:\\n\\n- GitHub: Drag & drop media directly into the PR description\\n\\n- YouTube: Include a link to a short demo\"}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create results JSON
|
||||
if [ -n "$WARNINGS_JSON" ]; then
|
||||
# Create JSON with warnings
|
||||
cat > pr-check-results.json << EOF
|
||||
{
|
||||
"fails": [],
|
||||
"warnings": [$WARNINGS_JSON],
|
||||
"messages": [],
|
||||
"markdowns": []
|
||||
}
|
||||
EOF
|
||||
echo "failed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# Create JSON with success
|
||||
cat > pr-check-results.json << 'EOF'
|
||||
{
|
||||
"fails": [],
|
||||
"warnings": [],
|
||||
"messages": [],
|
||||
"markdowns": []
|
||||
}
|
||||
EOF
|
||||
echo "failed=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Write PR metadata
|
||||
echo "${{ github.event.pull_request.number }}" > pr-number.txt
|
||||
echo "${{ github.event.pull_request.head.sha }}" > pr-sha.txt
|
||||
|
||||
- name: Upload results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: pr-check-results-${{ github.run_id }}
|
||||
path: |
|
||||
pr-check-results.json
|
||||
pr-number.txt
|
||||
pr-sha.txt
|
||||
retention-days: 1
|
||||
149
.github/workflows/pr-comment.yml
vendored
Normal file
149
.github/workflows/pr-comment.yml
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
name: PR Comment
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["PR Checks"]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
statuses: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
if: github.event.workflow_run.event == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: pr-check-results-${{ github.event.workflow_run.id }}
|
||||
path: /tmp/pr-artifacts
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
- name: Post results
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Helper function to safely read files
|
||||
function safeReadFile(filePath) {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) return null;
|
||||
return fs.readFileSync(filePath, 'utf8').trim();
|
||||
} catch (e) {
|
||||
console.error(`Error reading ${filePath}:`, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Read artifact files
|
||||
const artifactDir = '/tmp/pr-artifacts';
|
||||
const prNumber = safeReadFile(path.join(artifactDir, 'pr-number.txt'));
|
||||
const prSha = safeReadFile(path.join(artifactDir, 'pr-sha.txt'));
|
||||
const resultsJson = safeReadFile(path.join(artifactDir, 'pr-check-results.json'));
|
||||
|
||||
// Validate PR number
|
||||
if (!prNumber || isNaN(parseInt(prNumber))) {
|
||||
throw new Error('Invalid or missing PR number');
|
||||
}
|
||||
|
||||
// Parse and validate results
|
||||
let results;
|
||||
try {
|
||||
results = JSON.parse(resultsJson || '{}');
|
||||
} catch (e) {
|
||||
console.error('Failed to parse check results:', e);
|
||||
|
||||
// Post error comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: parseInt(prNumber),
|
||||
body: `⚠️ PR checks failed to complete properly. Error parsing results: ${e.message}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Format check messages
|
||||
const messages = [];
|
||||
|
||||
if (results.fails && results.fails.length > 0) {
|
||||
messages.push('### ❌ Failures\n' + results.fails.map(f => f.message).join('\n\n'));
|
||||
}
|
||||
|
||||
if (results.warnings && results.warnings.length > 0) {
|
||||
messages.push('### ⚠️ Warnings\n' + results.warnings.map(w => w.message).join('\n\n'));
|
||||
}
|
||||
|
||||
if (results.messages && results.messages.length > 0) {
|
||||
messages.push('### 💬 Messages\n' + results.messages.map(m => m.message).join('\n\n'));
|
||||
}
|
||||
|
||||
if (results.markdowns && results.markdowns.length > 0) {
|
||||
messages.push(...results.markdowns.map(m => m.message));
|
||||
}
|
||||
|
||||
// Find existing bot comment
|
||||
const comments = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: parseInt(prNumber)
|
||||
});
|
||||
|
||||
const botComment = comments.data.find(comment =>
|
||||
comment.user.type === 'Bot' &&
|
||||
comment.body.includes('<!-- pr-checks-comment -->')
|
||||
);
|
||||
|
||||
// Post comment if there are any messages
|
||||
if (messages.length > 0) {
|
||||
const body = messages.join('\n\n');
|
||||
const commentBody = `<!-- pr-checks-comment -->\n${body}`;
|
||||
|
||||
if (botComment) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: botComment.id,
|
||||
body: commentBody
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: parseInt(prNumber),
|
||||
body: commentBody
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// No messages - delete existing comment if present
|
||||
if (botComment) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: botComment.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Set commit status based on failures
|
||||
if (prSha) {
|
||||
const hasFailures = results.fails && results.fails.length > 0;
|
||||
const hasWarnings = results.warnings && results.warnings.length > 0;
|
||||
await github.rest.repos.createCommitStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
sha: prSha,
|
||||
state: hasFailures ? 'failure' : 'success',
|
||||
context: 'pr-checks',
|
||||
description: hasFailures
|
||||
? `${results.fails.length} check(s) failed`
|
||||
: hasWarnings
|
||||
? `${results.warnings.length} warning(s)`
|
||||
: 'All checks passed'
|
||||
});
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import { danger, fail } from 'danger'
|
||||
|
||||
// Check if we should run the checks
|
||||
const shouldRunChecks = async () => {
|
||||
const allChangedFiles = [
|
||||
...danger.git.modified_files,
|
||||
...danger.git.created_files
|
||||
]
|
||||
const srcChanges = allChangedFiles.filter((file) => file.startsWith('src/'))
|
||||
|
||||
if (srcChanges.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check total lines changed in src files
|
||||
let totalLinesChanged = 0
|
||||
for (const file of srcChanges) {
|
||||
const diff = await danger.git.diffForFile(file)
|
||||
if (diff) {
|
||||
// Count only lines with actual content (non-empty after trimming whitespace)
|
||||
// This excludes empty lines and lines containing only spaces/tabs
|
||||
const additions =
|
||||
diff.added?.split('\n').filter((line) => line.trim()).length || 0
|
||||
const deletions =
|
||||
diff.removed?.split('\n').filter((line) => line.trim()).length || 0
|
||||
totalLinesChanged += additions + deletions
|
||||
}
|
||||
}
|
||||
|
||||
return totalLinesChanged > 3
|
||||
}
|
||||
|
||||
// Check if browser tests were updated
|
||||
const checkBrowserTestCoverage = () => {
|
||||
const allChangedFiles = [
|
||||
...danger.git.modified_files,
|
||||
...danger.git.created_files
|
||||
]
|
||||
const hasBrowserTestChanges = allChangedFiles.some(
|
||||
(file) => file.startsWith('browser_tests/') && file.endsWith('.ts')
|
||||
)
|
||||
|
||||
if (!hasBrowserTestChanges) {
|
||||
fail(`🧪 **E2E Test Coverage Missing**
|
||||
|
||||
All changes should be covered under E2E testing. Please add or update browser tests.`)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for screen recording in PR description
|
||||
const checkScreenRecording = () => {
|
||||
const description = danger.github.pr.body || ''
|
||||
const hasRecording =
|
||||
/github\.com\/user-attachments\/assets\/[a-f0-9-]+/i.test(description) ||
|
||||
/youtube\.com\/watch|youtu\.be\//i.test(description)
|
||||
|
||||
if (!hasRecording) {
|
||||
fail(`📹 **Visual Documentation Missing**
|
||||
|
||||
Please add a screen recording or screenshot:
|
||||
- GitHub: Drag & drop media to PR description
|
||||
- YouTube: Add YouTube link`)
|
||||
}
|
||||
}
|
||||
|
||||
// Run the checks only if conditions are met
|
||||
shouldRunChecks().then((shouldRun) => {
|
||||
if (shouldRun) {
|
||||
checkBrowserTestCoverage()
|
||||
checkScreenRecording()
|
||||
}
|
||||
})
|
||||
806
package-lock.json
generated
806
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -45,7 +45,6 @@
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"chalk": "^5.3.0",
|
||||
"danger": "^13.0.4",
|
||||
"eslint": "^9.12.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
|
||||
@@ -38,23 +38,14 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import FileDownload from '@/components/common/FileDownload.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import { MODEL_SOURCES } from '@/constants/urls'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
|
||||
// TODO: Read this from server internal API rather than hardcoding here
|
||||
// as some installations may wish to use custom sources
|
||||
const allowedSources = [
|
||||
'https://civitai.com/',
|
||||
'https://huggingface.co/',
|
||||
'http://localhost:' // Included for testing usage only
|
||||
]
|
||||
/** @todo Read this from server internal API rather than hardcoding here */
|
||||
const allowedSources = MODEL_SOURCES.allowedDomains
|
||||
const allowedSuffixes = ['.safetensors', '.sft']
|
||||
// Models that fail above conditions but are still allowed
|
||||
const whiteListedUrls = new Set([
|
||||
'https://huggingface.co/stabilityai/stable-zero123/resolve/main/stable_zero123.ckpt',
|
||||
'https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_depth_sd14v1.pth?download=true',
|
||||
'https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth'
|
||||
])
|
||||
const whiteListedUrls = new Set(MODEL_SOURCES.whitelistedUrls)
|
||||
|
||||
interface ModelInfo {
|
||||
name: string
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
@@ -20,37 +21,24 @@ import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
const domWidgetStore = useDomWidgetStore()
|
||||
|
||||
const widgetStates = computed(() => {
|
||||
return [
|
||||
...domWidgetStore.activeWidgetStates,
|
||||
...domWidgetStore.inactiveWidgetStates
|
||||
]
|
||||
})
|
||||
const widgetStates = computed(() => domWidgetStore.activeWidgetStates)
|
||||
|
||||
const updateWidgets = () => {
|
||||
const lgCanvas = canvasStore.canvas
|
||||
if (!lgCanvas) return
|
||||
|
||||
const lowQuality = lgCanvas.low_quality
|
||||
const currentGraph = lgCanvas.graph
|
||||
|
||||
for (const widgetState of widgetStates.value) {
|
||||
const widget = widgetState.widget
|
||||
|
||||
// Use containerNode for promoted widgets, otherwise use widget.node
|
||||
const node = widget.containerNode || widget.node
|
||||
const node = widget.node as LGraphNode
|
||||
|
||||
const visible =
|
||||
node &&
|
||||
currentGraph?.nodes.includes(node) &&
|
||||
lgCanvas.isNodeVisible(node) &&
|
||||
!(widget.options.hideOnZoom && lowQuality) &&
|
||||
widget.isVisible()
|
||||
|
||||
widgetState.visible = visible ?? false
|
||||
|
||||
if (widgetState.visible && node) {
|
||||
widgetState.visible = visible
|
||||
if (visible) {
|
||||
const margin = widget.margin
|
||||
widgetState.pos = [node.pos[0] + margin, node.pos[1] + margin + widget.y]
|
||||
widgetState.size = [
|
||||
|
||||
@@ -72,6 +72,7 @@ import { useWorkflowAutoSave } from '@/composables/useWorkflowAutoSave'
|
||||
import { useWorkflowPersistence } from '@/composables/useWorkflowPersistence'
|
||||
import { CORE_SETTINGS } from '@/constants/coreSettings'
|
||||
import { i18n, t } from '@/i18n'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import { UnauthorizedError, api } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||
@@ -191,26 +192,22 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// Update the progress of executing nodes
|
||||
// Update the progress of the executing node
|
||||
watch(
|
||||
() =>
|
||||
[executionStore.nodeLocationProgressStates, canvasStore.canvas] as const,
|
||||
([nodeLocationProgressStates, canvas]) => {
|
||||
if (!canvas?.graph) return
|
||||
for (const node of canvas.graph.nodes) {
|
||||
const nodeLocatorId = useWorkflowStore().nodeIdToNodeLocatorId(node.id)
|
||||
const progressState = nodeLocationProgressStates[nodeLocatorId]
|
||||
if (progressState && progressState.state === 'running') {
|
||||
node.progress = progressState.value / progressState.max
|
||||
[
|
||||
executionStore.executingNodeId,
|
||||
executionStore.executingNodeProgress
|
||||
] satisfies [NodeId | null, number | null],
|
||||
([executingNodeId, executingNodeProgress]) => {
|
||||
for (const node of comfyApp.graph.nodes) {
|
||||
if (node.id == executingNodeId) {
|
||||
node.progress = executingNodeProgress ?? undefined
|
||||
} else {
|
||||
node.progress = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// Force canvas redraw to ensure progress updates are visible
|
||||
canvas.graph.setDirtyCanvas(true, false)
|
||||
},
|
||||
{ deep: true }
|
||||
}
|
||||
)
|
||||
|
||||
// Update node slot errors
|
||||
|
||||
@@ -61,15 +61,10 @@ const updateDomClipping = () => {
|
||||
if (!lgCanvas || !widgetElement.value) return
|
||||
|
||||
const selectedNode = Object.values(lgCanvas.selected_nodes ?? {})[0]
|
||||
if (!selectedNode) {
|
||||
// Clear clipping when no node is selected
|
||||
updateClipPath(widgetElement.value, lgCanvas.canvas, false, undefined)
|
||||
return
|
||||
}
|
||||
if (!selectedNode) return
|
||||
|
||||
// Use containerNode for promoted widgets, otherwise use widget.node
|
||||
const positioningNode = widget.containerNode || widget.node
|
||||
const isSelected = selectedNode === positioningNode
|
||||
const node = widget.node
|
||||
const isSelected = selectedNode === node
|
||||
const renderArea = selectedNode?.renderArea
|
||||
const offset = lgCanvas.ds.offset
|
||||
const scale = lgCanvas.ds.scale
|
||||
@@ -148,28 +143,11 @@ if (isDOMWidget(widget)) {
|
||||
const inputSpec = widget.node.constructor.nodeData
|
||||
const tooltip = inputSpec?.inputs?.[widget.name]?.tooltip
|
||||
|
||||
// Mount DOM element when widget is or becomes visible
|
||||
const mountElementIfVisible = () => {
|
||||
if (widgetState.visible && isDOMWidget(widget) && widgetElement.value) {
|
||||
// Only append if not already a child
|
||||
if (!widgetElement.value.contains(widget.element)) {
|
||||
widgetElement.value.appendChild(widget.element)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check on mount
|
||||
onMounted(() => {
|
||||
mountElementIfVisible()
|
||||
})
|
||||
|
||||
// And watch for visibility changes
|
||||
watch(
|
||||
() => widgetState.visible,
|
||||
() => {
|
||||
mountElementIfVisible()
|
||||
if (isDOMWidget(widget) && widgetElement.value) {
|
||||
widgetElement.value.appendChild(widget.element)
|
||||
}
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -20,44 +20,33 @@ import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
|
||||
|
||||
const modelValue = defineModel<string>({ required: true })
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
widget?: object
|
||||
nodeId: NodeId
|
||||
}>()
|
||||
|
||||
const executionStore = useExecutionStore()
|
||||
const isParentNodeExecuting = ref(true)
|
||||
const formattedText = computed(() => nl2br(linkifyHtml(modelValue.value)))
|
||||
|
||||
let parentNodeId: NodeId | null = null
|
||||
let executingNodeId: NodeId | null = null
|
||||
onMounted(() => {
|
||||
// Get the parent node ID from props if provided
|
||||
// For backward compatibility, fall back to the first executing node
|
||||
parentNodeId = props.nodeId
|
||||
executingNodeId = executionStore.executingNodeId
|
||||
})
|
||||
|
||||
// Watch for either a new node has starting execution or overall execution ending
|
||||
const stopWatching = watch(
|
||||
[() => executionStore.executingNodeIds, () => executionStore.isIdle],
|
||||
[() => executionStore.executingNode, () => executionStore.isIdle],
|
||||
() => {
|
||||
if (executionStore.isIdle) {
|
||||
isParentNodeExecuting.value = false
|
||||
stopWatching()
|
||||
return
|
||||
}
|
||||
|
||||
// Check if parent node is no longer in the executing nodes list
|
||||
if (
|
||||
parentNodeId &&
|
||||
!executionStore.executingNodeIds.includes(parentNodeId)
|
||||
executionStore.isIdle ||
|
||||
(executionStore.executingNode &&
|
||||
executionStore.executingNode.id !== executingNodeId)
|
||||
) {
|
||||
isParentNodeExecuting.value = false
|
||||
stopWatching()
|
||||
}
|
||||
|
||||
// Set parent node ID if not set yet
|
||||
if (!parentNodeId && executionStore.executingNodeIds.length > 0) {
|
||||
parentNodeId = executionStore.executingNodeIds[0]
|
||||
if (!executingNodeId) {
|
||||
executingNodeId = executionStore.executingNodeId
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -123,6 +123,7 @@ import Button from 'primevue/button'
|
||||
import { type CSSProperties, computed, nextTick, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { COMFY_URLS, GITHUB_REPOS, getDesktopGuideUrl } from '@/constants/urls'
|
||||
import { type ReleaseNote } from '@/services/releaseService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useReleaseStore } from '@/stores/releaseStore'
|
||||
@@ -143,11 +144,10 @@ interface MenuItem {
|
||||
|
||||
// Constants
|
||||
const EXTERNAL_LINKS = {
|
||||
DOCS: 'https://docs.comfy.org/',
|
||||
DISCORD: 'https://www.comfy.org/discord',
|
||||
GITHUB: 'https://github.com/comfyanonymous/ComfyUI',
|
||||
DESKTOP_GUIDE: 'https://comfyorg.notion.site/',
|
||||
UPDATE_GUIDE: 'https://docs.comfy.org/installation/update_comfyui'
|
||||
DOCS: COMFY_URLS.docs.base,
|
||||
DISCORD: COMFY_URLS.community.discord,
|
||||
GITHUB: GITHUB_REPOS.comfyui,
|
||||
UPDATE_GUIDE: COMFY_URLS.docs.installation.update
|
||||
} as const
|
||||
|
||||
const TIME_UNITS = {
|
||||
@@ -199,7 +199,7 @@ const menuItems = computed<MenuItem[]>(() => {
|
||||
type: 'item',
|
||||
label: t('helpCenter.desktopUserGuide'),
|
||||
action: () => {
|
||||
openExternalLink(EXTERNAL_LINKS.DESKTOP_GUIDE)
|
||||
openExternalLink(getDesktopGuideUrl(locale.value))
|
||||
emit('close')
|
||||
}
|
||||
},
|
||||
@@ -451,13 +451,13 @@ const onUpdate = (_: ReleaseNote): void => {
|
||||
const getChangelogUrl = (): string => {
|
||||
const isChineseLocale = locale.value === 'zh'
|
||||
return isChineseLocale
|
||||
? 'https://docs.comfy.org/zh-CN/changelog'
|
||||
: 'https://docs.comfy.org/changelog'
|
||||
? COMFY_URLS.docs.changelog.zh
|
||||
: COMFY_URLS.docs.changelog.en
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
if (!hasReleases.value) {
|
||||
if (showVersionUpdates.value && !hasReleases.value) {
|
||||
await releaseStore.fetchReleases()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { COMFY_URLS } from '@/constants/urls'
|
||||
import type { ReleaseNote } from '@/services/releaseService'
|
||||
import { useReleaseStore } from '@/stores/releaseStore'
|
||||
import { formatVersionAnchor } from '@/utils/formatUtil'
|
||||
@@ -72,8 +73,8 @@ const shouldShow = computed(
|
||||
const changelogUrl = computed(() => {
|
||||
const isChineseLocale = locale.value === 'zh'
|
||||
const baseUrl = isChineseLocale
|
||||
? 'https://docs.comfy.org/zh-CN/changelog'
|
||||
: 'https://docs.comfy.org/changelog'
|
||||
? COMFY_URLS.docs.changelog.zh
|
||||
: COMFY_URLS.docs.changelog.en
|
||||
|
||||
if (latestRelease.value?.version) {
|
||||
const versionAnchor = formatVersionAnchor(latestRelease.value.version)
|
||||
@@ -120,7 +121,7 @@ const handleLearnMore = () => {
|
||||
}
|
||||
|
||||
const handleUpdate = () => {
|
||||
window.open('https://docs.comfy.org/installation/update_comfyui', '_blank')
|
||||
window.open(COMFY_URLS.docs.installation.update, '_blank')
|
||||
dismissToast()
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ import { marked } from 'marked'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { COMFY_URLS } from '@/constants/urls'
|
||||
import type { ReleaseNote } from '@/services/releaseService'
|
||||
import { useReleaseStore } from '@/stores/releaseStore'
|
||||
import { formatVersionAnchor } from '@/utils/formatUtil'
|
||||
@@ -92,8 +93,8 @@ const shouldShow = computed(
|
||||
const changelogUrl = computed(() => {
|
||||
const isChineseLocale = locale.value === 'zh'
|
||||
const baseUrl = isChineseLocale
|
||||
? 'https://docs.comfy.org/zh-CN/changelog'
|
||||
: 'https://docs.comfy.org/changelog'
|
||||
? COMFY_URLS.docs.changelog.zh
|
||||
: COMFY_URLS.docs.changelog.en
|
||||
|
||||
if (latestRelease.value?.version) {
|
||||
const versionAnchor = formatVersionAnchor(latestRelease.value.version)
|
||||
@@ -217,7 +218,8 @@ defineExpose({
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
overflow: hidden;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
padding: 32px 32px 24px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ import Panel from 'primevue/panel'
|
||||
import { ModelRef, computed, onMounted, ref } from 'vue'
|
||||
|
||||
import MirrorItem from '@/components/install/mirror/MirrorItem.vue'
|
||||
import { PYPI_MIRROR, PYTHON_MIRROR, UVMirror } from '@/constants/uvMirrors'
|
||||
import { PYPI_MIRROR, PYTHON_MIRROR, UVMirror } from '@/constants/mirrors'
|
||||
import { t } from '@/i18n'
|
||||
import { isInChina } from '@/utils/networkUtil'
|
||||
import { ValidationState, mergeValidationStates } from '@/utils/validationUtil'
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import UrlInput from '@/components/common/UrlInput.vue'
|
||||
import { UVMirror } from '@/constants/uvMirrors'
|
||||
import { UVMirror } from '@/constants/mirrors'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { checkMirrorReachable } from '@/utils/networkUtil'
|
||||
import { ValidationState } from '@/utils/validationUtil'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useTitle } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
@@ -37,34 +36,11 @@ export const useBrowserTabTitle = () => {
|
||||
: DEFAULT_TITLE
|
||||
})
|
||||
|
||||
const nodeExecutionTitle = computed(() => {
|
||||
// Check if any nodes are in progress
|
||||
const nodeProgressEntries = Object.entries(
|
||||
executionStore.nodeProgressStates
|
||||
)
|
||||
const runningNodes = nodeProgressEntries.filter(
|
||||
([_, state]) => state.state === 'running'
|
||||
)
|
||||
|
||||
if (runningNodes.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// If multiple nodes are running
|
||||
if (runningNodes.length > 1) {
|
||||
return `${executionText.value}[${runningNodes.length} ${t('g.nodesRunning', 'nodes running')}]`
|
||||
}
|
||||
|
||||
// If only one node is running
|
||||
const [nodeId, state] = runningNodes[0]
|
||||
const progress = Math.round((state.value / state.max) * 100)
|
||||
const nodeType =
|
||||
executionStore.activePrompt?.workflow?.changeTracker?.activeState?.nodes.find(
|
||||
(n) => String(n.id) === nodeId
|
||||
)?.type || 'Node'
|
||||
|
||||
return `${executionText.value}[${progress}%] ${nodeType}`
|
||||
})
|
||||
const nodeExecutionTitle = computed(() =>
|
||||
executionStore.executingNode && executionStore.executingNodeProgress
|
||||
? `${executionText.value}[${Math.round(executionStore.executingNodeProgress * 100)}%] ${executionStore.executingNode.type}`
|
||||
: ''
|
||||
)
|
||||
|
||||
const workflowTitle = computed(
|
||||
() =>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
DEFAULT_DARK_COLOR_PALETTE,
|
||||
DEFAULT_LIGHT_COLOR_PALETTE
|
||||
} from '@/constants/coreColorPalettes'
|
||||
import { COMFY_URLS, GITHUB_REPOS } from '@/constants/urls'
|
||||
import { t } from '@/i18n'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
@@ -543,10 +544,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
menubarLabel: 'ComfyUI Issues',
|
||||
versionAdded: '1.5.5',
|
||||
function: () => {
|
||||
window.open(
|
||||
'https://github.com/comfyanonymous/ComfyUI/issues',
|
||||
'_blank'
|
||||
)
|
||||
window.open(GITHUB_REPOS.comfyuiIssues, '_blank')
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -556,7 +554,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
menubarLabel: 'ComfyUI Docs',
|
||||
versionAdded: '1.5.5',
|
||||
function: () => {
|
||||
window.open('https://docs.comfy.org/', '_blank')
|
||||
window.open(COMFY_URLS.docs.base, '_blank')
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -566,7 +564,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
menubarLabel: 'Comfy-Org Discord',
|
||||
versionAdded: '1.5.5',
|
||||
function: () => {
|
||||
window.open('https://www.comfy.org/discord', '_blank')
|
||||
window.open(COMFY_URLS.community.discord, '_blank')
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -646,7 +644,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
menubarLabel: 'ComfyUI Forum',
|
||||
versionAdded: '1.8.2',
|
||||
function: () => {
|
||||
window.open('https://forum.comfy.org/', '_blank')
|
||||
window.open(COMFY_URLS.community.forum.base, '_blank')
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -23,9 +23,6 @@ export const useTextPreviewWidget = (
|
||||
name: inputSpec.name,
|
||||
component: TextPreviewWidget,
|
||||
inputSpec,
|
||||
componentProps: {
|
||||
nodeId: node.id
|
||||
},
|
||||
options: {
|
||||
getValue: () => widgetValue.value,
|
||||
setValue: (value: string | object) => {
|
||||
|
||||
14
src/config/comfyDomain.ts
Normal file
14
src/config/comfyDomain.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Base domain configuration and core website URLs
|
||||
* Forkers can change the base domain to use their own
|
||||
*/
|
||||
export const COMFY_BASE_DOMAIN =
|
||||
import.meta.env.VITE_COMFY_BASE_DOMAIN || 'comfy.org'
|
||||
|
||||
const WEBSITE_BASE_URL = `https://www.${COMFY_BASE_DOMAIN}`
|
||||
|
||||
export const COMFY_WEBSITE_URLS = {
|
||||
base: WEBSITE_BASE_URL,
|
||||
termsOfService: `${WEBSITE_BASE_URL}/terms-of-service`,
|
||||
privacy: `${WEBSITE_BASE_URL}/privacy`
|
||||
}
|
||||
95
src/constants/urls.ts
Normal file
95
src/constants/urls.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* URL constants for ComfyUI frontend
|
||||
* Centralized location for all URL references
|
||||
*/
|
||||
import { COMFY_BASE_DOMAIN } from '@/config/comfyDomain'
|
||||
|
||||
const DOCS_BASE_URL = `https://docs.${COMFY_BASE_DOMAIN}`
|
||||
const FORUM_BASE_URL = `https://forum.${COMFY_BASE_DOMAIN}`
|
||||
const WEBSITE_BASE_URL = `https://www.${COMFY_BASE_DOMAIN}`
|
||||
|
||||
export const COMFY_URLS = {
|
||||
website: {
|
||||
base: WEBSITE_BASE_URL,
|
||||
termsOfService: `${WEBSITE_BASE_URL}/terms-of-service`,
|
||||
privacy: `${WEBSITE_BASE_URL}/privacy`
|
||||
},
|
||||
docs: {
|
||||
base: DOCS_BASE_URL,
|
||||
changelog: {
|
||||
en: `${DOCS_BASE_URL}/changelog`,
|
||||
zh: `${DOCS_BASE_URL}/zh-CN/changelog`
|
||||
},
|
||||
installation: {
|
||||
update: `${DOCS_BASE_URL}/installation/update_comfyui`
|
||||
},
|
||||
tutorials: {
|
||||
apiNodes: {
|
||||
overview: `${DOCS_BASE_URL}/tutorials/api-nodes/overview`,
|
||||
faq: `${DOCS_BASE_URL}/tutorials/api-nodes/faq`,
|
||||
pricing: `${DOCS_BASE_URL}/tutorials/api-nodes/pricing`,
|
||||
apiKeyLogin: `${DOCS_BASE_URL}/interface/user#logging-in-with-an-api-key`,
|
||||
nonWhitelistedLogin: `${DOCS_BASE_URL}/tutorials/api-nodes/overview#log-in-with-api-key-on-non-whitelisted-websites`
|
||||
}
|
||||
},
|
||||
getLocalized: (path: string, locale: string) => {
|
||||
return locale === 'zh' || locale === 'zh-CN'
|
||||
? `${DOCS_BASE_URL}/zh-CN/${path}`
|
||||
: `${DOCS_BASE_URL}/${path}`
|
||||
}
|
||||
},
|
||||
community: {
|
||||
discord: `${WEBSITE_BASE_URL}/discord`,
|
||||
forum: {
|
||||
base: `${FORUM_BASE_URL}/`,
|
||||
v1Feedback: `${FORUM_BASE_URL}/c/v1-feedback/`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const GITHUB_REPOS = {
|
||||
comfyui: 'https://github.com/comfyanonymous/ComfyUI',
|
||||
comfyuiIssues: 'https://github.com/comfyanonymous/ComfyUI/issues',
|
||||
frontend: 'https://github.com/Comfy-Org/ComfyUI_frontend',
|
||||
electron: 'https://github.com/Comfy-Org/electron',
|
||||
desktopPlatforms:
|
||||
'https://github.com/Comfy-Org/desktop#currently-supported-platforms'
|
||||
}
|
||||
|
||||
export const MODEL_SOURCES = {
|
||||
repos: {
|
||||
civitai: 'https://civitai.com/',
|
||||
huggingface: 'https://huggingface.co/'
|
||||
},
|
||||
whitelistedUrls: [
|
||||
'https://huggingface.co/stabilityai/stable-zero123/resolve/main/stable_zero123.ckpt',
|
||||
'https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_depth_sd14v1.pth?download=true',
|
||||
'https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth'
|
||||
],
|
||||
allowedDomains: [
|
||||
'https://civitai.com/',
|
||||
'https://huggingface.co/',
|
||||
'http://localhost:' // TODO: Remove in production
|
||||
]
|
||||
}
|
||||
|
||||
export const DEVELOPER_TOOLS = {
|
||||
git: 'https://git-scm.com/downloads/',
|
||||
vcRedist: 'https://aka.ms/vs/17/release/vc_redist.x64.exe',
|
||||
uv: 'https://docs.astral.sh/uv/getting-started/installation/'
|
||||
}
|
||||
|
||||
// Platform and locale-aware desktop guide URL generator
|
||||
export const getDesktopGuideUrl = (locale: string): string => {
|
||||
const isChineseLocale = locale === 'zh'
|
||||
const isMacOS =
|
||||
typeof navigator !== 'undefined' &&
|
||||
navigator.platform.toUpperCase().indexOf('MAC') >= 0
|
||||
|
||||
const platform = isMacOS ? 'macos' : 'windows'
|
||||
const baseUrl = isChineseLocale
|
||||
? `https://docs.${COMFY_BASE_DOMAIN}/zh-CN/installation/desktop`
|
||||
: `https://docs.${COMFY_BASE_DOMAIN}/installation/desktop`
|
||||
|
||||
return `${baseUrl}/${platform}`
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import log from 'loglevel'
|
||||
|
||||
import { PYTHON_MIRROR } from '@/constants/uvMirrors'
|
||||
import { t } from '@/i18n'
|
||||
import { PYTHON_MIRROR } from '@/constants/mirrors'
|
||||
import { getDesktopGuideUrl } from '@/constants/urls'
|
||||
import { i18n, t } from '@/i18n'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
@@ -159,7 +160,7 @@ import { checkMirrorReachable } from '@/utils/networkUtil'
|
||||
label: 'Desktop User Guide',
|
||||
icon: 'pi pi-book',
|
||||
function() {
|
||||
window.open('https://comfyorg.notion.site/', '_blank')
|
||||
window.open(getDesktopGuideUrl(i18n.global.locale.value), '_blank')
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
} from '@/schemas/comfyWorkflowSchema'
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { useWidgetStore } from '@/stores/widgetStore'
|
||||
@@ -1225,10 +1224,9 @@ export class GroupNodeHandler {
|
||||
node.onDrawForeground = function (ctx) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
onDrawForeground?.apply?.(this, arguments)
|
||||
const progressState = useExecutionStore().nodeProgressStates[this.id]
|
||||
if (
|
||||
progressState &&
|
||||
progressState.state === 'running' &&
|
||||
// @ts-expect-error fixme ts strict error
|
||||
+app.runningNodeId === this.id &&
|
||||
this.runningInternalNodeId !== null
|
||||
) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
@@ -1342,7 +1340,6 @@ export class GroupNodeHandler {
|
||||
this.node.onRemoved = function () {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
onRemoved?.apply(this, arguments)
|
||||
// api.removeEventListener('progress_state', progress_state)
|
||||
api.removeEventListener('executing', executing)
|
||||
api.removeEventListener('executed', executed)
|
||||
}
|
||||
|
||||
@@ -133,8 +133,7 @@
|
||||
"copyURL": "Copy URL",
|
||||
"releaseTitle": "{package} {version} Release",
|
||||
"progressCountOf": "of",
|
||||
"keybindingAlreadyExists": "Keybinding already exists on",
|
||||
"nodesRunning": "nodes running"
|
||||
"keybindingAlreadyExists": "Keybinding already exists on"
|
||||
},
|
||||
"manager": {
|
||||
"title": "Custom Nodes Manager",
|
||||
|
||||
@@ -336,7 +336,6 @@
|
||||
"noTasksFoundMessage": "No hay tareas en la cola.",
|
||||
"noWorkflowsFound": "No se encontraron flujos de trabajo.",
|
||||
"nodes": "Nodos",
|
||||
"nodesRunning": "nodos en ejecución",
|
||||
"ok": "OK",
|
||||
"openNewIssue": "Abrir nuevo problema",
|
||||
"overwrite": "Sobrescribir",
|
||||
|
||||
@@ -336,7 +336,6 @@
|
||||
"noTasksFoundMessage": "Il n'y a pas de tâches dans la file d'attente.",
|
||||
"noWorkflowsFound": "Aucun flux de travail trouvé.",
|
||||
"nodes": "Nœuds",
|
||||
"nodesRunning": "nœuds en cours d’exécution",
|
||||
"ok": "OK",
|
||||
"openNewIssue": "Ouvrir un nouveau problème",
|
||||
"overwrite": "Écraser",
|
||||
|
||||
@@ -336,7 +336,6 @@
|
||||
"noTasksFoundMessage": "キューにタスクがありません。",
|
||||
"noWorkflowsFound": "ワークフローが見つかりません。",
|
||||
"nodes": "ノード",
|
||||
"nodesRunning": "ノードが実行中",
|
||||
"ok": "OK",
|
||||
"openNewIssue": "新しい問題を開く",
|
||||
"overwrite": "上書き",
|
||||
|
||||
@@ -336,7 +336,6 @@
|
||||
"noTasksFoundMessage": "대기열에 작업이 없습니다.",
|
||||
"noWorkflowsFound": "워크플로를 찾을 수 없습니다.",
|
||||
"nodes": "노드",
|
||||
"nodesRunning": "노드 실행 중",
|
||||
"ok": "확인",
|
||||
"openNewIssue": "새 문제 열기",
|
||||
"overwrite": "덮어쓰기",
|
||||
|
||||
@@ -336,7 +336,6 @@
|
||||
"noTasksFoundMessage": "В очереди нет задач.",
|
||||
"noWorkflowsFound": "Рабочие процессы не найдены.",
|
||||
"nodes": "Узлы",
|
||||
"nodesRunning": "запущено узлов",
|
||||
"ok": "ОК",
|
||||
"openNewIssue": "Открыть новую проблему",
|
||||
"overwrite": "Перезаписать",
|
||||
|
||||
@@ -336,7 +336,6 @@
|
||||
"noTasksFoundMessage": "佇列中沒有任務。",
|
||||
"noWorkflowsFound": "找不到工作流程。",
|
||||
"nodes": "節點",
|
||||
"nodesRunning": "節點執行中",
|
||||
"ok": "確定",
|
||||
"openNewIssue": "開啟新問題",
|
||||
"overwrite": "覆蓋",
|
||||
|
||||
@@ -336,7 +336,6 @@
|
||||
"noTasksFoundMessage": "队列中没有任务。",
|
||||
"noWorkflowsFound": "未找到工作流。",
|
||||
"nodes": "节点",
|
||||
"nodesRunning": "节点正在运行",
|
||||
"ok": "确定",
|
||||
"openNewIssue": "打开新问题",
|
||||
"overwrite": "覆盖",
|
||||
@@ -786,13 +785,13 @@
|
||||
"Toggle Bottom Panel": "切换底部面板",
|
||||
"Toggle Focus Mode": "切换专注模式",
|
||||
"Toggle Logs Bottom Panel": "切换日志底部面板",
|
||||
"Toggle Model Library Sidebar": "切換模型庫側邊欄",
|
||||
"Toggle Node Library Sidebar": "切換節點庫側邊欄",
|
||||
"Toggle Queue Sidebar": "切換佇列側邊欄",
|
||||
"Toggle Model Library Sidebar": "切换模型库侧边栏",
|
||||
"Toggle Node Library Sidebar": "切换节点库侧边栏",
|
||||
"Toggle Queue Sidebar": "切换队列侧边栏",
|
||||
"Toggle Search Box": "切换搜索框",
|
||||
"Toggle Terminal Bottom Panel": "切换终端底部面板",
|
||||
"Toggle Theme (Dark/Light)": "切换主题(暗/亮)",
|
||||
"Toggle Workflows Sidebar": "切換工作流程側邊欄",
|
||||
"Toggle Workflows Sidebar": "切换工作流侧边栏",
|
||||
"Toggle the Custom Nodes Manager": "切换自定义节点管理器",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "切换自定义节点管理器进度条",
|
||||
"Undo": "撤销",
|
||||
|
||||
@@ -48,22 +48,6 @@ const zProgressWsMessage = z.object({
|
||||
node: zNodeId
|
||||
})
|
||||
|
||||
const zNodeProgressState = z.object({
|
||||
value: z.number(),
|
||||
max: z.number(),
|
||||
state: z.enum(['pending', 'running', 'finished', 'error']),
|
||||
node_id: zNodeId,
|
||||
prompt_id: zPromptId,
|
||||
display_node_id: zNodeId.optional(),
|
||||
parent_node_id: zNodeId.optional(),
|
||||
real_node_id: zNodeId.optional()
|
||||
})
|
||||
|
||||
const zProgressStateWsMessage = z.object({
|
||||
prompt_id: zPromptId,
|
||||
nodes: z.record(zNodeId, zNodeProgressState)
|
||||
})
|
||||
|
||||
const zExecutingWsMessage = z.object({
|
||||
node: zNodeId,
|
||||
display_node: zNodeId,
|
||||
|
||||
@@ -16,7 +16,6 @@ import type {
|
||||
HistoryTaskItem,
|
||||
LogsRawResponse,
|
||||
LogsWsMessage,
|
||||
NodeProgressState,
|
||||
PendingTaskItem,
|
||||
ProgressTextWsMessage,
|
||||
ProgressWsMessage,
|
||||
@@ -106,17 +105,7 @@ interface BackendApiCalls {
|
||||
logs: LogsWsMessage
|
||||
/** Binary preview/progress data */
|
||||
b_preview: Blob
|
||||
/** Binary preview with metadata (node_id, prompt_id) */
|
||||
b_preview_with_metadata: {
|
||||
blob: Blob
|
||||
nodeId: string
|
||||
parentNodeId: string
|
||||
displayNodeId: string
|
||||
realNodeId: string
|
||||
promptId: string
|
||||
}
|
||||
progress_text: ProgressTextWsMessage
|
||||
progress_state: NodeProgressState
|
||||
display_component: DisplayComponentWsMessage
|
||||
feature_flags: FeatureFlagsWsMessage
|
||||
}
|
||||
@@ -468,33 +457,6 @@ export class ComfyApi extends EventTarget {
|
||||
})
|
||||
this.dispatchCustomEvent('b_preview', imageBlob)
|
||||
break
|
||||
case 4:
|
||||
// PREVIEW_IMAGE_WITH_METADATA
|
||||
const decoder4 = new TextDecoder()
|
||||
const metadataLength = view.getUint32(4)
|
||||
const metadataBytes = event.data.slice(8, 8 + metadataLength)
|
||||
const metadata = JSON.parse(decoder4.decode(metadataBytes))
|
||||
const imageData4 = event.data.slice(8 + metadataLength)
|
||||
|
||||
let imageMime4 = metadata.image_type
|
||||
|
||||
const imageBlob4 = new Blob([imageData4], {
|
||||
type: imageMime4
|
||||
})
|
||||
|
||||
// Dispatch enhanced preview event with metadata
|
||||
this.dispatchCustomEvent('b_preview_with_metadata', {
|
||||
blob: imageBlob4,
|
||||
nodeId: metadata.node_id,
|
||||
displayNodeId: metadata.display_node_id,
|
||||
parentNodeId: metadata.parent_node_id,
|
||||
realNodeId: metadata.real_node_id,
|
||||
promptId: metadata.prompt_id
|
||||
})
|
||||
|
||||
// Also dispatch legacy b_preview for backward compatibility
|
||||
this.dispatchCustomEvent('b_preview', imageBlob4)
|
||||
break
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown binary websocket message of type ${eventType}`
|
||||
@@ -524,7 +486,6 @@ export class ComfyApi extends EventTarget {
|
||||
case 'execution_cached':
|
||||
case 'execution_success':
|
||||
case 'progress':
|
||||
case 'progress_state':
|
||||
case 'executed':
|
||||
case 'graphChanged':
|
||||
case 'promptQueued':
|
||||
|
||||
@@ -194,8 +194,6 @@ export class ComfyApp {
|
||||
|
||||
/**
|
||||
* @deprecated Use useExecutionStore().executingNodeId instead
|
||||
* TODO: Update to support multiple executing nodes. This getter returns only the first executing node.
|
||||
* Consider updating consumers to handle multiple nodes or use executingNodeIds array.
|
||||
*/
|
||||
get runningNodeId(): NodeId | null {
|
||||
return useExecutionStore().executingNodeId
|
||||
@@ -637,6 +635,10 @@ export class ComfyApp {
|
||||
|
||||
api.addEventListener('executing', () => {
|
||||
this.graph.setDirtyCanvas(true, false)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.revokePreviews(this.runningNodeId)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
delete this.nodePreviewImages[this.runningNodeId]
|
||||
})
|
||||
|
||||
api.addEventListener('executed', ({ detail }) => {
|
||||
@@ -687,13 +689,15 @@ export class ComfyApp {
|
||||
this.canvas.draw(true, true)
|
||||
})
|
||||
|
||||
api.addEventListener('b_preview_with_metadata', ({ detail }) => {
|
||||
// Enhanced preview with explicit node context
|
||||
const { blob, displayNodeId } = detail
|
||||
this.revokePreviews(displayNodeId)
|
||||
api.addEventListener('b_preview', ({ detail }) => {
|
||||
const id = this.runningNodeId
|
||||
if (id == null) return
|
||||
|
||||
const blob = detail
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
// Preview cleanup is now handled in progress_state event to support multiple concurrent previews
|
||||
this.nodePreviewImages[displayNodeId] = [blobUrl]
|
||||
// Ensure clean up if `executing` event is missed.
|
||||
this.revokePreviews(id)
|
||||
this.nodePreviewImages[id] = [blobUrl]
|
||||
})
|
||||
|
||||
api.init()
|
||||
|
||||
@@ -237,7 +237,6 @@ export class ComponentWidgetImpl<
|
||||
component: Component
|
||||
inputSpec: InputSpec
|
||||
props?: P
|
||||
componentProps?: Partial<P>
|
||||
options: DOMWidgetOptions<V>
|
||||
}) {
|
||||
super({
|
||||
@@ -246,9 +245,7 @@ export class ComponentWidgetImpl<
|
||||
})
|
||||
this.component = obj.component
|
||||
this.inputSpec = obj.inputSpec
|
||||
this.props = obj.componentProps
|
||||
? ({ ...obj.props, ...obj.componentProps } as P)
|
||||
: obj.props
|
||||
this.props = obj.props
|
||||
}
|
||||
|
||||
override computeLayoutSize() {
|
||||
|
||||
@@ -33,8 +33,6 @@ import type {
|
||||
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
|
||||
import { ComfyApp, app } from '@/scripts/app'
|
||||
import { $el } from '@/scripts/ui'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
@@ -89,14 +87,6 @@ export const useLitegraphService = () => {
|
||||
constructor() {
|
||||
super(app.graph, subgraph, instanceData)
|
||||
|
||||
// Set up callback for promoted widget registration
|
||||
this.onPromotedWidgetAdded = (widget) => {
|
||||
const domWidgetStore = useDomWidgetStore()
|
||||
if (!domWidgetStore.widgetStates.has(widget.id)) {
|
||||
domWidgetStore.registerWidget(widget)
|
||||
}
|
||||
}
|
||||
|
||||
this.#setupStrokeStyles()
|
||||
this.#addInputs(ComfyNode.nodeData.inputs)
|
||||
this.#addOutputs(ComfyNode.nodeData.outputs)
|
||||
@@ -117,11 +107,7 @@ export const useLitegraphService = () => {
|
||||
*/
|
||||
#setupStrokeStyles() {
|
||||
this.strokeStyles['running'] = function (this: LGraphNode) {
|
||||
const nodeId = String(this.id)
|
||||
const nodeLocatorId = useWorkflowStore().nodeIdToNodeLocatorId(nodeId)
|
||||
const state =
|
||||
useExecutionStore().nodeLocationProgressStates[nodeLocatorId]?.state
|
||||
if (state === 'running') {
|
||||
if (this.id == app.runningNodeId) {
|
||||
return { color: '#0f0' }
|
||||
}
|
||||
}
|
||||
@@ -376,11 +362,7 @@ export const useLitegraphService = () => {
|
||||
*/
|
||||
#setupStrokeStyles() {
|
||||
this.strokeStyles['running'] = function (this: LGraphNode) {
|
||||
const nodeId = String(this.id)
|
||||
const nodeLocatorId = useWorkflowStore().nodeIdToNodeLocatorId(nodeId)
|
||||
const state =
|
||||
useExecutionStore().nodeLocationProgressStates[nodeLocatorId]?.state
|
||||
if (state === 'running') {
|
||||
if (this.id == app.runningNodeId) {
|
||||
return { color: '#0f0' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { COMFY_URLS, GITHUB_REPOS } from '@/constants/urls'
|
||||
import { AboutPageBadge } from '@/types/comfy'
|
||||
import { electronAPI, isElectron } from '@/utils/envUtil'
|
||||
|
||||
@@ -24,20 +25,20 @@ export const useAboutPanelStore = defineStore('aboutPanel', () => {
|
||||
? 'v' + electronAPI().getComfyUIVersion()
|
||||
: coreVersion.value
|
||||
}`,
|
||||
url: 'https://github.com/comfyanonymous/ComfyUI',
|
||||
url: GITHUB_REPOS.comfyui,
|
||||
icon: 'pi pi-github'
|
||||
},
|
||||
{
|
||||
label: `ComfyUI_frontend v${frontendVersion}`,
|
||||
url: 'https://github.com/Comfy-Org/ComfyUI_frontend',
|
||||
url: GITHUB_REPOS.frontend,
|
||||
icon: 'pi pi-github'
|
||||
},
|
||||
{
|
||||
label: 'Discord',
|
||||
url: 'https://www.comfy.org/discord',
|
||||
url: COMFY_URLS.community.discord,
|
||||
icon: 'pi pi-discord'
|
||||
},
|
||||
{ label: 'ComfyOrg', url: 'https://www.comfy.org/', icon: 'pi pi-globe' }
|
||||
{ label: 'ComfyOrg', url: COMFY_URLS.website.base, icon: 'pi pi-globe' }
|
||||
])
|
||||
|
||||
const allBadges = computed<AboutPageBadge[]>(() => [
|
||||
|
||||
@@ -12,8 +12,6 @@ import type {
|
||||
ExecutionErrorWsMessage,
|
||||
ExecutionStartWsMessage,
|
||||
NodeError,
|
||||
NodeProgressState,
|
||||
ProgressStateWsMessage,
|
||||
ProgressTextWsMessage,
|
||||
ProgressWsMessage
|
||||
} from '@/schemas/apiSchema'
|
||||
@@ -23,9 +21,6 @@ import type {
|
||||
NodeId
|
||||
} from '@/schemas/comfyWorkflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { NodeLocatorId } from '@/types/nodeIdentification'
|
||||
import { createNodeLocatorId } from '@/types/nodeIdentification'
|
||||
|
||||
import { useCanvasStore } from './graphStore'
|
||||
import { ComfyWorkflow, useWorkflowStore } from './workflowStore'
|
||||
@@ -51,97 +46,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
const queuedPrompts = ref<Record<NodeId, QueuedPrompt>>({})
|
||||
const lastNodeErrors = ref<Record<NodeId, NodeError> | null>(null)
|
||||
const lastExecutionError = ref<ExecutionErrorWsMessage | null>(null)
|
||||
// This is the progress of all nodes in the currently executing workflow
|
||||
const nodeProgressStates = ref<Record<string, NodeProgressState>>({})
|
||||
|
||||
/**
|
||||
* Convert execution context node IDs to NodeLocatorIds
|
||||
* @param nodeId The node ID from execution context (could be execution ID)
|
||||
* @returns The NodeLocatorId
|
||||
*/
|
||||
const executionIdToNodeLocatorId = (
|
||||
nodeId: string | number
|
||||
): NodeLocatorId => {
|
||||
const nodeIdStr = String(nodeId)
|
||||
|
||||
if (!nodeIdStr.includes(':')) {
|
||||
// It's a top-level node ID
|
||||
return nodeIdStr as NodeLocatorId
|
||||
}
|
||||
|
||||
// It's an execution node ID
|
||||
const parts = nodeIdStr.split(':')
|
||||
const localNodeId = parts[parts.length - 1]
|
||||
const subgraphs = getSubgraphsFromInstanceIds(app.graph, parts)
|
||||
const nodeLocatorId = createNodeLocatorId(subgraphs.at(-1)!.id, localNodeId)
|
||||
return nodeLocatorId
|
||||
}
|
||||
|
||||
const mergeExecutionProgressStates = (
|
||||
currentState: NodeProgressState | undefined,
|
||||
newState: NodeProgressState
|
||||
): NodeProgressState => {
|
||||
if (currentState === undefined) {
|
||||
return newState
|
||||
}
|
||||
|
||||
const mergedState = { ...currentState }
|
||||
if (mergedState.state === 'error') {
|
||||
return mergedState
|
||||
} else if (newState.state === 'running') {
|
||||
const newPerc = newState.max > 0 ? newState.value / newState.max : 0.0
|
||||
const oldPerc =
|
||||
mergedState.max > 0 ? mergedState.value / mergedState.max : 0.0
|
||||
if (
|
||||
mergedState.state !== 'running' ||
|
||||
oldPerc === 0.0 ||
|
||||
newPerc < oldPerc
|
||||
) {
|
||||
mergedState.value = newState.value
|
||||
mergedState.max = newState.max
|
||||
}
|
||||
mergedState.state = 'running'
|
||||
}
|
||||
|
||||
return mergedState
|
||||
}
|
||||
|
||||
const nodeLocationProgressStates = computed<
|
||||
Record<NodeLocatorId, NodeProgressState>
|
||||
>(() => {
|
||||
const result: Record<NodeLocatorId, NodeProgressState> = {}
|
||||
|
||||
const states = nodeProgressStates.value // Apparently doing this inside `Object.entries` causes issues
|
||||
for (const [_, state] of Object.entries(states)) {
|
||||
const parts = String(state.display_node_id).split(':')
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const executionId = parts.slice(0, i + 1).join(':')
|
||||
const locatorId = executionIdToNodeLocatorId(executionId)
|
||||
if (!locatorId) continue
|
||||
|
||||
result[locatorId] = mergeExecutionProgressStates(
|
||||
result[locatorId],
|
||||
state
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// Easily access all currently executing node IDs
|
||||
const executingNodeIds = computed<NodeId[]>(() => {
|
||||
return Object.entries(nodeProgressStates)
|
||||
.filter(([_, state]) => state.state === 'running')
|
||||
.map(([nodeId, _]) => nodeId)
|
||||
})
|
||||
|
||||
// @deprecated For backward compatibility - stores the primary executing node ID
|
||||
const executingNodeId = computed<NodeId | null>(() => {
|
||||
return executingNodeIds.value.length > 0 ? executingNodeIds.value[0] : null
|
||||
})
|
||||
|
||||
// For backward compatibility - returns the primary executing node
|
||||
const executingNodeId = ref<NodeId | null>(null)
|
||||
const executingNode = computed<ComfyNode | null>(() => {
|
||||
if (!executingNodeId.value) return null
|
||||
|
||||
@@ -188,7 +93,30 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
return getSubgraphsFromInstanceIds(subgraph, subgraphNodeIds, subgraphs)
|
||||
}
|
||||
|
||||
// This is the progress of the currently executing node (for backward compatibility)
|
||||
const executionIdToCurrentId = (id: string) => {
|
||||
const subgraph = workflowStore.activeSubgraph
|
||||
|
||||
// Short-circuit: ID belongs to the parent workflow / no active subgraph
|
||||
if (!id.includes(':')) {
|
||||
return !subgraph ? id : undefined
|
||||
} else if (!subgraph) {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the hierarchical ID (e.g., "123:456:789")
|
||||
const subgraphNodeIds = id.split(':')
|
||||
|
||||
// If the last subgraph is the active subgraph, return the node ID
|
||||
const subgraphs = getSubgraphsFromInstanceIds(
|
||||
subgraph.rootGraph,
|
||||
subgraphNodeIds
|
||||
)
|
||||
if (subgraphs.at(-1) === subgraph) {
|
||||
return subgraphNodeIds.at(-1)
|
||||
}
|
||||
}
|
||||
|
||||
// This is the progress of the currently executing node, if any
|
||||
const _executingNodeProgress = ref<ProgressWsMessage | null>(null)
|
||||
const executingNodeProgress = computed(() =>
|
||||
_executingNodeProgress.value
|
||||
@@ -225,7 +153,6 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
api.addEventListener('executed', handleExecuted)
|
||||
api.addEventListener('executing', handleExecuting)
|
||||
api.addEventListener('progress', handleProgress)
|
||||
api.addEventListener('progress_state', handleProgressState)
|
||||
api.addEventListener('status', handleStatus)
|
||||
api.addEventListener('execution_error', handleExecutionError)
|
||||
}
|
||||
@@ -238,7 +165,6 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
api.removeEventListener('executed', handleExecuted)
|
||||
api.removeEventListener('executing', handleExecuting)
|
||||
api.removeEventListener('progress', handleProgress)
|
||||
api.removeEventListener('progress_state', handleProgressState)
|
||||
api.removeEventListener('status', handleStatus)
|
||||
api.removeEventListener('execution_error', handleExecutionError)
|
||||
api.removeEventListener('progress_text', handleProgressText)
|
||||
@@ -268,42 +194,19 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
|
||||
if (!activePrompt.value) return
|
||||
|
||||
// Update the executing nodes list
|
||||
if (typeof e.detail !== 'string') {
|
||||
if (activePromptId.value) {
|
||||
delete queuedPrompts.value[activePromptId.value]
|
||||
}
|
||||
activePromptId.value = null
|
||||
if (executingNodeId.value && activePrompt.value) {
|
||||
// Seems sometimes nodes that are cached fire executing but not executed
|
||||
activePrompt.value.nodes[executingNodeId.value] = true
|
||||
}
|
||||
}
|
||||
|
||||
function handleProgressState(e: CustomEvent<ProgressStateWsMessage>) {
|
||||
const { nodes } = e.detail
|
||||
|
||||
// Revoke previews for nodes that are starting to execute
|
||||
for (const nodeId in nodes) {
|
||||
const nodeState = nodes[nodeId]
|
||||
if (nodeState.state === 'running' && !nodeProgressStates.value[nodeId]) {
|
||||
// This node just started executing, revoke its previews
|
||||
// Note that we're doing the *actual* node id instead of the display node id
|
||||
// here intentionally. That way, we don't clear the preview every time a new node
|
||||
// within an expanded graph starts executing.
|
||||
app.revokePreviews(nodeId)
|
||||
delete app.nodePreviewImages[nodeId]
|
||||
}
|
||||
}
|
||||
|
||||
// Update the progress states for all nodes
|
||||
nodeProgressStates.value = nodes
|
||||
|
||||
// If we have progress for the currently executing node, update it for backwards compatibility
|
||||
if (executingNodeId.value && nodes[executingNodeId.value]) {
|
||||
const nodeState = nodes[executingNodeId.value]
|
||||
_executingNodeProgress.value = {
|
||||
value: nodeState.value,
|
||||
max: nodeState.max,
|
||||
prompt_id: nodeState.prompt_id,
|
||||
node: nodeState.display_node_id || nodeState.node_id
|
||||
if (typeof e.detail === 'string') {
|
||||
executingNodeId.value = executionIdToCurrentId(e.detail) ?? null
|
||||
} else {
|
||||
executingNodeId.value = e.detail
|
||||
if (executingNodeId.value === null) {
|
||||
if (activePromptId.value) {
|
||||
delete queuedPrompts.value[activePromptId.value]
|
||||
}
|
||||
activePromptId.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -336,7 +239,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
const { nodeId, text } = e.detail
|
||||
if (!text || !nodeId) return
|
||||
|
||||
// Handle execution node IDs for subgraphs
|
||||
// Handle hierarchical node IDs for subgraphs
|
||||
const currentId = getNodeIdIfExecuting(nodeId)
|
||||
const node = canvasStore.getCanvas().graph?.getNodeById(currentId)
|
||||
if (!node) return
|
||||
@@ -347,7 +250,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
function handleDisplayComponent(e: CustomEvent<DisplayComponentWsMessage>) {
|
||||
const { node_id: nodeId, component, props = {} } = e.detail
|
||||
|
||||
// Handle execution node IDs for subgraphs
|
||||
// Handle hierarchical node IDs for subgraphs
|
||||
const currentId = getNodeIdIfExecuting(nodeId)
|
||||
const node = canvasStore.getCanvas().graph?.getNodeById(currentId)
|
||||
if (!node) return
|
||||
@@ -387,18 +290,6 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a NodeLocatorId to an execution context ID
|
||||
* @param locatorId The NodeLocatorId
|
||||
* @returns The execution ID or null if conversion fails
|
||||
*/
|
||||
const nodeLocatorIdToExecutionId = (
|
||||
locatorId: NodeLocatorId | string
|
||||
): string | null => {
|
||||
const executionId = workflowStore.nodeLocatorIdToNodeExecutionId(locatorId)
|
||||
return executionId
|
||||
}
|
||||
|
||||
return {
|
||||
isIdle,
|
||||
clientId,
|
||||
@@ -419,13 +310,9 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
*/
|
||||
lastExecutionError,
|
||||
/**
|
||||
* The id of the node that is currently being executed (backward compatibility)
|
||||
* The id of the node that is currently being executed
|
||||
*/
|
||||
executingNodeId,
|
||||
/**
|
||||
* The list of all nodes that are currently executing
|
||||
*/
|
||||
executingNodeIds,
|
||||
/**
|
||||
* The prompt that is currently being executed
|
||||
*/
|
||||
@@ -443,25 +330,17 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
*/
|
||||
executionProgress,
|
||||
/**
|
||||
* The node that is currently being executed (backward compatibility)
|
||||
* The node that is currently being executed
|
||||
*/
|
||||
executingNode,
|
||||
/**
|
||||
* The progress of the executing node (backward compatibility)
|
||||
* The progress of the executing node (if the node reports progress)
|
||||
*/
|
||||
executingNodeProgress,
|
||||
/**
|
||||
* All node progress states from progress_state events
|
||||
*/
|
||||
nodeProgressStates,
|
||||
nodeLocationProgressStates,
|
||||
bindExecutionEvents,
|
||||
unbindExecutionEvents,
|
||||
storePrompt,
|
||||
// Raw executing progress data for backward compatibility in ComfyApp.
|
||||
_executingNodeProgress,
|
||||
// NodeLocatorId conversion helpers
|
||||
executionIdToNodeLocatorId,
|
||||
nodeLocatorIdToExecutionId
|
||||
_executingNodeProgress
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,18 +4,10 @@ import { defineStore } from 'pinia'
|
||||
import { type Raw, computed, markRaw, ref, shallowRef, watch } from 'vue'
|
||||
|
||||
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||
import { defaultGraphJSON } from '@/scripts/defaultGraph'
|
||||
import type { NodeExecutionId, NodeLocatorId } from '@/types/nodeIdentification'
|
||||
import {
|
||||
createNodeExecutionId,
|
||||
createNodeLocatorId,
|
||||
parseNodeExecutionId,
|
||||
parseNodeLocatorId
|
||||
} from '@/types/nodeIdentification'
|
||||
import { getPathDetails } from '@/utils/formatUtil'
|
||||
import { syncEntities } from '@/utils/syncUtil'
|
||||
import { isSubgraph } from '@/utils/typeGuardUtil'
|
||||
@@ -171,15 +163,6 @@ export interface WorkflowStore {
|
||||
/** Updates the {@link subgraphNamePath} and {@link isSubgraphActive} values. */
|
||||
updateActiveGraph: () => void
|
||||
executionIdToCurrentId: (id: string) => any
|
||||
nodeIdToNodeLocatorId: (nodeId: NodeId, subgraph?: Subgraph) => NodeLocatorId
|
||||
nodeExecutionIdToNodeLocatorId: (
|
||||
nodeExecutionId: NodeExecutionId | string
|
||||
) => NodeLocatorId | null
|
||||
nodeLocatorIdToNodeId: (locatorId: NodeLocatorId | string) => NodeId | null
|
||||
nodeLocatorIdToNodeExecutionId: (
|
||||
locatorId: NodeLocatorId | string,
|
||||
targetSubgraph?: Subgraph
|
||||
) => NodeExecutionId | null
|
||||
}
|
||||
|
||||
export const useWorkflowStore = defineStore('workflow', () => {
|
||||
@@ -490,7 +473,7 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the execution ID (e.g., "123:456:789")
|
||||
// Parse the hierarchical ID (e.g., "123:456:789")
|
||||
const subgraphNodeIds = id.split(':')
|
||||
|
||||
// Start from the root graph
|
||||
@@ -505,136 +488,6 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
|
||||
watch(activeWorkflow, updateActiveGraph)
|
||||
|
||||
/**
|
||||
* Convert a node ID to a NodeLocatorId
|
||||
* @param nodeId The local node ID
|
||||
* @param subgraph The subgraph containing the node (defaults to active subgraph)
|
||||
* @returns The NodeLocatorId (for root graph nodes, returns the node ID as-is)
|
||||
*/
|
||||
const nodeIdToNodeLocatorId = (
|
||||
nodeId: NodeId,
|
||||
subgraph?: Subgraph
|
||||
): NodeLocatorId => {
|
||||
const targetSubgraph = subgraph ?? activeSubgraph.value
|
||||
if (!targetSubgraph) {
|
||||
// Node is in the root graph, return the node ID as-is
|
||||
return String(nodeId) as NodeLocatorId
|
||||
}
|
||||
|
||||
return createNodeLocatorId(targetSubgraph.id, nodeId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an execution ID to a NodeLocatorId
|
||||
* @param nodeExecutionId The execution node ID (e.g., "123:456:789")
|
||||
* @returns The NodeLocatorId or null if conversion fails
|
||||
*/
|
||||
const nodeExecutionIdToNodeLocatorId = (
|
||||
nodeExecutionId: NodeExecutionId | string
|
||||
): NodeLocatorId | null => {
|
||||
// Handle simple node IDs (root graph - no colons)
|
||||
if (!nodeExecutionId.includes(':')) {
|
||||
return nodeExecutionId as NodeLocatorId
|
||||
}
|
||||
|
||||
const parts = parseNodeExecutionId(nodeExecutionId)
|
||||
if (!parts || parts.length === 0) return null
|
||||
|
||||
const nodeId = parts[parts.length - 1]
|
||||
const subgraphNodeIds = parts.slice(0, -1)
|
||||
|
||||
if (subgraphNodeIds.length === 0) {
|
||||
// Node is in root graph, return the node ID as-is
|
||||
return String(nodeId) as NodeLocatorId
|
||||
}
|
||||
|
||||
try {
|
||||
const subgraphs = getSubgraphsFromInstanceIds(
|
||||
comfyApp.graph,
|
||||
subgraphNodeIds.map((id) => String(id))
|
||||
)
|
||||
const immediateSubgraph = subgraphs[subgraphs.length - 1]
|
||||
return createNodeLocatorId(immediateSubgraph.id, nodeId)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the node ID from a NodeLocatorId
|
||||
* @param locatorId The NodeLocatorId
|
||||
* @returns The local node ID or null if invalid
|
||||
*/
|
||||
const nodeLocatorIdToNodeId = (
|
||||
locatorId: NodeLocatorId | string
|
||||
): NodeId | null => {
|
||||
const parsed = parseNodeLocatorId(locatorId)
|
||||
return parsed?.localNodeId ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a NodeLocatorId to an execution ID for a specific context
|
||||
* @param locatorId The NodeLocatorId
|
||||
* @param targetSubgraph The subgraph context (defaults to active subgraph)
|
||||
* @returns The execution ID or null if the node is not accessible from the target context
|
||||
*/
|
||||
const nodeLocatorIdToNodeExecutionId = (
|
||||
locatorId: NodeLocatorId | string,
|
||||
targetSubgraph?: Subgraph
|
||||
): NodeExecutionId | null => {
|
||||
const parsed = parseNodeLocatorId(locatorId)
|
||||
if (!parsed) return null
|
||||
|
||||
const { subgraphUuid, localNodeId } = parsed
|
||||
|
||||
// If no subgraph UUID, this is a root graph node
|
||||
if (!subgraphUuid) {
|
||||
return String(localNodeId) as NodeExecutionId
|
||||
}
|
||||
|
||||
// Find the path from root to the subgraph with this UUID
|
||||
const findSubgraphPath = (
|
||||
graph: LGraph | Subgraph,
|
||||
targetUuid: string,
|
||||
path: NodeId[] = []
|
||||
): NodeId[] | null => {
|
||||
if (isSubgraph(graph) && graph.id === targetUuid) {
|
||||
return path
|
||||
}
|
||||
|
||||
for (const node of graph._nodes) {
|
||||
if (node.isSubgraphNode?.() && (node as any).subgraph) {
|
||||
const result = findSubgraphPath((node as any).subgraph, targetUuid, [
|
||||
...path,
|
||||
node.id
|
||||
])
|
||||
if (result) return result
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const path = findSubgraphPath(comfyApp.graph, subgraphUuid)
|
||||
if (!path) return null
|
||||
|
||||
// If we have a target subgraph, check if the path goes through it
|
||||
if (
|
||||
targetSubgraph &&
|
||||
!path.some((_, idx) => {
|
||||
const subgraphs = getSubgraphsFromInstanceIds(
|
||||
comfyApp.graph,
|
||||
path.slice(0, idx + 1).map((id) => String(id))
|
||||
)
|
||||
return subgraphs[subgraphs.length - 1] === targetSubgraph
|
||||
})
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return createNodeExecutionId([...path, localNodeId])
|
||||
}
|
||||
|
||||
return {
|
||||
activeWorkflow,
|
||||
isActive,
|
||||
@@ -661,11 +514,7 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
isSubgraphActive,
|
||||
activeSubgraph,
|
||||
updateActiveGraph,
|
||||
executionIdToCurrentId,
|
||||
nodeIdToNodeLocatorId,
|
||||
nodeExecutionIdToNodeLocatorId,
|
||||
nodeLocatorIdToNodeId,
|
||||
nodeLocatorIdToNodeExecutionId
|
||||
executionIdToCurrentId
|
||||
}
|
||||
}) satisfies () => WorkflowStore
|
||||
|
||||
|
||||
@@ -699,6 +699,27 @@ export interface paths {
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/publishers/{publisherId}/nodes/{nodeId}/claim-my-node': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
get?: never
|
||||
put?: never
|
||||
/**
|
||||
* Claim nodeId into publisherId for the authenticated publisher
|
||||
* @description This endpoint allows a publisher to claim an unclaimed node that they own the repo, which is identified by the nodeId. The unclaimed node's repository must be owned by the authenticated user.
|
||||
*
|
||||
*/
|
||||
post: operations['claimMyNode']
|
||||
delete?: never
|
||||
options?: never
|
||||
head?: never
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/publishers/{publisherId}/nodes/v2': {
|
||||
parameters: {
|
||||
query?: never
|
||||
@@ -1061,6 +1082,23 @@ export interface paths {
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/bulk/nodes/versions': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
get?: never
|
||||
put?: never
|
||||
/** Retrieve multiple node versions in a single request */
|
||||
post: operations['getBulkNodeVersions']
|
||||
delete?: never
|
||||
options?: never
|
||||
head?: never
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/versions': {
|
||||
parameters: {
|
||||
query?: never
|
||||
@@ -1095,6 +1133,26 @@ export interface paths {
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/admin/nodes/{nodeId}': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
get?: never
|
||||
/**
|
||||
* Admin Update Node
|
||||
* @description Only admins can update a node with admin privileges.
|
||||
*/
|
||||
put: operations['adminUpdateNode']
|
||||
post?: never
|
||||
delete?: never
|
||||
options?: never
|
||||
head?: never
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/admin/nodes/{nodeId}/versions/{versionNumber}': {
|
||||
parameters: {
|
||||
query?: never
|
||||
@@ -2951,7 +3009,7 @@ export interface paths {
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/proxy/moonvalley/text-to-video': {
|
||||
'/proxy/moonvalley/prompts/text-to-video': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
@@ -2968,7 +3026,7 @@ export interface paths {
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/proxy/moonvalley/text-to-image': {
|
||||
'/proxy/moonvalley/prompts/text-to-image': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
@@ -3057,6 +3115,37 @@ export interface paths {
|
||||
export type webhooks = Record<string, never>
|
||||
export interface components {
|
||||
schemas: {
|
||||
ClaimMyNodeRequest: {
|
||||
/** @description GitHub token to verify if the user owns the repo of the node */
|
||||
GH_TOKEN: string
|
||||
}
|
||||
BulkNodeVersionsRequest: {
|
||||
/** @description List of node ID and version pairs to retrieve */
|
||||
node_versions: components['schemas']['NodeVersionIdentifier'][]
|
||||
}
|
||||
NodeVersionIdentifier: {
|
||||
/** @description The unique identifier of the node */
|
||||
node_id: string
|
||||
/** @description The version of the node */
|
||||
version: string
|
||||
}
|
||||
BulkNodeVersionsResponse: {
|
||||
/** @description List of retrieved node versions with their status */
|
||||
node_versions: components['schemas']['BulkNodeVersionResult'][]
|
||||
}
|
||||
BulkNodeVersionResult: {
|
||||
/** @description The node and version identifier */
|
||||
identifier: components['schemas']['NodeVersionIdentifier']
|
||||
/**
|
||||
* @description Status of the retrieval operation
|
||||
* @enum {string}
|
||||
*/
|
||||
status: 'success' | 'not_found' | 'error'
|
||||
/** @description The retrieved node version data (only present if status is success) */
|
||||
node_version?: components['schemas']['NodeVersion']
|
||||
/** @description Error message if retrieval failed (only present if status is error) */
|
||||
error_message?: string
|
||||
}
|
||||
PersonalAccessToken: {
|
||||
/**
|
||||
* Format: uuid
|
||||
@@ -8713,71 +8802,212 @@ export interface components {
|
||||
| 'computer-use-preview'
|
||||
| 'computer-use-preview-2025-03-11'
|
||||
| 'chatgpt-4o-latest'
|
||||
MoonvalleyInferenceParams: {
|
||||
/** @default 1080 */
|
||||
MoonvalleyTextToVideoInferenceParams: {
|
||||
/**
|
||||
* @description Height of the generated video in pixels
|
||||
* @default 1080
|
||||
*/
|
||||
height: number
|
||||
/** @default 1920 */
|
||||
/**
|
||||
* @description Width of the generated video in pixels
|
||||
* @default 1920
|
||||
*/
|
||||
width: number
|
||||
/** @default 64 */
|
||||
/**
|
||||
* @description Number of frames to generate
|
||||
* @default 64
|
||||
*/
|
||||
num_frames: number
|
||||
/** @default 24 */
|
||||
/**
|
||||
* @description Frames per second of the generated video
|
||||
* @default 24
|
||||
*/
|
||||
fps: number
|
||||
/**
|
||||
* Format: float
|
||||
* @default 12.5
|
||||
* @description Guidance scale for generation control
|
||||
* @default 10
|
||||
*/
|
||||
guidance_scale: number
|
||||
/** @description Random seed for generation (default: random) */
|
||||
seed?: number
|
||||
/** @default 80 */
|
||||
/**
|
||||
* @description Number of denoising steps
|
||||
* @default 80
|
||||
*/
|
||||
steps: number
|
||||
/** @default true */
|
||||
/**
|
||||
* @description Whether to use timestep transformation
|
||||
* @default true
|
||||
*/
|
||||
use_timestep_transform: boolean
|
||||
/**
|
||||
* Format: float
|
||||
* @description Shift value for generation control
|
||||
* @default 3
|
||||
*/
|
||||
shift_value: number
|
||||
/** @default true */
|
||||
/**
|
||||
* @description Whether to use guidance scheduling
|
||||
* @default true
|
||||
*/
|
||||
use_guidance_schedule: boolean
|
||||
/** @default true */
|
||||
/**
|
||||
* @description Whether to add quality guidance
|
||||
* @default true
|
||||
*/
|
||||
add_quality_guidance: boolean
|
||||
/**
|
||||
* Format: float
|
||||
* @description CLIP value for generation control
|
||||
* @default 3
|
||||
*/
|
||||
clip_value: number
|
||||
/** @default false */
|
||||
/**
|
||||
* @description Whether to use negative prompts
|
||||
* @default false
|
||||
*/
|
||||
use_negative_prompts: boolean
|
||||
/** @description Negative prompt text */
|
||||
negative_prompt?: string
|
||||
warmup_steps?: number
|
||||
cooldown_steps?: number
|
||||
/**
|
||||
* @description Number of warmup steps (calculated based on num_frames)
|
||||
* @default 0
|
||||
*/
|
||||
warmup_steps: number
|
||||
/**
|
||||
* @description Number of cooldown steps (calculated based on num_frames)
|
||||
* @default 75
|
||||
*/
|
||||
cooldown_steps: number
|
||||
/**
|
||||
* Format: float
|
||||
* @description Caching coefficient for optimization
|
||||
* @default 0.3
|
||||
*/
|
||||
caching_coefficient: number
|
||||
/** @default 3 */
|
||||
/**
|
||||
* @description Number of caching warmup steps
|
||||
* @default 3
|
||||
*/
|
||||
caching_warmup: number
|
||||
/** @default 3 */
|
||||
/**
|
||||
* @description Number of caching cooldown steps
|
||||
* @default 3
|
||||
*/
|
||||
caching_cooldown: number
|
||||
/** @default 0 */
|
||||
/**
|
||||
* @description Index of the conditioning frame
|
||||
* @default 0
|
||||
*/
|
||||
conditioning_frame_index: number
|
||||
}
|
||||
MoonvalleyVideoToVideoInferenceParams: {
|
||||
/**
|
||||
* Format: float
|
||||
* @description Guidance scale for generation control
|
||||
* @default 15
|
||||
*/
|
||||
guidance_scale: number
|
||||
/** @description Random seed for generation (default: random) */
|
||||
seed?: number
|
||||
/**
|
||||
* @description Number of denoising steps
|
||||
* @default 80
|
||||
*/
|
||||
steps: number
|
||||
/**
|
||||
* @description Whether to use timestep transformation
|
||||
* @default true
|
||||
*/
|
||||
use_timestep_transform: boolean
|
||||
/**
|
||||
* Format: float
|
||||
* @description Shift value for generation control
|
||||
* @default 3
|
||||
*/
|
||||
shift_value: number
|
||||
/**
|
||||
* @description Whether to use guidance scheduling
|
||||
* @default true
|
||||
*/
|
||||
use_guidance_schedule: boolean
|
||||
/**
|
||||
* @description Whether to add quality guidance
|
||||
* @default true
|
||||
*/
|
||||
add_quality_guidance: boolean
|
||||
/**
|
||||
* Format: float
|
||||
* @description CLIP value for generation control
|
||||
* @default 3
|
||||
*/
|
||||
clip_value: number
|
||||
/**
|
||||
* @description Whether to use negative prompts
|
||||
* @default false
|
||||
*/
|
||||
use_negative_prompts: boolean
|
||||
/** @description Negative prompt text */
|
||||
negative_prompt?: string
|
||||
/**
|
||||
* @description Number of warmup steps (calculated based on num_frames)
|
||||
* @default 24
|
||||
*/
|
||||
warmup_steps: number
|
||||
/**
|
||||
* @description Number of cooldown steps (calculated based on num_frames)
|
||||
* @default 36
|
||||
*/
|
||||
cooldown_steps: number
|
||||
/**
|
||||
* Format: float
|
||||
* @description Caching coefficient for optimization
|
||||
* @default 0.3
|
||||
*/
|
||||
caching_coefficient: number
|
||||
/**
|
||||
* @description Number of caching warmup steps
|
||||
* @default 3
|
||||
*/
|
||||
caching_warmup: number
|
||||
/**
|
||||
* @description Number of caching cooldown steps
|
||||
* @default 3
|
||||
*/
|
||||
caching_cooldown: number
|
||||
/**
|
||||
* @description Index of the conditioning frame
|
||||
* @default 0
|
||||
*/
|
||||
conditioning_frame_index: number
|
||||
}
|
||||
MoonvalleyTextToImageRequest: {
|
||||
prompt_text?: string
|
||||
image_url?: string
|
||||
inference_params?: components['schemas']['MoonvalleyInferenceParams']
|
||||
inference_params?: components['schemas']['MoonvalleyTextToVideoInferenceParams']
|
||||
webhook_url?: string
|
||||
}
|
||||
MoonvalleyTextToVideoRequest: {
|
||||
prompt_text?: string
|
||||
image_url?: string
|
||||
inference_params?: components['schemas']['MoonvalleyInferenceParams']
|
||||
inference_params?: components['schemas']['MoonvalleyTextToVideoInferenceParams']
|
||||
webhook_url?: string
|
||||
}
|
||||
MoonvalleyVideoToVideoRequest: components['schemas']['MoonvalleyTextToVideoRequest'] & {
|
||||
MoonvalleyVideoToVideoRequest: {
|
||||
/** @description Describes the video to generate */
|
||||
prompt_text: string
|
||||
/** @description Url to control video */
|
||||
video_url: string
|
||||
control_type: string
|
||||
/**
|
||||
* @description Supported types for video control
|
||||
* @enum {string}
|
||||
*/
|
||||
control_type: 'motion_control' | 'pose_control'
|
||||
/** @description Parameters for video-to-video generation inference */
|
||||
inference_params?: components['schemas']['MoonvalleyVideoToVideoInferenceParams']
|
||||
/** @description Optional webhook URL for notifications */
|
||||
webhook_url?: string
|
||||
}
|
||||
MoonvalleyPromptResponse: {
|
||||
id?: string
|
||||
@@ -10421,6 +10651,89 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
claimMyNode: {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path: {
|
||||
publisherId: string
|
||||
nodeId: string
|
||||
}
|
||||
cookie?: never
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['ClaimMyNodeRequest']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
/** @description Node claimed successfully */
|
||||
204: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Bad request, invalid input data */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Forbidden - various authorization and permission issues
|
||||
* Includes:
|
||||
* - The authenticated user does not have permission to claim the node
|
||||
* - The node is already claimed by another publisher
|
||||
* - The GH_TOKEN is invalid
|
||||
* - The repository is not owned by the authenticated GitHub user
|
||||
* */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Too many requests - GitHub API rate limit exceeded */
|
||||
429: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Service unavailable - GitHub API is currently unavailable */
|
||||
503: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
listNodesForPublisherV2: {
|
||||
parameters: {
|
||||
query?: {
|
||||
@@ -11709,6 +12022,48 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
getBulkNodeVersions: {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['BulkNodeVersionsRequest']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
/** @description Successfully retrieved node versions */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['BulkNodeVersionsResponse']
|
||||
}
|
||||
}
|
||||
/** @description Bad request, invalid input */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
listAllNodeVersions: {
|
||||
parameters: {
|
||||
query?: {
|
||||
@@ -11834,6 +12189,75 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
adminUpdateNode: {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path: {
|
||||
nodeId: string
|
||||
}
|
||||
cookie?: never
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Node']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
/** @description Node updated successfully */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['Node']
|
||||
}
|
||||
}
|
||||
/** @description Bad request, invalid input data. */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Forbidden */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Node not found */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
adminUpdateNodeVersion: {
|
||||
parameters: {
|
||||
query?: never
|
||||
|
||||
@@ -31,16 +31,6 @@ export type { ComfyApi } from '@/scripts/api'
|
||||
export type { ComfyApp } from '@/scripts/app'
|
||||
export type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
export type { InputSpec } from '@/schemas/nodeDefSchema'
|
||||
export type {
|
||||
NodeLocatorId,
|
||||
NodeExecutionId,
|
||||
isNodeLocatorId,
|
||||
isNodeExecutionId,
|
||||
parseNodeLocatorId,
|
||||
createNodeLocatorId,
|
||||
parseNodeExecutionId,
|
||||
createNodeExecutionId
|
||||
} from './nodeIdentification'
|
||||
export type {
|
||||
EmbeddingsResponse,
|
||||
ExtensionsResponse,
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
|
||||
/**
|
||||
* A globally unique identifier for nodes that maintains consistency across
|
||||
* multiple instances of the same subgraph.
|
||||
*
|
||||
* Format:
|
||||
* - For subgraph nodes: `<immediate-contained-subgraph-uuid>:<local-node-id>`
|
||||
* - For root graph nodes: `<local-node-id>`
|
||||
*
|
||||
* Examples:
|
||||
* - "a1b2c3d4-e5f6-7890-abcd-ef1234567890:123" (node in subgraph)
|
||||
* - "456" (node in root graph)
|
||||
*
|
||||
* Unlike execution IDs which change based on the instance path,
|
||||
* NodeLocatorId remains the same for all instances of a particular node.
|
||||
*/
|
||||
export type NodeLocatorId = string
|
||||
|
||||
/**
|
||||
* An execution identifier representing a node's position in nested subgraphs.
|
||||
* Also known as ExecutionId in some contexts.
|
||||
*
|
||||
* Format: Colon-separated path of node IDs
|
||||
* Example: "123:456:789" (node 789 in subgraph 456 in subgraph 123)
|
||||
*/
|
||||
export type NodeExecutionId = string
|
||||
|
||||
/**
|
||||
* Type guard to check if a value is a NodeLocatorId
|
||||
*/
|
||||
export function isNodeLocatorId(value: unknown): value is NodeLocatorId {
|
||||
if (typeof value !== 'string') return false
|
||||
|
||||
// Check if it's a simple node ID (root graph node)
|
||||
const parts = value.split(':')
|
||||
if (parts.length === 1) {
|
||||
// Simple node ID - must be non-empty
|
||||
return value.length > 0
|
||||
}
|
||||
|
||||
// Check for UUID:nodeId format
|
||||
if (parts.length !== 2) return false
|
||||
|
||||
// Check that node ID part is not empty
|
||||
if (!parts[1]) return false
|
||||
|
||||
// Basic UUID format check (8-4-4-4-12 hex characters)
|
||||
const uuidPattern =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||
return uuidPattern.test(parts[0])
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if a value is a NodeExecutionId
|
||||
*/
|
||||
export function isNodeExecutionId(value: unknown): value is NodeExecutionId {
|
||||
if (typeof value !== 'string') return false
|
||||
// Must contain at least one colon to be an execution ID
|
||||
return value.includes(':')
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a NodeLocatorId into its components
|
||||
* @param id The NodeLocatorId to parse
|
||||
* @returns The subgraph UUID and local node ID, or null if invalid
|
||||
*/
|
||||
export function parseNodeLocatorId(
|
||||
id: string
|
||||
): { subgraphUuid: string | null; localNodeId: NodeId } | null {
|
||||
if (!isNodeLocatorId(id)) return null
|
||||
|
||||
const parts = id.split(':')
|
||||
|
||||
if (parts.length === 1) {
|
||||
// Simple node ID (root graph)
|
||||
return {
|
||||
subgraphUuid: null,
|
||||
localNodeId: isNaN(Number(id)) ? id : Number(id)
|
||||
}
|
||||
}
|
||||
|
||||
const [subgraphUuid, localNodeId] = parts
|
||||
return {
|
||||
subgraphUuid,
|
||||
localNodeId: isNaN(Number(localNodeId)) ? localNodeId : Number(localNodeId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a NodeLocatorId from components
|
||||
* @param subgraphUuid The UUID of the immediate containing subgraph
|
||||
* @param localNodeId The local node ID within that subgraph
|
||||
* @returns A properly formatted NodeLocatorId
|
||||
*/
|
||||
export function createNodeLocatorId(
|
||||
subgraphUuid: string,
|
||||
localNodeId: NodeId
|
||||
): NodeLocatorId {
|
||||
return `${subgraphUuid}:${localNodeId}` as NodeLocatorId
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a NodeExecutionId into its component node IDs
|
||||
* @param id The NodeExecutionId to parse
|
||||
* @returns Array of node IDs from root to target, or null if not an execution ID
|
||||
*/
|
||||
export function parseNodeExecutionId(id: string): NodeId[] | null {
|
||||
if (!isNodeExecutionId(id)) return null
|
||||
|
||||
return id
|
||||
.split(':')
|
||||
.map((part) => (isNaN(Number(part)) ? part : Number(part)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a NodeExecutionId from an array of node IDs
|
||||
* @param nodeIds Array of node IDs from root to target
|
||||
* @returns A properly formatted NodeExecutionId
|
||||
*/
|
||||
export function createNodeExecutionId(nodeIds: NodeId[]): NodeExecutionId {
|
||||
return nodeIds.join(':') as NodeExecutionId
|
||||
}
|
||||
@@ -3,20 +3,12 @@ import { nextTick, reactive } from 'vue'
|
||||
|
||||
import { useBrowserTabTitle } from '@/composables/useBrowserTabTitle'
|
||||
|
||||
// Mock i18n module
|
||||
vi.mock('@/i18n', () => ({
|
||||
t: (key: string, fallback: string) =>
|
||||
key === 'g.nodesRunning' ? 'nodes running' : fallback
|
||||
}))
|
||||
|
||||
// Mock the execution store
|
||||
const executionStore = reactive({
|
||||
isIdle: true,
|
||||
executionProgress: 0,
|
||||
executingNode: null as any,
|
||||
executingNodeProgress: 0,
|
||||
nodeProgressStates: {} as any,
|
||||
activePrompt: null as any
|
||||
executingNodeProgress: 0
|
||||
})
|
||||
vi.mock('@/stores/executionStore', () => ({
|
||||
useExecutionStore: () => executionStore
|
||||
@@ -45,8 +37,6 @@ describe('useBrowserTabTitle', () => {
|
||||
executionStore.executionProgress = 0
|
||||
executionStore.executingNode = null as any
|
||||
executionStore.executingNodeProgress = 0
|
||||
executionStore.nodeProgressStates = {}
|
||||
executionStore.activePrompt = null
|
||||
|
||||
// reset setting and workflow stores
|
||||
;(settingStore.get as any).mockReturnValue('Enabled')
|
||||
@@ -107,41 +97,13 @@ describe('useBrowserTabTitle', () => {
|
||||
expect(document.title).toBe('[30%]ComfyUI')
|
||||
})
|
||||
|
||||
it('shows node execution title when executing a node using nodeProgressStates', async () => {
|
||||
it('shows node execution title when executing a node', async () => {
|
||||
executionStore.isIdle = false
|
||||
executionStore.executionProgress = 0.4
|
||||
executionStore.nodeProgressStates = {
|
||||
'1': { state: 'running', value: 5, max: 10, node: '1', prompt_id: 'test' }
|
||||
}
|
||||
executionStore.activePrompt = {
|
||||
workflow: {
|
||||
changeTracker: {
|
||||
activeState: {
|
||||
nodes: [{ id: 1, type: 'Foo' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
executionStore.executingNodeProgress = 0.5
|
||||
executionStore.executingNode = { type: 'Foo' }
|
||||
useBrowserTabTitle()
|
||||
await nextTick()
|
||||
expect(document.title).toBe('[40%][50%] Foo')
|
||||
})
|
||||
|
||||
it('shows multiple nodes running when multiple nodes are executing', async () => {
|
||||
executionStore.isIdle = false
|
||||
executionStore.executionProgress = 0.4
|
||||
executionStore.nodeProgressStates = {
|
||||
'1': {
|
||||
state: 'running',
|
||||
value: 5,
|
||||
max: 10,
|
||||
node: '1',
|
||||
prompt_id: 'test'
|
||||
},
|
||||
'2': { state: 'running', value: 8, max: 10, node: '2', prompt_id: 'test' }
|
||||
}
|
||||
useBrowserTabTitle()
|
||||
await nextTick()
|
||||
expect(document.title).toBe('[40%][2 nodes running]')
|
||||
})
|
||||
})
|
||||
|
||||
192
tests-ui/tests/constants/urlConstants.test.ts
Normal file
192
tests-ui/tests/constants/urlConstants.test.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { COMFY_BASE_DOMAIN, COMFY_WEBSITE_URLS } from '@/config/comfyDomain'
|
||||
import { PYPI_MIRROR, PYTHON_MIRROR } from '@/constants/mirrors'
|
||||
import {
|
||||
COMFY_URLS,
|
||||
DEVELOPER_TOOLS,
|
||||
GITHUB_REPOS,
|
||||
MODEL_SOURCES,
|
||||
getDesktopGuideUrl
|
||||
} from '@/constants/urls'
|
||||
|
||||
describe('URL Constants', () => {
|
||||
describe('URL Format Validation', () => {
|
||||
it('should have valid HTTPS URLs throughout', () => {
|
||||
const httpsPattern =
|
||||
/^https:\/\/[a-z0-9]+([-.][a-z0-9]+)*\.[a-z]{2,}(:[0-9]{1,5})?(\/.*)?$/i
|
||||
|
||||
// Test COMFY_URLS
|
||||
expect(COMFY_URLS.website.base).toMatch(httpsPattern)
|
||||
expect(COMFY_URLS.docs.base).toMatch(httpsPattern)
|
||||
expect(COMFY_URLS.community.discord).toMatch(httpsPattern)
|
||||
|
||||
// Test GITHUB_REPOS
|
||||
Object.values(GITHUB_REPOS).forEach((url) => {
|
||||
expect(url).toMatch(httpsPattern)
|
||||
})
|
||||
|
||||
// Test MODEL_SOURCES
|
||||
Object.values(MODEL_SOURCES.repos).forEach((url) => {
|
||||
expect(url).toMatch(httpsPattern)
|
||||
})
|
||||
|
||||
// Test DEVELOPER_TOOLS
|
||||
Object.values(DEVELOPER_TOOLS).forEach((url) => {
|
||||
expect(url).toMatch(httpsPattern)
|
||||
})
|
||||
})
|
||||
|
||||
it('should have proper GitHub URL format', () => {
|
||||
const githubPattern = /^https:\/\/github\.com\/[\w-]+\/[\w-]+(\/[\w-]+)?$/
|
||||
|
||||
expect(GITHUB_REPOS.comfyui).toMatch(githubPattern)
|
||||
expect(GITHUB_REPOS.comfyuiIssues).toMatch(githubPattern)
|
||||
expect(GITHUB_REPOS.frontend).toMatch(githubPattern)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Mirror Configuration', () => {
|
||||
it('should have valid mirror URLs', () => {
|
||||
const urlPattern =
|
||||
/^https?:\/\/[a-z0-9]+([-.][a-z0-9]+)*\.[a-z]{2,}(:[0-9]{1,5})?(\/.*)?$/i
|
||||
|
||||
expect(PYTHON_MIRROR.mirror).toMatch(urlPattern)
|
||||
expect(PYTHON_MIRROR.fallbackMirror).toMatch(urlPattern)
|
||||
expect(PYPI_MIRROR.mirror).toMatch(urlPattern)
|
||||
expect(PYPI_MIRROR.fallbackMirror).toMatch(urlPattern)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Domain Configuration', () => {
|
||||
it('should have valid domain format', () => {
|
||||
const domainPattern = /^[a-z0-9]+([-.][a-z0-9]+)*\.[a-z]{2,}$/i
|
||||
expect(COMFY_BASE_DOMAIN).toMatch(domainPattern)
|
||||
})
|
||||
|
||||
it('should construct proper website URLs from base domain', () => {
|
||||
const expectedBase = `https://www.${COMFY_BASE_DOMAIN}`
|
||||
expect(COMFY_WEBSITE_URLS.base).toBe(expectedBase)
|
||||
expect(COMFY_WEBSITE_URLS.termsOfService).toBe(
|
||||
`${expectedBase}/terms-of-service`
|
||||
)
|
||||
expect(COMFY_WEBSITE_URLS.privacy).toBe(`${expectedBase}/privacy`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Localization', () => {
|
||||
it('should handle valid language codes in getLocalized', () => {
|
||||
const validLanguageCodes = ['en', 'zh', 'ja', 'ko', 'es', 'fr', 'de']
|
||||
|
||||
validLanguageCodes.forEach((lang) => {
|
||||
const result = COMFY_URLS.docs.getLocalized('test-path', lang)
|
||||
expect(result).toMatch(
|
||||
/^https:\/\/docs\.comfy\.org\/(([a-z]{2}-[A-Z]{2}\/)?test-path)$/
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should properly format localized paths', () => {
|
||||
expect(COMFY_URLS.docs.getLocalized('test-path', 'en')).toBe(
|
||||
'https://docs.comfy.org/test-path'
|
||||
)
|
||||
expect(COMFY_URLS.docs.getLocalized('test-path', 'zh')).toBe(
|
||||
'https://docs.comfy.org/zh-CN/test-path'
|
||||
)
|
||||
expect(COMFY_URLS.docs.getLocalized('', 'en')).toBe(
|
||||
'https://docs.comfy.org/'
|
||||
)
|
||||
expect(COMFY_URLS.docs.getLocalized('', 'zh')).toBe(
|
||||
'https://docs.comfy.org/zh-CN/'
|
||||
)
|
||||
})
|
||||
|
||||
it('should generate platform and locale-aware desktop guide URLs', () => {
|
||||
// Mock navigator for testing
|
||||
const originalNavigator = global.navigator
|
||||
|
||||
// Test Windows platform
|
||||
Object.defineProperty(global, 'navigator', {
|
||||
value: { platform: 'Win32' },
|
||||
writable: true
|
||||
})
|
||||
|
||||
const winUrl = getDesktopGuideUrl('en')
|
||||
expect(winUrl).toBe('https://docs.comfy.org/installation/desktop/windows')
|
||||
|
||||
// Test macOS platform
|
||||
Object.defineProperty(global, 'navigator', {
|
||||
value: { platform: 'MacIntel' },
|
||||
writable: true
|
||||
})
|
||||
|
||||
const macUrl = getDesktopGuideUrl('en')
|
||||
expect(macUrl).toBe('https://docs.comfy.org/installation/desktop/macos')
|
||||
|
||||
// Test Chinese locale with macOS
|
||||
const zhMacUrl = getDesktopGuideUrl('zh')
|
||||
expect(zhMacUrl).toBe(
|
||||
'https://docs.comfy.org/zh-CN/installation/desktop/macos'
|
||||
)
|
||||
|
||||
// Test Chinese locale with Windows
|
||||
Object.defineProperty(global, 'navigator', {
|
||||
value: { platform: 'Win32' },
|
||||
writable: true
|
||||
})
|
||||
|
||||
const zhWinUrl = getDesktopGuideUrl('zh')
|
||||
expect(zhWinUrl).toBe(
|
||||
'https://docs.comfy.org/zh-CN/installation/desktop/windows'
|
||||
)
|
||||
|
||||
// Test other locales default to English
|
||||
const frUrl = getDesktopGuideUrl('fr')
|
||||
expect(frUrl).toBe('https://docs.comfy.org/installation/desktop/windows')
|
||||
|
||||
// Test environment without navigator
|
||||
Object.defineProperty(global, 'navigator', {
|
||||
value: undefined,
|
||||
writable: true
|
||||
})
|
||||
|
||||
const noNavUrl = getDesktopGuideUrl('en')
|
||||
expect(noNavUrl).toBe(
|
||||
'https://docs.comfy.org/installation/desktop/windows'
|
||||
)
|
||||
|
||||
// Restore original navigator
|
||||
Object.defineProperty(global, 'navigator', {
|
||||
value: originalNavigator,
|
||||
writable: true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Security', () => {
|
||||
it('should only use secure HTTPS for external URLs', () => {
|
||||
const allUrls = [
|
||||
...Object.values(GITHUB_REPOS),
|
||||
...Object.values(MODEL_SOURCES.repos),
|
||||
...Object.values(DEVELOPER_TOOLS),
|
||||
COMFY_URLS.website.base,
|
||||
COMFY_URLS.docs.base,
|
||||
COMFY_URLS.community.discord
|
||||
]
|
||||
|
||||
allUrls.forEach((url) => {
|
||||
expect(url.startsWith('https://')).toBe(true)
|
||||
expect(url.startsWith('http://')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('should have valid URL allowlist for model sources', () => {
|
||||
const urlPattern =
|
||||
/^https?:\/\/([a-z0-9]+([-.][a-z0-9]+)*\.[a-z]{2,}|localhost)(:[0-9]+)?\/?.*$/i
|
||||
|
||||
MODEL_SOURCES.allowedDomains.forEach((url) => {
|
||||
expect(url).toMatch(urlPattern)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,22 +1,11 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
|
||||
// Mock the workflowStore
|
||||
vi.mock('@/stores/workflowStore', () => ({
|
||||
useWorkflowStore: vi.fn(() => ({
|
||||
nodeExecutionIdToNodeLocatorId: vi.fn(),
|
||||
nodeIdToNodeLocatorId: vi.fn(),
|
||||
nodeLocatorIdToNodeExecutionId: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
// Remove any previous global types
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
// Empty interface to override any previous declarations
|
||||
interface Window {}
|
||||
}
|
||||
|
||||
@@ -33,16 +22,12 @@ vi.mock('@/composables/node/useNodeProgressText', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
// Mock the app import with proper implementation
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
graph: {
|
||||
getNodeById: vi.fn()
|
||||
},
|
||||
revokePreviews: vi.fn(),
|
||||
nodePreviewImages: {}
|
||||
// Create a local mock instead of using global to avoid conflicts
|
||||
const mockApp = {
|
||||
graph: {
|
||||
getNodeById: vi.fn()
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
describe('executionStore - display_component handling', () => {
|
||||
function createDisplayComponentEvent(
|
||||
@@ -62,7 +47,7 @@ describe('executionStore - display_component handling', () => {
|
||||
|
||||
function handleDisplayComponentMessage(event: CustomEvent) {
|
||||
const { node_id, component } = event.detail
|
||||
const node = vi.mocked(app.graph.getNodeById)(node_id)
|
||||
const node = mockApp.graph.getNodeById(node_id)
|
||||
if (node && component === 'ChatHistoryWidget') {
|
||||
mockShowChatHistory(node)
|
||||
}
|
||||
@@ -75,121 +60,23 @@ describe('executionStore - display_component handling', () => {
|
||||
})
|
||||
|
||||
it('handles ChatHistoryWidget display_component messages', () => {
|
||||
const mockNode = { id: '123' } as any
|
||||
vi.mocked(app.graph.getNodeById).mockReturnValue(mockNode)
|
||||
const mockNode = { id: '123' }
|
||||
mockApp.graph.getNodeById.mockReturnValue(mockNode)
|
||||
|
||||
const event = createDisplayComponentEvent('123')
|
||||
handleDisplayComponentMessage(event)
|
||||
|
||||
expect(app.graph.getNodeById).toHaveBeenCalledWith('123')
|
||||
expect(mockApp.graph.getNodeById).toHaveBeenCalledWith('123')
|
||||
expect(mockShowChatHistory).toHaveBeenCalledWith(mockNode)
|
||||
})
|
||||
|
||||
it('does nothing if node is not found', () => {
|
||||
vi.mocked(app.graph.getNodeById).mockReturnValue(null)
|
||||
mockApp.graph.getNodeById.mockReturnValue(null)
|
||||
|
||||
const event = createDisplayComponentEvent('non-existent')
|
||||
handleDisplayComponentMessage(event)
|
||||
|
||||
expect(app.graph.getNodeById).toHaveBeenCalledWith('non-existent')
|
||||
expect(mockApp.graph.getNodeById).toHaveBeenCalledWith('non-existent')
|
||||
expect(mockShowChatHistory).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('useExecutionStore - NodeLocatorId conversions', () => {
|
||||
let store: ReturnType<typeof useExecutionStore>
|
||||
let workflowStore: ReturnType<typeof useWorkflowStore>
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
|
||||
// Create the mock workflowStore instance
|
||||
const mockWorkflowStore = {
|
||||
nodeExecutionIdToNodeLocatorId: vi.fn(),
|
||||
nodeIdToNodeLocatorId: vi.fn(),
|
||||
nodeLocatorIdToNodeExecutionId: vi.fn()
|
||||
}
|
||||
|
||||
// Mock the useWorkflowStore function to return our mock
|
||||
vi.mocked(useWorkflowStore).mockReturnValue(mockWorkflowStore as any)
|
||||
|
||||
workflowStore = mockWorkflowStore as any
|
||||
store = useExecutionStore()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('executionIdToNodeLocatorId', () => {
|
||||
it('should convert execution ID to NodeLocatorId', () => {
|
||||
// Mock subgraph structure
|
||||
const mockSubgraph = {
|
||||
id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
_nodes: []
|
||||
}
|
||||
|
||||
const mockNode = {
|
||||
id: 123,
|
||||
isSubgraphNode: () => true,
|
||||
subgraph: mockSubgraph
|
||||
} as any
|
||||
|
||||
// Mock app.graph.getNodeById to return the mock node
|
||||
vi.mocked(app.graph.getNodeById).mockReturnValue(mockNode)
|
||||
|
||||
const result = store.executionIdToNodeLocatorId('123:456')
|
||||
|
||||
expect(result).toBe('a1b2c3d4-e5f6-7890-abcd-ef1234567890:456')
|
||||
})
|
||||
|
||||
it('should convert simple node ID to NodeLocatorId', () => {
|
||||
const result = store.executionIdToNodeLocatorId('123')
|
||||
|
||||
// For simple node IDs, it should return the ID as-is
|
||||
expect(result).toBe('123')
|
||||
})
|
||||
|
||||
it('should handle numeric node IDs', () => {
|
||||
const result = store.executionIdToNodeLocatorId(123)
|
||||
|
||||
// For numeric IDs, it should convert to string and return as-is
|
||||
expect(result).toBe('123')
|
||||
})
|
||||
|
||||
it('should return null when conversion fails', () => {
|
||||
// Mock app.graph.getNodeById to return null (node not found)
|
||||
vi.mocked(app.graph.getNodeById).mockReturnValue(null)
|
||||
|
||||
// This should throw an error as the node is not found
|
||||
expect(() => store.executionIdToNodeLocatorId('999:456')).toThrow(
|
||||
'Subgraph not found: 999'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('nodeLocatorIdToExecutionId', () => {
|
||||
it('should convert NodeLocatorId to execution ID', () => {
|
||||
const mockExecutionId = '123:456'
|
||||
vi.spyOn(workflowStore, 'nodeLocatorIdToNodeExecutionId').mockReturnValue(
|
||||
mockExecutionId as any
|
||||
)
|
||||
|
||||
const result = store.nodeLocatorIdToExecutionId(
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890:456'
|
||||
)
|
||||
|
||||
expect(workflowStore.nodeLocatorIdToNodeExecutionId).toHaveBeenCalledWith(
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890:456'
|
||||
)
|
||||
expect(result).toBe(mockExecutionId)
|
||||
})
|
||||
|
||||
it('should return null when conversion fails', () => {
|
||||
vi.spyOn(workflowStore, 'nodeLocatorIdToNodeExecutionId').mockReturnValue(
|
||||
null
|
||||
)
|
||||
|
||||
const result = store.nodeLocatorIdToExecutionId('invalid:format')
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Subgraph } from '@comfyorg/litegraph'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
@@ -12,7 +11,6 @@ import {
|
||||
useWorkflowBookmarkStore,
|
||||
useWorkflowStore
|
||||
} from '@/stores/workflowStore'
|
||||
import { isSubgraph } from '@/utils/typeGuardUtil'
|
||||
|
||||
// Add mock for api at the top of the file
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
@@ -28,15 +26,10 @@ vi.mock('@/scripts/api', () => ({
|
||||
// Mock comfyApp globally for the store setup
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
canvas: {} // Start with empty canvas object
|
||||
canvas: null // Start with canvas potentially undefined or null
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock isSubgraph
|
||||
vi.mock('@/utils/typeGuardUtil', () => ({
|
||||
isSubgraph: vi.fn(() => false)
|
||||
}))
|
||||
|
||||
describe('useWorkflowStore', () => {
|
||||
let store: ReturnType<typeof useWorkflowStore>
|
||||
let bookmarkStore: ReturnType<typeof useWorkflowBookmarkStore>
|
||||
@@ -525,13 +518,8 @@ describe('useWorkflowStore', () => {
|
||||
{ name: 'Level 1 Subgraph' },
|
||||
{ name: 'Level 2 Subgraph' }
|
||||
]
|
||||
} as any
|
||||
vi.mocked(comfyApp.canvas).subgraph = mockSubgraph
|
||||
|
||||
// Mock isSubgraph to return true for our mockSubgraph
|
||||
vi.mocked(isSubgraph).mockImplementation(
|
||||
(obj): obj is Subgraph => obj === mockSubgraph
|
||||
)
|
||||
}
|
||||
vi.mocked(comfyApp.canvas).subgraph = mockSubgraph as any
|
||||
|
||||
// Act: Trigger the update
|
||||
store.updateActiveGraph()
|
||||
@@ -548,13 +536,8 @@ describe('useWorkflowStore', () => {
|
||||
name: 'Initial Subgraph',
|
||||
pathToRootGraph: [{ name: 'Root' }, { name: 'Initial Subgraph' }],
|
||||
isRootGraph: false
|
||||
} as any
|
||||
vi.mocked(comfyApp.canvas).subgraph = initialSubgraph
|
||||
|
||||
// Mock isSubgraph to return true for our initialSubgraph
|
||||
vi.mocked(isSubgraph).mockImplementation(
|
||||
(obj): obj is Subgraph => obj === initialSubgraph
|
||||
)
|
||||
}
|
||||
vi.mocked(comfyApp.canvas).subgraph = initialSubgraph as any
|
||||
|
||||
// Trigger initial update based on the *first* workflow opened in beforeEach
|
||||
store.updateActiveGraph()
|
||||
@@ -578,11 +561,6 @@ describe('useWorkflowStore', () => {
|
||||
// This ensures the watcher *does* cause a state change we can assert
|
||||
vi.mocked(comfyApp.canvas).subgraph = undefined
|
||||
|
||||
// Mock isSubgraph to return false for undefined
|
||||
vi.mocked(isSubgraph).mockImplementation(
|
||||
(_obj): _obj is Subgraph => false
|
||||
)
|
||||
|
||||
await store.openWorkflow(workflow2) // This changes activeWorkflow and triggers the watch
|
||||
await nextTick() // Allow watcher and potential async operations in updateActiveGraph to complete
|
||||
|
||||
@@ -591,131 +569,4 @@ describe('useWorkflowStore', () => {
|
||||
expect(store.activeSubgraph).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('NodeLocatorId conversions', () => {
|
||||
beforeEach(() => {
|
||||
// Setup mock graph structure with subgraphs
|
||||
const mockSubgraph = {
|
||||
id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
_nodes: []
|
||||
}
|
||||
|
||||
const mockNode = {
|
||||
id: 123,
|
||||
isSubgraphNode: () => true,
|
||||
subgraph: mockSubgraph
|
||||
}
|
||||
|
||||
const mockRootGraph = {
|
||||
_nodes: [mockNode],
|
||||
subgraphs: new Map([[mockSubgraph.id, mockSubgraph]]),
|
||||
getNodeById: (id: string | number) => {
|
||||
if (String(id) === '123') return mockNode
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
vi.mocked(comfyApp).graph = mockRootGraph as any
|
||||
vi.mocked(comfyApp.canvas).subgraph = mockSubgraph as any
|
||||
store.activeSubgraph = mockSubgraph as any
|
||||
})
|
||||
|
||||
describe('nodeIdToNodeLocatorId', () => {
|
||||
it('should convert node ID to NodeLocatorId for subgraph nodes', () => {
|
||||
const result = store.nodeIdToNodeLocatorId(456)
|
||||
expect(result).toBe('a1b2c3d4-e5f6-7890-abcd-ef1234567890:456')
|
||||
})
|
||||
|
||||
it('should return simple node ID for root graph nodes', () => {
|
||||
store.activeSubgraph = undefined
|
||||
const result = store.nodeIdToNodeLocatorId(123)
|
||||
expect(result).toBe('123')
|
||||
})
|
||||
|
||||
it('should use provided subgraph instead of active one', () => {
|
||||
const customSubgraph = {
|
||||
id: 'custom-uuid-1234-5678-90ab-cdef12345678'
|
||||
} as any
|
||||
const result = store.nodeIdToNodeLocatorId(789, customSubgraph)
|
||||
expect(result).toBe('custom-uuid-1234-5678-90ab-cdef12345678:789')
|
||||
})
|
||||
})
|
||||
|
||||
describe('nodeExecutionIdToNodeLocatorId', () => {
|
||||
it('should convert execution ID to NodeLocatorId', () => {
|
||||
const result = store.nodeExecutionIdToNodeLocatorId('123:456')
|
||||
expect(result).toBe('a1b2c3d4-e5f6-7890-abcd-ef1234567890:456')
|
||||
})
|
||||
|
||||
it('should return simple node ID for root level nodes', () => {
|
||||
const result = store.nodeExecutionIdToNodeLocatorId('123')
|
||||
expect(result).toBe('123')
|
||||
})
|
||||
|
||||
it('should return null for invalid execution IDs', () => {
|
||||
const result = store.nodeExecutionIdToNodeLocatorId('999:456')
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('nodeLocatorIdToNodeId', () => {
|
||||
it('should extract node ID from NodeLocatorId', () => {
|
||||
const result = store.nodeLocatorIdToNodeId(
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890:456'
|
||||
)
|
||||
expect(result).toBe(456)
|
||||
})
|
||||
|
||||
it('should handle string node IDs', () => {
|
||||
const result = store.nodeLocatorIdToNodeId(
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890:node_1'
|
||||
)
|
||||
expect(result).toBe('node_1')
|
||||
})
|
||||
|
||||
it('should handle simple node IDs (root graph)', () => {
|
||||
const result = store.nodeLocatorIdToNodeId('123')
|
||||
expect(result).toBe(123)
|
||||
|
||||
const stringResult = store.nodeLocatorIdToNodeId('node_1')
|
||||
expect(stringResult).toBe('node_1')
|
||||
})
|
||||
|
||||
it('should return null for invalid NodeLocatorId', () => {
|
||||
const result = store.nodeLocatorIdToNodeId('invalid:format')
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('nodeLocatorIdToNodeExecutionId', () => {
|
||||
it('should convert NodeLocatorId to execution ID', () => {
|
||||
// Need to mock isSubgraph to identify our mockSubgraph
|
||||
vi.mocked(isSubgraph).mockImplementation((obj): obj is Subgraph => {
|
||||
return obj === store.activeSubgraph
|
||||
})
|
||||
|
||||
const result = store.nodeLocatorIdToNodeExecutionId(
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890:456'
|
||||
)
|
||||
expect(result).toBe('123:456')
|
||||
})
|
||||
|
||||
it('should handle simple node IDs (root graph)', () => {
|
||||
const result = store.nodeLocatorIdToNodeExecutionId('123')
|
||||
expect(result).toBe('123')
|
||||
})
|
||||
|
||||
it('should return null for unknown subgraph UUID', () => {
|
||||
const result = store.nodeLocatorIdToNodeExecutionId(
|
||||
'unknown-uuid-1234-5678-90ab-cdef12345678:456'
|
||||
)
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should return null for invalid NodeLocatorId', () => {
|
||||
const result = store.nodeLocatorIdToNodeExecutionId('invalid:format')
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import {
|
||||
type NodeLocatorId,
|
||||
createNodeExecutionId,
|
||||
createNodeLocatorId,
|
||||
isNodeExecutionId,
|
||||
isNodeLocatorId,
|
||||
parseNodeExecutionId,
|
||||
parseNodeLocatorId
|
||||
} from '@/types/nodeIdentification'
|
||||
|
||||
describe('nodeIdentification', () => {
|
||||
describe('NodeLocatorId', () => {
|
||||
const validUuid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
|
||||
const validNodeId = '123'
|
||||
const validNodeLocatorId = `${validUuid}:${validNodeId}` as NodeLocatorId
|
||||
|
||||
describe('isNodeLocatorId', () => {
|
||||
it('should return true for valid NodeLocatorId', () => {
|
||||
expect(isNodeLocatorId(validNodeLocatorId)).toBe(true)
|
||||
expect(isNodeLocatorId(`${validUuid}:456`)).toBe(true)
|
||||
expect(isNodeLocatorId(`${validUuid}:node_1`)).toBe(true)
|
||||
// Simple node IDs (root graph)
|
||||
expect(isNodeLocatorId('123')).toBe(true)
|
||||
expect(isNodeLocatorId('node_1')).toBe(true)
|
||||
expect(isNodeLocatorId('5')).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for invalid formats', () => {
|
||||
expect(isNodeLocatorId('123:456')).toBe(false) // No UUID in first part
|
||||
expect(isNodeLocatorId('not-a-uuid:123')).toBe(false)
|
||||
expect(isNodeLocatorId('')).toBe(false) // Empty string
|
||||
expect(isNodeLocatorId(':123')).toBe(false) // Empty UUID
|
||||
expect(isNodeLocatorId(`${validUuid}:`)).toBe(false) // Empty node ID
|
||||
expect(isNodeLocatorId(`${validUuid}:123:456`)).toBe(false) // Too many parts
|
||||
expect(isNodeLocatorId(123)).toBe(false) // Not a string
|
||||
expect(isNodeLocatorId(null)).toBe(false)
|
||||
expect(isNodeLocatorId(undefined)).toBe(false)
|
||||
})
|
||||
|
||||
it('should validate UUID format correctly', () => {
|
||||
// Valid UUID formats
|
||||
expect(
|
||||
isNodeLocatorId('00000000-0000-0000-0000-000000000000:123')
|
||||
).toBe(true)
|
||||
expect(
|
||||
isNodeLocatorId('A1B2C3D4-E5F6-7890-ABCD-EF1234567890:123')
|
||||
).toBe(true)
|
||||
|
||||
// Invalid UUID formats
|
||||
expect(isNodeLocatorId('00000000-0000-0000-0000-00000000000:123')).toBe(
|
||||
false
|
||||
) // Too short
|
||||
expect(
|
||||
isNodeLocatorId('00000000-0000-0000-0000-0000000000000:123')
|
||||
).toBe(false) // Too long
|
||||
expect(
|
||||
isNodeLocatorId('00000000_0000_0000_0000_000000000000:123')
|
||||
).toBe(false) // Wrong separator
|
||||
expect(
|
||||
isNodeLocatorId('g0000000-0000-0000-0000-000000000000:123')
|
||||
).toBe(false) // Invalid hex
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseNodeLocatorId', () => {
|
||||
it('should parse valid NodeLocatorId', () => {
|
||||
const result = parseNodeLocatorId(validNodeLocatorId)
|
||||
expect(result).toEqual({
|
||||
subgraphUuid: validUuid,
|
||||
localNodeId: 123
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle string node IDs', () => {
|
||||
const stringNodeId = `${validUuid}:node_1`
|
||||
const result = parseNodeLocatorId(stringNodeId)
|
||||
expect(result).toEqual({
|
||||
subgraphUuid: validUuid,
|
||||
localNodeId: 'node_1'
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle simple node IDs (root graph)', () => {
|
||||
const result = parseNodeLocatorId('123')
|
||||
expect(result).toEqual({
|
||||
subgraphUuid: null,
|
||||
localNodeId: 123
|
||||
})
|
||||
|
||||
const stringResult = parseNodeLocatorId('node_1')
|
||||
expect(stringResult).toEqual({
|
||||
subgraphUuid: null,
|
||||
localNodeId: 'node_1'
|
||||
})
|
||||
})
|
||||
|
||||
it('should return null for invalid formats', () => {
|
||||
expect(parseNodeLocatorId('123:456')).toBeNull() // No UUID in first part
|
||||
expect(parseNodeLocatorId('')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('createNodeLocatorId', () => {
|
||||
it('should create NodeLocatorId from components', () => {
|
||||
const result = createNodeLocatorId(validUuid, 123)
|
||||
expect(result).toBe(validNodeLocatorId)
|
||||
expect(isNodeLocatorId(result)).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle string node IDs', () => {
|
||||
const result = createNodeLocatorId(validUuid, 'node_1')
|
||||
expect(result).toBe(`${validUuid}:node_1`)
|
||||
expect(isNodeLocatorId(result)).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('NodeExecutionId', () => {
|
||||
describe('isNodeExecutionId', () => {
|
||||
it('should return true for execution IDs', () => {
|
||||
expect(isNodeExecutionId('123:456')).toBe(true)
|
||||
expect(isNodeExecutionId('123:456:789')).toBe(true)
|
||||
expect(isNodeExecutionId('node_1:node_2')).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for non-execution IDs', () => {
|
||||
expect(isNodeExecutionId('123')).toBe(false)
|
||||
expect(isNodeExecutionId('node_1')).toBe(false)
|
||||
expect(isNodeExecutionId('')).toBe(false)
|
||||
expect(isNodeExecutionId(123)).toBe(false)
|
||||
expect(isNodeExecutionId(null)).toBe(false)
|
||||
expect(isNodeExecutionId(undefined)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseNodeExecutionId', () => {
|
||||
it('should parse execution IDs correctly', () => {
|
||||
expect(parseNodeExecutionId('123:456')).toEqual([123, 456])
|
||||
expect(parseNodeExecutionId('123:456:789')).toEqual([123, 456, 789])
|
||||
expect(parseNodeExecutionId('node_1:node_2')).toEqual([
|
||||
'node_1',
|
||||
'node_2'
|
||||
])
|
||||
expect(parseNodeExecutionId('123:node_2:456')).toEqual([
|
||||
123,
|
||||
'node_2',
|
||||
456
|
||||
])
|
||||
})
|
||||
|
||||
it('should return null for non-execution IDs', () => {
|
||||
expect(parseNodeExecutionId('123')).toBeNull()
|
||||
expect(parseNodeExecutionId('')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('createNodeExecutionId', () => {
|
||||
it('should create execution IDs from node arrays', () => {
|
||||
expect(createNodeExecutionId([123, 456])).toBe('123:456')
|
||||
expect(createNodeExecutionId([123, 456, 789])).toBe('123:456:789')
|
||||
expect(createNodeExecutionId(['node_1', 'node_2'])).toBe(
|
||||
'node_1:node_2'
|
||||
)
|
||||
expect(createNodeExecutionId([123, 'node_2', 456])).toBe(
|
||||
'123:node_2:456'
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle single node ID', () => {
|
||||
const result = createNodeExecutionId([123])
|
||||
expect(result).toBe('123')
|
||||
// Single node IDs are not execution IDs
|
||||
expect(isNodeExecutionId(result)).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle empty array', () => {
|
||||
expect(createNodeExecutionId([])).toBe('')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Integration tests', () => {
|
||||
it('should round-trip NodeLocatorId correctly', () => {
|
||||
const uuid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
|
||||
const nodeId: NodeId = 123
|
||||
|
||||
const locatorId = createNodeLocatorId(uuid, nodeId)
|
||||
const parsed = parseNodeLocatorId(locatorId)
|
||||
|
||||
expect(parsed).toBeTruthy()
|
||||
expect(parsed!.subgraphUuid).toBe(uuid)
|
||||
expect(parsed!.localNodeId).toBe(nodeId)
|
||||
})
|
||||
|
||||
it('should round-trip NodeExecutionId correctly', () => {
|
||||
const nodeIds: NodeId[] = [123, 'node_2', 456]
|
||||
|
||||
const executionId = createNodeExecutionId(nodeIds)
|
||||
const parsed = parseNodeExecutionId(executionId)
|
||||
|
||||
expect(parsed).toEqual(nodeIds)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user