fix(workspace): derive Members access from Team plan#13810
Conversation
🎨 Storybook: ✅ Built — View Storybook📦 Bundle: 7.91 MB gzip 🔴 +166 BDetailsSummary
Category Glance App Entry Points — 47.3 kB (baseline 47.3 kB) • ⚪ 0 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.25 MB (baseline 1.25 MB) • ⚪ 0 BGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed Views & Navigation — 109 kB (baseline 109 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 11 added / 11 removed / 3 unchanged Panels & Settings — 550 kB (baseline 550 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 11 added / 11 removed / 15 unchanged User & Accounts — 29.1 kB (baseline 29.1 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 7 added / 7 removed / 3 unchanged Editors & Dialogs — 121 kB (baseline 121 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 5 added / 5 removed / 1 unchanged UI Components — 56.8 kB (baseline 56.8 kB) • ⚪ 0 BReusable component library chunks
Status: 5 added / 5 removed / 8 unchanged Data & Services — 273 kB (baseline 273 kB) • ⚪ 0 BStores, services, APIs, and repositories
Status: 13 added / 13 removed / 3 unchanged Utilities & Hooks — 3.4 MB (baseline 3.4 MB) • 🟢 -197 BHelpers, composables, and utility bundles
Status: 16 added / 16 removed / 20 unchanged Vendor & Third-Party — 15.7 MB (baseline 15.7 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 16 unchanged Other — 11.9 MB (baseline 11.9 MB) • 🔴 +1.05 kBBundles that do not match a named category
Status: 66 added / 66 removed / 97 unchanged ⚡ Performance Report
✅ No regressions detected. All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-07-18T23:36:07.952Z",
"gitSha": "dd594533a4cc8d41eb6fade3c2c3e54a1f842c4e",
"branch": "dante/fix-members-access-by-plan",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2034.9630000000047,
"styleRecalcs": 11,
"styleRecalcDurationMs": 6.461,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 375.631,
"heapDeltaBytes": -21500032,
"heapUsedBytes": 46829380,
"domNodes": -261,
"jsHeapTotalBytes": 19017728,
"scriptDurationMs": 27.007999999999996,
"eventListeners": -131,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "canvas-idle",
"durationMs": 1998.599000000013,
"styleRecalcs": 11,
"styleRecalcDurationMs": 6.370000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 352.235,
"heapDeltaBytes": -20395200,
"heapUsedBytes": 48318688,
"domNodes": -262,
"jsHeapTotalBytes": 20066304,
"scriptDurationMs": 12.062999999999999,
"eventListeners": -131,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1721.8050000000176,
"styleRecalcs": 73,
"styleRecalcDurationMs": 24.249,
"layouts": 12,
"layoutDurationMs": 2.671,
"taskDurationMs": 634.116,
"heapDeltaBytes": -15855236,
"heapUsedBytes": 52996080,
"domNodes": -263,
"jsHeapTotalBytes": 20328448,
"scriptDurationMs": 81.57000000000001,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1725.5059999999958,
"styleRecalcs": 73,
"styleRecalcDurationMs": 25.985,
"layouts": 12,
"layoutDurationMs": 2.528,
"taskDurationMs": 608.812,
"heapDeltaBytes": -22579788,
"heapUsedBytes": 46194440,
"domNodes": 57,
"jsHeapTotalBytes": 20328448,
"scriptDurationMs": 82.538,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1657.2150000000079,
"styleRecalcs": 29,
"styleRecalcDurationMs": 11.336,
"layouts": 6,
"layoutDurationMs": 0.46799999999999986,
"taskDurationMs": 240.35299999999998,
"heapDeltaBytes": 7258996,
"heapUsedBytes": 76144328,
"domNodes": 78,
"jsHeapTotalBytes": 19415040,
"scriptDurationMs": 13.241,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1671.3980000000106,
"styleRecalcs": 33,
"styleRecalcDurationMs": 12.952,
"layouts": 6,
"layoutDurationMs": 0.44899999999999995,
"taskDurationMs": 246.807,
"heapDeltaBytes": 7695788,
"heapUsedBytes": 76466156,
"domNodes": 81,
"jsHeapTotalBytes": 19152896,
"scriptDurationMs": 13.386,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 534.8490000000083,
"styleRecalcs": 13,
"styleRecalcDurationMs": 6.155,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 280.615,
"heapDeltaBytes": -11653984,
"heapUsedBytes": 57279900,
"domNodes": 22,
"jsHeapTotalBytes": 19677184,
"scriptDurationMs": 43.688,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 423.43700000003537,
"styleRecalcs": 14,
"styleRecalcDurationMs": 6.8340000000000005,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 249.326,
"heapDeltaBytes": -11874292,
"heapUsedBytes": 57068428,
"domNodes": 24,
"jsHeapTotalBytes": 19939328,
"scriptDurationMs": 37.304,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "large-graph-idle",
"durationMs": 1991.8499999999995,
"styleRecalcs": 11,
"styleRecalcDurationMs": 6.986000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 443.84499999999997,
"heapDeltaBytes": 5123836,
"heapUsedBytes": 64330808,
"domNodes": -264,
"jsHeapTotalBytes": 4669440,
"scriptDurationMs": 59.462,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2009.8610000000008,
"styleRecalcs": 10,
"styleRecalcDurationMs": 5.605,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 452.792,
"heapDeltaBytes": 5095328,
"heapUsedBytes": 66289660,
"domNodes": -265,
"jsHeapTotalBytes": 4669440,
"scriptDurationMs": 56.751000000000005,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2092.772000000025,
"styleRecalcs": 69,
"styleRecalcDurationMs": 9.468,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 797.8149999999999,
"heapDeltaBytes": 25523944,
"heapUsedBytes": 85785836,
"domNodes": -267,
"jsHeapTotalBytes": 5660672,
"scriptDurationMs": 258.778,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2054.7090000000026,
"styleRecalcs": 68,
"styleRecalcDurationMs": 9.982,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 770.097,
"heapDeltaBytes": 25303000,
"heapUsedBytes": 85899932,
"domNodes": -265,
"jsHeapTotalBytes": 5136384,
"scriptDurationMs": 241.67400000000004,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3061.494000000039,
"styleRecalcs": 66,
"styleRecalcDurationMs": 11.751000000000001,
"layouts": 60,
"layoutDurationMs": 5.671,
"taskDurationMs": 947.872,
"heapDeltaBytes": 17368848,
"heapUsedBytes": 80809308,
"domNodes": 18,
"jsHeapTotalBytes": 7864320,
"scriptDurationMs": 317.734,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-zoom",
"durationMs": 3071.0960000000114,
"styleRecalcs": 68,
"styleRecalcDurationMs": 11.921000000000001,
"layouts": 60,
"layoutDurationMs": 5.421,
"taskDurationMs": 925.2289999999998,
"heapDeltaBytes": 16616708,
"heapUsedBytes": 78165888,
"domNodes": 20,
"jsHeapTotalBytes": 8650752,
"scriptDurationMs": 320.65900000000005,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "minimap-idle",
"durationMs": 2005.3340000000048,
"styleRecalcs": 10,
"styleRecalcDurationMs": 5.19,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 438.81199999999995,
"heapDeltaBytes": 5453668,
"heapUsedBytes": 67605764,
"domNodes": -265,
"jsHeapTotalBytes": 4407296,
"scriptDurationMs": 55.074999999999996,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2006.838000000016,
"styleRecalcs": 10,
"styleRecalcDurationMs": 5.755,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 437.716,
"heapDeltaBytes": 5248544,
"heapUsedBytes": 67269316,
"domNodes": -265,
"jsHeapTotalBytes": 4931584,
"scriptDurationMs": 56.15200000000001,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 427.70199999995384,
"styleRecalcs": 48,
"styleRecalcDurationMs": 7.252,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 262.73,
"heapDeltaBytes": -11358840,
"heapUsedBytes": 57554676,
"domNodes": 22,
"jsHeapTotalBytes": 20463616,
"scriptDurationMs": 80.85600000000001,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 438.02299999998695,
"styleRecalcs": 48,
"styleRecalcDurationMs": 7.124999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 262.169,
"heapDeltaBytes": -11514540,
"heapUsedBytes": 57464388,
"domNodes": 22,
"jsHeapTotalBytes": 20463616,
"scriptDurationMs": 79.263,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-idle",
"durationMs": 1999.9349999999936,
"styleRecalcs": 11,
"styleRecalcDurationMs": 6.145,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 346.377,
"heapDeltaBytes": -21898388,
"heapUsedBytes": 46909756,
"domNodes": -262,
"jsHeapTotalBytes": 20066304,
"scriptDurationMs": 10.611,
"eventListeners": -131,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-idle",
"durationMs": 2021.9419999999673,
"styleRecalcs": 11,
"styleRecalcDurationMs": 5.887999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 339.872,
"heapDeltaBytes": -19875444,
"heapUsedBytes": 49091280,
"domNodes": -263,
"jsHeapTotalBytes": 20066304,
"scriptDurationMs": 10.992,
"eventListeners": -131,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1722.959000000003,
"styleRecalcs": 78,
"styleRecalcDurationMs": 26.723000000000003,
"layouts": 16,
"layoutDurationMs": 2.974,
"taskDurationMs": 554.0269999999999,
"heapDeltaBytes": -19409168,
"heapUsedBytes": 49495844,
"domNodes": -263,
"jsHeapTotalBytes": 19279872,
"scriptDurationMs": 58.837999999999994,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1690.477000000044,
"styleRecalcs": 76,
"styleRecalcDurationMs": 23.373000000000005,
"layouts": 16,
"layoutDurationMs": 2.548,
"taskDurationMs": 485.84000000000003,
"heapDeltaBytes": -5269460,
"heapUsedBytes": 63817664,
"domNodes": 64,
"jsHeapTotalBytes": 19415040,
"scriptDurationMs": 56.42199999999999,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-transition-enter",
"durationMs": 882.3570000000132,
"styleRecalcs": 19,
"styleRecalcDurationMs": 22.409,
"layouts": 15,
"layoutDurationMs": 8.689,
"taskDurationMs": 578.752,
"heapDeltaBytes": 29107568,
"heapUsedBytes": 96961220,
"domNodes": 13673,
"jsHeapTotalBytes": 14942208,
"scriptDurationMs": 20.04500000000001,
"eventListeners": 2369,
"totalBlockingTimeMs": 97,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8082.647999999949,
"styleRecalcs": 249,
"styleRecalcDurationMs": 23.793000000000003,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 2655.3909999999996,
"heapDeltaBytes": 14531364,
"heapUsedBytes": 73708804,
"domNodes": -265,
"jsHeapTotalBytes": 7495680,
"scriptDurationMs": 796.26,
"eventListeners": -113,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8088.463000000047,
"styleRecalcs": 250,
"styleRecalcDurationMs": 23.448999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 2625.4370000000004,
"heapDeltaBytes": 14903428,
"heapUsedBytes": 74040340,
"domNodes": -266,
"jsHeapTotalBytes": 7495680,
"scriptDurationMs": 791.129,
"eventListeners": -113,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 8140.341999999975,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 8112.423,
"heapDeltaBytes": -46338928,
"heapUsedBytes": 171229416,
"domNodes": -8311,
"jsHeapTotalBytes": -9375744,
"scriptDurationMs": 396.144,
"eventListeners": -16388,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.80000000000109
},
{
"name": "vue-large-graph-idle",
"durationMs": 8075.106000000005,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 8040.735999999999,
"heapDeltaBytes": -58067996,
"heapUsedBytes": 172184876,
"domNodes": -8311,
"jsHeapTotalBytes": -6230016,
"scriptDurationMs": 396.3179999999999,
"eventListeners": -16388,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.80000000000109
},
{
"name": "vue-large-graph-pan",
"durationMs": 10429.063999999982,
"styleRecalcs": 65,
"styleRecalcDurationMs": 11.28599999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 10407.943,
"heapDeltaBytes": -65880564,
"heapUsedBytes": 161416552,
"domNodes": -8311,
"jsHeapTotalBytes": -10686464,
"scriptDurationMs": 651.951,
"eventListeners": -16386,
"totalBlockingTimeMs": 13,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-pan",
"durationMs": 10396.655999999894,
"styleRecalcs": 65,
"styleRecalcDurationMs": 11.392000000000014,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 10377.592999999999,
"heapDeltaBytes": -39127092,
"heapUsedBytes": 171804560,
"domNodes": -8311,
"jsHeapTotalBytes": -10686464,
"scriptDurationMs": 706.0260000000001,
"eventListeners": -16386,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 33.29999999999927
},
{
"name": "workflow-execution",
"durationMs": 426.75800000000663,
"styleRecalcs": 13,
"styleRecalcDurationMs": 13.733,
"layouts": 2,
"layoutDurationMs": 0.42999999999999994,
"taskDurationMs": 85.01599999999999,
"heapDeltaBytes": -16190608,
"heapUsedBytes": 51733304,
"domNodes": 128,
"jsHeapTotalBytes": 7618560,
"scriptDurationMs": 6.973,
"eventListeners": 65,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "workflow-execution",
"durationMs": 424.2629999999963,
"styleRecalcs": 15,
"styleRecalcDurationMs": 12.636,
"layouts": 2,
"layoutDurationMs": 0.29100000000000004,
"taskDurationMs": 83.644,
"heapDeltaBytes": -16094296,
"heapUsedBytes": 51707180,
"domNodes": 121,
"jsHeapTotalBytes": 6569984,
"scriptDurationMs": 6.882000000000001,
"eventListeners": 67,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
}
]
}🎭 Playwright: ✅ 1714 passed, 0 failed · 2 flaky📊 Browser Reports
|
🎭 Playwright: ⏳ Running... |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughTeam-plan state is derived from billing context and used across membership composables, stores, settings panels, and browser fixtures. Member and invite fetching supports Team-entitled personal workspaces, while UI and invite actions wait for billing initialization and enforce plan and owner gating. ChangesTeam plan membership flow
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant WorkspacePanelContent
participant useTeamPlan
participant useBillingContext
participant teamWorkspaceStore
participant workspaceApi
WorkspacePanelContent->>useTeamPlan: read plan eligibility and loading state
useTeamPlan->>useBillingContext: read billing identity and subscription state
WorkspacePanelContent->>teamWorkspaceStore: fetch members and pending invites when eligible
teamWorkspaceStore->>workspaceApi: request member and invite data
Suggested reviewers: 🚥 Pre-merge checks | ✅ 6✅ Passed checks (6 passed)
✨ 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: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/platform/workspace/stores/teamWorkspaceStore.ts (1)
559-583: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winFix async deduplication race condition.
Returning early from
ensureMembersLoaded()when a fetch is in-flight resolves the Promise immediately for concurrent callers, before the members are actually loaded. Callers awaiting this method will proceed and read an emptystore.membersstate, which can lead to UI glitches or incorrect access-control logic.To properly deduplicate async calls without resolving early, cache and return the shared Promise itself.
🐛 Proposed fix using a Promise Map
- let inFlightMembersWorkspaceId: string | null = null + const inFlightMembersPromises = new Map<string, Promise<void>>() /** * Load the active workspace's members once. No-ops for already-loaded * workspaces and dedupes concurrent calls. A failed request is logged and * leaves the workspace unloaded so a later call retries. */ async function ensureMembersLoaded(): Promise<void> { const workspaceId = activeWorkspaceId.value if (!workspaceId) return if (loadedMemberWorkspaceIds.has(workspaceId)) return - if (inFlightMembersWorkspaceId === workspaceId) return - inFlightMembersWorkspaceId = workspaceId - try { - await fetchMembers() - loadedMemberWorkspaceIds.add(workspaceId) - } catch (e) { - console.error('Failed to load workspace members', e) - } finally { - if (inFlightMembersWorkspaceId === workspaceId) { - inFlightMembersWorkspaceId = null - } - } + const existingPromise = inFlightMembersPromises.get(workspaceId) + if (existingPromise) return existingPromise + + const promise = fetchMembers() + .then(() => { + loadedMemberWorkspaceIds.add(workspaceId) + }) + .catch((e) => { + console.error('Failed to load workspace members', e) + }) + .finally(() => { + inFlightMembersPromises.delete(workspaceId) + }) + + inFlightMembersPromises.set(workspaceId, promise) + return promise }🤖 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/stores/teamWorkspaceStore.ts` around lines 559 - 583, Update ensureMembersLoaded to cache the in-flight fetch Promise per workspace and return that shared Promise to concurrent callers instead of resolving immediately. Clear the cached Promise in a finally handler, preserve loadedMemberWorkspaceIds updates on success and retry behavior after failures, and use the existing activeWorkspaceId and fetchMembers flow.src/platform/workspace/components/dialogs/settings/MembersPanelContent.vue (1)
142-173: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winDo not render a personal-plan member row while billing is loading.
When
hasTeamPlanis initially false, the panel briefly renders the personal single-user state before initialization determines the actual plan.Proposed fix
-<template v-if="!hasTeamPlan"> +<template v-if="!isPlanLoading && !hasTeamPlan"> <MemberListItem ... /> </template> -<template v-else> +<template v-else-if="!isPlanLoading"> <MemberListItem v-for="..."🤖 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/components/dialogs/settings/MembersPanelContent.vue` around lines 142 - 173, Guard the personal-plan MemberListItem branch in MembersPanelContent so it does not render while billing or plan initialization is still loading. Preserve the existing personal member rendering once loading completes and retain the team-plan filteredMembers branch unchanged.
🤖 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/components/dialogs/settings/WorkspacePanelContent.test.ts`:
- Around line 181-186: Extend the test “waits for billing initialization before
fetching member data” to simulate billing initialization completing after render
by setting mockIsPlanLoading.value to false and awaiting the component’s
reactive update. Then assert mockFetchMembers and mockFetchPendingInvites are
called, covering the watcher’s deferred-fetch transition rather than only the
initial loading state.
In `@src/platform/workspace/composables/useMembersPanel.ts`:
- Around line 129-140: Update the permissions computed block so the
canManageMembers condition uses the initialized active-plan indicator
isOnTeamPlan together with the owner role, rather than hasTeamPlan. Ensure
canManageInvites and canInviteMembers continue deriving from this gated
canManageMembers value, while leaving canViewOtherMembers behavior unchanged.
---
Outside diff comments:
In `@src/platform/workspace/components/dialogs/settings/MembersPanelContent.vue`:
- Around line 142-173: Guard the personal-plan MemberListItem branch in
MembersPanelContent so it does not render while billing or plan initialization
is still loading. Preserve the existing personal member rendering once loading
completes and retain the team-plan filteredMembers branch unchanged.
In `@src/platform/workspace/stores/teamWorkspaceStore.ts`:
- Around line 559-583: Update ensureMembersLoaded to cache the in-flight fetch
Promise per workspace and return that shared Promise to concurrent callers
instead of resolving immediately. Clear the cached Promise in a finally handler,
preserve loadedMemberWorkspaceIds updates on success and retry behavior after
failures, and use the existing activeWorkspaceId and fetchMembers flow.
🪄 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: ASSERTIVE
Plan: Pro Plus
Run ID: b7957028-3ba8-4258-a893-6b54395eb6a6
📒 Files selected for processing (10)
src/platform/workspace/components/dialogs/settings/MembersPanelContent.test.tssrc/platform/workspace/components/dialogs/settings/MembersPanelContent.vuesrc/platform/workspace/components/dialogs/settings/WorkspacePanelContent.test.tssrc/platform/workspace/components/dialogs/settings/WorkspacePanelContent.vuesrc/platform/workspace/composables/useMembersPanel.test.tssrc/platform/workspace/composables/useMembersPanel.tssrc/platform/workspace/composables/useTeamPlan.test.tssrc/platform/workspace/composables/useTeamPlan.tssrc/platform/workspace/stores/teamWorkspaceStore.test.tssrc/platform/workspace/stores/teamWorkspaceStore.ts
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #13810 +/- ##
==========================================
- Coverage 79.15% 79.12% -0.03%
==========================================
Files 1684 1684
Lines 120428 119757 -671
Branches 42359 42896 +537
==========================================
- Hits 95322 94756 -566
+ Misses 24418 24305 -113
- Partials 688 696 +8
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 18 files with indirect coverage changes 🚀 New features to boost your workflow:
|
| watch( | ||
| [hasTeamPlan, isPlanLoading], | ||
| ([hasPlan, isLoading]) => { | ||
| if (!hasPlan || isLoading) return | ||
| void Promise.allSettled([fetchMembers(), fetchPendingInvites()]) | ||
| }, | ||
| { immediate: true } | ||
| ) |
There was a problem hiding this comment.
Nit: This would probably be cleaner as a
| watch( | |
| [hasTeamPlan, isPlanLoading], | |
| ([hasPlan, isLoading]) => { | |
| if (!hasPlan || isLoading) return | |
| void Promise.allSettled([fetchMembers(), fetchPendingInvites()]) | |
| }, | |
| { immediate: true } | |
| ) | |
| whenever( | |
| () => hasTeamPlan.value && !isPlanLoading.value, | |
| () => Promise.allSettled([fetchMembers(), fetchPendingInvites()]), | |
| { immediate: true } | |
| ) |
There was a problem hiding this comment.
Applied in 05a7c01. Replaced the manual watch and guard with VueUse whenever while preserving the Team-plan-after-initialization condition. WorkspacePanelContent tests pass 7/7, and format, lint, typecheck, and pre-push knip checks passed.
|
@coderabbitai approve |
✅ Action performedComments resolved and changes approved. |
|
@dante01yoon Successfully backported to #13813 |
|
@dante01yoon Successfully backported to #13814 |
Summary
Make Members eligibility follow the active billing plan instead of the personal/team workspace type. A personal workspace on a Team plan now gets the same Members experience as a team workspace on the same plan.
Root cause
The Members surface had three independent workspace-type gates:
useTeamPlantreated “team workspace + subscribed” as the Team-plan signal, so a personal workspace was always classified as not being on Team even when billing reported a Team plan.useMembersPanelinherited the personal-workspace permission and layout defaults, which forced the single-user row, disabled Invite, and rendered the Team upgrade banner.How this fixes it
hasTeamPlan, active Team-plan, cancelled/lapsed, and loading states from billing plan identity plus subscription state.Before / After
Before
A personal workspace with a Team plan was treated as a personal plan: Invite was disabled and the UI asked the user to upgrade to Team.
After
The same personal-workspace + Team-plan scenario loads the Team member list and enables owner member-management actions.
Review Focus
Validation
pnpm typecheckpassed.pnpm lintpassed.pnpm knippassed.memberRoleChange.spec.ts, including personal workspace + Team plan regression coverage.