mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 10:25:06 +03:00
Add feature to share a snippet with the entire project.
This commit is contained in:
1 parent
f76e814be0
commit
d71aeacd73
3 files changed
+245
-70
No files matched your search
@@ -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)
|
||||
<Dropdown.Item onClick={onClickRename} icon={<IconEdit2 size="tiny" />}>
|
||||
Rename query
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Separator />
|
||||
<Dropdown.Item onClick={onClickDelete} icon={<IconTrash size="tiny" />}>
|
||||
Delete query
|
||||
</Dropdown.Item>
|
||||
{visibility === 'user' && (
|
||||
<Dropdown.Item onClick={onClickShare} icon={<IconEdit2 size="tiny" />}>
|
||||
Share query with project
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<>
|
||||
<Dropdown.Separator />
|
||||
<Dropdown.Item onClick={onClickDelete} icon={<IconTrash size="tiny" />}>
|
||||
Delete query
|
||||
</Dropdown.Item>
|
||||
</>
|
||||
</>
|
||||
}
|
||||
>
|
||||
@@ -162,6 +190,18 @@ const QueryItemActions = observer(({ tabInfo, activeId }: QueryItemActionsProps)
|
||||
<p className="py-4 text-sm text-scale-1100">{`Are you sure you want to delete '${name}' ?`}</p>
|
||||
</Modal.Content>
|
||||
</ConfirmationModal>
|
||||
<ConfirmationModal
|
||||
header="Confirm to share"
|
||||
buttonLabel="Share query"
|
||||
buttonLoadingLabel="Sharing query"
|
||||
visible={shareModalOpen}
|
||||
onSelectConfirm={onConfirmShare}
|
||||
onSelectCancel={() => setShareModalOpen(false)}
|
||||
>
|
||||
<Modal.Content>
|
||||
<p className="py-4 text-sm text-scale-1100">{`Are you sure you want to share '${name}' with the project ?`}</p>
|
||||
</Modal.Content>
|
||||
</ConfirmationModal>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -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(() => {
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-6 px-3">
|
||||
{favorites.length >= 1 && (
|
||||
{favoriteSnippets.length >= 1 && (
|
||||
<div className="editor-product-menu">
|
||||
<div className="flex flex-row justify-between">
|
||||
<Menu.Group title="Favorites" />
|
||||
<IconSearch
|
||||
className={cn(
|
||||
'w-4',
|
||||
'h-4',
|
||||
'cursor-pointer',
|
||||
isFavoritesFilterOpen ? 'text-scale-1200' : 'text-scale-900'
|
||||
)}
|
||||
<button
|
||||
className="flex items-center w-4 h-4 cursor-pointer mr-3"
|
||||
onClick={() => {
|
||||
setIsFavoritesFilterOpen((state) => !state)
|
||||
setIsFavoritesFilterOpen(!isFavoritesFilterOpen)
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<IconSearch
|
||||
className={cn(
|
||||
'w-4',
|
||||
'h-4',
|
||||
'cursor-pointer',
|
||||
isFavoritesFilterOpen ? 'text-scale-1200' : 'text-scale-900'
|
||||
)}
|
||||
onClick={() => {
|
||||
setFavoritesFilterString('')
|
||||
setIsFavoritesFilterOpen((state) => !state)
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{isFavoritesFilterOpen && (
|
||||
<div className="pl-3 mb-2">
|
||||
@@ -152,67 +188,155 @@ const SideBarContent = observer(() => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
{favouriteTabs.map((tabInfo) => {
|
||||
const { id } = tabInfo || {}
|
||||
return <QueryItem key={id} tabInfo={tabInfo} />
|
||||
})}
|
||||
</div>
|
||||
{filteredFavoriteSnippets.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
{filteredFavoriteSnippets.map((tabInfo) => {
|
||||
const { id } = tabInfo || {}
|
||||
return <QueryItem key={id} tabInfo={tabInfo} />
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text text-sm h-32 border border-dashed flex flex-col gap-3 items-center justify-center px-3 mx-3 rounded">
|
||||
<span className="text-lighter">No queries found</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{queries.length >= 1 && (
|
||||
|
||||
{projectSnippets.length >= 1 && (
|
||||
<div className="editor-product-menu">
|
||||
<div className="flex flex-row justify-between">
|
||||
<Menu.Group title="SQL snippets" />
|
||||
<IconSearch
|
||||
className={cn(
|
||||
'w-4',
|
||||
'h-4',
|
||||
'cursor-pointer',
|
||||
isSnippetsFilterOpen ? 'text-scale-1200' : 'text-scale-900'
|
||||
)}
|
||||
<Menu.Group title="Project snippets" />
|
||||
<button
|
||||
className="flex items-center w-4 h-4 cursor-pointer mr-3"
|
||||
onClick={() => {
|
||||
setIsSnippetsFilterOpen((state) => !state)
|
||||
setIsProjectSnippetsFilterOpen(!isProjectSnippetsFilterOpen)
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<IconSearch
|
||||
className={cn(
|
||||
'w-4',
|
||||
'h-4',
|
||||
'cursor-pointer',
|
||||
isProjectSnippetsFilterOpen ? 'text-scale-1200' : 'text-scale-900'
|
||||
)}
|
||||
onClick={() => {
|
||||
setProjectSnippetsFilterString('')
|
||||
setIsProjectSnippetsFilterOpen((state) => !state)
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{isSnippetsFilterOpen && (
|
||||
<div className="pl-3 mb-2">
|
||||
{isProjectSnippetsFilterOpen && (
|
||||
<div className="pl-3 mb-2 mr-3">
|
||||
<Input
|
||||
autoFocus
|
||||
size="tiny"
|
||||
icon={<IconSearch size="tiny" />}
|
||||
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 && (
|
||||
<IconX
|
||||
size={'tiny'}
|
||||
className="mr-2 cursor-pointer"
|
||||
onClick={() => setSnippetsFilterString('')}
|
||||
onClick={() => setProjectSnippetsFilterString('')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{filteredProjectSnippets.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
{filteredProjectSnippets.map((tabInfo) => {
|
||||
const { id } = tabInfo || {}
|
||||
return <QueryItem key={id} tabInfo={tabInfo} />
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text text-sm h-32 border border-dashed flex flex-col gap-3 items-center justify-center px-3 mx-3 rounded">
|
||||
<span className="text-lighter">No queries found</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="editor-product-menu">
|
||||
<div className="flex flex-row justify-between">
|
||||
<Menu.Group title="Personal SQL snippets" />
|
||||
<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-scale-1200' : 'text-scale-900'
|
||||
)}
|
||||
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>
|
||||
)}
|
||||
{personalSnippets.length > 0 ? (
|
||||
<div className="space-y-1 pb-8">
|
||||
{snippetsTabs.map((tabInfo) => {
|
||||
{personalSnippets.map((tabInfo) => {
|
||||
const { id } = tabInfo || {}
|
||||
return <QueryItem key={id} tabInfo={tabInfo} />
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{snippetsFilterString.length > 0 &&
|
||||
favouriteTabs.length === 0 &&
|
||||
snippetsTabs.length === 0 && (
|
||||
<div className="px-4">
|
||||
<p className="text-sm">No queries found</p>
|
||||
) : (
|
||||
<div className="text text-sm h-32 border border-dashed flex flex-col gap-3 items-center justify-center px-3 mx-3 rounded">
|
||||
<span className="text-lighter">No queries found</span>
|
||||
<Button type="default" onClick={() => handleNewQuery()}>
|
||||
New Query
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user