diff --git a/src/components/dialog/content/setting/MembersPanelContent.vue b/src/components/dialog/content/setting/MembersPanelContent.vue
index db5e7c458..fb4d22dc7 100644
--- a/src/components/dialog/content/setting/MembersPanelContent.vue
+++ b/src/components/dialog/content/setting/MembersPanelContent.vue
@@ -234,7 +234,12 @@
{{ invite.name.charAt(0).toUpperCase() }}
@@ -338,8 +343,8 @@ const {
pendingInvites,
fetchMembers,
fetchPendingInvites,
- copyInviteLink,
- revokeInvite,
+ copyInviteLinkAndAccept,
+ acceptInvite,
isPersonalWorkspace,
permissions,
uiConfig,
@@ -446,14 +451,18 @@ function formatDate(date: Date): string {
return d(date, { dateStyle: 'medium' })
}
-function handleCopyInviteLink(invite: PendingInvite) {
- copyInviteLink(invite.id)
+async function handleCopyInviteLink(invite: PendingInvite) {
+ // Demo: copying invite link simulates user accepting, moves to active members
+ await copyInviteLinkAndAccept(invite.id)
+}
+
+function handleAcceptInvite(invite: PendingInvite) {
+ // Demo: clicking avatar accepts the invite, moves to active members
+ acceptInvite(invite.id)
}
function handleRevokeInvite(invite: PendingInvite) {
- showRevokeInviteDialog(() => {
- revokeInvite(invite.id)
- })
+ showRevokeInviteDialog(invite.id)
}
function handleCreateWorkspace() {
@@ -462,9 +471,7 @@ function handleCreateWorkspace() {
})
}
-function handleRemoveMember(_member: WorkspaceMember) {
- showRemoveMemberDialog(() => {
- // TODO: Implement actual remove member API call
- })
+function handleRemoveMember(member: WorkspaceMember) {
+ showRemoveMemberDialog(member.id)
}
diff --git a/src/components/dialog/content/setting/WorkspacePanelContent.vue b/src/components/dialog/content/setting/WorkspacePanelContent.vue
index 8375edb69..73f91bc9e 100644
--- a/src/components/dialog/content/setting/WorkspacePanelContent.vue
+++ b/src/components/dialog/content/setting/WorkspacePanelContent.vue
@@ -53,7 +53,7 @@
: null
"
:class="[
- 'flex items-center gap-2 px-3 py-2',
+ 'flex cursor-pointer items-center gap-2 px-3 py-2',
item.class,
item.disabled ? 'pointer-events-auto' : ''
]"
@@ -110,7 +110,8 @@ const { t } = useI18n()
const {
showLeaveWorkspaceDialog,
showDeleteWorkspaceDialog,
- showInviteMemberDialog
+ showInviteMemberDialog,
+ showEditWorkspaceDialog
} = useDialogService()
const { isActiveSubscription } = useSubscription()
const {
@@ -129,15 +130,11 @@ const {
const menu = ref | null>(null)
function handleLeaveWorkspace() {
- showLeaveWorkspaceDialog(() => {
- // TODO: Implement actual leave workspace API call
- })
+ showLeaveWorkspaceDialog()
}
function handleDeleteWorkspace() {
- showDeleteWorkspaceDialog(() => {
- // TODO: Implement actual delete workspace API call
- })
+ showDeleteWorkspaceDialog()
}
const isDeleteDisabled = computed(
@@ -164,31 +161,50 @@ function handleInviteMember() {
})
}
-const menuItems = computed(() => {
- const action = uiConfig.value.workspaceMenuAction
- if (!action) return []
+function handleEditWorkspace() {
+ showEditWorkspaceDialog()
+}
- if (action === 'delete') {
- return [
- {
- label: t('workspacePanel.menu.deleteWorkspace'),
- icon: 'pi pi-trash',
- class: isDeleteDisabled.value
- ? 'text-danger/50 cursor-not-allowed'
- : 'text-danger',
- disabled: isDeleteDisabled.value,
- command: isDeleteDisabled.value ? undefined : handleDeleteWorkspace
- }
- ]
+const menuItems = computed(() => {
+ const items: Array<{
+ label: string
+ icon: string
+ class?: string
+ disabled?: boolean
+ command?: () => void
+ }> = []
+
+ // Add "Edit workspace details" for PERSONAL and OWNER
+ if (uiConfig.value.showEditWorkspaceMenuItem) {
+ items.push({
+ label: t('workspacePanel.menu.editWorkspaceDetails'),
+ icon: 'pi pi-pencil',
+ command: handleEditWorkspace
+ })
}
- return [
- {
+ const action = uiConfig.value.workspaceMenuAction
+
+ // Add role-specific action
+ if (action === 'delete') {
+ items.push({
+ label: t('workspacePanel.menu.deleteWorkspace'),
+ icon: 'pi pi-trash',
+ class: isDeleteDisabled.value
+ ? 'text-danger/50 cursor-not-allowed'
+ : 'text-danger',
+ disabled: isDeleteDisabled.value,
+ command: isDeleteDisabled.value ? undefined : handleDeleteWorkspace
+ })
+ } else if (action === 'leave') {
+ items.push({
label: t('workspacePanel.menu.leaveWorkspace'),
icon: 'pi pi-sign-out',
command: handleLeaveWorkspace
- }
- ]
+ })
+ }
+
+ return items
})
onMounted(() => {
diff --git a/src/components/dialog/content/workspace/CreateWorkspaceDialogContent.vue b/src/components/dialog/content/workspace/CreateWorkspaceDialogContent.vue
index 0c317c6fe..2e7b457bf 100644
--- a/src/components/dialog/content/workspace/CreateWorkspaceDialogContent.vue
+++ b/src/components/dialog/content/workspace/CreateWorkspaceDialogContent.vue
@@ -62,14 +62,16 @@ import { useToast } from 'primevue/usetoast'
import { computed, ref } from 'vue'
import Button from '@/components/ui/button/Button.vue'
+import { useWorkspace } from '@/platform/workspace/composables/useWorkspace'
import { useDialogStore } from '@/stores/dialogStore'
const { onConfirm } = defineProps<{
- onConfirm: (name: string) => void | Promise
+ onConfirm?: (name: string) => void | Promise
}>()
const dialogStore = useDialogStore()
const toast = useToast()
+const { createWorkspace } = useWorkspace()
const loading = ref(false)
const workspaceName = ref('')
@@ -88,8 +90,13 @@ async function onCreate() {
if (!isValidName.value) return
loading.value = true
try {
- await onConfirm(workspaceName.value.trim())
+ const name = workspaceName.value.trim()
+ // Create workspace using global state (creates OWNER unsubscribed workspace)
+ createWorkspace(name)
+ // Call optional callback if provided
+ await onConfirm?.(name)
dialogStore.closeDialog({ key: 'create-workspace' })
+ // Show toast prompting to subscribe
toast.add({
group: 'workspace-created'
})
diff --git a/src/components/dialog/content/workspace/DeleteWorkspaceDialogContent.vue b/src/components/dialog/content/workspace/DeleteWorkspaceDialogContent.vue
index 5a455a953..64341ed27 100644
--- a/src/components/dialog/content/workspace/DeleteWorkspaceDialogContent.vue
+++ b/src/components/dialog/content/workspace/DeleteWorkspaceDialogContent.vue
@@ -41,13 +41,11 @@
import { ref } from 'vue'
import Button from '@/components/ui/button/Button.vue'
+import { useWorkspace } from '@/platform/workspace/composables/useWorkspace'
import { useDialogStore } from '@/stores/dialogStore'
-const { onConfirm } = defineProps<{
- onConfirm: () => void | Promise
-}>()
-
const dialogStore = useDialogStore()
+const { deleteWorkspace } = useWorkspace()
const loading = ref(false)
function onCancel() {
@@ -57,7 +55,7 @@ function onCancel() {
async function onDelete() {
loading.value = true
try {
- await onConfirm()
+ await deleteWorkspace()
dialogStore.closeDialog({ key: 'delete-workspace' })
} finally {
loading.value = false
diff --git a/src/components/dialog/content/workspace/EditWorkspaceDialogContent.vue b/src/components/dialog/content/workspace/EditWorkspaceDialogContent.vue
new file mode 100644
index 000000000..d981fc13b
--- /dev/null
+++ b/src/components/dialog/content/workspace/EditWorkspaceDialogContent.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+ {{ $t('workspacePanel.editWorkspaceDialog.title') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/dialog/content/workspace/LeaveWorkspaceDialogContent.vue b/src/components/dialog/content/workspace/LeaveWorkspaceDialogContent.vue
index da6703cba..4475fb5c8 100644
--- a/src/components/dialog/content/workspace/LeaveWorkspaceDialogContent.vue
+++ b/src/components/dialog/content/workspace/LeaveWorkspaceDialogContent.vue
@@ -41,13 +41,11 @@
import { ref } from 'vue'
import Button from '@/components/ui/button/Button.vue'
+import { useWorkspace } from '@/platform/workspace/composables/useWorkspace'
import { useDialogStore } from '@/stores/dialogStore'
-const { onConfirm } = defineProps<{
- onConfirm: () => void | Promise
-}>()
-
const dialogStore = useDialogStore()
+const { leaveWorkspace } = useWorkspace()
const loading = ref(false)
function onCancel() {
@@ -57,7 +55,7 @@ function onCancel() {
async function onLeave() {
loading.value = true
try {
- await onConfirm()
+ await leaveWorkspace()
dialogStore.closeDialog({ key: 'leave-workspace' })
} finally {
loading.value = false
diff --git a/src/components/dialog/content/workspace/RemoveMemberDialogContent.vue b/src/components/dialog/content/workspace/RemoveMemberDialogContent.vue
index 29f444587..4dab4a779 100644
--- a/src/components/dialog/content/workspace/RemoveMemberDialogContent.vue
+++ b/src/components/dialog/content/workspace/RemoveMemberDialogContent.vue
@@ -41,13 +41,15 @@
import { ref } from 'vue'
import Button from '@/components/ui/button/Button.vue'
+import { useWorkspace } from '@/platform/workspace/composables/useWorkspace'
import { useDialogStore } from '@/stores/dialogStore'
-const { onConfirm } = defineProps<{
- onConfirm: () => void | Promise
+const { memberId } = defineProps<{
+ memberId: string
}>()
const dialogStore = useDialogStore()
+const { removeMember } = useWorkspace()
const loading = ref(false)
function onCancel() {
@@ -57,7 +59,7 @@ function onCancel() {
async function onRemove() {
loading.value = true
try {
- await onConfirm()
+ await removeMember(memberId)
dialogStore.closeDialog({ key: 'remove-member' })
} finally {
loading.value = false
diff --git a/src/components/dialog/content/workspace/RevokeInviteDialogContent.vue b/src/components/dialog/content/workspace/RevokeInviteDialogContent.vue
index 76744a76d..58766372e 100644
--- a/src/components/dialog/content/workspace/RevokeInviteDialogContent.vue
+++ b/src/components/dialog/content/workspace/RevokeInviteDialogContent.vue
@@ -41,13 +41,15 @@
import { ref } from 'vue'
import Button from '@/components/ui/button/Button.vue'
+import { useWorkspace } from '@/platform/workspace/composables/useWorkspace'
import { useDialogStore } from '@/stores/dialogStore'
-const { onConfirm } = defineProps<{
- onConfirm: () => void | Promise
+const { inviteId } = defineProps<{
+ inviteId: string
}>()
const dialogStore = useDialogStore()
+const { revokeInvite } = useWorkspace()
const loading = ref(false)
function onCancel() {
@@ -57,7 +59,7 @@ function onCancel() {
async function onRevoke() {
loading.value = true
try {
- await onConfirm()
+ await revokeInvite(inviteId)
dialogStore.closeDialog({ key: 'revoke-invite' })
} finally {
loading.value = false
diff --git a/src/components/topbar/CurrentUserPopover.vue b/src/components/topbar/CurrentUserPopover.vue
index 77ebdd1de..96b2f4e2d 100644
--- a/src/components/topbar/CurrentUserPopover.vue
+++ b/src/components/topbar/CurrentUserPopover.vue
@@ -43,10 +43,10 @@
workspaceName
}}
- {{ subscriptionTierName }}
+ {{ workspaceTierName }}
{{ $t('workspaceSwitcher.subscribe') }}
@@ -112,7 +112,7 @@
size="sm"
class="w-full"
data-testid="subscribe-button"
- @click="handleOpenPlansAndPricing"
+ @click="handleOpenWorkspaceSettings"
>
{{ $t('subscription.subscribeNow') }}
@@ -246,7 +246,8 @@ const {
workspaceName,
workspaceRole,
isPersonalWorkspace,
- isWorkspaceSubscribed
+ isWorkspaceSubscribed,
+ subscriptionPlan
} = useWorkspace()
const workspaceSwitcherPopover = ref | null>(null)
@@ -261,14 +262,9 @@ const { userDisplayName, userEmail, userPhotoUrl, handleSignOut } =
const authActions = useFirebaseAuthActions()
const authStore = useFirebaseAuthStore()
const dialogService = useDialogService()
-const {
- isActiveSubscription,
- subscriptionTierName,
- subscriptionTier,
- fetchStatus
-} = useSubscription()
+const { isActiveSubscription, fetchStatus } = useSubscription()
const subscriptionDialog = useSubscriptionDialog()
-const { locale } = useI18n()
+const { locale, t } = useI18n()
const formattedBalance = computed(() => {
const cents =
@@ -285,11 +281,23 @@ const formattedBalance = computed(() => {
})
})
+// 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 canUpgrade = computed(() => {
- const tier = subscriptionTier.value
- return (
- tier === 'FOUNDERS_EDITION' || tier === 'STANDARD' || tier === 'CREATOR'
- )
+ // For workspace-based subscriptions, can upgrade if not on highest tier
+ return isWorkspaceSubscribed.value && subscriptionPlan.value !== null
})
// Menu visibility based on role
diff --git a/src/locales/en/main.json b/src/locales/en/main.json
index 4137e5dea..d95bee73c 100644
--- a/src/locales/en/main.json
+++ b/src/locales/en/main.json
@@ -2128,7 +2128,8 @@
"actions": {
"copyLink": "Copy invite link",
"revokeInvite": "Revoke invite",
- "removeMember": "Remove member"
+ "removeMember": "Remove member",
+ "acceptInvite": "Click to accept invite (demo)"
},
"noInvites": "No pending invites",
"noMembers": "No members",
@@ -2136,6 +2137,7 @@
"createNewWorkspace": "create a new one."
},
"menu": {
+ "editWorkspaceDetails": "Edit workspace details",
"leaveWorkspace": "Leave Workspace",
"deleteWorkspace": "Delete Workspace",
"deleteWorkspaceDisabledTooltip": "Cancel your workspace's active subscription first"
@@ -2178,6 +2180,11 @@
"namePlaceholder": "Enter workspace name",
"create": "Create"
},
+ "editWorkspaceDialog": {
+ "title": "Edit workspace details",
+ "nameLabel": "Workspace name",
+ "save": "Save changes"
+ },
"toast": {
"workspaceCreated": {
"title": "Workspace created",
diff --git a/src/platform/cloud/subscription/components/SubscriptionPanelContent.vue b/src/platform/cloud/subscription/components/SubscriptionPanelContent.vue
index f1fa68c37..b5b8cffbe 100644
--- a/src/platform/cloud/subscription/components/SubscriptionPanelContent.vue
+++ b/src/platform/cloud/subscription/components/SubscriptionPanelContent.vue
@@ -16,7 +16,7 @@
@@ -259,7 +259,12 @@ import { useWorkspace } from '@/platform/workspace/composables/useWorkspace'
import { cn } from '@/utils/tailwindUtil'
const authActions = useFirebaseAuthActions()
-const { permissions, isWorkspaceSubscribed, workspaceRole } = useWorkspace()
+const {
+ permissions,
+ isWorkspaceSubscribed,
+ workspaceRole,
+ subscribeWorkspace
+} = useWorkspace()
const { t, n } = useI18n()
// OWNER with unsubscribed workspace
@@ -267,6 +272,11 @@ const isOwnerUnsubscribed = computed(
() => workspaceRole.value === 'OWNER' && !isWorkspaceSubscribed.value
)
+// Demo: Subscribe workspace to PRO monthly plan
+function handleSubscribeWorkspace() {
+ subscribeWorkspace('PRO_MONTHLY')
+}
+
const {
isActiveSubscription,
isCancelled,
diff --git a/src/platform/workspace/composables/useWorkspace.ts b/src/platform/workspace/composables/useWorkspace.ts
index fcb81d77a..0c18f027f 100644
--- a/src/platform/workspace/composables/useWorkspace.ts
+++ b/src/platform/workspace/composables/useWorkspace.ts
@@ -1,8 +1,9 @@
-import { computed, ref } from 'vue'
+import { computed, ref, shallowRef } from 'vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
-export type WorkspaceRole = 'PERSONAL' | 'MEMBER' | 'OWNER'
+type WorkspaceRole = 'PERSONAL' | 'MEMBER' | 'OWNER'
+type SubscriptionPlan = 'PRO_MONTHLY' | 'PRO_YEARLY' | null
export interface WorkspaceMember {
id: string
@@ -20,10 +21,14 @@ export interface PendingInvite {
inviteLink: string
}
-interface WorkspaceMockData {
+interface Workspace {
id: string | null
name: string
role: WorkspaceRole
+ isSubscribed: boolean
+ subscriptionPlan: SubscriptionPlan
+ members: WorkspaceMember[]
+ pendingInvites: PendingInvite[]
}
export interface AvailableWorkspace {
@@ -33,7 +38,7 @@ export interface AvailableWorkspace {
}
/** Permission flags for workspace actions */
-export interface WorkspacePermissions {
+interface WorkspacePermissions {
canViewOtherMembers: boolean
canViewPendingInvites: boolean
canInviteMembers: boolean
@@ -45,7 +50,7 @@ export interface WorkspacePermissions {
}
/** UI configuration for workspace role */
-export interface WorkspaceUIConfig {
+interface WorkspaceUIConfig {
showMembersList: boolean
showPendingTab: boolean
showSearch: boolean
@@ -54,6 +59,7 @@ export interface WorkspaceUIConfig {
membersGridCols: string
pendingGridCols: string
headerGridCols: string
+ showEditWorkspaceMenuItem: boolean
workspaceMenuAction: 'leave' | 'delete' | null
workspaceMenuDisabledTooltip: string | null
}
@@ -66,7 +72,7 @@ const ROLE_PERMISSIONS: Record = {
canManageInvites: false,
canRemoveMembers: false,
canLeaveWorkspace: false,
- canAccessWorkspaceMenu: false,
+ canAccessWorkspaceMenu: true,
canManageSubscription: true
},
MEMBER: {
@@ -101,6 +107,7 @@ const ROLE_UI_CONFIG: Record = {
membersGridCols: 'grid-cols-1',
pendingGridCols: 'grid-cols-[50%_20%_20%_10%]',
headerGridCols: 'grid-cols-1',
+ showEditWorkspaceMenuItem: true,
workspaceMenuAction: null,
workspaceMenuDisabledTooltip: null
},
@@ -113,6 +120,7 @@ const ROLE_UI_CONFIG: Record = {
membersGridCols: 'grid-cols-[1fr_auto]',
pendingGridCols: 'grid-cols-[50%_20%_20%_10%]',
headerGridCols: 'grid-cols-[1fr_auto]',
+ showEditWorkspaceMenuItem: false,
workspaceMenuAction: 'leave',
workspaceMenuDisabledTooltip: null
},
@@ -125,129 +133,202 @@ const ROLE_UI_CONFIG: Record = {
membersGridCols: 'grid-cols-[50%_40%_10%]',
pendingGridCols: 'grid-cols-[50%_20%_20%_10%]',
headerGridCols: 'grid-cols-[50%_40%_10%]',
+ showEditWorkspaceMenuItem: true,
workspaceMenuAction: 'delete',
workspaceMenuDisabledTooltip:
'workspacePanel.menu.deleteWorkspaceDisabledTooltip'
}
}
-const MOCK_DATA: Record = {
- PERSONAL: {
+const MAX_OWNED_WORKSPACES = 10
+const MAX_WORKSPACE_MEMBERS = 50
+
+function generateId(): string {
+ return Math.random().toString(36).substring(2, 10)
+}
+
+function createPersonalWorkspace(): Workspace {
+ return {
id: null,
- name: 'Personal',
- role: 'PERSONAL'
- },
- MEMBER: {
- id: 'workspace-abc-123',
- name: 'Acme Corp',
- role: 'MEMBER'
- },
- OWNER: {
- id: 'workspace-xyz-789',
- name: 'Acme Corp',
- role: 'OWNER'
+ name: 'Personal workspace',
+ role: 'PERSONAL',
+ isSubscribed: true,
+ subscriptionPlan: null,
+ members: [],
+ pendingInvites: []
}
}
-/** Mock list of all available workspaces for the current user */
-const MOCK_AVAILABLE_WORKSPACES: AvailableWorkspace[] = [
- { id: null, name: 'Personal workspace', role: 'PERSONAL' },
- { id: 'workspace-comfy-001', name: 'Team Comfy', role: 'OWNER' },
- { id: 'workspace-orange-002', name: 'OrangeDesignStudio', role: 'MEMBER' },
- { id: 'workspace-001', name: 'Workspace001', role: 'MEMBER' },
- { id: 'workspace-002', name: 'Workspace002', role: 'MEMBER' }
-]
-
-const MAX_OWNED_WORKSPACES = 10
-
-const MOCK_MEMBERS: WorkspaceMember[] = [
- {
- id: '1',
- name: 'Alice',
- email: 'alice@example.com',
- joinDate: new Date('2025-11-15')
- },
- {
- id: '2',
- name: 'Bob',
- email: 'bob@example.com',
- joinDate: new Date('2025-12-01')
- },
- {
- id: '3',
- name: 'Charlie',
- email: 'charlie@example.com',
- joinDate: new Date('2026-01-05')
- }
-]
-
-const MOCK_PENDING_INVITES: PendingInvite[] = [
- {
- id: '1',
- name: 'John',
- email: 'john@gmail.com',
- inviteDate: new Date('2026-01-02'),
- expiryDate: new Date('2026-01-09'),
- inviteLink: 'https://example.com/invite/abc123'
- },
- {
- id: '2',
- name: 'User102',
- email: 'user102@gmail.com',
- inviteDate: new Date('2026-01-01'),
- expiryDate: new Date('2026-01-08'),
- inviteLink: 'https://example.com/invite/def456'
- },
- {
- id: '3',
- name: 'User944',
- email: 'user944@gmail.com',
- inviteDate: new Date('2026-01-01'),
- expiryDate: new Date('2026-01-08'),
- inviteLink: 'https://example.com/invite/ghi789'
- },
- {
- id: '4',
- name: 'User45',
- email: 'user45@gmail.com',
- inviteDate: new Date('2025-12-15'),
- expiryDate: new Date('2025-12-22'),
- inviteLink: 'https://example.com/invite/jkl012'
- },
- {
- id: '5',
- name: 'User944',
- email: 'user944@gmail.com',
- inviteDate: new Date('2025-12-05'),
- expiryDate: new Date('2025-12-22'),
- inviteLink: 'https://example.com/invite/mno345'
- }
-]
-
-// Constants
-const MAX_WORKSPACE_MEMBERS = 50
-
-// Shared state for workspace
-const _workspaceId = ref(null)
-const _workspaceName = ref('Personal workspace')
-const _workspaceRole = ref('PERSONAL')
-const _isWorkspaceSubscribed = ref(true)
+// Global state - start with personal workspace only
+const _workspaces = shallowRef([createPersonalWorkspace()])
+const _currentWorkspaceIndex = ref(0)
const _activeTab = ref('plan')
-const _members = ref([])
-const _pendingInvites = ref([])
-const _availableWorkspaces = ref(
- MOCK_AVAILABLE_WORKSPACES
-)
+
+// Helper to get current workspace
+function getCurrentWorkspace(): Workspace {
+ return _workspaces.value[_currentWorkspaceIndex.value]
+}
+
+// Helper to update current workspace
+function updateCurrentWorkspace(updates: Partial) {
+ const index = _currentWorkspaceIndex.value
+ const updated = { ..._workspaces.value[index], ...updates }
+ _workspaces.value = [
+ ..._workspaces.value.slice(0, index),
+ updated,
+ ..._workspaces.value.slice(index + 1)
+ ]
+}
+
+/**
+ * Switch to a different workspace
+ */
+function switchWorkspace(workspace: AvailableWorkspace) {
+ const index = _workspaces.value.findIndex((w) => w.id === workspace.id)
+ if (index !== -1) {
+ _currentWorkspaceIndex.value = index
+ }
+}
+
+/**
+ * Create a new workspace
+ */
+function createNewWorkspace(name: string): Workspace {
+ const newWorkspace: Workspace = {
+ id: `workspace-${generateId()}`,
+ name,
+ role: 'OWNER',
+ isSubscribed: false,
+ subscriptionPlan: null,
+ members: [],
+ pendingInvites: [
+ // Add one pending invite for testing revoke
+ {
+ id: generateId(),
+ name: 'PendingUser',
+ email: 'pending@example.com',
+ inviteDate: new Date(),
+ expiryDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
+ inviteLink: `https://cloud.comfy.org/workspace/invite/${generateId()}`
+ }
+ ]
+ }
+
+ _workspaces.value = [..._workspaces.value, newWorkspace]
+ // Switch to the new workspace
+ _currentWorkspaceIndex.value = _workspaces.value.length - 1
+
+ return newWorkspace
+}
+
+/**
+ * Subscribe the current workspace to a plan
+ */
+function subscribeCurrentWorkspace(plan: SubscriptionPlan = 'PRO_MONTHLY') {
+ updateCurrentWorkspace({
+ isSubscribed: true,
+ subscriptionPlan: plan
+ })
+}
+
+/**
+ * Delete the current workspace (OWNER only)
+ */
+function deleteCurrentWorkspace() {
+ const current = getCurrentWorkspace()
+ if (current.role === 'OWNER') {
+ _workspaces.value = _workspaces.value.filter((w) => w.id !== current.id)
+ _currentWorkspaceIndex.value = 0
+ }
+}
+
+/**
+ * Leave the current workspace (MEMBER only)
+ */
+function leaveCurrentWorkspace() {
+ const current = getCurrentWorkspace()
+ if (current.role === 'MEMBER') {
+ _workspaces.value = _workspaces.value.filter((w) => w.id !== current.id)
+ _currentWorkspaceIndex.value = 0
+ }
+}
+
+/**
+ * Add a member to the current workspace
+ */
+function addMemberToWorkspace(
+ member: Omit
+) {
+ const current = getCurrentWorkspace()
+ const newMember: WorkspaceMember = {
+ ...member,
+ id: generateId(),
+ joinDate: new Date()
+ }
+ updateCurrentWorkspace({
+ members: [...current.members, newMember]
+ })
+}
+
+/**
+ * Remove a member from the current workspace
+ */
+function removeMemberFromWorkspace(memberId: string) {
+ const current = getCurrentWorkspace()
+ updateCurrentWorkspace({
+ members: current.members.filter((m) => m.id !== memberId)
+ })
+}
+
+/**
+ * Update the current workspace name
+ */
+function updateWorkspaceNameFn(name: string) {
+ updateCurrentWorkspace({ name })
+}
+
+/**
+ * Revoke a pending invite
+ */
+function revokePendingInvite(inviteId: string) {
+ const current = getCurrentWorkspace()
+ updateCurrentWorkspace({
+ pendingInvites: current.pendingInvites.filter((i) => i.id !== inviteId)
+ })
+}
+
+/**
+ * Accept a pending invite (move to active members)
+ * For demo: simulates user accepting the invite
+ */
+function acceptPendingInvite(inviteId: string) {
+ const current = getCurrentWorkspace()
+ const invite = current.pendingInvites.find((i) => i.id === inviteId)
+ if (invite) {
+ // Remove from pending
+ const updatedPending = current.pendingInvites.filter(
+ (i) => i.id !== inviteId
+ )
+ // Add to active members
+ const newMember: WorkspaceMember = {
+ id: generateId(),
+ name: invite.name,
+ email: invite.email,
+ joinDate: new Date()
+ }
+ updateCurrentWorkspace({
+ pendingInvites: updatedPending,
+ members: [...current.members, newMember]
+ })
+ }
+}
/**
* Set workspace mock state for testing UI
* Usage in browser console: window.__setWorkspaceRole('OWNER')
*/
function setMockRole(role: WorkspaceRole) {
- const data = MOCK_DATA[role]
- _workspaceId.value = data.id
- _workspaceName.value = data.name
- _workspaceRole.value = data.role
+ updateCurrentWorkspace({ role })
}
/**
@@ -255,19 +336,7 @@ function setMockRole(role: WorkspaceRole) {
* Usage in browser console: window.__setWorkspaceSubscribed(false)
*/
function setMockSubscribed(subscribed: boolean) {
- _isWorkspaceSubscribed.value = subscribed
-}
-
-/**
- * Switch to a different workspace
- */
-function switchWorkspace(workspace: AvailableWorkspace) {
- _workspaceId.value = workspace.id
- _workspaceName.value = workspace.name
- _workspaceRole.value = workspace.role
- // Reset members/invites when switching
- _members.value = []
- _pendingInvites.value = []
+ updateCurrentWorkspace({ isSubscribed: subscribed })
}
// Expose to window for dev testing
@@ -290,43 +359,45 @@ if (typeof window !== 'undefined') {
export function useWorkspace() {
const { userDisplayName, userEmail } = useCurrentUser()
- const workspaceId = computed(() => _workspaceId.value)
- const workspaceName = computed(() => _workspaceName.value)
- const workspaceRole = computed(() => _workspaceRole.value)
+ // Computed from current workspace
+ const currentWorkspace = computed(() => getCurrentWorkspace())
+ const workspaceId = computed(() => currentWorkspace.value?.id ?? null)
+ const workspaceName = computed(
+ () => currentWorkspace.value?.name ?? 'Personal workspace'
+ )
+ const workspaceRole = computed(
+ () => currentWorkspace.value?.role ?? 'PERSONAL'
+ )
const activeTab = computed(() => _activeTab.value)
const isPersonalWorkspace = computed(
- () => _workspaceRole.value === 'PERSONAL'
+ () => currentWorkspace.value?.role === 'PERSONAL'
)
- const isWorkspaceSubscribed = computed(() => _isWorkspaceSubscribed.value)
+ const isWorkspaceSubscribed = computed(
+ () => currentWorkspace.value?.isSubscribed ?? false
+ )
+
+ const subscriptionPlan = computed(
+ () => currentWorkspace.value?.subscriptionPlan ?? null
+ )
const permissions = computed(
- () => ROLE_PERMISSIONS[_workspaceRole.value]
+ () => ROLE_PERMISSIONS[workspaceRole.value]
)
const uiConfig = computed(
- () => ROLE_UI_CONFIG[_workspaceRole.value]
+ () => ROLE_UI_CONFIG[workspaceRole.value]
)
function setActiveTab(tab: string | number) {
_activeTab.value = String(tab)
}
- const members = computed(() => _members.value)
- const pendingInvites = computed(() => _pendingInvites.value)
-
- const totalMemberSlots = computed(
- () => _members.value.length + _pendingInvites.value.length
- )
- const isInviteLimitReached = computed(
- () => totalMemberSlots.value >= MAX_WORKSPACE_MEMBERS
- )
-
- // TODO: Replace with actual API calls
- async function fetchMembers(): Promise {
- if (_workspaceRole.value === 'PERSONAL') {
- _members.value = [
+ // For personal workspace, always show current user as the only member
+ const members = computed(() => {
+ if (isPersonalWorkspace.value) {
+ return [
{
id: 'current-user',
name: userDisplayName.value ?? 'You',
@@ -334,27 +405,45 @@ export function useWorkspace() {
joinDate: new Date()
}
]
- } else {
- _members.value = MOCK_MEMBERS
}
- return _members.value
+ return currentWorkspace.value?.members ?? []
+ })
+ const pendingInvites = computed(
+ () => currentWorkspace.value?.pendingInvites ?? []
+ )
+
+ const totalMemberSlots = computed(
+ () => members.value.length + pendingInvites.value.length
+ )
+ const isInviteLimitReached = computed(
+ () => totalMemberSlots.value >= MAX_WORKSPACE_MEMBERS
+ )
+
+ // Fetch members - returns current user for personal workspace
+ async function fetchMembers(): Promise {
+ if (isPersonalWorkspace.value) {
+ return [
+ {
+ id: 'current-user',
+ name: userDisplayName.value ?? 'You',
+ email: userEmail.value ?? '',
+ joinDate: new Date()
+ }
+ ]
+ }
+ return members.value
}
async function fetchPendingInvites(): Promise {
- if (_workspaceRole.value === 'PERSONAL') {
- _pendingInvites.value = []
- } else {
- _pendingInvites.value = MOCK_PENDING_INVITES
- }
- return _pendingInvites.value
+ return pendingInvites.value
}
- async function revokeInvite(_inviteId: string): Promise {
- // TODO: API call to revoke invite
+ async function revokeInvite(inviteId: string): Promise {
+ revokePendingInvite(inviteId)
}
async function copyInviteLink(inviteId: string): Promise {
- const invite = _pendingInvites.value.find((i) => i.id === inviteId)
+ const invite = pendingInvites.value.find((i) => i.id === inviteId)
if (invite) {
await navigator.clipboard.writeText(invite.inviteLink)
return invite.inviteLink
@@ -362,35 +451,63 @@ export function useWorkspace() {
throw new Error('Invite not found')
}
+ /**
+ * Copy invite link and simulate member accepting (for demo)
+ * When copy link is clicked, the invited user "accepts" and becomes a member
+ */
+ async function copyInviteLinkAndAccept(inviteId: string): Promise {
+ const invite = pendingInvites.value.find((i) => i.id === inviteId)
+ if (invite) {
+ await navigator.clipboard.writeText(invite.inviteLink)
+
+ // Simulate user accepting invite: move from pending to active
+ revokePendingInvite(inviteId)
+ addMemberToWorkspace({
+ name: invite.name,
+ email: invite.email
+ })
+
+ return invite.inviteLink
+ }
+ throw new Error('Invite not found')
+ }
+
/**
* Create an invite link for a given email
- * TODO: Replace with actual API call
*/
async function createInviteLink(email: string): Promise {
// Simulate API delay
- await new Promise((resolve) => setTimeout(resolve, 1000))
+ await new Promise((resolve) => setTimeout(resolve, 500))
- // Generate mock invite link
- const inviteId = Math.random().toString(36).substring(2, 10)
- const inviteLink = `https://cloud.comfy.org/workspace?3423532/invite/hi789jkl012mno345pq`
+ const inviteId = generateId()
+ const inviteLink = `https://cloud.comfy.org/workspace/invite/${inviteId}`
- // Add to pending invites (mock)
+ // Add to pending invites
+ const current = getCurrentWorkspace()
const newInvite: PendingInvite = {
id: inviteId,
name: email.split('@')[0],
email,
inviteDate: new Date(),
- expiryDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
+ expiryDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
inviteLink
}
- _pendingInvites.value = [..._pendingInvites.value, newInvite]
+ updateCurrentWorkspace({
+ pendingInvites: [...current.pendingInvites, newInvite]
+ })
return inviteLink
}
- const availableWorkspaces = computed(() => _availableWorkspaces.value)
+ const availableWorkspaces = computed(() =>
+ _workspaces.value.map((w) => ({
+ id: w.id,
+ name: w.name,
+ role: w.role
+ }))
+ )
const ownedWorkspacesCount = computed(
- () => _availableWorkspaces.value.filter((w) => w.role === 'OWNER').length
+ () => _workspaces.value.filter((w) => w.role === 'OWNER').length
)
const canCreateWorkspace = computed(
() => ownedWorkspacesCount.value < MAX_OWNED_WORKSPACES
@@ -403,6 +520,7 @@ export function useWorkspace() {
activeTab,
isPersonalWorkspace,
isWorkspaceSubscribed,
+ subscriptionPlan,
permissions,
uiConfig,
setActiveTab,
@@ -411,6 +529,11 @@ export function useWorkspace() {
ownedWorkspacesCount,
canCreateWorkspace,
switchWorkspace,
+ // Workspace management
+ createWorkspace: createNewWorkspace,
+ subscribeWorkspace: subscribeCurrentWorkspace,
+ deleteWorkspace: deleteCurrentWorkspace,
+ leaveWorkspace: leaveCurrentWorkspace,
// Members
members,
pendingInvites,
@@ -419,8 +542,13 @@ export function useWorkspace() {
fetchMembers,
fetchPendingInvites,
revokeInvite,
+ acceptInvite: acceptPendingInvite,
copyInviteLink,
+ copyInviteLinkAndAccept,
createInviteLink,
+ addMember: addMemberToWorkspace,
+ removeMember: removeMemberFromWorkspace,
+ updateWorkspaceName: updateWorkspaceNameFn,
// Dev helpers
setMockRole,
setMockSubscribed
diff --git a/src/services/dialogService.ts b/src/services/dialogService.ts
index 944c915a6..c72ca8658 100644
--- a/src/services/dialogService.ts
+++ b/src/services/dialogService.ts
@@ -3,6 +3,7 @@ import type { Component } from 'vue'
import ApiNodesSignInContent from '@/components/dialog/content/ApiNodesSignInContent.vue'
import CreateWorkspaceDialogContent from '@/components/dialog/content/workspace/CreateWorkspaceDialogContent.vue'
+import EditWorkspaceDialogContent from '@/components/dialog/content/workspace/EditWorkspaceDialogContent.vue'
import DeleteWorkspaceDialogContent from '@/components/dialog/content/workspace/DeleteWorkspaceDialogContent.vue'
import InviteMemberDialogContent from '@/components/dialog/content/workspace/InviteMemberDialogContent.vue'
import LeaveWorkspaceDialogContent from '@/components/dialog/content/workspace/LeaveWorkspaceDialogContent.vue'
@@ -526,11 +527,10 @@ export const useDialogService = () => {
show()
}
- function showLeaveWorkspaceDialog(onConfirm: () => void | Promise) {
+ function showLeaveWorkspaceDialog() {
return dialogStore.showDialog({
key: 'leave-workspace',
component: LeaveWorkspaceDialogContent,
- props: { onConfirm },
dialogComponentProps: {
headless: true,
pt: {
@@ -542,11 +542,10 @@ export const useDialogService = () => {
})
}
- function showDeleteWorkspaceDialog(onConfirm: () => void | Promise) {
+ function showDeleteWorkspaceDialog() {
return dialogStore.showDialog({
key: 'delete-workspace',
component: DeleteWorkspaceDialogContent,
- props: { onConfirm },
dialogComponentProps: {
headless: true,
pt: {
@@ -558,11 +557,11 @@ export const useDialogService = () => {
})
}
- function showRemoveMemberDialog(onConfirm: () => void | Promise) {
+ function showRemoveMemberDialog(memberId: string) {
return dialogStore.showDialog({
key: 'remove-member',
component: RemoveMemberDialogContent,
- props: { onConfirm },
+ props: { memberId },
dialogComponentProps: {
headless: true,
pt: {
@@ -574,11 +573,11 @@ export const useDialogService = () => {
})
}
- function showRevokeInviteDialog(onConfirm: () => void | Promise) {
+ function showRevokeInviteDialog(inviteId: string) {
return dialogStore.showDialog({
key: 'revoke-invite',
component: RevokeInviteDialogContent,
- props: { onConfirm },
+ props: { inviteId },
dialogComponentProps: {
headless: true,
pt: {
@@ -626,6 +625,21 @@ export const useDialogService = () => {
})
}
+ function showEditWorkspaceDialog() {
+ return dialogStore.showDialog({
+ key: 'edit-workspace',
+ component: EditWorkspaceDialogContent,
+ dialogComponentProps: {
+ headless: true,
+ pt: {
+ header: { class: 'p-0! hidden' },
+ content: { class: 'p-0! m-0! rounded-2xl' },
+ root: { class: 'rounded-2xl max-w-[400px] w-full' }
+ }
+ }
+ })
+ }
+
return {
showLoadWorkflowWarning,
showMissingModelsWarning,
@@ -643,6 +657,7 @@ export const useDialogService = () => {
showRevokeInviteDialog,
showInviteMemberDialog,
showCreateWorkspaceDialog,
+ showEditWorkspaceDialog,
showExtensionDialog,
prompt,
showErrorDialog,