From bff34e1e9f490d96e90b31d49c9af79a52d84d8e Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 19 Sep 2023 10:19:38 +0300 Subject: [PATCH 1/5] chore(shared): Deprecate `invitationList` from useOrganization --- packages/shared/src/hooks/useOrganization.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/shared/src/hooks/useOrganization.tsx b/packages/shared/src/hooks/useOrganization.tsx index a29d68ef95b..0fbdfc6d58a 100644 --- a/packages/shared/src/hooks/useOrganization.tsx +++ b/packages/shared/src/hooks/useOrganization.tsx @@ -21,6 +21,9 @@ import type { PaginatedResources, PaginatedResourcesWithDefault } from './types' import { usePagesOrInfinite, useWithSafeValues } from './usePagesOrInfinite'; type UseOrganizationParams = { + /** + * @deprecated Use invitations instead + */ invitationList?: GetPendingInvitationsParams; /** * @deprecated Use `memberships` instead @@ -44,6 +47,13 @@ type UseOrganizationParams = { infinite?: boolean; keepPreviousData?: boolean; }); + + invitations?: + | true + | (GetMembersParams & { + infinite?: boolean; + keepPreviousData?: boolean; + }); }; type UseOrganizationReturn = From 4135e30defaa0636cbca7cc8297f4be9bf954152 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 22 Sep 2023 11:14:59 +0300 Subject: [PATCH 2/5] feat(clerk-js): Introduce `Organization.getInvitations` and deprecate `getPendingInvitations` --- .changeset/three-carrots-remain.md | 7 ++++++ .../src/core/resources/Organization.ts | 25 +++++++++++++++++++ packages/types/src/organization.ts | 20 +++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 .changeset/three-carrots-remain.md diff --git a/.changeset/three-carrots-remain.md b/.changeset/three-carrots-remain.md new file mode 100644 index 00000000000..67235f527cf --- /dev/null +++ b/.changeset/three-carrots-remain.md @@ -0,0 +1,7 @@ +--- +'@clerk/clerk-js': patch +'@clerk/types': patch +--- + +Introduces a new method for fetching organization invitations called `Organization.getInvitations`. +Deprecate `Organization.getPendingInvitations` diff --git a/packages/clerk-js/src/core/resources/Organization.ts b/packages/clerk-js/src/core/resources/Organization.ts index 9368c71a77e..8af29aa6355 100644 --- a/packages/clerk-js/src/core/resources/Organization.ts +++ b/packages/clerk-js/src/core/resources/Organization.ts @@ -4,6 +4,7 @@ import type { ClerkResourceReloadParams, CreateOrganizationParams, GetDomainsParams, + GetInvitationsParams, GetMembershipRequestParams, GetMemberships, GetPendingInvitationsParams, @@ -12,6 +13,7 @@ import type { OrganizationDomainJSON, OrganizationDomainResource, OrganizationInvitationJSON, + OrganizationInvitationResource, OrganizationJSON, OrganizationMembershipJSON, OrganizationMembershipRequestJSON, @@ -205,6 +207,29 @@ export class Organization extends BaseResource implements OrganizationResource { .catch(() => []); }; + getInvitations = async ( + getInvitationsParams?: GetInvitationsParams, + ): Promise> => { + return await BaseResource._fetch({ + path: `/organizations/${this.id}/invitations`, + method: 'GET', + search: convertPageToOffset(getInvitationsParams) as any, + }) + .then(res => { + const { data: requests, total_count } = + res?.response as unknown as ClerkPaginatedResponse; + + return { + total_count, + data: requests.map(request => new OrganizationInvitation(request)), + }; + }) + .catch(() => ({ + total_count: 0, + data: [], + })); + }; + addMember = async ({ userId, role }: AddMemberParams) => { const newMember = await BaseResource._fetch({ method: 'POST', diff --git a/packages/types/src/organization.ts b/packages/types/src/organization.ts index a6d335a6784..9b42c86a3a6 100644 --- a/packages/types/src/organization.ts +++ b/packages/types/src/organization.ts @@ -44,7 +44,11 @@ export interface OrganizationResource extends ClerkResource { updatedAt: Date; update: (params: UpdateOrganizationParams) => Promise; getMemberships: GetMemberships; + /** + * @deprecated Use `getInvitations` instead + */ getPendingInvitations: (params?: GetPendingInvitationsParams) => Promise; + getInvitations: (params?: GetInvitationsParams) => Promise>; getDomains: (params?: GetDomainsParams) => Promise>; getMembershipRequests: ( params?: GetMembershipRequestParams, @@ -80,6 +84,9 @@ export type GetMembersParams = { role?: MembershipRole[]; }; +/** + * @deprecated use `getInvitations` instead + */ export type GetPendingInvitationsParams = ClerkPaginationParams; export type GetDomainsParams = { /** @@ -94,6 +101,19 @@ export type GetDomainsParams = { enrollmentMode?: OrganizationEnrollmentMode; }; +export type GetInvitationsParams = { + /** + * This is the starting point for your fetched results. The initial value persists between re-renders + */ + initialPage?: number; + /** + * Maximum number of items returned per request. The initial value persists between re-renders + */ + pageSize?: number; + + status?: OrganizationInvitationStatus[]; +}; + export type GetMembershipRequestParams = { /** * This is the starting point for your fetched results. The initial value persists between re-renders From 40c4b44fbe055e8b0a192087c471b35b76e74866 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 22 Sep 2023 11:16:05 +0300 Subject: [PATCH 3/5] feat(shared): Introduce `invitations` and deprecate `invitationList` from useOrganization --- .changeset/fast-books-kiss.md | 6 ++ .../InvitedMembersList.tsx | 35 +++++------- packages/shared/src/hooks/useOrganization.tsx | 56 ++++++++++++++++++- 3 files changed, 73 insertions(+), 24 deletions(-) create mode 100644 .changeset/fast-books-kiss.md diff --git a/.changeset/fast-books-kiss.md b/.changeset/fast-books-kiss.md new file mode 100644 index 00000000000..133fb2133f4 --- /dev/null +++ b/.changeset/fast-books-kiss.md @@ -0,0 +1,6 @@ +--- +'@clerk/shared': patch +--- + +Introduce `invitations` in useOrganization, which enables to fetch invitations as paginated lists. +Deprecate `invitationList` in favor of the above introduction. diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/InvitedMembersList.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/InvitedMembersList.tsx index 17e96f12bf8..7214def4bad 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/InvitedMembersList.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/InvitedMembersList.tsx @@ -2,45 +2,36 @@ import type { OrganizationInvitationResource } from '@clerk/types'; import { useCoreOrganization } from '../../contexts'; import { localizationKeys, Td, Text } from '../../customizables'; -import { ThreeDotsMenu, useCardState, usePagination, UserPreview } from '../../elements'; +import { ThreeDotsMenu, useCardState, UserPreview } from '../../elements'; import { handleError, roleLocalizationKey } from '../../utils'; import { DataTable, RowContainer } from './MemberListTable'; -const ITEMS_PER_PAGE = 10; - export const InvitedMembersList = () => { const card = useCardState(); - const { page, changePage } = usePagination(); - const { organization, invitationList, ...rest } = useCoreOrganization({ - invitationList: { offset: (page - 1) * ITEMS_PER_PAGE, limit: ITEMS_PER_PAGE }, + const { organization, invitations } = useCoreOrganization({ + invitations: true, }); - const mutateSwrState = () => { - const unstable__mutate = (rest as any).unstable__mutate; - if (unstable__mutate && typeof unstable__mutate === 'function') { - unstable__mutate(); - } - }; - if (!organization) { return null; } const revoke = (invitation: OrganizationInvitationResource) => () => { return card - .runAsync(invitation.revoke) - .then(mutateSwrState) - .then(() => changePage(1)) + .runAsync(async () => { + await invitation.revoke(); + await (invitations as any).unstable__mutate?.(); + }) .catch(err => handleError(err, [], card.setError)); }; return ( null)} + itemCount={invitations?.count || 0} + pageCount={invitations?.pageCount || 0} + isLoading={invitations?.isLoading} emptyStateLocalizationKey={localizationKeys('organizationProfile.membersPage.invitationsTab.table__emptyRow')} headers={[ localizationKeys('organizationProfile.membersPage.activeMembersTab.tableHeader__user'), @@ -48,7 +39,7 @@ export const InvitedMembersList = () => { localizationKeys('organizationProfile.membersPage.activeMembersTab.tableHeader__role'), localizationKeys('organizationProfile.membersPage.activeMembersTab.tableHeader__actions'), ]} - rows={(invitationList || []).map(i => ( + rows={(invitations?.data || []).map(i => ( ; membershipRequests: PaginatedResourcesWithDefault; memberships: PaginatedResourcesWithDefault; + invitations: PaginatedResourcesWithDefault; } | { isLoaded: true; organization: OrganizationResource; + /** + * @deprecated Use `invitations` instead + */ invitationList: undefined; /** * @deprecated Use `memberships` instead @@ -82,10 +91,14 @@ type UseOrganizationReturn = domains: PaginatedResourcesWithDefault; membershipRequests: PaginatedResourcesWithDefault; memberships: PaginatedResourcesWithDefault; + invitations: PaginatedResourcesWithDefault; } | { isLoaded: boolean; organization: OrganizationResource | null; + /** + * @deprecated Use `invitations` instead + */ invitationList: OrganizationInvitationResource[] | null | undefined; /** * @deprecated Use `memberships` instead @@ -95,6 +108,7 @@ type UseOrganizationReturn = domains: PaginatedResources | null; membershipRequests: PaginatedResources | null; memberships: PaginatedResources | null; + invitations: PaginatedResources | null; }; type UseOrganization = (params?: UseOrganizationParams) => UseOrganizationReturn; @@ -121,6 +135,7 @@ export const useOrganization: UseOrganization = params => { domains: domainListParams, membershipRequests: membershipRequestsListParams, memberships: membersListParams, + invitations: invitationsListParams, } = params || {}; const { organization, lastOrganizationMember, lastOrganizationInvitation } = useOrganizationContext(); const session = useSessionContext(); @@ -149,6 +164,14 @@ export const useOrganization: UseOrganization = params => { infinite: false, }); + const invitationsSafeValues = useWithSafeValues(invitationsListParams, { + initialPage: 1, + pageSize: 10, + status: ['pending'], + keepPreviousData: false, + infinite: false, + }); + const clerk = useClerkInstanceContext(); const shouldFetch = !!(clerk.loaded && session && organization); @@ -180,6 +203,15 @@ export const useOrganization: UseOrganization = params => { role: membersSafeValues.role, }; + const invitationsParams = + typeof invitationsListParams === 'undefined' + ? undefined + : { + initialPage: invitationsSafeValues.initialPage, + pageSize: invitationsSafeValues.pageSize, + status: invitationsSafeValues.status, + }; + const domains = usePagesOrInfinite>( { ...domainParams, @@ -232,6 +264,22 @@ export const useOrganization: UseOrganization = params => { }, ); + const invitations = usePagesOrInfinite>( + { + ...invitationsParams, + }, + organization?.getInvitations, + { + keepPreviousData: membersSafeValues.keepPreviousData, + infinite: membersSafeValues.infinite, + enabled: !!invitationsParams, + }, + { + type: 'invitations', + organizationId: organization?.id, + }, + ); + // Some gymnastics to adhere to the rules of hooks // We need to make sure useSWR is called on every render const pendingInvitations = !clerk.loaded @@ -274,6 +322,7 @@ export const useOrganization: UseOrganization = params => { domains: undefinedPaginatedResource, membershipRequests: undefinedPaginatedResource, memberships: undefinedPaginatedResource, + invitations: undefinedPaginatedResource, }; } @@ -287,6 +336,7 @@ export const useOrganization: UseOrganization = params => { domains: null, membershipRequests: null, memberships: null, + invitations: null, }; } @@ -301,6 +351,7 @@ export const useOrganization: UseOrganization = params => { domains: undefinedPaginatedResource, membershipRequests: undefinedPaginatedResource, memberships: undefinedPaginatedResource, + invitations: undefinedPaginatedResource, }; } @@ -317,6 +368,7 @@ export const useOrganization: UseOrganization = params => { domains, membershipRequests, memberships, + invitations, }; }; From 1040fa08ee473958e78ab94e20341d066d369e02 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 22 Sep 2023 11:17:22 +0300 Subject: [PATCH 4/5] test(clerk-js): Use `getInvitations` instead of getPendingInvitations --- .../__snapshots__/Organization.test.ts.snap | 1 + .../OrganizationMembership.test.ts.snap | 1 + .../__tests__/OrganizationMembers.test.tsx | 21 ++++++++++++------- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/clerk-js/src/core/resources/__snapshots__/Organization.test.ts.snap b/packages/clerk-js/src/core/resources/__snapshots__/Organization.test.ts.snap index 522b944a0cf..184db5902e2 100644 --- a/packages/clerk-js/src/core/resources/__snapshots__/Organization.test.ts.snap +++ b/packages/clerk-js/src/core/resources/__snapshots__/Organization.test.ts.snap @@ -9,6 +9,7 @@ Organization { "destroy": [Function], "getDomain": [Function], "getDomains": [Function], + "getInvitations": [Function], "getMembershipRequests": [Function], "getMemberships": [Function], "getPendingInvitations": [Function], diff --git a/packages/clerk-js/src/core/resources/__snapshots__/OrganizationMembership.test.ts.snap b/packages/clerk-js/src/core/resources/__snapshots__/OrganizationMembership.test.ts.snap index a572c4e82f4..a3bbcee34d5 100644 --- a/packages/clerk-js/src/core/resources/__snapshots__/OrganizationMembership.test.ts.snap +++ b/packages/clerk-js/src/core/resources/__snapshots__/OrganizationMembership.test.ts.snap @@ -13,6 +13,7 @@ OrganizationMembership { "destroy": [Function], "getDomain": [Function], "getDomains": [Function], + "getInvitations": [Function], "getMembershipRequests": [Function], "getMemberships": [Function], "getPendingInvitations": [Function], diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx index f36b32128d2..384e418ad2f 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx @@ -147,7 +147,7 @@ describe('OrganizationMembers', () => { await waitFor(() => { expect(fixtures.clerk.organization?.getMemberships).toHaveBeenCalled(); - expect(fixtures.clerk.organization?.getPendingInvitations).not.toHaveBeenCalled(); + expect(fixtures.clerk.organization?.getInvitations).not.toHaveBeenCalled(); expect(fixtures.clerk.organization?.getMembershipRequests).not.toHaveBeenCalled(); expect(queryByText('test_user1')).toBeInTheDocument(); expect(queryByText('First1 Last1')).toBeInTheDocument(); @@ -258,14 +258,19 @@ describe('OrganizationMembers', () => { }); }); - fixtures.clerk.organization?.getPendingInvitations.mockReturnValue(Promise.resolve(invitationList)); + fixtures.clerk.organization?.getInvitations.mockReturnValue( + Promise.resolve({ + data: invitationList, + total_count: 2, + }), + ); const { queryByText, getByRole } = render(, { wrapper }); await userEvent.click(getByRole('tab', { name: 'Invitations' })); - expect(fixtures.clerk.organization?.getPendingInvitations).toHaveBeenCalled(); - expect(queryByText('admin1@clerk.dev')).toBeDefined(); - expect(queryByText('Admin')).toBeDefined(); - expect(queryByText('member2@clerk.dev')).toBeDefined(); - expect(queryByText('Member')).toBeDefined(); + expect(fixtures.clerk.organization?.getInvitations).toHaveBeenCalled(); + expect(queryByText('admin1@clerk.dev')).toBeInTheDocument(); + expect(queryByText('Admin')).toBeInTheDocument(); + expect(queryByText('member2@clerk.dev')).toBeInTheDocument(); + expect(queryByText('Member')).toBeInTheDocument(); }); it('changes tab and renders pending requests', async () => { @@ -342,6 +347,6 @@ describe('OrganizationMembers', () => { ); const { findByText } = render(, { wrapper }); await waitFor(() => expect(fixtures.clerk.organization?.getMemberships).toHaveBeenCalled()); - expect(await findByText('You')).toBeDefined(); + expect(await findByText('You')).toBeInTheDocument(); }); }); From 27448be2117734d609eaabc87ae447f1c62e0e93 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 22 Sep 2023 13:39:48 +0300 Subject: [PATCH 5/5] chore(shared): Use deprecate helper for useOrganization --- packages/clerk-js/src/core/resources/Organization.ts | 2 ++ packages/shared/src/hooks/useOrganization.tsx | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/packages/clerk-js/src/core/resources/Organization.ts b/packages/clerk-js/src/core/resources/Organization.ts index 8af29aa6355..c60077d46f9 100644 --- a/packages/clerk-js/src/core/resources/Organization.ts +++ b/packages/clerk-js/src/core/resources/Organization.ts @@ -1,3 +1,4 @@ +import { deprecated } from '@clerk/shared'; import type { AddMemberParams, ClerkPaginatedResponse, @@ -195,6 +196,7 @@ export class Organization extends BaseResource implements OrganizationResource { getPendingInvitations = async ( getPendingInvitationsParams?: GetPendingInvitationsParams, ): Promise => { + deprecated('getPendingInvitations', 'Use the `getInvitations` method instead.'); return await BaseResource._fetch({ path: `/organizations/${this.id}/invitations/pending`, method: 'GET', diff --git a/packages/shared/src/hooks/useOrganization.tsx b/packages/shared/src/hooks/useOrganization.tsx index 15e75b5a8eb..d8354846813 100644 --- a/packages/shared/src/hooks/useOrganization.tsx +++ b/packages/shared/src/hooks/useOrganization.tsx @@ -21,6 +21,7 @@ import { useSWR } from './clerk-swr'; import { useClerkInstanceContext, useOrganizationContext, useSessionContext } from './contexts'; import type { PaginatedResources, PaginatedResourcesWithDefault } from './types'; import { usePagesOrInfinite, useWithSafeValues } from './usePagesOrInfinite'; +import { deprecated } from '../utils'; type UseOrganizationParams = { /** @@ -290,6 +291,10 @@ export const useOrganization: UseOrganization = params => { ? () => [] as OrganizationMembershipResource[] : () => clerk.organization?.getMemberships(membershipListParams); + if (invitationListParams) { + deprecated('invitationList in useOrganization', 'Use the `invitations` property and return value instead.'); + } + const { data: invitationList, isValidating: isInvitationsLoading, @@ -301,6 +306,10 @@ export const useOrganization: UseOrganization = params => { pendingInvitations, ); + if (membershipListParams) { + deprecated('membershipList in useOrganization', 'Use the `memberships` property and return value instead.'); + } + const { data: membershipList, isValidating: isMembershipsLoading,