mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-09 08:38:13 +00:00
bdf47aebca4fa187f5bdaf2a9c90a0b3a7e2ae1f
1227 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c8692de343 | [automated] Update test expectations | ||
|
|
e38dca7f5f | Revert graph.spec.ts change | ||
|
|
d831b1de54 |
fix: use uuid subgraph ids in nested promoted model asset
The nested promoted-missing-model test asset used readable slug ids for its subgraph definitions. createNodeLocatorId only accepts UUID subgraph ids, so getLiveWidget could not resolve the interior nodes, and the stale promoted combobox never rendered disabled. Real subgraphs always get uuid ids; the asset now matches, which is the actual fix for the interior disabled-state assertion. Revert the useProcessedWidgets disabled-derivation change: with valid asset ids the original logic is correct, so the refactor was unnecessary. |
||
|
|
4fb92e0bf6 |
fix: derive vue widget disabled state from live input link
Query the live node's widget input slot (getSlotFromWidget) as the single
source of truth for whether a widget is linked/disabled, matching
LGraphNode.updateComputedDisabled. This replaces the buildSlotMetadata
snapshot derivation, which missed promoted interior subgraph widgets whose
input carries a link to the subgraph input node, leaving them enabled.
Also drop the runtime import('/src/...') setup from the legacy app-mode
spec (only resolvable against the dev server, not the CI dist build) in
favor of the native devtools widget plus enterAppModeWithInputs.
|
||
|
|
b6ebd544c7 | fix: resolve vue widget rendering regressions | ||
|
|
aa3d192636 | refactor: store widget render state separately | ||
|
|
5b98697bcf | refactor: store widget render state separately | ||
|
|
b51ea29074 |
test: clean up TemplateHelper route mocks (#13019)
## Summary Follow-up draft PR for the CodeRabbit issues created from the #12999 review. This keeps the original stabilization PR merged as-is and moves the non-functional TemplateHelper cleanup into its own small branch. ## Changes - Extracted TemplateHelper route patterns into named module-scope constants. - Normalized the TemplateHelper route patterns to anchored regexes with optional query-string handling. - Extracted `mockCustomTemplates()` from `mockIndex()` and made `mock()` register custom templates, core index, and thumbnails together. - Added a private `registerRoute()` helper so every mocked route is registered for teardown consistently. - Simplified the fixed empty custom-template response to `body: '{}'`. - Updated the cloud template filtering spec to use `templateApi.mock()` instead of manually combining thumbnail and index mocks. ## Issues - Closes #13014 - Closes #13016 - Closes #13017 - Closes #13018 - Related #13015: this PR normalizes the TemplateHelper route patterns only. The broader fixture-wide route pattern convention cleanup remains intentionally separate. ## Validation - `pnpm exec oxfmt --check browser_tests/fixtures/helpers/TemplateHelper.ts browser_tests/tests/templateFilteringCount.spec.ts` - `pnpm exec eslint browser_tests/fixtures/helpers/TemplateHelper.ts browser_tests/tests/templateFilteringCount.spec.ts` - `pnpm typecheck:browser` - Pre-commit hook also ran `oxfmt`, `oxlint`, `eslint`, `pnpm typecheck`, and `pnpm typecheck:browser` successfully. Note: I attempted the targeted cloud Playwright spec locally with `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5173 pnpm exec playwright test browser_tests/tests/templateFilteringCount.spec.ts --project=cloud`, but the local 5173 app was not running with the cloud distribution configuration, so the distribution-filter assertions failed in the expected local/cloud mismatch way. This should be verified by CI's cloud project. |
||
|
|
9d5719871a |
Compact vue nodes (#12886)
Updates vue nodes to be compact. This PR does modify the sizing of the asset dropdown (as used on nodes like "Load Image"). There are outstanding concerns about the visibility of the upload button and ongoing work to address this. | Before | After | | ------ | ----- | | <img width="360" alt="before" src="https://github.com/user-attachments/assets/5c866d6f-d83e-40e1-9d87-17b990d94e04"/> | <img width="360" alt="after" src="https://github.com/user-attachments/assets/2a809e90-13aa-4f95-8b73-3f20b02fd9a1" />| Subsumes #12678 --------- Co-authored-by: Alex <alex@Alexs-MacBook-Pro.local> Co-authored-by: github-actions <github-actions@github.com> |
||
|
|
d6c582c399 |
feat(billing): gate consolidated billing behind consolidated_billing_enabled flag (#13359)
## Summary Shields personal-workspace billing code paths behind the new `consolidated_billing_enabled` feature flag so they fall back to the **legacy** billing flow while the flag is `false`. Team workspaces are unaffected and continue to use the workspace-scoped billing flow. ## Changes - Add `consolidatedBillingEnabled` to `useFeatureFlags` (reads the `consolidated_billing_enabled` server flag / remote config, defaults to `false`) and to the `RemoteConfig` type. - New `useBillingRouting` composable — a single source of truth for whether the active workspace uses the workspace vs. legacy billing flow: - team workspaces disabled → legacy - personal workspace + consolidated billing off/missing → legacy - personal workspace + consolidated billing on → workspace - team workspace → workspace - workspace not loaded yet → legacy - Route `useBillingContext` and the affected UI sites (`SubscriptionPanel`, `useSubscriptionDialog`, `UsageLogsTable`, `TopUpCreditsDialogContentLegacy`) through `useBillingRouting` instead of keying on `teamWorkspacesEnabled` directly. - Update the storybook `useFeatureFlags` mock to stay in sync. ## Testing - `pnpm test:unit` for `useBillingRouting`, `useBillingContext`, `useSubscriptionDialog`, and `UsageLogsTable` (new + updated coverage for the routing matrix). Remaining quality gates (`typecheck`, `lint`) are being verified in CI. ## Related Requires the backend PR that adds the `consolidated_billing_enabled` flag to `/api/features`. --------- Co-authored-by: Amp <amp@ampcode.com> |
||
|
|
9e5fb67b76 |
Show app mode run validation warning (#12557)
## Summary Adds an app mode validation warning so users can see when a workflow has errors before running and jump directly back to graph mode to review them. ## Changes - **What**: Adds a reusable app mode warning banner above the Run button when the execution error store reports workflow errors, including validation and missing asset states. - **What**: Reuses the existing graph-error navigation flow so the warning action switches out of app mode and opens the Errors panel in graph mode. - **What**: Updates the app mode Run button icon and accessible label in the warning state while keeping the Run action non-blocking. - **What**: Adds unit coverage for the warning render/accessibility state and an E2E flow that triggers a validation failure, dismisses the overlay, and opens graph errors from the app mode warning. - **Breaking**: None. - **Dependencies**: None. ## Review Focus The warning intentionally mirrors graph mode behavior: it surfaces the error state but does not prevent the user from clicking Run. This avoids turning display-level validation signals into hard execution blockers. The warning is driven by the existing `hasAnyError` aggregate, so missing nodes, missing models, and missing media are included alongside prompt/node/execution errors. ## Tests - `pnpm format` - `pnpm lint` - `pnpm typecheck` - `pnpm test:unit` - `pnpm knip` - `pnpm test:browser:local browser_tests/tests/appModeValidationWarning.spec.ts` ## Screenshots <img width="461" height="994" alt="스크린샷 2026-06-25 오후 7 00 55" src="https://github.com/user-attachments/assets/f8fc20bf-d572-46b5-9fa4-312e7c4c8076" /> |
||
|
|
b132abc64a |
fix: center video asset in the Load Video node preview (#13172)
## Summary Center the Load Video node preview and keep the node from auto-resizing on clip load, so the video letterboxes in place like the Load Image node (FE-1092). ## Changes - **What**: Makes the video stay centered horizontally and vertically - Playwright browser test expectations updated at run https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/28267324092 ## Review Focus - Ensure that patterns and idioms were followed ## Screenshots (if applicable) Horizontally centered <img width="902" height="392" alt="image" src="https://github.com/user-attachments/assets/a9fbec56-1613-44b4-a423-9f709a246c63" /> Vertically centered <img width="220" height="1124" alt="image" src="https://github.com/user-attachments/assets/5497f39b-2ea2-4247-a087-a7d89768b4ce" /> Full aspect ratio <img width="433" height="672" alt="image" src="https://github.com/user-attachments/assets/d579fb14-34c6-4963-abc9-034611232d3d" /> Minimum size <img width="217" height="376" alt="image" src="https://github.com/user-attachments/assets/80df0411-3ff1-4050-ac8e-761b7b8a7c40" /> Preview centering is asserted in `browser_tests/tests/vueNodes/videoPreview.spec.ts`. --------- Co-authored-by: github-actions <github-actions@github.com> |
||
|
|
4a2393be48 |
chore: drop unnecessary exports on file-local types to satisfy knip (#13204)
Current `main` **fails a fresh `knip` run** with 13 unused exported types (exit 1). They're invisible on main because lint/knip only runs on `pull_request`/`merge_group`, never on push to main — so merge skew (one PR adds an export used by file X; a later PR removes X's usage) accumulates latent failures that ambush backport branches (e.g. #13163, #13162). Each of the 13 is `export`ed but referenced only within its own file (verified 0 importers; ≥2 in-file uses, so not dead code). Fix: drop the redundant `export`. Types cleaned: `VideoSource`, `ObjectInfoResponse`, `PromotedMissingModelWorkflow`, `PixelReadout`, `ResizeDirection`, `ResizeHandle`, `RunButtonTelemetryOptions`, `ResolvedModelNode`, `AccountPreconditionContext`, `SubscriptionDialogOptions`, `MonthlyCreditsUsage`, `MissingMediaReference`, `ResolvedHostWidget`. Reviewer note: `ResolvedHostWidget` and `ResolvedModelNode` sit under `renderer/extensions`/`platform/assets`; no in-repo importers, but if either is intended as published/extension-facing API, prefer a knip `entry`/`ignore` over un-exporting — flag in review and I'll adjust. After fresh `knip`: **0 unused exported types**. Supersedes #13179 (fixed only `AccountPreconditionContext`). Pairs with the push-gate workflow #13203 — merge this first so that gate is green on main. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c16f10b49e |
Long workflow name cleanup (#13180)
When loading a workflow by dragging and dropping an output from the assets sidebar, the very long and unhelpful url would be used as the workflow name. This is fixed by instead using the asset display name | Before | After | | ------ | ----- | | <img width="360" alt="before" src="https://github.com/user-attachments/assets/5c68ae48-1fa6-40e1-b2fb-6188ccd60391"/> | <img width="360" alt="after" src="https://github.com/user-attachments/assets/29770c35-da48-4be9-943e-8ee69eb25e6a" />| Additionally, a max width is added to the breadcrumb items to avoid extremely long names. | Before | After | | ------ | ----- | | <img width="360" alt="before" src="https://github.com/user-attachments/assets/508155ec-81d7-4ca5-8910-f42a70c9cb4b"/> | <img width="360" alt="after" src="https://github.com/user-attachments/assets/d335ceb7-bfeb-481f-a132-c700e017ee0c" />| |
||
|
|
b4ae6344d7 |
Brand local node IDs (#13085)
## Summary Adds a branded local `NodeId` helper and starts separating local node identity from serialized workflow IDs. ## Changes - **What**: Adds central `NodeId` parsing/branding helpers, migrates nearby widget identity types, keeps queue results at the serialized boundary, and removes misleading workflow `NodeId` usage from execution error maps. ## Review Focus Check that the first migration slice keeps serialized/API IDs as raw `number | string` while local UI/store IDs use the branded string type. ## Caveat `SUBGRAPH_INPUT_ID` and `SUBGRAPH_OUTPUT_ID` are now branded local `NodeId` string values internally instead of numeric sentinels. Reviewers should double-check extension compatibility for callers that import `Constants` and compare those values numerically. ## Screenshots (if applicable) N/A --------- Co-authored-by: GitHub Action <action@github.com> Co-authored-by: AustinMroz <austin@comfy.org> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
caabebe145 |
Redesign missing model detection contract for promoted subgraph widgets (#13059)
## Summary Redesign missing-model detection for ADR 0009 promoted subgraph widgets so candidates are created from the widget value the user can actually edit, while still using the concrete interior widget as the schema/options source. ## Why This PR Exists This PR comes from the follow-up missing-model detection work for the ADR 0009 / 1.46 subgraph widget changes introduced by [#12197](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12197). [#12197](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12197) intentionally changed promoted subgraph widgets to be represented through subgraph input links. After that change, the promoted widget on the host `SubgraphNode` is the editable value owner, and linked interior widgets are no longer guaranteed to mirror that value. The old missing-model contract still treated the concrete interior node widget as the effective source of truth in subgraph cases: - recursive scans entered the subgraph and scanned the interior widget value; - candidates were keyed by the interior node/widget identity; - the parent subgraph host mostly received propagated highlight/navigation behavior; - subgraph container widgets were not treated as first-class candidate sources. That contract breaks after ADR 0009. A user can resolve a missing model by changing the promoted host widget to an installed model, while the linked interior widget can still hold the old stale value. If detection keeps scanning the linked interior value, entering the subgraph or reloading the workflow can re-create a false missing-model error that no longer corresponds to the value the user can edit. ADR 0009 also means the same subgraph definition can be reused by multiple `SubgraphNode` hosts. Once missing-model detection moves from the interior definition widget to the promoted host widget, the selected value is no longer a property of the shared definition alone. It is a property of a specific host instance. That makes the old interior-node identity insufficient for mode changes, removal handling, and re-scan behavior: a single interior leaf definition can be reachable through multiple host execution paths, and only the affected host path should add, remove, or restore a candidate. This PR also builds on [#12990](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12990), which narrowed workflow-level `models[]` and embedded model data to metadata enrichment only. Together, the intended boundary is: - live widgets create missing-model candidates; - workflow/root-level `models[]` and node metadata only enrich candidates that already came from a live widget; - promoted widget values are read from the editable host widget, not inferred from stale interior `widgets_values`. ## Changes - **What**: - Introduces promoted-widget scan targets that split: - host promoted widget value and candidate identity; - concrete leaf widget/node definition data. - Scans the outermost unlinked promoted widget on a `SubgraphNode` host as the selected value owner. - Skips linked interior widgets as candidate sources, preventing stale linked widget values from producing duplicate or false missing-model candidates. - Resolves the concrete leaf widget for combo options, asset-widget support, node type, directory lookup, and embedded metadata enrichment. - Keys promoted missing-model candidates by the host execution id and host promoted widget name. - Adds `sourceExecutionId` to promoted candidates so liveness still follows the concrete source execution path, including nested inactive subgraph containers. - Uses source-scope activity for pipeline filtering and async verification, while keeping highlight/store/clearing identity host-keyed. - Removes host-keyed promoted candidates when their source execution scope is removed or bypassed. - Re-scans ancestor subgraph hosts when an interior source path is un-bypassed, so host-keyed promoted errors can reappear correctly. - Handles shared subgraph definitions by deriving promoted source paths from the concrete host instance path, rather than treating the shared definition node id as globally unique. - Shares promoted source resolution between Vue node processing and the right-side panel to avoid drift. - Aligns missing-model clearing across Vue node widgets, legacy canvas widgets, and right-side panel Parameters/Nodes section widgets. - Adds unit coverage for scan identity, source-scope liveness, dynamic mode changes, source-scope removal, shared-definition host isolation, and right-side panel clearing. - Adds nested promoted-widget E2E coverage for OSS and Cloud flows across Vue, Parameters tab, and legacy widget surfaces. - **Breaking**: None expected. - **Dependencies**: None. ## New Detection Contract A missing-model candidate is created from an unlinked final editable value owner. That value owner can be: - a normal node model widget; or - the outermost promoted model widget displayed on a `SubgraphNode` host. For promoted widgets: - the host promoted widget supplies the selected value; - the host execution id and host widget name are the candidate identity; - the concrete leaf widget supplies definition data such as combo options and asset-browser support; - the concrete source execution path is retained as `sourceExecutionId` for liveness only; - linked interior widgets are skipped as candidate sources because their values are not authoritative when driven by a promoted input. For a nested chain: `Outer promoted widget A -> inner promoted widget B -> concrete widget C` only `A` creates the candidate. `B` and `C` are linked along the promoted-input path and are skipped as selected-value sources, while `C` still provides the concrete widget definition used to evaluate `A`. ## Shared Definition And Source-Scope Liveness ADR 0009 promoted widgets make subgraphs behave more like reusable definitions with host-owned inputs. Two host `SubgraphNode`s can point at the same interior subgraph definition while carrying different promoted widget values. In that shape, the missing-model candidate must be keyed to the editable host surface, but the activity check cannot use the host id alone. For example, if two outer hosts share the same nested subgraph definition, one host can select a valid model while the other still selects a missing model. The result should be one missing-model reference, not a single definition-level error and not two errors after one host is fixed. Likewise, bypassing or un-bypassing an interior nested container should affect only the host execution paths that actually pass through that container. This PR therefore separates two concepts: - **candidate identity**: host execution id + host promoted widget name, used for storage, highlight, navigation, and clearing; - **candidate liveness**: concrete source execution path, used for scan-time activity checks, pipeline filtering, async verification, source-scope removal, and re-exposure after mode changes. That separation is the reason this PR updates more than the scan itself. Moving the detection target to the subgraph host also requires the mode-change and removal paths to understand that a host-keyed candidate can be invalidated by a descendant source path, and can need to be restored by re-scanning an ancestor host when an interior source path becomes active again. ## Review Focus Please review the identity split carefully: - candidate/store/highlight/clearing identity should remain host-keyed for promoted widgets; - liveness should use `sourceExecutionId` when present, falling back to `nodeId` for normal candidates; - scan-time activity checks should account for the source node itself and all ancestor subgraph containers; - source-scope removal should remove host-keyed candidates whose concrete source path was removed or bypassed; - un-bypassing an interior source path should re-scan affected ancestor subgraph hosts so host-keyed candidates can reappear; - shared subgraph definitions should not merge errors across different host instances; - linked interior widgets should not produce their own missing-model candidates; - asset-browser eligibility should be resolved from the concrete leaf node type and widget name, not the synthetic subgraph host type; - right-side panel edits should clear host missing-model errors and source validation errors consistently. The E2E matrix intentionally keeps nested promoted workflows only. Nested promoted widgets cover the same editable host path as single promoted widgets while also exercising the `A -> B -> C` chain that can break source-scope liveness and re-scan behavior. The nested fixture also includes multiple host instances that share the same subgraph definition, so it verifies that fixing one host does not accidentally clear or suppress another host's missing-model candidate. Direct/single promoted behavior is still covered at the unit level. ## Non-Goals - This PR does not reintroduce workflow-level `models[]` candidate creation. - This PR does not infer selected model values from `widgets_values`. - This PR does not synchronize linked interior widget values back from promoted host widgets. - This PR does not redesign missing-media scanning; missing media still skips subgraph containers and remains keyed by concrete interior paths. The shared async post-verification active-scope filter is intentionally stricter, so a pending missing-media candidate is no longer surfaced if its own node is bypassed or removed while verification is in flight. ## Validation - `pnpm exec vitest run src/components/rightSidePanel/parameters/SectionWidgets.test.ts src/platform/missingModel/missingModelScan.test.ts src/composables/graph/useErrorClearingHooks.test.ts src/platform/missingModel/missingModelPipeline.test.ts src/platform/missingModel/missingModelStore.test.ts src/utils/graphTraversalUtil.test.ts src/composables/graph/useGraphNodeManager.test.ts src/renderer/extensions/vueNodes/composables/useProcessedWidgets.test.ts --reporter=dot` - 8 files passed, 294 tests passed. - `pnpm exec vitest run src/platform/missingModel/missingModelScan.test.ts src/core/graph/subgraph/resolveConcretePromotedWidget.test.ts src/components/rightSidePanel/parameters/SectionWidgets.test.ts` - 3 files passed, 71 tests passed. - `pnpm typecheck` - `pnpm typecheck:browser` - `pnpm format:check` - targeted ESLint for changed production/unit/E2E files - `git diff --check` - `pnpm build` - `pnpm build:cloud` - OSS affected E2E on the 8188 build: - `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:8188 pnpm exec playwright test browser_tests/tests/propertiesPanel/errorsTabModeAware.spec.ts --project=chromium --grep "Changing an OSS .*promoted|Refreshing a resolved promoted|Reloading a resolved nested"` - 5 passed. - Cloud affected E2E on the 8188 cloud build: - `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:8188 pnpm exec playwright test browser_tests/tests/propertiesPanel/errorsTabCloudMissingModels.spec.ts --project=cloud --grep "Changing a Cloud .*promoted"` - 2 passed; the Cloud legacy promoted asset-modal case still fails until [#13075](https://github.com/Comfy-Org/ComfyUI_frontend/pull/13075) is merged. - Full OSS `errorsTabModeAware.spec.ts` on the 8188 build: - 23 passed; 3 existing paste/clipboard cases failed before the promoted subgraph section with node count remaining at 1 after `clipboard.paste()`. - Commit hooks ran `oxfmt`, `oxlint`, `eslint`, `pnpm typecheck`, and browser typecheck where applicable. - Pre-push hook ran `pnpm knip --cache`. ## Screenshots Before https://github.com/user-attachments/assets/6380c1da-1d92-4b70-888e-3ade572c4b5b After https://github.com/user-attachments/assets/4cfc24d6-3dc3-4e36-9b31-72fea6b3d9d5 |
||
|
|
7376402fc6 |
Essentials tab redesign (#12744)
Subsumes #12304 Redesigns the Essentials tab to be frontend designed with more accessible icons and tighter organization. <img width="381" height="1345" alt="image" src="https://github.com/user-attachments/assets/193f7f5f-20c8-4bf0-8304-ec2c990186d0" /> --------- Co-authored-by: comfydesigner <comfydesigner@users.noreply.github.com> Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: Alexander Brown <drjkl@comfy.org> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
13b42d9b59 |
Ensure dynamic combo children cleanup state (#13073)
#12617 introduced a regression in Dynamic Combos. If two options have child widgets of the same name (such as `bit_depth` on `Save Image (Advanced)`), then widget state would be incorrectly shared between the two widgets. This is resolved by having removed widgets also delete their state. There was previous interest in having widgets of this type keep state when valid. This interest remains, but will require a more controlled intentional implementation in the future. Since the bit depth options on `Save Image (Advanced)` could potentially be expanded in the future, this PR specifically adds a new devtools node for testing with. --------- Co-authored-by: Alexander Brown <drjkl@comfy.org> |
||
|
|
7ae3ad936c |
When dragging vue nodes, also drag reroutes (#12885)
`selectedItems` was being filtered to nodes and groups. Since no special behaviour is being performed on groups, the 'move groups' code is relaxed to instead 'move all non-node selected items'. |
||
|
|
6eaad99502 |
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" />| |
||
|
|
9209a4b923 |
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" />| |
||
|
|
e3049e7c31 |
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)
|
||
|
|
67009dcda2 |
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" /> | |
||
|
|
026b2c4795 |
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> |
||
|
|
f19597ce81 |
feat(billing): deep link to open the pricing table (FE-1104) (#13001)
## Summary Adds an in-app deep link that opens the pricing table directly, for driving pilot users straight to subscribe (request from nav/Alex). Resolves [FE-1104](https://linear.app/comfyorg/issue/FE-1104). - `/?pricing=1` — on app load, open the pricing table. - `/?pricing=team` / `/?pricing=personal` — open it on the Team / Personal plan tab (via the existing `UnifiedPricingTable` `initialPlanMode`). - Gated to the **original owner** via `useWorkspaceUI().permissions.canManageSubscriptionLifecycle` (personal user, or a team workspace's original owner). A member or a promoted owner is a **silent no-op**: the app loads normally, the param is stripped, no 404 / error / toast. - Off-cloud (OSS): the loader isn't instantiated, so the param is ignored. - Survives the login redirect via the preserved-query system, same as `?invite` / `?create_workspace`. ## How Mirrors the established URL-loader pattern (`useInviteUrlLoader` / `useCreateWorkspaceUrlLoader`): - `preservedQueryNamespaces.ts` / `router.ts` — register the `pricing` namespace + tracker key. - New `usePricingTableUrlLoader.ts` — hydrate preserved query, read `pricing`, strip the param + `clearPreservedQuery` in a single replace before any await, then `await fetchMembers()` (resolves the original-owner gate; no-ops for personal) and open the table only when the gate allows. - `GraphCanvas.vue` — call the loader in `onMounted` after the create-workspace loader (cloud only; not gated on the team-workspaces flag so it also drives personal/legacy users). - `useSubscriptionDialog.ts` — new `'deep_link'` value on `SubscriptionDialogReason`. ## Telemetry Eligible opens emit the existing `subscription_required_modal_opened` PostHog event with the new `reason: 'deep_link'`. Ineligible-click bounce rate is derivable from the autocaptured pageview URL (`?pricing=…`), so no new event plumbing. ## Stacking / dependencies This feature needs two sibling stacks off `main`: - **FE-934** (`#12666`, base of this PR) — the `UnifiedPricingTable` + `showPricingTable({ planMode })`. - **FE-770** (`#12829`) — the `canManageSubscriptionLifecycle` gate. **Merged into this branch**, so the diff against the FE-934 base includes FE-770's changes until it lands. Review the single `feat(billing): deep link…` commit. Once both land on `main`, rebase onto `main` and the diff collapses to just this feature. Do not merge before FE-770 and FE-934. Post-Billing-V1 follow-up. End-state: swap the FE original-owner heuristic for the BE workspace-level `is_original_owner` flag when it lands (removes the members-fetch). ## Tests - Unit (`usePricingTableUrlLoader.test.ts`, 12 cases): opens for an original owner; `team`/`personal` tab preselect; silent no-op + param-strip for a member/promoted owner; proves the gate is read only after `fetchMembers` resolves; preserved-query restore; empty/non-string/absent/unrecognized param; members-fetch failure strips+clears without opening. - E2E (`browser_tests/tests/dialogs/pricingTableDeepLink.spec.ts`, `@cloud`, 4 cases, verified locally): personal owner opens + URL stripped; `?pricing=team` lands on the active Team tab; team original owner opens (real `is_original_owner` + email gate); team member is a silent no-op + URL stripped. - Typecheck + related unit suites (`useSubscriptionDialog`, `useWorkspaceUI`, `teamWorkspaceStore`) green. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com> Co-authored-by: jaeone94 <89377375+jaeone94@users.noreply.github.com> Co-authored-by: github-actions <github-actions@github.com> |
||
|
|
52d430d1b6 |
fix(billing): repoint direct-bypass billing consumers to the facade (B3) (FE-933) (#12643)
## What **B3 — Repoint direct-bypass billing consumers to the facade.** Billing data was read from the legacy `useSubscription` store / `authStore` directly (empty or personal-only for team workspaces) instead of the workspace-aware `useBillingContext` facade. FE-933 (parent FE-903). > **Stacked on #12622 (B2 / FE-904)** — depends on the facade `tier` / `renewalDate` fields added there. Base is the B2 branch; retarget to `main` once B2 merges. ## Repointed consumers - **T3 — `SubscribeButton.vue`**: `subscribe_clicked` telemetry `current_tier` ← facade `tier` (was wrong/empty for team users) - **T4 — `PostHogTelemetryProvider.ts`**: PostHog `subscription_tier` person property ← facade `tier` watch (tier-segmented analytics was polluted for team users) - **T5 — `FreeTierDialogContent.vue`**: next-refresh date ← facade raw ISO `renewalDate`, formatted at the display site (the line silently disappeared for team users) - **`useSubscriptionActions.handleRefresh` + `SettingDialog` credits-nav**: balance refresh ← facade `fetchBalance()` (was legacy `/customers`-only `authActions.fetchBalance`) - **`CurrentUserPopoverLegacy.vue`**: tier badge / balance / skeleton / refreshes ← facade (`tier`, `balance`, `isLoading`, `fetchStatus`, `fetchBalance`); tier name via shared `useWorkspaceTierLabel` instead of a duplicated mapping - **`PricingTable.vue`**: `isActiveSubscription` / `isFreeTier` / `tier` / yearly-vs-monthly ← facade; the billing-portal flow (`accessBillingPortal` deep-links + proration) is intentionally unchanged — facade `manageSubscription` is not behavior-identical ## Out of scope (triaged) - `TopUpCreditsDialogContentLegacy` / `SubscriptionPanelContentLegacy` / `useSubscriptionDialog` / cancellation watcher — legacy-mode-only surfaces decommissioned by B1 (FE-966); repointing is churn, and `useSubscriptionDialog` would create a legacy↔facade cycle - `LegacyCreditsPanel` / `UserCredit` — deleted/orphaned by FE-964 (#12734); its successor `CreditsPanel.vue` keeps an `authStore.lastBalanceUpdateTime` watch (no facade equivalent yet) — follow-up after FE-964 lands ## Known semantic deltas (intentional, match shipped facade consumers) - Balance-refresh failures no longer toast: legacy `authActions.fetchBalance` wrapped errors with a toast; facade `fetchBalance` rejections are void-ed, same as `CurrentUserPopoverWorkspace` / `SubscriptionPanelContentWorkspace`. Facade-level error surfacing is a follow-up. - Popover skeleton keys on facade `isLoading` (init-time) rather than per-fetch `isFetchingBalance`, matching the workspace popover. ## Tests - New behavioral coverage: FreeTier renewal-date render/disappear, popover tier badge + balance from facade, current-plan highlight from facade tier+duration, facade-vs-legacy fetchBalance tripwire, PostHog `subscription_tier` from facade tier. - Local gates clean (typecheck / lint / format / dead-code); touched unit files 71/71 pass. ## E2E coverage Browser regression tests live in the stacked #12760 (`billingFacadeConsumers.spec.ts`, `@cloud`): avatar popover tier badge + balance, and the free-tier dialog renewal-date line (T5) rendered from the facade. The team-user telemetry fixes (PostHog person property, telemetry payload) are non-UI observables covered by unit tests that mock only the facade and fail on revert. --------- Co-authored-by: Alexander Brown <drjkl@comfy.org> |
||
|
|
c406042215 |
More robust drag cleanup (#13084)
Under some circumstances, (particularly with pointerCancel events) a drag operation could end without properly being cleaned up. When this occurs, the bugged state would manifest in comical ways - Nodes would 'run away' from the cursor <img width="1024" height="1024" alt="AnimateDiff_00001" src="https://github.com/user-attachments/assets/accfeac0-ce4c-4d8a-b3b8-6b243e8d5f8d" /> - Resizing the window could cause the zombie drag to move into the autopan region which would result in nodes rapidly scrolling away. <img width="1024" height="1024" alt="AnimateDiff_00002" src="https://github.com/user-attachments/assets/e30629f4-ddea-4981-83d8-0037b3010ad5" /> This is resolved by adding more robust cleanup for canceled drag events. This PR also cleanups a sizeable chunk of dead TransformPane code which was unused. |
||
|
|
395b0a1c89 |
fix: prevent NullGraphError on subgraph node removal (#11804)
## Summary Various race conditions can cause `NullGraphError` to be thrown after removing/converting a subgraph. This fix guards at call sites and refactors to add a pre-removal phase before the graph is nulled. ## Changes - **What**: - add pre-detach event (node:before-removed) so reactive consumers can drop references before node.graph is nulled - move selection and Vue node-manager teardown to this event to eliminate stale panel/render evaluations against detached nodes - guard SubgraphNode promoted-widget paths resilient on detached access and add regression coverage - **Breaking**: <!-- Any breaking changes (if none, remove this line) --> - **Dependencies**: <!-- New dependencies (if none, remove this line) --> ## Review Focus Alternative considered approach: - Guards: Guards were treating the symptom at every caller, and new callers may appear that won't know about this edge case. Adding a new hook for consumers to drop refs is safer than trying to guard every call site - the ones that are left in are safetynets and not the primary fix. - Large scale refactor (towards ADR0008) - requires additional scaffolding to already be in place to implement effectively, this fix simply adds a new hook and isnt incompatible with the projects future goals - Defer/remove/reorder graph null - The detach was explicitly added in #8180 to ensure GC - delaying is fragile and may not resolve the issue, difficult to prove and may surface a new race condition - Make rootGraph nullable - would require 100s of references to be updated, when `NullGraphError` was added in #8180 to throw a clear message when the graph for a removed subgraph node was referenced, potentially leading to other harder to track bugs without the exception Tests: - e2e test complexity is required to prove the issue happens, patching calls to add artificial delays. This isn't great, but I could not find a reliable way to recreate otherwise, unless we are happy to drop e2e and keep only unit tests. ┆Issue is synchronized with this [Notion page](https://app.notion.com/p/PR-11804-fix-prevent-NullGraphError-on-subgraph-node-removal-3536d73d3650814e9183e17067cc0992) by [Unito](https://www.unito.io) --------- Co-authored-by: Alexander Brown <drjkl@comfy.org> Co-authored-by: DrJKL <DrJKL0424@gmail.com> |
||
|
|
b165b3f999 |
fix: focus keybindings search when opening Manage Shortcuts (FE-845) (#12709)
## Summary Opening the Keybinding panel from the **Manage Shortcuts** button now focuses the **Search Keybindings** field instead of the **Search Settings** field. ## Changes - **What**: The Settings dialog's "Search Settings" input had an unconditional `autofocus`, so opening directly to the keybinding panel always stole focus to the wrong field. Made it conditional (`:autofocus="activeCategoryKey !== 'keybinding'"`) and added `autofocus` to the keybinding panel's own search input. ## Review Focus - `autofocus` maps to the native attribute, which only fires on DOM insertion — flipping the reactive `:autofocus` while navigating between categories inside the dialog will not re-steal focus, so there is no regression for in-dialog navigation. - Added an E2E test verified in both directions: it fails on the original code (Search Settings focused) and passes with the fix (Search Keybindings focused). Fixes FE-845 Co-authored-by: Dante <bunggl@naver.com> |
||
|
|
966659b303 |
fix: bind promoted asset modals (Legacy) to host widgets (#13075)
## Summary Bind asset-browser modal selections to the widget that actually opened the modal, so promoted subgraph asset widgets commit through the host promoted widget instead of the internal source widget closure. ## Changes - **What**: Makes the asset-browser modal commit path widget-owned: after a valid selection, `openModal` writes to the widget passed into the modal and notifies that widget's callback. - **What**: Captures workflow state after a successful value-changing asset selection, because the async modal `Use` action can run after the global mouseup-based change capture has already fired. - **What**: Preserves existing asset-browser filtering by keeping `nodeTypeForBrowser` and `inputNameForBrowser` captured in the asset widget's existing modal options closure. - **What**: Avoids adding promoted-widget-specific rebinding code to `litegraphService` and avoids changing LiteGraph core widget option types. - **What**: Only runs the source widget's `onValueChange` callback when the selected widget is the original owner widget created by `createAssetWidget`. - **What**: For cloned/transient host widgets, such as promoted subgraph asset widgets, dispatches `onWidgetChanged` through the widget's owning node instead of the internal source node. - **What**: Removes the duplicate PrimitiveNode callback dispatch because the asset modal commit path now centrally notifies the selected widget callback. - **What**: Adds stable asset-browser `data-testid`s and a cloud E2E regression for legacy promoted subgraph asset selection. - **What**: Adds unit coverage for both regular asset widget commits and cloned promoted-host asset modal commits, including workflow change capture. - **Breaking**: None. - **Dependencies**: None. ## Review Focus This PR supersedes #13074. The earlier direction treated the bug as a missing callback bridge in the async asset-browser commit path, but the ownership issue is more specific: promoted subgraph asset widgets reuse modal options that were created from the deepest concrete source widget. Those options still need to carry source metadata for filtering the asset browser, but the modal's `Use` action must commit to the widget that actually opened the modal. This matters after the History ADR 0009 subgraph widget changes shipped through #12197. In the 1.46 subgraph model, promoted widget values live on the subgraph host node and are not synchronized back into the internal widget. The internal source widget remains useful as the provider of asset-browser metadata, because `SubgraphNode` already resolves nested promotions down to the final concrete widget, but it should not own the edit commit. The final patch keeps that boundary narrow: - no `IWidgetOptions` or LiteGraph core type changes; - no asset-specific promoted-widget rebinding in `litegraphService`; - no new promoted-widget traversal logic, because the existing subgraph promotion path already resolves the final concrete source widget; - the modal commit path uses the widget passed to `openModal` as the value owner; - successful async modal commits explicitly capture workflow state when the selected value changes. Please focus review on whether `createAssetWidget` now preserves regular asset widget behavior while correctly handling cloned/transient host widgets. The key distinction is that the source `onValueChange` path only runs for the original owner widget; promoted host wrappers instead rely on their callback bridge and owning node's `onWidgetChanged` hook. A review pass also found that this PR makes an existing async modal weakness more visible: asset-browser selection happens from the modal button's `click` handler, while the global change tracker also captures on `mouseup`. Depending on event ordering, the automatic capture can occur before the selection mutates the widget. This PR now captures workflow state immediately after a successful value-changing asset selection so undo/modified tracking follows the same user-visible edit. Local verification: - `pnpm exec vitest run src/platform/assets/utils/createAssetWidget.test.ts --reporter=dot` - `pnpm exec vitest run src/platform/assets/utils/createAssetWidget.test.ts --coverage --reporter=dot --coverage.reporter=text --coverage.include=src/platform/assets/utils/createAssetWidget.ts` - `pnpm exec eslint src/platform/assets/utils/createAssetWidget.ts src/platform/assets/utils/createAssetWidget.test.ts` - `pnpm typecheck` - `pnpm format:check` - `pnpm build:cloud` --------- Co-authored-by: Alexis Rolland <alexisrolland@hotmail.com> |
||
|
|
07f881fc14 |
feat: float the Media Assets bulk-selection bar (#13043)
## Summary
Reskins the Media Assets bulk-selection bar into a prominent floating
pill so bulk-download actions are no longer easy to miss (Linear
FE-989).
## Changes
- **What**: The selection bar now floats over the bottom of the panel as
an inverted rounded pill — close · "{count} selected" · download ·
divider · delete — matching the Figma/prototype spacing, radius, and
shadow. Actions are icon-only with `v-tooltip` hints; the count uses
`tabular-nums` and reflects selected assets (not total outputs).
Extracted into a presentational `MediaAssetSelectionBar.vue` with a
Storybook story, a unit test, and an e2e guard that the count is
per-asset.
## Review Focus
- The pill floats via `position: absolute` (centered, `bottom-6`,
`z-40`) in the sidebar footer slot and overlays the bottom of the grid
by design.
- Count semantics changed from total outputs to selected-asset count
(`selectedAssets.length`).
- Out of scope, deferred per FE-989: marquee / select-all, pagination,
and the favorites / tags / label controls.
## Screenshots (if applicable)
<img width="1003" height="1767" alt="image"
src="https://github.com/user-attachments/assets/3a9ef884-e7f4-4d0b-a495-194ce0860db2"
/>
<img width="643" height="581" alt="image"
src="https://github.com/user-attachments/assets/1161884f-a9c2-4a2b-a20e-33ee3f189935"
/>
<img width="664" height="222" alt="image"
src="https://github.com/user-attachments/assets/b16b083c-bfd9-452d-b508-86b3cbfa9842"
/>
<img width="649" height="265" alt="image"
src="https://github.com/user-attachments/assets/a1076e34-58c9-4e7f-89c4-b21bb3281883"
/>
<img width="559" height="205" alt="image"
src="https://github.com/user-attachments/assets/09c24140-33ce-4629-b681-233c59916043"
/>
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
|
||
|
|
065650b3bf |
fix: open Vue context menu when right-clicking a group (#12971)
## Summary Right-clicking a frame (group) now opens the new Vue context menu instead of the legacy litegraph menu, matching the three-dot menu and node right-click. ## Changes - **What**: In Nodes 2.0 mode, a group right-click is routed to the existing `showNodeOptions` flow (the same menu the three-dot button and node right-click use) instead of litegraph's `processContextMenu`. The group is selected (unless already in the selection) so the menu targets it. Nodes, the canvas background, reroutes, and legacy rendering are unchanged. ## Review Focus - App-layer wrap of `LGraphCanvas.prototype.processContextMenu`, mirroring the existing `useContextMenuTranslation` pattern: no new methods on `LGraphCanvas` (ADR 0008). - Gated on `LiteGraph.vueNodesMode`; legacy rendering keeps the old menu, consistent with legacy node right-click. - Reroute guard: right-clicking a reroute inside a group still gets the legacy "Delete Reroute" menu, not the group menu. Fixes FE-1090 ## Screenshots (if applicable) <img width="719" height="788" alt="image" src="https://github.com/user-attachments/assets/8d514c6d-b7d0-4ec1-841e-677793daf3c7" /> --------- Co-authored-by: GitHub Action <action@github.com> |
||
|
|
a670944a05 |
Fix share auth attribution gap (#13064)
## Summary Logged-out users who open a share link and then sign up/in were not attributed to the share. The `share_id` capture lived in `useSharedWorkflowUrlLoader`, which only runs after `GraphView` mounts — i.e. after the cloud auth guard has already redirected the logged-out user to login. The capture never happened, so `trackAuth` fired without a `share_id`. This moves the capture into the cloud auth guard (`router.beforeEach`), so it runs on the initial navigation before any login redirect. The `share_id` is preserved across the auth round-trip and consumed on auth completion as before. ## Changes - Capture logged-out share attribution in the router guard instead of the share loader, via a new `preserveLoggedOutShareAuthAttribution` util - Extract `isValidShareId` into the shared util and reuse it in the loader (removes the duplicated regex) - Gate capture on `isCloud` (matching the cloud-only consumption); drop the now-dead capture branch from the loader - Make the accepted share-id shape explicit: ASCII alphanumeric start, ASCII alphanumeric/`_.-` after that, max 128 chars ## Notes - Capture no-ops when `share` is absent, so param-less redirects do not clear attribution - If another valid share link is visited before auth completes, the latest valid share replaces the previous attribution - `SHARE` and `SHARE_AUTH` stay separate intentionally: `SHARE` preserves the workflow-loading query, while `SHARE_AUTH` is consumed once by auth telemetry attribution - No behavior change for logged-in users or for share-dialog open/cancel ## Testing - New unit tests for `isValidShareId` and `preserveLoggedOutShareAuthAttribution` (valid/invalid/array/logged-in/boundary cases) - Auth store tests cover `share_id` propagation + consumption across email signup/login, Google, and GitHub - Updated loader and telemetry tests for the relocated capture and `share_id` passthrough - Cloud E2E regression covers logged-out `/?share=abc` redirecting to login after capturing share auth attribution |
||
|
|
ac56ecf009 |
refactor: unify image editor upload contract (FE-750) (#12318)
## Summary Collapse the OSS vs Cloud branching in the mask editor and painter uploads so both runtimes use the same contract — POST to `/upload/image` with `type: input` and no `subfolder`, then reference the result by filename only. ## Related - Companion spec change (Comfy-Org/ComfyUI): https://github.com/Comfy-Org/ComfyUI/pull/13968 deprecates `/api/upload/mask` and documents `/api/upload/image` as the unified upload contract. No runtime behavior changes on the server. ## Changes - **What**: - `useMaskEditorSaver.ts`: replace the separate `uploadMask` + `uploadImage` helpers with a single `uploadLayer`. All four layers (masked, paint, painted, paintedMasked) now go through `/upload/image`. Dropped the `original_ref` form field and the `clipspace` subfolder. `uploadLayer` throws on a non-ok status, on a non-JSON response body, and on a 200 response missing `data.name` (no more silent fallback to the pre-upload ref). - `usePainter.ts`: removed the runtime branch on upload type/subfolder and on the returned widget value. Always uploads as `type: input` with no subfolder; widget value is `${filename} [input]`. Added a `data.name` guard alongside the existing non-ok and JSON-parse guards. - `usePainter.test.ts`: assert the upload FormData carries `type=input` with no `subfolder`, plus new coverage for the missing-name and JSON-parse-failure error branches. - `browser_tests/tests/maskEditor.spec.ts`: drop the now-dead `**/upload/mask` Playwright route interceptors and tighten the save-success assertion to confirm exactly four `/upload/image` calls (one per layer). - **Breaking**: yes for downstream consumers — the mask editor no longer calls `/upload/mask`, and saved widget values for both editors no longer contain a subfolder prefix. Existing nodes will continue to load their referenced inputs because the filename is preserved; new saves emit the unified shape. ## Review Focus - Confirm the mask editor's four-layer upload (masked, paint, painted, paintedMasked) is still correct without `original_ref` — the layers are now independent uploads rather than alpha-composited server-side. The four blobs are composited client-side in `prepareOutputData` before upload, so the previous `original_ref` chain was vestigial — but worth a second look. - OSS-side painter widget back-compat: previously stored as `painter/foo.png [temp]`, new saves emit `foo.png [input]`. The old format remains valid in saved workflows; only the *new save* shape changes. Loading a pre-existing OSS workflow with a `painter/foo.png [temp]` widget value still resolves via the existing parser path. ## Test plan Unit: - [x] `usePainter.test.ts` — 30 tests pass, including the three new ones covering FormData payload shape, missing `data.name`, and bad JSON. E2E: - [x] `browser_tests/tests/maskEditor.spec.ts` — `save uploads all layers and closes dialog` and `save failure keeps dialog open` updated to the unified contract. Smoke-tested manually against a Cloud staging instance: - [x] **Painter node**: paint strokes → save → reload workflow → widget value resolves and the canvas re-renders cleanly. Upload responds with `subfolder: ""`, `type: "input"`. - [x] **Mask editor**: open editor with a base image, paint + mask, save. All four layers (`clipspace-mask-*`, `clipspace-paint-*`, `clipspace-painted-*`, `clipspace-painted-masked-*`) POST successfully to `/upload/image` and return 200 with the asset-aware response shape (`{ name, subfolder: "", type: "input", asset: { id, hash, tags } }`). The resulting `LoadImage` node references the painted-masked filename by basename only and re-renders. Not validated here: OSS ComfyUI core-side smoke. The contract is symmetric (the OSS `/upload/image` endpoint accepts the same fields the FE now sends), but a smoke against OSS HEAD is recommended before merge. --------- Co-authored-by: GitHub Action <action@github.com> |
||
|
|
e97cca9e4a |
feat: show node preview ghost when adding models from dialog & sidebar (#12765)
## Summary Adds consistent ghost node behavior when clicking "use" on models from either the model dialog or treeview - matching the Node Library & Node Search. ## Changes - **What**: - Split `createModelNodeFromAsset` into `resolveModelNodeFromAsset` and `startModelNodeDragFromAsset` to allow sharing logic between the two drag sources - Move NodeDragPreview file & use from being node library tab specific to app level in GraphCanvas so all drag sources share it - drag listeners now attach on `startDrag` and detach on cancel instead of living for the tab's lifetime - Updated `LGraphNodePreview` to accept widgetValues and prepend combo options with passed value so it shows in the preview as the default ## Review Focus - refactored positioning to use RAF with useMouse and transform to fix laggy-follow behavior present in Firefox ## Screenshots (if applicable) Model dialog + Node library https://github.com/user-attachments/assets/b227ac43-c6ea-4cf6-86ed-6cfb196fd80e Model library sidebar https://github.com/user-attachments/assets/bb546aee-5099-4df9-abe5-68bccd8fa2eb |
||
|
|
8d82944441 |
fix: limit workflow models to metadata enrichment (#12990)
## Summary This PR intentionally narrows workflow-embedded model metadata handling so root-level `models[]` and node-level embedded model metadata can enrich existing missing-model candidates, but can no longer create new candidates by themselves. ## Why this PR exists ADR 0009, **Subgraph promoted widgets use linked inputs**, changes promoted value ownership for subgraphs. That design was implemented by [#12197](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12197), **Subgraph Link Only Promotion (ADR 0009)**. Under ADR 0009, a promoted widget is represented as a standard linked `SubgraphInput` on the host `SubgraphNode`. The host boundary owns the promoted value identity through the host node locator plus `SubgraphInput.name`. The interior source widget remains the provider of schema, type, options, tooltip, defaults, diagnostics, and migration metadata, but it is not the persistence owner of the promoted value. This PR is a preparatory cleanup discovered while working on the missing model detection follow-up required by that ADR 0009 / [#12197](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12197) behavior. The follow-up needs missing model detection to respect the new subgraph promoted-widget ownership model. While reviewing that path, we found that the existing embedded model metadata fallback in `enrichWithEmbeddedMetadata` was doing more than metadata enrichment. The important finding was that this fallback was not just attaching metadata to candidates that had already been detected from live node widgets. It could also synthesize brand-new `MissingModelCandidate` entries from workflow JSON metadata, including root-level `models[]` entries, when no live candidate existed. That behavior is inaccurate for the missing model system for two reasons. First, the normal missing model lifecycle is anchored to a real node/widget binding. A candidate found from a COMBO or asset widget has a concrete `nodeId + widgetName` reference. That reference is what lets the UI surface the error, cache it as a pending warning, and later clear or resolve it when the underlying node/widget value is fixed. A root-level `models[]` entry does not reliably provide that anchor. If metadata-only fallback creates a candidate without a real live widget reference, the resulting error can be detected but cannot reliably travel through the existing clearing path. In practice, that can become an effectively unremovable missing model warning unless the user downloads exactly the same model referenced by the stale metadata. Second, a missing model error is meant to mean that a model-selecting widget on an active node references a value that is not available. Workflow JSON metadata by itself is not the same source of truth. If a model only appears in root workflow metadata, or appears in node metadata that is not represented by an active COMBO or asset widget candidate, that is a different kind of state from the existing missing model error model. Treating that metadata as a candidate creates a second, less reliable detector that is not aligned with the scan/clear lifecycle. This is especially important before the ADR 0009 missing-model follow-up. With linked-input promoted widgets, the host promoted value is the value that matters. The interior source widget may still carry stale or default metadata, and it must not become a second source of truth for missing model errors. A detection path that can create candidates directly from workflow metadata would make it harder to reason about which value actually produced the warning. For those reasons, this PR removes metadata-only candidate synthesis and keeps embedded metadata in the role it can perform safely: metadata enrichment. If the live widget/asset scan produces a candidate, embedded metadata may fill in `directory`, `url`, `hash`, and `hashType`. If no live candidate exists, the metadata is not enough to create a missing model warning. This PR is intended to land before the child PR that updates runtime missing model detection for ADR 0009 linked-input promoted widgets. ## Changes - **What**: Restrict `enrichWithEmbeddedMetadata` to enriching existing candidates instead of creating fallback candidates from unmatched root `models[]` or embedded model metadata. - **What**: Remove the now-unused installed-model check callback and asset-support callback from `enrichWithEmbeddedMetadata`. - **What**: Remove the now-unnecessary `modelStore.loadModelFolders()` path from the missing model pipeline, since embedded metadata no longer performs installed-model fallback detection. - **What**: Remove dead source-tracking metadata (`EmbeddedModelWithSource`, source node/widget fields, and widget-name lookup) that only existed to support metadata-only synthesis. - **What**: Update missing model tests so they assert the new contract: metadata enriches live candidates, but does not create candidates without a live scan result. - **What**: Delete obsolete fixtures that only covered the removed metadata-only synthesis path. - **Breaking**: None expected. This is an intentional narrowing of an inaccurate fallback detector, not a public API change. - **Dependencies**: None. ## Review Focus Please focus on whether the candidate lifecycle now has a single source of truth: live COMBO/asset widget scanning creates candidates, while workflow metadata only enriches those candidates. The intended behavioral change is that a model present only in workflow-level metadata, with no active node widget candidate referencing it, no longer appears as a missing model. This avoids surfacing warnings that cannot be cleared through the normal `nodeId + widgetName` path. The expected retained behavior is that active widget-referenced missing models are still detected by `scanAllModelCandidates`, and metadata from root `models[]` or node `properties.models` still supplies download-related fields for those live candidates. ## Screenshots (if applicable) Not applicable. This is a detection/pipeline behavior change covered by unit tests. ## Validation - `pnpm test:unit src/platform/missingModel/missingModelScan.test.ts src/platform/missingModel/missingModelPipeline.test.ts` - `pnpm exec eslint src/platform/missingModel/missingModelScan.ts src/platform/missingModel/missingModelScan.test.ts src/platform/missingModel/missingModelPipeline.ts src/platform/missingModel/missingModelPipeline.test.ts src/platform/missingModel/types.ts` - `pnpm exec oxfmt --check src/platform/missingModel/missingModelScan.ts src/platform/missingModel/missingModelScan.test.ts src/platform/missingModel/missingModelPipeline.ts src/platform/missingModel/missingModelPipeline.test.ts src/platform/missingModel/types.ts` - `pnpm typecheck` - pre-push hook: `knip --cache` |
||
|
|
d4be483c03 |
fix(billing): widen user popover so the credits row keeps both buttons inside (#13052)
## Issue In the top-right user popover, a **cancelled-but-still-active** personal subscription renders **both** "Add credits" and "Resubscribe" in the credits row (the user can still top up *and* re-subscribe during the grace period). With a wide (7-digit) credit balance, balance + help icon + both buttons exceeded the fixed `w-80` (320px) popover and the trailing "Resubscribe" button spilled past the right edge. Surfaced during FE-991 (Billing Rework V1) testing. Pre-existing on `main` — reproducible for any personal owner whose subscription is cancelled but not yet expired. ## Fix Make the popover width **fluid** instead of fixed: `w-fit` clamped to `min-w-80 max-w-96`. It stays **320px** in the common single-action case (unchanged) and only grows — to **~370px** — when the credits row actually needs the room for a second button. ## Before / After **Before (`w-80`)** — "Resubscribe" clipped past the popover edge: <img width="320" alt="before" src="https://github.com/user-attachments/assets/439baae8-9e04-4cdf-b43f-098fb5e3853f" /> **After — single action (stays 320px):** <img width="320" alt="after-single" src="https://github.com/user-attachments/assets/e96f784e-6afd-4286-80c3-1cf0ecec7aa8" /> **After — both actions (grows to ~370px, fits):** <img width="370" alt="after-both" src="https://github.com/user-attachments/assets/578c1528-24ad-4717-a2b5-33e1af78f048" /> ## Test Adds a `@cloud` e2e that opens the popover in the cancelled-but-active state (mocked `/customers/cloud-subscription-status` with `end_date` + a 7-digit balance) and asserts the "Resubscribe" button's right edge stays within the popover bounds — same bounding-box pattern as `workspaceSwitcher.spec.ts`. Validated red→green locally (fails on fixed `w-80`, passes with the fluid width); single-action width measured at 320px, both-action at ~370px. --------- Co-authored-by: GitHub Action <action@github.com> |
||
|
|
403353ac77 |
feat: add tab status indicator (running/done/errored) (#10177)
## Summary Adds indicator to show outcome of last job per tab, cleared next time the workflow is activated. ## Changes - **What**: - add workflow status tracking to execution store, handling various events - add icon to tab based on store - handle race condition where job finishes instantly (e.g. invalid workflow or already executed) ## Screenshots (if applicable) https://github.com/user-attachments/assets/8b1d8d8e-57d4-4ac2-9cc3-0d218d6eb0f7 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10177-feat-add-tab-status-indicator-running-done-errored-3266d73d365081a89f5dfd58487bb065) by [Unito](https://www.unito.io) --------- Co-authored-by: bymyself <cbyrne@comfy.org> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com> |
||
|
|
c4db198875 |
fix(test): de-flake Canvas ctrl+shift+vertical-drag zoom e2e (#13024)
## Observed flake
The e2e test `Canvas Interaction > Can zoom in/out with
ctrl+shift+vertical-drag` (`browser_tests/tests/interaction.spec.ts`)
intermittently failed in CI with:
```
Error: mouse.move: Test timeout of 15000ms exceeded
```
It failed all 3 retries on a single shard while 209 other tests on that
shard passed, and passed on a later re-run — i.e. genuinely flaky, not a
hard break.
## Root cause
The test pressed `Control` and `Shift` *down* once, then ran three
`canvasOps.dragAndDrop` gestures (each performs a `mouse.move(target, {
steps: 100 })`) with three `toHaveScreenshot` assertions interleaved,
and only released the modifiers at the very end — with no `try/finally`.
Two problems:
1. Holding the modifiers across all three drags means every one of the
~300 step-wise `mousemove` events drives litegraph's ctrl+shift zoom
handler (scale recompute + canvas redraw). Combined with the screenshot
captures in between, the main thread can saturate, and a single
`mouse.move` step can stall past the 15s test timeout. That is exactly
the failing call in the signature.
2. Without `try/finally`, a mid-test failure leaves `Control`/`Shift`
stuck down.
## Fix
Switch to the existing `canvasOps.ctrlShiftDrag(from, to)` helper, which
presses and releases `Control`+`Shift` around each individual gesture.
This is the robust pattern already used in `canvasSettings.spec.ts`. The
modifiers are never held across the heavy multi-drag + screenshot
sequence, and are always released.
Test intent, drag coordinates, and all three screenshot assertions are
unchanged.
## Validation
- Verified by reasoning: `ctrlShiftDrag` wraps the same `dragAndDrop`
with `keyboard.down/up` of the same modifiers, called with identical
`Position` args, so behavior and types are preserved.
- Could not run the browser e2e locally (requires a running ComfyUI
backend + Playwright browsers; `node_modules` not installed in this
environment). Relying on CI for the full e2e run.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
|
||
|
|
90c523b4a3 |
feat(dialog): flip remaining callers + default renderer to Reka (Phase 6a cutover, stacked on #12848. 6a-2) (#12593)
## Summary The **renderer cutover** for Phase 6: every remaining dialog caller is flipped to Reka, and `createDialog` now defaults `renderer: 'reka'` so the PrimeVue `Dialog` branch is no longer reached by default (it survives only as an explicit `renderer: 'primevue'` escape hatch, deleted in Phase 6b). > **Stacked on #12848** (mask editor + 3D viewer dialogs + dialog infra). Per @jtydhr88's review, the heavy, screenshot-bearing surface (3D + mask editor) was split into #12848 so it reviews and tests on its own. **Merge #12848 first**, then this PR's base auto-retargets to `main`. Parent: [FE-571](https://linear.app/comfyorg/issue/FE-571/dialog-system-migration-primevue-reka-ui-parent) This phase: [FE-578](https://linear.app/comfyorg/issue/FE-578/phase-6-remove-primevue-dialogconfirmdialog-imports-clean-up-css) ## Changes - **drop dead `ConfirmationService` registration** — zero `useConfirm`/`<ConfirmDialog>` consumers remain in `src/`; desktop-ui keeps its own. - **flip `showConfirmDialog`** — all six confirm callers render Reka chrome; width goes from PrimeVue auto-hug to fixed `size:'md'`, matching `dialogService.confirm()`. - **flip remaining `dialogService` + composable callers** — signin, update-password, top-up, workspace family, cancel-subscription, publish, cloud-notification, edit-keybinding / node-conflict / import-failed, upload-model, queue-clear-history, delete-assets, share / open-shared-workflow, subscription pricing. Self-styled panels get a shared transparent `w-fit` chrome replicating PrimeVue's auto-sized root. - **default `createDialog` to `renderer:'reka'`** — cuts over `showExtensionDialog` (third-party dialogs) and anything unflagged. The single-commit revert point. - **retarget class-based e2e selectors** — `BaseDialog` `.p-dialog` → `getByRole('dialog')`, `BuilderSaveAsHelper` close-X → `getByLabel`, `shareWorkflowDialog` role-based, dead `confirm-dialog` testid removed. - honor `[autofocus]` inside Reka dialogs; size the template browser dialog so the filter bar fits; drop redundant Tailwind width constraints on the remaining callers. ## Review focus 1. **`modal:false` on the pricing dialogs** — same trade-off as Settings/Manager (visual overlay without focus trap) because `PricingTable(.Workspace)` hosts a body-teleported PrimeVue `Popover`. 2. **`w-fit` shrink-wrapped chrome** for self-styled panels — replicates PrimeVue's shrink-to-fit root. 3. **Confirm width change** (auto-hug → fixed 576px `md`) — intentional consistency with `dialogService.confirm()`. ## Public API impact `createDialog` now defaults to Reka. Third-party extension dialogs render through Reka by default — a fixed `size:'md'` frame with a modal focus trap instead of PrimeVue auto-width; `renderer:'primevue'` remains an explicit escape hatch until Phase 6b. Worth a release note for extension authors. ## Out of scope (Phase 6b) PrimeVue branch deletion (`GlobalDialog.vue` legacy branch, `PrimeDialog` import, `.p-dialog` CSS/bridge tokens, `dialogStore` `pt`/`position`/`unstyled` typing) — lands after this soaks one cloud deploy cycle. ## 📸 Screenshots — manual verification Captured via Chrome DevTools (CDP) from this branch running locally in **cloud mode** (proxied to the `cloud.comfy.org` backend, free Personal Workspace). Every dialog below now renders through the **Reka** path — the PrimeVue `Dialog` branch is no longer reached. (Mask editor + 3D viewers live in the stacked base #12848.) **Confirm dialog** (`showConfirmDialog`) — Reka chrome at a fixed `size:'md'`, replacing PrimeVue's auto-hug width — *review focus #3* <img width="880" alt="confirm-dialog" src="https://github.com/user-attachments/assets/5d9953c1-4d0c-4ff9-adc7-88dd370c6a24" /> **Settings** — renders through Reka <img width="880" alt="settings" src="https://github.com/user-attachments/assets/44e3fd3f-8d9b-4322-8fbe-8ce8d94ed15d" /> **Edit Keybinding**, stacked on Settings — small-layout `w-fit` chrome; closing it leaves Settings open (stacked-dismiss holds) <img width="880" alt="edit-keybinding-nested" src="https://github.com/user-attachments/assets/d0875c00-7b9c-439d-b24d-ba6770009d08" /> **Subscription pricing** (`PricingTable`) — opened with `modal:false` because it hosts a body-teleported PrimeVue `Popover` — *review focus #1* <img width="880" alt="subscription-pricing" src="https://github.com/user-attachments/assets/3be20397-8a69-4b00-b803-73eff4e0e313" /> **Share** and **Publish** (open-shared-workflow + publish) — shared transparent shrink-wrapped (`w-fit`) chrome — *review focus #2* <img width="880" alt="share-dialog" src="https://github.com/user-attachments/assets/16f1c1b5-e35e-4664-a957-2f7f61ad96bd" /> <img width="880" alt="publish-dialog" src="https://github.com/user-attachments/assets/935ff453-5247-430f-9c21-2f500d4bc6e2" /> **Workspace** (workspace-family callers) <img width="880" alt="workspace-settings" src="https://github.com/user-attachments/assets/8031a352-f6fc-41e4-9567-e26e0c35ecd9" /> **Template selector** (`showExtensionDialog` / `useWorkflowTemplateSelectorDialog`) <img width="880" alt="templates-dialog" src="https://github.com/user-attachments/assets/9975ebbe-75ae-4ad9-a90a-248db4850e1a" /> **Account / workspace menu** (cloud) <img width="880" alt="account-menu" src="https://github.com/user-attachments/assets/5bc0cade-9bd9-49de-8bb4-779d65e211b0" /> |
||
|
|
78a8d6f8fc |
test: stabilize node help locale e2e (#12998)
## Summary
Stabilizes the locale-specific Node Help E2E by setting the locale
through the existing Playwright settings fixture before app bootstrap
instead of racing a workflow reload pulse.
## Changes
- **What**: Removed the brittle in-page locale mutation helper and uses
`test.use({ initialSettings: { 'Comfy.Locale': 'ja' } })` for the
locale-specific documentation case.
- **What**: Keeps the Japanese and English doc routes local to the test,
then verifies the Japanese help content after loading the default
workflow.
- **Dependencies**: None.
## Review Focus
Please focus on whether the E2E now waits on the correct setup boundary.
The previous helper watched `ChangeTracker.isLoadingGraph` after
changing `Comfy.Locale`; once unrelated workflow-load work became
faster, that loading pulse could complete before the helper observed it.
Pre-boot `initialSettings` avoids that timing dependency and uses
existing test infrastructure.
Verification:
- `pnpm exec oxfmt --check browser_tests/tests/nodeHelp.spec.ts
browser_tests/fixtures/helpers/WorkflowHelper.ts`
- `pnpm exec eslint browser_tests/tests/nodeHelp.spec.ts
browser_tests/fixtures/helpers/WorkflowHelper.ts`
- `pnpm typecheck:browser`
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5176 pnpm
exec playwright test browser_tests/tests/nodeHelp.spec.ts --grep "Should
handle locale-specific documentation" --project=chromium
--repeat-each=10`
## Screenshots (if applicable)
N/A
|
||
|
|
cc41e3e1ac |
test: stabilize cloud template filtering e2e (#12999)
## Summary Stabilizes the cloud template filtering E2E by making the test own both startup asset API responses and the complete template universe it asserts against. ## Changes - **What**: Uses the existing `createCloudAssetsFixture([])` fixture so startup `/api/assets` calls do not create an unrelated error toast that can intercept the Clear Filters click. - **What**: Extends `TemplateHelper.mockIndex()` to also mock `/api/workflow_templates` as an empty custom-template map, so tests that configure a core template index do not accidentally include custom templates from the local backend. - **Dependencies**: None. ## Review Focus Please focus on fixture ownership. This spec asserts exact template counts, so `templateApi.mockIndex()` should isolate the core template index and the custom workflow-template endpoint together. The asset fixture change is intentionally scoped to this cloud spec and reuses existing infrastructure instead of dismissing arbitrary toasts or forcing clicks. Verification: - `pnpm exec oxfmt --check browser_tests/tests/templateFilteringCount.spec.ts browser_tests/fixtures/helpers/TemplateHelper.ts browser_tests/fixtures/helpers/WorkflowHelper.ts` - `pnpm exec eslint browser_tests/tests/templateFilteringCount.spec.ts browser_tests/fixtures/helpers/TemplateHelper.ts browser_tests/fixtures/helpers/WorkflowHelper.ts` - `pnpm typecheck:browser` - `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:8188 PLAYWRIGHT_SETUP_API_URL=http://localhost:8188 pnpm exec playwright test browser_tests/tests/templateFilteringCount.spec.ts --grep "clear filters button resets" --project=cloud --repeat-each=5` - `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:8188 PLAYWRIGHT_SETUP_API_URL=http://localhost:8188 pnpm exec playwright test browser_tests/tests/templateFilteringCount.spec.ts --project=cloud` ## Screenshots (if applicable) N/A |
||
|
|
444dc3fccd |
test: stabilize mask editor screenshot e2e (#13011)
<img width="1155" height="648" alt="스크린샷 2026-06-19 오후 11 24 27" src="https://github.com/user-attachments/assets/01ed2607-662f-4735-b0c2-2f1a2c8a8811" /> ## Summary Stabilizes the Mask Editor screenshot E2E by hiding the transient brush cursor before capturing the dialog. ## Changes - **What**: Moves the pointer from the mask editor pointer zone to the Brush Settings panel before the screenshot and asserts that the brush cursor is hidden. - **Dependencies**: None. ## Review Focus Please check that the test still exercises the dialog UI while excluding only cursor-position noise from the screenshot. The PR also includes the existing browser-test `AppMode` type import fix needed for `typecheck:browser` on branches that touch `browser_tests/**`. Validation: - `pnpm exec oxfmt --check browser_tests/tests/maskEditor.spec.ts browser_tests/fixtures/helpers/WorkflowHelper.ts` - `pnpm exec eslint browser_tests/tests/maskEditor.spec.ts browser_tests/fixtures/helpers/WorkflowHelper.ts` - `pnpm typecheck:browser` - `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5173 pnpm exec playwright test browser_tests/tests/maskEditor.spec.ts --grep "opens mask editor from image preview button" --project=chromium --repeat-each=10` --------- Co-authored-by: github-actions <github-actions@github.com> |
||
|
|
ed028a88be |
Fix LiteGraph hidden widget metadata handling (FE-1014) (#12916)
## Summary
Fixes FE-1014 by making legacy LiteGraph honor backend-provided hidden
widget metadata.
This PR adds a regression test for the Painter node and updates
LiteGraph widget construction so that a backend input spec with `hidden:
true` is mirrored onto the top-level `widget.hidden` property that the
legacy canvas renderer actually reads.
## Problem
Backend node definitions can mark inputs as hidden, for example with
`extra_dict={"hidden": True}`. That metadata already flows into the
frontend widget options as `widget.options.hidden`, which is why Vue
nodes correctly hide those fields.
Legacy LiteGraph, however, does not use `widget.options.hidden` for
canvas visibility. Its rendering, layout, and hit-testing paths check
top-level `widget.hidden` instead. As a result, a field could be hidden
in Vue nodes while still appearing as an editable control in the legacy
LiteGraph canvas.
For affected nodes, this exposes fields that are intended to be
implementation details, schema/version values, or other non-user-facing
inputs.
## Root Cause
The frontend widget construction path copied backend display metadata
into `widget.options`, including:
- `advanced`
- `hidden`
But it did not mirror backend `hidden` metadata into `widget.hidden`.
That created a renderer split:
- Vue nodes and the right panel use `widget.options.hidden`.
- Legacy LiteGraph uses top-level `widget.hidden`.
So backend-hidden widgets were hidden in Vue mode but still visible and
clickable in legacy LiteGraph mode.
## Implementation
The production change is intentionally small and scoped to
backend-provided hidden metadata:
- Continue assigning `inputSpec.hidden` to `widget.options.hidden` as
before.
- When `inputSpec.hidden` is explicitly defined, also assign it to
top-level `widget.hidden`.
This keeps Vue behavior unchanged while making the legacy LiteGraph
renderer receive the same backend hidden signal through the field it
already uses for visibility.
The fix deliberately does not mirror `advanced` into top-level
`widget.advanced`. While investigating this area, I found that many
backend inputs define `advanced`, and changing legacy advanced-widget
behavior would be a much broader behavioral change than FE-1014
requires. This PR only addresses hidden metadata.
## Test Coverage
This PR adds and tightens Painter regression coverage because Painter
currently provides a concrete backend-hidden widget case:
- In Vue mode, the test verifies hidden Painter widgets are not rendered
to the user.
- In legacy LiteGraph mode, the test disables Vue nodes, loads the
Painter workflow, clicks the rows where backend-hidden number widgets
used to be exposed, and verifies the legacy graph editor dialog does not
open.
The legacy test specifically covers the backend-hidden number widgets
`width` and `height`. It uses user-observable behavior rather than
asserting internal widget flags directly.
A follow-up discussion is ongoing about the broader contract between
`widget.options.hidden` and top-level `widget.hidden`, especially for
frontend-extension-only hiding such as Painter `bg_color`. This PR
intentionally keeps that broader renderer-contract question out of scope
and focuses on backend `hidden` metadata from FE-1014.
## Validation
Validated locally with targeted Playwright coverage:
```bash
PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5173 pnpm exec playwright test browser_tests/tests/painter.spec.ts --project=chromium -g "Does not render hidden standard widgets|Does not open editors for backend-hidden number widget rows"
```
Result:
```text
2 passed
```
Also validated with linting:
```bash
pnpm eslint src/services/litegraphService.ts browser_tests/tests/painter.spec.ts
pnpm eslint browser_tests/tests/painter.spec.ts
```
The commit hooks also passed:
- `oxfmt`
- `oxlint`
- `eslint`
- `pnpm typecheck`
- `pnpm typecheck:browser`
## Notes
The new legacy test was confirmed red before the production fix and
green after the production fix, so it is not a vacuous assertion. The
final cleanup commit only tightens test naming and coordinate handling
while preserving the same regression intent.
|
||
|
|
fc4d44c3db |
[feat] migrate website navbar to shadcn-vue + mobile sheet drill-down (#12861)
## Summary - Migrate the website's top nav from bespoke components (`SiteNav`, `MobileMenu`, `NavDesktopLink`, `PillButton`, `MaskRevealButton`) to shadcn-vue primitives (`NavigationMenu`, `Sheet`, `Button`), split into `HeaderMain` → `HeaderMainDesktop` + `HeaderMainMobile`. - Mobile nav becomes a `Sheet` with drill-down sub-navigation, scroll lock, sticky CTAs, sr-only i18n title/description, and a back-to-home logo. - Desktop nav uses `NavigationMenu` with shared viewport, featured cards, `NavColumn` extraction, and centralized nav data in `data/mainNavigation.ts`. - Adds i18n strings for nav labels, dropdown column headers, close/back affordances, and the mobile menu description. ## Test plan - [ ] Desktop: hover PRODUCTS / COMMUNITY / COMPANY — dropdowns open with featured card + columns, NEW badges render, external links show the arrow-up-right icon, viewport is shared between triggers. - [ ] Mobile (<lg): open hamburger sheet — verify logo + close, body scroll is locked while open, top-level nav scrolls if it overflows, CTAs stay pinned at bottom. - [ ] Tap COMMUNITY / PRODUCTS / COMPANY — sub-panel slides over root nav, in-sheet BACK returns to root, links navigate correctly. - [ ] Reload `?locale=zh-CN`: dropdown labels, sheet title/description, BACK / close affordances all localized. - [ ] No regressions on `/cloud`, `/cloud/pricing`, `/customers`, etc. — pages that swap CTAs (`BrandButton` → shadcn `Button`) still render at every breakpoint. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com> Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: Alexander Brown <drjkl@comfy.org> |
||
|
|
7f25d28b71 |
Filter canvasOnly non-preview widgets in editor (#12957)
Non preview, `canvasOnly` widgets like `control_after_generate` could be displayed in the subgraph editor even though promoting them would have no visual or functional effect when in vue mode. In vue mode, these entries are now hidden from the list of candidate items for promotion to reduce confusion. |
||
|
|
67b884d0f7 |
fix(billing): route subscription/sign-in/credit preconditions to modal, out of error panel (FE-878) (#12785)
## Summary
Account preconditions (sign-in / subscription / credits) on running a
workflow now open their modal directly and stay out of the error panel +
error count — previously `subscription_required` fell through to a red
"1 ERROR — Subscription required to queue workflows" banner. This covers
**both** paths: the `execution_error` websocket event and the `POST
/prompt` 402 queue paywall (`{ type: "PAYMENT_REQUIRED", message:
"Subscription required to queue workflows" }`), which is the exact
payload reported in #12840.
## Changes
- **What**: `execution_error` is classified by a pure
`accountPreconditionRouting` resolver (precedence sign-in > subscription
> credits) and routed to the existing modal via
`useAccountPreconditionDialog`; `executionStore` returns early for
preconditions so they never populate `lastExecutionError` /
`lastPromptError` / `lastNodeErrors` → fully excluded from the panel and
`totalErrorCount`. Runtime credit error at a node → credits modal (out
of panel; can name the node).
- **Queue paywall**: the `queuePrompt` catch resolves the same
precondition from the `POST /prompt` 402 response and opens the modal,
short-circuiting before `lastPromptError`, so the queue paywall stays
out of the panel too. The runtime matcher learns the `"Subscription
required to queue workflows"` message.
- **Breaking**: none.
## Before / After
Free-tier queue paywall (`POST /prompt` → 402) on a cloud build:
**Before** — raw `Subscription required to queue workflows` surfaced in
the error panel (no actionable upgrade):
<img width="1600" height="873" alt="before-error-panel"
src="https://github.com/user-attachments/assets/1b76b742-16bf-47e3-9245-17e35f8f1e70"
/>
**After** — clean subscription modal opens; nothing in the error panel
or error count:
<img width="1600" height="873" alt="after-subscription-modal"
src="https://github.com/user-attachments/assets/13d238cb-20bf-4795-a530-5abcf9968dc7"
/>
## Review Focus
- **Routing-only — the run button is intentionally untouched.** The
original AC#3 ("no Subscribe-to-Run button") is superseded by the FE-978
run-lock decision (pre-emptive role-aware lock, Figma 3253-18671).
Complements #12786 (FE-978 run-lock); disjoint file sets.
- Tests: `accountPreconditionRouting` / `useAccountPreconditionDialog` /
`executionStore` — each precondition routes to its modal and is excluded
from the panel/count; precedence resolves on co-occurrence. Plus
Playwright `browser_tests/tests/subscriptionPaywallError.spec.ts` — the
queue paywall (402) stays out of the error panel, with a control
asserting ordinary queue errors still surface. typecheck / oxlint /
eslint / stylelint / oxfmt / knip clean.
Fixes FE-878
Fixes #12840
|
||
|
|
ab6c44aabf |
feat: remove deprecated group nodes, auto-convert to subgraphs on load (#12931)
## Summary Removes the deprecated Group Nodes feature and replaces it with a load-time migration that auto-converts any group nodes in a loaded workflow into Subgraphs (with accepted lossiness). ## Changes - **What**: - `groupNode.ts` is now a migration-only extension. `beforeConfigureGraph` registers temporary node types from `extra.groupNodes` so instances are created during `configure`; a new `afterConfigureGraph` hook converts every group node in the root graph to a subgraph (via `LGraph.convertToSubgraph`), re-scanning until none remain, then deletes `extra.groupNodes`. A failed conversion removes the offending node so loading never hangs or breaks. - Kept the minimum needed: `GroupNodeConfig` (builds the input/output/widget maps), a slimmed `GroupNodeHandler` exposing a rewritten `convertToNodes()` that no longer depends on the execution DTOs, the `globalDefs`/`addCustomNodeDefs` path, and the `nodeDefStore` `Object.assign` shim the migration relies on to detect group nodes. - Deleted: the Manage Group Nodes dialog (`groupNodeManage.ts`/`.css`), execution DTOs (`executableGroupNodeDto.ts`, `executableGroupNodeChildDTO.ts`), the create/builder flow, recreate, commands, keybindings, menus, the `isGroupNode` branches in the right-side panel / error grouping / focus composable, the group-node branches in node templates, dead i18n keys, and the now-unused `serialise` clipboard helper. - Rewrote `browser_tests/tests/groupNode.spec.ts` to assert auto-conversion; deleted the `ManageGroupNode` page object and `manageGroupNode()` helper. - Net: ~2,700 lines removed across 23 files (7 files deleted). - **Breaking**: Group nodes can no longer be created, managed, or executed. Existing workflows still load — their group nodes are converted to subgraphs on open. ## Review Focus - The load-time migration in `afterConfigureGraph` and the rewritten `GroupNodeHandler.convertToNodes()` (no longer uses the execution `getInnerNodes()` / DTOs; derives inner node type/index from `groupData.nodeData.nodes` and relies on `deserialiseAndCreate` + selection ordering). - Kept `nodeDefStore`'s `Object.assign(this, obj)` shim: the migration depends on it to propagate the group-node marker symbol onto the registered node definition. ### Accepted lossiness - Group nodes nested inside subgraphs (or inside other group nodes) convert into the root graph rather than their original container — essentially nonexistent in real legacy workflows since group nodes predate subgraphs. - Temporary `workflow>name` node types stay registered for the session; instantiating one auto-converts it to a subgraph. ## Verification `pnpm typecheck`, `typecheck:browser`, `knip`, `oxlint`, `eslint`, and `oxfmt` are green (also enforced by pre-commit hooks). Unit tests for the touched files could not be run locally due to a pre-existing environment error (`file:///assets/images/*.svg` passed to a Node filename API at import time, which also fails on unmodified test files); the browser spec requires a live server. --------- Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: AustinMroz <austin@comfy.org> Co-authored-by: GitHub Action <action@github.com> |
||
|
|
2cdaead000 |
fix(cloud): stop bouncing working users to /cloud/survey mid-session (FE-739) (#12621)
## Summary
Cloud users get yanked to `/cloud/survey` mid-workflow with no user
action. The redirect is **downstream of auth**: when the Cloud token is
briefly stale (token rotation / auth-refresh / reconnect window), the
authenticated survey-status check 401s, and the gate turned that
transient 401 into "survey not completed" → redirect.
Surveys are currently disabled for everyone on cloud via dynamicconfig
as the live mitigation. This PR lets us re-enable them safely
**without** waiting on the auth rework.
## Root cause
`getSurveyCompletedStatus()` returned `false` ("not completed") on
**any** non-200 — including a transient 401/403/5xx or network error —
and consumers treat `false` as a redirect to the survey. So a
stale-token 401 (or the page force-reload a 401 triggers in
`GraphCanvas`) bounced a working, already-onboarded user to the survey.
The real root cause of the transient 401s is a separate, still-open
effort: **FE-963** (reactive 401 re-mint + single retry), **FE-950/951**
(unified Cloud JWT), **BE-1125**. This PR does **not** fix those; it
stops the survey from being their user-visible casualty.
## The fix
`getSurveyCompletedStatus` now distinguishes the responses instead of
failing closed on all of them:
- **404** → not completed (show survey). This is the genuine signal: the
cloud backend (`GetSettingById`) returns 404 for a survey key that was
never stored, and a 404 is only reachable after a successful
authenticated read (a stale token 401s, never 404s), so it can't be a
transient false signal.
- **transient 401/403/5xx/network** → treat as completed (fail-safe), so
a working user is never bounced.
- **200** → completed iff `value` is non-empty (unchanged).
**No router change.** The `/` onboarding guard is untouched (router.ts
matches main), so the existing UX is preserved — a not-completed user is
still gated to the survey on load; only the spurious transient-failure
bounce is removed.
## Why #12301 was reverted, and how this differs
#12301 shipped a **blanket** fail-safe (`!response.ok → true`, 404
included), which made the survey unreachable for genuinely-not-completed
users (404 → "completed") and was reverted in #12344. This PR
special-cases **404 as the real not-completed signal** and fails safe
only on transient/ambiguous responses, so onboarding still works.
## Tests
- **Unit** (`auth.test.ts`): 200 non-empty → true; 200 empty / `null` /
missing `value` key → false; **404 → false**; 401/403/500/network →
true.
- **E2E** (`browser_tests/tests/cloudSurveyGate.spec.ts`, `@cloud`): a
transient 401 on `/` does **not** bounce a working user; a genuine 404
on `/` **does** route to the survey.
Linear: FE-739. Root cause (separate): FE-963 / FE-950 / FE-951 /
BE-1125.
|
||
|
|
1a27372e44 |
Fix 'insert as node' in sidebar tab (#12900)
When right clicking an output asset from the assets sidebar panel, the 'insert as node in workflow' action was twice bugged - The default type, as used for determining filename annotation, was set to the type of the file. This meant that annotations would never be applied to the filename - `temp` outputs would incorrectly be assigned the `output` type. - My fix for this one gives me a slightly bad taste in my mouth. Parsing URLs isn't great, but it's cleaner than needing to scan the (potentially sparse) full outputs to try and find the corresponding output. |