Merge branch 'main' of github.com:QuantumNous/new-api
This commit is contained in:
commit
a720064d91
@ -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
|
||||||
|
|||||||
25
web/default/src/components/ui/dropdown-menu-events.ts
vendored
Normal file
25
web/default/src/components/ui/dropdown-menu-events.ts
vendored
Normal 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?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
50
web/default/src/components/ui/dropdown-menu.test.tsx
vendored
Normal file
50
web/default/src/components/ui/dropdown-menu.test.tsx
vendored
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
17
web/default/src/components/ui/dropdown-menu.tsx
vendored
17
web/default/src/components/ui/dropdown-menu.tsx
vendored
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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>[] {
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user