diff --git a/apps/studio/components/interfaces/Database/EnumeratedTypes/EnumeratedTypes.tsx b/apps/studio/components/interfaces/Database/EnumeratedTypes/EnumeratedTypes.tsx index 419b4b7a77a..fb886c5c16c 100644 --- a/apps/studio/components/interfaces/Database/EnumeratedTypes/EnumeratedTypes.tsx +++ b/apps/studio/components/interfaces/Database/EnumeratedTypes/EnumeratedTypes.tsx @@ -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(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 = () => {
- + setSchemaSelectorOpen(true)} + side="bottom" + tooltipOpen={schemaSelectorOpen ? false : undefined} + > + + setSearch(e.target.value)} + onKeyDown={onSearchInputEscape(search, setSearch)} placeholder="Search for a type" icon={} /> @@ -132,13 +162,20 @@ export const EnumeratedTypes = () => {
{!isSchemaLocked && ( - + + )}
diff --git a/apps/studio/components/interfaces/Database/Extensions/Extensions.tsx b/apps/studio/components/interfaces/Database/Extensions/Extensions.tsx index 9d58a8c4761..0ea60b60e98 100644 --- a/apps/studio/components/interfaces/Database/Extensions/Extensions.tsx +++ b/apps/studio/components/interfaces/Database/Extensions/Extensions.tsx @@ -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('') + const searchInputRef = useRef(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 = () => { <>
setFilterString(e.target.value)} + onKeyDown={onSearchInputEscape(filterString, setFilterString)} className="w-52" icon={} /> diff --git a/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx b/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx index fb238fca1d9..030346a8ccf 100644 --- a/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx +++ b/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx @@ -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(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 = () => {
- { - setFilterString('') - setSelectedSchema(schema) - }} - /> + setSchemaSelectorOpen(true)} + side="bottom" + tooltipOpen={schemaSelectorOpen ? false : undefined} + > + { + setFilterString('') + setSelectedSchema(schema) + }} + open={schemaSelectorOpen} + onOpenChange={setSchemaSelectorOpen} + /> + } value={filterString} className="w-full lg:w-52" onChange={(e) => setFilterString(e.target.value)} + onKeyDown={onSearchInputEscape(filterString, setFilterString)} /> {
{!isSchemaLocked && ( <> - createFunction()} - className="grow" - tooltip={{ - content: { - side: 'bottom', - text: !canCreateFunctions - ? 'You need additional permissions to create functions' - : undefined, - }, - }} - > - Create a new function - + {canAddFunctions ? ( + createFunction()} + side="bottom" + > + + + ) : ( + + Create a new function + + )} { const { data: project } = useSelectedProjectQuery() const { schema: urlSchema, table } = useParams() const [search, setSearch] = useQueryState('search', parseAsString.withDefault('')) + const [schemaSelectorOpen, setSchemaSelectorOpen] = useState(false) + const searchInputRef = useRef(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 = () => {
)} {isSuccessSchemas && ( - + setSchemaSelectorOpen(true)} + side="bottom" + tooltipOpen={schemaSelectorOpen ? false : undefined} + > + + )} setSearch(e.target.value)} + onKeyDown={onSearchInputEscape(search, setSearch)} placeholder="Search for an index" icon={} /> {!isSchemaLocked && ( - + + )}
diff --git a/apps/studio/components/interfaces/Database/Publications/PublicationsList.tsx b/apps/studio/components/interfaces/Database/Publications/PublicationsList.tsx index 9d5923dc799..f7e2820a335 100644 --- a/apps/studio/components/interfaces/Database/Publications/PublicationsList.tsx +++ b/apps/studio/components/interfaces/Database/Publications/PublicationsList.tsx @@ -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('') + const searchInputRef = useRef(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 = () => {
} className="w-48" placeholder="Search for a publication" value={filterString} onChange={(e) => setFilterString(e.target.value)} + onKeyDown={onSearchInputEscape(filterString, setFilterString)} />
{isPermissionsLoaded && !canUpdatePublications && ( diff --git a/apps/studio/components/interfaces/Database/Roles/RolesList.tsx b/apps/studio/components/interfaces/Database/Roles/RolesList.tsx index 031e578b0b5..c71c19c3532 100644 --- a/apps/studio/components/interfaces/Database/Roles/RolesList.tsx +++ b/apps/studio/components/interfaces/Database/Roles/RolesList.tsx @@ -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(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 = () => {
} value={filterString} onChange={(event) => setFilterString(event.target.value)} + onKeyDown={onSearchInputEscape(filterString, setFilterString)} actions={ filterString && ( + + ) : ( + } + tooltip={{ + content: { + side: 'bottom', + text: 'You need additional permissions to add a new role', + }, + }} + > + Add role + + )}
diff --git a/apps/studio/components/interfaces/Database/Tables/TableList.tsx b/apps/studio/components/interfaces/Database/Tables/TableList.tsx index a3f9d6c2f29..47cbd65cf83 100644 --- a/apps/studio/components/interfaces/Database/Tables/TableList.tsx +++ b/apps/studio/components/interfaces/Database/Tables/TableList.tsx @@ -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(Object.values(ENTITY_TYPE)) + const [schemaSelectorOpen, setSchemaSelectorOpen] = useState(false) + const searchInputRef = useRef(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 = ({
- + setSchemaSelectorOpen(true)} + side="bottom" + tooltipOpen={schemaSelectorOpen ? false : undefined} + > + + + + ) : ( + } + disabled + tooltip={{ + content: { + side: 'bottom', + text: 'You need additional permissions to create tables', + }, + }} + > + New table + + ))}
diff --git a/apps/studio/components/interfaces/Database/Triggers/EventTriggersList/EventTriggersList.tsx b/apps/studio/components/interfaces/Database/Triggers/EventTriggersList/EventTriggersList.tsx index 0b067512829..1e90ff40acf 100644 --- a/apps/studio/components/interfaces/Database/Triggers/EventTriggersList/EventTriggersList.tsx +++ b/apps/studio/components/interfaces/Database/Triggers/EventTriggersList/EventTriggersList.tsx @@ -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(null) + const searchInputRef = useRef(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 = () => {
} value={filterString} className="w-full lg:w-64" onChange={(e) => setFilterString(e.target.value)} + onKeyDown={onSearchInputEscape(filterString, setFilterString)} /> {
- } - disabled={!canUpdateEventTriggers} - onClick={createEventTrigger} - tooltip={{ - content: { - side: 'bottom', - text: !canUpdateEventTriggers - ? 'You need additional permissions to add a new event trigger' - : undefined, - }, - }} - > - New trigger - + {canUpdateEventTriggers ? ( + + + + ) : ( + } + disabled + tooltip={{ + content: { + side: 'bottom', + text: 'You need additional permissions to add a new event trigger', + }, + }} + > + New trigger + + )}
diff --git a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx index 9c4b333f189..ae824fa2332 100644 --- a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx +++ b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx @@ -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(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();`)
- + setSchemaSelectorOpen(true)} + side="bottom" + tooltipOpen={schemaSelectorOpen ? false : undefined} + > + + } value={filterString} className="w-full lg:w-52" onChange={(e) => setFilterString(e.target.value)} + onKeyDown={onSearchInputEscape(filterString, setFilterString)} /> = { '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', diff --git a/apps/studio/components/ui/Shortcut.tsx b/apps/studio/components/ui/Shortcut.tsx index d75439361d0..7cf020951f6 100644 --- a/apps/studio/components/ui/Shortcut.tsx +++ b/apps/studio/components/ui/Shortcut.tsx @@ -67,7 +67,7 @@ export const Shortcut = ({ label, tooltipOpen, }: ShortcutProps) => { - useShortcut(id, onTrigger, options) + useShortcut(id, onTrigger, label !== undefined ? { ...options, label } : options) return ( setQuery(e.target.value)} + * onKeyDown={onSearchInputEscape(query, setQuery)} + * /> + */ +export const onSearchInputEscape = + (value: string, onClear: (next: string) => void) => + (event: KeyboardEvent) => { + if (event.key !== 'Escape') return + event.stopPropagation() + if (value.length > 0) { + onClear('') + } else { + event.currentTarget.blur() + } + } diff --git a/apps/studio/state/shortcuts/registry.ts b/apps/studio/state/shortcuts/registry.ts index ca6a19266a5..7de55569616 100644 --- a/apps/studio/state/shortcuts/registry.ts +++ b/apps/studio/state/shortcuts/registry.ts @@ -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 = { // Schema visualizer shortcut registration ...schemaVisualizerRegistry, + + // Shared list-page shortcut registration + ...listPageRegistry, } diff --git a/apps/studio/state/shortcuts/registry/list-page.ts b/apps/studio/state/shortcuts/registry/list-page.ts new file mode 100644 index 00000000000..7198b9d1e81 --- /dev/null +++ b/apps/studio/state/shortcuts/registry/list-page.ts @@ -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 = { + [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 }, + }, +} diff --git a/apps/studio/state/shortcuts/types.ts b/apps/studio/state/shortcuts/types.ts index 9230415dff1..ab2b4fcb4b0 100644 --- a/apps/studio/state/shortcuts/types.ts +++ b/apps/studio/state/shortcuts/types.ts @@ -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 + * ``. 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 } /** diff --git a/apps/studio/state/shortcuts/useShortcut.tsx b/apps/studio/state/shortcuts/useShortcut.tsx index edb39809bf1..fc16bd4c139 100644 --- a/apps/studio/state/shortcuts/useShortcut.tsx +++ b/apps/studio/state/shortcuts/useShortcut.tsx @@ -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: () => (
diff --git a/packages/ui/src/components/shadcn/ui/form.tsx b/packages/ui/src/components/shadcn/ui/form.tsx index 0798fe686f3..25db5a4ff66 100644 --- a/packages/ui/src/components/shadcn/ui/form.tsx +++ b/packages/ui/src/components/shadcn/ui/form.tsx @@ -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((props, ref) => { const { error, formItemId, formDescriptionId, formMessageId } = useFormField() return ( ) -} +}) +FormInputGroupInput.displayName = 'FormInputGroupInput' -function FormInputGroupTextArea(props: TextareaProps) { - const { error, formItemId, formDescriptionId, formMessageId } = useFormField() +const FormInputGroupTextArea = React.forwardRef( + (props, ref) => { + const { error, formItemId, formDescriptionId, formMessageId } = useFormField() - return ( - - ) -} + return ( + + ) + } +) +FormInputGroupTextArea.displayName = 'FormInputGroupTextArea' export { Form, diff --git a/packages/ui/src/components/shadcn/ui/input-group.tsx b/packages/ui/src/components/shadcn/ui/input-group.tsx index 8f44d508003..cfeea39d653 100644 --- a/packages/ui/src/components/shadcn/ui/input-group.tsx +++ b/packages/ui/src/components/shadcn/ui/input-group.tsx @@ -159,9 +159,10 @@ function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) { /* * If you need to use this component inside a
, use FormInputGroupInput instead. */ -function InputGroupInput({ className, ...props }: InputProps) { - return ( +const InputGroupInput = React.forwardRef( + ({ className, ...props }, ref) => ( ) -} +) +InputGroupInput.displayName = 'InputGroupInput' /* * If you need to use this component inside a , use FormInputGroupTextArea instead. */ -function InputGroupTextarea({ className, ...props }: TextareaProps) { - return ( +const InputGroupTextarea = React.forwardRef( + ({ className, ...props }, ref) => (