mirror of
https://github.com/supabase/supabase.git
synced 2026-06-11 06:19:22 +08:00
- Most changes are related to either types or `useRef` usages (it now requires an initial value). - also updated `vaul` to its latest version and haven't noticed any change ([design-system demo](https://design-system-git-react-19-supabase.vercel.app/design-system/docs/components/drawer)) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Upgraded workspace to React 19. * **Bug Fixes** * Improved null-safety and ref handling across editors, UI components, shortcuts, and markdown/image rendering to reduce runtime errors. * Safer event/timeout/interval cleanup and more robust command/context handling. * **Chores** * Bumped vaul dependency versions. * **Documentation** * Type and TypeScript accuracy improvements for clearer developer feedback. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45886) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
188 lines
5.8 KiB
TypeScript
188 lines
5.8 KiB
TypeScript
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import {
|
|
parseAsArrayOf,
|
|
parseAsBoolean,
|
|
parseAsString,
|
|
parseAsStringEnum,
|
|
useQueryState,
|
|
} from 'nuqs'
|
|
import { Dispatch, RefObject, SetStateAction, useEffect, useState } from 'react'
|
|
import type { CellKeyboardEvent, DataGridHandle } from 'react-data-grid'
|
|
|
|
import { MAX_BULK_DELETE } from './Users.constants'
|
|
import type { User } from '@/data/auth/users-infinite-query'
|
|
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
|
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
|
|
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
|
|
import { useShortcut } from '@/state/shortcuts/useShortcut'
|
|
|
|
interface UseAuthUsersShortcutsParams {
|
|
gridRef: RefObject<DataGridHandle | null>
|
|
searchInputRef: RefObject<HTMLInputElement | null>
|
|
users: User[]
|
|
selectedUsers: Set<unknown>
|
|
setSelectedUsers: Dispatch<SetStateAction<Set<unknown>>>
|
|
setSearch: Dispatch<SetStateAction<string>>
|
|
onRefresh: () => void
|
|
}
|
|
|
|
interface GridCellKeyDownArgs {
|
|
mode: 'SELECT' | 'EDIT'
|
|
row: unknown
|
|
rowIdx: number
|
|
}
|
|
|
|
interface UseAuthUsersShortcutsResult {
|
|
onCellKeyDown: (args: GridCellKeyDownArgs, event: CellKeyboardEvent) => void
|
|
showDeleteModal: boolean
|
|
setShowDeleteModal: Dispatch<SetStateAction<boolean>>
|
|
}
|
|
|
|
export function useAuthUsersShortcuts({
|
|
gridRef,
|
|
searchInputRef,
|
|
users,
|
|
selectedUsers,
|
|
setSelectedUsers,
|
|
setSearch,
|
|
onRefresh,
|
|
}: UseAuthUsersShortcutsParams): UseAuthUsersShortcutsResult {
|
|
const [hasCellSelected, setHasCellSelected] = useState(false)
|
|
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
|
|
|
const [selectedId, setSelectedId] = useQueryState(
|
|
'show',
|
|
parseAsString.withOptions({ history: 'push', clearOnDefault: true })
|
|
)
|
|
const [, setFilterKeywords] = useQueryState('keywords', { defaultValue: '' })
|
|
const [, setFilterUserType] = useQueryState(
|
|
'userType',
|
|
parseAsStringEnum(['all', 'verified', 'unverified', 'anonymous']).withDefault('all')
|
|
)
|
|
const [, setSelectedProviders] = useQueryState(
|
|
'providers',
|
|
parseAsArrayOf(parseAsString, ',').withDefault([])
|
|
)
|
|
const [sortByValue, setSortByValue] = useQueryState('sortBy', {
|
|
defaultValue: 'created_at:desc',
|
|
})
|
|
const [, setInviteVisible] = useQueryState(
|
|
'invite',
|
|
parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
|
|
)
|
|
const [, setCreateVisible] = useQueryState(
|
|
'new',
|
|
parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
|
|
)
|
|
|
|
const { can: canCreateUsers } = useAsyncCheckPermissions(
|
|
PermissionAction.AUTH_EXECUTE,
|
|
'create_user'
|
|
)
|
|
const { can: canInviteUsers } = useAsyncCheckPermissions(
|
|
PermissionAction.AUTH_EXECUTE,
|
|
'invite_user'
|
|
)
|
|
const showSendInvitation = useIsFeatureEnabled('authentication:show_send_invitation')
|
|
|
|
useEffect(() => {
|
|
const el = gridRef.current?.element
|
|
if (!el) return
|
|
const onFocusIn = () => setHasCellSelected(true)
|
|
const onFocusOut = (event: FocusEvent) => {
|
|
if (!el.contains(event.relatedTarget as Node | null)) {
|
|
setHasCellSelected(false)
|
|
}
|
|
}
|
|
el.addEventListener('focusin', onFocusIn)
|
|
el.addEventListener('focusout', onFocusOut)
|
|
return () => {
|
|
el.removeEventListener('focusin', onFocusIn)
|
|
el.removeEventListener('focusout', onFocusOut)
|
|
}
|
|
}, [gridRef])
|
|
|
|
useShortcut(
|
|
SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH,
|
|
() => {
|
|
searchInputRef.current?.focus()
|
|
searchInputRef.current?.select()
|
|
},
|
|
{ label: 'Search users' }
|
|
)
|
|
|
|
useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => {
|
|
setSearch('')
|
|
setFilterKeywords('')
|
|
setFilterUserType('all')
|
|
setSelectedProviders([])
|
|
})
|
|
|
|
useShortcut(SHORTCUT_IDS.AUTH_USERS_REFRESH, onRefresh)
|
|
|
|
useShortcut(SHORTCUT_IDS.AUTH_USERS_CLEAR_SORT, () => setSortByValue('created_at:desc'), {
|
|
enabled: sortByValue !== 'created_at:desc',
|
|
})
|
|
|
|
useShortcut(
|
|
SHORTCUT_IDS.AUTH_USERS_TOGGLE_ALL_SELECTION,
|
|
() => {
|
|
if (selectedUsers.size === users.length) {
|
|
setSelectedUsers(new Set([]))
|
|
} else {
|
|
setSelectedUsers(new Set(users.map((u) => u.id)))
|
|
}
|
|
},
|
|
{ enabled: users.length > 0 && users.length <= MAX_BULK_DELETE }
|
|
)
|
|
|
|
useShortcut(SHORTCUT_IDS.AUTH_USERS_DELETE_SELECTED, () => setShowDeleteModal(true), {
|
|
enabled: selectedUsers.size > 0,
|
|
})
|
|
|
|
useShortcut(
|
|
SHORTCUT_IDS.AUTH_USERS_EXIT_SELECTION,
|
|
() => {
|
|
setSelectedUsers(new Set([]))
|
|
setHasCellSelected(false)
|
|
;(document.activeElement as HTMLElement | null)?.blur()
|
|
},
|
|
{ enabled: !selectedId && (selectedUsers.size > 0 || hasCellSelected) }
|
|
)
|
|
|
|
useShortcut(SHORTCUT_IDS.AUTH_USERS_CLOSE_PANEL, () => setSelectedId(null), {
|
|
enabled: !!selectedId,
|
|
})
|
|
|
|
const startGridNavigation = () => {
|
|
if (users.length === 0) return
|
|
gridRef.current?.selectCell({ idx: 1, rowIdx: 0 })
|
|
}
|
|
|
|
useShortcut(SHORTCUT_IDS.AUTH_USERS_START_NAV_DOWN, startGridNavigation, {
|
|
enabled: !hasCellSelected && users.length > 0,
|
|
})
|
|
|
|
useShortcut(SHORTCUT_IDS.AUTH_USERS_START_NAV_UP, startGridNavigation, {
|
|
enabled: !hasCellSelected && users.length > 0,
|
|
})
|
|
|
|
useShortcut(SHORTCUT_IDS.AUTH_USERS_CREATE_USER, () => setCreateVisible(true), {
|
|
enabled: canCreateUsers,
|
|
})
|
|
|
|
useShortcut(SHORTCUT_IDS.AUTH_USERS_INVITE_USER, () => setInviteVisible(true), {
|
|
enabled: canInviteUsers && showSendInvitation,
|
|
})
|
|
|
|
const onCellKeyDown = (args: GridCellKeyDownArgs, event: CellKeyboardEvent) => {
|
|
if (args.mode !== 'SELECT' || event.key !== 'Enter') return
|
|
const id = (args.row as { id?: unknown } | null | undefined)?.id
|
|
if (typeof id !== 'string') return
|
|
setSelectedId(id)
|
|
gridRef.current?.scrollToCell({ idx: 0, rowIdx: args.rowIdx })
|
|
}
|
|
|
|
return { onCellKeyDown, showDeleteModal, setShowDeleteModal }
|
|
}
|