From 8d1e7d76de40c0ecb367c6745094dd0a75f764b3 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 10 Aug 2023 20:41:31 +0300 Subject: [PATCH] feat(clerk-js,types,localizations): Introduces Membership Requests in (#1576) * feat(clerk-js): Fetch domains by enrollmentMode * feat(clerk-js): Add Requests tabs in OrganizationMembers * test(clerk-js): Update snapshot of OrganizationMembershipRequest * chore(clerk-js): Rename MemberListTable to DataTable and make it more generic * chore(clerk-js): Add changeset * chore(clerk-js): Increase OrganizationProfile chunk by 0.5KB --- .changeset/giant-timers-film.md | 10 ++ packages/clerk-js/bundlewatch.config.json | 2 +- .../OrganizationMembershipRequest.ts | 3 + ...OrganizationMembershipRequest.test.ts.snap | 2 + .../OrganizationProfile/ActiveMembersList.tsx | 5 +- .../InvitedMembersList.tsx | 5 +- .../OrganizationProfile/MemberListTable.tsx | 29 ++--- .../OrganizationMembers.tsx | 110 ++--------------- .../OrganizationMembersTabInvitations.tsx | 112 ++++++++++++++++++ .../OrganizationMembersTabRequests.tsx | 79 ++++++++++++ .../OrganizationProfile/RequestToJoinList.tsx | 96 +++++++++++++++ packages/clerk-js/src/utils/pagesToOffset.ts | 11 +- packages/localizations/src/en-US.ts | 16 +++ packages/shared/src/hooks/useOrganization.tsx | 3 + packages/types/src/localization.ts | 15 +++ packages/types/src/organization.ts | 4 +- packages/types/src/organizationDomain.ts | 2 +- 17 files changed, 378 insertions(+), 126 deletions(-) create mode 100644 .changeset/giant-timers-film.md 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/.changeset/giant-timers-film.md b/.changeset/giant-timers-film.md new file mode 100644 index 0000000000..c670841716 --- /dev/null +++ b/.changeset/giant-timers-film.md @@ -0,0 +1,10 @@ +--- +'@clerk/localizations': patch +'@clerk/clerk-js': patch +'@clerk/shared': patch +'@clerk/types': patch +--- + +Introduces Membership Requests in + +- This is a list of users that have requested to join the active organization diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 0a7bcb82c5..0d05b1ca70 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -6,7 +6,7 @@ { "path": "./dist/vendors*.js", "maxSize": "70KB" }, { "path": "./dist/createorganization*.js", "maxSize": "5KB" }, { "path": "./dist/impersonationfab*.js", "maxSize": "5KB" }, - { "path": "./dist/organizationprofile*.js", "maxSize": "8KB" }, + { "path": "./dist/organizationprofile*.js", "maxSize": "8.5KB" }, { "path": "./dist/organizationswitcher*.js", "maxSize": "5KB" }, { "path": "./dist/signin*.js", "maxSize": "10KB" }, { "path": "./dist/signup*.js", "maxSize": "10KB" }, diff --git a/packages/clerk-js/src/core/resources/OrganizationMembershipRequest.ts b/packages/clerk-js/src/core/resources/OrganizationMembershipRequest.ts index c38b5cd1a3..1beb70211e 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/core/resources/__snapshots__/OrganizationMembershipRequest.test.ts.snap b/packages/clerk-js/src/core/resources/__snapshots__/OrganizationMembershipRequest.test.ts.snap index a3ec337996..b1d4f1c218 100644 --- a/packages/clerk-js/src/core/resources/__snapshots__/OrganizationMembershipRequest.test.ts.snap +++ b/packages/clerk-js/src/core/resources/__snapshots__/OrganizationMembershipRequest.test.ts.snap @@ -3,6 +3,7 @@ exports[`OrganizationMembership has the same initial properties 1`] = ` OrganizationMembershipRequest { "accept": [Function], + "createdAt": 1970-01-01T00:00:12.345Z, "id": "test_id", "organizationId": "test_org_id", "pathRoot": "", @@ -16,5 +17,6 @@ OrganizationMembershipRequest { "userId": undefined, }, "status": "pending", + "updatedAt": 1970-01-01T00:00:05.678Z, } `; diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/ActiveMembersList.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/ActiveMembersList.tsx index 94634f2e7a..d2c0957d69 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/ActiveMembersList.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/ActiveMembersList.tsx @@ -4,7 +4,7 @@ import { useCoreOrganization, useCoreUser } from '../../contexts'; import { Badge, localizationKeys, Td, Text } from '../../customizables'; import { ThreeDotsMenu, useCardState, usePagination, UserPreview } from '../../elements'; import { handleError, roleLocalizationKey } from '../../utils'; -import { MembersListTable, RoleSelect, RowContainer } from './MemberListTable'; +import { DataTable, RoleSelect, RowContainer } from './MemberListTable'; const ITEMS_PER_PAGE = 10; @@ -53,12 +53,13 @@ export const ActiveMembersList = () => { }; return ( - { }; return ( - void; itemCount: number; itemsPerPage: number; + emptyStateLocalizationKey: LocalizationKey; }; -export const MembersListTable = (props: MembersListTableProps) => { - const { headers, page, onPageChange, rows, isLoading, itemCount, itemsPerPage } = props; +export const DataTable = (props: MembersListTableProps) => { + const { headers, page, onPageChange, rows, isLoading, itemCount, itemsPerPage, emptyStateLocalizationKey } = props; const pageCount = rows.length !== 0 ? Math.ceil(itemCount / itemsPerPage) : 1; const startRowIndex = (page - 1) * rows.length; @@ -62,7 +50,10 @@ export const MembersListTable = (props: MembersListTableProps) => { ) : !rows.length ? ( - + ) : ( rows )} @@ -86,12 +77,12 @@ export const MembersListTable = (props: MembersListTableProps) => { ); }; -const EmptyRow = () => { +const EmptyRow = (props: { localizationKey: LocalizationKey }) => { return ( ({ 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 ( + <> +