fix: prevent crash on _ page when user has no organizations (#38640)

* fix: prevent crash on _ page when user has no organizations

* Nit refactors to make wild card pages consistent + fix loading state of org wildcard route

* Smol fix

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
CharisandJoshen Lim authored and GitHub committed 2025-09-12 13:50:02 +08:00
1 parent 271ee3af6d
commit 229f5560a6
4 files changed
+123 -148

No files matched your search

@@ -2,6 +2,7 @@ import { Plus } from 'lucide-react'
import Link from 'next/link'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { BASE_PATH } from 'lib/constants'
import {
Button,
Card,
@@ -16,6 +17,23 @@ import {
} from 'ui'
import { ShimmeringCard } from './ShimmeringCard'
export const Header = () => {
return (
<div className="border-default border-b p-3">
<div className="flex items-center space-x-2">
<Link href="/projects">
<img
src={`${BASE_PATH}/img/supabase-logo.svg`}
alt="Supabase"
className="border-default rounded border p-1 hover:border-white"
style={{ height: 24 }}
/>
</Link>
</div>
</div>
)
}
export const NoFilterResults = ({
filterStatus,
resetFilterStatus,
@@ -122,3 +140,21 @@ export const NoProjectsState = ({ slug }: { slug: string }) => {
</div>
)
}
export const NoOrganizationsState = () => {
return (
<div className="col-span-4 space-y-4 rounded-lg border border-dashed border-muted p-6 text-center">
<div className="space-y-1">
<p>You are not part of any organizations yet</p>
<p className="text-sm text-foreground-light">
Create your first organization to get started with Supabase
</p>
</div>
<div>
<Button asChild icon={<Plus />}>
<Link href="/new">New organization</Link>
</Button>
</div>
</div>
)
}
-1
View File
@@ -1,6 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
+51 -84
View File
@@ -1,31 +1,17 @@
import { Plus } from 'lucide-react'
import { NextPage } from 'next'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { ShimmeringCard } from 'components/interfaces/Home/ProjectList/ShimmeringCard'
import {
Header,
LoadingCardView,
NoOrganizationsState,
} from 'components/interfaces/Home/ProjectList/EmptyStates'
import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import CardButton from 'components/ui/CardButton'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { withAuth } from 'hooks/misc/withAuth'
import { BASE_PATH } from 'lib/constants'
import { Button, cn } from 'ui'
const Header = () => {
return (
<div className="border-default border-b p-3">
<div className="flex items-center space-x-2">
<Link href="/projects">
<img
src={`${BASE_PATH}/img/supabase-logo.svg`}
alt="Supabase"
className="border-default rounded border p-1 hover:border-white"
style={{ height: 24 }}
/>
</Link>
</div>
</div>
)
}
import { cn } from 'ui'
// [Joshen] Thinking we can deprecate this page in favor of /organizations
const GenericOrganizationPage: NextPage = () => {
@@ -52,70 +38,51 @@ const GenericOrganizationPage: NextPage = () => {
return (
<>
<Header />
<div className="flex flex-col mx-auto w-full space-y-3">
<h3 className="mt-8 text-2xl w-full max-w-5xl mx-auto">
Select an organization to continue
</h3>
<div
className="flex-grow py-6 overflow-y-auto"
style={{ maxHeight: 'calc(100vh - 49px - 64px)' }}
>
<div className="w-full max-w-5xl mx-auto flex flex-col gap-y-8">
{isLoading ? (
<ul
className={cn(
'mx-auto grid grid-cols-1 gap-4',
'sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3'
<PageLayout className="flex-grow min-h-0" title="Select an organization to continue">
<ScaffoldContainer>
<ScaffoldSection isFullWidth>
<div
className="flex-grow overflow-y-auto"
style={{ maxHeight: 'calc(100vh - 49px - 64px)' }}
>
<div className="w-full mx-auto flex flex-col gap-y-8">
{isLoading ? (
<LoadingCardView />
) : organizations?.length === 0 ? (
<NoOrganizationsState />
) : (
<ul
className={cn(
'w-full mx-auto grid grid-cols-1 gap-4',
'sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3'
)}
>
{organizations?.map((organization) => (
<li key={organization.slug} className="col-span-1">
<CardButton
linkHref={urlRewriterFactory(routeSlug)(organization.slug)}
title={
<div className="flex w-full flex-row justify-between gap-1">
<span className="flex-shrink truncate">{organization.name}</span>
</div>
}
footer={
<div className="flex items-end justify-between">
<span className="text-sm lowercase text-foreground-light">
{organization.slug}
</span>
</div>
}
/>
</li>
))}
</ul>
)}
>
<ShimmeringCard />
<ShimmeringCard />
</ul>
) : organizations?.length === 0 ? (
<div className="col-span-4 space-y-4 rounded-lg border border-dashed border-muted p-6 text-center">
<div className="space-y-1">
<p>You are not part of any organizations yet</p>
<p className="text-sm text-foreground-light">
Get started by creating a new organization.
</p>
</div>
<div>
<Button asChild icon={<Plus />}>
<Link href="/new">New organization</Link>
</Button>
</div>
</div>
) : (
<ul
className={cn(
'w-full mx-auto grid grid-cols-1 gap-4',
'sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3'
)}
>
{organizations?.map((organization) => (
<li key={organization.slug} className="col-span-1">
<CardButton
linkHref={urlRewriterFactory(routeSlug)(organization.slug)}
title={
<div className="flex w-full flex-row justify-between gap-1">
<span className="flex-shrink truncate">{organization.name}</span>
</div>
}
footer={
<div className="flex items-end justify-between">
<span className="text-sm lowercase text-foreground-light">
{organization.slug}
</span>
</div>
}
/>
</li>
))}
</ul>
)}
</div>
</div>
</div>
</div>
</ScaffoldSection>
</ScaffoldContainer>
</PageLayout>
</>
)
}
@@ -1,10 +1,14 @@
import { AlertTriangleIcon } from 'lucide-react'
import { NextPage } from 'next'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'common'
import {
Header,
LoadingCardView,
NoOrganizationsState,
} from 'components/interfaces/Home/ProjectList/EmptyStates'
import { ProjectList } from 'components/interfaces/Home/ProjectList/ProjectList'
import { HomePageActions } from 'components/interfaces/HomePageActions'
import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
@@ -12,7 +16,6 @@ import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { withAuth } from 'hooks/misc/withAuth'
import { BASE_PATH } from 'lib/constants'
import {
Alert_Shadcn_,
AlertDescription_Shadcn_,
@@ -23,44 +26,6 @@ import {
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
} from 'ui'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
const Header = () => {
return (
<div className="border-default border-b p-3">
<div className="flex items-center space-x-2">
<Link href="/projects">
<img
src={`${BASE_PATH}/img/supabase-logo.svg`}
alt="Supabase"
className="border-default rounded border p-1 hover:border-white"
style={{ height: 24 }}
/>
</Link>
</div>
</div>
)
}
const OrganizationLoadingState = () => {
return (
<>
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
<ShimmeringLoader className="w-1/4" />
</>
)
}
const OrganizationErrorState = () => {
return (
<Alert_Shadcn_ variant="warning">
<AlertTriangleIcon />
<AlertTitle_Shadcn_>Failed to load your Supabase organizations</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>Try refreshing the page</AlertDescription_Shadcn_>
</Alert_Shadcn_>
)
}
// [Joshen] I'd say we don't do route validation here, this page will act more
// like a proxy to the project specific pages, and we let those pages handle
@@ -108,7 +73,7 @@ const GenericProjectPage: NextPage = () => {
if (!!lastVisitedOrgSlug) {
setSlug(lastVisitedOrgSlug)
} else if (isSuccessOrganizations) {
setSlug(organizations[0].slug)
setSlug(organizations[0]?.slug)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lastVisitedOrgSlug, isSuccessOrganizations])
@@ -118,31 +83,39 @@ const GenericProjectPage: NextPage = () => {
<Header />
<PageLayout className="flex-grow min-h-0" title="Select a project to continue">
<ScaffoldContainer className="flex-grow flex flex-col">
<ScaffoldSection isFullWidth>
<div className="flex items-center gap-x-2">
<Select_Shadcn_ value={selectedSlug} onValueChange={setSlug}>
<SelectTrigger_Shadcn_ size="tiny" className="w-60 truncate">
<div className="flex items-center gap-x-2">
<p className="text-xs text-foreground-light">Organization:</p>
<SelectValue_Shadcn_ placeholder="Select an organization" />
</div>
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_ className="col-span-8">
{organizations.map((org) => (
<SelectItem_Shadcn_ key={org.slug} value={org.slug} className="text-xs">
{org.name}
</SelectItem_Shadcn_>
))}
</SelectContent_Shadcn_>
</Select_Shadcn_>
<HomePageActions hideNewProject />
</div>
</ScaffoldSection>
{organizations.length > 0 && (
<ScaffoldSection isFullWidth>
<div className="flex items-center gap-x-2">
<Select_Shadcn_ value={selectedSlug} onValueChange={setSlug}>
<SelectTrigger_Shadcn_ size="tiny" className="w-60 truncate">
<div className="flex items-center gap-x-2">
<p className="text-xs text-foreground-light">Organization:</p>
<SelectValue_Shadcn_ placeholder="Select an organization" />
</div>
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_ className="col-span-8">
{organizations.map((org) => (
<SelectItem_Shadcn_ key={org.slug} value={org.slug} className="text-xs">
{org.name}
</SelectItem_Shadcn_>
))}
</SelectContent_Shadcn_>
</Select_Shadcn_>
<HomePageActions hideNewProject />
</div>
</ScaffoldSection>
)}
<ScaffoldSection isFullWidth className="flex-grow pt-0 flex flex-col gap-y-4 h-px">
{isLoadingOrganizations ? (
<OrganizationLoadingState />
<LoadingCardView />
) : isErrorOrganizations ? (
<OrganizationErrorState />
<Alert_Shadcn_ variant="warning">
<AlertTriangleIcon />
<AlertTitle_Shadcn_>Failed to load your Supabase organizations</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>Try refreshing the page</AlertDescription_Shadcn_>
</Alert_Shadcn_>
) : organizations.length === 0 ? (
<NoOrganizationsState />
) : !!selectedOrganization ? (
<ProjectList
organization={selectedOrganization}