diff --git a/web/.gitignore b/web/.gitignore index 1a81282c4c..9de3dc83f9 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -64,6 +64,3 @@ public/fallback-*.js .vscode/settings.json .vscode/mcp.json - -# CodeGraph local index -.codegraph/ diff --git a/web/app/components/header/account-setting/members-page/__tests__/index.spec.tsx b/web/app/components/header/account-setting/members-page/__tests__/index.spec.tsx index 6266da59b8..92d5df556a 100644 --- a/web/app/components/header/account-setting/members-page/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/members-page/__tests__/index.spec.tsx @@ -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(, { @@ -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 + }) => (
Member Details Modal
{member.name}
{String(canAssignRoles)}
+
), @@ -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) + mockUpdateRolesOfMember.mockImplementation((_payload, options) => { + options?.onSuccess?.() + return Promise.resolve() + }) + vi.mocked(useUpdateRolesOfMember).mockReturnValue({ + mutateAsync: mockUpdateRolesOfMember, + } as unknown as ReturnType) 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() diff --git a/web/app/components/header/account-setting/members-page/assign-roles-modal/index.tsx b/web/app/components/header/account-setting/members-page/assign-roles-modal/index.tsx index 376a38a781..34f643d619 100644 --- a/web/app/components/header/account-setting/members-page/assign-roles-modal/index.tsx +++ b/web/app/components/header/account-setting/members-page/assign-roles-modal/index.tsx @@ -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(() => { - 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 = ({ }} > diff --git a/web/app/components/header/account-setting/members-page/index.tsx b/web/app/components/header/account-setting/members-page/index.tsx index c217c5e0e5..a9b2befa96 100644 --- a/web/app/components/header/account-setting/members-page/index.tsx +++ b/web/app/components/header/account-setting/members-page/index.tsx @@ -62,7 +62,6 @@ const MembersPage = () => { onSuccess: () => { toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) refetch() - handleCloseDetails() }, }) } diff --git a/web/app/components/header/account-setting/members-page/member-details-modal/index.tsx b/web/app/components/header/account-setting/members-page/member-details-modal/index.tsx index f70e1cc23f..41a2cd695e 100644 --- a/web/app/components/header/account-setting/members-page/member-details-modal/index.tsx +++ b/web/app/components/header/account-setting/members-page/member-details-modal/index.tsx @@ -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 && ( diff --git a/web/app/components/header/account-setting/members-page/member-menu.tsx b/web/app/components/header/account-setting/members-page/member-menu.tsx index 21c0585ece..5ea55adbb2 100644 --- a/web/app/components/header/account-setting/members-page/member-menu.tsx +++ b/web/app/components/header/account-setting/members-page/member-menu.tsx @@ -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) => { + if (e.key === 'Enter' || e.key === ' ') + e.stopPropagation() + }, []) if (!canAssignRoles && !canRemove && !showTransferOwnership) return null return ( - <> +
{assignModalOpen && ( setAssignModalOpen(false)} onSubmit={handleAssignRolesSubmit} /> )} - +
) }