chore: migrate Listbox to Shadcn components (#45279)

## Problem

We want to reduce the code we ship and maintain.

## Solution

Migrate old `<Listbox />` usage to the new Shadcn component.

## Screenshots

### Billing
Before:
<img width="536" height="458" alt="image"
src="https://github.com/user-attachments/assets/c8883bd8-cbbd-47cd-84a4-e37a36c05cd6"
/>

After:
<img width="541" height="451" alt="image"
src="https://github.com/user-attachments/assets/9c848b1b-e72a-413c-b264-48340ce0c7ef"
/>

### Foreign Key Editor
Before:
<img width="463" height="995" alt="image"
src="https://github.com/user-attachments/assets/4debeeeb-94bd-439d-8f57-a84a2ed5230a"
/>
<img width="428" height="241" alt="image"
src="https://github.com/user-attachments/assets/81a21d58-c023-445a-a6e8-c9be22b53075"
/>

After:
<img width="451" height="1007" alt="image"
src="https://github.com/user-attachments/assets/18c781c0-4f52-4ca2-99e6-1fc34dc857e3"
/>
<img width="434" height="240" alt="image"
src="https://github.com/user-attachments/assets/13fbb3ad-ef3c-499d-a27e-26cfae89ae44"
/>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Replaced legacy dropdowns with a unified Select component and
consistent form layout across payment method, org transfer,
foreign-key/column selectors, region selection, and other dropdowns.
* **Chores**
* Removed the legacy listbox implementation and its public exports from
the UI package.
* **Tests**
* Removed old listbox unit tests and updated e2e selectors to target the
new Select-based controls.
* **Style**
  * Deleted obsolete listbox-specific styles.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Gildas Garcia authored and GitHub committed 2026-04-28 14:30:49 +02:00
1 parent a96d3d2b21
commit 718021fc7b
11 files changed
+301 -726

No files matched your search

@@ -14,7 +14,15 @@ import {
useState,
} from 'react'
import { toast } from 'sonner'
import { Checkbox, Listbox } from 'ui'
import {
Checkbox,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { getStripeElementsAppearanceOptions } from '@/components/interfaces/Billing/Payment/Payment.utils'
@@ -289,48 +297,56 @@ const PaymentMethodSelection = forwardRef(function PaymentMethodSelection(
<p className="text-sm text-foreground-light">Retrieving payment methods</p>
</div>
) : paymentMethods?.data && paymentMethods?.data.length > 0 && !setupNewPaymentMethod ? (
<Listbox
<FormItemLayout
id="payment-method"
isReactForm={false}
layout={layout}
label="Payment method"
value={selectedPaymentMethod}
onChange={onSelectPaymentMethod}
className="flex items-center"
className="gap-[2px]"
size="tiny"
>
{paymentMethods?.data.map((method: any) => {
const label = `•••• •••• •••• ${method.card.last4}`
return (
<Listbox.Option
key={method.id}
label={label}
value={method.id}
addOnBefore={() => {
return (
<img
alt="Credit Card Brand"
src={`${BASE_PATH}/img/payment-methods/${method.card.brand
.replace(' ', '-')
.toLowerCase()}.png`}
width="32"
/>
)
}}
>
<div>{label}</div>
</Listbox.Option>
)
})}
<div
className="flex items-center px-3 py-2 space-x-2 transition cursor-pointer group hover:bg-surface-300"
onClick={() => {
setSetupNewPaymentMethod(true)
<Select_Shadcn_
value={selectedPaymentMethod}
onValueChange={(value) => {
if (value === 'new') {
setSetupNewPaymentMethod(true)
return
}
onSelectPaymentMethod(value)
}}
>
<Plus size={16} />
<p className="transition text-foreground-light group-hover:text-foreground">
Add new payment method
</p>
</div>
</Listbox>
<SelectTrigger_Shadcn_ id="payment-method">
<SelectValue_Shadcn_ className="flex gap-2" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
{paymentMethods?.data.map((method) => {
const label = `•••• •••• •••• ${method.card?.last4}`
return (
<SelectItem_Shadcn_ key={method.id} value={method.id}>
<div className="flex gap-2">
<img
alt="Credit Card Brand"
src={`${BASE_PATH}/img/payment-methods/${method.card?.brand
.replace(' ', '-')
.toLowerCase()}.png`}
width="32"
/>
{label}
</div>
</SelectItem_Shadcn_>
)
})}
<SelectItem_Shadcn_ value="new">
<div className="flex gap-2">
<Plus size={16} />
<p className="transition text-foreground-light group-hover:text-foreground">
Add new payment method
</p>
</div>
</SelectItem_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
</FormItemLayout>
) : null}
{stripePromise && setupIntent && customerProfile && (
@@ -1,11 +1,23 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useQueryClient } from '@tanstack/react-query'
import { useFlag } from 'common'
import { Loader, Shield, Users, Wrench } from 'lucide-react'
import { Loader, Shield, Wrench } from 'lucide-react'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { Button, InfoIcon, Listbox, Loading, Modal, WarningIcon } from 'ui'
import {
Button,
InfoIcon,
Loading,
Modal,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
WarningIcon,
} from 'ui'
import { Admonition } from 'ui-patterns'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { DocsButton } from '@/components/ui/DocsButton'
@@ -28,7 +40,7 @@ export const TransferProjectButton = () => {
const organizations = (allOrganizations || []).filter((it) => it.id !== projectOrgId)
const [selectedOrg, setSelectedOrg] = useState()
const [selectedOrg, setSelectedOrg] = useState<string>()
const {
mutate: transferProject,
@@ -185,27 +197,30 @@ export const TransferProjectButton = () => {
<InfoIcon /> You do not have any organizations you can transfer your project to.
</div>
) : (
<Listbox
label="Select Target Organization"
<FormItemLayout
id="organization"
isReactForm={false}
layout="vertical"
value={selectedOrg}
onChange={(slug) => setSelectedOrg(slug)}
placeholder="Select Organization"
label="Select Target Organization"
className="gap-[2px]"
size="tiny"
>
<Listbox.Option disabled key="no-results" label="Select Organization" value="">
Select Organization
</Listbox.Option>
{organizations.map((x: any) => (
<Listbox.Option
key={x.id}
label={x.name}
value={x.slug}
addOnBefore={() => <Users />}
>
{x.name}
</Listbox.Option>
))}
</Listbox>
<Select_Shadcn_
onValueChange={(slug) => setSelectedOrg(slug)}
value={selectedOrg}
>
<SelectTrigger_Shadcn_ id="organization">
<SelectValue_Shadcn_ placeholder="Select Organization" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
{organizations.map((x) => (
<SelectItem_Shadcn_ key={x.id} value={x.slug}>
{x.name}
</SelectItem_Shadcn_>
))}
</SelectContent_Shadcn_>
</Select_Shadcn_>
</FormItemLayout>
)}
</div>
)}
@@ -1,16 +1,21 @@
import { FOREIGN_KEY_CASCADE_ACTION } from '@supabase/pg-meta'
import type { PostgresTable } from '@supabase/postgres-meta'
import { sortBy } from 'lodash'
import { ArrowRight, Database, HelpCircle, Loader2, Table, X } from 'lucide-react'
import { ArrowRight, HelpCircle, Loader2, X } from 'lucide-react'
import { Fragment, useEffect, useState } from 'react'
import {
Alert_Shadcn_,
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Button,
Listbox,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
SidePanel,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { ActionBar } from '../ActionBar'
import { NUMERICAL_TYPES, TEXT_TYPES } from '../SidePanelEditor.constants'
@@ -24,7 +29,6 @@ import {
type ForeignKeyDirtyState,
} from './ForeignKeySelector.utils'
import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
import { DocsButton } from '@/components/ui/DocsButton'
import InformationBox from '@/components/ui/InformationBox'
import { useSchemasQuery } from '@/data/database/schemas-query'
import { useTableQuery } from '@/data/tables/table-retrieve-query'
@@ -32,7 +36,6 @@ import { useTablesQuery } from '@/data/tables/tables-query'
import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose'
import { DOCS_URL } from '@/lib/constants'
import { uuidv4 } from '@/lib/helpers'
const EMPTY_STATE: ForeignKey = {
@@ -258,61 +261,64 @@ export const ForeignKeySelector = ({
url="https://www.postgresql.org/docs/current/tutorial-fk.html"
urlLabel="Postgres Foreign Key Documentation"
/>
<Listbox
<FormItemLayout
id="schema"
isReactForm={false}
layout="vertical"
label="Select a schema"
value={fk.schema}
onChange={(value: string) => updateSelectedSchema(value)}
className="gap-[2px]"
size="tiny"
>
{sortedSchemas.map((schema) => {
return (
<Listbox.Option
key={schema.id}
value={schema.name}
label={schema.name}
className="min-w-96"
addOnBefore={() => <Database size={16} strokeWidth={1.5} />}
>
<div className="flex items-center gap-2">
{/* For aria searching to target the schema name instead of schema */}
<span className="hidden">{schema.name}</span>
<span className="text-foreground">{schema.name}</span>
</div>
</Listbox.Option>
)
})}
</Listbox>
<Listbox
<Select_Shadcn_
value={fk.schema}
onValueChange={(value) => updateSelectedSchema(value)}
>
<SelectTrigger_Shadcn_ id="schema">
<SelectValue_Shadcn_ />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
{sortedSchemas.map((schema) => (
<SelectItem_Shadcn_ key={schema.id} value={schema.name} className="min-w-96">
{schema.name}
</SelectItem_Shadcn_>
))}
</SelectContent_Shadcn_>
</Select_Shadcn_>
</FormItemLayout>
<FormItemLayout
id="table"
isReactForm={false}
layout="vertical"
label="Select a table to reference to"
value={selectedTable?.id ?? 1}
onChange={(value: string) => updateSelectedTable(Number(value))}
disabled={isLoadingSelectedTable}
className="gap-[2px]"
size="tiny"
>
<Listbox.Option key="empty" className="min-w-96" value={1} label="---">
---
</Listbox.Option>
{sortBy(tables, ['schema']).map((table) => {
return (
<Listbox.Option
key={table.id}
value={table.id}
label={table.name}
className="min-w-96"
addOnBefore={() => <Table size={16} strokeWidth={1.5} />}
>
<div className="flex items-center gap-2">
{/* For aria searching to target the table name instead of schema */}
<span className="hidden">{table.name}</span>
<span className="text-foreground-lighter">{table.schema}</span>
<span className="text-foreground">{table.name}</span>
</div>
</Listbox.Option>
)
})}
</Listbox>
<Select_Shadcn_
value={selectedTable?.id !== undefined ? String(selectedTable.id) : undefined}
onValueChange={(value) => updateSelectedTable(Number(value))}
disabled={isLoadingSelectedTable}
>
<SelectTrigger_Shadcn_ id="table">
<SelectValue_Shadcn_ />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
{sortBy(tables, ['schema']).map((table) => (
<SelectItem_Shadcn_
key={table.id}
value={table.id.toString()}
className="min-w-96"
>
<div className="flex items-center gap-2">
{/* For aria searching to target the table name instead of schema */}
<span className="hidden">{table.name}</span>
<span className="text-foreground-lighter">{table.schema}</span>
<span className="text-foreground">{table.name}</span>
</div>
</SelectItem_Shadcn_>
))}
</SelectContent_Shadcn_>
</Select_Shadcn_>
</FormItemLayout>
{fk.schema && fk.table && (
<>
@@ -331,10 +337,10 @@ export const ForeignKeySelector = ({
to reference to
</label>
<div className="grid grid-cols-10 gap-y-2">
<div className="col-span-5 text-xs text-foreground-lighter">
<div className="col-span-5 text-xs">
{selectedSchema}.{table.name.length > 0 ? table.name : '[unnamed table]'}
</div>
<div className="col-span-4 text-xs text-foreground-lighter text-right">
<div className="col-span-4 text-xs text-right">
{fk.schema}.{fk.table}
</div>
{fk.columns.length === 0 && (
@@ -347,73 +353,57 @@ export const ForeignKeySelector = ({
{fk.columns.map((_, idx) => (
<Fragment key={`${fk.schema}-${fk.table}-${idx}`}>
<div className="col-span-4">
<Listbox
id="column"
<Select_Shadcn_
value={fk.columns[idx].source}
onChange={(value: string) =>
updateSelectedColumn(idx, 'source', value)
}
onValueChange={(value) => updateSelectedColumn(idx, 'source', value)}
>
<Listbox.Option
key="empty"
value={''}
label="---"
className="!w-[170px]"
<SelectTrigger_Shadcn_
aria-label={`Column from ${selectedSchema}.${table.name.length > 0 ? table.name : '[unnamed table]'}`}
>
---
</Listbox.Option>
{(table?.columns ?? [])
.filter((x) => x.name.length !== 0)
.map((column) => (
<Listbox.Option
key={column.id}
value={column.name}
label={column.name}
className="!w-[170px]"
>
<SelectValue_Shadcn_ />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
{(table?.columns ?? [])
.filter((x) => x.name.length !== 0)
.map((column) => (
<SelectItem_Shadcn_ key={column.id} value={column.name}>
<div className="flex items-center gap-2">
<span className="text-foreground">{column.name}</span>
<span className="text-foreground-lighter">
{column.format === '' ? '-' : column.format}
</span>
</div>
</SelectItem_Shadcn_>
))}
</SelectContent_Shadcn_>
</Select_Shadcn_>
</div>
<div className="col-span-1 flex justify-center items-center">
<ArrowRight />
</div>
<div className="col-span-4">
<Select_Shadcn_
value={fk.columns[idx].target}
onValueChange={(value) => updateSelectedColumn(idx, 'target', value)}
>
<SelectTrigger_Shadcn_
aria-label={`Column from ${fk.schema}.${fk.table}`}
>
<SelectValue_Shadcn_ />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
{(selectedTable?.columns ?? []).map((column) => (
<SelectItem_Shadcn_ key={column.id} value={column.name}>
<div className="flex items-center gap-2">
<span className="text-foreground">{column.name}</span>
<span className="text-foreground-lighter">
{column.format === '' ? '-' : column.format}
</span>
</div>
</Listbox.Option>
</SelectItem_Shadcn_>
))}
</Listbox>
</div>
<div className="col-span-1 flex justify-center items-center">
<ArrowRight />
</div>
<div className="col-span-4">
<Listbox
id="column"
value={fk.columns[idx].target}
onChange={(value: string) =>
updateSelectedColumn(idx, 'target', value)
}
>
<Listbox.Option
key="empty"
value={''}
label="---"
className="!w-[170px]"
>
---
</Listbox.Option>
{(selectedTable?.columns ?? []).map((column) => (
<Listbox.Option
key={column.id}
value={column.name}
label={column.name}
className="!w-[170px]"
>
<div className="flex items-center gap-2">
<span className="text-foreground">{column.name}</span>
<span className="text-foreground-lighter">{column.format}</span>
</div>
</Listbox.Option>
))}
</Listbox>
</SelectContent_Shadcn_>
</Select_Shadcn_>
</div>
<div className="col-span-1 flex justify-end items-center">
<Button
@@ -520,11 +510,12 @@ export const ForeignKeySelector = ({
urlLabel="More information"
/>
<Listbox
<FormItemLayout
id="updateAction"
value={fk.updateAction}
isReactForm={false}
layout="vertical"
label="Action if referenced row is updated"
descriptionText={
description={
<p>
{generateCascadeActionDescription(
'update',
@@ -533,45 +524,60 @@ export const ForeignKeySelector = ({
)}
</p>
}
onChange={(value: string) => updateCascadeAction('updateAction', value)}
className="gap-[2px]"
size="tiny"
>
{FOREIGN_KEY_CASCADE_OPTIONS.filter((option) =>
['no-action', 'cascade', 'restrict'].includes(option.key)
).map((option) => (
<Listbox.Option key={option.key} value={option.value} label={option.label}>
<p className="text-foreground">{option.label}</p>
</Listbox.Option>
))}
</Listbox>
<Listbox
<Select_Shadcn_
value={fk.updateAction}
onValueChange={(value) => updateCascadeAction('updateAction', value)}
>
<SelectTrigger_Shadcn_ id="updateAction">
<SelectValue_Shadcn_ />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
{FOREIGN_KEY_CASCADE_OPTIONS.filter((option) =>
['no-action', 'cascade', 'restrict'].includes(option.key)
).map((option) => (
<SelectItem_Shadcn_ key={option.key} value={option.value}>
{option.label}
</SelectItem_Shadcn_>
))}
</SelectContent_Shadcn_>
</Select_Shadcn_>
</FormItemLayout>
<FormItemLayout
id="deletionAction"
value={fk.deletionAction}
className="[&>div>label]:flex [&>div>label]:items-center"
isReactForm={false}
layout="vertical"
label="Action if referenced row is removed"
// @ts-ignore
labelOptional={
<DocsButton href={`${DOCS_URL}/guides/database/postgres/cascade-deletes`} />
description={
<p>
{generateCascadeActionDescription(
'delete',
fk.deletionAction,
`${fk.schema}.${fk.table}`
)}
</p>
}
descriptionText={
<>
<p>
{generateCascadeActionDescription(
'delete',
fk.deletionAction,
`${fk.schema}.${fk.table}`
)}
</p>
</>
}
onChange={(value: string) => updateCascadeAction('deletionAction', value)}
className="gap-[2px]"
size="tiny"
>
{FOREIGN_KEY_CASCADE_OPTIONS.map((option) => (
<Listbox.Option key={option.key} value={option.value} label={option.label}>
<p className="text-foreground">{option.label}</p>
</Listbox.Option>
))}
</Listbox>
<Select_Shadcn_
value={fk.deletionAction}
onValueChange={(value) => updateCascadeAction('deletionAction', value)}
>
<SelectTrigger_Shadcn_ id="deletionAction">
<SelectValue_Shadcn_ />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
{FOREIGN_KEY_CASCADE_OPTIONS.map((option) => (
<SelectItem_Shadcn_ key={option.key} value={option.value}>
{option.label}
</SelectItem_Shadcn_>
))}
</SelectContent_Shadcn_>
</Select_Shadcn_>
</FormItemLayout>
</>
)}
</>
@@ -3,7 +3,18 @@ import { useParams } from 'common'
import { ChangeEvent, useEffect, useState } from 'react'
import { AWS_REGIONS } from 'shared-data'
import { toast } from 'sonner'
import { Alert, Button, Checkbox, Input, Listbox } from 'ui'
import {
Alert,
Button,
Checkbox,
Input,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { isVercelUrl } from '@/components/interfaces/Integrations/Vercel/VercelIntegration.utils'
import { Markdown } from '@/components/interfaces/Markdown'
@@ -65,7 +76,7 @@ const CreateProject = () => {
const [passwordStrengthMessage, setPasswordStrengthMessage] = useState('')
const [passwordStrengthScore, setPasswordStrengthScore] = useState(-1)
const [shouldRunMigrations, setShouldRunMigrations] = useState(true)
const [dbRegion, setDbRegion] = useState(PROVIDERS.AWS.default_region.displayName)
const [dbRegion, setDbRegion] = useState<string>(PROVIDERS.AWS.default_region.displayName)
const track = useTrack()
const snapshot = useIntegrationInstallationSnapshot()
@@ -276,33 +287,38 @@ const CreateProject = () => {
</div>
<div className="py-2">
<div className="mt-1">
<Listbox
<FormItemLayout
id="region"
isReactForm={false}
layout="vertical"
label="Region"
type="select"
value={dbRegion}
onChange={(region) => setDbRegion(region)}
descriptionText="Select a region close to your users for the best performance."
description="Select a region close to your users for the best performance."
className="gap-[2px]"
size="tiny"
>
{Object.keys(AWS_REGIONS).map((option: string, i) => {
const label = Object.values(AWS_REGIONS)[i].displayName
return (
<Listbox.Option
key={option}
label={label}
value={label}
addOnBefore={() => (
<img
alt="region icon"
className="w-5 rounded-sm"
src={`${BASE_PATH}/img/regions/${Object.values(AWS_REGIONS)[i].code}.svg`}
/>
)}
>
<span className="text-foreground">{label}</span>
</Listbox.Option>
)
})}
</Listbox>
<Select_Shadcn_ value={dbRegion} onValueChange={(region) => setDbRegion(region)}>
<SelectTrigger_Shadcn_ id="region">
<SelectValue_Shadcn_ />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
{Object.keys(AWS_REGIONS).map((option: string, i) => {
const label = Object.values(AWS_REGIONS)[i].displayName
return (
<SelectItem_Shadcn_ key={option} value={label}>
<div className="flex gap-2">
<img
alt="region icon"
className="w-5 rounded-sm"
src={`${BASE_PATH}/img/regions/${Object.values(AWS_REGIONS)[i].code}.svg`}
/>
<span>{label}</span>
</div>
</SelectItem_Shadcn_>
)
})}
</SelectContent_Shadcn_>
</Select_Shadcn_>
</FormItemLayout>
</div>
</div>
<div className="py-2 pb-4">
+12 -15
View File
@@ -1076,19 +1076,19 @@ testRunner('table editor', () => {
.click()
await page.getByRole('menuitem', { name: 'Edit table' }).click()
// Open foreign key selector
await page.getByRole('button', { name: 'Add foreign key relation' }).click()
// Select schema (should default to public)
await expect(page.getByRole('button', { name: 'Select a schema' })).toContainText('public')
// Select target table
const tableQueryPromise = waitForApiResponseWithTimeout(page, (response) =>
response.url().includes(`table-public-${targetTableName}`)
)
await page.getByRole('button', { name: 'Select a table to reference to' }).click()
await page.getByRole('menuitem', { name: `public ${targetTableName}` }).click()
// Open foreign key selector
await page.getByRole('button', { name: 'Add foreign key relation' }).click()
// Select schema (should default to public)
await expect(page.getByRole('combobox', { name: 'Select a schema' })).toContainText('public')
await page.getByRole('combobox', { name: 'Select a table to reference to' }).click()
await page.getByRole('option', { name: `public ${targetTableName}` }).click()
// Wait for table columns to load
await tableQueryPromise
@@ -1099,15 +1099,12 @@ testRunner('table editor', () => {
).toBeVisible()
// Select source column (id from source table)
await page.getByRole('button', { name: '---' }).first().click()
await page.getByRole('menuitem', { name: 'id int8' }).click()
// Wait for the first dropdown to update - there should only be one '---' button left now
await expect(page.getByRole('button', { name: '---' })).toHaveCount(1)
await page.getByRole('combobox', { name: 'Column from public.pw_table_fk_source' }).click()
await page.getByRole('option', { name: 'id int8' }).click()
// Select target column (id from target table)
await page.getByRole('button', { name: '---' }).first().click()
await page.getByRole('menuitem', { name: 'id int8' }).click()
await page.getByRole('combobox', { name: 'Column from public.pw_table_fk_target' }).click()
await page.getByRole('option', { name: 'id int8' }).click()
// Verify cascade action options are visible
await expect(page.getByText('Action if referenced row is updated')).toBeVisible()
-1
View File
@@ -36,7 +36,6 @@ export * from './src/lib/utils'
// DATA ENTRY
export * from './src/components/Listbox'
export * from './src/components/Input'
export * from './src/components/Toggle'
export * from './src/components/ExpandingTextArea'
@@ -1,288 +0,0 @@
'use client'
import { flatten } from 'lodash'
import { Check } from 'lucide-react'
import { DropdownMenu as DropdownMenuPrimitive } from 'radix-ui'
import React, { useEffect, useRef, useState } from 'react'
import { FormLayout } from '../../lib/Layout/FormLayout/FormLayout'
import InputErrorIcon from '../../lib/Layout/InputErrorIcon'
import InputIconContainer from '../../lib/Layout/InputIconContainer'
import styleHandler from '../../lib/theme/styleHandler'
import { cn } from '../../lib/utils/cn'
import { SelectContext } from './SelectContext'
export interface Props extends Omit<React.InputHTMLAttributes<HTMLButtonElement>, 'size'> {
className?: string
buttonClassName?: string
children: React.ReactNode
descriptionText?: string | React.ReactNode
error?: string
icon?: any
id?: string
label?: string | React.ReactNode
labelOptional?: string
layout?: 'horizontal' | 'vertical'
style?: React.CSSProperties
value?: any
reveal?: boolean
actions?: React.ReactNode
size?: 'tiny' | 'small' | 'medium' | 'large' | 'xlarge'
defaultValue?: any
validation?: (x: any) => void
optionsWidth?: number
// override the button prop for onchange we only return a single value
// rather than a ChangeEvent<HTMLButtonElement>
onChange?: (x: any) => void
}
/**
* @deprecated Use `import { Select_shadcn_ } from "ui"` or follow ComboBox convention or use ./ui-patterns/multi-select
*/
function Listbox({
children,
className,
buttonClassName,
descriptionText,
error,
icon,
id = '',
name = '',
label,
labelOptional,
layout,
value = undefined,
onChange,
onFocus,
onBlur,
style,
size = 'medium',
defaultValue,
disabled,
optionsWidth,
}: Props) {
const [selected, setSelected] = useState(undefined)
const [selectedNode, setSelectedNode] = useState<any>({})
const __styles = styleHandler('listbox')
const triggerRef = useRef<HTMLButtonElement>(null)
useEffect(() => {
if (value !== undefined) {
setSelected(value)
}
}, [value])
useEffect(() => {
// handle listbox options width size
function handleResize() {
// Set window width/height to state
// [Joshen] Note this causes some style conflicts if there are multiple listboxes
// rendered on the same page. All listbox option widths will be that of the latest
// listbox component that got rendered, rather than following its parent
document.documentElement.style.setProperty(
'--width-listbox',
`${optionsWidth ? optionsWidth : triggerRef.current?.offsetWidth}px`
)
}
// Add event listener
window.addEventListener('resize', handleResize)
// Call handler right away so state gets updated with initial window size
handleResize()
// Remove event listener on cleanup
return () => window.removeEventListener('resize', handleResize)
}, [])
useEffect(() => {
const data: any = children
const content: any = flatten(data)
function findNode(_value: any) {
return content.find((node: any) => node.props.value === _value)
}
/*
* value prop overrides everything
*/
if (value) {
setSelected(value)
const node: any = findNode(value)
setSelectedNode(node?.props ? node.props : undefined)
return
}
/*
* if no value prop, then use selected state
*/
if (selected) {
const node: any = findNode(selected)
setSelectedNode(node?.props ? node.props : undefined)
return
} else if (defaultValue) {
setSelected(defaultValue)
const node: any = findNode(defaultValue)
setSelectedNode(node?.props ? node.props : undefined)
return
} else {
/*
* if no selected value (including a `defaultvalue`), then use first child
*/
setSelectedNode(content[0]?.props)
return
}
}, [selected])
function handleOnChange(value: any) {
if (onChange) onChange(value)
setSelected(value)
}
let selectClasses = [__styles.container, __styles.base, buttonClassName]
let addonBeforeClasses = [__styles.addOnBefore]
if (error) selectClasses.push(__styles.variants.error)
if (!error) selectClasses.push(__styles.variants.standard)
// if (icon) selectClasses.push(SelectStyles['sbui-listbox--with-icon'])
if (icon) addonBeforeClasses.push(__styles.with_icon[size])
// if (size) selectClasses.push(SelectStyles[`sbui-listbox--${size}`])
if (size) selectClasses.push(__styles.size[size])
// if (borderless) selectClasses.push(SelectStyles['sbui-listbox--borderless'])
if (disabled) selectClasses.push(__styles.disabled)
return (
<FormLayout
label={label}
labelOptional={labelOptional}
layout={layout}
id={id}
error={error}
descriptionText={descriptionText}
className={className}
style={style}
size={size}
>
<DropdownMenuPrimitive.Root>
<DropdownMenuPrimitive.Trigger asChild disabled={disabled}>
<button
data-size={size}
ref={triggerRef}
className={cn(selectClasses)}
onBlur={onBlur}
onFocus={onFocus}
name={name}
id={id}
>
<span className={cn(addonBeforeClasses)}>
{icon && <InputIconContainer size={size} icon={icon} />}
{selectedNode?.addOnBefore && <selectedNode.addOnBefore />}
<span className={__styles.label}>{selectedNode?.label}</span>
</span>
<span className={__styles.chevron_container}>
<svg
className={__styles.chevron}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</span>
{error && (
<div className={__styles.actions_container}>
{error && <InputErrorIcon size={size} />}
</div>
)}
</button>
</DropdownMenuPrimitive.Trigger>
<DropdownMenuPrimitive.Content
sideOffset={6}
loop={true}
side={'bottom'}
align="center"
className={__styles.options_container}
>
<div>
<SelectContext.Provider value={{ onChange: handleOnChange, selected }}>
{children}
</SelectContext.Provider>
</div>
</DropdownMenuPrimitive.Content>
</DropdownMenuPrimitive.Root>
</FormLayout>
)
}
interface OptionProps {
id?: string
value: any
label: string
disabled?: boolean
children?: React.ReactNode | (({ active, selected }: any) => React.ReactNode)
className?: string
addOnBefore?: ({ active, selected }: any) => React.ReactNode
}
/**
* @deprecated Use ./Select_shadcn_ or follow ComboBox convention or use ./ui-patterns/multi-select
*/
function SelectOption({
id,
value,
disabled = false,
children,
className = '',
addOnBefore,
}: OptionProps) {
const __styles = styleHandler('listbox')
return (
<SelectContext.Consumer>
{({ onChange, selected }) => {
const active = selected === value ? true : false
return (
<DropdownMenuPrimitive.Item
key={id}
className={cn(
__styles.option,
active ? __styles.option_active : ' ',
disabled ? __styles.option_disabled : ' ',
className
)}
onSelect={() => (!disabled ? onChange(value) : {})}
>
<div className={__styles.option_inner}>
{addOnBefore && addOnBefore({ active, selected })}
<span>
{typeof children === 'function' ? children({ active, selected }) : children}
</span>
</div>
{active ? (
<span
className={cn(__styles.option_check, active ? __styles.option_check_active : '')}
>
<Check className={__styles.option_check_icon} aria-hidden="true" />
</span>
) : null}
</DropdownMenuPrimitive.Item>
)
}}
</SelectContext.Consumer>
)
}
Listbox.Option = SelectOption
export default Listbox
@@ -1,8 +0,0 @@
import { createContext } from 'react'
// Make sure the shape of the default value passed to
// createContext matches the shape that the consumers expect!
export const SelectContext = createContext({
onChange: (_e: any) => {},
selected: undefined,
})
@@ -1,145 +0,0 @@
/* .sbui-listbox-container {
@apply relative;
} */
/* .sbui-listbox {
@apply block w-full bg-white pl-3 pr-10 py-2 text-sm rounded-md transition-all;
@apply text-gray-700 border border-solid border-gray-300;
@apply cursor-default focus:outline-none focus:ring-1 focus:ring-brand-600 focus:border-brand-600;
@apply dark:bg-transparent dark:text-white dark:border-gray-500;
@apply dark:focus:border-brand-300;
-webkit-appearance: none;
-moz-appearance: none;
text-indent: 1px;
text-overflow: '';
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0);
transition: box-shadow 0.3s ease-in-out; */
/* // temporary fix
//
// temporary fix for supabase apps
// tailwind @base styles adds a dropdown chevron
// using background image as default
// */
/* background-image: none;
} */
.sbui-listbox:focus {
box-shadow: 0 0 0 2px rgba(62, 207, 142, 0.1);
}
.sbui-listbox--error {
@apply border-red-500;
}
.sbui-listbox--borderless {
@apply border-transparent shadow-none;
}
/*
Select sizes
*/
/* .sbui-listbox--tiny {
@apply px-2.5 py-1.5 text-xs;
}
.sbui-listbox--small {
@apply px-3 py-2 text-sm leading-4;
}
.sbui-listbox--medium {
@apply px-4 py-2 text-sm;
}
.sbui-listbox--large {
@apply px-4 py-2 text-base;
}
.sbui-listbox--xlarge {
@apply px-6 py-3 text-base;
} */
/* .sbui-listbox-actions-container {
@apply absolute inset-y-0 right-0 pl-3 pr-1 mr-5 flex items-center;
} */
/*
Select icon
*/
/* .sbui-listbox--with-icon {
@apply pl-7;
} */
/*
Add on before
*/
/* .sbui-listbox-addonbefore {
@apply w-full flex flex-row items-center space-x-3;
} */
.sbui-listbox-label {
@apply truncate;
}
/*
Select Chevron
*/
.sbui-listbox-chevron-container {
@apply absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none;
}
.sbui-listbox-chevron {
@apply h-5 w-5 text-gray-400;
}
/*
Option
*/
/* .sbui-listbox-option-container {
@apply list-none p-0 absolute mt-1 w-full bg-white dark:bg-gray-800 shadow-lg border border-solid border-control max-h-60 rounded-md py-1 text-base ring-1 ring-black/5 overflow-auto focus:outline-none sm:text-sm z-10;
} */
/* .sbui-listbox-option {
@apply cursor-pointer select-none relative py-2 pl-3 pr-9 text-gray-900 dark:text-white;
} */
/* .sbui-listbox-option--active {
@apply text-brand-600 dark:text-brand-600 bg-brand-600/20;
}
.sbui-listbox-option--disabled {
@apply cursor-not-allowed opacity-50;
} */
/* .sbui-listbox-option__inner {
@apply flex items-center space-x-3;
} */
/* .sbui-listbox-option__check {
@apply absolute inset-y-0 right-0 flex items-center pr-3;
}
.sbui-listbox-option__check--active {
@apply text-brand-600;
} */
.sbui-listbox-option__check__icon {
@apply h-5 w-5;
}
/*
Transition
*/
.sbui-listbox-transition--leave {
@apply transition ease-in duration-100;
}
.sbui-listbox-transition--leave-from {
@apply opacity-100;
}
.sbui-listbox-transition--leave-to {
@apply opacity-0;
}
@@ -1,29 +0,0 @@
import { render, screen } from '@testing-library/react'
import React from 'react'
import { describe, expect, it } from 'vitest'
import Select from './index'
describe('#Select', () => {
it.skip('should render select correctly', async () => {
render(
<Select data-testid="form-select">
<option>1</option>
<option>2</option>
</Select>
)
expect(screen.queryByTestId('form-select')).toBeInTheDocument()
})
it.skip('should have "form-select--error" class', () => {
render(
<Select isError data-testid="form-select">
<option>1</option>
<option>2</option>
</Select>
)
expect(screen.queryByTestId('form-select')).toHaveClass(
'form-select border-solid form-select--error'
)
})
})
@@ -1,4 +0,0 @@
import Listbox from './Listbox2'
export default Listbox
export { default as Listbox } from './Listbox2'