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 = () => {