feat: enhance member role assignment functionality and update related components

This commit is contained in:
twwu
2026-05-28 11:52:51 +08:00
parent 491747676a
commit a8cbdae2e3
6 changed files with 73 additions and 30 deletions

3
web/.gitignore vendored
View File

@ -64,6 +64,3 @@ public/fallback-*.js
.vscode/settings.json
.vscode/mcp.json
# CodeGraph local index
.codegraph/

View File

@ -9,12 +9,14 @@ import { Plan } from '@/app/components/billing/type'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
import { useUpdateRolesOfMember } from '@/service/access-control/use-member-roles'
import { useMembers } from '@/service/use-common'
import MembersPage from '../index'
vi.mock('@/context/app-context')
vi.mock('@/context/provider-context')
vi.mock('@/hooks/use-format-time-from-now')
vi.mock('@/service/access-control/use-member-roles')
vi.mock('@/service/use-common')
const renderMembersPage = () => renderWithSystemFeatures(<MembersPage />, {
@ -94,11 +96,22 @@ vi.mock('../transfer-ownership-modal', () => ({
),
}))
vi.mock('../member-details-modal', () => ({
default: ({ member, onClose, canAssignRoles }: { member: Member, onClose: () => void, canAssignRoles?: boolean }) => (
default: ({
member,
onClose,
canAssignRoles,
onAssignSubmit,
}: {
member: Member
onClose: () => void
canAssignRoles?: boolean
onAssignSubmit?: (roleIds: string[]) => void
}) => (
<div>
<div>Member Details Modal</div>
<div data-testid="details-member-name">{member.name}</div>
<div data-testid="details-can-assign">{String(canAssignRoles)}</div>
<button onClick={() => onAssignSubmit?.(['role-next'])}>Submit Member Roles</button>
<button onClick={onClose}>Close Member Details Modal</button>
</div>
),
@ -110,6 +123,7 @@ vi.mock('@/app/components/billing/upgrade-btn', () => ({
describe('MembersPage', () => {
const mockRefetch = vi.fn()
const mockFormatTimeFromNow = vi.fn(() => 'just now')
const mockUpdateRolesOfMember = vi.fn()
const mockAccounts: Member[] = [
{
@ -155,6 +169,13 @@ describe('MembersPage', () => {
data: { accounts: mockAccounts },
refetch: mockRefetch,
} as unknown as ReturnType<typeof useMembers>)
mockUpdateRolesOfMember.mockImplementation((_payload, options) => {
options?.onSuccess?.()
return Promise.resolve()
})
vi.mocked(useUpdateRolesOfMember).mockReturnValue({
mutateAsync: mockUpdateRolesOfMember,
} as unknown as ReturnType<typeof useUpdateRolesOfMember>)
vi.mocked(useProviderContext).mockReturnValue(createMockProviderContextValue({
enableBilling: false,
@ -486,6 +507,23 @@ describe('MembersPage', () => {
expect(screen.getByTestId('details-can-assign'))!.toHaveTextContent('false')
})
it('should keep member details open after role assignment succeeds', async () => {
const user = userEvent.setup()
renderMembersPage()
await user.click(screen.getByTestId('member-row-2'))
await user.click(screen.getByRole('button', { name: 'Submit Member Roles' }))
expect(mockUpdateRolesOfMember).toHaveBeenCalledWith({
memberId: '2',
roleIds: ['role-next'],
}, expect.any(Object))
expect(mockRefetch).toHaveBeenCalled()
expect(screen.getByText('Member Details Modal')).toBeInTheDocument()
expect(screen.getByTestId('details-member-name')).toHaveTextContent('Admin User')
})
it('should not open member details when clicking the member menu area', async () => {
const user = userEvent.setup()

View File

@ -1,6 +1,5 @@
'use client'
import type { Member } from '@/models/common'
import { Button } from '@langgenius/dify-ui/button'
import {
Dialog,
@ -14,7 +13,7 @@ import { useTranslation } from 'react-i18next'
import WorkspaceRoleCheckboxList from '../../workspace-role-checkbox-list'
export type AssignRolesModalProps = {
member: Member
selectedRoles: string[]
onClose: () => void
onSubmit: (roleIds: string[]) => void
}
@ -22,14 +21,12 @@ export type AssignRolesModalProps = {
type AssignRolesModalBodyProps = AssignRolesModalProps
const AssignRolesModalBody = ({
member,
selectedRoles,
onClose,
onSubmit,
}: AssignRolesModalBodyProps) => {
const { t } = useTranslation()
const [selected, setSelected] = useState<string[]>(() => {
return member.roles?.map(role => role.id) || []
})
const [selected, setSelected] = useState(selectedRoles)
const handleConfirm = () => {
onSubmit(selected)
@ -83,7 +80,7 @@ const AssignRolesModalBody = ({
}
const AssignRolesModal = ({
member,
selectedRoles,
onClose,
onSubmit,
}: AssignRolesModalProps) => {
@ -96,7 +93,7 @@ const AssignRolesModal = ({
}}
>
<AssignRolesModalBody
member={member}
selectedRoles={selectedRoles}
onClose={onClose}
onSubmit={onSubmit}
/>

View File

@ -62,7 +62,6 @@ const MembersPage = () => {
onSuccess: () => {
toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
refetch()
handleCloseDetails()
},
})
}

View File

@ -35,8 +35,9 @@ const MemberDetailsModal = ({
const roles = useMemo(() => rolesOfMember?.roles ?? [], [rolesOfMember?.roles])
const builtinRoles = roles.filter(role => role.is_builtin)
const customRoles = roles.filter(role => !role.is_builtin)
const builtinRoles = useMemo(() => roles.filter(role => role.is_builtin), [roles])
const customRoles = useMemo(() => roles.filter(role => !role.is_builtin), [roles])
const selectedRoles = useMemo(() => roles.map(role => role.id), [roles])
const handleClose = useCallback(() => {
setAssignOpen(false)
@ -48,9 +49,9 @@ const MemberDetailsModal = ({
}, [onAssignSubmit])
const handleRemove = useCallback((id: string) => {
const roleIds = roles.map(role => role.id).filter(roleId => roleId !== id)
const roleIds = selectedRoles.filter(roleId => roleId !== id)
onAssignSubmit?.(roleIds)
}, [roles, onAssignSubmit])
}, [selectedRoles, onAssignSubmit])
return (
<>
@ -167,7 +168,7 @@ const MemberDetailsModal = ({
{assignOpen && (
<AssignRolesModal
member={member}
selectedRoles={selectedRoles}
onClose={handleClose}
onSubmit={handleAssignSubmit}
/>

View File

@ -9,7 +9,7 @@ import {
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { toast } from '@langgenius/dify-ui/toast'
import { memo, useState } from 'react'
import { memo, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import { useUpdateRolesOfMember } from '@/service/access-control/use-member-roles'
@ -40,14 +40,16 @@ const MemberMenu = ({
const canRemove = !isOwner && !isCurrentUser
const showTransferOwnership = isOwner && canTransferOwnership
const handleOpenAssignRoles = () => {
const selectedRoles = useMemo(() => member.roles?.map(role => role.id) || [], [member.roles])
const handleOpenAssignRoles = useCallback(() => {
setOpen(false)
setAssignModalOpen(true)
}
}, [])
const { mutateAsync: updateRolesOfMember } = useUpdateRolesOfMember()
const handleAssignRolesSubmit = (roleIds: string[]) => {
const handleAssignRolesSubmit = useCallback((roleIds: string[]) => {
updateRolesOfMember({
memberId: member.id,
roleIds,
@ -57,9 +59,9 @@ const MemberMenu = ({
onOperate()
},
})
}
}, [member.id, onOperate, t, updateRolesOfMember])
const handleRemove = async () => {
const handleRemove = useCallback(async () => {
setOpen(false)
try {
await deleteMemberOrCancelInvitation({ url: `/workspaces/current/members/${member.id}` })
@ -68,18 +70,27 @@ const MemberMenu = ({
}
catch {
}
}
}, [member.id, onOperate, t])
const handleTransferOwnership = () => {
const handleTransferOwnership = useCallback(() => {
setOpen(false)
onTransferOwnership?.()
}
}, [onTransferOwnership])
const stopPropagationOnClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation()
}, [])
const stopPropagationOnKeyDown = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === 'Enter' || e.key === ' ')
e.stopPropagation()
}, [])
if (!canAssignRoles && !canRemove && !showTransferOwnership)
return null
return (
<>
<div onClick={stopPropagationOnClick} onKeyDown={stopPropagationOnKeyDown}>
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger
render={(
@ -129,12 +140,12 @@ const MemberMenu = ({
</DropdownMenu>
{assignModalOpen && (
<AssignRolesModal
member={member}
selectedRoles={selectedRoles}
onClose={() => setAssignModalOpen(false)}
onSubmit={handleAssignRolesSubmit}
/>
)}
</>
</div>
)
}