mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-11 09:42:22 +00:00
97c59f3adbbabe7678df9ccebcbcaf92ed708564
1208 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9d20bfd410 |
Add center dividing line to image compare node (#13132)
| Before | After | | ------ | ----- | | <img width="360" alt="before" src="https://github.com/user-attachments/assets/37afb473-161c-4350-881e-0ea908e28777"/> | <img width="360" alt="after" src="https://github.com/user-attachments/assets/d5acca61-3687-4c15-8029-ef2c88a06944" />| |
||
|
|
33f0993657 |
Add long widget values to tooltips (#12864)
If a widget value is long (> 10 characters) and on a known single-line widget (`number`, `combo`, or `string), then the widget's full value will be added to the tooltip. Additionally, margins on combo widgets are slightly tweaked so more of the text displays before truncation occurs. | Before | After | | ------ | ----- | | <img width="360" alt="before" src="https://github.com/user-attachments/assets/fefd76e9-6511-4e98-80f6-030a6dc34fb8" /> | <img width="360" alt="after" src="https://github.com/user-attachments/assets/0cbc100d-066e-4272-afe9-795e56c12353" />| |
||
|
|
312a25b63f |
feat(billing): single billing path — collapse personal/team dispatch to flag-only (B1 / FE-966) (#12953)
## What this does
Collapses the personal-vs-team billing dispatch so it keys ONLY on the
build/flag (`teamWorkspacesEnabled ? 'workspace' : 'legacy'`). Personal
now flows through `useWorkspaceBilling` (`/api/billing/*`), same as team
("personal plan = single-seat workspace"). This converges status /
balance / subscribe / preview / cancel / portal in one change.
Dispatch sites collapsed:
- `useBillingContext.ts` — `type` computed: dropped the
`store.isInPersonalWorkspace` branch → flag-only.
- `useBillingContext.ts` — D3 subscription→store mirror watch: dropped
the `isInPersonalWorkspace` early-return guard so personal also mirrors
into the workspace store.
- `useSubscriptionDialog.ts` — `useWorkspaceVariant` compound predicate
→ flag-only (personal + flag-on now uses the workspace required-dialog
variant).
- `SubscriptionPanel.vue` — already flag-only
(`v-if="teamWorkspacesEnabled"`); no change needed.
## Kept (Risk #6)
- The ~11 raw `workspace.type === 'personal'` checks in
`teamWorkspaceStore.ts` — workspace-TYPE membership logic
(can-delete/leave, fetch-members, switcher), NOT billing dispatch.
Untouched.
- `useLegacyBilling` / `useSubscription` / authStore billing methods
kept intact for the flag-OFF (OSS/Desktop) path.
## Flag-off unchanged
Flag-OFF (OSS/Desktop) still selects `legacy` (`/customers/*`). Verified
by unit test.
## Tests
- `useBillingContext`: flag-ON → personal selects `workspace`; flag-OFF
→ `legacy`; D3 mirror now fires for personal under flag-on.
- `useSubscriptionDialog`: flag-ON → workspace required-dialog variant
for personal; flag-OFF → legacy personal variant.
## Follow-up (deferred, not in this PR)
Post-flip cutover deletion of `useLegacyBilling`-only components:
`PricingTable.vue`, `SubscriptionPanelContentLegacy.vue`,
`TopUpCreditsDialogContentLegacy.vue`, `CurrentUserPopoverLegacy.vue`,
`subscriptionCheckoutUtil.ts`, `useSubscriptionCancellationWatcher.ts`.
- Fixes part of FE-903 (B1)
|
||
|
|
f346dd93d2 |
feat(workspace): promote/demote members via Change role menu (FE-770) (#12782)
Promote / demote workspace members ↔ owners in Settings ▸ Members, per [DES-222 / Figma 2993-15512](https://www.figma.com/design/CkFTD4c20PyRGpNVAJgpfV/Team-Plan---Workspaces?node-id=2993-15512) and the [permissions section 3343-22966](https://www.figma.com/design/CkFTD4c20PyRGpNVAJgpfV/Team-Plan---Workspaces?node-id=3343-22966). - Fixes [FE-770](https://linear.app/comfyorg/issue/FE-770/promote-demote-workspace-members-owners-settings-members) - Stacked on #12759 (`jaewon/fe-768-members-invite-ui`) ## Changes - Per-member row (…) menu → **Change role** submenu (Owner / Member, current role check-marked) + existing **Remove member**, replacing the shared PrimeVue `Menu` with the Reka `DropdownMenu`/`DropdownItem` (submenu opens right of parent, flips on collision; scalable for future roles). - **Make [name] an owner?** / **Demote [name] to member?** confirm dialogs (single `ChangeMemberRoleDialogContent`, copy 1:1 from Figma). - `workspaceApi.updateMemberRole` → `PATCH /api/workspace/members/:userId {role}` + `teamWorkspaceStore.changeMemberRole` (local role map update; Role column re-sorts). - **Original-owner guards** (Figma annotations): creator pinned to the top of the list, no row actions for anyone on that row; own row also has no actions. Creator inferred as earliest `joined_at` until BE exposes an explicit flag (tracked as the FE-770 BE blocker — same applies to the endpoint itself, which does not exist yet; UI is wired to the proposed contract). - `DropdownMenu` raised to `z-3000` so the row menu sits above the Settings modal (the Reka popper wrapper copies the content's computed z-index; static `z-1700` lost to dialogs in the `@primeuix` modal sequence). Also drops the always-rendered icon slot in `DropdownItem` so icon-less items (Change role / Remove member) align flush-left. ## User stories verified Viewer = an **owner** (promoted, not the workspace creator), so the creator guard and the self guard are exercised separately. | # | Click → action → expected | | --- | --- | | US1 | Member row (…) → menu shows **Change role ›** + **Remove member** | | US2 | Hover **Change role** → Owner / Member submenu, **current role check-marked** | | US3 | Click the current role (✓) → no dialog, no PATCH (no-op) | | US4 | Member row → **Owner** → "Make {name} an owner?" + "They'll have the same access as you — managing members, billing, and workspace settings." + Cancel / **Make owner** | | US5 | **Cancel** (or ✕) → dialog closes, role unchanged, no PATCH | | US6 | **Make owner** → `PATCH /api/workspace/members/:id {role:'owner'}` → Role column → Owner, row **re-sorts under the creator**, "Role updated" toast, the promoted row keeps its (…) menu | | US7 | Promoted owner row → **Member** → "Demote {name} to member?" + "They'll lose admin access." → **Demote to member** → Role column back to Member | | US8 | **Creator row (earliest joined) has no (…) button** — even for another owner | | US9 | **Own (You) row has no (…) button** — even when not the creator | | US10 | PATCH 500 → "Failed to update role" toast, **dialog stays open**, role unchanged | | US11 | Viewer with `member` role → no row actions anywhere | | US12 | **Remove member** → existing FE-768 "Remove this member?" dialog | ## Tests Each user story is covered by automated tests and confirmed by a manual CDP pass driving the real cloud app (mocked auth + boot + workspace/billing API). | Story | Unit / Component | E2E (Playwright) | CDP (live app) | | --- | :---: | :---: | :---: | | US1 row menu shows Change role + Remove member | ✅ | ✅ | ✅ | | US2 submenu checkmark follows current role | ✅ | ✅ | ✅ | | US3 picking the current role is a no-op | ✅ | ✅ | ✅ | | US4 promote dialog copy (Make owner) | ✅ | ✅ | ✅ | | US5 Cancel leaves role unchanged, no PATCH | ✅ | ✅ | ✅ | | US6 Make owner → PATCH, re-sort under creator, toast, stays demotable | ✅ | ✅ | ✅ | | US7 demote dialog (Demote to member) → role reverts | ✅ | ✅ | ✅ | | US8 creator row has no (…) menu | ✅ | ✅ | ✅ | | US9 own (You) row has no (…) menu | ✅ | ✅ | ✅ | | US10 PATCH 500 → error toast, dialog stays open | ✅ | ✅ | ✅ | | US11 member-role viewer sees no row actions | ✅ | — | — | | US12 Remove member → FE-768 remove dialog | ✅ | ✅ | ✅ | | Layer | File | What it covers | Result | | --- | --- | --- | --- | | E2E (`@cloud`) | `browser_tests/tests/dialogs/memberRoleChange.spec.ts` | 3 tests — guard rows (US1/US8/US9/US12), promote→re-sort→demote round trip (US3–US7), failed PATCH (US10). FE-964 boot pattern: `CloudAuthHelper` + remote-config flag mock + stateful route mocks capturing PATCH args. Reka submenu driven via `ArrowRight` (synthetic hover doesn't open it). | 3 / 3 green | | Component | `ChangeMemberRoleDialogContent.test.ts` | promote/demote copy, confirm → store + success toast + close, error keeps dialog open, cancel | green | | Component | `MembersPanelContent.test.ts` | creator/self rows hide the menu (US8/US9), member-viewer gating (US11) | green | | Composable | `useMembersPanel.test.ts` | menu factory labels/checkmarks/commands, same-role no-op, creator pin in `sortMembers`, `isOriginalOwner` | green | | Store | `teamWorkspaceStore.test.ts` | `changeMemberRole` success/failure, `originalOwnerId` inference | green | | CDP live | full cloud app on `local.comfy.org` (mocked auth + boot) | promote→re-sort→demote round trip with PATCH applied to mock state, guard rows, submenu checkmark, dialog copy, menu/dialog z-index above Settings, forced PATCH 500 → error toast | verified | ⚠️ Merge-gated on the BE role-change endpoint (no `PATCH /workspace/members/:userId` in cloud OpenAPI as of 2026-06-10; see FE-770 BE-blocker comment). ## Screenshots (local dev, workspace/billing API stubbed; vs Figma 2993-15512) | Members (before) | Change role submenu | | --- | --- | | <img alt="members" src="https://github.com/user-attachments/assets/686fec86-fcb5-4942-a745-50f367022ab0" /> | <img alt="submenu" src="https://github.com/user-attachments/assets/d6adeea8-7001-4c8d-91b7-f5bfc47a50d6" /> | | Promote dialog | After promote (Jane → Owner, still demotable) | Demote dialog | | --- | --- | --- | | <img alt="promote" src="https://github.com/user-attachments/assets/af638cde-2fd6-4c37-b203-78801eeb2785" /> | <img alt="after" src="https://github.com/user-attachments/assets/f47dc7af-6b1b-422c-8a9a-5ec889b9af11" /> | <img alt="demote" src="https://github.com/user-attachments/assets/9a861d04-a23b-4cd4-bc54-1ed3a66c6429" /> | |
||
|
|
07205c6e43 |
feat(billing): unify credits into a facade-driven CreditsTile (FE-964) (#12734)
## Summary ### AS IS <img width="1340" height="798" alt="Screenshot 2026-06-10 at 12 22 36 AM" src="https://github.com/user-attachments/assets/61636fa3-e80c-427b-855b-499e1eca67da" /> ### TO BE <img width="1301" height="793" alt="Screenshot 2026-06-10 at 12 22 39 AM" src="https://github.com/user-attachments/assets/62d9f5a6-da92-45df-94e7-cd3c244249f9" /> ### Empty states ([added to DES-247 on 2026-06-11](https://www.figma.com/design/CkFTD4c20PyRGpNVAJgpfV/Team-Plan---Workspaces?node-id=3349-29750)) | 0 monthly credits | 0 credits | | --- | --- | | <img alt="credits-empty-monthly" src="https://github.com/user-attachments/assets/b3c55d3b-79b0-47b1-9795-c8bf69d5efe2" /> | <img alt="credits-empty-all" src="https://github.com/user-attachments/assets/919081d6-64e1-483b-9c04-6b085243ebc1" /> | Consolidate the divergent Settings credits surfaces into one facade-driven **CreditsTile**, implementing the DES-247 redesign so personal and team modes always render the same balance from `useBillingContext`. ## Changes - **What**: - New `CreditsTile.vue` — total + `remaining`, a stacked monthly/additional progress bar, colored breakdown rows (`Monthly (refills …)` / `Additional`), refresh, and a permission-gated *Add credits* / *Upgrade to add credits* action. Owns the post-checkout (`focus` / `pending_topup`) balance refresh. - Extracted the duplicated inline credits card out of `SubscriptionPanelContentWorkspace.vue` **and** `SubscriptionPanelContentLegacy.vue` onto the shared tile. - Replaced `LegacyCreditsPanel.vue` (read `authStore.balance` directly) with `CreditsPanel.vue` routed through the tile; repointed `useSettingUI` and deleted the legacy panel. - `creditsProgress.ts` pure helper for the bar math + numeric credit getters on `useSubscriptionCredits`. - i18n keys for the unified tile labels. - DES-247 responsive variants via CSS container queries: below ~350px tile width the `{used} used` label, `remaining` suffix, and breakdown subtitle drop and the additional-credits value stacks under its label; below ~230px the monthly summary compacts (`105K left of 200K`). Additional-credits tooltip copy aligned with the updated design (per design feedback). - Empty states (added to DES-247 via Slack on 2026-06-11, low priority): once the monthly allowance is depleted, an info notice renders under the total (`Monthly credits are used up. Refills {date}` / `You're now spending additional credits.`), the monthly bar section dims to 30% opacity, and an `IN USE` pill marks *Additional credits*; once everything is depleted the notice switches to `You're out of credits. Credits refill {date}` and *Add credits* swaps to the `inverted` (filled-white) Button variant. Gated on a loaded balance so the notice never flashes while fetching. - **Dependencies**: Stacked on **#12622 (FE-904 / B2)** for the facade `tier` / `renewalDate` fields — base this PR against that branch; retarget to `main` once FE-904 merges. ## Review Focus - The tile reads everything from the facade (`balance.*Micros` as cents → credits, `subscription.tier`/`renewalDate`), so legacy and workspace modes share one source. - Monthly allowance still comes from `getTierCredits` (hardcoded tier nominal). With real data the monthly *remaining* can exceed the nominal (rolled-over credits), so the bar clamps to a full segment — same semantics as the prior `{monthly} / {planTotal}` display; the canonical allowance is a BE-1047 follow-up. - `LegacyCreditsPanel` deletion: `CreditsPanel.vue` retains the usage-history table + help links and reads the facade. ## Testing - Unit/component (36 green): `CreditsTile.test.ts` (render, zero-state, free-tier, permission gating, add-credits, mount+manual refresh, plus the empty states: depletion notice copy, `IN USE` badge, `inverted` button when fully out, no-flash-while-loading guard), `creditsProgress.test.ts` (clamping/stacking math), `useSubscriptionCredits.test.ts` (`*_micros`-as-cents), and `SubscriptionPanel.test.ts` updated for the extracted tile. - E2E (`@cloud`): `browser_tests/tests/dialogs/creditsTile.spec.ts` boots the cloud app against mocked Firebase auth + stubbed boot endpoints (no backend) and asserts the tile's total / progress bar / monthly+additional breakdown / add-credits in Settings ▸ Workspace ▸ Plan & Credits, then resizes to a narrow viewport and asserts the responsive variant (labels hidden, compacted `11K left of 21K`). A second test boots with a drained monthly balance (0-monthly notice + `IN USE` badge), then re-mocks a fully drained balance and refreshes the tile in place to assert the out-of-credits state. Both pass locally against a cloud dev server; runs in the `cloud` CI project. Drives a raw page because the shared `comfyPage` fixture expects the OSS devtools backend. - Screenshot-verified the tile at the three DES-247 reference widths (448 / 235 / 204px) against the Figma Responsiveness section — 1:1. - Verified live in the running app (Settings ▸ Workspace ▸ Plan & Credits) against the authenticated backend — renders 1:1 with DES-247. The empty-state screenshots above were captured the same way (authenticated app, real Pro subscription, balance endpoint stubbed to the depleted values via CDP). - `pnpm typecheck` / `typecheck:browser` / `lint` / `knip` green. Implements FE-964 (DES-247). --------- Co-authored-by: GitHub Action <action@github.com> |
||
|
|
09d59616f5 |
feat(billing): deep link to open the pricing table (FE-1104) (#13001)
## Summary Adds an in-app deep link that opens the pricing table directly, for driving pilot users straight to subscribe (request from nav/Alex). Resolves [FE-1104](https://linear.app/comfyorg/issue/FE-1104). - `/?pricing=1` — on app load, open the pricing table. - `/?pricing=team` / `/?pricing=personal` — open it on the Team / Personal plan tab (via the existing `UnifiedPricingTable` `initialPlanMode`). - Gated to the **original owner** via `useWorkspaceUI().permissions.canManageSubscriptionLifecycle` (personal user, or a team workspace's original owner). A member or a promoted owner is a **silent no-op**: the app loads normally, the param is stripped, no 404 / error / toast. - Off-cloud (OSS): the loader isn't instantiated, so the param is ignored. - Survives the login redirect via the preserved-query system, same as `?invite` / `?create_workspace`. ## How Mirrors the established URL-loader pattern (`useInviteUrlLoader` / `useCreateWorkspaceUrlLoader`): - `preservedQueryNamespaces.ts` / `router.ts` — register the `pricing` namespace + tracker key. - New `usePricingTableUrlLoader.ts` — hydrate preserved query, read `pricing`, strip the param + `clearPreservedQuery` in a single replace before any await, then `await fetchMembers()` (resolves the original-owner gate; no-ops for personal) and open the table only when the gate allows. - `GraphCanvas.vue` — call the loader in `onMounted` after the create-workspace loader (cloud only; not gated on the team-workspaces flag so it also drives personal/legacy users). - `useSubscriptionDialog.ts` — new `'deep_link'` value on `SubscriptionDialogReason`. ## Telemetry Eligible opens emit the existing `subscription_required_modal_opened` PostHog event with the new `reason: 'deep_link'`. Ineligible-click bounce rate is derivable from the autocaptured pageview URL (`?pricing=…`), so no new event plumbing. ## Stacking / dependencies This feature needs two sibling stacks off `main`: - **FE-934** (`#12666`, base of this PR) — the `UnifiedPricingTable` + `showPricingTable({ planMode })`. - **FE-770** (`#12829`) — the `canManageSubscriptionLifecycle` gate. **Merged into this branch**, so the diff against the FE-934 base includes FE-770's changes until it lands. Review the single `feat(billing): deep link…` commit. Once both land on `main`, rebase onto `main` and the diff collapses to just this feature. Do not merge before FE-770 and FE-934. Post-Billing-V1 follow-up. End-state: swap the FE original-owner heuristic for the BE workspace-level `is_original_owner` flag when it lands (removes the members-fetch). ## Tests - Unit (`usePricingTableUrlLoader.test.ts`, 12 cases): opens for an original owner; `team`/`personal` tab preselect; silent no-op + param-strip for a member/promoted owner; proves the gate is read only after `fetchMembers` resolves; preserved-query restore; empty/non-string/absent/unrecognized param; members-fetch failure strips+clears without opening. - E2E (`browser_tests/tests/dialogs/pricingTableDeepLink.spec.ts`, `@cloud`, 4 cases, verified locally): personal owner opens + URL stripped; `?pricing=team` lands on the active Team tab; team original owner opens (real `is_original_owner` + email gate); team member is a silent no-op + URL stripped. - Typecheck + related unit suites (`useSubscriptionDialog`, `useWorkspaceUI`, `teamWorkspaceStore`) green. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com> Co-authored-by: jaeone94 <89377375+jaeone94@users.noreply.github.com> Co-authored-by: github-actions <github-actions@github.com> |
||
|
|
52d430d1b6 |
fix(billing): repoint direct-bypass billing consumers to the facade (B3) (FE-933) (#12643)
## What **B3 — Repoint direct-bypass billing consumers to the facade.** Billing data was read from the legacy `useSubscription` store / `authStore` directly (empty or personal-only for team workspaces) instead of the workspace-aware `useBillingContext` facade. FE-933 (parent FE-903). > **Stacked on #12622 (B2 / FE-904)** — depends on the facade `tier` / `renewalDate` fields added there. Base is the B2 branch; retarget to `main` once B2 merges. ## Repointed consumers - **T3 — `SubscribeButton.vue`**: `subscribe_clicked` telemetry `current_tier` ← facade `tier` (was wrong/empty for team users) - **T4 — `PostHogTelemetryProvider.ts`**: PostHog `subscription_tier` person property ← facade `tier` watch (tier-segmented analytics was polluted for team users) - **T5 — `FreeTierDialogContent.vue`**: next-refresh date ← facade raw ISO `renewalDate`, formatted at the display site (the line silently disappeared for team users) - **`useSubscriptionActions.handleRefresh` + `SettingDialog` credits-nav**: balance refresh ← facade `fetchBalance()` (was legacy `/customers`-only `authActions.fetchBalance`) - **`CurrentUserPopoverLegacy.vue`**: tier badge / balance / skeleton / refreshes ← facade (`tier`, `balance`, `isLoading`, `fetchStatus`, `fetchBalance`); tier name via shared `useWorkspaceTierLabel` instead of a duplicated mapping - **`PricingTable.vue`**: `isActiveSubscription` / `isFreeTier` / `tier` / yearly-vs-monthly ← facade; the billing-portal flow (`accessBillingPortal` deep-links + proration) is intentionally unchanged — facade `manageSubscription` is not behavior-identical ## Out of scope (triaged) - `TopUpCreditsDialogContentLegacy` / `SubscriptionPanelContentLegacy` / `useSubscriptionDialog` / cancellation watcher — legacy-mode-only surfaces decommissioned by B1 (FE-966); repointing is churn, and `useSubscriptionDialog` would create a legacy↔facade cycle - `LegacyCreditsPanel` / `UserCredit` — deleted/orphaned by FE-964 (#12734); its successor `CreditsPanel.vue` keeps an `authStore.lastBalanceUpdateTime` watch (no facade equivalent yet) — follow-up after FE-964 lands ## Known semantic deltas (intentional, match shipped facade consumers) - Balance-refresh failures no longer toast: legacy `authActions.fetchBalance` wrapped errors with a toast; facade `fetchBalance` rejections are void-ed, same as `CurrentUserPopoverWorkspace` / `SubscriptionPanelContentWorkspace`. Facade-level error surfacing is a follow-up. - Popover skeleton keys on facade `isLoading` (init-time) rather than per-fetch `isFetchingBalance`, matching the workspace popover. ## Tests - New behavioral coverage: FreeTier renewal-date render/disappear, popover tier badge + balance from facade, current-plan highlight from facade tier+duration, facade-vs-legacy fetchBalance tripwire, PostHog `subscription_tier` from facade tier. - Local gates clean (typecheck / lint / format / dead-code); touched unit files 71/71 pass. ## E2E coverage Browser regression tests live in the stacked #12760 (`billingFacadeConsumers.spec.ts`, `@cloud`): avatar popover tier badge + balance, and the free-tier dialog renewal-date line (T5) rendered from the facade. The team-user telemetry fixes (PostHog person property, telemetry payload) are non-UI observables covered by unit tests that mock only the facade and fail on revert. --------- Co-authored-by: Alexander Brown <drjkl@comfy.org> |
||
|
|
c406042215 |
More robust drag cleanup (#13084)
Under some circumstances, (particularly with pointerCancel events) a drag operation could end without properly being cleaned up. When this occurs, the bugged state would manifest in comical ways - Nodes would 'run away' from the cursor <img width="1024" height="1024" alt="AnimateDiff_00001" src="https://github.com/user-attachments/assets/accfeac0-ce4c-4d8a-b3b8-6b243e8d5f8d" /> - Resizing the window could cause the zombie drag to move into the autopan region which would result in nodes rapidly scrolling away. <img width="1024" height="1024" alt="AnimateDiff_00002" src="https://github.com/user-attachments/assets/e30629f4-ddea-4981-83d8-0037b3010ad5" /> This is resolved by adding more robust cleanup for canceled drag events. This PR also cleanups a sizeable chunk of dead TransformPane code which was unused. |
||
|
|
395b0a1c89 |
fix: prevent NullGraphError on subgraph node removal (#11804)
## Summary Various race conditions can cause `NullGraphError` to be thrown after removing/converting a subgraph. This fix guards at call sites and refactors to add a pre-removal phase before the graph is nulled. ## Changes - **What**: - add pre-detach event (node:before-removed) so reactive consumers can drop references before node.graph is nulled - move selection and Vue node-manager teardown to this event to eliminate stale panel/render evaluations against detached nodes - guard SubgraphNode promoted-widget paths resilient on detached access and add regression coverage - **Breaking**: <!-- Any breaking changes (if none, remove this line) --> - **Dependencies**: <!-- New dependencies (if none, remove this line) --> ## Review Focus Alternative considered approach: - Guards: Guards were treating the symptom at every caller, and new callers may appear that won't know about this edge case. Adding a new hook for consumers to drop refs is safer than trying to guard every call site - the ones that are left in are safetynets and not the primary fix. - Large scale refactor (towards ADR0008) - requires additional scaffolding to already be in place to implement effectively, this fix simply adds a new hook and isnt incompatible with the projects future goals - Defer/remove/reorder graph null - The detach was explicitly added in #8180 to ensure GC - delaying is fragile and may not resolve the issue, difficult to prove and may surface a new race condition - Make rootGraph nullable - would require 100s of references to be updated, when `NullGraphError` was added in #8180 to throw a clear message when the graph for a removed subgraph node was referenced, potentially leading to other harder to track bugs without the exception Tests: - e2e test complexity is required to prove the issue happens, patching calls to add artificial delays. This isn't great, but I could not find a reliable way to recreate otherwise, unless we are happy to drop e2e and keep only unit tests. ┆Issue is synchronized with this [Notion page](https://app.notion.com/p/PR-11804-fix-prevent-NullGraphError-on-subgraph-node-removal-3536d73d3650814e9183e17067cc0992) by [Unito](https://www.unito.io) --------- Co-authored-by: Alexander Brown <drjkl@comfy.org> Co-authored-by: DrJKL <DrJKL0424@gmail.com> |
||
|
|
b165b3f999 |
fix: focus keybindings search when opening Manage Shortcuts (FE-845) (#12709)
## Summary Opening the Keybinding panel from the **Manage Shortcuts** button now focuses the **Search Keybindings** field instead of the **Search Settings** field. ## Changes - **What**: The Settings dialog's "Search Settings" input had an unconditional `autofocus`, so opening directly to the keybinding panel always stole focus to the wrong field. Made it conditional (`:autofocus="activeCategoryKey !== 'keybinding'"`) and added `autofocus` to the keybinding panel's own search input. ## Review Focus - `autofocus` maps to the native attribute, which only fires on DOM insertion — flipping the reactive `:autofocus` while navigating between categories inside the dialog will not re-steal focus, so there is no regression for in-dialog navigation. - Added an E2E test verified in both directions: it fails on the original code (Search Settings focused) and passes with the fix (Search Keybindings focused). Fixes FE-845 Co-authored-by: Dante <bunggl@naver.com> |
||
|
|
966659b303 |
fix: bind promoted asset modals (Legacy) to host widgets (#13075)
## Summary Bind asset-browser modal selections to the widget that actually opened the modal, so promoted subgraph asset widgets commit through the host promoted widget instead of the internal source widget closure. ## Changes - **What**: Makes the asset-browser modal commit path widget-owned: after a valid selection, `openModal` writes to the widget passed into the modal and notifies that widget's callback. - **What**: Captures workflow state after a successful value-changing asset selection, because the async modal `Use` action can run after the global mouseup-based change capture has already fired. - **What**: Preserves existing asset-browser filtering by keeping `nodeTypeForBrowser` and `inputNameForBrowser` captured in the asset widget's existing modal options closure. - **What**: Avoids adding promoted-widget-specific rebinding code to `litegraphService` and avoids changing LiteGraph core widget option types. - **What**: Only runs the source widget's `onValueChange` callback when the selected widget is the original owner widget created by `createAssetWidget`. - **What**: For cloned/transient host widgets, such as promoted subgraph asset widgets, dispatches `onWidgetChanged` through the widget's owning node instead of the internal source node. - **What**: Removes the duplicate PrimitiveNode callback dispatch because the asset modal commit path now centrally notifies the selected widget callback. - **What**: Adds stable asset-browser `data-testid`s and a cloud E2E regression for legacy promoted subgraph asset selection. - **What**: Adds unit coverage for both regular asset widget commits and cloned promoted-host asset modal commits, including workflow change capture. - **Breaking**: None. - **Dependencies**: None. ## Review Focus This PR supersedes #13074. The earlier direction treated the bug as a missing callback bridge in the async asset-browser commit path, but the ownership issue is more specific: promoted subgraph asset widgets reuse modal options that were created from the deepest concrete source widget. Those options still need to carry source metadata for filtering the asset browser, but the modal's `Use` action must commit to the widget that actually opened the modal. This matters after the History ADR 0009 subgraph widget changes shipped through #12197. In the 1.46 subgraph model, promoted widget values live on the subgraph host node and are not synchronized back into the internal widget. The internal source widget remains useful as the provider of asset-browser metadata, because `SubgraphNode` already resolves nested promotions down to the final concrete widget, but it should not own the edit commit. The final patch keeps that boundary narrow: - no `IWidgetOptions` or LiteGraph core type changes; - no asset-specific promoted-widget rebinding in `litegraphService`; - no new promoted-widget traversal logic, because the existing subgraph promotion path already resolves the final concrete source widget; - the modal commit path uses the widget passed to `openModal` as the value owner; - successful async modal commits explicitly capture workflow state when the selected value changes. Please focus review on whether `createAssetWidget` now preserves regular asset widget behavior while correctly handling cloned/transient host widgets. The key distinction is that the source `onValueChange` path only runs for the original owner widget; promoted host wrappers instead rely on their callback bridge and owning node's `onWidgetChanged` hook. A review pass also found that this PR makes an existing async modal weakness more visible: asset-browser selection happens from the modal button's `click` handler, while the global change tracker also captures on `mouseup`. Depending on event ordering, the automatic capture can occur before the selection mutates the widget. This PR now captures workflow state immediately after a successful value-changing asset selection so undo/modified tracking follows the same user-visible edit. Local verification: - `pnpm exec vitest run src/platform/assets/utils/createAssetWidget.test.ts --reporter=dot` - `pnpm exec vitest run src/platform/assets/utils/createAssetWidget.test.ts --coverage --reporter=dot --coverage.reporter=text --coverage.include=src/platform/assets/utils/createAssetWidget.ts` - `pnpm exec eslint src/platform/assets/utils/createAssetWidget.ts src/platform/assets/utils/createAssetWidget.test.ts` - `pnpm typecheck` - `pnpm format:check` - `pnpm build:cloud` --------- Co-authored-by: Alexis Rolland <alexisrolland@hotmail.com> |
||
|
|
07f881fc14 |
feat: float the Media Assets bulk-selection bar (#13043)
## Summary
Reskins the Media Assets bulk-selection bar into a prominent floating
pill so bulk-download actions are no longer easy to miss (Linear
FE-989).
## Changes
- **What**: The selection bar now floats over the bottom of the panel as
an inverted rounded pill — close · "{count} selected" · download ·
divider · delete — matching the Figma/prototype spacing, radius, and
shadow. Actions are icon-only with `v-tooltip` hints; the count uses
`tabular-nums` and reflects selected assets (not total outputs).
Extracted into a presentational `MediaAssetSelectionBar.vue` with a
Storybook story, a unit test, and an e2e guard that the count is
per-asset.
## Review Focus
- The pill floats via `position: absolute` (centered, `bottom-6`,
`z-40`) in the sidebar footer slot and overlays the bottom of the grid
by design.
- Count semantics changed from total outputs to selected-asset count
(`selectedAssets.length`).
- Out of scope, deferred per FE-989: marquee / select-all, pagination,
and the favorites / tags / label controls.
## Screenshots (if applicable)
<img width="1003" height="1767" alt="image"
src="https://github.com/user-attachments/assets/3a9ef884-e7f4-4d0b-a495-194ce0860db2"
/>
<img width="643" height="581" alt="image"
src="https://github.com/user-attachments/assets/1161884f-a9c2-4a2b-a20e-33ee3f189935"
/>
<img width="664" height="222" alt="image"
src="https://github.com/user-attachments/assets/b16b083c-bfd9-452d-b508-86b3cbfa9842"
/>
<img width="649" height="265" alt="image"
src="https://github.com/user-attachments/assets/a1076e34-58c9-4e7f-89c4-b21bb3281883"
/>
<img width="559" height="205" alt="image"
src="https://github.com/user-attachments/assets/09c24140-33ce-4629-b681-233c59916043"
/>
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
|
||
|
|
065650b3bf |
fix: open Vue context menu when right-clicking a group (#12971)
## Summary Right-clicking a frame (group) now opens the new Vue context menu instead of the legacy litegraph menu, matching the three-dot menu and node right-click. ## Changes - **What**: In Nodes 2.0 mode, a group right-click is routed to the existing `showNodeOptions` flow (the same menu the three-dot button and node right-click use) instead of litegraph's `processContextMenu`. The group is selected (unless already in the selection) so the menu targets it. Nodes, the canvas background, reroutes, and legacy rendering are unchanged. ## Review Focus - App-layer wrap of `LGraphCanvas.prototype.processContextMenu`, mirroring the existing `useContextMenuTranslation` pattern: no new methods on `LGraphCanvas` (ADR 0008). - Gated on `LiteGraph.vueNodesMode`; legacy rendering keeps the old menu, consistent with legacy node right-click. - Reroute guard: right-clicking a reroute inside a group still gets the legacy "Delete Reroute" menu, not the group menu. Fixes FE-1090 ## Screenshots (if applicable) <img width="719" height="788" alt="image" src="https://github.com/user-attachments/assets/8d514c6d-b7d0-4ec1-841e-677793daf3c7" /> --------- Co-authored-by: GitHub Action <action@github.com> |
||
|
|
a670944a05 |
Fix share auth attribution gap (#13064)
## Summary Logged-out users who open a share link and then sign up/in were not attributed to the share. The `share_id` capture lived in `useSharedWorkflowUrlLoader`, which only runs after `GraphView` mounts — i.e. after the cloud auth guard has already redirected the logged-out user to login. The capture never happened, so `trackAuth` fired without a `share_id`. This moves the capture into the cloud auth guard (`router.beforeEach`), so it runs on the initial navigation before any login redirect. The `share_id` is preserved across the auth round-trip and consumed on auth completion as before. ## Changes - Capture logged-out share attribution in the router guard instead of the share loader, via a new `preserveLoggedOutShareAuthAttribution` util - Extract `isValidShareId` into the shared util and reuse it in the loader (removes the duplicated regex) - Gate capture on `isCloud` (matching the cloud-only consumption); drop the now-dead capture branch from the loader - Make the accepted share-id shape explicit: ASCII alphanumeric start, ASCII alphanumeric/`_.-` after that, max 128 chars ## Notes - Capture no-ops when `share` is absent, so param-less redirects do not clear attribution - If another valid share link is visited before auth completes, the latest valid share replaces the previous attribution - `SHARE` and `SHARE_AUTH` stay separate intentionally: `SHARE` preserves the workflow-loading query, while `SHARE_AUTH` is consumed once by auth telemetry attribution - No behavior change for logged-in users or for share-dialog open/cancel ## Testing - New unit tests for `isValidShareId` and `preserveLoggedOutShareAuthAttribution` (valid/invalid/array/logged-in/boundary cases) - Auth store tests cover `share_id` propagation + consumption across email signup/login, Google, and GitHub - Updated loader and telemetry tests for the relocated capture and `share_id` passthrough - Cloud E2E regression covers logged-out `/?share=abc` redirecting to login after capturing share auth attribution |
||
|
|
ac56ecf009 |
refactor: unify image editor upload contract (FE-750) (#12318)
## Summary Collapse the OSS vs Cloud branching in the mask editor and painter uploads so both runtimes use the same contract — POST to `/upload/image` with `type: input` and no `subfolder`, then reference the result by filename only. ## Related - Companion spec change (Comfy-Org/ComfyUI): https://github.com/Comfy-Org/ComfyUI/pull/13968 deprecates `/api/upload/mask` and documents `/api/upload/image` as the unified upload contract. No runtime behavior changes on the server. ## Changes - **What**: - `useMaskEditorSaver.ts`: replace the separate `uploadMask` + `uploadImage` helpers with a single `uploadLayer`. All four layers (masked, paint, painted, paintedMasked) now go through `/upload/image`. Dropped the `original_ref` form field and the `clipspace` subfolder. `uploadLayer` throws on a non-ok status, on a non-JSON response body, and on a 200 response missing `data.name` (no more silent fallback to the pre-upload ref). - `usePainter.ts`: removed the runtime branch on upload type/subfolder and on the returned widget value. Always uploads as `type: input` with no subfolder; widget value is `${filename} [input]`. Added a `data.name` guard alongside the existing non-ok and JSON-parse guards. - `usePainter.test.ts`: assert the upload FormData carries `type=input` with no `subfolder`, plus new coverage for the missing-name and JSON-parse-failure error branches. - `browser_tests/tests/maskEditor.spec.ts`: drop the now-dead `**/upload/mask` Playwright route interceptors and tighten the save-success assertion to confirm exactly four `/upload/image` calls (one per layer). - **Breaking**: yes for downstream consumers — the mask editor no longer calls `/upload/mask`, and saved widget values for both editors no longer contain a subfolder prefix. Existing nodes will continue to load their referenced inputs because the filename is preserved; new saves emit the unified shape. ## Review Focus - Confirm the mask editor's four-layer upload (masked, paint, painted, paintedMasked) is still correct without `original_ref` — the layers are now independent uploads rather than alpha-composited server-side. The four blobs are composited client-side in `prepareOutputData` before upload, so the previous `original_ref` chain was vestigial — but worth a second look. - OSS-side painter widget back-compat: previously stored as `painter/foo.png [temp]`, new saves emit `foo.png [input]`. The old format remains valid in saved workflows; only the *new save* shape changes. Loading a pre-existing OSS workflow with a `painter/foo.png [temp]` widget value still resolves via the existing parser path. ## Test plan Unit: - [x] `usePainter.test.ts` — 30 tests pass, including the three new ones covering FormData payload shape, missing `data.name`, and bad JSON. E2E: - [x] `browser_tests/tests/maskEditor.spec.ts` — `save uploads all layers and closes dialog` and `save failure keeps dialog open` updated to the unified contract. Smoke-tested manually against a Cloud staging instance: - [x] **Painter node**: paint strokes → save → reload workflow → widget value resolves and the canvas re-renders cleanly. Upload responds with `subfolder: ""`, `type: "input"`. - [x] **Mask editor**: open editor with a base image, paint + mask, save. All four layers (`clipspace-mask-*`, `clipspace-paint-*`, `clipspace-painted-*`, `clipspace-painted-masked-*`) POST successfully to `/upload/image` and return 200 with the asset-aware response shape (`{ name, subfolder: "", type: "input", asset: { id, hash, tags } }`). The resulting `LoadImage` node references the painted-masked filename by basename only and re-renders. Not validated here: OSS ComfyUI core-side smoke. The contract is symmetric (the OSS `/upload/image` endpoint accepts the same fields the FE now sends), but a smoke against OSS HEAD is recommended before merge. --------- Co-authored-by: GitHub Action <action@github.com> |
||
|
|
e97cca9e4a |
feat: show node preview ghost when adding models from dialog & sidebar (#12765)
## Summary Adds consistent ghost node behavior when clicking "use" on models from either the model dialog or treeview - matching the Node Library & Node Search. ## Changes - **What**: - Split `createModelNodeFromAsset` into `resolveModelNodeFromAsset` and `startModelNodeDragFromAsset` to allow sharing logic between the two drag sources - Move NodeDragPreview file & use from being node library tab specific to app level in GraphCanvas so all drag sources share it - drag listeners now attach on `startDrag` and detach on cancel instead of living for the tab's lifetime - Updated `LGraphNodePreview` to accept widgetValues and prepend combo options with passed value so it shows in the preview as the default ## Review Focus - refactored positioning to use RAF with useMouse and transform to fix laggy-follow behavior present in Firefox ## Screenshots (if applicable) Model dialog + Node library https://github.com/user-attachments/assets/b227ac43-c6ea-4cf6-86ed-6cfb196fd80e Model library sidebar https://github.com/user-attachments/assets/bb546aee-5099-4df9-abe5-68bccd8fa2eb |
||
|
|
8d82944441 |
fix: limit workflow models to metadata enrichment (#12990)
## Summary This PR intentionally narrows workflow-embedded model metadata handling so root-level `models[]` and node-level embedded model metadata can enrich existing missing-model candidates, but can no longer create new candidates by themselves. ## Why this PR exists ADR 0009, **Subgraph promoted widgets use linked inputs**, changes promoted value ownership for subgraphs. That design was implemented by [#12197](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12197), **Subgraph Link Only Promotion (ADR 0009)**. Under ADR 0009, a promoted widget is represented as a standard linked `SubgraphInput` on the host `SubgraphNode`. The host boundary owns the promoted value identity through the host node locator plus `SubgraphInput.name`. The interior source widget remains the provider of schema, type, options, tooltip, defaults, diagnostics, and migration metadata, but it is not the persistence owner of the promoted value. This PR is a preparatory cleanup discovered while working on the missing model detection follow-up required by that ADR 0009 / [#12197](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12197) behavior. The follow-up needs missing model detection to respect the new subgraph promoted-widget ownership model. While reviewing that path, we found that the existing embedded model metadata fallback in `enrichWithEmbeddedMetadata` was doing more than metadata enrichment. The important finding was that this fallback was not just attaching metadata to candidates that had already been detected from live node widgets. It could also synthesize brand-new `MissingModelCandidate` entries from workflow JSON metadata, including root-level `models[]` entries, when no live candidate existed. That behavior is inaccurate for the missing model system for two reasons. First, the normal missing model lifecycle is anchored to a real node/widget binding. A candidate found from a COMBO or asset widget has a concrete `nodeId + widgetName` reference. That reference is what lets the UI surface the error, cache it as a pending warning, and later clear or resolve it when the underlying node/widget value is fixed. A root-level `models[]` entry does not reliably provide that anchor. If metadata-only fallback creates a candidate without a real live widget reference, the resulting error can be detected but cannot reliably travel through the existing clearing path. In practice, that can become an effectively unremovable missing model warning unless the user downloads exactly the same model referenced by the stale metadata. Second, a missing model error is meant to mean that a model-selecting widget on an active node references a value that is not available. Workflow JSON metadata by itself is not the same source of truth. If a model only appears in root workflow metadata, or appears in node metadata that is not represented by an active COMBO or asset widget candidate, that is a different kind of state from the existing missing model error model. Treating that metadata as a candidate creates a second, less reliable detector that is not aligned with the scan/clear lifecycle. This is especially important before the ADR 0009 missing-model follow-up. With linked-input promoted widgets, the host promoted value is the value that matters. The interior source widget may still carry stale or default metadata, and it must not become a second source of truth for missing model errors. A detection path that can create candidates directly from workflow metadata would make it harder to reason about which value actually produced the warning. For those reasons, this PR removes metadata-only candidate synthesis and keeps embedded metadata in the role it can perform safely: metadata enrichment. If the live widget/asset scan produces a candidate, embedded metadata may fill in `directory`, `url`, `hash`, and `hashType`. If no live candidate exists, the metadata is not enough to create a missing model warning. This PR is intended to land before the child PR that updates runtime missing model detection for ADR 0009 linked-input promoted widgets. ## Changes - **What**: Restrict `enrichWithEmbeddedMetadata` to enriching existing candidates instead of creating fallback candidates from unmatched root `models[]` or embedded model metadata. - **What**: Remove the now-unused installed-model check callback and asset-support callback from `enrichWithEmbeddedMetadata`. - **What**: Remove the now-unnecessary `modelStore.loadModelFolders()` path from the missing model pipeline, since embedded metadata no longer performs installed-model fallback detection. - **What**: Remove dead source-tracking metadata (`EmbeddedModelWithSource`, source node/widget fields, and widget-name lookup) that only existed to support metadata-only synthesis. - **What**: Update missing model tests so they assert the new contract: metadata enriches live candidates, but does not create candidates without a live scan result. - **What**: Delete obsolete fixtures that only covered the removed metadata-only synthesis path. - **Breaking**: None expected. This is an intentional narrowing of an inaccurate fallback detector, not a public API change. - **Dependencies**: None. ## Review Focus Please focus on whether the candidate lifecycle now has a single source of truth: live COMBO/asset widget scanning creates candidates, while workflow metadata only enriches those candidates. The intended behavioral change is that a model present only in workflow-level metadata, with no active node widget candidate referencing it, no longer appears as a missing model. This avoids surfacing warnings that cannot be cleared through the normal `nodeId + widgetName` path. The expected retained behavior is that active widget-referenced missing models are still detected by `scanAllModelCandidates`, and metadata from root `models[]` or node `properties.models` still supplies download-related fields for those live candidates. ## Screenshots (if applicable) Not applicable. This is a detection/pipeline behavior change covered by unit tests. ## Validation - `pnpm test:unit src/platform/missingModel/missingModelScan.test.ts src/platform/missingModel/missingModelPipeline.test.ts` - `pnpm exec eslint src/platform/missingModel/missingModelScan.ts src/platform/missingModel/missingModelScan.test.ts src/platform/missingModel/missingModelPipeline.ts src/platform/missingModel/missingModelPipeline.test.ts src/platform/missingModel/types.ts` - `pnpm exec oxfmt --check src/platform/missingModel/missingModelScan.ts src/platform/missingModel/missingModelScan.test.ts src/platform/missingModel/missingModelPipeline.ts src/platform/missingModel/missingModelPipeline.test.ts src/platform/missingModel/types.ts` - `pnpm typecheck` - pre-push hook: `knip --cache` |
||
|
|
d4be483c03 |
fix(billing): widen user popover so the credits row keeps both buttons inside (#13052)
## Issue In the top-right user popover, a **cancelled-but-still-active** personal subscription renders **both** "Add credits" and "Resubscribe" in the credits row (the user can still top up *and* re-subscribe during the grace period). With a wide (7-digit) credit balance, balance + help icon + both buttons exceeded the fixed `w-80` (320px) popover and the trailing "Resubscribe" button spilled past the right edge. Surfaced during FE-991 (Billing Rework V1) testing. Pre-existing on `main` — reproducible for any personal owner whose subscription is cancelled but not yet expired. ## Fix Make the popover width **fluid** instead of fixed: `w-fit` clamped to `min-w-80 max-w-96`. It stays **320px** in the common single-action case (unchanged) and only grows — to **~370px** — when the credits row actually needs the room for a second button. ## Before / After **Before (`w-80`)** — "Resubscribe" clipped past the popover edge: <img width="320" alt="before" src="https://github.com/user-attachments/assets/439baae8-9e04-4cdf-b43f-098fb5e3853f" /> **After — single action (stays 320px):** <img width="320" alt="after-single" src="https://github.com/user-attachments/assets/e96f784e-6afd-4286-80c3-1cf0ecec7aa8" /> **After — both actions (grows to ~370px, fits):** <img width="370" alt="after-both" src="https://github.com/user-attachments/assets/578c1528-24ad-4717-a2b5-33e1af78f048" /> ## Test Adds a `@cloud` e2e that opens the popover in the cancelled-but-active state (mocked `/customers/cloud-subscription-status` with `end_date` + a 7-digit balance) and asserts the "Resubscribe" button's right edge stays within the popover bounds — same bounding-box pattern as `workspaceSwitcher.spec.ts`. Validated red→green locally (fails on fixed `w-80`, passes with the fluid width); single-action width measured at 320px, both-action at ~370px. --------- Co-authored-by: GitHub Action <action@github.com> |
||
|
|
403353ac77 |
feat: add tab status indicator (running/done/errored) (#10177)
## Summary Adds indicator to show outcome of last job per tab, cleared next time the workflow is activated. ## Changes - **What**: - add workflow status tracking to execution store, handling various events - add icon to tab based on store - handle race condition where job finishes instantly (e.g. invalid workflow or already executed) ## Screenshots (if applicable) https://github.com/user-attachments/assets/8b1d8d8e-57d4-4ac2-9cc3-0d218d6eb0f7 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10177-feat-add-tab-status-indicator-running-done-errored-3266d73d365081a89f5dfd58487bb065) by [Unito](https://www.unito.io) --------- Co-authored-by: bymyself <cbyrne@comfy.org> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com> |
||
|
|
c4db198875 |
fix(test): de-flake Canvas ctrl+shift+vertical-drag zoom e2e (#13024)
## Observed flake
The e2e test `Canvas Interaction > Can zoom in/out with
ctrl+shift+vertical-drag` (`browser_tests/tests/interaction.spec.ts`)
intermittently failed in CI with:
```
Error: mouse.move: Test timeout of 15000ms exceeded
```
It failed all 3 retries on a single shard while 209 other tests on that
shard passed, and passed on a later re-run — i.e. genuinely flaky, not a
hard break.
## Root cause
The test pressed `Control` and `Shift` *down* once, then ran three
`canvasOps.dragAndDrop` gestures (each performs a `mouse.move(target, {
steps: 100 })`) with three `toHaveScreenshot` assertions interleaved,
and only released the modifiers at the very end — with no `try/finally`.
Two problems:
1. Holding the modifiers across all three drags means every one of the
~300 step-wise `mousemove` events drives litegraph's ctrl+shift zoom
handler (scale recompute + canvas redraw). Combined with the screenshot
captures in between, the main thread can saturate, and a single
`mouse.move` step can stall past the 15s test timeout. That is exactly
the failing call in the signature.
2. Without `try/finally`, a mid-test failure leaves `Control`/`Shift`
stuck down.
## Fix
Switch to the existing `canvasOps.ctrlShiftDrag(from, to)` helper, which
presses and releases `Control`+`Shift` around each individual gesture.
This is the robust pattern already used in `canvasSettings.spec.ts`. The
modifiers are never held across the heavy multi-drag + screenshot
sequence, and are always released.
Test intent, drag coordinates, and all three screenshot assertions are
unchanged.
## Validation
- Verified by reasoning: `ctrlShiftDrag` wraps the same `dragAndDrop`
with `keyboard.down/up` of the same modifiers, called with identical
`Position` args, so behavior and types are preserved.
- Could not run the browser e2e locally (requires a running ComfyUI
backend + Playwright browsers; `node_modules` not installed in this
environment). Relying on CI for the full e2e run.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
|
||
|
|
90c523b4a3 |
feat(dialog): flip remaining callers + default renderer to Reka (Phase 6a cutover, stacked on #12848. 6a-2) (#12593)
## Summary The **renderer cutover** for Phase 6: every remaining dialog caller is flipped to Reka, and `createDialog` now defaults `renderer: 'reka'` so the PrimeVue `Dialog` branch is no longer reached by default (it survives only as an explicit `renderer: 'primevue'` escape hatch, deleted in Phase 6b). > **Stacked on #12848** (mask editor + 3D viewer dialogs + dialog infra). Per @jtydhr88's review, the heavy, screenshot-bearing surface (3D + mask editor) was split into #12848 so it reviews and tests on its own. **Merge #12848 first**, then this PR's base auto-retargets to `main`. Parent: [FE-571](https://linear.app/comfyorg/issue/FE-571/dialog-system-migration-primevue-reka-ui-parent) This phase: [FE-578](https://linear.app/comfyorg/issue/FE-578/phase-6-remove-primevue-dialogconfirmdialog-imports-clean-up-css) ## Changes - **drop dead `ConfirmationService` registration** — zero `useConfirm`/`<ConfirmDialog>` consumers remain in `src/`; desktop-ui keeps its own. - **flip `showConfirmDialog`** — all six confirm callers render Reka chrome; width goes from PrimeVue auto-hug to fixed `size:'md'`, matching `dialogService.confirm()`. - **flip remaining `dialogService` + composable callers** — signin, update-password, top-up, workspace family, cancel-subscription, publish, cloud-notification, edit-keybinding / node-conflict / import-failed, upload-model, queue-clear-history, delete-assets, share / open-shared-workflow, subscription pricing. Self-styled panels get a shared transparent `w-fit` chrome replicating PrimeVue's auto-sized root. - **default `createDialog` to `renderer:'reka'`** — cuts over `showExtensionDialog` (third-party dialogs) and anything unflagged. The single-commit revert point. - **retarget class-based e2e selectors** — `BaseDialog` `.p-dialog` → `getByRole('dialog')`, `BuilderSaveAsHelper` close-X → `getByLabel`, `shareWorkflowDialog` role-based, dead `confirm-dialog` testid removed. - honor `[autofocus]` inside Reka dialogs; size the template browser dialog so the filter bar fits; drop redundant Tailwind width constraints on the remaining callers. ## Review focus 1. **`modal:false` on the pricing dialogs** — same trade-off as Settings/Manager (visual overlay without focus trap) because `PricingTable(.Workspace)` hosts a body-teleported PrimeVue `Popover`. 2. **`w-fit` shrink-wrapped chrome** for self-styled panels — replicates PrimeVue's shrink-to-fit root. 3. **Confirm width change** (auto-hug → fixed 576px `md`) — intentional consistency with `dialogService.confirm()`. ## Public API impact `createDialog` now defaults to Reka. Third-party extension dialogs render through Reka by default — a fixed `size:'md'` frame with a modal focus trap instead of PrimeVue auto-width; `renderer:'primevue'` remains an explicit escape hatch until Phase 6b. Worth a release note for extension authors. ## Out of scope (Phase 6b) PrimeVue branch deletion (`GlobalDialog.vue` legacy branch, `PrimeDialog` import, `.p-dialog` CSS/bridge tokens, `dialogStore` `pt`/`position`/`unstyled` typing) — lands after this soaks one cloud deploy cycle. ## 📸 Screenshots — manual verification Captured via Chrome DevTools (CDP) from this branch running locally in **cloud mode** (proxied to the `cloud.comfy.org` backend, free Personal Workspace). Every dialog below now renders through the **Reka** path — the PrimeVue `Dialog` branch is no longer reached. (Mask editor + 3D viewers live in the stacked base #12848.) **Confirm dialog** (`showConfirmDialog`) — Reka chrome at a fixed `size:'md'`, replacing PrimeVue's auto-hug width — *review focus #3* <img width="880" alt="confirm-dialog" src="https://github.com/user-attachments/assets/5d9953c1-4d0c-4ff9-adc7-88dd370c6a24" /> **Settings** — renders through Reka <img width="880" alt="settings" src="https://github.com/user-attachments/assets/44e3fd3f-8d9b-4322-8fbe-8ce8d94ed15d" /> **Edit Keybinding**, stacked on Settings — small-layout `w-fit` chrome; closing it leaves Settings open (stacked-dismiss holds) <img width="880" alt="edit-keybinding-nested" src="https://github.com/user-attachments/assets/d0875c00-7b9c-439d-b24d-ba6770009d08" /> **Subscription pricing** (`PricingTable`) — opened with `modal:false` because it hosts a body-teleported PrimeVue `Popover` — *review focus #1* <img width="880" alt="subscription-pricing" src="https://github.com/user-attachments/assets/3be20397-8a69-4b00-b803-73eff4e0e313" /> **Share** and **Publish** (open-shared-workflow + publish) — shared transparent shrink-wrapped (`w-fit`) chrome — *review focus #2* <img width="880" alt="share-dialog" src="https://github.com/user-attachments/assets/16f1c1b5-e35e-4664-a957-2f7f61ad96bd" /> <img width="880" alt="publish-dialog" src="https://github.com/user-attachments/assets/935ff453-5247-430f-9c21-2f500d4bc6e2" /> **Workspace** (workspace-family callers) <img width="880" alt="workspace-settings" src="https://github.com/user-attachments/assets/8031a352-f6fc-41e4-9567-e26e0c35ecd9" /> **Template selector** (`showExtensionDialog` / `useWorkflowTemplateSelectorDialog`) <img width="880" alt="templates-dialog" src="https://github.com/user-attachments/assets/9975ebbe-75ae-4ad9-a90a-248db4850e1a" /> **Account / workspace menu** (cloud) <img width="880" alt="account-menu" src="https://github.com/user-attachments/assets/5bc0cade-9bd9-49de-8bb4-779d65e211b0" /> |
||
|
|
78a8d6f8fc |
test: stabilize node help locale e2e (#12998)
## Summary
Stabilizes the locale-specific Node Help E2E by setting the locale
through the existing Playwright settings fixture before app bootstrap
instead of racing a workflow reload pulse.
## Changes
- **What**: Removed the brittle in-page locale mutation helper and uses
`test.use({ initialSettings: { 'Comfy.Locale': 'ja' } })` for the
locale-specific documentation case.
- **What**: Keeps the Japanese and English doc routes local to the test,
then verifies the Japanese help content after loading the default
workflow.
- **Dependencies**: None.
## Review Focus
Please focus on whether the E2E now waits on the correct setup boundary.
The previous helper watched `ChangeTracker.isLoadingGraph` after
changing `Comfy.Locale`; once unrelated workflow-load work became
faster, that loading pulse could complete before the helper observed it.
Pre-boot `initialSettings` avoids that timing dependency and uses
existing test infrastructure.
Verification:
- `pnpm exec oxfmt --check browser_tests/tests/nodeHelp.spec.ts
browser_tests/fixtures/helpers/WorkflowHelper.ts`
- `pnpm exec eslint browser_tests/tests/nodeHelp.spec.ts
browser_tests/fixtures/helpers/WorkflowHelper.ts`
- `pnpm typecheck:browser`
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5176 pnpm
exec playwright test browser_tests/tests/nodeHelp.spec.ts --grep "Should
handle locale-specific documentation" --project=chromium
--repeat-each=10`
## Screenshots (if applicable)
N/A
|
||
|
|
cc41e3e1ac |
test: stabilize cloud template filtering e2e (#12999)
## Summary Stabilizes the cloud template filtering E2E by making the test own both startup asset API responses and the complete template universe it asserts against. ## Changes - **What**: Uses the existing `createCloudAssetsFixture([])` fixture so startup `/api/assets` calls do not create an unrelated error toast that can intercept the Clear Filters click. - **What**: Extends `TemplateHelper.mockIndex()` to also mock `/api/workflow_templates` as an empty custom-template map, so tests that configure a core template index do not accidentally include custom templates from the local backend. - **Dependencies**: None. ## Review Focus Please focus on fixture ownership. This spec asserts exact template counts, so `templateApi.mockIndex()` should isolate the core template index and the custom workflow-template endpoint together. The asset fixture change is intentionally scoped to this cloud spec and reuses existing infrastructure instead of dismissing arbitrary toasts or forcing clicks. Verification: - `pnpm exec oxfmt --check browser_tests/tests/templateFilteringCount.spec.ts browser_tests/fixtures/helpers/TemplateHelper.ts browser_tests/fixtures/helpers/WorkflowHelper.ts` - `pnpm exec eslint browser_tests/tests/templateFilteringCount.spec.ts browser_tests/fixtures/helpers/TemplateHelper.ts browser_tests/fixtures/helpers/WorkflowHelper.ts` - `pnpm typecheck:browser` - `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:8188 PLAYWRIGHT_SETUP_API_URL=http://localhost:8188 pnpm exec playwright test browser_tests/tests/templateFilteringCount.spec.ts --grep "clear filters button resets" --project=cloud --repeat-each=5` - `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:8188 PLAYWRIGHT_SETUP_API_URL=http://localhost:8188 pnpm exec playwright test browser_tests/tests/templateFilteringCount.spec.ts --project=cloud` ## Screenshots (if applicable) N/A |
||
|
|
444dc3fccd |
test: stabilize mask editor screenshot e2e (#13011)
<img width="1155" height="648" alt="스크린샷 2026-06-19 오후 11 24 27" src="https://github.com/user-attachments/assets/01ed2607-662f-4735-b0c2-2f1a2c8a8811" /> ## Summary Stabilizes the Mask Editor screenshot E2E by hiding the transient brush cursor before capturing the dialog. ## Changes - **What**: Moves the pointer from the mask editor pointer zone to the Brush Settings panel before the screenshot and asserts that the brush cursor is hidden. - **Dependencies**: None. ## Review Focus Please check that the test still exercises the dialog UI while excluding only cursor-position noise from the screenshot. The PR also includes the existing browser-test `AppMode` type import fix needed for `typecheck:browser` on branches that touch `browser_tests/**`. Validation: - `pnpm exec oxfmt --check browser_tests/tests/maskEditor.spec.ts browser_tests/fixtures/helpers/WorkflowHelper.ts` - `pnpm exec eslint browser_tests/tests/maskEditor.spec.ts browser_tests/fixtures/helpers/WorkflowHelper.ts` - `pnpm typecheck:browser` - `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5173 pnpm exec playwright test browser_tests/tests/maskEditor.spec.ts --grep "opens mask editor from image preview button" --project=chromium --repeat-each=10` --------- Co-authored-by: github-actions <github-actions@github.com> |
||
|
|
ed028a88be |
Fix LiteGraph hidden widget metadata handling (FE-1014) (#12916)
## Summary
Fixes FE-1014 by making legacy LiteGraph honor backend-provided hidden
widget metadata.
This PR adds a regression test for the Painter node and updates
LiteGraph widget construction so that a backend input spec with `hidden:
true` is mirrored onto the top-level `widget.hidden` property that the
legacy canvas renderer actually reads.
## Problem
Backend node definitions can mark inputs as hidden, for example with
`extra_dict={"hidden": True}`. That metadata already flows into the
frontend widget options as `widget.options.hidden`, which is why Vue
nodes correctly hide those fields.
Legacy LiteGraph, however, does not use `widget.options.hidden` for
canvas visibility. Its rendering, layout, and hit-testing paths check
top-level `widget.hidden` instead. As a result, a field could be hidden
in Vue nodes while still appearing as an editable control in the legacy
LiteGraph canvas.
For affected nodes, this exposes fields that are intended to be
implementation details, schema/version values, or other non-user-facing
inputs.
## Root Cause
The frontend widget construction path copied backend display metadata
into `widget.options`, including:
- `advanced`
- `hidden`
But it did not mirror backend `hidden` metadata into `widget.hidden`.
That created a renderer split:
- Vue nodes and the right panel use `widget.options.hidden`.
- Legacy LiteGraph uses top-level `widget.hidden`.
So backend-hidden widgets were hidden in Vue mode but still visible and
clickable in legacy LiteGraph mode.
## Implementation
The production change is intentionally small and scoped to
backend-provided hidden metadata:
- Continue assigning `inputSpec.hidden` to `widget.options.hidden` as
before.
- When `inputSpec.hidden` is explicitly defined, also assign it to
top-level `widget.hidden`.
This keeps Vue behavior unchanged while making the legacy LiteGraph
renderer receive the same backend hidden signal through the field it
already uses for visibility.
The fix deliberately does not mirror `advanced` into top-level
`widget.advanced`. While investigating this area, I found that many
backend inputs define `advanced`, and changing legacy advanced-widget
behavior would be a much broader behavioral change than FE-1014
requires. This PR only addresses hidden metadata.
## Test Coverage
This PR adds and tightens Painter regression coverage because Painter
currently provides a concrete backend-hidden widget case:
- In Vue mode, the test verifies hidden Painter widgets are not rendered
to the user.
- In legacy LiteGraph mode, the test disables Vue nodes, loads the
Painter workflow, clicks the rows where backend-hidden number widgets
used to be exposed, and verifies the legacy graph editor dialog does not
open.
The legacy test specifically covers the backend-hidden number widgets
`width` and `height`. It uses user-observable behavior rather than
asserting internal widget flags directly.
A follow-up discussion is ongoing about the broader contract between
`widget.options.hidden` and top-level `widget.hidden`, especially for
frontend-extension-only hiding such as Painter `bg_color`. This PR
intentionally keeps that broader renderer-contract question out of scope
and focuses on backend `hidden` metadata from FE-1014.
## Validation
Validated locally with targeted Playwright coverage:
```bash
PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5173 pnpm exec playwright test browser_tests/tests/painter.spec.ts --project=chromium -g "Does not render hidden standard widgets|Does not open editors for backend-hidden number widget rows"
```
Result:
```text
2 passed
```
Also validated with linting:
```bash
pnpm eslint src/services/litegraphService.ts browser_tests/tests/painter.spec.ts
pnpm eslint browser_tests/tests/painter.spec.ts
```
The commit hooks also passed:
- `oxfmt`
- `oxlint`
- `eslint`
- `pnpm typecheck`
- `pnpm typecheck:browser`
## Notes
The new legacy test was confirmed red before the production fix and
green after the production fix, so it is not a vacuous assertion. The
final cleanup commit only tightens test naming and coordinate handling
while preserving the same regression intent.
|
||
|
|
fc4d44c3db |
[feat] migrate website navbar to shadcn-vue + mobile sheet drill-down (#12861)
## Summary - Migrate the website's top nav from bespoke components (`SiteNav`, `MobileMenu`, `NavDesktopLink`, `PillButton`, `MaskRevealButton`) to shadcn-vue primitives (`NavigationMenu`, `Sheet`, `Button`), split into `HeaderMain` → `HeaderMainDesktop` + `HeaderMainMobile`. - Mobile nav becomes a `Sheet` with drill-down sub-navigation, scroll lock, sticky CTAs, sr-only i18n title/description, and a back-to-home logo. - Desktop nav uses `NavigationMenu` with shared viewport, featured cards, `NavColumn` extraction, and centralized nav data in `data/mainNavigation.ts`. - Adds i18n strings for nav labels, dropdown column headers, close/back affordances, and the mobile menu description. ## Test plan - [ ] Desktop: hover PRODUCTS / COMMUNITY / COMPANY — dropdowns open with featured card + columns, NEW badges render, external links show the arrow-up-right icon, viewport is shared between triggers. - [ ] Mobile (<lg): open hamburger sheet — verify logo + close, body scroll is locked while open, top-level nav scrolls if it overflows, CTAs stay pinned at bottom. - [ ] Tap COMMUNITY / PRODUCTS / COMPANY — sub-panel slides over root nav, in-sheet BACK returns to root, links navigate correctly. - [ ] Reload `?locale=zh-CN`: dropdown labels, sheet title/description, BACK / close affordances all localized. - [ ] No regressions on `/cloud`, `/cloud/pricing`, `/customers`, etc. — pages that swap CTAs (`BrandButton` → shadcn `Button`) still render at every breakpoint. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com> Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: Alexander Brown <drjkl@comfy.org> |
||
|
|
7f25d28b71 |
Filter canvasOnly non-preview widgets in editor (#12957)
Non preview, `canvasOnly` widgets like `control_after_generate` could be displayed in the subgraph editor even though promoting them would have no visual or functional effect when in vue mode. In vue mode, these entries are now hidden from the list of candidate items for promotion to reduce confusion. |
||
|
|
67b884d0f7 |
fix(billing): route subscription/sign-in/credit preconditions to modal, out of error panel (FE-878) (#12785)
## Summary
Account preconditions (sign-in / subscription / credits) on running a
workflow now open their modal directly and stay out of the error panel +
error count — previously `subscription_required` fell through to a red
"1 ERROR — Subscription required to queue workflows" banner. This covers
**both** paths: the `execution_error` websocket event and the `POST
/prompt` 402 queue paywall (`{ type: "PAYMENT_REQUIRED", message:
"Subscription required to queue workflows" }`), which is the exact
payload reported in #12840.
## Changes
- **What**: `execution_error` is classified by a pure
`accountPreconditionRouting` resolver (precedence sign-in > subscription
> credits) and routed to the existing modal via
`useAccountPreconditionDialog`; `executionStore` returns early for
preconditions so they never populate `lastExecutionError` /
`lastPromptError` / `lastNodeErrors` → fully excluded from the panel and
`totalErrorCount`. Runtime credit error at a node → credits modal (out
of panel; can name the node).
- **Queue paywall**: the `queuePrompt` catch resolves the same
precondition from the `POST /prompt` 402 response and opens the modal,
short-circuiting before `lastPromptError`, so the queue paywall stays
out of the panel too. The runtime matcher learns the `"Subscription
required to queue workflows"` message.
- **Breaking**: none.
## Before / After
Free-tier queue paywall (`POST /prompt` → 402) on a cloud build:
**Before** — raw `Subscription required to queue workflows` surfaced in
the error panel (no actionable upgrade):
<img width="1600" height="873" alt="before-error-panel"
src="https://github.com/user-attachments/assets/1b76b742-16bf-47e3-9245-17e35f8f1e70"
/>
**After** — clean subscription modal opens; nothing in the error panel
or error count:
<img width="1600" height="873" alt="after-subscription-modal"
src="https://github.com/user-attachments/assets/13d238cb-20bf-4795-a530-5abcf9968dc7"
/>
## Review Focus
- **Routing-only — the run button is intentionally untouched.** The
original AC#3 ("no Subscribe-to-Run button") is superseded by the FE-978
run-lock decision (pre-emptive role-aware lock, Figma 3253-18671).
Complements #12786 (FE-978 run-lock); disjoint file sets.
- Tests: `accountPreconditionRouting` / `useAccountPreconditionDialog` /
`executionStore` — each precondition routes to its modal and is excluded
from the panel/count; precedence resolves on co-occurrence. Plus
Playwright `browser_tests/tests/subscriptionPaywallError.spec.ts` — the
queue paywall (402) stays out of the error panel, with a control
asserting ordinary queue errors still surface. typecheck / oxlint /
eslint / stylelint / oxfmt / knip clean.
Fixes FE-878
Fixes #12840
|
||
|
|
ab6c44aabf |
feat: remove deprecated group nodes, auto-convert to subgraphs on load (#12931)
## Summary Removes the deprecated Group Nodes feature and replaces it with a load-time migration that auto-converts any group nodes in a loaded workflow into Subgraphs (with accepted lossiness). ## Changes - **What**: - `groupNode.ts` is now a migration-only extension. `beforeConfigureGraph` registers temporary node types from `extra.groupNodes` so instances are created during `configure`; a new `afterConfigureGraph` hook converts every group node in the root graph to a subgraph (via `LGraph.convertToSubgraph`), re-scanning until none remain, then deletes `extra.groupNodes`. A failed conversion removes the offending node so loading never hangs or breaks. - Kept the minimum needed: `GroupNodeConfig` (builds the input/output/widget maps), a slimmed `GroupNodeHandler` exposing a rewritten `convertToNodes()` that no longer depends on the execution DTOs, the `globalDefs`/`addCustomNodeDefs` path, and the `nodeDefStore` `Object.assign` shim the migration relies on to detect group nodes. - Deleted: the Manage Group Nodes dialog (`groupNodeManage.ts`/`.css`), execution DTOs (`executableGroupNodeDto.ts`, `executableGroupNodeChildDTO.ts`), the create/builder flow, recreate, commands, keybindings, menus, the `isGroupNode` branches in the right-side panel / error grouping / focus composable, the group-node branches in node templates, dead i18n keys, and the now-unused `serialise` clipboard helper. - Rewrote `browser_tests/tests/groupNode.spec.ts` to assert auto-conversion; deleted the `ManageGroupNode` page object and `manageGroupNode()` helper. - Net: ~2,700 lines removed across 23 files (7 files deleted). - **Breaking**: Group nodes can no longer be created, managed, or executed. Existing workflows still load — their group nodes are converted to subgraphs on open. ## Review Focus - The load-time migration in `afterConfigureGraph` and the rewritten `GroupNodeHandler.convertToNodes()` (no longer uses the execution `getInnerNodes()` / DTOs; derives inner node type/index from `groupData.nodeData.nodes` and relies on `deserialiseAndCreate` + selection ordering). - Kept `nodeDefStore`'s `Object.assign(this, obj)` shim: the migration depends on it to propagate the group-node marker symbol onto the registered node definition. ### Accepted lossiness - Group nodes nested inside subgraphs (or inside other group nodes) convert into the root graph rather than their original container — essentially nonexistent in real legacy workflows since group nodes predate subgraphs. - Temporary `workflow>name` node types stay registered for the session; instantiating one auto-converts it to a subgraph. ## Verification `pnpm typecheck`, `typecheck:browser`, `knip`, `oxlint`, `eslint`, and `oxfmt` are green (also enforced by pre-commit hooks). Unit tests for the touched files could not be run locally due to a pre-existing environment error (`file:///assets/images/*.svg` passed to a Node filename API at import time, which also fails on unmodified test files); the browser spec requires a live server. --------- Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: AustinMroz <austin@comfy.org> Co-authored-by: GitHub Action <action@github.com> |
||
|
|
2cdaead000 |
fix(cloud): stop bouncing working users to /cloud/survey mid-session (FE-739) (#12621)
## Summary
Cloud users get yanked to `/cloud/survey` mid-workflow with no user
action. The redirect is **downstream of auth**: when the Cloud token is
briefly stale (token rotation / auth-refresh / reconnect window), the
authenticated survey-status check 401s, and the gate turned that
transient 401 into "survey not completed" → redirect.
Surveys are currently disabled for everyone on cloud via dynamicconfig
as the live mitigation. This PR lets us re-enable them safely
**without** waiting on the auth rework.
## Root cause
`getSurveyCompletedStatus()` returned `false` ("not completed") on
**any** non-200 — including a transient 401/403/5xx or network error —
and consumers treat `false` as a redirect to the survey. So a
stale-token 401 (or the page force-reload a 401 triggers in
`GraphCanvas`) bounced a working, already-onboarded user to the survey.
The real root cause of the transient 401s is a separate, still-open
effort: **FE-963** (reactive 401 re-mint + single retry), **FE-950/951**
(unified Cloud JWT), **BE-1125**. This PR does **not** fix those; it
stops the survey from being their user-visible casualty.
## The fix
`getSurveyCompletedStatus` now distinguishes the responses instead of
failing closed on all of them:
- **404** → not completed (show survey). This is the genuine signal: the
cloud backend (`GetSettingById`) returns 404 for a survey key that was
never stored, and a 404 is only reachable after a successful
authenticated read (a stale token 401s, never 404s), so it can't be a
transient false signal.
- **transient 401/403/5xx/network** → treat as completed (fail-safe), so
a working user is never bounced.
- **200** → completed iff `value` is non-empty (unchanged).
**No router change.** The `/` onboarding guard is untouched (router.ts
matches main), so the existing UX is preserved — a not-completed user is
still gated to the survey on load; only the spurious transient-failure
bounce is removed.
## Why #12301 was reverted, and how this differs
#12301 shipped a **blanket** fail-safe (`!response.ok → true`, 404
included), which made the survey unreachable for genuinely-not-completed
users (404 → "completed") and was reverted in #12344. This PR
special-cases **404 as the real not-completed signal** and fails safe
only on transient/ambiguous responses, so onboarding still works.
## Tests
- **Unit** (`auth.test.ts`): 200 non-empty → true; 200 empty / `null` /
missing `value` key → false; **404 → false**; 401/403/500/network →
true.
- **E2E** (`browser_tests/tests/cloudSurveyGate.spec.ts`, `@cloud`): a
transient 401 on `/` does **not** bounce a working user; a genuine 404
on `/` **does** route to the survey.
Linear: FE-739. Root cause (separate): FE-963 / FE-950 / FE-951 /
BE-1125.
|
||
|
|
1a27372e44 |
Fix 'insert as node' in sidebar tab (#12900)
When right clicking an output asset from the assets sidebar panel, the 'insert as node in workflow' action was twice bugged - The default type, as used for determining filename annotation, was set to the type of the file. This meant that annotations would never be applied to the filename - `temp` outputs would incorrectly be assigned the `output` type. - My fix for this one gives me a slightly bad taste in my mouth. Parsing URLs isn't great, but it's cleaner than needing to scan the (potentially sparse) full outputs to try and find the corresponding output. |
||
|
|
eafe2af91d |
Indicate in progress upload with spinner (#12673)
While a file is uploading to any of the file picker nodes (ie "Load Image"), the 'select folder' icon is replaced with a loading spinner. I had previously implemented this with a full progress bar indicating the rate of upload, but found it particularly uninformative when used on cloud. <img width="659" height="494" alt="image" src="https://github.com/user-attachments/assets/6d3ca82b-360b-44bc-b123-b276aae2c4d6" /> |
||
|
|
4c870d84ed |
Fix disabling of linked widgets in props panel (#12896)
| Before | After | | ------ | ----- | | <img width="360" alt="before" src="https://github.com/user-attachments/assets/9d602ee3-ff10-48b9-95ca-4c7f5ca57a45" /> | <img width="360" alt="after" src="https://github.com/user-attachments/assets/36e96aff-60ec-4f8c-b7c9-b4d68e03884c" />| Making reactivity function is a little bit clunkier than I would like, but it'll get simplified in the future by east coast swing |
||
|
|
0c23e8305f |
fix: skip templates modal when opening a template from the URL (#12835)
## Summary On first launch, the templates modal flashed open for a split second before a deeplinked template (`?template=`) loaded, which felt broken. ## Changes - **What**: Gate the first-launch templates modal on template URL intent, alongside the existing shared-workflow (`?share=`) check. When a template is being opened directly from the URL, the template modal no longer opens. Behavior is unchanged when no template is in the URL — the template modal still shows for first-time users. - Test util: Added browser_tests/fixtures/utils/flashDetector.ts — installs a pre-navigation requestAnimationFrame sampler that flags if a [data-testid] element ever renders, even for a single frame. This catches a brief flash that toBeHidden() (final-state only) cannot. ## Review Focus `hasTemplateUrlIntent()` mirrors the existing `hasSharedWorkflowIntent()` (direct `route.query` check plus preserved-query fallback for the `/user-select` redirect path). Two regression tests cover both the URL-param and preserved-intent cases. **Coverage:** - Unit (useWorkflowPersistenceV2.test.ts): the modal is not opened when a template param is in the URL, and when template intent is preserved across the /user-select redirect. - E2E (templates.spec.ts): templates dialog never flashes when first-time user opens a template link — verified red-without-fix, green-with-fix. Screen Recording https://github.com/user-attachments/assets/636094d4-0ef0-4e42-af32-d4e6c7ec5731 closes #12836 |
||
|
|
c408f39cee |
test: harden assets media-type filter spec against VirtualGrid flake (#12897)
## Summary Harden the cloud assets media-type filter spec against a VirtualGrid virtualization flake that intermittently failed CI at the `waitForAssets(4)` precondition. ## Changes - **What**: Replace the 7 `waitForAssets(MIXED_JOBS.length)` preconditions in `assets-filter.spec.ts` with `waitForAssets()` (first card visible = data loaded), and document why. ## Review Focus CI artifact (`playwright-report-cloud`) from the failing run showed only 3 of 4 cards in the DOM — the 3D card was missing and the audio card rendered taller (inline player). `VirtualGrid.vue` sizes its render window from a single uniform `itemHeight` measured off the first card, so the taller audio card pushes the 3D card out of the initial window and it is virtualized out of the DOM until a re-measure (same cause as #11635). Requiring all 4 cards to be mounted simultaneously fights virtualization. `tab.open()` already waits for the first card (data loaded), and filtering reads the full asset store regardless of what is mounted, so the per-filter count assertions still provide the real coverage. No behavioral change to the tests — only the readiness/precondition strategy. Verified `pnpm exec eslint` and `pnpm typecheck:browser` pass. Cloud E2E could not be run locally (needs a running frontend + backend); relies on the cloud CI job for confirmation. Related to #11635 --------- Co-authored-by: Amp <amp@ampcode.com> |
||
|
|
e994e4df58 |
refactor: store-backed WidgetId subgraph host widgets; delete widgetValueIO layer (#12617)
## Summary Make `WidgetId` (`graphId:nodeId:name`) the single canonical widget identity and represent subgraph promoted host widgets as ordinary store-backed widgets addressed by it. This deletes two whole indirection layers — the `world/*` widget-entity-IO layer and the `PromotedWidgetView` runtime — leaving one model: a widget's data lives in `widgetValueStore` keyed by `WidgetId`, and a `SubgraphNode` input references it via `input.widgetId`. **Net +304 lines across 107 files** (5,044 added / 4,740 deleted): production code is net **−798** (1,521 added / 2,319 deleted) while tests are net **+1,102** (3,523 added / 2,421 deleted). 10 files deleted outright, 14 added. ## What got deleted The old design wrapped every promoted subgraph widget in a synthetic `IBaseWidget` "view" object with live getters that followed the source widget, plus a manager to keep view identities stable, plus an IO indirection layer over the store. All of it is gone: - `promotedWidgetView.ts` — the `PromotedWidgetView` class (draw / pointer / DOM-sync / projection / deepest-source resolution getters) - `PromotedWidgetViewManager.ts` — view reconciliation/caching - `world/widgetValueIO.ts` — the IO wrapper over `widgetValueStore` - `world/entityIds.ts` + `world/brand.ts` — the `WidgetEntityId` branded-id layer and the `entityId` field - `widgetNodeTypeGuard.ts` — only used by the deleted view - the per-`SubgraphNode` view machinery (`_promotedViewManager`, `_cacheVersion`, view-key generation, DOM position-override cleanup) and every now-dead `isPromotedWidgetView` branch across the panel, menu, store, and util consumers - `domWidgetStore` position-override APIs (`setPositionOverride` / `clearPositionOverride`), only used to render a promoted DOM widget on a different host node ## Why it's simpler - One source of truth. A promoted host widget is `WidgetState` in the store, seeded from the source at promotion (`registerWidget` with a deep-cloned snapshot) and independent thereafter. No synthetic widget objects, no runtime source-following, no view cache to invalidate. - Resolution is data-driven. `resolveConcretePromotedWidget` walks `SubgraphNode` inputs (`input.widgetId` + `resolveSubgraphInputTarget`) instead of chasing view objects through `node.widgets`. This also **fixes two-layer nested promotion** — the previously-skipped parity test now passes and resolves through to the deepest concrete widget. - The right-panel Parameters tab renders a subgraph node's promoted widgets through the **same** store-backed path as ordinary node widgets: display reads `WidgetState` via `widget.widgetId`, and value writes go through `widgetValueStore.setValue(widgetId)`. ## Changes - **What**: - `WidgetId` branded type + `widgetId()` / `parseWidgetId()` / `isWidgetId()` and a `WidgetState` type; `widgetValueStore` is `WidgetId`-native (`registerWidget` / `getWidget` / `setValue` / `deleteWidget`). - Promotion creates host `WidgetState` then an input projection (`input.widgetId`); demotion clears it; serialization and legacy `proxyWidgets` migration round-trip through `input.widgetId`. - `promotedInputWidget.ts` projects a store-backed ordinary widget from an input slot; `SubgraphNode.widgets` is now a projected getter over inputs (kept Litegraph-shaped so the canvas renderer and extensions still read `node.widgets`). `invalidatePromotedViews()` is retained as a no-op for extension compatibility. - `promotedWidgetControl.ts` applies `control_after_generate` (e.g. seed increment) on the host node, since the interior control widget is link-fed and its value is dead; `syncPromotedComboHostOptions` mirrors interior combo options onto host state. - `multilineTextarea.ts` extracts the reusable multiline DOM-widget behavior out of `useStringWidget` and adds promoted multiline materialization via a `createPromotedHostWidget` app-layer hook (keeping Litegraph core free of Vue/Pinia/DOM). - Late-bound `LiteGraph` singleton holder (`litegraphInstance.ts`) to break a widget-init import cycle. - **Breaking**: - `IBaseWidget.entityId` removed — use `widgetId`. - `SubgraphNode.widgets` no longer exposes the old `PromotedWidgetView` objects; promoted state lives in `widgetValueStore` keyed by `input.widgetId` and `widgets` is a projection of inputs. The `widget-promoted` event now carries the concrete interior widget. Extension code reading `entityId` or relying on `PromotedWidgetView` is affected. ## Review Focus - Promotion / demotion / serialization round-tripping through `input.widgetId` + `widgetValueStore`, incl. the legacy `proxyWidgets` migration. - Snapshot-at-promotion semantics (host widget does not follow the source after creation), and the combo-options exception via `syncPromotedComboHostOptions`. - Two-layer nested resolution in `resolveConcretePromotedWidget` + `SubgraphNode` nested-source resolution. - The unified Parameters tab (`TabSubgraphInputs` → `SectionWidgets`): value edit / rename / favorite / hide / reorder for promoted inputs are wired through the store but warrant a visual/e2e pass. - Litegraph-compat seams worth a careful read: projected `SubgraphNode.widgets`, the canvas-edit `callback` bridge back to the store, host-level `control_after_generate`, and the late-bound `LiteGraph` holder / `domWidget.ts` import ordering. --------- Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: AustinMroz <austin@comfy.org> Co-authored-by: GitHub Action <action@github.com> |
||
|
|
cb52a3821b |
fix groups dragging children with control held (#12867)
When control is held, an active drag operation should cease applying movements to nodes contained by selected groups. This functionality was broken in vue mode because of unnecessary reimplementation of the code for calculating items contained by groups during drag operations |
||
|
|
941f220582 |
fix: bind replacement node widgets to reused id (#12872)
## Summary Fixes a Nodes 2.0 node replacement regression where widgets that only exist on the replacement node were not registered with the widget value store, causing their Vue-rendered controls to fall back to component defaults such as `0` instead of the replacement node's real widget default. The root cause is that `replaceWithMapping()` replaces the placeholder node in-place by writing directly to `graph._nodes` and `graph._nodes_by_id`. That path intentionally preserves the old node id, but it also bypasses the normal `LGraph.add()` flow that binds widgets to their owning node id. As a result, newly introduced bindable widgets on the replacement node could exist on the LiteGraph node object while remaining absent from `useWidgetValueStore`, which is the state Vue Nodes reads from when rendering widget controls. ## Changes - **What**: Bind every bindable widget on the replacement node to the reused node id inside `replaceWithMapping()` after the replacement node is inserted into the graph maps and before widget values are transferred. - **What**: Preserve the existing widget value transfer behavior for mapped widgets. Because widgets are now bound before `newWidget.value = oldValue` runs, transferred values are written through the normal widget store state instead of only mutating the unbound widget object. - **What**: Add a focused unit regression check that verifies replacement-only widgets are bound with the reused node id during node replacement. - **What**: Extend the existing node replacement Playwright coverage to assert the Vue Nodes rendered input for `KSampler.denoise` keeps the expected replacement value after the replacement flow. - **Breaking**: None. - **Dependencies**: None. ## Review Focus Please focus on the placement of the widget binding in `replaceWithMapping()`. The binding happens after the new node has been assigned the reused id and inserted into the graph's node maps, but before mapped widget values are copied over from the old node. This mirrors the important part of the normal graph add flow for widgets while keeping the in-place replacement behavior intact. The tests intentionally avoid asserting replacement-node fixture defaults in isolation. The unit test verifies the actual new side effect that prevents the regression: `setNodeId()` is called for a bindable widget that was not present on the old node. The Playwright assertion then covers the user-visible Nodes 2.0 symptom: the replacement widget is rendered from the widget store instead of falling back to the Vue numeric default. Linear: FE-1070 ## Validation - `pnpm vitest run src/platform/nodeReplacement/useNodeReplacement.test.ts` - `pnpm typecheck:browser` - `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5174 pnpm exec playwright test --project=chromium browser_tests/tests/nodeReplacement.spec.ts -g "Widget values are preserved after replacement"` - `pnpm lint` - `pnpm typecheck` - Commit hook also reran staged formatting/linting and `pnpm typecheck` during the final amend. ## Screenshots (if applicable) Before https://github.com/user-attachments/assets/dc4e8137-d8aa-4a70-9973-5559ed84b90e After https://github.com/user-attachments/assets/4c70b9e4-d971-4e94-8d2f-12b0f2b00a09 |
||
|
|
75553fc214 |
fix(settings): widen the Settings dialog to 1280 (#12849)
## Summary The redesigned Settings dialog (Figma DES `3253-16079`) is **1280px** wide, but it rendered at **960px**. Root cause — the width was capped at 960 in **two** layers: 1. `useSettingsDialog.ts` → `SETTINGS_CONTENT_CLASS` (`max-w-[960px]`) sizes the Reka dialog shell. 2. `SettingDialog.vue` → `<BaseModalLayout size="sm">` (`SIZE_CLASSES.sm = max-w-[960px]`) sizes the modal content. Widening only the shell leaves the inner `BaseModalLayout` at 960 (empty space on the right). This sets both to **1280px** and lets `BaseModalLayout` fill the shell (`size="full"`). The dialog size is **not** a workspace-specific concern, so it applies to all Settings (OSS + cloud) — no feature-flag gate. Found during FE-768 designer QA. ## Verification - Live: dialog measures 1280px, content area 1006px (was 960 / 688). - `useSettingsDialog.test.ts`: `contentClass` is 1280px (`size: 'full'`). - `pnpm typecheck` / `lint` / `format` / unit tests green. ## Test Plan - [x] Settings dialog renders at 1280px with the content filling the dialog - [x] Unit test asserts the 1280px sizing ## Screenshots Settings ▸ Plan & Credits at **1280px** (content fills the dialog; was 960px shell / 688px content area): **Personal — Pro:** <img width="720" alt="Settings dialog at 1280px — personal Pro" src="https://github.com/user-attachments/assets/adc2fd9f-d249-469f-b947-1ec8f674cbb0" /> **Team:** <img width="720" alt="Settings dialog at 1280px — team" src="https://github.com/user-attachments/assets/e7378067-11a2-411b-b37b-98c8aecb82b1" /> --------- Co-authored-by: github-actions <github-actions@github.com> |
||
|
|
7438f004c1 |
test: add mask editor load/save round-trip browser tests (#11369)
*PR Created by the Glary-Bot Agent* --- ## Summary Adds `browser_tests/tests/maskEditorLoadSave.spec.ts` covering the untested image loading, save round-trip, canvas dimension verification, and error handling paths in the mask editor. ### Coverage gaps filled - `useImageLoader.ts` — image loads onto canvas with correct dimensions - `useMaskEditorSaver.ts` — save uploads non-empty mask data, round-trip preserves state - `useMaskEditorLoader.ts` — editor initialization, canvas dimension matching - Error handling — partial upload failure keeps dialog open ### Test cases (5 tests, 2 groups) | Group | Tests | Behavior | |---|---|---| | Save round-trip | 3 | Save with drawn mask uploads non-empty data, save-and-reopen preserves mask state, canvas dimensions match loaded image | | Load and error handling | 2 | Opening editor loads image onto canvas, partial upload failure keeps dialog open | ### References - Reuses patterns from existing `maskEditor.spec.ts` (`loadImageOnNode`, `openMaskEditorDialog`, `getMaskCanvasPixelData`, `drawStrokeOnPointerZone`, route mocking for upload endpoints) - Follows `browser_tests/AGENTS.md` directory structure - Follows `browser_tests/FLAKE_PREVENTION_RULES.md` assertion patterns ### Verification - TypeScript: clean - ESLint: clean - oxlint: clean - oxfmt: formatted ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11369-test-add-mask-editor-load-save-round-trip-browser-tests-3466d73d3650818b8245c0b355011136) by [Unito](https://www.unito.io) --------- Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com> Co-authored-by: bymyself <cbyrne@comfy.org> |
||
|
|
cdde1248d4 |
Resolve errant executionIds on workflow restore (#12659)
Node previews are stored by `locatorId`, but sent from the server by `executionId`. Normally, this difference is reconciled when the event is received, but this step is skipped when the workflow is backgrounded. Upon reloading the workflow, these backlogged `executionId`s were incorrectly mapped directly onto node outputs. Any outputs located inside a subgraph would then fail to display because `executionId`s are now `locatorId`s. This is solved by resolving any `executionId`s at time of output restoration. Because `executionId`s can only leak into the outputs of backgrounded workflows, it is safe for resolved `executionId`s to overwrite any pre-existing `locatorId`s. It might wind up cleaner to instead properly enforce that the nodeOutputs cached by change tracker resolve a `locatorId` at time of receipt. This would follow naturally for properly branded id types, but would then require resolving `locatorId` from suspended workflows which is a good bit more involved. |
||
|
|
4b979f4ad0 |
feat(dialog): migrate mask editor + 3D viewer dialogs to the Reka renderer (FE-578) (6a -1) (#12848)
## Summary Splits the **heavy, hard-to-test surface** out of the Phase 6 dialog cutover (#12593) into its own independently reviewable, independently testable PR — per @jtydhr88's review feedback that #12593 bundled too many concepts (3D, mask editor, and the renderer cutover) to test thoroughly at once. This PR migrates only the four style-string dialog callers that carry **Playwright screenshot baselines** and **maximize behavior** — the mask editor and the 3D viewers — plus the shared dialog infrastructure they need. **#12593 is rebased on top of this PR** and now contains only the renderer cutover. Parent: [FE-571](https://linear.app/comfyorg/issue/FE-571/dialog-system-migration-primevue-reka-ui-parent) This phase: [FE-578](https://linear.app/comfyorg/issue/FE-578/phase-6-remove-primevue-dialogconfirmdialog-imports-clean-up-css) ## Why this is safe to land alone **The global renderer default stays `'primevue'`.** Every caller migrated here sets `renderer: 'reka'` explicitly, and the infra additions are purely additive. So no other dialog changes behavior and there is no half-migrated state — the default flip and the remaining caller migrations all live in the stacked cutover (#12593). ## Changes **Heavy callers → `renderer: 'reka'` + `size`/`contentClass`:** - Mask editor (`useMaskEditor.ts`) — `mask-editor-dialog` hook class moves to `contentClass` so `browser_tests` selectors keep working unchanged - 3D viewers ×4 (`ViewerControls.vue`, `AssetsSidebarTab.vue`, `JobHistorySidebarTab.vue`, `load3d.ts`) **Infra to reach Reka parity (additive):** - `dialogStore`: `headerClass`/`bodyClass`/`footerClass` (Reka-path analogues of `pt.header`/`pt.content`/`pt.footer`) - `GlobalDialog`: forward the section classes; merge `bodyClass` into the body wrapper - `DialogContent`: maximized re-asserts its dimension classes after the caller's `contentClass` so maximize wins, mirroring `.p-dialog-maximized` `!important` - `tailwind-utils`: teach tailwind-merge the `max-h-none` class so maximize can release the caller's `max-height` - `rekaPrimeVueBridge`: keep a backgrounded reka dialog from dismissing when a stacked dialog opens on top of it - `maskeditor/useKeyboard`: capture keydown so undo/redo survive the Reka focus trap ## Quality gates - [x] `pnpm typecheck` — clean - [x] `pnpm lint` / `pnpm format` — clean (lint-staged) - [x] `GlobalDialog.test.ts` — 25 passing (incl. new section-class + maximize-override + stacked-dismiss tests) - [x] Changed-source unit tests (`useMaskEditor`, `useKeyboard`, `ViewerControls`, `load3d`) — 77 passing - [ ] CI Playwright — mask editor baselines refreshed for the Reka chrome (`browser_tests/tests/maskEditor.spec.ts-snapshots/*`) ## Out of scope (stacked in #12593) The renderer cutover: `showConfirmDialog` flip, remaining `dialogService`/composable callers (signin, top-up, workspace, subscription, publish, share, …), **the `createDialog` default flip to `'reka'`**, e2e selector retargeting, and the `ConfirmationService` removal. PrimeVue branch deletion remains Phase 6b. ## 📸 Screenshots — before (PrimeVue) → after (Reka) Captured via Chrome DevTools against this branch in cloud mode (`cloud.comfy.org` backend), with an input image / `cube.obj` loaded. Only the dialog **chrome** migrates (PrimeVue `Dialog` → Reka `DialogContent`); the editor/viewer content is unchanged. ### Mask editor (`useMaskEditor`) | Before (PrimeVue) | After (Reka) | |---|---| | <img width="430" alt="mask editor before" src="https://github.com/user-attachments/assets/267e63b5-0832-409e-9c41-edf5ff96561f" /> | <img width="430" alt="mask editor after" src="https://github.com/user-attachments/assets/073cd824-8b01-4c07-99e1-a3a054906c7a" /> | ### 3D viewer (`load3d` / `ViewerControls`) | Before (PrimeVue) | After (Reka) | |---|---| | <img width="430" alt="3D viewer before" src="https://github.com/user-attachments/assets/17b2cd2f-18e4-4d9a-9e0e-80ef833db216" /> | <img width="430" alt="3D viewer after" src="https://github.com/user-attachments/assets/9e20a7a5-4d22-40e6-8fa2-ece58b6e4d20" /> | ### 3D viewer — maximized (maximize-wins dimension re-assertion in `DialogContent`) | Before (PrimeVue) | After (Reka) | |---|---| | <img width="430" alt="3D viewer maximized before" src="https://github.com/user-attachments/assets/b705a4d5-4657-41ad-b6f3-95e54494ac9b" /> | <img width="430" alt="3D viewer maximized after" src="https://github.com/user-attachments/assets/188de427-ab58-45a9-8666-967b2908c320" /> | |
||
|
|
700ff4644f |
feat(workspace): switcher popover left of profile menu + DES-246 copy (FE-769) (#12763)
## Summary
Aligns the workspace switcher and creation flow to DES-246 (FE-769): the
switcher popover now opens to the **left** of the profile menu instead
of on top of it, team workspace rows drop the tier badge, and the
create-workspace dialog matches the design's copy and surface.
## Changes
- **What**:
- `CurrentUserPopoverWorkspace.vue`: replace the nested PrimeVue
`Popover` (rendered on top of the menu) with an inline panel anchored
left of the selector row (`right-full`, top-aligned, outside-click close
via VueUse)
- `WorkspaceSwitcherPopover.vue`: tier badge only renders on the
personal workspace row ("Remove the tier badge for team workspaces,
since there'll only be one plan now")
- Copy (`en/main.json`): switcher create label "Create a team workspace"
("Explicitly say 'team'"); create dialog message "Workspaces keep your
projects and files organized. Subscribe to a Team plan to invite
members.", label "Workspace name", placeholder "Ex: Comfy Org"
- `CreateWorkspaceDialogContent.vue`: surface matched to the Create
Workspace / Default frame — 512px width, muted name label, filled 40px
TextInput (`bg-secondary-background`, `rounded-lg`, `px-4`)
- Invite-flow copy deltas from DES-246: none — #12759 (FE-768) already
matches the design verbatim
Was stacked on #12762 (FE-778); that PR merged, so this is now rebased
onto `main` with only the FE-769 commits.
- Fixes
[FE-769](https://linear.app/comfyorg/issue/FE-769/updates-to-misc-ux)
## Review Focus
- The switcher panel now lives inside the profile popover DOM (no
teleport): clicking a row keeps the menu open, outside-click closes only
the panel
- The CREATOR badge on the profile-menu workspace selector row (visible
in the Figma frame) is intentionally not included — it needs
`is_original_owner` from the BE role-change work and ships with FE-770
- `leave-last-workspace -> auto-create` flow is deferred (not V1),
intentionally untouched
## Screenshots (if applicable)
| Before (overlaps menu, tier badges, "Create new workspace") | After
(left of menu, no team badges, "Create a team workspace") |
| --- | --- |
| <img width="700" alt="before"
src="https://github.com/user-attachments/assets/5522fcca-91b5-49e6-beaa-df1b88bed018"
/> | <img width="1100" alt="after"
src="https://github.com/user-attachments/assets/ce74d42e-19bd-4fe6-9477-b22e5964736d"
/> |
Create-workspace dialog with DES-246 copy and surface (512px, filled
input):
<img width="900" alt="create dialog after"
src="https://github.com/user-attachments/assets/e78eff0a-1c0e-4bbb-ac70-6cc1da996682"
/>
|
||
|
|
6d43320b93 |
Simplify missing model error presentation (#12793)
## Summary Simplifies the Missing Models error card as the fifth slice of the catalog-driven error-tab redesign. This PR is intentionally larger than the previous slices because Missing Models is the only remaining error type where the card UI, OSS download flow, Cloud import flow, and shared model-import dialog all have to move together to preserve the resolution path. The high-level goal is to make Missing Models behave like the other simplified error cards: show the exact missing item, show the affected nodes, keep locate actions predictable, and only expose actions that can actually resolve the problem. This follows the staged error-tab cleanup plan: 1. #12683 refined validation, runtime, and prompt error presentation. 2. #12705 simplified missing media error presentation. 3. #12735 simplified missing node pack error presentation. 4. #12768 simplified swap node error presentation. 5. This PR simplifies missing model presentation and the model-import handoff. ## Why This PR Is Larger Missing Models has more resolution paths than the previous error groups: - OSS can refresh model state, download individual models, and download all available models. - Cloud cannot download directly from the panel; it resolves supported rows through the model import dialog. - Some Cloud rows cannot be resolved through import at all because the node/widget cannot consume imported model assets. - Importing from Cloud needs to know the originating missing-model row so it can lock the expected model type and apply the imported model back to the affected widgets. - Already-imported files can still be unusable if they were imported under a different model type than the missing node expects. Because of those constraints, splitting the card layout from the dialog handoff would leave either a misleading Import button or an import dialog that does not know what it is resolving. This PR keeps that behavior in one reviewable unit. ## User-Facing Behavior ### Shared Missing Models Card - Replaces the older grouped presentation with compact model rows. - Shows each missing model as the primary row label. - Shows model metadata as a smaller sublabel instead of using large section headers. - Keeps locate-node controls visually consistent with the other simplified error cards. - Keeps rows expandable when multiple nodes reference the same missing model. - Shows the affected node rows under expanded models. - Allows single-reference rows to locate the affected node without rendering an extra duplicate child row. - Keeps unknown rows visible, including their affected nodes, instead of silently hiding them. - Removes the old library-select UI from the Missing Models card. ### OSS Behavior - Keeps the refresh action available from the Missing Models group header. - Keeps individual Download actions for downloadable models. - Moves file size out of the Download button label and into the row sublabel. - Keeps Download all when multiple downloadable models are available. - Places Download all at the bottom of the card rather than competing with the group header. - Leaves rows without a download URL as non-downloadable instead of rendering a broken action. ### Cloud Behavior - Shows Import only for missing models that can be resolved by importing a model asset of the required type. - Separates models that cannot be resolved through Cloud import into an Import Not Supported section. - Gives unsupported rows a direct explanation: nodes referencing those models do not support imported models, so users need to open the node and choose a supported built-in model or replace the node with a supported loader. - Treats unknown model type/directory as unsupported for Cloud import, because the import dialog cannot lock a valid model type and the node cannot safely consume the imported asset. - Keeps affected nodes visible in the unsupported section so users still have a path to locate and replace the node manually. ## Cloud Import Dialog Changes The shared model import dialog now accepts missing-model context when opened from the Missing Models card. When that context is present: - The dialog shows which missing model will be replaced. - The dialog lists the affected node/widget references that will be updated. - The model type selector is locked to the required model directory/type. - The Back/import-another path is disabled when it would break the targeted missing-model flow. - Import progress can be associated with the originating missing-model row. - After import completion, matching missing-model references are applied automatically where possible. - If the selected file is already imported under an incompatible model type, the dialog shows a targeted failure state explaining why this import cannot resolve the missing model. This keeps the generic import dialog reusable while adding only the context-specific behavior needed for Missing Models. ## Implementation Notes - `MissingModelCard.vue` owns the card-level grouping and OSS/Cloud section decisions. - `MissingModelRow.vue` owns per-model row rendering, expansion, locate actions, import/download actions, and row-level progress states. - `useMissingModelInteractions.ts` remains the interaction layer for locating nodes and applying resolved model selections. - `UploadModelDialog.vue`, `UploadModelConfirmation.vue`, `UploadModelFooter.vue`, `UploadModelProgress.vue`, and `useUploadModelWizard.ts` receive the missing-model context needed by the Cloud import handoff. - `MissingModelLibrarySelect.vue` is removed because the simplified card no longer exposes that inline selection path. - Locale and selector changes are limited to the new simplified row/section states and removed unused Missing Models strings. ## Tests Added / Updated - Unit coverage for Missing Models card grouping and row states. - Unit coverage for importable vs unsupported Cloud rows. - Unit coverage for model row expansion, locate actions, progress display, and action availability. - Unit coverage for upload confirmation/footer/progress behavior when a missing-model context is present. - Unit coverage for incompatible already-imported model handling. - E2E coverage for OSS Missing Models presentation. - E2E coverage for mode-aware Missing Models interactions. - Cloud E2E coverage for importable rows vs Import Not Supported rows. - Cloud E2E coverage for opening the import dialog with missing-model replacement context. ## Review Focus - Cloud import eligibility: unsupported or unknown model rows should not expose Import as if the row can be resolved automatically. - Missing-model context in the import dialog: the required model type should be locked, and the affected node/widget references should be clear. - OSS parity: OSS should keep refresh, individual Download, and Download all while visually matching the simplified Cloud card where possible. - Narrow side panel behavior: row labels may wrap, but link, primary action, and locate controls should not overlap. - Scope boundaries: this PR intentionally does not redesign Missing Node Pack / Swap Node / Missing Media again; visual parity issues shared across those cards can be handled in a follow-up unification pass if needed. ## Validation - `pnpm format` - `pnpm lint` - `pnpm typecheck` - Related unit tests: 8 files / 84 tests passed - `pnpm build` - OSS Missing Models E2E: `errorsTabMissingModels.spec.ts` passed, 8/8 - Mode-aware Missing Models E2E subset passed, 11/11, excluding unrelated local paste clipboard cases - `pnpm build:cloud` - Cloud Missing Models E2E: `errorsTabCloudMissingModels.spec.ts` passed, 3/3 - Final Claude review: no Blocker or Major findings ## Breaking / Dependencies - Breaking: none. - Dependencies: none. ## Screenshots OSS <img width="575" height="393" alt="스크린샷 2026-06-12 오전 12 25 27" src="https://github.com/user-attachments/assets/f5c44f95-711a-4d3d-99bd-f39ac2bb2012" /> <img width="659" height="351" alt="스크린샷 2026-06-12 오전 12 24 37" src="https://github.com/user-attachments/assets/4bb65a47-c1aa-408b-836b-a1998412f815" /> Cloud <img width="688" height="357" alt="스크린샷 2026-06-12 오전 12 23 59" src="https://github.com/user-attachments/assets/9330a7e7-9f22-420f-82b3-dde0fb2b3dd1" /> <img width="531" height="437" alt="스크린샷 2026-06-12 오전 12 21 13" src="https://github.com/user-attachments/assets/734bd911-f6f7-4872-8868-bb927ddeedd8" /> New import model flow https://github.com/user-attachments/assets/c094c670-62b9-47ce-bfe1-2d09f4f7359d |
||
|
|
e138d17459 |
Fix themeing of nodes (#12712)
In moving styling to `documentElement`, #9516 introduced a regression preventing themes from styling nodes. | Before | After | | ------ | ----- | | <img width="360" alt="before" src="https://github.com/user-attachments/assets/0a27ea1b-ff15-4524-b491-a0de9fee6ed2" /> | <img width="360" alt="after" src="https://github.com/user-attachments/assets/36aee446-6b7d-4b05-96de-39b19989af0d" />| Note: Some elements (like the app mode toggle) are themed using different variables (`--secondary-background` instead of `--component-node-background`). I think the sanest approach is to define `--secondary-background` to be `--component-node-background` by default, but that feels better handled as a followup PR --------- Co-authored-by: Alexander Brown <drjkl@comfy.org> Co-authored-by: github-actions <github-actions@github.com> |
||
|
|
193f23e8c2 |
Revert "feat: default search to essentials when graph is empty" (#12814)
Reverts Comfy-Org/ComfyUI_frontend#12377 |
||
|
|
46526cfabd |
On mode toggle apply to group children (#12809)
When performing mode toggle operations (like bypass or mute) with a group (the colored rectangles) selected, nodes contained within the group will be considered selected and will have their state toggled. <img width="1024" height="1024" alt="AnimateDiff_00002" src="https://github.com/user-attachments/assets/c4e9db17-3fe8-4fd8-9012-0e9a0bc59707" /> |
||
|
|
c7797b201e |
Simplify swap node error presentation (#12768)
## Summary Simplifies the Swap Nodes error card as the fourth slice of the catalog/error-tab presentation refactor, aligning it with the newer compact error-row patterns while preserving the existing replace and locate behavior. This follows the staged rollout plan from the earlier error-tab PRs: 1. #12683 refined execution-style errors: validation, runtime, and prompt errors. 2. #12705 simplified missing media errors into flat, locatable rows. 3. #12735 simplified missing node pack errors and aligned grouped-row behavior. 4. This PR applies the same simplification pass to Swap Nodes errors. 5. A later PR is expected to handle Missing Models, which is larger and intentionally kept separate. After the Missing Models slice lands, a follow-up consistency PR will normalize the shared row/disclosure pattern across Missing Node Packs, Swap Nodes, and Missing Models together. That follow-up will cover parameterized i18n labels for disclosure controls, shared text-button styling, and consistent disclosure semantics/accessibility across those grouped rows. ## Changes - **What**: Reworks the Swap Nodes card rows so each replacement group is presented as a compact row with the source node type, replacement target, replace action, and locate action. - **What**: For a single affected node, the visible row label can be clicked to locate the node, matching the interaction model used by the newer missing-media and missing-node rows. - **What**: For multiple affected nodes with the same replacement target, the group renders a count badge and a disclosure row. Expanding the group shows the affected node rows, each with its own locate action. - **What**: Removes the old node-id badge path from Swap Nodes rows. Node-id badges remain available to the other error cards that still own that behavior. - **What**: Keeps replacement behavior unchanged: per-group replacement and replace-all still call through the existing node replacement store flow. - **What**: Adds regression coverage for the new grouped-row UI, including same-type grouping in both Vue Nodes and LiteGraph render modes. - **Breaking**: None. - **Dependencies**: None. ## Review Focus Please focus on the Swap Nodes presentation and interaction symmetry with the previous error-tab PRs: - Single-node groups should remain directly locatable via the row label and the locate icon. - Multi-node groups should expose the count and expand/collapse behavior without adding duplicate focusable disclosure controls. - The visible row labels intentionally keep their own accessible names, while the separate locate icon uses the generic `Locate node on canvas` accessible name. This mirrors the established pattern from the previous slices. - The newly added Playwright fixture covers two same-type replaceable nodes so duplicate group keys and grouped disclosure behavior are exercised end-to-end. ## Validation - `pnpm format` - `pnpm test:unit src/platform/nodeReplacement/components/SwapNodeGroupRow.test.ts` - `pnpm test:browser:local browser_tests/tests/nodeReplacement.spec.ts --project=chromium` - Pre-commit hook: lint-staged, stylelint, oxfmt, oxlint, eslint, `pnpm typecheck`, `pnpm typecheck:browser` - Pre-push hook: `pnpm knip --cache` - Additional parallel code review pass completed locally; no blocker or major issues remained. ## Screenshots (if applicable) This PR <img width="561" height="362" alt="스크린샷 2026-06-11 오전 3 46 06" src="https://github.com/user-attachments/assets/65395467-6c2f-4aa1-84c5-3d9614c00c80" /> old (Main) <img width="611" height="798" alt="스크린샷 2026-06-11 오전 3 46 32" src="https://github.com/user-attachments/assets/3862d5df-f839-40c0-9488-ce64b051378e" /> |
||
|
|
598cf33ab7 |
[bugfix] Truncate long workspace names in workspace switcher (#12762)
## Summary Long team workspace names wrapped onto multiple lines in the user-menu workspace switcher, overflowing the fixed 54px rows and breaking the dropdown layout. Applies the same single-line ellipsis pattern already used by the current-workspace header (`CurrentUserPopoverWorkspace.vue`). ## Changes - **What**: `truncate` on the switcher name span, `max-w-full` on the name row, `shrink-0` on avatar/tier badge/check icon so only the name shrinks (`WorkspaceSwitcherPopover.vue`, 5 lines) - Regression tests: Vitest component test + `@cloud` Playwright e2e measuring single-line render height Fixes [FE-778](https://linear.app/comfyorg/issue/FE-778/bug-team-workspace-names-wrapping-to-multiple-lines-display-poorly-in) ## Red-Green Verification | Commit | CI | Result | |---|---|---| | `30e04e2` test only | [Tests Unit](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/27278378157) / [Tests E2E](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/27278378213) | 🔴 new unit test + cloud e2e fail (proves tests catch the bug) | | `d8f9a5c` fix | [Tests Unit](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/27279508881) / [Tests E2E](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/27279508715) | 🟢 same tests pass | ## Screenshots | Before | After | |---|---| | <img width="320" alt="before — name wraps to 4 lines, rows collide" src="https://github.com/user-attachments/assets/90f3286a-5b50-4477-9b5c-9d32d0b026e4" /> | <img width="320" alt="after — single line with ellipsis, row height intact" src="https://github.com/user-attachments/assets/8e47bbb2-b5b1-4945-a008-68491f39dc46" /> | ## Review Focus - Truncation chain: the name span is a flex item, so `truncate` (overflow-hidden) zeroes its automatic min size; `max-w-full` caps the `items-start` row at the container width. Mirrors the header pattern — no new component. - Figma `Team Plan - Workspaces` (Workspaces Menu component, node 2045-14413) specifies compact single-line rows; long-name overflow was undesigned, truncation preserves the spec'd layout. |
||
|
|
1b14f4df8a |
Simplify missing node pack error presentation (#12735)
## Summary Simplify the Missing Node Packs error card so it follows the new error-tab item-row direction, with clearer pack rows, predictable locate behavior, and focused E2E coverage. This is the third PR in the staged error-tab simplification plan: 1. Merged: execution/prompt/validation error presentation and catalog grouping in #12683. 2. Merged: missing media presentation simplification in #12705. 3. This PR: missing node pack presentation simplification. 4. Planned next: swap-node presentation simplification. 5. Planned later: missing model presentation and action-flow simplification. ## Changes - **What**: Refactors Missing Node Packs rows so pack-level and node-level actions are easier to scan and more consistent with the rest of the refreshed Errors tab. - **What**: Removes the node-id badge from missing node pack rows, matching the simplified item-row direction. - **What**: Makes a single-node known pack row directly locatable from the pack label, rather than rendering an extra child row. - **What**: Keeps multi-node packs collapsed by default, with both the chevron and pack title toggling the child node list. - **What**: Keeps unknown packs expanded by default, including the single-node unknown-pack case, so users can still see the unresolved node type immediately. - **What**: Keeps per-node child rows clickable for locate-on-canvas behavior when a pack contains multiple affected nodes. - **What**: Replaces missing-node-pack action labels with shared `g.install` and `g.search` copy and removes now-unused English locale keys. - **What**: Adds targeted Playwright coverage for the simplified missing-node-pack card, including unknown-pack default rows, row-label locate behavior, and chevron/title expansion behavior. - **Breaking**: None. - **Dependencies**: None. ## Review Focus Please focus on the missing-node-pack row behavior: - Single known pack with one affected node should stay compact and locate the node from the pack label or locate icon. - Known packs with multiple affected nodes should show a count, start collapsed, and expand/collapse from either the chevron or title. - Unknown packs should expose the affected node rows immediately, including when there is only one affected node. - Locate actions should remain attached to the affected node rows, not to the parent pack when there are multiple nodes. - The E2E fixture intentionally uses two missing nodes with the same `cnr_id` and node sizes of `[400, 200]` to follow browser-test asset guidance. ## Validation - `pnpm format:check` - `pnpm lint` - `pnpm typecheck` - `pnpm knip --cache` via pre-push hook - `pnpm test:unit src/components/rightSidePanel/errors/MissingPackGroupRow.test.ts src/components/rightSidePanel/errors/MissingNodeCard.test.ts --run` - `pnpm test:browser:local browser_tests/tests/propertiesPanel/errorsTabMissingNodes.spec.ts --project=chromium` - Pre-commit hook: staged formatting, linting, `pnpm typecheck`, and `pnpm typecheck:browser` ## Screenshots This PR <img width="531" height="598" alt="스크린샷 2026-06-10 오전 1 54 31" src="https://github.com/user-attachments/assets/9c0addeb-92d2-4cef-a4f3-35a87bbad308" /> old (Main) <img width="509" height="807" alt="스크린샷 2026-06-10 오전 1 53 51" src="https://github.com/user-attachments/assets/b8488f73-d8ed-4356-bd4c-fc678ea205f7" /> |