mirror of
https://github.com/langgenius/dify.git
synced 2026-05-28 12:53:23 +08:00
feat: enhance member role assignment functionality and update related components
This commit is contained in:
3
web/.gitignore
vendored
3
web/.gitignore
vendored
@ -64,6 +64,3 @@ public/fallback-*.js
|
||||
|
||||
.vscode/settings.json
|
||||
.vscode/mcp.json
|
||||
|
||||
# CodeGraph local index
|
||||
.codegraph/
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -62,7 +62,6 @@ const MembersPage = () => {
|
||||
onSuccess: () => {
|
||||
toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
|
||||
refetch()
|
||||
handleCloseDetails()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user