mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-21 15:24:09 +00:00
feat: implemented workspace flow
This commit is contained in:
@@ -3,66 +3,98 @@
|
||||
<div class="rounded-2xl border border-interface-stroke p-6">
|
||||
<div>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-sm font-bold text-text-primary">
|
||||
{{ subscriptionTierName }}
|
||||
<!-- OWNER Unsubscribed State -->
|
||||
<template v-if="isOwnerUnsubscribed">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-sm font-bold text-text-primary">
|
||||
{{ $t('subscription.workspaceNotSubscribed') }}
|
||||
</div>
|
||||
<div class="text-sm text-text-secondary">
|
||||
{{ $t('subscription.subscriptionRequiredMessage') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-baseline gap-1 font-inter font-semibold">
|
||||
<span class="text-2xl">${{ tierPrice }}</span>
|
||||
<span class="text-base">{{
|
||||
$t('subscription.perMonth')
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="isActiveSubscription"
|
||||
class="text-sm text-text-secondary"
|
||||
<Button
|
||||
variant="primary"
|
||||
class="ml-auto rounded-lg px-4 py-2 text-sm font-normal"
|
||||
@click="showSubscriptionDialog"
|
||||
>
|
||||
<template v-if="isCancelled">
|
||||
{{
|
||||
$t('subscription.expiresDate', {
|
||||
date: formattedEndDate
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{
|
||||
$t('subscription.renewsDate', {
|
||||
date: formattedRenewalDate
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
{{ $t('subscription.subscribeNow') }}
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<!-- Normal Subscribed State -->
|
||||
<template v-else>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-sm font-bold text-text-primary">
|
||||
{{ subscriptionTierName }}
|
||||
</div>
|
||||
<div class="flex items-baseline gap-1 font-inter font-semibold">
|
||||
<span class="text-2xl">${{ tierPrice }}</span>
|
||||
<span class="text-base">{{ $t('subscription.perMonth') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="isActiveSubscription"
|
||||
class="text-sm text-text-secondary"
|
||||
>
|
||||
<template v-if="isCancelled">
|
||||
{{
|
||||
$t('subscription.expiresDate', {
|
||||
date: formattedEndDate
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{
|
||||
$t('subscription.renewsDate', {
|
||||
date: formattedRenewalDate
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
v-if="isActiveSubscription"
|
||||
variant="secondary"
|
||||
class="ml-auto rounded-lg px-4 py-2 text-sm font-normal text-text-primary bg-interface-menu-component-surface-selected"
|
||||
@click="
|
||||
async () => {
|
||||
await authActions.accessBillingPortal()
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ $t('subscription.manageSubscription') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="isActiveSubscription"
|
||||
variant="primary"
|
||||
class="rounded-lg px-4 py-2 text-sm font-normal text-text-primary"
|
||||
@click="showSubscriptionDialog"
|
||||
>
|
||||
{{ $t('subscription.upgradePlan') }}
|
||||
</Button>
|
||||
<template
|
||||
v-if="isActiveSubscription && permissions.canManageSubscription"
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
class="ml-auto rounded-lg px-4 py-2 text-sm font-normal text-text-primary bg-interface-menu-component-surface-selected"
|
||||
@click="
|
||||
async () => {
|
||||
await authActions.accessBillingPortal()
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ $t('subscription.managePayment') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
class="rounded-lg px-4 py-2 text-sm font-normal text-text-primary"
|
||||
@click="showSubscriptionDialog"
|
||||
>
|
||||
{{ $t('subscription.upgradePlan') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-tooltip="{ value: $t('g.moreOptions'), showDelay: 300 }"
|
||||
variant="muted-textonly"
|
||||
size="icon"
|
||||
:aria-label="$t('g.moreOptions')"
|
||||
@click="planMenu?.toggle($event)"
|
||||
>
|
||||
<i class="pi pi-ellipsis-h" />
|
||||
</Button>
|
||||
<Menu ref="planMenu" :model="planMenuItems" :popup="true" />
|
||||
</template>
|
||||
|
||||
<SubscribeButton
|
||||
v-else
|
||||
:label="$t('subscription.subscribeNow')"
|
||||
size="sm"
|
||||
:fluid="false"
|
||||
class="text-xs"
|
||||
@subscribed="handleRefresh"
|
||||
/>
|
||||
<SubscribeButton
|
||||
v-if="!isActiveSubscription"
|
||||
:label="$t('subscription.subscribeNow')"
|
||||
size="sm"
|
||||
:fluid="false"
|
||||
class="text-xs"
|
||||
@subscribed="handleRefresh"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -91,13 +123,9 @@
|
||||
<div class="text-sm text-muted">
|
||||
{{ $t('subscription.totalCredits') }}
|
||||
</div>
|
||||
<Skeleton
|
||||
v-if="isLoadingBalance"
|
||||
width="8rem"
|
||||
height="2rem"
|
||||
/>
|
||||
<Skeleton v-if="isLoadingBalance" width="8rem" height="2rem" />
|
||||
<div v-else class="text-2xl font-bold">
|
||||
{{ totalCredits }}
|
||||
{{ isOwnerUnsubscribed ? '0' : totalCredits }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -111,7 +139,9 @@
|
||||
width="5rem"
|
||||
height="1rem"
|
||||
/>
|
||||
<span v-else>{{ includedCreditsDisplay }}</span>
|
||||
<span v-else>{{
|
||||
isOwnerUnsubscribed ? '0 / 0' : includedCreditsDisplay
|
||||
}}</span>
|
||||
</td>
|
||||
<td class="align-middle" :title="creditsRemainingLabel">
|
||||
{{ creditsRemainingLabel }}
|
||||
@@ -124,7 +154,9 @@
|
||||
width="3rem"
|
||||
height="1rem"
|
||||
/>
|
||||
<span v-else>{{ prepaidCredits }}</span>
|
||||
<span v-else>{{
|
||||
isOwnerUnsubscribed ? '0' : prepaidCredits
|
||||
}}</span>
|
||||
</td>
|
||||
<td
|
||||
class="align-middle"
|
||||
@@ -146,7 +178,7 @@
|
||||
{{ $t('subscription.viewUsageHistory') }}
|
||||
</a>
|
||||
<Button
|
||||
v-if="isActiveSubscription"
|
||||
v-if="isActiveSubscription && !isOwnerUnsubscribed"
|
||||
variant="secondary"
|
||||
class="p-2 min-h-8 rounded-lg text-sm font-normal text-text-primary bg-interface-menu-component-surface-selected"
|
||||
@click="handleAddApiCredits"
|
||||
@@ -204,8 +236,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Menu from 'primevue/menu'
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import { computed, onBeforeUnmount, onMounted } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
@@ -222,11 +255,18 @@ import {
|
||||
getTierFeatures,
|
||||
getTierPrice
|
||||
} from '@/platform/cloud/subscription/constants/tierPricing'
|
||||
import { useWorkspace } from '@/platform/workspace/composables/useWorkspace'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const { permissions, isWorkspaceSubscribed, workspaceRole } = useWorkspace()
|
||||
const { t, n } = useI18n()
|
||||
|
||||
// OWNER with unsubscribed workspace
|
||||
const isOwnerUnsubscribed = computed(
|
||||
() => workspaceRole.value === 'OWNER' && !isWorkspaceSubscribed.value
|
||||
)
|
||||
|
||||
const {
|
||||
isActiveSubscription,
|
||||
isCancelled,
|
||||
@@ -240,6 +280,18 @@ const {
|
||||
|
||||
const { show: showSubscriptionDialog } = useSubscriptionDialog()
|
||||
|
||||
const planMenu = ref<InstanceType<typeof Menu> | null>(null)
|
||||
|
||||
const planMenuItems = computed(() => [
|
||||
{
|
||||
label: t('subscription.cancelSubscription'),
|
||||
icon: 'pi pi-times',
|
||||
command: async () => {
|
||||
await authActions.accessBillingPortal()
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const tierKey = computed(() => {
|
||||
const tier = subscriptionTier.value
|
||||
if (!tier) return DEFAULT_TIER_KEY
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
class="w-full border-none bg-transparent"
|
||||
>
|
||||
<template #optiongroup="{ option }">
|
||||
<Divider v-if="option.key !== 'workspace'" class="my-2" />
|
||||
<h3 class="px-2 py-1 text-xs font-semibold uppercase text-muted">
|
||||
<!-- <Divider v-if="option.key !== 'workspace'" class="my-2" /> -->
|
||||
<h3 class="text-xs font-semibold uppercase text-muted m-0 pt-6 pb-2">
|
||||
{{ option.label }}
|
||||
</h3>
|
||||
</template>
|
||||
@@ -96,8 +96,6 @@ const { defaultPanel } = defineProps<{
|
||||
| 'credits'
|
||||
| 'subscription'
|
||||
| 'workspace'
|
||||
| 'workspace-plan'
|
||||
| 'workspace-members'
|
||||
}>()
|
||||
|
||||
const {
|
||||
|
||||
@@ -12,7 +12,6 @@ import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { buildTree } from '@/utils/treeUtil'
|
||||
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import { useWorkspace } from '@/platform/workspace/composables/useWorkspace'
|
||||
|
||||
interface SettingPanelItem {
|
||||
node: SettingTreeNode
|
||||
@@ -30,8 +29,6 @@ export function useSettingUI(
|
||||
| 'credits'
|
||||
| 'subscription'
|
||||
| 'workspace'
|
||||
| 'workspace-plan'
|
||||
| 'workspace-members'
|
||||
) {
|
||||
const { t } = useI18n()
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
@@ -40,7 +37,6 @@ export function useSettingUI(
|
||||
|
||||
const { shouldRenderVueNodes } = useVueFeatureFlags()
|
||||
const { isActiveSubscription } = useSubscription()
|
||||
const { workspaceName } = useWorkspace()
|
||||
|
||||
const settingRoot = computed<SettingTreeNode>(() => {
|
||||
const root = buildTree(
|
||||
@@ -162,20 +158,6 @@ export function useSettingUI(
|
||||
)
|
||||
}
|
||||
|
||||
// Sidebar-only node for Plan & Credits (uses same WorkspacePanel component)
|
||||
const workspacePlanNode: SettingTreeNode = {
|
||||
key: 'workspace-plan',
|
||||
label: 'WorkspacePlan',
|
||||
children: []
|
||||
}
|
||||
|
||||
// Sidebar-only node for Members (uses same WorkspacePanel component)
|
||||
const workspaceMembersNode: SettingTreeNode = {
|
||||
key: 'workspace-members',
|
||||
label: 'WorkspaceMembers',
|
||||
children: []
|
||||
}
|
||||
|
||||
const keybindingPanel: SettingPanelItem = {
|
||||
node: {
|
||||
key: 'keybinding',
|
||||
@@ -252,8 +234,6 @@ export function useSettingUI(
|
||||
label: 'Workspace',
|
||||
children: [
|
||||
workspacePanel.node,
|
||||
workspacePlanNode,
|
||||
...(workspaceName.value ? [workspaceMembersNode] : []),
|
||||
...(isLoggedIn.value &&
|
||||
!(isCloud && window.__CONFIG__?.subscription_required)
|
||||
? [creditsPanel.node]
|
||||
|
||||
@@ -1,24 +1,428 @@
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
|
||||
export type WorkspaceRole = 'PERSONAL' | 'MEMBER' | 'OWNER'
|
||||
|
||||
export interface WorkspaceMember {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
joinDate: Date
|
||||
}
|
||||
|
||||
export interface PendingInvite {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
inviteDate: Date
|
||||
expiryDate: Date
|
||||
inviteLink: string
|
||||
}
|
||||
|
||||
interface WorkspaceMockData {
|
||||
id: string | null
|
||||
name: string
|
||||
role: WorkspaceRole
|
||||
}
|
||||
|
||||
export interface AvailableWorkspace {
|
||||
id: string | null
|
||||
name: string
|
||||
role: WorkspaceRole
|
||||
}
|
||||
|
||||
/** Permission flags for workspace actions */
|
||||
export interface WorkspacePermissions {
|
||||
canViewOtherMembers: boolean
|
||||
canViewPendingInvites: boolean
|
||||
canInviteMembers: boolean
|
||||
canManageInvites: boolean
|
||||
canRemoveMembers: boolean
|
||||
canLeaveWorkspace: boolean
|
||||
canAccessWorkspaceMenu: boolean
|
||||
canManageSubscription: boolean
|
||||
}
|
||||
|
||||
/** UI configuration for workspace role */
|
||||
export interface WorkspaceUIConfig {
|
||||
showMembersList: boolean
|
||||
showPendingTab: boolean
|
||||
showSearch: boolean
|
||||
showDateColumn: boolean
|
||||
showRoleBadge: boolean
|
||||
membersGridCols: string
|
||||
pendingGridCols: string
|
||||
headerGridCols: string
|
||||
workspaceMenuAction: 'leave' | 'delete' | null
|
||||
workspaceMenuDisabledTooltip: string | null
|
||||
}
|
||||
|
||||
const ROLE_PERMISSIONS: Record<WorkspaceRole, WorkspacePermissions> = {
|
||||
PERSONAL: {
|
||||
canViewOtherMembers: false,
|
||||
canViewPendingInvites: false,
|
||||
canInviteMembers: false,
|
||||
canManageInvites: false,
|
||||
canRemoveMembers: false,
|
||||
canLeaveWorkspace: false,
|
||||
canAccessWorkspaceMenu: false,
|
||||
canManageSubscription: true
|
||||
},
|
||||
MEMBER: {
|
||||
canViewOtherMembers: true,
|
||||
canViewPendingInvites: false,
|
||||
canInviteMembers: false,
|
||||
canManageInvites: false,
|
||||
canRemoveMembers: false,
|
||||
canLeaveWorkspace: true,
|
||||
canAccessWorkspaceMenu: true,
|
||||
canManageSubscription: false
|
||||
},
|
||||
OWNER: {
|
||||
canViewOtherMembers: true,
|
||||
canViewPendingInvites: true,
|
||||
canInviteMembers: true,
|
||||
canManageInvites: true,
|
||||
canRemoveMembers: true,
|
||||
canLeaveWorkspace: true,
|
||||
canAccessWorkspaceMenu: true,
|
||||
canManageSubscription: true
|
||||
}
|
||||
}
|
||||
|
||||
const ROLE_UI_CONFIG: Record<WorkspaceRole, WorkspaceUIConfig> = {
|
||||
PERSONAL: {
|
||||
showMembersList: false,
|
||||
showPendingTab: false,
|
||||
showSearch: false,
|
||||
showDateColumn: false,
|
||||
showRoleBadge: false,
|
||||
membersGridCols: 'grid-cols-1',
|
||||
pendingGridCols: 'grid-cols-[50%_20%_20%_10%]',
|
||||
headerGridCols: 'grid-cols-1',
|
||||
workspaceMenuAction: null,
|
||||
workspaceMenuDisabledTooltip: null
|
||||
},
|
||||
MEMBER: {
|
||||
showMembersList: true,
|
||||
showPendingTab: false,
|
||||
showSearch: true,
|
||||
showDateColumn: true,
|
||||
showRoleBadge: true,
|
||||
membersGridCols: 'grid-cols-[1fr_auto]',
|
||||
pendingGridCols: 'grid-cols-[50%_20%_20%_10%]',
|
||||
headerGridCols: 'grid-cols-[1fr_auto]',
|
||||
workspaceMenuAction: 'leave',
|
||||
workspaceMenuDisabledTooltip: null
|
||||
},
|
||||
OWNER: {
|
||||
showMembersList: true,
|
||||
showPendingTab: true,
|
||||
showSearch: true,
|
||||
showDateColumn: true,
|
||||
showRoleBadge: true,
|
||||
membersGridCols: 'grid-cols-[50%_40%_10%]',
|
||||
pendingGridCols: 'grid-cols-[50%_20%_20%_10%]',
|
||||
headerGridCols: 'grid-cols-[50%_40%_10%]',
|
||||
workspaceMenuAction: 'delete',
|
||||
workspaceMenuDisabledTooltip:
|
||||
'workspacePanel.menu.deleteWorkspaceDisabledTooltip'
|
||||
}
|
||||
}
|
||||
|
||||
const MOCK_DATA: Record<WorkspaceRole, WorkspaceMockData> = {
|
||||
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
|
||||
|
||||
// Shared state for workspace
|
||||
const _workspaceName = ref<string | null>(null)
|
||||
const _activeTab = ref<string>('general')
|
||||
const _workspaceId = ref<string | null>(null)
|
||||
const _workspaceName = ref<string>('Personal workspace')
|
||||
const _workspaceRole = ref<WorkspaceRole>('PERSONAL')
|
||||
const _isWorkspaceSubscribed = ref<boolean>(true)
|
||||
const _activeTab = ref<string>('plan')
|
||||
const _members = ref<WorkspaceMember[]>([])
|
||||
const _pendingInvites = ref<PendingInvite[]>([])
|
||||
const _availableWorkspaces = ref<AvailableWorkspace[]>(
|
||||
MOCK_AVAILABLE_WORKSPACES
|
||||
)
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Set workspace subscription state for testing UI
|
||||
* 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 = []
|
||||
}
|
||||
|
||||
// Expose to window for dev testing
|
||||
if (typeof window !== 'undefined') {
|
||||
;(
|
||||
window as Window & {
|
||||
__setWorkspaceRole?: typeof setMockRole
|
||||
__setWorkspaceSubscribed?: typeof setMockSubscribed
|
||||
}
|
||||
).__setWorkspaceRole = setMockRole
|
||||
;(
|
||||
window as Window & { __setWorkspaceSubscribed?: typeof setMockSubscribed }
|
||||
).__setWorkspaceSubscribed = setMockSubscribed
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for handling workspace data
|
||||
* TODO: Replace stubbed data with actual API call
|
||||
*/
|
||||
export function useWorkspace() {
|
||||
const { userDisplayName, userEmail } = useCurrentUser()
|
||||
|
||||
const workspaceId = computed(() => _workspaceId.value)
|
||||
const workspaceName = computed(() => _workspaceName.value)
|
||||
const workspaceRole = computed(() => _workspaceRole.value)
|
||||
const activeTab = computed(() => _activeTab.value)
|
||||
|
||||
const isPersonalWorkspace = computed(
|
||||
() => _workspaceRole.value === 'PERSONAL'
|
||||
)
|
||||
|
||||
const isWorkspaceSubscribed = computed(() => _isWorkspaceSubscribed.value)
|
||||
|
||||
const permissions = computed<WorkspacePermissions>(
|
||||
() => ROLE_PERMISSIONS[_workspaceRole.value]
|
||||
)
|
||||
|
||||
const uiConfig = computed<WorkspaceUIConfig>(
|
||||
() => 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<WorkspaceMember[]> {
|
||||
if (_workspaceRole.value === 'PERSONAL') {
|
||||
_members.value = [
|
||||
{
|
||||
id: 'current-user',
|
||||
name: userDisplayName.value ?? 'You',
|
||||
email: userEmail.value ?? '',
|
||||
joinDate: new Date()
|
||||
}
|
||||
]
|
||||
} else {
|
||||
_members.value = MOCK_MEMBERS
|
||||
}
|
||||
return _members.value
|
||||
}
|
||||
|
||||
async function fetchPendingInvites(): Promise<PendingInvite[]> {
|
||||
if (_workspaceRole.value === 'PERSONAL') {
|
||||
_pendingInvites.value = []
|
||||
} else {
|
||||
_pendingInvites.value = MOCK_PENDING_INVITES
|
||||
}
|
||||
return _pendingInvites.value
|
||||
}
|
||||
|
||||
async function revokeInvite(_inviteId: string): Promise<void> {
|
||||
// TODO: API call to revoke invite
|
||||
}
|
||||
|
||||
async function copyInviteLink(inviteId: string): Promise<string> {
|
||||
const invite = _pendingInvites.value.find((i) => i.id === inviteId)
|
||||
if (invite) {
|
||||
await navigator.clipboard.writeText(invite.inviteLink)
|
||||
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<string> {
|
||||
// Simulate API delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
// 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 (mock)
|
||||
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
|
||||
inviteLink
|
||||
}
|
||||
_pendingInvites.value = [..._pendingInvites.value, newInvite]
|
||||
|
||||
return inviteLink
|
||||
}
|
||||
|
||||
const availableWorkspaces = computed(() => _availableWorkspaces.value)
|
||||
const ownedWorkspacesCount = computed(
|
||||
() => _availableWorkspaces.value.filter((w) => w.role === 'OWNER').length
|
||||
)
|
||||
const canCreateWorkspace = computed(
|
||||
() => ownedWorkspacesCount.value < MAX_OWNED_WORKSPACES
|
||||
)
|
||||
|
||||
return {
|
||||
workspaceId,
|
||||
workspaceName,
|
||||
workspaceRole,
|
||||
activeTab,
|
||||
setActiveTab
|
||||
isPersonalWorkspace,
|
||||
isWorkspaceSubscribed,
|
||||
permissions,
|
||||
uiConfig,
|
||||
setActiveTab,
|
||||
// Workspace switching
|
||||
availableWorkspaces,
|
||||
ownedWorkspacesCount,
|
||||
canCreateWorkspace,
|
||||
switchWorkspace,
|
||||
// Members
|
||||
members,
|
||||
pendingInvites,
|
||||
totalMemberSlots,
|
||||
isInviteLimitReached,
|
||||
fetchMembers,
|
||||
fetchPendingInvites,
|
||||
revokeInvite,
|
||||
copyInviteLink,
|
||||
createInviteLink,
|
||||
// Dev helpers
|
||||
setMockRole,
|
||||
setMockSubscribed
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user