feat(workspace): creator-only canManageSubscriptionLifecycle permission (FE-770)#12829
Conversation
…on (FE-770) Adds a creator-only subscription-lifecycle permission so cancel / reactivate / downgrade can be gated to the workspace's original owner (any owner keeps canManageSubscription for manage-payment / top-up / change-commit), per the billing-permission matrix in the FE SSOT. ASSUMES /api/workspaces exposes a current-user-relative is_creator flag — BE spec is NOT finalized (field shape + original-owner determination are open, FE-770 Q3 / BE-1337). Fails closed: when is_creator is absent the lifecycle permission is false, so no behavior changes until the BE signal lands. Code comments mark every assumption point for revisit. Consumers (FE-978 cancel/ reactivate, FE-977 downgrade) wire to this once it is available.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds an optional ChangesCreator-Only Subscription Lifecycle Permissions
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 6 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (6 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/platform/workspace/api/workspaceApi.ts`:
- Around line 23-34: The new optional field is_creator was added to the
WorkspaceWithRole interface but the schema/typing that build UI state still
drops it; update the shared types and parsing to preserve this flag: add
is_creator?: boolean to the Workspace shape in workspaceTypes (match
WorkspaceWithRole) and update the parsing/schema in workspaceAuthStore (the code
that currently maps to { id, name, type, role }) to include and pass through
is_creator from the backend payload (or default to false/undefined if absent) so
the UI state receives the creator flag consistently and creator-only checks work
when the backend sends it.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 69139e30-e711-4ba9-a27b-1b18bc060099
📒 Files selected for processing (3)
src/platform/workspace/api/workspaceApi.tssrc/platform/workspace/composables/useWorkspaceUI.test.tssrc/platform/workspace/composables/useWorkspaceUI.ts
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #12829 +/- ##
===========================================
- Coverage 76.76% 62.99% -13.77%
===========================================
Files 1573 1461 -112
Lines 107148 74615 -32533
Branches 33135 19374 -13761
===========================================
- Hits 82248 47007 -35241
- Misses 23982 27262 +3280
+ Partials 918 346 -572
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 1172 files with indirect coverage changes 🚀 New features to boost your workflow:
|
… (FE-770) CodeRabbit (#12829): is_creator was declared on api/workspaceApi WorkspaceWithRole but the duplicate type in workspaceTypes.ts and the Zod schema in workspaceAuthStore stripped it, so the flag could be dropped on the auth/session parse path. Align both so the original-owner flag survives. Still ASSUMED / BE spec not finalized (FE-770 Q3 / BE-1337); optional, fails closed.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/platform/workspace/workspaceTypes.ts (1)
18-26: ⚡ Quick winDuplicate type definition creates sync burden.
WorkspaceWithRoleis defined in bothworkspaceTypes.tsandapi/workspaceApi.tswith identical fields. This duplication requires manual synchronization (as noted in the comment) and creates drift risk if one is updated without the other.Consider unifying these definitions by importing from a single source, or document a clear rationale for maintaining separate definitions if architectural constraints require it.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/platform/workspace/workspaceTypes.ts` around lines 18 - 26, There are duplicate type definitions for WorkspaceWithRole causing sync burden; remove the duplicate declaration in workspaceTypes.ts and instead import and re-export the single canonical WorkspaceWithRole type from the existing API module where it’s already defined (the api-side WorkspaceWithRole that includes id, name, type: 'personal'|'team', role: 'owner'|'member', and optional is_creator), then update any local references to use that imported type so only one source of truth remains.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/platform/workspace/workspaceTypes.ts`:
- Around line 18-26: There are duplicate type definitions for WorkspaceWithRole
causing sync burden; remove the duplicate declaration in workspaceTypes.ts and
instead import and re-export the single canonical WorkspaceWithRole type from
the existing API module where it’s already defined (the api-side
WorkspaceWithRole that includes id, name, type: 'personal'|'team', role:
'owner'|'member', and optional is_creator), then update any local references to
use that imported type so only one source of truth remains.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: db68885b-1087-463f-8997-3c01e02382be
📒 Files selected for processing (2)
src/platform/workspace/stores/workspaceAuthStore.tssrc/platform/workspace/workspaceTypes.ts
BE confirmed the current-user-relative is_creator boolean and that the creator is tracked explicitly (not by creation date). Field stays optional and gating fails closed until it actually ships on /api/workspaces.
…ps (FE-770) WorkspaceWithRole + WorkspaceWithRoleSchema are hand-rolled only because the cloud ingest OpenAPI doesn't expose is_creator yet. @comfyorg/ingest-types already generates the type + zWorkspaceWithRole; swap to them in this PR once the spec ships the field.
pythongosssss
left a comment
There was a problem hiding this comment.
lgtm, would like the comment to be updated before this gets in though
| joined_at: string | ||
| } | ||
|
|
||
| // TODO(this PR, once the cloud ingest OpenAPI exposes `is_creator`): drop this |
There was a problem hiding this comment.
TODO "this PR" is a bit confusing here
There was a problem hiding this comment.
Reworded in 96ac29b — dropped the this PR reference; it's now a plain forward TODO keyed on the cloud ingest OpenAPI exposing is_creator. The generated-type swap is tracked as a follow-up (PR description), since this one is safe to merge ahead of BE.
There was a problem hiding this comment.
Reworded in 96ac29b to drop the PR-scoped "this PR" phrasing — the TODO now just points at swapping the hand-rolled type/schema for the generated WorkspaceWithRole/zWorkspaceWithRole once BE adds is_original_owner to the ingest OpenAPI.
…(FE-770) Drop the confusing "this PR" reference per review; the comment outlives the PR, so describe the swap condition (OpenAPI exposes is_creator) as a plain TODO.
📦 Bundle: 7.45 MB gzip 🔴 +507 BDetailsSummary
Category Glance App Entry Points — 46.7 kB (baseline 46.7 kB) • ⚪ 0 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.25 MB (baseline 1.25 MB) • 🔴 +2 BGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed Views & Navigation — 95.3 kB (baseline 95.3 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 9 added / 9 removed / 3 unchanged Panels & Settings — 525 kB (baseline 525 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 11 added / 11 removed / 15 unchanged User & Accounts — 19.9 kB (baseline 19.9 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 6 added / 6 removed / 3 unchanged Editors & Dialogs — 112 kB (baseline 112 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 4 added / 4 removed / 1 unchanged UI Components — 57.2 kB (baseline 57.2 kB) • ⚪ 0 BReusable component library chunks
Status: 5 added / 5 removed / 8 unchanged Data & Services — 268 kB (baseline 268 kB) • ⚪ 0 BStores, services, APIs, and repositories
Status: 13 added / 13 removed / 3 unchanged Utilities & Hooks — 3.32 MB (baseline 3.32 MB) • 🔴 +1.61 kBHelpers, composables, and utility bundles
Status: 17 added / 17 removed / 13 unchanged Vendor & Third-Party — 15.3 MB (baseline 15.3 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 16 unchanged Other — 10.4 MB (baseline 10.4 MB) • 🔴 +300 BBundles that do not match a named category
Status: 66 added / 66 removed / 84 unchanged ⚡ Performance Report
Show regressions
All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-06-19T23:36:50.215Z",
"gitSha": "7961c02628035ed25c7684c3eea66dd297410a98",
"branch": "jaewon/fe-770-creator-lifecycle-permission",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2038.062000000025,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.008999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 354.132,
"heapDeltaBytes": -1866336,
"heapUsedBytes": 56429348,
"domNodes": 22,
"jsHeapTotalBytes": 24379392,
"scriptDurationMs": 20.627,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "canvas-idle",
"durationMs": 2040.7780000000457,
"styleRecalcs": 12,
"styleRecalcDurationMs": 10.254,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 365.99399999999997,
"heapDeltaBytes": -2014036,
"heapUsedBytes": 56134676,
"domNodes": 24,
"jsHeapTotalBytes": 26476544,
"scriptDurationMs": 23.557000000000002,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1944.3360000000212,
"styleRecalcs": 80,
"styleRecalcDurationMs": 44.983999999999995,
"layouts": 12,
"layoutDurationMs": 3.4909999999999997,
"taskDurationMs": 858.1959999999999,
"heapDeltaBytes": -7406240,
"heapUsedBytes": 51293192,
"domNodes": 63,
"jsHeapTotalBytes": 26476544,
"scriptDurationMs": 125.81600000000002,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1829.738999999904,
"styleRecalcs": 75,
"styleRecalcDurationMs": 35.941,
"layouts": 12,
"layoutDurationMs": 3.6550000000000002,
"taskDurationMs": 743.99,
"heapDeltaBytes": -7077736,
"heapUsedBytes": 51327508,
"domNodes": 58,
"jsHeapTotalBytes": 26476544,
"scriptDurationMs": 122.60499999999999,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1728.476999999998,
"styleRecalcs": 30,
"styleRecalcDurationMs": 15.572000000000001,
"layouts": 6,
"layoutDurationMs": 0.578,
"taskDurationMs": 318.48699999999997,
"heapDeltaBytes": 1485320,
"heapUsedBytes": 60179888,
"domNodes": 76,
"jsHeapTotalBytes": 25165824,
"scriptDurationMs": 17.855,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1713.9789999999948,
"styleRecalcs": 30,
"styleRecalcDurationMs": 17.126,
"layouts": 6,
"layoutDurationMs": 0.7129999999999999,
"taskDurationMs": 335.60999999999996,
"heapDeltaBytes": -5886040,
"heapUsedBytes": 46711044,
"domNodes": -271,
"jsHeapTotalBytes": 17428480,
"scriptDurationMs": 18.391000000000002,
"eventListeners": -188,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "dom-widget-clipping",
"durationMs": 591.6679999999701,
"styleRecalcs": 13,
"styleRecalcDurationMs": 8.704,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 362.536,
"heapDeltaBytes": 7264704,
"heapUsedBytes": 65647656,
"domNodes": 22,
"jsHeapTotalBytes": 18874368,
"scriptDurationMs": 65.092,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 546.5970000000198,
"styleRecalcs": 13,
"styleRecalcDurationMs": 8.941999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 344.913,
"heapDeltaBytes": 7256812,
"heapUsedBytes": 65606404,
"domNodes": 22,
"jsHeapTotalBytes": 18874368,
"scriptDurationMs": 62.647999999999996,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2001.2480000000323,
"styleRecalcs": 11,
"styleRecalcDurationMs": 9.451999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 511.9309999999999,
"heapDeltaBytes": -7987848,
"heapUsedBytes": 62201404,
"domNodes": 22,
"jsHeapTotalBytes": 11677696,
"scriptDurationMs": 93.10799999999999,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2021.7749999999342,
"styleRecalcs": 10,
"styleRecalcDurationMs": 8.38,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 484.677,
"heapDeltaBytes": -9345692,
"heapUsedBytes": 62979384,
"domNodes": 20,
"jsHeapTotalBytes": 9318400,
"scriptDurationMs": 88.17800000000001,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-pan",
"durationMs": 2140.2520000000322,
"styleRecalcs": 70,
"styleRecalcDurationMs": 19.837,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1087.638,
"heapDeltaBytes": 10330676,
"heapUsedBytes": 83150756,
"domNodes": 20,
"jsHeapTotalBytes": 10018816,
"scriptDurationMs": 408.28600000000006,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2093.4950000000754,
"styleRecalcs": 70,
"styleRecalcDurationMs": 20.244,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1038.932,
"heapDeltaBytes": 9707192,
"heapUsedBytes": 82308500,
"domNodes": 20,
"jsHeapTotalBytes": 11591680,
"scriptDurationMs": 381.209,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3093.127000000095,
"styleRecalcs": 65,
"styleRecalcDurationMs": 19.359,
"layouts": 60,
"layoutDurationMs": 7.812000000000001,
"taskDurationMs": 1296.2150000000001,
"heapDeltaBytes": -10633752,
"heapUsedBytes": 58531512,
"domNodes": 14,
"jsHeapTotalBytes": 14213120,
"scriptDurationMs": 488.89700000000005,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-zoom",
"durationMs": 3115.1270000000295,
"styleRecalcs": 65,
"styleRecalcDurationMs": 18.163999999999998,
"layouts": 60,
"layoutDurationMs": 7.617000000000001,
"taskDurationMs": 1247.5,
"heapDeltaBytes": 12911436,
"heapUsedBytes": 76257212,
"domNodes": 12,
"jsHeapTotalBytes": 7340032,
"scriptDurationMs": 472.563,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "minimap-idle",
"durationMs": 2001.3300000000527,
"styleRecalcs": 9,
"styleRecalcDurationMs": 9.576999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 479.511,
"heapDeltaBytes": -9322432,
"heapUsedBytes": 64372748,
"domNodes": 18,
"jsHeapTotalBytes": 7745536,
"scriptDurationMs": 85.64000000000001,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "minimap-idle",
"durationMs": 1997.0509999999422,
"styleRecalcs": 8,
"styleRecalcDurationMs": 8.05,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 485.339,
"heapDeltaBytes": -9751384,
"heapUsedBytes": 63897516,
"domNodes": 16,
"jsHeapTotalBytes": 8269824,
"scriptDurationMs": 86.62500000000001,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 547.3170000000209,
"styleRecalcs": 47,
"styleRecalcDurationMs": 10.963000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 360.074,
"heapDeltaBytes": 7682808,
"heapUsedBytes": 66343252,
"domNodes": 20,
"jsHeapTotalBytes": 19660800,
"scriptDurationMs": 126.434,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 537.6909999999953,
"styleRecalcs": 45,
"styleRecalcDurationMs": 9.395000000000003,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 344.9960000000001,
"heapDeltaBytes": 4829720,
"heapUsedBytes": 50293796,
"domNodes": -152,
"jsHeapTotalBytes": 10747904,
"scriptDurationMs": 121.61499999999998,
"eventListeners": -152,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 1990.7800000000293,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.356,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 357.034,
"heapDeltaBytes": -2432144,
"heapUsedBytes": 56368072,
"domNodes": 22,
"jsHeapTotalBytes": 25427968,
"scriptDurationMs": 18.452,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-idle",
"durationMs": 1990.634,
"styleRecalcs": 11,
"styleRecalcDurationMs": 9.782,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 340.881,
"heapDeltaBytes": -2552192,
"heapUsedBytes": 56117948,
"domNodes": 22,
"jsHeapTotalBytes": 25165824,
"scriptDurationMs": 17.76,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1710.0670000000378,
"styleRecalcs": 76,
"styleRecalcDurationMs": 38.058,
"layouts": 16,
"layoutDurationMs": 4.629,
"taskDurationMs": 690.9369999999999,
"heapDeltaBytes": -10677664,
"heapUsedBytes": 48043604,
"domNodes": 64,
"jsHeapTotalBytes": 25427968,
"scriptDurationMs": 96.29400000000001,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1674.910000000068,
"styleRecalcs": 77,
"styleRecalcDurationMs": 35.191,
"layouts": 16,
"layoutDurationMs": 3.785,
"taskDurationMs": 666.931,
"heapDeltaBytes": 15540240,
"heapUsedBytes": 74213348,
"domNodes": 65,
"jsHeapTotalBytes": 25952256,
"scriptDurationMs": 93.34100000000001,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-transition-enter",
"durationMs": 1018.7349999999924,
"styleRecalcs": 17,
"styleRecalcDurationMs": 28.394999999999996,
"layouts": 4,
"layoutDurationMs": 13.949000000000003,
"taskDurationMs": 734.1999999999999,
"heapDeltaBytes": 2524184,
"heapUsedBytes": 78080896,
"domNodes": 13833,
"jsHeapTotalBytes": 17301504,
"scriptDurationMs": 29.869999999999994,
"eventListeners": 2529,
"totalBlockingTimeMs": 154,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "viewport-pan-sweep",
"durationMs": 8161.031000000094,
"styleRecalcs": 251,
"styleRecalcDurationMs": 55.54,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3636.8140000000003,
"heapDeltaBytes": -2956680,
"heapUsedBytes": 67016000,
"domNodes": 20,
"jsHeapTotalBytes": 18931712,
"scriptDurationMs": 1228.732,
"eventListeners": 20,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333338,
"p95FrameDurationMs": 16.80000000000109
},
{
"name": "viewport-pan-sweep",
"durationMs": 8118.021999999996,
"styleRecalcs": 251,
"styleRecalcDurationMs": 56.214000000000006,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3686.668,
"heapDeltaBytes": -2949872,
"heapUsedBytes": 67177792,
"domNodes": 22,
"jsHeapTotalBytes": 17358848,
"scriptDurationMs": 1250.3029999999999,
"eventListeners": 20,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.80000000000109
},
{
"name": "vue-large-graph-idle",
"durationMs": 12757.903000000057,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12735.928999999998,
"heapDeltaBytes": -30337628,
"heapUsedBytes": 169755008,
"domNodes": -3310,
"jsHeapTotalBytes": 19894272,
"scriptDurationMs": 596.0350000000001,
"eventListeners": -16474,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 13043.097999999987,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 13025.794000000002,
"heapDeltaBytes": -41065720,
"heapUsedBytes": 170499324,
"domNodes": -3310,
"jsHeapTotalBytes": 22777856,
"scriptDurationMs": 596.662,
"eventListeners": -16472,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-pan",
"durationMs": 14904.403999999999,
"styleRecalcs": 70,
"styleRecalcDurationMs": 17.739000000000004,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14883.245999999997,
"heapDeltaBytes": -25433808,
"heapUsedBytes": 170283640,
"domNodes": -3308,
"jsHeapTotalBytes": 19632128,
"scriptDurationMs": 862.8760000000001,
"eventListeners": -16470,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.773333333333238,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-pan",
"durationMs": 14953.83199999992,
"styleRecalcs": 70,
"styleRecalcDurationMs": 17.485000000000028,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14927.952000000001,
"heapDeltaBytes": -41132120,
"heapUsedBytes": 170178996,
"domNodes": -3308,
"jsHeapTotalBytes": 20418560,
"scriptDurationMs": 852.466,
"eventListeners": -16470,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.220000000000073,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "workflow-execution",
"durationMs": 469.6369999999206,
"styleRecalcs": 24,
"styleRecalcDurationMs": 25.992,
"layouts": 5,
"layoutDurationMs": 1.613,
"taskDurationMs": 128.126,
"heapDeltaBytes": 5576088,
"heapUsedBytes": 65262896,
"domNodes": 192,
"jsHeapTotalBytes": 3145728,
"scriptDurationMs": 21.403,
"eventListeners": 69,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "workflow-execution",
"durationMs": 451.7759999999953,
"styleRecalcs": 17,
"styleRecalcDurationMs": 23.302999999999997,
"layouts": 5,
"layoutDurationMs": 1.1520000000000001,
"taskDurationMs": 116.746,
"heapDeltaBytes": 5250760,
"heapUsedBytes": 64922196,
"domNodes": 159,
"jsHeapTotalBytes": 3145728,
"scriptDurationMs": 21.275999999999996,
"eventListeners": 69,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000273
}
]
}🎭 Playwright: ✅ 1672 passed, 0 failed · 1 flaky📊 Browser Reports
🎨 Storybook: ✅ Built — View Storybook |
|
Option B follow-up (non-blocking) — when BE adds Current design here (Option A) derives the current-user original-owner signal from the member-list self-row ( Preferred end-state (Option B): expose
|
Owner Cancel (plan menu) + Resubscribe (panel button and the workspace popover re-activate path) now require canManageSubscriptionLifecycle (original owner) instead of canManageSubscription. Promoted (non-creator) owners keep manage- payment / upgrade / top-up; they no longer see cancel/reactivate. Per the billing-permission matrix (FE SSOT). Stacked on the permission infra (#12829). DRAFT / do-not-merge until the BE is_creator flag ships: with it absent the permission fails closed, which would hide cancel/reactivate from every owner (regression).
| () => { | ||
| const workspace = store.activeWorkspace | ||
| if (workspace?.type === 'team' && store.members.length === 0) { | ||
| void store.fetchMembers() |
There was a problem hiding this comment.
This may be better off living in the store as an "ensureMembersLoaded", guarded for in flight duplicate calls, catch the failed request and log if failed, and with a "membersLoaded" flag instead of the .length === 0 check
There was a problem hiding this comment.
Moved into the store as ensureMembersLoaded in 8278c9d: no-ops for personal/already-loaded workspaces, dedupes in-flight calls, and catches+logs failures (leaving the workspace unloaded so a later call retries). Used a per-workspace loaded Set rather than a single membersLoaded boolean — a global flag would skip loading the new team on workspace switch.
| { immediate: true } | ||
| ) | ||
|
|
||
| const isOriginalOwner = computed(() => store.isCurrentUserOriginalOwner) |
There was a problem hiding this comment.
Looks redundant, can just use store.isCurrentUserOriginalOwner
There was a problem hiding this comment.
Done in 8278c9d — inlined store.isCurrentUserOriginalOwner directly into the permissions computed.
Owner Cancel (plan menu) + Resubscribe (panel button and the workspace popover re-activate path) now require canManageSubscriptionLifecycle (original owner) instead of canManageSubscription. Promoted (non-creator) owners keep manage- payment / upgrade / top-up; they no longer see cancel/reactivate. Per the billing-permission matrix (FE SSOT). Stacked on the permission infra (#12829). DRAFT / do-not-merge until the BE is_creator flag ships: with it absent the permission fails closed, which would hide cancel/reactivate from every owner (regression).
…12830) Stacked on the permission infra #12829. Gates owner **Cancel** (plan menu) + **Resubscribe** (panel button + workspace popover re-activate path) on `canManageSubscriptionLifecycle` (original owner only), per the billing-permission matrix (FE SSOT). Promoted owners keep manage-payment / upgrade / top-up; members get none. **Original-owner signal (repointed).** Rebased onto the updated #12829 (`a51183ae`), so the gate now resolves from the member-list `is_original_owner` field that the cloud cutover (Comfy-Org/cloud #4359) ships: the store getter `isCurrentUserOriginalOwner` matches the current user's member self-row by email, and `useWorkspaceUI` eagerly `fetchMembers()` so billing surfaces have the roster loaded. This replaces the earlier `is_creator`-on-`/api/workspaces` assumption — BE standardized on a per-member `is_original_owner` instead. No `is_creator` remains. **Merge-gated** on cutover cloud#4359 reaching cloud main: until `GET /api/workspace/members` returns `is_original_owner` in prod, the gate fails closed (lifecycle actions hidden for every owner). Fail-closed = no regression pre-deploy, but do not merge until the field is live. **Follow-up (non-blocking).** Exposing `is_original_owner` (current-user-relative, sibling of `role`) on `/api/workspaces` — Hunter pre-approved 2026-06-17 — would let us read it directly and drop the eager member fetch. Tracked on #12829 / FE-770. Part of FE-978 (member run-lock modal shipped separately in #12786).
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Address review: own the team members-preload coordination in the store instead of the composable. ensureMembersLoaded no-ops for personal / already-loaded workspaces, dedupes in-flight calls, and logs failures so a later call retries. Inline the redundant isCurrentUserOriginalOwner computed.
|
Deployment failed with the following error: Learn More: https://vercel.com/uy-tieu-s-projects?upgradeToPro=build-rate-limit |
The watch now gates ensureMembersLoaded on activeWorkspace.type === 'team' so personal/undefined workspaces never invoke the store loader. This keeps consumer composables that mock the store minimally (useSubscriptionDialog) working, while the store method stays the idempotent safe loader.
|
@coderabbitai approve |
✅ Action performedComments resolved and changes approved. |
What
Adds a creator-only
canManageSubscriptionLifecyclepermission (cancel / reactivate / downgrade) touseWorkspaceUI:canManageSubscription(manage payment / top-up / change-commit).canManageSubscriptionLifecycle.Driven by an
is_creatorflag on the workspace from/api/workspaces, plumbed through the shared type and the auth/session Zod schema.Safe to merge ahead of BE / no regression
mainreferencescanManageSubscriptionLifecycleoris_creator.is_creatoris optional; when absent the permission isfalse. Existing permission values are unchanged, so existing consumers (SubscribeToRun,SubscriptionPanelContentWorkspace,CurrentUserPopoverWorkspace, …) behave identically.BE contract (confirmed with Hunter, 2026-06-17)
is_creator= current-user-relative boolean on/api/workspaces; the creator is tracked explicitly (not by creation date).created_by_user_id) — separate, still-open ask.Follow-up (separate PR, once BE ships)
is_creator+WorkspaceWithRoleSchemaare hand-rolled only because the cloud ingest OpenAPI doesn't expose the field yet.@comfyorg/ingest-typesalready generatesWorkspaceWithRole+zWorkspaceWithRole. Once BE addsis_creatortoservices/ingest/openapi.yaml, regenerate and swap the hand-rolled interface + schema for the generated ones (TODOs marked inworkspaceApi.ts/workspaceAuthStore.ts).