Compare commits
86 Commits
test-fix
...
live-previ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee4599368c | ||
|
|
c781421cad | ||
|
|
bdf6d4dea2 | ||
|
|
b8a796212c | ||
|
|
bc553f12be | ||
|
|
6bb35d46c1 | ||
|
|
68c38f0098 | ||
|
|
236247f05f | ||
|
|
87d6d18c57 | ||
|
|
87106ccb95 | ||
|
|
a20fb7d260 | ||
|
|
836cd7f9ba | ||
|
|
acd855601c | ||
|
|
423a2e76bc | ||
|
|
26578981d4 | ||
|
|
38fb53dca8 | ||
|
|
a832141a45 | ||
|
|
d1f0211b61 | ||
|
|
cc42c2967c | ||
|
|
bb51a5aa76 | ||
|
|
674d884e79 | ||
|
|
6f89d9a9f8 | ||
|
|
08b206f191 | ||
|
|
14d94da52b | ||
|
|
a521066b25 | ||
|
|
ada0993572 | ||
|
|
e42715086e | ||
|
|
92968f3f9b | ||
|
|
73692464ef | ||
|
|
61b3ca046a | ||
|
|
f8912ebaf4 | ||
|
|
80b87c1277 | ||
|
|
00fa9b691b | ||
|
|
0cff8eb357 | ||
|
|
7a0302ba7a | ||
|
|
a4d979e4c9 | ||
|
|
2f81e5b30a | ||
|
|
471ccca1dd | ||
|
|
3effe714f3 | ||
|
|
c43a4990a9 | ||
|
|
8dd5a9900b | ||
|
|
7a11dc59b6 | ||
|
|
ba768c32f3 | ||
|
|
e3f19ab856 | ||
|
|
a9f416233d | ||
|
|
b23a92b442 | ||
|
|
8c3caa77d6 | ||
|
|
ad6dda4435 | ||
|
|
c43cd287bb | ||
|
|
b347dd1734 | ||
|
|
1a6913c466 | ||
|
|
f80fc4cf9a | ||
|
|
8b8f3538bf | ||
|
|
ecd87ae0f4 | ||
|
|
693d408c4a | ||
|
|
1feee48284 | ||
|
|
bd6825a274 | ||
|
|
f490b81be5 | ||
|
|
ddbd26c062 | ||
|
|
adfd2e514e | ||
|
|
f0f554392d | ||
|
|
e639577685 | ||
|
|
596add9f63 | ||
|
|
5e4965d131 | ||
|
|
63ca4a3779 | ||
|
|
60647fd5b9 | ||
|
|
bbfada561e | ||
|
|
c4477fc7ab | ||
|
|
ef46f0cbf4 | ||
|
|
02d303c039 | ||
|
|
23b0d2eb7f | ||
|
|
cfbd5361d3 | ||
|
|
1e71eae177 | ||
|
|
2c3c97d4b5 | ||
|
|
f97cf77e75 | ||
|
|
2cf3739236 | ||
|
|
30fc784ae4 | ||
|
|
d14c416cc4 | ||
|
|
1f78b59afc | ||
|
|
cbe7e8967c | ||
|
|
2542449d45 | ||
|
|
3c550e953a | ||
|
|
6541f5cda5 | ||
|
|
812f1797d5 | ||
|
|
0be1da2041 | ||
|
|
879cb8f1a8 |
@@ -28,7 +28,7 @@ runs:
|
||||
REPO="${{ github.repository }}"
|
||||
|
||||
if [[ -z "$VERSION_FILE" ]]; then
|
||||
echo '::error::version_file input is required' >&2
|
||||
echo "::error::version_file input is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -55,11 +55,13 @@ runs:
|
||||
|
||||
case "$VERSION_FILE" in
|
||||
package.json)
|
||||
LINKS_VALUE=$'PyPI|https://pypi.org/project/comfyui-frontend-package/{{version}}/\n''npm types|https://npm.im/@comfyorg/comfyui-frontend-types@{{version}}'
|
||||
LINKS_VALUE=$(printf '%s\n%s' \
|
||||
'PyPI|https://pypi.org/project/comfyui-frontend-package/{{version}}/' \
|
||||
'npm types|https://www.npmjs.com/package/@comfyorg/comfyui-frontend-types/v/{{version}}')
|
||||
;;
|
||||
apps/desktop-ui/package.json)
|
||||
MARKER='desktop-release-summary'
|
||||
LINKS_VALUE='npm desktop UI|https://npm.im/@comfyorg/desktop-ui@{{version}}'
|
||||
LINKS_VALUE='npm desktop UI|https://www.npmjs.com/package/@comfyorg/desktop-ui/v/{{version}}'
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -69,12 +71,13 @@ runs:
|
||||
COMMENT_FILE=$(mktemp)
|
||||
|
||||
{
|
||||
printf '<!--%s:%s%s-->\n' "$MARKER" "$DIFF_PREFIX" "$NEW_VERSION"
|
||||
printf '%s\n\n' "$MESSAGE"
|
||||
printf -- '- %s: [%s%s...%s%s](%s)\n' "$DIFF_LABEL" "$DIFF_PREFIX" "$PREV_VERSION" "$DIFF_PREFIX" "$NEW_VERSION" "$DIFF_URL"
|
||||
echo "<!--$MARKER:$DIFF_PREFIX$NEW_VERSION-->"
|
||||
echo "$MESSAGE"
|
||||
echo ""
|
||||
echo "- $DIFF_LABEL: [\`$DIFF_PREFIX$PREV_VERSION...$DIFF_PREFIX$NEW_VERSION\`]($DIFF_URL)"
|
||||
|
||||
while IFS= read -r RAW_LINE; do
|
||||
LINE=$(printf '%s' "$RAW_LINE" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
LINE=$(echo "$RAW_LINE" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
[[ -z "$LINE" ]] && continue
|
||||
if [[ "$LINE" != *"|"* ]]; then
|
||||
echo "::warning::Skipping malformed link entry: $LINE" >&2
|
||||
@@ -84,16 +87,16 @@ runs:
|
||||
URL_TEMPLATE=${LINE#*|}
|
||||
URL=${URL_TEMPLATE//\{\{version\}\}/$NEW_VERSION}
|
||||
URL=${URL//\{\{prev_version\}\}/$PREV_VERSION}
|
||||
printf -- '- %s: %s\n' "$LABEL" "$URL"
|
||||
echo "- $LABEL: [\`$NEW_VERSION\`]($URL)"
|
||||
done <<< "$LINKS_VALUE"
|
||||
|
||||
printf '\n'
|
||||
echo ""
|
||||
} > "$COMMENT_FILE"
|
||||
|
||||
{
|
||||
echo "body<<'EOF'"
|
||||
echo "body<<COMMENT_BODY_END_MARKER"
|
||||
cat "$COMMENT_FILE"
|
||||
echo 'EOF'
|
||||
echo "COMMENT_BODY_END_MARKER"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
echo "prev_version=$PREV_VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "marker_search=<!--$MARKER:" >> "$GITHUB_OUTPUT"
|
||||
|
||||
2
.github/workflows/ci-json-validation.yaml
vendored
@@ -6,6 +6,8 @@ on:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
paths:
|
||||
- '**/*.json'
|
||||
|
||||
jobs:
|
||||
json-lint:
|
||||
|
||||
33
.github/workflows/ci-yaml-validation.yaml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: "CI: YAML Validation"
|
||||
description: "Validates YAML syntax and style using yamllint with relaxed rules"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '**/*.yml'
|
||||
- '**/*.yaml'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**/*.yml'
|
||||
- '**/*.yaml'
|
||||
|
||||
jobs:
|
||||
yaml-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install yamllint
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install yamllint
|
||||
|
||||
- name: Validate YAML syntax and style
|
||||
run: ./scripts/cicd/check-yaml.sh
|
||||
69
.github/workflows/pr-backport.yaml
vendored
@@ -164,6 +164,7 @@ jobs:
|
||||
|
||||
PENDING=()
|
||||
SKIPPED=()
|
||||
REUSED=()
|
||||
|
||||
for target in $REQUESTED_TARGETS; do
|
||||
SAFE_TARGET=$(echo "$target" | tr '/' '-')
|
||||
@@ -176,10 +177,22 @@ jobs:
|
||||
|
||||
if printf '%s\n' "${EXISTING_BRANCHES[@]:-}" |
|
||||
grep -Fq "refs/heads/${BACKPORT_BRANCH}"; then
|
||||
SKIPPED+=("$target")
|
||||
else
|
||||
PENDING+=("$target")
|
||||
OPEN_PR=$(
|
||||
gh pr list \
|
||||
--state open \
|
||||
--head "${BACKPORT_BRANCH}" \
|
||||
--json number \
|
||||
--jq 'if length > 0 then .[0].number else "" end'
|
||||
)
|
||||
if [ -n "$OPEN_PR" ]; then
|
||||
SKIPPED+=("${target} (PR #${OPEN_PR})")
|
||||
continue
|
||||
fi
|
||||
|
||||
REUSED+=("$BACKPORT_BRANCH")
|
||||
fi
|
||||
|
||||
PENDING+=("$target")
|
||||
done
|
||||
|
||||
SKIPPED_JOINED="${SKIPPED[*]:-}"
|
||||
@@ -187,16 +200,20 @@ jobs:
|
||||
|
||||
echo "already-exists=${SKIPPED_JOINED}" >> $GITHUB_OUTPUT
|
||||
echo "pending-targets=${PENDING_JOINED}" >> $GITHUB_OUTPUT
|
||||
echo "reused-branches=${REUSED[*]:-}" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ -z "$PENDING_JOINED" ]; then
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
if [ -n "$SKIPPED_JOINED" ]; then
|
||||
echo "::warning::Backport branches already exist for: ${SKIPPED_JOINED}"
|
||||
echo "::warning::Backport branches exist: ${SKIPPED_JOINED}"
|
||||
fi
|
||||
else
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
if [ -n "$SKIPPED_JOINED" ]; then
|
||||
echo "::notice::Skipping already backported targets: ${SKIPPED_JOINED}"
|
||||
echo "::notice::Skipping backport targets: ${SKIPPED_JOINED}"
|
||||
fi
|
||||
if [ "${#REUSED[@]}" -gt 0 ]; then
|
||||
echo "::notice::Reusing backport branches: ${REUSED[*]}"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -209,6 +226,11 @@ jobs:
|
||||
FAILED=""
|
||||
SUCCESS=""
|
||||
|
||||
CREATED_BRANCHES_FILE="$(
|
||||
mktemp "$RUNNER_TEMP/backport-branches-XXXXXX"
|
||||
)"
|
||||
echo "CREATED_BRANCHES_FILE=$CREATED_BRANCHES_FILE" >> "$GITHUB_ENV"
|
||||
|
||||
# Get PR data for manual triggers
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,mergeCommit)
|
||||
@@ -223,6 +245,12 @@ jobs:
|
||||
TARGET_BRANCH="${target}"
|
||||
SAFE_TARGET=$(echo "$TARGET_BRANCH" | tr '/' '-')
|
||||
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${SAFE_TARGET}"
|
||||
REMOTE_BACKPORT_EXISTS=false
|
||||
|
||||
if git ls-remote --exit-code origin "${BACKPORT_BRANCH}" >/dev/null 2>&1; then
|
||||
REMOTE_BACKPORT_EXISTS=true
|
||||
echo "::notice::Updating existing branch ${BACKPORT_BRANCH}"
|
||||
fi
|
||||
|
||||
echo "::group::Backporting to ${TARGET_BRANCH}"
|
||||
|
||||
@@ -247,7 +275,12 @@ jobs:
|
||||
|
||||
# Try cherry-pick
|
||||
if git cherry-pick "${MERGE_COMMIT}"; then
|
||||
if [ "$REMOTE_BACKPORT_EXISTS" = true ]; then
|
||||
git push --force-with-lease origin "${BACKPORT_BRANCH}"
|
||||
else
|
||||
git push origin "${BACKPORT_BRANCH}"
|
||||
fi
|
||||
echo "${BACKPORT_BRANCH}" >> "$CREATED_BRANCHES_FILE"
|
||||
SUCCESS="${SUCCESS}${TARGET_BRANCH}:${BACKPORT_BRANCH} "
|
||||
echo "Successfully created backport branch: ${BACKPORT_BRANCH}"
|
||||
# Return to main (keep the branch, we need it for PR)
|
||||
@@ -271,6 +304,13 @@ jobs:
|
||||
echo "success=${SUCCESS}" >> $GITHUB_OUTPUT
|
||||
echo "failed=${FAILED}" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ -s "$CREATED_BRANCHES_FILE" ]; then
|
||||
CREATED_LIST=$(paste -sd' ' "$CREATED_BRANCHES_FILE")
|
||||
echo "created-branches=${CREATED_LIST}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "created-branches=" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
if [ -n "${FAILED}" ]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -348,6 +388,25 @@ jobs:
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Cleanup stranded backport branches
|
||||
if: steps.filter-targets.outputs.skip != 'true' && failure()
|
||||
run: |
|
||||
FILE="${CREATED_BRANCHES_FILE:-}"
|
||||
|
||||
if [ -z "$FILE" ] || [ ! -f "$FILE" ]; then
|
||||
echo "No backport branches recorded for cleanup"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
while IFS= read -r branch; do
|
||||
[ -z "$branch" ] && continue
|
||||
printf 'Deleting branch %s\n' "${branch}"
|
||||
if ! git push origin --delete "$branch"; then
|
||||
echo "::warning::Failed to delete ${branch}"
|
||||
fi
|
||||
done < "$FILE"
|
||||
|
||||
|
||||
- name: Remove needs-backport label
|
||||
if: steps.filter-targets.outputs.skip != 'true' && success()
|
||||
run: gh pr edit ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }} --remove-label "needs-backport"
|
||||
|
||||
72
.github/workflows/release-branch-create.yaml
vendored
@@ -153,8 +153,78 @@ jobs:
|
||||
echo "EOF"
|
||||
} >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Post summary
|
||||
- name: Ensure release labels
|
||||
if: steps.check_version.outputs.is_minor_bump == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PR_GH_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
BRANCH_BASE="${{ steps.check_version.outputs.branch_base }}"
|
||||
|
||||
if [[ -z "$BRANCH_BASE" ]]; then
|
||||
echo "::error::Branch base not set; unable to manage labels"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
declare -A COLORS=(
|
||||
[core]="4361ee"
|
||||
[cloud]="4f6ef5"
|
||||
)
|
||||
|
||||
for PREFIX in core cloud; do
|
||||
LABEL="${PREFIX}/${BRANCH_BASE}"
|
||||
COLOR="${COLORS[$PREFIX]}"
|
||||
DESCRIPTION="Backport PRs for ${PREFIX} ${BRANCH_BASE}"
|
||||
|
||||
if gh label view "$LABEL" >/dev/null 2>&1; then
|
||||
gh label edit "$LABEL" \
|
||||
--color "$COLOR" \
|
||||
--description "$DESCRIPTION"
|
||||
echo "🔄 Updated label $LABEL"
|
||||
else
|
||||
gh label create "$LABEL" \
|
||||
--color "$COLOR" \
|
||||
--description "$DESCRIPTION"
|
||||
echo "✨ Created label $LABEL"
|
||||
fi
|
||||
done
|
||||
|
||||
MIN_LABELS_TO_KEEP=3
|
||||
MAX_LABELS_TO_FETCH=200
|
||||
|
||||
for PREFIX in core cloud; do
|
||||
mapfile -t LABELS < <(
|
||||
gh label list \
|
||||
--json name \
|
||||
--limit "$MAX_LABELS_TO_FETCH" \
|
||||
--jq '.[].name' |
|
||||
grep -E "^${PREFIX}/[0-9]+\.[0-9]+$" |
|
||||
sort -t/ -k2,2V
|
||||
)
|
||||
|
||||
TOTAL=${#LABELS[@]}
|
||||
|
||||
if (( TOTAL <= MIN_LABELS_TO_KEEP )); then
|
||||
echo "ℹ️ Nothing to prune for $PREFIX labels"
|
||||
continue
|
||||
fi
|
||||
|
||||
REMOVE_COUNT=$((TOTAL - MIN_LABELS_TO_KEEP))
|
||||
|
||||
if (( REMOVE_COUNT > 1 )); then
|
||||
REMOVE_COUNT=1
|
||||
fi
|
||||
|
||||
for ((i=0; i<REMOVE_COUNT; i++)); do
|
||||
OLD_LABEL="${LABELS[$i]}"
|
||||
gh label delete "$OLD_LABEL" --yes
|
||||
echo "🗑️ Removed old label $OLD_LABEL"
|
||||
done
|
||||
done
|
||||
|
||||
- name: Post summary
|
||||
if: always() && steps.check_version.outputs.is_minor_bump == 'true'
|
||||
run: |
|
||||
CURRENT_VERSION="${{ steps.check_version.outputs.current_version }}"
|
||||
RESULTS="${{ steps.create_branches.outputs.results }}"
|
||||
|
||||
@@ -92,4 +92,3 @@ jobs:
|
||||
base: ${{ github.event.inputs.branch }}
|
||||
labels: |
|
||||
Release
|
||||
|
||||
|
||||
43
.oxlintrc.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"ignorePatterns": [
|
||||
".i18nrc.cjs",
|
||||
"components.d.ts",
|
||||
"lint-staged.config.js",
|
||||
"vitest.setup.ts",
|
||||
"**/vite.config.*.timestamp*",
|
||||
"**/vitest.config.*.timestamp*",
|
||||
"packages/registry-types/src/comfyRegistryTypes.ts",
|
||||
"src/extensions/core/*",
|
||||
"src/scripts/*",
|
||||
"src/types/generatedManagerTypes.ts",
|
||||
"src/types/vue-shim.d.ts"
|
||||
],
|
||||
"rules": {
|
||||
"no-async-promise-executor": "off",
|
||||
"no-control-regex": "off",
|
||||
"no-eval": "off",
|
||||
"no-self-assign": "allow",
|
||||
"no-unused-expressions": "off",
|
||||
"no-unused-private-class-members": "off",
|
||||
"no-useless-rename": "off",
|
||||
"typescript/no-this-alias": "off",
|
||||
"typescript/no-unnecessary-parameter-property-assignment": "off",
|
||||
"typescript/no-unsafe-declaration-merging": "off",
|
||||
"typescript/no-unused-vars": "off",
|
||||
"unicorn/no-empty-file": "off",
|
||||
"unicorn/no-new-array": "off",
|
||||
"unicorn/no-single-promise-in-promise-methods": "off",
|
||||
"unicorn/no-useless-fallback-in-spread": "off",
|
||||
"unicorn/no-useless-spread": "off",
|
||||
"typescript/await-thenable": "off",
|
||||
"typescript/no-base-to-string": "off",
|
||||
"typescript/no-duplicate-type-constituents": "off",
|
||||
"typescript/no-for-in-array": "off",
|
||||
"typescript/no-meaningless-void-operator": "off",
|
||||
"typescript/no-redundant-type-constituents": "off",
|
||||
"typescript/restrict-template-expressions": "off",
|
||||
"typescript/unbound-method": "off",
|
||||
"typescript/no-floating-promises": "error"
|
||||
}
|
||||
}
|
||||
10
.yamllint
Normal file
@@ -0,0 +1,10 @@
|
||||
extends: default
|
||||
|
||||
ignore: |
|
||||
node_modules/
|
||||
dist/
|
||||
|
||||
rules:
|
||||
line-length: disable
|
||||
document-start: disable
|
||||
truthy: disable
|
||||
@@ -17,6 +17,7 @@ This bootstraps the monorepo with dependencies, builds, tests, and dev server ve
|
||||
- `pnpm typecheck`: Type checking
|
||||
- `pnpm build`: Build for production (via nx)
|
||||
- `pnpm lint`: Linting (via nx)
|
||||
- `pnpm oxlint`: Fast Rust-based linting with Oxc
|
||||
- `pnpm format`: Prettier formatting
|
||||
- `pnpm test:unit`: Run all unit tests
|
||||
- `pnpm test:browser`: Run E2E tests via Playwright
|
||||
|
||||
@@ -243,7 +243,7 @@ pnpm format
|
||||
|
||||
### Styling
|
||||
- Use Tailwind CSS classes instead of custom CSS
|
||||
- Follow the existing dark theme pattern: `dark-theme:` prefix (not `dark:`)
|
||||
- NEVER use `dark:` or `dark-theme:` tailwind variants. Instead use a semantic value from the `style.css` theme, e.g. `bg-node-component-surface`
|
||||
|
||||
### Internationalization
|
||||
- All user-facing strings must use vue-i18n
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
"build-storybook": "storybook build -o dist/storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@comfyorg/comfyui-electron-types": "0.4.73-0",
|
||||
"@comfyorg/comfyui-electron-types": "catalog:",
|
||||
"@comfyorg/shared-frontend-utils": "workspace:*",
|
||||
"@primevue/core": "catalog:",
|
||||
"@primevue/themes": "catalog:",
|
||||
|
||||
@@ -115,19 +115,18 @@ import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Message from 'primevue/message'
|
||||
import { type ModelRef, computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import type { ModelRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import MigrationPicker from '@/components/install/MigrationPicker.vue'
|
||||
import MirrorItem from '@/components/install/mirror/MirrorItem.vue'
|
||||
import {
|
||||
PYPI_MIRROR,
|
||||
PYTHON_MIRROR,
|
||||
type UVMirror
|
||||
} from '@/constants/uvMirrors'
|
||||
import { PYPI_MIRROR, PYTHON_MIRROR } from '@/constants/uvMirrors'
|
||||
import type { UVMirror } from '@/constants/uvMirrors'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import { ValidationState } from '@/utils/validationUtil'
|
||||
|
||||
import MigrationPicker from './MigrationPicker.vue'
|
||||
import MirrorItem from './mirror/MirrorItem.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const installPath = defineModel<string>('installPath', { required: true })
|
||||
@@ -229,6 +228,10 @@ const validatePath = async (path: string | undefined) => {
|
||||
}
|
||||
if (validation.parentMissing) errors.push(t('install.parentMissing'))
|
||||
if (validation.isOneDrive) errors.push(t('install.isOneDrive'))
|
||||
if (validation.isInsideAppInstallDir)
|
||||
errors.push(t('install.insideAppInstallDir'))
|
||||
if (validation.isInsideUpdaterCache)
|
||||
errors.push(t('install.insideUpdaterCache'))
|
||||
|
||||
if (validation.error)
|
||||
errors.push(`${t('install.unhandledError')}: ${validation.error}`)
|
||||
|
||||
@@ -16,7 +16,8 @@ export const DESKTOP_MAINTENANCE_TASKS: Readonly<MaintenanceTask>[] = [
|
||||
execute: async () => await electron.setBasePath(),
|
||||
name: 'Base path',
|
||||
shortDescription: 'Change the application base path.',
|
||||
errorDescription: 'Unable to open the base path. Please select a new one.',
|
||||
errorDescription:
|
||||
'The current base path is invalid or unsafe. Please select a new location.',
|
||||
description:
|
||||
'The base path is the default location where ComfyUI stores data. It is the location for the python environment, and may also contain models, custom nodes, and other extensions.',
|
||||
isInstallationFix: true,
|
||||
|
||||
@@ -85,6 +85,7 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
||||
const electron = electronAPI()
|
||||
|
||||
// Reactive state
|
||||
const lastUpdate = ref<InstallValidation | null>(null)
|
||||
const isRefreshing = ref(false)
|
||||
const isRunningTerminalCommand = computed(() =>
|
||||
tasks.value
|
||||
@@ -97,6 +98,13 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
||||
.some((task) => getRunner(task)?.executing)
|
||||
)
|
||||
|
||||
const unsafeBasePath = computed(
|
||||
() => lastUpdate.value?.unsafeBasePath === true
|
||||
)
|
||||
const unsafeBasePathReason = computed(
|
||||
() => lastUpdate.value?.unsafeBasePathReason
|
||||
)
|
||||
|
||||
// Task list
|
||||
const tasks = ref(DESKTOP_MAINTENANCE_TASKS)
|
||||
|
||||
@@ -123,6 +131,7 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
||||
* @param validationUpdate Update details passed in by electron
|
||||
*/
|
||||
const processUpdate = (validationUpdate: InstallValidation) => {
|
||||
lastUpdate.value = validationUpdate
|
||||
const update = validationUpdate as IndexedUpdate
|
||||
isRefreshing.value = true
|
||||
|
||||
@@ -155,7 +164,11 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
||||
}
|
||||
|
||||
const execute = async (task: MaintenanceTask) => {
|
||||
return getRunner(task).execute(task)
|
||||
const success = await getRunner(task).execute(task)
|
||||
if (success && task.isInstallationFix) {
|
||||
await refreshDesktopTasks()
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -163,6 +176,8 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
||||
isRefreshing,
|
||||
isRunningTerminalCommand,
|
||||
isRunningInstallationFix,
|
||||
unsafeBasePath,
|
||||
unsafeBasePathReason,
|
||||
execute,
|
||||
getRunner,
|
||||
processUpdate,
|
||||
|
||||
159
apps/desktop-ui/src/views/MaintenanceView.stories.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
// eslint-disable-next-line storybook/no-renderer-packages
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
type UnsafeReason = 'appInstallDir' | 'updaterCache' | 'oneDrive' | null
|
||||
type ValidationIssueState = 'OK' | 'warning' | 'error' | 'skipped'
|
||||
|
||||
type ValidationState = {
|
||||
inProgress: boolean
|
||||
installState: string
|
||||
basePath?: ValidationIssueState
|
||||
unsafeBasePath: boolean
|
||||
unsafeBasePathReason: UnsafeReason
|
||||
venvDirectory?: ValidationIssueState
|
||||
pythonInterpreter?: ValidationIssueState
|
||||
pythonPackages?: ValidationIssueState
|
||||
uv?: ValidationIssueState
|
||||
git?: ValidationIssueState
|
||||
vcRedist?: ValidationIssueState
|
||||
upgradePackages?: ValidationIssueState
|
||||
}
|
||||
|
||||
const validationState: ValidationState = {
|
||||
inProgress: false,
|
||||
installState: 'installed',
|
||||
basePath: 'OK',
|
||||
unsafeBasePath: false,
|
||||
unsafeBasePathReason: null,
|
||||
venvDirectory: 'OK',
|
||||
pythonInterpreter: 'OK',
|
||||
pythonPackages: 'OK',
|
||||
uv: 'OK',
|
||||
git: 'OK',
|
||||
vcRedist: 'OK',
|
||||
upgradePackages: 'OK'
|
||||
}
|
||||
|
||||
const createMockElectronAPI = () => {
|
||||
const logListeners: Array<(message: string) => void> = []
|
||||
|
||||
const getValidationUpdate = () => ({
|
||||
...validationState
|
||||
})
|
||||
|
||||
return {
|
||||
getPlatform: () => 'darwin',
|
||||
changeTheme: (_theme: unknown) => {},
|
||||
onLogMessage: (listener: (message: string) => void) => {
|
||||
logListeners.push(listener)
|
||||
},
|
||||
showContextMenu: (_options: unknown) => {},
|
||||
Events: {
|
||||
trackEvent: (_eventName: string, _data?: unknown) => {}
|
||||
},
|
||||
Validation: {
|
||||
onUpdate: (_callback: (update: unknown) => void) => {},
|
||||
async getStatus() {
|
||||
return getValidationUpdate()
|
||||
},
|
||||
async validateInstallation(callback: (update: unknown) => void) {
|
||||
callback(getValidationUpdate())
|
||||
},
|
||||
async complete() {
|
||||
// Only allow completion when the base path is safe
|
||||
return !validationState.unsafeBasePath
|
||||
},
|
||||
dispose: () => {}
|
||||
},
|
||||
setBasePath: () => Promise.resolve(true),
|
||||
reinstall: () => Promise.resolve(),
|
||||
uv: {
|
||||
installRequirements: () => Promise.resolve(),
|
||||
clearCache: () => Promise.resolve(),
|
||||
resetVenv: () => Promise.resolve()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ensureElectronAPI = () => {
|
||||
const globalWindow = window as unknown as { electronAPI?: unknown }
|
||||
if (!globalWindow.electronAPI) {
|
||||
globalWindow.electronAPI = createMockElectronAPI()
|
||||
}
|
||||
|
||||
return globalWindow.electronAPI
|
||||
}
|
||||
|
||||
const MaintenanceView = defineAsyncComponent(async () => {
|
||||
ensureElectronAPI()
|
||||
const module = await import('./MaintenanceView.vue')
|
||||
return module.default
|
||||
})
|
||||
|
||||
const meta: Meta<typeof MaintenanceView> = {
|
||||
title: 'Desktop/Views/MaintenanceView',
|
||||
component: MaintenanceView,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
backgrounds: {
|
||||
default: 'dark',
|
||||
values: [
|
||||
{ name: 'dark', value: '#0a0a0a' },
|
||||
{ name: 'neutral-900', value: '#171717' },
|
||||
{ name: 'neutral-950', value: '#0a0a0a' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
name: 'All tasks OK',
|
||||
render: () => ({
|
||||
components: { MaintenanceView },
|
||||
setup() {
|
||||
validationState.inProgress = false
|
||||
validationState.installState = 'installed'
|
||||
validationState.basePath = 'OK'
|
||||
validationState.unsafeBasePath = false
|
||||
validationState.unsafeBasePathReason = null
|
||||
validationState.venvDirectory = 'OK'
|
||||
validationState.pythonInterpreter = 'OK'
|
||||
validationState.pythonPackages = 'OK'
|
||||
validationState.uv = 'OK'
|
||||
validationState.git = 'OK'
|
||||
validationState.vcRedist = 'OK'
|
||||
validationState.upgradePackages = 'OK'
|
||||
ensureElectronAPI()
|
||||
return {}
|
||||
},
|
||||
template: '<MaintenanceView />'
|
||||
})
|
||||
}
|
||||
|
||||
export const UnsafeBasePathOneDrive: Story = {
|
||||
name: 'Unsafe base path (OneDrive)',
|
||||
render: () => ({
|
||||
components: { MaintenanceView },
|
||||
setup() {
|
||||
validationState.inProgress = false
|
||||
validationState.installState = 'installed'
|
||||
validationState.basePath = 'error'
|
||||
validationState.unsafeBasePath = true
|
||||
validationState.unsafeBasePathReason = 'oneDrive'
|
||||
validationState.venvDirectory = 'OK'
|
||||
validationState.pythonInterpreter = 'OK'
|
||||
validationState.pythonPackages = 'OK'
|
||||
validationState.uv = 'OK'
|
||||
validationState.git = 'OK'
|
||||
validationState.vcRedist = 'OK'
|
||||
validationState.upgradePackages = 'OK'
|
||||
ensureElectronAPI()
|
||||
return {}
|
||||
},
|
||||
template: '<MaintenanceView />'
|
||||
})
|
||||
}
|
||||
@@ -47,6 +47,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Unsafe migration warning -->
|
||||
<div v-if="taskStore.unsafeBasePath" class="my-4">
|
||||
<p class="flex items-start gap-3 text-neutral-300">
|
||||
<Tag
|
||||
icon="pi pi-exclamation-triangle"
|
||||
severity="warn"
|
||||
:value="t('icon.exclamation-triangle')"
|
||||
/>
|
||||
<span>
|
||||
<strong class="block mb-1">
|
||||
{{ t('maintenance.unsafeMigration.title') }}
|
||||
</strong>
|
||||
<span class="block mb-1">
|
||||
{{ unsafeReasonText }}
|
||||
</span>
|
||||
<span class="block text-sm text-neutral-400">
|
||||
{{ t('maintenance.unsafeMigration.action') }}
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Tasks -->
|
||||
<TaskListPanel
|
||||
class="border-neutral-700 border-solid border-x-0 border-y"
|
||||
@@ -89,10 +111,10 @@
|
||||
import { PrimeIcons } from '@primevue/core/api'
|
||||
import Button from 'primevue/button'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import Tag from 'primevue/tag'
|
||||
import Toast from 'primevue/toast'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { watch } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import RefreshButton from '@/components/common/RefreshButton.vue'
|
||||
import StatusTag from '@/components/maintenance/StatusTag.vue'
|
||||
@@ -139,6 +161,27 @@ const filterOptions = ref([
|
||||
/** Filter binding; can be set to show all tasks, or only errors. */
|
||||
const filter = ref<MaintenanceFilter>(filterOptions.value[0])
|
||||
|
||||
const unsafeReasonText = computed(() => {
|
||||
const reason = taskStore.unsafeBasePathReason
|
||||
if (!reason) {
|
||||
return t('maintenance.unsafeMigration.generic')
|
||||
}
|
||||
|
||||
if (reason === 'appInstallDir') {
|
||||
return t('maintenance.unsafeMigration.appInstallDir')
|
||||
}
|
||||
|
||||
if (reason === 'updaterCache') {
|
||||
return t('maintenance.unsafeMigration.updaterCache')
|
||||
}
|
||||
|
||||
if (reason === 'oneDrive') {
|
||||
return t('maintenance.unsafeMigration.oneDrive')
|
||||
}
|
||||
|
||||
return t('maintenance.unsafeMigration.generic')
|
||||
})
|
||||
|
||||
/** If valid, leave the validation window. */
|
||||
const completeValidation = async () => {
|
||||
const isValid = await electron.Validation.complete()
|
||||
|
||||
@@ -71,8 +71,8 @@ const updateConsent = async () => {
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('install.errorUpdatingConsent'),
|
||||
detail: t('install.errorUpdatingConsentDetail'),
|
||||
summary: t('install.settings.errorUpdatingConsent'),
|
||||
detail: t('install.settings.errorUpdatingConsentDetail'),
|
||||
life: 3000
|
||||
})
|
||||
} finally {
|
||||
|
||||
@@ -85,11 +85,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
InstallStage,
|
||||
type InstallStageInfo,
|
||||
type InstallStageName,
|
||||
ProgressStatus
|
||||
import { InstallStage, ProgressStatus } from '@comfyorg/comfyui-electron-types'
|
||||
import type {
|
||||
InstallStageInfo,
|
||||
InstallStageName
|
||||
} from '@comfyorg/comfyui-electron-types'
|
||||
import type { Terminal } from '@xterm/xterm'
|
||||
import Button from 'primevue/button'
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"@/*": ["./src/*"],
|
||||
"@frontend-locales/*": ["../../src/locales/*"]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -16,7 +16,6 @@ import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
|
||||
import { SettingDialog } from './components/SettingDialog'
|
||||
import {
|
||||
NodeLibrarySidebarTab,
|
||||
QueueSidebarTab,
|
||||
WorkflowsSidebarTab
|
||||
} from './components/SidebarTab'
|
||||
import { Topbar } from './components/Topbar'
|
||||
@@ -31,7 +30,6 @@ type WorkspaceStore = ReturnType<typeof useWorkspaceStore>
|
||||
class ComfyMenu {
|
||||
private _nodeLibraryTab: NodeLibrarySidebarTab | null = null
|
||||
private _workflowsTab: WorkflowsSidebarTab | null = null
|
||||
private _queueTab: QueueSidebarTab | null = null
|
||||
private _topbar: Topbar | null = null
|
||||
|
||||
public readonly sideToolbar: Locator
|
||||
@@ -60,11 +58,6 @@ class ComfyMenu {
|
||||
return this._workflowsTab
|
||||
}
|
||||
|
||||
get queueTab() {
|
||||
this._queueTab ??= new QueueSidebarTab(this.page)
|
||||
return this._queueTab
|
||||
}
|
||||
|
||||
get topbar() {
|
||||
this._topbar ??= new Topbar(this.page)
|
||||
return this._topbar
|
||||
|
||||
@@ -148,124 +148,3 @@ export class WorkflowsSidebarTab extends SidebarTab {
|
||||
.click()
|
||||
}
|
||||
}
|
||||
|
||||
export class QueueSidebarTab extends SidebarTab {
|
||||
constructor(public readonly page: Page) {
|
||||
super(page, 'queue')
|
||||
}
|
||||
|
||||
get root() {
|
||||
return this.page.locator('.sidebar-content-container', { hasText: 'Queue' })
|
||||
}
|
||||
|
||||
get tasks() {
|
||||
return this.root.locator('[data-virtual-grid-item]')
|
||||
}
|
||||
|
||||
get visibleTasks() {
|
||||
return this.tasks.locator('visible=true')
|
||||
}
|
||||
|
||||
get clearButton() {
|
||||
return this.root.locator('.clear-all-button')
|
||||
}
|
||||
|
||||
get collapseTasksButton() {
|
||||
return this.getToggleExpandButton(false)
|
||||
}
|
||||
|
||||
get expandTasksButton() {
|
||||
return this.getToggleExpandButton(true)
|
||||
}
|
||||
|
||||
get noResultsPlaceholder() {
|
||||
return this.root.locator('.no-results-placeholder')
|
||||
}
|
||||
|
||||
get galleryImage() {
|
||||
return this.page.locator('.galleria-image')
|
||||
}
|
||||
|
||||
private getToggleExpandButton(isExpanded: boolean) {
|
||||
const iconSelector = isExpanded ? '.pi-image' : '.pi-images'
|
||||
return this.root.locator(`.toggle-expanded-button ${iconSelector}`)
|
||||
}
|
||||
|
||||
async open() {
|
||||
await super.open()
|
||||
return this.root.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
async close() {
|
||||
await super.close()
|
||||
await this.root.waitFor({ state: 'hidden' })
|
||||
}
|
||||
|
||||
async expandTasks() {
|
||||
await this.expandTasksButton.click()
|
||||
await this.collapseTasksButton.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
async collapseTasks() {
|
||||
await this.collapseTasksButton.click()
|
||||
await this.expandTasksButton.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
async waitForTasks() {
|
||||
return Promise.all([
|
||||
this.tasks.first().waitFor({ state: 'visible' }),
|
||||
this.tasks.last().waitFor({ state: 'visible' })
|
||||
])
|
||||
}
|
||||
|
||||
async scrollTasks(direction: 'up' | 'down') {
|
||||
const scrollToEl =
|
||||
direction === 'up' ? this.tasks.last() : this.tasks.first()
|
||||
await scrollToEl.scrollIntoViewIfNeeded()
|
||||
await this.waitForTasks()
|
||||
}
|
||||
|
||||
async clearTasks() {
|
||||
await this.clearButton.click()
|
||||
const confirmButton = this.page.getByLabel('Delete')
|
||||
await confirmButton.click()
|
||||
await this.noResultsPlaceholder.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
/** Set the width of the tab (out of 100). Must call before opening the tab */
|
||||
async setTabWidth(width: number) {
|
||||
if (width < 0 || width > 100) {
|
||||
throw new Error('Width must be between 0 and 100')
|
||||
}
|
||||
return this.page.evaluate((width) => {
|
||||
localStorage.setItem('queue', JSON.stringify([width, 100 - width]))
|
||||
}, width)
|
||||
}
|
||||
|
||||
getTaskPreviewButton(taskIndex: number) {
|
||||
return this.tasks.nth(taskIndex).getByRole('button')
|
||||
}
|
||||
|
||||
async openTaskPreview(taskIndex: number) {
|
||||
const previewButton = this.getTaskPreviewButton(taskIndex)
|
||||
await previewButton.click()
|
||||
return this.galleryImage.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
getGalleryImage(imageFilename: string) {
|
||||
return this.galleryImage.and(this.page.getByAltText(imageFilename))
|
||||
}
|
||||
|
||||
getTaskImage(imageFilename: string) {
|
||||
return this.tasks.getByAltText(imageFilename)
|
||||
}
|
||||
|
||||
/** Trigger the queue store and tasks to update */
|
||||
async triggerTasksUpdate() {
|
||||
await this.page.evaluate(() => {
|
||||
window['app']['api'].dispatchCustomEvent('status', {
|
||||
exec_info: { queue_remaining: 0 }
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
interface ChatHistoryEntry {
|
||||
prompt: string
|
||||
response: string
|
||||
response_id: string
|
||||
}
|
||||
|
||||
async function renderChatHistory(page: Page, history: ChatHistoryEntry[]) {
|
||||
const nodeId = await page.evaluate(() => window['app'].graph.nodes[0]?.id)
|
||||
// Simulate API sending display_component message
|
||||
await page.evaluate(
|
||||
({ nodeId, history }) => {
|
||||
const event = new CustomEvent('display_component', {
|
||||
detail: {
|
||||
node_id: nodeId,
|
||||
component: 'ChatHistoryWidget',
|
||||
props: {
|
||||
history: JSON.stringify(history)
|
||||
}
|
||||
}
|
||||
})
|
||||
window['app'].api.dispatchEvent(event)
|
||||
return true
|
||||
},
|
||||
{ nodeId, history }
|
||||
)
|
||||
|
||||
return nodeId
|
||||
}
|
||||
|
||||
test.describe('Chat History Widget', () => {
|
||||
let nodeId: string
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
nodeId = await renderChatHistory(comfyPage.page, [
|
||||
{ prompt: 'Hello', response: 'World', response_id: '123' }
|
||||
])
|
||||
// Wait for chat history to be rendered
|
||||
await comfyPage.page.waitForSelector('.pi-pencil')
|
||||
})
|
||||
|
||||
test('displays chat history when receiving display_component message', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Verify the chat history is displayed correctly
|
||||
await expect(comfyPage.page.getByText('Hello')).toBeVisible()
|
||||
await expect(comfyPage.page.getByText('World')).toBeVisible()
|
||||
})
|
||||
|
||||
test('handles message editing interaction', async ({ comfyPage }) => {
|
||||
// Get first node's ID
|
||||
nodeId = await comfyPage.page.evaluate(() => {
|
||||
const node = window['app'].graph.nodes[0]
|
||||
|
||||
// Make sure the node has a prompt widget (for editing functionality)
|
||||
if (!node.widgets) {
|
||||
node.widgets = []
|
||||
}
|
||||
|
||||
// Add a prompt widget if it doesn't exist
|
||||
if (!node.widgets.find((w) => w.name === 'prompt')) {
|
||||
node.widgets.push({
|
||||
name: 'prompt',
|
||||
type: 'text',
|
||||
value: 'Original prompt'
|
||||
})
|
||||
}
|
||||
|
||||
return node.id
|
||||
})
|
||||
|
||||
await renderChatHistory(comfyPage.page, [
|
||||
{
|
||||
prompt: 'Message 1',
|
||||
response: 'Response 1',
|
||||
response_id: '123'
|
||||
},
|
||||
{
|
||||
prompt: 'Message 2',
|
||||
response: 'Response 2',
|
||||
response_id: '456'
|
||||
}
|
||||
])
|
||||
await comfyPage.page.waitForSelector('.pi-pencil')
|
||||
|
||||
const originalTextAreaInput = await comfyPage.page
|
||||
.getByPlaceholder('text')
|
||||
.nth(1)
|
||||
.inputValue()
|
||||
|
||||
// Click edit button on first message
|
||||
await comfyPage.page.getByLabel('Edit').first().click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Verify cancel button appears
|
||||
await expect(comfyPage.page.getByLabel('Cancel')).toBeVisible()
|
||||
|
||||
// Click cancel edit
|
||||
await comfyPage.page.getByLabel('Cancel').click()
|
||||
|
||||
// Verify prompt input is restored
|
||||
await expect(comfyPage.page.getByPlaceholder('text').nth(1)).toHaveValue(
|
||||
originalTextAreaInput
|
||||
)
|
||||
})
|
||||
|
||||
test('handles real-time updates to chat history', async ({ comfyPage }) => {
|
||||
// Send initial history
|
||||
await renderChatHistory(comfyPage.page, [
|
||||
{
|
||||
prompt: 'Initial message',
|
||||
response: 'Initial response',
|
||||
response_id: '123'
|
||||
}
|
||||
])
|
||||
await comfyPage.page.waitForSelector('.pi-pencil')
|
||||
|
||||
// Update history with additional messages
|
||||
await renderChatHistory(comfyPage.page, [
|
||||
{
|
||||
prompt: 'Follow-up',
|
||||
response: 'New response',
|
||||
response_id: '456'
|
||||
}
|
||||
])
|
||||
await comfyPage.page.waitForSelector('.pi-pencil')
|
||||
|
||||
// Move mouse over the canvas to force update
|
||||
await comfyPage.page.mouse.move(100, 100)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Verify new messages appear
|
||||
await expect(comfyPage.page.getByText('Follow-up')).toBeVisible()
|
||||
await expect(comfyPage.page.getByText('New response')).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -88,6 +88,10 @@ test.describe('Missing models warning', () => {
|
||||
|
||||
const downloadButton = missingModelsWarning.getByLabel('Download')
|
||||
await expect(downloadButton).toBeVisible()
|
||||
|
||||
// Check that the copy URL button is also visible for Desktop environment
|
||||
const copyUrlButton = missingModelsWarning.getByLabel('Copy URL')
|
||||
await expect(copyUrlButton).toBeVisible()
|
||||
})
|
||||
|
||||
test('Should display a warning when missing models are found in node properties', async ({
|
||||
@@ -101,6 +105,10 @@ test.describe('Missing models warning', () => {
|
||||
|
||||
const downloadButton = missingModelsWarning.getByLabel('Download')
|
||||
await expect(downloadButton).toBeVisible()
|
||||
|
||||
// Check that the copy URL button is also visible for Desktop environment
|
||||
const copyUrlButton = missingModelsWarning.getByLabel('Copy URL')
|
||||
await expect(copyUrlButton).toBeVisible()
|
||||
})
|
||||
|
||||
test('Should not display a warning when no missing models are found', async ({
|
||||
|
||||
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
@@ -2,6 +2,7 @@ import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../fixtures/ComfyPage'
|
||||
import { fitToViewInstant } from '../helpers/fitToView'
|
||||
|
||||
// TODO: there might be a better solution for this
|
||||
// Helper function to pan canvas and select node
|
||||
@@ -516,6 +517,7 @@ This is English documentation.
|
||||
)
|
||||
|
||||
await comfyPage.loadWorkflow('default')
|
||||
await fitToViewInstant(comfyPage)
|
||||
|
||||
// Select KSampler first
|
||||
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
|
||||
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 106 KiB |
@@ -1,210 +0,0 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
|
||||
|
||||
test.describe.skip('Queue sidebar', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
|
||||
test('can display tasks', async ({ comfyPage }) => {
|
||||
await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(1)
|
||||
})
|
||||
|
||||
test('can display tasks after closing then opening', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.close()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(1)
|
||||
})
|
||||
|
||||
test.describe('Virtual scroll', () => {
|
||||
const layouts = [
|
||||
{ description: 'Five columns layout', width: 95, rows: 3, cols: 5 },
|
||||
{ description: 'Three columns layout', width: 55, rows: 3, cols: 3 },
|
||||
{ description: 'Two columns layout', width: 40, rows: 3, cols: 2 }
|
||||
]
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage
|
||||
.setupHistory()
|
||||
.withTask(['example.webp'])
|
||||
.repeat(50)
|
||||
.setupRoutes()
|
||||
})
|
||||
|
||||
layouts.forEach(({ description, width, rows, cols }) => {
|
||||
const preRenderedRows = 1
|
||||
const preRenderedTasks = preRenderedRows * cols * 2
|
||||
const visibleTasks = rows * cols
|
||||
const expectRenderLimit = visibleTasks + preRenderedTasks
|
||||
|
||||
test.describe(description, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.menu.queueTab.setTabWidth(width)
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
})
|
||||
|
||||
test('should not render items outside of view', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const renderedCount =
|
||||
await comfyPage.menu.queueTab.visibleTasks.count()
|
||||
expect(renderedCount).toBeLessThanOrEqual(expectRenderLimit)
|
||||
})
|
||||
|
||||
test('should teardown items after scrolling away', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.menu.queueTab.scrollTasks('down')
|
||||
const renderedCount =
|
||||
await comfyPage.menu.queueTab.visibleTasks.count()
|
||||
expect(renderedCount).toBeLessThanOrEqual(expectRenderLimit)
|
||||
})
|
||||
|
||||
test('should re-render items after scrolling away then back', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.menu.queueTab.scrollTasks('down')
|
||||
await comfyPage.menu.queueTab.scrollTasks('up')
|
||||
const renderedCount =
|
||||
await comfyPage.menu.queueTab.visibleTasks.count()
|
||||
expect(renderedCount).toBeLessThanOrEqual(expectRenderLimit)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Expand tasks', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
// 2-item batch and 3-item batch -> 3 additional items when expanded
|
||||
await comfyPage
|
||||
.setupHistory()
|
||||
.withTask(['example.webp', 'example.webp', 'example.webp'])
|
||||
.withTask(['example.webp', 'example.webp'])
|
||||
.setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
})
|
||||
|
||||
test('can expand tasks with multiple outputs', async ({ comfyPage }) => {
|
||||
const initialCount = await comfyPage.menu.queueTab.visibleTasks.count()
|
||||
await comfyPage.menu.queueTab.expandTasks()
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(
|
||||
initialCount + 3
|
||||
)
|
||||
})
|
||||
|
||||
test('can collapse flat tasks', async ({ comfyPage }) => {
|
||||
const initialCount = await comfyPage.menu.queueTab.visibleTasks.count()
|
||||
await comfyPage.menu.queueTab.expandTasks()
|
||||
await comfyPage.menu.queueTab.collapseTasks()
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(
|
||||
initialCount
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Clear tasks', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage
|
||||
.setupHistory()
|
||||
.withTask(['example.webp'])
|
||||
.repeat(6)
|
||||
.setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
})
|
||||
|
||||
test('can clear all tasks', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.queueTab.clearTasks()
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(0)
|
||||
expect(
|
||||
await comfyPage.menu.queueTab.noResultsPlaceholder.isVisible()
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('can load new tasks after clearing all', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.queueTab.clearTasks()
|
||||
await comfyPage.menu.queueTab.close()
|
||||
await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Gallery', () => {
|
||||
const firstImage = 'example.webp'
|
||||
const secondImage = 'image32x32.webp'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage
|
||||
.setupHistory()
|
||||
.withTask([secondImage])
|
||||
.withTask([firstImage])
|
||||
.setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
await comfyPage.menu.queueTab.openTaskPreview(0)
|
||||
})
|
||||
|
||||
test('displays gallery image after opening task preview', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.nextFrame()
|
||||
await expect(
|
||||
comfyPage.menu.queueTab.getGalleryImage(firstImage)
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('maintains active gallery item when new tasks are added', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Add a new task while the gallery is still open
|
||||
const newImage = 'image64x64.webp'
|
||||
comfyPage.setupHistory().withTask([newImage])
|
||||
await comfyPage.menu.queueTab.triggerTasksUpdate()
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
const newTask = comfyPage.menu.queueTab.tasks.getByAltText(newImage)
|
||||
await newTask.waitFor({ state: 'visible' })
|
||||
// The active gallery item should still be the initial image
|
||||
await expect(
|
||||
comfyPage.menu.queueTab.getGalleryImage(firstImage)
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test.describe('Gallery navigation', () => {
|
||||
const paths: {
|
||||
description: string
|
||||
path: ('Right' | 'Left')[]
|
||||
end: string
|
||||
}[] = [
|
||||
{ description: 'Right', path: ['Right'], end: secondImage },
|
||||
{ description: 'Left', path: ['Right', 'Left'], end: firstImage },
|
||||
{ description: 'Left wrap', path: ['Left'], end: secondImage },
|
||||
{ description: 'Right wrap', path: ['Right', 'Right'], end: firstImage }
|
||||
]
|
||||
|
||||
paths.forEach(({ description, path, end }) => {
|
||||
test(`can navigate gallery ${description}`, async ({ comfyPage }) => {
|
||||
for (const direction of path)
|
||||
await comfyPage.page.keyboard.press(`Arrow${direction}`, {
|
||||
delay: 256
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
await expect(
|
||||
comfyPage.menu.queueTab.getGalleryImage(end)
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 35 KiB |
@@ -6,6 +6,7 @@ import {
|
||||
test.describe('Vue Nodes Zoom', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 8)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 142 KiB |
@@ -9,6 +9,7 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
test.describe('Vue Nodes - LOD', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 8)
|
||||
await comfyPage.setup()
|
||||
await comfyPage.loadWorkflow('default')
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 82 KiB |
@@ -3,6 +3,7 @@ import pluginJs from '@eslint/js'
|
||||
import pluginI18n from '@intlify/eslint-plugin-vue-i18n'
|
||||
import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'
|
||||
import { importX } from 'eslint-plugin-import-x'
|
||||
import oxlint from 'eslint-plugin-oxlint'
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
|
||||
import storybook from 'eslint-plugin-storybook'
|
||||
import unusedImports from 'eslint-plugin-unused-imports'
|
||||
@@ -33,7 +34,18 @@ const settings = {
|
||||
],
|
||||
noWarnOnMultipleProjects: true
|
||||
})
|
||||
]
|
||||
],
|
||||
'vue-i18n': {
|
||||
localeDir: [
|
||||
{
|
||||
pattern: './src/locales/**/*.json',
|
||||
localeKey: 'path',
|
||||
localePattern:
|
||||
/^\.?\/?src\/locales\/(?<locale>[A-Za-z0-9-]+)\/.+\.json$/
|
||||
}
|
||||
],
|
||||
messageSyntaxVersion: '^9.0.0'
|
||||
}
|
||||
} as const
|
||||
|
||||
const commonParserOptions = {
|
||||
@@ -94,19 +106,23 @@ export default defineConfig([
|
||||
// @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'],
|
||||
// @ts-expect-error Bad types in the plugin
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Type incompatibility between import-x plugin and ESLint config types
|
||||
importX.flatConfigs.recommended,
|
||||
// @ts-expect-error Bad types in the plugin
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Type incompatibility between import-x plugin and ESLint config types
|
||||
importX.flatConfigs.typescript,
|
||||
{
|
||||
plugins: {
|
||||
'unused-imports': unusedImports,
|
||||
// @ts-expect-error Bad types in the plugin
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Type incompatibility in i18n plugin
|
||||
'@intlify/vue-i18n': pluginI18n
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/prefer-as-const': 'off',
|
||||
@@ -124,8 +140,8 @@ export default defineConfig([
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||
'vue/no-v-html': 'off',
|
||||
// Enforce dark-theme: instead of dark: prefix
|
||||
'vue/no-restricted-class': ['error', '/^dark:/'],
|
||||
// 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',
|
||||
@@ -259,5 +275,7 @@ export default defineConfig([
|
||||
'@typescript-eslint/no-floating-promises': 'off',
|
||||
'no-console': 'off'
|
||||
}
|
||||
}
|
||||
},
|
||||
// Turn off ESLint rules that are already handled by oxlint
|
||||
...oxlint.buildFromOxlintConfigFile('./.oxlintrc.json')
|
||||
])
|
||||
|
||||
18
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.32.4",
|
||||
"version": "1.33.4",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -28,13 +28,14 @@
|
||||
"json-schema": "tsx scripts/generate-json-schema.ts",
|
||||
"knip:no-cache": "knip",
|
||||
"knip": "knip --cache",
|
||||
"lint:fix:no-cache": "eslint src --fix",
|
||||
"lint:fix": "eslint src --cache --fix",
|
||||
"lint:no-cache": "eslint src",
|
||||
"lint:fix:no-cache": "oxlint src --type-aware --fix && eslint src --fix",
|
||||
"lint:fix": "oxlint src --type-aware --fix && eslint src --cache --fix",
|
||||
"lint:no-cache": "oxlint src --type-aware && eslint src",
|
||||
"lint:unstaged:fix": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache --fix",
|
||||
"lint:unstaged": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache",
|
||||
"lint": "eslint src --cache",
|
||||
"lint": "oxlint src --type-aware && eslint src --cache",
|
||||
"locale": "lobe-i18n locale",
|
||||
"oxlint": "oxlint src --type-aware",
|
||||
"preinstall": "pnpm dlx only-allow pnpm",
|
||||
"prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true",
|
||||
"preview": "nx preview",
|
||||
@@ -42,6 +43,7 @@
|
||||
"stylelint:fix": "stylelint --cache --fix '{apps,packages,src}/**/*.{css,vue}'",
|
||||
"stylelint": "stylelint --cache '{apps,packages,src}/**/*.{css,vue}'",
|
||||
"test:browser": "pnpm exec nx e2e",
|
||||
"test:browser:local": "cross-env PLAYWRIGHT_LOCAL=1 pnpm test:browser",
|
||||
"test:unit": "nx run test",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"zipdist": "node scripts/zipdist.js",
|
||||
@@ -78,6 +80,7 @@
|
||||
"eslint-config-prettier": "catalog:",
|
||||
"eslint-import-resolver-typescript": "catalog:",
|
||||
"eslint-plugin-import-x": "catalog:",
|
||||
"eslint-plugin-oxlint": "catalog:",
|
||||
"eslint-plugin-prettier": "catalog:",
|
||||
"eslint-plugin-storybook": "catalog:",
|
||||
"eslint-plugin-unused-imports": "catalog:",
|
||||
@@ -93,6 +96,8 @@
|
||||
"markdown-table": "catalog:",
|
||||
"mixpanel-browser": "catalog:",
|
||||
"nx": "catalog:",
|
||||
"oxlint": "catalog:",
|
||||
"oxlint-tsgolint": "catalog:",
|
||||
"picocolors": "catalog:",
|
||||
"postcss-html": "catalog:",
|
||||
"prettier": "catalog:",
|
||||
@@ -123,9 +128,10 @@
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "catalog:",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "0.4.73-0",
|
||||
"@comfyorg/comfyui-electron-types": "catalog:",
|
||||
"@comfyorg/design-system": "workspace:*",
|
||||
"@comfyorg/registry-types": "workspace:*",
|
||||
"@comfyorg/shared-frontend-utils": "workspace:*",
|
||||
"@comfyorg/tailwind-utils": "workspace:*",
|
||||
"@iconify/json": "catalog:",
|
||||
"@primeuix/forms": "catalog:",
|
||||
|
||||
@@ -98,6 +98,9 @@
|
||||
|
||||
--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);
|
||||
|
||||
--color-blue-selection: rgb(from var(--color-azure-600) r g b / 0.3);
|
||||
--color-node-hover-100: rgb(from var(--color-charcoal-800) r g b/ 0.15);
|
||||
--color-node-hover-200: rgb(from var(--color-charcoal-800) r g b/ 0.1);
|
||||
@@ -160,7 +163,6 @@
|
||||
|
||||
--subscription-button-gradient: linear-gradient(315deg, rgb(105 230 255 / 0.15) 0%, rgb(99 73 233 / 0.50) 100%), radial-gradient(70.71% 70.71% at 50% 50%, rgb(62 99 222 / 0.15) 0.01%, rgb(66 0 123 / 0.50) 100%), linear-gradient(92deg, #D000FF 0.38%, #B009FE 37.07%, #3E1FFC 65.17%, #009DFF 103.86%), var(--color-button-surface, #2D2E32);
|
||||
|
||||
--modal-card-button-surface: var(--color-smoke-300);
|
||||
|
||||
/* Code styling colors for help menu*/
|
||||
--code-text-color: rgb(0 122 255 / 1);
|
||||
@@ -265,6 +267,14 @@
|
||||
--palette-interface-panel-selected-surface: color-mix(in srgb, var(--interface-panel-surface) 87.5%, var(--contrast-mix-color));
|
||||
--palette-interface-button-hover-surface: color-mix(in srgb, var(--interface-panel-surface) 82%, var(--contrast-mix-color));
|
||||
|
||||
--modal-card-background: var(--secondary-background);
|
||||
--modal-card-background-hovered: var(--secondary-background-hover);
|
||||
--modal-card-border-highlighted: var(--secondary-background-selected);
|
||||
--modal-card-button-surface: var(--color-smoke-300);
|
||||
--modal-card-placeholder-background: var(--color-smoke-600);
|
||||
--modal-card-tag-background: var(--color-smoke-400);
|
||||
--modal-card-tag-foreground: var(--base-foreground);
|
||||
--modal-panel-background: var(--color-white);
|
||||
}
|
||||
|
||||
.dark-theme {
|
||||
@@ -286,8 +296,6 @@
|
||||
|
||||
--subscription-button-gradient: linear-gradient(315deg, rgb(105 230 255 / 0.15) 0%, rgb(99 73 233 / 0.50) 100%), radial-gradient(70.71% 70.71% at 50% 50%, rgb(62 99 222 / 0.15) 0.01%, rgb(66 0 123 / 0.50) 100%), linear-gradient(92deg, #D000FF 0.38%, #B009FE 37.07%, #3E1FFC 65.17%, #009DFF 103.86%), var(--color-button-surface, #2D2E32);
|
||||
|
||||
--modal-card-button-surface: var(--color-charcoal-300);
|
||||
|
||||
--dialog-surface: var(--color-neutral-700);
|
||||
|
||||
--interface-menu-component-surface-hovered: var(--color-charcoal-400);
|
||||
@@ -326,6 +334,12 @@
|
||||
--node-stroke-error: var(--color-error);
|
||||
--node-stroke-executing: var(--color-azure-600);
|
||||
|
||||
/* Queue progress (dark theme) */
|
||||
--color-interface-panel-job-progress-primary: var(--color-cobalt-800);
|
||||
--color-interface-panel-job-progress-secondary: var(
|
||||
--color-alpha-azure-600-30
|
||||
);
|
||||
|
||||
--text-secondary: var(--color-slate-100);
|
||||
--text-primary: var(--color-white);
|
||||
|
||||
@@ -362,6 +376,16 @@
|
||||
--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);
|
||||
|
||||
--modal-card-background: var(--secondary-background);
|
||||
--modal-card-background-hovered: var(--secondary-background-hover);
|
||||
--modal-card-border-highlighted: var(--color-ash-400);
|
||||
--modal-card-button-surface: var(--color-charcoal-300);
|
||||
--modal-card-placeholder-background: var(--secondary-background);
|
||||
--modal-card-tag-background: var(--color-ash-800);
|
||||
--modal-card-tag-foreground: var(--base-foreground);
|
||||
--modal-panel-background: var(--color-charcoal-600);
|
||||
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@@ -372,7 +396,16 @@
|
||||
--color-button-surface: var(--button-surface);
|
||||
--color-button-surface-contrast: var(--button-surface-contrast);
|
||||
--color-subscription-button-gradient: var(--subscription-button-gradient);
|
||||
|
||||
--color-modal-card-background: var(--modal-card-background);
|
||||
--color-modal-card-background-hovered: var(--modal-card-background-hovered);
|
||||
--color-modal-card-border-highlighted: var(--modal-card-border-highlighted);
|
||||
--color-modal-card-button-surface: var(--modal-card-button-surface);
|
||||
--color-modal-card-placeholder-background: var(--modal-card-placeholder-background);
|
||||
--color-modal-card-tag-background: var(--modal-card-tag-background);
|
||||
--color-modal-card-tag-foreground: var(--modal-card-tag-foreground);
|
||||
--color-modal-panel-background: var(--modal-panel-background);
|
||||
|
||||
--color-dialog-surface: var(--dialog-surface);
|
||||
--color-interface-menu-component-surface-hovered: var(
|
||||
--interface-menu-component-surface-hovered
|
||||
@@ -1346,3 +1379,466 @@ audio.comfy-audio.empty-audio-widget {
|
||||
border-radius: 0;
|
||||
}
|
||||
/* END LOD specific styles */
|
||||
|
||||
/* ===================== Mask Editor Styles ===================== */
|
||||
/* To be migrated to Tailwind later */
|
||||
#maskEditor_brush {
|
||||
position: absolute;
|
||||
backgroundColor: transparent;
|
||||
z-index: 8889;
|
||||
pointer-events: none;
|
||||
border-radius: 50%;
|
||||
overflow: visible;
|
||||
outline: 1px dashed black;
|
||||
box-shadow: 0 0 0 1px white;
|
||||
}
|
||||
#maskEditor_brushPreviewGradient {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
display: none;
|
||||
}
|
||||
.maskEditor_sidePanelTitle {
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
font-family: sans-serif;
|
||||
color: var(--descrip-text);
|
||||
margin-top: 10px;
|
||||
}
|
||||
.maskEditor_sidePanelBrushShapeCircle {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border-color);
|
||||
pointer-events: auto;
|
||||
transition: background 0.1s;
|
||||
margin-left: 7.5px;
|
||||
}
|
||||
.maskEditor_sidePanelBrushRange {
|
||||
width: 180px;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
.maskEditor_sidePanelBrushRange::-webkit-slider-thumb {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 50%;
|
||||
cursor: grab;
|
||||
margin-top: -8px;
|
||||
background: var(--p-surface-700);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
.maskEditor_sidePanelBrushRange::-moz-range-thumb {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 50%;
|
||||
cursor: grab;
|
||||
background: var(--p-surface-800);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
.maskEditor_sidePanelBrushRange::-webkit-slider-runnable-track {
|
||||
background: var(--p-surface-700);
|
||||
height: 3px;
|
||||
}
|
||||
.maskEditor_sidePanelBrushRange::-moz-range-track {
|
||||
background: var(--p-surface-700);
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelBrushShapeSquare {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
margin: 5px;
|
||||
border: 1px solid var(--border-color);
|
||||
pointer-events: auto;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.maskEditor_brushShape_dark {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.maskEditor_brushShape_dark:hover {
|
||||
background: var(--p-surface-900);
|
||||
}
|
||||
|
||||
.maskEditor_brushShape_light {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.maskEditor_brushShape_light:hover {
|
||||
background: var(--comfy-menu-bg);
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelLayer {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
}
|
||||
.maskEditor_sidePanelLayerVisibilityContainer {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.maskEditor_sidePanelVisibilityToggle {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.maskEditor_sidePanelLayerIconContainer {
|
||||
width: 60px;
|
||||
height: 50px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
fill: var(--input-text);
|
||||
}
|
||||
.maskEditor_sidePanelLayerIconContainer svg {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
.maskEditor_sidePanelBigButton {
|
||||
width: 85px;
|
||||
height: 30px;
|
||||
background: rgb(0 0 0 / 0.2);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--input-text);
|
||||
font-family: sans-serif;
|
||||
font-size: 15px;
|
||||
pointer-events: auto;
|
||||
transition: background-color 0.1s;
|
||||
}
|
||||
.maskEditor_sidePanelBigButton:hover {
|
||||
background-color: var(--p-overlaybadge-outline-color);
|
||||
border: none;
|
||||
}
|
||||
.maskEditor_toolPanelContainer {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.maskEditor_toolPanelContainerSelected svg {
|
||||
fill: var(--p-button-text-primary-color) !important;
|
||||
}
|
||||
.maskEditor_toolPanelContainerSelected .maskEditor_toolPanelIndicator {
|
||||
display: block;
|
||||
}
|
||||
.maskEditor_toolPanelContainer svg {
|
||||
width: 75%;
|
||||
aspect-ratio: 1/1;
|
||||
fill: var(--p-button-text-secondary-color);
|
||||
}
|
||||
|
||||
.maskEditor_toolPanelContainerDark:hover {
|
||||
background-color: var(--p-surface-800);
|
||||
}
|
||||
|
||||
.maskEditor_toolPanelContainerLight:hover {
|
||||
background-color: var(--p-surface-300);
|
||||
}
|
||||
|
||||
.maskEditor_toolPanelIndicator {
|
||||
display: none;
|
||||
height: 100%;
|
||||
width: 4px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
background: var(--p-button-text-primary-color);
|
||||
}
|
||||
.maskEditor_sidePanelSeparator {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--border-color);
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
#maskEditorCanvasContainer {
|
||||
position: absolute;
|
||||
width: 1000px;
|
||||
height: 667px;
|
||||
left: 359px;
|
||||
top: 280px;
|
||||
}
|
||||
|
||||
.maskEditor_topPanelIconButton_dark {
|
||||
width: 50px;
|
||||
height: 30px;
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: background-color 0.1s;
|
||||
background: var(--p-surface-800);
|
||||
border: 1px solid var(--p-form-field-border-color);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.maskEditor_topPanelIconButton_dark:hover {
|
||||
background-color: var(--p-surface-900);
|
||||
}
|
||||
|
||||
.maskEditor_topPanelIconButton_dark svg {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
pointer-events: none;
|
||||
fill: var(--input-text);
|
||||
}
|
||||
|
||||
.maskEditor_topPanelIconButton_light {
|
||||
width: 50px;
|
||||
height: 30px;
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: background-color 0.1s;
|
||||
background: var(--comfy-menu-bg);
|
||||
border: 1px solid var(--p-form-field-border-color);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.maskEditor_topPanelIconButton_light:hover {
|
||||
background-color: var(--p-surface-300);
|
||||
}
|
||||
|
||||
.maskEditor_topPanelIconButton_light svg {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
pointer-events: none;
|
||||
fill: var(--input-text);
|
||||
}
|
||||
|
||||
.maskEditor_topPanelButton_dark {
|
||||
height: 30px;
|
||||
background: var(--p-surface-800);
|
||||
border: 1px solid var(--p-form-field-border-color);
|
||||
border-radius: 10px;
|
||||
color: var(--input-text);
|
||||
font-family: sans-serif;
|
||||
pointer-events: auto;
|
||||
transition: 0.1s;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.maskEditor_topPanelButton_dark:hover {
|
||||
background-color: var(--p-surface-900);
|
||||
}
|
||||
|
||||
.maskEditor_topPanelButton_light {
|
||||
height: 30px;
|
||||
background: var(--comfy-menu-bg);
|
||||
border: 1px solid var(--p-form-field-border-color);
|
||||
border-radius: 10px;
|
||||
color: var(--input-text);
|
||||
font-family: sans-serif;
|
||||
pointer-events: auto;
|
||||
transition: 0.1s;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.maskEditor_topPanelButton_light:hover {
|
||||
background-color: var(--p-surface-300);
|
||||
}
|
||||
|
||||
.maskEditor_sidePanel_paintBucket_Container {
|
||||
width: 180px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.maskEditor_sidePanel_colorSelect_Container {
|
||||
display: flex;
|
||||
width: 180px;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.maskEditor_sidePanel_colorSelect_tolerance_container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelContainerColumn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelContainerRow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.maskEditor_accent_bg_dark {
|
||||
background: var(--p-surface-800);
|
||||
}
|
||||
|
||||
.maskEditor_accent_bg_very_dark {
|
||||
background: var(--p-surface-900);
|
||||
}
|
||||
|
||||
.maskEditor_accent_bg_light {
|
||||
background: var(--p-surface-300);
|
||||
}
|
||||
|
||||
.maskEditor_accent_bg_very_light {
|
||||
background: var(--comfy-menu-bg);
|
||||
}
|
||||
|
||||
|
||||
.maskEditor_sidePanelToggleContainer {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelToggleSwitch {
|
||||
display: inline-block;
|
||||
border-radius: 16px;
|
||||
width: 40px;
|
||||
height: 24px;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
transition: background 0.25s;
|
||||
background: var(--p-surface-300);
|
||||
}
|
||||
|
||||
.dark-theme .maskEditor_sidePanelToggleSwitch {
|
||||
background: var(--p-surface-700);
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelToggleSwitch::before, .maskEditor_sidePanelToggleSwitch::after {
|
||||
content: "";
|
||||
}
|
||||
.maskEditor_sidePanelToggleSwitch::before {
|
||||
display: block;
|
||||
background: linear-gradient(to bottom, #fff 0%, #eee 100%);
|
||||
border-radius: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
transition: ease 0.2s;
|
||||
}
|
||||
.maskEditor_sidePanelToggleContainer:hover .maskEditor_sidePanelToggleSwitch::before {
|
||||
background: linear-gradient(to bottom, #fff 0%, #fff 100%);
|
||||
}
|
||||
.maskEditor_sidePanelToggleCheckbox:checked + .maskEditor_sidePanelToggleSwitch {
|
||||
background: var(--p-button-text-primary-color);
|
||||
}
|
||||
.maskEditor_sidePanelToggleCheckbox:checked + .maskEditor_sidePanelToggleSwitch::before {
|
||||
background: var(--comfy-menu-bg);
|
||||
left: 20px;
|
||||
}
|
||||
.dark-theme .maskEditor_sidePanelToggleCheckbox:checked + .maskEditor_sidePanelToggleSwitch::before {
|
||||
background: var(--p-surface-900);
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelToggleCheckbox {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelDropdown {
|
||||
border: 1px solid var(--p-form-field-border-color);
|
||||
background: var(--comfy-menu-bg);
|
||||
height: 24px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelDropdown option {
|
||||
background: var(--comfy-menu-bg);
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelDropdown:focus {
|
||||
outline: 1px solid var(--p-surface-300);
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelDropdown option:hover {
|
||||
background: white;
|
||||
}
|
||||
.maskEditor_sidePanelDropdown option:active {
|
||||
background: var(--p-surface-300);
|
||||
}
|
||||
|
||||
.dark-theme .maskEditor_sidePanelDropdown {
|
||||
background: var(--p-surface-900);
|
||||
}
|
||||
|
||||
.dark-theme .maskEditor_sidePanelDropdown option {
|
||||
background: var(--p-surface-900);
|
||||
}
|
||||
|
||||
.dark-theme .maskEditor_sidePanelDropdown:focus {
|
||||
outline: 1px solid var(--p-button-text-primary-color);
|
||||
}
|
||||
|
||||
.dark-theme .maskEditor_sidePanelDropdown option:active {
|
||||
background: var(--p-highlight-background);
|
||||
}
|
||||
|
||||
.maskEditor_layerRow {
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelLayerPreviewContainer {
|
||||
width: 40px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelLayerPreviewContainer > svg{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
fill: var(--p-surface-100);
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelImageLayerImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelSubTitle {
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
font-family: sans-serif;
|
||||
color: var(--descrip-text);
|
||||
}
|
||||
|
||||
.maskEditor_containerDropdown {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelLayerCheckbox {
|
||||
margin-left: 15px;
|
||||
}
|
||||
/* ===================== End of Mask Editor Styles ===================== */
|
||||
|
||||
3
packages/design-system/src/icons/file-output.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.33398 1.33337V4.00004C9.33398 4.35366 9.47446 4.6928 9.72451 4.94285C9.97456 5.1929 10.3137 5.33337 10.6673 5.33337H13.334M2.66732 4.66671V2.66671C2.66732 2.31309 2.80779 1.97395 3.05784 1.7239C3.30789 1.47385 3.64703 1.33337 4.00065 1.33337H10.0006L13.334 4.66671V13.3334C13.334 13.687 13.1935 14.0261 12.9435 14.2762C12.6934 14.5262 12.3543 14.6667 12.0006 14.6667L4.04264 14.666C3.77927 14.7004 3.51166 14.6552 3.2741 14.5365C3.03655 14.4177 2.83988 14.2307 2.70931 13.9994M3.33398 7.33337L1.33398 9.33337M1.33398 9.33337L3.33398 11.3334M1.33398 9.33337H8.00065" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 771 B |
@@ -1,27 +1,32 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
import type { PlaywrightTestConfig } from '@playwright/test'
|
||||
|
||||
const maybeLocalOptions: PlaywrightTestConfig = process.env.PLAYWRIGHT_LOCAL
|
||||
? {
|
||||
// VERY HELPFUL: Skip screenshot tests locally
|
||||
// grep: process.env.CI ? undefined : /^(?!.*screenshot).*$/,
|
||||
timeout: 30_000, // Longer timeout for breakpoints
|
||||
retries: 0, // No retries while debugging. Increase if writing new tests. that may be flaky.
|
||||
workers: 1, // Single worker for easier debugging. Increase to match CPU cores if you want to run a lot of tests in parallel.
|
||||
|
||||
use: {
|
||||
trace: 'on', // Always capture traces (CI uses 'on-first-retry')
|
||||
video: 'on' // Always record video (CI uses 'retain-on-failure')
|
||||
}
|
||||
}
|
||||
: {
|
||||
retries: process.env.CI ? 3 : 0,
|
||||
use: {
|
||||
trace: 'on-first-retry'
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './browser_tests',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
reporter: 'html',
|
||||
// /* // Toggle for [LOCAL] testing.
|
||||
retries: process.env.CI ? 3 : 0,
|
||||
use: {
|
||||
trace: 'on-first-retry'
|
||||
},
|
||||
/*/ // [LOCAL]
|
||||
// VERY HELPFUL: Skip screenshot tests locally
|
||||
// grep: process.env.CI ? undefined : /^(?!.*screenshot).*$/,
|
||||
timeout: 30_000, // Longer timeout for breakpoints
|
||||
retries: 0, // No retries while debugging. Increase if writing new tests. that may be flaky.
|
||||
workers: 4, // Single worker for easier debugging. Increase to match CPU cores if you want to run a lot of tests in parallel.
|
||||
|
||||
use: {
|
||||
trace: 'on', // Always capture traces (CI uses 'on-first-retry')
|
||||
video: 'on' // Always record video (CI uses 'retain-on-failure')
|
||||
},
|
||||
//*/
|
||||
...maybeLocalOptions,
|
||||
|
||||
globalSetup: './browser_tests/globalSetup.ts',
|
||||
globalTeardown: './browser_tests/globalTeardown.ts',
|
||||
|
||||
241
pnpm-lock.yaml
generated
@@ -9,12 +9,21 @@ catalogs:
|
||||
'@alloc/quick-lru':
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0
|
||||
'@comfyorg/comfyui-electron-types':
|
||||
specifier: 0.5.5
|
||||
version: 0.5.5
|
||||
'@eslint/js':
|
||||
specifier: ^9.35.0
|
||||
version: 9.35.0
|
||||
'@iconify-json/lucide':
|
||||
specifier: ^1.1.178
|
||||
version: 1.2.66
|
||||
'@iconify/json':
|
||||
specifier: ^2.2.380
|
||||
version: 2.2.380
|
||||
'@iconify/tailwind':
|
||||
specifier: ^1.1.3
|
||||
version: 1.2.0
|
||||
'@intlify/eslint-plugin-vue-i18n':
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
@@ -141,6 +150,9 @@ catalogs:
|
||||
eslint-plugin-import-x:
|
||||
specifier: ^4.16.1
|
||||
version: 4.16.1
|
||||
eslint-plugin-oxlint:
|
||||
specifier: 1.25.0
|
||||
version: 1.25.0
|
||||
eslint-plugin-prettier:
|
||||
specifier: ^5.5.4
|
||||
version: 5.5.4
|
||||
@@ -186,6 +198,12 @@ catalogs:
|
||||
nx:
|
||||
specifier: 21.4.1
|
||||
version: 21.4.1
|
||||
oxlint:
|
||||
specifier: ^1.25.0
|
||||
version: 1.28.0
|
||||
oxlint-tsgolint:
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
picocolors:
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
@@ -303,14 +321,17 @@ importers:
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.1
|
||||
'@comfyorg/comfyui-electron-types':
|
||||
specifier: 0.4.73-0
|
||||
version: 0.4.73-0
|
||||
specifier: 'catalog:'
|
||||
version: 0.5.5
|
||||
'@comfyorg/design-system':
|
||||
specifier: workspace:*
|
||||
version: link:packages/design-system
|
||||
'@comfyorg/registry-types':
|
||||
specifier: workspace:*
|
||||
version: link:packages/registry-types
|
||||
'@comfyorg/shared-frontend-utils':
|
||||
specifier: workspace:*
|
||||
version: link:packages/shared-frontend-utils
|
||||
'@comfyorg/tailwind-utils':
|
||||
specifier: workspace:*
|
||||
version: link:packages/tailwind-utils
|
||||
@@ -555,6 +576,9 @@ importers:
|
||||
eslint-plugin-import-x:
|
||||
specifier: 'catalog:'
|
||||
version: 4.16.1(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.4.2))
|
||||
eslint-plugin-oxlint:
|
||||
specifier: 'catalog:'
|
||||
version: 1.25.0
|
||||
eslint-plugin-prettier:
|
||||
specifier: 'catalog:'
|
||||
version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.6.2)
|
||||
@@ -600,6 +624,12 @@ importers:
|
||||
nx:
|
||||
specifier: 'catalog:'
|
||||
version: 21.4.1
|
||||
oxlint:
|
||||
specifier: 'catalog:'
|
||||
version: 1.28.0(oxlint-tsgolint@0.4.0)
|
||||
oxlint-tsgolint:
|
||||
specifier: 'catalog:'
|
||||
version: 0.4.0
|
||||
picocolors:
|
||||
specifier: 'catalog:'
|
||||
version: 1.1.1
|
||||
@@ -682,8 +712,8 @@ importers:
|
||||
apps/desktop-ui:
|
||||
dependencies:
|
||||
'@comfyorg/comfyui-electron-types':
|
||||
specifier: 0.4.73-0
|
||||
version: 0.4.73-0
|
||||
specifier: 'catalog:'
|
||||
version: 0.5.5
|
||||
'@comfyorg/shared-frontend-utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/shared-frontend-utils
|
||||
@@ -1426,8 +1456,8 @@ packages:
|
||||
'@cacheable/utils@2.0.3':
|
||||
resolution: {integrity: sha512-m7Rce68cMHlAUjvWBy9Ru1Nmw5gU0SjGGtQDdhpe6E0xnbcvrIY0Epy//JU1VYYBUTzrG9jvgmTauULGKzOkWA==}
|
||||
|
||||
'@comfyorg/comfyui-electron-types@0.4.73-0':
|
||||
resolution: {integrity: sha512-WlItGJQx9ZWShNG9wypx3kq+19pSig/U+s5sD2SAeEcMph4u8A/TS+lnRgdKhT58VT1uD7cMcj2SJpfdBPNWvw==}
|
||||
'@comfyorg/comfyui-electron-types@0.5.5':
|
||||
resolution: {integrity: sha512-f3XOXpMsALIwHakz7FekVPm4/Fh2pvJPEi8tRe8jYGBt8edsd4Mkkq31Yjs2Weem3BP7yNwbdNuSiQdP/pxJyg==}
|
||||
|
||||
'@csstools/color-helpers@5.1.0':
|
||||
resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
|
||||
@@ -2529,6 +2559,76 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@oxlint-tsgolint/darwin-arm64@0.4.0':
|
||||
resolution: {integrity: sha512-2jNvhxs6JJy93Z4SQ/VErODBzZtFKxQ+sybcKYcw5/K41tXOiBbJSwBMZ2PPvivCVkVcyOkJvfs5UXWW7o79uw==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxlint-tsgolint/darwin-x64@0.4.0':
|
||||
resolution: {integrity: sha512-6A+YBecdZhk2NJ8Dh3kRkR6htNekDmAopFkdyrtNvsHJs5qNNuwUv5RZlVMYiaQTh/Y/tZ0YWE4+cVdqPIEyxQ==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxlint-tsgolint/linux-arm64@0.4.0':
|
||||
resolution: {integrity: sha512-JaX8JfQnY3UwX7l6BXIjhEaJAVeKVASELLFCdoo5+DOHgPuiiSKcxCVgTl92WPAuS0TYFXOgqOg31WXkvdi8bQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint-tsgolint/linux-x64@0.4.0':
|
||||
resolution: {integrity: sha512-iu106lxV1O64O4vK2eRoIuY2iHuil/hyDNKLRNVaTg1un+yoxN6/C5uxrJix/EJ+1O27P9c+sXmMplcmbXujtg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint-tsgolint/win32-arm64@0.4.0':
|
||||
resolution: {integrity: sha512-KTp9EzkTCGAh4/sL3l5a9otX63TvTs5riBcrcqu0jYS3P762rZSezzMMDc0Ld51x+I37125p9+bue2vmlH/KbQ==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@oxlint-tsgolint/win32-x64@0.4.0':
|
||||
resolution: {integrity: sha512-ioyBLHx0HA+hn5of8mhnA8W8DWQyJEHc7SBvwku0EW9bWt7zvBtWRJfx1YilvM+KVBdLVX731qeofdJT1fbJiQ==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@oxlint/darwin-arm64@1.28.0':
|
||||
resolution: {integrity: sha512-H7J41/iKbgm7tTpdSnA/AtjEAhxyzNzCMKWtKU5wDuP2v39jrc3fasQEJruk6hj1YXPbJY4N+1nK/jE27GMGDQ==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxlint/darwin-x64@1.28.0':
|
||||
resolution: {integrity: sha512-bGsSDEwpyYzNc6FIwhTmbhSK7piREUjMlmWBt7eoR3ract0+RfhZYYG4se1Ngs+4WOFC0B3gbv23fyF+cnbGGQ==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxlint/linux-arm64-gnu@1.28.0':
|
||||
resolution: {integrity: sha512-eNH/evMpV3xAA4jIS8dMLcGkM/LK0WEHM0RO9bxrHPAwfS72jhyPJtd0R7nZhvhG6U1bhn5jhoXbk1dn27XIAQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint/linux-arm64-musl@1.28.0':
|
||||
resolution: {integrity: sha512-ickvpcekNeRLND3llndiZOtJBb6LDZqNnZICIDkovURkOIWPGJGmAxsHUOI6yW6iny9gLmIEIGl/c1b5nFk6Ag==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint/linux-x64-gnu@1.28.0':
|
||||
resolution: {integrity: sha512-DkgAh4LQ8NR3DwTT7/LGMhaMau0RtZkih91Ez5Usk7H7SOxo1GDi84beE7it2Q+22cAzgY4hbw3c6svonQTjxg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint/linux-x64-musl@1.28.0':
|
||||
resolution: {integrity: sha512-VBnMi3AJ2w5p/kgeyrjcGOKNY8RzZWWvlGHjCJwzqPgob4MXu6T+5Yrdi7EVJyIlouL8E3LYPYjmzB9NBi9gZw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint/win32-arm64@1.28.0':
|
||||
resolution: {integrity: sha512-tomhIks+4dKs8axB+s4GXHy+ZWXhUgptf1XnG5cZg8CzRfX4JFX9k8l2fPUgFwytWnyyvZaaXLRPWGzoZ6yoHQ==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@oxlint/win32-x64@1.28.0':
|
||||
resolution: {integrity: sha512-4+VO5P/UJ2nq9sj6kQToJxFy5cKs7dGIN2DiUSQ7cqyUi7EKYNQKe+98HFcDOjtm33jQOQnc4kw8Igya5KPozg==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@phenomnomnominal/tsquery@5.0.1':
|
||||
resolution: {integrity: sha512-3nVv+e2FQwsW8Aw6qTU6f+1rfcJ3hrcnvH/mu9i8YhxO+9sqbOfpL8m6PbET5+xKOlz/VSbp0RoYWYCtIsnmuA==}
|
||||
peerDependencies:
|
||||
@@ -4316,6 +4416,9 @@ packages:
|
||||
csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
data-urls@5.0.0:
|
||||
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -4732,6 +4835,9 @@ packages:
|
||||
'@typescript-eslint/parser':
|
||||
optional: true
|
||||
|
||||
eslint-plugin-oxlint@1.25.0:
|
||||
resolution: {integrity: sha512-grS4KdR9FAxoQC+wMkepeQHL4osMhoYfUI11Pot6Gitqr4wWi+JZrX0Shr8Bs9fjdWhEjtaZIV6cr4mbfytmyw==}
|
||||
|
||||
eslint-plugin-prettier@5.5.4:
|
||||
resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
@@ -5692,6 +5798,9 @@ packages:
|
||||
jsonc-parser@3.2.0:
|
||||
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
|
||||
|
||||
jsonc-parser@3.3.1:
|
||||
resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
|
||||
|
||||
jsondiffpatch@0.6.0:
|
||||
resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
@@ -6386,6 +6495,20 @@ packages:
|
||||
oxc-resolver@11.6.1:
|
||||
resolution: {integrity: sha512-WQgmxevT4cM5MZ9ioQnEwJiHpPzbvntV5nInGAKo9NQZzegcOonHvcVcnkYqld7bTG35UFHEKeF7VwwsmA3cZg==}
|
||||
|
||||
oxlint-tsgolint@0.4.0:
|
||||
resolution: {integrity: sha512-RpvLxPvSt0Xzr3frTiw5rP6HUW0djZ2uNdzHc8Pv556sbTnFWXmLdK8FRqqy7vVXZTkoVSdY3PsvOsVAqGjc+Q==}
|
||||
hasBin: true
|
||||
|
||||
oxlint@1.28.0:
|
||||
resolution: {integrity: sha512-gE97d0BcIlTTSJrim395B49mIbQ9VO8ZVoHdWai7Svl+lEeUAyCLTN4d7piw1kcB8VfgTp1JFVlAvMPD9GewMA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
oxlint-tsgolint: '>=0.4.0'
|
||||
peerDependenciesMeta:
|
||||
oxlint-tsgolint:
|
||||
optional: true
|
||||
|
||||
p-limit@3.1.0:
|
||||
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -6883,6 +7006,11 @@ packages:
|
||||
engines: {node: '>= 0.4'}
|
||||
hasBin: true
|
||||
|
||||
resolve@1.22.11:
|
||||
resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
hasBin: true
|
||||
|
||||
restore-cursor@3.1.0:
|
||||
resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -6978,6 +7106,11 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
semver@7.7.3:
|
||||
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
set-function-length@1.2.2:
|
||||
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -7698,8 +7831,8 @@ packages:
|
||||
vue-component-type-helpers@3.1.1:
|
||||
resolution: {integrity: sha512-B0kHv7qX6E7+kdc5nsaqjdGZ1KwNKSUQDWGy7XkTYT7wFsOpkEyaJ1Vq79TjwrrtuLRgizrTV7PPuC4rRQo+vw==}
|
||||
|
||||
vue-component-type-helpers@3.1.2:
|
||||
resolution: {integrity: sha512-ch3/SKBtxdZq18vsEntiGCdSszCRNfhX5QaTxjSacCAXLlNQRXfXo+ANjoQEYJMsJOJy1/vHF6Tkc4s85MS+zw==}
|
||||
vue-component-type-helpers@3.1.4:
|
||||
resolution: {integrity: sha512-Uws7Ew1OzTTqHW8ZVl/qLl/HB+jf08M0NdFONbVWAx0N4gMLK8yfZDgeB77hDnBmaigWWEn5qP8T9BG59jIeyQ==}
|
||||
|
||||
vue-demi@0.14.10:
|
||||
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
||||
@@ -8875,7 +9008,7 @@ snapshots:
|
||||
|
||||
'@cacheable/utils@2.0.3': {}
|
||||
|
||||
'@comfyorg/comfyui-electron-types@0.4.73-0': {}
|
||||
'@comfyorg/comfyui-electron-types@0.5.5': {}
|
||||
|
||||
'@csstools/color-helpers@5.1.0': {}
|
||||
|
||||
@@ -10092,6 +10225,48 @@ snapshots:
|
||||
'@oxc-resolver/binding-win32-x64-msvc@11.6.1':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/darwin-arm64@0.4.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/darwin-x64@0.4.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/linux-arm64@0.4.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/linux-x64@0.4.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/win32-arm64@0.4.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/win32-x64@0.4.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/darwin-arm64@1.28.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/darwin-x64@1.28.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/linux-arm64-gnu@1.28.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/linux-arm64-musl@1.28.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/linux-x64-gnu@1.28.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/linux-x64-musl@1.28.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/win32-arm64@1.28.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/win32-x64@1.28.0':
|
||||
optional: true
|
||||
|
||||
'@phenomnomnominal/tsquery@5.0.1(typescript@5.9.2)':
|
||||
dependencies:
|
||||
esquery: 1.6.0
|
||||
@@ -10458,7 +10633,7 @@ snapshots:
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
type-fest: 2.19.0
|
||||
vue: 3.5.13(typescript@5.9.2)
|
||||
vue-component-type-helpers: 3.1.2
|
||||
vue-component-type-helpers: 3.1.4
|
||||
|
||||
'@swc/helpers@0.5.17':
|
||||
dependencies:
|
||||
@@ -10830,7 +11005,7 @@ snapshots:
|
||||
|
||||
'@types/react@19.1.9':
|
||||
dependencies:
|
||||
csstype: 3.1.3
|
||||
csstype: 3.2.3
|
||||
|
||||
'@types/semver@7.7.0': {}
|
||||
|
||||
@@ -12009,6 +12184,8 @@ snapshots:
|
||||
|
||||
csstype@3.1.3: {}
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
data-urls@5.0.0:
|
||||
dependencies:
|
||||
whatwg-mimetype: 4.0.0
|
||||
@@ -12435,7 +12612,7 @@ snapshots:
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
is-core-module: 2.16.1
|
||||
resolve: 1.22.10
|
||||
resolve: 1.22.11
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
optional: true
|
||||
@@ -12516,6 +12693,10 @@ snapshots:
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
eslint-plugin-oxlint@1.25.0:
|
||||
dependencies:
|
||||
jsonc-parser: 3.3.1
|
||||
|
||||
eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.6.2):
|
||||
dependencies:
|
||||
eslint: 9.35.0(jiti@2.4.2)
|
||||
@@ -13577,10 +13758,12 @@ snapshots:
|
||||
acorn: 8.15.0
|
||||
eslint-visitor-keys: 3.4.3
|
||||
espree: 9.6.1
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
|
||||
jsonc-parser@3.2.0: {}
|
||||
|
||||
jsonc-parser@3.3.1: {}
|
||||
|
||||
jsondiffpatch@0.6.0:
|
||||
dependencies:
|
||||
'@types/diff-match-patch': 1.0.36
|
||||
@@ -14548,6 +14731,27 @@ snapshots:
|
||||
'@oxc-resolver/binding-win32-ia32-msvc': 11.6.1
|
||||
'@oxc-resolver/binding-win32-x64-msvc': 11.6.1
|
||||
|
||||
oxlint-tsgolint@0.4.0:
|
||||
optionalDependencies:
|
||||
'@oxlint-tsgolint/darwin-arm64': 0.4.0
|
||||
'@oxlint-tsgolint/darwin-x64': 0.4.0
|
||||
'@oxlint-tsgolint/linux-arm64': 0.4.0
|
||||
'@oxlint-tsgolint/linux-x64': 0.4.0
|
||||
'@oxlint-tsgolint/win32-arm64': 0.4.0
|
||||
'@oxlint-tsgolint/win32-x64': 0.4.0
|
||||
|
||||
oxlint@1.28.0(oxlint-tsgolint@0.4.0):
|
||||
optionalDependencies:
|
||||
'@oxlint/darwin-arm64': 1.28.0
|
||||
'@oxlint/darwin-x64': 1.28.0
|
||||
'@oxlint/linux-arm64-gnu': 1.28.0
|
||||
'@oxlint/linux-arm64-musl': 1.28.0
|
||||
'@oxlint/linux-x64-gnu': 1.28.0
|
||||
'@oxlint/linux-x64-musl': 1.28.0
|
||||
'@oxlint/win32-arm64': 1.28.0
|
||||
'@oxlint/win32-x64': 1.28.0
|
||||
oxlint-tsgolint: 0.4.0
|
||||
|
||||
p-limit@3.1.0:
|
||||
dependencies:
|
||||
yocto-queue: 0.1.0
|
||||
@@ -15159,6 +15363,13 @@ snapshots:
|
||||
path-parse: 1.0.7
|
||||
supports-preserve-symlinks-flag: 1.0.0
|
||||
|
||||
resolve@1.22.11:
|
||||
dependencies:
|
||||
is-core-module: 2.16.1
|
||||
path-parse: 1.0.7
|
||||
supports-preserve-symlinks-flag: 1.0.0
|
||||
optional: true
|
||||
|
||||
restore-cursor@3.1.0:
|
||||
dependencies:
|
||||
onetime: 5.1.2
|
||||
@@ -15263,6 +15474,8 @@ snapshots:
|
||||
|
||||
semver@7.7.2: {}
|
||||
|
||||
semver@7.7.3: {}
|
||||
|
||||
set-function-length@1.2.2:
|
||||
dependencies:
|
||||
define-data-property: 1.1.4
|
||||
@@ -16157,7 +16370,7 @@ snapshots:
|
||||
|
||||
vue-component-type-helpers@3.1.1: {}
|
||||
|
||||
vue-component-type-helpers@3.1.2: {}
|
||||
vue-component-type-helpers@3.1.4: {}
|
||||
|
||||
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)):
|
||||
dependencies:
|
||||
|
||||
@@ -4,6 +4,7 @@ packages:
|
||||
|
||||
catalog:
|
||||
'@alloc/quick-lru': ^5.2.0
|
||||
'@comfyorg/comfyui-electron-types': 0.5.5
|
||||
'@eslint/js': ^9.35.0
|
||||
'@iconify-json/lucide': ^1.1.178
|
||||
'@iconify/json': ^2.2.380
|
||||
@@ -50,6 +51,7 @@ catalog:
|
||||
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
|
||||
@@ -65,6 +67,8 @@ catalog:
|
||||
markdown-table: ^3.0.4
|
||||
mixpanel-browser: ^2.71.0
|
||||
nx: 21.4.1
|
||||
oxlint: ^1.25.0
|
||||
oxlint-tsgolint: ^0.4.0
|
||||
picocolors: ^1.1.1
|
||||
pinia: ^2.1.7
|
||||
postcss-html: ^1.8.0
|
||||
|
||||
14
scripts/cicd/check-yaml.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(git rev-parse --show-toplevel)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
mapfile -t yaml_files < <(git ls-files '*.yml' '*.yaml')
|
||||
|
||||
if [[ ${#yaml_files[@]} -eq 0 ]]; then
|
||||
echo "No YAML files found to lint"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
yamllint --config-file .yamllint "${yaml_files[@]}"
|
||||
@@ -30,7 +30,7 @@
|
||||
## Styling
|
||||
|
||||
- Use Tailwind CSS only (no custom CSS)
|
||||
- Dark theme: use "dark-theme:" prefix
|
||||
- Use the correct tokens from style.css in the design system package
|
||||
- For common operations, try to use existing VueUse composables that automatically handle effect scope
|
||||
- Example: Use `useElementHover` instead of manually managing mouseover/mouseout event listeners
|
||||
- Example: Use `useIntersectionObserver` for visibility detection instead of custom scroll handlers
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
<SubgraphBreadcrumb />
|
||||
</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-[var(--interface-stroke)] px-2 shadow-interface"
|
||||
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 -->
|
||||
@@ -14,28 +15,69 @@
|
||||
class="[&:not(:has(*>*:not(:empty)))]:hidden"
|
||||
></div>
|
||||
<ComfyActionbar />
|
||||
<IconButton
|
||||
v-tooltip.bottom="queueHistoryTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="queue-history-toggle relative mr-2 transition-colors duration-200 ease-in-out hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
|
||||
:class="queueHistoryButtonBackgroundClass"
|
||||
:aria-pressed="isQueueOverlayExpanded"
|
||||
:aria-label="
|
||||
t('sideToolbar.queueProgressOverlay.expandCollapsedQueue')
|
||||
"
|
||||
@click="toggleQueueOverlay"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--history] block size-4 text-muted-foreground"
|
||||
/>
|
||||
<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" />
|
||||
</div>
|
||||
<QueueProgressOverlay v-model:expanded="isQueueOverlayExpanded" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
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 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 { app } from '@/scripts/app'
|
||||
import { useQueueStore } from '@/stores/queueStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
const isDesktop = isElectron()
|
||||
const { t } = useI18n()
|
||||
const isQueueOverlayExpanded = ref(false)
|
||||
const queueStore = useQueueStore()
|
||||
const queuedCount = computed(() => queueStore.pendingTasks.length)
|
||||
const queueHistoryTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory'))
|
||||
)
|
||||
const queueHistoryButtonBackgroundClass = computed(() =>
|
||||
isQueueOverlayExpanded.value
|
||||
? 'bg-secondary-background-selected'
|
||||
: 'bg-secondary-background'
|
||||
)
|
||||
|
||||
// Maintain support for legacy topbar elements attached by custom scripts
|
||||
const legacyCommandsContainerRef = ref<HTMLElement>()
|
||||
@@ -45,6 +87,10 @@ onMounted(() => {
|
||||
legacyCommandsContainerRef.value.appendChild(app.menu.element)
|
||||
}
|
||||
})
|
||||
|
||||
const toggleQueueOverlay = () => {
|
||||
isQueueOverlayExpanded.value = !isQueueOverlayExpanded.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
|
||||
<Panel
|
||||
class="pointer-events-auto z-1000"
|
||||
class="pointer-events-auto z-1010"
|
||||
:style="style"
|
||||
:class="panelClass"
|
||||
:pt="{
|
||||
@@ -260,6 +260,7 @@ const actionbarClass = computed(() =>
|
||||
'w-[265px] 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',
|
||||
isMouseOverDropZone.value &&
|
||||
'border-[3px] opacity-100 scale-105 shadow-[0_0_20px] shadow-blue-500'
|
||||
)
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
<div class="queue-button-group flex">
|
||||
<SplitButton
|
||||
v-tooltip.bottom="{
|
||||
value: workspaceStore.shiftDown
|
||||
? $t('menu.runWorkflowFront')
|
||||
: $t('menu.runWorkflow'),
|
||||
value: queueButtonTooltip,
|
||||
showDelay: 600
|
||||
}"
|
||||
class="comfyui-queue-button"
|
||||
@@ -12,20 +10,12 @@
|
||||
severity="primary"
|
||||
size="small"
|
||||
:model="queueModeMenuItems"
|
||||
:disabled="hasMissingNodes"
|
||||
data-testid="queue-button"
|
||||
@click="queuePrompt"
|
||||
>
|
||||
<template #icon>
|
||||
<i v-if="workspaceStore.shiftDown" class="icon-[lucide--list-start]" />
|
||||
<i v-else-if="queueMode === 'disabled'" class="icon-[lucide--play]" />
|
||||
<i
|
||||
v-else-if="queueMode === 'instant'"
|
||||
class="icon-[lucide--fast-forward]"
|
||||
/>
|
||||
<i
|
||||
v-else-if="queueMode === 'change'"
|
||||
class="icon-[lucide--step-forward]"
|
||||
/>
|
||||
<i :class="iconClass" />
|
||||
</template>
|
||||
<template #item="{ item }">
|
||||
<Button
|
||||
@@ -42,46 +32,12 @@
|
||||
</template>
|
||||
</SplitButton>
|
||||
<BatchCountEdit />
|
||||
<ButtonGroup class="execution-actions flex flex-nowrap">
|
||||
<Button
|
||||
v-tooltip.bottom="{
|
||||
value: $t('menu.interrupt'),
|
||||
showDelay: 600
|
||||
}"
|
||||
icon="pi pi-times"
|
||||
:severity="executingPrompt ? 'danger' : 'secondary'"
|
||||
:disabled="!executingPrompt"
|
||||
text
|
||||
:aria-label="$t('menu.interrupt')"
|
||||
@click="() => commandStore.execute('Comfy.Interrupt')"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.bottom="{
|
||||
value: $t('sideToolbar.queueTab.clearPendingTasks'),
|
||||
showDelay: 600
|
||||
}"
|
||||
icon="pi pi-stop"
|
||||
:severity="hasPendingTasks ? 'danger' : 'secondary'"
|
||||
:disabled="!hasPendingTasks"
|
||||
text
|
||||
:aria-label="$t('sideToolbar.queueTab.clearPendingTasks')"
|
||||
@click="
|
||||
() => {
|
||||
if (queueCountStore.count.value > 1) {
|
||||
commandStore.execute('Comfy.ClearPendingTasks')
|
||||
}
|
||||
queueMode = 'disabled'
|
||||
}
|
||||
"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import Button from 'primevue/button'
|
||||
import ButtonGroup from 'primevue/buttongroup'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import SplitButton from 'primevue/splitbutton'
|
||||
import { computed } from 'vue'
|
||||
@@ -90,18 +46,17 @@ import { useI18n } from 'vue-i18n'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import {
|
||||
useQueuePendingTaskCountStore,
|
||||
useQueueSettingsStore
|
||||
} from '@/stores/queueStore'
|
||||
import { useQueueSettingsStore } from '@/stores/queueStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
||||
|
||||
import BatchCountEdit from '../BatchCountEdit.vue'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
|
||||
const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore())
|
||||
|
||||
const { hasMissingNodes } = useMissingNodes()
|
||||
|
||||
const { t } = useI18n()
|
||||
const queueModeMenuItemLookup = computed(() => {
|
||||
const items: Record<string, MenuItem> = {
|
||||
@@ -152,10 +107,34 @@ const queueModeMenuItems = computed(() =>
|
||||
Object.values(queueModeMenuItemLookup.value)
|
||||
)
|
||||
|
||||
const executingPrompt = computed(() => !!queueCountStore.count.value)
|
||||
const hasPendingTasks = computed(
|
||||
() => queueCountStore.count.value > 1 || queueMode.value !== 'disabled'
|
||||
)
|
||||
const iconClass = computed(() => {
|
||||
if (hasMissingNodes.value) {
|
||||
return 'icon-[lucide--triangle-alert]'
|
||||
}
|
||||
if (workspaceStore.shiftDown) {
|
||||
return 'icon-[lucide--list-start]'
|
||||
}
|
||||
if (queueMode.value === 'disabled') {
|
||||
return 'icon-[lucide--play]'
|
||||
}
|
||||
if (queueMode.value === 'instant') {
|
||||
return 'icon-[lucide--fast-forward]'
|
||||
}
|
||||
if (queueMode.value === 'change') {
|
||||
return 'icon-[lucide--step-forward]'
|
||||
}
|
||||
return 'icon-[lucide--play]'
|
||||
})
|
||||
|
||||
const queueButtonTooltip = computed(() => {
|
||||
if (hasMissingNodes.value) {
|
||||
return t('menu.runWorkflowDisabled')
|
||||
}
|
||||
if (workspaceStore.shiftDown) {
|
||||
return t('menu.runWorkflowFront')
|
||||
}
|
||||
return t('menu.runWorkflow')
|
||||
})
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
const queuePrompt = async (e: Event) => {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
class="flex flex-col"
|
||||
>
|
||||
<h3
|
||||
class="subcategory-title mb-4 text-xs font-bold tracking-wide text-surface-600 uppercase dark-theme:text-surface-400"
|
||||
class="subcategory-title mb-4 text-xs font-bold tracking-wide text-text-secondary uppercase"
|
||||
>
|
||||
{{ getSubcategoryTitle(subcategory) }}
|
||||
</h3>
|
||||
@@ -16,7 +16,7 @@
|
||||
<div
|
||||
v-for="command in subcategoryCommands"
|
||||
:key="command.id"
|
||||
class="shortcut-item flex items-center justify-between rounded py-2 transition-colors duration-200 hover:bg-surface-100 dark-theme:hover:bg-surface-700"
|
||||
class="shortcut-item flex items-center justify-between rounded py-2 transition-colors duration-200"
|
||||
>
|
||||
<div class="shortcut-info grow pr-4">
|
||||
<div class="shortcut-name text-sm font-medium">
|
||||
@@ -32,7 +32,7 @@
|
||||
<span
|
||||
v-for="key in command.keybinding!.combo.getKeySequences()"
|
||||
:key="key"
|
||||
class="key-badge min-w-6 rounded border bg-surface-200 px-2 py-1 text-center font-mono text-xs dark-theme:bg-surface-600"
|
||||
class="key-badge min-w-6 rounded bg-muted-background px-2 py-1 text-center font-mono text-xs"
|
||||
>
|
||||
{{ formatKey(key) }}
|
||||
</span>
|
||||
@@ -100,21 +100,3 @@ const formatKey = (key: string): string => {
|
||||
return keyMap[key] || key
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.subcategory-title {
|
||||
color: var(--p-text-muted-color);
|
||||
}
|
||||
|
||||
.key-badge {
|
||||
background-color: var(--p-surface-200);
|
||||
border: 1px solid var(--p-surface-300);
|
||||
min-width: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dark-theme .key-badge {
|
||||
background-color: var(--p-surface-600);
|
||||
border-color: var(--p-surface-500);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<a
|
||||
ref="wrapperRef"
|
||||
v-tooltip.bottom="{
|
||||
value: item.label,
|
||||
value: tooltipText,
|
||||
showDelay: 512
|
||||
}"
|
||||
draggable="false"
|
||||
@@ -16,6 +16,10 @@
|
||||
}"
|
||||
@click="handleClick"
|
||||
>
|
||||
<i
|
||||
v-if="hasMissingNodes && isRoot"
|
||||
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" />
|
||||
<i v-if="isActive" class="pi pi-angle-down text-[10px]"></i>
|
||||
@@ -64,6 +68,7 @@ import { useDialogService } from '@/services/dialogService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
import { appendJsonExt } from '@/utils/formatUtil'
|
||||
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
||||
|
||||
interface Props {
|
||||
item: MenuItem
|
||||
@@ -74,6 +79,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
isActive: false
|
||||
})
|
||||
|
||||
const { hasMissingNodes } = useMissingNodes()
|
||||
|
||||
const { t } = useI18n()
|
||||
const menu = ref<InstanceType<typeof Menu> & MenuState>()
|
||||
const dialogService = useDialogService()
|
||||
@@ -115,6 +122,14 @@ const rename = async (
|
||||
}
|
||||
|
||||
const isRoot = props.item.key === 'root'
|
||||
|
||||
const tooltipText = computed(() => {
|
||||
if (hasMissingNodes.value && isRoot) {
|
||||
return t('breadcrumbsMenu.missingNodesWarning')
|
||||
}
|
||||
return props.item.label
|
||||
})
|
||||
|
||||
const menuItems = computed<MenuItem[]>(() => {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -10,8 +10,7 @@ import { cn } from '@/utils/tailwindUtil'
|
||||
const iconGroupClasses = cn(
|
||||
'flex justify-center items-center shrink-0',
|
||||
'outline-hidden border-none p-0 rounded-lg',
|
||||
'bg-white dark-theme:bg-zinc-700',
|
||||
'text-neutral-950 dark-theme:text-white',
|
||||
'bg-secondary-background shadow-sm',
|
||||
'transition-all duration-200',
|
||||
'cursor-pointer'
|
||||
)
|
||||
|
||||
@@ -32,7 +32,7 @@ defineOptions({
|
||||
interface IconTextButtonProps extends BaseButtonProps {
|
||||
iconPosition?: 'left' | 'right'
|
||||
label: string
|
||||
onClick: () => void
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const {
|
||||
|
||||
@@ -7,13 +7,24 @@
|
||||
|
||||
<Popover
|
||||
ref="popover"
|
||||
:append-to="'body'"
|
||||
:auto-z-index="true"
|
||||
:base-z-index="1000"
|
||||
:dismissable="true"
|
||||
:close-on-escape="true"
|
||||
append-to="body"
|
||||
auto-z-index
|
||||
dismissable
|
||||
close-on-escape
|
||||
unstyled
|
||||
:pt="pt"
|
||||
:base-z-index="1000"
|
||||
:pt="{
|
||||
root: {
|
||||
class: cn('absolute z-50')
|
||||
},
|
||||
content: {
|
||||
class: cn(
|
||||
'mt-1 rounded-lg',
|
||||
'bg-secondary-background text-base-foreground',
|
||||
'shadow-lg'
|
||||
)
|
||||
}
|
||||
}"
|
||||
@show="$emit('menuOpened')"
|
||||
@hide="$emit('menuClosed')"
|
||||
>
|
||||
@@ -26,7 +37,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Popover from 'primevue/popover'
|
||||
import { computed, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { BaseButtonProps } from '@/types/buttonTypes'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
@@ -57,19 +68,4 @@ const toggle = (event: Event) => {
|
||||
const hide = () => {
|
||||
popover.value?.hide()
|
||||
}
|
||||
|
||||
const pt = computed(() => ({
|
||||
root: {
|
||||
class: cn('absolute z-50')
|
||||
},
|
||||
content: {
|
||||
class: cn(
|
||||
'mt-1 rounded-lg',
|
||||
'bg-white dark-theme:bg-zinc-800',
|
||||
'text-neutral dark-theme:text-white',
|
||||
'shadow-lg',
|
||||
'border border-zinc-200 dark-theme:border-zinc-700'
|
||||
)
|
||||
}
|
||||
}))
|
||||
</script>
|
||||
|
||||
@@ -47,8 +47,8 @@ const containerClasses = computed(() => {
|
||||
// Variant styles
|
||||
const variantClasses = {
|
||||
default: cn(
|
||||
hasBackground && 'bg-white dark-theme:bg-zinc-800',
|
||||
hasBorder && 'border border-zinc-200 dark-theme:border-zinc-700',
|
||||
hasBackground && 'bg-modal-card-background',
|
||||
hasBorder && 'border border-border-default',
|
||||
hasShadow && 'shadow-sm',
|
||||
hasCursor && 'cursor-pointer'
|
||||
),
|
||||
@@ -57,9 +57,9 @@ const containerClasses = computed(() => {
|
||||
'p-2 transition-colors duration-200'
|
||||
),
|
||||
outline: cn(
|
||||
hasBorder && 'border-2 border-zinc-300 dark-theme:border-zinc-600',
|
||||
hasBorder && 'border-2 border-border-subtle',
|
||||
hasCursor && 'cursor-pointer',
|
||||
'hover:border-zinc-400 dark-theme:hover:border-zinc-500 transition-colors'
|
||||
'hover:border-border-subtle/50 transition-colors'
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<template>
|
||||
<div class="line-clamp-2 h-7 text-xs text-zinc-500 dark-theme:text-zinc-400">
|
||||
<div class="line-clamp-2 h-7 text-xs text-muted-foreground">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { label, variant = 'dark' } = defineProps<{
|
||||
label: string
|
||||
variant?: 'dark' | 'light'
|
||||
variant?: 'dark' | 'light' | 'gray'
|
||||
}>()
|
||||
|
||||
const baseClasses =
|
||||
@@ -19,7 +19,10 @@ const baseClasses =
|
||||
|
||||
const variantStyles = {
|
||||
dark: 'bg-zinc-500/40 text-white/90',
|
||||
light: 'backdrop-blur-[2px] bg-white/50 text-zinc-900 dark-theme:text-white'
|
||||
light: cn('backdrop-blur-[2px] bg-base-background/50 text-base-foreground'),
|
||||
gray: cn(
|
||||
'backdrop-blur-[2px] bg-modal-card-tag-background text-base-foreground'
|
||||
)
|
||||
}
|
||||
|
||||
const chipClasses = computed(() => {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="file-action">
|
||||
<div class="file-action flex flex-row items-center gap-2">
|
||||
<Button
|
||||
v-if="status === null || status === 'error'"
|
||||
class="file-action-button"
|
||||
@@ -23,6 +23,13 @@
|
||||
icon="pi pi-download"
|
||||
@click="triggerDownload"
|
||||
/>
|
||||
<Button
|
||||
v-if="(status === null || status === 'error') && !!props.url"
|
||||
:label="$t('g.copyURL')"
|
||||
size="small"
|
||||
outlined
|
||||
@click="copyURL"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -80,6 +87,7 @@ import ProgressBar from 'primevue/progressbar'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import { useDownload } from '@/composables/useDownload'
|
||||
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
|
||||
import { formatSize } from '@/utils/formatUtil'
|
||||
@@ -100,6 +108,7 @@ const status = ref<string | null>(null)
|
||||
const fileSize = computed(() =>
|
||||
download.fileSize.value ? formatSize(download.fileSize.value) : '?'
|
||||
)
|
||||
const { copyToClipboard } = useCopyToClipboard()
|
||||
const electronDownloadStore = useElectronDownloadStore()
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const [savePath, filename] = props.label.split('/')
|
||||
@@ -126,4 +135,8 @@ const triggerDownload = async () => {
|
||||
const triggerCancelDownload = () => electronDownloadStore.cancel(props.url)
|
||||
const triggerPauseDownload = () => electronDownloadStore.pause(props.url)
|
||||
const triggerResumeDownload = () => electronDownloadStore.resume(props.url)
|
||||
|
||||
const copyURL = async () => {
|
||||
await copyToClipboard(props.url)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="preview-box flex h-16 w-16 items-center justify-center rounded border p-2"
|
||||
:class="{ 'bg-smoke-100 dark-theme:bg-smoke-800': !modelValue }"
|
||||
:class="{ 'bg-base-background': !modelValue }"
|
||||
>
|
||||
<img
|
||||
v-if="modelValue"
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
/>
|
||||
<div
|
||||
v-if="hasError"
|
||||
class="absolute inset-0 flex items-center justify-center bg-surface-50 text-muted dark-theme:bg-surface-800"
|
||||
class="absolute inset-0 flex items-center justify-center"
|
||||
>
|
||||
<img
|
||||
src="/assets/images/default-template.png"
|
||||
|
||||
@@ -35,6 +35,7 @@ import { ValidationState } from '@/utils/validationUtil'
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
validateUrlFn?: (url: string) => Promise<boolean>
|
||||
disableValidation?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -101,6 +102,8 @@ const defaultValidateUrl = async (url: string): Promise<boolean> => {
|
||||
}
|
||||
|
||||
const validateUrl = async (value: string) => {
|
||||
if (props.disableValidation) return
|
||||
|
||||
if (validationState.value === ValidationState.LOADING) return
|
||||
|
||||
const url = cleanInput(value)
|
||||
|
||||
@@ -364,10 +364,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Results Summary -->
|
||||
<div
|
||||
v-if="!isLoading"
|
||||
class="mt-6 px-6 text-sm text-neutral-600 dark-theme:text-neutral-400"
|
||||
>
|
||||
<div v-if="!isLoading" class="mt-6 px-6 text-sm text-muted">
|
||||
{{
|
||||
$t('templateWorkflows.resultsCount', {
|
||||
count: filteredCount,
|
||||
|
||||
@@ -29,7 +29,10 @@
|
||||
import Button from 'primevue/button'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useExternalLink } from '@/composables/useExternalLink'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { buildDocsUrl } = useExternalLink()
|
||||
|
||||
const { apiNodeNames, onLogin, onCancel } = defineProps<{
|
||||
apiNodeNames: string[]
|
||||
@@ -38,6 +41,9 @@ const { apiNodeNames, onLogin, onCancel } = defineProps<{
|
||||
}>()
|
||||
|
||||
const handleLearnMoreClick = () => {
|
||||
window.open('https://docs.comfy.org/tutorials/api-nodes/faq', '_blank')
|
||||
window.open(
|
||||
buildDocsUrl('/tutorials/api-nodes/faq', { includeLocale: true }),
|
||||
'_blank'
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -24,16 +24,14 @@
|
||||
:key="version"
|
||||
class="ml-4"
|
||||
>
|
||||
<div
|
||||
class="text-sm font-medium text-surface-600 dark-theme:text-surface-400"
|
||||
>
|
||||
<div class="text-sm font-medium text-surface-600">
|
||||
{{
|
||||
$t('loadWorkflowWarning.coreNodesFromVersion', {
|
||||
version: version || 'unknown'
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<div class="ml-4 text-sm text-surface-500 dark-theme:text-surface-500">
|
||||
<div class="ml-4 text-sm text-surface-500">
|
||||
{{ getUniqueNodeNames(nodes).join(', ') }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -49,14 +47,14 @@ import { computed } from 'vue'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
|
||||
const props = defineProps<{
|
||||
const { missingCoreNodes } = defineProps<{
|
||||
missingCoreNodes: Record<string, LGraphNode[]>
|
||||
}>()
|
||||
|
||||
const systemStatsStore = useSystemStatsStore()
|
||||
|
||||
const hasMissingCoreNodes = computed(() => {
|
||||
return Object.keys(props.missingCoreNodes).length > 0
|
||||
return Object.keys(missingCoreNodes).length > 0
|
||||
})
|
||||
|
||||
// Use computed for reactive version tracking
|
||||
@@ -66,7 +64,7 @@ const currentComfyUIVersion = computed<string | null>(() => {
|
||||
})
|
||||
|
||||
const sortedMissingCoreNodes = computed(() => {
|
||||
return Object.entries(props.missingCoreNodes).sort(([a], [b]) => {
|
||||
return Object.entries(missingCoreNodes).sort(([a], [b]) => {
|
||||
// Sort by version in descending order (newest first)
|
||||
return compare(b, a) // Reversed for descending order
|
||||
})
|
||||
|
||||
84
src/components/dialog/content/MissingNodesContent.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex w-[490px] flex-col border-t-1 border-border-default"
|
||||
:class="isCloud ? 'border-b-1' : ''"
|
||||
>
|
||||
<div class="flex h-full w-full flex-col gap-4 p-4">
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<p class="m-0 text-sm leading-4 text-muted-foreground">
|
||||
{{
|
||||
isCloud
|
||||
? $t('missingNodes.cloud.description')
|
||||
: $t('missingNodes.oss.description')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<MissingCoreNodesMessage v-if="!isCloud" :missing-core-nodes />
|
||||
|
||||
<!-- Missing Nodes List Wrapper -->
|
||||
<div
|
||||
class="comfy-missing-nodes flex flex-col max-h-[256px] rounded-lg py-2 scrollbar-custom bg-secondary-background"
|
||||
>
|
||||
<div
|
||||
v-for="(node, i) in uniqueNodes"
|
||||
:key="i"
|
||||
class="flex min-h-8 items-center justify-between px-4 py-2 bg-secondary-background text-muted-foreground"
|
||||
>
|
||||
<span class="text-xs">
|
||||
{{ node.label }}
|
||||
</span>
|
||||
<span v-if="node.hint" class="text-xs">{{ node.hint }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom instruction -->
|
||||
<div>
|
||||
<p class="m-0 text-sm leading-4 text-muted-foreground">
|
||||
{{
|
||||
isCloud
|
||||
? $t('missingNodes.cloud.replacementInstruction')
|
||||
: $t('missingNodes.oss.replacementInstruction')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
||||
|
||||
const props = defineProps<{
|
||||
missingNodeTypes: MissingNodeType[]
|
||||
}>()
|
||||
|
||||
// Get missing core nodes for OSS mode
|
||||
const { missingCoreNodes } = useMissingNodes()
|
||||
|
||||
const uniqueNodes = computed(() => {
|
||||
const seenTypes = new Set()
|
||||
return props.missingNodeTypes
|
||||
.filter((node) => {
|
||||
const type = typeof node === 'object' ? node.type : node
|
||||
if (seenTypes.has(type)) return false
|
||||
seenTypes.add(type)
|
||||
return true
|
||||
})
|
||||
.map((node) => {
|
||||
if (typeof node === 'object') {
|
||||
return {
|
||||
label: node.type,
|
||||
hint: node.hint,
|
||||
action: node.action
|
||||
}
|
||||
}
|
||||
return { label: node }
|
||||
})
|
||||
})
|
||||
</script>
|
||||
@@ -1,39 +1,42 @@
|
||||
<template>
|
||||
<NoResultsPlaceholder
|
||||
class="pb-0"
|
||||
icon="pi pi-exclamation-circle"
|
||||
:title="$t('loadWorkflowWarning.missingNodesTitle')"
|
||||
:message="$t('loadWorkflowWarning.missingNodesDescription')"
|
||||
/>
|
||||
<MissingCoreNodesMessage :missing-core-nodes="missingCoreNodes" />
|
||||
<ListBox
|
||||
:options="uniqueNodes"
|
||||
option-label="label"
|
||||
scroll-height="100%"
|
||||
class="comfy-missing-nodes"
|
||||
:pt="{
|
||||
list: { class: 'border-none' }
|
||||
}"
|
||||
<!-- Cloud mode: Learn More + Got It buttons -->
|
||||
<div
|
||||
v-if="isCloud"
|
||||
class="flex w-full items-center justify-between gap-2 py-2 px-4"
|
||||
>
|
||||
<template #option="slotProps">
|
||||
<div class="align-items-center flex">
|
||||
<span class="node-type">{{ slotProps.option.label }}</span>
|
||||
<span v-if="slotProps.option.hint" class="node-hint">{{
|
||||
slotProps.option.hint
|
||||
}}</span>
|
||||
<Button
|
||||
v-if="slotProps.option.action"
|
||||
:label="slotProps.option.action.text"
|
||||
size="small"
|
||||
outlined
|
||||
@click="slotProps.option.action.callback"
|
||||
<IconTextButton
|
||||
:label="$t('missingNodes.cloud.learnMore')"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
icon-position="left"
|
||||
as="a"
|
||||
href="https://www.comfy.org/cloud"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--info]"></i>
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<TextButton
|
||||
:label="$t('missingNodes.cloud.gotIt')"
|
||||
type="secondary"
|
||||
size="md"
|
||||
@click="handleGotItClick"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ListBox>
|
||||
<div v-if="showManagerButtons" class="flex justify-end py-3">
|
||||
|
||||
<!-- OSS mode: Open Manager + Install All buttons -->
|
||||
<div v-else-if="showManagerButtons" class="flex justify-end gap-1 py-2 px-4">
|
||||
<TextButton
|
||||
:label="$t('g.openManager')"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
@click="openManager"
|
||||
/>
|
||||
<PackInstallButton
|
||||
v-if="showInstallAllButton"
|
||||
type="secondary"
|
||||
size="md"
|
||||
:disabled="
|
||||
isLoading || !!error || missingNodePacks.length === 0 || isInstalling
|
||||
@@ -46,40 +49,32 @@
|
||||
: $t('manager.installAllMissingNodes')
|
||||
"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('g.openManager')"
|
||||
size="small"
|
||||
outlined
|
||||
@click="openManager"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import ListBox from 'primevue/listbox'
|
||||
import { computed, nextTick, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import TextButton from '@/components/button/TextButton.vue'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
|
||||
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
||||
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
|
||||
const props = defineProps<{
|
||||
missingNodeTypes: MissingNodeType[]
|
||||
}>()
|
||||
const dialogStore = useDialogStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
// Get missing node packs from workflow with loading and error states
|
||||
const { missingNodePacks, isLoading, error, missingCoreNodes } =
|
||||
useMissingNodes()
|
||||
const handleGotItClick = () => {
|
||||
dialogStore.closeDialog({ key: 'global-missing-nodes' })
|
||||
}
|
||||
|
||||
const { missingNodePacks, isLoading, error } = useMissingNodes()
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const managerState = useManagerState()
|
||||
|
||||
@@ -91,27 +86,6 @@ const isInstalling = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const uniqueNodes = computed(() => {
|
||||
const seenTypes = new Set()
|
||||
return props.missingNodeTypes
|
||||
.filter((node) => {
|
||||
const type = typeof node === 'object' ? node.type : node
|
||||
if (seenTypes.has(type)) return false
|
||||
seenTypes.add(type)
|
||||
return true
|
||||
})
|
||||
.map((node) => {
|
||||
if (typeof node === 'object') {
|
||||
return {
|
||||
label: node.type,
|
||||
hint: node.hint,
|
||||
action: node.action
|
||||
}
|
||||
}
|
||||
return { label: node }
|
||||
})
|
||||
})
|
||||
|
||||
// Show manager buttons unless manager is disabled
|
||||
const showManagerButtons = computed(() => {
|
||||
return managerState.shouldShowManagerButtons.value
|
||||
@@ -129,9 +103,6 @@ const openManager = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
// Computed to check if all missing nodes have been installed
|
||||
const allMissingNodesInstalled = computed(() => {
|
||||
return (
|
||||
@@ -140,13 +111,14 @@ const allMissingNodesInstalled = computed(() => {
|
||||
missingNodePacks.value?.length === 0
|
||||
)
|
||||
})
|
||||
// Watch for completion and close dialog
|
||||
|
||||
// Watch for completion and close dialog (OSS mode only)
|
||||
watch(allMissingNodesInstalled, async (allInstalled) => {
|
||||
if (allInstalled && showInstallAllButton.value) {
|
||||
if (!isCloud && allInstalled && showInstallAllButton.value) {
|
||||
// Use nextTick to ensure state updates are complete
|
||||
await nextTick()
|
||||
|
||||
dialogStore.closeDialog({ key: 'global-load-workflow-warning' })
|
||||
dialogStore.closeDialog({ key: 'global-missing-nodes' })
|
||||
|
||||
// Show success toast
|
||||
useToastStore().add({
|
||||
@@ -158,20 +130,3 @@ watch(allMissingNodesInstalled, async (allInstalled) => {
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.comfy-missing-nodes {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.node-hint {
|
||||
margin-left: 0.5rem;
|
||||
font-style: italic;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
:deep(.p-button) {
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
18
src/components/dialog/content/MissingNodesHeader.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div class="flex w-full items-center justify-between p-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="icon-[lucide--triangle-alert] text-gold-600"></i>
|
||||
<p class="m-0 text-sm">
|
||||
{{
|
||||
isCloud
|
||||
? $t('missingNodes.cloud.title')
|
||||
: $t('missingNodes.oss.title')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
</script>
|
||||
@@ -4,6 +4,7 @@
|
||||
<InputText
|
||||
ref="inputRef"
|
||||
v-model="inputValue"
|
||||
:placeholder
|
||||
autofocus
|
||||
@keyup.enter="onConfirm"
|
||||
@focus="selectAllText"
|
||||
@@ -28,6 +29,7 @@ const props = defineProps<{
|
||||
message: string
|
||||
defaultValue: string
|
||||
onConfirm: (value: string) => void
|
||||
placeholder?: string
|
||||
}>()
|
||||
|
||||
const inputValue = ref<string>(props.defaultValue)
|
||||
|
||||
@@ -123,6 +123,7 @@ import { computed, ref, watch } from 'vue'
|
||||
import UserCredit from '@/components/common/UserCredit.vue'
|
||||
import UsageLogsTable from '@/components/dialog/content/setting/UsageLogsTable.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useExternalLink } from '@/composables/useExternalLink'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
@@ -137,6 +138,7 @@ interface CreditHistoryItemData {
|
||||
isPositive: boolean
|
||||
}
|
||||
|
||||
const { buildDocsUrl } = useExternalLink()
|
||||
const dialogService = useDialogService()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
@@ -183,12 +185,17 @@ const handleMessageSupport = async () => {
|
||||
}
|
||||
|
||||
const handleFaqClick = () => {
|
||||
window.open('https://docs.comfy.org/tutorials/api-nodes/faq', '_blank')
|
||||
window.open(
|
||||
buildDocsUrl('/tutorials/api-nodes/faq', { includeLocale: true }),
|
||||
'_blank'
|
||||
)
|
||||
}
|
||||
|
||||
const handleOpenPartnerNodesInfo = () => {
|
||||
window.open(
|
||||
'https://docs.comfy.org/tutorials/api-nodes/overview#api-nodes',
|
||||
buildDocsUrl('/tutorials/api-nodes/overview#api-nodes', {
|
||||
includeLocale: true
|
||||
}),
|
||||
'_blank'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -452,10 +452,13 @@ onMounted(async () => {
|
||||
'Comfy.CustomColorPalettes'
|
||||
)
|
||||
|
||||
// Restore workflow and workflow tabs state from storage
|
||||
await workflowPersistence.restorePreviousWorkflow()
|
||||
// Restore saved workflow and workflow tabs state
|
||||
await workflowPersistence.initializeWorkflow()
|
||||
workflowPersistence.restoreWorkflowTabsState()
|
||||
|
||||
// Load template from URL if present
|
||||
await workflowPersistence.loadTemplateFromUrlIfPresent()
|
||||
|
||||
// Initialize release store to fetch releases from comfy-api (fire-and-forget)
|
||||
const { useReleaseStore } = await import(
|
||||
'@/platform/updates/common/releaseStore'
|
||||
|
||||
@@ -84,18 +84,6 @@ describe('BypassButton', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should show normal styling when node is not bypassed', () => {
|
||||
const normalNode = { ...mockLGraphNode, mode: LGraphEventMode.ALWAYS }
|
||||
canvasStore.selectedItems = [normalNode] as any
|
||||
|
||||
const wrapper = mountComponent()
|
||||
const button = wrapper.find('button')
|
||||
|
||||
expect(button.classes()).not.toContain(
|
||||
'dark-theme:[&:not(:active)]:!bg-charcoal-600'
|
||||
)
|
||||
})
|
||||
|
||||
it('should show bypassed styling when node is bypassed', async () => {
|
||||
const bypassedNode = { ...mockLGraphNode, mode: LGraphEventMode.BYPASS }
|
||||
canvasStore.selectedItems = [bypassedNode] as any
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<Button
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_Canvas_ToggleSelectedNodes_Bypass.label'),
|
||||
value: $t('commands.Comfy_Canvas_ToggleSelectedNodes_Bypass.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
@@ -18,11 +18,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
const toggleBypass = async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<Button
|
||||
v-tooltip.top="{
|
||||
value: $t('Edit Subgraph Widgets'),
|
||||
value: $t('commands.Comfy_Graph_EditSubgraphWidgets.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Button
|
||||
v-if="isUnpackVisible"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_Graph_UnpackSubgraph.label'),
|
||||
value: $t('commands.Comfy_Graph_UnpackSubgraph.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
@@ -17,7 +17,7 @@
|
||||
<Button
|
||||
v-else-if="isConvertVisible"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_Graph_ConvertToSubgraph.label'),
|
||||
value: $t('commands.Comfy_Graph_ConvertToSubgraph.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
@@ -34,12 +34,10 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useSelectionState } from '@/composables/graph/useSelectionState'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const { isSingleSubgraph, hasAnySelection } = useSelectionState()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Button
|
||||
v-show="isDeletable"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_Canvas_DeleteSelectedItems.label'),
|
||||
value: $t('commands.Comfy_Canvas_DeleteSelectedItems.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
@@ -17,13 +17,11 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useSelectionState } from '@/composables/graph/useSelectionState'
|
||||
import type { Positionable } from '@/lib/litegraph/src/interfaces'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const { selectedItems } = useSelectionState()
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
value: t('selectionToolbox.executeButton.tooltip'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
class="size-8 bg-azure-400 !p-0 dark-theme:bg-azure-600"
|
||||
class="size-8 bg-primary-background text-white p-0"
|
||||
text
|
||||
@mouseenter="() => handleMouseEnter()"
|
||||
@mouseleave="() => handleMouseLeave()"
|
||||
@click="handleClick"
|
||||
>
|
||||
<i class="icon-[lucide--play] size-4 text-white" />
|
||||
<i class="icon-[lucide--play] size-4" />
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<Button
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_3DViewer_Open3DViewer.label'),
|
||||
value: $t('commands.Comfy_3DViewer_Open3DViewer.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
@@ -15,7 +15,6 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Button
|
||||
v-show="isSingleImageNode"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_MaskEditor_OpenMaskEditor.label'),
|
||||
value: $t('commands.Comfy_MaskEditor_OpenMaskEditor.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
@@ -17,7 +17,6 @@
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { useSelectionState } from '@/composables/graph/useSelectionState'
|
||||
import { t } from '@/i18n'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="option.type === 'divider'"
|
||||
class="my-1 h-px bg-smoke-200 dark-theme:bg-zinc-700"
|
||||
/>
|
||||
<div v-if="option.type === 'divider'" class="my-1 h-px bg-border-default" />
|
||||
<div
|
||||
v-else
|
||||
role="button"
|
||||
@@ -26,18 +23,21 @@
|
||||
v-if="option.badge"
|
||||
:severity="option.badge === 'new' ? 'info' : 'secondary'"
|
||||
:value="t(option.badge)"
|
||||
:class="{
|
||||
'rounded-4xl bg-azure-400 dark-theme:bg-azure-600':
|
||||
option.badge === 'new',
|
||||
'rounded-4xl bg-slate-100 dark-theme:bg-black':
|
||||
option.badge === 'deprecated',
|
||||
'h-4 gap-2.5 px-1 text-[9px] text-white uppercase': true
|
||||
}"
|
||||
:class="
|
||||
cn(
|
||||
'h-4 gap-2.5 px-1 text-[9px] text-base-foreground uppercase rounded-4xl',
|
||||
{
|
||||
'bg-primary-background': option.badge === 'new',
|
||||
'bg-secondary-background': option.badge === 'deprecated'
|
||||
}
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
import Badge from 'primevue/badge'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
|
||||
@@ -8,7 +8,18 @@
|
||||
:dismissable="true"
|
||||
:close-on-escape="true"
|
||||
unstyled
|
||||
:pt="pt"
|
||||
:pt="{
|
||||
root: {
|
||||
class: 'absolute z-50 w-[300px]'
|
||||
},
|
||||
content: {
|
||||
class: [
|
||||
'mt-2 text-base-foreground rounded-lg',
|
||||
'shadow-lg border border-border-default',
|
||||
'bg-interface-panel-surface'
|
||||
]
|
||||
}
|
||||
}"
|
||||
@show="onPopoverShow"
|
||||
@hide="onPopoverHide"
|
||||
@wheel="canvasInteractions.forwardEventToCanvas"
|
||||
@@ -36,7 +47,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useRafFn } from '@vueuse/core'
|
||||
import Popover from 'primevue/popover'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import {
|
||||
forceCloseMoreOptionsSignal,
|
||||
@@ -253,19 +264,6 @@ const setSubmenuRef = (key: string, el: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
const pt = computed(() => ({
|
||||
root: {
|
||||
class: 'absolute z-50 w-[300px] px-[12]'
|
||||
},
|
||||
content: {
|
||||
class: [
|
||||
'mt-2 text-neutral dark-theme:text-white rounded-lg',
|
||||
'shadow-lg border border-zinc-200 dark-theme:border-zinc-700',
|
||||
'bg-interface-panel-surface'
|
||||
]
|
||||
}
|
||||
}))
|
||||
|
||||
// Distinguish outside click (PrimeVue dismiss) from programmatic hides.
|
||||
const onPopoverShow = () => {
|
||||
overlayElCache = resolveOverlayEl()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Button
|
||||
v-show="isVisible"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_PublishSubgraph.label'),
|
||||
value: $t('commands.Comfy_PublishSubgraph.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
@@ -18,13 +18,11 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
|
||||