@@ -130,8 +132,16 @@ const MembersPage = () => {
{account.name}
- {account.status === 'pending' && {t('members.pending', { ns: 'common' })}}
- {userProfile.email === account.email && {t('members.you', { ns: 'common' })}}
+ {account.status === 'pending' && (
+
+ {t('members.pending', { ns: 'common' })}
+
+ )}
+ {userProfile.email === account.email && (
+
+ {t('members.you', { ns: 'common' })}
+
+ )}
{account.email}
@@ -139,7 +149,7 @@ const MembersPage = () => {
{formatTimeFromNow(Number((account.last_active_at || account.created_at)) * 1000)}
-
+ {/*
{isCurrentWorkspaceOwner && account.role === 'owner' && isAllowTransferWorkspace && (
setShowTransferOwnershipModal(true)}>
)}
@@ -152,6 +162,21 @@ const MembersPage = () => {
{!isCurrentWorkspaceOwner && (
{RoleMap[account.role] || RoleMap.normal}
)}
+
*/}
+
+
+ {isCurrentWorkspaceManager && (
+ setShowTransferOwnershipModal(true)}
+ />
+ )}
))
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
new file mode 100644
index 0000000000..4dbc68d756
--- /dev/null
+++ b/web/app/components/header/account-setting/members-page/member-menu.tsx
@@ -0,0 +1,134 @@
+'use client'
+import type { Member } from '@/models/common'
+import { cn } from '@langgenius/dify-ui/cn'
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from '@langgenius/dify-ui/dropdown-menu'
+import { toast } from '@langgenius/dify-ui/toast'
+import { memo, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import ActionButton from '@/app/components/base/action-button'
+import { deleteMemberOrCancelInvitation } from '@/service/common'
+import AssignRolesModal from './assign-roles-modal'
+
+type MemberMenuProps = {
+ member: Member
+ operatorRole: string
+ canTransferOwnership?: boolean
+ onOperate: () => void
+ onTransferOwnership?: () => void
+}
+
+const MemberMenu = ({
+ member,
+ operatorRole,
+ canTransferOwnership = false,
+ onOperate,
+ onTransferOwnership,
+}: MemberMenuProps) => {
+ const { t } = useTranslation()
+ const [open, setOpen] = useState(false)
+ const [assignModalOpen, setAssignModalOpen] = useState(false)
+
+ const isOwner = member.role === 'owner'
+ const canAssignRoles
+ = !isOwner && (operatorRole === 'owner' || operatorRole === 'admin')
+ const canRemove = !isOwner
+ const showTransferOwnership = isOwner && canTransferOwnership
+
+ if (!canAssignRoles && !canRemove && !showTransferOwnership)
+ return null
+
+ const handleOpenAssignRoles = () => {
+ setOpen(false)
+ setAssignModalOpen(true)
+ }
+
+ const handleAssignRolesSubmit = (_roleIds: string[]) => {
+ // TODO: wire to backend once multi-role member endpoint is ready.
+ toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
+ onOperate()
+ }
+
+ const handleRemove = async () => {
+ setOpen(false)
+ try {
+ await deleteMemberOrCancelInvitation({ url: `/workspaces/current/members/${member.id}` })
+ onOperate()
+ toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
+ }
+ catch {
+ }
+ }
+
+ const handleTransferOwnership = () => {
+ setOpen(false)
+ onTransferOwnership?.()
+ }
+
+ return (
+ <>
+
+
+ )}
+ >
+
+
+
+ {canAssignRoles && (
+
+ {t('members.assignRoles', { ns: 'common', defaultValue: 'Assign Roles' })}
+
+ )}
+ {showTransferOwnership && (
+
+ {t('members.transferOwnership', { ns: 'common' })}
+
+ )}
+ {(canAssignRoles || showTransferOwnership) && canRemove && (
+
+ )}
+ {canRemove && (
+
+ {t('members.removeFromTeam', { ns: 'common' })}
+
+ )}
+
+
+ {assignModalOpen && (
+
setAssignModalOpen(false)}
+ onSubmit={handleAssignRolesSubmit}
+ />
+ )}
+ >
+ )
+}
+
+export default memo(MemberMenu)
diff --git a/web/app/components/header/account-setting/members-page/role-badges.tsx b/web/app/components/header/account-setting/members-page/role-badges.tsx
new file mode 100644
index 0000000000..139588f901
--- /dev/null
+++ b/web/app/components/header/account-setting/members-page/role-badges.tsx
@@ -0,0 +1,53 @@
+'use client'
+
+import { cn } from '@langgenius/dify-ui/cn'
+import { memo } from 'react'
+
+type RoleBadgeProps = {
+ label: string
+ className?: string
+}
+
+const RoleBadge = ({ label, className }: RoleBadgeProps) => {
+ return (
+
+ {label}
+
+ )
+}
+
+export type RoleBadgesProps = {
+ roles: string[]
+ max?: number
+ className?: string
+}
+
+const RoleBadges = ({ roles, max = 2, className }: RoleBadgesProps) => {
+ if (!roles.length)
+ return null
+
+ const visible = roles.slice(0, max)
+ const overflow = roles.slice(max)
+
+ return (
+
+ {visible.map(role => (
+
+ ))}
+ {overflow.length > 0 && (
+
+ {`+${overflow.length}`}
+
+ )}
+
+ )
+}
+
+export default memo(RoleBadges)
diff --git a/web/i18n/en-US/common.json b/web/i18n/en-US/common.json
index 77f31ae48c..dbc73f52b8 100644
--- a/web/i18n/en-US/common.json
+++ b/web/i18n/en-US/common.json
@@ -216,6 +216,12 @@
"loading": "Loading",
"members.admin": "Admin",
"members.adminTip": "Can build apps & manage team settings",
+ "members.assignRoles": "Assign Roles",
+ "members.assignRolesModal.description": "Select roles to assign to this member. All permissions from selected roles will be combined.",
+ "members.assignRolesModal.empty": "No matching roles",
+ "members.assignRolesModal.searchPlaceholder": "Search roles...",
+ "members.assignRolesModal.selectedCount": "{{count}} selected",
+ "members.assignRolesModal.title": "Assign Roles",
"members.builder": "Builder",
"members.builderTip": "Can build & edit own apps",
"members.datasetOperator": "Knowledge Admin",
@@ -237,6 +243,7 @@
"members.inviteTeamMemberTip": "They can access your team data directly after signing in.",
"members.invitedAsRole": "Invited as {{role}} user",
"members.lastActive": "LAST ACTIVE",
+ "members.memberActions": "Member actions",
"members.name": "NAME",
"members.normal": "Normal",
"members.normalTip": "Only can use apps, can not build apps",
diff --git a/web/i18n/zh-Hans/common.json b/web/i18n/zh-Hans/common.json
index 1564d1c8e5..8c6b839ab7 100644
--- a/web/i18n/zh-Hans/common.json
+++ b/web/i18n/zh-Hans/common.json
@@ -216,6 +216,12 @@
"loading": "加载中",
"members.admin": "管理员",
"members.adminTip": "能够建立应用程序和管理团队设置",
+ "members.assignRoles": "分配角色",
+ "members.assignRolesModal.description": "为该成员选择要分配的角色,所选角色的权限将被合并。",
+ "members.assignRolesModal.empty": "没有匹配的角色",
+ "members.assignRolesModal.searchPlaceholder": "搜索角色…",
+ "members.assignRolesModal.selectedCount": "已选 {{count}} 项",
+ "members.assignRolesModal.title": "分配角色",
"members.builder": "构建器",
"members.builderTip": "可以构建和编辑自己的应用程序",
"members.datasetOperator": "知识库管理员",
@@ -237,6 +243,7 @@
"members.inviteTeamMemberTip": "对方在登录后可以访问你的团队数据。",
"members.invitedAsRole": "邀请为{{role}}用户",
"members.lastActive": "上次活动时间",
+ "members.memberActions": "成员操作",
"members.name": "姓名",
"members.normal": "成员",
"members.normalTip": "只能使用应用程序,不能建立应用程序",