import { zodResolver } from '@hookform/resolvers/zod' import { IS_PLATFORM, useParams } from 'common' import { Check, Code, Plus } from 'lucide-react' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { Button, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogSection, DialogSectionSeparator, DialogTitle, Form, FormControl, FormField, FormItem, FormLabel, FormMessage, Input, Label, Popover, PopoverContent, PopoverTrigger, ScrollArea, } from 'ui' import * as z from 'zod' import { getContentById } from '@/data/content/content-id-query' import { useContentUpsertMutation } from '@/data/content/content-upsert-mutation' import { useSQLSnippetFolderCreateMutation } from '@/data/content/sql-folder-create-mutation' import { Snippet } from '@/data/content/sql-folders-query' import { SnippetWithContent, useSnippetFolders, useSqlEditorV2StateSnapshot, } from '@/state/sql-editor-v2' import { createTabId, useTabsStateSnapshot } from '@/state/tabs' interface MoveQueryModalProps { visible: boolean snippets?: Snippet[] onClose: () => void } /** * [Joshen] Just FYI react-accessible-tree-view doesn't support drag and drop for moving * files out of the box and we'll need to figure out a way to support this ideal UX. Same * thing for the Storage Explorer actually. So this is just a temporary UX till we can figure * out drag and drop that works nicely with the tree view. React beautiful dnd unfortunately * doesn't support drag drop into a folder kind of UX. */ export const MoveQueryModal = ({ visible, snippets = [], onClose }: MoveQueryModalProps) => { const { ref } = useParams() const snapV2 = useSqlEditorV2StateSnapshot() const tabsSnap = useTabsStateSnapshot() const router = useRouter() const [open, setOpen] = useState(false) const [selectedId, setSelectedId] = useState() const { mutateAsync: createFolder, isPending: isCreatingFolder } = useSQLSnippetFolderCreateMutation({ onError: (error) => { toast.error(`Failed to create new folder: ${error.message}`) }, }) const { mutateAsync: moveSnippetAsync, isPending: isMovingSnippet } = useContentUpsertMutation({ onError: (error) => { toast.error(`Failed to move query: ${error.message}`) }, }) const getFormSchema = () => { if (selectedId === 'new-folder') { return z .object({ name: z.string().min(1, 'Please provide a name for the folder'), }) .refine((data) => !snapV2.allFolderNames.includes(data.name), { message: 'This folder name already exists', path: ['name'], }) } else { return z.object({}) } } const FormSchema = getFormSchema() const form = useForm>({ mode: 'onSubmit', reValidateMode: 'onSubmit', resolver: zodResolver(FormSchema as any), defaultValues: { name: '' }, }) const folders = useSnippetFolders(ref as string) const selectedFolder = selectedId === 'root' ? 'Root of the editor' : selectedId === 'new-folder' ? 'Create a new folder' : folders.find((f) => f.id === selectedId)?.name const isCurrentFolder = snippets.length === 1 && ((!snippets[0].folder_id && selectedId === 'root') || snippets[0].folder_id === selectedId) const isMovingToSameFolder = snippets.length === 1 && ((!snippets[0].folder_id && selectedId === 'root') || snippets[0].folder_id === selectedId) const onConfirmMove = async (values: z.infer) => { if (!ref) return console.error('Project ref is required') try { let folderId = selectedId if (selectedId === 'new-folder' && 'name' in values) { const { id } = await createFolder({ projectRef: ref, name: values.name, }) folderId = id } await Promise.all( snippets.map(async (snippet) => { let snippetContent = (snippet as SnippetWithContent)?.content if (snippetContent === undefined) { const { content } = await getContentById({ projectRef: ref, id: snippet.id }) if ('unchecked_sql' in content) { snippetContent = content } } if (snippetContent === undefined) { return toast.error('Failed to save snippet: Unable to retrieve snippet contents') } else { const movedSnippet = await moveSnippetAsync({ projectRef: ref, payload: { id: snippet.id, type: 'sql', name: snippet.name, description: snippet.description, visibility: snippet.visibility, project_id: snippet.project_id, owner_id: snippet.owner_id, folder_id: selectedId === 'root' ? null : folderId, content: snippetContent as any, }, }) if (IS_PLATFORM) { snapV2.updateSnippet({ id: snippet.id, snippet: { ...snippet, folder_id: selectedId === 'root' ? null : folderId }, skipSave: true, }) } else if (movedSnippet) { // On selfhosted, we need to update the state with the moved snippet because the snippet depends on the // folder_id the moved snippet has a different id than the original snippet. // remove the old snippet from the state without saving to API snapV2.removeSnippet(snippet.id, true) snapV2.addSnippet({ projectRef: ref, snippet: movedSnippet }) // remove the tab for the old snippet if the snippet was open. Moving can also happen when the tab is not open. const tabId = createTabId('sql', { id: snippet.id }) if (tabsSnap.hasTab(tabId)) { tabsSnap.removeTab(tabId) await router.push(`/project/${ref}/sql/${movedSnippet.id}`) } } } }) ) toast.success( `Successfully moved ${snippets.length === 1 ? `"${snippets[0].name}"` : `${snippets.length} snippets`} to ${selectedId === 'root' ? 'the root of the editor' : selectedFolder}` ) onClose() } catch (error: any) { // error will be handled by the mutation's onError callback console.error('Error moving snippets:', error) } } useEffect(() => { if (visible && snippets !== undefined) { if (snippets.length === 1) { setSelectedId(snippets[0].folder_id ?? 'root') } else { setSelectedId('root') } form.reset({ name: '' }) } }, [visible, snippets]) return ( onClose()}>
Move {snippets.length === 1 ? `"${snippets[0].name}"` : `${snippets.length}`}{' '} snippet{snippets.length > 1 ? 's' : ''} to a folder Select which folder to move your quer{snippets.length > 1 ? 'ies' : 'y'} to
No folders found 6 ? 'h-[210px]' : ''}> { setOpen(false) setSelectedId('root') }} onClick={() => { setOpen(false) setSelectedId('root') }} > Root of the editor {snippets.length === 1 && snippets[0].folder_id === null && ` (Current)`} {selectedId === 'root' && } {folders?.map((folder) => ( { setOpen(false) setSelectedId(folder.id) }} onClick={() => { setOpen(false) setSelectedId(folder.id) }} > {folder.name} {snippets.length === 1 && snippets[0].folder_id === folder.id && ` (Current)`} {folder.id === selectedId && } ))} { setOpen(false) setSelectedId('new-folder') }} onClick={() => { setOpen(false) setSelectedId('new-folder') }} >

New folder

{selectedId === 'new-folder' && (
( Provide a name for your new folder )} />
)}
) }