import { PermissionAction } from '@supabase/shared-types/out/constants' import { keepPreviousData } from '@tanstack/react-query' import { IS_PLATFORM } from 'common' import { useParams } from 'common/hooks/useParams' import { Copy, Download, Edit, ExternalLink, Heart, Lock, Move, Plus, Share, Trash, } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { ComponentProps, useEffect } from 'react' import { Button, cn, ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, TreeViewItem, } from 'ui' import { createSqlSnippetSkeletonV2 } from '@/components/interfaces/SQLEditor/SQLEditor.utils' import { getContentById } from '@/data/content/content-id-query' import { useSQLSnippetFolderContentsQuery } from '@/data/content/sql-folder-contents-query' import { Snippet } from '@/data/content/sql-folders-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import useLatest from '@/hooks/misc/useLatest' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useProfile } from '@/lib/profile' import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2' interface SQLEditorTreeViewItemProps extends Omit< ComponentProps, 'name' | 'xPadding' > { element: any isMultiSelected?: boolean status?: 'editing' | 'saving' | 'idle' getNodeProps: () => any onSelectCreate?: () => void onSelectDelete?: () => void onSelectRename?: () => void onSelectMove?: () => void onSelectShare?: () => void onSelectUnshare?: () => void onSelectDownload?: () => void onSelectDeleteFolder?: () => void onEditSave?: (name: string) => void onMultiSelect?: (id: string) => void // Pagination/filtering options isLastItem: boolean hasNextPage?: boolean fetchNextPage?: () => void isFetchingNextPage?: boolean sort?: 'inserted_at' | 'name' name?: string onFolderContentsChange?: (info: { isLoading: boolean; snippets?: Snippet[] }) => void } export const SQLEditorTreeViewItem = ({ element, isBranch, isExpanded, level, status, isSelected, isMultiSelected, getNodeProps, onSelectCreate, onSelectDelete, onSelectRename, onSelectMove, onSelectShare, onSelectUnshare, onSelectDownload, onEditSave, onMultiSelect, isLastItem, hasNextPage: _hasNextPage, fetchNextPage: _fetchNextPage, isFetchingNextPage: _isFetchingNextPage, sort, name, onFolderContentsChange, ...props }: SQLEditorTreeViewItemProps) => { const router = useRouter() const { id, ref: projectRef } = useParams() const { profile } = useProfile() const { data: project } = useSelectedProjectQuery() const { className, onClick } = getNodeProps() const snapV2 = useSqlEditorV2StateSnapshot() const isOwner = profile?.id === element?.metadata.owner_id const isSharedSnippet = element.metadata.visibility === 'project' const isFavorite = element.metadata.favorite const isEditing = status === 'editing' const isSaving = status === 'saving' const { can: canCreateSQLSnippet } = useAsyncCheckPermissions( PermissionAction.CREATE, 'user_content', { resource: { type: 'sql', owner_id: profile?.id }, subject: { id: profile?.id }, } ) const parentId = element.parent === 0 ? undefined : element.parent const isEnabled = isBranch && isExpanded const { data, isSuccess, isLoading, isFetchingNextPage: isFetchingNextPageInFolder, hasNextPage: hasNextPageInFolder, fetchNextPage: fetchNestPageInFolder, isPlaceholderData, isFetching, } = useSQLSnippetFolderContentsQuery( { projectRef, folderId: parentId ?? element.id, name, sort, }, { enabled: isEnabled, placeholderData: keepPreviousData, } ) useEffect(() => { if (projectRef && isSuccess) { data.pages.forEach((page) => { page.contents?.forEach((snippet) => { snapV2.addSnippet({ projectRef, snippet, }) }) }) } }, [projectRef, data?.pages]) const onFolderContentsChangeRef = useLatest(onFolderContentsChange) useEffect(() => { if (isEnabled) { onFolderContentsChangeRef.current?.({ isLoading: isLoading || (isPlaceholderData && isFetching), snippets: data?.pages.flatMap((page) => page.contents ?? []), }) } }, [data?.pages, isFetching, isLoading, isPlaceholderData, isEnabled]) const isInFolder = parentId !== undefined const hasNextPage = isInFolder ? hasNextPageInFolder : _hasNextPage function fetchNextPage() { if (isInFolder) { fetchNestPageInFolder() } else if (typeof _fetchNextPage === 'function') { _fetchNextPage() } } const onToggleFavorite = () => { const snippetId = element.metadata.id if (snippetId) { if (isFavorite) snapV2.removeFavorite(snippetId) else snapV2.addFavorite(snippetId) } } const onSelectDuplicate = async () => { if (!profile) return console.error('Profile is required') if (!project) return console.error('Project is required') if (!projectRef) return console.error('Project ref is required') if (!id) return console.error('Snippet ID is required') const snippet = element.metadata let sql: string = '' if (snippet.content && snippet.content.unchecked_sql) { sql = snippet.content.unchecked_sql } else { // Fetch the content first const { content } = await getContentById({ projectRef, id: snippet.id }) if ('unchecked_sql' in content) { sql = content.unchecked_sql } } const snippetCopy = createSqlSnippetSkeletonV2({ name: `${snippet.name} (Duplicate)`, sql, owner_id: profile?.id, project_id: project?.id, }) snapV2.addSnippet({ projectRef, snippet: snippetCopy }) snapV2.addNeedsSaving(snippetCopy.id!) router.push(`/project/${projectRef}/sql/${snippetCopy.id}`) } return ( <> { if (onEditSave !== undefined) onEditSave(value) }} onClick={(e) => { if (!isBranch) { if (!e.shiftKey) { router.push(`/project/${projectRef}/sql/${element.id}`) } else if (id !== 'new') { onMultiSelect?.(element.id) } else { router.push(`/project/${projectRef}/sql/${element.id}`) } } else { // Prevent expanding folder while editing text // as the user may double click to select etc if (isEditing) { return } // When the item is a folder, we want to expand/close it onClick(e) } }} {...props} name={element.name} nameForTitle={props.nameForTitle} description={element.metadata?.description || undefined} xPadding={16} /> e.stopPropagation()}> {isBranch ? ( <> {onSelectCreate !== undefined && ( onSelectCreate()} onFocusCapture={(e) => e.stopPropagation()} > Create new snippet )} {onSelectRename !== undefined && isOwner && ( onSelectRename()} onFocusCapture={(e) => e.stopPropagation()} > Rename folder )} {onSelectDelete !== undefined && isOwner && ( <> onSelectDelete()} onFocusCapture={(e) => e.stopPropagation()} > Delete folder )} ) : isMultiSelected ? ( <> {onSelectMove !== undefined && ( onSelectMove()} onFocusCapture={(e) => e.stopPropagation()} > Move selected queries )} {onSelectDelete !== undefined && ( onSelectDelete()} onFocusCapture={(e) => e.stopPropagation()} > Delete selected queries )} ) : ( <> e.stopPropagation()} > Open in new tab {onSelectRename !== undefined && isOwner && ( onSelectRename()} onFocusCapture={(e) => e.stopPropagation()} > Rename query )} {onSelectMove !== undefined && isOwner && ( onSelectMove()} onFocusCapture={(e) => e.stopPropagation()} > Move query )} {onSelectShare !== undefined && !isSharedSnippet && canCreateSQLSnippet && IS_PLATFORM && ( onSelectShare()} onFocusCapture={(e) => e.stopPropagation()} > Share query with team )} {onSelectUnshare !== undefined && isSharedSnippet && isOwner && ( onSelectUnshare()} onFocusCapture={(e) => e.stopPropagation()} > Unshare query with team )} {onSelectDuplicate !== undefined && canCreateSQLSnippet && ( onSelectDuplicate()} onFocusCapture={(e) => e.stopPropagation()} > Duplicate query )} {IS_PLATFORM && ( onToggleFavorite()} onFocusCapture={(e) => e.stopPropagation()} > {isFavorite ? 'Remove from' : 'Add to'} favorites )} {onSelectDownload !== undefined && IS_PLATFORM && ( onSelectDownload()} onFocusCapture={(e) => e.stopPropagation()} > Download as migration file )} {onSelectDelete !== undefined && isOwner && ( <> onSelectDelete()}> Delete query )} )} {hasNextPage && typeof element.id === 'string' && isLastItem && (
1 ? 48 * (element.level - 1) : undefined, }} >
)} ) }