Merge branch 'main' of github.com:QuantumNous/new-api

This commit is contained in:
CaIon 2026-05-12 16:24:00 +08:00
commit a720064d91
No known key found for this signature in database
GPG Key ID: 0CFA613529A9921D
6 changed files with 103 additions and 33 deletions

View File

@ -8,4 +8,8 @@ docs
.eslintcache .eslintcache
.gocache .gocache
/web/node_modules /web/node_modules
/web/default/node_modules
/web/default/dist
/web/classic/node_modules
/web/classic/dist
!THIRD-PARTY-LICENSES.md !THIRD-PARTY-LICENSES.md

View File

@ -0,0 +1,25 @@
import type * as React from 'react'
export type DropdownMenuItemSelectEvent = React.MouseEvent<HTMLElement> & {
preventBaseUIHandler?: () => void
}
export type DropdownMenuItemSelectHandler = (
event: DropdownMenuItemSelectEvent
) => void
export function handleDropdownMenuItemSelect(
event: DropdownMenuItemSelectEvent,
onClick?: React.MouseEventHandler<HTMLElement>,
onSelect?: DropdownMenuItemSelectHandler
) {
onClick?.(event)
if (!event.defaultPrevented) {
onSelect?.(event)
}
if (event.defaultPrevented) {
event.preventBaseUIHandler?.()
}
}

View File

@ -0,0 +1,50 @@
import assert from 'node:assert/strict'
import { describe, test } from 'node:test'
import { handleDropdownMenuItemSelect } from './dropdown-menu-events'
function createMenuEvent() {
let defaultPrevented = false
let baseUIHandlerPrevented = false
return {
get defaultPrevented() {
return defaultPrevented
},
preventDefault() {
defaultPrevented = true
},
preventBaseUIHandler() {
baseUIHandlerPrevented = true
},
get baseUIHandlerPrevented() {
return baseUIHandlerPrevented
},
} as unknown as Parameters<typeof handleDropdownMenuItemSelect>[0] & {
baseUIHandlerPrevented: boolean
}
}
describe('DropdownMenuItem onSelect compatibility', () => {
test('calls the Radix-style onSelect handler on item click', () => {
const event = createMenuEvent()
let selected = false
handleDropdownMenuItemSelect(event, undefined, () => {
selected = true
})
assert.equal(selected, true)
assert.equal(event.baseUIHandlerPrevented, false)
})
test('keeps the Base UI menu open when onSelect prevents default', () => {
const event = createMenuEvent()
handleDropdownMenuItemSelect(event, undefined, (selectEvent) => {
selectEvent.preventDefault()
})
assert.equal(event.defaultPrevented, true)
assert.equal(event.baseUIHandlerPrevented, true)
})
})

View File

@ -21,6 +21,10 @@ import { Menu as MenuPrimitive } from '@base-ui/react/menu'
import { ArrowRight01Icon, Tick02Icon } from '@hugeicons/core-free-icons' import { ArrowRight01Icon, Tick02Icon } from '@hugeicons/core-free-icons'
import { HugeiconsIcon } from '@hugeicons/react' import { HugeiconsIcon } from '@hugeicons/react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import {
handleDropdownMenuItemSelect,
type DropdownMenuItemSelectHandler,
} from './dropdown-menu-events'
function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) { function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
return <MenuPrimitive.Root data-slot='dropdown-menu' {...props} /> return <MenuPrimitive.Root data-slot='dropdown-menu' {...props} />
@ -96,11 +100,21 @@ function DropdownMenuItem({
className, className,
inset, inset,
variant = 'default', variant = 'default',
onClick,
onSelect,
...props ...props
}: MenuPrimitive.Item.Props & { }: Omit<MenuPrimitive.Item.Props, 'onSelect'> & {
inset?: boolean inset?: boolean
variant?: 'default' | 'destructive' variant?: 'default' | 'destructive'
onSelect?: DropdownMenuItemSelectHandler
}) { }) {
const handleClick = React.useCallback(
(event: React.MouseEvent<HTMLElement>) => {
handleDropdownMenuItemSelect(event, onClick, onSelect)
},
[onClick, onSelect]
)
return ( return (
<MenuPrimitive.Item <MenuPrimitive.Item
data-slot='dropdown-menu-item' data-slot='dropdown-menu-item'
@ -110,6 +124,7 @@ function DropdownMenuItem({
"group/dropdown-menu-item focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:*:[svg]:text-destructive relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "group/dropdown-menu-item focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:*:[svg]:text-destructive relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
onClick={onClick || onSelect ? handleClick : undefined}
{...props} {...props}
/> />
) )

View File

@ -16,11 +16,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import { useMemo } from 'react'
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { type ColumnDef } from '@tanstack/react-table' import { type ColumnDef } from '@tanstack/react-table'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useAuthStore } from '@/stores/auth-store'
import { getUserGroups } from '@/lib/api' import { getUserGroups } from '@/lib/api'
import { formatQuota, formatTimestampToDate } from '@/lib/format' import { formatQuota, formatTimestampToDate } from '@/lib/format'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
@ -34,7 +32,6 @@ import {
import { DataTableColumnHeader } from '@/components/data-table' import { DataTableColumnHeader } from '@/components/data-table'
import { GroupBadge } from '@/components/group-badge' import { GroupBadge } from '@/components/group-badge'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import { getSystemOptions } from '@/features/system-settings/api'
import { API_KEY_STATUSES } from '../constants' import { API_KEY_STATUSES } from '../constants'
import { type ApiKey } from '../types' import { type ApiKey } from '../types'
import { import {
@ -51,31 +48,9 @@ function getQuotaProgressColor(percentage: number): string {
} }
function useGroupRatios(): Record<string, number> { function useGroupRatios(): Record<string, number> {
const isAdmin = useAuthStore((s) => const { data } = useQuery({
Boolean(s.auth.user?.role && s.auth.user.role >= 10)
)
const { data: adminData } = useQuery({
queryKey: ['system-options-group-ratio'],
queryFn: getSystemOptions,
enabled: isAdmin,
staleTime: 5 * 60 * 1000,
select: (res) => {
if (!res.success || !res.data) return {}
const option = res.data.find((o) => o.key === 'GroupRatio')
if (!option?.value) return {}
try {
return JSON.parse(option.value) as Record<string, number>
} catch {
return {}
}
},
})
const { data: userGroupsData } = useQuery({
queryKey: ['user-self-groups'], queryKey: ['user-self-groups'],
queryFn: getUserGroups, queryFn: getUserGroups,
enabled: !isAdmin,
staleTime: 5 * 60 * 1000, staleTime: 5 * 60 * 1000,
select: (res) => { select: (res) => {
if (!res.success || !res.data) return {} if (!res.success || !res.data) return {}
@ -89,10 +64,7 @@ function useGroupRatios(): Record<string, number> {
}, },
}) })
return useMemo( return data ?? {}
() => (isAdmin ? adminData : userGroupsData) ?? {},
[isAdmin, adminData, userGroupsData]
)
} }
export function useApiKeysColumns(): ColumnDef<ApiKey>[] { export function useApiKeysColumns(): ColumnDef<ApiKey>[] {

View File

@ -88,8 +88,12 @@ export function loadMessages(): Message[] | null {
try { try {
const saved = localStorage.getItem(STORAGE_KEYS.MESSAGES) const saved = localStorage.getItem(STORAGE_KEYS.MESSAGES)
if (saved) { if (saved) {
const parsed: Message[] = JSON.parse(saved) const parsed: unknown = JSON.parse(saved)
const sanitized = sanitizeMessagesOnLoad(parsed) if (!Array.isArray(parsed)) {
localStorage.removeItem(STORAGE_KEYS.MESSAGES)
return null
}
const sanitized = sanitizeMessagesOnLoad(parsed as Message[])
// Persist sanitized result to avoid re-sanitizing on subsequent loads // Persist sanitized result to avoid re-sanitizing on subsequent loads
if (sanitized !== parsed) { if (sanitized !== parsed) {
saveMessages(sanitized) saveMessages(sanitized)