diff --git a/apps/studio/TANSTACK_MIGRATION.md b/apps/studio/TANSTACK_MIGRATION.md index 2905911c698..548f5f6b8e2 100644 --- a/apps/studio/TANSTACK_MIGRATION.md +++ b/apps/studio/TANSTACK_MIGRATION.md @@ -77,7 +77,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp - [x] `routes/project/$ref/storage.tsx` — StorageLayout + StorageBucketsLayout (reads `storageLayoutTitle`, optional `skipStorageBucketsLayout`, `storageBucketsLayoutTitle`, `storageBucketsLayoutHideSubtitle` from leaf `staticData`). **Delta vs plan:** the shell wraps in BOTH StorageLayout and StorageBucketsLayout by default — every storage page except bucket-detail pages uses both. Bucket-detail pages set `skipStorageBucketsLayout: true`. `/storage/s3` uses `storageBucketsLayout{Title,HideSubtitle}` to override the inner header. - [x] `routes/project/$ref/realtime.tsx` — RealtimeLayout (reads `realtimeLayoutTitle` from leaf `staticData`) - [ ] `routes/project/$ref/functions.tsx` — EdgeFunctionsLayout -- [ ] `routes/project/$ref/branches.tsx` — BranchLayout + PageLayout +- [x] `routes/project/$ref/branches.tsx` — BranchLayout only. **Delta vs plan:** the per-page `PageLayout` (with different titles + primary/secondary actions) stays in each leaf. Hoisted `BranchesPageWrapper` and `MergeRequestsPageWrapper` to top-level exports in their respective `pages/...` files so the route files can import + re-use the same wrapping. - [ ] `routes/project/$ref/logs.tsx` — LogsLayout - [ ] `routes/project/$ref/observability.tsx` — ObservabilityLayout - [ ] `routes/project/$ref/advisors.tsx` — AdvisorsLayout @@ -226,8 +226,8 @@ These are the layout-only TanStack files. Most hold a single product layout comp ### Project shell — `/branches/*` -- [ ] `routes/project/$ref/branches/index.tsx` ← `pages/project/[ref]/branches/index.tsx` -- [ ] `routes/project/$ref/branches/merge-requests.tsx` ← `pages/project/[ref]/branches/merge-requests.tsx` +- [x] A `routes/project/$ref/branches/index.tsx` ← `pages/project/[ref]/branches/index.tsx` (route wraps in exported `BranchesPageWrapper` to preserve the page's `PageLayout` + Create-branch action) +- [x] A `routes/project/$ref/branches/merge-requests.tsx` ← `pages/project/[ref]/branches/merge-requests.tsx` (route wraps in exported `MergeRequestsPageWrapper`) ### Project shell — `/logs/*` diff --git a/apps/studio/pages/project/[ref]/branches/index.tsx b/apps/studio/pages/project/[ref]/branches/index.tsx index e0994a0fafa..fe98e0a0a24 100644 --- a/apps/studio/pages/project/[ref]/branches/index.tsx +++ b/apps/studio/pages/project/[ref]/branches/index.tsx @@ -3,7 +3,7 @@ import { useParams } from 'common' import { partition } from 'lodash' import { MessageCircle } from 'lucide-react' import { useRouter } from 'next/router' -import { useState } from 'react' +import { useState, type PropsWithChildren } from 'react' import { toast } from 'sonner' import { Button } from 'ui' @@ -193,69 +193,70 @@ const BranchesPage: NextPageWithLayout = () => { ) } -BranchesPage.getLayout = (page) => { - const BranchesPageWrapper = () => { - const snap = useAppStateSnapshot() - const { can: canCreateBranches } = useAsyncCheckPermissions( - PermissionAction.CREATE, - 'preview_branches', - { - resource: { is_default: false }, - } - ) +// Hoisted out of `getLayout` so the TanStack route can import it +// directly. Same shape and identical body as before — accepts the page +// content as `children` instead of capturing it from a closure. +export const BranchesPageWrapper = ({ children }: PropsWithChildren) => { + const snap = useAppStateSnapshot() + const { can: canCreateBranches } = useAsyncCheckPermissions( + PermissionAction.CREATE, + 'preview_branches', + { + resource: { is_default: false }, + } + ) - const primaryActions = ( - snap.setShowCreateBranchModal(true)} - tooltip={{ - content: { - side: 'bottom', - text: !canCreateBranches - ? 'You need additional permissions to create branches' - : undefined, - }, - }} - > - Create branch - - ) + const primaryActions = ( + snap.setShowCreateBranchModal(true)} + tooltip={{ + content: { + side: 'bottom', + text: !canCreateBranches + ? 'You need additional permissions to create branches' + : undefined, + }, + }} + > + Create branch + + ) - const secondaryActions = ( -
- - -
- ) - - return ( - - {page} - - ) - } + const secondaryActions = ( +
+ + +
+ ) return ( - - - - - + + {children} + ) } +BranchesPage.getLayout = (page) => ( + + + {page} + + +) + export default BranchesPage diff --git a/apps/studio/pages/project/[ref]/branches/merge-requests.tsx b/apps/studio/pages/project/[ref]/branches/merge-requests.tsx index 509fc9f9276..213dd05ec95 100644 --- a/apps/studio/pages/project/[ref]/branches/merge-requests.tsx +++ b/apps/studio/pages/project/[ref]/branches/merge-requests.tsx @@ -309,7 +309,7 @@ const MergeRequestsPage: NextPageWithLayout = () => { ) } -const MergeRequestsPageWrapper = ({ children }: PropsWithChildren<{}>) => { +export const MergeRequestsPageWrapper = ({ children }: PropsWithChildren<{}>) => { const router = useRouter() const { ref } = useParams() const { data: project } = useSelectedProjectQuery() diff --git a/apps/studio/routeTree.gen.ts b/apps/studio/routeTree.gen.ts index 527b2e0c053..107e0ff6a80 100644 --- a/apps/studio/routeTree.gen.ts +++ b/apps/studio/routeTree.gen.ts @@ -49,6 +49,7 @@ import { Route as ProjectRefSqlRouteImport } from './routes/project/$ref/sql' import { Route as ProjectRefRealtimeRouteImport } from './routes/project/$ref/realtime' import { Route as ProjectRefEditorRouteImport } from './routes/project/$ref/editor' import { Route as ProjectRefDatabaseRouteImport } from './routes/project/$ref/database' +import { Route as ProjectRefBranchesRouteImport } from './routes/project/$ref/branches' import { Route as ProjectRefAuthRouteImport } from './routes/project/$ref/auth' import { Route as ApiIntegrationsStripeSyncRouteImport } from './routes/api/integrations/stripe-sync' import { Route as ApiEdgeFunctionsTestRouteImport } from './routes/api/edge-functions/test' @@ -60,6 +61,7 @@ import { Route as AppAccountMeRouteImport } from './routes/_app/account/me' import { Route as AppAccountAuditRouteImport } from './routes/_app/account/audit' import { Route as ProjectRefSqlIndexRouteImport } from './routes/project/$ref/sql/index' import { Route as ProjectRefEditorIndexRouteImport } from './routes/project/$ref/editor/index' +import { Route as ProjectRefBranchesIndexRouteImport } from './routes/project/$ref/branches/index' import { Route as ApiPlatformProjectsIndexRouteImport } from './routes/api/platform/projects/index' import { Route as ApiPlatformProfileIndexRouteImport } from './routes/api/platform/profile/index' import { Route as ApiPlatformOrganizationsIndexRouteImport } from './routes/api/platform/organizations/index' @@ -84,6 +86,7 @@ import { Route as ProjectRefDatabaseIndexesRouteImport } from './routes/project/ import { Route as ProjectRefDatabaseFunctionsRouteImport } from './routes/project/$ref/database/functions' import { Route as ProjectRefDatabaseExtensionsRouteImport } from './routes/project/$ref/database/extensions' import { Route as ProjectRefDatabaseColumnPrivilegesRouteImport } from './routes/project/$ref/database/column-privileges' +import { Route as ProjectRefBranchesMergeRequestsRouteImport } from './routes/project/$ref/branches/merge-requests' import { Route as ProjectRefAuthUsersRouteImport } from './routes/project/$ref/auth/users' import { Route as ProjectRefAuthUrlConfigurationRouteImport } from './routes/project/$ref/auth/url-configuration' import { Route as ProjectRefAuthThirdPartyRouteImport } from './routes/project/$ref/auth/third-party' @@ -413,6 +416,11 @@ const ProjectRefDatabaseRoute = ProjectRefDatabaseRouteImport.update({ path: '/database', getParentRoute: () => ProjectRefRoute, } as any) +const ProjectRefBranchesRoute = ProjectRefBranchesRouteImport.update({ + id: '/branches', + path: '/branches', + getParentRoute: () => ProjectRefRoute, +} as any) const ProjectRefAuthRoute = ProjectRefAuthRouteImport.update({ id: '/auth', path: '/auth', @@ -469,6 +477,11 @@ const ProjectRefEditorIndexRoute = ProjectRefEditorIndexRouteImport.update({ path: '/', getParentRoute: () => ProjectRefEditorRoute, } as any) +const ProjectRefBranchesIndexRoute = ProjectRefBranchesIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => ProjectRefBranchesRoute, +} as any) const ApiPlatformProjectsIndexRoute = ApiPlatformProjectsIndexRouteImport.update({ id: '/api/platform/projects/', @@ -603,6 +616,12 @@ const ProjectRefDatabaseColumnPrivilegesRoute = path: '/column-privileges', getParentRoute: () => ProjectRefDatabaseRoute, } as any) +const ProjectRefBranchesMergeRequestsRoute = + ProjectRefBranchesMergeRequestsRouteImport.update({ + id: '/merge-requests', + path: '/merge-requests', + getParentRoute: () => ProjectRefBranchesRoute, + } as any) const ProjectRefAuthUsersRoute = ProjectRefAuthUsersRouteImport.update({ id: '/users', path: '/users', @@ -1370,6 +1389,7 @@ export interface FileRoutesByFullPath { '/api/edge-functions/test': typeof ApiEdgeFunctionsTestRoute '/api/integrations/stripe-sync': typeof ApiIntegrationsStripeSyncRoute '/project/$ref/auth': typeof ProjectRefAuthRouteWithChildren + '/project/$ref/branches': typeof ProjectRefBranchesRouteWithChildren '/project/$ref/database': typeof ProjectRefDatabaseRouteWithChildren '/project/$ref/editor': typeof ProjectRefEditorRouteWithChildren '/project/$ref/realtime': typeof ProjectRefRealtimeRouteWithChildren @@ -1418,6 +1438,7 @@ export interface FileRoutesByFullPath { '/project/$ref/auth/third-party': typeof ProjectRefAuthThirdPartyRoute '/project/$ref/auth/url-configuration': typeof ProjectRefAuthUrlConfigurationRoute '/project/$ref/auth/users': typeof ProjectRefAuthUsersRoute + '/project/$ref/branches/merge-requests': typeof ProjectRefBranchesMergeRequestsRoute '/project/$ref/database/column-privileges': typeof ProjectRefDatabaseColumnPrivilegesRoute '/project/$ref/database/extensions': typeof ProjectRefDatabaseExtensionsRoute '/project/$ref/database/functions': typeof ProjectRefDatabaseFunctionsRoute @@ -1442,6 +1463,7 @@ export interface FileRoutesByFullPath { '/api/platform/organizations/': typeof ApiPlatformOrganizationsIndexRoute '/api/platform/profile/': typeof ApiPlatformProfileIndexRoute '/api/platform/projects/': typeof ApiPlatformProjectsIndexRoute + '/project/$ref/branches/': typeof ProjectRefBranchesIndexRoute '/project/$ref/editor/': typeof ProjectRefEditorIndexRoute '/project/$ref/sql/': typeof ProjectRefSqlIndexRoute '/org/$slug/webhooks/$endpointId': typeof AppOrgSlugWebhooksEndpointIdRoute @@ -1616,6 +1638,7 @@ export interface FileRoutesByTo { '/project/$ref/auth/third-party': typeof ProjectRefAuthThirdPartyRoute '/project/$ref/auth/url-configuration': typeof ProjectRefAuthUrlConfigurationRoute '/project/$ref/auth/users': typeof ProjectRefAuthUsersRoute + '/project/$ref/branches/merge-requests': typeof ProjectRefBranchesMergeRequestsRoute '/project/$ref/database/column-privileges': typeof ProjectRefDatabaseColumnPrivilegesRoute '/project/$ref/database/extensions': typeof ProjectRefDatabaseExtensionsRoute '/project/$ref/database/functions': typeof ProjectRefDatabaseFunctionsRoute @@ -1639,6 +1662,7 @@ export interface FileRoutesByTo { '/api/platform/organizations': typeof ApiPlatformOrganizationsIndexRoute '/api/platform/profile': typeof ApiPlatformProfileIndexRoute '/api/platform/projects': typeof ApiPlatformProjectsIndexRoute + '/project/$ref/branches': typeof ProjectRefBranchesIndexRoute '/project/$ref/editor': typeof ProjectRefEditorIndexRoute '/project/$ref/sql': typeof ProjectRefSqlIndexRoute '/org/$slug/webhooks/$endpointId': typeof AppOrgSlugWebhooksEndpointIdRoute @@ -1772,6 +1796,7 @@ export interface FileRoutesById { '/api/edge-functions/test': typeof ApiEdgeFunctionsTestRoute '/api/integrations/stripe-sync': typeof ApiIntegrationsStripeSyncRoute '/project/$ref/auth': typeof ProjectRefAuthRouteWithChildren + '/project/$ref/branches': typeof ProjectRefBranchesRouteWithChildren '/project/$ref/database': typeof ProjectRefDatabaseRouteWithChildren '/project/$ref/editor': typeof ProjectRefEditorRouteWithChildren '/project/$ref/realtime': typeof ProjectRefRealtimeRouteWithChildren @@ -1820,6 +1845,7 @@ export interface FileRoutesById { '/project/$ref/auth/third-party': typeof ProjectRefAuthThirdPartyRoute '/project/$ref/auth/url-configuration': typeof ProjectRefAuthUrlConfigurationRoute '/project/$ref/auth/users': typeof ProjectRefAuthUsersRoute + '/project/$ref/branches/merge-requests': typeof ProjectRefBranchesMergeRequestsRoute '/project/$ref/database/column-privileges': typeof ProjectRefDatabaseColumnPrivilegesRoute '/project/$ref/database/extensions': typeof ProjectRefDatabaseExtensionsRoute '/project/$ref/database/functions': typeof ProjectRefDatabaseFunctionsRoute @@ -1844,6 +1870,7 @@ export interface FileRoutesById { '/api/platform/organizations/': typeof ApiPlatformOrganizationsIndexRoute '/api/platform/profile/': typeof ApiPlatformProfileIndexRoute '/api/platform/projects/': typeof ApiPlatformProjectsIndexRoute + '/project/$ref/branches/': typeof ProjectRefBranchesIndexRoute '/project/$ref/editor/': typeof ProjectRefEditorIndexRoute '/project/$ref/sql/': typeof ProjectRefSqlIndexRoute '/_app/org/$slug/webhooks/$endpointId': typeof AppOrgSlugWebhooksEndpointIdRoute @@ -1976,6 +2003,7 @@ export interface FileRouteTypes { | '/api/edge-functions/test' | '/api/integrations/stripe-sync' | '/project/$ref/auth' + | '/project/$ref/branches' | '/project/$ref/database' | '/project/$ref/editor' | '/project/$ref/realtime' @@ -2024,6 +2052,7 @@ export interface FileRouteTypes { | '/project/$ref/auth/third-party' | '/project/$ref/auth/url-configuration' | '/project/$ref/auth/users' + | '/project/$ref/branches/merge-requests' | '/project/$ref/database/column-privileges' | '/project/$ref/database/extensions' | '/project/$ref/database/functions' @@ -2048,6 +2077,7 @@ export interface FileRouteTypes { | '/api/platform/organizations/' | '/api/platform/profile/' | '/api/platform/projects/' + | '/project/$ref/branches/' | '/project/$ref/editor/' | '/project/$ref/sql/' | '/org/$slug/webhooks/$endpointId' @@ -2222,6 +2252,7 @@ export interface FileRouteTypes { | '/project/$ref/auth/third-party' | '/project/$ref/auth/url-configuration' | '/project/$ref/auth/users' + | '/project/$ref/branches/merge-requests' | '/project/$ref/database/column-privileges' | '/project/$ref/database/extensions' | '/project/$ref/database/functions' @@ -2245,6 +2276,7 @@ export interface FileRouteTypes { | '/api/platform/organizations' | '/api/platform/profile' | '/api/platform/projects' + | '/project/$ref/branches' | '/project/$ref/editor' | '/project/$ref/sql' | '/org/$slug/webhooks/$endpointId' @@ -2377,6 +2409,7 @@ export interface FileRouteTypes { | '/api/edge-functions/test' | '/api/integrations/stripe-sync' | '/project/$ref/auth' + | '/project/$ref/branches' | '/project/$ref/database' | '/project/$ref/editor' | '/project/$ref/realtime' @@ -2425,6 +2458,7 @@ export interface FileRouteTypes { | '/project/$ref/auth/third-party' | '/project/$ref/auth/url-configuration' | '/project/$ref/auth/users' + | '/project/$ref/branches/merge-requests' | '/project/$ref/database/column-privileges' | '/project/$ref/database/extensions' | '/project/$ref/database/functions' @@ -2449,6 +2483,7 @@ export interface FileRouteTypes { | '/api/platform/organizations/' | '/api/platform/profile/' | '/api/platform/projects/' + | '/project/$ref/branches/' | '/project/$ref/editor/' | '/project/$ref/sql/' | '/_app/org/$slug/webhooks/$endpointId' @@ -2925,6 +2960,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ProjectRefDatabaseRouteImport parentRoute: typeof ProjectRefRoute } + '/project/$ref/branches': { + id: '/project/$ref/branches' + path: '/branches' + fullPath: '/project/$ref/branches' + preLoaderRoute: typeof ProjectRefBranchesRouteImport + parentRoute: typeof ProjectRefRoute + } '/project/$ref/auth': { id: '/project/$ref/auth' path: '/auth' @@ -3002,6 +3044,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ProjectRefEditorIndexRouteImport parentRoute: typeof ProjectRefEditorRoute } + '/project/$ref/branches/': { + id: '/project/$ref/branches/' + path: '/' + fullPath: '/project/$ref/branches/' + preLoaderRoute: typeof ProjectRefBranchesIndexRouteImport + parentRoute: typeof ProjectRefBranchesRoute + } '/api/platform/projects/': { id: '/api/platform/projects/' path: '/api/platform/projects' @@ -3170,6 +3219,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ProjectRefDatabaseColumnPrivilegesRouteImport parentRoute: typeof ProjectRefDatabaseRoute } + '/project/$ref/branches/merge-requests': { + id: '/project/$ref/branches/merge-requests' + path: '/merge-requests' + fullPath: '/project/$ref/branches/merge-requests' + preLoaderRoute: typeof ProjectRefBranchesMergeRequestsRouteImport + parentRoute: typeof ProjectRefBranchesRoute + } '/project/$ref/auth/users': { id: '/project/$ref/auth/users' path: '/users' @@ -4212,6 +4268,19 @@ const ProjectRefAuthRouteWithChildren = ProjectRefAuthRoute._addFileChildren( ProjectRefAuthRouteChildren, ) +interface ProjectRefBranchesRouteChildren { + ProjectRefBranchesMergeRequestsRoute: typeof ProjectRefBranchesMergeRequestsRoute + ProjectRefBranchesIndexRoute: typeof ProjectRefBranchesIndexRoute +} + +const ProjectRefBranchesRouteChildren: ProjectRefBranchesRouteChildren = { + ProjectRefBranchesMergeRequestsRoute: ProjectRefBranchesMergeRequestsRoute, + ProjectRefBranchesIndexRoute: ProjectRefBranchesIndexRoute, +} + +const ProjectRefBranchesRouteWithChildren = + ProjectRefBranchesRoute._addFileChildren(ProjectRefBranchesRouteChildren) + interface ProjectRefDatabaseTriggersRouteChildren { ProjectRefDatabaseTriggersDataRoute: typeof ProjectRefDatabaseTriggersDataRoute ProjectRefDatabaseTriggersEventRoute: typeof ProjectRefDatabaseTriggersEventRoute @@ -4366,6 +4435,7 @@ const ProjectRefStorageRouteWithChildren = interface ProjectRefRouteChildren { ProjectRefAuthRoute: typeof ProjectRefAuthRouteWithChildren + ProjectRefBranchesRoute: typeof ProjectRefBranchesRouteWithChildren ProjectRefDatabaseRoute: typeof ProjectRefDatabaseRouteWithChildren ProjectRefEditorRoute: typeof ProjectRefEditorRouteWithChildren ProjectRefRealtimeRoute: typeof ProjectRefRealtimeRouteWithChildren @@ -4376,6 +4446,7 @@ interface ProjectRefRouteChildren { const ProjectRefRouteChildren: ProjectRefRouteChildren = { ProjectRefAuthRoute: ProjectRefAuthRouteWithChildren, + ProjectRefBranchesRoute: ProjectRefBranchesRouteWithChildren, ProjectRefDatabaseRoute: ProjectRefDatabaseRouteWithChildren, ProjectRefEditorRoute: ProjectRefEditorRouteWithChildren, ProjectRefRealtimeRoute: ProjectRefRealtimeRouteWithChildren, diff --git a/apps/studio/routes/project/$ref/branches.tsx b/apps/studio/routes/project/$ref/branches.tsx new file mode 100644 index 00000000000..5442d04064b --- /dev/null +++ b/apps/studio/routes/project/$ref/branches.tsx @@ -0,0 +1,20 @@ +import { createFileRoute, Outlet } from '@tanstack/react-router' + +import BranchLayout from '@/components/layouts/BranchLayout/BranchLayout' + +export const Route = createFileRoute('/project/$ref/branches')({ + component: BranchesShell, +}) + +// BranchLayout sibling-file shell. Each leaf renders its own per-page +// PageLayout (different titles + primary/secondary actions); we keep +// that wrapping in the leaf route files via the page-side +// `*PageWrapper` exports rather than trying to share PageLayout in the +// shell — the shell deliberately stops at BranchLayout. +function BranchesShell() { + return ( + + + + ) +} diff --git a/apps/studio/routes/project/$ref/branches/index.tsx b/apps/studio/routes/project/$ref/branches/index.tsx new file mode 100644 index 00000000000..810d1eaa299 --- /dev/null +++ b/apps/studio/routes/project/$ref/branches/index.tsx @@ -0,0 +1,15 @@ +import { createFileRoute } from '@tanstack/react-router' + +import BranchesPage, { BranchesPageWrapper } from '@/pages/project/[ref]/branches/index' + +export const Route = createFileRoute('/project/$ref/branches/')({ + component: BranchesIndexRoute, +}) + +function BranchesIndexRoute() { + return ( + + + + ) +} diff --git a/apps/studio/routes/project/$ref/branches/merge-requests.tsx b/apps/studio/routes/project/$ref/branches/merge-requests.tsx new file mode 100644 index 00000000000..e5367b92a22 --- /dev/null +++ b/apps/studio/routes/project/$ref/branches/merge-requests.tsx @@ -0,0 +1,17 @@ +import { createFileRoute } from '@tanstack/react-router' + +import MergeRequestsPage, { + MergeRequestsPageWrapper, +} from '@/pages/project/[ref]/branches/merge-requests' + +export const Route = createFileRoute('/project/$ref/branches/merge-requests')({ + component: MergeRequestsRoute, +}) + +function MergeRequestsRoute() { + return ( + + + + ) +}