mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 18:52:19 +00:00
feat: add workspace API and refactor useWorkspace
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -457,9 +457,7 @@ function handleRevokeInvite(invite: PendingInvite) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleCreateWorkspace() {
|
function handleCreateWorkspace() {
|
||||||
showCreateWorkspaceDialog(() => {
|
showCreateWorkspaceDialog()
|
||||||
// TODO: Implement actual create workspace API call
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRemoveMember(_member: WorkspaceMember) {
|
function handleRemoveMember(_member: WorkspaceMember) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!-- A button that shows current authenticated user's avatar -->
|
<!-- A button that shows current workspace's profile picture -->
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
@@ -16,7 +16,10 @@
|
|||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<UserAvatar :photo-url="photoURL" :class="compact && 'size-full'" />
|
<WorkspaceProfilePic
|
||||||
|
:workspace-name="workspaceName"
|
||||||
|
:class="compact && 'size-full'"
|
||||||
|
/>
|
||||||
|
|
||||||
<i v-if="showArrow" class="icon-[lucide--chevron-down] size-3 px-1" />
|
<i v-if="showArrow" class="icon-[lucide--chevron-down] size-3 px-1" />
|
||||||
</div>
|
</div>
|
||||||
@@ -38,11 +41,12 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Popover from 'primevue/popover'
|
import Popover from 'primevue/popover'
|
||||||
import { computed, ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
import UserAvatar from '@/components/common/UserAvatar.vue'
|
import WorkspaceProfilePic from '@/components/common/WorkspaceProfilePic.vue'
|
||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||||
|
import { useWorkspace } from '@/platform/workspace/composables/useWorkspace'
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
import CurrentUserPopover from './CurrentUserPopover.vue'
|
import CurrentUserPopover from './CurrentUserPopover.vue'
|
||||||
@@ -52,12 +56,10 @@ const { showArrow = true, compact = false } = defineProps<{
|
|||||||
compact?: boolean
|
compact?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { isLoggedIn, userPhotoUrl } = useCurrentUser()
|
const { isLoggedIn } = useCurrentUser()
|
||||||
|
const { workspaceName } = useWorkspace()
|
||||||
|
|
||||||
const popover = ref<InstanceType<typeof Popover> | null>(null)
|
const popover = ref<InstanceType<typeof Popover> | null>(null)
|
||||||
const photoURL = computed<string | undefined>(
|
|
||||||
() => userPhotoUrl.value ?? undefined
|
|
||||||
)
|
|
||||||
|
|
||||||
const closePopover = () => {
|
const closePopover = () => {
|
||||||
popover.value?.hide()
|
popover.value?.hide()
|
||||||
|
|||||||
@@ -104,7 +104,7 @@
|
|||||||
|
|
||||||
<!-- OWNER unsubscribed: Show Subscribe button (primary) -->
|
<!-- OWNER unsubscribed: Show Subscribe button (primary) -->
|
||||||
<div
|
<div
|
||||||
v-else-if="workspaceRole === 'OWNER' && !isWorkspaceSubscribed"
|
v-else-if="workspaceRole === 'owner' && !isWorkspaceSubscribed"
|
||||||
class="flex justify-center px-4 py-2"
|
class="flex justify-center px-4 py-2"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
@@ -298,13 +298,13 @@ const canUpgrade = computed(() => {
|
|||||||
// OWNER (unsubscribed): Plans & pricing, Partner nodes, Divider, Workspace settings, Account settings, Divider, Log out
|
// OWNER (unsubscribed): Plans & pricing, Partner nodes, Divider, Workspace settings, Account settings, Divider, Log out
|
||||||
// OWNER (subscribed): Plans & pricing, Manage plan, Partner nodes, Divider, Workspace settings, Account settings, Divider, Log out
|
// OWNER (subscribed): Plans & pricing, Manage plan, Partner nodes, Divider, Workspace settings, Account settings, Divider, Log out
|
||||||
const showPlansAndPricing = computed(
|
const showPlansAndPricing = computed(
|
||||||
() => isPersonalWorkspace.value || workspaceRole.value === 'OWNER'
|
() => isPersonalWorkspace.value || workspaceRole.value === 'owner'
|
||||||
)
|
)
|
||||||
const showManagePlan = computed(
|
const showManagePlan = computed(
|
||||||
() => showPlansAndPricing.value && isActiveSubscription.value
|
() => showPlansAndPricing.value && isActiveSubscription.value
|
||||||
)
|
)
|
||||||
const showCreditsSection = computed(
|
const showCreditsSection = computed(
|
||||||
() => isPersonalWorkspace.value || workspaceRole.value === 'OWNER'
|
() => isPersonalWorkspace.value || workspaceRole.value === 'owner'
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleOpenUserSettings = () => {
|
const handleOpenUserSettings = () => {
|
||||||
@@ -358,9 +358,7 @@ const handleSubscribed = async () => {
|
|||||||
|
|
||||||
const handleCreateWorkspace = () => {
|
const handleCreateWorkspace = () => {
|
||||||
workspaceSwitcherPopover.value?.hide()
|
workspaceSwitcherPopover.value?.hide()
|
||||||
dialogService.showCreateWorkspaceDialog(() => {
|
dialogService.showCreateWorkspaceDialog()
|
||||||
// TODO: Implement actual create workspace API call
|
|
||||||
})
|
|
||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
{{ workspace.name }}
|
{{ workspace.name }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="workspace.role !== 'PERSONAL'"
|
v-if="workspace.type !== 'personal'"
|
||||||
class="text-sm text-muted-foreground"
|
class="text-sm text-muted-foreground"
|
||||||
>
|
>
|
||||||
{{ getRoleLabel(workspace.role) }}
|
{{ getRoleLabel(workspace.role) }}
|
||||||
@@ -107,8 +107,8 @@ function isCurrentWorkspace(workspace: AvailableWorkspace): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getRoleLabel(role: AvailableWorkspace['role']): string {
|
function getRoleLabel(role: AvailableWorkspace['role']): string {
|
||||||
if (role === 'OWNER') return t('workspaceSwitcher.roleOwner')
|
if (role === 'owner') return t('workspaceSwitcher.roleOwner')
|
||||||
if (role === 'MEMBER') return t('workspaceSwitcher.roleMember')
|
if (role === 'member') return t('workspaceSwitcher.roleMember')
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ const { t, n } = useI18n()
|
|||||||
|
|
||||||
// OWNER with unsubscribed workspace
|
// OWNER with unsubscribed workspace
|
||||||
const isOwnerUnsubscribed = computed(
|
const isOwnerUnsubscribed = computed(
|
||||||
() => workspaceRole.value === 'OWNER' && !isWorkspaceSubscribed.value
|
() => workspaceRole.value === 'owner' && !isWorkspaceSubscribed.value
|
||||||
)
|
)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
210
src/platform/workspace/api/workspaceApi.ts
Normal file
210
src/platform/workspace/api/workspaceApi.ts
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import type { AxiosResponse } from 'axios'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import { getComfyApiBaseUrl } from '@/config/comfyApi'
|
||||||
|
import { t } from '@/i18n'
|
||||||
|
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||||
|
import type { AuthHeader } from '@/types/authTypes'
|
||||||
|
|
||||||
|
// Types aligned with backend API (matching useWorkspaceAuth types)
|
||||||
|
export type WorkspaceType = 'personal' | 'team'
|
||||||
|
export type WorkspaceRole = 'owner' | 'member'
|
||||||
|
|
||||||
|
export interface Workspace {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
type: WorkspaceType
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkspaceWithRole extends Workspace {
|
||||||
|
role: WorkspaceRole
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkspaceMember {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
role: WorkspaceRole
|
||||||
|
joined_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PendingInvite {
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
invited_at: string
|
||||||
|
expires_at: string
|
||||||
|
invite_link: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkspaceDetails extends WorkspaceWithRole {
|
||||||
|
members: WorkspaceMember[]
|
||||||
|
pending_invites: PendingInvite[]
|
||||||
|
subscription_status: {
|
||||||
|
is_active: boolean
|
||||||
|
plan: string | null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateWorkspacePayload {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateWorkspacePayload {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateInvitePayload {
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateInviteResponse {
|
||||||
|
id: string
|
||||||
|
invite_link: string
|
||||||
|
expires_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// API responses
|
||||||
|
export interface ListWorkspacesResponse {
|
||||||
|
workspaces: WorkspaceWithRole[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WorkspaceApiError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public readonly status?: number,
|
||||||
|
public readonly code?: string
|
||||||
|
) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'WorkspaceApiError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceApiClient = axios.create({
|
||||||
|
baseURL: getComfyApiBaseUrl(),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function withAuth<T>(
|
||||||
|
request: (headers: AuthHeader) => Promise<AxiosResponse<T>>
|
||||||
|
): Promise<T> {
|
||||||
|
const authHeader = await useFirebaseAuthStore().getAuthHeader()
|
||||||
|
if (!authHeader) {
|
||||||
|
throw new WorkspaceApiError(
|
||||||
|
t('toastMessages.userNotAuthenticated'),
|
||||||
|
401,
|
||||||
|
'NOT_AUTHENTICATED'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await request(authHeader)
|
||||||
|
return response.data
|
||||||
|
} catch (err) {
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
const status = err.response?.status
|
||||||
|
const message = err.response?.data?.message ?? err.message
|
||||||
|
throw new WorkspaceApiError(message, status)
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const workspaceApi = {
|
||||||
|
/**
|
||||||
|
* List all workspaces the user has access to
|
||||||
|
* GET /api/workspaces
|
||||||
|
*/
|
||||||
|
list: (): Promise<ListWorkspacesResponse> =>
|
||||||
|
withAuth((headers) => workspaceApiClient.get('/workspaces', { headers })),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get workspace details including members and invites
|
||||||
|
* GET /api/workspaces/:id
|
||||||
|
*/
|
||||||
|
get: (workspaceId: string): Promise<WorkspaceDetails> =>
|
||||||
|
withAuth((headers) =>
|
||||||
|
workspaceApiClient.get(`/workspaces/${workspaceId}`, { headers })
|
||||||
|
),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new workspace
|
||||||
|
* POST /api/workspaces
|
||||||
|
*/
|
||||||
|
create: (payload: CreateWorkspacePayload): Promise<WorkspaceWithRole> =>
|
||||||
|
withAuth((headers) =>
|
||||||
|
workspaceApiClient.post('/workspaces', payload, { headers })
|
||||||
|
),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update workspace name
|
||||||
|
* PATCH /api/workspaces/:id
|
||||||
|
*/
|
||||||
|
update: (
|
||||||
|
workspaceId: string,
|
||||||
|
payload: UpdateWorkspacePayload
|
||||||
|
): Promise<WorkspaceWithRole> =>
|
||||||
|
withAuth((headers) =>
|
||||||
|
workspaceApiClient.patch(`/workspaces/${workspaceId}`, payload, {
|
||||||
|
headers
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a workspace (owner only)
|
||||||
|
* DELETE /api/workspaces/:id
|
||||||
|
*/
|
||||||
|
delete: (workspaceId: string): Promise<void> =>
|
||||||
|
withAuth((headers) =>
|
||||||
|
workspaceApiClient.delete(`/workspaces/${workspaceId}`, { headers })
|
||||||
|
),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leave a workspace (member only)
|
||||||
|
* POST /api/workspaces/:id/leave
|
||||||
|
*/
|
||||||
|
leave: (workspaceId: string): Promise<void> =>
|
||||||
|
withAuth((headers) =>
|
||||||
|
workspaceApiClient.post(`/workspaces/${workspaceId}/leave`, null, {
|
||||||
|
headers
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an invite link for a workspace
|
||||||
|
* POST /api/workspaces/:id/invites
|
||||||
|
*/
|
||||||
|
createInvite: (
|
||||||
|
workspaceId: string,
|
||||||
|
payload: CreateInvitePayload
|
||||||
|
): Promise<CreateInviteResponse> =>
|
||||||
|
withAuth((headers) =>
|
||||||
|
workspaceApiClient.post(`/workspaces/${workspaceId}/invites`, payload, {
|
||||||
|
headers
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke a pending invite
|
||||||
|
* DELETE /api/workspaces/:id/invites/:inviteId
|
||||||
|
*/
|
||||||
|
revokeInvite: (workspaceId: string, inviteId: string): Promise<void> =>
|
||||||
|
withAuth((headers) =>
|
||||||
|
workspaceApiClient.delete(
|
||||||
|
`/workspaces/${workspaceId}/invites/${inviteId}`,
|
||||||
|
{ headers }
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a member from workspace
|
||||||
|
* DELETE /api/workspaces/:id/members/:memberId
|
||||||
|
*/
|
||||||
|
removeMember: (workspaceId: string, memberId: string): Promise<void> =>
|
||||||
|
withAuth((headers) =>
|
||||||
|
workspaceApiClient.delete(
|
||||||
|
`/workspaces/${workspaceId}/members/${memberId}`,
|
||||||
|
{ headers }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,16 +1,26 @@
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref, shallowRef } from 'vue'
|
||||||
|
import { createSharedComposable } from '@vueuse/core'
|
||||||
|
|
||||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||||
|
import type {
|
||||||
|
WorkspaceRole,
|
||||||
|
WorkspaceType,
|
||||||
|
WorkspaceWithRole
|
||||||
|
} from '../api/workspaceApi'
|
||||||
|
|
||||||
export type WorkspaceRole = 'PERSONAL' | 'MEMBER' | 'OWNER'
|
// Re-export API types for consumers
|
||||||
|
export type { WorkspaceRole, WorkspaceType, WorkspaceWithRole }
|
||||||
|
|
||||||
|
// Extended member type for UI (adds joinDate as Date)
|
||||||
export interface WorkspaceMember {
|
export interface WorkspaceMember {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
email: string
|
email: string
|
||||||
|
role?: WorkspaceRole
|
||||||
joinDate: Date
|
joinDate: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extended invite type for UI (adds dates as Date objects)
|
||||||
export interface PendingInvite {
|
export interface PendingInvite {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@@ -20,20 +30,24 @@ export interface PendingInvite {
|
|||||||
inviteLink: string
|
inviteLink: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WorkspaceMockData {
|
type SubscriptionPlan = 'PRO_MONTHLY' | 'PRO_YEARLY' | null
|
||||||
id: string | null
|
|
||||||
name: string
|
interface WorkspaceState extends WorkspaceWithRole {
|
||||||
role: WorkspaceRole
|
isSubscribed: boolean
|
||||||
|
subscriptionPlan: SubscriptionPlan
|
||||||
|
members: WorkspaceMember[]
|
||||||
|
pendingInvites: PendingInvite[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AvailableWorkspace {
|
export interface AvailableWorkspace {
|
||||||
id: string | null
|
id: string | null
|
||||||
name: string
|
name: string
|
||||||
|
type: WorkspaceType
|
||||||
role: WorkspaceRole
|
role: WorkspaceRole
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Permission flags for workspace actions */
|
/** Permission flags for workspace actions */
|
||||||
export interface WorkspacePermissions {
|
interface WorkspacePermissions {
|
||||||
canViewOtherMembers: boolean
|
canViewOtherMembers: boolean
|
||||||
canViewPendingInvites: boolean
|
canViewPendingInvites: boolean
|
||||||
canInviteMembers: boolean
|
canInviteMembers: boolean
|
||||||
@@ -45,7 +59,7 @@ export interface WorkspacePermissions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** UI configuration for workspace role */
|
/** UI configuration for workspace role */
|
||||||
export interface WorkspaceUIConfig {
|
interface WorkspaceUIConfig {
|
||||||
showMembersList: boolean
|
showMembersList: boolean
|
||||||
showPendingTab: boolean
|
showPendingTab: boolean
|
||||||
showSearch: boolean
|
showSearch: boolean
|
||||||
@@ -54,22 +68,45 @@ export interface WorkspaceUIConfig {
|
|||||||
membersGridCols: string
|
membersGridCols: string
|
||||||
pendingGridCols: string
|
pendingGridCols: string
|
||||||
headerGridCols: string
|
headerGridCols: string
|
||||||
|
showEditWorkspaceMenuItem: boolean
|
||||||
workspaceMenuAction: 'leave' | 'delete' | null
|
workspaceMenuAction: 'leave' | 'delete' | null
|
||||||
workspaceMenuDisabledTooltip: string | null
|
workspaceMenuDisabledTooltip: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const ROLE_PERMISSIONS: Record<WorkspaceRole, WorkspacePermissions> = {
|
// Role-based permissions mapping
|
||||||
PERSONAL: {
|
// Note: 'personal' type workspaces have owner role with restricted permissions
|
||||||
canViewOtherMembers: false,
|
function getPermissions(
|
||||||
canViewPendingInvites: false,
|
type: WorkspaceType,
|
||||||
canInviteMembers: false,
|
role: WorkspaceRole
|
||||||
canManageInvites: false,
|
): WorkspacePermissions {
|
||||||
canRemoveMembers: false,
|
if (type === 'personal') {
|
||||||
canLeaveWorkspace: false,
|
return {
|
||||||
canAccessWorkspaceMenu: false,
|
canViewOtherMembers: false,
|
||||||
canManageSubscription: true
|
canViewPendingInvites: false,
|
||||||
},
|
canInviteMembers: false,
|
||||||
MEMBER: {
|
canManageInvites: false,
|
||||||
|
canRemoveMembers: false,
|
||||||
|
canLeaveWorkspace: false,
|
||||||
|
canAccessWorkspaceMenu: true,
|
||||||
|
canManageSubscription: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role === 'owner') {
|
||||||
|
return {
|
||||||
|
canViewOtherMembers: true,
|
||||||
|
canViewPendingInvites: true,
|
||||||
|
canInviteMembers: true,
|
||||||
|
canManageInvites: true,
|
||||||
|
canRemoveMembers: true,
|
||||||
|
canLeaveWorkspace: true,
|
||||||
|
canAccessWorkspaceMenu: true,
|
||||||
|
canManageSubscription: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// member role
|
||||||
|
return {
|
||||||
canViewOtherMembers: true,
|
canViewOtherMembers: true,
|
||||||
canViewPendingInvites: false,
|
canViewPendingInvites: false,
|
||||||
canInviteMembers: false,
|
canInviteMembers: false,
|
||||||
@@ -78,33 +115,48 @@ const ROLE_PERMISSIONS: Record<WorkspaceRole, WorkspacePermissions> = {
|
|||||||
canLeaveWorkspace: true,
|
canLeaveWorkspace: true,
|
||||||
canAccessWorkspaceMenu: true,
|
canAccessWorkspaceMenu: true,
|
||||||
canManageSubscription: false
|
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> = {
|
function getUIConfig(
|
||||||
PERSONAL: {
|
type: WorkspaceType,
|
||||||
showMembersList: false,
|
role: WorkspaceRole
|
||||||
showPendingTab: false,
|
): WorkspaceUIConfig {
|
||||||
showSearch: false,
|
if (type === 'personal') {
|
||||||
showDateColumn: false,
|
return {
|
||||||
showRoleBadge: false,
|
showMembersList: false,
|
||||||
membersGridCols: 'grid-cols-1',
|
showPendingTab: false,
|
||||||
pendingGridCols: 'grid-cols-[50%_20%_20%_10%]',
|
showSearch: false,
|
||||||
headerGridCols: 'grid-cols-1',
|
showDateColumn: false,
|
||||||
workspaceMenuAction: null,
|
showRoleBadge: false,
|
||||||
workspaceMenuDisabledTooltip: null
|
membersGridCols: 'grid-cols-1',
|
||||||
},
|
pendingGridCols: 'grid-cols-[50%_20%_20%_10%]',
|
||||||
MEMBER: {
|
headerGridCols: 'grid-cols-1',
|
||||||
|
showEditWorkspaceMenuItem: true,
|
||||||
|
workspaceMenuAction: null,
|
||||||
|
workspaceMenuDisabledTooltip: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role === 'owner') {
|
||||||
|
return {
|
||||||
|
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%]',
|
||||||
|
showEditWorkspaceMenuItem: true,
|
||||||
|
workspaceMenuAction: 'delete',
|
||||||
|
workspaceMenuDisabledTooltip:
|
||||||
|
'workspacePanel.menu.deleteWorkspaceDisabledTooltip'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// member role
|
||||||
|
return {
|
||||||
showMembersList: true,
|
showMembersList: true,
|
||||||
showPendingTab: false,
|
showPendingTab: false,
|
||||||
showSearch: true,
|
showSearch: true,
|
||||||
@@ -113,248 +165,308 @@ const ROLE_UI_CONFIG: Record<WorkspaceRole, WorkspaceUIConfig> = {
|
|||||||
membersGridCols: 'grid-cols-[1fr_auto]',
|
membersGridCols: 'grid-cols-[1fr_auto]',
|
||||||
pendingGridCols: 'grid-cols-[50%_20%_20%_10%]',
|
pendingGridCols: 'grid-cols-[50%_20%_20%_10%]',
|
||||||
headerGridCols: 'grid-cols-[1fr_auto]',
|
headerGridCols: 'grid-cols-[1fr_auto]',
|
||||||
|
showEditWorkspaceMenuItem: false,
|
||||||
workspaceMenuAction: 'leave',
|
workspaceMenuAction: 'leave',
|
||||||
workspaceMenuDisabledTooltip: null
|
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 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
|
const MAX_WORKSPACE_MEMBERS = 50
|
||||||
|
|
||||||
// Shared state for workspace
|
function generateId(): string {
|
||||||
const _workspaceId = ref<string | null>(null)
|
return Math.random().toString(36).substring(2, 10)
|
||||||
const _workspaceName = ref<string>('Personal workspace')
|
}
|
||||||
const _workspaceRole = ref<WorkspaceRole>('PERSONAL')
|
|
||||||
const _isWorkspaceSubscribed = ref<boolean>(true)
|
function createPersonalWorkspace(): WorkspaceState {
|
||||||
|
return {
|
||||||
|
id: 'personal',
|
||||||
|
name: 'Personal workspace',
|
||||||
|
type: 'personal',
|
||||||
|
role: 'owner',
|
||||||
|
isSubscribed: true,
|
||||||
|
subscriptionPlan: null,
|
||||||
|
members: [],
|
||||||
|
pendingInvites: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// MODULE-LEVEL STATE
|
||||||
|
// Persists across component lifecycle - not disposed when components unmount
|
||||||
|
// =============================================================================
|
||||||
|
const _workspaces = shallowRef<WorkspaceState[]>([createPersonalWorkspace()])
|
||||||
|
const _currentWorkspaceIndex = ref(0)
|
||||||
const _activeTab = ref<string>('plan')
|
const _activeTab = ref<string>('plan')
|
||||||
const _members = ref<WorkspaceMember[]>([])
|
|
||||||
const _pendingInvites = ref<PendingInvite[]>([])
|
|
||||||
const _availableWorkspaces = ref<AvailableWorkspace[]>(
|
|
||||||
MOCK_AVAILABLE_WORKSPACES
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
// Helper to get current workspace
|
||||||
* Set workspace mock state for testing UI
|
function getCurrentWorkspace(): WorkspaceState {
|
||||||
* Usage in browser console: window.__setWorkspaceRole('OWNER')
|
return _workspaces.value[_currentWorkspaceIndex.value]
|
||||||
*/
|
}
|
||||||
function setMockRole(role: WorkspaceRole) {
|
|
||||||
const data = MOCK_DATA[role]
|
// Helper to update current workspace immutably
|
||||||
_workspaceId.value = data.id
|
function updateCurrentWorkspace(updates: Partial<WorkspaceState>) {
|
||||||
_workspaceName.value = data.name
|
const index = _currentWorkspaceIndex.value
|
||||||
_workspaceRole.value = data.role
|
const updated = { ..._workspaces.value[index], ...updates }
|
||||||
|
_workspaces.value = [
|
||||||
|
..._workspaces.value.slice(0, index),
|
||||||
|
updated,
|
||||||
|
..._workspaces.value.slice(index + 1)
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set workspace subscription state for testing UI
|
* Internal composable implementation for workspace management.
|
||||||
* Usage in browser console: window.__setWorkspaceSubscribed(false)
|
* Uses module-level state to persist across component lifecycle.
|
||||||
|
* Will integrate with useWorkspaceAuth once that PR lands.
|
||||||
*/
|
*/
|
||||||
function setMockSubscribed(subscribed: boolean) {
|
function useWorkspaceInternal() {
|
||||||
_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 { userDisplayName, userEmail } = useCurrentUser()
|
||||||
|
|
||||||
const workspaceId = computed(() => _workspaceId.value)
|
// Computed properties derived from module-level state
|
||||||
const workspaceName = computed(() => _workspaceName.value)
|
const currentWorkspace = computed(() => getCurrentWorkspace())
|
||||||
const workspaceRole = computed(() => _workspaceRole.value)
|
const workspaceId = computed(() => currentWorkspace.value?.id ?? null)
|
||||||
|
const workspaceName = computed(
|
||||||
|
() => currentWorkspace.value?.name ?? 'Personal workspace'
|
||||||
|
)
|
||||||
|
const workspaceType = computed(
|
||||||
|
() => currentWorkspace.value?.type ?? 'personal'
|
||||||
|
)
|
||||||
|
const workspaceRole = computed(() => currentWorkspace.value?.role ?? 'owner')
|
||||||
const activeTab = computed(() => _activeTab.value)
|
const activeTab = computed(() => _activeTab.value)
|
||||||
|
|
||||||
const isPersonalWorkspace = computed(
|
const isPersonalWorkspace = computed(
|
||||||
() => _workspaceRole.value === 'PERSONAL'
|
() => currentWorkspace.value?.type === 'personal'
|
||||||
)
|
)
|
||||||
|
|
||||||
const isWorkspaceSubscribed = computed(() => _isWorkspaceSubscribed.value)
|
const isWorkspaceSubscribed = computed(
|
||||||
|
() => currentWorkspace.value?.isSubscribed ?? false
|
||||||
const permissions = computed<WorkspacePermissions>(
|
|
||||||
() => ROLE_PERMISSIONS[_workspaceRole.value]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const uiConfig = computed<WorkspaceUIConfig>(
|
const subscriptionPlan = computed(
|
||||||
() => ROLE_UI_CONFIG[_workspaceRole.value]
|
() => currentWorkspace.value?.subscriptionPlan ?? null
|
||||||
)
|
)
|
||||||
|
|
||||||
function setActiveTab(tab: string | number) {
|
const permissions = computed<WorkspacePermissions>(() =>
|
||||||
_activeTab.value = String(tab)
|
getPermissions(workspaceType.value, workspaceRole.value)
|
||||||
}
|
|
||||||
|
|
||||||
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
|
const uiConfig = computed<WorkspaceUIConfig>(() =>
|
||||||
async function fetchMembers(): Promise<WorkspaceMember[]> {
|
getUIConfig(workspaceType.value, workspaceRole.value)
|
||||||
if (_workspaceRole.value === 'PERSONAL') {
|
)
|
||||||
_members.value = [
|
|
||||||
|
// For personal workspace, always show current user as the only member
|
||||||
|
const members = computed<WorkspaceMember[]>(() => {
|
||||||
|
if (isPersonalWorkspace.value) {
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
id: 'current-user',
|
id: 'current-user',
|
||||||
name: userDisplayName.value ?? 'You',
|
name: userDisplayName.value ?? 'You',
|
||||||
email: userEmail.value ?? '',
|
email: userEmail.value ?? '',
|
||||||
|
role: 'owner',
|
||||||
joinDate: new Date()
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
const availableWorkspaces = computed<AvailableWorkspace[]>(() =>
|
||||||
|
_workspaces.value.map((w) => ({
|
||||||
|
id: w.id,
|
||||||
|
name: w.name,
|
||||||
|
type: w.type,
|
||||||
|
role: w.role
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const ownedWorkspacesCount = computed(
|
||||||
|
() => _workspaces.value.filter((w) => w.role === 'owner').length
|
||||||
|
)
|
||||||
|
|
||||||
|
const canCreateWorkspace = computed(
|
||||||
|
() => ownedWorkspacesCount.value < MAX_OWNED_WORKSPACES
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tab management
|
||||||
|
function setActiveTab(tab: string | number) {
|
||||||
|
_activeTab.value = String(tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to a different workspace by ID.
|
||||||
|
* TODO: Integrate with useWorkspaceAuth.switchWorkspace() when PR lands
|
||||||
|
*/
|
||||||
|
function switchWorkspace(workspace: AvailableWorkspace) {
|
||||||
|
const index = _workspaces.value.findIndex((w) => w.id === workspace.id)
|
||||||
|
if (index !== -1) {
|
||||||
|
_currentWorkspaceIndex.value = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new workspace.
|
||||||
|
* TODO: Replace with workspaceApi.create() call
|
||||||
|
*/
|
||||||
|
function createWorkspace(name: string): WorkspaceState {
|
||||||
|
const newWorkspace: WorkspaceState = {
|
||||||
|
id: `workspace-${generateId()}`,
|
||||||
|
name,
|
||||||
|
type: 'team',
|
||||||
|
role: 'owner',
|
||||||
|
isSubscribed: false,
|
||||||
|
subscriptionPlan: null,
|
||||||
|
members: [],
|
||||||
|
pendingInvites: [
|
||||||
|
// Stub invite for testing
|
||||||
|
{
|
||||||
|
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]
|
||||||
|
_currentWorkspaceIndex.value = _workspaces.value.length - 1
|
||||||
|
|
||||||
|
return newWorkspace
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe the current workspace to a plan.
|
||||||
|
* TODO: Replace with subscription API call
|
||||||
|
*/
|
||||||
|
function subscribeWorkspace(plan: SubscriptionPlan = 'PRO_MONTHLY') {
|
||||||
|
updateCurrentWorkspace({
|
||||||
|
isSubscribed: true,
|
||||||
|
subscriptionPlan: plan
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the current workspace (owner only).
|
||||||
|
* TODO: Replace with workspaceApi.delete() call
|
||||||
|
*/
|
||||||
|
function deleteWorkspace() {
|
||||||
|
const current = getCurrentWorkspace()
|
||||||
|
if (current.role === 'owner' && current.type === 'team') {
|
||||||
|
_workspaces.value = _workspaces.value.filter((w) => w.id !== current.id)
|
||||||
|
_currentWorkspaceIndex.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leave the current workspace (member only).
|
||||||
|
* TODO: Replace with workspaceApi.leave() call
|
||||||
|
*/
|
||||||
|
function leaveWorkspace() {
|
||||||
|
const current = getCurrentWorkspace()
|
||||||
|
if (current.role === 'member') {
|
||||||
|
_workspaces.value = _workspaces.value.filter((w) => w.id !== current.id)
|
||||||
|
_currentWorkspaceIndex.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update workspace name.
|
||||||
|
* TODO: Replace with workspaceApi.update() call
|
||||||
|
*/
|
||||||
|
function updateWorkspaceName(name: string) {
|
||||||
|
updateCurrentWorkspace({ name })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a member to the current workspace.
|
||||||
|
* TODO: This happens via invite acceptance on backend
|
||||||
|
*/
|
||||||
|
function addMember(member: Omit<WorkspaceMember, 'id' | 'joinDate'>) {
|
||||||
|
const current = getCurrentWorkspace()
|
||||||
|
const newMember: WorkspaceMember = {
|
||||||
|
...member,
|
||||||
|
id: generateId(),
|
||||||
|
joinDate: new Date()
|
||||||
|
}
|
||||||
|
updateCurrentWorkspace({
|
||||||
|
members: [...current.members, newMember]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a member from the current workspace.
|
||||||
|
* TODO: Replace with workspaceApi.removeMember() call
|
||||||
|
*/
|
||||||
|
function removeMember(memberId: string) {
|
||||||
|
const current = getCurrentWorkspace()
|
||||||
|
updateCurrentWorkspace({
|
||||||
|
members: current.members.filter((m) => m.id !== memberId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke a pending invite.
|
||||||
|
* TODO: Replace with workspaceApi.revokeInvite() call
|
||||||
|
*/
|
||||||
|
function revokeInvite(inviteId: string) {
|
||||||
|
const current = getCurrentWorkspace()
|
||||||
|
updateCurrentWorkspace({
|
||||||
|
pendingInvites: current.pendingInvites.filter((i) => i.id !== inviteId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accept a pending invite (for demo/testing).
|
||||||
|
*/
|
||||||
|
function acceptInvite(inviteId: string) {
|
||||||
|
const current = getCurrentWorkspace()
|
||||||
|
const invite = current.pendingInvites.find((i) => i.id === inviteId)
|
||||||
|
if (invite) {
|
||||||
|
const updatedPending = current.pendingInvites.filter(
|
||||||
|
(i) => i.id !== inviteId
|
||||||
|
)
|
||||||
|
const newMember: WorkspaceMember = {
|
||||||
|
id: generateId(),
|
||||||
|
name: invite.name,
|
||||||
|
email: invite.email,
|
||||||
|
role: 'member',
|
||||||
|
joinDate: new Date()
|
||||||
|
}
|
||||||
|
updateCurrentWorkspace({
|
||||||
|
pendingInvites: updatedPending,
|
||||||
|
members: [...current.members, newMember]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Async API methods (stubs for now)
|
||||||
|
|
||||||
|
async function fetchMembers(): Promise<WorkspaceMember[]> {
|
||||||
|
// TODO: Replace with workspaceApi.get() call
|
||||||
|
return members.value
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchPendingInvites(): Promise<PendingInvite[]> {
|
async function fetchPendingInvites(): Promise<PendingInvite[]> {
|
||||||
if (_workspaceRole.value === 'PERSONAL') {
|
// TODO: Replace with workspaceApi.get() call
|
||||||
_pendingInvites.value = []
|
return 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> {
|
async function copyInviteLink(inviteId: string): Promise<string> {
|
||||||
const invite = _pendingInvites.value.find((i) => i.id === inviteId)
|
const invite = pendingInvites.value.find((i) => i.id === inviteId)
|
||||||
if (invite) {
|
if (invite) {
|
||||||
await navigator.clipboard.writeText(invite.inviteLink)
|
await navigator.clipboard.writeText(invite.inviteLink)
|
||||||
return invite.inviteLink
|
return invite.inviteLink
|
||||||
@@ -363,54 +475,101 @@ export function useWorkspace() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an invite link for a given email
|
* Copy invite link and simulate member accepting (for demo).
|
||||||
* TODO: Replace with actual API call
|
*/
|
||||||
|
async function copyInviteLinkAndAccept(inviteId: string): Promise<string> {
|
||||||
|
const invite = pendingInvites.value.find((i) => i.id === inviteId)
|
||||||
|
if (invite) {
|
||||||
|
await navigator.clipboard.writeText(invite.inviteLink)
|
||||||
|
revokeInvite(inviteId)
|
||||||
|
addMember({
|
||||||
|
name: invite.name,
|
||||||
|
email: invite.email,
|
||||||
|
role: 'member'
|
||||||
|
})
|
||||||
|
return invite.inviteLink
|
||||||
|
}
|
||||||
|
throw new Error('Invite not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an invite link for a given email.
|
||||||
|
* TODO: Replace with workspaceApi.createInvite() call
|
||||||
*/
|
*/
|
||||||
async function createInviteLink(email: string): Promise<string> {
|
async function createInviteLink(email: string): Promise<string> {
|
||||||
// Simulate API delay
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
||||||
|
|
||||||
// Generate mock invite link
|
const inviteId = generateId()
|
||||||
const inviteId = Math.random().toString(36).substring(2, 10)
|
const inviteLink = `https://cloud.comfy.org/workspace/invite/${inviteId}`
|
||||||
const inviteLink = `https://cloud.comfy.org/workspace?3423532/invite/hi789jkl012mno345pq`
|
|
||||||
|
|
||||||
// Add to pending invites (mock)
|
const current = getCurrentWorkspace()
|
||||||
const newInvite: PendingInvite = {
|
const newInvite: PendingInvite = {
|
||||||
id: inviteId,
|
id: inviteId,
|
||||||
name: email.split('@')[0],
|
name: email.split('@')[0],
|
||||||
email,
|
email,
|
||||||
inviteDate: new Date(),
|
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
|
inviteLink
|
||||||
}
|
}
|
||||||
_pendingInvites.value = [..._pendingInvites.value, newInvite]
|
updateCurrentWorkspace({
|
||||||
|
pendingInvites: [...current.pendingInvites, newInvite]
|
||||||
|
})
|
||||||
|
|
||||||
return inviteLink
|
return inviteLink
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableWorkspaces = computed(() => _availableWorkspaces.value)
|
// Dev helpers for testing UI states
|
||||||
const ownedWorkspacesCount = computed(
|
function setMockRole(role: WorkspaceRole) {
|
||||||
() => _availableWorkspaces.value.filter((w) => w.role === 'OWNER').length
|
updateCurrentWorkspace({ role })
|
||||||
)
|
}
|
||||||
const canCreateWorkspace = computed(
|
|
||||||
() => ownedWorkspacesCount.value < MAX_OWNED_WORKSPACES
|
function setMockSubscribed(subscribed: boolean) {
|
||||||
)
|
updateCurrentWorkspace({ isSubscribed: subscribed })
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMockType(type: WorkspaceType) {
|
||||||
|
updateCurrentWorkspace({ type })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose to window for dev testing
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const w = window as Window & {
|
||||||
|
__setWorkspaceRole?: typeof setMockRole
|
||||||
|
__setWorkspaceSubscribed?: typeof setMockSubscribed
|
||||||
|
__setWorkspaceType?: typeof setMockType
|
||||||
|
}
|
||||||
|
w.__setWorkspaceRole = setMockRole
|
||||||
|
w.__setWorkspaceSubscribed = setMockSubscribed
|
||||||
|
w.__setWorkspaceType = setMockType
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
// Current workspace state
|
||||||
workspaceId,
|
workspaceId,
|
||||||
workspaceName,
|
workspaceName,
|
||||||
|
workspaceType,
|
||||||
workspaceRole,
|
workspaceRole,
|
||||||
activeTab,
|
activeTab,
|
||||||
isPersonalWorkspace,
|
isPersonalWorkspace,
|
||||||
isWorkspaceSubscribed,
|
isWorkspaceSubscribed,
|
||||||
|
subscriptionPlan,
|
||||||
permissions,
|
permissions,
|
||||||
uiConfig,
|
uiConfig,
|
||||||
|
|
||||||
|
// Tab management
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
// Workspace switching
|
|
||||||
|
// Workspace switching/management
|
||||||
availableWorkspaces,
|
availableWorkspaces,
|
||||||
ownedWorkspacesCount,
|
ownedWorkspacesCount,
|
||||||
canCreateWorkspace,
|
canCreateWorkspace,
|
||||||
switchWorkspace,
|
switchWorkspace,
|
||||||
|
createWorkspace,
|
||||||
|
subscribeWorkspace,
|
||||||
|
deleteWorkspace,
|
||||||
|
leaveWorkspace,
|
||||||
|
updateWorkspaceName,
|
||||||
|
|
||||||
// Members
|
// Members
|
||||||
members,
|
members,
|
||||||
pendingInvites,
|
pendingInvites,
|
||||||
@@ -419,10 +578,28 @@ export function useWorkspace() {
|
|||||||
fetchMembers,
|
fetchMembers,
|
||||||
fetchPendingInvites,
|
fetchPendingInvites,
|
||||||
revokeInvite,
|
revokeInvite,
|
||||||
|
acceptInvite,
|
||||||
copyInviteLink,
|
copyInviteLink,
|
||||||
|
copyInviteLinkAndAccept,
|
||||||
createInviteLink,
|
createInviteLink,
|
||||||
|
addMember,
|
||||||
|
removeMember,
|
||||||
|
|
||||||
// Dev helpers
|
// Dev helpers
|
||||||
setMockRole,
|
setMockRole,
|
||||||
setMockSubscribed
|
setMockSubscribed,
|
||||||
|
setMockType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared composable for workspace management.
|
||||||
|
* Uses module-level state to persist across component lifecycle.
|
||||||
|
* The createSharedComposable wrapper ensures computed properties
|
||||||
|
* are shared efficiently across components.
|
||||||
|
*
|
||||||
|
* Future integration:
|
||||||
|
* - Will consume useWorkspaceAuth for authentication context
|
||||||
|
* - Will use workspaceApi for backend calls
|
||||||
|
*/
|
||||||
|
export const useWorkspace = createSharedComposable(useWorkspaceInternal)
|
||||||
|
|||||||
@@ -609,7 +609,7 @@ export const useDialogService = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showCreateWorkspaceDialog(
|
function showCreateWorkspaceDialog(
|
||||||
onConfirm: (name: string) => void | Promise<void>
|
onConfirm?: (name: string) => void | Promise<void>
|
||||||
) {
|
) {
|
||||||
return dialogStore.showDialog({
|
return dialogStore.showDialog({
|
||||||
key: 'create-workspace',
|
key: 'create-workspace',
|
||||||
|
|||||||
Reference in New Issue
Block a user