feat: new item style for table editor and empty states for both editors (#33357)

* feat: new item style for table editor and empty states for both editors

* flex shrink issue

* Update SqlEditorMenuStaticLinks.tsx

* Fix the test.

---------

Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
This commit is contained in:
Jonathan Summers-Muir
2025-02-06 17:22:50 +08:00
committed by GitHub
parent c8413fa022
commit 958cebbb2b
17 changed files with 645 additions and 389 deletions

View File

@@ -30,10 +30,6 @@ import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { useAppStateSnapshot } from 'state/app-state'
import { useParams } from 'common'
interface OngoingQueriesPanel {
onClose: () => void
}
export const OngoingQueriesPanel = () => {
const [_, setParams] = useUrlState({ replace: true })
const { viewOngoingQueries } = useParams()

View File

@@ -0,0 +1,34 @@
import { Pointer } from 'lucide-react'
import { TreeViewItem } from 'ui'
import { InnerSideBarEmptyPanel } from 'ui-patterns'
export const EmptyPrivateQueriesPanel = () => (
<InnerSideBarEmptyPanel
title="No private queries created yet"
description="Queries will be automatically saved once you start writing in the editor"
className="mx-4"
>
<div className="top-0 left-6 flex flex-col opacity-50 cursor-not-allowed bg-dash-sidebar h-content -mb-7 pointer-events-none scale-75">
<div className="relative h-content">
<div className="absolute inset-0 pointer-events-none z-10">
<div className="absolute inset-0 bg-gradient-to-t from-transparent from-80% to-100% to-background-surface-100 dark:to-background-surface-75" />
<div className="absolute inset-0 bg-gradient-to-r from-transparent from-50% to-100% to-background-surface-100 dark:to-background-surface-75" />
</div>
<div className="absolute left-[128px] bottom-[21px] text-foreground-muted z-10 pointer-events-none">
<Pointer size={16} className="text-foreground-light" strokeWidth={1.5} />
</div>
{[...Array(4)].map((_, i) => (
<div className="border-l pointer-events-none" key={`dummy-${i + 1}`}>
<TreeViewItem
isSelected={i === 2}
id={`dummy-${i + 1}`}
name={`dummy_query_${i + 1}`}
level={1}
xPadding={16}
/>
</div>
))}
</div>
</div>
</InnerSideBarEmptyPanel>
)

View File

@@ -1,10 +1,8 @@
import { ReactNode, useEffect, useMemo, useState } from 'react'
import { OngoingQueriesPanel } from 'components/interfaces/SQLEditor/OngoingQueriesPanel'
import { withAuth } from 'hooks/misc/withAuth'
import { ReactNode, useMemo } from 'react'
import ProjectLayout from '../ProjectLayout/ProjectLayout'
import { SQLEditorMenu } from './SQLEditorMenu'
import { useParams } from 'common'
export interface SQLEditorLayoutProps {
title: string

View File

@@ -1,9 +1,4 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { FilePlus, FolderPlus, Plus, X } from 'lucide-react'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { useDebounce } from '@uidotdev/usehooks'
import { useParams } from 'common'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
@@ -11,6 +6,11 @@ import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useProfile } from 'lib/profile'
import { FilePlus, FolderPlus, Plus, X } from 'lucide-react'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { getAppStateSnapshot } from 'state/app-state'
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
import {
Button,
@@ -27,11 +27,10 @@ import {
InnerSideBarFilterSearchInput,
InnerSideBarFilterSortDropdown,
InnerSideBarFilterSortDropdownItem,
InnerSideMenuItem,
} from 'ui-patterns/InnerSideMenu'
import { SQLEditorNav } from './SQLEditorNavV2/SQLEditorNav'
import { SqlEditorMenuStaticLinks } from './SqlEditorMenuStaticLinks'
import { SearchList } from './SQLEditorNavV2/SearchList'
import { getAppStateSnapshot } from 'state/app-state'
import { SQLEditorNav } from './SQLEditorNavV2/SQLEditorNav'
export const SQLEditorMenu = () => {
const router = useRouter()
@@ -157,23 +156,7 @@ export const SQLEditorMenu = () => {
<SearchList search={debouncedSearch} />
) : (
<>
<div className="px-2">
<InnerSideMenuItem
title="Templates"
isActive={router.asPath === `/project/${ref}/sql/templates`}
href={`/project/${ref}/sql/templates`}
>
Templates
</InnerSideMenuItem>
<InnerSideMenuItem
title="Quickstarts"
isActive={router.asPath === `/project/${ref}/sql/quickstarts`}
href={`/project/${ref}/sql/quickstarts`}
>
Quickstarts
</InnerSideMenuItem>
</div>
<SqlEditorMenuStaticLinks />
<SQLEditorNav sort={sort} />
</>
)}

View File

@@ -9,6 +9,7 @@ import { MoveQueryModal } from 'components/interfaces/SQLEditor/MoveQueryModal'
import RenameQueryModal from 'components/interfaces/SQLEditor/RenameQueryModal'
import { untitledSnippetTitle } from 'components/interfaces/SQLEditor/SQLEditor.constants'
import { createSqlSnippetSkeletonV2 } from 'components/interfaces/SQLEditor/SQLEditor.utils'
import EditorMenuListSkeleton from 'components/layouts/TableEditorLayout/EditorMenuListSkeleton'
import { useContentCountQuery } from 'data/content/content-count-query'
import { useContentDeleteMutation } from 'data/content/content-delete-mutation'
import { getContentById } from 'data/content/content-id-query'
@@ -36,6 +37,7 @@ import {
InnerSideMenuSeparator,
} from 'ui-patterns'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { EmptyPrivateQueriesPanel } from 'components/layouts/SQLEditorLayout/PrivateSqlSnippetEmpty'
import SQLEditorLoadingSnippets from './SQLEditorLoadingSnippets'
import { formatFolderResponseForTreeView, getLastItemIds, ROOT_NODE } from './SQLEditorNav.utils'
import { SQLEditorTreeViewItem } from './SQLEditorTreeViewItem'
@@ -53,6 +55,7 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
const project = useSelectedProject()
const { ref: projectRef, id } = useParams()
const snapV2 = useSqlEditorV2StateSnapshot()
// const tabStore = getTabsStore(projectRef)
const [sectionVisibility, setSectionVisibility] = useLocalStorage<SectionState>(
LOCAL_STORAGE_KEYS.SQL_EDITOR_SECTION_STATE(projectRef ?? ''),
DEFAULT_SECTION_STATE
@@ -487,14 +490,14 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
useEffect(() => {
if (projectRef && privateSnippetsPages) {
privateSnippetsPages.pages.forEach((page) => {
page.contents?.forEach((snippet) => {
page.contents?.forEach((snippet: Snippet) => {
snapV2.addSnippet({
projectRef,
snippet,
})
})
page.folders?.forEach((folder) => snapV2.addFolder({ projectRef, folder }))
page.folders?.forEach((folder: SnippetFolder) => snapV2.addFolder({ projectRef, folder }))
})
}
}, [projectRef, privateSnippetsPages?.pages])
@@ -527,58 +530,58 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
return (
<>
<InnerSideMenuSeparator />
<InnerSideMenuCollapsible
className="px-0"
open={showSharedSnippets}
onOpenChange={(value) => {
setSectionVisibility({ ...(sectionVisibility ?? DEFAULT_SECTION_STATE), shared: value })
}}
className="px-0"
>
<InnerSideMenuCollapsibleTrigger
title={`Shared ${numProjectSnippets > 0 ? ` (${numProjectSnippets})` : ''}`}
/>
<InnerSideMenuCollapsibleContent className="group-data-[state=open]:pt-2">
{isLoadingSharedSqlSnippets ? (
<SQLEditorLoadingSnippets />
) : numProjectSnippets === 0 ? (
{numProjectSnippets === 0 ? (
<InnerSideBarEmptyPanel
className="mx-2 px-3"
className="mx-2"
title="No shared queries"
description="Share queries with your team by right-clicking on the query"
description="Share queries with your team by right-clicking on the query."
/>
) : (
<TreeView
data={projectSnippetsTreeState}
aria-label="project-level-snippets"
nodeRenderer={({ element, ...props }) => (
<SQLEditorTreeViewItem
{...props}
element={element}
onSelectDelete={() => {
setShowDeleteModal(true)
setSelectedSnippets([element.metadata as unknown as Snippet])
}}
onSelectRename={() => {
setShowRenameModal(true)
setSelectedSnippetToRename(element.metadata as Snippet)
}}
onSelectDownload={() => {
setSelectedSnippetToDownload(element.metadata as Snippet)
}}
onSelectDuplicate={() => {
onSelectDuplicate(element.metadata as Snippet)
}}
onSelectUnshare={() => {
setSelectedSnippetToUnshare(element.metadata as Snippet)
}}
isLastItem={projectSnippetsLastItemIds.has(element.id as string)}
hasNextPage={hasMoreSharedSqlSnippets}
fetchNextPage={fetchNextSharedSqlSnippets}
isFetchingNextPage={isFetchingMoreSharedSqlSnippets}
/>
)}
nodeRenderer={({ element, ...props }) => {
const isActive = element.metadata?.id === id
return (
<SQLEditorTreeViewItem
{...props}
isSelected={isActive}
element={element}
onSelectDelete={() => {
setShowDeleteModal(true)
setSelectedSnippets([element.metadata as unknown as Snippet])
}}
onSelectRename={() => {
setShowRenameModal(true)
setSelectedSnippetToRename(element.metadata as Snippet)
}}
onSelectDownload={() => {
setSelectedSnippetToDownload(element.metadata as Snippet)
}}
onSelectDuplicate={() => {
onSelectDuplicate(element.metadata as Snippet)
}}
onSelectUnshare={() => {
setSelectedSnippetToUnshare(element.metadata as Snippet)
}}
isLastItem={projectSnippetsLastItemIds.has(element.id as string)}
hasNextPage={hasMoreSharedSqlSnippets}
fetchNextPage={fetchNextSharedSqlSnippets}
isFetchingNextPage={isFetchingMoreSharedSqlSnippets}
/>
)
}}
/>
)}
</InnerSideMenuCollapsibleContent>
@@ -615,34 +618,38 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
<TreeView
data={favoritesTreeState}
aria-label="favorite-snippets"
nodeRenderer={({ element, ...props }) => (
<SQLEditorTreeViewItem
{...props}
element={element}
onSelectDelete={() => {
setShowDeleteModal(true)
setSelectedSnippets([element.metadata as unknown as Snippet])
}}
onSelectRename={() => {
setShowRenameModal(true)
setSelectedSnippetToRename(element.metadata as Snippet)
}}
onSelectDownload={() => {
setSelectedSnippetToDownload(element.metadata as Snippet)
}}
onSelectDuplicate={() => {
onSelectDuplicate(element.metadata as Snippet)
}}
onSelectShare={() => setSelectedSnippetToShare(element.metadata as Snippet)}
onSelectUnshare={() => {
setSelectedSnippetToUnshare(element.metadata as Snippet)
}}
isLastItem={favoriteSnippetsLastItemIds.has(element.id as string)}
hasNextPage={hasMoreFavoriteSqlSnippets}
fetchNextPage={fetchNextFavoriteSqlSnippets}
isFetchingNextPage={isFetchingMoreFavoriteSqlSnippets}
/>
)}
nodeRenderer={({ element, ...props }) => {
const isActive = element.metadata?.id === id
return (
<SQLEditorTreeViewItem
{...props}
isSelected={isActive}
element={element}
onSelectDelete={() => {
setShowDeleteModal(true)
setSelectedSnippets([element.metadata as unknown as Snippet])
}}
onSelectRename={() => {
setShowRenameModal(true)
setSelectedSnippetToRename(element.metadata as Snippet)
}}
onSelectDownload={() => {
setSelectedSnippetToDownload(element.metadata as Snippet)
}}
onSelectDuplicate={() => {
onSelectDuplicate(element.metadata as Snippet)
}}
onSelectShare={() => setSelectedSnippetToShare(element.metadata as Snippet)}
onSelectUnshare={() => {
setSelectedSnippetToUnshare(element.metadata as Snippet)
}}
isLastItem={favoriteSnippetsLastItemIds.has(element.id as string)}
hasNextPage={hasMoreFavoriteSqlSnippets}
fetchNextPage={fetchNextFavoriteSqlSnippets}
isFetchingNextPage={isFetchingMoreFavoriteSqlSnippets}
/>
)
}}
/>
)}
</InnerSideMenuCollapsibleContent>
@@ -663,13 +670,9 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
/>
<InnerSideMenuCollapsibleContent className="group-data-[state=open]:pt-2">
{isLoading ? (
<SQLEditorLoadingSnippets />
<EditorMenuListSkeleton />
) : folders.length === 0 && numPrivateSnippets === 0 ? (
<InnerSideBarEmptyPanel
className="mx-3 px-4"
title="No queries created yet"
description="Queries will be automatically saved once you start writing in the editor"
/>
<EmptyPrivateQueriesPanel />
) : (
<TreeView
multiSelect
@@ -688,75 +691,82 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
}
}}
expandedIds={expandedFolderIds}
nodeRenderer={({ element, ...props }) => (
<SQLEditorTreeViewItem
{...props}
element={element}
isMultiSelected={selectedSnippets.length > 1}
isLastItem={privateSnippetsLastItemIds.has(element.id as string)}
status={props.isBranch ? snapV2.folders[element.id].status : 'idle'}
onMultiSelect={onMultiSelect}
onSelectCreate={() => {
if (profile && project) {
const snippet = createSqlSnippetSkeletonV2({
id: uuidv4(),
name: untitledSnippetTitle,
owner_id: profile?.id,
project_id: project?.id,
folder_id: element.id as string,
sql: '',
})
snapV2.addSnippet({ projectRef: project.ref, snippet })
router.push(`/project/${projectRef}/sql/${snippet.id}`)
}
}}
onSelectDelete={() => {
if (props.isBranch) {
setSelectedFolderToDelete(element.metadata as SnippetFolder)
} else {
setShowDeleteModal(true)
if (selectedSnippets.length === 0) {
setSelectedSnippets([element.metadata as unknown as Snippet])
nodeRenderer={({ element, ...props }) => {
const isActive = element.metadata?.id === id
return (
<SQLEditorTreeViewItem
{...props}
element={element}
isSelected={isActive}
isMultiSelected={selectedSnippets.length > 1}
isLastItem={privateSnippetsLastItemIds.has(element.id as string)}
status={props.isBranch ? snapV2.folders[element.id].status : 'idle'}
onMultiSelect={onMultiSelect}
onSelectCreate={() => {
if (profile && project) {
const snippet = createSqlSnippetSkeletonV2({
id: uuidv4(),
name: untitledSnippetTitle,
owner_id: profile?.id,
project_id: project?.id,
folder_id: element.id as string,
sql: '',
})
snapV2.addSnippet({ projectRef: project.ref, snippet })
router.push(`/project/${projectRef}/sql/${snippet.id}`)
}
}}
onSelectDelete={() => {
if (props.isBranch) {
setSelectedFolderToDelete(element.metadata as SnippetFolder)
} else {
setShowDeleteModal(true)
if (selectedSnippets.length === 0) {
setSelectedSnippets([element.metadata as unknown as Snippet])
}
}
}}
onSelectRename={() => {
if (props.isBranch) {
snapV2.editFolder(element.id as string)
} else {
setShowRenameModal(true)
setSelectedSnippetToRename(element.metadata as Snippet)
}
}}
onSelectMove={() => {
setShowMoveModal(true)
if (selectedSnippets.length === 0) {
setSelectedSnippets([element.metadata as Snippet])
}
}}
onSelectDownload={() =>
setSelectedSnippetToDownload(element.metadata as Snippet)
}
}}
onSelectRename={() => {
if (props.isBranch) {
snapV2.editFolder(element.id as string)
} else {
setShowRenameModal(true)
setSelectedSnippetToRename(element.metadata as Snippet)
}
}}
onSelectMove={() => {
setShowMoveModal(true)
if (selectedSnippets.length === 0) {
setSelectedSnippets([element.metadata as Snippet])
}
}}
onSelectDownload={() => setSelectedSnippetToDownload(element.metadata as Snippet)}
onSelectDuplicate={() => onSelectDuplicate(element.metadata as Snippet)}
onSelectShare={() => setSelectedSnippetToShare(element.metadata as Snippet)}
onEditSave={(name: string) => {
// [Joshen] Inline editing only for folders for now
if (name.length === 0 && element.id === 'new-folder') {
snapV2.removeFolder(element.id as string)
} else if (name.length > 0) {
snapV2.saveFolder({ id: element.id as string, name })
}
}}
hasNextPage={hasNextPage}
fetchNextPage={fetchNextPage}
isFetchingNextPage={isFetchingNextPage}
sort={sort}
onFolderContentsChange={({ isLoading, snippets }) => {
setSubResults((prev) => ({
...prev,
[element.id as string]: { snippets, isLoading },
}))
}}
/>
)}
onSelectDuplicate={() => onSelectDuplicate(element.metadata as Snippet)}
onSelectShare={() => setSelectedSnippetToShare(element.metadata as Snippet)}
onEditSave={(name: string) => {
// [Joshen] Inline editing only for folders for now
if (name.length === 0 && element.id === 'new-folder') {
snapV2.removeFolder(element.id as string)
} else if (name.length > 0) {
snapV2.saveFolder({ id: element.id as string, name })
}
}}
hasNextPage={hasNextPage}
fetchNextPage={fetchNextPage}
isFetchingNextPage={isFetchingNextPage}
sort={sort}
onFolderContentsChange={({ isLoading, snippets }) => {
setSubResults((prev) => ({
...prev,
[element.id as string]: { snippets, isLoading },
}))
}}
/>
)
}}
/>
)}
</InnerSideMenuCollapsibleContent>

View File

@@ -1,9 +1,4 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Copy, Download, Edit, ExternalLink, Lock, Move, Plus, Share, Trash } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { IS_PLATFORM } from 'common'
import { useParams } from 'common/hooks/useParams'
import { useSQLSnippetFolderContentsQuery } from 'data/content/sql-folder-contents-query'
@@ -11,6 +6,10 @@ import { Snippet } from 'data/content/sql-folders-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import useLatest from 'hooks/misc/useLatest'
import { useProfile } from 'lib/profile'
import { Copy, Download, Edit, ExternalLink, Lock, Move, Plus, Share, Trash } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { ComponentProps, useEffect } from 'react'
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
import {
Button,
@@ -22,12 +21,9 @@ import {
TreeViewItem,
} from 'ui'
interface SQLEditorTreeViewItemProps {
interface SQLEditorTreeViewItemProps
extends Omit<ComponentProps<typeof TreeViewItem>, 'name' | 'xPadding'> {
element: any
level: number
isBranch: boolean
isSelected: boolean
isExpanded: boolean
isMultiSelected?: boolean
status?: 'editing' | 'saving' | 'idle'
getNodeProps: () => any
@@ -79,11 +75,12 @@ export const SQLEditorTreeViewItem = ({
sort,
name,
onFolderContentsChange,
...props
}: SQLEditorTreeViewItemProps) => {
const router = useRouter()
const { id, ref: projectRef } = useParams()
const { profile } = useProfile()
const { className, onClick } = getNodeProps()
const snapV2 = useSqlEditorV2StateSnapshot()
const isOwner = profile?.id === element?.metadata.owner_id
@@ -163,12 +160,9 @@ export const SQLEditorTreeViewItem = ({
<ContextMenuTrigger_Shadcn_ asChild>
<TreeViewItem
level={level}
xPadding={16}
name={element.name}
className={className}
isExpanded={isExpanded}
isBranch={isBranch}
isSelected={isSelected || id === element.id}
isSelected={isSelected}
isEditing={isEditing}
isLoading={(isEnabled && isLoading) || isSaving}
onEditSubmit={(value) => {
@@ -189,10 +183,11 @@ export const SQLEditorTreeViewItem = ({
if (isEditing) {
return
}
onClick(e)
}
}}
{...props}
name={element.name}
xPadding={16}
/>
</ContextMenuTrigger_Shadcn_>
<ContextMenuContent_Shadcn_ onCloseAutoFocus={(e) => e.stopPropagation()}>

View File

@@ -0,0 +1,33 @@
import { useParams } from 'common'
import { useRouter } from 'next/router'
import { InnerSideMenuDataItem } from 'ui-patterns/InnerSideMenu'
const OPTIONS = ['templates', 'quickstarts'] as const
export function SqlEditorMenuStaticLinks() {
const { ref } = useParams()
const router = useRouter()
function isPageActive(key: string): boolean {
return router.asPath === `/project/${ref}/sql/${key}`
}
return (
<div>
{OPTIONS.map((pageId) => {
const active = isPageActive(pageId)
return (
<InnerSideMenuDataItem
title="Templates"
isActive={active}
isOpened={false}
href={`/project/${ref}/sql/${pageId}`}
className="capitalize"
>
{pageId}
</InnerSideMenuDataItem>
)
})}
</div>
)
}

View File

@@ -0,0 +1,25 @@
import { memo } from 'react'
import { Skeleton } from 'ui'
const EditorMenuListSkeleton = memo(function EditorMenuListSkeleton() {
const items = [
{ width: 'w-40', opacity: 'opacity-100' },
{ width: 'w-32', opacity: 'opacity-100' },
{ width: 'w-20', opacity: 'opacity-75' },
{ width: 'w-40', opacity: 'opacity-50' },
{ width: 'w-20', opacity: 'opacity-25' },
]
return (
<div className="px-4 flex flex-col gap-0">
{items.map((item, index) => (
<div key={index} className={`flex flex-row h-6 items-center gap-3 ${item.opacity}`}>
<Skeleton className="h-4 w-5" />
<Skeleton className={`h-4 ${item.width}`} />
</div>
))}
</div>
)
})
export default EditorMenuListSkeleton

View File

@@ -1,20 +1,3 @@
import saveAs from 'file-saver'
import {
Clipboard,
Copy,
Download,
Edit,
Eye,
Lock,
MoreHorizontal,
Table2,
Trash,
Unlock,
} from 'lucide-react'
import Link from 'next/link'
import Papa from 'papaparse'
import { toast } from 'sonner'
import { IS_PLATFORM } from 'common'
import {
MAX_EXPORT_ROW_COUNT,
@@ -25,6 +8,7 @@ import {
formatTableRowsToSQL,
getEntityLintDetails,
} from 'components/interfaces/TableGridEditor/TableEntity.utils'
import { EntityTypeIcon } from 'components/ui/EntityTypeIcon'
import type { ItemRenderer } from 'components/ui/InfiniteList'
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import { Entity } from 'data/entity-types/entity-types-infinite-query'
@@ -33,8 +17,14 @@ import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id'
import { getTableEditor } from 'data/table-editor/table-editor-query'
import { isTableLike } from 'data/table-editor/table-editor-types'
import { fetchAllTableRows } from 'data/table-rows/table-rows-query'
import saveAs from 'file-saver'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { copyToClipboard } from 'lib/helpers'
import { Clipboard, Copy, Download, Edit, Lock, MoreHorizontal, Trash, Unlock } from 'lucide-react'
import Link from 'next/link'
import Papa from 'papaparse'
import { toast } from 'sonner'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import {
cn,
@@ -49,13 +39,16 @@ import {
Tooltip,
TooltipContent,
TooltipTrigger,
TreeViewItemVariant,
} from 'ui'
import { useSnapshot } from 'valtio'
import { useProjectContext } from '../ProjectLayout/ProjectContext'
export interface EntityListItemProps {
id: number
id: number | string
projectRef: string
isLocked: boolean
isActive?: boolean
}
const EntityListItem: ItemRenderer<Entity, EntityListItemProps> = ({
@@ -63,6 +56,7 @@ const EntityListItem: ItemRenderer<Entity, EntityListItemProps> = ({
projectRef,
item: entity,
isLocked,
isActive: _isActive,
}) => {
const { project } = useProjectContext()
const snap = useTableEditorStateSnapshot()
@@ -212,106 +206,25 @@ const EntityListItem: ItemRenderer<Entity, EntityListItemProps> = ({
}
}
const EntityTooltipTrigger = ({ entity }: { entity: Entity }) => {
let tooltipContent = null
switch (entity.type) {
case ENTITY_TYPE.TABLE:
if (tableHasLints) {
tooltipContent = 'RLS disabled'
}
break
case ENTITY_TYPE.VIEW:
if (viewHasLints) {
tooltipContent = 'Security definer view'
}
break
case ENTITY_TYPE.MATERIALIZED_VIEW:
if (materializedViewHasLints) {
tooltipContent = 'Security definer view'
}
break
case ENTITY_TYPE.FOREIGN_TABLE:
tooltipContent = 'RLS is not enforced on foreign tables'
break
default:
break
}
if (tooltipContent) {
return (
<Tooltip disableHoverableContent={true}>
<TooltipTrigger className="min-w-4" asChild>
<Unlock
size={14}
strokeWidth={2}
className={cn('min-w-4', isActive ? 'text-warning-600' : 'text-warning-500')}
/>
</TooltipTrigger>
<TooltipContent side="bottom">{tooltipContent}</TooltipContent>
</Tooltip>
)
}
return null
}
return (
<div className="px-2">
<EditorTablePageLink
title={entity.name}
id={String(entity.id)}
href={`/project/${projectRef}/editor/${entity.id}?schema=${selectedSchema}`}
role="button"
aria-label={`View ${entity.name}`}
className={cn(
'w-full',
'flex items-center gap-2',
'py-1 px-2',
'text-light',
'rounded-md',
isActive ? 'bg-selection' : 'hover:bg-surface-200 focus:bg-surface-200',
'group',
'transition'
)}
>
<EditorTablePageLink
title={entity.name}
id={String(entity.id)}
href={`/project/${projectRef}/editor/${entity.id}?schema=${selectedSchema}`}
role="button"
aria-label={`View ${entity.name}`}
className={cn(
TreeViewItemVariant({
isSelected: isActive,
}),
'px-4'
)}
>
<>
{isActive && <div className="absolute left-0 h-full w-0.5 bg-foreground" />}
<Tooltip disableHoverableContent={true}>
<TooltipTrigger className="min-w-4" asChild>
{entity.type === ENTITY_TYPE.TABLE ? (
<Table2
size={15}
strokeWidth={1.5}
className={cn(
'text-foreground-muted group-hover:text-foreground-lighter',
isActive && 'text-foreground-lighter',
'transition-colors'
)}
/>
) : entity.type === ENTITY_TYPE.VIEW ? (
<Eye
size={15}
strokeWidth={1.5}
className={cn(
'text-foreground-muted group-hover:text-foreground-lighter',
isActive && 'text-foreground-lighter',
'transition-colors'
)}
/>
) : (
<div
className={cn(
'flex items-center justify-center text-xs h-4 w-4 rounded-[2px] font-bold',
entity.type === ENTITY_TYPE.FOREIGN_TABLE && 'text-yellow-900 bg-yellow-500',
entity.type === ENTITY_TYPE.MATERIALIZED_VIEW && 'text-purple-1000 bg-purple-500',
entity.type === ENTITY_TYPE.PARTITIONED_TABLE &&
'text-foreground-light bg-border-stronger'
)}
>
{Object.entries(ENTITY_TYPE)
.find(([, value]) => value === entity.type)?.[0]?.[0]
?.toUpperCase()}
</div>
)}
<TooltipTrigger className="min-w-4">
<EntityTypeIcon type={entity.type} isActive={isActive} />
</TooltipTrigger>
<TooltipContent side="bottom">{formatTooltipText(entity.type)}</TooltipContent>
</Tooltip>
@@ -332,12 +245,18 @@ const EntityListItem: ItemRenderer<Entity, EntityListItemProps> = ({
>
{entity.name}
</span>
<EntityTooltipTrigger entity={entity} />
<EntityTooltipTrigger
entity={entity}
isActive={isActive}
tableHasLints={tableHasLints}
viewHasLints={viewHasLints}
materializedViewHasLints={materializedViewHasLints}
/>
</div>
{canEdit && (
<DropdownMenu>
<DropdownMenuTrigger className="text-foreground-lighter transition-all hover:text-foreground data-[state=open]:text-foreground">
<DropdownMenuTrigger className="text-foreground-lighter transition-all text-transparent group-hover:text-foreground data-[state=open]:text-foreground">
<MoreHorizontal size={14} strokeWidth={2} />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="start" className="w-44">
@@ -435,9 +354,67 @@ const EntityListItem: ItemRenderer<Entity, EntityListItemProps> = ({
</DropdownMenuContent>
</DropdownMenu>
)}
</EditorTablePageLink>
</div>
</>
</EditorTablePageLink>
)
}
const EntityTooltipTrigger = ({
entity,
isActive,
tableHasLints,
viewHasLints,
materializedViewHasLints,
}: {
entity: Entity
isActive: boolean
tableHasLints: boolean
viewHasLints: boolean
materializedViewHasLints: boolean
}) => {
let tooltipContent = ''
switch (entity.type) {
case ENTITY_TYPE.TABLE:
if (tableHasLints) {
tooltipContent = 'RLS disabled'
}
break
case ENTITY_TYPE.VIEW:
if (viewHasLints) {
tooltipContent = 'Security definer view'
}
break
case ENTITY_TYPE.MATERIALIZED_VIEW:
if (materializedViewHasLints) {
tooltipContent = 'Security definer view'
}
break
case ENTITY_TYPE.FOREIGN_TABLE:
tooltipContent = 'RLS is not enforced on foreign tables'
break
default:
break
}
if (tooltipContent) {
return (
<Tooltip disableHoverableContent={true}>
<TooltipTrigger className="min-w-4">
<Unlock
size={14}
strokeWidth={2}
className={cn('min-w-4', isActive ? 'text-warning-600' : 'text-warning-500')}
/>
</TooltipTrigger>
<TooltipContent side="bottom">
<span>{tooltipContent}</span>
</TooltipContent>
</Tooltip>
)
}
return null
}
export default EntityListItem

View File

@@ -1,11 +1,8 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { partition } from 'lodash'
import { Filter, Plus } from 'lucide-react'
import { useRouter } from 'next/router'
import { useEffect, useMemo, useState } from 'react'
import { useBreakpoint, useParams } from 'common'
import { useParams } from 'common'
import { useBreakpoint } from 'common/hooks/useBreakpoint'
import { ProtectedSchemaModal } from 'components/interfaces/Database/ProtectedSchemaWarning'
import EditorMenuListSkeleton from 'components/layouts/TableEditorLayout/EditorMenuListSkeleton'
import AlertError from 'components/ui/AlertError'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import InfiniteList from 'components/ui/InfiniteList'
@@ -18,6 +15,10 @@ import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { partition } from 'lodash'
import { Filter, Plus } from 'lucide-react'
import { useRouter } from 'next/router'
import { useEffect, useMemo, useState } from 'react'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import {
AlertDescription_Shadcn_,
@@ -36,10 +37,10 @@ import {
InnerSideBarFilterSortDropdown,
InnerSideBarFilterSortDropdownItem,
InnerSideBarFilters,
InnerSideBarShimmeringLoaders,
} from 'ui-patterns/InnerSideMenu'
import { useProjectContext } from '../ProjectLayout/ProjectContext'
import EntityListItem from './EntityListItem'
import { TableMenuEmptyState } from './TableMenuEmptyState'
const TableEditorMenu = () => {
const { id: _id } = useParams()
@@ -114,10 +115,7 @@ const TableEditorMenu = () => {
return (
<>
<div
className="pt-5 flex flex-col flex-grow gap-5 h-full"
style={{ maxHeight: 'calc(100vh - 48px)' }}
>
<div className="flex flex-col flex-grow gap-5 pt-5 h-full">
<div className="flex flex-col gap-y-1.5">
<SchemaSelector
className="mx-4"
@@ -170,8 +168,8 @@ const TableEditorMenu = () => {
)}
</div>
</div>
<div className="flex flex-auto flex-col gap-2 pb-4 px-2">
<InnerSideBarFilters>
<div className="flex flex-auto flex-col gap-2 pb-4">
<InnerSideBarFilters className="mx-2">
<InnerSideBarFilterSearchInput
autoFocus={!isMobile}
name="search-tables"
@@ -248,20 +246,18 @@ const TableEditorMenu = () => {
</Popover_Shadcn_>
</InnerSideBarFilters>
{isLoading && <InnerSideBarShimmeringLoaders />}
{isLoading && <EditorMenuListSkeleton />}
{isError && (
<AlertError error={(error ?? null) as any} subject="Failed to retrieve tables" />
<div className="mx-4">
<AlertError error={(error ?? null) as any} subject="Failed to retrieve tables" />
</div>
)}
{isSuccess && (
<>
{searchText.length === 0 && (entityTypes?.length ?? 0) <= 0 && (
<InnerSideBarEmptyPanel
className="mx-2"
title="No entities available"
description="This schema has no entities available yet"
/>
<TableMenuEmptyState />
)}
{searchText.length > 0 && (entityTypes?.length ?? 0) <= 0 && (
<InnerSideBarEmptyPanel
@@ -271,9 +267,10 @@ const TableEditorMenu = () => {
/>
)}
{(entityTypes?.length ?? 0) > 0 && (
<div className="flex flex-1 -mx-2" data-testid="tables-list">
<div className="flex flex-1 flex-grow" data-testid="tables-list">
<InfiniteList
items={entityTypes}
// @ts-expect-error
ItemComponent={EntityListItem}
itemProps={{
projectRef: project?.ref!,

View File

@@ -0,0 +1,48 @@
import { useParams } from 'common'
import { Pointer } from 'lucide-react'
import { useRef } from 'react'
import { InnerSideBarEmptyPanel } from 'ui-patterns/InnerSideMenu'
import { cn, TreeViewItemVariant } from 'ui'
import { EntityTypeIcon } from 'components/ui/EntityTypeIcon'
export const TableMenuEmptyState = () => {
return (
<InnerSideBarEmptyPanel
title="No tables or views"
description="Any tables or views you create will be listed here."
className="mx-4"
>
<div className="top-0 left-6 flex flex-col opacity-50 cursor-not-allowed bg-dash-sidebar h-content -mb-7 pointer-events-none scale-75">
<div className="relative h-content">
<div className="absolute inset-0 pointer-events-none z-10">
<div className="absolute inset-0 bg-gradient-to-t from-transparent from-80% to-100% to-background-surface-100 dark:to-background-surface-75" />
<div className="absolute inset-0 bg-gradient-to-r from-transparent from-50% to-100% to-background-surface-100 dark:to-background-surface-75" />
</div>
<div className="absolute left-[150px] bottom-[21px] text-foreground-muted z-10 pointer-events-none">
<Pointer size={16} className="text-foreground-light" strokeWidth={1.5} />
</div>
{[...Array(4)].map((_, i) => (
<div className="border-l pointer-events-none" key={`some-${i}`}>
<div
className={cn(
'group',
TreeViewItemVariant({
isSelected: i === 2 ? true : false,
isOpened: i === 2 ? true : false,
isPreview: false,
}),
'px-4 min-w-40'
)}
aria-selected={i === 2}
>
{i === 2 && <div className="absolute left-0 h-full w-0.5 bg-foreground" />}
<EntityTypeIcon type={'r'} />
{`postgres_table_${i}`}
</div>
</div>
))}
</div>
</div>
</InnerSideBarEmptyPanel>
)
}

View File

@@ -0,0 +1,80 @@
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import { Eye, GitBranch, Table2 } from 'lucide-react'
import { cn, SQL_ICON } from 'ui'
interface EntityTypeIconProps {
type: 'sql' | 'schema' | 'new' | 'r' | 'v' | 'm' | 'f' | 'p'
size?: number
strokeWidth?: number
isActive?: boolean
}
export const EntityTypeIcon = ({
type,
size = 15,
strokeWidth = 1.5,
isActive,
}: EntityTypeIconProps) => {
if (type === 'sql') {
return (
<SQL_ICON
size={size}
className={cn(
'transition-colors',
'fill-foreground-muted',
'group-aria-selected:fill-foreground',
'w-4 h-4',
'-ml-0.5'
)}
strokeWidth={strokeWidth}
/>
)
}
if (type === ENTITY_TYPE.TABLE) {
return (
<Table2
size={size}
strokeWidth={strokeWidth}
className={cn(
'text-foreground-muted group-hover:text-foreground-lighter group-aria-selected:text-foreground',
isActive && 'text-foreground-light',
'transition-colors'
)}
/>
)
}
if (type === 'schema') {
return <GitBranch size={size} strokeWidth={strokeWidth} />
}
if (type === ENTITY_TYPE.VIEW) {
return (
<Eye
size={size}
strokeWidth={strokeWidth}
className={cn(
'text-foreground-muted group-hover:text-foreground-lighter',
isActive && 'text-foreground-lighter',
'transition-colors'
)}
/>
)
}
return (
<div
className={cn(
'flex items-center justify-center text-xs h-4 w-4 rounded-[2px] font-bold',
type === ENTITY_TYPE.FOREIGN_TABLE && 'text-yellow-900 bg-yellow-500',
type === ENTITY_TYPE.MATERIALIZED_VIEW && 'text-purple-1000 bg-purple-500',
type === ENTITY_TYPE.PARTITIONED_TABLE && 'text-foreground-light bg-border-stronger'
)}
>
{Object.entries(ENTITY_TYPE)
.find(([, value]) => value === type)?.[0]?.[0]
?.toUpperCase()}
</div>
)
}

View File

@@ -1,11 +1,10 @@
import { propsAreEqual } from 'lib/helpers'
import memoize from 'memoize-one'
import { CSSProperties, ComponentType, MutableRefObject, ReactNode, memo, useRef } from 'react'
import AutoSizer from 'react-virtualized-auto-sizer'
import { VariableSizeList, areEqual } from 'react-window'
import InfiniteLoader from 'react-window-infinite-loader'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { propsAreEqual } from 'lib/helpers'
import { Skeleton } from 'ui'
/**
* Note that the loading more logic of this component works best with a cursor-based
@@ -58,9 +57,28 @@ const Item = memo(<T, P>({ data, index, style }: ItemProps<T, P>) => {
<div style={style}>{LoaderComponent}</div>
) : (
<div className="space-y-1 my-1" style={style}>
<ShimmeringLoader />
<ShimmeringLoader />
<ShimmeringLoader />
<div className="flex flex-col gap-y-1">
<div className="flex flex-row h-6 px-4 items-center gap-2">
<Skeleton className="h-4 w-5" />
<Skeleton className="w-40 h-4" />
</div>
<div className="flex flex-row h-6 px-4 items-center gap-2">
<Skeleton className="h-4 w-5" />
<Skeleton className="w-32 h-4" />
</div>
<div className="flex flex-row h-6 px-4 items-center gap-2 opacity-75">
<Skeleton className="h-4 w-5" />
<Skeleton className="w-20 h-4" />
</div>
<div className="flex flex-row h-6 px-4 items-center gap-2 opacity-50">
<Skeleton className="h-4 w-5" />
<Skeleton className="w-40 h-4" />
</div>
<div className="flex flex-row h-6 px-4 items-center gap-2 opacity-25">
<Skeleton className="h-4 w-5" />
<Skeleton className="w-20 h-4" />
</div>
</div>
</div>
)
}, areEqual)
@@ -92,35 +110,34 @@ function InfiniteList<T, P>({
const itemData = createItemData(items, { itemProps, ItemComponent, LoaderComponent, listRef })
return (
<div className="relative flex flex-col flex-grow">
<div className="flex-grow">
<AutoSizer>
{({ height, width }: { height: number; width: number }) => (
<InfiniteLoader
itemCount={itemCount}
isItemLoaded={isItemLoaded}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<VariableSizeList
ref={(refy) => {
ref(refy)
listRef.current = refy
}}
height={height ?? 0}
width={width ?? 0}
itemCount={itemCount}
itemData={itemData}
itemSize={getItemSize}
onItemsRendered={onItemsRendered}
>
{Item}
</VariableSizeList>
)}
</InfiniteLoader>
)}
</AutoSizer>
</div>
<>
<AutoSizer>
{({ height, width }: { height: number; width: number }) => (
<InfiniteLoader
itemCount={itemCount}
isItemLoaded={isItemLoaded}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<VariableSizeList
ref={(refy) => {
ref(refy)
listRef.current = refy
}}
height={height ?? 0}
width={width ?? 0}
itemCount={itemCount}
itemData={itemData}
itemSize={getItemSize}
onItemsRendered={onItemsRendered}
>
{Item}
</VariableSizeList>
)}
</InfiniteLoader>
)}
</AutoSizer>
<div
style={{
position: 'absolute',
@@ -131,7 +148,7 @@ function InfiniteList<T, P>({
pointerEvents: 'none', //https://github.com/bvaughn/react-window/issues/455
}}
/>
</div>
</>
)
}

View File

@@ -1,10 +1,9 @@
import { Check, ChevronsUpDown, Plus } from 'lucide-react'
import { useState } from 'react'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { Check, ChevronsUpDown, Plus } from 'lucide-react'
import { useState } from 'react'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
@@ -21,6 +20,7 @@ import {
PopoverTrigger_Shadcn_,
Popover_Shadcn_,
ScrollArea,
Skeleton,
} from 'ui'
interface SchemaSelectorProps {
@@ -69,8 +69,14 @@ const SchemaSelector = ({
return (
<div className={className}>
{isSchemasLoading && (
<Button type="default" className="justify-start" block size={size} loading>
Loading schemas...
<Button
type="default"
key="schema-selector-skeleton"
className="w-full [&>span]:w-full"
size={size}
disabled
>
<Skeleton className="w-full h-3 bg-foreground-muted" />
</Button>
)}
@@ -95,14 +101,15 @@ const SchemaSelector = ({
size={size}
disabled={disabled}
type="default"
className={`w-full [&>span]:w-full`}
data-testid="schema-selector"
className={`w-full [&>span]:w-full !pr-1 space-x-1`}
iconRight={
<ChevronsUpDown className="text-foreground-muted" strokeWidth={2} size={14} />
}
>
{selectedSchemaName ? (
<div className="w-full flex gap-1">
<p className="text-foreground-lighter">schema:</p>
<p className="text-foreground-lighter">schema</p>
<p className="text-foreground">
{selectedSchemaName === '*' ? 'All schemas' : selectedSchemaName}
</p>
@@ -114,7 +121,12 @@ const SchemaSelector = ({
)}
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="p-0" side="bottom" align="start" sameWidthAsTrigger>
<PopoverContent_Shadcn_
className="p-0 min-w-[200px]"
side="bottom"
align="start"
sameWidthAsTrigger
>
<Command_Shadcn_>
<CommandInput_Shadcn_ placeholder="Find schema..." />
<CommandList_Shadcn_>

View File

@@ -15,6 +15,7 @@ import {
Tooltip,
TooltipContent,
TooltipTrigger,
TreeViewItemVariant,
cn,
} from 'ui'
import ShimmeringLoader from '../ShimmeringLoader'
@@ -115,6 +116,37 @@ const InnerSideMenuItem = forwardRef<
)
})
const InnerSideMenuDataItem = forwardRef<
ElementRef<typeof Link>,
React.ComponentPropsWithoutRef<typeof Link> & {
isActive?: boolean
forceHoverState?: boolean | null
isPreview?: boolean
isOpened?: boolean
}
>(({ isActive = true, forceHoverState, isPreview, isOpened = true, ...props }, ref) => {
return (
<Link
ref={ref}
{...props}
aria-current={isActive}
className={cn(
TreeViewItemVariant({
isSelected: isActive && !isPreview,
isOpened: isOpened && !isPreview,
isPreview,
}),
'px-4',
// forceHoverState && 'bg-surface-200',
props.className
)}
>
{!isPreview && isActive && <div className="absolute left-0 h-full w-0.5 bg-foreground" />}
{props.children}
</Link>
)
})
function InnerSideMenuItemLoading({
className,
...props
@@ -250,17 +282,18 @@ const InnerSideBarEmptyPanel = forwardRef<
ref={ref}
{...props}
className={cn(
'border bg-surface-100/50 flex flex-col gap-y-3 items-center justify-center rounded-md px-5 py-4',
'border border-muted bg-surface-100 dark:bg-surface-75 flex flex-col gap-y-3 items-center justify-center rounded-md px-5 py-4',
props.className
)}
>
<div className="flex flex-col gap-y-1 items-center justify-center">
<div className="w-full flex flex-col gap-y-1 items-center">
{illustration}
{title && <p className="text-xs text-foreground-light">{title}</p>}
{description && (
<p className="text-xs text-foreground-lighter text-center">{description}</p>
)}
{actions && <div className="mt-2">{actions}</div>}
{props.children}
</div>
</div>
)
@@ -277,6 +310,7 @@ export {
InnerSideMenuCollapsible,
InnerSideMenuCollapsibleContent,
InnerSideMenuCollapsibleTrigger,
InnerSideMenuDataItem,
InnerSideMenuItem,
InnerSideMenuItemLoading,
InnerSideMenuSeparator,

View File

@@ -5,9 +5,33 @@ import { ComponentPropsWithoutRef, ReactNode, forwardRef, useEffect, useRef, use
import TreeViewPrimitive, { flattenTree } from 'react-accessible-treeview'
import { cn } from '../../lib/utils'
import { Input } from '../shadcn/ui/input'
import { cva, VariantProps } from 'class-variance-authority'
const TreeView = TreeViewPrimitive
export type TreeViewItemVariantProps = VariantProps<typeof TreeViewItemVariant>
export const TreeViewItemVariant = cva(
// [Joshen Temp]: aria-selected:text-foreground not working as aria-selected property not rendered in DOM,
// [Joshen Temp]: aria-selected:!bg-selection not working as aria-selected property not rendered in DOM
'group relative transition-colors h-[28px] flex items-center gap-3 text-sm cursor-pointer select-none text-foreground-light hover:bg-control aria-expanded:bg-control data-[state=open]:bg-control', // data-[state=open]:bg-control bg state for context menu open
{
variants: {
isSelected: {
true: 'text-foreground !bg-selection', // bg state for context menu open
false: '',
},
isOpened: {
true: 'bg-control',
false: '',
},
isPreview: {
true: 'bg-control text-foreground',
false: '',
},
},
}
)
const TreeViewItem = forwardRef<
HTMLDivElement,
ComponentPropsWithoutRef<'div'> & {
@@ -15,6 +39,10 @@ const TreeViewItem = forwardRef<
level: number
/** Specifies if the item is expanded */
isExpanded?: boolean
/** Specifies if the item is opened somewhere */
isOpened?: boolean
/** Specifies if the item is a preview */
isPreview?: boolean
/** Specifies if the item is a branch */
isBranch?: boolean
/** The padding for each level of the item */
@@ -40,8 +68,10 @@ const TreeViewItem = forwardRef<
level = 1,
levelPadding = 56,
isExpanded = false,
isOpened = false,
isBranch = false,
isSelected = false,
isPreview = false,
isLoading = false,
xPadding = 16,
name = '',
@@ -92,20 +122,7 @@ const TreeViewItem = forwardRef<
aria-selected={isSelected}
aria-expanded={!isEditing && isExpanded}
{...props}
className={cn(
'group relative',
'transition-colors',
'h-[28px]',
'flex items-center gap-3',
'text-sm',
'cursor-pointer select-none',
'text-foreground-light',
'hover:bg-control',
'aria-expanded:bg-control',
isSelected ? 'text-foreground' : '', // [Joshen Temp]: aria-selected:text-foreground not working as aria-selected property not rendered in DOM,
isSelected ? '!bg-selection' : '', // [Joshen Temp]: aria-selected:!bg-selection not working as aria-selected property not rendered in DOM
'data-[state=open]:bg-control' // bg state for context menu open
)}
className={cn(TreeViewItemVariant({ isSelected, isOpened, isPreview }))}
style={{
paddingLeft:
level === 1 && !isBranch

View File

@@ -108,7 +108,7 @@ test.describe('Table Editor page', () => {
{ timeout: 0 }
)
await page.getByRole('button', { name: 'schema: public' }).click()
await page.getByTestId('schema-selector').click()
await page.getByRole('option', { name: 'auth' }).click()
// wait for the table data to load for the auth schema