Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/api/space-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ export const listSpaceUserMembers = (
export const removeSpaceUserMembers = (spaceId: string, uids: string[]) =>
api.post(`/v1/space/${spaceId}/members/remove`, { uids })

export const updateSpaceUserMemberRole = (
spaceId: string,
uid: string,
role: SpaceUserMember['role'],
) => api.put(`/v1/space/${spaceId}/members/${uid}/role`, { role })

export const createSpaceUserInvite = (
spaceId: string,
data: { max_uses?: number; expires_at?: string } = {},
Expand Down
14 changes: 10 additions & 4 deletions src/hooks/useSpaceScope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import * as manager from '../api/space'
import * as user from '../api/space-user'

export type SpaceScopeKind = 'super' | 'space'
export type SpaceScopeRole = 'super' | 0 | 1 | 2
export type SpaceMemberRole = 0 | 1 | 2
export type SpaceScopeRole = 'super' | SpaceMemberRole

export interface InviteListItem {
invite_code: string
Expand All @@ -25,7 +26,7 @@ export interface InviteListResp {
export interface MemberItem {
uid: string
name: string
role: 0 | 1 | 2
role: SpaceMemberRole
status?: number
robot?: 0 | 1
created_at?: string
Expand Down Expand Up @@ -85,6 +86,7 @@ export interface SpaceScope {
listMembers: (spaceId: string, params: ScopedMemberParams) => Promise<MemberListResp>
addMembers?: (spaceId: string, uids: string[]) => Promise<unknown>
removeMembers: (spaceId: string, uids: string[]) => Promise<unknown>
updateMemberRole: (spaceId: string, uid: string, role: SpaceMemberRole) => Promise<unknown>
listInvites: (spaceId: string, params: ScopedInviteParams) => Promise<InviteListResp>
createInvite: (
spaceId: string,
Expand Down Expand Up @@ -123,6 +125,8 @@ function buildSuperScope(): SpaceScope {
},
addMembers: (spaceId, uids) => manager.addSpaceMembers(spaceId, uids),
removeMembers: (spaceId, uids) => manager.removeSpaceMembers(spaceId, uids),
updateMemberRole: (spaceId, uid, role) =>
manager.updateSpaceMemberRole(spaceId, uid, role),
listInvites: async (spaceId, params) => {
const res = await manager.listSpaceInvites(spaceId, {
page_index: params.page_index,
Expand Down Expand Up @@ -153,7 +157,7 @@ function buildSuperScope(): SpaceScope {
}
}

function buildUserScope(role: 0 | 1 | 2): SpaceScope {
function buildUserScope(role: SpaceMemberRole): SpaceScope {
const isManager = role >= 1
return {
kind: 'space',
Expand Down Expand Up @@ -192,6 +196,8 @@ function buildUserScope(role: 0 | 1 | 2): SpaceScope {
}
},
removeMembers: (spaceId, uids) => user.removeSpaceUserMembers(spaceId, uids),
updateMemberRole: (spaceId, uid, nextRole) =>
user.updateSpaceUserMemberRole(spaceId, uid, nextRole),
listInvites: async (spaceId, params) => {
const resp = await user.listSpaceUserInvites(spaceId, params)
return { count: resp.count, list: resp.list }
Expand Down Expand Up @@ -230,7 +236,7 @@ function buildUserScope(role: 0 | 1 | 2): SpaceScope {
}
}

export function useSpaceScope(role?: 0 | 1 | 2): SpaceScope {
export function useSpaceScope(role?: SpaceMemberRole): SpaceScope {
const scope = useAuthStore((s) => s.scope)
return useMemo(() => {
if (scope === 'super') return buildSuperScope()
Expand Down
25 changes: 23 additions & 2 deletions src/pages/SpaceAdmin/SpaceAdminLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Outlet, useNavigate, useParams, useLocation } from 'react-router-dom'
import {
Expand Down Expand Up @@ -156,6 +156,27 @@ export default function SpaceAdminLayout() {
}
}, [spaceId, currentSpaceId, setCurrentSpaceId, setMySpaces, navigate])

// 角色变更等操作后重新校验当前用户在本空间的角色,避免 scope.role 过期
// (例如 owner 转让所有权后被降级为 admin,角色操作应及时消失)。
const refreshDetail = useCallback(async () => {
if (!spaceId) return
try {
const [list, d] = await Promise.all([
getMySpaces().catch(() => [] as MySpace[]),
getSpaceUserDetail(spaceId),
])
const managed = (list || []).filter((s) => s.role >= 1)
setMySpaces(managed)
if (!managed.some((s) => s.space_id === spaceId)) {
navigate('/space', { replace: true })
return
}
setDetail(d)
} catch (error) {
message.error((error as Error).message)
}
}, [spaceId, setMySpaces, navigate])

const handleLogout = () => {
logout()
navigate('/login')
Expand Down Expand Up @@ -465,7 +486,7 @@ export default function SpaceAdminLayout() {
destroyInactiveTabPane
tabBarStyle={{ marginBottom: 16 }}
/>
<Outlet context={{ detail }} />
<Outlet context={{ detail, refreshDetail }} />
</>
)}
</Content>
Expand Down
15 changes: 11 additions & 4 deletions src/pages/SpaceAdmin/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,25 @@ import AppBotsPage from '../AppBots'

interface Ctx {
detail: SpaceUserDetail
refreshDetail: () => void
}

function useSpaceCtx() {
const { spaceId } = useParams<{ spaceId: string }>()
const { detail } = useOutletContext<Ctx>()
const { detail, refreshDetail } = useOutletContext<Ctx>()
const scope = useSpaceScope(detail.role)
return { spaceId: spaceId!, detail, scope }
return { spaceId: spaceId!, detail, scope, refreshDetail }
}

export function MembersTab() {
const { spaceId, scope } = useSpaceCtx()
return <SpaceMembersPanel spaceId={spaceId} scope={scope} />
const { spaceId, scope, refreshDetail } = useSpaceCtx()
return (
<SpaceMembersPanel
spaceId={spaceId}
scope={scope}
onRoleChanged={refreshDetail}
/>
)
}

export function InvitesTab() {
Expand Down
23 changes: 17 additions & 6 deletions src/pages/Spaces/SpaceMembersPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@ import type { ColumnsType } from 'antd/es/table'
import { useTranslation } from 'react-i18next'

const { Text } = Typography
import { updateSpaceMemberRole, type SpaceMemberRole } from '../../api/space'
import type { MemberItem, SpaceScope } from '../../hooks/useSpaceScope'
import { useAuthStore } from '../../store/auth'
import type {
MemberItem,
SpaceScope,
SpaceMemberRole,
} from '../../hooks/useSpaceScope'

interface Props {
spaceId: string
scope: SpaceScope
readOnly?: boolean
onRoleChanged?: () => void
}

const ROLE_LABEL: Record<SpaceMemberRole, { textKey: string; tone: 'neutral' | 'warning' | 'brand' }> = {
Expand All @@ -31,8 +36,9 @@ const ROLE_LABEL: Record<SpaceMemberRole, { textKey: string; tone: 'neutral' | '

const PAGE_SIZE = 20

export default function SpaceMembersPanel({ spaceId, scope, readOnly = false }: Props) {
export default function SpaceMembersPanel({ spaceId, scope, readOnly = false, onRoleChanged }: Props) {
const { t } = useTranslation(['spaces', 'common'])
const viewerUid = useAuthStore((s) => s.uid)
const [loading, setLoading] = useState(false)
const [data, setData] = useState<MemberItem[]>([])
const [total, setTotal] = useState(0)
Expand All @@ -44,7 +50,7 @@ export default function SpaceMembersPanel({ spaceId, scope, readOnly = false }:

const canAdd = !readOnly && scope.canAddMembers
const canRemove = !readOnly && scope.canRemoveMembers
const canChangeRole = !readOnly && scope.kind === 'super'
const canChangeRole = !readOnly && (scope.kind === 'super' || scope.role === 2)

const fetchData = async (nextPage = page, kw = keyword) => {
setLoading(true)
Expand Down Expand Up @@ -84,9 +90,12 @@ export default function SpaceMembersPanel({ spaceId, scope, readOnly = false }:
: undefined,
onOk: async () => {
try {
await updateSpaceMemberRole(spaceId, uid, role)
await scope.api.updateMemberRole(spaceId, uid, role)
message.success(t('members.changeRole.success'))
fetchData()
// 角色变更可能改变当前用户自身权限(如转让所有权后被降级),
// 通知上层重新校验 scope,避免操作入口残留到刷新前。
onRoleChanged?.()
} catch (error) {
message.error((error as Error).message)
}
Expand Down Expand Up @@ -148,7 +157,9 @@ export default function SpaceMembersPanel({ spaceId, scope, readOnly = false }:
if (record.status !== undefined && record.status !== 1)
return <span style={{ color: 'var(--a-text-quaternary)' }}>—</span>
const items: MenuProps['items'] = []
if (canChangeRole) {
// 不对当前登录用户自己的行展示角色操作:owner 无法自降级、
// 也无需对自己做转让/升降级,这些请求注定被后端拒绝。
if (canChangeRole && record.uid !== viewerUid) {
if (record.role !== 2) {
items.push({
key: 'owner',
Expand Down
Loading