From d71aeacd739c2cc054e4047640d24fee12e48690 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Wed, 2 Aug 2023 10:34:24 +0200 Subject: [PATCH] Add feature to share a snippet with the entire project. --- .../layouts/SQLEditorLayout/QueryItem.tsx | 62 ++++- .../layouts/SQLEditorLayout/SQLEditorMenu.tsx | 242 +++++++++++++----- studio/state/sql-editor.ts | 11 + 3 files changed, 245 insertions(+), 70 deletions(-) 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)