+
{title}
This view will show real data from the API. Placeholder for the v2 prototype.
diff --git a/apps/studio/components/v2/views/V2TableDetailView.tsx b/apps/studio/components/v2/views/V2TableDetailView.tsx
index dcb884b82a3..428b5c42a26 100644
--- a/apps/studio/components/v2/views/V2TableDetailView.tsx
+++ b/apps/studio/components/v2/views/V2TableDetailView.tsx
@@ -1,17 +1,20 @@
'use client'
+import { useEffect } from 'react'
import { useParams } from 'next/navigation'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { TableGridEditor } from 'components/interfaces/TableGridEditor/TableGridEditor'
import { useV2Params } from '@/app/v2/V2ParamsContext'
+import { useV2DashboardStore } from '@/stores/v2-dashboard'
import { useProjectDetailQuery } from 'data/projects/project-detail-query'
import { useTableEditorQuery } from 'data/table-editor/table-editor-query'
export function V2TableDetailView({ subTab }: { subTab: string }) {
const params = useParams()
const { projectRef } = useV2Params()
+ const openDataTab = useV2DashboardStore((s) => s.openDataTab)
const tableId = params?.tableId as string
const id = tableId ? Number(tableId) : undefined
@@ -28,6 +31,19 @@ export function V2TableDetailView({ subTab }: { subTab: string }) {
{ enabled: Boolean(projectRef) && typeof id === 'number' && !Number.isNaN(id) }
)
+ // Register this detail tab once we know the table name
+ useEffect(() => {
+ if (!table || !projectRef || !tableId) return
+ openDataTab({
+ id: `tables-${tableId}`,
+ label: table.name,
+ type: 'detail',
+ category: 'tables',
+ domain: 'db',
+ path: `/dashboard/v2/project/${projectRef}/data/tables/${tableId}/data`,
+ })
+ }, [table, projectRef, tableId, openDataTab])
+
if (isError) {
return (
Failed to load table.
diff --git a/apps/studio/components/v2/views/V2TablesList.tsx b/apps/studio/components/v2/views/V2TablesList.tsx
index be4f2091907..fca13e850ab 100644
--- a/apps/studio/components/v2/views/V2TablesList.tsx
+++ b/apps/studio/components/v2/views/V2TablesList.tsx
@@ -2,7 +2,7 @@
import type { PostgresTable } from '@supabase/postgres-meta'
import Link from 'next/link'
-import { useMemo, useState } from 'react'
+import { useCallback, useEffect, useMemo, useState } from 'react'
import {
Select_Shadcn_,
SelectContent_Shadcn_,
@@ -23,8 +23,21 @@ import { useTablesQuery } from 'data/tables/tables-query'
export function V2TablesList() {
const { projectRef } = useV2Params()
- const addDetailTab = useV2DashboardStore((s) => s.addDetailTab)
+ const openDataTab = useV2DashboardStore((s) => s.openDataTab)
const [schema, setSchema] = useState('public')
+
+ // Register this list as an open tab
+ useEffect(() => {
+ if (!projectRef) return
+ openDataTab({
+ id: 'tables',
+ label: 'Tables',
+ type: 'list',
+ category: 'tables',
+ domain: 'db',
+ path: `/dashboard/v2/project/${projectRef}/data/tables`,
+ })
+ }, [projectRef, openDataTab])
const [search, setSearch] = useState('')
const { data: project, isPending: isProjectPending } = useProjectDetailQuery(
@@ -57,10 +70,20 @@ export function V2TablesList() {
const base = projectRef ? `/dashboard/v2/project/${projectRef}` : ''
- const openTable = (table: PostgresTable) => {
- const path = `${base}/data/tables/${table.id}`
- addDetailTab({ id: String(table.id), label: table.name, path })
- }
+ const openTable = useCallback(
+ (table: PostgresTable) => {
+ const path = `${base}/data/tables/${table.id}/data`
+ openDataTab({
+ id: `tables-${table.id}`,
+ label: table.name,
+ type: 'detail',
+ category: 'tables',
+ domain: 'db',
+ path,
+ })
+ },
+ [base, openDataTab]
+ )
const columns = useMemo
[]>(
() => [
@@ -71,7 +94,7 @@ export function V2TablesList() {
sortValue: (row) => row.name,
renderCell: (row) => (
openTable(row)}
className="truncate font-mono text-foreground hover:underline"
>
@@ -101,7 +124,7 @@ export function V2TablesList() {
),
},
],
- [base, schema]
+ [base, schema, openTable]
)
if (isTablesError) {
@@ -147,7 +170,7 @@ export function V2TablesList() {
emptyMessage={
search.trim() ? 'No tables match your filter.' : 'No tables in this schema.'
}
- onRowDoubleClick={(row) => openTable(row)}
+ onRowDoubleClick={openTable}
/>
)
diff --git a/apps/studio/hooks/misc/useTableEditorFiltersSort.ts b/apps/studio/hooks/misc/useTableEditorFiltersSort.ts
index 5851a8e4d90..57e4ff33636 100644
--- a/apps/studio/hooks/misc/useTableEditorFiltersSort.ts
+++ b/apps/studio/hooks/misc/useTableEditorFiltersSort.ts
@@ -1,20 +1,26 @@
-import { useRouter } from 'next/router'
+import { useRouter as useCompatRouter } from 'next/compat/router'
+import { usePathname, useRouter as useAppRouter, useSearchParams } from 'next/navigation'
import { useCallback, useMemo } from 'react'
export const useTableEditorFiltersSort = () => {
- const router = useRouter()
+ // Returns the pages-router instance, or null when running in the app router.
+ const pagesRouter = useCompatRouter()
+ // Always call navigation hooks unconditionally (Rules of Hooks).
+ const appRouter = useAppRouter()
+ const appSearchParams = useSearchParams()
+ const pathname = usePathname()
+
+ const isAppRouter = pagesRouter === null
const urlParams = useMemo(() => {
- return new URLSearchParams(router.asPath.split('?')[1])
- }, [router.asPath])
+ if (isAppRouter) {
+ return appSearchParams ?? new URLSearchParams()
+ }
+ return new URLSearchParams(pagesRouter?.asPath.split('?')[1])
+ }, [isAppRouter, pagesRouter, appSearchParams])
- const filters = useMemo(() => {
- return urlParams.getAll('filter')
- }, [urlParams])
-
- const sorts = useMemo(() => {
- return urlParams.getAll('sort')
- }, [urlParams])
+ const filters = useMemo(() => urlParams.getAll('filter'), [urlParams])
+ const sorts = useMemo(() => urlParams.getAll('sort'), [urlParams])
type SetParamsArgs = {
filter?: string[]
@@ -26,28 +32,37 @@ export const useTableEditorFiltersSort = () => {
const prevParams = { filter: filters, sort: sorts }
const newParams = fn(prevParams)
- const hasFilter = newParams.filter !== undefined
- const hasSort = newParams.sort !== undefined
-
- router.push(
- {
- query: {
- ...router.query,
- ...(hasFilter ? { filter: newParams.filter } : {}),
- ...(hasSort ? { sort: newParams.sort } : {}),
+ if (isAppRouter) {
+ // Build a new search-params string and replace the URL shallowly.
+ const next = new URLSearchParams(appSearchParams?.toString() ?? '')
+ if (newParams.filter !== undefined) {
+ next.delete('filter')
+ for (const f of newParams.filter) next.append('filter', f)
+ }
+ if (newParams.sort !== undefined) {
+ next.delete('sort')
+ for (const s of newParams.sort) next.append('sort', s)
+ }
+ const qs = next.toString()
+ appRouter.replace(`${pathname}${qs ? `?${qs}` : ''}`, { scroll: false })
+ } else {
+ const hasFilter = newParams.filter !== undefined
+ const hasSort = newParams.sort !== undefined
+ pagesRouter?.push(
+ {
+ query: {
+ ...pagesRouter.query,
+ ...(hasFilter ? { filter: newParams.filter } : {}),
+ ...(hasSort ? { sort: newParams.sort } : {}),
+ },
},
- },
- undefined,
- { shallow: true }
- )
+ undefined,
+ { shallow: true }
+ )
+ }
},
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [filters, sorts]
+ [isAppRouter, pagesRouter, appRouter, appSearchParams, pathname, filters, sorts]
)
- return {
- filters,
- sorts,
- setParams,
- }
+ return { filters, sorts, setParams }
}
diff --git a/apps/studio/stores/v2-dashboard.tsx b/apps/studio/stores/v2-dashboard.tsx
index 98714757695..547044dc489 100644
--- a/apps/studio/stores/v2-dashboard.tsx
+++ b/apps/studio/stores/v2-dashboard.tsx
@@ -11,10 +11,15 @@ import {
} from 'react'
export type RightPanelType = 'chat' | 'sql' | 'adv' | null
+export type DataTabDomain = 'db' | 'auth' | 'st' | 'fn' | 'rt'
+export type DataTabType = 'list' | 'detail'
-export interface DetailTab {
+export interface DataTab {
id: string
label: string
+ type: DataTabType
+ category: string
+ domain: DataTabDomain
path: string
}
@@ -22,18 +27,47 @@ export interface RecentItem {
id: string
label: string
path: string
- type: string
+ category: string
+ domain: DataTabDomain
timestamp: number
}
+// Exported so components can reference canonical mappings
+export const CATEGORY_DOMAIN: Record