Files
supabase/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx
Ivan Vasilov 308cd791a2 chore: Prep work for migrating to Tailwind v4 (#45285)
This PR preps the monorepo for a migration to Tailwind v4:
- Bump all Tailwind dependencies and libraries to the latest possible
version, while still compatible with Tailwind 3.
- Cleans up obsolete Tailwind 3 specific options and configs.
- Cleans up unused CSS files and fixes the CSS imports.
- Migrates all `important` uses in `@apply` lines to using the `!`
prefix.
- Move `typography.css` to the `config` package and import it from the
apps.
- Migrated all occurrences of `flex-grow`, `flex-shrink`,
`overflow-clip` and `overflow-ellipsis` since they're deprecated and
will be removed in Tailwind 4.
- Make the default theme object typesafe in the `ui` package.
- Migrate all `bg-opacity`, `border-opacity`, `ring-opacity` and
`divider-opacity` to the new format where they're declared as part of
the property color.
- Bump and unify all imports of `postcss` dependency.
2026-04-28 11:33:53 +02:00

170 lines
6.0 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useDebounce } from '@uidotdev/usehooks'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { FilePlus, FolderPlus, Plus, X } from 'lucide-react'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import {
InnerSideBarFilters,
InnerSideBarFilterSearchInput,
InnerSideBarFilterSortDropdown,
InnerSideBarFilterSortDropdownItem,
} from 'ui-patterns/InnerSideMenu'
import { SearchList } from './SQLEditorNavV2/SearchList'
import { SQLEditorNav } from './SQLEditorNavV2/SQLEditorNav'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useLocalStorage } from '@/hooks/misc/useLocalStorage'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useProfile } from '@/lib/profile'
import { getAppStateSnapshot } from '@/state/app-state'
import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2'
export const SQLEditorMenu = () => {
const router = useRouter()
const { ref } = useParams()
const { profile } = useProfile()
const { data: project } = useSelectedProjectQuery()
const snapV2 = useSqlEditorV2StateSnapshot()
const [search, setSearch] = useState('')
const [showSearch, setShowSearch] = useState(false)
const [sort, setSort] = useLocalStorage<'name' | 'inserted_at'>(
LOCAL_STORAGE_KEYS.SQL_EDITOR_SORT(ref ?? ''),
'inserted_at'
)
const appState = getAppStateSnapshot()
const debouncedSearch = useDebounce(search, 500)
const { can: canCreateSQLSnippet } = useAsyncCheckPermissions(
PermissionAction.CREATE,
'user_content',
{
resource: { type: 'sql', owner_id: profile?.id },
subject: { id: profile?.id },
}
)
const createNewFolder = () => {
if (!ref) return console.error('Project ref is required')
setSearch('')
setShowSearch(false)
snapV2.addNewFolder({ projectRef: ref })
}
const handleNewQuery = async () => {
if (!ref) return console.error('Project ref is required')
if (!project) return console.error('Project is required')
if (!profile) return console.error('Profile is required')
if (!canCreateSQLSnippet) {
return toast('Your queries will not be saved as you do not have sufficient permissions')
}
try {
router.push(`/project/${ref}/sql/new?skip=true`)
setSearch('')
setShowSearch(false)
} catch (error: any) {
toast.error(`Failed to create new query: ${error.message}`)
}
}
useEffect(() => {
setShowSearch(debouncedSearch.length > 0)
}, [debouncedSearch])
return (
<div className="h-full flex flex-col justify-between">
<div className="flex flex-col gap-y-4 grow">
<div className="mt-4 mx-4 flex items-center justify-between gap-x-2">
<InnerSideBarFilters className="w-full p-0 gap-0">
<InnerSideBarFilterSearchInput
name="search-queries"
placeholder="Search queries..."
aria-labelledby="Search queries"
value={search}
onChange={(e) => {
const value = e.target.value
setSearch(value)
if (value.length === 0) setShowSearch(false)
}}
onKeyDown={(e) => {
if (e.code === 'Escape') {
setSearch('')
setShowSearch(false)
}
}}
>
{showSearch ? (
<Tooltip>
<TooltipTrigger
className="absolute right-1 top-[.4rem] md:top-[.3rem] transition-colors text-foreground-light hover:text-foreground"
onClick={() => {
setSearch('')
setShowSearch(false)
}}
>
<X size={18} />
</TooltipTrigger>
<TooltipContent>Clear search</TooltipContent>
</Tooltip>
) : (
<InnerSideBarFilterSortDropdown
value={sort}
onValueChange={(value: any) => setSort(value)}
>
<InnerSideBarFilterSortDropdownItem key="name" value="name">
Alphabetical
</InnerSideBarFilterSortDropdownItem>
<InnerSideBarFilterSortDropdownItem key="inserted_at" value="inserted_at">
Created At
</InnerSideBarFilterSortDropdownItem>
</InnerSideBarFilterSortDropdown>
)}
</InnerSideBarFilterSearchInput>
</InnerSideBarFilters>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
data-testid="sql-editor-new-query-button"
type="default"
icon={<Plus className="text-foreground" />}
className="w-[26px]"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="bottom" className="w-48">
<DropdownMenuItem className="gap-x-2" onClick={() => handleNewQuery()}>
<FilePlus size={14} />
Create a new snippet
</DropdownMenuItem>
<DropdownMenuItem className="gap-x-2" onClick={() => createNewFolder()}>
<FolderPlus size={14} />
Create a new folder
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{showSearch ? <SearchList search={debouncedSearch} /> : <SQLEditorNav sort={sort} />}
</div>
<div className="p-4 border-t sticky bottom-0 bg-studio">
<Button block type="default" onClick={() => appState.setOnGoingQueriesPanelOpen(true)}>
View running queries
</Button>
</div>
</div>
)
}