@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.
- package/dist/components/auth/additional-field.d.ts +2 -2
- package/dist/components/auth/additional-field.js +133 -266
- package/dist/components/auth/api-key/api-key-skeleton.js +4 -20
- package/dist/components/auth/api-key/api-key.d.ts +6 -2
- package/dist/components/auth/api-key/api-key.js +16 -54
- package/dist/components/auth/api-key/api-keys-empty.d.ts +3 -1
- package/dist/components/auth/api-key/api-keys-empty.js +8 -34
- package/dist/components/auth/api-key/api-keys.d.ts +10 -2
- package/dist/components/auth/api-key/api-keys.js +21 -40
- package/dist/components/auth/api-key/create-api-key-dialog.d.ts +3 -1
- package/dist/components/auth/api-key/create-api-key-dialog.js +43 -73
- package/dist/components/auth/api-key/delete-api-key-dialog.d.ts +4 -2
- package/dist/components/auth/api-key/delete-api-key-dialog.js +16 -50
- package/dist/components/auth/api-key/new-api-key-dialog.js +23 -59
- package/dist/components/auth/api-key/organization-api-keys.d.ts +14 -0
- package/dist/components/auth/api-key/organization-api-keys.js +22 -0
- package/dist/components/auth/auth-provider.d.ts +1 -1
- package/dist/components/auth/auth-provider.js +10 -16
- package/dist/components/auth/auth.d.ts +3 -3
- package/dist/components/auth/auth.js +86 -61
- package/dist/components/auth/delete-user/danger-zone.d.ts +3 -3
- package/dist/components/auth/delete-user/danger-zone.js +11 -17
- package/dist/components/auth/delete-user/delete-account.d.ts +9 -0
- package/dist/components/auth/delete-user/delete-account.js +50 -0
- package/dist/components/auth/email/email-changed-email.d.ts +2 -2
- package/dist/components/auth/email/email-changed-email.js +22 -29
- package/dist/components/auth/email/email-verification-email.d.ts +2 -2
- package/dist/components/auth/email/email-verification-email.js +22 -29
- package/dist/components/auth/email/magic-link-email.d.ts +2 -2
- package/dist/components/auth/email/magic-link-email.js +22 -29
- package/dist/components/auth/email/new-device-email.d.ts +2 -2
- package/dist/components/auth/email/new-device-email.js +22 -29
- package/dist/components/auth/email/organization-invitation-email.d.ts +3 -0
- package/dist/components/auth/email/organization-invitation-email.js +23 -0
- package/dist/components/auth/email/otp-email.d.ts +2 -2
- package/dist/components/auth/email/otp-email.js +21 -28
- package/dist/components/auth/email/password-changed-email.d.ts +2 -2
- package/dist/components/auth/email/password-changed-email.js +22 -29
- package/dist/components/auth/email/reset-password-email.d.ts +2 -2
- package/dist/components/auth/email/reset-password-email.js +22 -29
- package/dist/components/auth/error-toaster.js +32 -16
- package/dist/components/auth/field-separator.d.ts +1 -1
- package/dist/components/auth/field-separator.js +4 -17
- package/dist/components/auth/forgot-password.d.ts +1 -1
- package/dist/components/auth/forgot-password.js +36 -88
- package/dist/components/auth/magic-link/magic-link-button.d.ts +1 -1
- package/dist/components/auth/magic-link/magic-link-button.js +33 -19
- package/dist/components/auth/magic-link/magic-link.d.ts +2 -2
- package/dist/components/auth/magic-link/magic-link.js +39 -86
- package/dist/components/auth/multi-session/manage-account.d.ts +1 -1
- package/dist/components/auth/multi-session/manage-account.js +32 -48
- package/dist/components/auth/multi-session/manage-accounts.d.ts +1 -1
- package/dist/components/auth/multi-session/manage-accounts.js +32 -33
- package/dist/components/auth/multi-session/switch-account-submenu-content.d.ts +3 -1
- package/dist/components/auth/multi-session/switch-account-submenu-content.js +24 -27
- package/dist/components/auth/multi-session/switch-account-submenu-item.d.ts +3 -2
- package/dist/components/auth/multi-session/switch-account-submenu-item.js +16 -19
- package/dist/components/auth/multi-session/switch-account-submenu.d.ts +2 -1
- package/dist/components/auth/multi-session/switch-account-submenu.js +23 -23
- package/dist/components/auth/organization/change-organization-logo.d.ts +4 -0
- package/dist/components/auth/organization/change-organization-logo.js +69 -0
- package/dist/components/auth/organization/create-organization-dialog.d.ts +13 -0
- package/dist/components/auth/organization/create-organization-dialog.js +37 -0
- package/dist/components/auth/organization/delete-organization-dialog.d.ts +7 -0
- package/dist/components/auth/organization/delete-organization-dialog.js +25 -0
- package/dist/components/auth/organization/delete-organization-skeleton.d.ts +4 -0
- package/dist/components/auth/organization/delete-organization-skeleton.js +8 -0
- package/dist/components/auth/organization/delete-organization.d.ts +5 -0
- package/dist/components/auth/organization/delete-organization.js +27 -0
- package/dist/components/auth/organization/invite-member-dialog.d.ts +13 -0
- package/dist/components/auth/organization/invite-member-dialog.js +47 -0
- package/dist/components/auth/organization/leave-organization-dialog.d.ts +7 -0
- package/dist/components/auth/organization/leave-organization-dialog.js +21 -0
- package/dist/components/auth/organization/leave-organization.d.ts +4 -0
- package/dist/components/auth/organization/leave-organization.js +16 -0
- package/dist/components/auth/organization/organization-danger-zone.d.ts +16 -0
- package/dist/components/auth/organization/organization-danger-zone.js +23 -0
- package/dist/components/auth/organization/organization-invitation-row-skeleton.d.ts +5 -0
- package/dist/components/auth/organization/organization-invitation-row-skeleton.js +9 -0
- package/dist/components/auth/organization/organization-invitation-row.d.ts +5 -0
- package/dist/components/auth/organization/organization-invitation-row.js +31 -0
- package/dist/components/auth/organization/organization-invitations-empty.d.ts +8 -0
- package/dist/components/auth/organization/organization-invitations-empty.js +13 -0
- package/dist/components/auth/organization/organization-invitations.d.ts +9 -0
- package/dist/components/auth/organization/organization-invitations.js +66 -0
- package/dist/components/auth/organization/organization-logo.d.ts +15 -0
- package/dist/components/auth/organization/organization-logo.js +14 -0
- package/dist/components/auth/organization/organization-member-row-skeleton.d.ts +4 -0
- package/dist/components/auth/organization/organization-member-row-skeleton.js +9 -0
- package/dist/components/auth/organization/organization-member-row.d.ts +9 -0
- package/dist/components/auth/organization/organization-member-row.js +34 -0
- package/dist/components/auth/organization/organization-members.d.ts +9 -0
- package/dist/components/auth/organization/organization-members.js +67 -0
- package/dist/components/auth/organization/organization-people.d.ts +10 -0
- package/dist/components/auth/organization/organization-people.js +11 -0
- package/dist/components/auth/organization/organization-profile.d.ts +9 -0
- package/dist/components/auth/organization/organization-profile.js +34 -0
- package/dist/components/auth/organization/organization-row.d.ts +8 -0
- package/dist/components/auth/organization/organization-row.js +31 -0
- package/dist/components/auth/organization/organization-settings.d.ts +12 -0
- package/dist/components/auth/organization/organization-settings.js +13 -0
- package/dist/components/auth/organization/organization-switcher.d.ts +20 -0
- package/dist/components/auth/organization/organization-switcher.js +48 -0
- package/dist/components/auth/organization/organization-view-skeleton.d.ts +6 -0
- package/dist/components/auth/organization/organization-view-skeleton.js +9 -0
- package/dist/components/auth/organization/organization-view.d.ts +15 -0
- package/dist/components/auth/organization/organization-view.js +31 -0
- package/dist/components/auth/organization/organization.d.ts +17 -0
- package/dist/components/auth/organization/organization.js +60 -0
- package/dist/components/auth/organization/organizations-empty.d.ts +4 -0
- package/dist/components/auth/organization/organizations-empty.js +9 -0
- package/dist/components/auth/organization/organizations-settings.d.ts +17 -0
- package/dist/components/auth/organization/organizations-settings.js +17 -0
- package/dist/components/auth/organization/organizations.d.ts +10 -0
- package/dist/components/auth/organization/organizations.js +21 -0
- package/dist/components/auth/organization/remove-member-dialog.d.ts +9 -0
- package/dist/components/auth/organization/remove-member-dialog.js +20 -0
- package/dist/components/auth/organization/slug-field.d.ts +19 -0
- package/dist/components/auth/organization/slug-field.js +38 -0
- package/dist/components/auth/organization/user-invitation-row-skeleton.d.ts +4 -0
- package/dist/components/auth/organization/user-invitation-row-skeleton.js +8 -0
- package/dist/components/auth/organization/user-invitation-row.d.ts +10 -0
- package/dist/components/auth/organization/user-invitation-row.js +22 -0
- package/dist/components/auth/organization/user-invitations-empty.d.ts +4 -0
- package/dist/components/auth/organization/user-invitations-empty.js +12 -0
- package/dist/components/auth/organization/user-invitations.d.ts +10 -0
- package/dist/components/auth/organization/user-invitations.js +18 -0
- package/dist/components/auth/passkey/add-passkey-dialog.js +21 -64
- package/dist/components/auth/passkey/delete-passkey-dialog.js +13 -46
- package/dist/components/auth/passkey/passkey-button.d.ts +1 -1
- package/dist/components/auth/passkey/passkey-button.js +24 -23
- package/dist/components/auth/passkey/passkey-skeleton.js +4 -16
- package/dist/components/auth/passkey/passkey.d.ts +1 -1
- package/dist/components/auth/passkey/passkey.js +16 -47
- package/dist/components/auth/passkey/passkeys-empty.js +8 -34
- package/dist/components/auth/passkey/passkeys.d.ts +1 -1
- package/dist/components/auth/passkey/passkeys.js +15 -41
- package/dist/components/auth/provider-button.d.ts +2 -2
- package/dist/components/auth/provider-button.js +28 -24
- package/dist/components/auth/provider-buttons.js +28 -18
- package/dist/components/auth/reset-password.d.ts +1 -1
- package/dist/components/auth/reset-password.js +56 -146
- package/dist/components/auth/settings/account/account-settings.d.ts +2 -2
- package/dist/components/auth/settings/account/account-settings.js +21 -19
- package/dist/components/auth/settings/account/change-avatar.js +53 -88
- package/dist/components/auth/settings/account/change-email.d.ts +1 -1
- package/dist/components/auth/settings/account/change-email.js +27 -53
- package/dist/components/auth/settings/account/user-profile.d.ts +1 -1
- package/dist/components/auth/settings/account/user-profile.js +64 -85
- package/dist/components/auth/settings/security/active-session.d.ts +1 -1
- package/dist/components/auth/settings/security/active-session.js +50 -57
- package/dist/components/auth/settings/security/active-sessions.d.ts +1 -1
- package/dist/components/auth/settings/security/active-sessions.js +21 -32
- package/dist/components/auth/settings/security/change-password.d.ts +1 -1
- package/dist/components/auth/settings/security/change-password.js +76 -186
- package/dist/components/auth/settings/security/linked-account.d.ts +1 -1
- package/dist/components/auth/settings/security/linked-account.js +37 -55
- package/dist/components/auth/settings/security/linked-accounts.d.ts +1 -1
- package/dist/components/auth/settings/security/linked-accounts.js +34 -43
- package/dist/components/auth/settings/security/security-settings.d.ts +2 -2
- package/dist/components/auth/settings/security/security-settings.js +19 -21
- package/dist/components/auth/settings/settings.d.ts +4 -4
- package/dist/components/auth/settings/settings.js +52 -44
- package/dist/components/auth/sign-in.d.ts +2 -2
- package/dist/components/auth/sign-in.js +63 -132
- package/dist/components/auth/sign-out.js +32 -26
- package/dist/components/auth/sign-up.d.ts +2 -2
- package/dist/components/auth/sign-up.js +97 -211
- package/dist/components/auth/theme/appearance.d.ts +1 -1
- package/dist/components/auth/theme/appearance.js +23 -71
- package/dist/components/auth/theme/theme-toggle-item.js +56 -61
- package/dist/components/auth/user/user-avatar.d.ts +4 -4
- package/dist/components/auth/user/user-avatar.js +31 -28
- package/dist/components/auth/user/user-button.d.ts +6 -7
- package/dist/components/auth/user/user-button.js +38 -72
- package/dist/components/auth/user/user-view.d.ts +13 -7
- package/dist/components/auth/user/user-view.js +26 -36
- package/dist/components/auth/username/sign-in-username.d.ts +2 -2
- package/dist/components/auth/username/sign-in-username.js +87 -147
- package/dist/components/auth/username/username-field.d.ts +1 -1
- package/dist/components/auth/username/username-field.js +44 -52
- package/dist/email.d.ts +8 -7
- package/dist/email.js +8 -8
- package/dist/index.d.ts +21 -21
- package/dist/index.js +21 -22
- package/dist/lib/auth/api-key-plugin.d.ts +7 -4
- package/dist/lib/auth/api-key-plugin.js +12 -10
- package/dist/lib/auth/auth-plugin.d.ts +10 -6
- package/dist/lib/auth/auth-plugin.js +1 -0
- package/dist/lib/auth/delete-user-plugin.d.ts +6 -6
- package/dist/lib/auth/delete-user-plugin.js +6 -9
- package/dist/lib/auth/magic-link-plugin.d.ts +5 -5
- package/dist/lib/auth/magic-link-plugin.js +17 -12
- package/dist/lib/auth/multi-session-plugin.d.ts +5 -5
- package/dist/lib/auth/multi-session-plugin.js +8 -11
- package/dist/lib/auth/organization-plugin.d.ts +37 -0
- package/dist/lib/auth/organization-plugin.js +19 -0
- package/dist/lib/auth/passkey-plugin.d.ts +5 -5
- package/dist/lib/auth/passkey-plugin.js +8 -11
- package/dist/lib/auth/theme-plugin.d.ts +6 -6
- package/dist/lib/auth/theme-plugin.js +25 -22
- package/dist/lib/auth/username-plugin.d.ts +11 -11
- package/dist/lib/auth/username-plugin.js +18 -17
- package/dist/plugins.d.ts +46 -22
- package/dist/plugins.js +47 -22
- package/package.json +25 -19
- package/src/components/auth/api-key/api-key.tsx +25 -16
- package/src/components/auth/api-key/api-keys-empty.tsx +8 -4
- package/src/components/auth/api-key/api-keys.tsx +52 -14
- package/src/components/auth/api-key/create-api-key-dialog.tsx +15 -2
- package/src/components/auth/api-key/delete-api-key-dialog.tsx +10 -2
- package/src/components/auth/api-key/organization-api-keys.tsx +55 -0
- package/src/components/auth/delete-user/danger-zone.tsx +3 -3
- package/src/components/auth/delete-user/{delete-user.tsx → delete-account.tsx} +9 -9
- package/src/components/auth/email/organization-invitation-email.tsx +38 -0
- package/src/components/auth/error-toaster.tsx +28 -10
- package/src/components/auth/forgot-password.tsx +10 -2
- package/src/components/auth/multi-session/manage-accounts.tsx +1 -1
- package/src/components/auth/multi-session/switch-account-submenu-content.tsx +7 -2
- package/src/components/auth/multi-session/switch-account-submenu-item.tsx +4 -2
- package/src/components/auth/multi-session/switch-account-submenu.tsx +8 -4
- package/src/components/auth/organization/change-organization-logo.tsx +171 -0
- package/src/components/auth/organization/create-organization-dialog.tsx +133 -0
- package/src/components/auth/organization/delete-organization-dialog.tsx +97 -0
- package/src/components/auth/organization/delete-organization-skeleton.tsx +17 -0
- package/src/components/auth/organization/delete-organization.tsx +77 -0
- package/src/components/auth/organization/invite-member-dialog.tsx +177 -0
- package/src/components/auth/organization/leave-organization-dialog.tsx +95 -0
- package/src/components/auth/organization/leave-organization.tsx +59 -0
- package/src/components/auth/organization/organization-danger-zone.tsx +68 -0
- package/src/components/auth/organization/organization-invitation-row-skeleton.tsx +29 -0
- package/src/components/auth/organization/organization-invitation-row.tsx +97 -0
- package/src/components/auth/organization/organization-invitations-empty.tsx +39 -0
- package/src/components/auth/organization/organization-invitations.tsx +351 -0
- package/src/components/auth/organization/organization-logo.tsx +64 -0
- package/src/components/auth/organization/organization-member-row-skeleton.tsx +24 -0
- package/src/components/auth/organization/organization-member-row.tsx +157 -0
- package/src/components/auth/organization/organization-members.tsx +284 -0
- package/src/components/auth/organization/organization-people.tsx +26 -0
- package/src/components/auth/organization/organization-profile.tsx +141 -0
- package/src/components/auth/organization/organization-row.tsx +66 -0
- package/src/components/auth/organization/organization-settings.tsx +38 -0
- package/src/components/auth/organization/organization-switcher.tsx +233 -0
- package/src/components/auth/organization/organization-view-skeleton.tsx +36 -0
- package/src/components/auth/organization/organization-view.tsx +109 -0
- package/src/components/auth/organization/organization.tsx +162 -0
- package/src/components/auth/organization/organizations-empty.tsx +35 -0
- package/src/components/auth/organization/organizations-settings.tsx +36 -0
- package/src/components/auth/organization/organizations.tsx +80 -0
- package/src/components/auth/organization/remove-member-dialog.tsx +95 -0
- package/src/components/auth/organization/slug-field.tsx +111 -0
- package/src/components/auth/organization/user-invitation-row-skeleton.tsx +17 -0
- package/src/components/auth/organization/user-invitation-row.tsx +90 -0
- package/src/components/auth/organization/user-invitations-empty.tsx +30 -0
- package/src/components/auth/organization/user-invitations.tsx +59 -0
- package/src/components/auth/settings/account/change-email.tsx +1 -1
- package/src/components/auth/settings/account/user-profile.tsx +5 -5
- package/src/components/auth/settings/security/active-sessions.tsx +6 -4
- package/src/components/auth/settings/security/linked-accounts.tsx +2 -2
- package/src/components/auth/settings/settings.tsx +77 -14
- package/src/components/auth/user/user-avatar.tsx +4 -1
- package/src/components/auth/user/user-button.tsx +14 -11
- package/src/components/auth/user/user-view.tsx +32 -12
- package/src/email.ts +1 -0
- package/src/lib/auth/api-key-plugin.ts +10 -4
- package/src/lib/auth/auth-plugin.ts +6 -1
- package/src/lib/auth/organization-plugin.tsx +32 -0
- package/src/plugins.ts +25 -1
- package/dist/components/auth/delete-user/delete-user.d.ts +0 -9
- package/dist/components/auth/delete-user/delete-user.js +0 -95
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useAuth, useAuthPlugin, useHasPermission, useListOrganizationInvitations } from "@better-auth-ui/react";
|
|
3
|
+
import { ChevronUp, Funnel, Xmark } from "@gravity-ui/icons";
|
|
4
|
+
import { Button, Chip, cn, Dropdown, Label, SearchField, Table } from "@heroui/react";
|
|
5
|
+
import { useMemo, useState } from "react";
|
|
6
|
+
import { organizationPlugin } from "../../../lib/auth/organization-plugin";
|
|
7
|
+
import { InviteMemberDialog } from "./invite-member-dialog";
|
|
8
|
+
import { OrganizationInvitationTableRow } from "./organization-invitation-row";
|
|
9
|
+
import { OrganizationInvitationRowSkeleton } from "./organization-invitation-row-skeleton";
|
|
10
|
+
import { OrganizationInvitationsEmpty } from "./organization-invitations-empty";
|
|
11
|
+
/**
|
|
12
|
+
* Organization invitations table with invite control and per-row actions.
|
|
13
|
+
*/
|
|
14
|
+
export function OrganizationInvitations({ className, ...props }) {
|
|
15
|
+
const { authClient, localization } = useAuth();
|
|
16
|
+
const { localization: organizationLocalization, roles } = useAuthPlugin(organizationPlugin);
|
|
17
|
+
const { data: invitations, isPending: invitationsPending } = useListOrganizationInvitations(authClient);
|
|
18
|
+
const { isPending: invitationPermissionPending } = useHasPermission(authClient, {
|
|
19
|
+
permissions: { invitation: ["cancel"] }
|
|
20
|
+
});
|
|
21
|
+
const isPending = invitationsPending || invitationPermissionPending;
|
|
22
|
+
const [sortDescriptor, setSortDescriptor] = useState();
|
|
23
|
+
const [roleFilter, setRoleFilter] = useState("all");
|
|
24
|
+
const [statusFilter, setStatusFilter] = useState("all");
|
|
25
|
+
const [search, setSearch] = useState("");
|
|
26
|
+
const filteredInvitations = useMemo(() => {
|
|
27
|
+
return invitations?.filter((invitation) => (roleFilter === "all" || invitation.role === roleFilter) &&
|
|
28
|
+
(statusFilter === "all" || invitation.status === statusFilter) &&
|
|
29
|
+
invitation.email.toLowerCase().includes(search.toLowerCase()));
|
|
30
|
+
}, [search, invitations, roleFilter, statusFilter]);
|
|
31
|
+
const sortedInvitations = useMemo(() => {
|
|
32
|
+
if (!sortDescriptor)
|
|
33
|
+
return filteredInvitations;
|
|
34
|
+
if (!filteredInvitations)
|
|
35
|
+
return filteredInvitations;
|
|
36
|
+
return [...filteredInvitations].sort((a, b) => {
|
|
37
|
+
const col = sortDescriptor.column;
|
|
38
|
+
let cmp = 0;
|
|
39
|
+
if (col === "createdAt") {
|
|
40
|
+
cmp = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
cmp = String(a[col]).localeCompare(String(b[col]));
|
|
44
|
+
}
|
|
45
|
+
if (sortDescriptor.direction === "descending") {
|
|
46
|
+
cmp *= -1;
|
|
47
|
+
}
|
|
48
|
+
return cmp;
|
|
49
|
+
});
|
|
50
|
+
}, [sortDescriptor, filteredInvitations]);
|
|
51
|
+
const [inviteOpen, setInviteOpen] = useState(false);
|
|
52
|
+
return (_jsxs("div", { className: cn("flex flex-col gap-3", className), ...props, children: [_jsx("h3", { className: "truncate text-sm font-semibold", children: organizationLocalization.invitations }), _jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(SearchField, { className: "min-w-0", "aria-label": organizationLocalization.search, value: search, onChange: setSearch, isDisabled: isPending, children: _jsxs(SearchField.Group, { children: [_jsx(SearchField.SearchIcon, {}), _jsx(SearchField.Input, { placeholder: organizationLocalization.search, className: "sm:w-[200px]" }), _jsx(SearchField.ClearButton, {})] }) }), _jsxs(Dropdown, { children: [_jsxs(Button, { size: "sm", variant: "secondary", isDisabled: isPending, children: [_jsx(Funnel, {}), organizationLocalization.role] }), _jsx(Dropdown.Popover, { children: _jsxs(Dropdown.Menu, { selectionMode: "single", selectedKeys: new Set([roleFilter]), onSelectionChange: (keys) => {
|
|
53
|
+
const key = [...keys][0];
|
|
54
|
+
setRoleFilter(key ?? "all");
|
|
55
|
+
}, children: [_jsxs(Dropdown.Item, { id: "all", textValue: organizationLocalization.all, children: [_jsx(Label, { children: organizationLocalization.all }), _jsx(Dropdown.ItemIndicator, {})] }), Object.entries(roles).map(([key, label]) => (_jsxs(Dropdown.Item, { id: key, textValue: label, children: [_jsx(Label, { children: label }), _jsx(Dropdown.ItemIndicator, {})] }, key)))] }) })] }), _jsxs(Dropdown, { children: [_jsxs(Button, { size: "sm", variant: "secondary", isDisabled: isPending, children: [_jsx(Funnel, {}), organizationLocalization.status] }), _jsx(Dropdown.Popover, { children: _jsxs(Dropdown.Menu, { selectionMode: "single", selectedKeys: new Set([statusFilter]), onSelectionChange: (keys) => {
|
|
56
|
+
const key = [...keys][0];
|
|
57
|
+
setStatusFilter(key ?? "all");
|
|
58
|
+
}, children: [_jsxs(Dropdown.Item, { id: "all", textValue: organizationLocalization.all, children: [_jsx(Label, { children: organizationLocalization.all }), _jsx(Dropdown.ItemIndicator, {})] }), ["pending", "accepted", "rejected", "canceled"].map((status) => (_jsxs(Dropdown.Item, { id: status, textValue: organizationLocalization[status] ?? status, children: [_jsx(Label, { children: organizationLocalization[status] ?? status }), _jsx(Dropdown.ItemIndicator, {})] }, status)))] }) })] })] }), (roleFilter !== "all" || statusFilter !== "all") && (_jsxs("div", { className: "flex flex-wrap gap-2", children: [roleFilter !== "all" && (_jsxs(Chip, { size: "sm", variant: "secondary", className: "w-fit", children: [_jsxs(Chip.Label, { children: [organizationLocalization.role, ":", " ", _jsx("span", { className: "capitalize", children: roles?.[roleFilter] ?? roleFilter })] }), _jsx("button", { type: "button", "aria-label": organizationLocalization.clear, className: "text-muted hover:text-foreground inline-flex cursor-pointer items-center", onClick: () => setRoleFilter("all"), children: _jsx(Xmark, { className: "size-3" }) })] })), statusFilter !== "all" && (_jsxs(Chip, { size: "sm", variant: "secondary", className: "w-fit", children: [_jsxs(Chip.Label, { children: [organizationLocalization.status, ":", " ", organizationLocalization[statusFilter] ?? statusFilter] }), _jsx("button", { type: "button", "aria-label": organizationLocalization.clear, className: "text-muted hover:text-foreground inline-flex cursor-pointer items-center", onClick: () => setStatusFilter("all"), children: _jsx(Xmark, { className: "size-3" }) })] }))] })), _jsx(Table, { children: _jsx(Table.ScrollContainer, { children: _jsxs(Table.Content, { "aria-label": organizationLocalization.invitations, sortDescriptor: sortDescriptor, onSortChange: (descriptor) => {
|
|
59
|
+
const shouldReset = sortDescriptor?.column === descriptor.column &&
|
|
60
|
+
descriptor.direction === "ascending";
|
|
61
|
+
setSortDescriptor(shouldReset ? undefined : descriptor);
|
|
62
|
+
}, children: [_jsxs(Table.Header, { children: [_jsx(Table.Column, { allowsSorting: true, isRowHeader: true, id: "email", children: ({ sortDirection }) => (_jsx(SortableColumnHeader, { sortDirection: sortDirection, children: localization.auth.email })) }), _jsx(Table.Column, { allowsSorting: true, id: "createdAt", children: ({ sortDirection }) => (_jsx(SortableColumnHeader, { sortDirection: sortDirection, children: organizationLocalization.invitedAt })) }), _jsx(Table.Column, { allowsSorting: true, id: "role", children: ({ sortDirection }) => (_jsx(SortableColumnHeader, { sortDirection: sortDirection, children: organizationLocalization.role })) }), _jsx(Table.Column, { allowsSorting: true, id: "status", children: ({ sortDirection }) => (_jsx(SortableColumnHeader, { sortDirection: sortDirection, children: organizationLocalization.status })) }), _jsx(Table.Column, { className: "text-end", children: organizationLocalization.actions })] }), _jsx(Table.Body, { renderEmptyState: () => (_jsx(OrganizationInvitationsEmpty, { onInvitePress: () => setInviteOpen(true) })), children: isPending ? (_jsx(OrganizationInvitationRowSkeleton, {})) : (sortedInvitations?.map((invitation) => (_jsx(OrganizationInvitationTableRow, { invitation: invitation }, invitation.id)))) })] }) }) })] }), _jsx(InviteMemberDialog, { isOpen: inviteOpen, onOpenChange: setInviteOpen })] }));
|
|
63
|
+
}
|
|
64
|
+
function SortableColumnHeader({ children, sortDirection }) {
|
|
65
|
+
return (_jsxs("span", { className: "flex items-center justify-between", children: [children, !!sortDirection && (_jsx(ChevronUp, { className: cn("size-3 transform transition-transform duration-100 ease-out", sortDirection === "descending" ? "rotate-180" : "") }))] }));
|
|
66
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type AvatarProps } from "@heroui/react";
|
|
2
|
+
import type { Organization } from "better-auth/client";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
export type OrganizationLogoProps = {
|
|
5
|
+
className?: string;
|
|
6
|
+
fallback?: ReactNode;
|
|
7
|
+
isPending?: boolean;
|
|
8
|
+
organization?: Partial<Organization>;
|
|
9
|
+
size?: AvatarProps["size"];
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Renders an organization logo image, initials fallback, or a briefcase icon;
|
|
13
|
+
* shows a circular skeleton while loading when `isPending` is set and `organization` is undefined.
|
|
14
|
+
*/
|
|
15
|
+
export declare function OrganizationLogo({ className, fallback, isPending, organization, size, style, ...props }: OrganizationLogoProps & AvatarProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Briefcase } from "@gravity-ui/icons";
|
|
3
|
+
import { Avatar, cn, Skeleton } from "@heroui/react";
|
|
4
|
+
/**
|
|
5
|
+
* Renders an organization logo image, initials fallback, or a briefcase icon;
|
|
6
|
+
* shows a circular skeleton while loading when `isPending` is set and `organization` is undefined.
|
|
7
|
+
*/
|
|
8
|
+
export function OrganizationLogo({ className, fallback, isPending, organization, size = "sm", style, ...props }) {
|
|
9
|
+
if (isPending && !organization) {
|
|
10
|
+
return (_jsx(Skeleton, { className: cn("rounded-full", size === "sm" ? "size-8" : size === "md" ? "size-10" : "size-12", className), style: style }));
|
|
11
|
+
}
|
|
12
|
+
const initials = organization?.name?.slice(0, 2).toUpperCase();
|
|
13
|
+
return (_jsxs(Avatar, { size: size, className: cn("rounded-full", className), style: style, ...props, children: [_jsx(Avatar.Image, { alt: organization?.name ?? "Organization", src: organization?.logo ?? undefined }), _jsx(Avatar.Fallback, { className: cn(size === "lg" ? "text-xl" : size === "md" ? "text-base" : "text-sm"), delayMs: organization?.logo ? 600 : undefined, children: fallback || initials || _jsx(Briefcase, { className: "size-4" }) })] }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Skeleton, Table } from "@heroui/react";
|
|
3
|
+
import { UserView } from "../user/user-view";
|
|
4
|
+
/**
|
|
5
|
+
* Placeholder row matching {@link OrganizationMemberRow} while members load.
|
|
6
|
+
*/
|
|
7
|
+
export function OrganizationMemberRowSkeleton() {
|
|
8
|
+
return (_jsxs(Table.Row, { children: [_jsx(Table.Cell, { children: _jsx(UserView, { isPending: true }) }), _jsx(Table.Cell, { children: _jsx(Skeleton, { className: "h-4 w-18 rounded-lg" }) }), _jsx(Table.Cell, { className: "flex justify-end", children: _jsx(Skeleton, { className: "size-8 rounded-full" }) })] }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Member, Organization, User } from "better-auth/client";
|
|
2
|
+
export type OrganizationMemberRowProps = {
|
|
3
|
+
member: Member & {
|
|
4
|
+
user: Partial<User>;
|
|
5
|
+
};
|
|
6
|
+
isOwner?: boolean;
|
|
7
|
+
organization: Organization;
|
|
8
|
+
};
|
|
9
|
+
export declare function OrganizationMemberRow({ member, isOwner, organization }: OrganizationMemberRowProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useAuth, useAuthPlugin, useHasPermission, useSession, useUpdateMemberRole } from "@better-auth-ui/react";
|
|
3
|
+
import { ArrowRightFromSquare, Pencil, TrashBin } from "@gravity-ui/icons";
|
|
4
|
+
import { Button, Dropdown, Label, Spinner, Table, toast } from "@heroui/react";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { organizationPlugin } from "../../../lib/auth/organization-plugin";
|
|
7
|
+
import { UserView } from "../user/user-view";
|
|
8
|
+
import { LeaveOrganizationDialog } from "./leave-organization-dialog";
|
|
9
|
+
import { OrganizationMemberRowSkeleton } from "./organization-member-row-skeleton";
|
|
10
|
+
import { RemoveMemberDialog } from "./remove-member-dialog";
|
|
11
|
+
export function OrganizationMemberRow({ member, isOwner, organization }) {
|
|
12
|
+
const { authClient } = useAuth();
|
|
13
|
+
const { localization: organizationLocalization, roles } = useAuthPlugin(organizationPlugin);
|
|
14
|
+
const { data: session } = useSession(authClient);
|
|
15
|
+
const { data: hasUpdatePermission, isPending: updatePermissionPending } = useHasPermission(authClient, {
|
|
16
|
+
permissions: { member: ["update"] }
|
|
17
|
+
});
|
|
18
|
+
const { data: hasDeletePermission, isPending: deletePermissionPending } = useHasPermission(authClient, {
|
|
19
|
+
permissions: { member: ["delete"] }
|
|
20
|
+
});
|
|
21
|
+
const isPending = updatePermissionPending || deletePermissionPending;
|
|
22
|
+
const { mutate: updateMemberRole, isPending: isUpdatingRole } = useUpdateMemberRole(authClient, {
|
|
23
|
+
onSuccess: () => toast.success(organizationLocalization.memberRoleUpdated)
|
|
24
|
+
});
|
|
25
|
+
const roleLabel = roles?.[member.role] ?? member.role;
|
|
26
|
+
const assignableRoles = Object.entries(roles).filter(([key]) => isOwner || key !== "owner");
|
|
27
|
+
const isCurrentUser = session?.user.id === member.userId;
|
|
28
|
+
const [removeOpen, setRemoveOpen] = useState(false);
|
|
29
|
+
const [leaveOpen, setLeaveOpen] = useState(false);
|
|
30
|
+
if (isPending) {
|
|
31
|
+
return _jsx(OrganizationMemberRowSkeleton, {});
|
|
32
|
+
}
|
|
33
|
+
return (_jsxs(Table.Row, { children: [_jsx(Table.Cell, { children: _jsx(UserView, { user: member.user }) }), _jsx(Table.Cell, { children: roleLabel }), _jsxs(Table.Cell, { children: [_jsxs("div", { className: "flex items-center justify-end gap-1", children: [hasUpdatePermission?.success && (_jsxs(Dropdown, { children: [_jsx(Button, { isIconOnly: true, size: "sm", variant: "tertiary", isDisabled: isUpdatingRole, "aria-label": organizationLocalization.changeMemberRole, children: isUpdatingRole ? (_jsx(Spinner, { color: "current", size: "sm" })) : (_jsx(Pencil, {})) }), _jsx(Dropdown.Popover, { className: "min-w-fit", children: _jsx(Dropdown.Menu, { children: assignableRoles.map(([role, label]) => (_jsx(Dropdown.Item, { textValue: label, isDisabled: member.role === role, onAction: () => updateMemberRole({ memberId: member.id, role }), children: _jsx(Label, { children: label }) }, role))) }) })] })), isCurrentUser ? (_jsx(Button, { isIconOnly: true, size: "sm", variant: "danger-soft", "aria-label": organizationLocalization.leaveOrganization, onPress: () => setLeaveOpen(true), children: _jsx(ArrowRightFromSquare, {}) })) : (hasDeletePermission?.success && (_jsx(Button, { isIconOnly: true, size: "sm", variant: "danger-soft", "aria-label": organizationLocalization.removeMember, onPress: () => setRemoveOpen(true), children: _jsx(TrashBin, {}) })))] }), isCurrentUser && organization ? (_jsx(LeaveOrganizationDialog, { isOpen: leaveOpen, onOpenChange: setLeaveOpen, organization: organization })) : (hasDeletePermission?.success && (_jsx(RemoveMemberDialog, { isOpen: removeOpen, onOpenChange: setRemoveOpen, member: member })))] })] }));
|
|
34
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ComponentProps } from "react";
|
|
2
|
+
/** Props for the {@link OrganizationMembers} component. */
|
|
3
|
+
export type OrganizationMembersProps = {
|
|
4
|
+
className?: string;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Organization members table with title, invite control, and per-row actions.
|
|
8
|
+
*/
|
|
9
|
+
export declare function OrganizationMembers({ className, ...props }: OrganizationMembersProps & ComponentProps<"div">): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useActiveOrganization, useAuth, useAuthPlugin, useHasPermission, useListOrganizationMembers, useSession } from "@better-auth-ui/react";
|
|
3
|
+
import { ChevronUp, Funnel, Xmark } from "@gravity-ui/icons";
|
|
4
|
+
import { Button, Chip, cn, Dropdown, Label, SearchField, Table } from "@heroui/react";
|
|
5
|
+
import { useMemo, useState } from "react";
|
|
6
|
+
import { organizationPlugin } from "../../../lib/auth/organization-plugin";
|
|
7
|
+
import { InviteMemberDialog } from "./invite-member-dialog";
|
|
8
|
+
import { OrganizationMemberRow } from "./organization-member-row";
|
|
9
|
+
import { OrganizationMemberRowSkeleton } from "./organization-member-row-skeleton";
|
|
10
|
+
/**
|
|
11
|
+
* Organization members table with title, invite control, and per-row actions.
|
|
12
|
+
*/
|
|
13
|
+
export function OrganizationMembers({ className, ...props }) {
|
|
14
|
+
const { authClient } = useAuth();
|
|
15
|
+
const { localization: organizationLocalization, roles } = useAuthPlugin(organizationPlugin);
|
|
16
|
+
const { data: session } = useSession(authClient);
|
|
17
|
+
const { data: activeOrganization, isPending: activeOrganizationPending } = useActiveOrganization(authClient);
|
|
18
|
+
const { data: membersData, isPending: membersPending } = useListOrganizationMembers(authClient);
|
|
19
|
+
const { isPending: updatePermissionPending } = useHasPermission(authClient, {
|
|
20
|
+
permissions: { member: ["update"] }
|
|
21
|
+
});
|
|
22
|
+
const { isPending: deletePermissionPending } = useHasPermission(authClient, {
|
|
23
|
+
permissions: { member: ["delete"] }
|
|
24
|
+
});
|
|
25
|
+
const isPending = activeOrganizationPending ||
|
|
26
|
+
membersPending ||
|
|
27
|
+
updatePermissionPending ||
|
|
28
|
+
deletePermissionPending;
|
|
29
|
+
const [sortDescriptor, setSortDescriptor] = useState();
|
|
30
|
+
const [roleFilter, setRoleFilter] = useState("all");
|
|
31
|
+
const [search, setSearch] = useState("");
|
|
32
|
+
const filteredMembers = useMemo(() => {
|
|
33
|
+
return membersData?.members.filter((member) => (roleFilter === "all" || member.role === roleFilter) &&
|
|
34
|
+
(member.user.name.toLowerCase().includes(search.toLowerCase()) ||
|
|
35
|
+
member.user.email.toLowerCase().includes(search.toLowerCase())));
|
|
36
|
+
}, [search, membersData?.members, roleFilter]);
|
|
37
|
+
const sortedMembers = useMemo(() => {
|
|
38
|
+
if (!sortDescriptor)
|
|
39
|
+
return filteredMembers;
|
|
40
|
+
if (!filteredMembers)
|
|
41
|
+
return filteredMembers;
|
|
42
|
+
return [...filteredMembers].sort((a, b) => {
|
|
43
|
+
const col = sortDescriptor.column;
|
|
44
|
+
const first = col === "user" ? a.user.name || a.user.email : String(a[col]);
|
|
45
|
+
const second = col === "user" ? b.user.name || b.user.email : String(b[col]);
|
|
46
|
+
let cmp = first.localeCompare(second);
|
|
47
|
+
if (sortDescriptor.direction === "descending") {
|
|
48
|
+
cmp *= -1;
|
|
49
|
+
}
|
|
50
|
+
return cmp;
|
|
51
|
+
});
|
|
52
|
+
}, [sortDescriptor, filteredMembers]);
|
|
53
|
+
const [inviteOpen, setInviteOpen] = useState(false);
|
|
54
|
+
const isOwner = membersData?.members.some((member) => member.role === "owner" && member.userId === session?.user.id);
|
|
55
|
+
return (_jsxs("div", { className: cn("flex flex-col gap-3", className), ...props, children: [_jsxs("div", { className: "flex items-end justify-between gap-3", children: [_jsx("h3", { className: "truncate text-sm font-semibold", children: organizationLocalization.members }), _jsx(Button, { className: "shrink-0", size: "sm", isDisabled: isPending, onPress: () => setInviteOpen(true), children: organizationLocalization.inviteMember })] }), _jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(SearchField, { className: "min-w-0", "aria-label": organizationLocalization.search, value: search, onChange: setSearch, isDisabled: isPending, children: _jsxs(SearchField.Group, { children: [_jsx(SearchField.SearchIcon, {}), _jsx(SearchField.Input, { placeholder: organizationLocalization.search, className: "sm:w-[200px]" }), _jsx(SearchField.ClearButton, {})] }) }), _jsxs(Dropdown, { children: [_jsxs(Button, { size: "sm", variant: "secondary", isDisabled: isPending, children: [_jsx(Funnel, {}), organizationLocalization.role] }), _jsx(Dropdown.Popover, { children: _jsxs(Dropdown.Menu, { selectionMode: "single", selectedKeys: new Set([roleFilter]), onSelectionChange: (keys) => {
|
|
56
|
+
const key = [...keys][0];
|
|
57
|
+
setRoleFilter(key ?? "all");
|
|
58
|
+
}, children: [_jsxs(Dropdown.Item, { id: "all", textValue: organizationLocalization.all, children: [_jsx(Label, { children: organizationLocalization.all }), _jsx(Dropdown.ItemIndicator, {})] }), Object.entries(roles).map(([role, label]) => (_jsxs(Dropdown.Item, { id: role, textValue: label, children: [_jsx(Label, { children: label }), _jsx(Dropdown.ItemIndicator, {})] }, role)))] }) })] })] }), roleFilter !== "all" && (_jsxs(Chip, { size: "sm", variant: "secondary", className: "w-fit", children: [_jsxs(Chip.Label, { children: [organizationLocalization.role, ":", " ", _jsx("span", { className: "capitalize", children: roles?.[roleFilter] ?? roleFilter })] }), _jsx("button", { type: "button", "aria-label": organizationLocalization.clear, className: "text-muted hover:text-foreground inline-flex cursor-pointer items-center", onClick: () => setRoleFilter("all"), children: _jsx(Xmark, { className: "size-3" }) })] })), _jsx(Table, { children: _jsx(Table.ScrollContainer, { children: _jsxs(Table.Content, { "aria-label": organizationLocalization.members, sortDescriptor: sortDescriptor, onSortChange: (descriptor) => {
|
|
59
|
+
const shouldReset = sortDescriptor?.column === descriptor.column &&
|
|
60
|
+
descriptor.direction === "ascending";
|
|
61
|
+
setSortDescriptor(shouldReset ? undefined : descriptor);
|
|
62
|
+
}, children: [_jsxs(Table.Header, { children: [_jsx(Table.Column, { allowsSorting: true, isRowHeader: true, id: "user", children: ({ sortDirection }) => (_jsx(SortableColumnHeader, { sortDirection: sortDirection, children: organizationLocalization.member })) }), _jsx(Table.Column, { allowsSorting: true, id: "role", children: ({ sortDirection }) => (_jsx(SortableColumnHeader, { sortDirection: sortDirection, children: organizationLocalization.role })) }), _jsx(Table.Column, { className: "text-end", children: organizationLocalization.actions })] }), _jsx(Table.Body, { children: isPending ? (_jsx(OrganizationMemberRowSkeleton, {})) : (!!activeOrganization &&
|
|
63
|
+
sortedMembers?.map((member) => (_jsx(OrganizationMemberRow, { member: member, isOwner: isOwner, organization: activeOrganization }, member.id)))) })] }) }) })] }), _jsx(InviteMemberDialog, { isOpen: inviteOpen, onOpenChange: setInviteOpen })] }));
|
|
64
|
+
}
|
|
65
|
+
function SortableColumnHeader({ children, sortDirection }) {
|
|
66
|
+
return (_jsxs("span", { className: "flex items-center justify-between", children: [children, !!sortDirection && (_jsx(ChevronUp, { className: cn("size-3 transform transition-transform duration-100 ease-out", sortDirection === "descending" ? "rotate-180" : "") }))] }));
|
|
67
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
/** Props for the {@link OrganizationPeople} component. */
|
|
3
|
+
export type OrganizationPeopleProps = {
|
|
4
|
+
className?: string;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Organization people UI: members table (see {@link OrganizationMembers}), then org
|
|
8
|
+
* invitations.
|
|
9
|
+
*/
|
|
10
|
+
export declare function OrganizationPeople({ className, ...props }: OrganizationPeopleProps & ComponentProps<"div">): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "@heroui/react";
|
|
3
|
+
import { OrganizationInvitations } from "./organization-invitations";
|
|
4
|
+
import { OrganizationMembers } from "./organization-members";
|
|
5
|
+
/**
|
|
6
|
+
* Organization people UI: members table (see {@link OrganizationMembers}), then org
|
|
7
|
+
* invitations.
|
|
8
|
+
*/
|
|
9
|
+
export function OrganizationPeople({ className, ...props }) {
|
|
10
|
+
return (_jsxs("div", { className: cn("flex flex-col gap-4 md:gap-6", className), ...props, children: [_jsx(OrganizationMembers, {}), _jsx(OrganizationInvitations, {})] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type CardProps } from "@heroui/react";
|
|
2
|
+
export type OrganizationProfileProps = {
|
|
3
|
+
className?: string;
|
|
4
|
+
variant?: CardProps["variant"];
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Profile card for the active organization: logo (when enabled), display name, and slug.
|
|
8
|
+
*/
|
|
9
|
+
export declare function OrganizationProfile({ className, variant, ...props }: OrganizationProfileProps & Omit<CardProps, "children">): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useActiveOrganization, useAuth, useAuthPlugin, useUpdateOrganization } from "@better-auth-ui/react";
|
|
3
|
+
import { Button, Card, cn, FieldError, Form, Input, Label, Skeleton, Spinner, TextField, toast } from "@heroui/react";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
import { organizationPlugin } from "../../../lib/auth/organization-plugin";
|
|
6
|
+
import { ChangeOrganizationLogo } from "./change-organization-logo";
|
|
7
|
+
import { SlugField } from "./slug-field";
|
|
8
|
+
/**
|
|
9
|
+
* Profile card for the active organization: logo (when enabled), display name, and slug.
|
|
10
|
+
*/
|
|
11
|
+
export function OrganizationProfile({ className, variant, ...props }) {
|
|
12
|
+
const { authClient, localization } = useAuth();
|
|
13
|
+
const { localization: organizationLocalization } = useAuthPlugin(organizationPlugin);
|
|
14
|
+
const { data: activeOrganization } = useActiveOrganization(authClient);
|
|
15
|
+
const [slug, setSlug] = useState(activeOrganization?.slug ?? "");
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
setSlug(activeOrganization?.slug ?? "");
|
|
18
|
+
}, [activeOrganization?.slug]);
|
|
19
|
+
const { mutate: commitOrganizationUpdate, isPending } = useUpdateOrganization(authClient, {
|
|
20
|
+
onSuccess: () => toast.success(organizationLocalization.organizationUpdatedSuccess)
|
|
21
|
+
});
|
|
22
|
+
function handleSubmit(e) {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
if (!activeOrganization)
|
|
25
|
+
return;
|
|
26
|
+
const formData = new FormData(e.currentTarget);
|
|
27
|
+
const name = formData.get("name");
|
|
28
|
+
commitOrganizationUpdate({
|
|
29
|
+
data: { name, slug }
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const inputVariant = variant === "transparent" ? "primary" : "secondary";
|
|
33
|
+
return (_jsxs("div", { children: [_jsx("h2", { className: cn("mb-3 text-sm font-semibold"), children: organizationLocalization.organizationProfile }), _jsx(Card, { className: cn(className), variant: variant, ...props, children: _jsx(Card.Content, { children: _jsxs(Form, { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [_jsx(ChangeOrganizationLogo, {}), _jsxs(TextField, { name: "name", defaultValue: activeOrganization?.name, isDisabled: isPending || !activeOrganization, children: [_jsx(Label, { children: organizationLocalization.name }), _jsx(Input, { className: cn(!activeOrganization && "hidden"), autoComplete: "organization", placeholder: organizationLocalization.namePlaceholder, variant: inputVariant }), !activeOrganization && (_jsx(Skeleton, { className: "h-10 w-full rounded-xl md:h-9" })), _jsx(FieldError, {})] }, `${activeOrganization?.id}-${activeOrganization?.name}-name`), activeOrganization ? (_jsx(SlugField, { value: slug, onChange: setSlug, currentSlug: activeOrganization.slug, isDisabled: isPending, variant: inputVariant })) : (_jsxs(TextField, { isDisabled: true, children: [_jsx(Label, { children: organizationLocalization.slug }), _jsx(Skeleton, { className: "h-10 w-full rounded-xl md:h-9" })] })), _jsxs(Button, { type: "submit", isPending: isPending, isDisabled: !activeOrganization, size: "sm", className: "mt-1", children: [isPending && _jsx(Spinner, { color: "current", size: "sm" }), localization.settings.saveChanges] })] }) }) })] }));
|
|
34
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Organization } from "better-auth/client";
|
|
2
|
+
export type OrganizationRowProps = {
|
|
3
|
+
organization: Organization;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Single organization row: logo and labels via {@link OrganizationView}, plus Manage.
|
|
7
|
+
*/
|
|
8
|
+
export declare function OrganizationRow({ organization }: OrganizationRowProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useAuth, useAuthPlugin, useSetActiveOrganization } from "@better-auth-ui/react";
|
|
3
|
+
import { Gear } from "@gravity-ui/icons";
|
|
4
|
+
import { Button, Spinner } from "@heroui/react";
|
|
5
|
+
import { organizationPlugin } from "../../../lib/auth/organization-plugin";
|
|
6
|
+
import { OrganizationView } from "./organization-view";
|
|
7
|
+
/**
|
|
8
|
+
* Single organization row: logo and labels via {@link OrganizationView}, plus Manage.
|
|
9
|
+
*/
|
|
10
|
+
export function OrganizationRow({ organization }) {
|
|
11
|
+
const { authClient, basePaths, navigate } = useAuth();
|
|
12
|
+
const { localization: organizationLocalization, viewPaths: organizationViewPaths, slug } = useAuthPlugin(organizationPlugin);
|
|
13
|
+
const { mutate: setActiveOrganization, isPending: setActivePending } = useSetActiveOrganization(authClient, {
|
|
14
|
+
onSuccess: () => {
|
|
15
|
+
navigate({
|
|
16
|
+
to: `${basePaths.organization}/${organizationViewPaths.organization.settings}`
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
function manageOrganization() {
|
|
21
|
+
if (slug !== undefined) {
|
|
22
|
+
navigate({
|
|
23
|
+
to: `${basePaths.organization}/${organization.slug}/${organizationViewPaths.organization.settings}`
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
setActiveOrganization({ organizationId: organization.id });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(OrganizationView, { organization: organization }), _jsxs(Button, { className: "ml-auto shrink-0", variant: "outline", size: "sm", isPending: setActivePending, onPress: manageOrganization, "aria-label": organizationLocalization.manage, children: [setActivePending ? _jsx(Spinner, { color: "current", size: "sm" }) : _jsx(Gear, {}), organizationLocalization.manage] })] }));
|
|
31
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type CardProps } from "@heroui/react";
|
|
2
|
+
import type { ComponentProps } from "react";
|
|
3
|
+
/** Props for the {@link OrganizationSettings} component. */
|
|
4
|
+
export type OrganizationSettingsProps = {
|
|
5
|
+
className?: string;
|
|
6
|
+
variant?: CardProps["variant"];
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Organization settings UI: profile card, plugin-contributed cards
|
|
10
|
+
* (`organizationCards`), then danger zone.
|
|
11
|
+
*/
|
|
12
|
+
export declare function OrganizationSettings({ className, variant, ...props }: OrganizationSettingsProps & ComponentProps<"div">): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useAuth } from "@better-auth-ui/react";
|
|
3
|
+
import { cn } from "@heroui/react";
|
|
4
|
+
import { OrganizationDangerZone } from "./organization-danger-zone";
|
|
5
|
+
import { OrganizationProfile } from "./organization-profile";
|
|
6
|
+
/**
|
|
7
|
+
* Organization settings UI: profile card, plugin-contributed cards
|
|
8
|
+
* (`organizationCards`), then danger zone.
|
|
9
|
+
*/
|
|
10
|
+
export function OrganizationSettings({ className, variant, ...props }) {
|
|
11
|
+
const { plugins } = useAuth();
|
|
12
|
+
return (_jsxs("div", { className: cn("flex flex-col gap-4 md:gap-6", className), ...props, children: [_jsx(OrganizationProfile, { variant: variant }), plugins.flatMap((plugin) => plugin.organizationCards?.map((Card, index) => (_jsx(Card, { variant: variant }, `${plugin.id}-${index.toString()}`)))), _jsx(OrganizationDangerZone, { variant: variant })] }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ButtonProps, type DropdownPopoverProps } from "@heroui/react";
|
|
2
|
+
import type { Organization } from "better-auth/client";
|
|
3
|
+
import { type ReactNode } from "react";
|
|
4
|
+
/** Props for the {@link OrganizationSwitcher} component. */
|
|
5
|
+
export type OrganizationSwitcherProps = {
|
|
6
|
+
className?: string;
|
|
7
|
+
placement?: DropdownPopoverProps["placement"];
|
|
8
|
+
variant?: ButtonProps["variant"];
|
|
9
|
+
trigger?: ReactNode;
|
|
10
|
+
hideCreate?: boolean;
|
|
11
|
+
hidePersonal?: boolean;
|
|
12
|
+
hideSettings?: boolean;
|
|
13
|
+
hideSlug?: boolean;
|
|
14
|
+
setActive?: (organization: Organization | null) => void;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Renders an organizations dropdown with a trigger button,
|
|
18
|
+
* header summary, and a menu of organizations to switch to.
|
|
19
|
+
*/
|
|
20
|
+
export declare function OrganizationSwitcher({ className, hideCreate, hidePersonal, hideSettings, hideSlug, setActive, placement, variant, size, trigger, ...props }: OrganizationSwitcherProps & ButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useActiveOrganization, useAuth, useAuthPlugin, useListOrganizations, useSession, useSetActiveOrganization } from "@better-auth-ui/react";
|
|
3
|
+
import { ChevronsExpandVertical, CirclePlus, Gear } from "@gravity-ui/icons";
|
|
4
|
+
import { Button, cn, Dropdown, Label, Link } from "@heroui/react";
|
|
5
|
+
import { buttonVariants } from "@heroui/styles";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { organizationPlugin } from "../../../lib/auth/organization-plugin";
|
|
8
|
+
import { UserView } from "../user/user-view";
|
|
9
|
+
import { CreateOrganizationDialog } from "./create-organization-dialog";
|
|
10
|
+
import { OrganizationView } from "./organization-view";
|
|
11
|
+
/**
|
|
12
|
+
* Renders an organizations dropdown with a trigger button,
|
|
13
|
+
* header summary, and a menu of organizations to switch to.
|
|
14
|
+
*/
|
|
15
|
+
export function OrganizationSwitcher({ className, hideCreate, hidePersonal, hideSettings, hideSlug = true, setActive, placement, variant = "ghost", size = "md", trigger, ...props }) {
|
|
16
|
+
const { authClient, navigate, basePaths, localization, viewPaths } = useAuth();
|
|
17
|
+
const { data: session, isPending: sessionPending } = useSession(authClient);
|
|
18
|
+
const { localization: organizationLocalization, viewPaths: organizationViewPaths, slug } = useAuthPlugin(organizationPlugin);
|
|
19
|
+
const { data: activeOrganization, isPending: activeOrganizationPending } = useActiveOrganization(authClient);
|
|
20
|
+
const { data: organizations, isPending: organizationsPending } = useListOrganizations(authClient);
|
|
21
|
+
const { mutate: setActiveOrganization } = useSetActiveOrganization(authClient);
|
|
22
|
+
const isPending = sessionPending ||
|
|
23
|
+
(!!session && (organizationsPending || activeOrganizationPending));
|
|
24
|
+
const [createOpen, setCreateOpen] = useState(false);
|
|
25
|
+
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
26
|
+
function handleSetActive(organization) {
|
|
27
|
+
if (setActive) {
|
|
28
|
+
setActive(organization);
|
|
29
|
+
}
|
|
30
|
+
else if (slug !== undefined) {
|
|
31
|
+
navigate({
|
|
32
|
+
to: organization
|
|
33
|
+
? `${basePaths.organization}/${organization.slug}/${organizationViewPaths.organization.settings}`
|
|
34
|
+
: `${basePaths.settings}/${viewPaths.settings.account}`
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
setActiveOrganization({ organizationId: organization?.id ?? null });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Dropdown, { isOpen: dropdownOpen, onOpenChange: setDropdownOpen, children: [trigger ? (_jsx(Dropdown.Trigger, { children: trigger })) : (_jsxs(Button, { variant: variant, className: cn("h-auto px-2 py-2 text-left", className), isDisabled: !session || isPending, "aria-label": organizationLocalization.organization, ...props, children: [isPending ? (_jsx(OrganizationView, { size: size, isPending: true, hideRole: true, hideSlug: hideSlug })) : activeOrganization ? (_jsx(OrganizationView, { size: size, hideRole: true, hideSlug: hideSlug })) : session && !hidePersonal ? (_jsx(UserView, { size: size, hideSubtitle: hideSlug })) : (_jsx(OrganizationView, { size: size, hideRole: true, hideSlug: hideSlug, organization: {
|
|
42
|
+
name: organizationLocalization.organization
|
|
43
|
+
} })), _jsx(ChevronsExpandVertical, { className: "size-3 shrink-0 text-muted" })] })), _jsxs(Dropdown.Popover, { placement: placement, className: "max-w-svw", children: [activeOrganization ? (_jsxs("div", { className: "flex items-center justify-between gap-4 px-4 pt-3", children: [_jsx(OrganizationView, { hideRole: true, hideSlug: hideSlug, organization: activeOrganization }), !hideSettings && (_jsxs(Link, { href: slug
|
|
44
|
+
? `${basePaths.organization}/${slug}/${organizationViewPaths.organization.settings}`
|
|
45
|
+
: `${basePaths.organization}/${organizationViewPaths.organization.settings}`, className: cn(buttonVariants({ variant: "outline", size: "sm" }), "shrink-0 gap-2"), onPress: () => setDropdownOpen(false), children: [_jsx(Gear, { className: "text-muted" }), organizationLocalization.manage] }))] })) : !isPending && session?.user && !hidePersonal ? (_jsxs("div", { className: "flex items-center justify-between gap-4 px-4 pt-3", children: [_jsx(UserView, { hideSubtitle: hideSlug }), !hideSettings && (_jsxs(Link, { href: `${basePaths.settings}/${viewPaths.settings.account}`, className: cn(buttonVariants({ variant: "outline", size: "sm" }), "shrink-0 gap-2"), onPress: () => setDropdownOpen(false), children: [_jsx(Gear, { className: "text-muted" }), localization.settings.settings] }))] })) : null, _jsxs(Dropdown.Menu, { children: [!!activeOrganization && !hidePersonal && (_jsx(Dropdown.Item, { textValue: organizationLocalization.personalAccount, onPress: () => handleSetActive(null), children: _jsx(UserView, { hideSubtitle: hideSlug }) })), organizations
|
|
46
|
+
?.filter((organization) => organization.id !== activeOrganization?.id)
|
|
47
|
+
?.map((organization) => (_jsx(Dropdown.Item, { textValue: organization.name, onPress: () => handleSetActive(organization), children: _jsx(OrganizationView, { hideRole: true, hideSlug: hideSlug, organization: organization }) }, organization.id))), !hideCreate && (_jsxs(Dropdown.Item, { textValue: organizationLocalization.createOrganization, onPress: () => setCreateOpen(true), children: [_jsx(CirclePlus, { className: "text-muted" }), _jsx(Label, { children: organizationLocalization.createOrganization })] }))] })] })] }), _jsx(CreateOrganizationDialog, { isOpen: createOpen, onOpenChange: setCreateOpen })] }));
|
|
48
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
import type { OrganizationViewProps } from "./organization-view";
|
|
3
|
+
/**
|
|
4
|
+
* Placeholder matching {@link OrganizationView} while organization data loads.
|
|
5
|
+
*/
|
|
6
|
+
export declare function OrganizationViewSkeleton({ className, hideSlug, size, ...props }: OrganizationViewProps & ComponentProps<"div">): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn, Skeleton } from "@heroui/react";
|
|
3
|
+
import { OrganizationLogo } from "./organization-logo";
|
|
4
|
+
/**
|
|
5
|
+
* Placeholder matching {@link OrganizationView} while organization data loads.
|
|
6
|
+
*/
|
|
7
|
+
export function OrganizationViewSkeleton({ className, hideSlug, size = "md", ...props }) {
|
|
8
|
+
return (_jsxs("div", { className: cn("flex min-w-0 items-center gap-2", className), ...props, children: [_jsx(OrganizationLogo, { isPending: true, className: size === "sm" ? "size-5" : undefined, size: size === "lg" ? "md" : "sm" }), _jsxs("div", { className: "flex flex-col min-w-0 gap-1", children: [_jsx(Skeleton, { className: "h-3.5 w-20 rounded-lg" }), !hideSlug && (_jsx(Skeleton, { className: "h-3 w-28 rounded-lg mt-[0.5px] mb-0.5" }))] })] }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type AvatarProps } from "@heroui/react";
|
|
2
|
+
import type { Organization } from "better-auth/client";
|
|
3
|
+
import type { ComponentProps } from "react";
|
|
4
|
+
export type OrganizationViewProps = {
|
|
5
|
+
className?: string;
|
|
6
|
+
isPending?: boolean;
|
|
7
|
+
size?: AvatarProps["size"];
|
|
8
|
+
hideRole?: boolean;
|
|
9
|
+
hideSlug?: boolean;
|
|
10
|
+
organization?: Partial<Organization>;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Compact organization row: logo, primary name, secondary slug — analogous to {@link UserView}.
|
|
14
|
+
*/
|
|
15
|
+
export declare function OrganizationView({ className, isPending, size, hideSlug, hideRole, organization, ...props }: OrganizationViewProps & ComponentProps<"div">): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useActiveOrganization, useAuth, useAuthPlugin, useListOrganizationMembers, useSession } from "@better-auth-ui/react";
|
|
3
|
+
import { Chip, cn } from "@heroui/react";
|
|
4
|
+
import { organizationPlugin } from "../../../lib/auth/organization-plugin";
|
|
5
|
+
import { OrganizationLogo } from "./organization-logo";
|
|
6
|
+
import { OrganizationViewSkeleton } from "./organization-view-skeleton";
|
|
7
|
+
/**
|
|
8
|
+
* Compact organization row: logo, primary name, secondary slug — analogous to {@link UserView}.
|
|
9
|
+
*/
|
|
10
|
+
export function OrganizationView({ className, isPending, size = "md", hideSlug, hideRole, organization, ...props }) {
|
|
11
|
+
const { authClient } = useAuth();
|
|
12
|
+
const { roles } = useAuthPlugin(organizationPlugin);
|
|
13
|
+
const { data: session } = useSession(authClient);
|
|
14
|
+
const { data: activeOrganization, isPending: activeOrganizationPending } = useActiveOrganization(authClient, {
|
|
15
|
+
enabled: !organization && !isPending
|
|
16
|
+
});
|
|
17
|
+
const resolvedOrganization = organization ?? activeOrganization;
|
|
18
|
+
const { data: membersList, isPending: membersPending } = useListOrganizationMembers(authClient, {
|
|
19
|
+
query: {
|
|
20
|
+
organizationId: resolvedOrganization?.id
|
|
21
|
+
},
|
|
22
|
+
enabled: !!resolvedOrganization?.id && !hideRole
|
|
23
|
+
});
|
|
24
|
+
const membership = membersList?.members?.find((member) => member.userId === session?.user.id);
|
|
25
|
+
if (isPending ||
|
|
26
|
+
(!organization && activeOrganizationPending) ||
|
|
27
|
+
(!hideRole && !!resolvedOrganization?.id && membersPending)) {
|
|
28
|
+
return (_jsx(OrganizationViewSkeleton, { className: className, hideSlug: hideSlug, size: size, ...props }));
|
|
29
|
+
}
|
|
30
|
+
return (_jsxs("div", { className: cn("flex min-w-0 items-center gap-2", className), ...props, children: [_jsx(OrganizationLogo, { organization: resolvedOrganization, className: size === "sm" ? "size-5 [&>span]:text-xs" : undefined, size: size === "lg" ? "md" : "sm" }), _jsxs("div", { className: "flex flex-col min-w-0", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx("p", { className: "text-foreground text-sm font-medium truncate leading-tight", children: resolvedOrganization?.name }), !hideRole && !!membership && (_jsx(Chip, { className: "shrink-0 -my-0.5", size: "sm", children: roles?.[membership.role] ?? membership.role }))] }), !hideSlug && (_jsx("p", { className: "text-muted text-xs truncate overflow-x-hidden font-mono leading-tight", children: resolvedOrganization?.slug }))] })] }));
|
|
31
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { OrganizationView } from "@better-auth-ui/core/plugins";
|
|
2
|
+
import { type CardProps } from "@heroui/react";
|
|
3
|
+
import { type ComponentProps } from "react";
|
|
4
|
+
export type OrganizationProps = {
|
|
5
|
+
className?: string;
|
|
6
|
+
hideNav?: boolean;
|
|
7
|
+
path?: string;
|
|
8
|
+
variant?: CardProps["variant"];
|
|
9
|
+
/** @remarks `OrganizationView` */
|
|
10
|
+
view?: OrganizationView;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Organization management shell: tabs for profile / danger zone and for
|
|
14
|
+
* people (members / invitations). Path segments come from
|
|
15
|
+
* `useAuthPlugin(organizationPlugin).viewPaths.organization`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function Organization({ className, hideNav, path, variant, view, ...props }: OrganizationProps & ComponentProps<"div">): import("react/jsx-runtime").JSX.Element | null;
|