mirror of
https://github.com/langgenius/dify.git
synced 2026-05-13 08:57:28 +08:00
209 lines
8.9 KiB
TypeScript
209 lines
8.9 KiB
TypeScript
'use client'
|
|
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 { useCallback, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { NUM_INFINITE } from '@/app/components/billing/config'
|
|
import { Plan } from '@/app/components/billing/type'
|
|
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 { LanguagesSupported } from '@/i18n-config/language'
|
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
|
import { useMembers } from '@/service/use-common'
|
|
import EditWorkspaceModal from './edit-workspace-modal'
|
|
import InviteButton from './invite-button'
|
|
import InviteModal from './invite-modal'
|
|
import InvitedModal from './invited-modal'
|
|
import MemberDetailsModal from './member-details-modal'
|
|
import MemberRow from './member-row'
|
|
import TransferOwnershipModal from './transfer-ownership-modal'
|
|
|
|
const MembersPage = () => {
|
|
const { t } = useTranslation()
|
|
const RoleMap = {
|
|
owner: t('members.owner', { ns: 'common' }),
|
|
admin: t('members.admin', { ns: 'common' }),
|
|
editor: t('members.editor', { ns: 'common' }),
|
|
dataset_operator: t('members.datasetOperator', { ns: 'common' }),
|
|
normal: t('members.normal', { ns: 'common' }),
|
|
}
|
|
const locale = useLocale()
|
|
|
|
const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager } = useAppContext()
|
|
const { data, refetch } = useMembers()
|
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
|
const [inviteModalVisible, setInviteModalVisible] = useState(false)
|
|
const [invitationResults, setInvitationResults] = useState<InvitationResult[]>([])
|
|
const [invitedModalVisible, setInvitedModalVisible] = useState(false)
|
|
const accounts = data?.accounts || []
|
|
const { plan, enableBilling, isAllowTransferWorkspace } = useProviderContext()
|
|
const isNotUnlimitedMemberPlan = enableBilling && plan.type !== Plan.team && plan.type !== Plan.enterprise
|
|
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 (
|
|
<>
|
|
<div className="flex flex-col">
|
|
<div className="mb-4 flex items-center gap-3 rounded-xl border-t-[0.5px] border-l-[0.5px] border-divider-subtle bg-linear-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1 p-3 pr-5">
|
|
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-components-icon-bg-blue-solid text-[20px]">
|
|
<span className="bg-linear-to-r from-components-avatar-shape-fill-stop-0 to-components-avatar-shape-fill-stop-100 bg-clip-text font-semibold text-shadow-shadow-1 uppercase opacity-90">
|
|
{currentWorkspace?.name[0]?.toLocaleUpperCase()}
|
|
</span>
|
|
</div>
|
|
<div className="grow">
|
|
<div className="flex items-center gap-1 system-md-semibold text-text-secondary">
|
|
<span>{currentWorkspace?.name}</span>
|
|
{isCurrentWorkspaceOwner && (
|
|
<span>
|
|
<Tooltip>
|
|
<TooltipTrigger
|
|
render={(
|
|
<div
|
|
className="cursor-pointer rounded-md p-1 hover:bg-black/5"
|
|
onClick={() => {
|
|
setEditWorkspaceModalVisible(true)
|
|
}}
|
|
>
|
|
<div
|
|
data-testid="edit-workspace-pencil"
|
|
className="i-ri-pencil-line h-4 w-4 text-text-tertiary"
|
|
/>
|
|
</div>
|
|
)}
|
|
/>
|
|
<TooltipContent>
|
|
{t('account.editWorkspaceInfo', { ns: 'common' })}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="mt-1 system-xs-medium text-text-tertiary">
|
|
{enableBilling && isNotUnlimitedMemberPlan
|
|
? (
|
|
<div className="flex space-x-1">
|
|
<div>
|
|
{t('plansCommon.member', { ns: 'billing' })}
|
|
{locale !== LanguagesSupported[1] && accounts.length > 1 && 's'}
|
|
</div>
|
|
<div className="">{accounts.length}</div>
|
|
<div>/</div>
|
|
<div>{plan.total.teamMembers === NUM_INFINITE ? t('plansCommon.unlimited', { ns: 'billing' }) : plan.total.teamMembers}</div>
|
|
</div>
|
|
)
|
|
: (
|
|
<div className="flex space-x-1">
|
|
<div>{accounts.length}</div>
|
|
<div>
|
|
{t('plansCommon.memberAfter', { ns: 'billing' })}
|
|
{locale !== LanguagesSupported[1] && accounts.length > 1 && 's'}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
</div>
|
|
{isMemberFull && (
|
|
<UpgradeBtn className="mr-2" loc="member-invite" />
|
|
)}
|
|
<div className="shrink-0">
|
|
{isCurrentWorkspaceManager && <InviteButton disabled={isMemberFull} onClick={() => setInviteModalVisible(true)} />}
|
|
</div>
|
|
</div>
|
|
<div className="overflow-visible lg:overflow-visible">
|
|
<div className="flex min-w-[480px] items-center border-b border-divider-regular py-[7px]">
|
|
<div className="grow px-3 system-xs-medium-uppercase text-text-tertiary">{t('members.name', { ns: 'common' })}</div>
|
|
<div className="w-[120px] shrink-0 system-xs-medium-uppercase text-text-tertiary">{t('members.lastActive', { ns: 'common' })}</div>
|
|
<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 => (
|
|
<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>
|
|
{
|
|
inviteModalVisible && (
|
|
<InviteModal
|
|
isEmailSetup={systemFeatures.is_email_setup}
|
|
onCancel={() => setInviteModalVisible(false)}
|
|
onSend={(invitationResults) => {
|
|
setInvitedModalVisible(true)
|
|
setInvitationResults(invitationResults)
|
|
refetch()
|
|
}}
|
|
/>
|
|
)
|
|
}
|
|
{
|
|
invitedModalVisible && (
|
|
<InvitedModal
|
|
invitationResults={invitationResults}
|
|
onCancel={() => setInvitedModalVisible(false)}
|
|
/>
|
|
)
|
|
}
|
|
{
|
|
editWorkspaceModalVisible && (
|
|
<EditWorkspaceModal
|
|
onCancel={() => setEditWorkspaceModalVisible(false)}
|
|
/>
|
|
)
|
|
}
|
|
{showTransferOwnershipModal && (
|
|
<TransferOwnershipModal
|
|
show={showTransferOwnershipModal}
|
|
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}
|
|
/>
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default MembersPage
|