dify/web/app/components/header/account-setting/members-page/index.tsx

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