useAuthUsersShortcuts.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import { PermissionAction } from '@supabase/shared-types/out/constants'
  2. import {
  3. parseAsArrayOf,
  4. parseAsBoolean,
  5. parseAsString,
  6. parseAsStringEnum,
  7. useQueryState,
  8. } from 'nuqs'
  9. import { Dispatch, RefObject, SetStateAction, useEffect, useState } from 'react'
  10. import type { CellKeyboardEvent, DataGridHandle } from 'react-data-grid'
  11. import { MAX_BULK_DELETE } from './Users.constants'
  12. import type { User } from '@/data/auth/users-infinite-query'
  13. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  14. import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
  15. import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
  16. import { useShortcut } from '@/state/shortcuts/useShortcut'
  17. interface UseAuthUsersShortcutsParams {
  18. gridRef: RefObject<DataGridHandle | null>
  19. searchInputRef: RefObject<HTMLInputElement | null>
  20. users: User[]
  21. selectedUsers: Set<unknown>
  22. setSelectedUsers: Dispatch<SetStateAction<Set<unknown>>>
  23. setSearch: Dispatch<SetStateAction<string>>
  24. onRefresh: () => void
  25. }
  26. interface GridCellKeyDownArgs {
  27. mode: 'SELECT' | 'EDIT'
  28. row: unknown
  29. rowIdx: number
  30. }
  31. interface UseAuthUsersShortcutsResult {
  32. onCellKeyDown: (args: GridCellKeyDownArgs, event: CellKeyboardEvent) => void
  33. showDeleteModal: boolean
  34. setShowDeleteModal: Dispatch<SetStateAction<boolean>>
  35. }
  36. export function useAuthUsersShortcuts({
  37. gridRef,
  38. searchInputRef,
  39. users,
  40. selectedUsers,
  41. setSelectedUsers,
  42. setSearch,
  43. onRefresh,
  44. }: UseAuthUsersShortcutsParams): UseAuthUsersShortcutsResult {
  45. const [hasCellSelected, setHasCellSelected] = useState(false)
  46. const [showDeleteModal, setShowDeleteModal] = useState(false)
  47. const [selectedId, setSelectedId] = useQueryState(
  48. 'show',
  49. parseAsString.withOptions({ history: 'push', clearOnDefault: true })
  50. )
  51. const [, setFilterKeywords] = useQueryState('keywords', { defaultValue: '' })
  52. const [, setFilterUserType] = useQueryState(
  53. 'userType',
  54. parseAsStringEnum(['all', 'verified', 'unverified', 'anonymous']).withDefault('all')
  55. )
  56. const [, setSelectedProviders] = useQueryState(
  57. 'providers',
  58. parseAsArrayOf(parseAsString, ',').withDefault([])
  59. )
  60. const [sortByValue, setSortByValue] = useQueryState('sortBy', {
  61. defaultValue: 'created_at:desc',
  62. })
  63. const [, setInviteVisible] = useQueryState(
  64. 'invite',
  65. parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
  66. )
  67. const [, setCreateVisible] = useQueryState(
  68. 'new',
  69. parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
  70. )
  71. const { can: canCreateUsers } = useAsyncCheckPermissions(
  72. PermissionAction.AUTH_EXECUTE,
  73. 'create_user'
  74. )
  75. const { can: canInviteUsers } = useAsyncCheckPermissions(
  76. PermissionAction.AUTH_EXECUTE,
  77. 'invite_user'
  78. )
  79. const showSendInvitation = useIsFeatureEnabled('authentication:show_send_invitation')
  80. useEffect(() => {
  81. const el = gridRef.current?.element
  82. if (!el) return
  83. const onFocusIn = () => setHasCellSelected(true)
  84. const onFocusOut = (event: FocusEvent) => {
  85. if (!el.contains(event.relatedTarget as Node | null)) {
  86. setHasCellSelected(false)
  87. }
  88. }
  89. el.addEventListener('focusin', onFocusIn)
  90. el.addEventListener('focusout', onFocusOut)
  91. return () => {
  92. el.removeEventListener('focusin', onFocusIn)
  93. el.removeEventListener('focusout', onFocusOut)
  94. }
  95. }, [gridRef])
  96. useShortcut(
  97. SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH,
  98. () => {
  99. searchInputRef.current?.focus()
  100. searchInputRef.current?.select()
  101. },
  102. { label: 'Search users' }
  103. )
  104. useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => {
  105. setSearch('')
  106. setFilterKeywords('')
  107. setFilterUserType('all')
  108. setSelectedProviders([])
  109. })
  110. useShortcut(SHORTCUT_IDS.AUTH_USERS_REFRESH, onRefresh)
  111. useShortcut(SHORTCUT_IDS.AUTH_USERS_CLEAR_SORT, () => setSortByValue('created_at:desc'), {
  112. enabled: sortByValue !== 'created_at:desc',
  113. })
  114. useShortcut(
  115. SHORTCUT_IDS.AUTH_USERS_TOGGLE_ALL_SELECTION,
  116. () => {
  117. if (selectedUsers.size === users.length) {
  118. setSelectedUsers(new Set([]))
  119. } else {
  120. setSelectedUsers(new Set(users.map((u) => u.id)))
  121. }
  122. },
  123. { enabled: users.length > 0 && users.length <= MAX_BULK_DELETE }
  124. )
  125. useShortcut(SHORTCUT_IDS.AUTH_USERS_DELETE_SELECTED, () => setShowDeleteModal(true), {
  126. enabled: selectedUsers.size > 0,
  127. })
  128. useShortcut(
  129. SHORTCUT_IDS.AUTH_USERS_EXIT_SELECTION,
  130. () => {
  131. setSelectedUsers(new Set([]))
  132. setHasCellSelected(false)
  133. ;(document.activeElement as HTMLElement | null)?.blur()
  134. },
  135. { enabled: !selectedId && (selectedUsers.size > 0 || hasCellSelected) }
  136. )
  137. useShortcut(SHORTCUT_IDS.AUTH_USERS_CLOSE_PANEL, () => setSelectedId(null), {
  138. enabled: !!selectedId,
  139. })
  140. const startGridNavigation = () => {
  141. if (users.length === 0) return
  142. gridRef.current?.selectCell({ idx: 1, rowIdx: 0 })
  143. }
  144. useShortcut(SHORTCUT_IDS.AUTH_USERS_START_NAV_DOWN, startGridNavigation, {
  145. enabled: !hasCellSelected && users.length > 0,
  146. })
  147. useShortcut(SHORTCUT_IDS.AUTH_USERS_START_NAV_UP, startGridNavigation, {
  148. enabled: !hasCellSelected && users.length > 0,
  149. })
  150. useShortcut(SHORTCUT_IDS.AUTH_USERS_CREATE_USER, () => setCreateVisible(true), {
  151. enabled: canCreateUsers,
  152. })
  153. useShortcut(SHORTCUT_IDS.AUTH_USERS_INVITE_USER, () => setInviteVisible(true), {
  154. enabled: canInviteUsers && showSendInvitation,
  155. })
  156. const onCellKeyDown = (args: GridCellKeyDownArgs, event: CellKeyboardEvent) => {
  157. if (args.mode !== 'SELECT' || event.key !== 'Enter') return
  158. const id = (args.row as { id?: unknown } | null | undefined)?.id
  159. if (typeof id !== 'string') return
  160. setSelectedId(id)
  161. gridRef.current?.scrollToCell({ idx: 0, rowIdx: args.rowIdx })
  162. }
  163. return { onCellKeyDown, showDeleteModal, setShowDeleteModal }
  164. }