mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Revert "Feat/sql editor support bulk deletes" (#21003)
* Revert "Feat/sql editor support bulk deletes (#20927)"
This reverts commit 27c2cff395.
* ci: Autofix updates from GitHub workflow
* Lint
---------
Co-authored-by: github-tidy-bot <github-tidy-bot@supabase.com>
This commit is contained in:
1 parent
27c2cff395
commit
30245c9490
17 files changed
+581
-686
No files matched your search
@@ -15,15 +15,15 @@
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next",
|
||||
},
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
// handle ui package paths
|
||||
"@ui/*": ["./../../packages/ui/src/*"],
|
||||
},
|
||||
"@ui/*": ["./../../packages/ui/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -5,9 +5,9 @@
|
||||
"noImplicitAny": false,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./*"],
|
||||
},
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -7,9 +7,9 @@
|
||||
"resolveJsonModule": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./*"],
|
||||
},
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -7,9 +7,9 @@
|
||||
"resolveJsonModule": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./*"],
|
||||
},
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./*"],
|
||||
"@ui/*": ["./../../packages/ui/src/*"], // handle ui package paths
|
||||
"@ui/*": ["./../../packages/ui/src/*"] // handle ui package paths
|
||||
},
|
||||
"target": "es2015",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
@@ -19,14 +19,14 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"module": "esnext",
|
||||
"module": "esnext"
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"pages/guides/append-test.js",
|
||||
"jest.config.mjs",
|
||||
"jest.config.mjs"
|
||||
],
|
||||
"exclude": ["node_modules"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -44,7 +44,7 @@ const SavedQueriesItem = ({ item }: SavedQueriesItemProps) => {
|
||||
console.error('Invalid project reference')
|
||||
return
|
||||
}
|
||||
await deleteContent({ projectRef: ref, ids: [item.id] })
|
||||
await deleteContent({ projectRef: ref, id: item.id })
|
||||
setShowConfirmModal(false)
|
||||
toast.success('Query deleted')
|
||||
} catch (error) {
|
||||
|
||||
@@ -9,8 +9,8 @@ const ProductMenuBar = ({ title, children }: PropsWithChildren<ProductMenuBarPro
|
||||
<div
|
||||
className={[
|
||||
'hide-scrollbar flex w-64 flex-col border-r', // Layout
|
||||
'bg-background',
|
||||
'border-default ',
|
||||
'bg-background',
|
||||
].join(' ')}
|
||||
>
|
||||
<div
|
||||
@@ -19,7 +19,9 @@ const ProductMenuBar = ({ title, children }: PropsWithChildren<ProductMenuBarPro
|
||||
>
|
||||
<h4 className="text-lg">{title}</h4>
|
||||
</div>
|
||||
<div className="flex-grow overflow-y-auto">{children}</div>
|
||||
<div className="flex-grow overflow-y-auto" style={{ maxHeight: 'calc(100vh - 96px)' }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import clsx from 'clsx'
|
||||
import { useParams } from 'common'
|
||||
import { noop } from 'lodash'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import {
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
Alert_Shadcn_,
|
||||
Checkbox,
|
||||
Checkbox_Shadcn_,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
IconAlertCircle,
|
||||
IconAlertTriangle,
|
||||
IconChevronDown,
|
||||
IconCopy,
|
||||
IconDownload,
|
||||
IconEdit2,
|
||||
IconEye,
|
||||
IconShare,
|
||||
IconTrash,
|
||||
IconUnlock,
|
||||
Modal,
|
||||
TooltipContent_Shadcn_,
|
||||
@@ -23,66 +33,28 @@ import {
|
||||
|
||||
import DownloadSnippetModal from 'components/interfaces/SQLEditor/DownloadSnippetModal'
|
||||
import RenameQueryModal from 'components/interfaces/SQLEditor/RenameQueryModal'
|
||||
import { createSqlSnippetSkeleton } from 'components/interfaces/SQLEditor/SQLEditor.utils'
|
||||
import SimpleCodeBlock from 'components/to-be-cleaned/SimpleCodeBlock'
|
||||
import ConfirmationModal from 'components/ui/ConfirmationModal'
|
||||
import CopyButton from 'components/ui/CopyButton'
|
||||
import { useContentDeleteMutation } from 'data/content/content-delete-mutation'
|
||||
import { SqlSnippet } from 'data/content/sql-snippets-query'
|
||||
import { useCheckPermissions, useSelectedProject, useStore } from 'hooks'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { uuidv4 } from 'lib/helpers'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import { useSqlEditorStateSnapshot } from 'state/sql-editor'
|
||||
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 QueryItem = ({ tabInfo }: QueryItemProps) => {
|
||||
const { ref, id: activeId } = useParams()
|
||||
const snap = useSqlEditorStateSnapshot()
|
||||
const { id, name, description, content, visibility } = tabInfo || {}
|
||||
const { id, name, description, content } = tabInfo || {}
|
||||
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.code === 404 && error.message.includes('Content 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) {
|
||||
@@ -93,113 +65,262 @@ const QueryItem = ({
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Tooltip_Shadcn_ delayDuration={100}>
|
||||
<TooltipTrigger_Shadcn_ asChild>
|
||||
<div
|
||||
key={id}
|
||||
className={clsx(
|
||||
'flex items-center justify-between rounded-md group',
|
||||
isActive ? 'bg-surface-300' : 'hover:bg-surface-200'
|
||||
)}
|
||||
ref={isActive ? (activeItemRef as React.RefObject<HTMLDivElement>) : null}
|
||||
>
|
||||
<Link href={`/project/${ref}/sql/${id}`} className="py-1 px-3 w-full overflow-hidden">
|
||||
<p
|
||||
title={description || name}
|
||||
className={clsx(
|
||||
isActive
|
||||
? 'text-foreground'
|
||||
: 'text-foreground-light group-hover:text-foreground/80',
|
||||
'text-sm transition overflow-hidden text-ellipsis'
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</p>
|
||||
</Link>
|
||||
<div className="pr-1">{<QueryItemActions tabInfo={tabInfo} activeId={activeId} />}</div>
|
||||
</div>
|
||||
</TooltipTrigger_Shadcn_>
|
||||
{!isActive && (
|
||||
<TooltipContent_Shadcn_
|
||||
side="right"
|
||||
align="start"
|
||||
className="w-96 flex flex-col gap-y-2 py-3 -translate-y-[4px]"
|
||||
>
|
||||
<p className="text-xs">Query preview:</p>
|
||||
<div className="bg-surface-300 py-2 px-3 rounded relative">
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TooltipContent_Shadcn_>
|
||||
)}
|
||||
</Tooltip_Shadcn_>
|
||||
)
|
||||
}
|
||||
|
||||
export default QueryItem
|
||||
|
||||
interface QueryItemActionsProps {
|
||||
tabInfo: SqlSnippet
|
||||
activeId: string | undefined
|
||||
}
|
||||
|
||||
const QueryItemActions = observer(({ tabInfo, activeId }: QueryItemActionsProps) => {
|
||||
const { ui } = useStore()
|
||||
const { ref } = useParams()
|
||||
const router = useRouter()
|
||||
const { profile } = useProfile()
|
||||
|
||||
const snap = useSqlEditorStateSnapshot()
|
||||
const project = useSelectedProject()
|
||||
|
||||
const { id: snippetID } = tabInfo || {}
|
||||
const snippet =
|
||||
snippetID !== undefined && snap.snippets && snap.snippets[snippetID] !== undefined
|
||||
? snap.snippets[snippetID]
|
||||
: null
|
||||
|
||||
const isSnippetOwner = profile?.id === snippet?.snippet.owner_id
|
||||
|
||||
const { mutate: deleteContent, isLoading: isDeleting } = useContentDeleteMutation({
|
||||
onSuccess(data) {
|
||||
if (data.id) snap.removeSnippet(data.id)
|
||||
|
||||
const existingSnippetIds = (snap.orders[ref!] ?? []).filter((x) => x !== id)
|
||||
if (existingSnippetIds.length === 0) {
|
||||
router.push(`/project/${ref}/sql/new`)
|
||||
} else {
|
||||
router.push(`/project/${ref}/sql/${existingSnippetIds[0]}`)
|
||||
}
|
||||
},
|
||||
onError(error, data) {
|
||||
if (error.code === 404 && error.message.includes('Content not found')) {
|
||||
if (data.id) snap.removeSnippet(data.id)
|
||||
const existingSnippetIds = (snap.orders[ref!] ?? []).filter((x) => x !== id)
|
||||
if (existingSnippetIds.length === 0) {
|
||||
router.push(`/project/${ref}/sql/new`)
|
||||
} else {
|
||||
router.push(`/project/${ref}/sql/${existingSnippetIds[0]}`)
|
||||
}
|
||||
} else {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to delete query: ${error.message}`,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const { id, name, visibility, content } = tabInfo || {}
|
||||
const [renameModalOpen, setRenameModalOpen] = useState(false)
|
||||
const [shareModalOpen, setShareModalOpen] = useState(false)
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
|
||||
const [isDownloadSnippetModalOpen, setIsDownloadSnippetModalOpen] = useState(false)
|
||||
const isActive = id === activeId
|
||||
|
||||
const canCreateSQLSnippet = useCheckPermissions(PermissionAction.CREATE, 'user_content', {
|
||||
resource: { type: 'sql', owner_id: profile?.id },
|
||||
subject: { id: profile?.id },
|
||||
})
|
||||
|
||||
const onCloseRenameModal = () => {
|
||||
setRenameModalOpen(false)
|
||||
}
|
||||
|
||||
const onClickRename = (e: any) => {
|
||||
e.stopPropagation()
|
||||
setRenameModalOpen(true)
|
||||
}
|
||||
|
||||
const onClickShare = (e: any) => {
|
||||
e.stopPropagation()
|
||||
setShareModalOpen(true)
|
||||
}
|
||||
|
||||
const onClickDelete = (e: any) => {
|
||||
e.stopPropagation()
|
||||
setDeleteModalOpen(true)
|
||||
}
|
||||
|
||||
const onConfirmShare = async () => {
|
||||
if (id) {
|
||||
try {
|
||||
snap.shareSnippet(id, 'project')
|
||||
return Promise.resolve()
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to share query: ${error.message}`)
|
||||
ui.setNotification({
|
||||
error,
|
||||
category: 'error',
|
||||
message: `Failed to share 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, id })
|
||||
}
|
||||
|
||||
const createPersonalCopy = async () => {
|
||||
if (!ref) return console.error('Project ref is required')
|
||||
if (!id) return console.error('Snippet ID is required')
|
||||
try {
|
||||
const snippet = createSqlSnippetSkeleton({
|
||||
id: uuidv4(),
|
||||
name,
|
||||
sql: content.sql,
|
||||
owner_id: profile?.id,
|
||||
project_id: project?.id,
|
||||
})
|
||||
snap.addSnippet(snippet as SqlSnippet, ref)
|
||||
snap.addNeedsSaving(snippet.id!)
|
||||
router.push(`/project/${ref}/sql/${snippet.id}`)
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to create a personal copy of this query: ${error.message}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip_Shadcn_ delayDuration={100}>
|
||||
<TooltipTrigger_Shadcn_ asChild>
|
||||
<div
|
||||
key={id}
|
||||
className={clsx(
|
||||
'h-7 pl-3 pr-2',
|
||||
'flex items-center justify-between rounded-md group relative',
|
||||
isActive ? 'bg-surface-300' : 'hover:bg-surface-200'
|
||||
<div className="group [div&>button[data-state='open']>span]:text-foreground-lighter flex items-center">
|
||||
{IS_PLATFORM ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<span
|
||||
className={clsx(
|
||||
'rounded p-1',
|
||||
isActive
|
||||
? 'text-foreground-light hover:bg-border-stronger'
|
||||
: 'text-background hover:bg-overlay-hover group-hover:text-foreground-light'
|
||||
)}
|
||||
>
|
||||
<IconChevronDown size="tiny" strokeWidth={2} />
|
||||
</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="end" className=" w-52 translate-x-[4px]">
|
||||
{isSnippetOwner && (
|
||||
<DropdownMenuItem onClick={onClickRename} className="space-x-2">
|
||||
<IconEdit2 size="tiny" />
|
||||
<p>Rename query</p>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
ref={isActive ? (activeItemRef as React.RefObject<HTMLDivElement>) : null}
|
||||
>
|
||||
{visibility === 'user' && (
|
||||
<Checkbox
|
||||
className={clsx(
|
||||
'transition absolute left-2.5 [&>input]:border-foreground-lighter',
|
||||
hasQueriesSelected ? '' : 'opacity-0 group-hover:opacity-100'
|
||||
)}
|
||||
checked={isSelected}
|
||||
onChange={(event) => {
|
||||
onSelectQuery((event.nativeEvent as KeyboardEvent).shiftKey)
|
||||
}}
|
||||
/>
|
||||
|
||||
{visibility === 'user' && canCreateSQLSnippet && (
|
||||
<DropdownMenuItem onClick={onClickShare} className="space-x-2">
|
||||
<IconShare size="tiny" />
|
||||
<p>Share query</p>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<div className="flex items-center justify-between w-full gap-x-2">
|
||||
<Link
|
||||
title={description || name}
|
||||
href={`/project/${ref}/sql/${id}`}
|
||||
className={clsx(
|
||||
'w-full overflow-hidden truncate',
|
||||
isActive
|
||||
? 'text-foreground'
|
||||
: 'text-foreground-light group-hover:text-foreground/80',
|
||||
'text-sm transition-all overflow-hidden text-ellipsis',
|
||||
hasQueriesSelected && visibility === 'user'
|
||||
? 'ml-5'
|
||||
: visibility === 'user'
|
||||
? 'group-hover:ml-5'
|
||||
: ''
|
||||
)}
|
||||
{visibility === 'project' && canCreateSQLSnippet && (
|
||||
<DropdownMenuItem onClick={createPersonalCopy} className="space-x-2">
|
||||
<IconCopy size="tiny" />
|
||||
<p>Duplicate personal copy</p>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{IS_PLATFORM && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => setIsDownloadSnippetModalOpen(!isDownloadSnippetModalOpen)}
|
||||
className="space-x-2"
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
{!hasQueriesSelected && (
|
||||
<QueryItemActions
|
||||
tabInfo={tabInfo}
|
||||
activeId={activeId}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
onSelectDeleteQuery={() => setDeleteModalOpen(true)}
|
||||
onSelectRenameQuery={() => setRenameModalOpen(true)}
|
||||
onSelectShareQuery={() => setShareModalOpen(true)}
|
||||
onSelectDownloadQuery={() => setIsDownloadSnippetModalOpen(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TooltipTrigger_Shadcn_>
|
||||
{!hideTooltip && (
|
||||
<TooltipContent_Shadcn_
|
||||
side="right"
|
||||
align="start"
|
||||
className="w-96 flex flex-col gap-y-2 py-3 -translate-y-[4px]"
|
||||
>
|
||||
<p className="text-xs">Query preview:</p>
|
||||
<div className="bg-surface-300 py-2 px-3 rounded relative">
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TooltipContent_Shadcn_>
|
||||
)}
|
||||
</Tooltip_Shadcn_>
|
||||
<IconDownload size="tiny" />
|
||||
<p>Download as migration file</p>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isSnippetOwner && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={onClickDelete} className="space-x-2">
|
||||
<IconTrash size="tiny" />
|
||||
<p>Delete query</p>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<Button asChild disabled type="text" style={{ padding: '3px' }}>
|
||||
<span></span>
|
||||
</Button>
|
||||
)}
|
||||
<RenameQueryModal
|
||||
snippet={tabInfo}
|
||||
visible={renameModalOpen}
|
||||
onCancel={() => setRenameModalOpen(false)}
|
||||
onComplete={() => setRenameModalOpen(false)}
|
||||
onCancel={onCloseRenameModal}
|
||||
onComplete={onCloseRenameModal}
|
||||
/>
|
||||
<ConfirmationModal
|
||||
header="Confirm to delete query"
|
||||
@@ -272,8 +393,6 @@ const QueryItem = ({
|
||||
visible={isDownloadSnippetModalOpen}
|
||||
onCancel={() => setIsDownloadSnippetModalOpen(false)}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default QueryItem
|
||||
})
|
||||
@@ -1,164 +0,0 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import clsx from 'clsx'
|
||||
import { IS_PLATFORM, useParams } from 'common'
|
||||
import { useRouter } from 'next/router'
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
IconChevronDown,
|
||||
IconCopy,
|
||||
IconDownload,
|
||||
IconEdit2,
|
||||
IconShare,
|
||||
IconTrash,
|
||||
} from 'ui'
|
||||
|
||||
import { createSqlSnippetSkeleton } from 'components/interfaces/SQLEditor/SQLEditor.utils'
|
||||
import { SqlSnippet } from 'data/content/sql-snippets-query'
|
||||
import { useCheckPermissions, useSelectedProject, useStore } from 'hooks'
|
||||
import { uuidv4 } from 'lib/helpers'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import { useSqlEditorStateSnapshot } from 'state/sql-editor'
|
||||
|
||||
interface QueryItemActionsProps {
|
||||
tabInfo: SqlSnippet
|
||||
activeId: string | undefined
|
||||
open: boolean
|
||||
setOpen: (value: boolean) => void
|
||||
onSelectRenameQuery: () => void
|
||||
onSelectDeleteQuery: () => void
|
||||
onSelectShareQuery: () => void
|
||||
onSelectDownloadQuery: () => void
|
||||
}
|
||||
|
||||
export const QueryItemActions = ({
|
||||
tabInfo,
|
||||
activeId,
|
||||
open,
|
||||
setOpen,
|
||||
onSelectRenameQuery,
|
||||
onSelectDeleteQuery,
|
||||
onSelectShareQuery,
|
||||
onSelectDownloadQuery,
|
||||
}: QueryItemActionsProps) => {
|
||||
const { ui } = useStore()
|
||||
const { ref } = useParams()
|
||||
const router = useRouter()
|
||||
const { profile } = useProfile()
|
||||
|
||||
const snap = useSqlEditorStateSnapshot()
|
||||
const project = useSelectedProject()
|
||||
|
||||
const { id, name, visibility, content } = tabInfo || {}
|
||||
|
||||
const canCreateSQLSnippet = useCheckPermissions(PermissionAction.CREATE, 'user_content', {
|
||||
resource: { type: 'sql', owner_id: profile?.id },
|
||||
subject: { id: profile?.id },
|
||||
})
|
||||
|
||||
const { id: snippetID } = tabInfo || {}
|
||||
const snippet =
|
||||
snippetID !== undefined && snap.snippets && snap.snippets[snippetID] !== undefined
|
||||
? snap.snippets[snippetID]
|
||||
: null
|
||||
|
||||
const isSnippetOwner = profile?.id === snippet?.snippet.owner_id
|
||||
|
||||
const onClickRename = (e: any) => {
|
||||
e.stopPropagation()
|
||||
onSelectRenameQuery()
|
||||
}
|
||||
|
||||
const onClickShare = (e: any) => {
|
||||
e.stopPropagation()
|
||||
onSelectShareQuery()
|
||||
}
|
||||
|
||||
const onClickDelete = (e: any) => {
|
||||
e.stopPropagation()
|
||||
onSelectDeleteQuery()
|
||||
}
|
||||
|
||||
const createPersonalCopy = async () => {
|
||||
if (!ref) return console.error('Project ref is required')
|
||||
if (!id) return console.error('Snippet ID is required')
|
||||
try {
|
||||
const snippet = createSqlSnippetSkeleton({
|
||||
id: uuidv4(),
|
||||
name,
|
||||
sql: content.sql,
|
||||
owner_id: profile?.id,
|
||||
project_id: project?.id,
|
||||
})
|
||||
snap.addSnippet(snippet as SqlSnippet, ref)
|
||||
snap.addNeedsSaving(snippet.id!)
|
||||
router.push(`/project/${ref}/sql/${snippet.id}`)
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to create a personal copy of this query: ${error.message}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group [div&>button[data-state='open']>span]:text-foreground-lighter flex items-center">
|
||||
{IS_PLATFORM ? (
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="text"
|
||||
className="px-1 opacity-50 hover:opacity-100"
|
||||
icon={<IconChevronDown size="tiny" strokeWidth={2} />}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="end" className=" w-52 translate-x-2">
|
||||
{isSnippetOwner && (
|
||||
<DropdownMenuItem onClick={onClickRename} className="space-x-2">
|
||||
<IconEdit2 size="tiny" />
|
||||
<p>Rename query</p>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{visibility === 'user' && canCreateSQLSnippet && (
|
||||
<DropdownMenuItem onClick={onClickShare} className="space-x-2">
|
||||
<IconShare size="tiny" />
|
||||
<p>Share query</p>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{visibility === 'project' && canCreateSQLSnippet && (
|
||||
<DropdownMenuItem onClick={createPersonalCopy} className="space-x-2">
|
||||
<IconCopy size="tiny" />
|
||||
<p>Duplicate personal copy</p>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{IS_PLATFORM && (
|
||||
<DropdownMenuItem onClick={() => onSelectDownloadQuery()} className="space-x-2">
|
||||
<IconDownload size="tiny" />
|
||||
<p>Download as migration file</p>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isSnippetOwner && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={onClickDelete} className="space-x-2">
|
||||
<IconTrash size="tiny" />
|
||||
<p>Delete query</p>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<Button asChild disabled type="text" style={{ padding: '3px' }}>
|
||||
<span />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { uniq } from 'lodash'
|
||||
|
||||
export const selectItemsInRange = (id: string, snippets: any[], selectedSnippets: string[]) => {
|
||||
const lastSelectedItemIndex = snippets.findIndex(
|
||||
(x) => x.id === selectedSnippets[selectedSnippets.length - 1]
|
||||
)
|
||||
const selectedItemIndex = snippets.findIndex((x) => x.id === id)
|
||||
|
||||
// Get the start and end index of the range to select
|
||||
const start = Math.min(selectedItemIndex, lastSelectedItemIndex)
|
||||
const end = Math.max(selectedItemIndex, lastSelectedItemIndex)
|
||||
|
||||
// Get the range to select and reverse the order if necessary
|
||||
const rangeToSelect = snippets.slice(start, end + 1)
|
||||
if (selectedItemIndex < lastSelectedItemIndex) rangeToSelect.reverse()
|
||||
|
||||
if (selectedSnippets.includes(id)) {
|
||||
const rangeToDeselectIds = rangeToSelect.map((item) => item.id)
|
||||
return selectedSnippets.filter((x) => x === id || !rangeToDeselectIds.includes(x))
|
||||
} else {
|
||||
return uniq(selectedSnippets.concat(rangeToSelect.map((x) => x.id) as string[]))
|
||||
}
|
||||
}
|
||||
@@ -3,50 +3,32 @@ import { useParams } from 'common'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useMemo, useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import {
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
Alert_Shadcn_,
|
||||
Button,
|
||||
IconPlus,
|
||||
IconSearch,
|
||||
IconX,
|
||||
Input,
|
||||
Menu,
|
||||
Modal,
|
||||
Separator,
|
||||
TooltipContent_Shadcn_,
|
||||
TooltipTrigger_Shadcn_,
|
||||
Tooltip_Shadcn_,
|
||||
} from 'ui'
|
||||
import { Button, IconPlus, IconSearch, IconX, Input, Menu, cn } from 'ui'
|
||||
|
||||
import { untitledSnippetTitle } from 'components/interfaces/SQLEditor/SQLEditor.constants'
|
||||
import { createSqlSnippetSkeleton } from 'components/interfaces/SQLEditor/SQLEditor.utils'
|
||||
import ConfirmationModal from 'components/ui/ConfirmationModal'
|
||||
import { WarningIcon } from 'components/ui/Icons'
|
||||
import ProductMenuItem from 'components/ui/ProductMenu/ProductMenuItem'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { useContentDeleteMutation } from 'data/content/content-delete-mutation'
|
||||
import { SqlSnippet, useSqlSnippetsQuery } from 'data/content/sql-snippets-query'
|
||||
import { useCheckPermissions, useSelectedProject, useStore } from 'hooks'
|
||||
import { uuidv4 } from 'lib/helpers'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import { useSnippets, useSqlEditorStateSnapshot } from 'state/sql-editor'
|
||||
import QueryItem from './QueryItem'
|
||||
import { partition, uniq, uniqBy } from 'lodash'
|
||||
import { selectItemsInRange } from './SQLEditorLayout.utils'
|
||||
|
||||
const SideBarContent = observer(() => {
|
||||
const { ui } = useStore()
|
||||
const { ref, id: activeId } = useParams()
|
||||
const { ref } = useParams()
|
||||
const router = useRouter()
|
||||
const { profile } = useProfile()
|
||||
const project = useSelectedProject()
|
||||
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||
const [selectedQueries, setSelectedQueries] = useState<string[]>([])
|
||||
const [personalSnippetsFilterString, setPersonalSnippetsFilterString] = useState('')
|
||||
const [projectSnippetsFilterString, setProjectSnippetsFilterString] = useState('')
|
||||
const [favoritesFilterString, setFavoritesFilterString] = useState('')
|
||||
const [isPersonalSnippetsFilterOpen, setIsPersonalSnippetsFilterOpen] = useState(false)
|
||||
const [isProjectSnippetsFilterOpen, setIsProjectSnippetsFilterOpen] = useState(false)
|
||||
const [isFavoritesFilterOpen, setIsFavoritesFilterOpen] = useState(false)
|
||||
|
||||
const snap = useSqlEditorStateSnapshot()
|
||||
const { isLoading, isSuccess } = useSqlSnippetsQuery(ref, {
|
||||
@@ -57,29 +39,6 @@ const SideBarContent = observer(() => {
|
||||
},
|
||||
})
|
||||
|
||||
const { mutate: deleteContent, isLoading: isDeleting } = useContentDeleteMutation({
|
||||
onSuccess: (data) => postDeleteCleanup(data),
|
||||
onError: (error, data) => {
|
||||
if (error.code === 404 && error.message.includes('Content not found')) {
|
||||
postDeleteCleanup(data.ids)
|
||||
} else {
|
||||
toast.error(`Failed to delete queries: ${error.message}`)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const postDeleteCleanup = (ids: string[]) => {
|
||||
setShowDeleteModal(false)
|
||||
setSelectedQueries([])
|
||||
if (ids.length > 0) ids.forEach((id) => snap.removeSnippet(id))
|
||||
const existingSnippetIds = (snap.orders[ref!] ?? []).filter((x) => !ids.includes(x))
|
||||
if (existingSnippetIds.length === 0) {
|
||||
router.push(`/project/${ref}/sql/new`)
|
||||
} else if (ids.includes(activeId as string)) {
|
||||
router.push(`/project/${ref}/sql/${existingSnippetIds[0]}`)
|
||||
}
|
||||
}
|
||||
|
||||
const snippets = useSnippets(ref)
|
||||
|
||||
const projectSnippets = useMemo(() => {
|
||||
@@ -87,90 +46,45 @@ const SideBarContent = observer(() => {
|
||||
}, [snippets])
|
||||
|
||||
const filteredProjectSnippets = useMemo(() => {
|
||||
if (searchText.length > 0) {
|
||||
if (projectSnippetsFilterString.length > 0) {
|
||||
return projectSnippets.filter((tab) =>
|
||||
tab.name.toLowerCase().includes(searchText.toLowerCase())
|
||||
tab.name.toLowerCase().includes(projectSnippetsFilterString.toLowerCase())
|
||||
)
|
||||
}
|
||||
return projectSnippets
|
||||
}, [projectSnippets, searchText])
|
||||
}, [projectSnippets, projectSnippetsFilterString])
|
||||
|
||||
const personalSnippets = useMemo(() => {
|
||||
const ss = snippets.filter(
|
||||
(snippet) => snippet.visibility === 'user' && !snippet.content.favorite
|
||||
)
|
||||
|
||||
if (searchText.length > 0) {
|
||||
return ss.filter((tab) => tab.name.toLowerCase().includes(searchText.toLowerCase()))
|
||||
if (personalSnippetsFilterString.length > 0) {
|
||||
return ss.filter((tab) =>
|
||||
tab.name.toLowerCase().includes(personalSnippetsFilterString.toLowerCase())
|
||||
)
|
||||
}
|
||||
return ss
|
||||
}, [searchText, snippets])
|
||||
}, [personalSnippetsFilterString, snippets])
|
||||
|
||||
const favoriteSnippets = useMemo(() => {
|
||||
return snippets.filter((snippet) => snippet.content.favorite)
|
||||
}, [snippets])
|
||||
|
||||
const filteredFavoriteSnippets = useMemo(() => {
|
||||
if (searchText.length > 0) {
|
||||
if (favoritesFilterString.length > 0) {
|
||||
return favoriteSnippets.filter((tab) =>
|
||||
tab.name.toLowerCase().includes(searchText.toLowerCase())
|
||||
tab.name.toLowerCase().includes(favoritesFilterString.toLowerCase())
|
||||
)
|
||||
}
|
||||
return favoriteSnippets
|
||||
}, [favoriteSnippets, searchText])
|
||||
}, [favoriteSnippets, favoritesFilterString])
|
||||
|
||||
const canCreateSQLSnippet = useCheckPermissions(PermissionAction.CREATE, 'user_content', {
|
||||
resource: { type: 'sql', owner_id: profile?.id },
|
||||
subject: { id: profile?.id },
|
||||
})
|
||||
|
||||
const onSelectQuery = (id?: string, isShiftHeld: boolean = false) => {
|
||||
if (id === undefined) return
|
||||
|
||||
if (isShiftHeld) {
|
||||
const [selectedFavSnippets, selectedOtherSnippets] = partition(
|
||||
selectedQueries,
|
||||
(id) => snippets.find((x) => x.id === id)?.content.favorite
|
||||
)
|
||||
const selectedSnippet = snippets.find((x) => x.id === id)
|
||||
const isFavourite = selectedSnippet?.content.favorite
|
||||
|
||||
if (isFavourite) {
|
||||
if (selectedFavSnippets.length > 0) {
|
||||
const updatedSelectedFavSnippets = selectItemsInRange(
|
||||
id,
|
||||
filteredFavoriteSnippets,
|
||||
selectedFavSnippets
|
||||
)
|
||||
setSelectedQueries(updatedSelectedFavSnippets.concat(selectedOtherSnippets))
|
||||
} else {
|
||||
selectQuery(id)
|
||||
}
|
||||
} else {
|
||||
if (selectedOtherSnippets.length > 0) {
|
||||
const updatedSelectedOtherSnippets = selectItemsInRange(
|
||||
id,
|
||||
personalSnippets,
|
||||
selectedOtherSnippets
|
||||
)
|
||||
setSelectedQueries(updatedSelectedOtherSnippets.concat(selectedFavSnippets))
|
||||
} else {
|
||||
selectQuery(id)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selectQuery(id)
|
||||
}
|
||||
}
|
||||
|
||||
const selectQuery = (id: string) => {
|
||||
if (selectedQueries.includes(id)) {
|
||||
setSelectedQueries(selectedQueries.filter((x) => x !== id))
|
||||
} else {
|
||||
setSelectedQueries(selectedQueries.concat([id]))
|
||||
}
|
||||
}
|
||||
|
||||
const handleNewQuery = async () => {
|
||||
if (!ref) return console.error('Project is required')
|
||||
if (!profile) return console.error('Profile is required')
|
||||
@@ -192,7 +106,13 @@ const SideBarContent = observer(() => {
|
||||
snap.addSnippet(snippet as SqlSnippet, ref)
|
||||
|
||||
router.push(`/project/${ref}/sql/${snippet.id}`)
|
||||
setSearchText('')
|
||||
// reset all search inputs when a new query is added
|
||||
setPersonalSnippetsFilterString('')
|
||||
setProjectSnippetsFilterString('')
|
||||
setFavoritesFilterString('')
|
||||
setIsPersonalSnippetsFilterOpen(false)
|
||||
setIsProjectSnippetsFilterOpen(false)
|
||||
setIsFavoritesFilterOpen(false)
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
@@ -201,206 +121,254 @@ const SideBarContent = observer(() => {
|
||||
}
|
||||
}
|
||||
|
||||
const onConfirmDelete = async () => {
|
||||
if (!ref) return console.error('Project ref is required')
|
||||
deleteContent({ projectRef: ref, ids: selectedQueries })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-6">
|
||||
<Menu type="pills">
|
||||
{isLoading ? (
|
||||
<div className="px-5 my-4 space-y-2">
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
<div className="mt-6">
|
||||
<Menu type="pills">
|
||||
{isLoading ? (
|
||||
<div className="px-5 my-4 space-y-2">
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
</div>
|
||||
) : isSuccess ? (
|
||||
<div className="space-y-6">
|
||||
<div className="px-3 flex flex-col gap-2">
|
||||
<Button
|
||||
type="default"
|
||||
className="mx-3 justify-start"
|
||||
onClick={() => {
|
||||
handleNewQuery()
|
||||
}}
|
||||
icon={<IconPlus size="tiny" />}
|
||||
>
|
||||
New query
|
||||
</Button>
|
||||
</div>
|
||||
) : isSuccess ? (
|
||||
<div className="space-y-6">
|
||||
<div className="px-4 flex flex-col gap-2">
|
||||
<Button
|
||||
type="default"
|
||||
className="justify-start"
|
||||
onClick={() => handleNewQuery()}
|
||||
icon={<IconPlus size="tiny" />}
|
||||
>
|
||||
New query
|
||||
</Button>
|
||||
<Input
|
||||
className="table-editor-search border-none"
|
||||
icon={
|
||||
<IconSearch className="text-foreground-lighter" size={12} strokeWidth={1.5} />
|
||||
}
|
||||
placeholder="Search queries"
|
||||
onChange={(e) => setSearchText(e.target.value.trim())}
|
||||
value={searchText}
|
||||
size="tiny"
|
||||
actions={
|
||||
searchText && (
|
||||
<Button type="text" className="px-1" onClick={() => setSearchText('')}>
|
||||
<IconX size={12} strokeWidth={2} />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<div className="space-y-6 px-3">
|
||||
<div>
|
||||
<ProductMenuItem
|
||||
name="Templates"
|
||||
isActive={router.asPath === `/project/${ref}/sql/templates`}
|
||||
url={`/project/${ref}/sql/templates`}
|
||||
/>
|
||||
<ProductMenuItem
|
||||
name="Quickstarts"
|
||||
isActive={router.asPath === `/project/${ref}/sql/quickstarts`}
|
||||
url={`/project/${ref}/sql/quickstarts`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-6 px-2">
|
||||
{searchText.length === 0 && (
|
||||
<div>
|
||||
<ProductMenuItem
|
||||
name="Templates"
|
||||
isActive={router.asPath === `/project/${ref}/sql/templates`}
|
||||
url={`/project/${ref}/sql/templates`}
|
||||
/>
|
||||
<ProductMenuItem
|
||||
name="Quickstarts"
|
||||
isActive={router.asPath === `/project/${ref}/sql/quickstarts`}
|
||||
url={`/project/${ref}/sql/quickstarts`}
|
||||
/>
|
||||
{favoriteSnippets.length >= 1 && (
|
||||
<div className="editor-product-menu">
|
||||
<div className="flex flex-row justify-between">
|
||||
<Menu.Group title="Favorites" />
|
||||
<button
|
||||
className="flex items-center w-4 h-4 cursor-pointer mr-3"
|
||||
onClick={() => {
|
||||
setIsFavoritesFilterOpen(!isFavoritesFilterOpen)
|
||||
}}
|
||||
>
|
||||
<IconSearch
|
||||
className={cn(
|
||||
'w-4',
|
||||
'h-4',
|
||||
'cursor-pointer',
|
||||
isFavoritesFilterOpen ? 'text-foreground' : 'text-foreground-lighter'
|
||||
)}
|
||||
onClick={() => {
|
||||
setFavoritesFilterString('')
|
||||
setIsFavoritesFilterOpen((state) => !state)
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{searchText.length > 0 &&
|
||||
filteredFavoriteSnippets.length === 0 &&
|
||||
filteredProjectSnippets.length === 0 &&
|
||||
personalSnippets.length === 0 && (
|
||||
<div className="h-32 border border-dashed flex flex-col gap-y-3 items-center justify-center rounded px-3">
|
||||
<div className="flex flex-col gap-y-1 items-center justify-center">
|
||||
<p className="text-xs text-foreground">No queries found</p>
|
||||
<p className="text-xs text-foreground-light text-center">
|
||||
Your search for "{searchText}" did not return any results
|
||||
</p>
|
||||
</div>
|
||||
<Button type="default" onClick={() => handleNewQuery()}>
|
||||
New query
|
||||
</Button>
|
||||
{isFavoritesFilterOpen && (
|
||||
<div className="pl-3 mb-2 mr-3">
|
||||
<Input
|
||||
autoFocus
|
||||
size="tiny"
|
||||
icon={<IconSearch size="tiny" />}
|
||||
placeholder="Filter"
|
||||
disabled={isLoading}
|
||||
onChange={(e) => setFavoritesFilterString(e.target.value)}
|
||||
value={favoritesFilterString}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
setIsFavoritesFilterOpen(false)
|
||||
setFavoritesFilterString('')
|
||||
}
|
||||
}}
|
||||
actions={
|
||||
favoritesFilterString && (
|
||||
<IconX
|
||||
size={'tiny'}
|
||||
className="mr-2 cursor-pointer"
|
||||
onClick={() => setFavoritesFilterString('')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredProjectSnippets.length > 0 ? (
|
||||
<div className="editor-product-menu">
|
||||
<Menu.Group title="Project queries" />
|
||||
|
||||
<div className="space-y-1">
|
||||
{filteredProjectSnippets.map((tabInfo) => {
|
||||
const { id } = tabInfo || {}
|
||||
return (
|
||||
<QueryItem
|
||||
key={id}
|
||||
tabInfo={tabInfo}
|
||||
hasQueriesSelected={selectedQueries.length > 0}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{selectedQueries.length > 0 && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="px-2 flex items-center gap-x-2">
|
||||
<Button block type="default" onClick={() => setShowDeleteModal(true)}>
|
||||
Delete {selectedQueries.length.toLocaleString()} quer
|
||||
{selectedQueries.length > 1 ? 'ies' : 'y'}
|
||||
</Button>
|
||||
<Tooltip_Shadcn_ delayDuration={100}>
|
||||
<TooltipTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
type="default"
|
||||
className="px-1"
|
||||
icon={<IconX />}
|
||||
onClick={() => setSelectedQueries([])}
|
||||
/>
|
||||
</TooltipTrigger_Shadcn_>
|
||||
<TooltipContent_Shadcn_ side="bottom">
|
||||
Clear selection
|
||||
</TooltipContent_Shadcn_>
|
||||
</Tooltip_Shadcn_>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{filteredFavoriteSnippets.length > 0 ? (
|
||||
<div className="editor-product-menu">
|
||||
<Menu.Group title="Favorites" />
|
||||
{filteredFavoriteSnippets.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
{filteredFavoriteSnippets.map((tabInfo) => {
|
||||
const { id } = tabInfo || {}
|
||||
return (
|
||||
<QueryItem
|
||||
key={id}
|
||||
tabInfo={tabInfo}
|
||||
isSelected={selectedQueries.includes(id as string)}
|
||||
hasQueriesSelected={selectedQueries.length > 0}
|
||||
onSelectQuery={(isShiftHeld) => onSelectQuery(id, isShiftHeld)}
|
||||
onDeleteQuery={postDeleteCleanup}
|
||||
/>
|
||||
)
|
||||
return <QueryItem key={id} tabInfo={tabInfo} />
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
) : (
|
||||
<div className="text-foreground text-sm h-32 border border-dashed flex flex-col gap-3 items-center justify-center px-3 mx-3 rounded">
|
||||
<span className="text-foreground-lighter">No queries found</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{personalSnippets.length > 0 ? (
|
||||
<div className="editor-product-menu">
|
||||
<Menu.Group title="Your queries" />
|
||||
<div className="space-y-1 pb-8">
|
||||
{personalSnippets.map((tabInfo) => {
|
||||
{projectSnippets.length >= 1 && (
|
||||
<div className="editor-product-menu">
|
||||
<div className="flex flex-row justify-between">
|
||||
<Menu.Group title="Project queries" />
|
||||
<button
|
||||
className="flex items-center w-4 h-4 cursor-pointer mr-3"
|
||||
onClick={() => {
|
||||
setIsProjectSnippetsFilterOpen(!isProjectSnippetsFilterOpen)
|
||||
}}
|
||||
>
|
||||
<IconSearch
|
||||
className={cn(
|
||||
'w-4',
|
||||
'h-4',
|
||||
'cursor-pointer',
|
||||
isProjectSnippetsFilterOpen
|
||||
? 'text-foreground'
|
||||
: 'text-foreground-lighter'
|
||||
)}
|
||||
onClick={() => {
|
||||
setProjectSnippetsFilterString('')
|
||||
setIsProjectSnippetsFilterOpen((state) => !state)
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{isProjectSnippetsFilterOpen && (
|
||||
<div className="pl-3 mb-2 mr-3">
|
||||
<Input
|
||||
autoFocus
|
||||
size="tiny"
|
||||
icon={<IconSearch size="tiny" />}
|
||||
placeholder="Filter"
|
||||
disabled={isLoading}
|
||||
onChange={(e) => setProjectSnippetsFilterString(e.target.value)}
|
||||
value={projectSnippetsFilterString}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
setIsProjectSnippetsFilterOpen(false)
|
||||
setProjectSnippetsFilterString('')
|
||||
}
|
||||
}}
|
||||
actions={
|
||||
projectSnippetsFilterString && (
|
||||
<IconX
|
||||
size={'tiny'}
|
||||
className="mr-2 cursor-pointer"
|
||||
onClick={() => setProjectSnippetsFilterString('')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{filteredProjectSnippets.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
{filteredProjectSnippets.map((tabInfo) => {
|
||||
const { id } = tabInfo || {}
|
||||
return (
|
||||
<QueryItem
|
||||
key={id}
|
||||
tabInfo={tabInfo}
|
||||
isSelected={selectedQueries.includes(id as string)}
|
||||
hasQueriesSelected={selectedQueries.length > 0}
|
||||
onSelectQuery={(isShiftHeld) => onSelectQuery(id, isShiftHeld)}
|
||||
onDeleteQuery={postDeleteCleanup}
|
||||
/>
|
||||
)
|
||||
return <QueryItem key={id} tabInfo={tabInfo} />
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-foreground text-sm h-32 border border-dashed flex flex-col gap-3 items-center justify-center px-3 mx-3 rounded">
|
||||
<span className="text-foreground-lighter">No queries found</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="editor-product-menu">
|
||||
<div className="flex flex-row justify-between">
|
||||
<Menu.Group title="Your queries" />
|
||||
<button
|
||||
className="flex items-center w-4 h-4 cursor-pointer mr-3"
|
||||
onClick={() => {
|
||||
setIsPersonalSnippetsFilterOpen(!isPersonalSnippetsFilterOpen)
|
||||
}}
|
||||
>
|
||||
<IconSearch
|
||||
className={cn(
|
||||
'w-4',
|
||||
'h-4',
|
||||
'cursor-pointer',
|
||||
isPersonalSnippetsFilterOpen ? 'text-foreground' : 'text-foreground-lighter'
|
||||
)}
|
||||
onClick={() => {
|
||||
setPersonalSnippetsFilterString('')
|
||||
setIsPersonalSnippetsFilterOpen((state) => !state)
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{isPersonalSnippetsFilterOpen && (
|
||||
<div className="pl-3 mb-2 mr-3">
|
||||
<Input
|
||||
autoFocus
|
||||
size="tiny"
|
||||
icon={<IconSearch size="tiny" />}
|
||||
placeholder="Filter"
|
||||
disabled={isLoading}
|
||||
onChange={(e) => setPersonalSnippetsFilterString(e.target.value)}
|
||||
value={personalSnippetsFilterString}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
setIsPersonalSnippetsFilterOpen(false)
|
||||
setPersonalSnippetsFilterString('')
|
||||
}
|
||||
}}
|
||||
actions={
|
||||
personalSnippetsFilterString && (
|
||||
<IconX
|
||||
size={'tiny'}
|
||||
className="mr-2 cursor-pointer"
|
||||
onClick={() => setPersonalSnippetsFilterString('')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
</Menu>
|
||||
</div>
|
||||
<ConfirmationModal
|
||||
header="Confirm to delete query"
|
||||
buttonLabel="Delete query"
|
||||
buttonLoadingLabel="Deleting query"
|
||||
size="medium"
|
||||
loading={isDeleting}
|
||||
visible={showDeleteModal}
|
||||
onSelectConfirm={onConfirmDelete}
|
||||
onSelectCancel={() => setShowDeleteModal(false)}
|
||||
>
|
||||
<Modal.Content>
|
||||
<div className="my-6">
|
||||
<div className="text-sm text-foreground-light grid gap-4">
|
||||
<div className="grid gap-y-4">
|
||||
<Alert_Shadcn_ variant="destructive">
|
||||
<WarningIcon />
|
||||
<AlertTitle_Shadcn_>This action cannot be undone</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
The selected SQL snippets cannot be recovered once deleted
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
<p>
|
||||
Are you sure you want to delete the selected {selectedQueries.length} quer
|
||||
{selectedQueries.length > 1 ? 'ies' : 'y'}?
|
||||
</p>
|
||||
)}
|
||||
{personalSnippets.length > 0 ? (
|
||||
<div className="space-y-1 pb-8">
|
||||
{personalSnippets.map((tabInfo) => {
|
||||
const { id } = tabInfo || {}
|
||||
return <QueryItem key={id} tabInfo={tabInfo} />
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-foreground text-sm h-32 border border-dashed flex flex-col gap-3 items-center justify-center px-3 mx-3 rounded">
|
||||
{filteredFavoriteSnippets.length === 0 && (
|
||||
<span className="text-foreground-lighter">No queries found</span>
|
||||
)}
|
||||
<Button type="default" onClick={() => handleNewQuery()}>
|
||||
New Query
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
</ConfirmationModal>
|
||||
</>
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
</Menu>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@ const TableEditorMenu = () => {
|
||||
size="tiny"
|
||||
actions={
|
||||
searchText && (
|
||||
<Button type="text" className="px-1" onClick={() => setSearchText('')}>
|
||||
<Button size="tiny" type="text" onClick={() => setSearchText('')}>
|
||||
<IconX size={12} strokeWidth={2} />
|
||||
</Button>
|
||||
)
|
||||
|
||||
+3
-3
@@ -13,13 +13,13 @@ const FileExplorerHeaderSelection = () => {
|
||||
} = storageExplorerStore
|
||||
|
||||
return (
|
||||
<div className="z-10 flex h-[40px] items-center rounded-t-md bg-brand-400 px-2 py-1 shadow [[data-theme*=dark]_&]:bg-brand-500">
|
||||
<div className="z-10 flex h-[40px] items-center rounded-t-md bg-brand-400 px-2 py-1 shadow [[data-theme*=dark]_&]:bg-brand-600">
|
||||
<Button
|
||||
icon={<IconX size={16} strokeWidth={2} />}
|
||||
type="text"
|
||||
onClick={() => clearSelectedItems()}
|
||||
/>
|
||||
<div className="ml-1 flex items-center space-x-3">
|
||||
<div className="ml-4 flex items-center space-x-3">
|
||||
<p className="mb-0 text-sm text-foreground">
|
||||
<span style={{ fontVariantNumeric: 'tabular-nums' }}>{selectedItems.length}</span> items
|
||||
selected
|
||||
@@ -37,7 +37,7 @@ const FileExplorerHeaderSelection = () => {
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
<div className="border-r border-green-900 py-3 opacity-50" />
|
||||
<div className="border-r border-green-800 py-3 opacity-50" />
|
||||
<Button
|
||||
icon={<IconTrash2 size={16} strokeWidth={2} />}
|
||||
type="primary"
|
||||
|
||||
Vendored
+9
-16
@@ -466,8 +466,8 @@ export interface paths {
|
||||
put: operations['ContentController_updateWholeContent']
|
||||
/** Creates project's content */
|
||||
post: operations['ContentController_createContent']
|
||||
/** Deletes project's contents */
|
||||
delete: operations['ContentController_deleteContents']
|
||||
/** Deletes project's content */
|
||||
delete: operations['ContentController_deleteContent']
|
||||
/** Updates project's content */
|
||||
patch: operations['ContentController_updateContent']
|
||||
}
|
||||
@@ -1238,8 +1238,8 @@ export interface paths {
|
||||
put: operations['ContentController_updateWholeContent']
|
||||
/** Creates project's content */
|
||||
post: operations['ContentController_createContent']
|
||||
/** Deletes project's contents */
|
||||
delete: operations['ContentController_deleteContents']
|
||||
/** Deletes project's content */
|
||||
delete: operations['ContentController_deleteContent']
|
||||
/** Updates project's content */
|
||||
patch: operations['ContentController_updateContent']
|
||||
}
|
||||
@@ -3677,9 +3677,6 @@ export interface components {
|
||||
content?: Record<string, never>
|
||||
owner_id?: number
|
||||
}
|
||||
BulkDeleteUserContentResponse: {
|
||||
id: string
|
||||
}
|
||||
DatabaseDetailResponse: {
|
||||
/** @enum {string} */
|
||||
status:
|
||||
@@ -8612,24 +8609,20 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Deletes project's contents */
|
||||
ContentController_deleteContents: {
|
||||
/** Deletes project's content */
|
||||
ContentController_deleteContent: {
|
||||
parameters: {
|
||||
query: {
|
||||
ids: string[]
|
||||
}
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
id: string
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['BulkDeleteUserContentResponse'][]
|
||||
'application/json': components['schemas']['UserContentObject']
|
||||
}
|
||||
}
|
||||
/** @description Failed to delete project's contents */
|
||||
/** @description Failed to delete project's content */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import { del } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import { delete_, isResponseOk } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { ResponseError, UserContent } from 'types'
|
||||
import { contentKeys } from './keys'
|
||||
|
||||
type DeleteContentVariables = { projectRef: string; ids: string[] }
|
||||
type DeleteContentVariables = { projectRef: string; id: string }
|
||||
|
||||
export async function deleteContents(
|
||||
{ projectRef, ids }: DeleteContentVariables,
|
||||
export async function deleteContent(
|
||||
{ projectRef, id }: DeleteContentVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
const { data, error } = await del('/platform/projects/{ref}/content', {
|
||||
headers: { Version: '2' },
|
||||
params: {
|
||||
path: { ref: projectRef },
|
||||
query: { ids },
|
||||
},
|
||||
signal,
|
||||
})
|
||||
const response = await delete_<UserContent>(
|
||||
`${API_URL}/projects/${projectRef}/content?id=${id}`,
|
||||
undefined,
|
||||
{ signal }
|
||||
)
|
||||
|
||||
if (error) throw error
|
||||
return data.map((x) => x.id)
|
||||
if (!isResponseOk(response)) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
type DeleteContentData = Awaited<ReturnType<typeof deleteContents>>
|
||||
type DeleteContentData = Awaited<ReturnType<typeof deleteContent>>
|
||||
|
||||
export const useContentDeleteMutation = ({
|
||||
onSuccess,
|
||||
@@ -37,7 +37,7 @@ export const useContentDeleteMutation = ({
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<DeleteContentData, ResponseError, DeleteContentVariables>(
|
||||
(args) => deleteContents(args),
|
||||
(args) => deleteContent(args),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
@@ -46,7 +46,7 @@ export const useContentDeleteMutation = ({
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to delete contents: ${data.message}`)
|
||||
toast.error(`Failed to delete content: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
|
||||
@@ -17,15 +17,15 @@
|
||||
"downlevelIteration": true,
|
||||
"incremental": true,
|
||||
"paths": {
|
||||
"@ui/*": ["./../../packages/ui/src/*"], // handle ui package paths
|
||||
"@ui/*": ["./../../packages/ui/src/*"] // handle ui package paths
|
||||
},
|
||||
"useDefineForClassFields": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next",
|
||||
},
|
||||
],
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./*"],
|
||||
"@ui/*": ["./../../packages/ui/src/*"], // handle ui package paths
|
||||
"@ui/*": ["./../../packages/ui/src/*"] // handle ui package paths
|
||||
},
|
||||
"downlevelIteration": true,
|
||||
"target": "es5",
|
||||
@@ -22,10 +22,10 @@
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next",
|
||||
},
|
||||
],
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
},
|
||||
"exclude": ["node_modules", "supabase"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"]
|
||||
}
|
||||
Reference in new issue
Block a user