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:
Joel 2026-05-07 15:46:47 +08:00 committed by GitHub
parent bb3de5dd32
commit 8b77ec7f31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 103 additions and 104 deletions

View File

@ -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": {

View File

@ -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 () => {

View File

@ -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