From 27d9b4452657d67a4e631ff8f96edd074c9dfaa4 Mon Sep 17 00:00:00 2001
From: Joshen Lim
Date: Fri, 13 Jun 2025 19:08:08 +0800
Subject: [PATCH] Consolidate copy to clipboard (#36353)
* Consolidate copy to clipboard
* Fix
* Fix some extra clipboard events.
* Fix the tests. Fix a small issue with the copy button.
* Fix
---------
Co-authored-by: Ivan Vasilov
---
.../ui/CodeBlock/CodeBlock.client.tsx | 9 ++--
.../grid/components/menu/RowContextMenu.tsx | 3 +-
apps/studio/components/grid/utils/common.ts | 9 ----
.../interfaces/App/CommandMenu/ApiKeys.tsx | 3 +-
.../interfaces/App/CommandMenu/ApiUrl.tsx | 3 +-
.../interfaces/Auth/Hooks/HookCard.tsx | 3 +-
.../interfaces/Auth/Users/Users.utils.tsx | 2 +-
.../Connect/ConnectionParameters.tsx | 2 +-
.../interfaces/Functions/CommandRender.tsx | 9 ++--
.../Functions/EdgeFunctionsListItem.tsx | 8 ++--
.../ProjectAPIDocs/Content/Introduction.tsx | 3 +-
.../SQLEditor/UtilityPanel/Results.tsx | 2 +-
.../interfaces/Settings/Logs/LogTable.tsx | 10 ++++-
.../Settings/Logs/Logs.DatePickers.tsx | 10 ++++-
.../Settings/Logs/LogsQueryPanel.tsx | 6 +--
.../TableEditorLayout/EntityListItem.tsx | 2 +-
.../StorageExplorer/StorageExplorer.utils.tsx | 6 +--
.../Storage/StorageExplorer/useCopyUrl.tsx | 2 +-
apps/studio/components/ui/CopyButton.tsx | 9 ++--
.../components/ui/DownloadResultsButton.tsx | 2 +-
.../components/ui/Logs/LogsExplorerHeader.tsx | 11 ++++-
apps/studio/lib/helpers.test.ts | 44 +++++++++++--------
apps/studio/lib/helpers.ts | 39 ----------------
apps/studio/pages/support/new.tsx | 10 ++---
apps/ui-library/components/copy-button.tsx | 3 +-
.../components/Nav/RightClickBrandLogo.tsx | 15 +++++--
packages/ui-patterns/src/DataInputs/Input.tsx | 24 ++++------
.../ui-patterns/src/TimestampInfo/index.tsx | 15 +++----
packages/ui/src/components/Input/Input.tsx | 44 +++++++------------
packages/ui/src/lib/utils/clipboard.ts | 6 ++-
30 files changed, 136 insertions(+), 178 deletions(-)
diff --git a/apps/docs/features/ui/CodeBlock/CodeBlock.client.tsx b/apps/docs/features/ui/CodeBlock/CodeBlock.client.tsx
index da8a74b65f2..9b3ca95161f 100644
--- a/apps/docs/features/ui/CodeBlock/CodeBlock.client.tsx
+++ b/apps/docs/features/ui/CodeBlock/CodeBlock.client.tsx
@@ -4,7 +4,7 @@ import { Check, Copy } from 'lucide-react'
import { type MouseEvent, useCallback, useEffect, useState } from 'react'
import { type ThemedToken } from 'shiki'
import { type NodeHover } from 'twoslash'
-import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
+import { cn, copyToClipboard, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
export function AnnotatedSpan({
token,
@@ -89,13 +89,10 @@ export function CodeCopyButton({ className, content }: { className?: string; con
const [copied, setCopied] = useState(false)
const handleCopy = async () => {
- try {
- await navigator.clipboard.writeText(content)
+ copyToClipboard(content, () => {
setCopied(true)
setTimeout(() => setCopied(false), 1000)
- } catch (error) {
- console.error('Failed to copy text: ', error)
- }
+ })
}
return (
diff --git a/apps/studio/components/grid/components/menu/RowContextMenu.tsx b/apps/studio/components/grid/components/menu/RowContextMenu.tsx
index dfa4d26bede..14a457eceb6 100644
--- a/apps/studio/components/grid/components/menu/RowContextMenu.tsx
+++ b/apps/studio/components/grid/components/menu/RowContextMenu.tsx
@@ -5,8 +5,9 @@ import { Item, ItemParams, Menu } from 'react-contexify'
import type { SupaRow } from 'components/grid/types'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { useTableEditorTableStateSnapshot } from 'state/table-editor-table'
+import { copyToClipboard } from 'ui'
import { ROW_CONTEXT_MENU_ID } from '.'
-import { copyToClipboard, formatClipboardValue } from '../../utils/common'
+import { formatClipboardValue } from '../../utils/common'
export type RowContextMenuProps = {
rows: SupaRow[]
diff --git a/apps/studio/components/grid/utils/common.ts b/apps/studio/components/grid/utils/common.ts
index 781c9066c3e..b532c45cff3 100644
--- a/apps/studio/components/grid/utils/common.ts
+++ b/apps/studio/components/grid/utils/common.ts
@@ -5,12 +5,3 @@ export function formatClipboardValue(value: any) {
}
return value
}
-
-export const copyToClipboard = (str: string, callback = () => {}) => {
- const focused = window.document.hasFocus()
- if (focused) {
- window.navigator?.clipboard?.writeText(str).then(callback)
- } else {
- console.warn('Unable to copy to clipboard')
- }
-}
diff --git a/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx b/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx
index c86051bba3f..7010e1a8c82 100644
--- a/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx
+++ b/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx
@@ -3,8 +3,7 @@ import { useMemo } from 'react'
import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
-import { copyToClipboard } from 'lib/helpers'
-import { Badge } from 'ui'
+import { Badge, copyToClipboard } from 'ui'
import type { ICommand } from 'ui-patterns/CommandMenu'
import {
PageType,
diff --git a/apps/studio/components/interfaces/App/CommandMenu/ApiUrl.tsx b/apps/studio/components/interfaces/App/CommandMenu/ApiUrl.tsx
index 50a6fb33c3b..dfad51d0667 100644
--- a/apps/studio/components/interfaces/App/CommandMenu/ApiUrl.tsx
+++ b/apps/studio/components/interfaces/App/CommandMenu/ApiUrl.tsx
@@ -2,8 +2,7 @@ import { Link } from 'lucide-react'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
-import { copyToClipboard } from 'lib/helpers'
-import { Badge } from 'ui'
+import { Badge, copyToClipboard } from 'ui'
import { useRegisterCommands, useSetCommandMenuOpen } from 'ui-patterns/CommandMenu'
import { COMMAND_MENU_SECTIONS } from './CommandMenu.utils'
import { orderCommandSectionsByPriority } from './ordering'
diff --git a/apps/studio/components/interfaces/Auth/Hooks/HookCard.tsx b/apps/studio/components/interfaces/Auth/Hooks/HookCard.tsx
index 80dc0264277..38bb4226a16 100644
--- a/apps/studio/components/interfaces/Auth/Hooks/HookCard.tsx
+++ b/apps/studio/components/interfaces/Auth/Hooks/HookCard.tsx
@@ -1,10 +1,9 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Check, Webhook } from 'lucide-react'
-import { Badge, Input } from 'ui'
+import { Badge, Input, copyToClipboard } from 'ui'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
-import { copyToClipboard } from 'lib/helpers'
import { Hook } from './hooks.constants'
interface HookCardProps {
diff --git a/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx b/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx
index 7f5be688a1c..ac1fee9298d 100644
--- a/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx
+++ b/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx
@@ -5,7 +5,6 @@ import { Column, useRowSelection } from 'react-data-grid'
import { User } from 'data/auth/users-infinite-query'
import { BASE_PATH } from 'lib/constants'
-import { copyToClipboard } from 'lib/helpers'
import {
Checkbox_Shadcn_,
cn,
@@ -14,6 +13,7 @@ import {
ContextMenuItem_Shadcn_,
ContextMenuSeparator_Shadcn_,
ContextMenuTrigger_Shadcn_,
+ copyToClipboard,
} from 'ui'
import { PROVIDERS_SCHEMAS } from '../AuthProvidersFormValidation'
import { ColumnConfiguration, USERS_TABLE_COLUMNS } from './Users.constants'
diff --git a/apps/studio/components/interfaces/Connect/ConnectionParameters.tsx b/apps/studio/components/interfaces/Connect/ConnectionParameters.tsx
index 3bb5f9edf62..d3654d9377c 100644
--- a/apps/studio/components/interfaces/Connect/ConnectionParameters.tsx
+++ b/apps/studio/components/interfaces/Connect/ConnectionParameters.tsx
@@ -1,13 +1,13 @@
import { Check, ChevronRight, Copy } from 'lucide-react'
import { useState } from 'react'
-import { copyToClipboard } from 'lib/helpers'
import {
Button,
cn,
Collapsible_Shadcn_,
CollapsibleContent_Shadcn_,
CollapsibleTrigger_Shadcn_,
+ copyToClipboard,
Separator,
} from 'ui'
diff --git a/apps/studio/components/interfaces/Functions/CommandRender.tsx b/apps/studio/components/interfaces/Functions/CommandRender.tsx
index 6b3a1afa803..8285ce8b3c2 100644
--- a/apps/studio/components/interfaces/Functions/CommandRender.tsx
+++ b/apps/studio/components/interfaces/Functions/CommandRender.tsx
@@ -1,6 +1,7 @@
import { Check, Clipboard } from 'lucide-react'
import { forwardRef, useState } from 'react'
-import { cn } from 'ui'
+
+import { cn, copyToClipboard } from 'ui'
const CommandRender = forwardRef(
({ commands, className }, ref) => {
@@ -35,10 +36,8 @@ const Command = ({ item }: any) => {
onClick={() => {
function onCopy(value: any) {
setIsCopied(true)
- navigator.clipboard.writeText(value).then()
- setTimeout(function () {
- setIsCopied(false)
- }, 3000)
+ copyToClipboard(value)
+ setTimeout(() => setIsCopied(false), 3000)
}
onCopy(item.command)
}}
diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx
index a0951552654..0d99237296c 100644
--- a/apps/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx
+++ b/apps/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx
@@ -8,7 +8,7 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectConte
import Table from 'components/to-be-cleaned/Table'
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
import type { EdgeFunctionsResponse } from 'data/edge-functions/edge-functions-query'
-import { Tooltip, TooltipContent, TooltipTrigger } from 'ui'
+import { copyToClipboard, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
interface EdgeFunctionsListItemProps {
function: EdgeFunctionsResponse
@@ -55,10 +55,8 @@ export const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemP
onClick={(event: any) => {
function onCopy(value: any) {
setIsCopied(true)
- navigator.clipboard.writeText(value).then()
- setTimeout(function () {
- setIsCopied(false)
- }, 3000)
+ copyToClipboard(value)
+ setTimeout(() => setIsCopied(false), 3000)
}
event.stopPropagation()
onCopy(endpoint)
diff --git a/apps/studio/components/interfaces/ProjectAPIDocs/Content/Introduction.tsx b/apps/studio/components/interfaces/ProjectAPIDocs/Content/Introduction.tsx
index 10edf7179ef..9ec3c001396 100644
--- a/apps/studio/components/interfaces/ProjectAPIDocs/Content/Introduction.tsx
+++ b/apps/studio/components/interfaces/ProjectAPIDocs/Content/Introduction.tsx
@@ -1,8 +1,7 @@
import { useParams } from 'common'
-import { Button, Input } from 'ui'
+import { Button, Input, copyToClipboard } from 'ui'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
-import { copyToClipboard } from 'lib/helpers'
import { Copy } from 'lucide-react'
import { useEffect, useState } from 'react'
import ContentSnippet from '../ContentSnippet'
diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/Results.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/Results.tsx
index a4bc9080273..32a96d76107 100644
--- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/Results.tsx
+++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/Results.tsx
@@ -3,13 +3,13 @@ import { useState } from 'react'
import DataGrid, { CalculatedColumn } from 'react-data-grid'
import { handleCopyCell } from 'components/grid/SupabaseGrid.utils'
-import { copyToClipboard } from 'lib/helpers'
import {
cn,
ContextMenu_Shadcn_,
ContextMenuContent_Shadcn_,
ContextMenuItem_Shadcn_,
ContextMenuTrigger_Shadcn_,
+ copyToClipboard,
} from 'ui'
import { CellDetailPanel } from './CellDetailPanel'
diff --git a/apps/studio/components/interfaces/Settings/Logs/LogTable.tsx b/apps/studio/components/interfaces/Settings/Logs/LogTable.tsx
index c19f5dde1b0..2006ee3dd82 100644
--- a/apps/studio/components/interfaces/Settings/Logs/LogTable.tsx
+++ b/apps/studio/components/interfaces/Settings/Logs/LogTable.tsx
@@ -11,10 +11,16 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { DownloadResultsButton } from 'components/ui/DownloadResultsButton'
import { useSelectedLog } from 'hooks/analytics/useSelectedLog'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
-import { copyToClipboard } from 'lib/helpers'
import { useProfile } from 'lib/profile'
import { ResponseError } from 'types'
-import { Button, ResizableHandle, ResizablePanel, ResizablePanelGroup, cn } from 'ui'
+import {
+ Button,
+ ResizableHandle,
+ ResizablePanel,
+ ResizablePanelGroup,
+ cn,
+ copyToClipboard,
+} from 'ui'
import AuthColumnRenderer from './LogColumnRenderers/AuthColumnRenderer'
import DatabaseApiColumnRender from './LogColumnRenderers/DatabaseApiColumnRender'
import DatabasePostgresColumnRender from './LogColumnRenderers/DatabasePostgresColumnRender'
diff --git a/apps/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx b/apps/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx
index a113cd1539f..f3576372338 100644
--- a/apps/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx
+++ b/apps/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx
@@ -7,10 +7,16 @@ import DatePicker from 'react-datepicker'
import { Label } from '@ui/components/shadcn/ui/label'
import { RadioGroup, RadioGroupItem } from '@ui/components/shadcn/ui/radio-group'
import TimeSplitInput from 'components/ui/DatePicker/TimeSplitInput'
-import { Button, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, Popover_Shadcn_, cn } from 'ui'
+import {
+ Button,
+ PopoverContent_Shadcn_,
+ PopoverTrigger_Shadcn_,
+ Popover_Shadcn_,
+ cn,
+ copyToClipboard,
+} from 'ui'
import { LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD } from './Logs.constants'
import type { DatetimeHelper } from './Logs.types'
-import { copyToClipboard } from 'lib/helpers'
export type DatePickerValue = {
to: string
diff --git a/apps/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx b/apps/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx
index d6bebe2d1db..5ea8b4292a3 100644
--- a/apps/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx
+++ b/apps/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx
@@ -1,17 +1,17 @@
+import dayjs from 'dayjs'
+import { BookOpen, Check, ChevronDown, Clipboard, ExternalLink, X } from 'lucide-react'
import Link from 'next/link'
import { ReactNode, useState } from 'react'
import { IS_PLATFORM } from 'common'
import Table from 'components/to-be-cleaned/Table'
-import dayjs from 'dayjs'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useFlag } from 'hooks/ui/useFlag'
-import { copyToClipboard } from 'lib/helpers'
-import { BookOpen, Check, ChevronDown, Clipboard, ExternalLink, X } from 'lucide-react'
import { logConstants } from 'shared-data'
import {
Badge,
Button,
+ copyToClipboard,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
diff --git a/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx b/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx
index 6387d26cb6a..d1e8269143b 100644
--- a/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx
+++ b/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx
@@ -27,11 +27,11 @@ import { isTableLike } from 'data/table-editor/table-editor-types'
import { fetchAllTableRows } from 'data/table-rows/table-rows-query'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { formatSql } from 'lib/formatSql'
-import { copyToClipboard } from 'lib/helpers'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { createTabId, useTabsStateSnapshot } from 'state/tabs'
import {
cn,
+ copyToClipboard,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.utils.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.utils.tsx
index f2f2d01d609..e20a143b53b 100644
--- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.utils.tsx
+++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.utils.tsx
@@ -1,8 +1,8 @@
+import { toast } from 'sonner'
+
import { downloadBucketObject } from 'data/storage/bucket-object-download-mutation'
import { StorageObject } from 'data/storage/bucket-objects-list-mutation'
-import { copyToClipboard } from 'lib/helpers'
-import { toast } from 'sonner'
-import { SONNER_DEFAULT_DURATION } from 'ui'
+import { SONNER_DEFAULT_DURATION, copyToClipboard } from 'ui'
import { STORAGE_ROW_STATUS, STORAGE_ROW_TYPES } from '../Storage.constants'
import { StorageItem, StorageItemMetadata, StorageItemWithColumn } from '../Storage.types'
diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/useCopyUrl.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/useCopyUrl.tsx
index e31ae431a0f..44a9f3b8125 100644
--- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/useCopyUrl.tsx
+++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/useCopyUrl.tsx
@@ -3,8 +3,8 @@ import { toast } from 'sonner'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
-import { copyToClipboard } from 'lib/helpers'
import { useStorageExplorerStateSnapshot } from 'state/storage-explorer'
+import { copyToClipboard } from 'ui'
import { URL_EXPIRY_DURATION } from '../Storage.constants'
import { fetchFileUrl } from './useFetchFileUrlQuery'
diff --git a/apps/studio/components/ui/CopyButton.tsx b/apps/studio/components/ui/CopyButton.tsx
index 38e2848a9f3..9ac56ba9df6 100644
--- a/apps/studio/components/ui/CopyButton.tsx
+++ b/apps/studio/components/ui/CopyButton.tsx
@@ -1,8 +1,7 @@
import { Check, Clipboard } from 'lucide-react'
-import React, { forwardRef, useEffect, useState } from 'react'
+import { ComponentProps, forwardRef, useEffect, useState } from 'react'
-import { copyToClipboard } from 'lib/helpers'
-import { Button, cn } from 'ui'
+import { Button, cn, copyToClipboard } from 'ui'
type CopyButtonBaseProps = {
iconOnly?: boolean
@@ -22,7 +21,7 @@ type CopyButtonWithAsyncText = CopyButtonBaseProps & {
}
export type CopyButtonProps = (CopyButtonWithText | CopyButtonWithAsyncText) &
- React.ComponentProps
+ ComponentProps
const CopyButton = forwardRef(
(
@@ -52,7 +51,7 @@ const CopyButton = forwardRef(
onClick={async (e) => {
const textToCopy = asyncText ? await asyncText() : text
setShowCopied(true)
- await copyToClipboard(textToCopy!)
+ copyToClipboard(textToCopy)
onClick?.(e)
}}
{...props}
diff --git a/apps/studio/components/ui/DownloadResultsButton.tsx b/apps/studio/components/ui/DownloadResultsButton.tsx
index ba2dee032bd..715e5aa7acb 100644
--- a/apps/studio/components/ui/DownloadResultsButton.tsx
+++ b/apps/studio/components/ui/DownloadResultsButton.tsx
@@ -5,9 +5,9 @@ import Papa from 'papaparse'
import { useMemo } from 'react'
import { toast } from 'sonner'
-import { copyToClipboard } from 'lib/helpers'
import {
Button,
+ copyToClipboard,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
diff --git a/apps/studio/components/ui/Logs/LogsExplorerHeader.tsx b/apps/studio/components/ui/Logs/LogsExplorerHeader.tsx
index 547924858ab..ae32da4ba62 100644
--- a/apps/studio/components/ui/Logs/LogsExplorerHeader.tsx
+++ b/apps/studio/components/ui/Logs/LogsExplorerHeader.tsx
@@ -4,9 +4,16 @@ import { useState } from 'react'
import { LOGS_EXPLORER_DOCS_URL } from 'components/interfaces/Settings/Logs/Logs.constants'
import Table from 'components/to-be-cleaned/Table'
-import { copyToClipboard } from 'lib/helpers'
import { logConstants } from 'shared-data'
-import { Button, SidePanel, Tabs, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
+import {
+ Button,
+ SidePanel,
+ Tabs,
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+ copyToClipboard,
+} from 'ui'
import { DocsButton } from '../DocsButton'
export interface LogsExplorerHeaderProps {
diff --git a/apps/studio/lib/helpers.test.ts b/apps/studio/lib/helpers.test.ts
index 63e4f88ebcf..419e90a75eb 100644
--- a/apps/studio/lib/helpers.test.ts
+++ b/apps/studio/lib/helpers.test.ts
@@ -1,29 +1,30 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
- tryParseJson,
- minifyJSON,
- prettifyJSON,
- removeJSONTrailingComma,
- timeout,
- getURL,
- makeRandomString,
- pluckObjectFields,
- tryParseInt,
- propsAreEqual,
- formatBytes,
- snakeToCamel,
- copyToClipboard,
detectBrowser,
detectOS,
- pluralize,
- isValidHttpUrl,
- removeCommentsFromSql,
- getSemanticVersion,
+ formatBytes,
+ formatCurrency,
getDatabaseMajorVersion,
getDistanceLatLonKM,
- formatCurrency,
+ getSemanticVersion,
+ getURL,
+ isValidHttpUrl,
+ makeRandomString,
+ minifyJSON,
+ pluckObjectFields,
+ pluralize,
+ prettifyJSON,
+ propsAreEqual,
+ removeCommentsFromSql,
+ removeJSONTrailingComma,
+ snakeToCamel,
+ timeout,
+ tryParseInt,
+ tryParseJson,
} from './helpers'
+import { copyToClipboard } from 'ui'
+
describe('tryParseJson', () => {
it('should return the parsed JSON', () => {
const result = tryParseJson('{"test": "test"}')
@@ -170,6 +171,9 @@ describe('copyToClipboard', () => {
},
})
+ // CopyToClipboard uses setTimeout to call the callback
+ vi.useFakeTimers()
+
// If ClipboardItem is used
vi.stubGlobal('ClipboardItem', function (items: any) {
return items
@@ -181,10 +185,12 @@ describe('copyToClipboard', () => {
afterEach(() => {
vi.unstubAllGlobals()
+ vi.useRealTimers()
})
it('uses clipboard.write if available', async () => {
await copyToClipboard('hello')
+ vi.runAllTimers()
expect(writeMock).toHaveBeenCalled()
})
diff --git a/apps/studio/lib/helpers.ts b/apps/studio/lib/helpers.ts
index e5baf72bfd8..268297a597e 100644
--- a/apps/studio/lib/helpers.ts
+++ b/apps/studio/lib/helpers.ts
@@ -1,6 +1,3 @@
-import { noop } from 'lodash'
-import { toast } from 'sonner'
-
export { default as passwordStrength } from './password-strength'
export { default as uuidv4 } from './uuid'
@@ -146,42 +143,6 @@ export const snakeToCamel = (str: string) =>
group.toUpperCase().replace('-', '').replace('_', '')
)
-/**
- * Copy text content (string or Promise) into Clipboard. Safari doesn't support write text into clipboard async,
- * so if you need to load text content async before coping, please use Promise for the 1st arg.
- *
- * IF YOU NEED TO CHANGE THIS FUNCTION, PLEASE TEST IT IN SAFARI with a promised string. Expiring URL to a file in a
- * private bucket will do.
- *
- * Copied code from https://wolfgangrittner.dev/how-to-use-clipboard-api-in-firefox/
- */
-export const copyToClipboard = async (str: string | Promise, callback = noop) => {
- const focused = window.document.hasFocus()
- if (focused) {
- if (typeof ClipboardItem && navigator.clipboard?.write) {
- // NOTE: Safari locks down the clipboard API to only work when triggered
- // by a direct user interaction. You can't use it async in a promise.
- // But! You can wrap the promise in a ClipboardItem, and give that to
- // the clipboard API.
- // Found this on https://developer.apple.com/forums/thread/691873
- const text = new ClipboardItem({
- 'text/plain': Promise.resolve(str).then((text) => new Blob([text], { type: 'text/plain' })),
- })
- navigator.clipboard.write([text]).then(callback)
- } else {
- // NOTE: Firefox has support for ClipboardItem and navigator.clipboard.write,
- // but those are behind `dom.events.asyncClipboard.clipboardItem` preference.
- // Good news is that other than Safari, Firefox does not care about
- // Clipboard API being used async in a Promise.
- Promise.resolve(str)
- .then((text) => navigator.clipboard?.writeText(text))
- .then(callback)
- }
- } else {
- toast.error('Unable to copy to clipboard')
- }
-}
-
export const detectBrowser = () => {
if (!navigator) return undefined
diff --git a/apps/studio/pages/support/new.tsx b/apps/studio/pages/support/new.tsx
index 5f3a76beffb..9f94fccad75 100644
--- a/apps/studio/pages/support/new.tsx
+++ b/apps/studio/pages/support/new.tsx
@@ -3,6 +3,7 @@ import Link from 'next/link'
import { useState } from 'react'
import SVG from 'react-inlinesvg'
+import { AIAssistantOption } from 'components/interfaces/Support/AIAssistantOption'
import Success from 'components/interfaces/Support/Success'
import { SupportFormV2 } from 'components/interfaces/Support/SupportFormV2'
import AppLayout from 'components/layouts/AppLayout/AppLayout'
@@ -15,8 +16,7 @@ import { withAuth } from 'hooks/misc/withAuth'
import { BASE_PATH } from 'lib/constants'
import { toast } from 'sonner'
import { NextPageWithLayout } from 'types'
-import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
-import { AIAssistantOption } from 'components/interfaces/Support/AIAssistantOption'
+import { Button, copyToClipboard, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
const SupportPage: NextPageWithLayout = () => {
const [sentCategory, setSentCategory] = useState()
@@ -118,9 +118,7 @@ const SupportPage: NextPageWithLayout = () => {
type="text"
text="support@supabase.com"
iconOnly
- onClick={() => {
- toast.success('Copied to clipboard')
- }}
+ onClick={() => toast.success('Copied to clipboard')}
/>
@@ -139,7 +137,7 @@ const SupportPage: NextPageWithLayout = () => {