mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-09 00:29:22 +00:00
97c59f3adbbabe7678df9ccebcbcaf92ed708564
8385 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
97c59f3adb |
refactor(litegraph): delete dead onGetNodeMenuOptions, deprecate onBeforeChange (#12230)
Stacks on #12228. Part of the LGraph dead-hook cleanup per AUDIT-LG.9. ## What - `LGraph.onGetNodeMenuOptions` — **deleted** (field + dispatcher in `LGraphCanvas.getNodeMenuOptions`). Zero ecosystem consumers. - `LGraph.onBeforeChange` — **deprecated, not deleted**. The field and the dispatch in `LGraph.beforeChange()` are kept, but assigning a handler now emits a one-time `warnDeprecated` nudging migration to `LGraphCanvas.onBeforeChange`. ## Why onBeforeChange is preserved The W2F-1 re-audit found `bmad4ever/ComfyUI-Bmad-DirtyUndoRedo` assigns `app.graph.onBeforeChange = fn` (the listener-assignment pattern). Deleting the field outright would silently turn those handlers into no-ops. Keeping it as a deprecated shim preserves backward compatibility during a grace period while signaling the intended replacement. `onAfterChange` is untouched (so `BennyKok/comfyui-deploy`'s `onAfterChange` wrapper keeps working). `LGraphCanvas.onBeforeChange` remains a separate field, and the canvas dispatch chain `this.canvasAction((c) => c.onBeforeChange?.(this))` is unchanged. ## Tests `LGraph.test.ts` covers the shim: the assigned listener is still invoked, the deprecation warning fires when used, and no warning fires when no listener is assigned. ## Sequencing - Stacks on #12228 - Sequences behind Alex's Phase B (#11939, #11811) --------- Co-authored-by: Connor Byrne <c.byrne@comfy.org> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Alexander Brown <drjkl@comfy.org> |
||
|
|
95522a7981 |
feat(billing): align workspace Plan & Credits panel to DES-186 (FE-768) (#12761)
## Summary Aligns the Settings ▸ Workspaces **Plan & Credits** panel to DES-186: state-driven subtitle, team/personal header variants, design perks, and a shared footer help bar. Stacked on FE-964 (#12734), which owns the CreditsTile content. ## Changes - **What**: - Subtitle per design variants (Figma 3255-21472): Active → "Renews on {date}", Ending → "Ends on {date}". `subscription_status: 'scheduled'` falls back to the Active treatment — the facade exposes no scheduled-plan target/date fields yet, so "Changes to {plan} on {date}" cannot be rendered (template reserved in i18n-ready form; see Linear note). - Team-active header: plan name "Team" + seat-aware workspace total (`seat_summary.total_cost_cents` from `/billing/plans`) as "$X USD / mo"; the Next-month-invoice card reads the same computed so the two can't disagree. Per-member tier price + "USD / mo / member" kept as the plans-unresolved fallback. - CTAs per design + designer annotation: "Manage billing" + "Change plan" on a team plan, "Upgrade plan" on personal. - Team perks (Figma 2993-14789): "Your plan includes everything in **Pro**, plus:" (i18n-t, plan name emphasized) + invite members / concurrent runs / shared credit pool / role-based permissions. - Personal no-subscription variant (Figma 2993-14604): "Free · $0 USD / mo" header + primary Subscribe CTA + "What's included:" with the max-runtime perk → "10 min max runtime" (`subscription.maxDuration.free` set to 10 min per DES 3253-16079). - Footer help bar (Learn more / Partner Nodes pricing table / Message support / Invoice history) extracted into `SubscriptionFooterLinks.vue`, shared by workspace + legacy panels; new surface-specific key for "Partner Nodes pricing table". - **Breaking**: none. ## Review Focus - Seat-aware price source (`currentPlan.seat_summary.total_cost_cents`) vs per-member fallback — fixture locks $320 vs $80. - `'scheduled'` → Active fallback (adapter-level test in `useWorkspaceBilling.test.ts`). - Free-state perk copy: `subscription.maxDuration.free` set to **10 min** per DES 3253-16079 (design confirmed; was 30 min). - Free-state overflow (`⋯`) button intentionally omitted: the only existing menu item (Cancel Subscription) doesn't apply to a free plan. Linear: [FE-768](https://linear.app/comfyorg/issue/FE-768/updates-to-workspaces-tab-of-settings) (Plan & Credits half; Members invite UI ships in #12759) ## Screenshots Captured on `local.comfy.org` dev (cloud-prod backend, authenticated session). Team and free states use client-side API stubs (XHR-level for `/api/billing/*` + `/api/workspace/*`, fetch-level for legacy `/customers/*`) since the test workspaces are unsubscribed; personal-active rows are real account data. | State | Before (FE-964 base) | After (this PR) | |---|---|---| | Team plan — active | <img width="400" alt="before: Pro, $100 USD/mo/member, Renews, Manage Payment/Upgrade Plan" src="https://github.com/user-attachments/assets/622a9a27-1875-4c08-92b7-9e43a8067c59" /> | <img width="400" alt="after: Team, $300 USD/mo, Renews on, Manage billing/Change plan, design perks" src="https://github.com/user-attachments/assets/adb0f767-d508-4455-ad8e-ee2d6ac419dc" /> | | Team plan — ending (cancelled) | <img width="400" alt="before: Expires Jul 10, 2026" src="https://github.com/user-attachments/assets/cb4bb978-4e8b-4372-8ecc-7265f477a828" /> | <img width="400" alt="after: Ends on Jul 10, 2026" src="https://github.com/user-attachments/assets/5466b99a-d016-4eba-a60f-99b87bd2693e" /> | | Personal — active (real data) | <img width="400" alt="before: Renews Mar 10 2027, Manage Payment" src="https://github.com/user-attachments/assets/34018400-930b-4147-bd0d-398fb4d159ee" /> | <img width="400" alt="after: Renews on Mar 10 2027, Manage billing" src="https://github.com/user-attachments/assets/87af7fa7-28cf-4cdf-b9a2-158b2a6eb979" /> | | Personal — no subscription | <img width="400" alt="before: generic not-on-a-subscription prompt" src="https://github.com/user-attachments/assets/7ee5b36e-1e07-4630-a9b7-680f4fad349b" /> | <img width="400" alt="after: Free $0 USD/mo header + Subscribe + What's included" src="https://github.com/user-attachments/assets/1eda1791-e3de-46c3-bb69-df0365545211" /> | --- ## Perk descriptions — designer QA (DES `3253-16079`) CDP-verified the Plan & Credits **perk list** per current plan against Figma. Captured at the full 1280px settings layout (width fix: #12849; the perk text/content is identical at the current 960px). | Plan | "Includes" header | Perks shown | |---|---|---| | Free (no subscription) | What's included: | 10 min max runtime | | Personal — Pro | Your plan includes: | 1 hr max run duration · RTX 6000 Pro (96GB VRAM) · Add more credits whenever · Import your own LoRAs | | Team | Your plan includes everything in **Pro**, plus: | Invite members · Members can run workflows concurrently · Shared credit pool for all members · Role-based permissions | **Free — Figma `3253-16079` (left) vs implementation (right):** | Figma | Implementation | |---|---| | <img width="480" alt="Figma DES 3253-16079 free settings" src="https://github.com/user-attachments/assets/cfe79570-f3ba-4627-a8eb-348b9158f6ac" /> | <img width="480" alt="App free settings — What's included: 10 min max runtime" src="https://github.com/user-attachments/assets/d89898e4-d819-486c-9b4f-c2fd61916783" /> | **Personal — Pro:** <img width="640" alt="App personal Pro — Your plan includes" src="https://github.com/user-attachments/assets/adc2fd9f-d249-469f-b947-1ec8f674cbb0" /> **Team:** <img width="640" alt="App team — Your plan includes everything in Pro, plus" src="https://github.com/user-attachments/assets/e7378067-11a2-411b-b37b-98c8aecb82b1" /> Open items (design): - Free perk now reads **"10 min max runtime"** (`subscription.maxDuration.free` set to 10 min) per Figma `3253-16079` — ✅ applied in this PR. - Personal-plan perk **stacking** (show lower-tier perks under the current tier) is an unresolved Figma thread on this node — not implemented. --------- Co-authored-by: GitHub Action <action@github.com> |
||
|
|
f5f1e69cd8 |
feat(auth): Cloudflare Turnstile on email signup (origin-gated) (#12924)
Adds a Cloudflare Turnstile widget to the email/password signup form
(web + desktop). The frontend renders the widget and attaches its token
to the signup request; the verification decision is made server-side.
## Design — config-driven, no origin sniffing
* The widget renders **iff** the `signup_turnstile` mode is `shadow` or
`enforce` **and** a `turnstile_sitekey` is present — both delivered via
cloud remote config. OSS / local builds receive no remote config, so it
never renders. Gating is a pure `isTurnstileEnabled(mode, siteKey)`; an
unknown mode normalizes to `off`.
* Submit is blocked only in **enforce**; **shadow** never blocks.
* The token is sent as `turnstile_token` (snake_case, optional) on the
customer-creation request.
* **OAuth** never renders the widget or sends a token (federated
providers are exempt).
## Behavior
* **Decision is server-side** — the frontend only renders the widget and
attaches the token; the backend verifies it and decides allow/block.
* **Mode-driven** — `off` (no-op) / `shadow` (render + attach, never
blocks) / `enforce` (blocks submit until solved).
* **Config-gated** — no `isCloud`/origin check in the client; the widget
is driven purely by the presence of the mode flag + sitekey in remote
config.
* **Fail-safe to off** — an unknown/missing mode or a missing sitekey
resolves to "don't render", so the feature is a no-op until both are
configured.
* The sitekey is a public, client-side value delivered per environment
via remote config; in dev it falls back to Cloudflare's always-pass test
sitekey.
## Files
New: `config/turnstile.ts`, `composables/auth/useTurnstile.ts` (+ test),
`composables/auth/turnstileScript.ts`,
`components/dialog/content/signin/TurnstileWidget.vue`. Edited:
`SignUpForm.vue`, `SignInContent.vue`, `useAuthActions.ts`,
`authStore.ts` (+ test), `remoteConfig/types.ts`,
`locales/en/main.json`.
## Flow
```mermaid
sequenceDiagram
actor U as User
participant FE as Signup form
participant CF as Cloudflare Turnstile
participant API as Backend signup API
Note over FE: renders only when mode is shadow or enforce<br/>and a sitekey is present
U->>FE: open email/password signup
FE->>CF: load widget with sitekey
CF-->>U: challenge (usually invisible)
U-->>CF: solve
CF-->>FE: token (single-use, short-lived)
U->>FE: submit
FE->>API: signup request with turnstile_token
Note over API: verifies the token server-side and<br/>decides allow/block (shadow never blocks)
API-->>FE: allowed, or blocked in enforce
```
## Rollout
Config-driven and a no-op until enabled:
1. **Merge + deploy** the FE — no visible change while the mode is `off`
/ no sitekey.
2. **Set** the `turnstile_sitekey` in remote config per environment.
3. **`signup_turnstile=shadow`** — the widget renders and attaches the
token; the server observes and never blocks.
4. → **`enforce`** — the FE blocks submit until the challenge is solved.
Kill switch: set the mode back to `off` and the widget stops rendering.
## Refactor: shared script loader
The Turnstile script loader was extracted to
`utils/loadExternalScript.ts` (`createScriptLoader`) and now also backs
the existing Typeform embed loader, removing duplicated
singleton/timeout/cleanup logic. Minor behavioral change: when a
matching `<script>` tag already exists in the DOM, the loader polls for
the global to become ready instead of attaching a `load` listener (which
may have already fired).
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
|
||
|
|
c202e3cc2d |
Allow Glary Bot through CLA (#13149)
## Summary - Add `Glary Bot` to the CLA Assistant allowlist. ## Context PR #13146 is blocked because its commit author is `Glary Bot <bot@glary.dev>`, which GitHub does not resolve to a GitHub user. The CLA action checks the unresolved commit author name after failing to find a linked GitHub account, so the existing `*[bot]` GitHub App allowlist does not apply. ## Validation - Ran `git diff --check`. |
||
|
|
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" />| |
||
|
|
c4ed398511 |
Add assertive profile to review settings (#13120)
## Summary Rabbit too nice. |
||
|
|
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" />| |
||
|
|
5b62ec2431 |
Update CLA allowlist (#13141)
It's `github-actions` without the `[bot]`. This was blocking every PR that contained updated browser test expectations. Additionally, the action already included an allow list for every account ending in `[bot]`. This made half the entries redundant. |
||
|
|
604e4d6414 |
Remove redundant Enter Subgraph action from Errors tab (#13136)
## Summary Remove the redundant **Enter Subgraph** action from Errors tab node cards. This button should not be part of the updated Errors tab design; it remained from the previous implementation and its removal was missed when the new interaction model was introduced. ## Changes - **What**: Removed the Errors tab `Enter Subgraph` button from `ErrorNodeCard`, along with the `enterSubgraph` event plumbing in `TabErrors`. - Removed the now-unused `useFocusNode().enterSubgraph()` helper path, since the Errors tab no longer has a separate subgraph-only action. - Removed the `ErrorCardData.isSubgraphNode` flag and its population in `useErrorGroups`, because it only existed to decide whether to show this button. - Removed the Storybook story and unit-test expectations that were specifically tied to the removed button/flag. - Removed the now-unused English `rightSidePanel.enterSubgraph` i18n entry. Non-English locale files are intentionally left untouched per the repo's localization update policy. ## Why The Errors tab already has a **Locate node on canvas** action. For errors inside subgraphs, that action navigates into the relevant subgraph and centers the target node on the canvas. The removed **Enter Subgraph** action was therefore a weaker duplicate: it entered the subgraph and fit the view, but did not provide the same direct target-node positioning. Keeping both actions made the card UI more crowded and exposed two very similar navigation paths with overlapping intent. The updated design should only keep the more useful locate action, so this PR removes the stale duplicate surface rather than adding another hidden/negative assertion around it. ## Review Focus Please verify that this only removes the Errors tab-specific action. The normal node footer/canvas subgraph navigation behavior remains untouched. Validation run locally: - `pnpm exec vitest run src/components/rightSidePanel/errors/ErrorNodeCard.test.ts src/components/rightSidePanel/errors/TabErrors.test.ts src/components/rightSidePanel/errors/useErrorGroups.test.ts` - `pnpm typecheck` - `pnpm lint` - `pnpm format:check` - `pnpm knip` ## Screenshot Before <img width="335" height="595" alt="스크린샷 2026-06-25 오후 5 33 37" src="https://github.com/user-attachments/assets/545f80e9-68bb-45ef-a4da-0a41012269f6" /> After <img width="344" height="591" alt="스크린샷 2026-06-25 오후 5 34 24" src="https://github.com/user-attachments/assets/7c1f1bf6-c5fd-4a43-9b5c-1392246070a8" /> |
||
|
|
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)
|
||
|
|
b414b1f082 |
feat(billing): inline Invite-your-team block on team-upgrade success (FE-965 / DES-394) (#12954)
Renders the inline **"Invite your team"** block in the team variant of FE-934's "You're all set" success card (`SubscriptionSuccessWorkspace.vue`), so a buyer can invite teammates right after a team-plan upgrade (DES-394, Figma 3084-18651). - New shared `InviteMembersForm.vue` (chips / `TagsInput` multi-email form); seats capped via `useBillingContext.getMaxSeats(tierKey)`; submit via `workspaceApi.createInvite`. - Team upgrades only — personal / single-seat plans show the plain success card; gated on `teamWorkspacesEnabled` + a team plan. - `workspace_invite_sent` telemetry distinguishing a post-upgrade invite from a Settings invite; success-card i18n + Storybook story. **Stacked on FE-934 #12975** (`jaewon/fe-934-team-subscribe-wire`, the success-card host). The PR base is that branch, so this diff is FE-965's delta only. Re-using the same form in the Settings invite dialog is out of scope here (belongs with FE-768 / a follow-up). ## Screenshots | Team upgrade — invite block | Personal / non-team — no invite block | |---|---| | <img src="https://github.com/user-attachments/assets/be5450fe-2b83-46bd-afbc-00e6d33590b7" width="420" /> | <img src="https://github.com/user-attachments/assets/a91909c7-7629-42ef-80b6-45fdb070a0e8" width="420" /> | Storybook: `Components/SubscriptionCheckoutSteps` → `TeamSuccessWithInvite` (with block) / `SuccessAllSet` (without). --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com> |
||
|
|
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> |
||
|
|
c39f378ba4 |
feat(billing): team-plan subscribe + checkout/confirm screen redesign (FE-934) (#12975)
## Summary Two related streams of FE-934 checkout/billing work on this branch: https://github.com/user-attachments/assets/af629def-543e-4bcd-894d-b35aa032fe0a 1. **Team-plan subscribe** wired to the BE-1254 credit-stop contract (replaces the "coming soon" toast stub). 2. **Checkout / confirm screen redesign** aligned to the updated DES mockup — yearly pricing display, dialog navigation, and the plan-change confirm. <img width="1078" height="427" alt="team subscribe" src="https://github.com/user-attachments/assets/2b5f7192-3c91-4e2d-b495-832ca5a26657" /> ## Changes ### Team-plan subscribe (credit-stop contract) - Team subscribe sends `{ plan_slug: team_per_credit_monthly | team_per_credit_annual, team_credit_stop_id, billing_cycle }` to `POST /api/billing/subscribe` and handles the response like the personal path (`subscribed` → success, `needs_payment_method` → payment URL, `pending_payment` → poll). Slider stops come from `GET /api/billing/plans → team_credit_stops` via `mapApiTeamCreditStops`, falling back to the hardcoded DES-197 stops so OSS / pre-deploy still render. `preview-subscribe` is unchanged — the team confirm step is display-only. - **Internal API change**: `workspaceApi.subscribe(planSlug, returnUrl?, cancelUrl?)` → `subscribe(planSlug, options?: SubscribeOptions)`; the billing facade (`useBillingContext` / `useWorkspaceBilling` / `useLegacyBilling`) and callers were updated to match. ### Checkout / confirm screen redesign (DES mockup) - **Yearly confirm**: headline is now the ÷12 monthly-equivalent with a `{total} Billed yearly` (yearly) / `Billed monthly` (monthly) subtitle; credits show `Each year credits refill to` (×12) for yearly; `Starting today` → `Starts today`. The team confirm now receives the active billing cycle (the subtitle was missing because it wasn't passed), and the redundant header credits/month line was dropped. - **Navigation**: the pricing table stays mounted (`v-show`, not `v-if`) so the plan / billing-cycle / credit-stop selection survives a round trip to the confirm step and back; **Backspace** mirrors the back arrow (ignored while an input/textarea/contenteditable is focused). - **Plan-change confirm**: rewritten from the 2-column current→new comparison to the **single-plan layout**, branching on `previewData.is_immediate` — immediate upgrades show prorated line items (`new_plan.price_cents − cost_today_cents` = credit) + upfront (yearly) / monthly credit refill + a prorated total ("Confirm upgrade"); scheduled downgrades show `Starts {date}`, $0 due today, and an "After that" block ("Confirm change"). - **Storybook**: `SubscriptionCheckoutSteps` stories for each new-sub / upgrade / downgrade variation (props-driven, no API). ## Review Focus - **Merge gate (team subscribe)** — ✅ **resolved**: BE-1254 is now merged in cloud `main`. The live `GET /api/billing/plans` returns `team_credit_stops` (`TeamCreditStop` struct + validation in `common/billing/catalog/catalog.go`, served for every workspace, asserted by the `billing_credit_stops_contract` smoke test). The gate is lifted. - **Fallback safety**: with the contract live, the hardcoded DES-197 stops are now purely the OSS / pre-deploy fallback — they render only when the API doesn't supply `team_credit_stops`. In that window a real subscribe is still impossible (no stop `id`) and surfaces a `teamPlan.unavailable` toast. Open question retained: hide/disable the team CTA in that window instead of toasting? - **Plan-change scope**: the single-plan confirm redesign covers **personal** changes (the real `previewSubscribe` path). **Team** plan changes route through the display-only team confirm and aren't wired to `previewSubscribe` (BE left `PreviewSubscribeRequest` as `plan_slug` only) — team-change proration is a follow-up. - **Dead locale keys**: the old 2-column transition keys (`everyMonthStarting`, `youllBeCharged`, `proratedRefund`, `proratedCharge`, `creditsRefillTo`, `switchToPlan`, `starting`, `ends`, `confirmPlanChange`) are now unused — can be removed in a follow-up cleanup. - **Out of scope**: the success "Your change is scheduled" variant from the mockup. - Based on `fe-934-unified-pricing-table`; base + `main` merged in to resolve conflicts (PR is now mergeable). ## Verification - Confirm / transition component tests green (`SubscriptionAddPaymentPreviewWorkspace`, `SubscriptionTransitionPreviewWorkspace`); oxlint / oxfmt clean locally; full typecheck runs in CI. - Each variation viewable in Storybook → `Components/SubscriptionCheckoutSteps`. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com> |
||
|
|
00c7eaef79 |
feat: route cloud auth through the single Cloud JWT under unified_cloud_auth (FE-950) - step 3 (#12708)
## Summary Behind `unified_cloud_auth` (default OFF), flip the two token accessors so every cloud request rides the single Cloud JWT minted in PR2. This is the consumer-flip phase of FE-950: PR2 built the dormant `unifiedToken` slot; this PR makes consumers read it — and surfaces the permanent-auth-failure path that the flip turns from graceful degradation into a hard stop. Stacked on #12704 (PR2), now merged; base is `main`. ## Changes - **What**: - `getAuthHeader()` — flag ON returns `{ Authorization: Bearer <unifiedToken> }` (or `null` if unminted), with **no** Firebase/API-key fallback. Flag OFF keeps the exact workspace → Firebase → API-key cascade. - `getAuthToken()` — flag ON returns the unified Cloud JWT (or `undefined`); flag OFF keeps workspace → Firebase. - Both accessors are the single seam every cloud consumer already routes through, so the flip propagates automatically with **no edits** to `fetchApi` (`scripts/api.ts`), `/customers/*` (authStore), `workspaceApi`, the WebSocket (`api.ts:568`), or backend-node auth (`app.ts:1593`). - **Surface permanent auth failures** (answers @pythongosssss's review on PR2). Under the flag there is no Firebase fallback, so a silent `clearUnifiedContext()` wipe would strand every cloud request until manual re-login — unlike the legacy path, which degrades to the Firebase token. `refreshUnified()` and `mintAtLogin()` now emit a user-facing error toast (keyed by error code off the existing `workspaceAuth.errors` i18n) on the permanent codes (`ACCESS_DENIED` / `WORKSPACE_NOT_FOUND` / `INVALID_FIREBASE_TOKEN` / `NOT_AUTHENTICATED`). `mintAtLogin()` now resolves `false` on a permanent failure instead of rejecting an unhandled `void`ed promise. Transient failures stay silent (proactive refresh still retries). Also trims the verbose unified-lifecycle comments flagged in review. - **Breaking**: None. Flag OFF is byte-for-byte the current cascade. ## Review Focus - **Single token, no fallback under the flag.** Tests assert `getAuthHeader`/`getAuthToken` return only the unified token and never call `getIdToken` or the API-key store; they return `null`/`undefined` (not a fallback) when the token is unminted. - **Surfacing, not recovery.** This PR makes the terminal state *visible* (toast); the existing router auth-guard still redirects to login on the next navigation. Active recovery (automatic re-mint on 401) stays in the deferred safety-net PR so the toast is never a dead-end "please re-login" with the fix one PR away. - **Flag-OFF parity.** The full existing cascade suite runs with `unifiedCloudAuthEnabled = false` (the `beforeEach` default) and stays green. ## Deferred (intentional) - **`acceptInvite` is left unchanged — still Firebase-authed.** It is the one cloud call that intentionally keeps the raw Firebase token, because the invite is accepted *before* the user is a member of the target workspace. Promoting it to the unified Cloud JWT first needs a quick check that `POST /invites/:token/accept` accepts a personal-scoped Cloud JWT for a not-yet-member; deferred until that is verified. `getFirebaseAuthHeader` / `getFirebaseAuthHeaderOrThrow` stay defined (their removal belongs to the later cleanup ticket, FE-951). No `workspaceApi.ts` change in this PR. - **The reactive 401 re-mint + retry safety net is a follow-up.** A clean place to intercept a `401` and re-mint once does not exist yet: cloud requests use raw `fetch` (`/customers/*`, `/auth/token`) plus several independent axios clients (`workspaceApi`, `customerEventsService`, registry, manager), with no shared response interceptor. PR2's `remintUnifiedOnce()` primitive is ready, and the proactive buffer-based refresh (`refreshUnified`) already covers the common token-*expiry* case, so this cross-cutting safety net (plus deciding whether the surfacing toast escalates to a guided re-login CTA once remint exists) lands in its own focused PR before any production rollout. Note this is orthogonal to the surfacing above: proactive refresh prevents expiry; it cannot prevent *revocation*, which is exactly what triggers the now-surfaced permanent-error path. ## Tests - Extended `authTokenPriority.test.ts`: flag-ON `getAuthHeader` returns only the unified JWT (Firebase + API-key + workspace untouched) and `null` when unminted; flag-ON `getAuthToken` returns the unified JWT (not Firebase) and `undefined` when unminted. Existing cascade tests prove flag-OFF parity. - Added to `useWorkspaceAuth.test.ts` (red-green + regression lock): a permanent refresh error toasts the **correct i18n key for each of the four permanent codes** (`it.for` over 403/404/401 + a lost-Firebase-token `NOT_AUTHENTICATED` case) and clears the slot; a permanent login-mint error toasts and resolves `false`. Negative guards prove the surfacing is **error-only and flag-scoped**: a transient (5xx) refresh does **not** toast and keeps the slot, a **successful** re-mint does not toast, and the unified lifecycle **never toasts when the flag is OFF** (even against a rejecting backend). ## Red-Green Verification | Commit | CI | Purpose | |--------|-----|---------| | `test: cover permanent unified-auth error surfacing` | 🔴 Red ([run](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/27455949404)) | Proves the tests catch the silent-failure gap | | `fix: surface permanent unified-auth errors instead of failing silently` | 🟢 Green ([run](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/27456200098)) | Proves the surfacing resolves it | Part of FE-950 (single Cloud-JWT provider at login, Phase 1). |
||
|
|
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> |
||
|
|
e8ea16463f |
feat(workspace): redesign Members invite UI to DES-186 (FE-768) (#12759)
## Summary
Redesigns the Settings ▸ Workspaces Members invite flow to DES-186:
email-chips invite dialog, Resend/Cancel pending-invite actions, and
explicit team-plan gating.
## Changes
- **What**:
- `InviteMemberDialogContent`: single-email link-generation flow →
comma/Enter/paste-separated email chips (`TagsInput`); batch
`createInvite` via `Promise.allSettled` — on partial failure, failed
emails stay as chips with an error toast; success shows an Invited
state.
- `PendingInvitesList`: inline Copy-link/Revoke buttons → `⋯` overflow
menu with **Resend invite** / **Cancel invite**. New
`teamWorkspaceStore.resendInvite` issues the fresh invite *before*
revoking the old one, so a failed resend never destroys the original.
- FE-built invite links removed
(`getInviteLink`/`createInviteLink`/`copyInviteLink`,
`PendingInvite.token`) — invite delivery is BE email per DES-186.
- New `useTeamPlan` composable: explicit `isOnTeamPlan` (seat-based
`maxSeats > 1` proxy until BE-1254 exposes the plan signal) replaces
`isSingleSeatPlan` branching in
`WorkspacePanelContent`/`useMembersPanel`.
- Personal/no-team-plan state per Figma 2993-14604: "To add teammates,
upgrade your plan." banner + **Upgrade to Team**; "Need more members?
Contact us" footer; upsell dialog copy → Team-plan framing; members sort
by Role column (was Join date).
- **Header layout per design (2nd commit)**: `[Search][Invite +][⋯
workspace menu]` moved from the dialog tab row into the members card
header row (the "N of M members" row) — the tab row right side is empty
in every DES-186 frame. Workspace menu extracted to
`WorkspaceMenuButton.vue`.
- **Single-member gating per design annotations**: when the owner is
alone — search hidden ("Show if more than 1 members"), Active/Pending
segmented tabs hidden, role column hidden, and the Members tab label
drops its count ("Remove '(0)' as well if it's just 1 member"). Tabs
reappear if pending invites exist so they stay manageable.
- **Breaking**: `teamWorkspaceStore` invite-link API removed (no
external consumers found in-repo).
## Review Focus
- Resend semantics (create-first, then revoke) and pending-invites state
update in `teamWorkspaceStore.resendInvite`.
- Invite-link removal assumes BE sends invite emails (DES-186 annotation
"Resends the invite email"). If BE email delivery is not yet live, this
PR should wait on that confirmation.
- Gating matrix: team-active owner → enabled; team at seat cap →
disabled+tooltip; non-team plan → upsell dialog; personal workspace →
disabled.
- Workspace `⋯` menu (Edit/Delete/Leave) now lives only on the Members
tab card header, matching DES-186 — the Plan & Credits tab no longer
exposes it (design shows the plan-card `⋯` there instead, shipped via
FE-964/FE-768b).
## Screenshots
Captured on `local.comfy.org` dev (cloud-prod backend, authenticated
session; team-plan states use client-side XHR-level API stubs for
subscription/members/invites since the test workspaces are
unsubscribed).
| Flow | Before (main) | After (this PR) |
|---|---|---|
| Members list — team plan | <img width="400" alt="before members"
src="https://github.com/user-attachments/assets/be214b95-4783-47ff-9539-59c8a33b5eb9"
/> | <img width="400" alt="after: Search/Invite/menu in the card header
row, Role column, design demo data"
src="https://github.com/user-attachments/assets/841c89d5-2a29-4eed-9c72-4a5ee8bee9f4"
/> |
| Pending invites — row actions | <img width="400" alt="before pending:
inline copy-link/revoke icons"
src="https://github.com/user-attachments/assets/1703850e-86bc-4735-81b3-7530c01ad46f"
/> | <img width="400" alt="after pending: overflow menu with
Resend/Cancel invite"
src="https://github.com/user-attachments/assets/9b1bbe03-d82b-4cf6-86f2-e01d7b898ae7"
/> |
| Team workspace with 1 member | (same chrome as members list: search +
tabs always shown) | <img width="400" alt="after: no search/tabs/role,
tab label without count, Invite + menu in card row"
src="https://github.com/user-attachments/assets/22c7f68a-eb0f-49a9-a203-516cd9b7e02d"
/> |
| Invite dialog | <img width="400" alt="before: single email, Create
link"
src="https://github.com/user-attachments/assets/c19d8bbb-feb5-4fe4-8541-6d52e6ab6600"
/> | <img width="400" alt="after: comma-separated email chips"
src="https://github.com/user-attachments/assets/101fdc7b-d6e0-4f7d-8966-894bbf16b4aa"
/> |
| Invite dialog — submit result | (copies a link) | <img width="400"
alt="after: Invited success state"
src="https://github.com/user-attachments/assets/f539a88c-0250-434c-bf4a-6ce714b30398"
/> |
| Upsell — invite without team plan | <img width="400" alt="before:
subscription required, Creator plan copy"
src="https://github.com/user-attachments/assets/bb2cb9fd-f298-4cb0-b39a-6d59061dcea1"
/> | <img width="400" alt="after: Team plan required, Upgrade to Team"
src="https://github.com/user-attachments/assets/45170ed5-63bd-469a-af12-197a8b7e09ee"
/> |
| Personal workspace — Members tab | (create-workspace hint text) | <img
width="400" alt="after: upgrade banner + disabled Invite"
src="https://github.com/user-attachments/assets/a0ae664b-2d20-4d87-900d-7a36872ecde3"
/> |
Linear:
[FE-768](https://linear.app/comfyorg/issue/FE-768/updates-to-workspaces-tab-of-settings)
(Members half; Plan & Credits panel ships separately stacked on FE-964)
|
||
|
|
da55529d23 |
GTM-93 point Windows download at comfy.org proxy (#12974)
## Summary - Point the website Windows desktop download URL at `https://comfy.org/download/windows/nsis/x64`. - Keep macOS on the existing ToDesktop URL. - Update the download page smoke test to expect the new Windows href. ## Context This is the frontend leg of the GTM-93 Windows MVP. ToDesktop still controls `download.comfy.org`; instead of changing DNS, the website sends Windows users to a controlled `comfy.org` proxy path that the router PR handles. The proxy forwards to ToDesktop and adds a tokenized `Content-Disposition` filename for Desktop to consume on Windows. Linear: https://linear.app/comfyorg/issue/GTM-93/fix-posthog-identify-call-unblock-funnel-attribution-desktop-funnel Router PR: https://github.com/Comfy-Org/comfy-router/pull/33 Desktop PR: https://github.com/Comfy-Org/Comfy-Desktop/pull/1149 ## Validation - `pnpm --filter @comfyorg/website run typecheck` - `pnpm --filter @comfyorg/website run build` - `pnpm --filter @comfyorg/website exec playwright test e2e/download.spec.ts` - pre-commit: `pnpm typecheck`, `pnpm typecheck:website` |
||
|
|
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> |
||
|
|
7ab6cb57c5 |
test: 1/x fix coverage run (#13086)
## Summary
Fix the two current blockers that prevented `pnpm test:coverage` from
completing on `main`.
Stack order: 1/x
## Changes
- Mock `load3dAdvanced` in the lazy-loader test so coverage does not
import the real Load3DAdvanced UI graph.
- Track the active workflow status in `useWorkflowStatusDismissal` so
terminal statuses arriving after activation are cleared.
## Test Results
| | before | after |
| -- | -- | -- |
| `pnpm test:coverage` | ❌ failed, so the stack had no usable coverage
baseline | ✅ passed with 877 test files passed; 11,772 passed / 8
skipped |
| focused tests | `load3dLazy` timed out; `useWorkflowStatusDismissal`
failed its active-workflow status case | ✅ `load3dLazy`: 13 passed;
`useWorkflowStatusDismissal`: 4 passed |
## Coverage
| | before | after |
| -- | -- | -- |
| statements | unavailable | 62.84% |
| branches | unavailable | 53.03% |
| functions | unavailable | 56.94% |
| lines | unavailable | 64.05% |
Screenshots: N/A, no UI change.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> <sup>[Cursor Bugbot](https://cursor.com/bugbot) is generating a
summary for commit
|
||
|
|
3c3a2ab4e2 |
fix: Load Audio node not caching execution (#12950)
## Summary This PR fixes a bug where the Load Audio node re-executes everytime. ## Changes - **What**: Mark `audioUIWidget.options.serialize = false` --------- Co-authored-by: Amp <amp@ampcode.com> |
||
|
|
a07854755f |
fix(billing): restore unified pricing dialog width (Reka renderer regression) (#13092)
## Summary Restore the unified "Choose a Plan" pricing dialog width — it was collapsing to the default `md` (576px) frame, so the 1280px table overflowed and rendered off-center with the right card clipped. ## Changes - **What**: `showPricingTable` opens the unified dialog (`SubscriptionRequiredDialogContentUnified`) with PrimeVue-path props for sizing (`style: 'max-width: 95vw'` + `pt`). Since #12593 (FE-578 Phase 6a) made **Reka the default dialog renderer**, those props are ignored — Reka sizes via `size`/`contentClass`, so the dialog fell back to `size: 'md'` (`max-w-xl` = 576px). The content root's `xl:w-[min(1280px,95vw)]` then overflowed the 576px box and shifted off-center. Moved the width onto a Reka `contentClass` (`w-fit max-w-[min(1280px,95vw)]`), matching the sibling subscription dialogs in the same file. ## Review Focus - **Regression origin**: the broken config landed when #12666 (FE-934, UnifiedPricingTable) merged on top of #12593's reka-default flip while still using the PrimeVue config. No merge conflict — the `style` line is valid but dead, so it broke silently. FE-991 (#12792) predates #12593, so it still rendered via PrimeVue and looked correct (matching the report that it was fine there). - **`w-fit` vs fixed width**: `w-fit` preserves the original "dialog hugs its content per step" intent — the content root only sets the 1280px width on the pricing step, so confirm/success steps still shrink instead of floating in a 1280px box. - Out of scope: the legacy-team / flag-off paths share a PrimeVue `style` shell and are likely affected the same way under Reka; left for a follow-up (flag-off is the lower-priority OSS path). ## Verification - Unit test `useSubscriptionDialog.test.ts` — red without the fix (dialog has no `contentClass`), green with it. - Verified live (cloud dev, viewport 1301px): box centered at 1236px (95vw), no overflow, all three personal cards visible. ## Screenshots Personal tab, viewport 1301px: | Before | After | | --- | --- | | <img width="480" alt="before" src="https://github.com/user-attachments/assets/e233fe00-f754-4e34-837f-cf6630ccbfb9" /> | <img width="480" alt="after" src="https://github.com/user-attachments/assets/dedd92b7-8707-4865-b7f3-289919043b48" /> | |
||
|
|
2adef5d9f6 |
Create script for pointing at prod and staging backends (#13096)
## Summary Allows engineers to run their localhost frontend while choosing which backend to point. This PR adds staging and prod as targets. ## Changes - **What**: New NPM scripts: `dev:cloud:test`, `dev:cloud:staging`, and `dev:cloud:prod`. `dev:cloud` points at `dev:cloud:test` - **Breaking**: None ## Why Currently, the testcloud environment is broken (backend config issue) and doesn't allow going through the subscription registration process. This also allows testing frontend code against backend changes being staged for release, as well as against actual backend production code. |
||
|
|
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> |
||
|
|
6068571b35 |
Refactor: Brand node execution and locator IDs (#13071)
## Summary - Brand `NodeExecutionId` and `NodeLocatorId` as distinct required string types. - Route execution/locator ID construction through existing helper functions instead of minting raw strings at call sites. - Update tests and boundary parsing to use branded IDs without conflating them with local `NodeId` values. ## Validation - `pnpm typecheck` - `pnpm test:unit src/types/nodeIdentification.test.ts src/stores/executionStore.test.ts src/renderer/extensions/vueNodes/components/NodeSlots.test.ts src/composables/graph/useErrorClearingHooks.test.ts src/platform/nodeReplacement/missingNodeScan.test.ts -- --runInBand` - `pnpm exec eslint src/types/nodeIdentification.ts src/utils/graphTraversalUtil.ts src/platform/workflow/management/stores/workflowStore.ts src/renderer/extensions/minimap/data/LayoutStoreDataSource.ts src/renderer/extensions/vueNodes/execution/useNodeExecutionState.ts src/stores/workspace/favoritedWidgetsStore.ts src/stores/nodeOutputStore.ts src/utils/__tests__/executionErrorTestUtils.ts src/platform/nodeReplacement/missingNodeScan.test.ts src/stores/executionStore.test.ts --cache` Note: full `pnpm lint` timed out after 5 minutes while still in stylelint startup, so targeted lint was run on changed files. ## Open Question - Should root-level node IDs like `1` be considered valid `NodeExecutionId` values, or should `isNodeExecutionId()` require a colon and callers use a separate type/helper for root execution IDs? |
||
|
|
e37f168eaa |
Add merge_group event to CLA workflow (#13093)
## Summary So it runs in the merge queue. |
||
|
|
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> |
||
|
|
d7f9754393 |
feat: add bounding boxes and colors widgets (CORE-292) (#12960)
## Summary
Add two reusable node widgets backed by native (non-string) values:
- Bounding boxes editor (BOUNDING_BOXES): draw, select, resize, and
label regions over an optional background image. Value is a native list
of `{ x, y, width, height, metadata }` pixel boxes; the editor works in
normalized space internally and converts at the value boundary,
rescaling when the node's width/height change.
- Colors palette (COLORS): native `string[]` of hex colors, sharing the
PaletteSwatchRow component (usePaletteSwatchRow composable).
Both reactively hide the width/height widgets while a background image
is connected by writing through the widget value store so the Vue node
re-renders.
Some design refer to KJ's node
BE: https://github.com/Comfy-Org/ComfyUI/pull/14537
Screenshot
<img width="3019" height="1470" alt="image"
src="https://github.com/user-attachments/assets/06795772-97e6-4084-9205-e370f955fb28"
/>
Co-authored-by: Alexis Rolland <alexisrolland@hotmail.com>
|
||
|
|
48a3ea0e92 |
feat: add HDR/EXR image viewer for SaveImageAdvanced outputs (#13049)
## Summary Browsers cannot render EXR/HDR in <img>, so these outputs showed as broken images. Add a full-screen three.js viewer holding a single WebGL context created on open and released on close, opened via an 'Open in HDR Viewer' action on EXR/HDR outputs in ImagePreview. The layout mirrors the 3D viewer: canvas on the left, grouped controls in a right-hand sidebar. The display pipeline (gamut -> exposure -> linear-to-sRGB -> dither -> clamp, plus clip warnings) is adapted from [HDRView](https://github.com/wkjarosz/hdrview). Source gamut is auto-detected from the EXR chromaticities attribute (Rec.709/Rec.2020) with a manual override. Inspection tools operate on the EXR float data kept on the CPU by EXRLoader: - pixel inspector: hover to read raw RGBA values and coordinates - statistics: min/max/mean/std-dev plus NaN and Inf counts - auto-exposure: set exposure so the max value maps to 1 - channel isolation: view R/G/B/A or luminance individually ## Screenshots (if applicable) https://github.com/user-attachments/assets/22b80718-4b15-41ee-86b5-8fe38a6a82e2 |
||
|
|
a8f8ba7580 |
fix: clamp sidebar tab labels to two lines with tooltip fallback (#12755)
## Summary
Sidebar tab labels no longer overflow the rail: they wrap up to 2 lines
max, then truncate with an ellipsis, with the full name always
recoverable via the hover tooltip (per design spec from Alex Tov in
FE-698).
## Changes
- **What**:
- Labels in `SidebarIcon.vue` now use `line-clamp-2` + `overflow-wrap:
break-word` + `whitespace-normal`, contained within the rail width minus
`--sidebar-padding` so text keeps breathing room from the rail border
(the base Button's `whitespace-nowrap` previously prevented any
wrapping, causing labels like "Input & Output" to be clipped on both
sides)
- Near-fit built-in labels ("Workflows", "Templates", "Shortcuts" —
wider than the floating-mode line) get soft hyphens (``) in their en
label strings, so they break cleanly as "Work-/flows" in floating mode
and render as a single unhyphenated line in connected mode (56px).
`hyphens: auto` can't do this because Chromium skips hyphenation for
capitalized words. Title/tooltip strings are untouched
- Tooltip falls back to the label when an extension registers a sidebar
tab without a tooltip, so clamped text is always recoverable on hover
## Review Focus
- Labels never bleed past the rail or get clipped by the rail's
`overflow-hidden`; long unbroken extension names (e.g.
`WASNodeSuitePreprocessors`) break mid-token across 2 lines + ellipsis,
matching the design mockup
- Soft hyphens live only in `sideToolbar.labels.*`, not in the
title/tooltip keys, so command palette / tooltip text stays clean
- No E2E regression test: the fix is pure CSS layout (line
wrapping/clamping), and per `AGENTS.md` testing guidelines we don't
write tests that depend on non-behavioral styling. The one behavioral
change (tooltip falls back to label) is covered by a unit test in
`SidebarIcon.test.ts`
Fixes
[FE-698](https://linear.app/comfyorg/issue/FE-698/bug-input-and-outputs-text-not-wrapping-in-left-sidebar)
---------
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> |
||
|
|
a95dab2f59 |
Update allowlist in CLA workflow (#13091)
Update `allowlist` in CLA workflow to add [actions-user](https://github.com/actions-user) |
||
|
|
5f90bacb73 |
ci: add CLA Assistant workflow (#13058)
Adds the CLA Assistant GitHub Actions workflow at `.github/workflows/cla.yml`, copied from https://github.com/Comfy-Org/comfy-cla/blob/main/.github/workflows/cla.yml --------- Co-authored-by: GitHub Action <action@github.com> |
||
|
|
84319bea13 |
refactor: drop redundant isCloud guards around telemetry calls (#13082)
## Summary
Remove redundant `if (isCloud)` guards around `useTelemetry()?.x()`
calls. `useTelemetry()` already returns `null` in OSS builds, so the
optional-chain calls no-op there — the guards only duplicated that
central contract.
## Changes
- **What**: Drop the `isCloud` guard wrapping telemetry calls across 9
files and remove the 5 now-unused `isCloud` imports (pure dedent —
implementations unchanged). Add two-path (cloud + OSS) characterization
tests for the two previously-uncovered composables
(`useTemplateWorkflows`, `useSubscriptionActions`).
## e2e
In local/OSS mode, useTelemetry() returns null, so no telemetry-related
behavior occurs, and the workflow loads as expected. There are no
local/OSS flow regressions for the exact template workflow paths touched
by the branch.
| before | after |
| -- | -- |
| <img width="1280" height="800" alt="before-01-templates-open"
src="https://github.com/user-attachments/assets/1cccc686-4e3a-4cf0-a578-a653a1383e3c"
/> | <img width="1280" height="800" alt="after-01-templates-open"
src="https://github.com/user-attachments/assets/ff834a58-4375-432a-8cc1-6e04ceeece77"
/> |
| <img width="1280" height="800" alt="before-02-template-loaded"
src="https://github.com/user-attachments/assets/1abd301b-d66d-4819-a0f3-9dff1a1e23b5"
/> | <img width="1280" height="800" alt="after-02-template-loaded"
src="https://github.com/user-attachments/assets/9fbb6903-c085-4744-b683-39b01680c654"
/> |
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Behavior is intended to be unchanged: OSS still no-ops via null
telemetry. Router page-view tracking may run in more build contexts but
remains guarded by optional chaining.
>
> **Overview**
> Removes duplicate **`if (isCloud)`** wrappers around
**`useTelemetry()?.…()`** across onboarding, auth, templates,
subscription UI, and routing. Call sites now rely on
**`useTelemetry()`** returning **`null`** in OSS (optional chaining
stays a no-op there), and several unused **`isCloud`** imports are
dropped.
>
> **`trackPageView`** in the router no longer bails early on cloud-only
or **`window`** checks; it always invokes
**`useTelemetry()?.trackPageView(...)`** on navigation.
>
> Adds characterization tests for **`useTemplateWorkflows`** and
**`useSubscriptionActions`** that assert telemetry fires when the mock
dispatcher is registered and does not when the mock simulates OSS
(**`useTelemetry()` → null**).
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
|
||
|
|
f076106ca5 |
fix: expand grouped assets when downloading multi-selection on OSS backend (#13079)
*PR Created by the Glary-Bot Agent* --- ## Summary When using the ComfyUI (non-cloud) backend, selecting a grouped asset (`outputCount > 1`) and clicking Download only downloaded the cover/preview image instead of every output in the group. The cloud backend already handles this correctly by exporting a ZIP server-side. The OSS path called `downloadFile(asset.preview_url, ...)` once per selected `AssetItem` and never expanded grouped assets into their individual outputs. ## Fix In `useMediaAssetActions.downloadAssets`, when any selected asset has `outputCount > 1` and we're on the OSS path, resolve each grouped asset into its individual outputs via the existing `resolveOutputAssetItems` utility and trigger one direct download per file. Non-grouped selections keep the original single-shot behaviour. After expansion the file list is deduplicated by `AssetItem.id` so a user who selects both an expanded stack parent and one of its children does not download the child twice. The success toast now reflects the actual number of files downloaded. - Single asset, single output → unchanged (1 download). - Multi-select of single-output assets → unchanged (N downloads). - Any selection containing a grouped asset → expanded via `resolveOutputAssetItems` (same code path the cloud ZIP and stack-expansion UI use). If resolution returns nothing, falls back to the preview download so the user still gets something. - Grouped parent + one of its expanded children selected → deduped, no double download. ## Tests Added unit tests in `useMediaAssetActions.test.ts` for the OSS path: - Expands a grouped asset into individual downloads. - Mixes grouped and single-output assets in one selection. - Falls back to the original asset when `resolveOutputAssetItems` returns empty. - Does not call `resolveOutputAssetItems` when no grouped assets are selected. - Deduplicates downloads when an expanded child is also selected alongside its parent. - Shows an error toast when resolution rejects. All 40 tests in the file pass; all 508 tests under `src/platform/assets` pass. `pnpm typecheck`, `pnpm exec eslint`, `pnpm exec oxfmt --check` all clean. ## Manual verification Tested against a `master` ComfyUI instance with default settings. Not tested against cloud - feature is gated to non cloud Performed by @synap5e <img width="448" height="618" alt="image" src="https://github.com/user-attachments/assets/daf32fa0-c5ec-47ca-bab3-d5ea3fb3d7cc" /> https://github.com/user-attachments/assets/a87ae1aa-836f-4cbc-9ef7-a35ed4f94ee7 https://github.com/user-attachments/assets/49d833bf-7b4e-4c53-b0d5-f16ff2108185 --------- Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com> |
||
|
|
d7fa853c06 |
[chore] Update Comfy Registry API types from comfy-api@8d142eb (#13080)
Regenerates `packages/registry-types/src/comfyRegistryTypes.ts` from the current comfy-api OpenAPI spec via `openapi-typescript`. The FE registry types were last regenerated in May; this brings them up to date. Notable addition consumed by #12924: the `createCustomer` request body (`CreateCustomerRequest` with an optional `turnstile_token`). `pnpm typecheck` passes against the regenerated types. |
||
|
|
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>
|
||
|
|
e14b5c6f3f |
feat: wire /api/billing/events into usage history behind teamWorkspacesEnabled (#12955)
## Summary
Wire the previously-dead `workspaceApi.getBillingEvents` (`GET
/api/billing/events`) into the existing usage/activity table behind
`teamWorkspacesEnabled`, so billing history can converge onto the
unified cloud feed (FE-969, "wire `GET /api/billing/events`" half).
## Changes
- **What**: `UsageLogsTable` sources events from
`workspaceApi.getBillingEvents` when `teamWorkspacesEnabled` is on, else
the legacy `customerEventsService` (`/customers/events`) — no change for
flag-off (legacy/personal). The two feeds share an identical response
envelope (`{total, events, page, limit, totalPages}`) and event object
(`{event_type, event_id, params, createdAt}`), so the table view is
reused unchanged (no design change). `topupTracker` now also recognizes
`topup_completed` so credit top-up telemetry works on the unified feed.
- **Breaking**: none (flag-gated; legacy path retained).
## Review Focus
This is a **draft** — it intentionally does NOT retire
`customerEventsService` yet. Two BE confirmations gate the rest:
**1. Does the store actually accumulate events?** For a personal
workspace today, `GET /api/billing/events` appears to hold only
`gpu_usage` (written directly by inference per `workspace_id`).
Webhook-sourced events (topup/subscription/invoice/credit) are dropped
unless the workspace is provisioned in the cloud billing system —
`GetWorkspaceBy{Stripe,Metronome}CustomerID` → "Not our customer,
ignoring". That provisioning is the BE-1047 cutover. Please confirm
which `event_type`s land in the store per env (e.g. pr-4359), personal
vs team.
**2. Are the incompatible fields compatible (or can they be made so)?**
The feeds use different vocabularies / `params` shapes:
- legacy: `credit_added` / `account_created` / `api_usage_*`, params
`{amount, api_name, model}`
- unified: `topup_completed` / `gpu_usage` / `api_node_usage` /
`invoice_*` / `subscription_activated` / `seat_*`, params e.g.
`gpu_usage {gpu_seconds, gpu_type, ...}`
The table degrades gracefully (badge falls back to the raw `event_type`;
the generic params tooltip still renders), but per-type labels/severity
and the details column need the confirmed `params` shapes before full
convergence + `customerEventsService` retirement.
Notes:
- Surfacing: on cloud with `subscription_required`, the Credits panel
hosting `UsageLogsTable` is hidden when `teamWorkspacesEnabled` is on
(team users see the workspace Plan & Credits panel, which has no
activity table). So this is plumbing; an in-app team history surface is
a separate design item.
- Full retirement of `customerEventsService` should land with the
BE-1047 cutover.
Local gates: unit tests (topupTracker + UsageLogsTable) pass, typecheck
/ oxlint / oxfmt clean.
---------
Co-authored-by: dante01yoon <dante01yoon@naver.com>
|
||
|
|
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> |
||
|
|
a58f927871 |
1.47.3 (#12976)
Patch version increment to 1.47.3 **Base branch:** `main` --------- Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com> Co-authored-by: github-actions <github-actions@github.com>v1.47.3 |
||
|
|
c304f206a6 |
fix(ci): strip served bundle scripts from e2e coverage before genhtml (#13077)
## Problem The **CI: E2E Coverage** `merge` job fails in the *Generate HTML coverage report* step ([example run](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/28051468752)): ``` genhtml: ERROR: localhost-8188/assets/nodeDefs-BNhq_6cm.js is not readable or doesn't exist. ##[error]Process completed with exit code 1. ``` V8/Playwright coverage records scripts the test server serves at `localhost-8188/assets/*.js` (built bundles), which are not source files on disk. `genhtml` aborts on the missing source even with `--ignore-errors source`, so the whole job fails. ## Change Add a *Strip non-source entries from coverage* step that runs `lcov --remove '*localhost-8188*'` on the merged lcov. It is placed **after** the data-loss validation (so the merged-vs-shard integrity check stays consistent) and **before** the Codecov upload and genhtml — which also makes the Codecov report more accurate, since those served bundles aren't repo source. ## Validation - YAML parses cleanly; pre-commit lint/format/typecheck pass. - The removed paths are non-source served assets only; real `src/**` entries (absolute paths under the repo workspace) are untouched by the `*localhost-8188*` pattern. - `--ignore-errors unused` guards against runs where no such entries exist. |
||
|
|
75ea646090 |
chore: disable CodeRabbit docstring coverage pre-merge check (#13027)
## Summary
The **Docstring Coverage** pre-merge check is currently blocking merges.
It is not defined in our `.coderabbit.yaml` — it is enabled via
CodeRabbit **organization-level settings** (this repo has "use
organization settings" toggled on, but the repo config file overrides
specific keys).
This PR explicitly opts the repo out of that check by setting
`reviews.pre_merge_checks.docstrings.mode: 'off'`. A repo-level
`.coderabbit.yaml` takes precedence over org settings, so this disables
the check **only for this repo** — other org repos that want it keep it.
```yaml
reviews:
pre_merge_checks:
docstrings:
mode: 'off'
```
## Why
- Docstring coverage is a poor fit for this TS/Vue frontend codebase and
was added at the org level, not chosen here.
- It's a blocking gate with no repo-level config, so it can't currently
be tuned from the dashboard without affecting other repos.
## References
- [Built-in pre-merge
checks](https://docs.coderabbit.ai/pr-reviews/pre-merge-checks)
- [Configuration
reference](https://docs.coderabbit.ai/reference/configuration)
- Prior art:
[crossplane/.coderabbit.yaml](https://github.com/crossplane/crossplane/blob/main/.coderabbit.yaml)
disables docstrings the same way
|
||
|
|
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 |
||
|
|
282b8cf819 |
fix(auth): show a clear message when sign-up is rejected by the backend (#13070)
## Problem
When the auth backend rejects a sign-up, the Firebase client SDK
surfaces the rejection wrapped in a generic error code (e.g.
`auth/internal-error`). `reportError` maps only by `error.code` →
`auth.errors.${code}`, so this case falls through to the generic
*"Something went wrong while signing you in. Please try again."* toast —
giving the user no actionable guidance.
## Change
- `useAuthActions.ts` — add a branch in `reportError` that matches a
stable `signup_blocked` token in `error.message` (matched
**case-insensitively** on the message, not `error.code`, which the SDK
wraps inconsistently across versions) and shows a dedicated message.
- `locales/en/main.json` — new `auth.errors.signupBlocked` string. Other
locales inherit it via `fallbackLocale: 'en'`.
- `useAuthActions.test.ts` — unit tests asserting the token path wins
over the generic fallback, and that the match is case-insensitive.
## Validation
- `vitest run src/composables/auth` — 12/12 pass (incl. token +
case-insensitive tests)
- `vue-tsc --noEmit` — clean
## Why no E2E (Playwright) test
This branch is reachable only when the **auth backend actively rejects a
Google-Workspace-SSO sign-up** (a server-side blocking decision) and the
Firebase SDK returns an error whose message carries the `signup_blocked`
token. Reproducing that in the browser E2E harness would require driving
a real federated-SSO sign-up against a backend configured to reject the
specific account — state the FE test environment cannot set up
deterministically. The logic under test is pure error-mapping with no
UI/navigation surface beyond the existing toast, so it is fully covered
by the unit tests above. The end-to-end path will instead be confirmed
via a one-time staging repro (see below).
## Draft — pending confirmation
Kept as **draft** until we confirm, via a staging repro, the exact error
shape the client receives for this rejection (the message-token match is
the robust hedge regardless of `error.code`, but a live repro makes it
deterministic). The backend that emits the `signup_blocked` token is a
separate change.
|
||
|
|
70bc8dc6e6 |
fix(api): isolate event-listener errors from global telemetry (#12740)
## ELI-5 Custom nodes can hook into app events (like "this node finished running"). If one of those hooks crashes, the browser shouts the crash into our global error channel — and our monitoring logs it as an app error, on every single execution. One sloppy third-party node can bury the real errors in thousands of copies of its own bug. This wraps each hook so if it crashes, we log a quiet warning for the node author instead of screaming it into the error dashboard. ## What `ComfyApi` extends `EventTarget`. When a listener throws, native `dispatchEvent` reports the exception to the **global** error handler (`window.onerror` / `reportError`) and continues. Our error monitoring (Datadog RUM) collects those as unhandled errors — so a single misbehaving custom-node listener (e.g. `comfyui-enricos-nodes` reading `.type` on an undefined message inside an `executed` handler) produces thousands of non-actionable error events. This wraps listeners registered via `addEventListener` / `addCustomEventListener` in a try/catch: - The wrapper is stored in a `WeakMap<original, wrapped>`, so `removeEventListener` / `removeCustomEventListener` still match the installed wrapper. (GC'd weakly.) - Caught errors are logged at **`console.warn`**, not `console.error` — RUM collects `console.error` by default, which would just relabel the same noise. Native `EventTarget` already isolates listeners from one another, so this does **not** change dispatch/continuation behaviour — it only changes where a thrown error goes (a warn in the console, visible to node authors, instead of an unhandled error in global telemetry). ## Test plan - [x] New `api.eventListeners.test.ts`: a throwing listener doesn't abort dispatch to other listeners; errors log at `warn` not `error`; `removeEventListener` still removes a guarded listener. (3 tests) - [x] Existing `api.featureFlags.test.ts` (15) and `changeTracker.test.ts` (16) still pass — 34 total green. - [x] oxfmt + oxlint clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: GitHub Action <action@github.com> |
||
|
|
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 |
||
|
|
49a7b7b558 |
feat(billing): UnifiedPricingTable — one table, personal/team plan toggle (B4 / FE-934) (#12666)
## What **B4 (FE-934): `UnifiedPricingTable`** — one pricing table for the **Jun-5 model** (typeless workspace; personal/team is a **plan**, shown as a Gamma-style **plan toggle** on one workspace), per **DES-197**. A new, flag-gated component that will replace the two legacy tables at cutover (strangler). ### video https://github.com/user-attachments/assets/82b704a4-101e-4609-8ff5-06b7cf7f9cd7 > **Stacked on #12644 (FE-935 `CreditSlider`).** Base is the slider branch — retarget to `main` once #12644 merges. The slider commits show in the diff until then. ## Changes - **`UnifiedPricingTable.vue`** — plan toggle (personal/team, flag-gated on `teamWorkspacesEnabled`); personal tier cards (facade `plans` + `TIER_PRICING` fallback, billing-cycle toggle); team column hosting `<CreditSlider>`; Enterprise card. Reuses `useBillingContext`; emits `subscribe`/`resubscribe` (personal) + `subscribeTeam` (team). - **`SubscriptionRequiredDialogContentUnified.vue`** — host; wires personal checkout through `useSubscriptionCheckout` (full flow incl. preview/transition + **new `'success'` step**); team checkout stubbed. - **Confirm/success screens (DES 3084-15873)** — aligned the reused `SubscriptionAddPaymentPreviewWorkspace` / `SubscriptionTransitionPreviewWorkspace` to DES-197 (drop `/member`, `comfy--credits` icon, plan-specific CTAs **"Subscribe to {plan}" / "Switch to {plan}"**); added **`SubscriptionSuccessWorkspace.vue`** ("You're all set") as the `'success'` checkout step. - **`showPricingTable`** — renders the unified host when `teamWorkspacesEnabled`, legacy `PricingTable.vue` for flag-off. (The old workspace-variant fork is removed; `PricingTableWorkspace.vue` becomes unused on flag-on → deleted at cutover.) - **i18n** — `subscription.planScope` / `teamPlan` / `enterprise` + `subscription.preview.{subscribeToPlan,switchToPlan}` + `subscription.success.*` keys. ## Strategy (new component + cutover — per FE-934) Build new, retire old at cutover (a later single PR deletes `PricingTable.vue` + `PricingTableWorkspace.vue` + the legacy dialog host + the dispatch fork; `TIER_PRICING` kept only as the flag-off/OSS fallback). Avoids half-migrated conditional cruft in the live tables. ## Screenshots Pricing captured live on the authenticated cloud-prod session (`local.comfy.org`, flag on). Confirm/success captured in Storybook (prop-driven) — a **personal** workspace can't reach these live until **B1/FE-966** flips its billing path off the legacy `/customers/*` adapter (whose `plans` is always empty); the screens themselves are unchanged regardless of source. **Pricing table** | Personal | Team | |---|---| | <img width="420" alt="pricing-personal" src="https://github.com/user-attachments/assets/2be3b8bc-ac54-41db-8c21-5c950d3e7338" /> | <img width="420" alt="pricing-team" src="https://github.com/user-attachments/assets/c4078eb4-ee7d-42f6-bcc3-375686ab7f1e" /> | **Confirm your payment / plan change** | New subscription | Plan change (Pro → Creator) | |---|---| | <img width="380" alt="confirm-new-subscription" src="https://github.com/user-attachments/assets/e371c744-dc64-43e8-b977-73f9f99f85bc" /> | <img width="380" alt="confirm-plan-change" src="https://github.com/user-attachments/assets/b1ee5ab3-c572-4c40-9b70-f078d66b78f4" /> | **You're all set (success)** <img width="380" alt="success-all-set" src="https://github.com/user-attachments/assets/8ec9e90f-8ba4-4cb0-81a7-6b4316e0c19e" /> ## ⚠️ BE-blocked (deferred) - **Team checkout**: the slider stop → plan-slug / subscribe-request shape is undefined (doc **Open Q#2** / "Team-slider contract", **BE-1254**). `subscribeTeam` is stubbed (toast "coming soon") until BE provides it. - **Live discount data**: stops come from the hardcoded **DES-197 fallback** (`teamPlanCreditStops.ts`) until `GET /api/billing/plans` carries them. - **Confirm "credits you'll get right away" line** + **async-success routing** (Stripe-tab/`pending_payment` → in-dialog success) deferred — see ticket notes; credits-cents unit is the open BE-1254 question. ## Verification - `vue-tsc --noEmit`: clean (pre-commit). - `oxlint`/`eslint`/`stylelint`/`oxfmt`: pass (pre-commit). - `vitest` `useSubscriptionDialog.test.ts` (11) + `useSubscriptionCheckout.test.ts` (17) + new `SubscriptionSuccessWorkspace.test.ts` (2): pass. - Personal checkout reuses the existing `useSubscriptionCheckout` flow; subscribed → new `'success'` step. ## Not in scope - Pixel-finalizing vs DES-197 (visual reference = Alex's **reference-only** #12042); personal-card detail refinement. - Settings / Misc-UX (FE-768/770); team-checkout wiring (BE contract). Design: **DES-197** / **3084-15873**. Survey: *FE Billing API Divergence* (B4 / P1 / P2 / P4). --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com> |
||
|
|
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` |