Files
supabase/apps/studio/components/ui/org-selector.tsx
Gildas Garcia 0713a1efc1 chore: remove shadcn suffix for Input, Textarea, Alert and Collapsible (#45867)
## Problem

Now that we migrated old components to their new shadcn alternatives, we
don't need the `_Shadcn_` suffix anymore.

## Solution

Remove it

<img width="659" height="609" alt="image"
src="https://github.com/user-attachments/assets/2d7271a9-066a-4dcc-92fe-729b106d2c2f"
/>
2026-05-15 14:55:37 +02:00

183 lines
6.1 KiB
TypeScript

import { ChevronDown } from 'lucide-react'
import Link from 'next/link'
import { parseAsString, useQueryState } from 'nuqs'
import { useMemo, useState } from 'react'
import { Badge, Button, Card, CardHeader, CardTitle, Input } from 'ui'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { ButtonTooltip } from './ButtonTooltip'
import { useFreeProjectLimitCheckQuery } from '@/data/organizations/free-project-limit-check-query'
import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
import type { Organization } from '@/types'
export interface ProjectClaimChooseOrgProps {
onSelect: (orgSlug: string) => void
maxOrgsToShow?: number
canCreateNewOrg: boolean
}
const OrganizationCard = ({
org,
onSelect,
}: {
org: Organization
onSelect: (orgSlug: string) => void
}) => {
const isFreePlan = org.plan?.id === 'free'
const { data: membersExceededLimit, isSuccess } = useFreeProjectLimitCheckQuery(
{ slug: org.slug },
{ enabled: isFreePlan }
)
const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0
const freePlanWithExceedingLimits = isFreePlan && hasMembersExceedingFreeTierLimit
return (
<Card
key={org.id}
className="hover:bg-surface-200 rounded-none first:rounded-t-lg last:rounded-b-lg -mb-px"
>
<CardHeader className="flex flex-row justify-between border-none space-y-0 space-x-2">
<CardTitle className="flex items-center gap-2 min-w-0 flex-1">
<span className="truncate min-w-0" title={org.name}>
{org.name}
</span>
<Badge className="shrink-0">{org.plan?.name}</Badge>
</CardTitle>
<ButtonTooltip
tooltip={{
content: {
text:
isSuccess && freePlanWithExceedingLimits ? (
<div className="space-y-3 w-96 p-2">
<p className="text-sm leading-normal">
The following members have reached their maximum limits for the number of
active free plan projects within organizations where they are an administrator
or owner:
</p>
<ul className="pl-5 list-disc">
{membersExceededLimit.map((member, idx: number) => (
<li key={`member-${idx}`}>
{member.username || member.primary_email} (Limit:{' '}
{member.free_project_limit} free projects)
</li>
))}
</ul>
<p className="text-sm leading-normal">
These members will need to either delete, pause, or upgrade one or more of
these projects before you're able to create a free project within this
organization.
</p>
</div>
) : undefined,
},
}}
size="small"
onClick={() => {
onSelect(org.slug)
}}
className="shrink-0"
disabled={isSuccess && freePlanWithExceedingLimits}
>
Choose
</ButtonTooltip>
</CardHeader>
</Card>
)
}
export function OrganizationSelector({
onSelect,
maxOrgsToShow = 5,
canCreateNewOrg,
}: ProjectClaimChooseOrgProps) {
const {
data: organizations = [],
isPending: isLoadingOrgs,
isSuccess: isSuccessOrgs,
isError: isErrorOrgs,
} = useOrganizationsQuery()
const [search, setSearch] = useQueryState(
'org',
parseAsString.withDefault('').withOptions({ clearOnDefault: true })
)
const [showAll, setShowAll] = useState(false)
const filteredOrgs = useMemo(() => {
if (!search) {
return showAll ? organizations : organizations.slice(0, maxOrgsToShow)
}
return organizations.filter((org) => org.name.toLowerCase().includes(search.toLowerCase()))
}, [organizations, search, showAll, maxOrgsToShow])
const searchParams = new URLSearchParams(location.search)
let pathname = location.pathname
const basePath = process.env.NEXT_PUBLIC_BASE_PATH
if (basePath) {
pathname = pathname.replace(basePath, '')
}
searchParams.set('returnTo', pathname)
const onSelectOrg = (orgSlug: string) => {
onSelect(orgSlug)
setSearch('')
}
return (
<div className="w-full flex flex-col gap-y-4">
{isLoadingOrgs ? (
<ShimmeringLoader />
) : isErrorOrgs ? (
<div>Error</div>
) : isSuccessOrgs && organizations.length === 0 ? (
<span className="text-sm text-foreground-light">
It seems you don't have any organizations yet.
</span>
) : (
<>
<Input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search..."
/>
<div>
{filteredOrgs.length === 0 && (
<div className="text-center text-foreground-light py-6">No organizations found.</div>
)}
{filteredOrgs.map((org) => (
<OrganizationCard key={org.id} org={org} onSelect={onSelectOrg} />
))}
{organizations.length > maxOrgsToShow && !showAll && !search && (
<div className="flex justify-center py-2">
<Button
icon={<ChevronDown className="w-4 h-4" />}
size="tiny"
onClick={() => {
setSearch('')
setShowAll(true)
}}
type="default"
>
Show all organizations
</Button>
</div>
)}
</div>
</>
)}
{canCreateNewOrg && (
<Card className="flex items-center justify-between border-dashed pr-6">
<CardHeader className="border-none">
<CardTitle>Need a new organization?</CardTitle>
</CardHeader>
<Button size="small" className="" asChild type="default">
<Link href={`/new?${searchParams.toString()}`}>New Organization</Link>
</Button>
</Card>
)}
</div>
)
}