Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added auth0-myorganization-js-1.1.0.tgz
Binary file not shown.
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
},
"dependencies": {
"@auth0/myaccount-js": "1.0.0-beta.0",
"@auth0/myorganization-js": "1.1.0",
"@auth0/myorganization-js": "file:../../auth0-myorganization-js-1.1.0.tgz",
"zod": "^3.22.4"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,11 @@ export interface OrganizationInvitationTabMessages {
email_required_error?: string;
roles_label?: string;
roles_placeholder?: string;
provider_label?: string;
provider_placeholder?: string;
connection_label?: string;
connection_placeholder?: string;
connection_helper?: string;
connection_group_user_store?: string;
connection_group_identity_provider?: string;
submit_button?: string;
creating?: string;
cancel_button?: string;
Expand All @@ -53,7 +56,7 @@ export interface OrganizationInvitationTabMessages {
email_label?: string;
status_label?: string;
roles_label?: string;
provider_label?: string;
connection_label?: string;
created_at_label?: string;
expires_at_label?: string;
invited_by_label?: string;
Expand Down Expand Up @@ -85,6 +88,7 @@ export interface OrganizationInvitationTabMessages {
revoke_failed?: string;
resend_failed?: string;
revoke_resend_failed?: string;
connection_required?: string;
copy_url_failed?: string;
};
}
11 changes: 7 additions & 4 deletions packages/core/src/i18n/translations/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1266,9 +1266,11 @@
"email_required_error": "Please enter at least one email address.",
"roles_label": "Roles",
"roles_placeholder": "Select roles (optional)",
"provider_label": "Provider",
"provider_placeholder": "Select provider (optional)",
"provider_helper": "If none is selected, the member can log in with any provider.",
"connection_label": "Connection",
"connection_placeholder": "Select connection",
"connection_helper": "Select the connection the member will use to log in.",
"connection_group_user_store": "User Directory",
"connection_group_identity_provider": "Enterprise SSO",
"submit_button": "Send Invite",
"creating": "Creating...",
"cancel_button": "Cancel",
Expand All @@ -1280,7 +1282,7 @@
"email_label": "Email",
"status_label": "Status",
"roles_label": "Roles",
"provider_label": "Identity Provider",
"connection_label": "Connection",
"created_at_label": "Created",
"expires_at_label": "Expires",
"invited_by_label": "Invited By",
Expand Down Expand Up @@ -1313,6 +1315,7 @@
"revoke_failed": "Failed to revoke invitation. Please try again.",
"resend_failed": "Failed to resend invitation. Please try again.",
"revoke_resend_failed": "Failed to revoke and resend invitation. Please try again.",
"connection_required": "At least one of identity_provider_id or user_store_id must be provided.",
"copy_url_failed": "Failed to copy invitation URL. Please try again."
}
}
Expand Down
11 changes: 7 additions & 4 deletions packages/core/src/i18n/translations/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -1267,9 +1267,11 @@
"email_required_error": "メールアドレスを1つ以上入力してください。",
"roles_label": "ロール",
"roles_placeholder": "ロールを選択(任意)",
"provider_label": "プロバイダー",
"provider_placeholder": "プロバイダーを選択(任意)",
"provider_helper": "選択しない場合、メンバーは任意のプロバイダーでログインできます。",
"connection_label": "接続",
"connection_placeholder": "接続を選択",
"connection_helper": "メンバーがログインに使用する接続を選択してください。",
"connection_group_user_store": "ユーザーディレクトリ",
"connection_group_identity_provider": "エンタープライズSSO",
"submit_button": "招待を送信",
"creating": "作成中...",
"cancel_button": "キャンセル",
Expand All @@ -1281,7 +1283,7 @@
"email_label": "メール",
"status_label": "ステータス",
"roles_label": "ロール",
"provider_label": "IDプロバイダー",
"connection_label": "接続",
"created_at_label": "作成日",
"expires_at_label": "有効期限",
"invited_by_label": "招待者",
Expand Down Expand Up @@ -1314,6 +1316,7 @@
"revoke_failed": "招待の取り消しに失敗しました。もう一度お試しください。",
"resend_failed": "招待の再送信に失敗しました。もう一度お試しください。",
"revoke_resend_failed": "招待の取り消しと再送信に失敗しました。もう一度お試しください。",
"connection_required": "identity_provider_id または user_store_id のいずれか一方を指定する必要があります。",
"copy_url_failed": "招待URLのコピーに失敗しました。もう一度お試しください。"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export const memberManagementQueryKeys = {
[...memberManagementQueryKeys.all, 'roles', 'search', term] as const,
members: () => [...memberManagementQueryKeys.all, 'members'] as const,
memberRoles: (id: string) => [...memberManagementQueryKeys.all, 'member-roles', id] as const,
identityProviders: () => [...memberManagementQueryKeys.all, 'identity-providers'] as const,
userStores: () => [...memberManagementQueryKeys.all, 'user-stores'] as const,
Comment thread
rax7389 marked this conversation as resolved.
organization: ['organization', 'details'] as const,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,13 @@ export type RoleId = MyOrganization.RoleId;
* Response content for listing organization roles.
*/
export type ListRolesResponseContent = MyOrganization.ListRolesResponseContent;

/**
* User store available for routing invitations (e.g. an enterprise directory).
*/
export type UserStore = MyOrganization.UserStore;

/**
* Response content for listing organization user stores.
*/
export type ListUserStoresResponseContent = MyOrganization.ListUserStoresResponseContent;
7 changes: 7 additions & 0 deletions packages/react/src/__tests__/utils/test-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,11 @@ export function setupJsdomMocks(): void {
if (typeof window !== 'undefined') {
global.PointerEvent = MockPointerEvent as typeof PointerEvent;
}

// jsdom lacks these APIs that Radix primitives (e.g. Select) rely on to open.
if (typeof Element !== 'undefined') {
Element.prototype.scrollIntoView = Element.prototype.scrollIntoView ?? vi.fn();
Element.prototype.hasPointerCapture = Element.prototype.hasPointerCapture ?? vi.fn();
Element.prototype.releasePointerCapture = Element.prototype.releasePointerCapture ?? vi.fn();
}
Comment thread
chakrihacker marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import * as useCoreClientModule from '@/hooks/shared/use-core-client';
import {
createMockDomain,
createMockVerifiedDomain,
createMockIdentityProvider,
createMockIdentityProviderAssociatedWithDomain,
createMockDomainTableProps,
createMockCreateAction,
createMockVerifyAction,
Expand Down Expand Up @@ -578,71 +576,6 @@ describe('DomainTable', () => {
});
});

describe('onOpenProvider', () => {
describe('when provider is clicked', () => {
it('should call onOpenProvider with provider details', async () => {
const user = userEvent.setup();
const onOpenProvider = vi.fn();
const provider = createMockIdentityProvider({
id: 'con_provider_view',
display_name: 'View Provider',
name: 'view-provider',
});

const apiService = mockCoreClient.getMyOrganizationApiClient();
(
apiService.organization.identityProviders.list as ReturnType<typeof vi.fn>
).mockResolvedValue({
identity_providers: [provider],
});
(
apiService.organization.domains.identityProviders.get as ReturnType<typeof vi.fn>
).mockResolvedValue({
identity_providers: [
createMockIdentityProviderAssociatedWithDomain({
id: provider.id,
name: provider.name,
display_name: provider.display_name,
strategy: provider.strategy,
is_associated: true,
}),
],
});

renderWithProviders(<DomainTable {...createMockDomainTableProps({ onOpenProvider })} />);

await waitForComponentToLoad();

const verifiedBadge = await screen.findByText(/shared\.domain_statuses\.verified/i);
const verifiedRow = verifiedBadge.closest('tr');
expect(verifiedRow).not.toBeNull();

const actionButton = within(verifiedRow as HTMLElement).getByRole('button');
await user.click(actionButton);

const configureMenuItem = await screen.findByRole('menuitem', {
name: /configure_button_text/i,
});
await user.click(configureMenuItem);

const configureModal = await screen.findByRole('dialog');
const viewProviderButton = await within(configureModal).findByRole('button', {
name: /view_provider_button_text/i,
});
await user.click(viewProviderButton);

expect(onOpenProvider).toHaveBeenCalledTimes(1);
expect(onOpenProvider).toHaveBeenCalledWith(
expect.objectContaining({
id: provider.id,
name: provider.name,
display_name: provider.display_name,
}),
);
});
});
});

describe('onCreateProvider', () => {
describe('when create provider is clicked', () => {
it('should call onCreateProvider', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ const createMockMemberManagementResult = (
availableRoles: createMockRoleOptions(),
searchedRoles: createMockRoleOptions(),
onRoleSearch: vi.fn(),
availableProviders: [],
availableConnections: [],
members: [member],
invitations: [invitation],
isFetchingInvitations: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function OrganizationMemberManagementView(props: OrganizationMemberManage
availableRoles,
searchedRoles,
onRoleSearch,
availableProviders,
availableConnections,
modalState,
membersUpdatedAt,
invitationsUpdatedAt,
Expand Down Expand Up @@ -246,7 +246,7 @@ export function OrganizationMemberManagementView(props: OrganizationMemberManage
isLoading={isCreatingInvitation}
customMessages={customMessages?.invitation}
availableRoles={searchedRoles}
availableProviders={availableProviders}
availableConnections={availableConnections}
style={currentStyles.variables}
onClose={closeModal}
onCreate={handleCreateSubmit}
Expand All @@ -261,7 +261,7 @@ export function OrganizationMemberManagementView(props: OrganizationMemberManage
isResending={isResendingInvitation}
customMessages={customMessages?.invitation}
availableRoles={availableRoles}
availableProviders={availableProviders}
availableConnections={availableConnections}
readOnly={readOnly}
style={currentStyles.variables}
onClose={closeModal}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
createMockPendingInvitation,
createMockExpiredInvitation,
createMockRoles,
createMockProviders,
createMockConnections,
} from '@/tests/utils/__mocks__/my-organization/member-management/invitation.mocks';
import { renderWithProviders, TestProvider } from '@/tests/utils/test-provider';

Expand Down Expand Up @@ -195,40 +195,63 @@ describe('OrganizationInvitationDetailsModal', () => {
});
});

describe('identity provider', () => {
it('should display provider name when resolved', () => {
describe('connection', () => {
it('should display identity provider name when resolved', () => {
const invitation = createMockInvitation({ identity_provider_id: 'con_provider1' });
const availableProviders = createMockProviders();

renderWithProviders(
<OrganizationInvitationDetailsModal
{...createMockDetailsModalProps({ invitation, availableProviders })}
{...createMockDetailsModalProps({
invitation,
availableConnections: createMockConnections(),
})}
/>,
);

expect(screen.getByDisplayValue('Google')).toBeInTheDocument();
});

it('should show provider ID as fallback when provider not found', () => {
it('should display user store name when resolved', () => {
const invitation = createMockInvitation({
identity_provider_id: undefined,
user_store_id: 'us_store1',
});

renderWithProviders(
<OrganizationInvitationDetailsModal
{...createMockDetailsModalProps({
invitation,
availableConnections: createMockConnections(),
})}
/>,
);

expect(screen.getByDisplayValue('Acme Directory')).toBeInTheDocument();
});

it('should show connection ID as fallback when connection not found', () => {
const invitation = createMockInvitation({ identity_provider_id: 'con_unknown' });

renderWithProviders(
<OrganizationInvitationDetailsModal
{...createMockDetailsModalProps({ invitation, availableProviders: [] })}
{...createMockDetailsModalProps({ invitation, availableConnections: [] })}
/>,
);

expect(screen.getByDisplayValue('con_unknown')).toBeInTheDocument();
});

it('should not display provider section when no provider assigned', () => {
const invitation = createMockInvitation({ identity_provider_id: undefined });
it('should not display connection section when no connection assigned', () => {
const invitation = createMockInvitation({
identity_provider_id: undefined,
user_store_id: undefined,
});

renderWithProviders(
<OrganizationInvitationDetailsModal {...createMockDetailsModalProps({ invitation })} />,
);

expect(screen.queryByText('invitation.details.provider_label')).not.toBeInTheDocument();
expect(screen.queryByText('invitation.details.connection_label')).not.toBeInTheDocument();
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function getStatusBadgeVariant(status: InvitationStatus): 'warning' | 'destructi
* @param props.isResending - Whether a resend action is in progress.
* @param props.customMessages - Custom translation messages.
* @param props.availableRoles - Available roles for display.
* @param props.availableProviders - Available providers for display.
* @param props.availableConnections - Merged identity providers + user stores for resolving the connection name.
* @param props.readOnly - Whether in read-only mode.
* @param props.onClose - Callback when modal is closed.
* @param props.onCopyUrl - Callback when copy URL is clicked.
Expand All @@ -62,7 +62,7 @@ export function OrganizationInvitationDetailsModal({
isResending = false,
customMessages = {},
availableRoles = [],
availableProviders = [],
availableConnections = [],
readOnly = false,
onClose,
onCopyUrl,
Expand All @@ -87,11 +87,12 @@ export function OrganizationInvitationDetailsModal({
.filter(Boolean);
}, [invitation?.roles, availableRoles]);

const providerName = React.useMemo(() => {
if (!invitation?.identity_provider_id) return null;
const provider = availableProviders.find((p) => p.id === invitation.identity_provider_id);
return provider?.name ?? invitation.identity_provider_id;
}, [invitation?.identity_provider_id, availableProviders]);
const connectionName = React.useMemo(() => {
const connectionId = invitation?.identity_provider_id ?? invitation?.user_store_id;
if (!connectionId) return null;
const connection = availableConnections.find((c) => c.id === connectionId);
return connection?.name ?? connectionId;
}, [invitation?.identity_provider_id, invitation?.user_store_id, availableConnections]);

const [copied, setCopied] = React.useState(false);
const copyTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
Expand Down Expand Up @@ -255,13 +256,13 @@ export function OrganizationInvitationDetailsModal({
<TextField value={invitation?.inviter?.name ?? '-'} readOnly />
</div>

{/* Identity Provider */}
{providerName && (
{/* Connection (identity provider or user directory) */}
{connectionName && (
<div className="space-y-2">
<Label className="text-sm font-medium text-muted-foreground">
{t('invitation.details.provider_label')}
{t('invitation.details.connection_label')}
</Label>
<TextField value={providerName} readOnly />
<TextField value={connectionName} readOnly />
Comment on lines +259 to +265

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a connection-neutral label for both connection types.

This block now displays identity providers and user stores, but still renders invitation.details.provider_label. If that translation is provider-specific, user-store invitations will be mislabeled. Use the new connection-specific/neutral translation key and update the corresponding assertion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/react/src/components/auth0/my-organization/shared/member-management/invitations/invitation-details/organization-invitation-details-modal.tsx`
around lines 259 - 265, Update the connection label in the invitation details
modal’s connectionName block to use the new connection-neutral translation key
instead of invitation.details.provider_label, and update the corresponding
assertion to expect the neutral label for both identity providers and user
stores.

</div>
)}
</div>
Expand Down
Loading