@better-auth-ui/heroui 1.6.8 → 1.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/dist/components/auth/additional-field.d.ts +2 -2
  2. package/dist/components/auth/additional-field.js +133 -266
  3. package/dist/components/auth/api-key/api-key-skeleton.js +4 -20
  4. package/dist/components/auth/api-key/api-key.d.ts +6 -2
  5. package/dist/components/auth/api-key/api-key.js +16 -54
  6. package/dist/components/auth/api-key/api-keys-empty.d.ts +3 -1
  7. package/dist/components/auth/api-key/api-keys-empty.js +8 -34
  8. package/dist/components/auth/api-key/api-keys.d.ts +10 -2
  9. package/dist/components/auth/api-key/api-keys.js +21 -40
  10. package/dist/components/auth/api-key/create-api-key-dialog.d.ts +3 -1
  11. package/dist/components/auth/api-key/create-api-key-dialog.js +43 -73
  12. package/dist/components/auth/api-key/delete-api-key-dialog.d.ts +4 -2
  13. package/dist/components/auth/api-key/delete-api-key-dialog.js +16 -50
  14. package/dist/components/auth/api-key/new-api-key-dialog.js +23 -59
  15. package/dist/components/auth/api-key/organization-api-keys.d.ts +14 -0
  16. package/dist/components/auth/api-key/organization-api-keys.js +22 -0
  17. package/dist/components/auth/auth-provider.d.ts +1 -1
  18. package/dist/components/auth/auth-provider.js +10 -16
  19. package/dist/components/auth/auth.d.ts +3 -3
  20. package/dist/components/auth/auth.js +86 -61
  21. package/dist/components/auth/delete-user/danger-zone.d.ts +3 -3
  22. package/dist/components/auth/delete-user/danger-zone.js +11 -17
  23. package/dist/components/auth/delete-user/delete-account.d.ts +9 -0
  24. package/dist/components/auth/delete-user/delete-account.js +50 -0
  25. package/dist/components/auth/email/email-changed-email.d.ts +2 -2
  26. package/dist/components/auth/email/email-changed-email.js +22 -29
  27. package/dist/components/auth/email/email-verification-email.d.ts +2 -2
  28. package/dist/components/auth/email/email-verification-email.js +22 -29
  29. package/dist/components/auth/email/magic-link-email.d.ts +2 -2
  30. package/dist/components/auth/email/magic-link-email.js +22 -29
  31. package/dist/components/auth/email/new-device-email.d.ts +2 -2
  32. package/dist/components/auth/email/new-device-email.js +22 -29
  33. package/dist/components/auth/email/organization-invitation-email.d.ts +3 -0
  34. package/dist/components/auth/email/organization-invitation-email.js +23 -0
  35. package/dist/components/auth/email/otp-email.d.ts +2 -2
  36. package/dist/components/auth/email/otp-email.js +21 -28
  37. package/dist/components/auth/email/password-changed-email.d.ts +2 -2
  38. package/dist/components/auth/email/password-changed-email.js +22 -29
  39. package/dist/components/auth/email/reset-password-email.d.ts +2 -2
  40. package/dist/components/auth/email/reset-password-email.js +22 -29
  41. package/dist/components/auth/error-toaster.js +32 -16
  42. package/dist/components/auth/field-separator.d.ts +1 -1
  43. package/dist/components/auth/field-separator.js +4 -17
  44. package/dist/components/auth/forgot-password.d.ts +1 -1
  45. package/dist/components/auth/forgot-password.js +36 -88
  46. package/dist/components/auth/magic-link/magic-link-button.d.ts +1 -1
  47. package/dist/components/auth/magic-link/magic-link-button.js +33 -19
  48. package/dist/components/auth/magic-link/magic-link.d.ts +2 -2
  49. package/dist/components/auth/magic-link/magic-link.js +39 -86
  50. package/dist/components/auth/multi-session/manage-account.d.ts +1 -1
  51. package/dist/components/auth/multi-session/manage-account.js +32 -48
  52. package/dist/components/auth/multi-session/manage-accounts.d.ts +1 -1
  53. package/dist/components/auth/multi-session/manage-accounts.js +32 -33
  54. package/dist/components/auth/multi-session/switch-account-submenu-content.d.ts +3 -1
  55. package/dist/components/auth/multi-session/switch-account-submenu-content.js +24 -27
  56. package/dist/components/auth/multi-session/switch-account-submenu-item.d.ts +3 -2
  57. package/dist/components/auth/multi-session/switch-account-submenu-item.js +16 -19
  58. package/dist/components/auth/multi-session/switch-account-submenu.d.ts +2 -1
  59. package/dist/components/auth/multi-session/switch-account-submenu.js +23 -23
  60. package/dist/components/auth/organization/change-organization-logo.d.ts +4 -0
  61. package/dist/components/auth/organization/change-organization-logo.js +69 -0
  62. package/dist/components/auth/organization/create-organization-dialog.d.ts +13 -0
  63. package/dist/components/auth/organization/create-organization-dialog.js +37 -0
  64. package/dist/components/auth/organization/delete-organization-dialog.d.ts +7 -0
  65. package/dist/components/auth/organization/delete-organization-dialog.js +25 -0
  66. package/dist/components/auth/organization/delete-organization-skeleton.d.ts +4 -0
  67. package/dist/components/auth/organization/delete-organization-skeleton.js +8 -0
  68. package/dist/components/auth/organization/delete-organization.d.ts +5 -0
  69. package/dist/components/auth/organization/delete-organization.js +27 -0
  70. package/dist/components/auth/organization/invite-member-dialog.d.ts +13 -0
  71. package/dist/components/auth/organization/invite-member-dialog.js +47 -0
  72. package/dist/components/auth/organization/leave-organization-dialog.d.ts +7 -0
  73. package/dist/components/auth/organization/leave-organization-dialog.js +21 -0
  74. package/dist/components/auth/organization/leave-organization.d.ts +4 -0
  75. package/dist/components/auth/organization/leave-organization.js +16 -0
  76. package/dist/components/auth/organization/organization-danger-zone.d.ts +16 -0
  77. package/dist/components/auth/organization/organization-danger-zone.js +23 -0
  78. package/dist/components/auth/organization/organization-invitation-row-skeleton.d.ts +5 -0
  79. package/dist/components/auth/organization/organization-invitation-row-skeleton.js +9 -0
  80. package/dist/components/auth/organization/organization-invitation-row.d.ts +5 -0
  81. package/dist/components/auth/organization/organization-invitation-row.js +31 -0
  82. package/dist/components/auth/organization/organization-invitations-empty.d.ts +8 -0
  83. package/dist/components/auth/organization/organization-invitations-empty.js +13 -0
  84. package/dist/components/auth/organization/organization-invitations.d.ts +9 -0
  85. package/dist/components/auth/organization/organization-invitations.js +66 -0
  86. package/dist/components/auth/organization/organization-logo.d.ts +15 -0
  87. package/dist/components/auth/organization/organization-logo.js +14 -0
  88. package/dist/components/auth/organization/organization-member-row-skeleton.d.ts +4 -0
  89. package/dist/components/auth/organization/organization-member-row-skeleton.js +9 -0
  90. package/dist/components/auth/organization/organization-member-row.d.ts +9 -0
  91. package/dist/components/auth/organization/organization-member-row.js +34 -0
  92. package/dist/components/auth/organization/organization-members.d.ts +9 -0
  93. package/dist/components/auth/organization/organization-members.js +67 -0
  94. package/dist/components/auth/organization/organization-people.d.ts +10 -0
  95. package/dist/components/auth/organization/organization-people.js +11 -0
  96. package/dist/components/auth/organization/organization-profile.d.ts +9 -0
  97. package/dist/components/auth/organization/organization-profile.js +34 -0
  98. package/dist/components/auth/organization/organization-row.d.ts +8 -0
  99. package/dist/components/auth/organization/organization-row.js +31 -0
  100. package/dist/components/auth/organization/organization-settings.d.ts +12 -0
  101. package/dist/components/auth/organization/organization-settings.js +13 -0
  102. package/dist/components/auth/organization/organization-switcher.d.ts +20 -0
  103. package/dist/components/auth/organization/organization-switcher.js +48 -0
  104. package/dist/components/auth/organization/organization-view-skeleton.d.ts +6 -0
  105. package/dist/components/auth/organization/organization-view-skeleton.js +9 -0
  106. package/dist/components/auth/organization/organization-view.d.ts +15 -0
  107. package/dist/components/auth/organization/organization-view.js +31 -0
  108. package/dist/components/auth/organization/organization.d.ts +17 -0
  109. package/dist/components/auth/organization/organization.js +60 -0
  110. package/dist/components/auth/organization/organizations-empty.d.ts +4 -0
  111. package/dist/components/auth/organization/organizations-empty.js +9 -0
  112. package/dist/components/auth/organization/organizations-settings.d.ts +17 -0
  113. package/dist/components/auth/organization/organizations-settings.js +17 -0
  114. package/dist/components/auth/organization/organizations.d.ts +10 -0
  115. package/dist/components/auth/organization/organizations.js +21 -0
  116. package/dist/components/auth/organization/remove-member-dialog.d.ts +9 -0
  117. package/dist/components/auth/organization/remove-member-dialog.js +20 -0
  118. package/dist/components/auth/organization/slug-field.d.ts +19 -0
  119. package/dist/components/auth/organization/slug-field.js +38 -0
  120. package/dist/components/auth/organization/user-invitation-row-skeleton.d.ts +4 -0
  121. package/dist/components/auth/organization/user-invitation-row-skeleton.js +8 -0
  122. package/dist/components/auth/organization/user-invitation-row.d.ts +10 -0
  123. package/dist/components/auth/organization/user-invitation-row.js +22 -0
  124. package/dist/components/auth/organization/user-invitations-empty.d.ts +4 -0
  125. package/dist/components/auth/organization/user-invitations-empty.js +12 -0
  126. package/dist/components/auth/organization/user-invitations.d.ts +10 -0
  127. package/dist/components/auth/organization/user-invitations.js +18 -0
  128. package/dist/components/auth/passkey/add-passkey-dialog.js +21 -64
  129. package/dist/components/auth/passkey/delete-passkey-dialog.js +13 -46
  130. package/dist/components/auth/passkey/passkey-button.d.ts +1 -1
  131. package/dist/components/auth/passkey/passkey-button.js +24 -23
  132. package/dist/components/auth/passkey/passkey-skeleton.js +4 -16
  133. package/dist/components/auth/passkey/passkey.d.ts +1 -1
  134. package/dist/components/auth/passkey/passkey.js +16 -47
  135. package/dist/components/auth/passkey/passkeys-empty.js +8 -34
  136. package/dist/components/auth/passkey/passkeys.d.ts +1 -1
  137. package/dist/components/auth/passkey/passkeys.js +15 -41
  138. package/dist/components/auth/provider-button.d.ts +2 -2
  139. package/dist/components/auth/provider-button.js +28 -24
  140. package/dist/components/auth/provider-buttons.js +28 -18
  141. package/dist/components/auth/reset-password.d.ts +1 -1
  142. package/dist/components/auth/reset-password.js +56 -146
  143. package/dist/components/auth/settings/account/account-settings.d.ts +2 -2
  144. package/dist/components/auth/settings/account/account-settings.js +21 -19
  145. package/dist/components/auth/settings/account/change-avatar.js +53 -88
  146. package/dist/components/auth/settings/account/change-email.d.ts +1 -1
  147. package/dist/components/auth/settings/account/change-email.js +27 -53
  148. package/dist/components/auth/settings/account/user-profile.d.ts +1 -1
  149. package/dist/components/auth/settings/account/user-profile.js +64 -85
  150. package/dist/components/auth/settings/security/active-session.d.ts +1 -1
  151. package/dist/components/auth/settings/security/active-session.js +50 -57
  152. package/dist/components/auth/settings/security/active-sessions.d.ts +1 -1
  153. package/dist/components/auth/settings/security/active-sessions.js +21 -32
  154. package/dist/components/auth/settings/security/change-password.d.ts +1 -1
  155. package/dist/components/auth/settings/security/change-password.js +76 -186
  156. package/dist/components/auth/settings/security/linked-account.d.ts +1 -1
  157. package/dist/components/auth/settings/security/linked-account.js +37 -55
  158. package/dist/components/auth/settings/security/linked-accounts.d.ts +1 -1
  159. package/dist/components/auth/settings/security/linked-accounts.js +34 -43
  160. package/dist/components/auth/settings/security/security-settings.d.ts +2 -2
  161. package/dist/components/auth/settings/security/security-settings.js +19 -21
  162. package/dist/components/auth/settings/settings.d.ts +4 -4
  163. package/dist/components/auth/settings/settings.js +52 -44
  164. package/dist/components/auth/sign-in.d.ts +2 -2
  165. package/dist/components/auth/sign-in.js +63 -132
  166. package/dist/components/auth/sign-out.js +32 -26
  167. package/dist/components/auth/sign-up.d.ts +2 -2
  168. package/dist/components/auth/sign-up.js +97 -211
  169. package/dist/components/auth/theme/appearance.d.ts +1 -1
  170. package/dist/components/auth/theme/appearance.js +23 -71
  171. package/dist/components/auth/theme/theme-toggle-item.js +56 -61
  172. package/dist/components/auth/user/user-avatar.d.ts +4 -4
  173. package/dist/components/auth/user/user-avatar.js +31 -28
  174. package/dist/components/auth/user/user-button.d.ts +6 -7
  175. package/dist/components/auth/user/user-button.js +38 -72
  176. package/dist/components/auth/user/user-view.d.ts +13 -7
  177. package/dist/components/auth/user/user-view.js +26 -36
  178. package/dist/components/auth/username/sign-in-username.d.ts +2 -2
  179. package/dist/components/auth/username/sign-in-username.js +87 -147
  180. package/dist/components/auth/username/username-field.d.ts +1 -1
  181. package/dist/components/auth/username/username-field.js +44 -52
  182. package/dist/email.d.ts +8 -7
  183. package/dist/email.js +8 -8
  184. package/dist/index.d.ts +21 -21
  185. package/dist/index.js +21 -22
  186. package/dist/lib/auth/api-key-plugin.d.ts +7 -4
  187. package/dist/lib/auth/api-key-plugin.js +12 -10
  188. package/dist/lib/auth/auth-plugin.d.ts +10 -6
  189. package/dist/lib/auth/auth-plugin.js +1 -0
  190. package/dist/lib/auth/delete-user-plugin.d.ts +6 -6
  191. package/dist/lib/auth/delete-user-plugin.js +6 -9
  192. package/dist/lib/auth/magic-link-plugin.d.ts +5 -5
  193. package/dist/lib/auth/magic-link-plugin.js +17 -12
  194. package/dist/lib/auth/multi-session-plugin.d.ts +5 -5
  195. package/dist/lib/auth/multi-session-plugin.js +8 -11
  196. package/dist/lib/auth/organization-plugin.d.ts +37 -0
  197. package/dist/lib/auth/organization-plugin.js +19 -0
  198. package/dist/lib/auth/passkey-plugin.d.ts +5 -5
  199. package/dist/lib/auth/passkey-plugin.js +8 -11
  200. package/dist/lib/auth/theme-plugin.d.ts +6 -6
  201. package/dist/lib/auth/theme-plugin.js +25 -22
  202. package/dist/lib/auth/username-plugin.d.ts +11 -11
  203. package/dist/lib/auth/username-plugin.js +18 -17
  204. package/dist/plugins.d.ts +46 -22
  205. package/dist/plugins.js +47 -22
  206. package/package.json +25 -19
  207. package/src/components/auth/api-key/api-key.tsx +25 -16
  208. package/src/components/auth/api-key/api-keys-empty.tsx +8 -4
  209. package/src/components/auth/api-key/api-keys.tsx +52 -14
  210. package/src/components/auth/api-key/create-api-key-dialog.tsx +15 -2
  211. package/src/components/auth/api-key/delete-api-key-dialog.tsx +10 -2
  212. package/src/components/auth/api-key/organization-api-keys.tsx +55 -0
  213. package/src/components/auth/delete-user/danger-zone.tsx +3 -3
  214. package/src/components/auth/delete-user/{delete-user.tsx → delete-account.tsx} +9 -9
  215. package/src/components/auth/email/organization-invitation-email.tsx +38 -0
  216. package/src/components/auth/error-toaster.tsx +28 -10
  217. package/src/components/auth/forgot-password.tsx +10 -2
  218. package/src/components/auth/multi-session/manage-accounts.tsx +1 -1
  219. package/src/components/auth/multi-session/switch-account-submenu-content.tsx +7 -2
  220. package/src/components/auth/multi-session/switch-account-submenu-item.tsx +4 -2
  221. package/src/components/auth/multi-session/switch-account-submenu.tsx +8 -4
  222. package/src/components/auth/organization/change-organization-logo.tsx +171 -0
  223. package/src/components/auth/organization/create-organization-dialog.tsx +133 -0
  224. package/src/components/auth/organization/delete-organization-dialog.tsx +97 -0
  225. package/src/components/auth/organization/delete-organization-skeleton.tsx +17 -0
  226. package/src/components/auth/organization/delete-organization.tsx +77 -0
  227. package/src/components/auth/organization/invite-member-dialog.tsx +177 -0
  228. package/src/components/auth/organization/leave-organization-dialog.tsx +95 -0
  229. package/src/components/auth/organization/leave-organization.tsx +59 -0
  230. package/src/components/auth/organization/organization-danger-zone.tsx +68 -0
  231. package/src/components/auth/organization/organization-invitation-row-skeleton.tsx +29 -0
  232. package/src/components/auth/organization/organization-invitation-row.tsx +97 -0
  233. package/src/components/auth/organization/organization-invitations-empty.tsx +39 -0
  234. package/src/components/auth/organization/organization-invitations.tsx +351 -0
  235. package/src/components/auth/organization/organization-logo.tsx +64 -0
  236. package/src/components/auth/organization/organization-member-row-skeleton.tsx +24 -0
  237. package/src/components/auth/organization/organization-member-row.tsx +157 -0
  238. package/src/components/auth/organization/organization-members.tsx +284 -0
  239. package/src/components/auth/organization/organization-people.tsx +26 -0
  240. package/src/components/auth/organization/organization-profile.tsx +141 -0
  241. package/src/components/auth/organization/organization-row.tsx +66 -0
  242. package/src/components/auth/organization/organization-settings.tsx +38 -0
  243. package/src/components/auth/organization/organization-switcher.tsx +233 -0
  244. package/src/components/auth/organization/organization-view-skeleton.tsx +36 -0
  245. package/src/components/auth/organization/organization-view.tsx +109 -0
  246. package/src/components/auth/organization/organization.tsx +162 -0
  247. package/src/components/auth/organization/organizations-empty.tsx +35 -0
  248. package/src/components/auth/organization/organizations-settings.tsx +36 -0
  249. package/src/components/auth/organization/organizations.tsx +80 -0
  250. package/src/components/auth/organization/remove-member-dialog.tsx +95 -0
  251. package/src/components/auth/organization/slug-field.tsx +111 -0
  252. package/src/components/auth/organization/user-invitation-row-skeleton.tsx +17 -0
  253. package/src/components/auth/organization/user-invitation-row.tsx +90 -0
  254. package/src/components/auth/organization/user-invitations-empty.tsx +30 -0
  255. package/src/components/auth/organization/user-invitations.tsx +59 -0
  256. package/src/components/auth/settings/account/change-email.tsx +1 -1
  257. package/src/components/auth/settings/account/user-profile.tsx +5 -5
  258. package/src/components/auth/settings/security/active-sessions.tsx +6 -4
  259. package/src/components/auth/settings/security/linked-accounts.tsx +2 -2
  260. package/src/components/auth/settings/settings.tsx +77 -14
  261. package/src/components/auth/user/user-avatar.tsx +4 -1
  262. package/src/components/auth/user/user-button.tsx +14 -11
  263. package/src/components/auth/user/user-view.tsx +32 -12
  264. package/src/email.ts +1 -0
  265. package/src/lib/auth/api-key-plugin.ts +10 -4
  266. package/src/lib/auth/auth-plugin.ts +6 -1
  267. package/src/lib/auth/organization-plugin.tsx +32 -0
  268. package/src/plugins.ts +25 -1
  269. package/dist/components/auth/delete-user/delete-user.d.ts +0 -9
  270. package/dist/components/auth/delete-user/delete-user.js +0 -95
@@ -0,0 +1,157 @@
1
+ import {
2
+ type OrganizationAuthClient,
3
+ useAuth,
4
+ useAuthPlugin,
5
+ useHasPermission,
6
+ useSession,
7
+ useUpdateMemberRole
8
+ } from "@better-auth-ui/react"
9
+ import { ArrowRightFromSquare, Pencil, TrashBin } from "@gravity-ui/icons"
10
+ import { Button, Dropdown, Label, Spinner, Table, toast } from "@heroui/react"
11
+ import type { Member, Organization, User } from "better-auth/client"
12
+ import { useState } from "react"
13
+
14
+ import { organizationPlugin } from "../../../lib/auth/organization-plugin"
15
+ import { UserView } from "../user/user-view"
16
+ import { LeaveOrganizationDialog } from "./leave-organization-dialog"
17
+ import { OrganizationMemberRowSkeleton } from "./organization-member-row-skeleton"
18
+ import { RemoveMemberDialog } from "./remove-member-dialog"
19
+
20
+ export type OrganizationMemberRowProps = {
21
+ member: Member & { user: Partial<User> }
22
+ isOwner?: boolean
23
+ organization: Organization
24
+ }
25
+
26
+ export function OrganizationMemberRow({
27
+ member,
28
+ isOwner,
29
+ organization
30
+ }: OrganizationMemberRowProps) {
31
+ const { authClient } = useAuth()
32
+ const { localization: organizationLocalization, roles } =
33
+ useAuthPlugin(organizationPlugin)
34
+
35
+ const { data: session } = useSession(authClient)
36
+
37
+ const { data: hasUpdatePermission, isPending: updatePermissionPending } =
38
+ useHasPermission(authClient as OrganizationAuthClient, {
39
+ permissions: { member: ["update"] }
40
+ })
41
+
42
+ const { data: hasDeletePermission, isPending: deletePermissionPending } =
43
+ useHasPermission(authClient as OrganizationAuthClient, {
44
+ permissions: { member: ["delete"] }
45
+ })
46
+
47
+ const isPending = updatePermissionPending || deletePermissionPending
48
+
49
+ const { mutate: updateMemberRole, isPending: isUpdatingRole } =
50
+ useUpdateMemberRole(authClient as OrganizationAuthClient, {
51
+ onSuccess: () => toast.success(organizationLocalization.memberRoleUpdated)
52
+ })
53
+
54
+ const roleLabel = roles?.[member.role] ?? member.role
55
+
56
+ const assignableRoles = Object.entries(roles).filter(
57
+ ([key]) => isOwner || key !== "owner"
58
+ )
59
+
60
+ const isCurrentUser = session?.user.id === member.userId
61
+
62
+ const [removeOpen, setRemoveOpen] = useState(false)
63
+ const [leaveOpen, setLeaveOpen] = useState(false)
64
+
65
+ if (isPending) {
66
+ return <OrganizationMemberRowSkeleton />
67
+ }
68
+
69
+ return (
70
+ <Table.Row>
71
+ <Table.Cell>
72
+ <UserView user={member.user} />
73
+ </Table.Cell>
74
+
75
+ <Table.Cell>{roleLabel}</Table.Cell>
76
+
77
+ <Table.Cell>
78
+ <div className="flex items-center justify-end gap-1">
79
+ {hasUpdatePermission?.success && (
80
+ <Dropdown>
81
+ <Button
82
+ isIconOnly
83
+ size="sm"
84
+ variant="tertiary"
85
+ isDisabled={isUpdatingRole}
86
+ aria-label={organizationLocalization.changeMemberRole}
87
+ >
88
+ {isUpdatingRole ? (
89
+ <Spinner color="current" size="sm" />
90
+ ) : (
91
+ <Pencil />
92
+ )}
93
+ </Button>
94
+
95
+ <Dropdown.Popover className="min-w-fit">
96
+ <Dropdown.Menu>
97
+ {assignableRoles.map(([role, label]) => (
98
+ <Dropdown.Item
99
+ key={role}
100
+ textValue={label}
101
+ isDisabled={member.role === role}
102
+ onAction={() =>
103
+ updateMemberRole({ memberId: member.id, role })
104
+ }
105
+ >
106
+ <Label>{label}</Label>
107
+ </Dropdown.Item>
108
+ ))}
109
+ </Dropdown.Menu>
110
+ </Dropdown.Popover>
111
+ </Dropdown>
112
+ )}
113
+
114
+ {isCurrentUser ? (
115
+ <Button
116
+ isIconOnly
117
+ size="sm"
118
+ variant="danger-soft"
119
+ aria-label={organizationLocalization.leaveOrganization}
120
+ onPress={() => setLeaveOpen(true)}
121
+ >
122
+ <ArrowRightFromSquare />
123
+ </Button>
124
+ ) : (
125
+ hasDeletePermission?.success && (
126
+ <Button
127
+ isIconOnly
128
+ size="sm"
129
+ variant="danger-soft"
130
+ aria-label={organizationLocalization.removeMember}
131
+ onPress={() => setRemoveOpen(true)}
132
+ >
133
+ <TrashBin />
134
+ </Button>
135
+ )
136
+ )}
137
+ </div>
138
+
139
+ {isCurrentUser && organization ? (
140
+ <LeaveOrganizationDialog
141
+ isOpen={leaveOpen}
142
+ onOpenChange={setLeaveOpen}
143
+ organization={organization}
144
+ />
145
+ ) : (
146
+ hasDeletePermission?.success && (
147
+ <RemoveMemberDialog
148
+ isOpen={removeOpen}
149
+ onOpenChange={setRemoveOpen}
150
+ member={member}
151
+ />
152
+ )
153
+ )}
154
+ </Table.Cell>
155
+ </Table.Row>
156
+ )
157
+ }
@@ -0,0 +1,284 @@
1
+ import {
2
+ type OrganizationAuthClient,
3
+ useActiveOrganization,
4
+ useAuth,
5
+ useAuthPlugin,
6
+ useHasPermission,
7
+ useListOrganizationMembers,
8
+ useSession
9
+ } from "@better-auth-ui/react"
10
+ import { ChevronUp, Funnel, Xmark } from "@gravity-ui/icons"
11
+ import {
12
+ Button,
13
+ Chip,
14
+ cn,
15
+ Dropdown,
16
+ Label,
17
+ SearchField,
18
+ type SortDescriptor,
19
+ Table
20
+ } from "@heroui/react"
21
+ import type { Member } from "better-auth/client"
22
+ import { type ComponentProps, type ReactNode, useMemo, useState } from "react"
23
+
24
+ import { organizationPlugin } from "../../../lib/auth/organization-plugin"
25
+ import { InviteMemberDialog } from "./invite-member-dialog"
26
+ import { OrganizationMemberRow } from "./organization-member-row"
27
+ import { OrganizationMemberRowSkeleton } from "./organization-member-row-skeleton"
28
+
29
+ /** Props for the {@link OrganizationMembers} component. */
30
+ export type OrganizationMembersProps = {
31
+ className?: string
32
+ }
33
+
34
+ /**
35
+ * Organization members table with title, invite control, and per-row actions.
36
+ */
37
+ export function OrganizationMembers({
38
+ className,
39
+ ...props
40
+ }: OrganizationMembersProps & ComponentProps<"div">) {
41
+ const { authClient } = useAuth()
42
+ const { localization: organizationLocalization, roles } =
43
+ useAuthPlugin(organizationPlugin)
44
+
45
+ const { data: session } = useSession(authClient)
46
+ const { data: activeOrganization, isPending: activeOrganizationPending } =
47
+ useActiveOrganization(authClient as OrganizationAuthClient)
48
+ const { data: membersData, isPending: membersPending } =
49
+ useListOrganizationMembers(authClient as OrganizationAuthClient)
50
+
51
+ const { isPending: updatePermissionPending } = useHasPermission(
52
+ authClient as OrganizationAuthClient,
53
+ {
54
+ permissions: { member: ["update"] }
55
+ }
56
+ )
57
+ const { isPending: deletePermissionPending } = useHasPermission(
58
+ authClient as OrganizationAuthClient,
59
+ {
60
+ permissions: { member: ["delete"] }
61
+ }
62
+ )
63
+
64
+ const isPending =
65
+ activeOrganizationPending ||
66
+ membersPending ||
67
+ updatePermissionPending ||
68
+ deletePermissionPending
69
+
70
+ const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>()
71
+ const [roleFilter, setRoleFilter] = useState("all")
72
+ const [search, setSearch] = useState("")
73
+
74
+ const filteredMembers = useMemo(() => {
75
+ return membersData?.members.filter(
76
+ (member) =>
77
+ (roleFilter === "all" || member.role === roleFilter) &&
78
+ (member.user.name.toLowerCase().includes(search.toLowerCase()) ||
79
+ member.user.email.toLowerCase().includes(search.toLowerCase()))
80
+ )
81
+ }, [search, membersData?.members, roleFilter])
82
+
83
+ const sortedMembers = useMemo(() => {
84
+ if (!sortDescriptor) return filteredMembers
85
+ if (!filteredMembers) return filteredMembers
86
+
87
+ return [...filteredMembers].sort((a, b) => {
88
+ const col = sortDescriptor.column as keyof Member | "user"
89
+ const first =
90
+ col === "user" ? a.user.name || a.user.email : String(a[col])
91
+ const second =
92
+ col === "user" ? b.user.name || b.user.email : String(b[col])
93
+
94
+ let cmp = first.localeCompare(second)
95
+ if (sortDescriptor.direction === "descending") {
96
+ cmp *= -1
97
+ }
98
+
99
+ return cmp
100
+ })
101
+ }, [sortDescriptor, filteredMembers])
102
+
103
+ const [inviteOpen, setInviteOpen] = useState(false)
104
+
105
+ const isOwner = membersData?.members.some(
106
+ (member) => member.role === "owner" && member.userId === session?.user.id
107
+ )
108
+
109
+ return (
110
+ <div className={cn("flex flex-col gap-3", className)} {...props}>
111
+ <div className="flex items-end justify-between gap-3">
112
+ <h3 className="truncate text-sm font-semibold">
113
+ {organizationLocalization.members}
114
+ </h3>
115
+
116
+ <Button
117
+ className="shrink-0"
118
+ size="sm"
119
+ isDisabled={isPending}
120
+ onPress={() => setInviteOpen(true)}
121
+ >
122
+ {organizationLocalization.inviteMember}
123
+ </Button>
124
+ </div>
125
+
126
+ <div className="flex flex-col gap-4">
127
+ <div className="flex items-center gap-3">
128
+ <SearchField
129
+ className="min-w-0"
130
+ aria-label={organizationLocalization.search}
131
+ value={search}
132
+ onChange={setSearch}
133
+ isDisabled={isPending}
134
+ >
135
+ <SearchField.Group>
136
+ <SearchField.SearchIcon />
137
+
138
+ <SearchField.Input
139
+ placeholder={organizationLocalization.search}
140
+ className="sm:w-[200px]"
141
+ />
142
+
143
+ <SearchField.ClearButton />
144
+ </SearchField.Group>
145
+ </SearchField>
146
+
147
+ <Dropdown>
148
+ <Button size="sm" variant="secondary" isDisabled={isPending}>
149
+ <Funnel />
150
+
151
+ {organizationLocalization.role}
152
+ </Button>
153
+
154
+ <Dropdown.Popover>
155
+ <Dropdown.Menu
156
+ selectionMode="single"
157
+ selectedKeys={new Set([roleFilter])}
158
+ onSelectionChange={(keys) => {
159
+ const key = [...keys][0] as string | undefined
160
+ setRoleFilter(key ?? "all")
161
+ }}
162
+ >
163
+ <Dropdown.Item
164
+ id="all"
165
+ textValue={organizationLocalization.all}
166
+ >
167
+ <Label>{organizationLocalization.all}</Label>
168
+
169
+ <Dropdown.ItemIndicator />
170
+ </Dropdown.Item>
171
+
172
+ {Object.entries(roles).map(([role, label]) => (
173
+ <Dropdown.Item key={role} id={role} textValue={label}>
174
+ <Label>{label}</Label>
175
+
176
+ <Dropdown.ItemIndicator />
177
+ </Dropdown.Item>
178
+ ))}
179
+ </Dropdown.Menu>
180
+ </Dropdown.Popover>
181
+ </Dropdown>
182
+ </div>
183
+
184
+ {roleFilter !== "all" && (
185
+ <Chip size="sm" variant="secondary" className="w-fit">
186
+ <Chip.Label>
187
+ {organizationLocalization.role}:{" "}
188
+ <span className="capitalize">
189
+ {roles?.[roleFilter] ?? roleFilter}
190
+ </span>
191
+ </Chip.Label>
192
+
193
+ <button
194
+ type="button"
195
+ aria-label={organizationLocalization.clear}
196
+ className="text-muted hover:text-foreground inline-flex cursor-pointer items-center"
197
+ onClick={() => setRoleFilter("all")}
198
+ >
199
+ <Xmark className="size-3" />
200
+ </button>
201
+ </Chip>
202
+ )}
203
+
204
+ <Table>
205
+ <Table.ScrollContainer>
206
+ <Table.Content
207
+ aria-label={organizationLocalization.members}
208
+ sortDescriptor={sortDescriptor}
209
+ onSortChange={(descriptor) => {
210
+ const shouldReset =
211
+ sortDescriptor?.column === descriptor.column &&
212
+ descriptor.direction === "ascending"
213
+ setSortDescriptor(shouldReset ? undefined : descriptor)
214
+ }}
215
+ >
216
+ <Table.Header>
217
+ <Table.Column allowsSorting isRowHeader id="user">
218
+ {({ sortDirection }) => (
219
+ <SortableColumnHeader sortDirection={sortDirection}>
220
+ {organizationLocalization.member}
221
+ </SortableColumnHeader>
222
+ )}
223
+ </Table.Column>
224
+
225
+ <Table.Column allowsSorting id="role">
226
+ {({ sortDirection }) => (
227
+ <SortableColumnHeader sortDirection={sortDirection}>
228
+ {organizationLocalization.role}
229
+ </SortableColumnHeader>
230
+ )}
231
+ </Table.Column>
232
+
233
+ <Table.Column className="text-end">
234
+ {organizationLocalization.actions}
235
+ </Table.Column>
236
+ </Table.Header>
237
+
238
+ <Table.Body>
239
+ {isPending ? (
240
+ <OrganizationMemberRowSkeleton />
241
+ ) : (
242
+ !!activeOrganization &&
243
+ sortedMembers?.map((member) => (
244
+ <OrganizationMemberRow
245
+ key={member.id}
246
+ member={member}
247
+ isOwner={isOwner}
248
+ organization={activeOrganization}
249
+ />
250
+ ))
251
+ )}
252
+ </Table.Body>
253
+ </Table.Content>
254
+ </Table.ScrollContainer>
255
+ </Table>
256
+ </div>
257
+
258
+ <InviteMemberDialog isOpen={inviteOpen} onOpenChange={setInviteOpen} />
259
+ </div>
260
+ )
261
+ }
262
+
263
+ function SortableColumnHeader({
264
+ children,
265
+ sortDirection
266
+ }: {
267
+ children: ReactNode
268
+ sortDirection?: "ascending" | "descending"
269
+ }) {
270
+ return (
271
+ <span className="flex items-center justify-between">
272
+ {children}
273
+
274
+ {!!sortDirection && (
275
+ <ChevronUp
276
+ className={cn(
277
+ "size-3 transform transition-transform duration-100 ease-out",
278
+ sortDirection === "descending" ? "rotate-180" : ""
279
+ )}
280
+ />
281
+ )}
282
+ </span>
283
+ )
284
+ }
@@ -0,0 +1,26 @@
1
+ import { cn } from "@heroui/react"
2
+ import type { ComponentProps } from "react"
3
+
4
+ import { OrganizationInvitations } from "./organization-invitations"
5
+ import { OrganizationMembers } from "./organization-members"
6
+
7
+ /** Props for the {@link OrganizationPeople} component. */
8
+ export type OrganizationPeopleProps = {
9
+ className?: string
10
+ }
11
+
12
+ /**
13
+ * Organization people UI: members table (see {@link OrganizationMembers}), then org
14
+ * invitations.
15
+ */
16
+ export function OrganizationPeople({
17
+ className,
18
+ ...props
19
+ }: OrganizationPeopleProps & ComponentProps<"div">) {
20
+ return (
21
+ <div className={cn("flex flex-col gap-4 md:gap-6", className)} {...props}>
22
+ <OrganizationMembers />
23
+ <OrganizationInvitations />
24
+ </div>
25
+ )
26
+ }
@@ -0,0 +1,141 @@
1
+ import {
2
+ type OrganizationAuthClient,
3
+ useActiveOrganization,
4
+ useAuth,
5
+ useAuthPlugin,
6
+ useUpdateOrganization
7
+ } from "@better-auth-ui/react"
8
+ import {
9
+ Button,
10
+ Card,
11
+ type CardProps,
12
+ cn,
13
+ FieldError,
14
+ Form,
15
+ Input,
16
+ Label,
17
+ Skeleton,
18
+ Spinner,
19
+ TextField,
20
+ toast
21
+ } from "@heroui/react"
22
+ import { type SyntheticEvent, useEffect, useState } from "react"
23
+
24
+ import { organizationPlugin } from "../../../lib/auth/organization-plugin"
25
+ import { ChangeOrganizationLogo } from "./change-organization-logo"
26
+ import { SlugField } from "./slug-field"
27
+
28
+ export type OrganizationProfileProps = {
29
+ className?: string
30
+ variant?: CardProps["variant"]
31
+ }
32
+
33
+ /**
34
+ * Profile card for the active organization: logo (when enabled), display name, and slug.
35
+ */
36
+ export function OrganizationProfile({
37
+ className,
38
+ variant,
39
+ ...props
40
+ }: OrganizationProfileProps & Omit<CardProps, "children">) {
41
+ const { authClient, localization } = useAuth()
42
+ const { localization: organizationLocalization } =
43
+ useAuthPlugin(organizationPlugin)
44
+
45
+ const { data: activeOrganization } = useActiveOrganization(
46
+ authClient as OrganizationAuthClient
47
+ )
48
+
49
+ const [slug, setSlug] = useState(activeOrganization?.slug ?? "")
50
+
51
+ useEffect(() => {
52
+ setSlug(activeOrganization?.slug ?? "")
53
+ }, [activeOrganization?.slug])
54
+
55
+ const { mutate: commitOrganizationUpdate, isPending } = useUpdateOrganization(
56
+ authClient as OrganizationAuthClient,
57
+ {
58
+ onSuccess: () =>
59
+ toast.success(organizationLocalization.organizationUpdatedSuccess)
60
+ }
61
+ )
62
+
63
+ function handleSubmit(e: SyntheticEvent<HTMLFormElement>) {
64
+ e.preventDefault()
65
+ if (!activeOrganization) return
66
+
67
+ const formData = new FormData(e.currentTarget)
68
+ const name = formData.get("name") as string
69
+
70
+ commitOrganizationUpdate({
71
+ data: { name, slug }
72
+ })
73
+ }
74
+
75
+ const inputVariant = variant === "transparent" ? "primary" : "secondary"
76
+
77
+ return (
78
+ <div>
79
+ <h2 className={cn("mb-3 text-sm font-semibold")}>
80
+ {organizationLocalization.organizationProfile}
81
+ </h2>
82
+
83
+ <Card className={cn(className)} variant={variant} {...props}>
84
+ <Card.Content>
85
+ <Form onSubmit={handleSubmit} className="flex flex-col gap-4">
86
+ <ChangeOrganizationLogo />
87
+
88
+ <TextField
89
+ key={`${activeOrganization?.id}-${activeOrganization?.name}-name`}
90
+ name="name"
91
+ defaultValue={activeOrganization?.name}
92
+ isDisabled={isPending || !activeOrganization}
93
+ >
94
+ <Label>{organizationLocalization.name}</Label>
95
+
96
+ <Input
97
+ className={cn(!activeOrganization && "hidden")}
98
+ autoComplete="organization"
99
+ placeholder={organizationLocalization.namePlaceholder}
100
+ variant={inputVariant}
101
+ />
102
+
103
+ {!activeOrganization && (
104
+ <Skeleton className="h-10 w-full rounded-xl md:h-9" />
105
+ )}
106
+
107
+ <FieldError />
108
+ </TextField>
109
+
110
+ {activeOrganization ? (
111
+ <SlugField
112
+ value={slug}
113
+ onChange={setSlug}
114
+ currentSlug={activeOrganization.slug}
115
+ isDisabled={isPending}
116
+ variant={inputVariant}
117
+ />
118
+ ) : (
119
+ <TextField isDisabled>
120
+ <Label>{organizationLocalization.slug}</Label>
121
+ <Skeleton className="h-10 w-full rounded-xl md:h-9" />
122
+ </TextField>
123
+ )}
124
+
125
+ <Button
126
+ type="submit"
127
+ isPending={isPending}
128
+ isDisabled={!activeOrganization}
129
+ size="sm"
130
+ className="mt-1"
131
+ >
132
+ {isPending && <Spinner color="current" size="sm" />}
133
+
134
+ {localization.settings.saveChanges}
135
+ </Button>
136
+ </Form>
137
+ </Card.Content>
138
+ </Card>
139
+ </div>
140
+ )
141
+ }
@@ -0,0 +1,66 @@
1
+ import {
2
+ type OrganizationAuthClient,
3
+ useAuth,
4
+ useAuthPlugin,
5
+ useSetActiveOrganization
6
+ } from "@better-auth-ui/react"
7
+ import { Gear } from "@gravity-ui/icons"
8
+ import { Button, Spinner } from "@heroui/react"
9
+ import type { Organization } from "better-auth/client"
10
+
11
+ import { organizationPlugin } from "../../../lib/auth/organization-plugin"
12
+ import { OrganizationView } from "./organization-view"
13
+
14
+ export type OrganizationRowProps = {
15
+ organization: Organization
16
+ }
17
+
18
+ /**
19
+ * Single organization row: logo and labels via {@link OrganizationView}, plus Manage.
20
+ */
21
+ export function OrganizationRow({ organization }: OrganizationRowProps) {
22
+ const { authClient, basePaths, navigate } = useAuth()
23
+ const {
24
+ localization: organizationLocalization,
25
+ viewPaths: organizationViewPaths,
26
+ slug
27
+ } = useAuthPlugin(organizationPlugin)
28
+
29
+ const { mutate: setActiveOrganization, isPending: setActivePending } =
30
+ useSetActiveOrganization(authClient as OrganizationAuthClient, {
31
+ onSuccess: () => {
32
+ navigate({
33
+ to: `${basePaths.organization}/${organizationViewPaths.organization.settings}`
34
+ })
35
+ }
36
+ })
37
+
38
+ function manageOrganization() {
39
+ if (slug !== undefined) {
40
+ navigate({
41
+ to: `${basePaths.organization}/${organization.slug}/${organizationViewPaths.organization.settings}`
42
+ })
43
+ } else {
44
+ setActiveOrganization({ organizationId: organization.id })
45
+ }
46
+ }
47
+
48
+ return (
49
+ <div className="flex items-center gap-3">
50
+ <OrganizationView organization={organization} />
51
+
52
+ <Button
53
+ className="ml-auto shrink-0"
54
+ variant="outline"
55
+ size="sm"
56
+ isPending={setActivePending}
57
+ onPress={manageOrganization}
58
+ aria-label={organizationLocalization.manage}
59
+ >
60
+ {setActivePending ? <Spinner color="current" size="sm" /> : <Gear />}
61
+
62
+ {organizationLocalization.manage}
63
+ </Button>
64
+ </div>
65
+ )
66
+ }