From 8950b7327fa8daa5ab16550eff98fc113a9982f3 Mon Sep 17 00:00:00 2001
From: --list <18093452+simula-r@users.noreply.github.com>
Date: Tue, 13 Jan 2026 23:32:18 -0800
Subject: [PATCH] feat: implemented workspace flow
---
.../content/setting/MembersPanelContent.vue | 31 +-
.../content/setting/WorkspacePanelContent.vue | 68 +--
.../CreateWorkspaceDialogContent.vue | 11 +-
.../DeleteWorkspaceDialogContent.vue | 8 +-
.../workspace/LeaveWorkspaceDialogContent.vue | 8 +-
.../workspace/RemoveMemberDialogContent.vue | 8 +-
.../workspace/RevokeInviteDialogContent.vue | 8 +-
src/components/topbar/CurrentUserPopover.vue | 38 +-
src/locales/en/main.json | 9 +-
.../components/SubscriptionPanelContent.vue | 14 +-
.../workspace/composables/useWorkspace.ts | 450 +++++++-----------
src/services/dialogService.ts | 31 +-
12 files changed, 243 insertions(+), 441 deletions(-)
diff --git a/src/components/dialog/content/setting/MembersPanelContent.vue b/src/components/dialog/content/setting/MembersPanelContent.vue
index fb4d22dc7..db5e7c458 100644
--- a/src/components/dialog/content/setting/MembersPanelContent.vue
+++ b/src/components/dialog/content/setting/MembersPanelContent.vue
@@ -234,12 +234,7 @@
{{ invite.name.charAt(0).toUpperCase() }}
@@ -343,8 +338,8 @@ const {
pendingInvites,
fetchMembers,
fetchPendingInvites,
- copyInviteLinkAndAccept,
- acceptInvite,
+ copyInviteLink,
+ revokeInvite,
isPersonalWorkspace,
permissions,
uiConfig,
@@ -451,18 +446,14 @@ function formatDate(date: Date): string {
return d(date, { dateStyle: 'medium' })
}
-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 handleCopyInviteLink(invite: PendingInvite) {
+ copyInviteLink(invite.id)
}
function handleRevokeInvite(invite: PendingInvite) {
- showRevokeInviteDialog(invite.id)
+ showRevokeInviteDialog(() => {
+ revokeInvite(invite.id)
+ })
}
function handleCreateWorkspace() {
@@ -471,7 +462,9 @@ function handleCreateWorkspace() {
})
}
-function handleRemoveMember(member: WorkspaceMember) {
- showRemoveMemberDialog(member.id)
+function handleRemoveMember(_member: WorkspaceMember) {
+ showRemoveMemberDialog(() => {
+ // TODO: Implement actual remove member API call
+ })
}
diff --git a/src/components/dialog/content/setting/WorkspacePanelContent.vue b/src/components/dialog/content/setting/WorkspacePanelContent.vue
index 73f91bc9e..8375edb69 100644
--- a/src/components/dialog/content/setting/WorkspacePanelContent.vue
+++ b/src/components/dialog/content/setting/WorkspacePanelContent.vue
@@ -53,7 +53,7 @@
: null
"
:class="[
- 'flex cursor-pointer items-center gap-2 px-3 py-2',
+ 'flex items-center gap-2 px-3 py-2',
item.class,
item.disabled ? 'pointer-events-auto' : ''
]"
@@ -110,8 +110,7 @@ const { t } = useI18n()
const {
showLeaveWorkspaceDialog,
showDeleteWorkspaceDialog,
- showInviteMemberDialog,
- showEditWorkspaceDialog
+ showInviteMemberDialog
} = useDialogService()
const { isActiveSubscription } = useSubscription()
const {
@@ -130,11 +129,15 @@ const {
const menu = ref | null>(null)
function handleLeaveWorkspace() {
- showLeaveWorkspaceDialog()
+ showLeaveWorkspaceDialog(() => {
+ // TODO: Implement actual leave workspace API call
+ })
}
function handleDeleteWorkspace() {
- showDeleteWorkspaceDialog()
+ showDeleteWorkspaceDialog(() => {
+ // TODO: Implement actual delete workspace API call
+ })
}
const isDeleteDisabled = computed(
@@ -161,50 +164,31 @@ function handleInviteMember() {
})
}
-function handleEditWorkspace() {
- showEditWorkspaceDialog()
-}
-
const menuItems = computed(() => {
- const items: Array<{
- label: string
- icon: string
- class?: string
- disabled?: boolean
- command?: () => void
- }> = []
+ const action = uiConfig.value.workspaceMenuAction
+ if (!action) return []
- // 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
- })
+ 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 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({
+ return [
+ {
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 2e7b457bf..0c317c6fe 100644
--- a/src/components/dialog/content/workspace/CreateWorkspaceDialogContent.vue
+++ b/src/components/dialog/content/workspace/CreateWorkspaceDialogContent.vue
@@ -62,16 +62,14 @@ 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('')
@@ -90,13 +88,8 @@ async function onCreate() {
if (!isValidName.value) return
loading.value = true
try {
- const name = workspaceName.value.trim()
- // Create workspace using global state (creates OWNER unsubscribed workspace)
- createWorkspace(name)
- // Call optional callback if provided
- await onConfirm?.(name)
+ await onConfirm(workspaceName.value.trim())
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 64341ed27..5a455a953 100644
--- a/src/components/dialog/content/workspace/DeleteWorkspaceDialogContent.vue
+++ b/src/components/dialog/content/workspace/DeleteWorkspaceDialogContent.vue
@@ -41,11 +41,13 @@
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() {
@@ -55,7 +57,7 @@ function onCancel() {
async function onDelete() {
loading.value = true
try {
- await deleteWorkspace()
+ await onConfirm()
dialogStore.closeDialog({ key: 'delete-workspace' })
} finally {
loading.value = false
diff --git a/src/components/dialog/content/workspace/LeaveWorkspaceDialogContent.vue b/src/components/dialog/content/workspace/LeaveWorkspaceDialogContent.vue
index 4475fb5c8..da6703cba 100644
--- a/src/components/dialog/content/workspace/LeaveWorkspaceDialogContent.vue
+++ b/src/components/dialog/content/workspace/LeaveWorkspaceDialogContent.vue
@@ -41,11 +41,13 @@
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() {
@@ -55,7 +57,7 @@ function onCancel() {
async function onLeave() {
loading.value = true
try {
- await leaveWorkspace()
+ await onConfirm()
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 4dab4a779..29f444587 100644
--- a/src/components/dialog/content/workspace/RemoveMemberDialogContent.vue
+++ b/src/components/dialog/content/workspace/RemoveMemberDialogContent.vue
@@ -41,15 +41,13 @@
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 { memberId } = defineProps<{
- memberId: string
+const { onConfirm } = defineProps<{
+ onConfirm: () => void | Promise
}>()
const dialogStore = useDialogStore()
-const { removeMember } = useWorkspace()
const loading = ref(false)
function onCancel() {
@@ -59,7 +57,7 @@ function onCancel() {
async function onRemove() {
loading.value = true
try {
- await removeMember(memberId)
+ await onConfirm()
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 58766372e..76744a76d 100644
--- a/src/components/dialog/content/workspace/RevokeInviteDialogContent.vue
+++ b/src/components/dialog/content/workspace/RevokeInviteDialogContent.vue
@@ -41,15 +41,13 @@
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 { inviteId } = defineProps<{
- inviteId: string
+const { onConfirm } = defineProps<{
+ onConfirm: () => void | Promise
}>()
const dialogStore = useDialogStore()
-const { revokeInvite } = useWorkspace()
const loading = ref(false)
function onCancel() {
@@ -59,7 +57,7 @@ function onCancel() {
async function onRevoke() {
loading.value = true
try {
- await revokeInvite(inviteId)
+ await onConfirm()
dialogStore.closeDialog({ key: 'revoke-invite' })
} finally {
loading.value = false
diff --git a/src/components/topbar/CurrentUserPopover.vue b/src/components/topbar/CurrentUserPopover.vue
index 96b2f4e2d..77ebdd1de 100644
--- a/src/components/topbar/CurrentUserPopover.vue
+++ b/src/components/topbar/CurrentUserPopover.vue
@@ -43,10 +43,10 @@
workspaceName
}}
- {{ workspaceTierName }}
+ {{ subscriptionTierName }}
{{ $t('workspaceSwitcher.subscribe') }}
@@ -112,7 +112,7 @@
size="sm"
class="w-full"
data-testid="subscribe-button"
- @click="handleOpenWorkspaceSettings"
+ @click="handleOpenPlansAndPricing"
>
{{ $t('subscription.subscribeNow') }}
@@ -246,8 +246,7 @@ const {
workspaceName,
workspaceRole,
isPersonalWorkspace,
- isWorkspaceSubscribed,
- subscriptionPlan
+ isWorkspaceSubscribed
} = useWorkspace()
const workspaceSwitcherPopover = ref | null>(null)
@@ -262,9 +261,14 @@ const { userDisplayName, userEmail, userPhotoUrl, handleSignOut } =
const authActions = useFirebaseAuthActions()
const authStore = useFirebaseAuthStore()
const dialogService = useDialogService()
-const { isActiveSubscription, fetchStatus } = useSubscription()
+const {
+ isActiveSubscription,
+ subscriptionTierName,
+ subscriptionTier,
+ fetchStatus
+} = useSubscription()
const subscriptionDialog = useSubscriptionDialog()
-const { locale, t } = useI18n()
+const { locale } = useI18n()
const formattedBalance = computed(() => {
const cents =
@@ -281,23 +285,11 @@ 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(() => {
- // For workspace-based subscriptions, can upgrade if not on highest tier
- return isWorkspaceSubscribed.value && subscriptionPlan.value !== null
+ const tier = subscriptionTier.value
+ return (
+ tier === 'FOUNDERS_EDITION' || tier === 'STANDARD' || tier === 'CREATOR'
+ )
})
// Menu visibility based on role
diff --git a/src/locales/en/main.json b/src/locales/en/main.json
index d95bee73c..4137e5dea 100644
--- a/src/locales/en/main.json
+++ b/src/locales/en/main.json
@@ -2128,8 +2128,7 @@
"actions": {
"copyLink": "Copy invite link",
"revokeInvite": "Revoke invite",
- "removeMember": "Remove member",
- "acceptInvite": "Click to accept invite (demo)"
+ "removeMember": "Remove member"
},
"noInvites": "No pending invites",
"noMembers": "No members",
@@ -2137,7 +2136,6 @@
"createNewWorkspace": "create a new one."
},
"menu": {
- "editWorkspaceDetails": "Edit workspace details",
"leaveWorkspace": "Leave Workspace",
"deleteWorkspace": "Delete Workspace",
"deleteWorkspaceDisabledTooltip": "Cancel your workspace's active subscription first"
@@ -2180,11 +2178,6 @@
"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 b5b8cffbe..f1fa68c37 100644
--- a/src/platform/cloud/subscription/components/SubscriptionPanelContent.vue
+++ b/src/platform/cloud/subscription/components/SubscriptionPanelContent.vue
@@ -16,7 +16,7 @@
@@ -259,12 +259,7 @@ import { useWorkspace } from '@/platform/workspace/composables/useWorkspace'
import { cn } from '@/utils/tailwindUtil'
const authActions = useFirebaseAuthActions()
-const {
- permissions,
- isWorkspaceSubscribed,
- workspaceRole,
- subscribeWorkspace
-} = useWorkspace()
+const { permissions, isWorkspaceSubscribed, workspaceRole } = useWorkspace()
const { t, n } = useI18n()
// OWNER with unsubscribed workspace
@@ -272,11 +267,6 @@ 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 0c18f027f..fcb81d77a 100644
--- a/src/platform/workspace/composables/useWorkspace.ts
+++ b/src/platform/workspace/composables/useWorkspace.ts
@@ -1,9 +1,8 @@
-import { computed, ref, shallowRef } from 'vue'
+import { computed, ref } from 'vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
-type WorkspaceRole = 'PERSONAL' | 'MEMBER' | 'OWNER'
-type SubscriptionPlan = 'PRO_MONTHLY' | 'PRO_YEARLY' | null
+export type WorkspaceRole = 'PERSONAL' | 'MEMBER' | 'OWNER'
export interface WorkspaceMember {
id: string
@@ -21,14 +20,10 @@ export interface PendingInvite {
inviteLink: string
}
-interface Workspace {
+interface WorkspaceMockData {
id: string | null
name: string
role: WorkspaceRole
- isSubscribed: boolean
- subscriptionPlan: SubscriptionPlan
- members: WorkspaceMember[]
- pendingInvites: PendingInvite[]
}
export interface AvailableWorkspace {
@@ -38,7 +33,7 @@ export interface AvailableWorkspace {
}
/** Permission flags for workspace actions */
-interface WorkspacePermissions {
+export interface WorkspacePermissions {
canViewOtherMembers: boolean
canViewPendingInvites: boolean
canInviteMembers: boolean
@@ -50,7 +45,7 @@ interface WorkspacePermissions {
}
/** UI configuration for workspace role */
-interface WorkspaceUIConfig {
+export interface WorkspaceUIConfig {
showMembersList: boolean
showPendingTab: boolean
showSearch: boolean
@@ -59,7 +54,6 @@ interface WorkspaceUIConfig {
membersGridCols: string
pendingGridCols: string
headerGridCols: string
- showEditWorkspaceMenuItem: boolean
workspaceMenuAction: 'leave' | 'delete' | null
workspaceMenuDisabledTooltip: string | null
}
@@ -72,7 +66,7 @@ const ROLE_PERMISSIONS: Record = {
canManageInvites: false,
canRemoveMembers: false,
canLeaveWorkspace: false,
- canAccessWorkspaceMenu: true,
+ canAccessWorkspaceMenu: false,
canManageSubscription: true
},
MEMBER: {
@@ -107,7 +101,6 @@ 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
},
@@ -120,7 +113,6 @@ 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
},
@@ -133,202 +125,129 @@ 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: {
+ 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'
+ }
+}
+
+/** 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
-function generateId(): string {
- return Math.random().toString(36).substring(2, 10)
-}
-
-function createPersonalWorkspace(): Workspace {
- return {
- id: null,
- name: 'Personal workspace',
- role: 'PERSONAL',
- isSubscribed: true,
- subscriptionPlan: null,
- members: [],
- pendingInvites: []
- }
-}
-
-// Global state - start with personal workspace only
-const _workspaces = shallowRef([createPersonalWorkspace()])
-const _currentWorkspaceIndex = ref(0)
+// Shared state for workspace
+const _workspaceId = ref(null)
+const _workspaceName = ref('Personal workspace')
+const _workspaceRole = ref('PERSONAL')
+const _isWorkspaceSubscribed = ref(true)
const _activeTab = ref('plan')
-
-// 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]
- })
- }
-}
+const _members = ref([])
+const _pendingInvites = ref([])
+const _availableWorkspaces = ref(
+ MOCK_AVAILABLE_WORKSPACES
+)
/**
* Set workspace mock state for testing UI
* Usage in browser console: window.__setWorkspaceRole('OWNER')
*/
function setMockRole(role: WorkspaceRole) {
- updateCurrentWorkspace({ role })
+ const data = MOCK_DATA[role]
+ _workspaceId.value = data.id
+ _workspaceName.value = data.name
+ _workspaceRole.value = data.role
}
/**
@@ -336,7 +255,19 @@ function setMockRole(role: WorkspaceRole) {
* Usage in browser console: window.__setWorkspaceSubscribed(false)
*/
function setMockSubscribed(subscribed: boolean) {
- updateCurrentWorkspace({ isSubscribed: subscribed })
+ _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 = []
}
// Expose to window for dev testing
@@ -359,70 +290,43 @@ if (typeof window !== 'undefined') {
export function useWorkspace() {
const { userDisplayName, userEmail } = useCurrentUser()
- // 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 workspaceId = computed(() => _workspaceId.value)
+ const workspaceName = computed(() => _workspaceName.value)
+ const workspaceRole = computed(() => _workspaceRole.value)
const activeTab = computed(() => _activeTab.value)
const isPersonalWorkspace = computed(
- () => currentWorkspace.value?.role === 'PERSONAL'
+ () => _workspaceRole.value === 'PERSONAL'
)
- const isWorkspaceSubscribed = computed(
- () => currentWorkspace.value?.isSubscribed ?? false
- )
-
- const subscriptionPlan = computed(
- () => currentWorkspace.value?.subscriptionPlan ?? null
- )
+ const isWorkspaceSubscribed = computed(() => _isWorkspaceSubscribed.value)
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)
}
- // 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',
- email: userEmail.value ?? '',
- joinDate: new Date()
- }
- ]
- }
- return currentWorkspace.value?.members ?? []
- })
- const pendingInvites = computed(
- () => currentWorkspace.value?.pendingInvites ?? []
- )
+ const members = computed(() => _members.value)
+ const pendingInvites = computed(() => _pendingInvites.value)
const totalMemberSlots = computed(
- () => members.value.length + pendingInvites.value.length
+ () => _members.value.length + _pendingInvites.value.length
)
const isInviteLimitReached = computed(
() => totalMemberSlots.value >= MAX_WORKSPACE_MEMBERS
)
- // Fetch members - returns current user for personal workspace
+ // TODO: Replace with actual API calls
async function fetchMembers(): Promise {
- if (isPersonalWorkspace.value) {
- return [
+ if (_workspaceRole.value === 'PERSONAL') {
+ _members.value = [
{
id: 'current-user',
name: userDisplayName.value ?? 'You',
@@ -430,20 +334,27 @@ export function useWorkspace() {
joinDate: new Date()
}
]
+ } else {
+ _members.value = MOCK_MEMBERS
}
- return members.value
+ return _members.value
}
async function fetchPendingInvites(): Promise {
- return pendingInvites.value
+ if (_workspaceRole.value === 'PERSONAL') {
+ _pendingInvites.value = []
+ } else {
+ _pendingInvites.value = MOCK_PENDING_INVITES
+ }
+ return _pendingInvites.value
}
- async function revokeInvite(inviteId: string): Promise {
- revokePendingInvite(inviteId)
+ async function revokeInvite(_inviteId: string): Promise {
+ // TODO: API call to revoke invite
}
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
@@ -451,63 +362,35 @@ 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, 500))
+ await new Promise((resolve) => setTimeout(resolve, 1000))
- const inviteId = generateId()
- const inviteLink = `https://cloud.comfy.org/workspace/invite/${inviteId}`
+ // Generate mock invite link
+ const inviteId = Math.random().toString(36).substring(2, 10)
+ const inviteLink = `https://cloud.comfy.org/workspace?3423532/invite/hi789jkl012mno345pq`
- // Add to pending invites
- const current = getCurrentWorkspace()
+ // Add to pending invites (mock)
const newInvite: PendingInvite = {
id: inviteId,
name: email.split('@')[0],
email,
inviteDate: new Date(),
- expiryDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
+ expiryDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
inviteLink
}
- updateCurrentWorkspace({
- pendingInvites: [...current.pendingInvites, newInvite]
- })
+ _pendingInvites.value = [..._pendingInvites.value, newInvite]
return inviteLink
}
- const availableWorkspaces = computed(() =>
- _workspaces.value.map((w) => ({
- id: w.id,
- name: w.name,
- role: w.role
- }))
- )
+ const availableWorkspaces = computed(() => _availableWorkspaces.value)
const ownedWorkspacesCount = computed(
- () => _workspaces.value.filter((w) => w.role === 'OWNER').length
+ () => _availableWorkspaces.value.filter((w) => w.role === 'OWNER').length
)
const canCreateWorkspace = computed(
() => ownedWorkspacesCount.value < MAX_OWNED_WORKSPACES
@@ -520,7 +403,6 @@ export function useWorkspace() {
activeTab,
isPersonalWorkspace,
isWorkspaceSubscribed,
- subscriptionPlan,
permissions,
uiConfig,
setActiveTab,
@@ -529,11 +411,6 @@ export function useWorkspace() {
ownedWorkspacesCount,
canCreateWorkspace,
switchWorkspace,
- // Workspace management
- createWorkspace: createNewWorkspace,
- subscribeWorkspace: subscribeCurrentWorkspace,
- deleteWorkspace: deleteCurrentWorkspace,
- leaveWorkspace: leaveCurrentWorkspace,
// Members
members,
pendingInvites,
@@ -542,13 +419,8 @@ 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 c72ca8658..944c915a6 100644
--- a/src/services/dialogService.ts
+++ b/src/services/dialogService.ts
@@ -3,7 +3,6 @@ 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'
@@ -527,10 +526,11 @@ export const useDialogService = () => {
show()
}
- function showLeaveWorkspaceDialog() {
+ function showLeaveWorkspaceDialog(onConfirm: () => void | Promise) {
return dialogStore.showDialog({
key: 'leave-workspace',
component: LeaveWorkspaceDialogContent,
+ props: { onConfirm },
dialogComponentProps: {
headless: true,
pt: {
@@ -542,10 +542,11 @@ export const useDialogService = () => {
})
}
- function showDeleteWorkspaceDialog() {
+ function showDeleteWorkspaceDialog(onConfirm: () => void | Promise) {
return dialogStore.showDialog({
key: 'delete-workspace',
component: DeleteWorkspaceDialogContent,
+ props: { onConfirm },
dialogComponentProps: {
headless: true,
pt: {
@@ -557,11 +558,11 @@ export const useDialogService = () => {
})
}
- function showRemoveMemberDialog(memberId: string) {
+ function showRemoveMemberDialog(onConfirm: () => void | Promise) {
return dialogStore.showDialog({
key: 'remove-member',
component: RemoveMemberDialogContent,
- props: { memberId },
+ props: { onConfirm },
dialogComponentProps: {
headless: true,
pt: {
@@ -573,11 +574,11 @@ export const useDialogService = () => {
})
}
- function showRevokeInviteDialog(inviteId: string) {
+ function showRevokeInviteDialog(onConfirm: () => void | Promise) {
return dialogStore.showDialog({
key: 'revoke-invite',
component: RevokeInviteDialogContent,
- props: { inviteId },
+ props: { onConfirm },
dialogComponentProps: {
headless: true,
pt: {
@@ -625,21 +626,6 @@ 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,
@@ -657,7 +643,6 @@ export const useDialogService = () => {
showRevokeInviteDialog,
showInviteMemberDialog,
showCreateWorkspaceDialog,
- showEditWorkspaceDialog,
showExtensionDialog,
prompt,
showErrorDialog,