fix: copy button not working on API Server and API Key pages (#34515)

Co-authored-by: Brian Wang <BrianWang1990@users.noreply.github.com>
Co-authored-by: test <test@testdeMac-mini.local>
Co-authored-by: BrianWang1990 <512dabing99@163.com>
Co-authored-by: Stephen Zhou <hi@hyoban.cc>
Co-authored-by: Stephen Zhou <38493346+hyoban@users.noreply.github.com>
This commit is contained in:
BrianWang1990 2026-04-09 10:49:40 +08:00 committed by GitHub
parent 7ca5b726a2
commit 9308287fea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 150 additions and 46 deletions

42
pnpm-lock.yaml generated
View File

@ -249,6 +249,9 @@ catalogs:
class-variance-authority:
specifier: 0.7.1
version: 0.7.1
client-only:
specifier: 0.0.1
version: 0.0.1
clsx:
specifier: 2.1.1
version: 2.1.1
@ -324,9 +327,6 @@ catalogs:
fast-deep-equal:
specifier: 3.1.3
version: 3.1.3
foxact:
specifier: 0.3.0
version: 0.3.0
happy-dom:
specifier: 20.8.9
version: 20.8.9
@ -736,6 +736,9 @@ importers:
class-variance-authority:
specifier: 'catalog:'
version: 0.7.1
client-only:
specifier: 'catalog:'
version: 0.0.1
clsx:
specifier: 'catalog:'
version: 2.1.1
@ -781,9 +784,6 @@ importers:
fast-deep-equal:
specifier: 'catalog:'
version: 3.1.3
foxact:
specifier: 'catalog:'
version: 0.3.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
hast-util-to-jsx-runtime:
specifier: 'catalog:'
version: 2.3.6
@ -5871,9 +5871,6 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
event-target-bus@1.0.0:
resolution: {integrity: sha512-uPcWKbj/BJU3Tbw9XqhHqET4/LBOhvv3/SJWr7NksxA6TC5YqBpaZgawE9R+WpYFCBFSAE4Vun+xQS6w4ABdlA==}
events@3.3.0:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
@ -5986,17 +5983,6 @@ packages:
engines: {node: '>=18.3.0'}
hasBin: true
foxact@0.3.0:
resolution: {integrity: sha512-CSlMlC0KlKQQEO83iLeQCLuT1V0OqnMWj7mjLstIDV8baMe1w4F7z3cz3/T+6Z8W12jqkQj07rwlw4Gi39knGg==}
peerDependencies:
react: '*'
react-dom: '*'
peerDependenciesMeta:
react:
optional: true
react-dom:
optional: true
fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
@ -7710,9 +7696,6 @@ packages:
resolution: {integrity: sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA==}
engines: {node: '>=10'}
server-only@0.0.1:
resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
sharp@0.34.5:
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@ -13552,8 +13535,6 @@ snapshots:
esutils@2.0.3: {}
event-target-bus@1.0.0: {}
events@3.3.0: {}
expand-template@2.0.3:
@ -13661,15 +13642,6 @@ snapshots:
dependencies:
fd-package-json: 2.0.0
foxact@0.3.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
client-only: 0.0.1
event-target-bus: 1.0.0
server-only: 0.0.1
optionalDependencies:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
fs-constants@1.0.0:
optional: true
@ -15905,8 +15877,6 @@ snapshots:
seroval@1.5.1: {}
server-only@0.0.1: {}
sharp@0.34.5:
dependencies:
'@img/colour': 1.1.0

View File

@ -129,6 +129,7 @@ catalog:
ahooks: 3.9.7
autoprefixer: 10.4.27
class-variance-authority: 0.7.1
client-only: 0.0.1
clsx: 2.1.1
cmdk: 1.1.1
code-inspector-plugin: 1.5.1
@ -154,7 +155,6 @@ catalog:
eslint-plugin-sonarjs: 4.0.2
eslint-plugin-storybook: 10.3.5
fast-deep-equal: 3.1.3
foxact: 0.3.0
happy-dom: 20.8.9
hast-util-to-jsx-runtime: 2.3.6
hono: 4.12.12

View File

@ -5,7 +5,7 @@ const mockCopy = vi.fn()
const mockReset = vi.fn()
let mockCopied = false
vi.mock('foxact/use-clipboard', () => ({
vi.mock('@/hooks/use-clipboard', () => ({
useClipboard: () => ({
copy: mockCopy,
reset: mockReset,

View File

@ -3,11 +3,11 @@ import {
RiClipboardFill,
RiClipboardLine,
} from '@remixicon/react'
import { useClipboard } from 'foxact/use-clipboard'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Tooltip from '@/app/components/base/tooltip'
import { useClipboard } from '@/hooks/use-clipboard'
import copyStyle from './style.module.css'
type Props = {

View File

@ -5,7 +5,7 @@ const copy = vi.fn()
const reset = vi.fn()
let copied = false
vi.mock('foxact/use-clipboard', () => ({
vi.mock('@/hooks/use-clipboard', () => ({
useClipboard: () => ({
copy,
reset,

View File

@ -1,7 +1,7 @@
'use client'
import { useClipboard } from 'foxact/use-clipboard'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useClipboard } from '@/hooks/use-clipboard'
import Tooltip from '../tooltip'
type Props = {

View File

@ -6,7 +6,7 @@ const mockCopy = vi.fn()
let mockCopied = false
const mockReset = vi.fn()
vi.mock('foxact/use-clipboard', () => ({
vi.mock('@/hooks/use-clipboard', () => ({
useClipboard: () => ({
copy: mockCopy,
copied: mockCopied,

View File

@ -1,8 +1,8 @@
'use client'
import type { InputProps } from '../input'
import { useClipboard } from 'foxact/use-clipboard'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { useClipboard } from '@/hooks/use-clipboard'
import { cn } from '@/utils/classnames'
import ActionButton from '../action-button'
import Tooltip from '../tooltip'

7
web/hooks/noop.ts Normal file
View File

@ -0,0 +1,7 @@
type Noop = {
// eslint-disable-next-line ts/no-explicit-any
(...args: any[]): any
}
/** @see https://foxact.skk.moe/noop */
export const noop: Noop = () => { /* noop */ }

View File

@ -0,0 +1,72 @@
import { useRef, useState } from 'react'
import { writeTextToClipboard } from '@/utils/clipboard'
import { noop } from './noop'
import { useStableHandler } from './use-stable-handler-only-when-you-know-what-you-are-doing-or-you-will-be-fired'
import { useCallback } from './use-typescript-happy-callback'
import 'client-only'
type UseClipboardOption = {
timeout?: number
usePromptAsFallback?: boolean
promptFallbackText?: string
onCopyError?: (error: Error) => void
}
/** @see https://foxact.skk.moe/use-clipboard */
export function useClipboard({
timeout = 1000,
usePromptAsFallback = false,
promptFallbackText = 'Failed to copy to clipboard automatically, please manually copy the text below.',
onCopyError,
}: UseClipboardOption = {}) {
const [error, setError] = useState<Error | null>(null)
const [copied, setCopied] = useState(false)
const copyTimeoutRef = useRef<number | null>(null)
const stablizedOnCopyError = useStableHandler<[e: Error], void>(onCopyError || noop)
const handleCopyResult = useCallback((isCopied: boolean) => {
if (copyTimeoutRef.current) {
clearTimeout(copyTimeoutRef.current)
}
if (isCopied) {
copyTimeoutRef.current = window.setTimeout(() => setCopied(false), timeout)
}
setCopied(isCopied)
}, [timeout])
const handleCopyError = useCallback((e: Error) => {
setError(e)
stablizedOnCopyError(e)
}, [stablizedOnCopyError])
const copy = useCallback(async (valueToCopy: string) => {
try {
await writeTextToClipboard(valueToCopy)
}
catch (e) {
if (usePromptAsFallback) {
try {
// eslint-disable-next-line no-alert -- prompt as fallback in case of copy error
window.prompt(promptFallbackText, valueToCopy)
}
catch (e2) {
handleCopyError(e2 as Error)
}
}
else {
handleCopyError(e as Error)
}
}
}, [handleCopyResult, promptFallbackText, handleCopyError, usePromptAsFallback])
const reset = useCallback(() => {
setCopied(false)
setError(null)
if (copyTimeoutRef.current) {
clearTimeout(copyTimeoutRef.current)
}
}, [])
return { copy, reset, error, copied }
}

View File

@ -0,0 +1,44 @@
import * as reactExports from 'react'
import { useCallback, useEffect, useLayoutEffect, useRef } from 'react'
// useIsomorphicInsertionEffect
const useInsertionEffect
= typeof window === 'undefined'
// useInsertionEffect is only available in React 18+
? useEffect
: reactExports.useInsertionEffect || useLayoutEffect
/**
* @see https://foxact.skk.moe/use-stable-handler-only-when-you-know-what-you-are-doing-or-you-will-be-fired
* Similar to useCallback, with a few subtle differences:
* - The returned function is a stable reference, and will always be the same between renders
* - No dependency lists required
* - Properties or state accessed within the callback will always be "current"
*/
// eslint-disable-next-line ts/no-explicit-any
export function useStableHandler<Args extends any[], Result>(
callback: (...args: Args) => Result,
): typeof callback {
// Keep track of the latest callback:
// eslint-disable-next-line ts/no-explicit-any
const latestRef = useRef<typeof callback>(shouldNotBeInvokedBeforeMount as any)
useInsertionEffect(() => {
latestRef.current = callback
}, [callback])
return useCallback<typeof callback>((...args) => {
const fn = latestRef.current
return fn(...args)
}, [])
}
/**
* Render methods should be pure, especially when concurrency is used,
* so we will throw this error if the callback is called while rendering.
*/
function shouldNotBeInvokedBeforeMount() {
throw new Error(
'foxact: the stablized handler cannot be invoked before the component has mounted.',
)
}

View File

@ -0,0 +1,10 @@
import { useCallback as useCallbackFromReact } from 'react'
/** @see https://foxact.skk.moe/use-typescript-happy-callback */
const useTypeScriptHappyCallback: <Args extends unknown[], R>(
fn: (...args: Args) => R,
deps: React.DependencyList,
) => (...args: Args) => R = useCallbackFromReact
/** @see https://foxact.skk.moe/use-typescript-happy-callback */
export const useCallback = useTypeScriptHappyCallback

View File

@ -85,6 +85,7 @@
"abcjs": "catalog:",
"ahooks": "catalog:",
"class-variance-authority": "catalog:",
"client-only": "catalog:",
"clsx": "catalog:",
"cmdk": "catalog:",
"copy-to-clipboard": "catalog:",
@ -100,7 +101,6 @@
"emoji-mart": "catalog:",
"es-toolkit": "catalog:",
"fast-deep-equal": "catalog:",
"foxact": "catalog:",
"hast-util-to-jsx-runtime": "catalog:",
"html-entities": "catalog:",
"html-to-image": "catalog:",

View File

@ -83,11 +83,12 @@ afterEach(async () => {
})
})
// mock foxact/use-clipboard - not available in test environment
vi.mock('foxact/use-clipboard', () => ({
// mock custom clipboard hook - wraps writeTextToClipboard with fallback
vi.mock('@/hooks/use-clipboard', () => ({
useClipboard: () => ({
copy: vi.fn(),
copied: false,
reset: vi.fn(),
}),
}))