fix(web/default): batch fix new UI issues #4880 #4893 #4817 #4877 #4898

- Add singleSelect to status/role filters in API keys, users, and redemptions tables (#4880)
- Fix affiliate link 404 by changing /register to /sign-up (#4893)
- Open FetchModelsDialog in channel creation mode via customFetcher prop (#4817)
- Add TruncatedText component with tooltip for long channel names, token names, and usernames (#4877)
- Elevate forgot-password link z-index to prevent label click interception (#4898)
This commit is contained in:
CaIon
2026-05-16 14:48:49 +08:00
parent 18282e610d
commit 3caa6e467b
10 changed files with 158 additions and 86 deletions
@@ -36,6 +36,7 @@ import {
} from '@/lib/format'
import { getLobeIcon } from '@/lib/lobe-icon'
import { cn, truncateText } from '@/lib/utils'
import { TruncatedText } from '@/components/truncated-text'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import {
@@ -556,7 +557,11 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
<div className='flex items-center gap-2'>
<div className='flex flex-col gap-1'>
<div className='flex items-center gap-1.5'>
<span className='font-medium'>{truncateText(name, 30)}</span>
<TruncatedText
text={name}
className='font-medium'
maxWidth='max-w-[180px]'
/>
{isPassThrough && (
<TooltipProvider delay={100}>
<Tooltip>
@@ -65,6 +65,8 @@ type FetchModelsDialogProps = {
onModelsSelected?: (models: string[]) => void
redirectModels?: string[]
redirectSourceModels?: string[]
customFetcher?: () => Promise<string[]>
existingModelsOverride?: string[]
}
export function FetchModelsDialog({
@@ -73,6 +75,8 @@ export function FetchModelsDialog({
onModelsSelected,
redirectModels = [],
redirectSourceModels = [],
customFetcher,
existingModelsOverride,
}: FetchModelsDialogProps) {
const { t } = useTranslation()
const { currentRow } = useChannels()
@@ -85,8 +89,10 @@ export function FetchModelsDialog({
// Parse existing models
const existingModels = useMemo(
() => parseModelsString(currentRow?.models || ''),
[currentRow?.models]
() =>
existingModelsOverride ??
parseModelsString(currentRow?.models || ''),
[existingModelsOverride, currentRow?.models]
)
// Categorize models with redirect models
@@ -121,26 +127,33 @@ export function FetchModelsDialog({
}, [fetchedModelSet, redirectSourceKeysSet, searchKeyword, selectedModels])
useEffect(() => {
if (open && currentRow) {
if (open && (currentRow || customFetcher)) {
handleFetchModels()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, currentRow?.id])
}, [open, currentRow?.id, customFetcher])
const handleFetchModels = async () => {
if (!currentRow) return
if (!currentRow && !customFetcher) return
setIsFetching(true)
try {
const response = await fetchUpstreamModels(currentRow.id)
if (response.success) {
const list = Array.isArray(response.data) ? response.data : []
if (customFetcher) {
const list = await customFetcher()
setFetchedModels(list)
setSelectedModels(existingModels)
toast.success(t('Fetched {{count}} models', { count: list.length }))
} else {
toast.error(response.message || t('Failed to fetch models'))
setFetchedModels([])
const response = await fetchUpstreamModels(currentRow!.id)
if (response.success) {
const list = Array.isArray(response.data) ? response.data : []
setFetchedModels(list)
setSelectedModels(existingModels)
toast.success(t('Fetched {{count}} models', { count: list.length }))
} else {
toast.error(response.message || t('Failed to fetch models'))
setFetchedModels([])
}
}
} catch (error: unknown) {
toast.error(
@@ -153,8 +166,6 @@ export function FetchModelsDialog({
}
const handleSave = async () => {
if (!currentRow) return
// If onModelsSelected callback is provided, use it (form filling mode)
if (onModelsSelected) {
onModelsSelected(selectedModels)
@@ -164,6 +175,7 @@ export function FetchModelsDialog({
}
// Otherwise, directly save to API (standalone mode)
if (!currentRow) return
setIsSaving(true)
try {
const modelsString = selectedModels.join(',')
@@ -357,12 +369,16 @@ export function FetchModelsDialog({
<DialogHeader>
<DialogTitle>{t('Fetch Models')}</DialogTitle>
<DialogDescription>
{t('Fetch available models for:')}{' '}
<strong>{currentRow?.name}</strong>
{currentRow
? <>
{t('Fetch available models for:')}{' '}
<strong>{currentRow.name}</strong>
</>
: t('Fetch available models from upstream')}
</DialogDescription>
</DialogHeader>
{!currentRow ? (
{!currentRow && !customFetcher ? (
<div className='text-muted-foreground py-8 text-center'>
{t('No channel selected')}
</div>
@@ -301,7 +301,6 @@ export function ChannelMutateDrawer({
const { setOpen } = useChannels()
const [isSubmitting, setIsSubmitting] = useState(false)
const [customModel, setCustomModel] = useState('')
const [isFetchingModels, setIsFetchingModels] = useState(false)
const [fetchModelsDialogOpen, setFetchModelsDialogOpen] = useState(false)
const [channelKey, setChannelKey] = useState<string | null>(null)
const [isChannelKeyLoading, setIsChannelKeyLoading] = useState(false)
@@ -767,43 +766,29 @@ export function ChannelMutateDrawer({
return
}
// For editing mode, open FetchModelsDialog to let user select
if (isEditing && currentRow) {
setFetchModelsDialogOpen(true)
return
}
// For creation mode, fetch and fill all models
const key = form.getValues('key')
if (!key?.trim()) {
toast.error(t('Please enter API key first'))
return
}
setIsFetchingModels(true)
try {
const response = await fetchModels({
type,
key,
base_url: form.getValues('base_url') || '',
})
if (response.success && response.data) {
updateModels(response.data, true)
toast.success(
t('Fetched {{count}} model(s) from upstream', {
count: response.data.length,
})
)
} else {
toast.error(t('No models fetched from upstream'))
// For creation mode, validate key before opening dialog
if (!isEditing) {
const key = form.getValues('key')
if (!key?.trim()) {
toast.error(t('Please enter API key first'))
return
}
} catch (error: unknown) {
toast.error(getErrorMessage(error) || t('Failed to fetch models'))
} finally {
setIsFetchingModels(false)
}
}, [isEditing, currentRow, form, t, updateModels])
setFetchModelsDialogOpen(true)
}, [isEditing, form, t])
const createModeFetcher = useCallback(async (): Promise<string[]> => {
const response = await fetchModels({
type: form.getValues('type'),
key: form.getValues('key'),
base_url: form.getValues('base_url') || '',
})
if (response.success && response.data) {
return response.data
}
throw new Error(response.message || 'No models fetched from upstream')
}, [form])
// Handle adding custom models
const handleAddCustomModels = useCallback(() => {
@@ -2234,13 +2219,8 @@ export function ChannelMutateDrawer({
variant='outline'
size='sm'
onClick={handleFetchModels}
disabled={isFetchingModels}
>
{isFetchingModels ? (
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
) : (
<Sparkles className='mr-2 h-4 w-4' />
)}
<Sparkles className='mr-2 h-4 w-4' />
{t('Fetch from Upstream')}
</Button>
)}
@@ -3390,19 +3370,20 @@ export function ChannelMutateDrawer({
/>
)}
{/* Fetch Models Dialog (for editing mode) */}
{isEditing && currentRow && (
<FetchModelsDialog
open={fetchModelsDialogOpen}
onOpenChange={setFetchModelsDialogOpen}
onModelsSelected={(models) => {
// Fill selected models to form
form.setValue('models', formatModelsArray(models))
}}
redirectModels={redirectModelList}
redirectSourceModels={redirectModelKeyList}
/>
)}
{/* Fetch Models Dialog */}
<FetchModelsDialog
open={fetchModelsDialogOpen}
onOpenChange={setFetchModelsDialogOpen}
onModelsSelected={(models) => {
form.setValue('models', formatModelsArray(models))
}}
redirectModels={redirectModelList}
redirectSourceModels={redirectModelKeyList}
customFetcher={!isEditing ? createModeFetcher : undefined}
existingModelsOverride={
!isEditing ? parseModelsString(form.getValues('models') || '') : undefined
}
/>
<SecureVerificationDialog
open={verificationOpen}