From c36b74fda67705f806565dfe75c5e12231c94cd6 Mon Sep 17 00:00:00 2001
From: dongmucat <1127093059@qq.com>
Date: Fri, 20 Mar 2026 11:34:09 +0800
Subject: [PATCH] feat(web): migrate shared select to radix
---
.../namespace/add-namespace-member-dialog.tsx | 29 +--
.../features/token/create-token-dialog.tsx | 28 ++-
web/src/features/token/token-list.tsx | 28 ++-
web/src/pages/admin/audit-log.tsx | 39 +++-
web/src/pages/admin/users.tsx | 45 +++--
web/src/pages/dashboard/namespace-members.tsx | 22 ++-
web/src/pages/dashboard/publish.tsx | 47 +++--
web/src/shared/ui/select.tsx | 172 ++++++++++++++++--
8 files changed, 317 insertions(+), 93 deletions(-)
diff --git a/web/src/features/namespace/add-namespace-member-dialog.tsx b/web/src/features/namespace/add-namespace-member-dialog.tsx
index 581ce169..95b1a01b 100644
--- a/web/src/features/namespace/add-namespace-member-dialog.tsx
+++ b/web/src/features/namespace/add-namespace-member-dialog.tsx
@@ -15,7 +15,13 @@ import {
} from '@/shared/ui/dialog'
import { Input } from '@/shared/ui/input'
import { Label } from '@/shared/ui/label'
-import { Select } from '@/shared/ui/select'
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/shared/ui/select'
interface AddNamespaceMemberDialogProps {
slug: string
@@ -212,16 +218,17 @@ export function AddNamespaceMemberDialog({ slug, children }: AddNamespaceMemberD
-
diff --git a/web/src/features/token/create-token-dialog.tsx b/web/src/features/token/create-token-dialog.tsx
index 180b2165..432420de 100644
--- a/web/src/features/token/create-token-dialog.tsx
+++ b/web/src/features/token/create-token-dialog.tsx
@@ -14,7 +14,13 @@ import {
import { Button } from '@/shared/ui/button'
import { Input } from '@/shared/ui/input'
import { Label } from '@/shared/ui/label'
-import { Select } from '@/shared/ui/select'
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/shared/ui/select'
import { centeredToastOptions, toast } from '@/shared/lib/toast'
import { formatLocalDateTime } from '@/shared/lib/date-time'
import type { CreateTokenRequest, CreateTokenResponse } from '@/api/types'
@@ -176,18 +182,22 @@ export function CreateTokenDialog({ children, existingNames = [] }: CreateTokenD
{
- setExpirationMode(e.target.value as TokenExpirationMode)
+ onValueChange={(value) => {
+ setExpirationMode(value as TokenExpirationMode)
setExpiresAtError(null)
}}
>
-
-
-
-
-
+
+
+
+
+ {t('createToken.expirationNever')}
+ {t('createToken.expiration7d')}
+ {t('createToken.expiration30d')}
+ {t('createToken.expiration90d')}
+ {t('createToken.expirationCustom')}
+
{expirationMode === 'custom' ? (
setExpirationDialog((current) => ({
+ onValueChange={(value) => setExpirationDialog((current) => ({
...current,
- mode: event.target.value as TokenExpirationMode,
+ mode: value as TokenExpirationMode,
}))}
>
-
-
-
-
-
+
+
+
+
+ {t('createToken.expirationNever')}
+ {t('createToken.expiration7d')}
+ {t('createToken.expiration30d')}
+ {t('createToken.expiration90d')}
+ {t('createToken.expirationCustom')}
+
{expirationDialog.mode === 'custom' ? (
diff --git a/web/src/pages/admin/audit-log.tsx b/web/src/pages/admin/audit-log.tsx
index a53e9e6a..a67e6771 100644
--- a/web/src/pages/admin/audit-log.tsx
+++ b/web/src/pages/admin/audit-log.tsx
@@ -4,7 +4,14 @@ import { formatLocalDateTime } from '@/shared/lib/date-time'
import { Card } from '@/shared/ui/card'
import { Input } from '@/shared/ui/input'
import { Button } from '@/shared/ui/button'
-import { Select } from '@/shared/ui/select'
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+ normalizeSelectValue,
+} from '@/shared/ui/select'
import {
Table,
TableBody,
@@ -42,6 +49,7 @@ const ACTION_OPTIONS = [
*/
export function AuditLogPage() {
const { t, i18n } = useTranslation()
+ const allActionFilterValue = '__all_actions__'
const [actionFilter, setActionFilter] = useState('')
const [userIdFilter, setUserIdFilter] = useState('')
const [requestIdFilter, setRequestIdFilter] = useState('')
@@ -105,15 +113,26 @@ export function AuditLogPage() {
@@ -288,12 +304,17 @@ export function AdminUsersPage() {
- setNewRole(e.target.value)}>
- {roleOptions.map((roleOption) => (
-
- ))}
+
+
+
+
+
+ {roleOptions.map((roleOption) => (
+
+ {roleOption.label}
+
+ ))}
+
diff --git a/web/src/pages/dashboard/namespace-members.tsx b/web/src/pages/dashboard/namespace-members.tsx
index 66184af6..959c3177 100644
--- a/web/src/pages/dashboard/namespace-members.tsx
+++ b/web/src/pages/dashboard/namespace-members.tsx
@@ -8,7 +8,13 @@ import { ConfirmDialog } from '@/shared/components/confirm-dialog'
import { DashboardPageHeader } from '@/shared/components/dashboard-page-header'
import { Button } from '@/shared/ui/button'
import { Card } from '@/shared/ui/card'
-import { Select } from '@/shared/ui/select'
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/shared/ui/select'
import {
useMyNamespaces,
useNamespaceDetail,
@@ -192,17 +198,21 @@ export function NamespaceMembersPage() {
{canManageMembers && !isOwner ? (
{
+ onValueChange={(value) => {
setDraftRoles((current) => ({
...current,
- [member.userId]: event.target.value,
+ [member.userId]: value,
}))
}}
>
-
-
+
+
+
+
+ {t('members.roleMember')}
+ {t('members.roleAdmin')}
+
) : (
setNamespaceSlug(e.target.value)}
+ value={normalizeSelectValue(namespaceSlug)}
+ onValueChange={setNamespaceSlug}
>
-
- {namespaces?.map((ns) => (
-
- ))}
+
+
+
+
+ {namespaces?.map((ns) => (
+
+ {ns.displayName} (@{ns.slug})
+
+ ))}
+
)}
- setVisibility(e.target.value)}
- >
-
-
-
+
+
+
+
+
+ {t('publish.visibilityOptions.public')}
+ {namespaceOnlyLabel}
+ {t('publish.visibilityOptions.private')}
+
diff --git a/web/src/shared/ui/select.tsx b/web/src/shared/ui/select.tsx
index f2726e01..80e5888c 100644
--- a/web/src/shared/ui/select.tsx
+++ b/web/src/shared/ui/select.tsx
@@ -1,19 +1,27 @@
import * as React from 'react'
+import * as SelectPrimitive from '@radix-ui/react-select'
+import { Check, ChevronDown, ChevronUp } from 'lucide-react'
import { cn } from '@/shared/lib/utils'
export const SELECT_TRIGGER_CLASS_NAME = cn(
- 'flex h-11 w-full rounded-lg border border-border/60 bg-secondary/50 px-4 py-2 text-sm text-foreground',
+ 'flex h-11 w-full items-center justify-between gap-2 rounded-lg border border-border/60 bg-secondary/50 px-4 py-2 text-sm text-foreground',
'ring-offset-background transition-all duration-200',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:border-primary/50',
- 'disabled:cursor-not-allowed disabled:opacity-50'
+ 'disabled:cursor-not-allowed disabled:opacity-50',
+ 'data-[placeholder]:text-muted-foreground [&>span]:line-clamp-1'
)
export const SELECT_CONTENT_CLASS_NAME = cn(
- 'z-50 overflow-hidden rounded-lg border border-border bg-popover text-popover-foreground shadow-md'
+ 'z-50 overflow-hidden rounded-lg border border-border bg-popover text-popover-foreground shadow-md',
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out',
+ 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
+ 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
+ 'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2',
+ 'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2'
)
export const SELECT_ITEM_CLASS_NAME = cn(
- 'relative flex w-full cursor-default select-none items-center rounded-md py-2 pl-8 pr-3 text-sm outline-none',
+ 'relative flex w-full cursor-default select-none items-center rounded-md py-2 pl-8 pr-8 text-sm outline-none',
'focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50'
)
@@ -21,22 +29,150 @@ export function normalizeSelectValue(value?: string | null) {
return typeof value === 'string' && value.length > 0 ? value : undefined
}
-interface SelectProps extends React.SelectHTMLAttributes {}
+const Select = SelectPrimitive.Root
+const SelectGroup = SelectPrimitive.Group
+const SelectValue = SelectPrimitive.Value
-const Select = React.forwardRef(
- ({ className, children, ...props }, ref) => {
- return (
- ,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+
+
+
+))
+
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
+
+const SelectScrollUpButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+
+SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
+
+const SelectScrollDownButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+
+SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
+
+const SelectContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, position = 'popper', ...props }, ref) => (
+
+
+
+
{children}
-
- )
- }
-)
+
+
+
+
+))
-Select.displayName = 'Select'
+SelectContent.displayName = SelectPrimitive.Content.displayName
-export { Select }
+const SelectLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+SelectLabel.displayName = SelectPrimitive.Label.displayName
+
+const SelectItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+
+SelectItem.displayName = SelectPrimitive.Item.displayName
+
+const SelectSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+SelectSeparator.displayName = SelectPrimitive.Separator.displayName
+
+export {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectSeparator,
+ SelectTrigger,
+ SelectValue,
+}