mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 18:22:40 +00:00
feat: workspace switcher and misc
This commit is contained in:
@@ -1,42 +1,73 @@
|
||||
<template>
|
||||
<div class="flex w-80 flex-col overflow-hidden rounded-lg">
|
||||
<div class="flex flex-col overflow-y-auto">
|
||||
<template
|
||||
v-for="workspace in availableWorkspaces"
|
||||
:key="workspace.id ?? 'personal'"
|
||||
>
|
||||
<div class="border-b border-border-default p-2">
|
||||
<button
|
||||
:class="
|
||||
cn(
|
||||
'flex h-[54px] w-full cursor-pointer items-center gap-2 rounded px-2 py-4 border-none bg-transparent',
|
||||
'hover:bg-secondary-background-hover',
|
||||
isCurrentWorkspace(workspace) && 'bg-secondary-background'
|
||||
)
|
||||
"
|
||||
@click="handleSelectWorkspace(workspace)"
|
||||
>
|
||||
<WorkspaceProfilePic
|
||||
class="size-8 text-sm"
|
||||
:workspace-name="workspace.name"
|
||||
/>
|
||||
<div class="flex min-w-0 flex-1 flex-col items-start gap-1">
|
||||
<span class="text-sm text-base-foreground">
|
||||
{{ workspace.name }}
|
||||
</span>
|
||||
<span
|
||||
v-if="workspace.type !== 'personal'"
|
||||
class="text-sm text-muted-foreground"
|
||||
>
|
||||
{{ getRoleLabel(workspace.role) }}
|
||||
</span>
|
||||
</div>
|
||||
<i
|
||||
v-if="isCurrentWorkspace(workspace)"
|
||||
class="pi pi-check text-sm text-base-foreground"
|
||||
/>
|
||||
</button>
|
||||
<!-- Loading state -->
|
||||
<div v-if="isFetchingWorkspaces" class="flex flex-col gap-2 p-2">
|
||||
<div
|
||||
v-for="i in 2"
|
||||
:key="i"
|
||||
class="flex h-[54px] animate-pulse items-center gap-2 rounded px-2 py-4"
|
||||
>
|
||||
<div class="size-8 rounded-full bg-secondary-background" />
|
||||
<div class="flex flex-1 flex-col gap-1">
|
||||
<div class="h-4 w-24 rounded bg-secondary-background" />
|
||||
<div class="h-3 w-16 rounded bg-secondary-background" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Workspace list -->
|
||||
<template v-else>
|
||||
<template
|
||||
v-for="workspace in availableWorkspaces"
|
||||
:key="workspace.id ?? 'personal'"
|
||||
>
|
||||
<div class="border-b border-border-default p-2">
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'group flex h-[54px] w-full items-center gap-2 rounded px-2 py-4',
|
||||
'hover:bg-secondary-background-hover',
|
||||
isCurrentWorkspace(workspace) && 'bg-secondary-background'
|
||||
)
|
||||
"
|
||||
>
|
||||
<button
|
||||
class="flex flex-1 cursor-pointer items-center gap-2 border-none bg-transparent p-0"
|
||||
@click="handleSelectWorkspace(workspace)"
|
||||
>
|
||||
<WorkspaceProfilePic
|
||||
class="size-8 text-sm"
|
||||
:workspace-name="workspace.name"
|
||||
/>
|
||||
<div class="flex min-w-0 flex-1 flex-col items-start gap-1">
|
||||
<span class="text-sm text-base-foreground">
|
||||
{{ workspace.name }}
|
||||
</span>
|
||||
<span
|
||||
v-if="workspace.type !== 'personal'"
|
||||
class="text-sm text-muted-foreground"
|
||||
>
|
||||
{{ getRoleLabel(workspace.role) }}
|
||||
</span>
|
||||
</div>
|
||||
<i
|
||||
v-if="isCurrentWorkspace(workspace)"
|
||||
class="pi pi-check text-sm text-base-foreground"
|
||||
/>
|
||||
</button>
|
||||
<!-- Delete button - only for team workspaces where user is owner -->
|
||||
<button
|
||||
v-if="canDeleteWorkspace(workspace)"
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded border-none bg-transparent text-muted-foreground opacity-0 transition-opacity hover:bg-error-background hover:text-error-foreground group-hover:opacity-100"
|
||||
:title="$t('g.delete')"
|
||||
@click.stop="handleDeleteWorkspace(workspace)"
|
||||
>
|
||||
<i class="pi pi-trash text-xs" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- <Divider class="mx-0 my-0" /> -->
|
||||
@@ -82,25 +113,51 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import WorkspaceProfilePic from '@/components/common/WorkspaceProfilePic.vue'
|
||||
import type { AvailableWorkspace } from '@/platform/workspace/composables/useWorkspace'
|
||||
import { useWorkspace } from '@/platform/workspace/composables/useWorkspace'
|
||||
import { useWorkspaceSwitch } from '@/platform/auth/workspace/useWorkspaceSwitch'
|
||||
import type {
|
||||
WorkspaceRole,
|
||||
WorkspaceType
|
||||
} from '@/platform/workspace/api/workspaceApi'
|
||||
import { useWorkspaceStore } from '@/platform/workspace/stores/workspaceStore'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
interface AvailableWorkspace {
|
||||
id: string
|
||||
name: string
|
||||
type: WorkspaceType
|
||||
role: WorkspaceRole
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [workspace: AvailableWorkspace]
|
||||
create: []
|
||||
delete: [workspace: AvailableWorkspace]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const {
|
||||
workspaceId,
|
||||
availableWorkspaces,
|
||||
canCreateWorkspace,
|
||||
switchWorkspace
|
||||
} = useWorkspace()
|
||||
const { switchWithConfirmation } = useWorkspaceSwitch()
|
||||
const { showDeleteWorkspaceDialog } = useDialogService()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const { workspaceId, workspaces, canCreateWorkspace, isFetchingWorkspaces } =
|
||||
storeToRefs(workspaceStore)
|
||||
|
||||
const availableWorkspaces = computed<AvailableWorkspace[]>(() =>
|
||||
workspaces.value.map((w) => ({
|
||||
id: w.id,
|
||||
name: w.name,
|
||||
type: w.type,
|
||||
role: w.role
|
||||
}))
|
||||
)
|
||||
|
||||
// Workspace store is initialized in router.ts before the app loads
|
||||
// This component just displays the already-loaded workspace data
|
||||
|
||||
function isCurrentWorkspace(workspace: AvailableWorkspace): boolean {
|
||||
return workspace.id === workspaceId.value
|
||||
@@ -112,12 +169,32 @@ function getRoleLabel(role: AvailableWorkspace['role']): string {
|
||||
return ''
|
||||
}
|
||||
|
||||
function handleSelectWorkspace(workspace: AvailableWorkspace) {
|
||||
switchWorkspace(workspace)
|
||||
emit('select', workspace)
|
||||
async function handleSelectWorkspace(workspace: AvailableWorkspace) {
|
||||
if (!workspace.id) {
|
||||
// Personal workspace doesn't have an ID in this context
|
||||
return
|
||||
}
|
||||
const success = await switchWithConfirmation(workspace.id)
|
||||
if (success) {
|
||||
emit('select', workspace)
|
||||
}
|
||||
}
|
||||
|
||||
function handleCreateWorkspace() {
|
||||
emit('create')
|
||||
}
|
||||
|
||||
function canDeleteWorkspace(workspace: AvailableWorkspace): boolean {
|
||||
// Can only delete team workspaces where user is owner
|
||||
return workspace.type === 'team' && workspace.role === 'owner'
|
||||
}
|
||||
|
||||
function handleDeleteWorkspace(workspace: AvailableWorkspace) {
|
||||
if (!workspace.id) return
|
||||
showDeleteWorkspaceDialog({
|
||||
workspaceId: workspace.id,
|
||||
workspaceName: workspace.name
|
||||
})
|
||||
emit('delete', workspace)
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user