Merge remote-tracking branch 'origin/main' into fix/remove-any-types-part8

This commit is contained in:
Johnpaul
2026-01-21 02:25:22 +01:00
55 changed files with 564 additions and 456 deletions

View File

@@ -122,7 +122,7 @@ echo " pnpm build - Build for production"
echo " pnpm test:unit - Run unit tests"
echo " pnpm typecheck - Run TypeScript checks"
echo " pnpm lint - Run ESLint"
echo " pnpm format - Format code with Prettier"
echo " pnpm format - Format code with oxfmt"
echo ""
echo "Next steps:"
echo "1. Run 'pnpm dev' to start developing"

View File

@@ -42,7 +42,7 @@ jobs:
- name: Run Stylelint with auto-fix
run: pnpm stylelint:fix
- name: Run Prettier with auto-format
- name: Run oxfmt with auto-format
run: pnpm format
- name: Check for changes
@@ -60,7 +60,7 @@ jobs:
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add .
git commit -m "[automated] Apply ESLint and Prettier fixes"
git commit -m "[automated] Apply ESLint and Oxfmt fixes"
git push
- name: Final validation
@@ -80,7 +80,7 @@ jobs:
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '## 🔧 Auto-fixes Applied\n\nThis PR has been automatically updated to fix linting and formatting issues.\n\n**⚠️ Important**: Your local branch is now behind. Run `git pull` before making additional changes to avoid conflicts.\n\n### Changes made:\n- ESLint auto-fixes\n- Prettier formatting'
body: '## 🔧 Auto-fixes Applied\n\nThis PR has been automatically updated to fix linting and formatting issues.\n\n**⚠️ Important**: Your local branch is now behind. Run `git pull` before making additional changes to avoid conflicts.\n\n### Changes made:\n- ESLint auto-fixes\n- Oxfmt formatting'
})
- name: Comment on PR about manual fix needed

View File

@@ -1,7 +1,7 @@
// This file is intentionally kept in CommonJS format (.cjs)
// to resolve compatibility issues with dependencies that require CommonJS.
// Do not convert this file to ESModule format unless all dependencies support it.
const { defineConfig } = require('@lobehub/i18n-cli');
const { defineConfig } = require('@lobehub/i18n-cli')
module.exports = defineConfig({
modelName: 'gpt-4.1',
@@ -10,7 +10,19 @@ module.exports = defineConfig({
entry: 'src/locales/en',
entryLocale: 'en',
output: 'src/locales',
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr', 'pt-BR', 'fa'],
outputLocales: [
'zh',
'zh-TW',
'ru',
'ja',
'ko',
'fr',
'es',
'ar',
'tr',
'pt-BR',
'fa'
],
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream, Civitai, Hugging Face.
'latent' is the short form of 'latent space'.
'mask' is in the context of image processing.
@@ -26,4 +38,4 @@ module.exports = defineConfig({
- Use Arabic-Indic numerals (۰-۹) for numbers where appropriate.
- Maintain consistency with terminology used in Persian software and design applications.
`
});
})

20
.oxfmtrc.json Normal file
View File

@@ -0,0 +1,20 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"singleQuote": true,
"tabWidth": 2,
"semi": false,
"trailingComma": "none",
"printWidth": 80,
"ignorePatterns": [
"packages/registry-types/src/comfyRegistryTypes.ts",
"src/types/generatedManagerTypes.ts",
"**/*.md",
"**/*.json",
"**/*.css",
"**/*.yaml",
"**/*.yml",
"**/*.html",
"**/*.svg",
"**/*.xml"
]
}

View File

@@ -1,2 +0,0 @@
packages/registry-types/src/comfyRegistryTypes.ts
src/types/generatedManagerTypes.ts

View File

@@ -1,11 +0,0 @@
{
"singleQuote": true,
"tabWidth": 2,
"semi": false,
"trailingComma": "none",
"printWidth": 80,
"importOrder": ["^@core/(.*)$", "<THIRD_PARTY_MODULES>", "^@/(.*)$", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"plugins": ["@prettier/plugin-oxc", "@trivago/prettier-plugin-sort-imports"]
}

View File

@@ -1,25 +1,22 @@
{
"recommendations": [
"antfu.vite",
"austenc.tailwind-docs",
"bradlc.vscode-tailwindcss",
"davidanson.vscode-markdownlint",
"dbaeumer.vscode-eslint",
"donjayamanne.githistory",
"eamodio.gitlens",
"esbenp.prettier-vscode",
"figma.figma-vscode-extension",
"github.vscode-github-actions",
"github.vscode-pull-request-github",
"hbenl.vscode-test-explorer",
"kisstkondoros.vscode-codemetrics",
"lokalise.i18n-ally",
"ms-playwright.playwright",
"oxc.oxc-vscode",
"sonarsource.sonarlint-vscode",
"vitest.explorer",
"vue.volar",
"sonarsource.sonarlint-vscode",
"deque-systems.vscode-axe-linter",
"kisstkondoros.vscode-codemetrics",
"donjayamanne.githistory",
"wix.vscode-import-cost",
"prograhammer.tslint-vue",
"antfu.vite"
"wix.vscode-import-cost"
]
}

View File

@@ -27,10 +27,10 @@ See @docs/guidance/*.md for file-type-specific conventions (auto-loaded by glob)
- Build output: `dist/`
- Configs
- `vite.config.mts`
- `vitest.config.ts`
- `playwright.config.ts`
- `eslint.config.ts`
- `.prettierrc`
- `.oxfmtrc.json`
- `.oxlintrc.json`
- etc.
## Monorepo Architecture
@@ -46,7 +46,7 @@ The project uses **Nx** for build orchestration and task management
- `pnpm test:unit`: Run Vitest unit tests
- `pnpm test:browser`: Run Playwright E2E tests (`browser_tests/`)
- `pnpm lint` / `pnpm lint:fix`: Lint (ESLint)
- `pnpm format` / `pnpm format:check`: Prettier
- `pnpm format` / `pnpm format:check`: oxfmt
- `pnpm typecheck`: Vue TSC type checking
- `pnpm storybook`: Start Storybook development server
@@ -72,7 +72,7 @@ The project uses **Nx** for build orchestration and task management
- Composition API only
- Tailwind 4 styling
- Avoid `<style>` blocks
- Style: (see `.prettierrc`)
- Style: (see `.oxfmtrc.json`)
- Indent 2 spaces
- single quotes
- no trailing semicolons

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View File

@@ -4,9 +4,7 @@ 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'
// WORKAROUND: eslint-plugin-prettier causes segfault on Node.js 24 + Windows
// See: https://github.com/nodejs/node/issues/58690
// Prettier is still run separately in lint-staged, so this is safe to disable
// eslint-config-prettier disables ESLint rules that conflict with formatters (oxfmt)
import eslintConfigPrettier from 'eslint-config-prettier'
import { configs as storybookConfigs } from 'eslint-plugin-storybook'
import unusedImports from 'eslint-plugin-unused-imports'
@@ -111,7 +109,7 @@ export default defineConfig([
tseslintConfigs.recommended,
// Difference in typecheck on CI vs Local
pluginVue.configs['flat/recommended'],
// Use eslint-config-prettier instead of eslint-plugin-prettier to avoid Node 24 segfault
// Disables ESLint rules that conflict with formatters
eslintConfigPrettier,
// @ts-expect-error Type incompatibility between storybook plugin and ESLint config types
storybookConfigs['flat/recommended'],

View File

@@ -1,25 +0,0 @@
import path from 'node:path'
export default {
'tests-ui/**': () => 'echo "Files in tests-ui/ are deprecated. Colocate tests with source files." && exit 1',
'./**/*.js': (stagedFiles) => formatAndEslint(stagedFiles),
'./**/*.{ts,tsx,vue,mts}': (stagedFiles) => [
...formatAndEslint(stagedFiles),
'pnpm typecheck'
]
}
function formatAndEslint(fileNames) {
// Convert absolute paths to relative paths for better ESLint resolution
const relativePaths = fileNames.map((f) => path.relative(process.cwd(), f))
const joinedPaths = relativePaths.map((p) => `"${p}"`).join(' ')
return [
`pnpm exec prettier --cache --write ${joinedPaths}`,
`pnpm exec oxlint --fix ${joinedPaths}`,
`pnpm exec eslint --cache --fix --no-warn-ignored ${joinedPaths}`
]
}

View File

@@ -1,6 +1,9 @@
import path from 'node:path'
export default {
'tests-ui/**': () =>
'echo "Files in tests-ui/ are deprecated. Colocate tests with source files." && exit 1',
'./**/*.js': (stagedFiles: string[]) => formatAndEslint(stagedFiles),
'./**/*.{ts,tsx,vue,mts}': (stagedFiles: string[]) => [
@@ -14,7 +17,7 @@ function formatAndEslint(fileNames: string[]) {
const relativePaths = fileNames.map((f) => path.relative(process.cwd(), f))
const joinedPaths = relativePaths.map((p) => `"${p}"`).join(' ')
return [
`pnpm exec prettier --cache --write ${joinedPaths}`,
`pnpm exec oxfmt --write ${joinedPaths}`,
`pnpm exec oxlint --fix ${joinedPaths}`,
`pnpm exec eslint --cache --fix --no-warn-ignored ${joinedPaths}`
]

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.38.6",
"version": "1.38.7",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -22,10 +22,8 @@
"dev:no-vue": "cross-env DISABLE_VUE_PLUGINS=true nx serve",
"dev": "nx serve",
"devtools:pycheck": "python3 -m compileall -q tools/devtools",
"format:check:no-cache": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
"format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}' --cache",
"format:no-cache": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --list-different",
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --cache --list-different",
"format:check": "oxfmt --check",
"format": "oxfmt --write",
"json-schema": "tsx scripts/generate-json-schema.ts",
"knip:no-cache": "knip",
"knip": "knip --cache",
@@ -63,14 +61,12 @@
"@nx/vite": "catalog:",
"@pinia/testing": "catalog:",
"@playwright/test": "catalog:",
"@prettier/plugin-oxc": "catalog:",
"@sentry/vite-plugin": "catalog:",
"@storybook/addon-docs": "catalog:",
"@storybook/addon-mcp": "catalog:",
"@storybook/vue3": "catalog:",
"@storybook/vue3-vite": "catalog:",
"@tailwindcss/vite": "catalog:",
"@trivago/prettier-plugin-sort-imports": "catalog:",
"@types/fs-extra": "catalog:",
"@types/jsdom": "catalog:",
"@types/node": "catalog:",
@@ -101,11 +97,11 @@
"markdown-table": "catalog:",
"mixpanel-browser": "catalog:",
"nx": "catalog:",
"oxfmt": "catalog:",
"oxlint": "catalog:",
"oxlint-tsgolint": "catalog:",
"picocolors": "catalog:",
"postcss-html": "catalog:",
"prettier": "catalog:",
"pretty-bytes": "catalog:",
"rollup-plugin-visualizer": "catalog:",
"storybook": "catalog:",

323
pnpm-lock.yaml generated
View File

@@ -48,9 +48,6 @@ catalogs:
'@playwright/test':
specifier: ^1.57.0
version: 1.57.0
'@prettier/plugin-oxc':
specifier: ^0.1.3
version: 0.1.3
'@primeuix/forms':
specifier: 0.0.2
version: 0.0.2
@@ -96,9 +93,6 @@ catalogs:
'@tailwindcss/vite':
specifier: ^4.1.12
version: 4.1.12
'@trivago/prettier-plugin-sort-imports':
specifier: ^5.2.0
version: 5.2.2
'@types/fs-extra':
specifier: ^11.0.4
version: 11.0.4
@@ -207,6 +201,9 @@ catalogs:
nx:
specifier: 22.2.6
version: 22.2.6
oxfmt:
specifier: ^0.26.0
version: 0.26.0
oxlint:
specifier: ^1.33.0
version: 1.33.0
@@ -222,9 +219,6 @@ catalogs:
postcss-html:
specifier: ^1.8.0
version: 1.8.0
prettier:
specifier: ^3.7.4
version: 3.7.4
pretty-bytes:
specifier: ^7.1.0
version: 7.1.0
@@ -540,9 +534,6 @@ importers:
'@playwright/test':
specifier: 'catalog:'
version: 1.57.0
'@prettier/plugin-oxc':
specifier: 'catalog:'
version: 0.1.3
'@sentry/vite-plugin':
specifier: 'catalog:'
version: 4.6.0
@@ -561,9 +552,6 @@ importers:
'@tailwindcss/vite':
specifier: 'catalog:'
version: 4.1.12(vite@8.0.0-beta.8(@types/node@24.10.4)(esbuild@0.27.1)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
'@trivago/prettier-plugin-sort-imports':
specifier: 'catalog:'
version: 5.2.2(@vue/compiler-sfc@3.5.25)(prettier@3.7.4)
'@types/fs-extra':
specifier: 'catalog:'
version: 11.0.4
@@ -654,6 +642,9 @@ importers:
nx:
specifier: 'catalog:'
version: 22.2.6
oxfmt:
specifier: 'catalog:'
version: 0.26.0
oxlint:
specifier: 'catalog:'
version: 1.33.0(oxlint-tsgolint@0.9.1)
@@ -666,9 +657,6 @@ importers:
postcss-html:
specifier: 'catalog:'
version: 1.8.0
prettier:
specifier: 'catalog:'
version: 3.7.4
pretty-bytes:
specifier: 'catalog:'
version: 7.1.0
@@ -2517,95 +2505,6 @@ packages:
'@one-ini/wasm@0.1.1':
resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==}
'@oxc-parser/binding-android-arm64@0.99.0':
resolution: {integrity: sha512-V4jhmKXgQQdRnm73F+r3ZY4pUEsijQeSraFeaCGng7abSNJGs76X6l82wHnmjLGFAeY00LWtjcELs7ZmbJ9+lA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@oxc-parser/binding-darwin-arm64@0.99.0':
resolution: {integrity: sha512-Rp41nf9zD5FyLZciS9l1GfK8PhYqrD5kEGxyTOA2esTLeAy37rZxetG2E3xteEolAkeb2WDkVrlxPtibeAncMg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@oxc-parser/binding-darwin-x64@0.99.0':
resolution: {integrity: sha512-WVonp40fPPxo5Gs0POTI57iEFv485TvNKOHMwZRhigwZRhZY2accEAkYIhei9eswF4HN5B44Wybkz7Gd1Qr/5Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@oxc-parser/binding-freebsd-x64@0.99.0':
resolution: {integrity: sha512-H30bjOOttPmG54gAqu6+HzbLEzuNOYO2jZYrIq4At+NtLJwvNhXz28Hf5iEAFZIH/4hMpLkM4VN7uc+5UlNW3Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@oxc-parser/binding-linux-arm-gnueabihf@0.99.0':
resolution: {integrity: sha512-0Z/Th0SYqzSRDPs6tk5lQdW0i73UCupnim3dgq2oW0//UdLonV/5wIZCArfKGC7w9y4h8TxgXpgtIyD1kKzzlQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@oxc-parser/binding-linux-arm-musleabihf@0.99.0':
resolution: {integrity: sha512-xo0wqNd5bpbzQVNpAIFbHk1xa+SaS/FGBABCd942SRTnrpxl6GeDj/s1BFaGcTl8MlwlKVMwOcyKrw/2Kdfquw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@oxc-parser/binding-linux-arm64-gnu@0.99.0':
resolution: {integrity: sha512-u26I6LKoLTPTd4Fcpr0aoAtjnGf5/ulMllo+QUiBhupgbVCAlaj4RyXH/mvcjcsl2bVBv9E/gYJZz2JjxQWXBA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
'@oxc-parser/binding-linux-arm64-musl@0.99.0':
resolution: {integrity: sha512-qhftDo2D37SqCEl3ZTa367NqWSZNb1Ddp34CTmShLKFrnKdNiUn55RdokLnHtf1AL5ssaQlYDwBECX7XiBWOhw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
'@oxc-parser/binding-linux-riscv64-gnu@0.99.0':
resolution: {integrity: sha512-zxn/xkf519f12FKkpL5XwJipsylfSSnm36h6c1zBDTz4fbIDMGyIhHfWfwM7uUmHo9Aqw1pLxFpY39Etv398+Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
'@oxc-parser/binding-linux-s390x-gnu@0.99.0':
resolution: {integrity: sha512-Y1eSDKDS5E4IVC7Oxw+NbYAKRmJPMJTIjW+9xOWwteDHkFqpocKe0USxog+Q1uhzalD9M0p9eXWEWdGQCMDBMQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
'@oxc-parser/binding-linux-x64-gnu@0.99.0':
resolution: {integrity: sha512-YVJMfk5cFWB8i2/nIrbk6n15bFkMHqWnMIWkVx7r2KwpTxHyFMfu2IpeVKo1ITDSmt5nBrGdLHD36QRlu2nDLg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
'@oxc-parser/binding-linux-x64-musl@0.99.0':
resolution: {integrity: sha512-2+SDPrie5f90A1b9EirtVggOgsqtsYU5raZwkDYKyS1uvJzjqHCDhG/f4TwQxHmIc5YkczdQfwvN91lwmjsKYQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
'@oxc-parser/binding-wasm32-wasi@0.99.0':
resolution: {integrity: sha512-DKA4j0QerUWSMADziLM5sAyM7V53Fj95CV9SjP77bPfEfT7MnvFKnneaRMqPK1cpzjAGiQF52OBUIKyk0dwOQA==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
'@oxc-parser/binding-win32-arm64-msvc@0.99.0':
resolution: {integrity: sha512-EaB3AvsxqdNUhh9FOoAxRZ2L4PCRwDlDb//QXItwyOJrX7XS+uGK9B1KEUV4FZ/7rDhHsWieLt5e07wl2Ti5AQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@oxc-parser/binding-win32-x64-msvc@0.99.0':
resolution: {integrity: sha512-sJN1Q8h7ggFOyDn0zsHaXbP/MklAVUvhrbq0LA46Qum686P3SZQHjbATqJn9yaVEvaSKXCshgl0vQ1gWkGgpcQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@oxc-project/runtime@0.108.0':
resolution: {integrity: sha512-J1cESY4anMO4i9KtCPmCfQAzAR00Uw4SWsDPFP10CIwDMugkh34UrTKByuYKuPaHy0XAk8LlJiZJq2OLMfbuIQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2613,9 +2512,6 @@ packages:
'@oxc-project/types@0.108.0':
resolution: {integrity: sha512-7lf13b2IA/kZO6xgnIZA88sq3vwrxWk+2vxf6cc+omwYCRTiA5e63Beqf3fz/v8jEviChWWmFYBwzfSeyrsj7Q==}
'@oxc-project/types@0.99.0':
resolution: {integrity: sha512-LLDEhXB7g1m5J+woRSgfKsFPS3LhR9xRhTeIoEBm5WrkwMxn6eZ0Ld0c0K5eHB57ChZX6I3uSmmLjZ8pcjlRcw==}
'@oxc-resolver/binding-android-arm-eabi@11.15.0':
resolution: {integrity: sha512-Q+lWuFfq7whNelNJIP1dhXaVz4zO9Tu77GcQHyxDWh3MaCoO2Bisphgzmsh4ZoUe2zIchQh6OvQL99GlWHg9Tw==}
cpu: [arm]
@@ -2716,6 +2612,46 @@ packages:
cpu: [x64]
os: [win32]
'@oxfmt/darwin-arm64@0.26.0':
resolution: {integrity: sha512-AAGc+8CffkiWeVgtWf4dPfQwHEE5c/j/8NWH7VGVxxJRCZFdmWcqCXprvL2H6qZFewvDLrFbuSPRCqYCpYGaTQ==}
cpu: [arm64]
os: [darwin]
'@oxfmt/darwin-x64@0.26.0':
resolution: {integrity: sha512-xFx5ijCTjw577wJvFlZEMmKDnp3HSCcbYdCsLRmC5i3TZZiDe9DEYh3P46uqhzj8BkEw1Vm1ZCWdl48aEYAzvQ==}
cpu: [x64]
os: [darwin]
'@oxfmt/linux-arm64-gnu@0.26.0':
resolution: {integrity: sha512-GubkQeQT5d3B/Jx/IiR7NMkSmXrCZcVI0BPh1i7mpFi8HgD1hQ/LbhiBKAMsMqs5bbugdQOgBEl8bOhe8JhW1g==}
cpu: [arm64]
os: [linux]
'@oxfmt/linux-arm64-musl@0.26.0':
resolution: {integrity: sha512-OEypUwK69bFPj+aa3/LYCnlIUPgoOLu//WNcriwpnWNmt47808Ht7RJSg+MNK8a7pSZHpXJ5/E6CRK/OTwFdaQ==}
cpu: [arm64]
os: [linux]
'@oxfmt/linux-x64-gnu@0.26.0':
resolution: {integrity: sha512-xO6iEW2bC6ZHyOTPmPWrg/nM6xgzyRPaS84rATy6F8d79wz69LdRdJ3l/PXlkqhi7XoxhvX4ExysA0Nf10ZZEQ==}
cpu: [x64]
os: [linux]
'@oxfmt/linux-x64-musl@0.26.0':
resolution: {integrity: sha512-Z3KuZFC+MIuAyFCXBHY71kCsdRq1ulbsbzTe71v+hrEv7zVBn6yzql+/AZcgfIaKzWO9OXNuz5WWLWDmVALwow==}
cpu: [x64]
os: [linux]
'@oxfmt/win32-arm64@0.26.0':
resolution: {integrity: sha512-3zRbqwVWK1mDhRhTknlQFpRFL9GhEB5GfU6U7wawnuEwpvi39q91kJ+SRJvJnhyPCARkjZBd1V8XnweN5IFd1g==}
cpu: [arm64]
os: [win32]
'@oxfmt/win32-x64@0.26.0':
resolution: {integrity: sha512-m8TfIljU22i9UEIkD+slGPifTFeaCwIUfxszN3E6ABWP1KQbtwSw9Ak0TdoikibvukF/dtbeyG3WW63jv9DnEg==}
cpu: [x64]
os: [win32]
'@oxlint-tsgolint/darwin-arm64@0.9.1':
resolution: {integrity: sha512-vk+8kChWqN+F+QUOvp4/6jDTlDCzXPgYGkxdi6EOUSOmCP1ix0uYOlIi/ytH2imXmC8YfPgLR/1BhqbsuDKuew==}
cpu: [arm64]
@@ -2824,10 +2760,6 @@ packages:
'@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
'@prettier/plugin-oxc@0.1.3':
resolution: {integrity: sha512-aABz3zIRilpWMekbt1FL1JVBQrQLR8L4Td2SRctECrWSsXGTNn/G1BqNSKCdbvQS1LWstAXfqcXzDki7GAAJyg==}
engines: {node: '>=14'}
'@primeuix/forms@0.0.2':
resolution: {integrity: sha512-DpecPQd/Qf/kav4LKCaIeGuT3AkwhJzuHCkLANTVlN/zBvo8KIj3OZHsCkm0zlIMVVnaJdtx1ULNlRQdudef+A==}
engines: {node: '>=12.11.0'}
@@ -3571,22 +3503,6 @@ packages:
'@tmcp/auth':
optional: true
'@trivago/prettier-plugin-sort-imports@5.2.2':
resolution: {integrity: sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==}
engines: {node: '>18.12'}
peerDependencies:
'@vue/compiler-sfc': 3.x
prettier: 2.x - 3.x
prettier-plugin-svelte: 3.x
svelte: 4.x || 5.x
peerDependenciesMeta:
'@vue/compiler-sfc':
optional: true
prettier-plugin-svelte:
optional: true
svelte:
optional: true
'@tweenjs/tween.js@23.1.3':
resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
@@ -6039,9 +5955,6 @@ packages:
engines: {node: '>=10'}
hasBin: true
javascript-natural-sort@0.7.1:
resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==}
jest-diff@30.2.0:
resolution: {integrity: sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
@@ -6889,13 +6802,14 @@ packages:
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
engines: {node: '>= 0.4'}
oxc-parser@0.99.0:
resolution: {integrity: sha512-MpS1lbd2vR0NZn1v0drpgu7RUFu3x9Rd0kxExObZc2+F+DIrV0BOMval/RO3BYGwssIOerII6iS8EbbpCCZQpQ==}
engines: {node: ^20.19.0 || >=22.12.0}
oxc-resolver@11.15.0:
resolution: {integrity: sha512-Hk2J8QMYwmIO9XTCUiOH00+Xk2/+aBxRUnhrSlANDyCnLYc32R1WSIq1sU2yEdlqd53FfMpPEpnBYIKQMzliJw==}
oxfmt@0.26.0:
resolution: {integrity: sha512-UDD1wFNwfeorMm2ZY0xy1KRAAvJ5NjKBfbDmiMwGP7baEHTq65cYpC0aPP+BGHc8weXUbSZaK8MdGyvuRUvS4Q==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
oxlint-tsgolint@0.9.1:
resolution: {integrity: sha512-w1lIvUDkkiAPFyo268SFGrdh1LQ3Lcs1XShES7I4X75TliQA0os5XJ5hNZ4lYsSevqcofgEtq4xq7rBumv69iQ==}
hasBin: true
@@ -7796,6 +7710,10 @@ packages:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
tinypool@2.0.0:
resolution: {integrity: sha512-/RX9RzeH2xU5ADE7n2Ykvmi9ED3FBGPAjw9u3zucrNNaEBIO0HPSYgL0NT7+3p147ojeSdaVu08F6hjpv31HJg==}
engines: {node: ^20.0.0 || >=22.0.0}
tinyrainbow@2.0.0:
resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
engines: {node: '>=14.0.0'}
@@ -10677,59 +10595,10 @@ snapshots:
'@one-ini/wasm@0.1.1': {}
'@oxc-parser/binding-android-arm64@0.99.0':
optional: true
'@oxc-parser/binding-darwin-arm64@0.99.0':
optional: true
'@oxc-parser/binding-darwin-x64@0.99.0':
optional: true
'@oxc-parser/binding-freebsd-x64@0.99.0':
optional: true
'@oxc-parser/binding-linux-arm-gnueabihf@0.99.0':
optional: true
'@oxc-parser/binding-linux-arm-musleabihf@0.99.0':
optional: true
'@oxc-parser/binding-linux-arm64-gnu@0.99.0':
optional: true
'@oxc-parser/binding-linux-arm64-musl@0.99.0':
optional: true
'@oxc-parser/binding-linux-riscv64-gnu@0.99.0':
optional: true
'@oxc-parser/binding-linux-s390x-gnu@0.99.0':
optional: true
'@oxc-parser/binding-linux-x64-gnu@0.99.0':
optional: true
'@oxc-parser/binding-linux-x64-musl@0.99.0':
optional: true
'@oxc-parser/binding-wasm32-wasi@0.99.0':
dependencies:
'@napi-rs/wasm-runtime': 1.1.1
optional: true
'@oxc-parser/binding-win32-arm64-msvc@0.99.0':
optional: true
'@oxc-parser/binding-win32-x64-msvc@0.99.0':
optional: true
'@oxc-project/runtime@0.108.0': {}
'@oxc-project/types@0.108.0': {}
'@oxc-project/types@0.99.0': {}
'@oxc-resolver/binding-android-arm-eabi@11.15.0':
optional: true
@@ -10792,6 +10661,30 @@ snapshots:
'@oxc-resolver/binding-win32-x64-msvc@11.15.0':
optional: true
'@oxfmt/darwin-arm64@0.26.0':
optional: true
'@oxfmt/darwin-x64@0.26.0':
optional: true
'@oxfmt/linux-arm64-gnu@0.26.0':
optional: true
'@oxfmt/linux-arm64-musl@0.26.0':
optional: true
'@oxfmt/linux-x64-gnu@0.26.0':
optional: true
'@oxfmt/linux-x64-musl@0.26.0':
optional: true
'@oxfmt/win32-arm64@0.26.0':
optional: true
'@oxfmt/win32-x64@0.26.0':
optional: true
'@oxlint-tsgolint/darwin-arm64@0.9.1':
optional: true
@@ -10866,10 +10759,6 @@ snapshots:
'@polka/url@1.0.0-next.29': {}
'@prettier/plugin-oxc@0.1.3':
dependencies:
oxc-parser: 0.99.0
'@primeuix/forms@0.0.2':
dependencies:
'@primeuix/utils': 0.3.2
@@ -11585,20 +11474,6 @@ snapshots:
esm-env: 1.2.2
tmcp: 1.19.0(typescript@5.9.3)
'@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.25)(prettier@3.7.4)':
dependencies:
'@babel/generator': 7.28.5
'@babel/parser': 7.28.5
'@babel/traverse': 7.28.5
'@babel/types': 7.28.5
javascript-natural-sort: 0.7.1
lodash: 4.17.21
prettier: 3.7.4
optionalDependencies:
'@vue/compiler-sfc': 3.5.25
transitivePeerDependencies:
- supports-color
'@tweenjs/tween.js@23.1.3': {}
'@tybys/wasm-util@0.10.1':
@@ -14431,8 +14306,6 @@ snapshots:
filelist: 1.0.4
minimatch: 3.1.2
javascript-natural-sort@0.7.1: {}
jest-diff@30.2.0:
dependencies:
'@jest/diff-sequences': 30.0.1
@@ -15531,26 +15404,6 @@ snapshots:
safe-push-apply: 1.0.0
optional: true
oxc-parser@0.99.0:
dependencies:
'@oxc-project/types': 0.99.0
optionalDependencies:
'@oxc-parser/binding-android-arm64': 0.99.0
'@oxc-parser/binding-darwin-arm64': 0.99.0
'@oxc-parser/binding-darwin-x64': 0.99.0
'@oxc-parser/binding-freebsd-x64': 0.99.0
'@oxc-parser/binding-linux-arm-gnueabihf': 0.99.0
'@oxc-parser/binding-linux-arm-musleabihf': 0.99.0
'@oxc-parser/binding-linux-arm64-gnu': 0.99.0
'@oxc-parser/binding-linux-arm64-musl': 0.99.0
'@oxc-parser/binding-linux-riscv64-gnu': 0.99.0
'@oxc-parser/binding-linux-s390x-gnu': 0.99.0
'@oxc-parser/binding-linux-x64-gnu': 0.99.0
'@oxc-parser/binding-linux-x64-musl': 0.99.0
'@oxc-parser/binding-wasm32-wasi': 0.99.0
'@oxc-parser/binding-win32-arm64-msvc': 0.99.0
'@oxc-parser/binding-win32-x64-msvc': 0.99.0
oxc-resolver@11.15.0:
optionalDependencies:
'@oxc-resolver/binding-android-arm-eabi': 11.15.0
@@ -15574,6 +15427,19 @@ snapshots:
'@oxc-resolver/binding-win32-ia32-msvc': 11.15.0
'@oxc-resolver/binding-win32-x64-msvc': 11.15.0
oxfmt@0.26.0:
dependencies:
tinypool: 2.0.0
optionalDependencies:
'@oxfmt/darwin-arm64': 0.26.0
'@oxfmt/darwin-x64': 0.26.0
'@oxfmt/linux-arm64-gnu': 0.26.0
'@oxfmt/linux-arm64-musl': 0.26.0
'@oxfmt/linux-x64-gnu': 0.26.0
'@oxfmt/linux-x64-musl': 0.26.0
'@oxfmt/win32-arm64': 0.26.0
'@oxfmt/win32-x64': 0.26.0
oxlint-tsgolint@0.9.1:
optionalDependencies:
'@oxlint-tsgolint/darwin-arm64': 0.9.1
@@ -15754,7 +15620,8 @@ snapshots:
prelude-ls@1.2.1: {}
prettier@3.7.4: {}
prettier@3.7.4:
optional: true
pretty-bytes@7.1.0: {}
@@ -16717,6 +16584,8 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
tinypool@2.0.0: {}
tinyrainbow@2.0.0: {}
tinyrainbow@3.0.3: {}

View File

@@ -17,7 +17,6 @@ catalog:
'@nx/vite': 22.2.6
'@pinia/testing': ^1.0.3
'@playwright/test': ^1.57.0
'@prettier/plugin-oxc': ^0.1.3
'@primeuix/forms': 0.0.2
'@primeuix/styled': 0.3.2
'@primeuix/utils': ^0.3.2
@@ -33,7 +32,6 @@ catalog:
'@storybook/vue3': ^10.1.9
'@storybook/vue3-vite': ^10.1.9
'@tailwindcss/vite': ^4.1.12
'@trivago/prettier-plugin-sort-imports': ^5.2.0
'@types/fs-extra': ^11.0.4
'@types/jsdom': ^21.1.7
'@types/node': ^24.1.0
@@ -70,12 +68,12 @@ catalog:
markdown-table: ^3.0.4
mixpanel-browser: ^2.71.0
nx: 22.2.6
oxfmt: ^0.26.0
oxlint: ^1.33.0
oxlint-tsgolint: ^0.9.1
picocolors: ^1.1.1
pinia: ^3.0.4
postcss-html: ^1.8.0
prettier: ^3.7.4
pretty-bytes: ^7.1.0
primeicons: ^7.0.0
primevue: ^4.2.5

View File

@@ -12,6 +12,7 @@ declare global {
const __ALGOLIA_API_KEY__: string
const __USE_PROD_CONFIG__: boolean
const __DISTRIBUTION__: 'desktop' | 'localhost' | 'cloud'
const __IS_NIGHTLY__: boolean
}
type GlobalWithDefines = typeof globalThis & {
@@ -22,6 +23,7 @@ type GlobalWithDefines = typeof globalThis & {
__ALGOLIA_API_KEY__: string
__USE_PROD_CONFIG__: boolean
__DISTRIBUTION__: 'desktop' | 'localhost' | 'cloud'
__IS_NIGHTLY__: boolean
window?: Record<string, unknown>
}
@@ -36,6 +38,7 @@ globalWithDefines.__ALGOLIA_APP_ID__ = ''
globalWithDefines.__ALGOLIA_API_KEY__ = ''
globalWithDefines.__USE_PROD_CONFIG__ = false
globalWithDefines.__DISTRIBUTION__ = 'localhost'
globalWithDefines.__IS_NIGHTLY__ = false
// Provide a minimal window shim for Node environment
// This is needed for code that checks window existence during imports

View File

@@ -28,7 +28,7 @@
/>
</div>
<div
class="node-actions touch:opacity-100 motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
class="node-actions flex gap-1 touch:opacity-100 motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
>
<slot name="actions" :node="props.node" />
</div>

View File

@@ -25,13 +25,31 @@ const widgetsSectionDataList = computed((): NodeWidgetsListList => {
return nodes.map((node) => {
const { widgets = [] } = node
const shownWidgets = widgets
.filter((w) => !(w.options?.canvasOnly || w.options?.hidden))
.filter(
(w) =>
!(w.options?.canvasOnly || w.options?.hidden || w.options?.advanced)
)
.map((widget) => ({ node, widget }))
return { widgets: shownWidgets, node }
})
})
const advancedWidgetsSectionDataList = computed((): NodeWidgetsListList => {
return nodes
.map((node) => {
const { widgets = [] } = node
const advancedWidgets = widgets
.filter(
(w) =>
!(w.options?.canvasOnly || w.options?.hidden) && w.options?.advanced
)
.map((widget) => ({ node, widget }))
return { widgets: advancedWidgets, node }
})
.filter(({ widgets }) => widgets.length > 0)
})
const isMultipleNodesSelected = computed(
() => widgetsSectionDataList.value.length > 1
)
@@ -56,6 +74,12 @@ const label = computed(() => {
: t('rightSidePanel.inputsNone')
: undefined // SectionWidgets display node titles by default
})
const advancedLabel = computed(() => {
return !mustShowNodeTitle && !isMultipleNodesSelected.value
? t('rightSidePanel.advancedInputs')
: undefined // SectionWidgets display node titles by default
})
</script>
<template>
@@ -93,4 +117,16 @@ const label = computed(() => {
class="border-b border-interface-stroke"
/>
</TransitionGroup>
<template v-if="advancedWidgetsSectionDataList.length > 0 && !isSearching">
<SectionWidgets
v-for="{ widgets, node } in advancedWidgetsSectionDataList"
:key="`advanced-${node.id}`"
:collapse="true"
:node
:label="advancedLabel"
:widgets
:show-locate-button="isMultipleNodesSelected"
class="border-b border-interface-stroke"
/>
</template>
</template>

View File

@@ -123,8 +123,7 @@ export const useContextMenuTranslation = () => {
}
// for capture translation text of input and widget
const extraInfo = (options.extra ||
options.parentMenu?.options?.extra) as
const extraInfo = (options.extra || options.parentMenu?.options?.extra) as
| { inputs?: INodeInputSlot[]; widgets?: IWidget[] }
| undefined
// widgets and inputs

View File

@@ -1,5 +1,6 @@
import { computed, reactive, readonly } from 'vue'
import { isCloud } from '@/platform/distribution/types'
import { remoteConfig } from '@/platform/remoteConfig/remoteConfig'
import { api } from '@/scripts/api'
@@ -95,6 +96,8 @@ export function useFeatureFlags() {
)
},
get teamWorkspacesEnabled() {
if (!isCloud) return false
return (
remoteConfig.value.team_workspaces_enabled ??
api.getServerFeature(ServerFeatureFlag.TEAM_WORKSPACES_ENABLED, false)

View File

@@ -1350,12 +1350,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
})
function inner_clicked(
this: ContextMenu<string>,
this: ContextMenuDivElement,
v?: string | IContextMenuValue<string>
) {
if (!node || typeof v === 'string' || !v?.value) return
const rect = this.root.getBoundingClientRect()
const rect = this.getBoundingClientRect()
canvas.showEditPropertyValue(node, v.value, {
position: [rect.left, rect.top]
})

View File

@@ -158,6 +158,7 @@
"choose_file_to_upload": "choose file to upload",
"capture": "capture",
"nodes": "Nodes",
"nodesCount": "{count} nodes | {count} node | {count} nodes",
"community": "Community",
"all": "All",
"versionMismatchWarning": "Version Compatibility Warning",

View File

@@ -1,40 +1,42 @@
<template>
<div class="flex gap-3">
<div class="flex gap-3 items-center">
<SearchBox
:model-value="searchQuery"
:placeholder="$t('sideToolbar.searchAssets') + '...'"
@update:model-value="handleSearchChange"
/>
<MediaAssetFilterButton
v-if="isCloud"
v-tooltip.top="{ value: $t('assetBrowser.filterBy') }"
size="md"
>
<template #default="{ close }">
<MediaAssetFilterMenu
:media-type-filters="mediaTypeFilters"
:close="close"
@update:media-type-filters="handleMediaTypeFiltersChange"
/>
</template>
</MediaAssetFilterButton>
<AssetSortButton
v-if="isCloud"
v-tooltip.top="{ value: $t('assetBrowser.sortBy') }"
size="md"
>
<template #default="{ close }">
<MediaAssetSortMenu
v-model:sort-by="sortBy"
:show-generation-time-sort
:close="close"
/>
</template>
</AssetSortButton>
<MediaAssetViewModeToggle
v-if="isQueuePanelV2Enabled"
v-model:view-mode="viewMode"
/>
<div class="flex gap-1.5 items-center">
<MediaAssetFilterButton
v-if="isCloud"
v-tooltip.top="{ value: $t('assetBrowser.filterBy') }"
size="md"
>
<template #default="{ close }">
<MediaAssetFilterMenu
:media-type-filters
:close
@update:media-type-filters="handleMediaTypeFiltersChange"
/>
</template>
</MediaAssetFilterButton>
<AssetSortButton
v-if="isCloud"
v-tooltip.top="{ value: $t('assetBrowser.sortBy') }"
size="md"
>
<template #default="{ close }">
<MediaAssetSortMenu
v-model:sort-by="sortBy"
:show-generation-time-sort
:close
/>
</template>
</AssetSortButton>
<MediaAssetViewModeToggle
v-if="isQueuePanelV2Enabled"
v-model:view-mode="viewMode"
/>
</div>
</div>
</template>

View File

@@ -9,6 +9,7 @@ type Distribution = 'desktop' | 'localhost' | 'cloud'
declare global {
const __DISTRIBUTION__: Distribution
const __IS_NIGHTLY__: boolean
}
/** Current distribution - replaced at compile time */
@@ -18,3 +19,10 @@ const DISTRIBUTION: Distribution = __DISTRIBUTION__
export const isDesktop = DISTRIBUTION === 'desktop' || isElectron() // TODO: replace with build var
export const isCloud = DISTRIBUTION === 'cloud'
// export const isLocalhost = DISTRIBUTION === 'localhost' || (!isDesktop && !isCloud)
/**
* Whether this is a nightly build (from main branch).
* Nightly builds may show experimental features and surveys.
* @public
*/
export const isNightly = __IS_NIGHTLY__

View File

@@ -0,0 +1,131 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const STORAGE_KEY = 'Comfy.FeatureUsage'
describe('useFeatureUsageTracker', () => {
beforeEach(() => {
localStorage.clear()
vi.resetModules()
})
afterEach(() => {
localStorage.clear()
})
it('initializes with zero count for new feature', async () => {
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
const { useCount } = useFeatureUsageTracker('test-feature')
expect(useCount.value).toBe(0)
})
it('increments count on trackUsage', async () => {
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
const { useCount, trackUsage } = useFeatureUsageTracker('test-feature')
expect(useCount.value).toBe(0)
trackUsage()
expect(useCount.value).toBe(1)
trackUsage()
expect(useCount.value).toBe(2)
})
it('sets firstUsed only on first use', async () => {
vi.useFakeTimers()
const firstTs = 1000000
vi.setSystemTime(firstTs)
try {
const { useFeatureUsageTracker } =
await import('./useFeatureUsageTracker')
const { usage, trackUsage } = useFeatureUsageTracker('test-feature')
trackUsage()
expect(usage.value?.firstUsed).toBe(firstTs)
vi.setSystemTime(firstTs + 5000)
trackUsage()
expect(usage.value?.firstUsed).toBe(firstTs)
} finally {
vi.useRealTimers()
}
})
it('updates lastUsed on each use', async () => {
vi.useFakeTimers()
try {
const { useFeatureUsageTracker } =
await import('./useFeatureUsageTracker')
const { usage, trackUsage } = useFeatureUsageTracker('test-feature')
trackUsage()
const firstLastUsed = usage.value?.lastUsed ?? 0
vi.advanceTimersByTime(10)
trackUsage()
expect(usage.value?.lastUsed).toBeGreaterThan(firstLastUsed)
} finally {
vi.useRealTimers()
}
})
it('reset clears feature data', async () => {
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
const { useCount, trackUsage, reset } =
useFeatureUsageTracker('test-feature')
trackUsage()
trackUsage()
expect(useCount.value).toBe(2)
reset()
expect(useCount.value).toBe(0)
})
it('tracks multiple features independently', async () => {
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
const featureA = useFeatureUsageTracker('feature-a')
const featureB = useFeatureUsageTracker('feature-b')
featureA.trackUsage()
featureA.trackUsage()
featureB.trackUsage()
expect(featureA.useCount.value).toBe(2)
expect(featureB.useCount.value).toBe(1)
})
it('persists to localStorage', async () => {
vi.useFakeTimers()
try {
const { useFeatureUsageTracker } =
await import('./useFeatureUsageTracker')
const { trackUsage } = useFeatureUsageTracker('persisted-feature')
trackUsage()
await vi.runAllTimersAsync()
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}')
expect(stored['persisted-feature']?.useCount).toBe(1)
} finally {
vi.useRealTimers()
}
})
it('loads existing data from localStorage', async () => {
localStorage.setItem(
STORAGE_KEY,
JSON.stringify({
'existing-feature': { useCount: 5, firstUsed: 1000, lastUsed: 2000 }
})
)
vi.resetModules()
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
const { useCount } = useFeatureUsageTracker('existing-feature')
expect(useCount.value).toBe(5)
})
})

View File

@@ -0,0 +1,46 @@
import { useStorage } from '@vueuse/core'
import { computed } from 'vue'
interface FeatureUsage {
useCount: number
firstUsed: number
lastUsed: number
}
type FeatureUsageRecord = Record<string, FeatureUsage>
const STORAGE_KEY = 'Comfy.FeatureUsage'
/**
* Tracks feature usage for survey eligibility.
* Persists to localStorage.
*/
export function useFeatureUsageTracker(featureId: string) {
const usageData = useStorage<FeatureUsageRecord>(STORAGE_KEY, {})
const usage = computed(() => usageData.value[featureId])
const useCount = computed(() => usage.value?.useCount ?? 0)
function trackUsage() {
const now = Date.now()
const existing = usageData.value[featureId]
usageData.value[featureId] = {
useCount: (existing?.useCount ?? 0) + 1,
firstUsed: existing?.firstUsed ?? now,
lastUsed: now
}
}
function reset() {
delete usageData.value[featureId]
}
return {
usage,
useCount,
trackUsage,
reset
}
}

View File

@@ -482,18 +482,30 @@ const lgraphNode = computed(() => {
const showAdvancedInputsButton = computed(() => {
const node = lgraphNode.value
if (!node || !(node instanceof SubgraphNode)) return false
if (!node) return false
// Check if there are hidden inputs (widgets not promoted)
const interiorNodes = node.subgraph.nodes
const allInteriorWidgets = interiorNodes.flatMap((n) => n.widgets ?? [])
// For subgraph nodes: check for unpromoted widgets
if (node instanceof SubgraphNode) {
const interiorNodes = node.subgraph.nodes
const allInteriorWidgets = interiorNodes.flatMap((n) => n.widgets ?? [])
return allInteriorWidgets.some((w) => !w.computedDisabled && !w.promoted)
}
return allInteriorWidgets.some((w) => !w.computedDisabled && !w.promoted)
// For regular nodes: show button if there are advanced widgets and they're currently hidden
const hasAdvancedWidgets = nodeData.widgets?.some((w) => w.options?.advanced)
return hasAdvancedWidgets && !node.showAdvanced
})
function handleShowAdvancedInputs() {
const rightSidePanelStore = useRightSidePanelStore()
rightSidePanelStore.focusSection('advanced-inputs')
const node = lgraphNode.value
if (!node) return
if (node instanceof SubgraphNode) {
const rightSidePanelStore = useRightSidePanelStore()
rightSidePanelStore.focusSection('advanced-inputs')
} else {
node.showAdvanced = true
}
}
const nodeMedia = computed(() => {

View File

@@ -1,7 +1,6 @@
<script setup lang="ts">
import Popover from 'primevue/popover'
import RadioButton from 'primevue/radiobutton'
import { computed, ref } from 'vue'
import { computed } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { ControlOptions } from '@/types/simplifiedWidget'
@@ -14,14 +13,8 @@ type ControlOption = {
title: string
}
const popover = ref()
const settingStore = useSettingStore()
const toggle = (event: Event) => {
popover.value.toggle(event)
}
defineExpose({ toggle })
const controlOptions: ControlOption[] = [
{
mode: 'fixed',
@@ -57,70 +50,63 @@ const controlMode = defineModel<ControlOptions>()
</script>
<template>
<Popover
ref="popover"
class="bg-interface-panel-surface border border-interface-stroke rounded-lg"
>
<div class="w-113 max-w-md p-4 space-y-4">
<div class="text-sm text-muted-foreground leading-tight">
{{ $t('widgets.valueControl.header.prefix') }}
<span class="text-base-foreground font-medium">
{{
widgetControlMode === 'before'
? $t('widgets.valueControl.header.before')
: $t('widgets.valueControl.header.after')
}}
</span>
{{ $t('widgets.valueControl.header.postfix') }}
</div>
<div class="w-113 max-w-md p-4 space-y-4">
<div class="text-sm text-muted-foreground leading-tight">
{{ $t('widgets.valueControl.header.prefix') }}
<span class="text-base-foreground font-medium">
{{
widgetControlMode === 'before'
? $t('widgets.valueControl.header.before')
: $t('widgets.valueControl.header.after')
}}
</span>
{{ $t('widgets.valueControl.header.postfix') }}
</div>
<div class="space-y-2">
<div
v-for="option in controlOptions"
:key="option.mode"
class="flex items-center justify-between py-2 gap-7"
>
<div class="flex items-center gap-2 flex-1 min-w-0">
<div
class="flex items-center justify-center w-8 h-8 rounded-lg flex-shrink-0 bg-secondary-background border border-border-subtle"
<div class="space-y-2">
<div
v-for="option in controlOptions"
:key="option.mode"
class="flex items-center justify-between py-2 gap-7"
>
<div class="flex items-center gap-2 flex-1 min-w-0">
<div
class="flex items-center justify-center w-8 h-8 rounded-lg flex-shrink-0 bg-secondary-background border border-border-subtle"
>
<i
v-if="option.icon"
:class="option.icon"
class="text-base text-base-foreground"
/>
<span
v-if="option.text"
class="text-xs font-normal text-base-foreground"
>
<i
v-if="option.icon"
:class="option.icon"
class="text-base text-base-foreground"
/>
<span
v-if="option.text"
class="text-xs font-normal text-base-foreground"
>
{{ option.text }}
</span>
</div>
<div class="flex flex-col gap-0.5 min-w-0 flex-1">
<div
class="text-sm font-normal text-base-foreground leading-tight"
>
<span>
{{ $t(`widgets.valueControl.${option.title}`) }}
</span>
</div>
<div
class="text-sm font-normal text-muted-foreground leading-tight"
>
{{ $t(`widgets.valueControl.${option.description}`) }}
</div>
</div>
{{ option.text }}
</span>
</div>
<RadioButton
v-model="controlMode"
class="flex-shrink-0"
:input-id="option.mode"
:value="option.mode"
/>
<div class="flex flex-col gap-0.5 min-w-0 flex-1">
<div class="text-sm font-normal text-base-foreground leading-tight">
<span>
{{ $t(`widgets.valueControl.${option.title}`) }}
</span>
</div>
<div
class="text-sm font-normal text-muted-foreground leading-tight"
>
{{ $t(`widgets.valueControl.${option.description}`) }}
</div>
</div>
</div>
<RadioButton
v-model="controlMode"
class="flex-shrink-0"
:input-id="option.mode"
:value="option.mode"
/>
</div>
</div>
</Popover>
</div>
</template>

View File

@@ -24,13 +24,5 @@ onMounted(() => {
})
</script>
<template>
<div
ref="domEl"
@pointerdown.stop
@pointermove.stop
@pointerup.stop
@mousedown.stop
@mousemove.stop
@mouseup.stop
/>
<div ref="domEl" @pointerdown.stop @pointermove.stop @pointerup.stop />
</template>

View File

@@ -3,6 +3,7 @@ import { onClickOutside } from '@vueuse/core'
import { computed, ref, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { evaluateInput } from '@/lib/litegraph/src/utils/widget'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import { cn } from '@/utils/tailwindUtil'
@@ -65,7 +66,6 @@ function updateValue(e: UIEvent) {
textEdit.value = false
}
const sharedButtonClass = 'w-8 bg-transparent border-0 text-sm text-smoke-700'
const canDecrement = computed(
() =>
modelValue.value > filteredProps.value.min &&
@@ -205,16 +205,17 @@ const sliderWidth = computed(() => {
class="bg-primary-background/15 absolute left-0 bottom-0 h-full rounded-lg pointer-events-none"
:style="{ width: `${sliderWidth}%` }"
/>
<button
<Button
v-if="!buttonsDisabled"
data-testid="decrement"
:class="
cn(sharedButtonClass, 'pi pi-minus', !canDecrement && 'opacity-60')
"
class="h-full w-8 rounded-r-none hover:bg-base-foreground/20 disabled:opacity-30"
variant="muted-textonly"
:disabled="!canDecrement"
tabindex="-1"
@click="modelValue -= stepValue"
/>
>
<i class="pi pi-minus" />
</Button>
<div class="relative min-w-[4ch] flex-1 py-1.5 my-0.25">
<input
ref="inputField"
@@ -262,16 +263,17 @@ const sliderWidth = computed(() => {
</div>
<slot />
<button
<Button
v-if="!buttonsDisabled"
data-testid="increment"
:class="
cn(sharedButtonClass, 'pi pi-plus', !canIncrement && 'opacity-60')
"
class="h-full w-8 rounded-l-none hover:bg-base-foreground/20 disabled:opacity-30"
variant="muted-textonly"
:disabled="!canIncrement"
tabindex="-1"
@click="modelValue += stepValue"
/>
>
<i class="pi pi-plus" />
</Button>
</div>
</WidgetLayoutField>
</template>

View File

@@ -2,6 +2,7 @@
import { computed, defineAsyncComponent, ref, watch } from 'vue'
import type { Component } from 'vue'
import Popover from '@/components/ui/Popover.vue'
import Button from '@/components/ui/button/Button.vue'
import type {
SimplifiedControlWidget,
@@ -19,8 +20,6 @@ const props = defineProps<{
const modelValue = defineModel<T>()
const popover = ref()
const controlModel = ref(props.widget.controlWidget.value)
const controlButtonIcon = computed(() => {
@@ -37,24 +36,24 @@ const controlButtonIcon = computed(() => {
})
watch(controlModel, props.widget.controlWidget.update)
const togglePopover = (event: Event) => {
popover.value.toggle(event)
}
</script>
<template>
<div class="relative grid grid-cols-subgrid">
<component :is="component" v-bind="$attrs" v-model="modelValue" :widget>
<Button
variant="textonly"
size="sm"
class="h-4 w-7 self-center rounded-xl bg-blue-100/30 p-0"
@click.stop.prevent="togglePopover"
>
<i :class="`${controlButtonIcon} text-blue-100 text-xs size-3.5`" />
</Button>
<Popover>
<template #button>
<Button
variant="textonly"
size="sm"
class="h-4 w-7 p-0 self-center rounded-xl bg-primary-background/30 hover:bg-primary-background-hover/30"
>
<i
:class="`${controlButtonIcon} text-primary-background text-xs w-full`"
/>
</Button>
</template>
<ValueControlPopover v-model="controlModel" />
</Popover>
</component>
<ValueControlPopover ref="popover" v-model="controlModel" />
</div>
</template>

View File

@@ -10,15 +10,22 @@ import type {
RegistryPack
} from '@/workbench/extensions/manager/types/comfyManagerTypes'
const translateMock = vi.hoisted(() =>
vi.fn((key: string, choice?: number) =>
typeof choice === 'number' ? `${key}-${choice}` : key
)
)
const dateMock = vi.hoisted(() => vi.fn(() => '2024. 1. 1.'))
// Mock dependencies
vi.mock('vue-i18n', () => ({
useI18n: vi.fn(() => ({
d: vi.fn(() => '2024. 1. 1.'),
t: vi.fn((key: string) => key)
d: dateMock,
t: translateMock
})),
createI18n: vi.fn(() => ({
global: {
t: vi.fn((key: string) => key),
t: translateMock,
te: vi.fn(() => true)
}
}))
@@ -187,6 +194,18 @@ describe('PackCard', () => {
// Should still render without errors
expect(wrapper.exists()).toBe(true)
})
it('should use localized singular/plural nodes label', () => {
const packWithNodes = {
...mockNodePack,
comfy_nodes: ['node-a']
} as MergedNodePack
const wrapper = createWrapper({ nodePack: packWithNodes })
expect(wrapper.text()).toContain('g.nodesCount-1')
expect(translateMock).toHaveBeenCalledWith('g.nodesCount', 1)
})
})
describe('component structure', () => {

View File

@@ -36,8 +36,8 @@
</p>
<div class="flex flex-col gap-y-2">
<div class="flex flex-1 items-center gap-2">
<div v-if="nodesCount" class="p-2 pl-0 text-xs">
{{ nodesCount }} {{ $t('g.nodes') }}
<div v-if="nodesLabel" class="p-2 pl-0 text-xs">
{{ nodesLabel }}
</div>
<PackVersionBadge
:node-pack="nodePack"
@@ -94,7 +94,7 @@ const { nodePack, isSelected = false } = defineProps<{
isSelected?: boolean
}>()
const { d } = useI18n()
const { d, t } = useI18n()
const colorPaletteStore = useColorPaletteStore()
const isLightTheme = computed(
@@ -115,6 +115,9 @@ const isDisabled = computed(
const nodesCount = computed(() =>
isMergedNodePack(nodePack) ? nodePack.comfy_nodes?.length : undefined
)
const nodesLabel = computed(() =>
nodesCount.value ? t('g.nodesCount', nodesCount.value) : ''
)
const publisherName = computed(() => {
if (!nodePack) return null

View File

@@ -51,6 +51,14 @@ const DISTRIBUTION: 'desktop' | 'localhost' | 'cloud' =
? 'cloud'
: 'localhost'
// Nightly builds are from main branch; RC/stable builds are from core/* branches
// Can be overridden via IS_NIGHTLY env var for testing
const IS_NIGHTLY =
process.env.IS_NIGHTLY === 'true' ||
(process.env.IS_NIGHTLY !== 'false' &&
process.env.CI === 'true' &&
process.env.GITHUB_REF_NAME === 'main')
// Disable Vue DevTools for production cloud distribution
const DISABLE_VUE_PLUGINS =
process.env.DISABLE_VUE_PLUGINS === 'true' ||
@@ -502,7 +510,8 @@ export default defineConfig({
__ALGOLIA_APP_ID__: JSON.stringify(process.env.ALGOLIA_APP_ID || ''),
__ALGOLIA_API_KEY__: JSON.stringify(process.env.ALGOLIA_API_KEY || ''),
__USE_PROD_CONFIG__: process.env.USE_PROD_CONFIG === 'true',
__DISTRIBUTION__: JSON.stringify(DISTRIBUTION)
__DISTRIBUTION__: JSON.stringify(DISTRIBUTION),
__IS_NIGHTLY__: JSON.stringify(IS_NIGHTLY)
},
resolve: {
@@ -536,7 +545,8 @@ export default defineConfig({
'**/dist/**',
'**/cypress/**',
'**/.{idea,git,cache,output,temp}/**',
'**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*'
'**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint}.config.*',
'**/.{oxlintrc,oxfmtrc}.json'
],
silent: 'passed-only'
}

View File

@@ -46,6 +46,7 @@ globalThis.__ALGOLIA_APP_ID__ = ''
globalThis.__ALGOLIA_API_KEY__ = ''
globalThis.__USE_PROD_CONFIG__ = false
globalThis.__DISTRIBUTION__ = 'localhost'
globalThis.__IS_NIGHTLY__ = false
// Define runtime config for tests
window.__CONFIG__ = {