Compare commits
96 Commits
test2
...
queue-over
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f52357f2a | ||
|
|
eb5327b4aa | ||
|
|
7c5859e186 | ||
|
|
d48aabbe4c | ||
|
|
04de43f90c | ||
|
|
51d2046c7d | ||
|
|
fdaba3a293 | ||
|
|
b10e67f6aa | ||
|
|
66f4dac7ca | ||
|
|
53dbca9fea | ||
|
|
18303fafce | ||
|
|
73678e0220 | ||
|
|
24ab32d9b4 | ||
|
|
d7d03bed8b | ||
|
|
4a8975ad27 | ||
|
|
c6737730f6 | ||
|
|
fd9747375d | ||
|
|
5187a77234 | ||
|
|
b22ba97a13 | ||
|
|
aab9a30511 | ||
|
|
b0f5a9ffe2 | ||
|
|
3f777fb54f | ||
|
|
6a73288ce3 | ||
|
|
d21ea0f65b | ||
|
|
7613e70f63 | ||
|
|
56b67085d0 | ||
|
|
4a91330e30 | ||
|
|
a1a507ed09 | ||
|
|
a7c694f248 | ||
|
|
0646bb368a | ||
|
|
a8ef7a602f | ||
|
|
9c157296be | ||
|
|
3e97225ff6 | ||
|
|
1cdea3063d | ||
|
|
b5ab45673a | ||
|
|
7d326cbc14 | ||
|
|
6b1bd4be16 | ||
|
|
57eee5c218 | ||
|
|
caca6c4163 | ||
|
|
0385a7de9b | ||
|
|
e41c6934db | ||
|
|
2a68dbff5a | ||
|
|
2957d9897f | ||
|
|
f2a0e5102e | ||
|
|
88bdc605a7 | ||
|
|
c1808db7c4 | ||
|
|
514c437a38 | ||
|
|
18b133d22f | ||
|
|
3e8a83547d | ||
|
|
91adcaf276 | ||
|
|
03e9dd4789 | ||
|
|
ac8c3847d2 | ||
|
|
c88fc99a86 | ||
|
|
3dd805a30e | ||
|
|
7c830a2f0b | ||
|
|
e7756eb6dd | ||
|
|
c83f3ff1a7 | ||
|
|
bf8d9de1c1 | ||
|
|
b9f75b6cc8 | ||
|
|
3c8b7b015c | ||
|
|
ee476842a3 | ||
|
|
97fa128999 | ||
|
|
1e22c9067d | ||
|
|
273e39bbd1 | ||
|
|
ca5f24fcd9 | ||
|
|
8ba8b21fa0 | ||
|
|
1522622427 | ||
|
|
d83c3122ab | ||
|
|
c99865ce7f | ||
|
|
29af56c154 | ||
|
|
a65e63a322 | ||
|
|
8e28dda85c | ||
|
|
a7de97470b | ||
|
|
5fad29ed37 | ||
|
|
ea59fb5fc3 | ||
|
|
5cba1e3f88 | ||
|
|
c8f88d5ba7 | ||
|
|
f5f0e20332 | ||
|
|
b6efc52bf8 | ||
|
|
1b2df19f1b | ||
|
|
0eba638a0f | ||
|
|
d60ecbb3c3 | ||
|
|
969466c0a0 | ||
|
|
87244a6954 | ||
|
|
0eb2b9171a | ||
|
|
24ee353465 | ||
|
|
73e09a7fff | ||
|
|
9863aa6321 | ||
|
|
f548af2f1d | ||
|
|
987dcb189d | ||
|
|
fceb0017ce | ||
|
|
6156e22bac | ||
|
|
62f9e91724 | ||
|
|
e83cf0f5f6 | ||
|
|
c24e2ab5ba | ||
|
|
72b5444d5a |
26
.github/workflows/ci-shell-validation.yaml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# Description: Runs shellcheck on tracked shell scripts when they change
|
||||
name: "CI: Shell Validation"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '**/*.sh'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**/*.sh'
|
||||
|
||||
jobs:
|
||||
shell-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install shellcheck
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y shellcheck
|
||||
|
||||
- name: Run shellcheck
|
||||
run: bash ./scripts/cicd/check-shell.sh
|
||||
4
.github/workflows/pr-backport.yaml
vendored
@@ -16,6 +16,10 @@ on:
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
concurrency:
|
||||
group: backport-${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
if: >
|
||||
|
||||
@@ -124,12 +124,16 @@ jobs:
|
||||
- name: Stage changed snapshot files
|
||||
id: changed-snapshots
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "=========================================="
|
||||
echo "STAGING CHANGED SNAPSHOTS (Shard ${{ matrix.shardIndex }})"
|
||||
echo "=========================================="
|
||||
|
||||
# Get list of changed snapshot files
|
||||
changed_files=$(git diff --name-only browser_tests/ 2>/dev/null | grep -E '\-snapshots/' || echo "")
|
||||
# Get list of changed snapshot files (including untracked/new files)
|
||||
changed_files=$( (
|
||||
git diff --name-only browser_tests/ 2>/dev/null || true
|
||||
git ls-files --others --exclude-standard browser_tests/ 2>/dev/null || true
|
||||
) | sort -u | grep -E '\-snapshots/' || true )
|
||||
|
||||
if [ -z "$changed_files" ]; then
|
||||
echo "No snapshot changes in this shard"
|
||||
@@ -151,6 +155,11 @@ jobs:
|
||||
# Strip 'browser_tests/' prefix to avoid double nesting
|
||||
echo "Copying changed files to staging directory..."
|
||||
while IFS= read -r file; do
|
||||
# Skip paths that no longer exist (e.g. deletions)
|
||||
if [ ! -f "$file" ]; then
|
||||
echo " → (skipped; not a file) $file"
|
||||
continue
|
||||
fi
|
||||
# Remove 'browser_tests/' prefix
|
||||
file_without_prefix="${file#browser_tests/}"
|
||||
# Create parent directories
|
||||
@@ -261,11 +270,19 @@ jobs:
|
||||
echo "CHANGES SUMMARY"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Changed files in browser_tests:"
|
||||
git diff --name-only browser_tests/ | head -20 || echo "No changes"
|
||||
echo ""
|
||||
echo "Total changes:"
|
||||
git diff --name-only browser_tests/ | wc -l || echo "0"
|
||||
echo "Changed files in browser_tests (including untracked):"
|
||||
CHANGES=$(git status --porcelain=v1 --untracked-files=all -- browser_tests/)
|
||||
if [ -z "$CHANGES" ]; then
|
||||
echo "No changes"
|
||||
echo ""
|
||||
echo "Total changes:"
|
||||
echo "0"
|
||||
else
|
||||
echo "$CHANGES" | head -50
|
||||
echo ""
|
||||
echo "Total changes:"
|
||||
echo "$CHANGES" | wc -l
|
||||
fi
|
||||
|
||||
- name: Commit updated expectations
|
||||
id: commit
|
||||
@@ -273,7 +290,7 @@ jobs:
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@github.com'
|
||||
|
||||
if git diff --quiet browser_tests/; then
|
||||
if [ -z "$(git status --porcelain=v1 --untracked-files=all -- browser_tests/)" ]; then
|
||||
echo "No changes to commit"
|
||||
echo "has-changes=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
|
||||
124
.github/workflows/release-version-bump.yaml
vendored
@@ -20,6 +20,13 @@ on:
|
||||
required: true
|
||||
default: 'main'
|
||||
type: string
|
||||
schedule:
|
||||
# 00:00 UTC ≈ 4:00 PM PST / 5:00 PM PDT on the previous calendar day
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
concurrency:
|
||||
group: release-version-bump
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
bump-version:
|
||||
@@ -29,15 +36,99 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Prepare inputs
|
||||
id: prepared-inputs
|
||||
shell: bash
|
||||
env:
|
||||
RAW_VERSION_TYPE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version_type || '' }}
|
||||
RAW_PRE_RELEASE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.pre_release || '' }}
|
||||
RAW_BRANCH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.branch || '' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION_TYPE="$RAW_VERSION_TYPE"
|
||||
PRE_RELEASE="$RAW_PRE_RELEASE"
|
||||
TARGET_BRANCH="$RAW_BRANCH"
|
||||
|
||||
if [[ -z "$VERSION_TYPE" ]]; then
|
||||
VERSION_TYPE='patch'
|
||||
fi
|
||||
|
||||
if [[ -z "$TARGET_BRANCH" ]]; then
|
||||
TARGET_BRANCH='main'
|
||||
fi
|
||||
|
||||
{
|
||||
echo "version_type=$VERSION_TYPE"
|
||||
echo "pre_release=$PRE_RELEASE"
|
||||
echo "branch=$TARGET_BRANCH"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Close stale nightly version bump PRs
|
||||
if: github.event_name == 'schedule'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
script: |
|
||||
const prefix = 'version-bump-'
|
||||
const closed = []
|
||||
const prs = await github.paginate(github.rest.pulls.list, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
per_page: 100
|
||||
})
|
||||
|
||||
for (const pr of prs) {
|
||||
if (!pr.head?.ref?.startsWith(prefix)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (pr.user?.login !== 'github-actions[bot]') {
|
||||
continue
|
||||
}
|
||||
|
||||
// Only clean up stale nightly PRs targeting main.
|
||||
// Adjust here if other target branches should be cleaned.
|
||||
if (pr.base?.ref !== 'main') {
|
||||
continue
|
||||
}
|
||||
|
||||
await github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pr.number,
|
||||
state: 'closed'
|
||||
})
|
||||
|
||||
try {
|
||||
await github.rest.git.deleteRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: `heads/${pr.head.ref}`
|
||||
})
|
||||
} catch (error) {
|
||||
if (![404, 422].includes(error.status)) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
closed.push(pr.number)
|
||||
}
|
||||
|
||||
core.info(`Closed ${closed.length} stale PR(s).`)
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
ref: ${{ steps.prepared-inputs.outputs.branch }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate branch exists
|
||||
env:
|
||||
TARGET_BRANCH: ${{ steps.prepared-inputs.outputs.branch }}
|
||||
run: |
|
||||
BRANCH="${{ github.event.inputs.branch }}"
|
||||
BRANCH="$TARGET_BRANCH"
|
||||
if ! git show-ref --verify --quiet "refs/heads/$BRANCH" && ! git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
|
||||
echo "❌ Branch '$BRANCH' does not exist"
|
||||
echo ""
|
||||
@@ -51,7 +142,7 @@ jobs:
|
||||
echo "✅ Branch '$BRANCH' exists"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
|
||||
with:
|
||||
version: 10
|
||||
|
||||
@@ -62,16 +153,31 @@ jobs:
|
||||
|
||||
- name: Bump version
|
||||
id: bump-version
|
||||
env:
|
||||
VERSION_TYPE: ${{ steps.prepared-inputs.outputs.version_type }}
|
||||
PRE_RELEASE: ${{ steps.prepared-inputs.outputs.pre_release }}
|
||||
run: |
|
||||
pnpm version ${{ github.event.inputs.version_type }} --preid ${{ github.event.inputs.pre_release }} --no-git-tag-version
|
||||
set -euo pipefail
|
||||
if [[ -n "$PRE_RELEASE" && ! "$VERSION_TYPE" =~ ^pre(major|minor|patch)$ && "$VERSION_TYPE" != "prerelease" ]]; then
|
||||
echo "❌ pre_release was provided but version_type='$VERSION_TYPE' does not support --preid"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -n "$PRE_RELEASE" ]]; then
|
||||
pnpm version "$VERSION_TYPE" --preid "$PRE_RELEASE" --no-git-tag-version
|
||||
else
|
||||
pnpm version "$VERSION_TYPE" --no-git-tag-version
|
||||
fi
|
||||
|
||||
NEW_VERSION=$(node -p "require('./package.json').version")
|
||||
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "NEW_VERSION=$NEW_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Format PR string
|
||||
id: capitalised
|
||||
env:
|
||||
VERSION_TYPE: ${{ steps.prepared-inputs.outputs.version_type }}
|
||||
run: |
|
||||
CAPITALISED_TYPE=${{ github.event.inputs.version_type }}
|
||||
echo "capitalised=${CAPITALISED_TYPE@u}" >> $GITHUB_OUTPUT
|
||||
CAPITALISED_TYPE="$VERSION_TYPE"
|
||||
echo "capitalised=${CAPITALISED_TYPE@u}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
|
||||
@@ -82,8 +188,8 @@ jobs:
|
||||
body: |
|
||||
${{ steps.capitalised.outputs.capitalised }} version increment to ${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
|
||||
**Base branch:** `${{ github.event.inputs.branch }}`
|
||||
**Base branch:** `${{ steps.prepared-inputs.outputs.branch }}`
|
||||
branch: version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
base: ${{ github.event.inputs.branch }}
|
||||
base: ${{ steps.prepared-inputs.outputs.branch }}
|
||||
labels: |
|
||||
Release
|
||||
|
||||
@@ -2,25 +2,98 @@
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"ignorePatterns": [
|
||||
".i18nrc.cjs",
|
||||
"components.d.ts",
|
||||
"lint-staged.config.js",
|
||||
"vitest.setup.ts",
|
||||
".nx/*",
|
||||
"**/vite.config.*.timestamp*",
|
||||
"**/vitest.config.*.timestamp*",
|
||||
"components.d.ts",
|
||||
"coverage/*",
|
||||
"dist/*",
|
||||
"packages/registry-types/src/comfyRegistryTypes.ts",
|
||||
"playwright-report/*",
|
||||
"src/extensions/core/*",
|
||||
"src/scripts/*",
|
||||
"src/types/generatedManagerTypes.ts",
|
||||
"src/types/vue-shim.d.ts"
|
||||
"src/types/vue-shim.d.ts",
|
||||
"test-results/*",
|
||||
"vitest.setup.ts"
|
||||
],
|
||||
"plugins": [
|
||||
"eslint",
|
||||
"import",
|
||||
"oxc",
|
||||
"typescript",
|
||||
"unicorn",
|
||||
"vitest",
|
||||
"vue"
|
||||
],
|
||||
"rules": {
|
||||
"no-async-promise-executor": "off",
|
||||
"no-console": [
|
||||
"error",
|
||||
{
|
||||
"allow": [
|
||||
"warn",
|
||||
"error"
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-control-regex": "off",
|
||||
"no-eval": "off",
|
||||
"no-redeclare": "error",
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"paths": [
|
||||
{
|
||||
"name": "primevue/calendar",
|
||||
"message": "Calendar is deprecated in PrimeVue 4+. Use DatePicker instead: import DatePicker from 'primevue/datepicker'"
|
||||
},
|
||||
{
|
||||
"name": "primevue/dropdown",
|
||||
"message": "Dropdown is deprecated in PrimeVue 4+. Use Select instead: import Select from 'primevue/select'"
|
||||
},
|
||||
{
|
||||
"name": "primevue/inputswitch",
|
||||
"message": "InputSwitch is deprecated in PrimeVue 4+. Use ToggleSwitch instead: import ToggleSwitch from 'primevue/toggleswitch'"
|
||||
},
|
||||
{
|
||||
"name": "primevue/overlaypanel",
|
||||
"message": "OverlayPanel is deprecated in PrimeVue 4+. Use Popover instead: import Popover from 'primevue/popover'"
|
||||
},
|
||||
{
|
||||
"name": "primevue/sidebar",
|
||||
"message": "Sidebar is deprecated in PrimeVue 4+. Use Drawer instead: import Drawer from 'primevue/drawer'"
|
||||
},
|
||||
{
|
||||
"name": "@/i18n--to-enable",
|
||||
"importNames": [
|
||||
"st",
|
||||
"t",
|
||||
"te",
|
||||
"d"
|
||||
],
|
||||
"message": "Don't import `@/i18n` directly, prefer `useI18n()`"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-self-assign": "allow",
|
||||
"no-unused-expressions": "off",
|
||||
"no-unused-private-class-members": "off",
|
||||
"no-useless-rename": "off",
|
||||
"import/default": "error",
|
||||
"import/export": "error",
|
||||
"import/namespace": "error",
|
||||
"import/no-duplicates": "error",
|
||||
"import/consistent-type-specifier-style": [
|
||||
"error",
|
||||
"prefer-top-level"
|
||||
],
|
||||
"jest/expect-expect": "off",
|
||||
"jest/no-conditional-expect": "off",
|
||||
"jest/no-disabled-tests": "off",
|
||||
"jest/no-standalone-expect": "off",
|
||||
"jest/valid-title": "off",
|
||||
"typescript/no-this-alias": "off",
|
||||
"typescript/no-unnecessary-parameter-property-assignment": "off",
|
||||
"typescript/no-unsafe-declaration-merging": "off",
|
||||
@@ -39,6 +112,18 @@
|
||||
"typescript/restrict-template-expressions": "off",
|
||||
"typescript/unbound-method": "off",
|
||||
"typescript/no-floating-promises": "error",
|
||||
"vue/no-import-compiler-macros": "error"
|
||||
}
|
||||
"vue/no-import-compiler-macros": "error",
|
||||
"vue/no-dupe-keys": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"**/*.{stories,test,spec}.ts",
|
||||
"**/*.stories.vue"
|
||||
],
|
||||
"rules": {
|
||||
"no-console": "allow"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -12,6 +12,9 @@
|
||||
"declaration-property-value-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"typesSyntax": {
|
||||
"radial-gradient()": "| <any-value>"
|
||||
},
|
||||
"ignoreProperties": {
|
||||
"speak": ["none"],
|
||||
"app-region": ["drag", "no-drag"],
|
||||
@@ -56,10 +59,7 @@
|
||||
"function-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignoreFunctions": [
|
||||
"theme",
|
||||
"v-bind"
|
||||
]
|
||||
"ignoreFunctions": ["theme", "v-bind"]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -51,8 +51,6 @@ defineProps<{
|
||||
canProceed: boolean
|
||||
/** Whether the location step should be disabled */
|
||||
disableLocationStep: boolean
|
||||
/** Whether the migration step should be disabled */
|
||||
disableMigrationStep: boolean
|
||||
/** Whether the settings step should be disabled */
|
||||
disableSettingsStep: boolean
|
||||
}>()
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
{
|
||||
"id": "e0cb1d7e-5437-4911-b574-c9603dfbeaee",
|
||||
"revision": 0,
|
||||
"last_node_id": 2,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 2,
|
||||
"type": "8bfe4227-f272-49e1-a892-0a972a86867c",
|
||||
"pos": [
|
||||
-317,
|
||||
-336
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
58
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"proxyWidgets": [
|
||||
[
|
||||
"-1",
|
||||
"batch_size"
|
||||
]
|
||||
]
|
||||
},
|
||||
"widgets_values": [
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"definitions": {
|
||||
"subgraphs": [
|
||||
{
|
||||
"id": "8bfe4227-f272-49e1-a892-0a972a86867c",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 1,
|
||||
"lastLinkId": 1,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "New Subgraph",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [
|
||||
-562,
|
||||
-358,
|
||||
120,
|
||||
60
|
||||
]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [
|
||||
-52,
|
||||
-358,
|
||||
120,
|
||||
40
|
||||
]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "b4a8bc2a-8e9f-41aa-938d-c567a11d2c00",
|
||||
"name": "batch_size",
|
||||
"type": "INT",
|
||||
"linkIds": [
|
||||
1
|
||||
],
|
||||
"pos": [
|
||||
-462,
|
||||
-338
|
||||
]
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "EmptyLatentImage",
|
||||
"pos": [
|
||||
-382,
|
||||
-376
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
106
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "batch_size",
|
||||
"name": "batch_size",
|
||||
"type": "INT",
|
||||
"widget": {
|
||||
"name": "batch_size"
|
||||
},
|
||||
"link": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "LATENT",
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "EmptyLatentImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
512,
|
||||
512,
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 1,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 1,
|
||||
"target_slot": 0,
|
||||
"type": "INT"
|
||||
}
|
||||
],
|
||||
"extra": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"config": {},
|
||||
"extra": {
|
||||
"frontendVersion": "1.35.1"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
After Width: | Height: | Size: 422 B |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 82 KiB |
@@ -13,6 +13,7 @@ import { ComfyTemplates } from '../helpers/templates'
|
||||
import { ComfyMouse } from './ComfyMouse'
|
||||
import { VueNodeHelpers } from './VueNodeHelpers'
|
||||
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
|
||||
import { QueueList } from './components/QueueList'
|
||||
import { SettingDialog } from './components/SettingDialog'
|
||||
import {
|
||||
NodeLibrarySidebarTab,
|
||||
@@ -126,20 +127,6 @@ class ConfirmDialog {
|
||||
const loc = this[locator]
|
||||
await expect(loc).toBeVisible()
|
||||
await loc.click()
|
||||
|
||||
// Wait for the dialog mask to disappear after confirming
|
||||
const mask = this.page.locator('.p-dialog-mask')
|
||||
const count = await mask.count()
|
||||
if (count > 0) {
|
||||
await mask.first().waitFor({ state: 'hidden', timeout: 3000 })
|
||||
}
|
||||
|
||||
// Wait for workflow service to finish if it's busy
|
||||
await this.page.waitForFunction(
|
||||
() => window['app']?.extensionManager?.workflow?.isBusy === false,
|
||||
undefined,
|
||||
{ timeout: 3000 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +152,7 @@ export class ComfyPage {
|
||||
|
||||
// Components
|
||||
public readonly searchBox: ComfyNodeSearchBox
|
||||
public readonly queueList: QueueList
|
||||
public readonly menu: ComfyMenu
|
||||
public readonly actionbar: ComfyActionbar
|
||||
public readonly templates: ComfyTemplates
|
||||
@@ -197,6 +185,7 @@ export class ComfyPage {
|
||||
this.visibleToasts = page.locator('.p-toast-message:visible')
|
||||
|
||||
this.searchBox = new ComfyNodeSearchBox(page)
|
||||
this.queueList = new QueueList(page)
|
||||
this.menu = new ComfyMenu(page)
|
||||
this.actionbar = new ComfyActionbar(page)
|
||||
this.templates = new ComfyTemplates(page)
|
||||
@@ -256,9 +245,6 @@ export class ComfyPage {
|
||||
await this.page.evaluate(async () => {
|
||||
await window['app'].extensionManager.workflow.syncWorkflows()
|
||||
})
|
||||
|
||||
// Wait for Vue to re-render the workflow list
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async setupUser(username: string) {
|
||||
@@ -585,9 +571,15 @@ export class ComfyPage {
|
||||
fileName?: string
|
||||
url?: string
|
||||
dropPosition?: Position
|
||||
waitForUpload?: boolean
|
||||
} = {}
|
||||
) {
|
||||
const { dropPosition = { x: 100, y: 100 }, fileName, url } = options
|
||||
const {
|
||||
dropPosition = { x: 100, y: 100 },
|
||||
fileName,
|
||||
url,
|
||||
waitForUpload = false
|
||||
} = options
|
||||
|
||||
if (!fileName && !url)
|
||||
throw new Error('Must provide either fileName or url')
|
||||
@@ -624,6 +616,14 @@ export class ComfyPage {
|
||||
// Dropping a URL (e.g., dropping image across browser tabs in Firefox)
|
||||
if (url) evaluateParams.url = url
|
||||
|
||||
// Set up response waiter for file uploads before triggering the drop
|
||||
const uploadResponsePromise = waitForUpload
|
||||
? this.page.waitForResponse(
|
||||
(resp) => resp.url().includes('/upload/') && resp.status() === 200,
|
||||
{ timeout: 10000 }
|
||||
)
|
||||
: null
|
||||
|
||||
// Execute the drag and drop in the browser
|
||||
await this.page.evaluate(async (params) => {
|
||||
const dataTransfer = new DataTransfer()
|
||||
@@ -690,12 +690,17 @@ export class ComfyPage {
|
||||
}
|
||||
}, evaluateParams)
|
||||
|
||||
// Wait for file upload to complete
|
||||
if (uploadResponsePromise) {
|
||||
await uploadResponsePromise
|
||||
}
|
||||
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async dragAndDropFile(
|
||||
fileName: string,
|
||||
options: { dropPosition?: Position } = {}
|
||||
options: { dropPosition?: Position; waitForUpload?: boolean } = {}
|
||||
) {
|
||||
return this.dragAndDropExternalResource({ fileName, ...options })
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ export class VueNodeHelpers {
|
||||
return {
|
||||
input: widget.locator('input'),
|
||||
incrementButton: widget.locator('button').first(),
|
||||
decrementButton: widget.locator('button').last()
|
||||
decrementButton: widget.locator('button').nth(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
57
browser_tests/fixtures/components/QueueList.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
export class QueueList {
|
||||
constructor(public readonly page: Page) {}
|
||||
|
||||
get toggleButton() {
|
||||
return this.page.getByTestId('queue-toggle-button')
|
||||
}
|
||||
|
||||
get inlineProgress() {
|
||||
return this.page.getByTestId('queue-inline-progress')
|
||||
}
|
||||
|
||||
get overlay() {
|
||||
return this.page.getByTestId('queue-overlay')
|
||||
}
|
||||
|
||||
get closeButton() {
|
||||
return this.page.getByTestId('queue-overlay-close-button')
|
||||
}
|
||||
|
||||
get jobItems() {
|
||||
return this.page.getByTestId('queue-job-item')
|
||||
}
|
||||
|
||||
get clearHistoryButton() {
|
||||
return this.page.getByRole('button', { name: /Clear History/i })
|
||||
}
|
||||
|
||||
async open() {
|
||||
if (!(await this.overlay.isVisible())) {
|
||||
await this.toggleButton.click()
|
||||
await expect(this.overlay).toBeVisible()
|
||||
}
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (await this.overlay.isVisible()) {
|
||||
await this.closeButton.click()
|
||||
await expect(this.overlay).not.toBeVisible()
|
||||
}
|
||||
}
|
||||
|
||||
async getJobCount(state?: string) {
|
||||
if (state) {
|
||||
return await this.page
|
||||
.locator(`[data-testid="queue-job-item"][data-job-state="${state}"]`)
|
||||
.count()
|
||||
}
|
||||
return await this.jobItems.count()
|
||||
}
|
||||
|
||||
getJobAction(actionKey: string) {
|
||||
return this.page.getByTestId(`job-action-${actionKey}`)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
import { test as base } from '@playwright/test'
|
||||
|
||||
import type { StatusWsMessage } from '../../src/schemas/apiSchema'
|
||||
|
||||
export type WsMessage = { type: 'status'; data: StatusWsMessage }
|
||||
|
||||
export const webSocketFixture = base.extend<{
|
||||
ws: { trigger(data: any, url?: string): Promise<void> }
|
||||
ws: { trigger(data: WsMessage, url?: string): Promise<void> }
|
||||
}>({
|
||||
ws: [
|
||||
async ({ page }, use) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import path from 'path'
|
||||
|
||||
import type {
|
||||
@@ -8,9 +9,20 @@ import type {
|
||||
|
||||
export class ComfyTemplates {
|
||||
readonly content: Locator
|
||||
readonly allTemplateCards: Locator
|
||||
|
||||
constructor(readonly page: Page) {
|
||||
this.content = page.getByTestId('template-workflows-content')
|
||||
this.allTemplateCards = page.locator('[data-testid^="template-workflow-"]')
|
||||
}
|
||||
|
||||
async waitForMinimumCardCount(count: number) {
|
||||
return await expect(async () => {
|
||||
const cardCount = await this.allTemplateCards.count()
|
||||
expect(cardCount).toBeGreaterThanOrEqual(count)
|
||||
}).toPass({
|
||||
timeout: 1_000
|
||||
})
|
||||
}
|
||||
|
||||
async loadTemplate(id: string) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Response } from '@playwright/test'
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
|
||||
import type { StatusWsMessage } from '../../src/schemas/apiSchema.ts'
|
||||
import { comfyPageFixture } from '../fixtures/ComfyPage.ts'
|
||||
import { webSocketFixture } from '../fixtures/ws.ts'
|
||||
import { comfyPageFixture } from '../fixtures/ComfyPage'
|
||||
import { webSocketFixture } from '../fixtures/ws'
|
||||
import type { WsMessage } from '../fixtures/ws'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, webSocketFixture)
|
||||
|
||||
@@ -61,7 +61,7 @@ test.describe('Actionbar', () => {
|
||||
|
||||
// Trigger a status websocket message
|
||||
const triggerStatus = async (queueSize: number) => {
|
||||
await ws.trigger({
|
||||
const message = {
|
||||
type: 'status',
|
||||
data: {
|
||||
status: {
|
||||
@@ -70,7 +70,9 @@ test.describe('Actionbar', () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
} as StatusWsMessage)
|
||||
} satisfies WsMessage
|
||||
|
||||
await ws.trigger(message)
|
||||
}
|
||||
|
||||
// Extract the width from the queue response
|
||||
|
||||
@@ -77,8 +77,7 @@ test.describe('Background Image Upload', () => {
|
||||
|
||||
// Verify the URL input now has an API URL
|
||||
const urlInput = backgroundImageSetting.locator('input[type="text"]')
|
||||
const inputValue = await urlInput.inputValue()
|
||||
expect(inputValue).toMatch(/^\/api\/view\?.*subfolder=backgrounds/)
|
||||
await expect(urlInput).toHaveValue(/^\/api\/view\?.*subfolder=backgrounds/)
|
||||
|
||||
// Verify clear button is now enabled
|
||||
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
|
||||
|
||||
@@ -36,9 +36,10 @@ test.describe('Execute to selected output nodes', () => {
|
||||
await output1.click('title')
|
||||
|
||||
await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes')
|
||||
|
||||
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output1.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output2.getWidget(0)).getValue()).toBe('')
|
||||
await expect(async () => {
|
||||
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output1.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output2.getWidget(0)).getValue()).toBe('')
|
||||
}).toPass({ timeout: 2_000 })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -306,14 +306,16 @@ test.describe('Node Interaction', () => {
|
||||
await comfyPage.canvas.click({
|
||||
position: numberWidgetPos
|
||||
})
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('prompt-dialog-opened.png')
|
||||
const legacyPrompt = comfyPage.page.locator('.graphdialog')
|
||||
await expect(legacyPrompt).toBeVisible()
|
||||
await comfyPage.delay(300)
|
||||
await comfyPage.canvas.click({
|
||||
position: {
|
||||
x: 10,
|
||||
y: 10
|
||||
}
|
||||
})
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('prompt-dialog-closed.png')
|
||||
await expect(legacyPrompt).toBeHidden()
|
||||
})
|
||||
|
||||
test('Can close prompt dialog with canvas click (text widget)', async ({
|
||||
@@ -327,18 +329,16 @@ test.describe('Node Interaction', () => {
|
||||
await comfyPage.canvas.click({
|
||||
position: textWidgetPos
|
||||
})
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'prompt-dialog-opened-text.png'
|
||||
)
|
||||
const legacyPrompt = comfyPage.page.locator('.graphdialog')
|
||||
await expect(legacyPrompt).toBeVisible()
|
||||
await comfyPage.delay(300)
|
||||
await comfyPage.canvas.click({
|
||||
position: {
|
||||
x: 10,
|
||||
y: 10
|
||||
}
|
||||
})
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'prompt-dialog-closed-text.png'
|
||||
)
|
||||
await expect(legacyPrompt).toBeHidden()
|
||||
})
|
||||
|
||||
test('Can double click node title to edit', async ({ comfyPage }) => {
|
||||
|
||||
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 45 KiB |
@@ -12,6 +12,7 @@ test.describe('Load Workflow in Media', () => {
|
||||
'edited_workflow.webp',
|
||||
'no_workflow.webp',
|
||||
'large_workflow.webp',
|
||||
'workflow_prompt_parameters.png',
|
||||
'workflow.webm',
|
||||
// Skipped due to 3d widget unstable visual result.
|
||||
// 3d widget shows grid after fully loaded.
|
||||
|
||||
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 44 KiB |
29
browser_tests/tests/mobileBaseline.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
test.describe('Mobile Baseline Snapshots', () => {
|
||||
test('@mobile empty canvas', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.ConfirmClear', false)
|
||||
await comfyPage.executeCommand('Comfy.ClearWorkflow')
|
||||
await expect(async () => {
|
||||
expect(await comfyPage.getGraphNodesCount()).toBe(0)
|
||||
}).toPass({ timeout: 256 })
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('mobile-empty-canvas.png')
|
||||
})
|
||||
|
||||
test('@mobile default workflow', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('default')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'mobile-default-workflow.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('@mobile settings dialog', async ({ comfyPage }) => {
|
||||
await comfyPage.settingDialog.open()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.settingDialog.root).toHaveScreenshot(
|
||||
'mobile-settings-dialog.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -260,6 +260,12 @@ test.describe('Release context menu', () => {
|
||||
|
||||
test('Can trigger on link release', async ({ comfyPage }) => {
|
||||
await comfyPage.disconnectEdge()
|
||||
const contextMenu = comfyPage.page.locator('.litecontextmenu')
|
||||
// Wait for context menu with correct title (slot name | slot type)
|
||||
// The title shows the output slot name and type from the disconnected link
|
||||
await expect(contextMenu.locator('.litemenu-title')).toContainText(
|
||||
'CLIP | CLIP'
|
||||
)
|
||||
await comfyPage.page.mouse.move(10, 10)
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
|
||||
157
browser_tests/tests/queue/queueList.spec.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
|
||||
import type { ComfyPage } from '../../fixtures/ComfyPage'
|
||||
import { comfyPageFixture } from '../../fixtures/ComfyPage'
|
||||
import { webSocketFixture } from '../../fixtures/ws'
|
||||
import type { WsMessage } from '../../fixtures/ws'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, webSocketFixture)
|
||||
|
||||
type QueueState = {
|
||||
running: QueueJob[]
|
||||
pending: QueueJob[]
|
||||
}
|
||||
|
||||
type QueueJob = [
|
||||
string,
|
||||
string,
|
||||
Record<string, unknown>,
|
||||
Record<string, unknown>,
|
||||
string[]
|
||||
]
|
||||
|
||||
type QueueController = {
|
||||
state: QueueState
|
||||
sync: (
|
||||
ws: { trigger(data: WsMessage, url?: string): Promise<void> },
|
||||
nextState: Partial<QueueState>
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
test.describe('Queue UI', () => {
|
||||
let queue: QueueController
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.page.route('**/api/prompt', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
prompt_id: 'mock-prompt-id',
|
||||
number: 1,
|
||||
node_errors: {}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Mock history to avoid pulling real data
|
||||
await comfyPage.page.route('**/api/history**', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ History: [] })
|
||||
})
|
||||
})
|
||||
|
||||
queue = await createQueueController(comfyPage)
|
||||
})
|
||||
|
||||
test('toggles overlay and updates count from status events', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await queue.sync(ws, { running: [], pending: [] })
|
||||
|
||||
await expect(comfyPage.queueList.toggleButton).toContainText('0')
|
||||
await expect(comfyPage.queueList.toggleButton).toContainText(/queued/i)
|
||||
await expect(comfyPage.queueList.overlay).toBeHidden()
|
||||
|
||||
await queue.sync(ws, {
|
||||
pending: [queueJob('1', 'mock-pending', 'client-a')]
|
||||
})
|
||||
|
||||
await expect(comfyPage.queueList.toggleButton).toContainText('1')
|
||||
await expect(comfyPage.queueList.toggleButton).toContainText(/queued/i)
|
||||
|
||||
await comfyPage.queueList.open()
|
||||
await expect(comfyPage.queueList.overlay).toBeVisible()
|
||||
await expect(comfyPage.queueList.jobItems).toHaveCount(1)
|
||||
|
||||
await comfyPage.queueList.close()
|
||||
await expect(comfyPage.queueList.overlay).toBeHidden()
|
||||
})
|
||||
|
||||
test('displays running and pending jobs via status updates', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await queue.sync(ws, {
|
||||
running: [queueJob('2', 'mock-running', 'client-b')],
|
||||
pending: [queueJob('3', 'mock-pending', 'client-c')]
|
||||
})
|
||||
|
||||
await comfyPage.queueList.open()
|
||||
await expect(comfyPage.queueList.jobItems).toHaveCount(2)
|
||||
|
||||
const firstJob = comfyPage.queueList.jobItems.first()
|
||||
await firstJob.hover()
|
||||
|
||||
const cancelAction = firstJob
|
||||
.getByTestId('job-action-cancel-running')
|
||||
.or(firstJob.getByTestId('job-action-cancel-hover'))
|
||||
|
||||
await expect(cancelAction).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
const queueJob = (
|
||||
queueIndex: string,
|
||||
promptId: string,
|
||||
clientId: string
|
||||
): QueueJob => [
|
||||
queueIndex,
|
||||
promptId,
|
||||
{ client_id: clientId },
|
||||
{ class_type: 'Note' },
|
||||
['output']
|
||||
]
|
||||
|
||||
const createQueueController = async (
|
||||
comfyPage: ComfyPage
|
||||
): Promise<QueueController> => {
|
||||
const state: QueueState = { running: [], pending: [] }
|
||||
|
||||
// Single queue handler reads the latest in-memory state
|
||||
await comfyPage.page.route('**/api/queue', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
queue_running: state.running,
|
||||
queue_pending: state.pending
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const sync = async (
|
||||
ws: { trigger(data: WsMessage, url?: string): Promise<void> },
|
||||
nextState: Partial<QueueState>
|
||||
) => {
|
||||
if (nextState.running) state.running = nextState.running
|
||||
if (nextState.pending) state.pending = nextState.pending
|
||||
|
||||
const total = state.running.length + state.pending.length
|
||||
const queueResponse = comfyPage.page.waitForResponse('**/api/queue')
|
||||
|
||||
await ws.trigger({
|
||||
type: 'status',
|
||||
data: {
|
||||
status: { exec_info: { queue_remaining: total } }
|
||||
}
|
||||
})
|
||||
|
||||
await queueResponse
|
||||
}
|
||||
|
||||
return { state, sync }
|
||||
}
|
||||
@@ -212,8 +212,12 @@ test.describe('Remote COMBO Widget', () => {
|
||||
// Click on the canvas to trigger widget refresh
|
||||
await comfyPage.page.mouse.click(400, 300)
|
||||
|
||||
const refreshedOptions = await getWidgetOptions(comfyPage, nodeName)
|
||||
expect(refreshedOptions).not.toEqual(initialOptions)
|
||||
await expect(async () => {
|
||||
const refreshedOptions = await getWidgetOptions(comfyPage, nodeName)
|
||||
expect(refreshedOptions).not.toEqual(initialOptions)
|
||||
}).toPass({
|
||||
timeout: 2_000
|
||||
})
|
||||
})
|
||||
|
||||
test('does not refresh when TTL is not set', async ({ comfyPage }) => {
|
||||
@@ -321,8 +325,12 @@ test.describe('Remote COMBO Widget', () => {
|
||||
await clickRefreshButton(comfyPage, nodeName)
|
||||
|
||||
// Verify the selected value of the widget is the first option in the refreshed list
|
||||
const refreshedValue = await getWidgetValue(comfyPage, nodeName)
|
||||
expect(refreshedValue).toEqual('new first option')
|
||||
await expect(async () => {
|
||||
const refreshedValue = await getWidgetValue(comfyPage, nodeName)
|
||||
expect(refreshedValue).toEqual('new first option')
|
||||
}).toPass({
|
||||
timeout: 2_000
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -290,16 +290,20 @@ test.describe('Node library sidebar', () => {
|
||||
await comfyPage.page.keyboard.insertText('bar')
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await comfyPage.nextFrame()
|
||||
expect(
|
||||
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
|
||||
).toEqual(['bar/'])
|
||||
expect(
|
||||
await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization')
|
||||
).toEqual({
|
||||
'bar/': {
|
||||
icon: 'pi-folder',
|
||||
color: '#007bff'
|
||||
}
|
||||
await expect(async () => {
|
||||
expect(
|
||||
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
|
||||
).toEqual(['bar/'])
|
||||
expect(
|
||||
await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization')
|
||||
).toEqual({
|
||||
'bar/': {
|
||||
icon: 'pi-folder',
|
||||
color: '#007bff'
|
||||
}
|
||||
})
|
||||
}).toPass({
|
||||
timeout: 2_000
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -329,6 +329,15 @@ test.describe('Subgraph Operations', () => {
|
||||
expect(newInputName).toBe(labelClickRenamedName)
|
||||
expect(newInputName).not.toBe(initialInputLabel)
|
||||
})
|
||||
test('Can create widget from link with compressed target_slot', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('subgraphs/subgraph-compressed-target-slot')
|
||||
const step = await comfyPage.page.evaluate(() => {
|
||||
return window['app'].graph.nodes[0].widgets[0].options.step
|
||||
})
|
||||
expect(step).toBe(10)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Subgraph Creation and Deletion', () => {
|
||||
|
||||
@@ -188,22 +188,19 @@ test.describe('Templates', () => {
|
||||
.locator('header')
|
||||
.filter({ hasText: 'Templates' })
|
||||
|
||||
const cardCount = await comfyPage.page
|
||||
.locator('[data-testid^="template-workflow-"]')
|
||||
.count()
|
||||
expect(cardCount).toBeGreaterThan(0)
|
||||
await comfyPage.templates.waitForMinimumCardCount(1)
|
||||
await expect(templateGrid).toBeVisible()
|
||||
await expect(nav).toBeVisible() // Nav should be visible at desktop size
|
||||
|
||||
const mobileSize = { width: 640, height: 800 }
|
||||
await comfyPage.page.setViewportSize(mobileSize)
|
||||
expect(cardCount).toBeGreaterThan(0)
|
||||
await comfyPage.templates.waitForMinimumCardCount(1)
|
||||
await expect(templateGrid).toBeVisible()
|
||||
await expect(nav).not.toBeVisible() // Nav should collapse at mobile size
|
||||
|
||||
const tabletSize = { width: 1024, height: 800 }
|
||||
await comfyPage.page.setViewportSize(tabletSize)
|
||||
expect(cardCount).toBeGreaterThan(0)
|
||||
await comfyPage.templates.waitForMinimumCardCount(1)
|
||||
await expect(templateGrid).toBeVisible()
|
||||
await expect(nav).toBeVisible() // Nav should be visible at tablet size
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 61 KiB |
@@ -0,0 +1,144 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '../../../../fixtures/ComfyPage'
|
||||
import { fitToViewInstant } from '../../../../helpers/fitToView'
|
||||
|
||||
test.describe('Vue Node Bring to Front', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.loadWorkflow('vueNodes/simple-triple')
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
await fitToViewInstant(comfyPage)
|
||||
})
|
||||
|
||||
/**
|
||||
* Helper to get the z-index of a node by its title
|
||||
*/
|
||||
async function getNodeZIndex(
|
||||
comfyPage: ComfyPage,
|
||||
title: string
|
||||
): Promise<number> {
|
||||
const node = comfyPage.vueNodes.getNodeByTitle(title)
|
||||
const style = await node.getAttribute('style')
|
||||
if (!style) {
|
||||
throw new Error(
|
||||
`Node "${title}" has no style attribute (observed: ${style})`
|
||||
)
|
||||
}
|
||||
const match = style.match(/z-index:\s*(\d+)/)
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`Node "${title}" has no z-index in style (observed: "${style}")`
|
||||
)
|
||||
}
|
||||
return parseInt(match[1], 10)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get the bounding box center of a node
|
||||
*/
|
||||
async function getNodeCenter(
|
||||
comfyPage: ComfyPage,
|
||||
title: string
|
||||
): Promise<{ x: number; y: number }> {
|
||||
const node = comfyPage.vueNodes.getNodeByTitle(title)
|
||||
const box = await node.boundingBox()
|
||||
if (!box) throw new Error(`Node "${title}" not found`)
|
||||
return { x: box.x + box.width / 2, y: box.y + box.height / 2 }
|
||||
}
|
||||
|
||||
test('should bring overlapped node to front when clicking on it', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Get initial positions
|
||||
const clipCenter = await getNodeCenter(comfyPage, 'CLIP Text Encode')
|
||||
const ksamplerHeader = await comfyPage.page
|
||||
.getByText('KSampler')
|
||||
.boundingBox()
|
||||
if (!ksamplerHeader) throw new Error('KSampler header not found')
|
||||
|
||||
// Drag KSampler on top of CLIP Text Encode
|
||||
await comfyPage.dragAndDrop(
|
||||
{ x: ksamplerHeader.x + 50, y: ksamplerHeader.y + 10 },
|
||||
clipCenter
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Screenshot showing KSampler on top of CLIP
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'bring-to-front-overlapped-before.png'
|
||||
)
|
||||
|
||||
// KSampler should be on top (higher z-index) after being dragged
|
||||
const ksamplerZIndexBefore = await getNodeZIndex(comfyPage, 'KSampler')
|
||||
const clipZIndexBefore = await getNodeZIndex(comfyPage, 'CLIP Text Encode')
|
||||
expect(ksamplerZIndexBefore).toBeGreaterThan(clipZIndexBefore)
|
||||
|
||||
// Click on CLIP Text Encode (underneath) - need to click on a visible part
|
||||
// Since KSampler is on top, we click on the edge of CLIP that should still be visible
|
||||
const clipNode = comfyPage.vueNodes.getNodeByTitle('CLIP Text Encode')
|
||||
const clipBox = await clipNode.boundingBox()
|
||||
if (!clipBox) throw new Error('CLIP node not found')
|
||||
|
||||
// Click on a visible edge of CLIP
|
||||
await comfyPage.page.mouse.click(clipBox.x + 30, clipBox.y + 10)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// CLIP should now be on top - compare post-action z-indices
|
||||
const clipZIndexAfter = await getNodeZIndex(comfyPage, 'CLIP Text Encode')
|
||||
const ksamplerZIndexAfter = await getNodeZIndex(comfyPage, 'KSampler')
|
||||
expect(clipZIndexAfter).toBeGreaterThan(ksamplerZIndexAfter)
|
||||
|
||||
// Screenshot showing CLIP now on top
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'bring-to-front-overlapped-after.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('should bring overlapped node to front when clicking on its widget', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Get CLIP Text Encode position (it has a text widget)
|
||||
const clipCenter = await getNodeCenter(comfyPage, 'CLIP Text Encode')
|
||||
|
||||
// Get VAE Decode position and drag it on top of CLIP
|
||||
const vaeHeader = await comfyPage.page.getByText('VAE Decode').boundingBox()
|
||||
if (!vaeHeader) throw new Error('VAE Decode header not found')
|
||||
|
||||
await comfyPage.dragAndDrop(
|
||||
{ x: vaeHeader.x + 50, y: vaeHeader.y + 10 },
|
||||
{ x: clipCenter.x - 50, y: clipCenter.y }
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// VAE should be on top after drag
|
||||
const vaeZIndexBefore = await getNodeZIndex(comfyPage, 'VAE Decode')
|
||||
const clipZIndexBefore = await getNodeZIndex(comfyPage, 'CLIP Text Encode')
|
||||
expect(vaeZIndexBefore).toBeGreaterThan(clipZIndexBefore)
|
||||
|
||||
// Screenshot showing VAE on top
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'bring-to-front-widget-overlapped-before.png'
|
||||
)
|
||||
|
||||
// Click on the text widget of CLIP Text Encode
|
||||
const clipNode = comfyPage.vueNodes.getNodeByTitle('CLIP Text Encode')
|
||||
const clipBox = await clipNode.boundingBox()
|
||||
if (!clipBox) throw new Error('CLIP node not found')
|
||||
await comfyPage.page.mouse.click(clipBox.x + 170, clipBox.y + 80)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// CLIP should now be on top - compare post-action z-indices
|
||||
const clipZIndexAfter = await getNodeZIndex(comfyPage, 'CLIP Text Encode')
|
||||
const vaeZIndexAfter = await getNodeZIndex(comfyPage, 'VAE Decode')
|
||||
expect(clipZIndexAfter).toBeGreaterThan(vaeZIndexAfter)
|
||||
|
||||
// Screenshot showing CLIP now on top after widget click
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'bring-to-front-widget-overlapped-after.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 113 KiB |
@@ -15,7 +15,9 @@ test.describe('Vue Integer Widget', () => {
|
||||
await comfyPage.loadWorkflow('vueNodes/linked-int-widget')
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
const seedWidget = comfyPage.vueNodes.getWidgetByName('KSampler', 'seed')
|
||||
const seedWidget = comfyPage.vueNodes
|
||||
.getWidgetByName('KSampler', 'seed')
|
||||
.first()
|
||||
const controls = comfyPage.vueNodes.getInputNumberControls(seedWidget)
|
||||
const initialValue = Number(await controls.input.inputValue())
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 85 KiB |
@@ -252,7 +252,8 @@ test.describe('Animated image widget', () => {
|
||||
|
||||
// Drag and drop image file onto the load animated webp node
|
||||
await comfyPage.dragAndDropFile('animated_webp.webp', {
|
||||
dropPosition: { x, y }
|
||||
dropPosition: { x, y },
|
||||
waitForUpload: true
|
||||
})
|
||||
|
||||
// Expect the filename combo value to be updated
|
||||
|
||||
@@ -62,16 +62,20 @@ export default defineConfig([
|
||||
{
|
||||
ignores: [
|
||||
'.i18nrc.cjs',
|
||||
'components.d.ts',
|
||||
'lint-staged.config.js',
|
||||
'vitest.setup.ts',
|
||||
'.nx/*',
|
||||
'**/vite.config.*.timestamp*',
|
||||
'**/vitest.config.*.timestamp*',
|
||||
'components.d.ts',
|
||||
'coverage/*',
|
||||
'dist/*',
|
||||
'packages/registry-types/src/comfyRegistryTypes.ts',
|
||||
'playwright-report/*',
|
||||
'src/extensions/core/*',
|
||||
'src/scripts/*',
|
||||
'src/types/generatedManagerTypes.ts',
|
||||
'src/types/vue-shim.d.ts'
|
||||
'src/types/vue-shim.d.ts',
|
||||
'test-results/*',
|
||||
'vitest.setup.ts'
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -103,24 +107,17 @@ export default defineConfig([
|
||||
|
||||
tseslintConfigs.recommended,
|
||||
// Difference in typecheck on CI vs Local
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Bad types in the plugin
|
||||
pluginVue.configs['flat/recommended'],
|
||||
eslintPluginPrettierRecommended,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Type incompatibility between import-x plugin and ESLint config types
|
||||
storybook.configs['flat/recommended'],
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Type incompatibility between import-x plugin and ESLint config types
|
||||
// @ts-expect-error Type incompatibility between import-x plugin and ESLint config types
|
||||
importX.flatConfigs.recommended,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Type incompatibility between import-x plugin and ESLint config types
|
||||
// @ts-expect-error Type incompatibility between import-x plugin and ESLint config types
|
||||
importX.flatConfigs.typescript,
|
||||
{
|
||||
plugins: {
|
||||
'unused-imports': unusedImports,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Type incompatibility in i18n plugin
|
||||
// @ts-expect-error Type incompatibility in i18n plugin
|
||||
'@intlify/vue-i18n': pluginI18n
|
||||
},
|
||||
rules: {
|
||||
@@ -135,59 +132,24 @@ export default defineConfig([
|
||||
allowInterfaces: 'always'
|
||||
}
|
||||
],
|
||||
'import-x/consistent-type-specifier-style': ['error', 'prefer-top-level'],
|
||||
'import-x/no-useless-path-segments': 'error',
|
||||
'import-x/no-relative-packages': 'error',
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||
'vue/no-v-html': 'off',
|
||||
// Prohibit dark-theme: and dark: prefixes
|
||||
'vue/no-restricted-class': ['error', '/^dark(-theme)?:/'],
|
||||
'vue/multi-word-component-names': 'off', // TODO: fix
|
||||
'vue/no-template-shadow': 'off', // TODO: fix
|
||||
'vue/match-component-import-name': 'error',
|
||||
/* Toggle on to do additional until we can clean up existing violations.
|
||||
'vue/no-unused-emit-declarations': 'error',
|
||||
'vue/no-unused-properties': 'error',
|
||||
'vue/no-unused-refs': 'error',
|
||||
'vue/no-use-v-else-with-v-for': 'error',
|
||||
'vue/no-useless-mustaches': 'error',
|
||||
'vue/no-useless-v-bind': 'error',
|
||||
// */
|
||||
'vue/one-component-per-file': 'off', // TODO: fix
|
||||
'vue/no-unused-emit-declarations': 'error',
|
||||
'vue/no-use-v-else-with-v-for': 'error',
|
||||
'vue/one-component-per-file': 'error',
|
||||
'vue/require-default-prop': 'off', // TODO: fix -- this one is very worthwhile
|
||||
// Restrict deprecated PrimeVue components
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'primevue/calendar',
|
||||
message:
|
||||
'Calendar is deprecated in PrimeVue 4+. Use DatePicker instead: import DatePicker from "primevue/datepicker"'
|
||||
},
|
||||
{
|
||||
name: 'primevue/dropdown',
|
||||
message:
|
||||
'Dropdown is deprecated in PrimeVue 4+. Use Select instead: import Select from "primevue/select"'
|
||||
},
|
||||
{
|
||||
name: 'primevue/inputswitch',
|
||||
message:
|
||||
'InputSwitch is deprecated in PrimeVue 4+. Use ToggleSwitch instead: import ToggleSwitch from "primevue/toggleswitch"'
|
||||
},
|
||||
{
|
||||
name: 'primevue/overlaypanel',
|
||||
message:
|
||||
'OverlayPanel is deprecated in PrimeVue 4+. Use Popover instead: import Popover from "primevue/popover"'
|
||||
},
|
||||
{
|
||||
name: 'primevue/sidebar',
|
||||
message:
|
||||
'Sidebar is deprecated in PrimeVue 4+. Use Drawer instead: import Drawer from "primevue/drawer"'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
// i18n rules
|
||||
'@intlify/vue-i18n/no-raw-text': [
|
||||
'error',
|
||||
@@ -275,12 +237,6 @@ export default defineConfig([
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.{test,spec,stories}.ts', '**/*.stories.vue'],
|
||||
rules: {
|
||||
'no-console': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['scripts/**/*.js'],
|
||||
languageOptions: {
|
||||
@@ -297,5 +253,14 @@ export default defineConfig([
|
||||
// Turn off ESLint rules that are already handled by oxlint
|
||||
...oxlint.buildFromOxlintConfigFile(
|
||||
path.resolve(import.meta.dirname, '.oxlintrc.json')
|
||||
)
|
||||
),
|
||||
{
|
||||
rules: {
|
||||
'import-x/default': 'off',
|
||||
'import-x/export': 'off',
|
||||
'import-x/namespace': 'off',
|
||||
'import-x/no-duplicates': 'off',
|
||||
'import-x/consistent-type-specifier-style': 'off'
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
export default {
|
||||
'./**/*.js': (stagedFiles) => formatAndEslint(stagedFiles),
|
||||
|
||||
'./**/*.{ts,tsx,vue,mts}': (stagedFiles) => [
|
||||
...formatAndEslint(stagedFiles),
|
||||
'pnpm typecheck'
|
||||
]
|
||||
}
|
||||
|
||||
function formatAndEslint(fileNames) {
|
||||
// Convert absolute paths to relative paths for better ESLint resolution
|
||||
const relativePaths = fileNames.map((f) => f.replace(process.cwd() + '/', ''))
|
||||
return [
|
||||
`pnpm exec eslint --cache --fix ${relativePaths.join(' ')}`,
|
||||
`pnpm exec prettier --cache --write ${relativePaths.join(' ')}`
|
||||
]
|
||||
}
|
||||
21
lint-staged.config.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import path from 'node:path'
|
||||
|
||||
export default {
|
||||
'./**/*.js': (stagedFiles: string[]) => formatAndEslint(stagedFiles),
|
||||
|
||||
'./**/*.{ts,tsx,vue,mts}': (stagedFiles: string[]) => [
|
||||
...formatAndEslint(stagedFiles),
|
||||
'pnpm typecheck'
|
||||
]
|
||||
}
|
||||
|
||||
function formatAndEslint(fileNames: string[]) {
|
||||
// Convert absolute paths to relative paths for better ESLint resolution
|
||||
const relativePaths = fileNames.map((f) => path.relative(process.cwd(), f))
|
||||
const joinedPaths = relativePaths.map((p) => `"${p}"`).join(' ')
|
||||
return [
|
||||
`pnpm exec prettier --cache --write ${joinedPaths}`,
|
||||
`pnpm exec oxlint --fix ${joinedPaths}`,
|
||||
`pnpm exec eslint --cache --fix --no-warn-ignored ${joinedPaths}`
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.35.1",
|
||||
"version": "1.35.6",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
--color-bypass: #6a246a;
|
||||
--color-error: #962a2a;
|
||||
|
||||
--color-comfy-menu-secondary: var(--comfy-menu-secondary-bg);
|
||||
|
||||
--color-interface-panel-job-progress-primary: var(--color-azure-300);
|
||||
--color-interface-panel-job-progress-secondary: var(--color-alpha-azure-600-30);
|
||||
@@ -261,6 +260,8 @@
|
||||
--component-node-widget-background-selected: var(--secondary-background-selected);
|
||||
--component-node-widget-background-disabled: var(--color-alpha-ash-500-20);
|
||||
--component-node-widget-background-highlighted: var(--color-ash-500);
|
||||
--component-node-widget-promoted: var(--color-purple-700);
|
||||
--component-node-widget-advanced: var(--color-azure-400);
|
||||
|
||||
/* Default UI element color palette variables */
|
||||
--palette-contrast-mix-color: #fff;
|
||||
@@ -384,6 +385,8 @@
|
||||
--component-node-widget-background-selected: var(--color-charcoal-100);
|
||||
--component-node-widget-background-disabled: var(--color-alpha-charcoal-600-30);
|
||||
--component-node-widget-background-highlighted: var(--color-graphite-400);
|
||||
--component-node-widget-promoted: var(--color-purple-700);
|
||||
--component-node-widget-advanced: var(--color-azure-600);
|
||||
|
||||
--modal-card-background: var(--secondary-background);
|
||||
--modal-card-background-hovered: var(--secondary-background-hover);
|
||||
@@ -434,7 +437,11 @@
|
||||
--color-interface-button-hover-surface: var(
|
||||
--interface-button-hover-surface
|
||||
);
|
||||
--color-comfy-input: var(--comfy-input-bg);
|
||||
--color-comfy-input-foreground: var(--input-text);
|
||||
--color-comfy-menu-bg: var(--comfy-menu-bg);
|
||||
--color-comfy-menu-secondary: var(--comfy-menu-secondary-bg);
|
||||
|
||||
--color-interface-stroke: var(--interface-stroke);
|
||||
--color-nav-background: var(--nav-background);
|
||||
--color-node-border: var(--node-border);
|
||||
@@ -490,6 +497,8 @@
|
||||
--color-component-node-widget-background-selected: var(--component-node-widget-background-selected);
|
||||
--color-component-node-widget-background-disabled: var(--component-node-widget-background-disabled);
|
||||
--color-component-node-widget-background-highlighted: var(--component-node-widget-background-highlighted);
|
||||
--color-component-node-widget-promoted: var(--component-node-widget-promoted);
|
||||
--color-component-node-widget-advanced: var(--component-node-widget-advanced);
|
||||
|
||||
/* Semantic tokens */
|
||||
--color-base-foreground: var(--base-foreground);
|
||||
@@ -1319,6 +1328,15 @@ audio.comfy-audio.empty-audio-widget {
|
||||
font-size 0.1s ease;
|
||||
}
|
||||
|
||||
/* Performance optimization during canvas interaction */
|
||||
.transform-pane--interacting .lg-node * {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.transform-pane--interacting .lg-node {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* ===================== Mask Editor Styles ===================== */
|
||||
/* To be migrated to Tailwind later */
|
||||
#maskEditor_brush {
|
||||
|
||||
308
packages/registry-types/src/comfyRegistryTypes.ts
generated
@@ -1945,6 +1945,40 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/proxy/kling/v1/images/omni-image": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** KlingAI Create Omni-Image Task */
|
||||
post: operations["klingCreateOmniImage"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/proxy/kling/v1/images/omni-image/{id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** KlingAI Query Single Omni-Image Task */
|
||||
get: operations["klingOmniImageQuerySingleTask"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/proxy/kling/v1/images/kolors-virtual-try-on": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -3876,7 +3910,7 @@ export interface components {
|
||||
* @description The subscription tier level
|
||||
* @enum {string}
|
||||
*/
|
||||
SubscriptionTier: "STANDARD" | "CREATOR" | "PRO";
|
||||
SubscriptionTier: "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION";
|
||||
FeaturesResponse: {
|
||||
/**
|
||||
* @description The conversion rate for partner nodes
|
||||
@@ -5096,6 +5130,71 @@ export interface components {
|
||||
};
|
||||
};
|
||||
};
|
||||
KlingOmniImageRequest: {
|
||||
/**
|
||||
* @description Model Name
|
||||
* @default kling-image-o1
|
||||
* @enum {string}
|
||||
*/
|
||||
model_name: "kling-image-o1";
|
||||
/** @description Text prompt words, which can include positive and negative descriptions. Must not exceed 2,500 characters. The Omni model can achieve various capabilities through Prompt with elements and images. Specify an image in the format of <<<>>>, such as <<<image_1>>>. */
|
||||
prompt: string;
|
||||
/** @description Reference Image List. Supports inputting image Base64 encoding or image URL (ensure accessibility). Supported formats include .jpg/.jpeg/.png. File size cannot exceed 10MB. Width and height dimensions shall not be less than 300px, aspect ratio between 1:2.5 ~ 2.5:1. Maximum 10 images. */
|
||||
image_list?: {
|
||||
/** @description Image Base64 encoding or image URL (ensure accessibility) */
|
||||
image?: string;
|
||||
}[];
|
||||
/**
|
||||
* @description Image generation resolution. 1k is 1K standard, 2k is 2K high-res, 4k is 4K high-res.
|
||||
* @default 1k
|
||||
* @enum {string}
|
||||
*/
|
||||
resolution: "1k" | "2k" | "4k";
|
||||
/**
|
||||
* @description Number of generated images. Value range [1,9].
|
||||
* @default 1
|
||||
*/
|
||||
n: number;
|
||||
/**
|
||||
* @description Aspect ratio of the generated images (width:height). auto is to intelligently generate images based on incoming content.
|
||||
* @default auto
|
||||
* @enum {string}
|
||||
*/
|
||||
aspect_ratio: "16:9" | "9:16" | "1:1" | "4:3" | "3:4" | "3:2" | "2:3" | "21:9" | "auto";
|
||||
/**
|
||||
* Format: uri
|
||||
* @description The callback notification address for the result of this task. If configured, the server will actively notify when the task status changes.
|
||||
*/
|
||||
callback_url?: string;
|
||||
/** @description Customized Task ID. Must be unique within a single user account. */
|
||||
external_task_id?: string;
|
||||
};
|
||||
KlingOmniImageResponse: {
|
||||
/** @description Error code */
|
||||
code?: number;
|
||||
/** @description Error message */
|
||||
message?: string;
|
||||
/** @description Request ID */
|
||||
request_id?: string;
|
||||
data?: {
|
||||
/** @description Task ID */
|
||||
task_id?: string;
|
||||
task_status?: components["schemas"]["KlingTaskStatus"];
|
||||
/** @description Task status information, displaying the failure reason when the task fails (such as triggering the content risk control of the platform, etc.) */
|
||||
task_status_msg?: string;
|
||||
task_info?: {
|
||||
/** @description Customer-defined task ID */
|
||||
external_task_id?: string;
|
||||
};
|
||||
/** @description Task creation time, Unix timestamp in milliseconds */
|
||||
created_at?: number;
|
||||
/** @description Task update time, Unix timestamp in milliseconds */
|
||||
updated_at?: number;
|
||||
task_result?: {
|
||||
images?: components["schemas"]["KlingImageResult"][];
|
||||
};
|
||||
};
|
||||
};
|
||||
KlingLipSyncInputObject: {
|
||||
/** @description The ID of the video generated by Kling AI. Only supports 5-second and 10-second videos generated within the last 30 days. */
|
||||
video_id?: string;
|
||||
@@ -10065,7 +10164,7 @@ export interface components {
|
||||
};
|
||||
BytePlusImageGenerationRequest: {
|
||||
/** @enum {string} */
|
||||
model: "seedream-3-0-t2i-250415" | "seededit-3-0-i2i-250628" | "seedream-4-0-250828";
|
||||
model: "seedream-3-0-t2i-250415" | "seededit-3-0-i2i-250628" | "seedream-4-0-250828" | "seedream-4-5-251128";
|
||||
/** @description Text description for image generation or transformation */
|
||||
prompt: string;
|
||||
/**
|
||||
@@ -10170,10 +10269,10 @@ export interface components {
|
||||
};
|
||||
BytePlusVideoGenerationRequest: {
|
||||
/**
|
||||
* @description The ID of the model to call. Available models include seedance-1-0-pro-250528, seedance-1-0-lite-t2v-250428, seedance-1-0-lite-i2v-250428
|
||||
* @description The ID of the model to call. Available models include seedance-1-0-pro-250528, seedance-1-0-pro-fast-251015, seedance-1-0-lite-t2v-250428, seedance-1-0-lite-i2v-250428
|
||||
* @enum {string}
|
||||
*/
|
||||
model: "seedance-1-0-pro-250528" | "seedance-1-0-lite-t2v-250428" | "seedance-1-0-lite-i2v-250428";
|
||||
model: "seedance-1-0-pro-250528" | "seedance-1-0-lite-t2v-250428" | "seedance-1-0-lite-i2v-250428" | "seedance-1-0-pro-fast-251015";
|
||||
/** @description The input content for the model to generate a video */
|
||||
content: components["schemas"]["BytePlusVideoGenerationContent"][];
|
||||
/**
|
||||
@@ -13947,6 +14046,15 @@ export interface operations {
|
||||
"application/json": components["schemas"]["Node"];
|
||||
};
|
||||
};
|
||||
/** @description Redirect to node with normalized name match */
|
||||
302: {
|
||||
headers: {
|
||||
/** @description URL of the node with the correct ID */
|
||||
Location?: string;
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
/** @description Forbidden */
|
||||
403: {
|
||||
headers: {
|
||||
@@ -18345,6 +18453,198 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
klingCreateOmniImage: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** @description Create task for generating omni-image */
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["KlingOmniImageRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful response (Request successful) */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["KlingOmniImageResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Invalid request parameters */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["KlingErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Authentication failed */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["KlingErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized access to requested resource */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["KlingErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Resource not found */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["KlingErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Account exception or Rate limit exceeded */
|
||||
429: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["KlingErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["KlingErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Service temporarily unavailable */
|
||||
503: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["KlingErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Server timeout */
|
||||
504: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["KlingErrorResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
klingOmniImageQuerySingleTask: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
/** @description Task ID or External Task ID. Can query by either task_id (generated by system) or external_task_id (customized task ID) */
|
||||
id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful response (Request successful) */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["KlingOmniImageResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Invalid request parameters */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["KlingErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Authentication failed */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["KlingErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized access to requested resource */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["KlingErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Resource not found */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["KlingErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Account exception or Rate limit exceeded */
|
||||
429: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["KlingErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["KlingErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Service temporarily unavailable */
|
||||
503: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["KlingErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Server timeout */
|
||||
504: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["KlingErrorResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
klingVirtualTryOnQueryTaskList: {
|
||||
parameters: {
|
||||
query?: {
|
||||
|
||||
1648
pnpm-lock.yaml
generated
@@ -5,7 +5,7 @@ packages:
|
||||
catalog:
|
||||
'@alloc/quick-lru': ^5.2.0
|
||||
'@comfyorg/comfyui-electron-types': 0.5.5
|
||||
'@eslint/js': ^9.35.0
|
||||
'@eslint/js': ^9.39.1
|
||||
'@iconify-json/lucide': ^1.1.178
|
||||
'@iconify/json': ^2.2.380
|
||||
'@iconify/tailwind': ^1.1.3
|
||||
@@ -17,7 +17,7 @@ catalog:
|
||||
'@nx/vite': 21.4.1
|
||||
'@pinia/testing': ^0.1.5
|
||||
'@playwright/test': ^1.52.0
|
||||
'@prettier/plugin-oxc': ^0.0.4
|
||||
'@prettier/plugin-oxc': ^0.1.3
|
||||
'@primeuix/forms': 0.0.2
|
||||
'@primeuix/styled': 0.3.2
|
||||
'@primeuix/utils': ^0.3.2
|
||||
@@ -48,15 +48,15 @@ catalog:
|
||||
axios: ^1.8.2
|
||||
cross-env: ^10.1.0
|
||||
dotenv: ^16.4.5
|
||||
eslint: ^9.34.0
|
||||
eslint: ^9.39.1
|
||||
eslint-config-prettier: ^10.1.8
|
||||
eslint-import-resolver-typescript: ^4.4.4
|
||||
eslint-plugin-import-x: ^4.16.1
|
||||
eslint-plugin-oxlint: 1.25.0
|
||||
eslint-plugin-prettier: ^5.5.4
|
||||
eslint-plugin-storybook: ^9.1.6
|
||||
eslint-plugin-unused-imports: ^4.2.0
|
||||
eslint-plugin-vue: ^10.4.0
|
||||
eslint-plugin-storybook: ^9.1.16
|
||||
eslint-plugin-unused-imports: ^4.3.0
|
||||
eslint-plugin-vue: ^10.6.2
|
||||
firebase: ^11.6.0
|
||||
globals: ^15.9.0
|
||||
happy-dom: ^15.11.0
|
||||
@@ -64,29 +64,29 @@ catalog:
|
||||
jiti: 2.4.2
|
||||
jsdom: ^26.1.0
|
||||
knip: ^5.62.0
|
||||
lint-staged: ^15.2.7
|
||||
lint-staged: ^15.5.2
|
||||
markdown-table: ^3.0.4
|
||||
mixpanel-browser: ^2.71.0
|
||||
nx: 21.4.1
|
||||
oxlint: ^1.25.0
|
||||
oxlint-tsgolint: ^0.4.0
|
||||
oxlint: ^1.32.0
|
||||
oxlint-tsgolint: ^0.8.4
|
||||
picocolors: ^1.1.1
|
||||
pinia: ^2.1.7
|
||||
postcss-html: ^1.8.0
|
||||
prettier: ^3.6.2
|
||||
prettier: ^3.7.4
|
||||
pretty-bytes: ^7.1.0
|
||||
primeicons: ^7.0.0
|
||||
primevue: ^4.2.5
|
||||
rollup-plugin-visualizer: ^6.0.4
|
||||
storybook: ^9.1.6
|
||||
stylelint: ^16.24.0
|
||||
storybook: ^9.1.16
|
||||
stylelint: ^16.26.1
|
||||
tailwindcss: ^4.1.12
|
||||
tailwindcss-primeui: ^0.6.1
|
||||
tsx: ^4.15.6
|
||||
tw-animate-css: ^1.3.8
|
||||
typegpu: ^0.8.2
|
||||
typescript: ^5.9.2
|
||||
typescript-eslint: ^8.44.0
|
||||
typescript: ^5.9.3
|
||||
typescript-eslint: ^8.49.0
|
||||
unplugin-icons: ^0.22.0
|
||||
unplugin-typegpu: 0.8.0
|
||||
unplugin-vue-components: ^0.28.0
|
||||
@@ -100,7 +100,7 @@ catalog:
|
||||
vue-eslint-parser: ^10.2.0
|
||||
vue-i18n: ^9.14.3
|
||||
vue-router: ^4.4.3
|
||||
vue-tsc: ^3.0.7
|
||||
vue-tsc: ^3.1.8
|
||||
vuefire: ^3.2.1
|
||||
yjs: ^13.6.27
|
||||
zod: ^3.23.8
|
||||
|
||||
19
scripts/cicd/check-shell.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(git rev-parse --show-toplevel)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
if ! command -v shellcheck >/dev/null 2>&1; then
|
||||
echo "Error: shellcheck is required but not installed" >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
mapfile -t shell_files < <(git ls-files -- '*.sh')
|
||||
|
||||
if [[ ${#shell_files[@]} -eq 0 ]]; then
|
||||
echo 'No shell scripts found.'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
shellcheck --format=gcc "${shell_files[@]}"
|
||||
@@ -74,7 +74,7 @@ deploy_report() {
|
||||
|
||||
|
||||
# Project name with dots converted to dashes for Cloudflare
|
||||
sanitized_browser=$(echo "$browser" | sed 's/\./-/g')
|
||||
sanitized_browser="${browser//./-}"
|
||||
project="comfyui-playwright-${sanitized_browser}"
|
||||
|
||||
echo "Deploying $browser to project $project on branch $branch..." >&2
|
||||
@@ -208,7 +208,7 @@ else
|
||||
|
||||
# Wait for all deployments to complete
|
||||
for pid in $pids; do
|
||||
wait $pid
|
||||
wait "$pid"
|
||||
done
|
||||
|
||||
# Collect URLs and counts in order
|
||||
@@ -254,9 +254,9 @@ else
|
||||
total_tests=0
|
||||
|
||||
# Parse counts and calculate totals
|
||||
IFS='|'
|
||||
set -- $all_counts
|
||||
for counts_json; do
|
||||
IFS='|' read -r -a counts_array <<< "$all_counts"
|
||||
for counts_json in "${counts_array[@]}"; do
|
||||
[ -z "$counts_json" ] && continue
|
||||
if [ "$counts_json" != "{}" ] && [ -n "$counts_json" ]; then
|
||||
# Parse JSON counts using simple grep/sed if jq is not available
|
||||
if command -v jq > /dev/null 2>&1; then
|
||||
@@ -324,13 +324,12 @@ $status_icon **$status_text**
|
||||
|
||||
# Add browser results with individual counts
|
||||
i=0
|
||||
IFS='|'
|
||||
set -- $all_counts
|
||||
for counts_json; do
|
||||
# Get browser name
|
||||
browser=$(echo "$BROWSERS" | cut -d' ' -f$((i + 1)))
|
||||
# Get URL at position i
|
||||
url=$(echo "$urls" | cut -d' ' -f$((i + 1)))
|
||||
IFS=' ' read -r -a browser_array <<< "$BROWSERS"
|
||||
IFS=' ' read -r -a url_array <<< "$urls"
|
||||
for counts_json in "${counts_array[@]}"; do
|
||||
[ -z "$counts_json" ] && { i=$((i + 1)); continue; }
|
||||
browser="${browser_array[$i]:-}"
|
||||
url="${url_array[$i]:-}"
|
||||
|
||||
if [ "$url" != "failed" ] && [ -n "$url" ]; then
|
||||
# Parse individual browser counts
|
||||
@@ -374,4 +373,4 @@ $status_icon **$status_text**
|
||||
🎉 Click on the links above to view detailed test results for each browser configuration."
|
||||
|
||||
post_comment "$comment"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -264,7 +264,7 @@ if (!releaseInfo) {
|
||||
}
|
||||
|
||||
// Output as JSON for GitHub Actions
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
console.log(JSON.stringify(releaseInfo, null, 2))
|
||||
|
||||
export { resolveRelease }
|
||||
|
||||
@@ -1,67 +1,56 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="!workspaceStore.focusMode"
|
||||
class="ml-1 flex gap-x-0.5 pt-1"
|
||||
@mouseenter="isTopMenuHovered = true"
|
||||
@mouseleave="isTopMenuHovered = false"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<SubgraphBreadcrumb />
|
||||
<div v-if="!workspaceStore.focusMode" class="ml-1 flex flex-col gap-1 pt-1">
|
||||
<div class="flex gap-x-0.5">
|
||||
<div class="min-w-0 flex-1">
|
||||
<SubgraphBreadcrumb />
|
||||
</div>
|
||||
|
||||
<div class="mx-1 flex flex-col items-end gap-1">
|
||||
<div
|
||||
ref="actionbarContainerRef"
|
||||
class="actionbar-container relative pointer-events-auto flex h-12 items-center overflow-hidden rounded-lg border border-interface-stroke px-2 shadow-interface"
|
||||
>
|
||||
<ActionBarButtons />
|
||||
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
|
||||
<div
|
||||
ref="legacyCommandsContainerRef"
|
||||
class="[&:not(:has(*>*:not(:empty)))]:hidden"
|
||||
></div>
|
||||
<ComfyActionbar
|
||||
v-model:docked="isActionbarDocked"
|
||||
v-model:queue-overlay-expanded="isQueueOverlayExpanded"
|
||||
:top-menu-container="actionbarContainerRef"
|
||||
/>
|
||||
<CurrentUserButton v-if="isLoggedIn" class="shrink-0" />
|
||||
<LoginButton v-else-if="isDesktop" />
|
||||
<IconButton
|
||||
v-if="!isRightSidePanelOpen"
|
||||
v-tooltip.bottom="rightSidePanelTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="mr-2 text-base-foreground transition-colors duration-200 ease-in-out bg-secondary-background hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
|
||||
:aria-label="t('rightSidePanel.togglePanel')"
|
||||
@click="rightSidePanelStore.togglePanel"
|
||||
>
|
||||
<i class="icon-[lucide--panel-right] size-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<QueueProgressOverlay v-model:expanded="isQueueOverlayExpanded" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mx-1 flex flex-col items-end gap-1">
|
||||
<div
|
||||
class="actionbar-container pointer-events-auto flex h-12 items-center rounded-lg border border-interface-stroke px-2 shadow-interface"
|
||||
>
|
||||
<ActionBarButtons />
|
||||
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
|
||||
<div
|
||||
ref="legacyCommandsContainerRef"
|
||||
class="[&:not(:has(*>*:not(:empty)))]:hidden"
|
||||
></div>
|
||||
<ComfyActionbar />
|
||||
<IconButton
|
||||
v-tooltip.bottom="queueHistoryTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="relative mr-2 text-base-foreground transition-colors duration-200 ease-in-out bg-secondary-background hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
|
||||
:aria-pressed="isQueueOverlayExpanded"
|
||||
:aria-label="
|
||||
t('sideToolbar.queueProgressOverlay.expandCollapsedQueue')
|
||||
"
|
||||
@click="toggleQueueOverlay"
|
||||
>
|
||||
<i class="icon-[lucide--history] size-4" />
|
||||
<span
|
||||
v-if="queuedCount > 0"
|
||||
class="absolute -top-1 -right-1 min-w-[16px] rounded-full bg-primary-background py-0.25 text-[10px] font-medium leading-[14px] text-white"
|
||||
>
|
||||
{{ queuedCount }}
|
||||
</span>
|
||||
</IconButton>
|
||||
<CurrentUserButton v-if="isLoggedIn" class="shrink-0" />
|
||||
<LoginButton v-else-if="isDesktop" />
|
||||
<IconButton
|
||||
v-if="!isRightSidePanelOpen"
|
||||
v-tooltip.bottom="rightSidePanelTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="mr-2 text-base-foreground transition-colors duration-200 ease-in-out bg-secondary-background hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
|
||||
:aria-label="t('rightSidePanel.togglePanel')"
|
||||
@click="rightSidePanelStore.togglePanel"
|
||||
>
|
||||
<i class="icon-[lucide--panel-right] size-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<QueueProgressOverlay
|
||||
v-model:expanded="isQueueOverlayExpanded"
|
||||
:menu-hovered="isTopMenuHovered"
|
||||
<div>
|
||||
<QueueInlineProgressSummary
|
||||
v-if="!isActionbarFloating"
|
||||
class="pr-1"
|
||||
:hidden="isQueueOverlayExpanded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -69,33 +58,39 @@ import { useI18n } from 'vue-i18n'
|
||||
import ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue'
|
||||
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import QueueInlineProgressSummary from '@/components/queue/QueueInlineProgressSummary.vue'
|
||||
import QueueProgressOverlay from '@/components/queue/QueueProgressOverlay.vue'
|
||||
import ActionBarButtons from '@/components/topbar/ActionBarButtons.vue'
|
||||
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
|
||||
import LoginButton from '@/components/topbar/LoginButton.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useQueueStore } from '@/stores/queueStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const settingsStore = useSettingStore()
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
const isDesktop = isElectron()
|
||||
const { t } = useI18n()
|
||||
const isQueueOverlayExpanded = ref(false)
|
||||
const queueStore = useQueueStore()
|
||||
const isTopMenuHovered = ref(false)
|
||||
const queuedCount = computed(() => queueStore.pendingTasks.length)
|
||||
const queueHistoryTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory'))
|
||||
const actionbarContainerRef = ref<HTMLElement>()
|
||||
const isActionbarDocked = useLocalStorage('Comfy.MenuPosition.Docked', true)
|
||||
const actionbarPosition = computed(() => settingsStore.get('Comfy.UseNewMenu'))
|
||||
const isActionbarEnabled = computed(
|
||||
() => actionbarPosition.value !== 'Disabled'
|
||||
)
|
||||
const isActionbarFloating = computed(
|
||||
() => isActionbarEnabled.value && !isActionbarDocked.value
|
||||
)
|
||||
|
||||
// Right side panel toggle
|
||||
const { isOpen: isRightSidePanelOpen } = storeToRefs(rightSidePanelStore)
|
||||
|
||||
const rightSidePanelTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('rightSidePanel.togglePanel'))
|
||||
)
|
||||
@@ -108,10 +103,6 @@ onMounted(() => {
|
||||
legacyCommandsContainerRef.value.appendChild(app.menu.element)
|
||||
}
|
||||
})
|
||||
|
||||
const toggleQueueOverlay = () => {
|
||||
isQueueOverlayExpanded.value = !isQueueOverlayExpanded.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex h-full items-center">
|
||||
<div
|
||||
v-if="isDragging && !isDocked"
|
||||
v-if="isDragging && !docked"
|
||||
:class="actionbarClass"
|
||||
@mouseenter="onMouseEnterDropZone"
|
||||
@mouseleave="onMouseLeaveDropZone"
|
||||
@@ -9,45 +9,101 @@
|
||||
{{ t('actionbar.dockToTop') }}
|
||||
</div>
|
||||
|
||||
<Panel
|
||||
class="pointer-events-auto"
|
||||
:style="style"
|
||||
<div
|
||||
ref="actionbarWrapperRef"
|
||||
:class="panelClass"
|
||||
:pt="{
|
||||
header: { class: 'hidden' },
|
||||
content: { class: isDocked ? 'p-0' : 'p-1' }
|
||||
}"
|
||||
:style="style"
|
||||
class="flex flex-col items-stretch"
|
||||
>
|
||||
<div ref="panelRef" class="flex items-center select-none">
|
||||
<span
|
||||
ref="dragHandleRef"
|
||||
:class="
|
||||
cn(
|
||||
'drag-handle cursor-grab w-3 h-max mr-2',
|
||||
isDragging && 'cursor-grabbing'
|
||||
)
|
||||
"
|
||||
/>
|
||||
|
||||
<ComfyRunButton />
|
||||
<IconButton
|
||||
v-tooltip.bottom="cancelJobTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="ml-2 bg-destructive-background text-base-foreground transition-colors duration-200 ease-in-out hover:bg-destructive-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-destructive-background"
|
||||
:disabled="isExecutionIdle"
|
||||
:aria-label="t('menu.interrupt')"
|
||||
@click="cancelCurrentJob"
|
||||
<Panel
|
||||
:class="
|
||||
cn(
|
||||
panelRootClass,
|
||||
isDragging ? 'pointer-events-none' : 'pointer-events-auto'
|
||||
)
|
||||
"
|
||||
:pt="{
|
||||
header: { class: 'hidden' },
|
||||
content: { class: 'p-0' }
|
||||
}"
|
||||
>
|
||||
<div
|
||||
ref="panelRef"
|
||||
:class="cn('flex flex-col', docked ? 'p-0' : 'p-1')"
|
||||
>
|
||||
<i class="icon-[lucide--x] size-4" />
|
||||
</IconButton>
|
||||
<div class="flex items-center select-none">
|
||||
<span
|
||||
ref="dragHandleRef"
|
||||
:class="
|
||||
cn(
|
||||
'drag-handle cursor-grab w-3 h-max mr-2',
|
||||
isDragging && 'cursor-grabbing'
|
||||
)
|
||||
"
|
||||
/>
|
||||
|
||||
<Suspense @resolve="comfyRunButtonResolved">
|
||||
<ComfyRunButton />
|
||||
</Suspense>
|
||||
<IconButton
|
||||
v-tooltip.bottom="cancelJobTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="ml-2 bg-destructive-background text-base-foreground transition-colors duration-200 ease-in-out hover:bg-destructive-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-destructive-background"
|
||||
:disabled="isExecutionIdle"
|
||||
:aria-label="t('menu.interrupt')"
|
||||
@click="cancelCurrentJob"
|
||||
>
|
||||
<i class="icon-[lucide--x] size-4" />
|
||||
</IconButton>
|
||||
<IconTextButton
|
||||
v-tooltip.bottom="queueHistoryTooltipConfig"
|
||||
size="sm"
|
||||
type="secondary"
|
||||
icon-position="right"
|
||||
data-testid="queue-toggle-button"
|
||||
class="ml-2 h-8 border-0 px-3 text-sm font-medium text-base-foreground cursor-pointer"
|
||||
:aria-pressed="props.queueOverlayExpanded"
|
||||
:aria-label="queueToggleLabel"
|
||||
:label="queueToggleLabel"
|
||||
@click="toggleQueueOverlay"
|
||||
>
|
||||
<!-- Custom implementation for static 1-2 digit shifts -->
|
||||
<span class="flex items-center gap-1">
|
||||
<span
|
||||
class="inline-flex min-w-[2ch] justify-center tabular-nums text-center"
|
||||
>
|
||||
{{ queuedCount }}
|
||||
</span>
|
||||
<span>{{ queuedSuffix }}</span>
|
||||
</span>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--chevron-down] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
<div v-if="isFloating" class="flex justify-end pt-1 pr-1">
|
||||
<QueueInlineProgressSummary
|
||||
class="pr-1"
|
||||
:hidden="props.queueOverlayExpanded"
|
||||
/>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
|
||||
<Teleport v-if="inlineProgressTarget" :to="inlineProgressTarget">
|
||||
<QueueInlineProgress
|
||||
:hidden="props.queueOverlayExpanded"
|
||||
data-testid="queue-inline-progress"
|
||||
/>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
unrefElement,
|
||||
useDraggable,
|
||||
useEventListener,
|
||||
useLocalStorage,
|
||||
@@ -56,35 +112,60 @@ import {
|
||||
import { clamp } from 'es-toolkit/compat'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import Panel from 'primevue/panel'
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import QueueInlineProgress from '@/components/queue/QueueInlineProgress.vue'
|
||||
import QueueInlineProgressSummary from '@/components/queue/QueueInlineProgressSummary.vue'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import { t } from '@/i18n'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useQueueStore } from '@/stores/queueStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import ComfyRunButton from './ComfyRunButton'
|
||||
|
||||
const props = defineProps<{
|
||||
queueOverlayExpanded: boolean
|
||||
topMenuContainer?: HTMLElement | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:queueOverlayExpanded', value: boolean): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const settingsStore = useSettingStore()
|
||||
const executionStore = useExecutionStore()
|
||||
const commandStore = useCommandStore()
|
||||
const { isIdle: isExecutionIdle } = storeToRefs(useExecutionStore())
|
||||
const queueStore = useQueueStore()
|
||||
|
||||
const position = computed(() => settingsStore.get('Comfy.UseNewMenu'))
|
||||
const visible = computed(() => position.value !== 'Disabled')
|
||||
|
||||
const tabContainer = document.querySelector('.workflow-tabs-container')
|
||||
const actionbarWrapperRef = ref<HTMLElement | null>(null)
|
||||
const panelRef = ref<HTMLElement | null>(null)
|
||||
const dragHandleRef = ref<HTMLElement | null>(null)
|
||||
const isDocked = useLocalStorage('Comfy.MenuPosition.Docked', true)
|
||||
const docked = defineModel<boolean>('docked', { default: false })
|
||||
const storedPosition = useLocalStorage('Comfy.MenuPosition.Floating', {
|
||||
x: 0,
|
||||
y: 0
|
||||
})
|
||||
const { x, y, style, isDragging } = useDraggable(panelRef, {
|
||||
const wrapperElement = computed(() => {
|
||||
const element = unrefElement(actionbarWrapperRef)
|
||||
return element instanceof HTMLElement ? element : null
|
||||
})
|
||||
const panelElement = computed(() => {
|
||||
const element = unrefElement(panelRef)
|
||||
return element instanceof HTMLElement ? element : null
|
||||
})
|
||||
const { x, y, style, isDragging } = useDraggable(wrapperElement, {
|
||||
initialValue: { x: 0, y: 0 },
|
||||
handle: dragHandleRef,
|
||||
containerElement: document.body,
|
||||
@@ -97,6 +178,33 @@ const { x, y, style, isDragging } = useDraggable(panelRef, {
|
||||
}
|
||||
})
|
||||
|
||||
// Queue and Execution logic
|
||||
const { isIdle: isExecutionIdle } = storeToRefs(executionStore)
|
||||
const queuedCount = computed(() => queueStore.pendingTasks.length)
|
||||
const queueToggleLabel = computed(() =>
|
||||
t('sideToolbar.queueProgressOverlay.toggleLabel', {
|
||||
count: queuedCount.value
|
||||
})
|
||||
)
|
||||
const queuedSuffix = computed(() =>
|
||||
t('sideToolbar.queueProgressOverlay.queuedSuffix')
|
||||
)
|
||||
const queueHistoryTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory'))
|
||||
)
|
||||
const cancelJobTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('menu.interrupt'))
|
||||
)
|
||||
|
||||
const toggleQueueOverlay = () => {
|
||||
emit('update:queueOverlayExpanded', !props.queueOverlayExpanded)
|
||||
}
|
||||
|
||||
const cancelCurrentJob = async () => {
|
||||
if (isExecutionIdle.value) return
|
||||
await commandStore.execute('Comfy.Interrupt')
|
||||
}
|
||||
|
||||
// Update storedPosition when x or y changes
|
||||
watchDebounced(
|
||||
[x, y],
|
||||
@@ -108,11 +216,12 @@ watchDebounced(
|
||||
|
||||
// Set initial position to bottom center
|
||||
const setInitialPosition = () => {
|
||||
if (panelRef.value) {
|
||||
const containerEl = wrapperElement.value
|
||||
if (containerEl) {
|
||||
const screenWidth = window.innerWidth
|
||||
const screenHeight = window.innerHeight
|
||||
const menuWidth = panelRef.value.offsetWidth
|
||||
const menuHeight = panelRef.value.offsetHeight
|
||||
const menuWidth = containerEl.offsetWidth
|
||||
const menuHeight = containerEl.offsetHeight
|
||||
|
||||
if (menuWidth === 0 || menuHeight === 0) {
|
||||
return
|
||||
@@ -139,7 +248,14 @@ const setInitialPosition = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
onMounted(setInitialPosition)
|
||||
|
||||
//The ComfyRunButton is a dynamic import. Which means it will not be loaded onMount in this component.
|
||||
//So we must use suspense resolve to ensure that is has loaded and updated the DOM before calling setInitialPosition()
|
||||
async function comfyRunButtonResolved() {
|
||||
await nextTick()
|
||||
setInitialPosition()
|
||||
}
|
||||
|
||||
watch(visible, async (newVisible) => {
|
||||
if (newVisible) {
|
||||
await nextTick(setInitialPosition)
|
||||
@@ -181,11 +297,12 @@ watch(
|
||||
)
|
||||
|
||||
const adjustMenuPosition = () => {
|
||||
if (panelRef.value) {
|
||||
const containerEl = wrapperElement.value
|
||||
if (containerEl) {
|
||||
const screenWidth = window.innerWidth
|
||||
const screenHeight = window.innerHeight
|
||||
const menuWidth = panelRef.value.offsetWidth
|
||||
const menuHeight = panelRef.value.offsetHeight
|
||||
const menuWidth = containerEl.offsetWidth
|
||||
const menuHeight = containerEl.offsetHeight
|
||||
|
||||
// Calculate distances to all edges
|
||||
const distanceLeft = lastDragState.value.x
|
||||
@@ -256,31 +373,27 @@ const onMouseLeaveDropZone = () => {
|
||||
watch(isDragging, (dragging) => {
|
||||
if (dragging) {
|
||||
// Starting to drag - undock if docked
|
||||
if (isDocked.value) {
|
||||
isDocked.value = false
|
||||
if (docked.value) {
|
||||
docked.value = false
|
||||
}
|
||||
} else {
|
||||
// Stopped dragging - dock if mouse is over drop zone
|
||||
if (isMouseOverDropZone.value) {
|
||||
isDocked.value = true
|
||||
docked.value = true
|
||||
}
|
||||
// Reset drop zone state
|
||||
isMouseOverDropZone.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const cancelJobTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('menu.interrupt'))
|
||||
)
|
||||
|
||||
const cancelCurrentJob = async () => {
|
||||
if (isExecutionIdle.value) return
|
||||
await commandStore.execute('Comfy.Interrupt')
|
||||
}
|
||||
|
||||
const isFloating = computed(() => visible.value && !docked.value)
|
||||
const inlineProgressTarget = computed(() => {
|
||||
if (!visible.value) return null
|
||||
if (isFloating.value) return panelElement.value
|
||||
return props.topMenuContainer ?? null
|
||||
})
|
||||
const actionbarClass = computed(() =>
|
||||
cn(
|
||||
'w-[200px] border-dashed border-blue-500 opacity-80',
|
||||
'w-[300px] border-dashed border-blue-500 opacity-80',
|
||||
'm-1.5 flex items-center justify-center self-stretch',
|
||||
'rounded-md before:w-50 before:-ml-50 before:h-full',
|
||||
'pointer-events-auto',
|
||||
@@ -290,11 +403,21 @@ const actionbarClass = computed(() =>
|
||||
)
|
||||
const panelClass = computed(() =>
|
||||
cn(
|
||||
'actionbar pointer-events-auto z-1300',
|
||||
isDragging.value && 'select-none pointer-events-none',
|
||||
isDocked.value
|
||||
? 'p-0 static mr-2 border-none bg-transparent'
|
||||
: 'fixed shadow-interface'
|
||||
'actionbar z-1300 overflow-hidden rounded-[var(--p-panel-border-radius)]',
|
||||
docked.value ? 'p-0 static mr-2 border-none bg-transparent' : 'fixed',
|
||||
isDragging.value ? 'select-none pointer-events-none' : 'pointer-events-auto'
|
||||
)
|
||||
)
|
||||
const panelRootClass = computed(() =>
|
||||
cn(
|
||||
'relative overflow-hidden rounded-[var(--p-panel-border-radius)]',
|
||||
docked.value
|
||||
? 'border-none shadow-none bg-transparent'
|
||||
: 'border border-interface-stroke shadow-interface'
|
||||
)
|
||||
)
|
||||
|
||||
defineExpose({
|
||||
isFloating
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -58,7 +58,7 @@ const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore())
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const hasMissingNodes = computed(() =>
|
||||
graphHasMissingNodes(app.graph, nodeDefStore.nodeDefsByName)
|
||||
graphHasMissingNodes(app.rootGraph, nodeDefStore.nodeDefsByName)
|
||||
)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -55,7 +55,6 @@ import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
const { t } = useI18n()
|
||||
|
||||
const { subcategories } = defineProps<{
|
||||
commands: ComfyCommandImpl[]
|
||||
subcategories: Record<string, ComfyCommandImpl[]>
|
||||
}>()
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
class="icon-[lucide--triangle-alert] text-warning-background"
|
||||
/>
|
||||
<span class="p-breadcrumb-item-label px-2">{{ item.label }}</span>
|
||||
<Tag v-if="item.isBlueprint" :value="'Blueprint'" severity="primary" />
|
||||
<Tag v-if="item.isBlueprint" value="Blueprint" severity="primary" />
|
||||
<i v-if="isActive" class="pi pi-angle-down text-[10px]"></i>
|
||||
</a>
|
||||
<Menu
|
||||
@@ -83,7 +83,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const hasMissingNodes = computed(() =>
|
||||
graphHasMissingNodes(app.graph, nodeDefStore.nodeDefsByName)
|
||||
graphHasMissingNodes(app.rootGraph, nodeDefStore.nodeDefsByName)
|
||||
)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -7,14 +7,15 @@
|
||||
@click="onClick"
|
||||
>
|
||||
<slot v-if="iconPosition !== 'right'" name="icon"></slot>
|
||||
<span>{{ label }}</span>
|
||||
<slot v-if="hasDefaultSlot"></slot>
|
||||
<span v-else>{{ label }}</span>
|
||||
<slot v-if="iconPosition === 'right'" name="icon"></slot>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
import { computed, useSlots } from 'vue'
|
||||
|
||||
import type { BaseButtonProps } from '@/types/buttonTypes'
|
||||
import {
|
||||
@@ -46,6 +47,9 @@ const {
|
||||
onClick
|
||||
} = defineProps<IconTextButtonProps>()
|
||||
|
||||
const slots = useSlots()
|
||||
const hasDefaultSlot = computed(() => Boolean(slots.default?.().length))
|
||||
|
||||
const buttonStyle = computed(() => {
|
||||
const baseClasses = `${getBaseButtonClasses()} justify-start gap-2`
|
||||
const sizeClasses = getButtonSizeClasses(size)
|
||||
|
||||
@@ -41,7 +41,7 @@ const {
|
||||
inputAttrs?: Record<string, string>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'edit', 'cancel'])
|
||||
const emit = defineEmits(['edit', 'cancel'])
|
||||
const inputValue = ref<string>(modelValue)
|
||||
const inputRef = ref<InstanceType<typeof InputText> | undefined>()
|
||||
const isCanceling = ref(false)
|
||||
|
||||
@@ -11,7 +11,6 @@ import InputText from 'primevue/inputtext'
|
||||
|
||||
const modelValue = defineModel<string>('modelValue')
|
||||
defineProps<{
|
||||
defaultValue?: string
|
||||
label?: string
|
||||
}>()
|
||||
|
||||
|
||||