From a08ccb55c17e58838a7335da6661de7783c7b998 Mon Sep 17 00:00:00 2001 From: Simula_r <18093452+simula-r@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:54:41 -0800 Subject: [PATCH] Workspaces 3 create a workspace (#8221) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Add workspace creation and management (create, edit, delete, leave, switch workspaces). Follow-up PR will add invite and membership flow. ## Changes - Workspace CRUD dialogs - Workspace switcher popover in topbar - Workspace settings panel - Subscription panel for workspace context ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8221-Workspaces-3-create-a-workspace-2ef6d73d36508155975ffa6e315971ec) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude Opus 4.5 --- src/components/common/WorkspaceProfilePic.vue | 43 ++ src/components/dialog/GlobalDialog.vue | 38 +- .../dialog/content/setting/WorkspacePanel.vue | 11 + .../content/setting/WorkspacePanelContent.vue | 163 +++++++ .../content/setting/WorkspaceSidebarItem.vue | 19 + .../CreateWorkspaceDialogContent.vue | 113 +++++ .../DeleteWorkspaceDialogContent.vue | 89 ++++ .../workspace/EditWorkspaceDialogContent.vue | 104 +++++ .../workspace/LeaveWorkspaceDialogContent.vue | 78 ++++ src/components/topbar/CurrentUserButton.vue | 48 +- .../topbar/CurrentUserPopoverWorkspace.vue | 337 ++++++++++++++ .../topbar/WorkspaceSwitcherPopover.vue | 166 +++++++ src/locales/en/main.json | 64 ++- .../components/SubscribeButton.vue | 6 +- .../components/SubscriptionPanel.vue | 381 +-------------- .../SubscriptionPanelContentLegacy.vue | 357 ++++++++++++++ .../SubscriptionPanelContentWorkspace.vue | 435 ++++++++++++++++++ .../composables/useSubscription.test.ts | 2 +- .../composables/useSubscription.ts | 6 +- .../components/SettingDialogContent.vue | 61 ++- .../settings/composables/useSettingUI.ts | 106 ++++- src/platform/workspace/api/workspaceApi.ts | 4 +- .../workspace/composables/useWorkspaceUI.ts | 125 +++++ .../stores/teamWorkspaceStore.test.ts | 23 +- .../workspace/stores/teamWorkspaceStore.ts | 152 ++++-- src/services/dialogService.ts | 76 ++- src/stores/firebaseAuthStore.ts | 3 +- src/views/GraphView.vue | 27 +- 28 files changed, 2595 insertions(+), 442 deletions(-) create mode 100644 src/components/common/WorkspaceProfilePic.vue create mode 100644 src/components/dialog/content/setting/WorkspacePanel.vue create mode 100644 src/components/dialog/content/setting/WorkspacePanelContent.vue create mode 100644 src/components/dialog/content/setting/WorkspaceSidebarItem.vue create mode 100644 src/components/dialog/content/workspace/CreateWorkspaceDialogContent.vue create mode 100644 src/components/dialog/content/workspace/DeleteWorkspaceDialogContent.vue create mode 100644 src/components/dialog/content/workspace/EditWorkspaceDialogContent.vue create mode 100644 src/components/dialog/content/workspace/LeaveWorkspaceDialogContent.vue create mode 100644 src/components/topbar/CurrentUserPopoverWorkspace.vue create mode 100644 src/components/topbar/WorkspaceSwitcherPopover.vue create mode 100644 src/platform/cloud/subscription/components/SubscriptionPanelContentLegacy.vue create mode 100644 src/platform/cloud/subscription/components/SubscriptionPanelContentWorkspace.vue create mode 100644 src/platform/workspace/composables/useWorkspaceUI.ts diff --git a/src/components/common/WorkspaceProfilePic.vue b/src/components/common/WorkspaceProfilePic.vue new file mode 100644 index 000000000..642317267 --- /dev/null +++ b/src/components/common/WorkspaceProfilePic.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/components/dialog/GlobalDialog.vue b/src/components/dialog/GlobalDialog.vue index afc056d61..2a1f0ef3d 100644 --- a/src/components/dialog/GlobalDialog.vue +++ b/src/components/dialog/GlobalDialog.vue @@ -4,7 +4,12 @@ v-for="item in dialogStore.dialogStack" :key="item.key" v-model:visible="item.visible" - class="global-dialog" + :class="[ + 'global-dialog', + item.key === 'global-settings' && teamWorkspacesEnabled + ? 'settings-dialog-workspace' + : '' + ]" v-bind="item.dialogComponentProps" :pt="item.dialogComponentProps.pt" :aria-labelledby="item.key" @@ -38,7 +43,15 @@ @@ -55,4 +68,27 @@ const dialogStore = useDialogStore() @apply p-2 2xl:p-[var(--p-dialog-content-padding)]; @apply pt-0; } + +/* Workspace mode: wider settings dialog */ +.settings-dialog-workspace { + width: 100%; + max-width: 1440px; +} + +.settings-dialog-workspace .p-dialog-content { + width: 100%; +} + +.manager-dialog { + height: 80vh; + max-width: 1724px; + max-height: 1026px; +} + +@media (min-width: 3000px) { + .manager-dialog { + max-width: 2200px; + max-height: 1320px; + } +} diff --git a/src/components/dialog/content/setting/WorkspacePanel.vue b/src/components/dialog/content/setting/WorkspacePanel.vue new file mode 100644 index 000000000..aff8f3733 --- /dev/null +++ b/src/components/dialog/content/setting/WorkspacePanel.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/components/dialog/content/setting/WorkspacePanelContent.vue b/src/components/dialog/content/setting/WorkspacePanelContent.vue new file mode 100644 index 000000000..9366a573f --- /dev/null +++ b/src/components/dialog/content/setting/WorkspacePanelContent.vue @@ -0,0 +1,163 @@ + + + diff --git a/src/components/dialog/content/setting/WorkspaceSidebarItem.vue b/src/components/dialog/content/setting/WorkspaceSidebarItem.vue new file mode 100644 index 000000000..cab92c7a8 --- /dev/null +++ b/src/components/dialog/content/setting/WorkspaceSidebarItem.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/components/dialog/content/workspace/CreateWorkspaceDialogContent.vue b/src/components/dialog/content/workspace/CreateWorkspaceDialogContent.vue new file mode 100644 index 000000000..b9444ce58 --- /dev/null +++ b/src/components/dialog/content/workspace/CreateWorkspaceDialogContent.vue @@ -0,0 +1,113 @@ + + + diff --git a/src/components/dialog/content/workspace/DeleteWorkspaceDialogContent.vue b/src/components/dialog/content/workspace/DeleteWorkspaceDialogContent.vue new file mode 100644 index 000000000..dea2da18d --- /dev/null +++ b/src/components/dialog/content/workspace/DeleteWorkspaceDialogContent.vue @@ -0,0 +1,89 @@ + + + diff --git a/src/components/dialog/content/workspace/EditWorkspaceDialogContent.vue b/src/components/dialog/content/workspace/EditWorkspaceDialogContent.vue new file mode 100644 index 000000000..62b650a4e --- /dev/null +++ b/src/components/dialog/content/workspace/EditWorkspaceDialogContent.vue @@ -0,0 +1,104 @@ + + + diff --git a/src/components/dialog/content/workspace/LeaveWorkspaceDialogContent.vue b/src/components/dialog/content/workspace/LeaveWorkspaceDialogContent.vue new file mode 100644 index 000000000..6a3d16c36 --- /dev/null +++ b/src/components/dialog/content/workspace/LeaveWorkspaceDialogContent.vue @@ -0,0 +1,78 @@ + + + diff --git a/src/components/topbar/CurrentUserButton.vue b/src/components/topbar/CurrentUserButton.vue index 151dfd405..849a9ba25 100644 --- a/src/components/topbar/CurrentUserButton.vue +++ b/src/components/topbar/CurrentUserButton.vue @@ -1,4 +1,4 @@ - + diff --git a/src/components/topbar/WorkspaceSwitcherPopover.vue b/src/components/topbar/WorkspaceSwitcherPopover.vue new file mode 100644 index 000000000..c8236535d --- /dev/null +++ b/src/components/topbar/WorkspaceSwitcherPopover.vue @@ -0,0 +1,166 @@ + + + diff --git a/src/locales/en/main.json b/src/locales/en/main.json index fbb3b965f..e7daacbd5 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -1286,7 +1286,10 @@ "VueNodes": "Nodes 2.0", "Nodes 2_0": "Nodes 2.0", "Execution": "Execution", - "PLY": "PLY" + "PLY": "PLY", + "Workspace": "Workspace", + "General": "General", + "Other": "Other" }, "serverConfigItems": { "listen": { @@ -2010,6 +2013,8 @@ "renewsDate": "Renews {date}", "expiresDate": "Expires {date}", "manageSubscription": "Manage subscription", + "managePayment": "Manage Payment", + "cancelSubscription": "Cancel Subscription", "partnerNodesBalance": "\"Partner Nodes\" Credit Balance", "partnerNodesDescription": "For running commercial/proprietary models", "totalCredits": "Total credits", @@ -2064,6 +2069,9 @@ "subscribeToRunFull": "Subscribe to Run", "subscribeNow": "Subscribe Now", "subscribeToComfyCloud": "Subscribe to Comfy Cloud", + "workspaceNotSubscribed": "This workspace is not on a subscription", + "subscriptionRequiredMessage": "A subscription is required for members to run workflows on Cloud", + "contactOwnerToSubscribe": "Contact the workspace owner to subscribe", "description": "Choose the best plan for you", "haveQuestions": "Have questions or wondering about enterprise?", "contactUs": "Contact us", @@ -2099,12 +2107,64 @@ "userSettings": { "title": "My Account Settings", "accountSettings": "Account settings", + "workspaceSettings": "Workspace settings", "name": "Name", "email": "Email", "provider": "Sign-in Provider", "notSet": "Not set", "updatePassword": "Update Password" }, + "workspacePanel": { + "tabs": { + "planCredits": "Plan & Credits" + }, + "menu": { + "editWorkspace": "Edit workspace details", + "leaveWorkspace": "Leave Workspace", + "deleteWorkspace": "Delete Workspace", + "deleteWorkspaceDisabledTooltip": "Cancel your workspace's active subscription first" + }, + "editWorkspaceDialog": { + "title": "Edit workspace details", + "nameLabel": "Workspace name", + "save": "Save" + }, + "leaveDialog": { + "title": "Leave this workspace?", + "message": "You won't be able to join again unless you contact the workspace owner.", + "leave": "Leave" + }, + "deleteDialog": { + "title": "Delete this workspace?", + "message": "Any unused credits or unsaved assets will be lost. This action cannot be undone.", + "messageWithName": "Delete \"{name}\"? Any unused credits or unsaved assets will be lost. This action cannot be undone." + }, + "createWorkspaceDialog": { + "title": "Create a new workspace", + "message": "Workspaces let members share a single credits pool. You'll become the owner after creating this.", + "nameLabel": "Workspace name*", + "namePlaceholder": "Enter workspace name", + "create": "Create" + }, + "toast": { + "workspaceUpdated": { + "title": "Workspace updated", + "message": "Workspace details have been saved." + }, + "failedToUpdateWorkspace": "Failed to update workspace", + "failedToCreateWorkspace": "Failed to create workspace", + "failedToDeleteWorkspace": "Failed to delete workspace", + "failedToLeaveWorkspace": "Failed to leave workspace" + } + }, + "workspaceSwitcher": { + "switchWorkspace": "Switch workspace", + "subscribe": "Subscribe", + "roleOwner": "Owner", + "roleMember": "Member", + "createWorkspace": "Create new workspace", + "maxWorkspacesReached": "You can only own 10 workspaces. Delete one to create a new one." + }, "selectionToolbox": { "executeButton": { "tooltip": "Execute to selected output nodes (Highlighted with orange border)", @@ -2667,4 +2727,4 @@ "tooltip": "You are using a nightly version of ComfyUI. Please use the feedback button to share your thoughts about these features." } } -} \ No newline at end of file +} diff --git a/src/platform/cloud/subscription/components/SubscribeButton.vue b/src/platform/cloud/subscription/components/SubscribeButton.vue index 1cf5096ce..8b7f05e79 100644 --- a/src/platform/cloud/subscription/components/SubscribeButton.vue +++ b/src/platform/cloud/subscription/components/SubscribeButton.vue @@ -2,7 +2,7 @@ - - - - - - -
-
-
-
- - -
-
- {{ $t('subscription.totalCredits') }} -
- -
- {{ totalCredits }} -
-
- - - - - - - - - - - - - -
- - {{ includedCreditsDisplay }} - - {{ creditsRemainingLabel }} -
- - {{ prepaidCredits }} - - {{ $t('subscription.creditsYouveAdded') }} -
- -
- - {{ $t('subscription.viewUsageHistory') }} - - -
-
-
-
- -
-
- {{ $t('subscription.yourPlanIncludes') }} -
- -
-
- - - {{ benefit.value }} - - - {{ benefit.label }} - -
-
-
-
- - - - - + + + +
- - diff --git a/src/platform/cloud/subscription/components/SubscriptionPanelContentLegacy.vue b/src/platform/cloud/subscription/components/SubscriptionPanelContentLegacy.vue new file mode 100644 index 000000000..a6d5f063b --- /dev/null +++ b/src/platform/cloud/subscription/components/SubscriptionPanelContentLegacy.vue @@ -0,0 +1,357 @@ + + + + + diff --git a/src/platform/cloud/subscription/components/SubscriptionPanelContentWorkspace.vue b/src/platform/cloud/subscription/components/SubscriptionPanelContentWorkspace.vue new file mode 100644 index 000000000..7605849e8 --- /dev/null +++ b/src/platform/cloud/subscription/components/SubscriptionPanelContentWorkspace.vue @@ -0,0 +1,435 @@ + + + + + diff --git a/src/platform/cloud/subscription/composables/useSubscription.test.ts b/src/platform/cloud/subscription/composables/useSubscription.test.ts index 9409709d3..ffa370a90 100644 --- a/src/platform/cloud/subscription/composables/useSubscription.test.ts +++ b/src/platform/cloud/subscription/composables/useSubscription.test.ts @@ -64,7 +64,7 @@ vi.mock('@/services/dialogService', () => ({ vi.mock('@/stores/firebaseAuthStore', () => ({ useFirebaseAuthStore: vi.fn(() => ({ - getAuthHeader: mockGetAuthHeader + getFirebaseAuthHeader: mockGetAuthHeader })), FirebaseAuthStoreError: class extends Error {} })) diff --git a/src/platform/cloud/subscription/composables/useSubscription.ts b/src/platform/cloud/subscription/composables/useSubscription.ts index e80c3642e..f5948ee69 100644 --- a/src/platform/cloud/subscription/composables/useSubscription.ts +++ b/src/platform/cloud/subscription/composables/useSubscription.ts @@ -38,7 +38,7 @@ function useSubscriptionInternal() { const { reportError, accessBillingPortal } = useFirebaseAuthActions() const { showSubscriptionRequiredDialog } = useDialogService() - const { getAuthHeader } = useFirebaseAuthStore() + const { getFirebaseAuthHeader } = useFirebaseAuthStore() const { wrapWithErrorHandlingAsync } = useErrorHandling() const { isLoggedIn } = useCurrentUser() @@ -168,7 +168,7 @@ function useSubscriptionInternal() { * @returns Subscription status or null if no subscription exists */ async function fetchSubscriptionStatus(): Promise { - const authHeader = await getAuthHeader() + const authHeader = await getFirebaseAuthHeader() if (!authHeader) { throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated')) } @@ -217,7 +217,7 @@ function useSubscriptionInternal() { const initiateSubscriptionCheckout = async (): Promise => { - const authHeader = await getAuthHeader() + const authHeader = await getFirebaseAuthHeader() if (!authHeader) { throw new FirebaseAuthStoreError( t('toastMessages.userNotAuthenticated') diff --git a/src/platform/settings/components/SettingDialogContent.vue b/src/platform/settings/components/SettingDialogContent.vue index 13da10437..628301735 100644 --- a/src/platform/settings/components/SettingDialogContent.vue +++ b/src/platform/settings/components/SettingDialogContent.vue @@ -1,6 +1,18 @@