Repository: supabase/supabase
Pull request #34461: use our own tree view
Author: login: SaxonF
Created: 2025-03-27T06:05:11Z | Merged: 2025-03-27T08:16:21Z

<img width="1016" alt="image" src="https://github.com/user-attachments/assets/87785039-56c5-432a-8302-a098ee8b45ad" />

This PR edits ui library component pages to use our own ui tree view component.


Diff:
diff --git a/apps/ui-library/components/block-item-code.tsx b/apps/ui-library/components/block-item-code.tsx
index af6960be53741..1187f340cb4cc 100644
--- a/apps/ui-library/components/block-item-code.tsx
+++ b/apps/ui-library/components/block-item-code.tsx
@@ -6,120 +6,118 @@ import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock'
 import { useState } from 'react'
 import supabaseTheme from '../lib/themes/supabase-2.json' assert { type: 'json' }
 import { BlockItemPreview } from './block-item'
-import { File, Folder } from './files'
+import { CodeBlock, CodeBlockLang, TreeView, TreeViewItem, flattenTree } from 'ui'
+import { File } from 'lucide-react'
 
 interface BlockItemCodeProps {
   files: RegistryNode[]
 }
 
-const demoUrls: Record<string, string> = {
-  'src/registry/blocks/password-based-auth-nextjs/app/login/page.tsx':
-    '/blocks/password-based-auth-nextjs/login',
-  'src/registry/blocks/password-based-auth-nextjs/app/sign-up/page.tsx':
-    '/blocks/password-based-auth-nextjs/sign-up',
-  'src/registry/blocks/password-based-auth-nextjs/app/sign-up-success/page.tsx':
-    '/blocks/password-based-auth-nextjs/sign-up-success',
-  'src/registry/blocks/password-based-auth-nextjs/app/forgot-password/page.tsx':
-    '/blocks/password-based-auth-nextjs/forgot-password',
-  'src/registry/blocks/password-based-auth-nextjs/app/update-password/page.tsx':
-    '/blocks/password-based-auth-nextjs/update-password',
+interface TreeNode {
+  name: string
+  children: TreeNode[]
+  metadata: { path: string }
 }
 
-function FileTree({
-  items,
-  onFileSelect,
-}: {
-  items: RegistryNode[]
-  onFileSelect: (file: RegistryNode) => void
-}) {
-  if (items.length === 0) {
-    return null
-  }
-
-  return (
-    <>
-      {items.map((item, index) => {
-        if (item.type === 'directory') {
-          return (
-            <Folder key={index} name={item.name} defaultOpen>
-              <FileTree onFileSelect={onFileSelect} items={item.children ?? []} />
-            </Folder>
-          )
-        }
-        return (
-          <File
-            key={index}
-            name={item.name}
-            onClick={() => onFileSelect(item)}
-            className="cursor-pointer"
-          />
-        )
-      })}
-    </>
+const flattenChildren = (files: RegistryNode[]): TreeNode[] => {
+  return files.map(
+    (node): TreeNode => ({
+      name: node.name,
+      children: node.children ? flattenChildren(node.children) : [],
+      metadata: { path: node.path },
+    })
   )
 }
 
+const findFirstFile = (nodes: RegistryNode[]): RegistryNode | null => {
+  for (const node of nodes) {
+    if (node.type === 'file') {
+      return node
+    }
+    if (node.children) {
+      const foundFile = findFirstFile(node.children)
+      if (foundFile) {
+        return foundFile
+      }
+    }
+  }
+  return null
+}
+
 export function BlockItemCode({ files }: BlockItemCodeProps) {
-  const initialFile = files.find((file) => file.type === 'file') || null
-  const [selectedFile, setSelectedFile] = useState(initialFile)
+  // Find the first file to select by default
+  const [selectedFile, setSelectedFile] = useState<RegistryNode | null>(findFirstFile(files))
+  const flattenedData = flattenTree({ name: '', children: flattenChildren(files) })
 
-  const selectedFilePath = selectedFile?.path.startsWith('/')
-    ? selectedFile?.path.slice(1)
-    : selectedFile?.path
-  const selectedFileExtension = (selectedFile?.name.split('.').pop() ?? 'ts') as 'ts' | 'tsx'
+  // Handle file selection from the TreeView
+  const handleNodeSelect = (element: any) => {
+    const findFileByPath = (nodes: RegistryNode[], path: string): RegistryNode | null => {
+      for (const node of nodes) {
+        if (node.path === path) {
+          return node
+        }
+        if (node.children) {
+          const found = findFileByPath(node.children, path)
+          if (found) {
+            return found
+          }
+        }
+      }
+      return null
+    }
 
-  const selectedFileDemoUrl = selectedFile?.originalPath
-    ? demoUrls[selectedFile?.originalPath]
-    : null
+    const filePath = element.metadata.path
+    const foundFile = findFileByPath(files, filePath)
+
+    if (foundFile?.type === 'directory') return
+
+    setSelectedFile(foundFile || null)
+  }
 
   return (
-    <div className="flex border rounded-lg overflow-hidden h-[652px] not-prose mt-4">
+    <div className="flex mt-4 border rounded-lg overflow-hidden h-[652px] not-prose">
       {/* File browser sidebar */}
-      <div className="w-64 border-r bg-muted/30 overflow-y-auto">
-        <div className="border-b bg-muted/50 h-12 items-center flex px-2">
-          <h3 className="font-medium text-sm">Files</h3>
-        </div>
-        <div className="p-2">
-          <FileTree items={files} onFileSelect={setSelectedFile} />
-        </div>
+      <div className="w-64 grow-0 shrink-0 flex-0 py-2 border-r bg-muted/30 overflow-y-auto">
+        <TreeView
+          data={flattenedData}
+          aria-label="file browser"
+          className="w-full"
+          defaultExpandedIds={flattenedData.filter((n) => n.children?.length).map((n) => n.id)}
+          defaultSelectedIds={flattenedData
+            .filter((n) => n.metadata?.path === selectedFile?.path)
+            .map((n) => n.id)}
+          onNodeSelect={({ element }) => handleNodeSelect(element)}
+          nodeRenderer={({ element, isBranch, isExpanded, getNodeProps, level, isSelected }) => (
+            <TreeViewItem
+              {...getNodeProps()}
+              isExpanded={isExpanded}
+              isBranch={isBranch}
+              isSelected={isSelected}
+              level={level}
+              icon={<File strokeWidth={1.5} size={16} className="shrink-0" />}
+              xPadding={16}
+              name={element.name}
+            />
+          )}
+        />
       </div>
 
       {/* Code display area */}
-      <div className="flex-1 overflow-auto bg-muted/10" key={selectedFile?.path}>
-        {selectedFile?.content ? (
-          <div className="h-full">
-            <Tabs defaultValue="code" className="h-full gap-0">
-              <div className="h-12 flex items-center justify-between px-2">
-                <span>{selectedFilePath}</span>
-                {selectedFileDemoUrl && (
-                  <TabsList>
-                    <TabsTrigger value="preview">Preview</TabsTrigger>
-                    <TabsTrigger value="code">Code</TabsTrigger>
-                  </TabsList>
-                )}
-              </div>
-              <TabsContent
-                value="code"
-                className="rounded-none [&_figure]:rounded-none [&_figure]:h-full [&_div[data-radix-scroll-area-viewport]]:min-h-[600px]"
-              >
-                <DynamicCodeBlock
-                  lang={selectedFileExtension}
-                  code={selectedFile?.content}
-                  /* the component supports a theme prop, but it's typed badly */
-                  options={{ theme: supabaseTheme } as any}
-                />
-              </TabsContent>
-              <TabsContent value="preview" className="h-full flex-1">
-                {selectedFileDemoUrl && <BlockItemPreview title="" src={selectedFileDemoUrl} />}
-              </TabsContent>
-            </Tabs>
-          </div>
-        ) : (
-          <div className="flex items-center justify-center h-full text-muted-foreground">
-            No file selected or file content unavailable
+      {selectedFile?.content ? (
+        <CodeBlock
+          wrapperClassName="w-full"
+          className="h-full max-w-none !w-full flex-1 font-mono text-xs rounded-none border-none"
+          language={selectedFile.name.split('.').pop() as CodeBlockLang}
+        >
+          {selectedFile?.content}
+        </CodeBlock>
+      ) : (
+        <div className="flex items-center justify-center h-full text-muted-foreground">
+          <div className="flex flex-col items-center gap-2">
+            <p>No file selected or file content unavailable</p>
           </div>
-        )}
-      </div>
+        </div>
+      )}
     </div>
   )
 }
diff --git a/packages/ui/src/components/TreeView/TreeView.tsx b/packages/ui/src/components/TreeView/TreeView.tsx
index ce9c57b6a320d..0c7d72fa7f283 100644
--- a/packages/ui/src/components/TreeView/TreeView.tsx
+++ b/packages/ui/src/components/TreeView/TreeView.tsx
@@ -13,7 +13,7 @@ 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
+  '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-transparent data-[state=open]:bg-transparent', // data-[state=open]:bg-control bg state for context menu open
   {
     variants: {
       isSelected: {


Reviews:
user: login: MildTomato
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T06:22:23Z
user: login: MildTomato
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T06:22:45Z
user: login: MildTomato
body: accepted. but see comment above first
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-27T06:23:28Z
user: login: ivasilov
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-27T08:04:01Z

Review comments:
diff_hunk: @@ -13,7 +13,7 @@ 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
+  '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-transparent data-[state=open]:bg-transparent', // data-[state=open]:bg-control bg state for context menu open
path: packages/ui/src/components/TreeView/TreeView.tsx
user: login: MildTomato
body: doesn't this break the SQL editor selected state ? @SaxonF 
created_at: 2025-03-27T06:22:23Z
author_association: MEMBER
diff_hunk: @@ -13,7 +13,7 @@ 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
+  '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-transparent data-[state=open]:bg-transparent', // data-[state=open]:bg-control bg state for context menu open
path: packages/ui/src/components/TreeView/TreeView.tsx
user: login: MildTomato
body: or just confirm it doesn't
created_at: 2025-03-27T06:22:45Z
author_association: MEMBER

PR comments:
user: login: vercel[bot]
created_at: 2025-03-27T06:05:16Z
body: [vc]: #4bWftqsQIqlC0C/ldMJBpace9PamuBRe9ORD3aY1QGE=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ6b25lLXd3dy1kb3QtY29tIiwicm9vdERpcmVjdG9yeSI6ImFwcHMvd3d3IiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3N1cGFiYXNlL3pvbmUtd3d3LWRvdC1jb20vNHB0dlJieU1ScUxYek0yUkV2ZHRObjNrWEgzUyIsInByZXZpZXdVcmwiOiJ6b25lLXd3dy1kb3QtY29tLWdpdC1jaG9yZS11cGRhdGUtdHJlZS1zdXBhYmFzZS52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InpvbmUtd3d3LWRvdC1jb20tZ2l0LWNob3JlLXVwZGF0ZS10cmVlLXN1cGFiYXNlLnZlcmNlbC5hcHAifX0seyJuYW1lIjoiZG9jcyIsInJvb3REaXJlY3RvcnkiOiJhcHBzL2RvY3MiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc3VwYWJhc2UvZG9jcy84UXpYUzdIU2VmdmR5SERycTlHSzVtSGdhZnVzIiwicHJldmlld1VybCI6ImRvY3MtZ2l0LWNob3JlLXVwZGF0ZS10cmVlLXN1cGFiYXNlLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiZG9jcy1naXQtY2hvcmUtdXBkYXRlLXRyZWUtc3VwYWJhc2UudmVyY2VsLmFwcCJ9fSx7Im5hbWUiOiJzdHVkaW8tc2VsZi1ob3N0ZWQiLCJyb290RGlyZWN0b3J5IjoiYXBwcy9zdHVkaW8iLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc3VwYWJhc2Uvc3R1ZGlvLXNlbGYtaG9zdGVkL0dMVzdCaHFTU3dOMkEzd3pLa25ZZURxVFVGNDciLCJwcmV2aWV3VXJsIjoic3R1ZGlvLXNlbGYtaG9zdGVkLWdpdC1jaG9yZS11cGRhdGUtdHJlZS1zdXBhYmFzZS52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InN0dWRpby1zZWxmLWhvc3RlZC1naXQtY2hvcmUtdXBkYXRlLXRyZWUtc3VwYWJhc2UudmVyY2VsLmFwcCJ9fSx7Im5hbWUiOiJzdHVkaW8iLCJyb290RGlyZWN0b3J5IjoiYXBwcy9zdHVkaW8iLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc3VwYWJhc2Uvc3R1ZGlvLzZkM2J5eno2VGlNSFh1N2NkN2JXejRMWmpNblYiLCJwcmV2aWV3VXJsIjoic3R1ZGlvLWdpdC1jaG9yZS11cGRhdGUtdHJlZS1zdXBhYmFzZS52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IklHTk9SRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn19LHsibmFtZSI6InN0dWRpby1zdGFnaW5nIiwicm9vdERpcmVjdG9yeSI6ImFwcHMvc3R1ZGlvIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3N1cGFiYXNlL3N0dWRpby1zdGFnaW5nLzY2Z0ZLQ3p2ZUtaSjc3M2dGMW8zRWsxMnVXN2IiLCJwcmV2aWV3VXJsIjoic3R1ZGlvLXN0YWdpbmctZ2l0LWNob3JlLXVwZGF0ZS10cmVlLXN1cGFiYXNlLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoic3R1ZGlvLXN0YWdpbmctZ2l0LWNob3JlLXVwZGF0ZS10cmVlLXN1cGFiYXNlLnZlcmNlbC5hcHAifX0seyJuYW1lIjoiZGVzaWduLXN5c3RlbSIsInJvb3REaXJlY3RvcnkiOiJhcHBzL2Rlc2lnbi1zeXN0ZW0iLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc3VwYWJhc2UvZGVzaWduLXN5c3RlbS9EYTVzNG11QXZSYWlLcHRHaU53TDFmRmhoUFJoIiwicHJldmlld1VybCI6ImRlc2lnbi1zeXN0ZW0tZ2l0LWNob3JlLXVwZGF0ZS10cmVlLXN1cGFiYXNlLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiZGVzaWduLXN5c3RlbS1naXQtY2hvcmUtdXBkYXRlLXRyZWUtc3VwYWJhc2UudmVyY2VsLmFwcCJ9fSx7Im5hbWUiOiJ1aS1saWJyYXJ5Iiwicm9vdERpcmVjdG9yeSI6ImFwcHMvdWktbGlicmFyeSIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9zdXBhYmFzZS91aS1saWJyYXJ5LzhSSjZhUkpONmNCTmJVSmNuSHZKaUVZaTVrdEQiLCJwcmV2aWV3VXJsIjoidWktbGlicmFyeS1naXQtY2hvcmUtdXBkYXRlLXRyZWUtc3VwYWJhc2UudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJ1aS1saWJyYXJ5LWdpdC1jaG9yZS11cGRhdGUtdHJlZS1zdXBhYmFzZS52ZXJjZWwuYXBwIn19XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **design-system** | ✅ Ready ([Inspect](https://vercel.com/supabase/design-system/Da5s4muAvRaiKptGiNwL1fFhhPRh)) | [Visit Preview](https://design-system-git-chore-update-tree-supabase.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/design-system-git-chore-update-tree-supabase.vercel.app?via=pr-comment-feedback-link) | Mar 27, 2025 6:15am |
| **docs** | ✅ Ready ([Inspect](https://vercel.com/supabase/docs/8QzXS7HSefvdyHDrq9GK5mHgafus)) | [Visit Preview](https://docs-git-chore-update-tree-supabase.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/docs-git-chore-update-tree-supabase.vercel.app?via=pr-comment-feedback-link) | Mar 27, 2025 6:15am |
| **studio-self-hosted** | ✅ Ready ([Inspect](https://vercel.com/supabase/studio-self-hosted/GLW7BhqSSwN2A3wzKknYeDqTUF47)) | [Visit Preview](https://studio-self-hosted-git-chore-update-tree-supabase.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/studio-self-hosted-git-chore-update-tree-supabase.vercel.app?via=pr-comment-feedback-link) | Mar 27, 2025 6:15am |
| **studio-staging** | ✅ Ready ([Inspect](https://vercel.com/supabase/studio-staging/66gFKCzveKZJ773gF1o3Ek12uW7b)) | [Visit Preview](https://studio-staging-git-chore-update-tree-supabase.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/studio-staging-git-chore-update-tree-supabase.vercel.app?via=pr-comment-feedback-link) | Mar 27, 2025 6:15am |
| **ui-library** | ✅ Ready ([Inspect](https://vercel.com/supabase/ui-library/8RJ6aRJN6cBNbUJcnHvJiEYi5ktD)) | [Visit Preview](https://ui-library-git-chore-update-tree-supabase.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/ui-library-git-chore-update-tree-supabase.vercel.app?via=pr-comment-feedback-link) | Mar 27, 2025 6:15am |
| **zone-www-dot-com** | ✅ Ready ([Inspect](https://vercel.com/supabase/zone-www-dot-com/4ptvRbyMRqLXzM2REvdtNn3kXH3S)) | [Visit Preview](https://zone-www-dot-com-git-chore-update-tree-supabase.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/zone-www-dot-com-git-chore-update-tree-supabase.vercel.app?via=pr-comment-feedback-link) | Mar 27, 2025 6:15am |

<details><summary>1 Skipped Deployment</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **studio** | ⬜️ Ignored ([Inspect](https://vercel.com/supabase/studio/6d3byzz6TiMHXu7cd7bWz4LZjMnV)) | [Visit Preview](https://studio-git-chore-update-tree-supabase.vercel.app) |  | Mar 27, 2025 6:15am |
</details>








author_association: NONE
user: login: supabase[bot]
created_at: 2025-03-27T06:05:17Z
body: [supa]:xguihxuzqibwxjnimxev
This pull request has been ignored for the connected project `xguihxuzqibwxjnimxev` because there are no changes detected in `supabase` directory. You can change this behaviour in [Project Integrations Settings ↗︎](https://supabase.com/dashboard/project/xguihxuzqibwxjnimxev/settings/integrations).

<hr/>

Preview Branches by Supabase.
Learn more about [Supabase Branching ↗︎](https://supabase.com/docs/guides/platform/branching).
author_association: NONE