mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 12:59:18 +08:00
feat: implement member details modal and role assignment functionality
This commit is contained in:
parent
b32ec8741e
commit
5907b3f809
@ -73,6 +73,16 @@ vi.mock('../transfer-ownership-modal', () => ({
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
vi.mock('../member-details-modal', () => ({
|
||||
default: ({ member, onClose, canAssignRoles }: { member: Member, onClose: () => void, canAssignRoles?: boolean }) => (
|
||||
<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={onClose}>Close Member Details Modal</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
vi.mock('@/app/components/billing/upgrade-btn', () => ({
|
||||
default: () => <div>Upgrade Button</div>,
|
||||
}))
|
||||
@ -368,6 +378,52 @@ describe('MembersPage', () => {
|
||||
expect(screen.getByText('common.members.normal'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should open member details modal when a member row is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
renderMembersPage()
|
||||
|
||||
await user.click(screen.getByTestId('member-row-2'))
|
||||
|
||||
expect(screen.getByText('Member Details Modal'))!.toBeInTheDocument()
|
||||
expect(screen.getByTestId('details-member-name'))!.toHaveTextContent('Admin User')
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Close Member Details Modal' }))
|
||||
expect(screen.queryByText('Member Details Modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should open member details modal via keyboard Enter', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
renderMembersPage()
|
||||
|
||||
const row = screen.getByTestId('member-row-2')
|
||||
row.focus()
|
||||
await user.keyboard('{Enter}')
|
||||
|
||||
expect(screen.getByText('Member Details Modal'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not allow assigning roles from member details when target is owner', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
renderMembersPage()
|
||||
|
||||
await user.click(screen.getByTestId('member-row-1'))
|
||||
|
||||
expect(screen.getByTestId('details-can-assign'))!.toHaveTextContent('false')
|
||||
})
|
||||
|
||||
it('should not open member details when clicking the member menu area', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
renderMembersPage()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /transfer ownership/i }))
|
||||
|
||||
expect(screen.queryByText('Member Details Modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show upgrade button when member limit is full', () => {
|
||||
vi.mocked(useProviderContext).mockReturnValue(createMockProviderContextValue({
|
||||
enableBilling: true,
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
'use client'
|
||||
import type { InvitationResult } from '@/models/common'
|
||||
import { Avatar } from '@langgenius/dify-ui/avatar'
|
||||
import type { InvitationResult, Member } from '@/models/common'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { NUM_INFINITE } from '@/app/components/billing/config'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
@ -11,7 +11,6 @@ import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||
import { useMembers } from '@/service/use-common'
|
||||
@ -19,8 +18,8 @@ import EditWorkspaceModal from './edit-workspace-modal'
|
||||
import InviteButton from './invite-button'
|
||||
import InviteModal from './invite-modal'
|
||||
import InvitedModal from './invited-modal'
|
||||
import MemberMenu from './member-menu'
|
||||
import RoleBadges from './role-badges'
|
||||
import MemberDetailsModal from './member-details-modal'
|
||||
import MemberRow from './member-row'
|
||||
import TransferOwnershipModal from './transfer-ownership-modal'
|
||||
|
||||
const MembersPage = () => {
|
||||
@ -37,7 +36,6 @@ const MembersPage = () => {
|
||||
const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager } = useAppContext()
|
||||
const { data, refetch } = useMembers()
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||
const [inviteModalVisible, setInviteModalVisible] = useState(false)
|
||||
const [invitationResults, setInvitationResults] = useState<InvitationResult[]>([])
|
||||
const [invitedModalVisible, setInvitedModalVisible] = useState(false)
|
||||
@ -47,6 +45,21 @@ const MembersPage = () => {
|
||||
const isMemberFull = enableBilling && isNotUnlimitedMemberPlan && accounts.length >= plan.total.teamMembers
|
||||
const [editWorkspaceModalVisible, setEditWorkspaceModalVisible] = useState(false)
|
||||
const [showTransferOwnershipModal, setShowTransferOwnershipModal] = useState(false)
|
||||
const [detailsMember, setDetailsMember] = useState<Member | null>(null)
|
||||
|
||||
const handleAssignRolesSubmit = (_roleIds: string[]) => {
|
||||
// TODO: wire to backend once multi-role member endpoint is ready.
|
||||
toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
|
||||
refetch()
|
||||
}
|
||||
|
||||
const handleOpenDetails = useCallback((member: Member) => {
|
||||
setDetailsMember(member)
|
||||
}, [])
|
||||
|
||||
const handleTransferOwnership = useCallback(() => {
|
||||
setShowTransferOwnershipModal(true)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -124,63 +137,20 @@ const MembersPage = () => {
|
||||
<div className="w-[215px] shrink-0 px-3 system-xs-medium-uppercase text-text-tertiary">{t('members.role', { ns: 'common' })}</div>
|
||||
</div>
|
||||
<div className="relative min-w-[480px]">
|
||||
{
|
||||
accounts.map(account => (
|
||||
<div key={account.id} className="flex border-b border-divider-subtle">
|
||||
<div className="flex grow items-center px-3 py-2">
|
||||
<Avatar avatar={account.avatar_url} size="sm" className="mr-2" name={account.name} />
|
||||
<div className="">
|
||||
<div className="system-sm-medium text-text-secondary">
|
||||
{account.name}
|
||||
{account.status === 'pending' && (
|
||||
<span className="ml-1 system-xs-medium text-text-warning">
|
||||
{t('members.pending', { ns: 'common' })}
|
||||
</span>
|
||||
)}
|
||||
{userProfile.email === account.email && (
|
||||
<span className="system-xs-regular text-text-tertiary">
|
||||
{t('members.you', { ns: 'common' })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="system-xs-regular text-text-tertiary">{account.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-[120px] shrink-0 items-center py-2 system-sm-regular text-text-secondary">
|
||||
{formatTimeFromNow(Number((account.last_active_at || account.created_at)) * 1000)}
|
||||
</div>
|
||||
{/* <div className="flex w-[215px] shrink-0 items-center">
|
||||
{isCurrentWorkspaceOwner && account.role === 'owner' && isAllowTransferWorkspace && (
|
||||
<TransferOwnership onOperate={() => setShowTransferOwnershipModal(true)}></TransferOwnership>
|
||||
)}
|
||||
{isCurrentWorkspaceOwner && account.role === 'owner' && !isAllowTransferWorkspace && (
|
||||
<div className="px-3 system-sm-regular text-text-secondary">{RoleMap[account.role] || RoleMap.normal}</div>
|
||||
)}
|
||||
{isCurrentWorkspaceOwner && account.role !== 'owner' && (
|
||||
<Operation member={account} operatorRole={currentWorkspace.role} onOperate={refetch} />
|
||||
)}
|
||||
{!isCurrentWorkspaceOwner && (
|
||||
<div className="px-3 system-sm-regular text-text-secondary">{RoleMap[account.role] || RoleMap.normal}</div>
|
||||
)}
|
||||
</div> */}
|
||||
<div className="flex w-[215px] shrink-0 items-center gap-2 px-3">
|
||||
<RoleBadges
|
||||
className="grow"
|
||||
roles={[RoleMap[account.role] || RoleMap.normal]}
|
||||
/>
|
||||
{isCurrentWorkspaceManager && (
|
||||
<MemberMenu
|
||||
member={account}
|
||||
operatorRole={currentWorkspace.role}
|
||||
canTransferOwnership={isCurrentWorkspaceOwner && isAllowTransferWorkspace}
|
||||
onOperate={refetch}
|
||||
onTransferOwnership={() => setShowTransferOwnershipModal(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
{accounts.map(account => (
|
||||
<MemberRow
|
||||
key={account.id}
|
||||
member={account}
|
||||
roleLabel={RoleMap[account.role] || RoleMap.normal}
|
||||
isCurrentUser={userProfile.email === account.email}
|
||||
canManage={isCurrentWorkspaceManager}
|
||||
operatorRole={currentWorkspace.role}
|
||||
canTransferOwnership={isCurrentWorkspaceOwner && isAllowTransferWorkspace}
|
||||
onOpenDetails={handleOpenDetails}
|
||||
onOperate={refetch}
|
||||
onTransferOwnership={handleTransferOwnership}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -218,6 +188,19 @@ const MembersPage = () => {
|
||||
onClose={() => setShowTransferOwnershipModal(false)}
|
||||
/>
|
||||
)}
|
||||
{detailsMember && (
|
||||
<MemberDetailsModal
|
||||
open={!!detailsMember}
|
||||
member={detailsMember}
|
||||
roleLabel={RoleMap[detailsMember.role] || RoleMap.normal}
|
||||
canAssignRoles={
|
||||
isCurrentWorkspaceManager
|
||||
&& detailsMember.role !== 'owner'
|
||||
}
|
||||
onClose={() => setDetailsMember(null)}
|
||||
onAssignSubmit={handleAssignRolesSubmit}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,145 @@
|
||||
'use client'
|
||||
|
||||
import type { Member } from '@/models/common'
|
||||
import { Avatar } from '@langgenius/dify-ui/avatar'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogCloseButton,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
} from '@langgenius/dify-ui/dialog'
|
||||
import { memo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AssignRolesModal from '../assign-roles-modal'
|
||||
import PermissionRoleChip from './permission-role-chip'
|
||||
|
||||
export type MemberDetailsModalProps = {
|
||||
open: boolean
|
||||
member: Member
|
||||
roleLabel: string
|
||||
canAssignRoles?: boolean
|
||||
onClose: () => void
|
||||
onAssignSubmit?: (roleIds: string[]) => void
|
||||
}
|
||||
|
||||
const MemberDetailsModal = ({
|
||||
open,
|
||||
member,
|
||||
roleLabel,
|
||||
canAssignRoles = false,
|
||||
onClose,
|
||||
onAssignSubmit,
|
||||
}: MemberDetailsModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [assignOpen, setAssignOpen] = useState(false)
|
||||
|
||||
const assignedRoles = [{ key: member.role, label: roleLabel }]
|
||||
|
||||
const handleAssignSubmit = (ids: string[]) => {
|
||||
onAssignSubmit?.(ids)
|
||||
setAssignOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(next) => {
|
||||
if (!next)
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<DialogContent className="w-[440px] overflow-visible p-0" backdropProps={{ forceRender: true }}>
|
||||
<div className="relative px-6 pt-6 pb-5">
|
||||
<DialogCloseButton />
|
||||
<DialogTitle className="pr-8 system-xl-semibold text-text-primary">
|
||||
{t('members.memberDetails.title', {
|
||||
ns: 'common',
|
||||
defaultValue: 'Member Details',
|
||||
})}
|
||||
</DialogTitle>
|
||||
|
||||
<div className="mt-5 flex items-center gap-3">
|
||||
<Avatar
|
||||
avatar={member.avatar_url}
|
||||
name={member.name}
|
||||
size="2xl"
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate system-md-semibold text-text-primary">
|
||||
{member.name}
|
||||
</div>
|
||||
<div className="truncate system-xs-regular text-text-tertiary">
|
||||
{member.email}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-divider-subtle px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5 system-sm-semibold text-text-secondary">
|
||||
<span>
|
||||
{t('members.memberDetails.assignedRoles', {
|
||||
ns: 'common',
|
||||
defaultValue: 'Assigned Roles',
|
||||
})}
|
||||
</span>
|
||||
<span className="system-xs-medium text-text-tertiary">
|
||||
{assignedRoles.length}
|
||||
</span>
|
||||
</div>
|
||||
{canAssignRoles && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={() => setAssignOpen(true)}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="mr-0.5 i-ri-add-line h-3.5 w-3.5"
|
||||
/>
|
||||
{t('members.memberDetails.assign', {
|
||||
ns: 'common',
|
||||
defaultValue: 'Assign',
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="mb-2 system-2xs-medium-uppercase text-text-tertiary">
|
||||
{t('members.memberDetails.generalGroup', {
|
||||
ns: 'common',
|
||||
defaultValue: 'GENERAL',
|
||||
})}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{assignedRoles.map(role => (
|
||||
<PermissionRoleChip
|
||||
key={role.key}
|
||||
roleKey={role.key}
|
||||
label={role.label}
|
||||
highlighted
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{assignOpen && (
|
||||
<AssignRolesModal
|
||||
open={assignOpen}
|
||||
member={member}
|
||||
onClose={() => setAssignOpen(false)}
|
||||
onSubmit={handleAssignSubmit}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(MemberDetailsModal)
|
||||
@ -0,0 +1,102 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
PreviewCard,
|
||||
PreviewCardContent,
|
||||
PreviewCardTrigger,
|
||||
} from '@langgenius/dify-ui/preview-card'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { getRolePermissionKeys } from './role-permissions'
|
||||
|
||||
export type PermissionRoleChipProps = {
|
||||
roleKey: string
|
||||
label: string
|
||||
highlighted?: boolean
|
||||
onRemove?: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const PermissionRoleChip = ({
|
||||
roleKey,
|
||||
label,
|
||||
highlighted = false,
|
||||
onRemove,
|
||||
className,
|
||||
}: PermissionRoleChipProps) => {
|
||||
const { t } = useTranslation()
|
||||
const permissions = getRolePermissionKeys(roleKey)
|
||||
const hasPermissions = permissions.length > 0
|
||||
|
||||
const chip = (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex h-6 max-w-full cursor-default items-center gap-1 rounded-md px-1.5 system-xs-medium shadow-xs',
|
||||
highlighted
|
||||
? 'bg-state-accent-hover text-text-accent'
|
||||
: 'bg-background-body text-text-secondary',
|
||||
className,
|
||||
)}
|
||||
data-testid="permission-role-chip"
|
||||
data-role-key={roleKey}
|
||||
>
|
||||
<span className="truncate">{label}</span>
|
||||
{onRemove && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('members.memberDetails.removeRoleAria', {
|
||||
ns: 'common',
|
||||
role: label,
|
||||
defaultValue: 'Remove {{role}} role',
|
||||
})}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onRemove()
|
||||
}}
|
||||
className={cn(
|
||||
'flex h-4 w-4 items-center justify-center rounded hover:bg-black/5',
|
||||
highlighted ? 'text-text-accent' : 'text-text-tertiary',
|
||||
)}
|
||||
>
|
||||
<span aria-hidden className="i-ri-close-line h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
|
||||
if (!hasPermissions)
|
||||
return chip
|
||||
|
||||
return (
|
||||
<PreviewCard>
|
||||
<PreviewCardTrigger render={chip} />
|
||||
<PreviewCardContent
|
||||
placement="bottom-start"
|
||||
popupClassName="min-w-[200px] max-w-[280px] p-3"
|
||||
>
|
||||
<div className="mb-2 system-sm-semibold text-text-accent">
|
||||
{label}
|
||||
</div>
|
||||
<ul className="flex flex-col gap-1.5 system-xs-regular text-text-secondary">
|
||||
{permissions.map(key => (
|
||||
<li key={key} className="flex items-start gap-2">
|
||||
<span
|
||||
aria-hidden
|
||||
className="mt-[7px] inline-block h-1 w-1 shrink-0 rounded-full bg-text-tertiary"
|
||||
/>
|
||||
<span>
|
||||
{t(`members.memberDetails.permissions.${key}`, {
|
||||
ns: 'common',
|
||||
defaultValue: key,
|
||||
})}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</PreviewCardContent>
|
||||
</PreviewCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(PermissionRoleChip)
|
||||
@ -0,0 +1,36 @@
|
||||
// TODO: replace with permissions fetched from the permissions API once available.
|
||||
// Mock mapping from a workspace role key to the list of i18n keys describing
|
||||
// what permission points that role grants.
|
||||
export const ROLE_PERMISSION_KEYS: Record<string, string[]> = {
|
||||
owner: [
|
||||
'inviteMembers',
|
||||
'removeMembers',
|
||||
'assignRoles',
|
||||
'workspaceSettings',
|
||||
'manageBilling',
|
||||
'transferOwnership',
|
||||
],
|
||||
admin: [
|
||||
'inviteMembers',
|
||||
'removeMembers',
|
||||
'assignRoles',
|
||||
'workspaceSettings',
|
||||
'manageBilling',
|
||||
],
|
||||
editor: [
|
||||
'createApps',
|
||||
'editApps',
|
||||
'createDatasets',
|
||||
'editDatasets',
|
||||
],
|
||||
dataset_operator: [
|
||||
'manageDatasets',
|
||||
],
|
||||
normal: [
|
||||
'useApps',
|
||||
],
|
||||
}
|
||||
|
||||
export const getRolePermissionKeys = (roleKey: string): string[] => {
|
||||
return ROLE_PERMISSION_KEYS[roleKey] ?? []
|
||||
}
|
||||
@ -0,0 +1,117 @@
|
||||
'use client'
|
||||
import type { KeyboardEvent } from 'react'
|
||||
import type { Member } from '@/models/common'
|
||||
import { Avatar } from '@langgenius/dify-ui/avatar'
|
||||
import { memo, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import MemberMenu from './member-menu'
|
||||
import RoleBadges from './role-badges'
|
||||
|
||||
type MemberRowProps = {
|
||||
member: Member
|
||||
roleLabel: string
|
||||
isCurrentUser: boolean
|
||||
canManage: boolean
|
||||
operatorRole: string
|
||||
canTransferOwnership: boolean
|
||||
onOpenDetails: (member: Member) => void
|
||||
onOperate: () => void
|
||||
onTransferOwnership: () => void
|
||||
}
|
||||
|
||||
const MemberRow = ({
|
||||
member,
|
||||
roleLabel,
|
||||
isCurrentUser,
|
||||
canManage,
|
||||
operatorRole,
|
||||
canTransferOwnership,
|
||||
onOpenDetails,
|
||||
onOperate,
|
||||
onTransferOwnership,
|
||||
}: MemberRowProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||
|
||||
const openDetails = useCallback(() => {
|
||||
onOpenDetails(member)
|
||||
}, [member, onOpenDetails])
|
||||
|
||||
const handleRowKeyDown = useCallback((e: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
openDetails()
|
||||
}
|
||||
}, [openDetails])
|
||||
|
||||
const stopPropagationOnClick = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
}, [])
|
||||
|
||||
const stopPropagationOnKeyDown = useCallback((e: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key === 'Enter' || e.key === ' ')
|
||||
e.stopPropagation()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
data-testid={`member-row-${member.id}`}
|
||||
aria-label={t('members.memberDetails.openAria', {
|
||||
ns: 'common',
|
||||
name: member.name,
|
||||
defaultValue: 'Open member details for {{name}}',
|
||||
})}
|
||||
className="flex cursor-pointer border-b border-divider-subtle hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:outline-hidden"
|
||||
onClick={openDetails}
|
||||
onKeyDown={handleRowKeyDown}
|
||||
>
|
||||
<div className="flex grow items-center px-3 py-2">
|
||||
<Avatar avatar={member.avatar_url} size="sm" className="mr-2" name={member.name} />
|
||||
<div className="">
|
||||
<div className="system-sm-medium text-text-secondary">
|
||||
{member.name}
|
||||
{member.status === 'pending' && (
|
||||
<span className="ml-1 system-xs-medium text-text-warning">
|
||||
{t('members.pending', { ns: 'common' })}
|
||||
</span>
|
||||
)}
|
||||
{isCurrentUser && (
|
||||
<span className="system-xs-regular text-text-tertiary">
|
||||
{t('members.you', { ns: 'common' })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="system-xs-regular text-text-tertiary">{member.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-[120px] shrink-0 items-center py-2 system-sm-regular text-text-secondary">
|
||||
{formatTimeFromNow(Number((member.last_active_at || member.created_at)) * 1000)}
|
||||
</div>
|
||||
<div
|
||||
className="flex w-[215px] shrink-0 items-center gap-2 px-3"
|
||||
onClick={stopPropagationOnClick}
|
||||
onKeyDown={stopPropagationOnKeyDown}
|
||||
role="presentation"
|
||||
>
|
||||
<RoleBadges
|
||||
className="grow"
|
||||
roles={[roleLabel]}
|
||||
/>
|
||||
{canManage && (
|
||||
<MemberMenu
|
||||
member={member}
|
||||
operatorRole={operatorRole}
|
||||
canTransferOwnership={canTransferOwnership}
|
||||
onOperate={onOperate}
|
||||
onTransferOwnership={onTransferOwnership}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(MemberRow)
|
||||
@ -244,6 +244,24 @@
|
||||
"members.invitedAsRole": "Invited as {{role}} user",
|
||||
"members.lastActive": "LAST ACTIVE",
|
||||
"members.memberActions": "Member actions",
|
||||
"members.memberDetails.assign": "Assign",
|
||||
"members.memberDetails.assignedRoles": "Assigned Roles",
|
||||
"members.memberDetails.generalGroup": "GENERAL",
|
||||
"members.memberDetails.openAria": "Open member details for {{name}}",
|
||||
"members.memberDetails.permissions.assignRoles": "Assign roles",
|
||||
"members.memberDetails.permissions.createApps": "Create apps",
|
||||
"members.memberDetails.permissions.createDatasets": "Create knowledge",
|
||||
"members.memberDetails.permissions.editApps": "Edit apps",
|
||||
"members.memberDetails.permissions.editDatasets": "Edit knowledge",
|
||||
"members.memberDetails.permissions.inviteMembers": "Invite members",
|
||||
"members.memberDetails.permissions.manageBilling": "Manage billing",
|
||||
"members.memberDetails.permissions.manageDatasets": "Manage knowledge",
|
||||
"members.memberDetails.permissions.removeMembers": "Remove members",
|
||||
"members.memberDetails.permissions.transferOwnership": "Transfer ownership",
|
||||
"members.memberDetails.permissions.useApps": "Use apps",
|
||||
"members.memberDetails.permissions.workspaceSettings": "Workspace settings",
|
||||
"members.memberDetails.removeRoleAria": "Remove {{role}} role",
|
||||
"members.memberDetails.title": "Member Details",
|
||||
"members.name": "NAME",
|
||||
"members.normal": "Normal",
|
||||
"members.normalTip": "Only can use apps, can not build apps",
|
||||
|
||||
@ -244,6 +244,24 @@
|
||||
"members.invitedAsRole": "邀请为{{role}}用户",
|
||||
"members.lastActive": "上次活动时间",
|
||||
"members.memberActions": "成员操作",
|
||||
"members.memberDetails.assign": "分配",
|
||||
"members.memberDetails.assignedRoles": "已分配角色",
|
||||
"members.memberDetails.generalGroup": "通用角色",
|
||||
"members.memberDetails.openAria": "打开 {{name}} 的成员详情",
|
||||
"members.memberDetails.permissions.assignRoles": "分配角色",
|
||||
"members.memberDetails.permissions.createApps": "创建应用",
|
||||
"members.memberDetails.permissions.createDatasets": "创建知识库",
|
||||
"members.memberDetails.permissions.editApps": "编辑应用",
|
||||
"members.memberDetails.permissions.editDatasets": "编辑知识库",
|
||||
"members.memberDetails.permissions.inviteMembers": "邀请成员",
|
||||
"members.memberDetails.permissions.manageBilling": "管理订阅",
|
||||
"members.memberDetails.permissions.manageDatasets": "管理知识库",
|
||||
"members.memberDetails.permissions.removeMembers": "移除成员",
|
||||
"members.memberDetails.permissions.transferOwnership": "转移所有权",
|
||||
"members.memberDetails.permissions.useApps": "使用应用",
|
||||
"members.memberDetails.permissions.workspaceSettings": "工作空间设置",
|
||||
"members.memberDetails.removeRoleAria": "移除 {{role}} 角色",
|
||||
"members.memberDetails.title": "成员详情",
|
||||
"members.name": "姓名",
|
||||
"members.normal": "成员",
|
||||
"members.normalTip": "只能使用应用程序,不能建立应用程序",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user