mirror of
https://github.com/langgenius/dify.git
synced 2026-04-15 09:57:03 +08:00
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:
parent
7ca5b726a2
commit
9308287fea
42
pnpm-lock.yaml
generated
42
pnpm-lock.yaml
generated
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
7
web/hooks/noop.ts
Normal 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 */ }
|
||||
72
web/hooks/use-clipboard.ts
Normal file
72
web/hooks/use-clipboard.ts
Normal 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 }
|
||||
}
|
||||
@ -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.',
|
||||
)
|
||||
}
|
||||
10
web/hooks/use-typescript-happy-callback.ts
Normal file
10
web/hooks/use-typescript-happy-callback.ts
Normal 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
|
||||
@ -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:",
|
||||
|
||||
@ -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(),
|
||||
}),
|
||||
}))
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user