@better-auth-ui/heroui 1.6.8 → 1.6.10

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 (195) hide show
  1. package/dist/components/auth/api-key/api-key.d.ts +5 -1
  2. package/dist/components/auth/api-key/api-keys-empty.d.ts +3 -1
  3. package/dist/components/auth/api-key/api-keys.d.ts +9 -1
  4. package/dist/components/auth/api-key/create-api-key-dialog.d.ts +3 -1
  5. package/dist/components/auth/api-key/delete-api-key-dialog.d.ts +3 -1
  6. package/dist/components/auth/api-key/organization-api-keys.d.ts +14 -0
  7. package/dist/components/auth/delete-user/danger-zone.d.ts +1 -1
  8. package/dist/components/auth/delete-user/{delete-user.d.ts → delete-account.d.ts} +2 -2
  9. package/dist/components/auth/email/organization-invitation-email.d.ts +3 -0
  10. package/dist/components/auth/multi-session/switch-account-submenu-content.d.ts +3 -1
  11. package/dist/components/auth/multi-session/switch-account-submenu-item.d.ts +2 -1
  12. package/dist/components/auth/multi-session/switch-account-submenu.d.ts +2 -1
  13. package/dist/components/auth/organization/change-organization-logo.d.ts +4 -0
  14. package/dist/components/auth/organization/create-organization-dialog.d.ts +13 -0
  15. package/dist/components/auth/organization/delete-organization-dialog.d.ts +7 -0
  16. package/dist/components/auth/organization/delete-organization-skeleton.d.ts +4 -0
  17. package/dist/components/auth/organization/delete-organization.d.ts +5 -0
  18. package/dist/components/auth/organization/invite-member-dialog.d.ts +13 -0
  19. package/dist/components/auth/organization/leave-organization-dialog.d.ts +7 -0
  20. package/dist/components/auth/organization/leave-organization.d.ts +4 -0
  21. package/dist/components/auth/organization/organization-danger-zone.d.ts +16 -0
  22. package/dist/components/auth/organization/organization-invitation-row-skeleton.d.ts +5 -0
  23. package/dist/components/auth/organization/organization-invitation-row.d.ts +5 -0
  24. package/dist/components/auth/organization/organization-invitations-empty.d.ts +8 -0
  25. package/dist/components/auth/organization/organization-invitations.d.ts +9 -0
  26. package/dist/components/auth/organization/organization-logo.d.ts +15 -0
  27. package/dist/components/auth/organization/organization-member-row-skeleton.d.ts +4 -0
  28. package/dist/components/auth/organization/organization-member-row.d.ts +9 -0
  29. package/dist/components/auth/organization/organization-members.d.ts +9 -0
  30. package/dist/components/auth/organization/organization-people.d.ts +10 -0
  31. package/dist/components/auth/organization/organization-profile.d.ts +9 -0
  32. package/dist/components/auth/organization/organization-row.d.ts +8 -0
  33. package/dist/components/auth/organization/organization-settings.d.ts +12 -0
  34. package/dist/components/auth/organization/organization-switcher.d.ts +20 -0
  35. package/dist/components/auth/organization/organization-view-skeleton.d.ts +6 -0
  36. package/dist/components/auth/organization/organization-view.d.ts +15 -0
  37. package/dist/components/auth/organization/organization.d.ts +17 -0
  38. package/dist/components/auth/organization/organizations-empty.d.ts +4 -0
  39. package/dist/components/auth/organization/organizations-settings.d.ts +17 -0
  40. package/dist/components/auth/organization/organizations.d.ts +10 -0
  41. package/dist/components/auth/organization/remove-member-dialog.d.ts +9 -0
  42. package/dist/components/auth/organization/slug-field.d.ts +19 -0
  43. package/dist/components/auth/organization/user-invitation-row-skeleton.d.ts +4 -0
  44. package/dist/components/auth/organization/user-invitation-row.d.ts +10 -0
  45. package/dist/components/auth/organization/user-invitations-empty.d.ts +4 -0
  46. package/dist/components/auth/organization/user-invitations.d.ts +10 -0
  47. package/dist/components/auth/settings/settings.d.ts +1 -1
  48. package/dist/components/auth/user/user-avatar.d.ts +1 -1
  49. package/dist/components/auth/user/user-button.d.ts +4 -5
  50. package/dist/components/auth/user/user-view.d.ts +10 -4
  51. package/dist/email.d.ts +1 -0
  52. package/dist/email.js +211 -8
  53. package/dist/index.js +1682 -22
  54. package/dist/lib/auth/api-key-plugin.d.ts +5 -2
  55. package/dist/lib/auth/auth-plugin.d.ts +6 -2
  56. package/dist/lib/auth/delete-user-plugin.d.ts +4 -4
  57. package/dist/lib/auth/magic-link-plugin.d.ts +2 -2
  58. package/dist/lib/auth/multi-session-plugin.d.ts +2 -2
  59. package/dist/lib/auth/organization-plugin.d.ts +37 -0
  60. package/dist/lib/auth/passkey-plugin.d.ts +2 -2
  61. package/dist/lib/auth/theme-plugin.d.ts +3 -3
  62. package/dist/lib/auth/username-plugin.d.ts +8 -8
  63. package/dist/plugins.d.ts +25 -1
  64. package/dist/plugins.js +2824 -22
  65. package/dist/user-view--47tVtJ3.js +113 -0
  66. package/package.json +23 -18
  67. package/src/components/auth/api-key/api-key.tsx +25 -16
  68. package/src/components/auth/api-key/api-keys-empty.tsx +8 -4
  69. package/src/components/auth/api-key/api-keys.tsx +52 -14
  70. package/src/components/auth/api-key/create-api-key-dialog.tsx +15 -2
  71. package/src/components/auth/api-key/delete-api-key-dialog.tsx +10 -2
  72. package/src/components/auth/api-key/organization-api-keys.tsx +55 -0
  73. package/src/components/auth/delete-user/danger-zone.tsx +3 -3
  74. package/src/components/auth/delete-user/{delete-user.tsx → delete-account.tsx} +9 -9
  75. package/src/components/auth/email/organization-invitation-email.tsx +38 -0
  76. package/src/components/auth/error-toaster.tsx +28 -10
  77. package/src/components/auth/forgot-password.tsx +10 -2
  78. package/src/components/auth/multi-session/manage-accounts.tsx +1 -1
  79. package/src/components/auth/multi-session/switch-account-submenu-content.tsx +7 -2
  80. package/src/components/auth/multi-session/switch-account-submenu-item.tsx +4 -2
  81. package/src/components/auth/multi-session/switch-account-submenu.tsx +8 -4
  82. package/src/components/auth/organization/change-organization-logo.tsx +171 -0
  83. package/src/components/auth/organization/create-organization-dialog.tsx +133 -0
  84. package/src/components/auth/organization/delete-organization-dialog.tsx +97 -0
  85. package/src/components/auth/organization/delete-organization-skeleton.tsx +17 -0
  86. package/src/components/auth/organization/delete-organization.tsx +77 -0
  87. package/src/components/auth/organization/invite-member-dialog.tsx +177 -0
  88. package/src/components/auth/organization/leave-organization-dialog.tsx +95 -0
  89. package/src/components/auth/organization/leave-organization.tsx +59 -0
  90. package/src/components/auth/organization/organization-danger-zone.tsx +68 -0
  91. package/src/components/auth/organization/organization-invitation-row-skeleton.tsx +29 -0
  92. package/src/components/auth/organization/organization-invitation-row.tsx +97 -0
  93. package/src/components/auth/organization/organization-invitations-empty.tsx +39 -0
  94. package/src/components/auth/organization/organization-invitations.tsx +351 -0
  95. package/src/components/auth/organization/organization-logo.tsx +64 -0
  96. package/src/components/auth/organization/organization-member-row-skeleton.tsx +24 -0
  97. package/src/components/auth/organization/organization-member-row.tsx +157 -0
  98. package/src/components/auth/organization/organization-members.tsx +284 -0
  99. package/src/components/auth/organization/organization-people.tsx +26 -0
  100. package/src/components/auth/organization/organization-profile.tsx +141 -0
  101. package/src/components/auth/organization/organization-row.tsx +66 -0
  102. package/src/components/auth/organization/organization-settings.tsx +38 -0
  103. package/src/components/auth/organization/organization-switcher.tsx +233 -0
  104. package/src/components/auth/organization/organization-view-skeleton.tsx +36 -0
  105. package/src/components/auth/organization/organization-view.tsx +109 -0
  106. package/src/components/auth/organization/organization.tsx +162 -0
  107. package/src/components/auth/organization/organizations-empty.tsx +35 -0
  108. package/src/components/auth/organization/organizations-settings.tsx +36 -0
  109. package/src/components/auth/organization/organizations.tsx +80 -0
  110. package/src/components/auth/organization/remove-member-dialog.tsx +95 -0
  111. package/src/components/auth/organization/slug-field.tsx +111 -0
  112. package/src/components/auth/organization/user-invitation-row-skeleton.tsx +17 -0
  113. package/src/components/auth/organization/user-invitation-row.tsx +90 -0
  114. package/src/components/auth/organization/user-invitations-empty.tsx +30 -0
  115. package/src/components/auth/organization/user-invitations.tsx +59 -0
  116. package/src/components/auth/settings/account/change-email.tsx +1 -1
  117. package/src/components/auth/settings/account/user-profile.tsx +5 -5
  118. package/src/components/auth/settings/security/active-sessions.tsx +6 -4
  119. package/src/components/auth/settings/security/linked-accounts.tsx +2 -2
  120. package/src/components/auth/settings/settings.tsx +77 -14
  121. package/src/components/auth/user/user-avatar.tsx +4 -1
  122. package/src/components/auth/user/user-button.tsx +14 -11
  123. package/src/components/auth/user/user-view.tsx +32 -12
  124. package/src/email.ts +1 -0
  125. package/src/lib/auth/api-key-plugin.ts +10 -4
  126. package/src/lib/auth/auth-plugin.ts +6 -1
  127. package/src/lib/auth/organization-plugin.tsx +32 -0
  128. package/src/plugins.ts +25 -1
  129. package/dist/components/auth/additional-field.js +0 -270
  130. package/dist/components/auth/api-key/api-key-skeleton.js +0 -21
  131. package/dist/components/auth/api-key/api-key.js +0 -55
  132. package/dist/components/auth/api-key/api-keys-empty.js +0 -35
  133. package/dist/components/auth/api-key/api-keys.js +0 -41
  134. package/dist/components/auth/api-key/create-api-key-dialog.js +0 -74
  135. package/dist/components/auth/api-key/delete-api-key-dialog.js +0 -51
  136. package/dist/components/auth/api-key/new-api-key-dialog.js +0 -60
  137. package/dist/components/auth/auth-provider.js +0 -17
  138. package/dist/components/auth/auth.js +0 -63
  139. package/dist/components/auth/delete-user/danger-zone.js +0 -18
  140. package/dist/components/auth/delete-user/delete-user.js +0 -95
  141. package/dist/components/auth/email/email-changed-email.js +0 -30
  142. package/dist/components/auth/email/email-verification-email.js +0 -30
  143. package/dist/components/auth/email/magic-link-email.js +0 -30
  144. package/dist/components/auth/email/new-device-email.js +0 -30
  145. package/dist/components/auth/email/otp-email.js +0 -29
  146. package/dist/components/auth/email/password-changed-email.js +0 -30
  147. package/dist/components/auth/email/reset-password-email.js +0 -30
  148. package/dist/components/auth/error-toaster.js +0 -17
  149. package/dist/components/auth/field-separator.js +0 -18
  150. package/dist/components/auth/forgot-password.js +0 -89
  151. package/dist/components/auth/magic-link/magic-link-button.js +0 -20
  152. package/dist/components/auth/magic-link/magic-link.js +0 -87
  153. package/dist/components/auth/multi-session/manage-account.js +0 -49
  154. package/dist/components/auth/multi-session/manage-accounts.js +0 -34
  155. package/dist/components/auth/multi-session/switch-account-submenu-content.js +0 -28
  156. package/dist/components/auth/multi-session/switch-account-submenu-item.js +0 -20
  157. package/dist/components/auth/multi-session/switch-account-submenu.js +0 -24
  158. package/dist/components/auth/passkey/add-passkey-dialog.js +0 -65
  159. package/dist/components/auth/passkey/delete-passkey-dialog.js +0 -47
  160. package/dist/components/auth/passkey/passkey-button.js +0 -24
  161. package/dist/components/auth/passkey/passkey-skeleton.js +0 -17
  162. package/dist/components/auth/passkey/passkey.js +0 -48
  163. package/dist/components/auth/passkey/passkeys-empty.js +0 -35
  164. package/dist/components/auth/passkey/passkeys.js +0 -42
  165. package/dist/components/auth/provider-button.js +0 -25
  166. package/dist/components/auth/provider-buttons.js +0 -19
  167. package/dist/components/auth/reset-password.js +0 -147
  168. package/dist/components/auth/settings/account/account-settings.js +0 -20
  169. package/dist/components/auth/settings/account/change-avatar.js +0 -89
  170. package/dist/components/auth/settings/account/change-email.js +0 -54
  171. package/dist/components/auth/settings/account/user-profile.js +0 -86
  172. package/dist/components/auth/settings/security/active-session.js +0 -59
  173. package/dist/components/auth/settings/security/active-sessions.js +0 -34
  174. package/dist/components/auth/settings/security/change-password.js +0 -189
  175. package/dist/components/auth/settings/security/linked-account.js +0 -56
  176. package/dist/components/auth/settings/security/linked-accounts.js +0 -45
  177. package/dist/components/auth/settings/security/security-settings.js +0 -22
  178. package/dist/components/auth/settings/settings.js +0 -45
  179. package/dist/components/auth/sign-in.js +0 -133
  180. package/dist/components/auth/sign-out.js +0 -27
  181. package/dist/components/auth/sign-up.js +0 -212
  182. package/dist/components/auth/theme/appearance.js +0 -72
  183. package/dist/components/auth/theme/theme-toggle-item.js +0 -62
  184. package/dist/components/auth/user/user-avatar.js +0 -29
  185. package/dist/components/auth/user/user-button.js +0 -74
  186. package/dist/components/auth/user/user-view.js +0 -37
  187. package/dist/components/auth/username/sign-in-username.js +0 -148
  188. package/dist/components/auth/username/username-field.js +0 -53
  189. package/dist/lib/auth/api-key-plugin.js +0 -10
  190. package/dist/lib/auth/delete-user-plugin.js +0 -10
  191. package/dist/lib/auth/magic-link-plugin.js +0 -13
  192. package/dist/lib/auth/multi-session-plugin.js +0 -12
  193. package/dist/lib/auth/passkey-plugin.js +0 -12
  194. package/dist/lib/auth/theme-plugin.js +0 -23
  195. package/dist/lib/auth/username-plugin.js +0 -18
@@ -0,0 +1,351 @@
1
+ import type { OrganizationLocalization } from "@better-auth-ui/core/plugins"
2
+ import {
3
+ type OrganizationAuthClient,
4
+ useAuth,
5
+ useAuthPlugin,
6
+ useHasPermission,
7
+ useListOrganizationInvitations
8
+ } from "@better-auth-ui/react"
9
+ import { ChevronUp, Funnel, Xmark } from "@gravity-ui/icons"
10
+ import {
11
+ Button,
12
+ Chip,
13
+ cn,
14
+ Dropdown,
15
+ Label,
16
+ SearchField,
17
+ type SortDescriptor,
18
+ Table
19
+ } from "@heroui/react"
20
+ import { type ComponentProps, type ReactNode, useMemo, useState } from "react"
21
+
22
+ import { organizationPlugin } from "../../../lib/auth/organization-plugin"
23
+ import { InviteMemberDialog } from "./invite-member-dialog"
24
+ import { OrganizationInvitationTableRow } from "./organization-invitation-row"
25
+ import { OrganizationInvitationRowSkeleton } from "./organization-invitation-row-skeleton"
26
+ import { OrganizationInvitationsEmpty } from "./organization-invitations-empty"
27
+
28
+ /** Props for the {@link OrganizationInvitations} component. */
29
+ export type OrganizationInvitationsProps = {
30
+ className?: string
31
+ }
32
+
33
+ /**
34
+ * Organization invitations table with invite control and per-row actions.
35
+ */
36
+ export function OrganizationInvitations({
37
+ className,
38
+ ...props
39
+ }: OrganizationInvitationsProps & ComponentProps<"div">) {
40
+ const { authClient, localization } = useAuth()
41
+ const { localization: organizationLocalization, roles } =
42
+ useAuthPlugin(organizationPlugin)
43
+
44
+ const { data: invitations, isPending: invitationsPending } =
45
+ useListOrganizationInvitations(authClient as OrganizationAuthClient)
46
+
47
+ const { isPending: invitationPermissionPending } = useHasPermission(
48
+ authClient as OrganizationAuthClient,
49
+ {
50
+ permissions: { invitation: ["cancel"] }
51
+ }
52
+ )
53
+
54
+ const isPending = invitationsPending || invitationPermissionPending
55
+
56
+ const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>()
57
+ const [roleFilter, setRoleFilter] = useState("all")
58
+ const [statusFilter, setStatusFilter] = useState("all")
59
+ const [search, setSearch] = useState("")
60
+
61
+ const filteredInvitations = useMemo(() => {
62
+ return invitations?.filter(
63
+ (invitation) =>
64
+ (roleFilter === "all" || invitation.role === roleFilter) &&
65
+ (statusFilter === "all" || invitation.status === statusFilter) &&
66
+ invitation.email.toLowerCase().includes(search.toLowerCase())
67
+ )
68
+ }, [search, invitations, roleFilter, statusFilter])
69
+
70
+ const sortedInvitations = useMemo(() => {
71
+ if (!sortDescriptor) return filteredInvitations
72
+ if (!filteredInvitations) return filteredInvitations
73
+
74
+ return [...filteredInvitations].sort((a, b) => {
75
+ const col = sortDescriptor.column as keyof typeof a
76
+ let cmp = 0
77
+
78
+ if (col === "createdAt") {
79
+ cmp = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
80
+ } else {
81
+ cmp = String(a[col]).localeCompare(String(b[col]))
82
+ }
83
+
84
+ if (sortDescriptor.direction === "descending") {
85
+ cmp *= -1
86
+ }
87
+
88
+ return cmp
89
+ })
90
+ }, [sortDescriptor, filteredInvitations])
91
+
92
+ const [inviteOpen, setInviteOpen] = useState(false)
93
+
94
+ return (
95
+ <div className={cn("flex flex-col gap-3", className)} {...props}>
96
+ <h3 className="truncate text-sm font-semibold">
97
+ {organizationLocalization.invitations}
98
+ </h3>
99
+
100
+ <div className="flex flex-col gap-4">
101
+ <div className="flex items-center gap-3">
102
+ <SearchField
103
+ className="min-w-0"
104
+ aria-label={organizationLocalization.search}
105
+ value={search}
106
+ onChange={setSearch}
107
+ isDisabled={isPending}
108
+ >
109
+ <SearchField.Group>
110
+ <SearchField.SearchIcon />
111
+
112
+ <SearchField.Input
113
+ placeholder={organizationLocalization.search}
114
+ className="sm:w-[200px]"
115
+ />
116
+
117
+ <SearchField.ClearButton />
118
+ </SearchField.Group>
119
+ </SearchField>
120
+
121
+ <Dropdown>
122
+ <Button size="sm" variant="secondary" isDisabled={isPending}>
123
+ <Funnel />
124
+
125
+ {organizationLocalization.role}
126
+ </Button>
127
+
128
+ <Dropdown.Popover>
129
+ <Dropdown.Menu
130
+ selectionMode="single"
131
+ selectedKeys={new Set([roleFilter])}
132
+ onSelectionChange={(keys) => {
133
+ const key = [...keys][0] as string | undefined
134
+ setRoleFilter(key ?? "all")
135
+ }}
136
+ >
137
+ <Dropdown.Item
138
+ id="all"
139
+ textValue={organizationLocalization.all}
140
+ >
141
+ <Label>{organizationLocalization.all}</Label>
142
+
143
+ <Dropdown.ItemIndicator />
144
+ </Dropdown.Item>
145
+
146
+ {Object.entries(roles).map(([key, label]) => (
147
+ <Dropdown.Item key={key} id={key} textValue={label}>
148
+ <Label>{label}</Label>
149
+
150
+ <Dropdown.ItemIndicator />
151
+ </Dropdown.Item>
152
+ ))}
153
+ </Dropdown.Menu>
154
+ </Dropdown.Popover>
155
+ </Dropdown>
156
+
157
+ <Dropdown>
158
+ <Button size="sm" variant="secondary" isDisabled={isPending}>
159
+ <Funnel />
160
+
161
+ {organizationLocalization.status}
162
+ </Button>
163
+
164
+ <Dropdown.Popover>
165
+ <Dropdown.Menu
166
+ selectionMode="single"
167
+ selectedKeys={new Set([statusFilter])}
168
+ onSelectionChange={(keys) => {
169
+ const key = [...keys][0] as string | undefined
170
+ setStatusFilter(key ?? "all")
171
+ }}
172
+ >
173
+ <Dropdown.Item
174
+ id="all"
175
+ textValue={organizationLocalization.all}
176
+ >
177
+ <Label>{organizationLocalization.all}</Label>
178
+
179
+ <Dropdown.ItemIndicator />
180
+ </Dropdown.Item>
181
+
182
+ {["pending", "accepted", "rejected", "canceled"].map(
183
+ (status) => (
184
+ <Dropdown.Item
185
+ key={status}
186
+ id={status}
187
+ textValue={
188
+ organizationLocalization[
189
+ status as keyof OrganizationLocalization
190
+ ] ?? status
191
+ }
192
+ >
193
+ <Label>
194
+ {organizationLocalization[
195
+ status as keyof OrganizationLocalization
196
+ ] ?? status}
197
+ </Label>
198
+
199
+ <Dropdown.ItemIndicator />
200
+ </Dropdown.Item>
201
+ )
202
+ )}
203
+ </Dropdown.Menu>
204
+ </Dropdown.Popover>
205
+ </Dropdown>
206
+ </div>
207
+
208
+ {(roleFilter !== "all" || statusFilter !== "all") && (
209
+ <div className="flex flex-wrap gap-2">
210
+ {roleFilter !== "all" && (
211
+ <Chip size="sm" variant="secondary" className="w-fit">
212
+ <Chip.Label>
213
+ {organizationLocalization.role}:{" "}
214
+ <span className="capitalize">
215
+ {roles?.[roleFilter] ?? roleFilter}
216
+ </span>
217
+ </Chip.Label>
218
+
219
+ <button
220
+ type="button"
221
+ aria-label={organizationLocalization.clear}
222
+ className="text-muted hover:text-foreground inline-flex cursor-pointer items-center"
223
+ onClick={() => setRoleFilter("all")}
224
+ >
225
+ <Xmark className="size-3" />
226
+ </button>
227
+ </Chip>
228
+ )}
229
+
230
+ {statusFilter !== "all" && (
231
+ <Chip size="sm" variant="secondary" className="w-fit">
232
+ <Chip.Label>
233
+ {organizationLocalization.status}:{" "}
234
+ {organizationLocalization[
235
+ statusFilter as keyof OrganizationLocalization
236
+ ] ?? statusFilter}
237
+ </Chip.Label>
238
+
239
+ <button
240
+ type="button"
241
+ aria-label={organizationLocalization.clear}
242
+ className="text-muted hover:text-foreground inline-flex cursor-pointer items-center"
243
+ onClick={() => setStatusFilter("all")}
244
+ >
245
+ <Xmark className="size-3" />
246
+ </button>
247
+ </Chip>
248
+ )}
249
+ </div>
250
+ )}
251
+
252
+ <Table>
253
+ <Table.ScrollContainer>
254
+ <Table.Content
255
+ aria-label={organizationLocalization.invitations}
256
+ sortDescriptor={sortDescriptor}
257
+ onSortChange={(descriptor) => {
258
+ const shouldReset =
259
+ sortDescriptor?.column === descriptor.column &&
260
+ descriptor.direction === "ascending"
261
+ setSortDescriptor(shouldReset ? undefined : descriptor)
262
+ }}
263
+ >
264
+ <Table.Header>
265
+ <Table.Column allowsSorting isRowHeader id="email">
266
+ {({ sortDirection }) => (
267
+ <SortableColumnHeader sortDirection={sortDirection}>
268
+ {localization.auth.email}
269
+ </SortableColumnHeader>
270
+ )}
271
+ </Table.Column>
272
+
273
+ <Table.Column allowsSorting id="createdAt">
274
+ {({ sortDirection }) => (
275
+ <SortableColumnHeader sortDirection={sortDirection}>
276
+ {organizationLocalization.invitedAt}
277
+ </SortableColumnHeader>
278
+ )}
279
+ </Table.Column>
280
+
281
+ <Table.Column allowsSorting id="role">
282
+ {({ sortDirection }) => (
283
+ <SortableColumnHeader sortDirection={sortDirection}>
284
+ {organizationLocalization.role}
285
+ </SortableColumnHeader>
286
+ )}
287
+ </Table.Column>
288
+
289
+ <Table.Column allowsSorting id="status">
290
+ {({ sortDirection }) => (
291
+ <SortableColumnHeader sortDirection={sortDirection}>
292
+ {organizationLocalization.status}
293
+ </SortableColumnHeader>
294
+ )}
295
+ </Table.Column>
296
+
297
+ <Table.Column className="text-end">
298
+ {organizationLocalization.actions}
299
+ </Table.Column>
300
+ </Table.Header>
301
+
302
+ <Table.Body
303
+ renderEmptyState={() => (
304
+ <OrganizationInvitationsEmpty
305
+ onInvitePress={() => setInviteOpen(true)}
306
+ />
307
+ )}
308
+ >
309
+ {isPending ? (
310
+ <OrganizationInvitationRowSkeleton />
311
+ ) : (
312
+ sortedInvitations?.map((invitation) => (
313
+ <OrganizationInvitationTableRow
314
+ key={invitation.id}
315
+ invitation={invitation}
316
+ />
317
+ ))
318
+ )}
319
+ </Table.Body>
320
+ </Table.Content>
321
+ </Table.ScrollContainer>
322
+ </Table>
323
+ </div>
324
+
325
+ <InviteMemberDialog isOpen={inviteOpen} onOpenChange={setInviteOpen} />
326
+ </div>
327
+ )
328
+ }
329
+
330
+ function SortableColumnHeader({
331
+ children,
332
+ sortDirection
333
+ }: {
334
+ children: ReactNode
335
+ sortDirection?: "ascending" | "descending"
336
+ }) {
337
+ return (
338
+ <span className="flex items-center justify-between">
339
+ {children}
340
+
341
+ {!!sortDirection && (
342
+ <ChevronUp
343
+ className={cn(
344
+ "size-3 transform transition-transform duration-100 ease-out",
345
+ sortDirection === "descending" ? "rotate-180" : ""
346
+ )}
347
+ />
348
+ )}
349
+ </span>
350
+ )
351
+ }
@@ -0,0 +1,64 @@
1
+ import { Briefcase } from "@gravity-ui/icons"
2
+ import { Avatar, type AvatarProps, cn, Skeleton } from "@heroui/react"
3
+ import type { Organization } from "better-auth/client"
4
+ import type { ReactNode } from "react"
5
+
6
+ export type OrganizationLogoProps = {
7
+ className?: string
8
+ fallback?: ReactNode
9
+ isPending?: boolean
10
+ organization?: Partial<Organization>
11
+ size?: AvatarProps["size"]
12
+ }
13
+
14
+ /**
15
+ * Renders an organization logo image, initials fallback, or a briefcase icon;
16
+ * shows a circular skeleton while loading when `isPending` is set and `organization` is undefined.
17
+ */
18
+ export function OrganizationLogo({
19
+ className,
20
+ fallback,
21
+ isPending,
22
+ organization,
23
+ size = "sm",
24
+ style,
25
+ ...props
26
+ }: OrganizationLogoProps & AvatarProps) {
27
+ if (isPending && !organization) {
28
+ return (
29
+ <Skeleton
30
+ className={cn(
31
+ "rounded-full",
32
+ size === "sm" ? "size-8" : size === "md" ? "size-10" : "size-12",
33
+ className
34
+ )}
35
+ style={style}
36
+ />
37
+ )
38
+ }
39
+
40
+ const initials = organization?.name?.slice(0, 2).toUpperCase()
41
+
42
+ return (
43
+ <Avatar
44
+ size={size}
45
+ className={cn("rounded-full", className)}
46
+ style={style}
47
+ {...props}
48
+ >
49
+ <Avatar.Image
50
+ alt={organization?.name ?? "Organization"}
51
+ src={organization?.logo ?? undefined}
52
+ />
53
+
54
+ <Avatar.Fallback
55
+ className={cn(
56
+ size === "lg" ? "text-xl" : size === "md" ? "text-base" : "text-sm"
57
+ )}
58
+ delayMs={organization?.logo ? 600 : undefined}
59
+ >
60
+ {fallback || initials || <Briefcase className="size-4" />}
61
+ </Avatar.Fallback>
62
+ </Avatar>
63
+ )
64
+ }
@@ -0,0 +1,24 @@
1
+ import { Skeleton, Table } from "@heroui/react"
2
+
3
+ import { UserView } from "../user/user-view"
4
+
5
+ /**
6
+ * Placeholder row matching {@link OrganizationMemberRow} while members load.
7
+ */
8
+ export function OrganizationMemberRowSkeleton() {
9
+ return (
10
+ <Table.Row>
11
+ <Table.Cell>
12
+ <UserView isPending />
13
+ </Table.Cell>
14
+
15
+ <Table.Cell>
16
+ <Skeleton className="h-4 w-18 rounded-lg" />
17
+ </Table.Cell>
18
+
19
+ <Table.Cell className="flex justify-end">
20
+ <Skeleton className="size-8 rounded-full" />
21
+ </Table.Cell>
22
+ </Table.Row>
23
+ )
24
+ }
@@ -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
+ }