mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 04:36:31 +08:00
fix: transfer workspace dropdown not show (#35876)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
bb3de5dd32
commit
8b77ec7f31
@ -2579,11 +2579,8 @@
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 3
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/model-provider-page/declarations.ts": {
|
||||
|
||||
@ -183,18 +183,20 @@ describe('TransferOwnershipModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should show error when sending verification email fails', async () => {
|
||||
it('should not show a modal-level toast and should stay on start step when sending verification email fails', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(sendOwnerEmail).mockRejectedValue(new Error('network error'))
|
||||
renderModal()
|
||||
await user.click(screen.getByTestId('transfer-modal-send-code'))
|
||||
|
||||
// The base service layer surfaces the real backend error. The modal itself
|
||||
// must NOT show an additional toast (e.g. "Error sending verification code: undefined").
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'error',
|
||||
message: expect.stringContaining('network error'),
|
||||
}))
|
||||
expect(sendOwnerEmail).toHaveBeenCalled()
|
||||
})
|
||||
expect(mockNotify).not.toHaveBeenCalled()
|
||||
// Should remain on the start step instead of advancing to the verify step.
|
||||
expect(screen.getByTestId('transfer-modal-send-code')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show error when ownership transfer fails', async () => {
|
||||
@ -229,7 +231,7 @@ describe('TransferOwnershipModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should show fallback error prefix when sendOwnerEmail throws null', async () => {
|
||||
it('should swallow null rejection from sendOwnerEmail without showing a modal-level toast', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(sendOwnerEmail).mockRejectedValue(null)
|
||||
|
||||
@ -237,11 +239,10 @@ describe('TransferOwnershipModal', () => {
|
||||
await user.click(screen.getByTestId('transfer-modal-send-code'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'error',
|
||||
message: expect.stringContaining('Error sending verification code:'),
|
||||
}))
|
||||
expect(sendOwnerEmail).toHaveBeenCalled()
|
||||
})
|
||||
expect(mockNotify).not.toHaveBeenCalled()
|
||||
expect(screen.getByTestId('transfer-modal-send-code')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show fallback error prefix when verifyOwnerEmail throws null', async () => {
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { ownershipTransfer, sendOwnerEmail, verifyOwnerEmail } from '@/service/common'
|
||||
import MemberSelector from './member-selector'
|
||||
@ -52,15 +51,10 @@ const TransferOwnershipModal = ({ onClose, show }: Props) => {
|
||||
}, 1000))
|
||||
}
|
||||
const sendEmail = async () => {
|
||||
try {
|
||||
const res = await sendOwnerEmail({})
|
||||
startCount()
|
||||
if (res.data)
|
||||
setStepToken(res.data)
|
||||
}
|
||||
catch (error) {
|
||||
toast.error(`Error sending verification code: ${error ? (error as any).message : ''}`)
|
||||
}
|
||||
const res = await sendOwnerEmail({})
|
||||
startCount()
|
||||
if (res.data)
|
||||
setStepToken(res.data)
|
||||
}
|
||||
const verifyEmailAddress = async (code: string, token: string, callback?: () => void) => {
|
||||
try {
|
||||
@ -81,8 +75,13 @@ const TransferOwnershipModal = ({ onClose, show }: Props) => {
|
||||
}
|
||||
}
|
||||
const sendCodeToOriginEmail = async () => {
|
||||
await sendEmail()
|
||||
setStep(STEP.verify)
|
||||
try {
|
||||
await sendEmail()
|
||||
setStep(STEP.verify)
|
||||
}
|
||||
catch {
|
||||
// The base service layer already surfaces the backend error (e.g. rate-limit) as a toast.
|
||||
}
|
||||
}
|
||||
const handleVerifyOriginEmail = async () => {
|
||||
await verifyEmailAddress(code, stepToken, () => setStep(STEP.transfer))
|
||||
@ -104,85 +103,87 @@ const TransferOwnershipModal = ({ onClose, show }: Props) => {
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Modal isShow={show} onClose={noop} wrapperClassName="z-1002" className="w-[420px]! p-6!">
|
||||
<div data-testid="transfer-modal-close" className="absolute top-5 right-5 cursor-pointer p-1.5" onClick={onClose}>
|
||||
<div className="i-ri-close-line h-5 w-5 text-text-tertiary" />
|
||||
</div>
|
||||
{step === STEP.start && (
|
||||
<>
|
||||
<div className="pb-3 title-2xl-semi-bold text-text-primary">{t('members.transferModal.title', { ns: 'common' })}</div>
|
||||
<div className="space-y-1 pt-1 pb-2">
|
||||
<div className="body-md-medium text-text-destructive">{t('members.transferModal.warning', { ns: 'common', workspace: currentWorkspace.name.replace(/'/g, '’') })}</div>
|
||||
<div className="body-md-regular text-text-secondary">{t('members.transferModal.warningTip', { ns: 'common' })}</div>
|
||||
<div className="body-md-regular text-text-secondary">
|
||||
<Trans i18nKey="members.transferModal.sendTip" ns="common" components={{ email: <span className="body-md-medium text-text-primary"></span> }} values={{ email: userProfile.email }} />
|
||||
<Dialog open={show}>
|
||||
<DialogContent className="w-[420px]">
|
||||
<div data-testid="transfer-modal-close" className="absolute top-5 right-5 cursor-pointer p-1.5" onClick={onClose}>
|
||||
<div className="i-ri-close-line h-5 w-5 text-text-tertiary" />
|
||||
</div>
|
||||
{step === STEP.start && (
|
||||
<>
|
||||
<div className="pb-3 title-2xl-semi-bold text-text-primary">{t('members.transferModal.title', { ns: 'common' })}</div>
|
||||
<div className="space-y-1 pt-1 pb-2">
|
||||
<div className="body-md-medium text-text-destructive">{t('members.transferModal.warning', { ns: 'common', workspace: currentWorkspace.name.replace(/'/g, '’') })}</div>
|
||||
<div className="body-md-regular text-text-secondary">{t('members.transferModal.warningTip', { ns: 'common' })}</div>
|
||||
<div className="body-md-regular text-text-secondary">
|
||||
<Trans i18nKey="members.transferModal.sendTip" ns="common" components={{ email: <span className="body-md-medium text-text-primary"></span> }} values={{ email: userProfile.email }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-3"></div>
|
||||
<div className="space-y-2">
|
||||
<Button data-testid="transfer-modal-send-code" className="w-full!" variant="primary" onClick={sendCodeToOriginEmail}>
|
||||
{t('members.transferModal.sendVerifyCode', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button data-testid="transfer-modal-cancel" className="w-full!" onClick={onClose}>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{step === STEP.verify && (
|
||||
<>
|
||||
<div className="pb-3 title-2xl-semi-bold text-text-primary">{t('members.transferModal.verifyEmail', { ns: 'common' })}</div>
|
||||
<div className="pt-1 pb-2">
|
||||
<div className="body-md-regular text-text-secondary">
|
||||
<Trans i18nKey="members.transferModal.verifyContent" ns="common" components={{ email: <span className="body-md-medium text-text-primary"></span> }} values={{ email: userProfile.email }} />
|
||||
<div className="pt-3"></div>
|
||||
<div className="space-y-2">
|
||||
<Button data-testid="transfer-modal-send-code" className="w-full!" variant="primary" onClick={sendCodeToOriginEmail}>
|
||||
{t('members.transferModal.sendVerifyCode', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button data-testid="transfer-modal-cancel" className="w-full!" onClick={onClose}>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="body-md-regular text-text-secondary">{t('members.transferModal.verifyContent2', { ns: 'common' })}</div>
|
||||
</div>
|
||||
<div className="pt-3">
|
||||
<div className="mb-1 flex h-6 items-center system-sm-medium text-text-secondary">{t('members.transferModal.codeLabel', { ns: 'common' })}</div>
|
||||
<Input data-testid="transfer-modal-code-input" className="w-full!" placeholder={t('members.transferModal.codePlaceholder', { ns: 'common' })} value={code} onChange={e => setCode(e.target.value)} maxLength={6} />
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
<Button data-testid="transfer-modal-continue" disabled={code.length !== 6} className="w-full!" variant="primary" onClick={handleVerifyOriginEmail}>
|
||||
{t('members.transferModal.continue', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button data-testid="transfer-modal-cancel" className="w-full!" onClick={onClose}>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-3 flex items-center gap-1 system-xs-regular text-text-tertiary">
|
||||
<span>{t('members.transferModal.resendTip', { ns: 'common' })}</span>
|
||||
{time > 0 && (<span>{t('members.transferModal.resendCount', { ns: 'common', count: time })}</span>)}
|
||||
{!time && (
|
||||
<span data-testid="transfer-modal-resend" onClick={sendCodeToOriginEmail} className="cursor-pointer system-xs-medium text-text-accent-secondary">
|
||||
{t('members.transferModal.resend', { ns: 'common' })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{step === STEP.transfer && (
|
||||
<>
|
||||
<div className="pb-3 title-2xl-semi-bold text-text-primary">{t('members.transferModal.title', { ns: 'common' })}</div>
|
||||
<div className="space-y-1 pt-1 pb-2">
|
||||
<div className="body-md-medium text-text-destructive">{t('members.transferModal.warning', { ns: 'common', workspace: currentWorkspace.name.replace(/'/g, '’') })}</div>
|
||||
<div className="body-md-regular text-text-secondary">{t('members.transferModal.warningTip', { ns: 'common' })}</div>
|
||||
</div>
|
||||
<div className="pt-3">
|
||||
<div className="mb-1 flex h-6 items-center system-sm-medium text-text-secondary">{t('members.transferModal.transferLabel', { ns: 'common' })}</div>
|
||||
<MemberSelector exclude={[userProfile.id]} value={newOwner} onSelect={setNewOwner} />
|
||||
</div>
|
||||
<div className="mt-4 space-y-2">
|
||||
<Button data-testid="transfer-modal-submit" disabled={!newOwner || isTransfer} className="w-full!" variant="primary" tone="destructive" onClick={handleTransfer}>
|
||||
{t('members.transferModal.transfer', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button data-testid="transfer-modal-cancel" className="w-full!" onClick={onClose}>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
)}
|
||||
{step === STEP.verify && (
|
||||
<>
|
||||
<div className="pb-3 title-2xl-semi-bold text-text-primary">{t('members.transferModal.verifyEmail', { ns: 'common' })}</div>
|
||||
<div className="pt-1 pb-2">
|
||||
<div className="body-md-regular text-text-secondary">
|
||||
<Trans i18nKey="members.transferModal.verifyContent" ns="common" 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('members.transferModal.verifyContent2', { ns: 'common' })}</div>
|
||||
</div>
|
||||
<div className="pt-3">
|
||||
<div className="mb-1 flex h-6 items-center system-sm-medium text-text-secondary">{t('members.transferModal.codeLabel', { ns: 'common' })}</div>
|
||||
<Input data-testid="transfer-modal-code-input" className="w-full!" placeholder={t('members.transferModal.codePlaceholder', { ns: 'common' })} value={code} onChange={e => setCode(e.target.value)} maxLength={6} />
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
<Button data-testid="transfer-modal-continue" disabled={code.length !== 6} className="w-full!" variant="primary" onClick={handleVerifyOriginEmail}>
|
||||
{t('members.transferModal.continue', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button data-testid="transfer-modal-cancel" className="w-full!" onClick={onClose}>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-3 flex items-center gap-1 system-xs-regular text-text-tertiary">
|
||||
<span>{t('members.transferModal.resendTip', { ns: 'common' })}</span>
|
||||
{time > 0 && (<span>{t('members.transferModal.resendCount', { ns: 'common', count: time })}</span>)}
|
||||
{!time && (
|
||||
<span data-testid="transfer-modal-resend" onClick={sendCodeToOriginEmail} className="cursor-pointer system-xs-medium text-text-accent-secondary">
|
||||
{t('members.transferModal.resend', { ns: 'common' })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{step === STEP.transfer && (
|
||||
<>
|
||||
<div className="pb-3 title-2xl-semi-bold text-text-primary">{t('members.transferModal.title', { ns: 'common' })}</div>
|
||||
<div className="space-y-1 pt-1 pb-2">
|
||||
<div className="body-md-medium text-text-destructive">{t('members.transferModal.warning', { ns: 'common', workspace: currentWorkspace.name.replace(/'/g, '’') })}</div>
|
||||
<div className="body-md-regular text-text-secondary">{t('members.transferModal.warningTip', { ns: 'common' })}</div>
|
||||
</div>
|
||||
<div className="pt-3">
|
||||
<div className="mb-1 flex h-6 items-center system-sm-medium text-text-secondary">{t('members.transferModal.transferLabel', { ns: 'common' })}</div>
|
||||
<MemberSelector exclude={[userProfile.id]} value={newOwner} onSelect={setNewOwner} />
|
||||
</div>
|
||||
<div className="mt-4 space-y-2">
|
||||
<Button data-testid="transfer-modal-submit" disabled={!newOwner || isTransfer} className="w-full!" variant="primary" tone="destructive" onClick={handleTransfer}>
|
||||
{t('members.transferModal.transfer', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button data-testid="transfer-modal-cancel" className="w-full!" onClick={onClose}>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
export default TransferOwnershipModal
|
||||
|
||||
Loading…
Reference in New Issue
Block a user