mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-30 17:25:45 +00:00
Compare commits
2 Commits
coderabbit
...
workspaces
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a4d9d341e | ||
|
|
6af10fca06 |
@@ -2403,7 +2403,8 @@
|
||||
"tabs": {
|
||||
"dashboard": "Dashboard",
|
||||
"planCredits": "Plan & Credits",
|
||||
"membersCount": "Members ({count})"
|
||||
"membersCount": "Members ({count})",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"dashboard": {
|
||||
"placeholder": "Dashboard workspace settings"
|
||||
@@ -2423,6 +2424,7 @@
|
||||
"actions": {
|
||||
"copyLink": "Copy invite link",
|
||||
"revokeInvite": "Revoke invite",
|
||||
"changeRole": "Change role",
|
||||
"removeMember": "Remove member"
|
||||
},
|
||||
"upsellBannerSubscribe": "Subscribe to the Creator plan or above to invite team members to this workspace.",
|
||||
@@ -2466,6 +2468,42 @@
|
||||
"message": "This member won't be able to join your workspace anymore. Their invite link will be invalidated.",
|
||||
"revoke": "Uninvite"
|
||||
},
|
||||
"transferOwnershipDialog": {
|
||||
"title": "Transfer ownership",
|
||||
"message": "Select a member to become the new owner. You will become a regular member.",
|
||||
"selectMember": "New owner",
|
||||
"transfer": "Transfer ownership",
|
||||
"noEligibleMembers": "No eligible members to transfer ownership to. Invite a member first."
|
||||
},
|
||||
"changeRoleDialog": {
|
||||
"title": "Change the role of {name}?",
|
||||
"selectNewRole": "Select a new role",
|
||||
"changeRole": "Change role",
|
||||
"error": "Failed to change role",
|
||||
"roles": {
|
||||
"owner": {
|
||||
"label": "Owner",
|
||||
"description": "Has full administrative access to the entire workspace."
|
||||
},
|
||||
"member": {
|
||||
"label": "Member",
|
||||
"description": "Can use workspace credits and view other members in the workspace."
|
||||
}
|
||||
}
|
||||
},
|
||||
"dangerZone": {
|
||||
"title": "Danger zone",
|
||||
"transferOwnership": {
|
||||
"title": "Transfer ownership",
|
||||
"description": "Transfer this workspace to another member. You will become a regular member.",
|
||||
"button": "Transfer"
|
||||
},
|
||||
"deleteWorkspace": {
|
||||
"title": "Delete this workspace",
|
||||
"description": "Permanently delete this workspace and all associated data. This cannot be undone.",
|
||||
"button": "Delete"
|
||||
}
|
||||
},
|
||||
"inviteUpsellDialog": {
|
||||
"titleNotSubscribed": "A subscription is required to invite members",
|
||||
"titleSingleSeat": "Your current plan supports a single seat",
|
||||
@@ -2517,7 +2555,12 @@
|
||||
"failedToCreateWorkspace": "Failed to create workspace",
|
||||
"failedToDeleteWorkspace": "Failed to delete workspace",
|
||||
"failedToLeaveWorkspace": "Failed to leave workspace",
|
||||
"failedToFetchWorkspaces": "Failed to load workspaces"
|
||||
"failedToFetchWorkspaces": "Failed to load workspaces",
|
||||
"ownershipTransferred": {
|
||||
"title": "Ownership transferred",
|
||||
"message": "You are now a member of this workspace."
|
||||
},
|
||||
"failedToTransferOwnership": "Failed to transfer ownership"
|
||||
}
|
||||
},
|
||||
"workspaceSwitcher": {
|
||||
|
||||
@@ -443,6 +443,40 @@ export const workspaceApi = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Transfer workspace ownership to another member.
|
||||
* POST /api/workspace/transfer-ownership
|
||||
*/
|
||||
async transferOwnership(targetUserId: string): Promise<void> {
|
||||
const headers = await getAuthHeaderOrThrow()
|
||||
try {
|
||||
await workspaceApiClient.post(
|
||||
api.apiURL('/workspace/transfer-ownership'),
|
||||
{ target_user_id: targetUserId },
|
||||
{ headers }
|
||||
)
|
||||
} catch (err) {
|
||||
handleAxiosError(err)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a member's role in the workspace.
|
||||
* PATCH /api/workspace/members/:userId/role
|
||||
*/
|
||||
async updateMemberRole(userId: string, role: WorkspaceRole): Promise<void> {
|
||||
const headers = await getAuthHeaderOrThrow()
|
||||
try {
|
||||
await workspaceApiClient.patch(
|
||||
api.apiURL(`/workspace/members/${userId}/role`),
|
||||
{ role },
|
||||
{ headers }
|
||||
)
|
||||
} catch (err) {
|
||||
handleAxiosError(err)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* List pending invites for the workspace.
|
||||
* GET /api/workspace/invites
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex w-full max-w-[360px] flex-col rounded-2xl border border-border-default bg-base-background"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div
|
||||
class="flex h-12 items-center justify-between border-b border-border-default px-4"
|
||||
>
|
||||
<h2 class="m-0 text-sm font-normal text-base-foreground">
|
||||
{{
|
||||
$t('workspacePanel.changeRoleDialog.title', {
|
||||
name: memberName
|
||||
})
|
||||
}}
|
||||
</h2>
|
||||
<button
|
||||
class="cursor-pointer rounded-sm border-none bg-transparent p-0 text-muted-foreground transition-colors hover:text-base-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-secondary-foreground"
|
||||
:aria-label="$t('g.close')"
|
||||
@click="onCancel"
|
||||
>
|
||||
<i class="pi pi-times size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="px-4 py-4">
|
||||
<p class="m-0 mb-4 text-sm text-muted-foreground">
|
||||
{{ $t('workspacePanel.changeRoleDialog.selectNewRole') }}
|
||||
</p>
|
||||
|
||||
<div role="radiogroup" class="flex flex-col gap-2">
|
||||
<label
|
||||
v-for="option in roleOptions"
|
||||
:key="option.value"
|
||||
:class="
|
||||
cn(
|
||||
'flex cursor-pointer items-start gap-3 rounded-lg border p-3 transition-colors',
|
||||
selectedRole === option.value
|
||||
? 'border-secondary-foreground bg-secondary-background/50'
|
||||
: 'border-border-default hover:bg-secondary-background/30'
|
||||
)
|
||||
"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="workspace-role"
|
||||
:value="option.value"
|
||||
:checked="selectedRole === option.value"
|
||||
class="mt-0.5 size-4 shrink-0 cursor-pointer accent-secondary-foreground"
|
||||
@change="selectedRole = option.value"
|
||||
/>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="text-sm font-medium text-base-foreground">
|
||||
{{ option.label }}
|
||||
</span>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{ option.description }}
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div
|
||||
class="flex items-center justify-end gap-4 border-t border-border-default px-4 py-4"
|
||||
>
|
||||
<Button variant="muted-textonly" @click="onCancel">
|
||||
{{ $t('g.cancel') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
:loading
|
||||
:disabled="selectedRole === currentRole"
|
||||
@click="onChangeRole"
|
||||
>
|
||||
{{ $t('workspacePanel.changeRoleDialog.changeRole') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import type { WorkspaceRole } from '@/platform/workspace/api/workspaceApi'
|
||||
import { useTeamWorkspaceStore } from '@/platform/workspace/stores/teamWorkspaceStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { memberId, memberName, currentRole } = defineProps<{
|
||||
memberId: string
|
||||
memberName: string
|
||||
currentRole: WorkspaceRole
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const dialogStore = useDialogStore()
|
||||
const workspaceStore = useTeamWorkspaceStore()
|
||||
const loading = ref(false)
|
||||
const selectedRole = ref<WorkspaceRole>(currentRole)
|
||||
|
||||
const roleOptions = [
|
||||
{
|
||||
value: 'owner' as WorkspaceRole,
|
||||
label: t('workspacePanel.changeRoleDialog.roles.owner.label'),
|
||||
description: t('workspacePanel.changeRoleDialog.roles.owner.description')
|
||||
},
|
||||
{
|
||||
value: 'member' as WorkspaceRole,
|
||||
label: t('workspacePanel.changeRoleDialog.roles.member.label'),
|
||||
description: t('workspacePanel.changeRoleDialog.roles.member.description')
|
||||
}
|
||||
]
|
||||
|
||||
function onCancel() {
|
||||
dialogStore.closeDialog({ key: 'change-role' })
|
||||
}
|
||||
|
||||
async function onChangeRole() {
|
||||
if (selectedRole.value === currentRole) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
await workspaceStore.updateMemberRole(memberId, selectedRole.value)
|
||||
dialogStore.closeDialog({ key: 'change-role' })
|
||||
} catch (error) {
|
||||
console.error('[ChangeRoleDialog] Failed to change role:', error)
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('workspacePanel.changeRoleDialog.error'),
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError'),
|
||||
life: 5000
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex w-full max-w-[360px] flex-col rounded-2xl border border-border-default bg-base-background"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div
|
||||
class="flex h-12 items-center justify-between border-b border-border-default px-4"
|
||||
>
|
||||
<h2 class="m-0 text-sm font-normal text-base-foreground">
|
||||
{{ $t('workspacePanel.transferOwnershipDialog.title') }}
|
||||
</h2>
|
||||
<button
|
||||
class="cursor-pointer rounded-sm border-none bg-transparent p-0 text-muted-foreground transition-colors hover:text-base-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-secondary-foreground"
|
||||
:aria-label="$t('g.close')"
|
||||
@click="onCancel"
|
||||
>
|
||||
<i class="pi pi-times size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="px-4 py-4">
|
||||
<p class="m-0 mb-4 text-sm text-muted-foreground">
|
||||
{{ $t('workspacePanel.transferOwnershipDialog.message') }}
|
||||
</p>
|
||||
|
||||
<template v-if="eligibleMembers.length > 0">
|
||||
<label class="mb-1 block text-xs font-medium text-muted-foreground">
|
||||
{{ $t('workspacePanel.transferOwnershipDialog.selectMember') }}
|
||||
</label>
|
||||
<select
|
||||
v-model="selectedMemberId"
|
||||
class="w-full rounded-lg border border-border-default bg-base-background px-3 py-2 text-sm text-base-foreground outline-none focus:ring-1 focus:ring-secondary-foreground"
|
||||
>
|
||||
<option value="" disabled>—</option>
|
||||
<option
|
||||
v-for="member in eligibleMembers"
|
||||
:key="member.id"
|
||||
:value="member.id"
|
||||
>
|
||||
{{ member.name }} ({{ member.email }})
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
<p v-else class="m-0 text-sm text-muted-foreground">
|
||||
{{ $t('workspacePanel.transferOwnershipDialog.noEligibleMembers') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center justify-end gap-4 px-4 py-4">
|
||||
<Button variant="muted-textonly" @click="onCancel">
|
||||
{{ $t('g.cancel') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="lg"
|
||||
:loading
|
||||
:disabled="!selectedMemberId || eligibleMembers.length === 0"
|
||||
@click="onTransfer"
|
||||
>
|
||||
{{ $t('workspacePanel.transferOwnershipDialog.transfer') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useTeamWorkspaceStore } from '@/platform/workspace/stores/teamWorkspaceStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const dialogStore = useDialogStore()
|
||||
const workspaceStore = useTeamWorkspaceStore()
|
||||
const { userEmail } = useCurrentUser()
|
||||
const loading = ref(false)
|
||||
const selectedMemberId = ref('')
|
||||
|
||||
const eligibleMembers = computed(() =>
|
||||
workspaceStore.members.filter(
|
||||
(m) =>
|
||||
m.role !== 'owner' &&
|
||||
m.email.toLowerCase() !== userEmail.value?.toLowerCase()
|
||||
)
|
||||
)
|
||||
|
||||
function onCancel() {
|
||||
dialogStore.closeDialog({ key: 'transfer-ownership' })
|
||||
}
|
||||
|
||||
async function onTransfer() {
|
||||
if (!selectedMemberId.value) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
await workspaceStore.transferOwnership(selectedMemberId.value)
|
||||
dialogStore.closeDialog({ key: 'transfer-ownership' })
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'[TransferOwnershipDialog] Failed to transfer ownership:',
|
||||
error
|
||||
)
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('workspacePanel.toast.failedToTransferOwnership'),
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError'),
|
||||
life: 5000
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -386,6 +386,7 @@ const { d, t } = useI18n()
|
||||
const toast = useToast()
|
||||
const { userPhotoUrl, userEmail, userDisplayName } = useCurrentUser()
|
||||
const {
|
||||
showChangeRoleDialog,
|
||||
showRemoveMemberDialog,
|
||||
showRevokeInviteDialog,
|
||||
showCreateWorkspaceDialog
|
||||
@@ -437,6 +438,15 @@ function getInviteInitial(email: string): string {
|
||||
}
|
||||
|
||||
const memberMenuItems = computed(() => [
|
||||
{
|
||||
label: t('workspacePanel.members.actions.changeRole'),
|
||||
icon: 'pi pi-users',
|
||||
command: () => {
|
||||
if (selectedMember.value) {
|
||||
handleChangeRole(selectedMember.value)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('workspacePanel.members.actions.removeMember'),
|
||||
icon: 'pi pi-user-minus',
|
||||
@@ -557,6 +567,14 @@ function handleCreateWorkspace() {
|
||||
showCreateWorkspaceDialog()
|
||||
}
|
||||
|
||||
function handleChangeRole(member: WorkspaceMember) {
|
||||
showChangeRoleDialog({
|
||||
memberId: member.id,
|
||||
memberName: member.name,
|
||||
currentRole: member.role
|
||||
})
|
||||
}
|
||||
|
||||
function handleRemoveMember(member: WorkspaceMember) {
|
||||
showRemoveMemberDialog(member.id)
|
||||
}
|
||||
|
||||
@@ -38,6 +38,18 @@
|
||||
})
|
||||
}}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
v-if="showSettingsTab"
|
||||
value="settings"
|
||||
:class="
|
||||
cn(
|
||||
tabTriggerBase,
|
||||
activeTab === 'settings' ? tabTriggerActive : tabTriggerInactive
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ $t('workspacePanel.tabs.settings') }}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
@@ -109,6 +121,75 @@
|
||||
<TabsContent value="members" class="mt-4">
|
||||
<MembersPanelContent :key="workspaceRole" />
|
||||
</TabsContent>
|
||||
<TabsContent v-if="showSettingsTab" value="settings" class="mt-4">
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="rounded-2xl border border-danger/30 p-6">
|
||||
<h3 class="m-0 mb-4 text-base font-semibold text-danger">
|
||||
{{ $t('workspacePanel.dangerZone.title') }}
|
||||
</h3>
|
||||
<div class="flex flex-col gap-4">
|
||||
<!-- Transfer Ownership -->
|
||||
<div
|
||||
class="flex items-center justify-between gap-4 rounded-lg border border-border-default p-4"
|
||||
>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-sm font-medium text-base-foreground">
|
||||
{{
|
||||
$t('workspacePanel.dangerZone.transferOwnership.title')
|
||||
}}
|
||||
</span>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{
|
||||
$t(
|
||||
'workspacePanel.dangerZone.transferOwnership.description'
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="md"
|
||||
class="shrink-0 border border-danger bg-transparent text-danger hover:bg-danger/10"
|
||||
@click="showTransferOwnershipDialog()"
|
||||
>
|
||||
{{ $t('workspacePanel.dangerZone.transferOwnership.button') }}
|
||||
</Button>
|
||||
</div>
|
||||
<!-- Delete Workspace -->
|
||||
<div
|
||||
class="flex items-center justify-between gap-4 rounded-lg border border-border-default p-4"
|
||||
>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-sm font-medium text-base-foreground">
|
||||
{{ $t('workspacePanel.dangerZone.deleteWorkspace.title') }}
|
||||
</span>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{
|
||||
$t(
|
||||
'workspacePanel.dangerZone.deleteWorkspace.description'
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
v-tooltip="
|
||||
isDeleteDisabled && deleteTooltip
|
||||
? { value: deleteTooltip, showDelay: 0 }
|
||||
: undefined
|
||||
"
|
||||
variant="destructive"
|
||||
size="md"
|
||||
class="shrink-0"
|
||||
:disabled="isDeleteDisabled"
|
||||
@click="handleDeleteWorkspace"
|
||||
>
|
||||
{{ $t('workspacePanel.dangerZone.deleteWorkspace.button') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</TabsRoot>
|
||||
</div>
|
||||
</template>
|
||||
@@ -147,6 +228,7 @@ const { t } = useI18n()
|
||||
const {
|
||||
showLeaveWorkspaceDialog,
|
||||
showDeleteWorkspaceDialog,
|
||||
showTransferOwnershipDialog,
|
||||
showInviteMemberDialog,
|
||||
showInviteMemberUpsellDialog,
|
||||
showEditWorkspaceDialog
|
||||
@@ -169,6 +251,12 @@ const { fetchMembers, fetchPendingInvites } = workspaceStore
|
||||
const { workspaceRole, permissions, uiConfig } = useWorkspaceUI()
|
||||
const activeTab = ref(defaultTab)
|
||||
|
||||
const showSettingsTab = computed(
|
||||
() =>
|
||||
workspaceStore.activeWorkspace?.type === 'team' &&
|
||||
workspaceRole.value === 'owner'
|
||||
)
|
||||
|
||||
const menu = ref<InstanceType<typeof Menu> | null>(null)
|
||||
|
||||
function handleLeaveWorkspace() {
|
||||
@@ -225,17 +313,7 @@ const menuItems = computed(() => {
|
||||
}
|
||||
|
||||
const action = uiConfig.value.workspaceMenuAction
|
||||
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') {
|
||||
if (action === 'leave') {
|
||||
items.push({
|
||||
label: t('workspacePanel.menu.leaveWorkspace'),
|
||||
icon: 'pi pi-sign-out',
|
||||
|
||||
@@ -13,6 +13,7 @@ interface WorkspacePermissions {
|
||||
canRemoveMembers: boolean
|
||||
canLeaveWorkspace: boolean
|
||||
canAccessWorkspaceMenu: boolean
|
||||
canTransferOwnership: boolean
|
||||
canManageSubscription: boolean
|
||||
canTopUp: boolean
|
||||
}
|
||||
@@ -45,6 +46,7 @@ function getPermissions(
|
||||
canRemoveMembers: false,
|
||||
canLeaveWorkspace: false,
|
||||
canAccessWorkspaceMenu: false,
|
||||
canTransferOwnership: false,
|
||||
canManageSubscription: true,
|
||||
canTopUp: true
|
||||
}
|
||||
@@ -59,6 +61,7 @@ function getPermissions(
|
||||
canRemoveMembers: true,
|
||||
canLeaveWorkspace: true,
|
||||
canAccessWorkspaceMenu: true,
|
||||
canTransferOwnership: true,
|
||||
canManageSubscription: true,
|
||||
canTopUp: true
|
||||
}
|
||||
@@ -73,6 +76,7 @@ function getPermissions(
|
||||
canRemoveMembers: false,
|
||||
canLeaveWorkspace: true,
|
||||
canAccessWorkspaceMenu: true,
|
||||
canTransferOwnership: false,
|
||||
canManageSubscription: false,
|
||||
canTopUp: false
|
||||
}
|
||||
|
||||
@@ -492,6 +492,28 @@ export const useTeamWorkspaceStore = defineStore('teamWorkspace', () => {
|
||||
// Code after this won't run (page reloads)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer ownership of the current workspace to another member.
|
||||
* After transfer, the page reloads to reflect the role change.
|
||||
*/
|
||||
async function transferOwnership(targetUserId: string): Promise<void> {
|
||||
const current = activeWorkspace.value
|
||||
if (!current || current.type === 'personal') {
|
||||
throw new Error('Cannot transfer ownership of personal workspace')
|
||||
}
|
||||
|
||||
const workspaceAuthStore = useWorkspaceAuthStore()
|
||||
|
||||
await workspaceApi.transferOwnership(targetUserId)
|
||||
|
||||
// Reload to get fresh token with updated role
|
||||
workspaceAuthStore.clearWorkspaceContext()
|
||||
if (current.id) {
|
||||
setLastWorkspaceId(current.id)
|
||||
}
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch members for the current workspace.
|
||||
*/
|
||||
@@ -507,6 +529,24 @@ export const useTeamWorkspaceStore = defineStore('teamWorkspace', () => {
|
||||
return members
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a member's role in the current workspace.
|
||||
*/
|
||||
async function updateMemberRole(
|
||||
userId: string,
|
||||
role: 'owner' | 'member'
|
||||
): Promise<void> {
|
||||
await workspaceApi.updateMemberRole(userId, role)
|
||||
const current = activeWorkspace.value
|
||||
if (current) {
|
||||
updateActiveWorkspace({
|
||||
members: current.members.map((m) =>
|
||||
m.id === userId ? { ...m, role } : m
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a member from the current workspace.
|
||||
*/
|
||||
@@ -672,9 +712,11 @@ export const useTeamWorkspaceStore = defineStore('teamWorkspace', () => {
|
||||
renameWorkspace,
|
||||
updateWorkspaceName,
|
||||
leaveWorkspace,
|
||||
transferOwnership,
|
||||
|
||||
// Member Actions
|
||||
fetchMembers,
|
||||
updateMemberRole,
|
||||
removeMember,
|
||||
|
||||
// Invite Actions
|
||||
|
||||
@@ -452,6 +452,16 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
async function showTransferOwnershipDialog() {
|
||||
const { default: component } =
|
||||
await import('@/platform/workspace/components/dialogs/TransferOwnershipDialogContent.vue')
|
||||
return dialogStore.showDialog({
|
||||
key: 'transfer-ownership',
|
||||
component,
|
||||
dialogComponentProps: workspaceDialogPt
|
||||
})
|
||||
}
|
||||
|
||||
async function showLeaveWorkspaceDialog() {
|
||||
const { default: component } =
|
||||
await import('@/platform/workspace/components/dialogs/LeaveWorkspaceDialogContent.vue')
|
||||
@@ -474,6 +484,21 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
async function showChangeRoleDialog(options: {
|
||||
memberId: string
|
||||
memberName: string
|
||||
currentRole: 'owner' | 'member'
|
||||
}) {
|
||||
const { default: component } =
|
||||
await import('@/platform/workspace/components/dialogs/ChangeRoleDialogContent.vue')
|
||||
return dialogStore.showDialog({
|
||||
key: 'change-role',
|
||||
component,
|
||||
props: options,
|
||||
dialogComponentProps: workspaceDialogPt
|
||||
})
|
||||
}
|
||||
|
||||
async function showRemoveMemberDialog(memberId: string) {
|
||||
const { default: component } =
|
||||
await import('@/platform/workspace/components/dialogs/RemoveMemberDialogContent.vue')
|
||||
@@ -565,9 +590,11 @@ export const useDialogService = () => {
|
||||
showLayoutDialog,
|
||||
showSmallLayoutDialog,
|
||||
showDeleteWorkspaceDialog,
|
||||
showTransferOwnershipDialog,
|
||||
showCreateWorkspaceDialog,
|
||||
showLeaveWorkspaceDialog,
|
||||
showEditWorkspaceDialog,
|
||||
showChangeRoleDialog,
|
||||
showRemoveMemberDialog,
|
||||
showRevokeInviteDialog,
|
||||
showInviteMemberDialog,
|
||||
|
||||
Reference in New Issue
Block a user