Skip to content

Workspaces 4 members invites#8245

Merged
simula-r merged 16 commits intomainfrom
workspaces-4-members-invites
Jan 24, 2026
Merged

Workspaces 4 members invites#8245
simula-r merged 16 commits intomainfrom
workspaces-4-members-invites

Conversation

@simula-r
Copy link
Contributor

@simula-r simula-r commented Jan 22, 2026

Summary

Add team workspace member management and invite system.

Changes

  • Add members panel with role management (owner/admin/member) and member removal
  • Add invite system with email invites, pending invite display, and revoke functionality
  • Add invite URL loading for accepting invites
  • Add subscription panel updates for member management
  • Add i18n translations for member and invite features

┆Issue is synchronized with this Notion page by Unito

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 22, 2026

📝 Walkthrough

Walkthrough

This PR introduces comprehensive workspace member management and team workspace invitation functionality, including new dialog components for member/invite operations, invite URL query parameter handling, extended permission/UI configurations, and feature-flag-driven UI integration.

Changes

Cohort / File(s) Summary
Workspace Avatar & Icon Sizing
src/components/common/WorkspaceProfilePic.vue, src/components/topbar/CurrentUserButton.vue
Avatar increased from size-6 to size-8; chevron icon increased from size-3 to size-4.
Dialog Pass-Through & Type Handling
src/components/dialog/GlobalDialog.vue
Replaced static pt binding with computed getDialogPt() function that conditionally merges padding mask for workspace settings when teamWorkspacesEnabled. Added conditional styling for dialog content height and overflow.
Confirmation Dialog Enhancements
src/components/dialog/content/ConfirmationDialogContent.vue
Added conditional button rendering; Cancel button hidden for 'info' type, replaced with primary OK button instead.
Workspace Member Management
src/components/dialog/content/setting/MembersPanelContent.vue
New component implementing dual-view panel (Active members and Pending invites) with search, sorting, date formatting, per-member actions, and role badges. Handles both Personal and Team workspace contexts.
Workspace Settings Panel
src/components/dialog/content/setting/WorkspacePanelContent.vue
Added unstyled Tabs for plan and members with dynamic counts; Invite button with tooltip and limit enforcement; replaced SubscriptionPanelContent with SubscriptionPanelContentWorkspace; integrated MembersPanelContent; added fetchMembers/fetchPendingInvites on mount.
Workspace Creation & Editing
src/components/dialog/content/workspace/CreateWorkspaceDialogContent.vue, src/components/dialog/content/workspace/EditWorkspaceDialogContent.vue
Expanded workspace name validation regex to permit apostrophes, periods, commas, parentheses, ampersand, and plus characters.
Member & Invite Dialogs
src/components/dialog/content/workspace/InviteMemberDialogContent.vue, src/components/dialog/content/workspace/RemoveMemberDialogContent.vue, src/components/dialog/content/workspace/RevokeInviteDialogContent.vue
Three new components: Invite dialog with two-step email/link flow; Remove member confirmation with loading state; Revoke invite confirmation with loading state. All with toast feedback and error handling.
Workspace Subscription UI
src/platform/cloud/subscription/components/SubscriptionPanelContentWorkspace.vue
Replaced owner unsubscribed guard with showSubscribePrompt; added isInPersonalWorkspace logic; introduced showBillingComingSoonDialog for team workspaces; updated layout with responsive flexbox and adjusted spacing.
Subscription Components
src/platform/cloud/subscription/components/SubscriptionPanel.vue
Changed teamWorkspacesEnabled to ComputedRef; added computed import.
Auth Header Renaming
src/platform/cloud/subscription/components/PricingTable.test.ts, src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.ts
Updated from getAuthHeader to getFirebaseAuthHeader in firebaseAuthStore public API.
Topbar Workspace Components
src/components/topbar/CurrentUserPopoverWorkspace.vue
Removed workspace tier badge and subscribe hint; updated credits display to depend on isActiveSubscription (personal) or isWorkspaceSubscribed (team); replaced disabled SubscribeButton with context-aware Navigate button for non-personal workspaces.
Workspace Switcher
src/components/topbar/WorkspaceSwitcherPopover.vue
Added tier labels via getTierLabel(); extended AvailableWorkspace with isSubscribed and subscriptionPlan; imported useSubscription for user subscription tier name; removed Divider element.
Invite URL Loading
src/platform/workspace/composables/useInviteUrlLoader.ts, src/platform/workspace/composables/useInviteUrlLoader.test.ts
New composable to load and process workspace invites from URL query with preserved query restoration, error handling, and cleanup. Includes comprehensive test suite.
Graph Canvas Integration
src/components/graph/GraphCanvas.vue
Added feature-flag-driven invite URL loader; watcher triggers loadInviteFromUrl() when teamWorkspacesEnabled becomes true.
Workspace UI Configuration
src/platform/workspace/composables/useWorkspaceUI.ts
Extended WorkspacePermissions with canViewOtherMembers, canViewPendingInvites, canInviteMembers, canManageInvites, canRemoveMembers; extended WorkspaceUIConfig with visibility flags and grid column definitions.
Workspace API & Store
src/platform/workspace/api/workspaceApi.ts, src/platform/workspace/stores/teamWorkspaceStore.ts
Added role field to Member interface; added getFirebaseHeaderOrThrow() helper; exported WorkspaceMember and PendingInvite types; updated mapApiMemberToWorkspaceMember to include role; initialized isSubscribed based on workspace type; added preserved invite query clearing on workspace creation/switch.
Feature Flag & Auth Updates
src/stores/workspaceAuthStore.ts, src/platform/auth/workspace/useWorkspaceAuth.test.ts
Migrated from remoteConfig.team_workspaces_enabled to useFeatureFlags().flags.teamWorkspacesEnabled; updated test mocking to reflect feature flag approach.
Navigation & Query Preservation
src/platform/navigation/preservedQueryNamespaces.ts, src/platform/navigation/preservedQueryManager.ts, src/router.ts
Added INVITE namespace to PRESERVED_QUERY_NAMESPACES; added invite to router preserved query mappings; reformatted early-return checks in preservedQueryManager for clarity.
Settings Dialog Layout
src/platform/settings/components/SettingDialogContent.vue
Updated layout classes for teamWorkspacesEnabled branch to full-height overflow-auto flexbox; adjusted ScrollPanel from fixed to responsive width; wrapped SearchBox with conditional padding; added scrollbar-custom class and changed Tabs overflow handling.
Dialog Service Helpers
src/services/dialogService.ts
Added 'info' type to ConfirmationDialogType; added four new dialog helpers: showRemoveMemberDialog(memberId), showInviteMemberDialog(), showRevokeInviteDialog(inviteId), showBillingComingSoonDialog() with dynamic imports and workspace styling.
Localization
src/locales/en/main.json
Added entries for workspace member management: invite/inviteMember/inviteLimitReached; added removeMemberDialog, revokeInviteDialog, inviteMemberDialog dialogs; extended workspacePanel.tabs with dashboard, planCredits, membersCount; added billing-related toasts and workspace lifecycle messages; expanded workspaceSwitcher with personal option.

Sequence Diagram(s)

sequenceDiagram
    participant App as GraphCanvas/App
    participant Watcher as Feature Flag Watcher
    participant Loader as useInviteUrlLoader
    participant Store as teamWorkspaceStore
    participant API as Workspace API
    participant Router as Router
    participant Toast as Toast Service

    App->>Watcher: Setup (on mounted)
    Watcher->>Watcher: Watch teamWorkspacesEnabled
    Watcher->>Loader: loadInviteFromUrl() when flag true
    
    Loader->>Loader: Check URL for 'invite' param
    alt Invite param exists
        Loader->>Loader: Restore preserved query
        Loader->>Loader: Merge preserved into route
        Loader->>Store: acceptInvite(token)
        Store->>API: POST /accept-invite
        API-->>Store: Success/Error response
        
        alt Accept successful
            Store-->>Loader: Success
            Loader->>Toast: Show success message
        else Accept failed
            Store-->>Loader: Error
            Loader->>Toast: Show error message
        end
    end
    
    Loader->>Router: Remove 'invite' param from URL
    Loader->>Loader: Clear preserved query
    Loader-->>App: Complete
Loading

Possibly related PRs

Suggested reviewers

  • shinshin86
  • KarryCharon
  • christian-byrne
  • Yorha4D
  • pythongosssss
✨ Finishing touches
  • 📝 Generate docstrings

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Jan 22, 2026

🎭 Playwright Tests: ⚠️ Passed with flaky tests

Results: 503 passed, 0 failed, 2 flaky, 8 skipped (Total: 513)

❌ Failed Tests

📊 Browser Reports
  • chromium: View Report (✅ 493 / ❌ 0 / ⚠️ 1 / ⏭️ 8)
  • chromium-2x: View Report (✅ 2 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • chromium-0.5x: View Report (✅ 1 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • mobile-chrome: View Report (✅ 7 / ❌ 0 / ⚠️ 1 / ⏭️ 0)

@github-actions
Copy link

github-actions bot commented Jan 22, 2026

🎨 Storybook Build Status

Build completed successfully!

⏰ Completed at: 01/24/2026, 09:46:33 PM UTC

🔗 Links


🎉 Your Storybook is ready for review!

@github-actions
Copy link

github-actions bot commented Jan 22, 2026

Bundle Size Report

Summary

  • Raw size: 21.9 MB baseline 21.8 MB — 🔴 +48.8 kB
  • Gzip: 4.56 MB baseline 4.54 MB — 🔴 +11.4 kB
  • Brotli: 3.38 MB baseline 3.37 MB — 🔴 +9.1 kB
  • Bundles: 170 current • 168 baseline • 163 added / 161 removed

Category Glance
Panels & Settings 🔴 +21.6 kB (462 kB) · Other 🔴 +14.6 kB (6.43 MB) · Data & Services 🔴 +9.03 kB (3.18 MB) · Graph Workspace 🔴 +2.05 kB (957 kB) · Utilities & Hooks 🔴 +1.19 kB (25.2 kB) · App Entry Points 🔴 +306 B (22.8 kB) · + 5 more

Per-category breakdown
App Entry Points — 22.8 kB (baseline 22.5 kB) • 🔴 +306 B

Main entry bundles and manifests

File Before After Δ Raw Δ Gzip Δ Brotli
assets/index-DVURpieA.js (new) 22.8 kB 🔴 +22.8 kB 🔴 +6.84 kB 🔴 +6.03 kB
assets/index-k2BKttsm.js (removed) 22.5 kB 🟢 -22.5 kB 🟢 -6.75 kB 🟢 -5.93 kB

Status: 1 added / 1 removed

Graph Workspace — 957 kB (baseline 955 kB) • 🔴 +2.05 kB

Graph editor runtime, canvas, workflow orchestration

File Before After Δ Raw Δ Gzip Δ Brotli
assets/GraphView-rRJVbYEv.js (new) 957 kB 🔴 +957 kB 🔴 +194 kB 🔴 +146 kB
assets/GraphView-CFUPxrQ2.js (removed) 955 kB 🟢 -955 kB 🟢 -193 kB 🟢 -146 kB

Status: 1 added / 1 removed

Views & Navigation — 80.7 kB (baseline 80.7 kB) • ⚪ 0 B

Top-level views, pages, and routed surfaces

File Before After Δ Raw Δ Gzip Δ Brotli
assets/CloudSurveyView-6Y0ahVlY.js (removed) 17.1 kB 🟢 -17.1 kB 🟢 -3.6 kB 🟢 -3.05 kB
assets/CloudSurveyView-BgPqGi2-.js (new) 17.1 kB 🔴 +17.1 kB 🔴 +3.6 kB 🔴 +3.05 kB
assets/CloudLoginView-B47dtZbi.js (removed) 11.8 kB 🟢 -11.8 kB 🟢 -3.1 kB 🟢 -2.73 kB
assets/CloudLoginView-DQuFqEe0.js (new) 11.8 kB 🔴 +11.8 kB 🔴 +3.1 kB 🔴 +2.72 kB
assets/UserCheckView-BWga_iWt.js (new) 10.5 kB 🔴 +10.5 kB 🔴 +2.45 kB 🔴 +2.12 kB
assets/UserCheckView-C6SeSnXb.js (removed) 10.5 kB 🟢 -10.5 kB 🟢 -2.44 kB 🟢 -2.13 kB
assets/CloudLayoutView-BaWcjUJx.js (new) 8.54 kB 🔴 +8.54 kB 🔴 +2.25 kB 🔴 +1.96 kB
assets/CloudLayoutView-CHGBgTSh.js (removed) 8.54 kB 🟢 -8.54 kB 🟢 -2.24 kB 🟢 -1.96 kB
assets/CloudSignupView-Bl4FD_iD.js (removed) 8.18 kB 🟢 -8.18 kB 🟢 -2.33 kB 🟢 -2.02 kB
assets/CloudSignupView-CY47WuI1.js (new) 8.18 kB 🔴 +8.18 kB 🔴 +2.33 kB 🔴 +2.03 kB
assets/CloudForgotPasswordView-BCqSUGlg.js (new) 6.26 kB 🔴 +6.26 kB 🔴 +1.93 kB 🔴 +1.69 kB
assets/CloudForgotPasswordView-BggWz4e3.js (removed) 6.26 kB 🟢 -6.26 kB 🟢 -1.93 kB 🟢 -1.69 kB
assets/UserSelectView-Bx5wXbFY.js (removed) 5.28 kB 🟢 -5.28 kB 🟢 -1.76 kB 🟢 -1.57 kB
assets/UserSelectView-zt1VK6bR.js (new) 5.28 kB 🔴 +5.28 kB 🔴 +1.76 kB 🔴 +1.58 kB
assets/CloudSubscriptionRedirectView-BKducxfl.js (removed) 5.27 kB 🟢 -5.27 kB 🟢 -1.73 kB 🟢 -1.54 kB
assets/CloudSubscriptionRedirectView-LaHg9q--.js (new) 5.27 kB 🔴 +5.27 kB 🔴 +1.73 kB 🔴 +1.54 kB
assets/CloudAuthTimeoutView-1E0EJJmb.js (removed) 5.24 kB 🟢 -5.24 kB 🟢 -1.71 kB 🟢 -1.49 kB
assets/CloudAuthTimeoutView-KnD4Qdaa.js (new) 5.24 kB 🔴 +5.24 kB 🔴 +1.71 kB 🔴 +1.48 kB
assets/CloudSorryContactSupportView-AxA3vaH4.js (removed) 1.97 kB 🟢 -1.97 kB 🟢 -702 B 🟢 -626 B
assets/CloudSorryContactSupportView-B5gYo5Lv.js (new) 1.97 kB 🔴 +1.97 kB 🔴 +702 B 🔴 +625 B
assets/layout-BhmMoK9L.js (removed) 500 B 🟢 -500 B 🟢 -308 B 🟢 -265 B
assets/layout-MvRmLoAK.js (new) 500 B 🔴 +500 B 🔴 +309 B 🔴 +266 B

Status: 11 added / 11 removed

Panels & Settings — 462 kB (baseline 440 kB) • 🔴 +21.6 kB

Configuration panels, inspectors, and settings screens

File Before After Δ Raw Δ Gzip Δ Brotli
assets/settings-CcQit-ab.js (removed) 38.3 kB 🟢 -38.3 kB 🟢 -9.22 kB 🟢 -7.3 kB
assets/settings-DxaZVjyr.js (new) 38.3 kB 🔴 +38.3 kB 🔴 +9.22 kB 🔴 +7.34 kB
assets/settings-C_UvTZu3.js (removed) 34.3 kB 🟢 -34.3 kB 🟢 -7.93 kB 🟢 -6.58 kB
assets/settings-CgtWExMH.js (new) 34.3 kB 🔴 +34.3 kB 🔴 +7.93 kB 🔴 +6.58 kB
assets/settings-BTc5qzP7.js (removed) 32.1 kB 🟢 -32.1 kB 🟢 -7.68 kB 🟢 -6.22 kB
assets/settings-jUaSxrVP.js (new) 32.1 kB 🔴 +32.1 kB 🔴 +7.68 kB 🔴 +6.22 kB
assets/settings-CgjSbLJ8.js (removed) 31.2 kB 🟢 -31.2 kB 🟢 -8.08 kB 🟢 -6.72 kB
assets/settings-ootI-8ie.js (new) 31.2 kB 🔴 +31.2 kB 🔴 +8.08 kB 🔴 +6.72 kB
assets/settings-69BUKeP-.js (removed) 30.4 kB 🟢 -30.4 kB 🟢 -7.76 kB 🟢 -6.72 kB
assets/settings-hFnVImU9.js (new) 30.4 kB 🔴 +30.4 kB 🔴 +7.75 kB 🔴 +6.72 kB
assets/WorkspacePanel-Dz_QtSDZ.js (new) 29.8 kB 🔴 +29.8 kB 🔴 +5.9 kB 🔴 +5.14 kB
assets/settings-CKVLGCg3.js (new) 29.6 kB 🔴 +29.6 kB 🔴 +7.62 kB 🔴 +6.63 kB
assets/settings-ZeWer40V.js (removed) 29.6 kB 🟢 -29.6 kB 🟢 -7.62 kB 🟢 -6.63 kB
assets/settings-BUjoxI2p.js (removed) 29.5 kB 🟢 -29.5 kB 🟢 -7.49 kB 🟢 -6.33 kB
assets/settings-DqRBPTem.js (new) 29.5 kB 🔴 +29.5 kB 🔴 +7.49 kB 🔴 +6.35 kB
assets/settings-DFFGGhiG.js (new) 28.9 kB 🔴 +28.9 kB 🔴 +7.36 kB 🔴 +6.32 kB
assets/settings-JHh6sqSp.js (removed) 28.9 kB 🟢 -28.9 kB 🟢 -7.36 kB 🟢 -6.33 kB
assets/settings-BJ0CiNVJ.js (removed) 28.6 kB 🟢 -28.6 kB 🟢 -7.71 kB 🟢 -6.38 kB
assets/settings-DhYavwEF.js (new) 28.6 kB 🔴 +28.6 kB 🔴 +7.71 kB 🔴 +6.37 kB
assets/settings-CuiAsdhp.js (removed) 25.9 kB 🟢 -25.9 kB 🟢 -7.64 kB 🟢 -6.16 kB
assets/settings-tU5WPebQ.js (new) 25.9 kB 🔴 +25.9 kB 🔴 +7.64 kB 🔴 +6.16 kB
assets/settings-9p7XO4_-.js (new) 25.2 kB 🔴 +25.2 kB 🔴 +7.41 kB 🔴 +5.76 kB
assets/settings-k3WiAtK4.js (removed) 25.2 kB 🟢 -25.2 kB 🟢 -7.41 kB 🟢 -5.76 kB
assets/LegacyCreditsPanel-Dhev4Lee.js (new) 23.8 kB 🔴 +23.8 kB 🔴 +5.94 kB 🔴 +5.22 kB
assets/LegacyCreditsPanel-xdhWXlSn.js (removed) 23.8 kB 🟢 -23.8 kB 🟢 -5.95 kB 🟢 -5.22 kB
assets/SubscriptionPanel-DEwEUI6r.js (new) 20.9 kB 🔴 +20.9 kB 🔴 +5.01 kB 🔴 +4.43 kB
assets/SubscriptionPanel-DJlwaJLV.js (removed) 20.9 kB 🟢 -20.9 kB 🟢 -5.01 kB 🟢 -4.41 kB
assets/KeybindingPanel-CgpBY1_6.js (new) 14.2 kB 🔴 +14.2 kB 🔴 +3.73 kB 🔴 +3.31 kB
assets/KeybindingPanel-hmPAUouv.js (removed) 14.2 kB 🟢 -14.2 kB 🟢 -3.73 kB 🟢 -3.31 kB
assets/AboutPanel-BGkb7It6.js (removed) 10.8 kB 🟢 -10.8 kB 🟢 -2.68 kB 🟢 -2.43 kB
assets/AboutPanel-CNNnOzsG.js (new) 10.8 kB 🔴 +10.8 kB 🔴 +2.68 kB 🔴 +2.42 kB
assets/ExtensionPanel-B7c1aZvc.js (removed) 10.2 kB 🟢 -10.2 kB 🟢 -2.71 kB 🟢 -2.4 kB
assets/ExtensionPanel-BpF4Y4wR.js (new) 10.2 kB 🔴 +10.2 kB 🔴 +2.71 kB 🔴 +2.4 kB
assets/WorkspacePanel-XzDQmuYz.js (removed) 8.17 kB 🟢 -8.17 kB 🟢 -2.31 kB 🟢 -2.08 kB
assets/ServerConfigPanel-CgcGbh64.js (new) 7.23 kB 🔴 +7.23 kB 🔴 +2.17 kB 🔴 +1.94 kB
assets/ServerConfigPanel-IMmrJhJl.js (removed) 7.23 kB 🟢 -7.23 kB 🟢 -2.16 kB 🟢 -1.94 kB
assets/UserPanel-BPhMT7Yz.js (new) 6.58 kB 🔴 +6.58 kB 🔴 +1.91 kB 🔴 +1.67 kB
assets/UserPanel-vwZ7ny7Z.js (removed) 6.58 kB 🟢 -6.58 kB 🟢 -1.9 kB 🟢 -1.67 kB
assets/config-BOw9pAVF.js (removed) 1.16 kB 🟢 -1.16 kB 🟢 -610 B 🟢 -536 B
assets/config-dcWWMIR8.js (new) 1.15 kB 🔴 +1.15 kB 🔴 +605 B 🔴 +529 B
assets/refreshRemoteConfig-BJV4Gq15.js (removed) 1.14 kB 🟢 -1.14 kB 🟢 -522 B 🟢 -450 B
assets/refreshRemoteConfig-D_7BKjXu.js (new) 1.14 kB 🔴 +1.14 kB 🔴 +524 B 🔴 +487 B
assets/cloudRemoteConfig-BLhvQ_AK.js (removed) 1.11 kB 🟢 -1.11 kB 🟢 -509 B 🟢 -441 B
assets/cloudRemoteConfig-voRih67i.js (new) 1.11 kB 🔴 +1.11 kB 🔴 +511 B 🔴 +439 B
assets/remoteConfig-BufOGckh.js (removed) 536 B 🟢 -536 B 🟢 -312 B 🟢 -279 B
assets/remoteConfig-w1E3DdAQ.js (new) 536 B 🔴 +536 B 🔴 +314 B 🔴 +278 B
assets/refreshRemoteConfig-CMlh1A8M.js (new) 169 B 🔴 +169 B 🔴 +108 B 🔴 +106 B
assets/refreshRemoteConfig-CUYsxEDn.js (removed) 169 B 🟢 -169 B 🟢 -108 B 🟢 -111 B

Status: 24 added / 24 removed

User & Accounts — 3.94 kB (baseline 3.94 kB) • ⚪ 0 B

Authentication, profile, and account management bundles

File Before After Δ Raw Δ Gzip Δ Brotli
assets/auth-BojpFskt.js (removed) 3.54 kB 🟢 -3.54 kB 🟢 -1.23 kB 🟢 -1.06 kB
assets/auth-CVazCZA-.js (new) 3.54 kB 🔴 +3.54 kB 🔴 +1.24 kB 🔴 +1.06 kB
assets/firebaseAuthStore-BpF5eJQ6.js (removed) 217 B 🟢 -217 B 🟢 -137 B 🟢 -138 B
assets/firebaseAuthStore-GOt5OKC-.js (new) 217 B 🔴 +217 B 🔴 +137 B 🔴 +131 B
assets/auth-BteezRia.js (new) 178 B 🔴 +178 B 🔴 +142 B 🔴 +145 B
assets/auth-D-codBK9.js (removed) 178 B 🟢 -178 B 🟢 -141 B 🟢 -130 B

Status: 3 added / 3 removed

Editors & Dialogs — 2.83 kB (baseline 2.83 kB) • ⚪ 0 B

Modals, dialogs, drawers, and in-app editors

File Before After Δ Raw Δ Gzip Δ Brotli
assets/useSubscriptionDialog-mZTLV2lj.js (new) 2.65 kB 🔴 +2.65 kB 🔴 +1.25 kB 🔴 +1.12 kB
assets/useSubscriptionDialog-O4eeHLKJ.js (removed) 2.65 kB 🟢 -2.65 kB 🟢 -1.26 kB 🟢 -1.11 kB
assets/useSubscriptionDialog-BGSxYE-X.js (new) 179 B 🔴 +179 B 🔴 +110 B 🔴 +98 B
assets/useSubscriptionDialog-Bt96GXGw.js (removed) 179 B 🟢 -179 B 🟢 -110 B 🟢 -98 B

Status: 2 added / 2 removed

UI Components — 33.7 kB (baseline 33.7 kB) • 🔴 +48 B

Reusable component library chunks

File Before After Δ Raw Δ Gzip Δ Brotli
assets/ComfyQueueButton-BLw7wcP5.js (removed) 9.52 kB 🟢 -9.52 kB 🟢 -2.69 kB 🟢 -2.42 kB
assets/ComfyQueueButton-CWABpIYo.js (new) 9.52 kB 🔴 +9.52 kB 🔴 +2.69 kB 🔴 +2.41 kB
assets/TopbarBadge-DHZYSmi1.js (new) 8.36 kB 🔴 +8.36 kB 🔴 +1.98 kB 🔴 +1.77 kB
assets/TopbarBadge-DigdimM2.js (removed) 8.36 kB 🟢 -8.36 kB 🟢 -1.97 kB 🟢 -1.76 kB
assets/SubscribeButton-B7Q6wFOD.js (new) 4.63 kB 🔴 +4.63 kB 🔴 +1.57 kB 🔴 +1.39 kB
assets/SubscribeButton-CLqPd7YR.js (removed) 4.63 kB 🟢 -4.63 kB 🟢 -1.57 kB 🟢 -1.39 kB
assets/Button-B9mYP1x0.js (new) 3.82 kB 🔴 +3.82 kB 🔴 +1.39 kB 🔴 +1.23 kB
assets/Button-CFyWbSrm.js (removed) 3.77 kB 🟢 -3.77 kB 🟢 -1.37 kB 🟢 -1.21 kB
assets/WidgetButton-BFtcBv-z.js (new) 2.41 kB 🔴 +2.41 kB 🔴 +980 B 🔴 +885 B
assets/WidgetButton-ZS6Pl-ac.js (removed) 2.41 kB 🟢 -2.41 kB 🟢 -979 B 🟢 -888 B
assets/CloudBadge-Ccqq3nf7.js (removed) 1.85 kB 🟢 -1.85 kB 🟢 -721 B 🟢 -641 B
assets/CloudBadge-NKX5XTLb.js (new) 1.85 kB 🔴 +1.85 kB 🔴 +724 B 🔴 +645 B
assets/UserAvatar-CIuPULbC.js (new) 1.73 kB 🔴 +1.73 kB 🔴 +726 B 🔴 +637 B
assets/UserAvatar-wKdSR6pn.js (removed) 1.73 kB 🟢 -1.73 kB 🟢 -726 B 🟢 -635 B
assets/cloudFeedbackTopbarButton-4Y7rCsOe.js (removed) 1.24 kB 🟢 -1.24 kB 🟢 -676 B 🟢 -578 B
assets/cloudFeedbackTopbarButton-B0wjliOt.js (new) 1.24 kB 🔴 +1.24 kB 🔴 +676 B 🔴 +576 B
assets/ComfyQueueButton-B6LY_XL5.js (new) 181 B 🔴 +181 B 🔴 +118 B 🔴 +114 B
assets/ComfyQueueButton-DYb6t-zA.js (removed) 181 B 🟢 -181 B 🟢 -118 B 🟢 -113 B

Status: 9 added / 9 removed

Data & Services — 3.18 MB (baseline 3.17 MB) • 🔴 +9.03 kB

Stores, services, APIs, and repositories

File Before After Δ Raw Δ Gzip Δ Brotli
assets/dialogService-mMlqhE8o.js (new) 1.99 MB 🔴 +1.99 MB 🔴 +422 kB 🔴 +322 kB
assets/dialogService-D0ZCVCfS.js (removed) 1.99 MB 🟢 -1.99 MB 🟢 -420 kB 🟢 -321 kB
assets/api-D6xVxgqk.js (new) 1.16 MB 🔴 +1.16 MB 🔴 +242 kB 🔴 +188 kB
assets/api-DOkJDpga.js (removed) 1.16 MB 🟢 -1.16 MB 🟢 -241 kB 🟢 -187 kB
assets/releaseStore-CVhJVtbi.js (new) 8.91 kB 🔴 +8.91 kB 🔴 +2.4 kB 🔴 +2.12 kB
assets/releaseStore-Dr6k3giI.js (removed) 8.91 kB 🟢 -8.91 kB 🟢 -2.4 kB 🟢 -2.12 kB
assets/keybindingService-CZBe1bV0.js (removed) 6.78 kB 🟢 -6.78 kB 🟢 -1.74 kB 🟢 -1.51 kB
assets/keybindingService-DEBDIegr.js (new) 6.78 kB 🔴 +6.78 kB 🔴 +1.74 kB 🔴 +1.51 kB
assets/serverConfigStore-B2LzN8g1.js (new) 2.64 kB 🔴 +2.64 kB 🔴 +879 B 🔴 +778 B
assets/serverConfigStore-Brqsn51D.js (removed) 2.64 kB 🟢 -2.64 kB 🟢 -876 B 🟢 -780 B
assets/userStore-CgkBs5Mb.js (removed) 2.16 kB 🟢 -2.16 kB 🟢 -812 B 🟢 -722 B
assets/userStore-tOCoxBCy.js (new) 2.16 kB 🔴 +2.16 kB 🔴 +813 B 🔴 +724 B
assets/audioService-DRNrRDSL.js (removed) 2.03 kB 🟢 -2.03 kB 🟢 -928 B 🟢 -822 B
assets/audioService-JQ8VSTpl.js (new) 2.03 kB 🔴 +2.03 kB 🔴 +932 B 🔴 +822 B
assets/teamWorkspaceStore-CFIt5kkV.js (new) 165 B 🔴 +165 B 🔴 +123 B 🔴 +106 B
assets/teamWorkspaceStore-D6ANjbIq.js (removed) 165 B 🟢 -165 B 🟢 -123 B 🟢 -115 B
assets/releaseStore-BOoM_38t.js (removed) 140 B 🟢 -140 B 🟢 -106 B 🟢 -106 B
assets/releaseStore-Db7QxTD7.js (new) 140 B 🔴 +140 B 🔴 +106 B 🔴 +107 B

Status: 9 added / 9 removed

Utilities & Hooks — 25.2 kB (baseline 24 kB) • 🔴 +1.19 kB

Helpers, composables, and utility bundles

File Before After Δ Raw Δ Gzip Δ Brotli
assets/colorUtil-8brfHtOx.js (new) 7.2 kB 🔴 +7.2 kB 🔴 +2.22 kB 🔴 +1.96 kB
assets/colorUtil-CCK-3Mky.js (removed) 7.2 kB 🟢 -7.2 kB 🟢 -2.22 kB 🟢 -1.95 kB
assets/useErrorHandling-BUTj1i59.js (new) 5.08 kB 🔴 +5.08 kB 🔴 +1.5 kB 🔴 +1.3 kB
assets/useErrorHandling-xE1O13Uj.js (removed) 5.08 kB 🟢 -5.08 kB 🟢 -1.5 kB 🟢 -1.31 kB
assets/useWorkspaceUI-DttqffY5.js (new) 3.42 kB 🔴 +3.42 kB 🔴 +973 B 🔴 +845 B
assets/useWorkspaceUI-DBFe5H7D.js (removed) 2.25 kB 🟢 -2.25 kB 🟢 -757 B 🟢 -692 B
assets/useSubscriptionActions-Bpnuocyv.js (new) 2.22 kB 🔴 +2.22 kB 🔴 +867 B 🔴 +761 B
assets/useSubscriptionActions-CKxRqoVq.js (removed) 2.22 kB 🟢 -2.22 kB 🟢 -866 B 🟢 -762 B
assets/subscriptionCheckoutUtil-bXzaWZ_k.js (new) 2 kB 🔴 +2 kB 🔴 +863 B 🔴 +751 B
assets/subscriptionCheckoutUtil-B5qtdqx_.js (removed) 1.98 kB 🟢 -1.98 kB 🟢 -862 B 🟢 -744 B
assets/markdownRendererUtil-DBMaRy6q.js (new) 1.78 kB 🔴 +1.78 kB 🔴 +886 B 🔴 +773 B
assets/markdownRendererUtil-lUeVlENO.js (removed) 1.78 kB 🟢 -1.78 kB 🟢 -883 B 🟢 -776 B
assets/useSubscriptionCredits-BB-bU1AQ.js (removed) 1.39 kB 🟢 -1.39 kB 🟢 -597 B 🟢 -528 B
assets/useSubscriptionCredits-Dc14IMgj.js (new) 1.39 kB 🔴 +1.39 kB 🔴 +597 B 🔴 +522 B
assets/audioUtils-BEV9kslB.js (new) 970 B 🔴 +970 B 🔴 +547 B 🔴 +460 B
assets/audioUtils-HeD3YZ9Q.js (removed) 970 B 🟢 -970 B 🟢 -549 B 🟢 -485 B
assets/tailwindUtil-CPQi43_-.js (removed) 488 B 🟢 -488 B 🟢 -292 B 🟢 -266 B
assets/tailwindUtil-Hy0jY5OA.js (new) 488 B 🔴 +488 B 🔴 +298 B 🔴 +267 B
assets/useCurrentUser-DUcEet-u.js (new) 145 B 🔴 +145 B 🔴 +114 B 🔴 +99 B
assets/useCurrentUser-v_KfBCDB.js (removed) 145 B 🟢 -145 B 🟢 -114 B 🟢 -106 B
assets/_plugin-vue_export-helper-DLRTaeJK.js 467 B 467 B ⚪ 0 B ⚪ 0 B ⚪ 0 B

Status: 10 added / 10 removed

Vendor & Third-Party — 10.7 MB (baseline 10.7 MB) • ⚪ 0 B

External libraries and shared vendor chunks

File Before After Δ Raw Δ Gzip Δ Brotli
assets/vendor-other-9wjwspTM.js (removed) 4.1 MB 🟢 -4.1 MB 🟢 -867 kB 🟢 -673 kB
assets/vendor-other-Dk4vQph5.js (new) 4.1 MB 🔴 +4.1 MB 🔴 +867 kB 🔴 +673 kB
assets/vendor-primevue-BK91gQps.js (new) 3.04 MB 🔴 +3.04 MB 🔴 +560 kB 🔴 +329 kB
assets/vendor-primevue-DfDkDSrj.js (removed) 3.04 MB 🟢 -3.04 MB 🟢 -560 kB 🟢 -328 kB
assets/vendor-chart-DdBDBwvF.js (new) 408 kB 🔴 +408 kB 🔴 +98.6 kB 🔴 +80.6 kB
assets/vendor-chart-OCOwIMuw.js (removed) 408 kB 🟢 -408 kB 🟢 -98.6 kB 🟢 -80.6 kB
assets/vendor-reka-ui-Bh_PdEOO.js (new) 256 kB 🔴 +256 kB 🔴 +50.6 kB 🔴 +42.1 kB
assets/vendor-reka-ui-v0jgPg5n.js (removed) 256 kB 🟢 -256 kB 🟢 -50.6 kB 🟢 -42.1 kB
assets/vendor-vue-CRvEKkVc.js (removed) 13.6 kB 🟢 -13.6 kB 🟢 -4.37 kB 🟢 -3.92 kB
assets/vendor-vue-DMjbEcx7.js (new) 13.6 kB 🔴 +13.6 kB 🔴 +4.38 kB 🔴 +3.92 kB
assets/vendor-three-BKpliY5_.js 1.83 MB 1.83 MB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-tiptap-BFRBT3RT.js 650 kB 650 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-xterm-PDw3y6Aq.js 398 kB 398 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B

Status: 5 added / 5 removed

Other — 6.43 MB (baseline 6.42 MB) • 🔴 +14.6 kB

Bundles that do not match a named category

File Before After Δ Raw Δ Gzip Δ Brotli
assets/nodeDefs-CcpUorR_.js (new) 440 kB 🔴 +440 kB 🔴 +68.2 kB 🔴 +43.9 kB
assets/nodeDefs-h_jQ7nTe.js (removed) 440 kB 🟢 -440 kB 🟢 -68.2 kB 🟢 -43.9 kB
assets/nodeDefs-CG0lNwST.js (removed) 405 kB 🟢 -405 kB 🟢 -62 kB 🟢 -40.9 kB
assets/nodeDefs-S8kvmuqz.js (new) 405 kB 🔴 +405 kB 🔴 +62 kB 🔴 +40.9 kB
assets/nodeDefs-BtmCD4Ap.js (removed) 405 kB 🟢 -405 kB 🟢 -64 kB 🟢 -42.3 kB
assets/nodeDefs-CF18koQc.js (new) 405 kB 🔴 +405 kB 🔴 +64 kB 🔴 +42.1 kB
assets/nodeDefs-BMK6BIfT.js (new) 376 kB 🔴 +376 kB 🔴 +62.2 kB 🔴 +40.6 kB
assets/nodeDefs-CvpOKsAt.js (removed) 376 kB 🟢 -376 kB 🟢 -62.2 kB 🟢 -40.6 kB
assets/nodeDefs-CPmZncLJ.js (removed) 366 kB 🟢 -366 kB 🟢 -60.9 kB 🟢 -40.3 kB
assets/nodeDefs-DsEMLGXe.js (new) 366 kB 🔴 +366 kB 🔴 +60.9 kB 🔴 +40.3 kB
assets/nodeDefs-BoAwAPDu.js (removed) 363 kB 🟢 -363 kB 🟢 -60.8 kB 🟢 -41.6 kB
assets/nodeDefs-BRyEXRs7.js (new) 363 kB 🔴 +363 kB 🔴 +60.8 kB 🔴 +41.6 kB
assets/nodeDefs-DT5ppw0r.js (new) 363 kB 🔴 +363 kB 🔴 +59.5 kB 🔴 +40.8 kB
assets/nodeDefs-DU5Y_lx_.js (removed) 363 kB 🟢 -363 kB 🟢 -59.5 kB 🟢 -40.8 kB
assets/nodeDefs-BhCOuEba.js (new) 359 kB 🔴 +359 kB 🔴 +58.4 kB 🔴 +40 kB
assets/nodeDefs-DDuiU_FH.js (removed) 359 kB 🟢 -359 kB 🟢 -58.4 kB 🟢 -40 kB
assets/nodeDefs-BvPKtkwY.js (removed) 356 kB 🟢 -356 kB 🟢 -57.6 kB 🟢 -39.4 kB
assets/nodeDefs-DPQWojsE.js (new) 356 kB 🔴 +356 kB 🔴 +57.6 kB 🔴 +39.4 kB
assets/nodeDefs-B6agiPX1.js (removed) 336 kB 🟢 -336 kB 🟢 -59.8 kB 🟢 -39.3 kB
assets/nodeDefs-CHsbMBTq.js (new) 336 kB 🔴 +336 kB 🔴 +59.8 kB 🔴 +39.3 kB
assets/nodeDefs-Bwv2VlMI.js (removed) 334 kB 🟢 -334 kB 🟢 -58.6 kB 🟢 -38 kB
assets/nodeDefs-DbcqYeAO.js (new) 334 kB 🔴 +334 kB 🔴 +58.6 kB 🔴 +38 kB
assets/core-BQgMVUHJ.js (removed) 178 kB 🟢 -178 kB 🟢 -43 kB 🟢 -35.9 kB
assets/core-ByqAJYC6.js (new) 178 kB 🔴 +178 kB 🔴 +43 kB 🔴 +35.9 kB
assets/main-4lckbWsV.js (removed) 168 kB 🟢 -168 kB 🟢 -44.6 kB 🟢 -35.3 kB
assets/main-D29Ga1Vv.js (new) 168 kB 🔴 +168 kB 🔴 +44.6 kB 🔴 +35.3 kB
assets/main-CpFx4JJ1.js (new) 151 kB 🔴 +151 kB 🔴 +40.1 kB 🔴 +32.4 kB
assets/main-Kfo8Pw0z.js (removed) 151 kB 🟢 -151 kB 🟢 -40.1 kB 🟢 -32.4 kB
assets/main-CsHi8V26.js (new) 146 kB 🔴 +146 kB 🔴 +40.3 kB 🔴 +32.1 kB
assets/main-DbXoj4jb.js (removed) 146 kB 🟢 -146 kB 🟢 -40.3 kB 🟢 -32.1 kB
assets/main-DVNwcedr.js (new) 140 kB 🔴 +140 kB 🔴 +40.1 kB 🔴 +32 kB
assets/main-e04f9KyF.js (removed) 140 kB 🟢 -140 kB 🟢 -40.1 kB 🟢 -32.1 kB
assets/main-C_hdRFbZ.js (new) 129 kB 🔴 +129 kB 🔴 +38.8 kB 🔴 +32.5 kB
assets/main-DsM3PqIA.js (removed) 129 kB 🟢 -129 kB 🟢 -38.8 kB 🟢 -32.5 kB
assets/main-BMqZNEiD.js (removed) 126 kB 🟢 -126 kB 🟢 -38.6 kB 🟢 -31.3 kB
assets/main-CfGGFRzU.js (new) 126 kB 🔴 +126 kB 🔴 +38.6 kB 🔴 +31.3 kB
assets/main-CZoshpks.js (removed) 124 kB 🟢 -124 kB 🟢 -37.9 kB 🟢 -31.6 kB
assets/main-WxzUNrgR.js (new) 124 kB 🔴 +124 kB 🔴 +37.9 kB 🔴 +31.6 kB
assets/main-CjO320gS.js (removed) 122 kB 🟢 -122 kB 🟢 -37.8 kB 🟢 -31.6 kB
assets/main-CvOyqCL5.js (new) 122 kB 🔴 +122 kB 🔴 +37.8 kB 🔴 +31.6 kB
assets/main-DTZ3Cq9q.js (new) 122 kB 🔴 +122 kB 🔴 +37.3 kB 🔴 +31.3 kB
assets/main-DxcvT18G.js (removed) 122 kB 🟢 -122 kB 🟢 -37.3 kB 🟢 -31.3 kB
assets/main-BKNTTNlG.js (new) 109 kB 🔴 +109 kB 🔴 +37.3 kB 🔴 +29.7 kB
assets/main-CVnZPXeK.js (removed) 109 kB 🟢 -109 kB 🟢 -37.3 kB 🟢 -29.7 kB
assets/main-Cro09VYh.js (new) 109 kB 🔴 +109 kB 🔴 +37.3 kB 🔴 +29.4 kB
assets/main-P2E7wvoy.js (removed) 109 kB 🟢 -109 kB 🟢 -37.3 kB 🟢 -29.4 kB
assets/WidgetSelect-BY60gWee.js (removed) 50.7 kB 🟢 -50.7 kB 🟢 -11.2 kB 🟢 -9.73 kB
assets/WidgetSelect-DYYIrQ27.js (new) 50.7 kB 🔴 +50.7 kB 🔴 +11.2 kB 🔴 +9.75 kB
assets/Load3DControls-BpKvEnsQ.js (removed) 35.9 kB 🟢 -35.9 kB 🟢 -5.87 kB 🟢 -5.08 kB
assets/Load3DControls-SfhSIwL7.js (new) 35.9 kB 🔴 +35.9 kB 🔴 +5.87 kB 🔴 +5.09 kB
assets/SubscriptionRequiredDialogContent-BT3B3StZ.js (new) 28.7 kB 🔴 +28.7 kB 🔴 +6.78 kB 🔴 +5.91 kB
assets/SubscriptionRequiredDialogContent-Dz8JW4c9.js (removed) 28.7 kB 🟢 -28.7 kB 🟢 -6.78 kB 🟢 -5.91 kB
assets/CurrentUserPopoverWorkspace-BzxPW4ci.js (new) 22.1 kB 🔴 +22.1 kB 🔴 +4.95 kB 🔴 +4.38 kB
assets/CurrentUserPopoverWorkspace-Kjs9-4b2.js (removed) 21.6 kB 🟢 -21.6 kB 🟢 -4.84 kB 🟢 -4.3 kB
assets/Load3D-BmzUOhAX.js (removed) 20.9 kB 🟢 -20.9 kB 🟢 -4.59 kB 🟢 -4.02 kB
assets/Load3D-DdwyGZ_S.js (new) 20.9 kB 🔴 +20.9 kB 🔴 +4.59 kB 🔴 +4.02 kB
assets/commands-Dmj47WTl.js (new) 20.6 kB 🔴 +20.6 kB 🔴 +3.96 kB 🔴 +3.07 kB
assets/commands-DZxSdjFq.js (removed) 20.6 kB 🟢 -20.6 kB 🟢 -3.96 kB 🟢 -3.07 kB
assets/commands-BC_Q8we6.js (new) 19.3 kB 🔴 +19.3 kB 🔴 +3.68 kB 🔴 +2.83 kB
assets/commands-COWR9vVf.js (removed) 19.3 kB 🟢 -19.3 kB 🟢 -3.68 kB 🟢 -2.83 kB
assets/commands-CuidUnsD.js (new) 19.3 kB 🔴 +19.3 kB 🔴 +3.69 kB 🔴 +2.9 kB
assets/commands-fiTVBhhf.js (removed) 19.3 kB 🟢 -19.3 kB 🟢 -3.69 kB 🟢 -2.9 kB
assets/commands-aJXmdzJz.js (removed) 18.8 kB 🟢 -18.8 kB 🟢 -3.79 kB 🟢 -2.95 kB
assets/commands-B52_zgXW.js (new) 18.8 kB 🔴 +18.8 kB 🔴 +3.79 kB 🔴 +2.93 kB
assets/commands-BEOD_uzq.js (removed) 18.5 kB 🟢 -18.5 kB 🟢 -3.52 kB 🟢 -2.92 kB
assets/commands-D5OfZ3bv.js (new) 18.5 kB 🔴 +18.5 kB 🔴 +3.52 kB 🔴 +2.91 kB
assets/WidgetInputNumber-BryWcj7S.js (removed) 18.3 kB 🟢 -18.3 kB 🟢 -4.51 kB 🟢 -4.02 kB
assets/WidgetInputNumber-CFLnXb-m.js (new) 18.3 kB 🔴 +18.3 kB 🔴 +4.51 kB 🔴 +4.02 kB
assets/WidgetRecordAudio-D_soN8g7.js (removed) 18.2 kB 🟢 -18.2 kB 🟢 -4.96 kB 🟢 -4.43 kB
assets/WidgetRecordAudio-DMEUQkWl.js (new) 18.2 kB 🔴 +18.2 kB 🔴 +4.96 kB 🔴 +4.43 kB
assets/SubscriptionPanelContentWorkspace-Buibhr7J.js (new) 18.2 kB 🔴 +18.2 kB 🔴 +4.48 kB 🔴 +3.9 kB
assets/commands-Dln4wOTe.js (removed) 18 kB 🟢 -18 kB 🟢 -3.51 kB 🟢 -2.9 kB
assets/commands-MVcGSIbM.js (new) 18 kB 🔴 +18 kB 🔴 +3.51 kB 🔴 +2.9 kB
assets/commands-BRKb82BA.js (removed) 18 kB 🟢 -18 kB 🟢 -3.4 kB 🟢 -2.76 kB
assets/commands-K3soE7da.js (new) 18 kB 🔴 +18 kB 🔴 +3.4 kB 🔴 +2.78 kB
assets/commands-BUGVA_C8.js (removed) 17.9 kB 🟢 -17.9 kB 🟢 -3.36 kB 🟢 -2.77 kB
assets/commands-CgVf6wUK.js (new) 17.9 kB 🔴 +17.9 kB 🔴 +3.36 kB 🔴 +2.77 kB
assets/commands-BZlNQPg2.js (new) 17.8 kB 🔴 +17.8 kB 🔴 +3.65 kB 🔴 +2.84 kB
assets/commands-DOKSrHX8.js (removed) 17.8 kB 🟢 -17.8 kB 🟢 -3.65 kB 🟢 -2.84 kB
assets/SubscriptionPanelContentWorkspace-C4i9EQmk.js (removed) 17.7 kB 🟢 -17.7 kB 🟢 -4.35 kB 🟢 -3.78 kB
assets/commands-BiO29O5L.js (removed) 17.2 kB 🟢 -17.2 kB 🟢 -3.57 kB 🟢 -2.73 kB
assets/commands-DUK2nLuH.js (new) 17.2 kB 🔴 +17.2 kB 🔴 +3.57 kB 🔴 +2.73 kB
assets/WidgetImageCrop-Bvnk2toF.js (new) 17.1 kB 🔴 +17.1 kB 🔴 +4.14 kB 🔴 +3.63 kB
assets/WidgetImageCrop-qAq8yuQk.js (removed) 17.1 kB 🟢 -17.1 kB 🟢 -4.14 kB 🟢 -3.63 kB
assets/commands-27PChCGl.js (new) 17 kB 🔴 +17 kB 🔴 +3.54 kB 🔴 +2.64 kB
assets/commands-DKxZF1hp.js (removed) 17 kB 🟢 -17 kB 🟢 -3.54 kB 🟢 -2.64 kB
assets/PanelTemplate-B-8bsWTI.js (removed) 16.2 kB 🟢 -16.2 kB 🟢 -5.45 kB 🟢 -4.79 kB
assets/PanelTemplate-BJJeFrmw.js (new) 16.2 kB 🔴 +16.2 kB 🔴 +5.45 kB 🔴 +4.79 kB
assets/LazyImage-Cb66OgLx.js (removed) 14.1 kB 🟢 -14.1 kB 🟢 -4 kB 🟢 -3.54 kB
assets/LazyImage-rWQpxwnM.js (new) 14.1 kB 🔴 +14.1 kB 🔴 +4 kB 🔴 +3.54 kB
assets/AudioPreviewPlayer-2AH2yofS.js (removed) 10.8 kB 🟢 -10.8 kB 🟢 -2.97 kB 🟢 -2.65 kB
assets/AudioPreviewPlayer-D9fwmSQG.js (new) 10.8 kB 🔴 +10.8 kB 🔴 +2.97 kB 🔴 +2.65 kB
assets/InviteMemberDialogContent-DJdxOlvm.js (new) 8.36 kB 🔴 +8.36 kB 🔴 +2.51 kB 🔴 +2.17 kB
assets/WidgetWithControl-C5VurCxo.js (removed) 8.02 kB 🟢 -8.02 kB 🟢 -2.65 kB 🟢 -2.38 kB
assets/WidgetWithControl-olcIAY-x.js (new) 8.02 kB 🔴 +8.02 kB 🔴 +2.64 kB 🔴 +2.38 kB
assets/CreateWorkspaceDialogContent-DoFbnX3c.js (new) 5.93 kB 🔴 +5.93 kB 🔴 +1.93 kB 🔴 +1.68 kB
assets/CreateWorkspaceDialogContent-DOnjUuRl.js (removed) 5.92 kB 🟢 -5.92 kB 🟢 -1.92 kB 🟢 -1.67 kB
assets/EditWorkspaceDialogContent-qr2O4Ffp.js (new) 5.7 kB 🔴 +5.7 kB 🔴 +1.88 kB 🔴 +1.65 kB
assets/EditWorkspaceDialogContent-2l19_tSt.js (removed) 5.69 kB 🟢 -5.69 kB 🟢 -1.87 kB 🟢 -1.64 kB
assets/ValueControlPopover-B6OgPV1a.js (new) 4.86 kB 🔴 +4.86 kB 🔴 +1.55 kB 🔴 +1.37 kB
assets/ValueControlPopover-DmaYahLa.js (removed) 4.86 kB 🟢 -4.86 kB 🟢 -1.54 kB 🟢 -1.37 kB
assets/WidgetBoundingBox-CgNPbPqq.js (new) 4.71 kB 🔴 +4.71 kB 🔴 +1.13 kB 🔴 +1.02 kB
assets/WidgetBoundingBox-KpJ-3L5b.js (removed) 4.71 kB 🟢 -4.71 kB 🟢 -1.13 kB 🟢 -991 B
assets/DeleteWorkspaceDialogContent-CFNwEldM.js (new) 4.59 kB 🔴 +4.59 kB 🔴 +1.56 kB 🔴 +1.35 kB
assets/DeleteWorkspaceDialogContent-LxMNxwSj.js (removed) 4.59 kB 🟢 -4.59 kB 🟢 -1.56 kB 🟢 -1.35 kB
assets/WidgetGalleria-DoosKHEY.js (removed) 4.57 kB 🟢 -4.57 kB 🟢 -1.57 kB 🟢 -1.42 kB
assets/WidgetGalleria-f72WaoQY.js (new) 4.57 kB 🔴 +4.57 kB 🔴 +1.57 kB 🔴 +1.42 kB
assets/LeaveWorkspaceDialogContent-BBtVv5m2.js (removed) 4.41 kB 🟢 -4.41 kB 🟢 -1.5 kB 🟢 -1.31 kB
assets/LeaveWorkspaceDialogContent-D0gHTbwz.js (new) 4.41 kB 🔴 +4.41 kB 🔴 +1.5 kB 🔴 +1.31 kB
assets/RemoveMemberDialogContent-BWIj6MRO.js (new) 4.38 kB 🔴 +4.38 kB 🔴 +1.45 kB 🔴 +1.27 kB
assets/RevokeInviteDialogContent-DtGer4jR.js (new) 4.29 kB 🔴 +4.29 kB 🔴 +1.47 kB 🔴 +1.29 kB
assets/Slider-BAEfKuro.js (new) 4.21 kB 🔴 +4.21 kB 🔴 +1.52 kB 🔴 +1.34 kB
assets/Slider-PbcJpAgB.js (removed) 4.21 kB 🟢 -4.21 kB 🟢 -1.52 kB 🟢 -1.34 kB
assets/WidgetImageCompare-D0Z-FLOB.js (removed) 3.79 kB 🟢 -3.79 kB 🟢 -1.28 kB 🟢 -1.12 kB
assets/WidgetImageCompare-Dd0p7l7V.js (new) 3.79 kB 🔴 +3.79 kB 🔴 +1.28 kB 🔴 +1.12 kB
assets/WidgetColorPicker-o5zzQJj-.js (removed) 3.71 kB 🟢 -3.71 kB 🟢 -1.38 kB 🟢 -1.25 kB
assets/WidgetColorPicker-x8L_rIAb.js (new) 3.71 kB 🔴 +3.71 kB 🔴 +1.38 kB 🔴 +1.25 kB
assets/WidgetTextarea-DJWb5MM7.js (removed) 3.52 kB 🟢 -3.52 kB 🟢 -1.33 kB 🟢 -1.17 kB
assets/WidgetTextarea-DwdEJ3mA.js (new) 3.52 kB 🔴 +3.52 kB 🔴 +1.33 kB 🔴 +1.17 kB
assets/preservedQueryNamespaces-D99dMmBP.js (removed) 3.23 kB 🟢 -3.23 kB 🟢 -1.07 kB 🟢 -930 B
assets/WidgetMarkdown-bP5F2-qx.js (new) 3.22 kB 🔴 +3.22 kB 🔴 +1.28 kB 🔴 +1.13 kB
assets/WidgetMarkdown-DKV1i22o.js (removed) 3.22 kB 🟢 -3.22 kB 🟢 -1.28 kB 🟢 -1.13 kB
assets/WidgetToggleSwitch-BIK7gguT.js (new) 3.08 kB 🔴 +3.08 kB 🔴 +1.19 kB 🔴 +1.06 kB
assets/WidgetToggleSwitch-CaP9JEnC.js (removed) 3.08 kB 🟢 -3.08 kB 🟢 -1.19 kB 🟢 -1.09 kB
assets/GlobalToast-1VtfcTBJ.js (removed) 3.05 kB 🟢 -3.05 kB 🟢 -1.1 kB 🟢 -948 B
assets/GlobalToast-BmloqLbC.js (new) 3.05 kB 🔴 +3.05 kB 🔴 +1.1 kB 🔴 +944 B
assets/SubscribeToRun-DStCWILg.js (new) 2.96 kB 🔴 +2.96 kB 🔴 +1.16 kB 🔴 +1.01 kB
assets/SubscribeToRun-ePuWbiNN.js (removed) 2.96 kB 🟢 -2.96 kB 🟢 -1.15 kB 🟢 -1.01 kB
assets/cloudSessionCookie-DSMbGj6j.js (new) 2.94 kB 🔴 +2.94 kB 🔴 +934 B 🔴 +799 B
assets/cloudSessionCookie-R4kUnrFx.js (removed) 2.94 kB 🟢 -2.94 kB 🟢 -935 B 🟢 -798 B
assets/MediaVideoTop-BhEl3YYq.js (removed) 2.84 kB 🟢 -2.84 kB 🟢 -1.06 kB 🟢 -916 B
assets/MediaVideoTop-bsLx-Wg6.js (new) 2.84 kB 🔴 +2.84 kB 🔴 +1.06 kB 🔴 +914 B
assets/WidgetChart-3mC7A_va.js (new) 2.79 kB 🔴 +2.79 kB 🔴 +1.06 kB 🔴 +962 B
assets/WidgetChart-Bdv_mOBE.js (removed) 2.79 kB 🟢 -2.79 kB 🟢 -1.05 kB 🟢 -964 B
assets/WidgetLayoutField-CZXFB71F.js (new) 2.61 kB 🔴 +2.61 kB 🔴 +1.01 kB 🔴 +892 B
assets/WidgetLayoutField-D80nWHnQ.js (removed) 2.61 kB 🟢 -2.61 kB 🟢 -1.01 kB 🟢 -892 B
assets/WidgetInputText-B1Jj_0fq.js (removed) 2.58 kB 🟢 -2.58 kB 🟢 -1.01 kB 🟢 -910 B
assets/WidgetInputText-DOFSeOZG.js (new) 2.58 kB 🔴 +2.58 kB 🔴 +1.01 kB 🔴 +912 B
assets/BaseViewTemplate-BuIsohLq.js (removed) 2.42 kB 🟢 -2.42 kB 🟢 -1.04 kB 🟢 -941 B
assets/BaseViewTemplate-FQtRKD3E.js (new) 2.42 kB 🔴 +2.42 kB 🔴 +1.05 kB 🔴 +943 B
assets/Media3DTop-0Ncely1V.js (removed) 2.38 kB 🟢 -2.38 kB 🟢 -1.01 kB 🟢 -880 B
assets/Media3DTop-8_T22Isd.js (new) 2.38 kB 🔴 +2.38 kB 🔴 +1.02 kB 🔴 +876 B
assets/MediaImageTop-1THG_DJM.js (new) 2.34 kB 🔴 +2.34 kB 🔴 +1 kB 🔴 +869 B
assets/MediaImageTop-355zzTFX.js (removed) 2.34 kB 🟢 -2.34 kB 🟢 -1 kB 🟢 -873 B
assets/MediaAudioTop-CBP8th-j.js (new) 2 kB 🔴 +2 kB 🔴 +881 B 🔴 +759 B
assets/MediaAudioTop-DAcbk8q3.js (removed) 2 kB 🟢 -2 kB 🟢 -883 B 🟢 -758 B
assets/CloudRunButtonWrapper-BDowYn02.js (removed) 1.79 kB 🟢 -1.79 kB 🟢 -642 B 🟢 -567 B
assets/CloudRunButtonWrapper-C6KcoBep.js (new) 1.79 kB 🔴 +1.79 kB 🔴 +642 B 🔴 +565 B
assets/auto-BmypP-XQ.js (new) 1.73 kB 🔴 +1.73 kB 🔴 +636 B 🔴 +580 B
assets/auto-ShMUXBDI.js (removed) 1.73 kB 🟢 -1.73 kB 🟢 -636 B 🟢 -562 B
assets/widgetPropFilter-BXooU3qU.js (removed) 1.31 kB 🟢 -1.31 kB 🟢 -569 B 🟢 -497 B
assets/widgetPropFilter-DOe9Bb1I.js (new) 1.31 kB 🔴 +1.31 kB 🔴 +568 B 🔴 +508 B
assets/cloudBadges-B5O8ioUI.js (removed) 1.08 kB 🟢 -1.08 kB 🟢 -538 B 🟢 -497 B
assets/cloudBadges-BKvGoBrU.js (new) 1.08 kB 🔴 +1.08 kB 🔴 +540 B 🔴 +504 B
assets/graphHasMissingNodes-DNbgGHqy.js (new) 1.06 kB 🔴 +1.06 kB 🔴 +461 B 🔴 +411 B
assets/graphHasMissingNodes-DVyXxxRS.js (removed) 1.06 kB 🟢 -1.06 kB 🟢 -459 B 🟢 -419 B
assets/cloudSubscription-CgYZmr_p.js (new) 976 B 🔴 +976 B 🔴 +465 B 🔴 +399 B
assets/cloudSubscription-DPeA-MsI.js (removed) 976 B 🟢 -976 B 🟢 -460 B 🟢 -399 B
assets/previousFullPath-B-XG7lU7.js (new) 838 B 🔴 +838 B 🔴 +415 B 🔴 +346 B
assets/previousFullPath-BgSMorGj.js (removed) 838 B 🟢 -838 B 🟢 -414 B 🟢 -379 B
assets/nightlyBadges-B8AH2ev_.js (new) 594 B 🔴 +594 B 🔴 +358 B 🔴 +312 B
assets/nightlyBadges-itcZr5qh.js (removed) 594 B 🟢 -594 B 🟢 -357 B 🟢 -308 B
assets/SubscriptionPanelContentWorkspace-DXHwGLrc.js (new) 266 B 🔴 +266 B 🔴 +136 B 🔴 +119 B
assets/SubscriptionPanelContentWorkspace-fF_uponM.js (removed) 266 B 🟢 -266 B 🟢 -136 B 🟢 -128 B
assets/WidgetBoundingBox-Bp7B7z1s.js (new) 186 B 🔴 +186 B 🔴 +119 B 🔴 +114 B
assets/WidgetBoundingBox-CTWd5ZRc.js (removed) 186 B 🟢 -186 B 🟢 -119 B 🟢 -119 B
assets/WidgetInputNumber-D43Cf1hi.js (removed) 186 B 🟢 -186 B 🟢 -119 B 🟢 -122 B
assets/WidgetInputNumber-tK6Z6kQB.js (new) 186 B 🔴 +186 B 🔴 +119 B 🔴 +121 B
assets/WidgetLegacy-C07LisUI.js (removed) 164 B 🟢 -164 B 🟢 -125 B 🟢 -112 B
assets/WidgetLegacy-DHWDek4J.js (new) 164 B 🔴 +164 B 🔴 +125 B 🔴 +103 B
assets/mixpanel.module-BiPjOPVW.js (new) 143 B 🔴 +143 B 🔴 +125 B 🔴 +107 B
assets/mixpanel.module-E4_aQOyn.js (removed) 143 B 🟢 -143 B 🟢 -125 B 🟢 -103 B
assets/Load3D-2FPT7usg.js (new) 131 B 🔴 +131 B 🔴 +107 B 🔴 +108 B
assets/Load3D-DDrULXdG.js (removed) 131 B 🟢 -131 B 🟢 -107 B 🟢 -107 B
assets/OBJLoader2WorkerModule-DTMpvldF.js 109 kB 109 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/rolldown-runtime-cVp-94Rc.js 1.96 kB 1.96 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/widget-CD3JnB1i.js 518 B 518 B ⚪ 0 B ⚪ 0 B ⚪ 0 B

Status: 88 added / 86 removed

@github-actions
Copy link

🔧 Auto-fixes Applied

This PR has been automatically updated to fix linting and formatting issues.

⚠️ Important: Your local branch is now behind. Run git pull before making additional changes to avoid conflicts.

Changes made:

  • ESLint auto-fixes
  • Oxfmt formatting

simula-r and others added 10 commits January 22, 2026 22:01
- Add edit, leave, and delete workspace dialog content components
- Add workspace settings panel with subscription content
- Fix subscription auth to use Firebase token instead of workspace token
- Disable subscribe button until billing BE is ready
- Hide edit option for personal workspaces in menu

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@simula-r simula-r added the preview-cpu Creates a preview ephemeral environment for this PR (CPU only) label Jan 23, 2026
@simula-r simula-r force-pushed the workspaces-4-members-invites branch from 7365b88 to 83a439f Compare January 23, 2026 08:19
@simula-r simula-r marked this pull request as ready for review January 24, 2026 07:48
@simula-r simula-r requested review from a team and pythongosssss as code owners January 24, 2026 07:48
@dosubot dosubot bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Jan 24, 2026
@simula-r simula-r added needs-backport Fix/change that needs to be cherry-picked to the current feature freeze branch cloud/1.37 labels Jan 24, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
src/components/dialog/content/workspace/CreateWorkspaceDialogContent.vue (1)

12-18: Consider using a common button component for the close icon.

Based on learnings, prefer the repo's common button components over plain HTML <button> elements for consistency and theming. An IconButton from src/components/button/ would be appropriate here.

src/platform/workspace/composables/useWorkspaceUI.ts (1)

81-94: Unused grid column values for personal workspace.

For personal workspaces, showMembersList, showPendingTab, and showDateColumn are all false, yet membersGridCols, pendingGridCols, and headerGridCols are still defined. While this doesn't cause issues, consider using empty strings or a sentinel value to make it explicit these aren't used, or document this is for type completeness.

src/components/topbar/CurrentUserPopoverWorkspace.vue (1)

76-78: Logic inconsistency between "Add Credits" button and displayed credits.

The "Add Credits" button visibility check uses isActiveSubscription && isWorkspaceSubscribed, but displayedCredits (lines 241-246) uses a different logic: personal workspaces check isActiveSubscription, while team workspaces check isWorkspaceSubscribed. This inconsistency could result in:

  • A personal workspace showing "0" credits when subscribed (because isWorkspaceSubscribed would be false)
  • The "Add Credits" button being hidden for a subscribed personal workspace

Consider aligning the condition:

🐛 Suggested fix
         <Button
-          v-if="isActiveSubscription && isWorkspaceSubscribed"
+          v-if="isPersonalWorkspace ? isActiveSubscription : isWorkspaceSubscribed"
           variant="secondary"
           size="sm"
src/components/dialog/GlobalDialog.vue (1)

7-12: Use cn() instead of array syntax for class binding.

Per coding guidelines, never use :class="[]" for merging class names; always use cn() from @/utils/tailwindUtil.

♻️ Suggested refactor
+import { cn } from '@/utils/tailwindUtil'

+const dialogClass = computed(() =>
+  cn(
+    'global-dialog',
+    item.key === 'global-settings' && teamWorkspacesEnabled.value
+      ? 'settings-dialog-workspace'
+      : ''
+  )
+)

Then in template:

-    :class="[
-      'global-dialog',
-      item.key === 'global-settings' && teamWorkspacesEnabled
-        ? 'settings-dialog-workspace'
-        : ''
-    ]"
+    :class="cn(
+      'global-dialog',
+      item.key === 'global-settings' && teamWorkspacesEnabled
+        ? 'settings-dialog-workspace'
+        : ''
+    )"
src/platform/cloud/subscription/components/SubscriptionPanelContentWorkspace.vue (1)

446-450: Style block may be necessary for PrimeVue deep styling.

While coding guidelines suggest avoiding <style> blocks, the :deep() selector is needed to override PrimeVue component internals. Consider adding a comment explaining why this override is necessary, or explore if this can be achieved via PrimeVue's passthrough (PT) system instead.

🤖 Fix all issues with AI agents
In `@src/components/dialog/content/setting/MembersPanelContent.vue`:
- Around line 457-462: The sort logic uses sortField (cast to 'inviteDate' |
'expiryDate') and calls .getTime() blindly, which can break when switching tabs
because PendingInvite doesn’t have joinDate; update the tab-switch or sort logic
to avoid this: either reset sortField to a valid pending-invite field when
current view changes (watch the tab state and set sortField to a safe default),
or guard the comparator inside the result.sort (check that a[field] and b[field]
exist and fall back to 0 or a stable tie-breaker) so .getTime() is never called
on undefined; look for symbols sortField, sortDirection, result.sort (and the
computed/filteredPendingInvites or filteredMembers computed) to apply the fix.
- Around line 324-329: Replace the plain HTML <button> with the project's common
Button component to follow design-system conventions: import the shared Button
(e.g., from src/components/button or src/components/ui/button) into
MembersPanelContent.vue and replace the element that calls handleCreateWorkspace
(the template button bound to `@click`="handleCreateWorkspace" and text
$t('workspacePanel.members.createNewWorkspace')) with that Button component,
preserving the click handler, translation key, and existing classes/props as
appropriate (use the Button's props for variant/underline styling instead of raw
classes).

In `@src/components/dialog/content/setting/WorkspacePanelContent.vue`:
- Around line 233-237: The onMounted callback calls async functions
fetchMembers() and fetchPendingInvites() without handling rejections; update the
onMounted(() => { ... }) block (which also calls setActiveTab(defaultTab)) to
handle async errors by either awaiting those calls inside an async function with
try/catch or chaining .catch(...) on each promise and logging or otherwise
handling failures (e.g., process errors via a local logger or show a user-facing
error state). Ensure you reference and update the onMounted callback and the
invocations of fetchMembers and fetchPendingInvites so no unhandled promise
rejections occur.

In `@src/components/dialog/content/workspace/CreateWorkspaceDialogContent.vue`:
- Around line 80-84: Extract the duplicated workspace name validation into a
shared utility: create src/platform/workspace/utils/workspaceValidation.ts
exporting WORKSPACE_NAME_REGEX, WORKSPACE_NAME_MIN_LENGTH,
WORKSPACE_NAME_MAX_LENGTH and a function isValidWorkspaceName(name: string):
boolean that implements the current logic (trim, length bounds, regex.test).
Replace the inline safeNameRegex and the isValidName computed in
CreateWorkspaceDialogContent.vue and the identical logic in
EditWorkspaceDialogContent.vue to call isValidWorkspaceName(workspaceName.value)
(or use the constants if needed) so both dialogs use the single source of truth.

In `@src/components/dialog/content/workspace/InviteMemberDialogContent.vue`:
- Around line 93-94: The SVG uses a hardcoded clipPath id "clip0_2127_14348" in
InviteMemberDialogContent.vue which can collide when multiple instances render;
update the template to generate a unique id per component instance (e.g.,
compose a base name with a unique suffix like the component UID or a short
random/token from data/computed) and bind that id to both the <clipPath> and the
corresponding clip-path attribute, or remove the clipPath entirely if it's not
required; update references in the SVG (the clip-path attribute) to use the same
dynamic id so they remain consistent.
- Around line 16-22: Replace the raw close <button> in
InviteMemberDialogContent.vue with the shared IconButton component used in
RevokeInviteDialogContent.vue: remove the custom <button> and <i> markup and
render <IconButton> (or the project’s common icon button) with the same
:aria-label="$t('g.close')" and `@click`="onCancel", supply the same icon (e.g.,
"pi pi-times" or via an icon prop/slot) and any needed styling props, and ensure
IconButton is imported/registered in the component so theming and accessibility
match the other dialog.
- Around line 72-98: The copy icon block uses a non-interactive <div> with
`@click` and a hardcoded stroke color; replace the outer <div> with a <button
type="button"> (keep the existing classes like "absolute right-4 top-2
cursor-pointer" but add accessible attributes such as aria-label="Copy invite
link" and a visible focus style) and move the `@click`="onCopyLink" to that button
so the onCopyLink method is keyboard- and screen-reader-accessible; update the
SVG stroke="white" to stroke="currentColor" and ensure the button has an
appropriate text color class (or inline color) so the icon adapts to light/dark
themes; ensure no other event handlers rely on the element being a div.
- Around line 143-152: The onCreateLink function lacks error handling so if
workspaceStore.createInviteLink(email.value) throws the user sees no feedback;
update onCreateLink to add a catch block that captures the thrown error, sets
loading.value = false, and shows a toast error like the existing onCopyLink
pattern (use the same toast.error call and message composition, e.g., include
error.message), while keeping the success path that sets generatedLink.value and
step.value = 'link'.

In `@src/components/dialog/content/workspace/RemoveMemberDialogContent.vue`:
- Around line 12-18: Replace the raw <button> in RemoveMemberDialogContent.vue
with the shared IconButton component: import IconButton and render it instead of
the HTML button, pass the aria-label binding (:aria-label="$t('g.close')") and
wire the `@click`="onCancel" listener, supply the pi pi-times icon (using
IconButton's icon prop or default slot as used across the codebase), and remove
the old button-specific classes since IconButton handles styling/theming. Ensure
the component is added to the component registration/exports so the template
recognizes IconButton.

In `@src/components/dialog/content/workspace/RevokeInviteDialogContent.vue`:
- Around line 12-18: Replace the raw <button> with the shared IconButton
component in RevokeInviteDialogContent.vue: import IconButton from the button
library (src/components/button/IconButton), remove the plain button element and
use <IconButton> instead, passing the aria-label via :aria-label="$t('g.close')"
and the click handler via `@click`="onCancel", and ensure the close icon (pi
pi-times) is rendered inside IconButton or passed via its icon prop and the same
styling/size is preserved; update imports and remove the old button markup so
the component uses the centralized IconButton for consistent theming.
- Around line 59-67: The onRevoke function currently swallows errors from
workspaceStore.revokeInvite(inviteId); add an explicit catch to handle failures:
catch the thrown error, call the same toast/notification helper used in
InviteMemberDialogContent.vue to display a user-facing failure message (include
error.message when available), then keep the existing finally block to set
loading.value = false; ensure dialogStore.closeDialog({ key: 'revoke-invite' })
remains in the success path only and do not rethrow the error after showing the
toast.

In `@src/components/topbar/WorkspaceSwitcherPopover.vue`:
- Around line 125-126: Remove the locally defined SubscriptionPlan type in
WorkspaceSwitcherPopover.vue and instead import SubscriptionPlan from the
existing definition in teamWorkspaceStore
(src/platform/workspace/stores/teamWorkspaceStore.ts); update the top of
WorkspaceSwitcherPopover.vue to add the import for SubscriptionPlan and delete
the local type alias so the component uses the single shared SubscriptionPlan
type from teamWorkspaceStore.

In `@src/platform/settings/components/SettingDialogContent.vue`:
- Around line 16-25: The v-model binding on the SearchBox uses the verbose form
v-model:model-value; change it to the standard shorthand v-model (i.e. bind
searchQuery with v-model="searchQuery") in the SettingDialogContent.vue template
where SearchBox is declared so the component still updates searchQuery and
handleSearch remains attached to the `@search` event.

In `@src/platform/workspace/api/workspaceApi.ts`:
- Around line 115-125: Extract the duplicated logic in getAuthHeaderOrThrow and
getFirebaseHeaderOrThrow into a single helper getHeaderOrThrow(getter) that
accepts an async getter returning AuthHeader | null, calls it, throws the
existing WorkspaceApiError with t('toastMessages.userNotAuthenticated'), 401,
'NOT_AUTHENTICATED' when null, and returns the header otherwise; then change
getAuthHeaderOrThrow and getFirebaseHeaderOrThrow to call getHeaderOrThrow(() =>
useAuthStore().getAuthHeader()) and getHeaderOrThrow(() =>
useFirebaseAuthStore().getFirebaseAuthHeader()) respectively (referencing the
existing function names getAuthHeaderOrThrow, getFirebaseHeaderOrThrow,
getHeaderOrThrow, WorkspaceApiError, and t).

In `@src/platform/workspace/composables/useWorkspaceUI.ts`:
- Around line 38-48: Update the personal-workspace UI permissions in
useWorkspaceUI.ts to reflect backend behavior by setting canLeaveWorkspace to
false; specifically change the returned object for the 'personal' branch (where
canLeaveWorkspace is currently true) so the UI won't show a leave action that
conflicts with teamWorkspaceStore.ts::leaveWorkspace() which throws "Cannot
leave personal workspace", and also ensure any UI configuration that sets
workspaceMenuAction to 'leave' for personal workspaces is removed or changed to
a non-leave action to keep front-end and backend consistent.

In `@src/services/dialogService.ts`:
- Around line 631-647: The empty onConfirm: () => {} in
showBillingComingSoonDialog is a no-op; either remove the onConfirm prop from
the props object or set it explicitly to undefined and add a short inline
comment to indicate the no-op is intentional for an info-only dialog (locate
showBillingComingSoonDialog, dialogStore.showDialog, and
ConfirmationDialogContent to apply the change).

Comment on lines +324 to +329
<button
class="underline bg-transparent border-none cursor-pointer"
@click="handleCreateWorkspace"
>
{{ $t('workspacePanel.members.createNewWorkspace') }}
</button>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Use Button component instead of plain <button> element.

Per project conventions, prefer the common button components from src/components/button/ or src/components/ui/button/ over plain HTML <button> elements for consistent theming.

Proposed fix
-    <button
-      class="underline bg-transparent border-none cursor-pointer"
+    <Button
+      variant="textonly"
+      size="sm"
       `@click`="handleCreateWorkspace"
     >
       {{ $t('workspacePanel.members.createNewWorkspace') }}
-    </button>
+    </Button>

Based on learnings, the repo prefers common button components over plain HTML elements for design system consistency.

🤖 Prompt for AI Agents
In `@src/components/dialog/content/setting/MembersPanelContent.vue` around lines
324 - 329, Replace the plain HTML <button> with the project's common Button
component to follow design-system conventions: import the shared Button (e.g.,
from src/components/button or src/components/ui/button) into
MembersPanelContent.vue and replace the element that calls handleCreateWorkspace
(the template button bound to `@click`="handleCreateWorkspace" and text
$t('workspacePanel.members.createNewWorkspace')) with that Button component,
preserving the click handler, translation key, and existing classes/props as
appropriate (use the Button's props for variant/underline styling instead of raw
classes).

Comment on lines 233 to 237
onMounted(() => {
setActiveTab(defaultTab)
fetchMembers()
fetchPendingInvites()
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unhandled promise rejections from async data fetching.

fetchMembers() and fetchPendingInvites() are async functions that return promises, but the errors are not handled. If either API call fails, the promise rejection will be unhandled.

Consider adding error handling or using a fire-and-forget pattern that suppresses errors gracefully.

Proposed fix: Add error handling
 onMounted(() => {
   setActiveTab(defaultTab)
-  fetchMembers()
-  fetchPendingInvites()
+  fetchMembers().catch(() => {
+    // Silent fail - members list will remain empty
+  })
+  fetchPendingInvites().catch(() => {
+    // Silent fail - pending invites will remain empty
+  })
 })

Alternatively, if errors should be surfaced:

+import { useToast } from 'primevue/usetoast'

+const toast = useToast()

 onMounted(async () => {
   setActiveTab(defaultTab)
+  try {
+    await Promise.all([fetchMembers(), fetchPendingInvites()])
+  } catch {
+    toast.add({
+      severity: 'error',
+      summary: t('g.error'),
+      detail: t('workspacePanel.members.fetchError'),
+      life: 3000
+    })
+  }
-  fetchMembers()
-  fetchPendingInvites()
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onMounted(() => {
setActiveTab(defaultTab)
fetchMembers()
fetchPendingInvites()
})
onMounted(() => {
setActiveTab(defaultTab)
fetchMembers().catch(() => {
// Silent fail - members list will remain empty
})
fetchPendingInvites().catch(() => {
// Silent fail - pending invites will remain empty
})
})
🤖 Prompt for AI Agents
In `@src/components/dialog/content/setting/WorkspacePanelContent.vue` around lines
233 - 237, The onMounted callback calls async functions fetchMembers() and
fetchPendingInvites() without handling rejections; update the onMounted(() => {
... }) block (which also calls setActiveTab(defaultTab)) to handle async errors
by either awaiting those calls inside an async function with try/catch or
chaining .catch(...) on each promise and logging or otherwise handling failures
(e.g., process errors via a local logger or show a user-facing error state).
Ensure you reference and update the onMounted callback and the invocations of
fetchMembers and fetchPendingInvites so no unhandled promise rejections occur.

Comment on lines 80 to 84
const isValidName = computed(() => {
const name = workspaceName.value.trim()
// Allow alphanumeric, spaces, hyphens, underscores (safe characters)
const safeNameRegex = /^[a-zA-Z0-9][a-zA-Z0-9\s\-_]*$/
const safeNameRegex = /^[a-zA-Z0-9][a-zA-Z0-9\s\-_'.,()&+]*$/
return name.length >= 1 && name.length <= 50 && safeNameRegex.test(name)
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider extracting shared validation logic.

The safeNameRegex and validation logic are duplicated identically in EditWorkspaceDialogContent.vue. Extract to a shared utility (e.g., workspaceValidation.ts) to ensure both dialogs stay in sync and simplify future updates.

♻️ Suggested shared utility

Create a new file src/platform/workspace/utils/workspaceValidation.ts:

export const WORKSPACE_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9\s\-_'.,()&+]*$/
export const WORKSPACE_NAME_MIN_LENGTH = 1
export const WORKSPACE_NAME_MAX_LENGTH = 50

export function isValidWorkspaceName(name: string): boolean {
  const trimmed = name.trim()
  return (
    trimmed.length >= WORKSPACE_NAME_MIN_LENGTH &&
    trimmed.length <= WORKSPACE_NAME_MAX_LENGTH &&
    WORKSPACE_NAME_REGEX.test(trimmed)
  )
}

Then in both dialog components:

+import { isValidWorkspaceName } from '@/platform/workspace/utils/workspaceValidation'

 const isValidName = computed(() => {
-  const name = workspaceName.value.trim()
-  const safeNameRegex = /^[a-zA-Z0-9][a-zA-Z0-9\s\-_'.,()&+]*$/
-  return name.length >= 1 && name.length <= 50 && safeNameRegex.test(name)
+  return isValidWorkspaceName(workspaceName.value)
 })
🤖 Prompt for AI Agents
In `@src/components/dialog/content/workspace/CreateWorkspaceDialogContent.vue`
around lines 80 - 84, Extract the duplicated workspace name validation into a
shared utility: create src/platform/workspace/utils/workspaceValidation.ts
exporting WORKSPACE_NAME_REGEX, WORKSPACE_NAME_MIN_LENGTH,
WORKSPACE_NAME_MAX_LENGTH and a function isValidWorkspaceName(name: string):
boolean that implements the current logic (trim, length bounds, regex.test).
Replace the inline safeNameRegex and the isValidName computed in
CreateWorkspaceDialogContent.vue and the identical logic in
EditWorkspaceDialogContent.vue to call isValidWorkspaceName(workspaceName.value)
(or use the constants if needed) so both dialogs use the single source of truth.

Comment on lines +16 to +22
<button
class="cursor-pointer rounded border-none bg-transparent p-0 text-muted-foreground transition-colors hover:text-base-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-secondary-foreground"
:aria-label="$t('g.close')"
@click="onCancel"
>
<i class="pi pi-times size-4" />
</button>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider using IconButton for the close button.

Same as in RevokeInviteDialogContent.vue - prefer common button components for consistency and theming.

🤖 Prompt for AI Agents
In `@src/components/dialog/content/workspace/InviteMemberDialogContent.vue` around
lines 16 - 22, Replace the raw close <button> in InviteMemberDialogContent.vue
with the shared IconButton component used in RevokeInviteDialogContent.vue:
remove the custom <button> and <i> markup and render <IconButton> (or the
project’s common icon button) with the same :aria-label="$t('g.close')" and
`@click`="onCancel", supply the same icon (e.g., "pi pi-times" or via an icon
prop/slot) and any needed styling props, and ensure IconButton is
imported/registered in the component so theming and accessibility match the
other dialog.

Comment on lines +125 to +126
type SubscriptionPlan = 'PRO_MONTHLY' | 'PRO_YEARLY' | null

Copy link
Contributor

@coderabbitai coderabbitai bot Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if SubscriptionPlan type is defined elsewhere in the codebase
rg "type SubscriptionPlan" --type ts
echo "---"
# Check for subscriptionPlan in workspace directory
rg "subscriptionPlan" src/platform/workspace/ --type ts
echo "---"
# Check WorkspaceSwitcherPopover.vue for context
rg "type SubscriptionPlan" src/components/topbar/WorkspaceSwitcherPopover.vue -A 5

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 773


Import SubscriptionPlan type from teamWorkspaceStore instead of defining it locally.

The SubscriptionPlan type is already defined in src/platform/workspace/stores/teamWorkspaceStore.ts with the identical definition. Remove the local type definition at lines 125-126 and import it from the store to maintain a single source of truth.

🤖 Prompt for AI Agents
In `@src/components/topbar/WorkspaceSwitcherPopover.vue` around lines 125 - 126,
Remove the locally defined SubscriptionPlan type in WorkspaceSwitcherPopover.vue
and instead import SubscriptionPlan from the existing definition in
teamWorkspaceStore (src/platform/workspace/stores/teamWorkspaceStore.ts); update
the top of WorkspaceSwitcherPopover.vue to add the import for SubscriptionPlan
and delete the local type alias so the component uses the single shared
SubscriptionPlan type from teamWorkspaceStore.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coderabbit comment above seems valid.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if SubscriptionPlan type is defined in teamWorkspaceStore
echo "=== Checking teamWorkspaceStore.ts for SubscriptionPlan type ==="
cat src/platform/workspace/stores/teamWorkspaceStore.ts | grep -n "SubscriptionPlan" -A 2 -B 2

echo -e "\n=== All SubscriptionPlan type definitions in codebase ==="
rg "type SubscriptionPlan|interface SubscriptionPlan" --type ts -n

echo -e "\n=== All usages of subscriptionPlan property ==="
rg "subscriptionPlan" --type ts -n | head -20

Length of output: 1223


🏁 Script executed:

#!/bin/bash
# Check if SubscriptionPlan is exported from teamWorkspaceStore
echo "=== Checking exports in teamWorkspaceStore.ts ==="
rg "export.*SubscriptionPlan" src/platform/workspace/stores/teamWorkspaceStore.ts -n

echo -e "\n=== Checking all export statements near SubscriptionPlan definition ==="
sed -n '30,40p' src/platform/workspace/stores/teamWorkspaceStore.ts

echo -e "\n=== Checking if WorkspaceState is exported ==="
rg "export.*WorkspaceState" src/platform/workspace/stores/teamWorkspaceStore.ts -n

Length of output: 432


@christian-byrne Actually, I need to correct myself - my original suggestion was mistaken.

After checking the codebase, the SubscriptionPlan type in teamWorkspaceStore.ts is not exported - it's defined as a private internal type used only within that store. The local definition in WorkspaceSwitcherPopover.vue is appropriate here.

Based on this project's principle of keeping the API surface minimal (only exporting when there's concrete usage elsewhere), the current approach with separate local definitions is actually the right pattern. The type is simple enough that having it defined in both places is acceptable and maintains proper encapsulation.

Sorry for the confusion in my initial review! The current code is fine as-is.


🧠 Learnings used
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7894
File: src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.test.ts:11-14
Timestamp: 2026-01-08T02:40:22.621Z
Learning: In the Comfy-Org/ComfyUI_frontend repository test files: When testing components, import the real type definitions from the component files instead of duplicating interface definitions in the test files. This prevents type drift and maintains consistency.

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: src/AGENTS.md:0-0
Timestamp: 2026-01-16T21:33:47.062Z
Learning: Applies to src/**/store/**/*.{js,ts} : Clean up subscriptions to prevent memory leaks in stores

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7416
File: src/stores/imagePreviewStore.ts:5-7
Timestamp: 2025-12-13T11:03:21.073Z
Learning: In the Comfy-Org/ComfyUI_frontend repository, linting rules enforce keeping `import type` statements separate from regular `import` statements, even when importing from the same module. Do not suggest consolidating them into a single import statement.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8248
File: src/platform/assets/schemas/assetSchema.ts:109-118
Timestamp: 2026-01-22T23:45:06.746Z
Learning: In the ComfyUI_frontend repository, don't suggest exporting schemas, constants, or other declarations unless they are actually used in other parts of the codebase. Keep the API surface minimal until there's a concrete need.

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-22T17:18:17.636Z
Learning: Applies to src/**/*.ts : Use separate `import type` statements for types, not inline `type` in mixed imports

Comment on lines 16 to 25
<div class="px-4">
<SearchBox
v-model:model-value="searchQuery"
class="settings-search-box mb-2 w-full"
:placeholder="$t('g.searchSettings') + '...'"
:debounce-time="128"
autofocus
@search="handleSearch"
/>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Simplify the v-model binding.

v-model:model-value is equivalent to v-model but unnecessarily verbose. Use the standard shorthand form.

♻️ Suggested simplification
       <SearchBox
-        v-model:model-value="searchQuery"
+        v-model="searchQuery"
         class="settings-search-box mb-2 w-full"
         :placeholder="$t('g.searchSettings') + '...'"
         :debounce-time="128"
         autofocus
         `@search`="handleSearch"
       />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="px-4">
<SearchBox
v-model:model-value="searchQuery"
class="settings-search-box mb-2 w-full"
:placeholder="$t('g.searchSettings') + '...'"
:debounce-time="128"
autofocus
@search="handleSearch"
/>
</div>
<div class="px-4">
<SearchBox
v-model="searchQuery"
class="settings-search-box mb-2 w-full"
:placeholder="$t('g.searchSettings') + '...'"
:debounce-time="128"
autofocus
`@search`="handleSearch"
/>
</div>
🤖 Prompt for AI Agents
In `@src/platform/settings/components/SettingDialogContent.vue` around lines 16 -
25, The v-model binding on the SearchBox uses the verbose form
v-model:model-value; change it to the standard shorthand v-model (i.e. bind
searchQuery with v-model="searchQuery") in the SettingDialogContent.vue template
where SearchBox is declared so the component still updates searchQuery and
handleSearch remains attached to the `@search` event.

Comment on lines +115 to +125
async function getFirebaseHeaderOrThrow() {
const authHeader = await useFirebaseAuthStore().getFirebaseAuthHeader()
if (!authHeader) {
throw new WorkspaceApiError(
t('toastMessages.userNotAuthenticated'),
401,
'NOT_AUTHENTICATED'
)
}
return authHeader
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider extracting shared logic to reduce duplication.

getFirebaseHeaderOrThrow is nearly identical to getAuthHeaderOrThrow (lines 103-113). Consider a shared helper:

async function getHeaderOrThrow(
  getter: () => Promise<AuthHeader | null>
): Promise<AuthHeader> {
  const authHeader = await getter()
  if (!authHeader) {
    throw new WorkspaceApiError(
      t('toastMessages.userNotAuthenticated'),
      401,
      'NOT_AUTHENTICATED'
    )
  }
  return authHeader
}

Then use it as:

const getAuthHeaderOrThrow = () => 
  getHeaderOrThrow(() => useFirebaseAuthStore().getAuthHeader())
const getFirebaseHeaderOrThrow = () => 
  getHeaderOrThrow(() => useFirebaseAuthStore().getFirebaseAuthHeader())

This is a minor improvement; the current implementation is clear and works correctly.

🤖 Prompt for AI Agents
In `@src/platform/workspace/api/workspaceApi.ts` around lines 115 - 125, Extract
the duplicated logic in getAuthHeaderOrThrow and getFirebaseHeaderOrThrow into a
single helper getHeaderOrThrow(getter) that accepts an async getter returning
AuthHeader | null, calls it, throws the existing WorkspaceApiError with
t('toastMessages.userNotAuthenticated'), 401, 'NOT_AUTHENTICATED' when null, and
returns the header otherwise; then change getAuthHeaderOrThrow and
getFirebaseHeaderOrThrow to call getHeaderOrThrow(() =>
useAuthStore().getAuthHeader()) and getHeaderOrThrow(() =>
useFirebaseAuthStore().getFirebaseAuthHeader()) respectively (referencing the
existing function names getAuthHeaderOrThrow, getFirebaseHeaderOrThrow,
getHeaderOrThrow, WorkspaceApiError, and t).

Comment on lines +631 to +647
function showBillingComingSoonDialog() {
return dialogStore.showDialog({
key: 'billing-coming-soon',
title: t('subscription.billingComingSoon.title'),
component: ConfirmationDialogContent,
props: {
message: t('subscription.billingComingSoon.message'),
type: 'info' as ConfirmationDialogType,
onConfirm: () => {}
},
dialogComponentProps: {
pt: {
root: { class: 'max-w-[360px]' }
}
}
})
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Empty onConfirm callback for info dialog is intentional but could be cleaner.

The onConfirm: () => {} is a no-op since info dialogs only show an OK button that closes the dialog. Consider adding a brief comment or using a more explicit approach.

Optional: Add clarity for no-op callback
 function showBillingComingSoonDialog() {
   return dialogStore.showDialog({
     key: 'billing-coming-soon',
     title: t('subscription.billingComingSoon.title'),
     component: ConfirmationDialogContent,
     props: {
       message: t('subscription.billingComingSoon.message'),
       type: 'info' as ConfirmationDialogType,
-      onConfirm: () => {}
+      // Info dialogs only show OK button; onConfirm unused but required by props
+      onConfirm: () => {}
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function showBillingComingSoonDialog() {
return dialogStore.showDialog({
key: 'billing-coming-soon',
title: t('subscription.billingComingSoon.title'),
component: ConfirmationDialogContent,
props: {
message: t('subscription.billingComingSoon.message'),
type: 'info' as ConfirmationDialogType,
onConfirm: () => {}
},
dialogComponentProps: {
pt: {
root: { class: 'max-w-[360px]' }
}
}
})
}
function showBillingComingSoonDialog() {
return dialogStore.showDialog({
key: 'billing-coming-soon',
title: t('subscription.billingComingSoon.title'),
component: ConfirmationDialogContent,
props: {
message: t('subscription.billingComingSoon.message'),
type: 'info' as ConfirmationDialogType,
// Info dialogs only show OK button; onConfirm unused but required by props
onConfirm: () => {}
},
dialogComponentProps: {
pt: {
root: { class: 'max-w-[360px]' }
}
}
})
}
🤖 Prompt for AI Agents
In `@src/services/dialogService.ts` around lines 631 - 647, The empty onConfirm:
() => {} in showBillingComingSoonDialog is a no-op; either remove the onConfirm
prop from the props object or set it explicitly to undefined and add a short
inline comment to indicate the no-op is intentional for an info-only dialog
(locate showBillingComingSoonDialog, dialogStore.showDialog, and
ConfirmationDialogContent to apply the change).

@simula-r simula-r force-pushed the workspaces-4-members-invites branch from 265a5b5 to 9eb015b Compare January 24, 2026 21:44
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.ts (2)

13-16: Prefer function declaration over function expression.

Per coding guidelines, prefer function declarations for pure functions.

♻️ Suggested refactor
-const getCheckoutTier = (
-  tierKey: TierKey,
-  billingCycle: BillingCycle
-): CheckoutTier => (billingCycle === 'yearly' ? `${tierKey}-yearly` : tierKey)
+function getCheckoutTier(
+  tierKey: TierKey,
+  billingCycle: BillingCycle
+): CheckoutTier {
+  return billingCycle === 'yearly' ? `${tierKey}-yearly` : tierKey
+}

Based on learnings, function declarations are preferred over function expressions for pure functions in this repository.


55-69: Potential issue with response body consumption.

The Response body is a ReadableStream that can only be consumed once. If response.json() fails after partially reading the stream, the subsequent response.text() call may throw or return incomplete data.

A safer approach is to read the body once as text, then attempt to parse it as JSON:

🔧 Suggested fix
   if (!response.ok) {
     let errorMessage = 'Failed to initiate checkout'
     try {
-      const errorData = await response.json()
-      errorMessage = errorData.message || errorMessage
+      const errorText = await response.text()
+      if (errorText) {
+        try {
+          const errorData = JSON.parse(errorText)
+          errorMessage = errorData.message || errorMessage
+        } catch {
+          errorMessage = errorText
+        }
+      } else {
+        errorMessage = `HTTP ${response.status} ${response.statusText}`
+      }
     } catch {
-      // If JSON parsing fails, try to get text response or use HTTP status
-      try {
-        const errorText = await response.text()
-        errorMessage =
-          errorText || `HTTP ${response.status} ${response.statusText}`
-      } catch {
-        errorMessage = `HTTP ${response.status} ${response.statusText}`
-      }
+      errorMessage = `HTTP ${response.status} ${response.statusText}`
     }
src/components/dialog/content/workspace/CreateWorkspaceDialogContent.vue (1)

12-18: Consider using IconButton for the close button.

Based on learnings, prefer the repo's common button components over plain HTML <button> elements. The close button could use IconButton from @/components/button/IconButton.vue for consistency with the project's design system.

♻️ Suggested refactor
+import IconButton from '@/components/button/IconButton.vue'
-      <button
-        class="cursor-pointer rounded border-none bg-transparent p-0 text-muted-foreground transition-colors hover:text-base-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-secondary-foreground"
-        :aria-label="$t('g.close')"
-        `@click`="onCancel"
-      >
-        <i class="pi pi-times size-4" />
-      </button>
+      <IconButton
+        icon="pi pi-times"
+        :aria-label="$t('g.close')"
+        `@click`="onCancel"
+      />
src/components/dialog/content/workspace/EditWorkspaceDialogContent.vue (1)

12-18: Consider using IconButton for consistency.

Similar to CreateWorkspaceDialogContent.vue, the close button could use the common IconButton component for design system consistency.

src/components/topbar/CurrentUserPopoverWorkspace.vue (1)

76-102: Potential logic issue in Add Credits button visibility.

Line 77 requires both isActiveSubscription && isWorkspaceSubscribed to be true, but isActiveSubscription comes from personal subscription state while isWorkspaceSubscribed is workspace-level. For personal workspaces, isWorkspaceSubscribed may not be set correctly, and for team workspaces, isActiveSubscription may be false. This could prevent the "Add Credits" button from showing in either context.

Consider aligning the condition with the displayedCredits logic (lines 241-245) that correctly uses an OR-like pattern based on workspace type.

🔧 Suggested fix
         <!-- Subscribed: Show Add Credits button -->
         <Button
-          v-if="isActiveSubscription && isWorkspaceSubscribed"
+          v-if="isPersonalWorkspace ? isActiveSubscription : isWorkspaceSubscribed"
           variant="secondary"
           size="sm"
           class="text-base-foreground"
           data-testid="add-credits-button"
           `@click`="handleTopUp"
         >
🤖 Fix all issues with AI agents
In `@src/components/dialog/content/setting/MembersPanelContent.vue`:
- Around line 133-138: The UserAvatar pass-through class uses a Tailwind
important modifier ('text-xl!') which violates the no-'!' rule; update the
:pt:icon:class binding on the UserAvatar component to use the regular 'text-xl'
class instead of 'text-xl!' (i.e., change the object key "{ 'text-xl!':
!userPhotoUrl }" to "{ 'text-xl': !userPhotoUrl }"), keeping the conditional on
userPhotoUrl unchanged.

In `@src/components/dialog/content/setting/WorkspacePanelContent.vue`:
- Around line 49-63: The Invite Button component currently sets an aria-label
which overrides the visible text; remove the
:aria-label="$t('workspacePanel.inviteMember')" attribute from the Button in
WorkspacePanelContent.vue (the Button rendered when permissions.canInviteMembers
is true) so the visible label "{{ $t('workspacePanel.invite') }}" becomes the
accessible name; keep the rest of the props and event handler
(isInviteLimitReached, inviteTooltip, `@click`="handleInviteMember") unchanged.
- Around line 85-89: The template in WorkspacePanelContent.vue currently uses a
:class array to merge classes; replace that array with a call to the project's
cn() utility so class merging follows the codebase convention — build the class
string by calling cn('flex items-center gap-2 px-3 py-2', item.class,
item.disabled ? 'pointer-events-auto' : 'cursor-pointer') ensuring you reference
the same item.class and item.disabled properties and import/resolve cn() in the
component if not already available.

In `@src/components/dialog/content/workspace/InviteMemberDialogContent.vue`:
- Around line 149-153: The toast in the catch block for the create-link flow
uses the wrong summary key; update the summary from
workspacePanel.inviteMemberDialog.linkCopyFailed to
workspacePanel.inviteMemberDialog.createLinkFailed in the onCreateLink error
handler (the catch inside InviteMemberDialogContent.vue where toast.add is
called) and add the corresponding message key
workspacePanel.inviteMemberDialog.createLinkFailed to src/locales/en/main.json
with an appropriate user-facing string so the toast shows an accurate
create-link failure message.

In `@src/components/dialog/content/workspace/RevokeInviteDialogContent.vue`:
- Around line 68-73: The toast in the catch block of
RevokeInviteDialogContent.vue currently leaves detail empty when the caught
value isn't an Error; update the catch block where toast.add(...) is called (the
catch surrounding the revoke invite flow) to provide a user-friendly fallback
detail (e.g. use the i18n translator t(...) for a generic "unknown error"
message) instead of undefined so the toast always shows actionable text; keep
existing severity/summary/life values and only change the detail expression to
use the error message when available or the translated fallback otherwise.

In `@src/platform/workspace/composables/useInviteUrlLoader.test.ts`:
- Around line 27-39: Replace the unsafe Record<string,string> mock type with
vue-router's LocationQuery: import { LocationQuery } from 'vue-router' and
change mockRouteQuery's value type to LocationQuery (i.e., const mockRouteQuery
= vi.hoisted(() => ({ value: {} as LocationQuery }))). Update the mock export
for useRoute to return mockRouteQuery.value as before and remove any runtime
casts like `as unknown as string` in tests that were compensating for the wrong
type; adjust assertions to use the proper LocationQuery types where needed
(references: mockRouteQuery, useRoute, mockRouterReplace).

In `@src/platform/workspace/composables/useInviteUrlLoader.ts`:
- Around line 84-88: In useInviteUrlLoader (the t(...) call for
'workspace.addedToWorkspace') remove the explicit escapeParameter: false option
so the i18n call becomes t('workspace.addedToWorkspace', { workspaceName:
result.workspaceName }) and let the default escaping remain; this ensures
workspaceName is not unescaped and follows the project's
sanitization/trusted-source guidelines.
♻️ Duplicate comments (12)
src/components/dialog/content/workspace/CreateWorkspaceDialogContent.vue (1)

80-84: Duplicate validation logic with EditWorkspaceDialogContent.vue.

The safeNameRegex and validation logic are identical in both dialog components. As noted in a previous review, consider extracting to a shared utility to ensure both dialogs stay in sync.

src/components/dialog/content/workspace/EditWorkspaceDialogContent.vue (1)

70-74: Validation logic is duplicated.

This validation regex and logic are identical to CreateWorkspaceDialogContent.vue. Consider extracting to a shared utility as suggested in previous reviews.

src/platform/settings/components/SettingDialogContent.vue (1)

16-25: Simplify v-model:model-value to v-model.

The verbose v-model:model-value binding is equivalent to just v-model. Use the standard shorthand.

♻️ Suggested fix
       <SearchBox
-        v-model:model-value="searchQuery"
+        v-model="searchQuery"
         class="settings-search-box mb-2 w-full"
         :placeholder="$t('g.searchSettings') + '...'"
         :debounce-time="128"
         autofocus
         `@search`="handleSearch"
       />
src/platform/workspace/api/workspaceApi.ts (1)

114-124: Consider extracting shared logic to reduce duplication.

getFirebaseHeaderOrThrow is nearly identical to getAuthHeaderOrThrow (lines 102-112). A shared helper accepting a getter function would reduce duplication.

This was already flagged in a previous review - the current implementation works correctly but could be consolidated.

src/components/dialog/content/workspace/RemoveMemberDialogContent.vue (1)

12-18: Replace plain HTML button with IconButton component.

Per repository conventions, prefer the common button components from src/components/button/ over plain HTML <button> elements for consistency and theming. This was flagged in a previous review.

src/components/dialog/content/workspace/RevokeInviteDialogContent.vue (1)

12-18: Replace the raw close button with the shared IconButton.

The current <button> bypasses the design-system button components and styling conventions used elsewhere.

♻️ Proposed refactor
+import IconButton from '@/components/button/IconButton.vue'
...
-      <button
-        class="cursor-pointer rounded border-none bg-transparent p-0 text-muted-foreground transition-colors hover:text-base-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-secondary-foreground"
-        :aria-label="$t('g.close')"
-        `@click`="onCancel"
-      >
-        <i class="pi pi-times size-4" />
-      </button>
+      <IconButton
+        icon="pi pi-times"
+        :aria-label="$t('g.close')"
+        `@click`="onCancel"
+      />

Based on learnings, prefer the shared button components from src/components/button/ for consistency.

src/components/dialog/content/workspace/InviteMemberDialogContent.vue (2)

16-22: Replace the raw close button with the shared IconButton.

The dialog header still uses a custom <button> instead of the standardized button components.

♻️ Proposed refactor
+import IconButton from '@/components/button/IconButton.vue'
...
-      <button
-        class="cursor-pointer rounded border-none bg-transparent p-0 text-muted-foreground transition-colors hover:text-base-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-secondary-foreground"
-        :aria-label="$t('g.close')"
-        `@click`="onCancel"
-      >
-        <i class="pi pi-times size-4" />
-      </button>
+      <IconButton
+        icon="pi pi-times"
+        :aria-label="$t('g.close')"
+        `@click`="onCancel"
+      />

Based on learnings, prefer the shared button components from src/components/button/.


72-97: Make the copy icon a proper button and avoid static SVG clipPath IDs.

The clickable <div> is not keyboard-accessible, the SVG color is hardcoded, and the static clipPath ID can collide across instances.

♻️ Suggested fix
-          <div
-            class="absolute right-4 top-2 cursor-pointer"
-            `@click`="onCopyLink"
-          >
+          <button
+            type="button"
+            class="absolute right-4 top-2 cursor-pointer border-none bg-transparent p-0 text-muted-foreground"
+            :aria-label="$t('workspacePanel.inviteMemberDialog.linkStep.copyLink')"
+            `@click`="onCopyLink"
+          >
             <svg
               xmlns="http://www.w3.org/2000/svg"
               width="16"
               height="16"
               viewBox="0 0 16 16"
               fill="none"
             >
-              <g clip-path="url(`#clip0_2127_14348`)">
+              <g>
                 <path
                   d="M2.66634 10.6666C1.93301 10.6666 1.33301 10.0666 1.33301 9.33325V2.66659C1.33301 1.93325 1.93301 1.33325 2.66634 1.33325H9.33301C10.0663 1.33325 10.6663 1.93325 10.6663 2.66659M6.66634 5.33325H13.333C14.0694 5.33325 14.6663 5.93021 14.6663 6.66658V13.3333C14.6663 14.0696 14.0694 14.6666 13.333 14.6666H6.66634C5.92996 14.6666 5.33301 14.0696 5.33301 13.3333V6.66658C5.33301 5.93021 5.92996 5.33325 6.66634 5.33325Z"
-                  stroke="white"
+                  stroke="currentColor"
                   stroke-width="1.3"
                   stroke-linecap="round"
                   stroke-linejoin="round"
                 />
               </g>
-              <defs>
-                <clipPath id="clip0_2127_14348">
-                  <rect width="16" height="16" fill="white" />
-                </clipPath>
-              </defs>
             </svg>
-          </div>
+          </button>
src/components/dialog/content/setting/MembersPanelContent.vue (1)

324-329: Use the shared Button component for the create-workspace link.

The plain <button> here bypasses the shared Button styling and theming.

♻️ Proposed refactor
-      <button
-        class="underline bg-transparent border-none cursor-pointer"
-        `@click`="handleCreateWorkspace"
-      >
-        {{ $t('workspacePanel.members.createNewWorkspace') }}
-      </button>
+      <Button variant="textonly" size="sm" `@click`="handleCreateWorkspace">
+        {{ $t('workspacePanel.members.createNewWorkspace') }}
+      </Button>

Based on learnings, prefer the shared Button component for consistency.

src/components/topbar/WorkspaceSwitcherPopover.vue (1)

125-126: Import SubscriptionPlan from the store instead of redefining it.

The type already exists in teamWorkspaceStore; keeping a duplicate risks drift.

♻️ Proposed refactor
+import type { SubscriptionPlan } from '@/platform/workspace/stores/teamWorkspaceStore'
...
-type SubscriptionPlan = 'PRO_MONTHLY' | 'PRO_YEARLY' | null
src/components/dialog/content/setting/WorkspacePanelContent.vue (1)

233-236: Unhandled async calls in onMounted.
This is a duplicate of a prior review comment: fetchMembers() and
fetchPendingInvites() should handle rejections to avoid unhandled promises.

src/services/dialogService.ts (1)

631-640: No-op onConfirm in billing dialog.
Duplicate of a prior review comment: consider removing or clarifying the no-op
callback for the info dialog.

Comment on lines +49 to +63
<Button
v-if="permissions.canInviteMembers"
v-tooltip="
inviteTooltip
? { value: inviteTooltip, showDelay: 0 }
: { value: $t('workspacePanel.inviteMember'), showDelay: 300 }
"
variant="secondary"
size="lg"
:disabled="isInviteLimitReached"
:class="isInviteLimitReached && 'opacity-50 cursor-not-allowed'"
:aria-label="$t('workspacePanel.inviteMember')"
@click="handleInviteMember"
>
{{ $t('workspacePanel.invite') }}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove aria-label from the text-labeled Invite button.
The visible label already provides the accessible name; an aria-label can be
redundant or override it.

♻️ Proposed fix
-          :aria-label="$t('workspacePanel.inviteMember')"

Based on learnings, avoid aria-label on buttons with visible text.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Button
v-if="permissions.canInviteMembers"
v-tooltip="
inviteTooltip
? { value: inviteTooltip, showDelay: 0 }
: { value: $t('workspacePanel.inviteMember'), showDelay: 300 }
"
variant="secondary"
size="lg"
:disabled="isInviteLimitReached"
:class="isInviteLimitReached && 'opacity-50 cursor-not-allowed'"
:aria-label="$t('workspacePanel.inviteMember')"
@click="handleInviteMember"
>
{{ $t('workspacePanel.invite') }}
<Button
v-if="permissions.canInviteMembers"
v-tooltip="
inviteTooltip
? { value: inviteTooltip, showDelay: 0 }
: { value: $t('workspacePanel.inviteMember'), showDelay: 300 }
"
variant="secondary"
size="lg"
:disabled="isInviteLimitReached"
:class="isInviteLimitReached && 'opacity-50 cursor-not-allowed'"
`@click`="handleInviteMember"
>
{{ $t('workspacePanel.invite') }}
🤖 Prompt for AI Agents
In `@src/components/dialog/content/setting/WorkspacePanelContent.vue` around lines
49 - 63, The Invite Button component currently sets an aria-label which
overrides the visible text; remove the
:aria-label="$t('workspacePanel.inviteMember')" attribute from the Button in
WorkspacePanelContent.vue (the Button rendered when permissions.canInviteMembers
is true) so the visible label "{{ $t('workspacePanel.invite') }}" becomes the
accessible name; keep the rest of the props and event handler
(isInviteLimitReached, inviteTooltip, `@click`="handleInviteMember") unchanged.

Comment on lines +149 to +153
} catch (error) {
toast.add({
severity: 'error',
summary: t('workspacePanel.inviteMemberDialog.linkCopyFailed'),
detail: error instanceof Error ? error.message : undefined,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use a create-link failure message for onCreateLink.

The toast summary uses linkCopyFailed, which is inaccurate for create-link errors.

🐛 Proposed fix
-      summary: t('workspacePanel.inviteMemberDialog.linkCopyFailed'),
+      summary: t('workspacePanel.inviteMemberDialog.createLinkFailed'),

Please add workspacePanel.inviteMemberDialog.createLinkFailed to src/locales/en/main.json. As per coding guidelines, error messages should be accurate and actionable.

🤖 Prompt for AI Agents
In `@src/components/dialog/content/workspace/InviteMemberDialogContent.vue` around
lines 149 - 153, The toast in the catch block for the create-link flow uses the
wrong summary key; update the summary from
workspacePanel.inviteMemberDialog.linkCopyFailed to
workspacePanel.inviteMemberDialog.createLinkFailed in the onCreateLink error
handler (the catch inside InviteMemberDialogContent.vue where toast.add is
called) and add the corresponding message key
workspacePanel.inviteMemberDialog.createLinkFailed to src/locales/en/main.json
with an appropriate user-facing string so the toast shows an accurate
create-link failure message.

Comment on lines +68 to +73
} catch (error) {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: error instanceof Error ? error.message : undefined,
life: 3000
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Provide a fallback detail for unknown revoke errors.

When error isn’t an Error, the toast detail is empty and the message becomes non-actionable.

🐛 Proposed fix
-      detail: error instanceof Error ? error.message : undefined,
+      detail: error instanceof Error ? error.message : t('g.unknownError'),

As per coding guidelines, error messages should remain user-friendly and actionable.

🤖 Prompt for AI Agents
In `@src/components/dialog/content/workspace/RevokeInviteDialogContent.vue` around
lines 68 - 73, The toast in the catch block of RevokeInviteDialogContent.vue
currently leaves detail empty when the caught value isn't an Error; update the
catch block where toast.add(...) is called (the catch surrounding the revoke
invite flow) to provide a user-friendly fallback detail (e.g. use the i18n
translator t(...) for a generic "unknown error" message) instead of undefined so
the toast always shows actionable text; keep existing severity/summary/life
values and only change the detail expression to use the error message when
available or the translated fallback otherwise.

Comment on lines +27 to +39
const mockRouteQuery = vi.hoisted(() => ({
value: {} as Record<string, string>
}))
const mockRouterReplace = vi.hoisted(() => vi.fn())

vi.mock('vue-router', () => ({
useRoute: () => ({
query: mockRouteQuery.value
}),
useRouter: () => ({
replace: mockRouterReplace
})
}))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, check the file structure and current implementation
cat -n src/platform/workspace/composables/useInviteUrlLoader.test.ts | head -100

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 3543


🏁 Script executed:

# Check lines around 223-230 to see the other location needing fixing
sed -n '220,235p' src/platform/workspace/composables/useInviteUrlLoader.test.ts

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 428


🏁 Script executed:

# Search for LocationQuery usage in test files
rg -n "LocationQuery" src -g "*.test.ts"

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 576


🏁 Script executed:

# Check how vue-router is imported and used in test files
rg -n "from 'vue-router'" src -g "*.test.ts" -A 1 | head -30

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 244


🏁 Script executed:

# Search for other useRoute mocks in test files to see patterns
rg -n "useRoute.*mock" src -g "*.test.ts" -B 2 -A 5 | head -50

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 52


🏁 Script executed:

# Check vue-router version and query type definition
cat package.json | rg -A 5 -B 5 '"vue-router"'

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 380


🏁 Script executed:

# Find LocationQuery type definition in node_modules or check imports
rg -n "type LocationQuery" src --max-count 5

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 52


🏁 Script executed:

# Get more context from the working example in previousFullPath.test.ts
cat -n src/platform/cloud/onboarding/utils/previousFullPath.test.ts | head -40

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 1484


🏁 Script executed:

# Check if LocationQuery allows both string and array values in the type
python3 << 'EOF'
# LocationQuery in vue-router is defined as:
# type LocationQuery = Record<string, string | string[]>
# This allows values to be either string or string[] (arrays)
# So the test file's assignment of { invite: ['array', 'value'] } 
# would be valid WITHOUT the "as unknown as string" cast if typed as LocationQuery

print("LocationQuery type structure:")
print("type LocationQuery = Record<string, string | string[]>")
print("")
print("This means:")
print("- Current: Record<string, string> - only allows string values")
print("- Proposed: LocationQuery - allows string | string[] values")
print("")
print("The array ['array', 'value'] is valid for LocationQuery but not Record<string, string>")
print("So the double-cast (as unknown as string) is a workaround for the type mismatch")
EOF

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 156


🏁 Script executed:

# Verify the actual vue-router LocationQuery export
rg -n "export.*LocationQuery" --type ts | head -5

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 52


Use LocationQuery type from vue-router instead of Record<string, string> to eliminate unsafe casts.

The mock currently uses Record<string, string> which doesn't match vue-router's actual query type (Record<string, string | string[]>), forcing the workaround as unknown as string cast when testing non-string query values. Import LocationQuery from vue-router and use it for the mock type—this removes the cast and aligns the test with how the API actually works, as already done in previousFullPath.test.ts.

♻️ Proposed refactor
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import type { LocationQuery } from 'vue-router'

 import { useInviteUrlLoader } from './useInviteUrlLoader'
 const mockRouteQuery = vi.hoisted(() => ({
-  value: {} as Record<string, string>
+  value: {} as LocationQuery
 }))
     it('ignores non-string invite param', async () => {
-      mockRouteQuery.value = { invite: ['array', 'value'] as unknown as string }
+      mockRouteQuery.value = { invite: ['array', 'value'] }
🤖 Prompt for AI Agents
In `@src/platform/workspace/composables/useInviteUrlLoader.test.ts` around lines
27 - 39, Replace the unsafe Record<string,string> mock type with vue-router's
LocationQuery: import { LocationQuery } from 'vue-router' and change
mockRouteQuery's value type to LocationQuery (i.e., const mockRouteQuery =
vi.hoisted(() => ({ value: {} as LocationQuery }))). Update the mock export for
useRoute to return mockRouteQuery.value as before and remove any runtime casts
like `as unknown as string` in tests that were compensating for the wrong type;
adjust assertions to use the proper LocationQuery types where needed
(references: mockRouteQuery, useRoute, mockRouterReplace).

Comment on lines +84 to +88
detail: t(
'workspace.addedToWorkspace',
{ workspaceName: result.workspaceName },
{ escapeParameter: false }
),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/platform/workspace/composables/useInviteUrlLoader.ts

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 3937


🏁 Script executed:

# Check the i18n translation file
cat -n src/locales/en/main.json | grep -A 5 "addedToWorkspace"

# Check teamWorkspaceStore to see acceptInvite
git ls-files | grep -i "teamWorkspaceStore"

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 446


🏁 Script executed:

cat -n src/platform/workspace/stores/teamWorkspaceStore.ts | head -100

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 3506


🏁 Script executed:

# Find acceptInvite method in teamWorkspaceStore.ts
grep -n "acceptInvite" src/platform/workspace/stores/teamWorkspaceStore.ts

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 190


🏁 Script executed:

sed -n '550,560p' src/platform/workspace/stores/teamWorkspaceStore.ts

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 437


🏁 Script executed:

# Check the workspace API to see the response structure
grep -n "acceptInvite" src/platform/workspace/api/workspaceApi.ts

# Search for PrimeVue Toast usage in the codebase to understand detail rendering
rg "toast.add" -A 5 -B 2 | head -50

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 4328


🏁 Script executed:

# Check the AcceptInviteResponse type definition
sed -n '314,330p' src/platform/workspace/api/workspaceApi.ts

# Search for how PrimeVue Toast component is used in templates
rg "Toast" src/App.vue -A 3 -B 3

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 548


🏁 Script executed:

# Find the AcceptInviteResponse type
grep -n "AcceptInviteResponse" src/platform/workspace/api/workspaceApi.ts

# Check where Toast component is mounted in the app
fd "App.vue" --type f

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 298


🏁 Script executed:

# Check the AcceptInviteResponse type definition
sed -n '60,70p' src/platform/workspace/api/workspaceApi.ts

# Check the Toast component usage in App.vue
cat -n src/App.vue | grep -A 5 -B 5 "Toast"

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 273


🏁 Script executed:

# Check the Toast component in App.vue
cat -n src/App.vue | head -40

# Search for Toast in the entire src directory
rg "Toast" src/App.vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 1811


🏁 Script executed:

# Search for Toast component in the layouts or main entry
fd "*.vue" -x grep -l "Toast" {} \;

# Check how PrimeVue is configured
fd "main.ts" -o "main.js"

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 488


🏁 Script executed:

# Search for Toast component in Vue files
rg "Toast" src --type vue

# Check the main.ts file
fd main.ts

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 154


🏁 Script executed:

# Check main.ts for Toast configuration
cat -n src/main.ts | head -60

# Search for Toast in TypeScript files
rg "Toast" src --type ts | head -20

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 3969


🏁 Script executed:

# Search for Toast component usage in Vue files (all files)
rg "Toast" src --glob "*.vue" -A 3 -B 3

# Check PrimeVue configuration
cat -n src/main.ts | sed -n '60,100p'

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 50382


🏁 Script executed:

# Check GlobalToast.vue more carefully to understand how detail is rendered
cat -n src/components/toast/GlobalToast.vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 2730


🏁 Script executed:

# Let me check PrimeVue Toast documentation or look for how detail is used
# Search for any v-html usage with toast detail
rg "detail.*v-html|v-html.*detail" src

# Check if there's any sanitization already applied to the translation output
rg "DOMPurify" src/platform/workspace --type ts

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 52


Reconsider disabling i18n escaping for workspaceName.

While PrimeVue Toast's detail property renders as plain text by default (not HTML), disabling escaping with escapeParameter: false is unnecessary and contradicts the coding guideline to "Sanitize HTML with DOMPurify" and "Validate trusted sources." Remove the escapeParameter option to keep default escaping:

Suggested fix
-        detail: t(
-          'workspace.addedToWorkspace',
-          { workspaceName: result.workspaceName },
-          { escapeParameter: false }
-        ),
+        detail: t('workspace.addedToWorkspace', {
+          workspaceName: result.workspaceName
+        }),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
detail: t(
'workspace.addedToWorkspace',
{ workspaceName: result.workspaceName },
{ escapeParameter: false }
),
detail: t('workspace.addedToWorkspace', {
workspaceName: result.workspaceName
}),
🤖 Prompt for AI Agents
In `@src/platform/workspace/composables/useInviteUrlLoader.ts` around lines 84 -
88, In useInviteUrlLoader (the t(...) call for 'workspace.addedToWorkspace')
remove the explicit escapeParameter: false option so the i18n call becomes
t('workspace.addedToWorkspace', { workspaceName: result.workspaceName }) and let
the default escaping remain; this ensures workspaceName is not unescaped and
follows the project's sanitization/trusted-source guidelines.

Copy link
Contributor

@christian-byrne christian-byrne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with nits


const dialogStore = useDialogStore()

function getDialogPt(item: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great

memberMenu.value?.toggle(event)
}

function isCurrentUser(member: WorkspaceMember): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't have any stable reference FK for users in the member list, I would want to create some abstraction on the currentUser concept that is like key. For now we would just duplicate the email value but if we later switch to a real key then refactoring will be a lot easier. WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit (not blocking): are any pieces of the UI here potentially going to be used elsewhere? Might be worth extracting them if so. Potential way to decompose it:

MembersPanelContent.vue
├── MembersPanelHeader.vue (search + count display)
├── MembersTabBar.vue (active/pending tabs + sort buttons)
├── MembersList.vue (active members rendering)
│   └── MemberListItem.vue
├── PendingInvitesList.vue (pending invites rendering)  
│   └── PendingInviteItem.vue
└── composables/
    └── useMemberFiltering.ts (search, sort, filter logic)

Comment on lines +175 to +205
<UserAvatar
class="size-8"
:photo-url="
isCurrentUser(member) ? userPhotoUrl : undefined
"
:pt:icon:class="{
'text-xl!': !isCurrentUser(member) || !userPhotoUrl
}"
/>
<div class="flex min-w-0 flex-1 flex-col gap-1">
<div class="flex items-center gap-2">
<span class="text-sm text-base-foreground">
{{ member.name }}
<span
v-if="isCurrentUser(member)"
class="text-muted-foreground"
>
({{ $t('g.you') }})
</span>
</span>
<span
v-if="uiConfig.showRoleBadge"
class="text-[10px] font-bold uppercase text-base-background bg-base-foreground px-1 py-0.5 rounded-full"
>
{{ getRoleBadgeLabel(member.role) }}
</span>
</div>
<span class="text-sm text-muted-foreground">
{{ member.email }}
</span>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit/question: Since this piece is very similar to the other above, would it be worth creating something like MemberListItem.vue?

Comment on lines +195 to +200
<span
v-if="uiConfig.showRoleBadge"
class="text-[10px] font-bold uppercase text-base-background bg-base-foreground px-1 py-0.5 rounded-full"
>
{{ getRoleBadgeLabel(member.role) }}
</span>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Do you want to create a RoleBadge.vue or add a variant to the existing Badge component? It looks like it's in this component twice and WorkspaceSwitcherPopover.vue as well

Comment on lines +235 to +236
fetchMembers()
fetchPendingInvites()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need a loading state or is it handled in some way already? E.g., useAsyncState (or just manual) and showing our spinner until resolve.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: We have some existing patterns for things like forms / validation / email validation in the codebase that might be worth standardizing on for this component.

Comment on lines +125 to +126
type SubscriptionPlan = 'PRO_MONTHLY' | 'PRO_YEARLY' | null

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coderabbit comment above seems valid.

const workflowPersistence = useWorkflowPersistence()
const { flags } = useFeatureFlags()
// Set up invite loader during setup phase so useRoute/useRouter work correctly
const inviteUrlLoader = isCloud ? useInviteUrlLoader() : null
Copy link
Contributor

@christian-byrne christian-byrne Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be better if the composable returned a no-op if the feature flag is off (to encapsulate complex/nuanced logic), rather than requiring callers to understand the nuance.

Copy link
Contributor

@christian-byrne christian-byrne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also same nit from previous PR where I feel this is somewhat confusing right now:

async function getAuthHeaderOrThrow() { ... }      // workspace token
async function getFirebaseHeaderOrThrow() { ... }

just in the sense that the distinction is not conveyed super explicitly by way of variable/function names. Wish we could encapsulate the nuance in the auth modules somehow.

@simula-r simula-r merged commit 4771565 into main Jan 24, 2026
28 checks passed
@simula-r simula-r deleted the workspaces-4-members-invites branch January 24, 2026 23:52
@github-actions
Copy link

⚠️ Backport to cloud/1.37 failed

Reason: Merge conflicts detected during cherry-pick of 4771565

📄 Conflicting files
src/components/dialog/GlobalDialog.vue
src/locales/en/main.json
src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.ts
🤖 Prompt for AI Agents
Backport PR #8245 (https://github.com/Comfy-Org/ComfyUI_frontend/pull/8245) to cloud/1.37.
Cherry-pick merge commit 4771565486e34db560a23fa931fbe3208f4132cd onto new branch
backport-8245-to-cloud-1.37 from origin/cloud/1.37.
Resolve conflicts in: src/components/dialog/GlobalDialog.vue src/locales/en/main.json src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.ts .
For test snapshots (browser_tests/**/*-snapshots/), accept PR version if
changed in original PR, else keep target. For package.json versions, keep
target branch. For pnpm-lock.yaml, regenerate with pnpm install.
Ask user for non-obvious conflicts.
Create PR titled "[backport cloud/1.37] <original title>" with label "backport".
See .github/workflows/pr-backport.yaml for workflow details.

cc @simula-r

simula-r added a commit that referenced this pull request Jan 25, 2026
Add team workspace member management and invite system.

- Add members panel with role management (owner/admin/member) and member removal
- Add invite system with email invites, pending invite display, and revoke functionality
- Add invite URL loading for accepting invites
- Add subscription panel updates for member management
- Add i18n translations for member and invite features

(cherry picked from commit 4771565)
simula-r added a commit that referenced this pull request Jan 25, 2026
## Summary

Backport of #8245 to cloud/1.37.

Add team workspace member management and invite system.

- Add members panel with role management (owner/admin/member) and member
removal
- Add invite system with email invites, pending invite display, and
revoke functionality
- Add invite URL loading for accepting invites
- Add subscription panel updates for member management
- Add i18n translations for member and invite features

## Conflict Resolution

- `src/components/dialog/GlobalDialog.vue`: Added missing
`DialogPassThroughOptions` import
- `src/locales/en/main.json`: Kept "nightly" section from main (was
present before PR)
- `src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.ts`:
Deleted (file doesn't exist in cloud/1.37, only contains unrelated
method rename)

(cherry picked from commit 4771565)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8301-backport-cloud-1-37-Workspaces-4-members-invites-2f36d73d36508119a388dac9d290efbd)
by [Unito](https://www.unito.io)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-backport Fix/change that needs to be cherry-picked to the current feature freeze branch preview-cpu Creates a preview ephemeral environment for this PR (CPU only) size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants