({
margin: 'auto',
display: 'block',
diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembers.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembers.tsx
index c5d563f54b..a85dfcad7b 100644
--- a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembers.tsx
+++ b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembers.tsx
@@ -1,10 +1,8 @@
-import { CalloutWithAction } from '../../common';
import { useCoreOrganization, useOrganizationProfileContext } from '../../contexts';
-import { Col, descriptors, Flex, Icon, localizationKeys } from '../../customizables';
+import { Col, descriptors, Flex, localizationKeys } from '../../customizables';
import {
CardAlert,
Header,
- IconButton,
NavbarMenuButtonRow,
Tab,
TabPanel,
@@ -14,15 +12,12 @@ import {
useCardState,
withCardStateProvider,
} from '../../elements';
-import { UserAdd } from '../../icons';
-import { useRouter } from '../../router';
import { ActiveMembersList } from './ActiveMembersList';
-import { DomainList } from './DomainList';
-import { InvitedMembersList } from './InvitedMembersList';
import { MembershipWidget } from './MembershipWidget';
+import { OrganizationMembersTabInvitations } from './OrganizationMembersTabInvitations';
+import { OrganizationMembersTabRequests } from './OrganizationMembersTabRequests';
export const OrganizationMembers = withCardStateProvider(() => {
- const { navigate } = useRouter();
const card = useCardState();
const { membership } = useCoreOrganization();
//@ts-expect-error
@@ -56,6 +51,9 @@ export const OrganizationMembers = withCardStateProvider(() => {
localizationKey={localizationKeys('organizationProfile.membersPage.start.headerTitle__invitations')}
/>
)}
+ {isAdmin && (
+
+ )}
@@ -72,96 +70,12 @@ export const OrganizationMembers = withCardStateProvider(() => {
{isAdmin && (
-
- {isAdmin && __unstable_manageBillingUrl && }
-
-
-
-
-
- navigate('organization-settings/domain')}
- />
- }
- redirectSubPath={'organization-settings/domain/'}
- verificationStatus={'verified'}
- enrollmentMode={'automatic_invitation'}
- />
-
-
-
-
-
-
-
-
- {isAdmin && (
- navigate('invite-members')}
- icon={
- ({ marginRight: t.space.$2 })}
- />
- }
- textVariant='buttonExtraSmallBold'
- localizationKey={localizationKeys('organizationProfile.membersPage.action__invite')}
- />
- )}
-
-
-
-
+
+
+ )}
+ {isAdmin && (
+
+
)}
diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembersTabInvitations.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembersTabInvitations.tsx
new file mode 100644
index 0000000000..77813a09fe
--- /dev/null
+++ b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembersTabInvitations.tsx
@@ -0,0 +1,112 @@
+import { CalloutWithAction } from '../../common';
+import { useCoreOrganization, useOrganizationProfileContext } from '../../contexts';
+import { Col, descriptors, Flex, Icon, localizationKeys } from '../../customizables';
+import { Header, IconButton } from '../../elements';
+import { UserAdd } from '../../icons';
+import { useRouter } from '../../router';
+import { DomainList } from './DomainList';
+import { InvitedMembersList } from './InvitedMembersList';
+import { MembershipWidget } from './MembershipWidget';
+
+export const OrganizationMembersTabInvitations = () => {
+ const { navigate } = useRouter();
+ const { membership } = useCoreOrganization();
+ //@ts-expect-error
+ const { __unstable_manageBillingUrl } = useOrganizationProfileContext();
+
+ const isAdmin = membership?.role === 'admin';
+
+ if (!isAdmin) {
+ return null;
+ }
+
+ return (
+
+ {__unstable_manageBillingUrl && }
+
+
+
+
+
+ navigate('organization-settings/domain')}
+ />
+ }
+ redirectSubPath={'organization-settings/domain/'}
+ verificationStatus={'verified'}
+ enrollmentMode={'automatic_invitation'}
+ />
+
+
+
+
+
+
+
+
+
+ navigate('invite-members')}
+ icon={
+ ({ marginRight: t.space.$2 })}
+ />
+ }
+ textVariant='buttonExtraSmallBold'
+ localizationKey={localizationKeys('organizationProfile.membersPage.action__invite')}
+ />
+
+
+
+
+ );
+};
diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembersTabRequests.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembersTabRequests.tsx
new file mode 100644
index 0000000000..6704fed41e
--- /dev/null
+++ b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembersTabRequests.tsx
@@ -0,0 +1,79 @@
+import { useCoreOrganization, useOrganizationProfileContext } from '../../contexts';
+import { Col, Flex, localizationKeys } from '../../customizables';
+import { Header } from '../../elements';
+import { DomainList } from './DomainList';
+import { MembershipWidget } from './MembershipWidget';
+import { RequestToJoinList } from './RequestToJoinList';
+
+export const OrganizationMembersTabRequests = () => {
+ const { membership } = useCoreOrganization();
+ //@ts-expect-error
+ const { __unstable_manageBillingUrl } = useOrganizationProfileContext();
+
+ const isAdmin = membership?.role === 'admin';
+
+ if (!isAdmin) {
+ return null;
+ }
+ return (
+
+ {__unstable_manageBillingUrl && }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/RequestToJoinList.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/RequestToJoinList.tsx
new file mode 100644
index 0000000000..432562f86e
--- /dev/null
+++ b/packages/clerk-js/src/ui/components/OrganizationProfile/RequestToJoinList.tsx
@@ -0,0 +1,96 @@
+import type { OrganizationMembershipRequestResource } from '@clerk/types';
+
+import { useCoreOrganization } from '../../contexts';
+import { Button, Flex, localizationKeys, Td } from '../../customizables';
+import { useCardState, UserPreview } from '../../elements';
+import { handleError } from '../../utils';
+import { DataTable, RowContainer } from './MemberListTable';
+
+const ITEMS_PER_PAGE = 10;
+export const RequestToJoinList = () => {
+ const card = useCardState();
+ const { organization, membershipRequests } = useCoreOrganization({
+ membershipRequests: {
+ pageSize: ITEMS_PER_PAGE,
+ },
+ });
+
+ const mutateSwrState = () => {
+ const unstable__mutate = (membershipRequests as any).unstable__mutate;
+ if (unstable__mutate && typeof unstable__mutate === 'function') {
+ unstable__mutate();
+ }
+ };
+
+ if (!organization) {
+ return null;
+ }
+
+ const approve = (request: OrganizationMembershipRequestResource) => () => {
+ return card
+ .runAsync(request.accept)
+ .then(mutateSwrState)
+ .catch(err => handleError(err, [], card.setError));
+ };
+
+ return (
+ null)}
+ itemCount={membershipRequests?.count ?? 0}
+ itemsPerPage={ITEMS_PER_PAGE}
+ isLoading={membershipRequests?.isFetching}
+ emptyStateLocalizationKey={localizationKeys('organizationProfile.membersPage.requestsTab.table__emptyRow')}
+ headers={[
+ localizationKeys('organizationProfile.membersPage.activeMembersTab.tableHeader__user'),
+ localizationKeys('organizationProfile.membersPage.requestsTab.tableHeader__requested'),
+ localizationKeys('organizationProfile.membersPage.activeMembersTab.tableHeader__actions'),
+ ]}
+ rows={(membershipRequests?.data || []).map(i => (
+
+ ))}
+ />
+ );
+};
+
+const RequestRow = (props: { request: OrganizationMembershipRequestResource; onAccept: () => unknown }) => {
+ const { request, onAccept } = props;
+
+ return (
+
+
+
+ |
+ {request.createdAt.toLocaleDateString()} |
+
+
+
+
+
+ |
+
+ );
+};
+
+const AcceptRejectRequestButtons = (props: { onAccept: () => unknown }) => {
+ const card = useCardState();
+ return (
+ <>
+
+ >
+ );
+};
diff --git a/packages/clerk-js/src/utils/pagesToOffset.ts b/packages/clerk-js/src/utils/pagesToOffset.ts
index 131c27b342..7b57d90cca 100644
--- a/packages/clerk-js/src/utils/pagesToOffset.ts
+++ b/packages/clerk-js/src/utils/pagesToOffset.ts
@@ -4,14 +4,21 @@ type Pages = {
initialPage?: number;
pageSize?: number;
};
-
+function getNonUndefinedValues(obj: Record): Record {
+ return Object.keys(obj).reduce((result, key) => {
+ if (obj[key] !== undefined) {
+ result[key] = obj[key];
+ }
+ return result;
+ }, {} as Record);
+}
export function convertPageToOffset(pageParams: T): ClerkPaginationParams {
const { pageSize, initialPage, ...restParams } = pageParams || {};
const _pageSize = pageSize ?? 10;
const _initialPage = initialPage ?? 1;
return {
- ...restParams,
+ ...getNonUndefinedValues(restParams),
limit: _pageSize,
offset: (_initialPage - 1) * _pageSize,
};
diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts
index e75c723fef..97f4282eae 100644
--- a/packages/localizations/src/en-US.ts
+++ b/packages/localizations/src/en-US.ts
@@ -612,6 +612,7 @@ export const enUS: LocalizationResource = {
headerTitle__members: 'Members',
headerTitle__invited: 'Invited',
headerTitle__invitations: 'Invitations',
+ headerTitle__requests: 'Requests',
},
activeMembersTab: {
tableHeader__user: 'User',
@@ -625,6 +626,7 @@ export const enUS: LocalizationResource = {
menuAction__revoke: 'Revoke invitation',
},
invitationsTab: {
+ table__emptyRow: 'No invitations to display',
manualInvitations: {
headerTitle: 'Individual invitations',
headerSubtitle: 'Browse and manage invited members.',
@@ -637,6 +639,20 @@ export const enUS: LocalizationResource = {
calloutActionLabel: 'Setup verified domain',
},
},
+ requestsTab: {
+ tableHeader__requested: 'Requested access',
+ menuAction__approve: 'Approve',
+ table__emptyRow: 'No requests to display',
+ requests: {
+ headerTitle: 'Requests',
+ headerSubtitle: 'Browse and manage users who requested to join the organization.',
+ },
+ autoSuggestions: {
+ headerTitle: 'Automatic suggestions',
+ headerSubtitle:
+ 'Users with an email address on your verified domain will see a suggestion to request to join the organization.',
+ },
+ },
},
},
createOrganization: {
diff --git a/packages/shared/src/hooks/useOrganization.tsx b/packages/shared/src/hooks/useOrganization.tsx
index 93e08c3e61..cd75aa96fa 100644
--- a/packages/shared/src/hooks/useOrganization.tsx
+++ b/packages/shared/src/hooks/useOrganization.tsx
@@ -95,6 +95,7 @@ export const useOrganization: UseOrganization = params => {
pageSize: 10,
keepPreviousData: false,
infinite: false,
+ enrollmentMode: undefined,
});
const membershipRequestSafeValues = useWithSafeValues(membershipRequestsListParams, {
@@ -115,6 +116,7 @@ export const useOrganization: UseOrganization = params => {
: {
initialPage: domainSafeValues.initialPage,
pageSize: domainSafeValues.pageSize,
+ enrollmentMode: domainSafeValues.enrollmentMode,
};
const membershipRequestParams =
@@ -123,6 +125,7 @@ export const useOrganization: UseOrganization = params => {
: {
initialPage: membershipRequestSafeValues.initialPage,
pageSize: membershipRequestSafeValues.pageSize,
+ status: membershipRequestSafeValues.status,
};
const domains = usePagesOrInfinite>(
diff --git a/packages/types/src/localization.ts b/packages/types/src/localization.ts
index 9f5787c4af..6f2ae449c9 100644
--- a/packages/types/src/localization.ts
+++ b/packages/types/src/localization.ts
@@ -635,6 +635,7 @@ type _LocalizationResource = {
*/
headerTitle__invited: LocalizationValue;
headerTitle__invitations: LocalizationValue;
+ headerTitle__requests: LocalizationValue;
};
activeMembersTab: {
tableHeader__user: LocalizationValue;
@@ -648,6 +649,7 @@ type _LocalizationResource = {
menuAction__revoke: LocalizationValue;
};
invitationsTab: {
+ table__emptyRow: LocalizationValue;
manualInvitations: {
headerTitle: LocalizationValue;
headerSubtitle: LocalizationValue;
@@ -659,6 +661,19 @@ type _LocalizationResource = {
calloutActionLabel: LocalizationValue;
};
};
+ requestsTab: {
+ tableHeader__requested: LocalizationValue;
+ menuAction__approve: LocalizationValue;
+ table__emptyRow: LocalizationValue;
+ requests: {
+ headerTitle: LocalizationValue;
+ headerSubtitle: LocalizationValue;
+ };
+ autoSuggestions: {
+ headerTitle: LocalizationValue;
+ headerSubtitle: LocalizationValue;
+ };
+ };
};
};
createOrganization: {
diff --git a/packages/types/src/organization.ts b/packages/types/src/organization.ts
index 375bc52019..3794692a21 100644
--- a/packages/types/src/organization.ts
+++ b/packages/types/src/organization.ts
@@ -1,5 +1,5 @@
import type { ClerkPaginatedResponse, ClerkPaginationParams } from './api';
-import type { OrganizationDomainResource } from './organizationDomain';
+import type { OrganizationDomainResource, OrganizationEnrollmentMode } from './organizationDomain';
import type { OrganizationInvitationResource, OrganizationInvitationStatus } from './organizationInvitation';
import type { MembershipRole, OrganizationMembershipResource } from './organizationMembership';
import type { OrganizationMembershipRequestResource } from './organizationMembershipRequest';
@@ -74,6 +74,8 @@ export type GetDomainsParams = {
* Maximum number of items returned per request. The initial value persists between re-renders
*/
pageSize?: number;
+
+ enrollmentMode?: OrganizationEnrollmentMode;
};
export type GetMembershipRequestParams = {
diff --git a/packages/types/src/organizationDomain.ts b/packages/types/src/organizationDomain.ts
index 5e3ca613c0..98933e1f72 100644
--- a/packages/types/src/organizationDomain.ts
+++ b/packages/types/src/organizationDomain.ts
@@ -9,7 +9,7 @@ export interface OrganizationDomainVerification {
export type OrganizationDomainVerificationStatus = 'unverified' | 'verified';
-export type OrganizationEnrollmentMode = 'manual_invitation' | 'automatic_invitation';
+export type OrganizationEnrollmentMode = 'manual_invitation' | 'automatic_invitation' | 'automatic_suggestion';
export interface OrganizationDomainResource extends ClerkResource {
id: string;
|