diff --git a/studio/components/layouts/SQLEditorLayout/QueryItem.tsx b/studio/components/layouts/SQLEditorLayout/QueryItem.tsx
index 8deac4330f8..56f5994d0cc 100644
--- a/studio/components/layouts/SQLEditorLayout/QueryItem.tsx
+++ b/studio/components/layouts/SQLEditorLayout/QueryItem.tsx
@@ -1,17 +1,17 @@
-import { useState, useEffect, useRef } from 'react'
+import clsx from 'clsx'
import { useParams } from 'common'
+import RenameQueryModal from 'components/interfaces/SQLEditor/RenameQueryModal'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import { useContentDeleteMutation } from 'data/content/content-delete-mutation'
+import { SqlSnippet } from 'data/content/sql-snippets-query'
import { useStore } from 'hooks'
import { IS_PLATFORM } from 'lib/constants'
import { observer } from 'mobx-react-lite'
-import { Dropdown, IconEdit2, IconTrash, Button, IconChevronDown, Modal } from 'ui'
-import RenameQueryModal from 'components/interfaces/SQLEditor/RenameQueryModal'
-import clsx from 'clsx'
import Link from 'next/link'
-import { useSqlEditorStateSnapshot } from 'state/sql-editor'
import { useRouter } from 'next/router'
-import { SqlSnippet } from 'data/content/sql-snippets-query'
+import { useEffect, useRef, useState } from 'react'
+import { useSqlEditorStateSnapshot } from 'state/sql-editor'
+import { Button, Dropdown, IconChevronDown, IconEdit2, IconTrash, Modal } from 'ui'
export interface QueryItemProps {
tabInfo: SqlSnippet
@@ -85,8 +85,9 @@ const QueryItemActions = observer(({ tabInfo, activeId }: QueryItemActionsProps)
},
})
- const { id, name } = tabInfo || {}
+ const { id, name, visibility } = tabInfo || {}
const [renameModalOpen, setRenameModalOpen] = useState(false)
+ const [shareModalOpen, setShareModalOpen] = useState(false)
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
const isActive = id === activeId
@@ -99,11 +100,31 @@ const QueryItemActions = observer(({ tabInfo, activeId }: QueryItemActionsProps)
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) {
+ 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')
@@ -121,10 +142,17 @@ const QueryItemActions = observer(({ tabInfo, activeId }: QueryItemActionsProps)
}>
Rename query
-
- }>
- Delete query
-
+ {visibility === 'user' && (
+ }>
+ Share query with project
+
+ )}
+ <>
+
+ }>
+ Delete query
+
+ >
>
}
>
@@ -162,6 +190,18 @@ const QueryItemActions = observer(({ tabInfo, activeId }: QueryItemActionsProps)
{`Are you sure you want to delete '${name}' ?`}
+ setShareModalOpen(false)}
+ >
+
+ {`Are you sure you want to share '${name}' with the project ?`}
+
+
)
})
diff --git a/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx b/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx
index 2c43969cf23..e22060db0b4 100644
--- a/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx
+++ b/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx
@@ -1,5 +1,4 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { partition } from 'lodash'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { useMemo, useState } from 'react'
@@ -14,7 +13,7 @@ import { useCheckPermissions, useStore } from 'hooks'
import { uuidv4 } from 'lib/helpers'
import { useProfile } from 'lib/profile'
import { useSnippets, useSqlEditorStateSnapshot } from 'state/sql-editor'
-import { IconSearch, IconX, Input, Menu, cn } from 'ui'
+import { Button, cn, IconSearch, IconX, Input, Menu } from 'ui'
import QueryItem from './QueryItem'
const SideBarContent = observer(() => {
@@ -23,9 +22,11 @@ const SideBarContent = observer(() => {
const router = useRouter()
const { profile } = useProfile()
- const [snippetsFilterString, setSnippetsFilterString] = useState('')
+ const [personalSnippetsFilterString, setPersonalSnippetsFilterString] = useState('')
+ const [projectSnippetsFilterString, setProjectSnippetsFilterString] = useState('')
const [favoritesFilterString, setFavoritesFilterString] = useState('')
- const [isSnippetsFilterOpen, setIsSnippetsFilterOpen] = useState(false)
+ const [isPersonalSnippetsFilterOpen, setIsPersonalSnippetsFilterOpen] = useState(false)
+ const [isProjectSnippetsFilterOpen, setIsProjectSnippetsFilterOpen] = useState(false)
const [isFavoritesFilterOpen, setIsFavoritesFilterOpen] = useState(false)
const snap = useSqlEditorStateSnapshot()
@@ -39,22 +40,42 @@ const SideBarContent = observer(() => {
const snippets = useSnippets(ref)
- const [favorites, queries] = useMemo(
- () => (snippets ? partition(snippets, (snippet) => snippet.content.favorite) : [[], []]),
- [snippets]
- )
+ const projectSnippets = useMemo(() => {
+ return snippets.filter((snippet) => snippet.visibility === 'project')
+ }, [snippets])
- const favouriteTabs =
- favoritesFilterString.length === 0
- ? favorites
- : favorites.filter((tab) =>
- tab.name.toLowerCase().includes(favoritesFilterString.toLowerCase())
- )
+ const filteredProjectSnippets = useMemo(() => {
+ if (projectSnippetsFilterString.length > 0) {
+ return projectSnippets.filter((tab) =>
+ tab.name.toLowerCase().includes(projectSnippetsFilterString.toLowerCase())
+ )
+ }
+ return projectSnippets
+ }, [projectSnippets, projectSnippetsFilterString])
- const snippetsTabs =
- snippetsFilterString.length === 0
- ? queries
- : queries.filter((tab) => tab.name.toLowerCase().includes(snippetsFilterString.toLowerCase()))
+ const personalSnippets = useMemo(() => {
+ const ss = snippets.filter((snippet) => snippet.visibility === 'user')
+
+ if (personalSnippetsFilterString.length > 0) {
+ return ss.filter((tab) =>
+ tab.name.toLowerCase().includes(personalSnippetsFilterString.toLowerCase())
+ )
+ }
+ return ss
+ }, [personalSnippetsFilterString, snippets])
+
+ const favoriteSnippets = useMemo(() => {
+ return snippets.filter((snippet) => snippet.content.favorite)
+ }, [snippets])
+
+ const filteredFavoriteSnippets = useMemo(() => {
+ if (favoritesFilterString.length > 0) {
+ return favoriteSnippets.filter((tab) =>
+ tab.name.toLowerCase().includes(favoritesFilterString.toLowerCase())
+ )
+ }
+ return favoriteSnippets
+ }, [favoriteSnippets, favoritesFilterString])
const canCreateSQLSnippet = useCheckPermissions(PermissionAction.CREATE, 'user_content', {
resource: { type: 'sql', owner_id: profile?.id },
@@ -80,6 +101,13 @@ const SideBarContent = observer(() => {
snap.addSnippet(data as SqlSnippet, ref, true)
router.push(`/project/${ref}/sql/${data.id}`)
+ // 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',
@@ -114,21 +142,29 @@ const SideBarContent = observer(() => {
/>
- {favorites.length >= 1 && (
+ {favoriteSnippets.length >= 1 && (
- {
- setIsFavoritesFilterOpen((state) => !state)
+ setIsFavoritesFilterOpen(!isFavoritesFilterOpen)
}}
- />
+ >
+ {
+ setFavoritesFilterString('')
+ setIsFavoritesFilterOpen((state) => !state)
+ }}
+ />
+
{isFavoritesFilterOpen && (
@@ -152,67 +188,155 @@ const SideBarContent = observer(() => {
/>
)}
-
- {favouriteTabs.map((tabInfo) => {
- const { id } = tabInfo || {}
- return
- })}
-
+ {filteredFavoriteSnippets.length > 0 ? (
+
+ {filteredFavoriteSnippets.map((tabInfo) => {
+ const { id } = tabInfo || {}
+ return
+ })}
+
+ ) : (
+
+ No queries found
+
+ )}
)}
- {queries.length >= 1 && (
+
+ {projectSnippets.length >= 1 && (
-
-
+
- {isSnippetsFilterOpen && (
-
+ {isProjectSnippetsFilterOpen && (
+
}
placeholder="Filter"
disabled={isLoading}
- onChange={(e) => setSnippetsFilterString(e.target.value)}
- value={snippetsFilterString}
+ onChange={(e) => setProjectSnippetsFilterString(e.target.value)}
+ value={projectSnippetsFilterString}
+ onKeyDown={(e) => {
+ if (e.key === 'Escape') {
+ setIsProjectSnippetsFilterOpen(false)
+ setProjectSnippetsFilterString('')
+ }
+ }}
actions={
- snippetsFilterString && (
+ projectSnippetsFilterString && (
setSnippetsFilterString('')}
+ onClick={() => setProjectSnippetsFilterString('')}
/>
)
}
/>
)}
+ {filteredProjectSnippets.length > 0 ? (
+
+ {filteredProjectSnippets.map((tabInfo) => {
+ const { id } = tabInfo || {}
+ return
+ })}
+
+ ) : (
+
+ No queries found
+
+ )}
+
+ )}
+
+
+
+
+
+
+ {isPersonalSnippetsFilterOpen && (
+
+ }
+ placeholder="Filter"
+ disabled={isLoading}
+ onChange={(e) => setPersonalSnippetsFilterString(e.target.value)}
+ value={personalSnippetsFilterString}
+ onKeyDown={(e) => {
+ if (e.key === 'Escape') {
+ setIsPersonalSnippetsFilterOpen(false)
+ setPersonalSnippetsFilterString('')
+ }
+ }}
+ actions={
+ personalSnippetsFilterString && (
+ setPersonalSnippetsFilterString('')}
+ />
+ )
+ }
+ />
+
+ )}
+ {personalSnippets.length > 0 ? (
- {snippetsTabs.map((tabInfo) => {
+ {personalSnippets.map((tabInfo) => {
const { id } = tabInfo || {}
return
})}
-
- )}
- {snippetsFilterString.length > 0 &&
- favouriteTabs.length === 0 &&
- snippetsTabs.length === 0 && (
-
-
No queries found
+ ) : (
+
+ No queries found
+
)}
+
) : (
diff --git a/studio/state/sql-editor.ts b/studio/state/sql-editor.ts
index 49833b3d8f1..b1af73ea2a1 100644
--- a/studio/state/sql-editor.ts
+++ b/studio/state/sql-editor.ts
@@ -132,6 +132,16 @@ export const sqlEditorState = proxy({
sqlEditorState.needsSaving.add(id)
}
},
+ shareSnippet: (id: string, visibility: 'user' | 'project' | 'org' | 'public') => {
+ if (sqlEditorState.snippets[id]) {
+ const { snippet, projectRef } = sqlEditorState.snippets[id]
+
+ snippet.visibility = visibility
+
+ sqlEditorState.reorderSnippets(projectRef)
+ sqlEditorState.needsSaving.add(id)
+ }
+ },
addNeedsSaving: (id: string) => {
sqlEditorState.needsSaving.add(id)
},
@@ -232,6 +242,7 @@ if (typeof window !== 'undefined') {
id,
name: snippet.snippet.name,
description: snippet.snippet.description,
+ visibility: snippet.snippet.visibility,
})
sqlEditorState.needsSaving.delete(id)