diff --git a/browser_tests/tests/propertiesPanel/propertiesPanel.spec.ts b/browser_tests/tests/propertiesPanel/propertiesPanel.spec.ts
index 9ff32c8a4..7370a92d5 100644
--- a/browser_tests/tests/propertiesPanel/propertiesPanel.spec.ts
+++ b/browser_tests/tests/propertiesPanel/propertiesPanel.spec.ts
@@ -9,7 +9,7 @@ test.describe('Properties panel', () => {
const { propertiesPanel } = comfyPage.menu
await expect(propertiesPanel.panelTitle).toContainText(
- 'No node(s) selected'
+ 'No item(s) selected'
)
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
diff --git a/src/components/common/WorkspaceProfilePic.vue b/src/components/common/WorkspaceProfilePic.vue
index 642317267..bc147a61c 100644
--- a/src/components/common/WorkspaceProfilePic.vue
+++ b/src/components/common/WorkspaceProfilePic.vue
@@ -1,6 +1,6 @@
@@ -41,11 +41,14 @@
diff --git a/src/components/dialog/content/ConfirmationDialogContent.vue b/src/components/dialog/content/ConfirmationDialogContent.vue
index 5dbb6cd9e..556379b3f 100644
--- a/src/components/dialog/content/ConfirmationDialogContent.vue
+++ b/src/components/dialog/content/ConfirmationDialogContent.vue
@@ -31,7 +31,12 @@
}}
-
+
+
+ {{ $t('g.ok') }}
+
diff --git a/src/components/dialog/content/setting/MembersPanelContent.vue b/src/components/dialog/content/setting/MembersPanelContent.vue
new file mode 100644
index 000000000..46a455e35
--- /dev/null
+++ b/src/components/dialog/content/setting/MembersPanelContent.vue
@@ -0,0 +1,511 @@
+
+
+
+
+
+
+
+
+ {{
+ $t('workspacePanel.members.membersCount', {
+ count: members.length
+ })
+ }}
+
+
+ {{
+ $t(
+ 'workspacePanel.members.pendingInvitesCount',
+ pendingInvites.length
+ )
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('workspacePanel.members.tabs.active') }}
+
+
+ {{
+ $t(
+ 'workspacePanel.members.tabs.pendingCount',
+ pendingInvites.length
+ )
+ }}
+
+
+
+
+
+ {{ $t('workspacePanel.members.columns.inviteDate') }}
+
+
+
+ {{ $t('workspacePanel.members.columns.expiryDate') }}
+
+
+
+
+
+
+ {{ $t('workspacePanel.members.columns.joinDate') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ userDisplayName }}
+
+ ({{ $t('g.you') }})
+
+
+
+ {{ $t('workspaceSwitcher.roleOwner') }}
+
+
+
+ {{ userEmail }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ member.name }}
+
+ ({{ $t('g.you') }})
+
+
+
+ {{ getRoleBadgeLabel(member.role) }}
+
+
+
+ {{ member.email }}
+
+
+
+
+
+ {{ formatDate(member.joinDate) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ getInviteInitial(invite.email) }}
+
+
+
+
+ {{ getInviteDisplayName(invite.email) }}
+
+
+ {{ invite.email }}
+
+
+
+
+
+ {{ formatDate(invite.inviteDate) }}
+
+
+
+ {{ formatDate(invite.expiryDate) }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('workspacePanel.members.noInvites') }}
+
+
+
+
+
+
+
+
+ {{ $t('workspacePanel.members.personalWorkspaceMessage') }}
+
+
+ {{ $t('workspacePanel.members.createNewWorkspace') }}
+
+
+
+
+
+
diff --git a/src/components/dialog/content/setting/WorkspacePanelContent.vue b/src/components/dialog/content/setting/WorkspacePanelContent.vue
index 9366a573f..483711649 100644
--- a/src/components/dialog/content/setting/WorkspacePanelContent.vue
+++ b/src/components/dialog/content/setting/WorkspacePanelContent.vue
@@ -9,17 +9,66 @@
{{ workspaceName }}
-
+
-
- {{ $t('workspacePanel.tabs.planCredits') }}
+
+
+ {{ $t('workspacePanel.tabs.planCredits') }}
+
+
+ {{
+ $t('workspacePanel.tabs.membersCount', {
+ count: isInPersonalWorkspace ? 1 : members.length
+ })
+ }}
+
-
+
+ {{ $t('workspacePanel.invite') }}
+
+
@@ -36,7 +85,7 @@
:class="[
'flex items-center gap-2 px-3 py-2',
item.class,
- item.disabled ? 'pointer-events-auto' : ''
+ item.disabled ? 'pointer-events-auto' : 'cursor-pointer'
]"
@click="
item.command?.({
@@ -53,9 +102,12 @@
-
+
-
+
+
+
+
@@ -74,11 +126,14 @@ import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import WorkspaceProfilePic from '@/components/common/WorkspaceProfilePic.vue'
+import MembersPanelContent from '@/components/dialog/content/setting/MembersPanelContent.vue'
import Button from '@/components/ui/button/Button.vue'
-import SubscriptionPanelContent from '@/platform/cloud/subscription/components/SubscriptionPanelContentWorkspace.vue'
+import { buttonVariants } from '@/components/ui/button/button.variants'
+import SubscriptionPanelContentWorkspace from '@/platform/cloud/subscription/components/SubscriptionPanelContentWorkspace.vue'
import { useWorkspaceUI } from '@/platform/workspace/composables/useWorkspaceUI'
import { useTeamWorkspaceStore } from '@/platform/workspace/stores/teamWorkspaceStore'
import { useDialogService } from '@/services/dialogService'
+import { cn } from '@/utils/tailwindUtil'
const { defaultTab = 'plan' } = defineProps<{
defaultTab?: string
@@ -88,12 +143,20 @@ const { t } = useI18n()
const {
showLeaveWorkspaceDialog,
showDeleteWorkspaceDialog,
+ showInviteMemberDialog,
showEditWorkspaceDialog
} = useDialogService()
const workspaceStore = useTeamWorkspaceStore()
-const { workspaceName, isWorkspaceSubscribed } = storeToRefs(workspaceStore)
-
-const { activeTab, setActiveTab, permissions, uiConfig } = useWorkspaceUI()
+const {
+ workspaceName,
+ members,
+ isInviteLimitReached,
+ isWorkspaceSubscribed,
+ isInPersonalWorkspace
+} = storeToRefs(workspaceStore)
+const { fetchMembers, fetchPendingInvites } = workspaceStore
+const { activeTab, setActiveTab, workspaceRole, permissions, uiConfig } =
+ useWorkspaceUI()
const menu = ref | null>(null)
@@ -123,6 +186,16 @@ const deleteTooltip = computed(() => {
return tooltipKey ? t(tooltipKey) : null
})
+const inviteTooltip = computed(() => {
+ if (!isInviteLimitReached.value) return null
+ return t('workspacePanel.inviteLimitReached')
+})
+
+function handleInviteMember() {
+ if (isInviteLimitReached.value) return
+ showInviteMemberDialog()
+}
+
const menuItems = computed(() => {
const items = []
@@ -159,5 +232,7 @@ const menuItems = computed(() => {
onMounted(() => {
setActiveTab(defaultTab)
+ fetchMembers()
+ fetchPendingInvites()
})
diff --git a/src/components/dialog/content/workspace/CreateWorkspaceDialogContent.vue b/src/components/dialog/content/workspace/CreateWorkspaceDialogContent.vue
index b9444ce58..6dca4c414 100644
--- a/src/components/dialog/content/workspace/CreateWorkspaceDialogContent.vue
+++ b/src/components/dialog/content/workspace/CreateWorkspaceDialogContent.vue
@@ -79,8 +79,7 @@ const workspaceName = ref('')
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)
})
diff --git a/src/components/dialog/content/workspace/EditWorkspaceDialogContent.vue b/src/components/dialog/content/workspace/EditWorkspaceDialogContent.vue
index 62b650a4e..8fa6213ef 100644
--- a/src/components/dialog/content/workspace/EditWorkspaceDialogContent.vue
+++ b/src/components/dialog/content/workspace/EditWorkspaceDialogContent.vue
@@ -69,7 +69,7 @@ const newWorkspaceName = ref(workspaceStore.workspaceName)
const isValidName = computed(() => {
const name = newWorkspaceName.value.trim()
- 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)
})
diff --git a/src/components/dialog/content/workspace/InviteMemberDialogContent.vue b/src/components/dialog/content/workspace/InviteMemberDialogContent.vue
new file mode 100644
index 000000000..479a6b6d1
--- /dev/null
+++ b/src/components/dialog/content/workspace/InviteMemberDialogContent.vue
@@ -0,0 +1,182 @@
+
+
+
+
+
+ {{
+ step === 'email'
+ ? $t('workspacePanel.inviteMemberDialog.title')
+ : $t('workspacePanel.inviteMemberDialog.linkStep.title')
+ }}
+
+
+
+
+
+
+
+
+
+
+ {{ $t('workspacePanel.inviteMemberDialog.message') }}
+
+
+
+
+
+
+
+ {{ $t('g.cancel') }}
+
+
+ {{ $t('workspacePanel.inviteMemberDialog.createLink') }}
+
+
+
+
+
+
+
+
+ {{ $t('workspacePanel.inviteMemberDialog.linkStep.message') }}
+
+
+ {{ email }}
+
+
+
+
+
+
+
+ {{ $t('g.cancel') }}
+
+
+ {{ $t('workspacePanel.inviteMemberDialog.linkStep.copyLink') }}
+
+
+
+
+
+
+
diff --git a/src/components/dialog/content/workspace/RemoveMemberDialogContent.vue b/src/components/dialog/content/workspace/RemoveMemberDialogContent.vue
new file mode 100644
index 000000000..2d085bec4
--- /dev/null
+++ b/src/components/dialog/content/workspace/RemoveMemberDialogContent.vue
@@ -0,0 +1,83 @@
+
+
+
+
+
+ {{ $t('workspacePanel.removeMemberDialog.title') }}
+
+
+
+
+
+
+
+
+
+ {{ $t('workspacePanel.removeMemberDialog.message') }}
+
+
+
+
+
+
+ {{ $t('g.cancel') }}
+
+
+ {{ $t('workspacePanel.removeMemberDialog.remove') }}
+
+
+
+
+
+
diff --git a/src/components/dialog/content/workspace/RevokeInviteDialogContent.vue b/src/components/dialog/content/workspace/RevokeInviteDialogContent.vue
new file mode 100644
index 000000000..1d9dacd9a
--- /dev/null
+++ b/src/components/dialog/content/workspace/RevokeInviteDialogContent.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+ {{ $t('workspacePanel.revokeInviteDialog.title') }}
+
+
+
+
+
+
+
+
+
+ {{ $t('workspacePanel.revokeInviteDialog.message') }}
+
+
+
+
+
+
+ {{ $t('g.cancel') }}
+
+
+ {{ $t('workspacePanel.revokeInviteDialog.revoke') }}
+
+
+
+
+
+
diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue
index 48d82e44b..b2b526eea 100644
--- a/src/components/graph/GraphCanvas.vue
+++ b/src/components/graph/GraphCanvas.vue
@@ -126,11 +126,13 @@ import { useNodeBadge } from '@/composables/node/useNodeBadge'
import { useCanvasDrop } from '@/composables/useCanvasDrop'
import { useContextMenuTranslation } from '@/composables/useContextMenuTranslation'
import { useCopy } from '@/composables/useCopy'
+import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { useGlobalLitegraph } from '@/composables/useGlobalLitegraph'
import { usePaste } from '@/composables/usePaste'
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
import { mergeCustomNodesI18n, t } from '@/i18n'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
+import { isCloud } from '@/platform/distribution/types'
import { useLitegraphSettings } from '@/platform/settings/composables/useLitegraphSettings'
import { CORE_SETTINGS } from '@/platform/settings/constants/coreSettings'
import { useSettingStore } from '@/platform/settings/settingStore'
@@ -139,6 +141,7 @@ import { useWorkflowService } from '@/platform/workflow/core/services/workflowSe
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useWorkflowAutoSave } from '@/platform/workflow/persistence/composables/useWorkflowAutoSave'
import { useWorkflowPersistence } from '@/platform/workflow/persistence/composables/useWorkflowPersistence'
+import { useInviteUrlLoader } from '@/platform/workspace/composables/useInviteUrlLoader'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
@@ -394,6 +397,9 @@ const loadCustomNodesI18n = async () => {
const comfyAppReady = ref(false)
const workflowPersistence = useWorkflowPersistence()
+const { flags } = useFeatureFlags()
+// Set up invite loader during setup phase so useRoute/useRouter work correctly
+const inviteUrlLoader = isCloud ? useInviteUrlLoader() : null
useCanvasDrop(canvasRef)
useLitegraphSettings()
useNodeBadge()
@@ -459,6 +465,22 @@ onMounted(async () => {
// Load template from URL if present
await workflowPersistence.loadTemplateFromUrlIfPresent()
+ // Accept workspace invite from URL if present (e.g., ?invite=TOKEN)
+ // Uses watch because feature flags load asynchronously - flag may be false initially
+ // then become true once remoteConfig or websocket features are loaded
+ if (inviteUrlLoader) {
+ const stopWatching = watch(
+ () => flags.teamWorkspacesEnabled,
+ async (enabled) => {
+ if (enabled) {
+ stopWatching()
+ await inviteUrlLoader.loadInviteFromUrl()
+ }
+ },
+ { immediate: true }
+ )
+ }
+
// Initialize release store to fetch releases from comfy-api (fire-and-forget)
const { useReleaseStore } =
await import('@/platform/updates/common/releaseStore')
diff --git a/src/components/topbar/CurrentUserButton.vue b/src/components/topbar/CurrentUserButton.vue
index 8a3760ced..4f6f4b6ed 100644
--- a/src/components/topbar/CurrentUserButton.vue
+++ b/src/components/topbar/CurrentUserButton.vue
@@ -27,7 +27,7 @@
:class="compact && 'size-full'"
/>
-
+
diff --git a/src/components/topbar/CurrentUserPopoverWorkspace.vue b/src/components/topbar/CurrentUserPopoverWorkspace.vue
index 51aa19dd5..8484b6033 100644
--- a/src/components/topbar/CurrentUserPopoverWorkspace.vue
+++ b/src/components/topbar/CurrentUserPopoverWorkspace.vue
@@ -36,15 +36,6 @@
{{
workspaceName
}}
-
- {{ workspaceTierName }}
-
-
- {{ $t('workspaceSwitcher.subscribe') }}
-
@@ -92,15 +83,23 @@
>
{{ $t('subscription.addCredits') }}
-
+
+
+
+ {{ $t('workspaceSwitcher.subscribe') }}
+
@@ -198,7 +197,6 @@ import Divider from 'primevue/divider'
import Popover from 'primevue/popover'
import Skeleton from 'primevue/skeleton'
import { computed, onMounted, ref } from 'vue'
-import { useI18n } from 'vue-i18n'
import UserAvatar from '@/components/common/UserAvatar.vue'
import WorkspaceProfilePic from '@/components/common/WorkspaceProfilePic.vue'
@@ -221,8 +219,7 @@ const workspaceStore = useTeamWorkspaceStore()
const {
workspaceName,
isInPersonalWorkspace: isPersonalWorkspace,
- isWorkspaceSubscribed,
- subscriptionPlan
+ isWorkspaceSubscribed
} = storeToRefs(workspaceStore)
const { workspaceRole } = useWorkspaceUI()
const workspaceSwitcherPopover = ref | null>(null)
@@ -240,24 +237,12 @@ const dialogService = useDialogService()
const { isActiveSubscription } = useSubscription()
const { totalCredits, isLoadingBalance } = useSubscriptionCredits()
const subscriptionDialog = useSubscriptionDialog()
-const { t } = useI18n()
-const displayedCredits = computed(() =>
- isWorkspaceSubscribed.value ? totalCredits.value : '0'
-)
-
-// Workspace subscription tier name (not user tier)
-const workspaceTierName = computed(() => {
- if (!isWorkspaceSubscribed.value) return null
- if (!subscriptionPlan.value) return null
- // Convert plan to display name
- if (subscriptionPlan.value === 'PRO_MONTHLY')
- return t('subscription.tiers.pro.name')
- if (subscriptionPlan.value === 'PRO_YEARLY')
- return t('subscription.tierNameYearly', {
- name: t('subscription.tiers.pro.name')
- })
- return null
+const displayedCredits = computed(() => {
+ const isSubscribed = isPersonalWorkspace.value
+ ? isActiveSubscription.value
+ : isWorkspaceSubscribed.value
+ return isSubscribed ? totalCredits.value : '0'
})
const canUpgrade = computed(() => {
diff --git a/src/components/topbar/WorkspaceSwitcherPopover.vue b/src/components/topbar/WorkspaceSwitcherPopover.vue
index 6f8c492c0..631514598 100644
--- a/src/components/topbar/WorkspaceSwitcherPopover.vue
+++ b/src/components/topbar/WorkspaceSwitcherPopover.vue
@@ -38,13 +38,22 @@
:workspace-name="workspace.name"
/>
-
- {{ workspace.name }}
-
-
+
+
+ {{
+ workspace.type === 'personal'
+ ? $t('workspaceSwitcher.personal')
+ : workspace.name
+ }}
+
+
+ {{ getTierLabel(workspace) }}
+
+
+
{{ getRoleLabel(workspace.role) }}
@@ -58,8 +67,6 @@
-
-
(() =>
id: w.id,
name: w.name,
type: w.type,
- role: w.role
+ role: w.role,
+ isSubscribed: w.isSubscribed,
+ subscriptionPlan: w.subscriptionPlan
}))
)
@@ -152,6 +167,22 @@ function getRoleLabel(role: AvailableWorkspace['role']): string {
return ''
}
+function getTierLabel(workspace: AvailableWorkspace): string | null {
+ // Personal workspace: use user's subscription tier
+ if (workspace.type === 'personal') {
+ return userSubscriptionTierName.value || null
+ }
+ // Team workspace: use workspace subscription plan
+ if (!workspace.isSubscribed || !workspace.subscriptionPlan) return null
+ if (workspace.subscriptionPlan === 'PRO_MONTHLY')
+ return t('subscription.tiers.pro.name')
+ if (workspace.subscriptionPlan === 'PRO_YEARLY')
+ return t('subscription.tierNameYearly', {
+ name: t('subscription.tiers.pro.name')
+ })
+ return null
+}
+
async function handleSelectWorkspace(workspace: AvailableWorkspace) {
const success = await switchWithConfirmation(workspace.id)
if (success) {
diff --git a/src/locales/en/main.json b/src/locales/en/main.json
index b85dba38f..55274cd42 100644
--- a/src/locales/en/main.json
+++ b/src/locales/en/main.json
@@ -1,6 +1,7 @@
{
"g": {
"user": "User",
+ "you": "You",
"currentUser": "Current user",
"empty": "Empty",
"noWorkflowsFound": "No workflows found.",
@@ -43,8 +44,6 @@
"comfy": "Comfy",
"refresh": "Refresh",
"refreshNode": "Refresh Node",
- "vitePreloadErrorTitle": "New Version Available",
- "vitePreloadErrorMessage": "A new version of the app has been released. Would you like to reload?\nIf not, some parts of the app might not work as expected.\nFeel free to decline and save your progress before reloading.",
"terminal": "Terminal",
"logs": "Logs",
"videoFailedToLoad": "Video failed to load",
@@ -164,6 +163,7 @@
"choose_file_to_upload": "choose file to upload",
"capture": "capture",
"nodes": "Nodes",
+ "nodesCount": "{count} nodes | {count} node | {count} nodes",
"community": "Community",
"all": "All",
"versionMismatchWarning": "Version Compatibility Warning",
@@ -276,6 +276,7 @@
"1x": "1x",
"2x": "2x",
"beta": "BETA",
+ "nightly": "NIGHTLY",
"profile": "Profile",
"noItems": "No items"
},
@@ -444,6 +445,8 @@
"Save Image": "Save Image",
"Rename": "Rename",
"RenameWidget": "Rename Widget",
+ "FavoriteWidget": "Favorite Widget",
+ "UnfavoriteWidget": "Unfavorite Widget",
"Copy": "Copy",
"Duplicate": "Duplicate",
"Paste": "Paste",
@@ -707,6 +710,8 @@
"noImportedFiles": "No imported files found",
"noGeneratedFiles": "No generated files found",
"generatedAssetsHeader": "Generated assets",
+ "importedAssetsHeader": "Imported assets",
+ "activeJobStatus": "Active job: {status}",
"noFilesFoundMessage": "Upload files or generate content to see them here",
"browseTemplates": "Browse example templates",
"openWorkflow": "Open workflow in local file system",
@@ -756,6 +761,7 @@
"sortJobs": "Sort jobs",
"sortBy": "Sort by",
"activeJobs": "{count} active job | {count} active jobs",
+ "activeJobsShort": "{count} active | {count} active",
"activeJobsSuffix": "active jobs",
"jobQueue": "Job Queue",
"expandCollapsedQueue": "Expand job queue",
@@ -1186,13 +1192,14 @@
"Queue Selected Output Nodes": "Queue Selected Output Nodes",
"Redo": "Redo",
"Refresh Node Definitions": "Refresh Node Definitions",
+ "Rename": "Rename",
"Save": "Save",
"Save As": "Save As",
"Show Settings Dialog": "Show Settings Dialog",
"Experimental: Enable AssetAPI": "Experimental: Enable AssetAPI",
"Canvas Performance": "Canvas Performance",
"Help Center": "Help Center",
- "toggle linear mode": "Toggle Simple Mode",
+ "Toggle Simple Mode": "Toggle Simple Mode",
"Toggle Queue Panel V2": "Toggle Queue Panel V2",
"Toggle Theme (Dark/Light)": "Toggle Theme (Dark/Light)",
"Undo": "Undo",
@@ -1281,7 +1288,6 @@
"Execution": "Execution",
"PLY": "PLY",
"Workspace": "Workspace",
- "General": "General",
"Other": "Other"
},
"serverConfigItems": {
@@ -1433,6 +1439,7 @@
"latent": "latent",
"mask": "mask",
"api node": "api node",
+ "Bria": "Bria",
"video": "video",
"ByteDance": "ByteDance",
"preprocessors": "preprocessors",
@@ -1483,6 +1490,7 @@
"lotus": "lotus",
"LTXV": "LTXV",
"Luma": "Luma",
+ "Meshy": "Meshy",
"MiniMax": "MiniMax",
"model_specific": "model_specific",
"Moonvalley Marey": "Moonvalley Marey",
@@ -1512,6 +1520,7 @@
"": "",
"camera": "camera",
"Wan": "Wan",
+ "WaveSpeed": "WaveSpeed",
"zimage": "zimage"
},
"dataTypes": {
@@ -1552,6 +1561,8 @@
"LUMA_REF": "LUMA_REF",
"MASK": "MASK",
"MESH": "MESH",
+ "MESHY_RIGGED_TASK_ID": "MESHY_RIGGED_TASK_ID",
+ "MESHY_TASK_ID": "MESHY_TASK_ID",
"MODEL": "MODEL",
"MODEL_PATCH": "MODEL_PATCH",
"MODEL_TASK_ID": "MODEL_TASK_ID",
@@ -1722,6 +1733,17 @@
"unsupportedFileType": "Unsupported file type (supports .gltf, .glb, .obj, .fbx, .stl)",
"uploadingModel": "Uploading 3D model..."
},
+ "imageCrop": {
+ "loading": "Loading...",
+ "noInputImage": "No input image connected",
+ "cropPreviewAlt": "Crop preview"
+ },
+ "boundingBox": {
+ "x": "X",
+ "y": "Y",
+ "width": "Width",
+ "height": "Height"
+ },
"toastMessages": {
"nothingToQueue": "Nothing to queue",
"pleaseSelectOutputNodes": "Please select output nodes",
@@ -2079,6 +2101,10 @@
"creator": "30 min",
"pro": "1 hr",
"founder": "30 min"
+ },
+ "billingComingSoon": {
+ "title": "Coming Soon",
+ "message": "Team billing is coming soon. You'll be able to subscribe to a plan for your workspace with per-seat pricing. Stay tuned for updates."
}
},
"userSettings": {
@@ -2092,8 +2118,38 @@
"updatePassword": "Update Password"
},
"workspacePanel": {
+ "invite": "Invite",
+ "inviteMember": "Invite member",
+ "inviteLimitReached": "You've reached the maximum of 50 members",
"tabs": {
- "planCredits": "Plan & Credits"
+ "dashboard": "Dashboard",
+ "planCredits": "Plan & Credits",
+ "membersCount": "Members ({count})"
+ },
+ "dashboard": {
+ "placeholder": "Dashboard workspace settings"
+ },
+ "members": {
+ "membersCount": "{count}/50 Members",
+ "pendingInvitesCount": "{count} pending invite | {count} pending invites",
+ "tabs": {
+ "active": "Active",
+ "pendingCount": "Pending ({count})"
+ },
+ "columns": {
+ "inviteDate": "Invite date",
+ "expiryDate": "Expiry date",
+ "joinDate": "Join date"
+ },
+ "actions": {
+ "copyLink": "Copy invite link",
+ "revokeInvite": "Revoke invite",
+ "removeMember": "Remove member"
+ },
+ "noInvites": "No pending invites",
+ "noMembers": "No members",
+ "personalWorkspaceMessage": "You can't invite other members to your personal workspace right now. To add members to a workspace,",
+ "createNewWorkspace": "create a new one."
},
"menu": {
"editWorkspace": "Edit workspace details",
@@ -2116,6 +2172,32 @@
"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."
},
+ "removeMemberDialog": {
+ "title": "Remove this member?",
+ "message": "This member will be removed from your workspace. Credits they've used will not be refunded.",
+ "remove": "Remove member",
+ "success": "Member removed",
+ "error": "Failed to remove member"
+ },
+ "revokeInviteDialog": {
+ "title": "Uninvite this person?",
+ "message": "This member won't be able to join your workspace anymore. Their invite link will be invalidated.",
+ "revoke": "Uninvite"
+ },
+ "inviteMemberDialog": {
+ "title": "Invite a person to this workspace",
+ "message": "Create a shareable invite link to send to someone",
+ "placeholder": "Enter the person's email",
+ "createLink": "Create link",
+ "linkStep": {
+ "title": "Send this link to the person",
+ "message": "Make sure their account uses this email.",
+ "copyLink": "Copy Link",
+ "done": "Done"
+ },
+ "linkCopied": "Copied",
+ "linkCopyFailed": "Failed to copy link"
+ },
"createWorkspaceDialog": {
"title": "Create a new workspace",
"message": "Workspaces let members share a single credits pool. You'll become the owner after creating this.",
@@ -2124,19 +2206,34 @@
"create": "Create"
},
"toast": {
+ "workspaceCreated": {
+ "title": "Workspace created",
+ "message": "Subscribe to a plan, invite teammates, and start collaborating.",
+ "subscribe": "Subscribe"
+ },
"workspaceUpdated": {
"title": "Workspace updated",
"message": "Workspace details have been saved."
},
+ "workspaceDeleted": {
+ "title": "Workspace deleted",
+ "message": "The workspace has been permanently deleted."
+ },
+ "workspaceLeft": {
+ "title": "Left workspace",
+ "message": "You have left the workspace."
+ },
"failedToUpdateWorkspace": "Failed to update workspace",
"failedToCreateWorkspace": "Failed to create workspace",
"failedToDeleteWorkspace": "Failed to delete workspace",
- "failedToLeaveWorkspace": "Failed to leave workspace"
+ "failedToLeaveWorkspace": "Failed to leave workspace",
+ "failedToFetchWorkspaces": "Failed to load workspaces"
}
},
"workspaceSwitcher": {
"switchWorkspace": "Switch workspace",
"subscribe": "Subscribe",
+ "personal": "Personal",
"roleOwner": "Owner",
"roleMember": "Member",
"createWorkspace": "Create new workspace",
@@ -2152,6 +2249,7 @@
"Set Group Nodes to Always": "Set Group Nodes to Always"
},
"widgets": {
+ "node2only": "Node 2.0 only",
"selectModel": "Select model",
"uploadSelect": {
"placeholder": "Select...",
@@ -2230,6 +2328,7 @@
"renderErrorState": "Render Error State"
},
"cloudOnboarding": {
+ "skipToCloudApp": "Skip to the cloud app",
"survey": {
"title": "Cloud Survey",
"placeholder": "Survey questions placeholder",
@@ -2508,7 +2607,7 @@
"zoom": "Zoom in",
"moreOptions": "More options",
"seeMoreOutputs": "See more outputs",
- "addToWorkflow": "Add to current workflow",
+ "insertAsNodeInWorkflow": "Insert as node in workflow",
"download": "Download",
"openWorkflow": "Open as workflow in new tab",
"exportWorkflow": "Export workflow",
@@ -2529,11 +2628,23 @@
"downloadSelectedAll": "Download all",
"deleteSelected": "Delete",
"deleteSelectedAll": "Delete all",
+ "insertAllAssetsAsNodes": "Insert all assets as nodes",
+ "openWorkflowAll": "Open all workflows",
+ "exportWorkflowAll": "Export all workflows",
"downloadStarted": "Downloading {count} files...",
"downloadsStarted": "Started downloading {count} file(s)",
"assetsDeletedSuccessfully": "{count} asset(s) deleted successfully",
"failedToDeleteAssets": "Failed to delete selected assets",
- "partialDeleteSuccess": "{succeeded} deleted successfully, {failed} failed"
+ "partialDeleteSuccess": "{succeeded} deleted successfully, {failed} failed",
+ "nodesAddedToWorkflow": "{count} node(s) added to workflow",
+ "failedToAddNodes": "Failed to add nodes to workflow",
+ "partialAddNodesSuccess": "{succeeded} added successfully, {failed} failed",
+ "workflowsOpened": "{count} workflow(s) opened in new tabs",
+ "noWorkflowsFound": "No workflow data found in selected assets",
+ "partialWorkflowsOpened": "{succeeded} workflow(s) opened, {failed} failed",
+ "workflowsExported": "{count} workflow(s) exported successfully",
+ "noWorkflowsToExport": "No workflow data found to export",
+ "partialWorkflowsExported": "{succeeded} exported successfully, {failed} failed"
},
"noJobIdFound": "No job ID found for this asset",
"unsupportedFileType": "Unsupported file type for loader node",
@@ -2599,8 +2710,10 @@
"rightSidePanel": {
"togglePanel": "Toggle properties panel",
"noSelection": "Select a node to see its properties and info.",
- "title": "No node(s) selected | 1 node selected | {count} nodes selected",
+ "workflowOverview": "Workflow Overview",
+ "title": "No item(s) selected | 1 item selected | {count} items selected",
"parameters": "Parameters",
+ "nodes": "Nodes",
"info": "Info",
"color": "Node color",
"pinned": "Pinned",
@@ -2610,9 +2723,44 @@
"inputs": "INPUTS",
"inputsNone": "NO INPUTS",
"inputsNoneTooltip": "Node has no inputs",
+ "advancedInputs": "ADVANCED INPUTS",
+ "showAdvancedInputsButton": "Show advanced inputs",
"properties": "Properties",
"nodeState": "Node state",
- "settings": "Settings"
+ "settings": "Settings",
+ "addFavorite": "Favorite",
+ "removeFavorite": "Unfavorite",
+ "hideInput": "Hide input",
+ "showInput": "Show input",
+ "locateNode": "Locate node on canvas",
+ "favorites": "FAVORITED INPUTS",
+ "favoritesNone": "NO FAVORITED INPUTS",
+ "favoritesNoneTooltip": "Star widgets to quickly access them without selecting nodes",
+ "globalSettings": {
+ "title": "Global Settings",
+ "searchPlaceholder": "Search quick settings...",
+ "nodes": "NODES",
+ "canvas": "CANVAS",
+ "connectionLinks": "CONNECTION LINKS",
+ "showAdvanced": "Show advanced parameters",
+ "showAdvancedTooltip": "This is an important setting that when set to TRUE, reveals all advanced parameters for nodes",
+ "showInfoBadges": "Show info badges",
+ "showToolbox": "Show toolbox on selection",
+ "nodes2": "Nodes 2.0",
+ "gridSpacing": "Grid spacing",
+ "snapNodesToGrid": "Snap nodes to grid",
+ "linkShape": "Link shape",
+ "showConnectedLinks": "Show connected links",
+ "viewAllSettings": "View all settings"
+ },
+ "groupSettings": "Group Settings",
+ "groups": "Groups",
+ "favoritesNoneDesc": "Inputs you favorite will show up here",
+ "noneSearchDesc": "No items match your search",
+ "nodesNoneDesc": "NO NODES",
+ "fallbackGroupTitle": "Group",
+ "fallbackNodeTitle": "Node",
+ "hideAdvancedInputsButton": "Hide advanced inputs"
},
"help": {
"recentReleases": "Recent releases",
@@ -2638,7 +2786,10 @@
"unsavedChanges": {
"title": "Unsaved Changes",
"message": "You have unsaved changes. Do you want to discard them and switch workspaces?"
- }
+ },
+ "inviteAccepted": "Invite Accepted",
+ "addedToWorkspace": "You have been added to {workspaceName}",
+ "inviteFailed": "Failed to Accept Invite"
},
"workspaceAuth": {
"errors": {
@@ -2648,5 +2799,12 @@
"workspaceNotFound": "Workspace not found",
"tokenExchangeFailed": "Failed to authenticate with workspace: {error}"
}
+ },
+
+ "nightly": {
+ "badge": {
+ "label": "Preview Version",
+ "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/auth/workspace/useWorkspaceAuth.test.ts b/src/platform/auth/workspace/useWorkspaceAuth.test.ts
index 68f5a198c..ca8152d16 100644
--- a/src/platform/auth/workspace/useWorkspaceAuth.test.ts
+++ b/src/platform/auth/workspace/useWorkspaceAuth.test.ts
@@ -26,14 +26,16 @@ vi.mock('@/i18n', () => ({
t: (key: string) => key
}))
-const mockRemoteConfig = vi.hoisted(() => ({
- value: {
- team_workspaces_enabled: true
- }
-}))
+const mockTeamWorkspacesEnabled = vi.hoisted(() => ({ value: true }))
-vi.mock('@/platform/remoteConfig/remoteConfig', () => ({
- remoteConfig: mockRemoteConfig
+vi.mock('@/composables/useFeatureFlags', () => ({
+ useFeatureFlags: () => ({
+ flags: {
+ get teamWorkspacesEnabled() {
+ return mockTeamWorkspacesEnabled.value
+ }
+ }
+ })
}))
const mockWorkspace = {
@@ -622,11 +624,11 @@ describe('useWorkspaceAuthStore', () => {
describe('feature flag disabled', () => {
beforeEach(() => {
- mockRemoteConfig.value.team_workspaces_enabled = false
+ mockTeamWorkspacesEnabled.value = false
})
afterEach(() => {
- mockRemoteConfig.value.team_workspaces_enabled = true
+ mockTeamWorkspacesEnabled.value = true
})
it('initializeFromSession returns false when flag disabled', () => {
diff --git a/src/platform/cloud/subscription/components/PricingTable.test.ts b/src/platform/cloud/subscription/components/PricingTable.test.ts
index 2094805d3..7e649e8fc 100644
--- a/src/platform/cloud/subscription/components/PricingTable.test.ts
+++ b/src/platform/cloud/subscription/components/PricingTable.test.ts
@@ -14,7 +14,7 @@ const mockSubscriptionTier = ref<
const mockIsYearlySubscription = ref(false)
const mockAccessBillingPortal = vi.fn()
const mockReportError = vi.fn()
-const mockGetAuthHeader = vi.fn(() =>
+const mockGetFirebaseAuthHeader = vi.fn(() =>
Promise.resolve({ Authorization: 'Bearer test-token' })
)
@@ -53,7 +53,7 @@ vi.mock('@/composables/useErrorHandling', () => ({
vi.mock('@/stores/firebaseAuthStore', () => ({
useFirebaseAuthStore: () => ({
- getAuthHeader: mockGetAuthHeader
+ getFirebaseAuthHeader: mockGetFirebaseAuthHeader
}),
FirebaseAuthStoreError: class extends Error {}
}))
diff --git a/src/platform/cloud/subscription/components/PricingTable.vue b/src/platform/cloud/subscription/components/PricingTable.vue
index 7f1a66834..92360746a 100644
--- a/src/platform/cloud/subscription/components/PricingTable.vue
+++ b/src/platform/cloud/subscription/components/PricingTable.vue
@@ -332,7 +332,7 @@ const tiers: PricingTierConfig[] = [
]
const { n } = useI18n()
-const { getAuthHeader } = useFirebaseAuthStore()
+const { getFirebaseAuthHeader } = useFirebaseAuthStore()
const { isActiveSubscription, subscriptionTier, isYearlySubscription } =
useSubscription()
const { accessBillingPortal, reportError } = useFirebaseAuthActions()
@@ -406,7 +406,7 @@ const getCreditsDisplay = (tier: PricingTierConfig): number =>
tier.pricing.credits * (currentBillingCycle.value === 'yearly' ? 12 : 1)
const initiateCheckout = async (tierKey: CheckoutTierKey) => {
- const authHeader = await getAuthHeader()
+ const authHeader = await getFirebaseAuthHeader()
if (!authHeader) {
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
}
diff --git a/src/platform/cloud/subscription/components/SubscriptionPanel.vue b/src/platform/cloud/subscription/components/SubscriptionPanel.vue
index e395cb7d1..4b5dc06a3 100644
--- a/src/platform/cloud/subscription/components/SubscriptionPanel.vue
+++ b/src/platform/cloud/subscription/components/SubscriptionPanel.vue
@@ -68,7 +68,7 @@