feat(studio): add keyboard shortcuts to Database listing pages (#45467)

## Summary

Adds the `O→S` / `Shift+F` / `Shift+N` / `F→C` shortcut set across every
Database listing page, on top of the schema-visualizer pattern from
#45386. Fixes [FE-3131](https://linear.app/supabase/issue/FE-3131).

A shared `list-page.*` registry replaces what would have been a per-page
registry file for each route, and `useShortcut`/`Shortcut` gain a
`label` override so a single ID renders contextually in Cmd+K and hover
tooltips.

## Pages and shortcuts

| Page | `O→S` | `Shift+F` | `Shift+N` | `F→C` |
| --- | --- | --- | --- | --- |
| Tables | Open schema selector | Search tables | Create new table |
Clears search + entity-type filter |
| Functions | Open schema selector | Search functions | Create new
function | Clears search + Return Type + Security |
| Triggers — data | Open schema selector | Search triggers | Create new
trigger | Clears search + Table filter |
| Triggers — event | — | Search event triggers | Create new event
trigger | Clears search + Owner filter |
| Enumerated Types | Open schema selector | Search enumerated types |
Create new enumerated type | Clears search |
| Indexes | Open schema selector | Search indexes | Create new index |
Clears search |
| Roles | — | Search roles | Add new role | Clears search + filter type
→ 'all' |
| Publications | — | Search publications | — | Clears search |
| Extensions | — | Search extensions | — | Clears search |

`Shift+N` only fires when the page-specific gate allows it (permission +
unlocked schema + any other prerequisite like `hasTables` for triggers).

## Test plan

- [ ] On each of the 9 pages, all four shortcuts behave as listed in the
table above
- [ ] Hover the wrapped controls — tooltip shows the page-specific label
and the right keybind badge
- [ ] Open the schema selector via `O→S` — the hover tooltip is
suppressed while the popover is open
- [ ] `Shift+N` is a no-op on locked schemas / when permission is
missing / when a prereq fails (e.g. no tables on Triggers data)
- [ ] `Shift+F` focuses the search input; while focused, Escape clears
(with text) → Escape blurs (when empty)
- [ ] `Cmd+K` shows the page-specific shortcut name while on each page;
the entry goes away on navigation
- [ ] `Mod+/` reference sheet shows one "List pages" group with 4
generic entries
- [ ] No regression to existing schema-visualizer / table-editor /
SQL-editor shortcut behavior

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

* **New Features**
* Added a dedicated list-page shortcut group in the shortcuts reference.

* **Improvements**
* Keyboard shortcuts across database list pages: focus & select search,
reset filters, and create-new-item shortcuts.
* Escape now clears/searches or blurs inputs to avoid accidental popover
closes.
* Create/new buttons respond to shortcuts when allowed; disabled actions
keep permission tooltips for clarity.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Ali Waseem authored and GitHub committed 2026-05-04 07:08:35 -06:00
1 parent b308983bb8
commit 618c902e1a
18 files changed
+578 -151

No files matched your search

@@ -1,6 +1,6 @@
import { Edit, MoreVertical, Search, Trash } from 'lucide-react'
import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs'
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { toast } from 'sonner'
import {
Button,
@@ -26,15 +26,21 @@ import EditEnumeratedTypeSidePanel from './EditEnumeratedTypeSidePanel'
import AlertError from '@/components/ui/AlertError'
import { DocsButton } from '@/components/ui/DocsButton'
import SchemaSelector from '@/components/ui/SchemaSelector'
import { Shortcut } from '@/components/ui/Shortcut'
import { useEnumeratedTypeDeleteMutation } from '@/data/enumerated-types/enumerated-type-delete-mutation'
import { useEnumeratedTypesQuery } from '@/data/enumerated-types/enumerated-types-query'
import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useIsProtectedSchema } from '@/hooks/useProtectedSchemas'
import { onSearchInputEscape } from '@/lib/keyboard'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
export const EnumeratedTypes = () => {
const { data: project } = useSelectedProjectQuery()
const [search, setSearch] = useState('')
const [schemaSelectorOpen, setSchemaSelectorOpen] = useState(false)
const searchInputRef = useRef<HTMLInputElement>(null)
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
const {
@@ -80,6 +86,19 @@ export const EnumeratedTypes = () => {
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
useShortcut(
SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH,
() => {
searchInputRef.current?.focus()
searchInputRef.current?.select()
},
{ label: 'Search enumerated types' }
)
useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => {
setSearch('')
})
const onConfirmDeleteType = () => {
if (typeToDelete === undefined) return console.error('No enumerated type selected')
if (project?.ref === undefined) return console.error('Project ref required')
@@ -112,18 +131,29 @@ export const EnumeratedTypes = () => {
<div className="space-y-4">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-2 flex-wrap">
<div className="flex flex-col lg:flex-row lg:items-center gap-2 flex-wrap">
<SchemaSelector
className="w-full lg:w-[180px]"
size="tiny"
showError={false}
selectedSchemaName={selectedSchema}
onSelectSchema={setSelectedSchema}
/>
<Shortcut
id={SHORTCUT_IDS.LIST_PAGE_FOCUS_SCHEMA}
onTrigger={() => setSchemaSelectorOpen(true)}
side="bottom"
tooltipOpen={schemaSelectorOpen ? false : undefined}
>
<SchemaSelector
className="w-full lg:w-[180px]"
size="tiny"
showError={false}
selectedSchemaName={selectedSchema}
onSelectSchema={setSelectedSchema}
open={schemaSelectorOpen}
onOpenChange={setSchemaSelectorOpen}
/>
</Shortcut>
<Input
ref={searchInputRef}
size="tiny"
value={search}
className="w-full lg:w-52"
onChange={(e) => setSearch(e.target.value)}
onKeyDown={onSearchInputEscape(search, setSearch)}
placeholder="Search for a type"
icon={<Search />}
/>
@@ -132,13 +162,20 @@ export const EnumeratedTypes = () => {
<div className="flex items-center gap-2">
<DocsButton href="https://www.postgresql.org/docs/current/datatype-enum.html" />
{!isSchemaLocked && (
<Button
className="ml-auto flex-1"
type="primary"
onClick={() => setShowCreateTypePanel(true)}
<Shortcut
id={SHORTCUT_IDS.LIST_PAGE_NEW_ITEM}
label="Create new enumerated type"
onTrigger={() => setShowCreateTypePanel(true)}
side="bottom"
>
Create type
</Button>
<Button
className="ml-auto flex-1"
type="primary"
onClick={() => setShowCreateTypePanel(true)}
>
Create type
</Button>
</Shortcut>
)}
</div>
</div>
@@ -2,7 +2,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { isNull, partition } from 'lodash'
import { AlertCircle, Search } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import {
Card,
Input,
@@ -23,11 +23,28 @@ import { NoSearchResults } from '@/components/ui/NoSearchResults'
import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { onSearchInputEscape } from '@/lib/keyboard'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
export const Extensions = () => {
const { filter } = useParams()
const { data: project } = useSelectedProjectQuery()
const [filterString, setFilterString] = useState<string>('')
const searchInputRef = useRef<HTMLInputElement>(null)
useShortcut(
SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH,
() => {
searchInputRef.current?.focus()
searchInputRef.current?.select()
},
{ label: 'Search extensions' }
)
useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => {
setFilterString('')
})
const { data = [], isPending: isLoading } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
@@ -63,10 +80,12 @@ export const Extensions = () => {
<>
<div className="mb-4">
<Input
inputRef={searchInputRef}
size="tiny"
placeholder="Search for an extension"
value={filterString}
onChange={(e) => setFilterString(e.target.value)}
onKeyDown={onSearchInputEscape(filterString, setFilterString)}
className="w-52"
icon={<Search />}
/>
@@ -1,9 +1,18 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Search } from 'lucide-react'
import { parseAsBoolean, parseAsJson, parseAsString, useQueryState } from 'nuqs'
import { useEffect } from 'react'
import { useEffect, useRef, useState } from 'react'
import { toast } from 'sonner'
import { AiIconAnimation, Card, Table, TableBody, TableHead, TableHeader, TableRow } from 'ui'
import {
AiIconAnimation,
Button,
Card,
Table,
TableBody,
TableHead,
TableHeader,
TableRow,
} from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
@@ -20,6 +29,7 @@ import ProductEmptyState from '@/components/to-be-cleaned/ProductEmptyState'
import AlertError from '@/components/ui/AlertError'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import SchemaSelector from '@/components/ui/SchemaSelector'
import { Shortcut } from '@/components/ui/Shortcut'
import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper'
import { useDatabaseFunctionDeleteMutation } from '@/data/database-functions/database-functions-delete-mutation'
import type { DatabaseFunction } from '@/data/database-functions/database-functions-query'
@@ -29,8 +39,11 @@ import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useIsProtectedSchema } from '@/hooks/useProtectedSchemas'
import { onSearchInputEscape } from '@/lib/keyboard'
import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
import { useEditorPanelStateSnapshot } from '@/state/editor-panel-state'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
const createFunctionSnippet = `create function function_name()
@@ -107,6 +120,9 @@ export const FunctionsList = () => {
parseAsJson(selectFilterSchema.parse)
)
const [schemaSelectorOpen, setSchemaSelectorOpen] = useState(false)
const searchInputRef = useRef<HTMLInputElement>(null)
const { can: canCreateFunctions } = useAsyncCheckPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'functions'
@@ -114,6 +130,23 @@ export const FunctionsList = () => {
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
const canAddFunctions = canCreateFunctions && !isSchemaLocked
useShortcut(
SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH,
() => {
searchInputRef.current?.focus()
searchInputRef.current?.select()
},
{ label: 'Search functions' }
)
useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => {
setFilterString('')
setReturnTypeFilter(null)
setSecurityFilter(null)
})
// [Joshen] This is to preload the data for the Schema Selector
useSchemasQuery({
projectRef: project?.ref,
@@ -236,23 +269,34 @@ export const FunctionsList = () => {
<div className="w-full space-y-4">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-2 flex-wrap">
<div className="flex flex-col lg:flex-row lg:items-center gap-2">
<SchemaSelector
className="w-full lg:w-[180px]"
size="tiny"
showError={false}
selectedSchemaName={selectedSchema}
onSelectSchema={(schema) => {
setFilterString('')
setSelectedSchema(schema)
}}
/>
<Shortcut
id={SHORTCUT_IDS.LIST_PAGE_FOCUS_SCHEMA}
onTrigger={() => setSchemaSelectorOpen(true)}
side="bottom"
tooltipOpen={schemaSelectorOpen ? false : undefined}
>
<SchemaSelector
className="w-full lg:w-[180px]"
size="tiny"
showError={false}
selectedSchemaName={selectedSchema}
onSelectSchema={(schema) => {
setFilterString('')
setSelectedSchema(schema)
}}
open={schemaSelectorOpen}
onOpenChange={setSchemaSelectorOpen}
/>
</Shortcut>
<Input
ref={searchInputRef}
placeholder="Search for a function"
size="tiny"
icon={<Search />}
value={filterString}
className="w-full lg:w-52"
onChange={(e) => setFilterString(e.target.value)}
onKeyDown={onSearchInputEscape(filterString, setFilterString)}
/>
<ReportsSelectFilter
label="Return Type"
@@ -275,21 +319,31 @@ export const FunctionsList = () => {
<div className="flex items-center gap-x-2">
{!isSchemaLocked && (
<>
<ButtonTooltip
disabled={!canCreateFunctions}
onClick={() => createFunction()}
className="grow"
tooltip={{
content: {
side: 'bottom',
text: !canCreateFunctions
? 'You need additional permissions to create functions'
: undefined,
},
}}
>
Create a new function
</ButtonTooltip>
{canAddFunctions ? (
<Shortcut
id={SHORTCUT_IDS.LIST_PAGE_NEW_ITEM}
label="Create new function"
onTrigger={() => createFunction()}
side="bottom"
>
<Button className="grow" onClick={() => createFunction()}>
Create a new function
</Button>
</Shortcut>
) : (
<ButtonTooltip
disabled
className="grow"
tooltip={{
content: {
side: 'bottom',
text: 'You need additional permissions to create functions',
},
}}
>
Create a new function
</ButtonTooltip>
)}
<ButtonTooltip
type="default"
disabled={!canCreateFunctions}
@@ -2,7 +2,7 @@ import { useParams } from 'common'
import { sortBy } from 'lodash'
import { AlertCircle, Search, Trash } from 'lucide-react'
import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs'
import { useEffect } from 'react'
import { useEffect, useRef, useState } from 'react'
import { toast } from 'sonner'
import {
Button,
@@ -24,18 +24,24 @@ import { CreateIndexSidePanel } from './CreateIndexSidePanel'
import AlertError from '@/components/ui/AlertError'
import CodeEditor from '@/components/ui/CodeEditor/CodeEditor'
import SchemaSelector from '@/components/ui/SchemaSelector'
import { Shortcut } from '@/components/ui/Shortcut'
import { useDatabaseIndexDeleteMutation } from '@/data/database-indexes/index-delete-mutation'
import { useIndexesQuery, type DatabaseIndex } from '@/data/database-indexes/indexes-query'
import { useSchemasQuery } from '@/data/database/schemas-query'
import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useIsProtectedSchema } from '@/hooks/useProtectedSchemas'
import { onSearchInputEscape } from '@/lib/keyboard'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
export const Indexes = () => {
const { data: project } = useSelectedProjectQuery()
const { schema: urlSchema, table } = useParams()
const [search, setSearch] = useQueryState('search', parseAsString.withDefault(''))
const [schemaSelectorOpen, setSchemaSelectorOpen] = useState(false)
const searchInputRef = useRef<HTMLInputElement>(null)
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
const {
@@ -84,6 +90,19 @@ export const Indexes = () => {
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
useShortcut(
SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH,
() => {
searchInputRef.current?.focus()
searchInputRef.current?.select()
},
{ label: 'Search indexes' }
)
useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => {
setSearch('')
})
const sortedIndexes = sortBy(allIndexes ?? [], (index) => index.name.toLocaleLowerCase())
const indexes =
search.length > 0
@@ -138,32 +157,51 @@ export const Indexes = () => {
</div>
)}
{isSuccessSchemas && (
<SchemaSelector
className="w-full lg:w-[180px]"
size="tiny"
showError={false}
selectedSchemaName={selectedSchema}
onSelectSchema={setSelectedSchema}
/>
<Shortcut
id={SHORTCUT_IDS.LIST_PAGE_FOCUS_SCHEMA}
onTrigger={() => setSchemaSelectorOpen(true)}
side="bottom"
tooltipOpen={schemaSelectorOpen ? false : undefined}
>
<SchemaSelector
className="w-full lg:w-[180px]"
size="tiny"
showError={false}
selectedSchemaName={selectedSchema}
onSelectSchema={setSelectedSchema}
open={schemaSelectorOpen}
onOpenChange={setSchemaSelectorOpen}
/>
</Shortcut>
)}
<Input
ref={searchInputRef}
size="tiny"
value={search}
className="w-full lg:w-52"
onChange={(e) => setSearch(e.target.value)}
onKeyDown={onSearchInputEscape(search, setSearch)}
placeholder="Search for an index"
icon={<Search />}
/>
{!isSchemaLocked && (
<Button
className="ml-auto grow lg:grow-0"
type="primary"
onClick={() => setShowCreateIndex(true)}
disabled={!isSuccessSchemas}
<Shortcut
id={SHORTCUT_IDS.LIST_PAGE_NEW_ITEM}
label="Create new index"
onTrigger={() => setShowCreateIndex(true)}
options={{ enabled: isSuccessSchemas }}
side="bottom"
>
Create index
</Button>
<Button
className="ml-auto grow lg:grow-0"
type="primary"
onClick={() => setShowCreateIndex(true)}
disabled={!isSuccessSchemas}
>
Create index
</Button>
</Shortcut>
)}
</div>
@@ -2,7 +2,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { AlertCircle, Info, Search } from 'lucide-react'
import Link from 'next/link'
import { useState } from 'react'
import { useRef, useState } from 'react'
import { toast } from 'sonner'
import {
Button,
@@ -29,6 +29,9 @@ import { useDatabasePublicationsQuery } from '@/data/database-publications/datab
import { useDatabasePublicationUpdateMutation } from '@/data/database-publications/database-publications-update-mutation'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { onSearchInputEscape } from '@/lib/keyboard'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
interface PublicationEvent {
event: string
@@ -39,6 +42,20 @@ export const PublicationsList = () => {
const { ref } = useParams()
const { data: project } = useSelectedProjectQuery()
const [filterString, setFilterString] = useState<string>('')
const searchInputRef = useRef<HTMLInputElement>(null)
useShortcut(
SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH,
() => {
searchInputRef.current?.focus()
searchInputRef.current?.select()
},
{ label: 'Search publications' }
)
useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => {
setFilterString('')
})
const {
data = [],
@@ -99,12 +116,14 @@ export const PublicationsList = () => {
<div className="flex items-center justify-between">
<div className="flex items-center">
<Input
ref={searchInputRef}
size="tiny"
icon={<Search />}
className="w-48"
placeholder="Search for a publication"
value={filterString}
onChange={(e) => setFilterString(e.target.value)}
onKeyDown={onSearchInputEscape(filterString, setFilterString)}
/>
</div>
{isPermissionsLoaded && !canUpdatePublications && (
@@ -2,7 +2,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { partition, sortBy } from 'lodash'
import { Plus, Search, X } from 'lucide-react'
import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs'
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { toast } from 'sonner'
import { Badge, Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
@@ -14,12 +14,16 @@ import { RoleRowSkeleton } from './RoleRowSkeleton'
import { SUPABASE_ROLES } from './Roles.constants'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { NoSearchResults } from '@/components/ui/NoSearchResults'
import { Shortcut } from '@/components/ui/Shortcut'
import { SparkBar } from '@/components/ui/SparkBar'
import { useDatabaseRoleDeleteMutation } from '@/data/database-roles/database-role-delete-mutation'
import { useDatabaseRolesQuery } from '@/data/database-roles/database-roles-query'
import { useMaxConnectionsQuery } from '@/data/database/max-connections-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { onSearchInputEscape } from '@/lib/keyboard'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
type SUPABASE_ROLE = (typeof SUPABASE_ROLES)[number]
@@ -28,12 +32,27 @@ export const RolesList = () => {
const [filterString, setFilterString] = useState('')
const [filterType, setFilterType] = useState<'all' | 'active'>('all')
const searchInputRef = useRef<HTMLInputElement>(null)
const { can: canUpdateRoles } = useAsyncCheckPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'roles'
)
useShortcut(
SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH,
() => {
searchInputRef.current?.focus()
searchInputRef.current?.select()
},
{ label: 'Search roles' }
)
useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => {
setFilterString('')
setFilterType('all')
})
const { data: maxConnData } = useMaxConnectionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
@@ -107,12 +126,14 @@ export const RolesList = () => {
<div className="mb-4 flex items-center justify-between gap-2 flex-wrap">
<div className="flex items-center space-x-4">
<Input
ref={searchInputRef}
size="tiny"
className="w-52"
placeholder="Search for a role"
icon={<Search />}
value={filterString}
onChange={(event) => setFilterString(event.target.value)}
onKeyDown={onSearchInputEscape(filterString, setFilterString)}
actions={
filterString && (
<Button
@@ -191,22 +212,36 @@ export const RolesList = () => {
))}
</TooltipContent>
</Tooltip>
<ButtonTooltip
type="primary"
disabled={!canUpdateRoles}
icon={<Plus size={12} />}
onClick={() => setIsCreatingRole(true)}
tooltip={{
content: {
side: 'bottom',
text: !canUpdateRoles
? 'You need additional permissions to add a new role'
: undefined,
},
}}
>
Add role
</ButtonTooltip>
{canUpdateRoles ? (
<Shortcut
id={SHORTCUT_IDS.LIST_PAGE_NEW_ITEM}
label="Add new role"
onTrigger={() => setIsCreatingRole(true)}
side="bottom"
>
<Button
type="primary"
icon={<Plus size={12} />}
onClick={() => setIsCreatingRole(true)}
>
Add role
</Button>
</Shortcut>
) : (
<ButtonTooltip
type="primary"
disabled
icon={<Plus size={12} />}
tooltip={{
content: {
side: 'bottom',
text: 'You need additional permissions to add a new role',
},
}}
>
Add role
</ButtonTooltip>
)}
</div>
</div>
@@ -6,7 +6,7 @@ import { Check, Copy, Edit, Eye, Filter, MoreVertical, Plus, Search, Trash, X }
import Link from 'next/link'
import { useRouter } from 'next/router'
import { parseAsString, useQueryState } from 'nuqs'
import { useState } from 'react'
import { useRef, useState } from 'react'
import {
Button,
Card,
@@ -42,6 +42,7 @@ import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip'
import { EntityTypeIcon } from '@/components/ui/EntityTypeIcon'
import SchemaSelector from '@/components/ui/SchemaSelector'
import { Shortcut } from '@/components/ui/Shortcut'
import { useDatabasePublicationsQuery } from '@/data/database-publications/database-publications-query'
import { ENTITY_TYPE } from '@/data/entity-types/entity-type-constants'
import { useForeignTablesQuery } from '@/data/foreign-tables/foreign-tables-query'
@@ -53,6 +54,9 @@ import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useIsProtectedSchema } from '@/hooks/useProtectedSchemas'
import { onSearchInputEscape } from '@/lib/keyboard'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
interface TableListProps {
onAddTable: () => void
@@ -77,6 +81,8 @@ export const TableList = ({
const [filterString, setFilterString] = useQueryState('search', parseAsString.withDefault(''))
const [visibleTypes, setVisibleTypes] = useState<string[]>(Object.values(ENTITY_TYPE))
const [schemaSelectorOpen, setSchemaSelectorOpen] = useState(false)
const searchInputRef = useRef<HTMLInputElement>(null)
const { can: canUpdateTables } = useAsyncCheckPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
@@ -187,6 +193,22 @@ export const TableList = ({
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
const canAddTables = canUpdateTables && !isSchemaLocked
useShortcut(
SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH,
() => {
searchInputRef.current?.focus()
searchInputRef.current?.select()
},
{ label: 'Search tables' }
)
useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => {
setVisibleTypes(Object.values(ENTITY_TYPE))
setFilterString('')
})
const error = tablesError || viewsError || materializedViewsError || foreignTablesError
const isError = isErrorTables || isErrorViews || isErrorMaterializedViews || isErrorForeignTables
const isLoading =
@@ -209,13 +231,22 @@ export const TableList = ({
<div className="flex flex-col gap-y-4">
<div className="flex flex-col lg:flex-row lg:items-center gap-2 flex-wrap">
<div className="flex gap-2 items-center">
<SchemaSelector
className="grow lg:grow-0 w-[180px]"
size="tiny"
showError={false}
selectedSchemaName={selectedSchema}
onSelectSchema={setSelectedSchema}
/>
<Shortcut
id={SHORTCUT_IDS.LIST_PAGE_FOCUS_SCHEMA}
onTrigger={() => setSchemaSelectorOpen(true)}
side="bottom"
tooltipOpen={schemaSelectorOpen ? false : undefined}
>
<SchemaSelector
className="grow lg:grow-0 w-[180px]"
size="tiny"
showError={false}
selectedSchemaName={selectedSchema}
onSelectSchema={setSelectedSchema}
open={schemaSelectorOpen}
onOpenChange={setSchemaSelectorOpen}
/>
</Shortcut>
<Popover_Shadcn_>
<PopoverTrigger_Shadcn_ asChild>
<Button
@@ -265,32 +296,43 @@ export const TableList = ({
</div>
<div className="flex grow justify-between gap-2 items-center">
<Input
ref={searchInputRef}
size="tiny"
containerClassName="grow lg:grow-0 w-52"
placeholder="Search for a table"
value={filterString}
onChange={(e) => setFilterString(e.target.value)}
onKeyDown={onSearchInputEscape(filterString, setFilterString)}
icon={<Search />}
/>
{!isSchemaLocked && (
<ButtonTooltip
className="w-auto ml-auto"
icon={<Plus />}
disabled={!canUpdateTables}
onClick={() => onAddTable()}
tooltip={{
content: {
side: 'bottom',
text: !canUpdateTables
? 'You need additional permissions to create tables'
: undefined,
},
}}
>
New table
</ButtonTooltip>
)}
{!isSchemaLocked &&
(canAddTables ? (
<Shortcut
id={SHORTCUT_IDS.LIST_PAGE_NEW_ITEM}
label="Create new table"
onTrigger={() => onAddTable()}
side="bottom"
>
<Button className="w-auto ml-auto" icon={<Plus />} onClick={() => onAddTable()}>
New table
</Button>
</Shortcut>
) : (
<ButtonTooltip
className="w-auto ml-auto"
icon={<Plus />}
disabled
tooltip={{
content: {
side: 'bottom',
text: 'You need additional permissions to create tables',
},
}}
>
New table
</ButtonTooltip>
))}
</div>
</div>
@@ -1,9 +1,9 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { DatabaseZap, Plus, Search } from 'lucide-react'
import { parseAsJson, parseAsString, useQueryState } from 'nuqs'
import { useMemo, useState } from 'react'
import { useMemo, useRef, useState } from 'react'
import { toast } from 'sonner'
import { Card, Input, Table, TableBody, TableHead, TableHeader, TableRow } from 'ui'
import { Button, Card, Input, Table, TableBody, TableHead, TableHeader, TableRow } from 'ui'
import { EmptyStatePresentational } from 'ui-patterns'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
@@ -19,6 +19,7 @@ import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/L
import AlertError from '@/components/ui/AlertError'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { DocsButton } from '@/components/ui/DocsButton'
import { Shortcut } from '@/components/ui/Shortcut'
import { useDatabaseEventTriggerDeleteMutation } from '@/data/database-event-triggers/database-event-trigger-delete-mutation'
import {
useDatabaseEventTriggersQuery,
@@ -27,9 +28,12 @@ import {
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { DOCS_URL } from '@/lib/constants'
import { onSearchInputEscape } from '@/lib/keyboard'
import { EMPTY_ARR } from '@/lib/void'
import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
import { useEditorPanelStateSnapshot } from '@/state/editor-panel-state'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
const DEFAULT_OWNER_FILTER = ['postgres']
@@ -46,6 +50,7 @@ export const EventTriggersList = () => {
)
const ownerFilterValue = ownerFilter ?? DEFAULT_OWNER_FILTER
const [triggerToDelete, setTriggerToDelete] = useState<DatabaseEventTrigger | null>(null)
const searchInputRef = useRef<HTMLInputElement>(null)
const { openSidebar } = useSidebarManagerSnapshot()
const aiSnap = useAiAssistantStateSnapshot()
const {
@@ -144,6 +149,20 @@ export const EventTriggersList = () => {
return uniqueOwners.includes('postgres') ? uniqueOwners : ['postgres', ...uniqueOwners]
}, [eventTriggers])
useShortcut(
SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH,
() => {
searchInputRef.current?.focus()
searchInputRef.current?.select()
},
{ label: 'Search event triggers' }
)
useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => {
setFilterString('')
setOwnerFilter(null)
})
const showEmptyState = useMemo(() => {
const hasPostgresOwnerTriggers = eventTriggers.some((trigger) => trigger.owner === 'postgres')
return (
@@ -166,12 +185,14 @@ export const EventTriggersList = () => {
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-2 flex-wrap">
<div className="flex flex-col lg:flex-row lg:items-center gap-2 flex-wrap">
<Input
inputRef={searchInputRef}
placeholder="Search for an event trigger"
size="tiny"
icon={<Search />}
value={filterString}
className="w-full lg:w-64"
onChange={(e) => setFilterString(e.target.value)}
onKeyDown={onSearchInputEscape(filterString, setFilterString)}
/>
<ReportsSelectFilter
label="Owner"
@@ -186,22 +207,32 @@ export const EventTriggersList = () => {
</div>
<div className="flex items-center gap-2">
<DocsButton href={`${DOCS_URL}/guides/database/postgres/event-triggers`} />
<ButtonTooltip
type="primary"
icon={<Plus size={12} />}
disabled={!canUpdateEventTriggers}
onClick={createEventTrigger}
tooltip={{
content: {
side: 'bottom',
text: !canUpdateEventTriggers
? 'You need additional permissions to add a new event trigger'
: undefined,
},
}}
>
New trigger
</ButtonTooltip>
{canUpdateEventTriggers ? (
<Shortcut
id={SHORTCUT_IDS.LIST_PAGE_NEW_ITEM}
label="Create new event trigger"
onTrigger={createEventTrigger}
side="bottom"
>
<Button type="primary" icon={<Plus size={12} />} onClick={createEventTrigger}>
New trigger
</Button>
</Shortcut>
) : (
<ButtonTooltip
type="primary"
icon={<Plus size={12} />}
disabled
tooltip={{
content: {
side: 'bottom',
text: 'You need additional permissions to add a new event trigger',
},
}}
>
New trigger
</ButtonTooltip>
)}
</div>
</div>
@@ -2,7 +2,7 @@ import type { PostgresTrigger } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { DatabaseZap, Search } from 'lucide-react'
import { parseAsBoolean, parseAsJson, parseAsString, useQueryState } from 'nuqs'
import { useEffect } from 'react'
import { useEffect, useRef, useState } from 'react'
import { toast } from 'sonner'
import { Card, Input, Table, TableBody, TableHead, TableHeader, TableRow } from 'ui'
import { EmptyStatePresentational } from 'ui-patterns'
@@ -22,6 +22,7 @@ import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/L
import AlertError from '@/components/ui/AlertError'
import { DocsButton } from '@/components/ui/DocsButton'
import SchemaSelector from '@/components/ui/SchemaSelector'
import { Shortcut } from '@/components/ui/Shortcut'
import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper'
import { useDatabaseTriggerDeleteMutation } from '@/data/database-triggers/database-trigger-delete-mutation'
import { useDatabaseTriggersQuery } from '@/data/database-triggers/database-triggers-query'
@@ -31,7 +32,10 @@ import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useIsProtectedSchema, useProtectedSchemas } from '@/hooks/useProtectedSchemas'
import { DOCS_URL } from '@/lib/constants'
import { onSearchInputEscape } from '@/lib/keyboard'
import { useEditorPanelStateSnapshot } from '@/state/editor-panel-state'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
export const TriggersList = () => {
@@ -45,6 +49,9 @@ export const TriggersList = () => {
parseAsJson(selectFilterSchema.parse).withDefault([])
)
const [schemaSelectorOpen, setSchemaSelectorOpen] = useState(false)
const searchInputRef = useRef<HTMLInputElement>(null)
const isInlineEditorEnabled = useIsInlineEditorEnabled()
const {
templates: editorPanelTemplates,
@@ -160,6 +167,30 @@ execute function function_name();`)
const schemaTriggers = triggers.filter((x) => x.schema === selectedSchema)
const tables = Array.from(new Set(schemaTriggers.map((x) => x.table))).sort()
const canAddTriggers = canCreateTriggers && !isSchemaLocked && hasTables
useShortcut(
SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH,
() => {
searchInputRef.current?.focus()
searchInputRef.current?.select()
},
{ label: 'Search triggers' }
)
useShortcut(
SHORTCUT_IDS.LIST_PAGE_NEW_ITEM,
() => {
createTrigger()
},
{ enabled: canAddTriggers, label: 'Create new trigger' }
)
useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => {
setFilterString('')
setTablesFilter([])
})
useEffect(() => {
if (isSuccess && !!triggerIdToEdit && !triggerToEdit) {
toast('Trigger not found')
@@ -194,20 +225,31 @@ execute function function_name();`)
<div className="space-y-4">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-2 flex-wrap">
<div className="flex flex-col lg:flex-row lg:items-center gap-2 flex-wrap">
<SchemaSelector
className="w-full lg:w-[180px]"
size="tiny"
showError={false}
selectedSchemaName={selectedSchema}
onSelectSchema={setSelectedSchema}
/>
<Shortcut
id={SHORTCUT_IDS.LIST_PAGE_FOCUS_SCHEMA}
onTrigger={() => setSchemaSelectorOpen(true)}
side="bottom"
tooltipOpen={schemaSelectorOpen ? false : undefined}
>
<SchemaSelector
className="w-full lg:w-[180px]"
size="tiny"
showError={false}
selectedSchemaName={selectedSchema}
onSelectSchema={setSelectedSchema}
open={schemaSelectorOpen}
onOpenChange={setSchemaSelectorOpen}
/>
</Shortcut>
<Input
inputRef={searchInputRef}
placeholder="Search for a trigger"
size="tiny"
icon={<Search />}
value={filterString}
className="w-full lg:w-52"
onChange={(e) => setFilterString(e.target.value)}
onKeyDown={onSearchInputEscape(filterString, setFilterString)}
/>
<ReportsSelectFilter
label="Table"
@@ -33,6 +33,7 @@ const GROUP_LABELS: Record<string, string> = {
'command-menu': 'Command Menu',
'data-table': 'Data Tables',
'inline-editor': 'Inline Editor',
'list-page': 'List pages',
nav: 'Navigation',
'operation-queue': 'Operation Queue',
results: 'Results',
@@ -52,6 +53,7 @@ const GROUP_ORDER = [
'data-table',
'table-editor',
'schema-visualizer',
'list-page',
'action-bar',
'operation-queue',
'unified-logs',
+1 -1
View File
@@ -67,7 +67,7 @@ export const Shortcut = ({
label,
tooltipOpen,
}: ShortcutProps) => {
useShortcut(id, onTrigger, options)
useShortcut(id, onTrigger, label !== undefined ? { ...options, label } : options)
return (
<ShortcutTooltip
+30
View File
@@ -0,0 +1,30 @@
import type { KeyboardEvent } from 'react'
type ClearableInputElement = HTMLInputElement | HTMLTextAreaElement
/**
* Staged-Escape handler for search/filter inputs:
* - Escape while the input has a value → clear the value (keeps focus)
* - Escape while the input is empty → blur the input
*
* Stops propagation on Escape so the keystroke doesn't bubble to dialog/popover
* close handlers when the consumer is nested inside one.
*
* @example
* <Input
* value={query}
* onChange={(e) => setQuery(e.target.value)}
* onKeyDown={onSearchInputEscape(query, setQuery)}
* />
*/
export const onSearchInputEscape =
<T extends ClearableInputElement>(value: string, onClear: (next: string) => void) =>
(event: KeyboardEvent<T>) => {
if (event.key !== 'Escape') return
event.stopPropagation()
if (value.length > 0) {
onClear('')
} else {
event.currentTarget.blur()
}
}
+7
View File
@@ -1,3 +1,4 @@
import { LIST_PAGE_SHORTCUT_IDS, listPageRegistry } from './registry/list-page'
import {
SCHEMA_VISUALIZER_SHORTCUT_IDS,
schemaVisualizerRegistry,
@@ -60,6 +61,9 @@ export const SHORTCUT_IDS = {
// Schema visualizer shortcuts
...SCHEMA_VISUALIZER_SHORTCUT_IDS,
// Shared list-page shortcuts (database/* listing pages, etc.)
...LIST_PAGE_SHORTCUT_IDS,
} as const
/**
@@ -314,4 +318,7 @@ export const SHORTCUT_DEFINITIONS: Record<ShortcutId, ShortcutDefinition> = {
// Schema visualizer shortcut registration
...schemaVisualizerRegistry,
// Shared list-page shortcut registration
...listPageRegistry,
}
@@ -0,0 +1,51 @@
import { RegistryDefinations } from '../types'
/**
* Shared shortcuts for "list pages" — the conventional Database/* page shape
* with a schema selector, a search input, a primary "create new" action, and
* one or more facet filters.
*
* Pages register their per-page handlers via `useShortcut`. Pass a `label`
* override to make the Cmd+K entry and hover tooltip read e.g. "Search tables"
* instead of the generic "Focus search" stored here.
*/
export const LIST_PAGE_SHORTCUT_IDS = {
LIST_PAGE_FOCUS_SCHEMA: 'list-page.focus-schema',
LIST_PAGE_FOCUS_SEARCH: 'list-page.focus-search',
LIST_PAGE_NEW_ITEM: 'list-page.new-item',
LIST_PAGE_RESET_FILTERS: 'list-page.reset-filters',
}
export type ListPageShortcutId =
(typeof LIST_PAGE_SHORTCUT_IDS)[keyof typeof LIST_PAGE_SHORTCUT_IDS]
export const listPageRegistry: RegistryDefinations<ListPageShortcutId> = {
[LIST_PAGE_SHORTCUT_IDS.LIST_PAGE_FOCUS_SCHEMA]: {
id: LIST_PAGE_SHORTCUT_IDS.LIST_PAGE_FOCUS_SCHEMA,
label: 'Open schema selector',
sequence: ['O', 'S'],
showInSettings: false,
options: { ignoreInputs: true, registerInCommandMenu: true },
},
[LIST_PAGE_SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH]: {
id: LIST_PAGE_SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH,
label: 'Focus search',
sequence: ['Shift+F'],
showInSettings: false,
options: { registerInCommandMenu: true },
},
[LIST_PAGE_SHORTCUT_IDS.LIST_PAGE_NEW_ITEM]: {
id: LIST_PAGE_SHORTCUT_IDS.LIST_PAGE_NEW_ITEM,
label: 'Create new item',
sequence: ['Shift+N'],
showInSettings: false,
options: { registerInCommandMenu: true },
},
[LIST_PAGE_SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS]: {
id: LIST_PAGE_SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS,
label: 'Reset filters',
sequence: ['F', 'C'],
showInSettings: false,
options: { ignoreInputs: true, registerInCommandMenu: true },
},
}
+10
View File
@@ -55,6 +55,16 @@ export interface ShortcutOptions {
* context-specific shortcuts that only make sense in certain views.
*/
registerInCommandMenu?: boolean
/**
* Override the registry's default `label` for the duration of this mount.
* Flows to the Cmd+K command-menu entry and the hover tooltip rendered via
* `<Shortcut>`. Use for shared shortcuts whose contextual label changes per
* page (e.g. `list-page.focus-search` rendering as "Search tables" on one
* page and "Search functions" on another). Leave `undefined` to use the
* registry default.
*/
label?: string
}
/**
+3 -2
View File
@@ -72,6 +72,7 @@ export function useShortcut(id: ShortcutId, callback: () => void, options?: Shor
const ignoreInputs = options?.ignoreInputs ?? def.options?.ignoreInputs
const registerInCommandMenu =
options?.registerInCommandMenu ?? def.options?.registerInCommandMenu ?? false
const label = options?.label ?? def.label
// Only include `ignoreInputs` when set. The library resolves it to a concrete
// boolean at register time (false for Meta/Ctrl/Escape, true otherwise), but
@@ -86,7 +87,7 @@ export function useShortcut(id: ShortcutId, callback: () => void, options?: Shor
// Handle overrides for command menu
const enabledInCommandMenu = enabled && registerInCommandMenu
const depsInCommandMenu = [enabled, def.label]
const depsInCommandMenu = [enabled, label]
const callbackRef = useLatest(callback)
const setCommandMenuOpen = useSetCommandMenuOpen()
const stableAction = useCallback(() => {
@@ -99,7 +100,7 @@ export function useShortcut(id: ShortcutId, callback: () => void, options?: Shor
[
{
id,
name: def.label,
name: label,
action: stableAction,
badge: () => (
<div className="flex items-center gap-1">
+20 -15
View File
@@ -1,8 +1,7 @@
'use client'
import { Label as LabelPrimitive, Slot as SlotPrimitive } from 'radix-ui'
import { AnimatePresence, motion } from 'framer-motion'
import { Label as LabelPrimitive, Slot as SlotPrimitive } from 'radix-ui'
import * as React from 'react'
import {
Controller,
@@ -182,31 +181,37 @@ const FormMessage = React.forwardRef<
FormMessage.displayName = 'FormMessage'
function FormInputGroupInput(props: InputProps) {
const FormInputGroupInput = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<InputGroupInput
ref={ref}
id={formItemId}
aria-describedby={!error ? formDescriptionId : `${formDescriptionId} ${formMessageId}`}
aria-invalid={!!error}
{...props}
/>
)
}
})
FormInputGroupInput.displayName = 'FormInputGroupInput'
function FormInputGroupTextArea(props: TextareaProps) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
const FormInputGroupTextArea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
(props, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<InputGroupTextarea
id={formItemId}
aria-describedby={!error ? formDescriptionId : `${formDescriptionId} ${formMessageId}`}
aria-invalid={!!error}
{...props}
/>
)
}
return (
<InputGroupTextarea
ref={ref}
id={formItemId}
aria-describedby={!error ? formDescriptionId : `${formDescriptionId} ${formMessageId}`}
aria-invalid={!!error}
{...props}
/>
)
}
)
FormInputGroupTextArea.displayName = 'FormInputGroupTextArea'
export {
Form,
@@ -159,9 +159,10 @@ function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
/*
* If you need to use this component inside a <Form>, use FormInputGroupInput instead.
*/
function InputGroupInput({ className, ...props }: InputProps) {
return (
const InputGroupInput = React.forwardRef<HTMLInputElement, InputProps>(
({ className, ...props }, ref) => (
<Input
ref={ref}
data-slot="input-group-control"
className={cn(
'flex-1 rounded-none border border-transparent -m-px bg-transparent shadow-none',
@@ -174,14 +175,16 @@ function InputGroupInput({ className, ...props }: InputProps) {
{...props}
/>
)
}
)
InputGroupInput.displayName = 'InputGroupInput'
/*
* If you need to use this component inside a <Form>, use FormInputGroupTextArea instead.
*/
function InputGroupTextarea({ className, ...props }: TextareaProps) {
return (
const InputGroupTextarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => (
<Textarea
ref={ref}
data-slot="input-group-control"
className={cn(
'flex-1 resize-none rounded-none border border-transparent bg-transparent py-0 shadow-none',
@@ -191,7 +194,8 @@ function InputGroupTextarea({ className, ...props }: TextareaProps) {
{...props}
/>
)
}
)
InputGroupTextarea.displayName = 'InputGroupTextarea'
export {
InputGroup,