mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-23 15:59:47 +00:00
Compare commits
41 Commits
v1.39.3
...
drjkl/plus
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a05a18e3c0 | ||
|
|
9faabd80e2 | ||
|
|
09d63c5ba3 | ||
|
|
b2c8ea3e50 | ||
|
|
f9af2cc4bd | ||
|
|
a3fba58c79 | ||
|
|
ae7728cd91 | ||
|
|
5847071bc1 | ||
|
|
cc10684c19 | ||
|
|
380db3ee10 | ||
|
|
cbdc7d030f | ||
|
|
21492ecca5 | ||
|
|
22755d2cb2 | ||
|
|
15f4f11ee2 | ||
|
|
bc19bb60fb | ||
|
|
139ee32d78 | ||
|
|
7928e8797d | ||
|
|
cd6cc7a51d | ||
|
|
aa979aa98c | ||
|
|
cc0307032a | ||
|
|
bc3b9585f9 | ||
|
|
1af9e00dd2 | ||
|
|
2f8bd7b04f | ||
|
|
cfdd002b7c | ||
|
|
7dd3098af5 | ||
|
|
2740c7cdd5 | ||
|
|
22daf48748 | ||
|
|
2ec954459c | ||
|
|
4e9f581625 | ||
|
|
d34d6a8a92 | ||
|
|
2a167a675d | ||
|
|
e1385b0d18 | ||
|
|
eaa3ff1579 | ||
|
|
4e20b7522b | ||
|
|
544ef5bb70 | ||
|
|
69129414d0 | ||
|
|
1bbbcfedf0 | ||
|
|
4f2872460c | ||
|
|
ea6928268f | ||
|
|
4a85bffb1f | ||
|
|
be7c34e28b |
@@ -391,7 +391,7 @@ echo "Last stable release: $LAST_STABLE"
|
||||
|
||||
```bash
|
||||
# Trigger the workflow
|
||||
gh workflow run version-bump.yaml -f version_type=${VERSION_TYPE}
|
||||
gh workflow run release-version-bump.yaml -f version_type=${VERSION_TYPE}
|
||||
|
||||
# Workflow runs quickly - usually creates PR within 30 seconds
|
||||
echo "Workflow triggered. Waiting for PR creation..."
|
||||
@@ -443,28 +443,21 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
gh pr view ${PR_NUMBER} --json labels | jq -r '.labels[].name' | grep -q "Release" || \
|
||||
echo "ERROR: Release label missing! Add it immediately!"
|
||||
```
|
||||
2. Check for update-locales commits:
|
||||
```bash
|
||||
# WARNING: update-locales may add [skip ci] which blocks release workflow!
|
||||
gh pr view ${PR_NUMBER} --json commits | grep -q "skip ci" && \
|
||||
echo "WARNING: [skip ci] detected - release workflow may not trigger!"
|
||||
```
|
||||
3. Verify version number in package.json
|
||||
4. Review all changed files
|
||||
5. Ensure no unintended changes included
|
||||
6. Wait for required PR checks:
|
||||
2. Verify version number in package.json
|
||||
3. Review all changed files
|
||||
4. Ensure no unintended changes included
|
||||
5. Wait for required PR checks:
|
||||
```bash
|
||||
gh pr checks ${PR_NUMBER} --watch
|
||||
```
|
||||
7. **FINAL CODE REVIEW**: Release label present and no [skip ci]?
|
||||
6. **FINAL CODE REVIEW**: Release label present and no [skip ci]?
|
||||
|
||||
### Step 12: Pre-Merge Validation
|
||||
|
||||
1. **Review Requirements**: Release PRs require approval
|
||||
2. Monitor CI checks - watch for update-locales
|
||||
3. **CRITICAL WARNING**: If update-locales adds [skip ci], the release workflow won't trigger!
|
||||
4. Check no new commits to main since PR creation
|
||||
5. **DEPLOYMENT READINESS**: Ready to merge?
|
||||
2. Monitor CI checks
|
||||
3. Check no new commits to main since PR creation
|
||||
4. **DEPLOYMENT READINESS**: Ready to merge?
|
||||
|
||||
### Step 13: Execute Release
|
||||
|
||||
|
||||
3
.coderabbit.yaml
Normal file
3
.coderabbit.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
issue_enrichment:
|
||||
auto_enrich:
|
||||
enabled: true
|
||||
5
.github/ISSUE_TEMPLATE/bug-report.yaml
vendored
5
.github/ISSUE_TEMPLATE/bug-report.yaml
vendored
@@ -10,10 +10,7 @@ body:
|
||||
options:
|
||||
- label: I am running the latest version of ComfyUI
|
||||
required: true
|
||||
- label: I have searched existing issues to make sure this isn't a duplicate
|
||||
required: true
|
||||
- label: I have tested with all custom nodes disabled ([see how](https://docs.comfy.org/troubleshooting/custom-node-issues#step-1%3A-test-with-all-custom-nodes-disabled))
|
||||
required: true
|
||||
- label: I have custom nodes enabled
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/feature-request.yaml
vendored
7
.github/ISSUE_TEMPLATE/feature-request.yaml
vendored
@@ -4,13 +4,6 @@ labels: []
|
||||
type: Feature
|
||||
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the problem you're experiencing, and that it's not addressed in a recent build/commit.
|
||||
options:
|
||||
- label: I have searched the existing issues and checked the recent builds/commits
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
|
||||
@@ -173,6 +173,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.setup.outputs.branch }}
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
|
||||
# Download all changed snapshot files from shards
|
||||
- name: Download snapshot artifacts
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -96,3 +96,5 @@ vitest.config.*.timestamp*
|
||||
|
||||
# Weekly docs check output
|
||||
/output.txt
|
||||
|
||||
# .vite-plus*.tmp.json
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 80,
|
||||
"ignorePatterns": [
|
||||
"packages/registry-types/src/comfyRegistryTypes.ts",
|
||||
"public/materialdesignicons.min.css",
|
||||
"src/types/generatedManagerTypes.ts",
|
||||
"**/__fixtures__/**/*.json"
|
||||
]
|
||||
}
|
||||
115
.oxlintrc.json
115
.oxlintrc.json
@@ -1,115 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"ignorePatterns": [
|
||||
".i18nrc.cjs",
|
||||
".nx/*",
|
||||
"**/vite.config.*.timestamp*",
|
||||
"**/vitest.config.*.timestamp*",
|
||||
"components.d.ts",
|
||||
"coverage/*",
|
||||
"dist/*",
|
||||
"packages/registry-types/src/comfyRegistryTypes.ts",
|
||||
"playwright-report/*",
|
||||
"src/extensions/core/*",
|
||||
"src/scripts/*",
|
||||
"src/types/generatedManagerTypes.ts",
|
||||
"src/types/vue-shim.d.ts",
|
||||
"test-results/*",
|
||||
"vitest.setup.ts"
|
||||
],
|
||||
"plugins": [
|
||||
"eslint",
|
||||
"import",
|
||||
"oxc",
|
||||
"typescript",
|
||||
"unicorn",
|
||||
"vitest",
|
||||
"vue"
|
||||
],
|
||||
"rules": {
|
||||
"no-async-promise-executor": "off",
|
||||
"no-console": [
|
||||
"error",
|
||||
{
|
||||
"allow": ["warn", "error"]
|
||||
}
|
||||
],
|
||||
"no-control-regex": "off",
|
||||
"no-eval": "off",
|
||||
"no-redeclare": "error",
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"paths": [
|
||||
{
|
||||
"name": "primevue/calendar",
|
||||
"message": "Calendar is deprecated in PrimeVue 4+. Use DatePicker instead: import DatePicker from 'primevue/datepicker'"
|
||||
},
|
||||
{
|
||||
"name": "primevue/dropdown",
|
||||
"message": "Dropdown is deprecated in PrimeVue 4+. Use Select instead: import Select from 'primevue/select'"
|
||||
},
|
||||
{
|
||||
"name": "primevue/inputswitch",
|
||||
"message": "InputSwitch is deprecated in PrimeVue 4+. Use ToggleSwitch instead: import ToggleSwitch from 'primevue/toggleswitch'"
|
||||
},
|
||||
{
|
||||
"name": "primevue/overlaypanel",
|
||||
"message": "OverlayPanel is deprecated in PrimeVue 4+. Use Popover instead: import Popover from 'primevue/popover'"
|
||||
},
|
||||
{
|
||||
"name": "primevue/sidebar",
|
||||
"message": "Sidebar is deprecated in PrimeVue 4+. Use Drawer instead: import Drawer from 'primevue/drawer'"
|
||||
},
|
||||
{
|
||||
"name": "@/i18n--to-enable",
|
||||
"importNames": ["st", "t", "te", "d"],
|
||||
"message": "Don't import `@/i18n` directly, prefer `useI18n()`"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-self-assign": "allow",
|
||||
"no-unused-expressions": "off",
|
||||
"no-unused-private-class-members": "off",
|
||||
"no-useless-rename": "off",
|
||||
"import/default": "error",
|
||||
"import/export": "error",
|
||||
"import/namespace": "error",
|
||||
"import/no-duplicates": "error",
|
||||
"import/consistent-type-specifier-style": ["error", "prefer-top-level"],
|
||||
"jest/expect-expect": "off",
|
||||
"jest/no-conditional-expect": "off",
|
||||
"jest/no-disabled-tests": "off",
|
||||
"jest/no-standalone-expect": "off",
|
||||
"jest/valid-title": "off",
|
||||
"typescript/no-this-alias": "off",
|
||||
"typescript/no-unnecessary-parameter-property-assignment": "off",
|
||||
"typescript/no-unsafe-declaration-merging": "off",
|
||||
"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",
|
||||
"vue/no-import-compiler-macros": "error",
|
||||
"vue/no-dupe-keys": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.{stories,test,spec}.ts", "**/*.stories.vue"],
|
||||
"rules": {
|
||||
"no-console": "allow"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -98,12 +98,10 @@ const config: StorybookConfig = {
|
||||
},
|
||||
build: {
|
||||
rolldownOptions: {
|
||||
experimental: {
|
||||
strictExecutionOrder: true
|
||||
},
|
||||
treeshake: false,
|
||||
output: {
|
||||
keepNames: true
|
||||
keepNames: true,
|
||||
strictExecutionOrder: true
|
||||
},
|
||||
onwarn: (warning, warn) => {
|
||||
// Suppress specific warnings
|
||||
|
||||
59
AGENTS.md
59
AGENTS.md
@@ -21,7 +21,7 @@ See @docs/guidance/\*.md for file-type-specific conventions (auto-loaded by glob
|
||||
- i18n: `src/i18n.ts`,
|
||||
- Entry Point: `src/main.ts`.
|
||||
- Tests:
|
||||
- unit/component in `tests-ui/` and `src/**/*.test.ts`
|
||||
- unit/component in `src/**/*.test.ts`
|
||||
- E2E (Playwright) in `browser_tests/**/*.spec.ts`
|
||||
- Public assets: `public/`
|
||||
- Build output: `dist/`
|
||||
@@ -264,7 +264,7 @@ A particular type of complexity is over-engineering, where developers have made
|
||||
|
||||
## Repository Navigation
|
||||
|
||||
- Check README files in key folders (tests-ui, browser_tests, composables, etc.)
|
||||
- Check README files in key folders (browser_tests, composables, etc.)
|
||||
- Prefer running single tests for performance
|
||||
- Use --help for unfamiliar CLI tools
|
||||
|
||||
@@ -312,3 +312,58 @@ When using `take_snapshot` to inspect dropdowns, listboxes, or other components
|
||||
- Put scripts used under `/temp/scripts/`
|
||||
- Put summaries of work performed under `/temp/summaries/`
|
||||
- Put TODOs and status updates under `/temp/in_progress/`
|
||||
|
||||
<!--VITE PLUS START-->
|
||||
|
||||
# Using Vite+, the Unified Toolchain for the Web
|
||||
|
||||
This project is using Vite+, a modern toolchain built on top of Vite, Rolldown, Vitest, tsdown, Oxlint, and Oxfmt. Vite+ wraps these tools and package manager commands in a single, global CLI called `vite`. Vite+ is distinct from Vite, but it invokes Vite through `vite dev` and `vite build`.
|
||||
|
||||
## Vite+ Workflow
|
||||
|
||||
`vite` is a global binary that handles the full development lifecycle. Run `vite help` to print a list of commands and `vite <command> --help` for information about a specific command.
|
||||
|
||||
### Vite+ Commands
|
||||
|
||||
- dev - Run the development server
|
||||
- build - Build for production
|
||||
- lint - Lint code
|
||||
- test - Run tests
|
||||
- fmt - Format code
|
||||
- lib - Build library
|
||||
- migrate - Migrate an existing project to Vite+
|
||||
- new - Create a new monorepo package (in-project) or a new project (global)
|
||||
- run - Run tasks from `package.json` scripts
|
||||
|
||||
These commands map to their corresponding tools. For example, `vite dev --port 3000` runs Vite's dev server and works the same as Vite. `vite test` runs JavaScript tests through the bundled Vitest. The version of all tools can be checked using `vite --version`. This is useful when researching documentation, features, and bugs.
|
||||
|
||||
### Package Manager Commands
|
||||
|
||||
Vite+ automatically detects and wraps the underlying package manager such as pnpm, npm, or Yarn through the `packageManager` field in `package.json` or package manager-specific lockfiles.
|
||||
|
||||
- install - Install all dependencies, or add packages if package names are provided
|
||||
- add - Add packages to dependencies
|
||||
- remove - Remove packages from dependencies
|
||||
- dlx - Execute a package binary without installing it as a dependency
|
||||
- info - View package information from the registry, including latest versions
|
||||
- link - Link packages for local development
|
||||
- outdated - Check for outdated packages
|
||||
- pm - Forward a command to the package manager
|
||||
- unlink - Unlink packages
|
||||
- update - Update packages to their latest versions
|
||||
- why - Show why a package is installed
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Using the package manager directly:** Do not use pnpm, npm, or Yarn directly. Vite+ can handle all package manager operations.
|
||||
- **Always use Vite commands to run tools:** Don't attempt to run `vite vitest` or `vite oxlint`. They do not exist. Use `vite test` and `vite lint` instead.
|
||||
- **Running scripts:** Vite+ commands take precedence over `package.json` scripts. If there is a `test` script defined in `scripts` that conflicts with the built-in `vite test` command, run it using `vite run test`.
|
||||
- **Do not install Vitest, Oxlint, Oxfmt, or tsdown directly:** Vite+ wraps these tools. They must not be installed directly. You cannot upgrade these tools by installing their latest versions. Always use Vite+ commands.
|
||||
- **Import JavaScript modules from `vite-plus`:** Instead of importing from `vite` or `vitest`, all modules should be imported from the project's `vite-plus` dependency. For example, `import { defineConfig } from 'vite-plus';` or `import { expect, test, vi } from 'vite-plus/test';`. You must not install `vitest` to import test utilities.
|
||||
- **Type-Aware Linting:** There is no need to install `oxlint-tsgolint`, `vite lint --type-aware` works out of the box.
|
||||
|
||||
## Review Checklist for Agents
|
||||
|
||||
- [ ] Run `vite install` after pulling remote changes and before getting started.
|
||||
- [ ] Run `vite lint`, `vite fmt`, and `vite test` to validate changes.
|
||||
<!--VITE PLUS END-->
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"vite": "catalog:",
|
||||
"vite-plugin-html": "catalog:",
|
||||
"vite-plugin-vue-devtools": "catalog:",
|
||||
"vite-plus": "catalog:",
|
||||
"vue-tsc": "catalog:"
|
||||
},
|
||||
"nx": {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { FileSystemIconLoader } from 'unplugin-icons/loaders'
|
||||
import IconsResolver from 'unplugin-icons/resolver'
|
||||
import Icons from 'unplugin-icons/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
import { defineConfig } from 'vite-plus'
|
||||
import { createHtmlPlugin } from 'vite-plugin-html'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
|
||||
BIN
browser_tests/assets/workflowInMedia/workflow_itxt.png
Normal file
BIN
browser_tests/assets/workflowInMedia/workflow_itxt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 517 B |
@@ -16,6 +16,7 @@ test.describe(
|
||||
'no_workflow.webp',
|
||||
'large_workflow.webp',
|
||||
'workflow_prompt_parameters.png',
|
||||
'workflow_itxt.png',
|
||||
'workflow.webm',
|
||||
// Skipped due to 3d widget unstable visual result.
|
||||
// 3d widget shows grid after fully loaded.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
type ComfyPage,
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '../../../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Vue Multiline String Widget', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from 'path'
|
||||
import type { Plugin } from 'vite'
|
||||
import type { Plugin } from 'vite-plus'
|
||||
|
||||
interface ShimResult {
|
||||
code: string
|
||||
@@ -9,7 +9,11 @@ interface ShimResult {
|
||||
const SKIP_WARNING_FILES = new Set(['scripts/app', 'scripts/api'])
|
||||
|
||||
/** Files that will be removed in v1.34 */
|
||||
const DEPRECATED_FILES = ['scripts/ui', 'extensions/core/groupNode'] as const
|
||||
const DEPRECATED_FILES = [
|
||||
'scripts/ui',
|
||||
'extensions/core/groupNode',
|
||||
'extensions/core/nodeTemplates'
|
||||
] as const
|
||||
|
||||
function getWarningMessage(
|
||||
fileKey: string,
|
||||
|
||||
@@ -8,7 +8,7 @@ export default {
|
||||
|
||||
'./**/*.{ts,tsx,vue,mts}': (stagedFiles: string[]) => [
|
||||
...formatAndEslint(stagedFiles),
|
||||
'pnpm typecheck'
|
||||
'vite run typecheck'
|
||||
]
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ function formatAndEslint(fileNames: string[]) {
|
||||
const relativePaths = fileNames.map((f) => path.relative(process.cwd(), f))
|
||||
const joinedPaths = relativePaths.map((p) => `"${p}"`).join(' ')
|
||||
return [
|
||||
`pnpm exec oxfmt --write ${joinedPaths}`,
|
||||
`pnpm exec oxlint --fix ${joinedPaths}`,
|
||||
`pnpm exec eslint --cache --fix --no-warn-ignored ${joinedPaths}`
|
||||
`vite fmt ${joinedPaths}`,
|
||||
`vite lint --fix ${joinedPaths}`,
|
||||
`vite dlx eslint --cache --fix --no-warn-ignored ${joinedPaths}`
|
||||
]
|
||||
}
|
||||
|
||||
25
package.json
25
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.39.3",
|
||||
"version": "1.39.5",
|
||||
"private": true,
|
||||
"description": "Official front-end implementation of ComfyUI",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -22,20 +22,20 @@
|
||||
"dev:no-vue": "cross-env DISABLE_VUE_PLUGINS=true nx serve",
|
||||
"dev": "nx serve",
|
||||
"devtools:pycheck": "python3 -m compileall -q tools/devtools",
|
||||
"format:check": "oxfmt --check",
|
||||
"format": "oxfmt --write",
|
||||
"format:check": "vite fmt --check",
|
||||
"format": "vite fmt --write",
|
||||
"json-schema": "tsx scripts/generate-json-schema.ts",
|
||||
"knip:no-cache": "knip",
|
||||
"knip": "knip --cache",
|
||||
"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:fix:no-cache": "vite lint src --type-aware --fix && eslint src --fix",
|
||||
"lint:fix": "vite lint src --type-aware --fix && eslint src --cache --fix",
|
||||
"lint:no-cache": "vite lint 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": "oxlint src --type-aware && eslint src --cache",
|
||||
"lint": "vite lint src --type-aware && eslint src --cache",
|
||||
"lint:desktop": "nx run @comfyorg/desktop-ui:lint",
|
||||
"locale": "lobe-i18n locale",
|
||||
"oxlint": "oxlint src --type-aware",
|
||||
"oxlint": "vite lint src --type-aware",
|
||||
"preinstall": "pnpm dlx only-allow pnpm",
|
||||
"prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true",
|
||||
"preview": "nx preview",
|
||||
@@ -160,9 +160,6 @@
|
||||
"markdown-table": "catalog:",
|
||||
"mixpanel-browser": "catalog:",
|
||||
"nx": "catalog:",
|
||||
"oxfmt": "catalog:",
|
||||
"oxlint": "catalog:",
|
||||
"oxlint-tsgolint": "catalog:",
|
||||
"picocolors": "catalog:",
|
||||
"postcss-html": "catalog:",
|
||||
"pretty-bytes": "catalog:",
|
||||
@@ -183,6 +180,7 @@
|
||||
"vite-plugin-dts": "catalog:",
|
||||
"vite-plugin-html": "catalog:",
|
||||
"vite-plugin-vue-devtools": "catalog:",
|
||||
"vite-plus": "catalog:",
|
||||
"vitest": "catalog:",
|
||||
"vue-component-type-helpers": "catalog:",
|
||||
"vue-eslint-parser": "catalog:",
|
||||
@@ -190,9 +188,8 @@
|
||||
"zip-dir": "^2.0.0",
|
||||
"zod-to-json-schema": "catalog:"
|
||||
},
|
||||
"packageManager": "pnpm@10.28.2",
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"vite": "^8.0.0-beta.8"
|
||||
}
|
||||
"overrides": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it } from 'vite-plus/test'
|
||||
|
||||
import { getMediaTypeFromFilename, truncateFilename } from './formatUtil'
|
||||
|
||||
|
||||
1573
pnpm-lock.yaml
generated
1573
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -41,8 +41,8 @@ catalog:
|
||||
'@vitest/coverage-v8': ^4.0.16
|
||||
'@vitest/ui': ^4.0.16
|
||||
'@vue/test-utils': ^2.4.6
|
||||
'@vueuse/core': ^11.0.0
|
||||
'@vueuse/integrations': ^13.9.0
|
||||
'@vueuse/core': ^14.2.0
|
||||
'@vueuse/integrations': ^14.2.0
|
||||
'@webgpu/types': ^0.1.66
|
||||
algoliasearch: ^5.21.0
|
||||
axios: ^1.8.2
|
||||
@@ -62,16 +62,13 @@ catalog:
|
||||
happy-dom: ^20.0.11
|
||||
husky: ^9.1.7
|
||||
jiti: 2.6.1
|
||||
jsonata: ^2.1.0
|
||||
jsdom: ^27.4.0
|
||||
jsonata: ^2.1.0
|
||||
knip: ^5.75.1
|
||||
lint-staged: ^16.2.7
|
||||
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
|
||||
@@ -92,11 +89,11 @@ catalog:
|
||||
unplugin-icons: ^22.5.0
|
||||
unplugin-typegpu: 0.8.0
|
||||
unplugin-vue-components: ^30.0.0
|
||||
vite: ^8.0.0-beta.8
|
||||
vite: npm:@voidzero-dev/vite-plus-core@latest
|
||||
vite-plugin-dts: ^4.5.4
|
||||
vite-plugin-html: ^3.2.2
|
||||
vite-plugin-vue-devtools: ^8.0.0
|
||||
vitest: ^4.0.16
|
||||
vitest: npm:@voidzero-dev/vite-plus-test@latest
|
||||
vue: ^3.5.13
|
||||
vue-component-type-helpers: ^3.2.1
|
||||
vue-eslint-parser: ^10.2.0
|
||||
@@ -109,6 +106,7 @@ catalog:
|
||||
zod: ^3.23.8
|
||||
zod-to-json-schema: ^3.24.1
|
||||
zod-validation-error: ^3.3.0
|
||||
vite-plus: latest
|
||||
|
||||
cleanupUnusedCatalogs: true
|
||||
|
||||
@@ -130,3 +128,12 @@ onlyBuiltDependencies:
|
||||
|
||||
overrides:
|
||||
'@types/eslint': '-'
|
||||
vite: 'catalog:'
|
||||
vitest: 'catalog:'
|
||||
peerDependencyRules:
|
||||
allowAny:
|
||||
- vite
|
||||
- vitest
|
||||
allowedVersions:
|
||||
vite: '*'
|
||||
vitest: '*'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
|
||||
import { downloadFile } from '@/base/common/downloadUtil'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, test } from 'vitest'
|
||||
import { describe, expect, test } from 'vite-plus/test'
|
||||
|
||||
import {
|
||||
CREDITS_PER_USD,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { computed, defineComponent, h, nextTick, onMounted } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import WorkspaceAuthGate from './WorkspaceAuthGate.vue'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
|
||||
import EssentialsPanel from '@/components/bottomPanel/tabs/shortcuts/EssentialsPanel.vue'
|
||||
import ShortcutsList from '@/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vite-plus/test'
|
||||
|
||||
import ShortcutsList from '@/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue'
|
||||
import type { ComfyCommandImpl } from '@/stores/commandStore'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import type { Mock } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Mock } from 'vite-plus/test'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'
|
||||
import ColorPicker from 'primevue/colorpicker'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { beforeEach, describe, expect, it } from 'vite-plus/test'
|
||||
import { createApp, nextTick } from 'vue'
|
||||
|
||||
import ColorCustomizationSelector from './ColorCustomizationSelector.vue'
|
||||
|
||||
@@ -38,7 +38,10 @@ const {
|
||||
alt = 'Image content'
|
||||
} = defineProps<{
|
||||
src: string
|
||||
class?: any
|
||||
class?:
|
||||
| string
|
||||
| Record<string, boolean>
|
||||
| (string | Record<string, boolean>)[]
|
||||
contain?: boolean
|
||||
alt?: string
|
||||
}>()
|
||||
|
||||
@@ -28,13 +28,17 @@ const deviceColumns: { field: keyof DeviceStats; header: string }[] = [
|
||||
{ field: 'torch_vram_free', header: 'Torch VRAM Free' }
|
||||
]
|
||||
|
||||
const formatValue = (value: any, field: string) => {
|
||||
const formatValue = (value: string | number, field: string) => {
|
||||
if (
|
||||
['vram_total', 'vram_free', 'torch_vram_total', 'torch_vram_free'].includes(
|
||||
field
|
||||
)
|
||||
) {
|
||||
return formatSize(value)
|
||||
const num = Number(value)
|
||||
if (Number.isFinite(num)) {
|
||||
return formatSize(num)
|
||||
}
|
||||
return value
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { beforeAll, describe, expect, it } from 'vitest'
|
||||
import { beforeAll, describe, expect, it } from 'vite-plus/test'
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import EditableText from './EditableText.vue'
|
||||
|
||||
@@ -47,7 +47,7 @@ import InputSlider from '@/components/common/InputSlider.vue'
|
||||
import UrlInput from '@/components/common/UrlInput.vue'
|
||||
import type { FormItem } from '@/platform/settings/types'
|
||||
|
||||
const formValue = defineModel<any>('formValue')
|
||||
const formValue = defineModel<unknown>('formValue')
|
||||
const props = defineProps<{
|
||||
item: FormItem
|
||||
id?: string
|
||||
@@ -61,7 +61,7 @@ function getFormAttrs(item: FormItem) {
|
||||
attrs['renderFunction'] = () =>
|
||||
inputType(
|
||||
props.item.name,
|
||||
(v: any) => (formValue.value = v),
|
||||
(v: unknown) => (formValue.value = v),
|
||||
formValue.value,
|
||||
item.attrs
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import RadioButton from 'primevue/radiobutton'
|
||||
import { beforeAll, describe, expect, it } from 'vitest'
|
||||
import { beforeAll, describe, expect, it } from 'vite-plus/test'
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import type { SettingOption } from '@/platform/settings/types'
|
||||
|
||||
@@ -20,14 +20,14 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts" generic="T extends string | number | boolean | null">
|
||||
import RadioButton from 'primevue/radiobutton'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { SettingOption } from '@/platform/settings/types'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any
|
||||
modelValue: T
|
||||
options?: (string | SettingOption | Record<string, string>)[]
|
||||
optionLabel?: string
|
||||
optionValue?: string
|
||||
@@ -35,7 +35,7 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
'update:modelValue': [value: any]
|
||||
'update:modelValue': [value: T]
|
||||
}>()
|
||||
|
||||
const normalizedOptions = computed<SettingOption[]>(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
|
||||
@@ -221,7 +221,7 @@ const wrapCommandWithErrorHandler = (
|
||||
) => {
|
||||
return isAsync
|
||||
? errorHandling.wrapWithErrorHandlingAsync(
|
||||
command as (...args: any[]) => Promise<any>,
|
||||
command as (event: MenuItemCommandEvent) => Promise<void>,
|
||||
menuTargetNode.value?.handleError
|
||||
)
|
||||
: errorHandling.wrapWithErrorHandling(
|
||||
|
||||
@@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils'
|
||||
import Badge from 'primevue/badge'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { createApp } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import PrimeVue from 'primevue/config'
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { beforeEach, describe, expect, it } from 'vite-plus/test'
|
||||
import { createApp, nextTick } from 'vue'
|
||||
|
||||
import UrlInput from './UrlInput.vue'
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { ComponentProps } from 'vue-component-type-helpers'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Avatar from 'primevue/avatar'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { beforeEach, describe, expect, it } from 'vite-plus/test'
|
||||
import { createApp, nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json' with { type: 'json' }
|
||||
|
||||
@@ -256,6 +256,11 @@
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<LogoOverlay
|
||||
v-if="template.logos?.length"
|
||||
:logos="template.logos"
|
||||
:get-logo-url="workflowTemplatesStore.getLogoUrl"
|
||||
/>
|
||||
<ProgressSpinner
|
||||
v-if="loadingTemplate === template.name"
|
||||
class="absolute inset-0 z-10 m-auto h-12 w-12"
|
||||
@@ -397,6 +402,7 @@ import AudioThumbnail from '@/components/templates/thumbnails/AudioThumbnail.vue
|
||||
import CompareSliderThumbnail from '@/components/templates/thumbnails/CompareSliderThumbnail.vue'
|
||||
import DefaultThumbnail from '@/components/templates/thumbnails/DefaultThumbnail.vue'
|
||||
import HoverDissolveThumbnail from '@/components/templates/thumbnails/HoverDissolveThumbnail.vue'
|
||||
import LogoOverlay from '@/components/templates/thumbnails/LogoOverlay.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
|
||||
import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
|
||||
@@ -406,8 +412,8 @@ import { useTemplateFiltering } from '@/composables/useTemplateFiltering'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useTemplateWorkflows } from '@/platform/workflow/templates/composables/useTemplateWorkflows'
|
||||
import { useWorkflowTemplatesStore } from '@/platform/workflow/templates/repositories/workflowTemplatesStore'
|
||||
import type { TemplateInfo } from '@/platform/workflow/templates/types/template'
|
||||
import { useWorkflowTemplatesStore } from '@/platform/workflow/templates/repositories/workflowTemplatesStore'
|
||||
import { TemplateIncludeOnDistributionEnum } from '@/platform/workflow/templates/types/template'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
import type { NavGroupData, NavItemData } from '@/types/navTypes'
|
||||
@@ -788,7 +794,7 @@ watch(
|
||||
)
|
||||
|
||||
// Methods
|
||||
const onLoadWorkflow = async (template: any) => {
|
||||
const onLoadWorkflow = async (template: TemplateInfo) => {
|
||||
loadingTemplate.value = template.name
|
||||
try {
|
||||
await loadWorkflowTemplate(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Message from 'primevue/message'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it } from 'vite-plus/test'
|
||||
|
||||
import CreditTopUpOption from '@/components/dialog/content/credit/CreditTopUpOption.vue'
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createPinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Tag from 'primevue/tag'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import SettingItem from '@/platform/settings/components/SettingItem.vue'
|
||||
|
||||
@@ -8,7 +8,7 @@ import DataTable from 'primevue/datatable'
|
||||
import Message from 'primevue/message'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
|
||||
@@ -63,14 +63,18 @@
|
||||
<i class="pi pi-sign-out" />
|
||||
{{ $t('auth.signOut.signOut') }}
|
||||
</Button>
|
||||
<Button
|
||||
<i18n-t
|
||||
v-if="!isApiKeyLogin"
|
||||
class="w-fit"
|
||||
variant="destructive-textonly"
|
||||
@click="handleDeleteAccount"
|
||||
keypath="auth.deleteAccount.contactSupport"
|
||||
tag="p"
|
||||
class="text-muted text-sm"
|
||||
>
|
||||
{{ $t('auth.deleteAccount.deleteAccount') }}
|
||||
</Button>
|
||||
<template #email>
|
||||
<a href="mailto:support@comfy.org" class="underline"
|
||||
>support@comfy.org</a
|
||||
>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -116,7 +120,6 @@ const {
|
||||
providerName,
|
||||
providerIcon,
|
||||
handleSignOut,
|
||||
handleSignIn,
|
||||
handleDeleteAccount
|
||||
handleSignIn
|
||||
} = useCurrentUser()
|
||||
</script>
|
||||
|
||||
@@ -7,7 +7,7 @@ import Button from '@/components/ui/button/Button.vue'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Message from 'primevue/message'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { createApp } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import InputText from 'primevue/inputtext'
|
||||
import Password from 'primevue/password'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import ToastService from 'primevue/toastservice'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Popover from 'primevue/popover'
|
||||
import type { ComponentPublicInstance } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
@@ -66,7 +67,7 @@ interface Props {
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
const buttonRef = ref<InstanceType<typeof Button>>()
|
||||
const buttonRef = ref<ComponentPublicInstance | null>(null)
|
||||
const popover = ref<InstanceType<typeof Popover>>()
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
@@ -92,7 +93,7 @@ const lockCommandText = computed(() =>
|
||||
)
|
||||
|
||||
const toggle = (event: Event) => {
|
||||
const el = (buttonRef.value as any)?.$el || buttonRef.value
|
||||
const el = buttonRef.value?.$el || buttonRef.value
|
||||
popover.value?.toggle(event, el)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import SelectionToolbox from '@/components/graph/SelectionToolbox.vue'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import ZoomControlsModal from '@/components/graph/modals/ZoomControlsModal.vue'
|
||||
|
||||
@@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import BypassButton from '@/components/graph/selectionToolbox/BypassButton.vue'
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Mock } from 'vitest'
|
||||
import type { Mock } from 'vite-plus/test'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
// Import after mocks
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import ExecuteButton from '@/components/graph/selectionToolbox/ExecuteButton.vue'
|
||||
|
||||
@@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import InfoButton from '@/components/graph/selectionToolbox/InfoButton.vue'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { defineComponent, h, nextTick, ref } from 'vue'
|
||||
|
||||
import HoneyToast from './HoneyToast.vue'
|
||||
|
||||
@@ -57,6 +57,41 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<label class="text-xs text-muted-foreground">
|
||||
{{ $t('imageCrop.ratio') }}
|
||||
</label>
|
||||
<Select v-model="selectedRatio">
|
||||
<SelectTrigger class="h-7 w-24 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="key in ratioKeys" :key="key" :value="key">
|
||||
{{ key === 'custom' ? $t('imageCrop.custom') : key }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
size="icon"
|
||||
:variant="isLockEnabled ? 'primary' : 'secondary'"
|
||||
class="size-7"
|
||||
:aria-label="
|
||||
isLockEnabled
|
||||
? $t('imageCrop.unlockRatio')
|
||||
: $t('imageCrop.lockRatio')
|
||||
"
|
||||
@click="isLockEnabled = !isLockEnabled"
|
||||
>
|
||||
<i
|
||||
:class="
|
||||
isLockEnabled
|
||||
? 'icon-[lucide--lock] size-3.5'
|
||||
: 'icon-[lucide--lock-open] size-3.5'
|
||||
"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<WidgetBoundingBox v-model="modelValue" class="shrink-0" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -65,7 +100,13 @@
|
||||
import { useTemplateRef } from 'vue'
|
||||
|
||||
import WidgetBoundingBox from '@/components/boundingbox/WidgetBoundingBox.vue'
|
||||
import { useImageCrop } from '@/composables/useImageCrop'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import Select from '@/components/ui/select/Select.vue'
|
||||
import SelectContent from '@/components/ui/select/SelectContent.vue'
|
||||
import SelectItem from '@/components/ui/select/SelectItem.vue'
|
||||
import SelectTrigger from '@/components/ui/select/SelectTrigger.vue'
|
||||
import SelectValue from '@/components/ui/select/SelectValue.vue'
|
||||
import { ASPECT_RATIOS, useImageCrop } from '@/composables/useImageCrop'
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { Bounds } from '@/renderer/core/layout/types'
|
||||
|
||||
@@ -80,10 +121,15 @@ const modelValue = defineModel<Bounds>({
|
||||
const imageEl = useTemplateRef<HTMLImageElement>('imageEl')
|
||||
const containerEl = useTemplateRef<HTMLDivElement>('containerEl')
|
||||
|
||||
const ratioKeys = Object.keys(ASPECT_RATIOS)
|
||||
|
||||
const {
|
||||
imageUrl,
|
||||
isLoading,
|
||||
|
||||
selectedRatio,
|
||||
isLockEnabled,
|
||||
|
||||
cropBoxStyle,
|
||||
cropImageStyle,
|
||||
resizeHandles,
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
ref="container"
|
||||
class="relative h-full w-full min-h-[200px]"
|
||||
data-capture-wheel="true"
|
||||
@pointerdown.stop
|
||||
tabindex="-1"
|
||||
@pointerdown.stop="focusContainer"
|
||||
@pointermove.stop
|
||||
@pointerup.stop
|
||||
@mousedown.stop
|
||||
@@ -45,6 +46,10 @@ const props = defineProps<{
|
||||
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
|
||||
function focusContainer() {
|
||||
container.value?.focus()
|
||||
}
|
||||
|
||||
const { isDragging, dragMessage, handleDragOver, handleDragLeave, handleDrop } =
|
||||
useLoad3dDrag({
|
||||
onModelDrop: async (file) => {
|
||||
|
||||
@@ -121,8 +121,9 @@ const mutationObserver = ref<MutationObserver | null>(null)
|
||||
|
||||
const isStandaloneMode = !props.node && props.modelUrl
|
||||
|
||||
// Use sync version since useLoad3dViewer is already imported (module is loaded)
|
||||
const viewer = props.node
|
||||
? useLoad3dService().getOrCreateViewer(toRaw(props.node))
|
||||
? useLoad3dService().getOrCreateViewerSync(toRaw(props.node), useLoad3dViewer)
|
||||
: useLoad3dViewer()
|
||||
|
||||
const { isDragging, dragMessage, handleDragOver, handleDragLeave, handleDrop } =
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import { beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeAll, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { createApp } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
@@ -76,14 +76,6 @@ describe('NodePreview', () => {
|
||||
expect(wrapper.find('._sb_preview_badge').text()).toBe('Preview')
|
||||
})
|
||||
|
||||
it('applies text-ellipsis class to node header for text truncation', () => {
|
||||
const wrapper = mountComponent()
|
||||
const nodeHeader = wrapper.find('.node_header')
|
||||
|
||||
expect(nodeHeader.classes()).toContain('text-ellipsis')
|
||||
expect(nodeHeader.classes()).toContain('mr-4')
|
||||
})
|
||||
|
||||
it('sets title attribute on node header with full display name', () => {
|
||||
const wrapper = mountComponent()
|
||||
const nodeHeader = wrapper.find('.node_header')
|
||||
|
||||
@@ -10,7 +10,7 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c6830
|
||||
<div v-else class="_sb_node_preview bg-component-node-background">
|
||||
<div class="_sb_table">
|
||||
<div
|
||||
class="node_header mr-4 text-ellipsis"
|
||||
class="node_header text-ellipsis"
|
||||
:title="nodeDef.display_name"
|
||||
:style="{
|
||||
backgroundColor: litegraphColors.NODE_DEFAULT_COLOR,
|
||||
@@ -124,7 +124,10 @@ const slotInputDefs = allInputDefs.filter(
|
||||
const widgetInputDefs = allInputDefs.filter((input) =>
|
||||
widgetStore.inputIsWidget(input)
|
||||
)
|
||||
const truncateDefaultValue = (value: any, charLimit: number = 32): string => {
|
||||
const truncateDefaultValue = (
|
||||
value: unknown,
|
||||
charLimit: number = 32
|
||||
): string => {
|
||||
let stringValue: string
|
||||
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it } from 'vite-plus/test'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import CompletionSummaryBanner from './CompletionSummaryBanner.vue'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import QueueOverlayActive from './QueueOverlayActive.vue'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it } from 'vite-plus/test'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import QueueOverlayEmpty from './QueueOverlayEmpty.vue'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
|
||||
@@ -264,6 +264,7 @@ const focusAssetInSidebar = async (item: JobListItem) => {
|
||||
throw new Error('Asset not found in media assets panel')
|
||||
}
|
||||
assetSelectionStore.setSelection([assetId])
|
||||
assetSelectionStore.setLastSelectedAssetId(assetId)
|
||||
}
|
||||
|
||||
const inspectJobAsset = wrapWithErrorHandlingAsync(
|
||||
|
||||
@@ -296,7 +296,7 @@ const extraRows = computed<DetailRow[]>(() => {
|
||||
]
|
||||
}
|
||||
if (jobState.value === 'completed') {
|
||||
const task = taskForJob.value as any
|
||||
const task = taskForJob.value
|
||||
const endTs: number | undefined = task?.executionEndTimestamp
|
||||
const execMs: number | undefined = task?.executionTime
|
||||
const generatedOnValue = endTs ? formatClockTime(endTs, locale.value) : ''
|
||||
@@ -321,7 +321,7 @@ const extraRows = computed<DetailRow[]>(() => {
|
||||
return rows
|
||||
}
|
||||
if (jobState.value === 'failed') {
|
||||
const task = taskForJob.value as any
|
||||
const task = taskForJob.value
|
||||
const execMs: number | undefined = task?.executionTime
|
||||
const failedAfterValue =
|
||||
execMs !== undefined ? formatElapsedTime(execMs) : ''
|
||||
|
||||
@@ -179,7 +179,7 @@ const onFilterClick = (event: Event) => {
|
||||
}
|
||||
}
|
||||
const selectWorkflowFilter = (value: 'all' | 'current') => {
|
||||
;(filterPopoverRef.value as any)?.hide?.()
|
||||
filterPopoverRef.value?.hide()
|
||||
emit('update:selectedWorkflowFilter', value)
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ const onSortClick = (event: Event) => {
|
||||
}
|
||||
|
||||
const selectSortMode = (value: JobSortMode) => {
|
||||
;(sortPopoverRef.value as any)?.hide?.()
|
||||
sortPopoverRef.value?.hide()
|
||||
emit('update:selectedSortMode', value)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { defineComponent, nextTick } from 'vue'
|
||||
|
||||
import JobGroupsList from '@/components/queue/job/JobGroupsList.vue'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { computed, ref } from 'vue'
|
||||
import type { ComputedRef } from 'vue'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it } from 'vite-plus/test'
|
||||
|
||||
import type { TaskItemImpl } from '@/stores/queueStore'
|
||||
import type { JobState } from '@/types/queue'
|
||||
|
||||
@@ -8,6 +8,7 @@ import Tab from '@/components/tab/Tab.vue'
|
||||
import TabList from '@/components/tab/TabList.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useGraphHierarchy } from '@/composables/graph/useGraphHierarchy'
|
||||
import type { ProxyWidgetsProperty } from '@/core/schemas/proxyWidget'
|
||||
import { st } from '@/i18n'
|
||||
import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
@@ -190,6 +191,12 @@ function handleTitleEdit(newTitle: string) {
|
||||
function handleTitleCancel() {
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
function handleProxyWidgetsUpdate(value: ProxyWidgetsProperty) {
|
||||
if (!selectedSingleNode.value) return
|
||||
;(selectedSingleNode.value as SubgraphNode).properties.proxyWidgets = value
|
||||
canvasStore.canvas?.setDirty(true, true)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -283,6 +290,7 @@ function handleTitleCancel() {
|
||||
<TabSubgraphInputs
|
||||
v-if="activeTab === 'parameters' && isSingleSubgraphNode"
|
||||
:node="selectedSingleNode as SubgraphNode"
|
||||
@update:proxy-widgets="handleProxyWidgetsUpdate"
|
||||
/>
|
||||
<TabNormalInputs
|
||||
v-else-if="activeTab === 'parameters'"
|
||||
|
||||
@@ -118,6 +118,15 @@ function handleLocateNode() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleWidgetValueUpdate(
|
||||
widget: IBaseWidget,
|
||||
newValue: string | number | boolean | object
|
||||
) {
|
||||
widget.value = newValue
|
||||
widget.callback?.(newValue)
|
||||
canvasStore.canvas?.setDirty(true, true)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
widgetsContainer,
|
||||
rootElement
|
||||
@@ -179,6 +188,7 @@ defineExpose({
|
||||
:show-node-name="showNodeName"
|
||||
:parents="parents"
|
||||
:is-shown-on-parents="isWidgetShownOnParents(node, widget)"
|
||||
@update:widget-value="handleWidgetValueUpdate(widget, $event)"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
|
||||
@@ -32,6 +32,10 @@ const { node } = defineProps<{
|
||||
node: SubgraphNode
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:proxyWidgets': [value: ProxyWidgetsProperty]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const canvasStore = useCanvasStore()
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
@@ -51,8 +55,7 @@ const proxyWidgets = customRef<ProxyWidgetsProperty>((track, trigger) => ({
|
||||
set(value?: ProxyWidgetsProperty) {
|
||||
trigger()
|
||||
if (!value) return
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
node.properties.proxyWidgets = value
|
||||
emit('update:proxyWidgets', value)
|
||||
}
|
||||
}))
|
||||
|
||||
|
||||
@@ -41,6 +41,10 @@ const {
|
||||
isShownOnParents?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:widgetValue': [value: string | number | boolean | object]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const canvasStore = useCanvasStore()
|
||||
const favoritedWidgetsStore = useFavoritedWidgetsStore()
|
||||
@@ -83,10 +87,7 @@ const widgetValue = computed({
|
||||
return widget.value
|
||||
},
|
||||
set: (newValue: string | number | boolean | object) => {
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
widget.value = newValue
|
||||
widget.callback?.(newValue)
|
||||
canvasStore.canvas?.setDirty(true, true)
|
||||
emit('update:widgetValue', newValue)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LGraphGroup } from '@/lib/litegraph/src/LGraphGroup'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { Positionable } from '@/lib/litegraph/src/interfaces'
|
||||
import { describe, expect, it, beforeEach } from 'vitest'
|
||||
import { describe, expect, it, beforeEach } from 'vite-plus/test'
|
||||
import { flatAndCategorizeSelectedItems, searchWidgets } from './shared'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it } from 'vite-plus/test'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import SidebarIcon from './SidebarIcon.vue'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="flex h-full flex-col">
|
||||
<!-- Active Jobs Grid -->
|
||||
<div
|
||||
v-if="isQueuePanelV2Enabled && activeJobItems.length"
|
||||
v-if="!isInFolderView && isQueuePanelV2Enabled && activeJobItems.length"
|
||||
class="grid max-h-[50%] scrollbar-custom overflow-y-auto"
|
||||
:style="gridStyle"
|
||||
>
|
||||
@@ -70,12 +70,14 @@ import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
const {
|
||||
assets,
|
||||
isSelected,
|
||||
isInFolderView = false,
|
||||
assetType = 'output',
|
||||
showOutputCount,
|
||||
getOutputCount
|
||||
} = defineProps<{
|
||||
assets: AssetItem[]
|
||||
isSelected: (assetId: string) => boolean
|
||||
isInFolderView?: boolean
|
||||
assetType?: 'input' | 'output'
|
||||
showOutputCount: (asset: AssetItem) => boolean
|
||||
getOutputCount: (asset: AssetItem) => number
|
||||
@@ -100,7 +102,7 @@ const isQueuePanelV2Enabled = computed(() =>
|
||||
type AssetGridItem = { key: string; asset: AssetItem }
|
||||
|
||||
const activeJobItems = computed(() =>
|
||||
jobItems.value.filter((item) => isActiveJobState(item.state))
|
||||
jobItems.value.filter((item) => isActiveJobState(item.state)).toReversed()
|
||||
)
|
||||
|
||||
const assetItems = computed<AssetGridItem[]>(() =>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { toRef } from 'vue'
|
||||
|
||||
import type { JobAction } from '@/composables/queue/useJobActions'
|
||||
import type { JobListItem } from '@/composables/queue/useJobList'
|
||||
import { useOutputStacks } from '@/platform/assets/composables/useOutputStacks'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { setMockJobActions } from '@/storybook/mocks/useJobActions'
|
||||
import { setMockJobItems } from '@/storybook/mocks/useJobList'
|
||||
@@ -138,16 +140,33 @@ function renderAssetsSidebarListView(args: StoryArgs) {
|
||||
setup() {
|
||||
setMockJobItems(args.jobs)
|
||||
setMockJobActions(args.actionsByJobId ?? {})
|
||||
const { assetItems, selectableAssets, isStackExpanded, toggleStack } =
|
||||
useOutputStacks({
|
||||
assets: toRef(args, 'assets')
|
||||
})
|
||||
const selectedIds = new Set(args.selectedAssetIds ?? [])
|
||||
function isSelected(assetId: string) {
|
||||
return selectedIds.has(assetId)
|
||||
}
|
||||
|
||||
return { args, isSelected }
|
||||
return {
|
||||
args,
|
||||
assetItems,
|
||||
selectableAssets,
|
||||
isSelected,
|
||||
isStackExpanded,
|
||||
toggleStack
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="h-[520px] w-[320px] overflow-hidden rounded-lg border border-panel-border">
|
||||
<AssetsSidebarListView :assets="args.assets" :is-selected="isSelected" />
|
||||
<AssetsSidebarListView
|
||||
:asset-items="assetItems"
|
||||
:selectable-assets="selectableAssets"
|
||||
:is-selected="isSelected"
|
||||
:is-stack-expanded="isStackExpanded"
|
||||
:toggle-stack="toggleStack"
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
150
src/components/sidebar/tabs/AssetsSidebarListView.test.ts
Normal file
150
src/components/sidebar/tabs/AssetsSidebarListView.test.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import AssetsSidebarListView from './AssetsSidebarListView.vue'
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/queue/useJobActions', () => ({
|
||||
useJobActions: () => ({
|
||||
cancelAction: { variant: 'ghost', label: 'Cancel', icon: 'pi pi-times' },
|
||||
canCancelJob: ref(false),
|
||||
runCancelJob: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
const mockJobItems = ref<
|
||||
Array<{
|
||||
id: string
|
||||
title: string
|
||||
meta: string
|
||||
state: string
|
||||
createTime?: number
|
||||
}>
|
||||
>([])
|
||||
|
||||
vi.mock('@/composables/queue/useJobList', () => ({
|
||||
useJobList: () => ({
|
||||
jobItems: mockJobItems
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/assetsStore', () => ({
|
||||
useAssetsStore: () => ({
|
||||
isAssetDeleting: () => false
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: () => ({
|
||||
get: (key: string) => key === 'Comfy.Queue.QPOV2'
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/queueUtil', () => ({
|
||||
isActiveJobState: (state: string) =>
|
||||
state === 'pending' || state === 'running'
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/queueDisplay', () => ({
|
||||
iconForJobState: () => 'pi pi-spinner'
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/assets/schemas/assetMetadataSchema', () => ({
|
||||
getOutputAssetMetadata: () => undefined
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/assets/utils/mediaIconUtil', () => ({
|
||||
iconForMediaType: () => 'pi pi-file'
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/formatUtil', () => ({
|
||||
formatDuration: (d: number) => `${d}s`,
|
||||
formatSize: (s: number) => `${s}B`,
|
||||
getMediaTypeFromFilename: () => 'image',
|
||||
truncateFilename: (name: string) => name
|
||||
}))
|
||||
|
||||
describe('AssetsSidebarListView', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockJobItems.value = []
|
||||
})
|
||||
|
||||
const defaultProps = {
|
||||
assetItems: [],
|
||||
selectableAssets: [],
|
||||
isSelected: () => false,
|
||||
isStackExpanded: () => false,
|
||||
toggleStack: async () => {}
|
||||
}
|
||||
|
||||
it('displays active jobs in oldest-first order (FIFO)', () => {
|
||||
mockJobItems.value = [
|
||||
{
|
||||
id: 'newest',
|
||||
title: 'Newest Job',
|
||||
meta: '',
|
||||
state: 'pending',
|
||||
createTime: 3000
|
||||
},
|
||||
{
|
||||
id: 'middle',
|
||||
title: 'Middle Job',
|
||||
meta: '',
|
||||
state: 'running',
|
||||
createTime: 2000
|
||||
},
|
||||
{
|
||||
id: 'oldest',
|
||||
title: 'Oldest Job',
|
||||
meta: '',
|
||||
state: 'pending',
|
||||
createTime: 1000
|
||||
}
|
||||
]
|
||||
|
||||
const wrapper = mount(AssetsSidebarListView, {
|
||||
props: defaultProps,
|
||||
shallow: true
|
||||
})
|
||||
|
||||
const jobListItems = wrapper.findAllComponents({ name: 'AssetsListItem' })
|
||||
expect(jobListItems).toHaveLength(3)
|
||||
|
||||
const displayedTitles = jobListItems.map((item) =>
|
||||
item.props('primaryText')
|
||||
)
|
||||
expect(displayedTitles).toEqual(['Oldest Job', 'Middle Job', 'Newest Job'])
|
||||
})
|
||||
|
||||
it('excludes completed and failed jobs from active jobs section', () => {
|
||||
mockJobItems.value = [
|
||||
{ id: 'pending', title: 'Pending', meta: '', state: 'pending' },
|
||||
{ id: 'completed', title: 'Completed', meta: '', state: 'completed' },
|
||||
{ id: 'failed', title: 'Failed', meta: '', state: 'failed' },
|
||||
{ id: 'running', title: 'Running', meta: '', state: 'running' }
|
||||
]
|
||||
|
||||
const wrapper = mount(AssetsSidebarListView, {
|
||||
props: defaultProps,
|
||||
shallow: true
|
||||
})
|
||||
|
||||
const jobListItems = wrapper.findAllComponents({ name: 'AssetsListItem' })
|
||||
expect(jobListItems).toHaveLength(2)
|
||||
|
||||
const displayedTitles = jobListItems.map((item) =>
|
||||
item.props('primaryText')
|
||||
)
|
||||
expect(displayedTitles).toContain('Running')
|
||||
expect(displayedTitles).toContain('Pending')
|
||||
expect(displayedTitles).not.toContain('Completed')
|
||||
expect(displayedTitles).not.toContain('Failed')
|
||||
})
|
||||
})
|
||||
@@ -40,7 +40,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="assets.length"
|
||||
v-if="assetItems.length"
|
||||
:class="cn('px-2', activeJobItems.length && 'mt-2')"
|
||||
>
|
||||
<div
|
||||
@@ -79,7 +79,12 @@
|
||||
type: getMediaTypeFromFilename(item.asset.name)
|
||||
})
|
||||
"
|
||||
:class="getAssetCardClass(isSelected(item.asset.id))"
|
||||
:class="
|
||||
cn(
|
||||
getAssetCardClass(isSelected(item.asset.id)),
|
||||
item.isChild && 'pl-6'
|
||||
)
|
||||
"
|
||||
:preview-url="item.asset.preview_url"
|
||||
:preview-alt="item.asset.name"
|
||||
:icon-name="
|
||||
@@ -87,10 +92,14 @@
|
||||
"
|
||||
:primary-text="getAssetPrimaryText(item.asset)"
|
||||
:secondary-text="getAssetSecondaryText(item.asset)"
|
||||
:stack-count="getStackCount(item.asset)"
|
||||
:stack-indicator-label="t('mediaAsset.actions.seeMoreOutputs')"
|
||||
:stack-expanded="isStackExpanded(item.asset)"
|
||||
@mouseenter="onAssetEnter(item.asset.id)"
|
||||
@mouseleave="onAssetLeave(item.asset.id)"
|
||||
@contextmenu.prevent.stop="emit('context-menu', $event, item.asset)"
|
||||
@click.stop="emit('select-asset', item.asset)"
|
||||
@click.stop="emit('select-asset', item.asset, selectableAssets)"
|
||||
@stack-toggle="void toggleStack(item.asset)"
|
||||
>
|
||||
<template v-if="hoveredAssetId === item.asset.id" #actions>
|
||||
<Button
|
||||
@@ -120,6 +129,7 @@ import { useJobActions } from '@/composables/queue/useJobActions'
|
||||
import type { JobListItem } from '@/composables/queue/useJobList'
|
||||
import { useJobList } from '@/composables/queue/useJobList'
|
||||
import AssetsListItem from '@/platform/assets/components/AssetsListItem.vue'
|
||||
import type { OutputStackListItem } from '@/platform/assets/composables/useOutputStacks'
|
||||
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { iconForMediaType } from '@/platform/assets/utils/mediaIconUtil'
|
||||
@@ -136,19 +146,25 @@ import { cn } from '@/utils/tailwindUtil'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
|
||||
const {
|
||||
assets,
|
||||
assetItems,
|
||||
selectableAssets,
|
||||
isSelected,
|
||||
isStackExpanded,
|
||||
toggleStack,
|
||||
assetType = 'output'
|
||||
} = defineProps<{
|
||||
assets: AssetItem[]
|
||||
assetItems: OutputStackListItem[]
|
||||
selectableAssets: AssetItem[]
|
||||
isSelected: (assetId: string) => boolean
|
||||
isStackExpanded: (asset: AssetItem) => boolean
|
||||
toggleStack: (asset: AssetItem) => Promise<void>
|
||||
assetType?: 'input' | 'output'
|
||||
}>()
|
||||
|
||||
const assetsStore = useAssetsStore()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'select-asset', asset: AssetItem): void
|
||||
(e: 'select-asset', asset: AssetItem, assets?: AssetItem[]): void
|
||||
(e: 'context-menu', event: MouseEvent, asset: AssetItem): void
|
||||
(e: 'approach-end'): void
|
||||
}>()
|
||||
@@ -162,11 +178,8 @@ const isQueuePanelV2Enabled = computed(() =>
|
||||
)
|
||||
const hoveredJobId = ref<string | null>(null)
|
||||
const hoveredAssetId = ref<string | null>(null)
|
||||
|
||||
type AssetListItem = { key: string; asset: AssetItem }
|
||||
|
||||
const activeJobItems = computed(() =>
|
||||
jobItems.value.filter((item) => isActiveJobState(item.state))
|
||||
jobItems.value.filter((item) => isActiveJobState(item.state)).toReversed()
|
||||
)
|
||||
const hoveredJob = computed(() =>
|
||||
hoveredJobId.value
|
||||
@@ -176,13 +189,6 @@ const hoveredJob = computed(() =>
|
||||
)
|
||||
const { cancelAction, canCancelJob, runCancelJob } = useJobActions(hoveredJob)
|
||||
|
||||
const assetItems = computed<AssetListItem[]>(() =>
|
||||
assets.map((asset) => ({
|
||||
key: `asset-${asset.id}`,
|
||||
asset
|
||||
}))
|
||||
)
|
||||
|
||||
const listGridStyle = {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'minmax(0, 1fr)',
|
||||
@@ -212,6 +218,19 @@ function getAssetSecondaryText(asset: AssetItem): string {
|
||||
return ''
|
||||
}
|
||||
|
||||
function getStackCount(asset: AssetItem): number | undefined {
|
||||
const metadata = getOutputAssetMetadata(asset.user_metadata)
|
||||
if (typeof metadata?.outputCount === 'number') {
|
||||
return metadata.outputCount
|
||||
}
|
||||
|
||||
if (Array.isArray(metadata?.allOutputs)) {
|
||||
return metadata.allOutputs.length
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
function getAssetCardClass(selected: boolean): string {
|
||||
return cn(
|
||||
'w-full text-text-primary transition-colors hover:bg-secondary-background-hover',
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
:show-generation-time-sort="activeTab === 'output'"
|
||||
/>
|
||||
<div
|
||||
v-if="isQueuePanelV2Enabled"
|
||||
v-if="isQueuePanelV2Enabled && !isInFolderView"
|
||||
class="flex items-center justify-between px-2 py-2 2xl:px-4"
|
||||
>
|
||||
<span class="text-sm text-muted-foreground">
|
||||
@@ -98,8 +98,11 @@
|
||||
<div v-else class="relative size-full" @click="handleEmptySpaceClick">
|
||||
<AssetsSidebarListView
|
||||
v-if="isListView"
|
||||
:assets="displayAssets"
|
||||
:asset-items="listViewAssetItems"
|
||||
:is-selected="isSelected"
|
||||
:selectable-assets="listViewSelectableAssets"
|
||||
:is-stack-expanded="isListViewStackExpanded"
|
||||
:toggle-stack="toggleListViewStack"
|
||||
:asset-type="activeTab"
|
||||
@select-asset="handleAssetSelect"
|
||||
@context-menu="handleAssetContextMenu"
|
||||
@@ -109,6 +112,7 @@
|
||||
v-else
|
||||
:assets="displayAssets"
|
||||
:is-selected="isSelected"
|
||||
:is-in-folder-view="isInFolderView"
|
||||
:asset-type="activeTab"
|
||||
:show-output-count="shouldShowOutputCount"
|
||||
:get-output-count="getOutputCount"
|
||||
@@ -215,7 +219,9 @@ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import Load3dViewerContent from '@/components/load3d/Load3dViewerContent.vue'
|
||||
// Lazy-loaded to avoid pulling THREE.js into the main bundle
|
||||
const Load3dViewerContent = () =>
|
||||
import('@/components/load3d/Load3dViewerContent.vue')
|
||||
import AssetsSidebarGridView from '@/components/sidebar/tabs/AssetsSidebarGridView.vue'
|
||||
import AssetsSidebarListView from '@/components/sidebar/tabs/AssetsSidebarListView.vue'
|
||||
import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue'
|
||||
@@ -230,12 +236,13 @@ import { useMediaAssets } from '@/platform/assets/composables/media/useMediaAsse
|
||||
import { useAssetSelection } from '@/platform/assets/composables/useAssetSelection'
|
||||
import { useMediaAssetActions } from '@/platform/assets/composables/useMediaAssetActions'
|
||||
import { useMediaAssetFiltering } from '@/platform/assets/composables/useMediaAssetFiltering'
|
||||
import { useOutputStacks } from '@/platform/assets/composables/useOutputStacks'
|
||||
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import type { MediaKind } from '@/platform/assets/schemas/mediaAssetSchema'
|
||||
import { resolveOutputAssetItems } from '@/platform/assets/utils/outputAssetUtil'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { getJobDetail } from '@/services/jobOutputCache'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
@@ -243,12 +250,6 @@ import { ResultItemImpl, useQueueStore } from '@/stores/queueStore'
|
||||
import { formatDuration, getMediaTypeFromFilename } from '@/utils/formatUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
interface JobOutputItem {
|
||||
filename: string
|
||||
subfolder: string
|
||||
type: string
|
||||
}
|
||||
|
||||
const { t, n } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const queueStore = useQueueStore()
|
||||
@@ -323,6 +324,7 @@ const {
|
||||
hasSelection,
|
||||
clearSelection,
|
||||
getSelectedAssets,
|
||||
reconcileSelection,
|
||||
getOutputCount,
|
||||
getTotalOutputCount,
|
||||
activate: activateSelection,
|
||||
@@ -392,7 +394,21 @@ const displayAssets = computed(() => {
|
||||
return filteredAssets.value
|
||||
})
|
||||
|
||||
const selectedAssets = computed(() => getSelectedAssets(displayAssets.value))
|
||||
const {
|
||||
assetItems: listViewAssetItems,
|
||||
selectableAssets: listViewSelectableAssets,
|
||||
isStackExpanded: isListViewStackExpanded,
|
||||
toggleStack: toggleListViewStack
|
||||
} = useOutputStacks({
|
||||
assets: computed(() => displayAssets.value)
|
||||
})
|
||||
|
||||
const visibleAssets = computed(() => {
|
||||
if (!isListView.value) return displayAssets.value
|
||||
return listViewSelectableAssets.value
|
||||
})
|
||||
|
||||
const selectedAssets = computed(() => getSelectedAssets(visibleAssets.value))
|
||||
|
||||
const isBulkMode = computed(
|
||||
() => hasSelection.value && selectedAssets.value.length > 1
|
||||
@@ -412,7 +428,10 @@ const showEmptyState = computed(
|
||||
activeJobsCount.value === 0
|
||||
)
|
||||
|
||||
watch(displayAssets, (newAssets) => {
|
||||
watch(visibleAssets, (newAssets) => {
|
||||
// Alternative: keep hidden selections and surface them in UI; for now prune
|
||||
// so selection stays consistent with what this view can act on.
|
||||
reconcileSelection(newAssets)
|
||||
if (currentGalleryAssetId.value && galleryActiveIndex.value !== -1) {
|
||||
const newIndex = newAssets.findIndex(
|
||||
(asset) => asset.id === currentGalleryAssetId.value
|
||||
@@ -430,7 +449,7 @@ watch(galleryActiveIndex, (index) => {
|
||||
})
|
||||
|
||||
const galleryItems = computed(() => {
|
||||
return displayAssets.value.map((asset) => {
|
||||
return visibleAssets.value.map((asset) => {
|
||||
const mediaType = getMediaTypeFromFilename(asset.name)
|
||||
const resultItem = new ResultItemImpl({
|
||||
filename: asset.name,
|
||||
@@ -470,9 +489,10 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const handleAssetSelect = (asset: AssetItem) => {
|
||||
const index = displayAssets.value.findIndex((a) => a.id === asset.id)
|
||||
handleAssetClick(asset, index, displayAssets.value)
|
||||
function handleAssetSelect(asset: AssetItem, assets?: AssetItem[]) {
|
||||
const assetList = assets ?? visibleAssets.value
|
||||
const index = assetList.findIndex((a) => a.id === asset.id)
|
||||
handleAssetClick(asset, index, assetList)
|
||||
}
|
||||
|
||||
function handleAssetContextMenu(event: MouseEvent, asset: AssetItem) {
|
||||
@@ -557,7 +577,7 @@ const handleZoomClick = (asset: AssetItem) => {
|
||||
}
|
||||
|
||||
currentGalleryAssetId.value = asset.id
|
||||
const index = displayAssets.value.findIndex((a) => a.id === asset.id)
|
||||
const index = visibleAssets.value.findIndex((a) => a.id === asset.id)
|
||||
if (index !== -1) {
|
||||
galleryActiveIndex.value = index
|
||||
}
|
||||
@@ -570,7 +590,7 @@ const enterFolderView = async (asset: AssetItem) => {
|
||||
return
|
||||
}
|
||||
|
||||
const { promptId, allOutputs, executionTimeInSeconds, outputCount } = metadata
|
||||
const { promptId, executionTimeInSeconds } = metadata
|
||||
|
||||
if (!promptId) {
|
||||
console.warn('Missing required folder view data')
|
||||
@@ -580,62 +600,21 @@ const enterFolderView = async (asset: AssetItem) => {
|
||||
folderPromptId.value = promptId
|
||||
folderExecutionTime.value = executionTimeInSeconds
|
||||
|
||||
// Determine which outputs to display
|
||||
let outputsToDisplay = allOutputs ?? []
|
||||
|
||||
// If outputCount indicates more outputs than we have, fetch full outputs
|
||||
const needsFullOutputs =
|
||||
typeof outputCount === 'number' &&
|
||||
outputCount > 1 &&
|
||||
outputsToDisplay.length < outputCount
|
||||
|
||||
if (needsFullOutputs) {
|
||||
try {
|
||||
const jobDetail = await getJobDetail(promptId)
|
||||
if (jobDetail?.outputs) {
|
||||
// Convert job outputs to ResultItemImpl array
|
||||
outputsToDisplay = Object.entries(jobDetail.outputs).flatMap(
|
||||
([nodeId, nodeOutputs]) =>
|
||||
Object.entries(nodeOutputs).flatMap(([mediaType, items]) =>
|
||||
(items as JobOutputItem[])
|
||||
.map(
|
||||
(item) =>
|
||||
new ResultItemImpl({
|
||||
...item,
|
||||
nodeId,
|
||||
mediaType
|
||||
})
|
||||
)
|
||||
.filter((r) => r.supportsPreview)
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch job detail for folder view:', error)
|
||||
outputsToDisplay = []
|
||||
}
|
||||
let folderItems: AssetItem[] = []
|
||||
try {
|
||||
folderItems = await resolveOutputAssetItems(metadata, {
|
||||
createdAt: asset.created_at
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to resolve outputs for folder view:', error)
|
||||
}
|
||||
|
||||
if (outputsToDisplay.length === 0) {
|
||||
if (folderItems.length === 0) {
|
||||
console.warn('No outputs available for folder view')
|
||||
return
|
||||
}
|
||||
|
||||
folderAssets.value = outputsToDisplay.map((output) => ({
|
||||
id: `${output.nodeId}-${output.filename}`,
|
||||
name: output.filename,
|
||||
size: 0,
|
||||
created_at: asset.created_at,
|
||||
tags: ['output'],
|
||||
preview_url: output.url,
|
||||
user_metadata: {
|
||||
promptId,
|
||||
nodeId: output.nodeId,
|
||||
subfolder: output.subfolder,
|
||||
executionTimeInSeconds,
|
||||
workflow: metadata.workflow
|
||||
}
|
||||
}))
|
||||
folderAssets.value = folderItems
|
||||
}
|
||||
|
||||
const exitFolderView = () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Galleria from 'primevue/galleria'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { createApp, nextTick } from 'vue'
|
||||
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vite-plus/test'
|
||||
|
||||
import AudioThumbnail from '@/components/templates/thumbnails/AudioThumbnail.vue'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import BaseThumbnail from '@/components/templates/thumbnails/BaseThumbnail.vue'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import CompareSliderThumbnail from '@/components/templates/thumbnails/CompareSliderThumbnail.vue'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vite-plus/test'
|
||||
|
||||
import DefaultThumbnail from '@/components/templates/thumbnails/DefaultThumbnail.vue'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vite-plus/test'
|
||||
|
||||
import HoverDissolveThumbnail from '@/components/templates/thumbnails/HoverDissolveThumbnail.vue'
|
||||
|
||||
|
||||
157
src/components/templates/thumbnails/LogoOverlay.test.ts
Normal file
157
src/components/templates/thumbnails/LogoOverlay.test.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import type { ComponentProps } from 'vue-component-type-helpers'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { describe, expect, it, vi } from 'vite-plus/test'
|
||||
|
||||
import LogoOverlay from '@/components/templates/thumbnails/LogoOverlay.vue'
|
||||
import type { LogoInfo } from '@/platform/workflow/templates/types/template'
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string) =>
|
||||
key === 'templates.logoProviderSeparator' ? ' & ' : key,
|
||||
locale: ref('en')
|
||||
})
|
||||
}))
|
||||
|
||||
type LogoOverlayProps = ComponentProps<typeof LogoOverlay>
|
||||
|
||||
describe('LogoOverlay', () => {
|
||||
function mockGetLogoUrl(provider: string) {
|
||||
return `/logos/${provider}.png`
|
||||
}
|
||||
|
||||
function mountOverlay(
|
||||
logos: LogoInfo[],
|
||||
props: Partial<LogoOverlayProps> = {}
|
||||
) {
|
||||
return mount(LogoOverlay, {
|
||||
props: {
|
||||
logos,
|
||||
getLogoUrl: mockGetLogoUrl,
|
||||
...props
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
it('renders nothing when logos array is empty', () => {
|
||||
const wrapper = mountOverlay([])
|
||||
expect(wrapper.findAll('img')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('renders a single logo with correct src and alt', () => {
|
||||
const wrapper = mountOverlay([{ provider: 'Google' }])
|
||||
const img = wrapper.find('img')
|
||||
expect(img.attributes('src')).toBe('/logos/Google.png')
|
||||
expect(img.attributes('alt')).toBe('Google')
|
||||
})
|
||||
|
||||
it('renders multiple separate logo entries', () => {
|
||||
const wrapper = mountOverlay([
|
||||
{ provider: 'Google' },
|
||||
{ provider: 'OpenAI' },
|
||||
{ provider: 'Stability' }
|
||||
])
|
||||
expect(wrapper.findAll('img')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('displays provider name as label for single provider', () => {
|
||||
const wrapper = mountOverlay([{ provider: 'Google' }])
|
||||
const span = wrapper.find('span')
|
||||
expect(span.text()).toBe('Google')
|
||||
})
|
||||
|
||||
it('images are not draggable', () => {
|
||||
const wrapper = mountOverlay([{ provider: 'Google' }])
|
||||
const img = wrapper.find('img')
|
||||
expect(img.attributes('draggable')).toBe('false')
|
||||
})
|
||||
|
||||
it('filters out logos with empty URLs', () => {
|
||||
function getLogoUrl(provider: string) {
|
||||
return provider === 'Google' ? '/logos/Google.png' : ''
|
||||
}
|
||||
const wrapper = mount(LogoOverlay, {
|
||||
props: {
|
||||
logos: [{ provider: 'Google' }, { provider: 'Unknown' }],
|
||||
getLogoUrl
|
||||
}
|
||||
})
|
||||
expect(wrapper.findAll('img')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('renders one logo per unique provider', () => {
|
||||
const wrapper = mountOverlay([
|
||||
{ provider: 'Google' },
|
||||
{ provider: 'OpenAI' }
|
||||
])
|
||||
expect(wrapper.findAll('img')).toHaveLength(2)
|
||||
})
|
||||
|
||||
describe('stacked logos', () => {
|
||||
it('renders multiple providers as stacked overlapping logos', () => {
|
||||
const wrapper = mountOverlay([{ provider: ['WaveSpeed', 'Hunyuan'] }])
|
||||
const images = wrapper.findAll('img')
|
||||
expect(images).toHaveLength(2)
|
||||
expect(images[0].attributes('alt')).toBe('WaveSpeed')
|
||||
expect(images[1].attributes('alt')).toBe('Hunyuan')
|
||||
})
|
||||
|
||||
it('joins provider names with locale-aware conjunction for default label', () => {
|
||||
const wrapper = mountOverlay([{ provider: ['WaveSpeed', 'Hunyuan'] }])
|
||||
const span = wrapper.find('span')
|
||||
expect(span.text()).toBe('WaveSpeed and Hunyuan')
|
||||
})
|
||||
|
||||
it('uses custom label when provided', () => {
|
||||
const wrapper = mountOverlay([
|
||||
{ provider: ['WaveSpeed', 'Hunyuan'], label: 'Custom Label' }
|
||||
])
|
||||
const span = wrapper.find('span')
|
||||
expect(span.text()).toBe('Custom Label')
|
||||
})
|
||||
|
||||
it('applies negative gap for overlap effect', () => {
|
||||
const wrapper = mountOverlay([
|
||||
{ provider: ['WaveSpeed', 'Hunyuan'], gap: -8 }
|
||||
])
|
||||
const images = wrapper.findAll('img')
|
||||
expect(images[1].attributes('style')).toContain('margin-left: -8px')
|
||||
})
|
||||
|
||||
it('applies default gap when not specified', () => {
|
||||
const wrapper = mountOverlay([{ provider: ['WaveSpeed', 'Hunyuan'] }])
|
||||
const images = wrapper.findAll('img')
|
||||
expect(images[1].attributes('style')).toContain('margin-left: -6px')
|
||||
})
|
||||
|
||||
it('filters out invalid providers from stacked logos', () => {
|
||||
function getLogoUrl(provider: string) {
|
||||
return provider === 'WaveSpeed' ? '/logos/WaveSpeed.png' : ''
|
||||
}
|
||||
const wrapper = mount(LogoOverlay, {
|
||||
props: {
|
||||
logos: [{ provider: ['WaveSpeed', 'Unknown'] }],
|
||||
getLogoUrl
|
||||
}
|
||||
})
|
||||
expect(wrapper.findAll('img')).toHaveLength(1)
|
||||
expect(wrapper.find('span').text()).toBe('WaveSpeed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('error handling', () => {
|
||||
it('keeps showing remaining providers when one image fails in stacked logos', async () => {
|
||||
const wrapper = mountOverlay([{ provider: ['Google', 'OpenAI'] }])
|
||||
const images = wrapper.findAll('[data-testid="logo-img"]')
|
||||
expect(images).toHaveLength(2)
|
||||
|
||||
await images[0].trigger('error')
|
||||
await nextTick()
|
||||
|
||||
const remainingImages = wrapper.findAll('[data-testid="logo-img"]')
|
||||
expect(remainingImages).toHaveLength(2)
|
||||
expect(remainingImages[1].attributes('alt')).toBe('OpenAI')
|
||||
})
|
||||
})
|
||||
})
|
||||
117
src/components/templates/thumbnails/LogoOverlay.vue
Normal file
117
src/components/templates/thumbnails/LogoOverlay.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div
|
||||
v-for="logo in validLogos"
|
||||
:key="logo.key"
|
||||
:class="
|
||||
cn('pointer-events-none absolute z-10', logo.position ?? defaultPosition)
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-show="!hasAllFailed(logo.providers)"
|
||||
data-testid="logo-pill"
|
||||
class="flex items-center gap-1.5 rounded-full bg-black/20 py-1 pr-2"
|
||||
:style="{ opacity: logo.opacity ?? 0.85 }"
|
||||
>
|
||||
<div class="ml-0.5 flex items-center">
|
||||
<img
|
||||
v-for="(provider, providerIndex) in logo.providers"
|
||||
:key="provider"
|
||||
data-testid="logo-img"
|
||||
:src="logo.urls[providerIndex]"
|
||||
:alt="provider"
|
||||
class="h-6 w-6 rounded-full border-2 border-white object-cover"
|
||||
:class="{ relative: providerIndex > 0 }"
|
||||
:style="
|
||||
providerIndex > 0 ? { marginLeft: `${logo.gap ?? -6}px` } : {}
|
||||
"
|
||||
draggable="false"
|
||||
@error="onImageError(provider)"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-white">
|
||||
{{ logo.label }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { LogoInfo } from '@/platform/workflow/templates/types/template'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
function formatProviderList(providers: string[]): string {
|
||||
const localeValue = String(locale.value)
|
||||
try {
|
||||
return new Intl.ListFormat(localeValue, {
|
||||
style: 'long',
|
||||
type: 'conjunction'
|
||||
}).format(providers)
|
||||
} catch {
|
||||
return providers.join(t('templates.logoProviderSeparator'))
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
logos,
|
||||
getLogoUrl,
|
||||
defaultPosition = 'top-2 left-2'
|
||||
} = defineProps<{
|
||||
logos: LogoInfo[]
|
||||
getLogoUrl: (provider: string) => string
|
||||
defaultPosition?: string
|
||||
}>()
|
||||
|
||||
const failedLogos = ref(new Set<string>())
|
||||
|
||||
function onImageError(provider: string) {
|
||||
failedLogos.value = new Set([...failedLogos.value, provider])
|
||||
}
|
||||
|
||||
function hasAllFailed(providers: string[]): boolean {
|
||||
return providers.every((p) => failedLogos.value.has(p))
|
||||
}
|
||||
|
||||
interface ValidatedLogo {
|
||||
key: string
|
||||
providers: string[]
|
||||
urls: string[]
|
||||
label: string
|
||||
position: string | undefined
|
||||
opacity: number | undefined
|
||||
gap: number | undefined
|
||||
}
|
||||
|
||||
const validLogos = computed<ValidatedLogo[]>(() => {
|
||||
const result: ValidatedLogo[] = []
|
||||
|
||||
logos.forEach((logo, index) => {
|
||||
const providers = Array.isArray(logo.provider)
|
||||
? logo.provider
|
||||
: [logo.provider]
|
||||
const urls = providers.map((p) => getLogoUrl(p))
|
||||
const validProviders = providers.filter((_, i) => urls[i])
|
||||
const validUrls = urls.filter((url) => url)
|
||||
|
||||
if (validProviders.length === 0) return
|
||||
|
||||
const providerKey = validProviders.join('-')
|
||||
const layoutKey = `${logo.position ?? ''}-${logo.opacity ?? ''}-${logo.gap ?? ''}`
|
||||
result.push({
|
||||
key: providerKey ? `${providerKey}-${layoutKey}` : `logo-${index}`,
|
||||
providers: validProviders,
|
||||
urls: validUrls,
|
||||
label: logo.label ?? formatProviderList(validProviders),
|
||||
position: logo.position,
|
||||
opacity: logo.opacity,
|
||||
gap: logo.gap
|
||||
})
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { h } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterAll, beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { h } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'
|
||||
import Popover from 'primevue/popover'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it } from 'vite-plus/test'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import type { TopbarBadge as TopbarBadgeType } from '@/types/comfy'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it } from 'vite-plus/test'
|
||||
import { h, nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
@@ -119,26 +119,28 @@ describe('TagsInput with child components', () => {
|
||||
})
|
||||
|
||||
it('exits edit mode when clicking outside', async () => {
|
||||
const outsideElement = document.createElement('div')
|
||||
document.body.appendChild(outsideElement)
|
||||
const wrapper = mount<typeof TagsInput<string>>(TagsInput, {
|
||||
props: {
|
||||
modelValue: ['tag1']
|
||||
},
|
||||
slots: {
|
||||
default: () => h(TagsInputInput, { placeholder: 'Add tag...' })
|
||||
},
|
||||
attachTo: document.body
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.trigger('click')
|
||||
await nextTick()
|
||||
expect(wrapper.find('input').exists()).toBe(true)
|
||||
|
||||
document.body.click()
|
||||
outsideElement.dispatchEvent(new PointerEvent('click', { bubbles: true }))
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('input').exists()).toBe(false)
|
||||
|
||||
wrapper.unmount()
|
||||
outsideElement.remove()
|
||||
})
|
||||
|
||||
it('shows placeholder when modelValue is empty', async () => {
|
||||
|
||||
@@ -85,7 +85,7 @@ The following diagram shows how composables fit into the application architectur
|
||||
|
||||
## Composable Categories
|
||||
|
||||
The following tables list ALL composables in the system as of 2025-01-30:
|
||||
The following tables list ALL composables in the system as of 2026-01-30:
|
||||
|
||||
### Auth
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { t } from '@/i18n'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
@@ -13,8 +10,6 @@ export const useCurrentUser = () => {
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const commandStore = useCommandStore()
|
||||
const apiKeyStore = useApiKeyAuthStore()
|
||||
const dialogService = useDialogService()
|
||||
const { deleteAccount } = useFirebaseAuthActions()
|
||||
|
||||
const firebaseUser = computed(() => authStore.currentUser)
|
||||
const isApiKeyLogin = computed(() => apiKeyStore.isAuthenticated)
|
||||
@@ -116,18 +111,6 @@ export const useCurrentUser = () => {
|
||||
await commandStore.execute('Comfy.User.OpenSignInDialog')
|
||||
}
|
||||
|
||||
const handleDeleteAccount = async () => {
|
||||
const confirmed = await dialogService.confirm({
|
||||
title: t('auth.deleteAccount.confirmTitle'),
|
||||
message: t('auth.deleteAccount.confirmMessage'),
|
||||
type: 'delete'
|
||||
})
|
||||
|
||||
if (confirmed) {
|
||||
await deleteAccount()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loading: authStore.loading,
|
||||
isLoggedIn,
|
||||
@@ -141,7 +124,6 @@ export const useCurrentUser = () => {
|
||||
resolvedUserInfo,
|
||||
handleSignOut,
|
||||
handleSignIn,
|
||||
handleDeleteAccount,
|
||||
onUserResolved,
|
||||
onTokenRefreshed,
|
||||
onUserLogout
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user