Compare commits

..

99 Commits

Author SHA1 Message Date
DrJKL
1c48d41a88 fix: resolve lint errors and document describe naming convention
Amp-Thread-ID: https://ampcode.com/threads/T-019c4b83-5c21-714d-9c01-38e2e748c019
Co-authored-by: Amp <amp@ampcode.com>
2026-02-11 01:00:59 -08:00
DrJKL
2fc88abd59 Merge remote-tracking branch 'origin/main' into drjkl/I-have-a-ruler-and-I-know-how-to-use-it 2026-02-10 22:43:11 -08:00
DrJKL
b92fa9efe8 Merge remote-tracking branch 'origin/main' into drjkl/I-have-a-ruler-and-I-know-how-to-use-it 2026-02-10 22:37:20 -08:00
DrJKL
0a5af960d5 fix: replace props.* references with destructured prop variables
Amp-Thread-ID: https://ampcode.com/threads/T-019c4a8a-ada4-74ac-9496-ed1d43ee8ed2
Co-authored-by: Amp <amp@ampcode.com>
2026-02-10 18:51:56 -08:00
Alexander Brown
cdd8105b1a Merge branch 'main' into drjkl/I-have-a-ruler-and-I-know-how-to-use-it 2026-02-10 18:48:33 -08:00
DrJKL
53013d04ef fix: use component __name for describe() labels in Vue component tests
Vue component objects render as [object Object] when passed directly
to describe(). Use Component.__name ?? 'Component' to produce readable
suite labels in test reporters.

Fixes vitest/prefer-describe-function-title for 69 component test files.

Amp-Thread-ID: https://ampcode.com/threads/T-019c4a6c-7593-710e-bf99-02821f6b76ba
Co-authored-by: Amp <amp@ampcode.com>
2026-02-10 18:19:49 -08:00
Alexander Brown
18f3877cab fix: resolve new oxlint violations after upgrade
- Fix import ordering (absolute before relative imports)

- Fix no-immediate-mutation (Set.add/push/Object.assign after init)

- Migrate describe.each to describe.for

- Hoist vi.mock to top level in firebaseAuthStore test

- Fix unsafe optional chaining in subgraphStore

- Fix acceptTypes computed returning null instead of undefined

- Configure vue/return-in-computed-property treatUndefinedAsUnspecified

- Add oxlint-disable for incorrect prefer-describe-function-title auto-fixes

Amp-Thread-ID: https://ampcode.com/threads/T-019c495f-2269-701f-9a3f-c6fe378804ba
Co-authored-by: Amp <amp@ampcode.com>
2026-02-10 14:52:12 -08:00
Alexander Brown
21445f1faf deps: Update oxfmt and oxlint 2026-02-10 13:01:38 -08:00
Alexander Brown
6a1dcf8a1e Merge branch 'main' into drjkl/I-have-a-ruler-and-I-know-how-to-use-it 2026-02-10 12:46:06 -08:00
Alexander Brown
a13d28cc16 Merge branch 'main' into drjkl/I-have-a-ruler-and-I-know-how-to-use-it 2026-02-10 12:34:58 -08:00
Alexander Brown
2ed5618331 fix: move import above vi.mock() calls to satisfy import/first rule
Amp-Thread-ID: https://ampcode.com/threads/T-019c4174-f519-717b-9274-b17c24711353
Co-authored-by: Amp <amp@ampcode.com>
2026-02-09 00:16:35 -08:00
Alexander Brown
c4c65070e9 Merge remote-tracking branch 'origin/main' into drjkl/I-have-a-ruler-and-I-know-how-to-use-it 2026-02-09 00:13:16 -08:00
Alexander Brown
7ad917343e Merge branch 'main' into drjkl/I-have-a-ruler-and-I-know-how-to-use-it 2026-02-07 22:36:21 -08:00
Alexander Brown
25cc481e08 Merge branch 'main' into drjkl/I-have-a-ruler-and-I-know-how-to-use-it 2026-02-07 19:50:46 -08:00
Alexander Brown
8ff385fc4d feat: wrap async Preview3d component in Suspense boundary
Amp-Thread-ID: https://ampcode.com/threads/T-019c3b5a-1924-741a-9970-e14ef828eb46
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 19:45:46 -08:00
Alexander Brown
6ae2bc0e2a fix: handle floating promise in DropZone onDragDrop call
Amp-Thread-ID: https://ampcode.com/threads/T-019c3b58-626e-743f-8d69-5c4ff6bac504
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 19:44:11 -08:00
Alexander Brown
ae8940c0c0 fix: use defineAsyncComponent for Preview3d, clean up RAF state in minimap tests
Amp-Thread-ID: https://ampcode.com/threads/T-019c3b52-524e-770c-9752-1bcf3f5c6388
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 19:36:54 -08:00
Alexander Brown
6465c48423 fix: forward class prop via cn() in Select and SelectValue wrappers
Amp-Thread-ID: https://ampcode.com/threads/T-019c3b4d-3855-7669-877f-fc96de0f89b6
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 19:32:21 -08:00
Alexander Brown
6db94117b8 fix: misc cleanups and small fixes
- Remove duplicate console.error in TopMenuSection
- Use cn() for class merging in NoResultsPlaceholder
- Refactor UrlInput to use defineModel instead of manual v-model
- Use getElementById instead of querySelector for ID lookup in LGraphCanvas
- Fix LiteGraphGlobal.registerNodeType category when type has no slash
- Replace null with undefined in LGraphNode test mock computeds

Amp-Thread-ID: https://ampcode.com/threads/T-019c3b43-b81f-7028-b3d4-be4e08d63238
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 19:23:20 -08:00
Alexander Brown
58d82b789b fix: use Vue useId() instead of Math.random() for NodeSearchBox inputId
Amp-Thread-ID: https://ampcode.com/threads/T-019c3b39-c0e9-7116-9e25-0381624c4b82
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 19:10:23 -08:00
Alexander Brown
da43303ecf Fix merge issue. 2026-02-07 18:44:57 -08:00
Alexander Brown
e00e2848a8 fix: exclude JSONC files from strict JSON validation
.oxlintrc.json uses JSONC (comments), which jq cannot parse.

Amp-Thread-ID: https://ampcode.com/threads/T-019c3a27-819d-712a-8762-03ee5eb6e76c
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 18:37:41 -08:00
Alexander Brown
f2d5c415ae Merge branch 'main' into drjkl/I-have-a-ruler-and-I-know-how-to-use-it 2026-02-07 18:32:29 -08:00
Alexander Brown
0edd5b17e7 Merge issues 2026-02-07 14:11:53 -08:00
Alexander Brown
8376db4813 fix: use oxlint-disable-next-line instead of eslint-disable-next-line
Amp-Thread-ID: https://ampcode.com/threads/T-019c39e2-0b8a-725a-b765-28f091b790f4
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 14:00:34 -08:00
Alexander Brown
d9e9d68230 chore: disable high-violation rules pending incremental cleanup
7 rules set to off with TODO comments noting violation counts:

- no-param-reassign (104), prefer-destructuring (581)

- promise/prefer-await-to-callbacks (76), promise/prefer-await-to-then (91)

- unicorn/consistent-function-scoping (147), unicorn/no-array-for-each (165)

- typescript/prefer-nullish-coalescing (372)

Amp-Thread-ID: https://ampcode.com/threads/T-019c39e2-0b8a-725a-b765-28f091b790f4
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 14:00:31 -08:00
Alexander Brown
06b96b13b9 chore: promote 0-violation guardrail rules from warn to error
vitest/prefer-describe-function-title, unicorn/no-immediate-mutation,

promise/no-nesting, typescript/prefer-optional-chain

All had 0 violations. vitest/warn-todo kept as warn (intentional annotation).

Amp-Thread-ID: https://ampcode.com/threads/T-019c39e2-0b8a-725a-b765-28f091b790f4
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 14:00:27 -08:00
Alexander Brown
05c5d08acb chore: enable oxlint rule promise/prefer-await-to-callbacks
76 warnings across many files. Prefer async/await over callback patterns.

Too many violations for one pass; kept as warn for incremental cleanup.

Amp-Thread-ID: https://ampcode.com/threads/T-019c39e2-0b8a-725a-b765-28f091b790f4
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 14:00:24 -08:00
Alexander Brown
0aec287e83 chore: enable oxlint rule promise/prefer-await-to-then
91 warnings across the codebase. Kept as warn for incremental cleanup — .then() usage is widespread. Configured with strict: false.

Amp-Thread-ID: https://ampcode.com/threads/T-019c39dc-eeba-709e-9885-eb3d0a605157
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 14:00:21 -08:00
Alexander Brown
a2fdb2fcb2 chore: enable oxlint rule eslint/prefer-destructuring
Amp-Thread-ID: https://ampcode.com/threads/T-019c39d8-20d7-71d2-9feb-52961de2c1f0
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 14:00:17 -08:00
Alexander Brown
20b16009c7 chore: enable oxlint rule eslint/no-param-reassign
Enabled as warn. 104 warnings — too many to fix in one pass.

Disallows reassigning function parameters. Aligns with AGENTS.md immutability preference.

Amp-Thread-ID: https://ampcode.com/threads/T-019c39d6-cc76-7120-b787-8be5974594c2
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 14:00:15 -08:00
Alexander Brown
f13a88a64e chore: enable oxlint rule eslint/func-style
16 violations fixed: converted const fn = function() to function declarations (or arrow functions where type narrowing required it). Enabled as error.

Amp-Thread-ID: https://ampcode.com/threads/T-019c39cd-c141-776b-9285-eabd3d9ffacd
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 14:00:11 -08:00
Alexander Brown
31a1e14d2c chore: enable oxlint rule unicorn/prefer-set-has
5 violations fixed across 3 files — converted array+includes to Set+has for constant membership-check collections. All violations were small constant arrays used only for .includes() lookups, safe to convert.

Amp-Thread-ID: https://ampcode.com/threads/T-019c39c6-819f-77ef-809f-4a55da6b327c
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 14:00:07 -08:00
Alexander Brown
924db2b815 chore: enable oxlint rule unicorn/no-array-for-each
Enabled as warn with 165 violations. Enforces for...of over .forEach(). Too many violations for one pass; kept as warn for incremental cleanup. No fixes applied.

Amp-Thread-ID: https://ampcode.com/threads/T-019c39c4-ce2d-7532-b4d9-454b7220e93f
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 14:00:02 -08:00
Alexander Brown
e0090a5a4b chore: enable oxlint rule unicorn/consistent-function-scoping
147 warnings across many files. Configured with checkArrowFunctions: true.
Kept as warn due to high violation count (50+ threshold).
Enforces moving functions that don't capture outer scope to module level.

Amp-Thread-ID: https://ampcode.com/threads/T-019c39bf-3aef-723e-8f3a-d4c5c8648cff
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:59:57 -08:00
Alexander Brown
620ad24796 chore: enable oxlint rule typescript/prefer-optional-chain
0 violations found — guardrail rule enforcing foo?.bar over foo && foo.bar. Enabled as warn due to dangerous auto-fix semantics (optional chaining may change return types).

Amp-Thread-ID: https://ampcode.com/threads/T-019c39ba-188b-711c-a738-dcefc8c37a37
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:59:56 -08:00
Alexander Brown
ea0e6b9d2a chore: enable oxlint rule typescript/prefer-nullish-coalescing
372 warnings (0 errors). Enabled as warn with ignoreConditionalTests.

Too many violations for one pass - kept as warn for incremental cleanup.

Amp-Thread-ID: https://ampcode.com/threads/T-019c39b4-0744-73d6-925e-1ede662289f9
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:59:54 -08:00
Alexander Brown
fd78ec3077 chore: enable oxlint rule promise/param-names
38 violations fixed across 5 files. All were shorthand Promise param names (r, resolveFn/rejectFn, _) renamed to resolve/reject/_resolve/_reject.

Amp-Thread-ID: https://ampcode.com/threads/T-019c39af-8fe0-73ad-a7fd-1373cc0380db
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:59:52 -08:00
Alexander Brown
d02dfca1de chore: enable oxlint rule promise/prefer-catch
0 violations found — guardrail rule enforcing .catch(fn) over .then(null, fn) or .then(a, b).

Amp-Thread-ID: https://ampcode.com/threads/T-019c39aa-ceeb-723e-9660-29c0178b9e45
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:59:50 -08:00
Alexander Brown
81606328b8 chore: enable oxlint rule promise/no-nesting
Add promise plugin to oxlint config. Enable promise/no-nesting as warn.

0 no-nesting violations found. 1 auto-enabled no-callback-in-promise false positive fixed by renaming callback parameter in useMinimap.test.ts.

Amp-Thread-ID: https://ampcode.com/threads/T-019c39a3-7c77-75a0-9e4d-9e1310d099aa
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:59:47 -08:00
Alexander Brown
ee1d61b71b chore: enable oxlint rule prefer-template
61 violations across 39 files converted from string concatenation to template literals. Auto-fix listed as planned but not implemented — all fixes applied manually.

Amp-Thread-ID: https://ampcode.com/threads/T-019c3992-c9b1-753d-8f16-4712af1f1ee8
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:59:43 -08:00
Alexander Brown
f7b50067a6 chore: enable oxlint rule import/first
99 violations fixed across 35 files. Reordered imports so absolute imports (packages, @/ aliases) come before relative imports (./, ../). Configured with absolute-first option.

Amp-Thread-ID: https://ampcode.com/threads/T-019c3986-6dde-744d-84bf-8a7e445b9bf7
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:59:34 -08:00
Alexander Brown
00aa420049 chore: enable oxlint rule eslint/no-else-return
29 violations fixed across 25 files.
Configured with allowElseIf: false to also flatten else-if chains after returns.
19 auto-fixed by oxlint, 10 manually fixed (cascaded else-if/else chains).
Fixed broken auto-fix output in useConflictDetection.ts (dangling code block).

Amp-Thread-ID: https://ampcode.com/threads/T-019c3978-5586-76db-9c5b-25ca30371483
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:59:08 -08:00
Alexander Brown
eb883c507d chore: enable oxlint rule unicorn/prefer-array-find
0 violations found - guardrail rule enforcing .find() over .filter()[0]

Amp-Thread-ID: https://ampcode.com/threads/T-019c3973-c4a9-755c-9d6d-2a1ba5b448c6
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:59:05 -08:00
Alexander Brown
4aa40a560c chore: enable oxlint rule unicorn/prefer-add-event-listener
14 violations across 6 files. Converted .onX= assignments to addEventListener().

In useNodeFileInput.ts, extracted handler to named function for proper removeEventListener cleanup.

In litegraphService.ts, introduced const img to allow TypeScript narrowing inside Promise closure, removing 3 @ts-expect-error comments.

Amp-Thread-ID: https://ampcode.com/threads/T-019c38c9-c4b0-71f7-a19f-9b9c339bb99d
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:59:03 -08:00
Alexander Brown
f998fc0aab chore: enable oxlint rule unicorn/no-immediate-mutation
Severity: warn (guardrail). 0 violations found.
Amp-Thread-ID: https://ampcode.com/threads/T-019c38c5-639d-76a6-85e4-69e082fab2b8
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:58:59 -08:00
Alexander Brown
c03cd17c87 chore: enable oxlint rule unicorn/no-lonely-if
17 violations across 12 files. All manually fixed by merging nested if conditions into combined && expressions. No inline disables needed.

Notable: deduplicated redundant dialog_close_on_mouse_leave checks in LGraphCanvas.ts (outer and inner if both checked the same flag).
Amp-Thread-ID: https://ampcode.com/threads/T-019c38bd-b37e-702f-88c4-cac54c012fc8
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:58:38 -08:00
Alexander Brown
62108147c3 chore: enable oxlint rule unicorn/no-typeof-undefined
8 violations fixed across 5 files. All were safe property access on window/globalThis/local variables — converted typeof x === 'undefined' to x === undefined.

Amp-Thread-ID: https://ampcode.com/threads/T-019c38b7-1552-748f-b5f1-28558b9402e6
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:55:10 -08:00
Alexander Brown
94523defc1 chore: enable oxlint rule vitest/warn-todo
Severity: warn (annotation, not blocking)

0 violations found. 2 existing it.todo() calls will appear as warnings.

Amp-Thread-ID: https://ampcode.com/threads/T-019c38b2-c3b8-71fd-9681-66837588b9d7
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:55:09 -08:00
Alexander Brown
a4df681fb5 chore: enable oxlint rule vitest/consistent-test-filename
0 violations — guardrail rule enforcing .test.ts naming for test files in src/.

Amp-Thread-ID: https://ampcode.com/threads/T-019c38ae-48df-7394-b615-5201058b4cc6
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:55:07 -08:00
Alexander Brown
5f3fd7f7be chore: enable oxlint rule vitest/consistent-each-for
Guardrail rule enforcing .for() over .each() for parameterized tests.

0 violations found.

Amp-Thread-ID: https://ampcode.com/threads/T-019c38a9-e1b3-754a-8609-2665c0a27fc5
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:55:06 -08:00
Alexander Brown
7c18a5a185 chore: enable oxlint rule eslint/prefer-spread
0 violations found. Guardrail rule enforcing spread syntax over .apply() patterns (e.g. Math.max(...args) over Math.max.apply(Math, args)).

Amp-Thread-ID: https://ampcode.com/threads/T-019c38a5-8149-7258-b2de-011e60870c8b
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:55:04 -08:00
Alexander Brown
99bc407a1d chore: enable oxlint rule eslint/prefer-rest-params
0 violations found. Guardrail rule enforcing ...args over arguments object.

Amp-Thread-ID: https://ampcode.com/threads/T-019c389d-fb63-71b1-ab49-b5a1c8ee3447
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:55:01 -08:00
Alexander Brown
e60a475d8e chore: enable oxlint rule eslint/no-return-assign
29 violations fixed across 17 files. Converted arrow expression bodies with assignments to statement bodies, and removed return-value assignments from regular functions.

Amp-Thread-ID: https://ampcode.com/threads/T-019c3893-7b2f-74f2-98db-41bbdee782d6
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:55:00 -08:00
Alexander Brown
b56a028635 chore: enable oxlint rule eslint/no-new-func
Security guardrail — disallows new Function() (equivalent to eval).

0 violations found.

Amp-Thread-ID: https://ampcode.com/threads/T-019c388f-28d4-704a-a76b-c87393d45baa
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:57 -08:00
Alexander Brown
cc0bdf22e7 chore: enable oxlint rule eslint/preserve-caught-error
0 violations found — guardrail rule enforcing { cause: err } when re-throwing in catch blocks.

Amp-Thread-ID: https://ampcode.com/threads/T-019c388a-6748-75ba-9e5f-bacc83714f68
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:56 -08:00
Alexander Brown
82b4be3988 chore: enable oxlint rule eslint/no-useless-concat
0 violations found — guardrail rule preventing concatenation of adjacent string literals.

Amp-Thread-ID: https://ampcode.com/threads/T-019c3885-c77f-76dc-8f05-a1de1c72cf28
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:54 -08:00
Alexander Brown
5540d22f64 chore: enable oxlint rule eslint/no-throw-literal
13 violations, all in litegraph. Wrapped string/template literal throws with new Error().

Files: ContextMenu.ts, LiteGraphGlobal.ts, LGraph.ts, LGraphNode.ts, LGraphCanvas.ts
Amp-Thread-ID: https://ampcode.com/threads/T-019c387e-a22a-722b-a017-fac342453e76
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:53 -08:00
Alexander Brown
a3cef10edf chore: enable oxlint rule eslint/no-useless-call
2 violations found, both false positives in useCachedRequest.test.ts where .call(null) invokes an object method named 'call', not Function.prototype.call. Added inline disables with justification.

Amp-Thread-ID: https://ampcode.com/threads/T-019c3878-879e-75b2-8290-3006270b31d0
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:51 -08:00
Alexander Brown
deb9603b30 chore: enable oxlint rule unicorn/no-useless-collection-argument
9 violations fixed across 5 files.

Removed empty array args from Set/Map constructors (new Set([]) -> new Set()) and unnecessary ?? [] fallbacks (new Set(x ?? []) -> new Set(x)) since collection constructors handle undefined.

Amp-Thread-ID: https://ampcode.com/threads/T-019c3871-8533-745a-9f66-9641cacfc473
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:49 -08:00
Alexander Brown
82b83460fc chore: enable oxlint rule unicorn/no-useless-switch-case
15 violations across 11 files. All were empty case clauses falling through directly to default — removed the redundant case labels.

Amp-Thread-ID: https://ampcode.com/threads/T-019c386a-7f3e-7141-ac49-cd6203103ef4
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:47 -08:00
Alexander Brown
cd3b1c5afe chore: enable oxlint rule unicorn/no-this-assignment
4 violations in litegraph code. Refactored prompt close() and showSearchBox close() to arrow functions. Inline-disabled 3 where hoisted function declarations genuinely need outer this (ContextMenu inner_onclick, showConnectionMenu inner_clicked, showSearchBox select/refreshHelper).

Amp-Thread-ID: https://ampcode.com/threads/T-019c385c-c19d-7298-9572-7651ebbf68a0
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:45 -08:00
Alexander Brown
aa7e59deaa chore: enable oxlint rule unicorn/no-abusive-eslint-disable
0 violations found. Guardrail rule disallowing blanket eslint-disable/oxlint-disable comments without specifying rule names.

Amp-Thread-ID: https://ampcode.com/threads/T-019c3857-b5df-7192-886e-dcbf9cc8357a
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:43 -08:00
Alexander Brown
4fc40e0039 chore: enable oxlint rule unicorn/error-message
0 violations found — guardrail rule requiring message arg when constructing Error/TypeError/etc.

Amp-Thread-ID: https://ampcode.com/threads/T-019c3852-b601-732e-b86b-a85266327c90
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:41 -08:00
Alexander Brown
ac5c84b514 chore: enable oxlint rule vitest/no-conditional-tests
0 violations found. Guardrail rule disallowing if/ternary wrapping it/test/describe blocks.

Amp-Thread-ID: https://ampcode.com/threads/T-019c384e-648e-7299-8539-91a298c83abc
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:39 -08:00
Alexander Brown
0b1c2217e5 chore: enable oxlint rule vitest/hoisted-apis-on-top
0 violations found — guardrail rule ensuring vi.mock, vi.unmock, vi.hoisted are at file top level.

Amp-Thread-ID: https://ampcode.com/threads/T-019c3846-699e-7777-b31b-baa3e0a92c43
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:38 -08:00
Alexander Brown
2269275bd9 chore: enable oxlint rule eslint/no-unneeded-ternary
5 violations fixed across 3 files:

- measure.ts: condition ? false : true → !(condition)

- LGraphCanvas.ts: condition ? false : true → negated conditions

- coreSettings.ts (3): isCloud ? false : true → !isCloud, isCloud ? true : false → isCloud

Configured with defaultAssignment: false.

Amp-Thread-ID: https://ampcode.com/threads/T-019c3840-1a10-763b-9a6f-2e14e29395a9
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:36 -08:00
Alexander Brown
37f8b73cfd chore: enable oxlint rule eslint/operator-assignment
1 violation fixed: currentStep.value = currentStep.value - 1 → currentStep.value -= 1 in useUploadModelWizard.ts

Amp-Thread-ID: https://ampcode.com/threads/T-019c383a-a0af-7187-be7c-a9dbcbb20ced
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:34 -08:00
Alexander Brown
7741a9bbb2 chore: enable oxlint rule eslint/yoda
Disallows Yoda conditions (e.g. 'red' === value). Configured with 'never' and exceptRange: true. 0 violations found — guardrail rule.

Amp-Thread-ID: https://ampcode.com/threads/T-019c3836-2e2e-75bc-9d0d-36420bdd8fad
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:33 -08:00
Alexander Brown
19bcbce4a6 chore: enable oxlint rule eslint/prefer-object-has-own
5 violations auto-fixed across 3 files. Replaced Object.prototype.hasOwnProperty.call() with Object.hasOwn().

Amp-Thread-ID: https://ampcode.com/threads/T-019c382f-0e79-7471-b0cc-6c6432e7ce6b
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:31 -08:00
Alexander Brown
53fa5c22c1 chore: enable oxlint rule eslint/prefer-object-spread
5 violations across 3 files. 3 auto-fixed to object spread, 2 inline-disabled (LiteGraph class instance spread loses methods; array spread overwrites length/Symbol.iterator). Removed unused DefaultOptions and HasShowSearchCallback types. Removed redundant position default (always overwritten by required optPass.position).

Amp-Thread-ID: https://ampcode.com/threads/T-019c3826-0f23-70fe-ac56-513f4f83c86d
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:29 -08:00
Alexander Brown
8d53bbf263 chore: enable oxlint rule eslint/prefer-const
0 violations — guardrail rule enforcing const when never reassigned.

Amp-Thread-ID: https://ampcode.com/threads/T-019c3821-72c3-75bc-8d82-058490bade7a
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:27 -08:00
Alexander Brown
0d274344a1 chore: enable oxlint rule eslint/no-var
0 violations found — var is not used in src/. Guardrail rule.

Amp-Thread-ID: https://ampcode.com/threads/T-019c381d-3e37-7629-a0a6-423f01fcac19
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:25 -08:00
Alexander Brown
e6ec331a71 chore: enable oxlint rule eslint/no-useless-constructor
1 violation fixed: removed empty constructor from mock class in useSelectedLiteGraphItems.test.ts.

Amp-Thread-ID: https://ampcode.com/threads/T-019c381b-011d-725f-8b44-1616bbb37dfe
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:25 -08:00
Alexander Brown
8c38d8a5de chore: enable oxlint rule eslint/eqeqeq
283 violations fixed across 31 files. Configured with [always, {null: ignore}] to allow idiomatic == null checks.

Added String() coercion for NodeId comparisons against string proxy widget IDs. Replaced @ts-expect-error directives with proper (e.target as Element) casts in LGraphCanvas.ts.

Amp-Thread-ID: https://ampcode.com/threads/T-019c3805-11cc-7475-80bb-47de0d690fc4
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:23 -08:00
Alexander Brown
9f525bb540 chore: enable oxlint rule unicorn/no-negation-in-equality-check
0 violations found. Guardrail against bugs like !foo === bar.

Amp-Thread-ID: https://ampcode.com/threads/T-019c3800-f5ff-740f-9d31-c32bbd842a7d
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:21 -08:00
Alexander Brown
25a25efbb2 chore: enable oxlint rule typescript/prefer-ts-expect-error
0 violations found — no @ts-ignore usages exist in src/.

This rule ensures @ts-expect-error is used instead of @ts-ignore, which is safer because it errors when the suppression is no longer needed.

Amp-Thread-ID: https://ampcode.com/threads/T-019c37ff-d1fc-72ae-8064-4f5546a78c38
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:19 -08:00
Alexander Brown
933fc35f02 chore: enable oxlint rule unicorn/prefer-classlist-toggle
1 violation in GraphView.vue: if/else classList.add/remove converted to classList.toggle('dark-theme', !light_theme)

Amp-Thread-ID: https://ampcode.com/threads/T-019c37fa-bed2-7283-98ca-a2c7d5c3aa91
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:17 -08:00
Alexander Brown
27a5701636 chore: enable oxlint rule unicorn/prefer-spread
86 violations fixed across 59 files. Converted Array.from(x) to [...x], .concat() to spread, and .slice() to spread.

3 inline disables: 2 in useBrushDrawing.ts (ArrayBuffer not iterable), 1 in NodeSettings.vue (spread widens union type).

Amp-Thread-ID: https://ampcode.com/threads/T-019c37f1-73b2-7147-90b3-282867667e38
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:15 -08:00
Alexander Brown
af48faee96 chore: enable oxlint rule unicorn/prefer-query-selector
11 violations auto-fixed across 8 files. Converted getElementById to querySelector and getElementsByTagName to querySelectorAll/querySelector. Added generic type params to querySelector calls where needed for type safety. Updated 2 test spies in SignInForm.test.ts to use scoped querySelector mocks.

Amp-Thread-ID: https://ampcode.com/threads/T-019c37de-071a-7047-9ac3-585e2082c082
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:13 -08:00
Alexander Brown
4c5e213cd5 chore: enable oxlint rule unicorn/no-useless-undefined
84 violations fixed across 39 files. Configured with
checkArguments: false to avoid conflicts with TypeScript
function signatures requiring explicit undefined args.

Resolved 11 vue/return-in-computed-property eslint conflicts
by restructuring computed properties to use ternary expressions
or lookup objects instead of bare returns. 1 inline disable
used where restructuring was impractical.

Amp-Thread-ID: https://ampcode.com/threads/T-019c37c4-8030-7088-b95c-2ef35bbec64e
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:10 -08:00
Alexander Brown
a98d01b2a2 chore: enable oxlint rule unicorn/prefer-math-min-max
1 violation in queueStore.ts: ternary comparison replaced with Math.max(idx, 0)

Amp-Thread-ID: https://ampcode.com/threads/T-019c37bf-46e1-772b-aafa-dcc5824d21c3
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:08 -08:00
Alexander Brown
8fe2ed3efe chore: enable oxlint rule unicorn/prefer-prototype-methods
0 violations found. Rule enforces Array.prototype.slice.apply() over [].slice.apply().

Amp-Thread-ID: https://ampcode.com/threads/T-019c37bb-4101-756f-a11a-43929dd76f58
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:05 -08:00
Alexander Brown
e981c38df7 chore: enable oxlint rule unicorn/prefer-type-error
1 violation fixed in useComboWidget.test.ts: Error -> TypeError after typeof check.

Amp-Thread-ID: https://ampcode.com/threads/T-019c37b9-2b26-71eb-9e39-834413472473
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:54:05 -08:00
Alexander Brown
a516c1cb45 chore: enable oxlint rule unicorn/prefer-string-slice
24 violations auto-fixed across 17 files.

All .substring()/.substr() calls converted to .slice() — safe because all arguments are non-negative.

Amp-Thread-ID: https://ampcode.com/threads/T-019c37b2-17fa-74f5-abae-f4e915c7a9a5
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:53:01 -08:00
Alexander Brown
7821b69ef7 chore: enable oxlint rule unicorn/prefer-string-replace-all
38 violations auto-fixed across 22 files. Replaces .replace(/regex/g, ...) with .replaceAll(string, ...) or .replaceAll(/regex/g, ...) as appropriate.

Amp-Thread-ID: https://ampcode.com/threads/T-019c37a9-c58c-77fe-bbac-f0344f9debaf
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:52:42 -08:00
Alexander Brown
6a420f2896 chore: enable oxlint rule unicorn/prefer-regexp-test
2 violations in surveyNormalization.ts: .match() in boolean context replaced with RegExp#test().

Amp-Thread-ID: https://ampcode.com/threads/T-019c37a3-cf5d-71ce-94b8-35b49cb38319
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:52:39 -08:00
Alexander Brown
2d20de7e1f chore: enable oxlint rule unicorn/no-length-as-slice-end
0 violations found — rule acts as a guardrail for future code.

Amp-Thread-ID: https://ampcode.com/threads/T-019c379f-7f8d-7369-bfce-97aba5462e40
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:52:37 -08:00
Alexander Brown
4e9dc97ad5 chore: enable oxlint rule unicorn/prefer-array-flat-map
2 violations auto-fixed: .map().flat() converted to .flatMap() in SelectionToolbox.vue and serverConfigStore.ts

Amp-Thread-ID: https://ampcode.com/threads/T-019c3797-72f6-77f4-b097-999543928f71
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:52:36 -08:00
Alexander Brown
bb6ad22003 chore: enable oxlint rule unicorn/no-instanceof-array
0 violations found. Rule enforces Array.isArray() over instanceof Array.

Amp-Thread-ID: https://ampcode.com/threads/T-019c3792-8934-7763-ab09-70ff08764435
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:52:35 -08:00
Alexander Brown
febc1951d4 chore: enable oxlint rule unicorn/catch-error-name
93 violations found, 92 auto-fixed, 1 inline-disabled (nested catch
in useErrorHandling.ts where outer scope already uses `error`).

Configured with `ignore: ["^error\\w+$"]` to allow `errorCaught` as
a catch variable name where renaming to `error` would shadow a
reactive `error` ref in the same scope (common pattern in composables
and stores).

Amp-Thread-ID: https://ampcode.com/threads/T-019c3782-789b-75a8-9653-d5e030adda1d
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:52:29 -08:00
Alexander Brown
d012682dec chore: enable oxlint rule unicorn/prefer-optional-catch-binding
21 violations auto-fixed across 17 files. All were unused catch binding parameters (e.g. catch (error) -> catch).

Amp-Thread-ID: https://ampcode.com/threads/T-019c377b-4568-7288-94da-08ca003caa04
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:39:33 -08:00
Alexander Brown
826f4b1d80 chore: enable oxlint rule unicorn/prefer-string-trim-start-end
0 violations found. Guardrail rule to enforce trimStart()/trimEnd() over trimLeft()/trimRight().

Amp-Thread-ID: https://ampcode.com/threads/T-019c3776-cdd3-747b-9fa7-013a5f364282
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:39:32 -08:00
Alexander Brown
832b34c381 chore: enable oxlint rule unicorn/throw-new-error
4 violations in src/utils/linkFixer.ts, all auto-fixed (added missing 'new' keyword).

Amp-Thread-ID: https://ampcode.com/threads/T-019c3771-486f-727c-af65-8f53c9d3142a
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:39:31 -08:00
Alexander Brown
74dbad2404 chore: enable oxlint rule vitest/prefer-describe-function-title
Enabled as warn severity. No violations found (0 warnings, 0 errors).

This rule auto-fixes describe('fnName', ...) to describe(fnName, ...) when fnName is an imported symbol, using the function reference as the title.

Amp-Thread-ID: https://ampcode.com/threads/T-019c376c-efce-716a-84a7-4b9070b42ac4
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:39:29 -08:00
Alexander Brown
aaf33ce6ad chore: enable oxlint rule vitest/consistent-vitest-vi
0 violations found. Enforces vi.* over vitest.* in test files.

Amp-Thread-ID: https://ampcode.com/threads/T-019c3768-5c10-72bf-a87e-13da5999876c
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:39:27 -08:00
Alexander Brown
0bff3fe7ea feat: migrate all defineProps to reactive destructured pattern
Convert all 120 `const props = defineProps<...>()` and bare
`defineProps<...>()` usages to Vue 3.5 reactive destructured props.
Update all `props.X` references to direct destructured names in both
script and template sections.

Upgrade `vue/define-props-destructuring` oxlint rule from "warn" to
"error" to enforce the pattern going forward.

Amp-Thread-ID: https://ampcode.com/threads/T-019c3716-74a2-7347-8b74-ad6ce2d8c9a6
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:39:27 -08:00
Alexander Brown
cd70d7a576 feat: add vue/define-props-destructuring lint rule as warning
Amp-Thread-ID: https://ampcode.com/threads/T-019c3676-05f6-76e3-b673-165fa08c1b46
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:39:24 -08:00
Alexander Brown
e974f88e86 refactor: migrate withDefaults to reactive destructured props
Replace all 14 usages of withDefaults(defineProps<...>(), {...}) with
Vue 3.5 reactive destructured props pattern across the codebase.

- Drop redundant `undefined` defaults (already the default for optional props)
- Rename `duration` computed to `effectiveDuration` in TransitionCollapse
  to avoid shadowing the destructured prop
- Remove phantom `runningNodeName` default in QueueJobItem (not in type)

Amp-Thread-ID: https://ampcode.com/threads/T-019c3676-05f6-76e3-b673-165fa08c1b46
Co-authored-by: Amp <amp@ampcode.com>
2026-02-07 13:39:21 -08:00
689 changed files with 5245 additions and 5602 deletions

View File

@@ -21,6 +21,7 @@
"eslint",
"import",
"oxc",
"promise",
"typescript",
"unicorn",
"vitest",
@@ -28,6 +29,12 @@
],
"rules": {
"no-async-promise-executor": "off",
"no-else-return": [
"error",
{
"allowElseIf": false
}
],
"no-console": [
"error",
{
@@ -35,8 +42,29 @@
}
],
"no-control-regex": "off",
"eqeqeq": [
"error",
"always",
{
"null": "ignore"
}
],
"func-style": [
"error",
"declaration",
{
"allowArrowFunctions": true
}
],
"no-eval": "off",
"no-new-func": "error",
// TODO: Enable and fix 104 violations
"no-param-reassign": "off",
"no-redeclare": "error",
"no-return-assign": ["error", "always"],
"no-throw-literal": "error",
"no-useless-constructor": "error",
"no-var": "error",
"no-restricted-imports": [
"error",
{
@@ -64,15 +92,66 @@
]
}
],
"no-unneeded-ternary": [
"error",
{
"defaultAssignment": false
}
],
"no-useless-call": "error",
"no-useless-concat": "error",
"prefer-const": "error",
// TODO: Enable and fix 581 violations
"prefer-destructuring": "off",
"prefer-object-has-own": "error",
"prefer-object-spread": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"prefer-template": "error",
"promise/no-nesting": "error",
"promise/param-names": "error",
// TODO: Enable and fix 76 violations
"promise/prefer-await-to-callbacks": "off",
// TODO: Enable and fix 91 violations
"promise/prefer-await-to-then": "off",
"promise/prefer-catch": "error",
"preserve-caught-error": "error",
"yoda": [
"error",
"never",
{
"exceptRange": true
}
],
"no-self-assign": "allow",
"no-unused-expressions": "off",
"no-unused-private-class-members": "off",
"no-useless-rename": "off",
"operator-assignment": ["error", "always"],
"import/default": "error",
"import/export": "error",
"import/first": ["error", "absolute-first"],
"import/namespace": "error",
"import/no-duplicates": "error",
"import/consistent-type-specifier-style": ["error", "prefer-top-level"],
"vitest/consistent-each-for": [
"error",
{
"test": "for",
"describe": "for"
}
],
"vitest/consistent-test-filename": [
"error",
{
"pattern": ".*\\.test\\.ts$"
}
],
"vitest/consistent-vitest-vi": "error",
"vitest/warn-todo": "warn",
"vitest/hoisted-apis-on-top": "error",
"vitest/no-conditional-tests": "error",
"vitest/prefer-describe-function-title": "error",
"jest/expect-expect": "off",
"jest/no-conditional-expect": "off",
"jest/no-disabled-tests": "off",
@@ -82,11 +161,55 @@
"typescript/no-unnecessary-parameter-property-assignment": "off",
"typescript/no-unsafe-declaration-merging": "off",
"typescript/no-unused-vars": "off",
"unicorn/catch-error-name": [
"error",
{
"ignore": ["^error\\w+$"]
}
],
// TODO: Enable and fix 147 violations
"unicorn/consistent-function-scoping": "off",
"unicorn/error-message": "error",
"unicorn/no-abusive-eslint-disable": "error",
// TODO: Enable and fix 165 violations
"unicorn/no-array-for-each": "off",
"unicorn/no-immediate-mutation": "error",
"unicorn/no-instanceof-array": "error",
"unicorn/no-length-as-slice-end": "error",
"unicorn/no-lonely-if": "error",
"unicorn/no-negation-in-equality-check": "error",
"unicorn/no-typeof-undefined": "error",
"unicorn/prefer-math-min-max": "error",
"unicorn/prefer-array-flat-map": "error",
"unicorn/no-empty-file": "off",
"unicorn/no-new-array": "off",
"unicorn/prefer-add-event-listener": "error",
"unicorn/prefer-array-find": "error",
"unicorn/no-useless-undefined": [
"error",
{
"checkArguments": false,
"checkArrowFunctionBody": false
}
],
"unicorn/prefer-classlist-toggle": "error",
"unicorn/no-single-promise-in-promise-methods": "off",
"unicorn/no-this-assignment": "error",
"unicorn/no-useless-collection-argument": "error",
"unicorn/no-useless-switch-case": "error",
"unicorn/no-useless-fallback-in-spread": "off",
"unicorn/no-useless-spread": "off",
"unicorn/prefer-optional-catch-binding": "error",
"unicorn/prefer-prototype-methods": "error",
"unicorn/prefer-query-selector": "error",
"unicorn/prefer-spread": "error",
"unicorn/prefer-regexp-test": "error",
"unicorn/prefer-set-has": "error",
"unicorn/prefer-string-replace-all": "error",
"unicorn/prefer-string-slice": "error",
"unicorn/prefer-string-trim-start-end": "error",
"unicorn/prefer-type-error": "error",
"unicorn/throw-new-error": "error",
"typescript/await-thenable": "off",
"typescript/no-base-to-string": "off",
"typescript/no-duplicate-type-constituents": "off",
@@ -96,7 +219,12 @@
"typescript/restrict-template-expressions": "off",
"typescript/unbound-method": "off",
"typescript/no-floating-promises": "error",
"typescript/no-explicit-any": "error",
// TODO: Enable and fix 372 violations (use { "ignoreConditionalTests": true })
"typescript/prefer-nullish-coalescing": "off",
// TODO: Enable and fix violations
"typescript/prefer-optional-chain": "off",
"typescript/prefer-ts-expect-error": "error",
"vue/define-props-destructuring": "error",
"vue/no-import-compiler-macros": "error",
"vue/no-dupe-keys": "error"
},
@@ -115,7 +243,8 @@
"no-control-regex": "error",
"no-useless-rename": "error",
"no-unused-private-class-members": "error",
"unicorn/no-empty-file": "error"
"unicorn/no-empty-file": "error",
"vitest/consistent-test-filename": "off"
}
}
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -12,6 +12,18 @@ This guide covers patterns and examples for testing Vue components in the ComfyU
6. [Asynchronous Component Testing](#asynchronous-component-testing)
7. [Working with Vue Reactivity](#working-with-vue-reactivity)
## Describe Block Naming
Use `Component.__name ?? 'ComponentName'` for the top-level `describe` title. This passes the function reference (satisfying the `prefer-describe-function-title` lint rule) while providing a readable fallback:
```typescript
import MyComponent from './MyComponent.vue'
describe(MyComponent.__name ?? 'MyComponent', () => {
// ...
})
```
## Basic Component Testing
Basic approach to testing a component's rendering and structure:
@@ -21,7 +33,7 @@ Basic approach to testing a component's rendering and structure:
import { mount } from '@vue/test-utils'
import SidebarIcon from './SidebarIcon.vue'
describe('SidebarIcon', () => {
describe(SidebarIcon.__name ?? 'SidebarIcon', () => {
const exampleProps = {
icon: 'pi pi-cog',
selected: false

View File

@@ -138,6 +138,10 @@ export default defineConfig([
'import-x/no-useless-path-segments': 'error',
'import-x/no-relative-packages': 'error',
'unused-imports/no-unused-imports': 'error',
'vue/return-in-computed-property': [
'error',
{ treatUndefinedAsUnspecified: false }
],
'vue/no-v-html': 'off',
// Prohibit dark-theme: and dark: prefixes
'vue/no-restricted-class': ['error', '/^dark(-theme)?:/'],

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 9">
<path d="M1.82148 8.68376C1.61587 8.68376 1.44996 8.60733 1.34177 8.46284C1.23057 8.31438 1.20157 8.10711 1.26219 7.89434L1.50561 7.03961C1.52502 6.97155 1.51151 6.89831 1.46918 6.8417C1.42684 6.7852 1.3606 6.75194 1.29025 6.75194H0.590376C0.384656 6.75194 0.21875 6.67562 0.110614 6.53113C-0.000591531 6.38256 -0.0295831 6.17529 0.0310774 5.96252L0.867308 3.03952L0.959638 2.71838C1.08375 2.28258 1.53638 1.9284 1.96878 1.9284H2.80622C2.90615 1.9284 2.99406 1.86177 3.02157 1.76508L3.29852 0.79284C3.4225 0.357484 3.87514 0.0033043 4.30753 0.0033043L6.09854 0.000112775L7.40967 0C7.61533 0 7.78124 0.0763259 7.88937 0.220813C8.00058 0.369269 8.02957 0.576538 7.96895 0.78931L7.59405 2.10572C7.4701 2.54096 7.01746 2.89503 6.58507 2.89503L4.79008 2.89844H3.95292C3.8531 2.89844 3.7653 2.96496 3.73762 3.06155L3.03961 5.49964C3.02008 5.56781 3.03359 5.64127 3.07604 5.69787C3.11837 5.75437 3.18461 5.78763 3.2549 5.78763C3.25507 5.78763 4.44105 5.78532 4.44105 5.78532H5.7483C5.95396 5.78532 6.11986 5.86164 6.228 6.00613C6.33921 6.1547 6.3682 6.36197 6.30754 6.57474L5.93263 7.89092C5.80869 8.32628 5.35605 8.68034 4.92366 8.68034L3.12872 8.68376H1.82148Z" fill="#8A8A8A"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

477
pnpm-lock.yaml generated
View File

@@ -205,14 +205,14 @@ catalogs:
specifier: 22.2.6
version: 22.2.6
oxfmt:
specifier: ^0.26.0
version: 0.26.0
specifier: ^0.31.0
version: 0.31.0
oxlint:
specifier: ^1.33.0
version: 1.33.0
specifier: ^1.46.0
version: 1.46.0
oxlint-tsgolint:
specifier: ^0.9.1
version: 0.9.1
specifier: ^0.11.5
version: 0.11.5
picocolors:
specifier: ^1.1.1
version: 1.1.1
@@ -650,13 +650,13 @@ importers:
version: 22.2.6
oxfmt:
specifier: 'catalog:'
version: 0.26.0
version: 0.31.0
oxlint:
specifier: 'catalog:'
version: 1.33.0(oxlint-tsgolint@0.9.1)
version: 1.46.0(oxlint-tsgolint@0.11.5)
oxlint-tsgolint:
specifier: 'catalog:'
version: 0.9.1
version: 0.11.5
picocolors:
specifier: 'catalog:'
version: 1.1.1
@@ -2626,113 +2626,269 @@ packages:
cpu: [x64]
os: [win32]
'@oxfmt/darwin-arm64@0.26.0':
resolution: {integrity: sha512-AAGc+8CffkiWeVgtWf4dPfQwHEE5c/j/8NWH7VGVxxJRCZFdmWcqCXprvL2H6qZFewvDLrFbuSPRCqYCpYGaTQ==}
'@oxfmt/binding-android-arm-eabi@0.31.0':
resolution: {integrity: sha512-2A7s+TmsY7xF3yM0VWXq2YJ82Z7Rd7AOKraotyp58Fbk7q9cFZKczW6Zrz/iaMaJYfR/UHDxF3kMR11vayflug==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [android]
'@oxfmt/binding-android-arm64@0.31.0':
resolution: {integrity: sha512-3ppKOIf2lQv/BFhRyENWs/oarueppCEnPNo0Az2fKkz63JnenRuJPoHaGRrMHg1oFMUitdYy+YH29Cv5ISZWRQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@oxfmt/binding-darwin-arm64@0.31.0':
resolution: {integrity: sha512-eFhNnle077DPRW6QPsBtl/wEzPoqgsB1LlzDRYbbztizObHdCo6Yo8T0hew9+HoYtnVMAP19zcRE7VG9OfqkMw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@oxfmt/darwin-x64@0.26.0':
resolution: {integrity: sha512-xFx5ijCTjw577wJvFlZEMmKDnp3HSCcbYdCsLRmC5i3TZZiDe9DEYh3P46uqhzj8BkEw1Vm1ZCWdl48aEYAzvQ==}
'@oxfmt/binding-darwin-x64@0.31.0':
resolution: {integrity: sha512-9UQSunEqokhR1WnlQCgJjkjw13y8PSnBvR98L78beGudTtNSaPMgwE7t/T0IPDibtDTxeEt+IQVKoQJ+8Jo6Lg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@oxfmt/linux-arm64-gnu@0.26.0':
resolution: {integrity: sha512-GubkQeQT5d3B/Jx/IiR7NMkSmXrCZcVI0BPh1i7mpFi8HgD1hQ/LbhiBKAMsMqs5bbugdQOgBEl8bOhe8JhW1g==}
'@oxfmt/binding-freebsd-x64@0.31.0':
resolution: {integrity: sha512-FHo7ITkDku3kQ8/44nU6IGR1UNH22aqYM3LV2ytV40hWSMVllXFlM+xIVusT+I/SZBAtuFpwEWzyS+Jn4TkkAQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@oxfmt/binding-linux-arm-gnueabihf@0.31.0':
resolution: {integrity: sha512-o1NiDlJDO9SOoY5wH8AyPUX60yQcOwu5oVuepi2eetArBp0iFF9qIH1uLlZsUu4QQ6ywqxcJSMjXCqGKC5uQFg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@oxfmt/binding-linux-arm-musleabihf@0.31.0':
resolution: {integrity: sha512-VXiRxlBz7ivAIjhnnVBEYdjCQ66AsjM0YKxYAcliS0vPqhWKiScIT61gee0DPCVaw1XcuW8u19tfRwpfdYoreg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@oxfmt/binding-linux-arm64-gnu@0.31.0':
resolution: {integrity: sha512-ryGPOtPViNcjs8N8Ap+wn7SM6ViiLzR9f0Pu7yprae+wjl6qwnNytzsUe7wcb+jT43DJYmvemFqE8tLVUavYbQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
'@oxfmt/linux-arm64-musl@0.26.0':
resolution: {integrity: sha512-OEypUwK69bFPj+aa3/LYCnlIUPgoOLu//WNcriwpnWNmt47808Ht7RJSg+MNK8a7pSZHpXJ5/E6CRK/OTwFdaQ==}
'@oxfmt/binding-linux-arm64-musl@0.31.0':
resolution: {integrity: sha512-BA3Euxp4bfd+AU3cKPgmHL44BbuBtmQTyAQoVDhX/nqPgbS/auoGp71uQBE4SAPTsQM/FcXxfKmCAdBS7ygF9w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
'@oxfmt/linux-x64-gnu@0.26.0':
resolution: {integrity: sha512-xO6iEW2bC6ZHyOTPmPWrg/nM6xgzyRPaS84rATy6F8d79wz69LdRdJ3l/PXlkqhi7XoxhvX4ExysA0Nf10ZZEQ==}
'@oxfmt/binding-linux-ppc64-gnu@0.31.0':
resolution: {integrity: sha512-wIiKHulVWE9s6PSftPItucTviyCvjugwPqEyUl1VD47YsFqa5UtQTknBN49NODHJvBgO+eqqUodgRqmNMp3xyw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-riscv64-gnu@0.31.0':
resolution: {integrity: sha512-6cM8Jt54bg9V/JoeUWhwnzHAS9Kvgc0oFsxql8PVs/njAGs0H4r+GEU4d+LXZPwI3b3ZUuzpbxlRJzer8KW+Cg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-riscv64-musl@0.31.0':
resolution: {integrity: sha512-d+b05wXVRGaO6gobTaDqUdBvTXwYc0ro7k1UVC37k4VimLRQOzEZqTwVinqIX3LxTaFCmfO1yG00u9Pct3AKwQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@oxfmt/binding-linux-s390x-gnu@0.31.0':
resolution: {integrity: sha512-Q+i2kj8e+two9jTZ3vxmxdNlg++qShe1ODL6xV4+Qt6SnJYniMxfcqphuXli4ft270kuHqd8HSVZs84CsSh1EA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-x64-gnu@0.31.0':
resolution: {integrity: sha512-F2Z5ffj2okhaQBi92MylwZddKvFPBjrsZnGvvRmVvWRf8WJ0WkKUTtombDgRYNDgoW7GBUUrNNNgWhdB7kVjBA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
'@oxfmt/linux-x64-musl@0.26.0':
resolution: {integrity: sha512-Z3KuZFC+MIuAyFCXBHY71kCsdRq1ulbsbzTe71v+hrEv7zVBn6yzql+/AZcgfIaKzWO9OXNuz5WWLWDmVALwow==}
'@oxfmt/binding-linux-x64-musl@0.31.0':
resolution: {integrity: sha512-Vz7dZQd1yhE5wTWujGanPmZgDtzLZS1PQoeMmUj89p4eMTmpIkvWaIr3uquJCbh8dQd5cPZrFvMmdDgcY5z+GA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
'@oxfmt/win32-arm64@0.26.0':
resolution: {integrity: sha512-3zRbqwVWK1mDhRhTknlQFpRFL9GhEB5GfU6U7wawnuEwpvi39q91kJ+SRJvJnhyPCARkjZBd1V8XnweN5IFd1g==}
'@oxfmt/binding-openharmony-arm64@0.31.0':
resolution: {integrity: sha512-nm0gus6R5V9tM1XaELiiIduUzmdBuCefkwToWKL4UtuFoMCGkigVQnbzHwPTGLVWOEF6wTQucFA8Fn1U8hxxVw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@oxfmt/binding-win32-arm64-msvc@0.31.0':
resolution: {integrity: sha512-mMpvvPpoLD97Q2TMhjWDJSn+ib3kN+H+F4gq9p88zpeef6sqWc9djorJ3JXM2sOZMJ6KZ+1kSJfe0rkji74Pog==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@oxfmt/win32-x64@0.26.0':
resolution: {integrity: sha512-m8TfIljU22i9UEIkD+slGPifTFeaCwIUfxszN3E6ABWP1KQbtwSw9Ak0TdoikibvukF/dtbeyG3WW63jv9DnEg==}
'@oxfmt/binding-win32-ia32-msvc@0.31.0':
resolution: {integrity: sha512-zTngbPyrTDBYJFVQa4OJldM6w1Rqzi8c0/eFxAEbZRoj6x149GkyMkAY3kM+09ZhmszFitCML2S3p10NE2XmHA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ia32]
os: [win32]
'@oxfmt/binding-win32-x64-msvc@0.31.0':
resolution: {integrity: sha512-TB30D+iRLe6eUbc/utOA93+FNz5C6vXSb/TEhwvlODhKYZZSSKn/lFpYzZC7bdhx3a8m4Jq8fEUoCJ6lKnzdpA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@oxlint-tsgolint/darwin-arm64@0.9.1':
resolution: {integrity: sha512-vk+8kChWqN+F+QUOvp4/6jDTlDCzXPgYGkxdi6EOUSOmCP1ix0uYOlIi/ytH2imXmC8YfPgLR/1BhqbsuDKuew==}
'@oxlint-tsgolint/darwin-arm64@0.11.5':
resolution: {integrity: sha512-mzsjJVIUgcGJovBXME63VW2Uau7MS/xCe7xdYj2BplSCuRb5Yoy7WuwCIlbD5ISHjnS6rx26oD2kmzHLRV5Wfw==}
cpu: [arm64]
os: [darwin]
'@oxlint-tsgolint/darwin-x64@0.9.1':
resolution: {integrity: sha512-yXmqr7El17+Oo56fWkPdUluU8d0jWxwRwAe1QZ0Xprxul9FHJeR/O2oYuBUngvCi02dbt0VZlwgJXcljQEdHlQ==}
'@oxlint-tsgolint/darwin-x64@0.11.5':
resolution: {integrity: sha512-zItUS0qLzSzVy0ZQHc4MOphA9lVeP5jffsgZFLCdo+JqmkbVZ14aDtiVUHSHi2hia+qatbb109CHQ9YIl0x7+A==}
cpu: [x64]
os: [darwin]
'@oxlint-tsgolint/linux-arm64@0.9.1':
resolution: {integrity: sha512-ukLb35BHSsxXaVEe8eIvYXMTxOdv8K4CySmtkWyc0pJT0q8zh85by1bsREWAP2hZc0wN0ClHjZHPdKY3958Jwg==}
'@oxlint-tsgolint/linux-arm64@0.11.5':
resolution: {integrity: sha512-R0r/3QTdMtIjfUOM1oxIaCV0s+j7xrnUe4CXo10ZbBzlXfMesWYNcf/oCrhsy87w0kCPFsg58nAdKaIR8xylFg==}
cpu: [arm64]
os: [linux]
'@oxlint-tsgolint/linux-x64@0.9.1':
resolution: {integrity: sha512-kkxSS/meANLun4dHep2wnfvo8OHJKgdxzuY3RoooSWorVqw3/K5Qttmo0OQFt7UNq/oisn0YTaNhV28S0nAWyQ==}
'@oxlint-tsgolint/linux-x64@0.11.5':
resolution: {integrity: sha512-g23J3T29EHWUQYC6aTwLnhwcFtjQh+VfxyGuFjYGGTLhESdlQH9E/pwsN8K9HaAiYWjI51m3r3BqQjXxEW8Jjg==}
cpu: [x64]
os: [linux]
'@oxlint-tsgolint/win32-arm64@0.9.1':
resolution: {integrity: sha512-F9tiZZRn3x+kjXJC8GAE5C5xkvD8b8unoFeh7mS5W4USAH8+AzYydzLev5rAW2uXdOqtkO30EJl0ygl68Zlb8w==}
'@oxlint-tsgolint/win32-arm64@0.11.5':
resolution: {integrity: sha512-MJNT/MPUIZKQCRtCX5s6pCnoe7If/i3RjJzFMe4kSLomRsHrNFYOJBwt4+w/Hqfyg9jNOgR8tbgdx6ofjHaPMQ==}
cpu: [arm64]
os: [win32]
'@oxlint-tsgolint/win32-x64@0.9.1':
resolution: {integrity: sha512-DKTBgKUbycKNYgpWpglEHzkgiNVSG1rZmfiqw7w31keAq8q7avNGhz2WNmsRvXh8IGNw1PMb7vgxwUK8eyXIeg==}
'@oxlint-tsgolint/win32-x64@0.11.5':
resolution: {integrity: sha512-IQmj4EkcZOBlLnj1CdxKFrWT7NAWXZ9ypZ874X/w7S5gRzB2sO4KmE6Z0MWxx05pL9AQF+CWVRjZrKVIYWTzPg==}
cpu: [x64]
os: [win32]
'@oxlint/darwin-arm64@1.33.0':
resolution: {integrity: sha512-PmEQDLHAxiAdyttQ1ZWXd+5VpHLbHf3FTMJL9bg5TZamDnhNiW/v0Pamv3MTAdymnoDI3H8IVLAN/SAseV/adw==}
'@oxlint/binding-android-arm-eabi@1.46.0':
resolution: {integrity: sha512-vLPcE+HcZ/W/0cVA1KLuAnoUSejGougDH/fDjBFf0Q+rbBIyBNLevOhgx3AnBNAt3hcIGY7U05ISbJCKZeVa3w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [android]
'@oxlint/binding-android-arm64@1.46.0':
resolution: {integrity: sha512-b8IqCczUsirdtJ3R/be4cRm64I5pMPafMO/9xyTAZvc+R/FxZHMQuhw0iNT9hQwRn+Uo5rNAoA8QS7QurG2QeA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@oxlint/binding-darwin-arm64@1.46.0':
resolution: {integrity: sha512-CfC/KGnNMhI01dkfCMjquKnW4zby3kqD5o/9XA7+pgo9I4b+Nipm+JVFyZPWMNwKqLXNmi35GTLWjs9svPxlew==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@oxlint/darwin-x64@1.33.0':
resolution: {integrity: sha512-2R9aH3kR0X2M30z5agGikv3tfNTi8/uLhU5/tYktu33VGUXpbf0OLZSlD25UEuwOKAlf3RVtzV5oDyjoq93JuQ==}
'@oxlint/binding-darwin-x64@1.46.0':
resolution: {integrity: sha512-m38mKPsV3rBdWOJ4TAGZiUjWU8RGrBxsmdSeMQ0bPr/8O6CUOm/RJkPBf0GAfPms2WRVcbkfEXvIiPshAeFkeA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@oxlint/linux-arm64-gnu@1.33.0':
resolution: {integrity: sha512-yb/k8GaMDgnX2LyO6km33kKItZ/n573SlbiHBBFU2HmeU7tzEHL5jHkHQXXcysUkapmqHd7UsDhOZDqPmXaQRg==}
'@oxlint/binding-freebsd-x64@1.46.0':
resolution: {integrity: sha512-YaFRKslSAfuMwn7ejS1/wo9jENqQigpGBjjThX+mrpmEROLYGky+zIC5xSVGRng28U92VEDVbSNJ/sguz3dUAA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@oxlint/binding-linux-arm-gnueabihf@1.46.0':
resolution: {integrity: sha512-Nlw+5mSZQtkg1Oj0N8ulxzG8ATpmSDz5V2DNaGhaYAVlcdR8NYSm/xTOnweOXc/UOOv3LwkPPYzqcfPhu2lEkA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@oxlint/binding-linux-arm-musleabihf@1.46.0':
resolution: {integrity: sha512-d3Y5y4ukMqAGnWLMKpwqj8ftNUaac7pA0NrId4AZ77JvHzezmxEcm2gswaBw2HW8y1pnq6KDB0vEPPvpTfDLrA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@oxlint/binding-linux-arm64-gnu@1.46.0':
resolution: {integrity: sha512-jkjx+XSOPuFR+C458prQmehO+v0VK19/3Hj2mOYDF4hHUf3CzmtA4fTmQUtkITZiGHnky7Oao6JeJX24mrX7WQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
'@oxlint/linux-arm64-musl@1.33.0':
resolution: {integrity: sha512-03pt9IO1C4ZfVOW6SQiOK26mzklAhLM3Kc79OXpX1kgZRlxk+rvFoMhlgCOzn7tEdrEgbePkBoxNnwDnJDFqJQ==}
'@oxlint/binding-linux-arm64-musl@1.46.0':
resolution: {integrity: sha512-X/aPB1rpJUdykjWSeeGIbjk6qbD8VDulgLuTSMWgr/t6m1ljcAjqHb1g49pVG9bZl55zjECgzvlpPLWnfb4FMQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
'@oxlint/linux-x64-gnu@1.33.0':
resolution: {integrity: sha512-Z7ImLWM50FoVXzYvyxUQ+QwBkBfRyK4YdLEGonyAGMp7iT3DksonDaTK9ODnJ1qHyAyAZCvuqXD7AEDsDvzDbA==}
'@oxlint/binding-linux-ppc64-gnu@1.46.0':
resolution: {integrity: sha512-AymyOxGWwKY2KJa8b+h8iLrYJZbWKYCjqctSc2q6uIAkYPrCsxcWlge1JP6XZ14Sa80DVMwI/QvktbytSV+xVw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-riscv64-gnu@1.46.0':
resolution: {integrity: sha512-PkeVdPKCDA59rlMuucsel2LjlNEpslQN5AhkMMorIJZItbbqi/0JSuACCzaiIcXYv0oNfbeQ8rbOBikv+aT6cg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-riscv64-musl@1.46.0':
resolution: {integrity: sha512-snQaRLO/X+Ry/CxX1px1g8GUbmXzymdRs+/RkP2bySHWZFhFDtbLm2hA1ujX/jKlTLMJDZn4hYzFGLDwG/Rh2w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@oxlint/binding-linux-s390x-gnu@1.46.0':
resolution: {integrity: sha512-kZhDMwUe/sgDTluGao9c0Dqc1JzV6wPzfGo0l/FLQdh5Zmp39Yg1FbBsCgsJfVKmKl1fNqsHyFLTShWMOlOEhA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-x64-gnu@1.46.0':
resolution: {integrity: sha512-n5a7VtQTxHZ13cNAKQc3ziARv5bE1Fx868v/tnhZNVUjaRNYe5uiUrRJ/LZghdAzOxVuQGarjjq/q4QM2+9OPA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
'@oxlint/linux-x64-musl@1.33.0':
resolution: {integrity: sha512-idb55Uzu5kkqqpMiVUfI9nP7zOqPZinQKsIRQAIU40wILcf/ijvhNZKIu3ucDMmye0n6IWOaSnxIRL5W2fNoUQ==}
'@oxlint/binding-linux-x64-musl@1.46.0':
resolution: {integrity: sha512-KpsDU/BhdVn3iKCLxMXAOZIpO8fS0jEA5iluRoK1rhHPwKtpzEm/OCwERsu/vboMSZm66qnoTUVXRPJ8M+iKVQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
'@oxlint/win32-arm64@1.33.0':
resolution: {integrity: sha512-wKKFt7cubfrLelNzdmDsNSmtBrlSUe1fWus587+uSxDZdpFbQ7liU0gsUlCbcHvym0H1Tc2O3K3cnLrgQORLPQ==}
'@oxlint/binding-openharmony-arm64@1.46.0':
resolution: {integrity: sha512-jtbqUyEXlsDlRmMtTZqNbw49+1V/WxqNAR5l0S3OEkdat9diI5I+eqq9IT+jb5cSDdszTGcXpn7S3+gUYSydxQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@oxlint/binding-win32-arm64-msvc@1.46.0':
resolution: {integrity: sha512-EE8NjpqEZPwHQVigNvdyJ11dZwWIfsfn4VeBAuiJeAdrnY4HFX27mIjJINJgP5ZdBYEFV1OWH/eb9fURCYel8w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@oxlint/win32-x64@1.33.0':
resolution: {integrity: sha512-ReyR8rNHjKNnO7dxGny9RCPELRAdhm3y780FNBcA07E1wvxSCkB+Mn5db0Pa5bRmxrsU/MTZ/aaBFa+ERXDdXw==}
'@oxlint/binding-win32-ia32-msvc@1.46.0':
resolution: {integrity: sha512-BHyk3H/HRdXs+uImGZ/2+qCET+B8lwGHOm7m54JiJEEUWf3zYCFX/Df1SPqtozWWmnBvioxoTG1J3mPRAr8KUA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ia32]
os: [win32]
'@oxlint/binding-win32-x64-msvc@1.46.0':
resolution: {integrity: sha512-DJbQsSJUr4KSi9uU0QqOgI7PX2C+fKGZX+YDprt3vM2sC0dWZsgVTLoN2vtkNyEWJSY2mnvRFUshWXT3bmo0Ug==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
@@ -6842,21 +6998,21 @@ packages:
oxc-resolver@11.15.0:
resolution: {integrity: sha512-Hk2J8QMYwmIO9XTCUiOH00+Xk2/+aBxRUnhrSlANDyCnLYc32R1WSIq1sU2yEdlqd53FfMpPEpnBYIKQMzliJw==}
oxfmt@0.26.0:
resolution: {integrity: sha512-UDD1wFNwfeorMm2ZY0xy1KRAAvJ5NjKBfbDmiMwGP7baEHTq65cYpC0aPP+BGHc8weXUbSZaK8MdGyvuRUvS4Q==}
oxfmt@0.31.0:
resolution: {integrity: sha512-ukl7nojEuJUGbqR4ijC0Z/7a6BYpD4RxLS2UsyJKgbeZfx6TNrsa48veG0z2yQbhTx1nVnes4GIcqMn7n2jFtw==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
oxlint-tsgolint@0.9.1:
resolution: {integrity: sha512-w1lIvUDkkiAPFyo268SFGrdh1LQ3Lcs1XShES7I4X75TliQA0os5XJ5hNZ4lYsSevqcofgEtq4xq7rBumv69iQ==}
oxlint-tsgolint@0.11.5:
resolution: {integrity: sha512-4uVv43EhkeMvlxDU1GUsR5P5c0q74rB/pQRhjGsTOnMIrDbg3TABTntRyeAkmXItqVEJTcDRv9+Yk+LFXkHKlg==}
hasBin: true
oxlint@1.33.0:
resolution: {integrity: sha512-4WCL0K8jiOshwJ8WrVk35VAuVaZHC0iX6asjKsrENOrynkAAGcTLLx0Urf0eXZ1Tq7r+qAe3Z9EyHMFPzVyUkg==}
oxlint@1.46.0:
resolution: {integrity: sha512-I9h42QDtAVsRwoueJ4PL/7qN5jFzIUXvbO4Z5ddtII92ZCiD7uiS/JW2V4viBSfGLsbZkQp3YEs6Ls4I8q+8tA==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
oxlint-tsgolint: '>=0.9.0'
oxlint-tsgolint: '>=0.11.2'
peerDependenciesMeta:
oxlint-tsgolint:
optional: true
@@ -7447,6 +7603,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
semver@7.7.4:
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
engines: {node: '>=10'}
hasBin: true
set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
@@ -7757,8 +7918,8 @@ packages:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
tinypool@2.0.0:
resolution: {integrity: sha512-/RX9RzeH2xU5ADE7n2Ykvmi9ED3FBGPAjw9u3zucrNNaEBIO0HPSYgL0NT7+3p147ojeSdaVu08F6hjpv31HJg==}
tinypool@2.1.0:
resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==}
engines: {node: ^20.0.0 || >=22.0.0}
tinyrainbow@2.0.0:
@@ -10726,70 +10887,136 @@ snapshots:
'@oxc-resolver/binding-win32-x64-msvc@11.15.0':
optional: true
'@oxfmt/darwin-arm64@0.26.0':
'@oxfmt/binding-android-arm-eabi@0.31.0':
optional: true
'@oxfmt/darwin-x64@0.26.0':
'@oxfmt/binding-android-arm64@0.31.0':
optional: true
'@oxfmt/linux-arm64-gnu@0.26.0':
'@oxfmt/binding-darwin-arm64@0.31.0':
optional: true
'@oxfmt/linux-arm64-musl@0.26.0':
'@oxfmt/binding-darwin-x64@0.31.0':
optional: true
'@oxfmt/linux-x64-gnu@0.26.0':
'@oxfmt/binding-freebsd-x64@0.31.0':
optional: true
'@oxfmt/linux-x64-musl@0.26.0':
'@oxfmt/binding-linux-arm-gnueabihf@0.31.0':
optional: true
'@oxfmt/win32-arm64@0.26.0':
'@oxfmt/binding-linux-arm-musleabihf@0.31.0':
optional: true
'@oxfmt/win32-x64@0.26.0':
'@oxfmt/binding-linux-arm64-gnu@0.31.0':
optional: true
'@oxlint-tsgolint/darwin-arm64@0.9.1':
'@oxfmt/binding-linux-arm64-musl@0.31.0':
optional: true
'@oxlint-tsgolint/darwin-x64@0.9.1':
'@oxfmt/binding-linux-ppc64-gnu@0.31.0':
optional: true
'@oxlint-tsgolint/linux-arm64@0.9.1':
'@oxfmt/binding-linux-riscv64-gnu@0.31.0':
optional: true
'@oxlint-tsgolint/linux-x64@0.9.1':
'@oxfmt/binding-linux-riscv64-musl@0.31.0':
optional: true
'@oxlint-tsgolint/win32-arm64@0.9.1':
'@oxfmt/binding-linux-s390x-gnu@0.31.0':
optional: true
'@oxlint-tsgolint/win32-x64@0.9.1':
'@oxfmt/binding-linux-x64-gnu@0.31.0':
optional: true
'@oxlint/darwin-arm64@1.33.0':
'@oxfmt/binding-linux-x64-musl@0.31.0':
optional: true
'@oxlint/darwin-x64@1.33.0':
'@oxfmt/binding-openharmony-arm64@0.31.0':
optional: true
'@oxlint/linux-arm64-gnu@1.33.0':
'@oxfmt/binding-win32-arm64-msvc@0.31.0':
optional: true
'@oxlint/linux-arm64-musl@1.33.0':
'@oxfmt/binding-win32-ia32-msvc@0.31.0':
optional: true
'@oxlint/linux-x64-gnu@1.33.0':
'@oxfmt/binding-win32-x64-msvc@0.31.0':
optional: true
'@oxlint/linux-x64-musl@1.33.0':
'@oxlint-tsgolint/darwin-arm64@0.11.5':
optional: true
'@oxlint/win32-arm64@1.33.0':
'@oxlint-tsgolint/darwin-x64@0.11.5':
optional: true
'@oxlint/win32-x64@1.33.0':
'@oxlint-tsgolint/linux-arm64@0.11.5':
optional: true
'@oxlint-tsgolint/linux-x64@0.11.5':
optional: true
'@oxlint-tsgolint/win32-arm64@0.11.5':
optional: true
'@oxlint-tsgolint/win32-x64@0.11.5':
optional: true
'@oxlint/binding-android-arm-eabi@1.46.0':
optional: true
'@oxlint/binding-android-arm64@1.46.0':
optional: true
'@oxlint/binding-darwin-arm64@1.46.0':
optional: true
'@oxlint/binding-darwin-x64@1.46.0':
optional: true
'@oxlint/binding-freebsd-x64@1.46.0':
optional: true
'@oxlint/binding-linux-arm-gnueabihf@1.46.0':
optional: true
'@oxlint/binding-linux-arm-musleabihf@1.46.0':
optional: true
'@oxlint/binding-linux-arm64-gnu@1.46.0':
optional: true
'@oxlint/binding-linux-arm64-musl@1.46.0':
optional: true
'@oxlint/binding-linux-ppc64-gnu@1.46.0':
optional: true
'@oxlint/binding-linux-riscv64-gnu@1.46.0':
optional: true
'@oxlint/binding-linux-riscv64-musl@1.46.0':
optional: true
'@oxlint/binding-linux-s390x-gnu@1.46.0':
optional: true
'@oxlint/binding-linux-x64-gnu@1.46.0':
optional: true
'@oxlint/binding-linux-x64-musl@1.46.0':
optional: true
'@oxlint/binding-openharmony-arm64@1.46.0':
optional: true
'@oxlint/binding-win32-arm64-msvc@1.46.0':
optional: true
'@oxlint/binding-win32-ia32-msvc@1.46.0':
optional: true
'@oxlint/binding-win32-x64-msvc@1.46.0':
optional: true
'@phenomnomnominal/tsquery@5.0.1(typescript@5.9.3)':
@@ -14489,7 +14716,7 @@ snapshots:
acorn: 8.15.0
eslint-visitor-keys: 3.4.3
espree: 9.6.1
semver: 7.7.3
semver: 7.7.4
jsonc-parser@3.2.0: {}
@@ -15512,39 +15739,61 @@ snapshots:
'@oxc-resolver/binding-win32-ia32-msvc': 11.15.0
'@oxc-resolver/binding-win32-x64-msvc': 11.15.0
oxfmt@0.26.0:
oxfmt@0.31.0:
dependencies:
tinypool: 2.0.0
tinypool: 2.1.0
optionalDependencies:
'@oxfmt/darwin-arm64': 0.26.0
'@oxfmt/darwin-x64': 0.26.0
'@oxfmt/linux-arm64-gnu': 0.26.0
'@oxfmt/linux-arm64-musl': 0.26.0
'@oxfmt/linux-x64-gnu': 0.26.0
'@oxfmt/linux-x64-musl': 0.26.0
'@oxfmt/win32-arm64': 0.26.0
'@oxfmt/win32-x64': 0.26.0
'@oxfmt/binding-android-arm-eabi': 0.31.0
'@oxfmt/binding-android-arm64': 0.31.0
'@oxfmt/binding-darwin-arm64': 0.31.0
'@oxfmt/binding-darwin-x64': 0.31.0
'@oxfmt/binding-freebsd-x64': 0.31.0
'@oxfmt/binding-linux-arm-gnueabihf': 0.31.0
'@oxfmt/binding-linux-arm-musleabihf': 0.31.0
'@oxfmt/binding-linux-arm64-gnu': 0.31.0
'@oxfmt/binding-linux-arm64-musl': 0.31.0
'@oxfmt/binding-linux-ppc64-gnu': 0.31.0
'@oxfmt/binding-linux-riscv64-gnu': 0.31.0
'@oxfmt/binding-linux-riscv64-musl': 0.31.0
'@oxfmt/binding-linux-s390x-gnu': 0.31.0
'@oxfmt/binding-linux-x64-gnu': 0.31.0
'@oxfmt/binding-linux-x64-musl': 0.31.0
'@oxfmt/binding-openharmony-arm64': 0.31.0
'@oxfmt/binding-win32-arm64-msvc': 0.31.0
'@oxfmt/binding-win32-ia32-msvc': 0.31.0
'@oxfmt/binding-win32-x64-msvc': 0.31.0
oxlint-tsgolint@0.9.1:
oxlint-tsgolint@0.11.5:
optionalDependencies:
'@oxlint-tsgolint/darwin-arm64': 0.9.1
'@oxlint-tsgolint/darwin-x64': 0.9.1
'@oxlint-tsgolint/linux-arm64': 0.9.1
'@oxlint-tsgolint/linux-x64': 0.9.1
'@oxlint-tsgolint/win32-arm64': 0.9.1
'@oxlint-tsgolint/win32-x64': 0.9.1
'@oxlint-tsgolint/darwin-arm64': 0.11.5
'@oxlint-tsgolint/darwin-x64': 0.11.5
'@oxlint-tsgolint/linux-arm64': 0.11.5
'@oxlint-tsgolint/linux-x64': 0.11.5
'@oxlint-tsgolint/win32-arm64': 0.11.5
'@oxlint-tsgolint/win32-x64': 0.11.5
oxlint@1.33.0(oxlint-tsgolint@0.9.1):
oxlint@1.46.0(oxlint-tsgolint@0.11.5):
optionalDependencies:
'@oxlint/darwin-arm64': 1.33.0
'@oxlint/darwin-x64': 1.33.0
'@oxlint/linux-arm64-gnu': 1.33.0
'@oxlint/linux-arm64-musl': 1.33.0
'@oxlint/linux-x64-gnu': 1.33.0
'@oxlint/linux-x64-musl': 1.33.0
'@oxlint/win32-arm64': 1.33.0
'@oxlint/win32-x64': 1.33.0
oxlint-tsgolint: 0.9.1
'@oxlint/binding-android-arm-eabi': 1.46.0
'@oxlint/binding-android-arm64': 1.46.0
'@oxlint/binding-darwin-arm64': 1.46.0
'@oxlint/binding-darwin-x64': 1.46.0
'@oxlint/binding-freebsd-x64': 1.46.0
'@oxlint/binding-linux-arm-gnueabihf': 1.46.0
'@oxlint/binding-linux-arm-musleabihf': 1.46.0
'@oxlint/binding-linux-arm64-gnu': 1.46.0
'@oxlint/binding-linux-arm64-musl': 1.46.0
'@oxlint/binding-linux-ppc64-gnu': 1.46.0
'@oxlint/binding-linux-riscv64-gnu': 1.46.0
'@oxlint/binding-linux-riscv64-musl': 1.46.0
'@oxlint/binding-linux-s390x-gnu': 1.46.0
'@oxlint/binding-linux-x64-gnu': 1.46.0
'@oxlint/binding-linux-x64-musl': 1.46.0
'@oxlint/binding-openharmony-arm64': 1.46.0
'@oxlint/binding-win32-arm64-msvc': 1.46.0
'@oxlint/binding-win32-ia32-msvc': 1.46.0
'@oxlint/binding-win32-x64-msvc': 1.46.0
oxlint-tsgolint: 0.11.5
p-limit@3.1.0:
dependencies:
@@ -16282,6 +16531,8 @@ snapshots:
semver@7.7.3: {}
semver@7.7.4: {}
set-function-length@1.2.2:
dependencies:
define-data-property: 1.1.4
@@ -16682,7 +16933,7 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
tinypool@2.0.0: {}
tinypool@2.1.0: {}
tinyrainbow@2.0.0: {}

View File

@@ -69,9 +69,9 @@ catalog:
markdown-table: ^3.0.4
mixpanel-browser: ^2.71.0
nx: 22.2.6
oxfmt: ^0.26.0
oxlint: ^1.33.0
oxlint-tsgolint: ^0.9.1
oxfmt: ^0.31.0
oxlint: ^1.46.0
oxlint-tsgolint: ^0.11.5
picocolors: ^1.1.1
pinia: ^3.0.4
postcss-html: ^1.8.0

View File

@@ -33,6 +33,7 @@ fi
EXCLUDE_PATTERNS=(
'**/tsconfig*.json'
'.oxlintrc.json'
)
if [ -n "${JSON_LINT_EXCLUDES:-}" ]; then

View File

@@ -16,12 +16,12 @@ import { computed, onMounted } from 'vue'
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
import config from '@/config'
import { app } from '@/scripts/app'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
import { electronAPI } from '@/utils/envUtil'
import { isDesktop } from '@/platform/distribution/types'
import { app } from '@/scripts/app'
const workspaceStore = useWorkspaceStore()
app.extensionManager = useWorkspaceStore()

View File

@@ -49,7 +49,7 @@ describe('downloadUtil', () => {
vi.unstubAllGlobals()
})
describe('downloadFile', () => {
describe(downloadFile, () => {
it('should create and trigger download with basic URL', () => {
const testUrl = 'https://example.com/image.png'
@@ -285,7 +285,7 @@ describe('downloadUtil', () => {
})
})
describe('extractFilenameFromContentDisposition', () => {
describe(extractFilenameFromContentDisposition, () => {
it('returns null for null header', () => {
expect(extractFilenameFromContentDisposition(null)).toBeNull()
})

View File

@@ -172,19 +172,17 @@ const splitterRefreshKey = computed(() => {
return `main-splitter${rightSidePanelVisible.value ? '-with-right-panel' : ''}-${sidebarLocation.value}`
})
const firstPanelStyle = computed(() => {
if (sidebarLocation.value === 'left') {
return { display: sidebarPanelVisible.value ? 'flex' : 'none' }
}
return undefined
})
const firstPanelStyle = computed(() =>
sidebarLocation.value === 'left'
? { display: sidebarPanelVisible.value ? 'flex' : 'none' }
: undefined
)
const lastPanelStyle = computed(() => {
if (sidebarLocation.value === 'right') {
return { display: sidebarPanelVisible.value ? 'flex' : 'none' }
}
return undefined
})
const lastPanelStyle = computed(() =>
sidebarLocation.value === 'right'
? { display: sidebarPanelVisible.value ? 'flex' : 'none' }
: undefined
)
</script>
<style scoped>

View File

@@ -2,7 +2,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 { computed, defineComponent, h, nextTick, onMounted, ref } from 'vue'
import { computed, defineComponent, h, nextTick, onMounted } from 'vue'
import type { Component } from 'vue'
import { createI18n } from 'vue-i18n'
@@ -19,11 +19,7 @@ import { useExecutionStore } from '@/stores/executionStore'
import { TaskItemImpl, useQueueStore } from '@/stores/queueStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
const mockData = vi.hoisted(() => ({
isLoggedIn: false,
isDesktop: false,
setShowConflictRedDot: (_value: boolean) => {}
}))
const mockData = vi.hoisted(() => ({ isLoggedIn: false, isDesktop: false }))
vi.mock('@/composables/auth/useCurrentUser', () => ({
useCurrentUser: () => {
@@ -40,36 +36,6 @@ vi.mock('@/platform/distribution/types', () => ({
return mockData.isDesktop
}
}))
vi.mock('@/platform/updates/common/releaseStore', () => ({
useReleaseStore: () => ({
shouldShowRedDot: computed(() => true)
})
}))
vi.mock(
'@/workbench/extensions/manager/composables/useConflictAcknowledgment',
() => {
const shouldShowConflictRedDot = ref(false)
mockData.setShowConflictRedDot = (value: boolean) => {
shouldShowConflictRedDot.value = value
}
return {
useConflictAcknowledgment: () => ({
shouldShowRedDot: shouldShowConflictRedDot
})
}
}
)
vi.mock('@/workbench/extensions/manager/composables/useManagerState', () => ({
useManagerState: () => ({
shouldShowManagerButtons: computed(() => true),
openManager: vi.fn()
})
}))
vi.mock('@/stores/firebaseAuthStore', () => ({
useFirebaseAuthStore: vi.fn(() => ({
currentUser: null,
@@ -142,13 +108,12 @@ function createTask(id: string, status: JobStatus): TaskItemImpl {
return new TaskItemImpl(createJob(id, status))
}
describe('TopMenuSection', () => {
describe(TopMenuSection.__name ?? 'TopMenuSection', () => {
beforeEach(() => {
vi.resetAllMocks()
localStorage.clear()
mockData.isDesktop = false
mockData.isLoggedIn = false
mockData.setShowConflictRedDot(false)
})
describe('authentication state', () => {
@@ -277,7 +242,7 @@ describe('TopMenuSection', () => {
vi.mocked(settingStore.get).mockImplementation((key) => {
if (key === 'Comfy.Queue.QPOV2') return qpoV2Enabled
if (key === 'Comfy.UseNewMenu') return 'Top'
return undefined
return
})
}
@@ -365,16 +330,4 @@ describe('TopMenuSection', () => {
const model = menu.props('model') as MenuItem[]
expect(model[0]?.disabled).toBe(false)
})
it('shows manager red dot only for manager conflicts', async () => {
const wrapper = createWrapper()
// Release red dot is mocked as true globally for this test file.
expect(wrapper.find('span.bg-red-500').exists()).toBe(false)
mockData.setShowConflictRedDot(true)
await nextTick()
expect(wrapper.find('span.bg-red-500').exists()).toBe(true)
})
})

View File

@@ -145,6 +145,7 @@ import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
@@ -172,6 +173,8 @@ const sidebarTabStore = useSidebarTabStore()
const { activeJobsCount } = storeToRefs(queueStore)
const { isOverlayExpanded: isQueueOverlayExpanded } = storeToRefs(queueUIStore)
const { activeSidebarTabId } = storeToRefs(sidebarTabStore)
const releaseStore = useReleaseStore()
const { shouldShowRedDot: showReleaseRedDot } = storeToRefs(releaseStore)
const { shouldShowRedDot: shouldShowConflictRedDot } =
useConflictAcknowledgment()
const isTopMenuHovered = ref(false)
@@ -233,8 +236,10 @@ const queueContextMenuItems = computed<MenuItem[]>(() => [
}
])
// Use either release red dot or conflict red dot
const shouldShowRedDot = computed((): boolean => {
return shouldShowConflictRedDot.value
const releaseRedDot = showReleaseRedDot.value
return releaseRedDot || shouldShowConflictRedDot.value
})
// Right side panel toggle
@@ -282,9 +287,8 @@ const openCustomNodeManager = async () => {
} catch (error) {
try {
toastErrorHandler(error)
} catch (toastError) {
} catch (error) {
console.error(error)
console.error(toastError)
}
}
}

View File

@@ -54,7 +54,7 @@ vi.mock('primevue/progressspinner', () => ({
default: { template: '<div class="progress-spinner" />' }
}))
describe('WorkspaceAuthGate', () => {
describe(WorkspaceAuthGate.__name ?? 'WorkspaceAuthGate', () => {
beforeEach(() => {
vi.clearAllMocks()
mockIsCloud.value = true

View File

@@ -51,7 +51,7 @@ vi.mock('@/stores/commandStore', () => ({
})
}))
describe('EssentialsPanel', () => {
describe(EssentialsPanel.__name ?? 'EssentialsPanel', () => {
beforeEach(() => {
setActivePinia(createPinia())
})

View File

@@ -23,7 +23,7 @@ vi.mock('vue-i18n', () => ({
})
}))
describe('ShortcutsList', () => {
describe(ShortcutsList.__name ?? 'ShortcutsList', () => {
const mockCommands: ComfyCommandImpl[] = [
{
id: 'Workflow.New',

View File

@@ -106,7 +106,7 @@ const mountBaseTerminal = () => {
})
}
describe('BaseTerminal', () => {
describe(BaseTerminal.__name ?? 'BaseTerminal', () => {
let wrapper: VueWrapper<InstanceType<typeof BaseTerminal>> | undefined
beforeEach(() => {

View File

@@ -62,8 +62,8 @@ const terminalCreated = (
onMounted(async () => {
try {
await loadLogEntries()
} catch (err) {
console.error('Error loading logs', err)
} catch (error) {
console.error('Error loading logs', error)
// On older backends the endpoints won't exist
errorMessage.value =
'Unable to load logs, please ensure you have updated your ComfyUI backend.'

View File

@@ -78,9 +78,7 @@ interface Props {
isActive?: boolean
}
const props = withDefaults(defineProps<Props>(), {
isActive: false
})
const { item, isActive = false } = defineProps<Props>()
const nodeDefStore = useNodeDefStore()
const hasMissingNodes = computed(() =>
@@ -103,7 +101,7 @@ const rename = async (
) => {
if (newName && newName !== initialName) {
// Synchronize the node titles with the new name
props.item.updateTitle?.(newName)
item.updateTitle?.(newName)
if (workflowStore.activeSubgraph) {
workflowStore.activeSubgraph.name = newName
@@ -127,13 +125,13 @@ const rename = async (
}
}
const isRoot = props.item.key === 'root'
const isRoot = item.key === 'root'
const tooltipText = computed(() => {
if (hasMissingNodes.value && isRoot) {
return t('breadcrumbsMenu.missingNodesWarning')
}
return props.item.label
return item.label
})
const startRename = async () => {
@@ -145,7 +143,7 @@ const startRename = async () => {
}
isEditing.value = true
itemLabel.value = props.item.label as string
itemLabel.value = item.label as string
void nextTick(() => {
if (itemInputRef.value?.$el) {
itemInputRef.value.$el.focus()
@@ -165,12 +163,12 @@ const handleClick = (event: MouseEvent) => {
}
if (event.detail === 1) {
if (props.isActive) {
if (isActive) {
menu.value?.toggle(event)
} else {
props.item.command?.({ item: props.item, originalEvent: event })
item.command?.({ item, originalEvent: event })
}
} else if (props.isActive && event.detail === 2) {
} else if (isActive && event.detail === 2) {
menu.value?.hide()
event.stopPropagation()
event.preventDefault()
@@ -180,7 +178,7 @@ const handleClick = (event: MouseEvent) => {
const inputBlur = async (doRename: boolean) => {
if (doRename) {
await rename(itemLabel.value, props.item.label as string)
await rename(itemLabel.value, item.label as string)
}
isEditing.value = false

View File

@@ -7,123 +7,128 @@ import { createApp, nextTick } from 'vue'
import ColorCustomizationSelector from './ColorCustomizationSelector.vue'
describe('ColorCustomizationSelector', () => {
const colorOptions = [
{ name: 'Blue', value: '#0d6efd' },
{ name: 'Green', value: '#28a745' }
]
describe(
ColorCustomizationSelector.__name ?? 'ColorCustomizationSelector',
() => {
const colorOptions = [
{ name: 'Blue', value: '#0d6efd' },
{ name: 'Green', value: '#28a745' }
]
beforeEach(() => {
// Setup PrimeVue
const app = createApp({})
app.use(PrimeVue)
})
beforeEach(() => {
// Setup PrimeVue
const app = createApp({})
app.use(PrimeVue)
})
const mountComponent = (props = {}) => {
return mount(ColorCustomizationSelector, {
global: {
plugins: [PrimeVue],
components: { SelectButton, ColorPicker }
},
props: {
modelValue: null,
colorOptions,
...props
}
const mountComponent = (props = {}) => {
return mount(ColorCustomizationSelector, {
global: {
plugins: [PrimeVue],
components: { SelectButton, ColorPicker }
},
props: {
modelValue: null,
colorOptions,
...props
}
})
}
it('renders predefined color options and custom option', () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
expect(selectButton.props('options')).toHaveLength(
colorOptions.length + 1
)
expect(selectButton.props('options')?.at(-1)?.name).toBe('_custom')
})
it('initializes with predefined color when provided', async () => {
const wrapper = mountComponent({
modelValue: '#0d6efd'
})
await nextTick()
const selectButton = wrapper.findComponent(SelectButton)
expect(selectButton.props('modelValue')).toEqual({
name: 'Blue',
value: '#0d6efd'
})
})
it('initializes with custom color when non-predefined color provided', async () => {
const wrapper = mountComponent({
modelValue: '#123456'
})
await nextTick()
const selectButton = wrapper.findComponent(SelectButton)
const colorPicker = wrapper.findComponent(ColorPicker)
expect(selectButton.props('modelValue').name).toBe('_custom')
expect(colorPicker.props('modelValue')).toBe('123456')
})
it('shows color picker when custom option is selected', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
// Select custom option
await selectButton.setValue({ name: '_custom', value: '' })
expect(wrapper.findComponent(ColorPicker).exists()).toBe(true)
})
it('emits update when predefined color is selected', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
await selectButton.setValue(colorOptions[0])
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['#0d6efd'])
})
it('emits update when custom color is changed', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
// Select custom option
await selectButton.setValue({ name: '_custom', value: '' })
// Change custom color
const colorPicker = wrapper.findComponent(ColorPicker)
await colorPicker.setValue('ff0000')
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['#ff0000'])
})
it('inherits color from previous selection when switching to custom', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
// First select a predefined color
await selectButton.setValue(colorOptions[0])
// Then switch to custom
await selectButton.setValue({ name: '_custom', value: '' })
const colorPicker = wrapper.findComponent(ColorPicker)
expect(colorPicker.props('modelValue')).toBe('0d6efd')
})
it('handles null modelValue correctly', async () => {
const wrapper = mountComponent({
modelValue: null
})
await nextTick()
const selectButton = wrapper.findComponent(SelectButton)
expect(selectButton.props('modelValue')).toEqual({
name: '_custom',
value: ''
})
})
}
it('renders predefined color options and custom option', () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
expect(selectButton.props('options')).toHaveLength(colorOptions.length + 1)
expect(selectButton.props('options')?.at(-1)?.name).toBe('_custom')
})
it('initializes with predefined color when provided', async () => {
const wrapper = mountComponent({
modelValue: '#0d6efd'
})
await nextTick()
const selectButton = wrapper.findComponent(SelectButton)
expect(selectButton.props('modelValue')).toEqual({
name: 'Blue',
value: '#0d6efd'
})
})
it('initializes with custom color when non-predefined color provided', async () => {
const wrapper = mountComponent({
modelValue: '#123456'
})
await nextTick()
const selectButton = wrapper.findComponent(SelectButton)
const colorPicker = wrapper.findComponent(ColorPicker)
expect(selectButton.props('modelValue').name).toBe('_custom')
expect(colorPicker.props('modelValue')).toBe('123456')
})
it('shows color picker when custom option is selected', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
// Select custom option
await selectButton.setValue({ name: '_custom', value: '' })
expect(wrapper.findComponent(ColorPicker).exists()).toBe(true)
})
it('emits update when predefined color is selected', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
await selectButton.setValue(colorOptions[0])
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['#0d6efd'])
})
it('emits update when custom color is changed', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
// Select custom option
await selectButton.setValue({ name: '_custom', value: '' })
// Change custom color
const colorPicker = wrapper.findComponent(ColorPicker)
await colorPicker.setValue('ff0000')
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['#ff0000'])
})
it('inherits color from previous selection when switching to custom', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
// First select a predefined color
await selectButton.setValue(colorOptions[0])
// Then switch to custom
await selectButton.setValue({ name: '_custom', value: '' })
const colorPicker = wrapper.findComponent(ColorPicker)
expect(colorPicker.props('modelValue')).toBe('0d6efd')
})
it('handles null modelValue correctly', async () => {
const wrapper = mountComponent({
modelValue: null
})
await nextTick()
const selectButton = wrapper.findComponent(SelectButton)
expect(selectButton.props('modelValue')).toEqual({
name: '_custom',
value: ''
})
})
})
)

View File

@@ -5,7 +5,7 @@
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
const props = defineProps<{
const { renderFunction } = defineProps<{
renderFunction: () => HTMLElement
}>()
@@ -14,12 +14,12 @@ const container = ref<HTMLElement | null>(null)
function renderContent() {
if (container.value) {
container.value.innerHTML = ''
const element = props.renderFunction()
const element = renderFunction()
container.value.appendChild(element)
}
}
onMounted(renderContent)
watch(() => props.renderFunction, renderContent)
watch(() => renderFunction, renderContent)
</script>

View File

@@ -52,7 +52,7 @@ import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
const { t } = useI18n()
const props = defineProps<{
const { modelValue, initialIcon, initialColor } = defineProps<{
modelValue: boolean
initialIcon?: string
initialColor?: string
@@ -64,7 +64,7 @@ const emit = defineEmits<{
}>()
const visible = computed({
get: () => props.modelValue,
get: () => modelValue,
set: (value) => emit('update:modelValue', value)
})
@@ -96,17 +96,13 @@ const defaultIcon = iconOptions.find(
// @ts-expect-error fixme ts strict error
const selectedIcon = ref<{ name: string; value: string }>(defaultIcon)
const finalColor = ref(
props.initialColor || nodeBookmarkStore.defaultBookmarkColor
)
const finalColor = ref(initialColor || nodeBookmarkStore.defaultBookmarkColor)
const resetCustomization = () => {
// @ts-expect-error fixme ts strict error
selectedIcon.value =
iconOptions.find((option) => option.value === props.initialIcon) ||
defaultIcon
finalColor.value =
props.initialColor || nodeBookmarkStore.defaultBookmarkColor
iconOptions.find((option) => option.value === initialIcon) || defaultIcon
finalColor.value = initialColor || nodeBookmarkStore.defaultBookmarkColor
}
const confirmCustomization = () => {
@@ -119,7 +115,7 @@ const closeDialog = () => {
}
watch(
() => props.modelValue,
() => modelValue,
(newValue: boolean) => {
if (newValue) {
resetCustomization()

View File

@@ -5,7 +5,7 @@
{{ col.header }}
</div>
<div>
{{ formatValue(props.device[col.field], col.field) }}
{{ formatValue(device[col.field], col.field) }}
</div>
</template>
</div>
@@ -15,7 +15,7 @@
import type { DeviceStats } from '@/schemas/apiSchema'
import { formatSize } from '@/utils/formatUtil'
const props = defineProps<{
const { device } = defineProps<{
device: DeviceStats
}>()

View File

@@ -6,7 +6,7 @@ import { createApp } from 'vue'
import EditableText from './EditableText.vue'
describe('EditableText', () => {
describe(EditableText.__name ?? 'EditableText', () => {
beforeAll(() => {
// Create a Vue app instance for PrimeVue
const app = createApp({})

View File

@@ -5,10 +5,10 @@
<i v-if="status === 'completed'" class="pi pi-check text-green-500" />
<div class="file-info">
<div class="file-details">
<span class="file-type" :title="hint">{{ label }}</span>
<span class="file-type" :title="displayHint">{{ displayLabel }}</span>
</div>
<div v-if="props.error" class="file-error">
{{ props.error }}
<div v-if="error" class="file-error">
{{ error }}
</div>
</div>
@@ -18,14 +18,14 @@
class="file-action-button"
variant="secondary"
size="sm"
:disabled="!!props.error"
:disabled="!!error"
@click="triggerDownload"
>
<i class="pi pi-download" />
{{ $t('g.downloadWithSize', { size: fileSize }) }}
</Button>
<Button
v-if="(status === null || status === 'error') && !!props.url"
v-if="(status === null || status === 'error') && !!url"
variant="secondary"
size="sm"
@click="copyURL"
@@ -53,7 +53,7 @@
class="file-action-button"
variant="secondary"
size="sm"
:disabled="!!props.error"
:disabled="!!error"
@click="triggerPauseDownload"
>
<i class="pi pi-pause-circle" />
@@ -66,7 +66,7 @@
variant="secondary"
size="sm"
:aria-label="t('electronFileDownload.resume')"
:disabled="!!props.error"
:disabled="!!error"
@click="triggerResumeDownload"
>
<i class="pi pi-play-circle" />
@@ -78,7 +78,7 @@
variant="destructive"
size="sm"
:aria-label="t('electronFileDownload.cancel')"
:disabled="!!props.error"
:disabled="!!error"
@click="triggerCancelDownload"
>
<i class="pi pi-times-circle" />
@@ -98,7 +98,7 @@ import { useDownload } from '@/composables/useDownload'
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
import { formatSize } from '@/utils/formatUtil'
const props = defineProps<{
const { url, hint, label, error } = defineProps<{
url: string
hint?: string
label?: string
@@ -106,9 +106,9 @@ const props = defineProps<{
}>()
const { t } = useI18n()
const label = computed(() => props.label || props.url.split('/').pop())
const hint = computed(() => props.hint || props.url)
const download = useDownload(props.url)
const displayLabel = computed(() => label || url.split('/').pop())
const displayHint = computed(() => hint || url)
const download = useDownload(url)
const downloadProgress = ref<number>(0)
const status = ref<string | null>(null)
const fileSize = computed(() =>
@@ -117,10 +117,10 @@ const fileSize = computed(() =>
const { copyToClipboard } = useCopyToClipboard()
const electronDownloadStore = useElectronDownloadStore()
// @ts-expect-error fixme ts strict error
const [savePath, filename] = props.label.split('/')
const [savePath, filename] = label.split('/')
electronDownloadStore.$subscribe((_, { downloads }) => {
const download = downloads.find((download) => props.url === download.url)
const download = downloads.find((download) => url === download.url)
if (download) {
// @ts-expect-error fixme ts strict error
@@ -132,17 +132,17 @@ electronDownloadStore.$subscribe((_, { downloads }) => {
const triggerDownload = async () => {
await electronDownloadStore.start({
url: props.url,
url,
savePath: savePath.trim(),
filename: filename.trim()
})
}
const triggerCancelDownload = () => electronDownloadStore.cancel(props.url)
const triggerPauseDownload = () => electronDownloadStore.pause(props.url)
const triggerResumeDownload = () => electronDownloadStore.resume(props.url)
const triggerCancelDownload = () => electronDownloadStore.cancel(url)
const triggerPauseDownload = () => electronDownloadStore.pause(url)
const triggerResumeDownload = () => electronDownloadStore.resume(url)
const copyURL = async () => {
await copyToClipboard(props.url)
await copyToClipboard(url)
}
</script>

View File

@@ -5,10 +5,7 @@
:ref="
(el) => {
if (el)
mountCustomExtension(
props.extension as CustomExtension,
el as HTMLElement
)
mountCustomExtension(extension as CustomExtension, el as HTMLElement)
}
"
/>
@@ -19,17 +16,17 @@ import { onBeforeUnmount } from 'vue'
import type { CustomExtension, VueExtension } from '@/types/extensionTypes'
const props = defineProps<{
const { extension } = defineProps<{
extension: VueExtension | CustomExtension
}>()
const mountCustomExtension = (extension: CustomExtension, el: HTMLElement) => {
extension.render(el)
const mountCustomExtension = (ext: CustomExtension, el: HTMLElement) => {
ext.render(el)
}
onBeforeUnmount(() => {
if (props.extension.type === 'custom' && props.extension.destroy) {
props.extension.destroy()
if (extension.type === 'custom' && extension.destroy) {
extension.destroy()
}
})
</script>

View File

@@ -3,35 +3,35 @@
<div class="flex flex-row items-center gap-2">
<div>
<div>
<span :title="hint">{{ label }}</span>
<span :title="displayHint">{{ displayLabel }}</span>
</div>
<Message
v-if="props.error"
v-if="error"
severity="error"
icon="pi pi-exclamation-triangle"
size="small"
variant="outlined"
class="my-2 h-min max-w-xs px-1"
:title="props.error"
:title="error"
:pt="{
text: { class: 'overflow-hidden text-ellipsis' }
}"
>
{{ props.error }}
{{ error }}
</Message>
</div>
<div>
<Button
variant="secondary"
:disabled="!!props.error"
:title="props.url"
:disabled="!!error"
:title="url"
@click="download.triggerBrowserDownload"
>
{{ $t('g.downloadWithSize', { size: fileSize }) }}
</Button>
</div>
<div>
<Button variant="secondary" :disabled="!!props.error" @click="copyURL">
<Button variant="secondary" :disabled="!!error" @click="copyURL">
{{ $t('g.copyURL') }}
</Button>
</div>
@@ -47,22 +47,22 @@ import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { useDownload } from '@/composables/useDownload'
import { formatSize } from '@/utils/formatUtil'
const props = defineProps<{
const { url, hint, label, error } = defineProps<{
url: string
hint?: string
label?: string
error?: string
}>()
const label = computed(() => props.label || props.url.split('/').pop())
const displayLabel = computed(() => label || url.split('/').pop())
const hint = computed(() => props.hint || props.url)
const download = useDownload(props.url)
const displayHint = computed(() => hint || url)
const download = useDownload(url)
const fileSize = computed(() =>
download.fileSize.value ? formatSize(download.fileSize.value) : '?'
)
const copyURL = async () => {
await copyToClipboard(props.url)
await copyToClipboard(url)
}
const { copyToClipboard } = useCopyToClipboard()

View File

@@ -10,7 +10,7 @@ import ColorPicker from 'primevue/colorpicker'
import InputText from 'primevue/inputtext'
const modelValue = defineModel<string>('modelValue')
defineProps<{
const { label } = defineProps<{
label?: string
}>()

View File

@@ -45,7 +45,7 @@ import { ref } from 'vue'
import Button from '@/components/ui/button/Button.vue'
defineProps<{
const { modelValue } = defineProps<{
modelValue: string
}>()
@@ -64,9 +64,9 @@ const handleFileUpload = (event: Event) => {
if (target.files && target.files[0]) {
const file = target.files[0]
const reader = new FileReader()
reader.onload = (e) => {
reader.addEventListener('load', (e) => {
emit('update:modelValue', e.target?.result as string)
}
})
reader.readAsDataURL(file)
}
}

View File

@@ -2,16 +2,12 @@
<template>
<div class="flex flex-row items-center gap-2">
<div class="form-label flex grow items-center">
<span
:id="`${props.id}-label`"
class="text-muted"
:class="props.labelClass"
>
<span :id="`${id}-label`" class="text-muted" :class="labelClass">
<slot name="name-prefix" />
{{ props.item.name }}
{{ item.name }}
<i
v-if="props.item.tooltip"
v-tooltip="props.item.tooltip"
v-if="item.tooltip"
v-tooltip="item.tooltip"
class="pi pi-info-circle bg-transparent"
/>
<slot name="name-suffix" />
@@ -19,11 +15,11 @@
</div>
<div class="form-input flex justify-end">
<component
:is="markRaw(getFormComponent(props.item))"
:id="props.id"
:is="markRaw(getFormComponent(item))"
:id="id"
v-model:model-value="formValue"
:aria-labelledby="`${props.id}-label`"
v-bind="getFormAttrs(props.item)"
:aria-labelledby="`${id}-label`"
v-bind="getFormAttrs(item)"
/>
</div>
</div>
@@ -48,35 +44,37 @@ import UrlInput from '@/components/common/UrlInput.vue'
import type { FormItem } from '@/platform/settings/types'
const formValue = defineModel<unknown>('formValue')
const props = defineProps<{
const { item, id, labelClass } = defineProps<{
item: FormItem
id?: string
labelClass?: string | Record<string, boolean>
}>()
function getFormAttrs(item: FormItem) {
const attrs = { ...(item.attrs || {}) }
const inputType = item.type
function getFormAttrs(formItem: FormItem) {
const attrs = { ...(formItem.attrs || {}) }
const inputType = formItem.type
if (typeof inputType === 'function') {
attrs['renderFunction'] = () =>
inputType(
props.item.name,
(v: unknown) => (formValue.value = v),
formItem.name,
(v: unknown) => {
formValue.value = v
},
formValue.value,
item.attrs
formItem.attrs
)
}
switch (item.type) {
switch (formItem.type) {
case 'combo':
case 'radio':
attrs['options'] =
typeof item.options === 'function'
typeof formItem.options === 'function'
? // @ts-expect-error: Audit and deprecate usage of legacy options type:
// (value) => [string | {text: string, value: string}]
item.options(formValue.value)
: item.options
formItem.options(formValue.value)
: formItem.options
if (typeof item.options?.[0] !== 'string') {
if (typeof formItem.options?.[0] !== 'string') {
attrs['optionLabel'] = 'text'
attrs['optionValue'] = 'value'
}
@@ -85,11 +83,11 @@ function getFormAttrs(item: FormItem) {
return attrs
}
function getFormComponent(item: FormItem): Component {
if (typeof item.type === 'function') {
function getFormComponent(formItem: FormItem): Component {
if (typeof formItem.type === 'function') {
return CustomFormValue
}
switch (item.type) {
switch (formItem.type) {
case 'boolean':
return ToggleSwitch
case 'number':

View File

@@ -5,239 +5,242 @@ import { beforeAll, describe, expect, it } from 'vitest'
import { createApp } from 'vue'
import type { SettingOption } from '@/platform/settings/types'
import FormRadioGroup from './FormRadioGroup.vue'
import type { ComponentProps } from 'vue-component-type-helpers'
describe('FormRadioGroup', () => {
beforeAll(() => {
const app = createApp({})
app.use(PrimeVue)
})
import FormRadioGroup from './FormRadioGroup.vue'
type FormRadioGroupProps = ComponentProps<typeof FormRadioGroup>
const mountComponent = (props: FormRadioGroupProps, options = {}) => {
return mount(FormRadioGroup, {
global: {
plugins: [PrimeVue],
components: { RadioButton }
},
props,
...options
describe(
(FormRadioGroup as { __name?: string }).__name ?? 'FormRadioGroup',
() => {
beforeAll(() => {
const app = createApp({})
app.use(PrimeVue)
})
type FormRadioGroupProps = ComponentProps<typeof FormRadioGroup>
const mountComponent = (props: FormRadioGroupProps, options = {}) => {
return mount(FormRadioGroup, {
global: {
plugins: [PrimeVue],
components: { RadioButton }
},
props,
...options
})
}
describe('normalizedOptions computed property', () => {
it('handles string array options', () => {
const wrapper = mountComponent({
modelValue: 'option1',
options: ['option1', 'option2', 'option3'],
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(3)
expect(radioButtons[0].props('value')).toBe('option1')
expect(radioButtons[1].props('value')).toBe('option2')
expect(radioButtons[2].props('value')).toBe('option3')
const labels = wrapper.findAll('label')
expect(labels[0].text()).toBe('option1')
expect(labels[1].text()).toBe('option2')
expect(labels[2].text()).toBe('option3')
})
it('handles SettingOption array', () => {
const options: SettingOption[] = [
{ text: 'Small', value: 'sm' },
{ text: 'Medium', value: 'md' },
{ text: 'Large', value: 'lg' }
]
const wrapper = mountComponent({
modelValue: 'md',
options,
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(3)
expect(radioButtons[0].props('value')).toBe('sm')
expect(radioButtons[1].props('value')).toBe('md')
expect(radioButtons[2].props('value')).toBe('lg')
const labels = wrapper.findAll('label')
expect(labels[0].text()).toBe('Small')
expect(labels[1].text()).toBe('Medium')
expect(labels[2].text()).toBe('Large')
})
it('handles SettingOption with undefined value (uses text as value)', () => {
const options: SettingOption[] = [
{ text: 'Option A', value: undefined },
{ text: 'Option B' }
]
const wrapper = mountComponent({
modelValue: 'Option A',
options,
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons[0].props('value')).toBe('Option A')
expect(radioButtons[1].props('value')).toBe('Option B')
})
it('handles custom object with optionLabel and optionValue', () => {
const options = [
{ name: 'First Option', id: '1' },
{ name: 'Second Option', id: '2' },
{ name: 'Third Option', id: '3' }
]
const wrapper = mountComponent({
modelValue: 2,
options,
optionLabel: 'name',
optionValue: 'id',
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(3)
expect(radioButtons[0].props('value')).toBe('1')
expect(radioButtons[1].props('value')).toBe('2')
expect(radioButtons[2].props('value')).toBe('3')
const labels = wrapper.findAll('label')
expect(labels[0].text()).toBe('First Option')
expect(labels[1].text()).toBe('Second Option')
expect(labels[2].text()).toBe('Third Option')
})
it('handles mixed array with strings and SettingOptions', () => {
const options: (string | SettingOption)[] = [
'Simple String',
{ text: 'Complex Option', value: 'complex' },
'Another String'
]
const wrapper = mountComponent({
modelValue: 'complex',
options,
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(3)
expect(radioButtons[0].props('value')).toBe('Simple String')
expect(radioButtons[1].props('value')).toBe('complex')
expect(radioButtons[2].props('value')).toBe('Another String')
const labels = wrapper.findAll('label')
expect(labels[0].text()).toBe('Simple String')
expect(labels[1].text()).toBe('Complex Option')
expect(labels[2].text()).toBe('Another String')
})
it('handles empty options array', () => {
const wrapper = mountComponent({
modelValue: null,
options: [],
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(0)
})
it('handles undefined options gracefully', () => {
const wrapper = mountComponent({
modelValue: null,
options: undefined,
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(0)
})
it('handles object with missing properties gracefully', () => {
const options = [{ label: 'Option 1', val: 'opt1' }]
const wrapper = mountComponent({
modelValue: 'opt1',
options,
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(1)
const labels = wrapper.findAll('label')
expect(labels[0].text()).toBe('Unknown')
})
})
describe('component functionality', () => {
it('sets correct input-id and name attributes', () => {
const options = ['A', 'B']
const wrapper = mountComponent({
modelValue: 'A',
options,
id: 'my-radio-group'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons[0].props('inputId')).toBe('my-radio-group-A')
expect(radioButtons[0].props('name')).toBe('my-radio-group')
expect(radioButtons[1].props('inputId')).toBe('my-radio-group-B')
expect(radioButtons[1].props('name')).toBe('my-radio-group')
})
it('associates labels with radio buttons correctly', () => {
const options = ['Yes', 'No']
const wrapper = mountComponent({
modelValue: 'Yes',
options,
id: 'confirm-radio'
})
const labels = wrapper.findAll('label')
expect(labels[0].attributes('for')).toBe('confirm-radio-Yes')
expect(labels[1].attributes('for')).toBe('confirm-radio-No')
})
it('sets aria-describedby attribute correctly', () => {
const options: SettingOption[] = [
{ text: 'Option 1', value: 'opt1' },
{ text: 'Option 2', value: 'opt2' }
]
const wrapper = mountComponent({
modelValue: 'opt1',
options,
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons[0].attributes('aria-describedby')).toBe(
'Option 1-label'
)
expect(radioButtons[1].attributes('aria-describedby')).toBe(
'Option 2-label'
)
})
})
}
describe('normalizedOptions computed property', () => {
it('handles string array options', () => {
const wrapper = mountComponent({
modelValue: 'option1',
options: ['option1', 'option2', 'option3'],
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(3)
expect(radioButtons[0].props('value')).toBe('option1')
expect(radioButtons[1].props('value')).toBe('option2')
expect(radioButtons[2].props('value')).toBe('option3')
const labels = wrapper.findAll('label')
expect(labels[0].text()).toBe('option1')
expect(labels[1].text()).toBe('option2')
expect(labels[2].text()).toBe('option3')
})
it('handles SettingOption array', () => {
const options: SettingOption[] = [
{ text: 'Small', value: 'sm' },
{ text: 'Medium', value: 'md' },
{ text: 'Large', value: 'lg' }
]
const wrapper = mountComponent({
modelValue: 'md',
options,
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(3)
expect(radioButtons[0].props('value')).toBe('sm')
expect(radioButtons[1].props('value')).toBe('md')
expect(radioButtons[2].props('value')).toBe('lg')
const labels = wrapper.findAll('label')
expect(labels[0].text()).toBe('Small')
expect(labels[1].text()).toBe('Medium')
expect(labels[2].text()).toBe('Large')
})
it('handles SettingOption with undefined value (uses text as value)', () => {
const options: SettingOption[] = [
{ text: 'Option A', value: undefined },
{ text: 'Option B' }
]
const wrapper = mountComponent({
modelValue: 'Option A',
options,
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons[0].props('value')).toBe('Option A')
expect(radioButtons[1].props('value')).toBe('Option B')
})
it('handles custom object with optionLabel and optionValue', () => {
const options = [
{ name: 'First Option', id: '1' },
{ name: 'Second Option', id: '2' },
{ name: 'Third Option', id: '3' }
]
const wrapper = mountComponent({
modelValue: 2,
options,
optionLabel: 'name',
optionValue: 'id',
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(3)
expect(radioButtons[0].props('value')).toBe('1')
expect(radioButtons[1].props('value')).toBe('2')
expect(radioButtons[2].props('value')).toBe('3')
const labels = wrapper.findAll('label')
expect(labels[0].text()).toBe('First Option')
expect(labels[1].text()).toBe('Second Option')
expect(labels[2].text()).toBe('Third Option')
})
it('handles mixed array with strings and SettingOptions', () => {
const options: (string | SettingOption)[] = [
'Simple String',
{ text: 'Complex Option', value: 'complex' },
'Another String'
]
const wrapper = mountComponent({
modelValue: 'complex',
options,
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(3)
expect(radioButtons[0].props('value')).toBe('Simple String')
expect(radioButtons[1].props('value')).toBe('complex')
expect(radioButtons[2].props('value')).toBe('Another String')
const labels = wrapper.findAll('label')
expect(labels[0].text()).toBe('Simple String')
expect(labels[1].text()).toBe('Complex Option')
expect(labels[2].text()).toBe('Another String')
})
it('handles empty options array', () => {
const wrapper = mountComponent({
modelValue: null,
options: [],
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(0)
})
it('handles undefined options gracefully', () => {
const wrapper = mountComponent({
modelValue: null,
options: undefined,
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(0)
})
it('handles object with missing properties gracefully', () => {
const options = [{ label: 'Option 1', val: 'opt1' }]
const wrapper = mountComponent({
modelValue: 'opt1',
options,
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(1)
const labels = wrapper.findAll('label')
expect(labels[0].text()).toBe('Unknown')
})
})
describe('component functionality', () => {
it('sets correct input-id and name attributes', () => {
const options = ['A', 'B']
const wrapper = mountComponent({
modelValue: 'A',
options,
id: 'my-radio-group'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons[0].props('inputId')).toBe('my-radio-group-A')
expect(radioButtons[0].props('name')).toBe('my-radio-group')
expect(radioButtons[1].props('inputId')).toBe('my-radio-group-B')
expect(radioButtons[1].props('name')).toBe('my-radio-group')
})
it('associates labels with radio buttons correctly', () => {
const options = ['Yes', 'No']
const wrapper = mountComponent({
modelValue: 'Yes',
options,
id: 'confirm-radio'
})
const labels = wrapper.findAll('label')
expect(labels[0].attributes('for')).toBe('confirm-radio-Yes')
expect(labels[1].attributes('for')).toBe('confirm-radio-No')
})
it('sets aria-describedby attribute correctly', () => {
const options: SettingOption[] = [
{ text: 'Option 1', value: 'opt1' },
{ text: 'Option 2', value: 'opt2' }
]
const wrapper = mountComponent({
modelValue: 'opt1',
options,
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons[0].attributes('aria-describedby')).toBe(
'Option 1-label'
)
expect(radioButtons[1].attributes('aria-describedby')).toBe(
'Option 2-label'
)
})
})
})
)

View File

@@ -26,7 +26,7 @@ import { computed } from 'vue'
import type { SettingOption } from '@/platform/settings/types'
const props = defineProps<{
const { modelValue, options, optionLabel, optionValue, id } = defineProps<{
modelValue: T
options?: (string | SettingOption | Record<string, string>)[]
optionLabel?: string
@@ -39,9 +39,9 @@ defineEmits<{
}>()
const normalizedOptions = computed<SettingOption[]>(() => {
if (!props.options) return []
if (!options) return []
return props.options.map((option) => {
return options.map((option) => {
if (typeof option === 'string') {
return { text: option, value: option }
}
@@ -54,8 +54,8 @@ const normalizedOptions = computed<SettingOption[]>(() => {
}
// Handle optionLabel/optionValue
return {
text: option[props.optionLabel || 'text'] || 'Unknown',
value: option[props.optionValue || 'value']
text: option[optionLabel || 'text'] || 'Unknown',
value: option[optionValue || 'value']
}
})
})

View File

@@ -30,24 +30,25 @@ import InputNumber from 'primevue/inputnumber'
import Knob from 'primevue/knob'
import { ref, watch } from 'vue'
const props = defineProps<{
modelValue: number
inputClass?: string
knobClass?: string
min?: number
max?: number
step?: number
resolution?: number
}>()
const { modelValue, inputClass, knobClass, min, max, step, resolution } =
defineProps<{
modelValue: number
inputClass?: string
knobClass?: string
min?: number
max?: number
step?: number
resolution?: number
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: number): void
}>()
const localValue = ref(props.modelValue)
const localValue = ref(modelValue)
watch(
() => props.modelValue,
() => modelValue,
(newValue) => {
localValue.value = newValue
}
@@ -56,18 +57,18 @@ watch(
const updateValue = (newValue: number | null) => {
if (newValue === null) {
// If the input is cleared, reset to the minimum value or 0
newValue = Number(props.min) || 0
newValue = Number(min) || 0
}
const min = Number(props.min ?? Number.NEGATIVE_INFINITY)
const max = Number(props.max ?? Number.POSITIVE_INFINITY)
const step = Number(props.step) || 1
const minVal = Number(min ?? Number.NEGATIVE_INFINITY)
const maxVal = Number(max ?? Number.POSITIVE_INFINITY)
const stepVal = Number(step) || 1
// Ensure the value is within the allowed range
newValue = Math.max(min, Math.min(max, newValue))
newValue = Math.max(minVal, Math.min(maxVal, newValue))
// Round to the nearest step
newValue = Math.round(newValue / step) * step
newValue = Math.round(newValue / stepVal) * stepVal
// Update local value and emit change
localValue.value = newValue
@@ -76,11 +77,11 @@ const updateValue = (newValue: number | null) => {
const displayValue = (value: number): string => {
updateValue(value)
const stepString = (props.step ?? 1).toString()
const resolution = stepString.includes('.')
const stepString = (step ?? 1).toString()
const decimalPlaces = stepString.includes('.')
? stepString.split('.')[1].length
: 0
return value.toFixed(props.resolution ?? resolution)
return value.toFixed(resolution ?? decimalPlaces)
}
defineOptions({

View File

@@ -29,7 +29,7 @@ import InputNumber from 'primevue/inputnumber'
import Slider from 'primevue/slider'
import { ref, watch } from 'vue'
const props = defineProps<{
const { modelValue, inputClass, sliderClass, min, max, step } = defineProps<{
modelValue: number
inputClass?: string
sliderClass?: string
@@ -42,10 +42,10 @@ const emit = defineEmits<{
(e: 'update:modelValue', value: number): void
}>()
const localValue = ref(props.modelValue)
const localValue = ref(modelValue)
watch(
() => props.modelValue,
() => modelValue,
(newValue) => {
localValue.value = newValue
}
@@ -54,18 +54,18 @@ watch(
const updateValue = (newValue: number | null) => {
if (newValue === null) {
// If the input is cleared, reset to the minimum value or 0
newValue = Number(props.min) || 0
newValue = Number(min) || 0
}
const min = Number(props.min ?? Number.NEGATIVE_INFINITY)
const max = Number(props.max ?? Number.POSITIVE_INFINITY)
const step = Number(props.step) || 1
const minVal = Number(min ?? Number.NEGATIVE_INFINITY)
const maxVal = Number(max ?? Number.POSITIVE_INFINITY)
const stepVal = Number(step) || 1
// Ensure the value is within the allowed range
newValue = Math.max(min, Math.min(max, newValue))
newValue = Math.max(minVal, Math.min(maxVal, newValue))
// Round to the nearest step
newValue = Math.round(newValue / step) * step
newValue = Math.round(newValue / stepVal) * stepVal
// Update local value and emit change
localValue.value = newValue

View File

@@ -41,7 +41,6 @@ const spinnerSizeClass = computed(() => {
switch (size) {
case 'sm':
return 'h-6 w-6 border-2'
case 'md':
default:
return 'h-12 w-12 border-4'
}

View File

@@ -1,5 +1,5 @@
<template>
<div class="no-results-placeholder h-full p-8" :class="props.class">
<div :class="cn('no-results-placeholder h-full p-8', className)">
<Card>
<template #content>
<div class="flex flex-col items-center">
@@ -25,8 +25,16 @@
import Card from 'primevue/card'
import Button from '@/components/ui/button/Button.vue'
import { cn } from '@/utils/tailwindUtil'
const props = defineProps<{
const {
class: className,
icon,
title,
message,
textClass,
buttonLabel
} = defineProps<{
class?: string
icon?: string
title: string

View File

@@ -19,7 +19,7 @@ const i18n = createI18n({
}
})
describe('SearchBox', () => {
describe((SearchBox as { __name?: string }).__name ?? 'SearchBox', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers()

View File

@@ -18,7 +18,7 @@ export interface SearchFilter {
id: string | number
}
defineProps<Omit<SearchFilter, 'id'>>()
const { text, badge, badgeClass } = defineProps<Omit<SearchFilter, 'id'>>()
defineEmits(['remove'])
</script>

View File

@@ -21,9 +21,9 @@
<h2 class="mb-4 text-2xl font-semibold">
{{ $t('g.devices') }}
</h2>
<TabView v-if="props.stats.devices.length > 1">
<TabView v-if="stats.devices.length > 1">
<TabPanel
v-for="device in props.stats.devices"
v-for="device in stats.devices"
:key="device.index"
:header="device.name"
:value="device.index"
@@ -31,7 +31,7 @@
<DeviceInfo :device="device" />
</TabPanel>
</TabView>
<DeviceInfo v-else :device="props.stats.devices[0]" />
<DeviceInfo v-else :device="stats.devices[0]" />
</div>
</template>
</div>
@@ -48,16 +48,16 @@ import { isCloud } from '@/platform/distribution/types'
import type { SystemStats } from '@/schemas/apiSchema'
import { formatCommitHash, formatSize } from '@/utils/formatUtil'
const props = defineProps<{
const { stats } = defineProps<{
stats: SystemStats
}>()
const systemInfo = computed(() => ({
...props.stats.system,
argv: props.stats.system.argv.join(' ')
...stats.system,
argv: stats.system.argv.join(' ')
}))
const hasDevices = computed(() => props.stats.devices.length > 0)
const hasDevices = computed(() => stats.devices.length > 0)
type SystemInfoKey = keyof SystemStats['system']

View File

@@ -4,7 +4,7 @@
v-model:expanded-keys="expandedKeys"
v-model:selection-keys="selectionKeys"
class="tree-explorer px-2 py-0 2xl:px-4 bg-transparent"
:class="props.class"
:class="className"
:value="renderedRoot.children"
selection-mode="single"
:pt="{
@@ -12,9 +12,9 @@
nodeContent: ({ context }) => ({
class: 'group/tree-node',
onClick: (e: MouseEvent) =>
onNodeContentClick(e, context.node as RenderedTreeExplorerNode<T>),
onNodeContentClick(e, context.node as RenderedTreeExplorerNode),
onContextmenu: (e: MouseEvent) =>
handleContextMenu(e, context.node as RenderedTreeExplorerNode<T>)
handleContextMenu(e, context.node as RenderedTreeExplorerNode)
}),
nodeToggleButton: () => ({
onClick: (e: MouseEvent) => {
@@ -36,11 +36,11 @@
</Tree>
<ContextMenu ref="menu" :model="menuItems" />
</template>
<script setup lang="ts" generic="T">
<script setup lang="ts">
import ContextMenu from 'primevue/contextmenu'
import type { MenuItem, MenuItemCommandEvent } from 'primevue/menuitem'
import Tree from 'primevue/tree'
import { computed, provide, ref, shallowRef } from 'vue'
import { computed, provide, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
@@ -68,14 +68,14 @@ const selectionKeys = defineModel<Record<string, boolean>>('selectionKeys')
// Tracks whether the caller has set the selectionKeys model.
const storeSelectionKeys = selectionKeys.value !== undefined
const props = defineProps<{
root: TreeExplorerNode<T>
const { root, class: className } = defineProps<{
root: TreeExplorerNode
class?: string
}>()
const emit = defineEmits<{
(e: 'nodeClick', node: RenderedTreeExplorerNode<T>, event: MouseEvent): void
(e: 'nodeDelete', node: RenderedTreeExplorerNode<T>): void
(e: 'contextMenu', node: RenderedTreeExplorerNode<T>, event: MouseEvent): void
(e: 'nodeClick', node: RenderedTreeExplorerNode, event: MouseEvent): void
(e: 'nodeDelete', node: RenderedTreeExplorerNode): void
(e: 'contextMenu', node: RenderedTreeExplorerNode, event: MouseEvent): void
}>()
const {
@@ -83,19 +83,19 @@ const {
getAddFolderMenuItem,
handleFolderCreation,
addFolderCommand
} = useTreeFolderOperations<T>(
/* expandNode */ (node: TreeExplorerNode<T>) => {
} = useTreeFolderOperations(
/* expandNode */ (node: TreeExplorerNode) => {
expandedKeys.value[node.key] = true
}
)
const renderedRoot = computed<RenderedTreeExplorerNode<T>>(() => {
const renderedRoot = fillNodeInfo(props.root)
const renderedRoot = computed<RenderedTreeExplorerNode>(() => {
const renderedRoot = fillNodeInfo(root)
return newFolderNode.value
? combineTrees(renderedRoot, newFolderNode.value)
: renderedRoot
})
const getTreeNodeIcon = (node: TreeExplorerNode<T>) => {
const getTreeNodeIcon = (node: TreeExplorerNode) => {
if (node.getIcon) {
const icon = node.getIcon()
if (icon) {
@@ -111,9 +111,7 @@ const getTreeNodeIcon = (node: TreeExplorerNode<T>) => {
const isExpanded = expandedKeys.value?.[node.key] ?? false
return isExpanded ? 'pi pi-folder-open' : 'pi pi-folder'
}
const fillNodeInfo = (
node: TreeExplorerNode<T>
): RenderedTreeExplorerNode<T> => {
const fillNodeInfo = (node: TreeExplorerNode): RenderedTreeExplorerNode => {
const children = node.children?.map(fillNodeInfo) ?? []
const totalLeaves = node.leaf
? 1
@@ -130,7 +128,7 @@ const fillNodeInfo = (
}
const onNodeContentClick = async (
e: MouseEvent,
node: RenderedTreeExplorerNode<T>
node: RenderedTreeExplorerNode
) => {
if (!storeSelectionKeys) {
selectionKeys.value = {}
@@ -141,22 +139,20 @@ const onNodeContentClick = async (
emit('nodeClick', node, e)
}
const menu = ref<InstanceType<typeof ContextMenu> | null>(null)
const menuTargetNode = shallowRef<RenderedTreeExplorerNode<T> | null>(null)
const menuTargetNode = ref<RenderedTreeExplorerNode | null>(null)
const extraMenuItems = computed(() => {
const node = menuTargetNode.value
return node?.contextMenuItems
? typeof node.contextMenuItems === 'function'
? node.contextMenuItems(node)
: node.contextMenuItems
return menuTargetNode.value?.contextMenuItems
? typeof menuTargetNode.value.contextMenuItems === 'function'
? menuTargetNode.value.contextMenuItems(menuTargetNode.value)
: menuTargetNode.value.contextMenuItems
: []
})
const renameEditingNode = shallowRef<RenderedTreeExplorerNode<T> | null>(null)
const renameEditingNode = ref<RenderedTreeExplorerNode | null>(null)
const errorHandling = useErrorHandling()
const handleNodeLabelEdit = async (
n: RenderedTreeExplorerNode,
node: RenderedTreeExplorerNode,
newName: string
) => {
const node = n as RenderedTreeExplorerNode<T>
await errorHandling.wrapWithErrorHandlingAsync(
async () => {
if (node.key === newFolderNode.value?.key) {
@@ -174,36 +170,35 @@ const handleNodeLabelEdit = async (
provide(InjectKeyHandleEditLabelFunction, handleNodeLabelEdit)
const { t } = useI18n()
const renameCommand = (node: RenderedTreeExplorerNode<T>) => {
const renameCommand = (node: RenderedTreeExplorerNode) => {
renameEditingNode.value = node
}
const deleteCommand = async (node: RenderedTreeExplorerNode<T>) => {
const deleteCommand = async (node: RenderedTreeExplorerNode) => {
await node.handleDelete?.()
emit('nodeDelete', node)
}
const menuItems = computed<MenuItem[]>(() => {
const node = menuTargetNode.value
return [
getAddFolderMenuItem(node),
const menuItems = computed<MenuItem[]>(() =>
[
getAddFolderMenuItem(menuTargetNode.value),
{
label: t('g.rename'),
icon: 'pi pi-file-edit',
command: () => {
if (node) {
renameCommand(node)
if (menuTargetNode.value) {
renameCommand(menuTargetNode.value)
}
},
visible: node?.handleRename !== undefined
visible: menuTargetNode.value?.handleRename !== undefined
},
{
label: t('g.delete'),
icon: 'pi pi-trash',
command: async () => {
if (node) {
await deleteCommand(node)
if (menuTargetNode.value) {
await deleteCommand(menuTargetNode.value)
}
},
visible: node?.handleDelete !== undefined,
visible: menuTargetNode.value?.handleDelete !== undefined,
isAsync: true // The delete command can be async
},
...extraMenuItems.value
@@ -215,12 +210,9 @@ const menuItems = computed<MenuItem[]>(() => {
})
: undefined
}))
})
)
const handleContextMenu = (
e: MouseEvent,
node: RenderedTreeExplorerNode<T>
) => {
const handleContextMenu = (e: MouseEvent, node: RenderedTreeExplorerNode) => {
menuTargetNode.value = node
emit('contextMenu', node, e)
if (menuItems.value.filter((item) => item.visible).length > 0) {
@@ -232,13 +224,15 @@ const wrapCommandWithErrorHandler = (
command: (event: MenuItemCommandEvent) => void,
{ isAsync = false }: { isAsync: boolean }
) => {
const node = menuTargetNode.value
return isAsync
? errorHandling.wrapWithErrorHandlingAsync(
command as (event: MenuItemCommandEvent) => Promise<void>,
node?.handleError
menuTargetNode.value?.handleError
)
: errorHandling.wrapWithErrorHandling(
command,
menuTargetNode.value?.handleError
)
: errorHandling.wrapWithErrorHandling(command, node?.handleError)
}
defineExpose({

View File

@@ -19,7 +19,7 @@ const i18n = createI18n({
messages: {}
})
describe('TreeExplorerTreeNode', () => {
describe(TreeExplorerTreeNode.__name ?? 'TreeExplorerTreeNode', () => {
const mockNode = {
key: '1',
label: 'Test Node',

View File

@@ -5,21 +5,21 @@
'tree-node',
{
'can-drop': canDrop,
'tree-folder': !props.node.leaf,
'tree-leaf': props.node.leaf
'tree-folder': !node.leaf,
'tree-leaf': node.leaf
}
]"
:data-testid="`tree-node-${node.key}`"
>
<div class="node-content">
<span class="node-label">
<slot name="before-label" :node="props.node" />
<slot name="before-label" :node="node" />
<EditableText
:model-value="node.label"
:is-editing="isEditing"
@edit="handleRename"
/>
<slot name="after-label" :node="props.node" />
<slot name="after-label" :node="node" />
</span>
<Badge
v-if="showNodeBadgeText"
@@ -31,12 +31,12 @@
<div
class="node-actions flex gap-1 touch:opacity-100 motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
>
<slot name="actions" :node="props.node" />
<slot name="actions" :node="node" />
</div>
</div>
</template>
<script setup lang="ts" generic="T">
<script setup lang="ts">
import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview'
import Badge from 'primevue/badge'
import { computed, inject, ref } from 'vue'
@@ -52,35 +52,35 @@ import type {
TreeExplorerDragAndDropData
} from '@/types/treeExplorerTypes'
const props = defineProps<{
node: RenderedTreeExplorerNode<T>
const { node } = defineProps<{
node: RenderedTreeExplorerNode
}>()
const emit = defineEmits<{
(
e: 'itemDropped',
node: RenderedTreeExplorerNode<T>,
data: RenderedTreeExplorerNode<T>
node: RenderedTreeExplorerNode,
data: RenderedTreeExplorerNode
): void
(e: 'dragStart', node: RenderedTreeExplorerNode<T>): void
(e: 'dragEnd', node: RenderedTreeExplorerNode<T>): void
(e: 'dragStart', node: RenderedTreeExplorerNode): void
(e: 'dragEnd', node: RenderedTreeExplorerNode): void
}>()
const nodeBadgeText = computed<string>(() => {
if (props.node.leaf) {
if (node.leaf) {
return ''
}
if (props.node.badgeText !== undefined && props.node.badgeText !== null) {
return props.node.badgeText
if (node.badgeText !== undefined && node.badgeText !== null) {
return node.badgeText
}
return props.node.totalLeaves.toString()
return node.totalLeaves.toString()
})
const showNodeBadgeText = computed<boolean>(() => nodeBadgeText.value !== '')
const isEditing = computed<boolean>(() => props.node.isEditingLabel ?? false)
const isEditing = computed<boolean>(() => node.isEditingLabel ?? false)
const handleEditLabel = inject(InjectKeyHandleEditLabelFunction)
const handleRename = (newName: string) => {
handleEditLabel?.(props.node as RenderedTreeExplorerNode, newName)
handleEditLabel?.(node, newName)
}
const container = ref<HTMLElement | null>(null)
@@ -89,21 +89,21 @@ const canDrop = ref(false)
const treeNodeElementGetter = () =>
container.value?.closest('.p-tree-node-content') as HTMLElement
if (props.node.draggable) {
if (node.draggable) {
usePragmaticDraggable(treeNodeElementGetter, {
getInitialData: () => {
return {
type: 'tree-explorer-node',
data: props.node
data: node
}
},
onDragStart: () => emit('dragStart', props.node),
onDrop: () => emit('dragEnd', props.node),
onGenerateDragPreview: props.node.renderDragPreview
onDragStart: () => emit('dragStart', node),
onDrop: () => emit('dragEnd', node),
onGenerateDragPreview: node.renderDragPreview
? ({ nativeSetDragImage }) => {
setCustomNativeDragPreview({
render: ({ container }) => {
return props.node.renderDragPreview?.(container)
return node.renderDragPreview?.(container)
},
nativeSetDragImage
})
@@ -112,18 +112,14 @@ if (props.node.draggable) {
})
}
if (props.node.droppable) {
if (node.droppable) {
usePragmaticDroppable(treeNodeElementGetter, {
onDrop: async (event) => {
const dndData = event.source.data as TreeExplorerDragAndDropData
if (dndData.type === 'tree-explorer-node') {
await props.node.handleDrop?.(dndData as TreeExplorerDragAndDropData<T>)
await node.handleDrop?.(dndData)
canDrop.value = false
emit(
'itemDropped',
props.node,
dndData.data as RenderedTreeExplorerNode<T>
)
emit('itemDropped', node, dndData.data)
}
},
onDragEnter: (event) => {

View File

@@ -6,10 +6,11 @@ import InputText from 'primevue/inputtext'
import { beforeEach, describe, expect, it } from 'vitest'
import { createApp, nextTick } from 'vue'
import UrlInput from './UrlInput.vue'
import type { ComponentProps } from 'vue-component-type-helpers'
describe('UrlInput', () => {
import UrlInput from './UrlInput.vue'
describe(UrlInput.__name ?? 'UrlInput', () => {
beforeEach(() => {
const app = createApp({})
app.use(PrimeVue)

View File

@@ -17,7 +17,7 @@
'pi pi-times cursor-pointer text-red-500':
validationState === ValidationState.INVALID
}"
@click="validateUrl(props.modelValue)"
@click="validateUrl(model)"
/>
</IconField>
</template>
@@ -32,40 +32,34 @@ import { isValidUrl } from '@/utils/formatUtil'
import { checkUrlReachable } from '@/utils/networkUtil'
import { ValidationState } from '@/utils/validationUtil'
const props = defineProps<{
modelValue: string
const model = defineModel<string>({ required: true })
const { validateUrlFn } = defineProps<{
validateUrlFn?: (url: string) => Promise<boolean>
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
'state-change': [state: ValidationState]
}>()
const validationState = ref<ValidationState>(ValidationState.IDLE)
const cleanInput = (value: string): string =>
value ? value.replace(/\s+/g, '') : ''
value ? value.replaceAll(/\s+/g, '') : ''
// Add internal value state
const internalValue = ref(cleanInput(props.modelValue))
const internalValue = ref(cleanInput(model.value))
// Watch for external modelValue changes
watch(
() => props.modelValue,
async (newValue: string) => {
internalValue.value = cleanInput(newValue)
await validateUrl(newValue)
}
)
watch(model, async (newValue: string) => {
internalValue.value = cleanInput(newValue)
await validateUrl(newValue)
})
watch(validationState, (newState) => {
emit('state-change', newState)
})
// Validate on mount
onMounted(async () => {
await validateUrl(props.modelValue)
await validateUrl(model.value)
})
const handleInput = (value: string | undefined) => {
@@ -87,7 +81,7 @@ const handleBlur = async () => {
}
// Emit the update only on blur
emit('update:modelValue', normalizedUrl)
model.value = normalizedUrl
}
// Default validation implementation
@@ -113,7 +107,7 @@ const validateUrl = async (value: string) => {
validationState.value = ValidationState.LOADING
try {
const isValid = await (props.validateUrlFn ?? defaultValidateUrl)(url)
const isValid = await (validateUrlFn ?? defaultValidateUrl)(url)
validationState.value = isValid
? ValidationState.VALID
: ValidationState.INVALID

View File

@@ -23,7 +23,7 @@ const i18n = createI18n({
}
})
describe('UserAvatar', () => {
describe(UserAvatar.__name ?? 'UserAvatar', () => {
beforeEach(() => {
const app = createApp({})
app.use(PrimeVue)

View File

@@ -39,7 +39,7 @@ vi.mock('@/stores/firebaseAuthStore', () => ({
}))
}))
describe('UserCredit', () => {
describe(UserCredit.__name ?? 'UserCredit', () => {
beforeEach(() => {
vi.clearAllMocks()
mockBalance.value = {

View File

@@ -444,7 +444,6 @@ const distributions = computed(() => {
return [TemplateIncludeOnDistributionEnum.Cloud]
case 'localhost':
return [TemplateIncludeOnDistributionEnum.Local]
case 'desktop':
default:
if (systemStatsStore.systemStats?.system.os === 'darwin') {
return [
@@ -595,12 +594,10 @@ const coordinateNavAndSort = (source: 'nav' | 'sort') => {
// When navigating away from 'Popular' category while sort is 'Popular', reset sort to default.
sortBy.value = 'default'
}
} else if (source === 'sort') {
} else if (source === 'sort' && isPopularNav && !isPopularSort) {
// When sort is changed away from 'Popular' while in the 'Popular' category,
// reset the category to 'All Templates' to avoid a confusing state.
if (isPopularNav && !isPopularSort) {
selectedNavItem.value = 'all'
}
selectedNavItem.value = 'all'
}
}
@@ -681,37 +678,37 @@ const runsOnOptions = computed(() =>
const modelFilterLabel = computed(() => {
if (selectedModelObjects.value.length === 0) {
return t('templateWorkflows.modelFilter', 'Model Filter')
} else if (selectedModelObjects.value.length === 1) {
return selectedModelObjects.value[0].name
} else {
return t('templateWorkflows.modelsSelected', {
count: selectedModelObjects.value.length
})
}
if (selectedModelObjects.value.length === 1) {
return selectedModelObjects.value[0].name
}
return t('templateWorkflows.modelsSelected', {
count: selectedModelObjects.value.length
})
})
const useCaseFilterLabel = computed(() => {
if (selectedUseCaseObjects.value.length === 0) {
return t('templateWorkflows.useCaseFilter', 'Use Case')
} else if (selectedUseCaseObjects.value.length === 1) {
return selectedUseCaseObjects.value[0].name
} else {
return t('templateWorkflows.useCasesSelected', {
count: selectedUseCaseObjects.value.length
})
}
if (selectedUseCaseObjects.value.length === 1) {
return selectedUseCaseObjects.value[0].name
}
return t('templateWorkflows.useCasesSelected', {
count: selectedUseCaseObjects.value.length
})
})
const runsOnFilterLabel = computed(() => {
if (selectedRunsOnObjects.value.length === 0) {
return t('templateWorkflows.runsOnFilter', 'Runs On')
} else if (selectedRunsOnObjects.value.length === 1) {
return selectedRunsOnObjects.value[0].name
} else {
return t('templateWorkflows.runsOnSelected', {
count: selectedRunsOnObjects.value.length
})
}
if (selectedRunsOnObjects.value.length === 1) {
return selectedRunsOnObjects.value[0].name
}
return t('templateWorkflows.runsOnSelected', {
count: selectedRunsOnObjects.value.length
})
})
// Sort options

View File

@@ -26,7 +26,7 @@ const handleBeforeUnload = (event: BeforeUnloadEvent) => {
event.preventDefault()
return true
}
return undefined
return
}
onMounted(() => {

View File

@@ -6,7 +6,7 @@
</div>
</template>
<script setup lang="ts">
defineProps<{
const { title } = defineProps<{
title?: string
}>()
</script>

View File

@@ -120,7 +120,13 @@ import { useDialogService } from '@/services/dialogService'
import type { ConfirmationDialogType } from '@/services/dialogService'
import { useDialogStore } from '@/stores/dialogStore'
const props = defineProps<{
const {
message,
type,
onConfirm: onConfirmProp,
itemList,
hint
} = defineProps<{
message: string
type: ConfirmationDialogType
onConfirm: (value?: boolean) => void
@@ -143,14 +149,14 @@ function openBlueprintOverwriteSetting() {
const doNotAskAgain = ref(false)
const onDeny = () => {
props.onConfirm(false)
onConfirmProp(false)
useDialogStore().closeDialog()
}
const onConfirm = () => {
if (props.type === 'overwriteBlueprint' && doNotAskAgain.value)
if (type === 'overwriteBlueprint' && doNotAskAgain.value)
void useSettingStore().set('Comfy.Workflow.WarnBlueprintOverwrite', false)
props.onConfirm(true)
onConfirmProp(true)
useDialogStore().closeDialog()
}
</script>

View File

@@ -30,7 +30,7 @@ const createMockNode = (type: string, version?: string): LGraphNode =>
outputs: []
})
describe('MissingCoreNodesMessage', () => {
describe(MissingCoreNodesMessage.__name ?? 'MissingCoreNodesMessage', () => {
const mockSystemStatsStore = {
systemStats: null as { system?: { comfyui_version?: string } } | null,
refetchSystemStats: vi.fn()

View File

@@ -94,7 +94,7 @@ interface ModelInfo {
folder_path?: string
}
const props = defineProps<{
const { missingModels: missingModelsProp, paths } = defineProps<{
missingModels: ModelInfo[]
paths: Record<string, string[]>
}>()
@@ -113,9 +113,9 @@ function openShowMissingModelsSetting() {
const modelDownloads = ref<Record<string, ModelInfo>>({})
const missingModels = computed(() => {
return props.missingModels.map((model) => {
const paths = props.paths[model.directory]
if (model.directory_invalid || !paths) {
return missingModelsProp.map((model) => {
const modelPaths = paths[model.directory]
if (model.directory_invalid || !modelPaths) {
return {
label: `${model.directory} / ${model.name}`,
url: model.url,
@@ -130,7 +130,7 @@ const missingModels = computed(() => {
name: model.name,
directory: model.directory,
url: model.url,
folder_path: paths[0]
folder_path: modelPaths[0]
}
modelDownloads.value[model.name] = downloadInfo
if (!whiteListedUrls.has(model.url)) {
@@ -157,7 +157,7 @@ const missingModels = computed(() => {
progress: downloadInfo.progress,
error: downloadInfo.error,
name: model.name,
paths: paths,
paths: modelPaths,
folderPath: downloadInfo.folder_path
}
})

View File

@@ -54,7 +54,7 @@ import { isCloud } from '@/platform/distribution/types'
import type { MissingNodeType } from '@/types/comfy'
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
const props = defineProps<{
const { missingNodeTypes } = defineProps<{
missingNodeTypes: MissingNodeType[]
}>()
@@ -63,7 +63,7 @@ const { missingCoreNodes } = useMissingNodes()
const uniqueNodes = computed(() => {
const seenTypes = new Set()
return props.missingNodeTypes
return missingNodeTypes
.filter((node) => {
const type = typeof node === 'object' ? node.type : node
if (seenTypes.has(type)) return false

View File

@@ -25,17 +25,22 @@ import { ref } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import { useDialogStore } from '@/stores/dialogStore'
const props = defineProps<{
const {
message,
defaultValue,
onConfirm: onConfirmProp,
placeholder
} = defineProps<{
message: string
defaultValue: string
onConfirm: (value: string) => void
placeholder?: string
}>()
const inputValue = ref<string>(props.defaultValue)
const inputValue = ref<string>(defaultValue)
const onConfirm = () => {
props.onConfirm(inputValue.value)
onConfirmProp(inputValue.value)
useDialogStore().closeDialog()
}

View File

@@ -25,7 +25,7 @@ const mountOption = (
}
})
describe('CreditTopUpOption', () => {
describe(CreditTopUpOption.__name ?? 'CreditTopUpOption', () => {
it('renders credit amount and description', () => {
const wrapper = mountOption({ credits: 5000, description: '~500 videos*' })
expect(wrapper.text()).toContain('5,000')

View File

@@ -11,23 +11,20 @@ import { computed } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import { useTelemetry } from '@/platform/telemetry'
const props = defineProps<{
const { errorMessage, repoOwner, repoName } = defineProps<{
errorMessage: string
repoOwner: string
repoName: string
}>()
const queryString = computed(() => props.errorMessage + ' is:issue')
const queryString = computed(() => `${errorMessage} is:issue`)
/**
* Open GitHub issues search and track telemetry.
*/
const openGitHubIssues = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'error_dialog_find_existing_issues_clicked'
})
const query = encodeURIComponent(queryString.value)
const url = `https://github.com/${props.repoOwner}/${props.repoName}/issues?q=${query}`
const url = `https://github.com/${repoOwner}/${repoName}/issues?q=${query}`
window.open(url, '_blank')
}
</script>

View File

@@ -268,7 +268,7 @@ async function saveKeybinding() {
const commandId = currentEditingCommand.value?.id
const combo = newBindingKeyCombo.value
cancelEdit()
if (!combo || commandId == undefined) return
if (!combo || commandId == null) return
const updated = keybindingStore.updateKeybindingOnCommand(
new KeybindingImpl({ commandId, combo })

View File

@@ -1,5 +1,5 @@
<template>
<TabPanel :value="props.value" class="h-full w-full" :class="props.class">
<TabPanel :value class="h-full w-full" :class="panelClass">
<div class="flex h-full w-full flex-col gap-2">
<slot name="header" />
<ScrollPanel class="h-0 grow pr-2">
@@ -14,7 +14,7 @@
import ScrollPanel from 'primevue/scrollpanel'
import TabPanel from 'primevue/tabpanel'
const props = defineProps<{
const { value, class: panelClass } = defineProps<{
value: string
class?: string
}>()

View File

@@ -17,7 +17,7 @@ vi.mock('@/utils/formatUtil', () => ({
normalizeI18nKey: vi.fn()
}))
describe('SettingItem', () => {
describe(SettingItem.__name ?? 'SettingItem', () => {
const mountComponent = (props: Record<string, unknown>, options = {}) => {
return mount(SettingItem, {
global: {

View File

@@ -76,7 +76,7 @@ const i18n = createI18n({
}
})
describe('UsageLogsTable', () => {
describe(UsageLogsTable.__name ?? 'UsageLogsTable', () => {
const mockEventsResponse = {
events: [
{

View File

@@ -169,9 +169,10 @@ const loadEvents = async () => {
} else {
error.value = customerEventService.error.value || 'Failed to load events'
}
} catch (err) {
error.value = err instanceof Error ? err.message : 'Unknown error'
console.error('Error loading events:', err)
} catch (errorCaught) {
error.value =
errorCaught instanceof Error ? errorCaught.message : 'Unknown error'
console.error('Error loading events:', errorCaught)
} finally {
loading.value = false
}

View File

@@ -56,7 +56,7 @@ const i18n = createI18n({
}
})
describe('ApiKeyForm', () => {
describe(ApiKeyForm.__name ?? 'ApiKeyForm', () => {
beforeEach(() => {
const app = createApp({})
app.use(PrimeVue)

View File

@@ -58,7 +58,7 @@ vi.mock('primevue/usetoast', () => ({
}))
}))
describe('SignInForm', () => {
describe(SignInForm.__name ?? 'SignInForm', () => {
beforeEach(() => {
vi.clearAllMocks()
mockSendPasswordReset.mockReset()
@@ -110,12 +110,17 @@ describe('SignInForm', () => {
'span.text-muted.text-base.font-medium.cursor-pointer'
)
// Mock getElementById to track focus
// Mock querySelector to track focus on the email input
const mockFocus = vi.fn()
const mockElement: Partial<HTMLElement> = { focus: mockFocus }
vi.spyOn(document, 'getElementById').mockReturnValue(
mockElement as HTMLElement
)
const originalQuerySelector = document.querySelector.bind(document)
const spy = vi
.spyOn(document, 'querySelector')
.mockImplementation((selector: string) => {
if (selector === '#comfy-org-sign-in-email')
return mockElement as HTMLElement
return originalQuerySelector(selector)
})
// Click forgot password link while email is empty
await forgotPasswordSpan.trigger('click')
@@ -129,10 +134,9 @@ describe('SignInForm', () => {
})
// Should focus email input
expect(document.getElementById).toHaveBeenCalledWith(
'comfy-org-sign-in-email'
)
expect(spy).toHaveBeenCalledWith('#comfy-org-sign-in-email')
expect(mockFocus).toHaveBeenCalled()
spy.mockRestore()
// Should NOT call sendPasswordReset
expect(mockSendPasswordReset).not.toHaveBeenCalled()
@@ -212,7 +216,7 @@ describe('SignInForm', () => {
expect(wrapper.findComponent(ProgressSpinner).exists()).toBe(true)
expect(wrapper.findComponent(Button).exists()).toBe(false)
} catch (error) {
} catch {
// Fallback test - check HTML content if component rendering fails
mockLoading = true
const wrapper = mountComponent()
@@ -270,21 +274,25 @@ describe('SignInForm', () => {
onSubmit: (data: { valid: boolean; values: unknown }) => void
}
// Mock getElementById to track focus
// Mock querySelector to track focus on the email input
const mockFocus = vi.fn()
const mockElement: Partial<HTMLElement> = { focus: mockFocus }
vi.spyOn(document, 'getElementById').mockReturnValue(
mockElement as HTMLElement
)
const originalQuerySelector = document.querySelector.bind(document)
const spy = vi
.spyOn(document, 'querySelector')
.mockImplementation((selector: string) => {
if (selector === '#comfy-org-sign-in-email')
return mockElement as HTMLElement
return originalQuerySelector(selector)
})
// Call handleForgotPassword with no email
await component.handleForgotPassword('', false)
// Should focus email input
expect(document.getElementById).toHaveBeenCalledWith(
'comfy-org-sign-in-email'
)
expect(spy).toHaveBeenCalledWith('#comfy-org-sign-in-email')
expect(mockFocus).toHaveBeenCalled()
spy.mockRestore()
})
it('does not focus email input when valid email is provided', async () => {

View File

@@ -120,7 +120,7 @@ const handleForgotPassword = async (
life: 5_000
})
// Focus the email input
document.getElementById(emailInputId)?.focus?.()
document.querySelector<HTMLElement>(`#${emailInputId}`)?.focus?.()
return
}
await firebaseAuthActions.sendPasswordReset(email)

View File

@@ -52,7 +52,7 @@ import Button from '@/components/ui/button/Button.vue'
import { useBillingContext } from '@/composables/billing/useBillingContext'
import { useDialogStore } from '@/stores/dialogStore'
const props = defineProps<{
const { cancelAt } = defineProps<{
cancelAt?: string
}>()
@@ -64,7 +64,7 @@ const { cancelSubscription, fetchStatus, subscription } = useBillingContext()
const isLoading = ref(false)
const formattedEndDate = computed(() => {
const dateStr = props.cancelAt ?? subscription.value?.endDate
const dateStr = cancelAt ?? subscription.value?.endDate
if (!dateStr) return t('subscription.cancelDialog.endOfBillingPeriod')
const date = new Date(dateStr)
return date.toLocaleDateString('en-US', {

View File

@@ -70,17 +70,31 @@
@click="onSelectLink"
/>
<div
class="absolute right-3 top-2.5 cursor-pointer"
class="absolute right-4 top-2 cursor-pointer"
@click="onCopyLink"
>
<i
:class="
cn(
'pi size-4',
justCopied ? 'pi-check text-green-500' : 'pi-copy'
)
"
/>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<g clip-path="url(#clip0_2127_14348)">
<path
d="M2.66634 10.6666C1.93301 10.6666 1.33301 10.0666 1.33301 9.33325V2.66659C1.33301 1.93325 1.93301 1.33325 2.66634 1.33325H9.33301C10.0663 1.33325 10.6663 1.93325 10.6663 2.66659M6.66634 5.33325H13.333C14.0694 5.33325 14.6663 5.93021 14.6663 6.66658V13.3333C14.6663 14.0696 14.0694 14.6666 13.333 14.6666H6.66634C5.92996 14.6666 5.33301 14.0696 5.33301 13.3333V6.66658C5.33301 5.93021 5.92996 5.33325 6.66634 5.33325Z"
stroke="white"
stroke-width="1.3"
stroke-linecap="round"
stroke-linejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_2127_14348">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
</div>
</div>
</div>
@@ -104,7 +118,6 @@ import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { cn } from '@/utils/tailwindUtil'
import { useTeamWorkspaceStore } from '@/platform/workspace/stores/teamWorkspaceStore'
import { useDialogStore } from '@/stores/dialogStore'
@@ -117,7 +130,6 @@ const loading = ref(false)
const email = ref('')
const step = ref<'email' | 'link'>('email')
const generatedLink = ref('')
const justCopied = ref(false)
const isValidEmail = computed(() => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
@@ -149,10 +161,6 @@ async function onCreateLink() {
async function onCopyLink() {
try {
await navigator.clipboard.writeText(generatedLink.value)
justCopied.value = true
setTimeout(() => {
justCopied.value = false
}, 759)
toast.add({
severity: 'success',
summary: t('workspacePanel.inviteMemberDialog.linkCopied'),

View File

@@ -66,7 +66,7 @@ interface Props {
buttonStyles?: Record<string, string>
}
defineProps<Props>()
const { buttonStyles } = defineProps<Props>()
const buttonRef = ref<ComponentPublicInstance | null>(null)
const popover = ref<InstanceType<typeof Popover>>()
const commandStore = useCommandStore()

View File

@@ -65,11 +65,12 @@ const updateWidgets = () => {
const canvasStore = useCanvasStore()
whenever(
() => canvasStore.canvas,
(canvas) =>
(canvas.onDrawForeground = useChainCallback(
(canvas) => {
canvas.onDrawForeground = useChainCallback(
canvas.onDrawForeground,
updateWidgets
)),
)
},
{ immediate: true }
)
</script>

View File

@@ -162,6 +162,8 @@ import { useColorPaletteService } from '@/services/colorPaletteService'
import { useNewUserService } from '@/services/useNewUserService'
import { storeToRefs } from 'pinia'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { isCloud } from '@/platform/distribution/types'
import { useBootstrapStore } from '@/stores/bootstrapStore'
import { useCommandStore } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
@@ -171,11 +173,9 @@ import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isNativeWindow } from '@/utils/envUtil'
import { forEachNode } from '@/utils/graphTraversalUtil'
import { useInviteUrlLoader } from '@/platform/workspace/composables/useInviteUrlLoader'
import SelectionRectangle from './SelectionRectangle.vue'
import { isCloud } from '@/platform/distribution/types'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { useInviteUrlLoader } from '@/platform/workspace/composables/useInviteUrlLoader'
const { t } = useI18n()
const emit = defineEmits<{
@@ -248,9 +248,9 @@ watch(
}
)
const allNodes = computed((): VueNodeData[] =>
Array.from(vueNodeLifecycle.nodeManager.value?.vueNodeData?.values() ?? [])
)
const allNodes = computed((): VueNodeData[] => [
...(vueNodeLifecycle.nodeManager.value?.vueNodeData?.values() ?? [])
])
function onLinkOverlayReady(el: HTMLCanvasElement) {
if (!canvasStore.canvas) return

View File

@@ -114,8 +114,8 @@ const { isModalVisible, toggleModal, hideModal, hasActivePopup } =
useZoomControls()
const stringifiedMinimapStyles = computed(() => {
const buttonGroupKeys = ['borderRadius']
const buttonKeys = ['borderRadius']
const buttonGroupKeys = new Set(['borderRadius'])
const buttonKeys = new Set(['borderRadius'])
const additionalButtonStyles = {
border: 'none'
}
@@ -124,14 +124,12 @@ const stringifiedMinimapStyles = computed(() => {
const buttonStyles = {
...Object.fromEntries(
Object.entries(containerStyles).filter(([key]) =>
buttonKeys.includes(key)
)
Object.entries(containerStyles).filter(([key]) => buttonKeys.has(key))
),
...additionalButtonStyles
}
const buttonGroupStyles = Object.entries(containerStyles)
.filter(([key]) => buttonGroupKeys.includes(key))
.filter(([key]) => buttonGroupKeys.has(key))
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
return { buttonStyles, buttonGroupStyles }

View File

@@ -248,8 +248,8 @@ defineExpose({ toggle, hide, isOpen, show })
function showColorPopover(event: MouseEvent) {
event.stopPropagation()
event.preventDefault()
const target = Array.from((event.currentTarget as HTMLElement).children).find(
(el) => el.classList.contains('icon-[lucide--chevron-right]')
const target = [...(event.currentTarget as HTMLElement).children].find((el) =>
el.classList.contains('icon-[lucide--chevron-right]')
) as HTMLElement
colorPickerMenu.value?.toggle(event, target)
}

View File

@@ -34,14 +34,14 @@ const left = ref<string>()
const top = ref<string>()
function hideTooltip() {
return (tooltipText.value = '')
tooltipText.value = ''
}
async function showTooltip(tooltip: string | null | undefined) {
if (!tooltip) return
left.value = comfyApp.canvas.mouse[0] + 'px'
top.value = comfyApp.canvas.mouse[1] + 'px'
left.value = `${comfyApp.canvas.mouse[0]}px`
top.value = `${comfyApp.canvas.mouse[1]}px`
tooltipText.value = tooltip
await nextTick()
@@ -50,11 +50,11 @@ async function showTooltip(tooltip: string | null | undefined) {
if (!rect) return
if (rect.right > window.innerWidth) {
left.value = comfyApp.canvas.mouse[0] - rect.width + 'px'
left.value = `${comfyApp.canvas.mouse[0] - rect.width}px`
}
if (rect.top < 0) {
top.value = comfyApp.canvas.mouse[1] + rect.height + 'px'
top.value = `${comfyApp.canvas.mouse[1] + rect.height}px`
}
}

View File

@@ -102,7 +102,7 @@ vi.mock('@/stores/nodeDefStore', () => ({
})
}))
describe('SelectionToolbox', () => {
describe(SelectionToolbox.__name ?? 'SelectionToolbox', () => {
let canvasStore: ReturnType<typeof useCanvasStore>
const i18n = createI18n({

View File

@@ -82,16 +82,14 @@ const { visible } = useSelectionToolboxPosition(toolboxRef)
const extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {
const commandIds = new Set<string>(
canvasStore.selectedItems
.map(
(item) =>
extensionService
.invokeExtensions('getSelectionToolboxCommands', item)
.flat() as string[]
)
.flat()
canvasStore.selectedItems.flatMap(
(item) =>
extensionService
.invokeExtensions('getSelectionToolboxCommands', item)
.flat() as string[]
)
)
return Array.from(commandIds)
return [...commandIds]
.map((commandId) => commandStore.getCommand(commandId))
.filter((command): command is ComfyCommandImpl => command !== undefined)
})

View File

@@ -68,7 +68,7 @@ const createWrapper = (props = {}) => {
})
}
describe('ZoomControlsModal', () => {
describe(ZoomControlsModal.__name ?? 'ZoomControlsModal', () => {
beforeEach(() => {
vi.resetAllMocks()
})

View File

@@ -84,7 +84,7 @@ interface Props {
visible: boolean
}
const props = defineProps<Props>()
const { visible } = defineProps<Props>()
const interval = ref<number | null>(null)
@@ -132,7 +132,7 @@ const zoomToFitCommandText = computed(() =>
const zoomInputContainer = ref<HTMLDivElement | null>(null)
watch(
() => props.visible,
() => visible,
async (newVal) => {
if (newVal) {
await nextTick()

View File

@@ -20,7 +20,7 @@ vi.mock('@/utils/litegraphUtil', () => ({
isLGraphNode: vi.fn(() => true)
}))
describe('BypassButton', () => {
describe(BypassButton.__name ?? 'BypassButton', () => {
let canvasStore: ReturnType<typeof useCanvasStore>
let commandStore: ReturnType<typeof useCommandStore>

View File

@@ -45,7 +45,7 @@ vi.mock('@/lib/litegraph/src/litegraph', async () => {
// Mock the colorUtil module
vi.mock('@/utils/colorUtil', () => ({
adjustColor: vi.fn((color: string) => color + '_light')
adjustColor: vi.fn((color: string) => `${color}_light`)
}))
// Mock the litegraphUtil module
@@ -56,7 +56,7 @@ vi.mock('@/utils/litegraphUtil', () => ({
isReroute: vi.fn(() => false)
}))
describe('ColorPickerButton', () => {
describe(ColorPickerButton.__name ?? 'ColorPickerButton', () => {
let canvasStore: ReturnType<typeof useCanvasStore>
let workflowStore: ReturnType<typeof useWorkflowStore>

View File

@@ -80,7 +80,7 @@ interface Emits {
(e: 'submenu-click', subOption: SubMenuOption): void
}
const props = defineProps<Props>()
const { option } = defineProps<Props>()
const emit = defineEmits<Emits>()
const { getCurrentShape } = useNodeCustomization()
@@ -113,9 +113,9 @@ const isShapeSelected = (subOption: SubMenuOption): boolean => {
const isColorSubmenu = computed(() => {
return (
props.option.submenu &&
props.option.submenu.length > 0 &&
props.option.submenu.every((item) => item.color && !item.icon)
option.submenu &&
option.submenu.length > 0 &&
option.submenu.every((item) => item.color && !item.icon)
)
})
</script>

View File

@@ -30,7 +30,7 @@ vi.mock('@/composables/graph/useSelectionState', () => ({
}))
}))
describe('ExecuteButton', () => {
describe(ExecuteButton.__name ?? 'ExecuteButton', () => {
let mockCanvas: LGraphCanvas
let mockSelectedNodes: LGraphNode[]

View File

@@ -24,7 +24,7 @@ import type { ComfyCommand } from '@/stores/commandStore'
import { useCommandStore } from '@/stores/commandStore'
import { normalizeI18nKey } from '@/utils/formatUtil'
defineProps<{
const { command } = defineProps<{
command: ComfyCommand
}>()

View File

@@ -18,7 +18,7 @@ vi.mock('@/stores/workspace/rightSidePanelStore', () => ({
})
}))
describe('InfoButton', () => {
describe(InfoButton.__name ?? 'InfoButton', () => {
const i18n = createI18n({
legacy: false,
locale: 'en',

View File

@@ -20,7 +20,7 @@ import { useExecutionStore } from '@/stores/executionStore'
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
const modelValue = defineModel<string>({ required: true })
const props = defineProps<{
const { nodeId } = defineProps<{
nodeId: NodeId
}>()
@@ -30,7 +30,7 @@ const formattedText = computed(() => {
const src = modelValue.value
// Turn [[label|url]] into placeholders to avoid interfering with linkifyHtml
const tokens: { label: string; url: string }[] = []
const holed = src.replace(
const holed = src.replaceAll(
/\[\[([^|\]]+)\|([^\]]+)\]\]/g,
(_m, label, url) => {
tokens.push({ label: String(label), url: String(url) })
@@ -42,10 +42,10 @@ const formattedText = computed(() => {
let html = nl2br(linkifyHtml(holed))
// Restore placeholders as <a>...</a> (minimal escaping + http default)
html = html.replace(/__LNK(\d+)__/g, (_m, i) => {
html = html.replaceAll(/__LNK(\d+)__/g, (_m, i) => {
const { label, url } = tokens[+i]
const safeHref = url.replace(/"/g, '&quot;')
const safeLabel = label.replace(/</g, '&lt;').replace(/>/g, '&gt;')
const safeHref = url.replaceAll('"', '&quot;')
const safeLabel = label.replaceAll('<', '&lt;').replaceAll('>', '&gt;')
return /^https?:\/\//i.test(url)
? `<a href="${safeHref}" target="_blank" rel="noopener noreferrer">${safeLabel}</a>`
: safeLabel
@@ -58,7 +58,7 @@ let parentNodeId: NodeId | null = null
onMounted(() => {
// Get the parent node ID from props if provided
// For backward compatibility, fall back to the first executing node
parentNodeId = props.nodeId
parentNodeId = nodeId
})
// Watch for either a new node has starting execution or overall execution ending

View File

@@ -593,11 +593,11 @@ const onUpdateComfyUI = async (): Promise<void> => {
})
await rebootComfyUI()
} catch (err) {
} catch (error) {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: err instanceof Error ? err.message : t('g.unknownError'),
detail: error instanceof Error ? error.message : t('g.unknownError'),
life: 5000
})
}

View File

@@ -5,7 +5,7 @@ import { defineComponent, h, nextTick, ref } from 'vue'
import HoneyToast from './HoneyToast.vue'
describe('HoneyToast', () => {
describe(HoneyToast.__name ?? 'HoneyToast', () => {
beforeEach(() => {
vi.clearAllMocks()
document.body.innerHTML = ''

View File

@@ -110,7 +110,7 @@ import { ASPECT_RATIOS, useImageCrop } from '@/composables/useImageCrop'
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
import type { Bounds } from '@/renderer/core/layout/types'
const props = defineProps<{
const { nodeId } = defineProps<{
nodeId: NodeId
}>()
@@ -142,5 +142,5 @@ const {
handleResizeStart,
handleResizeMove,
handleResizeEnd
} = useImageCrop(props.nodeId, { imageEl, containerEl, modelValue })
} = useImageCrop(nodeId, { imageEl, containerEl, modelValue })
</script>

View File

@@ -170,7 +170,7 @@ const getLabel = (val: string | null | undefined) => {
// Extract complex style logic from template
const optionStyle = computed(() => {
if (!popoverMinWidth && !popoverMaxWidth) return undefined
if (!popoverMinWidth && !popoverMaxWidth) return
const styles: string[] = []
if (popoverMinWidth) styles.push(`min-width: ${popoverMinWidth}`)

View File

@@ -84,24 +84,24 @@ import { app } from '@/scripts/app'
import type { ComponentWidget } from '@/scripts/domWidget'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
const props = defineProps<{
const { widget, nodeId } = defineProps<{
widget: ComponentWidget<string[]> | SimplifiedWidget
nodeId?: NodeId
}>()
function isComponentWidget(
widget: ComponentWidget<string[]> | SimplifiedWidget
): widget is ComponentWidget<string[]> {
return 'node' in widget && widget.node !== undefined
w: ComponentWidget<string[]> | SimplifiedWidget
): w is ComponentWidget<string[]> {
return 'node' in w && w.node !== undefined
}
const node = ref<LGraphNode | null>(null)
if (isComponentWidget(props.widget)) {
node.value = props.widget.node
} else if (props.nodeId) {
if (isComponentWidget(widget)) {
node.value = widget.node
} else if (nodeId) {
onMounted(() => {
node.value = app.rootGraph?.getNodeById(props.nodeId!) || null
node.value = app.rootGraph?.getNodeById(nodeId!) || null
})
}

View File

@@ -35,7 +35,14 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
import LoadingOverlay from '@/components/common/LoadingOverlay.vue'
import { useLoad3dDrag } from '@/composables/useLoad3dDrag'
const props = defineProps<{
const {
initializeLoad3d,
cleanup,
loading,
loadingMessage,
onModelDrop,
isPreview
} = defineProps<{
initializeLoad3d: (containerRef: HTMLElement) => Promise<void>
cleanup: () => void
loading: boolean
@@ -53,20 +60,20 @@ function focusContainer() {
const { isDragging, dragMessage, handleDragOver, handleDragLeave, handleDrop } =
useLoad3dDrag({
onModelDrop: async (file) => {
if (props.onModelDrop) {
await props.onModelDrop(file)
if (onModelDrop) {
await onModelDrop(file)
}
},
disabled: computed(() => props.isPreview)
disabled: computed(() => isPreview)
})
onMounted(() => {
if (container.value) {
void props.initializeLoad3d(container.value)
void initializeLoad3d(container.value)
}
})
onUnmounted(() => {
props.cleanup()
cleanup()
})
</script>

Some files were not shown because too many files have changed in this diff Show More