mirror of
https://github.com/supabase/supabase.git
synced 2026-10-18 23:59:08 +03:00
* Update the design of the sonner toasts. Add the close button by default. * Migrate studio and www apps to use the SonnerToaster. * Migrate all toasts from studio. * Migrate all leftover toasts in studio. * Add a new toast component with progress. Use it in studio. * Migrate the design-system app. * Refactor the consent toast to use sonner. * Switch docs to use the new sonner toasts. * Remove toast examples from the design-system app. * Remove all toast-related components and old code. * Fix the progress bar in the toast progress component. Also make the bottom components vertically centered. * Fix the width of the toast progress. * Use text-foreground-lighter instead of muted for ToastProgress text * Rename ToastProgress to SonnerProgress. * Shorten the text in sonner progress. * Use the correct classes for the close button. Add a const var for the default toast duration. Remove the custom width class from sonner. * Set the position for all progress toasts to bottom right. Set the duration for all toasts to the default (when reusing a toast id from loading/progress toast, the duration is set to infinity). * Fix the playwright tests. * Refactor imports to use ui instead of @ui. * Change all imports of react-hot-toast with sonner. These components were merged since the last commit to this branch. * Remove react-hot-toast lib. --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com> Co-authored-by: Jonathan Summers-Muir <MildTomato@users.noreply.github.com>
240 lines
8.0 KiB
TypeScript
240 lines
8.0 KiB
TypeScript
import { useParams } from 'common'
|
|
import { noop } from 'lodash'
|
|
import { Eye, Unlock } from 'lucide-react'
|
|
import { useEffect, useRef, useState } from 'react'
|
|
import { toast } from 'sonner'
|
|
|
|
import DownloadSnippetModal from 'components/interfaces/SQLEditor/DownloadSnippetModal'
|
|
import RenameQueryModal from 'components/interfaces/SQLEditor/RenameQueryModal'
|
|
import SimpleCodeBlock from 'components/to-be-cleaned/SimpleCodeBlock'
|
|
import CopyButton from 'components/ui/CopyButton'
|
|
import { useContentDeleteMutation } from 'data/content/content-delete-mutation'
|
|
import type { SqlSnippet } from 'data/content/sql-snippets-query'
|
|
import { useSqlEditorStateSnapshot } from 'state/sql-editor'
|
|
import {
|
|
Checkbox_Shadcn_,
|
|
HoverCardContent_Shadcn_,
|
|
HoverCardTrigger_Shadcn_,
|
|
HoverCard_Shadcn_,
|
|
cn,
|
|
} from 'ui'
|
|
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
|
import { InnerSideMenuItem } from 'ui-patterns/InnerSideMenu'
|
|
import { QueryItemActions } from './QueryItemActions'
|
|
|
|
export interface QueryItemProps {
|
|
tabInfo: SqlSnippet
|
|
isSelected?: boolean
|
|
hasQueriesSelected?: boolean
|
|
onSelectQuery?: (isShiftHeld: boolean) => void
|
|
onDeleteQuery?: (ids: string[]) => void
|
|
}
|
|
|
|
const QueryItem = ({
|
|
tabInfo,
|
|
isSelected = false,
|
|
hasQueriesSelected = false,
|
|
onSelectQuery = noop,
|
|
onDeleteQuery = noop,
|
|
}: QueryItemProps) => {
|
|
const [open, setOpen] = useState(false)
|
|
const [renameModalOpen, setRenameModalOpen] = useState(false)
|
|
const [shareModalOpen, setShareModalOpen] = useState(false)
|
|
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
|
|
const [isDownloadSnippetModalOpen, setIsDownloadSnippetModalOpen] = useState(false)
|
|
|
|
const { ref, id: activeId } = useParams()
|
|
const snap = useSqlEditorStateSnapshot()
|
|
const { id, name, description, content, visibility, type } = tabInfo || {}
|
|
const isSQLSnippet = type === 'sql'
|
|
const isActive = id === activeId
|
|
const activeItemRef = useRef<HTMLElement | null>(null)
|
|
|
|
const hideTooltip =
|
|
open ||
|
|
isActive ||
|
|
renameModalOpen ||
|
|
shareModalOpen ||
|
|
deleteModalOpen ||
|
|
isDownloadSnippetModalOpen
|
|
|
|
const { mutate: deleteContent, isLoading: isDeleting } = useContentDeleteMutation({
|
|
onSuccess: (data) => {
|
|
onDeleteQuery(data)
|
|
},
|
|
onError: (error, data) => {
|
|
if (error.message.includes('Contents not found')) {
|
|
onDeleteQuery(data.ids)
|
|
} else {
|
|
toast.error(`Failed to delete query: ${error.message}`)
|
|
}
|
|
},
|
|
})
|
|
|
|
const onConfirmDelete = async () => {
|
|
if (!ref) return console.error('Project ref is required')
|
|
if (!id) return console.error('Snippet ID is required')
|
|
deleteContent({ projectRef: ref, ids: [id] })
|
|
}
|
|
|
|
useEffect(() => {
|
|
// scroll to active item
|
|
if (isActive && activeItemRef.current) {
|
|
// race condition hack
|
|
setTimeout(() => {
|
|
activeItemRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
|
}, 0)
|
|
}
|
|
})
|
|
|
|
const onConfirmShare = async () => {
|
|
if (id) {
|
|
try {
|
|
snap.shareSnippet(id, 'project')
|
|
return Promise.resolve()
|
|
} catch (error: any) {
|
|
toast.error(`Failed to share query: ${error.message}`)
|
|
}
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<HoverCard_Shadcn_ openDelay={200}>
|
|
<HoverCardTrigger_Shadcn_ asChild>
|
|
<InnerSideMenuItem
|
|
title={description || name}
|
|
ref={isActive ? (activeItemRef as React.RefObject<HTMLAnchorElement>) : null}
|
|
href={`/project/${ref}/sql/${id}`}
|
|
key={id}
|
|
className={'flex gap-3 items-center'}
|
|
forceHoverState={open}
|
|
isActive={isActive}
|
|
>
|
|
{visibility === 'user' && (
|
|
<Checkbox_Shadcn_
|
|
className={cn(
|
|
'transition absolute left-2.5 border-strong',
|
|
hasQueriesSelected ? '' : 'opacity-0 group-hover:opacity-100'
|
|
)}
|
|
checked={isSelected}
|
|
onCheckedChange={onSelectQuery}
|
|
/>
|
|
)}
|
|
<span
|
|
className={cn(
|
|
'transition-all',
|
|
'w-full overflow-hidden truncate',
|
|
hasQueriesSelected && visibility === 'user'
|
|
? 'ml-5'
|
|
: visibility === 'user'
|
|
? 'group-hover:ml-5'
|
|
: '',
|
|
'text-ellipsis truncate'
|
|
)}
|
|
>
|
|
{name}
|
|
</span>
|
|
{!hasQueriesSelected && (
|
|
<QueryItemActions
|
|
tabInfo={tabInfo}
|
|
open={open}
|
|
setOpen={setOpen}
|
|
onSelectDeleteQuery={() => setDeleteModalOpen(true)}
|
|
onSelectRenameQuery={() => setRenameModalOpen(true)}
|
|
onSelectShareQuery={() => setShareModalOpen(true)}
|
|
onSelectDownloadQuery={() => setIsDownloadSnippetModalOpen(true)}
|
|
/>
|
|
)}
|
|
</InnerSideMenuItem>
|
|
</HoverCardTrigger_Shadcn_>
|
|
{!hideTooltip && isSQLSnippet && (
|
|
<HoverCardContent_Shadcn_ side="right" align="center" className="w-96" animate="slide-in">
|
|
<>
|
|
{content.sql.trim() ? (
|
|
<SimpleCodeBlock
|
|
showCopy={false}
|
|
className="sql"
|
|
parentClassName="!p-0 [&>div>span]:text-xs [&>div>span]:tracking-tighter"
|
|
>
|
|
{content.sql.replaceAll('\n', ' ').replaceAll(/\s+/g, ' ').slice(0, 43) +
|
|
`${content.sql.length > 43 ? '...' : ''}`}
|
|
</SimpleCodeBlock>
|
|
) : (
|
|
<p className="text-xs text-foreground-lighter">This query is empty</p>
|
|
)}
|
|
{content.sql.trim() && (
|
|
<CopyButton
|
|
iconOnly
|
|
type="default"
|
|
className="px-1 absolute top-1.5 right-1.5"
|
|
text={content.sql}
|
|
/>
|
|
)}
|
|
</>
|
|
</HoverCardContent_Shadcn_>
|
|
)}
|
|
</HoverCard_Shadcn_>
|
|
<RenameQueryModal
|
|
snippet={tabInfo}
|
|
visible={renameModalOpen}
|
|
onCancel={() => setRenameModalOpen(false)}
|
|
onComplete={() => setRenameModalOpen(false)}
|
|
/>
|
|
<ConfirmationModal
|
|
title="Confirm to delete query"
|
|
confirmLabel="Delete query"
|
|
confirmLabelLoading="Deleting query"
|
|
size="medium"
|
|
loading={isDeleting}
|
|
visible={deleteModalOpen}
|
|
variant={'destructive'}
|
|
onCancel={() => setDeleteModalOpen(false)}
|
|
onConfirm={onConfirmDelete}
|
|
alert={
|
|
visibility === 'project'
|
|
? {
|
|
title: 'This SQL snippet will be lost forever',
|
|
description:
|
|
'Deleting this query will remove it for all members of the project team.',
|
|
}
|
|
: undefined
|
|
}
|
|
>
|
|
<p className="text-sm">Are you sure you want to delete '{name}'?</p>
|
|
</ConfirmationModal>
|
|
<ConfirmationModal
|
|
title="Confirm sharing query"
|
|
size="medium"
|
|
confirmLabel="Share query"
|
|
confirmLabelLoading="Sharing query"
|
|
visible={shareModalOpen}
|
|
onCancel={() => setShareModalOpen(false)}
|
|
onConfirm={onConfirmShare}
|
|
alert={{
|
|
title: 'This SQL query will become public to all team members',
|
|
description: 'Anyone with access to the project can view it',
|
|
}}
|
|
>
|
|
<ul className="text-sm text-foreground-light space-y-5">
|
|
<li className="flex gap-3">
|
|
<Eye />
|
|
<span>Project members will have read-only access to this query.</span>
|
|
</li>
|
|
<li className="flex gap-3">
|
|
<Unlock />
|
|
<span>Anyone will be able to duplicate it to their personal snippets.</span>
|
|
</li>
|
|
</ul>
|
|
</ConfirmationModal>
|
|
<DownloadSnippetModal
|
|
id={id as string}
|
|
visible={isDownloadSnippetModalOpen}
|
|
onCancel={() => setIsDownloadSnippetModalOpen(false)}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default QueryItem
|