Workspaces 3 create a workspace (#8221)

## 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 <noreply@anthropic.com>
This commit is contained in:
Simula_r
2026-01-22 13:54:41 -08:00
committed by GitHub
parent 5c142275ad
commit a08ccb55c1
28 changed files with 2595 additions and 442 deletions

View File

@@ -3,19 +3,21 @@ import type { Component } from 'vue'
import { useI18n } from 'vue-i18n'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
import { isCloud } from '@/platform/distribution/types'
import type { SettingTreeNode } from '@/platform/settings/settingStore'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { SettingParams } from '@/platform/settings/types'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import { isElectron } from '@/utils/envUtil'
import { normalizeI18nKey } from '@/utils/formatUtil'
import { buildTree } from '@/utils/treeUtil'
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
interface SettingPanelItem {
node: SettingTreeNode
component: Component
props?: Record<string, unknown>
}
export function useSettingUI(
@@ -27,15 +29,21 @@ export function useSettingUI(
| 'user'
| 'credits'
| 'subscription'
| 'workspace'
) {
const { t } = useI18n()
const { isLoggedIn } = useCurrentUser()
const settingStore = useSettingStore()
const activeCategory = ref<SettingTreeNode | null>(null)
const { flags } = useFeatureFlags()
const { shouldRenderVueNodes } = useVueFeatureFlags()
const { isActiveSubscription } = useSubscription()
const teamWorkspacesEnabled = computed(
() => isCloud && flags.teamWorkspacesEnabled
)
const settingRoot = computed<SettingTreeNode>(() => {
const root = buildTree(
Object.values(settingStore.settingsById).filter(
@@ -64,6 +72,33 @@ export function useSettingUI(
() => settingRoot.value.children ?? []
)
// Core setting categories (built-in to ComfyUI) in display order
// 'Other' includes floating settings that don't have a specific category
const CORE_CATEGORIES_ORDER = [
'Comfy',
'LiteGraph',
'Appearance',
'3D',
'Mask Editor',
'Other'
]
const CORE_CATEGORIES = new Set(CORE_CATEGORIES_ORDER)
const coreSettingCategories = computed<SettingTreeNode[]>(() => {
const categories = settingCategories.value.filter((node) =>
CORE_CATEGORIES.has(node.label)
)
return categories.sort(
(a, b) =>
CORE_CATEGORIES_ORDER.indexOf(a.label) -
CORE_CATEGORIES_ORDER.indexOf(b.label)
)
})
const customNodeSettingCategories = computed<SettingTreeNode[]>(() =>
settingCategories.value.filter((node) => !CORE_CATEGORIES.has(node.label))
)
// Define panel items
const aboutPanel: SettingPanelItem = {
node: {
@@ -118,6 +153,22 @@ export function useSettingUI(
)
}
// Workspace panel: only available on cloud with team workspaces enabled
const workspacePanel: SettingPanelItem = {
node: {
key: 'workspace',
label: 'Workspace',
children: []
},
component: defineAsyncComponent(
() => import('@/components/dialog/content/setting/WorkspacePanel.vue')
)
}
const shouldShowWorkspacePanel = computed(
() => teamWorkspacesEnabled.value && isLoggedIn.value
)
const keybindingPanel: SettingPanelItem = {
node: {
key: 'keybinding',
@@ -156,13 +207,14 @@ export function useSettingUI(
aboutPanel,
creditsPanel,
userPanel,
...(shouldShowWorkspacePanel.value ? [workspacePanel] : []),
keybindingPanel,
extensionPanel,
...(isElectron() ? [serverConfigPanel] : []),
...(shouldShowPlanCreditsPanel.value && subscriptionPanel
? [subscriptionPanel]
: [])
].filter((panel) => panel.component)
].filter((panel) => panel !== null && panel.component)
)
/**
@@ -186,7 +238,47 @@ export function useSettingUI(
)
})
const groupedMenuTreeNodes = computed<SettingTreeNode[]>(() => [
// Sidebar structure when team workspaces is enabled
const workspaceMenuTreeNodes = computed<SettingTreeNode[]>(() => [
// Workspace settings
translateCategory({
key: 'workspace',
label: 'Workspace',
children: [
...(shouldShowWorkspacePanel.value ? [workspacePanel.node] : []),
...(isLoggedIn.value &&
!(isCloud && window.__CONFIG__?.subscription_required)
? [creditsPanel.node]
: [])
].map(translateCategory)
}),
// General settings - Profile + all core settings + special panels
translateCategory({
key: 'general',
label: 'General',
children: [
translateCategory(userPanel.node),
...coreSettingCategories.value.map(translateCategory),
translateCategory(keybindingPanel.node),
translateCategory(extensionPanel.node),
translateCategory(aboutPanel.node),
...(isElectron() ? [translateCategory(serverConfigPanel.node)] : [])
]
}),
// Custom node settings (only shown if custom nodes have registered settings)
...(customNodeSettingCategories.value.length > 0
? [
translateCategory({
key: 'other',
label: 'Other',
children: customNodeSettingCategories.value.map(translateCategory)
})
]
: [])
])
// Sidebar structure when team workspaces is disabled (legacy)
const legacyMenuTreeNodes = computed<SettingTreeNode[]>(() => [
// Account settings - show different panels based on distribution and auth state
{
key: 'account',
@@ -223,6 +315,12 @@ export function useSettingUI(
}
])
const groupedMenuTreeNodes = computed<SettingTreeNode[]>(() =>
teamWorkspacesEnabled.value
? workspaceMenuTreeNodes.value
: legacyMenuTreeNodes.value
)
onMounted(() => {
activeCategory.value = defaultCategory.value
})