mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
fix: Make the onCopyUrl work on Safari (#27215)
* Add a new hook for copying URLs. * Refactor the copyToClipboard function. * Make the onCopyUrl function awaitable. * Fix the test. --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
1 parent
c9ebb812c9
commit
0895081f4f
11 files changed
+99
-93
No files matched your search
@@ -4,10 +4,7 @@ import { Button, Form, Input, Listbox, Modal } from 'ui'
|
||||
|
||||
import { DATETIME_FORMAT } from 'lib/constants'
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
|
||||
export interface CustomExpiryModalProps {
|
||||
onCopyUrl: (name: string, url: string) => void
|
||||
}
|
||||
import { useCopyUrl } from './useCopyUrl'
|
||||
|
||||
const unitMap = {
|
||||
days: 3600 * 24,
|
||||
@@ -16,8 +13,9 @@ const unitMap = {
|
||||
years: 3600 * 24 * 365,
|
||||
}
|
||||
|
||||
const CustomExpiryModal = ({ onCopyUrl }: CustomExpiryModalProps) => {
|
||||
const CustomExpiryModal = () => {
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const { onCopyUrl } = useCopyUrl(storageExplorerStore.projectRef)
|
||||
const { getFileUrl, selectedFileCustomExpiry, setSelectedFileCustomExpiry } = storageExplorerStore
|
||||
|
||||
const visible = selectedFileCustomExpiry !== undefined
|
||||
@@ -38,9 +36,9 @@ const CustomExpiryModal = ({ onCopyUrl }: CustomExpiryModalProps) => {
|
||||
initialValues={{ expiresIn: '', units: 'days' }}
|
||||
onSubmit={async (values: any, { setSubmitting }: any) => {
|
||||
setSubmitting(true)
|
||||
onCopyUrl(
|
||||
await onCopyUrl(
|
||||
selectedFileCustomExpiry!.name,
|
||||
await getFileUrl(
|
||||
getFileUrl(
|
||||
selectedFileCustomExpiry!,
|
||||
values.expiresIn * unitMap[values.units as 'days' | 'weeks' | 'months' | 'years']
|
||||
)
|
||||
|
||||
@@ -20,7 +20,6 @@ export interface FileExplorerProps {
|
||||
onSelectAllItemsInColumn: (index: number) => void
|
||||
onSelectColumnEmptySpace: (index: number) => void
|
||||
onColumnLoadMore: (index: number, column: StorageColumn) => void
|
||||
onCopyUrl: (name: string, url: string) => void
|
||||
}
|
||||
|
||||
const FileExplorer = ({
|
||||
@@ -34,7 +33,6 @@ const FileExplorer = ({
|
||||
onSelectAllItemsInColumn = noop,
|
||||
onSelectColumnEmptySpace = noop,
|
||||
onColumnLoadMore = noop,
|
||||
onCopyUrl = noop,
|
||||
}: FileExplorerProps) => {
|
||||
const fileExplorerRef = useRef<any>(null)
|
||||
|
||||
@@ -53,7 +51,7 @@ const FileExplorer = ({
|
||||
className="file-explorer flex flex-grow overflow-x-auto justify-between h-full w-full"
|
||||
>
|
||||
<ColumnContextMenu id={CONTEXT_MENU_KEYS.STORAGE_COLUMN} />
|
||||
<ItemContextMenu id={CONTEXT_MENU_KEYS.STORAGE_ITEM} onCopyUrl={onCopyUrl} />
|
||||
<ItemContextMenu id={CONTEXT_MENU_KEYS.STORAGE_ITEM} />
|
||||
<FolderContextMenu id={CONTEXT_MENU_KEYS.STORAGE_FOLDER} />
|
||||
{view === STORAGE_VIEWS.COLUMNS ? (
|
||||
<div className="flex">
|
||||
@@ -71,7 +69,6 @@ const FileExplorer = ({
|
||||
onSelectAllItemsInColumn={onSelectAllItemsInColumn}
|
||||
onSelectColumnEmptySpace={onSelectColumnEmptySpace}
|
||||
onColumnLoadMore={onColumnLoadMore}
|
||||
onCopyUrl={onCopyUrl}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -90,7 +87,6 @@ const FileExplorer = ({
|
||||
onSelectAllItemsInColumn={onSelectAllItemsInColumn}
|
||||
onSelectColumnEmptySpace={onSelectColumnEmptySpace}
|
||||
onColumnLoadMore={onColumnLoadMore}
|
||||
onCopyUrl={onCopyUrl}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -66,7 +66,6 @@ export interface FileExplorerColumnProps {
|
||||
onSelectAllItemsInColumn: (index: number) => void
|
||||
onSelectColumnEmptySpace: (index: number) => void
|
||||
onColumnLoadMore: (index: number, column: StorageColumn) => void
|
||||
onCopyUrl: (name: string, url: string) => void
|
||||
}
|
||||
|
||||
const FileExplorerColumn = ({
|
||||
@@ -82,7 +81,6 @@ const FileExplorerColumn = ({
|
||||
onSelectAllItemsInColumn = noop,
|
||||
onSelectColumnEmptySpace = noop,
|
||||
onColumnLoadMore = noop,
|
||||
onCopyUrl = noop,
|
||||
}: FileExplorerColumnProps) => {
|
||||
const [isDraggedOver, setIsDraggedOver] = useState(false)
|
||||
const fileExplorerColumnRef = useRef<any>(null)
|
||||
@@ -224,7 +222,6 @@ const FileExplorerColumn = ({
|
||||
selectedItems,
|
||||
openedFolders,
|
||||
selectedFilePreview,
|
||||
onCopyUrl,
|
||||
}}
|
||||
ItemComponent={FileExplorerRow}
|
||||
getItemSize={(index) => (index !== 0 && index === columnItems.length ? 85 : 37)}
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
import { StorageItem, StorageItemWithColumn } from '../Storage.types'
|
||||
import FileExplorerRowEditing from './FileExplorerRowEditing'
|
||||
import { copyPathToFolder } from './StorageExplorer.utils'
|
||||
import { useCopyUrl } from './useCopyUrl'
|
||||
|
||||
export const RowIcon = ({
|
||||
view,
|
||||
@@ -97,7 +98,6 @@ export interface FileExplorerRowProps {
|
||||
selectedItems: StorageItemWithColumn[]
|
||||
openedFolders: StorageItem[]
|
||||
selectedFilePreview: (StorageItemWithColumn & { previewUrl: string | undefined }) | null
|
||||
onCopyUrl: (name: string, url: string) => void
|
||||
}
|
||||
|
||||
const FileExplorerRow: ItemRenderer<StorageItem, FileExplorerRowProps> = ({
|
||||
@@ -108,7 +108,6 @@ const FileExplorerRow: ItemRenderer<StorageItem, FileExplorerRowProps> = ({
|
||||
selectedItems = [],
|
||||
openedFolders = [],
|
||||
selectedFilePreview,
|
||||
onCopyUrl,
|
||||
}) => {
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const {
|
||||
@@ -130,6 +129,7 @@ const FileExplorerRow: ItemRenderer<StorageItem, FileExplorerRowProps> = ({
|
||||
downloadFolder,
|
||||
selectRangeItems,
|
||||
} = storageExplorerStore
|
||||
const { onCopyUrl } = useCopyUrl(storageExplorerStore.projectRef)
|
||||
|
||||
const isPublic = selectedBucket.public
|
||||
const itemWithColumnIndex = { ...item, columnIndex }
|
||||
@@ -213,11 +213,8 @@ const FileExplorerRow: ItemRenderer<StorageItem, FileExplorerRowProps> = ({
|
||||
{
|
||||
name: 'Get URL',
|
||||
icon: <IconClipboard size="tiny" />,
|
||||
onClick: async () =>
|
||||
onCopyUrl(
|
||||
itemWithColumnIndex.name,
|
||||
await getFileUrl(itemWithColumnIndex)
|
||||
),
|
||||
onClick: () =>
|
||||
onCopyUrl(itemWithColumnIndex.name, getFileUrl(itemWithColumnIndex)),
|
||||
},
|
||||
]
|
||||
: [
|
||||
@@ -227,26 +224,26 @@ const FileExplorerRow: ItemRenderer<StorageItem, FileExplorerRowProps> = ({
|
||||
children: [
|
||||
{
|
||||
name: 'Expire in 1 week',
|
||||
onClick: async () =>
|
||||
onClick: () =>
|
||||
onCopyUrl(
|
||||
itemWithColumnIndex.name,
|
||||
await getFileUrl(itemWithColumnIndex, URL_EXPIRY_DURATION.WEEK)
|
||||
getFileUrl(itemWithColumnIndex, URL_EXPIRY_DURATION.WEEK)
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Expire in 1 month',
|
||||
onClick: async () =>
|
||||
onClick: () =>
|
||||
onCopyUrl(
|
||||
itemWithColumnIndex.name,
|
||||
await getFileUrl(itemWithColumnIndex, URL_EXPIRY_DURATION.MONTH)
|
||||
getFileUrl(itemWithColumnIndex, URL_EXPIRY_DURATION.MONTH)
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Expire in 1 year',
|
||||
onClick: async () =>
|
||||
onClick: () =>
|
||||
onCopyUrl(
|
||||
itemWithColumnIndex.name,
|
||||
await getFileUrl(itemWithColumnIndex, URL_EXPIRY_DURATION.YEAR)
|
||||
getFileUrl(itemWithColumnIndex, URL_EXPIRY_DURATION.YEAR)
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { noop } from 'lodash'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Item, Menu, Separator, Submenu } from 'react-contexify'
|
||||
import 'react-contexify/dist/ReactContexify.css'
|
||||
@@ -9,13 +8,13 @@ import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStor
|
||||
import { IconChevronRight, IconClipboard, IconDownload, IconEdit, IconMove, IconTrash2 } from 'ui'
|
||||
import { URL_EXPIRY_DURATION } from '../Storage.constants'
|
||||
import { StorageItemWithColumn } from '../Storage.types'
|
||||
import { useCopyUrl } from './useCopyUrl'
|
||||
|
||||
interface ItemContextMenuProps {
|
||||
id: string
|
||||
onCopyUrl: (name: string, url: string) => void
|
||||
}
|
||||
|
||||
const ItemContextMenu = ({ id = '', onCopyUrl = noop }: ItemContextMenuProps) => {
|
||||
const ItemContextMenu = ({ id = '' }: ItemContextMenuProps) => {
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const {
|
||||
getFileUrl,
|
||||
@@ -26,6 +25,7 @@ const ItemContextMenu = ({ id = '', onCopyUrl = noop }: ItemContextMenuProps) =>
|
||||
setSelectedItemsToMove,
|
||||
setSelectedFileCustomExpiry,
|
||||
} = storageExplorerStore
|
||||
const { onCopyUrl } = useCopyUrl(storageExplorerStore.projectRef)
|
||||
const isPublic = selectedBucket.public
|
||||
const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*')
|
||||
|
||||
@@ -34,7 +34,7 @@ const ItemContextMenu = ({ id = '', onCopyUrl = noop }: ItemContextMenuProps) =>
|
||||
switch (event) {
|
||||
case 'copy':
|
||||
if (expiresIn !== undefined && expiresIn < 0) return setSelectedFileCustomExpiry(item)
|
||||
else return onCopyUrl(item.name, await getFileUrl(item, expiresIn))
|
||||
else return onCopyUrl(item.name, getFileUrl(item, expiresIn))
|
||||
case 'rename':
|
||||
return setSelectedItemToRename(item)
|
||||
case 'move':
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
IconX,
|
||||
} from 'ui'
|
||||
import { URL_EXPIRY_DURATION } from '../Storage.constants'
|
||||
import { useCopyUrl } from './useCopyUrl'
|
||||
|
||||
const PreviewFile = ({ mimeType, previewUrl }: { mimeType?: string; previewUrl?: string }) => {
|
||||
if (!mimeType || !previewUrl) {
|
||||
@@ -99,11 +100,7 @@ const PreviewFile = ({ mimeType, previewUrl }: { mimeType?: string; previewUrl?:
|
||||
)
|
||||
}
|
||||
|
||||
export interface PreviewPaneProps {
|
||||
onCopyUrl: (name: string, url: string) => void
|
||||
}
|
||||
|
||||
const PreviewPane = ({ onCopyUrl }: PreviewPaneProps) => {
|
||||
const PreviewPane = () => {
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const {
|
||||
getFileUrl,
|
||||
@@ -114,6 +111,7 @@ const PreviewPane = ({ onCopyUrl }: PreviewPaneProps) => {
|
||||
setSelectedItemsToDelete,
|
||||
setSelectedFileCustomExpiry,
|
||||
} = storageExplorerStore
|
||||
const { onCopyUrl } = useCopyUrl(storageExplorerStore.projectRef)
|
||||
|
||||
const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*')
|
||||
|
||||
@@ -208,7 +206,7 @@ const PreviewPane = ({ onCopyUrl }: PreviewPaneProps) => {
|
||||
<Button
|
||||
type="outline"
|
||||
icon={<IconClipboard size={16} strokeWidth={2} />}
|
||||
onClick={async () => onCopyUrl(file.name, await getFileUrl(file))}
|
||||
onClick={() => onCopyUrl(file.name, getFileUrl(file))}
|
||||
disabled={file.isCorrupted}
|
||||
>
|
||||
Get URL
|
||||
@@ -228,24 +226,24 @@ const PreviewPane = ({ onCopyUrl }: PreviewPaneProps) => {
|
||||
<DropdownMenuContent side="bottom" align="center">
|
||||
<DropdownMenuItem
|
||||
key="expires-one-week"
|
||||
onClick={async () =>
|
||||
onCopyUrl(file.name, await getFileUrl(file, URL_EXPIRY_DURATION.WEEK))
|
||||
onClick={() =>
|
||||
onCopyUrl(file.name, getFileUrl(file, URL_EXPIRY_DURATION.WEEK))
|
||||
}
|
||||
>
|
||||
Expire in 1 week
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
key="expires-one-month"
|
||||
onClick={async () =>
|
||||
onCopyUrl(file.name, await getFileUrl(file, URL_EXPIRY_DURATION.MONTH))
|
||||
onClick={() =>
|
||||
onCopyUrl(file.name, getFileUrl(file, URL_EXPIRY_DURATION.MONTH))
|
||||
}
|
||||
>
|
||||
Expire in 1 month
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
key="expires-one-year"
|
||||
onClick={async () =>
|
||||
onCopyUrl(file.name, await getFileUrl(file, URL_EXPIRY_DURATION.YEAR))
|
||||
onClick={() =>
|
||||
onCopyUrl(file.name, getFileUrl(file, URL_EXPIRY_DURATION.YEAR))
|
||||
}
|
||||
>
|
||||
Expire in 1 year
|
||||
|
||||
@@ -2,14 +2,10 @@ import { useParams } from 'common'
|
||||
import { compact, get, isEmpty, uniqBy } from 'lodash'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import { useProjectSettingsQuery } from 'data/config/project-settings-query'
|
||||
import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query'
|
||||
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
|
||||
import type { Bucket } from 'data/storage/buckets-query'
|
||||
import { DEFAULT_PROJECT_API_SERVICE_ID, IS_PLATFORM } from 'lib/constants'
|
||||
import { copyToClipboard } from 'lib/helpers'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
import { STORAGE_ROW_TYPES, STORAGE_VIEWS } from '../Storage.constants'
|
||||
import ConfirmDeleteModal from './ConfirmDeleteModal'
|
||||
@@ -57,19 +53,11 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => {
|
||||
const storageExplorerRef = useRef(null)
|
||||
|
||||
const { ref } = useParams()
|
||||
const { data: customDomainData } = useCustomDomainsQuery({ projectRef: ref })
|
||||
const { data: projectSettings } = useProjectSettingsQuery({ projectRef: ref })
|
||||
|
||||
// [Joshen] This is to ensure that StorageExplorerStore can get the storage file size limit
|
||||
// Will be better once we deprecate the mobx store entirely, which we will get there
|
||||
useProjectStorageConfigQuery({ projectRef: ref }, { enabled: IS_PLATFORM })
|
||||
|
||||
const apiService = (projectSettings?.services ?? []).find(
|
||||
(x) => x.app.id == DEFAULT_PROJECT_API_SERVICE_ID
|
||||
)
|
||||
const apiConfig = apiService?.app_config
|
||||
const apiUrl = `${apiConfig?.protocol ?? 'https'}://${apiConfig?.endpoint ?? '-'}`
|
||||
|
||||
// This state exists outside of the header because FileExplorerColumn needs to listen to these as well
|
||||
// I'm keeping them outside of the mobx store as I feel that the store should contain persistent data
|
||||
// Things like showing results from a search filter is "temporary", hence we use react state to manage
|
||||
@@ -190,16 +178,6 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => {
|
||||
clearSelectedItems()
|
||||
}
|
||||
|
||||
const onCopyUrl = (name: string, url: string) => {
|
||||
const formattedUrl =
|
||||
customDomainData?.customDomain?.status === 'active'
|
||||
? url.replace(apiUrl, `https://${customDomainData.customDomain.hostname}`)
|
||||
: url
|
||||
copyToClipboard(formattedUrl, () => {
|
||||
toast.success(`Copied URL for ${name} to clipboard.`)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={storageExplorerRef}
|
||||
@@ -231,9 +209,8 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => {
|
||||
onColumnLoadMore={(index, column) =>
|
||||
fetchMoreFolderContents(index, column, itemSearchString)
|
||||
}
|
||||
onCopyUrl={onCopyUrl}
|
||||
/>
|
||||
<PreviewPane onCopyUrl={onCopyUrl} />
|
||||
<PreviewPane />
|
||||
</div>
|
||||
<ConfirmDeleteModal
|
||||
visible={selectedItemsToDelete.length > 0}
|
||||
@@ -248,7 +225,7 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => {
|
||||
onSelectCancel={clearSelectedItemsToMove}
|
||||
onSelectMove={onMoveSelectedFiles}
|
||||
/>
|
||||
<CustomExpiryModal onCopyUrl={onCopyUrl} />
|
||||
<CustomExpiryModal />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useProjectSettingsQuery } from 'data/config/project-settings-query'
|
||||
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
|
||||
import { DEFAULT_PROJECT_API_SERVICE_ID } from 'lib/constants/infrastructure'
|
||||
import { copyToClipboard } from 'lib/helpers'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
export const useCopyUrl = (ref: string) => {
|
||||
const { data: customDomainData } = useCustomDomainsQuery({ projectRef: ref })
|
||||
const { data: projectSettings } = useProjectSettingsQuery({ projectRef: ref })
|
||||
|
||||
const apiService = (projectSettings?.services ?? []).find(
|
||||
(x) => x.app.id == DEFAULT_PROJECT_API_SERVICE_ID
|
||||
)
|
||||
const apiConfig = apiService?.app_config
|
||||
const apiUrl = `${apiConfig?.protocol ?? 'https'}://${apiConfig?.endpoint ?? '-'}`
|
||||
|
||||
const onCopyUrl = (name: string, url: string | Promise<string>) => {
|
||||
const formattedUrl = Promise.resolve(url).then((url) => {
|
||||
return customDomainData?.customDomain?.status === 'active'
|
||||
? url.replace(apiUrl, `https://${customDomainData.customDomain.hostname}`)
|
||||
: url
|
||||
})
|
||||
|
||||
return copyToClipboard(formattedUrl, () => {
|
||||
toast.success(`Copied URL for ${name} to clipboard.`)
|
||||
})
|
||||
}
|
||||
|
||||
return { onCopyUrl }
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { copyToClipboard } from 'lib/helpers'
|
||||
import { Check, Clipboard } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { copyToClipboard } from 'lib/helpers'
|
||||
import { Button, ButtonProps } from 'ui'
|
||||
|
||||
export interface CopyButtonProps extends ButtonProps {
|
||||
@@ -28,9 +29,9 @@ const CopyButton = ({
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
onClick={async (e) => {
|
||||
setShowCopied(true)
|
||||
copyToClipboard(text)
|
||||
await copyToClipboard(text)
|
||||
onClick?.(e)
|
||||
}}
|
||||
icon={
|
||||
|
||||
+31
-19
@@ -1,5 +1,7 @@
|
||||
import { noop } from 'lodash'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
export { default as passwordStrength } from './password-strength'
|
||||
export { default as uuidv4 } from './uuid'
|
||||
|
||||
export const tryParseJson = (jsonString: any) => {
|
||||
@@ -136,28 +138,38 @@ export const snakeToCamel = (str: string) =>
|
||||
)
|
||||
|
||||
/**
|
||||
* Copy text content (string or Promise<string>) 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<string> for the 1st arg.
|
||||
* Copy text content (string or Promise<string>) 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<string> 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<string>, callback = () => {}) => {
|
||||
try {
|
||||
const focused = window.document.hasFocus()
|
||||
if (focused) {
|
||||
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
||||
const text = await Promise.resolve(str)
|
||||
Promise.resolve(window.navigator?.clipboard?.writeText(text)).then(callback)
|
||||
return
|
||||
}
|
||||
|
||||
Promise.resolve(str)
|
||||
.then((text) => window.navigator?.clipboard?.writeText(text))
|
||||
.then(callback)
|
||||
export const copyToClipboard = async (str: string | Promise<string>, 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 {
|
||||
console.warn('Unable to copy to clipboard')
|
||||
// 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)
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(`Unable to copy to clipboard: ${error.message}`)
|
||||
} else {
|
||||
toast.error('Unable to copy to clipboard')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ const EMPTY_FOLDER_PLACEHOLDER_FILE_NAME = '.emptyFolderPlaceholder'
|
||||
const STORAGE_PROGRESS_INFO_TEXT = "Do not close the browser until it's completed"
|
||||
|
||||
class StorageExplorerStore {
|
||||
private projectRef: string = ''
|
||||
projectRef: string = ''
|
||||
view: STORAGE_VIEWS = STORAGE_VIEWS.COLUMNS
|
||||
sortBy: STORAGE_SORT_BY = STORAGE_SORT_BY.NAME
|
||||
sortByOrder: STORAGE_SORT_BY_ORDER = STORAGE_SORT_BY_ORDER.ASC
|
||||
|
||||
Reference in new issue
Block a user