mirror of
https://github.com/langgenius/dify.git
synced 2026-01-08 07:14:14 +00:00
Compare commits
8 Commits
feat/detec
...
feat/owner
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54fb94cdf5 | ||
|
|
b1b4dfb5fb | ||
|
|
e643899a58 | ||
|
|
920db45216 | ||
|
|
7bf8186f6f | ||
|
|
b83dac92bd | ||
|
|
86caad17c8 | ||
|
|
f0955ab438 |
@@ -99,7 +99,8 @@ export type CurrentPlanInfoBackend = {
|
|||||||
workspace_members: {
|
workspace_members: {
|
||||||
size: number
|
size: number
|
||||||
limit: number
|
limit: number
|
||||||
}
|
},
|
||||||
|
is_allow_transfer_workspace: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SubscriptionItem = {
|
export type SubscriptionItem = {
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import InviteModal from './invite-modal'
|
import InviteModal from './invite-modal'
|
||||||
import InvitedModal from './invited-modal'
|
import InvitedModal from './invited-modal'
|
||||||
import EditWorkspaceModal from './edit-workspace-modal'
|
import EditWorkspaceModal from './edit-workspace-modal'
|
||||||
|
import TransferOwnershipModal from './transfer-ownership-modal'
|
||||||
import Operation from './operation'
|
import Operation from './operation'
|
||||||
|
import TransferOwnership from './operation/transfer-ownership'
|
||||||
import { fetchMembers } from '@/service/common'
|
import { fetchMembers } from '@/service/common'
|
||||||
import I18n from '@/context/i18n'
|
import I18n from '@/context/i18n'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
@@ -52,10 +54,11 @@ const MembersPage = () => {
|
|||||||
const [invitationResults, setInvitationResults] = useState<InvitationResult[]>([])
|
const [invitationResults, setInvitationResults] = useState<InvitationResult[]>([])
|
||||||
const [invitedModalVisible, setInvitedModalVisible] = useState(false)
|
const [invitedModalVisible, setInvitedModalVisible] = useState(false)
|
||||||
const accounts = data?.accounts || []
|
const accounts = data?.accounts || []
|
||||||
const { plan, enableBilling } = useProviderContext()
|
const { plan, enableBilling, isAllowTransferWorkspace } = useProviderContext()
|
||||||
const isNotUnlimitedMemberPlan = enableBilling && plan.type !== Plan.team && plan.type !== Plan.enterprise
|
const isNotUnlimitedMemberPlan = enableBilling && plan.type !== Plan.team && plan.type !== Plan.enterprise
|
||||||
const isMemberFull = enableBilling && isNotUnlimitedMemberPlan && accounts.length >= plan.total.teamMembers
|
const isMemberFull = enableBilling && isNotUnlimitedMemberPlan && accounts.length >= plan.total.teamMembers
|
||||||
const [editWorkspaceModalVisible, setEditWorkspaceModalVisible] = useState(false)
|
const [editWorkspaceModalVisible, setEditWorkspaceModalVisible] = useState(false)
|
||||||
|
const [showTransferOwnershipModal, setShowTransferOwnershipModal] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -133,11 +136,18 @@ const MembersPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className='system-sm-regular flex w-[104px] shrink-0 items-center py-2 text-text-secondary'>{dayjs(Number((account.last_active_at || account.created_at)) * 1000).locale(locale === 'zh-Hans' ? 'zh-cn' : 'en').fromNow()}</div>
|
<div className='system-sm-regular flex w-[104px] shrink-0 items-center py-2 text-text-secondary'>{dayjs(Number((account.last_active_at || account.created_at)) * 1000).locale(locale === 'zh-Hans' ? 'zh-cn' : 'en').fromNow()}</div>
|
||||||
<div className='flex w-[96px] shrink-0 items-center'>
|
<div className='flex w-[96px] shrink-0 items-center'>
|
||||||
{
|
{isCurrentWorkspaceOwner && account.role === 'owner' && isAllowTransferWorkspace && (
|
||||||
isCurrentWorkspaceOwner && account.role !== 'owner'
|
<TransferOwnership onOperate={() => setShowTransferOwnershipModal(true)}></TransferOwnership>
|
||||||
? <Operation member={account} operatorRole={currentWorkspace.role} onOperate={mutate} />
|
)}
|
||||||
: <div className='system-sm-regular px-3 text-text-secondary'>{RoleMap[account.role] || RoleMap.normal}</div>
|
{isCurrentWorkspaceOwner && account.role === 'owner' && !isAllowTransferWorkspace && (
|
||||||
}
|
<div className='system-sm-regular px-3 text-text-secondary'>{RoleMap[account.role] || RoleMap.normal}</div>
|
||||||
|
)}
|
||||||
|
{isCurrentWorkspaceOwner && account.role !== 'owner' && (
|
||||||
|
<Operation member={account} operatorRole={currentWorkspace.role} onOperate={mutate} />
|
||||||
|
)}
|
||||||
|
{!isCurrentWorkspaceOwner && (
|
||||||
|
<div className='system-sm-regular px-3 text-text-secondary'>{RoleMap[account.role] || RoleMap.normal}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
@@ -173,6 +183,12 @@ const MembersPage = () => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{showTransferOwnershipModal && (
|
||||||
|
<TransferOwnershipModal
|
||||||
|
show={showTransferOwnershipModal}
|
||||||
|
onClose={() => setShowTransferOwnershipModal(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
'use client'
|
||||||
|
import { Fragment } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import {
|
||||||
|
RiArrowDownSLine,
|
||||||
|
} from '@remixicon/react'
|
||||||
|
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onOperate: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const TransferOwnership = ({ onOperate }: Props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu as="div" className="relative h-full w-full">
|
||||||
|
{
|
||||||
|
({ open }) => (
|
||||||
|
<>
|
||||||
|
<MenuButton className={cn('system-sm-regular group flex h-full w-full cursor-pointer items-center justify-between px-3 text-text-secondary hover:bg-state-base-hover', open && 'bg-state-base-hover')}>
|
||||||
|
{t('common.members.owner')}
|
||||||
|
<RiArrowDownSLine className={cn('h-4 w-4 group-hover:block', open ? 'block' : 'hidden')} />
|
||||||
|
</MenuButton>
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<MenuItems
|
||||||
|
className={cn('absolute right-0 top-[52px] z-10 origin-top-right rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm')}
|
||||||
|
>
|
||||||
|
<div className="p-1">
|
||||||
|
<MenuItem>
|
||||||
|
<div className='flex cursor-pointer rounded-lg px-3 py-2 hover:bg-state-base-hover' onClick={onOperate}>
|
||||||
|
<div className='system-md-regular whitespace-nowrap text-text-secondary'>{t('common.members.transferOwnership')}</div>
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
</div>
|
||||||
|
</MenuItems>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Menu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TransferOwnership
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import { useContext } from 'use-context-selector'
|
||||||
|
import { RiCloseLine } from '@remixicon/react'
|
||||||
|
import { useAppContext } from '@/context/app-context'
|
||||||
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
|
import Modal from '@/app/components/base/modal'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import Input from '@/app/components/base/input'
|
||||||
|
import MemberSelector from './member-selector'
|
||||||
|
import {
|
||||||
|
ownershipTransfer,
|
||||||
|
sendOwnerEmail,
|
||||||
|
verifyOwnerEmail,
|
||||||
|
} from '@/service/common'
|
||||||
|
import { noop } from 'lodash-es'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
show: boolean
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
enum STEP {
|
||||||
|
start = 'start',
|
||||||
|
verify = 'verify',
|
||||||
|
transfer = 'transfer',
|
||||||
|
}
|
||||||
|
|
||||||
|
const TransferOwnershipModal = ({ onClose, show }: Props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { notify } = useContext(ToastContext)
|
||||||
|
const { currentWorkspace, userProfile } = useAppContext()
|
||||||
|
const [step, setStep] = useState<STEP>(STEP.start)
|
||||||
|
const [code, setCode] = useState<string>('')
|
||||||
|
const [time, setTime] = useState<number>(0)
|
||||||
|
const [stepToken, setStepToken] = useState<string>('')
|
||||||
|
const [newOwner, setNewOwner] = useState<string>('')
|
||||||
|
const [isTransfer, setIsTransfer] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const startCount = () => {
|
||||||
|
setTime(60)
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setTime((prev) => {
|
||||||
|
if (prev <= 0) {
|
||||||
|
clearInterval(timer)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return prev - 1
|
||||||
|
})
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendEmail = async () => {
|
||||||
|
try {
|
||||||
|
const res = await sendOwnerEmail({
|
||||||
|
language: userProfile.interface_language,
|
||||||
|
})
|
||||||
|
startCount()
|
||||||
|
if (res.data)
|
||||||
|
setStepToken(res.data)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
notify({
|
||||||
|
type: 'error',
|
||||||
|
message: `Error sending verification code: ${error ? (error as any).message : ''}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyEmailAddress = async (code: string, token: string, callback?: () => void) => {
|
||||||
|
try {
|
||||||
|
const res = await verifyOwnerEmail({
|
||||||
|
code,
|
||||||
|
token,
|
||||||
|
})
|
||||||
|
if (res.is_valid) {
|
||||||
|
setStepToken(res.token)
|
||||||
|
callback?.()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
notify({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Verifying email failed',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
notify({
|
||||||
|
type: 'error',
|
||||||
|
message: `Error verifying email: ${error ? (error as any).message : ''}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendCodeToOriginEmail = async () => {
|
||||||
|
await sendEmail()
|
||||||
|
setStep(STEP.verify)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleVerifyOriginEmail = async () => {
|
||||||
|
await verifyEmailAddress(code, stepToken, () => setStep(STEP.transfer))
|
||||||
|
setCode('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTransfer = async () => {
|
||||||
|
setIsTransfer(true)
|
||||||
|
try {
|
||||||
|
await ownershipTransfer(
|
||||||
|
newOwner,
|
||||||
|
{
|
||||||
|
token: stepToken,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
globalThis.location.reload()
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
notify({
|
||||||
|
type: 'error',
|
||||||
|
message: `Error ownership transfer: ${error ? (error as any).message : ''}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setIsTransfer(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isShow={show}
|
||||||
|
onClose={noop}
|
||||||
|
className='!w-[420px] !p-6'
|
||||||
|
>
|
||||||
|
<div className='absolute right-5 top-5 cursor-pointer p-1.5' onClick={onClose}>
|
||||||
|
<RiCloseLine className='h-5 w-5 text-text-tertiary' />
|
||||||
|
</div>
|
||||||
|
{step === STEP.start && (
|
||||||
|
<>
|
||||||
|
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.members.transferModal.title')}</div>
|
||||||
|
<div className='space-y-1 pb-2 pt-1'>
|
||||||
|
<div className='body-md-medium text-text-destructive'>{t('common.members.transferModal.warning', { workspace: currentWorkspace.name.replace(/'/g, '’') })}</div>
|
||||||
|
<div className='body-md-regular text-text-secondary'>{t('common.members.transferModal.warningTip')}</div>
|
||||||
|
<div className='body-md-regular text-text-secondary'>
|
||||||
|
<Trans
|
||||||
|
i18nKey="common.members.transferModal.sendTip"
|
||||||
|
components={{ email: <span className='body-md-medium text-text-primary'></span> }}
|
||||||
|
values={{ email: userProfile.email }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='pt-3'></div>
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<Button
|
||||||
|
className='!w-full'
|
||||||
|
variant='primary'
|
||||||
|
onClick={sendCodeToOriginEmail}
|
||||||
|
>
|
||||||
|
{t('common.members.transferModal.sendVerifyCode')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className='!w-full'
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
{t('common.operation.cancel')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{step === STEP.verify && (
|
||||||
|
<>
|
||||||
|
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.members.transferModal.verifyEmail')}</div>
|
||||||
|
<div className='pb-2 pt-1'>
|
||||||
|
<div className='body-md-regular text-text-secondary'>
|
||||||
|
<Trans
|
||||||
|
i18nKey="common.members.transferModal.verifyContent"
|
||||||
|
components={{ email: <span className='body-md-medium text-text-primary'></span> }}
|
||||||
|
values={{ email: userProfile.email }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='body-md-regular text-text-secondary'>{t('common.members.transferModal.verifyContent2')}</div>
|
||||||
|
</div>
|
||||||
|
<div className='pt-3'>
|
||||||
|
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>{t('common.members.transferModal.codeLabel')}</div>
|
||||||
|
<Input
|
||||||
|
className='!w-full'
|
||||||
|
placeholder={t('common.members.transferModal.codePlaceholder')}
|
||||||
|
value={code}
|
||||||
|
onChange={e => setCode(e.target.value)}
|
||||||
|
maxLength={6}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='mt-3 space-y-2'>
|
||||||
|
<Button
|
||||||
|
disabled={code.length !== 6}
|
||||||
|
className='!w-full'
|
||||||
|
variant='primary'
|
||||||
|
onClick={handleVerifyOriginEmail}
|
||||||
|
>
|
||||||
|
{t('common.members.transferModal.continue')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className='!w-full'
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
{t('common.operation.cancel')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className='system-xs-regular mt-3 flex items-center gap-1 text-text-tertiary'>
|
||||||
|
<span>{t('common.members.transferModal.resendTip')}</span>
|
||||||
|
{time > 0 && (
|
||||||
|
<span>{t('common.members.transferModal.resendCount', { count: time })}</span>
|
||||||
|
)}
|
||||||
|
{!time && (
|
||||||
|
<span onClick={sendCodeToOriginEmail} className='system-xs-medium cursor-pointer text-text-accent-secondary'>{t('common.members.transferModal.resend')}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{step === STEP.transfer && (
|
||||||
|
<>
|
||||||
|
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.members.transferModal.title')}</div>
|
||||||
|
<div className='space-y-1 pb-2 pt-1'>
|
||||||
|
<div className='body-md-medium text-text-destructive'>{t('common.members.transferModal.warning', { workspace: currentWorkspace.name.replace(/'/g, '’') })}</div>
|
||||||
|
<div className='body-md-regular text-text-secondary'>{t('common.members.transferModal.warningTip')}</div>
|
||||||
|
</div>
|
||||||
|
<div className='pt-3'>
|
||||||
|
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>{t('common.members.transferModal.transferLabel')}</div>
|
||||||
|
<MemberSelector
|
||||||
|
exclude={[userProfile.id]}
|
||||||
|
value={newOwner}
|
||||||
|
onSelect={setNewOwner}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='mt-4 space-y-2'>
|
||||||
|
<Button
|
||||||
|
disabled={!newOwner || isTransfer}
|
||||||
|
className='!w-full'
|
||||||
|
variant='warning'
|
||||||
|
onClick={handleTransfer}
|
||||||
|
>
|
||||||
|
{t('common.members.transferModal.transfer')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className='!w-full'
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
{t('common.operation.cancel')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TransferOwnershipModal
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React, { useMemo, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import useSWR from 'swr'
|
||||||
|
import {
|
||||||
|
RiArrowDownSLine,
|
||||||
|
} from '@remixicon/react'
|
||||||
|
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||||
|
import Avatar from '@/app/components/base/avatar'
|
||||||
|
import Input from '@/app/components/base/input'
|
||||||
|
import { fetchMembers } from '@/service/common'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value?: any
|
||||||
|
onSelect: (value: any) => void
|
||||||
|
exclude?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const MemberSelector: FC<Props> = ({
|
||||||
|
value,
|
||||||
|
onSelect,
|
||||||
|
exclude = [],
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [searchValue, setSearchValue] = useState('')
|
||||||
|
|
||||||
|
const { data } = useSWR(
|
||||||
|
{
|
||||||
|
url: '/workspaces/current/members',
|
||||||
|
params: {},
|
||||||
|
},
|
||||||
|
fetchMembers,
|
||||||
|
)
|
||||||
|
|
||||||
|
const currentValue = useMemo(() => {
|
||||||
|
if (!data?.accounts) return null
|
||||||
|
const accounts = data.accounts || []
|
||||||
|
if (!value) return null
|
||||||
|
return accounts.find(account => account.id === value)
|
||||||
|
}, [data, value])
|
||||||
|
|
||||||
|
const filteredList = useMemo(() => {
|
||||||
|
if (!data?.accounts) return []
|
||||||
|
const accounts = data.accounts
|
||||||
|
if (!searchValue) return accounts.filter(account => !exclude.includes(account.id))
|
||||||
|
return accounts.filter((account) => {
|
||||||
|
const name = account.name || ''
|
||||||
|
const email = account.email || ''
|
||||||
|
return name.toLowerCase().includes(searchValue.toLowerCase())
|
||||||
|
|| email.toLowerCase().includes(searchValue.toLowerCase())
|
||||||
|
}).filter(account => !exclude.includes(account.id))
|
||||||
|
}, [data, searchValue, exclude])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PortalToFollowElem
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
placement='bottom'
|
||||||
|
offset={4}
|
||||||
|
>
|
||||||
|
<PortalToFollowElemTrigger
|
||||||
|
className='w-full'
|
||||||
|
onClick={() => setOpen(v => !v)}
|
||||||
|
>
|
||||||
|
<div className={cn('group flex cursor-pointer items-center gap-1.5 rounded-lg bg-components-input-bg-normal px-2 py-1 hover:bg-state-base-hover-alt', open && 'bg-state-base-hover-alt')}>
|
||||||
|
{!currentValue && (
|
||||||
|
<div className='system-sm-regular grow p-1 text-components-input-text-placeholder'>{t('common.members.transferModal.transferPlaceholder')}</div>
|
||||||
|
)}
|
||||||
|
{currentValue && (
|
||||||
|
<>
|
||||||
|
<Avatar avatar={currentValue.avatar_url} size={24} name={currentValue.name} />
|
||||||
|
<div className='system-sm-medium grow truncate text-text-secondary'>{currentValue.name}</div>
|
||||||
|
<div className='system-xs-regular text-text-quaternary'>{currentValue.email}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<RiArrowDownSLine className={cn('h-4 w-4 text-text-quaternary group-hover:text-text-secondary', open && 'text-text-secondary')} />
|
||||||
|
</div>
|
||||||
|
</PortalToFollowElemTrigger>
|
||||||
|
<PortalToFollowElemContent className='z-[1000]'>
|
||||||
|
<div className='min-w-[372px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm'>
|
||||||
|
<div className='p-2 pb-1'>
|
||||||
|
<Input
|
||||||
|
showLeftIcon
|
||||||
|
value={searchValue}
|
||||||
|
onChange={e => setSearchValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='p-1'>
|
||||||
|
{filteredList.map(account => (
|
||||||
|
<div
|
||||||
|
key={account.id}
|
||||||
|
className='flex cursor-pointer items-center gap-2 rounded-lg py-1 pl-2 pr-3 hover:bg-state-base-hover'
|
||||||
|
onClick={() => {
|
||||||
|
onSelect(account.id)
|
||||||
|
setOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar avatar={account.avatar_url} size={24} name={account.name} />
|
||||||
|
<div className='system-sm-medium grow truncate text-text-secondary'>{account.name}</div>
|
||||||
|
<div className='system-xs-regular text-text-quaternary'>{account.email}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PortalToFollowElemContent>
|
||||||
|
</PortalToFollowElem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default MemberSelector
|
||||||
@@ -56,6 +56,7 @@ type ProviderContextState = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
refreshLicenseLimit: () => void
|
refreshLicenseLimit: () => void
|
||||||
|
isAllowTransferWorkspace: boolean
|
||||||
}
|
}
|
||||||
const ProviderContext = createContext<ProviderContextState>({
|
const ProviderContext = createContext<ProviderContextState>({
|
||||||
modelProviders: [],
|
modelProviders: [],
|
||||||
@@ -97,6 +98,7 @@ const ProviderContext = createContext<ProviderContextState>({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
refreshLicenseLimit: noop,
|
refreshLicenseLimit: noop,
|
||||||
|
isAllowTransferWorkspace: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const useProviderContext = () => useContext(ProviderContext)
|
export const useProviderContext = () => useContext(ProviderContext)
|
||||||
@@ -134,6 +136,7 @@ export const ProviderContextProvider = ({
|
|||||||
const [enableEducationPlan, setEnableEducationPlan] = useState(false)
|
const [enableEducationPlan, setEnableEducationPlan] = useState(false)
|
||||||
const [isEducationWorkspace, setIsEducationWorkspace] = useState(false)
|
const [isEducationWorkspace, setIsEducationWorkspace] = useState(false)
|
||||||
const { data: isEducationAccount } = useEducationStatus(!enableEducationPlan)
|
const { data: isEducationAccount } = useEducationStatus(!enableEducationPlan)
|
||||||
|
const [isAllowTransferWorkspace, setIsAllowTransferWorkspace] = useState(false)
|
||||||
|
|
||||||
const fetchPlan = async () => {
|
const fetchPlan = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -162,6 +165,8 @@ export const ProviderContextProvider = ({
|
|||||||
setWebappCopyrightEnabled(true)
|
setWebappCopyrightEnabled(true)
|
||||||
if (data.workspace_members)
|
if (data.workspace_members)
|
||||||
setLicenseLimit({ workspace_members: data.workspace_members })
|
setLicenseLimit({ workspace_members: data.workspace_members })
|
||||||
|
if (data.is_allow_transfer_workspace)
|
||||||
|
setIsAllowTransferWorkspace(data.is_allow_transfer_workspace)
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error('Failed to fetch plan info:', error)
|
console.error('Failed to fetch plan info:', error)
|
||||||
@@ -222,6 +227,7 @@ export const ProviderContextProvider = ({
|
|||||||
webappCopyrightEnabled,
|
webappCopyrightEnabled,
|
||||||
licenseLimit,
|
licenseLimit,
|
||||||
refreshLicenseLimit: fetchPlan,
|
refreshLicenseLimit: fetchPlan,
|
||||||
|
isAllowTransferWorkspace,
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</ProviderContext.Provider>
|
</ProviderContext.Provider>
|
||||||
|
|||||||
@@ -274,6 +274,26 @@ const translation = {
|
|||||||
disInvite: 'Cancel the invitation',
|
disInvite: 'Cancel the invitation',
|
||||||
deleteMember: 'Delete Member',
|
deleteMember: 'Delete Member',
|
||||||
you: '(You)',
|
you: '(You)',
|
||||||
|
transferOwnership: 'Transfer Ownership',
|
||||||
|
transferModal: {
|
||||||
|
title: 'Transfer workspace ownership',
|
||||||
|
warning: 'You\'re about to transfer ownership of “{{workspace}}”. This takes effect immediately and can\'t be undone.',
|
||||||
|
warningTip: 'You\'ll become an admin member, and the new owner will have full control.',
|
||||||
|
sendTip: 'If you continue, we\'ll send a verification code to <email>{{email}}</email> for re-authentication.',
|
||||||
|
verifyEmail: 'Verify your email',
|
||||||
|
verifyContent: 'Your current email is <email>{{email}}</email>.',
|
||||||
|
verifyContent2: 'We\'ll send a temporary verification code to this email for re-authentication.',
|
||||||
|
codeLabel: 'Verification code',
|
||||||
|
codePlaceholder: 'Paste the 6-digit code',
|
||||||
|
resendTip: 'Didn\'t receive a code?',
|
||||||
|
resendCount: 'Resend in {{count}}s',
|
||||||
|
resend: 'Resend',
|
||||||
|
transferLabel: 'Transfer workspace ownership to',
|
||||||
|
transferPlaceholder: 'Select a workspace member…',
|
||||||
|
sendVerifyCode: 'Send verification code',
|
||||||
|
continue: 'Continue',
|
||||||
|
transfer: 'Transfer workspace ownership',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
integrations: {
|
integrations: {
|
||||||
connected: 'Connected',
|
connected: 'Connected',
|
||||||
|
|||||||
@@ -275,6 +275,26 @@ const translation = {
|
|||||||
disInvite: '招待をキャンセル',
|
disInvite: '招待をキャンセル',
|
||||||
deleteMember: 'メンバーを削除',
|
deleteMember: 'メンバーを削除',
|
||||||
you: '(あなた)',
|
you: '(あなた)',
|
||||||
|
transferOwnership: '所有権の移転',
|
||||||
|
transferModal: {
|
||||||
|
title: 'ワークスペースの所有権を移する',
|
||||||
|
warning: '「{{workspace}}」の所有権を移しようとしています。この操作は即時に有効となり、元に戻すことはできません。',
|
||||||
|
warningTip: 'あなたは管理者メンバーになり、新しいオーナーがすべての権限を持つことになります。',
|
||||||
|
sendTip: '続行する場合は、本人確認のため <email>{{email}}</email> に認証コードを送信します。',
|
||||||
|
verifyEmail: '現在のメールアドレスを確認',
|
||||||
|
verifyContent: '現在のメールアドレスは <email>{{email}}</email>。',
|
||||||
|
verifyContent2: 'このメールアドレスに一時的な認証コードを送信し、再認証を行います。',
|
||||||
|
codeLabel: '認証コード',
|
||||||
|
codePlaceholder: '6 桁のコードを入力してください',
|
||||||
|
resendTip: '認証コードを受け取れない場合は、',
|
||||||
|
resendCount: '{{count}} 秒後に再送信',
|
||||||
|
resend: '認証コードを再送信',
|
||||||
|
transferLabel: 'ワークスペースの所有権を転移する相手は',
|
||||||
|
transferPlaceholder: 'メールアドレスを入力してください',
|
||||||
|
sendVerifyCode: '認証コードを送信',
|
||||||
|
continue: '続行する',
|
||||||
|
transfer: 'ワークスペースの所有権を移する',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
integrations: {
|
integrations: {
|
||||||
connected: '接続済み',
|
connected: '接続済み',
|
||||||
|
|||||||
@@ -274,6 +274,26 @@ const translation = {
|
|||||||
builderTip: '可以构建和编辑自己的应用程序',
|
builderTip: '可以构建和编辑自己的应用程序',
|
||||||
setBuilder: 'Set as builder(设置为构建器)',
|
setBuilder: 'Set as builder(设置为构建器)',
|
||||||
builder: '构建器',
|
builder: '构建器',
|
||||||
|
transferOwnership: '转移所有权',
|
||||||
|
transferModal: {
|
||||||
|
title: '转移工作空间所有权',
|
||||||
|
warning: '您即将转让“{{workspace}}”的所有权。此操作立即生效,且无法撤消。',
|
||||||
|
warningTip: '您将成为管理员成员,新所有者将拥有完全控制权。',
|
||||||
|
sendTip: '如果您继续,我们将向 <email>{{email}}</email> 发送验证码以进行重新验证。',
|
||||||
|
verifyEmail: '验证你的邮箱',
|
||||||
|
verifyContent: '您当前的电子邮件地址是 <email>{{email}}</email>。',
|
||||||
|
verifyContent2: '我们将向此电子邮件发送临时验证码,以便重新进行身份验证。',
|
||||||
|
codeLabel: '验证码',
|
||||||
|
codePlaceholder: '输入 6 位数字验证码',
|
||||||
|
resendTip: '没有收到验证码?',
|
||||||
|
resendCount: '请在 {{count}} 秒后重新发送',
|
||||||
|
resend: '重新发送',
|
||||||
|
transferLabel: '新所有者',
|
||||||
|
transferPlaceholder: '选择一个成员',
|
||||||
|
sendVerifyCode: '发送验证码',
|
||||||
|
continue: '继续',
|
||||||
|
transfer: '转移工作空间所有权',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
integrations: {
|
integrations: {
|
||||||
connected: '登录方式',
|
connected: '登录方式',
|
||||||
|
|||||||
@@ -131,6 +131,15 @@ export const deleteMemberOrCancelInvitation: Fetcher<CommonResponse, { url: stri
|
|||||||
return del<CommonResponse>(url)
|
return del<CommonResponse>(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const sendOwnerEmail = (body: { language?: string }) =>
|
||||||
|
post<CommonResponse & { data: string }>('/workspaces/current/members/send-owner-transfer-confirm-email', { body })
|
||||||
|
|
||||||
|
export const verifyOwnerEmail = (body: { code: string; token: string }) =>
|
||||||
|
post<CommonResponse & { is_valid: boolean; email: string; token: string }>('/workspaces/current/members/owner-transfer-check', { body })
|
||||||
|
|
||||||
|
export const ownershipTransfer = (memberID: string, body: { token: string }) =>
|
||||||
|
post<CommonResponse & { is_valid: boolean; email: string; token: string }>(`/workspaces/current/members/${memberID}/owner-transfer`, { body })
|
||||||
|
|
||||||
export const fetchFilePreview: Fetcher<{ content: string }, { fileID: string }> = ({ fileID }) => {
|
export const fetchFilePreview: Fetcher<{ content: string }, { fileID: string }> = ({ fileID }) => {
|
||||||
return get<{ content: string }>(`/files/${fileID}/preview`)
|
return get<{ content: string }>(`/files/${fileID}/preview`)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user