From dcb875159cc6b363e30998bb5242460e5407fe15 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 10 Aug 2023 18:45:25 +0300 Subject: [PATCH 1/6] feat(clerk-js): Fetch domains by enrollmentMode --- .../core/resources/OrganizationMembershipRequest.ts | 3 +++ packages/clerk-js/src/utils/pagesToOffset.ts | 11 +++++++++-- packages/shared/src/hooks/useOrganization.tsx | 3 +++ packages/types/src/organization.ts | 4 +++- packages/types/src/organizationDomain.ts | 2 +- 5 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/clerk-js/src/core/resources/OrganizationMembershipRequest.ts b/packages/clerk-js/src/core/resources/OrganizationMembershipRequest.ts index c38b5cd1a3d..1beb70211e0 100644 --- a/packages/clerk-js/src/core/resources/OrganizationMembershipRequest.ts +++ b/packages/clerk-js/src/core/resources/OrganizationMembershipRequest.ts @@ -1,6 +1,7 @@ import type { OrganizationInvitationStatus, OrganizationMembershipRequestResource, PublicUserData } from '@clerk/types'; import type { OrganizationMembershipRequestJSON } from '@clerk/types'; +import { unixEpochToDate } from '../../utils/date'; import { BaseResource } from './Base'; export class OrganizationMembershipRequest extends BaseResource implements OrganizationMembershipRequestResource { @@ -27,6 +28,8 @@ export class OrganizationMembershipRequest extends BaseResource implements Organ this.id = data.id; this.organizationId = data.organization_id; this.status = data.status; + this.createdAt = unixEpochToDate(data.created_at); + this.updatedAt = unixEpochToDate(data.updated_at); if (data.public_user_data) { this.publicUserData = { firstName: data.public_user_data.first_name, diff --git a/packages/clerk-js/src/utils/pagesToOffset.ts b/packages/clerk-js/src/utils/pagesToOffset.ts index 131c27b3425..7b57d90cca6 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/shared/src/hooks/useOrganization.tsx b/packages/shared/src/hooks/useOrganization.tsx index 93e08c3e61a..cd75aa96faf 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/organization.ts b/packages/types/src/organization.ts index 375bc520194..3794692a216 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 5e3ca613c06..98933e1f722 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; From 5a2d88f35cd4778672f76eaaf769ddaaf0ca0b31 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 10 Aug 2023 18:46:45 +0300 Subject: [PATCH 2/6] feat(clerk-js): Add Requests tabs in OrganizationMembers --- .../OrganizationMembers.tsx | 110 ++--------------- .../OrganizationMembersTabInvitations.tsx | 112 ++++++++++++++++++ .../OrganizationMembersTabRequests.tsx | 79 ++++++++++++ .../OrganizationProfile/RequestToJoinList.tsx | 95 +++++++++++++++ packages/localizations/src/en-US.ts | 14 +++ packages/types/src/localization.ts | 13 ++ 6 files changed, 325 insertions(+), 98 deletions(-) create mode 100644 packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembersTabInvitations.tsx create mode 100644 packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembersTabRequests.tsx create mode 100644 packages/clerk-js/src/ui/components/OrganizationProfile/RequestToJoinList.tsx diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembers.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembers.tsx index c5d563f54b8..a85dfcad7b3 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 00000000000..77813a09fe0 --- /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 00000000000..6704fed41ef --- /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 00000000000..7ec29e76c70 --- /dev/null +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/RequestToJoinList.tsx @@ -0,0 +1,95 @@ +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 { MembersListTable, 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} + 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 ( + <> +