Skip to content

Commit 6c8ea1a

Browse files
committed
feat(core, react): add service, react types, and invitation components
1 parent 0d08c3c commit 6c8ea1a

14 files changed

Lines changed: 1536 additions & 7 deletions

File tree

175 KB
Binary file not shown.

packages/core/src/services/my-organization/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
export * from './organization-management';
88
export * from './idp-management';
99
export * from './domain-management';
10+
export * from './member-management/member-management-types';
1011
export * from './config';
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/**
2+
* Member management type definitions for organization member and invitation operations.
3+
* @module member-management-types
4+
* @internal
5+
*/
6+
import type { MyOrganization } from '@auth0/myorganization-js';
7+
8+
/**
9+
* Organization member ID type.
10+
*/
11+
export type OrgMemberId = MyOrganization.OrgMemberId;
12+
13+
/**
14+
* Organization member entity.
15+
*/
16+
export type OrgMember = MyOrganization.OrgMember;
17+
18+
/**
19+
* Organization member role.
20+
*/
21+
export type OrgMemberRole = MyOrganization.OrgMemberRole;
22+
23+
/**
24+
* Organization member role ID.
25+
*/
26+
export type OrgMemberRoleId = MyOrganization.OrgMemberRoleId;
27+
28+
/**
29+
* Response content for listing organization members.
30+
*/
31+
export type ListOrganizationMembersResponseContent =
32+
MyOrganization.ListOrganizationMembersResponseContent;
33+
34+
/**
35+
* Response content for getting a single organization member.
36+
*/
37+
export type GetOrganizationMemberResponseContent =
38+
MyOrganization.GetOrganizationMemberResponseContent;
39+
40+
/**
41+
* Request parameters for listing organization members.
42+
*/
43+
export type ListOrganizationMembersRequestParameters =
44+
MyOrganization.ListOrganizationMembersRequestParameters;
45+
46+
/**
47+
* Response content for getting organization member roles.
48+
*/
49+
export type GetOrganizationMemberRolesResponseContent =
50+
MyOrganization.GetOrganizationMemberRolesResponseContent;
51+
52+
/**
53+
* Request content for assigning a role to an organization member.
54+
*/
55+
export type AssignOrganizationMemberRoleRequestContent =
56+
MyOrganization.AssignOrganizationMemberRoleRequestContent;
57+
58+
/**
59+
* Response content for assigning a role to an organization member.
60+
*/
61+
export type AssignOrganizationMemberRoleResponseContent =
62+
MyOrganization.AssignOrganizationMemberRoleResponseContent;
63+
64+
/**
65+
* Invitation ID type.
66+
*/
67+
export type InvitationId = MyOrganization.InvitationId;
68+
69+
/**
70+
* Member invitation entity.
71+
*/
72+
export type MemberInvitation = MyOrganization.MemberInvitation;
73+
74+
/**
75+
* Member invitation invitee details.
76+
*/
77+
export type MemberInvitationInvitee = MyOrganization.MemberInvitationInvitee;
78+
79+
/**
80+
* Member invitation inviter details.
81+
*/
82+
export type MemberInvitationInviter = MyOrganization.MemberInvitationInviter;
83+
84+
/**
85+
* Response content for listing member invitations.
86+
*/
87+
export type ListMembersInvitationsResponseContent =
88+
MyOrganization.ListMembersInvitationsResponseContent;
89+
90+
/**
91+
* Request parameters for listing member invitations.
92+
*/
93+
export type ListMemberInvitationsRequestParameters =
94+
MyOrganization.ListMemberInvitationsRequestParameters;
95+
96+
/**
97+
* Request content for creating a member invitation.
98+
*/
99+
export type CreateMemberInvitationRequestContent =
100+
MyOrganization.CreateMemberInvitationRequestContent;
101+
102+
/**
103+
* Response content for creating a member invitation.
104+
*/
105+
export type CreateMemberInvitationResponseContent =
106+
MyOrganization.CreateMemberInvitationResponseContent;
107+
108+
/**
109+
* Response content for getting a member invitation.
110+
*/
111+
export type GetMemberInvitationResponseContent = MyOrganization.GetMemberInvitationResponseContent;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
/**
2+
* Organization invitation details modal component.
3+
* @module organization-invitation-details-modal
4+
*/
5+
6+
import type { MemberInvitation } from '@auth0/universal-components-core';
7+
import { Link } from 'lucide-react';
8+
import * as React from 'react';
9+
10+
import { CopyableTextField } from '@/components/auth0/shared/copyable-text-field';
11+
import { Badge } from '@/components/ui/badge';
12+
import { Button } from '@/components/ui/button';
13+
import {
14+
Dialog,
15+
DialogContent,
16+
DialogDescription,
17+
DialogFooter,
18+
DialogHeader,
19+
DialogTitle,
20+
} from '@/components/ui/dialog';
21+
import { Label } from '@/components/ui/label';
22+
import { TextField } from '@/components/ui/text-field';
23+
import { TextFieldGroup } from '@/components/ui/text-field-group';
24+
import { useTranslator } from '@/hooks/shared/use-translator';
25+
import { getInvitationStatus } from '@/lib/utils/my-organization/member-management/member-management-utils';
26+
import type {
27+
InvitationStatus,
28+
RoleOption,
29+
IdentityProviderOption,
30+
OrganizationInvitationTabMessages,
31+
} from '@/types/my-organization/member-management/organization-invitation-table-types';
32+
33+
export interface OrganizationInvitationDetailsModalProps {
34+
invitation: MemberInvitation | null;
35+
isOpen: boolean;
36+
isRevoking?: boolean;
37+
isResending?: boolean;
38+
customMessages?: Partial<OrganizationInvitationTabMessages>;
39+
availableRoles?: RoleOption[];
40+
availableProviders?: IdentityProviderOption[];
41+
readOnly?: boolean;
42+
onClose: () => void;
43+
onCopyUrl?: (invitation: MemberInvitation) => void;
44+
onRevoke?: (invitation?: MemberInvitation) => void;
45+
onResend?: (invitation?: MemberInvitation) => void;
46+
className?: string;
47+
}
48+
49+
/**
50+
* Returns the badge variant for a given invitation status.
51+
* @param status - The invitation status.
52+
* @returns The badge variant string.
53+
*/
54+
function getStatusBadgeVariant(status: InvitationStatus): 'warning' | 'destructive' {
55+
return status === 'pending' ? 'warning' : 'destructive';
56+
}
57+
58+
/**
59+
* Modal for viewing invitation details with revoke and resend actions.
60+
* @param props - The component props.
61+
* @param props.invitation - The invitation to display.
62+
* @param props.isOpen - Whether the modal is open.
63+
* @param props.isRevoking - Whether a revoke action is in progress.
64+
* @param props.isResending - Whether a resend action is in progress.
65+
* @param props.customMessages - Custom translation messages.
66+
* @param props.availableRoles - Available roles for display.
67+
* @param props.availableProviders - Available providers for display.
68+
* @param props.readOnly - Whether in read-only mode.
69+
* @param props.onClose - Callback when modal is closed.
70+
* @param props.onCopyUrl - Callback when copy URL is clicked.
71+
* @param props.onRevoke - Callback when revoke is clicked.
72+
* @param props.onResend - Callback when revoke and resend is clicked.
73+
* @param props.className - Optional CSS class name.
74+
* @returns The modal component.
75+
*/
76+
export function OrganizationInvitationDetailsModal({
77+
invitation,
78+
isOpen,
79+
isRevoking = false,
80+
isResending = false,
81+
customMessages = {},
82+
availableRoles = [],
83+
availableProviders = [],
84+
readOnly = false,
85+
onClose,
86+
onCopyUrl,
87+
onRevoke,
88+
onResend,
89+
className,
90+
}: OrganizationInvitationDetailsModalProps): React.JSX.Element {
91+
const { t } = useTranslator('member_management', customMessages);
92+
93+
const status = invitation ? getInvitationStatus(invitation) : 'pending';
94+
const isPending = status === 'pending';
95+
const isActionInProgress = isRevoking || isResending;
96+
97+
const roleNames = React.useMemo(() => {
98+
if (!invitation?.roles || invitation.roles.length === 0) return [];
99+
return invitation.roles
100+
.map((roleId) => {
101+
const role = availableRoles.find((r) => r.id === roleId);
102+
return role?.name ?? roleId;
103+
})
104+
.filter(Boolean);
105+
}, [invitation?.roles, availableRoles]);
106+
107+
const providerName = React.useMemo(() => {
108+
if (!invitation?.identity_provider_id) return null;
109+
const provider = availableProviders.find((p) => p.id === invitation.identity_provider_id);
110+
return provider?.name ?? invitation.identity_provider_id;
111+
}, [invitation?.identity_provider_id, availableProviders]);
112+
113+
const handleCopyUrl = React.useCallback(() => {
114+
if (invitation) {
115+
onCopyUrl?.(invitation);
116+
}
117+
}, [invitation, onCopyUrl]);
118+
119+
const handleRevoke = React.useCallback(() => {
120+
if (invitation) {
121+
onRevoke?.(invitation);
122+
}
123+
}, [invitation, onRevoke]);
124+
125+
const handleResend = React.useCallback(() => {
126+
if (invitation) {
127+
onResend?.(invitation);
128+
}
129+
}, [invitation, onResend]);
130+
131+
return (
132+
<Dialog open={isOpen} onOpenChange={onClose}>
133+
<DialogContent className={className}>
134+
<DialogHeader>
135+
<div className="flex items-center gap-2">
136+
<DialogTitle>{t('invitation.details.title')}</DialogTitle>
137+
<Badge variant={getStatusBadgeVariant(status)} size="sm">
138+
{isPending
139+
? t('invitation.table.status_pending')
140+
: t('invitation.table.status_expired')}
141+
</Badge>
142+
</div>
143+
<DialogDescription className="sr-only">{t('invitation.details.title')}</DialogDescription>
144+
</DialogHeader>
145+
146+
<div className="space-y-4 py-4">
147+
{/* Email */}
148+
<div className="space-y-2">
149+
<Label className="text-sm font-medium text-muted-foreground">
150+
{t('invitation.details.email_label')}
151+
</Label>
152+
<TextField value={invitation?.invitee?.email ?? '-'} readOnly />
153+
</div>
154+
155+
{/* Created At */}
156+
<div className="space-y-2">
157+
<Label className="text-sm font-medium text-muted-foreground">
158+
{t('invitation.details.created_at_label')}
159+
</Label>
160+
<TextField
161+
value={
162+
invitation?.created_at ? new Date(invitation.created_at).toLocaleString() : '-'
163+
}
164+
readOnly
165+
/>
166+
</div>
167+
168+
{/* Expires At */}
169+
<div className="space-y-2">
170+
<Label className="text-sm font-medium text-muted-foreground">
171+
{t('invitation.details.expires_at_label')}
172+
</Label>
173+
<TextField
174+
value={
175+
invitation?.expires_at ? new Date(invitation.expires_at).toLocaleString() : '-'
176+
}
177+
readOnly
178+
/>
179+
</div>
180+
181+
{/* Roles */}
182+
<div className="space-y-2">
183+
<Label className="text-sm font-medium text-muted-foreground">
184+
{t('invitation.details.roles_label')}
185+
</Label>
186+
{roleNames.length > 0 ? (
187+
<TextFieldGroup
188+
chips={roleNames.map((name) => ({ label: name, value: name }))}
189+
summarizeChips={false}
190+
disabled
191+
readOnly
192+
/>
193+
) : (
194+
<TextField value="-" readOnly />
195+
)}
196+
</div>
197+
198+
{/* Invitation URL */}
199+
{invitation?.invitation_url && (
200+
<div className="space-y-2">
201+
<Label className="text-sm font-medium text-muted-foreground">
202+
{t('invitation.details.invitation_url_label')}
203+
</Label>
204+
<CopyableTextField
205+
value={invitation.invitation_url}
206+
readOnly
207+
onCopy={handleCopyUrl}
208+
startAdornment={<Link className="h-4 w-4 text-muted-foreground" />}
209+
/>
210+
</div>
211+
)}
212+
213+
{/* Revoke / Resend Actions (inline, below invitation URL) */}
214+
{!readOnly && (
215+
<div className="flex flex-wrap gap-2">
216+
<Button variant="outline" onClick={handleResend} disabled={isActionInProgress}>
217+
{t('invitation.details.resend_button')}
218+
</Button>
219+
<Button variant="destructive" onClick={handleRevoke} disabled={isActionInProgress}>
220+
{t('invitation.details.revoke_button')}
221+
</Button>
222+
</div>
223+
)}
224+
225+
{/* Invited By */}
226+
<div className="space-y-2">
227+
<Label className="text-sm font-medium text-muted-foreground">
228+
{t('invitation.details.invited_by_label')}
229+
</Label>
230+
<TextField value={invitation?.inviter?.name ?? '-'} readOnly />
231+
</div>
232+
233+
{/* Identity Provider */}
234+
{providerName && (
235+
<div className="space-y-2">
236+
<Label className="text-sm font-medium text-muted-foreground">
237+
{t('invitation.details.provider_label')}
238+
</Label>
239+
<TextField value={providerName} readOnly />
240+
</div>
241+
)}
242+
</div>
243+
244+
<DialogFooter>
245+
<Button onClick={onClose}>{t('invitation.details.close_button')}</Button>
246+
</DialogFooter>
247+
</DialogContent>
248+
</Dialog>
249+
);
250+
}

0 commit comments

Comments
 (0)